Skip to content
This repository was archived by the owner on Sep 8, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 41 additions & 14 deletions packages/context-guard-receipt/tests/contract/test_g008_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,33 @@ def _process_is_alive(pid: int) -> bool:
return True


def _read_pid_when_written(path: Path, timeout: float = 5.0) -> int:
"""Wait for a child's pid file to hold a pid, not merely to exist.

`open(path, "w")` creates the file before anything is written to it, so a
reader that waits on the path existing — `Path.is_file()` or
`Path.exists()` — can win the race and read an empty string. That is a
flake, not a product defect: it surfaced on CI as `ValueError: invalid
literal for int() with base 10: ''` while the same commit passed on a
re-run. Wait for a finished record instead.
"""
deadline = time.monotonic() + timeout
last = ""
while time.monotonic() < deadline:
try:
last = path.read_text(encoding="ascii")
except (FileNotFoundError, UnicodeDecodeError):
last = ""
# The writers end the pid with a newline, so a trailing newline is what
# makes "the write finished" observable. Without it a torn read of
# "12345" could return 12 and look entirely valid.
body = last.strip()
if last.endswith("\n") and body:
return int(body)
time.sleep(0.01)
raise AssertionError(f"pid file never became readable: {path} (last read {last!r})")


def _wait_for_process_exit(pid: int, timeout: float = 3.0) -> bool:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
Expand Down Expand Up @@ -947,14 +974,13 @@ def interrupt_after_child_starts() -> None:
with self.assertRaises(KeyboardInterrupt):
harness.run(
"import os,time; "
"open('child.pid','w').write(str(os.getpid())); "
"open('child.pid','w').write(str(os.getpid()) + chr(10)); "
"time.sleep(30)"
)
finally:
interrupter.join(timeout=5.0)

self.assertTrue(pid_path.is_file())
child_pid = int(pid_path.read_text(encoding="ascii"))
child_pid = _read_pid_when_written(pid_path)
try:
os.kill(child_pid, 0)
except ProcessLookupError:
Expand Down Expand Up @@ -1029,7 +1055,7 @@ def test_sigterm_runs_cleanup_before_the_wrapper_exits(self) -> None:
child_code = (
"import os,signal,sys,time; "
"signal.signal(signal.SIGTERM,signal.SIG_IGN); "
"open(sys.argv[1],'w').write(str(os.getpid())); "
"open(sys.argv[1],'w').write(str(os.getpid()) + chr(10)); "
"time.sleep(30)"
)
wrapper_code = (
Expand Down Expand Up @@ -1064,8 +1090,7 @@ def test_sigterm_runs_cleanup_before_the_wrapper_exits(self) -> None:
if wrapper.poll() is not None:
break
time.sleep(0.01)
self.assertTrue(pid_path.is_file())
child_pid = int(pid_path.read_text(encoding="ascii"))
child_pid = _read_pid_when_written(pid_path)
wrapper.send_signal(signal.SIGTERM)
time.sleep(0.05)
if wrapper.poll() is None:
Expand Down Expand Up @@ -1361,7 +1386,7 @@ def test_setsid_descendant_that_closes_stdio_blocks_normal_publication(self) ->
" except OSError: pass\n"
" signal.signal(signal.SIGTERM,signal.SIG_IGN)\n"
" with open(sys.argv[1],'w') as stream:\n"
" stream.write(str(os.getpid())); stream.flush(); os.fsync(stream.fileno())\n"
" stream.write(str(os.getpid()) + chr(10)); stream.flush(); os.fsync(stream.fileno())\n"
" time.sleep(30)\n"
" os._exit(0)\n"
"deadline=time.monotonic()+3\n"
Expand All @@ -1375,15 +1400,16 @@ def test_setsid_descendant_that_closes_stdio_blocks_normal_publication(self) ->
arguments=(str(pid_path),),
limits=module.RunnerLimits(timeout_seconds=0.4),
)
self.assertTrue(pid_path.is_file())
detached_pid = int(pid_path.read_text(encoding="ascii"))
detached_pid = _read_pid_when_written(pid_path)
self.assertEqual(result.error_code, module.RunnerErrorCode.TIMEOUT)
self.assertIsNone(result.receipt)
self.assertEqual(harness.factory_calls, 0)
self.assertTrue(_wait_for_process_exit(detached_pid))
finally:
if detached_pid is None and pid_path.is_file():
detached_pid = int(pid_path.read_text(encoding="ascii"))
# Best effort during cleanup: the child may have died mid-write.
raw = pid_path.read_text(encoding="ascii").strip()
detached_pid = int(raw) if raw.isdigit() else None
if detached_pid is not None:
_kill_test_process_group(detached_pid)

Expand All @@ -1403,7 +1429,7 @@ def test_sigterm_cleanup_reaps_tracked_setsid_descendant(self) -> None:
" except OSError: pass\n"
" signal.signal(signal.SIGTERM,signal.SIG_IGN)\n"
" with open(sys.argv[1],'w') as stream:\n"
" stream.write(str(os.getpid())); stream.flush(); os.fsync(stream.fileno())\n"
" stream.write(str(os.getpid()) + chr(10)); stream.flush(); os.fsync(stream.fileno())\n"
" time.sleep(30)\n"
" os._exit(0)\n"
"time.sleep(30)\n"
Expand Down Expand Up @@ -1442,8 +1468,7 @@ def test_sigterm_cleanup_reaps_tracked_setsid_descendant(self) -> None:
if wrapper.poll() is not None:
break
time.sleep(0.01)
self.assertTrue(pid_path.is_file())
detached_pid = int(pid_path.read_text(encoding="ascii"))
detached_pid = _read_pid_when_written(pid_path)
time.sleep(0.2)
wrapper.send_signal(signal.SIGTERM)
stdout, stderr = wrapper.communicate(timeout=5.0)
Expand All @@ -1456,7 +1481,9 @@ def test_sigterm_cleanup_reaps_tracked_setsid_descendant(self) -> None:
wrapper.kill()
wrapper.communicate(timeout=5.0)
if detached_pid is None and pid_path.is_file():
detached_pid = int(pid_path.read_text(encoding="ascii"))
# Best effort during cleanup: the child may have died mid-write.
raw = pid_path.read_text(encoding="ascii").strip()
detached_pid = int(raw) if raw.isdigit() else None
if detached_pid is not None:
_kill_test_process_group(detached_pid)

Expand Down
2 changes: 1 addition & 1 deletion tests/test_contextguard_stage2_feasibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -898,7 +898,7 @@ def is_weightclass_scaffolding_path(path_text: str) -> bool:
{'file_type': 'regular',
'mode': '0644',
'path': 'packages/context-guard-receipt/tests/contract/test_g008_runner.py',
'sha256': '39f7874f98df99245d9d4ee345f5cc83c05e094a64096521d61fa73ddf359744'},
'sha256': 'bf9a2a15254ac6a230fb86767a586cc0c3657be6e57a7924b5abe50ae69ad8a9'},
{'file_type': 'regular',
'mode': '0644',
'path': 'packages/context-guard-receipt/tests/contract/test_g009_cli.py',
Expand Down