summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVictor Stinner <victor.stinner@gmail.com>2017-11-21 23:34:02 (GMT)
committerGitHub <noreply@github.com>2017-11-21 23:34:02 (GMT)
commit803ddd8ce22f0de3ab42fb98a225a704c000ef06 (patch)
treea4193e3e3380dbe654b556042f4a6fc00f48a97e
parent431665bf1971e66c51f59abf0693f700ff7919e8 (diff)
downloadcpython-803ddd8ce22f0de3ab42fb98a225a704c000ef06.zip
cpython-803ddd8ce22f0de3ab42fb98a225a704c000ef06.tar.gz
cpython-803ddd8ce22f0de3ab42fb98a225a704c000ef06.tar.bz2
bpo-31324: Optimize support._match_test() (#4421)
* Rename support._match_test() to support.match_test(): make it public * Remove support.match_tests global variable. It is replaced with a new support.set_match_tests() function, so match_test() doesn't have to check each time if patterns were modified. * Rewrite match_test(): use different code paths depending on the kind of patterns for best performances. Co-Authored-By: Serhiy Storchaka <storchaka@gmail.com>
-rw-r--r--Lib/test/libregrtest/main.py4
-rw-r--r--Lib/test/libregrtest/runtest.py2
-rw-r--r--Lib/test/support/__init__.py67
-rw-r--r--Lib/test/test_support.py53
4 files changed, 111 insertions, 15 deletions
diff --git a/Lib/test/libregrtest/main.py b/Lib/test/libregrtest/main.py
index 9871a28..ce01c8c 100644
--- a/Lib/test/libregrtest/main.py
+++ b/Lib/test/libregrtest/main.py
@@ -257,12 +257,12 @@ class Regrtest:
if isinstance(test, unittest.TestSuite):
self._list_cases(test)
elif isinstance(test, unittest.TestCase):
- if support._match_test(test):
+ if support.match_test(test):
print(test.id())
def list_cases(self):
support.verbose = False
- support.match_tests = self.ns.match_tests
+ support.set_match_tests(self.ns.match_tests)
for test in self.selected:
abstest = get_abs_module(self.ns, test)
diff --git a/Lib/test/libregrtest/runtest.py b/Lib/test/libregrtest/runtest.py
index dbd4634..12bf422 100644
--- a/Lib/test/libregrtest/runtest.py
+++ b/Lib/test/libregrtest/runtest.py
@@ -102,7 +102,7 @@ def runtest(ns, test):
if use_timeout:
faulthandler.dump_traceback_later(ns.timeout, exit=True)
try:
- support.match_tests = ns.match_tests
+ support.set_match_tests(ns.match_tests)
# reset the environment_altered flag to detect if a test altered
# the environment
support.environment_altered = False
diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
index 527cf7f..71d9c2c 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -278,7 +278,6 @@ max_memuse = 0 # Disable bigmem tests (they will still be run with
# small sizes, to make sure they work.)
real_max_memuse = 0
failfast = False
-match_tests = None
# _original_stdout is meant to hold stdout at the time regrtest began.
# This may be "the real" stdout, or IDLE's emulation of stdout, or whatever.
@@ -1900,21 +1899,65 @@ def _run_suite(suite):
raise TestFailed(err)
-def _match_test(test):
- global match_tests
+# By default, don't filter tests
+_match_test_func = None
+_match_test_patterns = None
- if match_tests is None:
+
+def match_test(test):
+ # Function used by support.run_unittest() and regrtest --list-cases
+ if _match_test_func is None:
return True
- test_id = test.id()
+ else:
+ return _match_test_func(test.id())
- for match_test in match_tests:
- if fnmatch.fnmatchcase(test_id, match_test):
- return True
- for name in test_id.split("."):
- if fnmatch.fnmatchcase(name, match_test):
+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'.
+ #
+ # Reject patterns which contain fnmatch patterns: '*', '?', '[...]'
+ # or '[!...]'. For example, reject 'test_access*'.
+ return ('.' in pattern) and (not re.search(r'[?*\[\]]', pattern))
+
+
+def set_match_tests(patterns):
+ global _match_test_func, _match_test_patterns
+
+ if patterns == _match_test_patterns:
+ # No change: no need to recompile patterns.
+ return
+
+ if not patterns:
+ func = None
+ elif all(map(_is_full_match_test, patterns)):
+ # Simple case: all patterns are full test identifier.
+ # The test.bisect utility only uses such full test identifiers.
+ func = set(patterns).__contains__
+ else:
+ 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):
+ if regex_match(test_id):
+ # The regex matchs the whole identifier like
+ # 'test.test_os.FileTests.test_access'
return True
- return False
+ 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(".")))
+
+ func = match_test_regex
+
+ # Create a copy since patterns can be mutable and so modified later
+ _match_test_patterns = tuple(patterns)
+ _match_test_func = func
+
def run_unittest(*classes):
@@ -1931,7 +1974,7 @@ def run_unittest(*classes):
suite.addTest(cls)
else:
suite.addTest(unittest.makeSuite(cls))
- _filter_suite(suite, _match_test)
+ _filter_suite(suite, match_test)
_run_suite(suite)
#=======================================================================
diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py
index 4a577ef..4756def 100644
--- a/Lib/test/test_support.py
+++ b/Lib/test/test_support.py
@@ -483,6 +483,59 @@ 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')
+
+ with support.swap_attr(support, '_match_test_func', None):
+ # match all
+ support.set_match_tests([])
+ 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()])
+ self.assertTrue(support.match_test(test_access))
+ self.assertFalse(support.match_test(test_chdir))
+
+ # match the module name
+ support.set_match_tests(['test_os'])
+ self.assertTrue(support.match_test(test_access))
+ self.assertTrue(support.match_test(test_chdir))
+
+ # Test '*' pattern
+ support.set_match_tests(['test_*'])
+ self.assertTrue(support.match_test(test_access))
+ self.assertTrue(support.match_test(test_chdir))
+
+ # Test case sensitivity
+ support.set_match_tests(['filetests'])
+ self.assertFalse(support.match_test(test_access))
+ support.set_match_tests(['FileTests'])
+ self.assertTrue(support.match_test(test_access))
+
+ # Test pattern containing '.' and a '*' metacharacter
+ support.set_match_tests(['*test_os.*.test_*'])
+ self.assertTrue(support.match_test(test_access))
+ self.assertTrue(support.match_test(test_chdir))
+
+ # Multiple patterns
+ support.set_match_tests([test_access.id(), test_chdir.id()])
+ self.assertTrue(support.match_test(test_access))
+ self.assertTrue(support.match_test(test_chdir))
+
+ support.set_match_tests(['test_access', 'DONTMATCH'])
+ self.assertTrue(support.match_test(test_access))
+ self.assertFalse(support.match_test(test_chdir))
+
+
# XXX -follows a list of untested API
# make_legacy_pyc
# is_resource_enabled