diff --git a/googlemock/include/gmock/gmock-matchers.h b/googlemock/include/gmock/gmock-matchers.h index 2f49371a6..bdd8084d5 100644 --- a/googlemock/include/gmock/gmock-matchers.h +++ b/googlemock/include/gmock/gmock-matchers.h @@ -3944,6 +3944,114 @@ class [[nodiscard]] UnorderedElementsAreMatcherImpl ::std::vector> matchers_; }; +// Implements ContainsSubequence(). +template +class [[nodiscard]] ContainsSubsequenceMatcherImpl + : public MatcherInterface { + public: + typedef GTEST_REMOVE_REFERENCE_AND_CONST_(Container) RawContainer; + typedef internal::StlContainerView View; + typedef typename View::type StlContainer; + typedef typename View::const_reference StlContainerReference; + typedef typename internal::RangeTraits::value_type Element; + + // Constructs the matcher from a sequence of element values or + // element matchers. + template + ContainsSubsequenceMatcherImpl(InputIter first, InputIter last) { + std::copy(first, last, std::back_inserter(matchers_)); + } + + // Describes what this matcher does. + void DescribeTo(::std::ostream* os) const override { + if (matchers_.size() == 0) { + *os << "contains an empty sequence"; + return; + } + *os << "contains in order a subsequence of elements that matches: "; + for (size_t i = 0; i != matchers_.size(); ++i) { + if (i > 0) { + *os << ", then "; + } + matchers_[i].DescribeTo(os); + } + } + + // Describes what the negation of this matcher does. + void DescribeNegationTo(::std::ostream* os) const override { + if (matchers_.size() == 0) { + *os << "does not contain an empty sequence"; + return; + } + *os << "does not contain in order a subsequence of elements that matches "; + for (size_t i = 0; i != matchers_.size(); ++i) { + if (i > 0) { + *os << ", then "; + } + matchers_[i].DescribeTo(os); + } + } + + bool MatchAndExplain(Container container, + MatchResultListener* listener) const override { + StlContainerReference stl_container = View::ConstReference(container); + + size_t num_matches = 0; + // Track which elements match which matcher. + size_t num_elements_examined = 0; + std::vector match_indices; + for (const auto& element : stl_container) { + if (num_matches == matchers_.size()) { + break; + } + StringMatchResultListener inner_listener; + if (matchers_[num_matches].MatchAndExplain(element, &inner_listener)) { + ++num_matches; + match_indices.push_back(num_elements_examined); + } + + ++num_elements_examined; + } + + if (num_matches < matchers_.size()) { + // We provide an explanation of the first matcher that failed to match + // when trying to match the subsequence greedily. A better approach would + // be to compute the longest common subsequence (LCS) between the + // elements and the matchers and then provide explanations for any + // remaining matchers and elements that couldn't match each other, but + // this introduces a fair bit of complexity. + if (listener->IsInterested()) { + for (size_t i = 0; i < match_indices.size(); ++i) { + *listener << "found match for matcher #" << i + << " with element at position #" << match_indices[i] + << ", "; + } + + if (num_matches > 0) { + *listener << "but "; + } + + *listener << "could not find a match for matcher #" << num_matches + << " ("; + matchers_[num_matches].DescribeTo(listener->stream()); + *listener << ")"; + + if (num_matches > 0) { + *listener << " after the last match at position #" + << match_indices[num_matches - 1]; + } + } + + return false; + } + + return true; + } + + private: + ::std::vector> matchers_; +}; + // Functor for use in TransformTuple. // Performs MatcherCast on an input argument of any type. template @@ -3954,6 +4062,33 @@ struct CastAndAppendTransform { } }; +// Implements ContainsSubsequence. +template +class [[nodiscard]] ContainsSubsequenceMatcher { + public: + explicit ContainsSubsequenceMatcher(const MatcherTuple& args) + : matchers_(args) {} + + template + // NOLINTNEXTLINE(google-explicit-constructor) + operator Matcher() const { + typedef GTEST_REMOVE_REFERENCE_AND_CONST_(Container) RawContainer; + typedef typename internal::StlContainerView::type View; + typedef typename internal::RangeTraits::value_type Element; + typedef ::std::vector> MatcherVec; + MatcherVec matchers; + matchers.reserve(::std::tuple_size::value); + TransformTupleValues(CastAndAppendTransform(), matchers_, + ::std::back_inserter(matchers)); + return Matcher( + new ContainsSubsequenceMatcherImpl(matchers.begin(), + matchers.end())); + } + + private: + const MatcherTuple matchers_; +}; + // Implements UnorderedElementsAre. template class [[nodiscard]] UnorderedElementsAreMatcher { @@ -5422,6 +5557,18 @@ UnorderedElementsAre(const Args&... matchers) { std::make_tuple(matchers...)); } +// ContainsSubsequence(m1, m2, ..., mk) matches a container that contains +// elements that match m1, m2, ..., mk in that order with possible gaps +// between them. +template +internal::ContainsSubsequenceMatcher< + ::std::tuple::type...>> +ContainsSubsequence(const Args&... matchers) { + return internal::ContainsSubsequenceMatcher< + ::std::tuple::type...>>( + ::std::make_tuple(matchers...)); +} + // Define variadic matcher versions. template internal::AllOfMatcher::type...> AllOf( diff --git a/googlemock/test/gmock-matchers-containers_test.cc b/googlemock/test/gmock-matchers-containers_test.cc index a5e1aebb1..697509f71 100644 --- a/googlemock/test/gmock-matchers-containers_test.cc +++ b/googlemock/test/gmock-matchers-containers_test.cc @@ -3439,6 +3439,78 @@ TEST(ContainsTest, WorksForTwoDimensionalNativeArray) { EXPECT_THAT(a, Contains(Not(Contains(5)))); } +// Tests ContainsSubsequence(). + +TEST(ContainsSubsequenceTest, WorksForNativeArray) { + const int a[] = {1, 2, 3, 4, 5}; + EXPECT_THAT(a, ContainsSubsequence(1, 3, 4)); + EXPECT_THAT(a, Not(ContainsSubsequence(1, 3, 2))); +} + +TEST(ContainsSubsequenceTest, AcceptsMatcher) { + const int a[] = {1, 2, 3, 4, 5}; + EXPECT_THAT(a, ContainsSubsequence(Eq(1), Gt(3), Gt(4))); + EXPECT_THAT(a, Not(ContainsSubsequence(1, Gt(3), Lt(3)))); +} + +TEST(ContainsSubsequenceTest, WorksForTwoDimensionalNativeArray) { + int a[][3] = {{1, 2, 3}, {7, 8, 9}, {4, 5, 6}}; + EXPECT_THAT(a, ContainsSubsequence(ElementsAre(1, 2, 3), Contains(4))); + EXPECT_THAT(a, + Not(ContainsSubsequence(Contains(1), Contains(8), Contains(9)))); +} + +TEST(ContainsSubsequenceTest, WorksForVector) { + const vector a = {1, 2, 3, 4, 5}; + EXPECT_THAT(a, ContainsSubsequence(1, 3, 4)); + EXPECT_THAT(a, Not(ContainsSubsequence(1, 3, 2))); +} + +TEST(ContainsSubsequenceTest, WorksForEmptySmallSizedSubsequences) { + const int a[] = {1, 2, 3, 4, 5}; + EXPECT_THAT(a, ContainsSubsequence()); + EXPECT_THAT(a, ContainsSubsequence(Gt(4))); + EXPECT_THAT(a, Not(ContainsSubsequence(Gt(6)))); + EXPECT_THAT(a, ContainsSubsequence(Lt(2), Gt(3))); + EXPECT_THAT(a, Not(ContainsSubsequence(Lt(2), Lt(2)))); +} + +TEST(ContainsSubsequenceTest, DescribesItselfCorrectly) { + Matcher m = ContainsSubsequence(1, 3, 4); + EXPECT_EQ( + "contains in order a subsequence of elements that matches: is equal to " + "1, then is equal to 3, then is equal to 4", + Describe(m)); + m = ContainsSubsequence(Eq(1), Gt(3), Gt(4)); + EXPECT_EQ( + "contains in order a subsequence of elements that matches: is equal to " + "1, then is > 3, then is > 4", + Describe(m)); + + m = Not(ContainsSubsequence(1, 3, 4)); + EXPECT_EQ( + "does not contain in order a subsequence of elements that matches is " + "equal to 1, then is equal to 3, then is equal to 4", + Describe(m)); +} + +TEST(ContainsSubsequenceTest, ExplainsMismatchCorrectlyForSingleMatcher) { + const int a[] = {1, 2, 3, 4, 5}; + Matcher m = ContainsSubsequence(Eq(6)); + EXPECT_EQ(Explain(m, a), + "could not find a match for matcher #0 (is equal to 6)"); +} + +TEST(ContainsSubsequenceTest, ExplainsMismatchCorrectlyForMultipleMatchers) { + const int a[] = {1, 2, 3, 4, 5}; + Matcher m = ContainsSubsequence(Eq(2), Gt(4), Gt(4)); + EXPECT_EQ( + Explain(m, a), + "found match for matcher #0 with element at position #1, found match for " + "matcher #1 with element at position #4, but could not find a match for " + "matcher #2 (is > 4) after the last match at position #4"); +} + } // namespace } // namespace gmock_matchers_test } // namespace testing