[MemProf] Templatize CallStackRadixTreeBuilder (NFC) (#117014)
[llvm-project.git] / .ci / generate_test_report.py
blob237f45e6f08e06376af3ca310bb7fc4409cb32cd
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
7 import argparse
8 import subprocess
9 import unittest
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):
24 self.assertEqual(
25 _generate_report(
26 "Foo",
28 junit_from_xml(
29 dedent(
30 """\
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">
34 </testsuite>
35 </testsuites>"""
40 ("", None),
43 def test_no_failures(self):
44 self.assertEqual(
45 _generate_report(
46 "Foo",
48 junit_from_xml(
49 dedent(
50 """\
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"/>
55 </testsuite>
56 </testsuites>"""
62 dedent(
63 """\
64 # Foo
66 * 1 test passed"""
68 "success",
72 def test_report_single_file_single_testsuite(self):
73 self.assertEqual(
74 _generate_report(
75 "Foo",
77 junit_from_xml(
78 dedent(
79 """\
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"/>
86 </testcase>
87 <testcase classname="Bar/test_3" name="test_3" time="0.02">
88 <failure><![CDATA[Output goes here]]></failure>
89 </testcase>
90 <testcase classname="Bar/test_4" name="test_4" time="0.02">
91 <failure><![CDATA[Other output goes here]]></failure>
92 </testcase>
93 </testsuite>
94 </testsuites>"""
100 dedent(
101 """\
102 # Foo
104 * 1 test passed
105 * 1 test skipped
106 * 2 tests failed
108 ## Failed Tests
109 (click to see output)
111 ### Bar
112 <details>
113 <summary>Bar/test_3/test_3</summary>
116 Output goes here
118 </details>
119 <details>
120 <summary>Bar/test_4/test_4</summary>
123 Other output goes here
125 </details>"""
127 "error",
131 MULTI_SUITE_OUTPUT = (
132 dedent(
133 """\
134 # ABC and DEF
136 * 1 test passed
137 * 1 test skipped
138 * 2 tests failed
140 ## Failed Tests
141 (click to see output)
143 ### ABC
144 <details>
145 <summary>ABC/test_2/test_2</summary>
148 ABC/test_2 output goes here
150 </details>
152 ### DEF
153 <details>
154 <summary>DEF/test_2/test_2</summary>
157 DEF/test_2 output goes here
159 </details>"""
161 "error",
164 def test_report_single_file_multiple_testsuites(self):
165 self.assertEqual(
166 _generate_report(
167 "ABC and DEF",
169 junit_from_xml(
170 dedent(
171 """\
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>
178 </testcase>
179 </testsuite>
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"/>
183 </testcase>
184 <testcase classname="DEF/test_2" name="test_2" time="0.02">
185 <failure><![CDATA[DEF/test_2 output goes here]]></failure>
186 </testcase>
187 </testsuite>
188 </testsuites>"""
193 self.MULTI_SUITE_OUTPUT,
196 def test_report_multiple_files_multiple_testsuites(self):
197 self.assertEqual(
198 _generate_report(
199 "ABC and DEF",
201 junit_from_xml(
202 dedent(
203 """\
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>
210 </testcase>
211 </testsuite>
212 </testsuites>"""
215 junit_from_xml(
216 dedent(
217 """\
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"/>
223 </testcase>
224 <testcase classname="DEF/test_2" name="test_2" time="0.02">
225 <failure><![CDATA[DEF/test_2 output goes here]]></failure>
226 </testcase>
227 </testsuite>
228 </testsuites>"""
233 self.MULTI_SUITE_OUTPUT,
236 def test_report_dont_list_failures(self):
237 self.assertEqual(
238 _generate_report(
239 "Foo",
241 junit_from_xml(
242 dedent(
243 """\
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>
249 </testcase>
250 </testsuite>
251 </testsuites>"""
255 list_failures=False,
258 dedent(
259 """\
260 # Foo
262 * 1 test failed
264 Failed tests and their output was too large to report. Download the build's log file to see the details."""
266 "error",
270 def test_report_size_limit(self):
271 self.assertEqual(
272 _generate_report(
273 "Foo",
275 junit_from_xml(
276 dedent(
277 """\
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>
283 </testcase>
284 </testsuite>
285 </testsuites>"""
289 size_limit=128,
292 dedent(
293 """\
294 # Foo
296 * 1 test failed
298 Failed tests and their output was too large to report. Download the build's log file to see the details."""
300 "error",
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")
315 failures = {}
316 tests_run = 0
317 tests_skipped = 0
318 tests_failed = 0
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:
327 if (
328 not test.is_passed
329 and test.result
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)
338 if not tests_run:
339 return ("", style)
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"
349 if tests_passed:
350 report.append(f"* {tests_passed} {plural(tests_passed)} passed")
351 if tests_skipped:
352 report.append(f"* {tests_skipped} {plural(tests_skipped)} skipped")
353 if tests_failed:
354 report.append(f"* {tests_failed} {plural(tests_failed)} failed")
356 if not list_failures:
357 report.extend(
360 "Failed tests and their output was too large to report. "
361 "Download the build's log file to see the details.",
364 elif failures:
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:
370 report.extend(
372 "<details>",
373 f"<summary>{name}</summary>",
375 "```",
376 output,
377 "```",
378 "</details>",
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)
386 return report, style
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()
395 parser.add_argument(
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)
404 if report:
405 p = subprocess.Popen(
407 "buildkite-agent",
408 "annotate",
409 "--context",
410 args.context,
411 "--style",
412 style,
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)
422 if p.returncode:
423 raise RuntimeError(f"Failed to send report to buildkite-agent:\n{err}")