validate_failures.py 19 KB

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