// This file is part of Substrate. // Copyright (C) 2021-2022 Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // 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::*; use crate::{ mock::{test_utils::*, *}, ListBags, ListNodes, }; use frame_election_provider_support::{SortedListProvider, VoteWeight}; use frame_support::{assert_noop, assert_ok, assert_storage_noop}; #[test] fn basic_setup_works() { ExtBuilder::default().build_and_execute(|| { // syntactic sugar to create a raw node let node = |id, prev, next, bag_upper| Node:: { id, prev, next, bag_upper, _phantom: PhantomData, }; assert_eq!(ListNodes::::count(), 4); assert_eq!(ListNodes::::iter().count(), 4); assert_eq!(ListBags::::iter().count(), 2); assert_eq!(List::::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]); // the state of the bags is as expected assert_eq!( ListBags::::get(10).unwrap(), Bag:: { head: Some(1), tail: Some(1), bag_upper: 0, _phantom: PhantomData } ); assert_eq!( ListBags::::get(1_000).unwrap(), Bag:: { head: Some(2), tail: Some(4), bag_upper: 0, _phantom: PhantomData } ); assert_eq!(ListNodes::::get(2).unwrap(), node(2, None, Some(3), 1_000)); assert_eq!(ListNodes::::get(3).unwrap(), node(3, Some(2), Some(4), 1_000)); assert_eq!(ListNodes::::get(4).unwrap(), node(4, Some(3), None, 1_000)); assert_eq!(ListNodes::::get(1).unwrap(), node(1, None, None, 10)); // non-existent id does not have a storage footprint assert_eq!(ListNodes::::get(42), None); // iteration of the bags would yield: assert_eq!( List::::iter().map(|n| *n.id()).collect::>(), vec![2, 3, 4, 1], // ^^ note the order of insertion in genesis! ); }); } #[test] fn notional_bag_for_works() { // under a threshold gives the next threshold. assert_eq!(notional_bag_for::(0), 10); assert_eq!(notional_bag_for::(9), 10); // at a threshold gives that threshold. assert_eq!(notional_bag_for::(10), 10); // above the threshold, gives the next threshold. assert_eq!(notional_bag_for::(11), 20); let max_explicit_threshold = *::BagThresholds::get().last().unwrap(); assert_eq!(max_explicit_threshold, 10_000); // if the max explicit threshold is less than T::Value::max_value(), assert!(VoteWeight::MAX > max_explicit_threshold); // then anything above it will belong to the T::Value::max_value() bag. assert_eq!(notional_bag_for::(max_explicit_threshold), max_explicit_threshold); assert_eq!(notional_bag_for::(max_explicit_threshold + 1), VoteWeight::MAX); } #[test] fn remove_last_node_in_bags_cleans_bag() { ExtBuilder::default().build_and_execute(|| { // given assert_eq!(List::::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]); // bump 1 to a bigger bag List::::remove(&1).unwrap(); assert_ok!(List::::insert(1, 10_000)); // then the bag with bound 10 is wiped from storage. assert_eq!(List::::get_bags(), vec![(1_000, vec![2, 3, 4]), (10_000, vec![1])]); // and can be recreated again as needed. assert_ok!(List::::insert(77, 10)); assert_eq!( List::::get_bags(), vec![(10, vec![77]), (1_000, vec![2, 3, 4]), (10_000, vec![1])] ); }); } #[test] fn migrate_works() { ExtBuilder::default() .add_ids(vec![(710, 15), (711, 16), (712, 2_000)]) .build_and_execute(|| { // given assert_eq!( List::::get_bags(), vec![ (10, vec![1]), (20, vec![710, 711]), (1_000, vec![2, 3, 4]), (2_000, vec![712]) ] ); let old_thresholds = ::BagThresholds::get(); assert_eq!(old_thresholds, vec![10, 20, 30, 40, 50, 60, 1_000, 2_000, 10_000]); // when the new thresholds adds `15` and removes `2_000` const NEW_THRESHOLDS: &'static [VoteWeight] = &[10, 15, 20, 30, 40, 50, 60, 1_000, 10_000]; BagThresholds::set(NEW_THRESHOLDS); // and we call List::::migrate(old_thresholds); // then assert_eq!( List::::get_bags(), vec![ (10, vec![1]), (15, vec![710]), // nodes in range 11 ..= 15 move from bag 20 to bag 15 (20, vec![711]), (1_000, vec![2, 3, 4]), // nodes in range 1_001 ..= 2_000 move from bag 2_000 to bag 10_000 (10_000, vec![712]), ] ); }); } mod list { use super::*; #[test] fn iteration_is_semi_sorted() { ExtBuilder::default() .add_ids(vec![(5, 2_000), (6, 2_000)]) .build_and_execute(|| { // given assert_eq!( List::::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4]), (2_000, vec![5, 6])] ); assert_eq!( get_list_as_ids(), vec![ 5, 6, // best bag 2, 3, 4, // middle bag 1, // last bag. ] ); // when adding an id that has a higher weight than pre-existing ids in the bag assert_ok!(List::::insert(7, 10)); // then assert_eq!( get_list_as_ids(), vec![ 5, 6, // best bag 2, 3, 4, // middle bag 1, 7, // last bag; new id is last. ] ); }) } /// we can `take` x ids, even if that quantity ends midway through a list. #[test] fn take_works() { ExtBuilder::default() .add_ids(vec![(5, 2_000), (6, 2_000)]) .build_and_execute(|| { // given assert_eq!( List::::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4]), (2_000, vec![5, 6])] ); // when let iteration = List::::iter().map(|node| *node.id()).take(4).collect::>(); // then assert_eq!( iteration, vec![ 5, 6, // best bag, fully iterated 2, 3, // middle bag, partially iterated ] ); }) } #[test] fn insert_works() { ExtBuilder::default().build_and_execute(|| { // when inserting into an existing bag assert_ok!(List::::insert(5, 1_000)); // then assert_eq!(List::::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4, 5])]); assert_eq!(get_list_as_ids(), vec![2, 3, 4, 5, 1]); // when inserting into a non-existent bag assert_ok!(List::::insert(6, 1_001)); // then assert_eq!( List::::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4, 5]), (2_000, vec![6])] ); assert_eq!(get_list_as_ids(), vec![6, 2, 3, 4, 5, 1]); }); } #[test] fn insert_errors_with_duplicate_id() { ExtBuilder::default().build_and_execute(|| { // given assert!(get_list_as_ids().contains(&3)); // then assert_storage_noop!(assert_eq!( List::::insert(3, 20).unwrap_err(), ListError::Duplicate )); }); } #[test] fn remove_works() { use crate::{ListBags, ListNodes}; let ensure_left = |id, counter| { assert!(!ListNodes::::contains_key(id)); assert_eq!(ListNodes::::count(), counter); assert_eq!(ListNodes::::iter().count() as u32, counter); }; ExtBuilder::default().build_and_execute(|| { // removing a non-existent id is a noop assert!(!ListNodes::::contains_key(42)); assert_noop!(List::::remove(&42), ListError::NodeNotFound); // when removing a node from a bag with multiple nodes: List::::remove(&2).unwrap(); // then assert_eq!(get_list_as_ids(), vec![3, 4, 1]); assert_eq!(List::::get_bags(), vec![(10, vec![1]), (1_000, vec![3, 4])]); ensure_left(2, 3); // when removing a node from a bag with only one node: List::::remove(&1).unwrap(); // then assert_eq!(get_list_as_ids(), vec![3, 4]); assert_eq!(List::::get_bags(), vec![(1_000, vec![3, 4])]); ensure_left(1, 2); // bag 10 is removed assert!(!ListBags::::contains_key(10)); // remove remaining ids to make sure storage cleans up as expected List::::remove(&3).unwrap(); ensure_left(3, 1); assert_eq!(get_list_as_ids(), vec![4]); List::::remove(&4).unwrap(); ensure_left(4, 0); assert_eq!(get_list_as_ids(), Vec::::new()); // bags are deleted via removals assert_eq!(ListBags::::iter().count(), 0); }); } #[test] fn remove_many_is_noop_with_non_existent_ids() { ExtBuilder::default().build_and_execute(|| { let non_existent_ids = vec![&42, &666, &13]; // when account ids don' exist in the list assert!(non_existent_ids.iter().all(|id| !BagsList::contains(id))); // then removing them is a noop assert_storage_noop!(List::::remove_many(non_existent_ids)); }); } #[test] fn update_position_for_works() { ExtBuilder::default().build_and_execute(|| { // given a correctly placed account 1 at bag 10. let node = Node::::get(&1).unwrap(); assert!(!node.is_misplaced(10)); // .. it is invalid with weight 20 assert!(node.is_misplaced(20)); // move it to bag 20. assert_eq!(List::::update_position_for(node, 20), Some((10, 20))); assert_eq!(List::::get_bags(), vec![(20, vec![1]), (1_000, vec![2, 3, 4])]); // get the new updated node; try and update the position with no change in weight. let node = Node::::get(&1).unwrap(); assert_storage_noop!(assert_eq!( List::::update_position_for(node.clone(), 20), None )); // then move it to bag 1_000 by giving it weight 500. assert_eq!(List::::update_position_for(node.clone(), 500), Some((20, 1_000))); assert_eq!(List::::get_bags(), vec![(1_000, vec![2, 3, 4, 1])]); // moving within that bag again is a noop let node = Node::::get(&1).unwrap(); assert_storage_noop!(assert_eq!( List::::update_position_for(node.clone(), 750), None, )); assert_storage_noop!(assert_eq!( List::::update_position_for(node, 1_000), None, )); }); } #[test] fn sanity_check_works() { ExtBuilder::default().build_and_execute_no_post_check(|| { assert_ok!(List::::sanity_check()); }); // make sure there are no duplicates. ExtBuilder::default().build_and_execute_no_post_check(|| { Bag::::get(10).unwrap().insert_unchecked(2); assert_eq!(List::::sanity_check(), Err("duplicate identified")); }); // ensure count is in sync with `ListNodes::count()`. ExtBuilder::default().build_and_execute_no_post_check(|| { assert_eq!(crate::ListNodes::::count(), 4); // we do some wacky stuff here to get access to the counter, since it is (reasonably) // not exposed as mutable in any sense. frame_support::generate_storage_alias!( BagsList, CounterForListNodes => Value ); CounterForListNodes::mutate(|counter| *counter += 1); assert_eq!(crate::ListNodes::::count(), 5); assert_eq!(List::::sanity_check(), Err("iter_count != stored_count")); }); } #[test] fn contains_works() { ExtBuilder::default().build_and_execute(|| { assert!(GENESIS_IDS.iter().all(|(id, _)| List::::contains(id))); let non_existent_ids = vec![&42, &666, &13]; assert!(non_existent_ids.iter().all(|id| !List::::contains(id))); }) } #[test] #[should_panic = "given nodes must always have a valid bag. qed."] fn put_in_front_of_panics_if_bag_not_found() { ExtBuilder::default().skip_genesis_ids().build_and_execute_no_post_check(|| { let node_10_no_bag = Node:: { id: 10, prev: None, next: None, bag_upper: 15, _phantom: PhantomData, }; let node_11_no_bag = Node:: { id: 11, prev: None, next: None, bag_upper: 15, _phantom: PhantomData, }; // given ListNodes::::insert(10, node_10_no_bag); ListNodes::::insert(11, node_11_no_bag); StakingMock::set_score_of(&10, 14); StakingMock::set_score_of(&11, 15); assert!(!ListBags::::contains_key(15)); assert_eq!(List::::get_bags(), vec![]); // then .. this panics let _ = List::::put_in_front_of(&10, &11); }); } #[test] fn insert_at_unchecked_at_is_only_node() { // Note that this `insert_at_unchecked` test should fail post checks because node 42 does // not get re-assigned the correct bagu pper. This is because `insert_at_unchecked` assumes // both nodes are already in the same bag with the correct bag upper. ExtBuilder::default().build_and_execute_no_post_check(|| { // given assert_eq!(List::::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]); // implicitly also test that `node`'s `prev`/`next` are correctly re-assigned. let node_42 = Node:: { id: 42, prev: Some(1), next: Some(2), bag_upper: 1_000, _phantom: PhantomData, }; assert!(!crate::ListNodes::::contains_key(42)); let node_1 = crate::ListNodes::::get(&1).unwrap(); // when List::::insert_at_unchecked(node_1, node_42); // then assert_eq!( List::::get_bags(), vec![(10, vec![42, 1]), (1_000, vec![2, 3, 4])] ); }) } #[test] fn insert_at_unchecked_at_is_head() { ExtBuilder::default().build_and_execute(|| { // given assert_eq!(List::::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]); // implicitly also test that `node`'s `prev`/`next` are correctly re-assigned. let node_42 = Node:: { id: 42, prev: Some(4), next: None, bag_upper: 1_000, _phantom: PhantomData, }; assert!(!crate::ListNodes::::contains_key(42)); let node_2 = crate::ListNodes::::get(&2).unwrap(); // when List::::insert_at_unchecked(node_2, node_42); // then assert_eq!( List::::get_bags(), vec![(10, vec![1]), (1_000, vec![42, 2, 3, 4])] ); }) } #[test] fn insert_at_unchecked_at_is_non_terminal() { ExtBuilder::default().build_and_execute(|| { // given assert_eq!(List::::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]); // implicitly also test that `node`'s `prev`/`next` are correctly re-assigned. let node_42 = Node:: { id: 42, prev: None, next: Some(2), bag_upper: 1_000, _phantom: PhantomData, }; assert!(!crate::ListNodes::::contains_key(42)); let node_3 = crate::ListNodes::::get(&3).unwrap(); // when List::::insert_at_unchecked(node_3, node_42); // then assert_eq!( List::::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 42, 3, 4])] ); }) } #[test] fn insert_at_unchecked_at_is_tail() { ExtBuilder::default().build_and_execute(|| { // given assert_eq!(List::::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]); // implicitly also test that `node`'s `prev`/`next` are correctly re-assigned. let node_42 = Node:: { id: 42, prev: Some(42), next: Some(42), bag_upper: 1_000, _phantom: PhantomData, }; assert!(!crate::ListNodes::::contains_key(42)); let node_4 = crate::ListNodes::::get(&4).unwrap(); // when List::::insert_at_unchecked(node_4, node_42); // then assert_eq!( List::::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 42, 4])] ); }) } } mod bags { use super::*; #[test] fn get_works() { ExtBuilder::default().build_and_execute(|| { let check_bag = |bag_upper, head, tail, ids| { let bag = Bag::::get(bag_upper).unwrap(); let bag_ids = bag.iter().map(|n| *n.id()).collect::>(); assert_eq!(bag, Bag:: { head, tail, bag_upper, _phantom: PhantomData }); assert_eq!(bag_ids, ids); }; assert_eq!(List::::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]); // we can fetch them check_bag(10, Some(1), Some(1), vec![1]); check_bag(1_000, Some(2), Some(4), vec![2, 3, 4]); // and all other bag thresholds don't get bags. ::BagThresholds::get() .iter() .chain(iter::once(&VoteWeight::MAX)) .filter(|bag_upper| !vec![10, 1_000].contains(bag_upper)) .for_each(|bag_upper| { assert_storage_noop!(assert_eq!(Bag::::get(*bag_upper), None)); assert!(!ListBags::::contains_key(*bag_upper)); }); // when we make a pre-existing bag empty List::::remove(&1).unwrap(); // then assert_eq!(Bag::::get(10), None) }); } #[test] fn insert_node_sets_proper_bag() { ExtBuilder::default().build_and_execute_no_post_check(|| { let node = |id, bag_upper| Node:: { id, prev: None, next: None, bag_upper, _phantom: PhantomData, }; assert_eq!(List::::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]); let mut bag_10 = Bag::::get(10).unwrap(); bag_10.insert_node_unchecked(node(42, 5)); assert_eq!( ListNodes::::get(&42).unwrap(), Node { bag_upper: 10, prev: Some(1), next: None, id: 42, _phantom: PhantomData } ); }); } #[test] fn insert_node_happy_paths_works() { ExtBuilder::default().build_and_execute_no_post_check(|| { let node = |id, bag_upper| Node:: { id, prev: None, next: None, bag_upper, _phantom: PhantomData, }; // when inserting into a bag with 1 node let mut bag_10 = Bag::::get(10).unwrap(); bag_10.insert_node_unchecked(node(42, bag_10.bag_upper)); // then assert_eq!(bag_as_ids(&bag_10), vec![1, 42]); // when inserting into a bag with 3 nodes let mut bag_1000 = Bag::::get(1_000).unwrap(); bag_1000.insert_node_unchecked(node(52, bag_1000.bag_upper)); // then assert_eq!(bag_as_ids(&bag_1000), vec![2, 3, 4, 52]); // when inserting into a new bag let mut bag_20 = Bag::::get_or_make(20); bag_20.insert_node_unchecked(node(62, bag_20.bag_upper)); // then assert_eq!(bag_as_ids(&bag_20), vec![62]); // when inserting a node pointing to the accounts not in the bag let node_61 = Node:: { id: 61, prev: Some(21), next: Some(101), bag_upper: 20, _phantom: PhantomData, }; bag_20.insert_node_unchecked(node_61); // then ids are in order assert_eq!(bag_as_ids(&bag_20), vec![62, 61]); // and when the node is re-fetched all the info is correct assert_eq!( Node::::get(&61).unwrap(), Node:: { id: 61, prev: Some(62), next: None, bag_upper: 20, _phantom: PhantomData, } ); // state of all bags is as expected bag_20.put(); // need to put this newly created bag so its in the storage map assert_eq!( List::::get_bags(), vec![(10, vec![1, 42]), (20, vec![62, 61]), (1_000, vec![2, 3, 4, 52])] ); }); } // Document improper ways `insert_node` may be getting used. #[test] fn insert_node_bad_paths_documented() { let node = |id, prev, next, bag_upper| Node:: { id, prev, next, bag_upper, _phantom: PhantomData, }; ExtBuilder::default().build_and_execute_no_post_check(|| { // when inserting a node with both prev & next pointing at an account in an incorrect // bag. let mut bag_1000 = Bag::::get(1_000).unwrap(); bag_1000.insert_node_unchecked(node(42, Some(1), Some(1), 500)); // then the proper prev and next is set. assert_eq!(bag_as_ids(&bag_1000), vec![2, 3, 4, 42]); // and when the node is re-fetched all the info is correct assert_eq!( Node::::get(&42).unwrap(), node(42, Some(4), None, bag_1000.bag_upper) ); }); ExtBuilder::default().build_and_execute_no_post_check(|| { // given 3 is in bag_1000 (and not a tail node) let mut bag_1000 = Bag::::get(1_000).unwrap(); assert_eq!(bag_as_ids(&bag_1000), vec![2, 3, 4]); // when inserting a node with duplicate id 3 bag_1000.insert_node_unchecked(node(3, None, None, bag_1000.bag_upper)); // then all the nodes after the duplicate are lost (because it is set as the tail) assert_eq!(bag_as_ids(&bag_1000), vec![2, 3]); // also in the full iteration, 2 and 3 are from bag_1000 and 1 is from bag_10. assert_eq!(get_list_as_ids(), vec![2, 3, 1]); // and the last accessible node has an **incorrect** prev pointer. assert_eq!( Node::::get(&3).unwrap(), node(3, Some(4), None, bag_1000.bag_upper) ); }); ExtBuilder::default().build_and_execute_no_post_check(|| { // when inserting a duplicate id of the head let mut bag_1000 = Bag::::get(1_000).unwrap(); assert_eq!(bag_as_ids(&bag_1000), vec![2, 3, 4]); bag_1000.insert_node_unchecked(node(2, None, None, 0)); // then all nodes after the head are lost assert_eq!(bag_as_ids(&bag_1000), vec![2]); // and the re-fetched node has bad pointers assert_eq!( Node::::get(&2).unwrap(), node(2, Some(4), None, bag_1000.bag_upper) ); // ^^^ despite being the bags head, it has a prev assert_eq!( bag_1000, Bag { head: Some(2), tail: Some(2), bag_upper: 1_000, _phantom: PhantomData } ) }); } // Panics in case of duplicate tail insert (which would result in an infinite loop). #[test] #[cfg_attr( debug_assertions, should_panic = "system logic error: inserting a node who has the id of tail" )] fn insert_node_duplicate_tail_panics_with_debug_assert() { ExtBuilder::default().build_and_execute(|| { let node = |id, prev, next, bag_upper| Node:: { id, prev, next, bag_upper, _phantom: PhantomData, }; // given assert_eq!(List::::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])],); let mut bag_1000 = Bag::::get(1_000).unwrap(); // when inserting a duplicate id that is already the tail assert_eq!(bag_1000.tail, Some(4)); assert_eq!(bag_1000.iter().count(), 3); bag_1000.insert_node_unchecked(node(4, None, None, bag_1000.bag_upper)); // panics in debug assert_eq!(bag_1000.iter().count(), 3); // in release we expect it to silently ignore the request. }); } #[test] fn remove_node_happy_paths_works() { ExtBuilder::default() .add_ids(vec![ (11, 10), (12, 10), (13, 1_000), (14, 1_000), (15, 2_000), (16, 2_000), (17, 2_000), (18, 2_000), (19, 2_000), ]) .build_and_execute_no_post_check(|| { let mut bag_10 = Bag::::get(10).unwrap(); let mut bag_1000 = Bag::::get(1_000).unwrap(); let mut bag_2000 = Bag::::get(2_000).unwrap(); // given assert_eq!(bag_as_ids(&bag_10), vec![1, 11, 12]); assert_eq!(bag_as_ids(&bag_1000), vec![2, 3, 4, 13, 14]); assert_eq!(bag_as_ids(&bag_2000), vec![15, 16, 17, 18, 19]); // when removing a node that is not pointing at the head or tail let node_4 = Node::::get(&4).unwrap(); let node_4_pre_remove = node_4.clone(); bag_1000.remove_node_unchecked(&node_4); // then assert_eq!(bag_as_ids(&bag_1000), vec![2, 3, 13, 14]); assert_ok!(bag_1000.sanity_check()); // and the node isn't mutated when its removed assert_eq!(node_4, node_4_pre_remove); // when removing a head that is not pointing at the tail let node_2 = Node::::get(&2).unwrap(); bag_1000.remove_node_unchecked(&node_2); // then assert_eq!(bag_as_ids(&bag_1000), vec![3, 13, 14]); assert_ok!(bag_1000.sanity_check()); // when removing a tail that is not pointing at the head let node_14 = Node::::get(&14).unwrap(); bag_1000.remove_node_unchecked(&node_14); // then assert_eq!(bag_as_ids(&bag_1000), vec![3, 13]); assert_ok!(bag_1000.sanity_check()); // when removing a tail that is pointing at the head let node_13 = Node::::get(&13).unwrap(); bag_1000.remove_node_unchecked(&node_13); // then assert_eq!(bag_as_ids(&bag_1000), vec![3]); assert_ok!(bag_1000.sanity_check()); // when removing a node that is both the head & tail let node_3 = Node::::get(&3).unwrap(); bag_1000.remove_node_unchecked(&node_3); bag_1000.put(); // put into storage so `get` returns the updated bag // then assert_eq!(Bag::::get(1_000), None); // when removing a node that is pointing at both the head & tail let node_11 = Node::::get(&11).unwrap(); bag_10.remove_node_unchecked(&node_11); // then assert_eq!(bag_as_ids(&bag_10), vec![1, 12]); assert_ok!(bag_10.sanity_check()); // when removing a head that is pointing at the tail let node_1 = Node::::get(&1).unwrap(); bag_10.remove_node_unchecked(&node_1); // then assert_eq!(bag_as_ids(&bag_10), vec![12]); assert_ok!(bag_10.sanity_check()); // and since we updated the bag's head/tail, we need to write this storage so we // can correctly `get` it again in later checks bag_10.put(); // when removing a node that is pointing at the head but not the tail let node_16 = Node::::get(&16).unwrap(); bag_2000.remove_node_unchecked(&node_16); // then assert_eq!(bag_as_ids(&bag_2000), vec![15, 17, 18, 19]); assert_ok!(bag_2000.sanity_check()); // when removing a node that is pointing at tail, but not head let node_18 = Node::::get(&18).unwrap(); bag_2000.remove_node_unchecked(&node_18); // then assert_eq!(bag_as_ids(&bag_2000), vec![15, 17, 19]); assert_ok!(bag_2000.sanity_check()); // finally, when reading from storage, the state of all bags is as expected assert_eq!( List::::get_bags(), vec![(10, vec![12]), (2_000, vec![15, 17, 19])] ); }); } #[test] fn remove_node_bad_paths_documented() { ExtBuilder::default().build_and_execute_no_post_check(|| { let bad_upper_node_2 = Node:: { id: 2, prev: None, next: Some(3), bag_upper: 10, // should be 1_000 _phantom: PhantomData, }; let mut bag_1000 = Bag::::get(1_000).unwrap(); // when removing a node that is in the bag but has the wrong upper bag_1000.remove_node_unchecked(&bad_upper_node_2); bag_1000.put(); // then the node is no longer in any bags assert_eq!(List::::get_bags(), vec![(10, vec![1]), (1_000, vec![3, 4])]); // .. and the bag it was removed from let bag_1000 = Bag::::get(1_000).unwrap(); // is sane assert_ok!(bag_1000.sanity_check()); // and has the correct head and tail. assert_eq!(bag_1000.head, Some(3)); assert_eq!(bag_1000.tail, Some(4)); }); // Removing a node that is in another bag, will mess up that other bag. ExtBuilder::default().build_and_execute_no_post_check(|| { // given a tail node is in bag 1_000 let node_4 = Node::::get(&4).unwrap(); // when we remove it from bag 10 let mut bag_10 = Bag::::get(10).unwrap(); bag_10.remove_node_unchecked(&node_4); bag_10.put(); // then bag remove was called on is ok, let bag_10 = Bag::::get(10).unwrap(); assert_eq!(bag_10.tail, Some(1)); assert_eq!(bag_10.head, Some(1)); // but the bag that the node belonged to is in an invalid state let bag_1000 = Bag::::get(1_000).unwrap(); // because it still has the removed node as its tail. assert_eq!(bag_1000.tail, Some(4)); assert_eq!(bag_1000.head, Some(2)); }); } } mod node { use super::*; #[test] fn is_misplaced_works() { ExtBuilder::default().build_and_execute(|| { let node = Node::::get(&1).unwrap(); // given assert_eq!(node.bag_upper, 10); // then within bag 10 its not misplaced, assert!(!node.is_misplaced(0)); assert!(!node.is_misplaced(9)); assert!(!node.is_misplaced(10)); // and out of bag 10 it is misplaced assert!(node.is_misplaced(11)); }); } }