blob: 8c30b58daabe14861cfc5b063657744c5a318809 [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 Kuvyrkov88799c62024-04-01 12:33:09 +000068_VALID_RESULTS_REX = re.compile('([A-Z]+):\s*(\S+)\s*(.*)')
69
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +000070# Formats of .sum file sections
71_TOOL_LINE_FORMAT = '\t\t=== %s tests ===\n'
Christophe Lyona7d8c4c2023-04-14 12:01:23 +000072_EXP_LINE_FORMAT = '\nRunning %s:%s ...\n'
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +000073_SUMMARY_LINE_FORMAT = '\n\t\t=== %s Summary ===\n'
74
75# ... and their compiled regexs.
76_TOOL_LINE_REX = re.compile('^\t\t=== (.*) tests ===\n')
Christophe Lyona7d8c4c2023-04-14 12:01:23 +000077# Match .exp file name, optionally prefixed by a "tool:" name and a
78# path ending with "testsuite/"
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +000079_EXP_LINE_REX = re.compile('^Running (?:.*:)?(.*) \.\.\.\n')
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +000080_SUMMARY_LINE_REX = re.compile('^\t\t=== (.*) Summary ===\n')
81
Maxim Kuvyrkov59877482021-07-07 11:22:26 +000082# Subdirectory of srcdir in which to find the manifest file.
83_MANIFEST_SUBDIR = 'contrib/testsuite-management'
84
85# Pattern for naming manifest files.
86# The first argument should be the toplevel GCC(/GNU tool) source directory.
87# The second argument is the manifest subdir.
88# The third argument is the manifest target, which defaults to the target
89# triplet used during the build.
90_MANIFEST_PATH_PATTERN = '%s/%s/%s.xfail'
91
92# The options passed to the program.
93_OPTIONS = None
94
95def Error(msg):
Maxim Kuvyrkov63ad5352021-07-04 07:38:22 +000096 print('error: %s' % msg, file=sys.stderr)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +000097 sys.exit(1)
98
99
100class TestResult(object):
101 """Describes a single DejaGNU test result as emitted in .sum files.
102
103 We are only interested in representing unsuccessful tests. So, only
104 a subset of all the tests are loaded.
105
106 The summary line used to build the test result should have this format:
107
108 attrlist | XPASS: gcc.dg/unroll_1.c (test for excess errors)
109 ^^^^^^^^ ^^^^^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
110 optional state name description
111 attributes
112
113 Attributes:
114 attrlist: A comma separated list of attributes.
115 Valid values:
116 flaky Indicates that this test may not always fail. These
117 tests are reported, but their presence does not affect
118 the results.
119
120 expire=YYYYMMDD After this date, this test will produce an error
121 whether it is in the manifest or not.
122
123 state: One of UNRESOLVED, XPASS or FAIL.
124 name: File name for the test.
125 description: String describing the test (flags used, dejagnu message, etc)
126 ordinal: Monotonically increasing integer.
127 It is used to keep results for one .exp file sorted
128 by the order the tests were run.
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000129 tool: Top-level testsuite name (aka "tool" in DejaGnu parlance) of the test.
130 exp: Name of .exp testsuite file.
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000131 """
132
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000133 def __init__(self, summary_line, ordinal, tool, exp):
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000134 try:
135 (self.attrs, summary_line) = SplitAttributesFromSummaryLine(summary_line)
136 try:
137 (self.state,
138 self.name,
Maxim Kuvyrkov966fac42024-04-01 11:47:27 +0000139 self.description) = _INTERESTING_RESULTS_REX.match(summary_line).groups()
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000140 if _OPTIONS.srcpath_regex and _OPTIONS.srcpath_regex != '':
141 self.description = re.sub(_OPTIONS.srcpath_regex, '',
142 self.description)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000143 except:
Thiago Jung Bauermann7b82a592023-04-22 14:12:06 +0000144 print('Failed to parse summary line: "%s"' % summary_line,
145 file=sys.stderr)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000146 raise
147 self.ordinal = ordinal
Maxim Kuvyrkovf09ab0e2021-08-30 14:19:04 +0000148 if tool == None or exp == None:
149 # .sum file seem to be broken. There was no "tool" and/or "exp"
150 # lines preceding this result.
Thiago Jung Bauermann7b82a592023-04-22 14:12:06 +0000151 print(f'.sum file seems to be broken: tool="{tool}", exp="{exp}", summary_line="{summary_line}"',
152 file=sys.stderr)
Maxim Kuvyrkovf09ab0e2021-08-30 14:19:04 +0000153 raise
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000154 self.tool = tool
155 self.exp = exp
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000156 except ValueError:
157 Error('Cannot parse summary line "%s"' % summary_line)
158
Maxim Kuvyrkov966fac42024-04-01 11:47:27 +0000159 if self.state not in _INTERESTING_RESULTS:
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000160 Error('Invalid test result %s in "%s" (parsed as "%s")' % (
161 self.state, summary_line, self))
162
163 def __lt__(self, other):
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000164 if (self.tool != other.tool):
165 return self.tool < other.tool
166 if (self.exp != other.exp):
167 return self.exp < other.exp
168 if (self.name != other.name):
169 return self.name < other.name
170 return self.ordinal < other.ordinal
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000171
172 def __hash__(self):
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000173 return (hash(self.state) ^ hash(self.tool) ^ hash(self.exp)
174 ^ hash(self.name) ^ hash(self.description))
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000175
Maxim Kuvyrkov7df81782023-05-25 06:42:06 +0000176 # Note that we don't include "attrs" in this comparison. This means that
177 # result entries "FAIL: test" and "flaky | FAIL: test" are considered
178 # the same. Therefore the ResultSet will preserve only the first occurence.
179 # In practice this means that flaky entries should preceed expected fails
180 # entries.
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000181 def __eq__(self, other):
182 return (self.state == other.state and
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000183 self.tool == other.tool and
184 self.exp == other.exp and
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000185 self.name == other.name and
186 self.description == other.description)
187
188 def __ne__(self, other):
189 return not (self == other)
190
191 def __str__(self):
192 attrs = ''
193 if self.attrs:
194 attrs = '%s | ' % self.attrs
195 return '%s%s: %s %s' % (attrs, self.state, self.name, self.description)
196
197 def ExpirationDate(self):
198 # Return a datetime.date object with the expiration date for this
199 # test result. Return None, if no expiration has been set.
200 if re.search(r'expire=', self.attrs):
201 expiration = re.search(r'expire=(\d\d\d\d)(\d\d)(\d\d)', self.attrs)
202 if not expiration:
203 Error('Invalid expire= format in "%s". Must be of the form '
204 '"expire=YYYYMMDD"' % self)
205 return datetime.date(int(expiration.group(1)),
206 int(expiration.group(2)),
207 int(expiration.group(3)))
208 return None
209
210 def HasExpired(self):
211 # Return True if the expiration date of this result has passed.
212 expiration_date = self.ExpirationDate()
213 if expiration_date:
Maxim Kuvyrkov158e61d2023-05-25 12:18:30 +0000214 return _OPTIONS.expiry_today_date > expiration_date
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000215
216
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000217class ResultSet(set):
218 """Describes a set of DejaGNU test results.
219 This set can be read in from .sum files or emitted as a manifest.
220
221 Attributes:
222 current_tool: Name of the current top-level DejaGnu testsuite.
223 current_exp: Name of the current .exp testsuite file.
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000224 testsuites: A set of (tool, exp) tuples representing encountered testsuites.
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000225 """
226
227 def __init__(self):
228 super().__init__()
229 self.ResetToolExp()
Maxim Kuvyrkov88799c62024-04-01 12:33:09 +0000230 self.testsuites = set()
231 self.total = 0
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000232
233 def update(self, other):
234 super().update(other)
235 self.testsuites.update(other.testsuites)
Maxim Kuvyrkov88799c62024-04-01 12:33:09 +0000236 self.total += other.total
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000237
238 def ResetToolExp(self):
239 self.current_tool = None
240 self.current_exp = None
241
242 def MakeTestResult(self, summary_line, ordinal=-1):
243 return TestResult(summary_line, ordinal,
244 self.current_tool, self.current_exp)
245
246 def Print(self, outfile=sys.stdout):
247 current_tool = None
248 current_exp = None
249
250 for result in sorted(self):
251 if current_tool != result.tool:
252 current_tool = result.tool
253 outfile.write(_TOOL_LINE_FORMAT % current_tool)
254 if current_exp != result.exp:
255 current_exp = result.exp
Christophe Lyona7d8c4c2023-04-14 12:01:23 +0000256 outfile.write(_EXP_LINE_FORMAT % (current_tool, current_exp))
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000257 outfile.write('%s\n' % result)
258
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000259 # Check if testsuite of expected_result is present in current results.
260 # This is used to compare partial test results against a full manifest.
261 def HasTestsuite(self, expected_result):
262 return (expected_result.tool, expected_result.exp) in self.testsuites
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000263
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000264def GetMakefileValue(makefile_name, value_name):
265 if os.path.exists(makefile_name):
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000266 makefile = open(makefile_name, encoding='latin-1', mode='r')
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000267 for line in makefile:
268 if line.startswith(value_name):
269 (_, value) = line.split('=', 1)
270 value = value.strip()
271 makefile.close()
272 return value
273 makefile.close()
274 return None
275
276
277def ValidBuildDirectory(builddir):
278 if (not os.path.exists(builddir) or
279 not os.path.exists('%s/Makefile' % builddir)):
280 return False
281 return True
282
283
284def IsComment(line):
285 """Return True if line is a comment."""
286 return line.startswith('#')
287
288
289def SplitAttributesFromSummaryLine(line):
290 """Splits off attributes from a summary line, if present."""
Maxim Kuvyrkov966fac42024-04-01 11:47:27 +0000291 if '|' in line and not _INTERESTING_RESULTS_REX.match(line):
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000292 (attrs, line) = line.split('|', 1)
293 attrs = attrs.strip()
294 else:
295 attrs = ''
296 line = line.strip()
297 return (attrs, line)
298
299
Thiago Jung Bauermann41505402023-06-14 14:37:00 +0200300def IsInterestingResult(result_set, line):
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000301 """Return True if line is one of the summary lines we care about."""
302 (_, line) = SplitAttributesFromSummaryLine(line)
Maxim Kuvyrkov966fac42024-04-01 11:47:27 +0000303 interesting_result = bool(_INTERESTING_RESULTS_REX.match(line))
Thiago Jung Bauermann41505402023-06-14 14:37:00 +0200304
Maxim Kuvyrkovc0253642024-04-01 11:51:59 +0000305 # If there's no .exp defined it means that either the results section hasn't
Thiago Jung Bauermann41505402023-06-14 14:37:00 +0200306 # started yet, or it is already over.
Maxim Kuvyrkovc0253642024-04-01 11:51:59 +0000307 if interesting_result and result_set.current_exp is None:
Thiago Jung Bauermann41505402023-06-14 14:37:00 +0200308 if _OPTIONS.verbosity >= 3:
Maxim Kuvyrkovc0253642024-04-01 11:51:59 +0000309 print(f'WARNING: Result "{line}" found outside sum file boundaries.')
Thiago Jung Bauermann41505402023-06-14 14:37:00 +0200310 return False
311
Maxim Kuvyrkov966fac42024-04-01 11:47:27 +0000312 return interesting_result
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000313
314
Maxim Kuvyrkov88799c62024-04-01 12:33:09 +0000315def IsValidResult(result_set, line):
316 """Return True if line is a valid test result."""
317 valid_result = bool(_VALID_RESULTS_REX.match(line))
318
319 # If there's no .exp defined it means that either the results section hasn't
320 # started yet, or it is already over.
321 if valid_result and result_set.current_exp is None:
322 if _OPTIONS.verbosity >= 3:
323 print(f'WARNING: Result "{line}" found outside sum file boundaries.')
324 return False
325
326 return valid_result
327
328
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000329def IsToolLine(line):
330 """Return True if line mentions the tool (in DejaGnu terms) for the following tests."""
331 return bool(_TOOL_LINE_REX.match(line))
332
333
334def IsExpLine(line):
335 """Return True if line mentions the .exp file for the following tests."""
336 return bool(_EXP_LINE_REX.match(line))
337
338
339def IsSummaryLine(line):
340 """Return True if line starts .sum footer."""
341 return bool(_SUMMARY_LINE_REX.match(line))
342
343
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000344def IsInclude(line):
345 """Return True if line is an include of another file."""
346 return line.startswith("@include ")
347
348
349def GetIncludeFile(line, includer):
350 """Extract the name of the include file from line."""
351 includer_dir = os.path.dirname(includer)
352 include_file = line[len("@include "):]
353 return os.path.join(includer_dir, include_file.strip())
354
355
356def IsNegativeResult(line):
357 """Return True if line should be removed from the expected results."""
358 return line.startswith("@remove ")
359
360
361def GetNegativeResult(line):
362 """Extract the name of the negative result from line."""
363 line = line[len("@remove "):]
364 return line.strip()
365
366
367def ParseManifestWorker(result_set, manifest_path):
368 """Read manifest_path, adding the contents to result_set."""
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000369 if _OPTIONS.verbosity >= 5:
Maxim Kuvyrkov63ad5352021-07-04 07:38:22 +0000370 print('Parsing manifest file %s.' % manifest_path)
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000371 manifest_file = open(manifest_path, encoding='latin-1', mode='r')
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000372 for orig_line in manifest_file:
373 line = orig_line.strip()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000374 if line == "":
375 pass
376 elif IsComment(line):
377 pass
378 elif IsNegativeResult(line):
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000379 result_set.remove(result_set.MakeTestResult(GetNegativeResult(line)))
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000380 elif IsInclude(line):
381 ParseManifestWorker(result_set, GetIncludeFile(line, manifest_path))
Thiago Jung Bauermann41505402023-06-14 14:37:00 +0200382 elif IsInterestingResult(result_set, line):
Maxim Kuvyrkova6b29dd2023-04-12 14:35:39 +0000383 result = result_set.MakeTestResult(line)
384 if result.HasExpired():
385 # Ignore expired manifest entries.
Maxim Kuvyrkov7df81782023-05-25 06:42:06 +0000386 if _OPTIONS.verbosity >= 4:
Maxim Kuvyrkova6b29dd2023-04-12 14:35:39 +0000387 print('WARNING: Expected failure "%s" has expired.' % line.strip())
388 continue
389 result_set.add(result)
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000390 elif IsExpLine(orig_line):
391 result_set.current_exp = _EXP_LINE_REX.match(orig_line).groups()[0]
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000392 if _OPTIONS.srcpath_regex and _OPTIONS.srcpath_regex != '':
393 result_set.current_exp = re.sub(_OPTIONS.srcpath_regex, '',
394 result_set.current_exp)
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000395 elif IsToolLine(orig_line):
396 result_set.current_tool = _TOOL_LINE_REX.match(orig_line).groups()[0]
Maxim Kuvyrkovc0253642024-04-01 11:51:59 +0000397 result_set.current_exp = None
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000398 elif IsSummaryLine(orig_line):
399 result_set.ResetToolExp()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000400 else:
401 Error('Unrecognized line in manifest file: %s' % line)
402 manifest_file.close()
403
404
405def ParseManifest(manifest_path):
406 """Create a set of TestResult instances from the given manifest file."""
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000407 result_set = ResultSet()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000408 ParseManifestWorker(result_set, manifest_path)
409 return result_set
410
411
412def ParseSummary(sum_fname):
413 """Create a set of TestResult instances from the given summary file."""
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000414 result_set = ResultSet()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000415 # ordinal is used when sorting the results so that tests within each
416 # .exp file are kept sorted.
417 ordinal=0
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000418 sum_file = open(sum_fname, encoding='latin-1', mode='r')
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000419 for line in sum_file:
Thiago Jung Bauermann41505402023-06-14 14:37:00 +0200420 if IsInterestingResult(result_set, line):
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000421 result = result_set.MakeTestResult(line, ordinal)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000422 ordinal += 1
423 if result.HasExpired():
Maxim Kuvyrkova6b29dd2023-04-12 14:35:39 +0000424 # ??? What is the use-case for this? How "expiry" annotations are
425 # ??? supposed to be added to .sum results?
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000426 # Tests that have expired are not added to the set of expected
427 # results. If they are still present in the set of actual results,
428 # they will cause an error to be reported.
Maxim Kuvyrkov7df81782023-05-25 06:42:06 +0000429 if _OPTIONS.verbosity >= 4:
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000430 print('WARNING: Expected failure "%s" has expired.' % line.strip())
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000431 continue
432 result_set.add(result)
Maxim Kuvyrkov88799c62024-04-01 12:33:09 +0000433 result_set.total += 1
434 elif IsValidResult(result_set, line):
435 result_set.total += 1
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000436 elif IsExpLine(line):
437 result_set.current_exp = _EXP_LINE_REX.match(line).groups()[0]
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000438 if _OPTIONS.srcpath_regex and _OPTIONS.srcpath_regex != '':
439 result_set.current_exp = re.sub(_OPTIONS.srcpath_regex, '',
440 result_set.current_exp)
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000441 result_set.testsuites.add((result_set.current_tool,
442 result_set.current_exp))
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000443 elif IsToolLine(line):
444 result_set.current_tool = _TOOL_LINE_REX.match(line).groups()[0]
Maxim Kuvyrkovd8951a22021-07-08 08:20:28 +0000445 result_set.current_exp = None
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000446 elif IsSummaryLine(line):
447 result_set.ResetToolExp()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000448 sum_file.close()
449 return result_set
450
451
452def GetManifest(manifest_path):
453 """Build a set of expected failures from the manifest file.
454
455 Each entry in the manifest file should have the format understood
456 by the TestResult constructor.
457
458 If no manifest file exists for this target, it returns an empty set.
459 """
460 if os.path.exists(manifest_path):
461 return ParseManifest(manifest_path)
462 else:
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000463 return ResultSet()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000464
465
466def CollectSumFiles(builddir):
467 sum_files = []
468 for root, dirs, files in os.walk(builddir):
469 for ignored in ('.svn', '.git'):
470 if ignored in dirs:
471 dirs.remove(ignored)
472 for fname in files:
473 if fname.endswith('.sum'):
474 sum_files.append(os.path.join(root, fname))
475 return sum_files
476
477
Maxim Kuvyrkov8ef7c852021-07-08 08:21:18 +0000478def GetResults(sum_files, build_results = None):
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000479 """Collect all the test results from the given .sum files."""
Maxim Kuvyrkov8ef7c852021-07-08 08:21:18 +0000480 if build_results == None:
481 build_results = ResultSet()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000482 for sum_fname in sum_files:
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000483 if _OPTIONS.verbosity >= 3:
484 print('\t%s' % sum_fname)
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000485 build_results.update(ParseSummary(sum_fname))
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000486 return build_results
487
Maxim Kuvyrkovc0014022024-04-01 12:26:44 +0000488class ResultsStats:
489 """Describes statistics of DejaGNU test results.
490
491 Attributes:
492 fails: Number of non-flaky failed tests in the results.
493 flaky: Number of flaky entries in the manifest.
494 total: Total number of tests in the results, including flaky passes and
495 fails.
496 """
497
498 def __init__(self):
499 self.fails = 0
500 self.flaky = 0
501 self.total = 0
502
503 def Print(self, outfile=sys.stdout):
504 outfile.write(_SUMMARY_LINE_FORMAT % 'Results')
505 outfile.write(f'\n')
506 outfile.write(f'# of stable fails\t\t{self.fails}\n')
507 outfile.write(f'# of flaky entries\t\t{self.flaky}\n')
508 outfile.write(f'# of all tests\t\t\t{self.total}\n')
509
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000510
511def CompareResults(manifest, actual):
512 """Compare sets of results and return two lists:
513 - List of results present in ACTUAL but missing from MANIFEST.
514 - List of results present in MANIFEST but missing from ACTUAL.
515 """
516 # Collect all the actual results not present in the manifest.
517 # Results in this set will be reported as errors.
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000518 actual_vs_manifest = ResultSet()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000519 for actual_result in actual:
520 if actual_result not in manifest:
521 actual_vs_manifest.add(actual_result)
522
523 # Collect all the tests in the manifest that were not found
524 # in the actual results.
525 # Results in this set will be reported as warnings (since
526 # they are expected failures that are not failing anymore).
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000527 manifest_vs_actual = ResultSet()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000528 for expected_result in manifest:
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000529 # We try to support comparing partial results vs full manifest
530 # (e.g., manifest has failures for gcc, g++, gfortran, but we ran only
531 # g++ testsuite). To achieve this we record encountered testsuites in
532 # actual.testsuites set, and then we check it here using HasTestsuite().
533 if expected_result not in actual and actual.HasTestsuite(expected_result):
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000534 manifest_vs_actual.add(expected_result)
535
536 return actual_vs_manifest, manifest_vs_actual
537
538
Maxim Kuvyrkov918bc262021-07-08 08:27:39 +0000539def GetManifestPath(user_provided_must_exist):
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000540 """Return the full path to the manifest file."""
541 manifest_path = _OPTIONS.manifest
542 if manifest_path:
543 if user_provided_must_exist and not os.path.exists(manifest_path):
544 Error('Manifest does not exist: %s' % manifest_path)
545 return manifest_path
546 else:
Maxim Kuvyrkov918bc262021-07-08 08:27:39 +0000547 (srcdir, target) = GetBuildData()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000548 if not srcdir:
549 Error('Could not determine the location of GCC\'s source tree. '
550 'The Makefile does not contain a definition for "srcdir".')
551 if not target:
552 Error('Could not determine the target triplet for this build. '
553 'The Makefile does not contain a definition for "target_alias".')
554 return _MANIFEST_PATH_PATTERN % (srcdir, _MANIFEST_SUBDIR, target)
555
556
557def GetBuildData():
558 if not ValidBuildDirectory(_OPTIONS.build_dir):
559 # If we have been given a set of results to use, we may
560 # not be inside a valid GCC build directory. In that case,
561 # the user must provide both a manifest file and a set
562 # of results to check against it.
563 if not _OPTIONS.results or not _OPTIONS.manifest:
564 Error('%s is not a valid GCC top level build directory. '
565 'You must use --manifest and --results to do the validation.' %
566 _OPTIONS.build_dir)
567 else:
568 return None, None
569 srcdir = GetMakefileValue('%s/Makefile' % _OPTIONS.build_dir, 'srcdir =')
570 target = GetMakefileValue('%s/Makefile' % _OPTIONS.build_dir, 'target_alias=')
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000571 if _OPTIONS.verbosity >= 3:
572 print('Source directory: %s' % srcdir)
573 print('Build target: %s' % target)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000574 return srcdir, target
575
576
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000577def PrintSummary(summary):
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000578 summary.Print()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000579
580def GetSumFiles(results, build_dir):
581 if not results:
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000582 if _OPTIONS.verbosity >= 3:
583 print('Getting actual results from build directory %s' % build_dir)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000584 sum_files = CollectSumFiles(build_dir)
585 else:
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000586 if _OPTIONS.verbosity >= 3:
587 print('Getting actual results from user-provided results')
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000588 sum_files = results.split()
589 return sum_files
590
Maxim Kuvyrkoveb482c72024-04-01 12:30:19 +0000591def DiscardFlaky(expected, actual):
592 flaky_list = []
593 for expected_result in expected:
594 if 'flaky' in expected_result.attrs:
595 flaky_list.append(expected_result)
596
597 for expected_result in flaky_list:
598 expected.remove(expected_result)
599 actual.discard(expected_result)
600
601 return len(flaky_list)
602
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000603
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000604def PerformComparison(expected, actual):
Maxim Kuvyrkovc0014022024-04-01 12:26:44 +0000605 stats = ResultsStats()
Maxim Kuvyrkoveb482c72024-04-01 12:30:19 +0000606 stats.total = actual.total
607 # We need to ignore flaky tests in comparison, so remove them now from
608 # both expected and actual sets.
609 stats.flaky = DiscardFlaky(expected, actual)
Maxim Kuvyrkovc0014022024-04-01 12:26:44 +0000610 stats.fails = len(actual)
611
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000612 actual_vs_expected, expected_vs_actual = CompareResults(expected, actual)
613
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000614 if _OPTIONS.inverse_match:
615 # Switch results if inverse comparison is requested.
616 # This is useful in detecting flaky tests that FAILed in expected set,
617 # but PASSed in actual set.
618 actual_vs_expected, expected_vs_actual \
Maxim Kuvyrkov7df81782023-05-25 06:42:06 +0000619 = expected_vs_actual, actual_vs_expected
Maxim Kuvyrkovc0014022024-04-01 12:26:44 +0000620 stats = None
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000621
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000622 tests_ok = True
623 if len(actual_vs_expected) > 0:
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000624 if _OPTIONS.verbosity >= 3:
625 print('\n\nUnexpected results in this build (new failures)')
626 if _OPTIONS.verbosity >= 1:
627 PrintSummary(actual_vs_expected)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000628 tests_ok = False
629
Maxim Kuvyrkovc0014022024-04-01 12:26:44 +0000630 if _OPTIONS.verbosity >= 1 and stats:
631 stats.Print()
632
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000633 if _OPTIONS.verbosity >= 2 and len(expected_vs_actual) > 0:
634 print('\n\nExpected results not present in this build (fixed tests)'
635 '\n\nNOTE: This is not a failure. It just means that these '
636 'tests were expected\nto fail, but either they worked in '
637 'this configuration or they were not\npresent at all.\n')
638 PrintSummary(expected_vs_actual)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000639
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000640 if tests_ok and _OPTIONS.verbosity >= 3:
Maxim Kuvyrkov63ad5352021-07-04 07:38:22 +0000641 print('\nSUCCESS: No unexpected failures.')
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000642
643 return tests_ok
644
645
646def CheckExpectedResults():
Maxim Kuvyrkov918bc262021-07-08 08:27:39 +0000647 manifest_path = GetManifestPath(True)
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000648 if _OPTIONS.verbosity >= 3:
649 print('Manifest: %s' % manifest_path)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000650 manifest = GetManifest(manifest_path)
651 sum_files = GetSumFiles(_OPTIONS.results, _OPTIONS.build_dir)
652 actual = GetResults(sum_files)
653
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000654 if _OPTIONS.verbosity >= 5:
655 print('\n\nTests expected to fail')
656 PrintSummary(manifest)
657 print('\n\nActual test results')
658 PrintSummary(actual)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000659
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000660 return PerformComparison(manifest, actual)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000661
662
663def ProduceManifest():
Maxim Kuvyrkov918bc262021-07-08 08:27:39 +0000664 manifest_path = GetManifestPath(False)
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000665 if _OPTIONS.verbosity >= 3:
666 print('Manifest: %s' % manifest_path)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000667 if os.path.exists(manifest_path) and not _OPTIONS.force:
668 Error('Manifest file %s already exists.\nUse --force to overwrite.' %
669 manifest_path)
670
671 sum_files = GetSumFiles(_OPTIONS.results, _OPTIONS.build_dir)
672 actual = GetResults(sum_files)
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000673 manifest_file = open(manifest_path, encoding='latin-1', mode='w')
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000674 actual.Print(manifest_file)
675 actual.Print()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000676 manifest_file.close()
677
678 return True
679
680
681def CompareBuilds():
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000682 sum_files = GetSumFiles(_OPTIONS.results, _OPTIONS.build_dir)
683 actual = GetResults(sum_files)
684
Maxim Kuvyrkov8ef7c852021-07-08 08:21:18 +0000685 clean = ResultSet()
686
687 if _OPTIONS.manifest:
Maxim Kuvyrkov918bc262021-07-08 08:27:39 +0000688 manifest_path = GetManifestPath(True)
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000689 if _OPTIONS.verbosity >= 3:
690 print('Manifest: %s' % manifest_path)
Maxim Kuvyrkov8ef7c852021-07-08 08:21:18 +0000691 clean = GetManifest(manifest_path)
692
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000693 clean_sum_files = GetSumFiles(_OPTIONS.results, _OPTIONS.clean_build)
Maxim Kuvyrkov8ef7c852021-07-08 08:21:18 +0000694 clean = GetResults(clean_sum_files, clean)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000695
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000696 return PerformComparison(clean, actual)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000697
698
699def Main(argv):
700 parser = optparse.OptionParser(usage=__doc__)
701
702 # Keep the following list sorted by option name.
703 parser.add_option('--build_dir', action='store', type='string',
704 dest='build_dir', default='.',
705 help='Build directory to check (default = .)')
706 parser.add_option('--clean_build', action='store', type='string',
707 dest='clean_build', default=None,
708 help='Compare test results from this build against '
709 'those of another (clean) build. Use this option '
710 'when comparing the test results of your patch versus '
711 'the test results of a clean build without your patch. '
712 'You must provide the path to the top directory of your '
713 'clean build.')
714 parser.add_option('--force', action='store_true', dest='force',
715 default=False, help='When used with --produce_manifest, '
716 'it will overwrite an existing manifest file '
717 '(default = False)')
Maxim Kuvyrkov158e61d2023-05-25 12:18:30 +0000718 parser.add_option('--expiry_date', action='store',
719 dest='expiry_today_date', default=None,
720 help='Use provided date YYYYMMDD to decide whether '
721 'manifest entries with expiry settings have expired '
722 'or not. (default = Use today date)')
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000723 parser.add_option('--srcpath', action='store', type='string',
724 dest='srcpath_regex', default='[^ ]+/testsuite/',
725 help='Remove provided path (can be a regex) from '
726 'the result entries. This is useful to remove '
727 'occasional filesystem path from the results. '
728 '(default = "[^ ]+/testsuite/")')
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000729 parser.add_option('--inverse_match', action='store_true',
730 dest='inverse_match', default=False,
731 help='Inverse result sets in comparison. '
732 'Output unexpected passes as unexpected failures and '
733 'unexpected failures as unexpected passes. '
734 'This is used to catch FAIL->PASS flaky tests. '
735 '(default = False)')
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000736 parser.add_option('--manifest', action='store', type='string',
737 dest='manifest', default=None,
738 help='Name of the manifest file to use (default = '
739 'taken from '
740 'contrib/testsuite-managment/<target_alias>.xfail)')
741 parser.add_option('--produce_manifest', action='store_true',
742 dest='produce_manifest', default=False,
743 help='Produce the manifest for the current '
744 'build (default = False)')
745 parser.add_option('--results', action='store', type='string',
746 dest='results', default=None, help='Space-separated list '
747 'of .sum files with the testing results to check. The '
748 'only content needed from these files are the lines '
749 'starting with FAIL, XPASS or UNRESOLVED (default = '
750 '.sum files collected from the build directory).')
751 parser.add_option('--verbosity', action='store', dest='verbosity',
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000752 type='int', default=3, help='Verbosity level '
753 '(default = 3). Level 0: only error output, this is '
754 'useful in scripting when only the exit code is used. '
755 'Level 1: output unexpected failures. '
756 'Level 2: output unexpected passes. '
757 'Level 3: output helpful information. '
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000758 'Level 4: output notification on expired entries. '
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000759 'Level 5: output debug information.')
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000760 global _OPTIONS
761 (_OPTIONS, _) = parser.parse_args(argv[1:])
762
Maxim Kuvyrkov158e61d2023-05-25 12:18:30 +0000763 # Set "today" date to compare expiration entries against.
764 # Setting expiration date into the future allows re-detection of flaky
765 # tests and creating fresh entries for them before the current flaky entries
766 # expire.
767 if _OPTIONS.expiry_today_date:
768 today_date = re.search(r'(\d\d\d\d)(\d\d)(\d\d)',
769 _OPTIONS.expiry_today_date)
770 if not today_date:
771 Error('Invalid --expiry_today_date format "%s". Must be of the form '
772 '"expire=YYYYMMDD"' % _OPTIONS.expiry_today_date)
773 _OPTIONS.expiry_today_date=datetime.date(int(today_date.group(1)),
774 int(today_date.group(2)),
775 int(today_date.group(3)))
776 else:
777 _OPTIONS.expiry_today_date = datetime.date.today()
778
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000779 if _OPTIONS.produce_manifest:
780 retval = ProduceManifest()
781 elif _OPTIONS.clean_build:
782 retval = CompareBuilds()
783 else:
784 retval = CheckExpectedResults()
785
786 if retval:
787 return 0
788 else:
Maxim Kuvyrkov972bb812021-08-30 14:18:09 +0000789 return 2
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000790
791
792if __name__ == '__main__':
793 retval = Main(sys.argv)
794 sys.exit(retval)