From 5e884a6a3690bc4e187577a6fcd5b73da7c941cc Mon Sep 17 00:00:00 2001 From: Rain Date: Sun, 19 Jul 2026 18:24:03 -0700 Subject: [PATCH] [spr] initial version Created using spr 1.3.6-beta.1 --- CHANGELOG.md | 6 + crates/iddqd/src/bi_hash_map/imp.rs | 105 ++++++++++++++- crates/iddqd/src/id_hash_map/imp.rs | 77 +++++++++++ crates/iddqd/src/tri_hash_map/imp.rs | 126 +++++++++++++++++- crates/iddqd/tests/integration/bi_hash_map.rs | 95 ++++++++++++- crates/iddqd/tests/integration/id_hash_map.rs | 58 +++++++- .../iddqd/tests/integration/pathological.rs | 23 ++++ .../iddqd/tests/integration/tri_hash_map.rs | 111 ++++++++++++++- 8 files changed, 586 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a45d6443..45d81adb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## Unreleased - ReleaseDate +### Added + +- `from_iter_unique` constructors on `IdHashMap`, `BiHashMap`, and `TriHashMap`, matching the existing `IdOrdMap::from_iter_unique`. These build a map from an iterator and, rather than overwriting, return an error on the first item that conflicts with an already-inserted one. + + Because a value in a `BiHashMap` or `TriHashMap` can conflict on more than one key at once, the error reports every distinct existing item it collides with (up to two for `BiHashMap` and up to three for `TriHashMap`). + ### Changed - MSRV updated to Rust 1.86. diff --git a/crates/iddqd/src/bi_hash_map/imp.rs b/crates/iddqd/src/bi_hash_map/imp.rs index ab010741..6cf95db4 100644 --- a/crates/iddqd/src/bi_hash_map/imp.rs +++ b/crates/iddqd/src/bi_hash_map/imp.rs @@ -540,6 +540,91 @@ impl } } +impl + BiHashMap +{ + /// Creates a new `BiHashMap` from an iterator of values, rejecting + /// duplicates. + /// + /// A value conflicts when either of its keys matches an + /// already-inserted item, so a single value can collide with up to two + /// distinct existing items (one per key). On the first conflict, this + /// returns a [`DuplicateItem`] error containing the new value and every + /// conflicting item. + /// + /// To overwrite duplicates instead, use [`BiHashMap::from_iter`]. + /// + /// # Examples + /// + /// ``` + /// # #[cfg(feature = "default-hasher")] { + /// use iddqd::{BiHashItem, BiHashMap, bi_upcast}; + /// + /// #[derive(Debug, PartialEq, Eq)] + /// struct Item { + /// id: u32, + /// name: String, + /// value: i32, + /// } + /// + /// impl BiHashItem for Item { + /// type K1<'a> = u32; + /// type K2<'a> = &'a str; + /// + /// fn key1(&self) -> Self::K1<'_> { + /// self.id + /// } + /// fn key2(&self) -> Self::K2<'_> { + /// &self.name + /// } + /// bi_upcast!(); + /// } + /// + /// let items = vec![ + /// Item { id: 1, name: "foo".to_string(), value: 42 }, + /// Item { id: 2, name: "bar".to_string(), value: 99 }, + /// ]; + /// + /// // Successful creation with unique keys. + /// let map: BiHashMap = BiHashMap::from_iter_unique(items).unwrap(); + /// assert_eq!(map.len(), 2); + /// assert_eq!(map.get1(&1).unwrap().value, 42); + /// + /// // Error with a duplicate key1. + /// let duplicate_items = vec![ + /// Item { id: 1, name: "foo".to_string(), value: 42 }, + /// Item { id: 1, name: "baz".to_string(), value: 99 }, + /// ]; + /// assert!(BiHashMap::::from_iter_unique(duplicate_items).is_err()); + /// # } + /// ``` + pub fn from_iter_unique>( + iter: I, + ) -> Result> { + let iter = iter.into_iter(); + let mut map = Self::default(); + map.reserve(iter.size_hint().0); + for value in iter { + if let Err((value, indexes)) = + map.insert_unique_or_dup_indexes(value) + { + // Removal produces owned duplicates, so that we don't need to + // specify `T: Clone` here. + let duplicates = indexes + .iter() + .map(|ix| { + map.remove_by_index(*ix) + .expect("duplicate index is present") + }) + .collect(); + return Err(DuplicateItem::__internal_new(value, duplicates)); + } + } + + Ok(map) + } +} + impl BiHashMap { /// Returns the hasher. #[cfg(feature = "daft")] @@ -2303,6 +2388,19 @@ impl BiHashMap { &mut self, value: T, ) -> Result> { + match self.insert_unique_or_dup_indexes(value) { + Ok(index) => Ok(index), + Err((value, duplicates)) => Err(DuplicateItem::__internal_new( + value, + duplicates.iter().map(|ix| &self.items[*ix]).collect(), + )), + } + } + + fn insert_unique_or_dup_indexes( + &mut self, + value: T, + ) -> Result)> { let mut duplicates = BTreeSet::new(); // Check for duplicates *before* inserting the new item, because we @@ -2329,10 +2427,7 @@ impl BiHashMap { }; if !duplicates.is_empty() { - return Err(DuplicateItem::__internal_new( - value, - duplicates.iter().map(|ix| &self.items[*ix]).collect(), - )); + return Err((value, duplicates)); } let next_index = self.items.assert_can_grow().insert(value); @@ -2789,6 +2884,8 @@ impl IntoIterator /// The `FromIterator` implementation for `BiHashMap` overwrites duplicate /// items. /// +/// To reject duplicates, use [`BiHashMap::from_iter_unique`]. +/// /// # Examples /// /// ``` diff --git a/crates/iddqd/src/id_hash_map/imp.rs b/crates/iddqd/src/id_hash_map/imp.rs index 926f085e..ba271403 100644 --- a/crates/iddqd/src/id_hash_map/imp.rs +++ b/crates/iddqd/src/id_hash_map/imp.rs @@ -417,6 +417,81 @@ impl IdHashMap { } } +impl + IdHashMap +{ + /// Creates a new `IdHashMap` from an iterator of values, rejecting + /// duplicates. + /// + /// To overwrite duplicates instead, use [`IdHashMap::from_iter`]. + /// + /// # Examples + /// + /// ``` + /// # #[cfg(feature = "default-hasher")] { + /// use iddqd::{IdHashItem, IdHashMap, id_upcast}; + /// + /// #[derive(Debug, PartialEq, Eq, Hash)] + /// struct Item { + /// id: String, + /// value: u32, + /// } + /// + /// impl IdHashItem for Item { + /// type Key<'a> = &'a str; + /// fn key(&self) -> Self::Key<'_> { + /// &self.id + /// } + /// id_upcast!(); + /// } + /// + /// let items = vec![ + /// Item { id: "foo".to_string(), value: 42 }, + /// Item { id: "bar".to_string(), value: 99 }, + /// ]; + /// + /// // Successful creation with unique keys. + /// let map: IdHashMap = IdHashMap::from_iter_unique(items).unwrap(); + /// assert_eq!(map.len(), 2); + /// assert_eq!(map.get("foo").unwrap().value, 42); + /// + /// // Error with duplicate keys. + /// let duplicate_items = vec![ + /// Item { id: "foo".to_string(), value: 42 }, + /// Item { id: "foo".to_string(), value: 99 }, + /// ]; + /// assert!(IdHashMap::::from_iter_unique(duplicate_items).is_err()); + /// # } + /// ``` + pub fn from_iter_unique>( + iter: I, + ) -> Result> { + let iter = iter.into_iter(); + let mut map = Self::default(); + map.reserve(iter.size_hint().0); + for value in iter { + // It would be nice to use insert_unique here, but that would return + // a `DuplicateItem`, which can only be converted into an + // owned value if T: Clone. Doing this via the Entry API means we + // can return a `DuplicateItem` without requiring T to be Clone. + match map.entry(value.key()) { + Entry::Occupied(entry) => { + let duplicate = entry.remove(); + return Err(DuplicateItem::__internal_new( + value, + vec![duplicate], + )); + } + Entry::Vacant(entry) => { + entry.insert_known_unique(value); + } + } + } + + Ok(map) + } +} + impl IdHashMap { #[cfg(feature = "daft")] pub(crate) fn hasher(&self) -> &S { @@ -1816,6 +1891,8 @@ impl IntoIterator /// The `FromIterator` implementation for `IdHashMap` overwrites duplicate /// items. /// +/// To reject duplicates, use [`IdHashMap::from_iter_unique`]. +/// /// # Examples /// /// ``` diff --git a/crates/iddqd/src/tri_hash_map/imp.rs b/crates/iddqd/src/tri_hash_map/imp.rs index 3454a562..a4eaf54a 100644 --- a/crates/iddqd/src/tri_hash_map/imp.rs +++ b/crates/iddqd/src/tri_hash_map/imp.rs @@ -585,6 +585,110 @@ impl } } +impl + TriHashMap +{ + /// Creates a new `TriHashMap` from an iterator of values, rejecting + /// duplicates. + /// + /// A value conflicts when any of its keys matches an already-inserted + /// item, so a single value can collide with up to three distinct + /// existing items (one per key). On the first conflict, this returns a + /// [`DuplicateItem`] error containing the new value and every + /// conflicting item. + /// + /// To overwrite duplicates instead, use [`TriHashMap::from_iter`]. + /// + /// # Examples + /// + /// ``` + /// # #[cfg(feature = "default-hasher")] { + /// use iddqd::{TriHashItem, TriHashMap, tri_upcast}; + /// + /// #[derive(Debug, PartialEq, Eq)] + /// struct Item { + /// id: u32, + /// name: String, + /// email: String, + /// } + /// + /// impl TriHashItem for Item { + /// type K1<'a> = u32; + /// type K2<'a> = &'a str; + /// type K3<'a> = &'a str; + /// fn key1(&self) -> Self::K1<'_> { + /// self.id + /// } + /// fn key2(&self) -> Self::K2<'_> { + /// &self.name + /// } + /// fn key3(&self) -> Self::K3<'_> { + /// &self.email + /// } + /// tri_upcast!(); + /// } + /// + /// let items = vec![ + /// Item { + /// id: 1, + /// name: "foo".to_string(), + /// email: "foo@example.com".to_string(), + /// }, + /// Item { + /// id: 2, + /// name: "bar".to_string(), + /// email: "bar@example.com".to_string(), + /// }, + /// ]; + /// + /// // Successful creation with unique keys. + /// let map: TriHashMap = TriHashMap::from_iter_unique(items).unwrap(); + /// assert_eq!(map.len(), 2); + /// assert_eq!(map.get1(&1).unwrap().name, "foo"); + /// + /// // Error with a duplicate key3. + /// let duplicate_items = vec![ + /// Item { + /// id: 1, + /// name: "foo".to_string(), + /// email: "dup@example.com".to_string(), + /// }, + /// Item { + /// id: 2, + /// name: "bar".to_string(), + /// email: "dup@example.com".to_string(), + /// }, + /// ]; + /// assert!(TriHashMap::::from_iter_unique(duplicate_items).is_err()); + /// # } + /// ``` + pub fn from_iter_unique>( + iter: I, + ) -> Result> { + let iter = iter.into_iter(); + let mut map = Self::default(); + map.reserve(iter.size_hint().0); + for value in iter { + if let Err((value, indexes)) = + map.insert_unique_or_dup_indexes(value) + { + // Removal produces owned duplicates, so that we don't need to + // specify `T: Clone` here. + let duplicates = indexes + .iter() + .map(|ix| { + map.remove_by_index(*ix) + .expect("duplicate index is present") + }) + .collect(); + return Err(DuplicateItem::__internal_new(value, duplicates)); + } + } + + Ok(map) + } +} + impl TriHashMap { /// Returns the hasher. #[cfg(feature = "daft")] @@ -1489,6 +1593,19 @@ impl TriHashMap { &mut self, value: T, ) -> Result<(), DuplicateItem> { + match self.insert_unique_or_dup_indexes(value) { + Ok(_) => Ok(()), + Err((value, duplicates)) => Err(DuplicateItem::__internal_new( + value, + duplicates.iter().map(|ix| &self.items[*ix]).collect(), + )), + } + } + + fn insert_unique_or_dup_indexes( + &mut self, + value: T, + ) -> Result)> { let mut duplicates = BTreeSet::new(); // Check for duplicates *before* inserting the new item, because we @@ -1522,10 +1639,7 @@ impl TriHashMap { }; if !duplicates.is_empty() { - return Err(DuplicateItem::__internal_new( - value, - duplicates.iter().map(|ix| &self.items[*ix]).collect(), - )); + return Err((value, duplicates)); } let next_index = self.items.assert_can_grow().insert(value); @@ -1535,7 +1649,7 @@ impl TriHashMap { e2.unwrap().insert(next_index); e3.unwrap().insert(next_index); - Ok(()) + Ok(next_index) } /// Returns true if the map contains a single item that matches all three @@ -3191,6 +3305,8 @@ impl IntoIterator /// The `FromIterator` implementation for `TriHashMap` overwrites duplicate /// items. /// +/// To reject duplicates, use [`TriHashMap::from_iter_unique`]. +/// /// # Examples /// /// ``` diff --git a/crates/iddqd/tests/integration/bi_hash_map.rs b/crates/iddqd/tests/integration/bi_hash_map.rs index 5adccce1..fdeeba00 100644 --- a/crates/iddqd/tests/integration/bi_hash_map.rs +++ b/crates/iddqd/tests/integration/bi_hash_map.rs @@ -543,14 +543,22 @@ fn proptest_permutation_eq(tc: TestCase) { let mut map1 = BiHashMap::::make_new(); let mut map2 = BiHashMap::::make_new(); - for item in set { + for item in set.clone() { map1.insert_unique(item).expect("set is deduplicated"); } - for item in set2 { + for item in set2.clone() { map2.insert_unique(item).expect("set is deduplicated"); } assert_eq_props(&map1, &map2); + + let map3 = BiHashMap::::from_iter_unique(set) + .unwrap(); + let map4 = + BiHashMap::::from_iter_unique(set2) + .unwrap(); + assert_eq_props(&map1, &map3); + assert_eq_props(&map3, &map4); } // Test various conditions for non-equality. @@ -613,6 +621,89 @@ fn test_permutation_eq_examples() { } } +#[test] +fn from_iter_unique_empty_is_ok() { + let map = + BiHashMap::::from_iter_unique(Vec::new()) + .expect("empty iterator yields an empty map"); + assert!(map.is_empty()); +} + +#[test] +fn from_iter_unique_success_matches_insert_unique() { + let items = [ + TestItem::new(1, 'a', "x", "first"), + TestItem::new(2, 'b', "y", "second"), + TestItem::new(3, 'c', "z", "third"), + ]; + + let map = BiHashMap::::from_iter_unique( + items.clone(), + ) + .expect("unique keys build a map"); + + let mut expected = BiHashMap::::make_new(); + for item in items { + expected.insert_unique(item).expect("items are unique"); + } + + assert_eq_props(&map, &expected); +} + +#[test] +fn from_iter_unique_duplicate_key1_reports_error() { + let existing = TestItem::new(1, 'a', "x", "first"); + let new_item = TestItem::new(1, 'z', "w", "dup"); + let items = [existing.clone(), new_item.clone()]; + + let error = + BiHashMap::::from_iter_unique(items) + .unwrap_err(); + assert_eq!(error.new_item(), &new_item); + assert_eq!(error.duplicates(), &[existing]); +} + +#[test] +fn from_iter_unique_duplicate_key2_reports_error() { + let existing = TestItem::new(1, 'a', "x", "first"); + let new_item = TestItem::new(9, 'a', "w", "dup"); + let items = [existing.clone(), new_item.clone()]; + + let error = + BiHashMap::::from_iter_unique(items) + .unwrap_err(); + assert_eq!(error.new_item(), &new_item); + assert_eq!(error.duplicates(), &[existing]); +} + +#[test] +fn from_iter_unique_both_keys_match_one_item_reports_single_duplicate() { + let existing = TestItem::new(1, 'a', "x", "first"); + let other = TestItem::new(2, 'b', "y", "second"); + let new_item = TestItem::new(1, 'a', "w", "dup"); + let items = [existing.clone(), other, new_item.clone()]; + + let error = + BiHashMap::::from_iter_unique(items) + .unwrap_err(); + assert_eq!(error.new_item(), &new_item); + assert_eq!(error.duplicates(), &[existing]); +} + +#[test] +fn from_iter_unique_keys_match_distinct_items_reports_both() { + let a = TestItem::new(1, 'a', "x", "a"); + let b = TestItem::new(2, 'b', "y", "b"); + let new_item = TestItem::new(1, 'b', "w", "dup"); + let items = [a.clone(), b.clone(), new_item.clone()]; + + let error = + BiHashMap::::from_iter_unique(items) + .unwrap_err(); + assert_eq!(error.new_item(), &new_item); + assert_eq!(error.duplicates(), &[a, b]); +} + #[test] #[should_panic(expected = "key1 changed during RefMut borrow")] fn get_mut_panics_if_key1_changes() { diff --git a/crates/iddqd/tests/integration/id_hash_map.rs b/crates/iddqd/tests/integration/id_hash_map.rs index cc5e68a1..063baec3 100644 --- a/crates/iddqd/tests/integration/id_hash_map.rs +++ b/crates/iddqd/tests/integration/id_hash_map.rs @@ -176,6 +176,52 @@ fn test_extend() { assert_eq!(map.get(&TestKey1::new(&2)).unwrap().value, "w"); } +#[test] +fn from_iter_unique_duplicate_key_reports_error() { + let existing = TestItem::new(1, 'a', "x", "first"); + let new_item = TestItem::new(1, 'c', "z", "dup"); + let items = [ + existing.clone(), + TestItem::new(2, 'b', "y", "second"), + new_item.clone(), + ]; + + let error = + IdHashMap::::from_iter_unique(items) + .unwrap_err(); + assert_eq!(error.new_item(), &new_item); + assert_eq!(error.duplicates(), &[existing]); +} + +#[test] +fn from_iter_unique_empty_is_ok() { + let map = + IdHashMap::::from_iter_unique(Vec::new()) + .expect("empty iterator yields an empty map"); + assert!(map.is_empty()); +} + +#[test] +fn from_iter_unique_success_matches_insert_unique() { + let items = [ + TestItem::new(1, 'a', "x", "first"), + TestItem::new(2, 'b', "y", "second"), + TestItem::new(3, 'c', "z", "third"), + ]; + + let map = IdHashMap::::from_iter_unique( + items.clone(), + ) + .expect("unique keys build a map"); + + let mut expected = IdHashMap::::make_new(); + for item in items { + expected.insert_unique(item).expect("items are unique"); + } + + assert_eq_props(&map, &expected); +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum CompactnessChange { /// The operation makes the map non-compact. @@ -424,14 +470,22 @@ fn proptest_permutation_eq(tc: TestCase) { let mut map1 = IdHashMap::::make_new(); let mut map2 = IdHashMap::::make_new(); - for item in set { + for item in set.clone() { map1.insert_unique(item).expect("set is deduplicated"); } - for item in set2 { + for item in set2.clone() { map2.insert_unique(item).expect("set is deduplicated"); } assert_eq_props(&map1, &map2); + + let map3 = IdHashMap::::from_iter_unique(set) + .unwrap(); + let map4 = + IdHashMap::::from_iter_unique(set2) + .unwrap(); + assert_eq_props(&map1, &map3); + assert_eq_props(&map3, &map4); } // Test various conditions for non-equality. diff --git a/crates/iddqd/tests/integration/pathological.rs b/crates/iddqd/tests/integration/pathological.rs index 5d94d9f6..8659ca93 100644 --- a/crates/iddqd/tests/integration/pathological.rs +++ b/crates/iddqd/tests/integration/pathological.rs @@ -1085,6 +1085,29 @@ fn id_hash_flip_key_insert_overwrite_inserts_under_stale_hash() { ); } +#[test] +fn id_hash_flip_key_from_iter_unique_inserts_under_stale_hash() { + let map = + IdHashMap::::from_iter_unique([ + FlipItem::plain(0), + FlipItem::plain(1), + FlipItem::plain(2), + FlipItem::flips_after_first_key_call(99, 42), + ]) + .expect("the flip evades the duplicate check"); + + assert_eq!(map.len(), 4); + assert!(map.get(&99u32).is_none()); + assert!(map.get(&42u32).is_none()); + assert!( + map.validate(ValidateCompact::NonCompact).is_err(), + "the stale-hash item is unfindable by its current key" + ); + map.validate_structural(ValidateCompact::NonCompact).expect( + "map remains structurally sound after a flip-key from_iter_unique", + ); +} + #[test] fn id_ord_flip_key_insert_overwrite_inserts_logical_duplicate() { let mut map = IdOrdMap::::new(); diff --git a/crates/iddqd/tests/integration/tri_hash_map.rs b/crates/iddqd/tests/integration/tri_hash_map.rs index fce9df94..7828d5cb 100644 --- a/crates/iddqd/tests/integration/tri_hash_map.rs +++ b/crates/iddqd/tests/integration/tri_hash_map.rs @@ -548,14 +548,23 @@ fn proptest_permutation_eq(tc: TestCase) { let mut map1 = TriHashMap::::make_new(); let mut map2 = TriHashMap::::make_new(); - for item in set { + for item in set.clone() { map1.insert_unique(item).expect("set is deduplicated"); } - for item in set2 { + for item in set2.clone() { map2.insert_unique(item).expect("set is deduplicated"); } assert_eq_props(&map1, &map2); + + let map3 = + TriHashMap::::from_iter_unique(set) + .unwrap(); + let map4 = + TriHashMap::::from_iter_unique(set2) + .unwrap(); + assert_eq_props(&map1, &map3); + assert_eq_props(&map3, &map4); } // Test various conditions for non-equality. @@ -632,6 +641,104 @@ fn test_permutation_eq_examples() { } } +#[test] +fn from_iter_unique_empty_is_ok() { + let map = TriHashMap::::from_iter_unique( + Vec::new(), + ) + .expect("empty iterator yields an empty map"); + assert!(map.is_empty()); +} + +#[test] +fn from_iter_unique_success_matches_insert_unique() { + let items = [ + TestItem::new(1, 'a', "x", "first"), + TestItem::new(2, 'b', "y", "second"), + TestItem::new(3, 'c', "z", "third"), + ]; + + let map = TriHashMap::::from_iter_unique( + items.clone(), + ) + .expect("unique keys build a map"); + + let mut expected = TriHashMap::::make_new(); + for item in items { + expected.insert_unique(item).expect("items are unique"); + } + + assert_eq_props(&map, &expected); +} + +#[test] +fn from_iter_unique_duplicate_key1_reports_error() { + let existing = TestItem::new(1, 'a', "x", "first"); + let new_item = TestItem::new(1, 'z', "zz", "dup"); + let items = [existing.clone(), new_item.clone()]; + + let error = + TriHashMap::::from_iter_unique(items) + .unwrap_err(); + assert_eq!(error.new_item(), &new_item); + assert_eq!(error.duplicates(), &[existing]); +} + +#[test] +fn from_iter_unique_duplicate_key2_reports_error() { + let existing = TestItem::new(1, 'a', "x", "first"); + let new_item = TestItem::new(9, 'a', "zz", "dup"); + let items = [existing.clone(), new_item.clone()]; + + let error = + TriHashMap::::from_iter_unique(items) + .unwrap_err(); + assert_eq!(error.new_item(), &new_item); + assert_eq!(error.duplicates(), &[existing]); +} + +#[test] +fn from_iter_unique_duplicate_key3_reports_error() { + let existing = TestItem::new(1, 'a', "x", "first"); + let new_item = TestItem::new(9, 'z', "x", "dup"); + let items = [existing.clone(), new_item.clone()]; + + let error = + TriHashMap::::from_iter_unique(items) + .unwrap_err(); + assert_eq!(error.new_item(), &new_item); + assert_eq!(error.duplicates(), &[existing]); +} + +#[test] +fn from_iter_unique_all_keys_match_one_item_reports_single_duplicate() { + let existing = TestItem::new(1, 'a', "x", "first"); + let other = TestItem::new(2, 'b', "y", "second"); + let new_item = TestItem::new(1, 'a', "x", "dup"); + let items = [existing.clone(), other, new_item.clone()]; + + let error = + TriHashMap::::from_iter_unique(items) + .unwrap_err(); + assert_eq!(error.new_item(), &new_item); + assert_eq!(error.duplicates(), &[existing]); +} + +#[test] +fn from_iter_unique_keys_match_distinct_items_reports_all() { + let a = TestItem::new(1, 'a', "xa", "a"); + let b = TestItem::new(2, 'b', "xb", "b"); + let c = TestItem::new(3, 'c', "xc", "c"); + let new_item = TestItem::new(1, 'b', "xc", "dup"); + let items = [a.clone(), b.clone(), c.clone(), new_item.clone()]; + + let error = + TriHashMap::::from_iter_unique(items) + .unwrap_err(); + assert_eq!(error.new_item(), &new_item); + assert_eq!(error.duplicates(), &[a, b, c]); +} + #[test] #[should_panic(expected = "key1 changed during RefMut borrow")] fn get_mut_panics_if_key1_changes() {