diff --git a/crates/storage/src/collections/hashmap/fuzz_tests.rs b/crates/storage/src/collections/hashmap/fuzz_tests.rs index 6c8436bf11e..8f942105418 100644 --- a/crates/storage/src/collections/hashmap/fuzz_tests.rs +++ b/crates/storage/src/collections/hashmap/fuzz_tests.rs @@ -13,12 +13,25 @@ // limitations under the License. use super::HashMap as StorageHashMap; -use crate::traits::{ - KeyPtr, - SpreadLayout, +use crate::{ + test_utils::FuzzCollection, + traits::{ + KeyPtr, + PackedLayout, + SpreadLayout, + }, + Pack, }; use ink_primitives::Key; use itertools::Itertools; +use quickcheck::{ + Arbitrary, + Gen, +}; +use std::{ + collections::HashMap, + iter::FromIterator, +}; /// Conducts repeated insert and remove operations into the map by iterating /// over `xs`. For each odd `x` in `xs` a defined number of insert operations @@ -172,3 +185,80 @@ fn fuzz_defrag(xs: Vec, inserts_each: u8) { }) .unwrap() } + +impl Arbitrary for StorageHashMap +where + K: Arbitrary + Ord + PackedLayout + Send + Clone + std::hash::Hash + 'static, + V: Arbitrary + PackedLayout + Send + Clone + 'static, +{ + fn arbitrary(g: &mut Gen) -> StorageHashMap { + let hmap = HashMap::::arbitrary(g); + StorageHashMap::::from_iter(hmap) + } +} + +impl Clone for StorageHashMap +where + K: Ord + PackedLayout + Clone + std::hash::Hash, + V: PackedLayout + Clone, +{ + fn clone(&self) -> Self { + let mut shmap = StorageHashMap::::new(); + self.iter().for_each(|(k, v)| { + let _ = shmap.insert(k.clone(), v.clone()); + }); + shmap + } +} + +impl<'a, K, V> FuzzCollection for &'a mut StorageHashMap +where + V: Clone + PackedLayout + 'a, + K: PackedLayout + Ord + Clone + 'a, +{ + type Collection = StorageHashMap; + type Item = (&'a K, &'a mut V); + + /// Makes `self` equal to `instance2` by executing a series of operations + /// on `self`. + fn equalize(&mut self, instance2: &Self::Collection) { + let hmap_keys = self.keys().cloned().collect::>(); + for k in hmap_keys { + if !instance2.contains_key(&k) { + let _ = self.take(&k); + } + } + + let template_keys = instance2.keys().cloned(); + for k in template_keys { + if let Some(template_val) = instance2.get(&k) { + let _ = self.insert(k, template_val.clone()); + } + } + } + + /// `item` is an item from the hash map. We check if `item.key` is + /// in `self` and if existent assign its value to `item.value` + /// of `self` and assign it to `val`. + /// + /// Hence this method only might modify values of `item`, leaving + /// others intact. + fn assign(&mut self, item: Self::Item) { + let (key, value) = item; + if let Some(existent_value) = self.get(key) { + *value = existent_value.clone(); + } + } +} + +crate::fuzz_storage!("hashmap_1", StorageHashMap); +crate::fuzz_storage!("hashmap_2", StorageHashMap>>>); +crate::fuzz_storage!( + "hashmap_3", + StorageHashMap>, Option>>> +); +crate::fuzz_storage!( + "hashmap_4", + StorageHashMap, (bool, (u32, u128))> +); +crate::fuzz_storage!("hashmap_5", StorageHashMap, u32)>); diff --git a/crates/storage/src/collections/vec/fuzz_tests.rs b/crates/storage/src/collections/vec/fuzz_tests.rs new file mode 100644 index 00000000000..c10ad36c6ac --- /dev/null +++ b/crates/storage/src/collections/vec/fuzz_tests.rs @@ -0,0 +1,85 @@ +// Copyright 2018-2021 Parity Technologies (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::Vec as StorageVec; +use crate::{ + test_utils::FuzzCollection, + traits::{ + KeyPtr, + PackedLayout, + SpreadLayout, + }, + Pack, +}; + +use quickcheck::{ + Arbitrary, + Gen, +}; +use std::{ + iter::FromIterator, + vec::Vec, +}; + +impl Arbitrary for StorageVec +where + T: Arbitrary + PackedLayout + Send + Clone + 'static, +{ + fn arbitrary(g: &mut Gen) -> StorageVec { + let vec = Vec::::arbitrary(g); + StorageVec::::from_iter(vec) + } +} + +impl Clone for StorageVec +where + T: PackedLayout + Clone, +{ + fn clone(&self) -> Self { + let mut svec = StorageVec::::new(); + self.iter().for_each(|v| svec.push(v.clone())); + svec + } +} + +impl<'a, T> FuzzCollection for &'a mut StorageVec +where + T: Clone + PackedLayout, +{ + type Collection = StorageVec; + type Item = &'a mut T; + + /// Makes `self` equal to `instance2` by executing a series of operations + /// on `self`. + fn equalize(&mut self, instance2: &Self::Collection) { + self.clear(); + instance2.into_iter().for_each(|v| self.push(v.clone())); + } + + /// `val` is a value from the vector. We take an element out + /// of `self` and assign it to `val`. + /// + /// Hence this method only might modify values of `item`, leaving + /// others intact. + fn assign(&mut self, val: Self::Item) { + if let Some(popped_val) = self.pop() { + *val = popped_val.clone(); + } + } +} + +crate::fuzz_storage!("vec_1", StorageVec); +crate::fuzz_storage!("vec_2", StorageVec>>>); +crate::fuzz_storage!("vec_3", StorageVec<(bool, (u32, u128))>); +crate::fuzz_storage!("vec_4", StorageVec<(i128, u32, bool, Option<(u32, i128)>)>); diff --git a/crates/storage/src/collections/vec/mod.rs b/crates/storage/src/collections/vec/mod.rs index 827bd5bee9f..51053245d80 100644 --- a/crates/storage/src/collections/vec/mod.rs +++ b/crates/storage/src/collections/vec/mod.rs @@ -24,6 +24,9 @@ mod storage; #[cfg(test)] mod tests; +#[cfg(all(test, feature = "ink-fuzz-tests"))] +mod fuzz_tests; + pub use self::iter::{ Iter, IterMut, diff --git a/crates/storage/src/pack.rs b/crates/storage/src/pack.rs index dce6b7e586c..97b0bf13e18 100644 --- a/crates/storage/src/pack.rs +++ b/crates/storage/src/pack.rs @@ -509,3 +509,17 @@ mod tests { }) } } + +#[cfg(all(test, feature = "std", feature = "ink-fuzz-tests"))] +use quickcheck::{ + Arbitrary, + Gen, +}; + +#[cfg(all(test, feature = "std", feature = "ink-fuzz-tests"))] +impl Arbitrary for Pack { + fn arbitrary(g: &mut Gen) -> Pack { + let a = ::arbitrary(g); + Pack::new(a) + } +} diff --git a/crates/storage/src/test_utils.rs b/crates/storage/src/test_utils.rs index 9c1b8806bc8..31e37ba2845 100644 --- a/crates/storage/src/test_utils.rs +++ b/crates/storage/src/test_utils.rs @@ -76,3 +76,138 @@ macro_rules! push_pull_works_for_primitive { } }; } + +/// A trait to enable running some fuzz tests on a collection. +pub trait FuzzCollection { + type Collection; + type Item; + + /// Executes a series of operations on `self` in order to make it + /// equal to `template`. + fn equalize(&mut self, template: &Self::Collection); + + /// Takes a value from `self` and puts it into `item`. + fn assign(&mut self, item: Self::Item); +} + +/// Creates two fuzz tests. Both tests have the same flow: +/// - Take two instances of the collection, generated by our fuzzer +/// - Push `instance2` to storage, pull it out and assert that what +/// is pulled out is what was pushed. +/// - Do some mutations on the `pulled` object. Here the two tests +/// behave differently: +/// +/// * `fuzz_ $id _mutate_some` Mutates some entries of the data +/// structure based on the content of `instance2`. +/// +/// * `fuzz_ $id _mutate_all` Mutates the entire data structure, +/// so that it has the same content as `instance2`. +/// +/// - Push the mutated `pulled` object into storage again, pull it +/// out as `pulled2` and assert that both objects are equal. +/// - Clear the object from storage and assert that storage was +/// cleared up properly, without any leftovers. +#[macro_export] +macro_rules! fuzz_storage { + ($id:literal, $collection_type:ty) => { + ::paste::paste! { + /// Does some basic storage interaction tests whilst mutating + /// *some* of the data structure's entries. + #[allow(trivial_casts)] + #[quickcheck] + fn [< fuzz_ $id _mutate_some >] ( + instance1: $collection_type, + mut instance2: $collection_type, + ) { + ink_env::test::run_test::(|_| { + // we push the generated object into storage + let root_key = ink_primitives::Key::from([0x42; 32]); + let ptr = KeyPtr::from(root_key); + crate::traits::push_spread_root(&instance1, &mut root_key.clone()); + + // we pull what's in storage and assert that this is what was just pushed + let mut pulled: $collection_type = crate::traits::pull_spread_root(&root_key.clone()); + assert_eq!(instance1, pulled); + + // we iterate over what was pulled and call `assign` for all entries. + // this function may or may not modify elements of `pulled`. + pulled.iter_mut().for_each(|item| { + // this may leave some entries of `pulled` in `State::Preserved`. + // even though the instance which is supposed to be mutated is + // `pulled`, we still need to call this on a mutable `instance2`, + // since e.g. Vec does a `pop()` in assign, so that we don't always + // execute the same operation. + (&mut instance2).assign(item); + }); + + // we push the `pulled` object, on which we just executed mutations + // back into storage and asserts it can be pulled out intact again. + crate::traits::push_spread_root(&pulled, &mut root_key.clone()); + let pulled2: $collection_type = crate::traits::pull_spread_root(&mut root_key.clone()); + assert_eq!(pulled, pulled2); + + // we clear the object from storage and assert that everything was + // removed without any leftovers. + SpreadLayout::clear_spread(&pulled2, &mut ptr.clone()); + crate::test_utils::assert_storage_clean(); + + Ok(()) + }) + .unwrap() + } + + /// Does some basic storage interaction tests whilst mutating + /// *all* of the data structure's entries. + #[allow(trivial_casts)] + #[quickcheck] + fn [< fuzz_ $id _mutate_all >] ( + instance1: $collection_type, + instance2: $collection_type, + ) { + ink_env::test::run_test::(|_| { + // we push the generated object into storage + let root_key = ink_primitives::Key::from([0x42; 32]); + let ptr = KeyPtr::from(root_key); + crate::traits::push_spread_root(&instance1, &mut root_key.clone()); + + // we pull what's in storage and assert that this is what was just pushed + let mut pulled: $collection_type = crate::traits::pull_spread_root(&root_key.clone()); + assert_eq!(instance1, pulled); + + // `pulled` is going to be equalized to ` + (&mut pulled).equalize(&instance2); + + // we push the `pulled` object, on which we just executed mutations + // back into storage and assert it can be pulled out intact again and + // is equal to `instance2`. + crate::traits::push_spread_root(&pulled, &mut root_key.clone()); + let pulled2: $collection_type = crate::traits::pull_spread_root(&mut root_key.clone()); + assert_eq!(pulled, pulled2); + assert_eq!(pulled2, instance2); + + // we clear the object from storage and assert that everything was + // removed without any leftovers. + SpreadLayout::clear_spread(&pulled2, &mut ptr.clone()); + crate::test_utils::assert_storage_clean(); + + Ok(()) + + }) + .unwrap() + } + } + }; +} + +/// Asserts that the storage is empty, without any leftovers. +pub fn assert_storage_clean() { + let contract_id = + ink_env::test::get_current_contract_account_id::() + .expect("contract id must exist"); + let used_cells = + ink_env::test::count_used_storage_cells::( + &contract_id, + ) + .expect("used cells must be returned"); + assert_eq!(used_cells, 0); +}