The OS lanes are marker-driven: list_os_marked_tests.py picks the files
a lane imports from their platforms() specs and the lane selects with
-m platforms. A test gated with skipif(sys.platform != "win32") is
therefore never imported on the Windows lane and skipped everywhere else
— it runs on no host. skipif(sys.platform == "win32") tests were merely
invisible to the lane bookkeeping, but the rule the tree now follows is
one host marker, never a bare skipif.
Mechanical mapping, semantics preserved: skip-on-Windows → "posix",
skip-off-Windows → "windows", skip-off-Linux → "linux", skip-on-macOS →
"not macos". The former skip reasons stay as trailing comments. A
non-host condition (os.geteuid() == 0) stays a separate skipif beside
the marker, spelled getattr(os, "geteuid", ...) so the decorator still
imports on Windows.
Where the conversion would stack two platforms() marks on one test (the
conftest rejects that at collection) the narrower mark wins:
- test_update_wedged_gateway: the class is already platforms("linux");
its per-test "needs UNIX sockets" marks were redundant and are gone.
- test_process_registry.TestSystemdCgroupIsolation: the class-level
skip-on-Windows moves onto the 11 methods that had no host mark; the
11 platforms("linux") methods keep theirs.
- test_file_ops_single_roundtrip: the two fifo tests drop their
platforms("linux") in favour of the module's "posix" (mkfifo exists on
macOS; both tests already skip when it does not).
- test_linux_desktop_entry / test_gateway_job_teardown_live: duplicate
or wider marks removed.
174 lines
5.1 KiB
Python
174 lines
5.1 KiB
Python
"""Tests for tools.pty_query_responder (port of openai/codex#41436).
|
|
|
|
Covers the byte-level scanner (exact queries, chunk splits, DEC private-mode
|
|
queries, passthrough of unhandled sequences) and a live PTY E2E where a
|
|
subprocess blocks on a cursor-position report until answered.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from tools.pty_query_responder import PtyQueryResponder
|
|
|
|
|
|
def test_plain_output_passes_through():
|
|
r = PtyQueryResponder()
|
|
out, resp = r.process(b"hello world\n")
|
|
assert out == b"hello world\n"
|
|
assert resp == b""
|
|
|
|
|
|
def test_device_status_report_answered_and_stripped():
|
|
r = PtyQueryResponder()
|
|
out, resp = r.process(b"before\x1b[5nafter")
|
|
assert out == b"beforeafter"
|
|
assert resp == b"\x1b[0n"
|
|
|
|
|
|
def test_window_size_query_reports_spawn_dimensions():
|
|
r = PtyQueryResponder(rows=30, cols=120)
|
|
out, resp = r.process(b"\x1b[18t")
|
|
assert out == b""
|
|
assert resp == b"\x1b[8;30;120t"
|
|
|
|
|
|
def test_cursor_position_report():
|
|
r = PtyQueryResponder()
|
|
out, resp = r.process(b"\x1b[6n")
|
|
assert out == b""
|
|
assert resp == b"\x1b[1;1R"
|
|
|
|
|
|
def test_dec_private_mode_query_reported_unrecognized():
|
|
r = PtyQueryResponder()
|
|
out, resp = r.process(b"\x1b[?1049$p")
|
|
assert out == b""
|
|
assert resp == b"\x1b[?1049;0$y"
|
|
|
|
|
|
def test_combined_stream_matches_codex_fixture():
|
|
# Mirrors the driver-backed test in openai/codex#41436: queries split
|
|
# across chunks, mixed with a color escape and plain text.
|
|
r = PtyQueryResponder()
|
|
out1, resp1 = r.process(b"before\x1b[")
|
|
out2, resp2 = r.process(b"5n\x1b[18t\x1b[6n\x1b[?1049$p\x1b[31mafter")
|
|
assert resp1 + resp2 == b"\x1b[0n\x1b[8;24;80t\x1b[1;1R\x1b[?1049;0$y"
|
|
assert out1 + out2 + r.flush() == b"before\x1b[31mafter"
|
|
|
|
|
|
def test_query_split_across_many_chunks():
|
|
r = PtyQueryResponder()
|
|
total_out = b""
|
|
total_resp = b""
|
|
for b in (b"\x1b", b"[", b"6", b"n"):
|
|
out, resp = r.process(b)
|
|
total_out += out
|
|
total_resp += resp
|
|
assert total_out == b""
|
|
assert total_resp == b"\x1b[1;1R"
|
|
|
|
|
|
def test_unhandled_csi_sequence_passes_through():
|
|
r = PtyQueryResponder()
|
|
out, resp = r.process(b"\x1b[31mred\x1b[0m")
|
|
assert out == b"\x1b[31mred\x1b[0m"
|
|
assert resp == b""
|
|
|
|
|
|
def test_non_csi_escape_passes_through():
|
|
r = PtyQueryResponder()
|
|
out, resp = r.process(b"\x1bMreverse")
|
|
assert out == b"\x1bMreverse"
|
|
assert resp == b""
|
|
|
|
|
|
def test_fresh_esc_aborts_partial_sequence():
|
|
r = PtyQueryResponder()
|
|
out, resp = r.process(b"\x1b[6\x1b[5n")
|
|
# The aborted partial "\x1b[6" is flushed through; the complete
|
|
# device-status query is answered and stripped.
|
|
assert out == b"\x1b[6"
|
|
assert resp == b"\x1b[0n"
|
|
|
|
|
|
def test_oversized_mode_query_passes_through():
|
|
r = PtyQueryResponder()
|
|
seq = b"\x1b[?12345678901$p" # 11 digits > MAX_MODE_DIGITS
|
|
out, resp = r.process(seq)
|
|
assert resp == b""
|
|
assert out + r.flush() == seq
|
|
|
|
|
|
def test_flush_returns_incomplete_tail():
|
|
r = PtyQueryResponder()
|
|
out, resp = r.process(b"text\x1b[1")
|
|
assert out == b"text"
|
|
assert resp == b""
|
|
assert r.flush() == b"\x1b[1"
|
|
# flush is destructive
|
|
assert r.flush() == b""
|
|
|
|
|
|
def test_dec_mode_non_digit_passes_through():
|
|
r = PtyQueryResponder()
|
|
seq = b"\x1b[?10a9$p"
|
|
out, resp = r.process(seq)
|
|
assert resp == b""
|
|
assert out + r.flush() == seq
|
|
|
|
|
|
@pytest.mark.platforms("posix") # POSIX ptyprocess only
|
|
def test_live_pty_subprocess_unblocked_by_cursor_report(tmp_path):
|
|
"""E2E: a PTY subprocess blocking on ESC[6n exits once answered.
|
|
|
|
Mirrors direct_terminal_queries_are_answered from openai/codex#41436.
|
|
"""
|
|
ptyprocess = pytest.importorskip("ptyprocess")
|
|
from tools.pty_query_responder import PtyQueryResponder
|
|
|
|
script = (
|
|
"stty -echo -icanon; printf 'alpha\\033[6n'; "
|
|
"dd bs=1 count=6 2>/dev/null; printf '\\nok'"
|
|
)
|
|
proc = ptyprocess.PtyProcess.spawn(
|
|
["/bin/sh", "-c", script], dimensions=(24, 80)
|
|
)
|
|
responder = PtyQueryResponder()
|
|
output = b""
|
|
deadline = time.time() + 10
|
|
try:
|
|
while proc.isalive() and time.time() < deadline:
|
|
try:
|
|
chunk = proc.read(4096)
|
|
except EOFError:
|
|
break
|
|
if not chunk:
|
|
continue
|
|
out, replies = responder.process(chunk)
|
|
output += out
|
|
if replies:
|
|
proc.write(replies)
|
|
# Drain anything left after exit.
|
|
while True:
|
|
try:
|
|
chunk = proc.read(4096)
|
|
except EOFError:
|
|
break
|
|
if not chunk:
|
|
break
|
|
out, replies = responder.process(chunk)
|
|
output += out
|
|
finally:
|
|
if proc.isalive():
|
|
proc.terminate(force=True)
|
|
pytest.fail(f"subprocess still blocked; output={output!r}")
|
|
output += responder.flush()
|
|
assert b"alpha" in output
|
|
assert b"ok" in output
|
|
# The query itself must have been stripped from the captured output,
|
|
# and the echoed reply is what dd consumed (not visible w/ -echo).
|
|
assert b"\x1b[6n" not in output
|