]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-62432: unittest runner: Exit code 5 if no tests were run (#102051)
authorStefano Rivera <stefano@rivera.za.net>
Thu, 27 Apr 2023 01:28:46 +0000 (18:28 -0700)
committerGitHub <noreply@github.com>
Thu, 27 Apr 2023 01:28:46 +0000 (01:28 +0000)
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
Doc/library/unittest.rst
Lib/test/test_unittest/test_program.py
Lib/test/test_unittest/test_result.py
Lib/test/test_unittest/test_runner.py
Lib/unittest/main.py
Lib/unittest/runner.py
Misc/ACKS
Misc/NEWS.d/next/Library/2023-02-19-12-37-08.gh-issue-62432.GnBFIB.rst [new file with mode: 0644]

index a7c74cfa4fb477ec6f9a0964ac8484b9bae688af..c70153dfcd69e1a3a364f758e686bb11adac4f37 100644 (file)
@@ -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`.
index f138f6836514e0c8eaed3a1b8843683b430de4dd..f6d52f93e4a25f96d6d1403ab40aba87544089da 100644 (file)
@@ -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
index 37d0fe12409ea40483912513c81e131f99cff5e7..db551b7890ca3e3ee58122554e727f3199292f30 100644 (file)
@@ -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()
index ceb4c8acde532cd088f56094e1c270173434b9c0..f3b2c0cffd45139a6556810c879e1bb91d8ba546 100644 (file)
@@ -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):
index 0792750ffd9e0dfe81a08c68dabcebf6685f4308..51b81a6c3728bbf35d0ddf47faec93ce63fd55e5 100644 (file)
@@ -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
index a51c5c562df09d56aa76d3bb09c82a4fc0eb6604..e3c020e0ace96de1641bc6fdfab09aa2861908f9 100644 (file)
@@ -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:
index 19475698a4bc37fd8d983ff69adb4bdae4ea00c9..65be5cfc3c79458d50d599d2ab4880fbe204beda 100644 (file)
--- 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 (file)
index 0000000..a8d66ea
--- /dev/null
@@ -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.