From c155d14be8bdee2ec0e2d58eedee38b116158801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 19 Dec 2025 15:49:02 +0000 Subject: [PATCH] Be more lenient to stream=None being passed into TestResult classes Subtle regression from 2.8.0, which added verbosity support --- NEWS | 5 ++ tests/test_testresult.py | 59 +++++++++++++++ testtools/testresult/real.py | 135 +++++++++++++++++++---------------- 3 files changed, 137 insertions(+), 62 deletions(-) diff --git a/NEWS b/NEWS index 066bb4db..64972e63 100644 --- a/NEWS +++ b/NEWS @@ -17,6 +17,11 @@ Improvements * Support binary contents in ``FileContains`` matcher. (Jelmer Vernooij, #538) +* Allow stream=None to be passed to various TestResult + classes that now support verbosity; fixes a regression +from 2.8.0. + (Jelmer Vernooij) + 2.8.1 ~~~~~ diff --git a/tests/test_testresult.py b/tests/test_testresult.py index 3ed29f7e..b068b0ed 100644 --- a/tests/test_testresult.py +++ b/tests/test_testresult.py @@ -2078,6 +2078,65 @@ def run_tests(): ), ) + def test_none_stream_is_accepted(self): + """TextTestResult should accept None as stream for backward compatibility.""" + result = TextTestResult(None) + test = make_test() + # Should not raise AttributeError + result.startTestRun() + result.startTest(test) + result.addSuccess(test) + result.stopTest(test) + result.stopTestRun() + + def test_none_stream_with_failure(self): + """TextTestResult with None stream should handle failures.""" + result = TextTestResult(None) + test = make_failing_test() + # Should not raise AttributeError + test.run(result) + self.assertEqual(1, len(result.failures)) + + def test_verbosity_zero_produces_no_output(self): + """TextTestResult with verbosity=0 should produce no per-test output.""" + stream = io.StringIO() + result = TextTestResult(stream, verbosity=0) + test = make_test() + result.startTestRun() + result.startTest(test) + result.addSuccess(test) + result.stopTest(test) + # Get output up to stopTestRun (which still outputs summary) + output_before_stop = stream.getvalue() + # Should only have "Tests running...\n" from startTestRun + self.assertEqual("Tests running...\n", output_before_stop) + + def test_verbosity_zero_allows_subclass_control(self): + """Subclasses can use verbosity=0 to control their own output.""" + + class CustomResult(TextTestResult): + def __init__(self, stream): + super().__init__(stream, verbosity=0) + + def addSuccess(self, test, details=None): + super().addSuccess(test, details) + self.stream.write("CUSTOM_SUCCESS_MARKER\n") + + stream = io.StringIO() + result = CustomResult(stream) + test = make_test() + result.startTestRun() + result.startTest(test) + result.addSuccess(test) + result.stopTest(test) + output = stream.getvalue() + # Should have custom output but not parent's dot or "ok" + self.assertIn("CUSTOM_SUCCESS_MARKER\n", output) + self.assertNotIn("ok\n", output) + # Count dots - "Tests running..." has 3 dots, should not have a 4th + # from the success indicator + self.assertEqual(output.count("."), 3) + class TestThreadSafeForwardingResult(TestCase): """Tests for `TestThreadSafeForwardingResult`.""" diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index 35d23fd7..3f1cb269 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -1244,6 +1244,8 @@ def _delta_to_float(self, a_timedelta, precision): ) def _show_list(self, label, error_list): + if self.stream is None: + return for test, output in error_list: self.stream.write(self.sep1) self.stream.write(f"{label}: {test.id()}\n") @@ -1252,69 +1254,76 @@ def _show_list(self, label, error_list): def startTest(self, test): super().startTest(test) - if self.verbosity >= 2: + if self.stream is not None and 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() + if self.stream is not None: + 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() + if self.stream is not None: + 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() + if self.stream is not None: + 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() + if self.stream is not None: + 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() + if self.stream is not None: + 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() + if self.stream is not None: + 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() - self.stream.write("Tests running...\n") + if self.stream is not None: + self.stream.write("Tests running...\n") def stopTestRun(self): if self.testsRun != 1: @@ -1324,31 +1333,33 @@ def stopTestRun(self): stop = self._now() self._show_list("ERROR", self.errors) self._show_list("FAIL", self.failures) - for test in self.unexpectedSuccesses: + if self.stream is not None: + for test in self.unexpectedSuccesses: + 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"{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"Ran {self.testsRun} test{plural} in " - f"{self._delta_to_float(stop - self.__start, 3):.3f}s\n" - ) - if self.wasSuccessful(): - self.stream.write("OK\n") - else: - self.stream.write("FAILED (") - details = [] - failure_count = sum( - len(x) for x in (self.failures, self.errors, self.unexpectedSuccesses) + f"Ran {self.testsRun} test{plural} in " + f"{self._delta_to_float(stop - self.__start, 3):.3f}s\n" ) - details.append(f"failures={failure_count}") - self.stream.write(", ".join(details)) - self.stream.write(")\n") + if self.wasSuccessful(): + self.stream.write("OK\n") + else: + self.stream.write("FAILED (") + details = [] + failure_count = sum( + len(x) + for x in (self.failures, self.errors, self.unexpectedSuccesses) + ) + details.append(f"failures={failure_count}") + self.stream.write(", ".join(details)) + self.stream.write(")\n") super().stopTestRun()