xref: /llvm-project/.ci/generate_test_report.py (revision 1b199d19902a752433c397377567ff381261e94a)
1889b3c94SDavid Spickett# Script to parse many JUnit XML result files and send a report to the buildkite
2889b3c94SDavid Spickett# agent as an annotation.
3889b3c94SDavid Spickett#
4889b3c94SDavid Spickett# To run the unittests:
5889b3c94SDavid Spickett# python3 -m unittest discover -p generate_test_report.py
6889b3c94SDavid Spickett
7889b3c94SDavid Spickettimport argparse
871fd5288SDavid Spickettimport os
9889b3c94SDavid Spickettimport subprocess
10889b3c94SDavid Spickettimport unittest
11889b3c94SDavid Spickettfrom io import StringIO
12889b3c94SDavid Spickettfrom junitparser import JUnitXml, Failure
13889b3c94SDavid Spickettfrom textwrap import dedent
14889b3c94SDavid Spickett
15889b3c94SDavid Spickett
16889b3c94SDavid Spickettdef junit_from_xml(xml):
17889b3c94SDavid Spickett    return JUnitXml.fromfile(StringIO(xml))
18889b3c94SDavid Spickett
19889b3c94SDavid Spickett
20889b3c94SDavid Spickettclass TestReports(unittest.TestCase):
21889b3c94SDavid Spickett    def test_title_only(self):
22*1b199d19SDavid Spickett        self.assertEqual(_generate_report("Foo", 0, []), ("", "success"))
23889b3c94SDavid Spickett
24889b3c94SDavid Spickett    def test_no_tests_in_testsuite(self):
25889b3c94SDavid Spickett        self.assertEqual(
26889b3c94SDavid Spickett            _generate_report(
27889b3c94SDavid Spickett                "Foo",
28*1b199d19SDavid Spickett                1,
29889b3c94SDavid Spickett                [
30889b3c94SDavid Spickett                    junit_from_xml(
31889b3c94SDavid Spickett                        dedent(
32889b3c94SDavid Spickett                            """\
33889b3c94SDavid Spickett          <?xml version="1.0" encoding="UTF-8"?>
34889b3c94SDavid Spickett          <testsuites time="0.00">
35889b3c94SDavid Spickett          <testsuite name="Empty" tests="0" failures="0" skipped="0" time="0.00">
36889b3c94SDavid Spickett          </testsuite>
37889b3c94SDavid Spickett          </testsuites>"""
38889b3c94SDavid Spickett                        )
39889b3c94SDavid Spickett                    )
40889b3c94SDavid Spickett                ],
41889b3c94SDavid Spickett            ),
42889b3c94SDavid Spickett            ("", None),
43889b3c94SDavid Spickett        )
44889b3c94SDavid Spickett
45889b3c94SDavid Spickett    def test_no_failures(self):
46889b3c94SDavid Spickett        self.assertEqual(
47889b3c94SDavid Spickett            _generate_report(
48889b3c94SDavid Spickett                "Foo",
49*1b199d19SDavid Spickett                0,
50889b3c94SDavid Spickett                [
51889b3c94SDavid Spickett                    junit_from_xml(
52889b3c94SDavid Spickett                        dedent(
53889b3c94SDavid Spickett                            """\
54889b3c94SDavid Spickett          <?xml version="1.0" encoding="UTF-8"?>
55889b3c94SDavid Spickett          <testsuites time="0.00">
56889b3c94SDavid Spickett          <testsuite name="Passed" tests="1" failures="0" skipped="0" time="0.00">
57889b3c94SDavid Spickett          <testcase classname="Bar/test_1" name="test_1" time="0.00"/>
58889b3c94SDavid Spickett          </testsuite>
59889b3c94SDavid Spickett          </testsuites>"""
60889b3c94SDavid Spickett                        )
61889b3c94SDavid Spickett                    )
62889b3c94SDavid Spickett                ],
63889b3c94SDavid Spickett            ),
64889b3c94SDavid Spickett            (
65889b3c94SDavid Spickett                dedent(
66889b3c94SDavid Spickett                    """\
67889b3c94SDavid Spickett              # Foo
68889b3c94SDavid Spickett
69889b3c94SDavid Spickett              * 1 test passed"""
70889b3c94SDavid Spickett                ),
71889b3c94SDavid Spickett                "success",
72889b3c94SDavid Spickett            ),
73889b3c94SDavid Spickett        )
74889b3c94SDavid Spickett
75*1b199d19SDavid Spickett    def test_no_failures_build_failed(self):
76*1b199d19SDavid Spickett        self.assertEqual(
77*1b199d19SDavid Spickett            _generate_report(
78*1b199d19SDavid Spickett                "Foo",
79*1b199d19SDavid Spickett                1,
80*1b199d19SDavid Spickett                [
81*1b199d19SDavid Spickett                    junit_from_xml(
82*1b199d19SDavid Spickett                        dedent(
83*1b199d19SDavid Spickett                            """\
84*1b199d19SDavid Spickett          <?xml version="1.0" encoding="UTF-8"?>
85*1b199d19SDavid Spickett          <testsuites time="0.00">
86*1b199d19SDavid Spickett          <testsuite name="Passed" tests="1" failures="0" skipped="0" time="0.00">
87*1b199d19SDavid Spickett          <testcase classname="Bar/test_1" name="test_1" time="0.00"/>
88*1b199d19SDavid Spickett          </testsuite>
89*1b199d19SDavid Spickett          </testsuites>"""
90*1b199d19SDavid Spickett                        )
91*1b199d19SDavid Spickett                    )
92*1b199d19SDavid Spickett                ],
93*1b199d19SDavid Spickett                buildkite_info={
94*1b199d19SDavid Spickett                    "BUILDKITE_ORGANIZATION_SLUG": "organization_slug",
95*1b199d19SDavid Spickett                    "BUILDKITE_PIPELINE_SLUG": "pipeline_slug",
96*1b199d19SDavid Spickett                    "BUILDKITE_BUILD_NUMBER": "build_number",
97*1b199d19SDavid Spickett                    "BUILDKITE_JOB_ID": "job_id",
98*1b199d19SDavid Spickett                },
99*1b199d19SDavid Spickett            ),
100*1b199d19SDavid Spickett            (
101*1b199d19SDavid Spickett                dedent(
102*1b199d19SDavid Spickett                    """\
103*1b199d19SDavid Spickett              # Foo
104*1b199d19SDavid Spickett
105*1b199d19SDavid Spickett              * 1 test passed
106*1b199d19SDavid Spickett
107*1b199d19SDavid Spickett              All tests passed but another part of the build **failed**.
108*1b199d19SDavid Spickett
109*1b199d19SDavid Spickett              [Download](https://buildkite.com/organizations/organization_slug/pipelines/pipeline_slug/builds/build_number/jobs/job_id/download.txt) the build's log file to see the details."""
110*1b199d19SDavid Spickett                ),
111*1b199d19SDavid Spickett                "error",
112*1b199d19SDavid Spickett            ),
113*1b199d19SDavid Spickett        )
114*1b199d19SDavid Spickett
115889b3c94SDavid Spickett    def test_report_single_file_single_testsuite(self):
116889b3c94SDavid Spickett        self.assertEqual(
117889b3c94SDavid Spickett            _generate_report(
118889b3c94SDavid Spickett                "Foo",
119*1b199d19SDavid Spickett                1,
120889b3c94SDavid Spickett                [
121889b3c94SDavid Spickett                    junit_from_xml(
122889b3c94SDavid Spickett                        dedent(
123889b3c94SDavid Spickett                            """\
124889b3c94SDavid Spickett          <?xml version="1.0" encoding="UTF-8"?>
125889b3c94SDavid Spickett          <testsuites time="8.89">
126889b3c94SDavid Spickett          <testsuite name="Bar" tests="4" failures="2" skipped="1" time="410.63">
127889b3c94SDavid Spickett          <testcase classname="Bar/test_1" name="test_1" time="0.02"/>
128889b3c94SDavid Spickett          <testcase classname="Bar/test_2" name="test_2" time="0.02">
129889b3c94SDavid Spickett            <skipped message="Reason"/>
130889b3c94SDavid Spickett          </testcase>
131889b3c94SDavid Spickett          <testcase classname="Bar/test_3" name="test_3" time="0.02">
132889b3c94SDavid Spickett            <failure><![CDATA[Output goes here]]></failure>
133889b3c94SDavid Spickett          </testcase>
134889b3c94SDavid Spickett          <testcase classname="Bar/test_4" name="test_4" time="0.02">
135889b3c94SDavid Spickett            <failure><![CDATA[Other output goes here]]></failure>
136889b3c94SDavid Spickett          </testcase>
137889b3c94SDavid Spickett          </testsuite>
138889b3c94SDavid Spickett          </testsuites>"""
139889b3c94SDavid Spickett                        )
140889b3c94SDavid Spickett                    )
141889b3c94SDavid Spickett                ],
142889b3c94SDavid Spickett            ),
143889b3c94SDavid Spickett            (
144889b3c94SDavid Spickett                dedent(
145889b3c94SDavid Spickett                    """\
146889b3c94SDavid Spickett          # Foo
147889b3c94SDavid Spickett
148889b3c94SDavid Spickett          * 1 test passed
149889b3c94SDavid Spickett          * 1 test skipped
150889b3c94SDavid Spickett          * 2 tests failed
151889b3c94SDavid Spickett
152889b3c94SDavid Spickett          ## Failed Tests
153889b3c94SDavid Spickett          (click to see output)
154889b3c94SDavid Spickett
155889b3c94SDavid Spickett          ### Bar
156889b3c94SDavid Spickett          <details>
157889b3c94SDavid Spickett          <summary>Bar/test_3/test_3</summary>
158889b3c94SDavid Spickett
159889b3c94SDavid Spickett          ```
160889b3c94SDavid Spickett          Output goes here
161889b3c94SDavid Spickett          ```
162889b3c94SDavid Spickett          </details>
163889b3c94SDavid Spickett          <details>
164889b3c94SDavid Spickett          <summary>Bar/test_4/test_4</summary>
165889b3c94SDavid Spickett
166889b3c94SDavid Spickett          ```
167889b3c94SDavid Spickett          Other output goes here
168889b3c94SDavid Spickett          ```
169889b3c94SDavid Spickett          </details>"""
170889b3c94SDavid Spickett                ),
171889b3c94SDavid Spickett                "error",
172889b3c94SDavid Spickett            ),
173889b3c94SDavid Spickett        )
174889b3c94SDavid Spickett
175889b3c94SDavid Spickett    MULTI_SUITE_OUTPUT = (
176889b3c94SDavid Spickett        dedent(
177889b3c94SDavid Spickett            """\
178889b3c94SDavid Spickett        # ABC and DEF
179889b3c94SDavid Spickett
180889b3c94SDavid Spickett        * 1 test passed
181889b3c94SDavid Spickett        * 1 test skipped
182889b3c94SDavid Spickett        * 2 tests failed
183889b3c94SDavid Spickett
184889b3c94SDavid Spickett        ## Failed Tests
185889b3c94SDavid Spickett        (click to see output)
186889b3c94SDavid Spickett
187889b3c94SDavid Spickett        ### ABC
188889b3c94SDavid Spickett        <details>
189889b3c94SDavid Spickett        <summary>ABC/test_2/test_2</summary>
190889b3c94SDavid Spickett
191889b3c94SDavid Spickett        ```
192889b3c94SDavid Spickett        ABC/test_2 output goes here
193889b3c94SDavid Spickett        ```
194889b3c94SDavid Spickett        </details>
195889b3c94SDavid Spickett
196889b3c94SDavid Spickett        ### DEF
197889b3c94SDavid Spickett        <details>
198889b3c94SDavid Spickett        <summary>DEF/test_2/test_2</summary>
199889b3c94SDavid Spickett
200889b3c94SDavid Spickett        ```
201889b3c94SDavid Spickett        DEF/test_2 output goes here
202889b3c94SDavid Spickett        ```
203889b3c94SDavid Spickett        </details>"""
204889b3c94SDavid Spickett        ),
205889b3c94SDavid Spickett        "error",
206889b3c94SDavid Spickett    )
207889b3c94SDavid Spickett
208889b3c94SDavid Spickett    def test_report_single_file_multiple_testsuites(self):
209889b3c94SDavid Spickett        self.assertEqual(
210889b3c94SDavid Spickett            _generate_report(
211889b3c94SDavid Spickett                "ABC and DEF",
212*1b199d19SDavid Spickett                1,
213889b3c94SDavid Spickett                [
214889b3c94SDavid Spickett                    junit_from_xml(
215889b3c94SDavid Spickett                        dedent(
216889b3c94SDavid Spickett                            """\
217889b3c94SDavid Spickett          <?xml version="1.0" encoding="UTF-8"?>
218889b3c94SDavid Spickett          <testsuites time="8.89">
219889b3c94SDavid Spickett          <testsuite name="ABC" tests="2" failures="1" skipped="0" time="410.63">
220889b3c94SDavid Spickett          <testcase classname="ABC/test_1" name="test_1" time="0.02"/>
221889b3c94SDavid Spickett          <testcase classname="ABC/test_2" name="test_2" time="0.02">
222889b3c94SDavid Spickett            <failure><![CDATA[ABC/test_2 output goes here]]></failure>
223889b3c94SDavid Spickett          </testcase>
224889b3c94SDavid Spickett          </testsuite>
225889b3c94SDavid Spickett          <testsuite name="DEF" tests="2" failures="1" skipped="1" time="410.63">
226889b3c94SDavid Spickett          <testcase classname="DEF/test_1" name="test_1" time="0.02">
227889b3c94SDavid Spickett            <skipped message="reason"/>
228889b3c94SDavid Spickett          </testcase>
229889b3c94SDavid Spickett          <testcase classname="DEF/test_2" name="test_2" time="0.02">
230889b3c94SDavid Spickett            <failure><![CDATA[DEF/test_2 output goes here]]></failure>
231889b3c94SDavid Spickett          </testcase>
232889b3c94SDavid Spickett          </testsuite>
233889b3c94SDavid Spickett          </testsuites>"""
234889b3c94SDavid Spickett                        )
235889b3c94SDavid Spickett                    )
236889b3c94SDavid Spickett                ],
237889b3c94SDavid Spickett            ),
238889b3c94SDavid Spickett            self.MULTI_SUITE_OUTPUT,
239889b3c94SDavid Spickett        )
240889b3c94SDavid Spickett
241889b3c94SDavid Spickett    def test_report_multiple_files_multiple_testsuites(self):
242889b3c94SDavid Spickett        self.assertEqual(
243889b3c94SDavid Spickett            _generate_report(
244889b3c94SDavid Spickett                "ABC and DEF",
245*1b199d19SDavid Spickett                1,
246889b3c94SDavid Spickett                [
247889b3c94SDavid Spickett                    junit_from_xml(
248889b3c94SDavid Spickett                        dedent(
249889b3c94SDavid Spickett                            """\
250889b3c94SDavid Spickett          <?xml version="1.0" encoding="UTF-8"?>
251889b3c94SDavid Spickett          <testsuites time="8.89">
252889b3c94SDavid Spickett          <testsuite name="ABC" tests="2" failures="1" skipped="0" time="410.63">
253889b3c94SDavid Spickett          <testcase classname="ABC/test_1" name="test_1" time="0.02"/>
254889b3c94SDavid Spickett          <testcase classname="ABC/test_2" name="test_2" time="0.02">
255889b3c94SDavid Spickett            <failure><![CDATA[ABC/test_2 output goes here]]></failure>
256889b3c94SDavid Spickett          </testcase>
257889b3c94SDavid Spickett          </testsuite>
258889b3c94SDavid Spickett          </testsuites>"""
259889b3c94SDavid Spickett                        )
260889b3c94SDavid Spickett                    ),
261889b3c94SDavid Spickett                    junit_from_xml(
262889b3c94SDavid Spickett                        dedent(
263889b3c94SDavid Spickett                            """\
264889b3c94SDavid Spickett          <?xml version="1.0" encoding="UTF-8"?>
265889b3c94SDavid Spickett          <testsuites time="8.89">
266889b3c94SDavid Spickett          <testsuite name="DEF" tests="2" failures="1" skipped="1" time="410.63">
267889b3c94SDavid Spickett          <testcase classname="DEF/test_1" name="test_1" time="0.02">
268889b3c94SDavid Spickett            <skipped message="reason"/>
269889b3c94SDavid Spickett          </testcase>
270889b3c94SDavid Spickett          <testcase classname="DEF/test_2" name="test_2" time="0.02">
271889b3c94SDavid Spickett            <failure><![CDATA[DEF/test_2 output goes here]]></failure>
272889b3c94SDavid Spickett          </testcase>
273889b3c94SDavid Spickett          </testsuite>
274889b3c94SDavid Spickett          </testsuites>"""
275889b3c94SDavid Spickett                        )
276889b3c94SDavid Spickett                    ),
277889b3c94SDavid Spickett                ],
278889b3c94SDavid Spickett            ),
279889b3c94SDavid Spickett            self.MULTI_SUITE_OUTPUT,
280889b3c94SDavid Spickett        )
281889b3c94SDavid Spickett
282889b3c94SDavid Spickett    def test_report_dont_list_failures(self):
283889b3c94SDavid Spickett        self.assertEqual(
284889b3c94SDavid Spickett            _generate_report(
285889b3c94SDavid Spickett                "Foo",
286*1b199d19SDavid Spickett                1,
287889b3c94SDavid Spickett                [
288889b3c94SDavid Spickett                    junit_from_xml(
289889b3c94SDavid Spickett                        dedent(
290889b3c94SDavid Spickett                            """\
291889b3c94SDavid Spickett          <?xml version="1.0" encoding="UTF-8"?>
292889b3c94SDavid Spickett          <testsuites time="0.02">
293889b3c94SDavid Spickett          <testsuite name="Bar" tests="1" failures="1" skipped="0" time="0.02">
294889b3c94SDavid Spickett          <testcase classname="Bar/test_1" name="test_1" time="0.02">
295889b3c94SDavid Spickett            <failure><![CDATA[Output goes here]]></failure>
296889b3c94SDavid Spickett          </testcase>
297889b3c94SDavid Spickett          </testsuite>
298889b3c94SDavid Spickett          </testsuites>"""
299889b3c94SDavid Spickett                        )
300889b3c94SDavid Spickett                    )
301889b3c94SDavid Spickett                ],
302889b3c94SDavid Spickett                list_failures=False,
303889b3c94SDavid Spickett            ),
304889b3c94SDavid Spickett            (
305889b3c94SDavid Spickett                dedent(
306889b3c94SDavid Spickett                    """\
307889b3c94SDavid Spickett          # Foo
308889b3c94SDavid Spickett
309889b3c94SDavid Spickett          * 1 test failed
310889b3c94SDavid Spickett
311889b3c94SDavid Spickett          Failed tests and their output was too large to report. Download the build's log file to see the details."""
312889b3c94SDavid Spickett                ),
313889b3c94SDavid Spickett                "error",
314889b3c94SDavid Spickett            ),
315889b3c94SDavid Spickett        )
316889b3c94SDavid Spickett
31771fd5288SDavid Spickett    def test_report_dont_list_failures_link_to_log(self):
31871fd5288SDavid Spickett        self.assertEqual(
31971fd5288SDavid Spickett            _generate_report(
32071fd5288SDavid Spickett                "Foo",
321*1b199d19SDavid Spickett                1,
32271fd5288SDavid Spickett                [
32371fd5288SDavid Spickett                    junit_from_xml(
32471fd5288SDavid Spickett                        dedent(
32571fd5288SDavid Spickett                            """\
32671fd5288SDavid Spickett          <?xml version="1.0" encoding="UTF-8"?>
32771fd5288SDavid Spickett          <testsuites time="0.02">
32871fd5288SDavid Spickett          <testsuite name="Bar" tests="1" failures="1" skipped="0" time="0.02">
32971fd5288SDavid Spickett          <testcase classname="Bar/test_1" name="test_1" time="0.02">
33071fd5288SDavid Spickett            <failure><![CDATA[Output goes here]]></failure>
33171fd5288SDavid Spickett          </testcase>
33271fd5288SDavid Spickett          </testsuite>
33371fd5288SDavid Spickett          </testsuites>"""
33471fd5288SDavid Spickett                        )
33571fd5288SDavid Spickett                    )
33671fd5288SDavid Spickett                ],
33771fd5288SDavid Spickett                list_failures=False,
33871fd5288SDavid Spickett                buildkite_info={
33971fd5288SDavid Spickett                    "BUILDKITE_ORGANIZATION_SLUG": "organization_slug",
34071fd5288SDavid Spickett                    "BUILDKITE_PIPELINE_SLUG": "pipeline_slug",
34171fd5288SDavid Spickett                    "BUILDKITE_BUILD_NUMBER": "build_number",
34271fd5288SDavid Spickett                    "BUILDKITE_JOB_ID": "job_id",
34371fd5288SDavid Spickett                },
34471fd5288SDavid Spickett            ),
34571fd5288SDavid Spickett            (
34671fd5288SDavid Spickett                dedent(
34771fd5288SDavid Spickett                    """\
34871fd5288SDavid Spickett          # Foo
34971fd5288SDavid Spickett
35071fd5288SDavid Spickett          * 1 test failed
35171fd5288SDavid Spickett
35271fd5288SDavid Spickett          Failed tests and their output was too large to report. [Download](https://buildkite.com/organizations/organization_slug/pipelines/pipeline_slug/builds/build_number/jobs/job_id/download.txt) the build's log file to see the details."""
35371fd5288SDavid Spickett                ),
35471fd5288SDavid Spickett                "error",
35571fd5288SDavid Spickett            ),
35671fd5288SDavid Spickett        )
35771fd5288SDavid Spickett
358889b3c94SDavid Spickett    def test_report_size_limit(self):
359889b3c94SDavid Spickett        self.assertEqual(
360889b3c94SDavid Spickett            _generate_report(
361889b3c94SDavid Spickett                "Foo",
362*1b199d19SDavid Spickett                1,
363889b3c94SDavid Spickett                [
364889b3c94SDavid Spickett                    junit_from_xml(
365889b3c94SDavid Spickett                        dedent(
366889b3c94SDavid Spickett                            """\
367889b3c94SDavid Spickett          <?xml version="1.0" encoding="UTF-8"?>
368889b3c94SDavid Spickett          <testsuites time="0.02">
369889b3c94SDavid Spickett          <testsuite name="Bar" tests="1" failures="1" skipped="0" time="0.02">
370889b3c94SDavid Spickett          <testcase classname="Bar/test_1" name="test_1" time="0.02">
371889b3c94SDavid Spickett            <failure><![CDATA[Some long output goes here...]]></failure>
372889b3c94SDavid Spickett          </testcase>
373889b3c94SDavid Spickett          </testsuite>
374889b3c94SDavid Spickett          </testsuites>"""
375889b3c94SDavid Spickett                        )
376889b3c94SDavid Spickett                    )
377889b3c94SDavid Spickett                ],
378889b3c94SDavid Spickett                size_limit=128,
379889b3c94SDavid Spickett            ),
380889b3c94SDavid Spickett            (
381889b3c94SDavid Spickett                dedent(
382889b3c94SDavid Spickett                    """\
383889b3c94SDavid Spickett          # Foo
384889b3c94SDavid Spickett
385889b3c94SDavid Spickett          * 1 test failed
386889b3c94SDavid Spickett
387889b3c94SDavid Spickett          Failed tests and their output was too large to report. Download the build's log file to see the details."""
388889b3c94SDavid Spickett                ),
389889b3c94SDavid Spickett                "error",
390889b3c94SDavid Spickett            ),
391889b3c94SDavid Spickett        )
392889b3c94SDavid Spickett
393889b3c94SDavid Spickett
394889b3c94SDavid Spickett# Set size_limit to limit the byte size of the report. The default is 1MB as this
395889b3c94SDavid Spickett# is the most that can be put into an annotation. If the generated report exceeds
396889b3c94SDavid Spickett# this limit and failures are listed, it will be generated again without failures
397889b3c94SDavid Spickett# listed. This minimal report will always fit into an annotation.
398889b3c94SDavid Spickett# If include failures is False, total number of test will be reported but their names
399889b3c94SDavid Spickett# and output will not be.
40071fd5288SDavid Spickettdef _generate_report(
40171fd5288SDavid Spickett    title,
402*1b199d19SDavid Spickett    return_code,
40371fd5288SDavid Spickett    junit_objects,
40471fd5288SDavid Spickett    size_limit=1024 * 1024,
40571fd5288SDavid Spickett    list_failures=True,
40671fd5288SDavid Spickett    buildkite_info=None,
40771fd5288SDavid Spickett):
408889b3c94SDavid Spickett    if not junit_objects:
409*1b199d19SDavid Spickett        # Note that we do not post an empty report, therefore we can ignore a
410*1b199d19SDavid Spickett        # non-zero return code in situations like this.
411*1b199d19SDavid Spickett        #
412*1b199d19SDavid Spickett        # If we were going to post a report, then yes, it would be misleading
413*1b199d19SDavid Spickett        # to say we succeeded when the final return code was non-zero.
4146a12b43aSDavid Spickett        return ("", "success")
415889b3c94SDavid Spickett
416889b3c94SDavid Spickett    failures = {}
417889b3c94SDavid Spickett    tests_run = 0
418889b3c94SDavid Spickett    tests_skipped = 0
419889b3c94SDavid Spickett    tests_failed = 0
420889b3c94SDavid Spickett
421889b3c94SDavid Spickett    for results in junit_objects:
422889b3c94SDavid Spickett        for testsuite in results:
423889b3c94SDavid Spickett            tests_run += testsuite.tests
424889b3c94SDavid Spickett            tests_skipped += testsuite.skipped
425889b3c94SDavid Spickett            tests_failed += testsuite.failures
426889b3c94SDavid Spickett
427889b3c94SDavid Spickett            for test in testsuite:
428889b3c94SDavid Spickett                if (
429889b3c94SDavid Spickett                    not test.is_passed
430889b3c94SDavid Spickett                    and test.result
431889b3c94SDavid Spickett                    and isinstance(test.result[0], Failure)
432889b3c94SDavid Spickett                ):
433889b3c94SDavid Spickett                    if failures.get(testsuite.name) is None:
434889b3c94SDavid Spickett                        failures[testsuite.name] = []
435889b3c94SDavid Spickett                    failures[testsuite.name].append(
436889b3c94SDavid Spickett                        (test.classname + "/" + test.name, test.result[0].text)
437889b3c94SDavid Spickett                    )
438889b3c94SDavid Spickett
439889b3c94SDavid Spickett    if not tests_run:
4403b8426d3SDavid Spickett        return ("", None)
441889b3c94SDavid Spickett
442*1b199d19SDavid Spickett    style = "success"
443*1b199d19SDavid Spickett    # Either tests failed, or all tests passed but something failed to build.
444*1b199d19SDavid Spickett    if tests_failed or return_code != 0:
445*1b199d19SDavid Spickett        style = "error"
446*1b199d19SDavid Spickett
447889b3c94SDavid Spickett    report = [f"# {title}", ""]
448889b3c94SDavid Spickett
449889b3c94SDavid Spickett    tests_passed = tests_run - tests_skipped - tests_failed
450889b3c94SDavid Spickett
451889b3c94SDavid Spickett    def plural(num_tests):
452889b3c94SDavid Spickett        return "test" if num_tests == 1 else "tests"
453889b3c94SDavid Spickett
454889b3c94SDavid Spickett    if tests_passed:
455889b3c94SDavid Spickett        report.append(f"* {tests_passed} {plural(tests_passed)} passed")
456889b3c94SDavid Spickett    if tests_skipped:
457889b3c94SDavid Spickett        report.append(f"* {tests_skipped} {plural(tests_skipped)} skipped")
458889b3c94SDavid Spickett    if tests_failed:
459889b3c94SDavid Spickett        report.append(f"* {tests_failed} {plural(tests_failed)} failed")
460889b3c94SDavid Spickett
46171fd5288SDavid Spickett    if buildkite_info is not None:
46271fd5288SDavid Spickett        log_url = (
46371fd5288SDavid Spickett            "https://buildkite.com/organizations/{BUILDKITE_ORGANIZATION_SLUG}/"
46471fd5288SDavid Spickett            "pipelines/{BUILDKITE_PIPELINE_SLUG}/builds/{BUILDKITE_BUILD_NUMBER}/"
46571fd5288SDavid Spickett            "jobs/{BUILDKITE_JOB_ID}/download.txt".format(**buildkite_info)
46671fd5288SDavid Spickett        )
46771fd5288SDavid Spickett        download_text = f"[Download]({log_url})"
46871fd5288SDavid Spickett    else:
46971fd5288SDavid Spickett        download_text = "Download"
47071fd5288SDavid Spickett
471*1b199d19SDavid Spickett    if not list_failures:
472889b3c94SDavid Spickett        report.extend(
473889b3c94SDavid Spickett            [
474889b3c94SDavid Spickett                "",
475889b3c94SDavid Spickett                "Failed tests and their output was too large to report. "
47671fd5288SDavid Spickett                f"{download_text} the build's log file to see the details.",
477889b3c94SDavid Spickett            ]
478889b3c94SDavid Spickett        )
479889b3c94SDavid Spickett    elif failures:
480889b3c94SDavid Spickett        report.extend(["", "## Failed Tests", "(click to see output)"])
481889b3c94SDavid Spickett
482889b3c94SDavid Spickett        for testsuite_name, failures in failures.items():
483889b3c94SDavid Spickett            report.extend(["", f"### {testsuite_name}"])
484889b3c94SDavid Spickett            for name, output in failures:
485889b3c94SDavid Spickett                report.extend(
486889b3c94SDavid Spickett                    [
487889b3c94SDavid Spickett                        "<details>",
488889b3c94SDavid Spickett                        f"<summary>{name}</summary>",
489889b3c94SDavid Spickett                        "",
490889b3c94SDavid Spickett                        "```",
491889b3c94SDavid Spickett                        output,
492889b3c94SDavid Spickett                        "```",
493889b3c94SDavid Spickett                        "</details>",
494889b3c94SDavid Spickett                    ]
495889b3c94SDavid Spickett                )
496*1b199d19SDavid Spickett    elif return_code != 0:
497*1b199d19SDavid Spickett        # No tests failed but the build was in a failed state. Bring this to the user's
498*1b199d19SDavid Spickett        # attention.
499*1b199d19SDavid Spickett        report.extend(
500*1b199d19SDavid Spickett            [
501*1b199d19SDavid Spickett                "",
502*1b199d19SDavid Spickett                "All tests passed but another part of the build **failed**.",
503*1b199d19SDavid Spickett                "",
504*1b199d19SDavid Spickett                f"{download_text} the build's log file to see the details.",
505*1b199d19SDavid Spickett            ]
506*1b199d19SDavid Spickett        )
507889b3c94SDavid Spickett
508889b3c94SDavid Spickett    report = "\n".join(report)
509889b3c94SDavid Spickett    if len(report.encode("utf-8")) > size_limit:
51071fd5288SDavid Spickett        return _generate_report(
51171fd5288SDavid Spickett            title,
512*1b199d19SDavid Spickett            return_code,
51371fd5288SDavid Spickett            junit_objects,
51471fd5288SDavid Spickett            size_limit,
51571fd5288SDavid Spickett            list_failures=False,
51671fd5288SDavid Spickett            buildkite_info=buildkite_info,
51771fd5288SDavid Spickett        )
518889b3c94SDavid Spickett
519889b3c94SDavid Spickett    return report, style
520889b3c94SDavid Spickett
521889b3c94SDavid Spickett
522*1b199d19SDavid Spickettdef generate_report(title, return_code, junit_files, buildkite_info):
52371fd5288SDavid Spickett    return _generate_report(
52471fd5288SDavid Spickett        title,
525*1b199d19SDavid Spickett        return_code,
52671fd5288SDavid Spickett        [JUnitXml.fromfile(p) for p in junit_files],
52771fd5288SDavid Spickett        buildkite_info=buildkite_info,
52871fd5288SDavid Spickett    )
529889b3c94SDavid Spickett
530889b3c94SDavid Spickett
531889b3c94SDavid Spickettif __name__ == "__main__":
532889b3c94SDavid Spickett    parser = argparse.ArgumentParser()
533889b3c94SDavid Spickett    parser.add_argument(
534889b3c94SDavid Spickett        "title", help="Title of the test report, without Markdown formatting."
535889b3c94SDavid Spickett    )
536889b3c94SDavid Spickett    parser.add_argument("context", help="Annotation context to write to.")
537*1b199d19SDavid Spickett    parser.add_argument("return_code", help="The build's return code.", type=int)
538889b3c94SDavid Spickett    parser.add_argument("junit_files", help="Paths to JUnit report files.", nargs="*")
539889b3c94SDavid Spickett    args = parser.parse_args()
540889b3c94SDavid Spickett
54171fd5288SDavid Spickett    # All of these are required to build a link to download the log file.
54271fd5288SDavid Spickett    env_var_names = [
54371fd5288SDavid Spickett        "BUILDKITE_ORGANIZATION_SLUG",
54471fd5288SDavid Spickett        "BUILDKITE_PIPELINE_SLUG",
54571fd5288SDavid Spickett        "BUILDKITE_BUILD_NUMBER",
54671fd5288SDavid Spickett        "BUILDKITE_JOB_ID",
54771fd5288SDavid Spickett    ]
54871fd5288SDavid Spickett    buildkite_info = {k: v for k, v in os.environ.items() if k in env_var_names}
54971fd5288SDavid Spickett    if len(buildkite_info) != len(env_var_names):
55071fd5288SDavid Spickett        buildkite_info = None
55171fd5288SDavid Spickett
552*1b199d19SDavid Spickett    report, style = generate_report(
553*1b199d19SDavid Spickett        args.title, args.return_code, args.junit_files, buildkite_info
554*1b199d19SDavid Spickett    )
555889b3c94SDavid Spickett
5566a12b43aSDavid Spickett    if report:
557889b3c94SDavid Spickett        p = subprocess.Popen(
558889b3c94SDavid Spickett            [
559889b3c94SDavid Spickett                "buildkite-agent",
560889b3c94SDavid Spickett                "annotate",
561889b3c94SDavid Spickett                "--context",
562889b3c94SDavid Spickett                args.context,
563889b3c94SDavid Spickett                "--style",
564889b3c94SDavid Spickett                style,
565889b3c94SDavid Spickett            ],
566889b3c94SDavid Spickett            stdin=subprocess.PIPE,
567889b3c94SDavid Spickett            stderr=subprocess.PIPE,
568889b3c94SDavid Spickett            universal_newlines=True,
569889b3c94SDavid Spickett        )
570889b3c94SDavid Spickett
571889b3c94SDavid Spickett        # The report can be larger than the buffer for command arguments so we send
572889b3c94SDavid Spickett        # it over stdin instead.
573889b3c94SDavid Spickett        _, err = p.communicate(input=report)
574889b3c94SDavid Spickett        if p.returncode:
575889b3c94SDavid Spickett            raise RuntimeError(f"Failed to send report to buildkite-agent:\n{err}")
576