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