use super::*;
use crate::RevisionId;
#[test]
fn new_matches_python_defaults() {
let state = DirState::new(
"/tmp/.bzr/checkout/dirstate",
Box::new(DefaultSHA1Provider::new()),
0,
true,
false,
);
assert_eq!(state.filename, PathBuf::from("/tmp/.bzr/checkout/dirstate"));
assert_eq!(state.header_state, MemoryState::NotInMemory);
assert_eq!(state.dirblock_state, MemoryState::NotInMemory);
assert!(!state.changes_aborted);
assert!(state.dirblocks.is_empty());
assert!(state.ghosts.is_empty());
assert!(state.parents.is_empty());
assert_eq!(state.end_of_header, None);
assert_eq!(state.cutoff_time, None);
assert_eq!(state.num_entries, 0);
assert_eq!(state.lock_state, None);
assert!(state.known_hash_changes.is_empty());
assert_eq!(state.worth_saving_limit, 0);
assert!(!state.fdatasync);
assert!(state.use_filesystem_for_exec);
assert_eq!(state.bisect_page_size, BISECT_PAGE_SIZE);
assert!(state.id_index.is_none());
}
#[test]
fn new_honours_overrides() {
let state = DirState::new(
"dirstate",
Box::new(DefaultSHA1Provider::new()),
-1,
false,
true,
);
assert_eq!(state.worth_saving_limit, -1);
assert!(!state.use_filesystem_for_exec);
assert!(state.fdatasync);
}
fn make_header_bytes(parents: &[&[u8]], ghosts: &[&[u8]]) -> Vec<u8> {
let parents_line = get_parents_line(parents);
let ghosts_line = get_ghosts_line(ghosts);
let lines: Vec<&[u8]> = vec![parents_line.as_slice(), ghosts_line.as_slice()];
let chunks = get_output_lines(lines);
chunks.into_iter().flatten().collect()
}
#[test]
fn read_header_no_parents_no_ghosts() {
let bytes = make_header_bytes(&[], &[]);
let header = read_header(&bytes).expect("parse header");
assert_eq!(header.num_entries, 0);
assert!(header.parents.is_empty());
assert!(header.ghosts.is_empty());
}
#[test]
fn load_bytes_reads_an_initialised_dirstate() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("dirstate");
{
let mut transport = FileTransport::new(&path);
let mut state =
DirState::initialize(&mut transport, path.clone(), Box::new(DefaultSHA1Provider))
.expect("initialize");
state.save_to(&mut transport).unwrap();
}
let data = std::fs::read(&path).unwrap();
let mut loaded = DirState::new(&path, Box::new(DefaultSHA1Provider), 0, true, false);
loaded.load_bytes(&data).expect("load_bytes");
assert!(loaded.parents.is_empty());
let roots: Vec<_> = loaded
.iter_entries()
.filter(|e| e.key.dirname.is_empty() && e.key.basename.is_empty())
.collect();
assert_eq!(roots.len(), 1);
assert_eq!(roots[0].key.file_id, crate::inventory::ROOT_ID.to_vec());
}
#[test]
fn read_header_with_parents_and_ghosts() {
let bytes = make_header_bytes(&[b"rev-a", b"rev-b"], &[b"ghost-1"]);
let header = read_header(&bytes).expect("parse header");
assert_eq!(header.parents, vec![b"rev-a".to_vec(), b"rev-b".to_vec()]);
assert_eq!(header.ghosts, vec![b"ghost-1".to_vec()]);
}
#[test]
fn read_header_matches_python_generated_bytes() {
let bytes: &[u8] = b"#bazaar dirstate flat format 3\n\
crc32: 2265437010\n\
num_entries: 0\n\
2\x00rev-a\x00rev-b\x00\n\
\x001\x00ghost-1\x00\n\x00";
let header = read_header(bytes).expect("parse header");
assert_eq!(header.crc_expected, 2265437010);
assert_eq!(header.num_entries, 0);
assert_eq!(header.parents, vec![b"rev-a".to_vec(), b"rev-b".to_vec()]);
assert_eq!(header.ghosts, vec![b"ghost-1".to_vec()]);
}
#[test]
fn read_header_populates_struct_fields() {
let bytes = make_header_bytes(&[b"rev-a"], &[]);
let mut state = DirState::new(
"dirstate",
Box::new(DefaultSHA1Provider::new()),
0,
true,
false,
);
state.read_header(&bytes).expect("parse header");
assert_eq!(state.header_state, MemoryState::InMemoryUnmodified);
assert_eq!(state.parents, vec![b"rev-a".to_vec()]);
assert!(state.ghosts.is_empty());
assert_eq!(state.num_entries, 0);
assert!(state.end_of_header.is_some());
}
#[test]
fn read_header_rejects_wrong_format_line() {
let bytes = b"#bazaar dirstate flat format 2\ncrc32: 0\nnum_entries: 0\n0\n\x000\n";
match read_header(bytes) {
Err(HeaderError::BadFormatLine(line)) => {
assert_eq!(line, HEADER_FORMAT_2.to_vec());
}
other => panic!("expected BadFormatLine, got {:?}", other),
}
}
#[test]
fn read_header_rejects_missing_crc() {
let mut bytes = Vec::new();
bytes.extend_from_slice(HEADER_FORMAT_3);
bytes.extend_from_slice(b"not-a-crc-line\n");
assert!(matches!(
read_header(&bytes),
Err(HeaderError::MissingCrcLine(_))
));
}
#[test]
fn read_header_rejects_bad_num_entries() {
let mut bytes = Vec::new();
bytes.extend_from_slice(HEADER_FORMAT_3);
bytes.extend_from_slice(b"crc32: 0\n");
bytes.extend_from_slice(b"num_entries: abc\n");
bytes.extend_from_slice(b"0\n\x000\n");
assert!(matches!(
read_header(&bytes),
Err(HeaderError::BadNumEntries(_))
));
}
fn entry_line(
dirname: &[u8],
basename: &[u8],
file_id: &[u8],
trees: &[(&[u8], &[u8], u64, bool, &[u8])],
) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(dirname);
out.push(0);
out.extend_from_slice(basename);
out.push(0);
out.extend_from_slice(file_id);
for (minikind, fingerprint, size, executable, info) in trees {
out.push(0);
out.extend_from_slice(minikind);
out.push(0);
out.extend_from_slice(fingerprint);
out.push(0);
out.extend_from_slice(format!("{}", size).as_bytes());
out.push(0);
out.push(if *executable { b'y' } else { b'n' });
out.push(0);
out.extend_from_slice(info);
}
out
}
fn make_body_bytes(parents: &[&[u8]], ghosts: &[&[u8]], entries: &[Vec<u8>]) -> Vec<u8> {
let parents_line = get_parents_line(parents);
let ghosts_line = get_ghosts_line(ghosts);
let mut lines: Vec<&[u8]> = vec![parents_line.as_slice(), ghosts_line.as_slice()];
for e in entries {
lines.push(e.as_slice());
}
let chunks = get_output_lines(lines);
let data: Vec<u8> = chunks.into_iter().flatten().collect();
let header = read_header(&data).expect("header parses");
data[header.end_of_header..].to_vec()
}
#[test]
fn parse_dirblocks_empty_body() {
let blocks = parse_dirblocks(&[], 1, 0).expect("empty body parses");
assert!(blocks.is_empty());
}
#[test]
fn parse_dirblocks_single_root_entry_one_tree() {
let nullstat = b"x".repeat(32);
let entry = entry_line(
b"",
b"",
b"TREE_ROOT",
&[(b"d", b"", 0, false, nullstat.as_slice())],
);
let body = make_body_bytes(&[], &[], &[entry]);
let blocks = parse_dirblocks(&body, 1, 1).expect("parse dirblocks");
assert_eq!(blocks.len(), 2, "expected two sentinel blocks");
assert_eq!(blocks[0].dirname, b"".to_vec());
assert_eq!(blocks[1].dirname, b"".to_vec());
assert_eq!(blocks[0].entries.len(), 1);
let entry = &blocks[0].entries[0];
assert_eq!(entry.key.dirname, b"".to_vec());
assert_eq!(entry.key.basename, b"".to_vec());
assert_eq!(entry.key.file_id, b"TREE_ROOT".to_vec());
assert_eq!(entry.trees.len(), 1);
let tree = &entry.trees[0];
assert_eq!(tree.minikind, Kind::Directory);
assert_eq!(tree.fingerprint, Vec::<u8>::new());
assert_eq!(tree.size, 0);
assert!(!tree.executable);
assert_eq!(tree.packed_stat, nullstat);
}
#[test]
fn parse_dirblocks_multiple_dirs_group_by_dirname() {
let nullstat = b"x".repeat(32);
let entries = vec![
entry_line(
b"",
b"",
b"TREE_ROOT",
&[(b"d", b"", 0, false, nullstat.as_slice())],
),
entry_line(
b"a",
b"file-a",
b"fid-a",
&[(b"f", b"sha-a", 5, true, nullstat.as_slice())],
),
entry_line(
b"b",
b"file-b",
b"fid-b",
&[(b"f", b"sha-b", 7, false, nullstat.as_slice())],
),
];
let body = make_body_bytes(&[], &[], &entries);
let blocks = parse_dirblocks(&body, 1, 3).expect("parse dirblocks");
assert_eq!(blocks.len(), 4);
assert_eq!(blocks[0].dirname, b"".to_vec());
assert_eq!(blocks[0].entries.len(), 1);
assert_eq!(blocks[1].dirname, b"".to_vec());
assert_eq!(blocks[1].entries.len(), 0);
assert_eq!(blocks[2].dirname, b"a".to_vec());
assert_eq!(blocks[2].entries.len(), 1);
assert_eq!(blocks[2].entries[0].key.basename, b"file-a".to_vec());
assert!(blocks[2].entries[0].trees[0].executable);
assert_eq!(blocks[2].entries[0].trees[0].size, 5);
assert_eq!(blocks[3].dirname, b"b".to_vec());
assert_eq!(blocks[3].entries.len(), 1);
assert_eq!(blocks[3].entries[0].trees[0].size, 7);
assert!(!blocks[3].entries[0].trees[0].executable);
}
#[test]
fn parse_dirblocks_rejects_wrong_entry_count() {
let nullstat = b"x".repeat(32);
let entry = entry_line(
b"",
b"",
b"TREE_ROOT",
&[(b"d", b"", 0, false, nullstat.as_slice())],
);
let body = make_body_bytes(&[], &[], &[entry]);
match parse_dirblocks(&body, 1, 2) {
Err(DirblocksError::WrongEntryCount {
expected: 2,
actual: 1,
}) => {}
other => panic!("expected WrongEntryCount, got {:?}", other),
}
}
#[test]
fn parse_dirblocks_multi_tree() {
let nullstat = b"x".repeat(32);
let entry = entry_line(
b"",
b"README",
b"file-id-1",
&[
(b"f", b"sha-current", 10, true, nullstat.as_slice()),
(b"f", b"sha-parent", 8, false, nullstat.as_slice()),
],
);
let body = make_body_bytes(&[b"rev-a"], &[], &[entry]);
let blocks = parse_dirblocks(&body, 2, 1).expect("parse");
assert_eq!(blocks.len(), 2);
let e = &blocks[0].entries[0];
assert_eq!(e.trees.len(), 2);
assert_eq!(e.trees[0].fingerprint, b"sha-current".to_vec());
assert_eq!(e.trees[0].size, 10);
assert!(e.trees[0].executable);
assert_eq!(e.trees[1].fingerprint, b"sha-parent".to_vec());
assert_eq!(e.trees[1].size, 8);
assert!(!e.trees[1].executable);
}
#[test]
fn parse_dirblocks_matches_python_saved_file() {
let bytes: &[u8] = b"#bazaar dirstate flat format 3\n\
crc32: 2823629280\n\
num_entries: 1\n\
0\x00\n\
\x000\x00\n\
\x00\x00\x00TREE_ROOT\x00d\x00\x000\x00n\x00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\x00\n\x00";
let header = read_header(bytes).expect("parse header");
assert_eq!(header.num_entries, 1);
assert!(header.parents.is_empty());
assert!(header.ghosts.is_empty());
let body = &bytes[header.end_of_header..];
let blocks = parse_dirblocks(body, 1, header.num_entries).expect("parse body");
assert_eq!(blocks.len(), 2);
assert_eq!(blocks[0].entries.len(), 1);
let entry = &blocks[0].entries[0];
assert_eq!(entry.key.file_id, b"TREE_ROOT".to_vec());
assert_eq!(entry.trees[0].minikind, Kind::Directory);
assert_eq!(entry.trees[0].packed_stat, b"x".repeat(32));
}
fn make_entry(dirname: &[u8], basename: &[u8], file_id: &[u8]) -> Entry {
Entry {
key: EntryKey {
dirname: dirname.to_vec(),
basename: basename.to_vec(),
file_id: file_id.to_vec(),
},
trees: vec![TreeData {
minikind: Kind::File,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: b"x".repeat(32),
}],
}
}
#[test]
fn split_root_dirblock_separates_root_from_contents() {
let mut dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![
make_entry(b"", b"", b"TREE_ROOT"),
make_entry(b"", b"README", b"fid-readme"),
make_entry(b"", b"CONTRIBUTING", b"fid-contrib"),
],
},
Dirblock {
dirname: Vec::new(),
entries: Vec::new(),
},
];
split_root_dirblock_into_contents(&mut dirblocks).expect("split");
assert_eq!(dirblocks.len(), 2);
assert_eq!(dirblocks[0].entries.len(), 1);
assert_eq!(dirblocks[0].entries[0].key.file_id, b"TREE_ROOT".to_vec());
assert_eq!(dirblocks[1].entries.len(), 2);
assert_eq!(dirblocks[1].entries[0].key.basename, b"README".to_vec());
assert_eq!(
dirblocks[1].entries[1].key.basename,
b"CONTRIBUTING".to_vec()
);
}
#[test]
fn split_root_dirblock_preserves_order_within_partitions() {
let mut dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![
make_entry(b"", b"a", b"fid-a"),
make_entry(b"", b"", b"TREE_ROOT"),
make_entry(b"", b"b", b"fid-b"),
],
},
Dirblock {
dirname: Vec::new(),
entries: Vec::new(),
},
];
split_root_dirblock_into_contents(&mut dirblocks).expect("split");
assert_eq!(dirblocks[0].entries.len(), 1);
assert_eq!(dirblocks[0].entries[0].key.file_id, b"TREE_ROOT".to_vec());
assert_eq!(dirblocks[1].entries.len(), 2);
assert_eq!(dirblocks[1].entries[0].key.basename, b"a".to_vec());
assert_eq!(dirblocks[1].entries[1].key.basename, b"b".to_vec());
}
#[test]
fn split_root_dirblock_leaves_later_blocks_alone() {
let mut dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![make_entry(b"", b"", b"TREE_ROOT")],
},
Dirblock {
dirname: Vec::new(),
entries: Vec::new(),
},
Dirblock {
dirname: b"subdir".to_vec(),
entries: vec![make_entry(b"subdir", b"file", b"fid-s")],
},
];
split_root_dirblock_into_contents(&mut dirblocks).expect("split");
assert_eq!(dirblocks.len(), 3);
assert_eq!(dirblocks[2].dirname, b"subdir".to_vec());
assert_eq!(dirblocks[2].entries.len(), 1);
}
#[test]
fn split_root_dirblock_rejects_missing_sentinel() {
let mut dirblocks = vec![Dirblock {
dirname: Vec::new(),
entries: Vec::new(),
}];
assert_eq!(
split_root_dirblock_into_contents(&mut dirblocks),
Err(SplitRootError::MissingSentinels)
);
}
#[test]
fn split_root_dirblock_rejects_polluted_sentinel() {
let mut dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![make_entry(b"", b"", b"TREE_ROOT")],
},
Dirblock {
dirname: Vec::new(),
entries: vec![make_entry(b"", b"x", b"fid-x")],
},
];
match split_root_dirblock_into_contents(&mut dirblocks) {
Err(SplitRootError::BadSecondSentinel {
dirname,
entry_count,
}) => {
assert!(dirname.is_empty());
assert_eq!(entry_count, 1);
}
other => panic!("expected BadSecondSentinel, got {:?}", other),
}
}
fn dirblock_with_entries(dirname: &[u8], entries: Vec<Entry>) -> Dirblock {
Dirblock {
dirname: dirname.to_vec(),
entries,
}
}
fn make_dirblocks(subdirs: Vec<(&[u8], Vec<Entry>)>) -> Vec<Dirblock> {
let mut blocks = vec![
dirblock_with_entries(b"", Vec::new()),
dirblock_with_entries(b"", Vec::new()),
];
for (dirname, entries) in subdirs {
blocks.push(dirblock_with_entries(dirname, entries));
}
blocks
}
fn tree(minikind: Kind) -> TreeData {
TreeData {
minikind,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: b"x".repeat(32),
}
}
fn entry_with_trees(
dirname: &[u8],
basename: &[u8],
file_id: &[u8],
trees: Vec<TreeData>,
) -> Entry {
Entry {
key: EntryKey {
dirname: dirname.to_vec(),
basename: basename.to_vec(),
file_id: file_id.to_vec(),
},
trees,
}
}
#[test]
fn bisect_dirblock_component_order_not_byte_order() {
let blocks = make_dirblocks(vec![
(b"a", vec![]),
(b"a/b", vec![]),
(b"a-b", vec![]),
(b"b", vec![]),
]);
assert_eq!(bisect_dirblock(&blocks, b"a", 1, blocks.len()), 2);
assert_eq!(bisect_dirblock(&blocks, b"a/b", 1, blocks.len()), 3);
assert_eq!(bisect_dirblock(&blocks, b"a-b", 1, blocks.len()), 4);
assert_eq!(bisect_dirblock(&blocks, b"b", 1, blocks.len()), 5);
assert_eq!(bisect_dirblock(&blocks, b"aa", 1, blocks.len()), 5);
}
fn assert_bisect_matches_bisect_left(paths: &[&[u8]]) {
let split: Vec<Vec<&[u8]>> = paths.iter().map(|p| split_dirname(p)).collect();
let mut sorted = split.clone();
sorted.sort();
assert_eq!(split, sorted, "test input paths are not sorted");
let blocks: Vec<Dirblock> = paths
.iter()
.map(|p| Dirblock {
dirname: p.to_vec(),
entries: Vec::new(),
})
.collect();
for probe in paths {
let got = bisect_dirblock(&blocks, probe, 0, blocks.len());
let probe_split = split_dirname(probe);
let expected = split.partition_point(|s| *s < probe_split);
assert_eq!(
got, expected,
"bisect_dirblock disagreed for {:?}: got {}, expected {}",
probe, got, expected,
);
}
}
#[test]
fn bisect_dirblock_simple() {
let paths: Vec<&[u8]> = vec![b"", b"a", b"b", b"c", b"d"];
assert_bisect_matches_bisect_left(&paths);
}
#[test]
fn bisect_dirblock_involved() {
let paths: Vec<&[u8]> = vec![
b"", b"a", b"a/a", b"a/a/a", b"a/a/z", b"a/a-a", b"a/a-z", b"a/z", b"a/z/a", b"a/z/z",
b"a/z-a", b"a/z-z", b"a-a", b"a-z", b"z", b"z/a/a", b"z/a/z", b"z/a-a", b"z/a-z", b"z/z",
b"z/z/a", b"z/z/z", b"z/z-a", b"z/z-z", b"z-a", b"z-z",
];
assert_bisect_matches_bisect_left(&paths);
}
#[test]
fn find_block_index_from_key_root_fast_path() {
let blocks = make_dirblocks(vec![(b"sub", vec![])]);
let key = EntryKey {
dirname: b"".to_vec(),
basename: b"".to_vec(),
file_id: b"TREE_ROOT".to_vec(),
};
assert_eq!(find_block_index_from_key(&blocks, &key), (0, true));
}
#[test]
fn find_block_index_from_key_hit_and_miss() {
let blocks = make_dirblocks(vec![(b"a", vec![]), (b"c", vec![])]);
let hit = EntryKey {
dirname: b"a".to_vec(),
basename: b"foo".to_vec(),
file_id: b"".to_vec(),
};
assert_eq!(find_block_index_from_key(&blocks, &hit), (2, true));
let miss = EntryKey {
dirname: b"b".to_vec(),
basename: b"foo".to_vec(),
file_id: b"".to_vec(),
};
assert_eq!(find_block_index_from_key(&blocks, &miss), (3, false));
}
#[test]
fn find_entry_index_exact_and_insertion() {
let block = vec![
entry_with_trees(b"dir", b"a", b"fid-a", vec![tree(Kind::File)]),
entry_with_trees(b"dir", b"b", b"fid-b", vec![tree(Kind::File)]),
entry_with_trees(b"dir", b"c", b"fid-c", vec![tree(Kind::File)]),
];
let hit = EntryKey {
dirname: b"dir".to_vec(),
basename: b"b".to_vec(),
file_id: b"fid-b".to_vec(),
};
assert_eq!(find_entry_index(&hit, &block), (1, true));
let miss_before = EntryKey {
dirname: b"dir".to_vec(),
basename: b"ab".to_vec(),
file_id: b"".to_vec(),
};
assert_eq!(find_entry_index(&miss_before, &block), (1, false));
let miss_end = EntryKey {
dirname: b"dir".to_vec(),
basename: b"z".to_vec(),
file_id: b"".to_vec(),
};
assert_eq!(find_entry_index(&miss_end, &block), (3, false));
}
#[test]
fn get_block_entry_index_finds_live_entry() {
let blocks = make_dirblocks(vec![(
b"dir",
vec![entry_with_trees(
b"dir",
b"a",
b"fid-a",
vec![tree(Kind::File)],
)],
)]);
let bei = get_block_entry_index(&blocks, b"dir", b"a", 0);
assert_eq!(bei.block_index, 2);
assert_eq!(bei.entry_index, 0);
assert!(bei.dir_present);
assert!(bei.path_present);
}
#[test]
fn get_block_entry_index_absent_dir() {
let blocks = make_dirblocks(vec![(b"a", vec![])]);
let bei = get_block_entry_index(&blocks, b"missing", b"file", 0);
assert!(!bei.dir_present);
assert!(!bei.path_present);
}
#[test]
fn get_block_entry_index_skips_absent_and_relocated() {
let blocks = make_dirblocks(vec![(
b"dir",
vec![
entry_with_trees(b"dir", b"a", b"fid-absent", vec![tree(Kind::Absent)]),
entry_with_trees(b"dir", b"a", b"fid-live", vec![tree(Kind::File)]),
],
)]);
let bei = get_block_entry_index(&blocks, b"dir", b"a", 0);
assert!(bei.path_present);
assert_eq!(bei.entry_index, 1);
assert_eq!(
blocks[bei.block_index].entries[bei.entry_index].key.file_id,
b"fid-live".to_vec()
);
}
#[test]
fn get_block_entry_index_all_absent_returns_not_present() {
let blocks = make_dirblocks(vec![(
b"dir",
vec![
entry_with_trees(b"dir", b"a", b"fid-1", vec![tree(Kind::Absent)]),
entry_with_trees(b"dir", b"a", b"fid-2", vec![tree(Kind::Relocated)]),
],
)]);
let bei = get_block_entry_index(&blocks, b"dir", b"a", 0);
assert!(bei.dir_present);
assert!(!bei.path_present);
assert_eq!(bei.entry_index, 2);
}
const PACKED_STAT: &[u8] = b"AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk";
const NULL_SHA: &[u8] = b"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
fn stat_tree(minikind: Kind) -> TreeData {
TreeData {
minikind,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: PACKED_STAT.to_vec(),
}
}
fn file_tree(size: u64) -> TreeData {
TreeData {
minikind: Kind::File,
fingerprint: NULL_SHA.to_vec(),
size,
executable: false,
packed_stat: PACKED_STAT.to_vec(),
}
}
fn create_dirstate_with_root_and_subdir() -> DirState {
let mut state = fresh_state();
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"",
b"a-root-value",
vec![stat_tree(Kind::Directory)],
)],
},
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"subdir",
b"subdir-id",
vec![stat_tree(Kind::Directory)],
)],
},
];
state
}
fn create_complex_dirstate() -> DirState {
let mut state = fresh_state();
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"",
b"a-root-value",
vec![stat_tree(Kind::Directory)],
)],
},
Dirblock {
dirname: Vec::new(),
entries: vec![
entry_with_trees(b"", b"a", b"a-dir", vec![stat_tree(Kind::Directory)]),
entry_with_trees(b"", b"b", b"b-dir", vec![stat_tree(Kind::Directory)]),
entry_with_trees(b"", b"c", b"c-file", vec![file_tree(10)]),
entry_with_trees(b"", b"d", b"d-file", vec![file_tree(20)]),
],
},
Dirblock {
dirname: b"a".to_vec(),
entries: vec![
entry_with_trees(b"a", b"e", b"e-dir", vec![stat_tree(Kind::Directory)]),
entry_with_trees(b"a", b"f", b"f-file", vec![file_tree(30)]),
],
},
Dirblock {
dirname: b"b".to_vec(),
entries: vec![
entry_with_trees(b"b", b"g", b"g-file", vec![file_tree(30)]),
entry_with_trees(b"b", b"h\xc3\xa5", b"h-\xc3\xa5-file", vec![file_tree(40)]),
],
},
];
state
}
fn create_dirstate_with_two_trees() -> DirState {
let mut state = fresh_state();
state.parents = vec![b"parent".to_vec()];
let stat_current = TreeData {
minikind: Kind::Directory,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: PACKED_STAT.to_vec(),
};
let stat_parent_dir = TreeData {
minikind: Kind::Directory,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: b"parent-revid".to_vec(),
};
let null_parent = TreeData {
minikind: Kind::Absent,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: Vec::new(),
};
let file_cur = |size: u64| TreeData {
minikind: Kind::File,
fingerprint: NULL_SHA.to_vec(),
size,
executable: false,
packed_stat: PACKED_STAT.to_vec(),
};
let file_parent = |fingerprint: &[u8], size: u64| TreeData {
minikind: Kind::File,
fingerprint: fingerprint.to_vec(),
size,
executable: false,
packed_stat: b"parent-revid".to_vec(),
};
let relocated = |to: &[u8]| TreeData {
minikind: Kind::Relocated,
fingerprint: to.to_vec(),
size: 0,
executable: false,
packed_stat: Vec::new(),
};
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![Entry {
key: EntryKey {
dirname: b"".to_vec(),
basename: b"".to_vec(),
file_id: b"a-root-value".to_vec(),
},
trees: vec![stat_current.clone(), stat_parent_dir.clone()],
}],
},
Dirblock {
dirname: Vec::new(),
entries: vec![
Entry {
key: EntryKey {
dirname: b"".to_vec(),
basename: b"a".to_vec(),
file_id: b"a-dir".to_vec(),
},
trees: vec![stat_current.clone(), stat_parent_dir.clone()],
},
Entry {
key: EntryKey {
dirname: b"".to_vec(),
basename: b"b".to_vec(),
file_id: b"b-dir".to_vec(),
},
trees: vec![stat_current.clone(), stat_parent_dir.clone()],
},
Entry {
key: EntryKey {
dirname: b"".to_vec(),
basename: b"c".to_vec(),
file_id: b"c-file".to_vec(),
},
trees: vec![file_cur(10), relocated(b"b/j")],
},
Entry {
key: EntryKey {
dirname: b"".to_vec(),
basename: b"d".to_vec(),
file_id: b"d-file".to_vec(),
},
trees: vec![file_cur(20), file_parent(b"d", 20)],
},
],
},
Dirblock {
dirname: b"a".to_vec(),
entries: vec![
Entry {
key: EntryKey {
dirname: b"a".to_vec(),
basename: b"e".to_vec(),
file_id: b"e-dir".to_vec(),
},
trees: vec![stat_current.clone(), stat_parent_dir.clone()],
},
Entry {
key: EntryKey {
dirname: b"a".to_vec(),
basename: b"f".to_vec(),
file_id: b"f-file".to_vec(),
},
trees: vec![file_cur(30), file_parent(b"f", 20)],
},
],
},
Dirblock {
dirname: b"b".to_vec(),
entries: vec![
Entry {
key: EntryKey {
dirname: b"b".to_vec(),
basename: b"g".to_vec(),
file_id: b"g-file".to_vec(),
},
trees: vec![file_cur(30), null_parent.clone()],
},
Entry {
key: EntryKey {
dirname: b"b".to_vec(),
basename: b"h\xc3\xa5".to_vec(),
file_id: b"h-\xc3\xa5-file1".to_vec(),
},
trees: vec![file_cur(40), null_parent.clone()],
},
Entry {
key: EntryKey {
dirname: b"b".to_vec(),
basename: b"h\xc3\xa5".to_vec(),
file_id: b"h-\xc3\xa5-file2".to_vec(),
},
trees: vec![null_parent.clone(), file_parent(b"h", 20)],
},
Entry {
key: EntryKey {
dirname: b"b".to_vec(),
basename: b"i".to_vec(),
file_id: b"i-file".to_vec(),
},
trees: vec![null_parent.clone(), file_parent(b"h", 20)],
},
Entry {
key: EntryKey {
dirname: b"b".to_vec(),
basename: b"j".to_vec(),
file_id: b"c-file".to_vec(),
},
trees: vec![relocated(b"c"), file_parent(b"j", 20)],
},
],
},
];
state
}
#[test]
fn iter_child_entries_children_b_tree_one() {
let state = create_dirstate_with_two_trees();
let children: Vec<_> = state.iter_child_entries(1, b"b").collect();
let basenames: Vec<&[u8]> = children.iter().map(|e| e.key.basename.as_slice()).collect();
let file_ids: Vec<&[u8]> = children.iter().map(|e| e.key.file_id.as_slice()).collect();
assert_eq!(basenames, vec![&b"h\xc3\xa5"[..], b"i", b"j"]);
assert_eq!(
file_ids,
vec![&b"h-\xc3\xa5-file2"[..], b"i-file", b"c-file"]
);
}
#[test]
fn iter_child_entries_root_tree_one() {
let state = create_dirstate_with_two_trees();
let basenames: Vec<&[u8]> = state
.iter_child_entries(1, b"")
.map(|e| e.key.basename.as_slice())
.collect();
let expected: Vec<&[u8]> = vec![b"a", b"b", b"d", b"e", b"f", b"h\xc3\xa5", b"i", b"j"];
assert_eq!(basenames, expected);
}
#[test]
fn iter_child_entries_non_directory_returns_empty() {
let state = create_complex_dirstate();
let mut children = state.iter_child_entries(0, b"c");
assert!(children.next().is_none());
}
#[test]
fn split_path_utf8_matches_osutils_split() {
assert_eq!(split_path_utf8(b"a/b/c"), (&b"a/b"[..], &b"c"[..]));
assert_eq!(split_path_utf8(b"a"), (&b""[..], &b"a"[..]));
assert_eq!(split_path_utf8(b""), (&b""[..], &b""[..]));
assert_eq!(split_path_utf8(b"a/"), (&b"a"[..], &b""[..]));
}
#[test]
fn maybe_remove_row_keeps_row_with_any_live_tree() {
let mut entries = vec![
entry_with_trees(
b"",
b"a",
b"fid-a",
vec![tree(Kind::File), tree(Kind::Absent)],
),
entry_with_trees(
b"",
b"b",
b"fid-b",
vec![tree(Kind::Absent), tree(Kind::Absent)],
),
];
let mut id_index = IdIndex::new();
let fid_a = FileId::from(&b"fid-a".to_vec());
id_index.add((b"", b"a", &fid_a));
let removed = DirState::maybe_remove_row(&mut entries, 0, &mut id_index);
assert!(!removed);
assert_eq!(entries.len(), 2);
assert_eq!(id_index.get(&fid_a).len(), 1);
}
#[test]
fn maybe_remove_row_drops_row_when_all_trees_dead() {
let mut entries = vec![
entry_with_trees(b"", b"a", b"fid-a", vec![tree(Kind::File)]),
entry_with_trees(
b"",
b"b",
b"fid-b",
vec![tree(Kind::Absent), tree(Kind::Relocated)],
),
];
let mut id_index = IdIndex::new();
let fid_a = FileId::from(&b"fid-a".to_vec());
let fid_b = FileId::from(&b"fid-b".to_vec());
id_index.add((b"", b"a", &fid_a));
id_index.add((b"", b"b", &fid_b));
let removed = DirState::maybe_remove_row(&mut entries, 1, &mut id_index);
assert!(removed);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].key.file_id, b"fid-a".to_vec());
assert!(id_index.get(&fid_b).is_empty());
assert_eq!(id_index.get(&fid_a).len(), 1);
}
#[test]
fn sort_entries_orders_by_dirname_basename_file_id() {
let mut entries = vec![
make_entry(b"a", b"e", b"fid-e"),
make_entry(b"", b"", b"TREE_ROOT"),
make_entry(b"b", b"g", b"fid-g"),
make_entry(b"", b"a", b"fid-a"),
make_entry(b"a", b"f", b"fid-f"),
];
DirState::sort_entries(&mut entries);
let keys: Vec<(&[u8], &[u8], &[u8])> = entries
.iter()
.map(|e| {
(
e.key.dirname.as_slice(),
e.key.basename.as_slice(),
e.key.file_id.as_slice(),
)
})
.collect();
assert_eq!(
keys,
vec![
(b"".as_slice(), b"".as_slice(), b"TREE_ROOT".as_slice()),
(b"".as_slice(), b"a".as_slice(), b"fid-a".as_slice()),
(b"a".as_slice(), b"e".as_slice(), b"fid-e".as_slice()),
(b"a".as_slice(), b"f".as_slice(), b"fid-f".as_slice()),
(b"b".as_slice(), b"g".as_slice(), b"fid-g".as_slice()),
]
);
}
#[test]
fn sort_entries_breaks_basename_ties_on_file_id() {
let mut entries = vec![
make_entry(b"a", b"e", b"fid-z"),
make_entry(b"a", b"e", b"fid-a"),
];
DirState::sort_entries(&mut entries);
assert_eq!(entries[0].key.file_id, b"fid-a".to_vec());
assert_eq!(entries[1].key.file_id, b"fid-z".to_vec());
}
#[test]
fn sort_entries_split_ordering_differs_from_raw_bytes() {
let mut entries = vec![
make_entry(b"a-b", b"x", b"fid-x"),
make_entry(b"a/c", b"y", b"fid-y"),
make_entry(b"a", b"z", b"fid-z"),
];
DirState::sort_entries(&mut entries);
let dirnames: Vec<&[u8]> = entries.iter().map(|e| e.key.dirname.as_slice()).collect();
assert_eq!(
dirnames,
vec![b"a".as_slice(), b"a/c".as_slice(), b"a-b".as_slice()]
);
}
#[test]
fn entries_for_path_returns_all_rows_at_path() {
let state = create_complex_dirstate();
let rows = state.entries_for_path(b"a/e");
assert!(!rows.is_empty());
for row in &rows {
assert_eq!(row.key.dirname, b"a".to_vec());
assert_eq!(row.key.basename, b"e".to_vec());
}
assert!(rows.iter().any(|r| r.key.file_id == b"e-dir".to_vec()));
}
#[test]
fn entries_for_path_root_returns_root_row() {
let state = create_complex_dirstate();
let rows = state.entries_for_path(b"");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].key.dirname, b"".to_vec());
assert_eq!(rows[0].key.basename, b"".to_vec());
assert_eq!(rows[0].key.file_id, b"a-root-value".to_vec());
}
#[test]
fn entries_for_path_missing_directory_returns_empty() {
let state = create_complex_dirstate();
assert!(state.entries_for_path(b"nosuchdir/nope").is_empty());
}
#[test]
fn entries_for_path_missing_basename_returns_empty() {
let state = create_complex_dirstate();
assert!(state.entries_for_path(b"a/nope").is_empty());
}
#[test]
fn get_entry_by_path_simple_structure() {
let state = create_dirstate_with_root_and_subdir();
let root = state.get_entry_by_path(0, b"").expect("root");
assert_eq!(root.key.file_id, b"a-root-value".to_vec());
let subdir = state.get_entry_by_path(0, b"subdir").expect("subdir");
assert_eq!(subdir.key.basename, b"subdir".to_vec());
assert_eq!(subdir.key.file_id, b"subdir-id".to_vec());
assert!(state.get_entry_by_path(0, b"missing").is_none());
assert!(state.get_entry_by_path(0, b"missing/foo").is_none());
assert!(state.get_entry_by_path(0, b"subdir/foo").is_none());
}
#[test]
fn get_entry_by_path_complex_structure_exists() {
let state = create_complex_dirstate();
let cases: &[(&[u8], &[u8], &[u8], &[u8])] = &[
(b"", b"", b"", b"a-root-value"),
(b"a", b"", b"a", b"a-dir"),
(b"b", b"", b"b", b"b-dir"),
(b"c", b"", b"c", b"c-file"),
(b"d", b"", b"d", b"d-file"),
(b"a/e", b"a", b"e", b"e-dir"),
(b"a/f", b"a", b"f", b"f-file"),
(b"b/g", b"b", b"g", b"g-file"),
(b"b/h\xc3\xa5", b"b", b"h\xc3\xa5", b"h-\xc3\xa5-file"),
];
for (path, dirname, basename, file_id) in cases {
let entry = state
.get_entry_by_path(0, path)
.unwrap_or_else(|| panic!("expected entry at {:?}", path));
assert_eq!(
entry.key.dirname,
dirname.to_vec(),
"dirname for {:?}",
path
);
assert_eq!(
entry.key.basename,
basename.to_vec(),
"basename for {:?}",
path
);
assert_eq!(
entry.key.file_id,
file_id.to_vec(),
"file_id for {:?}",
path
);
}
}
#[test]
fn iter_entries_yields_every_entry_across_blocks() {
let state = create_complex_dirstate();
let entries: Vec<&[u8]> = state
.iter_entries()
.map(|e| e.key.file_id.as_slice())
.collect();
let expected: Vec<&[u8]> = vec![
b"a-root-value",
b"a-dir",
b"b-dir",
b"c-file",
b"d-file",
b"e-dir",
b"f-file",
b"g-file",
b"h-\xc3\xa5-file",
];
assert_eq!(entries, expected);
}
#[test]
fn build_id_index_maps_every_file_id_to_its_key() {
let state = create_complex_dirstate();
let idx = state.build_id_index();
let expected: &[(&[u8], &[u8], &[u8])] = &[
(b"a-root-value", b"", b""),
(b"a-dir", b"", b"a"),
(b"b-dir", b"", b"b"),
(b"c-file", b"", b"c"),
(b"d-file", b"", b"d"),
(b"e-dir", b"a", b"e"),
(b"f-file", b"a", b"f"),
(b"g-file", b"b", b"g"),
(b"h-\xc3\xa5-file", b"b", b"h\xc3\xa5"),
];
for (file_id, dirname, basename) in expected {
let got = idx.get(&FileId::from(&file_id.to_vec()));
assert_eq!(got.len(), 1, "expected one entry for {:?}", file_id);
assert_eq!(got[0].0, dirname.to_vec());
assert_eq!(got[0].1, basename.to_vec());
assert_eq!(got[0].2.as_bytes(), *file_id);
}
}
#[test]
fn build_id_index_collapses_duplicate_file_ids_across_trees() {
let state = create_dirstate_with_two_trees();
let idx = state.build_id_index();
let c_file_entries = idx.get(&FileId::from(&b"c-file".to_vec()));
assert_eq!(c_file_entries.len(), 2);
let basenames: Vec<&[u8]> = c_file_entries
.iter()
.map(|(_, b, _)| b.as_slice())
.collect();
assert!(basenames.contains(&&b"c"[..]));
assert!(basenames.contains(&&b"j"[..]));
}
#[test]
fn get_entry_by_file_id_direct_hit_in_tree_zero() {
let mut state = create_complex_dirstate();
let result = state.get_entry_by_file_id(0, b"c-file", false);
match result {
GetEntryResult::Entry(key) => {
assert_eq!(key.dirname, b"".to_vec());
assert_eq!(key.basename, b"c".to_vec());
assert_eq!(key.file_id, b"c-file".to_vec());
}
other => panic!("expected Entry, got {:?}", other),
}
}
#[test]
fn get_entry_by_file_id_follows_relocation_chain() {
let mut state = create_dirstate_with_two_trees();
let result = state.get_entry_by_file_id(1, b"c-file", false);
match result {
GetEntryResult::Entry(key) => {
assert_eq!(key.dirname, b"b".to_vec());
assert_eq!(key.basename, b"j".to_vec());
assert_eq!(key.file_id, b"c-file".to_vec());
}
other => panic!("expected Entry, got {:?}", other),
}
}
#[test]
fn get_entry_by_file_id_not_found_for_unknown_id() {
let mut state = create_complex_dirstate();
assert_eq!(
state.get_entry_by_file_id(0, b"nonexistent", false),
GetEntryResult::NotFound
);
}
#[test]
fn get_entry_by_file_id_absent_without_include_deleted() {
let mut state = create_dirstate_with_two_trees();
assert_eq!(
state.get_entry_by_file_id(1, b"g-file", false),
GetEntryResult::NotFound
);
}
#[test]
fn get_entry_by_file_id_absent_with_include_deleted() {
let mut state = create_dirstate_with_two_trees();
match state.get_entry_by_file_id(1, b"g-file", true) {
GetEntryResult::Entry(key) => {
assert_eq!(key.basename, b"g".to_vec());
assert_eq!(key.file_id, b"g-file".to_vec());
}
other => panic!("expected Entry, got {:?}", other),
}
}
fn dmp_present_dir() -> TreeData {
TreeData {
minikind: Kind::Directory,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: PACKED_STAT.to_vec(),
}
}
fn dmp_present_file() -> TreeData {
TreeData {
minikind: Kind::File,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: PACKED_STAT.to_vec(),
}
}
fn dmp_absent() -> TreeData {
TreeData {
minikind: Kind::Absent,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: Vec::new(),
}
}
fn dmp_relocated(real_path: &[u8]) -> TreeData {
TreeData {
minikind: Kind::Relocated,
fingerprint: real_path.to_vec(),
size: 0,
executable: false,
packed_stat: Vec::new(),
}
}
fn mk_entry(dirname: &[u8], basename: &[u8], file_id: &[u8], trees: Vec<TreeData>) -> Entry {
Entry {
key: EntryKey {
dirname: dirname.to_vec(),
basename: basename.to_vec(),
file_id: file_id.to_vec(),
},
trees,
}
}
#[test]
fn discard_merge_parents_no_parents_is_noop() {
let mut state = fresh_state();
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: Vec::new(),
},
Dirblock {
dirname: Vec::new(),
entries: Vec::new(),
},
];
state.discard_merge_parents();
assert!(state.parents.is_empty());
assert!(state.ghosts.is_empty());
assert_eq!(state.dirblocks.len(), 2);
}
#[test]
fn discard_merge_parents_one_parent_is_noop_on_dirblocks() {
let mut state = fresh_state();
state.parents = vec![b"parent-id".to_vec()];
let original_dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![mk_entry(
b"",
b"",
b"a-root-value",
vec![dmp_present_dir(), dmp_present_dir()],
)],
},
Dirblock {
dirname: Vec::new(),
entries: Vec::new(),
},
];
state.dirblocks = original_dirblocks.clone();
state.discard_merge_parents();
assert_eq!(state.parents, vec![b"parent-id".to_vec()]);
assert_eq!(state.dirblocks, original_dirblocks);
}
#[test]
fn discard_merge_parents_strips_merge_column() {
let mut state = fresh_state();
state.parents = vec![b"parent-id".to_vec(), b"merged-id".to_vec()];
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![mk_entry(
b"",
b"",
b"a-root-value",
vec![dmp_present_dir(), dmp_present_dir(), dmp_present_dir()],
)],
},
Dirblock {
dirname: Vec::new(),
entries: Vec::new(),
},
];
state.discard_merge_parents();
assert_eq!(state.parents, vec![b"parent-id".to_vec()]);
assert_eq!(state.dirblocks[0].entries.len(), 1);
assert_eq!(state.dirblocks[0].entries[0].trees.len(), 2);
assert_eq!(state.dirblocks[1].entries.len(), 0);
}
#[test]
fn discard_merge_parents_removes_absent_only_rows() {
let mut state = fresh_state();
state.parents = vec![b"parent-id".to_vec(), b"merged-id".to_vec()];
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![mk_entry(
b"",
b"",
b"a-root-value",
vec![dmp_present_dir(), dmp_present_dir(), dmp_present_dir()],
)],
},
Dirblock {
dirname: Vec::new(),
entries: vec![
mk_entry(
b"",
b"file-in-merged",
b"b-file-id",
vec![dmp_absent(), dmp_absent(), dmp_present_file()],
),
mk_entry(
b"",
b"file-in-root",
b"a-file-id",
vec![dmp_present_file(), dmp_present_file(), dmp_present_file()],
),
],
},
];
state.discard_merge_parents();
assert_eq!(state.dirblocks[1].entries.len(), 1);
assert_eq!(
state.dirblocks[1].entries[0].key.basename,
b"file-in-root".to_vec()
);
assert_eq!(state.dirblocks[1].entries[0].trees.len(), 2);
}
#[test]
fn discard_merge_parents_removes_dead_relocation_rows() {
let mut state = fresh_state();
state.parents = vec![b"parent-id".to_vec(), b"merged-id".to_vec()];
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![mk_entry(
b"",
b"",
b"a-root-value",
vec![dmp_present_dir(), dmp_present_dir(), dmp_present_dir()],
)],
},
Dirblock {
dirname: Vec::new(),
entries: vec![
mk_entry(
b"",
b"file-in-1",
b"c-file-id",
vec![
dmp_absent(),
dmp_present_file(),
dmp_relocated(b"file-in-2"),
],
),
mk_entry(
b"",
b"file-in-2",
b"c-file-id",
vec![
dmp_absent(),
dmp_relocated(b"file-in-1"),
dmp_present_file(),
],
),
mk_entry(
b"",
b"file-in-root",
b"a-file-id",
vec![dmp_present_file(), dmp_present_file(), dmp_present_file()],
),
mk_entry(
b"",
b"file-s",
b"b-file-id",
vec![dmp_relocated(b"file-t"), dmp_absent(), dmp_present_file()],
),
mk_entry(
b"",
b"file-t",
b"b-file-id",
vec![dmp_present_file(), dmp_absent(), dmp_relocated(b"file-s")],
),
],
},
];
state.discard_merge_parents();
let surviving: Vec<&[u8]> = state.dirblocks[1]
.entries
.iter()
.map(|e| e.key.basename.as_slice())
.collect();
assert_eq!(
surviving,
vec![&b"file-in-1"[..], b"file-in-root", b"file-t"]
);
for entry in &state.dirblocks[1].entries {
assert_eq!(entry.trees.len(), 2, "{:?}", entry.key.basename);
}
}
#[test]
fn discard_merge_parents_empties_block_of_merge_only_children() {
let mut state = fresh_state();
state.parents = vec![b"parent-id".to_vec(), b"merged-id".to_vec()];
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![mk_entry(
b"",
b"",
b"a-root-value",
vec![dmp_present_dir(), dmp_present_dir(), dmp_present_dir()],
)],
},
Dirblock {
dirname: Vec::new(),
entries: vec![mk_entry(
b"",
b"sub",
b"dir-id",
vec![dmp_present_dir(), dmp_present_dir(), dmp_present_dir()],
)],
},
Dirblock {
dirname: b"sub".to_vec(),
entries: vec![
mk_entry(
b"sub",
b"child1",
b"child1-id",
vec![dmp_absent(), dmp_absent(), dmp_present_file()],
),
mk_entry(
b"sub",
b"child2",
b"child2-id",
vec![dmp_absent(), dmp_absent(), dmp_present_file()],
),
mk_entry(
b"sub",
b"child3",
b"child3-id",
vec![dmp_absent(), dmp_absent(), dmp_present_file()],
),
],
},
];
state.discard_merge_parents();
assert_eq!(state.dirblocks.len(), 3);
assert_eq!(state.dirblocks[0].entries.len(), 1);
assert_eq!(state.dirblocks[1].entries.len(), 1);
assert_eq!(state.dirblocks[2].dirname, b"sub".to_vec());
assert!(
state.dirblocks[2].entries.is_empty(),
"all children were merge-only and should have been removed"
);
for block in &state.dirblocks {
for entry in &block.entries {
assert_eq!(entry.trees.len(), 2);
}
}
}
#[test]
fn discard_merge_parents_ghost_first_parent_replaces_with_null_parent() {
let mut state = fresh_state();
state.parents = vec![b"parent-id".to_vec(), b"merged-id".to_vec()];
state.ghosts = vec![b"parent-id".to_vec()];
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![mk_entry(
b"",
b"",
b"a-root-value",
vec![dmp_present_dir(), dmp_present_dir(), dmp_present_dir()],
)],
},
Dirblock {
dirname: Vec::new(),
entries: Vec::new(),
},
];
state.discard_merge_parents();
assert_eq!(state.parents, vec![b"parent-id".to_vec()]);
assert!(state.ghosts.is_empty());
let root = &state.dirblocks[0].entries[0];
assert_eq!(root.trees.len(), 2);
assert_eq!(root.trees[0].minikind, Kind::Directory);
assert_eq!(root.trees[1].minikind, Kind::Absent);
assert!(root.trees[1].fingerprint.is_empty());
assert!(root.trees[1].packed_stat.is_empty());
}
fn tree_with_fingerprint(minikind: Kind, fingerprint: &[u8]) -> TreeData {
TreeData {
minikind,
fingerprint: fingerprint.to_vec(),
size: 0,
executable: false,
packed_stat: b"x".repeat(32),
}
}
fn absent_fixture(entries: Vec<Entry>) -> DirState {
let mut state = fresh_state();
state.parents = vec![b"parent-id".to_vec(), b"merged-id".to_vec()];
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: Vec::new(),
},
Dirblock {
dirname: Vec::new(),
entries: Vec::new(),
},
Dirblock {
dirname: b"dir".to_vec(),
entries,
},
];
state
}
#[test]
fn make_absent_removes_last_reference_and_updates_id_index() {
let mut state = absent_fixture(vec![entry_with_trees(
b"dir",
b"a",
b"fid-a",
vec![tree(Kind::File)],
)]);
let fid = FileId::from(&b"fid-a".to_vec());
state.get_or_build_id_index();
assert_eq!(state.id_index.as_ref().unwrap().get(&fid).len(), 1);
let last_reference = state
.make_absent(&EntryKey {
dirname: b"dir".to_vec(),
basename: b"a".to_vec(),
file_id: b"fid-a".to_vec(),
})
.expect("make_absent");
assert!(last_reference);
assert!(state.dirblocks[2].entries.is_empty());
assert!(state.id_index.as_ref().unwrap().get(&fid).is_empty());
assert_eq!(state.dirblock_state, MemoryState::InMemoryModified);
}
#[test]
fn make_absent_sets_tree0_absent_when_other_trees_keep_entry() {
let mut state = absent_fixture(vec![entry_with_trees(
b"dir",
b"a",
b"fid-a",
vec![tree(Kind::File), tree(Kind::Directory)],
)]);
let last_reference = state
.make_absent(&EntryKey {
dirname: b"dir".to_vec(),
basename: b"a".to_vec(),
file_id: b"fid-a".to_vec(),
})
.expect("make_absent");
assert!(!last_reference);
assert_eq!(state.dirblocks[2].entries.len(), 1);
let entry = &state.dirblocks[2].entries[0];
assert_eq!(entry.trees[0].minikind, Kind::Absent);
assert!(entry.trees[0].fingerprint.is_empty());
assert!(entry.trees[0].packed_stat.is_empty());
assert_eq!(entry.trees[1].minikind, Kind::Directory);
}
#[test]
fn make_absent_follows_relocation_and_updates_target() {
let mut state = absent_fixture(vec![
entry_with_trees(
b"dir",
b"a",
b"fid-a",
vec![
tree(Kind::File),
tree_with_fingerprint(Kind::Relocated, b"dir/b"),
],
),
entry_with_trees(
b"dir",
b"b",
b"fid-a",
vec![tree(Kind::File), tree(Kind::File)],
),
]);
let fid = FileId::from(&b"fid-a".to_vec());
state.get_or_build_id_index();
assert_eq!(state.id_index.as_ref().unwrap().get(&fid).len(), 2);
let last_reference = state
.make_absent(&EntryKey {
dirname: b"dir".to_vec(),
basename: b"a".to_vec(),
file_id: b"fid-a".to_vec(),
})
.expect("make_absent");
assert!(last_reference);
assert_eq!(state.dirblocks[2].entries.len(), 1);
assert_eq!(state.dirblocks[2].entries[0].key.basename, b"b".to_vec());
let survivor = &state.dirblocks[2].entries[0];
assert_eq!(survivor.trees[0].minikind, Kind::Absent);
assert_eq!(survivor.trees[1].minikind, Kind::File);
assert_eq!(state.id_index.as_ref().unwrap().get(&fid).len(), 1);
}
#[test]
fn make_absent_absent_parent_row_is_ignored() {
let mut state = absent_fixture(vec![entry_with_trees(
b"dir",
b"a",
b"fid-a",
vec![tree(Kind::File), tree(Kind::Absent)],
)]);
let last_reference = state
.make_absent(&EntryKey {
dirname: b"dir".to_vec(),
basename: b"a".to_vec(),
file_id: b"fid-a".to_vec(),
})
.expect("make_absent");
assert!(last_reference);
assert!(state.dirblocks[2].entries.is_empty());
}
#[test]
fn packed_stat_index_only_contains_file_entries() {
let mut state = fresh_state();
let f1 = TreeData {
minikind: Kind::File,
fingerprint: b"sha-f1".to_vec(),
size: 10,
executable: false,
packed_stat: b"stat-f1".to_vec(),
};
let f2 = TreeData {
minikind: Kind::File,
fingerprint: b"sha-f2".to_vec(),
size: 20,
executable: true,
packed_stat: b"stat-f2".to_vec(),
};
let dir = tree(Kind::Directory);
let symlink = TreeData {
minikind: Kind::Symlink,
fingerprint: b"target".to_vec(),
size: 0,
executable: false,
packed_stat: b"stat-l".to_vec(),
};
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(b"", b"", b"TREE_ROOT", vec![dir.clone()])],
},
Dirblock {
dirname: Vec::new(),
entries: vec![
entry_with_trees(b"", b"a", b"fid-a", vec![f1.clone()]),
entry_with_trees(b"", b"b", b"fid-b", vec![f2.clone()]),
entry_with_trees(b"", b"c", b"fid-c", vec![symlink]),
entry_with_trees(b"", b"d", b"fid-d", vec![tree(Kind::Absent)]),
],
},
];
let idx = state.build_packed_stat_index();
assert_eq!(idx.len(), 2);
assert_eq!(idx.get(&b"stat-f1".to_vec()).unwrap(), &b"sha-f1".to_vec());
assert_eq!(idx.get(&b"stat-f2".to_vec()).unwrap(), &b"sha-f2".to_vec());
}
#[test]
fn get_or_build_packed_stat_index_caches_result() {
let mut state = fresh_state();
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"",
b"TREE_ROOT",
vec![tree(Kind::Directory)],
)],
},
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"a",
b"fid-a",
vec![TreeData {
minikind: Kind::File,
fingerprint: b"sha-a".to_vec(),
size: 1,
executable: false,
packed_stat: b"stat-a".to_vec(),
}],
)],
},
];
assert!(state.packed_stat_index.is_none());
let idx = state.get_or_build_packed_stat_index();
assert_eq!(idx.len(), 1);
assert!(state.packed_stat_index.is_some());
let idx_again = state.get_or_build_packed_stat_index();
assert_eq!(idx_again.len(), 1);
}
#[test]
fn packed_stat_index_invalidated_by_set_data() {
let mut state = fresh_state();
state.packed_stat_index = Some(HashMap::new());
state.set_data(Vec::new(), Vec::new());
assert!(state.packed_stat_index.is_none());
}
#[test]
fn packed_stat_index_invalidated_by_wipe_state() {
let mut state = fresh_state();
state.packed_stat_index = Some(HashMap::new());
state.wipe_state();
assert!(state.packed_stat_index.is_none());
}
#[test]
fn make_absent_missing_entry_returns_error() {
let mut state = absent_fixture(vec![entry_with_trees(
b"dir",
b"a",
b"fid-a",
vec![tree(Kind::File)],
)]);
let err = state
.make_absent(&EntryKey {
dirname: b"dir".to_vec(),
basename: b"ghost".to_vec(),
file_id: b"fid-ghost".to_vec(),
})
.unwrap_err();
assert!(matches!(err, MakeAbsentError::EntryNotFound { .. }));
}
fn add_fixture() -> DirState {
let mut state = fresh_state();
state.parents = vec![];
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![Entry {
key: EntryKey {
dirname: Vec::new(),
basename: Vec::new(),
file_id: b"TREE_ROOT".to_vec(),
},
trees: vec![TreeData {
minikind: Kind::Directory,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: b"x".repeat(32),
}],
}],
},
Dirblock {
dirname: Vec::new(),
entries: Vec::new(),
},
];
state
}
#[test]
fn add_inserts_new_file_at_root() {
let mut state = add_fixture();
let stat = b"x".repeat(32);
state
.add(
b"a",
b"",
b"a",
b"fid-a",
crate::osutils::Kind::File,
7,
&stat,
b"sha1",
)
.expect("add");
assert_eq!(state.dirblocks[1].entries.len(), 1);
let entry = &state.dirblocks[1].entries[0];
assert_eq!(entry.key.basename, b"a");
assert_eq!(entry.key.file_id, b"fid-a");
assert_eq!(entry.trees[0].minikind, Kind::File);
assert_eq!(entry.trees[0].size, 7);
assert_eq!(entry.trees[0].fingerprint, b"sha1");
}
#[test]
fn add_directory_creates_child_block() {
let mut state = add_fixture();
let stat = b"x".repeat(32);
state
.add(
b"sub",
b"",
b"sub",
b"fid-sub",
crate::osutils::Kind::Directory,
0,
&stat,
b"",
)
.expect("add");
let block_names: Vec<&[u8]> = state
.dirblocks
.iter()
.map(|b| b.dirname.as_slice())
.collect();
assert!(block_names.contains(&b"sub".as_slice()));
}
#[test]
fn add_duplicate_file_id_errors() {
let mut state = add_fixture();
let stat = b"x".repeat(32);
state
.add(
b"a",
b"",
b"a",
b"fid-a",
crate::osutils::Kind::File,
1,
&stat,
b"",
)
.expect("first add");
let err = state
.add(
b"b",
b"",
b"b",
b"fid-a",
crate::osutils::Kind::File,
1,
&stat,
b"",
)
.unwrap_err();
assert!(matches!(err, AddError::DuplicateFileId { .. }));
}
#[test]
fn add_second_path_same_basename_errors() {
let mut state = add_fixture();
let stat = b"x".repeat(32);
state
.add(
b"a",
b"",
b"a",
b"fid-a",
crate::osutils::Kind::File,
1,
&stat,
b"",
)
.expect("first add");
let err = state
.add(
b"a",
b"",
b"a",
b"fid-other",
crate::osutils::Kind::File,
1,
&stat,
b"",
)
.unwrap_err();
assert!(matches!(err, AddError::AlreadyAdded { .. }));
}
#[test]
fn add_parent_missing_errors_not_versioned() {
let mut state = add_fixture();
let stat = b"x".repeat(32);
let err = state
.add(
b"missing/child",
b"missing",
b"child",
b"fid-c",
crate::osutils::Kind::File,
0,
&stat,
b"",
)
.unwrap_err();
assert!(matches!(err, AddError::NotVersioned { .. }));
}
#[test]
fn set_path_id_rejects_non_root_path() {
let mut state = add_fixture();
let err = state.set_path_id(b"foo", b"new-id").unwrap_err();
assert!(matches!(err, SetPathIdError::NonRootPath));
}
#[test]
fn set_path_id_unchanged_id_is_noop() {
let mut state = add_fixture();
let before = state.dirblocks.clone();
state.set_path_id(b"", b"TREE_ROOT").expect("same-id noop");
assert_eq!(state.dirblocks, before);
}
#[test]
fn set_path_id_rewrites_root_and_preserves_packed_stat() {
let mut state = add_fixture();
let original_packed_stat = state.dirblocks[0].entries[0].trees[0].packed_stat.clone();
state.set_path_id(b"", b"new-id").expect("set_path_id");
assert_eq!(state.dirblocks[0].entries.len(), 1);
let new_root = &state.dirblocks[0].entries[0];
assert_eq!(new_root.key.file_id, b"new-id");
assert_eq!(new_root.trees[0].minikind, Kind::Directory);
assert_eq!(new_root.trees[0].packed_stat, original_packed_stat);
}
#[test]
fn set_state_from_inventory_rename_same_id_bug_395556() {
let mut state = add_fixture();
let stat = b"x".repeat(32);
state
.add(
b"b",
b"",
b"b",
b"b-id",
crate::osutils::Kind::File,
0,
&stat,
b"",
)
.expect("add");
let inv_after_rename: Vec<(Vec<u8>, Vec<u8>, Kind, Vec<u8>, bool)> = vec![
(
Vec::new(),
b"TREE_ROOT".to_vec(),
Kind::Directory,
Vec::new(),
false,
),
(
b"a".to_vec(),
b"b-id".to_vec(),
Kind::File,
Vec::new(),
false,
),
];
state
.set_state_from_inventory(inv_after_rename)
.expect("set_state_from_inventory");
let mut live_entries = Vec::new();
for block in &state.dirblocks {
for entry in &block.entries {
let t0 = match entry.trees.first().map(|t| t.minikind) {
Some(k) if !k.is_absent_or_relocated() => k,
_ => continue,
};
live_entries.push((
entry.key.dirname.clone(),
entry.key.basename.clone(),
entry.key.file_id.clone(),
t0,
));
}
}
assert_eq!(
live_entries,
vec![
(
Vec::new(),
Vec::new(),
b"TREE_ROOT".to_vec(),
Kind::Directory
),
(Vec::new(), b"a".to_vec(), b"b-id".to_vec(), Kind::File),
]
);
}
#[test]
fn walkdirs_utf8_visits_depth_first() {
let mut t = MemoryTransport::new();
let stat_dir = StatInfo {
mode: 0o040755,
size: 0,
mtime: 0,
ctime: 0,
dev: 1,
ino: 1,
};
let stat_file = StatInfo {
mode: 0o100644,
size: 3,
mtime: 0,
ctime: 0,
dev: 1,
ino: 2,
};
t.set_fs(b"", stat_dir, None);
t.set_fs(b"a", stat_file, None);
t.set_fs(b"b", stat_dir, None);
t.set_fs(b"b/c", stat_file, None);
t.set_fs(b"f", stat_file, None);
let mut walker = WalkDirsUtf8::new(b"", b"");
let mut visited: Vec<Vec<u8>> = Vec::new();
while walker
.next_dir(&t, |_rel, abspath, _entries| {
visited.push(abspath.to_vec());
})
.unwrap()
{}
assert_eq!(
visited,
vec![b"".to_vec(), b"b".to_vec()],
"expected only directories visited in depth-first order"
);
}
#[test]
fn walkdirs_utf8_skips_pruned_subdirectories() {
let mut t = MemoryTransport::new();
let stat_dir = StatInfo {
mode: 0o040755,
size: 0,
mtime: 0,
ctime: 0,
dev: 1,
ino: 1,
};
let stat_file = StatInfo {
mode: 0o100644,
size: 3,
mtime: 0,
ctime: 0,
dev: 1,
ino: 2,
};
t.set_fs(b"", stat_dir, None);
t.set_fs(b"b", stat_dir, None);
t.set_fs(b"b/c", stat_file, None);
let mut walker = WalkDirsUtf8::new(b"", b"");
let mut visited: Vec<Vec<u8>> = Vec::new();
while walker
.next_dir(&t, |_rel, abspath, entries| {
visited.push(abspath.to_vec());
entries.retain(|e| e.basename != b"b");
})
.unwrap()
{}
assert_eq!(visited, vec![b"".to_vec()], "pruned dir should not recurse");
}
#[test]
fn walkdirs_utf8_depth_first_across_siblings() {
let mut t = MemoryTransport::new();
let dir = StatInfo {
mode: 0o040755,
size: 0,
mtime: 0,
ctime: 0,
dev: 1,
ino: 1,
};
let file = StatInfo {
mode: 0o100644,
size: 0,
mtime: 0,
ctime: 0,
dev: 1,
ino: 2,
};
t.set_fs(b"", dir, None);
t.set_fs(b"a", dir, None);
t.set_fs(b"a/b", dir, None);
t.set_fs(b"a/b/foo", file, None);
t.set_fs(b"a-b", dir, None);
t.set_fs(b"a-b/bar", file, None);
let mut walker = WalkDirsUtf8::new(b"", b"");
let mut visited: Vec<Vec<u8>> = Vec::new();
while walker
.next_dir(&t, |rel, _abs, _entries| {
visited.push(rel.to_vec());
})
.unwrap()
{}
assert_eq!(
visited,
vec![
b"".to_vec(),
b"a".to_vec(),
b"a/b".to_vec(),
b"a-b".to_vec(),
]
);
}
#[test]
fn iter_changes_next_emits_unversioned_files() {
let mut t = MemoryTransport::new();
let stat_dir = StatInfo {
mode: 0o040755,
size: 0,
mtime: 0,
ctime: 0,
dev: 1,
ino: 1,
};
let stat_file = StatInfo {
mode: 0o100644,
size: 5,
mtime: 0,
ctime: 0,
dev: 1,
ino: 2,
};
t.set_fs(b"", stat_dir, None);
t.set_fs(b"a", stat_file, None);
let mut state = add_fixture();
let mut pstate = ProcessEntryState {
source_index: None,
target_index: 0,
include_unchanged: false,
want_unversioned: true,
partial: false,
supports_tree_reference: false,
root_abspath: Vec::new(),
searched_specific_files: std::collections::HashSet::new(),
search_specific_files: std::collections::HashSet::from([Vec::new()]),
search_specific_file_parents: std::collections::HashSet::new(),
searched_exact_paths: std::collections::HashSet::new(),
seen_ids: std::collections::HashSet::new(),
new_dirname_to_file_id: std::collections::HashMap::new(),
old_dirname_to_file_id: std::collections::HashMap::new(),
last_source_parent: None,
last_target_parent: None,
};
let mut iter = IterChangesIter::new();
let mut changes = Vec::new();
loop {
match state.iter_changes_next(&mut iter, &mut pstate, &t).unwrap() {
Some(c) => changes.push(c),
None => break,
}
}
let unversioned_for_a = changes
.iter()
.any(|c| c.new_path.as_deref() == Some(b"a" as &[u8]) && c.file_id.is_empty());
assert!(
unversioned_for_a,
"expected unversioned-file change for 'a'; got: {:?}",
changes
);
}
#[test]
fn update_entry_refreshes_sha_after_content_change() {
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let fpath = dir.path().join("a-file");
{
let mut f = std::fs::File::create(&fpath).unwrap();
f.write_all(b"first content\n").unwrap();
}
let mut state = add_fixture();
state.parents = vec![b"parent-rev".to_vec()];
state.dirblocks[0].entries[0].trees.push(TreeData {
minikind: Kind::Directory,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: Vec::new(),
});
let stat = b"x".repeat(32);
state
.add(
b"a-file",
b"",
b"a-file",
b"file-id",
crate::osutils::Kind::File,
0,
&stat,
b"",
)
.expect("add");
let bei = state.get_block_entry_index(b"", b"a-file", 0);
state.dirblocks[bei.block_index].entries[bei.entry_index].trees[1] = TreeData {
minikind: Kind::File,
fingerprint: b"parent-sha".to_vec(),
size: 0,
executable: false,
packed_stat: Vec::new(),
};
state.cutoff_time = Some(i64::MAX);
let meta = std::fs::symlink_metadata(&fpath).unwrap();
let stat_info = StatInfo {
mode: {
#[cfg(unix)]
{
meta.mode()
}
#[cfg(not(unix))]
{
0o100644
}
},
size: meta.len(),
mtime: metadata_mtime_secs(&meta),
ctime: metadata_ctime_secs(&meta),
dev: {
#[cfg(unix)]
{
meta.dev()
}
#[cfg(not(unix))]
{
0
}
},
ino: {
#[cfg(unix)]
{
meta.ino()
}
#[cfg(not(unix))]
{
0
}
},
};
let key = EntryKey {
dirname: Vec::new(),
basename: b"a-file".to_vec(),
file_id: b"file-id".to_vec(),
};
let abspath_bytes = fpath.as_os_str().as_encoded_bytes().to_vec();
let transport = MemoryTransport::new();
let result = state
.update_entry(&key, &abspath_bytes, &stat_info, &transport)
.expect("update_entry");
let sha = result.expect("file should yield a sha");
assert_eq!(
std::str::from_utf8(&sha).unwrap(),
"c0a245ade45b97366321074bb27a39a6ae1dc4fc"
);
let bei = state.get_block_entry_index(b"", b"a-file", 0);
assert!(bei.path_present);
let entry = &state.dirblocks[bei.block_index].entries[bei.entry_index];
assert_eq!(entry.trees[0].fingerprint, sha);
}
#[test]
fn bisect_roundtrips_via_get_lines() {
let mut state = add_fixture();
let stat = b"x".repeat(32);
state
.add(
b"alpha",
b"",
b"alpha",
b"a-id",
crate::osutils::Kind::File,
11,
&stat,
b"sha-a",
)
.expect("add alpha");
state
.add(
b"bravo",
b"",
b"bravo",
b"b-id",
crate::osutils::Kind::File,
22,
&stat,
b"sha-b",
)
.expect("add bravo");
let lines = state.get_lines();
let buf: Vec<u8> = lines.into_iter().flatten().collect();
let mut reader = DirState::new(
"/tmp/fake",
Box::new(DefaultSHA1Provider::new()),
0,
true,
false,
);
reader.read_header(&buf).expect("read_header");
reader.dirblock_state = MemoryState::NotInMemory;
let buf_clone = buf.clone();
let read_range = move |offset: u64, len: usize| -> Result<Vec<u8>, BisectError> {
let start = offset as usize;
let end = std::cmp::min(start + len, buf_clone.len());
if start > buf_clone.len() {
return Ok(Vec::new());
}
Ok(buf_clone[start..end].to_vec())
};
let file_size = buf.len() as u64;
let found = reader
.bisect(vec![b"bravo".to_vec()], file_size, read_range)
.expect("bisect");
let bravo = found.get(b"bravo".as_slice()).expect("bravo present");
assert_eq!(bravo.len(), 1);
assert_eq!(bravo[0].key.basename, b"bravo");
assert_eq!(bravo[0].key.file_id, b"b-id");
assert_eq!(bravo[0].trees[0].size, 22);
assert_eq!(bravo[0].trees[0].fingerprint, b"sha-b");
}
#[test]
fn set_parent_trees_simple_case() {
let mut state = add_fixture();
let stat = b"x".repeat(32);
state
.add(
b"a-file",
b"",
b"a-file",
b"file-id",
crate::osutils::Kind::File,
0,
&stat,
b"",
)
.expect("add");
let details_root = TreeData {
minikind: Kind::Directory,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: b"rev1".to_vec(),
};
let details_file = TreeData {
minikind: Kind::File,
fingerprint: b"sha1-parent".to_vec(),
size: 42,
executable: false,
packed_stat: b"rev1".to_vec(),
};
let parent_entries = vec![
(Vec::new(), b"TREE_ROOT".to_vec(), details_root.clone()),
(
b"a-file".to_vec(),
b"file-id".to_vec(),
details_file.clone(),
),
];
state
.set_parent_trees(vec![b"rev1".to_vec()], vec![], vec![parent_entries])
.expect("set_parent_trees");
let bei = get_block_entry_index(&state.dirblocks, b"", b"", 0);
assert!(bei.path_present);
let root = &state.dirblocks[bei.block_index].entries[bei.entry_index];
assert_eq!(root.trees.len(), 2);
assert_eq!(root.trees[1], details_root);
let bei = get_block_entry_index(&state.dirblocks, b"", b"a-file", 0);
assert!(bei.path_present);
let file_entry = &state.dirblocks[bei.block_index].entries[bei.entry_index];
assert_eq!(file_entry.trees.len(), 2);
assert_eq!(file_entry.trees[1], details_file);
assert_eq!(state.parents, vec![b"rev1".to_vec()]);
assert!(state.ghosts.is_empty());
}
#[test]
fn set_parent_trees_ghost_parent_has_no_entries() {
let mut state = add_fixture();
let stat = b"x".repeat(32);
state
.add(
b"x",
b"",
b"x",
b"x-id",
crate::osutils::Kind::File,
0,
&stat,
b"",
)
.expect("add");
state
.set_parent_trees(
vec![b"ghost-rev".to_vec()],
vec![b"ghost-rev".to_vec()],
vec![], )
.expect("set_parent_trees");
for block in &state.dirblocks {
for entry in &block.entries {
assert_eq!(entry.trees.len(), 1);
}
}
assert_eq!(state.parents, vec![b"ghost-rev".to_vec()]);
assert_eq!(state.ghosts, vec![b"ghost-rev".to_vec()]);
}
#[test]
fn set_parent_trees_cross_path_relocation() {
let mut state = add_fixture();
let stat = b"x".repeat(32);
state
.add(
b"new-path",
b"",
b"new-path",
b"fid",
crate::osutils::Kind::File,
0,
&stat,
b"",
)
.expect("add");
let root_details = TreeData {
minikind: Kind::Directory,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: b"rev".to_vec(),
};
let file_details = TreeData {
minikind: Kind::File,
fingerprint: b"old-sha".to_vec(),
size: 7,
executable: false,
packed_stat: b"rev".to_vec(),
};
let parent_entries = vec![
(Vec::new(), b"TREE_ROOT".to_vec(), root_details),
(b"old-path".to_vec(), b"fid".to_vec(), file_details.clone()),
];
state
.set_parent_trees(vec![b"rev".to_vec()], vec![], vec![parent_entries])
.expect("set_parent_trees");
let bei = get_block_entry_index(&state.dirblocks, b"", b"new-path", 0);
assert!(bei.path_present);
let new_entry = &state.dirblocks[bei.block_index].entries[bei.entry_index];
assert_eq!(new_entry.trees[1].minikind, Kind::Relocated);
assert_eq!(new_entry.trees[1].fingerprint, b"old-path");
let bei = get_block_entry_index(&state.dirblocks, b"", b"old-path", 1);
assert!(bei.path_present);
let old_entry = &state.dirblocks[bei.block_index].entries[bei.entry_index];
assert_eq!(old_entry.trees[0].minikind, Kind::Relocated);
assert_eq!(old_entry.trees[0].fingerprint, b"new-path");
assert_eq!(old_entry.trees[1], file_details);
}
#[test]
fn set_path_id_zeroes_packed_stat_when_parents_retain_entry() {
let mut state = add_fixture();
state.parents = vec![b"parent-rev".to_vec()];
state.dirblocks[0].entries[0].trees.push(TreeData {
minikind: Kind::Directory,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: Vec::new(),
});
state.set_path_id(b"", b"new-id").expect("set_path_id");
let new_root = state
.dirblocks
.iter()
.flat_map(|b| b.entries.iter())
.find(|e| e.key.file_id == b"new-id")
.expect("new root entry");
assert_eq!(new_root.trees[0].packed_stat, b"");
}
fn basis_details(minikind: Kind, fingerprint: &[u8], size: u64, executable: bool) -> TreeData {
TreeData {
minikind,
fingerprint: fingerprint.to_vec(),
size,
executable,
packed_stat: b"x".repeat(32),
}
}
fn null_parent_details() -> TreeData {
TreeData {
minikind: Kind::Absent,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: Vec::new(),
}
}
fn basis_adds_fixture_one_file() -> DirState {
let mut state = fresh_state();
state.parents = vec![b"parent-id".to_vec()];
let tree0 = basis_details(Kind::File, b"sha-r", 10, false);
let tree1 = null_parent_details();
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"",
b"TREE_ROOT",
vec![tree(Kind::Directory), tree(Kind::Directory)],
)],
},
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"README",
b"fid-readme",
vec![tree0, tree1],
)],
},
];
state
}
#[test]
fn update_basis_apply_adds_inserts_new_entry() {
let mut state = basis_adds_fixture_one_file();
let mut adds = vec![BasisAdd {
old_path: None,
new_path: b"a.txt".to_vec(),
file_id: b"fid-a".to_vec(),
new_details: basis_details(Kind::File, b"sha-a", 7, false),
real_add: true,
}];
state.update_basis_apply_adds(&mut adds).expect("apply");
let block = &state.dirblocks[1];
assert_eq!(block.entries.len(), 2);
assert_eq!(block.entries[0].key.basename, b"README".to_vec());
assert_eq!(block.entries[1].key.basename, b"a.txt".to_vec());
assert_eq!(block.entries[1].trees[0].minikind, Kind::Absent);
assert_eq!(block.entries[1].trees[1].minikind, Kind::File);
assert_eq!(block.entries[1].trees[1].fingerprint, b"sha-a".to_vec());
}
#[test]
fn update_basis_apply_adds_updates_absent_tree1_slot_in_place() {
let mut state = basis_adds_fixture_one_file();
let mut adds = vec![BasisAdd {
old_path: None,
new_path: b"README".to_vec(),
file_id: b"fid-readme".to_vec(),
new_details: basis_details(Kind::File, b"sha-updated", 42, true),
real_add: true,
}];
state.update_basis_apply_adds(&mut adds).expect("apply");
let block = &state.dirblocks[1];
assert_eq!(block.entries.len(), 1);
assert_eq!(
block.entries[0].trees[1].fingerprint,
b"sha-updated".to_vec()
);
assert_eq!(block.entries[0].trees[1].size, 42);
assert!(block.entries[0].trees[1].executable);
}
#[test]
fn update_basis_apply_adds_conflicting_existing_basis_is_invalid() {
let mut state = basis_adds_fixture_one_file();
state.dirblocks[1].entries[0].trees[1] = basis_details(Kind::File, b"sha-existing", 11, false);
let mut adds = vec![BasisAdd {
old_path: None,
new_path: b"README".to_vec(),
file_id: b"fid-readme".to_vec(),
new_details: basis_details(Kind::File, b"sha-new", 22, false),
real_add: true,
}];
let err = state.update_basis_apply_adds(&mut adds).unwrap_err();
match err {
BasisApplyError::Invalid { path, reason, .. } => {
assert_eq!(path, b"README".to_vec());
assert!(
reason.contains("basis target already existed"),
"{}",
reason
);
}
other => panic!("expected Invalid, got {:?}", other),
}
}
#[test]
fn update_basis_apply_adds_real_add_with_old_path_is_invalid() {
let mut state = basis_adds_fixture_one_file();
let mut adds = vec![BasisAdd {
old_path: Some(b"some/old".to_vec()),
new_path: b"new.txt".to_vec(),
file_id: b"fid-new".to_vec(),
new_details: basis_details(Kind::File, b"sha", 0, false),
real_add: true,
}];
let err = state.update_basis_apply_adds(&mut adds).unwrap_err();
assert!(matches!(err, BasisApplyError::Invalid { .. }));
}
#[test]
fn update_basis_apply_changes_updates_existing_tree1_slot() {
let mut state = fresh_state();
state.parents = vec![b"parent-id".to_vec()];
let tree0 = basis_details(Kind::File, b"sha-r", 10, false);
let tree1 = basis_details(Kind::File, b"sha-old", 10, false);
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"",
b"TREE_ROOT",
vec![tree(Kind::Directory), tree(Kind::Directory)],
)],
},
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"README",
b"fid-readme",
vec![tree0.clone(), tree1],
)],
},
];
let new_details = basis_details(Kind::File, b"sha-updated", 99, true);
let changes = vec![(
b"README".to_vec(),
b"README".to_vec(),
b"fid-readme".to_vec(),
new_details.clone(),
)];
state
.update_basis_apply_changes(&changes)
.expect("apply_changes");
let entry = &state.dirblocks[1].entries[0];
assert_eq!(entry.trees[0].fingerprint, b"sha-r".to_vec());
assert_eq!(entry.trees[1].fingerprint, b"sha-updated".to_vec());
assert_eq!(entry.trees[1].size, 99);
assert!(entry.trees[1].executable);
}
#[test]
fn update_basis_apply_changes_absent_entry_is_invalid() {
let mut state = basis_adds_fixture_one_file();
let changes = vec![(
b"README".to_vec(),
b"README".to_vec(),
b"fid-readme".to_vec(),
basis_details(Kind::File, b"sha", 1, false),
)];
let err = state.update_basis_apply_changes(&changes).unwrap_err();
assert!(matches!(err, BasisApplyError::Invalid { .. }));
}
#[test]
fn update_basis_apply_deletes_removes_row_when_active_also_absent() {
let mut state = fresh_state();
state.parents = vec![b"parent-id".to_vec()];
let t0 = null_parent_details();
let t1 = basis_details(Kind::File, b"sha", 1, false);
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"",
b"TREE_ROOT",
vec![tree(Kind::Directory), tree(Kind::Directory)],
)],
},
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"README",
b"fid-readme",
vec![t0, t1],
)],
},
];
let deletes = vec![(
b"README".to_vec(),
None::<Vec<u8>>,
b"fid-readme".to_vec(),
true,
)];
state
.update_basis_apply_deletes(&deletes)
.expect("apply_deletes");
assert!(state.dirblocks[1].entries.is_empty());
}
#[test]
fn update_basis_apply_deletes_keeps_row_when_active_still_present() {
let mut state = basis_adds_fixture_one_file();
state.dirblocks[1].entries[0].trees[1] = basis_details(Kind::File, b"sha-old", 10, false);
let deletes = vec![(
b"README".to_vec(),
Some(b"README.new".to_vec()),
b"fid-readme".to_vec(),
false,
)];
state
.update_basis_apply_deletes(&deletes)
.expect("apply_deletes");
let entry = &state.dirblocks[1].entries[0];
assert_eq!(entry.trees[1].minikind, Kind::Absent);
assert!(entry.trees[1].fingerprint.is_empty());
}
#[test]
fn update_basis_apply_deletes_bad_delta_is_invalid() {
let mut state = basis_adds_fixture_one_file();
let deletes = vec![(
b"README".to_vec(),
Some(b"README.new".to_vec()),
b"fid-readme".to_vec(),
true,
)];
let err = state.update_basis_apply_deletes(&deletes).unwrap_err();
match err {
BasisApplyError::Invalid { reason, .. } => {
assert!(reason.contains("bad delete delta"), "{}", reason);
}
other => panic!("expected Invalid, got {:?}", other),
}
}
#[test]
fn update_basis_apply_deletes_missing_entry_is_invalid() {
let mut state = basis_adds_fixture_one_file();
let deletes = vec![(
b"ghost".to_vec(),
None::<Vec<u8>>,
b"fid-ghost".to_vec(),
true,
)];
let err = state.update_basis_apply_deletes(&deletes).unwrap_err();
match err {
BasisApplyError::Invalid { reason, .. } => {
assert!(reason.contains("basis tree does not contain"), "{}", reason);
}
other => panic!("expected Invalid, got {:?}", other),
}
}
#[test]
fn update_basis_apply_adds_sorts_input_so_parents_come_first() {
let mut state = fresh_state();
state.parents = vec![b"parent-id".to_vec()];
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"",
b"TREE_ROOT",
vec![tree(Kind::Directory), tree(Kind::Directory)],
)],
},
Dirblock {
dirname: Vec::new(),
entries: Vec::new(),
},
];
let mut adds = vec![
BasisAdd {
old_path: None,
new_path: b"dir/child".to_vec(),
file_id: b"fid-child".to_vec(),
new_details: basis_details(Kind::File, b"sha-c", 1, false),
real_add: true,
},
BasisAdd {
old_path: None,
new_path: b"dir".to_vec(),
file_id: b"fid-dir".to_vec(),
new_details: basis_details(Kind::Directory, b"", 0, false),
real_add: true,
},
];
state.update_basis_apply_adds(&mut adds).expect("apply");
let dirblock_names: Vec<&[u8]> = state
.dirblocks
.iter()
.map(|b| b.dirname.as_slice())
.collect();
assert!(dirblock_names.iter().any(|&n| n == b"dir"));
let dir_block = state
.dirblocks
.iter()
.find(|b| b.dirname == b"dir")
.unwrap();
assert_eq!(dir_block.entries.len(), 1);
assert_eq!(dir_block.entries[0].key.basename, b"child".to_vec());
}
fn one_tree_root_state() -> DirState {
let mut state = fresh_state();
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"",
b"TREE_ROOT",
vec![tree(Kind::Directory)],
)],
},
Dirblock {
dirname: Vec::new(),
entries: Vec::new(),
},
];
state
}
#[test]
fn update_by_delta_add_file_at_root() {
let mut state = one_tree_root_state();
let entries = vec![FlatDeltaEntry {
old_path: None,
new_path: Some(b"README".to_vec()),
file_id: b"fid-r".to_vec(),
parent_id: Some(b"TREE_ROOT".to_vec()),
minikind: Kind::File,
executable: false,
fingerprint: Vec::new(),
}];
state.update_by_delta(entries).expect("update_by_delta");
let bei = get_block_entry_index(&state.dirblocks, b"", b"README", 0);
assert!(bei.path_present);
let entry = &state.dirblocks[bei.block_index].entries[bei.entry_index];
assert_eq!(entry.trees[0].minikind, Kind::File);
assert_eq!(entry.key.file_id, b"fid-r".to_vec());
}
#[test]
fn update_by_delta_delete_then_reinsert_different_id_is_rejected() {
let mut state = one_tree_root_state();
state.dirblocks[1].entries.push(entry_with_trees(
b"",
b"README",
b"fid-existing",
vec![tree(Kind::File)],
));
let entries = vec![FlatDeltaEntry {
old_path: None,
new_path: Some(b"OTHER".to_vec()),
file_id: b"fid-existing".to_vec(),
parent_id: Some(b"TREE_ROOT".to_vec()),
minikind: Kind::File,
executable: false,
fingerprint: Vec::new(),
}];
let err = state.update_by_delta(entries).unwrap_err();
match err {
BasisApplyError::Invalid { file_id, .. } => {
assert_eq!(file_id, b"fid-existing".to_vec());
}
other => panic!("expected Invalid, got {:?}", other),
}
}
#[test]
fn update_by_delta_repeated_file_id_is_rejected() {
let mut state = one_tree_root_state();
let entries = vec![
FlatDeltaEntry {
old_path: None,
new_path: Some(b"a".to_vec()),
file_id: b"fid-dup".to_vec(),
parent_id: Some(b"TREE_ROOT".to_vec()),
minikind: Kind::File,
executable: false,
fingerprint: Vec::new(),
},
FlatDeltaEntry {
old_path: None,
new_path: Some(b"b".to_vec()),
file_id: b"fid-dup".to_vec(),
parent_id: Some(b"TREE_ROOT".to_vec()),
minikind: Kind::File,
executable: false,
fingerprint: Vec::new(),
},
];
let err = state.update_by_delta(entries).unwrap_err();
match err {
BasisApplyError::Invalid { reason, .. } => {
assert_eq!(reason, "repeated file_id");
}
other => panic!("expected Invalid, got {:?}", other),
}
}
#[test]
fn update_by_delta_rename_expands_children() {
let mut state = fresh_state();
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"",
b"TREE_ROOT",
vec![tree(Kind::Directory)],
)],
},
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"a",
b"a-dir",
vec![tree(Kind::Directory)],
)],
},
Dirblock {
dirname: b"a".to_vec(),
entries: vec![entry_with_trees(
b"a",
b"f",
b"f-file",
vec![tree(Kind::File)],
)],
},
];
let entries = vec![FlatDeltaEntry {
old_path: Some(b"a".to_vec()),
new_path: Some(b"z".to_vec()),
file_id: b"a-dir".to_vec(),
parent_id: Some(b"TREE_ROOT".to_vec()),
minikind: Kind::Directory,
executable: false,
fingerprint: Vec::new(),
}];
state.update_by_delta(entries).expect("rename");
let old_a = get_block_entry_index(&state.dirblocks, b"", b"a", 0);
assert!(!old_a.path_present, "a should be gone from tree 0");
let old_af = get_block_entry_index(&state.dirblocks, b"a", b"f", 0);
assert!(!old_af.path_present, "a/f should be gone from tree 0");
let new_z = get_block_entry_index(&state.dirblocks, b"", b"z", 0);
assert!(new_z.path_present, "z should be present in tree 0");
let new_zf = get_block_entry_index(&state.dirblocks, b"z", b"f", 0);
assert!(new_zf.path_present, "z/f should be present in tree 0");
assert_eq!(
state.dirblocks[new_zf.block_index].entries[new_zf.entry_index]
.key
.file_id,
b"f-file".to_vec()
);
}
fn two_tree_basis_state() -> DirState {
let mut state = fresh_state();
state.parents = vec![b"old-revid".to_vec()];
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"",
b"TREE_ROOT",
vec![tree(Kind::Directory), tree(Kind::Directory)],
)],
},
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"README",
b"fid-readme",
vec![
basis_details(Kind::File, b"sha-cur", 10, false),
basis_details(Kind::File, b"sha-old", 10, false),
],
)],
},
];
state
}
#[test]
fn update_basis_by_delta_in_place_change() {
let mut state = two_tree_basis_state();
let entries = vec![FlatBasisDeltaEntry {
old_path: Some(b"README".to_vec()),
new_path: Some(b"README".to_vec()),
file_id: b"fid-readme".to_vec(),
parent_id: Some(b"TREE_ROOT".to_vec()),
details: Some((
Kind::File,
b"sha-new".to_vec(),
20,
false,
b"new-revid".to_vec(),
)),
}];
state
.update_basis_by_delta(entries, b"new-revid".to_vec())
.expect("update_basis_by_delta");
let bei = get_block_entry_index(&state.dirblocks, b"", b"README", 1);
assert!(bei.path_present);
let entry = &state.dirblocks[bei.block_index].entries[bei.entry_index];
assert_eq!(entry.trees[1].fingerprint, b"sha-new".to_vec());
assert_eq!(entry.trees[1].size, 20);
assert_eq!(entry.trees[0].fingerprint, b"sha-cur".to_vec());
}
#[test]
fn update_basis_by_delta_add_new_file() {
let mut state = two_tree_basis_state();
let entries = vec![FlatBasisDeltaEntry {
old_path: None,
new_path: Some(b"NEWFILE".to_vec()),
file_id: b"fid-new".to_vec(),
parent_id: Some(b"TREE_ROOT".to_vec()),
details: Some((
Kind::File,
b"sha-new".to_vec(),
5,
false,
b"new-revid".to_vec(),
)),
}];
state
.update_basis_by_delta(entries, b"new-revid".to_vec())
.expect("update_basis_by_delta");
let bei = get_block_entry_index(&state.dirblocks, b"", b"NEWFILE", 1);
assert!(bei.path_present);
let entry = &state.dirblocks[bei.block_index].entries[bei.entry_index];
assert_eq!(entry.trees[1].minikind, Kind::File);
assert_eq!(entry.key.file_id, b"fid-new".to_vec());
}
#[test]
fn update_basis_by_delta_rename_directory_with_child() {
let mut state = fresh_state();
state.parents = vec![b"old-revid".to_vec()];
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"",
b"TREE_ROOT",
vec![tree(Kind::Directory), tree(Kind::Directory)],
)],
},
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"a",
b"a-dir",
vec![tree(Kind::Directory), tree(Kind::Directory)],
)],
},
Dirblock {
dirname: b"a".to_vec(),
entries: vec![entry_with_trees(
b"a",
b"f",
b"f-file",
vec![
basis_details(Kind::File, b"sha-cur-f", 3, false),
basis_details(Kind::File, b"sha-old-f", 3, false),
],
)],
},
];
let entries = vec![FlatBasisDeltaEntry {
old_path: Some(b"a".to_vec()),
new_path: Some(b"z".to_vec()),
file_id: b"a-dir".to_vec(),
parent_id: Some(b"TREE_ROOT".to_vec()),
details: Some((Kind::Directory, Vec::new(), 0, false, b"new-revid".to_vec())),
}];
state
.update_basis_by_delta(entries, b"new-revid".to_vec())
.expect("update_basis_by_delta");
let old_a = get_block_entry_index(&state.dirblocks, b"", b"a", 1);
assert!(!old_a.path_present, "a should be gone from tree 1");
let old_af = get_block_entry_index(&state.dirblocks, b"a", b"f", 1);
assert!(!old_af.path_present, "a/f should be gone from tree 1");
let new_z = get_block_entry_index(&state.dirblocks, b"", b"z", 1);
assert!(new_z.path_present, "z should be present in tree 1");
let new_zf = get_block_entry_index(&state.dirblocks, b"z", b"f", 1);
assert!(new_zf.path_present, "z/f should be present in tree 1");
assert_eq!(
state.dirblocks[new_zf.block_index].entries[new_zf.entry_index]
.key
.file_id,
b"f-file".to_vec()
);
}
#[test]
fn update_basis_by_delta_delete_file() {
let mut state = two_tree_basis_state();
let entries = vec![FlatBasisDeltaEntry {
old_path: Some(b"README".to_vec()),
new_path: None,
file_id: b"fid-readme".to_vec(),
parent_id: None,
details: None,
}];
state
.update_basis_by_delta(entries, b"new-revid".to_vec())
.expect("update_basis_by_delta");
let bei = get_block_entry_index(&state.dirblocks, b"", b"README", 1);
assert!(!bei.path_present);
}
#[test]
fn get_or_build_id_index_caches_result() {
let mut state = create_complex_dirstate();
assert!(state.id_index.is_none());
state.get_or_build_id_index();
assert!(state.id_index.is_some());
state.dirblocks.clear();
let idx_after = state.get_or_build_id_index();
assert!(
idx_after
.get(&FileId::from(&b"a-root-value".to_vec()))
.iter()
.any(|(_, _, f)| f.as_bytes() == b"a-root-value"),
"cache should survive dirblock mutation"
);
}
#[test]
fn get_entry_by_path_complex_structure_missing() {
let state = create_complex_dirstate();
for path in [&b"_"[..], b"_\xc3\xa5", b"a/b", b"c/d"] {
assert!(
state.get_entry_by_path(0, path).is_none(),
"expected None for {:?}",
path
);
}
}
#[test]
fn get_block_entry_index_simple_structure() {
let state = create_dirstate_with_root_and_subdir();
let bei = state.get_block_entry_index(b"", b"subdir", 0);
assert_eq!(bei.block_index, 1);
assert_eq!(bei.entry_index, 0);
assert!(bei.dir_present);
assert!(bei.path_present);
let bei = state.get_block_entry_index(b"", b"bdir", 0);
assert_eq!(bei.block_index, 1);
assert_eq!(bei.entry_index, 0);
assert!(bei.dir_present);
assert!(!bei.path_present);
let bei = state.get_block_entry_index(b"", b"zdir", 0);
assert_eq!(bei.block_index, 1);
assert_eq!(bei.entry_index, 1);
assert!(bei.dir_present);
assert!(!bei.path_present);
let bei = state.get_block_entry_index(b"a", b"foo", 0);
assert_eq!(bei.block_index, 2);
assert_eq!(bei.entry_index, 0);
assert!(!bei.dir_present);
assert!(!bei.path_present);
let bei = state.get_block_entry_index(b"subdir", b"foo", 0);
assert_eq!(bei.block_index, 2);
assert!(!bei.dir_present);
assert!(!bei.path_present);
}
#[test]
fn get_block_entry_index_complex_structure_exists() {
let state = create_complex_dirstate();
let bei = state.get_block_entry_index(b"", b"", 0);
assert_eq!(
(
bei.block_index,
bei.entry_index,
bei.dir_present,
bei.path_present
),
(0, 0, true, true)
);
for (i, basename) in [&b"a"[..], b"b", b"c", b"d"].iter().enumerate() {
let bei = state.get_block_entry_index(b"", basename, 0);
assert_eq!(
(
bei.block_index,
bei.entry_index,
bei.dir_present,
bei.path_present
),
(1, i, true, true),
"root/{:?}",
basename
);
}
let bei = state.get_block_entry_index(b"a", b"e", 0);
assert_eq!(
(
bei.block_index,
bei.entry_index,
bei.dir_present,
bei.path_present
),
(2, 0, true, true)
);
let bei = state.get_block_entry_index(b"a", b"f", 0);
assert_eq!(
(
bei.block_index,
bei.entry_index,
bei.dir_present,
bei.path_present
),
(2, 1, true, true)
);
let bei = state.get_block_entry_index(b"b", b"g", 0);
assert_eq!(
(
bei.block_index,
bei.entry_index,
bei.dir_present,
bei.path_present
),
(3, 0, true, true)
);
let bei = state.get_block_entry_index(b"b", b"h\xc3\xa5", 0);
assert_eq!(
(
bei.block_index,
bei.entry_index,
bei.dir_present,
bei.path_present
),
(3, 1, true, true)
);
}
#[test]
fn get_block_entry_index_complex_structure_missing() {
let state = create_complex_dirstate();
let bei = state.get_block_entry_index(b"", b"", 0);
assert_eq!(
(
bei.block_index,
bei.entry_index,
bei.dir_present,
bei.path_present
),
(0, 0, true, true)
);
let bei = state.get_block_entry_index(b"", b"_", 0);
assert_eq!(
(
bei.block_index,
bei.entry_index,
bei.dir_present,
bei.path_present
),
(1, 0, true, false)
);
let bei = state.get_block_entry_index(b"", b"aa", 0);
assert_eq!(
(
bei.block_index,
bei.entry_index,
bei.dir_present,
bei.path_present
),
(1, 1, true, false)
);
let bei = state.get_block_entry_index(b"", b"h\xc3\xa5", 0);
assert_eq!(
(
bei.block_index,
bei.entry_index,
bei.dir_present,
bei.path_present
),
(1, 4, true, false)
);
let bei = state.get_block_entry_index(b"_", b"a", 0);
assert_eq!(
(
bei.block_index,
bei.entry_index,
bei.dir_present,
bei.path_present
),
(2, 0, false, false)
);
let bei = state.get_block_entry_index(b"aa", b"a", 0);
assert_eq!(
(
bei.block_index,
bei.entry_index,
bei.dir_present,
bei.path_present
),
(3, 0, false, false)
);
let bei = state.get_block_entry_index(b"bb", b"a", 0);
assert_eq!(
(
bei.block_index,
bei.entry_index,
bei.dir_present,
bei.path_present
),
(4, 0, false, false)
);
let bei = state.get_block_entry_index(b"a/e", b"a", 0);
assert_eq!(
(
bei.block_index,
bei.entry_index,
bei.dir_present,
bei.path_present
),
(3, 0, false, false)
);
let bei = state.get_block_entry_index(b"e", b"a", 0);
assert_eq!(
(
bei.block_index,
bei.entry_index,
bei.dir_present,
bei.path_present
),
(4, 0, false, false)
);
}
#[test]
fn dirstate_method_wrappers_delegate_to_free_functions() {
let mut state = DirState::new(
"dirstate",
Box::new(DefaultSHA1Provider::new()),
0,
true,
false,
);
state.dirblocks = make_dirblocks(vec![(
b"dir",
vec![entry_with_trees(
b"dir",
b"a",
b"fid-a",
vec![tree(Kind::File)],
)],
)]);
let key = EntryKey {
dirname: b"dir".to_vec(),
basename: b"a".to_vec(),
file_id: b"fid-a".to_vec(),
};
assert_eq!(state.find_block_index_from_key(&key), (2, true));
let block = &state.dirblocks[2].entries.clone();
assert_eq!(state.find_entry_index(&key, block), (0, true));
let bei = state.get_block_entry_index(b"dir", b"a", 0);
assert_eq!(bei.block_index, 2);
assert!(bei.path_present);
}
#[test]
fn entry_to_line_single_tree_matches_expected_layout() {
let nullstat = b"x".repeat(32);
let entry = Entry {
key: EntryKey {
dirname: b"".to_vec(),
basename: b"README".to_vec(),
file_id: b"fid-readme".to_vec(),
},
trees: vec![TreeData {
minikind: Kind::File,
fingerprint: b"sha1value".to_vec(),
size: 42,
executable: true,
packed_stat: nullstat.clone(),
}],
};
let line = entry_to_line(&entry);
let mut expected = Vec::new();
expected.extend_from_slice(b"\x00README\x00fid-readme\x00f\x00sha1value\x0042\x00y\x00");
expected.extend_from_slice(&nullstat);
assert_eq!(line, expected);
}
#[test]
fn entry_to_line_multi_tree() {
let nullstat = b"x".repeat(32);
let entry = Entry {
key: EntryKey {
dirname: b"sub".to_vec(),
basename: b"f".to_vec(),
file_id: b"fid".to_vec(),
},
trees: vec![
TreeData {
minikind: Kind::File,
fingerprint: b"cur".to_vec(),
size: 7,
executable: false,
packed_stat: nullstat.clone(),
},
TreeData {
minikind: Kind::Absent,
fingerprint: b"".to_vec(),
size: 0,
executable: false,
packed_stat: nullstat.clone(),
},
],
};
let line = entry_to_line(&entry);
let mut expected = Vec::new();
expected.extend_from_slice(b"sub\x00f\x00fid\x00f\x00cur\x007\x00n\x00");
expected.extend_from_slice(&nullstat);
expected.extend_from_slice(b"\x00a\x00\x000\x00n\x00");
expected.extend_from_slice(&nullstat);
assert_eq!(line, expected);
}
#[test]
fn entry_to_line_with_parent_matches_python_bytes() {
let entry = Entry {
key: EntryKey {
dirname: b"".to_vec(),
basename: b"".to_vec(),
file_id: b"a-root-value".to_vec(),
},
trees: vec![
TreeData {
minikind: Kind::Directory,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: PACKED_STAT.to_vec(),
},
TreeData {
minikind: Kind::Absent,
fingerprint: b"dirname/basename".to_vec(),
size: 0,
executable: false,
packed_stat: Vec::new(),
},
],
};
let expected: &[u8] = b"\x00\x00a-root-value\x00\
d\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00\
a\x00dirname/basename\x000\x00n\x00";
assert_eq!(entry_to_line(&entry), expected);
}
#[test]
fn entry_to_line_with_two_parents_at_different_paths_matches_python_bytes() {
let entry = Entry {
key: EntryKey {
dirname: b"".to_vec(),
basename: b"".to_vec(),
file_id: b"a-root-value".to_vec(),
},
trees: vec![
TreeData {
minikind: Kind::Directory,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: PACKED_STAT.to_vec(),
},
TreeData {
minikind: Kind::Directory,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: b"rev_id".to_vec(),
},
TreeData {
minikind: Kind::Absent,
fingerprint: b"dirname/basename".to_vec(),
size: 0,
executable: false,
packed_stat: Vec::new(),
},
],
};
let expected: &[u8] = b"\x00\x00a-root-value\x00\
d\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00\
d\x00\x000\x00n\x00rev_id\x00\
a\x00dirname/basename\x000\x00n\x00";
assert_eq!(entry_to_line(&entry), expected);
}
#[test]
fn entry_to_line_round_trip_through_parse_dirblocks() {
let nullstat = b"x".repeat(32);
let original = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![Entry {
key: EntryKey {
dirname: b"".to_vec(),
basename: b"".to_vec(),
file_id: b"TREE_ROOT".to_vec(),
},
trees: vec![TreeData {
minikind: Kind::Directory,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: nullstat.clone(),
}],
}],
},
Dirblock {
dirname: Vec::new(),
entries: vec![Entry {
key: EntryKey {
dirname: b"".to_vec(),
basename: b"README".to_vec(),
file_id: b"fid-readme".to_vec(),
},
trees: vec![TreeData {
minikind: Kind::File,
fingerprint: b"sha1".to_vec(),
size: 10,
executable: true,
packed_stat: nullstat.clone(),
}],
}],
},
];
let mut state = DirState::new(
"dirstate",
Box::new(DefaultSHA1Provider::new()),
0,
true,
false,
);
state.dirblocks = original.clone();
let chunks = state.get_lines();
let data: Vec<u8> = chunks.into_iter().flatten().collect();
let header = read_header(&data).expect("parse header");
assert_eq!(header.num_entries, 2);
let body = &data[header.end_of_header..];
let mut parsed = parse_dirblocks(body, 1, header.num_entries).expect("parse body");
split_root_dirblock_into_contents(&mut parsed).expect("split");
assert_eq!(parsed.len(), 2);
assert_eq!(parsed[0].entries.len(), 1);
assert_eq!(parsed[0].entries[0].key.file_id, b"TREE_ROOT".to_vec());
assert_eq!(parsed[1].entries.len(), 1);
assert_eq!(parsed[1].entries[0].key.basename, b"README".to_vec());
assert_eq!(parsed[1].entries[0].trees[0].size, 10);
assert!(parsed[1].entries[0].trees[0].executable);
}
#[test]
fn dirblocks_to_entry_lines_empty() {
assert!(dirblocks_to_entry_lines(&[]).is_empty());
}
#[test]
fn dirblocks_to_entry_lines_skips_empty_blocks() {
let nullstat = b"x".repeat(32);
let blocks = vec![
Dirblock {
dirname: Vec::new(),
entries: Vec::new(),
},
Dirblock {
dirname: b"sub".to_vec(),
entries: vec![Entry {
key: EntryKey {
dirname: b"sub".to_vec(),
basename: b"f".to_vec(),
file_id: b"fid".to_vec(),
},
trees: vec![TreeData {
minikind: Kind::File,
fingerprint: b"sha1".to_vec(),
size: 1,
executable: false,
packed_stat: nullstat.clone(),
}],
}],
},
];
let lines = dirblocks_to_entry_lines(&blocks);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0], entry_to_line(&blocks[1].entries[0]));
}
#[test]
fn dirblocks_to_entry_lines_preserves_order_across_blocks() {
let nullstat = b"x".repeat(32);
let make_entry = |basename: &[u8], file_id: &[u8]| Entry {
key: EntryKey {
dirname: b"".to_vec(),
basename: basename.to_vec(),
file_id: file_id.to_vec(),
},
trees: vec![TreeData {
minikind: Kind::File,
fingerprint: b"sha".to_vec(),
size: 1,
executable: false,
packed_stat: nullstat.clone(),
}],
};
let blocks = vec![
Dirblock {
dirname: b"a".to_vec(),
entries: vec![
make_entry(b"first", b"id-a1"),
make_entry(b"second", b"id-a2"),
],
},
Dirblock {
dirname: b"b".to_vec(),
entries: vec![make_entry(b"only", b"id-b1")],
},
];
let lines = dirblocks_to_entry_lines(&blocks);
assert_eq!(lines.len(), 3);
assert_eq!(lines[0], entry_to_line(&blocks[0].entries[0]));
assert_eq!(lines[1], entry_to_line(&blocks[0].entries[1]));
assert_eq!(lines[2], entry_to_line(&blocks[1].entries[0]));
}
#[test]
fn dirstate_get_lines_matches_python_saved_bytes() {
let nullstat = b"x".repeat(32);
let mut state = DirState::new(
"dirstate",
Box::new(DefaultSHA1Provider::new()),
0,
true,
false,
);
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![Entry {
key: EntryKey {
dirname: b"".to_vec(),
basename: b"".to_vec(),
file_id: b"TREE_ROOT".to_vec(),
},
trees: vec![TreeData {
minikind: Kind::Directory,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: nullstat,
}],
}],
},
Dirblock {
dirname: Vec::new(),
entries: Vec::new(),
},
];
let chunks = state.get_lines();
let actual: Vec<u8> = chunks.into_iter().flatten().collect();
let expected: &[u8] = b"#bazaar dirstate flat format 3\n\
crc32: 2823629280\n\
num_entries: 1\n\
0\x00\n\
\x000\x00\n\
\x00\x00\x00TREE_ROOT\x00d\x00\x000\x00n\x00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\x00\n\x00";
assert_eq!(actual, expected);
}
#[test]
fn dirstate_get_lines_multi_tree_with_parent_matches_python() {
let nullstat = b"x".repeat(32);
let mut state = DirState::new(
"dirstate",
Box::new(DefaultSHA1Provider::new()),
0,
true,
false,
);
state.parents = vec![b"rev-a".to_vec()];
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![Entry {
key: EntryKey {
dirname: b"".to_vec(),
basename: b"".to_vec(),
file_id: b"TREE_ROOT".to_vec(),
},
trees: vec![
TreeData {
minikind: Kind::Directory,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: nullstat.clone(),
},
TreeData {
minikind: Kind::Directory,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: nullstat.clone(),
},
],
}],
},
Dirblock {
dirname: Vec::new(),
entries: vec![Entry {
key: EntryKey {
dirname: b"".to_vec(),
basename: b"README".to_vec(),
file_id: b"fid-readme".to_vec(),
},
trees: vec![
TreeData {
minikind: Kind::File,
fingerprint: b"sha-cur".to_vec(),
size: 10,
executable: true,
packed_stat: nullstat.clone(),
},
TreeData {
minikind: Kind::File,
fingerprint: b"sha-par".to_vec(),
size: 8,
executable: false,
packed_stat: nullstat,
},
],
}],
},
];
let chunks = state.get_lines();
let actual: Vec<u8> = chunks.into_iter().flatten().collect();
let expected: &[u8] = b"#bazaar dirstate flat format 3\n\
crc32: 2831533605\n\
num_entries: 2\n\
1\x00rev-a\x00\n\
\x000\x00\n\
\x00\x00\x00TREE_ROOT\x00d\x00\x000\x00n\x00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\x00d\x00\x000\x00n\x00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\x00\n\
\x00\x00README\x00fid-readme\x00f\x00sha-cur\x0010\x00y\x00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\x00f\x00sha-par\x008\x00n\x00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\x00\n\x00";
assert_eq!(actual, expected);
}
fn fresh_state() -> DirState {
DirState::new(
"dirstate",
Box::new(DefaultSHA1Provider::new()),
0,
true,
false,
)
}
fn entry_key(dirname: &[u8], basename: &[u8], file_id: &[u8]) -> EntryKey {
EntryKey {
dirname: dirname.to_vec(),
basename: basename.to_vec(),
file_id: file_id.to_vec(),
}
}
#[test]
fn mark_modified_no_hash_changes_marks_full_dirblock_state() {
let mut state = fresh_state();
state.dirblock_state = MemoryState::InMemoryUnmodified;
state.mark_modified(&[], false);
assert_eq!(state.dirblock_state, MemoryState::InMemoryModified);
assert_eq!(state.header_state, MemoryState::NotInMemory);
assert!(state.known_hash_changes.is_empty());
}
#[test]
fn mark_modified_hash_only_promotes_unmodified_to_hash_modified() {
let mut state = fresh_state();
state.dirblock_state = MemoryState::InMemoryUnmodified;
let key = entry_key(b"", b"README", b"fid-readme");
state.mark_modified(std::slice::from_ref(&key), false);
assert_eq!(state.dirblock_state, MemoryState::InMemoryHashModified);
assert!(state.known_hash_changes.contains(&key));
}
#[test]
fn mark_modified_hash_only_promotes_not_in_memory_to_hash_modified() {
let mut state = fresh_state();
assert_eq!(state.dirblock_state, MemoryState::NotInMemory);
state.mark_modified(&[entry_key(b"", b"a", b"fid-a")], false);
assert_eq!(state.dirblock_state, MemoryState::InMemoryHashModified);
}
#[test]
fn mark_modified_hash_only_leaves_in_memory_modified_alone() {
let mut state = fresh_state();
state.dirblock_state = MemoryState::InMemoryModified;
state.mark_modified(&[entry_key(b"", b"a", b"fid-a")], false);
assert_eq!(state.dirblock_state, MemoryState::InMemoryModified);
}
#[test]
fn mark_modified_header_flag_promotes_header_state() {
let mut state = fresh_state();
state.header_state = MemoryState::InMemoryUnmodified;
state.mark_modified(&[], true);
assert_eq!(state.header_state, MemoryState::InMemoryModified);
assert_eq!(state.dirblock_state, MemoryState::InMemoryModified);
}
#[test]
fn num_present_parents_subtracts_ghosts() {
let mut state = fresh_state();
state.parents = vec![b"rev-a".to_vec(), b"rev-b".to_vec(), b"rev-c".to_vec()];
state.ghosts = vec![b"rev-b".to_vec()];
assert_eq!(state.num_present_parents(), 2);
}
#[test]
fn num_present_parents_no_parents() {
let state = fresh_state();
assert_eq!(state.num_present_parents(), 0);
}
#[test]
fn num_present_parents_saturates_when_ghosts_exceed_parents() {
let mut state = fresh_state();
state.parents = vec![b"rev-a".to_vec()];
state.ghosts = vec![b"g1".to_vec(), b"g2".to_vec()];
assert_eq!(state.num_present_parents(), 0);
}
#[test]
fn entries_to_current_state_builds_expected_dirblock_layout() {
let mut state = fresh_state();
let nullstat = b"x".repeat(32);
let mk_entry = |dirname: &[u8], basename: &[u8], file_id: &[u8]| Entry {
key: EntryKey {
dirname: dirname.to_vec(),
basename: basename.to_vec(),
file_id: file_id.to_vec(),
},
trees: vec![TreeData {
minikind: Kind::Directory,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: nullstat.clone(),
}],
};
let new_entries = vec![
mk_entry(b"", b"", b"TREE_ROOT"),
mk_entry(b"", b"README", b"fid-readme"),
mk_entry(b"", b"sub", b"fid-sub"),
mk_entry(b"sub", b"inner", b"fid-inner"),
];
state
.entries_to_current_state(new_entries)
.expect("entries_to_current_state");
assert_eq!(state.dirblocks.len(), 3);
assert_eq!(state.dirblocks[0].entries.len(), 1);
assert_eq!(
state.dirblocks[0].entries[0].key.file_id,
b"TREE_ROOT".to_vec()
);
assert_eq!(state.dirblocks[1].entries.len(), 2);
assert_eq!(
state.dirblocks[1].entries[0].key.basename,
b"README".to_vec()
);
assert_eq!(state.dirblocks[1].entries[1].key.basename, b"sub".to_vec());
assert_eq!(state.dirblocks[2].dirname, b"sub".to_vec());
assert_eq!(state.dirblocks[2].entries.len(), 1);
assert_eq!(
state.dirblocks[2].entries[0].key.basename,
b"inner".to_vec()
);
}
#[test]
fn entries_to_current_state_rejects_missing_root_row() {
let mut state = fresh_state();
let entry = Entry {
key: EntryKey {
dirname: b"".to_vec(),
basename: b"README".to_vec(),
file_id: b"fid".to_vec(),
},
trees: vec![tree(Kind::File)],
};
match state.entries_to_current_state(vec![entry]) {
Err(EntriesToStateError::MissingRootRow { key }) => {
assert_eq!(key.basename, b"README".to_vec());
}
other => panic!("expected MissingRootRow, got {:?}", other),
}
}
#[test]
fn entries_to_current_state_rejects_empty_list() {
let mut state = fresh_state();
assert_eq!(
state.entries_to_current_state(Vec::new()),
Err(EntriesToStateError::Empty)
);
}
#[test]
fn ensure_block_root_shortcut_returns_one() {
let mut state = fresh_state();
state.dirblocks = make_dirblocks(vec![]);
assert_eq!(state.ensure_block(0, 0, b""), Ok(1));
assert_eq!(state.dirblocks.len(), 2);
}
#[test]
fn ensure_block_creates_missing_block() {
let mut state = fresh_state();
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"",
b"TREE_ROOT",
vec![tree(Kind::Directory)],
)],
},
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"sub",
b"fid-sub",
vec![tree(Kind::Directory)],
)],
},
];
let idx = state.ensure_block(1, 0, b"sub").expect("ensure");
assert_eq!(idx, 2);
assert_eq!(state.dirblocks.len(), 3);
assert_eq!(state.dirblocks[2].dirname, b"sub".to_vec());
assert!(state.dirblocks[2].entries.is_empty());
}
#[test]
fn ensure_block_idempotent_for_existing_block() {
let mut state = fresh_state();
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"",
b"TREE_ROOT",
vec![tree(Kind::Directory)],
)],
},
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"sub",
b"fid-sub",
vec![tree(Kind::Directory)],
)],
},
Dirblock {
dirname: b"sub".to_vec(),
entries: vec![],
},
];
let idx = state.ensure_block(1, 0, b"sub").expect("ensure");
assert_eq!(idx, 2);
assert_eq!(state.dirblocks.len(), 3);
}
#[test]
fn ensure_block_rejects_bad_dirname() {
let mut state = fresh_state();
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"",
b"TREE_ROOT",
vec![tree(Kind::Directory)],
)],
},
Dirblock {
dirname: Vec::new(),
entries: vec![entry_with_trees(
b"",
b"sub",
b"fid-sub",
vec![tree(Kind::Directory)],
)],
},
];
let err = state.ensure_block(1, 0, b"other").expect_err("bad dirname");
assert_eq!(err, EnsureBlockError::BadDirname(b"other".to_vec()));
assert_eq!(state.dirblocks.len(), 2);
}
#[test]
fn mark_unmodified_resets_everything() {
let mut state = fresh_state();
state.header_state = MemoryState::InMemoryModified;
state.dirblock_state = MemoryState::InMemoryHashModified;
state
.known_hash_changes
.insert(entry_key(b"", b"x", b"fid"));
state.mark_unmodified();
assert_eq!(state.header_state, MemoryState::InMemoryUnmodified);
assert_eq!(state.dirblock_state, MemoryState::InMemoryUnmodified);
assert!(state.known_hash_changes.is_empty());
}
#[test]
fn dirstate_split_root_dirblock_method_wires_through() {
let mut state = DirState::new(
"dirstate",
Box::new(DefaultSHA1Provider::new()),
0,
true,
false,
);
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![
make_entry(b"", b"", b"TREE_ROOT"),
make_entry(b"", b"README", b"fid-readme"),
],
},
Dirblock {
dirname: Vec::new(),
entries: Vec::new(),
},
];
state.split_root_dirblock_into_contents().expect("split");
assert_eq!(state.dirblocks[0].entries.len(), 1);
assert_eq!(state.dirblocks[1].entries.len(), 1);
assert_eq!(
state.dirblocks[1].entries[0].key.basename,
b"README".to_vec()
);
}
#[test]
fn parse_dirblocks_rejects_bad_size() {
let nullstat = b"x".repeat(32);
let mut entry = Vec::new();
entry.extend_from_slice(b"");
entry.push(0);
entry.extend_from_slice(b"");
entry.push(0);
entry.extend_from_slice(b"TREE_ROOT");
entry.push(0);
entry.extend_from_slice(b"d");
entry.push(0);
entry.extend_from_slice(b"");
entry.push(0);
entry.extend_from_slice(b"not-a-number");
entry.push(0);
entry.push(b'n');
entry.push(0);
entry.extend_from_slice(nullstat.as_slice());
let body = make_body_bytes(&[], &[], &[entry]);
match parse_dirblocks(&body, 1, 1) {
Err(DirblocksError::BadSize(bytes)) => {
assert_eq!(bytes, b"not-a-number".to_vec());
}
other => panic!("expected BadSize, got {:?}", other),
}
}
struct MemoryTransport {
contents: Option<Vec<u8>>,
lock: Option<LockState>,
fs: std::collections::HashMap<Vec<u8>, (StatInfo, Option<Vec<u8>>)>,
}
impl MemoryTransport {
fn new() -> Self {
Self {
contents: None,
lock: None,
fs: std::collections::HashMap::new(),
}
}
fn with_contents(bytes: &[u8]) -> Self {
Self {
contents: Some(bytes.to_vec()),
lock: None,
fs: std::collections::HashMap::new(),
}
}
#[allow(dead_code)]
fn set_fs(&mut self, path: &[u8], info: StatInfo, symlink_target: Option<Vec<u8>>) {
self.fs.insert(path.to_vec(), (info, symlink_target));
}
}
impl Transport for MemoryTransport {
fn exists(&self) -> Result<bool, TransportError> {
Ok(self.contents.is_some())
}
fn lock_read(&mut self) -> Result<(), TransportError> {
if self.lock.is_some() {
return Err(TransportError::AlreadyLocked);
}
self.lock = Some(LockState::Read);
Ok(())
}
fn lock_write(&mut self) -> Result<(), TransportError> {
if self.lock.is_some() {
return Err(TransportError::AlreadyLocked);
}
self.lock = Some(LockState::Write);
if self.contents.is_none() {
self.contents = Some(Vec::new());
}
Ok(())
}
fn unlock(&mut self) -> Result<(), TransportError> {
if self.lock.is_none() {
return Err(TransportError::NotLocked);
}
self.lock = None;
Ok(())
}
fn lock_state(&self) -> Option<LockState> {
self.lock
}
fn read_all(&mut self) -> Result<Vec<u8>, TransportError> {
if self.lock.is_none() {
return Err(TransportError::NotLocked);
}
self.contents
.clone()
.ok_or_else(|| TransportError::NotFound("memory".to_string()))
}
fn write_all(&mut self, bytes: &[u8]) -> Result<(), TransportError> {
match self.lock {
Some(LockState::Write) => {}
Some(LockState::Read) => {
return Err(TransportError::Other(
"write_all requires a write lock".to_string(),
));
}
None => return Err(TransportError::NotLocked),
}
self.contents = Some(bytes.to_vec());
Ok(())
}
fn fdatasync(&mut self) -> Result<(), TransportError> {
Ok(())
}
fn lstat(&self, abspath: &[u8]) -> Result<StatInfo, TransportError> {
self.fs
.get(abspath)
.map(|(info, _)| *info)
.ok_or_else(|| TransportError::NotFound(String::from_utf8_lossy(abspath).into_owned()))
}
fn read_link(&self, abspath: &[u8]) -> Result<Vec<u8>, TransportError> {
self.fs
.get(abspath)
.and_then(|(_, link)| link.clone())
.ok_or_else(|| TransportError::NotFound(String::from_utf8_lossy(abspath).into_owned()))
}
fn is_tree_reference_dir(&self, _abspath: &[u8]) -> Result<bool, TransportError> {
Ok(false)
}
fn list_dir(&self, abspath: &[u8]) -> Result<Vec<DirEntryInfo>, TransportError> {
let prefix: Vec<u8> = if abspath.is_empty() {
Vec::new()
} else {
let mut p = abspath.to_vec();
p.push(b'/');
p
};
let mut out = Vec::new();
let mut found_dir = abspath.is_empty();
for (path, (info, link)) in &self.fs {
if path.as_slice() == abspath {
found_dir = true;
continue;
}
if !path.starts_with(&prefix) {
continue;
}
let tail = &path[prefix.len()..];
if tail.iter().any(|&b| b == b'/') {
continue;
}
let kind = if info.is_dir() {
Some(crate::osutils::Kind::Directory)
} else if info.is_file() {
Some(crate::osutils::Kind::File)
} else if info.is_symlink() {
Some(crate::osutils::Kind::Symlink)
} else {
None
};
let _ = link; out.push(DirEntryInfo {
basename: tail.to_vec(),
kind,
stat: *info,
abspath: path.clone(),
});
}
if !found_dir {
return Err(TransportError::NotFound(
String::from_utf8_lossy(abspath).into_owned(),
));
}
Ok(out)
}
}
#[test]
fn transport_exists_reports_contents_presence() {
let empty = MemoryTransport::new();
assert!(!empty.exists().unwrap());
let populated = MemoryTransport::with_contents(b"hi");
assert!(populated.exists().unwrap());
}
#[test]
fn transport_read_all_requires_lock() {
let mut t = MemoryTransport::with_contents(b"hi");
assert_eq!(t.read_all().unwrap_err(), TransportError::NotLocked);
t.lock_read().unwrap();
assert_eq!(t.read_all().unwrap(), b"hi".to_vec());
}
#[test]
fn transport_write_all_requires_write_lock() {
let mut t = MemoryTransport::with_contents(b"hi");
assert_eq!(t.write_all(b"new").unwrap_err(), TransportError::NotLocked);
t.lock_read().unwrap();
assert!(matches!(
t.write_all(b"new").unwrap_err(),
TransportError::Other(_)
));
t.unlock().unwrap();
t.lock_write().unwrap();
t.write_all(b"new").unwrap();
assert_eq!(t.read_all().unwrap(), b"new".to_vec());
}
#[test]
fn transport_write_all_truncates_trailing_bytes() {
let mut t = MemoryTransport::with_contents(b"previous long contents");
t.lock_write().unwrap();
t.write_all(b"short").unwrap();
assert_eq!(t.read_all().unwrap(), b"short".to_vec());
}
#[test]
fn transport_lock_write_creates_missing_file() {
let mut t = MemoryTransport::new();
assert!(!t.exists().unwrap());
t.lock_write().unwrap();
assert!(t.exists().unwrap());
assert_eq!(t.read_all().unwrap(), Vec::<u8>::new());
}
#[test]
fn transport_double_lock_is_error() {
let mut t = MemoryTransport::with_contents(b"");
t.lock_read().unwrap();
assert_eq!(t.lock_read().unwrap_err(), TransportError::AlreadyLocked);
assert_eq!(t.lock_write().unwrap_err(), TransportError::AlreadyLocked);
}
#[test]
fn transport_unlock_without_lock_is_error() {
let mut t = MemoryTransport::with_contents(b"");
assert_eq!(t.unlock().unwrap_err(), TransportError::NotLocked);
}
#[test]
fn transport_lock_state_tracks_current_lock() {
let mut t = MemoryTransport::with_contents(b"");
assert_eq!(t.lock_state(), None);
t.lock_read().unwrap();
assert_eq!(t.lock_state(), Some(LockState::Read));
t.unlock().unwrap();
assert_eq!(t.lock_state(), None);
t.lock_write().unwrap();
assert_eq!(t.lock_state(), Some(LockState::Write));
}
#[test]
fn transport_fdatasync_is_noop_on_memory_transport() {
let mut t = MemoryTransport::with_contents(b"");
t.fdatasync().unwrap();
t.lock_write().unwrap();
t.fdatasync().unwrap();
}
#[test]
fn transport_round_trip_through_get_lines_and_read_header() {
let nullstat = b"x".repeat(32);
let mut state = fresh_state();
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![Entry {
key: EntryKey {
dirname: b"".to_vec(),
basename: b"".to_vec(),
file_id: b"TREE_ROOT".to_vec(),
},
trees: vec![TreeData {
minikind: Kind::Directory,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: nullstat,
}],
}],
},
Dirblock {
dirname: Vec::new(),
entries: Vec::new(),
},
];
let chunks = state.get_lines();
let bytes: Vec<u8> = chunks.into_iter().flatten().collect();
let mut t = MemoryTransport::new();
t.lock_write().unwrap();
t.write_all(&bytes).unwrap();
t.unlock().unwrap();
t.lock_read().unwrap();
let read_back = t.read_all().unwrap();
assert_eq!(read_back, bytes);
let header = read_header(&read_back).expect("header parses");
assert_eq!(header.num_entries, 1);
}
fn minimal_populated_state() -> DirState {
let nullstat = b"x".repeat(32);
let mut state = fresh_state();
state.dirblocks = vec![
Dirblock {
dirname: Vec::new(),
entries: vec![Entry {
key: EntryKey {
dirname: b"".to_vec(),
basename: b"".to_vec(),
file_id: b"TREE_ROOT".to_vec(),
},
trees: vec![TreeData {
minikind: Kind::Directory,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: nullstat,
}],
}],
},
Dirblock {
dirname: Vec::new(),
entries: Vec::new(),
},
];
state
}
#[test]
fn worth_saving_full_dirblock_modification_always_saves() {
let mut state = fresh_state();
state.dirblock_state = MemoryState::InMemoryModified;
assert!(state.worth_saving());
}
#[test]
fn worth_saving_header_modification_always_saves() {
let mut state = fresh_state();
state.header_state = MemoryState::InMemoryModified;
assert!(state.worth_saving());
}
#[test]
fn worth_saving_unmodified_state_is_not_worth_saving() {
let mut state = fresh_state();
state.header_state = MemoryState::InMemoryUnmodified;
state.dirblock_state = MemoryState::InMemoryUnmodified;
assert!(!state.worth_saving());
}
#[test]
fn worth_saving_hash_only_under_limit_is_not_worth_saving() {
let mut state = fresh_state();
state.worth_saving_limit = 5;
state.dirblock_state = MemoryState::InMemoryHashModified;
state
.known_hash_changes
.insert(entry_key(b"", b"a", b"fid-a"));
assert!(!state.worth_saving());
}
#[test]
fn worth_saving_hash_only_at_or_above_limit_saves() {
let mut state = fresh_state();
state.worth_saving_limit = 2;
state.dirblock_state = MemoryState::InMemoryHashModified;
state
.known_hash_changes
.insert(entry_key(b"", b"a", b"fid-a"));
state
.known_hash_changes
.insert(entry_key(b"", b"b", b"fid-b"));
assert!(state.worth_saving());
}
#[test]
fn worth_saving_hash_only_with_negative_limit_never_saves() {
let mut state = fresh_state();
state.worth_saving_limit = -1;
state.dirblock_state = MemoryState::InMemoryHashModified;
for i in 0..10 {
state
.known_hash_changes
.insert(entry_key(b"", &[b'a' + i], b"fid"));
}
assert!(!state.worth_saving());
}
#[test]
fn save_to_writes_get_lines_and_marks_unmodified() {
let mut state = minimal_populated_state();
state.dirblock_state = MemoryState::InMemoryModified;
let expected: Vec<u8> = state.get_lines().into_iter().flatten().collect();
let mut t = MemoryTransport::new();
t.lock_write().unwrap();
let wrote = state.save_to(&mut t).expect("save_to");
assert!(wrote);
assert_eq!(t.read_all().unwrap(), expected);
assert_eq!(state.dirblock_state, MemoryState::InMemoryUnmodified);
assert_eq!(state.header_state, MemoryState::InMemoryUnmodified);
}
#[test]
fn save_to_honours_changes_aborted() {
let mut state = minimal_populated_state();
state.dirblock_state = MemoryState::InMemoryModified;
state.changes_aborted = true;
let mut t = MemoryTransport::new();
t.lock_write().unwrap();
let wrote = state.save_to(&mut t).expect("save_to");
assert!(!wrote);
assert_eq!(t.read_all().unwrap(), Vec::<u8>::new());
assert_eq!(state.dirblock_state, MemoryState::InMemoryModified);
}
#[test]
fn save_to_skips_when_not_worth_saving() {
let mut state = minimal_populated_state();
state.header_state = MemoryState::InMemoryUnmodified;
state.dirblock_state = MemoryState::InMemoryUnmodified;
let mut t = MemoryTransport::new();
t.lock_write().unwrap();
let wrote = state.save_to(&mut t).expect("save_to");
assert!(!wrote);
assert_eq!(t.read_all().unwrap(), Vec::<u8>::new());
}
#[test]
fn set_data_replaces_parents_and_dirblocks_and_marks_modified() {
let mut state = fresh_state();
state.header_state = MemoryState::InMemoryUnmodified;
state.dirblock_state = MemoryState::InMemoryUnmodified;
state.id_index = Some(IdIndex::new());
let new_parents = vec![b"rev-x".to_vec()];
let new_dirblocks = vec![Dirblock {
dirname: b"sub".to_vec(),
entries: Vec::new(),
}];
state.set_data(new_parents.clone(), new_dirblocks.clone());
assert_eq!(state.parents, new_parents);
assert_eq!(state.dirblocks.len(), 1);
assert_eq!(state.dirblocks[0].dirname, b"sub".to_vec());
assert_eq!(state.dirblock_state, MemoryState::InMemoryModified);
assert_eq!(state.header_state, MemoryState::InMemoryModified);
assert!(state.id_index.is_none());
}
#[test]
fn wipe_state_resets_all_fields() {
let mut state = minimal_populated_state();
state.parents = vec![b"rev-a".to_vec(), b"rev-b".to_vec()];
state.ghosts = vec![b"rev-b".to_vec()];
state.header_state = MemoryState::InMemoryModified;
state.dirblock_state = MemoryState::InMemoryModified;
state.changes_aborted = true;
state.end_of_header = Some(42);
state.cutoff_time = Some(123);
let _ = state.get_or_build_id_index();
assert!(state.id_index.is_some());
state.wipe_state();
assert_eq!(state.header_state, MemoryState::NotInMemory);
assert_eq!(state.dirblock_state, MemoryState::NotInMemory);
assert!(!state.changes_aborted);
assert!(state.parents.is_empty());
assert!(state.ghosts.is_empty());
assert!(state.dirblocks.is_empty());
assert!(state.id_index.is_none());
assert!(state.end_of_header.is_none());
assert!(state.cutoff_time.is_none());
}
#[test]
fn save_to_requires_write_lock() {
let mut state = minimal_populated_state();
state.dirblock_state = MemoryState::InMemoryModified;
let mut t = MemoryTransport::new();
assert!(matches!(
state.save_to(&mut t).unwrap_err(),
TransportError::Other(_)
));
t.lock_read().unwrap();
assert!(matches!(
state.save_to(&mut t).unwrap_err(),
TransportError::Other(_)
));
}
fn decode_packed_stat(packed: &str) -> Vec<u8> {
use base64::Engine;
base64::engine::general_purpose::STANDARD_NO_PAD
.decode(packed)
.expect("decode pack_stat output")
}
#[test]
fn pack_stat_zero_inputs_encode_to_24_zero_bytes() {
let packed = pack_stat(0, 0, 0, 0, 0, 0);
assert_eq!(packed, "A".repeat(32));
assert_eq!(decode_packed_stat(&packed), vec![0u8; 24]);
}
#[test]
fn pack_stat_layout_is_big_endian_six_fields() {
let packed = pack_stat(
0x01020304, 0x05060708, 0x090a0b0c, 0x0d0e0f10, 0x11121314, 0x15161718,
);
let bytes = decode_packed_stat(&packed);
assert_eq!(bytes.len(), 24);
assert_eq!(&bytes[0..4], &[0x01, 0x02, 0x03, 0x04]);
assert_eq!(&bytes[4..8], &[0x05, 0x06, 0x07, 0x08]);
assert_eq!(&bytes[8..12], &[0x09, 0x0a, 0x0b, 0x0c]);
assert_eq!(&bytes[12..16], &[0x0d, 0x0e, 0x0f, 0x10]);
assert_eq!(&bytes[16..20], &[0x11, 0x12, 0x13, 0x14]);
assert_eq!(&bytes[20..24], &[0x15, 0x16, 0x17, 0x18]);
}
#[test]
fn pack_stat_truncates_to_low_32_bits() {
let packed_lo = pack_stat(0x0000_0000_DEAD_BEEF, 0, 0, 0, 0, 0);
let packed_hi = pack_stat(0xFFFF_FFFF_DEAD_BEEF, 0, 0, 0, 0, 0);
assert_eq!(packed_lo, packed_hi);
let bytes = decode_packed_stat(&packed_lo);
assert_eq!(&bytes[0..4], &[0xDE, 0xAD, 0xBE, 0xEF]);
}
#[test]
fn pack_stat_all_max_encodes_to_24_ff_bytes() {
let packed = pack_stat(u64::MAX, u64::MAX, u64::MAX, u64::MAX, u64::MAX, u32::MAX);
assert_eq!(decode_packed_stat(&packed), vec![0xFFu8; 24]);
}
#[test]
fn pack_stat_canonical_packed_stat_round_trips() {
let bytes = decode_packed_stat(std::str::from_utf8(PACKED_STAT).unwrap());
let read_be32 = |off: usize| {
((bytes[off] as u64) << 24)
| ((bytes[off + 1] as u64) << 16)
| ((bytes[off + 2] as u64) << 8)
| (bytes[off + 3] as u64)
};
let size = read_be32(0);
let mtime = read_be32(4);
let ctime = read_be32(8);
let dev = read_be32(12);
let ino = read_be32(16);
let mode = read_be32(20) as u32;
let repacked = pack_stat(size, mtime, ctime, dev, ino, mode);
assert_eq!(repacked.as_bytes(), PACKED_STAT);
}
#[test]
fn pack_stat_metadata_round_trips_via_real_filesystem() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("probe");
std::fs::write(&path, b"hello").expect("write probe");
let metadata = std::fs::metadata(&path).expect("metadata");
let packed = pack_stat_metadata(&metadata);
let bytes = decode_packed_stat(&packed);
assert_eq!(bytes.len(), 24);
assert_eq!(&bytes[0..4], &[0, 0, 0, 5]);
}
#[test]
fn stat_to_kind_recognises_directory() {
let dir = tempfile::tempdir().expect("tempdir");
let metadata = std::fs::metadata(dir.path()).expect("metadata");
assert_eq!(stat_to_kind(&metadata), Some(Kind::Directory));
}
#[test]
fn stat_to_kind_recognises_regular_file() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("probe");
std::fs::write(&path, b"x").expect("write probe");
let metadata = std::fs::metadata(&path).expect("metadata");
assert_eq!(stat_to_kind(&metadata), Some(Kind::File));
}
#[test]
#[cfg(unix)]
fn stat_to_kind_recognises_symlink() {
let dir = tempfile::tempdir().expect("tempdir");
let target = dir.path().join("real");
std::fs::write(&target, b"x").expect("write target");
let link = dir.path().join("link");
std::os::unix::fs::symlink(&target, &link).expect("symlink");
let metadata = std::fs::symlink_metadata(&link).expect("metadata");
assert_eq!(stat_to_kind(&metadata), Some(Kind::Symlink));
}
#[test]
fn id_index_get_is_empty_by_default() {
let idx = IdIndex::new();
assert!(idx.get(&FileId::from(&b"missing".to_vec())).is_empty());
}
#[test]
fn id_index_add_and_get_round_trip() {
let mut idx = IdIndex::new();
let fid = FileId::from(&b"fid".to_vec());
idx.add((b"dir", b"name", &fid));
let got = idx.get(&fid);
assert_eq!(got.len(), 1);
assert_eq!(got[0].0, b"dir".to_vec());
assert_eq!(got[0].1, b"name".to_vec());
assert_eq!(got[0].2, fid);
}
#[test]
fn id_index_add_records_duplicate_paths_for_one_id() {
let mut idx = IdIndex::new();
let fid = FileId::from(&b"fid".to_vec());
idx.add((b"old", b"name", &fid));
idx.add((b"new", b"name", &fid));
let got = idx.get(&fid);
assert_eq!(got.len(), 2);
}
#[test]
fn id_index_remove_drops_only_matching_row() {
let mut idx = IdIndex::new();
let fid = FileId::from(&b"fid".to_vec());
idx.add((b"a", b"x", &fid));
idx.add((b"b", b"x", &fid));
idx.remove((b"a", b"x", &fid));
let got = idx.get(&fid);
assert_eq!(got.len(), 1);
assert_eq!(got[0].0, b"b".to_vec());
}
#[test]
fn id_index_iter_all_yields_every_row_across_ids() {
let mut idx = IdIndex::new();
let fid_a = FileId::from(&b"a".to_vec());
let fid_b = FileId::from(&b"b".to_vec());
idx.add((b"d1", b"f1", &fid_a));
idx.add((b"d2", b"f2", &fid_a));
idx.add((b"d3", b"f3", &fid_b));
let count = idx.iter_all().count();
assert_eq!(count, 3);
}
#[test]
fn id_index_file_ids_yields_each_id_once() {
let mut idx = IdIndex::new();
let fid_a = FileId::from(&b"a".to_vec());
let fid_b = FileId::from(&b"b".to_vec());
idx.add((b"d1", b"f", &fid_a));
idx.add((b"d2", b"f", &fid_a));
idx.add((b"d3", b"f", &fid_b));
let mut ids: Vec<_> = idx.file_ids().cloned().collect();
ids.sort();
assert_eq!(ids, vec![fid_a, fid_b]);
}
#[test]
fn id_index_clear_drops_everything() {
let mut idx = IdIndex::new();
let fid = FileId::from(&b"fid".to_vec());
idx.add((b"d", b"f", &fid));
idx.clear();
assert!(idx.get(&fid).is_empty());
assert_eq!(idx.iter_all().count(), 0);
assert_eq!(idx.file_ids().count(), 0);
}
#[test]
fn inv_entry_to_details_root() {
let entry = InventoryEntry::Root {
file_id: FileId::from(&b"TREE_ROOT".to_vec()),
revision: Some(RevisionId::from(b"rev-1".as_ref())),
};
let (kind, fingerprint, size, executable, tree_data) = inv_entry_to_details(&entry);
assert_eq!(kind, Kind::Directory);
assert!(fingerprint.is_empty());
assert_eq!(size, 0);
assert!(!executable);
assert_eq!(tree_data, b"rev-1".to_vec());
}
#[test]
fn inv_entry_to_details_directory_without_revision() {
let entry = InventoryEntry::Directory {
file_id: FileId::from(&b"d".to_vec()),
revision: None,
parent_id: FileId::from(&b"TREE_ROOT".to_vec()),
name: "sub".into(),
};
let (kind, fingerprint, size, executable, tree_data) = inv_entry_to_details(&entry);
assert_eq!(kind, Kind::Directory);
assert!(fingerprint.is_empty());
assert_eq!(size, 0);
assert!(!executable);
assert!(tree_data.is_empty());
}
#[test]
fn inv_entry_to_details_file_with_sha_and_size() {
let entry = InventoryEntry::File {
file_id: FileId::from(&b"f".to_vec()),
revision: Some(RevisionId::from(b"rev-2".as_ref())),
parent_id: FileId::from(&b"TREE_ROOT".to_vec()),
name: "README".into(),
text_sha1: Some(b"deadbeef".to_vec()),
text_size: Some(42),
text_id: None,
executable: true,
};
let (kind, fingerprint, size, executable, tree_data) = inv_entry_to_details(&entry);
assert_eq!(kind, Kind::File);
assert_eq!(fingerprint, b"deadbeef".to_vec());
assert_eq!(size, 42);
assert!(executable);
assert_eq!(tree_data, b"rev-2".to_vec());
}
#[test]
fn inv_entry_to_details_file_with_missing_sha_defaults_to_empty() {
let entry = InventoryEntry::File {
file_id: FileId::from(&b"f".to_vec()),
revision: None,
parent_id: FileId::from(&b"TREE_ROOT".to_vec()),
name: "README".into(),
text_sha1: None,
text_size: None,
text_id: None,
executable: false,
};
let (_, fingerprint, size, executable, _) = inv_entry_to_details(&entry);
assert!(fingerprint.is_empty());
assert_eq!(size, 0);
assert!(!executable);
}
#[test]
fn inv_entry_to_details_link_uses_target_as_fingerprint() {
let entry = InventoryEntry::Link {
file_id: FileId::from(&b"l".to_vec()),
name: "ln".into(),
parent_id: FileId::from(&b"TREE_ROOT".to_vec()),
symlink_target: Some("../target".into()),
revision: Some(RevisionId::from(b"rev-3".as_ref())),
};
let (kind, fingerprint, size, executable, tree_data) = inv_entry_to_details(&entry);
assert_eq!(kind, Kind::Symlink);
assert_eq!(fingerprint, b"../target".to_vec());
assert_eq!(size, 0);
assert!(!executable);
assert_eq!(tree_data, b"rev-3".to_vec());
}
#[test]
fn inv_entry_to_details_link_without_target_defaults_to_empty() {
let entry = InventoryEntry::Link {
file_id: FileId::from(&b"l".to_vec()),
name: "ln".into(),
parent_id: FileId::from(&b"TREE_ROOT".to_vec()),
symlink_target: None,
revision: None,
};
let (_, fingerprint, _, _, tree_data) = inv_entry_to_details(&entry);
assert!(fingerprint.is_empty());
assert!(tree_data.is_empty());
}
#[test]
fn inv_entry_to_details_tree_reference_uses_reference_revision_as_fingerprint() {
let entry = InventoryEntry::TreeReference {
file_id: FileId::from(&b"tr".to_vec()),
revision: Some(RevisionId::from(b"outer".as_ref())),
reference_revision: Some(RevisionId::from(b"inner".as_ref())),
name: "subtree".into(),
parent_id: FileId::from(&b"TREE_ROOT".to_vec()),
};
let (kind, fingerprint, size, executable, tree_data) = inv_entry_to_details(&entry);
assert_eq!(kind, Kind::TreeReference);
assert_eq!(fingerprint, b"inner".to_vec());
assert_eq!(size, 0);
assert!(!executable);
assert_eq!(tree_data, b"outer".to_vec());
}
#[test]
fn fields_per_entry_zero_parents_yields_nine() {
assert_eq!(fields_per_entry(0), 9);
}
#[test]
fn fields_per_entry_grows_by_five_per_parent() {
assert_eq!(fields_per_entry(1), 14);
assert_eq!(fields_per_entry(2), 19);
assert_eq!(fields_per_entry(7), 44);
}
#[test]
fn get_parents_line_empty() {
assert_eq!(get_parents_line(&[]), b"0".to_vec());
}
#[test]
fn get_parents_line_single() {
let parents: &[&[u8]] = &[b"rev-1"];
assert_eq!(get_parents_line(parents), b"1\0rev-1".to_vec());
}
#[test]
fn get_parents_line_multiple() {
let parents: &[&[u8]] = &[b"rev-1", b"rev-2", b"rev-3"];
assert_eq!(
get_parents_line(parents),
b"3\0rev-1\0rev-2\0rev-3".to_vec()
);
}
#[test]
fn get_ghosts_line_empty() {
assert_eq!(get_ghosts_line(&[]), b"0".to_vec());
}
#[test]
fn get_ghosts_line_one() {
let ghosts: &[&[u8]] = &[b"ghost-rev"];
assert_eq!(get_ghosts_line(ghosts), b"1\0ghost-rev".to_vec());
}
#[test]
fn get_output_lines_round_trips_via_read_header() {
let parents_bytes = get_parents_line(&[b"parent-rev".as_slice()]);
let ghosts_bytes = get_ghosts_line(&[b"ghost-rev".as_slice()]);
let chunks = get_output_lines(vec![&parents_bytes, &ghosts_bytes]);
let blob: Vec<u8> = chunks.into_iter().flatten().collect();
let header = read_header(&blob).expect("round-trip header");
assert_eq!(header.parents, vec![b"parent-rev".to_vec()]);
assert_eq!(header.ghosts, vec![b"ghost-rev".to_vec()]);
assert_eq!(header.num_entries, 0);
}
#[test]
fn get_output_lines_emits_format3_banner_first() {
let parents = get_parents_line(&[]);
let ghosts = get_ghosts_line(&[]);
let chunks = get_output_lines(vec![&parents, &ghosts]);
assert_eq!(chunks[0], HEADER_FORMAT_3.to_vec());
assert!(chunks[1].starts_with(b"crc32: "));
assert!(chunks[2].starts_with(b"num_entries: "));
}
#[test]
fn kind_minikind_round_trip() {
for k in [
Kind::Absent,
Kind::File,
Kind::Directory,
Kind::Relocated,
Kind::Symlink,
Kind::TreeReference,
] {
let byte = k.to_minikind();
assert_eq!(Kind::from_minikind(byte).unwrap(), k);
}
}
#[test]
fn kind_minikind_byte_values_match_python() {
assert_eq!(Kind::Absent.to_minikind(), b'a');
assert_eq!(Kind::File.to_minikind(), b'f');
assert_eq!(Kind::Directory.to_minikind(), b'd');
assert_eq!(Kind::Relocated.to_minikind(), b'r');
assert_eq!(Kind::Symlink.to_minikind(), b'l');
assert_eq!(Kind::TreeReference.to_minikind(), b't');
}
#[test]
fn kind_from_minikind_rejects_unknown_byte() {
assert_eq!(Kind::from_minikind(b'x'), Err(b'x'));
assert_eq!(Kind::from_minikind(0), Err(0));
assert_eq!(Kind::from_minikind(0xFF), Err(0xFF));
}
#[test]
fn kind_to_char_matches_minikind() {
assert_eq!(Kind::File.to_char(), 'f');
assert_eq!(Kind::Directory.to_char(), 'd');
assert_eq!(Kind::Symlink.to_char(), 'l');
assert_eq!(Kind::TreeReference.to_char(), 't');
assert_eq!(Kind::Absent.to_char(), 'a');
assert_eq!(Kind::Relocated.to_char(), 'r');
}
#[test]
fn kind_as_str_and_display_match() {
let cases = [
(Kind::Absent, "absent"),
(Kind::File, "file"),
(Kind::Directory, "directory"),
(Kind::Relocated, "relocated"),
(Kind::Symlink, "symlink"),
(Kind::TreeReference, "tree-reference"),
];
for (kind, name) in cases {
assert_eq!(kind.as_str(), name);
assert_eq!(format!("{}", kind), name);
}
}
#[test]
fn kind_is_fdlt_excludes_absent_and_relocated() {
assert!(Kind::File.is_fdlt());
assert!(Kind::Directory.is_fdlt());
assert!(Kind::Symlink.is_fdlt());
assert!(Kind::TreeReference.is_fdlt());
assert!(!Kind::Absent.is_fdlt());
assert!(!Kind::Relocated.is_fdlt());
}
#[test]
fn kind_is_fdltr_excludes_only_absent() {
assert!(Kind::File.is_fdltr());
assert!(Kind::Directory.is_fdltr());
assert!(Kind::Symlink.is_fdltr());
assert!(Kind::TreeReference.is_fdltr());
assert!(Kind::Relocated.is_fdltr());
assert!(!Kind::Absent.is_fdltr());
}
#[test]
fn kind_is_absent_or_relocated_only_those_two() {
assert!(Kind::Absent.is_absent_or_relocated());
assert!(Kind::Relocated.is_absent_or_relocated());
assert!(!Kind::File.is_absent_or_relocated());
assert!(!Kind::Directory.is_absent_or_relocated());
assert!(!Kind::Symlink.is_absent_or_relocated());
assert!(!Kind::TreeReference.is_absent_or_relocated());
}
#[test]
fn kind_to_osutils_kind_maps_real_kinds() {
assert_eq!(
Kind::File.to_osutils_kind(),
Some(crate::osutils::Kind::File)
);
assert_eq!(
Kind::Directory.to_osutils_kind(),
Some(crate::osutils::Kind::Directory)
);
assert_eq!(
Kind::Symlink.to_osutils_kind(),
Some(crate::osutils::Kind::Symlink)
);
assert_eq!(
Kind::TreeReference.to_osutils_kind(),
Some(crate::osutils::Kind::TreeReference)
);
assert_eq!(Kind::Absent.to_osutils_kind(), None);
assert_eq!(Kind::Relocated.to_osutils_kind(), None);
}
#[test]
fn kind_from_osutils_kind_maps_all_four() {
assert_eq!(Kind::from(crate::osutils::Kind::File), Kind::File);
assert_eq!(Kind::from(crate::osutils::Kind::Directory), Kind::Directory);
assert_eq!(Kind::from(crate::osutils::Kind::Symlink), Kind::Symlink);
assert_eq!(
Kind::from(crate::osutils::Kind::TreeReference),
Kind::TreeReference
);
}
#[test]
fn option_kind_is_live_only_for_real_kinds() {
assert!(Some(Kind::File).is_live());
assert!(Some(Kind::Directory).is_live());
assert!(Some(Kind::Symlink).is_live());
assert!(Some(Kind::TreeReference).is_live());
assert!(!Some(Kind::Absent).is_live());
assert!(!Some(Kind::Relocated).is_live());
let none: Option<Kind> = None;
assert!(!none.is_live());
}
#[test]
fn option_kind_is_not_live_is_complement_of_is_live() {
for k in [
Some(Kind::File),
Some(Kind::Directory),
Some(Kind::Symlink),
Some(Kind::TreeReference),
Some(Kind::Absent),
Some(Kind::Relocated),
None,
] {
assert_eq!(k.is_live(), !k.is_not_live());
}
}
fn run_iter_changes(
state: &mut DirState,
transport: &dyn Transport,
want_unversioned: bool,
include_unchanged: bool,
search_specific_files: std::collections::HashSet<Vec<u8>>,
) -> Vec<DirstateChange> {
let mut pstate = ProcessEntryState {
source_index: None,
target_index: 0,
include_unchanged,
want_unversioned,
partial: search_specific_files.iter().any(|p| !p.is_empty()),
supports_tree_reference: false,
root_abspath: Vec::new(),
searched_specific_files: std::collections::HashSet::new(),
search_specific_files,
search_specific_file_parents: std::collections::HashSet::new(),
searched_exact_paths: std::collections::HashSet::new(),
seen_ids: std::collections::HashSet::new(),
new_dirname_to_file_id: std::collections::HashMap::new(),
old_dirname_to_file_id: std::collections::HashMap::new(),
last_source_parent: None,
last_target_parent: None,
};
let mut iter = IterChangesIter::new();
let mut out = Vec::new();
while let Some(change) = state
.iter_changes_next(&mut iter, &mut pstate, transport)
.unwrap()
{
out.push(change);
}
out
}
fn dir_stat() -> StatInfo {
StatInfo {
mode: 0o040755,
size: 0,
mtime: 0,
ctime: 0,
dev: 1,
ino: 1,
}
}
fn file_stat(size: u64, ino: u64) -> StatInfo {
StatInfo {
mode: 0o100644,
size,
mtime: 0,
ctime: 0,
dev: 1,
ino,
}
}
fn versioned_file_row(
state: &mut DirState,
basename: &[u8],
file_id: &[u8],
on_disk_stat: &StatInfo,
fingerprint: &[u8],
) {
let packed_stat = pack_stat(
on_disk_stat.size,
on_disk_stat.mtime as u64,
on_disk_stat.ctime as u64,
on_disk_stat.dev,
on_disk_stat.ino,
on_disk_stat.mode,
)
.into_bytes();
state
.add(
basename,
b"",
basename,
file_id,
crate::osutils::Kind::File,
on_disk_stat.size,
&packed_stat,
fingerprint,
)
.expect("add versioned row");
}
#[test]
fn iter_changes_empty_state_empty_fs_yields_only_root() {
let mut t = MemoryTransport::new();
t.set_fs(b"", dir_stat(), None);
let mut state = add_fixture();
let changes = run_iter_changes(
&mut state,
&t,
false,
false,
std::collections::HashSet::from([Vec::new()]),
);
assert_eq!(
changes.len(),
1,
"expected only the root; got {:?}",
changes
);
assert_eq!(changes[0].file_id, b"TREE_ROOT".to_vec());
assert!(changes[0].new_versioned);
}
#[test]
fn iter_changes_unversioned_file_suppressed_when_want_unversioned_false() {
let mut t = MemoryTransport::new();
t.set_fs(b"", dir_stat(), None);
t.set_fs(b"unv", file_stat(3, 2), None);
let mut state = add_fixture();
let changes = run_iter_changes(
&mut state,
&t,
false,
false,
std::collections::HashSet::from([Vec::new()]),
);
assert!(
!changes.iter().any(|c| !c.new_versioned),
"want_unversioned=false should suppress unversioned entries; got {:?}",
changes
);
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].file_id, b"TREE_ROOT".to_vec());
}
#[test]
fn iter_changes_unversioned_file_emitted_when_want_unversioned_true() {
let mut t = MemoryTransport::new();
t.set_fs(b"", dir_stat(), None);
t.set_fs(b"unv", file_stat(3, 2), None);
let mut state = add_fixture();
let changes = run_iter_changes(
&mut state,
&t,
true,
false,
std::collections::HashSet::from([Vec::new()]),
);
let unversioned: Vec<_> = changes.iter().filter(|c| !c.new_versioned).collect();
assert_eq!(
unversioned.len(),
1,
"expected one unversioned change; got {:?}",
changes
);
assert_eq!(unversioned[0].new_path.as_deref(), Some(b"unv" as &[u8]));
assert!(unversioned[0].file_id.is_empty());
}
#[test]
fn iter_changes_versioned_file_present_emits_added_entry() {
let mut t = MemoryTransport::new();
let f_stat = file_stat(5, 2);
t.set_fs(b"", dir_stat(), None);
t.set_fs(b"a", f_stat, None);
let mut state = add_fixture();
versioned_file_row(&mut state, b"a", b"fid-a", &f_stat, b"sha1");
let changes = run_iter_changes(
&mut state,
&t,
false,
false,
std::collections::HashSet::from([Vec::new()]),
);
let entry = changes
.iter()
.find(|c| c.file_id == b"fid-a")
.unwrap_or_else(|| panic!("expected fid-a change; got {:?}", changes));
assert_eq!(entry.new_path.as_deref(), Some(b"a" as &[u8]));
assert!(entry.new_versioned);
assert!(entry.old_path.is_none());
assert!(!entry.old_versioned);
}
#[test]
fn iter_changes_versioned_file_missing_on_disk_still_emits_entry() {
let mut t = MemoryTransport::new();
t.set_fs(b"", dir_stat(), None);
let saved_stat = file_stat(5, 99);
let mut state = add_fixture();
versioned_file_row(&mut state, b"a", b"fid-a", &saved_stat, b"sha1");
let changes = run_iter_changes(
&mut state,
&t,
false,
false,
std::collections::HashSet::from([Vec::new()]),
);
let entry = changes
.iter()
.find(|c| c.file_id == b"fid-a")
.unwrap_or_else(|| panic!("expected fid-a in changes; got {:?}", changes));
assert!(entry.new_versioned);
assert!(entry.target_kind.is_none());
}
#[test]
fn iter_changes_specific_file_scope_skips_siblings() {
let mut t = MemoryTransport::new();
t.set_fs(b"", dir_stat(), None);
t.set_fs(b"a", file_stat(5, 2), None);
t.set_fs(b"b", file_stat(99, 3), None);
let saved = file_stat(5, 2);
let mut state = add_fixture();
versioned_file_row(&mut state, b"a", b"fid-a", &saved, b"sha1-a");
versioned_file_row(&mut state, b"b", b"fid-b", &saved, b"sha1-b");
let changes = run_iter_changes(
&mut state,
&t,
false,
false,
std::collections::HashSet::from([b"a".to_vec()]),
);
assert!(
!changes.iter().any(|c| c.file_id == b"fid-b"),
"scope b\"a\" should not report b\"b\"; got {:?}",
changes
);
assert!(
changes.iter().any(|c| c.file_id == b"fid-a"),
"expected fid-a to be included in scope; got {:?}",
changes
);
}
#[test]
fn iter_changes_specific_file_scope_with_unknown_path_yields_nothing() {
let mut t = MemoryTransport::new();
t.set_fs(b"", dir_stat(), None);
let mut state = add_fixture();
let changes = run_iter_changes(
&mut state,
&t,
false,
false,
std::collections::HashSet::from([b"missing".to_vec()]),
);
assert!(changes.is_empty(), "expected no changes; got {:?}", changes);
}
#[test]
fn statinfo_is_file_only_for_regular_files() {
let regular = StatInfo {
mode: 0o100644,
size: 0,
mtime: 0,
ctime: 0,
dev: 0,
ino: 0,
};
assert!(regular.is_file());
assert!(!regular.is_dir());
assert!(!regular.is_symlink());
}
#[test]
fn statinfo_is_dir_only_for_directories() {
let dir = StatInfo {
mode: 0o040755,
size: 0,
mtime: 0,
ctime: 0,
dev: 0,
ino: 0,
};
assert!(dir.is_dir());
assert!(!dir.is_file());
assert!(!dir.is_symlink());
}
#[test]
fn statinfo_is_symlink_only_for_symlinks() {
let link = StatInfo {
mode: 0o120777,
size: 0,
mtime: 0,
ctime: 0,
dev: 0,
ino: 0,
};
assert!(link.is_symlink());
assert!(!link.is_file());
assert!(!link.is_dir());
}
#[test]
fn statinfo_is_none_of_three_for_fifo_or_socket() {
let fifo = StatInfo {
mode: 0o010644,
size: 0,
mtime: 0,
ctime: 0,
dev: 0,
ino: 0,
};
assert!(!fifo.is_file());
assert!(!fifo.is_dir());
assert!(!fifo.is_symlink());
}
#[test]
fn transport_error_from_io_not_found_maps_to_not_found_variant() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
let te: TransportError = io_err.into();
assert!(
matches!(te, TransportError::NotFound(_)),
"expected NotFound; got {:?}",
te
);
}
#[test]
fn transport_error_from_io_other_kinds_map_to_io_variant() {
let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
let te: TransportError = io_err.into();
match te {
TransportError::Io { kind, .. } => assert_eq!(kind, std::io::ErrorKind::PermissionDenied),
other => panic!("expected Io; got {:?}", other),
}
}
#[test]
fn transport_error_display_covers_every_variant() {
let cases = [
TransportError::NotFound("p".into()),
TransportError::LockContention("p".into()),
TransportError::NotLocked,
TransportError::AlreadyLocked,
TransportError::Io {
kind: std::io::ErrorKind::Other,
message: "boom".into(),
},
TransportError::Other("x".into()),
];
for err in cases {
let s = format!("{}", err);
assert!(!s.is_empty(), "empty display for {:?}", err);
}
}
#[test]
fn default_sha1_provider_default_and_new_match() {
let _a = DefaultSHA1Provider::new();
let _b = DefaultSHA1Provider;
let _c: DefaultSHA1Provider = Default::default();
}
#[test]
fn default_sha1_provider_sha1_matches_known_value() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("hello");
std::fs::write(&path, b"hello").expect("write");
let provider = DefaultSHA1Provider::new();
let sha = provider.sha1(&path).expect("sha1");
assert_eq!(sha, "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d");
}
#[test]
fn default_sha1_provider_stat_and_sha1_returns_consistent_pair() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("data");
std::fs::write(&path, b"abc").expect("write");
let provider = DefaultSHA1Provider::new();
let (stat, sha) = provider.stat_and_sha1(&path).expect("stat_and_sha1");
assert_eq!(stat.size, 3);
assert!(stat.is_file());
assert_eq!(sha, "a9993e364706816aba3e25717850c26c9cd0d89d");
}
#[test]
fn tree0_minikind_returns_file_for_present_entry() {
let mut state = add_fixture();
versioned_file_row(&mut state, b"a", b"fid-a", &file_stat(5, 2), b"sha");
let key = EntryKey {
dirname: b"".to_vec(),
basename: b"a".to_vec(),
file_id: b"fid-a".to_vec(),
};
assert_eq!(state.tree0_minikind(&key), Some(Kind::File));
}
#[test]
fn tree0_minikind_returns_none_for_missing_entry() {
let state = add_fixture();
let key = EntryKey {
dirname: b"".to_vec(),
basename: b"missing".to_vec(),
file_id: b"missing-id".to_vec(),
};
assert_eq!(state.tree0_minikind(&key), None);
}
#[test]
fn set_tree0_replaces_tree0_when_key_present() {
let mut state = add_fixture();
versioned_file_row(&mut state, b"a", b"fid-a", &file_stat(5, 2), b"sha");
let key = EntryKey {
dirname: b"".to_vec(),
basename: b"a".to_vec(),
file_id: b"fid-a".to_vec(),
};
let new_details = TreeData {
minikind: Kind::Symlink,
fingerprint: b"target".to_vec(),
size: 6,
executable: false,
packed_stat: b"y".repeat(32),
};
state
.set_tree0(&key, new_details.clone())
.expect("set_tree0");
let block = state
.dirblocks
.iter()
.find(|b| {
b.dirname.is_empty() && !b.entries.is_empty() && b.entries[0].key.basename == b"a"
})
.expect("entry block");
let row = block.entries.iter().find(|e| e.key == key).expect("row");
assert_eq!(row.trees[0], new_details);
}
#[test]
fn set_tree0_returns_entry_not_found_when_block_present_but_no_entry() {
let mut state = add_fixture();
let key = EntryKey {
dirname: b"".to_vec(),
basename: b"missing".to_vec(),
file_id: b"x".to_vec(),
};
let details = TreeData {
minikind: Kind::File,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: b"x".repeat(32),
};
let err = state.set_tree0(&key, details).unwrap_err();
assert!(
matches!(err, MakeAbsentError::EntryNotFound { .. }),
"got {:?}",
err
);
}
#[test]
fn set_tree0_returns_block_not_found_when_dirblock_missing() {
let mut state = add_fixture();
let key = EntryKey {
dirname: b"no-such-dir".to_vec(),
basename: b"x".to_vec(),
file_id: b"x".to_vec(),
};
let details = TreeData {
minikind: Kind::File,
fingerprint: Vec::new(),
size: 0,
executable: false,
packed_stat: b"x".repeat(32),
};
let err = state.set_tree0(&key, details).unwrap_err();
assert!(
matches!(err, MakeAbsentError::BlockNotFound { .. }),
"got {:?}",
err
);
}
#[test]
fn compute_sha_cutoff_time_caches_value() {
let mut state = add_fixture();
let first = state.compute_sha_cutoff_time();
assert_eq!(state.cutoff_time, Some(first));
let second = state.compute_sha_cutoff_time();
assert_eq!(state.cutoff_time, Some(second));
assert!(second >= first);
}
#[test]
fn observed_sha1_skips_non_regular_files() {
let mut state = add_fixture();
versioned_file_row(&mut state, b"a", b"fid-a", &file_stat(5, 2), b"sha");
let key = EntryKey {
dirname: b"".to_vec(),
basename: b"a".to_vec(),
file_id: b"fid-a".to_vec(),
};
let result = state
.observed_sha1(&key, b"newsha", 0o040755, 0, 0, 0, 0, 0)
.expect("observed_sha1");
assert!(result.is_none());
}
#[test]
fn observed_sha1_skips_when_stat_falls_inside_uncacheable_window() {
let mut state = add_fixture();
versioned_file_row(&mut state, b"a", b"fid-a", &file_stat(5, 2), b"sha");
let key = EntryKey {
dirname: b"".to_vec(),
basename: b"a".to_vec(),
file_id: b"fid-a".to_vec(),
};
state.cutoff_time = Some(100);
let result = state
.observed_sha1(&key, b"newsha", 0o100644, 5, 200, 200, 1, 2)
.expect("observed_sha1");
assert!(
result.is_none(),
"expected None on racy stat; got {:?}",
result
);
}
#[test]
fn observed_sha1_writes_tree0_for_cacheable_regular_file() {
let mut state = add_fixture();
versioned_file_row(&mut state, b"a", b"fid-a", &file_stat(5, 2), b"oldsha");
let key = EntryKey {
dirname: b"".to_vec(),
basename: b"a".to_vec(),
file_id: b"fid-a".to_vec(),
};
state.cutoff_time = Some(1000);
let result = state
.observed_sha1(&key, b"newsha", 0o100644, 5, 100, 100, 1, 2)
.expect("observed_sha1");
let new_tree0 = result.expect("expected Some(TreeData) on cacheable observe");
assert_eq!(new_tree0.minikind, Kind::File);
assert_eq!(new_tree0.fingerprint, b"newsha".to_vec());
assert_eq!(new_tree0.size, 5);
let block = &state
.dirblocks
.iter()
.find(|b| {
b.dirname.is_empty() && !b.entries.is_empty() && b.entries[0].key.basename == b"a"
})
.unwrap();
let row = &block.entries[0];
assert_eq!(row.trees[0].fingerprint, b"newsha".to_vec());
}
#[test]
fn observed_sha1_returns_entry_not_found_for_unknown_key() {
let mut state = add_fixture();
state.cutoff_time = Some(1000);
let key = EntryKey {
dirname: b"".to_vec(),
basename: b"missing".to_vec(),
file_id: b"x".to_vec(),
};
let err = state
.observed_sha1(&key, b"sha", 0o100644, 0, 100, 100, 1, 2)
.unwrap_err();
assert!(
matches!(err, UpdateEntryError::EntryNotFound),
"got {:?}",
err
);
}
#[test]
fn bootstrap_new_parent_slot_appends_one_absent_tree_per_row() {
let mut state = add_fixture();
versioned_file_row(&mut state, b"a", b"fid-a", &file_stat(5, 2), b"sha");
let pre_lens: Vec<usize> = state
.dirblocks
.iter()
.flat_map(|b| b.entries.iter().map(|e| e.trees.len()))
.collect();
state.bootstrap_new_parent_slot();
let post_lens: Vec<usize> = state
.dirblocks
.iter()
.flat_map(|b| b.entries.iter().map(|e| e.trees.len()))
.collect();
assert_eq!(post_lens.len(), pre_lens.len());
for (pre, post) in pre_lens.iter().zip(post_lens.iter()) {
assert_eq!(*post, pre + 1);
}
for block in &state.dirblocks {
for entry in &block.entries {
let last = entry.trees.last().unwrap();
assert_eq!(last.minikind, Kind::Absent);
assert!(last.fingerprint.is_empty());
}
}
}
#[test]
fn add_path_inserts_entry_using_path_string() {
let mut state = add_fixture();
state
.add_path("a", b"fid-a", crate::osutils::Kind::File, None, b"")
.expect("add_path");
let row = state
.get_entry_by_path(0, b"a")
.expect("entry must exist after add_path");
assert_eq!(row.key.file_id, b"fid-a".to_vec());
assert_eq!(row.trees[0].minikind, Kind::File);
}
#[test]
fn add_path_rejects_dot_basename() {
let mut state = add_fixture();
let err = state
.add_path(".", b"fid", crate::osutils::Kind::File, None, b"")
.unwrap_err();
assert!(
matches!(err, AddError::InvalidEntryName { .. }),
"got {:?}",
err
);
}
#[test]
fn add_path_rejects_dotdot_basename() {
let mut state = add_fixture();
let err = state
.add_path("sub/..", b"fid", crate::osutils::Kind::File, None, b"")
.unwrap_err();
assert!(
matches!(err, AddError::InvalidEntryName { .. }),
"got {:?}",
err
);
}
#[test]
fn apply_removals_makes_entry_absent() {
let mut state = add_fixture();
state
.add_path("a", b"fid-a", crate::osutils::Kind::File, None, b"")
.expect("add_path");
state
.apply_removals(&[(b"fid-a".to_vec(), b"a".to_vec())])
.expect("apply_removals");
let entry = state.get_entry_by_file_id(0, b"fid-a", false);
assert!(
matches!(entry, super::GetEntryResult::NotFound),
"expected NotFound; got {:?}",
entry
);
}
#[test]
fn apply_removals_with_wrong_file_id_returns_invalid() {
let mut state = add_fixture();
state
.add_path("a", b"fid-a", crate::osutils::Kind::File, None, b"")
.expect("add_path");
let err = state
.apply_removals(&[(b"wrong-id".to_vec(), b"a".to_vec())])
.unwrap_err();
assert!(
matches!(err, BasisApplyError::Invalid { .. }),
"got {:?}",
err
);
}
#[test]
fn apply_removals_with_unknown_path_returns_invalid() {
let mut state = add_fixture();
let err = state
.apply_removals(&[(b"fid".to_vec(), b"missing".to_vec())])
.unwrap_err();
assert!(
matches!(err, BasisApplyError::Invalid { .. }),
"got {:?}",
err
);
}
#[test]
fn validate_accepts_clean_fixture() {
let state = add_fixture();
state.validate().expect("clean fixture must validate");
}
#[test]
fn validate_rejects_dirblock_not_starting_with_root() {
let mut state = add_fixture();
state.dirblocks[0].dirname = b"sub".to_vec();
assert!(state.validate().is_err());
}
#[test]
fn iter_changes_iter_default_matches_new() {
let from_new = IterChangesIter::new();
let from_default: IterChangesIter = Default::default();
assert!(from_new.pending.is_empty());
assert!(from_default.pending.is_empty());
assert!(from_new.current_root.is_none());
assert!(from_default.current_root.is_none());
}
#[test]
fn bisect_error_display_covers_every_variant() {
let cases = [
BisectError::ReadError("ouch".into()),
BisectError::TooManySeeks,
BisectError::BadSize("not a number".into()),
BisectError::BadMinikind(b'?'),
];
for err in cases {
let s = format!("{}", err);
assert!(!s.is_empty(), "empty display for {:?}", err);
}
assert_eq!(
format!("{}", BisectError::ReadError("oh no".into())),
"read error: oh no"
);
assert_eq!(format!("{}", BisectError::TooManySeeks), "too many seeks");
assert_eq!(
format!("{}", BisectError::BadSize("xyz".into())),
"bad size field: xyz"
);
assert!(format!("{}", BisectError::BadMinikind(b'q')).contains("invalid minikind"));
}
#[test]
fn bisect_error_implements_std_error() {
fn assert_error<E: std::error::Error>(_: &E) {}
assert_error(&BisectError::TooManySeeks);
}
#[test]
fn header_error_display_covers_every_variant() {
let cases = [
HeaderError::BadFormatLine(b"#bad\n".to_vec()),
HeaderError::MissingCrcLine(b"x\n".to_vec()),
HeaderError::BadCrc(b"abc".to_vec()),
HeaderError::MissingNumEntriesLine(b"x\n".to_vec()),
HeaderError::BadNumEntries(b"abc".to_vec()),
HeaderError::BadParentsLine,
HeaderError::BadGhostsLine,
HeaderError::UnexpectedEof,
];
for err in cases {
let s = format!("{}", err);
assert!(!s.is_empty(), "empty display for {:?}", err);
}
}
#[test]
fn header_error_equality_is_structural() {
assert_eq!(HeaderError::UnexpectedEof, HeaderError::UnexpectedEof);
assert_eq!(
HeaderError::BadCrc(b"x".to_vec()),
HeaderError::BadCrc(b"x".to_vec())
);
assert_ne!(
HeaderError::BadCrc(b"x".to_vec()),
HeaderError::BadCrc(b"y".to_vec())
);
assert_ne!(HeaderError::BadParentsLine, HeaderError::BadGhostsLine);
}
#[test]
fn header_struct_equality_compares_all_fields() {
let a = Header {
crc_expected: 1,
num_entries: 0,
parents: vec![b"p".to_vec()],
ghosts: Vec::new(),
end_of_header: 80,
};
let b = Header {
crc_expected: 1,
num_entries: 0,
parents: vec![b"p".to_vec()],
ghosts: Vec::new(),
end_of_header: 80,
};
assert_eq!(a, b);
let c = Header {
crc_expected: 2,
num_entries: 0,
parents: vec![b"p".to_vec()],
ghosts: Vec::new(),
end_of_header: 80,
};
assert_ne!(a, c);
}
#[test]
fn process_entry_error_display_covers_every_variant() {
let cases = [
ProcessEntryError::DirstateCorrupt("bad".into()),
ProcessEntryError::BadFileKind {
path: b"foo".to_vec(),
mode: 0o010644,
},
ProcessEntryError::Internal("boom".into()),
];
for err in cases {
let s = format!("{}", err);
assert!(!s.is_empty(), "empty display for {:?}", err);
}
let pe = ProcessEntryError::BadFileKind {
path: b"sock".to_vec(),
mode: 0o140644,
};
let s = format!("{}", pe);
assert!(s.contains("bad file kind"), "got {:?}", s);
assert!(s.contains("sock"), "got {:?}", s);
}
#[test]
fn ensure_block_error_display_includes_dirname() {
let s = format!("{}", EnsureBlockError::BadDirname(b"oops".to_vec()));
assert!(s.contains("bad dirname"), "got {:?}", s);
let want = format!("{:?}", b"oops".to_vec());
assert!(s.contains(&want), "got {:?}", s);
}
#[test]
fn split_root_error_display_covers_both_variants() {
let s = format!("{}", SplitRootError::MissingSentinels);
assert!(s.contains("sentinel"), "got {:?}", s);
let s = format!(
"{}",
SplitRootError::BadSecondSentinel {
dirname: b"foo".to_vec(),
entry_count: 3,
}
);
let want = format!("{:?}", b"foo".to_vec());
assert!(s.contains(&want), "got {:?}", s);
assert!(s.contains("3 entries"), "got {:?}", s);
}
#[test]
fn entries_to_state_error_display_covers_every_variant() {
let s = format!("{}", EntriesToStateError::Empty);
assert_eq!(s, "new_entries is empty");
let s = format!(
"{}",
EntriesToStateError::MissingRootRow {
key: EntryKey {
dirname: b"x".to_vec(),
basename: b"y".to_vec(),
file_id: b"z".to_vec(),
}
}
);
assert!(s.contains("Missing root row"));
let s = format!(
"{}",
EntriesToStateError::SplitFailed(SplitRootError::MissingSentinels)
);
assert!(s.contains("split_root_dirblock_into_contents"));
}
#[test]
fn make_absent_error_display_covers_every_variant() {
let key = EntryKey {
dirname: b"".to_vec(),
basename: b"a".to_vec(),
file_id: b"fid".to_vec(),
};
let cases = [
MakeAbsentError::BlockNotFound { key: key.clone() },
MakeAbsentError::EntryNotFound { key: key.clone() },
MakeAbsentError::UpdateBlockNotFound { key: key.clone() },
MakeAbsentError::UpdateEntryNotFound { key: key.clone() },
MakeAbsentError::BadRow { key },
];
for err in cases {
let s = format!("{}", err);
assert!(!s.is_empty(), "empty display for {:?}", err);
}
}
#[test]
fn update_entry_error_display_covers_every_variant() {
let cases = [
UpdateEntryError::EntryNotFound,
UpdateEntryError::UnexpectedKind(Kind::Relocated),
UpdateEntryError::Other("boom".into()),
];
for err in cases {
let s = format!("{}", err);
assert!(s.starts_with("update_entry"), "got {:?}", s);
}
let io_err = UpdateEntryError::Io(std::io::Error::new(std::io::ErrorKind::Other, "boom"));
let s = format!("{}", io_err);
assert!(s.starts_with("update_entry: i/o error"));
}
#[test]
fn set_path_id_error_display_covers_both_variants() {
let s = format!("{}", SetPathIdError::NonRootPath);
assert!(s.contains("only supports the root path"));
let s = format!("{}", SetPathIdError::Internal { reason: "x".into() });
assert!(s.contains("internal error"));
}
#[test]
fn add_error_display_covers_every_variant() {
let cases = [
AddError::DuplicateFileId {
file_id: b"f".to_vec(),
info: "info".into(),
},
AddError::AlreadyAdded {
path: b"a".to_vec(),
},
AddError::NotVersioned {
path: b"a".to_vec(),
},
AddError::AlreadyAddedAssertion {
basename: b"x".to_vec(),
file_id: b"f".to_vec(),
},
AddError::Internal { reason: "r".into() },
AddError::InvalidNormalization { path: "p".into() },
AddError::InvalidEntryName { name: ".".into() },
];
for err in cases {
let s = format!("{}", err);
assert!(!s.is_empty(), "empty display for {:?}", err);
}
}
#[test]
fn basis_apply_error_display_covers_every_variant() {
let cases = [
BasisApplyError::Invalid {
path: b"p".to_vec(),
file_id: b"f".to_vec(),
reason: "r".into(),
},
BasisApplyError::NotImplemented { reason: "r".into() },
BasisApplyError::Internal { reason: "r".into() },
BasisApplyError::NotVersioned {
path: b"p".to_vec(),
},
BasisApplyError::MismatchedEntryFileId {
new_path: b"p".to_vec(),
file_id: b"f".to_vec(),
entry_debug: "Entry".into(),
},
BasisApplyError::NewPathWithoutEntry {
new_path: b"p".to_vec(),
file_id: b"f".to_vec(),
},
];
for err in cases {
let s = format!("{}", err);
assert!(!s.is_empty(), "empty display for {:?}", err);
}
}
#[test]
fn validate_error_display_passes_message_through() {
let err = ValidateError("dirblocks broken".into());
assert_eq!(format!("{}", err), "dirblocks broken");
}
#[test]
fn errors_implement_std_error() {
fn assert_error<E: std::error::Error>(_: &E) {}
assert_error(&MakeAbsentError::BadRow {
key: EntryKey {
dirname: Vec::new(),
basename: Vec::new(),
file_id: Vec::new(),
},
});
assert_error(&SplitRootError::MissingSentinels);
assert_error(&UpdateEntryError::EntryNotFound);
assert_error(&SetPathIdError::NonRootPath);
assert_error(&AddError::AlreadyAdded { path: Vec::new() });
assert_error(&BasisApplyError::NotVersioned { path: Vec::new() });
assert_error(&ValidateError("x".into()));
assert_error(&EnsureBlockError::BadDirname(Vec::new()));
assert_error(&EntriesToStateError::Empty);
assert_error(&HeaderError::UnexpectedEof);
assert_error(&ProcessEntryError::Internal("x".into()));
}