3 # Script to compare testsuite failures against a list of known-to-fail
6 # Contributed by Diego Novillo <dnovillo@google.com>
8 # Copyright (C) 2011-2023 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)
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
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
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
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.
81 print('error: %s' % msg
, file=sys
.stderr
)
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
99 attrlist: A comma separated list of attributes.
101 flaky Indicates that this test may not always fail. These
102 tests are reported, but their presence does not affect
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):
118 (self
.attrs
, summary_line
) = SplitAttributesFromSummaryLine(summary_line
)
122 self
.description
) = re
.match(r
'([A-Z]+):\s*(\S+)\s*(.*)',
123 summary_line
).groups()
125 print('Failed to parse summary line: "%s"' % summary_line
)
127 self
.ordinal
= ordinal
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
))
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
)
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
)
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)))
169 def HasExpired(self
):
170 # Return True if the expiration date of this result has passed.
171 expiration_date
= self
.ExpirationDate()
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()
190 def ValidBuildDirectory(builddir
):
191 if (not os
.path
.exists(builddir
) or
192 not os
.path
.exists('%s/Makefile' % builddir
)):
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()
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
))
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 "):]
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
:
251 elif IsComment(line
):
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
))
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."""
267 ParseManifestWorker(result_set
, manifest_path
)
271 def ParseSummary(sum_fname
):
272 """Create a set of TestResult instances from the given summary file."""
274 # ordinal is used when sorting the results so that tests within each
275 # .exp file are kept sorted.
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
)
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())
288 result_set
.add(result
)
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
)
307 def CollectSumFiles(builddir
):
309 for root
, dirs
, files
in os
.walk(builddir
):
310 for ignored
in ('.svn', '.git'):
314 if fname
.endswith('.sum'):
315 sum_files
.append(os
.path
.join(root
, fname
))
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
)
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
:
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
359 if user_provided_must_exist
and not os
.path
.exists(manifest_path
):
360 Error('Manifest does not exist: %s' % manifest_path
)
364 Error('Could not determine the location of GCC\'s source tree. '
365 'The Makefile does not contain a definition for "srcdir".')
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
)
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.' %
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
):
397 def GetSumFiles(results
, build_dir
):
399 print('Getting actual results from build directory %s' % build_dir
)
400 sum_files
= CollectSumFiles(build_dir
)
402 print('Getting actual results from user-provided results')
403 sum_files
= results
.split()
407 def PerformComparison(expected
, actual
, ignore_missing_failures
):
408 actual_vs_expected
, expected_vs_actual
= CompareResults(expected
, actual
)
411 if len(actual_vs_expected
) > 0:
412 PrintSummary('Unexpected results in this build (new failures)',
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',
424 print('\nSUCCESS: No unexpected failures.')
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.' %
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
):
457 manifest_file
.write('%s\n' % result
)
458 manifest_file
.close()
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
)
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 '
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 '
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 '
502 parser
.add_option('--manifest', action
='store', type='string',
503 dest
='manifest', default
=None,
504 help='Name of the manifest file to use (default = '
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)')
520 (_OPTIONS
, _
) = parser
.parse_args(argv
[1:])
522 if _OPTIONS
.produce_manifest
:
523 retval
= ProduceManifest()
524 elif _OPTIONS
.clean_build
:
525 retval
= CompareBuilds()
527 retval
= CheckExpectedResults()
535 if __name__
== '__main__':
536 retval
= Main(sys
.argv
)