// Copyright 2018 The Abseil Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #ifndef ABSL_HASH_HASH_TESTING_H_ #define ABSL_HASH_HASH_TESTING_H_ #include #include #include #include #include "gmock/gmock.h" #include "gtest/gtest.h" #include "absl/hash/internal/spy_hash_state.h" #include "absl/meta/type_traits.h" #include "absl/strings/str_cat.h" #include "absl/types/variant.h" namespace absl { ABSL_NAMESPACE_BEGIN // Run the absl::Hash algorithm over all the elements passed in and verify that // their hash expansion is congruent with their `==` operator. // // It is used in conjunction with EXPECT_TRUE. Failures will output information // on what requirement failed and on which objects. // // Users should pass a collection of types as either an initializer list or a // container of cases. // // EXPECT_TRUE(absl::VerifyTypeImplementsAbslHashCorrectly( // {v1, v2, ..., vN})); // // std::vector cases; // // Fill cases... // EXPECT_TRUE(absl::VerifyTypeImplementsAbslHashCorrectly(cases)); // // Users can pass a variety of types for testing heterogeneous lookup with // `std::make_tuple`: // // EXPECT_TRUE(absl::VerifyTypeImplementsAbslHashCorrectly( // std::make_tuple(v1, v2, ..., vN))); // // // Ideally, the values passed should provide enough coverage of the `==` // operator and the AbslHashValue implementations. // For dynamically sized types, the empty state should usually be included in // the values. // // The function accepts an optional comparator function, in case that `==` is // not enough for the values provided. // // Usage: // // EXPECT_TRUE(absl::VerifyTypeImplementsAbslHashCorrectly( // std::make_tuple(v1, v2, ..., vN), MyCustomEq{})); // // It checks the following requirements: // 1. The expansion for a value is deterministic. // 2. For any two objects `a` and `b` in the sequence, if `a == b` evaluates // to true, then their hash expansion must be equal. // 3. If `a == b` evaluates to false their hash expansion must be unequal. // 4. If `a == b` evaluates to false neither hash expansion can be a // suffix of the other. // 5. AbslHashValue overloads should not be called by the user. They are only // meant to be called by the framework. Users should call H::combine() and // H::combine_contiguous(). // 6. No moved-from instance of the hash state is used in the implementation // of AbslHashValue. // // The values do not have to have the same type. This can be useful for // equivalent types that support heterogeneous lookup. // // A possible reason for breaking (2) is combining state in the hash expansion // that was not used in `==`. // For example: // // struct Bad2 { // int a, b; // template // friend H AbslHashValue(H state, Bad2 x) { // // Uses a and b. // return H::combine(std::move(state), x.a, x.b); // } // friend bool operator==(Bad2 x, Bad2 y) { // // Only uses a. // return x.a == y.a; // } // }; // // As for (3), breaking this usually means that there is state being passed to // the `==` operator that is not used in the hash expansion. // For example: // // struct Bad3 { // int a, b; // template // friend H AbslHashValue(H state, Bad3 x) { // // Only uses a. // return H::combine(std::move(state), x.a); // } // friend bool operator==(Bad3 x, Bad3 y) { // // Uses a and b. // return x.a == y.a && x.b == y.b; // } // }; // // Finally, a common way to break 4 is by combining dynamic ranges without // combining the size of the range. // For example: // // struct Bad4 { // int *p, size; // template // friend H AbslHashValue(H state, Bad4 x) { // return H::combine_contiguous(std::move(state), x.p, x.p + x.size); // } // friend bool operator==(Bad4 x, Bad4 y) { // // Compare two ranges for equality. C++14 code can instead use std::equal. // return absl::equal(x.p, x.p + x.size, y.p, y.p + y.size); // } // }; // // An easy solution to this is to combine the size after combining the range, // like so: // template // friend H AbslHashValue(H state, Bad4 x) { // return H::combine( // H::combine_contiguous(std::move(state), x.p, x.p + x.size), x.size); // } // template ABSL_MUST_USE_RESULT testing::AssertionResult VerifyTypeImplementsAbslHashCorrectly(const Container& values); template ABSL_MUST_USE_RESULT testing::AssertionResult VerifyTypeImplementsAbslHashCorrectly(const Container& values, Eq equals); template ABSL_MUST_USE_RESULT testing::AssertionResult VerifyTypeImplementsAbslHashCorrectly(std::initializer_list values); template ABSL_MUST_USE_RESULT testing::AssertionResult VerifyTypeImplementsAbslHashCorrectly(std::initializer_list values, Eq equals); namespace hash_internal { struct PrintVisitor { size_t index; template std::string operator()(const T* value) const { return absl::StrCat("#", index, "(", testing::PrintToString(*value), ")"); } }; template struct EqVisitor { Eq eq; template bool operator()(const T* t, const U* u) const { return eq(*t, *u); } }; struct ExpandVisitor { template SpyHashState operator()(const T* value) const { return SpyHashState::combine(SpyHashState(), *value); } }; template ABSL_MUST_USE_RESULT testing::AssertionResult VerifyTypeImplementsAbslHashCorrectly(const Container& values, Eq equals) { using V = typename Container::value_type; struct Info { const V& value; size_t index; std::string ToString() const { return absl::visit(PrintVisitor{index}, value); } SpyHashState expand() const { return absl::visit(ExpandVisitor{}, value); } }; using EqClass = std::vector; std::vector classes; // Gather the values in equivalence classes. size_t i = 0; for (const auto& value : values) { EqClass* c = nullptr; for (auto& eqclass : classes) { if (absl::visit(EqVisitor{equals}, value, eqclass[0].value)) { c = &eqclass; break; } } if (c == nullptr) { classes.emplace_back(); c = &classes.back(); } c->push_back({value, i}); ++i; // Verify potential errors captured by SpyHashState. if (auto error = c->back().expand().error()) { return testing::AssertionFailure() << *error; } } if (classes.size() < 2) { return testing::AssertionFailure() << "At least two equivalence classes are expected."; } // We assume that equality is correctly implemented. // Now we verify that AbslHashValue is also correctly implemented. for (const auto& c : classes) { // All elements of the equivalence class must have the same hash // expansion. const SpyHashState expected = c[0].expand(); for (const Info& v : c) { if (v.expand() != v.expand()) { return testing::AssertionFailure() << "Hash expansion for " << v.ToString() << " is non-deterministic."; } if (v.expand() != expected) { return testing::AssertionFailure() << "Values " << c[0].ToString() << " and " << v.ToString() << " evaluate as equal but have an unequal hash expansion."; } } // Elements from other classes must have different hash expansion. for (const auto& c2 : classes) { if (&c == &c2) continue; const SpyHashState c2_hash = c2[0].expand(); switch (SpyHashState::Compare(expected, c2_hash)) { case SpyHashState::CompareResult::kEqual: return testing::AssertionFailure() << "Values " << c[0].ToString() << " and " << c2[0].ToString() << " evaluate as unequal but have an equal hash expansion."; case SpyHashState::CompareResult::kBSuffixA: return testing::AssertionFailure() << "Hash expansion of " << c2[0].ToString() << " is a suffix of the hash expansion of " << c[0].ToString() << "."; case SpyHashState::CompareResult::kASuffixB: return testing::AssertionFailure() << "Hash expansion of " << c[0].ToString() << " is a suffix of the hash expansion of " << c2[0].ToString() << "."; case SpyHashState::CompareResult::kUnequal: break; } } } return testing::AssertionSuccess(); } template struct TypeSet { template ...>::value> struct Insert { using type = TypeSet; }; template struct Insert { using type = TypeSet; }; template