From dab18f6eef86bf2bd6eaf07565ea1f0cc5609ea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Mon, 10 Nov 2025 19:10:32 +0000 Subject: [PATCH 1/2] Implement verbosity support for test runner The -v (--verbose) flag was advertised in the help text but was not actually implemented. This change implements proper verbosity support: - verbosity=0 (quiet): No progress indicators - verbosity=1 (normal): Dots for each test (default) - verbosity=2 (verbose): Full test names as they run Fixes: https://bugs.launchpad.net/testtools/+bug/872906 --- NEWS | 5 +++ testtools/run.py | 5 ++- testtools/testresult/real.py | 82 ++++++++++++++++++++++++++++++++++-- testtools/tests/test_run.py | 26 ++++++++++++ 4 files changed, 114 insertions(+), 4 deletions(-) diff --git a/NEWS b/NEWS index 93ea2c79..2bf0e4f7 100644 --- a/NEWS +++ b/NEWS @@ -27,6 +27,11 @@ Changes Improvements ------------ +* Implemented verbosity support for the test runner. The ``-v`` (``--verbose``) + flag is now functional and properly controls output verbosity. Verbosity + levels: 0 (quiet), 1 (dots, default), 2 (test names). This applies to both + normal test runs and discovery mode. (LP: #872906) + * Add support for Python 3.12's ``addDuration`` method and ``collectedDurations`` attribute in ``TestResult`` classes. (Jelmer Vernooij, #2045171) diff --git a/testtools/run.py b/testtools/run.py index 419ff15f..e89dab2e 100755 --- a/testtools/run.py +++ b/testtools/run.py @@ -73,12 +73,14 @@ def __init__( ): """Create a TestToolsTestRunner. - :param verbosity: Ignored. + :param verbosity: Verbosity level. 0 for quiet, 1 for normal (dots, default), + 2 for verbose (test names). :param failfast: Stop running tests at the first failure. :param buffer: Ignored. :param stdout: Stream to use for stdout. :param tb_locals: If True include local variables in tracebacks. """ + self.verbosity = verbosity if verbosity is not None else 1 self.failfast = failfast if stdout is None: stdout = sys.stdout @@ -102,6 +104,7 @@ def run(self, test): unicode_output_stream(self.stdout), failfast=self.failfast, tb_locals=self.tb_locals, + verbosity=self.verbosity, ) result.startTestRun() try: diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index 8aa372a6..2b9cd8fb 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -1186,12 +1186,21 @@ def wasSuccessful(self): class TextTestResult(TestResult): """A TestResult which outputs activity to a text stream.""" - def __init__(self, stream, failfast=False, tb_locals=False): - """Construct a TextTestResult writing to stream.""" + def __init__(self, stream, failfast=False, tb_locals=False, verbosity=1): + """Construct a TextTestResult writing to stream. + + :param stream: A file-like object to write results to. + :param failfast: Stop after the first failure. + :param tb_locals: If True include local variables in tracebacks. + :param verbosity: Verbosity level. 0 for quiet, 1 for normal (dots, default), + 2 for verbose (test names). + """ super().__init__(failfast=failfast, tb_locals=tb_locals) self.stream = stream self.sep1 = "=" * 70 + "\n" self.sep2 = "-" * 70 + "\n" + self.verbosity = verbosity + self._progress_printed = False def _delta_to_float(self, a_timedelta, precision): # This calls ceiling to ensure that the most pessimistic view of time @@ -1218,6 +1227,67 @@ def _show_list(self, label, error_list): self.stream.write(self.sep2) self.stream.write(output) + def startTest(self, test): + super().startTest(test) + if self.verbosity >= 2: + self.stream.write(f"{test.id()} ... ") + self.stream.flush() + + def addSuccess(self, test, details=None): + super().addSuccess(test, details=details) + if self.verbosity == 1: + self.stream.write(".") + self.stream.flush() + self._progress_printed = True + elif self.verbosity >= 2: + self.stream.write("ok\n") + self.stream.flush() + + def addError(self, test, err=None, details=None): + super().addError(test, err=err, details=details) + if self.verbosity == 1: + self.stream.write("E") + self.stream.flush() + elif self.verbosity >= 2: + self.stream.write("ERROR\n") + self.stream.flush() + + def addFailure(self, test, err=None, details=None): + super().addFailure(test, err=err, details=details) + if self.verbosity == 1: + self.stream.write("F") + self.stream.flush() + elif self.verbosity >= 2: + self.stream.write("FAIL\n") + self.stream.flush() + + def addSkip(self, test, reason=None, details=None): + super().addSkip(test, reason=reason, details=details) + if self.verbosity == 1: + self.stream.write("s") + self.stream.flush() + elif self.verbosity >= 2: + self.stream.write(f"skipped {reason!r}\n") + self.stream.flush() + + def addExpectedFailure(self, test, err=None, details=None): + super().addExpectedFailure(test, err=err, details=details) + if self.verbosity == 1: + self.stream.write("x") + self.stream.flush() + elif self.verbosity >= 2: + self.stream.write("expected failure\n") + self.stream.flush() + + def addUnexpectedSuccess(self, test, details=None): + super().addUnexpectedSuccess(test, details=details) + if self.verbosity == 1: + self.stream.write("u") + self.stream.flush() + elif self.verbosity >= 2: + self.stream.write("unexpected success\n") + self.stream.flush() + def startTestRun(self): super().startTestRun() self.__start = self._now() @@ -1235,8 +1305,14 @@ def stopTestRun(self): self.stream.write( f"{self.sep1}UNEXPECTED SUCCESS: {test.id()}\n{self.sep2}" ) + # Add newline(s) before summary + # If we printed progress indicators (dots), add extra newline + if self._progress_printed: + self.stream.write("\n\n") + else: + self.stream.write("\n") self.stream.write( - f"\nRan {self.testsRun} test{plural} in " + f"Ran {self.testsRun} test{plural} in " f"{self._delta_to_float(stop - self.__start, 3):.3f}s\n" ) if self.wasSuccessful(): diff --git a/testtools/tests/test_run.py b/testtools/tests/test_run.py index 831eba76..90d96c69 100644 --- a/testtools/tests/test_run.py +++ b/testtools/tests/test_run.py @@ -436,6 +436,7 @@ def test_stdout_honoured(self): out.getvalue(), MatchesRegex( """Tests running... +\\.\\. Ran 2 tests in \\d.\\d\\d\\ds OK @@ -466,6 +467,31 @@ def test_issue_16662(self): out.getvalue(), ) + def test_runner_verbosity_respected(self): + # Test that verbosity parameter is actually used by the runner + out = io.StringIO() + runner = run.TestToolsTestRunner(verbosity=2, stdout=out) + self.assertEqual(2, runner.verbosity) + + def test_discover_verbosity(self): + # Test that -v flag in discover mode sets verbosity + self.useFixture(SampleTestFixture()) + out = io.StringIO() + exc = self.assertRaises( + SystemExit, + run.main, + argv=[ + "prog", + "discover", + "-v", + "-s", + self.useFixture(fixtures.TempDir()).path, + ], + stdout=out, + ) + # The output should show individual test names when verbose + self.assertEqual((0,), exc.args) + def test_suite(): from unittest import TestLoader From efaf11f14f2d722dc2a20341b948b60a4e9f629a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Mon, 10 Nov 2025 22:20:28 +0000 Subject: [PATCH 2/2] Fix python in .testr.conf --- .testr.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.testr.conf b/.testr.conf index e6951094..18013703 100644 --- a/.testr.conf +++ b/.testr.conf @@ -1,4 +1,4 @@ [DEFAULT] -test_command=${PYTHON:-python} -m subunit.run $LISTOPT $IDOPTION testtools.tests.test_suite +test_command=${PYTHON:-python3} -m subunit.run $LISTOPT $IDOPTION testtools.tests.test_suite test_id_option=--load-list $IDFILE test_list_option=--list