blob: 2efd70ff1919aa1b93b0e0f6bebb4fd1e2721c0c [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
Maxim Kuvyrkova0d90542024-07-23 11:37:04 +000062import json
Maxim Kuvyrkov59877482021-07-07 11:22:26 +000063
Maxim Kuvyrkova0d90542024-07-23 11:37:04 +000064# Results that we want keep an eye on.
65# Note, 'NOEXE' state is used by llvm-test-suite.
66_INTERESTING_RESULTS = [ 'FAIL', 'UNRESOLVED', 'XPASS', 'ERROR', 'NOEXE' ]
Maxim Kuvyrkov8704bc12023-05-03 15:03:34 +000067# <STATE>: <NAME> <DESCRIPTION"
Maxim Kuvyrkov966fac42024-04-01 11:47:27 +000068_INTERESTING_RESULTS_REX = re.compile('(%s):\s*(\S+)\s*(.*)'
69 % "|".join(_INTERESTING_RESULTS))
Maxim Kuvyrkov8704bc12023-05-03 15:03:34 +000070
Maxim Kuvyrkov88799c62024-04-01 12:33:09 +000071_VALID_RESULTS_REX = re.compile('([A-Z]+):\s*(\S+)\s*(.*)')
72
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +000073# Formats of .sum file sections
74_TOOL_LINE_FORMAT = '\t\t=== %s tests ===\n'
Christophe Lyona7d8c4c2023-04-14 12:01:23 +000075_EXP_LINE_FORMAT = '\nRunning %s:%s ...\n'
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +000076_SUMMARY_LINE_FORMAT = '\n\t\t=== %s Summary ===\n'
77
78# ... and their compiled regexs.
79_TOOL_LINE_REX = re.compile('^\t\t=== (.*) tests ===\n')
Christophe Lyona7d8c4c2023-04-14 12:01:23 +000080# Match .exp file name, optionally prefixed by a "tool:" name and a
81# path ending with "testsuite/"
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +000082_EXP_LINE_REX = re.compile('^Running (?:.*:)?(.*) \.\.\.\n')
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +000083_SUMMARY_LINE_REX = re.compile('^\t\t=== (.*) Summary ===\n')
84
Maxim Kuvyrkov59877482021-07-07 11:22:26 +000085# Subdirectory of srcdir in which to find the manifest file.
86_MANIFEST_SUBDIR = 'contrib/testsuite-management'
87
88# Pattern for naming manifest files.
89# The first argument should be the toplevel GCC(/GNU tool) source directory.
90# The second argument is the manifest subdir.
91# The third argument is the manifest target, which defaults to the target
92# triplet used during the build.
93_MANIFEST_PATH_PATTERN = '%s/%s/%s.xfail'
94
95# The options passed to the program.
96_OPTIONS = None
97
98def Error(msg):
Maxim Kuvyrkov63ad5352021-07-04 07:38:22 +000099 print('error: %s' % msg, file=sys.stderr)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000100 sys.exit(1)
101
102
103class TestResult(object):
104 """Describes a single DejaGNU test result as emitted in .sum files.
105
106 We are only interested in representing unsuccessful tests. So, only
107 a subset of all the tests are loaded.
108
109 The summary line used to build the test result should have this format:
110
111 attrlist | XPASS: gcc.dg/unroll_1.c (test for excess errors)
112 ^^^^^^^^ ^^^^^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
113 optional state name description
114 attributes
115
116 Attributes:
117 attrlist: A comma separated list of attributes.
118 Valid values:
119 flaky Indicates that this test may not always fail. These
120 tests are reported, but their presence does not affect
121 the results.
122
123 expire=YYYYMMDD After this date, this test will produce an error
124 whether it is in the manifest or not.
125
126 state: One of UNRESOLVED, XPASS or FAIL.
127 name: File name for the test.
128 description: String describing the test (flags used, dejagnu message, etc)
129 ordinal: Monotonically increasing integer.
130 It is used to keep results for one .exp file sorted
131 by the order the tests were run.
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000132 tool: Top-level testsuite name (aka "tool" in DejaGnu parlance) of the test.
133 exp: Name of .exp testsuite file.
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000134 """
135
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000136 def __init__(self, summary_line, ordinal, tool, exp):
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000137 try:
138 (self.attrs, summary_line) = SplitAttributesFromSummaryLine(summary_line)
139 try:
140 (self.state,
141 self.name,
Maxim Kuvyrkov966fac42024-04-01 11:47:27 +0000142 self.description) = _INTERESTING_RESULTS_REX.match(summary_line).groups()
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000143 if _OPTIONS.srcpath_regex and _OPTIONS.srcpath_regex != '':
144 self.description = re.sub(_OPTIONS.srcpath_regex, '',
145 self.description)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000146 except:
Thiago Jung Bauermann7b82a592023-04-22 14:12:06 +0000147 print('Failed to parse summary line: "%s"' % summary_line,
148 file=sys.stderr)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000149 raise
150 self.ordinal = ordinal
Maxim Kuvyrkovf09ab0e2021-08-30 14:19:04 +0000151 if tool == None or exp == None:
152 # .sum file seem to be broken. There was no "tool" and/or "exp"
153 # lines preceding this result.
Thiago Jung Bauermann7b82a592023-04-22 14:12:06 +0000154 print(f'.sum file seems to be broken: tool="{tool}", exp="{exp}", summary_line="{summary_line}"',
155 file=sys.stderr)
Maxim Kuvyrkovf09ab0e2021-08-30 14:19:04 +0000156 raise
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000157 self.tool = tool
158 self.exp = exp
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000159 except ValueError:
160 Error('Cannot parse summary line "%s"' % summary_line)
161
Maxim Kuvyrkov966fac42024-04-01 11:47:27 +0000162 if self.state not in _INTERESTING_RESULTS:
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000163 Error('Invalid test result %s in "%s" (parsed as "%s")' % (
164 self.state, summary_line, self))
165
166 def __lt__(self, other):
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000167 if (self.tool != other.tool):
168 return self.tool < other.tool
169 if (self.exp != other.exp):
170 return self.exp < other.exp
171 if (self.name != other.name):
172 return self.name < other.name
173 return self.ordinal < other.ordinal
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000174
175 def __hash__(self):
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000176 return (hash(self.state) ^ hash(self.tool) ^ hash(self.exp)
177 ^ hash(self.name) ^ hash(self.description))
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000178
Maxim Kuvyrkov7df81782023-05-25 06:42:06 +0000179 # Note that we don't include "attrs" in this comparison. This means that
180 # result entries "FAIL: test" and "flaky | FAIL: test" are considered
181 # the same. Therefore the ResultSet will preserve only the first occurence.
182 # In practice this means that flaky entries should preceed expected fails
183 # entries.
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000184 def __eq__(self, other):
185 return (self.state == other.state and
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000186 self.tool == other.tool and
187 self.exp == other.exp and
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000188 self.name == other.name and
189 self.description == other.description)
190
191 def __ne__(self, other):
192 return not (self == other)
193
194 def __str__(self):
195 attrs = ''
196 if self.attrs:
197 attrs = '%s | ' % self.attrs
198 return '%s%s: %s %s' % (attrs, self.state, self.name, self.description)
199
200 def ExpirationDate(self):
201 # Return a datetime.date object with the expiration date for this
202 # test result. Return None, if no expiration has been set.
203 if re.search(r'expire=', self.attrs):
204 expiration = re.search(r'expire=(\d\d\d\d)(\d\d)(\d\d)', self.attrs)
205 if not expiration:
206 Error('Invalid expire= format in "%s". Must be of the form '
207 '"expire=YYYYMMDD"' % self)
208 return datetime.date(int(expiration.group(1)),
209 int(expiration.group(2)),
210 int(expiration.group(3)))
211 return None
212
213 def HasExpired(self):
214 # Return True if the expiration date of this result has passed.
215 expiration_date = self.ExpirationDate()
216 if expiration_date:
Maxim Kuvyrkov158e61d2023-05-25 12:18:30 +0000217 return _OPTIONS.expiry_today_date > expiration_date
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000218
219
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000220class ResultSet(set):
221 """Describes a set of DejaGNU test results.
222 This set can be read in from .sum files or emitted as a manifest.
223
224 Attributes:
225 current_tool: Name of the current top-level DejaGnu testsuite.
226 current_exp: Name of the current .exp testsuite file.
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000227 testsuites: A set of (tool, exp) tuples representing encountered testsuites.
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000228 """
229
230 def __init__(self):
231 super().__init__()
232 self.ResetToolExp()
Maxim Kuvyrkov88799c62024-04-01 12:33:09 +0000233 self.testsuites = set()
234 self.total = 0
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000235
236 def update(self, other):
237 super().update(other)
238 self.testsuites.update(other.testsuites)
Maxim Kuvyrkov88799c62024-04-01 12:33:09 +0000239 self.total += other.total
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000240
241 def ResetToolExp(self):
242 self.current_tool = None
243 self.current_exp = None
244
245 def MakeTestResult(self, summary_line, ordinal=-1):
246 return TestResult(summary_line, ordinal,
247 self.current_tool, self.current_exp)
248
249 def Print(self, outfile=sys.stdout):
250 current_tool = None
251 current_exp = None
252
253 for result in sorted(self):
254 if current_tool != result.tool:
255 current_tool = result.tool
256 outfile.write(_TOOL_LINE_FORMAT % current_tool)
Maxim Kuvyrkov3a777532024-04-18 11:35:58 +0000257 current_exp = None
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000258 if current_exp != result.exp:
259 current_exp = result.exp
Christophe Lyona7d8c4c2023-04-14 12:01:23 +0000260 outfile.write(_EXP_LINE_FORMAT % (current_tool, current_exp))
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000261 outfile.write('%s\n' % result)
262
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000263 # Check if testsuite of expected_result is present in current results.
264 # This is used to compare partial test results against a full manifest.
265 def HasTestsuite(self, expected_result):
266 return (expected_result.tool, expected_result.exp) in self.testsuites
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000267
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000268def GetMakefileValue(makefile_name, value_name):
269 if os.path.exists(makefile_name):
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000270 makefile = open(makefile_name, encoding='latin-1', mode='r')
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000271 for line in makefile:
272 if line.startswith(value_name):
273 (_, value) = line.split('=', 1)
274 value = value.strip()
275 makefile.close()
276 return value
277 makefile.close()
278 return None
279
280
281def ValidBuildDirectory(builddir):
282 if (not os.path.exists(builddir) or
283 not os.path.exists('%s/Makefile' % builddir)):
284 return False
285 return True
286
287
288def IsComment(line):
289 """Return True if line is a comment."""
290 return line.startswith('#')
291
292
293def SplitAttributesFromSummaryLine(line):
294 """Splits off attributes from a summary line, if present."""
Maxim Kuvyrkov966fac42024-04-01 11:47:27 +0000295 if '|' in line and not _INTERESTING_RESULTS_REX.match(line):
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000296 (attrs, line) = line.split('|', 1)
297 attrs = attrs.strip()
298 else:
299 attrs = ''
300 line = line.strip()
301 return (attrs, line)
302
303
Thiago Jung Bauermann41505402023-06-14 14:37:00 +0200304def IsInterestingResult(result_set, line):
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000305 """Return True if line is one of the summary lines we care about."""
306 (_, line) = SplitAttributesFromSummaryLine(line)
Maxim Kuvyrkov966fac42024-04-01 11:47:27 +0000307 interesting_result = bool(_INTERESTING_RESULTS_REX.match(line))
Thiago Jung Bauermann41505402023-06-14 14:37:00 +0200308
Maxim Kuvyrkovc0253642024-04-01 11:51:59 +0000309 # If there's no .exp defined it means that either the results section hasn't
Thiago Jung Bauermann41505402023-06-14 14:37:00 +0200310 # started yet, or it is already over.
Maxim Kuvyrkovc0253642024-04-01 11:51:59 +0000311 if interesting_result and result_set.current_exp is None:
Thiago Jung Bauermann41505402023-06-14 14:37:00 +0200312 if _OPTIONS.verbosity >= 3:
Maxim Kuvyrkovc0253642024-04-01 11:51:59 +0000313 print(f'WARNING: Result "{line}" found outside sum file boundaries.')
Thiago Jung Bauermann41505402023-06-14 14:37:00 +0200314 return False
315
Maxim Kuvyrkov966fac42024-04-01 11:47:27 +0000316 return interesting_result
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000317
318
Maxim Kuvyrkov88799c62024-04-01 12:33:09 +0000319def IsValidResult(result_set, line):
320 """Return True if line is a valid test result."""
321 valid_result = bool(_VALID_RESULTS_REX.match(line))
322
323 # If there's no .exp defined it means that either the results section hasn't
324 # started yet, or it is already over.
325 if valid_result and result_set.current_exp is None:
326 if _OPTIONS.verbosity >= 3:
327 print(f'WARNING: Result "{line}" found outside sum file boundaries.')
328 return False
329
330 return valid_result
331
332
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000333def IsToolLine(line):
334 """Return True if line mentions the tool (in DejaGnu terms) for the following tests."""
335 return bool(_TOOL_LINE_REX.match(line))
336
337
338def IsExpLine(line):
339 """Return True if line mentions the .exp file for the following tests."""
340 return bool(_EXP_LINE_REX.match(line))
341
342
343def IsSummaryLine(line):
344 """Return True if line starts .sum footer."""
345 return bool(_SUMMARY_LINE_REX.match(line))
346
347
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000348def IsInclude(line):
349 """Return True if line is an include of another file."""
350 return line.startswith("@include ")
351
352
353def GetIncludeFile(line, includer):
354 """Extract the name of the include file from line."""
355 includer_dir = os.path.dirname(includer)
356 include_file = line[len("@include "):]
357 return os.path.join(includer_dir, include_file.strip())
358
359
360def IsNegativeResult(line):
361 """Return True if line should be removed from the expected results."""
362 return line.startswith("@remove ")
363
364
365def GetNegativeResult(line):
366 """Extract the name of the negative result from line."""
367 line = line[len("@remove "):]
368 return line.strip()
369
370
371def ParseManifestWorker(result_set, manifest_path):
372 """Read manifest_path, adding the contents to result_set."""
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000373 if _OPTIONS.verbosity >= 5:
Maxim Kuvyrkov63ad5352021-07-04 07:38:22 +0000374 print('Parsing manifest file %s.' % manifest_path)
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000375 manifest_file = open(manifest_path, encoding='latin-1', mode='r')
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000376 for orig_line in manifest_file:
377 line = orig_line.strip()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000378 if line == "":
379 pass
380 elif IsComment(line):
381 pass
382 elif IsNegativeResult(line):
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000383 result_set.remove(result_set.MakeTestResult(GetNegativeResult(line)))
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000384 elif IsInclude(line):
385 ParseManifestWorker(result_set, GetIncludeFile(line, manifest_path))
Thiago Jung Bauermann41505402023-06-14 14:37:00 +0200386 elif IsInterestingResult(result_set, line):
Maxim Kuvyrkova6b29dd2023-04-12 14:35:39 +0000387 result = result_set.MakeTestResult(line)
388 if result.HasExpired():
389 # Ignore expired manifest entries.
Maxim Kuvyrkov7df81782023-05-25 06:42:06 +0000390 if _OPTIONS.verbosity >= 4:
Maxim Kuvyrkova6b29dd2023-04-12 14:35:39 +0000391 print('WARNING: Expected failure "%s" has expired.' % line.strip())
392 continue
393 result_set.add(result)
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000394 elif IsExpLine(orig_line):
395 result_set.current_exp = _EXP_LINE_REX.match(orig_line).groups()[0]
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000396 if _OPTIONS.srcpath_regex and _OPTIONS.srcpath_regex != '':
397 result_set.current_exp = re.sub(_OPTIONS.srcpath_regex, '',
398 result_set.current_exp)
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000399 elif IsToolLine(orig_line):
400 result_set.current_tool = _TOOL_LINE_REX.match(orig_line).groups()[0]
Maxim Kuvyrkovc0253642024-04-01 11:51:59 +0000401 result_set.current_exp = None
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000402 elif IsSummaryLine(orig_line):
403 result_set.ResetToolExp()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000404 else:
405 Error('Unrecognized line in manifest file: %s' % line)
406 manifest_file.close()
407
408
409def ParseManifest(manifest_path):
410 """Create a set of TestResult instances from the given manifest file."""
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000411 result_set = ResultSet()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000412 ParseManifestWorker(result_set, manifest_path)
413 return result_set
414
415
Maxim Kuvyrkova0d90542024-07-23 11:37:04 +0000416def convertJSONResult(json_result):
417 """Convert JSON result into a dejagnu-like line."""
418 # We are just starting to process JSON data (from LLVM's testsuite),
419 # and it seems prudent to convert JSON into dejagnu-like lines, so that we
420 # can re-use dejagnu parsing logic, rather than duplicating it for JSON.
421
422 name = json_result['name']
423 tool_exp = name.split(' :: ')
424 tool_exp[1] = os.path.dirname(tool_exp[1])
425 line = json_result['code'] + ': ' + json_result['name']
426 tool_exp.append(line)
427 return tool_exp
428
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000429def ParseSummary(sum_fname):
430 """Create a set of TestResult instances from the given summary file."""
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000431 result_set = ResultSet()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000432 # ordinal is used when sorting the results so that tests within each
433 # .exp file are kept sorted.
434 ordinal=0
Maxim Kuvyrkova0d90542024-07-23 11:37:04 +0000435
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000436 sum_file = open(sum_fname, encoding='latin-1', mode='r')
Maxim Kuvyrkova0d90542024-07-23 11:37:04 +0000437 file_results = sum_file
438 json_input = sum_fname.endswith('.json')
439
440 if json_input:
Christophe Lyon23708902024-08-08 16:03:26 +0000441 try:
442 file_results = json.load(sum_file)
443 except:
444 if _OPTIONS.verbosity >= 2:
445 # GCC's testing generates random .json files, which we can detect
446 # as results. Some of them cannot even be parsed.
447 print(f'WARNING: cannot parse JSON result file "{sum_fname}"')
448 return result_set
449
Maxim Kuvyrkova9243212024-08-03 09:37:01 +0000450 if not 'tests' in file_results:
451 if _OPTIONS.verbosity >= 2:
452 # GCC's testing generates random .json files, which we can detect
453 # as results.
454 print(f'WARNING: malformed JSON result file "{sum_fname}"')
455 return result_set
456
Maxim Kuvyrkova0d90542024-07-23 11:37:04 +0000457 file_results = file_results['tests']
458
459 for file_result in file_results:
460 if json_input:
461 (result_set.current_tool, result_set.current_exp, line)\
462 = convertJSONResult(file_result)
Maxim Kuvyrkov13a82492024-07-31 07:32:16 +0000463 # This is a copy of logic from IsExpLine below
464 if _OPTIONS.srcpath_regex and _OPTIONS.srcpath_regex != '':
465 result_set.current_exp = re.sub(_OPTIONS.srcpath_regex, '',
466 result_set.current_exp)
467 result_set.testsuites.add((result_set.current_tool,
468 result_set.current_exp))
Thiago Jung Bauermann672eb022024-07-24 14:43:47 -0300469 else:
470 line = file_result
Maxim Kuvyrkova0d90542024-07-23 11:37:04 +0000471
Thiago Jung Bauermann41505402023-06-14 14:37:00 +0200472 if IsInterestingResult(result_set, line):
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000473 result = result_set.MakeTestResult(line, ordinal)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000474 ordinal += 1
475 if result.HasExpired():
Maxim Kuvyrkova6b29dd2023-04-12 14:35:39 +0000476 # ??? What is the use-case for this? How "expiry" annotations are
477 # ??? supposed to be added to .sum results?
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000478 # Tests that have expired are not added to the set of expected
479 # results. If they are still present in the set of actual results,
480 # they will cause an error to be reported.
Maxim Kuvyrkov7df81782023-05-25 06:42:06 +0000481 if _OPTIONS.verbosity >= 4:
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000482 print('WARNING: Expected failure "%s" has expired.' % line.strip())
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000483 continue
484 result_set.add(result)
Maxim Kuvyrkov88799c62024-04-01 12:33:09 +0000485 result_set.total += 1
486 elif IsValidResult(result_set, line):
487 result_set.total += 1
Maxim Kuvyrkova0d90542024-07-23 11:37:04 +0000488 elif json_input:
489 Error('Unrecognized json result: %s' % line)
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000490 elif IsExpLine(line):
491 result_set.current_exp = _EXP_LINE_REX.match(line).groups()[0]
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000492 if _OPTIONS.srcpath_regex and _OPTIONS.srcpath_regex != '':
493 result_set.current_exp = re.sub(_OPTIONS.srcpath_regex, '',
494 result_set.current_exp)
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000495 result_set.testsuites.add((result_set.current_tool,
496 result_set.current_exp))
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000497 elif IsToolLine(line):
498 result_set.current_tool = _TOOL_LINE_REX.match(line).groups()[0]
Maxim Kuvyrkovd8951a22021-07-08 08:20:28 +0000499 result_set.current_exp = None
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000500 elif IsSummaryLine(line):
501 result_set.ResetToolExp()
Maxim Kuvyrkova0d90542024-07-23 11:37:04 +0000502
503 if json_input:
504 result_set.ResetToolExp()
505
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000506 sum_file.close()
507 return result_set
508
509
510def GetManifest(manifest_path):
511 """Build a set of expected failures from the manifest file.
512
513 Each entry in the manifest file should have the format understood
514 by the TestResult constructor.
515
516 If no manifest file exists for this target, it returns an empty set.
517 """
518 if os.path.exists(manifest_path):
519 return ParseManifest(manifest_path)
520 else:
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000521 return ResultSet()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000522
523
524def CollectSumFiles(builddir):
525 sum_files = []
526 for root, dirs, files in os.walk(builddir):
527 for ignored in ('.svn', '.git'):
528 if ignored in dirs:
529 dirs.remove(ignored)
530 for fname in files:
Maxim Kuvyrkovdfc04502024-07-29 13:00:19 +0000531 if fname.endswith('.sum') or fname.endswith('.json'):
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000532 sum_files.append(os.path.join(root, fname))
533 return sum_files
534
535
Maxim Kuvyrkov8ef7c852021-07-08 08:21:18 +0000536def GetResults(sum_files, build_results = None):
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000537 """Collect all the test results from the given .sum files."""
Maxim Kuvyrkov8ef7c852021-07-08 08:21:18 +0000538 if build_results == None:
539 build_results = ResultSet()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000540 for sum_fname in sum_files:
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000541 if _OPTIONS.verbosity >= 3:
542 print('\t%s' % sum_fname)
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000543 build_results.update(ParseSummary(sum_fname))
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000544 return build_results
545
Maxim Kuvyrkovc0014022024-04-01 12:26:44 +0000546class ResultsStats:
547 """Describes statistics of DejaGNU test results.
548
549 Attributes:
550 fails: Number of non-flaky failed tests in the results.
551 flaky: Number of flaky entries in the manifest.
552 total: Total number of tests in the results, including flaky passes and
553 fails.
554 """
555
556 def __init__(self):
557 self.fails = 0
558 self.flaky = 0
559 self.total = 0
560
561 def Print(self, outfile=sys.stdout):
562 outfile.write(_SUMMARY_LINE_FORMAT % 'Results')
563 outfile.write(f'\n')
564 outfile.write(f'# of stable fails\t\t{self.fails}\n')
565 outfile.write(f'# of flaky entries\t\t{self.flaky}\n')
566 outfile.write(f'# of all tests\t\t\t{self.total}\n')
567
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000568
569def CompareResults(manifest, actual):
570 """Compare sets of results and return two lists:
571 - List of results present in ACTUAL but missing from MANIFEST.
572 - List of results present in MANIFEST but missing from ACTUAL.
573 """
574 # Collect all the actual results not present in the manifest.
575 # Results in this set will be reported as errors.
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000576 actual_vs_manifest = ResultSet()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000577 for actual_result in actual:
578 if actual_result not in manifest:
579 actual_vs_manifest.add(actual_result)
580
581 # Collect all the tests in the manifest that were not found
582 # in the actual results.
583 # Results in this set will be reported as warnings (since
584 # they are expected failures that are not failing anymore).
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000585 manifest_vs_actual = ResultSet()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000586 for expected_result in manifest:
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000587 # We try to support comparing partial results vs full manifest
588 # (e.g., manifest has failures for gcc, g++, gfortran, but we ran only
589 # g++ testsuite). To achieve this we record encountered testsuites in
590 # actual.testsuites set, and then we check it here using HasTestsuite().
591 if expected_result not in actual and actual.HasTestsuite(expected_result):
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000592 manifest_vs_actual.add(expected_result)
593
594 return actual_vs_manifest, manifest_vs_actual
595
596
Maxim Kuvyrkov918bc262021-07-08 08:27:39 +0000597def GetManifestPath(user_provided_must_exist):
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000598 """Return the full path to the manifest file."""
599 manifest_path = _OPTIONS.manifest
600 if manifest_path:
601 if user_provided_must_exist and not os.path.exists(manifest_path):
602 Error('Manifest does not exist: %s' % manifest_path)
603 return manifest_path
604 else:
Maxim Kuvyrkov918bc262021-07-08 08:27:39 +0000605 (srcdir, target) = GetBuildData()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000606 if not srcdir:
607 Error('Could not determine the location of GCC\'s source tree. '
608 'The Makefile does not contain a definition for "srcdir".')
609 if not target:
610 Error('Could not determine the target triplet for this build. '
611 'The Makefile does not contain a definition for "target_alias".')
612 return _MANIFEST_PATH_PATTERN % (srcdir, _MANIFEST_SUBDIR, target)
613
614
615def GetBuildData():
616 if not ValidBuildDirectory(_OPTIONS.build_dir):
617 # If we have been given a set of results to use, we may
618 # not be inside a valid GCC build directory. In that case,
619 # the user must provide both a manifest file and a set
620 # of results to check against it.
621 if not _OPTIONS.results or not _OPTIONS.manifest:
622 Error('%s is not a valid GCC top level build directory. '
623 'You must use --manifest and --results to do the validation.' %
624 _OPTIONS.build_dir)
625 else:
626 return None, None
627 srcdir = GetMakefileValue('%s/Makefile' % _OPTIONS.build_dir, 'srcdir =')
628 target = GetMakefileValue('%s/Makefile' % _OPTIONS.build_dir, 'target_alias=')
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000629 if _OPTIONS.verbosity >= 3:
630 print('Source directory: %s' % srcdir)
631 print('Build target: %s' % target)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000632 return srcdir, target
633
634
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000635def PrintSummary(summary):
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000636 summary.Print()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000637
638def GetSumFiles(results, build_dir):
639 if not results:
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000640 if _OPTIONS.verbosity >= 3:
641 print('Getting actual results from build directory %s' % build_dir)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000642 sum_files = CollectSumFiles(build_dir)
643 else:
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000644 if _OPTIONS.verbosity >= 3:
645 print('Getting actual results from user-provided results')
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000646 sum_files = results.split()
647 return sum_files
648
Maxim Kuvyrkoveb482c72024-04-01 12:30:19 +0000649def DiscardFlaky(expected, actual):
650 flaky_list = []
651 for expected_result in expected:
652 if 'flaky' in expected_result.attrs:
653 flaky_list.append(expected_result)
654
655 for expected_result in flaky_list:
656 expected.remove(expected_result)
657 actual.discard(expected_result)
658
659 return len(flaky_list)
660
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000661
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000662def PerformComparison(expected, actual):
Maxim Kuvyrkovc0014022024-04-01 12:26:44 +0000663 stats = ResultsStats()
Maxim Kuvyrkoveb482c72024-04-01 12:30:19 +0000664 stats.total = actual.total
665 # We need to ignore flaky tests in comparison, so remove them now from
666 # both expected and actual sets.
667 stats.flaky = DiscardFlaky(expected, actual)
Maxim Kuvyrkovc0014022024-04-01 12:26:44 +0000668 stats.fails = len(actual)
669
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000670 actual_vs_expected, expected_vs_actual = CompareResults(expected, actual)
671
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000672 if _OPTIONS.inverse_match:
673 # Switch results if inverse comparison is requested.
674 # This is useful in detecting flaky tests that FAILed in expected set,
675 # but PASSed in actual set.
676 actual_vs_expected, expected_vs_actual \
Maxim Kuvyrkov7df81782023-05-25 06:42:06 +0000677 = expected_vs_actual, actual_vs_expected
Maxim Kuvyrkov55213032024-04-08 12:52:21 +0000678 stats.fails = len(expected)
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000679
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000680 tests_ok = True
681 if len(actual_vs_expected) > 0:
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000682 if _OPTIONS.verbosity >= 3:
683 print('\n\nUnexpected results in this build (new failures)')
684 if _OPTIONS.verbosity >= 1:
685 PrintSummary(actual_vs_expected)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000686 tests_ok = False
687
Maxim Kuvyrkov55213032024-04-08 12:52:21 +0000688 if _OPTIONS.verbosity >= 1:
Maxim Kuvyrkovc0014022024-04-01 12:26:44 +0000689 stats.Print()
690
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000691 if _OPTIONS.verbosity >= 2 and len(expected_vs_actual) > 0:
692 print('\n\nExpected results not present in this build (fixed tests)'
693 '\n\nNOTE: This is not a failure. It just means that these '
694 'tests were expected\nto fail, but either they worked in '
695 'this configuration or they were not\npresent at all.\n')
696 PrintSummary(expected_vs_actual)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000697
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000698 if tests_ok and _OPTIONS.verbosity >= 3:
Maxim Kuvyrkov63ad5352021-07-04 07:38:22 +0000699 print('\nSUCCESS: No unexpected failures.')
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000700
701 return tests_ok
702
703
704def CheckExpectedResults():
Maxim Kuvyrkov918bc262021-07-08 08:27:39 +0000705 manifest_path = GetManifestPath(True)
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000706 if _OPTIONS.verbosity >= 3:
707 print('Manifest: %s' % manifest_path)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000708 manifest = GetManifest(manifest_path)
709 sum_files = GetSumFiles(_OPTIONS.results, _OPTIONS.build_dir)
710 actual = GetResults(sum_files)
711
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000712 if _OPTIONS.verbosity >= 5:
713 print('\n\nTests expected to fail')
714 PrintSummary(manifest)
715 print('\n\nActual test results')
716 PrintSummary(actual)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000717
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000718 return PerformComparison(manifest, actual)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000719
720
721def ProduceManifest():
Maxim Kuvyrkov918bc262021-07-08 08:27:39 +0000722 manifest_path = GetManifestPath(False)
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000723 if _OPTIONS.verbosity >= 3:
724 print('Manifest: %s' % manifest_path)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000725 if os.path.exists(manifest_path) and not _OPTIONS.force:
726 Error('Manifest file %s already exists.\nUse --force to overwrite.' %
727 manifest_path)
728
729 sum_files = GetSumFiles(_OPTIONS.results, _OPTIONS.build_dir)
730 actual = GetResults(sum_files)
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000731 manifest_file = open(manifest_path, encoding='latin-1', mode='w')
Maxim Kuvyrkov51e3fa12021-07-04 10:58:53 +0000732 actual.Print(manifest_file)
733 actual.Print()
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000734 manifest_file.close()
735
736 return True
737
738
739def CompareBuilds():
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000740 sum_files = GetSumFiles(_OPTIONS.results, _OPTIONS.build_dir)
741 actual = GetResults(sum_files)
742
Maxim Kuvyrkov8ef7c852021-07-08 08:21:18 +0000743 clean = ResultSet()
744
745 if _OPTIONS.manifest:
Maxim Kuvyrkov918bc262021-07-08 08:27:39 +0000746 manifest_path = GetManifestPath(True)
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000747 if _OPTIONS.verbosity >= 3:
748 print('Manifest: %s' % manifest_path)
Maxim Kuvyrkov8ef7c852021-07-08 08:21:18 +0000749 clean = GetManifest(manifest_path)
750
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000751 clean_sum_files = GetSumFiles(_OPTIONS.results, _OPTIONS.clean_build)
Maxim Kuvyrkov8ef7c852021-07-08 08:21:18 +0000752 clean = GetResults(clean_sum_files, clean)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000753
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000754 return PerformComparison(clean, actual)
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000755
756
757def Main(argv):
758 parser = optparse.OptionParser(usage=__doc__)
759
760 # Keep the following list sorted by option name.
761 parser.add_option('--build_dir', action='store', type='string',
762 dest='build_dir', default='.',
763 help='Build directory to check (default = .)')
764 parser.add_option('--clean_build', action='store', type='string',
765 dest='clean_build', default=None,
766 help='Compare test results from this build against '
767 'those of another (clean) build. Use this option '
768 'when comparing the test results of your patch versus '
769 'the test results of a clean build without your patch. '
770 'You must provide the path to the top directory of your '
771 'clean build.')
772 parser.add_option('--force', action='store_true', dest='force',
773 default=False, help='When used with --produce_manifest, '
774 'it will overwrite an existing manifest file '
775 '(default = False)')
Maxim Kuvyrkov158e61d2023-05-25 12:18:30 +0000776 parser.add_option('--expiry_date', action='store',
777 dest='expiry_today_date', default=None,
778 help='Use provided date YYYYMMDD to decide whether '
779 'manifest entries with expiry settings have expired '
780 'or not. (default = Use today date)')
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000781 parser.add_option('--srcpath', action='store', type='string',
782 dest='srcpath_regex', default='[^ ]+/testsuite/',
783 help='Remove provided path (can be a regex) from '
784 'the result entries. This is useful to remove '
785 'occasional filesystem path from the results. '
786 '(default = "[^ ]+/testsuite/")')
Maxim Kuvyrkovd8d6c472023-05-03 15:53:17 +0000787 parser.add_option('--inverse_match', action='store_true',
788 dest='inverse_match', default=False,
789 help='Inverse result sets in comparison. '
790 'Output unexpected passes as unexpected failures and '
791 'unexpected failures as unexpected passes. '
792 'This is used to catch FAIL->PASS flaky tests. '
793 '(default = False)')
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000794 parser.add_option('--manifest', action='store', type='string',
795 dest='manifest', default=None,
796 help='Name of the manifest file to use (default = '
797 'taken from '
798 'contrib/testsuite-managment/<target_alias>.xfail)')
799 parser.add_option('--produce_manifest', action='store_true',
800 dest='produce_manifest', default=False,
801 help='Produce the manifest for the current '
802 'build (default = False)')
803 parser.add_option('--results', action='store', type='string',
804 dest='results', default=None, help='Space-separated list '
805 'of .sum files with the testing results to check. The '
806 'only content needed from these files are the lines '
807 'starting with FAIL, XPASS or UNRESOLVED (default = '
808 '.sum files collected from the build directory).')
809 parser.add_option('--verbosity', action='store', dest='verbosity',
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000810 type='int', default=3, help='Verbosity level '
811 '(default = 3). Level 0: only error output, this is '
812 'useful in scripting when only the exit code is used. '
813 'Level 1: output unexpected failures. '
814 'Level 2: output unexpected passes. '
815 'Level 3: output helpful information. '
Maxim Kuvyrkov8396bb32023-06-14 14:32:38 +0000816 'Level 4: output notification on expired entries. '
Maxim Kuvyrkov40205382021-07-12 15:41:47 +0000817 'Level 5: output debug information.')
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000818 global _OPTIONS
819 (_OPTIONS, _) = parser.parse_args(argv[1:])
820
Maxim Kuvyrkov158e61d2023-05-25 12:18:30 +0000821 # Set "today" date to compare expiration entries against.
822 # Setting expiration date into the future allows re-detection of flaky
823 # tests and creating fresh entries for them before the current flaky entries
824 # expire.
825 if _OPTIONS.expiry_today_date:
826 today_date = re.search(r'(\d\d\d\d)(\d\d)(\d\d)',
827 _OPTIONS.expiry_today_date)
828 if not today_date:
829 Error('Invalid --expiry_today_date format "%s". Must be of the form '
830 '"expire=YYYYMMDD"' % _OPTIONS.expiry_today_date)
831 _OPTIONS.expiry_today_date=datetime.date(int(today_date.group(1)),
832 int(today_date.group(2)),
833 int(today_date.group(3)))
834 else:
835 _OPTIONS.expiry_today_date = datetime.date.today()
836
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000837 if _OPTIONS.produce_manifest:
838 retval = ProduceManifest()
839 elif _OPTIONS.clean_build:
840 retval = CompareBuilds()
841 else:
842 retval = CheckExpectedResults()
843
844 if retval:
845 return 0
846 else:
Maxim Kuvyrkov972bb812021-08-30 14:18:09 +0000847 return 2
Maxim Kuvyrkov59877482021-07-07 11:22:26 +0000848
849
850if __name__ == '__main__':
851 retval = Main(sys.argv)
852 sys.exit(retval)