d: Merge upstream dmd eb7bee331, druntime 27834edb, phobos ac296f80c.
[official-gcc.git] / contrib / testsuite-management / validate_failures.py
blob925ba22ea0f915aa2f9f9589696b6f579110ad0b
1 #!/usr/bin/env python3
3 # Script to compare testsuite failures against a list of known-to-fail
4 # tests.
6 # Contributed by Diego Novillo <dnovillo@google.com>
8 # Copyright (C) 2011-2013 Free Software Foundation, Inc.
10 # This file is part of GCC.
12 # GCC is free software; you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation; either version 3, or (at your option)
15 # any later version.
17 # GCC is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
22 # You should have received a copy of the GNU General Public License
23 # along with GCC; see the file COPYING. If not, write to
24 # the Free Software Foundation, 51 Franklin Street, Fifth Floor,
25 # Boston, MA 02110-1301, USA.
27 """This script provides a coarser XFAILing mechanism that requires no
28 detailed DejaGNU markings. This is useful in a variety of scenarios:
30 - Development branches with many known failures waiting to be fixed.
31 - Release branches with known failures that are not considered
32 important for the particular release criteria used in that branch.
34 The script must be executed from the toplevel build directory. When
35 executed it will:
37 1- Determine the target built: TARGET
38 2- Determine the source directory: SRCDIR
39 3- Look for a failure manifest file in
40 <SRCDIR>/<MANIFEST_SUBDIR>/<MANIFEST_NAME>.xfail
41 4- Collect all the <tool>.sum files from the build tree.
42 5- Produce a report stating:
43 a- Failures expected in the manifest but not present in the build.
44 b- Failures in the build not expected in the manifest.
45 6- If all the build failures are expected in the manifest, it exits
46 with exit code 0. Otherwise, it exits with error code 1.
48 Manifest files contain expected DejaGNU results that are otherwise
49 treated as failures.
50 They may also contain additional text:
52 # This is a comment. - self explanatory
53 @include file - the file is a path relative to the includer
54 @remove result text - result text is removed from the expected set
55 """
57 import datetime
58 import optparse
59 import os
60 import re
61 import sys
63 # Handled test results.
64 _VALID_TEST_RESULTS = [ 'FAIL', 'UNRESOLVED', 'XPASS', 'ERROR' ]
65 _VALID_TEST_RESULTS_REX = re.compile("%s" % "|".join(_VALID_TEST_RESULTS))
67 # Subdirectory of srcdir in which to find the manifest file.
68 _MANIFEST_SUBDIR = 'contrib/testsuite-management'
70 # Pattern for naming manifest files.
71 # The first argument should be the toplevel GCC(/GNU tool) source directory.
72 # The second argument is the manifest subdir.
73 # The third argument is the manifest target, which defaults to the target
74 # triplet used during the build.
75 _MANIFEST_PATH_PATTERN = '%s/%s/%s.xfail'
77 # The options passed to the program.
78 _OPTIONS = None
80 def Error(msg):
81 print('error: %s' % msg, file=sys.stderr)
82 sys.exit(1)
85 class TestResult(object):
86 """Describes a single DejaGNU test result as emitted in .sum files.
88 We are only interested in representing unsuccessful tests. So, only
89 a subset of all the tests are loaded.
91 The summary line used to build the test result should have this format:
93 attrlist | XPASS: gcc.dg/unroll_1.c (test for excess errors)
94 ^^^^^^^^ ^^^^^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
95 optional state name description
96 attributes
98 Attributes:
99 attrlist: A comma separated list of attributes.
100 Valid values:
101 flaky Indicates that this test may not always fail. These
102 tests are reported, but their presence does not affect
103 the results.
105 expire=YYYYMMDD After this date, this test will produce an error
106 whether it is in the manifest or not.
108 state: One of UNRESOLVED, XPASS or FAIL.
109 name: File name for the test.
110 description: String describing the test (flags used, dejagnu message, etc)
111 ordinal: Monotonically increasing integer.
112 It is used to keep results for one .exp file sorted
113 by the order the tests were run.
116 def __init__(self, summary_line, ordinal=-1):
117 try:
118 (self.attrs, summary_line) = SplitAttributesFromSummaryLine(summary_line)
119 try:
120 (self.state,
121 self.name,
122 self.description) = re.match(r'([A-Z]+):\s*(\S+)\s*(.*)',
123 summary_line).groups()
124 except:
125 print('Failed to parse summary line: "%s"' % summary_line)
126 raise
127 self.ordinal = ordinal
128 except ValueError:
129 Error('Cannot parse summary line "%s"' % summary_line)
131 if self.state not in _VALID_TEST_RESULTS:
132 Error('Invalid test result %s in "%s" (parsed as "%s")' % (
133 self.state, summary_line, self))
135 def __lt__(self, other):
136 return (self.name < other.name or
137 (self.name == other.name and self.ordinal < other.ordinal))
139 def __hash__(self):
140 return hash(self.state) ^ hash(self.name) ^ hash(self.description)
142 def __eq__(self, other):
143 return (self.state == other.state and
144 self.name == other.name and
145 self.description == other.description)
147 def __ne__(self, other):
148 return not (self == other)
150 def __str__(self):
151 attrs = ''
152 if self.attrs:
153 attrs = '%s | ' % self.attrs
154 return '%s%s: %s %s' % (attrs, self.state, self.name, self.description)
156 def ExpirationDate(self):
157 # Return a datetime.date object with the expiration date for this
158 # test result. Return None, if no expiration has been set.
159 if re.search(r'expire=', self.attrs):
160 expiration = re.search(r'expire=(\d\d\d\d)(\d\d)(\d\d)', self.attrs)
161 if not expiration:
162 Error('Invalid expire= format in "%s". Must be of the form '
163 '"expire=YYYYMMDD"' % self)
164 return datetime.date(int(expiration.group(1)),
165 int(expiration.group(2)),
166 int(expiration.group(3)))
167 return None
169 def HasExpired(self):
170 # Return True if the expiration date of this result has passed.
171 expiration_date = self.ExpirationDate()
172 if expiration_date:
173 now = datetime.date.today()
174 return now > expiration_date
177 def GetMakefileValue(makefile_name, value_name):
178 if os.path.exists(makefile_name):
179 makefile = open(makefile_name, encoding='latin-1', mode='r')
180 for line in makefile:
181 if line.startswith(value_name):
182 (_, value) = line.split('=', 1)
183 value = value.strip()
184 makefile.close()
185 return value
186 makefile.close()
187 return None
190 def ValidBuildDirectory(builddir):
191 if (not os.path.exists(builddir) or
192 not os.path.exists('%s/Makefile' % builddir)):
193 return False
194 return True
197 def IsComment(line):
198 """Return True if line is a comment."""
199 return line.startswith('#')
202 def SplitAttributesFromSummaryLine(line):
203 """Splits off attributes from a summary line, if present."""
204 if '|' in line and not _VALID_TEST_RESULTS_REX.match(line):
205 (attrs, line) = line.split('|', 1)
206 attrs = attrs.strip()
207 else:
208 attrs = ''
209 line = line.strip()
210 return (attrs, line)
213 def IsInterestingResult(line):
214 """Return True if line is one of the summary lines we care about."""
215 (_, line) = SplitAttributesFromSummaryLine(line)
216 return bool(_VALID_TEST_RESULTS_REX.match(line))
219 def IsInclude(line):
220 """Return True if line is an include of another file."""
221 return line.startswith("@include ")
224 def GetIncludeFile(line, includer):
225 """Extract the name of the include file from line."""
226 includer_dir = os.path.dirname(includer)
227 include_file = line[len("@include "):]
228 return os.path.join(includer_dir, include_file.strip())
231 def IsNegativeResult(line):
232 """Return True if line should be removed from the expected results."""
233 return line.startswith("@remove ")
236 def GetNegativeResult(line):
237 """Extract the name of the negative result from line."""
238 line = line[len("@remove "):]
239 return line.strip()
242 def ParseManifestWorker(result_set, manifest_path):
243 """Read manifest_path, adding the contents to result_set."""
244 if _OPTIONS.verbosity >= 1:
245 print('Parsing manifest file %s.' % manifest_path)
246 manifest_file = open(manifest_path, encoding='latin-1', mode='r')
247 for line in manifest_file:
248 line = line.strip()
249 if line == "":
250 pass
251 elif IsComment(line):
252 pass
253 elif IsNegativeResult(line):
254 result_set.remove(TestResult(GetNegativeResult(line)))
255 elif IsInclude(line):
256 ParseManifestWorker(result_set, GetIncludeFile(line, manifest_path))
257 elif IsInterestingResult(line):
258 result_set.add(TestResult(line))
259 else:
260 Error('Unrecognized line in manifest file: %s' % line)
261 manifest_file.close()
264 def ParseManifest(manifest_path):
265 """Create a set of TestResult instances from the given manifest file."""
266 result_set = set()
267 ParseManifestWorker(result_set, manifest_path)
268 return result_set
271 def ParseSummary(sum_fname):
272 """Create a set of TestResult instances from the given summary file."""
273 result_set = set()
274 # ordinal is used when sorting the results so that tests within each
275 # .exp file are kept sorted.
276 ordinal=0
277 sum_file = open(sum_fname, encoding='latin-1', mode='r')
278 for line in sum_file:
279 if IsInterestingResult(line):
280 result = TestResult(line, ordinal)
281 ordinal += 1
282 if result.HasExpired():
283 # Tests that have expired are not added to the set of expected
284 # results. If they are still present in the set of actual results,
285 # they will cause an error to be reported.
286 print('WARNING: Expected failure "%s" has expired.' % line.strip())
287 continue
288 result_set.add(result)
289 sum_file.close()
290 return result_set
293 def GetManifest(manifest_path):
294 """Build a set of expected failures from the manifest file.
296 Each entry in the manifest file should have the format understood
297 by the TestResult constructor.
299 If no manifest file exists for this target, it returns an empty set.
301 if os.path.exists(manifest_path):
302 return ParseManifest(manifest_path)
303 else:
304 return set()
307 def CollectSumFiles(builddir):
308 sum_files = []
309 for root, dirs, files in os.walk(builddir):
310 for ignored in ('.svn', '.git'):
311 if ignored in dirs:
312 dirs.remove(ignored)
313 for fname in files:
314 if fname.endswith('.sum'):
315 sum_files.append(os.path.join(root, fname))
316 return sum_files
319 def GetResults(sum_files):
320 """Collect all the test results from the given .sum files."""
321 build_results = set()
322 for sum_fname in sum_files:
323 print('\t%s' % sum_fname)
324 build_results |= ParseSummary(sum_fname)
325 return build_results
328 def CompareResults(manifest, actual):
329 """Compare sets of results and return two lists:
330 - List of results present in ACTUAL but missing from MANIFEST.
331 - List of results present in MANIFEST but missing from ACTUAL.
333 # Collect all the actual results not present in the manifest.
334 # Results in this set will be reported as errors.
335 actual_vs_manifest = set()
336 for actual_result in actual:
337 if actual_result not in manifest:
338 actual_vs_manifest.add(actual_result)
340 # Collect all the tests in the manifest that were not found
341 # in the actual results.
342 # Results in this set will be reported as warnings (since
343 # they are expected failures that are not failing anymore).
344 manifest_vs_actual = set()
345 for expected_result in manifest:
346 # Ignore tests marked flaky.
347 if 'flaky' in expected_result.attrs:
348 continue
349 if expected_result not in actual:
350 manifest_vs_actual.add(expected_result)
352 return actual_vs_manifest, manifest_vs_actual
355 def GetManifestPath(srcdir, target, user_provided_must_exist):
356 """Return the full path to the manifest file."""
357 manifest_path = _OPTIONS.manifest
358 if manifest_path:
359 if user_provided_must_exist and not os.path.exists(manifest_path):
360 Error('Manifest does not exist: %s' % manifest_path)
361 return manifest_path
362 else:
363 if not srcdir:
364 Error('Could not determine the location of GCC\'s source tree. '
365 'The Makefile does not contain a definition for "srcdir".')
366 if not target:
367 Error('Could not determine the target triplet for this build. '
368 'The Makefile does not contain a definition for "target_alias".')
369 return _MANIFEST_PATH_PATTERN % (srcdir, _MANIFEST_SUBDIR, target)
372 def GetBuildData():
373 if not ValidBuildDirectory(_OPTIONS.build_dir):
374 # If we have been given a set of results to use, we may
375 # not be inside a valid GCC build directory. In that case,
376 # the user must provide both a manifest file and a set
377 # of results to check against it.
378 if not _OPTIONS.results or not _OPTIONS.manifest:
379 Error('%s is not a valid GCC top level build directory. '
380 'You must use --manifest and --results to do the validation.' %
381 _OPTIONS.build_dir)
382 else:
383 return None, None
384 srcdir = GetMakefileValue('%s/Makefile' % _OPTIONS.build_dir, 'srcdir =')
385 target = GetMakefileValue('%s/Makefile' % _OPTIONS.build_dir, 'target_alias=')
386 print('Source directory: %s' % srcdir)
387 print('Build target: %s' % target)
388 return srcdir, target
391 def PrintSummary(msg, summary):
392 print('\n\n%s' % msg)
393 for result in sorted(summary):
394 print(result)
397 def GetSumFiles(results, build_dir):
398 if not results:
399 print('Getting actual results from build directory %s' % build_dir)
400 sum_files = CollectSumFiles(build_dir)
401 else:
402 print('Getting actual results from user-provided results')
403 sum_files = results.split()
404 return sum_files
407 def PerformComparison(expected, actual, ignore_missing_failures):
408 actual_vs_expected, expected_vs_actual = CompareResults(expected, actual)
410 tests_ok = True
411 if len(actual_vs_expected) > 0:
412 PrintSummary('Unexpected results in this build (new failures)',
413 actual_vs_expected)
414 tests_ok = False
416 if not ignore_missing_failures and len(expected_vs_actual) > 0:
417 PrintSummary('Expected results not present in this build (fixed tests)'
418 '\n\nNOTE: This is not a failure. It just means that these '
419 'tests were expected\nto fail, but either they worked in '
420 'this configuration or they were not\npresent at all.\n',
421 expected_vs_actual)
423 if tests_ok:
424 print('\nSUCCESS: No unexpected failures.')
426 return tests_ok
429 def CheckExpectedResults():
430 srcdir, target = GetBuildData()
431 manifest_path = GetManifestPath(srcdir, target, True)
432 print('Manifest: %s' % manifest_path)
433 manifest = GetManifest(manifest_path)
434 sum_files = GetSumFiles(_OPTIONS.results, _OPTIONS.build_dir)
435 actual = GetResults(sum_files)
437 if _OPTIONS.verbosity >= 1:
438 PrintSummary('Tests expected to fail', manifest)
439 PrintSummary('\nActual test results', actual)
441 return PerformComparison(manifest, actual, _OPTIONS.ignore_missing_failures)
444 def ProduceManifest():
445 (srcdir, target) = GetBuildData()
446 manifest_path = GetManifestPath(srcdir, target, False)
447 print('Manifest: %s' % manifest_path)
448 if os.path.exists(manifest_path) and not _OPTIONS.force:
449 Error('Manifest file %s already exists.\nUse --force to overwrite.' %
450 manifest_path)
452 sum_files = GetSumFiles(_OPTIONS.results, _OPTIONS.build_dir)
453 actual = GetResults(sum_files)
454 manifest_file = open(manifest_path, encoding='latin-1', mode='w')
455 for result in sorted(actual):
456 print(result)
457 manifest_file.write('%s\n' % result)
458 manifest_file.close()
460 return True
463 def CompareBuilds():
464 (srcdir, target) = GetBuildData()
466 sum_files = GetSumFiles(_OPTIONS.results, _OPTIONS.build_dir)
467 actual = GetResults(sum_files)
469 clean_sum_files = GetSumFiles(_OPTIONS.results, _OPTIONS.clean_build)
470 clean = GetResults(clean_sum_files)
472 return PerformComparison(clean, actual, _OPTIONS.ignore_missing_failures)
475 def Main(argv):
476 parser = optparse.OptionParser(usage=__doc__)
478 # Keep the following list sorted by option name.
479 parser.add_option('--build_dir', action='store', type='string',
480 dest='build_dir', default='.',
481 help='Build directory to check (default = .)')
482 parser.add_option('--clean_build', action='store', type='string',
483 dest='clean_build', default=None,
484 help='Compare test results from this build against '
485 'those of another (clean) build. Use this option '
486 'when comparing the test results of your patch versus '
487 'the test results of a clean build without your patch. '
488 'You must provide the path to the top directory of your '
489 'clean build.')
490 parser.add_option('--force', action='store_true', dest='force',
491 default=False, help='When used with --produce_manifest, '
492 'it will overwrite an existing manifest file '
493 '(default = False)')
494 parser.add_option('--ignore_missing_failures', action='store_true',
495 dest='ignore_missing_failures', default=False,
496 help='When a failure is expected in the manifest but '
497 'it is not found in the actual results, the script '
498 'produces a note alerting to this fact. This means '
499 'that the expected failure has been fixed, or '
500 'it did not run, or it may simply be flaky '
501 '(default = False)')
502 parser.add_option('--manifest', action='store', type='string',
503 dest='manifest', default=None,
504 help='Name of the manifest file to use (default = '
505 'taken from '
506 'contrib/testsuite-managment/<target_alias>.xfail)')
507 parser.add_option('--produce_manifest', action='store_true',
508 dest='produce_manifest', default=False,
509 help='Produce the manifest for the current '
510 'build (default = False)')
511 parser.add_option('--results', action='store', type='string',
512 dest='results', default=None, help='Space-separated list '
513 'of .sum files with the testing results to check. The '
514 'only content needed from these files are the lines '
515 'starting with FAIL, XPASS or UNRESOLVED (default = '
516 '.sum files collected from the build directory).')
517 parser.add_option('--verbosity', action='store', dest='verbosity',
518 type='int', default=0, help='Verbosity level (default = 0)')
519 global _OPTIONS
520 (_OPTIONS, _) = parser.parse_args(argv[1:])
522 if _OPTIONS.produce_manifest:
523 retval = ProduceManifest()
524 elif _OPTIONS.clean_build:
525 retval = CompareBuilds()
526 else:
527 retval = CheckExpectedResults()
529 if retval:
530 return 0
531 else:
532 return 1
535 if __name__ == '__main__':
536 retval = Main(sys.argv)
537 sys.exit(retval)