use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Arc;
use rusqlite::Connection;
use super::{callbacks, vfs, CodeMapHandleGuard, GuardError, Mode, Role};
#[cfg(unix)]
use super::{OpenAccess, ProductionBase, ProductionKind};
use crate::error::SqliteError;
use crate::StorageBackend;
#[path = "ledger_lifetime_tests.rs"]
mod ledger_lifetime;
#[cfg(unix)]
#[path = "sidecar_barrier_tests.rs"]
mod sidecar_barrier;
const CHILD_TEST: &str = "KHIVE_CODE_MAP_VFS_TEST_CHILD";
#[cfg(unix)]
const PRODUCTION_LOCK_PROBE: &str = "KHIVE_CODE_MAP_VFS_PRODUCTION_LOCK_PROBE";
fn run_in_child() -> bool {
let thread = std::thread::current();
let name = thread.name().expect("libtest names its test threads");
if std::env::var(CHILD_TEST).ok().as_deref() == Some(name) {
return false;
}
let output = Command::new(std::env::current_exe().expect("current test executable"))
.args(["--exact", name, "--nocapture", "--test-threads=1"])
.env(CHILD_TEST, name)
.output()
.expect("spawn isolated VFS test");
assert!(
output.status.success()
&& String::from_utf8_lossy(&output.stdout)
.contains("test result: ok. 1 passed; 0 failed;"),
"isolated VFS test failed:\n{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
true
}
fn fixture() -> tempfile::TempDir {
let plain_temp_root = std::env::temp_dir()
.canonicalize()
.expect("plain absolute temp root");
tempfile::Builder::new()
.prefix("kh-code-map-vfs-")
.tempdir_in(plain_temp_root)
.expect("private VFS fixture")
}
fn open_code_map(
path: impl AsRef<Path>,
protected_main: &[PathBuf],
protected_events: &[PathBuf],
) -> Result<StorageBackend, SqliteError> {
StorageBackend::sqlite_code_map_with_policies(
path,
protected_main,
protected_events,
crate::DiskGuardEnvironment::default()
.resolve(Some(0), Some(100))
.unwrap(),
crate::PoolConfig::for_test()
.volume_lock_dir
.expect("private fixture lock directory"),
)
}
#[cfg(unix)]
#[test]
fn configured_production_parent_alias_is_sampled() {
if run_in_child() {
return;
}
let dir = fixture();
let physical_ancestor = dir.path().join("physical-ancestor");
let alias_ancestor = dir.path().join("alias-ancestor");
let physical_root = physical_ancestor.join("configured-root");
let configured_root = alias_ancestor.join("configured-root");
std::fs::create_dir_all(&physical_root).unwrap();
std::os::unix::fs::symlink(&physical_ancestor, &alias_ancestor).unwrap();
let production = physical_root.join("production.db");
seed_rollback(&production);
let configured_production = configured_root.join("production.db");
let target = dir.path().join("dedicated-map.db");
let backend = open_code_map(&target, std::slice::from_ref(&configured_production), &[])
.expect("configured parent alias resolves before protected leaf sampling");
drop(backend);
let alias = dir.path().join("production-hardlink.db");
std::fs::hard_link(&production, &alias).unwrap();
let error = open_code_map(&alias, std::slice::from_ref(&configured_production), &[])
.err()
.expect("physical production identity remains protected");
assert!(
error.to_string().contains("protected production identity"),
"{error}"
);
}
#[cfg(unix)]
#[test]
fn planted_target_parent_symlink_below_fixture_root_refuses() {
if run_in_child() {
return;
}
let dir = fixture();
let physical_root = dir.path().join("target-root");
let planted = dir.path().join("planted-link");
std::fs::create_dir(&physical_root).unwrap();
std::os::unix::fs::symlink(&physical_root, &planted).unwrap();
let target = planted.join("code-map.db");
let error = open_code_map(&target, &[], &[])
.err()
.expect("target parent symlink must refuse before opening a database");
assert!(
error
.to_string()
.contains("symlinked code-map parent component"),
"{error}"
);
assert!(!physical_root.join("code-map.db").exists());
}
#[test]
fn missing_target_parent_is_refused_and_not_created() {
if run_in_child() {
return;
}
let dir = fixture();
let parent = dir.path().join("missing-parent");
let error = open_code_map(parent.join("code-map.db"), &[], &[])
.err()
.expect("a missing parent directory must refuse the open");
assert!(
error
.to_string()
.contains("parent directory of a code-map database must already exist"),
"{error}"
);
assert!(!parent.exists());
}
fn seed_rollback(path: &Path) {
let conn = Connection::open(path).expect("seed SQLite database");
conn.execute_batch(
"CREATE TABLE witness(value INTEGER NOT NULL); INSERT INTO witness VALUES(7)",
)
.expect("seed witness row");
}
#[cfg(unix)]
fn main_base(path: &Path) -> ProductionBase {
ProductionBase {
path: path.to_path_buf(),
kind: ProductionKind::Main,
}
}
fn companion(path: &Path, suffix: &str) -> PathBuf {
let mut name = path.as_os_str().to_os_string();
name.push(suffix);
PathBuf::from(name)
}
#[test]
fn fresh_first_open_stays_in_delete_and_never_uses_shm() {
if run_in_child() {
return;
}
let dir = fixture();
let target = dir.path().join("code-map.db");
let registrations_before = vfs::registration_count();
let shm_before = callbacks::shm_violation_count();
let backend = open_code_map(&target, &[], &[]).expect("first guarded open");
{
let writer = backend.pool().writer().expect("guarded writer");
let mode: String = writer
.conn()
.query_row("PRAGMA journal_mode", [], |row| row.get(0))
.expect("journal mode");
assert_eq!(mode.to_ascii_lowercase(), "delete");
writer
.conn()
.execute_batch(
"CREATE TABLE witness(value INTEGER NOT NULL); INSERT INTO witness VALUES(1)",
)
.expect("first write through guarded VFS");
}
assert!(target.exists());
assert!(!companion(&target, "-wal").exists());
assert!(!companion(&target, "-shm").exists());
assert_eq!(callbacks::shm_violation_count(), shm_before);
assert_eq!(vfs::registration_count(), registrations_before + 1);
drop(backend);
let reopened = open_code_map(&target, &[], &[]).expect("reuse guarded VFS");
assert_eq!(vfs::registration_count(), registrations_before + 1);
let count: i64 = reopened
.pool()
.writer()
.expect("reopened writer")
.conn()
.query_row("SELECT COUNT(*) FROM witness", [], |row| row.get(0))
.expect("persisted first write");
assert_eq!(count, 1);
}
#[test]
fn live_rollback_pool_reopens_without_quiescent_wal_transition() {
if run_in_child() {
return;
}
let dir = fixture();
let target = dir.path().join("live-rollback-code-map.db");
let first = open_code_map(&target, &[], &[]).expect("first guarded pool");
first
.pool()
.writer()
.expect("first writer")
.conn()
.execute_batch("CREATE TABLE witness(value INTEGER); INSERT INTO witness VALUES(7)")
.expect("seed a committed DELETE-mode row");
let second = open_code_map(&target, &[], &[])
.expect("a live rollback pool must not trigger a WAL transition");
let writer = second.pool().writer().expect("second guarded writer");
let mode: String = writer
.conn()
.query_row("PRAGMA journal_mode", [], |row| row.get(0))
.expect("second journal mode");
let count: i64 = writer
.conn()
.query_row("SELECT COUNT(*) FROM witness", [], |row| row.get(0))
.expect("first pool's row remains visible");
assert_eq!(mode.to_ascii_lowercase(), "delete");
assert_eq!(count, 1);
assert!(!companion(&target, "-wal").exists());
assert!(!companion(&target, "-shm").exists());
}
#[test]
fn prior_wal_is_converted_to_delete_without_losing_rows_or_using_shm() {
if run_in_child() {
return;
}
let dir = fixture();
let target = dir.path().join("prior-wal-code-map.db");
let seed = Connection::open(&target).expect("seed prior WAL with ordinary SQLite");
let seeded_mode: String = seed
.query_row("PRAGMA journal_mode=WAL", [], |row| row.get(0))
.expect("enable WAL on prior map");
assert_eq!(seeded_mode.to_ascii_lowercase(), "wal");
seed.execute_batch(
"CREATE TABLE witness(value INTEGER NOT NULL); INSERT INTO witness VALUES(7)",
)
.expect("seed prior WAL row");
drop(seed);
let shm_before = callbacks::shm_violation_count();
let backend = open_code_map(&target, &[], &[]).expect("guarded quiescent WAL transition");
let writer = backend.pool().writer().expect("post-transition writer");
let mode: String = writer
.conn()
.query_row("PRAGMA journal_mode", [], |row| row.get(0))
.expect("post-transition mode");
let value: i64 = writer
.conn()
.query_row("SELECT value FROM witness", [], |row| row.get(0))
.expect("prior WAL row survived");
assert_eq!(mode.to_ascii_lowercase(), "delete");
assert_eq!(value, 7);
assert!(!companion(&target, "-wal").exists());
assert!(!companion(&target, "-shm").exists());
assert_eq!(callbacks::shm_violation_count(), shm_before);
}
#[test]
fn transition_rejects_new_wal_and_shm_names_before_cleanup() {
if run_in_child() {
return;
}
let dir = fixture();
for (index, (suffix, role)) in [("-wal", Role::TransitionWal), ("-shm", Role::Shm)]
.into_iter()
.enumerate()
{
let target = dir.path().join(format!("prior-wal-{index}.db"));
let seed = Connection::open(&target).unwrap();
let mode: String = seed
.query_row("PRAGMA journal_mode=WAL", [], |row| row.get(0))
.unwrap();
assert_eq!(mode.to_ascii_lowercase(), "wal");
seed.execute_batch("CREATE TABLE witness(value INTEGER)")
.unwrap();
drop(seed);
for old_suffix in ["-wal", "-shm"] {
let old = companion(&target, old_suffix);
if old.exists() {
std::fs::remove_file(old).unwrap();
}
}
let guard = CodeMapHandleGuard::new(target.clone(), Mode::QuiescentWalTransition, vec![])
.expect("attest original WAL target");
let initial = guard.preflight().unwrap();
let unexpected = companion(&target, suffix);
std::fs::write(&unexpected, b"unattested-companion").unwrap();
let error = guard
.verify_transition_companions(&initial)
.expect_err("new companion must fail attestation");
assert!(matches!(&error, GuardError::ProtectedChanged), "{error}");
let error = guard
.delete(role, true)
.expect_err("pre-switch companion deletion must be forbidden");
assert!(matches!(&error, GuardError::Unsafe { .. }), "{error}");
assert_eq!(
std::fs::read(&unexpected).unwrap(),
b"unattested-companion".to_vec()
);
}
}
#[test]
fn prior_wal_with_rollback_journal_refuses_incomplete_and_preserves_sidecars() {
if run_in_child() {
return;
}
let dir = fixture();
let target = dir.path().join("prior-wal.db");
let seed = Connection::open(&target).unwrap();
let mode: String = seed
.query_row("PRAGMA journal_mode=WAL", [], |row| row.get(0))
.unwrap();
assert_eq!(mode.to_ascii_lowercase(), "wal");
seed.execute_batch("CREATE TABLE witness(value INTEGER)")
.unwrap();
drop(seed);
let main_before = std::fs::read(&target).unwrap();
let sidecars = [
(companion(&target, "-wal"), b"prior-wal".to_vec()),
(companion(&target, "-shm"), b"prior-shm".to_vec()),
(companion(&target, "-journal"), b"prior-journal".to_vec()),
];
for (path, bytes) in &sidecars {
std::fs::write(path, bytes).unwrap();
}
let shm_before = callbacks::shm_violation_count();
let error = super::prepare_rollback_target(target.clone(), vec![])
.expect_err("mixed WAL and journal must refuse before DELETE setter");
assert!(
matches!(
&error,
super::transition::TransitionError::Incomplete {
stage: "WAL admission",
..
}
),
"{error}"
);
assert_eq!(std::fs::read(&target).unwrap(), main_before);
for (path, bytes) in &sidecars {
assert_eq!(std::fs::read(path).unwrap().as_slice(), bytes.as_slice());
}
assert_eq!(callbacks::shm_violation_count(), shm_before);
}
#[test]
fn byte_copy_of_production_is_not_an_identity_alias() {
if run_in_child() {
return;
}
let dir = fixture();
let production = dir.path().join("production.db");
let target = dir.path().join("code-map.db");
seed_rollback(&production);
std::fs::copy(&production, &target).expect("copy equal bytes to a different inode");
let backend = open_code_map(&target, std::slice::from_ref(&production), &[])
.expect("independent byte copy must be admissible");
backend
.pool()
.writer()
.expect("guarded writer")
.conn()
.execute("INSERT INTO witness VALUES(8)", [])
.expect("write independent code map");
let original_count: i64 = Connection::open(&production)
.expect("production remains openable")
.query_row("SELECT COUNT(*) FROM witness", [], |row| row.get(0))
.expect("production witness");
assert_eq!(original_count, 1);
assert_eq!(super::quarantine_occupancy(), 0);
}
#[cfg(unix)]
fn has_moved(conn: &Connection) -> (std::ffi::c_int, std::ffi::c_int) {
let mut moved: std::ffi::c_int = -1;
let result = unsafe {
rusqlite::ffi::sqlite3_file_control(
conn.handle(),
c"main".as_ptr(),
rusqlite::ffi::SQLITE_FCNTL_HAS_MOVED,
(&mut moved as *mut std::ffi::c_int).cast(),
)
};
(result, moved)
}
#[cfg(unix)]
#[test]
fn opened_main_reports_when_its_path_names_another_file() {
if run_in_child() {
return;
}
let dir = fixture();
let target = dir.path().join("code-map.db");
let backend = open_code_map(&target, &[], &[]).expect("first guarded open");
let writer = backend.pool().writer().expect("guarded writer");
assert_eq!(has_moved(writer.conn()), (rusqlite::ffi::SQLITE_OK, 0));
let moved = dir.path().join("moved.db");
std::fs::rename(&target, &moved).expect("move the opened main");
assert_eq!(has_moved(writer.conn()), (rusqlite::ffi::SQLITE_OK, 1));
std::fs::copy(&moved, &target).expect("equal bytes at the old path");
assert_eq!(has_moved(writer.conn()), (rusqlite::ffi::SQLITE_OK, 1));
std::fs::remove_file(&target).expect("drop the copy");
std::fs::rename(&moved, &target).expect("restore the opened main");
assert_eq!(has_moved(writer.conn()), (rusqlite::ffi::SQLITE_OK, 0));
}
#[cfg(unix)]
#[test]
fn main_hardlink_swap_is_quarantined_without_poisoning_production() {
if let Some(path) = std::env::var_os(PRODUCTION_LOCK_PROBE) {
let conn = Connection::open(PathBuf::from(path)).expect("open production from lock probe");
conn.busy_timeout(std::time::Duration::ZERO).unwrap();
let error = conn
.execute_batch("BEGIN IMMEDIATE")
.expect_err("other process must not acquire the production writer lock");
assert!(matches!(
&error,
rusqlite::Error::SqliteFailure(sqlite, _)
if matches!(
sqlite.code,
rusqlite::ErrorCode::DatabaseBusy | rusqlite::ErrorCode::DatabaseLocked
)
));
return;
}
if run_in_child() {
return;
}
let dir = fixture();
let production = dir.path().join("production.db");
let target = dir.path().join("code-map.db");
seed_rollback(&production);
std::fs::copy(&production, &target).unwrap();
let guard =
CodeMapHandleGuard::new(target.clone(), Mode::Rollback, vec![main_base(&production)])
.unwrap();
let production_before = std::fs::read(&production).unwrap();
let quarantine_before = super::quarantine_occupancy();
let production_conn = Connection::open(&production).unwrap();
production_conn
.execute_batch("BEGIN IMMEDIATE")
.expect("hold production writer lock before swapped open");
let swap_target = target.clone();
let swap_production = production.clone();
super::set_before_os_open(move || {
std::fs::remove_file(&swap_target).unwrap();
std::fs::hard_link(&swap_production, &swap_target).unwrap();
});
let error = guard
.open(Role::Main, OpenAccess::ReadWrite)
.expect_err("swapped production identity must be refused");
assert!(
matches!(&error, GuardError::ProtectedAlias { .. }),
"{error}"
);
assert_eq!(super::quarantine_occupancy(), quarantine_before + 1);
let name = std::thread::current().name().unwrap().to_owned();
let probe = Command::new(std::env::current_exe().unwrap())
.args(["--exact", name.as_str(), "--nocapture", "--test-threads=1"])
.env(CHILD_TEST, &name)
.env(PRODUCTION_LOCK_PROBE, &production)
.output()
.expect("probe production writer lock from another process");
assert!(
probe.status.success()
&& String::from_utf8_lossy(&probe.stdout)
.contains("test result: ok. 1 passed; 0 failed;"),
"production lock was lost after alias refusal:\n{}\n{}",
String::from_utf8_lossy(&probe.stdout),
String::from_utf8_lossy(&probe.stderr)
);
production_conn
.execute_batch("INSERT INTO witness VALUES(9); COMMIT")
.expect("same production connection must finish its write after refusal");
let count: i64 = production_conn
.query_row("SELECT COUNT(*) FROM witness", [], |row| row.get(0))
.unwrap();
assert_eq!(count, 2);
let production_after = std::fs::read(&production).unwrap();
assert!(production_after.starts_with(b"SQLite format 3\0"));
assert_ne!(production_after, production_before);
}
#[cfg(unix)]
#[test]
fn main_symlink_swap_is_refused_without_an_opened_handle() {
if run_in_child() {
return;
}
let dir = fixture();
let production = dir.path().join("production.db");
let target = dir.path().join("code-map.db");
seed_rollback(&production);
std::fs::copy(&production, &target).unwrap();
let guard =
CodeMapHandleGuard::new(target.clone(), Mode::Rollback, vec![main_base(&production)])
.unwrap();
let production_before = std::fs::read(&production).unwrap();
let quarantine_before = super::quarantine_occupancy();
let swap_target = target.clone();
let swap_production = production.clone();
super::set_before_os_open(move || {
std::fs::remove_file(&swap_target).unwrap();
std::os::unix::fs::symlink(&swap_production, &swap_target).unwrap();
});
let error = guard
.open(Role::Main, OpenAccess::ReadWrite)
.expect_err("symlink swap must be refused");
assert!(matches!(&error, GuardError::Io { .. }), "{error}");
assert_eq!(super::quarantine_occupancy(), quarantine_before);
assert_eq!(std::fs::read(&production).unwrap(), production_before);
}
#[test]
fn multiply_linked_journal_is_refused_before_any_open() {
if run_in_child() {
return;
}
let dir = fixture();
let target = dir.path().join("code-map.db");
let other = dir.path().join("other-journal");
seed_rollback(&target);
std::fs::write(&other, b"journal-canary").unwrap();
let journal = companion(&target, "-journal");
std::fs::hard_link(&other, &journal).unwrap();
let original = std::fs::read(&other).unwrap();
let Err(error) = CodeMapHandleGuard::new(target.clone(), Mode::Rollback, vec![]) else {
panic!("multiply linked journal must be refused");
};
assert!(matches!(&error, GuardError::Unsafe { .. }), "{error}");
assert_eq!(std::fs::read(&other).unwrap(), original);
assert_eq!(std::fs::read(&journal).unwrap(), original);
assert_eq!(super::quarantine_occupancy(), 0);
}
#[cfg(unix)]
#[test]
fn quarantine_cap_refuses_before_another_native_open() {
use std::sync::atomic::{AtomicBool, Ordering};
if run_in_child() {
return;
}
let dir = fixture();
let production = dir.path().join("production.db");
let target = dir.path().join("code-map.db");
seed_rollback(&production);
std::fs::copy(&production, &target).unwrap();
let guard =
CodeMapHandleGuard::new(target.clone(), Mode::Rollback, vec![main_base(&production)])
.unwrap();
assert_eq!(super::quarantine_occupancy(), 0);
for count in 1..=super::QUARANTINE_CAP {
if count > 1 {
std::fs::remove_file(&target).unwrap();
std::fs::copy(&production, &target).unwrap();
}
let swap_target = target.clone();
let swap_production = production.clone();
super::set_before_os_open(move || {
std::fs::remove_file(&swap_target).unwrap();
std::fs::hard_link(&swap_production, &swap_target).unwrap();
});
let error = guard
.open(Role::Main, OpenAccess::ReadWrite)
.expect_err("swapped identity must be quarantined");
assert!(
matches!(&error, GuardError::ProtectedAlias { .. }),
"{error}"
);
assert_eq!(super::quarantine_occupancy(), count);
}
let opened = Arc::new(AtomicBool::new(false));
let hook_opened = Arc::clone(&opened);
super::set_before_os_open(move || hook_opened.store(true, Ordering::SeqCst));
let error = guard
.open(Role::Main, OpenAccess::ReadWrite)
.expect_err("quarantine cap must refuse further opens");
assert!(matches!(&error, GuardError::QuarantineFull), "{error}");
assert!(!opened.load(Ordering::SeqCst));
let Err(error) = CodeMapHandleGuard::new(target, Mode::Rollback, vec![]) else {
panic!("new guards must also refuse at the cap");
};
assert!(matches!(&error, GuardError::QuarantineFull), "{error}");
assert!(error.to_string().contains("restart"));
}
#[test]
fn rollback_registration_reuses_contract_then_caps_distinct_targets() {
if run_in_child() {
return;
}
let dir = fixture();
let target = dir.path().join("code-map-0.db");
let before = vfs::registration_count();
let first = Arc::new(CodeMapHandleGuard::new(target.clone(), Mode::Rollback, vec![]).unwrap());
let first_name = vfs::register(first).expect("first rollback VFS registration");
let again = Arc::new(CodeMapHandleGuard::new(target, Mode::Rollback, vec![]).unwrap());
assert_eq!(vfs::register(again).unwrap(), first_name);
assert_eq!(vfs::registration_count(), before + 1);
for index in 1..512 {
let target = dir.path().join(format!("code-map-{index}.db"));
let guard = Arc::new(CodeMapHandleGuard::new(target, Mode::Rollback, vec![]).unwrap());
vfs::register(guard).expect("distinct target below registration cap");
}
assert_eq!(vfs::registration_count(), before + 512);
let overflow = dir.path().join("code-map-overflow.db");
let guard = Arc::new(CodeMapHandleGuard::new(overflow, Mode::Rollback, vec![]).unwrap());
let error = vfs::register(guard).expect_err("distinct target beyond the cap must be refused");
assert!(matches!(&error, GuardError::RegistrationFull), "{error}");
assert!(error.to_string().contains("restart"));
assert_eq!(vfs::registration_count(), before + 512);
}
#[cfg(unix)]
#[test]
fn refused_open_names_the_guard_reason_through_cannot_open() {
if run_in_child() {
return;
}
let dir = fixture();
let production = dir.path().join("production.db");
let target = dir.path().join("code-map.db");
seed_rollback(&production);
std::fs::copy(&production, &target).unwrap();
let guard = Arc::new(
CodeMapHandleGuard::new(target.clone(), Mode::Rollback, vec![main_base(&production)])
.unwrap(),
);
let name = vfs::register(guard).unwrap();
let swap_target = target.clone();
let swap_production = production.clone();
super::set_before_os_open(move || {
std::fs::remove_file(&swap_target).unwrap();
std::os::unix::fs::symlink(&swap_production, &swap_target).unwrap();
});
let Err(error) = crate::pool::ConnectionPool::new(crate::pool::PoolConfig {
path: Some(target.clone()),
code_map_vfs: Some(name.clone()),
wal_mode: false,
..crate::pool::PoolConfig::default()
}) else {
panic!("a main swapped for a symlink must refuse the guarded open");
};
match &error {
SqliteError::Rusqlite(rusqlite::Error::SqliteFailure(code, Some(message))) => {
assert_eq!(code.code, rusqlite::ErrorCode::CannotOpen, "{error}");
assert!(
message.contains("code-map VFS refused the Main open: code-map VFS cannot prove"),
"{error}"
);
}
other => panic!("expected CannotOpen carrying the refusal, got {other}"),
}
assert_eq!(
vfs::take_refusal(&name),
None,
"a reported refusal is consumed"
);
}
#[test]
fn migration_failure_names_a_recorded_refusal_once() {
if run_in_child() {
return;
}
let dir = fixture();
let target = dir.path().join("code-map.db");
seed_rollback(&target);
let guard = Arc::new(CodeMapHandleGuard::new(target, Mode::Rollback, vec![]).unwrap());
let name = vfs::register(Arc::clone(&guard)).unwrap();
guard.record_refusal(format!(
"code-map VFS refused the {:?} open: {}",
Role::Journal,
GuardError::ProtectedChanged
));
let unrelated = super::with_refusal(SqliteError::InvalidData("unrelated".into()), &name);
assert!(
matches!(&unrelated, SqliteError::InvalidData(message) if message == "unrelated"),
"{unrelated}"
);
let failed = || SqliteError::Migration {
version: 5,
error: "unable to open database file".into(),
};
assert_eq!(
super::with_refusal(failed(), &name).to_string(),
"migration v5 failed: unable to open database file; code-map VFS refused the Journal \
open: code-map VFS protected production paths changed during admission"
);
assert_eq!(
super::with_refusal(failed(), &name).to_string(),
"migration v5 failed: unable to open database file"
);
}
fn cannot_open() -> SqliteError {
SqliteError::Rusqlite(rusqlite::Error::SqliteFailure(
rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_CANTOPEN),
Some("unable to open database file".into()),
))
}
#[test]
fn transition_guard_refusal_is_taken_by_its_vfs_name() {
if run_in_child() {
return;
}
let dir = fixture();
let target = dir.path().join("prior-wal.db");
let seed = Connection::open(&target).unwrap();
let mode: String = seed
.query_row("PRAGMA journal_mode=WAL", [], |row| row.get(0))
.unwrap();
assert_eq!(mode.to_ascii_lowercase(), "wal");
drop(seed);
let guard = Arc::new(
CodeMapHandleGuard::new(target, Mode::QuiescentWalTransition, vec![])
.expect("attest original WAL target"),
);
let name = vfs::register(Arc::clone(&guard)).unwrap();
guard.record_refusal("transition refusal".into());
assert_eq!(
vfs::take_refusal(&name).as_deref(),
Some("transition refusal")
);
}
#[test]
fn only_a_refusal_recorded_during_the_operation_is_named() {
if run_in_child() {
return;
}
let dir = fixture();
let target = dir.path().join("code-map.db");
seed_rollback(&target);
let guard = Arc::new(CodeMapHandleGuard::new(target, Mode::Rollback, vec![]).unwrap());
let name = vfs::register(Arc::clone(&guard)).unwrap();
guard.record_refusal("earlier refusal".into());
let error = super::naming_refusal::<()>(&name, || Err(cannot_open())).unwrap_err();
assert_eq!(error.to_string(), cannot_open().to_string());
let error = super::naming_refusal::<()>(&name, || {
guard.record_refusal("refusal during the open".into());
Err(cannot_open())
})
.unwrap_err();
assert!(
error.to_string().ends_with("; refusal during the open"),
"{error}"
);
}
#[test]
fn migration_failure_for_another_cause_leaves_the_refusal() {
if run_in_child() {
return;
}
let dir = fixture();
let target = dir.path().join("code-map.db");
seed_rollback(&target);
let guard = Arc::new(CodeMapHandleGuard::new(target, Mode::Rollback, vec![]).unwrap());
let name = vfs::register(Arc::clone(&guard)).unwrap();
guard.record_refusal("unrelated refusal".into());
let failed = SqliteError::Migration {
version: 5,
error: "near \"x\": syntax error".into(),
};
assert_eq!(
super::with_refusal(failed, &name).to_string(),
"migration v5 failed: near \"x\": syntax error"
);
assert_eq!(
vfs::take_refusal(&name).as_deref(),
Some("unrelated refusal")
);
}
#[test]
fn cantopen_message_is_what_sqlite_reports() {
if run_in_child() {
return;
}
let dir = fixture();
let error = Connection::open_with_flags(
dir.path().join("missing-dir").join("map.db"),
rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE,
)
.unwrap_err();
assert!(
matches!(&error, rusqlite::Error::SqliteFailure(code, _)
if code.code == rusqlite::ErrorCode::CannotOpen),
"{error}"
);
assert!(
error.to_string().contains(super::CANTOPEN_MESSAGE),
"{error}"
);
}
#[cfg(unix)]
#[test]
fn refused_guarded_transition_open_names_the_guard_reason() {
if run_in_child() {
return;
}
let dir = fixture();
let production = dir.path().join("production.db");
let target = dir.path().join("code-map.db");
seed_rollback(&production);
std::fs::copy(&production, &target).unwrap();
let guard = Arc::new(
CodeMapHandleGuard::new(target.clone(), Mode::Rollback, vec![main_base(&production)])
.unwrap(),
);
let name = vfs::register(guard).unwrap();
let swap_target = target.clone();
let swap_production = production.clone();
super::set_before_os_open(move || {
std::fs::remove_file(&swap_target).unwrap();
std::os::unix::fs::symlink(&swap_production, &swap_target).unwrap();
});
let Err(error) = super::transition::open_guarded(
&target,
rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
&name,
"guarded WAL open",
|stage, error| super::transition::TransitionError::Incomplete {
stage,
reason: error.to_string(),
busy: false,
},
) else {
panic!("a main swapped for a symlink must refuse the guarded open");
};
assert!(
error
.to_string()
.contains("code-map VFS refused the Main open: code-map VFS cannot prove"),
"{error}"
);
}
#[test]
fn guarded_transition_open_does_not_report_an_earlier_refusal() {
if run_in_child() {
return;
}
let dir = fixture();
let target = dir.path().join("code-map.db");
seed_rollback(&target);
let guard = Arc::new(CodeMapHandleGuard::new(target.clone(), Mode::Rollback, vec![]).unwrap());
let name = vfs::register(Arc::clone(&guard)).unwrap();
guard.record_refusal("earlier refusal".into());
let Err(error) = super::transition::open_guarded(
&target,
rusqlite::OpenFlags::empty(),
&name,
"guarded rollback reopen",
|stage, error| super::transition::TransitionError::Partial {
stage,
reason: error.to_string(),
},
) else {
panic!("an open without an access flag must fail");
};
assert!(!error.to_string().contains("earlier refusal"), "{error}");
}