Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions testtools/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ def unicode_output_stream(stream: IO[str]) -> IO[str]:
The wrapper only allows unicode to be written, not non-ascii bytestrings,
which is a good thing to ensure sanity and sanitation.
"""
warnings.warn(
"This is not necessary in Python 3.",
DeprecationWarning,
stacklevel=2,
)

if sys.platform == "cli" or isinstance(stream, (io.TextIOWrapper, io.StringIO)):
# Best to never encode before writing in IronPython, or if it is
# already a TextIO [which in the io library has no encoding
Expand Down
114 changes: 29 additions & 85 deletions testtools/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,13 @@
import unittest
from argparse import ArgumentParser
from collections.abc import Callable
from functools import partial
from types import ModuleType
from typing import IO, Any, TextIO
from typing import IO, Any

from testtools import TextTestResult
from testtools.compat import unicode_output_stream
from testtools.testsuite import filter_by_ids, iterate_tests, sorted_tests

# unittest.TestProgram has these methods but mypy's stubs don't include them
# We'll just use unittest.TestProgram directly and ignore the type errors

# TODO: Remove these aliases and constants in 3.0
defaultTestLoader = unittest.defaultTestLoader
defaultTestLoaderCls = unittest.TestLoader
have_discover = True
Expand Down Expand Up @@ -121,9 +117,8 @@ def run(
self, test: unittest.TestSuite | unittest.TestCase
) -> unittest.TestResult | None:
"""Run the given test case or test suite."""
stream: TextIO = unicode_output_stream(self.stdout) # type: ignore[assignment]
result = TextTestResult(
stream,
self.stdout,
failfast=self.failfast or False,
tb_locals=self.tb_locals,
verbosity=self.verbosity,
Expand All @@ -135,49 +130,34 @@ def run(
result.stopTestRun()


####################
# Taken from python 2.7 and slightly modified for compatibility with
# older versions. Delete when 2.7 is the oldest supported version.
# Modifications:
# - If --catch is given, check that installHandler is available, as
# it won't be on old python versions or python builds without signals.
# - --list has been added which can list tests (should be upstreamed).
# - --load-list has been added which can reduce the tests used (should be
# upstreamed).


# Note that we need to duplicate a lot of the upstream implementation here.
# This has been reported as https://github.com/python/cpython/issues/67049
class TestProgram(unittest.TestProgram):
"""A command-line program that runs a set of tests; this is primarily
for making test modules conveniently executable.

