|
18 | 18 | _IS_WINDOWS = os.name == "nt" |
19 | 19 |
|
20 | 20 |
|
| 21 | +def check_version(): |
| 22 | + """Minimal Python version checks and platform capability report.""" |
| 23 | + ver = sys.version_info |
| 24 | + assert ver >= (3, 6), "Python >= 3.6 required, got {}.{}".format(ver[0], ver[1]) |
| 25 | + print("Python {}.{}.{} on {}".format(ver[0], ver[1], ver[2], sys.platform)) |
| 26 | + |
| 27 | + if _IS_WINDOWS: |
| 28 | + has_get_blocking = hasattr(os, "get_blocking") |
| 29 | + print( |
| 30 | + " os.get_blocking: {}".format( |
| 31 | + "available" if has_get_blocking else "unavailable (Python <3.12)" |
| 32 | + ) |
| 33 | + ) |
| 34 | + if has_get_blocking: |
| 35 | + # Probe whether it actually works on console handles |
| 36 | + try: |
| 37 | + os.get_blocking(sys.stdout.fileno()) |
| 38 | + print(" os.get_blocking(stdout): works") |
| 39 | + except OSError: |
| 40 | + print(" os.get_blocking(stdout): OSError (console handle)") |
| 41 | + |
| 42 | + print("version checks passed") |
| 43 | + |
| 44 | + |
21 | 45 | def check_rawterm_units(): |
22 | 46 | """rawterm Color, Style, Key, Box -- no terminal required.""" |
23 | 47 | from rawterm import Style, Color, Key, Box, NAMED_COLORS |
@@ -165,6 +189,21 @@ def check_windows_console(): |
165 | 189 | stdout_h, out_mode.value |
166 | 190 | ), "SetConsoleMode(stdout, restore) failed" |
167 | 191 |
|
| 192 | + # Test VT100 + DISABLE_NEWLINE_AUTO_RETURN combination |
| 193 | + DISABLE_NEWLINE_AUTO_RETURN = 0x0008 |
| 194 | + new_out_both = ( |
| 195 | + out_mode.value |
| 196 | + | ENABLE_VIRTUAL_TERMINAL_PROCESSING |
| 197 | + | DISABLE_NEWLINE_AUTO_RETURN |
| 198 | + ) |
| 199 | + dnar_ok = kernel32.SetConsoleMode(stdout_h, new_out_both) |
| 200 | + kernel32.SetConsoleMode(stdout_h, out_mode.value) # always restore |
| 201 | + print( |
| 202 | + " DISABLE_NEWLINE_AUTO_RETURN: {}".format( |
| 203 | + "supported" if dnar_ok else "not supported (pre-1607)" |
| 204 | + ) |
| 205 | + ) |
| 206 | + |
168 | 207 | # Test VT100 input mode (may fail on older Windows -- not fatal) |
169 | 208 | ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200 |
170 | 209 | new_in = (in_mode.value | ENABLE_VIRTUAL_TERMINAL_INPUT) & ~0x0007 |
@@ -265,8 +304,61 @@ def check_menuconfig_headless(): |
265 | 304 | print("menuconfig headless + style validation passed") |
266 | 305 |
|
267 | 306 |
|
| 307 | +def check_flush_robustness(): |
| 308 | + """Verify _flush() survives missing or broken os.get_blocking. |
| 309 | +
|
| 310 | + Exercises the (AttributeError, OSError) handling added to fix |
| 311 | + issue #48: on Python <3.11, os.get_blocking does not exist; on |
| 312 | + Windows 3.11 console handles, it raises OSError. In both cases |
| 313 | + the flush must still run. |
| 314 | + """ |
| 315 | + import io |
| 316 | + from unittest import mock |
| 317 | + from rawterm import Terminal |
| 318 | + |
| 319 | + # Construct a Terminal without entering raw mode -- we only need |
| 320 | + # _flush() and _write_raw(), which operate on sys.stdout.buffer. |
| 321 | + term = object.__new__(Terminal) |
| 322 | + |
| 323 | + # Redirect stdout.buffer to a BytesIO so we can verify output |
| 324 | + buf = io.BytesIO() |
| 325 | + fake_stdout = mock.MagicMock() |
| 326 | + fake_stdout.buffer = buf |
| 327 | + fake_stdout.fileno.return_value = 1 |
| 328 | + |
| 329 | + with mock.patch("sys.stdout", fake_stdout): |
| 330 | + # Case 1: os.get_blocking raises AttributeError (Python <3.11) |
| 331 | + # create=True: os.get_blocking may not exist on Windows Python <3.12 |
| 332 | + with mock.patch("os.get_blocking", side_effect=AttributeError, create=True): |
| 333 | + term._write_raw("hello") |
| 334 | + term._flush() |
| 335 | + assert buf.getvalue() == b"hello", "flush after AttributeError" |
| 336 | + |
| 337 | + buf.seek(0) |
| 338 | + buf.truncate() |
| 339 | + |
| 340 | + # Case 2: os.get_blocking raises OSError (Windows console handle) |
| 341 | + with mock.patch("os.get_blocking", side_effect=OSError, create=True): |
| 342 | + term._write_raw(" world") |
| 343 | + term._flush() |
| 344 | + assert buf.getvalue() == b" world", "flush after OSError" |
| 345 | + |
| 346 | + buf.seek(0) |
| 347 | + buf.truncate() |
| 348 | + |
| 349 | + # Case 3: os.get_blocking works normally (returns True) |
| 350 | + with mock.patch("os.get_blocking", return_value=True, create=True): |
| 351 | + term._write_raw("ok") |
| 352 | + term._flush() |
| 353 | + assert buf.getvalue() == b"ok", "flush with normal get_blocking" |
| 354 | + |
| 355 | + print("_flush() robustness checks passed") |
| 356 | + |
| 357 | + |
268 | 358 | if __name__ == "__main__": |
| 359 | + check_version() |
269 | 360 | check_rawterm_units() |
| 361 | + check_flush_robustness() |
270 | 362 | check_windows_console() |
271 | 363 | check_terminal_init() |
272 | 364 | check_menuconfig_headless() |
|
0 commit comments