| // 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 <cstddef> | |
| #include <initializer_list> | |
| #include <string> | |
| #include <tuple> | |
| #include <type_traits> | |
| #include <utility> | |
| #include <variant> | |
| #include <vector> | |
| #include "gmock/gmock.h" | |
| #include "gtest/gtest.h" | |
| #include "absl/base/config.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<MyType> 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 <typename H> | |
| // 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 <typename H> | |
| // 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 <typename H> | |
| // 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 std::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 <typename H> | |
| // 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 <int&... ExplicitBarrier, typename Container> | |
| testing::AssertionResult VerifyTypeImplementsAbslHashCorrectly( | |
| const Container& values); | |
| template <int&... ExplicitBarrier, typename Container, typename Eq> | |
| testing::AssertionResult VerifyTypeImplementsAbslHashCorrectly( | |
| const Container& values, Eq equals); | |
| template <int&..., typename T> | |
| testing::AssertionResult VerifyTypeImplementsAbslHashCorrectly( | |
| std::initializer_list<T> values); | |
| template <int&..., typename T, typename Eq> | |
| testing::AssertionResult VerifyTypeImplementsAbslHashCorrectly( | |
| std::initializer_list<T> values, Eq equals); | |
| namespace hash_internal { | |
| struct PrintVisitor { | |
| size_t index; | |
| template <typename T> | |
| std::string operator()(const T* value) const { | |
| return absl::StrCat("#", index, "(", testing::PrintToString(*value), ")"); | |
| } | |
| }; | |
| template <typename Eq> | |
| struct EqVisitor { | |
| Eq eq; | |
| template <typename T, typename U> | |
| bool operator()(const T* t, const U* u) const { | |
| return eq(*t, *u); | |
| } | |
| }; | |
| struct ExpandVisitor { | |
| template <typename T> | |
| SpyHashState operator()(const T* value) const { | |
| return SpyHashState::combine(SpyHashState(), *value); | |
| } | |
| }; | |
| template <typename Container, typename Eq> | |
| 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 std::visit(PrintVisitor{index}, value); | |
| } | |
| SpyHashState expand() const { return std::visit(ExpandVisitor{}, value); } | |
| }; | |
| using EqClass = std::vector<Info>; | |
| std::vector<EqClass> classes; | |
| // Gather the values in equivalence classes. | |
| size_t i = 0; | |
| for (const auto& value : values) { | |
| EqClass* c = nullptr; | |
| for (auto& eqclass : classes) { | |
| if (std::visit(EqVisitor<Eq>{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 unequal hash expansions (" | |
| << expected << " vs. " << v.expand() << ")."; | |
| } | |
| } | |
| // 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:" | |
| << c2_hash << "."; | |
| case SpyHashState::CompareResult::kBSuffixA: | |
| return testing::AssertionFailure() | |
| << "Hash expansion of " << c2[0].ToString() << ";" << c2_hash | |
| << " is a suffix of the hash expansion of " << c[0].ToString() | |
| << ";" << expected << "."; | |
| case SpyHashState::CompareResult::kASuffixB: | |
| return testing::AssertionFailure() | |
| << "Hash expansion of " << c[0].ToString() << ";" | |
| << expected << " is a suffix of the hash expansion of " | |
| << c2[0].ToString() << ";" << c2_hash << "."; | |
| case SpyHashState::CompareResult::kUnequal: | |
| break; | |
| } | |
| } | |
| } | |
| return testing::AssertionSuccess(); | |
| } | |
| template <typename... T> | |
| struct TypeSet { | |
| template <typename U, bool = std::disjunction_v<std::is_same<T, U>...>> | |
| struct Insert { | |
| using type = TypeSet<U, T...>; | |
| }; | |
| template <typename U> | |
| struct Insert<U, true> { | |
| using type = TypeSet; | |
| }; | |
| template <template <typename...> class C> | |
| using apply = C<T...>; | |
| }; | |
| template <typename... T> | |
| struct MakeTypeSet : TypeSet<> {}; | |
| template <typename T, typename... Ts> | |
| struct MakeTypeSet<T, Ts...> : MakeTypeSet<Ts...>::template Insert<T>::type {}; | |
| template <typename... T> | |
| using VariantForTypes = typename MakeTypeSet< | |
| const std::decay_t<T>*...>::template apply<std::variant>; | |
| template <typename Container> | |
| struct ContainerAsVector { | |
| using V = std::variant<const typename Container::value_type*>; | |
| using Out = std::vector<V>; | |
| static Out Do(const Container& values) { | |
| Out out; | |
| for (const auto& v : values) out.push_back(&v); | |
| return out; | |
| } | |
| }; | |
| template <typename... T> | |
| struct ContainerAsVector<std::tuple<T...>> { | |
| using V = VariantForTypes<T...>; | |
| using Out = std::vector<V>; | |
| template <size_t... I> | |
| static Out DoImpl(const std::tuple<T...>& tuple, std::index_sequence<I...>) { | |
| return Out{&std::get<I>(tuple)...}; | |
| } | |
| static Out Do(const std::tuple<T...>& values) { | |
| return DoImpl(values, std::index_sequence_for<T...>()); | |
| } | |
| }; | |
| template <> | |
| struct ContainerAsVector<std::tuple<>> { | |
| static std::vector<VariantForTypes<int>> Do(std::tuple<>) { return {}; } | |
| }; | |
| struct DefaultEquals { | |
| template <typename T, typename U> | |
| bool operator()(const T& t, const U& u) const { | |
| return t == u; | |
| } | |
| }; | |
| } // namespace hash_internal | |
| template <int&..., typename Container> | |
| testing::AssertionResult VerifyTypeImplementsAbslHashCorrectly( | |
| const Container& values) { | |
| return hash_internal::VerifyTypeImplementsAbslHashCorrectly( | |
| hash_internal::ContainerAsVector<Container>::Do(values), | |
| hash_internal::DefaultEquals{}); | |
| } | |
| template <int&..., typename Container, typename Eq> | |
| testing::AssertionResult VerifyTypeImplementsAbslHashCorrectly( | |
| const Container& values, Eq equals) { | |
| return hash_internal::VerifyTypeImplementsAbslHashCorrectly( | |
| hash_internal::ContainerAsVector<Container>::Do(values), equals); | |
| } | |
| template <int&..., typename T> | |
| testing::AssertionResult VerifyTypeImplementsAbslHashCorrectly( | |
| std::initializer_list<T> values) { | |
| return hash_internal::VerifyTypeImplementsAbslHashCorrectly( | |
| hash_internal::ContainerAsVector<std::initializer_list<T>>::Do(values), | |
| hash_internal::DefaultEquals{}); | |
| } | |
| template <int&..., typename T, typename Eq> | |
| testing::AssertionResult VerifyTypeImplementsAbslHashCorrectly( | |
| std::initializer_list<T> values, Eq equals) { | |
| return hash_internal::VerifyTypeImplementsAbslHashCorrectly( | |
| hash_internal::ContainerAsVector<std::initializer_list<T>>::Do(values), | |
| equals); | |
| } | |
| ABSL_NAMESPACE_END | |
| } // namespace absl | |
| #endif // ABSL_HASH_HASH_TESTING_H_ |