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