CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
365 lines
13 KiB
Python
365 lines
13 KiB
Python
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
# See https://llvm.org/LICENSE.txt for license information.
|
|
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
"""Library to parse JUnit XML files and return a markdown report."""
|
|
|
|
from typing import TypedDict
|
|
import platform
|
|
|
|
from junitparser import JUnitXml, Failure
|
|
|
|
|
|
# This data structure should match the definition in llvm-zorg in
|
|
# premerge/advisor/advisor_lib.py
|
|
class FailureExplanation(TypedDict):
|
|
name: str
|
|
explained: bool
|
|
reason: str | None
|
|
|
|
|
|
SEE_BUILD_FILE_STR = "Download the build's log file to see the details."
|
|
UNRELATED_FAILURES_STR = (
|
|
"If these failures are unrelated to your changes (for example "
|
|
"tests are broken or flaky at HEAD), please open an issue at "
|
|
"https://github.com/llvm/llvm-project/issues and add the "
|
|
"`infrastructure` label."
|
|
)
|
|
# The maximum number of lines to pull from a ninja failure.
|
|
NINJA_LOG_SIZE_THRESHOLD = 500
|
|
|
|
|
|
def _parse_ninja_log(ninja_log: list[str]) -> list[tuple[str, str]]:
|
|
"""Parses an individual ninja log."""
|
|
failures = []
|
|
index = 0
|
|
while index < len(ninja_log):
|
|
while index < len(ninja_log) and not ninja_log[index].startswith("FAILED:"):
|
|
index += 1
|
|
if index == len(ninja_log):
|
|
# We hit the end of the log without finding a build failure, go to
|
|
# the next log.
|
|
return failures
|
|
# If we are doing a build with LLVM_ENABLE_RUNTIMES, we can have nested
|
|
# ninja invocations. The sub-ninja will print that a subcommand failed,
|
|
# and then the outer ninja will list the command that failed. We should
|
|
# ignore the outer failure.
|
|
if ninja_log[index - 1].startswith("ninja: build stopped:"):
|
|
index += 1
|
|
continue
|
|
# We are trying to parse cases like the following:
|
|
#
|
|
# [4/5] test/4.stamp
|
|
# FAILED: touch test/4.stamp
|
|
# touch test/4.stamp
|
|
#
|
|
# index will point to the line that starts with Failed:. The progress
|
|
# indicator is sometimes the line before this ([4/5] test/4.stamp) and
|
|
# will contain a pretty printed version of the target being built
|
|
# (test/4.stamp) when accurate. We instead parse the failed line rather
|
|
# than the progress indicator as the progress indicator may not be
|
|
# aligned with the failure.
|
|
failing_action = ninja_log[index].split("FAILED: ")[1]
|
|
failure_log = []
|
|
|
|
# Parse the lines above the FAILED: string if the line does not come
|
|
# immediately after a progress indicator to ensure that we capture the
|
|
# entire failure message.
|
|
if not ninja_log[index - 1].startswith("["):
|
|
before_index = index - 1
|
|
while before_index > 0 and not ninja_log[before_index].startswith("["):
|
|
failure_log.append(ninja_log[before_index])
|
|
before_index = before_index - 1
|
|
failure_log.reverse()
|
|
|
|
# Parse the failure information, which comes after the FAILED: tag.
|
|
while (
|
|
index < len(ninja_log)
|
|
and not ninja_log[index].startswith("[")
|
|
and not ninja_log[index].startswith("ninja: build stopped:")
|
|
and len(failure_log) < NINJA_LOG_SIZE_THRESHOLD
|
|
):
|
|
failure_log.append(ninja_log[index])
|
|
index += 1
|
|
failures.append((failing_action, "\n".join(failure_log)))
|
|
return failures
|
|
|
|
|
|
def find_failure_in_ninja_logs(ninja_logs: list[list[str]]) -> list[tuple[str, str]]:
|
|
"""Extracts failure messages from ninja output.
|
|
|
|
This function takes stdout/stderr from ninja in the form of a list of files
|
|
represented as a list of lines. This function then returns tuples containing
|
|
the name of the target and the error message.
|
|
|
|
Args:
|
|
ninja_logs: A list of files in the form of a list of lines representing the log
|
|
files captured from ninja.
|
|
|
|
Returns:
|
|
A list of tuples. The first string is the name of the target that failed. The
|
|
second string is the error message.
|
|
"""
|
|
failures = []
|
|
for ninja_log in ninja_logs:
|
|
log_failures = _parse_ninja_log(ninja_log)
|
|
failures.extend(log_failures)
|
|
return failures
|
|
|
|
|
|
def _format_failures(
|
|
failures: list[tuple[str, str]], failure_explanations: dict[str, FailureExplanation]
|
|
) -> list[str]:
|
|
"""Formats failures into summary views for the report."""
|
|
output = []
|
|
for build_failure in failures:
|
|
failed_action, failure_message = build_failure
|
|
failure_explanation = None
|
|
if failed_action in failure_explanations:
|
|
failure_explanation = failure_explanations[failed_action]
|
|
output.append("<details>")
|
|
if failure_explanation:
|
|
output.extend(
|
|
[
|
|
f"<summary>{failed_action} (Likely Already Failing)</summary>" "",
|
|
failure_explanation["reason"],
|
|
"",
|
|
]
|
|
)
|
|
else:
|
|
output.extend([f"<summary>{failed_action}</summary>", ""])
|
|
output.extend(
|
|
[
|
|
"```",
|
|
failure_message,
|
|
"```",
|
|
"</details>",
|
|
]
|
|
)
|
|
return output
|
|
|
|
|
|
def get_failures(junit_objects) -> dict[str, list[tuple[str, str]]]:
|
|
failures = {}
|
|
for results in junit_objects:
|
|
for testsuite in results:
|
|
for test in testsuite:
|
|
if (
|
|
not test.is_passed
|
|
and test.result
|
|
and isinstance(test.result[0], Failure)
|
|
):
|
|
if failures.get(testsuite.name) is None:
|
|
failures[testsuite.name] = []
|
|
failures[testsuite.name].append(
|
|
(test.classname + "/" + test.name, test.result[0].text)
|
|
)
|
|
return failures
|
|
|
|
|
|
def are_all_failures_explained(
|
|
failures: list[tuple[str, str]], failure_explanations: dict[str, FailureExplanation]
|
|
) -> bool:
|
|
for failed_action, _ in failures:
|
|
if failed_action not in failure_explanations:
|
|
return False
|
|
else:
|
|
assert failure_explanations[failed_action]["explained"]
|
|
return True
|
|
|
|
|
|
# Set size_limit to limit the byte size of the report. The default is 1MB as this
|
|
# is the most that can be put into an annotation. If the generated report exceeds
|
|
# this limit and failures are listed, it will be generated again without failures
|
|
# listed. This minimal report will always fit into an annotation.
|
|
# If include failures is False, total number of test will be reported but their names
|
|
# and output will not be.
|
|
def generate_report(
|
|
title,
|
|
return_code,
|
|
junit_objects,
|
|
ninja_logs: list[list[str]],
|
|
size_limit=1024 * 1024,
|
|
list_failures=True,
|
|
failure_explanations_list: list[FailureExplanation] = [],
|
|
) -> tuple[str, bool]:
|
|
failures = get_failures(junit_objects)
|
|
tests_run = 0
|
|
tests_skipped = 0
|
|
tests_failed = 0
|
|
|
|
failure_explanations: dict[str, FailureExplanation] = {}
|
|
for failure_explanation in failure_explanations_list:
|
|
if not failure_explanation["explained"]:
|
|
continue
|
|
failure_explanations[failure_explanation["name"]] = failure_explanation
|
|
all_failures_explained = True
|
|
if failures:
|
|
for _, failures_list in failures.items():
|
|
all_failures_explained &= are_all_failures_explained(
|
|
failures_list, failure_explanations
|
|
)
|
|
|
|
for results in junit_objects:
|
|
for testsuite in results:
|
|
tests_run += testsuite.tests
|
|
tests_skipped += testsuite.skipped
|
|
tests_failed += testsuite.failures
|
|
|
|
report = [f"# {title}", ""]
|
|
|
|
if tests_run == 0:
|
|
if return_code == 0:
|
|
report.extend(
|
|
[
|
|
":white_check_mark: The build succeeded and no tests ran. "
|
|
"This is not expected and likely means something was "
|
|
"configured incorrectly. If you suspect an infrastructure "
|
|
"issue, please open an issue at "
|
|
"https://github.com/llvm/llvm-project/issues and attach the "
|
|
"infrastructure label."
|
|
]
|
|
)
|
|
else:
|
|
ninja_failures = find_failure_in_ninja_logs(ninja_logs)
|
|
all_failures_explained &= are_all_failures_explained(
|
|
ninja_failures, failure_explanations
|
|
)
|
|
if not ninja_failures:
|
|
all_failures_explained = False
|
|
report.extend(
|
|
[
|
|
"The build failed before running any tests. Detailed "
|
|
"information about the build failure could not be "
|
|
"automatically obtained.",
|
|
"",
|
|
SEE_BUILD_FILE_STR,
|
|
"",
|
|
UNRELATED_FAILURES_STR,
|
|
]
|
|
)
|
|
else:
|
|
report.extend(
|
|
[
|
|
"The build failed before running any tests. Click on a "
|
|
"failure below to see the details.",
|
|
"",
|
|
]
|
|
)
|
|
report.extend(_format_failures(ninja_failures, failure_explanations))
|
|
report.extend(
|
|
[
|
|
"",
|
|
UNRELATED_FAILURES_STR,
|
|
]
|
|
)
|
|
return ("\n".join(report), all_failures_explained)
|
|
|
|
tests_passed = tests_run - tests_skipped - tests_failed
|
|
|
|
def plural(num_tests):
|
|
return "test" if num_tests == 1 else "tests"
|
|
|
|
if tests_passed:
|
|
report.append(f"* {tests_passed} {plural(tests_passed)} passed")
|
|
if tests_skipped:
|
|
report.append(f"* {tests_skipped} {plural(tests_skipped)} skipped")
|
|
if tests_failed:
|
|
report.append(f"* {tests_failed} {plural(tests_failed)} failed")
|
|
|
|
if not list_failures:
|
|
report.extend(
|
|
[
|
|
"",
|
|
"Failed tests and their output was too large to report. "
|
|
+ SEE_BUILD_FILE_STR,
|
|
]
|
|
)
|
|
elif failures:
|
|
report.extend(
|
|
["", "## Failed Tests", "(click on a test name to see its output)"]
|
|
)
|
|
|
|
for testsuite_name, failures in failures.items():
|
|
report.extend(["", f"### {testsuite_name}"])
|
|
report.extend(_format_failures(failures, failure_explanations))
|
|
elif return_code != 0:
|
|
# No tests failed but the build was in a failed state. Bring this to the user's
|
|
# attention.
|
|
ninja_failures = find_failure_in_ninja_logs(ninja_logs)
|
|
if not ninja_failures:
|
|
all_failures_explained = False
|
|
report.extend(
|
|
[
|
|
"",
|
|
"All executed tests passed, but another part of the build **failed**. "
|
|
"Information about the build failure could not be automatically "
|
|
"obtained.",
|
|
"",
|
|
SEE_BUILD_FILE_STR,
|
|
]
|
|
)
|
|
else:
|
|
all_failures_explained &= are_all_failures_explained(
|
|
ninja_failures, failure_explanations
|
|
)
|
|
report.extend(
|
|
[
|
|
"",
|
|
"All executed tests passed, but another part of the build **failed**. Click on "
|
|
"a failure below to see the details.",
|
|
"",
|
|
]
|
|
)
|
|
report.extend(_format_failures(ninja_failures, failure_explanations))
|
|
else:
|
|
report.extend(
|
|
["", ":white_check_mark: The build succeeded and all tests passed."]
|
|
)
|
|
|
|
if failures or return_code != 0:
|
|
report.extend(["", UNRELATED_FAILURES_STR])
|
|
|
|
report = "\n".join(report)
|
|
if len(report.encode("utf-8")) > size_limit:
|
|
return generate_report(
|
|
title,
|
|
return_code,
|
|
junit_objects,
|
|
ninja_logs,
|
|
size_limit,
|
|
list_failures=False,
|
|
)
|
|
|
|
return (report, all_failures_explained)
|
|
|
|
|
|
def load_info_from_files(build_log_files):
|
|
junit_files = [
|
|
junit_file for junit_file in build_log_files if junit_file.endswith(".xml")
|
|
]
|
|
ninja_log_files = [
|
|
ninja_log for ninja_log in build_log_files if ninja_log.endswith(".log")
|
|
]
|
|
ninja_logs = []
|
|
for ninja_log_file in ninja_log_files:
|
|
with open(ninja_log_file, "r") as ninja_log_file_handle:
|
|
ninja_logs.append(
|
|
[log_line.strip() for log_line in ninja_log_file_handle.readlines()]
|
|
)
|
|
return [JUnitXml.fromfile(p) for p in junit_files], ninja_logs
|
|
|
|
|
|
def generate_report_from_files(title, return_code, build_log_files):
|
|
junit_objects, ninja_logs = load_info_from_files(build_log_files)
|
|
return generate_report(title, return_code, junit_objects, ninja_logs)
|
|
|
|
|
|
def compute_platform_title() -> str:
|
|
logo = ":window:" if platform.system() == "Windows" else ":penguin:"
|
|
# On Linux the machine value is x86_64 on Windows it is AMD64.
|
|
if platform.machine() == "x86_64" or platform.machine() == "AMD64":
|
|
arch = "x64"
|
|
else:
|
|
arch = platform.machine()
|
|
return f"{logo} {platform.system()} {arch} Test Results"
|