bpo-32071: Add unittest -k option (#4496)

* bpo-32071: Add unittest -k option
This commit is contained in:
Jonas Haag 2017-11-25 16:23:52 +01:00 committed by Antoine Pitrou
parent 8d9bb11d8f
commit 5b48dc638b
6 changed files with 126 additions and 14 deletions

View File

@ -219,6 +219,22 @@ Command-line options
Stop the test run on the first error or failure.
.. cmdoption:: -k
Only run test methods and classes that match the pattern or substring.
This option may be used multiple times, in which case all test cases that
match of the given patterns are included.
Patterns that contain a wildcard character (``*``) are matched against the
test name using :meth:`fnmatch.fnmatchcase`; otherwise simple case-sensitive
substring matching is used.
Patterns are matched against the fully qualified test method name as
imported by the test loader.
For example, ``-k foo`` matches ``foo_tests.SomeTest.test_something``,
``bar_tests.SomeTest.test_foo``, but not ``bar_tests.FooTest.test_something``.
.. cmdoption:: --locals
Show local variables in tracebacks.
@ -229,6 +245,9 @@ Command-line options
.. versionadded:: 3.5
The command-line option ``--locals``.
.. versionadded:: 3.7
The command-line option ``-k``.
The command line can also be used for test discovery, for running all of the
tests in a project or just a subset.
@ -1745,6 +1764,21 @@ Loading and running tests
This affects all the :meth:`loadTestsFrom\*` methods.
.. attribute:: testNamePatterns
List of Unix shell-style wildcard test name patterns that test methods
have to match to be included in test suites (see ``-v`` option).
If this attribute is not ``None`` (the default), all test methods to be
included in test suites must match one of the patterns in this list.
Note that matches are always performed using :meth:`fnmatch.fnmatchcase`,
so unlike patterns passed to the ``-v`` option, simple substring patterns
will have to be converted using ``*`` wildcards.
This affects all the :meth:`loadTestsFrom\*` methods.
.. versionadded:: 3.7
.. class:: TestResult

View File

@ -8,7 +8,7 @@ import types
import functools
import warnings
from fnmatch import fnmatch
from fnmatch import fnmatch, fnmatchcase
from . import case, suite, util
@ -70,6 +70,7 @@ class TestLoader(object):
"""
testMethodPrefix = 'test'
sortTestMethodsUsing = staticmethod(util.three_way_cmp)
testNamePatterns = None
suiteClass = suite.TestSuite
_top_level_dir = None
@ -222,11 +223,15 @@ class TestLoader(object):
def getTestCaseNames(self, testCaseClass):
"""Return a sorted sequence of method names found within testCaseClass
"""
def isTestMethod(attrname, testCaseClass=testCaseClass,
prefix=self.testMethodPrefix):
return attrname.startswith(prefix) and \
callable(getattr(testCaseClass, attrname))
testFnNames = list(filter(isTestMethod, dir(testCaseClass)))
def shouldIncludeMethod(attrname):
testFunc = getattr(testCaseClass, attrname)
isTestMethod = attrname.startswith(self.testMethodPrefix) and callable(testFunc)
if not isTestMethod:
return False
fullName = '%s.%s' % (testCaseClass.__module__, testFunc.__qualname__)
return self.testNamePatterns is None or \
any(fnmatchcase(fullName, pattern) for pattern in self.testNamePatterns)
testFnNames = list(filter(shouldIncludeMethod, dir(testCaseClass)))
if self.sortTestMethodsUsing:
testFnNames.sort(key=functools.cmp_to_key(self.sortTestMethodsUsing))
return testFnNames
@ -486,16 +491,17 @@ class TestLoader(object):
defaultTestLoader = TestLoader()
def _makeLoader(prefix, sortUsing, suiteClass=None):
def _makeLoader(prefix, sortUsing, suiteClass=None, testNamePatterns=None):
loader = TestLoader()
loader.sortTestMethodsUsing = sortUsing
loader.testMethodPrefix = prefix
loader.testNamePatterns = testNamePatterns
if suiteClass:
loader.suiteClass = suiteClass
return loader
def getTestCaseNames(testCaseClass, prefix, sortUsing=util.three_way_cmp):
return _makeLoader(prefix, sortUsing).getTestCaseNames(testCaseClass)
def getTestCaseNames(testCaseClass, prefix, sortUsing=util.three_way_cmp, testNamePatterns=None):
return _makeLoader(prefix, sortUsing, testNamePatterns=testNamePatterns).getTestCaseNames(testCaseClass)
def makeSuite(testCaseClass, prefix='test', sortUsing=util.three_way_cmp,
suiteClass=suite.TestSuite):

View File

@ -46,6 +46,12 @@ def _convert_names(names):
return [_convert_name(name) for name in names]
def _convert_select_pattern(pattern):
if not '*' in pattern:
pattern = '*%s*' % pattern
return pattern
class TestProgram(object):
"""A command-line program that runs a set of tests; this is primarily
for making test modules conveniently executable.
@ -53,7 +59,7 @@ class TestProgram(object):
# defaults for testing
module=None
verbosity = 1
failfast = catchbreak = buffer = progName = warnings = None
failfast = catchbreak = buffer = progName = warnings = testNamePatterns = None
_discovery_parser = None
def __init__(self, module='__main__', defaultTest=None, argv=None,
@ -140,8 +146,13 @@ class TestProgram(object):
self.testNames = list(self.defaultTest)
self.createTests()
def createTests(self):
if self.testNames is None:
def createTests(self, from_discovery=False, Loader=None):
if self.testNamePatterns:
self.testLoader.testNamePatterns = self.testNamePatterns
if from_discovery:
loader = self.testLoader if Loader is None else Loader()
self.test = loader.discover(self.start, self.pattern, self.top)
elif self.testNames is None:
self.test = self.testLoader.loadTestsFromModule(self.module)
else:
self.test = self.testLoader.loadTestsFromNames(self.testNames,
@ -179,6 +190,11 @@ class TestProgram(object):
action='store_true',
help='Buffer stdout and stderr during tests')
self.buffer = False
if self.testNamePatterns is None:
parser.add_argument('-k', dest='testNamePatterns',
action='append', type=_convert_select_pattern,
help='Only run tests which match the given substring')
self.testNamePatterns = []
return parser
@ -225,8 +241,7 @@ class TestProgram(object):
self._initArgParsers()
self._discovery_parser.parse_args(argv, self)
loader = self.testLoader if Loader is None else Loader()
self.test = loader.discover(self.start, self.pattern, self.top)
self.createTests(from_discovery=True, Loader=Loader)
def runTests(self):
if self.catchbreak:

View File

@ -1226,6 +1226,33 @@ class Test_TestLoader(unittest.TestCase):
names = ['test_1', 'test_2', 'test_3']
self.assertEqual(loader.getTestCaseNames(TestC), names)
# "Return a sorted sequence of method names found within testCaseClass"
#
# If TestLoader.testNamePatterns is set, only tests that match one of these
# patterns should be included.
def test_getTestCaseNames__testNamePatterns(self):
class MyTest(unittest.TestCase):
def test_1(self): pass
def test_2(self): pass
def foobar(self): pass
loader = unittest.TestLoader()
loader.testNamePatterns = []
self.assertEqual(loader.getTestCaseNames(MyTest), [])
loader.testNamePatterns = ['*1']
self.assertEqual(loader.getTestCaseNames(MyTest), ['test_1'])
loader.testNamePatterns = ['*1', '*2']
self.assertEqual(loader.getTestCaseNames(MyTest), ['test_1', 'test_2'])
loader.testNamePatterns = ['*My*']
self.assertEqual(loader.getTestCaseNames(MyTest), ['test_1', 'test_2'])
loader.testNamePatterns = ['*my*']
self.assertEqual(loader.getTestCaseNames(MyTest), [])
################################################################
### /Tests for TestLoader.getTestCaseNames()

View File

@ -2,6 +2,7 @@ import io
import os
import sys
import subprocess
from test import support
import unittest
import unittest.test
@ -409,6 +410,33 @@ class TestCommandLineArgs(unittest.TestCase):
# for invalid filenames should we raise a useful error rather than
# leaving the current error message (import of filename fails) in place?
def testParseArgsSelectedTestNames(self):
program = self.program
argv = ['progname', '-k', 'foo', '-k', 'bar', '-k', '*pat*']
program.createTests = lambda: None
program.parseArgs(argv)
self.assertEqual(program.testNamePatterns, ['*foo*', '*bar*', '*pat*'])
def testSelectedTestNamesFunctionalTest(self):
def run_unittest(args):
p = subprocess.Popen([sys.executable, '-m', 'unittest'] + args,
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, cwd=os.path.dirname(__file__))
with p:
_, stderr = p.communicate()
return stderr.decode()
t = '_test_warnings'
self.assertIn('Ran 7 tests', run_unittest([t]))
self.assertIn('Ran 7 tests', run_unittest(['-k', 'TestWarnings', t]))
self.assertIn('Ran 7 tests', run_unittest(['discover', '-p', '*_test*', '-k', 'TestWarnings']))
self.assertIn('Ran 2 tests', run_unittest(['-k', 'f', t]))
self.assertIn('Ran 7 tests', run_unittest(['-k', 't', t]))
self.assertIn('Ran 3 tests', run_unittest(['-k', '*t', t]))
self.assertIn('Ran 7 tests', run_unittest(['-k', '*test_warnings.*Warning*', t]))
self.assertIn('Ran 1 test', run_unittest(['-k', '*test_warnings.*warning*', t]))
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,2 @@
Added the ``-k`` command-line option to ``python -m unittest`` to run only
tests that match the given pattern(s).