| #!/usr/bin/env vpython3 |
| # Copyright 2016 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from __future__ import print_function |
| |
| import argparse |
| import re |
| import sys |
| import urllib2 |
| |
| |
| _BUILD_REGEX = r'builds/(\d+)' |
| _REVISION_REGEX = r'\nCr-Commit-Position: refs/heads/(?:master|main)@{#(\d+)}' |
| |
| |
| class _Color(object): |
| HEADER = '\033[95m' |
| OKBLUE = '\033[94m' |
| OKGREEN = '\033[92m' |
| WARNING = '\033[93m' |
| FAIL = '\033[91m' |
| ENDC = '\033[0m' |
| BOLD = '\033[1m' |
| UNDERLINE = '\033[4m' |
| |
| |
| def _PrintWithColor(text, *colors): |
| print(''.join(colors) + text + _Color.ENDC) |
| |
| |
| def _ExtractBuildRevisionRange(build_page_content, build_url, test_name): |
| if not test_name in build_page_content: |
| raise Exception( |
| 'Cannot find %s in %s. If you think this is an exception step, please, ' |
| 'restart the process with build# = last test build - 1' |
| % (test_name, build_url)) |
| if not ('failed ' + test_name) in build_page_content: |
| return None |
| revisions = re.findall(_REVISION_REGEX, build_page_content) |
| revisions = list(int(r) for r in revisions) |
| return revisions |
| |
| def _ShouldSkipBuild(build_page_content, build_url): |
| _GLOBAL_FAILURE = [ |
| 'failed sharded perf tests', |
| 'exception sharded perf tests', |
| ] |
| for failure in _GLOBAL_FAILURE: |
| if failure in build_page_content: |
| print( |
| _PrintWithColor( |
| "Warning: %s has '%s'." |
| ' Skipping this build' % (build_url, failure), _Color.WARNING)) |
| return True |
| return False |
| |
| |
| def FindFirstFailureRange(build_url, test_name): |
| build_number = re.findall(_BUILD_REGEX, build_url) |
| assert len(build_number) == 1, ( |
| 'Must put in a valid build url with build number') |
| build_number = int(build_number[0]) |
| |
| initial_build_url = build_url[:build_url.find('builds/')] + 'builds/' |
| while True: |
| current_build_url = initial_build_url + str(build_number) |
| build_number -= 1 |
| print('\rProcess %s' % current_build_url,) |
| sys.stdout.flush() |
| build_page_content = urllib2.urlopen(current_build_url).read() |
| if _ShouldSkipBuild(build_page_content, build_url): |
| continue |
| failure_revisions = _ExtractBuildRevisionRange( |
| build_page_content, current_build_url, test_name) |
| if failure_revisions == None: |
| return first_failure_revisions, first_failed_build |
| else: |
| first_failure_revisions = failure_revisions |
| first_failed_build = current_build_url |
| |
| |
| def Main(args): |
| parser = argparse.ArgumentParser( |
| 'Find first failed revision range for a given failed test. Notes that ' |
| 'this tool cannot handle flaky test failures.') |
| parser.add_argument('test_name') |
| parser.add_argument('build_url', |
| help='A build url which |test_name| is failing') |
| options = parser.parse_args(args) |
| first_failure_revisions, first_failed_build = FindFirstFailureRange( |
| options.build_url, options.test_name) |
| print() |
| print() |
| _PrintWithColor( |
| 'First failure range: %s - %s CLs' % ( |
| (min(first_failure_revisions), max(first_failure_revisions)), |
| len(set(first_failure_revisions))), |
| _Color.BOLD, _Color.OKGREEN) |
| _PrintWithColor('First failed build: %s' % first_failed_build, |
| _Color.BOLD, _Color.OKGREEN) |
| return 0 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(Main(sys.argv[1:])) |