From 1ca8f43b011ad61f4627a3339333406f15a46431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ho=C5=99e=C5=88ovsk=C3=BD?= Date: Tue, 3 Apr 2018 23:28:14 +0200 Subject: [PATCH] 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 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 --- CMakeLists.txt | 2 + docs/matchers.md | 19 ++++++ include/internal/catch_capture_matchers.h | 1 + include/internal/catch_matchers_generic.cpp | 9 +++ include/internal/catch_matchers_generic.hpp | 58 +++++++++++++++++++ .../Baselines/compact.sw.approved.txt | 4 ++ .../Baselines/console.std.approved.txt | 4 +- .../Baselines/console.sw.approved.txt | 42 +++++++++++++- .../SelfTest/Baselines/junit.sw.approved.txt | 4 +- .../SelfTest/Baselines/xml.sw.approved.txt | 45 +++++++++++++- .../SelfTest/UsageTests/Matchers.tests.cpp | 23 ++++++++ 11 files changed, 204 insertions(+), 7 deletions(-) create mode 100644 include/internal/catch_matchers_generic.cpp create mode 100644 include/internal/catch_matchers_generic.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 46dfd735..5484c857 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -152,6 +152,7 @@ set(INTERNAL_HEADERS ${HEADER_DIR}/internal/catch_list.h ${HEADER_DIR}/internal/catch_matchers.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_vector.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_matchers.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_message.cpp ${HEADER_DIR}/internal/catch_registry_hub.cpp diff --git a/docs/matchers.md b/docs/matchers.md index fb95ce26..adbd4ce7 100644 --- a/docs/matchers.md +++ b/docs/matchers.md @@ -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. +### 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 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 It's easy to provide your own matchers to extend Catch or just to work with your own types. diff --git a/include/internal/catch_capture_matchers.h b/include/internal/catch_capture_matchers.h index 358bbc3b..9026aebf 100644 --- a/include/internal/catch_capture_matchers.h +++ b/include/internal/catch_capture_matchers.h @@ -11,6 +11,7 @@ #include "catch_capture.hpp" #include "catch_matchers.h" #include "catch_matchers_floating.h" +#include "catch_matchers_generic.hpp" #include "catch_matchers_string.h" #include "catch_matchers_vector.h" diff --git a/include/internal/catch_matchers_generic.cpp b/include/internal/catch_matchers_generic.cpp new file mode 100644 index 00000000..300102e0 --- /dev/null +++ b/include/internal/catch_matchers_generic.cpp @@ -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 + '"'; + } +} diff --git a/include/internal/catch_matchers_generic.hpp b/include/internal/catch_matchers_generic.hpp new file mode 100644 index 00000000..7c4f9f1d --- /dev/null +++ b/include/internal/catch_matchers_generic.hpp @@ -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 +#include + +namespace Catch { +namespace Matchers { +namespace Generic { + +namespace Detail { + std::string finalizeDescription(const std::string& desc); +} + +template +class PredicateMatcher : public MatcherBase { + std::function m_predicate; + std::string m_description; +public: + + PredicateMatcher(std::function 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 is hard (but possible) and + // requires a lot of TMP. + template + Generic::PredicateMatcher Predicate(std::function const& predicate, std::string const& description = "") { + return Generic::PredicateMatcher(predicate, description); + } + +} // namespace Matchers +} // namespace Catch + +#endif // TWOBLUECUBES_CATCH_MATCHERS_GENERIC_HPP_INCLUDED diff --git a/projects/SelfTest/Baselines/compact.sw.approved.txt b/projects/SelfTest/Baselines/compact.sw.approved.txt index a233aa6c..ccc8dbae 100644 --- a/projects/SelfTest/Baselines/compact.sw.approved.txt +++ b/projects/SelfTest/Baselines/compact.sw.approved.txt @@ -97,6 +97,10 @@ Approx.tests.cpp:: passed: 0 == Approx( dZero) for: 0 == Approx( 0. Approx.tests.cpp:: passed: 0 == Approx( dSmall ).margin( 0.001 ) for: 0 == Approx( 0.00001 ) Approx.tests.cpp:: passed: 1.234f == Approx( dMedium ) for: 1.234f == Approx( 1.234 ) Approx.tests.cpp:: passed: dMedium == Approx( 1.234f ) for: 1.234 == Approx( 1.2339999676 ) +Matchers.tests.cpp:: passed: 1, Predicate(alwaysTrue, "always true") for: 1 matches predicate: "always true" +Matchers.tests.cpp:: passed: 1, !Predicate(alwaysFalse, "always false") for: 1 not matches predicate: "always false" +Matchers.tests.cpp:: passed: "Hello olleH", Predicate( [] (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:: passed: "This wouldn't pass", !Predicate( [] (std::string const& str) -> bool { return str.front() == str.back(); } ) for: "This wouldn't pass" not matches undescribed predicate Tricky.tests.cpp:: passed: true Tricky.tests.cpp:: passed: true Tricky.tests.cpp:: passed: true diff --git a/projects/SelfTest/Baselines/console.std.approved.txt b/projects/SelfTest/Baselines/console.std.approved.txt index bd5f62cf..c5b91182 100644 --- a/projects/SelfTest/Baselines/console.std.approved.txt +++ b/projects/SelfTest/Baselines/console.std.approved.txt @@ -1084,6 +1084,6 @@ due to unexpected exception with message: Why would you throw a std::string? =============================================================================== -test cases: 203 | 150 passed | 49 failed | 4 failed as expected -assertions: 1057 | 929 passed | 107 failed | 21 failed as expected +test cases: 204 | 151 passed | 49 failed | 4 failed as expected +assertions: 1061 | 933 passed | 107 failed | 21 failed as expected diff --git a/projects/SelfTest/Baselines/console.sw.approved.txt b/projects/SelfTest/Baselines/console.sw.approved.txt index 244bcffc..814e09af 100644 --- a/projects/SelfTest/Baselines/console.sw.approved.txt +++ b/projects/SelfTest/Baselines/console.sw.approved.txt @@ -814,6 +814,44 @@ PASSED: with expansion: 1.234 == Approx( 1.2339999676 ) +------------------------------------------------------------------------------- +Arbitrary predicate matcher + Function pointer +------------------------------------------------------------------------------- +Matchers.tests.cpp: +............................................................................... + +Matchers.tests.cpp:: +PASSED: + REQUIRE_THAT( 1, Predicate(alwaysTrue, "always true") ) +with expansion: + 1 matches predicate: "always true" + +Matchers.tests.cpp:: +PASSED: + REQUIRE_THAT( 1, !Predicate(alwaysFalse, "always false") ) +with expansion: + 1 not matches predicate: "always false" + +------------------------------------------------------------------------------- +Arbitrary predicate matcher + Lambdas + different type +------------------------------------------------------------------------------- +Matchers.tests.cpp: +............................................................................... + +Matchers.tests.cpp:: +PASSED: + REQUIRE_THAT( "Hello olleH", Predicate( [] (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:: +PASSED: + REQUIRE_THAT( "This wouldn't pass", !Predicate( [] (std::string const& str) -> bool { return str.front() == str.back(); } ) ) +with expansion: + "This wouldn't pass" not matches undescribed predicate + ------------------------------------------------------------------------------- Assertions then sections ------------------------------------------------------------------------------- @@ -8897,6 +8935,6 @@ Misc.tests.cpp:: PASSED: =============================================================================== -test cases: 203 | 137 passed | 62 failed | 4 failed as expected -assertions: 1071 | 929 passed | 121 failed | 21 failed as expected +test cases: 204 | 138 passed | 62 failed | 4 failed as expected +assertions: 1075 | 933 passed | 121 failed | 21 failed as expected diff --git a/projects/SelfTest/Baselines/junit.sw.approved.txt b/projects/SelfTest/Baselines/junit.sw.approved.txt index ea31fb6e..bc2600e6 100644 --- a/projects/SelfTest/Baselines/junit.sw.approved.txt +++ b/projects/SelfTest/Baselines/junit.sw.approved.txt @@ -1,7 +1,7 @@ - + @@ -110,6 +110,8 @@ Exception.tests.cpp: + + diff --git a/projects/SelfTest/Baselines/xml.sw.approved.txt b/projects/SelfTest/Baselines/xml.sw.approved.txt index 2d24824a..b6d18960 100644 --- a/projects/SelfTest/Baselines/xml.sw.approved.txt +++ b/projects/SelfTest/Baselines/xml.sw.approved.txt @@ -870,6 +870,47 @@ + +
+ + + 1, Predicate<int>(alwaysTrue, "always true") + + + 1 matches predicate: "always true" + + + + + 1, !Predicate<int>(alwaysFalse, "always false") + + + 1 not matches predicate: "always false" + + + +
+
+ + + "Hello olleH", Predicate<std::string>( [] (std::string const& str) -> bool { return str.front() == str.back(); }, "First and last character should be equal") + + + "Hello olleH" matches predicate: "First and last character should be equal" + + + + + "This wouldn't pass", !Predicate<std::string>( [] (std::string const& str) -> bool { return str.front() == str.back(); } ) + + + "This wouldn't pass" not matches undescribed predicate + + + +
+ +
@@ -9841,7 +9882,7 @@ loose text artifact - + - + diff --git a/projects/SelfTest/UsageTests/Matchers.tests.cpp b/projects/SelfTest/UsageTests/Matchers.tests.cpp index 7032c2cc..7bdc466b 100644 --- a/projects/SelfTest/UsageTests/Matchers.tests.cpp +++ b/projects/SelfTest/UsageTests/Matchers.tests.cpp @@ -32,6 +32,9 @@ namespace { namespace MatchersTests { 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 #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(alwaysTrue, "always true")); + REQUIRE_THAT(1, !Predicate(alwaysFalse, "always false")); + } + SECTION("Lambdas + different type") { + REQUIRE_THAT("Hello olleH", + Predicate( + [] (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 const& str) -> bool { return str.front() == str.back(); } + ) + ); + } + } + } } // namespace MatchersTests #endif // CATCH_CONFIG_DISABLE_MATCHERS