/** @file DArenaHashMap.test.cpp * * @author Roland Conybeare, Jan 2026 **/ #include "DArenaHashMap.hpp" #include "random_hash_ops.hpp" #include #include #include #include namespace xo { using xo::map::DArenaHashMapUtil; using xo::map::DArenaHashMap; using xo::rng::random_seed; using xo::rng::xoshiro256ss; using utest::UtestTools; using utest::HashMapUtil; namespace ut { TEST_CASE("DArenaHashMap-ctor", "[arena][DArenaHashMap]") { using HashMap = DArenaHashMap; HashMap map; REQUIRE(map.empty()); REQUIRE(map.size() == 0); REQUIRE(map.groups() == 1); REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size); } TEST_CASE("DArenaHashMap-ctor2", "[arena][DArenaHashMap]") { using HashMap = DArenaHashMap; HashMap map(257); REQUIRE(map.empty()); REQUIRE(map.size() == 0); REQUIRE(map.capacity() == map.groups() * DArenaHashMapUtil::c_group_size); REQUIRE(map.capacity() == std::max(512ul, DArenaHashMapUtil::c_group_size)); } TEST_CASE("DArenaHashMap-try-insert", "[arena][DArenaHashMap]") { using HashMap = DArenaHashMap; HashMap map; REQUIRE(map.empty()); REQUIRE(map.size() == 0); REQUIRE(map.groups() == 1); REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size); { auto x = map.try_insert(std::make_pair(1, 11)); REQUIRE(x.first); REQUIRE(x.second); REQUIRE(!map.empty()); REQUIRE(map.size() == 1); REQUIRE(map.groups() == 1); REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size); REQUIRE(map.load_factor() == 1/16.0); /* verify iteration */ { size_t n = 0; for (auto & ix : map) { REQUIRE(ix.first == 1); REQUIRE(ix.second == 11); ++n; } REQUIRE(n == map.size()); } } { auto x = map.try_insert(std::make_pair(2, 9)); REQUIRE(x.first); REQUIRE(x.second); REQUIRE(!map.empty()); REQUIRE(map.size() == 2); REQUIRE(map.groups() == 1); REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size); REQUIRE(map.load_factor() == 2/16.0); /* verify iteration */ { size_t n = 0; for (auto & ix : map) { ++n; } REQUIRE(n == map.size()); } } { auto x = map.try_insert(std::make_pair(259, 12)); REQUIRE(x.first); REQUIRE(x.second); REQUIRE(!map.empty()); REQUIRE(map.size() == 3); REQUIRE(map.groups() == 1); REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size); REQUIRE(map.load_factor() == 3/16.0); /* verify iteration */ { size_t n = 0; for (auto & ix : map) { switch (ix.first) { case 1: REQUIRE(ix.second == 11); break; case 2: REQUIRE(ix.second == 9); break; case 259: REQUIRE(ix.second == 12); break; default: REQUIRE(false); } ++n; } REQUIRE(n == map.size()); } } { map.clear(); REQUIRE(map.empty()); REQUIRE(map.size() == 0); REQUIRE(map.groups() == 0); REQUIRE(map.capacity() == 0); } /* slightly different starting point, 0 capacity! */ { auto x = map.try_insert(std::make_pair(1, 11)); /* try_insert should fail - no capacity */ REQUIRE(!x.first); REQUIRE(!x.second); } { /* insert will grow hash table */ auto x = map.insert(std::make_pair(1, 11)); CHECK(x); REQUIRE(!map.empty()); REQUIRE(map.size() == 1); REQUIRE(map.groups() == 1); REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size); REQUIRE(map.load_factor() == 1/16.0); /* verify iteration */ { size_t n = 0; for (auto & ix : map) { REQUIRE(ix.first == 1); REQUIRE(ix.second == 11); ++n; } REQUIRE(n == map.size()); } } } TEST_CASE("DArenaHashMap-try-insert2", "[arena][DArenaHashMap]") { using HashMap = DArenaHashMap; std::uint64_t seed = 17747889312058974961ul; //random_seed(&seed); // to get new random seed //log && log(xtag("seed", seed)); auto rgen = xoshiro256ss(seed); /* 1. Perform series of tests with increasing scale * 2. Each test may run in two modes: * a. silent fast fail. just report success. * In this mode avoid catch2 REQUIRE * b. noisy. run with logging enabled * This mode automatically invoked when silent mode * observes test failure */ for (std::uint32_t n = 0; n <= 8; ) { HashMap hash_map; auto test_fn = [&rgen, &hash_map](bool dbg_flag, std::uint32_t n) { bool ok_flag = true; ok_flag &= HashMapUtil::random_inserts(n, dbg_flag, &rgen, &hash_map); ok_flag &= HashMapUtil::check_forward_iterator(0.0 /*dvalue*/, dbg_flag, hash_map); /* regular forward iterator, but start at hash_map.end() and use operator-- */ ok_flag &= HashMapUtil::check_backward_iterator(0.0 /*dvalue*/, dbg_flag, hash_map); ok_flag &= HashMapUtil::random_lookups(0.0 /*dvalue*/, dbg_flag, &rgen, hash_map); return ok_flag; }; bool ok_flag = UtestTools::bimodal_test("DArenaHashMap-try-insert2", test_fn, n); if (n == 0) n = 1; else n = 2*n; } } TEST_CASE("DArenaHashMap-operator-bracket", "[arena][DArenaHashMap]") { using HashMap = DArenaHashMap; HashMap map; // insert via operator[] map[1] = 100; map[2] = 200; map[3] = 300; REQUIRE(map.size() == 3); // read back via operator[] REQUIRE(map[1] == 100); REQUIRE(map[2] == 200); REQUIRE(map[3] == 300); // update via operator[] map[2] = 250; REQUIRE(map[2] == 250); REQUIRE(map.size() == 3); // size unchanged // verify via find { auto it = map.find(1); REQUIRE(it != map.end()); REQUIRE(it->second == 100); } { auto it = map.find(2); REQUIRE(it != map.end()); REQUIRE(it->second == 250); } { auto it = map.find(3); REQUIRE(it != map.end()); REQUIRE(it->second == 300); } // operator[] on non-existent key creates default entry int & val = map[999]; REQUIRE(map.size() == 4); REQUIRE(val == 0); // default-initialized val = 999; REQUIRE(map[999] == 999); } TEST_CASE("DArenaHashMap-string_view-key", "[arena][DArenaHashMap]") { using HashMap = DArenaHashMap; HashMap map(1024); map["hello"] = 42; REQUIRE(map.size() == 1); REQUIRE(map.verify_ok()); map["world"] = 100; REQUIRE(map.size() == 2); REQUIRE(map.verify_ok()); REQUIRE(map["hello"] == 42); REQUIRE(map["world"] == 100); } // TODO: // - let's try getting lcov to work in xo-umbrella2 } } /* end DArenaHashMap.test.cpp */