diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 4928d217..44d44306 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -91,4 +91,4 @@ jobs: components: miri - name: Run tests with Miri - run: cargo miri test -- --skip ::stress_test --skip ::anchor + run: cargo miri test -- -Z unstable-options --report-time --skip ::anchor diff --git a/common/lib/src/test_utils.rs b/common/lib/src/test_utils.rs index 223de039..677fa285 100644 --- a/common/lib/src/test_utils.rs +++ b/common/lib/src/test_utils.rs @@ -10,13 +10,22 @@ /// value reduced by given factor. Using `divisor` is better than dividing the /// result because if requested number of tests is non-zero, this function will /// always return at least one. +/// +/// When running under Miri, the returned value is clamped to be at most five. +/// The idea being to avoid stress tests, which run for good several second when +/// run normally, taking minutes or hours when run through Miri. pub fn get_iteration_count(divisor: usize) -> usize { use core::str::FromStr; let n = std::env::var_os("STRESS_TEST_ITERATIONS") .map(|val| usize::from_str(val.to_str().unwrap()).unwrap()) .unwrap_or(100_000); - match n { + let n = match n { 0 => 0, n => 1.max(n / divisor), + }; + if cfg!(miri) { + n.min(5) + } else { + n } } diff --git a/common/sealable-trie/src/trie/tests.rs b/common/sealable-trie/src/trie/tests.rs index 71433b4a..064a993d 100644 --- a/common/sealable-trie/src/trie/tests.rs +++ b/common/sealable-trie/src/trie/tests.rs @@ -7,127 +7,172 @@ use memory::test_utils::TestAllocator; use rand::Rng; #[track_caller] -fn do_test_inserts<'a>( +fn make_trie_impl<'a>( keys: impl IntoIterator, - want_root: &str, - want_nodes: usize, - verbose: bool, + mut set: impl FnMut(&mut TestTrie, &'a [u8]), + want: Option<(&str, usize)>, ) -> TestTrie { let keys = keys.into_iter(); let count = keys.size_hint().1.unwrap_or(1000).saturating_mul(4); let mut trie = TestTrie::new(count); for key in keys { - trie.set(key, verbose) + set(&mut trie, key) } - if !want_root.is_empty() { + if let Some((want_root, want_nodes)) = want { let want_root = CryptoHash::from_base64(want_root).unwrap(); - assert_eq!(&want_root, trie.hash()); - } - if want_nodes != usize::MAX { - assert_eq!(want_nodes, trie.nodes_count()); + assert_eq!((&want_root, want_nodes), (trie.hash(), trie.nodes_count())); } trie } -#[test] -fn test_msb_difference() { - do_test_inserts( - [&[0][..], &[0x80][..]], - "Stmrss0PVu2RSGiHibdgHlBNxN/XPsqJsIlWoAAdI5g=", - 3, - true, - ); +/// Constructs a trie with given keys. +#[track_caller] +fn make_trie_from_keys<'a>( + keys: impl IntoIterator, + want: Option<(&str, usize)>, + verbose: bool, +) -> TestTrie { + let set = |trie: &mut TestTrie, key| trie.set(key, verbose); + make_trie_impl(keys, set, want) } +/// Constructs a trie with given keys whose all keys are sealed. +/// +/// Uses `set_and_seal` to seal keys when adding them to the trie. This makes +/// them sealed. +#[track_caller] +fn make_sealed_trie_from_keys<'a>( + keys: impl IntoIterator, + want: Option<(&str, usize)>, + verbose: bool, +) -> TestTrie { + let set = |trie: &mut TestTrie, key| trie.set_and_seal(key, verbose); + make_trie_impl(keys, set, want) +} + +/// Tests creating a trie where the very first bit of the two keys differs. #[test] -fn test_sequence() { - do_test_inserts( - b"0123456789:;<=>?".iter().map(core::slice::from_ref), - "T9199/qDmjbqYqxaHrGh024lQRuTZcXBisiXCSwfNd4=", - 16, +fn test_msb_difference() { + make_trie_from_keys( + [&[0][..], &[0x80][..]], + Some(("Stmrss0PVu2RSGiHibdgHlBNxN/XPsqJsIlWoAAdI5g=", 3)), true, ); } +/// Tests inserting an Extension node whose key spans two bytes. #[test] fn test_2byte_extension() { - do_test_inserts( + make_trie_from_keys( [&[123, 40][..], &[134, 233][..]], - "KuGB/DlpPNpq95GPa47hyiWwWLqBvwStKohETSTCTWQ=", - 3, + Some(("KuGB/DlpPNpq95GPa47hyiWwWLqBvwStKohETSTCTWQ=", 3)), true, ); } +/// Tests setting value on a key and on a prefix of the key. #[test] fn test_prefix() { let key = b"xy"; - do_test_inserts( + make_trie_from_keys( [&key[..], &key[..1]], - "gVrQ18qbqdhGPIIXSvlVD5dSyTy1OvduWpPsl4viANw=", - 3, + Some(("gVrQ18qbqdhGPIIXSvlVD5dSyTy1OvduWpPsl4viANw=", 3)), true, ); - do_test_inserts( + make_trie_from_keys( [&key[..1], &key[..]], - "8LpINasPAwifquBydtqD7RFSgBZidoc2XmtNkThh23U=", - 3, + Some(("8LpINasPAwifquBydtqD7RFSgBZidoc2XmtNkThh23U=", 3)), true, ); } +/// Creates a trie with sequential keys. Returns `(trie, keys)` pair. +/// +/// If `small` is true constructs a small trie with just four sequential keys. +/// Otherwise constructs a larger trie with 16 sequential keys. The small trie +/// is intended for Miri tests which run prohibitively long on full versions of +/// tests. +/// +/// If `sealed` is true, rather than inserting the keys into the trie, +/// `set_and_seal` them. +fn make_trie(small: bool, sealed: bool) -> (TestTrie, &'static [u8]) { + const KEYS: &[u8; 16] = b"0123456789:;<=>?"; + let (keys, hash, count, sealed_count) = if small { + (6, "Ag69KY5nI5NtAAXRy1ZIy4kUxcDgVyJZ6XkdX1dEinw=", 7, 3) + } else { + (16, "T9199/qDmjbqYqxaHrGh024lQRuTZcXBisiXCSwfNd4=", 16, 1) + }; + let trie = if sealed { + make_sealed_trie_from_keys( + KEYS[..keys].iter().map(core::slice::from_ref), + Some((hash, sealed_count)), + true, + ) + } else { + make_trie_from_keys( + KEYS[..keys].iter().map(core::slice::from_ref), + Some((hash, count)), + true, + ) + }; + (trie, &KEYS[..keys]) +} + +/// Tests sealing all keys of a trie. +#[cfg(not(miri))] #[test] fn test_seal() { - const HASH: &str = "T9199/qDmjbqYqxaHrGh024lQRuTZcXBisiXCSwfNd4="; - let mut trie = do_test_inserts( - b"0123456789:;<=>?".iter().map(core::slice::from_ref), - HASH, - 16, - true, - ); - - for b in b'0'..=b'?' { - trie.seal(&[b], true); + let (mut trie, keys) = make_trie(false, false); + let hash = trie.hash().clone(); + for b in keys { + trie.seal(core::slice::from_ref(b), true); } - // Sealing doesn’t affect the hash. - let hash = CryptoHash::from_base64(HASH).unwrap(); - assert_eq!(&hash, trie.hash()); - assert_eq!(1, trie.nodes_count()); + assert_eq!((&hash, 1), (trie.hash(), trie.nodes_count())); } +/// Tests sealing all keys of a small trie. #[test] -fn test_set_and_seal() { - const HASH: &str = "T9199/qDmjbqYqxaHrGh024lQRuTZcXBisiXCSwfNd4="; - let mut trie = TestTrie::new(100); - for b in b'0'..=b'?' { - trie.set_and_seal(&[b], true); +fn test_seal_small() { + let (mut trie, keys) = make_trie(true, false); + let hash = trie.hash().clone(); + for b in keys { + trie.seal(core::slice::from_ref(b), true); } - // Sealing doesn’t affect the hash. - let hash = CryptoHash::from_base64(HASH).unwrap(); - assert_eq!(&hash, trie.hash()); - assert_eq!(1, trie.nodes_count()); + assert_eq!((&hash, 3), (trie.hash(), trie.nodes_count())); } +/// Tests using `set_and_seal` to create a trie with all keys sealed. +#[cfg(not(miri))] +#[test] +fn test_set_and_seal() { make_trie(false, true); } +/// Tests using `set_and_seal` to create a small trie with all keys sealed. #[test] -fn test_del_simple() { - let mut trie = do_test_inserts( - b"0123456789:;<=>?".iter().map(core::slice::from_ref), - "T9199/qDmjbqYqxaHrGh024lQRuTZcXBisiXCSwfNd4=", - 16, - true, - ); +fn test_set_and_seal_small() { make_trie(true, true); } - for b in b'0'..=b';' { - trie.del(&[b], true); +fn do_test_del((mut trie, keys): (TestTrie, &[u8]), want_mid_count: usize) { + let (left, right) = keys.split_at(keys.len() / 2); + for b in left { + trie.del(core::slice::from_ref(b), true); } - assert_eq!(4, trie.nodes_count()); - for b in b'<'..=b'?' { - trie.del(&[b], true); + assert_eq!(want_mid_count, trie.nodes_count()); + for b in right { + trie.del(core::slice::from_ref(b), true); } assert!(trie.is_empty()); } +/// Tests deleting all keys of a trie. +#[cfg(not(miri))] +#[test] +fn test_del() { do_test_del(make_trie(false, false), 8); } + +/// Tests deleting all keys of a small trie. +#[test] +fn test_del_small() { do_test_del(make_trie(true, false), 5); } + +/// Tests whether deleting a node in between two Extension nodes causes the two +/// Extension nodes to be rebalanced. #[test] fn test_del_extension_0() { // Construct a trie with following nodes: @@ -145,25 +190,25 @@ fn test_del_extension_0() { 00000000 00000000 0000" )[..], ]; - let mut trie = do_test_inserts( + let mut trie = make_trie_from_keys( keys, - "k/+TqL56p1FI5Y7prnZ488jE6QsP1HjbxMNrLvnDEHw=", - 5, + Some(("k/+TqL56p1FI5Y7prnZ488jE6QsP1HjbxMNrLvnDEHw=", 5)), true, ); trie.del(keys[1], true); assert_eq!(2, trie.nodes_count()); } +/// Tests whether deleting a node in between two Extension nodes causes the two +/// Extension nodes to be merged. #[test] fn test_del_extension_1() { // Construct a trie with `Extension → Value → Extension` chain and delete // the Value. The Extensions should be merged into one. let keys = [&hex!("00")[..], &hex!("00 FF")[..]]; - let mut trie = do_test_inserts( + let mut trie = make_trie_from_keys( keys, - "nmNwDIXQlBwdFRUKHk+1A6mki0W6O3EP5/LIzexY1lc=", - 3, + Some(("nmNwDIXQlBwdFRUKHk+1A6mki0W6O3EP5/LIzexY1lc=", 3)), true, ); trie.del(keys[0], true); @@ -195,10 +240,9 @@ fn stress_test() { // Insert count/2 random keys. let mut rand_keys = RandKeys { buf: &mut [0; 35], rng: rand::thread_rng() }; - let mut trie = do_test_inserts( + let mut trie = make_trie_from_keys( (&mut rand_keys).take((count / 2).max(1)), - "", - usize::MAX, + None, false, );