| // Copyright 2026 The Pigweed Authors |
| // |
| // 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 |
| // |
| // https://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. |
| |
| #include "pw_containers/dynamic_hash_map.h" |
| |
| #include <algorithm> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "pw_allocator/fault_injecting_allocator.h" |
| #include "pw_allocator/testing.h" |
| #include "pw_containers/internal/test_helpers.h" |
| #include "pw_unit_test/framework.h" |
| |
| namespace { |
| |
| using pw::allocator::test::AllocatorForTest; |
| using pw::allocator::test::FaultInjectingAllocator; |
| using pw::containers::test::CopyOnly; |
| using pw::containers::test::Counter; |
| using pw::containers::test::MoveOnly; |
| |
| class DynamicHashMapTest : public ::testing::Test { |
| protected: |
| DynamicHashMapTest() : allocator_(allocator_for_test_) {} |
| |
| AllocatorForTest<2048> allocator_for_test_; |
| FaultInjectingAllocator allocator_; |
| }; |
| |
| TEST_F(DynamicHashMapTest, ConstructDestruct) { |
| pw::DynamicHashMap<int, int> map(allocator_); |
| EXPECT_TRUE(map.empty()); |
| EXPECT_EQ(map.begin(), map.end()); |
| EXPECT_EQ(map.size(), 0u); |
| } |
| |
| TEST_F(DynamicHashMapTest, SelfMoveAssign) { |
| pw::DynamicHashMap<int, int> map(allocator_); |
| map.emplace(1, 10); |
| map.emplace(2, 20); |
| |
| auto& map_ref = map; |
| map = std::move(map_ref); |
| |
| EXPECT_EQ(map.size(), 2u); |
| EXPECT_EQ(map.at(1), 10); |
| EXPECT_EQ(map.at(2), 20); |
| } |
| |
| TEST_F(DynamicHashMapTest, VerifyDestruction) { |
| Counter::Reset(); |
| { |
| pw::DynamicHashMap<int, Counter> map(allocator_); |
| map.emplace(1); |
| map.emplace(2); |
| EXPECT_EQ(Counter::created, 2); |
| } |
| EXPECT_EQ(Counter::destroyed, 2); |
| } |
| |
| TEST_F(DynamicHashMapTest, InsertAndFind) { |
| pw::DynamicHashMap<int, std::string> map(allocator_); |
| |
| auto result = map.insert({1, "one"}); |
| EXPECT_TRUE(result.second); |
| EXPECT_EQ(result.first->first, 1); |
| EXPECT_EQ(result.first->second, "one"); |
| EXPECT_EQ(map.size(), 1u); |
| |
| EXPECT_TRUE(map.contains(1)); |
| EXPECT_FALSE(map.contains(2)); |
| |
| auto it = map.find(1); |
| ASSERT_NE(it, map.end()); |
| EXPECT_EQ(it->first, 1); |
| EXPECT_EQ(it->second, "one"); |
| |
| EXPECT_EQ(map.find(2), map.end()); |
| |
| auto result2 = map.insert({1, "another one"}); |
| EXPECT_FALSE(result2.second); |
| EXPECT_EQ(result2.first->first, 1); |
| EXPECT_EQ(result2.first->second, "one"); |
| EXPECT_EQ(map.size(), 1u); |
| } |
| |
| TEST_F(DynamicHashMapTest, TryInsertAllocationFailure) { |
| pw::DynamicHashMap<int, int> map(allocator_); |
| |
| const std::pair<const int, int> item1{1, 10}; |
| auto result = map.try_insert(item1); |
| EXPECT_TRUE(result.has_value()); |
| EXPECT_TRUE(result->second); |
| EXPECT_EQ(result->first->first, 1); |
| EXPECT_EQ(result->first->second, 10); |
| |
| allocator_.DisableAll(); |
| const std::pair<const int, int> item2{2, 20}; |
| auto result2 = map.try_insert(item2); |
| EXPECT_FALSE(result2.has_value()); |
| allocator_.EnableAll(); |
| |
| EXPECT_EQ(map.size(), 1u); |
| EXPECT_TRUE(map.contains(1)); |
| EXPECT_FALSE(map.contains(2)); |
| } |
| |
| TEST_F(DynamicHashMapTest, InsertInitializerList) { |
| pw::DynamicHashMap<int, int> map(allocator_); |
| |
| map.insert({{1, 10}, {2, 20}}); |
| EXPECT_EQ(map.size(), 2u); |
| EXPECT_EQ(map.at(1), 10); |
| EXPECT_EQ(map.at(2), 20); |
| } |
| |
| TEST_F(DynamicHashMapTest, InsertMove) { |
| pw::DynamicHashMap<int, MoveOnly> map(allocator_); |
| |
| auto result = map.insert({1, MoveOnly(10)}); |
| EXPECT_TRUE(result.second); |
| EXPECT_EQ(result.first->first, 1); |
| EXPECT_EQ(result.first->second.value, 10); |
| EXPECT_EQ(map.size(), 1u); |
| } |
| |
| TEST_F(DynamicHashMapTest, InsertRange) { |
| pw::DynamicHashMap<int, int> map(allocator_); |
| std::vector<std::pair<int, int>> values = {{1, 10}, {2, 20}, {3, 30}}; |
| |
| map.insert(values.begin(), values.end()); |
| EXPECT_EQ(map.size(), 3u); |
| EXPECT_EQ(map.at(1), 10); |
| EXPECT_EQ(map.at(2), 20); |
| EXPECT_EQ(map.at(3), 30); |
| } |
| |
| TEST_F(DynamicHashMapTest, InsertWithAutoRehash) { |
| pw::DynamicHashMap<int, int> map(allocator_); |
| map.max_load_factor_percent(50u); |
| |
| for (int i = 0; i < 20; ++i) { |
| map.insert({i, i}); |
| } |
| EXPECT_EQ(map.size(), 20u); |
| |
| for (int i = 0; i < 20; ++i) { |
| EXPECT_TRUE(map.contains(i)); |
| EXPECT_EQ(map.at(i), i); |
| } |
| } |
| |
| TEST_F(DynamicHashMapTest, Emplace) { |
| pw::DynamicHashMap<int, std::string> map(allocator_); |
| |
| auto result = map.emplace(1, "one"); |
| EXPECT_TRUE(result.second); |
| EXPECT_EQ(result.first->first, 1); |
| EXPECT_EQ(result.first->second, "one"); |
| |
| auto result2 = map.emplace(1, "another one"); |
| EXPECT_FALSE(result2.second); |
| EXPECT_EQ(result2.first->second, "one"); |
| } |
| |
| TEST_F(DynamicHashMapTest, EmplacePiecewise) { |
| struct CustomType { |
| CustomType(int a, int b) : sum(a + b) {} |
| CustomType(const CustomType&) = delete; |
| int sum; |
| }; |
| pw::DynamicHashMap<int, CustomType> map(allocator_); |
| |
| EXPECT_TRUE(map.try_emplace(1, 10, 20).has_value()); |
| EXPECT_EQ(map.at(1).sum, 30); |
| } |
| |
| TEST_F(DynamicHashMapTest, TryEmplaceAllocationFailure) { |
| pw::DynamicHashMap<int, int> map(allocator_); |
| |
| auto result = map.try_emplace(1, 10); |
| EXPECT_TRUE(result.has_value()); |
| EXPECT_TRUE(result->second); |
| EXPECT_EQ(result->first->first, 1); |
| EXPECT_EQ(result->first->second, 10); |
| |
| allocator_.DisableAll(); |
| auto result2 = map.try_emplace(2, 20); |
| EXPECT_FALSE(result2.has_value()); |
| allocator_.EnableAll(); |
| |
| EXPECT_EQ(map.size(), 1u); |
| } |
| |
| TEST_F(DynamicHashMapTest, At) { |
| pw::DynamicHashMap<int, std::string> map(allocator_); |
| map.insert({1, "one"}); |
| |
| EXPECT_EQ(map.at(1), "one"); |
| map.at(1) = "new one"; |
| EXPECT_EQ(map.at(1), "new one"); |
| |
| const auto& const_map = map; |
| EXPECT_EQ(const_map.at(1), "new one"); |
| |
| EXPECT_DEATH_IF_SUPPORTED(map.at(2), ""); |
| } |
| |
| TEST_F(DynamicHashMapTest, OperatorBrackets) { |
| pw::DynamicHashMap<int, std::string> map(allocator_); |
| |
| map[1] = "one"; |
| EXPECT_EQ(map.size(), 1u); |
| EXPECT_EQ(map[1], "one"); |
| |
| map[1] = "new one"; |
| EXPECT_EQ(map.size(), 1u); |
| EXPECT_EQ(map[1], "new one"); |
| |
| EXPECT_EQ(map[2], ""); |
| EXPECT_EQ(map.size(), 2u); |
| EXPECT_TRUE(map.contains(2)); |
| } |
| |
| TEST_F(DynamicHashMapTest, OperatorBracketsAllocationFailure) { |
| pw::DynamicHashMap<int, int> map(allocator_); |
| |
| map[1] = 10; |
| EXPECT_EQ(map.size(), 1u); |
| EXPECT_EQ(map[1], 10); |
| |
| allocator_.DisableAll(); |
| EXPECT_DEATH_IF_SUPPORTED(map[2] = 20, ""); |
| allocator_.EnableAll(); |
| EXPECT_EQ(map.size(), 1u); |
| } |
| |
| TEST_F(DynamicHashMapTest, Erase) { |
| pw::DynamicHashMap<int, int> map(allocator_); |
| map.insert({1, 10}); |
| map.insert({2, 20}); |
| map.insert({3, 30}); |
| |
| EXPECT_EQ(map.size(), 3u); |
| EXPECT_EQ(map.erase(2), 1u); |
| EXPECT_EQ(map.size(), 2u); |
| EXPECT_FALSE(map.contains(2)); |
| EXPECT_EQ(map.erase(2), 0u); |
| |
| auto it = map.find(1); |
| ASSERT_NE(it, map.end()); |
| map.erase(it); |
| EXPECT_EQ(map.size(), 1u); |
| EXPECT_FALSE(map.contains(1)); |
| |
| map.erase(map.begin(), map.end()); |
| EXPECT_TRUE(map.empty()); |
| } |
| |
| TEST_F(DynamicHashMapTest, Clear) { |
| pw::DynamicHashMap<int, int> map(allocator_); |
| map.insert({1, 10}); |
| map.insert({2, 20}); |
| |
| EXPECT_FALSE(map.empty()); |
| map.clear(); |
| EXPECT_TRUE(map.empty()); |
| EXPECT_EQ(map.size(), 0u); |
| } |
| |
| TEST_F(DynamicHashMapTest, Reset) { |
| pw::DynamicHashMap<int, Counter> map(allocator_); |
| map.emplace(1); |
| map.emplace(2); |
| EXPECT_FALSE(map.empty()); |
| Counter::Reset(); |
| |
| map.reset(); |
| |
| EXPECT_TRUE(map.empty()); |
| EXPECT_EQ(map.size(), 0u); |
| EXPECT_EQ(Counter::destroyed, 2); |
| } |
| |
| TEST_F(DynamicHashMapTest, Iterators) { |
| pw::DynamicHashMap<int, int> map(allocator_); |
| map.insert({3, 30}); |
| map.insert({1, 10}); |
| map.insert({2, 20}); |
| |
| std::vector<std::pair<int, int>> expected = {{1, 10}, {2, 20}, {3, 30}}; |
| |
| // Helper to collect and sort pairs because HashMap iteration order is |
| // undefined |
| auto collect_sorted = [](auto& map_obj) { |
| std::vector<std::pair<int, int>> result; |
| for (const auto& pair : map_obj) { |
| result.emplace_back(pair); |
| } |
| std::sort(result.begin(), result.end(), [](const auto& a, const auto& b) { |
| return a.first < b.first; |
| }); |
| return result; |
| }; |
| |
| std::vector<std::pair<int, int>> actual = collect_sorted(map); |
| EXPECT_EQ(actual, expected); |
| |
| // Test const iterators |
| const auto& const_map = map; |
| std::vector<std::pair<int, int>> const_actual = collect_sorted(const_map); |
| EXPECT_EQ(const_actual, expected); |
| } |
| |
| TEST_F(DynamicHashMapTest, EqualRange) { |
| pw::DynamicHashMap<int, int> map(allocator_); |
| map.insert({1, 10}); |
| map.insert({3, 30}); |
| map.insert({5, 50}); |
| |
| // Key present |
| auto range = map.equal_range(3); |
| ASSERT_NE(range.first, map.end()); |
| EXPECT_EQ(range.first->first, 3); |
| EXPECT_EQ(range.first->second, 30); |
| EXPECT_EQ(range.second, ++(map.find(3))); |
| |
| auto range_absent = map.equal_range(4); |
| EXPECT_EQ(range_absent.first, map.end()); |
| EXPECT_EQ(range_absent.second, map.end()); |
| } |
| |
| TEST_F(DynamicHashMapTest, Swap) { |
| AllocatorForTest<1024> allocator2_for_test; |
| FaultInjectingAllocator allocator2(allocator2_for_test); |
| |
| pw::DynamicHashMap<int, int> map1(allocator_); |
| map1.insert({1, 10}); |
| |
| pw::DynamicHashMap<int, int> map2(allocator2); |
| map2.insert({2, 20}); |
| map2.insert({3, 30}); |
| |
| map1.swap(map2); |
| |
| EXPECT_EQ(map1.size(), 2u); |
| EXPECT_TRUE(map1.contains(2)); |
| EXPECT_TRUE(map1.contains(3)); |
| EXPECT_FALSE(map1.contains(1)); |
| |
| EXPECT_EQ(map2.size(), 1u); |
| EXPECT_TRUE(map2.contains(1)); |
| } |
| |
| TEST_F(DynamicHashMapTest, Merge) { |
| pw::DynamicHashMap<int, int> map1(allocator_); |
| map1.insert({1, 10}); |
| map1.insert({2, 20}); |
| |
| AllocatorForTest<1024> allocator2_for_test; |
| FaultInjectingAllocator allocator2(allocator2_for_test); |
| pw::DynamicHashMap<int, int> map2(allocator2); |
| map2.insert({2, 200}); |
| map2.insert({3, 30}); |
| |
| map1.merge(map2); |
| |
| EXPECT_EQ(map1.size(), 3u); |
| EXPECT_EQ(map1.at(1), 10); |
| EXPECT_EQ(map1.at(2), 20); |
| EXPECT_EQ(map1.at(3), 30); |
| |
| EXPECT_EQ(map2.size(), 1u); |
| EXPECT_TRUE(map2.contains(2)); |
| EXPECT_EQ(map2.at(2), 200); |
| } |
| |
| TEST_F(DynamicHashMapTest, Reserve) { |
| pw::DynamicHashMap<int, int> map(allocator_); |
| map.reserve(100); |
| map.emplace(1, 1); |
| EXPECT_LE(map.load_factor_percent(), 1u); |
| } |
| |
| TEST_F(DynamicHashMapTest, MoveConstruct) { |
| pw::DynamicHashMap<int, MoveOnly> map(allocator_); |
| map.emplace(1, MoveOnly(1)); |
| map.emplace(2, MoveOnly(2)); |
| |
| pw::DynamicHashMap<int, MoveOnly> moved_into(std::move(map)); |
| |
| EXPECT_EQ(map.size(), 0u); // NOLINT(bugprone-use-after-move) |
| ASSERT_EQ(moved_into.size(), 2u); |
| EXPECT_EQ(moved_into.at(1).value, 1); |
| EXPECT_EQ(moved_into.at(2).value, 2); |
| } |
| |
| TEST_F(DynamicHashMapTest, MoveAssign) { |
| pw::DynamicHashMap<int, MoveOnly> map(allocator_); |
| map.emplace(1, MoveOnly(1)); |
| map.emplace(2, MoveOnly(2)); |
| |
| pw::DynamicHashMap<int, MoveOnly> moved_into(allocator_); |
| moved_into.emplace(3, MoveOnly(3)); |
| |
| moved_into = std::move(map); |
| |
| EXPECT_EQ(map.size(), 0u); // NOLINT(bugprone-use-after-move) |
| ASSERT_EQ(moved_into.size(), 2u); |
| EXPECT_EQ(moved_into.at(1).value, 1); |
| EXPECT_EQ(moved_into.at(2).value, 2); |
| EXPECT_FALSE(moved_into.contains(3)); |
| } |
| |
| TEST_F(DynamicHashMapTest, MoveAssign_DestroysOldElementsAndFreesBuffer) { |
| pw::DynamicHashMap<int, Counter> map1(allocator_); |
| pw::DynamicHashMap<int, Counter> map2(allocator_); |
| |
| map1.emplace(1); |
| map1.emplace(2); |
| |
| map2.emplace(3); |
| map2.emplace(4); |
| map2.emplace(5); |
| |
| ASSERT_EQ(map1.size(), 2u); |
| ASSERT_EQ(map2.size(), 3u); |
| |
| Counter::Reset(); |
| const size_t initial_bytes = allocator_for_test_.GetAllocated(); |
| |
| map1 = std::move(map2); |
| |
| EXPECT_EQ(map1.size(), 3u); |
| EXPECT_EQ(Counter::destroyed, 2); |
| EXPECT_LT(allocator_for_test_.GetAllocated(), initial_bytes); |
| } |
| |
| TEST_F(DynamicHashMapTest, HashCollisions) { |
| struct BadHash { |
| size_t operator()(int) const { return 0; } // All keys hash to 0 |
| }; |
| |
| pw::DynamicHashMap<int, int, BadHash> map(allocator_); |
| |
| map.insert({1, 10}); |
| map.insert({2, 20}); |
| map.insert({3, 30}); |
| |
| EXPECT_EQ(map.size(), 3u); |
| EXPECT_TRUE(map.contains(1)); |
| EXPECT_TRUE(map.contains(2)); |
| EXPECT_TRUE(map.contains(3)); |
| |
| EXPECT_EQ(map.at(1), 10); |
| EXPECT_EQ(map.at(2), 20); |
| EXPECT_EQ(map.at(3), 30); |
| |
| map.erase(2); |
| EXPECT_EQ(map.size(), 2u); |
| EXPECT_FALSE(map.contains(2)); |
| EXPECT_EQ(map.at(1), 10); |
| EXPECT_EQ(map.at(3), 30); |
| } |
| |
| // Test that DynamicHashMap<K, T> is NOT copy constructible |
| static_assert(!std::is_copy_constructible_v<pw::DynamicHashMap<int, int>>); |
| |
| // Test that DynamicHashMap<K, T> is move constructible |
| static_assert(std::is_move_constructible_v<pw::DynamicHashMap<int, MoveOnly>>); |
| |
| // Test that DynamicHashMap<K, T> is NOT copy assignable |
| static_assert(!std::is_copy_assignable_v<pw::DynamicHashMap<int, CopyOnly>>); |
| |
| // Test that DynamicHashMap<K, T> is move assignable |
| static_assert(std::is_move_assignable_v<pw::DynamicHashMap<int, MoveOnly>>); |
| |
| } // namespace |