1 /*
2  * Copyright (c) Facebook, Inc. and its affiliates.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 #pragma once
18 
19 #include <cstring>
20 #include <string_view>
21 #include <type_traits>
22 #include <utility>
23 #include <fatal/type/enum.h>
24 #include <fatal/type/variant_traits.h>
25 #include <folly/Overload.h>
26 #include <folly/Traits.h>
27 #include <folly/lang/Pretty.h>
28 
29 namespace apache::thrift {
30 
31 namespace detail {
32 // Forward declaration from header `thrift/lib/cpp2/gen/module_types_h.h`
33 // which is supposed to only be included by generated files.
34 template <typename Tag>
35 struct invoke_reffer;
36 } // namespace detail
37 
38 namespace test::detail {
39 
40 template <typename FieldTag>
getFieldName()41 std::string_view getFieldName() {
42   // Thrift generates the tags in the 'apache::thrift::tag' namespace,
43   // so we can just skip it to provide a better name for the users.
44   // -1 because we don't want to count the terminating \0 character.
45   constexpr std::size_t offset = sizeof("apache::thrift::tag::") - 1;
46   // it's safe to return the string_view because pretty_name() returns a
47   // statically allocated char *
48   std::string_view name{folly::pretty_name<FieldTag>()};
49   if (name.size() > offset) {
50     name.remove_prefix(offset);
51   }
52   return name;
53 }
54 
55 template <typename FieldTag, typename InnerMatcher>
56 class ThriftFieldMatcher {
57  public:
ThriftFieldMatcher(InnerMatcher matcher)58   explicit ThriftFieldMatcher(InnerMatcher matcher)
59       : matcher_(std::move(matcher)) {}
60 
61   template <
62       typename ThriftStruct,
63       typename = std::enable_if_t<
64           is_thrift_exception_v<folly::remove_cvref_t<ThriftStruct>> ||
65           is_thrift_struct_v<folly::remove_cvref_t<ThriftStruct>>>>
66   operator testing::Matcher<ThriftStruct>() const {
67     return testing::Matcher<ThriftStruct>(
68         new Impl<const ThriftStruct&>(matcher_));
69   }
70 
71  private:
72   using Accessor = thrift::detail::invoke_reffer<FieldTag>;
73   // For field_ref, we want to forward the value pointed to the matcher
74   // directly (it's fully transparent).
75   // For optional_field_ref, we don't want to do it becase the mental model
76   // is as if it was an std::optional<field_ref>. If we were to forward the
77   // value, it would be impossible to check if it is empty.
78   // Instead, we send the optional_field_ref to the matcher. The user can
79   // provide a testing::Optional matcher to match the value contained in it.
80 
81   template <typename ThriftStruct>
82   class Impl : public testing::MatcherInterface<ThriftStruct> {
83    private:
84     using FieldRef = decltype(Accessor{}(std::declval<ThriftStruct>()));
85     using FieldReferenceType = typename FieldRef::reference_type;
86     inline static constexpr bool IsOptionalFieldRef =
87         std::is_same_v<optional_field_ref<FieldReferenceType>, FieldRef>;
88     // See above on why we do this switch
89     using MatchedValue = std::conditional_t<
90         IsOptionalFieldRef,
91         optional_field_ref<FieldReferenceType>,
92         FieldReferenceType>;
93 
94    public:
95     template <typename MatcherOrPolymorphicMatcher>
Impl(const MatcherOrPolymorphicMatcher & matcher)96     explicit Impl(const MatcherOrPolymorphicMatcher& matcher)
97         : concrete_matcher_(testing::MatcherCast<MatchedValue>(matcher)) {}
98 
DescribeTo(::std::ostream * os)99     void DescribeTo(::std::ostream* os) const override {
100       *os << "is an object whose field `" << getFieldName<FieldTag>() << "` ";
101       concrete_matcher_.DescribeTo(os);
102     }
103 
DescribeNegationTo(::std::ostream * os)104     void DescribeNegationTo(::std::ostream* os) const override {
105       *os << "is an object whose field `" << getFieldName<FieldTag>() << "` ";
106       concrete_matcher_.DescribeNegationTo(os);
107     }
108 
MatchAndExplain(ThriftStruct obj,testing::MatchResultListener * listener)109     bool MatchAndExplain(
110         ThriftStruct obj,
111         testing::MatchResultListener* listener) const override {
112       *listener << "whose field `" << getFieldName<FieldTag>() << "` is ";
113       auto val = Accessor{}(obj);
114       // Using gtest internals??
115       // This function does some printing and formatting.
116       // Here we want the same behaviour as other matchers, so
117       // this allows us to save code and stay consistent.
118       // If in later gtest versions this breaks, we can either
119       //  1. use the new functionality looking at how
120       //     testing::FieldMatcher is implemented, or
121       //  2. copy the old implementation here.
122       using testing::internal::MatchPrintAndExplain;
123       if constexpr (IsOptionalFieldRef) {
124         return MatchPrintAndExplain(val, concrete_matcher_, listener);
125       } else {
126         return MatchPrintAndExplain(*val, concrete_matcher_, listener);
127       }
128     }
129 
130    private:
131     const testing::Matcher<MatchedValue> concrete_matcher_;
132   }; // class Impl
133 
134   const InnerMatcher matcher_;
135 };
136 
137 template <typename FieldTag, typename InnerMatcher>
138 class IsThriftUnionWithMatcher {
139  public:
IsThriftUnionWithMatcher(InnerMatcher matcher)140   explicit IsThriftUnionWithMatcher(InnerMatcher matcher)
141       : matcher_(std::move(matcher)) {}
142 
143   template <
144       typename ThriftUnion,
145       typename = std::enable_if_t<
146           is_thrift_union_v<folly::remove_cvref_t<ThriftUnion>>>>
147   operator testing::Matcher<ThriftUnion>() const {
148     static_assert(
149         SupportsReflection<folly::remove_cvref_t<ThriftUnion>>,
150         "Include the _fatal_union.h header for the Thrift file defining this union");
151     return testing::Matcher<ThriftUnion>(
152         new Impl<const ThriftUnion&>(matcher_));
153   }
154 
155  private:
156   // Needed in order to provide a good error message in the static assert
157   template <typename T>
158   constexpr static inline bool SupportsReflection =
159       fatal::has_variant_traits<T>::value;
160 
161   using Accessor = thrift::detail::invoke_reffer<FieldTag>;
162 
163   template <typename ThriftUnion>
164   class Impl : public testing::MatcherInterface<ThriftUnion> {
165    private:
166     using FieldRef = decltype(Accessor{}(std::declval<ThriftUnion>()));
167     // Unions do not support optional fields, so this is always a concrete
168     // value (no optional_ref)
169     using FieldReferenceType = typename FieldRef::reference_type;
170 
171    public:
172     template <typename MatcherOrPolymorphicMatcher>
Impl(const MatcherOrPolymorphicMatcher & matcher)173     explicit Impl(const MatcherOrPolymorphicMatcher& matcher)
174         : concrete_matcher_(testing::MatcherCast<FieldReferenceType>(matcher)) {
175     }
176 
DescribeTo(::std::ostream * os)177     void DescribeTo(::std::ostream* os) const override {
178       *os << "is an union with `" << getFieldName<FieldTag>()
179           << "` active, which ";
180       concrete_matcher_.DescribeTo(os);
181     }
182 
DescribeNegationTo(::std::ostream * os)183     void DescribeNegationTo(::std::ostream* os) const override {
184       *os << "is an union without `" << getFieldName<FieldTag>()
185           << "` active, or the active value ";
186       concrete_matcher_.DescribeNegationTo(os);
187     }
188 
MatchAndExplain(ThriftUnion obj,testing::MatchResultListener * listener)189     bool MatchAndExplain(
190         ThriftUnion obj,
191         testing::MatchResultListener* listener) const override {
192       std::optional<bool> matches;
193       using descriptors = typename fatal::variant_traits<
194           folly::remove_cvref_t<ThriftUnion>>::descriptors;
195       fatal::scalar_search<descriptors, fatal::get_type::id>(
196           obj.getType(), [&](auto indexed) {
197             using descriptor = decltype(fatal::tag_type(indexed));
198             using tag = typename descriptor::metadata::tag;
199             auto active = fatal::enum_to_string(obj.getType(), "unknown");
200             if constexpr (std::is_same_v<FieldTag, tag>) {
201               *listener << "whose active member `" << active << "` is ";
202               matches = testing::internal::MatchPrintAndExplain(
203                   descriptor::get(obj), concrete_matcher_, listener);
204             } else {
205               *listener << "whose active member `" << active << "` is "
206                         << testing::PrintToString(descriptor::get(obj));
207               matches = false;
208             }
209           });
210       if (matches) {
211         return *matches;
212       }
213       *listener << "which is unset";
214       return false;
215     }
216 
217    private:
218     const testing::Matcher<FieldReferenceType> concrete_matcher_;
219   }; // class Impl
220 
221   const InnerMatcher matcher_;
222 };
223 
224 } // namespace test::detail
225 } // namespace apache::thrift
226