1 # Script to parse many JUnit XML result files and send a report to the buildkite
2 # agent as an annotation.
4 # To run the unittests:
5 # python3 -m unittest discover -p generate_test_report.py
10 from io
import StringIO
11 from junitparser
import JUnitXml
, Failure
12 from textwrap
import dedent
15 def junit_from_xml(xml
):
16 return JUnitXml
.fromfile(StringIO(xml
))
19 class TestReports(unittest
.TestCase
):
20 def test_title_only(self
):
21 self
.assertEqual(_generate_report("Foo", []), ("", None))
23 def test_no_tests_in_testsuite(self
):
31 <?xml version="1.0" encoding="UTF-8"?>
32 <testsuites time="0.00">
33 <testsuite name="Empty" tests="0" failures="0" skipped="0" time="0.00">
43 def test_no_failures(self
):
51 <?xml version="1.0" encoding="UTF-8"?>
52 <testsuites time="0.00">
53 <testsuite name="Passed" tests="1" failures="0" skipped="0" time="0.00">
54 <testcase classname="Bar/test_1" name="test_1" time="0.00"/>
72 def test_report_single_file_single_testsuite(self
):
80 <?xml version="1.0" encoding="UTF-8"?>
81 <testsuites time="8.89">
82 <testsuite name="Bar" tests="4" failures="2" skipped="1" time="410.63">
83 <testcase classname="Bar/test_1" name="test_1" time="0.02"/>
84 <testcase classname="Bar/test_2" name="test_2" time="0.02">
85 <skipped message="Reason"/>
87 <testcase classname="Bar/test_3" name="test_3" time="0.02">
88 <failure><![CDATA[Output goes here]]></failure>
90 <testcase classname="Bar/test_4" name="test_4" time="0.02">
91 <failure><![CDATA[Other output goes here]]></failure>
109 (click to see output)
113 <summary>Bar/test_3/test_3</summary>
120 <summary>Bar/test_4/test_4</summary>
123 Other output goes here
131 MULTI_SUITE_OUTPUT
= (
141 (click to see output)
145 <summary>ABC/test_2/test_2</summary>
148 ABC/test_2 output goes here
154 <summary>DEF/test_2/test_2</summary>
157 DEF/test_2 output goes here
164 def test_report_single_file_multiple_testsuites(self
):
172 <?xml version="1.0" encoding="UTF-8"?>
173 <testsuites time="8.89">
174 <testsuite name="ABC" tests="2" failures="1" skipped="0" time="410.63">
175 <testcase classname="ABC/test_1" name="test_1" time="0.02"/>
176 <testcase classname="ABC/test_2" name="test_2" time="0.02">
177 <failure><![CDATA[ABC/test_2 output goes here]]></failure>
180 <testsuite name="DEF" tests="2" failures="1" skipped="1" time="410.63">
181 <testcase classname="DEF/test_1" name="test_1" time="0.02">
182 <skipped message="reason"/>
184 <testcase classname="DEF/test_2" name="test_2" time="0.02">
185 <failure><![CDATA[DEF/test_2 output goes here]]></failure>
193 self
.MULTI_SUITE_OUTPUT
,
196 def test_report_multiple_files_multiple_testsuites(self
):
204 <?xml version="1.0" encoding="UTF-8"?>
205 <testsuites time="8.89">
206 <testsuite name="ABC" tests="2" failures="1" skipped="0" time="410.63">
207 <testcase classname="ABC/test_1" name="test_1" time="0.02"/>
208 <testcase classname="ABC/test_2" name="test_2" time="0.02">
209 <failure><![CDATA[ABC/test_2 output goes here]]></failure>
218 <?xml version="1.0" encoding="UTF-8"?>
219 <testsuites time="8.89">
220 <testsuite name="DEF" tests="2" failures="1" skipped="1" time="410.63">
221 <testcase classname="DEF/test_1" name="test_1" time="0.02">
222 <skipped message="reason"/>
224 <testcase classname="DEF/test_2" name="test_2" time="0.02">
225 <failure><![CDATA[DEF/test_2 output goes here]]></failure>
233 self
.MULTI_SUITE_OUTPUT
,
236 def test_report_dont_list_failures(self
):
244 <?xml version="1.0" encoding="UTF-8"?>
245 <testsuites time="0.02">
246 <testsuite name="Bar" tests="1" failures="1" skipped="0" time="0.02">
247 <testcase classname="Bar/test_1" name="test_1" time="0.02">
248 <failure><![CDATA[Output goes here]]></failure>
264 Failed tests and their output was too large to report. Download the build's log file to see the details."""
270 def test_report_size_limit(self
):
278 <?xml version="1.0" encoding="UTF-8"?>
279 <testsuites time="0.02">
280 <testsuite name="Bar" tests="1" failures="1" skipped="0" time="0.02">
281 <testcase classname="Bar/test_1" name="test_1" time="0.02">
282 <failure><![CDATA[Some long output goes here...]]></failure>
298 Failed tests and their output was too large to report. Download the build's log file to see the details."""
305 # Set size_limit to limit the byte size of the report. The default is 1MB as this
306 # is the most that can be put into an annotation. If the generated report exceeds
307 # this limit and failures are listed, it will be generated again without failures
308 # listed. This minimal report will always fit into an annotation.
309 # If include failures is False, total number of test will be reported but their names
310 # and output will not be.
311 def _generate_report(title
, junit_objects
, size_limit
=1024 * 1024, list_failures
=True):
312 if not junit_objects
:
313 return ("", "success")
320 for results
in junit_objects
:
321 for testsuite
in results
:
322 tests_run
+= testsuite
.tests
323 tests_skipped
+= testsuite
.skipped
324 tests_failed
+= testsuite
.failures
326 for test
in testsuite
:
330 and isinstance(test
.result
[0], Failure
)
332 if failures
.get(testsuite
.name
) is None:
333 failures
[testsuite
.name
] = []
334 failures
[testsuite
.name
].append(
335 (test
.classname
+ "/" + test
.name
, test
.result
[0].text
)
341 style
= "error" if tests_failed
else "success"
342 report
= [f
"# {title}", ""]
344 tests_passed
= tests_run
- tests_skipped
- tests_failed
346 def plural(num_tests
):
347 return "test" if num_tests
== 1 else "tests"
350 report
.append(f
"* {tests_passed} {plural(tests_passed)} passed")
352 report
.append(f
"* {tests_skipped} {plural(tests_skipped)} skipped")
354 report
.append(f
"* {tests_failed} {plural(tests_failed)} failed")
356 if not list_failures
:
360 "Failed tests and their output was too large to report. "
361 "Download the build's log file to see the details.",
365 report
.extend(["", "## Failed Tests", "(click to see output)"])
367 for testsuite_name
, failures
in failures
.items():
368 report
.extend(["", f
"### {testsuite_name}"])
369 for name
, output
in failures
:
373 f
"<summary>{name}</summary>",
382 report
= "\n".join(report
)
383 if len(report
.encode("utf-8")) > size_limit
:
384 return _generate_report(title
, junit_objects
, size_limit
, list_failures
=False)
389 def generate_report(title
, junit_files
):
390 return _generate_report(title
, [JUnitXml
.fromfile(p
) for p
in junit_files
])
393 if __name__
== "__main__":
394 parser
= argparse
.ArgumentParser()
396 "title", help="Title of the test report, without Markdown formatting."
398 parser
.add_argument("context", help="Annotation context to write to.")
399 parser
.add_argument("junit_files", help="Paths to JUnit report files.", nargs
="*")
400 args
= parser
.parse_args()
402 report
, style
= generate_report(args
.title
, args
.junit_files
)
405 p
= subprocess
.Popen(
414 stdin
=subprocess
.PIPE
,
415 stderr
=subprocess
.PIPE
,
416 universal_newlines
=True,
419 # The report can be larger than the buffer for command arguments so we send
420 # it over stdin instead.
421 _
, err
= p
.communicate(input=report
)
423 raise RuntimeError(f
"Failed to send report to buildkite-agent:\n{err}")