liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
//! Fixtures built once per test binary and read by every test process.
//!
//! nextest runs each test in a process of its own, so a `OnceLock` holds a
//! fixture for one test only. "Once" is therefore held on disk, beside the
//! binary: the first process to take a fixture's build lock builds it,
//! records the build and marks it complete; every later process finds the
//! mark and builds nothing. A test that writes takes a private copy.
//!
//! Layout: `target/<profile>/test-fixtures/<binary>/<key>/<fixture>/`. The key
//! is the blake3 hash of the binary's bytes, computed once per process, so a
//! rebuilt binary builds its fixtures afresh from its own code and never
//! reads a fixture another build wrote, whatever its length or modification
//! time. Every named fixture of one build sits under its one key.
//!
//! A process reading a key holds a shared lock on `<key>/key.lock` for as
//! long as it reads. A build removes the binary's other keys only by taking
//! each one's lock exclusively without waiting, so a key some process is
//! reading is never removed; it is left for a later build. Taking a key's
//! shared lock and removing a key both happen under `<binary>/registry.lock`,
//! so a key cannot be removed between its creation and its first lock.

use std::error::Error;
use std::fs::{self, File, OpenOptions};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use std::sync::atomic::{AtomicU64, Ordering};

use rustix::fs::{FlockOperation, flock};
use rustix::io::Errno;

/// What a fixture helper answers.
pub type FixtureResult<T> = Result<T, Box<dyn Error>>;

/// The checked-in store-open ceilings, one per test, read at compile time so
/// the file is the only source.
const CEILINGS: &str = include_str!("../tests/ceilings/test-cost.toml");

/// Store opens this process spent building shared fixtures. Only the process
/// that builds a fixture pays them, so they are not the test's own.
static FIXTURE_OPENS: AtomicU64 = AtomicU64::new(0);

/// The directory holding every key of this binary, and this build's key.
fn binary_key() -> FixtureResult<(PathBuf, String)> {
    static KEY: OnceLock<String> = OnceLock::new();
    let exe = std::env::current_exe()?;
    let name = exe
        .file_name()
        .and_then(|name| name.to_str())
        .ok_or("the test binary has no file name")?
        .to_owned();
    // target/<profile>/deps/<binary>: the fixtures sit in target/<profile>.
    let profile = exe
        .parent()
        .and_then(Path::parent)
        .ok_or("the test binary has no target directory above it")?;
    let binary_dir = profile.join("test-fixtures").join(name);
    if let Some(key) = KEY.get() {
        return Ok((binary_dir, key.clone()));
    }
    let hashed = content_hash(&exe)?;
    Ok((binary_dir, KEY.get_or_init(|| hashed).clone()))
}

/// The blake3 hash of the file at `path`, in hex.
///
/// # Errors
/// The file cannot be opened or read.
pub fn content_hash(path: &Path) -> FixtureResult<String> {
    let mut hasher = blake3::Hasher::new();
    hasher.update_reader(File::open(path)?)?;
    Ok(hasher.finalize().to_hex().to_string())
}

/// Opens `path`, creating it, and takes `operation` on it, answering the file
/// that holds the lock.
fn lock_file(path: &Path, operation: FlockOperation) -> FixtureResult<File> {
    let file = OpenOptions::new()
        .create(true)
        .truncate(false)
        .write(true)
        .open(path)?;
    flock(&file, operation)?;
    Ok(file)
}

/// Takes the shared lock on key `key` of `binary_dir`, creating the key, and
/// answers the file holding it: the key is not removed while it is held.
fn hold_key(binary_dir: &Path, key: &str) -> FixtureResult<File> {
    fs::create_dir_all(binary_dir)?;
    let registry = lock_file(
        &binary_dir.join("registry.lock"),
        FlockOperation::LockShared,
    )?;
    let held = (|| {
        let dir = binary_dir.join(key);
        fs::create_dir_all(&dir)?;
        lock_file(&dir.join("key.lock"), FlockOperation::LockShared)
    })();
    flock(&registry, FlockOperation::Unlock)?;
    held
}

