OLD | NEW |
---|---|
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved. | 2 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 | 6 |
7 """Parses and displays the contents of one or more autoserv result directories. | 7 """Parses and displays the contents of one or more autoserv result directories. |
8 | 8 |
9 This script parses the contents of one or more autoserv results folders and | 9 This script parses the contents of one or more autoserv results folders and |
10 generates test reports. | 10 generates test reports. |
(...skipping 10 matching lines...) Expand all Loading... | |
21 sys.path.append(constants.CROSUTILS_LIB_DIR) | 21 sys.path.append(constants.CROSUTILS_LIB_DIR) |
22 from cros_build_lib import Color, Die | 22 from cros_build_lib import Color, Die |
23 | 23 |
24 _STDOUT_IS_TTY = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() | 24 _STDOUT_IS_TTY = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() |
25 | 25 |
26 # List of crashes which are okay to ignore. This list should almost always be | 26 # List of crashes which are okay to ignore. This list should almost always be |
27 # empty. If you add an entry, mark it with a TODO(<your name>) and the issue | 27 # empty. If you add an entry, mark it with a TODO(<your name>) and the issue |
28 # filed for the crash. | 28 # filed for the crash. |
29 _CRASH_WHITELIST = {} | 29 _CRASH_WHITELIST = {} |
30 | 30 |
31 class ReportGenerator(object): | |
32 """Collects and displays data from autoserv results directories. | |
33 | 31 |
34 This class collects status and performance data from one or more autoserv | 32 class ResultCollector(object): |
35 result directories and generates test reports. | 33 """Collects status and performance data from an autoserv results directory.""" |
36 """ | |
37 | 34 |
38 _KEYVAL_INDENT = 2 | 35 def __init__(self, collect_perf=True, strip_text=''): |
36 """Initialize ResultsCollector class. | |
39 | 37 |
40 def __init__(self, options, args): | 38 Args: |
41 self._options = options | 39 collect_perf: Should perf keyvals be collected? |
42 self._args = args | 40 strip_text: Prefix to strip from test directory names. |
43 self._color = Color(options.color) | 41 """ |
42 self._collect_perf = collect_perf | |
43 self._strip_text = strip_text | |
44 | 44 |
45 def _CollectPerf(self, testdir): | 45 def _CollectPerf(self, testdir): |
46 """Parses keyval file under testdir. | 46 """Parses keyval file under testdir. |
47 | 47 |
48 If testdir contains a result folder, process the keyval file and return | 48 If testdir contains a result folder, process the keyval file and return |
49 a dictionary of perf keyval pairs. | 49 a dictionary of perf keyval pairs. |
50 | 50 |
51 Args: | 51 Args: |
52 testdir: The autoserv test result directory. | 52 testdir: The autoserv test result directory. |
53 | 53 |
54 Returns: | 54 Returns: |
55 If the perf option is disabled or the there's no keyval file under | 55 If the perf option is disabled or the there's no keyval file under |
56 testdir, returns an empty dictionary. Otherwise, returns a dictionary of | 56 testdir, returns an empty dictionary. Otherwise, returns a dictionary of |
57 parsed keyvals. Duplicate keys are uniquified by their instance number. | 57 parsed keyvals. Duplicate keys are uniquified by their instance number. |
58 """ | 58 """ |
59 | 59 |
60 perf = {} | 60 perf = {} |
61 if not self._options.perf: | 61 if not self._collect_perf: |
62 return perf | 62 return perf |
63 | 63 |
64 keyval_file = os.path.join(testdir, 'results', 'keyval') | 64 keyval_file = os.path.join(testdir, 'results', 'keyval') |
65 if not os.path.isfile(keyval_file): | 65 if not os.path.isfile(keyval_file): |
66 return perf | 66 return perf |
67 | 67 |
68 instances = {} | 68 instances = {} |
69 | 69 |
70 for line in open(keyval_file): | 70 for line in open(keyval_file): |
71 match = re.search(r'^(.+){perf}=(.+)$', line) | 71 match = re.search(r'^(.+){perf}=(.+)$', line) |
72 if match: | 72 if match: |
73 key = match.group(1) | 73 key = match.group(1) |
74 val = match.group(2) | 74 val = match.group(2) |
75 | 75 |
76 # If the same key name was generated multiple times, uniquify all | 76 # If the same key name was generated multiple times, uniquify all |
77 # instances other than the first one by adding the instance count | 77 # instances other than the first one by adding the instance count |
78 # to the key name. | 78 # to the key name. |
79 key_inst = key | 79 key_inst = key |
80 instance = instances.get(key, 0) | 80 instance = instances.get(key, 0) |
81 if instance: | 81 if instance: |
82 key_inst = '%s{%d}' % (key, instance) | 82 key_inst = '%s{%d}' % (key, instance) |
83 instances[key] = instance + 1 | 83 instances[key] = instance + 1 |
84 | 84 |
85 perf[key_inst] = val | 85 perf[key_inst] = val |
86 | 86 |
87 return perf | 87 return perf |
88 | 88 |
89 def _CollectResult(self, testdir): | 89 def _CollectResult(self, testdir, results): |
90 """Adds results stored under testdir to the self._results dictionary. | 90 """Adds results stored under testdir to the self._results dictionary. |
91 | 91 |
92 If testdir contains 'status.log' or 'status' files, assume it's a test | 92 If testdir contains 'status.log' or 'status' files, assume it's a test |
93 result directory and add the results data to the self._results dictionary. | 93 result directory and add the results data to the self._results dictionary. |
94 The test directory name is used as a key into the results dictionary. | 94 The test directory name is used as a key into the results dictionary. |
95 | 95 |
96 Args: | 96 Args: |
97 testdir: The autoserv test result directory. | 97 testdir: The autoserv test result directory. |
98 results: Results dictionary to store results in. | |
98 """ | 99 """ |
99 | 100 |
100 status_file = os.path.join(testdir, 'status.log') | 101 status_file = os.path.join(testdir, 'status.log') |
101 if not os.path.isfile(status_file): | 102 if not os.path.isfile(status_file): |
102 status_file = os.path.join(testdir, 'status') | 103 status_file = os.path.join(testdir, 'status') |
103 if not os.path.isfile(status_file): | 104 if not os.path.isfile(status_file): |
104 return | 105 return |
105 | 106 |
106 # Remove false positives that are missing a debug dir. | |
107 if not os.path.exists(os.path.join(testdir, 'debug')): | |
108 return | |
sosa
2011/03/31 20:03:34
Why move this into the caller but not the above?
DaleCurtis
2011/03/31 20:17:43
Just checking for the debug directory prunes every
| |
109 | |
110 status_raw = open(status_file, 'r').read() | 107 status_raw = open(status_file, 'r').read() |
111 status = 'FAIL' | 108 status = 'FAIL' |
112 if (re.search(r'GOOD.+completed successfully', status_raw) and | 109 if (re.search(r'GOOD.+completed successfully', status_raw) and |
113 not re.search(r'ABORT|ERROR|FAIL|TEST_NA', status_raw)): | 110 not re.search(r'ABORT|ERROR|FAIL|TEST_NA', status_raw)): |
114 status = 'PASS' | 111 status = 'PASS' |
115 | 112 |
116 perf = self._CollectPerf(testdir) | 113 perf = self._CollectPerf(testdir) |
117 | 114 |
118 if testdir.startswith(self._options.strip): | 115 if testdir.startswith(self._strip_text): |
119 testdir = testdir.replace(self._options.strip, '', 1) | 116 testdir = testdir.replace(self._strip_text, '', 1) |
120 | 117 |
121 crashes = [] | 118 crashes = [] |
122 regex = re.compile('Received crash notification for ([-\w]+).+ (sig \d+)') | 119 regex = re.compile('Received crash notification for ([-\w]+).+ (sig \d+)') |
123 for match in regex.finditer(status_raw): | 120 for match in regex.finditer(status_raw): |
124 if (match.group(1) in _CRASH_WHITELIST and | 121 if (match.group(1) in _CRASH_WHITELIST and |
125 match.group(2) in _CRASH_WHITELIST[match.group(1)]): | 122 match.group(2) in _CRASH_WHITELIST[match.group(1)]): |
126 continue | 123 continue |
127 crashes.append('%s %s' % match.groups()) | 124 crashes.append('%s %s' % match.groups()) |
128 | 125 |
129 self._results[testdir] = {'crashes': crashes, | 126 results[testdir] = {'crashes': crashes, 'status': status, 'perf': perf} |
130 'status': status, | |
131 'perf': perf} | |
132 | 127 |
133 def _CollectResultsRec(self, resdir): | 128 def CollectResults(self, resdir): |
134 """Recursively collect results into the self._results dictionary. | 129 """Recursively collect results into a dictionary. |
135 | 130 |
136 Args: | 131 Args: |
137 resdir: results/test directory to parse results from and recurse into. | 132 resdir: results/test directory to parse results from and recurse into. |
133 | |
134 Returns: | |
135 Dictionary of results. | |
138 """ | 136 """ |
137 results = {} | |
138 self._CollectResult(resdir, results) | |
139 for testdir in glob.glob(os.path.join(resdir, '*')): | |
140 # Remove false positives that are missing a debug dir. | |
141 if not os.path.exists(os.path.join(testdir, 'debug')): | |
142 continue | |
139 | 143 |
140 self._CollectResult(resdir) | 144 results.update(self.CollectResults(testdir)) |
141 for testdir in glob.glob(os.path.join(resdir, '*')): | 145 return results |
142 self._CollectResultsRec(testdir) | 146 |
147 | |
148 class ReportGenerator(object): | |
149 """Collects and displays data from autoserv results directories. | |
150 | |
151 This class collects status and performance data from one or more autoserv | |
152 result directories and generates test reports. | |
153 """ | |
154 | |
155 _KEYVAL_INDENT = 2 | |
156 | |
157 def __init__(self, options, args): | |
158 self._options = options | |
159 self._args = args | |
160 self._color = Color(options.color) | |
143 | 161 |
144 def _CollectResults(self): | 162 def _CollectResults(self): |
145 """Parses results into the self._results dictionary. | 163 """Parses results into the self._results dictionary. |
146 | 164 |
147 Initializes a dictionary (self._results) with test folders as keys and | 165 Initializes a dictionary (self._results) with test folders as keys and |
148 result data (status, perf keyvals) as values. | 166 result data (status, perf keyvals) as values. |
149 """ | 167 """ |
150 self._results = {} | 168 self._results = {} |
169 collector = ResultCollector(self._options.perf, self._options.strip) | |
151 for resdir in self._args: | 170 for resdir in self._args: |
152 if not os.path.isdir(resdir): | 171 if not os.path.isdir(resdir): |
153 Die('\'%s\' does not exist' % resdir) | 172 Die('\'%s\' does not exist' % resdir) |
154 self._CollectResultsRec(resdir) | 173 self._results.update(collector.CollectResults(resdir)) |
155 | 174 |
156 if not self._results: | 175 if not self._results: |
157 Die('no test directories found') | 176 Die('no test directories found') |
158 | 177 |
159 def GetTestColumnWidth(self): | 178 def _GetTestColumnWidth(self): |
160 """Returns the test column width based on the test data. | 179 """Returns the test column width based on the test data. |
161 | 180 |
162 Aligns the test results by formatting the test directory entry based on | 181 Aligns the test results by formatting the test directory entry based on |
163 the longest test directory or perf key string stored in the self._results | 182 the longest test directory or perf key string stored in the self._results |
164 dictionary. | 183 dictionary. |
165 | 184 |
166 Returns: | 185 Returns: |
167 The width for the test columnt. | 186 The width for the test columnt. |
168 """ | 187 """ |
169 width = len(max(self._results, key=len)) | 188 width = len(max(self._results, key=len)) |
(...skipping 10 matching lines...) Expand all Loading... | |
180 Prints a result table to stdout. Each row of the table contains the test | 199 Prints a result table to stdout. Each row of the table contains the test |
181 result directory and the test result (PASS, FAIL). If the perf option is | 200 result directory and the test result (PASS, FAIL). If the perf option is |
182 enabled, each test entry is followed by perf keyval entries from the test | 201 enabled, each test entry is followed by perf keyval entries from the test |
183 results. | 202 results. |
184 """ | 203 """ |
185 tests = self._results.keys() | 204 tests = self._results.keys() |
186 tests.sort() | 205 tests.sort() |
187 | 206 |
188 tests_with_errors = [] | 207 tests_with_errors = [] |
189 | 208 |
190 width = self.GetTestColumnWidth() | 209 width = self._GetTestColumnWidth() |
191 line = ''.ljust(width + 5, '-') | 210 line = ''.ljust(width + 5, '-') |
192 | 211 |
193 crashes = {} | 212 crashes = {} |
194 tests_pass = 0 | 213 tests_pass = 0 |
195 print line | 214 print line |
196 for test in tests: | 215 for test in tests: |
197 # Emit the test/status entry first | 216 # Emit the test/status entry first |
198 test_entry = test.ljust(width) | 217 test_entry = test.ljust(width) |
199 result = self._results[test] | 218 result = self._results[test] |
200 status_entry = result['status'] | 219 status_entry = result['status'] |
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
250 print 'Total unique crashes: ' + self._color.Color(Color.BOLD, | 269 print 'Total unique crashes: ' + self._color.Color(Color.BOLD, |
251 str(len(crashes))) | 270 str(len(crashes))) |
252 else: | 271 else: |
253 print self._color.Color(Color.GREEN, | 272 print self._color.Color(Color.GREEN, |
254 'No crashes detected during testing.') | 273 'No crashes detected during testing.') |
255 | 274 |
256 # Print out error log for failed tests. | 275 # Print out error log for failed tests. |
257 if self._options.print_debug: | 276 if self._options.print_debug: |
258 for test in tests_with_errors: | 277 for test in tests_with_errors: |
259 debug_file_regex = os.path.join(self._options.strip, test, 'debug', | 278 debug_file_regex = os.path.join(self._options.strip, test, 'debug', |
260 '%s*.ERROR' % os.path.basename(test)) | 279 '%s*.ERROR' % os.path.basename(test)) |
261 for path in glob.glob(debug_file_regex): | 280 for path in glob.glob(debug_file_regex): |
262 try: | 281 try: |
263 fh = open(path) | 282 fh = open(path) |
264 print >> sys.stderr, ( | 283 print >> sys.stderr, ( |
265 '\n========== ERROR FILE %s FOR TEST %s ==============\n' % ( | 284 '\n========== ERROR FILE %s FOR TEST %s ==============\n' % ( |
266 path, test)) | 285 path, test)) |
267 out = fh.read() | 286 out = fh.read() |
268 while out: | 287 while out: |
269 print >> sys.stderr, out | 288 print >> sys.stderr, out |
270 out = fh.read() | 289 out = fh.read() |
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
319 if not args: | 338 if not args: |
320 parser.print_help() | 339 parser.print_help() |
321 Die('no result directories provided') | 340 Die('no result directories provided') |
322 | 341 |
323 generator = ReportGenerator(options, args) | 342 generator = ReportGenerator(options, args) |
324 generator.Run() | 343 generator.Run() |
325 | 344 |
326 | 345 |
327 if __name__ == '__main__': | 346 if __name__ == '__main__': |
328 main() | 347 main() |
OLD | NEW |