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 json | |
6 import logging | |
7 import requests | |
8 import sys | |
9 import urllib | |
10 import urlparse | |
11 import argparse | |
12 import re | |
13 | |
14 import requests_cache | |
15 | |
16 requests_cache.install_cache('reasons') | |
17 | |
18 | |
19 # This is relative to build/scripts: | |
20 # https://chromium.googlesource.com/chromium/tools/build/+/master/scripts | |
21 BUILD_SCRIPTS_PATH = "/src/build/scripts" | |
22 sys.path.append(BUILD_SCRIPTS_PATH) | |
23 from common import gtest_utils | |
24 | |
25 # Python logging is stupidly verbose to configure. | |
26 def setup_logging(): | |
27 logger = logging.getLogger(__name__) | |
28 logger.setLevel(logging.DEBUG) | |
29 handler = logging.StreamHandler() | |
30 handler.setLevel(logging.DEBUG) | |
31 formatter = logging.Formatter('%(levelname)s: %(message)s') | |
32 handler.setFormatter(formatter) | |
33 logger.addHandler(handler) | |
34 return logger, handler | |
35 | |
36 | |
37 log, logging_handler = setup_logging() | |
38 | |
39 | |
40 def build_url(master_url, builder_name, build_number): | |
41 quoted_name = urllib.pathname2url(builder_name) | |
42 args = (master_url, quoted_name, build_number) | |
43 return "%s/builders/%s/builds/%s" % args | |
44 | |
45 | |
46 def stdio_for_step(master_url, builder_name, build, step): | |
47 # FIXME: Should get this from the step in some way? | |
48 base_url = build_url(master_url, builder_name, build['number']) | |
49 stdio_url = "%s/steps/%s/logs/stdio/text" % (base_url, step['name']) | |
50 | |
51 try: | |
52 return requests.get(stdio_url).text | |
53 except requests.exceptions.ConnectionError, e: | |
54 # Some builders don't save logs for whatever reason. | |
55 log.error('Failed to fetch %s: %s' % (stdio_url, e)) | |
56 return None | |
57 | |
58 | |
59 # These are reason finders, more than splitters? | |
60 class GTestSplitter(object): | |
61 def handles_step(self, step): | |
62 step_name = step['name'] | |
63 # Silly heuristic, at least we won't bother processing | |
64 # stdio from gclient revert, etc. | |
65 if step_name.endswith('tests'): | |
66 return True | |
67 | |
68 KNOWN_STEPS = [ | |
ojan
2014/07/22 02:01:26
I'd leave this out for now. Can always add it back
| |
69 # There are probably other gtest steps not named 'tests'. | |
70 ] | |
71 return step_name in KNOWN_STEPS | |
72 | |
73 def split_step(self, step, build, builder_name, master_url): | |
74 stdio_log = stdio_for_step(master_url, builder_name, build, step) | |
75 # Can't split if we can't get the logs. | |
76 if not stdio_log: | |
77 return None | |
78 | |
79 # Lines this fails for: | |
80 #[ FAILED ] ExtensionApiTest.TabUpdate, where TypeParam = and GetParam() = (10907 ms) | |
81 | |
82 log_parser = gtest_utils.GTestLogParser() | |
83 for line in stdio_log.split('\n'): | |
84 log_parser.ProcessLine(line) | |
85 | |
86 failed_tests = log_parser.FailedTests() | |
87 if failed_tests: | |
88 return failed_tests | |
89 # Failed to split, just group with the general failures. | |
90 log.debug('First Line: %s' % stdio_log.split('\n')[0]) | |
91 return None | |
92 | |
93 | |
94 # Our Android tests produce very gtest-like output, but not | |
95 # quite GTestLogParser-compatible (it parse the name of the | |
96 # test as org.chromium). | |
97 | |
98 class JUnitSplitter(object): | |
99 def handles_step(self, step): | |
100 KNOWN_STEPS = [ | |
101 'androidwebview_instrumentation_tests', | |
102 'mojotest_instrumentation_tests', # Are these always java? | |
103 ] | |
104 return step['name'] in KNOWN_STEPS | |
105 | |
106 FAILED_REGEXP = re.compile('\[\s+FAILED\s+\] (?P<test_name>\S+)( \(.*\))?$') | |
107 | |
108 def failed_tests_from_stdio(self, stdio): | |
109 failed_tests = [] | |
110 for line in stdio.split('\n'): | |
111 match = self.FAILED_REGEXP.search(line) | |
112 if match: | |
113 failed_tests.append(match.group('test_name')) | |
114 return failed_tests | |
115 | |
116 def split_step(self, step, build, builder_name, master_url): | |
117 stdio_log = stdio_for_step(master_url, builder_name, build, step) | |
118 # Can't split if we can't get the logs. | |
119 if not stdio_log: | |
120 return None | |
121 | |
122 failed_tests = self.failed_tests_from_stdio(stdio_log) | |
123 if failed_tests: | |
124 return failed_tests | |
125 # Failed to split, just group with the general failures. | |
126 log.debug('First Line: %s' % stdio_log.split('\n')[0]) | |
127 return None | |
128 | |
129 | |
130 def decode_results(results, include_expected=False): | |
131 tests = convert_trie_to_flat_paths(results['tests']) | |
132 failures = {} | |
133 flakes = {} | |
134 passes = {} | |
135 for (test, result) in tests.iteritems(): | |
136 if include_expected or result.get('is_unexpected'): | |
137 actual_results = result['actual'].split() | |
138 expected_results = result['expected'].split() | |
139 if len(actual_results) > 1: | |
140 if actual_results[1] in expected_results: | |
141 flakes[test] = actual_results[0] | |
ojan
2014/07/22 02:01:26
Can we just store result['actual'] here? That way
| |
142 else: | |
143 # We report the first failure type back, even if the second | |
144 # was more severe. | |
145 failures[test] = actual_results[0] | |
ojan
2014/07/22 02:01:25
Ditto
| |
146 elif actual_results[0] == 'PASS': | |
147 passes[test] = result | |
148 else: | |
149 failures[test] = actual_results[0] | |
150 | |
151 return (passes, failures, flakes) | |
152 | |
153 | |
154 def convert_trie_to_flat_paths(trie, prefix=None): | |
155 # Cloned from webkitpy.layout_tests.layout_package.json_results_generator | |
156 # so that this code can stand alone. | |
157 result = {} | |
158 for name, data in trie.iteritems(): | |
159 if prefix: | |
160 name = prefix + "/" + name | |
161 | |
162 if len(data) and not "actual" in data and not "expected" in data: | |
163 result.update(convert_trie_to_flat_paths(data, name)) | |
164 else: | |
165 result[name] = data | |
166 | |
167 return result | |
168 | |
169 | |
170 class LayoutTestsSplitter(object): | |
171 def handles_step(self, step): | |
172 return step['name'] == 'webkit_tests' | |
173 | |
174 def split_step(self, step, build, builder_name, master_url): | |
175 # WTF? The android bots call it archive_webkit_results and the rest call it archive_webkit_tests_results? | |
176 archive_names = ['archive_webkit_results', 'archive_webkit_tests_results'] | |
177 archive_step = next((step for step in build['steps'] if step['name'] in arch ive_names), None) | |
178 url_to_build = build_url(master_url, builder_name, build['number']) | |
179 | |
180 if not archive_step: | |
181 log.warn('No archive step in %s' % url_to_build) | |
182 # print json.dumps(build['steps'], indent=1) | |
ojan
2014/07/22 02:01:26
Delete dead code?
| |
183 return None | |
184 | |
185 html_results_url = archive_step['urls'].get('layout test results') | |
186 # FIXME: Here again, Android is a special snowflake. | |
187 if not html_results_url: | |
188 html_results_url = archive_step['urls'].get('results') | |
189 | |
190 if not html_results_url: | |
191 webkit_tests_step = next((step for step in build['steps'] if step['name'] == 'webkit_tests'), None) | |
192 # Common cause of this is an exception in the webkit_tests step. | |
193 if webkit_tests_step['results'][0] != 5: | |
194 log.warn('No results url for archive step in %s' % url_to_build) | |
195 # print json.dumps(archive_step, indent=1) | |
ojan
2014/07/22 02:01:25
Ditto
| |
196 return None | |
197 | |
198 # !@?#!$^&$% WTF HOW DO URLS HAVE \r in them!?! | |
ojan
2014/07/22 02:01:26
Wat
| |
199 html_results_url = html_results_url.replace('\r', '') | |
200 | |
201 jsonp_url = urlparse.urljoin(html_results_url, 'failing_results.json') | |
202 # FIXME: Silly that this is still JSONP. | |
ojan
2014/07/22 02:01:26
This is because failing_results.json is used by re
| |
203 jsonp_string = requests.get(jsonp_url).text | |
204 if "The specified key does not exist" in jsonp_string: | |
205 log.warn('%s missing for %s' % (jsonp_url, url_to_build)) | |
206 return None | |
207 | |
208 json_string = jsonp_string[len('ADD_RESULTS('):-len(');')] | |
209 try: | |
210 results = json.loads(json_string) | |
211 passes, failures, flakes = decode_results(results) | |
212 if failures: | |
213 return ['%s:%s' % (name, types) for name, types in failures.items()] | |
214 except ValueError, e: | |
215 print archive_step['urls'] | |
216 print html_results_url | |
217 print "Failed %s, %s at decode of: %s" % (jsonp_url, e, jsonp_string) | |
218 | |
219 # Failed to split, just group with the general failures. | |
220 return None | |
221 | |
222 | |
223 class CompileSplitter(object): | |
224 def handles_step(self, step): | |
225 return step['name'] == 'compile' | |
226 | |
227 # Compile example: | |
ojan
2014/07/22 02:01:26
Even better than examples in comments is examples
| |
228 # FAILED: /mnt/data/b/build/goma/gomacc ... | |
229 # ../../v8/src/base/platform/time.cc:590:7: error: use of undeclared identifier 'close' | |
230 | |
231 # Linker example: | |
232 # FAILED: /b/build/goma/gomacc ... | |
233 # obj/chrome/browser/extensions/interactive_ui_tests.extension_commands_global_r egistry_apitest.o:extension_commands_global_registry_apitest.cc:function extensi ons::SendNativeKeyEventToXDisplay(ui::KeyboardCode, bool, bool, bool): error: un defined reference to 'gfx::GetXDisplay()' | |
234 | |
235 def split_step(self, step, build, builder_name, master_url): | |
236 stdio = stdio_for_step(master_url, builder_name, build, step) | |
237 # Can't split if we can't get the logs. | |
238 if not stdio: | |
ojan
2014/07/22 02:01:26
Should we log an error or something here?
| |
239 return None | |
240 | |
241 compile_regexp = re.compile(r'(?P<path>.*):(?P<line>\d+):(?P<column>\d+): er ror:') | |
242 | |
243 # FIXME: I'm sure there is a cleaner way to do this. | |
244 next_line_is_failure = False | |
245 for line in stdio.split('\n'): | |
246 if not next_line_is_failure: | |
247 if line.startswith('FAILED: '): | |
248 next_line_is_failure = True | |
249 continue | |
250 | |
251 match = compile_regexp.match(line) | |
252 if match: | |
253 return ['%s:%s' % (match.group('path'), match.group('line'))] | |
254 break | |
255 | |
256 return None | |
257 | |
258 | |
259 # This is a hack I wrote because all the perf bots are failing with: | |
260 # E 0.009s Main File not found /b/build/slave/Android_GN_Perf/build/src/out/ step_results/dromaeo.jslibstyleprototype | |
261 # and it's nice to group them by something at least! | |
262 # Often just hits: | |
263 # 2 new files were left in c:\users\chrome~1.per\appdata\local\temp: Fix the tes ts to clean up themselves. | |
264 # so disabled for now. | |
265 class GenericRunTests(object): | |
266 def handles_step(self, step): | |
267 return True | |
268 | |
269 def split_step(self, step, build, builder_name, master_url): | |
270 stdio = stdio_for_step(master_url, builder_name, build, step) | |
271 # Can't split if we can't get the logs. | |
272 if not stdio: | |
273 return None | |
274 | |
275 last_line = None | |
276 for line in stdio.split('\n'): | |
277 if last_line and line.startswith('exit code (as seen by runtest.py):'): | |
278 return [last_line] | |
279 last_line = line | |
280 | |
281 | |
282 STEP_SPLITTERS = [ | |
283 CompileSplitter(), | |
284 LayoutTestsSplitter(), | |
285 JUnitSplitter(), | |
286 GTestSplitter(), | |
287 # GenericRunTests(), | |
ojan
2014/07/22 02:01:26
Should we delete this if we're going to comment it
| |
288 ] | |
289 | |
290 | |
291 # For testing: | |
292 def main(args): | |
293 parser = argparse.ArgumentParser() | |
294 parser.add_argument('stdio_url', action='store') | |
295 args = parser.parse_args(args) | |
296 | |
297 # https://build.chromium.org/p/chromium.win/builders/XP%20Tests%20(1)/builds/3 1886/steps/browser_tests/logs/stdio | |
298 url_regexp = re.compile('(?P<master_url>.*)/builders/(?P<builder_name>.*)/buil ds/(?P<build_number>.*)/steps/(?P<step_name>.*)/logs/stdio') | |
299 match = url_regexp.match(args.stdio_url) | |
300 if not match: | |
301 print "Failed to parse URL: %s" % args.stdio_url | |
302 sys.exit(1) | |
303 | |
304 step = { | |
305 'name': match.group('step_name'), | |
306 } | |
307 build = { | |
308 'number': match.group('build_number'), | |
309 } | |
310 splitter = next((splitter for splitter in STEP_SPLITTERS if splitter.handles_s tep(step)), None) | |
311 builder_name = urllib.unquote_plus(match.group('builder_name')) | |
312 master_url = match.group('master_url') | |
313 print splitter.split_step(step, build, builder_name, master_url) | |
314 | |
315 | |
316 if __name__ == '__main__': | |
317 sys.exit(main(sys.argv[1:])) | |
OLD | NEW |