oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1#!/usr/bin/env python3
2"""SIGKILL an actual native editing process mid-typing, reopen offline, drain
3against real authenticated Docs handlers, and compare the server snapshot.
4Requires the CRDT framework built by check-native.sh; never touches user data.
5"""
6import argparse
7import random
8import os
9from pathlib import Path
10import signal
11import subprocess
12import tempfile
13import time
14
15parser = argparse.ArgumentParser(description=__doc__)
16parser.add_argument("--random", action="store_true", help="kill during unpaused typing and concurrent replica writes")
17parser.add_argument("--seed", type=int, default=None, help="reproduce the controller's random delay")
18args = parser.parse_args()
19seed = args.seed if args.seed is not None else random.SystemRandom().randrange(2**32)
20kill_delay = random.Random(seed).uniform(0, .5)
21root = Path(__file__).resolve().parents[2]
22
23def wait_for(path, process, timeout=300):
24 deadline = time.monotonic() + timeout
25 while not path.exists():
26 if process.poll() is not None:
27 raise RuntimeError(f"process exited {process.returncode} before {path.name}")
28 if time.monotonic() > deadline:
29 raise TimeoutError(path.name)
30 time.sleep(.02)
31
32with tempfile.TemporaryDirectory(prefix="docs-k3-kill-", dir="/tmp") as tmp:
33 scratch = Path(tmp)
34 env = dict(os.environ, DOCS_KILL_DIR=tmp, DOCS_KILL_RANDOM="1" if args.random else "0", TMPDIR="/tmp")
35 server_log = (scratch / "server.log").open("w")
36 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)
37 writer = None
38 try:
39 wait_for(scratch / "connection.json", server)
40 command = ["swift", "test", "--package-path", "native/apple", "--filter", "OfflineKillTests"]
41 with (scratch / "writer.log").open("w") as log:
42 writer = subprocess.Popen(command, cwd=root, env=dict(env, DOCS_KILL_MODE="write"), stdout=log, stderr=subprocess.STDOUT, start_new_session=True)
43 wait_for(scratch / "kill-now", writer)
44 if args.random:
45 # Only the controller waits; the editing process never pauses
46 # for us and can die during a command, enqueue, or writer tick.
47 time.sleep(kill_delay)
48 pid = int((scratch / "pid").read_text())
49 os.kill(pid, signal.SIGKILL)
50 result = writer.wait(timeout=20)
51 assert result != 0, "typing process unexpectedly succeeded"
52 # Include assertions emitted during the controller’s random delay.
53 assert ": error:" not in (scratch / "writer.log").read_text(), "typing assertions failed before kill"
54 if args.random:
55 print(f"SIGKILL during unpaused typing + writer ticks (seed={seed}, delay={kill_delay:.6f}s)", flush=True)
56 else:
57 print("SIGKILL delivered after 12 durable edits, before coalesced replica write", flush=True)
58 recovered = subprocess.run(command, cwd=root, env=dict(env, DOCS_KILL_MODE="recover"), capture_output=True, text=True, timeout=120)
59 if recovered.returncode:
60 print(recovered.stdout)
61 print(recovered.stderr)
62 raise RuntimeError("offline recovery / real server convergence failed")
63 (scratch / "done").touch()
64 assert server.wait(timeout=15) == 0, "real server fixture failed"
65 print("PASS: offline reopen preserves recorded prefix; real server equals reopened replica; no gaps or duplicate text", flush=True)
66 except Exception:
67 # Logs contain only test-generated state. Never print connection.json.
68 for name in ("server.log", "writer.log"):
69 path = scratch / name
70 if path.exists():
71 print(path.read_text()[-12000:])
72 raise
73 finally:
74 for process in (writer, server):
75 if process is not None and process.poll() is None:
76 os.killpg(process.pid, signal.SIGTERM)
77 process.wait(timeout=10)
78 server_log.close()
79