]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
test: Extract "abstract" test logic into a decorator
authorBen Darnell <ben@bendarnell.com>
Thu, 11 Jul 2024 17:41:31 +0000 (13:41 -0400)
committerBen Darnell <ben@bendarnell.com>
Thu, 11 Jul 2024 17:41:31 +0000 (13:41 -0400)
This makes the pattern reusable (and discoverable) for other tests.

Also exclude these skipped tests from the reporting that is meant
to highlight environmental issues.

This could potentially move to tornado.testing (or even upstream to
the stdlib) if it proves generally useful.

tornado/test/httpserver_test.py
tornado/test/runtests.py
tornado/test/util.py

index 330140b2ab578de7cad7de415307da351839dd48..e6488420ffc1f1ecb44ade5fb1f3b03b1b566d38 100644 (file)
@@ -28,7 +28,7 @@ from tornado.testing import (
     ExpectLog,
     gen_test,
 )
-from tornado.test.util import skipOnTravis
+from tornado.test.util import skipOnTravis, abstract_base_test
 from tornado.web import Application, RequestHandler, stream_request_body
 
 from contextlib import closing
@@ -814,6 +814,7 @@ class ManualProtocolTest(HandlerBaseTestCase):
         self.assertEqual(self.fetch_json("/")["protocol"], "https")
 
 
+@abstract_base_test
 class UnixSocketTest(AsyncTestCase):
     """HTTPServers can listen on Unix sockets too.
 
@@ -828,8 +829,6 @@ class UnixSocketTest(AsyncTestCase):
     address = ""
 
     def setUp(self):
-        if type(self) is UnixSocketTest:
-            raise unittest.SkipTest("abstract base class")
         super().setUp()
         app = Application([("/hello", HelloWorldRequestHandler)])
         self.server = HTTPServer(app)
index ef6a503b424d8e1cd4b948be9e62942619578f16..d7eb51f9aec275357f687ddaa9e7412427bc822d 100644 (file)
@@ -13,6 +13,7 @@ from tornado.httpclient import AsyncHTTPClient
 from tornado.httpserver import HTTPServer
 from tornado.netutil import Resolver
 from tornado.options import define, add_parse_callback, options
+from tornado.test.util import ABT_SKIP_MESSAGE
 
 
 TEST_MODULES = [
@@ -60,9 +61,20 @@ def all():
 
 
 def test_runner_factory(stderr):
+
+    class TornadoTextTestResult(unittest.TextTestResult):
+        def addSkip(self, test, reason):
+            if reason == ABT_SKIP_MESSAGE:
+                # Don't report abstract base tests as skips in our own tooling.
+                #
+                # See tornado.test.util.abstract_base_test.
+                return
+            super().addSkip(test, reason)
+
     class TornadoTextTestRunner(unittest.TextTestRunner):
         def __init__(self, *args, **kwargs):
             kwargs["stream"] = stderr
+            kwargs["resultclass"] = TornadoTextTestResult
             super().__init__(*args, **kwargs)
 
         def run(self, test):
index 53719ac959bb45d64e4b8a6a94b8b3e0579b01ab..40ff9d65e60d895f28f85847ebc230914e0b61b0 100644 (file)
@@ -4,12 +4,14 @@ import platform
 import socket
 import sys
 import textwrap
-import typing  # noqa: F401
+import typing
 import unittest
 import warnings
 
 from tornado.testing import bind_unused_port
 
+_TestCaseType = typing.TypeVar("_TestCaseType", bound=typing.Type[unittest.TestCase])
+
 skipIfNonUnix = unittest.skipIf(
     os.name != "posix" or sys.platform == "cygwin", "non-unix platform"
 )
@@ -112,3 +114,36 @@ def ignore_deprecation():
     with warnings.catch_warnings():
         warnings.simplefilter("ignore", DeprecationWarning)
         yield
+
+
+ABT_SKIP_MESSAGE = "abstract base class"
+
+
+def abstract_base_test(cls: _TestCaseType) -> _TestCaseType:
+    """Decorator to mark a test class as an "abstract" base class.
+
+    This is different from a regular abstract base class because
+    we do not limit instantiation of the class. (If we did, it would
+    interfere with test discovery). Instead, we prevent the tests from
+    being run.
+
+    Subclasses of an abstract base test are run as normal. There is
+    no support for the ``@abstractmethod`` decorator so there is no runtime
+    check that all such methods are implemented.
+
+    Note that while it is semantically cleaner to modify the test loader
+    to exclude abstract base tests, this is more complicated and would
+    interfere with third-party test runners. This approach degrades
+    gracefully to other tools such as editor-integrated testing.
+    """
+
+    # Type-checking fails due to https://github.com/python/mypy/issues/14458
+    # @functools.wraps(cls)
+    class AbstractBaseWrapper(cls):  # type: ignore
+        @classmethod
+        def setUpClass(cls):
+            if cls is AbstractBaseWrapper:
+                raise unittest.SkipTest(ABT_SKIP_MESSAGE)
+            super(AbstractBaseWrapper, cls).setUpClass()
+
+    return AbstractBaseWrapper  # type: ignore