Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Composed matchers #340

Closed
chghehe opened this issue Aug 26, 2024 · 15 comments
Closed

Composed matchers #340

chghehe opened this issue Aug 26, 2024 · 15 comments

Comments

@chghehe
Copy link

chghehe commented Aug 26, 2024

There are no useful composed matchers like the ones in GMock: ElementsAre, AllOf, Field etc. This might be easily implemented using C++11/14. Here is C++20 implementation which I use in my code (C++14 requires a little bit more code for that):

template<typename... Args>
auto ElementsAre(Args const&... args)
{
  return trompeloeil::make_matcher<trompeloeil::wildcard>(
    [](std::ranges::range auto const& rng, auto&&... args) -> bool
      {
        auto it = std::begin(rng);
        auto const res = ([&it](auto&& arg) -> bool {
            if(it == std::end(rng))
              return false;
            auto const& x = *(it++);
            if constexpr (trompeloeil::is_matcher<std::decay_t<decltype(arg)>>::value)
              return arg.matches(x);
            else
              return arg == x;
          }(args) and ...);

          return res and it == std::end(rng);
      },
    [](std::ostream& os, auto&&... args)
      {
        os << " elements are [";
        char const* prefix = " ";
        ([&](auto&& arg) {
          os << prefix;
          trompeloeil::print(os, arg);
          prefix = ", ";
        }(args), ...);

        os << " ]";
      },
      args...
    );
}
@rollbear
Copy link
Owner

This is an interesting idea. I have very deliberately refrained from adding such things, because I think GMock suffers from having a too large API that no one can remember, but your example is undoubtedly very sweet. Thank you.

Do you have a wish-list for matchers?

@rollbear
Copy link
Owner

Have a look at branch range_matchers and let me know what you think.

@chghehe
Copy link
Author

chghehe commented Sep 23, 2024

Hey!

Do you have a wish-list for matchers?

Basically, all of GMock matchers are quite universal and useful. These matchers make mock-testing significantly easier. This is the only reason I sometimes prefer GMock.

@chghehe
Copy link
Author

chghehe commented Sep 23, 2024

Have a look at branch range_matchers and let me know what you think.

Thank you!

Just checked the branch:

  • range_is_any is missing (matches at least one element)
  • Unordered range matcher is missing but it is strongly required when you compare unordered containers.
  • Maybe some subset matchers are needed.

But I was originally referring to composed matchers, not only range matchers. Making complex matchers from the basic and simple ones would be a great feature.
Let's say we have a range of structures. Sure, we can match this range by value(s), but what if you need to match only some subset of fields? (This was a case I decided to create this topic).
Or, for instance, we have a range of pointers and we want to match values they points to, but not the pointers themselves.
These cases are quite common.

Thus, matchers like Pointee, Field (as well as Property), AllOf, AnyOf, NoneOf are quite important for composing purposes.

In my initial example we can do such things (and I do in my code):

struct S { int x; int y; };

ElementsAre(
  AllOf(Field(&S::x, 10), Field(&S::y, 20)),
  AllOf(Field(&S::x, 100))
  )

@chghehe
Copy link
Author

chghehe commented Sep 23, 2024

Here is my Field implementation:

template<typename T, typename C, typename M>
auto Field(
  T C::*member,
  M const& matcher,
  std::string const& member_name)
{
  return trompeloeil::make_matcher<trompeloeil::wildcard>(
    [](auto const& obj, auto const& /*name*/, auto const& member, M const& matcher) -> bool
      {
        if constexpr (trompeloeil::is_matcher<M>::value)
          return matcher.matches(std::invoke(member, obj));
        else
          return matcher == std::invoke(member, obj);
      },
    [](std::ostream& os, auto const& member_name, auto const& member, M const& matcher)
      {
        os << " matching " << member_name << " with ";
        trompeloeil::print(os, matcher);
      },
    member_name,
    member,
    matcher
  );
}

Probably, the name member_is is better than Field, and member_name might be obtained by stringification in a special macro (like MEMBER_IS(T::x, 123)) for convenience.

@rollbear
Copy link
Owner

Thank you. I need to think a bit about the latter. I like the idea.

@rollbear
Copy link
Owner

Added these all. I'm using MEMBER_IS as the name, I think it's clearer. I'm not super happy with the error reporting when they are composed, but I think it's acceptable.

@chghehe
Copy link
Author

chghehe commented Sep 29, 2024

Thank you!

