Files
CC-Tweaked/tools/parse-reports.py
Jonathan Coates c748278331 Export tests reports via OTR
- Configure JUnit tests to write OTR files.
 - Add a custom OTR reporter to the gametest framework.
 - Update parse-reports to ingest OTR files.
 - Gather all report files and publish them as an artifact.
2026-10-25 12:11:46 +00:00

181 lines
5.4 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
#
# SPDX-License-Identifier: MPL-2.0
"""
Parse reports generated by Gradle and convert them into GitHub annotations.
See https://github.com/actions/toolkit/blob/master/docs/problem-matchers.md and
https://github.com/actions/toolkit/blob/master/docs/commands.md.
"""
import dataclasses
import os.path
import pathlib
import re
import xml.etree.ElementTree as ET
LUA_ERROR_LOCATION = re.compile(r"^\s+(/[\w./-]+):(\d+):", re.MULTILINE)
JAVA_LUA_ERROR_LOCATION = re.compile(r"^java.lang.IllegalStateException: (/[\w./-]+):(\d+):")
JAVA_ERROR_LOCATION = re.compile(r"^\tat ([\w.]+)\.[\w]+\([\w.]+:(\d+)\)$", re.MULTILINE)
ERROR_MESSAGE = re.compile(r"(.*)\nstack traceback:", re.DOTALL)
SPACES = re.compile(r"\s+")
SOURCE_LOCATIONS = [
"src/main/java",
"src/main/resources/data/computercraft/lua",
"src/test/java",
"src/test/resources",
]
PROJECT_LOCATIONS = [
"projects/core-api",
"projects/core",
"projects/common-api",
"projects/common",
"projects/fabric-api",
"projects/fabric",
"projects/forge-api",
"projects/forge",
]
def find_file(path: str) -> str | None:
while len(path) > 0 and path[0] == "/":
path = path[1:]
for project in PROJECT_LOCATIONS:
for source_dir in SOURCE_LOCATIONS:
child_path = os.path.join(project, source_dir, path)
if os.path.exists(child_path):
return child_path
return None
def find_location(message: str) -> tuple[str, str] | None:
location = LUA_ERROR_LOCATION.search(message)
if location:
file = find_file(location[1])
if file:
return file, location[2]
location = JAVA_LUA_ERROR_LOCATION.search(message)
if location:
file = find_file(location[1])
if file:
return file, location[2]
for location in JAVA_ERROR_LOCATION.findall(message):
file = find_file(location[0].replace(".", "/") + ".java")
if file:
return file, location[1]
return None
_OTR_CORE = "https://schemas.opentest4j.org/reporting/core/0.2.0"
_OTR_EVENTS = "https://schemas.opentest4j.org/reporting/events/0.2.0"
_OTR_JAVA = "https://schemas.opentest4j.org/reporting/java/0.2.0"
def _parse_junit_file(path: pathlib.Path):
tests: dict[str, str] = {}
tree = ET.parse(path).getroot()
for event in tree:
if event.tag == f"{{{_OTR_CORE}}}infrastructure":
continue
elif event.tag == f"{{{_OTR_EVENTS}}}started":
test_id: str = event.get("id")
test_name: str = event.get("name")
if (parent_id := event.get("parentId")) is not None:
test_name = tests[parent_id] + " ⊳ " + test_name
tests[test_id] = test_name
elif event.tag == f"{{{_OTR_EVENTS}}}finished":
test_id: str = event.get("id")
test_name = tests[test_id]
result = event.find(f"./{{{_OTR_CORE}}}result")
if result is None:
continue
if result.get("status") in ("SUCCESSFUL", "ABORTED"):
continue
reason = result.find(f"./{{{_OTR_CORE}}}reason")
throwable = result.find(f"./{{{_OTR_JAVA}}}throwable")
message = reason.text if reason is not None else ""
full_message = throwable.text if throwable is not None else ""
location = find_location(full_message)
if (match := ERROR_MESSAGE.match(message)) is not None:
error = match.group(1)
else:
error = message
error = SPACES.sub(' ', error)
if location:
print(f"## {location[0]}:{location[1]}: {test_name} failed: {error}")
else:
print(f"::error::{test_name} failed: {error}")
if len(full_message) > 0:
print("::group::Full error message")
print(full_message)
print("::endgroup::")
else:
print(f"Unknown tag: {event.tag}")
def parse_junit() -> None:
"""
Scrape JUnit test reports for errors. We determine the location from the Lua
or Java stacktrace.
"""
print("::add-matcher::.github/matchers/junit.json")
for project in PROJECT_LOCATIONS:
for path in pathlib.Path(os.path.join(project, "build/test-results")).glob("*/open-test-report.xml"):
_parse_junit_file(path)
for path in pathlib.Path(os.path.join(project, "build/test-results")).glob("run*.xml"):
_parse_junit_file(path)
print("::remove-matcher owner=junit::")
def _parse_checkstyle(path: pathlib.Path):
for file in ET.parse(path).getroot():
for error in file:
filename = os.path.relpath(file.attrib["name"])
attrib = error.attrib
print(
f"{attrib['severity']} {filename}:{attrib['line']}:{attrib.get('column', 1)}: {SPACES.sub(' ', attrib['message'])}"
)
def parse_checkstyle() -> None:
"""
Scrape JUnit test reports for errors. We determine the location from the Lua
or Java stacktrace.
"""
print("::add-matcher::.github/matchers/checkstyle.json")
for project in PROJECT_LOCATIONS:
for path in pathlib.Path(os.path.join(project, "build/reports/checkstyle/")).glob("*.xml"):
_parse_checkstyle(path)
print("::remove-matcher owner=checkstyle::")
if __name__ == "__main__":
parse_junit()
parse_checkstyle()