Files
RedBear-OS/src/tests.rs
T

1105 lines
42 KiB
Rust

use crate::{
htree::{HTreeHash, HTreeNode, HTreePtr, HTREE_IDX_ENTRIES},
transaction::{level_data, level_data_mut, FsCtx},
BlockAddr, BlockData, BlockMeta, BlockPtr, DirEntry, DirList, DiskMemory, DiskSparse,
FileSystem, Node, TreePtr, ALLOC_GC_THRESHOLD, BLOCK_SIZE,
};
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering::Relaxed;
use std::{fs, time};
static IMAGE_SEQ: AtomicUsize = AtomicUsize::new(0);
fn with_redoxfs<T, F>(callback: F) -> T
where
T: Send + Sync + 'static,
F: FnOnce(FileSystem<DiskSparse>) -> T + Send + Sync + 'static,
{
let disk_path = format!("image{}.bin", IMAGE_SEQ.fetch_add(1, Relaxed));
let res = {
let disk = DiskSparse::create(dbg!(&disk_path), 1024 * 1024 * 1024).unwrap();
let ctime = dbg!(time::SystemTime::now().duration_since(time::UNIX_EPOCH)).unwrap();
let fs = FileSystem::create(disk, None, ctime.as_secs(), ctime.subsec_nanos()).unwrap();
callback(fs)
};
dbg!(fs::remove_file(dbg!(disk_path))).unwrap();
res
}
#[test]
fn many_create_remove_should_not_increase_size() {
with_redoxfs(|mut fs| {
let initially_free = fs.allocator().free();
let tree_ptr = TreePtr::<Node>::root();
let name = "test";
// Iterate over 255 times to prove deleted files don't retain space within the node tree
// Iterate to an ALLOC_GC_THRESHOLD boundary to ensure the allocator GC reclaims space
let start = fs.header.generation.to_ne();
let end = start + ALLOC_GC_THRESHOLD;
let end = end - (end % ALLOC_GC_THRESHOLD) + 1 + ALLOC_GC_THRESHOLD;
for i in start..end {
let _ = fs
.tx(|tx| {
tx.create_node(
tree_ptr,
&format!("{}{}", name, i),
Node::MODE_FILE | 0o644,
1,
0,
)?;
tx.remove_node(tree_ptr, &format!("{}{}", name, i), Node::MODE_FILE)
})
.unwrap();
}
// Any value greater than 0 indicates a storage leak
let diff = initially_free - fs.allocator().free();
assert_eq!(diff, 0);
});
}
#[test]
fn many_create_then_many_remove_should_not_increase_size() {
with_redoxfs(|mut fs| {
let tree_ptr = TreePtr::<Node>::root();
let initially_free = fs.allocator().free();
let initial_size = fs.tx(|tx| tx.read_tree(tree_ptr)).unwrap().data().size();
let end = 3000;
for i in 0..end {
let _ = fs
.tx(|tx| {
tx.create_node(
tree_ptr,
&format!("test{}", i),
Node::MODE_FILE | 0o644,
1,
0,
)
})
.unwrap();
}
for i in 0..end {
let result =
fs.tx(|tx| tx.remove_node(tree_ptr, &format!("test{}", i), Node::MODE_FILE));
if result.is_err() {
println!("Failed to delete on iteration {i}");
}
result.unwrap();
}
let final_size = fs.tx(|tx| tx.read_tree(tree_ptr)).unwrap().data().size();
assert_eq!(initial_size, final_size);
// Any value greater than 0 indicates a storage leak
let _ = fs.tx(|tx| tx.sync(true));
let diff = initially_free - fs.allocator().free();
assert_eq!(diff, 0);
});
}
#[test]
fn empty_dir() {
with_redoxfs(|mut fs| {
let root_ptr = TreePtr::root();
let empty_dir = fs
.tx(|tx| tx.create_node(root_ptr, "my_dir", Node::MODE_DIR, 1, 0))
.unwrap();
// List
let mut children = Vec::<DirEntry>::new();
fs.tx(|tx| tx.child_nodes(empty_dir.ptr(), &mut children))
.unwrap();
assert_eq!(children.len(), 0);
// Find
let error = fs.tx(|tx| tx.find_node(empty_dir.ptr(), "does_not_exist"));
assert!(error.is_err());
assert_eq!(error.unwrap_err().errno, syscall::error::ENOENT);
// Remove
let error = fs.tx(|tx| tx.remove_node(empty_dir.ptr(), "does_not_exist", Node::MODE_FILE));
assert!(error.is_err());
assert_eq!(error.unwrap_err().errno, syscall::error::ENOENT);
})
}
// TODO: When increasing the total_count to 8000, the Allocator's deallocate() function surfaces as "slow" according to flamegraph. This
// appears to be the result of bulk deleting in this test, but I would bet that any filesystem that has lived for a long time would
// start to see degraded performance due to this.
#[test]
fn many_create_write_list_find_read_delete() {
let disk = DiskMemory::new(1024 * 1024 * 1024);
let ctime = time::SystemTime::now()
.duration_since(time::UNIX_EPOCH)
.unwrap();
let mut fs = FileSystem::create(disk, None, ctime.as_secs(), ctime.subsec_nanos()).unwrap();
let tree_ptr = TreePtr::<Node>::root();
let total_count = 3000;
// Create a bunch of files
for i in 0..total_count {
let result = fs.tx(|tx| {
tx.create_node(
tree_ptr,
&format!("file{i:05}"),
Node::MODE_FILE | 0o644,
1,
0,
)
});
if result.is_err() {
println!("Failure on create iteration {i}");
}
let file_node = result.unwrap();
let result = fs.tx(|tx| {
tx.write_node(
file_node.ptr(),
0,
format!("Hello World! #{i}").as_bytes(),
ctime.as_secs(),
ctime.subsec_nanos(),
)
});
if result.is_err() {
println!("Failure on write iteration {i}");
}
assert!(result.unwrap() > 0)
}
// Confirm that they can be listed
{
let mut children = Vec::<DirEntry>::with_capacity(total_count);
fs.tx(|tx| tx.child_nodes(tree_ptr, &mut children)).unwrap();
assert_eq!(
children.len(),
total_count,
"The list of children should match the number of files created."
);
let mut children: Vec<String> = children
.iter()
.map(|entry| entry.name().unwrap_or_default().to_string())
.collect();
children.sort();
for i in 0..total_count {
let expected = format!("file{i:05}");
let idx = children.binary_search(&expected);
assert!(idx.is_ok(), "Children did not contain '{}'", expected);
}
}
// Find and read the files
for i in 0..total_count {
let result = fs.tx(|tx| tx.find_node(tree_ptr, &format!("file{i:05}")));
if result.is_err() {
println!("Failure on find node iteration {i}");
}
let file_node = result.unwrap();
let offset = 0;
let mut buf = [0_u8; 32];
let result = fs.tx(|tx| {
tx.read_node(
file_node.ptr(),
offset,
&mut buf,
ctime.as_secs(),
ctime.subsec_nanos(),
)
});
if result.is_err() {
println!("Failure on read iteration {i}");
}
let size = result.unwrap();
let body = std::str::from_utf8(&buf[..size]).unwrap();
assert_eq!(body, format!("Hello World! #{i}"));
}
// Delete all the files
for i in 0..total_count {
let file_name = format!("file{i:05}");
if let Err(e) = fs.tx(|tx| tx.remove_node(tree_ptr, &file_name, Node::MODE_FILE)) {
println!("Failure on delete iteration {i}");
panic!("{e}");
}
let result = fs.tx(|tx| tx.find_node(tree_ptr, &file_name));
if result.is_ok() || result.unwrap_err().errno != syscall::error::ENOENT {
println!("Failure on delete verification iteration {i}");
panic!("Deletion appears to have failed");
}
}
}
//
// MARK: H-Tree tests
//
// Note that most of these tests use a test specific HTreeHash implementation that will simply parse the numeric
// value after two underscores in the name. So a name of `my_file__10` would have a HTreeHash value of 10. This
// allows for some explicit placement of test values into the H-tree.
//
/// Create an unnaturally narrow but deep H-tree structure for efficient testing of the internal
/// algorithms used to change the H-tree state.
fn create_minimal_l2_htree(
child1_name: &str,
mut fs: FileSystem<DiskSparse>,
) -> (FileSystem<DiskSparse>, TreePtr<Node>) {
let parent_ptr = TreePtr::<Node>::root();
let child_ptr = fs
.tx(|tx| {
let mut parent = tx.read_tree(parent_ptr).unwrap();
let child1_block_data = BlockData::new(
unsafe { tx.allocate(&mut FsCtx, BlockMeta::default()) }.unwrap(),
Node::new(
Node::MODE_FILE,
parent.data().uid(),
parent.data().gid(),
1,
0,
),
);
let child1_block_ptr = unsafe { tx.write_block(child1_block_data) }.unwrap();
let child1_ptr = tx.insert_tree(child1_block_ptr).unwrap();
let child1_dir_entry = DirEntry::new(child1_ptr, child1_name);
let child1_htree_hash = HTreeHash::from_name(child1_name);
let mut dir_list = BlockData::<DirList>::empty(BlockAddr::default()).unwrap();
dir_list.data_mut().append(&child1_dir_entry);
let dir_ptr = tx.sync_block(&mut parent, dir_list).unwrap();
let mut l1 = BlockData::<HTreeNode<DirList>>::empty(BlockAddr::default()).unwrap();
l1.data_mut().ptrs[0] = HTreePtr::new(child1_htree_hash, dir_ptr);
let l1_ptr = tx.sync_block(&mut parent, l1).unwrap();
let mut l2 =
BlockData::<HTreeNode<HTreeNode<DirList>>>::empty(BlockAddr::default()).unwrap();
l2.data_mut().ptrs[0] = HTreePtr::new(child1_htree_hash, l1_ptr);
let l2_ptr = tx.sync_block(&mut parent, l2).unwrap();
let l2_ptr = unsafe { l2_ptr.cast() };
level_data_mut(&mut parent)?.level0[0] = BlockPtr::marker(2);
level_data_mut(&mut parent)?.level0[1] = l2_ptr;
let size = parent.data().size() + BLOCK_SIZE * 4;
parent.data_mut().size = size.into();
tx.sync_tree(parent).unwrap();
Ok(child1_ptr)
})
.unwrap();
(fs, child_ptr)
}
#[test]
fn insert_dir_entry_without_hash_change() {
with_redoxfs(|fs| {
let parent_ptr = TreePtr::<Node>::root();
// GIVEN a directory with H-Tree populated to level 2 and a new entry that lands
// in the last existing DirList, but the hash sorts lower than the max hash in the DirList
let child1_name = "child1__9";
let child2_name = "child2__1";
let child1_htree_hash = HTreeHash::from_name(child1_name);
let (mut fs, child1_ptr) = create_minimal_l2_htree(child1_name, fs);
let _ = fs.tx(|tx| {
// WHEN the new child node is added to the parent directory
let child2_node = tx
.create_node(parent_ptr, child2_name, Node::MODE_FILE, 2, 0)
.unwrap();
// THEN the child node is added, but the H-Tree retains its structure, and the updated nodes retain
// the old HTreeHash value
let parent = tx.read_tree(parent_ptr).unwrap();
assert!(level_data(&parent)?.level0[0].is_marker());
assert_eq!(level_data(&parent)?.level0[0].addr().level().0, 2);
let l2_ptr = unsafe { level_data(&parent)?.level0[1].cast() };
let l2: BlockData<HTreeNode<HTreeNode<DirList>>> = tx.read_block(l2_ptr).unwrap();
let l1_ptr = l2.data().ptrs[0];
let l1 = tx.read_block(l1_ptr.ptr).unwrap();
assert_eq!(l1_ptr.htree_hash, child1_htree_hash);
let dir_list_ptr = l1.data().ptrs[0];
let dir_list = tx.read_block(dir_list_ptr.ptr).unwrap();
assert_eq!(dir_list_ptr.htree_hash, child1_htree_hash);
let mut entries: Vec<String> = dir_list
.data()
.entries()
.map(|e| e.name().unwrap().to_string())
.collect();
entries.sort();
assert_eq!(entries.len(), 2);
assert_eq!(entries, vec![child1_name, child2_name]);
// Validate listing child_nodes works
let mut children = Vec::new();
tx.child_nodes(parent_ptr, &mut children).unwrap();
let mut children: Vec<&str> = children.iter().map(|e| e.name().unwrap()).collect();
children.sort();
assert_eq!(children, entries);
// Validate find_node works
assert_eq!(
tx.find_node(parent_ptr, child1_name).unwrap().ptr().id(),
child1_ptr.id()
);
assert_eq!(
tx.find_node(parent_ptr, child2_name).unwrap().ptr().id(),
child2_node.ptr().id()
);
// WHEN the new child node is removed from the parent directory
tx.remove_node(parent_ptr, child2_name, Node::MODE_FILE)
.unwrap();
// THEN the child node is removed, the H-Tree retains its structure, and the updated nodes retain
// the old HTreeHash value
let parent = tx.read_tree(parent_ptr).unwrap();
assert!(level_data(&parent)?.level0[0].is_marker());
assert_eq!(level_data(&parent)?.level0[0].addr().level().0, 2);
let l2_ptr = unsafe { level_data(&parent)?.level0[1].cast() };
let l2: BlockData<HTreeNode<HTreeNode<DirList>>> = tx.read_block(l2_ptr).unwrap();
let l1_ptr = l2.data().ptrs[0];
let l1 = tx.read_block(l1_ptr.ptr).unwrap();
assert_eq!(l1_ptr.htree_hash, child1_htree_hash);
let dir_list_ptr = l1.data().ptrs[0];
let dir_list = tx.read_block(dir_list_ptr.ptr).unwrap();
assert_eq!(dir_list_ptr.htree_hash, child1_htree_hash);
let entries: Vec<String> = dir_list
.data()
.entries()
.map(|e| e.name().unwrap().to_string())
.collect();
assert_eq!(entries.len(), 1);
assert_eq!(entries, vec![child1_name]);
// Validate listing child_nodes works
let mut children = Vec::new();
tx.child_nodes(parent_ptr, &mut children).unwrap();
let children: Vec<&str> = children.iter().map(|e| e.name().unwrap()).collect();
assert_eq!(children, entries);
// Validate find_node works
assert_eq!(
tx.find_node(parent_ptr, child1_name).unwrap().ptr().id(),
child1_ptr.id()
);
assert_eq!(
tx.find_node(parent_ptr, child2_name).unwrap_err().errno,
syscall::error::ENOENT
);
Ok(())
});
});
}
#[test]
fn insert_dir_entry_with_hash_change() {
with_redoxfs(|fs| {
let parent_ptr = TreePtr::<Node>::root();
// GIVEN a directory with H-Tree populated to level 2 and a new entry that lands
// in the last existing DirList, and the hash is sorted after the max hash in the DirList
let child1_name = "child1__1";
let child2_name = "child2__9";
let (mut fs, child1_ptr) = create_minimal_l2_htree(child1_name, fs);
let _ = fs.tx(|tx| {
// WHEN the new child node is added to the parent directory
let child2_node = tx
.create_node(parent_ptr, child2_name, Node::MODE_FILE, 2, 0)
.unwrap();
// THEN the child node is added, the H-Tree retains its structure, and the updated nodes adopt
// the new HTreeHash value
let child2_htree_hash = HTreeHash::from_name(child2_name);
let parent = tx.read_tree(parent_ptr).unwrap();
assert!(level_data(&parent)?.level0[0].is_marker());
assert_eq!(level_data(&parent)?.level0[0].addr().level().0, 2);
let l2_ptr = unsafe { level_data(&parent)?.level0[1].cast() };
let l2: BlockData<HTreeNode<HTreeNode<DirList>>> = tx.read_block(l2_ptr).unwrap();
let l1_ptr = l2.data().ptrs[0];
let l1 = tx.read_block(l1_ptr.ptr).unwrap();
assert_eq!(l1_ptr.htree_hash, child2_htree_hash);
let dir_list_ptr = l1.data().ptrs[0];
let dir_list = tx.read_block(dir_list_ptr.ptr).unwrap();
assert_eq!(dir_list_ptr.htree_hash, child2_htree_hash);
let mut entries: Vec<String> = dir_list
.data()
.entries()
.map(|e| e.name().unwrap().to_string())
.collect();
entries.sort();
assert_eq!(entries.len(), 2);
assert_eq!(entries, vec![child1_name, child2_name]);
// Validate listing child_nodes works
let mut children = Vec::new();
tx.child_nodes(parent_ptr, &mut children).unwrap();
let mut children: Vec<&str> = children.iter().map(|e| e.name().unwrap()).collect();
children.sort();
assert_eq!(children, entries);
// Validate find_node works
assert_eq!(
tx.find_node(parent_ptr, child1_name).unwrap().ptr().id(),
child1_ptr.id()
);
assert_eq!(
tx.find_node(parent_ptr, child2_name).unwrap().ptr().id(),
child2_node.ptr().id()
);
// WHEN the new child node is removed from the parent directory
tx.remove_node(parent_ptr, child2_name, Node::MODE_FILE)
.unwrap();
// THEN the child node is removed, the H-Tree retains its structure, and the updated nodes revert
// to child1's HTreeHash value
let child1_htree_hash = HTreeHash::from_name(child1_name);
let parent = tx.read_tree(parent_ptr).unwrap();
assert!(level_data(&parent)?.level0[0].is_marker());
assert_eq!(level_data(&parent)?.level0[0].addr().level().0, 2);
let l2_ptr = unsafe { level_data(&parent)?.level0[1].cast() };
let l2: BlockData<HTreeNode<HTreeNode<DirList>>> = tx.read_block(l2_ptr).unwrap();
let l1_ptr = l2.data().ptrs[0];
let l1 = tx.read_block(l1_ptr.ptr).unwrap();
assert_eq!(l1_ptr.htree_hash, child1_htree_hash);
let dir_list_ptr = l1.data().ptrs[0];
let dir_list = tx.read_block(dir_list_ptr.ptr).unwrap();
assert_eq!(dir_list_ptr.htree_hash, child1_htree_hash);
let entries: Vec<String> = dir_list
.data()
.entries()
.map(|e| e.name().unwrap().to_string())
.collect();
assert_eq!(entries.len(), 1);
assert_eq!(entries, vec![child1_name]);
// Validate listing child_nodes works
let mut children = Vec::new();
tx.child_nodes(parent_ptr, &mut children).unwrap();
let children: Vec<&str> = children.iter().map(|e| e.name().unwrap()).collect();
assert_eq!(children, entries);
// Validate find_node works
assert_eq!(
tx.find_node(parent_ptr, child1_name).unwrap().ptr().id(),
child1_ptr.id()
);
assert_eq!(
tx.find_node(parent_ptr, child2_name).unwrap_err().errno,
syscall::error::ENOENT
);
Ok(())
});
});
}
#[test]
fn delete_to_empty() {
with_redoxfs(|fs| {
let parent_ptr = TreePtr::<Node>::root();
// GIVEN a nearly empty tree
let child_name = "child1__9";
let (mut fs, _child_ptr) = create_minimal_l2_htree(child_name, fs);
// WHEN the last directory entry is removed
fs.tx(|tx| tx.remove_node(parent_ptr, child_name, Node::MODE_FILE))
.unwrap();
// THEN the directory entry is removed, as are all the H-tree nodes
fs.tx(|tx| {
assert_eq!(
tx.find_node(parent_ptr, child_name).unwrap_err().errno,
syscall::error::ENOENT
);
let parent = tx.read_tree(parent_ptr).unwrap();
assert!(!level_data(&parent)?.level0[0].is_marker());
assert!(level_data(&parent)?.level0[0].addr().is_null());
Ok(())
})
.unwrap();
});
}
#[test]
fn split_htree_level0_to_level1() {
with_redoxfs(|mut fs| {
let parent_ptr = TreePtr::<Node>::root();
// GIVEN a full root DirList
fs.tx(|tx| {
for i in 0..16 {
let child_name = format!("child__{i:0243}");
tx.create_node(parent_ptr, child_name.as_str(), Node::MODE_FILE, 1, 0)
.unwrap();
}
// Confirm preconditions: the level 0 is full of the expected entries.
let parent = tx.read_tree(parent_ptr).unwrap();
assert!(level_data(&parent)?.level0[0].is_marker());
assert_eq!(level_data(&parent)?.level0[0].addr().level().0, 0);
assert!(!level_data(&parent)?.level0[0].addr().is_null());
let dir_ptr: BlockPtr<DirList> = unsafe { level_data(&parent)?.level0[1].cast() };
let dir_list = tx.read_block(dir_ptr).unwrap();
for (i, entry) in dir_list.data().entries().enumerate() {
assert_eq!(entry.name().unwrap(), format!("child__{i:0243}"));
}
Ok(())
})
.unwrap();
// WHEN one more entry is added
fs.tx(|tx| {
tx.create_node(
parent_ptr,
format!("child__{:0243}", 16).as_str(),
Node::MODE_FILE,
1,
0,
)
})
.unwrap();
// THEN the level is increased and the DirList is split
fs.tx(|tx| {
let parent = tx.read_tree(parent_ptr).unwrap();
assert!(level_data(&parent)?.level0[0].is_marker());
assert_eq!(level_data(&parent)?.level0[0].addr().level().0, 1);
assert!(!level_data(&parent)?.level0[1].addr().is_null());
let htree_ptr: BlockPtr<HTreeNode<DirList>> =
unsafe { level_data(&parent)?.level0[1].cast() };
let htree_node = tx.read_block(htree_ptr).unwrap();
assert!(!htree_node.data().ptrs[0].is_null());
assert_eq!(
htree_node.data().ptrs[0].htree_hash,
HTreeHash::from_name(format!("child__{:0243}", 7).as_str())
);
assert!(!htree_node.data().ptrs[1].is_null());
assert_eq!(
htree_node.data().ptrs[1].htree_hash,
HTreeHash::from_name(format!("child__{:0243}", 16).as_str())
);
assert!(htree_node.data().ptrs[2].is_null());
let dir_list1 = tx.read_block(htree_node.data().ptrs[0].ptr).unwrap();
let dir_list2 = tx.read_block(htree_node.data().ptrs[1].ptr).unwrap();
assert_eq!(dir_list1.data().entry_count(), 8);
assert_eq!(dir_list2.data().entry_count(), 9);
for (i, entry) in dir_list1.data().entries().enumerate() {
assert_eq!(entry.name().unwrap(), format!("child__{i:0243}"));
}
for (i, entry) in dir_list2.data().entries().enumerate() {
let i = i + dir_list1.data().entry_count();
assert_eq!(entry.name().unwrap(), format!("child__{i:0243}"));
}
Ok(())
})
.unwrap();
// WHEN all entries in the first split are removed
fs.tx(|tx| {
for i in 0..8 {
tx.remove_node(
parent_ptr,
format!("child__{i:0243}").as_str(),
Node::MODE_FILE,
)
.unwrap();
}
Ok(())
})
.unwrap();
// THEN only the other split remains
fs.tx(|tx| {
let parent = tx.read_tree(parent_ptr).unwrap();
assert!(level_data(&parent)?.level0[0].is_marker());
assert_eq!(level_data(&parent)?.level0[0].addr().level().0, 1);
assert!(!level_data(&parent)?.level0[1].addr().is_null());
let htree_ptr: BlockPtr<HTreeNode<DirList>> =
unsafe { level_data(&parent)?.level0[1].cast() };
let htree_node = tx.read_block(htree_ptr).unwrap();
assert!(!htree_node.data().ptrs[0].is_null());
assert_eq!(
htree_node.data().ptrs[0].htree_hash,
HTreeHash::from_name(format!("child__{:0243}", 16).as_str())
);
assert!(htree_node.data().ptrs[1].is_null());
Ok(())
})
.unwrap();
// WHEN all entries in the second split are removed
fs.tx(|tx| {
for i in 8..17 {
let name = format!("child__{i:0243}");
let result = tx.remove_node(parent_ptr, name.as_str(), Node::MODE_FILE);
result.unwrap_or_else(|e| {
panic!(
"Failed to remove file {name} with hash {:?} error {:?}",
HTreeHash::from_name(&name),
e
)
});
}
Ok(())
})
.unwrap();
// THEN the level1 is collapsed back to an empty state
fs.tx(|tx| {
let parent = tx.read_tree(parent_ptr).unwrap();
assert!(!level_data(&parent)?.level0[0].is_marker());
assert!(level_data(&parent)?.level0[1].is_null());
Ok(())
})
.unwrap();
});
}
#[test]
fn split_htree_with_multiple_levels() {
with_redoxfs(|fs| {
let parent_ptr = TreePtr::<Node>::root();
let (mut fs, _) = create_minimal_l2_htree(format!("child__{:0243}", 1000).as_str(), fs);
// GIVEN a full root leaf node (DirList) with a full H-tree branch
fs.tx(|tx| {
for i in 1..16 {
let i = i + 1000;
let child_name = format!("child__{i:0243}");
tx.create_node(parent_ptr, child_name.as_str(), Node::MODE_FILE, 1, 0)
.unwrap();
}
// Confirm preconditions: the level 0 is full of the expected entries.
let mut parent = tx.read_tree(parent_ptr).unwrap();
assert!(level_data(&parent)?.level0[0].is_marker());
assert_eq!(level_data(&parent)?.level0[0].addr().level().0, 2);
let l2_ptr: BlockPtr<HTreeNode<HTreeNode<DirList>>> =
unsafe { level_data(&parent)?.level0[1].cast() };
let mut l2_node = tx.read_block(l2_ptr).unwrap();
for i in 0..HTREE_IDX_ENTRIES {
if i == 0 {
assert!(!l2_node.data().ptrs[i].is_null());
} else {
assert!(l2_node.data().ptrs[i].is_null());
l2_node.data_mut().ptrs[i] = HTreePtr::new(HTreeHash::MAX, BlockPtr::marker(15))
}
}
let l1_ptr = l2_node.data().ptrs[0];
let mut l1_node = tx.read_block(l1_ptr.ptr).unwrap();
for i in 0..HTREE_IDX_ENTRIES {
if i == 0 {
assert!(!l1_node.data().ptrs[i].is_null());
} else {
assert!(l1_node.data().ptrs[i].is_null());
l1_node.data_mut().ptrs[i] = HTreePtr::new(HTreeHash::MAX, BlockPtr::marker(15))
}
}
l2_node.data_mut().ptrs[0].ptr = unsafe { tx.write_block(l1_node) }.unwrap();
let l2_record_ptr = unsafe { tx.write_block(l2_node) }.unwrap();
level_data_mut(&mut parent)?.level0[1] = unsafe { l2_record_ptr.cast() };
tx.sync_tree(parent).unwrap();
Ok(())
})
.unwrap();
// WHEN another entry is added to the full DirList
fs.tx(|tx| {
tx.create_node(
parent_ptr,
format!("child__{:0243}", 1).as_str(),
Node::MODE_FILE,
1,
0,
)
})
.unwrap();
// THEN the branch splits all the way to the root, increasing the level
fs.tx(|tx| {
let parent = tx.read_tree(parent_ptr).unwrap();
assert!(level_data(&parent)?.level0[0].is_marker());
assert_eq!(level_data(&parent)?.level0[0].addr().level().0, 3);
assert!(!level_data(&parent)?.level0[1].addr().is_null());
let htree_ptr: BlockPtr<HTreeNode<HTreeNode<HTreeNode<DirList>>>> =
unsafe { level_data(&parent)?.level0[1].cast() };
let htree_node = tx.read_block(htree_ptr).unwrap();
// Note that while a split tries to evenly divide the H-tree entries between the new two sibling nodes,
// it tries to keep hash collisions together. This unnatural test scenario has a ton of the same max
// value hash, so those get grouped together, and all our varying named entries end up in the other.
assert!(!htree_node.data().ptrs[0].is_null());
assert_eq!(
htree_node.data().ptrs[0].htree_hash,
HTreeHash::from_name(format!("child__{:0243}", 1015).as_str())
);
assert!(!htree_node.data().ptrs[1].is_null());
assert_eq!(htree_node.data().ptrs[1].htree_hash, HTreeHash::MAX);
assert!(htree_node.data().ptrs[2].is_null());
let l3_node = tx.read_block(htree_node.data().ptrs[0].ptr).unwrap();
let l2_node = tx.read_block(l3_node.data().ptrs[0].ptr).unwrap();
assert_eq!(
l2_node.data().ptrs[0].htree_hash,
HTreeHash::from_name(format!("child__{:0243}", 1006).as_str())
);
assert_eq!(
l2_node.data().ptrs[1].htree_hash,
HTreeHash::from_name(format!("child__{:0243}", 1015).as_str())
);
assert!(l2_node.data().ptrs[2].is_null());
Ok(())
})
.unwrap();
// WHEN the max HTreeHash is removed from the smaller sibling
fs.tx(|tx| {
tx.remove_node(
parent_ptr,
format!("child__{:0243}", 1015).as_str(),
Node::MODE_FILE,
)
})
.unwrap();
// THEN the HTreeHash values for that branch are updated
fs.tx(|tx| {
let parent = tx.read_tree(parent_ptr).unwrap();
let htree_ptr: BlockPtr<HTreeNode<HTreeNode<HTreeNode<DirList>>>> =
unsafe { level_data(&parent)?.level0[1].cast() };
let htree_node = tx.read_block(htree_ptr).unwrap();
assert!(!htree_node.data().ptrs[0].is_null());
assert_eq!(
htree_node.data().ptrs[0].htree_hash,
HTreeHash::from_name(format!("child__{:0243}", 1014).as_str())
);
assert!(!htree_node.data().ptrs[1].is_null());
assert_eq!(htree_node.data().ptrs[1].htree_hash, HTreeHash::MAX);
assert!(htree_node.data().ptrs[2].is_null());
let l3_node = tx.read_block(htree_node.data().ptrs[0].ptr).unwrap();
let l2_node = tx.read_block(l3_node.data().ptrs[0].ptr).unwrap();
assert_eq!(
l2_node.data().ptrs[0].htree_hash,
HTreeHash::from_name(format!("child__{:0243}", 1006).as_str())
);
assert_eq!(
l2_node.data().ptrs[1].htree_hash,
HTreeHash::from_name(format!("child__{:0243}", 1014).as_str())
);
assert!(l2_node.data().ptrs[2].is_null());
Ok(())
})
.unwrap();
// WHEN removing all of one DirList
fs.tx(|tx| {
for i in 7..15 {
let x = 1000 + i;
tx.remove_node(
parent_ptr,
format!("child__{x:0243}").as_str(),
Node::MODE_FILE,
)
.unwrap();
}
Ok(())
})
.unwrap();
// THEN that HTreeNode is returned to empty
fs.tx(|tx| {
let parent = tx.read_tree(parent_ptr).unwrap();
let htree_ptr: BlockPtr<HTreeNode<HTreeNode<HTreeNode<DirList>>>> =
unsafe { level_data(&parent)?.level0[1].cast() };
let htree_node = tx.read_block(htree_ptr).unwrap();
assert!(!htree_node.data().ptrs[0].is_null());
assert_eq!(
htree_node.data().ptrs[0].htree_hash,
HTreeHash::from_name(format!("child__{:0243}", 1006).as_str())
);
assert!(!htree_node.data().ptrs[1].is_null());
assert_eq!(htree_node.data().ptrs[1].htree_hash, HTreeHash::MAX);
assert!(htree_node.data().ptrs[2].is_null());
let l3_node = tx.read_block(htree_node.data().ptrs[0].ptr).unwrap();
let l2_node = tx.read_block(l3_node.data().ptrs[0].ptr).unwrap();
assert_eq!(
l2_node.data().ptrs[0].htree_hash,
HTreeHash::from_name(format!("child__{:0243}", 1006).as_str())
);
assert!(l2_node.data().ptrs[1].is_null());
assert!(l2_node.data().ptrs[2].is_null());
Ok(())
})
.unwrap();
// WHEN removing the other small DirList
fs.tx(|tx| {
tx.remove_node(
parent_ptr,
format!("child__{:0243}", 1).as_str(),
Node::MODE_FILE,
)
.unwrap();
for i in 0..7 {
let x = 1000 + i;
tx.remove_node(
parent_ptr,
format!("child__{x:0243}").as_str(),
Node::MODE_FILE,
)
.unwrap();
}
Ok(())
})
.unwrap();
// THEN that HTreeNode is returned to empty
fs.tx(|tx| {
let parent = tx.read_tree(parent_ptr).unwrap();
let htree_ptr: BlockPtr<HTreeNode<HTreeNode<HTreeNode<DirList>>>> =
unsafe { level_data(&parent)?.level0[1].cast() };
let htree_node = tx.read_block(htree_ptr).unwrap();
assert!(!htree_node.data().ptrs[0].is_null());
assert_eq!(htree_node.data().ptrs[0].htree_hash, HTreeHash::MAX);
assert!(htree_node.data().ptrs[1].is_null());
Ok(())
})
.unwrap();
});
}
/// Test a pathological case of many HTreeHash collisions. This should never happen in reality,
/// but the system can support it.
#[test]
fn split_htree_with_multiple_levels_using_duplicates() {
with_redoxfs(|fs| {
let parent_ptr = TreePtr::<Node>::root();
let (mut fs, _) = create_minimal_l2_htree(format!("child{:0242}__0", 0).as_str(), fs);
// GIVEN a full root leaf node (DirList) with a full H-tree branch
fs.tx(|tx| {
for i in 1..16 {
let child_name = format!("child{i:0242}__0");
tx.create_node(parent_ptr, child_name.as_str(), Node::MODE_FILE, 1, 0)
.unwrap();
}
// Confirm preconditions: the level 0 is full of the expected entries.
let mut parent = tx.read_tree(parent_ptr).unwrap();
assert!(level_data(&parent)?.level0[0].is_marker());
assert_eq!(level_data(&parent)?.level0[0].addr().level().0, 2);
let l2_ptr: BlockPtr<HTreeNode<HTreeNode<DirList>>> =
unsafe { level_data(&parent)?.level0[1].cast() };
let mut l2_node = tx.read_block(l2_ptr).unwrap();
for i in 0..HTREE_IDX_ENTRIES {
if i == 0 {
assert!(!l2_node.data().ptrs[i].is_null());
} else {
assert!(l2_node.data().ptrs[i].is_null());
l2_node.data_mut().ptrs[i] = HTreePtr::new(HTreeHash::MAX, BlockPtr::marker(15))
}
}
let l1_ptr = l2_node.data().ptrs[0];
let mut l1_node = tx.read_block(l1_ptr.ptr).unwrap();
for i in 0..HTREE_IDX_ENTRIES {
if i == 0 {
assert!(!l1_node.data().ptrs[i].is_null());
} else {
assert!(l1_node.data().ptrs[i].is_null());
l1_node.data_mut().ptrs[i] = HTreePtr::new(HTreeHash::MAX, BlockPtr::marker(15))
}
}
l2_node.data_mut().ptrs[0].ptr = unsafe { tx.write_block(l1_node) }.unwrap();
let l2_record_ptr = unsafe { tx.write_block(l2_node) }.unwrap();
level_data_mut(&mut parent)?.level0[1] = unsafe { l2_record_ptr.cast() };
tx.sync_tree(parent).unwrap();
Ok(())
})
.unwrap();
// WHEN another entry is added to the full DirList
fs.tx(|tx| tx.create_node(parent_ptr, "child__0", Node::MODE_FILE, 1, 0))
.unwrap();
// THEN the branch splits all the way to the root, increasing the level
fs.tx(|tx| {
let parent = tx.read_tree(parent_ptr).unwrap();
assert!(level_data(&parent)?.level0[0].is_marker());
assert_eq!(level_data(&parent)?.level0[0].addr().level().0, 3);
assert!(!level_data(&parent)?.level0[1].addr().is_null());
let htree_ptr: BlockPtr<HTreeNode<HTreeNode<HTreeNode<DirList>>>> =
unsafe { level_data(&parent)?.level0[1].cast() };
let htree_node = tx.read_block(htree_ptr).unwrap();
// Note that while a split tries to evenly divide the H-tree entries between the new two sibling nodes,
// it tries to keep hash collisions together. This unnatural test scenario has a ton of the same max
// value hash, so those get grouped together, and all our other entries are grouped with the same hash
// value of zero.
assert!(!htree_node.data().ptrs[0].is_null());
assert_eq!(
htree_node.data().ptrs[0].htree_hash,
HTreeHash::from_name("__0")
);
assert!(!htree_node.data().ptrs[1].is_null());
assert_eq!(htree_node.data().ptrs[1].htree_hash, HTreeHash::MAX);
assert!(htree_node.data().ptrs[2].is_null());
let l3_node = tx.read_block(htree_node.data().ptrs[0].ptr).unwrap();
let l2_node = tx.read_block(l3_node.data().ptrs[0].ptr).unwrap();
assert_eq!(
l2_node.data().ptrs[0].htree_hash,
HTreeHash::from_name("__0")
);
assert_eq!(
l2_node.data().ptrs[1].htree_hash,
HTreeHash::from_name("__0")
);
assert!(l2_node.data().ptrs[2].is_null());
Ok(())
})
.unwrap();
// THEN all the colliding files can be listed
fs.tx(|tx| {
tx.find_node(parent_ptr, "child__0").unwrap();
for i in 0..16 {
let name = format!("child{i:0242}__0");
let result = tx.find_node(parent_ptr, name.as_str());
assert!(result.is_ok(), "Could not read {name}");
}
Ok(())
})
.unwrap();
// AND the first of the split DirLists has empty space while the second is full
fs.tx(|tx| {
let parent = tx.read_tree(parent_ptr).unwrap();
let htree_ptr: BlockPtr<HTreeNode<HTreeNode<HTreeNode<DirList>>>> =
unsafe { level_data(&parent)?.level0[1].cast() };
let htree_node = tx.read_block(htree_ptr).unwrap();
assert!(!htree_node.data().ptrs[0].is_null());
assert_eq!(
htree_node.data().ptrs[0].htree_hash,
HTreeHash::from_name("__0")
);
assert!(!htree_node.data().ptrs[1].is_null());
assert_eq!(htree_node.data().ptrs[1].htree_hash, HTreeHash::MAX);
assert!(htree_node.data().ptrs[2].is_null());
let l3_node = tx.read_block(htree_node.data().ptrs[0].ptr).unwrap();
let l2_node = tx.read_block(l3_node.data().ptrs[0].ptr).unwrap();
assert_eq!(
l2_node.data().ptrs[0].htree_hash,
HTreeHash::from_name("__0")
);
assert_eq!(
l2_node.data().ptrs[1].htree_hash,
HTreeHash::from_name("__0")
);
assert!(l2_node.data().ptrs[2].is_null());
let dir1 = tx.read_block(l2_node.data().ptrs[0].ptr).unwrap();
for (i, entry) in dir1.data().entries().enumerate() {
if i == 0 {
assert!(
!entry.node_ptr().is_null(),
"Entry {i} in dir1 should not be null"
);
assert_eq!(
HTreeHash::from_name(entry.name().unwrap()),
HTreeHash::from_name("__0"),
"Entry {i} with name {}",
entry.name().unwrap()
);
} else {
assert!(
entry.node_ptr().is_null(),
"Entry {i} in dir1 should be null"
);
}
}
let dir2 = tx.read_block(l2_node.data().ptrs[1].ptr).unwrap();
for (i, entry) in dir2.data().entries().enumerate() {
assert!(
!entry.node_ptr().is_null(),
"Entry {i} in dir2 should not be null"
);
assert_eq!(
HTreeHash::from_name(entry.name().unwrap()),
HTreeHash::from_name("__0"),
"Entry {i} with name {}",
entry.name().unwrap()
);
}
Ok(())
})
.unwrap();
});
}