| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/env python | |
| 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
| 3 # Use of this source code is governed by a BSD-style license that can be | |
| 4 # found in the LICENSE file. | |
| 5 | |
| 6 """Runs through isolate_test_cases.py all the tests cases in a google-test | |
| 7 executable, grabs the failures and traces them to generate a new .isolate. | |
| 8 | |
| 9 This scripts requires a .isolated file. This file is generated from a .isolate | |
| 10 file. You can use 'GYP_DEFINES=test_isolation_mode=check ninja foo_test_run' to | |
| 11 generate it. | |
| 12 """ | |
| 13 | |
| 14 import json | |
| 15 import logging | |
| 16 import os | |
| 17 import subprocess | |
| 18 import sys | |
| 19 import tempfile | |
| 20 | |
| 21 import isolate | |
| 22 import isolate_test_cases | |
| 23 import run_test_cases | |
| 24 | |
| 25 ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| 26 | |
| 27 | |
| 28 def with_tempfile(function): | |
| 29 """Creates a temporary file and calls the inner function.""" | |
| 30 def hook(*args, **kwargs): | |
| 31 handle, tempfilepath = tempfile.mkstemp(prefix='fix_test_cases') | |
| 32 os.close(handle) | |
| 33 try: | |
| 34 return function(tempfilepath, *args, **kwargs) | |
| 35 finally: | |
| 36 try: | |
| 37 os.remove(tempfilepath) | |
| 38 except OSError, e: | |
| 39 print >> sys.stderr, 'Failed to remove %s: %s' % (tempfilepath, e) | |
| 40 return hook | |
| 41 | |
| 42 | |
| 43 def load_run_test_cases_results(run_test_cases_file): | |
| 44 """Loads a .run_test_cases result file. | |
| 45 | |
| 46 Returns a tuple of two lists, (success, failures). | |
| 47 """ | |
| 48 if not os.path.isfile(run_test_cases_file): | |
| 49 print >> sys.stderr, 'Failed to find %s' % run_test_cases_file | |
| 50 return None, None | |
| 51 with open(run_test_cases_file) as f: | |
| 52 try: | |
| 53 data = json.load(f) | |
| 54 except ValueError as e: | |
| 55 print >> sys.stderr, ('Unable to load json file, %s: %s' % | |
| 56 (run_test_cases_file, str(e))) | |
| 57 return None, None | |
| 58 failure = [ | |
| 59 test for test, runs in data['test_cases'].iteritems() | |
| 60 if not any(run['returncode'] == 0 for run in runs) | |
| 61 ] | |
| 62 success = [ | |
| 63 test for test, runs in data['test_cases'].iteritems() | |
| 64 if any(run['returncode'] == 0 for run in runs) | |
| 65 ] | |
| 66 return success, failure | |
| 67 | |
| 68 | |
| 69 def add_verbosity(cmd, verbosity): | |
| 70 """Adds --verbose flags to |cmd| depending on verbosity.""" | |
| 71 if verbosity: | |
| 72 cmd.append('--verbose') | |
| 73 if verbosity > 1: | |
| 74 cmd.append('--verbose') | |
| 75 | |
| 76 | |
| 77 # This function requires 2 temporary files. | |
| 78 @with_tempfile | |
| 79 @with_tempfile | |
| 80 def run_tests( | |
| 81 tempfilepath_cases, tempfilepath_result, isolated, test_cases, verbosity): | |
| 82 """Runs all the test cases in an isolated environment.""" | |
| 83 with open(tempfilepath_cases, 'w') as f: | |
| 84 f.write('\n'.join(test_cases)) | |
| 85 cmd = [ | |
| 86 sys.executable, os.path.join(ROOT_DIR, 'isolate.py'), | |
| 87 'run', | |
| 88 '--isolated', isolated, | |
| 89 ] | |
| 90 # Make sure isolate.py is verbose. | |
| 91 add_verbosity(cmd, verbosity) | |
| 92 cmd += [ | |
| 93 '--', | |
| 94 # This assumes run_test_cases.py is used. | |
| 95 '--result', tempfilepath_result, | |
| 96 '--test-case-file', tempfilepath_cases, | |
| 97 # Do not retry; it's faster to trace flaky test than retrying each failing | |
| 98 # tests 3 times. | |
| 99 # Do not use --run-all, iterate multiple times instead. | |
| 100 '--retries', '0', | |
| 101 # Trace at most 25 test cases at a time. While this may seem particularly | |
| 102 # small, it is because the tracer doesn't scale well on Windows and tends to | |
| 103 # generate multi-gigabytes data files, that needs to be read N-times the | |
| 104 # number of test cases. On linux and OSX it's not that much a big deal but | |
| 105 # if there's 25 test cases that are broken, it's likely that there's a core | |
| 106 # file missing anyway. | |
| 107 '--max-failures', '25', | |
| 108 ] | |
| 109 # Make sure run_test_cases.py is verbose. | |
| 110 add_verbosity(cmd, verbosity) | |
| 111 logging.debug(cmd) | |
| 112 retcode = subprocess.call(cmd) | |
| 113 success, failures = load_run_test_cases_results(tempfilepath_result) | |
| 114 # Returning non-zero must match having failures. | |
| 115 assert bool(retcode) == (failures is None or bool(failures)) | |
| 116 return success, failures | |
| 117 | |
| 118 | |
| 119 @with_tempfile | |
| 120 def trace_some(tempfilepath, isolated, test_cases, verbosity): | |
| 121 """Traces the test cases.""" | |
| 122 with open(tempfilepath, 'w') as f: | |
| 123 f.write('\n'.join(test_cases)) | |
| 124 cmd = [ | |
| 125 sys.executable, os.path.join(ROOT_DIR, 'isolate_test_cases.py'), | |
| 126 '--isolated', isolated, | |
| 127 '--test-case-file', tempfilepath, | |
| 128 # Do not use --run-all here, we assume the test cases will pass inside the | |
| 129 # checkout. | |
| 130 ] | |
| 131 add_verbosity(cmd, verbosity) | |
| 132 logging.debug(cmd) | |
| 133 return subprocess.call(cmd) | |
| 134 | |
| 135 | |
| 136 def fix_all(isolated, all_test_cases, verbosity): | |
| 137 """Runs all the test cases in a gtest executable and trace the failing tests. | |
| 138 | |
| 139 Returns True on success. | |
| 140 | |
| 141 Makes sure the test passes afterward. | |
| 142 """ | |
| 143 # These environment variables could have adverse side-effects. | |
| 144 # TODO(maruel): Be more intelligent about it, for now be safe. | |
| 145 blacklist = set(run_test_cases.KNOWN_GTEST_ENV_VARS) - set([ | |
| 146 'GTEST_SHARD_INDEX', 'GTEST_TOTAL_SHARDS']) | |
| 147 for i in blacklist: | |
| 148 if i in os.environ: | |
| 149 print >> sys.stderr, 'Please unset %s' % i | |
| 150 return False | |
| 151 | |
| 152 # Run until test cases remain to be tested. | |
| 153 remaining_test_cases = all_test_cases[:] | |
| 154 if not remaining_test_cases: | |
| 155 print >> sys.stderr, 'Didn\'t find any test case to run' | |
| 156 return 1 | |
| 157 | |
| 158 previous_failures = set() | |
| 159 had_failure = False | |
| 160 while remaining_test_cases: | |
| 161 # pylint is confused about with_tempfile. | |
| 162 # pylint: disable=E1120 | |
| 163 print( | |
| 164 '\nTotal: %5d; Remaining: %5d' % ( | |
| 165 len(all_test_cases), len(remaining_test_cases))) | |
| 166 success, failures = run_tests(isolated, remaining_test_cases, verbosity) | |
| 167 if success is None: | |
| 168 if had_failure: | |
| 169 print >> sys.stderr, 'Failed to run test cases' | |
| 170 return 1 | |
| 171 # Maybe there's even enough things mapped to start the child process. | |
| 172 logging.info('Failed to run, trace one test case.') | |
| 173 had_failure = True | |
| 174 success = [] | |
| 175 failures = [remaining_test_cases[0]] | |
| 176 print( | |
| 177 '\nTotal: %5d; Tried to run: %5d; Ran: %5d; Succeeded: %5d; Failed: %5d' | |
| 178 % ( | |
| 179 len(all_test_cases), | |
| 180 len(remaining_test_cases), | |
| 181 len(success) + len(failures), | |
| 182 len(success), | |
| 183 len(failures))) | |
| 184 | |
| 185 if not failures: | |
| 186 print('I\'m done. Have a nice day!') | |
| 187 return True | |
| 188 | |
| 189 previous_failures.difference_update(success) | |
| 190 # If all the failures had already failed at least once. | |
| 191 if previous_failures.issuperset(failures): | |
| 192 print('The last trace didn\'t help, aborting.') | |
| 193 return False | |
| 194 | |
| 195 # Test cases that passed to not need to be retried anymore. | |
| 196 remaining_test_cases = [ | |
| 197 i for i in remaining_test_cases if i not in success and i not in failures | |
| 198 ] | |
| 199 # Make sure the failures at put at the end. This way if some tests fails | |
| 200 # simply because they are broken, and not because of test isolation, the | |
| 201 # other tests will still be traced. | |
| 202 remaining_test_cases.extend(failures) | |
| 203 | |
| 204 # Trace the test cases and update the .isolate file. | |
| 205 print('\nTracing the %d failing tests.' % len(failures)) | |
| 206 if trace_some(isolated, failures, verbosity): | |
| 207 logging.info('The tracing itself failed.') | |
| 208 return False | |
| 209 previous_failures.update(failures) | |
| 210 | |
| 211 | |
| 212 def main(): | |
| 213 run_test_cases.run_isolated.disable_buffering() | |
| 214 parser = run_test_cases.OptionParserTestCases( | |
| 215 usage='%prog <options> -s <something.isolated>') | |
| 216 parser.add_option( | |
| 217 '-s', '--isolated', | |
| 218 help='The isolated file') | |
| 219 options, args = parser.parse_args() | |
| 220 | |
| 221 if args: | |
| 222 parser.error('Unsupported arg: %s' % args) | |
| 223 isolate.parse_isolated_option(parser, options, os.getcwd(), True) | |
| 224 | |
| 225 _, command, test_cases = isolate_test_cases.safely_load_isolated( | |
| 226 parser, options) | |
| 227 if not command: | |
| 228 parser.error('A command must be defined') | |
| 229 if not test_cases: | |
| 230 parser.error('No test case to run') | |
| 231 return not fix_all(options.isolated, test_cases, options.verbose) | |
| 232 | |
| 233 | |
| 234 if __name__ == '__main__': | |
| 235 sys.exit(main()) | |
| OLD | NEW |