swactor/crates/bindings/python/tests/test_bootstrap.py

461 lines
14 KiB
Python
Raw Normal View History

"""Bootstrap-slice behavior guarantees for the ``swactor`` Python binding.
Defends the agreed invariants:
- B1 deterministic handoff: a valid exec-time bootstrap reaches ``main``
- B2 fail-fast purity: every bootstrap defect raises ``BootstrapError``
before ``main`` is invoked
- B3 version gate rejects, never guesses
- B4 arbitrary header bytes never crash the process
- B5 grandchild isolation: post-bootstrap children inherit no bootstrap fds
- B6 wake opacity: bootstrap does not consume the wake descriptor
- B7 exit-code contract across a real process boundary
- B8 the two environment names are the whole handoff ABI
- B9 no identity leakage on the Python-visible surface
"""
from __future__ import annotations
import os
import random
import select
import struct
import subprocess
import sys
import tempfile
from pathlib import Path
import pytest
import swactor
MAGIC = int.from_bytes(b"SWBS", "little")
ENV_ARENA = "SWACTOR_ARENA_FD"
ENV_WAKE = "SWACTOR_WAKE_FD"
VERSION = 1
ARENA_SIZE = 1 << 20
MAIN_RAN = []
# ─── fixtures ────────────────────────────────────────────────────────────────
def pack_header(
arena_size=ARENA_SIZE,
ring_offset=4096,
ring_capacity=8192,
ring_generation=1,
*,
magic=MAGIC,
version=VERSION,
reserved0=0,
reserved1=0,
):
return struct.pack(
"<IHHQQQQ",
magic,
version,
reserved0,
arena_size,
ring_offset,
ring_capacity,
ring_generation,
) + struct.pack("<Q", reserved1)
def _create_backing() -> int:
if hasattr(os, "memfd_create"):
return os.memfd_create("test-arena", 0)
# Not every interpreter exposes memfd_create; an unlinked temp file is an
# equally anonymous regular-file inode for ftruncate/pwrite/mmap.
fd, path = tempfile.mkstemp(prefix="swactor-test-arena-")
os.unlink(path)
return fd
def make_arena(header: bytes, size: int = ARENA_SIZE) -> int:
fd = _create_backing()
os.ftruncate(fd, size)
os.pwrite(fd, header, 0)
return fd
def make_wake(signaled: bool) -> tuple[int, int]:
read_fd, write_fd = os.pipe()
if signaled:
os.write(write_fd, b"x")
# Keep the write end open: closing it would signal EOF and make the read
# end "readable" without a wake ever being delivered.
return read_fd, write_fd
@pytest.fixture
def valid_env(monkeypatch):
"""A complete, valid bootstrap in the current process."""
keep_open = []
def install(*, header=None, arena_size=ARENA_SIZE, signaled=True):
arena_fd = make_arena(
header if header is not None else pack_header(arena_size=arena_size),
arena_size,
)
wake_fd, write_fd = make_wake(signaled)
keep_open.append(write_fd)
# Make the wake fd inheritable so only run()'s CLOEXEC can hide it
# from children (B5 evidence).
os.set_inheritable(wake_fd, True)
monkeypatch.setenv(ENV_ARENA, str(arena_fd))
monkeypatch.setenv(ENV_WAKE, str(wake_fd))
return arena_fd, wake_fd
yield install
for write_fd in keep_open:
os.close(write_fd)
# ─── B1 / B9: valid bootstrap reaches main with a clean surface ─────────────
def test_run_constructs_context_and_data(valid_env):
valid_env()
seen = {}
async def main(ctx):
seen["ctx"] = ctx
swactor.run(main)
assert isinstance(seen["ctx"], swactor.Context)
assert isinstance(seen["ctx"].data, swactor.DataPlane)
# B9: no identities leak onto the surface.
assert {a for a in dir(seen["ctx"]) if not a.startswith("_")} == {"data"}
leaked = {"arena", "resolved", "control_ring", "offset", "generation"}
assert not leaked & {a for a in dir(seen["ctx"].data) if not a.startswith("_")}
def test_run_passes_the_same_context_instance(valid_env):
valid_env()
seen = {}
async def main(ctx):
seen["one"] = ctx
seen["data"] = ctx.data
swactor.run(main)
assert seen["one"].data is seen["data"]
# ─── B5: fd hygiene ──────────────────────────────────────────────────────────
def test_arena_fd_closed_after_run(valid_env):
arena_fd, _ = valid_env()
async def main(ctx):
pass
swactor.run(main)
with pytest.raises(OSError):
os.fstat(arena_fd)
# ─── B6: wake opacity ────────────────────────────────────────────────────────
@pytest.mark.parametrize("signaled,expected", [(True, 1), (False, 0)])
def test_bootstrap_does_not_consume_wake(valid_env, signaled, expected):
valid_env(signaled=signaled)
readable = {}
async def main(ctx):
wake = int(os.environ[ENV_WAKE])
readable["value"] = int(bool(select.select([wake], [], [], 0)[0]))
swactor.run(main)
assert readable["value"] == expected
def test_wake_fd_cloexec_armed_by_run(valid_env):
_, wake_fd = valid_env()
async def main(ctx):
child = subprocess.Popen(
[
sys.executable,
"-c",
"import os, sys; "
"sys.exit(0 if sys.argv[1] not in os.listdir('/proc/self/fd') else 3)",
str(wake_fd),
],
close_fds=False,
)
child.wait()
assert child.returncode != 3, "close_fds=False child saw the wake fd"
swactor.run(main)
# ─── B2 / B3 / B4: defect table, in process ─────────────────────────────────
def _expect_bootstrap_failure(monkeypatch, header=None, *, env=None, arena_size=ARENA_SIZE, match=""):
calls = []
async def main(ctx): # pragma: no cover - must never run
calls.append(ctx)
if env is not None:
monkeypatch.setenv(ENV_ARENA, str(env[0]))
monkeypatch.setenv(ENV_WAKE, str(env[1]))
else:
arena_fd = make_arena(header if header is not None else pack_header(arena_size=arena_size), arena_size)
wake_fd, _keep_wake = make_wake(True)
monkeypatch.setenv(ENV_ARENA, str(arena_fd))
monkeypatch.setenv(ENV_WAKE, str(wake_fd))
with pytest.raises(swactor.BootstrapError, match=match) as excinfo:
swactor.run(main)
assert isinstance(excinfo.value, swactor.SwactorError)
assert calls == [], "main must never run on a bootstrap defect"
def test_missing_env_rejected(monkeypatch):
monkeypatch.delenv(ENV_ARENA, raising=False)
monkeypatch.delenv(ENV_WAKE, raising=False)
calls = []
async def main(ctx): # pragma: no cover - must never run
calls.append(ctx)
with pytest.raises(swactor.BootstrapError, match="is not set"):
swactor.run(main)
assert calls == []
def test_non_numeric_fd_rejected(monkeypatch):
_expect_bootstrap_failure(
monkeypatch, env=("not-a-number", "also-not"), match="not a descriptor number"
)
def test_negative_fd_rejected(monkeypatch):
_expect_bootstrap_failure(monkeypatch, env=("-1", "-1"), match="non-negative")
def test_closed_fd_rejected(monkeypatch):
closed = os.pipe()[0]
os.close(closed)
_expect_bootstrap_failure(monkeypatch, env=(closed, 1), match="stat arena descriptor")
def test_short_backing_rejected(monkeypatch):
_expect_bootstrap_failure(
monkeypatch,
header=b"\0" * 16,
arena_size=16,
match="shorter than the 48-byte bootstrap header",
)
@pytest.mark.parametrize(
"header,match",
[
(pack_header(magic=MAGIC ^ 0xFF), "magic mismatch"),
(pack_header(version=0), "unsupported bootstrap version 0"),
(pack_header(version=2), "unsupported bootstrap version 2"),
(pack_header(reserved0=1), "reserved bytes at offset 6"),
(pack_header(reserved1=1), "reserved bytes at offset 40"),
(
pack_header(arena_size=ARENA_SIZE, ring_offset=16),
"overlaps the bootstrap header",
),
(
pack_header(ring_offset=ARENA_SIZE, ring_capacity=1),
"exceeds arena size",
),
(
pack_header(ring_offset=2**64 - 8, ring_capacity=16),
"exceeds arena size",
),
(pack_header(ring_capacity=0), "capacity is zero"),
(pack_header(ring_generation=0), "generation is zero"),
],
)
def test_header_defects_rejected(monkeypatch, header, match):
_expect_bootstrap_failure(monkeypatch, header, match=match)
def test_lying_arena_size_rejected(monkeypatch):
_expect_bootstrap_failure(
monkeypatch,
pack_header(arena_size=ARENA_SIZE),
arena_size=ARENA_SIZE + 1,
match="disagrees with backing length",
)
def test_arbitrary_header_bytes_never_crash(monkeypatch):
rng = random.Random(0x53574253)
for _ in range(128):
fields = {
"arena_size": rng.choice([rng.getrandbits(64), ARENA_SIZE, 0, 1]),
"ring_offset": rng.choice(
[rng.getrandbits(64), 16, 4096, ARENA_SIZE, 2**64 - 8]
),
"ring_capacity": rng.choice([rng.getrandbits(64), 0, 1, 8192, 2**64 - 1]),
"ring_generation": rng.choice([rng.getrandbits(64), 0, 1, 7]),
}
header = pack_header(**fields)
ran = []
async def main(ctx): # pragma: no cover - only on accidental success
ran.append(ctx)
arena_fd = make_arena(header)
wake_fd, _keep_wake = make_wake(False)
monkeypatch.setenv(ENV_ARENA, str(arena_fd))
monkeypatch.setenv(ENV_WAKE, str(wake_fd))
end = fields["ring_offset"] + fields["ring_capacity"]
should_parse = (
fields["arena_size"] == ARENA_SIZE
and fields["ring_capacity"] > 0
and fields["ring_generation"] > 0
and fields["ring_offset"] >= 48
and end <= ARENA_SIZE
)
if should_parse:
swactor.run(main)
assert ran, "parser accepted fields that satisfy no invariant"
else:
with pytest.raises(swactor.BootstrapError):
swactor.run(main)
assert not ran
# ─── B7 / B1 / B8: real process boundary ────────────────────────────────────
PROBE = Path(__file__).parent / "probe.py"
def _spawn(argv, *, arena_fd, wake_fd, env_extra=None):
env = {k: v for k, v in os.environ.items() if k not in (ENV_ARENA, ENV_WAKE)}
env[ENV_ARENA] = str(arena_fd)
env[ENV_WAKE] = str(wake_fd)
if env_extra:
env.update(env_extra)
for fd in (arena_fd, wake_fd):
os.set_inheritable(fd, True)
return subprocess.run(
argv,
env=env,
pass_fds=(arena_fd, wake_fd),
capture_output=True,
text=True,
timeout=60,
)
def _parse_facts(stdout):
facts = dict(
part.split("=", 1) for part in stdout.strip().split() if "=" in part
)
return facts
def test_probe_succeeds_across_exec_boundary():
arena_fd = make_arena(pack_header())
wake_fd, _keep_wake = make_wake(signaled=True)
result = _spawn([sys.executable, str(PROBE)], arena_fd=arena_fd, wake_fd=wake_fd)
assert result.returncode == 0, result.stderr
facts = _parse_facts(result.stdout)
assert facts["HAS_DATA"] == "1"
assert facts["WAKE_READABLE"] == "1", "B6: presignaled wake must survive exec"
assert facts["WAKE_LEAKED"] == "0", "B5: grandchild must not see the wake fd"
def test_probe_reports_unsignaled_wake_across_exec_boundary():
arena_fd = make_arena(pack_header())
wake_fd, _keep_wake = make_wake(signaled=False)
result = _spawn([sys.executable, str(PROBE)], arena_fd=arena_fd, wake_fd=wake_fd)
assert result.returncode == 0, result.stderr
assert _parse_facts(result.stdout)["WAKE_READABLE"] == "0"
_MAIN_RETURN_SCRIPT = (
"import swactor\n"
"async def main(ctx):\n"
" print('MAIN_OK')\n"
"swactor.run(main)\n"
)
_MAIN_RAISE_SCRIPT = (
"import swactor\n"
"async def main(ctx):\n"
" raise RuntimeError('boom')\n"
"swactor.run(main)\n"
)
_NEVER_RUN_SCRIPT = (
"import swactor\n"
"async def main(ctx):\n"
" raise AssertionError('must not run')\n"
"swactor.run(main)\n"
)
def test_exit_zero_when_main_returns():
arena_fd = make_arena(pack_header())
wake_fd, _keep_wake = make_wake(True)
result = _spawn(
[sys.executable, "-c", _MAIN_RETURN_SCRIPT],
arena_fd=arena_fd,
wake_fd=wake_fd,
)
assert result.returncode == 0, result.stderr
assert "MAIN_OK" in result.stdout
def test_exit_nonzero_when_main_raises_with_traceback():
arena_fd = make_arena(pack_header())
wake_fd, _keep_wake = make_wake(True)
result = _spawn(
[sys.executable, "-c", _MAIN_RAISE_SCRIPT],
arena_fd=arena_fd,
wake_fd=wake_fd,
)
assert result.returncode != 0
assert "boom" in result.stderr
def test_exit_nonzero_on_bootstrap_defect_without_entering_main():
arena_fd = make_arena(pack_header(magic=MAGIC ^ 0xFF))
wake_fd, _keep_wake = make_wake(True)
result = _spawn(
[sys.executable, "-c", _NEVER_RUN_SCRIPT],
arena_fd=arena_fd,
wake_fd=wake_fd,
)
assert result.returncode != 0
assert "BootstrapError" in result.stderr
assert "must not run" not in result.stderr
def test_handoff_uses_only_the_two_env_names():
arena_fd = make_arena(pack_header())
wake_fd, _keep_wake = make_wake(True)
result = _spawn(
[sys.executable, "-c", _MAIN_RETURN_SCRIPT],
arena_fd=arena_fd,
wake_fd=wake_fd,
)
assert result.returncode == 0
# B8 is structural: the binding reads exactly ENV_ARENA and ENV_WAKE.
# Prove it by renaming one and observing failure.
env = {k: v for k, v in os.environ.items() if k not in (ENV_ARENA, ENV_WAKE)}
env["SWACTOR_DATA_PLANE_INPUT"] = "ignored-legacy-name"
env[ENV_WAKE] = str(wake_fd)
for fd in (arena_fd, wake_fd):
os.set_inheritable(fd, True)
missing = subprocess.run(
[sys.executable, "-c", _NEVER_RUN_SCRIPT],
env=env,
pass_fds=(arena_fd, wake_fd),
capture_output=True,
text=True,
timeout=60,
)
assert missing.returncode != 0
assert ENV_ARENA in missing.stderr