From: Ben Darnell Date: Thu, 11 Jul 2024 17:41:31 +0000 (-0400) Subject: test: Extract "abstract" test logic into a decorator X-Git-Tag: v6.5.0b1~41^2~6 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=23a54a725f71c34657a54a1b4138656cfc834cb7;p=thirdparty%2Ftornado.git test: Extract "abstract" test logic into a decorator 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. --- diff --git a/tornado/test/httpserver_test.py b/tornado/test/httpserver_test.py index 330140b2..e6488420 100644 --- a/tornado/test/httpserver_test.py +++ b/tornado/test/httpserver_test.py @@ -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) diff --git a/tornado/test/runtests.py b/tornado/test/runtests.py index ef6a503b..d7eb51f9 100644 --- a/tornado/test/runtests.py +++ b/tornado/test/runtests.py @@ -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): diff --git a/tornado/test/util.py b/tornado/test/util.py index 53719ac9..40ff9d65 100644 --- a/tornado/test/util.py +++ b/tornado/test/util.py @@ -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