use std::fs;
use std::path::Path;
mod directory;
use crate::RepositoryLayout;
use crate::test_support::unique_temp_dir;
use super::{
MutationRoot, TestFailPoint, append_file_required, ensure_directory_required,
fail_once_for_test, promote_file_required, remove_file_required,
truncate_existing_file_required, write_file_atomically, write_worktree_file_atomically,
};
fn mutation_root(path: &Path) -> MutationRoot {
match MutationRoot::open(path) {
Ok(root) => root,
Err(error) => panic!("test mutation root failed: {error}"),
}
}
#[test]
fn root_capability_remains_bound_after_path_replacement() {
let path = unique_temp_dir("root-replacement");
let replacement = path.with_extension("replacement");
let root = mutation_root(&path);
assert!(fs::rename(&path, &replacement).is_ok());
assert!(fs::create_dir(&path).is_ok());
assert!(write_file_atomically(&root, Path::new("state"), b"bound").is_ok());
assert_eq!(
fs::read(replacement.join("state")).unwrap_or_default(),
b"bound"
);
assert!(!path.join("state").exists());
let _ = fs::remove_dir_all(path);
let _ = fs::remove_dir_all(replacement);
}
#[test]
fn worktree_writer_remains_bound_after_root_replacement() {
let path = unique_temp_dir("worktree-root-replacement");
let replacement = path.with_extension("replacement");
let root = mutation_root(&path);
assert!(fs::rename(&path, &replacement).is_ok());
assert!(fs::create_dir(&path).is_ok());
assert!(write_worktree_file_atomically(&root, Path::new("file"), b"bound").is_ok());
assert_eq!(
fs::read(replacement.join("file")).unwrap_or_default(),
b"bound"
);
assert!(!path.join("file").exists());
let _ = fs::remove_dir_all(path);
let _ = fs::remove_dir_all(replacement);
}
#[test]
fn worktree_write_sync_failure_retains_file_and_is_retryable() {
let path = unique_temp_dir("worktree-write-failure");
let root = mutation_root(&path);
fail_once_for_test(TestFailPoint::MutableParentSync);
assert!(write_worktree_file_atomically(&root, Path::new("file"), b"retained").is_err());
assert_eq!(fs::read(path.join("file")).unwrap_or_default(), b"retained");
assert!(write_worktree_file_atomically(&root, Path::new("file"), b"retry").is_ok());
assert_eq!(fs::read(path.join("file")).unwrap_or_default(), b"retry");
let _ = fs::remove_dir_all(path);
}
#[test]
fn repository_layout_remains_bound_after_prikk_replacement() {
let path = unique_temp_dir("repository-root-replacement");
let layout = match RepositoryLayout::init(path.clone()) {
Ok(layout) => layout,
Err(error) => panic!("repository layout failed: {error}"),
};
let displaced = path.join(".prikk-displaced");
assert!(fs::rename(layout.prikk_dir(), &displaced).is_ok());
assert!(fs::create_dir(layout.prikk_dir()).is_ok());
assert!(
write_file_atomically(
layout.repository_mutation_root(),
Path::new("authority-test"),
b"bound"
)
.is_ok()
);
assert_eq!(
fs::read(displaced.join("authority-test")).unwrap_or_default(),
b"bound"
);
assert!(!layout.prikk_dir().join("authority-test").exists());
let _ = fs::remove_dir_all(path);
}
#[test]
fn required_open_failure_has_no_side_effect_and_is_retryable() {
let path = unique_temp_dir("required-open-failure");
let root = mutation_root(&path);
fail_once_for_test(TestFailPoint::RequiredOpen);
assert!(write_file_atomically(&root, Path::new("state"), b"candidate").is_err());
assert!(!path.join("state").exists());
assert!(write_file_atomically(&root, Path::new("state"), b"retry").is_ok());
let _ = fs::remove_dir_all(path);
}
#[test]
fn mutable_atomic_write_replaces_complete_content() {
let path = unique_temp_dir("required-atomic-write");
let root = mutation_root(&path);
assert!(write_file_atomically(&root, Path::new("state"), b"first").is_ok());
assert!(write_file_atomically(&root, Path::new("state"), b"second").is_ok());
assert_eq!(fs::read(path.join("state")).unwrap_or_default(), b"second");
let _ = fs::remove_dir_all(path);
}
#[test]
fn failed_mutable_parent_sync_retains_replaced_final_name() {
let path = unique_temp_dir("required-mutable-sync-failure");
let root = mutation_root(&path);
fail_once_for_test(TestFailPoint::MutableParentSync);
assert!(write_file_atomically(&root, Path::new("state"), b"retained").is_err());
assert_eq!(
fs::read(path.join("state")).unwrap_or_default(),
b"retained"
);
assert!(write_file_atomically(&root, Path::new("state"), b"retry").is_ok());
assert_eq!(fs::read(path.join("state")).unwrap_or_default(), b"retry");
let _ = fs::remove_dir_all(path);
}
#[test]
fn failed_mutable_file_sync_keeps_only_non_authoritative_temp() {
let path = unique_temp_dir("required-mutable-file-failure");
let root = mutation_root(&path);
fail_once_for_test(TestFailPoint::MutableFileSync);
assert!(write_file_atomically(&root, Path::new("state"), b"candidate").is_err());
assert!(!path.join("state").exists());
let debris = fs::read_dir(&path)
.map(|entries| entries.filter_map(std::result::Result::ok).count())
.unwrap_or_default();
assert_eq!(debris, 1);
let _ = fs::remove_dir_all(path);
}
#[test]
fn failed_mutable_rename_keeps_previous_authoritative_state() {
let path = unique_temp_dir("required-mutable-rename-failure");
let root = mutation_root(&path);
assert!(write_file_atomically(&root, Path::new("state"), b"previous").is_ok());
fail_once_for_test(TestFailPoint::MutableRename);
assert!(write_file_atomically(&root, Path::new("state"), b"candidate").is_err());
assert_eq!(
fs::read(path.join("state")).unwrap_or_default(),
b"previous"
);
let _ = fs::remove_dir_all(path);
}
#[test]
fn failed_append_write_is_retryable() {
let path = unique_temp_dir("append-write-failure");
let root = mutation_root(&path);
fail_once_for_test(TestFailPoint::AppendWrite);
assert!(append_file_required(&root, Path::new("log"), b"record").is_err());
assert_eq!(fs::read(path.join("log")).unwrap_or_default(), b"");
assert!(append_file_required(&root, Path::new("log"), b"record").is_ok());
assert_eq!(fs::read(path.join("log")).unwrap_or_default(), b"record");
let _ = fs::remove_dir_all(path);
}
#[test]
fn failed_truncate_retains_previous_state_and_is_retryable() {
let path = unique_temp_dir("truncate-failure");
let root = mutation_root(&path);
assert!(fs::write(path.join("wal"), b"complete-partial").is_ok());
fail_once_for_test(TestFailPoint::Truncate);
assert!(truncate_existing_file_required(&root, Path::new("wal"), 8).is_err());
assert_eq!(
fs::read(path.join("wal")).unwrap_or_default(),
b"complete-partial"
);
assert!(truncate_existing_file_required(&root, Path::new("wal"), 8).is_ok());
assert_eq!(fs::read(path.join("wal")).unwrap_or_default(), b"complete");
let _ = fs::remove_dir_all(path);
}
#[test]
fn failed_unlink_retains_file_and_cleanup_sync_reports_removed_state() {
let path = unique_temp_dir("unlink-failure");
let root = mutation_root(&path);
assert!(fs::write(path.join("entry"), b"state").is_ok());
fail_once_for_test(TestFailPoint::Unlink);
assert!(remove_file_required(&root, Path::new("entry")).is_err());
assert!(path.join("entry").is_file());
fail_once_for_test(TestFailPoint::CleanupDirectorySync);
assert!(super::remove_worktree_file_required(&root, Path::new("entry")).is_err());
assert!(!path.join("entry").exists());
let _ = fs::remove_dir_all(path);
}
#[cfg(target_os = "linux")]
#[test]
fn append_and_truncate_reject_fifo_without_blocking() {
use std::sync::mpsc;
use std::time::Duration;
use rustix::fs::{CWD, Mode, mkfifoat};
let path = unique_temp_dir("fifo-final-entry");
let fifo = path.join("wal");
assert!(mkfifoat(CWD, &fifo, Mode::from_raw_mode(0o600)).is_ok());
let root = mutation_root(&path);
let (sender, receiver) = mpsc::channel();
std::thread::spawn(move || {
let append = append_file_required(&root, Path::new("wal"), b"record");
let truncate = truncate_existing_file_required(&root, Path::new("wal"), 0);
let _ = sender.send((append, truncate));
});
let result = receiver.recv_timeout(Duration::from_secs(1));
assert!(result.is_ok(), "special-file rejection must be bounded");
if let Ok((append, truncate)) = result {
assert!(append.is_err());
assert!(truncate.is_err());
}
let _ = fs::remove_file(fifo);
let _ = fs::remove_dir_all(path);
}
#[cfg(not(target_os = "linux"))]
#[test]
fn unsupported_mutation_fails_before_filesystem_side_effect() {
let path = unique_temp_dir("unsupported-mutation");
let root = mutation_root(&path);
assert!(write_file_atomically(&root, Path::new("state"), b"candidate").is_err());
assert!(!path.join("state").exists());
let _ = fs::remove_dir_all(path);
}
#[test]
fn promotion_destination_sync_failure_retains_destination_state() {
let path = unique_temp_dir("required-promotion-destination");
let root = mutation_root(&path);
assert!(ensure_directory_required(&root, Path::new("source")).is_ok());
assert!(ensure_directory_required(&root, Path::new("destination")).is_ok());
assert!(write_file_atomically(&root, Path::new("source/candidate"), b"pointer").is_ok());
fail_once_for_test(TestFailPoint::PromotionDestinationSync);
assert!(
promote_file_required(
&root,
Path::new("source/candidate"),
Path::new("destination/pointer")
)
.is_err()
);
assert!(!path.join("source/candidate").exists());
assert_eq!(
fs::read(path.join("destination/pointer")).unwrap_or_default(),
b"pointer"
);
let _ = fs::remove_dir_all(path);
}
#[test]
fn promotion_rename_failure_retains_source_only() {
let path = unique_temp_dir("required-promotion-rename");
let root = mutation_root(&path);
assert!(ensure_directory_required(&root, Path::new("source")).is_ok());
assert!(ensure_directory_required(&root, Path::new("destination")).is_ok());
assert!(write_file_atomically(&root, Path::new("source/candidate"), b"pointer").is_ok());
fail_once_for_test(TestFailPoint::PromotionRename);
assert!(
promote_file_required(
&root,
Path::new("source/candidate"),
Path::new("destination/pointer")
)
.is_err()
);
assert_eq!(
fs::read(path.join("source/candidate")).unwrap_or_default(),
b"pointer"
);
assert!(!path.join("destination/pointer").exists());
let _ = fs::remove_dir_all(path);
}
#[test]
fn promotion_source_sync_failure_reports_committed_destination() {
let path = unique_temp_dir("required-promotion-source");
let root = mutation_root(&path);
assert!(ensure_directory_required(&root, Path::new("source")).is_ok());
assert!(ensure_directory_required(&root, Path::new("destination")).is_ok());
assert!(write_file_atomically(&root, Path::new("source/candidate"), b"pointer").is_ok());
fail_once_for_test(TestFailPoint::PromotionSourceSync);
assert!(
promote_file_required(
&root,
Path::new("source/candidate"),
Path::new("destination/pointer")
)
.is_err()
);
assert!(!path.join("source/candidate").exists());
assert_eq!(
fs::read(path.join("destination/pointer")).unwrap_or_default(),
b"pointer"
);
let _ = fs::remove_dir_all(path);
}