diff --git a/wnfs/proptest-regressions/public/directory.txt b/wnfs/proptest-regressions/public/directory.txt new file mode 100644 index 00000000..8e3876bc --- /dev/null +++ b/wnfs/proptest-regressions/public/directory.txt @@ -0,0 +1,10 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 4ca9259264a7fb240270a7f3a3c9f702cc5f3f7ec8f8add28e774546901bb064 # shrinks to input = _TestMergeDirectoryPreferredArgs { path: [] } +cc 52d317f93f0d815bfd054e6147b32492abc79b100274458f3fc266d1d9f40083 # shrinks to input = _TestMergeCommutativityArgs { ops0: [Write(["a"], "a"), Mkdir(["a"])], ops1: [] } +cc 5d512e34a6b76473ff418d6cc7730003875ae30727a3155b2abc13d5f8313b58 # shrinks to input = _TestMergeCommutativityArgs { fs0: FileSystem { files: {}, dirs: {} }, fs1: FileSystem { files: {["a"]: "a"}, dirs: {["a"]} } } +cc d4c4529fd972a2a6af4dcecd28a289d11451203600ae18e001dbdd42fe19e245 # shrinks to input = _TestMergeCommutativityArgs { fs0: FileSystem { files: {["b"]: "a", ["b", "a"]: "a"}, dirs: {} }, fs1: FileSystem { files: {}, dirs: {} } } diff --git a/wnfs/src/public/directory.rs b/wnfs/src/public/directory.rs index ba50517c..fdee7081 100644 --- a/wnfs/src/public/directory.rs +++ b/wnfs/src/public/directory.rs @@ -1383,6 +1383,193 @@ mod tests { } } +#[cfg(test)] +mod proptests { + use super::*; + use proptest::{ + collection::{btree_map, btree_set, vec}, + prelude::*, + }; + use test_strategy::proptest; + use wnfs_common::MemoryBlockStore; + + #[derive(Debug, Clone)] + struct FileSystem { + files: BTreeMap, String>, + dirs: BTreeSet>, + } + + fn file_system() -> impl Strategy { + ( + btree_map(vec(simple_string(), 1..10), simple_string(), 0..40), + btree_set(vec(simple_string(), 1..10), 0..40), + ) + .prop_map(|(mut files, dirs)| { + files = files + .into_iter() + .filter(|(file_path, _)| { + !dirs + .iter() + .any(|dir_path| !dir_path.starts_with(&file_path)) + }) + .collect(); + FileSystem { files, dirs } + }) + .prop_filter("file overwritten by directory", valid_fs) + } + + fn simple_string() -> impl Strategy { + (0..6u32).prop_map(|c| char::from_u32('a' as u32 + c).unwrap().to_string()) + } + + fn valid_fs(fs: &FileSystem) -> bool { + fs.files.iter().all(|(file_path, _)| { + !fs.dirs + .iter() + .any(|dir_path| dir_path.starts_with(&file_path)) + && !fs + .files + .iter() + .any(|(other_path, _)| other_path.starts_with(&file_path)) + }) + } + + async fn convert_fs( + fs: FileSystem, + time: DateTime, + store: &impl BlockStore, + ) -> Result> { + let mut dir = PublicDirectory::new_rc(time); + let FileSystem { files, dirs } = fs; + for (path, content) in files.iter() { + dir.write(&path, content.clone().into_bytes(), time, store) + .await?; + } + + for path in dirs.iter() { + dir.mkdir(&path, time, store).await?; + } + + Ok(dir) + } + + #[proptest] + fn test_merge_directory_preferred(#[strategy(vec(simple_string(), 1..10))] path: Vec) { + async_std::task::block_on(async move { + let store = &MemoryBlockStore::new(); + let time = Utc::now(); + + let root0 = &mut PublicDirectory::new_rc(time); + let root1 = &mut PublicDirectory::new_rc(time); + + root0 + .write(&path, b"Should be overwritten".into(), time, store) + .await + .unwrap(); + + root1.mkdir(&path, time, store).await.unwrap(); + + root0.merge(root1, time, store).await.unwrap(); + + let node = root0 + .get_node(&path, store) + .await + .unwrap() + .expect("merged fs contains the node"); + + prop_assert!(node.is_dir()); + + Ok(()) + })?; + } + + #[proptest] + fn test_merge_commutativity( + #[strategy(file_system())] fs0: FileSystem, + #[strategy(file_system())] fs1: FileSystem, + ) { + async_std::task::block_on(async move { + let store = &MemoryBlockStore::new(); + let time = Utc::now(); + + let root0 = convert_fs(fs0, time, store).await.unwrap(); + let root1 = convert_fs(fs1, time, store).await.unwrap(); + + let mut merge_one_way = Arc::clone(&root0); + merge_one_way.merge(&root1, time, store).await.unwrap(); + let mut merge_other_way = Arc::clone(&root1); + merge_other_way.merge(&root0, time, store).await.unwrap(); + + let cid_one_way = merge_one_way.store(store).await.unwrap(); + let cid_other_way = merge_other_way.store(store).await.unwrap(); + + prop_assert_eq!(cid_one_way, cid_other_way); + + Ok(()) + })?; + } + + #[proptest] + fn test_merge_associativity( + #[strategy(file_system())] fs0: FileSystem, + #[strategy(file_system())] fs1: FileSystem, + #[strategy(file_system())] fs2: FileSystem, + ) { + async_std::task::block_on(async move { + let store = &MemoryBlockStore::new(); + let time = Utc::now(); + let root0 = convert_fs(fs0, time, store).await.unwrap(); + let root1 = convert_fs(fs1, time, store).await.unwrap(); + let root2 = convert_fs(fs2, time, store).await.unwrap(); + + let mut merge_0_1_then_2 = Arc::clone(&root0); + merge_0_1_then_2.merge(&root1, time, store).await.unwrap(); + merge_0_1_then_2.merge(&root2, time, store).await.unwrap(); + + let mut merge_1_2 = Arc::clone(&root1); + merge_1_2.merge(&root2, time, store).await.unwrap(); + let mut merge_0_with_1_2 = Arc::clone(&root0); + merge_0_with_1_2 + .merge(&merge_1_2, time, store) + .await + .unwrap(); + + let cid_one_way = merge_0_1_then_2.store(store).await.unwrap(); + let cid_other_way = merge_0_with_1_2.store(store).await.unwrap(); + + prop_assert_eq!(cid_one_way, cid_other_way); + + Ok(()) + })?; + } + + #[proptest] + fn test_merge_directories_preserved( + #[strategy(file_system())] fs0: FileSystem, + #[strategy(file_system())] fs1: FileSystem, + ) { + async_std::task::block_on(async move { + let store = &MemoryBlockStore::new(); + let time = Utc::now(); + + let mut all_dirs = fs0.dirs.clone(); + all_dirs.extend(fs1.dirs.iter().cloned()); + + let mut root = convert_fs(fs0, time, store).await.unwrap(); + let root1 = convert_fs(fs1, time, store).await.unwrap(); + + root.merge(&root1, time, store).await.unwrap(); + + for dir in all_dirs { + let exists = root.get_node(&dir, store).await.unwrap().is_some(); + prop_assert!(exists); + } + + Ok(()) + })?; + } +} + #[cfg(test)] mod snapshot_tests { use super::*;