Chromium Code Reviews| 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 |