Unverified Commit 2c1904fe authored by Michael Müller's avatar Michael Müller Committed by GitHub
Browse files

[storage] Add some tests for storage primitives (#529)

* [chores] Fix typo entitiy ➜ entity

* [storage] Extract push_pull_works_for_primitive macro

* [storage] Add tests for Tuple

* [storage] Add tests for Option, Result, Box

* [storage] Add tests for Array

* [storage] Add fuzz tests for primitives

* [chores] Fix typo

* [storage] Add fuzz test for HashMap::defrag

* [storage] Improve unit tests

* [storage] Clarify TestResult::discard
parent 44a3f49e
Pipeline #111330 passed with stages
in 8 minutes and 40 seconds
......@@ -139,7 +139,7 @@ Storing or loading complex data structures to and from contract storage can be d
For example it might be a very good idea to store all the information under the same cell if all the information is very compact. For example when we are dealing with a byte vector that is expected to never be larger than approx a thousand elements it would probably be more efficient if we store all those thousand bytes in the same cell and especially if we often access many of those (or all) in our contract messages.
On the other hand spreading information across as many cells as possible might be much more efficient if we are dealing with big data structures, a lot of information that is not compact, or when messages that operate on the data always only need a small fraction of the whole data.
An example for this use case is if you have a vector of user accounts where each account stores potentially a lot of information, e.g. a 32-byte hash etc and where our messages only every operate on only a few of those at a time.
An example for this use case is if you have a vector of user accounts where each account stores potentially a lot of information, e.g. a 32-byte hash etc and where our messages only every operate on only a few of those at a time.
The `ink_storage` crate provides the user full control over the strategy or a mix of these two root strategies through some fundamental abstractions that we are briefly presenting to you.
......@@ -211,7 +211,7 @@ pub struct Packed {
### Opting-out of Storage
If you are in need of storing some temporary information across method and message boundaries ink! will have your back with the `ink_storage::Memory` abstraction. It allows you to simply opt-out of using the storage for the wrapped entitiy at all and thus is very similar to Solidity's very own `memory` annotation.
If you are in need of storing some temporary information across method and message boundaries ink! will have your back with the `ink_storage::Memory` abstraction. It allows you to simply opt-out of using the storage for the wrapped entity at all and thus is very similar to Solidity's very own `memory` annotation.
An example below:
......@@ -329,7 +329,7 @@ We won't be going into the details for any of those but will briefly present the
## Merging of ink! Attributes
It is possible to merge attributes that share a common flagged entitiy.
It is possible to merge attributes that share a common flagged entity.
The example below demonstrates this for a payable message with a custom selector.
```rust
......
......@@ -13,6 +13,11 @@
// limitations under the License.
use super::HashMap as StorageHashMap;
use crate::traits::{
KeyPtr,
SpreadLayout,
};
use ink_primitives::Key;
use itertools::Itertools;
/// Conducts repeated insert and remove operations into the map by iterating
......@@ -25,7 +30,7 @@ use itertools::Itertools;
///
/// `inserts_each` was chosen as `u8` to keep the number of inserts per `x` in
/// a reasonable range.
fn insert_and_remove(xs: Vec<i32>, inserts_each: u8) {
fn insert_and_remove(xs: Vec<i32>, inserts_each: u8) -> StorageHashMap<i32, i32> {
let mut map = <StorageHashMap<i32, i32>>::new();
let mut cnt_inserts = 0;
let mut previous_even_x = None;
......@@ -66,12 +71,13 @@ fn insert_and_remove(xs: Vec<i32>, inserts_each: u8) {
previous_even_x = None;
}
}
map
}
#[quickcheck]
fn inserts_and_removes(xs: Vec<i32>, inserts_each: u8) {
ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
insert_and_remove(xs, inserts_each);
let _ = insert_and_remove(xs, inserts_each);
Ok(())
})
.unwrap()
......@@ -126,3 +132,43 @@ fn removes(xs: Vec<i32>, xth: usize) {
})
.unwrap()
}
#[quickcheck]
fn defrag(xs: Vec<i32>, inserts_each: u8) {
ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
// Create a `HashMap<i32, i32>` and execute some pseudo-randomized
// insert/remove operations on it.
let mut map = insert_and_remove(xs, inserts_each);
// Build a collection of the keys/values in this hash map
let kv_pairs: Vec<(i32, i32)> = map
.keys
.iter()
.map(|key| {
(
key.to_owned(),
map.get(key).expect("value must exist").to_owned(),
)
})
.collect();
assert_eq!(map.len(), kv_pairs.len() as u32);
// Then defragment the hash map
map.defrag(None);
// Then we push the defragmented hash map to storage and pull it again
let root_key = Key::from([0x00; 32]);
SpreadLayout::push_spread(&map, &mut KeyPtr::from(root_key));
let map2: StorageHashMap<i32, i32> =
SpreadLayout::pull_spread(&mut KeyPtr::from(root_key));
// Assert that everything that should be is still in the hash map
assert_eq!(map2.len(), kv_pairs.len() as u32);
for (key, val) in kv_pairs {
assert_eq!(map2.get(&key), Some(&val));
}
Ok(())
})
.unwrap()
}
......@@ -373,7 +373,7 @@ where
// limit set to 1 after every successful removal.
if let Some(0) = max_iterations {
// Bail out early if the iteration limit is set to 0 anyways to
// completely avoid doing work in this case.y
// completely avoid doing work in this case.
return 0
}
let len_vacant = self.keys.capacity() - self.keys.len();
......
......@@ -57,6 +57,9 @@ pub mod traits;
#[cfg(test)]
mod hashmap_entry_api_tests;
#[cfg(test)]
mod test_utils;
#[doc(inline)]
pub use self::{
alloc::Box,
......
// Copyright 2018-2020 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.
//! Utilities for testing if the storage interaction of an object
//! which is pushed/pulled/cleared to/from storage behaves as it should.
/// Runs `f` using the off-chain testing environment.
#[cfg(test)]
pub fn run_test<F>(f: F)
where
F: FnOnce(),
{
ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
f();
Ok(())
})
.unwrap()
}
/// Creates two tests:
/// (1) Tests if an object which is `push_spread`-ed to storage results in exactly
/// the same object when it is `pull_spread`-ed again. Subsequently the object
/// undergoes the same test for `push_packed` and `pull_packed`.
/// (2) Tests if `clear_spread` removes the object properly from storage.
#[macro_export]
macro_rules! push_pull_works_for_primitive {
( $name:ty, [$($value:expr),*] ) => {
paste::item! {
#[test]
#[allow(non_snake_case)]
fn [<$name _pull_push_works>] () {
crate::test_utils::run_test(|| {
$({
let x: $name = $value;
let key = ink_primitives::Key::from([0x42; 32]);
let key2 = ink_primitives::Key::from([0x77; 32]);
crate::traits::push_spread_root(&x, &key);
let y: $name = crate::traits::pull_spread_root(&key);
assert_eq!(x, y);
crate::traits::push_packed_root(&x, &key2);
let z: $name = crate::traits::pull_packed_root(&key2);
assert_eq!(x, z);
})*
})
}
#[test]
#[should_panic(expected = "storage entry was empty")]
#[allow(non_snake_case)]
fn [<$name _clean_works>]() {
crate::test_utils::run_test(|| {
$({
let x: $name = $value;
let key = ink_primitives::Key::from([0x42; 32]);
crate::traits::push_spread_root(&x, &key);
// Works since we just populated the storage.
let y: $name = crate::traits::pull_spread_root(&key);
assert_eq!(x, y);
crate::traits::clear_spread_root(&x, &key);
// Panics since it loads eagerly from cleared storage.
let _: $name = crate::traits::pull_spread_root(&key);
})*
})
}
}
};
}
......@@ -88,3 +88,31 @@ macro_rules! impl_layout_for_array {
}
}
forward_supported_array_lens!(impl_layout_for_array);
#[cfg(test)]
mod tests {
use crate::push_pull_works_for_primitive;
type Array = [i32; 4];
push_pull_works_for_primitive!(
Array,
[
[0, 1, 2, 3],
[i32::MAX, i32::MIN, i32::MAX, i32::MIN],
[Default::default(), i32::MAX, Default::default(), i32::MIN]
]
);
type ArrayTuples = [(i32, i32); 2];
push_pull_works_for_primitive!(
ArrayTuples,
[
[(0, 1), (2, 3)],
[(i32::MAX, i32::MIN), (i32::MIN, i32::MAX)],
[
(Default::default(), i32::MAX),
(Default::default(), i32::MIN)
]
]
);
}
// Copyright 2018-2020 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.
//! Fuzz tests for some storage primitives.
#[cfg(all(test, feature = "std", feature = "ink-fuzz-tests"))]
use quickcheck::TestResult;
#[cfg(all(test, feature = "std", feature = "ink-fuzz-tests"))]
use std::convert::AsMut;
/// Receives a slice, returns an array.
fn clone_into_array<A, T>(slice: &[T]) -> A
where
A: Default + AsMut<[T]>,
T: Clone,
{
let mut a = A::default();
<A as AsMut<[T]>>::as_mut(&mut a).clone_from_slice(slice);
a
}
/// Tests if a fuzzed `[i32; 32]` array results in the same object when
/// pushed/pulled from storage (for `spread` and `packed`).
#[quickcheck]
fn fuzz_pull_push_pull_array(x: Vec<i32>) -> TestResult {
// We want to have only vectors of length 32 fuzzed in here.
// The reason is that quickcheck does not directly support
// Array's as a parameter to be fuzzed. So we use this
// workaround of asking for a Vec with length 32 and convert
// it to an array with 32 elements subsequently.
//
// The guided fuzzing will notice that every Vec of greater/smaller
// length is always discarded and aim to input vectors of length 32.
if x.len() != 32 {
return TestResult::discard()
}
ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
let key = ink_primitives::Key::from([0x42; 32]);
let key2 = ink_primitives::Key::from([0x77; 32]);
let arr: [i32; 32] = clone_into_array(&x[0..32]);
crate::traits::push_spread_root(&arr, &key);
let y: [i32; 32] = crate::traits::pull_spread_root(&key);
assert_eq!(arr, y);
crate::traits::push_packed_root(&arr, &key2);
let z: [i32; 32] = crate::traits::pull_packed_root(&key2);
assert_eq!(arr, z);
Ok(())
})
.unwrap();
TestResult::from_bool(true)
}
/// Tests if a fuzzed `String` results in the same object when pushed/pulled
/// from storage (for `spread` and `packed`).
#[cfg(feature = "ink-fuzz-tests")]
#[quickcheck]
fn fuzz_pull_push_pull_string(x: String) {
ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
let key = ink_primitives::Key::from([0x42; 32]);
let key2 = ink_primitives::Key::from([0x77; 32]);
crate::traits::push_spread_root(&x, &key);
let y: String = crate::traits::pull_spread_root(&key);
assert_eq!(x, y);
crate::traits::push_packed_root(&x, &key2);
let z: String = crate::traits::pull_packed_root(&key2);
assert_eq!(x, z);
Ok(())
})
.unwrap()
}
......@@ -88,6 +88,9 @@ mod collections;
mod prims;
mod tuples;
#[cfg(all(test, feature = "ink-fuzz-tests"))]
mod fuzz_tests;
use super::{
clear_packed_root,
pull_packed_root,
......
......@@ -233,70 +233,10 @@ where
#[cfg(test)]
mod tests {
use crate::traits::{
clear_spread_root,
pull_packed_root,
pull_spread_root,
push_packed_root,
push_spread_root,
};
use crate::push_pull_works_for_primitive;
use ink_env::AccountId;
use ink_primitives::Key;
/// Runs `f` using the off-chain testing environment.
fn run_test<F>(f: F)
where
F: FnOnce(),
{
ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
f();
Ok(())
})
.unwrap()
}
macro_rules! push_pull_works_for_primitive {
( $name:ty, [$($value:expr),*] ) => {
paste::item! {
#[test]
#[allow(non_snake_case)]
fn [<$name _pull_push_works>] () {
run_test(|| {
$({
let x: $name = $value;
let key = Key::from([0x42; 32]);
let key2 = Key::from([0x77; 32]);
push_spread_root(&x, &key);
let y: $name = pull_spread_root(&key);
assert_eq!(x, y);
push_packed_root(&x, &key2);
let z: $name = pull_packed_root(&key2);
assert_eq!(x, z);
})*
})
}
#[test]
#[should_panic(expected = "storage entry was empty")]
#[allow(non_snake_case)]
fn [<$name _clean_works>]() {
run_test(|| {
$({
let x: $name = $value;
let key = Key::from([0x42; 32]);
push_spread_root(&x, &key);
// Works since we just populated the storage.
let y: $name = pull_spread_root(&key);
assert_eq!(x, y);
clear_spread_root(&x, &key);
// Panics since it loads eagerly from cleared storage.
let _: $name = pull_spread_root(&key);
})*
})
}
}
};
}
push_pull_works_for_primitive!(bool, [false, true]);
push_pull_works_for_primitive!(
String,
......@@ -334,4 +274,16 @@ mod tests {
u128,
[0, Default::default(), 50, u128::MIN, u128::MAX]
);
type OptionU8 = Option<u8>;
push_pull_works_for_primitive!(OptionU8, [Some(13u8), None]);
type ResultU8 = Result<u8, bool>;
push_pull_works_for_primitive!(ResultU8, [Ok(13u8), Err(false)]);
type BoxU8 = Box<u8>;
push_pull_works_for_primitive!(BoxU8, [Box::new(27u8)]);
type BoxOptionU8 = Box<Option<u8>>;
push_pull_works_for_primitive!(BoxOptionU8, [Box::new(Some(27)), Box::new(None)]);
}
......@@ -100,3 +100,39 @@ impl_layout_for_tuple!(A, B, C, D, E, F, G);
impl_layout_for_tuple!(A, B, C, D, E, F, G, H);
impl_layout_for_tuple!(A, B, C, D, E, F, G, H, I);
impl_layout_for_tuple!(A, B, C, D, E, F, G, H, I, J);
#[cfg(test)]
mod tests {
use crate::push_pull_works_for_primitive;
type TupleSix = (i32, u32, String, u8, bool, Box<Option<i32>>);
push_pull_works_for_primitive!(
TupleSix,
[
(
-1,
1,
String::from("foobar"),
13,
true,
Box::new(Some(i32::MIN))
),
(
i32::MIN,
u32::MAX,
String::from("❤ ♡ ❤ ♡ ❤"),
Default::default(),
false,
Box::new(Some(i32::MAX))
),
(
Default::default(),
Default::default(),
Default::default(),
Default::default(),
Default::default(),
Default::default()
)
]
);
}
......@@ -102,7 +102,7 @@ where
<T as SpreadLayout>::clear_spread(entity, &mut ptr);
}
/// Pushes the entitiy to the contract storage using spread layout.
/// Pushes the entity to the contract storage using spread layout.
///
/// The root key denotes the offset into the contract storage where the
/// instance of type `T` is being pushed to.
......@@ -143,7 +143,7 @@ where
entity
}
/// Pushes the entitiy to the contract storage using packed layout.
/// Pushes the entity to the contract storage using packed layout.
///
/// The root key denotes the offset into the contract storage where the
/// instance of type `T` is being pushed to.
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment