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.
This commit is contained in:
Serhiy Storchaka 2023-10-25 12:41:21 +03:00 committed by GitHub
parent a8a89fcd1f
commit f6a45a03d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 266 additions and 342 deletions

View File

@ -508,34 +508,6 @@ The :mod:`test.support` module defines the following functions:
Define match patterns on test filenames and test method names for filtering tests.
.. function:: run_unittest(*classes)
Execute :class:`unittest.TestCase` subclasses passed to the function. The
function scans the classes for methods starting with the prefix ``test_``
and executes the tests individually.
It is also legal to pass strings as parameters; these should be keys in
``sys.modules``. Each associated module will be scanned by
``unittest.TestLoader.loadTestsFromModule()``. This is usually seen in the
following :func:`test_main` function::
def test_main():
support.run_unittest(__name__)
This will run all tests defined in the named module.
.. function:: run_doctest(module, verbosity=None, optionflags=0)
Run :func:`doctest.testmod` on the given *module*. Return
``(failure_count, test_count)``.
If *verbosity* is ``None``, :func:`doctest.testmod` is run with verbosity
set to :data:`verbose`. Otherwise, it is run with verbosity set to
``None``. *optionflags* is passed as ``optionflags`` to
:func:`doctest.testmod`.
.. function:: get_pagesize()
Get size of a page in bytes.

View File

@ -0,0 +1,72 @@
import itertools
import operator
import re
# By default, don't filter tests
_test_matchers = ()
_test_patterns = ()
def match_test(test):
# Function used by support.run_unittest() and regrtest --list-cases
result = False
for matcher, result in reversed(_test_matchers):
if matcher(test.id()):
return result
return not result
def _is_full_match_test(pattern):
# If a pattern contains at least one dot, it's considered
# as a full test identifier.
# Example: 'test.test_os.FileTests.test_access'.
#
# ignore patterns which contain fnmatch patterns: '*', '?', '[...]'
# or '[!...]'. For example, ignore 'test_access*'.
return ('.' in pattern) and (not re.search(r'[?*\[\]]', pattern))
def set_match_tests(patterns):
global _test_matchers, _test_patterns
if not patterns:
_test_matchers = ()
_test_patterns = ()
else:
itemgetter = operator.itemgetter
patterns = tuple(patterns)
if patterns != _test_patterns:
_test_matchers = [
(_compile_match_function(map(itemgetter(0), it)), result)
for result, it in itertools.groupby(patterns, itemgetter(1))
]
_test_patterns = patterns
def _compile_match_function(patterns):
patterns = list(patterns)
if all(map(_is_full_match_test, patterns)):
# Simple case: all patterns are full test identifier.
# The test.bisect_cmd utility only uses such full test identifiers.
return set(patterns).__contains__
else:
import fnmatch
regex = '|'.join(map(fnmatch.translate, patterns))
# The search *is* case sensitive on purpose:
# don't use flags=re.IGNORECASE
regex_match = re.compile(regex).match
def match_test_regex(test_id, regex_match=regex_match):
if regex_match(test_id):
# The regex matches the whole identifier, for example
# 'test.test_os.FileTests.test_access'.
return True
else:
# Try to match parts of the test identifier.
# For example, split 'test.test_os.FileTests.test_access'
# into: 'test', 'test_os', 'FileTests' and 'test_access'.
return any(map(regex_match, test_id.split(".")))
return match_test_regex

View File

@ -4,6 +4,7 @@ import unittest
from test import support
from .filter import match_test, set_match_tests
from .utils import (
StrPath, TestName, TestTuple, TestList, TestFilter,
abs_module_name, count, printlist)
@ -79,14 +80,14 @@ def _list_cases(suite):
if isinstance(test, unittest.TestSuite):
_list_cases(test)
elif isinstance(test, unittest.TestCase):
if support.match_test(test):
if match_test(test):
print(test.id())
def list_cases(tests: TestTuple, *,
match_tests: TestFilter | None = None,
test_dir: StrPath | None = None):
support.verbose = False
support.set_match_tests(match_tests)
set_match_tests(match_tests)
skipped = []
for test_name in tests:

