@@ -1642,197 +1642,132 @@ def test_env_changed(self):
16421642 success = True ),
16431643 stats = 2 )
16441644
1645- def create_marked_test (self , name , code ):
1646- # Create a test: return its name and the line number of each of its
1647- # "# marker" comments
1648- code = textwrap .dedent (code )
1649- name = self .create_test (name , code )
1650- markers = {match [1 ]: str (lineno )
1651- for lineno , text in enumerate (code .splitlines (), 1 )
1652- if (match := re .search (r'# (\w+)$' , text ))}
1653- return name , markers
1654-
16551645 def run_tests_github (self , * args , exitcode = 0 ):
1656- # Run tests as in GitHub Actions: return the output and the job
1657- # summary (None if regrtest didn't write it )
1646+ # Run tests as in GitHub Actions: return the output, the annotations
1647+ # as (file, line, title) tuples and the job summary ('' if not written )
16581648 filename = os .path .join (self .tmptestdir , 'github_step_summary.md' )
1659- self .addCleanup (os_helper .unlink , filename )
16601649 os_helper .unlink (filename )
1661- env = dict (os .environ )
1650+ env = dict (os .environ , GITHUB_STEP_SUMMARY = filename )
16621651 env .pop ('SOURCE_DATE_EPOCH' , None )
1663- env ['GITHUB_STEP_SUMMARY' ] = filename
16641652 output = self .run_tests (* args , env = env , exitcode = exitcode )
1653+ annotations = re .findall (r'^::error file=[^,]*?([^/,]+)'
1654+ r'(?:,line=(\d+))?,title=([^:]*)::' ,
1655+ output , re .MULTILINE )
1656+ # Every annotation has a file and a title
1657+ self .assertEqual (len (annotations ),
1658+ len (re .findall ('^::error' , output , re .MULTILINE )),
1659+ output )
16651660 try :
16661661 with open (filename , encoding = 'utf-8' ) as fp :
1667- return output , fp .read ()
1662+ return output , annotations , fp .read ()
16681663 except FileNotFoundError :
1669- return output , None
1664+ return output , annotations , ''
16701665
16711666 def test_github_annotations (self ):
1672- # Every test failure is annotated once in the GitHub Actions job log,
1673- # located in the test file, at the failing line of the test file if
1674- # there is a traceback. The job summary lists the failures.
1675- cases , lines = self . create_marked_test ( 'github_cases' , """
1676- import doctest, json, sys, unittest
1667+ # Every failure is annotated in the GitHub Actions job log, located
1668+ # in its test file, at the failing line ("# fail") for test cases.
1669+ # The job summary lists the failures.
1670+ code = textwrap . dedent ( """
1671+ import doctest, sys, unittest
16771672
16781673 def load_tests(loader, tests, pattern):
16791674 tests.addTests(doctest.DocTestSuite(sys.modules[__name__]))
16801675 return tests
16811676
16821677 def doctest_fail():
16831678 '''
1684- >>> 1 + 1 # doctest
1679+ >>> 1 + 1 # fail
16851680 3
16861681 '''
16871682
16881683 class Tests(unittest.TestCase):
16891684 def test_error(self):
1690- json.loads("{") # error
1685+ {}['key'] # fail
16911686
16921687 def test_fail(self):
16931688 self.assertEqual(1, 2) # fail
16941689
16951690 def test_subtest(self):
1696- with self.subTest(x=1.5 ):
1697- self.fail("subtest" ) # subtest
1691+ with self.subTest(x=1):
1692+ self.fail() # fail
16981693
16991694 class SetUpClassTests(unittest.TestCase):
1695+ # Fixture errors are only listed in the job summary
17001696 @classmethod
17011697 def setUpClass(cls):
1702- raise ValueError("setUpClass")
1698+ raise ValueError
17031699
17041700 def test_never(self):
17051701 pass
17061702 """ )
1707- setup_module , _ = self .create_marked_test (
1708- 'github_setup_module' , """
1709- import unittest
1710-
1711- def setUpModule():
1712- raise ValueError("setUpModule")
1713-
1714- class Tests(unittest.TestCase):
1715- def test_never(self):
1716- pass
1717- """ )
1718- env_changed , _ = self .create_marked_test ('github_env_changed' , """
1703+ cases = self .create_test ('github_cases' , code )
1704+ fail_lines = {str (lineno )
1705+ for lineno , line in enumerate (code .splitlines (), 1 )
1706+ if line .endswith ('# fail' )}
1707+ env_changed = self .create_test ('github_env_changed' , textwrap .dedent ("""
17191708 import os, unittest
17201709
17211710 class Tests(unittest.TestCase):
17221711 def test_env_changed(self):
1723- os.environ[" REGRTEST_GITHUB_ENV_CHANGED" ] = "1"
1724- """ )
1725- crash , _ = self .create_marked_test ('github_crash' , """
1712+ os.environ[' REGRTEST_GITHUB_ENV_CHANGED' ] = '1'
1713+ """ ))
1714+ crash = self .create_test ('github_crash' , textwrap . dedent ( """
17261715 import os, unittest
17271716
17281717 class Tests(unittest.TestCase):
17291718 def test_crash(self):
17301719 os._exit(1)
1731- """ )
1732-
1733- # Annotated test cases: (title, file, line)
1734- test_cases = [
1735- (f'test_error ({ cases } .Tests.test_error)' ,
1736- f'{ cases } .py' , lines ['error' ]),
1737- (f'test_fail ({ cases } .Tests.test_fail)' ,
1738- f'{ cases } .py' , lines ['fail' ]),
1739- (f'test_subtest ({ cases } .Tests.test_subtest) (x=1.5)' ,
1740- f'{ cases } .py' , lines ['subtest' ]),
1741- # Doctest examples are subtests: "[0]" is the example index
1742- (f'doctest_fail ({ cases } ) [0]' ,
1743- f'{ cases } .py' , lines ['doctest' ]),
1720+ """ ))
1721+ case_titles = [
1722+ f'test_error ({ cases } .Tests.test_error)' ,
1723+ f'test_fail ({ cases } .Tests.test_fail)' ,
1724+ f'test_subtest ({ cases } .Tests.test_subtest) (x=1)' ,
1725+ f'doctest_fail ({ cases } ) [0]' ,
17441726 ]
1745- # Test cases only listed in the job summary: fixture errors
1746- # (setUpClass, setUpModule) are not annotated
1747- failed_titles = [title for title , _ , _ in test_cases ] + [
1748- f'setUpClass ({ cases } .SetUpClassTests)' ,
1749- f'setUpModule ({ setup_module } )' ,
1750- ]
1751- # Test files: heading in the job summary
1752- test_files = {
1753- cases : f'### { cases } failed (2 errors, 3 failures)' ,
1754- setup_module : f'### { setup_module } failed (1 error)' ,
1755- env_changed : f'### { env_changed } failed (env changed)' ,
1756- crash : f'### { crash } worker non-zero exit code' ,
1757- }
17581727
17591728 # Representative command lines of the CI jobs
1760- command_lines = {
1761- # "make ci", Windows, macOS, installed Python; the JIT jobs use
1762- # the equivalent "-j0 --verbose2 --verbose3". Only failures of
1763- # the re-run are annotated, not the reported first failures.
1764- 'fast-ci' : ['--fast-ci' , '-j2' ],
1765- # Sanitizers and Hypothesis: env changed is not a failure
1766- 'parallel' : ['-j2' , '-W' ],
1767- # iOS and WASI run tests in the main process: a crash kills it
1768- 'single-process' : ['--fast-ci' , '--single-process' ],
1769- # Profile task of PGO and BOLT builds: failures stop the build
1770- 'pgo' : ['--pgo' ],
1771- }
1772- for name , args in command_lines .items ():
1773- with self .subTest (name ):
1774- tests = list (test_files )
1775- if '--single-process' in args or '--pgo' in args :
1776- tests .remove (crash )
1777- output , summary = self .run_tests_github (
1729+ for args in (
1730+ # "make ci": only the failures of the re-run are annotated
1731+ ['--fast-ci' , '-j2' ],
1732+ # Sanitizers: env changed is not a failure
1733+ ['-j2' , '-W' ],
1734+ # iOS and WASI: a crash would kill the main process
1735+ ['--fast-ci' , '--single-process' ],
1736+ # PGO profile task
1737+ ['--pgo' ],
1738+ ):
1739+ with self .subTest (args = args ):
1740+ in_process = '--single-process' in args or '--pgo' in args
1741+ tests = [cases , env_changed ] + ([] if in_process else [crash ])
1742+ output , annotations , summary = self .run_tests_github (
17781743 * args , * tests , exitcode = EXITCODE_BAD_TEST )
17791744
1780- expected = list ( test_cases )
1745+ expected = [( cases , title ) for title in case_titles ]
17811746 if '--fast-ci' in args :
1782- expected .append ((env_changed , f' { env_changed } .py' , None ))
1747+ expected .append ((env_changed , env_changed ))
17831748 if crash in tests :
1784- expected .append ((crash , f'{ crash } .py' , None ))
1785-
1786- # "::error file=...,line=...,title=...::message"
1787- annotations = []
1788- output_lines = output .splitlines ()
1789- for i , line in enumerate (output_lines ):
1790- if not line .startswith ('::error ' ):
1791- continue
1792- _ , props , message = line .split ('::' , 2 )
1793- props = props .removeprefix ('error ' ).split (',' )
1794- props = dict (prop .split ('=' , 1 ) for prop in props )
1795- title = props ['title' ]
1796- annotations .append ((title , os .path .basename (props ['file' ]),
1797- props .get ('line' )))
1798- before = output_lines [:i ]
1799- if title == env_changed :
1800- # Right after the env changed warnings
1801- self .assertStartsWith (before [- 1 ], 'Warning -- ' )
1802- elif title != crash :
1803- # Right after the failure report of the test, which
1804- # ends with the exception and an empty line
1805- report = next (
1806- text for text in reversed (before )
1807- if text .startswith (('ERROR: ' , 'FAIL: ' )))
1808- self .assertEqual (report .split (': ' , 1 )[1 ], title )
1809- message = message .replace ('%0A' , '\n ' ).splitlines ()
1810- self .assertEqual (before [- len (message ) - 1 :],
1811- message + ['' ])
1812- self .assertCountEqual (annotations , expected , output )
1813-
1814- # The job summary lists the failed tests in completion order
1815- summary_lines = summary .splitlines ()
1816- self .assertEqual (summary_lines [0 ],
1817- f'## FAILURE: { len (tests )} test files and '
1818- f'{ len (failed_titles )} test cases failed' )
1749+ expected .append ((crash , crash ))
18191750 self .assertCountEqual (
1820- [line for line in summary_lines
1821- if line .startswith ('### ' )],
1822- [test_files [name ] for name in tests ])
1823- self .assertIn ('- os.environ was modified' , summary_lines )
1824- self .assertCountEqual (
1825- re .findall (r'<summary>(.*)</summary>' , summary ),
1826- failed_titles )
1827- self .assertEqual (summary .count ('```pytb\n ' ),
1828- len (failed_titles ))
1751+ [(file , title ) for file , _ , title in annotations ],
1752+ [(f'{ name } .py' , title ) for name , title in expected ],
1753+ output )
1754+ for file , line , title in annotations :
1755+ # Test files without a failed test case have no line
1756+ self .assertIn (line , fail_lines if file == f'{ cases } .py'
1757+ else {'' }, title )
1758+
1759+ self .assertIn (f'## FAILURE: { len (tests )} test files and '
1760+ f'5 test cases failed' , summary )
1761+ for name in tests :
1762+ self .assertIn (f'### { name } ' , summary )
1763+ self .assertIn ('- os.environ was modified' , summary )
1764+ self .assertEqual (summary .count ('<summary>' ), 5 )
18291765
18301766 def test_github_summary_success (self ):
18311767 # No annotation and no job summary when all tests pass
1832- testname = self .create_test ()
1833- output , summary = self .run_tests_github (testname )
1834- self .assertNotIn ('::error' , output )
1835- self .assertIsNone (summary )
1768+ output , annotations , summary = self .run_tests_github (self .create_test ())
1769+ self .assertEqual (annotations , [])
1770+ self .assertEqual (summary , '' )
18361771
18371772 def test_rerun_fail (self ):
18381773 # FAILURE then FAILURE
0 commit comments