blob: c9b63c7f7697f88ff50e44f5fce9ab5e8c9a2c67 [file]
// 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