#![allow(clippy::expect_used)]
use super::{LiveNode, NodeContent, NodeLifecycleState, Tombstone};
use prikk_object::{NodeId, NodeKind, ObjectId};
use crate::path::RepoPath;
fn nid(byte: u8) -> NodeId {
NodeId::from_bytes([byte; 32])
}
fn path(value: &str) -> RepoPath {
RepoPath::parse(value).expect("valid repo path")
}
fn oid(byte: u8) -> ObjectId {
ObjectId::from_bytes([byte; 32])
}
fn file_node(p: &str, blob: u8, mode: u32) -> LiveNode {
LiveNode {
path: path(p),
kind: NodeKind::TextFile,
content: NodeContent::File {
blob_id: oid(blob),
mode,
},
}
}
fn symlink_node(p: &str, target: &str) -> LiveNode {
LiveNode {
path: path(p),
kind: NodeKind::Symlink,
content: NodeContent::Symlink {
target: target.to_string(),
},
}
}
#[test]
fn create_makes_node_live_and_indexed_by_path() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(0x11), file_node("a.txt", 0xaa, 0o100_644))
.expect("fresh create");
assert_eq!(state.live_count(), 1);
assert_eq!(state.node_id_at(&path("a.txt")), Some(nid(0x11)));
assert!(state.live_node(&nid(0x11)).is_some());
}
#[test]
fn create_rejects_currently_live_node_id() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(0x11), file_node("a.txt", 0xaa, 0o100_644))
.expect("first create");
let err = state
.create_node(nid(0x11), file_node("b.txt", 0xbb, 0o100_644))
.expect_err("live reuse rejected");
assert!(format!("{err:?}").contains("already live"));
assert_eq!(state.live_count(), 1);
}
#[test]
fn create_rejects_occupied_path() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(0x11), file_node("a.txt", 0xaa, 0o100_644))
.expect("first create");
let err = state
.create_node(nid(0x22), file_node("a.txt", 0xbb, 0o100_644))
.expect_err("occupied path rejected");
assert!(format!("{err:?}").contains("occupied"));
}
#[test]
fn delete_removes_node_from_live_and_path_index() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(0x11), file_node("a.txt", 0xaa, 0o100_644))
.expect("create");
state.delete_node(nid(0x11)).expect("delete");
assert_eq!(state.live_count(), 0);
assert_eq!(state.node_id_at(&path("a.txt")), None);
assert!(state.live_node(&nid(0x11)).is_none());
}
#[test]
fn delete_then_restoration_equivalent_recreate_is_accepted() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(0x11), file_node("a.txt", 0xaa, 0o100_644))
.expect("create");
state.delete_node(nid(0x11)).expect("delete");
state
.create_node(nid(0x11), file_node("a.txt", 0xaa, 0o100_644))
.expect("restoration-equivalent recreate");
assert_eq!(state.live_count(), 1);
assert_eq!(state.node_id_at(&path("a.txt")), Some(nid(0x11)));
}
#[test]
fn reintroduction_rejected_on_blob_mismatch() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(0x11), file_node("a.txt", 0xaa, 0o100_644))
.expect("create");
state.delete_node(nid(0x11)).expect("delete");
let err = state
.create_node(nid(0x11), file_node("a.txt", 0xbb, 0o100_644))
.expect_err("blob mismatch rejected");
assert!(format!("{err:?}").contains("restoration-equivalent"));
}
#[test]
fn reintroduction_rejected_on_mode_mismatch() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(0x11), file_node("a.txt", 0xaa, 0o100_644))
.expect("create");
state.delete_node(nid(0x11)).expect("delete");
let err = state
.create_node(nid(0x11), file_node("a.txt", 0xaa, 0o100_755))
.expect_err("mode mismatch rejected");
assert!(format!("{err:?}").contains("restoration-equivalent"));
}
#[test]
fn reintroduction_rejected_on_path_mismatch() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(0x11), file_node("a.txt", 0xaa, 0o100_644))
.expect("create");
state.delete_node(nid(0x11)).expect("delete");
let err = state
.create_node(nid(0x11), file_node("b.txt", 0xaa, 0o100_644))
.expect_err("path mismatch rejected");
assert!(format!("{err:?}").contains("restoration-equivalent"));
}
#[test]
fn reintroduction_rejected_on_kind_mismatch() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(0x11), file_node("a.txt", 0xaa, 0o100_644))
.expect("create");
state.delete_node(nid(0x11)).expect("delete");
let err = state
.create_node(nid(0x11), symlink_node("a.txt", "t.txt"))
.expect_err("kind mismatch rejected");
assert!(format!("{err:?}").contains("restoration-equivalent"));
}
#[test]
fn symlink_restoration_equivalence_matches_on_target() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(0x11), symlink_node("link", "t.txt"))
.expect("create symlink");
state.delete_node(nid(0x11)).expect("delete");
state
.create_node(nid(0x11), symlink_node("link", "t.txt"))
.expect("restoration-equivalent symlink recreate");
state.delete_node(nid(0x11)).expect("delete again");
let err = state
.create_node(nid(0x11), symlink_node("link", "other.txt"))
.expect_err("target mismatch rejected");
assert!(format!("{err:?}").contains("restoration-equivalent"));
}
#[test]
fn rename_preserves_node_id_and_moves_path() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(0x11), file_node("a.txt", 0xaa, 0o100_644))
.expect("create");
state.rename_node(nid(0x11), path("b.txt")).expect("rename");
assert_eq!(state.node_id_at(&path("a.txt")), None);
assert_eq!(state.node_id_at(&path("b.txt")), Some(nid(0x11)));
let live = state.live_node(&nid(0x11)).expect("still live");
assert_eq!(live.path.as_str(), "b.txt");
}
#[test]
fn rename_rejects_target_occupied_by_other_live_node() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(0x11), file_node("a.txt", 0xaa, 0o100_644))
.expect("create a");
state
.create_node(nid(0x22), file_node("b.txt", 0xbb, 0o100_644))
.expect("create b");
let err = state
.rename_node(nid(0x11), path("b.txt"))
.expect_err("occupied rename target rejected");
assert!(format!("{err:?}").contains("occupied"));
}
#[test]
fn delete_and_rename_reject_non_live_node_id() {
let mut state = NodeLifecycleState::new();
let del_err = state.delete_node(nid(0x99)).expect_err("delete non-live");
assert!(format!("{del_err:?}").contains("not live"));
let ren_err = state
.rename_node(nid(0x99), path("z.txt"))
.expect_err("rename non-live");
assert!(format!("{ren_err:?}").contains("not live"));
}
#[test]
fn create_rejects_file_kind_with_symlink_content() {
let mut state = NodeLifecycleState::new();
let bad = LiveNode {
path: path("a.txt"),
kind: NodeKind::TextFile,
content: NodeContent::Symlink {
target: "t.txt".to_string(),
},
};
let err = state
.create_node(nid(0x11), bad)
.expect_err("kind/content mismatch rejected");
assert!(format!("{err:?}").contains("discriminator mismatch"));
}
#[test]
fn create_rejects_symlink_kind_with_file_content() {
let mut state = NodeLifecycleState::new();
let bad = LiveNode {
path: path("link"),
kind: NodeKind::Symlink,
content: NodeContent::File {
blob_id: oid(0xaa),
mode: 0o100_644,
},
};
let err = state
.create_node(nid(0x11), bad)
.expect_err("kind/content mismatch rejected");
assert!(format!("{err:?}").contains("discriminator mismatch"));
}
#[test]
fn create_accepts_binary_file_kind_with_file_content() {
let mut state = NodeLifecycleState::new();
let node = LiveNode {
path: path("img.bin"),
kind: NodeKind::BinaryFile,
content: NodeContent::File {
blob_id: oid(0xcc),
mode: 0o100_644,
},
};
state
.create_node(nid(0x33), node)
.expect("binary file accepted");
assert_eq!(state.live_count(), 1);
}
#[test]
fn internal_consistency_holds_after_operations() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(0x11), file_node("a.txt", 0xaa, 0o100_644))
.expect("create a");
state
.create_node(nid(0x22), file_node("b.txt", 0xbb, 0o100_644))
.expect("create b");
state.rename_node(nid(0x11), path("c.txt")).expect("rename");
state.delete_node(nid(0x22)).expect("delete b");
state
.validate_internal_consistency()
.expect("bijection holds");
assert_eq!(state.live_count(), 1);
assert_eq!(state.node_id_at(&path("c.txt")), Some(nid(0x11)));
}
fn file_tombstone(p: &str, blob: u8, mode: u32) -> Tombstone {
Tombstone {
path: path(p),
kind: NodeKind::TextFile,
content: NodeContent::File {
blob_id: oid(blob),
mode,
},
}
}
#[test]
fn seed_live_node_builds_live_state() {
let mut state = NodeLifecycleState::new();
state
.seed_live_node(nid(0x11), file_node("a.txt", 0xaa, 0o100_644))
.expect("seed live");
assert_eq!(state.live_count(), 1);
assert_eq!(state.node_id_at(&path("a.txt")), Some(nid(0x11)));
state
.validate_internal_consistency()
.expect("consistent after seed");
}
#[test]
fn seeded_tombstone_accepts_restoration_equivalent_create_across_boundary() {
let mut state = NodeLifecycleState::new();
state
.seed_tombstone(nid(0x11), file_tombstone("a.txt", 0xaa, 0o100_644))
.expect("seed tombstone");
state
.create_node(nid(0x11), file_node("a.txt", 0xaa, 0o100_644))
.expect("restoration-equivalent recreate accepted across boundary");
assert_eq!(state.live_count(), 1);
}
#[test]
fn seeded_tombstone_rejects_non_equivalent_create_across_boundary() {
let mut state = NodeLifecycleState::new();
state
.seed_tombstone(nid(0x11), file_tombstone("a.txt", 0xaa, 0o100_644))
.expect("seed tombstone");
let err = state
.create_node(nid(0x11), file_node("a.txt", 0xbb, 0o100_644))
.expect_err("non-equivalent reuse rejected across boundary");
assert!(format!("{err:?}").contains("restoration-equivalent"));
}
#[test]
fn seed_rejects_all_zero_node_id() {
let mut state = NodeLifecycleState::new();
let live_err = state
.seed_live_node(nid(0x00), file_node("a.txt", 0xaa, 0o100_644))
.expect_err("all-zero live id rejected");
assert!(format!("{live_err:?}").contains("all-zero"));
let tomb_err = state
.seed_tombstone(nid(0x00), file_tombstone("a.txt", 0xaa, 0o100_644))
.expect_err("all-zero tombstone id rejected");
assert!(format!("{tomb_err:?}").contains("all-zero"));
}
#[test]
fn seed_live_node_rejects_duplicate_id_and_path() {
let mut state = NodeLifecycleState::new();
state
.seed_live_node(nid(0x11), file_node("a.txt", 0xaa, 0o100_644))
.expect("first seed");
let dup_id = state
.seed_live_node(nid(0x11), file_node("b.txt", 0xbb, 0o100_644))
.expect_err("duplicate live id rejected");
assert!(format!("{dup_id:?}").contains("duplicate live node_id"));
let dup_path = state
.seed_live_node(nid(0x22), file_node("a.txt", 0xcc, 0o100_644))
.expect_err("duplicate live path rejected");
assert!(format!("{dup_path:?}").contains("duplicate live path"));
}
#[test]
fn seed_tombstone_rejects_currently_live_node() {
let mut state = NodeLifecycleState::new();
state
.seed_live_node(nid(0x11), file_node("a.txt", 0xaa, 0o100_644))
.expect("seed live");
let err = state
.seed_tombstone(nid(0x11), file_tombstone("a.txt", 0xaa, 0o100_644))
.expect_err("tombstone for live node rejected");
assert!(format!("{err:?}").contains("currently-live"));
}
#[test]
fn seed_rejects_kind_content_mismatch() {
let mut state = NodeLifecycleState::new();
let bad = LiveNode {
path: path("a.txt"),
kind: NodeKind::Symlink,
content: NodeContent::File {
blob_id: oid(0xaa),
mode: 0o100_644,
},
};
let err = state
.seed_live_node(nid(0x11), bad)
.expect_err("mismatched seed rejected");
assert!(format!("{err:?}").contains("discriminator mismatch"));
}
#[test]
fn restoration_equivalent_recreate_clears_tombstone() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(0x11), file_node("a.txt", 0xaa, 0o100_644))
.expect("create");
state.delete_node(nid(0x11)).expect("delete");
state
.create_node(nid(0x11), file_node("a.txt", 0xaa, 0o100_644))
.expect("restore");
assert!(state.live_node(&nid(0x11)).is_some());
state
.validate_internal_consistency()
.expect("no live/tombstone overlap after restore");
}
#[test]
fn create_node_rejects_all_zero_node_id() {
let mut state = NodeLifecycleState::new();
let err = state
.create_node(nid(0x00), file_node("a.txt", 0xaa, 0o100_644))
.expect_err("all-zero node_id");
assert!(format!("{err:?}").contains("zero"));
}
#[test]
fn change_file_mode_updates_mode_exactly() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(1), file_node("a.txt", 7, 0o100_644))
.expect("create");
state
.change_file_mode(nid(1), 0o100_644, 0o100_755)
.expect("chmod");
match &state.live_node(&nid(1)).expect("live").content {
NodeContent::File { mode, .. } => assert_eq!(*mode, 0o100_755),
other => panic!("expected file content, got {other:?}"),
}
}
#[test]
fn change_file_mode_rejects_old_mode_mismatch() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(1), file_node("a.txt", 7, 0o100_644))
.expect("create");
assert!(
state
.change_file_mode(nid(1), 0o100_600, 0o100_755)
.is_err()
);
}
#[test]
fn change_file_mode_rejects_symlink() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(2), symlink_node("link", "target"))
.expect("create symlink");
assert!(state.change_file_mode(nid(2), 0, 0o100_755).is_err());
}
#[test]
fn change_file_mode_rejects_dead_node() {
let mut state = NodeLifecycleState::new();
assert!(
state
.change_file_mode(nid(9), 0o100_644, 0o100_755)
.is_err()
);
}
#[test]
fn delete_node_checked_rejects_preimage_mismatch() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(1), file_node("a.txt", 7, 0o100_644))
.expect("create");
let wrong = file_node("a.txt", 7, 0o100_600);
assert!(state.delete_node_checked(nid(1), &wrong).is_err());
assert!(state.live_node(&nid(1)).is_some());
}
#[test]
fn delete_node_checked_accepts_exact_preimage() {
let mut state = NodeLifecycleState::new();
let node = file_node("a.txt", 7, 0o100_644);
state.create_node(nid(1), node.clone()).expect("create");
let deleted = state.delete_node_checked(nid(1), &node).expect("delete");
assert_eq!(deleted, node);
assert!(state.live_node(&nid(1)).is_none());
}
#[test]
fn rename_node_checked_rejects_old_path_mismatch() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(1), file_node("a.txt", 7, 0o100_644))
.expect("create");
assert!(
state
.rename_node_checked(nid(1), &path("wrong.txt"), path("b.txt"))
.is_err()
);
assert_eq!(state.node_id_at(&path("a.txt")), Some(nid(1)));
}
#[test]
fn rename_node_checked_accepts_correct_old_path() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(1), file_node("a.txt", 7, 0o100_644))
.expect("create");
state
.rename_node_checked(nid(1), &path("a.txt"), path("b.txt"))
.expect("rename");
assert_eq!(state.node_id_at(&path("b.txt")), Some(nid(1)));
assert_eq!(state.node_id_at(&path("a.txt")), None);
}
fn binary_file_node(p: &str, blob: u8, mode: u32) -> LiveNode {
LiveNode {
path: path(p),
kind: NodeKind::BinaryFile,
content: NodeContent::File {
blob_id: oid(blob),
mode,
},
}
}
#[test]
fn replace_file_blob_updates_blob_preserving_mode() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(1), binary_file_node("a.bin", 7, 0o100_644))
.expect("create");
state
.replace_file_blob(nid(1), oid(7), oid(8))
.expect("replace");
match &state.live_node(&nid(1)).expect("live").content {
NodeContent::File { blob_id, mode } => {
assert_eq!(*blob_id, oid(8));
assert_eq!(*mode, 0o100_644);
}
other => panic!("expected file, got {other:?}"),
}
}
#[test]
fn replace_file_blob_rejects_old_blob_mismatch() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(1), binary_file_node("a.bin", 7, 0o100_644))
.expect("create");
assert!(state.replace_file_blob(nid(1), oid(99), oid(8)).is_err());
}
#[test]
fn replace_file_blob_rejects_text_file_node() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(1), file_node("a.txt", 7, 0o100_644))
.expect("create");
assert!(state.replace_file_blob(nid(1), oid(7), oid(8)).is_err());
}
#[test]
fn replace_file_blob_rejects_dead_node() {
let mut state = NodeLifecycleState::new();
assert!(state.replace_file_blob(nid(9), oid(7), oid(8)).is_err());
}
#[test]
fn set_text_blob_updates_text_node_blob() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(1), file_node("a.txt", 7, 0o100_644))
.expect("create");
state.set_text_blob(nid(1), oid(8)).expect("set");
match &state.live_node(&nid(1)).expect("live").content {
NodeContent::File { blob_id, mode } => {
assert_eq!(*blob_id, oid(8));
assert_eq!(*mode, 0o100_644);
}
other => panic!("expected file, got {other:?}"),
}
}
#[test]
fn set_text_blob_rejects_binary_node() {
let mut state = NodeLifecycleState::new();
state
.create_node(nid(1), binary_file_node("a.bin", 7, 0o100_644))
.expect("create");
assert!(state.set_text_blob(nid(1), oid(8)).is_err());
}
#[test]
fn set_text_blob_rejects_dead_node() {
let mut state = NodeLifecycleState::new();
assert!(state.set_text_blob(nid(9), oid(8)).is_err());
}