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. 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 .. cmdoption:: --locals
Show local variables in tracebacks. Show local variables in tracebacks.
@ -229,6 +245,9 @@ Command-line options
.. versionadded:: 3.5 .. versionadded:: 3.5
The command-line option ``--locals``. 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 The command line can also be used for test discovery, for running all of the
tests in a project or just a subset. tests in a project or just a subset.
@ -1745,6 +1764,21 @@ Loading and running tests
This affects all the :meth:`loadTestsFrom\*` methods. 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 .. class:: TestResult

View File

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

View File

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

View File

@ -1226,6 +1226,33 @@ class Test_TestLoader(unittest.TestCase):
names = ['test_1', 'test_2', 'test_3'] names = ['test_1', 'test_2', 'test_3']
self.assertEqual(loader.getTestCaseNames(TestC), names) 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() ### /Tests for TestLoader.getTestCaseNames()

View File

@ -2,6 +2,7 @@ import io
import os import os
import sys import sys
import subprocess
from test import support from test import support
import unittest import unittest
import unittest.test import unittest.test
@ -409,6 +410,33 @@ class TestCommandLineArgs(unittest.TestCase):
# for invalid filenames should we raise a useful error rather than # for invalid filenames should we raise a useful error rather than
# leaving the current error message (import of filename fails) in place? # 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__': if __name__ == '__main__':
unittest.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).