mirror of https://github.com/python/cpython
gh-62432: unittest runner: Exit code 5 if no tests were run (#102051)
As discussed in https://discuss.python.org/t/unittest-fail-if-zero-tests-were-discovered/21498/7 It is common for test runner misconfiguration to fail to find any tests, This should be an error. Fixes: #62432
This commit is contained in:
parent
dc3f97549a
commit
76632b836c
|
@ -2281,7 +2281,8 @@ Loading and running tests
|
||||||
|
|
||||||
The *testRunner* argument can either be a test runner class or an already
|
The *testRunner* argument can either be a test runner class or an already
|
||||||
created instance of it. By default ``main`` calls :func:`sys.exit` with
|
created instance of it. By default ``main`` calls :func:`sys.exit` with
|
||||||
an exit code indicating success or failure of the tests run.
|
an exit code indicating success (0) or failure (1) of the tests run.
|
||||||
|
An exit code of 5 indicates that no tests were run.
|
||||||
|
|
||||||
The *testLoader* argument has to be a :class:`TestLoader` instance,
|
The *testLoader* argument has to be a :class:`TestLoader` instance,
|
||||||
and defaults to :data:`defaultTestLoader`.
|
and defaults to :data:`defaultTestLoader`.
|
||||||
|
|
|
@ -71,15 +71,22 @@ class Test_TestProgram(unittest.TestCase):
|
||||||
def testUnexpectedSuccess(self):
|
def testUnexpectedSuccess(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class FooBarLoader(unittest.TestLoader):
|
class Empty(unittest.TestCase):
|
||||||
"""Test loader that returns a suite containing FooBar."""
|
pass
|
||||||
|
|
||||||
|
class TestLoader(unittest.TestLoader):
|
||||||
|
"""Test loader that returns a suite containing the supplied testcase."""
|
||||||
|
|
||||||
|
def __init__(self, testcase):
|
||||||
|
self.testcase = testcase
|
||||||
|
|
||||||
def loadTestsFromModule(self, module):
|
def loadTestsFromModule(self, module):
|
||||||
return self.suiteClass(
|
return self.suiteClass(
|
||||||
[self.loadTestsFromTestCase(Test_TestProgram.FooBar)])
|
[self.loadTestsFromTestCase(self.testcase)])
|
||||||
|
|
||||||
def loadTestsFromNames(self, names, module):
|
def loadTestsFromNames(self, names, module):
|
||||||
return self.suiteClass(
|
return self.suiteClass(
|
||||||
[self.loadTestsFromTestCase(Test_TestProgram.FooBar)])
|
[self.loadTestsFromTestCase(self.testcase)])
|
||||||
|
|
||||||
def test_defaultTest_with_string(self):
|
def test_defaultTest_with_string(self):
|
||||||
class FakeRunner(object):
|
class FakeRunner(object):
|
||||||
|
@ -92,7 +99,7 @@ class Test_TestProgram(unittest.TestCase):
|
||||||
runner = FakeRunner()
|
runner = FakeRunner()
|
||||||
program = unittest.TestProgram(testRunner=runner, exit=False,
|
program = unittest.TestProgram(testRunner=runner, exit=False,
|
||||||
defaultTest='test.test_unittest',
|
defaultTest='test.test_unittest',
|
||||||
testLoader=self.FooBarLoader())
|
testLoader=self.TestLoader(self.FooBar))
|
||||||
sys.argv = old_argv
|
sys.argv = old_argv
|
||||||
self.assertEqual(('test.test_unittest',), program.testNames)
|
self.assertEqual(('test.test_unittest',), program.testNames)
|
||||||
|
|
||||||
|
@ -108,7 +115,7 @@ class Test_TestProgram(unittest.TestCase):
|
||||||
program = unittest.TestProgram(
|
program = unittest.TestProgram(
|
||||||
testRunner=runner, exit=False,
|
testRunner=runner, exit=False,
|
||||||
defaultTest=['test.test_unittest', 'test.test_unittest2'],
|
defaultTest=['test.test_unittest', 'test.test_unittest2'],
|
||||||
testLoader=self.FooBarLoader())
|
testLoader=self.TestLoader(self.FooBar))
|
||||||
sys.argv = old_argv
|
sys.argv = old_argv
|
||||||
self.assertEqual(['test.test_unittest', 'test.test_unittest2'],
|
self.assertEqual(['test.test_unittest', 'test.test_unittest2'],
|
||||||
program.testNames)
|
program.testNames)
|
||||||
|
@ -118,7 +125,7 @@ class Test_TestProgram(unittest.TestCase):
|
||||||
program = unittest.main(exit=False,
|
program = unittest.main(exit=False,
|
||||||
argv=["foobar"],
|
argv=["foobar"],
|
||||||
testRunner=unittest.TextTestRunner(stream=stream),
|
testRunner=unittest.TextTestRunner(stream=stream),
|
||||||
testLoader=self.FooBarLoader())
|
testLoader=self.TestLoader(self.FooBar))
|
||||||
self.assertTrue(hasattr(program, 'result'))
|
self.assertTrue(hasattr(program, 'result'))
|
||||||
out = stream.getvalue()
|
out = stream.getvalue()
|
||||||
self.assertIn('\nFAIL: testFail ', out)
|
self.assertIn('\nFAIL: testFail ', out)
|
||||||
|
@ -130,13 +137,13 @@ class Test_TestProgram(unittest.TestCase):
|
||||||
|
|
||||||
def test_Exit(self):
|
def test_Exit(self):
|
||||||
stream = BufferedWriter()
|
stream = BufferedWriter()
|
||||||
self.assertRaises(
|
with self.assertRaises(SystemExit) as cm:
|
||||||
SystemExit,
|
unittest.main(
|
||||||
unittest.main,
|
argv=["foobar"],
|
||||||
argv=["foobar"],
|
testRunner=unittest.TextTestRunner(stream=stream),
|
||||||
testRunner=unittest.TextTestRunner(stream=stream),
|
exit=True,
|
||||||
exit=True,
|
testLoader=self.TestLoader(self.FooBar))
|
||||||
testLoader=self.FooBarLoader())
|
self.assertEqual(cm.exception.code, 1)
|
||||||
out = stream.getvalue()
|
out = stream.getvalue()
|
||||||
self.assertIn('\nFAIL: testFail ', out)
|
self.assertIn('\nFAIL: testFail ', out)
|
||||||
self.assertIn('\nERROR: testError ', out)
|
self.assertIn('\nERROR: testError ', out)
|
||||||
|
@ -147,12 +154,11 @@ class Test_TestProgram(unittest.TestCase):
|
||||||
|
|
||||||
def test_ExitAsDefault(self):
|
def test_ExitAsDefault(self):
|
||||||
stream = BufferedWriter()
|
stream = BufferedWriter()
|
||||||
self.assertRaises(
|
with self.assertRaises(SystemExit):
|
||||||
SystemExit,
|
unittest.main(
|
||||||
unittest.main,
|
argv=["foobar"],
|
||||||
argv=["foobar"],
|
testRunner=unittest.TextTestRunner(stream=stream),
|
||||||
testRunner=unittest.TextTestRunner(stream=stream),
|
testLoader=self.TestLoader(self.FooBar))
|
||||||
testLoader=self.FooBarLoader())
|
|
||||||
out = stream.getvalue()
|
out = stream.getvalue()
|
||||||
self.assertIn('\nFAIL: testFail ', out)
|
self.assertIn('\nFAIL: testFail ', out)
|
||||||
self.assertIn('\nERROR: testError ', out)
|
self.assertIn('\nERROR: testError ', out)
|
||||||
|
@ -161,6 +167,17 @@ class Test_TestProgram(unittest.TestCase):
|
||||||
'expected failures=1, unexpected successes=1)\n')
|
'expected failures=1, unexpected successes=1)\n')
|
||||||
self.assertTrue(out.endswith(expected))
|
self.assertTrue(out.endswith(expected))
|
||||||
|
|
||||||
|
def test_ExitEmptySuite(self):
|
||||||
|
stream = BufferedWriter()
|
||||||
|
with self.assertRaises(SystemExit) as cm:
|
||||||
|
unittest.main(
|
||||||
|
argv=["empty"],
|
||||||
|
testRunner=unittest.TextTestRunner(stream=stream),
|
||||||
|
testLoader=self.TestLoader(self.Empty))
|
||||||
|
self.assertEqual(cm.exception.code, 5)
|
||||||
|
out = stream.getvalue()
|
||||||
|
self.assertIn('\nNO TESTS RAN\n', out)
|
||||||
|
|
||||||
|
|
||||||
class InitialisableProgram(unittest.TestProgram):
|
class InitialisableProgram(unittest.TestProgram):
|
||||||
exit = False
|
exit = False
|
||||||
|
|
|
@ -451,6 +451,7 @@ class Test_TestResult(unittest.TestCase):
|
||||||
stream = BufferedWriter()
|
stream = BufferedWriter()
|
||||||
runner = unittest.TextTestRunner(stream=stream, failfast=True)
|
runner = unittest.TextTestRunner(stream=stream, failfast=True)
|
||||||
def test(result):
|
def test(result):
|
||||||
|
result.testsRun += 1
|
||||||
self.assertTrue(result.failfast)
|
self.assertTrue(result.failfast)
|
||||||
result = runner.run(test)
|
result = runner.run(test)
|
||||||
stream.flush()
|
stream.flush()
|
||||||
|
|
|
@ -577,6 +577,16 @@ class TestClassCleanup(unittest.TestCase):
|
||||||
'inner setup', 'inner test', 'inner cleanup',
|
'inner setup', 'inner test', 'inner cleanup',
|
||||||
'end outer test', 'outer cleanup'])
|
'end outer test', 'outer cleanup'])
|
||||||
|
|
||||||
|
def test_run_empty_suite_error_message(self):
|
||||||
|
class EmptyTest(unittest.TestCase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
suite = unittest.defaultTestLoader.loadTestsFromTestCase(EmptyTest)
|
||||||
|
runner = getRunner()
|
||||||
|
runner.run(suite)
|
||||||
|
|
||||||
|
self.assertIn("\nNO TESTS RAN\n", runner.stream.getvalue())
|
||||||
|
|
||||||
|
|
||||||
class TestModuleCleanUp(unittest.TestCase):
|
class TestModuleCleanUp(unittest.TestCase):
|
||||||
def test_add_and_do_ModuleCleanup(self):
|
def test_add_and_do_ModuleCleanup(self):
|
||||||
|
|
|
@ -9,6 +9,7 @@ from . import loader, runner
|
||||||
from .signals import installHandler
|
from .signals import installHandler
|
||||||
|
|
||||||
__unittest = True
|
__unittest = True
|
||||||
|
_NO_TESTS_EXITCODE = 5
|
||||||
|
|
||||||
MAIN_EXAMPLES = """\
|
MAIN_EXAMPLES = """\
|
||||||
Examples:
|
Examples:
|
||||||
|
@ -279,6 +280,12 @@ class TestProgram(object):
|
||||||
testRunner = self.testRunner
|
testRunner = self.testRunner
|
||||||
self.result = testRunner.run(self.test)
|
self.result = testRunner.run(self.test)
|
||||||
if self.exit:
|
if self.exit:
|
||||||
sys.exit(not self.result.wasSuccessful())
|
if self.result.testsRun == 0:
|
||||||
|
sys.exit(_NO_TESTS_EXITCODE)
|
||||||
|
elif self.result.wasSuccessful():
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
main = TestProgram
|
main = TestProgram
|
||||||
|
|
|
@ -274,6 +274,8 @@ class TextTestRunner(object):
|
||||||
infos.append("failures=%d" % failed)
|
infos.append("failures=%d" % failed)
|
||||||
if errored:
|
if errored:
|
||||||
infos.append("errors=%d" % errored)
|
infos.append("errors=%d" % errored)
|
||||||
|
elif run == 0:
|
||||||
|
self.stream.write("NO TESTS RAN")
|
||||||
else:
|
else:
|
||||||
self.stream.write("OK")
|
self.stream.write("OK")
|
||||||
if skipped:
|
if skipped:
|
||||||
|
|
|
@ -1513,6 +1513,7 @@ Vlad Riscutia
|
||||||
Wes Rishel
|
Wes Rishel
|
||||||
Daniel Riti
|
Daniel Riti
|
||||||
Juan M. Bello Rivas
|
Juan M. Bello Rivas
|
||||||
|
Stefano Rivera
|
||||||
Llandy Riveron Del Risco
|
Llandy Riveron Del Risco
|
||||||
Mohd Sanad Zaki Rizvi
|
Mohd Sanad Zaki Rizvi
|
||||||
Davide Rizzo
|
Davide Rizzo
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
The :mod:`unittest` runner will now exit with status code 5 if no tests
|
||||||
|
were run. It is common for test runner misconfiguration to fail to find
|
||||||
|
any tests, this should be an error.
|
Loading…
Reference in New Issue