| /* |
| * Copyright (C) 2026 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include <wtf/InlineMap.h> |
| |
| #include "MoveOnly.h" |
| #include "RefLogger.h" |
| #include "Test.h" |
| #include <wtf/HashSet.h> |
| #include <wtf/PackedRefPtr.h> |
| #include <wtf/Ref.h> |
| #include <wtf/RefPtr.h> |
| #include <wtf/Vector.h> |
| #include <wtf/text/MakeString.h> |
| #include <wtf/text/StringHash.h> |
| |
| namespace TestWebKitAPI { |
| |
| TEST(WTF_InlineMap, Empty) |
| { |
| // A freshly constructed map is empty and all queries return no results. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_TRUE(map.isEmpty()); |
| EXPECT_EQ(map.size(), 0u); |
| EXPECT_FALSE(map.contains(1)); |
| EXPECT_FALSE(map.contains(2)); |
| EXPECT_TRUE(map.find(1) == map.end()); |
| EXPECT_TRUE(map.begin() == map.end()); |
| } |
| |
| TEST(WTF_InlineMap, BasicAddAndFind) |
| { |
| // Adding a single entry makes it findable; missing keys return end(). |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| auto result = map.add(1, 100); |
| EXPECT_TRUE(result.isNewEntry); |
| EXPECT_EQ(result.iterator->key, 1u); |
| EXPECT_EQ(result.iterator->value, 100u); |
| |
| EXPECT_FALSE(map.isEmpty()); |
| EXPECT_EQ(map.size(), 1u); |
| EXPECT_TRUE(map.contains(1)); |
| EXPECT_FALSE(map.contains(2)); |
| |
| auto it = map.find(1); |
| EXPECT_FALSE(it == map.end()); |
| EXPECT_EQ(it->key, 1u); |
| EXPECT_EQ(it->value, 100u); |
| |
| it = map.find(2); |
| EXPECT_TRUE(it == map.end()); |
| } |
| |
| TEST(WTF_InlineMap, DuplicateAdd) |
| { |
| // Adding a key that already exists preserves the original value. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| auto result1 = map.add(1, 100); |
| EXPECT_TRUE(result1.isNewEntry); |
| |
| auto result2 = map.add(1, 200); |
| EXPECT_FALSE(result2.isNewEntry); |
| EXPECT_EQ(result2.iterator->value, 100u); // Original value preserved |
| |
| EXPECT_EQ(map.size(), 1u); |
| } |
| |
| TEST(WTF_InlineMap, StorageModeTransitions) |
| { |
| // Map transitions from inline to hashed storage when inline capacity is exceeded. |
| // Explicitly specify InitialCapacity=3 and InitialHashedCapacity=8 |
| InlineMap<unsigned, unsigned, 3, IntHash<unsigned>> map; |
| |
| // New map starts empty |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // First add stays in linear mode |
| map.add(1, 10); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Stays linear through capacity (3 entries) |
| map.add(2, 20); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| map.add(3, 30); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Fourth entry triggers transition to hashed mode |
| map.add(4, 40); |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Stays hashed as more entries are added |
| map.add(5, 50); |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // All entries should be accessible |
| EXPECT_EQ(map.size(), 5u); |
| for (unsigned i = 1; i <= 5; ++i) { |
| auto it = map.find(i); |
| ASSERT_FALSE(it == map.end()); |
| EXPECT_EQ(it->value, i * 10); |
| } |
| } |
| |
| TEST(WTF_InlineMap, LinearMode) |
| { |
| // Entries within inline capacity are stored inline and are all findable. |
| // Explicitly specify InitialCapacity=3 to test linear storage behavior |
| InlineMap<unsigned, unsigned, 3, IntHash<unsigned>> map; |
| |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| for (unsigned i = 1; i <= 3; ++i) { |
| auto result = map.add(i, i * 10); |
| EXPECT_TRUE(result.isNewEntry); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| } |
| |
| EXPECT_EQ(map.size(), 3u); |
| |
| for (unsigned i = 1; i <= 3; ++i) { |
| EXPECT_TRUE(map.contains(i)); |
| auto it = map.find(i); |
| EXPECT_FALSE(it == map.end()); |
| EXPECT_EQ(it->value, i * 10); |
| } |
| |
| EXPECT_FALSE(map.contains(4)); |
| } |
| |
| TEST(WTF_InlineMap, GrowToHashedMode) |
| { |
| // Growing well past inline capacity preserves all entries in hashed mode. |
| // Explicitly specify InitialCapacity=3 and InitialHashedCapacity=8 |
| InlineMap<unsigned, unsigned, 3, IntHash<unsigned>> map; |
| |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Fill linear capacity (3 entries) |
| for (unsigned i = 1; i <= 3; ++i) { |
| map.add(i, i * 10); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| } |
| |
| // Adding one more should trigger transition to hashed mode |
| map.add(4, 40); |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Fill beyond initial capacity to trigger growth |
| for (unsigned i = 5; i <= 100; ++i) { |
| auto result = map.add(i, i * 10); |
| EXPECT_TRUE(result.isNewEntry); |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| } |
| |
| EXPECT_EQ(map.size(), 100u); |
| |
| // Verify all entries are still accessible |
| for (unsigned i = 1; i <= 100; ++i) { |
| EXPECT_TRUE(map.contains(i)); |
| auto it = map.find(i); |
| EXPECT_FALSE(it == map.end()); |
| EXPECT_EQ(it->value, i * 10); |
| } |
| |
| EXPECT_FALSE(map.contains(101)); |
| } |
| |
| TEST(WTF_InlineMap, Iteration) |
| { |
| // Range-based for visits every entry exactly once. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 10; ++i) |
| map.add(i, i * 10); |
| |
| HashSet<unsigned, IntHash<unsigned>, WTF::UnsignedWithZeroKeyHashTraits<unsigned>> seenKeys; |
| unsigned count = 0; |
| |
| for (auto& entry : map) { |
| seenKeys.add(entry.key); |
| EXPECT_EQ(entry.value, entry.key * 10); |
| ++count; |
| } |
| |
| EXPECT_EQ(count, 10u); |
| EXPECT_EQ(seenKeys.size(), 10u); |
| |
| for (unsigned i = 1; i <= 10; ++i) |
| EXPECT_TRUE(seenKeys.contains(i)); |
| } |
| |
| TEST(WTF_InlineMap, IterationAfterGrowth) |
| { |
| // Iteration works correctly after the map has grown to hashed mode. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 100; ++i) |
| map.add(i, i * 10); |
| |
| HashSet<unsigned, IntHash<unsigned>, WTF::UnsignedWithZeroKeyHashTraits<unsigned>> seenKeys; |
| unsigned count = 0; |
| |
| for (auto& entry : map) { |
| seenKeys.add(entry.key); |
| EXPECT_EQ(entry.value, entry.key * 10); |
| ++count; |
| } |
| |
| EXPECT_EQ(count, 100u); |
| EXPECT_EQ(seenKeys.size(), 100u); |
| } |
| |
| TEST(WTF_InlineMap, MoveConstruction) |
| { |
| // Move constructor transfers all entries and leaves the source empty. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map1; |
| |
| for (unsigned i = 1; i <= 10; ++i) |
| map1.add(i, i * 10); |
| |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map2(WTF::move(map1)); |
| |
| EXPECT_TRUE(map1.isEmpty()); |
| EXPECT_EQ(map2.size(), 10u); |
| |
| for (unsigned i = 1; i <= 10; ++i) { |
| EXPECT_TRUE(map2.contains(i)); |
| EXPECT_EQ(map2.find(i)->value, i * 10); |
| } |
| } |
| |
| TEST(WTF_InlineMap, MoveAssignment) |
| { |
| // Move assignment replaces the target's content and leaves the source empty. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map1; |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map2; |
| |
| for (unsigned i = 1; i <= 10; ++i) |
| map1.add(i, i * 10); |
| |
| map2.add(100, 1000); |
| |
| map2 = WTF::move(map1); |
| |
| EXPECT_TRUE(map1.isEmpty()); |
| EXPECT_EQ(map2.size(), 10u); |
| EXPECT_FALSE(map2.contains(100)); |
| |
| for (unsigned i = 1; i <= 10; ++i) |
| EXPECT_TRUE(map2.contains(i)); |
| } |
| |
| TEST(WTF_InlineMap, CopyConstructionEmpty) |
| { |
| // Copy-constructing from an empty map produces another empty map. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map1; |
| |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map2(map1); |
| |
| EXPECT_TRUE(map1.isEmpty()); |
| EXPECT_TRUE(map2.isEmpty()); |
| EXPECT_EQ(map1.size(), 0u); |
| EXPECT_EQ(map2.size(), 0u); |
| } |
| |
| TEST(WTF_InlineMap, CopyConstructionLinearMode) |
| { |
| // Copy-constructing a map in inline mode produces an independent copy. |
| InlineMap<unsigned, unsigned, 3, IntHash<unsigned>> map1; |
| |
| map1.add(1, 10); |
| map1.add(2, 20); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map1)); |
| |
| InlineMap<unsigned, unsigned, 3, IntHash<unsigned>> map2(map1); |
| |
| // Original should be unchanged |
| EXPECT_EQ(map1.size(), 2u); |
| EXPECT_TRUE(map1.contains(1)); |
| EXPECT_TRUE(map1.contains(2)); |
| EXPECT_EQ(map1.find(1)->value, 10u); |
| EXPECT_EQ(map1.find(2)->value, 20u); |
| |
| // Copy should have same content |
| EXPECT_EQ(map2.size(), 2u); |
| EXPECT_TRUE(map2.contains(1)); |
| EXPECT_TRUE(map2.contains(2)); |
| EXPECT_EQ(map2.find(1)->value, 10u); |
| EXPECT_EQ(map2.find(2)->value, 20u); |
| |
| // Modifying copy should not affect original |
| map2.add(3, 30); |
| EXPECT_EQ(map1.size(), 2u); |
| EXPECT_EQ(map2.size(), 3u); |
| EXPECT_FALSE(map1.contains(3)); |
| EXPECT_TRUE(map2.contains(3)); |
| } |
| |
| TEST(WTF_InlineMap, CopyConstructionHashedMode) |
| { |
| // Copy-constructing a map in hashed mode produces an independent copy. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map1; |
| |
| for (unsigned i = 1; i <= 20; ++i) |
| map1.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map1)); |
| |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map2(map1); |
| |
| // Original should be unchanged |
| EXPECT_EQ(map1.size(), 20u); |
| for (unsigned i = 1; i <= 20; ++i) { |
| EXPECT_TRUE(map1.contains(i)); |
| EXPECT_EQ(map1.find(i)->value, i * 10); |
| } |
| |
| // Copy should have same content |
| EXPECT_EQ(map2.size(), 20u); |
| for (unsigned i = 1; i <= 20; ++i) { |
| EXPECT_TRUE(map2.contains(i)); |
| EXPECT_EQ(map2.find(i)->value, i * 10); |
| } |
| |
| // Modifying copy should not affect original |
| map2.add(100, 1000); |
| EXPECT_EQ(map1.size(), 20u); |
| EXPECT_EQ(map2.size(), 21u); |
| EXPECT_FALSE(map1.contains(100)); |
| EXPECT_TRUE(map2.contains(100)); |
| } |
| |
| TEST(WTF_InlineMap, CopyAssignment) |
| { |
| // Copy assignment replaces the target's content with a copy of the source. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map1; |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map2; |
| |
| for (unsigned i = 1; i <= 10; ++i) |
| map1.add(i, i * 10); |
| |
| map2.add(100, 1000); |
| |
| map2 = map1; |
| |
| // Original should be unchanged |
| EXPECT_EQ(map1.size(), 10u); |
| for (unsigned i = 1; i <= 10; ++i) |
| EXPECT_TRUE(map1.contains(i)); |
| |
| // Assigned map should have same content, old content replaced |
| EXPECT_EQ(map2.size(), 10u); |
| EXPECT_FALSE(map2.contains(100)); |
| for (unsigned i = 1; i <= 10; ++i) { |
| EXPECT_TRUE(map2.contains(i)); |
| EXPECT_EQ(map2.find(i)->value, i * 10); |
| } |
| } |
| |
| TEST(WTF_InlineMap, CopyAssignmentToSelf) |
| { |
| // Self-assignment is a no-op and preserves the map's content. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 10; ++i) |
| map.add(i, i * 10); |
| |
| // Use a reference to avoid -Wself-assign-overloaded warning |
| auto& ref = map; |
| map = ref; |
| |
| // Map should be unchanged after self-assignment |
| EXPECT_EQ(map.size(), 10u); |
| for (unsigned i = 1; i <= 10; ++i) { |
| EXPECT_TRUE(map.contains(i)); |
| EXPECT_EQ(map.find(i)->value, i * 10); |
| } |
| } |
| |
| TEST(WTF_InlineMap, CopyConstructionWithStrings) |
| { |
| // Copy construction works correctly with String key/value types. |
| InlineMap<String, String, 5, StringHash> map1; |
| |
| map1.add("key1"_s, "value1"_s); |
| map1.add("key2"_s, "value2"_s); |
| map1.add("key3"_s, "value3"_s); |
| |
| InlineMap<String, String, 5, StringHash> map2(map1); |
| |
| EXPECT_EQ(map1.size(), 3u); |
| EXPECT_EQ(map2.size(), 3u); |
| |
| EXPECT_EQ(map1.find("key1"_s)->value, "value1"_s); |
| EXPECT_EQ(map2.find("key1"_s)->value, "value1"_s); |
| EXPECT_EQ(map1.find("key2"_s)->value, "value2"_s); |
| EXPECT_EQ(map2.find("key2"_s)->value, "value2"_s); |
| EXPECT_EQ(map1.find("key3"_s)->value, "value3"_s); |
| EXPECT_EQ(map2.find("key3"_s)->value, "value3"_s); |
| } |
| |
| TEST(WTF_InlineMap, RemoveFromEmpty) |
| { |
| // Removing from an empty map returns false. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| EXPECT_FALSE(map.remove(1)); |
| EXPECT_TRUE(map.isEmpty()); |
| } |
| |
| TEST(WTF_InlineMap, RemoveLinearMode) |
| { |
| // Removing entries in inline mode works and the map can be emptied completely. |
| InlineMap<unsigned, unsigned, 3, IntHash<unsigned>> map; |
| |
| map.add(1, 10); |
| map.add(2, 20); |
| map.add(3, 30); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_EQ(map.size(), 3u); |
| |
| // Remove middle entry |
| EXPECT_TRUE(map.remove(2)); |
| EXPECT_EQ(map.size(), 2u); |
| EXPECT_TRUE(map.contains(1)); |
| EXPECT_FALSE(map.contains(2)); |
| EXPECT_TRUE(map.contains(3)); |
| EXPECT_EQ(map.find(1)->value, 10u); |
| EXPECT_EQ(map.find(3)->value, 30u); |
| |
| // Remove first entry |
| EXPECT_TRUE(map.remove(1)); |
| EXPECT_EQ(map.size(), 1u); |
| EXPECT_FALSE(map.contains(1)); |
| EXPECT_TRUE(map.contains(3)); |
| |
| // Remove last entry |
| EXPECT_TRUE(map.remove(3)); |
| EXPECT_EQ(map.size(), 0u); |
| EXPECT_TRUE(map.isEmpty()); |
| EXPECT_FALSE(map.contains(3)); |
| |
| // Remove from empty should return false |
| EXPECT_FALSE(map.remove(1)); |
| } |
| |
| TEST(WTF_InlineMap, RemoveNonexistentLinearMode) |
| { |
| // Removing a key not present in inline mode returns false and changes nothing. |
| InlineMap<unsigned, unsigned, 3, IntHash<unsigned>> map; |
| |
| map.add(1, 10); |
| map.add(2, 20); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| EXPECT_FALSE(map.remove(3)); |
| EXPECT_EQ(map.size(), 2u); |
| EXPECT_TRUE(map.contains(1)); |
| EXPECT_TRUE(map.contains(2)); |
| } |
| |
| TEST(WTF_InlineMap, RemoveHashedMode) |
| { |
| // Removing entries in hashed mode leaves the remaining entries intact. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 20; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_EQ(map.size(), 20u); |
| |
| // Remove several entries |
| EXPECT_TRUE(map.remove(5)); |
| EXPECT_TRUE(map.remove(10)); |
| EXPECT_TRUE(map.remove(15)); |
| |
| EXPECT_EQ(map.size(), 17u); |
| EXPECT_FALSE(map.contains(5)); |
| EXPECT_FALSE(map.contains(10)); |
| EXPECT_FALSE(map.contains(15)); |
| |
| // Other entries should still be accessible |
| for (unsigned i = 1; i <= 20; ++i) { |
| if (i == 5 || i == 10 || i == 15) |
| continue; |
| EXPECT_TRUE(map.contains(i)); |
| EXPECT_EQ(map.find(i)->value, i * 10); |
| } |
| |
| // Remove nonexistent should return false |
| EXPECT_FALSE(map.remove(5)); |
| EXPECT_FALSE(map.remove(100)); |
| } |
| |
| TEST(WTF_InlineMap, RemoveAndReaddHashedMode) |
| { |
| // A removed key can be re-added with a different value. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 10; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Remove and re-add with different value |
| EXPECT_TRUE(map.remove(5)); |
| EXPECT_FALSE(map.contains(5)); |
| |
| auto result = map.add(5, 500); |
| EXPECT_TRUE(result.isNewEntry); |
| EXPECT_TRUE(map.contains(5)); |
| EXPECT_EQ(map.find(5)->value, 500u); |
| EXPECT_EQ(map.size(), 10u); |
| } |
| |
| TEST(WTF_InlineMap, RemoveAllHashedMode) |
| { |
| // Removing all entries one by one leaves the map empty. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 10; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Remove all entries |
| for (unsigned i = 1; i <= 10; ++i) |
| EXPECT_TRUE(map.remove(i)); |
| |
| EXPECT_EQ(map.size(), 0u); |
| EXPECT_TRUE(map.isEmpty()); |
| |
| // All entries should be gone |
| for (unsigned i = 1; i <= 10; ++i) |
| EXPECT_FALSE(map.contains(i)); |
| } |
| |
| TEST(WTF_InlineMap, IterationAfterRemove) |
| { |
| // Iteration skips removed entries and visits only live ones. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 10; ++i) |
| map.add(i, i * 10); |
| |
| // Remove some entries |
| map.remove(2); |
| map.remove(5); |
| map.remove(8); |
| |
| // Iteration should only visit non-removed entries |
| HashSet<unsigned, IntHash<unsigned>, WTF::UnsignedWithZeroKeyHashTraits<unsigned>> seenKeys; |
| unsigned count = 0; |
| |
| for (auto& entry : map) { |
| seenKeys.add(entry.key); |
| EXPECT_EQ(entry.value, entry.key * 10); |
| ++count; |
| } |
| |
| EXPECT_EQ(count, 7u); |
| EXPECT_EQ(seenKeys.size(), 7u); |
| EXPECT_FALSE(seenKeys.contains(2)); |
| EXPECT_FALSE(seenKeys.contains(5)); |
| EXPECT_FALSE(seenKeys.contains(8)); |
| } |
| |
| TEST(WTF_InlineMap, RemoveWithStrings) |
| { |
| // Remove works correctly with String keys. |
| InlineMap<String, unsigned, 5, StringHash> map; |
| |
| map.add("one"_s, 1); |
| map.add("two"_s, 2); |
| map.add("three"_s, 3); |
| |
| EXPECT_TRUE(map.remove("two"_s)); |
| EXPECT_EQ(map.size(), 2u); |
| EXPECT_TRUE(map.contains("one"_s)); |
| EXPECT_FALSE(map.contains("two"_s)); |
| EXPECT_TRUE(map.contains("three"_s)); |
| |
| EXPECT_FALSE(map.remove("four"_s)); |
| EXPECT_FALSE(map.remove("two"_s)); |
| } |
| |
| TEST(WTF_InlineMap, GrowAfterRemove) |
| { |
| // The hash table can grow correctly after entries have been removed. |
| InlineMap<unsigned, unsigned, 3, IntHash<unsigned>> map; |
| |
| // Fill to trigger hashed mode |
| for (unsigned i = 1; i <= 4; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Remove some entries |
| map.remove(2); |
| map.remove(3); |
| EXPECT_EQ(map.size(), 2u); |
| |
| // Add more entries to trigger growth |
| for (unsigned i = 5; i <= 20; ++i) |
| map.add(i, i * 10); |
| |
| // Verify all expected entries are present |
| EXPECT_TRUE(map.contains(1)); |
| EXPECT_FALSE(map.contains(2)); |
| EXPECT_FALSE(map.contains(3)); |
| EXPECT_TRUE(map.contains(4)); |
| for (unsigned i = 5; i <= 20; ++i) |
| EXPECT_TRUE(map.contains(i)); |
| } |
| |
| TEST(WTF_InlineMap, PointerKeys) |
| { |
| // Raw pointers work as map keys. |
| InlineMap<int*, int, 5> map; |
| |
| constexpr unsigned arraySize = 50; |
| int array[arraySize]; |
| |
| for (unsigned i = 0; i < arraySize; ++i) { |
| array[i] = i; |
| int* ptr = &array[i]; |
| EXPECT_FALSE(map.contains(ptr)); |
| auto result = map.add(ptr, i * 10); |
| EXPECT_TRUE(result.isNewEntry); |
| EXPECT_TRUE(map.contains(ptr)); |
| } |
| |
| EXPECT_EQ(map.size(), arraySize); |
| |
| for (unsigned i = 0; i < arraySize; ++i) { |
| int* ptr = &array[i]; |
| auto it = map.find(ptr); |
| EXPECT_FALSE(it == map.end()); |
| EXPECT_EQ(it->value, static_cast<int>(i * 10)); |
| } |
| } |
| |
| TEST(WTF_InlineMap, ConstIteration) |
| { |
| // Iteration works through a const reference to the map. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 10; ++i) |
| map.add(i, i * 10); |
| |
| const auto& constMap = map; |
| unsigned count = 0; |
| |
| for (const auto& entry : constMap) { |
| EXPECT_EQ(entry.value, entry.key * 10); |
| ++count; |
| } |
| |
| EXPECT_EQ(count, 10u); |
| } |
| |
| TEST(WTF_InlineMap, ConstFind) |
| { |
| // find() works through a const reference to the map. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| map.add(1, 100); |
| |
| const auto& constMap = map; |
| |
| auto it = constMap.find(1); |
| EXPECT_FALSE(it == constMap.end()); |
| EXPECT_EQ(it->value, 100u); |
| |
| it = constMap.find(2); |
| EXPECT_TRUE(it == constMap.end()); |
| } |
| |
| TEST(WTF_InlineMap, MoveOnlyValues) |
| { |
| // Move-only types work as map values. |
| InlineMap<unsigned, MoveOnly, 5, IntHash<unsigned>> map; |
| |
| for (size_t i = 0; i < 100; ++i) { |
| MoveOnly moveOnly(i + 1); |
| auto result = map.add(static_cast<unsigned>(i + 1), WTF::move(moveOnly)); |
| EXPECT_TRUE(result.isNewEntry); |
| } |
| |
| EXPECT_EQ(map.size(), 100u); |
| |
| for (size_t i = 0; i < 100; ++i) { |
| auto it = map.find(static_cast<unsigned>(i + 1)); |
| ASSERT_FALSE(it == map.end()); |
| EXPECT_EQ(it->value.value(), i + 1); |
| } |
| } |
| |
| TEST(WTF_InlineMap, MoveOnlyKeys) |
| { |
| // Move-only types work as map keys, including duplicate detection. |
| InlineMap<MoveOnly, unsigned, 5, DefaultHash<MoveOnly>> map; |
| |
| for (size_t i = 0; i < 100; ++i) { |
| MoveOnly moveOnly(i + 1); |
| auto result = map.add(WTF::move(moveOnly), static_cast<unsigned>(i + 1)); |
| EXPECT_TRUE(result.isNewEntry); |
| } |
| |
| EXPECT_EQ(map.size(), 100u); |
| |
| for (size_t i = 0; i < 100; ++i) { |
| auto it = map.find(MoveOnly(i + 1)); |
| ASSERT_FALSE(it == map.end()); |
| EXPECT_EQ(it->value, static_cast<unsigned>(i + 1)); |
| } |
| |
| // Verify duplicate add doesn't insert |
| for (size_t i = 0; i < 100; ++i) |
| EXPECT_FALSE(map.add(MoveOnly(i + 1), static_cast<unsigned>(i + 1)).isNewEntry); |
| } |
| |
| namespace { |
| |
| template<typename T> struct ZeroHash : public IntHash<T> { |
| static unsigned hash(const T&) { return 0; } |
| }; |
| |
| } // anonymous namespace |
| |
| TEST(WTF_InlineMap, HashCollisions) |
| { |
| // All entries remain accessible even when every key hashes to the same bucket. |
| // Use a hash that always returns 0 to force all entries into the same bucket |
| InlineMap<unsigned, unsigned, 5, ZeroHash<unsigned>> map; |
| |
| // Add enough entries to trigger hashed mode |
| for (unsigned i = 1; i <= 20; ++i) { |
| auto result = map.add(i, i * 10); |
| EXPECT_TRUE(result.isNewEntry); |
| } |
| |
| EXPECT_EQ(map.size(), 20u); |
| |
| // Verify all entries are still accessible despite hash collisions |
| for (unsigned i = 1; i <= 20; ++i) { |
| EXPECT_TRUE(map.contains(i)); |
| auto it = map.find(i); |
| ASSERT_FALSE(it == map.end()); |
| EXPECT_EQ(it->value, i * 10); |
| } |
| |
| // Verify non-existent key lookup |
| EXPECT_FALSE(map.contains(100)); |
| } |
| |
| TEST(WTF_InlineMap, IteratorComparison) |
| { |
| // Iterator == and \!= operators work correctly, including const conversions. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| map.add(1, 100); |
| |
| ASSERT_TRUE(map.begin() != map.end()); |
| ASSERT_FALSE(map.begin() == map.end()); |
| |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>>::const_iterator begin = map.begin(); |
| ASSERT_TRUE(begin == map.begin()); |
| ASSERT_TRUE(map.begin() == begin); |
| ASSERT_TRUE(begin != map.end()); |
| ASSERT_TRUE(map.end() != begin); |
| ASSERT_FALSE(begin != map.begin()); |
| ASSERT_FALSE(map.begin() != begin); |
| ASSERT_FALSE(begin == map.end()); |
| ASSERT_FALSE(map.end() == begin); |
| } |
| |
| namespace { |
| |
| class DestructorCounter { |
| public: |
| static unsigned destructorCount; |
| |
| struct TestingScope { |
| TestingScope() { destructorCount = 0; } |
| }; |
| |
| DestructorCounter() = default; |
| DestructorCounter(unsigned value) |
| : m_value(value) |
| { } |
| |
| DestructorCounter(DestructorCounter&& other) |
| : m_value(other.m_value) |
| { |
| other.m_value = 0; |
| } |
| |
| DestructorCounter& operator=(DestructorCounter&& other) |
| { |
| m_value = other.m_value; |
| other.m_value = 0; |
| return *this; |
| } |
| |
| ~DestructorCounter() |
| { |
| if (m_value != emptyValue()) |
| ++destructorCount; |
| } |
| |
| unsigned value() const { return m_value; } |
| |
| static constexpr unsigned emptyValue() { return std::numeric_limits<unsigned>::max(); } |
| |
| private: |
| unsigned m_value { emptyValue() }; |
| }; |
| |
| unsigned DestructorCounter::destructorCount = 0; |
| |
| } // anonymous namespace |
| |
| TEST(WTF_InlineMap, DestructorCalledOnClear) |
| { |
| // Value destructors are called when the map is destroyed (inline mode). |
| DestructorCounter::TestingScope scope; |
| |
| { |
| InlineMap<unsigned, DestructorCounter, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 3; ++i) |
| map.add(i, DestructorCounter(i)); |
| |
| EXPECT_EQ(map.size(), 3u); |
| EXPECT_EQ(DestructorCounter::destructorCount, 3u); // Moved-from temporaries |
| } |
| |
| // Destructor should be called for all 3 entries when map is destroyed |
| EXPECT_EQ(DestructorCounter::destructorCount, 6u); |
| } |
| |
| TEST(WTF_InlineMap, DestructorCalledOnClearAfterGrowth) |
| { |
| // Value destructors are called for all live entries after growth (hashed mode). |
| DestructorCounter::TestingScope scope; |
| unsigned countBeforeDestruction = 0; |
| |
| { |
| InlineMap<unsigned, DestructorCounter, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 100; ++i) |
| map.add(i, DestructorCounter(i)); |
| |
| EXPECT_EQ(map.size(), 100u); |
| |
| // Record the count before map destruction. This includes temporaries from add() |
| // calls as well as internal temporaries created during hash table growth. |
| countBeforeDestruction = DestructorCounter::destructorCount; |
| } |
| |
| // Map destruction should destroy exactly 100 live entries |
| EXPECT_EQ(DestructorCounter::destructorCount, countBeforeDestruction + 100u); |
| } |
| |
| TEST(WTF_InlineMap, StringKeys) |
| { |
| // String objects work as map keys. |
| InlineMap<String, unsigned, 5, StringHash> map; |
| |
| map.add("one"_s, 1); |
| map.add("two"_s, 2); |
| map.add("three"_s, 3); |
| |
| EXPECT_EQ(map.size(), 3u); |
| EXPECT_TRUE(map.contains("one"_s)); |
| EXPECT_TRUE(map.contains("two"_s)); |
| EXPECT_TRUE(map.contains("three"_s)); |
| EXPECT_FALSE(map.contains("four"_s)); |
| |
| EXPECT_EQ(map.find("one"_s)->value, 1u); |
| EXPECT_EQ(map.find("two"_s)->value, 2u); |
| EXPECT_EQ(map.find("three"_s)->value, 3u); |
| } |
| |
| TEST(WTF_InlineMap, StringValues) |
| { |
| // String objects work as map values. |
| InlineMap<unsigned, String, 5, IntHash<unsigned>> map; |
| |
| map.add(1, "one"_s); |
| map.add(2, "two"_s); |
| map.add(3, "three"_s); |
| |
| EXPECT_EQ(map.size(), 3u); |
| EXPECT_EQ(map.find(1)->value, "one"_s); |
| EXPECT_EQ(map.find(2)->value, "two"_s); |
| EXPECT_EQ(map.find(3)->value, "three"_s); |
| } |
| |
| TEST(WTF_InlineMap, StringKeysAndValues) |
| { |
| // String-to-String map works, and duplicate add preserves the original value. |
| InlineMap<String, String, 5, StringHash> map; |
| |
| map.add("key1"_s, "value1"_s); |
| map.add("key2"_s, "value2"_s); |
| map.add("key3"_s, "value3"_s); |
| |
| EXPECT_EQ(map.size(), 3u); |
| EXPECT_EQ(map.find("key1"_s)->value, "value1"_s); |
| EXPECT_EQ(map.find("key2"_s)->value, "value2"_s); |
| EXPECT_EQ(map.find("key3"_s)->value, "value3"_s); |
| |
| // Duplicate add should not overwrite |
| auto result = map.add("key1"_s, "newvalue"_s); |
| EXPECT_FALSE(result.isNewEntry); |
| EXPECT_EQ(map.find("key1"_s)->value, "value1"_s); |
| } |
| |
| TEST(WTF_InlineMap, StringKeysGrowth) |
| { |
| // String keys survive growth from inline to hashed mode. |
| InlineMap<String, unsigned, 5, StringHash> map; |
| |
| // Add enough entries to trigger growth to hashed mode |
| for (unsigned i = 1; i <= 100; ++i) |
| map.add(makeString("key"_s, i), i); |
| |
| EXPECT_EQ(map.size(), 100u); |
| |
| // Verify all entries are still accessible |
| for (unsigned i = 1; i <= 100; ++i) { |
| auto key = makeString("key"_s, i); |
| EXPECT_TRUE(map.contains(key)); |
| auto it = map.find(key); |
| ASSERT_FALSE(it == map.end()); |
| EXPECT_EQ(it->value, i); |
| } |
| |
| EXPECT_FALSE(map.contains("key101"_s)); |
| } |
| |
| TEST(WTF_InlineMap, RefPtrKeys) |
| { |
| // RefPtr objects work as map keys, looked up by raw pointer. |
| DerivedRefLogger a("a"); |
| DerivedRefLogger b("b"); |
| DerivedRefLogger c("c"); |
| |
| InlineMap<RefPtr<RefLogger>, int, 5> map; |
| |
| map.add(RefPtr<RefLogger>(&a), 1); |
| map.add(RefPtr<RefLogger>(&b), 2); |
| map.add(RefPtr<RefLogger>(&c), 3); |
| |
| EXPECT_EQ(map.size(), 3u); |
| EXPECT_TRUE(map.contains(&a)); |
| EXPECT_TRUE(map.contains(&b)); |
| EXPECT_TRUE(map.contains(&c)); |
| |
| EXPECT_EQ(map.find(&a)->value, 1); |
| EXPECT_EQ(map.find(&b)->value, 2); |
| EXPECT_EQ(map.find(&c)->value, 3); |
| } |
| |
| TEST(WTF_InlineMap, RefPtrValues) |
| { |
| // RefPtr objects work as map values. |
| DerivedRefLogger a("a"); |
| DerivedRefLogger b("b"); |
| |
| InlineMap<unsigned, RefPtr<RefLogger>, 5, IntHash<unsigned>> map; |
| |
| map.add(1, RefPtr<RefLogger>(&a)); |
| map.add(2, RefPtr<RefLogger>(&b)); |
| |
| EXPECT_EQ(map.size(), 2u); |
| EXPECT_EQ(map.find(1)->value.get(), &a); |
| EXPECT_EQ(map.find(2)->value.get(), &b); |
| } |
| |
| TEST(WTF_InlineMap, RefKeys) |
| { |
| // Ref objects work as map keys and are properly deref'd on destruction. |
| RefLogger a("a"); |
| |
| { |
| InlineMap<Ref<RefLogger>, int, 5> map; |
| |
| Ref<RefLogger> ref(a); |
| map.add(WTF::move(ref), 1); |
| |
| EXPECT_EQ(map.size(), 1u); |
| |
| // Verify through iteration since we don't have translator support |
| bool found = false; |
| for (auto& entry : map) { |
| if (entry.key.ptr() == &a) { |
| EXPECT_EQ(entry.value, 1); |
| found = true; |
| } |
| } |
| EXPECT_TRUE(found); |
| } |
| |
| EXPECT_STREQ("ref(a) deref(a) ", takeLogStr().c_str()); |
| } |
| |
| TEST(WTF_InlineMap, RefValues) |
| { |
| // Ref objects work as map values and are properly deref'd on destruction. |
| RefLogger a("a"); |
| |
| { |
| InlineMap<unsigned, Ref<RefLogger>, 5, IntHash<unsigned>> map; |
| |
| Ref<RefLogger> ref(a); |
| map.add(1, WTF::move(ref)); |
| |
| EXPECT_EQ(map.size(), 1u); |
| EXPECT_EQ(map.find(1)->value.ptr(), &a); |
| } |
| |
| EXPECT_STREQ("ref(a) deref(a) ", takeLogStr().c_str()); |
| } |
| |
| TEST(WTF_InlineMap, RefKeysGrowth) |
| { |
| // Ref keys work correctly through growth transitions. |
| // Test that Ref keys work correctly through growth transitions |
| Vector<Ref<RefLogger>> loggers; |
| for (int i = 0; i < 50; ++i) |
| loggers.append(adoptRef(*new RefLogger("a"))); |
| |
| { |
| InlineMap<Ref<RefLogger>, int, 5> map; |
| |
| for (int i = 0; i < 50; ++i) { |
| Ref<RefLogger> ref = loggers[i].copyRef(); |
| map.add(WTF::move(ref), i + 1); |
| } |
| |
| EXPECT_EQ(map.size(), 50u); |
| |
| // Verify all entries through iteration |
| unsigned count = 0; |
| for (auto& entry : map) { |
| ++count; |
| // Just verify values are in expected range |
| EXPECT_GE(entry.value, 1); |
| EXPECT_LE(entry.value, 50); |
| } |
| EXPECT_EQ(count, 50u); |
| } |
| } |
| |
| TEST(WTF_InlineMap, ClearEmpty) |
| { |
| // Clearing an already-empty map is a no-op. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| map.clear(); |
| |
| EXPECT_TRUE(map.isEmpty()); |
| EXPECT_EQ(map.size(), 0u); |
| } |
| |
| TEST(WTF_InlineMap, ClearLinearMode) |
| { |
| // Clearing in inline mode removes entries but preserves inline storage. |
| InlineMap<unsigned, unsigned, 3, IntHash<unsigned>> map; |
| |
| map.add(1, 10); |
| map.add(2, 20); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_EQ(map.size(), 2u); |
| |
| map.clear(); |
| |
| // Storage is preserved, just cleared |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_TRUE(map.isEmpty()); |
| EXPECT_EQ(map.size(), 0u); |
| |
| // Should be able to add entries again after clear |
| map.add(3, 30); |
| EXPECT_EQ(map.size(), 1u); |
| EXPECT_TRUE(map.contains(3)); |
| EXPECT_FALSE(map.contains(1)); |
| EXPECT_FALSE(map.contains(2)); |
| } |
| |
| TEST(WTF_InlineMap, ClearHashedMode) |
| { |
| // Clearing in hashed mode frees heap storage and transitions back to inline. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 20; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_EQ(map.size(), 20u); |
| |
| map.clear(); |
| |
| // Should transition back to inline |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_TRUE(map.isEmpty()); |
| EXPECT_EQ(map.size(), 0u); |
| |
| // Should be able to add entries again after clear |
| for (unsigned i = 100; i <= 104; ++i) |
| map.add(i, i); |
| |
| EXPECT_EQ(map.size(), 5u); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| for (unsigned i = 100; i <= 104; ++i) |
| EXPECT_TRUE(map.contains(i)); |
| EXPECT_FALSE(map.contains(1)); |
| |
| // Adding one more should transition to hashed |
| map.add(105, 105); |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| } |
| |
| TEST(WTF_InlineMap, ClearWithStrings) |
| { |
| // Clearing works correctly with String key/value types. |
| InlineMap<String, String, 5, StringHash> map; |
| |
| map.add("key1"_s, "value1"_s); |
| map.add("key2"_s, "value2"_s); |
| map.add("key3"_s, "value3"_s); |
| |
| EXPECT_EQ(map.size(), 3u); |
| |
| map.clear(); |
| |
| EXPECT_TRUE(map.isEmpty()); |
| EXPECT_FALSE(map.contains("key1"_s)); |
| } |
| |
| TEST(WTF_InlineMap, ReserveInitialCapacityZero) |
| { |
| // Reserving zero capacity keeps the map in inline mode. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| map.reserveInitialCapacity(0); |
| |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_TRUE(map.isEmpty()); |
| } |
| |
| TEST(WTF_InlineMap, ReserveInitialCapacityLinear) |
| { |
| // Reserving within inline capacity keeps inline storage. |
| InlineMap<unsigned, unsigned, 3, IntHash<unsigned>> map; |
| |
| map.reserveInitialCapacity(2); |
| |
| // Should allocate linear storage |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_TRUE(map.isEmpty()); |
| |
| // Should be able to add entries |
| map.add(1, 10); |
| map.add(2, 20); |
| EXPECT_EQ(map.size(), 2u); |
| EXPECT_TRUE(map.contains(1)); |
| EXPECT_TRUE(map.contains(2)); |
| } |
| |
| TEST(WTF_InlineMap, ReserveInitialCapacityHashed) |
| { |
| // Reserving beyond inline capacity switches directly to hashed storage. |
| InlineMap<unsigned, unsigned, 3, IntHash<unsigned>> map; |
| |
| map.reserveInitialCapacity(10); |
| |
| // Should allocate hashed storage directly |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_TRUE(map.isEmpty()); |
| |
| // Should be able to add entries without triggering growth |
| for (unsigned i = 1; i <= 10; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_EQ(map.size(), 10u); |
| for (unsigned i = 1; i <= 10; ++i) { |
| EXPECT_TRUE(map.contains(i)); |
| EXPECT_EQ(map.find(i)->value, i * 10); |
| } |
| } |
| |
| TEST(WTF_InlineMap, ReserveInitialCapacityLarge) |
| { |
| // Reserving a large capacity pre-allocates hashed storage for many entries. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| map.reserveInitialCapacity(100); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_TRUE(map.isEmpty()); |
| |
| // Add all 100 entries |
| for (unsigned i = 1; i <= 100; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_EQ(map.size(), 100u); |
| for (unsigned i = 1; i <= 100; ++i) |
| EXPECT_TRUE(map.contains(i)); |
| } |
| |
| TEST(WTF_InlineMap, ValuesIteration) |
| { |
| // The values() range visits every value exactly once. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 10; ++i) |
| map.add(i, i * 10); |
| |
| unsigned sum = 0; |
| unsigned count = 0; |
| for (auto& value : map.values()) { |
| sum += value; |
| ++count; |
| } |
| |
| EXPECT_EQ(count, 10u); |
| // Sum of 10 + 20 + ... + 100 = 550 |
| EXPECT_EQ(sum, 550u); |
| } |
| |
| TEST(WTF_InlineMap, ValuesIterationModify) |
| { |
| // Values can be modified in-place through the values() iterator. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 5; ++i) |
| map.add(i, i); |
| |
| // Modify values through the values iterator |
| for (auto& value : map.values()) |
| value *= 10; |
| |
| // Verify modifications |
| for (unsigned i = 1; i <= 5; ++i) |
| EXPECT_EQ(map.find(i)->value, i * 10); |
| } |
| |
| TEST(WTF_InlineMap, ValuesIterationEmpty) |
| { |
| // The values() range on an empty map produces zero iterations. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| unsigned count = 0; |
| for (auto& value : map.values()) { |
| UNUSED_PARAM(value); |
| ++count; |
| } |
| |
| EXPECT_EQ(count, 0u); |
| } |
| |
| TEST(WTF_InlineMap, ValuesIterationConst) |
| { |
| // The values() range works through a const reference. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 5; ++i) |
| map.add(i, i * 10); |
| |
| const auto& constMap = map; |
| unsigned sum = 0; |
| for (const auto& value : constMap.values()) |
| sum += value; |
| |
| EXPECT_EQ(sum, 150u); // 10 + 20 + 30 + 40 + 50 |
| } |
| |
| TEST(WTF_InlineMap, SwapBothEmpty) |
| { |
| // Swapping two empty maps is a no-op. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map1; |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map2; |
| |
| map1.swap(map2); |
| |
| EXPECT_TRUE(map1.isEmpty()); |
| EXPECT_TRUE(map2.isEmpty()); |
| } |
| |
| TEST(WTF_InlineMap, SwapOneEmpty) |
| { |
| // Swapping a populated map with an empty one exchanges their contents. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map1; |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map2; |
| |
| for (unsigned i = 1; i <= 5; ++i) |
| map1.add(i, i * 10); |
| |
| EXPECT_EQ(map1.size(), 5u); |
| EXPECT_TRUE(map2.isEmpty()); |
| |
| map1.swap(map2); |
| |
| EXPECT_TRUE(map1.isEmpty()); |
| EXPECT_EQ(map2.size(), 5u); |
| for (unsigned i = 1; i <= 5; ++i) { |
| EXPECT_TRUE(map2.contains(i)); |
| EXPECT_EQ(map2.find(i)->value, i * 10); |
| } |
| } |
| |
| TEST(WTF_InlineMap, SwapBothPopulated) |
| { |
| // Swapping two populated maps exchanges their contents. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map1; |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map2; |
| |
| for (unsigned i = 1; i <= 5; ++i) |
| map1.add(i, i * 10); |
| |
| for (unsigned i = 100; i <= 103; ++i) |
| map2.add(i, i); |
| |
| EXPECT_EQ(map1.size(), 5u); |
| EXPECT_EQ(map2.size(), 4u); |
| |
| map1.swap(map2); |
| |
| EXPECT_EQ(map1.size(), 4u); |
| EXPECT_EQ(map2.size(), 5u); |
| |
| // Verify map1 now has map2's original content |
| for (unsigned i = 100; i <= 103; ++i) { |
| EXPECT_TRUE(map1.contains(i)); |
| EXPECT_EQ(map1.find(i)->value, i); |
| } |
| EXPECT_FALSE(map1.contains(1)); |
| |
| // Verify map2 now has map1's original content |
| for (unsigned i = 1; i <= 5; ++i) { |
| EXPECT_TRUE(map2.contains(i)); |
| EXPECT_EQ(map2.find(i)->value, i * 10); |
| } |
| EXPECT_FALSE(map2.contains(100)); |
| } |
| |
| TEST(WTF_InlineMap, SwapWithStrings) |
| { |
| // Swap works correctly with String key/value types. |
| InlineMap<String, String, 5, StringHash> map1; |
| InlineMap<String, String, 5, StringHash> map2; |
| |
| map1.add("key1"_s, "value1"_s); |
| map1.add("key2"_s, "value2"_s); |
| |
| map2.add("other"_s, "data"_s); |
| |
| map1.swap(map2); |
| |
| EXPECT_EQ(map1.size(), 1u); |
| EXPECT_EQ(map2.size(), 2u); |
| |
| EXPECT_TRUE(map1.contains("other"_s)); |
| EXPECT_EQ(map1.find("other"_s)->value, "data"_s); |
| |
| EXPECT_TRUE(map2.contains("key1"_s)); |
| EXPECT_TRUE(map2.contains("key2"_s)); |
| } |
| |
| // --- PackedRefPtr key tests (matching production use in VariableEnvironment) --- |
| |
| TEST(WTF_InlineMap, PackedRefPtrKeysBasic) |
| { |
| // PackedRefPtr<StringImpl> keys work for basic add/find/contains. |
| Vector<String> strings; |
| strings.append("alpha"_s); |
| strings.append("beta"_s); |
| strings.append("gamma"_s); |
| |
| InlineMap<PackedRefPtr<StringImpl>, unsigned, 5> map; |
| |
| for (unsigned i = 0; i < strings.size(); ++i) { |
| auto result = map.add(strings[i].impl(), i + 1); |
| EXPECT_TRUE(result.isNewEntry); |
| } |
| |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_EQ(map.size(), 3u); |
| |
| for (unsigned i = 0; i < strings.size(); ++i) { |
| EXPECT_TRUE(map.contains(strings[i].impl())); |
| auto it = map.find(strings[i].impl()); |
| ASSERT_FALSE(it == map.end()); |
| EXPECT_EQ(it->value, i + 1); |
| } |
| |
| EXPECT_FALSE(map.contains(String("delta"_s).impl())); |
| } |
| |
| TEST(WTF_InlineMap, PackedRefPtrKeysGrowth) |
| { |
| // PackedRefPtr keys survive growth from inline to hashed mode. |
| constexpr unsigned count = 50; |
| Vector<String> strings; |
| for (unsigned i = 0; i < count; ++i) |
| strings.append(makeString("key_"_s, i)); |
| |
| InlineMap<PackedRefPtr<StringImpl>, unsigned, 5> map; |
| |
| for (unsigned i = 0; i < count; ++i) { |
| auto result = map.add(strings[i].impl(), i * 10); |
| EXPECT_TRUE(result.isNewEntry); |
| } |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_EQ(map.size(), count); |
| |
| for (unsigned i = 0; i < count; ++i) { |
| EXPECT_TRUE(map.contains(strings[i].impl())); |
| auto it = map.find(strings[i].impl()); |
| ASSERT_FALSE(it == map.end()); |
| EXPECT_EQ(it->value, i * 10); |
| } |
| |
| EXPECT_FALSE(map.contains(String("nonexistent"_s).impl())); |
| } |
| |
| TEST(WTF_InlineMap, PackedRefPtrKeysRemoveAndReinsert) |
| { |
| // PackedRefPtr keys can be removed and re-inserted with new values. |
| constexpr unsigned count = 20; |
| Vector<String> strings; |
| for (unsigned i = 0; i < count; ++i) |
| strings.append(makeString("var_"_s, i)); |
| |
| InlineMap<PackedRefPtr<StringImpl>, unsigned, 5> map; |
| |
| for (unsigned i = 0; i < count; ++i) |
| map.add(strings[i].impl(), i); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Remove even-indexed entries |
| for (unsigned i = 0; i < count; i += 2) |
| EXPECT_TRUE(map.remove(strings[i].impl())); |
| |
| EXPECT_EQ(map.size(), count / 2); |
| |
| // Verify odd-indexed entries are still present |
| for (unsigned i = 1; i < count; i += 2) { |
| EXPECT_TRUE(map.contains(strings[i].impl())); |
| EXPECT_EQ(map.find(strings[i].impl())->value, i); |
| } |
| |
| // Re-add even-indexed entries with new values |
| for (unsigned i = 0; i < count; i += 2) { |
| auto result = map.add(strings[i].impl(), i + 1000); |
| EXPECT_TRUE(result.isNewEntry); |
| } |
| |
| EXPECT_EQ(map.size(), count); |
| |
| // Verify all entries |
| for (unsigned i = 0; i < count; ++i) { |
| EXPECT_TRUE(map.contains(strings[i].impl())); |
| unsigned expectedValue = (i % 2) ? i : i + 1000; |
| EXPECT_EQ(map.find(strings[i].impl())->value, expectedValue); |
| } |
| } |
| |
| TEST(WTF_InlineMap, PackedRefPtrKeysCopy) |
| { |
| // Copying a map with PackedRefPtr keys produces an independent copy. |
| constexpr unsigned count = 20; |
| Vector<String> strings; |
| for (unsigned i = 0; i < count; ++i) |
| strings.append(makeString("name_"_s, i)); |
| |
| InlineMap<PackedRefPtr<StringImpl>, unsigned, 5> map1; |
| |
| for (unsigned i = 0; i < count; ++i) |
| map1.add(strings[i].impl(), i); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map1)); |
| |
| InlineMap<PackedRefPtr<StringImpl>, unsigned, 5> map2(map1); |
| |
| EXPECT_EQ(map1.size(), count); |
| EXPECT_EQ(map2.size(), count); |
| |
| for (unsigned i = 0; i < count; ++i) { |
| EXPECT_TRUE(map1.contains(strings[i].impl())); |
| EXPECT_TRUE(map2.contains(strings[i].impl())); |
| EXPECT_EQ(map1.find(strings[i].impl())->value, i); |
| EXPECT_EQ(map2.find(strings[i].impl())->value, i); |
| } |
| |
| // Modifying copy should not affect original |
| map2.add(String("extra"_s).impl(), 999); |
| EXPECT_EQ(map1.size(), count); |
| EXPECT_EQ(map2.size(), count + 1); |
| } |
| |
| // --- Stress tests --- |
| |
| TEST(WTF_InlineMap, StressInsertions) |
| { |
| // Inserting 1000 entries works and all are retrievable. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 1000; ++i) { |
| auto result = map.add(i, i * 10); |
| EXPECT_TRUE(result.isNewEntry); |
| } |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_EQ(map.size(), 1000u); |
| |
| for (unsigned i = 1; i <= 1000; ++i) { |
| EXPECT_TRUE(map.contains(i)); |
| auto it = map.find(i); |
| ASSERT_FALSE(it == map.end()); |
| EXPECT_EQ(it->value, i * 10); |
| } |
| |
| EXPECT_FALSE(map.contains(1001)); |
| } |
| |
| TEST(WTF_InlineMap, StressInsertRemoveReinsert) |
| { |
| // Bulk remove of odd keys and re-insert with new values preserves integrity. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| // Add 200 entries |
| for (unsigned i = 1; i <= 200; ++i) |
| map.add(i, i); |
| |
| EXPECT_EQ(map.size(), 200u); |
| |
| // Remove odd-keyed entries |
| for (unsigned i = 1; i <= 200; i += 2) |
| EXPECT_TRUE(map.remove(i)); |
| |
| EXPECT_EQ(map.size(), 100u); |
| |
| // Verify even entries remain, odd entries gone |
| for (unsigned i = 1; i <= 200; ++i) { |
| if (!(i % 2)) { |
| EXPECT_TRUE(map.contains(i)); |
| EXPECT_EQ(map.find(i)->value, i); |
| } else |
| EXPECT_FALSE(map.contains(i)); |
| } |
| |
| // Re-add odd entries with new values |
| for (unsigned i = 1; i <= 200; i += 2) { |
| auto result = map.add(i, i + 1000); |
| EXPECT_TRUE(result.isNewEntry); |
| } |
| |
| EXPECT_EQ(map.size(), 200u); |
| |
| // Verify all entries |
| for (unsigned i = 1; i <= 200; ++i) { |
| EXPECT_TRUE(map.contains(i)); |
| unsigned expectedValue = (i % 2) ? i + 1000 : i; |
| EXPECT_EQ(map.find(i)->value, expectedValue); |
| } |
| } |
| |
| TEST(WTF_InlineMap, StressRemoveAll) |
| { |
| // Removing all 500 entries then re-adding them works correctly. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| // Add 500 entries |
| for (unsigned i = 1; i <= 500; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_EQ(map.size(), 500u); |
| |
| // Remove all one by one |
| for (unsigned i = 1; i <= 500; ++i) |
| EXPECT_TRUE(map.remove(i)); |
| |
| EXPECT_EQ(map.size(), 0u); |
| EXPECT_TRUE(map.isEmpty()); |
| |
| for (unsigned i = 1; i <= 500; ++i) |
| EXPECT_FALSE(map.contains(i)); |
| |
| // Re-add 500 entries with different values |
| for (unsigned i = 1; i <= 500; ++i) { |
| auto result = map.add(i, i + 5000); |
| EXPECT_TRUE(result.isNewEntry); |
| } |
| |
| EXPECT_EQ(map.size(), 500u); |
| |
| for (unsigned i = 1; i <= 500; ++i) { |
| EXPECT_TRUE(map.contains(i)); |
| EXPECT_EQ(map.find(i)->value, i + 5000); |
| } |
| } |
| |
| TEST(WTF_InlineMap, DuplicateAddHashedMode) |
| { |
| // Duplicate adds in hashed mode preserve original values. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 20; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_EQ(map.size(), 20u); |
| |
| // Attempt duplicate adds |
| for (unsigned i = 1; i <= 20; ++i) { |
| auto result = map.add(i, i * 100); |
| EXPECT_FALSE(result.isNewEntry); |
| EXPECT_EQ(result.iterator->value, i * 10); // Original value preserved |
| } |
| |
| EXPECT_EQ(map.size(), 20u); |
| } |
| |
| TEST(WTF_InlineMap, IterationInlineMode) |
| { |
| // Iteration visits all entries while still in inline mode. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 3; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| HashSet<unsigned, IntHash<unsigned>, WTF::UnsignedWithZeroKeyHashTraits<unsigned>> seenKeys; |
| unsigned count = 0; |
| |
| for (auto& entry : map) { |
| seenKeys.add(entry.key); |
| EXPECT_EQ(entry.value, entry.key * 10); |
| ++count; |
| } |
| |
| EXPECT_EQ(count, 3u); |
| EXPECT_EQ(seenKeys.size(), 3u); |
| for (unsigned i = 1; i <= 3; ++i) |
| EXPECT_TRUE(seenKeys.contains(i)); |
| } |
| |
| TEST(WTF_InlineMap, MoveConstructionInlineMode) |
| { |
| // Move construction in inline mode transfers entries and preserves inline storage. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map1; |
| |
| for (unsigned i = 1; i <= 3; ++i) |
| map1.add(i, i * 10); |
| |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map1)); |
| |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map2(WTF::move(map1)); |
| |
| EXPECT_TRUE(map1.isEmpty()); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map1)); |
| EXPECT_EQ(map2.size(), 3u); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map2)); |
| |
| for (unsigned i = 1; i <= 3; ++i) { |
| EXPECT_TRUE(map2.contains(i)); |
| EXPECT_EQ(map2.find(i)->value, i * 10); |
| } |
| } |
| |
| TEST(WTF_InlineMap, MoveConstructionHashedMode) |
| { |
| // Move construction in hashed mode steals the heap pointer. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map1; |
| |
| for (unsigned i = 1; i <= 20; ++i) |
| map1.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map1)); |
| |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map2(WTF::move(map1)); |
| |
| EXPECT_TRUE(map1.isEmpty()); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map1)); // Reset to inline after move |
| EXPECT_EQ(map2.size(), 20u); |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map2)); // Stole heap pointer |
| |
| for (unsigned i = 1; i <= 20; ++i) { |
| EXPECT_TRUE(map2.contains(i)); |
| EXPECT_EQ(map2.find(i)->value, i * 10); |
| } |
| } |
| |
| TEST(WTF_InlineMap, MoveAssignmentInlineToInline) |
| { |
| // Move assignment between two inline maps works correctly. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map1; |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map2; |
| |
| for (unsigned i = 1; i <= 3; ++i) |
| map1.add(i, i * 10); |
| |
| map2.add(100, 1000); |
| |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map1)); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map2)); |
| |
| map2 = WTF::move(map1); |
| |
| EXPECT_TRUE(map1.isEmpty()); |
| EXPECT_EQ(map2.size(), 3u); |
| EXPECT_FALSE(map2.contains(100)); |
| |
| for (unsigned i = 1; i <= 3; ++i) { |
| EXPECT_TRUE(map2.contains(i)); |
| EXPECT_EQ(map2.find(i)->value, i * 10); |
| } |
| } |
| |
| TEST(WTF_InlineMap, SwapInlineAndHashed) |
| { |
| // Swapping an inline map with a hashed map exchanges both content and storage mode. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> inlineMap; |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> hashedMap; |
| |
| for (unsigned i = 1; i <= 3; ++i) |
| inlineMap.add(i, i * 10); |
| |
| for (unsigned i = 100; i <= 120; ++i) |
| hashedMap.add(i, i); |
| |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(inlineMap)); |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(hashedMap)); |
| |
| inlineMap.swap(hashedMap); |
| |
| // inlineMap now has hashed content |
| EXPECT_EQ(inlineMap.size(), 21u); |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(inlineMap)); |
| for (unsigned i = 100; i <= 120; ++i) { |
| EXPECT_TRUE(inlineMap.contains(i)); |
| EXPECT_EQ(inlineMap.find(i)->value, i); |
| } |
| EXPECT_FALSE(inlineMap.contains(1)); |
| |
| // hashedMap now has inline content |
| EXPECT_EQ(hashedMap.size(), 3u); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(hashedMap)); |
| for (unsigned i = 1; i <= 3; ++i) { |
| EXPECT_TRUE(hashedMap.contains(i)); |
| EXPECT_EQ(hashedMap.find(i)->value, i * 10); |
| } |
| EXPECT_FALSE(hashedMap.contains(100)); |
| } |
| |
| TEST(WTF_InlineMap, ClearThenGrow) |
| { |
| // A cleared map transitions to inline and can grow back to hashed. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 20; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| map.clear(); |
| |
| EXPECT_TRUE(map.isEmpty()); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Add enough entries to trigger growth back to hashed |
| for (unsigned i = 1; i <= 100; ++i) |
| map.add(i, i + 100); |
| |
| EXPECT_EQ(map.size(), 100u); |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| for (unsigned i = 1; i <= 100; ++i) { |
| EXPECT_TRUE(map.contains(i)); |
| EXPECT_EQ(map.find(i)->value, i + 100); |
| } |
| } |
| |
| TEST(WTF_InlineMap, CopyWithDeletedEntries) |
| { |
| // Copying a map with deleted tombstones preserves them in the copy. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map1; |
| |
| for (unsigned i = 1; i <= 20; ++i) |
| map1.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map1)); |
| |
| // Remove some entries to create deleted tombstones |
| map1.remove(3); |
| map1.remove(7); |
| map1.remove(11); |
| map1.remove(15); |
| map1.remove(19); |
| |
| EXPECT_EQ(map1.size(), 15u); |
| |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map2(map1); |
| |
| EXPECT_EQ(map2.size(), 15u); |
| |
| // Verify copy has exactly the right entries |
| for (unsigned i = 1; i <= 20; ++i) { |
| if (i == 3 || i == 7 || i == 11 || i == 15 || i == 19) |
| EXPECT_FALSE(map2.contains(i)); |
| else { |
| EXPECT_TRUE(map2.contains(i)); |
| EXPECT_EQ(map2.find(i)->value, i * 10); |
| } |
| } |
| |
| // Adding to copy should not affect original |
| map2.add(3, 999); |
| EXPECT_TRUE(map2.contains(3)); |
| EXPECT_FALSE(map1.contains(3)); |
| } |
| |
| TEST(WTF_InlineMap, RemoveLastInlineEntry) |
| { |
| // Removing the sole inline entry leaves the map empty and still usable. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| map.add(42, 420); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_EQ(map.size(), 1u); |
| |
| EXPECT_TRUE(map.remove(42)); |
| EXPECT_EQ(map.size(), 0u); |
| EXPECT_TRUE(map.isEmpty()); |
| EXPECT_FALSE(map.contains(42)); |
| |
| // Map should still be usable |
| map.add(99, 990); |
| EXPECT_EQ(map.size(), 1u); |
| EXPECT_TRUE(map.contains(99)); |
| EXPECT_EQ(map.find(99)->value, 990u); |
| } |
| |
| // --- Shrink / deleted-count / transition-back-to-inline tests --- |
| |
| TEST(WTF_InlineMap, ShrinkAfterRemovalTransitionsToInline) |
| { |
| // Removing enough entries from hashed mode triggers shrink back to inline. |
| // With InlineCapacity=5, adding 20 entries gives capacity=32. |
| // Shrink triggers when m_size * 6 < capacity. At capacity=32, that's m_size < 5.33. |
| // Since m_size=5 <= InlineCapacity=5, we transition to inline. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 20; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_EQ(map.size(), 20u); |
| |
| // Remove entries 6 through 20, leaving keys 1-5 |
| for (unsigned i = 6; i <= 20; ++i) |
| EXPECT_TRUE(map.remove(i)); |
| |
| EXPECT_EQ(map.size(), 5u); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // All remaining entries should be accessible |
| for (unsigned i = 1; i <= 5; ++i) { |
| EXPECT_TRUE(map.contains(i)); |
| EXPECT_EQ(map.find(i)->value, i * 10); |
| } |
| for (unsigned i = 6; i <= 20; ++i) |
| EXPECT_FALSE(map.contains(i)); |
| } |
| |
| TEST(WTF_InlineMap, ShrinkAfterRemovalReducesCapacity) |
| { |
| // Removing entries reduces capacity but stays hashed when size > InlineCapacity. |
| // With InlineCapacity=3, adding 100 entries gives capacity=256. |
| // Remove down to 10 entries: should shrink but remain hashed since 10 > 3. |
| InlineMap<unsigned, unsigned, 3, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 100; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| unsigned originalCapacity = WTF::InlineMapAccessForTesting::capacity(map); |
| |
| // Remove entries, keeping 1-10 |
| for (unsigned i = 11; i <= 100; ++i) |
| EXPECT_TRUE(map.remove(i)); |
| |
| EXPECT_EQ(map.size(), 10u); |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_LT(WTF::InlineMapAccessForTesting::capacity(map), originalCapacity); |
| |
| for (unsigned i = 1; i <= 10; ++i) { |
| EXPECT_TRUE(map.contains(i)); |
| EXPECT_EQ(map.find(i)->value, i * 10); |
| } |
| } |
| |
| TEST(WTF_InlineMap, DeletedCountTracking) |
| { |
| // Deleted count is incremented on remove and decremented when a deleted slot is reused. |
| InlineMap<unsigned, unsigned, 3, IntHash<unsigned>> map; |
| |
| // Go to hashed mode |
| for (unsigned i = 1; i <= 10; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_EQ(WTF::InlineMapAccessForTesting::deletedCount(map), 0u); |
| |
| // Remove 2 entries (these won't trigger shrink since 8*6=48 is not < 16) |
| EXPECT_TRUE(map.remove(1)); |
| EXPECT_TRUE(map.remove(2)); |
| EXPECT_EQ(map.size(), 8u); |
| EXPECT_EQ(WTF::InlineMapAccessForTesting::deletedCount(map), 2u); |
| |
| // Re-add one of the removed keys — should reuse a deleted slot |
| auto result = map.add(1, 100); |
| EXPECT_TRUE(result.isNewEntry); |
| EXPECT_EQ(WTF::InlineMapAccessForTesting::deletedCount(map), 1u); |
| EXPECT_EQ(map.size(), 9u); |
| } |
| |
| TEST(WTF_InlineMap, DeletedCountResetAfterRehash) |
| { |
| // Deleted count resets to 0 after a rehash triggered by expansion. |
| InlineMap<unsigned, unsigned, 3, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 10; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Remove a couple entries to build up deleted count |
| map.remove(1); |
| map.remove(2); |
| EXPECT_EQ(WTF::InlineMapAccessForTesting::deletedCount(map), 2u); |
| |
| // Add enough new entries to trigger expansion |
| // Current: m_size=8, deletedCount=2, capacity=16 |
| // Expansion triggers when (8+2)*4 >= 16*3 -> 40 >= 48 -> false |
| // After adding 4 more: m_size=12, deletedCount=2 -> (12+2)*4 = 56 >= 48 -> true |
| for (unsigned i = 11; i <= 14; ++i) |
| map.add(i, i * 10); |
| |
| // After expansion, deleted count should be 0 |
| EXPECT_EQ(WTF::InlineMapAccessForTesting::deletedCount(map), 0u); |
| |
| // All live entries should be accessible |
| EXPECT_FALSE(map.contains(1)); |
| EXPECT_FALSE(map.contains(2)); |
| for (unsigned i = 3; i <= 14; ++i) { |
| EXPECT_TRUE(map.contains(i)); |
| EXPECT_EQ(map.find(i)->value, i * 10); |
| } |
| } |
| |
| TEST(WTF_InlineMap, CompactOnlyWhenManyDeleted) |
| { |
| // When many entries are deleted and new (non-reusable) keys are added, |
| // the expansion check triggers. If shouldCompactOnly is true, the table |
| // rehashes at the same capacity rather than doubling. |
| InlineMap<unsigned, unsigned, 3, IntHash<unsigned>> map; |
| |
| // Build a larger table: add 24 entries to get capacity=32 |
| // (transitions: 4→cap8, 6→cap16, 12→cap32) |
| for (unsigned i = 1; i <= 24; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_EQ(WTF::InlineMapAccessForTesting::capacity(map), 32u); |
| |
| // Remove 20 entries, keeping only 4 live entries (keys 21-24). |
| // shouldShrink: 4*6=24 < 32? Yes — but shrink happens one removal at a time. |
| // At m_size=5: 5*6=30 < 32 → shrink to 16. deletedCount resets to 0. |
| // Then at m_size=4 in cap=16: 4*6=24 not < 16 → no further shrink. |
| for (unsigned i = 1; i <= 20; ++i) |
| map.remove(i); |
| |
| // After shrinks, we should be at capacity=16 with 4 live entries. |
| EXPECT_EQ(map.size(), 4u); |
| |
| // Now create deleted entries without triggering shrink. |
| // Add 8 more entries (keys 25-32) to get m_size=12 at some capacity, |
| // then remove 6 of them. |
| for (unsigned i = 25; i <= 32; ++i) |
| map.add(i, i * 10); |
| |
| // Remove 6 entries without hitting shrink threshold |
| for (unsigned i = 25; i <= 30; ++i) |
| map.remove(i); |
| |
| // Now we have 6 live entries and 6 deleted entries. |
| EXPECT_EQ(map.size(), 6u); |
| unsigned currentCapacity = WTF::InlineMapAccessForTesting::capacity(map); |
| |
| // Add new keys (that won't match any deleted slot's key) until expansion triggers. |
| // Expansion triggers when (m_size + deletedCount) * 4 >= capacity * 3. |
| // Each add of a new key into an empty slot: m_size++, deletedCount unchanged, sum increases. |
| // Each add into a deleted slot: m_size++, deletedCount--, sum unchanged. |
| // We keep adding until expansion happens. |
| unsigned nextKey = 100; |
| while (WTF::InlineMapAccessForTesting::capacity(map) == currentCapacity) { |
| map.add(nextKey, nextKey * 10); |
| ++nextKey; |
| } |
| |
| // After expansion, deletedCount should be 0 |
| EXPECT_EQ(WTF::InlineMapAccessForTesting::deletedCount(map), 0u); |
| |
| // All live entries should be accessible |
| for (unsigned i = 21; i <= 24; ++i) |
| EXPECT_TRUE(map.contains(i)); |
| for (unsigned i = 31; i <= 32; ++i) |
| EXPECT_TRUE(map.contains(i)); |
| } |
| |
| TEST(WTF_InlineMap, ClearTransitionsToInline) |
| { |
| // Clearing a hashed-mode map transitions back to inline mode. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 20; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| map.clear(); |
| |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_TRUE(map.isEmpty()); |
| EXPECT_EQ(map.size(), 0u); |
| |
| // Re-adding entries should work with normal inline→hashed lifecycle |
| for (unsigned i = 1; i <= 5; ++i) { |
| map.add(i, i); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| } |
| |
| map.add(6, 6); |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| } |
| |
| TEST(WTF_InlineMap, ClearEmptyHashedTransitionsToInline) |
| { |
| // Clearing an empty hashed-mode map (from reserveInitialCapacity) transitions to inline. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| map.reserveInitialCapacity(20); |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_TRUE(map.isEmpty()); |
| |
| map.clear(); |
| |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_TRUE(map.isEmpty()); |
| |
| // Should be usable as a fresh map |
| map.add(1, 10); |
| EXPECT_EQ(map.size(), 1u); |
| EXPECT_TRUE(map.contains(1)); |
| } |
| |
| TEST(WTF_InlineMap, ShrinkToInlineWithStrings) |
| { |
| // Shrink-to-inline transition works with String keys. |
| InlineMap<String, unsigned, 5, StringHash> map; |
| |
| for (unsigned i = 1; i <= 20; ++i) |
| map.add(makeString("key"_s, i), i); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Remove entries, keeping only 5 |
| for (unsigned i = 6; i <= 20; ++i) |
| EXPECT_TRUE(map.remove(makeString("key"_s, i))); |
| |
| EXPECT_EQ(map.size(), 5u); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Verify remaining entries |
| for (unsigned i = 1; i <= 5; ++i) { |
| auto key = makeString("key"_s, i); |
| EXPECT_TRUE(map.contains(key)); |
| EXPECT_EQ(map.find(key)->value, i); |
| } |
| } |
| |
| TEST(WTF_InlineMap, ShrinkPreservesDataIntegrity) |
| { |
| // Exact key-value pairs survive the hashed→inline transition, and the map |
| // can grow back to hashed afterwards. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 50; ++i) |
| map.add(i, i * 100); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Remove all but keys 10, 20, 30, 40, 50 |
| for (unsigned i = 1; i <= 50; ++i) { |
| if (i % 10) |
| map.remove(i); |
| } |
| |
| EXPECT_EQ(map.size(), 5u); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Verify exact key-value pairs |
| EXPECT_EQ(map.find(10)->value, 1000u); |
| EXPECT_EQ(map.find(20)->value, 2000u); |
| EXPECT_EQ(map.find(30)->value, 3000u); |
| EXPECT_EQ(map.find(40)->value, 4000u); |
| EXPECT_EQ(map.find(50)->value, 5000u); |
| |
| // Grow back to hashed |
| for (unsigned i = 51; i <= 60; ++i) |
| map.add(i, i * 100); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_EQ(map.size(), 15u); |
| |
| // Original entries still accessible |
| EXPECT_EQ(map.find(10)->value, 1000u); |
| EXPECT_EQ(map.find(50)->value, 5000u); |
| } |
| |
| TEST(WTF_InlineMap, RepeatedGrowShrinkCycles) |
| { |
| // Multiple cycles of grow-to-hashed then shrink-to-inline work correctly. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| unsigned nextKey = 1; |
| |
| for (int cycle = 0; cycle < 3; ++cycle) { |
| // Add 20 entries to go hashed |
| unsigned firstKey = nextKey; |
| for (unsigned i = 0; i < 20; ++i) { |
| map.add(nextKey, nextKey * 10); |
| ++nextKey; |
| } |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Remove all but 2 entries from this batch |
| for (unsigned i = firstKey; i < firstKey + 18; ++i) |
| map.remove(i); |
| |
| // Verify the map is inline if small enough, and the remaining entries are correct |
| if (map.size() <= 5) |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| unsigned remaining1 = firstKey + 18; |
| unsigned remaining2 = firstKey + 19; |
| EXPECT_TRUE(map.contains(remaining1)); |
| EXPECT_TRUE(map.contains(remaining2)); |
| } |
| |
| // After 3 cycles, we have 6 entries (2 per cycle) |
| EXPECT_EQ(map.size(), 6u); |
| } |
| |
| TEST(WTF_InlineMap, RemoveAllOneByOneTriggersShrink) |
| { |
| // Removing all entries one by one eventually transitions back to inline. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 20; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| bool becameInline = false; |
| for (unsigned i = 1; i <= 20; ++i) { |
| EXPECT_TRUE(map.remove(i)); |
| if (!becameInline && WTF::InlineMapAccessForTesting::isInline(map)) |
| becameInline = true; |
| } |
| |
| EXPECT_TRUE(becameInline); |
| EXPECT_TRUE(map.isEmpty()); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| } |
| |
| TEST(WTF_InlineMap, DeletedCountWithCollisions) |
| { |
| // Deleted count tracking works correctly under hash collisions. |
| InlineMap<unsigned, unsigned, 5, ZeroHash<unsigned>> map; |
| |
| // All entries hash to bucket 0 |
| for (unsigned i = 1; i <= 10; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_EQ(WTF::InlineMapAccessForTesting::deletedCount(map), 0u); |
| |
| // Remove 3 entries |
| map.remove(3); |
| map.remove(5); |
| map.remove(7); |
| EXPECT_EQ(WTF::InlineMapAccessForTesting::deletedCount(map), 3u); |
| EXPECT_EQ(map.size(), 7u); |
| |
| // Re-add one of them — should reuse a deleted slot and decrement deletedCount |
| map.add(3, 300); |
| EXPECT_EQ(WTF::InlineMapAccessForTesting::deletedCount(map), 2u); |
| EXPECT_EQ(map.size(), 8u); |
| EXPECT_EQ(map.find(3)->value, 300u); |
| |
| // All remaining entries accessible |
| for (unsigned i = 1; i <= 10; ++i) { |
| if (i == 5 || i == 7) |
| EXPECT_FALSE(map.contains(i)); |
| else |
| EXPECT_TRUE(map.contains(i)); |
| } |
| } |
| |
| TEST(WTF_InlineMap, ExpandWithDeletedEntriesPreservesData) |
| { |
| // Expansion triggered by deleted entries filling the table preserves all live data. |
| // We add entries until expansion triggers, then verify everything is intact. |
| InlineMap<unsigned, unsigned, 3, IntHash<unsigned>> map; |
| |
| // Build up a table then create deleted entries |
| for (unsigned i = 1; i <= 10; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Remove entries but stay above shrink threshold |
| map.remove(1); |
| map.remove(2); |
| map.remove(3); |
| map.remove(4); |
| // m_size=6, some deletedCount, some capacity |
| EXPECT_EQ(map.size(), 6u); |
| |
| unsigned capacityBefore = WTF::InlineMapAccessForTesting::capacity(map); |
| |
| // Add new keys until expansion triggers (capacity changes) |
| unsigned nextKey = 100; |
| while (WTF::InlineMapAccessForTesting::capacity(map) == capacityBefore) { |
| map.add(nextKey, nextKey * 10); |
| ++nextKey; |
| } |
| |
| // After expansion, deleted count should be 0 |
| EXPECT_EQ(WTF::InlineMapAccessForTesting::deletedCount(map), 0u); |
| |
| // All live entries preserved |
| for (unsigned i = 5; i <= 10; ++i) { |
| EXPECT_TRUE(map.contains(i)); |
| EXPECT_EQ(map.find(i)->value, i * 10); |
| } |
| for (unsigned i = 100; i < nextKey; ++i) { |
| EXPECT_TRUE(map.contains(i)); |
| EXPECT_EQ(map.find(i)->value, i * 10); |
| } |
| for (unsigned i = 1; i <= 4; ++i) |
| EXPECT_FALSE(map.contains(i)); |
| } |
| |
| TEST(WTF_InlineMap, MoveConstructionPreservesDeletedCount) |
| { |
| // Move-constructing from a hashed map with deleted entries transfers deletedCount. |
| InlineMap<unsigned, unsigned, 3, IntHash<unsigned>> map1; |
| |
| for (unsigned i = 1; i <= 10; ++i) |
| map1.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map1)); |
| |
| // Remove entries without triggering shrink |
| map1.remove(1); |
| map1.remove(2); |
| EXPECT_EQ(WTF::InlineMapAccessForTesting::deletedCount(map1), 2u); |
| |
| InlineMap<unsigned, unsigned, 3, IntHash<unsigned>> map2(WTF::move(map1)); |
| |
| EXPECT_TRUE(map1.isEmpty()); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map1)); |
| |
| EXPECT_EQ(map2.size(), 8u); |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map2)); |
| EXPECT_EQ(WTF::InlineMapAccessForTesting::deletedCount(map2), 2u); |
| |
| for (unsigned i = 3; i <= 10; ++i) { |
| EXPECT_TRUE(map2.contains(i)); |
| EXPECT_EQ(map2.find(i)->value, i * 10); |
| } |
| } |
| |
| TEST(WTF_InlineMap, CopyConstructionPreservesDeletedCount) |
| { |
| // Copy-constructing from a hashed map with deleted entries preserves deletedCount. |
| InlineMap<unsigned, unsigned, 3, IntHash<unsigned>> map1; |
| |
| for (unsigned i = 1; i <= 10; ++i) |
| map1.add(i, i * 10); |
| |
| map1.remove(1); |
| map1.remove(2); |
| EXPECT_EQ(WTF::InlineMapAccessForTesting::deletedCount(map1), 2u); |
| |
| InlineMap<unsigned, unsigned, 3, IntHash<unsigned>> map2(map1); |
| |
| // Both maps should have same state |
| EXPECT_EQ(map1.size(), 8u); |
| EXPECT_EQ(map2.size(), 8u); |
| EXPECT_EQ(WTF::InlineMapAccessForTesting::deletedCount(map1), 2u); |
| EXPECT_EQ(WTF::InlineMapAccessForTesting::deletedCount(map2), 2u); |
| |
| // Both maps should have the same entries |
| for (unsigned i = 3; i <= 10; ++i) { |
| EXPECT_TRUE(map1.contains(i)); |
| EXPECT_TRUE(map2.contains(i)); |
| EXPECT_EQ(map1.find(i)->value, i * 10); |
| EXPECT_EQ(map2.find(i)->value, i * 10); |
| } |
| EXPECT_FALSE(map1.contains(1)); |
| EXPECT_FALSE(map2.contains(1)); |
| } |
| |
| TEST(WTF_InlineMap, ShrinkWithMoveOnlyValues) |
| { |
| // Move-only values survive the hashed→inline transition. |
| InlineMap<unsigned, MoveOnly, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 20; ++i) |
| map.add(i, MoveOnly(i)); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Remove entries, keeping 1-4 |
| for (unsigned i = 5; i <= 20; ++i) |
| map.remove(i); |
| |
| EXPECT_EQ(map.size(), 4u); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| for (unsigned i = 1; i <= 4; ++i) { |
| auto it = map.find(i); |
| ASSERT_FALSE(it == map.end()); |
| EXPECT_EQ(it->value.value(), i); |
| } |
| } |
| |
| TEST(WTF_InlineMap, ShrinkDoesNotHappenAboveThreshold) |
| { |
| // Removing entries that keep the load above 1/6 does not trigger shrink. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 20; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| unsigned capacityBefore = WTF::InlineMapAccessForTesting::capacity(map); |
| |
| // With capacity=32, shrink triggers when m_size * 6 < 32, i.e., m_size < 5.33 |
| // Removing down to m_size=6 should NOT trigger shrink (6*6=36 >= 32) |
| for (unsigned i = 7; i <= 20; ++i) |
| EXPECT_TRUE(map.remove(i)); |
| |
| EXPECT_EQ(map.size(), 6u); |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_EQ(WTF::InlineMapAccessForTesting::capacity(map), capacityBefore); |
| EXPECT_EQ(WTF::InlineMapAccessForTesting::deletedCount(map), 14u); |
| |
| // All remaining entries accessible |
| for (unsigned i = 1; i <= 6; ++i) { |
| EXPECT_TRUE(map.contains(i)); |
| EXPECT_EQ(map.find(i)->value, i * 10); |
| } |
| } |
| |
| TEST(WTF_InlineMap, ClearHashedMapWithDeletedEntries) |
| { |
| // clear() on a hashed map with nonzero deletedCount transitions to inline. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 20; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Remove some entries to build up deletedCount |
| map.remove(3); |
| map.remove(7); |
| map.remove(11); |
| EXPECT_EQ(WTF::InlineMapAccessForTesting::deletedCount(map), 3u); |
| EXPECT_EQ(map.size(), 17u); |
| |
| map.clear(); |
| EXPECT_TRUE(map.isEmpty()); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Re-adding entries works normally |
| for (unsigned i = 1; i <= 10; ++i) |
| map.add(i, i * 100); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| EXPECT_EQ(map.size(), 10u); |
| for (unsigned i = 1; i <= 10; ++i) |
| EXPECT_EQ(map.find(i)->value, i * 100); |
| } |
| |
| TEST(WTF_InlineMap, CopyAfterShrinkToInline) |
| { |
| // Copy and move construction work correctly after a hashed→inline shrink. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 20; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Remove down to 5 entries, triggering shrink to inline |
| for (unsigned i = 6; i <= 20; ++i) |
| map.remove(i); |
| |
| EXPECT_EQ(map.size(), 5u); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Copy-construct from the shrunk inline map |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> mapCopy(map); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(mapCopy)); |
| EXPECT_EQ(mapCopy.size(), 5u); |
| for (unsigned i = 1; i <= 5; ++i) { |
| EXPECT_TRUE(mapCopy.contains(i)); |
| EXPECT_EQ(mapCopy.find(i)->value, i * 10); |
| } |
| |
| // Original still intact |
| for (unsigned i = 1; i <= 5; ++i) { |
| EXPECT_TRUE(map.contains(i)); |
| EXPECT_EQ(map.find(i)->value, i * 10); |
| } |
| |
| // Move-construct from the shrunk inline map |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> mapMoved(WTF::move(map)); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(mapMoved)); |
| EXPECT_EQ(mapMoved.size(), 5u); |
| for (unsigned i = 1; i <= 5; ++i) { |
| EXPECT_TRUE(mapMoved.contains(i)); |
| EXPECT_EQ(mapMoved.find(i)->value, i * 10); |
| } |
| |
| EXPECT_TRUE(map.isEmpty()); |
| } |
| |
| TEST(WTF_InlineMap, IterationAfterShrinkToInline) |
| { |
| // Range-for iteration works correctly after hashed→inline shrink. |
| InlineMap<unsigned, unsigned, 5, IntHash<unsigned>> map; |
| |
| for (unsigned i = 1; i <= 20; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Remove down to 4 entries (keys 1-4 survive) |
| for (unsigned i = 5; i <= 20; ++i) |
| map.remove(i); |
| |
| EXPECT_EQ(map.size(), 4u); |
| EXPECT_TRUE(WTF::InlineMapAccessForTesting::isInline(map)); |
| |
| // Iterate and collect all entries |
| HashSet<unsigned> seenKeys; |
| unsigned total = 0; |
| for (auto& entry : map) { |
| EXPECT_TRUE(entry.key >= 1 && entry.key <= 4); |
| EXPECT_EQ(entry.value, entry.key * 10); |
| seenKeys.add(entry.key); |
| ++total; |
| } |
| |
| EXPECT_EQ(total, 4u); |
| for (unsigned i = 1; i <= 4; ++i) |
| EXPECT_TRUE(seenKeys.contains(i)); |
| } |
| |
| TEST(WTF_InlineMap, MustRehashInPlacePathDirect) |
| { |
| // Directly tests the mustRehashInPlace() path: many deleted entries cause |
| // expand() to rehash in place (same capacity) rather than doubling. |
| InlineMap<unsigned, unsigned, 3, IntHash<unsigned>> map; |
| |
| // Add enough entries to get a known capacity |
| for (unsigned i = 1; i <= 20; ++i) |
| map.add(i, i * 10); |
| |
| EXPECT_FALSE(WTF::InlineMapAccessForTesting::isInline(map)); |
| unsigned initialCapacity = WTF::InlineMapAccessForTesting::capacity(map); |
| |
| // Remove entries to create many deleted slots. |
| // Keep enough live entries that shouldShrink() is false but mustRehashInPlace() is true. |
| // shouldShrink: m_size * 6 < capacity => false when m_size >= capacity/6 |
| // mustRehashInPlace: m_size * 6 < capacity * 2 => true when m_size < capacity/3 |
| // So we need capacity/6 <= m_size < capacity/3. |
| // With capacity=32: we need 6 <= m_size < 11. |
| unsigned targetSize = initialCapacity / 4; // safely between capacity/6 and capacity/3 |
| if (targetSize < initialCapacity / 6 + 1) |
| targetSize = initialCapacity / 6 + 1; |
| |
| for (unsigned i = targetSize + 1; i <= 20; ++i) |
| map.remove(i); |
| |
| EXPECT_EQ(map.size(), targetSize); |
| unsigned capacityAfterRemoves = WTF::InlineMapAccessForTesting::capacity(map); |
| unsigned deletedAfterRemoves = WTF::InlineMapAccessForTesting::deletedCount(map); |
| EXPECT_GT(deletedAfterRemoves, 0u); |
| |
| // Now add new unique keys until expansion triggers. |
| // Because (m_size + deletedCount) * 4 >= capacity * 3, the expansion check fires. |
| // Since mustRehashInPlace() is true, it should rehash to the same capacity. |
| unsigned nextKey = 1000; |
| while (WTF::InlineMapAccessForTesting::capacity(map) == capacityAfterRemoves |
| && WTF::InlineMapAccessForTesting::deletedCount(map) > 0) { |
| map.add(nextKey, nextKey * 10); |
| ++nextKey; |
| } |
| |
| // Either capacity stayed the same (rehash in place reclaimed deleted slots) |
| // or if all deleted slots were reused before expansion, deletedCount hit 0. |
| // In either case, deletedCount should be 0 after rehash. |
| EXPECT_EQ(WTF::InlineMapAccessForTesting::deletedCount(map), 0u); |
| |
| // Verify all live entries are accessible |
| for (unsigned i = 1; i <= targetSize; ++i) { |
| EXPECT_TRUE(map.contains(i)); |
| EXPECT_EQ(map.find(i)->value, i * 10); |
| } |
| for (unsigned i = 1000; i < nextKey; ++i) { |
| EXPECT_TRUE(map.contains(i)); |
| EXPECT_EQ(map.find(i)->value, i * 10); |
| } |
| } |
| |
| } // namespace TestWebKitAPI |