dg-extract-results.py 24 KB

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