haematite-cli 0.21.9

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
use std::cell::Cell;
use std::error::Error;
use std::fs;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::PathBuf;

use super::Config;
use serde_json::{Value, json};

type TestResult = Result<(), Box<dyn Error>>;

thread_local! {
    static INPUT: Cell<Option<(PathBuf, u32)>> = const { Cell::new(None) };
}

pub fn input() -> Option<(PathBuf, u32)> {
    INPUT.with(|slot| {
        let input = slot.take();
        let copy = input.clone();
        slot.set(input);
        copy
    })
}

pub struct Fixture {
    pub(crate) directory: tempfile::TempDir,
    pub(crate) config_path: PathBuf,
    pub(crate) data_dir: PathBuf,
    pub(crate) value: Value,
}

impl Fixture {
    pub(crate) fn config(&self) -> Result<Config, serde_json::Error> {
        serde_json::from_value(self.value.clone())
    }
}

pub fn with_fixture(run: impl FnOnce(&Fixture) -> TestResult) -> TestResult {
    let directory = tempfile::Builder::new().prefix("i").tempdir_in("/tmp")?;
    fs::set_permissions(directory.path(), fs::Permissions::from_mode(0o700))?;
    let uid = fs::metadata(directory.path())?.uid();
    let data_dir = directory.path().join("database");
    fs::create_dir(&data_dir)?;
    let config_path = directory.path().join("service.json");
    let value = json!({
        "data_dir": data_dir,
        "branch_cache_bytes_per_shard": 1_048_576,
        "max_frame": 65_536, "max_clients": 1, "max_page": 8, "timeout_ms": 1000,
        "create": {"shards": 1, "cache_bytes_total": 1_048_576, "executor_threads": 1}
    });
    fs::write(&config_path, serde_json::to_vec(&value)?)?;
    let previous = INPUT.with(|slot| slot.replace(Some((directory.path().to_path_buf(), uid))));
    let fixture = Fixture {
        directory,
        config_path,
        data_dir,
        value,
    };
    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| run(&fixture)));
    INPUT.with(|slot| slot.set(previous));
    let cleanup = fixture.directory.close();
    match (result, cleanup) {
        (Ok(result), Ok(())) => result,
        (Ok(Ok(())), Err(error)) => Err(error.into()),
        (Ok(Err(test)), Err(cleanup)) => Err(format!("test: {test}; cleanup: {cleanup}").into()),
        (Err(panic), cleanup) => {
            if let Err(error) = cleanup {
                eprintln!("fixture cleanup: {error}");
            }
            std::panic::resume_unwind(panic)
        }
    }
}

#[test]
fn missing_socket_configuration_reads_without_runtime_effects() -> TestResult {
    with_fixture(|fixture| {
        fixture.config()?;
        assert!(!fixture.directory.path().join("haem").try_exists()?);
        Ok(())
    })
}

#[test]
fn startup_and_restart_choose_the_same_default_endpoint() -> TestResult {
    with_fixture(|fixture| {
        let config = fixture.config()?;
        let starting = config.endpoint(true)?;
        let restarting = config.endpoint(false)?;
        assert_eq!(starting.socket, restarting.socket);
        assert_eq!(starting.name, "database");
        assert_eq!(
            starting.socket.parent(),
            Some(fixture.directory.path().join("haem").as_path())
        );
        Ok(())
    })
}

#[test]
fn old_explicit_socket_and_logical_name_override_the_defaults() -> TestResult {
    with_fixture(|fixture| {
        let socket = fixture.directory.path().join("legacy.sock");
        let mut value = fixture.value.clone();
        value["socket"] = json!(socket);
        let config: Config = serde_json::from_value(value.clone())?;
        assert_eq!(config.socket.as_deref(), Some(socket.as_path()));
        assert!(config.name.is_none());
        assert_eq!(config.endpoint(false)?.socket, socket);
        assert!(!fixture.directory.path().join("haem").try_exists()?);
        value["name"] = json!("chosen");
        let named: Config = serde_json::from_value(value)?;
        assert_eq!(named.endpoint(false)?.name, "chosen");
        assert_eq!(named.endpoint(false)?.socket, socket);
        Ok(())
    })
}

/// Two stores whose data folders share a last component register under one
/// name. The second serve is refused with the store name, the name socket's
/// path and that another running serve holds it, never the lock's raw
/// would-block.
#[test]
fn a_second_serve_on_a_folder_of_the_same_name_is_refused_by_name() -> TestResult {
    with_fixture(|fixture| {
        let mut held = Vec::new();
        let mut refused = Vec::new();
        for parent in ["one", "two"] {
            let data_dir = fixture.directory.path().join(parent).join("store");
            fs::create_dir_all(&data_dir)?;
            let mut value = fixture.value.clone();
            value["data_dir"] = json!(data_dir);
            let config: Config = serde_json::from_value(value)?;
            let endpoint = config
                .endpoint(true)
                .map_err(|refusal| refusal.to_string())?;
            match super::claim_name(&config, &endpoint, true) {
                Ok(name) => held.push(name),
                Err(refusal) => refused.push((refusal, endpoint)),
            }
        }
        assert_eq!(held.len(), 1, "the first serve holds the name");
        let (refusal, endpoint) = refused.pop().ok_or("the second serve was admitted")?;
        assert_eq!(endpoint.name, "store");
        assert_eq!(
            refusal,
            format!(
                "bind service socket: the store name \"store\" is already served by another running haematite serve ({}); give this data folder another name",
                endpoint.socket.display()
            )
        );
        Ok(())
    })
}