blob: 9b8d0f5d1bd70615925fe2df88231f8d7f2a355f [file] [log] [blame]
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +00001#!/usr/bin/env python3
Maxim Kuvyrkov59877482021-07-07 11:22:26 +00002
3# Script to compare testsuite failures against a list of known-to-fail
4# tests.
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +00005
Maxim Kuvyrkov59877482021-07-07 11:22:26 +00006# Contributed by Diego Novillo <dnovillo@google.com>
7#
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +00008# Copyright (C) 2011-2023 Free Software Foundation, Inc.
Maxim Kuvyrkov59877482021-07-07 11:22:26 +00009#
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
28detailed 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
34The script must be executed from the toplevel build directory. When
35executed it will:
36
371- Determine the target built: TARGET
382- Determine the source directory: SRCDIR
393- Look for a failure manifest file in
40 <SRCDIR>/<MANIFEST_SUBDIR>/<MANIFEST_NAME>.xfail
414- Collect all the <tool>.sum files from the build tree.
425- 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.
456- 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
48Manifest files contain expected DejaGNU results that are otherwise
49treated as failures.
50They 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
57import datetime
58import optparse
59import os
60import re
61import sys
62
Maxim Kuvyrkov966fac42024-04-01 11:47:27 +000063_INTERESTING_RESULTS = [ 'FAIL', 'UNRESOLVED', 'XPASS', 'ERROR' ]
Maxim Kuvyrkov8704bc12023-05-03 15:03:34 +000064# <STATE>: <NAME> <DESCRIPTION"
Maxim Kuvyrkov966fac42024-04-01 11:47:27 +000065_INTERESTING_RESULTS_REX = re.compile('(%s):\s*(\S+)\s*(.*)'
66 % "|".join(_INTERESTING_RESULTS))
Maxim Kuvyrkov8704bc12023-05-03 15:03:34 +000067
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +000068# Formats of .sum file sections
69_TOOL_LINE_FORMAT = '\t\t=== %s tests ===\n'
Christophe Lyona7d8c4c2023-04-14 12:01:23 +000070_EXP_LINE_FORMAT = '\nRunning %s:%s ...\n'
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +000071_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')
Christophe Lyona7d8c4c2023-04-14 12:01:23 +000075# Match .exp file name, optionally prefixed by a "tool:" name and a
76# path ending with "testsuite/"
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +000077_EXP_LINE_REX = re.compile('^Running (?:.*:)?(.*) \.\.\.\n')
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +000078_SUMMARY_LINE_REX = re.compile('^\t\t=== (.*) Summary ===\n')
79
Maxim Kuvyrkov59877482021-07-07 11:22:26 +000080# 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
93def Error(msg):
Maxim Kuvyrkov63ad5352021-07-04 07:38:22 +000094 print('error: %s' % msg, file=sys.stderr)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +000095 sys.exit(1)
96
97
98class 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.
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000127 tool: Top-level testsuite name (aka "tool" in DejaGnu parlance) of the test.
128 exp: Name of .exp testsuite file.
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000129 """
130
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000131 def __init__(self, summary_line, ordinal, tool, exp):
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000132 try:
133 (self.attrs, summary_line) = SplitAttributesFromSummaryLine(summary_line)
134 try:
135 (self.state,
136 self.name,
Maxim Kuvyrkov966fac42024-04-01 11:47:27 +0000137 self.description) = _INTERESTING_RESULTS_REX.match(summary_line).groups()
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000138 if _OPTIONS.srcpath_regex and _OPTIONS.srcpath_regex != '':
139 self.description = re.sub(_OPTIONS.srcpath_regex, '',
140 self.description)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000141 except:
Thiago Jung Bauermann7b82a592023-04-22 14:12:06 +0000142 print('Failed to parse summary line: "%s"' % summary_line,
143 file=sys.stderr)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000144 raise
145 self.ordinal = ordinal
Maxim Kuvyrkovf09ab0e2021-08-30 14:19:04 +0000146 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.
Thiago Jung Bauermann7b82a592023-04-22 14:12:06 +0000149 print(f'.sum file seems to be broken: tool="{tool}", exp="{exp}", summary_line="{summary_line}"',
150 file=sys.stderr)
Maxim Kuvyrkovf09ab0e2021-08-30 14:19:04 +0000151 raise
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000152 self.tool = tool
153 self.exp = exp
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000154 except ValueError:
155 Error('Cannot parse summary line "%s"' % summary_line)
156
Maxim Kuvyrkov966fac42024-04-01 11:47:27 +0000157 if self.state not in _INTERESTING_RESULTS:
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000158 Error('Invalid test result %s in "%s" (parsed as "%s")' % (
159 self.state, summary_line, self))
160
161 def __lt__(self, other):
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000162 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
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000169
170 def __hash__(self):
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000171 return (hash(self.state) ^ hash(self.tool) ^ hash(self.exp)
172 ^ hash(self.name) ^ hash(self.description))
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000173
Maxim Kuvyrkov7df81782023-05-25 06:42:06 +0000174 # 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.
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000179 def __eq__(self, other):
180 return (self.state == other.state and
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000181 self.tool == other.tool and
182 self.exp == other.exp and
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000183 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:
Maxim Kuvyrkov158e61d2023-05-25 12:18:30 +0000212 return _OPTIONS.expiry_today_date > expiration_date
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000213
214
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000215class 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.
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000222 testsuites: A set of (tool, exp) tuples representing encountered testsuites.
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000223 """
224
225 def __init__(self):
226 super().__init__()
227 self.ResetToolExp()
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000228 self.testsuites=set()
229
230 def update(self, other):
231 super().update(other)
232 self.testsuites.update(other.testsuites)
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000233
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
Christophe Lyona7d8c4c2023-04-14 12:01:23 +0000252 outfile.write(_EXP_LINE_FORMAT % (current_tool, current_exp))
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000253 outfile.write('%s\n' % result)
254
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000255 # Check if testsuite of expected_result is present in current results.
256 # This is used to compare partial test results against a full manifest.
257 def HasTestsuite(self, expected_result):
258 return (expected_result.tool, expected_result.exp) in self.testsuites
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000259
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000260def GetMakefileValue(makefile_name, value_name):
261 if os.path.exists(makefile_name):
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000262 makefile = open(makefile_name, encoding='latin-1', mode='r')
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000263 for line in makefile:
264 if line.startswith(value_name):
265 (_, value) = line.split('=', 1)
266 value = value.strip()
267 makefile.close()
268 return value
269 makefile.close()
270 return None
271
272
273def ValidBuildDirectory(builddir):
274 if (not os.path.exists(builddir) or
275 not os.path.exists('%s/Makefile' % builddir)):
276 return False
277 return True
278
279
280def IsComment(line):
281 """Return True if line is a comment."""
282 return line.startswith('#')
283
284
285def SplitAttributesFromSummaryLine(line):
286 """Splits off attributes from a summary line, if present."""
Maxim Kuvyrkov966fac42024-04-01 11:47:27 +0000287 if '|' in line and not _INTERESTING_RESULTS_REX.match(line):
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000288 (attrs, line) = line.split('|', 1)
289 attrs = attrs.strip()
290 else:
291 attrs = ''
292 line = line.strip()
293 return (attrs, line)
294
295
Thiago Jung Bauermann41505402023-06-14 14:37:00 +0200296def IsInterestingResult(result_set, line):
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000297 """Return True if line is one of the summary lines we care about."""
298 (_, line) = SplitAttributesFromSummaryLine(line)
Maxim Kuvyrkov966fac42024-04-01 11:47:27 +0000299 interesting_result = bool(_INTERESTING_RESULTS_REX.match(line))
Thiago Jung Bauermann41505402023-06-14 14:37:00 +0200300
Maxim Kuvyrkovc0253642024-04-01 11:51:59 +0000301 # If there's no .exp defined it means that either the results section hasn't
Thiago Jung Bauermann41505402023-06-14 14:37:00 +0200302 # started yet, or it is already over.
Maxim Kuvyrkovc0253642024-04-01 11:51:59 +0000303 if interesting_result and result_set.current_exp is None:
Thiago Jung Bauermann41505402023-06-14 14:37:00 +0200304 if _OPTIONS.verbosity >= 3:
Maxim Kuvyrkovc0253642024-04-01 11:51:59 +0000305 print(f'WARNING: Result "{line}" found outside sum file boundaries.')
Thiago Jung Bauermann41505402023-06-14 14:37:00 +0200306 return False
307
Maxim Kuvyrkov966fac42024-04-01 11:47:27 +0000308 return interesting_result
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000309
310
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000311def IsToolLine(line):
312 """Return True if line mentions the tool (in DejaGnu terms) for the following tests."""
313 return bool(_TOOL_LINE_REX.match(line))
314
315
316def IsExpLine(line):
317 """Return True if line mentions the .exp file for the following tests."""
318 return bool(_EXP_LINE_REX.match(line))
319
320
321def IsSummaryLine(line):
322 """Return True if line starts .sum footer."""
323 return bool(_SUMMARY_LINE_REX.match(line))
324
325
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000326def IsInclude(line):
327 """Return True if line is an include of another file."""
328 return line.startswith("@include ")
329
330
331def GetIncludeFile(line, includer):
332 """Extract the name of the include file from line."""
333 includer_dir = os.path.dirname(includer)
334 include_file = line[len("@include "):]
335 return os.path.join(includer_dir, include_file.strip())
336
337
338def IsNegativeResult(line):
339 """Return True if line should be removed from the expected results."""
340 return line.startswith("@remove ")
341
342
343def GetNegativeResult(line):
344 """Extract the name of the negative result from line."""
345 line = line[len("@remove "):]
346 return line.strip()
347
348
349def ParseManifestWorker(result_set, manifest_path):
350 """Read manifest_path, adding the contents to result_set."""
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000351 if _OPTIONS.verbosity >= 5:
Maxim Kuvyrkov63ad5352021-07-04 07:38:22 +0000352 print('Parsing manifest file %s.' % manifest_path)
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000353 manifest_file = open(manifest_path, encoding='latin-1', mode='r')
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000354 for orig_line in manifest_file:
355 line = orig_line.strip()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000356 if line == "":
357 pass
358 elif IsComment(line):
359 pass
360 elif IsNegativeResult(line):
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000361 result_set.remove(result_set.MakeTestResult(GetNegativeResult(line)))
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000362 elif IsInclude(line):
363 ParseManifestWorker(result_set, GetIncludeFile(line, manifest_path))
Thiago Jung Bauermann41505402023-06-14 14:37:00 +0200364 elif IsInterestingResult(result_set, line):
Maxim Kuvyrkova6b29dd2023-04-12 14:35:39 +0000365 result = result_set.MakeTestResult(line)
366 if result.HasExpired():
367 # Ignore expired manifest entries.
Maxim Kuvyrkov7df81782023-05-25 06:42:06 +0000368 if _OPTIONS.verbosity >= 4:
Maxim Kuvyrkova6b29dd2023-04-12 14:35:39 +0000369 print('WARNING: Expected failure "%s" has expired.' % line.strip())
370 continue
371 result_set.add(result)
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000372 elif IsExpLine(orig_line):
373 result_set.current_exp = _EXP_LINE_REX.match(orig_line).groups()[0]
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000374 if _OPTIONS.srcpath_regex and _OPTIONS.srcpath_regex != '':
375 result_set.current_exp = re.sub(_OPTIONS.srcpath_regex, '',
376 result_set.current_exp)
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000377 elif IsToolLine(orig_line):
378 result_set.current_tool = _TOOL_LINE_REX.match(orig_line).groups()[0]
Maxim Kuvyrkovc0253642024-04-01 11:51:59 +0000379 result_set.current_exp = None
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000380 elif IsSummaryLine(orig_line):
381 result_set.ResetToolExp()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000382 else:
383 Error('Unrecognized line in manifest file: %s' % line)
384 manifest_file.close()
385
386
387def ParseManifest(manifest_path):
388 """Create a set of TestResult instances from the given manifest file."""
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000389 result_set = ResultSet()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000390 ParseManifestWorker(result_set, manifest_path)
391 return result_set
392
393
394def ParseSummary(sum_fname):
395 """Create a set of TestResult instances from the given summary file."""
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000396 result_set = ResultSet()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000397 # ordinal is used when sorting the results so that tests within each
398 # .exp file are kept sorted.
399 ordinal=0
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000400 sum_file = open(sum_fname, encoding='latin-1', mode='r')
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000401 for line in sum_file:
Thiago Jung Bauermann41505402023-06-14 14:37:00 +0200402 if IsInterestingResult(result_set, line):
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000403 result = result_set.MakeTestResult(line, ordinal)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000404 ordinal += 1
405 if result.HasExpired():
Maxim Kuvyrkova6b29dd2023-04-12 14:35:39 +0000406 # ??? What is the use-case for this? How "expiry" annotations are
407 # ??? supposed to be added to .sum results?
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000408 # Tests that have expired are not added to the set of expected
409 # results. If they are still present in the set of actual results,
410 # they will cause an error to be reported.
Maxim Kuvyrkov7df81782023-05-25 06:42:06 +0000411 if _OPTIONS.verbosity >= 4:
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000412 print('WARNING: Expected failure "%s" has expired.' % line.strip())
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000413 continue
414 result_set.add(result)
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000415 elif IsExpLine(line):
416 result_set.current_exp = _EXP_LINE_REX.match(line).groups()[0]
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000417 if _OPTIONS.srcpath_regex and _OPTIONS.srcpath_regex != '':
418 result_set.current_exp = re.sub(_OPTIONS.srcpath_regex, '',
419 result_set.current_exp)
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000420 result_set.testsuites.add((result_set.current_tool,
421 result_set.current_exp))
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000422 elif IsToolLine(line):
423 result_set.current_tool = _TOOL_LINE_REX.match(line).groups()[0]
Maxim Kuvyrkovd8951a22021-07-08 08:20:28 +0000424 result_set.current_exp = None
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000425 elif IsSummaryLine(line):
426 result_set.ResetToolExp()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000427 sum_file.close()
428 return result_set
429
430
431def GetManifest(manifest_path):
432 """Build a set of expected failures from the manifest file.
433
434 Each entry in the manifest file should have the format understood
435 by the TestResult constructor.
436
437 If no manifest file exists for this target, it returns an empty set.
438 """
439 if os.path.exists(manifest_path):
440 return ParseManifest(manifest_path)
441 else:
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000442 return ResultSet()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000443
444
445def CollectSumFiles(builddir):
446 sum_files = []
447 for root, dirs, files in os.walk(builddir):
448 for ignored in ('.svn', '.git'):
449 if ignored in dirs:
450 dirs.remove(ignored)
451 for fname in files:
452 if fname.endswith('.sum'):
453 sum_files.append(os.path.join(root, fname))
454 return sum_files
455
456
Maxim Kuvyrkov8ef7c852021-07-08 08:21:18 +0000457def GetResults(sum_files, build_results = None):
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000458 """Collect all the test results from the given .sum files."""
Maxim Kuvyrkov8ef7c852021-07-08 08:21:18 +0000459 if build_results == None:
460 build_results = ResultSet()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000461 for sum_fname in sum_files:
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000462 if _OPTIONS.verbosity >= 3:
463 print('\t%s' % sum_fname)
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000464 build_results.update(ParseSummary(sum_fname))
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000465 return build_results
466
Maxim Kuvyrkovc0014022024-04-01 12:26:44 +0000467class ResultsStats:
468 """Describes statistics of DejaGNU test results.
469
470 Attributes:
471 fails: Number of non-flaky failed tests in the results.
472 flaky: Number of flaky entries in the manifest.
473 total: Total number of tests in the results, including flaky passes and
474 fails.
475 """
476
477 def __init__(self):
478 self.fails = 0
479 self.flaky = 0
480 self.total = 0
481
482 def Print(self, outfile=sys.stdout):
483 outfile.write(_SUMMARY_LINE_FORMAT % 'Results')
484 outfile.write(f'\n')
485 outfile.write(f'# of stable fails\t\t{self.fails}\n')
486 outfile.write(f'# of flaky entries\t\t{self.flaky}\n')
487 outfile.write(f'# of all tests\t\t\t{self.total}\n')
488
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000489
490def CompareResults(manifest, actual):
491 """Compare sets of results and return two lists:
492 - List of results present in ACTUAL but missing from MANIFEST.
493 - List of results present in MANIFEST but missing from ACTUAL.
494 """
495 # Collect all the actual results not present in the manifest.
496 # Results in this set will be reported as errors.
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000497 actual_vs_manifest = ResultSet()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000498 for actual_result in actual:
499 if actual_result not in manifest:
500 actual_vs_manifest.add(actual_result)
501
502 # Collect all the tests in the manifest that were not found
503 # in the actual results.
504 # Results in this set will be reported as warnings (since
505 # they are expected failures that are not failing anymore).
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000506 manifest_vs_actual = ResultSet()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000507 for expected_result in manifest:
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000508 # We try to support comparing partial results vs full manifest
509 # (e.g., manifest has failures for gcc, g++, gfortran, but we ran only
510 # g++ testsuite). To achieve this we record encountered testsuites in
511 # actual.testsuites set, and then we check it here using HasTestsuite().
512 if expected_result not in actual and actual.HasTestsuite(expected_result):
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000513 manifest_vs_actual.add(expected_result)
514
515 return actual_vs_manifest, manifest_vs_actual
516
517
Maxim Kuvyrkov918bc262021-07-08 08:27:39 +0000518def GetManifestPath(user_provided_must_exist):
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000519 """Return the full path to the manifest file."""
520 manifest_path = _OPTIONS.manifest
521 if manifest_path:
522 if user_provided_must_exist and not os.path.exists(manifest_path):
523 Error('Manifest does not exist: %s' % manifest_path)
524 return manifest_path
525 else:
Maxim Kuvyrkov918bc262021-07-08 08:27:39 +0000526 (srcdir, target) = GetBuildData()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000527 if not srcdir:
528 Error('Could not determine the location of GCC\'s source tree. '
529 'The Makefile does not contain a definition for "srcdir".')
530 if not target:
531 Error('Could not determine the target triplet for this build. '
532 'The Makefile does not contain a definition for "target_alias".')
533 return _MANIFEST_PATH_PATTERN % (srcdir, _MANIFEST_SUBDIR, target)
534
535
536def GetBuildData():
537 if not ValidBuildDirectory(_OPTIONS.build_dir):
538 # If we have been given a set of results to use, we may
539 # not be inside a valid GCC build directory. In that case,
540 # the user must provide both a manifest file and a set
541 # of results to check against it.
542 if not _OPTIONS.results or not _OPTIONS.manifest:
543 Error('%s is not a valid GCC top level build directory. '
544 'You must use --manifest and --results to do the validation.' %
545 _OPTIONS.build_dir)
546 else:
547 return None, None
548 srcdir = GetMakefileValue('%s/Makefile' % _OPTIONS.build_dir, 'srcdir =')
549 target = GetMakefileValue('%s/Makefile' % _OPTIONS.build_dir, 'target_alias=')
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000550 if _OPTIONS.verbosity >= 3:
551 print('Source directory: %s' % srcdir)
552 print('Build target: %s' % target)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000553 return srcdir, target
554
555
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000556def PrintSummary(summary):
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000557 summary.Print()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000558
559def GetSumFiles(results, build_dir):
560 if not results:
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000561 if _OPTIONS.verbosity >= 3:
562 print('Getting actual results from build directory %s' % build_dir)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000563 sum_files = CollectSumFiles(build_dir)
564 else:
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000565 if _OPTIONS.verbosity >= 3:
566 print('Getting actual results from user-provided results')
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000567 sum_files = results.split()
568 return sum_files
569
Maxim Kuvyrkoveb482c72024-04-01 12:30:19 +0000570def DiscardFlaky(expected, actual):
571 flaky_list = []
572 for expected_result in expected:
573 if 'flaky' in expected_result.attrs:
574 flaky_list.append(expected_result)
575
576 for expected_result in flaky_list:
577 expected.remove(expected_result)
578 actual.discard(expected_result)
579
580 return len(flaky_list)
581
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000582
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000583def PerformComparison(expected, actual):
Maxim Kuvyrkovc0014022024-04-01 12:26:44 +0000584 stats = ResultsStats()
Maxim Kuvyrkoveb482c72024-04-01 12:30:19 +0000585 stats.total = actual.total
586 # We need to ignore flaky tests in comparison, so remove them now from
587 # both expected and actual sets.
588 stats.flaky = DiscardFlaky(expected, actual)
Maxim Kuvyrkovc0014022024-04-01 12:26:44 +0000589 stats.fails = len(actual)
590
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000591 actual_vs_expected, expected_vs_actual = CompareResults(expected, actual)
592
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000593 if _OPTIONS.inverse_match:
594 # Switch results if inverse comparison is requested.
595 # This is useful in detecting flaky tests that FAILed in expected set,
596 # but PASSed in actual set.
597 actual_vs_expected, expected_vs_actual \
Maxim Kuvyrkov7df81782023-05-25 06:42:06 +0000598 = expected_vs_actual, actual_vs_expected
Maxim Kuvyrkovc0014022024-04-01 12:26:44 +0000599 stats = None
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000600
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000601 tests_ok = True
602 if len(actual_vs_expected) > 0:
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000603 if _OPTIONS.verbosity >= 3:
604 print('\n\nUnexpected results in this build (new failures)')
605 if _OPTIONS.verbosity >= 1:
606 PrintSummary(actual_vs_expected)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000607 tests_ok = False
608
Maxim Kuvyrkovc0014022024-04-01 12:26:44 +0000609 if _OPTIONS.verbosity >= 1 and stats:
610 stats.Print()
611
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000612 if _OPTIONS.verbosity >= 2 and len(expected_vs_actual) > 0:
613 print('\n\nExpected results not present in this build (fixed tests)'
614 '\n\nNOTE: This is not a failure. It just means that these '
615 'tests were expected\nto fail, but either they worked in '
616 'this configuration or they were not\npresent at all.\n')
617 PrintSummary(expected_vs_actual)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000618
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000619 if tests_ok and _OPTIONS.verbosity >= 3:
Maxim Kuvyrkov63ad5352021-07-04 07:38:22 +0000620 print('\nSUCCESS: No unexpected failures.')
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000621
622 return tests_ok
623
624
625def CheckExpectedResults():
Maxim Kuvyrkov918bc262021-07-08 08:27:39 +0000626 manifest_path = GetManifestPath(True)
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000627 if _OPTIONS.verbosity >= 3:
628 print('Manifest: %s' % manifest_path)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000629 manifest = GetManifest(manifest_path)
630 sum_files = GetSumFiles(_OPTIONS.results, _OPTIONS.build_dir)
631 actual = GetResults(sum_files)
632
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000633 if _OPTIONS.verbosity >= 5:
634 print('\n\nTests expected to fail')
635 PrintSummary(manifest)
636 print('\n\nActual test results')
637 PrintSummary(actual)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000638
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000639 return PerformComparison(manifest, actual)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000640
641
642def ProduceManifest():
Maxim Kuvyrkov918bc262021-07-08 08:27:39 +0000643 manifest_path = GetManifestPath(False)
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000644 if _OPTIONS.verbosity >= 3:
645 print('Manifest: %s' % manifest_path)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000646 if os.path.exists(manifest_path) and not _OPTIONS.force:
647 Error('Manifest file %s already exists.\nUse --force to overwrite.' %
648 manifest_path)
649
650 sum_files = GetSumFiles(_OPTIONS.results, _OPTIONS.build_dir)
651 actual = GetResults(sum_files)
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000652 manifest_file = open(manifest_path, encoding='latin-1', mode='w')
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000653 actual.Print(manifest_file)
654 actual.Print()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000655 manifest_file.close()
656
657 return True
658
659
660def CompareBuilds():
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000661 sum_files = GetSumFiles(_OPTIONS.results, _OPTIONS.build_dir)
662 actual = GetResults(sum_files)
663
Maxim Kuvyrkov8ef7c852021-07-08 08:21:18 +0000664 clean = ResultSet()
665
666 if _OPTIONS.manifest:
Maxim Kuvyrkov918bc262021-07-08 08:27:39 +0000667 manifest_path = GetManifestPath(True)
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000668 if _OPTIONS.verbosity >= 3:
669 print('Manifest: %s' % manifest_path)
Maxim Kuvyrkov8ef7c852021-07-08 08:21:18 +0000670 clean = GetManifest(manifest_path)
671
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000672 clean_sum_files = GetSumFiles(_OPTIONS.results, _OPTIONS.clean_build)
Maxim Kuvyrkov8ef7c852021-07-08 08:21:18 +0000673 clean = GetResults(clean_sum_files, clean)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000674
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000675 return PerformComparison(clean, actual)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000676
677
678def Main(argv):
679 parser = optparse.OptionParser(usage=__doc__)
680
681 # Keep the following list sorted by option name.
682 parser.add_option('--build_dir', action='store', type='string',
683 dest='build_dir', default='.',
684 help='Build directory to check (default = .)')
685 parser.add_option('--clean_build', action='store', type='string',
686 dest='clean_build', default=None,
687 help='Compare test results from this build against '
688 'those of another (clean) build. Use this option '
689 'when comparing the test results of your patch versus '
690 'the test results of a clean build without your patch. '
691 'You must provide the path to the top directory of your '
692 'clean build.')
693 parser.add_option('--force', action='store_true', dest='force',
694 default=False, help='When used with --produce_manifest, '
695 'it will overwrite an existing manifest file '
696 '(default = False)')
Maxim Kuvyrkov158e61d2023-05-25 12:18:30 +0000697 parser.add_option('--expiry_date', action='store',
698 dest='expiry_today_date', default=None,
699 help='Use provided date YYYYMMDD to decide whether '
700 'manifest entries with expiry settings have expired '
701 'or not. (default = Use today date)')
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000702 parser.add_option('--srcpath', action='store', type='string',
703 dest='srcpath_regex', default='[^ ]+/testsuite/',
704 help='Remove provided path (can be a regex) from '
705 'the result entries. This is useful to remove '
706 'occasional filesystem path from the results. '
707 '(default = "[^ ]+/testsuite/")')
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000708 parser.add_option('--inverse_match', action='store_true',
709 dest='inverse_match', default=False,
710 help='Inverse result sets in comparison. '
711 'Output unexpected passes as unexpected failures and '
712 'unexpected failures as unexpected passes. '
713 'This is used to catch FAIL->PASS flaky tests. '
714 '(default = False)')
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000715 parser.add_option('--manifest', action='store', type='string',
716 dest='manifest', default=None,
717 help='Name of the manifest file to use (default = '
718 'taken from '
719 'contrib/testsuite-managment/<target_alias>.xfail)')
720 parser.add_option('--produce_manifest', action='store_true',
721 dest='produce_manifest', default=False,
722 help='Produce the manifest for the current '
723 'build (default = False)')
724 parser.add_option('--results', action='store', type='string',
725 dest='results', default=None, help='Space-separated list '
726 'of .sum files with the testing results to check. The '
727 'only content needed from these files are the lines '
728 'starting with FAIL, XPASS or UNRESOLVED (default = '
729 '.sum files collected from the build directory).')
730 parser.add_option('--verbosity', action='store', dest='verbosity',
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000731 type='int', default=3, help='Verbosity level '
732 '(default = 3). Level 0: only error output, this is '
733 'useful in scripting when only the exit code is used. '
734 'Level 1: output unexpected failures. '
735 'Level 2: output unexpected passes. '
736 'Level 3: output helpful information. '
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000737 'Level 4: output notification on expired entries. '
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000738 'Level 5: output debug information.')
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000739 global _OPTIONS
740 (_OPTIONS, _) = parser.parse_args(argv[1:])
741
Maxim Kuvyrkov158e61d2023-05-25 12:18:30 +0000742 # Set "today" date to compare expiration entries against.
743 # Setting expiration date into the future allows re-detection of flaky
744 # tests and creating fresh entries for them before the current flaky entries
745 # expire.
746 if _OPTIONS.expiry_today_date:
747 today_date = re.search(r'(\d\d\d\d)(\d\d)(\d\d)',
748 _OPTIONS.expiry_today_date)
749 if not today_date:
750 Error('Invalid --expiry_today_date format "%s". Must be of the form '
751 '"expire=YYYYMMDD"' % _OPTIONS.expiry_today_date)
752 _OPTIONS.expiry_today_date=datetime.date(int(today_date.group(1)),
753 int(today_date.group(2)),
754 int(today_date.group(3)))
755 else:
756 _OPTIONS.expiry_today_date = datetime.date.today()
757
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000758 if _OPTIONS.produce_manifest:
759 retval = ProduceManifest()
760 elif _OPTIONS.clean_build:
761 retval = CompareBuilds()
762 else:
763 retval = CheckExpectedResults()
764
765 if retval:
766 return 0
767 else:
Maxim Kuvyrkov972bb812021-08-30 14:18:09 +0000768 return 2
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000769
770
771if __name__ == '__main__':
772 retval = Main(sys.argv)
773 sys.exit(retval)