/// Removes every key of `binary_dir` other than `keep` that no process holds.
/// A key whose lock is held is left in place for a later build to remove.
///
/// # Errors
/// A lock fails, or a key's directory cannot be read or removed.
fn remove_stale_keys(binary_dir: &Path, keep: &str) -> FixtureResult<()> {
    let registry = lock_file(
        &binary_dir.join("registry.lock"),
        FlockOperation::LockExclusive,
    )?;
    let removed = (|| -> FixtureResult<()> {
        for entry in fs::read_dir(binary_dir)? {
            let entry = entry?;
            if !entry.file_type()?.is_dir() || entry.file_name() == keep {
                continue;
            }
            let path = entry.path();
            let lock = OpenOptions::new()
                .create(true)
                .truncate(false)
                .write(true)
                .open(path.join("key.lock"))?;
            match flock(&lock, FlockOperation::NonBlockingLockExclusive) {
                Ok(()) => fs::remove_dir_all(&path)?,
                Err(error) if error == Errno::WOULDBLOCK => {}
                Err(error) => return Err(error.into()),
            }
        }
        Ok(())
    })();
    flock(&registry, FlockOperation::Unlock)?;
    removed
}

/// How many times fixture `name` has been built for this build of this
/// binary, read from its own record; zero before the first build.
///
/// # Errors
/// A lock fails, or a record that exists cannot be read or parsed.
pub fn fixture_builds(name: &str) -> FixtureResult<u64> {
    let (binary_dir, key) = binary_key()?;
    let held = hold_key(&binary_dir, &key)?;
    let builds = read_builds(&binary_dir.join(&key).join(name));
    flock(&held, FlockOperation::Unlock)?;
    builds
}

fn read_builds(root: &Path) -> FixtureResult<u64> {
    let record = root.join("builds");
    if !record.exists() {
        return Ok(0);
    }
    let mut text = String::new();
    File::open(record)?.read_to_string(&mut text)?;
    Ok(text.trim().parse()?)
}

/// Runs `read` over the directory of fixture `name` under key `key` of
/// `binary_dir`, holding the key shared throughout. The fixture is built by
/// `build` the first time any process of the key asks for it: `build`
/// receives an empty directory and must leave the fixture complete in it; a
/// build that failed leaves no mark and is built again by the next caller.
/// `read` must only read.
fn with_fixture_in<T>(
    binary_dir: &Path,
    key: &str,
    name: &str,
    build: impl FnOnce(&Path) -> FixtureResult<()>,
    read: impl FnOnce(&Path) -> FixtureResult<T>,
) -> FixtureResult<T> {
    let held = hold_key(binary_dir, key)?;
    let result = (|| {
        let root = binary_dir.join(key).join(name);
        fs::create_dir_all(&root)?;
        let lock = lock_file(&root.join("build.lock"), FlockOperation::LockExclusive)?;
        let build_outcome = build_once(binary_dir, key, &root, build);
        flock(&lock, FlockOperation::Unlock)?;
        build_outcome?;
        read(&root.join("data"))
    })();
    flock(&held, FlockOperation::Unlock)?;
    result
}

/// Builds the fixture at `root` unless it is marked complete, first removing
/// the binary's other keys that no process holds.
fn build_once(
    binary_dir: &Path,
    key: &str,
    root: &Path,
    build: impl FnOnce(&Path) -> FixtureResult<()>,
) -> FixtureResult<()> {
    if root.join("complete").exists() {
        return Ok(());
    }
    remove_stale_keys(binary_dir, key)?;
    let data = root.join("data");
    if data.exists() {
        fs::remove_dir_all(&data)?;
    }
    fs::create_dir_all(&data)?;
    let opened_before = liminal::durability::stores_opened();
    build(&data)?;
    FIXTURE_OPENS.fetch_add(
        liminal::durability::stores_opened().saturating_sub(opened_before),
        Ordering::Relaxed,
    );
    let builds = read_builds(root)? + 1;
    File::create(root.join("builds"))?.write_all(builds.to_string().as_bytes())?;
    File::create(root.join("complete"))?;
    Ok(())
}

