0
0
mirror of https://github.com/google/googletest.git synced 2025-03-20 10:53:47 +00:00

Add a testing::ConvertGenerator overload that accepts a converting functor. This allows the use of classes that do not have a converting ctor to the desired type.

PiperOrigin-RevId: 733383835
Change-Id: I6fbf79db0509b3d4fe8305a83ed47fceaa820e47
This commit is contained in:
Abseil Team 2025-03-04 10:39:43 -08:00 committed by Copybara-Service
parent 24a9e940d4
commit e88cb95b92
4 changed files with 278 additions and 20 deletions

View File

@ -110,7 +110,7 @@ namespace:
| `ValuesIn(container)` or `ValuesIn(begin,end)` | Yields values from a C-style array, an STL-style container, or an iterator range `[begin, end)`. |
| `Bool()` | Yields sequence `{false, true}`. |
| `Combine(g1, g2, ..., gN)` | Yields as `std::tuple` *n*-tuples all combinations (Cartesian product) of the values generated by the given *n* generators `g1`, `g2`, ..., `gN`. |
| `ConvertGenerator<T>(g)` | Yields values generated by generator `g`, `static_cast` to `T`. |
| `ConvertGenerator<T>(g)` or `ConvertGenerator(g, func)` | Yields values generated by generator `g`, `static_cast` from `T`. (Note: `T` might not be what you expect. See [*Using ConvertGenerator*](#using-convertgenerator) below.) The second overload uses `func` to perform the conversion. |
The optional last argument *`name_generator`* is a function or functor that
generates custom test name suffixes based on the test parameters. The function
@ -137,6 +137,103 @@ For more information, see
See also
[`GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST`](#GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST).
###### Using `ConvertGenerator`
The functions listed in the table above appear to return generators that create
values of the desired types, but this is not generally the case. Rather, they
typically return factory objects that convert to the the desired generators.
This affords some flexibility in allowing you to specify values of types that
are different from, yet implicitly convertible to, the actual parameter type
required by your fixture class.
For example, you can do the following with a fixture that requires an `int`
parameter:
```cpp
INSTANTIATE_TEST_SUITE_P(MyInstantiation, MyTestSuite,
testing::Values(1, 1.2)); // Yes, Values() supports heterogeneous argument types.
```
It might seem obvious that `1.2` &mdash; a `double` &mdash; will be converted to
an `int` but in actuality it requires some template gymnastics involving the
indirection described in the previous paragraph.
What if your parameter type is not implicitly convertible from the generated
type but is *explicitly* convertible? There will be no automatic conversion, but
you can force it by applying `ConvertGenerator<T>`. The compiler can
automatically deduce the target type (your fixture's parameter type), but
because of the aforementioned indirection it cannot decide what the generated
type should be. You need to tell it, by providing the type `T` explicitly. Thus
`T` should not be your fixture's parameter type, but rather an intermediate type
that is supported by the factory object, and which can be `static_cast` to the
fixture's parameter type:
```cpp
// The fixture's parameter type.
class MyParam {
public:
// Explicit converting ctor.
explicit MyParam(const std::tuple<int, bool>& t);
...
};
INSTANTIATE_TEST_SUITE_P(MyInstantiation, MyTestSuite,
ConvertGenerator<std::tuple<int, bool>>(Combine(Values(0.1, 1.2), Bool())));
```
In this example `Combine` supports the generation of `std::tuple<int, bool>>`
objects (even though the provided values for the first tuple element are
`double`s) and those `tuple`s get converted into `MyParam` objects by virtue of
the call to `ConvertGenerator`.
For parameter types that are not convertible from the generated types you can
provide a callable that does the conversion. The callable accepts an object of
the generated type and returns an object of the fixture's parameter type. The
generated type can often be deduced by the compiler from the callable's call
signature so you do not usually need specify it explicitly (but see a caveat
below).
```cpp
// The fixture's parameter type.
class MyParam {
public:
MyParam(int, bool);
...
};
INSTANTIATE_TEST_SUITE_P(MyInstantiation, MyTestSuite,
ConvertGenerator(Combine(Values(1, 1.2), Bool()),
[](const std::tuple<int i, bool>& t){
const auto [i, b] = t;
return MyParam(i, b);
}));
```
The callable may be anything that can be used to initialize a `std::function`
with the appropriate call signature. Note the callable's return object gets
`static_cast` to the fixture's parameter type, so it does not have to be of that
exact type, only convertible to it.
**Caveat:** Consider the following example.
```cpp
INSTANTIATE_TEST_SUITE_P(MyInstantiation, MyTestSuite,
ConvertGenerator(Values(std::string("s")), [](std::string_view s) { ... }));
```
The `string` argument gets copied into the factory object returned by `Values`.
Then, because the generated type deduced from the lambda is `string_view`, the
factory object spawns a generator that holds a `string_view` referencing that
`string`. Unfortunately, by the time this generator gets invoked, the factory
object is gone and the `string_view` is dangling.
To overcome this problem you can specify the generated type explicitly:
`ConvertGenerator<std::string>(Values(std::string("s")), [](std::string_view s)
{ ... })`. Alternatively, you can change the lambda's signature to take a
`std::string` or a `const std::string&` (the latter will not leave you with a
dangling reference because the type deduction strips off the reference and the
`const`).
### TYPED_TEST_SUITE {#TYPED_TEST_SUITE}
`TYPED_TEST_SUITE(`*`TestFixtureName`*`,`*`Types`*`)`

View File

@ -174,6 +174,7 @@ TEST_P(DerivedTest, DoesBlah) {
#endif // 0
#include <functional>
#include <iterator>
#include <utility>
@ -413,7 +414,8 @@ internal::CartesianProductHolder<Generator...> Combine(const Generator&... g) {
// Synopsis:
// ConvertGenerator<T>(gen)
// - returns a generator producing the same elements as generated by gen, but
// each element is static_cast to type T before being returned
// each T-typed element is static_cast to a type deduced from the interface
// that accepts this generator, and then returned
//
// It is useful when using the Combine() function to get the generated
// parameters in a custom type instead of std::tuple
@ -441,10 +443,65 @@ internal::CartesianProductHolder<Generator...> Combine(const Generator&... g) {
// Combine(Values("cat", "dog"),
// Values(BLACK, WHITE))));
//
template <typename T>
internal::ParamConverterGenerator<T> ConvertGenerator(
internal::ParamGenerator<T> gen) {
return internal::ParamConverterGenerator<T>(gen);
template <typename RequestedT>
internal::ParamConverterGenerator<RequestedT> ConvertGenerator(
internal::ParamGenerator<RequestedT> gen) {
return internal::ParamConverterGenerator<RequestedT>(std::move(gen));
}
// As above, but takes a callable as a second argument. The callable converts
// the generated parameter to the test fixture's parameter type. This allows you
// to use a parameter type that does not have a converting constructor from the
// generated type.
//
// Example:
//
// This will instantiate tests in test suite AnimalTest each one with
// the parameter values tuple("cat", BLACK), tuple("cat", WHITE),
// tuple("dog", BLACK), and tuple("dog", WHITE):
//
// enum Color { BLACK, GRAY, WHITE };
// struct ParamType {
// std::string animal;
// Color color;
// };
// class AnimalTest
// : public testing::TestWithParam<ParamType> {...};
//
// TEST_P(AnimalTest, AnimalLooksNice) {...}
//
// INSTANTIATE_TEST_SUITE_P(
// AnimalVariations, AnimalTest,
// ConvertGenerator(Combine(Values("cat", "dog"), Values(BLACK, WHITE)),
// [](std::tuple<std::string, Color> t) {
// return ParamType{.animal = std::get<0>(t),
// .color = std::get<1>(t)};
// }));
//
template <typename T, int&... ExplicitArgumentBarrier, typename Gen,
typename Func,
typename StdFunction = decltype(std::function(std::declval<Func>()))>
internal::ParamConverterGenerator<T, StdFunction> ConvertGenerator(Gen&& gen,
Func&& f) {
return internal::ParamConverterGenerator<T, StdFunction>(
std::forward<Gen>(gen), std::forward<Func>(f));
}
// As above, but infers the T from the supplied std::function instead of
// having the caller specify it.
template <int&... ExplicitArgumentBarrier, typename Gen, typename Func,
typename StdFunction = decltype(std::function(std::declval<Func>()))>
auto ConvertGenerator(Gen&& gen, Func&& f) {
constexpr bool is_single_arg_std_function =
internal::IsSingleArgStdFunction<StdFunction>::value;
if constexpr (is_single_arg_std_function) {
return ConvertGenerator<
typename internal::FuncSingleParamType<StdFunction>::type>(
std::forward<Gen>(gen), std::forward<Func>(f));
} else {
static_assert(is_single_arg_std_function,
"The call signature must contain a single argument.");
}
}
#define TEST_P(test_suite_name, test_name) \

View File

@ -39,6 +39,7 @@
#include <ctype.h>
#include <cassert>
#include <functional>
#include <iterator>
#include <map>
#include <memory>
@ -529,8 +530,7 @@ class ParameterizedTestSuiteInfo : public ParameterizedTestSuiteInfoBase {
// prefix). test_base_name is the name of an individual test without
// parameter index. For the test SequenceA/FooTest.DoBar/1 FooTest is
// test suite base name and DoBar is test base name.
void AddTestPattern(const char*,
const char* test_base_name,
void AddTestPattern(const char*, const char* test_base_name,
TestMetaFactoryBase<ParamType>* meta_factory,
CodeLocation code_location) {
tests_.emplace_back(
@ -952,11 +952,11 @@ class CartesianProductHolder {
std::tuple<Gen...> generators_;
};
template <typename From, typename To>
template <typename From, typename To, typename Func>
class ParamGeneratorConverter : public ParamGeneratorInterface<To> {
public:
ParamGeneratorConverter(ParamGenerator<From> gen) // NOLINT
: generator_(std::move(gen)) {}
ParamGeneratorConverter(ParamGenerator<From> gen, Func converter) // NOLINT
: generator_(std::move(gen)), converter_(std::move(converter)) {}
ParamIteratorInterface<To>* Begin() const override {
return new Iterator(this, generator_.begin(), generator_.end());
@ -965,13 +965,21 @@ class ParamGeneratorConverter : public ParamGeneratorInterface<To> {
return new Iterator(this, generator_.end(), generator_.end());
}
// Returns the std::function wrapping the user-supplied converter callable. It
// is used by the iterator (see class Iterator below) to convert the object
// (of type FROM) returned by the ParamGenerator to an object of a type that
// can be static_cast to type TO.
const Func& TypeConverter() const { return converter_; }
private:
class Iterator : public ParamIteratorInterface<To> {
public:
Iterator(const ParamGeneratorInterface<To>* base, ParamIterator<From> it,
Iterator(const ParamGeneratorConverter* base, ParamIterator<From> it,
ParamIterator<From> end)
: base_(base), it_(it), end_(end) {
if (it_ != end_) value_ = std::make_shared<To>(static_cast<To>(*it_));
if (it_ != end_)
value_ =
std::make_shared<To>(static_cast<To>(base->TypeConverter()(*it_)));
}
~Iterator() override = default;
@ -980,7 +988,9 @@ class ParamGeneratorConverter : public ParamGeneratorInterface<To> {
}
void Advance() override {
++it_;
if (it_ != end_) value_ = std::make_shared<To>(static_cast<To>(*it_));
if (it_ != end_)
value_ =
std::make_shared<To>(static_cast<To>(base_->TypeConverter()(*it_)));
}
ParamIteratorInterface<To>* Clone() const override {
return new Iterator(*this);
@ -1000,30 +1010,54 @@ class ParamGeneratorConverter : public ParamGeneratorInterface<To> {
private:
Iterator(const Iterator& other) = default;
const ParamGeneratorInterface<To>* const base_;
const ParamGeneratorConverter* const base_;
ParamIterator<From> it_;
ParamIterator<From> end_;
std::shared_ptr<To> value_;
}; // class ParamGeneratorConverter::Iterator
ParamGenerator<From> generator_;
Func converter_;
}; // class ParamGeneratorConverter
template <class Gen>
template <class GeneratedT,
typename StdFunction =
std::function<const GeneratedT&(const GeneratedT&)>>
class ParamConverterGenerator {
public:
ParamConverterGenerator(ParamGenerator<Gen> g) // NOLINT
: generator_(std::move(g)) {}
ParamConverterGenerator(ParamGenerator<GeneratedT> g) // NOLINT
: generator_(std::move(g)), converter_(Identity) {}
ParamConverterGenerator(ParamGenerator<GeneratedT> g, StdFunction converter)
: generator_(std::move(g)), converter_(std::move(converter)) {}
template <typename T>
operator ParamGenerator<T>() const { // NOLINT
return ParamGenerator<T>(new ParamGeneratorConverter<Gen, T>(generator_));
return ParamGenerator<T>(
new ParamGeneratorConverter<GeneratedT, T, StdFunction>(generator_,
converter_));
}
private:
ParamGenerator<Gen> generator_;
static const GeneratedT& Identity(const GeneratedT& v) { return v; }
ParamGenerator<GeneratedT> generator_;
StdFunction converter_;
};
// Template to determine the param type of a single-param std::function.
template <typename T>
struct FuncSingleParamType;
template <typename R, typename P>
struct FuncSingleParamType<std::function<R(P)>> {
using type = std::remove_cv_t<std::remove_reference_t<P>>;
};
template <typename T>
struct IsSingleArgStdFunction : public std::false_type {};
template <typename R, typename P>
struct IsSingleArgStdFunction<std::function<R(P)>> : public std::true_type {};
} // namespace internal
} // namespace testing

View File

@ -35,12 +35,17 @@
#include "test/googletest-param-test-test.h"
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <iostream>
#include <list>
#include <set>
#include <sstream>
#include <string>
#include <string_view>
#include <tuple>
#include <type_traits>
#include <vector>
#include "gtest/gtest.h"
@ -583,6 +588,71 @@ TEST(ConvertTest, NonDefaultConstructAssign) {
EXPECT_TRUE(it == gen.end());
}
TEST(ConvertTest, WithConverterLambdaAndDeducedType) {
const ParamGenerator<ConstructFromT<int8_t>> gen =
ConvertGenerator(Values("0", std::string("1")), [](const std::string& s) {
size_t pos;
int64_t value = std::stoll(s, &pos);
EXPECT_EQ(pos, s.size());
return value;
});
ConstructFromT<int8_t> expected_values[] = {ConstructFromT<int8_t>(0),
ConstructFromT<int8_t>(1)};
VerifyGenerator(gen, expected_values);
}
TEST(ConvertTest, WithConverterLambdaAndExplicitType) {
auto convert_generator = ConvertGenerator<std::string>(
Values("0", std::string("1")), [](std::string_view s) {
size_t pos;
int64_t value = std::stoll(std::string(s), &pos);
EXPECT_EQ(pos, s.size());
return value;
});
constexpr bool is_correct_type = std::is_same_v<
decltype(convert_generator),
testing::internal::ParamConverterGenerator<
std::string, std::function<int64_t(std::string_view)>>>;
EXPECT_TRUE(is_correct_type);
const ParamGenerator<ConstructFromT<int8_t>> gen = convert_generator;
ConstructFromT<int8_t> expected_values[] = {ConstructFromT<int8_t>(0),
ConstructFromT<int8_t>(1)};
VerifyGenerator(gen, expected_values);
}
TEST(ConvertTest, WithConverterFunctionPointer) {
int64_t (*func_ptr)(const std::string&) = [](const std::string& s) {
size_t pos;
int64_t value = std::stoll(s, &pos);
EXPECT_EQ(pos, s.size());
return value;
};
const ParamGenerator<ConstructFromT<int8_t>> gen =
ConvertGenerator(Values("0", std::string("1")), func_ptr);
ConstructFromT<int8_t> expected_values[] = {ConstructFromT<int8_t>(0),
ConstructFromT<int8_t>(1)};
VerifyGenerator(gen, expected_values);
}
TEST(ConvertTest, WithConverterFunctionReference) {
int64_t (*func_ptr)(const std::string&) = [](const std::string& s) {
size_t pos;
int64_t value = std::stoll(s, &pos);
EXPECT_EQ(pos, s.size());
return value;
};
int64_t (&func_ref)(const std::string&) = *func_ptr;
const ParamGenerator<ConstructFromT<int8_t>> gen =
ConvertGenerator(Values("0", std::string("1")), func_ref);
ConstructFromT<int8_t> expected_values[] = {ConstructFromT<int8_t>(0),
ConstructFromT<int8_t>(1)};
VerifyGenerator(gen, expected_values);
}
// Tests that an generator produces correct sequence after being
// assigned from another generator.
TEST(ParamGeneratorTest, AssignmentWorks) {