use std::error::Error;
use super::journal;
use crate::branch::{
BranchKind, BranchRefError, BranchRefRecord, BranchRefStore, BranchShardRef, CommitLog,
SnapshotError, SnapshotRegistry,
};
use crate::db::{Database, DatabaseConfig, DatabaseError};
use crate::tree::Hash;
type TestResult = Result<(), Box<dyn Error>>;
fn config(data_dir: &std::path::Path) -> DatabaseConfig {
DatabaseConfig {
data_dir: data_dir.to_path_buf(),
shard_count: 1,
distributed: None,
executor_threads: None,
}
}
fn record(name: &str) -> BranchRefRecord {
BranchRefRecord {
name: name.to_owned(),
created: 1,
kind: BranchKind::Work,
namespace_lineage: None,
seq: 0,
timestamp: 1,
shards: vec![BranchShardRef {
shard_id: 0,
fork_anchor: Hash::from_bytes([7; 32]),
head: Hash::from_bytes([7; 32]),
}],
parents: Vec::new(),
}
}
#[test]
fn wall_l4_persist_failpoint_refuses_both_opens() -> TestResult {
let _guard = journal::test_guard();
let temp = tempfile::tempdir()?;
journal::arm_failpoint();
let registry = SnapshotRegistry::open(temp.path().join("fresh-reg").join("registry.bin"));
assert!(
matches!(registry, Err(SnapshotError::Io(_))),
"SnapshotRegistry::open must refuse on a containing-dir fence failure, got {registry:?}"
);
let temp = tempfile::tempdir()?;
journal::arm_failpoint();
let log = CommitLog::open(temp.path().join("fresh-log").join("commit.log"));
assert!(
matches!(log, Err(SnapshotError::Io(_))),
"CommitLog::open must refuse on a containing-dir fence failure, got {log:?}"
);
Ok(())
}
#[test]
fn wall_l4_persist_lossy_cut_loses_named_snapshot() -> TestResult {
let _guard = journal::test_guard();
let temp = tempfile::tempdir()?;
let path = temp.path().join("meta").join("registry.bin");
let mut registry = SnapshotRegistry::open(&path)?;
registry.name_at("live", Hash::from_bytes([1; 32]), 5)?;
assert_eq!(
journal::pending_len(),
0,
"a real fence leaves nothing pending"
);
journal::cut()?;
let reopened = SnapshotRegistry::open(&path)?;
assert_eq!(reopened.get("live"), Some(Hash::from_bytes([1; 32])));
let temp = tempfile::tempdir()?;
let path = temp.path().join("meta").join("registry.bin");
journal::set_lossy(true);
let mut registry = SnapshotRegistry::open(&path)?;
registry.name_at("doomed", Hash::from_bytes([2; 32]), 5)?;
journal::set_lossy(false);
assert!(
journal::pending_len() >= 1,
"unfenced metadata must remain pending"
);
journal::cut()?;
let reopened = SnapshotRegistry::open(&path)?;
assert_eq!(
reopened.get("doomed"),
None,
"the CUT must rewind the unfenced containing dir, losing the named snapshot"
);
Ok(())
}
#[test]
fn wall_l4l5_refstore_failpoint_refuses() -> TestResult {
let _guard = journal::test_guard();
let temp = tempfile::tempdir()?;
journal::arm_failpoint();
let opened = BranchRefStore::open(temp.path().join("fresh-refs"));
assert!(
matches!(opened, Err(BranchRefError::Io(_))),
"BranchRefStore::open must refuse on a refs-dir fence failure, got {opened:?}"
);
let temp = tempfile::tempdir()?;
let mut store = BranchRefStore::open(temp.path().join("refs"))?;
journal::arm_failpoint();
let created = store.create(record("main"));
assert!(
created.is_err(),
"a create whose entry fence fails must refuse (create-install site)"
);
let temp = tempfile::tempdir()?;
let mut store = BranchRefStore::open(temp.path().join("refs"))?;
store.create(record("main"))?;
journal::arm_failpoint();
let advanced = store.advance(
"main",
1,
0,
&[(0, Hash::from_bytes([9; 32]))],
Vec::new(),
2,
);
assert!(
advanced.is_err(),
"an advance whose entry fence fails must refuse (cas-replace install site)"
);
Ok(())
}
#[test]
fn wall_l4l5_refstore_lossy_cut_loses_record() -> TestResult {
let _guard = journal::test_guard();
let temp = tempfile::tempdir()?;
let refs = temp.path().join("refs");
let mut store = BranchRefStore::open(&refs)?;
store.create(record("keep"))?;
assert_eq!(
journal::pending_len(),
0,
"a real entry fence leaves nothing pending"
);
journal::cut()?;
let reopened = BranchRefStore::open(&refs)?;
assert!(
reopened.get("keep").is_some(),
"a fenced record survives the CUT"
);
let temp = tempfile::tempdir()?;
let refs = temp.path().join("refs");
let mut store = BranchRefStore::open(&refs)?;
journal::reset(); journal::set_lossy(true);
store.create(record("lost"))?;
journal::set_lossy(false);
assert!(
journal::pending_len() >= 1,
"an unfenced record must remain pending"
);
journal::cut()?;
let reopened = BranchRefStore::open(&refs)?;
assert!(
reopened.get("lost").is_none(),
"the CUT must rewind the unfenced record; a cold reopen sees no branch"
);
Ok(())
}
#[test]
fn wall_l5_startup_failpoint_refuses_create() -> TestResult {
let _guard = journal::test_guard();
let temp = tempfile::tempdir()?;
let data_dir = temp.path().join("db");
journal::arm_failpoint();
let created = Database::create(config(&data_dir));
assert!(
matches!(created, Err(DatabaseError::DirectoryCreate(_))),
"an injected data_dir fence failure must refuse with DirectoryCreate, got {created:?}"
);
assert!(
!data_dir.join("config.json").exists(),
"a refused create must not have written a durable config.json"
);
Ok(())
}
#[test]
fn wall_l5_startup_lossy_cut_loses_data_dir() -> TestResult {
let _guard = journal::test_guard();
let temp = tempfile::tempdir()?;
let data_dir = temp.path().join("db");
let database = Database::create(config(&data_dir))?;
drop(database);
journal::reset(); journal::cut()?;
assert!(
data_dir.join("config.json").exists(),
"a fenced data_dir survives the CUT"
);
drop(Database::open(&data_dir)?);
let temp = tempfile::tempdir()?;
let data_dir = temp.path().join("db");
journal::set_lossy(true);
let database = Database::create(config(&data_dir))?;
journal::set_lossy(false);
drop(database);
assert!(
journal::pending_len() >= 1,
"an unfenced data_dir must remain pending"
);
journal::cut()?;
assert!(
!data_dir.exists(),
"the CUT must rewind the unfenced data_dir entry; the database is unreachable"
);
Ok(())
}