Skip to content

Commit 8507dba

Browse files
[llvm][llvm-lit] Add option to create unique result file names if results already exist (#112729)
When running a build like: ``` ninja check-clang check-llvm ``` Prior to my changes you ended up with one results file, in this specific case Junit XML: ``` results.xml ``` This would only include the last set of tests lit ran, which were for llvm. To get around this, many CI systems will run one check target, move the file away, then run another, somehow propgating the return code as well. ``` rectode=0 for target in targets: ninja target retcode=$? mv results.xml results-${target}.xml <report the overall return code> ``` I want to use something like this Buildkite reporting plugin in CI, which needs to have all the results available: https://buildkite.com/docs/agent/v3/cli-annotate#using-annotations-to-report-test-results Modifying CI's build scripts for Windows and Linux is a lot of work. So my changes instead make lit detect an existing result file and modify the file name to find a new file to write to. Now you will get: ``` results.xml results.<tempfile generated value>.xml ``` This will work for all result file types since I'm doing it in the base Report class. Now you've got separate files, it's easy to collect them with `<path>/*.xml`. Note that the `<tempfile generated value>` is not ordered.
1 parent 4f06f79 commit 8507dba

File tree

3 files changed

+89
-38
lines changed

3 files changed

+89
-38
lines changed

llvm/utils/lit/lit/cl_arguments.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,15 @@ def parse_args():
175175
type=lit.reports.TimeTraceReport,
176176
help="Write Chrome tracing compatible JSON to the specified file",
177177
)
178+
execution_group.add_argument(
179+
"--use-unique-output-file-name",
180+
help="When enabled, lit will not overwrite existing test report files. "
181+
"Instead it will write to a new file named the same as the output file "
182+
"name but with an extra part before the file extension. For example "
183+
"if results.xml already exists, results.<something>.xml will be written "
184+
"to. The <something> is not ordered in any way. [Default: Off]",
185+
action="store_true",
186+
)
178187
execution_group.add_argument(
179188
"--timeout",
180189
dest="maxIndividualTestTime",
@@ -332,16 +341,21 @@ def parse_args():
332341
else:
333342
opts.shard = None
334343

335-
opts.reports = filter(
336-
None,
337-
[
338-
opts.output,
339-
opts.xunit_xml_output,
340-
opts.resultdb_output,
341-
opts.time_trace_output,
342-
],
344+
opts.reports = list(
345+
filter(
346+
None,
347+
[
348+
opts.output,
349+
opts.xunit_xml_output,
350+
opts.resultdb_output,
351+
opts.time_trace_output,
352+
],
353+
)
343354
)
344355

356+
for report in opts.reports:
357+
report.use_unique_output_file_name = opts.use_unique_output_file_name
358+
345359
return opts
346360

347361

llvm/utils/lit/lit/reports.py

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import abc
12
import base64
23
import datetime
34
import itertools
45
import json
6+
import os
7+
import tempfile
58

69
from xml.sax.saxutils import quoteattr as quo
710

@@ -14,11 +17,34 @@ def by_suite_and_test_path(test):
1417
return (test.suite.name, id(test.suite), test.path_in_suite)
1518

1619

17-
class JsonReport(object):
20+
class Report(object):
1821
def __init__(self, output_file):
1922
self.output_file = output_file
23+
# Set by the option parser later.
24+
self.use_unique_output_file_name = False
2025

2126
def write_results(self, tests, elapsed):
27+
if self.use_unique_output_file_name:
28+
filename, ext = os.path.splitext(os.path.basename(self.output_file))
29+
fd, _ = tempfile.mkstemp(
30+
suffix=ext, prefix=f"{filename}.", dir=os.path.dirname(self.output_file)
31+
)
32+
report_file = os.fdopen(fd, "w")
33+
else:
34+
# Overwrite if the results already exist.
35+
report_file = open(self.output_file, "w")
36+
37+
with report_file:
38+
self._write_results_to_file(tests, elapsed, report_file)
39+
40+
@abc.abstractmethod
41+
def _write_results_to_file(self, tests, elapsed, file):
42+
"""Write test results to the file object "file"."""
43+
pass
44+
45+
46+
class JsonReport(Report):
47+
def _write_results_to_file(self, tests, elapsed, file):
2248
unexecuted_codes = {lit.Test.EXCLUDED, lit.Test.SKIPPED}
2349
tests = [t for t in tests if t.result.code not in unexecuted_codes]
2450
# Construct the data we will write.
@@ -67,9 +93,8 @@ def write_results(self, tests, elapsed):
6793

6894
tests_data.append(test_data)
6995

70-
with open(self.output_file, "w") as file:
71-
json.dump(data, file, indent=2, sort_keys=True)
72-
file.write("\n")
96+
json.dump(data, file, indent=2, sort_keys=True)
97+
file.write("\n")
7398

7499

75100
_invalid_xml_chars_dict = {
@@ -88,21 +113,18 @@ def remove_invalid_xml_chars(s):
88113
return s.translate(_invalid_xml_chars_dict)
89114

90115

91-
class XunitReport(object):
92-
def __init__(self, output_file):
93-
self.output_file = output_file
94-
self.skipped_codes = {lit.Test.EXCLUDED, lit.Test.SKIPPED, lit.Test.UNSUPPORTED}
116+
class XunitReport(Report):
117+
skipped_codes = {lit.Test.EXCLUDED, lit.Test.SKIPPED, lit.Test.UNSUPPORTED}
95118

96-
def write_results(self, tests, elapsed):
119+
def _write_results_to_file(self, tests, elapsed, file):
97120
tests.sort(key=by_suite_and_test_path)
98121
tests_by_suite = itertools.groupby(tests, lambda t: t.suite)
99122

100-
with open(self.output_file, "w") as file:
101-
file.write('<?xml version="1.0" encoding="UTF-8"?>\n')
102-
file.write('<testsuites time="{time:.2f}">\n'.format(time=elapsed))
103-
for suite, test_iter in tests_by_suite:
104-
self._write_testsuite(file, suite, list(test_iter))
105-
file.write("</testsuites>\n")
123+
file.write('<?xml version="1.0" encoding="UTF-8"?>\n')
124+
file.write('<testsuites time="{time:.2f}">\n'.format(time=elapsed))
125+
for suite, test_iter in tests_by_suite:
126+
self._write_testsuite(file, suite, list(test_iter))
127+
file.write("</testsuites>\n")
106128

107129
def _write_testsuite(self, file, suite, tests):
108130
skipped = 0
@@ -206,11 +228,8 @@ def gen_resultdb_test_entry(
206228
return test_data
207229

208230

209-
class ResultDBReport(object):
210-
def __init__(self, output_file):
211-
self.output_file = output_file
212-
213-
def write_results(self, tests, elapsed):
231+
class ResultDBReport(Report):
232+
def _write_results_to_file(self, tests, elapsed, file):
214233
unexecuted_codes = {lit.Test.EXCLUDED, lit.Test.SKIPPED}
215234
tests = [t for t in tests if t.result.code not in unexecuted_codes]
216235
data = {}
@@ -249,17 +268,14 @@ def write_results(self, tests, elapsed):
249268
)
250269
)
251270

252-
with open(self.output_file, "w") as file:
253-
json.dump(data, file, indent=2, sort_keys=True)
254-
file.write("\n")
271+
json.dump(data, file, indent=2, sort_keys=True)
272+
file.write("\n")
255273

256274

257-
class TimeTraceReport(object):
258-
def __init__(self, output_file):
259-
self.output_file = output_file
260-
self.skipped_codes = {lit.Test.EXCLUDED, lit.Test.SKIPPED, lit.Test.UNSUPPORTED}
275+
class TimeTraceReport(Report):
276+
skipped_codes = {lit.Test.EXCLUDED, lit.Test.SKIPPED, lit.Test.UNSUPPORTED}
261277

262-
def write_results(self, tests, elapsed):
278+
def _write_results_to_file(self, tests, elapsed, file):
263279
# Find when first test started so we can make start times relative.
264280
first_start_time = min([t.result.start for t in tests])
265281
events = [
@@ -270,8 +286,7 @@ def write_results(self, tests, elapsed):
270286

271287
json_data = {"traceEvents": events}
272288

273-
with open(self.output_file, "w") as time_trace_file:
274-
json.dump(json_data, time_trace_file, indent=2, sort_keys=True)
289+
json.dump(json_data, time_trace_file, indent=2, sort_keys=True)
275290

276291
def _get_test_event(self, test, first_start_time):
277292
test_name = test.getFullName()
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
## Check that lit will not overwrite existing result files when given
2+
## --use-unique-output-file-name.
3+
4+
## Files are overwritten without the option.
5+
# RUN: rm -f %t.xunit*.xml
6+
# RUN: echo "test" > %t.xunit.xml
7+
# RUN: not %{lit} --xunit-xml-output %t.xunit.xml %{inputs}/xunit-output
8+
# RUN: FileCheck < %t.xunit.xml %s --check-prefix=NEW
9+
# NEW: <?xml version="1.0" encoding="UTF-8"?>
10+
# NEW-NEXT: <testsuites time="{{[0-9.]+}}">
11+
## (other tests will check the contents of the whole file)
12+
13+
# RUN: rm -f %t.xunit*.xml
14+
# RUN: echo "test" > %t.xunit.xml
15+
## Files should not be overwritten with the option.
16+
# RUN: not %{lit} --xunit-xml-output %t.xunit.xml --use-unique-output-file-name %{inputs}/xunit-output
17+
# RUN: FileCheck < %t.xunit.xml %s --check-prefix=EXISTING
18+
# EXISTING: test
19+
## Results in a new file with some discriminator added.
20+
# RUN: ls -l %t.xunit*.xml | wc -l | FileCheck %s --check-prefix=NUMFILES
21+
# NUMFILES: 2
22+
# RUN: FileCheck < %t.xunit.*.xml %s --check-prefix=NEW

0 commit comments

Comments
 (0)