"""Utilities for working with report formats generated by Unreal Engine."""
# Standard Library
import datetime
import json
import os
from typing import Any, Dict, List, Optional
from xml.dom import minidom
from xml.etree import ElementTree
from xml.etree.ElementTree import Element
[docs]def report_timestamp_to_iso8601_timestamp(timestamp: str) -> str:
"""Convert Unreal JSON report formatted timestamp to ISO8601 timestamp."""
return datetime.datetime.strptime(timestamp, "%Y.%m.%d-%H.%M.%S").isoformat()
[docs]def report_entry_to_entry_xml(entry: Dict[str, Any]) -> Element:
"""Convert Unreal JSON report entry into jUnit failure XML."""
failure = Element("failure")
failure.set("type", entry.get("event", {}).get("type", ""))
timestamp = entry.get("timestamp")
if timestamp is not None:
failure.set("timestamp", report_timestamp_to_iso8601_timestamp(timestamp))
failure.set(
"message",
f'{entry.get("filename","")}:{entry.get("lineNumber","")} {entry.get("event",{}).get("message","")}',
)
return failure
[docs]def report_test_to_testcase_xml(test: Dict[str, Any]) -> Element:
"""Convert Unreal JSON report test into jUnit testcase."""
test_case = Element("testcase")
test_case.set("name", test.get("testDisplayName", ""))
test_case.set("classname", test.get("fullTestPath", ""))
test_case.set("status", test.get("state", ""))
for entry in test.get("entries", []):
test_case.append(report_entry_to_entry_xml(entry))
return test_case
[docs]def report_object_to_testsuite_xml(report: Dict[str, Any]) -> Element:
"""Convert Unreal JSON report into jUnit testsuite."""
test_suite = Element("testsuite")
test_suite.set("tests", str(len(report.get("tests", []))))
test_suite.set("failures", str(report.get("failed", 0)))
test_suite.set("skipped", str(report.get("notRun", 0)))
test_suite.set("time", str(report.get("totalDuration", 0.0)))
timestamp = report.get("reportCreatedOn")
if timestamp is not None:
test_suite.set(
"timestamp",
report_timestamp_to_iso8601_timestamp(timestamp),
)
branch, changelist, platform = report.get("clientDescriptor", " - - ").split(" - ")
properties = Element("properties")
branch_property = Element("property")
branch_property.set("name", "branch")
branch_property.set("value", branch)
properties.append(branch_property)
changelist_property = Element("property")
changelist_property.set("name", "changelist")
changelist_property.set("value", changelist)
properties.append(changelist_property)
platform_property = Element("property")
platform_property.set("name", "platform")
platform_property.set("value", platform)
properties.append(platform_property)
test_suite.append(properties)
for test in report.get("tests", []):
test_suite.append(report_test_to_testcase_xml(test))
return test_suite
[docs]def json_report_to_dict(report_file: str) -> Dict[str, Any]:
"""Deserialize Unreal JSON report file into a dictionary."""
if not os.path.isfile(report_file):
raise ValueError(f"JSON report not found: {report_file}")
if not os.path.splitext(report_file)[-1] == ".json":
raise ValueError(f"Report file is not JSON: {report_file}")
with open(report_file, "r", encoding="utf-8-sig") as json_report:
report_dct = json.load(json_report)
if not isinstance(report_dct, dict):
raise ValueError(f"JSON report returns non-object: {report_file}")
return report_dct
[docs]def write_junit_xml_report(report_file: str, test_suites: Element) -> None:
"""Write XML test suites to a file."""
if not os.path.splitext(report_file)[-1] == ".xml":
raise ValueError(f"Report file is not XML: {report_file}")
report_dir = os.path.dirname(report_file)
if not os.path.isdir(report_dir):
os.makedirs(report_dir, exist_ok=True)
with open(report_file, "w", encoding="utf-8") as xml_report:
xml_report.write(
minidom.parseString(ElementTree.tostring(test_suites, "utf-8")).toprettyxml(
indent=" " * 4
)
)
[docs]def json_reports_to_junit_xml(junit_file: str, *json_reports: str) -> None:
"""Convert a JSON report from Unreal automation to jUnit XML format."""
test_suites = Element("testsuites")
test_suites.set("name", "Unreal Automation Tests")
total_tests = 0
total_failures = 0
total_errors = 0
total_time = 0.0
for report in json_reports:
test_suite = report_object_to_testsuite_xml(json_report_to_dict(report))
test_suite.set("name", os.path.splitext(os.path.basename(report))[0])
total_tests += int(test_suite.get("tests", 0))
total_failures += int(test_suite.get("failures", 0))
total_errors += int(test_suite.get("errors", 0))
total_time += float(test_suite.get("time", 0.0))
test_suites.append(test_suite)
test_suites.set("tests", str(total_tests))
test_suites.set("failures", str(total_failures))
test_suites.set("errors", str(total_errors))
test_suites.set("time", str(total_time))
write_junit_xml_report(junit_file, test_suites)