summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStefano Rivera <stefano@rivera.za.net>2023-04-27 01:28:46 (GMT)
committerGitHub <noreply@github.com>2023-04-27 01:28:46 (GMT)
commit76632b836cf81a95301f4eb1fa43682e8d9ffa67 (patch)
treee34dfe04b641f614b06423ff7abd853d765e452d
parentdc3f97549a8fe4f7fea8d0326e394760b51caa6e (diff)
downloadcpython-76632b836cf81a95301f4eb1fa43682e8d9ffa67.zip
cpython-76632b836cf81a95301f4eb1fa43682e8d9ffa67.tar.gz
cpython-76632b836cf81a95301f4eb1fa43682e8d9ffa67.tar.bz2
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
-rw-r--r--Doc/library/unittest.rst3
-rw-r--r--Lib/test/test_unittest/test_program.py57
-rw-r--r--Lib/test/test_unittest/test_result.py1
-rw-r--r--Lib/test/test_unittest/test_runner.py10
-rw-r--r--Lib/unittest/main.py9
-rw-r--r--Lib/unittest/runner.py2
-rw-r--r--Misc/ACKS1
-rw-r--r--Misc/NEWS.d/next/Library/2023-02-19-12-37-08.gh-issue-62432.GnBFIB.rst3
8 files changed, 64 insertions, 22 deletions
diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst
index a7c74cf..c70153d 100644
--- a/Doc/library/unittest.rst
+++ b/Doc/library/unittest.rst
@@ -2281,7 +2281,8 @@ Loading and running tests
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
- 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,
and defaults to :data:`defaultTestLoader`.
diff --git a/Lib/test/test_unittest/test_program.py b/Lib/test/test_unittest/test_program.py
index f138f68..f6d52f9 100644
--- a/Lib/test/test_unittest/test_program.py
+++ b/Lib/test/test_unittest/test_program.py
@@ -71,15 +71,22 @@ class Test_TestProgram(unittest.TestCase):
def testUnexpectedSuccess(self):
pass
- class FooBarLoader(unittest.TestLoader):
- """Test loader that returns a suite containing FooBar."""
+ class Empty(unittest.TestCase):
+ 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):
return self.suiteClass(
- [self.loadTestsFromTestCase(Test_TestProgram.FooBar)])
+ [self.loadTestsFromTestCase(self.testcase)])
def loadTestsFromNames(self, names, module):
return self.suiteClass(
- [self.loadTestsFromTestCase(Test_TestProgram.FooBar)])
+ [self.loadTestsFromTestCase(self.testcase)])
def test_defaultTest_with_string(self):
class FakeRunner(object):
@@ -92,7 +99,7 @@ class Test_TestProgram(unittest.TestCase):
runner = FakeRunner()
program = unittest.TestProgram(testRunner=runner, exit=False,
defaultTest='test.test_unittest',
- testLoader=self.FooBarLoader())
+ testLoader=self.TestLoader(self.FooBar))
sys.argv = old_argv
self.assertEqual(('test.test_unittest',), program.testNames)
@@ -108,7 +115,7 @@ class Test_TestProgram(unittest.TestCase):
program = unittest.TestProgram(
testRunner=runner, exit=False,
defaultTest=['test.test_unittest', 'test.test_unittest2'],
- testLoader=self.FooBarLoader())
+ testLoader=self.TestLoader(self.FooBar))
sys.argv = old_argv
self.assertEqual(['test.test_unittest', 'test.test_unittest2'],
program.testNames)
@@ -118,7 +125,7 @@ class Test_TestProgram(unittest.TestCase):
program = unittest.main(exit=False,
argv=["foobar"],
testRunner=unittest.TextTestRunner(stream=stream),
- testLoader=self.FooBarLoader())
+ testLoader=self.TestLoader(self.FooBar))
self.assertTrue(hasattr(program, 'result'))
out = stream.getvalue()
self.assertIn('\nFAIL: testFail ', out)
@@ -130,13 +137,13 @@ class Test_TestProgram(unittest.TestCase):
def test_Exit(self):
stream = BufferedWriter()
- self.assertRaises(
- SystemExit,
- unittest.main,
- argv=["foobar"],
- testRunner=unittest.TextTestRunner(stream=stream),
- exit=True,
- testLoader=self.FooBarLoader())
+ with self.assertRaises(SystemExit) as cm:
+ unittest.main(
+ argv=["foobar"],
+ testRunner=unittest.TextTestRunner(stream=stream),
+ exit=True,
+ testLoader=self.TestLoader(self.FooBar))
+ self.assertEqual(cm.exception.code, 1)
out = stream.getvalue()
self.assertIn('\nFAIL: testFail ', out)
self.assertIn('\nERROR: testError ', out)
@@ -147,12 +154,11 @@ class Test_TestProgram(unittest.TestCase):
def test_ExitAsDefault(self):
stream = BufferedWriter()
- self.assertRaises(
- SystemExit,
- unittest.main,
- argv=["foobar"],
- testRunner=unittest.TextTestRunner(stream=stream),
- testLoader=self.FooBarLoader())
+ with self.assertRaises(SystemExit):
+ unittest.main(
+ argv=["foobar"],
+ testRunner=unittest.TextTestRunner(stream=stream),
+ testLoader=self.TestLoader(self.FooBar))
out = stream.getvalue()
self.assertIn('\nFAIL: testFail ', out)
self.assertIn('\nERROR: testError ', out)
@@ -161,6 +167,17 @@ class Test_TestProgram(unittest.TestCase):
'expected failures=1, unexpected successes=1)\n')
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):
exit = False
diff --git a/Lib/test/test_unittest/test_result.py b/Lib/test/test_unittest/test_result.py
index 37d0fe1..db551b7 100644
--- a/Lib/test/test_unittest/test_result.py
+++ b/Lib/test/test_unittest/test_result.py
@@ -451,6 +451,7 @@ class Test_TestResult(unittest.TestCase):
stream = BufferedWriter()
runner = unittest.TextTestRunner(stream=stream, failfast=True)
def test(result):
+ result.testsRun += 1
self.assertTrue(result.failfast)
result = runner.run(test)
stream.flush()
diff --git a/Lib/test/test_unittest/test_runner.py b/Lib/test/test_unittest/test_runner.py
index ceb4c8a..f3b2c0c 100644
--- a/Lib/test/test_unittest/test_runner.py
+++ b/Lib/test/test_unittest/test_runner.py
@@ -577,6 +577,16 @@ class TestClassCleanup(unittest.TestCase):
'inner setup', 'inner test', 'inner 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):
def test_add_and_do_ModuleCleanup(self):
diff --git a/Lib/unittest/main.py b/Lib/unittest/main.py
index 0792750..51b81a6 100644
--- a/Lib/unittest/main.py
+++ b/Lib/unittest/main.py
@@ -9,6 +9,7 @@ from . import loader, runner
from .signals import installHandler
__unittest = True
+_NO_TESTS_EXITCODE = 5
MAIN_EXAMPLES = """\
Examples:
@@ -279,6 +280,12 @@ class TestProgram(object):
testRunner = self.testRunner
self.result = testRunner.run(self.test)
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
diff --git a/Lib/unittest/runner.py b/Lib/unittest/runner.py
index a51c5c5..e3c020e 100644
--- a/Lib/unittest/runner.py
+++ b/Lib/unittest/runner.py
@@ -274,6 +274,8 @@ class TextTestRunner(object):
infos.append("failures=%d" % failed)
if errored:
infos.append("errors=%d" % errored)
+ elif run == 0:
+ self.stream.write("NO TESTS RAN")
else:
self.stream.write("OK")
if skipped:
diff --git a/Misc/ACKS b/Misc/ACKS
index 1947569..65be5cf 100644
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -1513,6 +1513,7 @@ Vlad Riscutia
Wes Rishel
Daniel Riti
Juan M. Bello Rivas
+Stefano Rivera
Llandy Riveron Del Risco
Mohd Sanad Zaki Rizvi
Davide Rizzo
diff --git a/Misc/NEWS.d/next/Library/2023-02-19-12-37-08.gh-issue-62432.GnBFIB.rst b/Misc/NEWS.d/next/Library/2023-02-19-12-37-08.gh-issue-62432.GnBFIB.rst
new file mode 100644
index 0000000..a8d66ea
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-02-19-12-37-08.gh-issue-62432.GnBFIB.rst
@@ -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.