From f5e3a447c8d6769cb58d17343fd97d39359532a8 Mon Sep 17 00:00:00 2001 From: Romain BOULLARD Date: Sun, 9 Aug 2026 15:33:12 +0200 Subject: [PATCH] SlotMap testing --- .../Include/Utils/Containers/SlotMap.hpp | 4 +- Bigfoot/Tests/Utils/Containers/SlotMap.cpp | 888 ++++++++++++++++++ 2 files changed, 890 insertions(+), 2 deletions(-) diff --git a/Bigfoot/Sources/Utils/Include/Utils/Containers/SlotMap.hpp b/Bigfoot/Sources/Utils/Include/Utils/Containers/SlotMap.hpp index 3ce2cfe..cf7de95 100644 --- a/Bigfoot/Sources/Utils/Include/Utils/Containers/SlotMap.hpp +++ b/Bigfoot/Sources/Utils/Include/Utils/Containers/SlotMap.hpp @@ -227,7 +227,7 @@ class DenseSlotMap: public SlotMapBase } [[nodiscard]] - std::size_t GetCapacity() const + std::size_t Capacity() const { return m_data.capacity(); } @@ -753,7 +753,7 @@ class StableSlotMap: public SlotMapBase } [[nodiscard]] - std::size_t GetCapacity() const + std::size_t Capacity() const { return m_pages.size() * PAGE_SIZE; } diff --git a/Bigfoot/Tests/Utils/Containers/SlotMap.cpp b/Bigfoot/Tests/Utils/Containers/SlotMap.cpp index 82ec1ac..b7d18cb 100644 --- a/Bigfoot/Tests/Utils/Containers/SlotMap.cpp +++ b/Bigfoot/Tests/Utils/Containers/SlotMap.cpp @@ -111,6 +111,17 @@ TYPED_TEST(SlotMapFixture, Has_ReturnFalseIfSlotMapDoesNotHaveTheKey) /****************************************************************************************/ +TYPED_TEST(SlotMapFixture, Has_ReturnFalseForStaleKey) +{ + const SlotMapKey staleKey = this->m_slotMap.Insert(42); + this->m_slotMap.Remove(staleKey); + std::ignore = this->m_slotMap.Insert(69); + + EXPECT_FALSE(this->m_slotMap.Has(staleKey)); +} + +/****************************************************************************************/ + TYPED_TEST(SlotMapFixture, Size_ShouldReturnTheSizeOfTheSlotMap) { EXPECT_EQ(this->m_slotMap.Size(), 0); @@ -147,4 +158,881 @@ TYPED_TEST(SlotMapFixture, Empty_ShouldReturnFalseIfTheSlotMapIsNotEmpty) EXPECT_FALSE(this->m_slotMap.Empty()); this->m_slotMap.Remove(key2); } + +/****************************************************************************************/ + +TYPED_TEST(SlotMapFixture, Insert_ShouldReturnAValidSlotKey) +{ + EXPECT_TRUE(this->m_slotMap.Insert(42).Valid()); +} + +/****************************************************************************************/ + +TYPED_TEST(SlotMapFixture, Insert_ShouldRecycleIndicesWhenPossible) +{ + const SlotMapKey key1 = this->m_slotMap.Insert(42); + const SlotMapKey key2 = this->m_slotMap.Insert(69); + + this->m_slotMap.Remove(key1); + this->m_slotMap.Remove(key2); + + const SlotMapKey key11 = this->m_slotMap.Insert(42); + const SlotMapKey key21 = this->m_slotMap.Insert(69); + + EXPECT_EQ(key1.GetVersion() + 1, key11.GetVersion()); + EXPECT_EQ(key1.GetIndex(), key21.GetIndex()); //LIFO + + EXPECT_EQ(key2.GetVersion() + 1, key21.GetVersion()); + EXPECT_EQ(key2.GetIndex(), key11.GetIndex()); //LIFO +} + +/****************************************************************************************/ + +TYPED_TEST(SlotMapFixture, Get_ShouldReturnTheValue) +{ + const SlotMapKey key = this->m_slotMap.Insert(42); + EXPECT_EQ(*this->m_slotMap.Get(key), 42); + EXPECT_EQ(*const_cast(this->m_slotMap).Get(key), 42); +} + +/****************************************************************************************/ + +TYPED_TEST(SlotMapFixture, Get_ShouldNullptrIfTheValueIsNotInMap) +{ + const SlotMapKey key {1, 0}; + EXPECT_EQ(this->m_slotMap.Get(key), nullptr); + EXPECT_EQ(const_cast(this->m_slotMap).Get(key), nullptr); +} + +/****************************************************************************************/ + +TYPED_TEST(SlotMapFixture, Get_ShouldReturnNullptrForStaleKey) +{ + const SlotMapKey staleKey = this->m_slotMap.Insert(42); + this->m_slotMap.Remove(staleKey); + std::ignore = this->m_slotMap.Insert(69); + + EXPECT_EQ(this->m_slotMap.Get(staleKey), nullptr); + EXPECT_EQ(const_cast(this->m_slotMap).Get(staleKey), nullptr); +} + +/****************************************************************************************/ + +TYPED_TEST(SlotMapFixture, Remove_ShouldRemoveTheValue) +{ + const SlotMapKey key = this->m_slotMap.Insert(42); + EXPECT_TRUE(this->m_slotMap.Has(key)); + this->m_slotMap.Remove(key); + EXPECT_FALSE(this->m_slotMap.Has(key)); +} + +/****************************************************************************************/ + +TYPED_TEST(SlotMapFixture, Remove_ShouldBeANoOpForStaleKey) +{ + const SlotMapKey staleKey = this->m_slotMap.Insert(42); + this->m_slotMap.Remove(staleKey); + const SlotMapKey freshKey = this->m_slotMap.Insert(69); + + this->m_slotMap.Remove(staleKey); + + EXPECT_TRUE(this->m_slotMap.Has(freshKey)); + EXPECT_EQ(*this->m_slotMap.Get(freshKey), 69); + EXPECT_EQ(this->m_slotMap.Size(), 1); +} + +/****************************************************************************************/ + +TYPED_TEST(SlotMapFixture, Reset_ShouldClearTheMapAndOldKeysAreValid) +{ + const SlotMapKey key1 = this->m_slotMap.Insert(42); + const SlotMapKey key2 = this->m_slotMap.Insert(69); + + this->m_slotMap.Reset(); + + EXPECT_EQ(this->m_slotMap.Size(), 0); + EXPECT_TRUE(this->m_slotMap.Empty()); + + EXPECT_FALSE(this->m_slotMap.Has(key1)); + EXPECT_FALSE(this->m_slotMap.Has(key2)); + + EXPECT_EQ(this->m_slotMap.Insert(42), key1); + EXPECT_EQ(this->m_slotMap.Insert(69), key2); +} + +/****************************************************************************************/ + +TYPED_TEST(SlotMapFixture, Clear_ShouldClearTheMapAndOldKeysAreInvalid) +{ + const SlotMapKey key1 = this->m_slotMap.Insert(42); + const SlotMapKey key2 = this->m_slotMap.Insert(69); + + this->m_slotMap.Clear(); + + EXPECT_EQ(this->m_slotMap.Size(), 0); + EXPECT_TRUE(this->m_slotMap.Empty()); + + EXPECT_FALSE(this->m_slotMap.Has(key1)); + EXPECT_FALSE(this->m_slotMap.Has(key2)); + + EXPECT_NE(this->m_slotMap.Insert(42), key1); + EXPECT_NE(this->m_slotMap.Insert(69), key2); +} + +/****************************************************************************************/ + +TYPED_TEST(SlotMapFixture, Iterator) +{ + EXPECT_EQ(this->m_slotMap.begin(), this->m_slotMap.end()); + + std::ignore = this->m_slotMap.Insert(1); + const SlotMapKey key2 = this->m_slotMap.Insert(2); + std::ignore = this->m_slotMap.Insert(3); + + // Forward traversal follows insertion order + std::uint32_t expected = 1; + for (auto it = this->m_slotMap.begin(); it != this->m_slotMap.end(); ++it) + { + EXPECT_EQ(*it, expected); + ++expected; + } + + // Pre-increment advances and returns *this + auto preIt = this->m_slotMap.begin(); + auto& preIncrementResult = ++preIt; + EXPECT_EQ(&preIncrementResult, &preIt); + EXPECT_EQ(*preIt, 2); + + // Post-increment advances but returns the old value + auto postIt = this->m_slotMap.begin(); + const auto postIncrementResult = postIt++; + EXPECT_EQ(*postIncrementResult, 1); + EXPECT_EQ(*postIt, 2); + + // Pre-decrement retreats and returns *this + auto preDecIt = this->m_slotMap.end(); + --preDecIt; // now at 3 + auto& preDecrementResult = --preDecIt; // now at 2 + EXPECT_EQ(&preDecrementResult, &preDecIt); + EXPECT_EQ(*preDecIt, 2); + + // Post-decrement retreats but returns the old value + auto postDecIt = this->m_slotMap.end(); + --postDecIt; // now at 3 + const auto postDecrementResult = postDecIt--; + EXPECT_EQ(*postDecrementResult, 3); + EXPECT_EQ(*postDecIt, 2); + + // Removed slots are skipped during traversal + this->m_slotMap.Remove(key2); + + std::size_t count = 0; + std::uint32_t sum = 0; + for (const std::uint32_t value: this->m_slotMap) + { + ++count; + sum += value; + } + EXPECT_EQ(count, 2); + EXPECT_EQ(sum, 4); + + // Multi-page traversal: StableSlotMap's PAGE_SIZE is 64, so 100 elements force it to span pages + this->m_slotMap.Reset(); + + constexpr std::uint32_t elementCount = 100; + eastl::array keys; + for (std::uint32_t value = 0; value < elementCount; ++value) + { + keys[value] = this->m_slotMap.Insert(value); + } + + expected = 0; + for (const std::uint32_t value: this->m_slotMap) + { + EXPECT_EQ(value, expected); + ++expected; + } + EXPECT_EQ(expected, elementCount); + + // Explicitly cross the page boundary (index 63 -> 64) forward and backward + auto boundaryIt = this->m_slotMap.begin(); + for (std::uint32_t step = 0; step < 63; ++step) + { + ++boundaryIt; + } + EXPECT_EQ(*boundaryIt, 63); + ++boundaryIt; + EXPECT_EQ(*boundaryIt, 64); + --boundaryIt; + EXPECT_EQ(*boundaryIt, 63); + + // Pre/post-increment/decrement across the boundary + auto preBoundaryIt = this->m_slotMap.begin(); + auto& preBoundaryIncrementResult = ++preBoundaryIt; + EXPECT_EQ(&preBoundaryIncrementResult, &preBoundaryIt); + EXPECT_EQ(*preBoundaryIt, 1); + + auto postBoundaryIt = this->m_slotMap.begin(); + const auto postBoundaryIncrementResult = postBoundaryIt++; + EXPECT_EQ(*postBoundaryIncrementResult, 0); + EXPECT_EQ(*postBoundaryIt, 1); + + auto preBoundaryDecIt = this->m_slotMap.end(); + --preBoundaryDecIt; // now at 99 + auto& preBoundaryDecrementResult = --preBoundaryDecIt; // now at 98 + EXPECT_EQ(&preBoundaryDecrementResult, &preBoundaryDecIt); + EXPECT_EQ(*preBoundaryDecIt, 98); + + auto postBoundaryDecIt = this->m_slotMap.end(); + --postBoundaryDecIt; // now at 99 + const auto postBoundaryDecrementResult = postBoundaryDecIt--; + EXPECT_EQ(*postBoundaryDecrementResult, 99); + EXPECT_EQ(*postBoundaryDecIt, 98); + + // Remove elements straddling the boundary and check the holes are skipped + this->m_slotMap.Remove(keys[63]); + this->m_slotMap.Remove(keys[64]); + + count = 0; + for (const std::uint32_t value: this->m_slotMap) + { + EXPECT_NE(value, 63); + EXPECT_NE(value, 64); + ++count; + } + EXPECT_EQ(count, elementCount - 2); + + // A fully empty leading page must be skipped by begin()/SeekFirstFrom + this->m_slotMap.Reset(); + eastl::array leadingKeys; + for (std::uint32_t value = 0; value < elementCount; ++value) + { + leadingKeys[value] = this->m_slotMap.Insert(value); + } + for (std::uint32_t value = 0; value < 64; ++value) + { + this->m_slotMap.Remove(leadingKeys[value]); + } + + count = 0; + sum = 0; + for (const std::uint32_t value: this->m_slotMap) + { + ++count; + sum += value; + } + EXPECT_EQ(count, elementCount - 64); + EXPECT_EQ(sum, 2934u); // 64 + 65 + ... + 99 + + // A fully empty trailing page must be skipped when retreating past it + this->m_slotMap.Reset(); + eastl::array trailingKeys; + for (std::uint32_t value = 0; value < elementCount; ++value) + { + trailingKeys[value] = this->m_slotMap.Insert(value); + } + for (std::uint32_t value = 64; value < elementCount; ++value) + { + this->m_slotMap.Remove(trailingKeys[value]); + } + + count = 0; + sum = 0; + for (const std::uint32_t value: this->m_slotMap) + { + ++count; + sum += value; + } + EXPECT_EQ(count, 64u); + EXPECT_EQ(sum, 2016u); // 0 + 1 + ... + 63 +} + +/****************************************************************************************/ + +TYPED_TEST(SlotMapFixture, ConstIterator) +{ + const TypeParam& constMap = this->m_slotMap; + + // const_iterator is reachable two ways: cbegin()/cend(), or begin()/end() on a const object + EXPECT_EQ(this->m_slotMap.cbegin(), this->m_slotMap.cend()); + EXPECT_EQ(constMap.begin(), constMap.end()); + + std::ignore = this->m_slotMap.Insert(1); + const SlotMapKey key2 = this->m_slotMap.Insert(2); + std::ignore = this->m_slotMap.Insert(3); + + std::uint32_t expected = 1; + for (auto it = this->m_slotMap.cbegin(); it != this->m_slotMap.cend(); ++it) + { + EXPECT_EQ(*it, expected); + ++expected; + } + + expected = 1; + for (auto it = constMap.begin(); it != constMap.end(); ++it) + { + EXPECT_EQ(*it, expected); + ++expected; + } + + // Pre-increment advances and returns *this + auto preIt = this->m_slotMap.cbegin(); + auto& preIncrementResult = ++preIt; + EXPECT_EQ(&preIncrementResult, &preIt); + EXPECT_EQ(*preIt, 2); + + // Post-increment advances but returns the old value + auto postIt = this->m_slotMap.cbegin(); + const auto postIncrementResult = postIt++; + EXPECT_EQ(*postIncrementResult, 1); + EXPECT_EQ(*postIt, 2); + + // Pre-decrement retreats and returns *this + auto preDecIt = this->m_slotMap.cend(); + --preDecIt; // now at 3 + auto& preDecrementResult = --preDecIt; // now at 2 + EXPECT_EQ(&preDecrementResult, &preDecIt); + EXPECT_EQ(*preDecIt, 2); + + // Post-decrement retreats but returns the old value + auto postDecIt = this->m_slotMap.cend(); + --postDecIt; // now at 3 + const auto postDecrementResult = postDecIt--; + EXPECT_EQ(*postDecrementResult, 3); + EXPECT_EQ(*postDecIt, 2); + + // Removed slots are skipped identically through cbegin()/cend() and the const-object overloads + this->m_slotMap.Remove(key2); + + std::size_t count = 0; + std::uint32_t sum = 0; + for (auto it = this->m_slotMap.cbegin(); it != this->m_slotMap.cend(); ++it) + { + ++count; + sum += *it; + } + EXPECT_EQ(count, 2); + EXPECT_EQ(sum, 4); + + count = 0; + sum = 0; + for (auto it = constMap.begin(); it != constMap.end(); ++it) + { + ++count; + sum += *it; + } + EXPECT_EQ(count, 2); + EXPECT_EQ(sum, 4); + + // Multi-page traversal: StableSlotMap's PAGE_SIZE is 64, so 100 elements force it to span pages + this->m_slotMap.Reset(); + + constexpr std::uint32_t elementCount = 100; + eastl::array keys; + for (std::uint32_t value = 0; value < elementCount; ++value) + { + keys[value] = this->m_slotMap.Insert(value); + } + + expected = 0; + for (auto it = this->m_slotMap.cbegin(); it != this->m_slotMap.cend(); ++it) + { + EXPECT_EQ(*it, expected); + ++expected; + } + EXPECT_EQ(expected, elementCount); + + // Explicitly cross the page boundary (index 63 -> 64) forward and backward + auto boundaryIt = this->m_slotMap.cbegin(); + for (std::uint32_t step = 0; step < 63; ++step) + { + ++boundaryIt; + } + EXPECT_EQ(*boundaryIt, 63); + ++boundaryIt; + EXPECT_EQ(*boundaryIt, 64); + --boundaryIt; + EXPECT_EQ(*boundaryIt, 63); + + // Pre/post-increment/decrement across the boundary + auto preBoundaryIt = this->m_slotMap.cbegin(); + auto& preBoundaryIncrementResult = ++preBoundaryIt; + EXPECT_EQ(&preBoundaryIncrementResult, &preBoundaryIt); + EXPECT_EQ(*preBoundaryIt, 1); + + auto postBoundaryIt = this->m_slotMap.cbegin(); + const auto postBoundaryIncrementResult = postBoundaryIt++; + EXPECT_EQ(*postBoundaryIncrementResult, 0); + EXPECT_EQ(*postBoundaryIt, 1); + + auto preBoundaryDecIt = this->m_slotMap.cend(); + --preBoundaryDecIt; // now at 99 + auto& preBoundaryDecrementResult = --preBoundaryDecIt; // now at 98 + EXPECT_EQ(&preBoundaryDecrementResult, &preBoundaryDecIt); + EXPECT_EQ(*preBoundaryDecIt, 98); + + auto postBoundaryDecIt = this->m_slotMap.cend(); + --postBoundaryDecIt; // now at 99 + const auto postBoundaryDecrementResult = postBoundaryDecIt--; + EXPECT_EQ(*postBoundaryDecrementResult, 99); + EXPECT_EQ(*postBoundaryDecIt, 98); + + // Remove elements straddling the boundary and check the holes are skipped + this->m_slotMap.Remove(keys[63]); + this->m_slotMap.Remove(keys[64]); + + count = 0; + for (auto it = this->m_slotMap.cbegin(); it != this->m_slotMap.cend(); ++it) + { + EXPECT_NE(*it, 63); + EXPECT_NE(*it, 64); + ++count; + } + EXPECT_EQ(count, elementCount - 2); + + // A fully empty leading page must be skipped by cbegin()/SeekFirstFrom + this->m_slotMap.Reset(); + eastl::array leadingKeys; + for (std::uint32_t value = 0; value < elementCount; ++value) + { + leadingKeys[value] = this->m_slotMap.Insert(value); + } + for (std::uint32_t value = 0; value < 64; ++value) + { + this->m_slotMap.Remove(leadingKeys[value]); + } + + count = 0; + sum = 0; + for (auto it = this->m_slotMap.cbegin(); it != this->m_slotMap.cend(); ++it) + { + ++count; + sum += *it; + } + EXPECT_EQ(count, elementCount - 64); + EXPECT_EQ(sum, 2934u); // 64 + 65 + ... + 99 + + // A fully empty trailing page must be skipped when retreating past it + this->m_slotMap.Reset(); + eastl::array trailingKeys; + for (std::uint32_t value = 0; value < elementCount; ++value) + { + trailingKeys[value] = this->m_slotMap.Insert(value); + } + for (std::uint32_t value = 64; value < elementCount; ++value) + { + this->m_slotMap.Remove(trailingKeys[value]); + } + + count = 0; + sum = 0; + for (auto it = this->m_slotMap.cbegin(); it != this->m_slotMap.cend(); ++it) + { + ++count; + sum += *it; + } + EXPECT_EQ(count, 64u); + EXPECT_EQ(sum, 2016u); // 0 + 1 + ... + 63 +} + +/****************************************************************************************/ + +TYPED_TEST(SlotMapFixture, ReverseIterator) +{ + EXPECT_EQ(this->m_slotMap.rbegin(), this->m_slotMap.rend()); + + std::ignore = this->m_slotMap.Insert(1); + const SlotMapKey key2 = this->m_slotMap.Insert(2); + std::ignore = this->m_slotMap.Insert(3); + + // Reverse traversal mirrors insertion order + std::uint32_t expected = 3; + for (auto it = this->m_slotMap.rbegin(); it != this->m_slotMap.rend(); ++it) + { + EXPECT_EQ(*it, expected); + --expected; + } + + // Pre-increment advances and returns *this + auto preIt = this->m_slotMap.rbegin(); + auto& preIncrementResult = ++preIt; + EXPECT_EQ(&preIncrementResult, &preIt); + EXPECT_EQ(*preIt, 2); + + // Post-increment advances but returns the old value + auto postIt = this->m_slotMap.rbegin(); + const auto postIncrementResult = postIt++; + EXPECT_EQ(*postIncrementResult, 3); + EXPECT_EQ(*postIt, 2); + + // Pre-decrement retreats and returns *this + auto preDecIt = this->m_slotMap.rend(); + --preDecIt; // now at 1 + auto& preDecrementResult = --preDecIt; // now at 2 + EXPECT_EQ(&preDecrementResult, &preDecIt); + EXPECT_EQ(*preDecIt, 2); + + // Post-decrement retreats but returns the old value + auto postDecIt = this->m_slotMap.rend(); + --postDecIt; // now at 1 + const auto postDecrementResult = postDecIt--; + EXPECT_EQ(*postDecrementResult, 1); + EXPECT_EQ(*postDecIt, 2); + + // Removed slots are skipped during traversal + this->m_slotMap.Remove(key2); + + std::size_t count = 0; + std::uint32_t sum = 0; + for (auto it = this->m_slotMap.rbegin(); it != this->m_slotMap.rend(); ++it) + { + ++count; + sum += *it; + } + EXPECT_EQ(count, 2); + EXPECT_EQ(sum, 4); + + // Multi-page traversal: StableSlotMap's PAGE_SIZE is 64, so 100 elements force it to span pages + this->m_slotMap.Reset(); + + constexpr std::uint32_t elementCount = 100; + eastl::array keys; + for (std::uint32_t value = 0; value < elementCount; ++value) + { + keys[value] = this->m_slotMap.Insert(value); + } + + expected = elementCount - 1; + for (auto it = this->m_slotMap.rbegin(); it != this->m_slotMap.rend(); ++it) + { + EXPECT_EQ(*it, expected); + --expected; + } + + // Explicitly cross the page boundary (value 64 -> 63) forward and backward + auto boundaryIt = this->m_slotMap.rbegin(); + while (*boundaryIt != 64) + { + ++boundaryIt; + } + ++boundaryIt; + EXPECT_EQ(*boundaryIt, 63); + --boundaryIt; + EXPECT_EQ(*boundaryIt, 64); + + // Pre/post-increment/decrement across the boundary + auto preBoundaryIt = this->m_slotMap.rbegin(); + auto& preBoundaryIncrementResult = ++preBoundaryIt; + EXPECT_EQ(&preBoundaryIncrementResult, &preBoundaryIt); + EXPECT_EQ(*preBoundaryIt, 98); + + auto postBoundaryIt = this->m_slotMap.rbegin(); + const auto postBoundaryIncrementResult = postBoundaryIt++; + EXPECT_EQ(*postBoundaryIncrementResult, 99); + EXPECT_EQ(*postBoundaryIt, 98); + + auto preBoundaryDecIt = this->m_slotMap.rend(); + --preBoundaryDecIt; // now at 0 + auto& preBoundaryDecrementResult = --preBoundaryDecIt; // now at 1 + EXPECT_EQ(&preBoundaryDecrementResult, &preBoundaryDecIt); + EXPECT_EQ(*preBoundaryDecIt, 1); + + auto postBoundaryDecIt = this->m_slotMap.rend(); + --postBoundaryDecIt; // now at 0 + const auto postBoundaryDecrementResult = postBoundaryDecIt--; + EXPECT_EQ(*postBoundaryDecrementResult, 0); + EXPECT_EQ(*postBoundaryDecIt, 1); + + // Remove elements straddling the boundary and check the holes are skipped + this->m_slotMap.Remove(keys[63]); + this->m_slotMap.Remove(keys[64]); + + count = 0; + for (auto it = this->m_slotMap.rbegin(); it != this->m_slotMap.rend(); ++it) + { + EXPECT_NE(*it, 63); + EXPECT_NE(*it, 64); + ++count; + } + EXPECT_EQ(count, elementCount - 2); + + // A fully empty leading page must be skipped when retreating past it (rend() side) + this->m_slotMap.Reset(); + eastl::array leadingKeys; + for (std::uint32_t value = 0; value < elementCount; ++value) + { + leadingKeys[value] = this->m_slotMap.Insert(value); + } + for (std::uint32_t value = 0; value < 64; ++value) + { + this->m_slotMap.Remove(leadingKeys[value]); + } + + count = 0; + sum = 0; + for (auto it = this->m_slotMap.rbegin(); it != this->m_slotMap.rend(); ++it) + { + ++count; + sum += *it; + } + EXPECT_EQ(count, elementCount - 64); + EXPECT_EQ(sum, 2934u); // 64 + 65 + ... + 99 + + // A fully empty trailing page must be skipped by rbegin()/SeekLastFrom + this->m_slotMap.Reset(); + eastl::array trailingKeys; + for (std::uint32_t value = 0; value < elementCount; ++value) + { + trailingKeys[value] = this->m_slotMap.Insert(value); + } + for (std::uint32_t value = 64; value < elementCount; ++value) + { + this->m_slotMap.Remove(trailingKeys[value]); + } + + count = 0; + sum = 0; + for (auto it = this->m_slotMap.rbegin(); it != this->m_slotMap.rend(); ++it) + { + ++count; + sum += *it; + } + EXPECT_EQ(count, 64u); + EXPECT_EQ(sum, 2016u); // 0 + 1 + ... + 63 +} + +/****************************************************************************************/ + +TYPED_TEST(SlotMapFixture, ConstReverseIterator) +{ + const TypeParam& constMap = this->m_slotMap; + + // const_reverse_iterator is reachable two ways: crbegin()/crend(), or rbegin()/rend() on a + // const object + EXPECT_EQ(this->m_slotMap.crbegin(), this->m_slotMap.crend()); + EXPECT_EQ(constMap.rbegin(), constMap.rend()); + + std::ignore = this->m_slotMap.Insert(1); + const SlotMapKey key2 = this->m_slotMap.Insert(2); + std::ignore = this->m_slotMap.Insert(3); + + std::uint32_t expected = 3; + for (auto it = this->m_slotMap.crbegin(); it != this->m_slotMap.crend(); ++it) + { + EXPECT_EQ(*it, expected); + --expected; + } + + expected = 3; + for (auto it = constMap.rbegin(); it != constMap.rend(); ++it) + { + EXPECT_EQ(*it, expected); + --expected; + } + + // Pre-increment advances and returns *this + auto preIt = this->m_slotMap.crbegin(); + auto& preIncrementResult = ++preIt; + EXPECT_EQ(&preIncrementResult, &preIt); + EXPECT_EQ(*preIt, 2); + + // Post-increment advances but returns the old value + auto postIt = this->m_slotMap.crbegin(); + const auto postIncrementResult = postIt++; + EXPECT_EQ(*postIncrementResult, 3); + EXPECT_EQ(*postIt, 2); + + // Pre-decrement retreats and returns *this + auto preDecIt = this->m_slotMap.crend(); + --preDecIt; // now at 1 + auto& preDecrementResult = --preDecIt; // now at 2 + EXPECT_EQ(&preDecrementResult, &preDecIt); + EXPECT_EQ(*preDecIt, 2); + + // Post-decrement retreats but returns the old value + auto postDecIt = this->m_slotMap.crend(); + --postDecIt; // now at 1 + const auto postDecrementResult = postDecIt--; + EXPECT_EQ(*postDecrementResult, 1); + EXPECT_EQ(*postDecIt, 2); + + // Removed slots are skipped identically through crbegin()/crend() and the const-object overloads + this->m_slotMap.Remove(key2); + + std::size_t count = 0; + std::uint32_t sum = 0; + for (auto it = this->m_slotMap.crbegin(); it != this->m_slotMap.crend(); ++it) + { + ++count; + sum += *it; + } + EXPECT_EQ(count, 2); + EXPECT_EQ(sum, 4); + + count = 0; + sum = 0; + for (auto it = constMap.rbegin(); it != constMap.rend(); ++it) + { + ++count; + sum += *it; + } + EXPECT_EQ(count, 2); + EXPECT_EQ(sum, 4); + + // Multi-page traversal: StableSlotMap's PAGE_SIZE is 64, so 100 elements force it to span pages + this->m_slotMap.Reset(); + + constexpr std::uint32_t elementCount = 100; + eastl::array keys; + for (std::uint32_t value = 0; value < elementCount; ++value) + { + keys[value] = this->m_slotMap.Insert(value); + } + + expected = elementCount - 1; + for (auto it = this->m_slotMap.crbegin(); it != this->m_slotMap.crend(); ++it) + { + EXPECT_EQ(*it, expected); + --expected; + } + + // Explicitly cross the page boundary (value 64 -> 63) forward and backward + auto boundaryIt = this->m_slotMap.crbegin(); + while (*boundaryIt != 64) + { + ++boundaryIt; + } + ++boundaryIt; + EXPECT_EQ(*boundaryIt, 63); + --boundaryIt; + EXPECT_EQ(*boundaryIt, 64); + + // Pre/post-increment/decrement across the boundary + auto preBoundaryIt = this->m_slotMap.crbegin(); + auto& preBoundaryIncrementResult = ++preBoundaryIt; + EXPECT_EQ(&preBoundaryIncrementResult, &preBoundaryIt); + EXPECT_EQ(*preBoundaryIt, 98); + + auto postBoundaryIt = this->m_slotMap.crbegin(); + const auto postBoundaryIncrementResult = postBoundaryIt++; + EXPECT_EQ(*postBoundaryIncrementResult, 99); + EXPECT_EQ(*postBoundaryIt, 98); + + auto preBoundaryDecIt = this->m_slotMap.crend(); + --preBoundaryDecIt; // now at 0 + auto& preBoundaryDecrementResult = --preBoundaryDecIt; // now at 1 + EXPECT_EQ(&preBoundaryDecrementResult, &preBoundaryDecIt); + EXPECT_EQ(*preBoundaryDecIt, 1); + + auto postBoundaryDecIt = this->m_slotMap.crend(); + --postBoundaryDecIt; // now at 0 + const auto postBoundaryDecrementResult = postBoundaryDecIt--; + EXPECT_EQ(*postBoundaryDecrementResult, 0); + EXPECT_EQ(*postBoundaryDecIt, 1); + + // Remove elements straddling the boundary and check the holes are skipped + this->m_slotMap.Remove(keys[63]); + this->m_slotMap.Remove(keys[64]); + + count = 0; + for (auto it = this->m_slotMap.crbegin(); it != this->m_slotMap.crend(); ++it) + { + EXPECT_NE(*it, 63); + EXPECT_NE(*it, 64); + ++count; + } + EXPECT_EQ(count, elementCount - 2); + + // A fully empty leading page must be skipped when retreating past it (crend() side) + this->m_slotMap.Reset(); + eastl::array leadingKeys; + for (std::uint32_t value = 0; value < elementCount; ++value) + { + leadingKeys[value] = this->m_slotMap.Insert(value); + } + for (std::uint32_t value = 0; value < 64; ++value) + { + this->m_slotMap.Remove(leadingKeys[value]); + } + + count = 0; + sum = 0; + for (auto it = this->m_slotMap.crbegin(); it != this->m_slotMap.crend(); ++it) + { + ++count; + sum += *it; + } + EXPECT_EQ(count, elementCount - 64); + EXPECT_EQ(sum, 2934u); // 64 + 65 + ... + 99 + + // A fully empty trailing page must be skipped by crbegin()/SeekLastFrom + this->m_slotMap.Reset(); + eastl::array trailingKeys; + for (std::uint32_t value = 0; value < elementCount; ++value) + { + trailingKeys[value] = this->m_slotMap.Insert(value); + } + for (std::uint32_t value = 64; value < elementCount; ++value) + { + this->m_slotMap.Remove(trailingKeys[value]); + } + + count = 0; + sum = 0; + for (auto it = this->m_slotMap.crbegin(); it != this->m_slotMap.crend(); ++it) + { + ++count; + sum += *it; + } + EXPECT_EQ(count, 64u); + EXPECT_EQ(sum, 2016u); // 0 + 1 + ... + 63 +} + +/****************************************************************************************/ + +class StableSlotMapFixture: public ::testing::Test +{ + protected: + StableSlotMap m_slotMap; +}; + +/****************************************************************************************/ + +TEST_F(StableSlotMapFixture, PointersAreStable) +{ + const SlotMapKey key = m_slotMap.Insert(42); + const std::uint32_t* const pointer = m_slotMap.Get(key); + + // Inserting enough elements to span multiple pages (PAGE_SIZE is 64) must not move already + // -inserted data: unlike DenseSlotMap's contiguous eastl::vector, growth only appends new + // Page objects (each individually heap-allocated), it never reallocates existing ones. + constexpr std::uint32_t elementCount = 200; + eastl::array keys; + for (std::uint32_t value = 0; value < elementCount; ++value) + { + keys[value] = m_slotMap.Insert(value); + } + + EXPECT_EQ(m_slotMap.Get(key), pointer); + EXPECT_EQ(*pointer, 42); + + // Removing unrelated elements, including ones sharing the same page as key, must not move it + // either: Remove() only resets that slot's optional, it never compacts the page's array. + for (std::uint32_t value = 0; value < elementCount; value += 2) + { + m_slotMap.Remove(keys[value]); + } + + EXPECT_EQ(m_slotMap.Get(key), pointer); + EXPECT_EQ(*pointer, 42); + + // Re-inserting into the freed slots must not move it either + for (std::uint32_t value = 0; value < elementCount / 2; ++value) + { + std::ignore = m_slotMap.Insert(value); + } + + EXPECT_EQ(m_slotMap.Get(key), pointer); + EXPECT_EQ(*pointer, 42); +} } // namespace Bigfoot