OLD | NEW |
(Empty) | |
| 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 |
| 5 import contextlib |
| 6 import hashlib |
| 7 |
| 8 from recipe_engine import recipe_api |
| 9 |
| 10 |
| 11 PATCH_STORAGE_RIETVELD = 'rietveld' |
| 12 PATCH_STORAGE_GIT = 'git' |
| 13 PATCH_STORAGE_SVN = 'svn' |
| 14 |
| 15 |
| 16 class TryserverApi(recipe_api.RecipeApi): |
| 17 def __init__(self, *args, **kwargs): |
| 18 super(TryserverApi, self).__init__(*args, **kwargs) |
| 19 self._failure_reasons = [] |
| 20 |
| 21 @property |
| 22 def patch_url(self): |
| 23 """Reads patch_url property and corrects it if needed.""" |
| 24 url = self.m.properties.get('patch_url') |
| 25 return url |
| 26 |
| 27 @property |
| 28 def is_tryserver(self): |
| 29 """Returns true iff we can apply_issue or patch.""" |
| 30 return (self.can_apply_issue or self.is_patch_in_svn or |
| 31 self.is_patch_in_git or self.is_gerrit_issue) |
| 32 |
| 33 @property |
| 34 def can_apply_issue(self): |
| 35 """Returns true iff the properties exist to apply_issue from rietveld.""" |
| 36 return (self.m.properties.get('rietveld') |
| 37 and 'issue' in self.m.properties |
| 38 and 'patchset' in self.m.properties) |
| 39 |
| 40 @property |
| 41 def is_gerrit_issue(self): |
| 42 """Returns true iff the properties exist to match a Gerrit issue.""" |
| 43 return ('event.patchSet.ref' in self.m.properties and |
| 44 'event.change.url' in self.m.properties and |
| 45 'event.change.id' in self.m.properties) |
| 46 |
| 47 @property |
| 48 def is_patch_in_svn(self): |
| 49 """Returns true iff the properties exist to patch from a patch URL.""" |
| 50 return self.patch_url |
| 51 |
| 52 @property |
| 53 def is_patch_in_git(self): |
| 54 return (self.m.properties.get('patch_storage') == PATCH_STORAGE_GIT and |
| 55 self.m.properties.get('patch_repo_url') and |
| 56 self.m.properties.get('patch_ref')) |
| 57 |
| 58 def _apply_patch_step(self, patch_file=None, patch_content=None, root=None): |
| 59 assert not (patch_file and patch_content), ( |
| 60 'Please only specify either patch_file or patch_content, not both!') |
| 61 patch_cmd = [ |
| 62 'patch', |
| 63 '--dir', root or self.m.path['checkout'], |
| 64 '--force', |
| 65 '--forward', |
| 66 '--remove-empty-files', |
| 67 '--strip', '0', |
| 68 ] |
| 69 if patch_file: |
| 70 patch_cmd.extend(['--input', patch_file]) |
| 71 |
| 72 self.m.step('apply patch', patch_cmd, |
| 73 stdin=patch_content) |
| 74 |
| 75 def apply_from_svn(self, cwd): |
| 76 """Downloads patch from patch_url using svn-export and applies it""" |
| 77 # TODO(nodir): accept these properties as parameters |
| 78 patch_url = self.patch_url |
| 79 root = cwd |
| 80 if root is None: |
| 81 issue_root = self.m.rietveld.calculate_issue_root() |
| 82 root = self.m.path['checkout'].join(issue_root) |
| 83 |
| 84 patch_file = self.m.raw_io.output('.diff') |
| 85 ext = '.bat' if self.m.platform.is_win else '' |
| 86 svn_cmd = ['svn' + ext, 'export', '--force', patch_url, patch_file] |
| 87 |
| 88 result = self.m.step('download patch', svn_cmd, |
| 89 step_test_data=self.test_api.patch_content) |
| 90 result.presentation.logs['patch.diff'] = ( |
| 91 result.raw_io.output.split('\n')) |
| 92 |
| 93 patch_content = self.m.raw_io.input(result.raw_io.output) |
| 94 self._apply_patch_step(patch_content=patch_content, root=root) |
| 95 |
| 96 def apply_from_git(self, cwd): |
| 97 """Downloads patch from given git repo and ref and applies it""" |
| 98 # TODO(nodir): accept these properties as parameters |
| 99 patch_repo_url = self.m.properties['patch_repo_url'] |
| 100 patch_ref = self.m.properties['patch_ref'] |
| 101 |
| 102 patch_dir = self.m.path.mkdtemp('patch') |
| 103 git_setup_py = self.m.path['build'].join('scripts', 'slave', 'git_setup.py') |
| 104 git_setup_args = ['--path', patch_dir, '--url', patch_repo_url] |
| 105 patch_path = patch_dir.join('patch.diff') |
| 106 |
| 107 self.m.python('patch git setup', git_setup_py, git_setup_args) |
| 108 self.m.git('fetch', 'origin', patch_ref, |
| 109 name='patch fetch', cwd=patch_dir) |
| 110 self.m.git('clean', '-f', '-d', '-x', |
| 111 name='patch clean', cwd=patch_dir) |
| 112 self.m.git('checkout', '-f', 'FETCH_HEAD', |
| 113 name='patch git checkout', cwd=patch_dir) |
| 114 self._apply_patch_step(patch_file=patch_path, root=cwd) |
| 115 self.m.step('remove patch', ['rm', '-rf', patch_dir]) |
| 116 |
| 117 def determine_patch_storage(self): |
| 118 """Determines patch_storage automatically based on properties.""" |
| 119 storage = self.m.properties.get('patch_storage') |
| 120 if storage: |
| 121 return storage |
| 122 |
| 123 if self.can_apply_issue: |
| 124 return PATCH_STORAGE_RIETVELD |
| 125 elif self.is_patch_in_svn: |
| 126 return PATCH_STORAGE_SVN |
| 127 |
| 128 def maybe_apply_issue(self, cwd=None, authentication=None): |
| 129 """If we're a trybot, apply a codereview issue. |
| 130 |
| 131 Args: |
| 132 cwd: If specified, apply the patch from the specified directory. |
| 133 authentication: authentication scheme whenever apply_issue.py is called. |
| 134 This is only used if the patch comes from Rietveld. Possible values: |
| 135 None, 'oauth2' (see also api.rietveld.apply_issue.) |
| 136 """ |
| 137 storage = self.determine_patch_storage() |
| 138 |
| 139 if storage == PATCH_STORAGE_RIETVELD: |
| 140 return self.m.rietveld.apply_issue( |
| 141 self.m.rietveld.calculate_issue_root(), |
| 142 authentication=authentication) |
| 143 elif storage == PATCH_STORAGE_SVN: |
| 144 return self.apply_from_svn(cwd) |
| 145 elif storage == PATCH_STORAGE_GIT: |
| 146 return self.apply_from_git(cwd) |
| 147 else: |
| 148 # Since this method is "maybe", we don't raise an Exception. |
| 149 pass |
| 150 |
| 151 def get_files_affected_by_patch(self): |
| 152 git_diff_kwargs = {} |
| 153 issue_root = self.m.rietveld.calculate_issue_root() |
| 154 if issue_root: |
| 155 git_diff_kwargs['cwd'] = self.m.path['checkout'].join(issue_root) |
| 156 step_result = self.m.git('diff', '--cached', '--name-only', |
| 157 name='git diff to analyze patch', |
| 158 stdout=self.m.raw_io.output(), |
| 159 step_test_data=lambda: |
| 160 self.m.raw_io.test_api.stream_output('foo.cc'), |
| 161 **git_diff_kwargs) |
| 162 paths = step_result.stdout.split() |
| 163 if issue_root: |
| 164 paths = [self.m.path.join(issue_root, path) for path in paths] |
| 165 if self.m.platform.is_win: |
| 166 # Looks like "analyze" wants POSIX slashes even on Windows (since git |
| 167 # uses that format even on Windows). |
| 168 paths = [path.replace('\\', '/') for path in paths] |
| 169 |
| 170 step_result.presentation.logs['files'] = paths |
| 171 return paths |
| 172 |
| 173 def set_subproject_tag(self, subproject_tag): |
| 174 """Adds a subproject tag to the build. |
| 175 |
| 176 This can be used to distinguish between builds that execute different steps |
| 177 depending on what was patched, e.g. blink vs. pure chromium patches. |
| 178 """ |
| 179 assert self.is_tryserver |
| 180 |
| 181 step_result = self.m.step.active_result |
| 182 step_result.presentation.properties['subproject_tag'] = subproject_tag |
| 183 |
| 184 def _set_failure_type(self, failure_type): |
| 185 if not self.is_tryserver: |
| 186 return |
| 187 |
| 188 step_result = self.m.step.active_result |
| 189 step_result.presentation.properties['failure_type'] = failure_type |
| 190 |
| 191 def set_patch_failure_tryjob_result(self): |
| 192 """Mark the tryjob result as failure to apply the patch.""" |
| 193 self._set_failure_type('PATCH_FAILURE') |
| 194 |
| 195 def set_compile_failure_tryjob_result(self): |
| 196 """Mark the tryjob result as a compile failure.""" |
| 197 self._set_failure_type('COMPILE_FAILURE') |
| 198 |
| 199 def set_test_failure_tryjob_result(self): |
| 200 """Mark the tryjob result as a test failure. |
| 201 |
| 202 This means we started running actual tests (not prerequisite steps |
| 203 like checkout or compile), and some of these tests have failed. |
| 204 """ |
| 205 self._set_failure_type('TEST_FAILURE') |
| 206 |
| 207 def set_invalid_test_results_tryjob_result(self): |
| 208 """Mark the tryjob result as having invalid test results. |
| 209 |
| 210 This means we run some tests, but the results were not valid |
| 211 (e.g. no list of specific test cases that failed, or too many |
| 212 tests failing, etc). |
| 213 """ |
| 214 self._set_failure_type('INVALID_TEST_RESULTS') |
| 215 |
| 216 def add_failure_reason(self, reason): |
| 217 """ |
| 218 Records a more detailed reason why build is failing. |
| 219 |
| 220 The reason can be any JSON-serializable object. |
| 221 """ |
| 222 assert self.m.json.is_serializable(reason) |
| 223 self._failure_reasons.append(reason) |
| 224 |
| 225 @contextlib.contextmanager |
| 226 def set_failure_hash(self): |
| 227 """ |
| 228 Context manager that sets a failure_hash build property on StepFailure. |
| 229 |
| 230 This can be used to easily compare whether two builds have failed |
| 231 for the same reason. For example, if a patch is bad (breaks something), |
| 232 we'd expect it to always break in the same way. Different failures |
| 233 for the same patch are usually a sign of flakiness. |
| 234 """ |
| 235 try: |
| 236 yield |
| 237 except self.m.step.StepFailure as e: |
| 238 self.add_failure_reason(e.reason) |
| 239 |
| 240 failure_hash = hashlib.sha1() |
| 241 failure_hash.update(self.m.json.dumps(self._failure_reasons)) |
| 242 |
| 243 step_result = self.m.step.active_result |
| 244 step_result.presentation.properties['failure_hash'] = \ |
| 245 failure_hash.hexdigest() |
| 246 |
| 247 raise |
OLD | NEW |