| // Copyright (c) 2020 The Chromium Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "hash_table_base.h" | |
| #include "util/test/test.h" | |
| #include <algorithm> | |
| #include <vector> | |
| // This unit-test is also used to illustrate how to use HashTableBase<> | |
| // in a concrete way. Here, each node is a simple pointer to a Int class | |
| // that wraps a simple integer, but also keeps tracks of | |
| // construction/destruction steps in global counters. This is used by the | |
| // test to verify that operations like copies or moves do not miss or | |
| // create allocations/deallocations. | |
| // | |
| // Because the derived table HashTableTest owns all pointer objects, it | |
| // needs to manually create/deallocate them in its destructor, copy/move | |
| // constructors and operators, as well as insert()/erase()/clear() methods. | |
| // | |
| // Finally, iteration support is provided through const_iterator and | |
| // begin(), end() methods, which are enough for range-based for loops. | |
| // | |
| // A simple int wrapper class that can also count creation/destruction. | |
| class Int { | |
| public: | |
| explicit Int(int x) : x_(x) { creation_counter++; } | |
| Int(const Int& other) : x_(other.x_) { creation_counter++; } | |
| ~Int() { destruction_counter++; } | |
| int& x() { return x_; } | |
| const int& x() const { return x_; } | |
| size_t hash() const { return static_cast<size_t>(x_); } | |
| static void ResetCounters() { | |
| creation_counter = 0; | |
| destruction_counter = 0; | |
| } | |
| int x_; | |
| static size_t creation_counter; | |
| static size_t destruction_counter; | |
| }; | |
| size_t Int::creation_counter; | |
| size_t Int::destruction_counter; | |
| // A HashTableBase<> node type that contains a simple pointer to a Int value. | |
| struct TestHashNode { | |
| Int* int_ptr; | |
| bool is_null() const { return !int_ptr; } | |
| bool is_tombstone() const { return int_ptr == &kTombstone; } | |
| bool is_valid() const { return !is_null() && !is_tombstone(); } | |
| size_t hash_value() const { return int_ptr->hash(); } | |
| static Int kTombstone; | |
| }; | |
| // Value is irrelevant since only its address is used. | |
| Int TestHashNode::kTombstone(-1); | |
| // TestHashTable derives a HashTableBase<> instantiation to demonstrate | |
| // full uses of the template. This includes: | |
| // | |
| // - Storing a pointer in each node, and managing ownership of pointed | |
| // objects explicitly in the destructor, copy/move constructor and | |
| // operator, as well as insert() and erase() methods. | |
| // | |
| // - The internal pointed objects are Int instance, but the TestHashTable | |
| // API masks that entirely, instead implementing a simple set of integers, | |
| // including iteration support. | |
| // | |
| // Note that placing the integers directly in the nodes would be much easier, | |
| // but would not allow demonstrating how to manage ownership in thedestructor | |
| class TestHashTable : public HashTableBase<TestHashNode> { | |
| public: | |
| using BaseType = HashTableBase<TestHashNode>; | |
| using Node = BaseType::Node; | |
| static_assert(std::is_same<Node, TestHashNode>::value, | |
| "HashTableBase<>::Node is not defined properly!"); | |
| BaseType& asBaseType() { return *this; } | |
| const BaseType& asBaseType() const { return *this; } | |
| TestHashTable() = default; | |
| // IMPORTANT NOTE: Because the table contains bare owning pointers, we | |
| // have to use explicit copy and move constructor/operators for things | |
| // to work as expected. This is yet another reason why HashTableBase<> | |
| // should only be used with care (preferably with non-owning pointers). | |
| // | |
| TestHashTable(const TestHashTable& other) : BaseType(other) { | |
| // Only node (i.e. pointers) are copied by the base type. | |
| for (Node& node : ValidNodesRange()) { | |
| node.int_ptr = new Int(*node.int_ptr); | |
| } | |
| } | |
| TestHashTable& operator=(const TestHashTable& other) { | |
| if (this != &other) { | |
| this->~TestHashTable(); | |
| new (this) TestHashTable(other); | |
| } | |
| return *this; | |
| } | |
| TestHashTable(TestHashTable&& other) noexcept : BaseType(std::move(other)) {} | |
| TestHashTable& operator=(TestHashTable&& other) noexcept { | |
| if (this != &other) { | |
| this->~TestHashTable(); | |
| new (this) TestHashTable(std::move(other)); | |
| } | |
| return *this; | |
| } | |
| ~TestHashTable() { | |
| // Discard all valid Int pointers in the hash table. | |
| for (Node& node : ValidNodesRange()) | |
| delete node.int_ptr; | |
| } | |
| // Return true iff the table contains |x|. | |
| bool contains(int x) const { | |
| size_t hash = static_cast<size_t>(x); | |
| Node* node = NodeLookup( | |
| hash, [&](const Node* node) { return node->int_ptr->x() == x; }); | |
| return node->is_valid(); | |
| } | |
| // Try to insert |x| in the table. Returns true on success, or false if | |
| // the value was already in it. | |
| bool insert(int x) { | |
| size_t hash = static_cast<size_t>(x); | |
| Node* node = NodeLookup( | |
| hash, [&](const Node* node) { return node->int_ptr->x() == x; }); | |
| if (node->is_valid()) | |
| return false; | |
| bool was_tombstone = node->is_tombstone(); | |
| node->int_ptr = new Int(x); | |
| UpdateAfterInsert(was_tombstone); | |
| return true; | |
| } | |
| // Try to remove |x| from the table. Return true on success, or false | |
| // if the value was not in it. | |
| bool erase(int x) { | |
| size_t hash = static_cast<size_t>(x); | |
| Node* node = NodeLookup( | |
| hash, [&](const Node* node) { return node->int_ptr->x() == x; }); | |
| if (!node->is_valid()) | |
| return false; | |
| delete node->int_ptr; | |
| node->int_ptr = &TestHashNode::kTombstone; | |
| UpdateAfterRemoval(); | |
| return true; | |
| } | |
| // Remove all items | |
| void clear() { | |
| // Remove all pointed objects, since NodeClear() will not do it. | |
| for (Node& node : ValidNodesRange()) | |
| delete node.int_ptr; | |
| NodeClear(); | |
| } | |
| // Define const_iterator that point to the integer value instead of the node | |
| // of the Int instance to completely hide these from this class' API. | |
| struct const_iterator : public BaseType::NodeIterator { | |
| const int& operator*() const { | |
| return (this->BaseType::NodeIterator::operator*()).int_ptr->x(); | |
| } | |
| const int* operator->() const { return &(this->operator*()); } | |
| const_iterator& operator++() { | |
| BaseType::NodeIterator::operator++(); | |
| return *this; | |
| } | |
| const_iterator operator++(int) { | |
| const_iterator result = *this; | |
| BaseType::NodeIterator::operator++(); | |
| return result; | |
| } | |
| }; | |
| const_iterator begin() const { return {BaseType::NodeBegin()}; } | |
| const_iterator end() const { return {BaseType::NodeEnd()}; } | |
| }; | |
| TEST(HashTableBaseTest, Construction) { | |
| Int::ResetCounters(); | |
| { | |
| TestHashTable table; | |
| EXPECT_TRUE(table.empty()); | |
| EXPECT_EQ(table.size(), 0u); | |
| EXPECT_EQ(table.begin(), table.end()); | |
| } | |
| // No item was created or destroyed. | |
| EXPECT_EQ(Int::creation_counter, 0u); | |
| EXPECT_EQ(Int::destruction_counter, 0u); | |
| } | |
| TEST(HashTableBaseTest, InsertionsAndLookups) { | |
| Int::ResetCounters(); | |
| { | |
| TestHashTable table; | |
| table.insert(1); | |
| table.insert(5); | |
| table.insert(7); | |
| EXPECT_FALSE(table.empty()); | |
| EXPECT_EQ(table.size(), 3u); | |
| EXPECT_NE(table.begin(), table.end()); | |
| EXPECT_EQ(Int::creation_counter, 3u); | |
| EXPECT_EQ(Int::destruction_counter, 0u); | |
| EXPECT_FALSE(table.contains(0)); | |
| EXPECT_TRUE(table.contains(1)); | |
| EXPECT_FALSE(table.contains(2)); | |
| EXPECT_FALSE(table.contains(3)); | |
| EXPECT_TRUE(table.contains(5)); | |
| EXPECT_FALSE(table.contains(6)); | |
| EXPECT_TRUE(table.contains(7)); | |
| EXPECT_FALSE(table.contains(8)); | |
| } | |
| EXPECT_EQ(Int::creation_counter, 3u); | |
| EXPECT_EQ(Int::destruction_counter, 3u); | |
| } | |
| TEST(HashTableBaseTest, CopyAssignment) { | |
| Int::ResetCounters(); | |
| { | |
| TestHashTable table; | |
| table.insert(1); | |
| table.insert(5); | |
| table.insert(7); | |
| EXPECT_FALSE(table.empty()); | |
| EXPECT_EQ(table.size(), 3u); | |
| TestHashTable table2; | |
| EXPECT_TRUE(table2.empty()); | |
| table2 = table; | |
| EXPECT_FALSE(table2.empty()); | |
| EXPECT_EQ(table2.size(), 3u); | |
| EXPECT_FALSE(table.empty()); | |
| EXPECT_EQ(table.size(), 3u); | |
| EXPECT_EQ(Int::creation_counter, 6u); | |
| EXPECT_EQ(Int::destruction_counter, 0u); | |
| EXPECT_FALSE(table.contains(0)); | |
| EXPECT_TRUE(table.contains(1)); | |
| EXPECT_FALSE(table.contains(2)); | |
| EXPECT_FALSE(table.contains(3)); | |
| EXPECT_TRUE(table.contains(5)); | |
| EXPECT_FALSE(table.contains(6)); | |
| EXPECT_TRUE(table.contains(7)); | |
| EXPECT_FALSE(table.contains(8)); | |
| EXPECT_FALSE(table2.contains(0)); | |
| EXPECT_TRUE(table2.contains(1)); | |
| EXPECT_FALSE(table2.contains(2)); | |
| EXPECT_FALSE(table2.contains(3)); | |
| EXPECT_TRUE(table2.contains(5)); | |
| EXPECT_FALSE(table2.contains(6)); | |
| EXPECT_TRUE(table2.contains(7)); | |
| EXPECT_FALSE(table2.contains(8)); | |
| } | |
| EXPECT_EQ(Int::creation_counter, 6u); | |
| EXPECT_EQ(Int::destruction_counter, 6u); | |
| } | |
| TEST(HashTableBaseTest, MoveAssignment) { | |
| Int::ResetCounters(); | |
| { | |
| TestHashTable table; | |
| table.insert(1); | |
| table.insert(5); | |
| table.insert(7); | |
| EXPECT_FALSE(table.empty()); | |
| EXPECT_EQ(table.size(), 3u); | |
| TestHashTable table2; | |
| EXPECT_TRUE(table2.empty()); | |
| table2 = std::move(table); | |
| EXPECT_FALSE(table2.empty()); | |
| EXPECT_EQ(table2.size(), 3u); | |
| EXPECT_TRUE(table.empty()); | |
| EXPECT_EQ(table.size(), 0u); | |
| EXPECT_EQ(Int::creation_counter, 3u); | |
| EXPECT_EQ(Int::destruction_counter, 0u); | |
| EXPECT_FALSE(table2.contains(0)); | |
| EXPECT_TRUE(table2.contains(1)); | |
| EXPECT_FALSE(table2.contains(2)); | |
| EXPECT_FALSE(table2.contains(3)); | |
| EXPECT_TRUE(table2.contains(5)); | |
| EXPECT_FALSE(table2.contains(6)); | |
| EXPECT_TRUE(table2.contains(7)); | |
| EXPECT_FALSE(table2.contains(8)); | |
| } | |
| EXPECT_EQ(Int::creation_counter, 3u); | |
| EXPECT_EQ(Int::destruction_counter, 3u); | |
| } | |
| TEST(HashTableBaseTest, Clear) { | |
| Int::ResetCounters(); | |
| { | |
| TestHashTable table; | |
| table.insert(1); | |
| table.insert(5); | |
| table.insert(7); | |
| EXPECT_FALSE(table.empty()); | |
| EXPECT_EQ(table.size(), 3u); | |
| table.clear(); | |
| EXPECT_TRUE(table.empty()); | |
| EXPECT_EQ(Int::creation_counter, 3u); | |
| EXPECT_EQ(Int::destruction_counter, 3u); | |
| } | |
| EXPECT_EQ(Int::creation_counter, 3u); | |
| EXPECT_EQ(Int::destruction_counter, 3u); | |
| } | |
| TEST(HashTableBaseTest, Iteration) { | |
| TestHashTable table; | |
| table.insert(1); | |
| table.insert(5); | |
| table.insert(7); | |
| EXPECT_FALSE(table.empty()); | |
| EXPECT_EQ(table.size(), 3u); | |
| std::vector<int> values; | |
| for (const int& x : table) | |
| values.push_back(x); | |
| std::sort(values.begin(), values.end()); | |
| EXPECT_EQ(values.size(), 3u); | |
| EXPECT_EQ(values[0], 1); | |
| EXPECT_EQ(values[1], 5); | |
| EXPECT_EQ(values[2], 7); | |
| } | |
| TEST(HashTableBaseTest, Erase) { | |
| TestHashTable table; | |
| // Use sequention hashes to test that tombstone_count_ grows the hash table. | |
| for (int i = 0; i < 16; ++i) { | |
| table.insert(i); | |
| table.erase(i); | |
| EXPECT_EQ(table.size(), 0u); | |
| } | |
| } |