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
5 changes: 5 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~

Expand Down
59 changes: 59 additions & 0 deletions tests/test_testresult.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`."""
Expand Down
135 changes: 73 additions & 62 deletions testtools/testresult/real.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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:
Expand All @@ -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()


Expand Down