Skip to content

Commit dfc2486

Browse files
cristipufuclaude
andcommitted
test: add interceptor teardown tests for cp1252 stdout buffer preservation
Covers: - Buffer remains usable after teardown (not closed) - No ValueError when writing to stdout after teardown - utf8_stdout wrapper not created when encoding is already UTF-8 - Double-teardown safety (del guard) - Correct teardown order: detach before handler close - No wrapper created when job_id is set (file handler path) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1b18eef commit dfc2486

File tree

1 file changed

+163
-0
lines changed

1 file changed

+163
-0
lines changed

tests/test_interceptor.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
"""Tests for UiPathRuntimeLogsInterceptor teardown with non-UTF-8 stdout."""
2+
3+
import io
4+
import logging
5+
import sys
6+
from unittest.mock import patch
7+
8+
import pytest
9+
10+
from uipath.runtime.logging._interceptor import UiPathRuntimeLogsInterceptor
11+
12+
13+
@pytest.fixture(autouse=True)
14+
def _isolate_logging():
15+
"""Save and restore logging state so tests don't leak into each other."""
16+
root = logging.getLogger()
17+
original_level = root.level
18+
original_handlers = list(root.handlers)
19+
original_stdout = sys.stdout
20+
original_stderr = sys.stderr
21+
yield
22+
root.setLevel(original_level)
23+
root.handlers = original_handlers
24+
sys.stdout = original_stdout
25+
sys.stderr = original_stderr
26+
logging.disable(logging.NOTSET)
27+
28+
29+
def _make_cp1252_stdout() -> io.TextIOWrapper:
30+
"""Create a TextIOWrapper that mimics Windows cp1252 piped stdout."""
31+
raw_buffer = io.BytesIO()
32+
return io.TextIOWrapper(raw_buffer, encoding="cp1252", line_buffering=True)
33+
34+
35+
class TestInterceptorTeardownPreservesBuffer:
36+
"""Verify that teardown does not destroy the underlying stdout buffer."""
37+
38+
def test_buffer_usable_after_teardown(self):
39+
"""After setup+teardown the original stdout buffer must still be writable."""
40+
fake_stdout = _make_cp1252_stdout()
41+
42+
with patch.object(sys, "stdout", fake_stdout), patch.object(
43+
sys, "stderr", fake_stdout
44+
):
45+
interceptor = UiPathRuntimeLogsInterceptor(job_id=None)
46+
47+
# The wrapper should have been created because encoding is cp1252
48+
assert hasattr(interceptor, "utf8_stdout")
49+
50+
interceptor.setup()
51+
interceptor.teardown()
52+
53+
# The underlying buffer must still be open and writable
54+
assert not fake_stdout.buffer.closed
55+
fake_stdout.buffer.write(b"still alive")
56+
57+
def test_no_valueerror_writing_after_teardown(self):
58+
"""Writing to the original stdout after teardown must not raise ValueError."""
59+
fake_stdout = _make_cp1252_stdout()
60+
61+
with patch.object(sys, "stdout", fake_stdout), patch.object(
62+
sys, "stderr", fake_stdout
63+
):
64+
interceptor = UiPathRuntimeLogsInterceptor(job_id=None)
65+
interceptor.setup()
66+
interceptor.teardown()
67+
68+
# This simulates what click.echo() does — write to the restored stdout
69+
fake_stdout.write("no crash")
70+
fake_stdout.flush()
71+
72+
def test_utf8_stdout_not_created_for_utf8_encoding(self):
73+
"""When stdout is already UTF-8, no wrapper should be created."""
74+
utf8_stdout = io.TextIOWrapper(
75+
io.BytesIO(), encoding="utf-8", line_buffering=True
76+
)
77+
78+
with patch.object(sys, "stdout", utf8_stdout), patch.object(
79+
sys, "stderr", utf8_stdout
80+
):
81+
interceptor = UiPathRuntimeLogsInterceptor(job_id=None)
82+
83+
assert not hasattr(interceptor, "utf8_stdout")
84+
85+
def test_utf8_stdout_attr_removed_after_teardown(self):
86+
"""After teardown, the utf8_stdout attribute should be deleted (double-teardown guard)."""
87+
fake_stdout = _make_cp1252_stdout()
88+
89+
with patch.object(sys, "stdout", fake_stdout), patch.object(
90+
sys, "stderr", fake_stdout
91+
):
92+
interceptor = UiPathRuntimeLogsInterceptor(job_id=None)
93+
interceptor.setup()
94+
interceptor.teardown()
95+
96+
assert not hasattr(interceptor, "utf8_stdout")
97+
98+
def test_double_teardown_does_not_raise(self):
99+
"""Calling teardown twice must not raise (guarded by del)."""
100+
fake_stdout = _make_cp1252_stdout()
101+
102+
with patch.object(sys, "stdout", fake_stdout), patch.object(
103+
sys, "stderr", fake_stdout
104+
):
105+
interceptor = UiPathRuntimeLogsInterceptor(job_id=None)
106+
interceptor.setup()
107+
interceptor.teardown()
108+
# Second teardown should be safe
109+
interceptor.teardown()
110+
111+
112+
class TestInterceptorTeardownOrder:
113+
"""Verify that detach happens before handler close."""
114+
115+
def test_detach_called_before_handler_close(self):
116+
"""utf8_stdout.detach() must execute before log_handler.close()."""
117+
fake_stdout = _make_cp1252_stdout()
118+
call_order: list[str] = []
119+
120+
with patch.object(sys, "stdout", fake_stdout), patch.object(
121+
sys, "stderr", fake_stdout
122+
):
123+
interceptor = UiPathRuntimeLogsInterceptor(job_id=None)
124+
assert hasattr(interceptor, "utf8_stdout")
125+
126+
# Wrap detach and close to record call order
127+
original_detach = interceptor.utf8_stdout.detach
128+
original_close = interceptor.log_handler.close
129+
130+
def tracked_detach():
131+
call_order.append("detach")
132+
return original_detach()
133+
134+
def tracked_close():
135+
call_order.append("handler_close")
136+
return original_close()
137+
138+
interceptor.utf8_stdout.detach = tracked_detach
139+
interceptor.log_handler.close = tracked_close
140+
141+
interceptor.setup()
142+
interceptor.teardown()
143+
144+
assert "detach" in call_order
145+
assert "handler_close" in call_order
146+
assert call_order.index("detach") < call_order.index("handler_close")
147+
148+
149+
class TestInterceptorWithJobId:
150+
"""When job_id is set, a file handler is used — no utf8_stdout wrapper."""
151+
152+
def test_no_utf8_wrapper_with_job_id(self, tmp_path):
153+
"""File-based handler path should never create utf8_stdout."""
154+
fake_stdout = _make_cp1252_stdout()
155+
156+
with patch.object(sys, "stdout", fake_stdout), patch.object(
157+
sys, "stderr", fake_stdout
158+
):
159+
interceptor = UiPathRuntimeLogsInterceptor(
160+
job_id="job-123", dir=str(tmp_path), file="test.log"
161+
)
162+
163+
assert not hasattr(interceptor, "utf8_stdout")

0 commit comments

Comments
 (0)