| 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 """Traces each test cases of a google-test executable individually. | |
| 7 | |
| 8 Gives detailed information about each test case. The logs can be read afterward | |
| 9 with ./trace_inputs.py read -l /path/to/executable.logs | |
| 10 """ | |
| 11 | |
| 12 import logging | |
| 13 import multiprocessing | |
| 14 import os | |
| 15 import re | |
| 16 import sys | |
| 17 import time | |
| 18 | |
| 19 import run_test_cases | |
| 20 import trace_inputs | |
| 21 | |
| 22 BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| 23 ROOT_DIR = os.path.dirname(os.path.dirname(BASE_DIR)) | |
| 24 | |
| 25 | |
| 26 def sanitize_test_case_name(test_case): | |
| 27 """Removes characters that are valid as test case names but invalid as file | |
| 28 names. | |
| 29 """ | |
| 30 return test_case.replace('/', '-') | |
| 31 | |
| 32 | |
| 33 class Tracer(object): | |
| 34 def __init__(self, tracer, cmd, cwd_dir, progress): | |
| 35 # Constants | |
| 36 self.tracer = tracer | |
| 37 self.cmd = cmd[:] | |
| 38 self.cwd_dir = cwd_dir | |
| 39 self.progress = progress | |
| 40 | |
| 41 def map(self, test_case): | |
| 42 """Traces a single test case and returns its output.""" | |
| 43 cmd = self.cmd[:] | |
| 44 cmd.append('--gtest_filter=%s' % test_case) | |
| 45 tracename = sanitize_test_case_name(test_case) | |
| 46 | |
| 47 out = [] | |
| 48 for retry in range(5): | |
| 49 start = time.time() | |
| 50 returncode, output = self.tracer.trace(cmd, self.cwd_dir, tracename, True) | |
| 51 duration = time.time() - start | |
| 52 # TODO(maruel): Define a way to detect if a strace log is valid. | |
| 53 valid = True | |
| 54 out.append( | |
| 55 { | |
| 56 'test_case': test_case, | |
| 57 'tracename': tracename, | |
| 58 'returncode': returncode, | |
| 59 'duration': duration, | |
| 60 'valid': valid, | |
| 61 'output': output, | |
| 62 }) | |
| 63 logging.debug( | |
| 64 'Tracing %s done: %d, %.1fs' % (test_case, returncode, duration)) | |
| 65 if retry: | |
| 66 self.progress.update_item( | |
| 67 '%s - %d' % (test_case, retry), True, not valid) | |
| 68 else: | |
| 69 self.progress.update_item(test_case, True, not valid) | |
| 70 if valid: | |
| 71 break | |
| 72 return out | |
| 73 | |
| 74 | |
| 75 def trace_test_cases(cmd, cwd_dir, test_cases, jobs, logname): | |
| 76 """Traces each test cases individually but all in parallel.""" | |
| 77 assert os.path.isabs(cwd_dir) and os.path.isdir(cwd_dir), cwd_dir | |
| 78 | |
| 79 if not test_cases: | |
| 80 return [] | |
| 81 | |
| 82 # Resolve any symlink. | |
| 83 cwd_dir = os.path.realpath(cwd_dir) | |
| 84 assert os.path.isdir(cwd_dir) | |
| 85 | |
| 86 api = trace_inputs.get_api() | |
| 87 api.clean_trace(logname) | |
| 88 | |
| 89 jobs = jobs or multiprocessing.cpu_count() | |
| 90 # Try to do black magic here by guessing a few of the run_test_cases.py | |
| 91 # flags. It's cheezy but it works. | |
| 92 for i, v in enumerate(cmd): | |
| 93 if v.endswith('run_test_cases.py'): | |
| 94 # Found it. Process the arguments here. | |
| 95 _, options, _ = run_test_cases.process_args(cmd[i:]) | |
| 96 # Always override with the lowest value. | |
| 97 jobs = min(options.jobs, jobs) | |
| 98 break | |
| 99 | |
| 100 progress = run_test_cases.Progress(len(test_cases)) | |
| 101 with run_test_cases.ThreadPool(progress, jobs, jobs, | |
| 102 len(test_cases)) as pool: | |
| 103 with api.get_tracer(logname) as tracer: | |
| 104 function = Tracer(tracer, cmd, cwd_dir, progress).map | |
| 105 for test_case in test_cases: | |
| 106 pool.add_task(0, function, test_case) | |
| 107 | |
| 108 results = pool.join() | |
| 109 print('') | |
| 110 return results | |
| 111 | |
| 112 | |
| 113 def write_details(logname, outfile, root_dir, blacklist, results): | |
| 114 """Writes an .test_cases file with all the information about each test | |
| 115 case. | |
| 116 """ | |
| 117 api = trace_inputs.get_api() | |
| 118 logs = dict( | |
| 119 (i.pop('trace'), i) for i in api.parse_log(logname, blacklist, None)) | |
| 120 results_processed = {} | |
| 121 exception = None | |
| 122 for items in results: | |
| 123 item = items[-1] | |
| 124 assert item['valid'] | |
| 125 # Load the results; | |
| 126 log_dict = logs[item['tracename']] | |
| 127 if log_dict.get('exception'): | |
| 128 exception = exception or log_dict['exception'] | |
| 129 continue | |
| 130 trace_result = log_dict['results'] | |
| 131 if root_dir: | |
| 132 trace_result = trace_result.strip_root(root_dir) | |
| 133 results_processed[item['test_case']] = { | |
| 134 'trace': trace_result.flatten(), | |
| 135 'duration': item['duration'], | |
| 136 'output': item['output'], | |
| 137 'returncode': item['returncode'], | |
| 138 } | |
| 139 | |
| 140 # Make it dense if there is more than 20 results. | |
| 141 trace_inputs.write_json( | |
| 142 outfile, | |
| 143 results_processed, | |
| 144 len(results_processed) > 20) | |
| 145 if exception: | |
| 146 raise exception[0], exception[1], exception[2] | |
| 147 | |
| 148 | |
| 149 def main(): | |
| 150 """CLI frontend to validate arguments.""" | |
| 151 run_test_cases.run_isolated.disable_buffering() | |
| 152 parser = run_test_cases.OptionParserTestCases( | |
| 153 usage='%prog <options> [gtest]') | |
| 154 parser.format_description = lambda *_: parser.description | |
| 155 parser.add_option( | |
| 156 '-o', '--out', | |
| 157 help='output file, defaults to <executable>.test_cases') | |
| 158 parser.add_option( | |
| 159 '-r', '--root-dir', | |
| 160 help='Root directory under which file access should be noted') | |
| 161 # TODO(maruel): Add support for options.timeout. | |
| 162 parser.remove_option('--timeout') | |
| 163 options, args = parser.parse_args() | |
| 164 | |
| 165 if not args: | |
| 166 parser.error( | |
| 167 'Please provide the executable line to run, if you need fancy things ' | |
| 168 'like xvfb, start this script from *inside* xvfb, it\'ll be much faster' | |
| 169 '.') | |
| 170 | |
| 171 cmd = run_test_cases.fix_python_path(args) | |
| 172 cmd[0] = os.path.abspath(cmd[0]) | |
| 173 if not os.path.isfile(cmd[0]): | |
| 174 parser.error('Tracing failed for: %s\nIt doesn\'t exit' % ' '.join(cmd)) | |
| 175 | |
| 176 if not options.out: | |
| 177 options.out = '%s.test_cases' % cmd[-1] | |
| 178 options.out = os.path.abspath(options.out) | |
| 179 if options.root_dir: | |
| 180 options.root_dir = os.path.abspath(options.root_dir) | |
| 181 logname = options.out + '.log' | |
| 182 | |
| 183 def blacklist(f): | |
| 184 return any(re.match(b, f) for b in options.blacklist) | |
| 185 | |
| 186 test_cases = parser.process_gtest_options(cmd, os.getcwd(), options) | |
| 187 | |
| 188 # Then run them. | |
| 189 print('Tracing...') | |
| 190 results = trace_test_cases( | |
| 191 cmd, | |
| 192 os.getcwd(), | |
| 193 test_cases, | |
| 194 options.jobs, | |
| 195 logname) | |
| 196 print('Reading trace logs...') | |
| 197 write_details(logname, options.out, options.root_dir, blacklist, results) | |
| 198 return 0 | |
| 199 | |
| 200 | |
| 201 if __name__ == '__main__': | |
| 202 sys.exit(main()) | |
| OLD | NEW |