Commit d08930fa authored by Luke Zielinski's avatar Luke Zielinski Committed by Commit Bot

Add ability to fetch try job results directly from buildbucket.

This CL is mostly a no-op because the new method is not called during regular workflow.
A follow-up CL will switch to calling buildbucket instead of git-cl.

There is further work to refactor this into its own class (outside of git-cl.py) and
potentially add more functionality to also start try jobs.

Main change is to generate a LUCI auth token earlier in the flow,
but this should be a no-op as well.

Bug: 1005801
Change-Id: I2353b3ad0b24d86a5c55da93dd5c1c1eec280f19
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1831979
Commit-Queue: Luke Z <lpz@chromium.org>
Reviewed-by: default avatarRobert Ma <robertma@chromium.org>
Cr-Commit-Position: refs/heads/master@{#702135}
parent 274517fd
...@@ -13,8 +13,10 @@ import json ...@@ -13,8 +13,10 @@ import json
import logging import logging
import re import re
from blinkpy.common.net.buildbot import Build, filter_latest_builds
from blinkpy.common.checkout.git import Git from blinkpy.common.checkout.git import Git
from blinkpy.common.net.buildbot import Build, filter_latest_builds
from blinkpy.common.net.luci_auth import LuciAuth
_log = logging.getLogger(__name__) _log = logging.getLogger(__name__)
...@@ -22,6 +24,10 @@ _log = logging.getLogger(__name__) ...@@ -22,6 +24,10 @@ _log = logging.getLogger(__name__)
# in order to authenticate with buildbucket. # in order to authenticate with buildbucket.
_COMMANDS_THAT_TAKE_REFRESH_TOKEN = ('try',) _COMMANDS_THAT_TAKE_REFRESH_TOKEN = ('try',)
# These characters always appear at the beginning of the SearchBuilds response
# from BuildBucket.
SEARCHBUILDS_RESPONSE_PREFIX = ")]}'"
class CLStatus(collections.namedtuple('CLStatus', ('status', 'try_job_results'))): class CLStatus(collections.namedtuple('CLStatus', ('status', 'try_job_results'))):
"""Represents the current status of a particular CL. """Represents the current status of a particular CL.
...@@ -43,6 +49,17 @@ class TryJobStatus(collections.namedtuple('TryJobStatus', ('status', 'result'))) ...@@ -43,6 +49,17 @@ class TryJobStatus(collections.namedtuple('TryJobStatus', ('status', 'result')))
assert result in (None, 'FAILURE', 'SUCCESS', 'CANCELED') assert result in (None, 'FAILURE', 'SUCCESS', 'CANCELED')
return super(TryJobStatus, cls).__new__(cls, status, result) return super(TryJobStatus, cls).__new__(cls, status, result)
@staticmethod
def from_bb_status(bb_status):
assert bb_status in ('SCHEDULED', 'STARTED', 'SUCCESS', 'FAILURE', 'INFRA_FAILURE', 'CANCELLED')
if bb_status in ('SCHEDULED', 'STARTED'):
return TryJobStatus(bb_status, None)
else:
# Map result INFRA_FAILURE to FAILURE to avoid introducing a new
# result, and it amounts to the same thing anyway.
return TryJobStatus('COMPLETED',
'FAILURE' if bb_status == 'INFRA_FAILURE' else bb_status)
class GitCL(object): class GitCL(object):
...@@ -112,6 +129,9 @@ class GitCL(object): ...@@ -112,6 +129,9 @@ class GitCL(object):
def _get_cl_status(self): def _get_cl_status(self):
return self.run(['status', '--field=status']).strip() return self.run(['status', '--field=status']).strip()
def _get_latest_patchset(self):
return self.run(['status', '--field=patch']).strip()
def wait_for_try_jobs( def wait_for_try_jobs(
self, poll_delay_seconds=10 * 60, timeout_seconds=120 * 60, self, poll_delay_seconds=10 * 60, timeout_seconds=120 * 60,
cq_only=False): cq_only=False):
...@@ -206,6 +226,30 @@ class GitCL(object): ...@@ -206,6 +226,30 @@ class GitCL(object):
builder_names, include_swarming_tasks=False, cq_only=cq_only, builder_names, include_swarming_tasks=False, cq_only=cq_only,
patchset=patchset)) patchset=patchset))
def latest_try_jobs_from_bb(
self, issue_number, builder_names=None, cq_only=False, patchset=None):
"""Fetches a dict of Build to TryJobStatus for the latest try jobs.
This variant fetches try job data from buildbucket directly.
This includes jobs that are not yet finished and builds with infra
failures, so if a build is in this list, that doesn't guarantee that
there are results.
Args:
issue_number: The git cl/issue number we're working with.
builder_names: Optional list of builders used to filter results.
cq_only: If True, only include CQ jobs.
patchset: If given, use this patchset instead of the latest.
Returns:
A dict mapping Build objects to TryJobStatus objects, with
only the latest jobs included.
"""
return self.filter_latest(
self.try_job_results_from_bb(
issue_number, builder_names, cq_only=cq_only, patchset=patchset))
@staticmethod @staticmethod
def filter_latest(try_results): def filter_latest(try_results):
"""Returns the latest entries from from a Build to TryJobStatus dict.""" """Returns the latest entries from from a Build to TryJobStatus dict."""
...@@ -233,6 +277,24 @@ class GitCL(object): ...@@ -233,6 +277,24 @@ class GitCL(object):
build_to_status[self._build(result)] = self._try_job_status(result) build_to_status[self._build(result)] = self._try_job_status(result)
return build_to_status return build_to_status
def try_job_results_from_bb(
self, issue_number, builder_names=None, cq_only=False, patchset=None):
"""Returns a dict mapping Build objects to TryJobStatus objects."""
raw_results_json = self.fetch_raw_try_job_results_from_bb(issue_number, patchset)
build_to_status = {}
for build in raw_results_json['builds']:
builder_name = build['builder']['builder']
if builder_names and builder_name not in builder_names:
continue
is_cq = 'tags' in build and {'key': 'user_agent', 'value': 'cq'} in build['tags']
is_experimental = 'tags' in build and {'key': 'cq_experimental', 'value': 'true'} in build['tags']
if cq_only and not (is_cq and not is_experimental):
continue
build_number = build['number']
status = build['status']
build_to_status[Build(builder_name, build_number)] = TryJobStatus.from_bb_status(status)
return build_to_status
def fetch_raw_try_job_results(self, patchset=None): def fetch_raw_try_job_results(self, patchset=None):
"""Requests results of try jobs for the current CL and the parsed JSON. """Requests results of try jobs for the current CL and the parsed JSON.
...@@ -251,6 +313,71 @@ class GitCL(object): ...@@ -251,6 +313,71 @@ class GitCL(object):
self._host.filesystem.remove(results_path) self._host.filesystem.remove(results_path)
return json.loads(contents) return json.loads(contents)
def fetch_raw_try_job_results_from_bb(self, issue_number, patchset=None):
"""Gets try job results for the specified CL from buildbucket.
This uses the SearchBuilds rpc format specified in
https://cs.chromium.org/chromium/infra/go/src/go.chromium.org/luci/buildbucket/proto/rpc.proto
The response is a list of dicts of the following form:
{
"builds": [
{
"status": <status>
"builder": {
"builder": <builder_name>
},
"number": <build_number>,
"tags": [
{
"key": <tag key>
"value": <tag value>
},
... more tags
]
},
... more builds
}
This method returns the JSON representation of the above response.
"""
if not patchset:
patchset = self._get_latest_patchset()
luci_token = LuciAuth(self._host).get_access_token()
hed = {
'Authorization': 'Bearer ' + luci_token,
'Accept': 'application/json',
'Content-Type': 'application/json',
}
data = {
'predicate': {
'gerritChanges': [
{
'host': 'chromium-review.googlesource.com',
'project': 'chromium/src',
'change': issue_number,
'patchset': patchset
}
]
},
'fields': 'builds.*.builder.builder,builds.*.status,builds.*.tags,builds.*.number'
}
url = 'https://cr-buildbucket.appspot.com/prpc/buildbucket.v2.Builds/SearchBuilds'
req_body = json.dumps(data)
_log.debug("Sending SearchBuilds request. Url: %s with Body: %s" % (url, req_body))
response = self._host.web.request('POST', url, data=req_body, headers=hed)
if response.getcode() == 200:
response_body = response.read()
if response_body.startswith(SEARCHBUILDS_RESPONSE_PREFIX):
response_body = response_body[len(SEARCHBUILDS_RESPONSE_PREFIX):]
return json.loads(response_body)
_log.error("Failed to fetch tryjob results from buildbucket (status=%s)" % response.status)
_log.debug("Full SearchBuilds response: %s" % str(response))
return None
@staticmethod @staticmethod
def _build(result_dict): def _build(result_dict):
"""Converts a parsed try result dict to a Build object.""" """Converts a parsed try result dict to a Build object."""
......
...@@ -8,7 +8,9 @@ from blinkpy.common.host_mock import MockHost ...@@ -8,7 +8,9 @@ from blinkpy.common.host_mock import MockHost
from blinkpy.common.net.buildbot import Build from blinkpy.common.net.buildbot import Build
from blinkpy.common.net.git_cl import CLStatus from blinkpy.common.net.git_cl import CLStatus
from blinkpy.common.net.git_cl import GitCL from blinkpy.common.net.git_cl import GitCL
from blinkpy.common.net.git_cl import SEARCHBUILDS_RESPONSE_PREFIX
from blinkpy.common.net.git_cl import TryJobStatus from blinkpy.common.net.git_cl import TryJobStatus
from blinkpy.common.net.web_mock import MockWeb
from blinkpy.common.system.executive_mock import MockExecutive from blinkpy.common.system.executive_mock import MockExecutive
...@@ -548,3 +550,101 @@ class GitCLTest(unittest.TestCase): ...@@ -548,3 +550,101 @@ class GitCLTest(unittest.TestCase):
# so if we only care about other-builder, not builder-a, # so if we only care about other-builder, not builder-a,
# then no exception is raised. # then no exception is raised.
self.assertEqual(git_cl.try_job_results(['other-builder']), {}) self.assertEqual(git_cl.try_job_results(['other-builder']), {})
def test_try_job_results_from_bb(self):
git_cl = GitCL(MockHost())
git_cl._host.web = MockWeb(responses=[
{
'status_code': 200,
'body': SEARCHBUILDS_RESPONSE_PREFIX +
"""{
"builds": [
{
"status": "SUCCESS",
"builder": {
"builder": "builder-a"
},
"number": 111,
"tags": [
{
"key": "user_agent",
"value": "cq"
}
]
},
{
"status": "SCHEDULED",
"builder": {
"builder": "builder-b"
},
"number": 222
},
{
"status": "INFRA_FAILURE",
"builder": {
"builder": "builder-c"
},
"number": 333
}
]
}"""
}
])
self.assertEqual(
git_cl.try_job_results_from_bb(issue_number=None),
{
Build('builder-a', 111): TryJobStatus('COMPLETED', 'SUCCESS'),
Build('builder-b', 222): TryJobStatus('SCHEDULED', None),
# INFRA_FAILURE is mapped to FAILURE for this build.
Build('builder-c', 333): TryJobStatus('COMPLETED', 'FAILURE'),
})
def test_try_job_results_from_bb_skip_experimental_cq(self):
git_cl = GitCL(MockHost())
git_cl._host.web = MockWeb(responses=[
{
'status_code': 200,
'body': SEARCHBUILDS_RESPONSE_PREFIX +
"""{
"builds": [
{
"status": "SUCCESS",
"builder": {
"builder": "builder-a"
},
"number": 111,
"tags": [
{
"key": "user_agent",
"value": "cq"
}
]
},
{
"status": "SUCCESS",
"builder": {
"builder": "builder-b"
},
"number": 222,
"tags": [
{
"key": "user_agent",
"value": "cq"
},
{
"key": "cq_experimental",
"value": "true"
}
]
}
]
}"""
}
])
self.assertEqual(
# Only one build appears - builder-b is ignored because it is
# experimental.
git_cl.try_job_results_from_bb(issue_number=None, cq_only=True),
{
Build('builder-a', 111): TryJobStatus('COMPLETED', 'SUCCESS'),
})
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment