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