use std::error::Error;
use std::num::NonZeroUsize;
use haematite::db::DEFAULT_RECLAIM_MARK_BYTES;
use haematite::store::NodeCacheBudget;
use super::super::storage::Storage;
use super::{Creation, creation_config};
#[test]
fn a_store_the_service_creates_reclaims_under_the_default_bound() -> Result<(), Box<dyn Error>> {
let directory = tempfile::tempdir()?;
let data_dir = directory.path().join("store");
let creation = Creation {
shards: NonZeroUsize::new(2).ok_or("two shards")?,
cache_bytes_total: NonZeroUsize::new(1 << 20).ok_or("a cache")?,
executor_threads: NonZeroUsize::new(1).ok_or("one thread")?,
};
let config = creation_config(data_dir.clone(), &creation);
assert_eq!(config.retention, haematite::Retention::reclaim(0));
let created = Storage::create(
config,
NodeCacheBudget::Bytes(creation.cache_bytes_total),
fixture_long_wait_interval(),
)
.map_err(|failure| format!("{failure:?}"))?;
drop(created);
let reopened = haematite::Database::open_path_writer(
&data_dir,
haematite::RuntimeOptions {
long_wait_interval: fixture_long_wait_interval(),
reclaim_start: haematite::ReclaimStart::AtOpen,
},
)?;
assert_eq!(
reopened.reclaim_mark_bytes(),
Some(DEFAULT_RECLAIM_MARK_BYTES)
);
Ok(())
}
mod socket_preflight {
use std::error::Error;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use serde_json::{Value, json};
use super::super::run;
type TestResult = Result<(), Box<dyn Error>>;
struct Fixture {
directory: tempfile::TempDir,
data: PathBuf,
socket: PathBuf,
}
impl Fixture {
fn new() -> Result<Self, Box<dyn Error>> {
let directory = tempfile::tempdir()?;
fs::set_permissions(directory.path(), fs::Permissions::from_mode(0o700))?;
let data = directory.path().join("database");
let socket = directory.path().join("socket/haematite.sock");
Ok(Self {
directory,
data,
socket,
})
}
fn private_parent(&self) -> TestResult {
let parent = self.socket.parent().ok_or("missing socket parent")?;
fs::create_dir(parent)?;
fs::set_permissions(parent, fs::Permissions::from_mode(0o700))?;
Ok(())
}
fn config(&self) -> Value {
json!({
"data_dir": self.data,
"socket": self.socket,
"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
}
})
}
fn refusal(&self, config: &Value) -> Result<String, Box<dyn Error>> {
let path = self.directory.path().join("serve.json");
fs::write(&path, serde_json::to_vec(config)?)?;
let args = [
"--config".to_owned(),
path.to_str().ok_or("non-UTF-8 fixture path")?.to_owned(),
"--create".to_owned(),
];
run(args.into_iter())
.err()
.ok_or_else(|| "invalid startup configuration was accepted".into())
}
fn untouched(&self) -> TestResult {
assert!(
!self.data.try_exists()?,
"startup refusal created the store"
);
assert!(
!self.socket.try_exists()?,
"startup refusal created a socket"
);
let parent = self.socket.parent().ok_or("missing socket parent")?;
if parent.is_dir() {
assert!(
!parent.join("haematite.sock.lock").try_exists()?,
"startup refusal created the ownership anchor"
);
}
Ok(())
}
}
fn private_directory_refusal() -> &'static str {
"bind service socket: socket directory must be private (mode 0700)"
}
#[test]
fn create_refuses_non_private_socket_directory_before_creating_database() -> TestResult {
let fixture = Fixture::new()?;
fixture.private_parent()?;
let parent = fixture.socket.parent().ok_or("missing socket parent")?;
fs::set_permissions(parent, fs::Permissions::from_mode(0o755))?;
assert_eq!(
fixture.refusal(&fixture.config())?,
private_directory_refusal()
);
fixture.untouched()?;
assert_eq!(fs::metadata(parent)?.permissions().mode() & 0o777, 0o755);
Ok(())
}
#[test]
fn create_refuses_missing_socket_directory_before_creating_database() -> TestResult {
let fixture = Fixture::new()?;
let parent = fixture.socket.parent().ok_or("missing socket parent")?;
let missing = fs::metadata(parent).err().ok_or("socket parent exists")?;
assert_eq!(
fixture.refusal(&fixture.config())?,
format!("bind service socket: {missing}")
);
fixture.untouched()?;
assert!(!parent.try_exists()?);
Ok(())
}
#[test]
fn create_refuses_non_directory_socket_parent_before_creating_database() -> TestResult {
let fixture = Fixture::new()?;
let parent = fixture.socket.parent().ok_or("missing socket parent")?;
fs::write(parent, b"preserve")?;
assert_eq!(
fixture.refusal(&fixture.config())?,
private_directory_refusal()
);
assert!(
!fixture.data.try_exists()?,
"startup refusal created the store"
);
assert_eq!(fs::read(parent)?, b"preserve");
Ok(())
}
#[test]
fn create_refuses_zero_listener_settings_before_creating_database() -> TestResult {
for name in ["max_frame", "max_clients", "timeout_ms"] {
let fixture = Fixture::new()?;
fixture.private_parent()?;
let mut config = fixture.config();
config[name] = json!(0);
let refusal = fixture.refusal(&config)?;
assert!(refusal.starts_with("service config:"), "{name}: {refusal}");
fixture.untouched()?;
}
Ok(())
}
#[test]
fn a_missing_create_section_does_not_create_a_store_or_socket() -> TestResult {
let fixture = Fixture::new()?;
fixture.private_parent()?;
let mut config = fixture.config();
config
.as_object_mut()
.ok_or("configuration is not an object")?
.remove("create");
assert_eq!(
fixture.refusal(&config)?,
"--create needs a create section in the configuration"
);
fixture.untouched()?;
Ok(())
}
}
#[test]
fn release_date_reads_its_four_arguments() -> Result<(), Box<dyn Error>> {
let args = [
"--config",
"/c.json",
"--branch",
"work",
"--generation",
"7",
"--at",
"9",
];
let (path, request) = super::parse_date(args.map(str::to_owned).into_iter())?;
assert_eq!(path, std::path::PathBuf::from("/c.json"));
assert_eq!(
(request.branch.as_str(), request.generation, request.at),
("work", 7, 9)
);
Ok(())
}
#[test]
fn release_date_refuses_a_repeated_missing_or_bad_argument() {
let cases: [&[&str]; 4] = [
&["--config", "/c.json", "--config", "/d.json"],
&[
"--config",
"/c.json",
"--branch",
"work",
"--generation",
"7",
],
&[
"--config",
"/c.json",
"--branch",
"work",
"--generation",
"seven",
"--at",
"9",
],
&["--branch"],
];
for case in cases {
let args = case.iter().map(|arg| (*arg).to_owned());
assert!(super::parse_date(args).is_err(), "{case:?}");
}
assert!(super::release(["stats".to_owned()].into_iter()).is_err());
}
const fn fixture_long_wait_interval() -> std::time::Duration {
std::time::Duration::from_secs(1)
}