Skip to content

Commit beb8e94

Browse files
committed
fix: sync verify_output_token flaky fix from lab1 (retry loop with 5s deadline)
1 parent 7b332e2 commit beb8e94

1 file changed

Lines changed: 94 additions & 9 deletions

File tree

harness/verifier.py

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def verify_env_check(
3333
expect_rows: Optional[int] = None,
3434
expect_cols: Optional[int] = None,
3535
) -> Result:
36+
"""Verify that probe reported correct environment."""
3637
msgs = self._sb.get_messages(session_id=session_id, msg_type="env_check")
3738
if not msgs:
3839
return Result(False, f"No env_check message from session {session_id}")
@@ -52,6 +53,7 @@ def verify_env_check(
5253
return Result(True, "env_check OK", data=msg)
5354

5455
def verify_pgrp_isolation(self, sessions: list[str]) -> Result:
56+
"""Verify that probes in different panes have different process groups."""
5557
pgrps = {}
5658
for sid in sessions:
5759
msgs = self._sb.get_messages(session_id=sid, msg_type="env_check")
@@ -68,6 +70,7 @@ def verify_signal_delivery(
6870
session_id: str,
6971
signal_name: str,
7072
) -> Result:
73+
"""Verify that probe received a specific signal."""
7174
msgs = self._sb.get_messages(session_id=session_id, msg_type="signal")
7275
matching = [m for m in msgs if m.get("signal") == signal_name]
7376
if not matching:
@@ -84,9 +87,13 @@ def verify_signal_isolation(
8487
inactive: list[str],
8588
signal: str,
8689
) -> Result:
90+
"""Verify signal was received by active pane but NOT by inactive panes."""
91+
# Active should have it
8792
active_result = self.verify_signal_delivery(active, signal)
8893
if not active_result.passed:
8994
return Result(False, f"Active pane {active} did not receive {signal}")
95+
96+
# Inactive should not
9097
for sid in inactive:
9198
msgs = self._sb.get_messages(session_id=sid, msg_type="signal")
9299
matching = [m for m in msgs if m.get("signal") == signal]
@@ -99,20 +106,53 @@ def verify_signal_isolation(
99106
return Result(True, f"{signal} isolation OK: {active} got it, {inactive} did not")
100107

101108
def verify_output_token(self, session_id: str) -> Result:
102-
msgs = self._sb.get_messages(session_id=session_id, msg_type="output_token")
103-
if not msgs:
104-
return Result(False, f"No output_token from {session_id}")
105-
token = msgs[0]["token"]
106-
raw = self._driver.read_raw(timeout=2.0)
107-
if token.encode() in raw:
108-
return Result(True, "output_token passthrough OK", data={"token": token})
109+
"""Verify that probe's output token appeared in client byte stream.
110+
111+
1. Wait for probe to emit a fresh token via sideband (proves probe alive).
112+
2. Read client output and check if that token traversed the PTY pipeline.
113+
114+
Probe sends sideband first then stdout, so by the time we see the
115+
sideband message, the token is already in the PTY/client buffer.
116+
"""
117+
import time as _time
118+
old_count = len(self._sb.get_messages(
119+
session_id=session_id, msg_type="output_token"))
120+
# Step 1: wait for a fresh sideband token (probe emits every ~1s)
121+
deadline = _time.monotonic() + 3.0
122+
new_msgs = []
123+
while _time.monotonic() < deadline:
124+
new_msgs = self._sb.get_messages(
125+
session_id=session_id, msg_type="output_token")[old_count:]
126+
if new_msgs:
127+
break
128+
_time.sleep(0.2)
129+
if not new_msgs:
130+
return Result(False,
131+
f"No new output_token from probe within 3s "
132+
f"(had {old_count} before)")
133+
# Step 2: read client output in a loop — under high-throughput
134+
# scenarios, read_raw may return early (pexpect shrinks timeout
135+
# after the first chunk), so retry until the token appears or we
136+
# hit the overall deadline.
137+
raw = b""
138+
read_deadline = _time.monotonic() + 5.0
139+
while _time.monotonic() < read_deadline:
140+
raw += self._driver.read_raw(timeout=1.0)
141+
for m in reversed(new_msgs):
142+
if m["token"].encode() in raw:
143+
return Result(True, "output_token passthrough OK",
144+
data={"token": m["token"]})
145+
_time.sleep(0.1)
109146
return Result(
110147
False,
111-
f"Token {token} not found in client output ({len(raw)} bytes read)",
112-
data={"token": token, "raw_len": len(raw)},
148+
f"Token {new_msgs[-1]['token']} not found in client output "
149+
f"({len(raw)} bytes read, {len(new_msgs)} new tokens from sideband)",
150+
data={"token": new_msgs[-1]["token"], "raw_len": len(raw),
151+
"new_token_count": len(new_msgs)},
113152
)
114153

115154
def verify_input_token(self, session_id: str) -> Result:
155+
"""Send a token via client stdin and verify probe received it."""
116156
token = secrets.token_hex(16)
117157
self._driver.send_line(token)
118158

@@ -128,3 +168,48 @@ def verify_input_token(self, session_id: str) -> Result:
128168
f"Probe did not report receiving token {token}",
129169
data={"sent": token, "received_msgs": msgs},
130170
)
171+
172+
def verify_file_contains(self, file_path: str, pattern: str) -> Result:
173+
"""Check that a file exists and contains the given pattern."""
174+
if not os.path.exists(file_path):
175+
return Result(False, f"File not found: {file_path}")
176+
with open(file_path, "r", errors="replace") as f:
177+
content = f.read()
178+
if pattern in content:
179+
return Result(True, f"Pattern found in {file_path}")
180+
return Result(False, f"Pattern '{pattern}' not in {file_path} ({len(content)} bytes)")
181+
182+
def verify_fd_count(self, pid: int, max_fds: int) -> Result:
183+
"""Check that a process has at most max_fds open file descriptors."""
184+
try:
185+
proc = psutil.Process(pid)
186+
fds = proc.num_fds()
187+
except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
188+
return Result(False, f"Cannot check fds: {e}")
189+
passed = fds <= max_fds
190+
return Result(passed, f"fd count: {fds} (max {max_fds})", data={"fds": fds})
191+
192+
def verify_winsize_update(
193+
self,
194+
session_id: str,
195+
rows: int,
196+
cols: int,
197+
) -> Result:
198+
"""After resize, verify probe got SIGWINCH and updated winsize."""
199+
# Check for SIGWINCH
200+
sig_result = self.verify_signal_delivery(session_id, "SIGWINCH")
201+
if not sig_result.passed:
202+
return Result(False, "No SIGWINCH received after resize")
203+
# Check latest env_check for updated winsize
204+
msgs = self._sb.get_messages(session_id=session_id, msg_type="env_check")
205+
if not msgs:
206+
return Result(False, "No env_check after SIGWINCH")
207+
latest = msgs[-1]
208+
ws = latest.get("winsize", {})
209+
if ws.get("rows") == rows and ws.get("cols") == cols:
210+
return Result(True, f"winsize updated to {rows}x{cols}", data=latest)
211+
return Result(
212+
False,
213+
f"Expected {rows}x{cols}, got {ws.get('rows')}x{ws.get('cols')}",
214+
data=latest,
215+
)

0 commit comments

Comments
 (0)