Skip to content

Commit fe08263

Browse files
committed
Simplify filename extraction, use pathlib
1 parent 2ca705b commit fe08263

4 files changed

Lines changed: 73 additions & 73 deletions

File tree

‎Lib/test/libregrtest/setup.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ def setup_tests(runtests: RunTests) -> None:
112112

113113
set_match_tests(runtests.match_tests)
114114

115-
from .testresult import RegressionTestResult
116115
if runtests.use_junit:
116+
from .testresult import RegressionTestResult
117117
support.junit_xml_list = []
118118
RegressionTestResult.USE_XML = True
119119
else:

‎Lib/test/libregrtest/testresult.py‎

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,23 @@
44

55
import functools
66
import io
7-
import re
87
import sys
98
import time
109
import traceback
1110
import unittest
1211
from test import support
1312
from test.libregrtest.utils import print_github_annotation, sanitize_xml
1413

14+
def test_to_filename(test):
15+
test = getattr(test, 'test_case', test) # subTest()
16+
if dt_test := getattr(test, '_dt_test', None): # doctest.DocTestCase
17+
return dt_test.filename
18+
module = type(test).__module__
19+
# Fixture errors (setUpClass, setUpModule) are not supported
20+
if module.startswith('unittest.'):
21+
return None
22+
return getattr(sys.modules.get(module), '__file__', None)
23+
1524
class RegressionTestResult(unittest.TextTestResult):
1625
USE_XML = False
1726

@@ -136,19 +145,9 @@ def printErrorList(self, flavour, errors):
136145
super().printErrorList(flavour, [(test, err)])
137146
# Annotate just after the failure report, so that the annotation
138147
# links to the report in the job log
139-
print_github_annotation(str(test), err, self._test_file(test),
140-
file=self.stream)
141-
142-
@staticmethod
143-
def _test_file(test):
144-
# Test id: "module.Class.method", "module.function" (doctest), or
145-
# "setUpClass (module.Class)" (error in a class or module fixture)
146-
name = getattr(test, 'test_case', test).id() # subTest()
147-
if match := re.fullmatch(r'\w+ \((.+)\)', name):
148-
name = match[1]
149-
while name and name not in sys.modules:
150-
name = name.rpartition('.')[0]
151-
return getattr(sys.modules.get(name), '__file__', None)
148+
if filename := test_to_filename(test):
149+
print_github_annotation(str(test), err, filename,
150+
file=self.stream)
152151

153152
def get_xml_element(self):
154153
if not self.USE_XML:

‎Lib/test/libregrtest/utils.py‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import locale
44
import math
55
import os.path
6+
import pathlib
67
import platform
78
import random
89
import re
@@ -141,16 +142,21 @@ def print_warning(msg: str) -> None:
141142
orig_unraisablehook: Callable[..., None] | None = None
142143

143144

145+
# Stdlib directory: Lib/ in a source checkout or an installed lib/python3.X/
146+
STDLIB_DIR = pathlib.Path(os.__file__).parent
147+
# Stdlib directory relative to the repository
148+
REPOSITORY_LIB_DIR = pathlib.Path("Lib")
149+
150+
144151
def repository_path(filename: str) -> str:
145152
"""Path of a stdlib file relative to the repository.
146153
147-
Map the stdlib directory (Lib/ in a source checkout or an installed
148-
lib/python3.X/) to Lib/. Return other paths unchanged.
154+
Map STDLIB_DIR to Lib/. Return other paths unchanged.
149155
"""
150-
stdlib_dir = os.path.dirname(os.__file__) + os.sep
151-
if not filename.startswith(stdlib_dir):
156+
path = pathlib.Path(filename)
157+
if not path.is_relative_to(STDLIB_DIR):
152158
return filename
153-
return "Lib/" + filename.removeprefix(stdlib_dir).replace(os.sep, "/")
159+
return (REPOSITORY_LIB_DIR / path.relative_to(STDLIB_DIR)).as_posix()
154160

155161

156162
# Escape the data and the properties of GitHub Actions workflow commands

‎Lib/test/test_regrtest.py‎

Lines changed: 48 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)