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