Skip to content

Commit f6a45a0

Browse files
gh-111165: Move test running code from test.support to libregrtest (GH-111166)
Remove no longer used functions run_unittest() and run_doctest() from the test.support module.
1 parent a8a89fc commit f6a45a0

File tree

12 files changed

+266
-342
lines changed

12 files changed

+266
-342
lines changed

Doc/library/test.rst

-28
Original file line numberDiff line numberDiff line change
@@ -508,34 +508,6 @@ The :mod:`test.support` module defines the following functions:
508508
Define match patterns on test filenames and test method names for filtering tests.
509509

510510

511-
.. function:: run_unittest(*classes)
512-
513-
Execute :class:`unittest.TestCase` subclasses passed to the function. The
514-
function scans the classes for methods starting with the prefix ``test_``
515-
and executes the tests individually.
516-
517-
It is also legal to pass strings as parameters; these should be keys in
518-
``sys.modules``. Each associated module will be scanned by
519-
``unittest.TestLoader.loadTestsFromModule()``. This is usually seen in the
520-
following :func:`test_main` function::
521-
522-
def test_main():
523-
support.run_unittest(__name__)
524-
525-
This will run all tests defined in the named module.
526-
527-
528-
.. function:: run_doctest(module, verbosity=None, optionflags=0)
529-
530-
Run :func:`doctest.testmod` on the given *module*. Return
531-
``(failure_count, test_count)``.
532-
533-
If *verbosity* is ``None``, :func:`doctest.testmod` is run with verbosity
534-
set to :data:`verbose`. Otherwise, it is run with verbosity set to
535-
``None``. *optionflags* is passed as ``optionflags`` to
536-
:func:`doctest.testmod`.
537-
538-
539511
.. function:: get_pagesize()
540512

541513
Get size of a page in bytes.

Lib/test/libregrtest/filter.py

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import itertools
2+
import operator
3+
import re
4+
5+
6+
# By default, don't filter tests
7+
_test_matchers = ()
8+
_test_patterns = ()
9+
10+
11+
def match_test(test):
12+
# Function used by support.run_unittest() and regrtest --list-cases
13+
result = False
14+
for matcher, result in reversed(_test_matchers):
15+
if matcher(test.id()):
16+
return result
17+
return not result
18+
19+
20+
def _is_full_match_test(pattern):
21+
# If a pattern contains at least one dot, it's considered
22+
# as a full test identifier.
23+
# Example: 'test.test_os.FileTests.test_access'.
24+
#
25+
# ignore patterns which contain fnmatch patterns: '*', '?', '[...]'
26+
# or '[!...]'. For example, ignore 'test_access*'.
27+
return ('.' in pattern) and (not re.search(r'[?*\[\]]', pattern))
28+
29+
30+
def set_match_tests(patterns):
31+
global _test_matchers, _test_patterns
32+
33+
if not patterns:
34+
_test_matchers = ()
35+
_test_patterns = ()
36+
else:
37+
itemgetter = operator.itemgetter
38+
patterns = tuple(patterns)
39+
if patterns != _test_patterns:
40+
_test_matchers = [
41+
(_compile_match_function(map(itemgetter(0), it)), result)
42+
for result, it in itertools.groupby(patterns, itemgetter(1))
43+
]
44+
_test_patterns = patterns
45+
46+
47+
def _compile_match_function(patterns):
48+
patterns = list(patterns)
49+
50+
if all(map(_is_full_match_test, patterns)):
51+
# Simple case: all patterns are full test identifier.
52+
# The test.bisect_cmd utility only uses such full test identifiers.
53+
return set(patterns).__contains__
54+
else:
55+
import fnmatch
56+
regex = '|'.join(map(fnmatch.translate, patterns))
57+
# The search *is* case sensitive on purpose:
58+
# don't use flags=re.IGNORECASE
59+
regex_match = re.compile(regex).match
60+
61+
def match_test_regex(test_id, regex_match=regex_match):
62+
if regex_match(test_id):
63+
# The regex matches the whole identifier, for example
64+
# 'test.test_os.FileTests.test_access'.
65+
return True
66+
else:
67+
# Try to match parts of the test identifier.
68+
# For example, split 'test.test_os.FileTests.test_access'
69+
# into: 'test', 'test_os', 'FileTests' and 'test_access'.
70+
return any(map(regex_match, test_id.split(".")))
71+
72+
return match_test_regex

Lib/test/libregrtest/findtests.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from test import support
66

7+
from .filter import match_test, set_match_tests
78
from .utils import (
89
StrPath, TestName, TestTuple, TestList, TestFilter,
910
abs_module_name, count, printlist)
@@ -79,14 +80,14 @@ def _list_cases(suite):
7980
if isinstance(test, unittest.TestSuite):
8081
_list_cases(test)
8182
elif isinstance(test, unittest.TestCase):
82-
if support.match_test(test):
83+
if match_test(test):
8384
print(test.id())
8485

8586
def list_cases(tests: TestTuple, *,
8687
match_tests: TestFilter | None = None,
8788
test_dir: StrPath | None = None):
8889
support.verbose = False
89-
support.set_match_tests(match_tests)
90+
set_match_tests(match_tests)
9091

