aboutsummaryrefslogtreecommitdiff
path: root/fcp/testing/testing.h
blob: 2fa855d3b19000954b2fe3bd8a73edcb3ce0d226 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
/*
 * Copyright 2017 Google LLC
 *
 * 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
 *
 *      http://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 FCP_TESTING_TESTING_H_
#define FCP_TESTING_TESTING_H_

#include <iostream>
#include <memory>
#include <string>
#include <type_traits>

#include "google/protobuf/util/message_differencer.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "absl/status/status.h"
#include "absl/strings/string_view.h"
#include "fcp/base/error.h"
#include "fcp/base/monitoring.h"
#include "fcp/base/platform.h"
#include "fcp/base/result.h"
#include "fcp/base/source_location.h"
#include "fcp/testing/result_matchers.h"

#include "fcp/testing/parse_text_proto.h"

// This file defines platform dependent utilities for testing,
// based on the public version of googletest.

namespace fcp {

// A macro for use inside a GTest test that executes the provided code as a
// function returning a Result and asserts that the return value is not an
// Error.
//
// The code provided to the macro will be much like the code one would write in
// the body of a regular test, with the differences being that the code must
// return Result<Unit>, and only EXPECT_* statements are allowed, not ASSERT_*.
//
// This makes it possible to greatly simplify the test body by using FCP_TRY(),
// rather than having to check in the test body that every return value of
// Result type is not an error.
//
// Example:
//
//   TEST(FooTest, GetFoo) {
//     FCP_EXPECT_NO_ERROR(
//       Foo foo = FCP_TRY(GetFoo());
//       EXPECT_TRUE(foo.HasBar());
//       return Unit{};
//     );
//   }
#define FCP_EXPECT_NO_ERROR(test_contents)           \
  auto test_fn = []() -> Result<Unit> test_contents; \
  ASSERT_THAT(test_fn(), testing::Not(IsError()))

// Convenience macros for `EXPECT_THAT(s, IsOk())`, where `s` is either
// a `Status` or a `StatusOr<T>`.
// Old versions of the protobuf library define EXPECT_OK as well, so we only
// conditionally define our version.
#if !defined(EXPECT_OK)
#define EXPECT_OK(result) EXPECT_THAT(result, fcp::IsOk())
#endif
#define ASSERT_OK(result) ASSERT_THAT(result, fcp::IsOk())

/** Returns the current test's name. */
std::string TestName();

/**
 * Gets path to a test data file based on a path relative to project root.
 */
std::string GetTestDataPath(absl::string_view relative_path);

/**
 * Creates a temporary file name with given suffix unique for the running test.
 */
std::string TemporaryTestFile(absl::string_view suffix);

/**
 * Verifies a provided content against an expected stored in a baseline file.
 * Returns an empty string if both are identical, otherwise a diagnostic
 * message for error reports.
 *
 * A return status of not ok indicates an operational error which made the
 * comparison impossible.
 *
 * The baseline file name must be provided relative to the project root.
 */
StatusOr<std::string> VerifyAgainstBaseline(absl::string_view baseline_file,
                                            absl::string_view content);

/**
 * Polymorphic matchers for Status or StatusOr on status code.
 */
template <typename T>
bool IsCode(StatusOr<T> const& x, StatusCode code) {
  return x.status().code() == code;
}
inline bool IsCode(Status const& x, StatusCode code) {
  return x.code() == code;
}

template <typename T>
class StatusMatcherImpl : public ::testing::MatcherInterface<T> {
 public:
  explicit StatusMatcherImpl(StatusCode code) : code_(code) {}
  void DescribeTo(::std::ostream* os) const override {
    *os << "is " << absl::StatusCodeToString(code_);
  }
  void DescribeNegationTo(::std::ostream* os) const override {
    *os << "is not " << absl::StatusCodeToString(code_);
  }
  bool MatchAndExplain(
      T x, ::testing::MatchResultListener* listener) const override {
    return IsCode(x, code_);
  }

 private:
  StatusCode code_;
};

class StatusMatcher {
 public:
  explicit StatusMatcher(StatusCode code) : code_(code) {}

  template <typename T>
  operator testing::Matcher<T>() const {  // NOLINT
    return ::testing::MakeMatcher(new StatusMatcherImpl<T>(code_));
  }

 private:
  StatusCode code_;
};

StatusMatcher IsCode(StatusCode code);
StatusMatcher IsOk();

template <typename T>
class ProtoMatcherImpl : public ::testing::MatcherInterface<T> {
 public:
  explicit ProtoMatcherImpl(const google::protobuf::Message& arg)
      : arg_(CloneMessage(arg)) {}

  explicit ProtoMatcherImpl(const std::string& arg) : arg_(ParseMessage(arg)) {}

  void DescribeTo(::std::ostream* os) const override {
    *os << "is " << arg_->DebugString();
  }
  void DescribeNegationTo(::std::ostream* os) const override {
    *os << "is not " << arg_->DebugString();
  }
  bool MatchAndExplain(
      T x, ::testing::MatchResultListener* listener) const override {
    if (x.GetDescriptor()->full_name() != arg_->GetDescriptor()->full_name()) {
      *listener << "Argument proto is of type "
                << arg_->GetDescriptor()->full_name()
                << " but expected proto of type "
                << x.GetDescriptor()->full_name();
      return false;
    }

    google::protobuf::util::MessageDifferencer differencer;
    std::string reported_differences;
    differencer.ReportDifferencesToString(&reported_differences);
    if (!differencer.Compare(*arg_, x)) {
      *listener << reported_differences;
      return false;
    }
    return true;
  }

 private:
  static std::unique_ptr<google::protobuf::Message> CloneMessage(
      const google::protobuf::Message& message) {
    std::unique_ptr<google::protobuf::Message> copy_of_message =
        absl::WrapUnique(message.New());
    copy_of_message->CopyFrom(message);
    return copy_of_message;
  }

  static std::unique_ptr<google::protobuf::Message> ParseMessage(
      const std::string& proto_text) {
    using V = std::remove_cv_t<std::remove_reference_t<T>>;
    std::unique_ptr<V> message = std::make_unique<V>();
    *message = PARSE_TEXT_PROTO(proto_text);
    return message;
  }

  std::unique_ptr<google::protobuf::Message> arg_;
};

template <typename T>
class ProtoMatcher {
 public:
  explicit ProtoMatcher(const T& arg) : arg_(arg) {}

  template <typename U>
  operator testing::Matcher<U>() const {  // NOLINT
    using V = std::remove_cv_t<std::remove_reference_t<U>>;
    static_assert(std::is_base_of<google::protobuf::Message, V>::value &&
                  !std::is_same<google::protobuf::Message, V>::value);
    return ::testing::MakeMatcher(new ProtoMatcherImpl<U>(arg_));
  }

 private:
  T arg_;
};

// Proto matcher that takes another proto message reference as an argument.
template <class T,
          typename std::enable_if<std::is_base_of<google::protobuf::Message, T>::value &&
                                      !std::is_same<google::protobuf::Message, T>::value,
                                  int>::type = 0>
inline ProtoMatcher<T> EqualsProto(const T& arg) {
  return ProtoMatcher<T>(arg);
}

// Proto matcher that takes a text proto as an argument.
inline ProtoMatcher<std::string> EqualsProto(const std::string& arg) {
  return ProtoMatcher<std::string>(arg);
}

// Utility function which creates and traces an instance of test error
Error TraceTestError(SourceLocation loc = SourceLocation::current());

}  // namespace fcp

#endif  // FCP_TESTING_TESTING_H_