111
|
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()
|