9192
skipped = []
9293
for test_name in tests:

Lib/test/libregrtest/result.py

+24-2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,35 @@
22
import json
33
from typing import Any
44

5-
from test.support import TestStats
6-
75
from .utils import (
86
StrJSON, TestName, FilterTuple,
97
format_duration, normalize_test_name, print_warning)
108

119

10+
@dataclasses.dataclass(slots=True)
11+
class TestStats:
12+
tests_run: int = 0
13+
failures: int = 0
14+
skipped: int = 0
15+
16+
@staticmethod
17+
def from_unittest(result):
18+
return TestStats(result.testsRun,
19+
len(result.failures),
20+
len(result.skipped))
21+
22+
@staticmethod
23+
def from_doctest(results):
24+
return TestStats(results.attempted,
25+
results.failed,
26+
results.skipped)
27+
28+
def accumulate(self, stats):
29+
self.tests_run += stats.tests_run
30+
self.failures += stats.failures
31+
self.skipped += stats.skipped
32+
33+
1234
# Avoid enum.Enum to reduce the number of imports when tests are run
1335
class State:
1436
PASSED = "PASSED"

Lib/test/libregrtest/results.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import sys
2-
from test.support import TestStats
32

43
from .runtests import RunTests
5-
from .result import State, TestResult
4+
from .result import State, TestResult, TestStats
65
from .utils import (
76
StrPath, TestName, TestTuple, TestList, FilterDict,
87
printlist, count, format_duration)

Lib/test/libregrtest/setup.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from test import support
99
from test.support.os_helper import TESTFN_UNDECODABLE, FS_NONASCII
1010

11+
from .filter import set_match_tests
1112
from .runtests import RunTests
1213
from .utils import (
1314
setup_unraisable_hook, setup_threading_excepthook, fix_umask,
@@ -92,11 +93,11 @@ def setup_tests(runtests: RunTests):
9293
support.PGO = runtests.pgo
9394
support.PGO_EXTENDED = runtests.pgo_extended
9495

95-
support.set_match_tests(runtests.match_tests)
96+
set_match_tests(runtests.match_tests)
9697

9798
if runtests.use_junit:
9899
support.junit_xml_list = []
99-
from test.support.testresult import RegressionTestResult
100+
from .testresult import RegressionTestResult
100101
RegressionTestResult.USE_XML = True
101102
else:
102103
support.junit_xml_list = None

Lib/test/libregrtest/single.py

+44-3
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@
99
import unittest
1010

1111
from test import support
12-
from test.support import TestStats
1312
from test.support import threading_helper
1413

15-
from .result import State, TestResult
14+
from .filter import match_test
15+
from .result import State, TestResult, TestStats
1616
from .runtests import RunTests
1717
from .save_env import saved_test_environment
1818
from .setup import setup_tests
19+
from .testresult import get_test_runner
1920
from .utils import (
2021
TestName,
2122
clear_caches, remove_testfn, abs_module_name, print_warning)
@@ -33,7 +34,47 @@ def run_unittest(test_mod):
3334
print(error, file=sys.stderr)
3435
if loader.errors:
3536
raise Exception("errors while loading tests")
36-
return support.run_unittest(tests)
37+
_filter_suite(tests, match_test)
38+
return _run_suite(tests)
39+
40+
def _filter_suite(suite, pred):
41+
"""Recursively filter test cases in a suite based on a predicate."""
42+
newtests = []
43+
for test in suite._tests:
44+
if isinstance(test, unittest.TestSuite):
45+
_filter_suite(test, pred)
46+
newtests.append(test)
47+
else:
48+
if pred(test):
49+
newtests.append(test)
50+
suite._tests = newtests
51+
52+
def _run_suite(suite):
53+
"""Run tests from a unittest.TestSuite-derived class."""
54+
runner = get_test_runner(sys.stdout,
55+
verbosity=support.verbose,
56+
capture_output=(support.junit_xml_list is not None))
57+
58+
result = runner.run(suite)
59+
60+
if support.junit_xml_list is not None:
61+
support.junit_xml_list.append(result.get_xml_element())
62+
63+
if not result.testsRun and not result.skipped and not result.errors:
64+
raise support.TestDidNotRun
65+
if not result.wasSuccessful():
66+
stats = TestStats.from_unittest(result)
67+
if len(result.errors) == 1 and not result.failures:
68+
err = result.errors[0][1]
69+
elif len(result.failures) == 1 and not result.errors:
70+
err = result.failures[0][1]
71+
else:
72+
err = "multiple errors occurred"
73+
if not verbose: err += "; run in verbose mode for details"
74+
errors = [(str(tc), exc_str) for tc, exc_str in result.errors]
75+
failures = [(str(tc), exc_str) for tc, exc_str in result.failures]
76+
raise support.TestFailedWithDetails(err, errors, failures, stats=stats)
77+
return result
3778

3879

3980
def regrtest_runner(result: TestResult, test_func, runtests: RunTests) -> None:
File renamed without changes.

0 commit comments

Comments
 (0)