#!/usr/bin/env python3 """SIGKILL an actual native editing process mid-typing, reopen offline, drain against real authenticated Docs handlers, and compare the server snapshot. Requires the CRDT framework built by check-native.sh; never touches user data. """ import argparse import random import os from pathlib import Path import signal import subprocess import tempfile import time parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--random", action="store_true", help="kill during unpaused typing and concurrent replica writes") parser.add_argument("--seed", type=int, default=None, help="reproduce the controller's random delay") args = parser.parse_args() seed = args.seed if args.seed is not None else random.SystemRandom().randrange(2**32) kill_delay = random.Random(seed).uniform(0, .5) root = Path(__file__).resolve().parents[2] def wait_for(path, process, timeout=300): deadline = time.monotonic() + timeout while not path.exists(): if process.poll() is not None: raise RuntimeError(f"process exited {process.returncode} before {path.name}") if time.monotonic() > deadline: raise TimeoutError(path.name) time.sleep(.02) with tempfile.TemporaryDirectory(prefix="docs-k3-kill-", dir="/tmp") as tmp: scratch = Path(tmp) env = dict(os.environ, DOCS_KILL_DIR=tmp, DOCS_KILL_RANDOM="1" if args.random else "0", TMPDIR="/tmp") server_log = (scratch / "server.log").open("w") server = subprocess.Popen(["go", "test", "./internal/docs", "-run", "^TestNativeKillServer$", "-count=1", "-timeout=8m"], cwd=root, env=env, stdout=server_log, stderr=subprocess.STDOUT, start_new_session=True) writer = None try: wait_for(scratch / "connection.json", server) command = ["swift", "test", "--package-path", "native/apple", "--filter", "OfflineKillTests"] with (scratch / "writer.log").open("w") as log: writer = subprocess.Popen(command, cwd=root, env=dict(env, DOCS_KILL_MODE="write"), stdout=log, stderr=subprocess.STDOUT, start_new_session=True) wait_for(scratch / "kill-now", writer) if args.random: # Only the controller waits; the editing process never pauses # for us and can die during a command, enqueue, or writer tick. time.sleep(kill_delay) pid = int((scratch / "pid").read_text()) os.kill(pid, signal.SIGKILL) result = writer.wait(timeout=20) assert result != 0, "typing process unexpectedly succeeded" # Include assertions emitted during the controller’s random delay. assert ": error:" not in (scratch / "writer.log").read_text(), "typing assertions failed before kill" if args.random: print(f"SIGKILL during unpaused typing + writer ticks (seed={seed}, delay={kill_delay:.6f}s)", flush=True) else: print("SIGKILL delivered after 12 durable edits, before coalesced replica write", flush=True) recovered = subprocess.run(command, cwd=root, env=dict(env, DOCS_KILL_MODE="recover"), capture_output=True, text=True, timeout=120) if recovered.returncode: print(recovered.stdout) print(recovered.stderr) raise RuntimeError("offline recovery / real server convergence failed") (scratch / "done").touch() assert server.wait(timeout=15) == 0, "real server fixture failed" print("PASS: offline reopen preserves recorded prefix; real server equals reopened replica; no gaps or duplicate text", flush=True) except Exception: # Logs contain only test-generated state. Never print connection.json. for name in ("server.log", "writer.log"): path = scratch / name if path.exists(): print(path.read_text()[-12000:]) raise finally: for process in (writer, server): if process is not None and process.poll() is None: os.killpg(process.pid, signal.SIGTERM) process.wait(timeout=10) server_log.close()