-
Notifications
You must be signed in to change notification settings - Fork 14.4k
[ci] New script to generate test reports as Buildkite Annotations #113447
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,328 @@ | ||
# Script to parse many JUnit XML result files and send a report to the buildkite | ||
# agent as an annotation. | ||
# | ||
# To run the unittests: | ||
# python3 -m unittest discover -p generate_test_report.py | ||
|
||
import argparse | ||
import unittest | ||
from io import StringIO | ||
from junitparser import JUnitXml, Failure | ||
DavidSpickett marked this conversation as resolved.
Show resolved
Hide resolved
|
||
from textwrap import dedent | ||
from subprocess import check_call | ||
|
||
|
||
def junit_from_xml(xml): | ||
return JUnitXml.fromfile(StringIO(xml)) | ||
|
||
|
||
class TestReports(unittest.TestCase): | ||
DavidSpickett marked this conversation as resolved.
Show resolved
Hide resolved
|
||
def test_title_only(self): | ||
self.assertEqual(_generate_report("Foo", []), ("", None)) | ||
|
||
def test_no_tests_in_testsuite(self): | ||
self.assertEqual( | ||
_generate_report( | ||
"Foo", | ||
[ | ||
junit_from_xml( | ||
dedent( | ||
"""\ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<testsuites time="0.00"> | ||
<testsuite name="Empty" tests="0" failures="0" skipped="0" time="0.00"> | ||
</testsuite> | ||
</testsuites>""" | ||
) | ||
) | ||
], | ||
), | ||
("", None), | ||
) | ||
|
||
def test_no_failures(self): | ||
self.assertEqual( | ||
_generate_report( | ||
"Foo", | ||
[ | ||
junit_from_xml( | ||
dedent( | ||
"""\ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<testsuites time="0.00"> | ||
<testsuite name="Passed" tests="1" failures="0" skipped="0" time="0.00"> | ||
<testcase classname="Bar/test_1" name="test_1" time="0.00"/> | ||
</testsuite> | ||
</testsuites>""" | ||
) | ||
) | ||
], | ||
), | ||
( | ||
dedent( | ||
"""\ | ||
# Foo | ||
* 1 test passed""" | ||
), | ||
"success", | ||
), | ||
) | ||
|
||
def test_report_single_file_single_testsuite(self): | ||
self.assertEqual( | ||
_generate_report( | ||
"Foo", | ||
[ | ||
junit_from_xml( | ||
dedent( | ||
"""\ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<testsuites time="8.89"> | ||
<testsuite name="Bar" tests="4" failures="2" skipped="1" time="410.63"> | ||
<testcase classname="Bar/test_1" name="test_1" time="0.02"/> | ||
<testcase classname="Bar/test_2" name="test_2" time="0.02"> | ||
<skipped message="Reason"/> | ||
</testcase> | ||
<testcase classname="Bar/test_3" name="test_3" time="0.02"> | ||
<failure><![CDATA[Output goes here]]></failure> | ||
</testcase> | ||
<testcase classname="Bar/test_4" name="test_4" time="0.02"> | ||
<failure><![CDATA[Other output goes here]]></failure> | ||
</testcase> | ||
</testsuite> | ||
</testsuites>""" | ||
) | ||
) | ||
], | ||
), | ||
( | ||
dedent( | ||
"""\ | ||
# Foo | ||
* 1 test passed | ||
* 1 test skipped | ||
* 2 tests failed | ||
## Failed tests | ||
(click to see output) | ||
### Bar | ||
<details> | ||
<summary>Bar/test_3/test_3</summary> | ||
``` | ||
Output goes here | ||
``` | ||
</details> | ||
<details> | ||
<summary>Bar/test_4/test_4</summary> | ||
``` | ||
Other output goes here | ||
``` | ||
</details>""" | ||
), | ||
"error", | ||
), | ||
) | ||
|
||
MULTI_SUITE_OUTPUT = ( | ||
dedent( | ||
"""\ | ||
# ABC and DEF | ||
* 1 test passed | ||
* 1 test skipped | ||
* 2 tests failed | ||
## Failed tests | ||
(click to see output) | ||
### ABC | ||
<details> | ||
<summary>ABC/test_2/test_2</summary> | ||
``` | ||
ABC/test_2 output goes here | ||
``` | ||
</details> | ||
### DEF | ||
<details> | ||
<summary>DEF/test_2/test_2</summary> | ||
``` | ||
DEF/test_2 output goes here | ||
``` | ||
</details>""" | ||
), | ||
"error", | ||
) | ||
|
||
def test_report_single_file_multiple_testsuites(self): | ||
self.assertEqual( | ||
_generate_report( | ||
"ABC and DEF", | ||
[ | ||
junit_from_xml( | ||
dedent( | ||
"""\ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<testsuites time="8.89"> | ||
<testsuite name="ABC" tests="2" failures="1" skipped="0" time="410.63"> | ||
<testcase classname="ABC/test_1" name="test_1" time="0.02"/> | ||
<testcase classname="ABC/test_2" name="test_2" time="0.02"> | ||
<failure><![CDATA[ABC/test_2 output goes here]]></failure> | ||
</testcase> | ||
</testsuite> | ||
<testsuite name="DEF" tests="2" failures="1" skipped="1" time="410.63"> | ||
<testcase classname="DEF/test_1" name="test_1" time="0.02"> | ||
<skipped message="reason"/> | ||
</testcase> | ||
<testcase classname="DEF/test_2" name="test_2" time="0.02"> | ||
<failure><![CDATA[DEF/test_2 output goes here]]></failure> | ||
</testcase> | ||
</testsuite> | ||
</testsuites>""" | ||
) | ||
) | ||
], | ||
), | ||
self.MULTI_SUITE_OUTPUT, | ||
) | ||
|
||
def test_report_multiple_files_multiple_testsuites(self): | ||
self.assertEqual( | ||
_generate_report( | ||
"ABC and DEF", | ||
[ | ||
junit_from_xml( | ||
dedent( | ||
"""\ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<testsuites time="8.89"> | ||
<testsuite name="ABC" tests="2" failures="1" skipped="0" time="410.63"> | ||
<testcase classname="ABC/test_1" name="test_1" time="0.02"/> | ||
<testcase classname="ABC/test_2" name="test_2" time="0.02"> | ||
<failure><![CDATA[ABC/test_2 output goes here]]></failure> | ||
</testcase> | ||
</testsuite> | ||
</testsuites>""" | ||
) | ||
), | ||
junit_from_xml( | ||
dedent( | ||
"""\ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<testsuites time="8.89"> | ||
<testsuite name="DEF" tests="2" failures="1" skipped="1" time="410.63"> | ||
<testcase classname="DEF/test_1" name="test_1" time="0.02"> | ||
<skipped message="reason"/> | ||
</testcase> | ||
<testcase classname="DEF/test_2" name="test_2" time="0.02"> | ||
<failure><![CDATA[DEF/test_2 output goes here]]></failure> | ||
</testcase> | ||
</testsuite> | ||
</testsuites>""" | ||
) | ||
), | ||
], | ||
), | ||
self.MULTI_SUITE_OUTPUT, | ||
) | ||
|
||
|
||
def _generate_report(title, junit_objects): | ||
style = None | ||
|
||
if not junit_objects: | ||
return ("", style) | ||
|
||
failures = {} | ||
tests_run = 0 | ||
tests_skipped = 0 | ||
tests_failed = 0 | ||
|
||
for results in junit_objects: | ||
for testsuite in results: | ||
tests_run += testsuite.tests | ||
tests_skipped += testsuite.skipped | ||
tests_failed += testsuite.failures | ||
|
||
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) | ||
) | ||
|
||
if not tests_run: | ||
return ("", style) | ||
|
||
style = "error" if tests_failed else "success" | ||
report = [f"# {title}", ""] | ||
|
||
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 failures: | ||
report.extend(["", "## Failed tests", "(click to see output)"]) | ||
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>", | ||
] | ||
) | ||
|
||
return "\n".join(report), style | ||
|
||
|
||
def generate_report(title, junit_files): | ||
return _generate_report(title, [JUnitXml.fromfile(p) for p in junit_files]) | ||
|
||
|
||
if __name__ == "__main__": | ||
parser = argparse.ArgumentParser() | ||
parser.add_argument( | ||
"title", help="Title of the test report, without Markdown formatting." | ||
) | ||
parser.add_argument("context", help="Annotation context to write to.") | ||
parser.add_argument("junit_files", help="Paths to JUnit report files.", nargs="*") | ||
args = parser.parse_args() | ||
|
||
report, style = generate_report(args.title, args.junit_files) | ||
check_call( | ||
DavidSpickett marked this conversation as resolved.
Show resolved
Hide resolved
|
||
[ | ||
"buildkite-agent", | ||
"annotate", | ||
"--context", | ||
args.context, | ||
"--style", | ||
style, | ||
report, | ||
] | ||
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
junitparser==3.2.0 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.