Skip to content

Commit

Permalink
Add generalized collection fuzz testing (#601)
Browse files Browse the repository at this point in the history
* Add generalized collection fuzz testing

* Use `StorageHashMap::FromIter`

* Use `saturating_mul`

* Update to new `quickcheck` API

* Adopt year to 2021 in license

* Implement suggestions from review
  • Loading branch information
Michael Müller authored Feb 4, 2021
1 parent 209c1a7 commit a50bf6d
Show file tree
Hide file tree
Showing 5 changed files with 330 additions and 3 deletions.
96 changes: 93 additions & 3 deletions crates/storage/src/collections/hashmap/fuzz_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -172,3 +185,80 @@ fn fuzz_defrag(xs: Vec<i32>, inserts_each: u8) {
})
.unwrap()
}

impl<K, V> Arbitrary for StorageHashMap<K, V>
where
K: Arbitrary + Ord + PackedLayout + Send + Clone + std::hash::Hash + 'static,
V: Arbitrary + PackedLayout + Send + Clone + 'static,
{
fn arbitrary(g: &mut Gen) -> StorageHashMap<K, V> {
let hmap = HashMap::<K, V>::arbitrary(g);
StorageHashMap::<K, V>::from_iter(hmap)
}
}

impl<K, V> Clone for StorageHashMap<K, V>
where
K: Ord + PackedLayout + Clone + std::hash::Hash,
V: PackedLayout + Clone,
{
fn clone(&self) -> Self {
let mut shmap = StorageHashMap::<K, V>::new();
self.iter().for_each(|(k, v)| {
let _ = shmap.insert(k.clone(), v.clone());
});
shmap
}
}

impl<'a, K, V> FuzzCollection for &'a mut StorageHashMap<K, V>
where
V: Clone + PackedLayout + 'a,
K: PackedLayout + Ord + Clone + 'a,
{
type Collection = StorageHashMap<K, V>;
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::<Vec<K>>();
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<u32, u32>);
crate::fuzz_storage!("hashmap_2", StorageHashMap<u32, Option<Pack<Option<u32>>>>);
crate::fuzz_storage!(
"hashmap_3",
StorageHashMap<Option<Option<u32>>, Option<Pack<Option<u32>>>>
);
crate::fuzz_storage!(
"hashmap_4",
StorageHashMap<Pack<(u32, i128)>, (bool, (u32, u128))>
);
crate::fuzz_storage!("hashmap_5", StorageHashMap<u32, (i128, u32, bool, Option<(u32, i128)>, u32)>);
85 changes: 85 additions & 0 deletions crates/storage/src/collections/vec/fuzz_tests.rs
Original file line number Diff line number Diff line change
@@ -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<T> Arbitrary for StorageVec<T>
where
T: Arbitrary + PackedLayout + Send + Clone + 'static,
{
fn arbitrary(g: &mut Gen) -> StorageVec<T> {
let vec = Vec::<T>::arbitrary(g);
StorageVec::<T>::from_iter(vec)
}
}

impl<T> Clone for StorageVec<T>
where
T: PackedLayout + Clone,
{
fn clone(&self) -> Self {
let mut svec = StorageVec::<T>::new();
self.iter().for_each(|v| svec.push(v.clone()));
svec
}
}

impl<'a, T> FuzzCollection for &'a mut StorageVec<T>
where
T: Clone + PackedLayout,
{
type Collection = StorageVec<T>;
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<u32>);
crate::fuzz_storage!("vec_2", StorageVec<Option<Pack<Option<u32>>>>);
crate::fuzz_storage!("vec_3", StorageVec<(bool, (u32, u128))>);
crate::fuzz_storage!("vec_4", StorageVec<(i128, u32, bool, Option<(u32, i128)>)>);
3 changes: 3 additions & 0 deletions crates/storage/src/collections/vec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions crates/storage/src/pack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: Arbitrary + PackedLayout + Send + Clone + 'static> Arbitrary for Pack<T> {
fn arbitrary(g: &mut Gen) -> Pack<T> {
let a = <T as Arbitrary>::arbitrary(g);
Pack::new(a)
}
}
135 changes: 135 additions & 0 deletions crates/storage/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<ink_env::DefaultEnvironment, _>(|_| {
// 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::<ink_env::DefaultEnvironment, _>(|_| {
// 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::<ink_env::DefaultEnvironment>()
.expect("contract id must exist");
let used_cells =
ink_env::test::count_used_storage_cells::<ink_env::DefaultEnvironment>(
&contract_id,
)
.expect("used cells must be returned");
assert_eq!(used_cells, 0);
}

0 comments on commit a50bf6d

Please sign in to comment.