diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d2c41fa4..82c9b6a5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -73,6 +73,7 @@ set(INTERNAL_HEADERS ${SOURCES_DIR}/catch_leak_detector.h ${SOURCES_DIR}/catch_list.h ${SOURCES_DIR}/matchers/catch_matchers.hpp + ${SOURCES_DIR}/matchers/catch_matchers_contains.hpp ${SOURCES_DIR}/matchers/catch_matchers_exception.hpp ${SOURCES_DIR}/matchers/catch_matchers_floating.hpp ${SOURCES_DIR}/matchers/catch_matchers_generic.hpp diff --git a/src/catch2/matchers/catch_matchers_contains.hpp b/src/catch2/matchers/catch_matchers_contains.hpp new file mode 100644 index 00000000..0fcd80a6 --- /dev/null +++ b/src/catch2/matchers/catch_matchers_contains.hpp @@ -0,0 +1,102 @@ +/* + * 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_CONTAINS_HPP_INCLUDED +#define TWOBLUECUBES_CATCH_MATCHERS_CONTAINS_HPP_INCLUDED + +#include + +#include +#include +#include + +namespace Catch { + namespace Matchers { + //! Matcher for checking that an element in range is equal to specific element + template + class ContainsElementMatcher final : public MatcherGenericBase { + T m_desired; + Equality m_eq; + public: + template + ContainsElementMatcher(T2&& target, Equality2&& predicate): + m_desired(std::forward(target)), + m_eq(std::forward(predicate)) + {} + + std::string describe() const override { + return "contains element " + Catch::Detail::stringify(m_desired); + } + + template + bool match(RangeLike&& rng) const { + using std::begin; using std::end; + + return end(rng) != std::find_if(begin(rng), end(rng), + [&](auto const& elem) { + return m_eq(elem, m_desired); + }); + } + }; + + //! Meta-matcher for checking that an element in a range matches a specific matcher + template + class ContainsMatcherMatcher final : public MatcherGenericBase { + Matcher m_matcher; + public: + // Note that we do a copy+move to avoid having to SFINAE this + // constructor (and also avoid some perfect forwarding failure + // cases) + ContainsMatcherMatcher(Matcher matcher): + m_matcher(std::move(matcher)) + {} + + template + bool match(RangeLike&& rng) const { + using std::begin; using std::endl; + for (auto&& elem : rng) { + if (m_matcher.match(elem)) { + return true; + } + } + return false; + } + + std::string describe() const override { + return "contains element matching " + m_matcher.describe(); + } + }; + + /** + * Creates a matcher that checks whether a range contains a specific element. + * + * Uses `std::equal_to` to do the comparison + */ + template + std::enable_if_t::value, + ContainsElementMatcher>> Contains(T&& elem) { + return { std::forward(elem), std::equal_to<>{} }; + } + + //! Creates a matcher that checks whether a range contains element matching a matcher + template + std::enable_if_t::value, + ContainsMatcherMatcher> Contains(Matcher&& matcher) { + return { std::forward(matcher) }; + } + + /** + * Creates a matcher that checks whether a range contains a specific element. + * + * Uses `eq` to do the comparisons + */ + template + ContainsElementMatcher Contains(T&& elem, Equality&& eq) { + return { std::forward(elem), std::forward(eq) }; + } + + } +} + +#endif // TWOBLUECUBES_CATCH_MATCHERS_CONTAINS_HPP_INCLUDED diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 642f8f71..0f5142a3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -50,6 +50,7 @@ set(TEST_SOURCES ${SELF_TEST_DIR}/UsageTests/ToStringWhich.tests.cpp ${SELF_TEST_DIR}/UsageTests/Tricky.tests.cpp ${SELF_TEST_DIR}/UsageTests/VariadicMacros.tests.cpp + ${SELF_TEST_DIR}/UsageTests/MatchersRanges.tests.cpp ${SELF_TEST_DIR}/UsageTests/Matchers.tests.cpp ) CheckFileList(TEST_SOURCES ${SELF_TEST_DIR}) diff --git a/tests/SelfTest/Baselines/automake.sw.approved.txt b/tests/SelfTest/Baselines/automake.sw.approved.txt index db7f4006..aee52e51 100644 --- a/tests/SelfTest/Baselines/automake.sw.approved.txt +++ b/tests/SelfTest/Baselines/automake.sw.approved.txt @@ -80,6 +80,7 @@ Nor would this :test-result: PASS Approximate comparisons with mixed numeric types :test-result: PASS Arbitrary predicate matcher :test-result: PASS Assertions then sections +:test-result: PASS Basic use of the Contains range matcher :test-result: PASS CAPTURE can deal with complex expressions :test-result: PASS CAPTURE can deal with complex expressions involving commas :test-result: PASS CAPTURE parses string and character constants diff --git a/tests/SelfTest/Baselines/compact.sw.approved.txt b/tests/SelfTest/Baselines/compact.sw.approved.txt index eb8ccc42..74bf50b7 100644 --- a/tests/SelfTest/Baselines/compact.sw.approved.txt +++ b/tests/SelfTest/Baselines/compact.sw.approved.txt @@ -244,6 +244,18 @@ Tricky.tests.cpp:: passed: true Tricky.tests.cpp:: passed: true Tricky.tests.cpp:: passed: true Tricky.tests.cpp:: passed: true +MatchersRanges.tests.cpp:: passed: a, Contains(1) for: { 1, 2, 3 } contains element 1 +MatchersRanges.tests.cpp:: passed: b, Contains(1) for: { 0, 1, 2 } contains element 1 +MatchersRanges.tests.cpp:: passed: c, !Contains(1) for: { 4, 5, 6 } not contains element 1 +MatchersRanges.tests.cpp:: passed: a, Contains(0, close_enough) for: { 1, 2, 3 } contains element 0 +MatchersRanges.tests.cpp:: passed: b, Contains(0, close_enough) for: { 0, 1, 2 } contains element 0 +MatchersRanges.tests.cpp:: passed: c, !Contains(0, close_enough) for: { 4, 5, 6 } not contains element 0 +MatchersRanges.tests.cpp:: passed: a, Contains(4, [](auto&& lhs, size_t sz) { return lhs.size() == sz; }) for: { "abc", "abcd", "abcde" } contains element 4 +MatchersRanges.tests.cpp:: passed: in, Contains(1) for: { 1, 2, 3, 4, 5 } contains element 1 +MatchersRanges.tests.cpp:: passed: in, !Contains(8) for: { 1, 2, 3, 4, 5 } not contains element 8 +MatchersRanges.tests.cpp:: passed: in, Contains(MoveOnlyTestElement{ 2 }) for: { 1, 2, 3 } contains element 2 +MatchersRanges.tests.cpp:: passed: in, !Contains(MoveOnlyTestElement{ 9 }) for: { 1, 2, 3 } not contains element 9 +MatchersRanges.tests.cpp:: passed: in, Contains(Catch::Matchers::WithinAbs(0.5, 0.5)) for: { 1.0, 2.0, 3.0, 0.0 } contains element matching is within 0.5 of 0.5 Message.tests.cpp:: passed: with 7 messages: 'a := 1' and 'b := 2' and 'c := 3' and 'a + b := 3' and 'a+b := 3' and 'c > b := true' and 'a == 1 := true' Message.tests.cpp:: passed: with 7 messages: 'std::vector{1, 2, 3}[0, 1, 2] := 3' and 'std::vector{1, 2, 3}[(0, 1)] := 2' and 'std::vector{1, 2, 3}[0] := 1' and '(helper_1436{12, -12}) := { 12, -12 }' and '(helper_1436(-12, 12)) := { -12, 12 }' and '(1, 2) := 2' and '(2, 3) := 3' Message.tests.cpp:: passed: with 11 messages: '("comma, in string", "escaped, \", ") := "escaped, ", "' and '"single quote in string,'," := "single quote in string,',"' and '"some escapes, \\,\\\\" := "some escapes, \,\\"' and '"some, ), unmatched, } prenheses {[<" := "some, ), unmatched, } prenheses {[<"' and ''"' := '"'' and ''\'' := '''' and '',' := ','' and ''}' := '}'' and '')' := ')'' and ''(' := '('' and ''{' := '{'' diff --git a/tests/SelfTest/Baselines/console.std.approved.txt b/tests/SelfTest/Baselines/console.std.approved.txt index cc6328e8..339d9b0d 100644 --- a/tests/SelfTest/Baselines/console.std.approved.txt +++ b/tests/SelfTest/Baselines/console.std.approved.txt @@ -1380,6 +1380,6 @@ due to unexpected exception with message: Why would you throw a std::string? =============================================================================== -test cases: 328 | 254 passed | 70 failed | 4 failed as expected -assertions: 1839 | 1687 passed | 131 failed | 21 failed as expected +test cases: 329 | 255 passed | 70 failed | 4 failed as expected +assertions: 1851 | 1699 passed | 131 failed | 21 failed as expected diff --git a/tests/SelfTest/Baselines/console.sw.approved.txt b/tests/SelfTest/Baselines/console.sw.approved.txt index 243ab6f9..ad53af5d 100644 --- a/tests/SelfTest/Baselines/console.sw.approved.txt +++ b/tests/SelfTest/Baselines/console.sw.approved.txt @@ -1948,6 +1948,108 @@ Tricky.tests.cpp: Tricky.tests.cpp:: PASSED: REQUIRE( true ) +------------------------------------------------------------------------------- +Basic use of the Contains range matcher + Different argument ranges, same element type, default comparison +------------------------------------------------------------------------------- +MatchersRanges.tests.cpp: +............................................................................... + +MatchersRanges.tests.cpp:: PASSED: + REQUIRE_THAT( a, Contains(1) ) +with expansion: + { 1, 2, 3 } contains element 1 + +MatchersRanges.tests.cpp:: PASSED: + REQUIRE_THAT( b, Contains(1) ) +with expansion: + { 0, 1, 2 } contains element 1 + +MatchersRanges.tests.cpp:: PASSED: + REQUIRE_THAT( c, !Contains(1) ) +with expansion: + { 4, 5, 6 } not contains element 1 + +------------------------------------------------------------------------------- +Basic use of the Contains range matcher + Different argument ranges, same element type, custom comparison +------------------------------------------------------------------------------- +MatchersRanges.tests.cpp: +............................................................................... + +MatchersRanges.tests.cpp:: PASSED: + REQUIRE_THAT( a, Contains(0, close_enough) ) +with expansion: + { 1, 2, 3 } contains element 0 + +MatchersRanges.tests.cpp:: PASSED: + REQUIRE_THAT( b, Contains(0, close_enough) ) +with expansion: + { 0, 1, 2 } contains element 0 + +MatchersRanges.tests.cpp:: PASSED: + REQUIRE_THAT( c, !Contains(0, close_enough) ) +with expansion: + { 4, 5, 6 } not contains element 0 + +------------------------------------------------------------------------------- +Basic use of the Contains range matcher + Different element type, custom comparisons +------------------------------------------------------------------------------- +MatchersRanges.tests.cpp: +............................................................................... + +MatchersRanges.tests.cpp:: PASSED: + REQUIRE_THAT( a, Contains(4, [](auto&& lhs, size_t sz) { return lhs.size() == sz; }) ) +with expansion: + { "abc", "abcd", "abcde" } contains element 4 + +------------------------------------------------------------------------------- +Basic use of the Contains range matcher + Can handle type that requires ADL-found free function begin and end +------------------------------------------------------------------------------- +MatchersRanges.tests.cpp: +............................................................................... + +MatchersRanges.tests.cpp:: PASSED: + REQUIRE_THAT( in, Contains(1) ) +with expansion: + { 1, 2, 3, 4, 5 } contains element 1 + +MatchersRanges.tests.cpp:: PASSED: + REQUIRE_THAT( in, !Contains(8) ) +with expansion: + { 1, 2, 3, 4, 5 } not contains element 8 + +------------------------------------------------------------------------------- +Basic use of the Contains range matcher + Initialization with move only types +------------------------------------------------------------------------------- +MatchersRanges.tests.cpp: +............................................................................... + +MatchersRanges.tests.cpp:: PASSED: + REQUIRE_THAT( in, Contains(MoveOnlyTestElement{ 2 }) ) +with expansion: + { 1, 2, 3 } contains element 2 + +MatchersRanges.tests.cpp:: PASSED: + REQUIRE_THAT( in, !Contains(MoveOnlyTestElement{ 9 }) ) +with expansion: + { 1, 2, 3 } not contains element 9 + +------------------------------------------------------------------------------- +Basic use of the Contains range matcher + Matching using matcher +------------------------------------------------------------------------------- +MatchersRanges.tests.cpp: +............................................................................... + +MatchersRanges.tests.cpp:: PASSED: + REQUIRE_THAT( in, Contains(Catch::Matchers::WithinAbs(0.5, 0.5)) ) +with expansion: + { 1.0, 2.0, 3.0, 0.0 } contains element matching is within 0.5 of 0.5 + ------------------------------------------------------------------------------- CAPTURE can deal with complex expressions ------------------------------------------------------------------------------- @@ -14396,6 +14498,6 @@ Misc.tests.cpp: Misc.tests.cpp:: PASSED: =============================================================================== -test cases: 328 | 238 passed | 86 failed | 4 failed as expected -assertions: 1856 | 1687 passed | 148 failed | 21 failed as expected +test cases: 329 | 239 passed | 86 failed | 4 failed as expected +assertions: 1868 | 1699 passed | 148 failed | 21 failed as expected diff --git a/tests/SelfTest/Baselines/junit.sw.approved.txt b/tests/SelfTest/Baselines/junit.sw.approved.txt index f3d7ba04..c1dedc83 100644 --- a/tests/SelfTest/Baselines/junit.sw.approved.txt +++ b/tests/SelfTest/Baselines/junit.sw.approved.txt @@ -1,7 +1,7 @@ - + @@ -342,6 +342,12 @@ Exception.tests.cpp: + + + + + + diff --git a/tests/SelfTest/Baselines/sonarqube.sw.approved.txt b/tests/SelfTest/Baselines/sonarqube.sw.approved.txt index c554f8d9..b89ec533 100644 --- a/tests/SelfTest/Baselines/sonarqube.sw.approved.txt +++ b/tests/SelfTest/Baselines/sonarqube.sw.approved.txt @@ -1215,6 +1215,14 @@ Matchers.tests.cpp: + + + + + + + + diff --git a/tests/SelfTest/Baselines/tap.sw.approved.txt b/tests/SelfTest/Baselines/tap.sw.approved.txt index 85a67157..61876cff 100644 --- a/tests/SelfTest/Baselines/tap.sw.approved.txt +++ b/tests/SelfTest/Baselines/tap.sw.approved.txt @@ -486,6 +486,30 @@ ok {test-number} - true ok {test-number} - true # Assertions then sections ok {test-number} - true +# Basic use of the Contains range matcher +ok {test-number} - a, Contains(1) for: { 1, 2, 3 } contains element 1 +# Basic use of the Contains range matcher +ok {test-number} - b, Contains(1) for: { 0, 1, 2 } contains element 1 +# Basic use of the Contains range matcher +ok {test-number} - c, !Contains(1) for: { 4, 5, 6 } not contains element 1 +# Basic use of the Contains range matcher +ok {test-number} - a, Contains(0, close_enough) for: { 1, 2, 3 } contains element 0 +# Basic use of the Contains range matcher +ok {test-number} - b, Contains(0, close_enough) for: { 0, 1, 2 } contains element 0 +# Basic use of the Contains range matcher +ok {test-number} - c, !Contains(0, close_enough) for: { 4, 5, 6 } not contains element 0 +# Basic use of the Contains range matcher +ok {test-number} - a, Contains(4, [](auto&& lhs, size_t sz) { return lhs.size() == sz; }) for: { "abc", "abcd", "abcde" } contains element 4 +# Basic use of the Contains range matcher +ok {test-number} - in, Contains(1) for: { 1, 2, 3, 4, 5 } contains element 1 +# Basic use of the Contains range matcher +ok {test-number} - in, !Contains(8) for: { 1, 2, 3, 4, 5 } not contains element 8 +# Basic use of the Contains range matcher +ok {test-number} - in, Contains(MoveOnlyTestElement{ 2 }) for: { 1, 2, 3 } contains element 2 +# Basic use of the Contains range matcher +ok {test-number} - in, !Contains(MoveOnlyTestElement{ 9 }) for: { 1, 2, 3 } not contains element 9 +# Basic use of the Contains range matcher +ok {test-number} - in, Contains(Catch::Matchers::WithinAbs(0.5, 0.5)) for: { 1.0, 2.0, 3.0, 0.0 } contains element matching is within 0.5 of 0.5 # CAPTURE can deal with complex expressions ok {test-number} - with 7 messages: 'a := 1' and 'b := 2' and 'c := 3' and 'a + b := 3' and 'a+b := 3' and 'c > b := true' and 'a == 1 := true' # CAPTURE can deal with complex expressions involving commas @@ -3704,5 +3728,5 @@ ok {test-number} - q3 == 23. for: 23.0 == 23.0 ok {test-number} - # xmlentitycheck ok {test-number} - -1..1848 +1..1860 diff --git a/tests/SelfTest/Baselines/teamcity.sw.approved.txt b/tests/SelfTest/Baselines/teamcity.sw.approved.txt index dbfb9835..b0566b41 100644 --- a/tests/SelfTest/Baselines/teamcity.sw.approved.txt +++ b/tests/SelfTest/Baselines/teamcity.sw.approved.txt @@ -193,6 +193,8 @@ Exception.tests.cpp:|nunexpected exception with message:|n "unexpe ##teamcity[testFinished name='Arbitrary predicate matcher' duration="{duration}"] ##teamcity[testStarted name='Assertions then sections'] ##teamcity[testFinished name='Assertions then sections' duration="{duration}"] +##teamcity[testStarted name='Basic use of the Contains range matcher'] +##teamcity[testFinished name='Basic use of the Contains range matcher' duration="{duration}"] ##teamcity[testStarted name='CAPTURE can deal with complex expressions'] ##teamcity[testFinished name='CAPTURE can deal with complex expressions' duration="{duration}"] ##teamcity[testStarted name='CAPTURE can deal with complex expressions involving commas'] diff --git a/tests/SelfTest/Baselines/xml.sw.approved.txt b/tests/SelfTest/Baselines/xml.sw.approved.txt index bbd37d53..31f16fc0 100644 --- a/tests/SelfTest/Baselines/xml.sw.approved.txt +++ b/tests/SelfTest/Baselines/xml.sw.approved.txt @@ -2178,6 +2178,123 @@ Nor would this + +
+ + + a, Contains(1) + + + { 1, 2, 3 } contains element 1 + + + + + b, Contains(1) + + + { 0, 1, 2 } contains element 1 + + + + + c, !Contains(1) + + + { 4, 5, 6 } not contains element 1 + + + +
+
+ + + a, Contains(0, close_enough) + + + { 1, 2, 3 } contains element 0 + + + + + b, Contains(0, close_enough) + + + { 0, 1, 2 } contains element 0 + + + + + c, !Contains(0, close_enough) + + + { 4, 5, 6 } not contains element 0 + + + +
+
+ + + a, Contains(4, [](auto&& lhs, size_t sz) { return lhs.size() == sz; }) + + + { "abc", "abcd", "abcde" } contains element 4 + + + +
+
+ + + in, Contains(1) + + + { 1, 2, 3, 4, 5 } contains element 1 + + + + + in, !Contains(8) + + + { 1, 2, 3, 4, 5 } not contains element 8 + + + +
+
+ + + in, Contains(MoveOnlyTestElement{ 2 }) + + + { 1, 2, 3 } contains element 2 + + + + + in, !Contains(MoveOnlyTestElement{ 9 }) + + + { 1, 2, 3 } not contains element 9 + + + +
+
+ + + in, Contains(Catch::Matchers::WithinAbs(0.5, 0.5)) + + + { 1.0, 2.0, 3.0, 0.0 } contains element matching is within 0.5 of 0.5 + + + +
+ +
a := 1 @@ -17264,7 +17381,7 @@ loose text artifact - + - + diff --git a/tests/SelfTest/UsageTests/MatchersRanges.tests.cpp b/tests/SelfTest/UsageTests/MatchersRanges.tests.cpp new file mode 100644 index 00000000..7cf5590b --- /dev/null +++ b/tests/SelfTest/UsageTests/MatchersRanges.tests.cpp @@ -0,0 +1,107 @@ +// 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) + +#include +#include +#include + +#include +#include +#include +#include + +namespace unrelated { + class needs_ADL_begin { + std::array elements{ {1, 2, 3, 4, 5} }; + public: + using iterator = std::array::iterator; + using const_iterator = std::array::const_iterator; + + const_iterator Begin() const { return elements.begin(); } + const_iterator End() const { return elements.end(); } + + friend const_iterator begin(needs_ADL_begin const& lhs) { + return lhs.Begin(); + } + friend const_iterator end(needs_ADL_begin const& rhs) { + return rhs.End(); + } + }; +} // end anon namespace + +struct MoveOnlyTestElement { + int num = 0; + MoveOnlyTestElement(int n) :num(n) {} + + MoveOnlyTestElement(MoveOnlyTestElement&& rhs) = default; + MoveOnlyTestElement& operator=(MoveOnlyTestElement&& rhs) = default; + + friend bool operator==(MoveOnlyTestElement const& lhs, MoveOnlyTestElement const& rhs) { + return lhs.num == rhs.num; + } + + friend std::ostream& operator<<(std::ostream& out, MoveOnlyTestElement const& elem) { + out << elem.num; + return out; + } +}; + +TEST_CASE("Basic use of the Contains range matcher", "[matchers][templated][contains]") { + using Catch::Matchers::Contains; + + SECTION("Different argument ranges, same element type, default comparison") { + std::array a{ { 1,2,3 } }; + std::vector b{ 0,1,2 }; + std::list c{ 4,5,6 }; + + // A contains 1 + REQUIRE_THAT(a, Contains(1)); + // B contains 1 + REQUIRE_THAT(b, Contains(1)); + // C does not contain 1 + REQUIRE_THAT(c, !Contains(1)); + } + + SECTION("Different argument ranges, same element type, custom comparison") { + std::array a{ { 1,2,3 } }; + std::vector b{ 0,1,2 }; + std::list c{ 4,5,6 }; + + auto close_enough = [](int lhs, int rhs) { return std::abs(lhs - rhs) <= 1; }; + + // A contains 1, which is "close enough" to 0 + REQUIRE_THAT(a, Contains(0, close_enough)); + // B contains 0 directly + REQUIRE_THAT(b, Contains(0, close_enough)); + // C does not contain anything "close enough" to 0 + REQUIRE_THAT(c, !Contains(0, close_enough)); + } + + SECTION("Different element type, custom comparisons") { + std::array a{ { "abc", "abcd" , "abcde" } }; + + REQUIRE_THAT(a, Contains(4, [](auto&& lhs, size_t sz) { + return lhs.size() == sz; + })); + } + + SECTION("Can handle type that requires ADL-found free function begin and end") { + unrelated::needs_ADL_begin in; + + REQUIRE_THAT(in, Contains(1)); + REQUIRE_THAT(in, !Contains(8)); + } + + SECTION("Initialization with move only types") { + std::array in{ { MoveOnlyTestElement{ 1 }, MoveOnlyTestElement{ 2 }, MoveOnlyTestElement{ 3 } } }; + + REQUIRE_THAT(in, Contains(MoveOnlyTestElement{ 2 })); + REQUIRE_THAT(in, !Contains(MoveOnlyTestElement{ 9 })); + } + + SECTION("Matching using matcher") { + std::array in{ {1, 2, 3} }; + + REQUIRE_THAT(in, Contains(Catch::Matchers::WithinAbs(0.5, 0.5))); + } +}