View File

@ -2,13 +2,35 @@ import dataclasses
import json
from typing import Any
from test.support import TestStats
from .utils import (
StrJSON, TestName, FilterTuple,
format_duration, normalize_test_name, print_warning)
@dataclasses.dataclass(slots=True)
class TestStats:
tests_run: int = 0
failures: int = 0
skipped: int = 0
@staticmethod
def from_unittest(result):
return TestStats(result.testsRun,
len(result.failures),
len(result.skipped))
@staticmethod
def from_doctest(results):
return TestStats(results.attempted,
results.failed,
results.skipped)
def accumulate(self, stats):
self.tests_run += stats.tests_run
self.failures += stats.failures
self.skipped += stats.skipped
# Avoid enum.Enum to reduce the number of imports when tests are run
class State:
PASSED = "PASSED"

View File

@ -1,8 +1,7 @@
import sys
from test.support import TestStats
from .runtests import RunTests
from .result import State, TestResult
from .result import State, TestResult, TestStats
from .utils import (
StrPath, TestName, TestTuple, TestList, FilterDict,
printlist, count, format_duration)

View File

@ -8,6 +8,7 @@ import unittest
from test import support
from test.support.os_helper import TESTFN_UNDECODABLE, FS_NONASCII
from .filter import set_match_tests
from .runtests import RunTests
from .utils import (
setup_unraisable_hook, setup_threading_excepthook, fix_umask,
@ -92,11 +93,11 @@ def setup_tests(runtests: RunTests):
support.PGO = runtests.pgo
support.PGO_EXTENDED = runtests.pgo_extended
support.set_match_tests(runtests.match_tests)
set_match_tests(runtests.match_tests)
if runtests.use_junit:
support.junit_xml_list = []
from test.support.testresult import RegressionTestResult
from .testresult import RegressionTestResult
RegressionTestResult.USE_XML = True
else:
support.junit_xml_list = None

View File

@ -9,13 +9,14 @@ import traceback
import unittest
from test import support
from test.support import TestStats
from test.support import threading_helper
from .result import State, TestResult
from .filter import match_test
from .result import State, TestResult, TestStats
from .runtests import RunTests
from .save_env import saved_test_environment
from .setup import setup_tests
from .testresult import get_test_runner
from .utils import (
TestName,
clear_caches, remove_testfn, abs_module_name, print_warning)
@ -33,7 +34,47 @@ def run_unittest(test_mod):
print(error, file=sys.stderr)
if loader.errors:
raise Exception("errors while loading tests")
return support.run_unittest(tests)
_filter_suite(tests, match_test)
return _run_suite(tests)
def _filter_suite(suite, pred):
"""Recursively filter test cases in a suite based on a predicate."""
newtests = []
for test in suite._tests:
if isinstance(test, unittest.TestSuite):
_filter_suite(test, pred)
newtests.append(test)
else:
if pred(test):
newtests.append(test)
suite._tests = newtests
def _run_suite(suite):
"""Run tests from a unittest.TestSuite-derived class."""
runner = get_test_runner(sys.stdout,
verbosity=support.verbose,
capture_output=(support.junit_xml_list is not None))
result = runner.run(suite)
if support.junit_xml_list is not None:
support.junit_xml_list.append(result.get_xml_element())
if not result.testsRun and not result.skipped and not result.errors:
raise support.TestDidNotRun
if not result.wasSuccessful():
stats = TestStats.from_unittest(result)
if len(result.errors) == 1 and not result.failures:
err = result.errors[0][1]
elif len(result.failures) == 1 and not result.errors:
err = result.failures[0][1]
else:
err = "multiple errors occurred"
if not verbose: err += "; run in verbose mode for details"
errors = [(str(tc), exc_str) for tc, exc_str in result.errors]
failures = [(str(tc), exc_str) for tc, exc_str in result.failures]
raise support.TestFailedWithDetails(err, errors, failures, stats=stats)
return result
def regrtest_runner(result: TestResult, test_func, runtests: RunTests) -> None:

View File

@ -6,10 +6,8 @@ if __name__ != 'test.support':
import contextlib
import dataclasses
import functools
import itertools
import getpass
import _opcode
import operator
import os
import re
import stat
@ -21,8 +19,6 @@ import types
import unittest
import warnings
from .testresult import get_test_runner
__all__ = [
# globals
@ -36,7 +32,6 @@ __all__ = [
"is_resource_enabled", "requires", "requires_freebsd_version",
"requires_linux_version", "requires_mac_ver",
"check_syntax_error",
"run_unittest", "run_doctest",
"requires_gzip", "requires_bz2", "requires_lzma",
"bigmemtest", "bigaddrspacetest", "cpython_only", "get_attribute",
"requires_IEEE_754", "requires_zlib",
@ -1120,156 +1115,6 @@ def requires_specialization(test):
return unittest.skipUnless(
_opcode.ENABLE_SPECIALIZATION, "requires specialization")(test)
def _filter_suite(suite, pred):
"""Recursively filter test cases in a suite based on a predicate."""
newtests = []
for test in suite._tests:
if isinstance(test, unittest.TestSuite):
_filter_suite(test, pred)
newtests.append(test)
else:
if pred(test):
newtests.append(test)
suite._tests = newtests
@dataclasses.dataclass(slots=True)
class TestStats:
tests_run: int = 0
failures: int = 0
skipped: int = 0
@staticmethod
def from_unittest(result):
return TestStats(result.testsRun,
len(result.failures),
len(result.skipped))
@staticmethod
def from_doctest(results):
return TestStats(results.attempted,
results.failed,
results.skipped)
def accumulate(self, stats):
self.tests_run += stats.tests_run
self.failures += stats.failures
self.skipped += stats.skipped
def _run_suite(suite):
"""Run tests from a unittest.TestSuite-derived class."""
runner = get_test_runner(sys.stdout,
verbosity=verbose,
capture_output=(junit_xml_list is not None))
result = runner.run(suite)
if junit_xml_list is not None:
junit_xml_list.append(result.get_xml_element())
if not result.testsRun and not result.skipped and not result.errors:
raise TestDidNotRun
if not result.wasSuccessful():
stats = TestStats.from_unittest(result)
if len(result.errors) == 1 and not result.failures:
err = result.errors[0][1]
elif len(result.failures) == 1 and not result.errors:
err = result.failures[0][1]
else:
err = "multiple errors occurred"
if not verbose: err += "; run in verbose mode for details"
errors = [(str(tc), exc_str) for tc, exc_str in result.errors]
failures = [(str(tc), exc_str) for tc, exc_str in result.failures]
raise TestFailedWithDetails(err, errors, failures, stats=stats)
return result
# By default, don't filter tests
_test_matchers = ()
_test_patterns = ()
def match_test(test):
# Function used by support.run_unittest() and regrtest --list-cases
result = False
for matcher, result in reversed(_test_matchers):
if matcher(test.id()):
return result
return not result
def _is_full_match_test(pattern):
# If a pattern contains at least one dot, it's considered
# as a full test identifier.
# Example: 'test.test_os.FileTests.test_access'.
#
# ignore patterns which contain fnmatch patterns: '*', '?', '[...]'
# or '[!...]'. For example, ignore 'test_access*'.
return ('.' in pattern) and (not re.search(r'[?*\[\]]', pattern))
def set_match_tests(patterns):
global _test_matchers, _test_patterns
if not patterns:
_test_matchers = ()
_test_patterns = ()
else:
itemgetter = operator.itemgetter
patterns = tuple(patterns)
if patterns != _test_patterns:
_test_matchers = [
(_compile_match_function(map(itemgetter(0), it)), result)
for result, it in itertools.groupby(patterns, itemgetter(1))
]
_test_patterns = patterns
def _compile_match_function(patterns):
patterns = list(patterns)
if all(map(_is_full_match_test, patterns)):
# Simple case: all patterns are full test identifier.
# The test.bisect_cmd utility only uses such full test identifiers.
return set(patterns).__contains__
else:
import fnmatch
regex = '|'.join(map(fnmatch.translate, patterns))
# The search *is* case sensitive on purpose:
# don't use flags=re.IGNORECASE
regex_match = re.compile(regex).match
def match_test_regex(test_id, regex_match=regex_match):
if regex_match(test_id):
# The regex matches the whole identifier, for example
# 'test.test_os.FileTests.test_access'.
return True
else:
# Try to match parts of the test identifier.
# For example, split 'test.test_os.FileTests.test_access'
# into: 'test', 'test_os', 'FileTests' and 'test_access'.
return any(map(regex_match, test_id.split(".")))
return match_test_regex
def run_unittest(*classes):
"""Run tests from unittest.TestCase-derived classes."""
valid_types = (unittest.TestSuite, unittest.TestCase)
loader = unittest.TestLoader()
suite = unittest.TestSuite()
for cls in classes:
if isinstance(cls, str):
if cls in sys.modules:
suite.addTest(loader.loadTestsFromModule(sys.modules[cls]))
else:
raise ValueError("str arguments must be keys in sys.modules")
elif isinstance(cls, valid_types):
suite.addTest(cls)
else:
suite.addTest(loader.loadTestsFromTestCase(cls))
_filter_suite(suite, match_test)
return _run_suite(suite)
#=======================================================================
# Check for the presence of docstrings.
@ -1291,38 +1136,6 @@ requires_docstrings = unittest.skipUnless(HAVE_DOCSTRINGS,
"test requires docstrings")
#=======================================================================
# doctest driver.
def run_doctest(module, verbosity=None, optionflags=0):
"""Run doctest on the given module. Return (#failures, #tests).
If optional argument verbosity is not specified (or is None), pass
support's belief about verbosity on to doctest. Else doctest's
usual behavior is used (it searches sys.argv for -v).
"""
import doctest
if verbosity is None:
verbosity = verbose
else:
verbosity = None
results = doctest.testmod(module,
verbose=verbosity,
optionflags=optionflags)
if results.failed:
stats = TestStats.from_doctest(results)
raise TestFailed(f"{results.failed} of {results.attempted} "
f"doctests failed",
stats=stats)
if verbose:
print('doctest (%s) ... %d tests with zero failures' %
(module.__name__, results.attempted))
return results
#=======================================================================
# Support for saving and restoring the imported modules.

View File

@ -22,11 +22,13 @@ import tempfile
import textwrap
import unittest
from test import support
from test.support import os_helper, TestStats, without_optimizer
from test.support import os_helper, without_optimizer
from test.libregrtest import cmdline
from test.libregrtest import main
from test.libregrtest import setup
from test.libregrtest import utils
from test.libregrtest.filter import set_match_tests, match_test
from test.libregrtest.result import TestStats
from test.libregrtest.utils import normalize_test_name
if not support.has_subprocess_support:
@ -2182,6 +2184,120 @@ class TestUtils(unittest.TestCase):
format_resources((*ALL_RESOURCES, "tzdata")),
'resources: all,tzdata')
def test_match_test(self):
class Test:
def __init__(self, test_id):
self.test_id = test_id
def id(self):
return self.test_id
test_access = Test('test.test_os.FileTests.test_access')
test_chdir = Test('test.test_os.Win32ErrorTests.test_chdir')
test_copy = Test('test.test_shutil.TestCopy.test_copy')
# Test acceptance
with support.swap_attr(support, '_test_matchers', ()):
# match all
set_match_tests([])
self.assertTrue(match_test(test_access))
self.assertTrue(match_test(test_chdir))
# match all using None
set_match_tests(None)
self.assertTrue(match_test(test_access))
self.assertTrue(match_test(test_chdir))
# match the full test identifier
set_match_tests([(test_access.id(), True)])
self.assertTrue(match_test(test_access))
self.assertFalse(match_test(test_chdir))
# match the module name
set_match_tests([('test_os', True)])
self.assertTrue(match_test(test_access))
self.assertTrue(match_test(test_chdir))
self.assertFalse(match_test(test_copy))
# Test '*' pattern
set_match_tests([('test_*', True)])
self.assertTrue(match_test(test_access))
self.assertTrue(match_test(test_chdir))
# Test case sensitivity
set_match_tests([('filetests', True)])
self.assertFalse(match_test(test_access))
set_match_tests([('FileTests', True)])
self.assertTrue(match_test(test_access))
# Test pattern containing '.' and a '*' metacharacter
set_match_tests([('*test_os.*.test_*', True)])
self.assertTrue(match_test(test_access))
self.assertTrue(match_test(test_chdir))
self.assertFalse(match_test(test_copy))
# Multiple patterns
set_match_tests([(test_access.id(), True), (test_chdir.id(), True)])
self.assertTrue(match_test(test_access))
self.assertTrue(match_test(test_chdir))
self.assertFalse(match_test(test_copy))
set_match_tests([('test_access', True), ('DONTMATCH', True)])
self.assertTrue(match_test(test_access))
self.assertFalse(match_test(test_chdir))
# Test rejection
with support.swap_attr(support, '_test_matchers', ()):
# match the full test identifier
set_match_tests([(test_access.id(), False)])
self.assertFalse(match_test(test_access))
self.assertTrue(match_test(test_chdir))
# match the module name
set_match_tests([('test_os', False)])
self.assertFalse(match_test(test_access))
self.assertFalse(match_test(test_chdir))
self.assertTrue(match_test(test_copy))
# Test '*' pattern
set_match_tests([('test_*', False)])
self.assertFalse(match_test(test_access))
self.assertFalse(match_test(test_chdir))
# Test case sensitivity
set_match_tests([('filetests', False)])
self.assertTrue(match_test(test_access))
set_match_tests([('FileTests', False)])
self.assertFalse(match_test(test_access))
# Test pattern containing '.' and a '*' metacharacter
set_match_tests([('*test_os.*.test_*', False)])
self.assertFalse(match_test(test_access))
self.assertFalse(match_test(test_chdir))
self.assertTrue(match_test(test_copy))
# Multiple patterns
set_match_tests([(test_access.id(), False), (test_chdir.id(), False)])
self.assertFalse(match_test(test_access))
self.assertFalse(match_test(test_chdir))
self.assertTrue(match_test(test_copy))
set_match_tests([('test_access', False), ('DONTMATCH', False)])
self.assertFalse(match_test(test_access))
self.assertTrue(match_test(test_chdir))
# Test mixed filters
with support.swap_attr(support, '_test_matchers', ()):
set_match_tests([('*test_os', False), ('test_access', True)])
self.assertTrue(match_test(test_access))
self.assertFalse(match_test(test_chdir))
self.assertTrue(match_test(test_copy))
set_match_tests([('*test_os', True), ('test_access', False)])
self.assertFalse(match_test(test_access))
self.assertTrue(match_test(test_chdir))
self.assertFalse(match_test(test_copy))
if __name__ == '__main__':
unittest.main()

View File

@ -547,120 +547,6 @@ class TestSupport(unittest.TestCase):
with self.subTest(opts=opts):
self.check_options(opts, 'optim_args_from_interpreter_flags')
def test_match_test(self):
class Test:
def __init__(self, test_id):
self.test_id = test_id
def id(self):
return self.test_id
test_access = Test('test.test_os.FileTests.test_access')
test_chdir = Test('test.test_os.Win32ErrorTests.test_chdir')
test_copy = Test('test.test_shutil.TestCopy.test_copy')
# Test acceptance
with support.swap_attr(support, '_test_matchers', ()):
# match all
support.set_match_tests([])
self.assertTrue(support.match_test(test_access))
self.assertTrue(support.match_test(test_chdir))
# match all using None
support.set_match_tests(None)
self.assertTrue(support.match_test(test_access))
self.assertTrue(support.match_test(test_chdir))
# match the full test identifier
support.set_match_tests([(test_access.id(), True)])
self.assertTrue(support.match_test(test_access))
self.assertFalse(support.match_test(test_chdir))
# match the module name
support.set_match_tests([('test_os', True)])
self.assertTrue(support.match_test(test_access))
self.assertTrue(support.match_test(test_chdir))
self.assertFalse(support.match_test(test_copy))
# Test '*' pattern
support.set_match_tests([('test_*', True)])
self.assertTrue(support.match_test(test_access))
self.assertTrue(support.match_test(test_chdir))
# Test case sensitivity
support.set_match_tests([('filetests', True)])
self.assertFalse(support.match_test(test_access))
support.set_match_tests([('FileTests', True)])
self.assertTrue(support.match_test(test_access))
# Test pattern containing '.' and a '*' metacharacter
support.set_match_tests([('*test_os.*.test_*', True)])
self.assertTrue(support.match_test(test_access))
self.assertTrue(support.match_test(test_chdir))
self.assertFalse(support.match_test(test_copy))
# Multiple patterns
support.set_match_tests([(test_access.id(), True), (test_chdir.id(), True)])
self.assertTrue(support.match_test(test_access))
self.assertTrue(support.match_test(test_chdir))
self.assertFalse(support.match_test(test_copy))
support.set_match_tests([('test_access', True), ('DONTMATCH', True)])
self.assertTrue(support.match_test(test_access))
self.assertFalse(support.match_test(test_chdir))
# Test rejection
with support.swap_attr(support, '_test_matchers', ()):
# match the full test identifier
support.set_match_tests([(test_access.id(), False)])
self.assertFalse(support.match_test(test_access))
self.assertTrue(support.match_test(test_chdir))
# match the module name
support.set_match_tests([('test_os', False)])
self.assertFalse(support.match_test(test_access))
self.assertFalse(support.match_test(test_chdir))
self.assertTrue(support.match_test(test_copy))
# Test '*' pattern
support.set_match_tests([('test_*', False)])
self.assertFalse(support.match_test(test_access))
self.assertFalse(support.match_test(test_chdir))
# Test case sensitivity
support.set_match_tests([('filetests', False)])
self.assertTrue(support.match_test(test_access))
support.set_match_tests([('FileTests', False)])
self.assertFalse(support.match_test(test_access))
# Test pattern containing '.' and a '*' metacharacter
support.set_match_tests([('*test_os.*.test_*', False)])
self.assertFalse(support.match_test(test_access))
self.assertFalse(support.match_test(test_chdir))
self.assertTrue(support.match_test(test_copy))
# Multiple patterns
support.set_match_tests([(test_access.id(), False), (test_chdir.id(), False)])
self.assertFalse(support.match_test(test_access))
self.assertFalse(support.match_test(test_chdir))
self.assertTrue(support.match_test(test_copy))
support.set_match_tests([('test_access', False), ('DONTMATCH', False)])
self.assertFalse(support.match_test(test_access))
self.assertTrue(support.match_test(test_chdir))
# Test mixed filters
with support.swap_attr(support, '_test_matchers', ()):
support.set_match_tests([('*test_os', False), ('test_access', True)])
self.assertTrue(support.match_test(test_access))
self.assertFalse(support.match_test(test_chdir))
self.assertTrue(support.match_test(test_copy))
support.set_match_tests([('*test_os', True), ('test_access', False)])
self.assertFalse(support.match_test(test_access))
self.assertTrue(support.match_test(test_chdir))
self.assertFalse(support.match_test(test_copy))
@unittest.skipIf(support.is_emscripten, "Unstable in Emscripten")
@unittest.skipIf(support.is_wasi, "Unavailable on WASI")
def test_fd_count(self):
@ -861,7 +747,6 @@ class TestSupport(unittest.TestCase):
# precisionbigmemtest
# bigaddrspacetest
# requires_resource
# run_doctest
# threading_cleanup
# reap_threads
# can_symlink

View File

@ -0,0 +1,2 @@
Remove no longer used functions ``run_unittest()`` and ``run_doctest()``
from the :mod:`test.support` module.