Document two surprises in DoAllAction::NonFinalType.

PiperOrigin-RevId: 441589196
Change-Id: Ic3e483ca70d72261046bad464d817f9dfd4bec65
This commit is contained in:
Aaron Jacobs 2022-04-13 15:03:55 -07:00 committed by Copybara-Service
parent 733f875989
commit 80600e56cc
2 changed files with 128 additions and 0 deletions

View File

@ -1295,6 +1295,51 @@ struct WithArgsAction {
template <typename... Actions>
struct DoAllAction {
private:
// The type of reference that should be provided to an initial action for a
// mocked function parameter of type T.
//
// There are two quirks here:
//
// * Unlike most forwarding functions, we pass scalars through by value.
// This isn't strictly necessary because an lvalue reference would work
// fine too and be consistent with other non-reference types, but it's
// perhaps less surprising.
//
// For example if the mocked function has signature void(int), then it
// might seem surprising for the user's initial action to need to be
// convertible to Action<void(const int&)>. This is perhaps less
// surprising for a non-scalar type where there may be a performance
// impact, or it might even be impossible, to pass by value.
//
// * More surprisingly, `const T&` is often not a const reference type.
// By the reference collapsing rules in C++17 [dcl.ref]/6, if T refers to
// U& or U&& for some non-scalar type U, then NonFinalType<T> is U&. In
// other words, we may hand over a non-const reference.
//
// So for example, given some non-scalar type Obj we have the following
// mappings:
//
// T NonFinalType<T>
// ------- ---------------
// Obj const Obj&
// Obj& Obj&
// Obj&& Obj&
// const Obj const Obj&
// const Obj& const Obj&
// const Obj&& const Obj&
//
// In other words, the initial actions get a mutable view of an non-scalar
// argument if and only if the mock function itself accepts a non-const
// reference type. They are never given an rvalue reference to an
// non-scalar type.
//
// This situation makes sense if you imagine use with a matcher that is
// designed to write through a reference. For example, if the caller wants
// to fill in a reference argument and then return a canned value:
//
// EXPECT_CALL(mock, Call)
// .WillOnce(DoAll(SetArgReferee<0>(17), Return(19)));
//
template <typename T>
using NonFinalType =
typename std::conditional<std::is_scalar<T>::value, T, const T&>::type;

View File

@ -1192,6 +1192,89 @@ TEST(AssignTest, CompatibleTypes) {
EXPECT_DOUBLE_EQ(5, x);
}
// DoAll should never provide rvalue references to the initial actions. If the
// mock action itself accepts an rvalue reference or a non-scalar object by
// value then the final action should receive an rvalue reference, but initial
// actions should receive only lvalue references.
TEST(DoAll, ProvidesLvalueReferencesToInitialActions) {
struct Obj {};
// Mock action accepts by value: the initial action should be fed a const
// lvalue reference, and the final action an rvalue reference.
{
struct InitialAction {
void operator()(Obj&) const { FAIL() << "Unexpected call"; }
void operator()(const Obj&) const {}
void operator()(Obj&&) const { FAIL() << "Unexpected call"; }
void operator()(const Obj&&) const { FAIL() << "Unexpected call"; }
};
MockFunction<void(Obj)> mock;
EXPECT_CALL(mock, Call)
.WillOnce(DoAll(InitialAction{}, InitialAction{}, [](Obj&&) {}))
.WillRepeatedly(DoAll(InitialAction{}, InitialAction{}, [](Obj&&) {}));
mock.AsStdFunction()(Obj{});
mock.AsStdFunction()(Obj{});
}
// Mock action accepts by const lvalue reference: both actions should receive
// a const lvalue reference.
{
struct InitialAction {
void operator()(Obj&) const { FAIL() << "Unexpected call"; }
void operator()(const Obj&) const {}
void operator()(Obj&&) const { FAIL() << "Unexpected call"; }
void operator()(const Obj&&) const { FAIL() << "Unexpected call"; }
};
MockFunction<void(const Obj&)> mock;
EXPECT_CALL(mock, Call)
.WillOnce(DoAll(InitialAction{}, InitialAction{}, [](const Obj&) {}))
.WillRepeatedly(
DoAll(InitialAction{}, InitialAction{}, [](const Obj&) {}));
mock.AsStdFunction()(Obj{});
mock.AsStdFunction()(Obj{});
}
// Mock action accepts by non-const lvalue reference: both actions should get
// a non-const lvalue reference if they want them.
{
struct InitialAction {
void operator()(Obj&) const {}
void operator()(Obj&&) const { FAIL() << "Unexpected call"; }
};
MockFunction<void(Obj&)> mock;
EXPECT_CALL(mock, Call)
.WillOnce(DoAll(InitialAction{}, InitialAction{}, [](Obj&) {}))
.WillRepeatedly(DoAll(InitialAction{}, InitialAction{}, [](Obj&) {}));
Obj obj;
mock.AsStdFunction()(obj);
mock.AsStdFunction()(obj);
}
// Mock action accepts by rvalue reference: the initial actions should receive
// a non-const lvalue reference if it wants it, and the final action an rvalue
// reference.
{
struct InitialAction {
void operator()(Obj&) const {}
void operator()(Obj&&) const { FAIL() << "Unexpected call"; }
};
MockFunction<void(Obj &&)> mock;
EXPECT_CALL(mock, Call)
.WillOnce(DoAll(InitialAction{}, InitialAction{}, [](Obj&&) {}))
.WillRepeatedly(DoAll(InitialAction{}, InitialAction{}, [](Obj&&) {}));
mock.AsStdFunction()(Obj{});
mock.AsStdFunction()(Obj{});
}
}
// Tests using WithArgs and with an action that takes 1 argument.
TEST(WithArgsTest, OneArg) {
Action<bool(double x, int n)> a = WithArgs<1>(Invoke(Unary)); // NOLINT