Coverage for tests / test_win_console.py: 100%
197 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-09 00:15 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-09 00:15 +0000
1"""
2Tests for the Windows console functionality.
3Created after vendoring Click to ensure test coverage is back up to 100%.
4"""
6import ctypes 1cdabefg
7import io 1cdabefg
8import sys 1cdabefg
10import pytest 1cdabefg
11import typer 1cdabefg
12from typer.testing import CliRunner 1cdabefg
14from .utils import needs_windows 1cdabefg
16pytestmark = needs_windows 1cdabefg
19if sys.platform == "win32": 1cdabefg
20 from typer._click import _compat, _winconsole 1ab
23def _identity_buffer(obj, writable=False): # noqa: ARG001 1cdabefg
24 return obj 1ab
27def _route_console_stream(target_name, wrapper, state=None): 1cdabefg
28 def patched_windows_stream(stream, encoding, errors): # noqa: ARG001 1ab
29 current_target = getattr(sys, target_name) 1ab
30 if stream is current_target: 1ab
31 if state is not None and target_name == "stderr": 1ab
32 state["stderr_wrap_calls"] += 1 1ab
33 buffer = getattr(stream, "buffer", None) 1ab
34 return wrapper(buffer) if buffer else None 1ab
35 return None 1ab
37 return patched_windows_stream 1ab
40def _capture_write_console(state): 1cdabefg
41 def fake_write_console(handle, buffer, units_to_write, units_written_ptr, reserved): # noqa: ARG001 1ab
42 state["write_calls"] += 1 1ab
43 bytes_to_write = units_to_write * 2 1ab
44 state["written"].extend(buffer[:bytes_to_write]) 1ab
45 units_written_ptr._obj.value = units_to_write 1ab
46 return 1 1ab
48 return fake_write_console 1ab
51def test_winconsole_stdin(monkeypatch): 1cdabefg
52 runner = CliRunner() 1ab
53 app = typer.Typer() 1ab
55 @app.command() 1ab
56 def read_name(config: typer.FileText = typer.Option(...)) -> None: 1ab
57 name = config.readline().strip() 1ab
58 typer.echo(f"Hello {name}") 1ab
60 utf16_data = bytearray("Rick\r\n".encode("utf-16-le")) 1ab
61 state = {"pos": 0, "read_calls": 0} 1ab
63 def fake_read_console(handle, buffer, units_to_read, units_read_ptr, reserved): # noqa: ARG001 1ab
64 state["read_calls"] += 1 1ab
65 max_bytes = units_to_read * 2 1ab
66 chunk = utf16_data[state["pos"] : state["pos"] + max_bytes] 1ab
67 if chunk: 1ab
68 buffer[0 : len(chunk)] = chunk 1ab
69 state["pos"] += len(chunk) 1ab
70 units_read_ptr._obj.value = len(chunk) // 2 1ab
71 return 1 1ab
73 return 1 # pragma: no cover
75 monkeypatch.setattr(_winconsole, "get_buffer", _identity_buffer) 1ab
76 monkeypatch.setattr(_winconsole, "ReadConsoleW", fake_read_console) 1ab
77 monkeypatch.setattr(_winconsole, "GetLastError", lambda: 0) 1ab
78 monkeypatch.setattr( 1ab
79 _compat,
80 "_get_windows_console_stream",
81 _route_console_stream("stdin", _winconsole._get_text_stdin),
82 )
84 result = runner.invoke(app, ["--config", "-"]) 1ab
85 assert result.exit_code == 0, result.output 1ab
86 assert "Hello Rick" in result.stdout 1ab
87 assert state["read_calls"] > 0 1ab
90def test_winconsole_stdout(monkeypatch): 1cdabefg
91 runner = CliRunner() 1ab
92 app = typer.Typer() 1ab
93 state = {"write_calls": 0, "written": bytearray()} 1ab
95 @app.command() 1ab
96 def write_message(out: typer.FileTextWrite = typer.Option(...)) -> None: 1ab
97 out.write("Hello Summer\n") 1ab
99 monkeypatch.setattr(_winconsole, "get_buffer", _identity_buffer) 1ab
100 monkeypatch.setattr(_winconsole, "WriteConsoleW", _capture_write_console(state)) 1ab
101 monkeypatch.setattr(_winconsole, "GetLastError", lambda: 0) 1ab
102 monkeypatch.setattr( 1ab
103 _compat,
104 "_get_windows_console_stream",
105 _route_console_stream("stdout", _winconsole._get_text_stdout),
106 )
108 result = runner.invoke(app, ["--out", "-"]) 1ab
109 assert result.exit_code == 0, result.output 1ab
110 assert state["write_calls"] > 0 1ab
111 assert _winconsole._WindowsConsoleWriter(1).isatty() is True 1ab
112 decoded = state["written"].decode("utf-16-le", errors="ignore") 1ab
113 assert "Hello Summer\r\n" in decoded 1ab
116def test_winconsole_stderr(monkeypatch): 1cdabefg
117 runner = CliRunner() 1ab
118 app = typer.Typer() 1ab
119 state = {"write_calls": 0, "written": bytearray(), "stderr_wrap_calls": 0} 1ab
121 @app.command() 1ab
122 def main() -> None: 1ab
123 typer.echo("Ran out of adventure time!", err=True) 1ab
125 monkeypatch.setattr(_winconsole, "get_buffer", _identity_buffer) 1ab
126 monkeypatch.setattr(_winconsole, "WriteConsoleW", _capture_write_console(state)) 1ab
127 monkeypatch.setattr(_winconsole, "GetLastError", lambda: 0) 1ab
128 monkeypatch.setattr( 1ab
129 _compat,
130 "_get_windows_console_stream",
131 _route_console_stream("stderr", _winconsole._get_text_stderr, state),
132 )
134 result = runner.invoke(app) 1ab
135 assert result.exit_code == 0, result.output 1ab
136 assert state["stderr_wrap_calls"] > 0 1ab
137 assert state["write_calls"] > 0 1ab
138 decoded = state["written"].decode("utf-16-le", errors="ignore") 1ab
139 assert "Ran out of adventure time!\r\n" in decoded 1ab
142@pytest.mark.parametrize( 1cdabefg
143 ("writable", "source", "expected_flags"),
144 [
145 (True, bytearray(b"python"), 1), # PyBUF_WRITABLE
146 (False, b"python", 0), # PyBUF_SIMPLE
147 ],
148)
149def test_get_buffer(monkeypatch, writable, source, expected_flags): 1cdabefg
150 state = {"flags": None, "released": 0} 1ab
151 if writable: 1ab
152 backing = (_winconsole.c_char * len(source)).from_buffer(source) 1ab
153 else:
154 backing = (_winconsole.c_char * len(source)).from_buffer_copy(source) 1ab
155 backing_ptr = _winconsole.c_void_p(ctypes.addressof(backing)) 1ab
157 def fake_object_get_buffer(obj, buf_ref, flags): # noqa: ARG001 1ab
158 state["flags"] = flags 1ab
159 buf = buf_ref._obj 1ab
160 buf.buf = backing_ptr 1ab
161 buf.len = len(source) 1ab
163 def fake_buffer_release(buf_ref): # noqa: ARG001 1ab
164 state["released"] += 1 1ab
166 monkeypatch.setattr(_winconsole, "PyObject_GetBuffer", fake_object_get_buffer) 1ab
167 monkeypatch.setattr(_winconsole, "PyBuffer_Release", fake_buffer_release) 1ab
169 probe = source if writable else b"x" 1ab
170 out = _winconsole.get_buffer(probe, writable=writable) 1ab
171 if writable: 1ab
172 # mutate the first byte of "python" to obtain another beloved programming language
173 out[0] = b"c" 1ab
174 assert source == bytearray(b"cython") 1ab
175 else:
176 assert bytes(out[: len(source)]) == source 1ab
177 assert state["flags"] == expected_flags 1ab
178 assert state["released"] == 1 1ab
181def test_isatty(): 1cdabefg
182 assert _winconsole._WindowsConsoleRawIOBase(None).isatty() is True 1ab
183 assert _winconsole._WindowsConsoleReader(0).isatty() is True 1ab
184 assert _winconsole._WindowsConsoleReader(1).isatty() is True 1ab
187def test_console_stream(): 1cdabefg
188 class NamedBytesIO(io.BytesIO): 1ab
189 name = "fake-buffer" 1ab
191 def isatty(self): 1ab
192 return False 1ab
194 stream = _winconsole.ConsoleStream( 1ab
195 io.TextIOWrapper(io.BytesIO(), encoding="utf-8"), NamedBytesIO()
196 )
197 assert stream.isatty() is False 1ab
198 assert stream.name == "fake-buffer" 1ab
199 assert "fake-buffer" in repr(stream) 1ab
200 assert "utf-8" in repr(stream) 1ab
202 # test writelines
203 stream.writelines(["hello", " ", "world"]) 1ab
204 stream._text_stream.flush() 1ab
205 assert stream._text_stream.buffer.getvalue().decode("utf-8") == "hello world" 1ab
207 # Cover bytes write path.
208 assert stream.write(b"!") == 1 1ab
209 assert stream.buffer.getvalue().endswith(b"!") 1ab
212@pytest.mark.parametrize( 1cdabefg
213 ("error", "msg"),
214 [
215 (0, "ERROR_SUCCESS"), # ERROR_SUCCESS
216 (8, "ERROR_NOT_ENOUGH_MEMORY"), # ERROR_NOT_ENOUGH_MEMORY
217 (342, "Windows error 342"),
218 ],
219)
220def test_error_message(error, msg): 1cdabefg
221 writer = _winconsole._WindowsConsoleWriter 1ab
222 assert writer._get_error_message(error) == msg 1ab
225def test_is_console(): 1cdabefg
226 assert _winconsole._is_console(object()) is False 1ab
229def test_get_windows_console_stream_factory_and_buffer_paths(monkeypatch): 1cdabefg
230 monkeypatch.setattr(_winconsole, "_is_console", lambda f: True) 1ab
231 monkeypatch.setattr(_winconsole, "get_buffer", object()) 1ab
233 class FakeStream: 1ab
234 def __init__(self, fd, buffer=None): 1ab
235 self._fd = fd 1ab
236 self.buffer = buffer 1ab
238 def fileno(self): 1ab
239 return self._fd 1ab
241 wrapped = {"called": False, "buffer": None} 1ab
243 def fake_factory(buffer): 1ab
244 wrapped["called"] = True 1ab
245 wrapped["buffer"] = buffer 1ab
246 return "wrapped", buffer 1ab
248 monkeypatch.setattr(_winconsole, "_stream_factories", {7: fake_factory}) 1ab
250 # Known console stream preconditions pass, but no stream factory for this fd.
251 get_stream = _winconsole._get_windows_console_stream 1ab
252 assert get_stream(FakeStream(99, object()), "utf-16-le", "strict") is None 1ab
254 # Factory exists, but stream has no usable .buffer.
255 assert get_stream(FakeStream(7, None), "utf-16-le", "strict") is None 1ab
257 # Factory exists and buffer is present, so wrapper result is returned.
258 raw_buffer = object() 1ab
259 out = get_stream(FakeStream(7, raw_buffer), "utf-16-le", "strict") 1ab
260 assert out == ("wrapped", raw_buffer) 1ab
261 assert wrapped["called"] is True 1ab
262 assert wrapped["buffer"] is raw_buffer 1ab
265def test_windows_console_reader(monkeypatch): 1cdabefg
266 reader = _winconsole._WindowsConsoleReader(42) 1ab
268 # Empty input buffer returns early
269 assert reader.readinto(bytearray()) == 0 1ab
271 # Require an even number of bytes
272 with pytest.raises(ValueError): 1ab
273 reader.readinto(bytearray(3)) 1ab
275 def writable_buffer(obj, writable=False): # noqa: ARG001 1ab
276 return (ctypes.c_char * len(obj)).from_buffer(obj) 1ab
278 monkeypatch.setattr(_winconsole, "get_buffer", writable_buffer) 1ab
280 def patch_console(read_console, error): 1ab
281 monkeypatch.setattr(_winconsole, "ReadConsoleW", read_console) 1ab
282 monkeypatch.setattr(_winconsole, "GetLastError", lambda: error) 1ab
284 def make_read(payload=b"", rv=1, units_read=None): 1ab
285 def read_console(handle, buffer, units_to_read, units_read_ptr, reserved): # noqa: ARG001 1ab
286 bytes_to_copy = min(len(payload), units_to_read * 2) 1ab
287 if bytes_to_copy: 1ab
288 buffer[0:bytes_to_copy] = payload[:bytes_to_copy] 1ab
289 read_units = units_read if units_read is not None else bytes_to_copy // 2 1ab
290 units_read_ptr._obj.value = read_units 1ab
291 return rv 1ab
293 return read_console 1ab
295 # Normal successful read returns the number of bytes read
296 patch_console(make_read(payload=b"A\x00B\x00"), _winconsole.ERROR_SUCCESS) 1ab
297 assert reader.readinto(bytearray(4)) == 4 1ab
299 # CTRL+Z (EOF) should be translated into an empty read
300 patch_console( 1ab
301 make_read(payload=_winconsole.EOF + b"\x00", units_read=1),
302 _winconsole.ERROR_SUCCESS,
303 )
304 assert reader.readinto(bytearray(2)) == 0 1ab
306 # An aborted read should sleep briefly while waiting for KeyboardInterrupt
307 sleep_state = {"calls": 0} 1ab
309 def fake_sleep(seconds): 1ab
310 sleep_state["calls"] += 1 1ab
311 assert seconds == 0.1 1ab
313 monkeypatch.setattr( 1ab
314 _winconsole, "time", type("FakeTime", (), {"sleep": fake_sleep})
315 )
316 patch_console( 1ab
317 make_read(payload=b"Z\x00", units_read=1),
318 _winconsole.ERROR_OPERATION_ABORTED,
319 )
320 assert reader.readinto(bytearray(2)) == 2 1ab
321 assert sleep_state["calls"] == 1 1ab
323 # Failed reads propagate a Windows error
324 patch_console(make_read(rv=0), _winconsole.ERROR_NOT_ENOUGH_MEMORY) 1ab
325 with pytest.raises(OSError, match="Windows error"): 1ab
326 reader.readinto(bytearray(2)) 1ab