Chromium Code Reviews| Index: Tools/AutoSheriff/reasons.py |
| diff --git a/Tools/AutoSheriff/reasons.py b/Tools/AutoSheriff/reasons.py |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..149aac4d2fc1a83a8a451e34bfd9ababc0ea3b5d |
| --- /dev/null |
| +++ b/Tools/AutoSheriff/reasons.py |
| @@ -0,0 +1,317 @@ |
| +# Copyright 2014 The Chromium Authors. All rights reserved. |
| +# Use of this source code is governed by a BSD-style license that can be |
| +# found in the LICENSE file. |
| + |
| +import json |
| +import logging |
| +import requests |
| +import sys |
| +import urllib |
| +import urlparse |
| +import argparse |
| +import re |
| + |
| +import requests_cache |
| + |
| +requests_cache.install_cache('reasons') |
| + |
| + |
| +# This is relative to build/scripts: |
| +# https://chromium.googlesource.com/chromium/tools/build/+/master/scripts |
| +BUILD_SCRIPTS_PATH = "/src/build/scripts" |
| +sys.path.append(BUILD_SCRIPTS_PATH) |
| +from common import gtest_utils |
| + |
| +# Python logging is stupidly verbose to configure. |
| +def setup_logging(): |
| + logger = logging.getLogger(__name__) |
| + logger.setLevel(logging.DEBUG) |
| + handler = logging.StreamHandler() |
| + handler.setLevel(logging.DEBUG) |
| + formatter = logging.Formatter('%(levelname)s: %(message)s') |
| + handler.setFormatter(formatter) |
| + logger.addHandler(handler) |
| + return logger, handler |
| + |
| + |
| +log, logging_handler = setup_logging() |
| + |
| + |
| +def build_url(master_url, builder_name, build_number): |
| + quoted_name = urllib.pathname2url(builder_name) |
| + args = (master_url, quoted_name, build_number) |
| + return "%s/builders/%s/builds/%s" % args |
| + |
| + |
| +def stdio_for_step(master_url, builder_name, build, step): |
| +# FIXME: Should get this from the step in some way? |
| + base_url = build_url(master_url, builder_name, build['number']) |
| + stdio_url = "%s/steps/%s/logs/stdio/text" % (base_url, step['name']) |
| + |
| + try: |
| + return requests.get(stdio_url).text |
| + except requests.exceptions.ConnectionError, e: |
| + # Some builders don't save logs for whatever reason. |
| + log.error('Failed to fetch %s: %s' % (stdio_url, e)) |
| + return None |
| + |
| + |
| +# These are reason finders, more than splitters? |
| +class GTestSplitter(object): |
| + def handles_step(self, step): |
| + step_name = step['name'] |
| + # Silly heuristic, at least we won't bother processing |
| + # stdio from gclient revert, etc. |
| + if step_name.endswith('tests'): |
| + return True |
| + |
| + KNOWN_STEPS = [ |
|
ojan
2014/07/22 02:01:26
I'd leave this out for now. Can always add it back
|
| + # There are probably other gtest steps not named 'tests'. |
| + ] |
| + return step_name in KNOWN_STEPS |
| + |
| + def split_step(self, step, build, builder_name, master_url): |
| + stdio_log = stdio_for_step(master_url, builder_name, build, step) |
| + # Can't split if we can't get the logs. |
| + if not stdio_log: |
| + return None |
| + |
| + # Lines this fails for: |
| + #[ FAILED ] ExtensionApiTest.TabUpdate, where TypeParam = and GetParam() = (10907 ms) |
| + |
| + log_parser = gtest_utils.GTestLogParser() |
| + for line in stdio_log.split('\n'): |
| + log_parser.ProcessLine(line) |
| + |
| + failed_tests = log_parser.FailedTests() |
| + if failed_tests: |
| + return failed_tests |
| + # Failed to split, just group with the general failures. |
| + log.debug('First Line: %s' % stdio_log.split('\n')[0]) |
| + return None |
| + |
| + |
| +# Our Android tests produce very gtest-like output, but not |
| +# quite GTestLogParser-compatible (it parse the name of the |
| +# test as org.chromium). |
| + |
| +class JUnitSplitter(object): |
| + def handles_step(self, step): |
| + KNOWN_STEPS = [ |
| + 'androidwebview_instrumentation_tests', |
| + 'mojotest_instrumentation_tests', # Are these always java? |
| + ] |
| + return step['name'] in KNOWN_STEPS |
| + |
| + FAILED_REGEXP = re.compile('\[\s+FAILED\s+\] (?P<test_name>\S+)( \(.*\))?$') |
| + |
| + def failed_tests_from_stdio(self, stdio): |
| + failed_tests = [] |
| + for line in stdio.split('\n'): |
| + match = self.FAILED_REGEXP.search(line) |
| + if match: |
| + failed_tests.append(match.group('test_name')) |
| + return failed_tests |
| + |
| + def split_step(self, step, build, builder_name, master_url): |
| + stdio_log = stdio_for_step(master_url, builder_name, build, step) |
| + # Can't split if we can't get the logs. |
| + if not stdio_log: |
| + return None |
| + |
| + failed_tests = self.failed_tests_from_stdio(stdio_log) |
| + if failed_tests: |
| + return failed_tests |
| + # Failed to split, just group with the general failures. |
| + log.debug('First Line: %s' % stdio_log.split('\n')[0]) |
| + return None |
| + |
| + |
| +def decode_results(results, include_expected=False): |
| + tests = convert_trie_to_flat_paths(results['tests']) |
| + failures = {} |
| + flakes = {} |
| + passes = {} |
| + for (test, result) in tests.iteritems(): |
| + if include_expected or result.get('is_unexpected'): |
| + actual_results = result['actual'].split() |
| + expected_results = result['expected'].split() |
| + if len(actual_results) > 1: |
| + if actual_results[1] in expected_results: |
| + flakes[test] = actual_results[0] |
|
ojan
2014/07/22 02:01:26
Can we just store result['actual'] here? That way
|
| + else: |
| + # We report the first failure type back, even if the second |
| + # was more severe. |
| + failures[test] = actual_results[0] |
|
ojan
2014/07/22 02:01:25
Ditto
|
| + elif actual_results[0] == 'PASS': |
| + passes[test] = result |
| + else: |
| + failures[test] = actual_results[0] |
| + |
| + return (passes, failures, flakes) |
| + |
| + |
| +def convert_trie_to_flat_paths(trie, prefix=None): |
| + # Cloned from webkitpy.layout_tests.layout_package.json_results_generator |
| + # so that this code can stand alone. |
| + result = {} |
| + for name, data in trie.iteritems(): |
| + if prefix: |
| + name = prefix + "/" + name |
| + |
| + if len(data) and not "actual" in data and not "expected" in data: |
| + result.update(convert_trie_to_flat_paths(data, name)) |
| + else: |
| + result[name] = data |
| + |
| + return result |
| + |
| + |
| +class LayoutTestsSplitter(object): |
| + def handles_step(self, step): |
| + return step['name'] == 'webkit_tests' |
| + |
| + def split_step(self, step, build, builder_name, master_url): |
| + # WTF? The android bots call it archive_webkit_results and the rest call it archive_webkit_tests_results? |
| + archive_names = ['archive_webkit_results', 'archive_webkit_tests_results'] |
| + archive_step = next((step for step in build['steps'] if step['name'] in archive_names), None) |
| + url_to_build = build_url(master_url, builder_name, build['number']) |
| + |
| + if not archive_step: |
| + log.warn('No archive step in %s' % url_to_build) |
| + # print json.dumps(build['steps'], indent=1) |
|
ojan
2014/07/22 02:01:26
Delete dead code?
|
| + return None |
| + |
| + html_results_url = archive_step['urls'].get('layout test results') |
| + # FIXME: Here again, Android is a special snowflake. |
| + if not html_results_url: |
| + html_results_url = archive_step['urls'].get('results') |
| + |
| + if not html_results_url: |
| + webkit_tests_step = next((step for step in build['steps'] if step['name'] == 'webkit_tests'), None) |
| + # Common cause of this is an exception in the webkit_tests step. |
| + if webkit_tests_step['results'][0] != 5: |
| + log.warn('No results url for archive step in %s' % url_to_build) |
| + # print json.dumps(archive_step, indent=1) |
|
ojan
2014/07/22 02:01:25
Ditto
|
| + return None |
| + |
| + # !@?#!$^&$% WTF HOW DO URLS HAVE \r in them!?! |
|
ojan
2014/07/22 02:01:26
Wat
|
| + html_results_url = html_results_url.replace('\r', '') |
| + |
| + jsonp_url = urlparse.urljoin(html_results_url, 'failing_results.json') |
| + # FIXME: Silly that this is still JSONP. |
|
ojan
2014/07/22 02:01:26
This is because failing_results.json is used by re
|
| + jsonp_string = requests.get(jsonp_url).text |
| + if "The specified key does not exist" in jsonp_string: |
| + log.warn('%s missing for %s' % (jsonp_url, url_to_build)) |
| + return None |
| + |
| + json_string = jsonp_string[len('ADD_RESULTS('):-len(');')] |
| + try: |
| + results = json.loads(json_string) |
| + passes, failures, flakes = decode_results(results) |
| + if failures: |
| + return ['%s:%s' % (name, types) for name, types in failures.items()] |
| + except ValueError, e: |
| + print archive_step['urls'] |
| + print html_results_url |
| + print "Failed %s, %s at decode of: %s" % (jsonp_url, e, jsonp_string) |
| + |
| + # Failed to split, just group with the general failures. |
| + return None |
| + |
| + |
| +class CompileSplitter(object): |
| + def handles_step(self, step): |
| + return step['name'] == 'compile' |
| + |
| +# Compile example: |
|
ojan
2014/07/22 02:01:26
Even better than examples in comments is examples
|
| +# FAILED: /mnt/data/b/build/goma/gomacc ... |
| +# ../../v8/src/base/platform/time.cc:590:7: error: use of undeclared identifier 'close' |
| + |
| +# Linker example: |
| +# FAILED: /b/build/goma/gomacc ... |
| +# obj/chrome/browser/extensions/interactive_ui_tests.extension_commands_global_registry_apitest.o:extension_commands_global_registry_apitest.cc:function extensions::SendNativeKeyEventToXDisplay(ui::KeyboardCode, bool, bool, bool): error: undefined reference to 'gfx::GetXDisplay()' |
| + |
| + def split_step(self, step, build, builder_name, master_url): |
| + stdio = stdio_for_step(master_url, builder_name, build, step) |
| + # Can't split if we can't get the logs. |
| + if not stdio: |
|
ojan
2014/07/22 02:01:26
Should we log an error or something here?
|
| + return None |
| + |
| + compile_regexp = re.compile(r'(?P<path>.*):(?P<line>\d+):(?P<column>\d+): error:') |
| + |
| + # FIXME: I'm sure there is a cleaner way to do this. |
| + next_line_is_failure = False |
| + for line in stdio.split('\n'): |
| + if not next_line_is_failure: |
| + if line.startswith('FAILED: '): |
| + next_line_is_failure = True |
| + continue |
| + |
| + match = compile_regexp.match(line) |
| + if match: |
| + return ['%s:%s' % (match.group('path'), match.group('line'))] |
| + break |
| + |
| + return None |
| + |
| + |
| +# This is a hack I wrote because all the perf bots are failing with: |
| +# E 0.009s Main File not found /b/build/slave/Android_GN_Perf/build/src/out/step_results/dromaeo.jslibstyleprototype |
| +# and it's nice to group them by something at least! |
| +# Often just hits: |
| +# 2 new files were left in c:\users\chrome~1.per\appdata\local\temp: Fix the tests to clean up themselves. |
| +# so disabled for now. |
| +class GenericRunTests(object): |
| + def handles_step(self, step): |
| + return True |
| + |
| + def split_step(self, step, build, builder_name, master_url): |
| + stdio = stdio_for_step(master_url, builder_name, build, step) |
| + # Can't split if we can't get the logs. |
| + if not stdio: |
| + return None |
| + |
| + last_line = None |
| + for line in stdio.split('\n'): |
| + if last_line and line.startswith('exit code (as seen by runtest.py):'): |
| + return [last_line] |
| + last_line = line |
| + |
| + |
| +STEP_SPLITTERS = [ |
| + CompileSplitter(), |
| + LayoutTestsSplitter(), |
| + JUnitSplitter(), |
| + GTestSplitter(), |
| + # GenericRunTests(), |
|
ojan
2014/07/22 02:01:26
Should we delete this if we're going to comment it
|
| +] |
| + |
| + |
| +# For testing: |
| +def main(args): |
| + parser = argparse.ArgumentParser() |
| + parser.add_argument('stdio_url', action='store') |
| + args = parser.parse_args(args) |
| + |
| + # https://build.chromium.org/p/chromium.win/builders/XP%20Tests%20(1)/builds/31886/steps/browser_tests/logs/stdio |
| + url_regexp = re.compile('(?P<master_url>.*)/builders/(?P<builder_name>.*)/builds/(?P<build_number>.*)/steps/(?P<step_name>.*)/logs/stdio') |
| + match = url_regexp.match(args.stdio_url) |
| + if not match: |
| + print "Failed to parse URL: %s" % args.stdio_url |
| + sys.exit(1) |
| + |
| + step = { |
| + 'name': match.group('step_name'), |
| + } |
| + build = { |
| + 'number': match.group('build_number'), |
| + } |
| + splitter = next((splitter for splitter in STEP_SPLITTERS if splitter.handles_step(step)), None) |
| + builder_name = urllib.unquote_plus(match.group('builder_name')) |
| + master_url = match.group('master_url') |
| + print splitter.split_step(step, build, builder_name, master_url) |
| + |
| + |
| +if __name__ == '__main__': |
| + sys.exit(main(sys.argv[1:])) |