Chromium Code Reviews| 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 |