import json import time from glob import glob from typing import List import typer from typing_extensions import TypedDict from util.expansions import get_expansion from buildscripts.parse_test_xml import parse_test_xml class Result(TypedDict): """A single test result""" status: str test_file: str log_raw: str start: float end: float class Report(TypedDict): """Test report for Evergreen""" results: List[Result] def main(testlog_dir: str): """Create an report.json for Evergreen from bazel test logs.""" if not get_expansion("create_bazel_test_report"): print( "Expansion create_bazel_test_report is not set, skipping creating a report from Bazel test logs." ) return report = Report({"results": []}) for test_xml in glob(f"{testlog_dir}/**/test.xml", recursive=True): testsuite = parse_test_xml(test_xml).find("testsuite") testcase = testsuite.find("testcase") # Replace part of the name added by the remote test wrapper script test_file = testcase.attrib["name"].replace("_remote_exec", "") if testcase.find("error") is not None: status = "fail" else: status = "pass" log_raw = testsuite.find("system-out").text # Bazel gives just a duration, while Evergreen expects a start and end # time to calculate the duration. Evergreen itself does something similar # for when only a duration is known. duration = testcase.attrib["time"] start = time.time() end = start + int(duration) report["results"].append( Result( { "test_file": test_file, "status": status, "start": start, "end": end, "log_raw": log_raw, } ) ) if report["results"]: with open("report.json", "wt") as fh: json.dump(report, fh) else: print(f"No test.xml found within {testlog_dir}. Not creating a report.") if __name__ == "__main__": typer.run(main)