Add PredicateMatcher that takes an arbitrary predicate functions

Also adds `Predicate` helper function to create `PredicateMatcher`.
Because of limitations in type inference it needs to be explicitly
typed, like so
`Predicate<std::string>([](std::string const& str) { ... })`.
It also takes an optional second argument for description of the
predicate.

It is possible to infer the argument with sufficient TMP, see
https://stackoverflow.com/questions/43560492/how-to-extract-lambdas-return-type-and-variadic-parameters-pack-back-from-gener/43561563#43561563
but I don't think that the magic is worth introducing ATM.

Closes #1236
This commit is contained in:
Martin Hořeňovský 2018-04-03 23:28:14 +02:00
parent dfb83f20e9
commit 1ca8f43b01
11 changed files with 204 additions and 7 deletions

View File

@ -152,6 +152,7 @@ set(INTERNAL_HEADERS
${HEADER_DIR}/internal/catch_list.h ${HEADER_DIR}/internal/catch_list.h
${HEADER_DIR}/internal/catch_matchers.h ${HEADER_DIR}/internal/catch_matchers.h
${HEADER_DIR}/internal/catch_matchers_floating.h ${HEADER_DIR}/internal/catch_matchers_floating.h
${HEADER_DIR}/internal/catch_matchers_generic.hpp
${HEADER_DIR}/internal/catch_matchers_string.h ${HEADER_DIR}/internal/catch_matchers_string.h
${HEADER_DIR}/internal/catch_matchers_vector.h ${HEADER_DIR}/internal/catch_matchers_vector.h
${HEADER_DIR}/internal/catch_message.h ${HEADER_DIR}/internal/catch_message.h
@ -221,6 +222,7 @@ set(IMPL_SOURCES
${HEADER_DIR}/internal/catch_leak_detector.cpp ${HEADER_DIR}/internal/catch_leak_detector.cpp
${HEADER_DIR}/internal/catch_matchers.cpp ${HEADER_DIR}/internal/catch_matchers.cpp
${HEADER_DIR}/internal/catch_matchers_floating.cpp ${HEADER_DIR}/internal/catch_matchers_floating.cpp
${HEADER_DIR}/internal/catch_matchers_generic.cpp
${HEADER_DIR}/internal/catch_matchers_string.cpp ${HEADER_DIR}/internal/catch_matchers_string.cpp
${HEADER_DIR}/internal/catch_message.cpp ${HEADER_DIR}/internal/catch_message.cpp
${HEADER_DIR}/internal/catch_registry_hub.cpp ${HEADER_DIR}/internal/catch_registry_hub.cpp

View File

@ -53,6 +53,25 @@ The floating point matchers are `WithinULP` and `WithinAbs`. `WithinAbs` accepts
Do note that ULP-based checks only make sense when both compared numbers are of the same type and `WithinULP` will use type of its argument as the target type. This means that `WithinULP(1.f, 1)` will expect to compare `float`s, but `WithinULP(1., 1)` will expect to compare `double`s. Do note that ULP-based checks only make sense when both compared numbers are of the same type and `WithinULP` will use type of its argument as the target type. This means that `WithinULP(1.f, 1)` will expect to compare `float`s, but `WithinULP(1., 1)` will expect to compare `double`s.
### Generic matchers
Catch also aims to provide a set of generic matchers. Currently this set
contains only a matcher that takes arbitrary callable predicate and applies
it onto the provided object.
Because of type inference limitations, the argument type of the predicate
has to be provided explicitly. Example:
```cpp
REQUIRE_THAT("Hello olleH",
Predicate<std::string>(
[] (std::string const& str) -> bool { return str.front() == str.back(); },
"First and last character should be equal")
);
```
The second argument is an optional description of the predicate, and is
used only during reporting of the result.
## Custom matchers ## Custom matchers
It's easy to provide your own matchers to extend Catch or just to work with your own types. It's easy to provide your own matchers to extend Catch or just to work with your own types.

View File

@ -11,6 +11,7 @@
#include "catch_capture.hpp" #include "catch_capture.hpp"
#include "catch_matchers.h" #include "catch_matchers.h"
#include "catch_matchers_floating.h" #include "catch_matchers_floating.h"
#include "catch_matchers_generic.hpp"
#include "catch_matchers_string.h" #include "catch_matchers_string.h"
#include "catch_matchers_vector.h" #include "catch_matchers_vector.h"

View File

@ -0,0 +1,9 @@
#include "catch_matchers_generic.hpp"
std::string Catch::Matchers::Generic::Detail::finalizeDescription(const std::string& desc) {
if (desc.empty()) {
return "matches undescribed predicate";
} else {
return "matches predicate: \"" + desc + '"';
}
}

View File

@ -0,0 +1,58 @@
/*
* Created by Martin Hořeňovský on 03/04/2017.
*
* Distributed under the Boost Software License, Version 1.0. (See accompanying
* file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
*/
#ifndef TWOBLUECUBES_CATCH_MATCHERS_GENERIC_HPP_INCLUDED
#define TWOBLUECUBES_CATCH_MATCHERS_GENERIC_HPP_INCLUDED
#include "catch_common.h"
#include "catch_matchers.h"
#include <functional>
#include <string>
namespace Catch {
namespace Matchers {
namespace Generic {
namespace Detail {
std::string finalizeDescription(const std::string& desc);
}
template <typename T>
class PredicateMatcher : public MatcherBase<T> {
std::function<bool(T const&)> m_predicate;
std::string m_description;
public:
PredicateMatcher(std::function<bool(T const&)> const& elem, std::string const& descr)
:m_predicate(std::move(elem)),
m_description(Detail::finalizeDescription(descr))
{}
bool match( T const& item ) const override {
return m_predicate(item);
}
std::string describe() const override {
return m_description;
}
};
} // namespace Generic
// The following functions create the actual matcher objects.
// The user has to explicitly specify type to the function, because
// infering std::function<bool(T const&)> is hard (but possible) and
// requires a lot of TMP.
template<typename T>
Generic::PredicateMatcher<T> Predicate(std::function<bool(T const&)> const& predicate, std::string const& description = "") {
return Generic::PredicateMatcher<T>(predicate, description);
}
} // namespace Matchers
} // namespace Catch
#endif // TWOBLUECUBES_CATCH_MATCHERS_GENERIC_HPP_INCLUDED

View File

@ -97,6 +97,10 @@ Approx.tests.cpp:<line number>: passed: 0 == Approx( dZero) for: 0 == Approx( 0.
Approx.tests.cpp:<line number>: passed: 0 == Approx( dSmall ).margin( 0.001 ) for: 0 == Approx( 0.00001 ) Approx.tests.cpp:<line number>: passed: 0 == Approx( dSmall ).margin( 0.001 ) for: 0 == Approx( 0.00001 )
Approx.tests.cpp:<line number>: passed: 1.234f == Approx( dMedium ) for: 1.234f == Approx( 1.234 ) Approx.tests.cpp:<line number>: passed: 1.234f == Approx( dMedium ) for: 1.234f == Approx( 1.234 )
Approx.tests.cpp:<line number>: passed: dMedium == Approx( 1.234f ) for: 1.234 == Approx( 1.2339999676 ) Approx.tests.cpp:<line number>: passed: dMedium == Approx( 1.234f ) for: 1.234 == Approx( 1.2339999676 )
Matchers.tests.cpp:<line number>: passed: 1, Predicate<int>(alwaysTrue, "always true") for: 1 matches predicate: "always true"
Matchers.tests.cpp:<line number>: passed: 1, !Predicate<int>(alwaysFalse, "always false") for: 1 not matches predicate: "always false"
Matchers.tests.cpp:<line number>: passed: "Hello olleH", Predicate<std::string>( [] (std::string const& str) -> bool { return str.front() == str.back(); }, "First and last character should be equal") for: "Hello olleH" matches predicate: "First and last character should be equal"
Matchers.tests.cpp:<line number>: passed: "This wouldn't pass", !Predicate<std::string>( [] (std::string const& str) -> bool { return str.front() == str.back(); } ) for: "This wouldn't pass" not matches undescribed predicate
Tricky.tests.cpp:<line number>: passed: true Tricky.tests.cpp:<line number>: passed: true
Tricky.tests.cpp:<line number>: passed: true Tricky.tests.cpp:<line number>: passed: true
Tricky.tests.cpp:<line number>: passed: true Tricky.tests.cpp:<line number>: passed: true

View File

@ -1084,6 +1084,6 @@ due to unexpected exception with message:
Why would you throw a std::string? Why would you throw a std::string?
=============================================================================== ===============================================================================
test cases: 203 | 150 passed | 49 failed | 4 failed as expected test cases: 204 | 151 passed | 49 failed | 4 failed as expected
assertions: 1057 | 929 passed | 107 failed | 21 failed as expected assertions: 1061 | 933 passed | 107 failed | 21 failed as expected

View File

@ -814,6 +814,44 @@ PASSED:
with expansion: with expansion:
1.234 == Approx( 1.2339999676 ) 1.234 == Approx( 1.2339999676 )
-------------------------------------------------------------------------------
Arbitrary predicate matcher
Function pointer
-------------------------------------------------------------------------------
Matchers.tests.cpp:<line number>
...............................................................................
Matchers.tests.cpp:<line number>:
PASSED:
REQUIRE_THAT( 1, Predicate<int>(alwaysTrue, "always true") )
with expansion:
1 matches predicate: "always true"
Matchers.tests.cpp:<line number>:
PASSED:
REQUIRE_THAT( 1, !Predicate<int>(alwaysFalse, "always false") )
with expansion:
1 not matches predicate: "always false"
-------------------------------------------------------------------------------
Arbitrary predicate matcher
Lambdas + different type
-------------------------------------------------------------------------------
Matchers.tests.cpp:<line number>
...............................................................................
Matchers.tests.cpp:<line number>:
PASSED:
REQUIRE_THAT( "Hello olleH", Predicate<std::string>( [] (std::string const& str) -> bool { return str.front() == str.back(); }, "First and last character should be equal") )
with expansion:
"Hello olleH" matches predicate: "First and last character should be equal"
Matchers.tests.cpp:<line number>:
PASSED:
REQUIRE_THAT( "This wouldn't pass", !Predicate<std::string>( [] (std::string const& str) -> bool { return str.front() == str.back(); } ) )
with expansion:
"This wouldn't pass" not matches undescribed predicate
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
Assertions then sections Assertions then sections
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
@ -8897,6 +8935,6 @@ Misc.tests.cpp:<line number>:
PASSED: PASSED:
=============================================================================== ===============================================================================
test cases: 203 | 137 passed | 62 failed | 4 failed as expected test cases: 204 | 138 passed | 62 failed | 4 failed as expected
assertions: 1071 | 929 passed | 121 failed | 21 failed as expected assertions: 1075 | 933 passed | 121 failed | 21 failed as expected

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<testsuitesloose text artifact <testsuitesloose text artifact
> >
<testsuite name="<exe-name>" errors="17" failures="105" tests="1072" hostname="tbd" time="{duration}" timestamp="{iso8601-timestamp}"> <testsuite name="<exe-name>" errors="17" failures="105" tests="1076" hostname="tbd" time="{duration}" timestamp="{iso8601-timestamp}">
<testcase classname="<exe-name>.global" name="# A test name that starts with a #" time="{duration}"/> <testcase classname="<exe-name>.global" name="# A test name that starts with a #" time="{duration}"/>
<testcase classname="<exe-name>.global" name="#1005: Comparing pointer to int and long (NULL can be either on various systems)" time="{duration}"/> <testcase classname="<exe-name>.global" name="#1005: Comparing pointer to int and long (NULL can be either on various systems)" time="{duration}"/>
<testcase classname="<exe-name>.global" name="#1027" time="{duration}"/> <testcase classname="<exe-name>.global" name="#1027" time="{duration}"/>
@ -110,6 +110,8 @@ Exception.tests.cpp:<line number>
<testcase classname="<exe-name>.global" name="Approximate comparisons with floats" time="{duration}"/> <testcase classname="<exe-name>.global" name="Approximate comparisons with floats" time="{duration}"/>
<testcase classname="<exe-name>.global" name="Approximate comparisons with ints" time="{duration}"/> <testcase classname="<exe-name>.global" name="Approximate comparisons with ints" time="{duration}"/>
<testcase classname="<exe-name>.global" name="Approximate comparisons with mixed numeric types" time="{duration}"/> <testcase classname="<exe-name>.global" name="Approximate comparisons with mixed numeric types" time="{duration}"/>
<testcase classname="<exe-name>.global" name="Arbitrary predicate matcher/Function pointer" time="{duration}"/>
<testcase classname="<exe-name>.global" name="Arbitrary predicate matcher/Lambdas + different type" time="{duration}"/>
<testcase classname="<exe-name>.global" name="Assertions then sections" time="{duration}"/> <testcase classname="<exe-name>.global" name="Assertions then sections" time="{duration}"/>
<testcase classname="<exe-name>.global" name="Assertions then sections/A section" time="{duration}"/> <testcase classname="<exe-name>.global" name="Assertions then sections/A section" time="{duration}"/>
<testcase classname="<exe-name>.global" name="Assertions then sections/A section/Another section" time="{duration}"/> <testcase classname="<exe-name>.global" name="Assertions then sections/A section/Another section" time="{duration}"/>

View File

@ -870,6 +870,47 @@
</Expression> </Expression>
<OverallResult success="true"/> <OverallResult success="true"/>
</TestCase> </TestCase>
<TestCase name="Arbitrary predicate matcher" tags="[generic][matchers]" filename="projects/<exe-name>/UsageTests/Matchers.tests.cpp" >
<Section name="Function pointer" filename="projects/<exe-name>/UsageTests/Matchers.tests.cpp" >
<Expression success="true" type="REQUIRE_THAT" filename="projects/<exe-name>/UsageTests/Matchers.tests.cpp" >
<Original>
1, Predicate&lt;int>(alwaysTrue, "always true")
</Original>
<Expanded>
1 matches predicate: "always true"
</Expanded>
</Expression>
<Expression success="true" type="REQUIRE_THAT" filename="projects/<exe-name>/UsageTests/Matchers.tests.cpp" >
<Original>
1, !Predicate&lt;int>(alwaysFalse, "always false")
</Original>
<Expanded>
1 not matches predicate: "always false"
</Expanded>
</Expression>
<OverallResults successes="2" failures="0" expectedFailures="0"/>
</Section>
<Section name="Lambdas + different type" filename="projects/<exe-name>/UsageTests/Matchers.tests.cpp" >
<Expression success="true" type="REQUIRE_THAT" filename="projects/<exe-name>/UsageTests/Matchers.tests.cpp" >
<Original>
"Hello olleH", Predicate&lt;std::string>( [] (std::string const&amp; str) -> bool { return str.front() == str.back(); }, "First and last character should be equal")
</Original>
<Expanded>
"Hello olleH" matches predicate: "First and last character should be equal"
</Expanded>
</Expression>
<Expression success="true" type="REQUIRE_THAT" filename="projects/<exe-name>/UsageTests/Matchers.tests.cpp" >
<Original>
"This wouldn't pass", !Predicate&lt;std::string>( [] (std::string const&amp; str) -> bool { return str.front() == str.back(); } )
</Original>
<Expanded>
"This wouldn't pass" not matches undescribed predicate
</Expanded>
</Expression>
<OverallResults successes="2" failures="0" expectedFailures="0"/>
</Section>
<OverallResult success="true"/>
</TestCase>
<TestCase name="Assertions then sections" tags="[Tricky]" filename="projects/<exe-name>/UsageTests/Tricky.tests.cpp" > <TestCase name="Assertions then sections" tags="[Tricky]" filename="projects/<exe-name>/UsageTests/Tricky.tests.cpp" >
<Expression success="true" type="REQUIRE" filename="projects/<exe-name>/UsageTests/Tricky.tests.cpp" > <Expression success="true" type="REQUIRE" filename="projects/<exe-name>/UsageTests/Tricky.tests.cpp" >
<Original> <Original>
@ -9841,7 +9882,7 @@ loose text artifact
</Section> </Section>
<OverallResult success="true"/> <OverallResult success="true"/>
</TestCase> </TestCase>
<OverallResults successes="929" failures="122" expectedFailures="21"/> <OverallResults successes="933" failures="122" expectedFailures="21"/>
</Group> </Group>
<OverallResults successes="929" failures="121" expectedFailures="21"/> <OverallResults successes="933" failures="121" expectedFailures="21"/>
</Catch> </Catch>

View File

@ -32,6 +32,9 @@ namespace { namespace MatchersTests {
return "some completely different text that contains one common word"; return "some completely different text that contains one common word";
} }
inline bool alwaysTrue(int) { return true; }
inline bool alwaysFalse(int) { return false; }
#ifdef _MSC_VER #ifdef _MSC_VER
#pragma warning(disable:4702) // Unreachable code -- MSVC 19 (VS 2015) sees right through the indirection #pragma warning(disable:4702) // Unreachable code -- MSVC 19 (VS 2015) sees right through the indirection
@ -396,6 +399,26 @@ namespace { namespace MatchersTests {
} }
} }
TEST_CASE("Arbitrary predicate matcher", "[matchers][generic]") {
SECTION("Function pointer") {
REQUIRE_THAT(1, Predicate<int>(alwaysTrue, "always true"));
REQUIRE_THAT(1, !Predicate<int>(alwaysFalse, "always false"));
}
SECTION("Lambdas + different type") {
REQUIRE_THAT("Hello olleH",
Predicate<std::string>(
[] (std::string const& str) -> bool { return str.front() == str.back(); },
"First and last character should be equal")
);
REQUIRE_THAT("This wouldn't pass",
!Predicate<std::string>(
[] (std::string const& str) -> bool { return str.front() == str.back(); }
)
);
}
}
} } // namespace MatchersTests } } // namespace MatchersTests
#endif // CATCH_CONFIG_DISABLE_MATCHERS #endif // CATCH_CONFIG_DISABLE_MATCHERS