mod common;
use common::child_test_command;
use mace::{Mace, Options, RandomPath};
use std::path::Path;
use std::process::Output;
fn cli() -> std::process::Command {
child_test_command(Path::new(env!("CARGO_BIN_EXE_mace-kv")))
}
fn run(args: &[&str]) -> Output {
cli().args(args).output().expect("spawn the mace-kv binary")
}
fn stdout(output: &Output) -> String {
String::from_utf8_lossy(&output.stdout).into_owned()
}
fn stderr(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).into_owned()
}
fn seed(root: &Path) {
let mut opt = Options::new(root);
opt.sync_on_write = true;
let mace = Mace::new(opt.validate().expect("validate")).expect("open store");
let db = mace.new_bucket("x", Default::default()).expect("bucket");
for index in 0..16u32 {
let tx = db.begin().expect("txn");
tx.put(format!("k_{index:04}"), vec![b'v'; 128])
.expect("put");
tx.commit().expect("commit");
}
db.checkpoint_and_wait();
}
#[test]
fn a_healthy_directory_is_reported_clean() {
let root = RandomPath::tmp();
std::fs::create_dir_all(&*root).expect("scratch dir");
seed(&root);
let output = run(&["check", &root.to_string_lossy()]);
assert_eq!(output.status.code(), Some(0), "{}", stderr(&output));
let text = stdout(&output);
assert!(text.contains(": ok"), "unexpected report: {text}");
}
#[test]
fn a_checkpoint_copy_is_reported_clean_by_the_cli() {
let scratch = RandomPath::tmp();
std::fs::create_dir_all(&*scratch).expect("scratch dir");
let root = scratch.join("db");
let mut opt = Options::new(&root);
opt.sync_on_write = true;
let mace = Mace::new(opt.validate().expect("validate")).expect("open store");
let db = mace.new_bucket("x", Default::default()).expect("bucket");
for index in 0..16u32 {
let tx = db.begin().expect("txn");
tx.put(format!("k_{index:04}"), vec![b'v'; 128])
.expect("put");
tx.commit().expect("commit");
}
db.checkpoint_and_wait();
let backup = scratch.join("backup");
mace.create_checkpoint(&backup).expect("checkpoint copy");
drop(db);
drop(mace);
let backup = backup.to_string_lossy().into_owned();
let output = run(&["check", &backup]);
assert_eq!(
output.status.code(),
Some(0),
"a checkpoint copy must check clean, stdout: {} stderr: {}",
stdout(&output),
stderr(&output)
);
assert!(
stdout(&output).contains(": ok"),
"unexpected report: {}",
stdout(&output)
);
}
#[test]
fn damage_is_reported_with_a_stable_category() {
let root = RandomPath::tmp();
std::fs::create_dir_all(&*root).expect("scratch dir");
seed(&root);
let data = std::fs::read_dir(Options::new(&*root).data_root())
.expect("data dir")
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.collect::<Vec<_>>();
assert!(!data.is_empty(), "fixture must write a payload file");
std::fs::remove_file(&data[0]).expect("remove payload file");
let output = run(&["check", &root.to_string_lossy()]);
assert_eq!(output.status.code(), Some(1), "{}", stderr(&output));
let text = stdout(&output);
assert!(
text.contains("bad_payload_file:"),
"damage must be reported under its category: {text}"
);
assert!(
text.contains("file=") && !text.contains("file=-"),
"the report must locate the file: {text}"
);
}
#[test]
fn a_missing_directory_is_not_created() {
let root = RandomPath::tmp();
let missing = root.join("absent");
assert!(!missing.exists());
let output = run(&["check", &missing.to_string_lossy()]);
assert_eq!(output.status.code(), Some(1), "{}", stderr(&output));
assert!(
!missing.exists(),
"checking a missing directory must not create it"
);
}
#[test]
fn usage_errors_exit_with_code_two() {
for args in [
vec![],
vec!["check"],
vec!["check", "a", "b"],
vec!["verify"],
] {
let output = run(&args);
assert_eq!(
output.status.code(),
Some(2),
"args {args:?} must be a usage error, stderr: {}",
stderr(&output)
);
assert!(
stderr(&output).contains("Usage: mace-kv"),
"args {args:?} must print usage on stderr, got: {}",
stderr(&output)
);
assert!(
stdout(&output).trim().is_empty(),
"args {args:?} must not report anything on stdout, got: {}",
stdout(&output)
);
}
}
#[test]
fn help_and_version_exit_zero_on_stdout() {
for args in [vec!["--help"], vec!["check", "--help"]] {
let output = run(&args);
assert_eq!(
output.status.code(),
Some(0),
"args {args:?} must not be an error, stderr: {}",
stderr(&output)
);
assert!(
stdout(&output).contains("Usage: mace-kv"),
"args {args:?} must print usage on stdout, got: {}",
stdout(&output)
);
assert!(
stderr(&output).trim().is_empty(),
"args {args:?} must not write to stderr, got: {}",
stderr(&output)
);
}
let output = run(&["--version"]);
assert_eq!(output.status.code(), Some(0));
assert!(
stdout(&output).starts_with("mace-kv "),
"the version must name the program, got: {}",
stdout(&output)
);
}
#[test]
fn a_live_database_is_reported_as_busy_not_as_damage() {
let root = RandomPath::tmp();
std::fs::create_dir_all(&*root).expect("scratch dir");
seed(&root);
let mut opt = Options::new(&*root);
opt.sync_on_write = true;
let live = Mace::new(opt.validate().expect("validate")).expect("live open");
let output = run(&["check", &root.to_string_lossy()]);
let text = stdout(&output);
assert!(
text.contains("metadata_busy:"),
"a locked store must be reported under its own category: {text}"
);
assert!(
!text.contains("unreadable_metadata:"),
"lock contention is not corruption and must not claim unreadable metadata: {text}"
);
assert_eq!(output.status.code(), Some(1), "{}", stderr(&output));
drop(live);
}
#[test]
fn page_level_metadata_damage_terminates_the_cli() {
let root = RandomPath::tmp();
std::fs::create_dir_all(&*root).expect("scratch dir");
let mut opt = Options::new(&*root);
opt.sync_on_write = true;
let mace = Mace::new(opt.validate().expect("validate")).expect("open store");
let mut buckets = Vec::new();
for index in 0..4 {
buckets.push(
mace.new_bucket(format!("b{index}"), Default::default())
.expect("bucket"),
);
}
for index in 0..4000u32 {
let db = &buckets[(index as usize) % buckets.len()];
let tx = db.begin().expect("txn");
tx.put(format!("k_{index:05}"), vec![b'v'; 256])
.expect("put");
tx.commit().expect("commit");
}
for db in &buckets {
db.checkpoint_and_wait();
}
drop(buckets);
drop(mace);
let manifest = Options::new(&*root).manifest();
let pages = std::fs::metadata(&manifest).expect("metadata").len() / 4096;
assert!(
pages > 20,
"fixture must span many metadata pages, got {pages}"
);
let file = std::fs::OpenOptions::new()
.write(true)
.open(&manifest)
.expect("open manifest");
file.set_len(8 * 4096).expect("truncate manifest");
drop(file);
let output = run(&["check", &root.to_string_lossy()]);
let text = stderr(&output);
assert!(
text.contains("BTREE_FATAL"),
"page damage must surface the upstream fatal line, got: {text}"
);
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
assert_eq!(
output.status.signal(),
Some(6),
"page-level metadata damage is fatal by design"
);
}
#[cfg(not(unix))]
assert!(!output.status.success());
}