Extends unittest.TestProgram with:
- ``-l``/``--list``: list tests rather than running them
- ``--load-list``: filter tests to those listed in a file
- ``stdout`` parameter: control where output goes
- Sorted test ordering after discovery
"""

# defaults for testing
module: ModuleType | None = None
verbosity: int = 1
failfast: bool | None = None
catchbreak: bool | None = None
buffer: bool | None = None
progName: str | None = None
_discovery_parser: ArgumentParser | None = None
test: Any # Set by parent class
# https://github.com/python/typeshed/pull/15559
module: ModuleType | None = None # narrower than parent's str | ModuleType | None
test: Any # set by parent's createTests(), not in typeshed
stdout: IO[str]
exit: bool
tb_locals: bool
defaultTest: str | None
listtests: bool
load_list: str | None
testRunner: Callable[..., TestToolsTestRunner] | TestToolsTestRunner | None
testLoader: unittest.TestLoader
result: unittest.TestResult

def __init__(
self,
module: str | ModuleType | None = __name__,
defaultTest: str | None = None,
argv: list[str] | None = None,
testRunner: Callable[..., TestToolsTestRunner]
| TestToolsTestRunner
| None = None,
testRunner: (
Callable[..., TestToolsTestRunner] | TestToolsTestRunner | None
) = None,
testLoader: unittest.TestLoader = defaultTestLoader,
exit: bool = True,
verbosity: int = 1,
Expand Down Expand Up @@ -208,7 +188,6 @@ def __init__(
self.buffer = buffer
self.tb_locals = tb_locals
self.defaultTest = defaultTest
# XXX: Local edit (see http://bugs.python.org/issue22860)
self.listtests = False
self.load_list = None
self.testRunner = testRunner
Expand All @@ -221,20 +200,15 @@ def __init__(
progName = os.path.basename(argv[0])
self.progName = progName
self.parseArgs(argv)
# XXX: Local edit (see http://bugs.python.org/issue22860)
if self.load_list:
# TODO: preserve existing suites (like testresources does in
# OptimisingTestSuite.add, but with a standard protocol).
# This is needed because the load_tests hook allows arbitrary
# suites, even if that is rarely used.
source = open(self.load_list, "rb")
try:
with open(self.load_list, "rb") as source:
lines = source.readlines()
finally:
source.close()
test_ids = {line.strip().decode("utf-8") for line in lines}
self.test = filter_by_ids(self.test, test_ids)
# XXX: Local edit (see http://bugs.python.org/issue22860)
if not self.listtests:
self.runTests()
else:
Expand All @@ -247,8 +221,8 @@ def __init__(
del self.testLoader.errors[:]

def _getParentArgParser(self) -> ArgumentParser:
# this method is not part of the type stubs
parser: ArgumentParser = super()._getParentArgParser() # type: ignore[misc]
# XXX: Local edit (see http://bugs.python.org/issue22860)
parser.add_argument(
"-l",
"--list",
Expand All @@ -269,13 +243,12 @@ def _getParentArgParser(self) -> ArgumentParser:
def _do_discovery(
self, argv: list[str], Loader: type[unittest.TestLoader] | None = None
) -> None:
# this method is not part of the type stubs
super()._do_discovery(argv, Loader=Loader) # type: ignore[misc]
# XXX: Local edit (see http://bugs.python.org/issue22860)
self.test = sorted_tests(self.test)

def runTests(self) -> None:
# XXX: Local edit (see http://bugs.python.org/issue22860)
if self.catchbreak and getattr(unittest, "installHandler", None) is not None:
if self.catchbreak:
unittest.installHandler()
testRunner = self._get_runner()
result = testRunner.run(self.test)
Expand All @@ -285,54 +258,25 @@ def runTests(self) -> None:
sys.exit(not self.result.wasSuccessful())

def _get_runner(self) -> TestToolsTestRunner:
# XXX: Local edit (see http://bugs.python.org/issue22860)
runner_or_factory = self.testRunner
if runner_or_factory is None:
runner_or_factory = TestToolsTestRunner

# If it's already an instance, return it directly
if isinstance(runner_or_factory, TestToolsTestRunner):
return runner_or_factory

# It's a callable (class or factory function)
runner_factory: Callable[..., TestToolsTestRunner] = runner_or_factory
try:
try:
testRunner = runner_factory(
verbosity=self.verbosity,
failfast=self.failfast,
buffer=self.buffer,
stdout=self.stdout,
tb_locals=self.tb_locals,
)
except TypeError:
# didn't accept the tb_locals parameter
testRunner = runner_factory(
verbosity=self.verbosity,
failfast=self.failfast,
buffer=self.buffer,
stdout=self.stdout,
)
except TypeError:
# didn't accept the verbosity, buffer, failfast or stdout arguments
# Try with the prior contract
try:
testRunner = runner_factory(
verbosity=self.verbosity, failfast=self.failfast, buffer=self.buffer
)
except TypeError:
# Now try calling it with defaults
testRunner = runner_factory()
return testRunner


################
return runner_factory(
verbosity=self.verbosity,
failfast=self.failfast,
buffer=self.buffer,
stdout=self.stdout,
tb_locals=self.tb_locals,
)


def main(argv: list[str], stdout: IO[str]) -> None:
TestProgram(
argv=argv, testRunner=partial(TestToolsTestRunner, stdout=stdout), stdout=stdout
)
TestProgram(argv=argv, stdout=stdout)


if __name__ == "__main__":
Expand Down
4 changes: 2 additions & 2 deletions testtools/testresult/real.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@
from operator import methodcaller
from queue import Queue
from typing import (
IO,
TYPE_CHECKING,
ClassVar,
Protocol,
TextIO,
TypeAlias,
TypedDict,
TypeVar,
Expand Down Expand Up @@ -1539,7 +1539,7 @@ class TextTestResult(TestResult):

def __init__(
self,
stream: TextIO | None,
stream: IO[str] | None,
failfast: bool = False,
tb_locals: bool = False,
verbosity: int = 1,
Expand Down