/// A private copy of fixture `name` of this build (built by `build` the first
/// time any process asks for it) in a fresh temporary directory the test may
/// write to. The key is held shared while the copy is taken.
///
/// # Errors
/// The fixture cannot be built or copied.
pub fn fixture_copy(
    name: &str,
    build: impl FnOnce(&Path) -> FixtureResult<()>,
) -> FixtureResult<tempfile::TempDir> {
    let (binary_dir, key) = binary_key()?;
    with_fixture_in(&binary_dir, &key, name, build, copy_out)
}

/// Copies the fixture directory `source` into a fresh temporary directory.
fn copy_out(source: &Path) -> FixtureResult<tempfile::TempDir> {
    let copy = tempfile::tempdir()?;
    copy_tree(source, copy.path())?;
    Ok(copy)
}

/// Copies the directory tree at `from` into `to`, which may already exist.
fn copy_tree(from: &Path, to: &Path) -> FixtureResult<()> {
    fs::create_dir_all(to)?;
    for entry in fs::read_dir(from)? {
        let entry = entry?;
        let target = to.join(entry.file_name());
        if entry.file_type()?.is_dir() {
            copy_tree(&entry.path(), &target)?;
        } else {
            fs::copy(entry.path(), &target)?;
        }
    }
    Ok(())
}

/// Pins the store opens of the test `test` in `module` (its
/// `module_path!()`), to be called at the end of its body: the stores this
/// process opened through liminal's open paths, less those spent building
/// shared fixtures, are appended to this binary's ledger as `test opens` and
/// compared with the test's ceiling in `tests/ceilings/test-cost.toml`, keyed
/// `module::test`. Above the ceiling fails; more than ten per
/// cent under prints "ceiling can drop". The ledger's sum over a run is the
/// binary's store opens, for the reader; it is never a gate.
///
/// # Errors
/// The ledger cannot be written, the ceiling file does not parse or has no
/// ceiling for `test`, or the opens are above it.
pub fn pin_store_opens(module: &str, test: &str) -> FixtureResult<()> {
    let test = format!("{module}::{test}");
    let opens =
        liminal::durability::stores_opened().saturating_sub(FIXTURE_OPENS.load(Ordering::Relaxed));
    append_ledger(&test, opens)?;
    for line in check_store_opens(CEILINGS, &test, opens)? {
        println!("{line}");
    }
    Ok(())
}

/// Appends `test opens` to this binary's store-opens ledger under its lock.
fn append_ledger(test: &str, opens: u64) -> FixtureResult<()> {
    let (binary_dir, key) = binary_key()?;
    let held = hold_key(&binary_dir, &key)?;
    let written = (|| -> FixtureResult<()> {
        let dir = binary_dir.join(&key);
        let lock = lock_file(&dir.join("ledger.lock"), FlockOperation::LockExclusive)?;
        let appended = OpenOptions::new()
            .create(true)
            .append(true)
            .open(dir.join("store-opens.ledger"))
            .and_then(|mut ledger| writeln!(ledger, "{test} {opens}"));
        flock(&lock, FlockOperation::Unlock)?;
        appended?;
        Ok(())
    })();
    flock(&held, FlockOperation::Unlock)?;
    written
}

