xref: /llvm-project/llvm/utils/lit/lit/reports.py (revision 8507dbaec3f644b8a0c6291f097800d82a4f4b16)
1*8507dbaeSDavid Spickettimport abc
2253cb50cSHaowei Wuimport base64
3253cb50cSHaowei Wuimport datetime
47ffb5bc2SJulian Lettnerimport itertools
57ffb5bc2SJulian Lettnerimport json
6*8507dbaeSDavid Spickettimport os
7*8507dbaeSDavid Spickettimport tempfile
87ffb5bc2SJulian Lettner
97ffb5bc2SJulian Lettnerfrom xml.sax.saxutils import quoteattr as quo
107ffb5bc2SJulian Lettner
117ffb5bc2SJulian Lettnerimport lit.Test
127ffb5bc2SJulian Lettner
137ffb5bc2SJulian Lettner
14840bc47fSJulian Lettnerdef by_suite_and_test_path(test):
15840bc47fSJulian Lettner    # Suite names are not necessarily unique.  Include object identity in sort
16840bc47fSJulian Lettner    # key to avoid mixing tests of different suites.
17840bc47fSJulian Lettner    return (test.suite.name, id(test.suite), test.path_in_suite)
18840bc47fSJulian Lettner
19840bc47fSJulian Lettner
20*8507dbaeSDavid Spickettclass Report(object):
217ffb5bc2SJulian Lettner    def __init__(self, output_file):
227ffb5bc2SJulian Lettner        self.output_file = output_file
23*8507dbaeSDavid Spickett        # Set by the option parser later.
24*8507dbaeSDavid Spickett        self.use_unique_output_file_name = False
257ffb5bc2SJulian Lettner
267ffb5bc2SJulian Lettner    def write_results(self, tests, elapsed):
27*8507dbaeSDavid Spickett        if self.use_unique_output_file_name:
28*8507dbaeSDavid Spickett            filename, ext = os.path.splitext(os.path.basename(self.output_file))
29*8507dbaeSDavid Spickett            fd, _ = tempfile.mkstemp(
30*8507dbaeSDavid Spickett                suffix=ext, prefix=f"{filename}.", dir=os.path.dirname(self.output_file)
31*8507dbaeSDavid Spickett            )
32*8507dbaeSDavid Spickett            report_file = os.fdopen(fd, "w")
33*8507dbaeSDavid Spickett        else:
34*8507dbaeSDavid Spickett            # Overwrite if the results already exist.
35*8507dbaeSDavid Spickett            report_file = open(self.output_file, "w")
36*8507dbaeSDavid Spickett
37*8507dbaeSDavid Spickett        with report_file:
38*8507dbaeSDavid Spickett            self._write_results_to_file(tests, elapsed, report_file)
39*8507dbaeSDavid Spickett
40*8507dbaeSDavid Spickett    @abc.abstractmethod
41*8507dbaeSDavid Spickett    def _write_results_to_file(self, tests, elapsed, file):
42*8507dbaeSDavid Spickett        """Write test results to the file object "file"."""
43*8507dbaeSDavid Spickett        pass
44*8507dbaeSDavid Spickett
45*8507dbaeSDavid Spickett
46*8507dbaeSDavid Spickettclass JsonReport(Report):
47*8507dbaeSDavid Spickett    def _write_results_to_file(self, tests, elapsed, file):
48968f58c6SJulian Lettner        unexecuted_codes = {lit.Test.EXCLUDED, lit.Test.SKIPPED}
49968f58c6SJulian Lettner        tests = [t for t in tests if t.result.code not in unexecuted_codes]
507ffb5bc2SJulian Lettner        # Construct the data we will write.
517ffb5bc2SJulian Lettner        data = {}
527ffb5bc2SJulian Lettner        # Encode the current lit version as a schema version.
53b71edfaaSTobias Hieta        data["__version__"] = lit.__versioninfo__
54b71edfaaSTobias Hieta        data["elapsed"] = elapsed
557ffb5bc2SJulian Lettner        # FIXME: Record some information on the lit configuration used?
567ffb5bc2SJulian Lettner        # FIXME: Record information from the individual test suites?
577ffb5bc2SJulian Lettner
587ffb5bc2SJulian Lettner        # Encode the tests.
59b71edfaaSTobias Hieta        data["tests"] = tests_data = []
607ffb5bc2SJulian Lettner        for test in tests:
617ffb5bc2SJulian Lettner            test_data = {
62b71edfaaSTobias Hieta                "name": test.getFullName(),
63b71edfaaSTobias Hieta                "code": test.result.code.name,
64b71edfaaSTobias Hieta                "output": test.result.output,
65b71edfaaSTobias Hieta                "elapsed": test.result.elapsed,
66b71edfaaSTobias Hieta            }
677ffb5bc2SJulian Lettner
687ffb5bc2SJulian Lettner            # Add test metrics, if present.
697ffb5bc2SJulian Lettner            if test.result.metrics:
70b71edfaaSTobias Hieta                test_data["metrics"] = metrics_data = {}
717ffb5bc2SJulian Lettner                for key, value in test.result.metrics.items():
727ffb5bc2SJulian Lettner                    metrics_data[key] = value.todata()
737ffb5bc2SJulian Lettner
747ffb5bc2SJulian Lettner            # Report micro-tests separately, if present
757ffb5bc2SJulian Lettner            if test.result.microResults:
767ffb5bc2SJulian Lettner                for key, micro_test in test.result.microResults.items():
777ffb5bc2SJulian Lettner                    # Expand parent test name with micro test name
787ffb5bc2SJulian Lettner                    parent_name = test.getFullName()
79b71edfaaSTobias Hieta                    micro_full_name = parent_name + ":" + key
807ffb5bc2SJulian Lettner
817ffb5bc2SJulian Lettner                    micro_test_data = {
82b71edfaaSTobias Hieta                        "name": micro_full_name,
83b71edfaaSTobias Hieta                        "code": micro_test.code.name,
84b71edfaaSTobias Hieta                        "output": micro_test.output,
85b71edfaaSTobias Hieta                        "elapsed": micro_test.elapsed,
86b71edfaaSTobias Hieta                    }
877ffb5bc2SJulian Lettner                    if micro_test.metrics:
88b71edfaaSTobias Hieta                        micro_test_data["metrics"] = micro_metrics_data = {}
897ffb5bc2SJulian Lettner                        for key, value in micro_test.metrics.items():
907ffb5bc2SJulian Lettner                            micro_metrics_data[key] = value.todata()
917ffb5bc2SJulian Lettner
927ffb5bc2SJulian Lettner                    tests_data.append(micro_test_data)
937ffb5bc2SJulian Lettner
947ffb5bc2SJulian Lettner            tests_data.append(test_data)
957ffb5bc2SJulian Lettner
967ffb5bc2SJulian Lettner        json.dump(data, file, indent=2, sort_keys=True)
97b71edfaaSTobias Hieta        file.write("\n")
987ffb5bc2SJulian Lettner
997ffb5bc2SJulian Lettner
100b71edfaaSTobias Hieta_invalid_xml_chars_dict = {
101b71edfaaSTobias Hieta    c: None for c in range(32) if chr(c) not in ("\t", "\n", "\r")
102b71edfaaSTobias Hieta}
1033b3cdcc7SAlex Richardson
1043b3cdcc7SAlex Richardson
1053b3cdcc7SAlex Richardsondef remove_invalid_xml_chars(s):
1063b3cdcc7SAlex Richardson    # According to the XML 1.0 spec, control characters other than
1073b3cdcc7SAlex Richardson    # \t,\r, and \n are not permitted anywhere in the document
1083b3cdcc7SAlex Richardson    # (https://www.w3.org/TR/xml/#charsets) and therefore this function
1093b3cdcc7SAlex Richardson    # removes them to produce a valid XML document.
1103b3cdcc7SAlex Richardson    #
1113b3cdcc7SAlex Richardson    # Note: In XML 1.1 only \0 is illegal (https://www.w3.org/TR/xml11/#charsets)
1123b3cdcc7SAlex Richardson    # but lit currently produces XML 1.0 output.
1133b3cdcc7SAlex Richardson    return s.translate(_invalid_xml_chars_dict)
1143b3cdcc7SAlex Richardson
1153b3cdcc7SAlex Richardson
116*8507dbaeSDavid Spickettclass XunitReport(Report):
117*8507dbaeSDavid Spickett    skipped_codes = {lit.Test.EXCLUDED, lit.Test.SKIPPED, lit.Test.UNSUPPORTED}
1187ffb5bc2SJulian Lettner
119*8507dbaeSDavid Spickett    def _write_results_to_file(self, tests, elapsed, file):
120840bc47fSJulian Lettner        tests.sort(key=by_suite_and_test_path)
1217ffb5bc2SJulian Lettner        tests_by_suite = itertools.groupby(tests, lambda t: t.suite)
1227ffb5bc2SJulian Lettner
1237ffb5bc2SJulian Lettner        file.write('<?xml version="1.0" encoding="UTF-8"?>\n')
124722e5d6aSAlex Richardson        file.write('<testsuites time="{time:.2f}">\n'.format(time=elapsed))
1257ffb5bc2SJulian Lettner        for suite, test_iter in tests_by_suite:
1267ffb5bc2SJulian Lettner            self._write_testsuite(file, suite, list(test_iter))
127b71edfaaSTobias Hieta        file.write("</testsuites>\n")
1287ffb5bc2SJulian Lettner
1297ffb5bc2SJulian Lettner    def _write_testsuite(self, file, suite, tests):
130383df163SDavid Spickett        skipped = 0
131383df163SDavid Spickett        failures = 0
132383df163SDavid Spickett        time = 0.0
133383df163SDavid Spickett
134383df163SDavid Spickett        for t in tests:
135383df163SDavid Spickett            if t.result.code in self.skipped_codes:
136383df163SDavid Spickett                skipped += 1
137383df163SDavid Spickett            if t.isFailure():
138383df163SDavid Spickett                failures += 1
1394f06f79cSDavid Spickett            time += t.result.elapsed or 0.0
1407ffb5bc2SJulian Lettner
141b71edfaaSTobias Hieta        name = suite.config.name.replace(".", "-")
142b71edfaaSTobias Hieta        file.write(
143383df163SDavid Spickett            f'<testsuite name={quo(name)} tests="{len(tests)}" failures="{failures}" skipped="{skipped}" time="{time:.2f}">\n'
144b71edfaaSTobias Hieta        )
1457ffb5bc2SJulian Lettner        for test in tests:
1467ffb5bc2SJulian Lettner            self._write_test(file, test, name)
147b71edfaaSTobias Hieta        file.write("</testsuite>\n")
1487ffb5bc2SJulian Lettner
1497ffb5bc2SJulian Lettner    def _write_test(self, file, test, suite_name):
150b71edfaaSTobias Hieta        path = "/".join(test.path_in_suite[:-1]).replace(".", "_")
151b71edfaaSTobias Hieta        class_name = f"{suite_name}.{path or suite_name}"
1527ffb5bc2SJulian Lettner        name = test.path_in_suite[-1]
1537ffb5bc2SJulian Lettner        time = test.result.elapsed or 0.0
154b71edfaaSTobias Hieta        file.write(
155b71edfaaSTobias Hieta            f'<testcase classname={quo(class_name)} name={quo(name)} time="{time:.2f}"'
156b71edfaaSTobias Hieta        )
1577ffb5bc2SJulian Lettner
1587ffb5bc2SJulian Lettner        if test.isFailure():
159b71edfaaSTobias Hieta            file.write(">\n  <failure><![CDATA[")
1607ffb5bc2SJulian Lettner            # In the unlikely case that the output contains the CDATA
1617ffb5bc2SJulian Lettner            # terminator we wrap it by creating a new CDATA block.
162b71edfaaSTobias Hieta            output = test.result.output.replace("]]>", "]]]]><![CDATA[>")
1637ffb5bc2SJulian Lettner            if isinstance(output, bytes):
164b71edfaaSTobias Hieta                output = output.decode("utf-8", "ignore")
1653b3cdcc7SAlex Richardson
1663b3cdcc7SAlex Richardson            # Failing test  output sometimes contains control characters like
1673b3cdcc7SAlex Richardson            # \x1b (e.g. if there was some -fcolor-diagnostics output) which are
1683b3cdcc7SAlex Richardson            # not allowed inside XML files.
1693b3cdcc7SAlex Richardson            # This causes problems with CI systems: for example, the Jenkins
1703b3cdcc7SAlex Richardson            # JUnit XML will throw an exception when ecountering those
1713b3cdcc7SAlex Richardson            # characters and similar problems also occur with GitLab CI.
1723b3cdcc7SAlex Richardson            output = remove_invalid_xml_chars(output)
1737ffb5bc2SJulian Lettner            file.write(output)
174b71edfaaSTobias Hieta            file.write("]]></failure>\n</testcase>\n")
1757ffb5bc2SJulian Lettner        elif test.result.code in self.skipped_codes:
1767ffb5bc2SJulian Lettner            reason = self._get_skip_reason(test)
177b71edfaaSTobias Hieta            file.write(f">\n  <skipped message={quo(reason)}/>\n</testcase>\n")
1787ffb5bc2SJulian Lettner        else:
179b71edfaaSTobias Hieta            file.write("/>\n")
1807ffb5bc2SJulian Lettner
1817ffb5bc2SJulian Lettner    def _get_skip_reason(self, test):
1827ffb5bc2SJulian Lettner        code = test.result.code
1837ffb5bc2SJulian Lettner        if code == lit.Test.EXCLUDED:
184b71edfaaSTobias Hieta            return "Test not selected (--filter, --max-tests)"
1857ffb5bc2SJulian Lettner        if code == lit.Test.SKIPPED:
186b71edfaaSTobias Hieta            return "User interrupt"
1877ffb5bc2SJulian Lettner
1887ffb5bc2SJulian Lettner        assert code == lit.Test.UNSUPPORTED
1897ffb5bc2SJulian Lettner        features = test.getMissingRequiredFeatures()
1907ffb5bc2SJulian Lettner        if features:
191b71edfaaSTobias Hieta            return "Missing required feature(s): " + ", ".join(features)
192b71edfaaSTobias Hieta        return "Unsupported configuration"
19398827fedSRussell Gallop
19498827fedSRussell Gallop
195253cb50cSHaowei Wudef gen_resultdb_test_entry(
196253cb50cSHaowei Wu    test_name, start_time, elapsed_time, test_output, result_code, is_expected
197253cb50cSHaowei Wu):
198253cb50cSHaowei Wu    test_data = {
199b71edfaaSTobias Hieta        "testId": test_name,
200b71edfaaSTobias Hieta        "start_time": datetime.datetime.fromtimestamp(start_time).isoformat() + "Z",
201b71edfaaSTobias Hieta        "duration": "%.9fs" % elapsed_time,
202b71edfaaSTobias Hieta        "summary_html": '<p><text-artifact artifact-id="artifact-content-in-request"></p>',
203b71edfaaSTobias Hieta        "artifacts": {
204b71edfaaSTobias Hieta            "artifact-content-in-request": {
205b71edfaaSTobias Hieta                "contents": base64.b64encode(test_output.encode("utf-8")).decode(
206b71edfaaSTobias Hieta                    "utf-8"
207253cb50cSHaowei Wu                ),
208253cb50cSHaowei Wu            },
209253cb50cSHaowei Wu        },
210b71edfaaSTobias Hieta        "expected": is_expected,
211253cb50cSHaowei Wu    }
212253cb50cSHaowei Wu    if (
213253cb50cSHaowei Wu        result_code == lit.Test.PASS
214253cb50cSHaowei Wu        or result_code == lit.Test.XPASS
215253cb50cSHaowei Wu        or result_code == lit.Test.FLAKYPASS
216253cb50cSHaowei Wu    ):
217b71edfaaSTobias Hieta        test_data["status"] = "PASS"
218253cb50cSHaowei Wu    elif result_code == lit.Test.FAIL or result_code == lit.Test.XFAIL:
219b71edfaaSTobias Hieta        test_data["status"] = "FAIL"
220253cb50cSHaowei Wu    elif (
221253cb50cSHaowei Wu        result_code == lit.Test.UNSUPPORTED
222253cb50cSHaowei Wu        or result_code == lit.Test.SKIPPED
223253cb50cSHaowei Wu        or result_code == lit.Test.EXCLUDED
224253cb50cSHaowei Wu    ):
225b71edfaaSTobias Hieta        test_data["status"] = "SKIP"
226253cb50cSHaowei Wu    elif result_code == lit.Test.UNRESOLVED or result_code == lit.Test.TIMEOUT:
227b71edfaaSTobias Hieta        test_data["status"] = "ABORT"
228253cb50cSHaowei Wu    return test_data
229253cb50cSHaowei Wu
230253cb50cSHaowei Wu
231*8507dbaeSDavid Spickettclass ResultDBReport(Report):
232*8507dbaeSDavid Spickett    def _write_results_to_file(self, tests, elapsed, file):
233253cb50cSHaowei Wu        unexecuted_codes = {lit.Test.EXCLUDED, lit.Test.SKIPPED}
234253cb50cSHaowei Wu        tests = [t for t in tests if t.result.code not in unexecuted_codes]
235253cb50cSHaowei Wu        data = {}
236b71edfaaSTobias Hieta        data["__version__"] = lit.__versioninfo__
237b71edfaaSTobias Hieta        data["elapsed"] = elapsed
238253cb50cSHaowei Wu        # Encode the tests.
239b71edfaaSTobias Hieta        data["tests"] = tests_data = []
240253cb50cSHaowei Wu        for test in tests:
241253cb50cSHaowei Wu            tests_data.append(
242253cb50cSHaowei Wu                gen_resultdb_test_entry(
243253cb50cSHaowei Wu                    test_name=test.getFullName(),
244253cb50cSHaowei Wu                    start_time=test.result.start,
245253cb50cSHaowei Wu                    elapsed_time=test.result.elapsed,
246253cb50cSHaowei Wu                    test_output=test.result.output,
247253cb50cSHaowei Wu                    result_code=test.result.code,
248253cb50cSHaowei Wu                    is_expected=not test.result.code.isFailure,
249253cb50cSHaowei Wu                )
250253cb50cSHaowei Wu            )
251253cb50cSHaowei Wu            if test.result.microResults:
252253cb50cSHaowei Wu                for key, micro_test in test.result.microResults.items():
253253cb50cSHaowei Wu                    # Expand parent test name with micro test name
254253cb50cSHaowei Wu                    parent_name = test.getFullName()
255b71edfaaSTobias Hieta                    micro_full_name = parent_name + ":" + key + "microres"
256253cb50cSHaowei Wu                    tests_data.append(
257253cb50cSHaowei Wu                        gen_resultdb_test_entry(
258253cb50cSHaowei Wu                            test_name=micro_full_name,
259253cb50cSHaowei Wu                            start_time=micro_test.start
260253cb50cSHaowei Wu                            if micro_test.start
261253cb50cSHaowei Wu                            else test.result.start,
262253cb50cSHaowei Wu                            elapsed_time=micro_test.elapsed
263253cb50cSHaowei Wu                            if micro_test.elapsed
264253cb50cSHaowei Wu                            else test.result.elapsed,
265253cb50cSHaowei Wu                            test_output=micro_test.output,
266253cb50cSHaowei Wu                            result_code=micro_test.code,
267253cb50cSHaowei Wu                            is_expected=not micro_test.code.isFailure,
268253cb50cSHaowei Wu                        )
269253cb50cSHaowei Wu                    )
270253cb50cSHaowei Wu
271253cb50cSHaowei Wu        json.dump(data, file, indent=2, sort_keys=True)
272b71edfaaSTobias Hieta        file.write("\n")
273253cb50cSHaowei Wu
274253cb50cSHaowei Wu
275*8507dbaeSDavid Spickettclass TimeTraceReport(Report):
276*8507dbaeSDavid Spickett    skipped_codes = {lit.Test.EXCLUDED, lit.Test.SKIPPED, lit.Test.UNSUPPORTED}
27798827fedSRussell Gallop
278*8507dbaeSDavid Spickett    def _write_results_to_file(self, tests, elapsed, file):
27998827fedSRussell Gallop        # Find when first test started so we can make start times relative.
28098827fedSRussell Gallop        first_start_time = min([t.result.start for t in tests])
281b71edfaaSTobias Hieta        events = [
282b71edfaaSTobias Hieta            self._get_test_event(x, first_start_time)
283b71edfaaSTobias Hieta            for x in tests
284b71edfaaSTobias Hieta            if x.result.code not in self.skipped_codes
285b71edfaaSTobias Hieta        ]
28698827fedSRussell Gallop
287b71edfaaSTobias Hieta        json_data = {"traceEvents": events}
28898827fedSRussell Gallop
28998827fedSRussell Gallop        json.dump(json_data, time_trace_file, indent=2, sort_keys=True)
29098827fedSRussell Gallop
29198827fedSRussell Gallop    def _get_test_event(self, test, first_start_time):
29298827fedSRussell Gallop        test_name = test.getFullName()
29398827fedSRussell Gallop        elapsed_time = test.result.elapsed or 0.0
29498827fedSRussell Gallop        start_time = test.result.start - first_start_time if test.result.start else 0.0
29598827fedSRussell Gallop        pid = test.result.pid or 0
29698827fedSRussell Gallop        return {
297b71edfaaSTobias Hieta            "pid": pid,
298b71edfaaSTobias Hieta            "tid": 1,
299b71edfaaSTobias Hieta            "ph": "X",
300b71edfaaSTobias Hieta            "ts": int(start_time * 1000000.0),
301b71edfaaSTobias Hieta            "dur": int(elapsed_time * 1000000.0),
302b71edfaaSTobias Hieta            "name": test_name,
30398827fedSRussell Gallop        }
304