2 # Copyright (c) 2012 The Native Client Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """Parse the report output of the llvm test suite or regression tests,
7 filter out known failures, and check for new failures
9 pnacl/scripts/parse_llvm_test_report.py [options]+ reportfile
23 def ParseCommandLine(argv):
24 parser = optparse.OptionParser(prog=argv[0])
25 parser.add_option('-x', '--exclude', action='append', dest='excludes',
27 help='Add list of excluded tests (expected fails)')
28 parser.add_option('-c', '--check-excludes', action='store_true',
29 default=False, dest='check_excludes',
30 help='Report tests which unexpectedly pass')
31 parser.add_option('-v', '--verbose', action='store_true',
32 default=False, dest='verbose',
33 help='Print compilation/run logs of failing tests')
34 parser.add_option('-p', '--build-path', dest='buildpath',
35 help='Path to test-suite build directory')
36 parser.add_option('-a', '--attribute', dest='attributes', action='append',
38 help='Add attribute of test configuration (e.g. arch)')
39 parser.add_option('-t', '--testsuite', action='store_true', dest='testsuite',
41 parser.add_option('-l', '--lit', action='store_true', dest='lit',
44 options, args = parser.parse_args(argv[1:])
48 print >> sys.stderr, text
52 return name.find('/') != -1
54 def GetShortname(fullname):
55 return fullname.split('/')[-1]
57 def ParseTestsuiteCSV(filecontents):
58 ''' Parse a CSV file output by llvm testsuite with a record for each test.
59 returns 2 dictionaries:
60 1) a mapping from the short name of the test (without the path) to
61 a list of full pathnames that match it. It contains all the tests.
62 2) a mapping of all test failures, mapping full test path to the type
63 of failure (compile or exec)
67 reader = csv.DictReader(StringIO.StringIO(filecontents))
72 fullname = row['Program']
73 shortname = GetShortname(fullname)
74 fullnames = alltests.get(shortname, [])
75 fullnames.append(fullname)
76 alltests[shortname] = fullnames
79 failures[fullname] = 'compile'
80 elif row['Exec'] == '*':
81 failures[fullname] = 'exec'
83 logging.info('%d tests, %d failures', testcount, len(failures))
84 return alltests, failures
86 def ParseLit(filecontents):
87 ''' Parse the output of the LLVM regression test runner (lit/make check).
88 returns a dictionary mapping test name to the type of failure
89 (Clang, LLVM, LLVMUnit, etc)
94 for line in filecontents.splitlines():
98 if l[0] in ('PASS:', 'FAIL:', 'XFAIL:', 'XPASS:', 'UNSUPPORTED:'):
100 fullname = ''.join(l[1:4])
101 shortname = GetShortname(fullname)
102 fullnames = alltests.get(shortname, [])
103 fullnames.append(fullname)
104 alltests[shortname] = fullnames
105 if l[0] in ('FAIL:', 'XPASS:'):
106 failures[fullname] = l[1]
107 logging.info('%d tests, %d failures', testcount, len(failures))
108 return alltests, failures
110 def ParseExcludeFile(filename, config_attributes,
111 check_test_names=False, alltests=None):
112 ''' Parse a list of excludes (known test failures). Excludes can be specified
113 by shortname (e.g. fbench) or by full path
114 (e.g. SingleSource/Benchmarks/Misc/fbench) but if there is more than
115 one test with the same shortname, the full name must be given.
116 Errors are reported if an exclude does not match exactly one test
117 in alltests, or if there are duplicate excludes.
120 Number of failures in the exclusion file.
126 if not line: continue
127 if line.startswith('#'): continue
128 tokens = line.split()
131 attributes = set(tokens[1].split(','))
132 if not attributes.issubset(config_attributes):
136 if testname in EXCLUDES:
137 logging.error('Duplicate exclude: %s', line)
139 if IsFullname(testname):
140 shortname = GetShortname(testname)
141 if shortname not in alltests or testname not in alltests[shortname]:
142 logging.error('Exclude %s not found in list of tests', line)
146 # short name is specified
148 if shortname not in alltests:
149 logging.error('Exclude %s not found in list of tests', shortname)
151 if len(alltests[shortname]) > 1:
152 logging.error('Exclude %s matches more than one test: %s. ' +
153 'Specify full name in exclude file.',
154 shortname, str(alltests[shortname]))
156 fullname = alltests[shortname][0]
158 if fullname in EXCLUDES:
159 logging.error('Duplicate exclude %s', fullname)
162 EXCLUDES[fullname] = filename
164 logging.info('Parsed %s: now %d total excludes', filename, len(EXCLUDES))
167 def DumpFileContents(name):
168 error = not os.path.exists(name)
171 logging.debug(open(name, 'rb').read())
175 logging.error("Couldn't open file: %s", name)
176 # Make the bots go red
177 logging.error('@@@STEP_FAILURE@@@')
179 def PrintTestsuiteCompilationResult(path, test):
180 ''' Print the compilation and run results for the specified test in the
182 These results are left in several different log files by the testsuite
183 driver, and are different for MultiSource/SingleSource tests
185 logging.debug('RESULTS for %s', test)
186 testpath = os.path.join(path, test)
187 testdir, testname = os.path.split(testpath)
188 outputdir = os.path.join(testdir, 'Output')
190 logging.debug('COMPILE phase')
191 logging.debug('OBJECT file phase')
192 if test.startswith('MultiSource'):
193 for f in os.listdir(outputdir):
194 if f.endswith('llvm.o.compile'):
195 DumpFileContents(os.path.join(outputdir, f))
196 elif test.startswith('SingleSource'):
197 DumpFileContents(os.path.join(outputdir, testname + '.llvm.o.compile'))
199 Fatal('ERROR: unrecognized test type ' + test)
201 logging.debug('PEXE generation phase')
202 DumpFileContents(os.path.join(outputdir,
203 testname + '.nonfinal.pexe.compile'))
205 logging.debug('PEXE finalization phase')
206 DumpFileContents(os.path.join(outputdir, testname + '.final.pexe.finalize'))
208 logging.debug('TRANSLATION phase')
209 DumpFileContents(os.path.join(outputdir, testname + '.nexe.translate'))
211 logging.debug('EXECUTION phase')
212 logging.debug('native output:')
213 DumpFileContents(os.path.join(outputdir, testname + '.out-nat'))
214 logging.debug('pnacl output:')
215 DumpFileContents(os.path.join(outputdir, testname + '.out-pnacl'))
218 options, args = ParseCommandLine(argv)
221 Fatal('Must specify filename to parse')
223 return Report(vars(options), filename=filename)
226 def Report(options, filename=None, filecontents=None):
227 loglevel = logging.INFO
228 if options['verbose']:
229 loglevel = logging.DEBUG
230 logging.basicConfig(level=loglevel, format='%(message)s')
232 if not (filename or filecontents):
233 Fatal('ERROR: must specify filename or filecontents')
236 logging.debug('Full test results:')
239 with open(filename, 'rb') as f:
240 filecontents = f.read();
241 # get the set of tests and failures
242 if options['testsuite']:
243 if options['verbose'] and options['buildpath'] is None:
244 Fatal('ERROR: must specify build path if verbose output is desired')
245 alltests, failures = ParseTestsuiteCSV(filecontents)
246 check_test_names = True
248 alltests, failures = ParseLit(filecontents)
249 check_test_names = True
251 Fatal('Must specify either testsuite (-t) or lit (-l) output format')
253 # get the set of excludes
254 exclusion_failures = 0
255 for f in options['excludes']:
256 exclusion_failures += ParseExcludeFile(f, set(options['attributes']),
257 check_test_names=check_test_names,
260 # Regardless of the verbose option, do a dry run of
261 # PrintTestsuiteCompilationResult so we can catch errors when intermediate
262 # filenames in the compilation pipeline change.
263 # E.g. https://code.google.com/p/nativeclient/issues/detail?id=3659
264 if len(alltests) and options['testsuite']:
265 logging.disable(logging.INFO)
266 PrintTestsuiteCompilationResult(options['buildpath'],
267 alltests.values()[0][0])
268 logging.disable(logging.NOTSET)
270 # intersect them and check for unexpected fails/passes
271 unexpected_failures = 0
272 unexpected_passes = 0
273 for tests in alltests.itervalues():
276 if test not in EXCLUDES:
277 unexpected_failures += 1
278 logging.info('[ FAILED ] %s: %s failure', test, failures[test])
279 if options['testsuite']:
280 PrintTestsuiteCompilationResult(options['buildpath'], test)
281 elif test in EXCLUDES:
282 unexpected_passes += 1
283 logging.info('%s: unexpected success', test)
285 logging.info('%d unexpected failures %d unexpected passes',
286 unexpected_failures, unexpected_passes)
287 if exclusion_failures:
288 logging.info('%d problems in known_failures exclusion files',
291 if options['check_excludes']:
292 return unexpected_failures + unexpected_passes + exclusion_failures > 0
293 return unexpected_failures + exclusion_failures > 0
295 if __name__ == '__main__':
296 sys.exit(main(sys.argv))