Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .ci/generate_test_report_github.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("return_code", help="The build's return code.", type=int)
parser.add_argument("junit_files", help="Paths to JUnit report files.", nargs="*")
parser.add_argument(
"build_test_logs", help="Paths to JUnit report files and ninja logs.", nargs="*"
)
args = parser.parse_args()

report = generate_test_report_lib.generate_report_from_files(
PLATFORM_TITLES[platform.system()], args.return_code, args.junit_files
PLATFORM_TITLES[platform.system()], args.return_code, args.build_test_logs
)

print(report)
158 changes: 137 additions & 21 deletions .ci/generate_test_report_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,78 @@
"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
# index will point to the line that starts with Failed:. The progress
# indicator is the line before this and contains a pretty printed version
# of the target being built. We use this and remove the progress information
# to get a succinct name for the target.
failing_action = ninja_log[index - 1].split("] ")[1]
failure_log = []
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_ninja_failures(ninja_failures: list[tuple[str, str]]) -> list[str]:
"""Formats ninja failures into summary views for the report."""
output = []
for build_failure in ninja_failures:
failed_action, failure_message = build_failure
output.extend(
[
"<details>",
f"<summary>{failed_action}</summary>",
"",
"```",
failure_message,
"```",
"</details>",
]
)
return output


# Set size_limit to limit the byte size of the report. The default is 1MB as this
Expand All @@ -24,6 +96,7 @@ def generate_report(
title,
return_code,
junit_objects,
ninja_logs: list[list[str]],
size_limit=1024 * 1024,
list_failures=True,
):
Expand Down Expand Up @@ -61,15 +134,34 @@ def generate_report(
]
)
else:
report.extend(
[
"The build failed before running any tests.",
"",
SEE_BUILD_FILE_STR,
"",
UNRELATED_FAILURES_STR,
]
)
ninja_failures = find_failure_in_ninja_logs(ninja_logs)
if not ninja_failures:
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_ninja_failures(ninja_failures))
report.extend(
[
"",
UNRELATED_FAILURES_STR,
]
)
return "\n".join(report)

tests_passed = tests_run - tests_skipped - tests_failed
Expand Down Expand Up @@ -114,14 +206,28 @@ def plural(num_tests):
elif return_code != 0:
# No tests failed but the build was in a failed state. Bring this to the user's
# attention.
report.extend(
[
"",
"All tests passed but another part of the build **failed**.",
"",
SEE_BUILD_FILE_STR,
]
)
ninja_failures = find_failure_in_ninja_logs(ninja_logs)
if not ninja_failures:
report.extend(
[
"",
"All tests passed but another part of the build **failed**. "
"Information about the build failure could not be automatically "
"obtained.",
"",
SEE_BUILD_FILE_STR,
]
)
else:
report.extend(
[
"",
"All tests passed but another part of the build **failed**. Click on "
"a failure below to see the details.",
"",
]
)
report.extend(_format_ninja_failures(ninja_failures))

if failures or return_code != 0:
report.extend(["", UNRELATED_FAILURES_STR])
Expand All @@ -139,9 +245,19 @@ def plural(num_tests):
return report


def generate_report_from_files(title, return_code, junit_files):
def generate_report_from_files(title, return_code, 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 generate_report(
title,
return_code,
[JUnitXml.fromfile(p) for p in junit_files],
title, return_code, [JUnitXml.fromfile(p) for p in junit_files], ninja_logs
)
Loading
Loading