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(())
})
}