From e88cb95b92acbdce9b058dd894a68e1281b38495 Mon Sep 17 00:00:00 2001 From: Abseil Team Date: Tue, 4 Mar 2025 10:39:43 -0800 Subject: [PATCH] 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 --- docs/reference/testing.md | 99 ++++++++++++++++++- googletest/include/gtest/gtest-param-test.h | 67 ++++++++++++- .../include/gtest/internal/gtest-param-util.h | 62 +++++++++--- googletest/test/googletest-param-test-test.cc | 70 +++++++++++++ 4 files changed, 278 insertions(+), 20 deletions(-) diff --git a/docs/reference/testing.md b/docs/reference/testing.md index 3ed52111..d7dc5cf9 100644 --- a/docs/reference/testing.md +++ b/docs/reference/testing.md @@ -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(g)` | Yields values generated by generator `g`, `static_cast` to `T`. | +| `ConvertGenerator(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` — a `double` — 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`. 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& t); + ... +}; + +INSTANTIATE_TEST_SUITE_P(MyInstantiation, MyTestSuite, + ConvertGenerator>(Combine(Values(0.1, 1.2), Bool()))); +``` + +In this example `Combine` supports the generation of `std::tuple>` +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& 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(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`*`)` diff --git a/googletest/include/gtest/gtest-param-test.h b/googletest/include/gtest/gtest-param-test.h index dbfc5c8d..9e023f96 100644 --- a/googletest/include/gtest/gtest-param-test.h +++ b/googletest/include/gtest/gtest-param-test.h @@ -174,6 +174,7 @@ TEST_P(DerivedTest, DoesBlah) { #endif // 0 +#include #include #include @@ -413,7 +414,8 @@ internal::CartesianProductHolder Combine(const Generator&... g) { // Synopsis: // ConvertGenerator(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 Combine(const Generator&... g) { // Combine(Values("cat", "dog"), // Values(BLACK, WHITE)))); // -template -internal::ParamConverterGenerator ConvertGenerator( - internal::ParamGenerator gen) { - return internal::ParamConverterGenerator(gen); +template +internal::ParamConverterGenerator ConvertGenerator( + internal::ParamGenerator gen) { + return internal::ParamConverterGenerator(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 {...}; +// +// TEST_P(AnimalTest, AnimalLooksNice) {...} +// +// INSTANTIATE_TEST_SUITE_P( +// AnimalVariations, AnimalTest, +// ConvertGenerator(Combine(Values("cat", "dog"), Values(BLACK, WHITE)), +// [](std::tuple t) { +// return ParamType{.animal = std::get<0>(t), +// .color = std::get<1>(t)}; +// })); +// +template ()))> +internal::ParamConverterGenerator ConvertGenerator(Gen&& gen, + Func&& f) { + return internal::ParamConverterGenerator( + std::forward(gen), std::forward(f)); +} + +// As above, but infers the T from the supplied std::function instead of +// having the caller specify it. +template ()))> +auto ConvertGenerator(Gen&& gen, Func&& f) { + constexpr bool is_single_arg_std_function = + internal::IsSingleArgStdFunction::value; + if constexpr (is_single_arg_std_function) { + return ConvertGenerator< + typename internal::FuncSingleParamType::type>( + std::forward(gen), std::forward(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) \ diff --git a/googletest/include/gtest/internal/gtest-param-util.h b/googletest/include/gtest/internal/gtest-param-util.h index cc7ea531..a092a86a 100644 --- a/googletest/include/gtest/internal/gtest-param-util.h +++ b/googletest/include/gtest/internal/gtest-param-util.h @@ -39,6 +39,7 @@ #include #include +#include #include #include #include @@ -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* meta_factory, CodeLocation code_location) { tests_.emplace_back( @@ -952,11 +952,11 @@ class CartesianProductHolder { std::tuple generators_; }; -template +template class ParamGeneratorConverter : public ParamGeneratorInterface { public: - ParamGeneratorConverter(ParamGenerator gen) // NOLINT - : generator_(std::move(gen)) {} + ParamGeneratorConverter(ParamGenerator gen, Func converter) // NOLINT + : generator_(std::move(gen)), converter_(std::move(converter)) {} ParamIteratorInterface* Begin() const override { return new Iterator(this, generator_.begin(), generator_.end()); @@ -965,13 +965,21 @@ class ParamGeneratorConverter : public ParamGeneratorInterface { 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 { public: - Iterator(const ParamGeneratorInterface* base, ParamIterator it, + Iterator(const ParamGeneratorConverter* base, ParamIterator it, ParamIterator end) : base_(base), it_(it), end_(end) { - if (it_ != end_) value_ = std::make_shared(static_cast(*it_)); + if (it_ != end_) + value_ = + std::make_shared(static_cast(base->TypeConverter()(*it_))); } ~Iterator() override = default; @@ -980,7 +988,9 @@ class ParamGeneratorConverter : public ParamGeneratorInterface { } void Advance() override { ++it_; - if (it_ != end_) value_ = std::make_shared(static_cast(*it_)); + if (it_ != end_) + value_ = + std::make_shared(static_cast(base_->TypeConverter()(*it_))); } ParamIteratorInterface* Clone() const override { return new Iterator(*this); @@ -1000,30 +1010,54 @@ class ParamGeneratorConverter : public ParamGeneratorInterface { private: Iterator(const Iterator& other) = default; - const ParamGeneratorInterface* const base_; + const ParamGeneratorConverter* const base_; ParamIterator it_; ParamIterator end_; std::shared_ptr value_; }; // class ParamGeneratorConverter::Iterator ParamGenerator generator_; + Func converter_; }; // class ParamGeneratorConverter -template +template > class ParamConverterGenerator { public: - ParamConverterGenerator(ParamGenerator g) // NOLINT - : generator_(std::move(g)) {} + ParamConverterGenerator(ParamGenerator g) // NOLINT + : generator_(std::move(g)), converter_(Identity) {} + + ParamConverterGenerator(ParamGenerator g, StdFunction converter) + : generator_(std::move(g)), converter_(std::move(converter)) {} template operator ParamGenerator() const { // NOLINT - return ParamGenerator(new ParamGeneratorConverter(generator_)); + return ParamGenerator( + new ParamGeneratorConverter(generator_, + converter_)); } private: - ParamGenerator generator_; + static const GeneratedT& Identity(const GeneratedT& v) { return v; } + + ParamGenerator generator_; + StdFunction converter_; }; +// Template to determine the param type of a single-param std::function. +template +struct FuncSingleParamType; +template +struct FuncSingleParamType> { + using type = std::remove_cv_t>; +}; + +template +struct IsSingleArgStdFunction : public std::false_type {}; +template +struct IsSingleArgStdFunction> : public std::true_type {}; + } // namespace internal } // namespace testing diff --git a/googletest/test/googletest-param-test-test.cc b/googletest/test/googletest-param-test-test.cc index c9c5e78e..bc130601 100644 --- a/googletest/test/googletest-param-test-test.cc +++ b/googletest/test/googletest-param-test-test.cc @@ -35,12 +35,17 @@ #include "test/googletest-param-test-test.h" #include +#include +#include +#include #include #include #include #include #include +#include #include +#include #include #include "gtest/gtest.h" @@ -583,6 +588,71 @@ TEST(ConvertTest, NonDefaultConstructAssign) { EXPECT_TRUE(it == gen.end()); } +TEST(ConvertTest, WithConverterLambdaAndDeducedType) { + const ParamGenerator> 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 expected_values[] = {ConstructFromT(0), + ConstructFromT(1)}; + VerifyGenerator(gen, expected_values); +} + +TEST(ConvertTest, WithConverterLambdaAndExplicitType) { + auto convert_generator = ConvertGenerator( + 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>>; + EXPECT_TRUE(is_correct_type); + const ParamGenerator> gen = convert_generator; + + ConstructFromT expected_values[] = {ConstructFromT(0), + ConstructFromT(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> gen = + ConvertGenerator(Values("0", std::string("1")), func_ptr); + + ConstructFromT expected_values[] = {ConstructFromT(0), + ConstructFromT(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> gen = + ConvertGenerator(Values("0", std::string("1")), func_ref); + + ConstructFromT expected_values[] = {ConstructFromT(0), + ConstructFromT(1)}; + VerifyGenerator(gen, expected_values); +} + // Tests that an generator produces correct sequence after being // assigned from another generator. TEST(ParamGeneratorTest, AssignmentWorks) {