dg-extract-results.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  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):\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 expected passes\t\t',
  124. '# of unexpected failures\t',
  125. '# of unexpected successes\t',
  126. '# of expected failures\t\t',
  127. '# of unknown successes\t\t',
  128. '# of known failures\t\t',
  129. '# of untested testcases\t\t',
  130. '# of unresolved testcases\t',
  131. '# of unsupported tests\t\t'
  132. ]
  133. self.runs = dict()
  134. def usage (self):
  135. name = sys.argv[0]
  136. sys.stderr.write ('Usage: ' + name
  137. + ''' [-t tool] [-l variant-list] [-L] log-or-sum-file ...
  138. tool The tool (e.g. g++, libffi) for which to create a
  139. new test summary file. If not specified then output
  140. is created for all tools.
  141. variant-list One or more test variant names. If the list is
  142. not specified then one is constructed from all
  143. variants in the files for <tool>.
  144. sum-file A test summary file with the format of those
  145. created by runtest from DejaGnu.
  146. If -L is used, merge *.log files instead of *.sum. In this
  147. mode the exact order of lines may not be preserved, just different
  148. Running *.exp chunks should be in correct order.
  149. ''')
  150. sys.exit (1)
  151. def fatal (self, what, string):
  152. if not what:
  153. what = sys.argv[0]
  154. sys.stderr.write (what + ': ' + string + '\n')
  155. sys.exit (1)
  156. # Parse the command-line arguments.
  157. def parse_cmdline (self):
  158. try:
  159. (options, self.files) = getopt.getopt (sys.argv[1:], 'l:t:L')
  160. if len (self.files) == 0:
  161. self.usage()
  162. for (option, value) in options:
  163. if option == '-l':
  164. self.variations.append (value)
  165. elif option == '-t':
  166. self.tools.append (value)
  167. else:
  168. self.do_sum = False
  169. except getopt.GetoptError as e:
  170. self.fatal (None, e.msg)
  171. # Try to parse time string TIME, returning an arbitrary time on failure.
  172. # Getting this right is just a nice-to-have so failures should be silent.
  173. def parse_time (self, time):
  174. try:
  175. return datetime.strptime (time, '%c')
  176. except ValueError:
  177. return datetime.now()
  178. # Parse an integer and abort on failure.
  179. def parse_int (self, filename, value):
  180. try:
  181. return int (value)
  182. except ValueError:
  183. self.fatal (filename, 'expected an integer, got: ' + value)
  184. # Return a list that represents no test results.
  185. def zero_counts (self):
  186. return [0 for x in self.count_names]
  187. # Return the ToolRun for tool NAME.
  188. def get_tool (self, name):
  189. if name not in self.runs:
  190. self.runs[name] = ToolRun (name)
  191. return self.runs[name]
  192. # Add the result counts in list FROMC to TOC.
  193. def accumulate_counts (self, toc, fromc):
  194. for i in range (len (self.count_names)):
  195. toc[i] += fromc[i]
  196. # Parse the list of variations after 'Schedule of variations:'.
  197. # Return the number seen.
  198. def parse_variations (self, filename, file):
  199. num_variations = 0
  200. while True:
  201. line = file.readline()
  202. if line == '':
  203. self.fatal (filename, 'could not parse variation list')
  204. if line == '\n':
  205. break
  206. self.known_variations.add (line.strip())
  207. num_variations += 1
  208. return num_variations
  209. # Parse from the first line after 'Running target ...' to the end
  210. # of the run's summary.
  211. def parse_run (self, filename, file, tool, variation, num_variations):
  212. header = None
  213. harness = None
  214. segment = None
  215. final_using = 0
  216. # If this is the first run for this variation, add any text before
  217. # the first harness to the header.
  218. if not variation.header:
  219. segment = Segment (filename, file.tell())
  220. variation.header = segment
  221. # Parse up until the first line of the summary.
  222. if num_variations == 1:
  223. end = '\t\t=== ' + tool.name + ' Summary ===\n'
  224. else:
  225. end = ('\t\t=== ' + tool.name + ' Summary for '
  226. + variation.name + ' ===\n')
  227. while True:
  228. line = file.readline()
  229. if line == '':
  230. self.fatal (filename, 'no recognised summary line')
  231. if line == end:
  232. break
  233. # Look for the start of a new harness.
  234. if line.startswith ('Running ') and line.endswith (' ...\n'):
  235. # Close off the current harness segment, if any.
  236. if harness:
  237. segment.lines -= final_using
  238. harness.add_segment (first_key, segment)
  239. name = line[len ('Running '):-len(' ...\n')]
  240. harness = variation.get_harness (name)
  241. segment = Segment (filename, file.tell())
  242. first_key = None
  243. final_using = 0
  244. continue
  245. # Record test results. Associate the first test result with
  246. # the harness segment, so that if a run for a particular harness
  247. # has been split up, we can reassemble the individual segments
  248. # in a sensible order.
  249. #
  250. # dejagnu sometimes issues warnings about the testing environment
  251. # before running any tests. Treat them as part of the header
  252. # rather than as a test result.
  253. match = self.result_re.match (line)
  254. if match and (harness or not line.startswith ('WARNING:')):
  255. if not harness:
  256. self.fatal (filename, 'saw test result before harness name')
  257. name = match.group (2)
  258. # Ugly hack to get the right order for gfortran.
  259. if name.startswith ('gfortran.dg/g77/'):
  260. name = 'h' + name
  261. key = (name, len (harness.results))
  262. harness.results.append ((key, line))
  263. if not first_key and sort_logs:
  264. first_key = key
  265. # 'Using ...' lines are only interesting in a header. Splitting
  266. # the test up into parallel runs leads to more 'Using ...' lines
  267. # than there would be in a single log.
  268. if line.startswith ('Using '):
  269. final_using += 1
  270. else:
  271. final_using = 0
  272. # Add other text to the current segment, if any.
  273. if segment:
  274. segment.lines += 1
  275. # Close off the final harness segment, if any.
  276. if harness:
  277. segment.lines -= final_using
  278. harness.add_segment (first_key, segment)
  279. # Parse the rest of the summary (the '# of ' lines).
  280. if len (variation.counts) == 0:
  281. variation.counts = self.zero_counts()
  282. while True:
  283. before = file.tell()
  284. line = file.readline()
  285. if line == '':
  286. break
  287. if line == '\n':
  288. continue
  289. if not line.startswith ('# '):
  290. file.seek (before)
  291. break
  292. found = False
  293. for i in range (len (self.count_names)):
  294. if line.startswith (self.count_names[i]):
  295. count = line[len (self.count_names[i]):-1].strip()
  296. variation.counts[i] += self.parse_int (filename, count)
  297. found = True
  298. break
  299. if not found:
  300. self.fatal (filename, 'unknown test result: ' + line[:-1])
  301. # Parse an acats run, which uses a different format from dejagnu.
  302. # We have just skipped over '=== acats configuration ==='.
  303. def parse_acats_run (self, filename, file):
  304. # Parse the preamble, which describes the configuration and logs
  305. # the creation of support files.
  306. record = (self.acats_premable == '')
  307. if record:
  308. self.acats_premable = '\t\t=== acats configuration ===\n'
  309. while True:
  310. line = file.readline()
  311. if line == '':
  312. self.fatal (filename, 'could not parse acats preamble')
  313. if line == '\t\t=== acats tests ===\n':
  314. break
  315. if record:
  316. self.acats_premable += line
  317. # Parse the test results themselves, using a dummy variation name.
  318. tool = self.get_tool ('acats')
  319. variation = tool.get_variation ('none')
  320. self.parse_run (filename, file, tool, variation, 1)
  321. # Parse the failure list.
  322. while True:
  323. before = file.tell()
  324. line = file.readline()
  325. if line.startswith ('*** FAILURES: '):
  326. self.acats_failures.append (line[len ('*** FAILURES: '):-1])
  327. continue
  328. file.seek (before)
  329. break
  330. # Parse the final summary at the end of a log in order to capture
  331. # the version output that follows it.
  332. def parse_final_summary (self, filename, file):
  333. record = (self.version_output == '')
  334. while True:
  335. line = file.readline()
  336. if line == '':
  337. break
  338. if line.startswith ('# of '):
  339. continue
  340. if record:
  341. self.version_output += line
  342. if line == '\n':
  343. break
  344. # Parse a .log or .sum file.
  345. def parse_file (self, filename, file):
  346. tool = None
  347. target = None
  348. num_variations = 1
  349. while True:
  350. line = file.readline()
  351. if line == '':
  352. return
  353. # Parse the list of variations, which comes before the test
  354. # runs themselves.
  355. if line.startswith ('Schedule of variations:'):
  356. num_variations = self.parse_variations (filename, file)
  357. continue
  358. # Parse a testsuite run for one tool/variation combination.
  359. if line.startswith ('Running target '):
  360. name = line[len ('Running target '):-1]
  361. if not tool:
  362. self.fatal (filename, 'could not parse tool name')
  363. if name not in self.known_variations:
  364. self.fatal (filename, 'unknown target: ' + name)
  365. self.parse_run (filename, file, tool,
  366. tool.get_variation (name),
  367. num_variations)
  368. # If there is only one variation then there is no separate
  369. # summary for it. Record any following version output.
  370. if num_variations == 1:
  371. self.parse_final_summary (filename, file)
  372. continue
  373. # Parse the start line. In the case where several files are being
  374. # parsed, pick the one with the earliest time.
  375. match = self.test_run_re.match (line)
  376. if match:
  377. time = self.parse_time (match.group (2))
  378. if not self.start_line or self.start_line[0] > time:
  379. self.start_line = (time, line)
  380. continue
  381. # Parse the form used for native testing.
  382. if line.startswith ('Native configuration is '):
  383. self.native_line = line
  384. continue
  385. # Parse the target triplet.
  386. if line.startswith ('Target is '):
  387. self.target_line = line
  388. continue
  389. # Parse the host triplet.
  390. if line.startswith ('Host is '):
  391. self.host_line = line
  392. continue
  393. # Parse the acats premable.
  394. if line == '\t\t=== acats configuration ===\n':
  395. self.parse_acats_run (filename, file)
  396. continue
  397. # Parse the tool name.
  398. match = self.tool_re.match (line)
  399. if match:
  400. tool = self.get_tool (match.group (1))
  401. continue
  402. # Skip over the final summary (which we instead create from
  403. # individual runs) and parse the version output.
  404. if tool and line == '\t\t=== ' + tool.name + ' Summary ===\n':
  405. if file.readline() != '\n':
  406. self.fatal (filename, 'expected blank line after summary')
  407. self.parse_final_summary (filename, file)
  408. continue
  409. # Parse the completion line. In the case where several files
  410. # are being parsed, pick the one with the latest time.
  411. match = self.completed_re.match (line)
  412. if match:
  413. time = self.parse_time (match.group (1))
  414. if not self.end_line or self.end_line[0] < time:
  415. self.end_line = (time, line)
  416. continue
  417. # Sanity check to make sure that important text doesn't get
  418. # dropped accidentally.
  419. if strict and line.strip() != '':
  420. self.fatal (filename, 'unrecognised line: ' + line[:-1])
  421. # Output a segment of text.
  422. def output_segment (self, segment):
  423. with safe_open (segment.filename) as file:
  424. file.seek (segment.start)
  425. for i in range (segment.lines):
  426. sys.stdout.write (file.readline())
  427. # Output a summary giving the number of times each type of result has
  428. # been seen.
  429. def output_summary (self, tool, counts):
  430. for i in range (len (self.count_names)):
  431. name = self.count_names[i]
  432. # dejagnu only prints result types that were seen at least once,
  433. # but acats always prints a number of unexpected failures.
  434. if (counts[i] > 0
  435. or (tool.name == 'acats'
  436. and name.startswith ('# of unexpected failures'))):
  437. sys.stdout.write ('%s%d\n' % (name, counts[i]))
  438. # Output unified .log or .sum information for a particular variation,
  439. # with a summary at the end.
  440. def output_variation (self, tool, variation):
  441. self.output_segment (variation.header)
  442. for harness in sorted (variation.harnesses.values(),
  443. key = attrgetter ('name')):
  444. sys.stdout.write ('Running ' + harness.name + ' ...\n')
  445. if self.do_sum:
  446. harness.results.sort()
  447. for (key, line) in harness.results:
  448. sys.stdout.write (line)
  449. else:
  450. # Rearrange the log segments into test order (but without
  451. # rearranging text within those segments).
  452. for key in sorted (harness.segments.keys()):
  453. self.output_segment (harness.segments[key])
  454. for segment in harness.empty:
  455. self.output_segment (segment)
  456. if len (self.variations) > 1:
  457. sys.stdout.write ('\t\t=== ' + tool.name + ' Summary for '
  458. + variation.name + ' ===\n\n')
  459. self.output_summary (tool, variation.counts)
  460. # Output unified .log or .sum information for a particular tool,
  461. # with a summary at the end.
  462. def output_tool (self, tool):
  463. counts = self.zero_counts()
  464. if tool.name == 'acats':
  465. # acats doesn't use variations, so just output everything.
  466. # It also has a different approach to whitespace.
  467. sys.stdout.write ('\t\t=== ' + tool.name + ' tests ===\n')
  468. for variation in tool.variations.values():
  469. self.output_variation (tool, variation)
  470. self.accumulate_counts (counts, variation.counts)
  471. sys.stdout.write ('\t\t=== ' + tool.name + ' Summary ===\n')
  472. else:
  473. # Output the results in the usual dejagnu runtest format.
  474. sys.stdout.write ('\n\t\t=== ' + tool.name + ' tests ===\n\n'
  475. 'Schedule of variations:\n')
  476. for name in self.variations:
  477. if name in tool.variations:
  478. sys.stdout.write (' ' + name + '\n')
  479. sys.stdout.write ('\n')
  480. for name in self.variations:
  481. if name in tool.variations:
  482. variation = tool.variations[name]
  483. sys.stdout.write ('Running target '
  484. + variation.name + '\n')
  485. self.output_variation (tool, variation)
  486. self.accumulate_counts (counts, variation.counts)
  487. sys.stdout.write ('\n\t\t=== ' + tool.name + ' Summary ===\n\n')
  488. self.output_summary (tool, counts)
  489. def main (self):
  490. self.parse_cmdline()
  491. try:
  492. # Parse the input files.
  493. for filename in self.files:
  494. with safe_open (filename) as file:
  495. self.parse_file (filename, file)
  496. # Decide what to output.
  497. if len (self.variations) == 0:
  498. self.variations = sorted (self.known_variations)
  499. else:
  500. for name in self.variations:
  501. if name not in self.known_variations:
  502. self.fatal (None, 'no results for ' + name)
  503. if len (self.tools) == 0:
  504. self.tools = sorted (self.runs.keys())
  505. # Output the header.
  506. if self.start_line:
  507. sys.stdout.write (self.start_line[1])
  508. sys.stdout.write (self.native_line)
  509. sys.stdout.write (self.target_line)
  510. sys.stdout.write (self.host_line)
  511. sys.stdout.write (self.acats_premable)
  512. # Output the main body.
  513. for name in self.tools:
  514. if name not in self.runs:
  515. self.fatal (None, 'no results for ' + name)
  516. self.output_tool (self.runs[name])
  517. # Output the footer.
  518. if len (self.acats_failures) > 0:
  519. sys.stdout.write ('*** FAILURES: '
  520. + ' '.join (self.acats_failures) + '\n')
  521. sys.stdout.write (self.version_output)
  522. if self.end_line:
  523. sys.stdout.write (self.end_line[1])
  524. except IOError as e:
  525. self.fatal (e.filename, e.strerror)
  526. Prog().main()