api.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. # Copyright 2014 The Chromium Authors. All rights reserved.
  2. # Use of this source code is governed by a BSD-style license that can be
  3. # found in the LICENSE file.
  4. import contextlib
  5. import hashlib
  6. from recipe_engine import recipe_api
  7. class TryserverApi(recipe_api.RecipeApi):
  8. def __init__(self, *args, **kwargs):
  9. super(TryserverApi, self).__init__(*args, **kwargs)
  10. self._gerrit_change = None # self.m.buildbucket.common_pb2.GerritChange
  11. self._gerrit_change_repo_url = None
  12. self._gerrit_info_initialized = False
  13. self._gerrit_change_target_ref = None
  14. self._gerrit_change_fetch_ref = None
  15. def initialize(self):
  16. changes = self.m.buildbucket.build.input.gerrit_changes
  17. if len(changes) == 1:
  18. cl = changes[0]
  19. self._gerrit_change = cl
  20. git_host = cl.host
  21. gs_suffix = '-review.googlesource.com'
  22. if git_host.endswith(gs_suffix):
  23. git_host = '%s.googlesource.com' % git_host[:-len(gs_suffix)]
  24. self._gerrit_change_repo_url = 'https://%s/%s' % (git_host, cl.project)
  25. @property
  26. def gerrit_change(self):
  27. """Returns current gerrit change, if there is exactly one.
  28. Returns a self.m.buildbucket.common_pb2.GerritChange or None.
  29. """
  30. return self._gerrit_change
  31. @property
  32. def gerrit_change_repo_url(self):
  33. """Returns canonical URL of the gitiles repo of the current Gerrit CL.
  34. Populated iff gerrit_change is populated.
  35. """
  36. return self._gerrit_change_repo_url
  37. def _ensure_gerrit_change_info(self):
  38. """Initializes extra info about gerrit_change, fetched from Gerrit server.
  39. Initializes _gerrit_change_target_ref and _gerrit_change_fetch_ref.
  40. May emit a step when called for the first time.
  41. """
  42. cl = self.gerrit_change
  43. if not cl: # pragma: no cover
  44. return
  45. if self._gerrit_info_initialized:
  46. return
  47. td = self._test_data if self._test_data.enabled else {}
  48. mock_res = [{
  49. 'branch': td.get('gerrit_change_target_ref', 'master'),
  50. 'revisions': {
  51. '184ebe53805e102605d11f6b143486d15c23a09c': {
  52. '_number': str(cl.patchset),
  53. 'ref': 'refs/changes/%02d/%d/%d' % (
  54. cl.change % 100, cl.change, cl.patchset),
  55. },
  56. },
  57. }]
  58. res = self.m.gerrit.get_changes(
  59. host='https://' + cl.host,
  60. query_params=[('change', cl.change)],
  61. # This list must remain static/hardcoded.
  62. # If you need extra info, either change it here (hardcoded) or
  63. # fetch separately.
  64. o_params=['ALL_REVISIONS', 'DOWNLOAD_COMMANDS'],
  65. limit=1,
  66. name='fetch current CL info',
  67. timeout=600,
  68. step_test_data=lambda: self.m.json.test_api.output(mock_res))[0]
  69. self._gerrit_change_target_ref = res['branch']
  70. if not self._gerrit_change_target_ref.startswith('refs/'):
  71. self._gerrit_change_target_ref = (
  72. 'refs/heads/' + self._gerrit_change_target_ref)
  73. for rev in res['revisions'].values():
  74. if int(rev['_number']) == self.gerrit_change.patchset:
  75. self._gerrit_change_fetch_ref = rev['ref']
  76. break
  77. self._gerrit_info_initialized = True
  78. @property
  79. def gerrit_change_fetch_ref(self):
  80. """Returns gerrit patch ref, e.g. "refs/heads/45/12345/6, or None.
  81. Populated iff gerrit_change is populated.
  82. """
  83. self._ensure_gerrit_change_info()
  84. return self._gerrit_change_fetch_ref
  85. @property
  86. def gerrit_change_target_ref(self):
  87. """Returns gerrit change destination ref, e.g. "refs/heads/master".
  88. Populated iff gerrit_change is populated.
  89. """
  90. self._ensure_gerrit_change_info()
  91. return self._gerrit_change_target_ref
  92. @property
  93. def is_tryserver(self):
  94. """Returns true iff we have a change to check out."""
  95. return (self.is_patch_in_git or self.is_gerrit_issue)
  96. @property
  97. def is_gerrit_issue(self):
  98. """Returns true iff the properties exist to match a Gerrit issue."""
  99. if self.gerrit_change:
  100. return True
  101. # TODO(tandrii): remove this, once nobody is using buildbot Gerrit Poller.
  102. return ('event.patchSet.ref' in self.m.properties and
  103. 'event.change.url' in self.m.properties and
  104. 'event.change.id' in self.m.properties)
  105. @property
  106. def is_patch_in_git(self):
  107. return (self.m.properties.get('patch_storage') == 'git' and
  108. self.m.properties.get('patch_repo_url') and
  109. self.m.properties.get('patch_ref'))
  110. def get_files_affected_by_patch(self, patch_root, **kwargs):
  111. """Returns list of paths to files affected by the patch.
  112. Argument:
  113. patch_root: path relative to api.path['root'], usually obtained from
  114. api.gclient.get_gerrit_patch_root().
  115. Returned paths will be relative to to patch_root.
  116. """
  117. cwd = self.m.context.cwd or self.m.path['start_dir'].join(patch_root)
  118. with self.m.context(cwd=cwd):
  119. step_result = self.m.git(
  120. '-c', 'core.quotePath=false', 'diff', '--cached', '--name-only',
  121. name='git diff to analyze patch',
  122. stdout=self.m.raw_io.output(),
  123. step_test_data=lambda:
  124. self.m.raw_io.test_api.stream_output('foo.cc'),
  125. **kwargs)
  126. paths = [self.m.path.join(patch_root, p) for p in
  127. step_result.stdout.split()]
  128. if self.m.platform.is_win:
  129. # Looks like "analyze" wants POSIX slashes even on Windows (since git
  130. # uses that format even on Windows).
  131. paths = [path.replace('\\', '/') for path in paths]
  132. step_result.presentation.logs['files'] = paths
  133. return paths
  134. def set_subproject_tag(self, subproject_tag):
  135. """Adds a subproject tag to the build.
  136. This can be used to distinguish between builds that execute different steps
  137. depending on what was patched, e.g. blink vs. pure chromium patches.
  138. """
  139. assert self.is_tryserver
  140. step_result = self.m.step('TRYJOB SET SUBPROJECT_TAG', cmd=None)
  141. step_result.presentation.properties['subproject_tag'] = subproject_tag
  142. step_result.presentation.step_text = subproject_tag
  143. def _set_failure_type(self, failure_type):
  144. if not self.is_tryserver:
  145. return
  146. # TODO(iannucci): add API to set properties regardless of the current step.
  147. step_result = self.m.step('TRYJOB FAILURE', cmd=None)
  148. step_result.presentation.properties['failure_type'] = failure_type
  149. step_result.presentation.step_text = failure_type
  150. step_result.presentation.status = 'FAILURE'
  151. def set_patch_failure_tryjob_result(self):
  152. """Mark the tryjob result as failure to apply the patch."""
  153. self._set_failure_type('PATCH_FAILURE')
  154. def set_compile_failure_tryjob_result(self):
  155. """Mark the tryjob result as a compile failure."""
  156. self._set_failure_type('COMPILE_FAILURE')
  157. def set_test_failure_tryjob_result(self):
  158. """Mark the tryjob result as a test failure.
  159. This means we started running actual tests (not prerequisite steps
  160. like checkout or compile), and some of these tests have failed.
  161. """
  162. self._set_failure_type('TEST_FAILURE')
  163. def set_invalid_test_results_tryjob_result(self):
  164. """Mark the tryjob result as having invalid test results.
  165. This means we run some tests, but the results were not valid
  166. (e.g. no list of specific test cases that failed, or too many
  167. tests failing, etc).
  168. """
  169. self._set_failure_type('INVALID_TEST_RESULTS')
  170. def set_test_timeout_tryjob_result(self):
  171. """Mark the tryjob result as a test timeout.
  172. This means tests were scheduled but didn't finish executing within the
  173. timeout.
  174. """
  175. self._set_failure_type('TEST_TIMEOUT')
  176. def set_test_expired_tryjob_result(self):
  177. """Mark the tryjob result as a test expiration.
  178. This means a test task expired and was never scheduled, most likely due to
  179. lack of capacity.
  180. """
  181. self._set_failure_type('TEST_EXPIRED')
  182. def get_footers(self, patch_text=None):
  183. """Retrieves footers from the patch description.
  184. footers are machine readable tags embedded in commit messages. See
  185. git-footers documentation for more information.
  186. """
  187. if patch_text is None:
  188. if self.gerrit_change:
  189. # TODO: reuse _ensure_gerrit_change_info.
  190. patch_text = self.m.gerrit.get_change_description(
  191. 'https://%s' % self.gerrit_change.host,
  192. int(self.gerrit_change.change),
  193. int(self.gerrit_change.patchset))
  194. result = self.m.python(
  195. 'parse description', self.repo_resource('git_footers.py'),
  196. args=['--json', self.m.json.output()],
  197. stdin=self.m.raw_io.input(data=patch_text))
  198. return result.json.output
  199. def get_footer(self, tag, patch_text=None):
  200. """Gets a specific tag from a CL description"""
  201. return self.get_footers(patch_text).get(tag, [])
  202. def normalize_footer_name(self, footer):
  203. return '-'.join([ word.title() for word in footer.strip().split('-') ])