@@ -1668,22 +1668,6 @@ def run_tests_github(self, *args, exitcode=0):
16681668 except FileNotFoundError :
16691669 return output , None
16701670
1671- @staticmethod
1672- def parse_github_annotations (output ):
1673- # Return the (title, file, line) of each "::error" annotation
1674- def unescape (text ):
1675- return (text .replace ('%3A' , ':' ).replace ('%2C' , ',' )
1676- .replace ('%0A' , '\n ' ).replace ('%0D' , '\r ' )
1677- .replace ('%25' , '%' ))
1678-
1679- annotations = []
1680- for match in re .finditer (r'^::error (.*?)::' , output , re .MULTILINE ):
1681- props = dict (prop .split ('=' , 1 ) for prop in match [1 ].split (',' ))
1682- props = {key : unescape (value ) for key , value in props .items ()}
1683- annotations .append ((props ['title' ], props ['file' ],
1684- props .get ('line' )))
1685- return annotations
1686-
16871671 def test_github_annotations (self ):
16881672 # Every test failure is annotated once in the GitHub Actions job log,
16891673 # located in the test file, at the failing line of the test file if
@@ -1715,17 +1699,17 @@ def test_subtest(self):
17151699 class SetUpClassTests(unittest.TestCase):
17161700 @classmethod
17171701 def setUpClass(cls):
1718- raise ValueError("setUpClass") # setUpClass
1702+ raise ValueError("setUpClass")
17191703
17201704 def test_never(self):
17211705 pass
17221706 """ )
1723- setup_module , setup_module_lines = self .create_marked_test (
1707+ setup_module , _ = self .create_marked_test (
17241708 'github_setup_module' , """
17251709 import unittest
17261710
17271711 def setUpModule():
1728- raise ValueError("setUpModule") # setUpModule
1712+ raise ValueError("setUpModule")
17291713
17301714 class Tests(unittest.TestCase):
17311715 def test_never(self):
@@ -1746,24 +1730,23 @@ def test_crash(self):
17461730 os._exit(1)
17471731 """ )
17481732
1749- def path (name ):
1750- return os .path .join (self .tmptestdir , f'{ name } .py' )
1751-
1752- # Test cases: (title, file, line)
1733+ # Annotated test cases: (title, file, line)
17531734 test_cases = [
17541735 (f'test_error ({ cases } .Tests.test_error)' ,
1755- path ( cases ) , lines ['error' ]),
1736+ f' { cases } .py' , lines ['error' ]),
17561737 (f'test_fail ({ cases } .Tests.test_fail)' ,
1757- path ( cases ) , lines ['fail' ]),
1738+ f' { cases } .py' , lines ['fail' ]),
17581739 (f'test_subtest ({ cases } .Tests.test_subtest) (x=1.5)' ,
1759- path ( cases ) , lines ['subtest' ]),
1740+ f' { cases } .py' , lines ['subtest' ]),
17601741 # Doctest examples are subtests: "[0]" is the example index
17611742 (f'doctest_fail ({ cases } ) [0]' ,
1762- path (cases ), lines ['doctest' ]),
1763- (f'setUpClass ({ cases } .SetUpClassTests)' ,
1764- path (cases ), lines ['setUpClass' ]),
1765- (f'setUpModule ({ setup_module } )' ,
1766- path (setup_module ), setup_module_lines ['setUpModule' ]),
1743+ f'{ cases } .py' , lines ['doctest' ]),
1744+ ]
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 } )' ,
17671750 ]
17681751 # Test files: heading in the job summary
17691752 test_files = {
@@ -1794,43 +1777,55 @@ def path(name):
17941777 output , summary = self .run_tests_github (
17951778 * args , * tests , exitcode = EXITCODE_BAD_TEST )
17961779
1797- annotations = list (test_cases )
1780+ expected = list (test_cases )
17981781 if '--fast-ci' in args :
1799- annotations .append ((env_changed , path ( env_changed ) , None ))
1782+ expected .append ((env_changed , f' { env_changed } .py' , None ))
18001783 if crash in tests :
1801- annotations .append ((crash , path (crash ), None ))
1802- self .assertCountEqual (self .parse_github_annotations (output ),
1803- annotations , output )
1804- # Each test case is annotated right after its failure report
1805- for title , _ , _ in annotations [:len (test_cases )]:
1806- title = re .escape (title )
1807- self .assertRegex (
1808- output ,
1809- rf'(?m)^(?:ERROR|FAIL): { title } \n'
1810- rf'(?:(?!={{70}}$).*\n)*?::error .*title={ title } ::' )
1811- # The env changed annotation comes right after the warnings
1812- if '--fast-ci' in args :
1813- self .assertRegex (
1814- output ,
1815- rf'(?m)^Warning -- os\.environ was modified by '
1816- rf'{ env_changed } \n(?:Warning -- .*\n)*'
1817- rf'::error .*title={ env_changed } ::' )
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 )
18181813
18191814 # The job summary lists the failed tests in completion order
18201815 summary_lines = summary .splitlines ()
18211816 self .assertEqual (summary_lines [0 ],
18221817 f'## FAILURE: { len (tests )} test files and '
1823- f'{ len (test_cases )} test cases failed' )
1818+ f'{ len (failed_titles )} test cases failed' )
18241819 self .assertCountEqual (
18251820 [line for line in summary_lines
18261821 if line .startswith ('### ' )],
18271822 [test_files [name ] for name in tests ])
18281823 self .assertIn ('- os.environ was modified' , summary_lines )
18291824 self .assertCountEqual (
18301825 re .findall (r'<summary>(.*)</summary>' , summary ),
1831- [ title for title , _ , _ in test_cases ] )
1826+ failed_titles )
18321827 self .assertEqual (summary .count ('```pytb\n ' ),
1833- len (test_cases ))
1828+ len (failed_titles ))
18341829
18351830 def test_github_summary_success (self ):
18361831 # No annotation and no job summary when all tests pass
0 commit comments