Home | History | Annotate | Line # | Download | only in testsuite-management
      1 #!/usr/bin/env python3
      2 
      3 # Script to compare testsuite failures against a list of known-to-fail
      4 # tests.
      5 
      6 # Contributed by Diego Novillo <dnovillo (at] google.com>
      7 #
      8 # Copyright (C) 2011-2024 Free Software Foundation, Inc.
      9 #
     10 # This file is part of GCC.
     11 #
     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.
     16 #
     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.
     21 #
     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.
     26 
     27 """This script provides a coarser XFAILing mechanism that requires no
     28 detailed DejaGNU markings.  This is useful in a variety of scenarios:
     29 
     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.
     33 
     34 The script must be executed from the toplevel build directory.  When
     35 executed it will:
     36 
     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.
     47 
     48 Manifest files contain expected DejaGNU results that are otherwise
     49 treated as failures.
     50 They may also contain additional text:
     51 
     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 """
     56 
     57 import datetime
     58 import optparse
     59 import os
     60 import re
     61 import sys
     62 
     63 _VALID_TEST_RESULTS = [ 'FAIL', 'UNRESOLVED', 'XPASS', 'ERROR' ]
     64 # <STATE>: <NAME> <DESCRIPTION"
     65 _VALID_TEST_RESULTS_REX = re.compile('(%s):\s*(\S+)\s*(.*)'
     66                                      % "|".join(_VALID_TEST_RESULTS))
     67 
     68 # Formats of .sum file sections
     69 _TOOL_LINE_FORMAT = '\t\t=== %s tests ===\n'
     70 _EXP_LINE_FORMAT = '\nRunning %s:%s ...\n'
     71 _SUMMARY_LINE_FORMAT = '\n\t\t=== %s Summary ===\n'
     72 
     73 # ... and their compiled regexs.
     74 _TOOL_LINE_REX = re.compile('^\t\t=== (.*) tests ===\n')
     75 # Match .exp file name, optionally prefixed by a "tool:" name and a
     76 # path ending with "testsuite/"
     77 _EXP_LINE_REX = re.compile('^Running (?:.*:)?(.*) \.\.\.\n')
     78 _SUMMARY_LINE_REX = re.compile('^\t\t=== (.*) Summary ===\n')
     79 
     80 # Subdirectory of srcdir in which to find the manifest file.
     81 _MANIFEST_SUBDIR = 'contrib/testsuite-management'
     82 
     83 # Pattern for naming manifest files.
     84 # The first argument should be the toplevel GCC(/GNU tool) source directory.
     85 # The second argument is the manifest subdir.
     86 # The third argument is the manifest target, which defaults to the target
     87 # triplet used during the build.
     88 _MANIFEST_PATH_PATTERN = '%s/%s/%s.xfail'
     89 
     90 # The options passed to the program.
     91 _OPTIONS = None
     92 
     93 def Error(msg):
     94   print('error: %s' % msg, file=sys.stderr)
     95   sys.exit(1)
     96 
     97 
     98 class TestResult(object):
     99   """Describes a single DejaGNU test result as emitted in .sum files.
    100 
    101   We are only interested in representing unsuccessful tests.  So, only
    102   a subset of all the tests are loaded.
    103 
    104   The summary line used to build the test result should have this format:
    105 
    106   attrlist | XPASS: gcc.dg/unroll_1.c (test for excess errors)
    107   ^^^^^^^^   ^^^^^  ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
    108   optional   state  name              description
    109   attributes
    110 
    111   Attributes:
    112     attrlist: A comma separated list of attributes.
    113       Valid values:
    114         flaky            Indicates that this test may not always fail.  These
    115                          tests are reported, but their presence does not affect
    116                          the results.
    117 
    118         expire=YYYYMMDD  After this date, this test will produce an error
    119                          whether it is in the manifest or not.
    120 
    121     state: One of UNRESOLVED, XPASS or FAIL.
    122     name: File name for the test.
    123     description: String describing the test (flags used, dejagnu message, etc)
    124     ordinal: Monotonically increasing integer.
    125              It is used to keep results for one .exp file sorted
    126              by the order the tests were run.
    127     tool: Top-level testsuite name (aka "tool" in DejaGnu parlance) of the test.
    128     exp: Name of .exp testsuite file.
    129   """
    130 
    131   def __init__(self, summary_line, ordinal, tool, exp):
    132     try:
    133       (self.attrs, summary_line) = SplitAttributesFromSummaryLine(summary_line)
    134       try:
    135         (self.state,
    136          self.name,
    137          self.description) = _VALID_TEST_RESULTS_REX.match(summary_line).groups()
    138         if _OPTIONS.srcpath_regex and _OPTIONS.srcpath_regex != '':
    139           self.description = re.sub(_OPTIONS.srcpath_regex, '',
    140                                     self.description)
    141       except:
    142         print('Failed to parse summary line: "%s"' % summary_line,
    143               file=sys.stderr)
    144         raise
    145       self.ordinal = ordinal
    146       if tool == None or exp == None:
    147         # .sum file seem to be broken.  There was no "tool" and/or "exp"
    148         # lines preceding this result.
    149         print(f'.sum file seems to be broken: tool="{tool}", exp="{exp}", summary_line="{summary_line}"',
    150               file=sys.stderr)
    151         raise
    152       self.tool = tool
    153       self.exp = exp
    154     except ValueError:
    155       Error('Cannot parse summary line "%s"' % summary_line)
    156 
    157     if self.state not in _VALID_TEST_RESULTS:
    158       Error('Invalid test result %s in "%s" (parsed as "%s")' % (
    159             self.state, summary_line, self))
    160 
    161   def __lt__(self, other):
    162     if (self.tool != other.tool):
    163       return self.tool < other.tool
    164     if (self.exp != other.exp):
    165       return self.exp < other.exp
    166     if (self.name != other.name):
    167       return self.name < other.name
    168     return self.ordinal < other.ordinal
    169 
    170   def __hash__(self):
    171     return (hash(self.state) ^ hash(self.tool) ^ hash(self.exp)
    172             ^ hash(self.name) ^ hash(self.description))
    173 
    174   # Note that we don't include "attrs" in this comparison.  This means that
    175   # result entries "FAIL: test" and "flaky | FAIL: test" are considered
    176   # the same.  Therefore the ResultSet will preserve only the first occurence.
    177   # In practice this means that flaky entries should preceed expected fails
    178   # entries.
    179   def __eq__(self, other):
    180     return (self.state == other.state and
    181             self.tool == other.tool and
    182             self.exp == other.exp and
    183             self.name == other.name and
    184             self.description == other.description)
    185 
    186   def __ne__(self, other):
    187     return not (self == other)
    188 
    189   def __str__(self):
    190     attrs = ''
    191     if self.attrs:
    192       attrs = '%s | ' % self.attrs
    193     return '%s%s: %s %s' % (attrs, self.state, self.name, self.description)
    194 
    195   def ExpirationDate(self):
    196     # Return a datetime.date object with the expiration date for this
    197     # test result.  Return None, if no expiration has been set.
    198     if re.search(r'expire=', self.attrs):
    199       expiration = re.search(r'expire=(\d\d\d\d)(\d\d)(\d\d)', self.attrs)
    200       if not expiration:
    201         Error('Invalid expire= format in "%s".  Must be of the form '
    202               '"expire=YYYYMMDD"' % self)
    203       return datetime.date(int(expiration.group(1)),
    204                            int(expiration.group(2)),
    205                            int(expiration.group(3)))
    206     return None
    207 
    208   def HasExpired(self):
    209     # Return True if the expiration date of this result has passed.
    210     expiration_date = self.ExpirationDate()
    211     if expiration_date:
    212       return _OPTIONS.expiry_today_date > expiration_date
    213 
    214 
    215 class ResultSet(set):
    216   """Describes a set of DejaGNU test results.
    217   This set can be read in from .sum files or emitted as a manifest.
    218 
    219   Attributes:
    220     current_tool: Name of the current top-level DejaGnu testsuite.
    221     current_exp: Name of the current .exp testsuite file.
    222     testsuites: A set of (tool, exp) tuples representing encountered testsuites.
    223   """
    224 
    225   def __init__(self):
    226     super().__init__()
    227     self.ResetToolExp()
    228     self.testsuites=set()
    229 
    230   def update(self, other):
    231     super().update(other)
    232     self.testsuites.update(other.testsuites)
    233 
    234   def ResetToolExp(self):
    235     self.current_tool = None
    236     self.current_exp = None
    237 
    238   def MakeTestResult(self, summary_line, ordinal=-1):
    239     return TestResult(summary_line, ordinal,
    240                       self.current_tool, self.current_exp)
    241 
    242   def Print(self, outfile=sys.stdout):
    243     current_tool = None
    244     current_exp = None
    245 
    246     for result in sorted(self):
    247       if current_tool != result.tool:
    248         current_tool = result.tool
    249         outfile.write(_TOOL_LINE_FORMAT % current_tool)
    250       if current_exp != result.exp:
    251         current_exp = result.exp
    252         outfile.write(_EXP_LINE_FORMAT % (current_tool, current_exp))
    253       outfile.write('%s\n' % result)
    254 
    255     outfile.write(_SUMMARY_LINE_FORMAT % 'Results')
    256 
    257   # Check if testsuite of expected_result is present in current results.
    258   # This is used to compare partial test results against a full manifest.
    259   def HasTestsuite(self, expected_result):
    260     return (expected_result.tool, expected_result.exp) in self.testsuites
    261 
    262 def GetMakefileValue(makefile_name, value_name):
    263   if os.path.exists(makefile_name):
    264     makefile = open(makefile_name, encoding='latin-1', mode='r')
    265     for line in makefile:
    266       if line.startswith(value_name):
    267         (_, value) = line.split('=', 1)
    268         value = value.strip()
    269         makefile.close()
    270         return value
    271     makefile.close()
    272   return None
    273 
    274 
    275 def ValidBuildDirectory(builddir):
    276   if (not os.path.exists(builddir) or
    277       not os.path.exists('%s/Makefile' % builddir)):
    278     return False
    279   return True
    280 
    281 
    282 def IsComment(line):
    283   """Return True if line is a comment."""
    284   return line.startswith('#')
    285 
    286 
    287 def SplitAttributesFromSummaryLine(line):
    288   """Splits off attributes from a summary line, if present."""
    289   if '|' in line and not _VALID_TEST_RESULTS_REX.match(line):
    290     (attrs, line) = line.split('|', 1)
    291     attrs = attrs.strip()
    292   else:
    293     attrs = ''
    294   line = line.strip()
    295   return (attrs, line)
    296 
    297 
    298 def IsInterestingResult(result_set, line):
    299   """Return True if line is one of the summary lines we care about."""
    300   (_, line) = SplitAttributesFromSummaryLine(line)
    301   valid_result = bool(_VALID_TEST_RESULTS_REX.match(line))
    302 
    303   # If there's no tool defined it means that either the results section hasn't
    304   # started yet, or it is already over.
    305   if valid_result and result_set.current_tool is None:
    306     if _OPTIONS.verbosity >= 3:
    307       print(f'WARNING: Result "{line}" found outside sum file boundaries.',
    308             file=sys.stderr)
    309     return False
    310 
    311   return valid_result
    312 
    313 
    314 def IsToolLine(line):
    315   """Return True if line mentions the tool (in DejaGnu terms) for the following tests."""
    316   return bool(_TOOL_LINE_REX.match(line))
    317 
    318 
    319 def IsExpLine(line):
    320   """Return True if line mentions the .exp file for the following tests."""
    321   return bool(_EXP_LINE_REX.match(line))
    322 
    323 
    324 def IsSummaryLine(line):
    325   """Return True if line starts .sum footer."""
    326   return bool(_SUMMARY_LINE_REX.match(line))
    327 
    328 
    329 def IsInclude(line):
    330   """Return True if line is an include of another file."""
    331   return line.startswith("@include ")
    332 
    333 
    334 def GetIncludeFile(line, includer):
    335   """Extract the name of the include file from line."""
    336   includer_dir = os.path.dirname(includer)
    337   include_file = line[len("@include "):]
    338   return os.path.join(includer_dir, include_file.strip())
    339 
    340 
    341 def IsNegativeResult(line):
    342   """Return True if line should be removed from the expected results."""
    343   return line.startswith("@remove ")
    344 
    345 
    346 def GetNegativeResult(line):
    347   """Extract the name of the negative result from line."""
    348   line = line[len("@remove "):]
    349   return line.strip()
    350 
    351 
    352 def ParseManifestWorker(result_set, manifest_path):
    353   """Read manifest_path, adding the contents to result_set."""
    354   if _OPTIONS.verbosity >= 5:
    355     print('Parsing manifest file %s.' % manifest_path)
    356   manifest_file = open(manifest_path, encoding='latin-1', mode='r')
    357   for orig_line in manifest_file:
    358     line = orig_line.strip()
    359     if line == "":
    360       pass
    361     elif IsComment(line):
    362       pass
    363     elif IsNegativeResult(line):
    364       result_set.remove(result_set.MakeTestResult(GetNegativeResult(line)))
    365     elif IsInclude(line):
    366       ParseManifestWorker(result_set, GetIncludeFile(line, manifest_path))
    367     elif IsInterestingResult(result_set, line):
    368       result = result_set.MakeTestResult(line)
    369       if result.HasExpired():
    370         # Ignore expired manifest entries.
    371         if _OPTIONS.verbosity >= 4:
    372           print('WARNING: Expected failure "%s" has expired.' % line.strip())
    373         continue
    374       result_set.add(result)
    375     elif IsExpLine(orig_line):
    376       result_set.current_exp = _EXP_LINE_REX.match(orig_line).groups()[0]
    377       if _OPTIONS.srcpath_regex and _OPTIONS.srcpath_regex != '':
    378         result_set.current_exp = re.sub(_OPTIONS.srcpath_regex, '',
    379                                         result_set.current_exp)
    380     elif IsToolLine(orig_line):
    381       result_set.current_tool = _TOOL_LINE_REX.match(orig_line).groups()[0]
    382     elif IsSummaryLine(orig_line):
    383       result_set.ResetToolExp()
    384     else:
    385       Error('Unrecognized line in manifest file: %s' % line)
    386   manifest_file.close()
    387 
    388 
    389 def ParseManifest(manifest_path):
    390   """Create a set of TestResult instances from the given manifest file."""
    391   result_set = ResultSet()
    392   ParseManifestWorker(result_set, manifest_path)
    393   return result_set
    394 
    395 
    396 def ParseSummary(sum_fname):
    397   """Create a set of TestResult instances from the given summary file."""
    398   result_set = ResultSet()
    399   # ordinal is used when sorting the results so that tests within each
    400   # .exp file are kept sorted.
    401   ordinal=0
    402   sum_file = open(sum_fname, encoding='latin-1', mode='r')
    403   for line in sum_file:
    404     if IsInterestingResult(result_set, line):
    405       result = result_set.MakeTestResult(line, ordinal)
    406       ordinal += 1
    407       if result.HasExpired():
    408         # ??? What is the use-case for this?  How "expiry" annotations are
    409         # ??? supposed to be added to .sum results?
    410         # Tests that have expired are not added to the set of expected
    411         # results. If they are still present in the set of actual results,
    412         # they will cause an error to be reported.
    413         if _OPTIONS.verbosity >= 4:
    414           print('WARNING: Expected failure "%s" has expired.' % line.strip())
    415         continue
    416       result_set.add(result)
    417     elif IsExpLine(line):
    418       result_set.current_exp = _EXP_LINE_REX.match(line).groups()[0]
    419       if _OPTIONS.srcpath_regex and _OPTIONS.srcpath_regex != '':
    420         result_set.current_exp = re.sub(_OPTIONS.srcpath_regex, '',
    421                                         result_set.current_exp)
    422       result_set.testsuites.add((result_set.current_tool,
    423                                  result_set.current_exp))
    424     elif IsToolLine(line):
    425       result_set.current_tool = _TOOL_LINE_REX.match(line).groups()[0]
    426       result_set.current_exp = None
    427     elif IsSummaryLine(line):
    428       result_set.ResetToolExp()
    429   sum_file.close()
    430   return result_set
    431 
    432 
    433 def GetManifest(manifest_path):
    434   """Build a set of expected failures from the manifest file.
    435 
    436   Each entry in the manifest file should have the format understood
    437   by the TestResult constructor.
    438 
    439   If no manifest file exists for this target, it returns an empty set.
    440   """
    441   if os.path.exists(manifest_path):
    442     return ParseManifest(manifest_path)
    443   else:
    444     return ResultSet()
    445 
    446 
    447 def CollectSumFiles(builddir):
    448   sum_files = []
    449   for root, dirs, files in os.walk(builddir):
    450     for ignored in ('.svn', '.git'):
    451       if ignored in dirs:
    452         dirs.remove(ignored)
    453     for fname in files:
    454       if fname.endswith('.sum'):
    455         sum_files.append(os.path.join(root, fname))
    456   return sum_files
    457 
    458 
    459 def GetResults(sum_files, build_results = None):
    460   """Collect all the test results from the given .sum files."""
    461   if build_results == None:
    462     build_results = ResultSet()
    463   for sum_fname in sum_files:
    464     if _OPTIONS.verbosity >= 3:
    465       print('\t%s' % sum_fname)
    466     build_results.update(ParseSummary(sum_fname))
    467   return build_results
    468 
    469 
    470 def CompareResults(manifest, actual):
    471   """Compare sets of results and return two lists:
    472      - List of results present in ACTUAL but missing from MANIFEST.
    473      - List of results present in MANIFEST but missing from ACTUAL.
    474   """
    475   # Collect all the actual results not present in the manifest.
    476   # Results in this set will be reported as errors.
    477   actual_vs_manifest = ResultSet()
    478   for actual_result in actual:
    479     if actual_result not in manifest:
    480       actual_vs_manifest.add(actual_result)
    481 
    482   # Collect all the tests in the manifest that were not found
    483   # in the actual results.
    484   # Results in this set will be reported as warnings (since
    485   # they are expected failures that are not failing anymore).
    486   manifest_vs_actual = ResultSet()
    487   for expected_result in manifest:
    488     # Ignore tests marked flaky.
    489     if 'flaky' in expected_result.attrs:
    490       continue
    491     # We try to support comparing partial results vs full manifest
    492     # (e.g., manifest has failures for gcc, g++, gfortran, but we ran only
    493     # g++ testsuite).  To achieve this we record encountered testsuites in
    494     # actual.testsuites set, and then we check it here using HasTestsuite().
    495     if expected_result not in actual and actual.HasTestsuite(expected_result):
    496       manifest_vs_actual.add(expected_result)
    497 
    498   return actual_vs_manifest, manifest_vs_actual
    499 
    500 
    501 def GetManifestPath(user_provided_must_exist):
    502   """Return the full path to the manifest file."""
    503   manifest_path = _OPTIONS.manifest
    504   if manifest_path:
    505     if user_provided_must_exist and not os.path.exists(manifest_path):
    506       Error('Manifest does not exist: %s' % manifest_path)
    507     return manifest_path
    508   else:
    509     (srcdir, target) = GetBuildData()
    510     if not srcdir:
    511       Error('Could not determine the location of GCC\'s source tree. '
    512             'The Makefile does not contain a definition for "srcdir".')
    513     if not target:
    514       Error('Could not determine the target triplet for this build. '
    515             'The Makefile does not contain a definition for "target_alias".')
    516     return _MANIFEST_PATH_PATTERN % (srcdir, _MANIFEST_SUBDIR, target)
    517 
    518 
    519 def GetBuildData():
    520   if not ValidBuildDirectory(_OPTIONS.build_dir):
    521     # If we have been given a set of results to use, we may
    522     # not be inside a valid GCC build directory.  In that case,
    523     # the user must provide both a manifest file and a set
    524     # of results to check against it.
    525     if not _OPTIONS.results or not _OPTIONS.manifest:
    526       Error('%s is not a valid GCC top level build directory. '
    527             'You must use --manifest and --results to do the validation.' %
    528             _OPTIONS.build_dir)
    529     else:
    530       return None, None
    531   srcdir = GetMakefileValue('%s/Makefile' % _OPTIONS.build_dir, 'srcdir =')
    532   target = GetMakefileValue('%s/Makefile' % _OPTIONS.build_dir, 'target_alias=')
    533   if _OPTIONS.verbosity >= 3:
    534     print('Source directory: %s' % srcdir)
    535     print('Build target:     %s' % target)
    536   return srcdir, target
    537 
    538 
    539 def PrintSummary(summary):
    540   summary.Print()
    541 
    542 def GetSumFiles(results, build_dir):
    543   if not results:
    544     if _OPTIONS.verbosity >= 3:
    545       print('Getting actual results from build directory %s' % build_dir)
    546     sum_files = CollectSumFiles(build_dir)
    547   else:
    548     if _OPTIONS.verbosity >= 3:
    549       print('Getting actual results from user-provided results')
    550     sum_files = results.split()
    551   return sum_files
    552 
    553 
    554 def PerformComparison(expected, actual):
    555   actual_vs_expected, expected_vs_actual = CompareResults(expected, actual)
    556 
    557   if _OPTIONS.inverse_match:
    558     # Switch results if inverse comparison is requested.
    559     # This is useful in detecting flaky tests that FAILed in expected set,
    560     # but PASSed in actual set.
    561     actual_vs_expected, expected_vs_actual \
    562       = expected_vs_actual, actual_vs_expected
    563 
    564   tests_ok = True
    565   if len(actual_vs_expected) > 0:
    566     if _OPTIONS.verbosity >= 3:
    567       print('\n\nUnexpected results in this build (new failures)')
    568     if _OPTIONS.verbosity >= 1:
    569       PrintSummary(actual_vs_expected)
    570     tests_ok = False
    571 
    572   if _OPTIONS.verbosity >= 2 and len(expected_vs_actual) > 0:
    573     print('\n\nExpected results not present in this build (fixed tests)'
    574           '\n\nNOTE: This is not a failure.  It just means that these '
    575           'tests were expected\nto fail, but either they worked in '
    576           'this configuration or they were not\npresent at all.\n')
    577     PrintSummary(expected_vs_actual)
    578 
    579   if tests_ok and _OPTIONS.verbosity >= 3:
    580     print('\nSUCCESS: No unexpected failures.')
    581 
    582   return tests_ok
    583 
    584 
    585 def CheckExpectedResults():
    586   manifest_path = GetManifestPath(True)
    587   if _OPTIONS.verbosity >= 3:
    588     print('Manifest:         %s' % manifest_path)
    589   manifest = GetManifest(manifest_path)
    590   sum_files = GetSumFiles(_OPTIONS.results, _OPTIONS.build_dir)
    591   actual = GetResults(sum_files)
    592 
    593   if _OPTIONS.verbosity >= 5:
    594     print('\n\nTests expected to fail')
    595     PrintSummary(manifest)
    596     print('\n\nActual test results')
    597     PrintSummary(actual)
    598 
    599   return PerformComparison(manifest, actual)
    600 
    601 
    602 def ProduceManifest():
    603   manifest_path = GetManifestPath(False)
    604   if _OPTIONS.verbosity >= 3:
    605     print('Manifest:         %s' % manifest_path)
    606   if os.path.exists(manifest_path) and not _OPTIONS.force:
    607     Error('Manifest file %s already exists.\nUse --force to overwrite.' %
    608           manifest_path)
    609 
    610   sum_files = GetSumFiles(_OPTIONS.results, _OPTIONS.build_dir)
    611   actual = GetResults(sum_files)
    612   manifest_file = open(manifest_path, encoding='latin-1', mode='w')
    613   actual.Print(manifest_file)
    614   actual.Print()
    615   manifest_file.close()
    616 
    617   return True
    618 
    619 
    620 def CompareBuilds():
    621   sum_files = GetSumFiles(_OPTIONS.results, _OPTIONS.build_dir)
    622   actual = GetResults(sum_files)
    623 
    624   clean = ResultSet()
    625 
    626   if _OPTIONS.manifest:
    627     manifest_path = GetManifestPath(True)
    628     if _OPTIONS.verbosity >= 3:
    629       print('Manifest:         %s' % manifest_path)
    630     clean = GetManifest(manifest_path)
    631 
    632   clean_sum_files = GetSumFiles(_OPTIONS.results, _OPTIONS.clean_build)
    633   clean = GetResults(clean_sum_files, clean)
    634 
    635   return PerformComparison(clean, actual)
    636 
    637 
    638 def Main(argv):
    639   parser = optparse.OptionParser(usage=__doc__)
    640 
    641   # Keep the following list sorted by option name.
    642   parser.add_option('--build_dir', action='store', type='string',
    643                     dest='build_dir', default='.',
    644                     help='Build directory to check (default = .)')
    645   parser.add_option('--clean_build', action='store', type='string',
    646                     dest='clean_build', default=None,
    647                     help='Compare test results from this build against '
    648                     'those of another (clean) build.  Use this option '
    649                     'when comparing the test results of your patch versus '
    650                     'the test results of a clean build without your patch. '
    651                     'You must provide the path to the top directory of your '
    652                     'clean build.')
    653   parser.add_option('--force', action='store_true', dest='force',
    654                     default=False, help='When used with --produce_manifest, '
    655                     'it will overwrite an existing manifest file '
    656                     '(default = False)')
    657   parser.add_option('--expiry_date', action='store',
    658                     dest='expiry_today_date', default=None,
    659                     help='Use provided date YYYYMMDD to decide whether '
    660                     'manifest entries with expiry settings have expired '
    661                     'or not. (default = Use today date)')
    662   parser.add_option('--srcpath', action='store', type='string',
    663                     dest='srcpath_regex', default='[^ ]+/testsuite/',
    664                     help='Remove provided path (can be a regex) from '
    665                     'the result entries.  This is useful to remove '
    666                     'occasional filesystem path from the results. '
    667                     '(default = "[^ ]+/testsuite/")')
    668   parser.add_option('--inverse_match', action='store_true',
    669                     dest='inverse_match', default=False,
    670                     help='Inverse result sets in comparison. '
    671                     'Output unexpected passes as unexpected failures and '
    672                     'unexpected failures as unexpected passes. '
    673                     'This is used to catch FAIL->PASS flaky tests. '
    674                     '(default = False)')
    675   parser.add_option('--manifest', action='store', type='string',
    676                     dest='manifest', default=None,
    677                     help='Name of the manifest file to use (default = '
    678                     'taken from '
    679                     'contrib/testsuite-managment/<target_alias>.xfail)')
    680   parser.add_option('--produce_manifest', action='store_true',
    681                     dest='produce_manifest', default=False,
    682                     help='Produce the manifest for the current '
    683                     'build (default = False)')
    684   parser.add_option('--results', action='store', type='string',
    685                     dest='results', default=None, help='Space-separated list '
    686                     'of .sum files with the testing results to check. The '
    687                     'only content needed from these files are the lines '
    688                     'starting with FAIL, XPASS or UNRESOLVED (default = '
    689                     '.sum files collected from the build directory).')
    690   parser.add_option('--verbosity', action='store', dest='verbosity',
    691                     type='int', default=3, help='Verbosity level '
    692                     '(default = 3). Level 0: only error output, this is '
    693                     'useful in scripting when only the exit code is used. '
    694                     'Level 1: output unexpected failures. '
    695                     'Level 2: output unexpected passes. '
    696                     'Level 3: output helpful information. '
    697                     'Level 4: output notification on expired entries. '
    698                     'Level 5: output debug information.')
    699   global _OPTIONS
    700   (_OPTIONS, _) = parser.parse_args(argv[1:])
    701 
    702   # Set "today" date to compare expiration entries against.
    703   # Setting expiration date into the future allows re-detection of flaky
    704   # tests and creating fresh entries for them before the current flaky entries
    705   # expire.
    706   if _OPTIONS.expiry_today_date:
    707     today_date = re.search(r'(\d\d\d\d)(\d\d)(\d\d)',
    708                            _OPTIONS.expiry_today_date)
    709     if not today_date:
    710         Error('Invalid --expiry_today_date format "%s".  Must be of the form '
    711               '"expire=YYYYMMDD"' % _OPTIONS.expiry_today_date)
    712     _OPTIONS.expiry_today_date=datetime.date(int(today_date.group(1)),
    713                                              int(today_date.group(2)),
    714                                              int(today_date.group(3)))
    715   else:
    716     _OPTIONS.expiry_today_date = datetime.date.today()
    717 
    718   if _OPTIONS.produce_manifest:
    719     retval = ProduceManifest()
    720   elif _OPTIONS.clean_build:
    721     retval = CompareBuilds()
    722   else:
    723     retval = CheckExpectedResults()
    724 
    725   if retval:
    726     return 0
    727   else:
    728     return 2
    729 
    730 
    731 if __name__ == '__main__':
    732   retval = Main(sys.argv)
    733   sys.exit(retval)
    734