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 |