Introduce ContainsSubsequence matcher.

Subsequences are defined as: "a subsequence of a given sequence is a sequence that can be derived from the given sequence by deleting some or no elements without changing the order of the remaining elements."
See: https://en.wikipedia.org/wiki/Subsequence

This new matcher checks if a container contains elements that match a given sequence of matchers in the specified order, but not necessarily contiguously.
The implementation is very similar to other matchers like ElementsAre or Contains.

PiperOrigin-RevId: 918323422
Change-Id: I56d7ebbe6f81038c93546ef7585db59eea5dbd57
This commit is contained in:
Abseil Team 2026-05-20 02:39:23 -07:00 committed by Copybara-Service
parent dc3c9eda2f
commit add971c7cb
2 changed files with 219 additions and 0 deletions

View File

@ -3944,6 +3944,114 @@ class [[nodiscard]] UnorderedElementsAreMatcherImpl
::std::vector<Matcher<const Element&>> matchers_;
};
// Implements ContainsSubequence().
template <typename Container>
class [[nodiscard]] ContainsSubsequenceMatcherImpl
: public MatcherInterface<Container> {
public:
typedef GTEST_REMOVE_REFERENCE_AND_CONST_(Container) RawContainer;
typedef internal::StlContainerView<RawContainer> View;
typedef typename View::type StlContainer;
typedef typename View::const_reference StlContainerReference;
typedef typename internal::RangeTraits<StlContainer>::value_type Element;
// Constructs the matcher from a sequence of element values or
// element matchers.
template <typename InputIter>
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<size_t> 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<Matcher<const Element&>> matchers_;
};
// Functor for use in TransformTuple.
// Performs MatcherCast<Target> on an input argument of any type.
template <typename Target>
@ -3954,6 +4062,33 @@ struct CastAndAppendTransform {
}
};
// Implements ContainsSubsequence.
template <typename MatcherTuple>
class [[nodiscard]] ContainsSubsequenceMatcher {
public:
explicit ContainsSubsequenceMatcher(const MatcherTuple& args)
: matchers_(args) {}
template <typename Container>
// NOLINTNEXTLINE(google-explicit-constructor)
operator Matcher<Container>() const {
typedef GTEST_REMOVE_REFERENCE_AND_CONST_(Container) RawContainer;
typedef typename internal::StlContainerView<RawContainer>::type View;
typedef typename internal::RangeTraits<View>::value_type Element;
typedef ::std::vector<Matcher<const Element&>> MatcherVec;
MatcherVec matchers;
matchers.reserve(::std::tuple_size<MatcherTuple>::value);
TransformTupleValues(CastAndAppendTransform<const Element&>(), matchers_,
::std::back_inserter(matchers));
return Matcher<Container>(
new ContainsSubsequenceMatcherImpl<const Container&>(matchers.begin(),
matchers.end()));
}
private:
const MatcherTuple matchers_;
};
// Implements UnorderedElementsAre.
template <typename MatcherTuple>
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 <typename... Args>
internal::ContainsSubsequenceMatcher<
::std::tuple<typename ::std::decay<const Args&>::type...>>
ContainsSubsequence(const Args&... matchers) {
return internal::ContainsSubsequenceMatcher<
::std::tuple<typename ::std::decay<const Args&>::type...>>(
::std::make_tuple(matchers...));
}
// Define variadic matcher versions.
template <typename... Args>
internal::AllOfMatcher<typename std::decay<const Args&>::type...> AllOf(

View File

@ -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<int> 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<const int (&)[5]> 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<const int (&)[5]> 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<const int (&)[5]> 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