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:49 -07:00
committed by Copybara-Service
parent dc3c9eda2f
commit add971c7cb
2 changed files with 219 additions and 0 deletions
+147
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(