Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
194 changes: 192 additions & 2 deletions .ci/all_requirements.txt

Large diffs are not rendered by default.

15 changes: 3 additions & 12 deletions .ci/generate_test_report_github.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,10 @@
"""Script to generate a build report for Github."""

import argparse
import platform

import generate_test_report_lib


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"


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("return_code", help="The build's return code.", type=int)
Expand All @@ -28,7 +17,9 @@ def compute_platform_title() -> str:
args = parser.parse_args()

report = generate_test_report_lib.generate_report_from_files(
compute_platform_title(), args.return_code, args.build_test_logs
generate_test_report_lib.compute_platform_title(),
args.return_code,
args.build_test_logs,
)

print(report)
71 changes: 51 additions & 20 deletions .ci/generate_test_report_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,20 @@
# 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 "
Expand Down Expand Up @@ -82,16 +94,29 @@ def find_failure_in_ninja_logs(ninja_logs: list[list[str]]) -> list[tuple[str, s
return failures


def _format_ninja_failures(ninja_failures: list[tuple[str, str]]) -> list[str]:
"""Formats ninja failures into summary views for the report."""
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 ninja_failures:
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(
[
"<details>",
f"<summary>{failed_action}</summary>",
"",
"```",
failure_message,
"```",
Expand Down Expand Up @@ -132,12 +157,19 @@ def generate_report(
ninja_logs: list[list[str]],
size_limit=1024 * 1024,
list_failures=True,
failure_explanations_list: list[FailureExplanation] = [],
):
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

for results in junit_objects:
for testsuite in results:
tests_run += testsuite.tests
Expand Down Expand Up @@ -176,7 +208,7 @@ def generate_report(
"",
]
)
report.extend(_format_ninja_failures(ninja_failures))
report.extend(_format_failures(ninja_failures, failure_explanations))
report.extend(
[
"",
Expand Down Expand Up @@ -212,18 +244,7 @@ def plural(num_tests):

for testsuite_name, failures in failures.items():
report.extend(["", f"### {testsuite_name}"])
for name, output in failures:
report.extend(
[
"<details>",
f"<summary>{name}</summary>",
"",
"```",
output,
"```",
"</details>",
]
)
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.
Expand All @@ -248,7 +269,7 @@ def plural(num_tests):
"",
]
)
report.extend(_format_ninja_failures(ninja_failures))
report.extend(_format_failures(ninja_failures, failure_explanations))

if failures or return_code != 0:
report.extend(["", UNRELATED_FAILURES_STR])
Expand Down Expand Up @@ -285,3 +306,13 @@ def load_info_from_files(build_log_files):
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"
101 changes: 101 additions & 0 deletions .ci/generate_test_report_lib_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,107 @@ def test_report_size_limit(self):
),
)

def test_report_ninja_explanation(self):
self.assertEqual(
generate_test_report_lib.generate_report(
"Foo",
1,
[],
[
[
"[1/5] test/1.stamp",
"[2/5] test/2.stamp",
"[3/5] test/3.stamp",
"[4/5] test/4.stamp",
"FAILED: test/4.stamp",
"touch test/4.stamp",
"Half Moon Bay.",
"[5/5] test/5.stamp",
]
],
failure_explanations_list=[
{
"name": "test/4.stamp",
"explained": True,
"reason": "Failing at head",
}
],
),
dedent(
"""\
# Foo

The build failed before running any tests. Click on a failure below to see the details.

<details>
<summary>test/4.stamp (Likely Already Failing)</summary>
Failing at head

```
FAILED: test/4.stamp
touch test/4.stamp
Half Moon Bay.
```
</details>

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."""
),
)

def test_report_test_failure_explanation(self):
self.assertEqual(
generate_test_report_lib.generate_report(
"Foo",
1,
[
junit_from_xml(
dedent(
"""\
<?xml version="1.0" encoding="UTF-8"?>
<testsuites time="8.89">
<testsuite name="Bar" tests="1" failures="1" skipped="0" time="410.63">
<testcase classname="Bar/test_3" name="test_3" time="0.02">
<failure><![CDATA[Error! Expected Big Sur to be next to the ocean.]]></failure>
</testcase>
</testsuite>
</testsuites>"""
)
)
],
[],
failure_explanations_list=[
{
"name": "Bar/test_3/test_3",
"explained": True,
"reason": "Big Sur is next to the Pacific.",
}
],
),
(
dedent(
"""\
# Foo

* 1 test failed

## Failed Tests
(click on a test name to see its output)

### Bar
<details>
<summary>Bar/test_3/test_3 (Likely Already Failing)</summary>
Big Sur is next to the Pacific.

```
Error! Expected Big Sur to be next to the ocean.
```
</details>

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."""
)
),
)

def test_generate_report_end_to_end(self):
with tempfile.TemporaryDirectory() as temp_dir:
junit_xml_file = os.path.join(temp_dir, "junit.xml")
Expand Down
68 changes: 65 additions & 3 deletions .ci/premerge_advisor_explain.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,58 @@
"""Script for getting explanations from the premerge advisor."""

import argparse
import os
import platform
import sys
import json

import requests
import github
import github.PullRequest

import generate_test_report_lib

PREMERGE_ADVISOR_URL = (
"http://premerge-advisor.premerge-advisor.svc.cluster.local:5000/explain"
)
COMMENT_TAG = "<!--PREMERGE ADVISOR COMMENT: {platform}-->"


def main(commit_sha: str, build_log_files: list[str]):
def get_comment_id(platform: str, pr: github.PullRequest.PullRequest) -> int | None:
platform_comment_tag = COMMENT_TAG.format(platform=platform)
for comment in pr.as_issue().get_comments():
if platform_comment_tag in comment.body:
return comment.id
return None


def get_comment(
github_token: str,
pr_number: int,
body: str,
) -> dict[str, str]:
repo = github.Github(github_token).get_repo("llvm/llvm-project")
pr = repo.get_issue(pr_number).as_pull_request()
comment = {"body": body}
comment_id = get_comment_id(platform.system(), pr)
if comment_id:
comment["id"] = comment_id


def main(
commit_sha: str,
build_log_files: list[str],
github_token: str,
pr_number: int,
return_code: int,
):
if return_code == 0:
with open("comment", "w") as comment_file_handle:
comment = get_comment(
":white_check_mark: With the latest revision this PR passed "
"the premerge checks."
)
if comment["id"]:
json.dump([comment], comment_file_handle)
junit_objects, ninja_logs = generate_test_report_lib.load_info_from_files(
build_log_files
)
Expand Down Expand Up @@ -45,13 +83,31 @@ def main(commit_sha: str, build_log_files: list[str]):
)
if advisor_response.status_code == 200:
print(advisor_response.json())
comments = [
get_comment(
github_token,
pr_number,
generate_test_report_lib.generate_report(
generate_test_report_lib.compute_platform_title(),
return_code,
junit_objects,
ninja_logs,
failure_explanations_list=advisor_response.json(),
),
)
]
with open("comment", "w") as comment_file_handle:
json.dump(comments, comment_file_handle)
else:
print(advisor_response.reason)


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("commit_sha", help="The base commit SHA for the test.")
parser.add_argument("return_code", help="The build's return code", type=int)
parser.add_argument("github_token", help="Github authentication token", type=str)
parser.add_argument("pr_number", help="The PR number", type=int)
parser.add_argument(
"build_log_files", help="Paths to JUnit report files and ninja logs.", nargs="*"
)
Expand All @@ -62,4 +118,10 @@ def main(commit_sha: str, build_log_files: list[str]):
if platform.machine() == "arm64":
sys.exit(0)

main(args.commit_sha, args.build_log_files)
main(
args.commit_sha,
args.build_log_files,
args.github_token,
args.pr_number,
args.return_code,
)
1 change: 1 addition & 0 deletions .ci/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
junitparser==3.2.0
google-cloud-storage==3.3.0
PyGithub==2.8.1
Loading
Loading