Skip to content

Commit aaaa9b7

Browse files
Copilotigaw
andauthored
tests: fix tap_runner to route diagnostic output to stderr
Agent-Logs-Url: https://github.com/igaw/nvme-cli/sessions/7ae055d8-4c72-40a1-bd86-f23660b6c0bb Co-authored-by: igaw <1050803+igaw@users.noreply.github.com>
1 parent e4eaf26 commit aaaa9b7

2 files changed

Lines changed: 16 additions & 115 deletions

File tree

tests/nvme_test.py

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,31 +36,6 @@
3636
from nvme_test_logger import TestNVMeLogger
3737

3838

39-
def find_config_file(filename='config.json'):
40-
""" Search for a configuration file by walking up the directory tree.
41-
42-
Starting from the directory containing this module, walk toward the
43-
filesystem root until a file with the given name is found.
44-
- Args:
45-
- filename: name of the config file to search for
46-
- Returns:
47-
- Absolute path to the config file
48-
- Raises:
49-
- FileNotFoundError if the file is not found in any ancestor directory
50-
"""
51-
directory = os.path.dirname(os.path.abspath(__file__))
52-
while True:
53-
candidate = os.path.join(directory, filename)
54-
if os.path.isfile(candidate):
55-
return candidate
56-
parent = os.path.dirname(directory)
57-
if parent == directory:
58-
raise FileNotFoundError(
59-
f"Config file '{filename}' not found in '{os.path.dirname(os.path.abspath(__file__))}' "
60-
f"or any of its parent directories")
61-
directory = parent
62-
63-
6439
def to_decimal(value):
6540
""" Wrapper for converting numbers to base 10 decimal
6641
- Args:
@@ -102,7 +77,7 @@ def setUp(self):
10277
self.do_validate_pci_device = True
10378
self.default_nsid = 0x1
10479
self.flbas = 0
105-
self.config_file = find_config_file()
80+
self.config_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.json')
10681

10782
self.load_config()
10883
if self.do_validate_pci_device:

tests/tap_runner.py

Lines changed: 15 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -15,86 +15,18 @@
1515
import argparse
1616
import importlib
1717
import io
18-
import os
1918
import sys
20-
import threading
2119
import traceback
2220
import unittest
2321

2422

25-
class DiagnosticCapture(io.TextIOBase):
26-
"""Capture writes and re-emit them as TAP diagnostic lines (# ...)."""
27-
28-
def __init__(self, real_stdout: io.TextIOBase) -> None:
29-
self._real = real_stdout
30-
self._buf = ''
31-
32-
def write(self, text: str) -> int:
33-
self._buf += text
34-
while '\n' in self._buf:
35-
line, self._buf = self._buf.split('\n', 1)
36-
self._real.write('# {}\n'.format(line))
37-
self._real.flush()
38-
return len(text)
39-
40-
def flush(self) -> None:
41-
if self._buf:
42-
self._real.write('# {}\n'.format(self._buf))
43-
self._buf = ''
44-
self._real.flush()
45-
46-
47-
class FDCapture:
48-
"""Redirect a file descriptor at the OS level and re-emit captured output
49-
as TAP diagnostic lines. This intercepts writes from subprocesses which
50-
bypass the Python-level sys.stderr redirect."""
51-
52-
def __init__(self, fd: int, real_stdout: io.TextIOBase) -> None:
53-
self._fd = fd
54-
self._real = real_stdout
55-
self._saved_fd = os.dup(fd)
56-
r_fd, w_fd = os.pipe()
57-
os.dup2(w_fd, fd)
58-
os.close(w_fd)
59-
self._thread = threading.Thread(target=self._reader, args=(r_fd,),
60-
daemon=True)
61-
# daemon=True: if restore() is somehow never called (e.g. os._exit()),
62-
# the process can still exit rather than hang on a blocking read.
63-
self._thread.start()
64-
65-
def _reader(self, r_fd: int) -> None:
66-
buf = b''
67-
# Open unbuffered (bufsize=0) so bytes are delivered to the reader
68-
# as soon as they are written, without waiting for a buffer to fill.
69-
with open(r_fd, 'rb', 0) as f:
70-
while True:
71-
chunk = f.read(4096)
72-
if not chunk:
73-
break
74-
buf += chunk
75-
while b'\n' in buf:
76-
line, buf = buf.split(b'\n', 1)
77-
self._real.write(
78-
'# {}\n'.format(line.decode('utf-8', errors='replace')))
79-
self._real.flush()
80-
if buf:
81-
self._real.write(
82-
'# {}\n'.format(buf.decode('utf-8', errors='replace')))
83-
self._real.flush()
84-
85-
def restore(self) -> None:
86-
"""Restore the original file descriptor and wait for the reader to drain."""
87-
os.dup2(self._saved_fd, self._fd)
88-
os.close(self._saved_fd)
89-
self._thread.join()
90-
91-
9223
class TAPTestResult(unittest.TestResult):
9324
"""Collect unittest results and render them as TAP version 13."""
9425

95-
def __init__(self, stream: io.TextIOBase) -> None:
26+
def __init__(self, stream: io.TextIOBase, diag_stream: io.TextIOBase) -> None:
9627
super().__init__()
9728
self._stream = stream
29+
self._diag_stream = diag_stream
9830
self._test_count = 0
9931

10032
def _description(self, test: unittest.TestCase) -> str:
@@ -112,20 +44,20 @@ def addError(self, test: unittest.TestCase, err: object) -> None:
11244
self._test_count += 1
11345
self._stream.write('not ok {} - {}\n'.format(
11446
self._test_count, self._description(test)))
115-
for line in traceback.format_exception(*err): # type: ignore[misc]
116-
for subline in line.splitlines():
117-
self._stream.write('# {}\n'.format(subline))
11847
self._stream.flush()
48+
self._diag_stream.write(
49+
''.join(traceback.format_exception(*err))) # type: ignore[misc]
50+
self._diag_stream.flush()
11951

12052
def addFailure(self, test: unittest.TestCase, err: object) -> None:
12153
super().addFailure(test, err)
12254
self._test_count += 1
12355
self._stream.write('not ok {} - {}\n'.format(
12456
self._test_count, self._description(test)))
125-
for line in traceback.format_exception(*err): # type: ignore[misc]
126-
for subline in line.splitlines():
127-
self._stream.write('# {}\n'.format(subline))
12857
self._stream.flush()
58+
self._diag_stream.write(
59+
''.join(traceback.format_exception(*err))) # type: ignore[misc]
60+
self._diag_stream.flush()
12961

13062
def addSkip(self, test: unittest.TestCase, reason: str) -> None:
13163
super().addSkip(test, reason)
@@ -165,23 +97,17 @@ def run_tests(test_module_name: str, start_dir: str | None = None) -> bool:
16597
real_stdout.write('1..{}\n'.format(suite.countTestCases()))
16698
real_stdout.flush()
16799

168-
# Redirect stdout and stderr so any print()/sys.stderr.write() calls from
169-
# setUp/tearDown/tests are re-emitted as TAP diagnostic lines and do not
170-
# break the TAP stream.
171-
sys.stdout = DiagnosticCapture(real_stdout) # type: ignore[assignment]
172-
sys.stderr = DiagnosticCapture(real_stdout) # type: ignore[assignment]
173-
# Also redirect fd 2 at the OS level so that subprocess stderr (which
174-
# inherits the raw file descriptor and bypasses sys.stderr) is captured.
175-
stderr_fd_capture = FDCapture(2, real_stdout)
100+
# Redirect sys.stdout to real_stderr so that print()/sys.stdout.write()
101+
# calls from setUp/tearDown/tests do not pollute the TAP stream on stdout.
102+
# All diagnostic output (print statements, subprocess stderr via fd 2, and
103+
# test failure tracebacks) goes to stderr so that 'meson test -v' shows it
104+
# live on the terminal.
105+
sys.stdout = real_stderr # type: ignore[assignment]
176106
try:
177-
result = TAPTestResult(real_stdout)
107+
result = TAPTestResult(real_stdout, real_stderr)
178108
suite.run(result)
179109
finally:
180-
sys.stdout.flush()
181110
sys.stdout = real_stdout
182-
sys.stderr.flush()
183-
sys.stderr = real_stderr
184-
stderr_fd_capture.restore()
185111

186112
return result.wasSuccessful()
187113

0 commit comments

Comments
 (0)