/// Compares `opens` with the ceiling for `test` in the ceiling text
/// `ceilings`, answering the "ceiling can drop" line when it is more than ten
/// per cent under.
fn check_store_opens(ceilings: &str, test: &str, opens: u64) -> FixtureResult<Vec<String>> {
    let table: toml::Table = ceilings.parse()?;
    let ceiling = table
        .get("store_opens")
        .and_then(toml::Value::as_table)
        .and_then(|opens| opens.get(test))
        .and_then(toml::Value::as_integer)
        .ok_or_else(|| {
            format!("the ceiling file has no store_opens ceiling for {test}; measured {opens}")
        })?;
    let ceiling = u64::try_from(ceiling)?;
    if opens > ceiling {
        return Err(format!("{test} opened {opens} stores, over its ceiling of {ceiling}").into());
    }
    let mut lines = vec![format!("{test} store_opens {opens} (ceiling {ceiling})")];
    if opens.saturating_mul(10) < ceiling.saturating_mul(9) {
        lines.push(format!(
            "ceiling can drop: {test} store_opens {opens}, ceiling {ceiling}"
        ));
    }
    Ok(lines)
}

#[cfg(test)]
mod tests {
    use std::fs;
    use std::path::Path;

    use super::{FixtureResult, check_store_opens, fixture_builds, fixture_copy, with_fixture_in};

    const CEILINGS: &str = "[store_opens]\n\"a\" = 10\n";

    #[test]
    fn store_opens_fail_over_their_ceiling_and_say_when_it_can_drop() -> FixtureResult<()> {
        assert!(check_store_opens(CEILINGS, "a", 11).is_err());
        assert!(check_store_opens(CEILINGS, "missing", 0).is_err());
        let at = check_store_opens(CEILINGS, "a", 10)?;
        assert!(!at.iter().any(|line| line.starts_with("ceiling can drop")));
        let nine = check_store_opens(CEILINGS, "a", 9)?;
        assert!(!nine.iter().any(|line| line.starts_with("ceiling can drop")));
        let eight = check_store_opens(CEILINGS, "a", 8)?;
        assert!(
            eight
                .iter()
                .any(|line| line.starts_with("ceiling can drop"))
        );
        Ok(())
    }

    #[test]
    fn a_fixture_is_built_once_and_copied_whole() -> FixtureResult<()> {
        let build = |dir: &Path| -> FixtureResult<()> {
            fs::create_dir_all(dir.join("nested"))?;
            fs::write(dir.join("nested").join("row"), b"one")?;
            Ok(())
        };
        drop(fixture_copy("fixture-self-test", build)?);
        let copy = fixture_copy("fixture-self-test", |_| {
            Err("a built fixture is never built again".into())
        })?;
        assert_eq!(fs::read(copy.path().join("nested").join("row"))?, b"one");
        assert_eq!(fixture_builds("fixture-self-test")?, 1);
        Ok(())
    }

    /// Writes `row` into a fixture directory.
    fn seed(row: &'static [u8]) -> impl FnOnce(&Path) -> FixtureResult<()> {
        move |data: &Path| Ok(fs::write(data.join("row"), row)?)
    }

    /// A key some process is reading survives another build's cleanup, and is
    /// removed by the first build after its reader lets go.
    #[test]
    fn a_key_being_read_survives_another_builds_cleanup() -> FixtureResult<()> {
        let binary = tempfile::tempdir()?;
        let dir = binary.path();
        with_fixture_in(dir, "build-a", "seed", seed(b"a"), |_| Ok(()))?;

        let read_during_cleanup = with_fixture_in(
            dir,
            "build-a",
            "seed",
            |_| Err("a built fixture is never built again".into()),
            |data| {
                with_fixture_in(dir, "build-b", "seed", seed(b"b"), |_| Ok(()))?;
                Ok(fs::read(data.join("row"))?)
            },
        )?;
        assert_eq!(read_during_cleanup, b"a");
        assert!(dir.join("build-a").join("seed").join("complete").exists());

        with_fixture_in(dir, "build-c", "seed", seed(b"c"), |_| Ok(()))?;
        assert!(!dir.join("build-a").exists());
        assert!(!dir.join("build-b").exists());
        assert!(dir.join("build-c").join("seed").join("complete").exists());
        Ok(())
    }
}