Mercurial > hg > CbC > CbC_gcc
comparison contrib/dg-extract-results.py @ 111:04ced10e8804
gcc 7
author | kono |
---|---|
date | Fri, 27 Oct 2017 22:46:09 +0900 |
parents | |
children | 1830386684a0 |
comparison
equal
deleted
inserted
replaced
68:561a7518be6b | 111:04ced10e8804 |
---|---|
1 #!/usr/bin/python | |
2 # | |
3 # Copyright (C) 2014 Free Software Foundation, Inc. | |
4 # | |
5 # This script is free software; you can redistribute it and/or modify | |
6 # it under the terms of the GNU General Public License as published by | |
7 # the Free Software Foundation; either version 3, or (at your option) | |
8 # any later version. | |
9 | |
10 import sys | |
11 import getopt | |
12 import re | |
13 import io | |
14 from datetime import datetime | |
15 from operator import attrgetter | |
16 | |
17 # True if unrecognised lines should cause a fatal error. Might want to turn | |
18 # this on by default later. | |
19 strict = False | |
20 | |
21 # True if the order of .log segments should match the .sum file, false if | |
22 # they should keep the original order. | |
23 sort_logs = True | |
24 | |
25 # A version of open() that is safe against whatever binary output | |
26 # might be added to the log. | |
27 def safe_open (filename): | |
28 if sys.version_info >= (3, 0): | |
29 return open (filename, 'r', errors = 'surrogateescape') | |
30 return open (filename, 'r') | |
31 | |
32 # Force stdout to handle escape sequences from a safe_open file. | |
33 if sys.version_info >= (3, 0): | |
34 sys.stdout = io.TextIOWrapper (sys.stdout.buffer, | |
35 errors = 'surrogateescape') | |
36 | |
37 class Named: | |
38 def __init__ (self, name): | |
39 self.name = name | |
40 | |
41 class ToolRun (Named): | |
42 def __init__ (self, name): | |
43 Named.__init__ (self, name) | |
44 # The variations run for this tool, mapped by --target_board name. | |
45 self.variations = dict() | |
46 | |
47 # Return the VariationRun for variation NAME. | |
48 def get_variation (self, name): | |
49 if name not in self.variations: | |
50 self.variations[name] = VariationRun (name) | |
51 return self.variations[name] | |
52 | |
53 class VariationRun (Named): | |
54 def __init__ (self, name): | |
55 Named.__init__ (self, name) | |
56 # A segment of text before the harness runs start, describing which | |
57 # baseboard files were loaded for the target. | |
58 self.header = None | |
59 # The harnesses run for this variation, mapped by filename. | |
60 self.harnesses = dict() | |
61 # A list giving the number of times each type of result has | |
62 # been seen. | |
63 self.counts = [] | |
64 | |
65 # Return the HarnessRun for harness NAME. | |
66 def get_harness (self, name): | |
67 if name not in self.harnesses: | |
68 self.harnesses[name] = HarnessRun (name) | |
69 return self.harnesses[name] | |
70 | |
71 class HarnessRun (Named): | |
72 def __init__ (self, name): | |
73 Named.__init__ (self, name) | |
74 # Segments of text that make up the harness run, mapped by a test-based | |
75 # key that can be used to order them. | |
76 self.segments = dict() | |
77 # Segments of text that make up the harness run but which have | |
78 # no recognized test results. These are typically harnesses that | |
79 # are completely skipped for the target. | |
80 self.empty = [] | |
81 # A list of results. Each entry is a pair in which the first element | |
82 # is a unique sorting key and in which the second is the full | |
83 # PASS/FAIL line. | |
84 self.results = [] | |
85 | |
86 # Add a segment of text to the harness run. If the segment includes | |
87 # test results, KEY is an example of one of them, and can be used to | |
88 # combine the individual segments in order. If the segment has no | |
89 # test results (e.g. because the harness doesn't do anything for the | |
90 # current configuration) then KEY is None instead. In that case | |
91 # just collect the segments in the order that we see them. | |
92 def add_segment (self, key, segment): | |
93 if key: | |
94 assert key not in self.segments | |
95 self.segments[key] = segment | |
96 else: | |
97 self.empty.append (segment) | |
98 | |
99 class Segment: | |
100 def __init__ (self, filename, start): | |
101 self.filename = filename | |
102 self.start = start | |
103 self.lines = 0 | |
104 | |
105 class Prog: | |
106 def __init__ (self): | |
107 # The variations specified on the command line. | |
108 self.variations = [] | |
109 # The variations seen in the input files. | |
110 self.known_variations = set() | |
111 # The tools specified on the command line. | |
112 self.tools = [] | |
113 # Whether to create .sum rather than .log output. | |
114 self.do_sum = True | |
115 # Regexps used while parsing. | |
116 self.test_run_re = re.compile (r'^Test Run By (\S+) on (.*)$') | |
117 self.tool_re = re.compile (r'^\t\t=== (.*) tests ===$') | |
118 self.result_re = re.compile (r'^(PASS|XPASS|FAIL|XFAIL|UNRESOLVED' | |
119 r'|WARNING|ERROR|UNSUPPORTED|UNTESTED' | |
120 r'|KFAIL):\s*(.+)') | |
121 self.completed_re = re.compile (r'.* completed at (.*)') | |
122 # Pieces of text to write at the head of the output. | |
123 # start_line is a pair in which the first element is a datetime | |
124 # and in which the second is the associated 'Test Run By' line. | |
125 self.start_line = None | |
126 self.native_line = '' | |
127 self.target_line = '' | |
128 self.host_line = '' | |
129 self.acats_premable = '' | |
130 # Pieces of text to write at the end of the output. | |
131 # end_line is like start_line but for the 'runtest completed' line. | |
132 self.acats_failures = [] | |
133 self.version_output = '' | |
134 self.end_line = None | |
135 # Known summary types. | |
136 self.count_names = [ | |
137 '# of DejaGnu errors\t\t', | |
138 '# of expected passes\t\t', | |
139 '# of unexpected failures\t', | |
140 '# of unexpected successes\t', | |
141 '# of expected failures\t\t', | |
142 '# of unknown successes\t\t', | |
143 '# of known failures\t\t', | |
144 '# of untested testcases\t\t', | |
145 '# of unresolved testcases\t', | |
146 '# of unsupported tests\t\t' | |
147 ] | |
148 self.runs = dict() | |
149 | |
150 def usage (self): | |
151 name = sys.argv[0] | |
152 sys.stderr.write ('Usage: ' + name | |
153 + ''' [-t tool] [-l variant-list] [-L] log-or-sum-file ... | |
154 | |
155 tool The tool (e.g. g++, libffi) for which to create a | |
156 new test summary file. If not specified then output | |
157 is created for all tools. | |
158 variant-list One or more test variant names. If the list is | |
159 not specified then one is constructed from all | |
160 variants in the files for <tool>. | |
161 sum-file A test summary file with the format of those | |
162 created by runtest from DejaGnu. | |
163 If -L is used, merge *.log files instead of *.sum. In this | |
164 mode the exact order of lines may not be preserved, just different | |
165 Running *.exp chunks should be in correct order. | |
166 ''') | |
167 sys.exit (1) | |
168 | |
169 def fatal (self, what, string): | |
170 if not what: | |
171 what = sys.argv[0] | |
172 sys.stderr.write (what + ': ' + string + '\n') | |
173 sys.exit (1) | |
174 | |
175 # Parse the command-line arguments. | |
176 def parse_cmdline (self): | |
177 try: | |
178 (options, self.files) = getopt.getopt (sys.argv[1:], 'l:t:L') | |
179 if len (self.files) == 0: | |
180 self.usage() | |
181 for (option, value) in options: | |
182 if option == '-l': | |
183 self.variations.append (value) | |
184 elif option == '-t': | |
185 self.tools.append (value) | |
186 else: | |
187 self.do_sum = False | |
188 except getopt.GetoptError as e: | |
189 self.fatal (None, e.msg) | |
190 | |
191 # Try to parse time string TIME, returning an arbitrary time on failure. | |
192 # Getting this right is just a nice-to-have so failures should be silent. | |
193 def parse_time (self, time): | |
194 try: | |
195 return datetime.strptime (time, '%c') | |
196 except ValueError: | |
197 return datetime.now() | |
198 | |
199 # Parse an integer and abort on failure. | |
200 def parse_int (self, filename, value): | |
201 try: | |
202 return int (value) | |
203 except ValueError: | |
204 self.fatal (filename, 'expected an integer, got: ' + value) | |
205 | |
206 # Return a list that represents no test results. | |
207 def zero_counts (self): | |
208 return [0 for x in self.count_names] | |
209 | |
210 # Return the ToolRun for tool NAME. | |
211 def get_tool (self, name): | |
212 if name not in self.runs: | |
213 self.runs[name] = ToolRun (name) | |
214 return self.runs[name] | |
215 | |
216 # Add the result counts in list FROMC to TOC. | |
217 def accumulate_counts (self, toc, fromc): | |
218 for i in range (len (self.count_names)): | |
219 toc[i] += fromc[i] | |
220 | |
221 # Parse the list of variations after 'Schedule of variations:'. | |
222 # Return the number seen. | |
223 def parse_variations (self, filename, file): | |
224 num_variations = 0 | |
225 while True: | |
226 line = file.readline() | |
227 if line == '': | |
228 self.fatal (filename, 'could not parse variation list') | |
229 if line == '\n': | |
230 break | |
231 self.known_variations.add (line.strip()) | |
232 num_variations += 1 | |
233 return num_variations | |
234 | |
235 # Parse from the first line after 'Running target ...' to the end | |
236 # of the run's summary. | |
237 def parse_run (self, filename, file, tool, variation, num_variations): | |
238 header = None | |
239 harness = None | |
240 segment = None | |
241 final_using = 0 | |
242 | |
243 # If this is the first run for this variation, add any text before | |
244 # the first harness to the header. | |
245 if not variation.header: | |
246 segment = Segment (filename, file.tell()) | |
247 variation.header = segment | |
248 | |
249 # Parse the rest of the summary (the '# of ' lines). | |
250 if len (variation.counts) == 0: | |
251 variation.counts = self.zero_counts() | |
252 | |
253 # Parse up until the first line of the summary. | |
254 if num_variations == 1: | |
255 end = '\t\t=== ' + tool.name + ' Summary ===\n' | |
256 else: | |
257 end = ('\t\t=== ' + tool.name + ' Summary for ' | |
258 + variation.name + ' ===\n') | |
259 while True: | |
260 line = file.readline() | |
261 if line == '': | |
262 self.fatal (filename, 'no recognised summary line') | |
263 if line == end: | |
264 break | |
265 | |
266 # Look for the start of a new harness. | |
267 if line.startswith ('Running ') and line.endswith (' ...\n'): | |
268 # Close off the current harness segment, if any. | |
269 if harness: | |
270 segment.lines -= final_using | |
271 harness.add_segment (first_key, segment) | |
272 name = line[len ('Running '):-len(' ...\n')] | |
273 harness = variation.get_harness (name) | |
274 segment = Segment (filename, file.tell()) | |
275 first_key = None | |
276 final_using = 0 | |
277 continue | |
278 | |
279 # Record test results. Associate the first test result with | |
280 # the harness segment, so that if a run for a particular harness | |
281 # has been split up, we can reassemble the individual segments | |
282 # in a sensible order. | |
283 # | |
284 # dejagnu sometimes issues warnings about the testing environment | |
285 # before running any tests. Treat them as part of the header | |
286 # rather than as a test result. | |
287 match = self.result_re.match (line) | |
288 if match and (harness or not line.startswith ('WARNING:')): | |
289 if not harness: | |
290 self.fatal (filename, 'saw test result before harness name') | |
291 name = match.group (2) | |
292 # Ugly hack to get the right order for gfortran. | |
293 if name.startswith ('gfortran.dg/g77/'): | |
294 name = 'h' + name | |
295 key = (name, len (harness.results)) | |
296 harness.results.append ((key, line)) | |
297 if not first_key and sort_logs: | |
298 first_key = key | |
299 if line.startswith ('ERROR: (DejaGnu)'): | |
300 for i in range (len (self.count_names)): | |
301 if 'DejaGnu errors' in self.count_names[i]: | |
302 variation.counts[i] += 1 | |
303 break | |
304 | |
305 # 'Using ...' lines are only interesting in a header. Splitting | |
306 # the test up into parallel runs leads to more 'Using ...' lines | |
307 # than there would be in a single log. | |
308 if line.startswith ('Using '): | |
309 final_using += 1 | |
310 else: | |
311 final_using = 0 | |
312 | |
313 # Add other text to the current segment, if any. | |
314 if segment: | |
315 segment.lines += 1 | |
316 | |
317 # Close off the final harness segment, if any. | |
318 if harness: | |
319 segment.lines -= final_using | |
320 harness.add_segment (first_key, segment) | |
321 | |
322 while True: | |
323 before = file.tell() | |
324 line = file.readline() | |
325 if line == '': | |
326 break | |
327 if line == '\n': | |
328 continue | |
329 if not line.startswith ('# '): | |
330 file.seek (before) | |
331 break | |
332 found = False | |
333 for i in range (len (self.count_names)): | |
334 if line.startswith (self.count_names[i]): | |
335 count = line[len (self.count_names[i]):-1].strip() | |
336 variation.counts[i] += self.parse_int (filename, count) | |
337 found = True | |
338 break | |
339 if not found: | |
340 self.fatal (filename, 'unknown test result: ' + line[:-1]) | |
341 | |
342 # Parse an acats run, which uses a different format from dejagnu. | |
343 # We have just skipped over '=== acats configuration ==='. | |
344 def parse_acats_run (self, filename, file): | |
345 # Parse the preamble, which describes the configuration and logs | |
346 # the creation of support files. | |
347 record = (self.acats_premable == '') | |
348 if record: | |
349 self.acats_premable = '\t\t=== acats configuration ===\n' | |
350 while True: | |
351 line = file.readline() | |
352 if line == '': | |
353 self.fatal (filename, 'could not parse acats preamble') | |
354 if line == '\t\t=== acats tests ===\n': | |
355 break | |
356 if record: | |
357 self.acats_premable += line | |
358 | |
359 # Parse the test results themselves, using a dummy variation name. | |
360 tool = self.get_tool ('acats') | |
361 variation = tool.get_variation ('none') | |
362 self.parse_run (filename, file, tool, variation, 1) | |
363 | |
364 # Parse the failure list. | |
365 while True: | |
366 before = file.tell() | |
367 line = file.readline() | |
368 if line.startswith ('*** FAILURES: '): | |
369 self.acats_failures.append (line[len ('*** FAILURES: '):-1]) | |
370 continue | |
371 file.seek (before) | |
372 break | |
373 | |
374 # Parse the final summary at the end of a log in order to capture | |
375 # the version output that follows it. | |
376 def parse_final_summary (self, filename, file): | |
377 record = (self.version_output == '') | |
378 while True: | |
379 line = file.readline() | |
380 if line == '': | |
381 break | |
382 if line.startswith ('# of '): | |
383 continue | |
384 if record: | |
385 self.version_output += line | |
386 if line == '\n': | |
387 break | |
388 | |
389 # Parse a .log or .sum file. | |
390 def parse_file (self, filename, file): | |
391 tool = None | |
392 target = None | |
393 num_variations = 1 | |
394 while True: | |
395 line = file.readline() | |
396 if line == '': | |
397 return | |
398 | |
399 # Parse the list of variations, which comes before the test | |
400 # runs themselves. | |
401 if line.startswith ('Schedule of variations:'): | |
402 num_variations = self.parse_variations (filename, file) | |
403 continue | |
404 | |
405 # Parse a testsuite run for one tool/variation combination. | |
406 if line.startswith ('Running target '): | |
407 name = line[len ('Running target '):-1] | |
408 if not tool: | |
409 self.fatal (filename, 'could not parse tool name') | |
410 if name not in self.known_variations: | |
411 self.fatal (filename, 'unknown target: ' + name) | |
412 self.parse_run (filename, file, tool, | |
413 tool.get_variation (name), | |
414 num_variations) | |
415 # If there is only one variation then there is no separate | |
416 # summary for it. Record any following version output. | |
417 if num_variations == 1: | |
418 self.parse_final_summary (filename, file) | |
419 continue | |
420 | |
421 # Parse the start line. In the case where several files are being | |
422 # parsed, pick the one with the earliest time. | |
423 match = self.test_run_re.match (line) | |
424 if match: | |
425 time = self.parse_time (match.group (2)) | |
426 if not self.start_line or self.start_line[0] > time: | |
427 self.start_line = (time, line) | |
428 continue | |
429 | |
430 # Parse the form used for native testing. | |
431 if line.startswith ('Native configuration is '): | |
432 self.native_line = line | |
433 continue | |
434 | |
435 # Parse the target triplet. | |
436 if line.startswith ('Target is '): | |
437 self.target_line = line | |
438 continue | |
439 | |
440 # Parse the host triplet. | |
441 if line.startswith ('Host is '): | |
442 self.host_line = line | |
443 continue | |
444 | |
445 # Parse the acats premable. | |
446 if line == '\t\t=== acats configuration ===\n': | |
447 self.parse_acats_run (filename, file) | |
448 continue | |
449 | |
450 # Parse the tool name. | |
451 match = self.tool_re.match (line) | |
452 if match: | |
453 tool = self.get_tool (match.group (1)) | |
454 continue | |
455 | |
456 # Skip over the final summary (which we instead create from | |
457 # individual runs) and parse the version output. | |
458 if tool and line == '\t\t=== ' + tool.name + ' Summary ===\n': | |
459 if file.readline() != '\n': | |
460 self.fatal (filename, 'expected blank line after summary') | |
461 self.parse_final_summary (filename, file) | |
462 continue | |
463 | |
464 # Parse the completion line. In the case where several files | |
465 # are being parsed, pick the one with the latest time. | |
466 match = self.completed_re.match (line) | |
467 if match: | |
468 time = self.parse_time (match.group (1)) | |
469 if not self.end_line or self.end_line[0] < time: | |
470 self.end_line = (time, line) | |
471 continue | |
472 | |
473 # Sanity check to make sure that important text doesn't get | |
474 # dropped accidentally. | |
475 if strict and line.strip() != '': | |
476 self.fatal (filename, 'unrecognised line: ' + line[:-1]) | |
477 | |
478 # Output a segment of text. | |
479 def output_segment (self, segment): | |
480 with safe_open (segment.filename) as file: | |
481 file.seek (segment.start) | |
482 for i in range (segment.lines): | |
483 sys.stdout.write (file.readline()) | |
484 | |
485 # Output a summary giving the number of times each type of result has | |
486 # been seen. | |
487 def output_summary (self, tool, counts): | |
488 for i in range (len (self.count_names)): | |
489 name = self.count_names[i] | |
490 # dejagnu only prints result types that were seen at least once, | |
491 # but acats always prints a number of unexpected failures. | |
492 if (counts[i] > 0 | |
493 or (tool.name == 'acats' | |
494 and name.startswith ('# of unexpected failures'))): | |
495 sys.stdout.write ('%s%d\n' % (name, counts[i])) | |
496 | |
497 # Output unified .log or .sum information for a particular variation, | |
498 # with a summary at the end. | |
499 def output_variation (self, tool, variation): | |
500 self.output_segment (variation.header) | |
501 for harness in sorted (variation.harnesses.values(), | |
502 key = attrgetter ('name')): | |
503 sys.stdout.write ('Running ' + harness.name + ' ...\n') | |
504 if self.do_sum: | |
505 harness.results.sort() | |
506 for (key, line) in harness.results: | |
507 sys.stdout.write (line) | |
508 else: | |
509 # Rearrange the log segments into test order (but without | |
510 # rearranging text within those segments). | |
511 for key in sorted (harness.segments.keys()): | |
512 self.output_segment (harness.segments[key]) | |
513 for segment in harness.empty: | |
514 self.output_segment (segment) | |
515 if len (self.variations) > 1: | |
516 sys.stdout.write ('\t\t=== ' + tool.name + ' Summary for ' | |
517 + variation.name + ' ===\n\n') | |
518 self.output_summary (tool, variation.counts) | |
519 | |
520 # Output unified .log or .sum information for a particular tool, | |
521 # with a summary at the end. | |
522 def output_tool (self, tool): | |
523 counts = self.zero_counts() | |
524 if tool.name == 'acats': | |
525 # acats doesn't use variations, so just output everything. | |
526 # It also has a different approach to whitespace. | |
527 sys.stdout.write ('\t\t=== ' + tool.name + ' tests ===\n') | |
528 for variation in tool.variations.values(): | |
529 self.output_variation (tool, variation) | |
530 self.accumulate_counts (counts, variation.counts) | |
531 sys.stdout.write ('\t\t=== ' + tool.name + ' Summary ===\n') | |
532 else: | |
533 # Output the results in the usual dejagnu runtest format. | |
534 sys.stdout.write ('\n\t\t=== ' + tool.name + ' tests ===\n\n' | |
535 'Schedule of variations:\n') | |
536 for name in self.variations: | |
537 if name in tool.variations: | |
538 sys.stdout.write (' ' + name + '\n') | |
539 sys.stdout.write ('\n') | |
540 for name in self.variations: | |
541 if name in tool.variations: | |
542 variation = tool.variations[name] | |
543 sys.stdout.write ('Running target ' | |
544 + variation.name + '\n') | |
545 self.output_variation (tool, variation) | |
546 self.accumulate_counts (counts, variation.counts) | |
547 sys.stdout.write ('\n\t\t=== ' + tool.name + ' Summary ===\n\n') | |
548 self.output_summary (tool, counts) | |
549 | |
550 def main (self): | |
551 self.parse_cmdline() | |
552 try: | |
553 # Parse the input files. | |
554 for filename in self.files: | |
555 with safe_open (filename) as file: | |
556 self.parse_file (filename, file) | |
557 | |
558 # Decide what to output. | |
559 if len (self.variations) == 0: | |
560 self.variations = sorted (self.known_variations) | |
561 else: | |
562 for name in self.variations: | |
563 if name not in self.known_variations: | |
564 self.fatal (None, 'no results for ' + name) | |
565 if len (self.tools) == 0: | |
566 self.tools = sorted (self.runs.keys()) | |
567 | |
568 # Output the header. | |
569 if self.start_line: | |
570 sys.stdout.write (self.start_line[1]) | |
571 sys.stdout.write (self.native_line) | |
572 sys.stdout.write (self.target_line) | |
573 sys.stdout.write (self.host_line) | |
574 sys.stdout.write (self.acats_premable) | |
575 | |
576 # Output the main body. | |
577 for name in self.tools: | |
578 if name not in self.runs: | |
579 self.fatal (None, 'no results for ' + name) | |
580 self.output_tool (self.runs[name]) | |
581 | |
582 # Output the footer. | |
583 if len (self.acats_failures) > 0: | |
584 sys.stdout.write ('*** FAILURES: ' | |
585 + ' '.join (self.acats_failures) + '\n') | |
586 sys.stdout.write (self.version_output) | |
587 if self.end_line: | |
588 sys.stdout.write (self.end_line[1]) | |
589 except IOError as e: | |
590 self.fatal (e.filename, e.strerror) | |
591 | |
592 Prog().main() |