DrinkMeAlice. TestServer
(drink_me_alice v0.1.0)
Copy Markdown
Owned, private, loopback-only SurrealDB server child — for tests of this library and of anything built on it.
- Data lives under a caller-provided private directory (
<dir>/data, SurrealKV); the directory is created mode 0700. - Root credentials are freshly generated per server, passed only via the
SURREAL_USER/SURREAL_PASSenvironment (never on the command line), and never written to logs or receipts. - Bound to an ephemeral loopback port picked by the kernel.
- The server's full version identity is recorded as the first line of
<dir>/server.log— a bare version number does not identify an artifact. - Port (stderr) output is appended to
<dir>/server.logwith a real cumulative byte cap (262144);server.log_bytestracks it. Long-running probes should calldrain/2inside their wait loops so diagnostic bytes never accumulate in the port mailbox. - Startup reports definite failure on early child exit (including bind
failure); a
/health200 counts only while the owned child is alive. - Concurrent starts on the SAME data dir are refused, never raced.
Within this VM a per-dir claim — a dedicated holder process per
start, monitoring the port owner, so one caller may hold several
stores while a claim still dies with its owner — makes the second
caller fail fast with
{:error, {:data_dir_in_use, dir, holder_pid}}. Against an already-existing store, the SurrealKV endpoint is scanned for a SUSPECTED foreignsurreal start(typically a leftover from a killed VM) and refused with{:error, {:data_dir_lock_suspected, dir, suspects}}BEFORE any child is spawned or the old log is touched. The scan is best-effort, fail-open, and suspicion is a hint to verify — not verified lock ownership; an early exit that still slips through its race window is reported with the log tail and the suspected foreign holders. stop/1escalates TERM -> KILL under finite deadlines and confirms the OS child is reaped; the last observed exit status is preserved, and the data-dir claim is released — only for THIS instance's claim, only on proven cleanup, and SYNCHRONOUSLY: normallystop/1does not report completion before the registration is removed, so an immediate same-dir restart is not refused by our own stale claim. If the bounded release handshake times out (a holder that cannot run), the result preserves both facts as{:error, {:claim_release_unconfirmed, holder, cleanup}}. A stale handle or an unproven cleanup cannot strip a live claim; a FAILED start with unproven child cleanup ({:acquisition_unclean, reason, holder}or{:cleanup_unproven, other, holder}) retains its claim — with the holder pid in the error as a receipt — until the starting process dies.
Version qualification
The suite is written and qualified against SurrealDB
3.0.5 ONLY. start/1 resolves the executable
(SURREAL_BIN, else surreal on PATH), records its identity and
refuses a different version with {:error, {:unqualified_server, reason}} — a later 3.x build is not
automatically the qualified behavior. Set SURREAL_ALLOW_UNQUALIFIED=1
to start anyway: that is a compatibility EXPERIMENT, not a
qualification, and every such run says so on stdout.
Summary
Functions
Drain port output into server.log and return the updated server.
Whether :live tests may run: :ok, {:skip, reason}, or — with
SURREAL_REQUIRE_QUALIFIED=1 set (CI) — {:error, reason} for
anything that would otherwise be skipped, so a qualification job fails
instead of silently excluding its live suite. A different installed
server version skips with the reason; set
SURREAL_ALLOW_UNQUALIFIED=1 to run it anyway as a compatibility
experiment whose results are NOT a qualification.
The first major.minor.patch in a version line, or :error.
The SurrealDB version this suite is qualified against.
{bin, identity} for the SurrealDB executable: SURREAL_BIN if set,
else surreal on PATH. identity is the FULL surreal version
output — build metadata included, because a version number alone does
not identify the artifact under test.
Start a private server. Options
Stop the server: drain queued port messages, TERM, bounded grace, KILL, drain again, confirm reaped — then release the data-dir claim so the documented restart-on-the-same-dir flow keeps working.
The SurrealDB binary path, raising when absent (deprecated raise
behavior kept for direct callers; prefer resolve_bin/0).
Types
@type t() :: %DrinkMeAlice.TestServer{ claim_pid: pid() | nil, creds: creds(), dir: String.t(), exit_status: integer() | nil, http_port: :inet.port_number(), log_bytes: non_neg_integer(), log_path: String.t(), os_pid: non_neg_integer(), port: port() }
Functions
@spec drain(t(), non_neg_integer()) :: t()
Drain port output into server.log and return the updated server.
Always performs a nonblocking burst pass first (bounded at
512 queued items) so an already-queued exit_status is
observed even with timeout: 0; then blocks up to timeout for trailing
output. Call inside long wait loops so diagnostic bytes never accumulate
unboundedly in the port mailbox.
Whether :live tests may run: :ok, {:skip, reason}, or — with
SURREAL_REQUIRE_QUALIFIED=1 set (CI) — {:error, reason} for
anything that would otherwise be skipped, so a qualification job fails
instead of silently excluding its live suite. A different installed
server version skips with the reason; set
SURREAL_ALLOW_UNQUALIFIED=1 to run it anyway as a compatibility
experiment whose results are NOT a qualification.
The first major.minor.patch in a version line, or :error.
@spec qualified_version() :: String.t()
The SurrealDB version this suite is qualified against.
{bin, identity} for the SurrealDB executable: SURREAL_BIN if set,
else surreal on PATH. identity is the FULL surreal version
output — build metadata included, because a version number alone does
not identify the artifact under test.
The version probe is bounded (2000 ms) and
typed: a missing binary, an unusable path, a nonzero exit or a hung
command is {:error, reason} — never a raise, never a parsed
substring of failed output.
Start a private server. Options:
:dir(required) — private root;<dir>/datais the SurrealKV path,<dir>/server.logrecords the server identity and bounded stderr.:port— bind this port instead of a free one (restart on the same endpoint, so a conn configured once reconnects to it).:user/:pass— reuse these root credentials instead of generating fresh ones. REQUIRED when restarting onto an existing SurrealKV store: the stored root user persists, so fresh creds would not take effect and every signin would fail -32002.:args— extrasurreal startCLI args (e.g.--allow-net,--async-event-interval); must never contain credentials.:env— extra env pairs (charlist keys/values).:health—falseto skip the /health wait.
A caller may hold several servers at once (each start claims its own
directory through a dedicated holder process). Refuses with
{:error, {:unqualified_server, reason}} before creating anything
when the resolved executable is not the qualified version and
SURREAL_ALLOW_UNQUALIFIED=1 is not set. Refuses with
{:error, {:data_dir_in_use, dir, holder_pid}} when another live
claim in this VM already owns dir. When the store already exists,
a best-effort host process scan runs: a SUSPECTED foreign holder (a
process whose executable is named surreal and whose arguments
carry the exact endpoint token) refuses with
{:error, {:data_dir_lock_suspected, dir, suspects}} before any
child is spawned. A command-line match is a hint, not verified lock
ownership: identify the process, confirm it really owns this store,
and stop that owner — do not kill on a command-line match alone, and
never delete a store that may still be active. The scan is
fail-open. Directories are claimed by their expanded path. On error,
pre-spawn failures release the claim synchronously (if that release
cannot be confirmed, the result says so via
{:claim_release_unconfirmed, holder, reason}); a start whose child
was acquired but not proven cleaned up retains the claim — with the
holder pid in the error as a receipt — until the starting process
dies. Diagnostics are best-effort (a broken IO device cannot prevent
finalization), and EVERY exception after acquisition — including one
raised by failing diagnostics inside a rescue handler — is converted
to proven cleanup or a retained claim with a typed receipt
({:startup_failed, {:finalization_crashed, cause}, ...}).
Stop the server: drain queued port messages, TERM, bounded grace, KILL, drain again, confirm reaped — then release the data-dir claim so the documented restart-on-the-same-dir flow keeps working.
The claim is released ONLY when cleanup is proven AND this instance
still owns the claim: the live registration must be this start's
holder process, so a stale handle stopped after a replacement start
cannot release the replacement's claim, and an unproven cleanup
({:error, :unreaped} and friends) leaves the exclusion in place
while the child may still hold the store — it clears when the owning
process dies.
Release itself is a bounded synchronous handshake: normally stop/1
does not return until the registration is removed. If confirmation
times out (a holder that cannot run), the public result preserves
BOTH facts — the child cleanup proof and the unresolved release —
as {:error, {:claim_release_unconfirmed, holder, cleanup}}; the
claim clears when the holder next runs or its owner dies, and an
immediate same-directory restart may still be refused until then.
Returns {:exit_status, n} only for an observed OS exit code;
{:reaped, :status_unobserved} when the child is confirmed gone but no
exit_status message was observed; {:error, ...} when cleanup cannot be
proven or the claim release is unconfirmed.
The SurrealDB binary path, raising when absent (deprecated raise
behavior kept for direct callers; prefer resolve_bin/0).