Yeah, description of composed matchers might be quite long, but still informative.
Maybe, for range matchers we can use newlines in the description / multiline description, and print each element on a separate line with some indent (looks quite complex change, especially for recursive range matchers, probably it would be required to change the interface of description makers to pass current indentation)?

And few points about range matchers.

Do they respect mutable range views like std::generator<>? I worked a lot with std::experimental::generator last time (and also created #339 about generators), it is really convenient thing, but testing frameworks are not ready to correctly handle it. According to the code of is_checker, looks like the answer is "no", std::generator<> will not work, because is_checker works with const R&. Btw, for readability, I would rename is_checker -> range_is_checker.

And, I suppose, along with a heterogeneous matcher (which range_is is), we need homogeneous matchers to be able to compare range with another range (ElementsAreArray, UnorderedElementsAreArray in gmock). Thus, I guess to use the name elements_are and unordered_elements_are (unordered is well known name) for element-wise heterogeneous matching, and range_is, unordered_range_is for testing range on equality homogeneously (by now I use .WITH and call std::ranges::equal for these purposes):

  • elements_are(e1, ..., eN) -- matches range argument by elements e1, ..., eN (values and matchers)
  • unordered_elements_are(e1, ..., eN) -- matches range argument by elements e1, ..., eN regardless of the order of elements (values and matchers)
  • each(e) or each_element_is(e) -- matches all elements of the range argument by value e (value or matcher)
  • contains(e1, ..., eN) -- range argument contains different elements e1, ..., eN (values and matchers)
  • range_is(r) or range_eq(r) -- equal-compares range argument with another range r (values only)
  • unordered_range_is(r) or unordered_range_eq(r) -- equal-compares range argument with another range r regardless of the elements order (values only)

For homogeneous matching of ranges we can add extra overloads for iterator-based construction, e.g.: range_is(begin, end). AFAIK, std::ranges works well with C-arrays, but it may require C++20, thus, probably, such overloads should be added as well.

How's that? What do you say?

@rollbear
Copy link
Owner

The current implementation does not work with generators, but most can be made to work. I think range_ends_with would be a major challenge, though.

range_eq(range) is arguably already in the API. It's eq(range). See https://godbolt.org/z/T5zoa1erx

I'll definitely have a stab at making the matchers work with generators, and I'll take your name suggestions into account since I'm not very happy with the names I've used.

I do fear a very unfortunate growth in the API. One of the reasons I originally made Trompeloeil, is that I strongly dislike the very bloated API of gmock (which has become even more bloated in the 10 years since then). Nothing in this branch adds any new functionality. Everything can already be expressed using .WITH(expression). Do these additions address frequent enough uses to warrant the growth of the API?

@chghehe
Copy link
Author

chghehe commented Sep 30, 2024

range_eq(range) is arguably already in the API. It's eq(range). See https://godbolt.org/z/T5zoa1erx

In the example, you equal-compare two std::vector using its operator ==, but not ranges. I have to use exactly the same type as argument has for that. Basically, ranges are compared via std::equal or std::ranges::equal (+unordered versions).

Do these additions address frequent enough uses to warrant the growth of the API?

Unfortunately, .WITH does not provide error descriptions.
I have to write such matchers almost every time I use Trompeloeil. They provide descriptions, they are much more readable and reusable.
Actually, I don't think that GMock API is over-bloated. It is quite rich, functional and usable, while you still may use .With() and simple matchers. But GMock has no built-in integration with testing frameworks other than GTest and a few other restrictions like lack of dynamic expectations (I'm not sure that it is true today).

@rollbear
Copy link
Owner

rollbear commented Oct 5, 2024

They should work with input ranges now, except for "ends_with". I'll try to work on the names tomorrow.

@rollbear
Copy link
Owner

rollbear commented Oct 6, 2024

Phiew, that took a lot of time. The range-matchers are now overloaded to either take matchers as direct arguments, or a range of elements to compare with. Also improved the names a bit (IMO). Curiously, it works fine to pass, e.g. a std::vector of matchers instead of raw values for equality comparison, provided that all matchers are of the same type. I don't really see a use for it, but I don't see a reason to add code to explicitly ban it either.

It does look pretty neat. Thanks for all the help.

@chghehe
Copy link
Author

chghehe commented Oct 6, 2024

Thank you!

@rollbear
Copy link
Owner

rollbear commented Oct 6, 2024

Merged to main. I will leave the issue open until a release has been tagged.

@rollbear
Copy link
Owner

rollbear commented Oct 7, 2024

Included in release v49

@rollbear rollbear closed this as completed Oct 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants
@rollbear @chghehe and others