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