"""Install a marker-controlled SQLite xSync gate before an exclusive daemon boots.""" import json from pathlib import Path import shlex import subprocess import sys from harness import ROOT, wait_until class StorageSync: """Construct in prepare(app); always release the gate before daemon teardown.""" def __init__(self, app): self.prefix = app.root / "storage-sync" self.kind = None source = ROOT / "test/e2e/albedo_storage_sync_nif" library = app.root / source.name erlang = self._run( [ "erl", "+S", "2:2", "-noshell", "-eval", "io:put_chars(code:root_dir()), halt().", ] ).strip() command = [ "cc", "-std=c11", "-Wall", "-Wextra", "-Werror", "-fPIC", "-shared", "-I", str(Path(erlang) / "usr/include"), "-I", str(ROOT / "build/dev/erlang/esqlite/c_src/sqlite3"), str(source.with_suffix(".c")), "-o", str(library.with_suffix(".so")), ] if sys.platform == "darwin": command += ["-undefined", "dynamic_lookup"] else: command += ["-ldl"] self._run(command) self._run( ["erlc", "-Werror", "-o", str(app.root), str(source.with_suffix(".erl"))] ) expression = ( f"ok = albedo_storage_sync_nif:install({json.dumps(str(library))}, " f"{json.dumps(str(self.prefix))})." ) flags = f"-pa {shlex.quote(str(app.root))} -eval {shlex.quote(expression)}" # ERL_FLAGS runs after the launcher's -eval main; AFLAGS runs before it. app.daemon.env["ERL_AFLAGS"] = ( flags + " " + app.daemon.env.get("ERL_AFLAGS", "") ) app.daemon.env["ALBEDO_INSPECT"] = "1" @staticmethod def _run(command): result = subprocess.run(command, capture_output=True, text=True, timeout=30) if result.returncode: raise AssertionError( f"storage sync fixture compilation failed:\n{result.stdout}{result.stderr}" ) return result.stdout def _marker(self, suffix): return Path(f"{self.prefix}.{self.kind}.{suffix}") def arm(self, kind="main"): """Block the next main-database or WAL sync until release().""" if kind not in ("main", "wal"): raise ValueError(f"unknown sync kind: {kind}") if self.kind is not None: raise AssertionError("release the previous sync gate before rearming") self.kind = kind self._marker("entered").unlink(missing_ok=True) self._marker("block").touch() def wait_entered(self, timeout=10): wait_until( self._marker("entered").exists, timeout, "SQLite never entered gated xSync" ) def release(self): """Unblock xSync and wait until it has left the gate.""" if self.kind is None: return self._marker("block").unlink(missing_ok=True) wait_until( lambda: not self._marker("entered").exists(), 10, "SQLite xSync did not leave its gate", ) self.kind = None