1414
1515import argparse
1616import importlib
17+ import io
1718import sys
1819import traceback
1920import unittest
2021
2122
23+ class DiagnosticCapture (io .TextIOBase ):
24+ """Capture writes and re-emit them as TAP diagnostic lines (# ...)."""
25+
26+ def __init__ (self , real_stdout : io .TextIOBase ) -> None :
27+ self ._real = real_stdout
28+ self ._buf = ''
29+
30+ def write (self , text : str ) -> int :
31+ self ._buf += text
32+ while '\n ' in self ._buf :
33+ line , self ._buf = self ._buf .split ('\n ' , 1 )
34+ self ._real .write ('# {}\n ' .format (line ))
35+ self ._real .flush ()
36+ return len (text )
37+
38+ def flush (self ) -> None :
39+ if self ._buf :
40+ self ._real .write ('# {}\n ' .format (self ._buf ))
41+ self ._buf = ''
42+ self ._real .flush ()
43+
44+
2245class TAPTestResult (unittest .TestResult ):
2346 """Collect unittest results and render them as TAP version 13."""
2447
@@ -72,12 +95,11 @@ def addUnexpectedSuccess(self, test: unittest.TestCase) -> None:
7295 self ._lines .append ('not ok {} - {} # TODO unexpected success\n ' .format (
7396 self ._test_count , self ._description (test )))
7497
75- def print_tap (self , stream : object = sys .stdout ) -> None :
76- stream .write ('TAP version 13\n ' ) # type: ignore[union-attr]
77- stream .write ('1..{}\n ' .format (self ._test_count )) # type: ignore[union-attr]
98+ def print_tap (self , stream : io .TextIOBase ) -> None :
99+ stream .write ('1..{}\n ' .format (self ._test_count ))
78100 for line in self ._lines :
79- stream .write (line ) # type: ignore[union-attr]
80- stream .flush () # type: ignore[union-attr]
101+ stream .write (line )
102+ stream .flush ()
81103
82104
83105def run_tests (test_module_name : str , start_dir : str | None = None ) -> bool :
@@ -89,9 +111,22 @@ def run_tests(test_module_name: str, start_dir: str | None = None) -> bool:
89111 loader = unittest .TestLoader ()
90112 suite = loader .loadTestsFromModule (module )
91113
92- result = TAPTestResult ()
93- suite .run (result )
94- result .print_tap ()
114+ real_stdout = sys .stdout
115+ # TAP version header must be the very first line on stdout.
116+ real_stdout .write ('TAP version 13\n ' )
117+ real_stdout .flush ()
118+
119+ # Redirect stdout so any print() calls from setUp/tearDown/tests are
120+ # re-emitted as TAP diagnostic lines and do not break the TAP stream.
121+ sys .stdout = DiagnosticCapture (real_stdout ) # type: ignore[assignment]
122+ try :
123+ result = TAPTestResult ()
124+ suite .run (result )
125+ finally :
126+ sys .stdout .flush ()
127+ sys .stdout = real_stdout
128+
129+ result .print_tap (real_stdout )
95130 return result .wasSuccessful ()
96131
97132
0 commit comments