Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ num-traits = { version = "0.2.1", default-features = false, features = ["i128"]
# Never use this crate outside of the off-chain environment!
rand = { version = "0.7", default-features = false, features = ["alloc"], optional = true }

[dev-dependencies]
quickcheck = "0.9.0"
quickcheck_macros = "0.8.0"
itertools = "0.8.2"

[features]
default = ["std"]
std = [
Expand All @@ -52,3 +57,4 @@ ink-generate-abi = [
"type-metadata",
"std",
]
ink-fuzz = ["std"]
35 changes: 35 additions & 0 deletions core/src/env/engine/off_chain/test_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,25 @@ where
})
}

/// Initializes the whole off-chain environment.
/// Uses a fresh off-chain environment instance!
///
/// # Note
///
/// - Initializes the off-chain environment with default values that fit most
/// uses cases.
/// - The off-chain environment _must_ be initialized before use.
pub fn recreate_and_initialize_as_default<T>() -> Result<()>
where
T: EnvTypes,
<T as EnvTypes>::AccountId: From<[u8; 32]>,
{
<EnvInstance as OnInstance>::on_instance(|instance| {
*instance = EnvInstance::uninitialized();
instance.initialize_as_default::<T>()
})
}

/// Runs the given closure test function with the default configuartion
/// for the off-chain environment.
pub fn run_test<T, F>(f: F) -> Result<()>
Expand All @@ -312,6 +331,22 @@ where
f(default_accounts)
}

/// Runs the given closure test function with the default configuration
/// for the off-chain environment.
///
/// Doesn't reuse an off-chain environment which might already exist in
/// this thread, but instead uses a new off-chain environment instance.
pub fn run_multiple_tests_in_thread<T, F>(f: F) -> Result<()>
where
T: EnvTypes,
F: FnOnce(DefaultAccounts<T>) -> Result<()>,
<T as EnvTypes>::AccountId: From<[u8; 32]>,
{
recreate_and_initialize_as_default::<T>()?;
let default_accounts = default_accounts::<T>()?;
f(default_accounts)
}

/// Returns the total number of reads and writes of the contract's storage.
pub fn get_contract_storage_rw<T>(account_id: &T::AccountId) -> Result<(usize, usize)>
where
Expand Down
4 changes: 4 additions & 0 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@
#[cfg(not(feature = "std"))]
extern crate ink_alloc;

#[cfg(all(test, feature = "std", feature = "ink-fuzz"))]
#[macro_use(quickcheck)]
extern crate quickcheck_macros;

pub mod env;
pub mod storage;

Expand Down
106 changes: 106 additions & 0 deletions core/src/storage/collections/hash_map/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,70 @@ use crate::{
};
use ink_primitives::Key;

#[cfg(feature = "ink-fuzz")]
use itertools::Itertools;

fn new_empty<K, V>() -> storage::HashMap<K, V> {
unsafe {
let mut alloc = BumpAlloc::from_raw_parts(Key([0x0; 32]));
storage::HashMap::allocate_using(&mut alloc).initialize_into(())
}
}

/// 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
/// (`inserts_each`) is executed. For each even `x` it's asserted that the
/// previously inserted elements are in the map and they are removed subsequently.
///
/// The reasoning behind this even/odd sequence is to introduce some
/// randomness into when elements are inserted/removed.
///
/// `inserts_each` was chosen as `u8` to keep the number of inserts per `x` in
/// a reasonable range.
#[cfg(feature = "ink-fuzz")]
fn insert_and_remove(xs: Vec<i32>, inserts_each: u8) {
let mut map = new_empty();
let mut cnt_inserts = 0;
let mut previous_even_x = None;
let inserts_each = inserts_each as i32;

xs.into_iter().for_each(|x| {
if x % 2 == 0 {
// On even numbers we insert
for key in x..x + inserts_each {
let val = key * 10;
if let None = map.insert(key, val) {
assert_eq!(map.get(&key), Some(&val));
cnt_inserts += 1;
}
assert_eq!(map.len(), cnt_inserts);
}
if let None = previous_even_x {
previous_even_x = Some(x);
}
} else if x % 2 == 1 && previous_even_x.is_some() {
// If it's an odd number and we inserted in a previous run we assert
// that the last insert worked correctly and remove the elements again.
//
// It can happen that after one insert run there are many more
// insert runs (i.e. even `x` in `xs`) before we remove the numbers
// of the last run again. This is intentional, as to include testing
// if subsequent insert operations have an effect on already inserted
// items.
let x = previous_even_x.unwrap();
for key in x..x + inserts_each {
let val = key * 10;
assert_eq!(map.get(&key), Some(&val));
assert_eq!(map.remove(&key), Some(val));
assert_eq!(map.get(&key), None);
cnt_inserts -= 1;
assert_eq!(map.len(), cnt_inserts);
}
previous_even_x = None;
}
});
}

#[test]
fn new_unchecked() -> Result<()> {
env::test::run_test::<env::DefaultEnvTypes, _>(|_| {
Expand Down Expand Up @@ -188,3 +245,52 @@ fn mutate_with() -> Result<()> {
Ok(())
})
}

#[cfg(feature = "ink-fuzz")]
#[quickcheck]
fn randomized_inserts_and_removes_hm(xs: Vec<i32>, inserts_each: u8) -> Result<()> {
env::test::run_multiple_tests_in_thread::<env::DefaultEnvTypes, _>(|_| {
insert_and_remove(xs, inserts_each);
Ok(())
})
}

/// Inserts all elements from `xs`. Then removes each `xth` element from the map
/// and asserts that all non-`xth` elements are still in the map.
#[cfg(feature = "ink-fuzz")]
#[quickcheck]
fn randomized_removes(xs: Vec<i32>, xth: usize) -> Result<()> {
env::test::run_multiple_tests_in_thread::<env::DefaultEnvTypes, _>(|_| {
// given
let xs: Vec<i32> = xs.into_iter().unique().collect();
let xth = xth.max(1);
let mut map = new_empty();
let mut len = map.len();

// when
// 1) insert all
xs.iter().for_each(|i| {
assert_eq!(map.insert(*i, i * 10), None);
len += 1;
assert_eq!(map.len(), len);
});

// 2) remove every `xth` element of `xs` from the map
xs.iter().enumerate().for_each(|(x, i)| {
if x % xth == 0 {
assert_eq!(map.remove(&i), Some(i * 10));
len -= 1;
}
assert_eq!(map.len(), len);
});

// then
// everything else must still be get-able
xs.iter().enumerate().for_each(|(x, i)| {
if x % xth != 0 {
assert_eq!(map.get(&i), Some(&(i * 10)));
}
});
Ok(())
})
}