123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399 |
- # Copyright 2014 The Chromium Authors. All rights reserved.
- # Use of this source code is governed by a BSD-style license that can be
- # found in the LICENSE file.
- import contextlib
- import hashlib
- from recipe_engine import recipe_api
- class Constants:
- def __init__(self):
- self.NONTRIVIAL_ROLL_FOOTER = 'Recipe-Nontrivial-Roll'
- self.MANUAL_CHANGE_FOOTER = 'Recipe-Manual-Change'
- self.BYPASS_FOOTER = 'Recipe-Tryjob-Bypass-Reason'
- self.SKIP_RETRY_FOOTER = 'Disable-Retries'
- self.CQ_DEPEND_FOOTER = 'Cq-Depend'
- self.ALL_VALID_FOOTERS = set([
- self.NONTRIVIAL_ROLL_FOOTER, self.MANUAL_CHANGE_FOOTER,
- self.BYPASS_FOOTER, self.SKIP_RETRY_FOOTER, self.CQ_DEPEND_FOOTER
- ])
- constants = Constants()
- class TryserverApi(recipe_api.RecipeApi):
- def __init__(self, *args, **kwargs):
- super(TryserverApi, self).__init__(*args, **kwargs)
- self._gerrit_change = None # self.m.buildbucket.common_pb2.GerritChange
- self._gerrit_change_repo_url = None
- self._gerrit_change_repo_host = None
- self._gerrit_change_repo_project = None
- self._gerrit_info_initialized = False
- self._gerrit_change_target_ref = None
- self._gerrit_change_fetch_ref = None
- self._gerrit_change_owner = None
- self._change_footers = None
- self._gerrit_commit_message = None
- def initialize(self):
- changes = self.m.buildbucket.build.input.gerrit_changes
- if len(changes) == 1:
- self.set_change(changes[0])
- @property
- def valid_footers(self): #pragma: nocover
- return constants.ALL_VALID_FOOTERS
- @property
- def constants(self): #pragma: nocover
- # Nocover to be removed when callers (not within depot_tools) exercise this
- return constants
- @property
- def gerrit_change(self):
- """Returns current gerrit change, if there is exactly one.
- Returns a self.m.buildbucket.common_pb2.GerritChange or None.
- """
- return self._gerrit_change
- @property
- def gerrit_change_repo_url(self):
- """Returns canonical URL of the gitiles repo of the current Gerrit CL.
- Populated iff gerrit_change is populated.
- """
- return self._gerrit_change_repo_url
- @property
- def gerrit_change_repo_host(self):
- """Returns the host of the gitiles repo of the current Gerrit CL.
- Populated iff gerrit_change is populated.
- """
- return self._gerrit_change_repo_host
- @property
- def gerrit_change_repo_project(self):
- """Returns the project of the gitiles repo of the current Gerrit CL.
- Populated iff gerrit_change is populated.
- """
- return self._gerrit_change_repo_project
- @property
- def gerrit_change_owner(self):
- """Returns owner of the current Gerrit CL.
- Populated iff gerrit_change is populated.
- Is a dictionary with keys like "name".
- """
- self._ensure_gerrit_change_info()
- return self._gerrit_change_owner
- @property
- def gerrit_change_review_url(self):
- """Returns the review URL for the active patchset."""
- # Gerrit redirects to insert the project into the URL.
- gerrit_change = self._gerrit_change
- return 'https://%s/c/%s/%s' % (
- gerrit_change.host, gerrit_change.change, gerrit_change.patchset)
- def _ensure_gerrit_change_info(self):
- """Initializes extra info about gerrit_change, fetched from Gerrit server.
- Initializes _gerrit_change_target_ref and _gerrit_change_fetch_ref.
- May emit a step when called for the first time.
- """
- cl = self.gerrit_change
- if not cl: # pragma: no cover
- return
- if self._gerrit_info_initialized:
- return
- td = self._test_data if self._test_data.enabled else {}
- mock_res = [{
- 'branch': td.get('gerrit_change_target_ref', 'main'),
- 'revisions': {
- '184ebe53805e102605d11f6b143486d15c23a09c': {
- '_number': str(cl.patchset),
- 'ref': 'refs/changes/%02d/%d/%d' % (
- cl.change % 100, cl.change, cl.patchset),
- },
- },
- 'owner': {
- 'name': 'John Doe',
- },
- }]
- res = self.m.gerrit.get_changes(
- host='https://' + cl.host,
- query_params=[('change', cl.change)],
- # This list must remain static/hardcoded.
- # If you need extra info, either change it here (hardcoded) or
- # fetch separately.
- o_params=['ALL_REVISIONS', 'DOWNLOAD_COMMANDS'],
- limit=1,
- name='fetch current CL info',
- timeout=360,
- step_test_data=lambda: self.m.json.test_api.output(mock_res))[0]
- self._gerrit_change_target_ref = res['branch']
- if not self._gerrit_change_target_ref.startswith('refs/'):
- self._gerrit_change_target_ref = (
- 'refs/heads/' + self._gerrit_change_target_ref)
- for rev in res['revisions'].values():
- if int(rev['_number']) == self.gerrit_change.patchset:
- self._gerrit_change_fetch_ref = rev['ref']
- break
- self._gerrit_change_owner = res['owner']
- self._gerrit_info_initialized = True
- @property
- def gerrit_change_fetch_ref(self):
- """Returns gerrit patch ref, e.g. "refs/heads/45/12345/6, or None.
- Populated iff gerrit_change is populated.
- """
- self._ensure_gerrit_change_info()
- return self._gerrit_change_fetch_ref
- @property
- def gerrit_change_target_ref(self):
- """Returns gerrit change destination ref, e.g. "refs/heads/main".
- Populated iff gerrit_change is populated.
- """
- self._ensure_gerrit_change_info()
- return self._gerrit_change_target_ref
- @property
- def gerrit_change_number(self):
- """Returns gerrit change patchset, e.g. 12345 for a patch ref of
- "refs/heads/45/12345/6".
- Populated iff gerrit_change is populated. Returns None if not populated.
- """
- self._ensure_gerrit_change_info()
- if not self._gerrit_change: #pragma: nocover
- return None
- return int(self._gerrit_change.change)
- @property
- def gerrit_patchset_number(self):
- """Returns gerrit change patchset, e.g. 6 for a patch ref of
- "refs/heads/45/12345/6".
- Populated iff gerrit_change is populated Returns None if not populated..
- """
- self._ensure_gerrit_change_info()
- if not self._gerrit_change: #pragma: nocover
- return None
- return int(self._gerrit_change.patchset)
- @property
- def is_tryserver(self):
- """Returns true iff we have a change to check out."""
- return (self.is_patch_in_git or self.is_gerrit_issue)
- @property
- def is_gerrit_issue(self):
- """Returns true iff the properties exist to match a Gerrit issue."""
- if self.gerrit_change:
- return True
- # TODO(tandrii): remove this, once nobody is using buildbot Gerrit Poller.
- return ('event.patchSet.ref' in self.m.properties and
- 'event.change.url' in self.m.properties and
- 'event.change.id' in self.m.properties)
- @property
- def is_patch_in_git(self):
- return (self.m.properties.get('patch_storage') == 'git' and
- self.m.properties.get('patch_repo_url') and
- self.m.properties.get('patch_ref'))
- def require_is_tryserver(self):
- if self.m.tryserver.is_tryserver:
- return
- status = self.m.step.EXCEPTION
- step_text = 'This recipe requires a gerrit CL for the source under test'
- if self.m.led.launched_by_led:
- status = self.m.step.FAILURE
- step_text += (
- "\n run 'led edit-cr-cl <source CL URL>' to attach a CL to test"
- )
- self.m.step.empty('not a tryjob', status=status, step_text=step_text)
- def get_files_affected_by_patch(self, patch_root,
- report_files_via_property=None,
- **kwargs):
- """Returns list of paths to files affected by the patch.
- Args:
- * patch_root: path relative to api.path['root'], usually obtained from
- api.gclient.get_gerrit_patch_root().
- * report_files_via_property: name of the output property to report the
- list of the files. If None (default), do not report.
- Returned paths will be relative to to api.path['root'].
- """
- cwd = self.m.context.cwd or self.m.path['start_dir'].join(patch_root)
- with self.m.context(cwd=cwd):
- step_result = self.m.git(
- '-c', 'core.quotePath=false', 'diff', '--cached', '--name-only',
- name='git diff to analyze patch',
- stdout=self.m.raw_io.output(),
- step_test_data=lambda:
- self.m.raw_io.test_api.stream_output('foo.cc'),
- **kwargs)
- paths = [self.m.path.join(patch_root, p.decode('utf-8')) for p in
- step_result.stdout.splitlines()]
- paths.sort()
- if self.m.platform.is_win:
- # Looks like "analyze" wants POSIX slashes even on Windows (since git
- # uses that format even on Windows).
- paths = [path.replace('\\', '/') for path in paths]
- step_result.presentation.logs['files'] = paths
- if report_files_via_property:
- step_result.presentation.properties[report_files_via_property] = {
- 'total_count': len(paths),
- # Do not report too many because it might violate build size limits,
- # and isn't very useful anyway.
- 'first_100': paths[:100],
- }
- return paths
- def set_subproject_tag(self, subproject_tag):
- """Adds a subproject tag to the build.
- This can be used to distinguish between builds that execute different steps
- depending on what was patched, e.g. blink vs. pure chromium patches.
- """
- assert self.is_tryserver
- step_result = self.m.step('TRYJOB SET SUBPROJECT_TAG', cmd=None)
- step_result.presentation.properties['subproject_tag'] = subproject_tag
- step_result.presentation.step_text = subproject_tag
- def _set_failure_type(self, failure_type):
- if not self.is_tryserver:
- return
- # TODO(iannucci): add API to set properties regardless of the current step.
- step_result = self.m.step('TRYJOB FAILURE', cmd=None)
- step_result.presentation.properties['failure_type'] = failure_type
- step_result.presentation.step_text = failure_type
- step_result.presentation.status = 'FAILURE'
- def set_patch_failure_tryjob_result(self):
- """Mark the tryjob result as failure to apply the patch."""
- self._set_failure_type('PATCH_FAILURE')
- def set_compile_failure_tryjob_result(self):
- """Mark the tryjob result as a compile failure."""
- self._set_failure_type('COMPILE_FAILURE')
- def set_test_failure_tryjob_result(self):
- """Mark the tryjob result as a test failure.
- This means we started running actual tests (not prerequisite steps
- like checkout or compile), and some of these tests have failed.
- """
- self._set_failure_type('TEST_FAILURE')
- def set_invalid_test_results_tryjob_result(self):
- """Mark the tryjob result as having invalid test results.
- This means we run some tests, but the results were not valid
- (e.g. no list of specific test cases that failed, or too many
- tests failing, etc).
- """
- self._set_failure_type('INVALID_TEST_RESULTS')
- def set_test_timeout_tryjob_result(self):
- """Mark the tryjob result as a test timeout.
- This means tests were scheduled but didn't finish executing within the
- timeout.
- """
- self._set_failure_type('TEST_TIMEOUT')
- def set_test_expired_tryjob_result(self):
- """Mark the tryjob result as a test expiration.
- This means a test task expired and was never scheduled, most likely due to
- lack of capacity.
- """
- self._set_failure_type('TEST_EXPIRED')
- def get_footers(self, patch_text=None):
- """Retrieves footers from the patch description.
- footers are machine readable tags embedded in commit messages. See
- git-footers documentation for more information.
- """
- return self._get_footers(patch_text)
- def _ensure_gerrit_commit_message(self):
- """Fetch full commit message for Gerrit change."""
- self._ensure_gerrit_change_info()
- self._gerrit_commit_message = self.m.gerrit.get_change_description(
- 'https://%s' % self.gerrit_change.host,
- self.gerrit_change_number,
- self.gerrit_patchset_number,
- timeout=360)
- def _get_footers(self, patch_text=None):
- if patch_text is not None:
- return self._get_footer_step(patch_text)
- if self._change_footers: #pragma: nocover
- return self._change_footers
- if self.gerrit_change:
- self._ensure_gerrit_commit_message()
- self._change_footers = self._get_footer_step(self._gerrit_commit_message)
- return self._change_footers
- raise Exception(
- 'No patch text or associated changelist, cannot get footers') #pragma: nocover
- def _get_footer_step(self, patch_text):
- result = self.m.step('parse description', [
- 'python3',
- self.repo_resource('git_footers.py'), '--json',
- self.m.json.output()
- ],
- stdin=self.m.raw_io.input(data=patch_text))
- return result.json.output
- def get_footer(self, tag, patch_text=None):
- """Gets a specific tag from a CL description"""
- footers = self._get_footers(patch_text)
- if footers is None:
- return []
- return footers.get(tag, [])
- def normalize_footer_name(self, footer):
- return '-'.join([ word.title() for word in footer.strip().split('-') ])
- def set_change(self, change):
- """Set the gerrit change for this module.
- Args:
- * change: a self.m.buildbucket.common_pb2.GerritChange.
- """
- self._gerrit_info_initialized = False
- self._gerrit_change = change
- gs_suffix = '-review.googlesource.com'
- host = change.host
- if host.endswith(gs_suffix):
- host = '%s.googlesource.com' % host[:-len(gs_suffix)]
- self._gerrit_change_repo_url = 'https://%s/%s' % (host, change.project)
- self._gerrit_change_repo_host = host
- self._gerrit_change_repo_project = change.project
|