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 |