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

Tracking Issue for assert_matches #82775

Open
5 of 7 tasks
Tracked by #16
m-ou-se opened this issue Mar 4, 2021 · 95 comments · May be fixed by #137487
Open
5 of 7 tasks
Tracked by #16

Tracking Issue for assert_matches #82775

m-ou-se opened this issue Mar 4, 2021 · 95 comments · May be fixed by #137487
Labels
C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. finished-final-comment-period The final comment period is finished for this PR / Issue. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.

Comments

@m-ou-se
Copy link
Member

m-ou-se commented Mar 4, 2021

Feature gate: #![feature(assert_matches)]

This is a tracking issue for the assert_matches!() and debug_assert_matches!() macros.

Public API

// core

macro_rules! assert_matches { .. }
macro_rules! debug_assert_matches { .. }

Steps / History

Unresolved Questions

  • Add => expr syntax?
    • Nope, too confusing. (See discussion below.)
@m-ou-se m-ou-se added T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC labels Mar 4, 2021
@m-ou-se
Copy link
Member Author

m-ou-se commented Mar 9, 2021

Several implementations of assert_matches!() in the ecosystem also accept an => expr syntax, to do something with the matched values or even return something from the macro.

This is currently not supported by std::assert_matches!() as implemented in #82770.

Adding it would allow things like:

let thing = assert_matches!(thing, Some(x) => x);

and

assert_matches!(thing, Some((x, y)) => {
    assert_eq!(x.a, 10);
    assert_eq!(y.b(), 20);
});

@drahnr
Copy link
Contributor

drahnr commented Mar 11, 2021

As a (almost) daily user of the assert_matches crate, I have to admit that without the additional $pat => $expr syntax, for most me it would be still impractical to use.

I.e. asserting a certain sequence of messages with a mocked context and assuring the messages have expected content (which by design may not impl {Partial,}Eq) I would usually use (super simplified):

type A = u8;
struct B(u8);
struct C(u8);
struct D(u8);

enum Msgs {
Specific { a: A, b: B, c: C, d: D},
// snip
}
assert_matches!(rx.next().unwrap(), Msgs::Specific { a, b: B(b), .. } => { assert_eq!(a, b); })
assert_matches!(rx.next().unwrap(), Msgs::Specific { b, c: C(c), .. } => { assert_eq!(b, c); })
..

so for this the extension with => is required to replace the crate assert_matches in practice from my experience.

@m-ou-se
Copy link
Member Author

m-ou-se commented Mar 11, 2021

In cases like that, the if guard on the pattern also works, right?

assert_matches!(rx.next(), Some(Msgs::Specific { a, b: B(b), .. }) if a == b);

@drahnr
Copy link
Contributor

drahnr commented Mar 23, 2021

While your approach is valid for simple cases, I disagree for the general use case. This is not very ergonomic as soon as there are various sub patterns, that have to be asserted against transformed struct member values or larger enums with multiple elements common for messaging enums.

assert_matches!(rx.next(), Some(Msgs::Specific { a, b: B(b), .. }) => {
  assert_eq!(a, b);
  let container = transform($container);
  if container.is_empty() {
       assert!(..);
  } else {
      assert!(..);
  }
});
..

It could still be expressed with boolean expression combinators using the current impl, yet that adds a lot visual complexity that imho should not exist in a test case.

Note that I am not saying the impl is bad or anything :) - I would very much like to see assert_matches! in std! This effort is much appreciated!

@m-ou-se
Copy link
Member Author

m-ou-se commented Mar 23, 2021

@rust-lang/libs Ping for opinions.

Should assert_matches accept => { .. } blocks to allow for nested asserts (and other code)? The existing assert_matches macros in the ecosystem accept that. But I personally feel it makes things a bit too complicated and unclear.

@BurntSushi
Copy link
Member

It's an interesting extension. At first it looked a bit odd to me, but after ruminating on it a bit, it does seem fairly natural to me.

In terms of moving forward:

  1. Would adding this later be a backwards compatible addition?
  2. How often is this extension used in practice? In theory, we could search the code on crates.io and count the ratio between uses of assert_matches! with and without this extension. If the ratio is very small, then maybe we land it without the extension if the answer to (1) is "yes."

@joshtriplett
Copy link
Member

This makes me think of the proposal to add => syntax to matches!, which the libs team declined.

This seems very similar, and I feel like the arguments are the same. If we have assert_matches!(x, Some(Variant(a, b)) => a == b), similar use cases would motivate if matches!(x, Some(Variant(a, b)) => a == b) { ... }.

I personally feel that in both cases the => syntax adds enough complexity to make it worth spelling out a match or if let, instead. I don't really want to see an entire block of arbitrary code embedded inside an assert_matches!. I also think this will become easier to write when we finish if let chaining.

@m-ou-se
Copy link
Member Author

m-ou-se commented Mar 24, 2021

assert_matches!(x, Some((a, b)) if a == b) would assert that x is a Some and a and b are equal, but
assert_matches!(x, Some((a, b)) => a == b) would only assert that x is a Some, and return the value of a == b without asserting anything about it. The difference there is far too subtle for me.

We could require the => expr to be (), but I don't think that solves the problem of unclear semantics entirely.

@joshtriplett
Copy link
Member

joshtriplett commented Mar 24, 2021

assert_matches!(x, Some((a, b)) if a == b) would assert that x is a Some and a and b are equal, but assert_matches!(x, Some((a, b)) => a == b) would only assert that x is a Some, and return the value of a == b without asserting anything about it. The difference there is far too subtle for me.

For me as well; that was not clear to me at all.

@BoxyUwU
Copy link
Member

BoxyUwU commented Jun 4, 2021

A separate macro that always requires a => expr arm i.e. unwrap_matches extract_matches might make it less ambiguous/more clear

@gilescope
Copy link
Contributor

Well if we're agreed on leaving it as is then we can head towards stabilising it...

@gThorondorsen
Copy link

@drahnr in #82775 (comment)

While your approach is valid for simple cases, I disagree for the general use case. This is not very ergonomic as soon as there are various sub patterns, that have to be asserted against transformed struct member values or larger enums with multiple elements common for messaging enums.

assert_matches!(rx.next(), Some(Msgs::Specific { a, b: B(b), .. }) => {
  assert_eq!(a, b);
  let container = transform($container);
  if container.is_empty() {
       assert!(..);
  } else {
      assert!(..);
  }
});
..

It could still be expressed with boolean expression combinators using the current impl, yet that adds a lot visual complexity that imho should not exist in a test case.

Note that I am not saying the impl is bad or anything :) - I would very much like to see assert_matches! in std! This effort is much appreciated!

Your example can be rewritten to a match guard that always returns true except when it panics, like this.

-assert_matches!(rx.next(), Some(Msgs::Specific { a, b: B(b), .. }) => {
+assert_matches!(rx.next(), Some(Msgs::Specific { a, b: B(b), .. }) if ({
     assert_eq!(a, b);
     let container = transform($container);
     if container.is_empty() {
         assert!(/* … */);
     } else {
         assert!(/* … */);
     }
-});
+    true
+}));

Definitely non-obvious when you don't know about it, but hardly worse to write if you are adding to an existing test suite with the pattern everywhere.

@drahnr
Copy link
Contributor

drahnr commented Dec 1, 2021

Re-reading the comments, I fully support the current impl and withdraw my previous concerns.

@fee1-dead
Copy link
Member

I would prefer the macro to expand as let .. else { panic!() } and users would be able to use the bindings after the assertion.

Does this sound reasonable? Would feature(let_else) block the stabilization of this macro, or can we use allow_internal_unstable?

@jhpratt
Copy link
Member

jhpratt commented Dec 14, 2021

@fee1-dead that would be very confusing to me. An assertion should assert and nothing more imo

@steffahn
Copy link
Member

steffahn commented Jan 2, 2022

I think there's a lot of value in having assert_matches!(...) be basically just an assert!(matches!(...)) with better error messages (and hence with an additional Debug bound). It makes the macro much easier to understand if those two expressions are essentially equivalent. Even if assert_matches and matches would both support the => expr syntax, this rule would be broken: At least following the existing (rejected) proposal for matches' support of => expr, the "equivalent" to assert_matches!(... => ...) would suddenly be matches!(... => ...).unwrap().

@m-ou-se
Copy link
Member Author

m-ou-se commented Jan 3, 2022

Looks like we reached consensus on not having the => syntax.

@rfcbot merge

@rfcbot
Copy link

rfcbot commented Jan 3, 2022

Team member @m-ou-se has proposed to merge this. The next step is review by the rest of the tagged team members:

No concerns currently listed.

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. labels Jan 3, 2022
@rfcbot
Copy link

rfcbot commented Jan 5, 2022

🔔 This is now entering its final comment period, as per the review above. 🔔

@rfcbot rfcbot added final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. and removed proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. labels Jan 5, 2022
@rfcbot rfcbot added the finished-final-comment-period The final comment period is finished for this PR / Issue. label Jan 15, 2022
@nnethercote
Copy link
Contributor

I'm going to add some meta-commentary because I think this conversation has gotten to a point that's not productive.

  • There have been 20 comments in the past couple of days.
  • Nine of them by @Voultapher, who strongly opposes the syntax.
  • Eleven of them by seven other people, none of whom appear to oppose the syntax.

Ultimately it's up to the libs-api team to decide this. I'm sure they will consider the newest arguments put forth.

@nnethercote nnethercote removed the I-libs-api-nominated Nominated for discussion during a libs-api team meeting. label Feb 1, 2024
@Firstyear
Copy link
Contributor

I think more misunderstandings leading to incorrect usage would occur if the syntax deviated from match

This comment pretty much nails it. assert_matches!() should be equivalent to "assert!(matches!( .. ))" else people will be surprised if it acts differently.

@8573
Copy link

8573 commented Feb 2, 2024

My two cents are that the precedence of | vs if in patterns is confusing (whether in assert_matches, matches, or match) and that the solution would be not to disallow if in one use of patterns but to use parentheses to clarify the precedence, with an optional Clippy lint to require the parentheses.

@pravic
Copy link
Contributor

pravic commented Feb 2, 2024

Why are you, guys, arguing in the assert_matches topic if the syntax is exactly the same as in matches! macro? You should have had this discussion during matches! stabilization - here it's too late because assert_matches is simply assert!(matches!(expr), msg).

P.S. And if matches! has different rules, assert_matches! should be exactly the same - otherwise, it's a bad architectural choice.

@Voultapher
Copy link
Contributor

Voultapher commented Feb 2, 2024

Why are you, guys, arguing in the assert_matches topic if the syntax is exactly the same as in matches! macro?

Please carefully read what I've previously written. I think there is a meaningful difference. Here are the two core sources of confusion:

  1. What part of the pattern does the if bin to? Ok(x) | Err(x) if x.len(), the Err or both? That ship as you said has sailed a long time ago, even long before the matches! macro.
  2. I think the intuitive reading of assert_matches!(c, Ok(x) if x.len() < 100) is: "assert that it matches the pattern if the condition is met". However that's not the semantics. It's actually "assert that it matches the pattern and the condition is met". I argue that while matches! is affected by this to some degree, it's significantly worse for assert_matches! because of the logic inversion. I've made that point a couple times now, for example here.

@nnethercote
Copy link
Contributor

Everyone, please stop, the same points are just being rehashed.

@Rigidity
Copy link

Rigidity commented Mar 5, 2024

3 years and no stabilization for something as simple as this? 😦

@rust-lang rust-lang locked and limited conversation to collaborators Mar 5, 2024
@rust-lang rust-lang unlocked this conversation Apr 15, 2024
@oli-obk
Copy link
Contributor

oli-obk commented Apr 15, 2024

Tracking issues have a tendency to become unmanageable. Please open a dedicated new issue and link to this issue for absolutely any topics you want to discuss or have questions about. See rust-lang/compiler-team#739 for details and discussions on this prospective policy.

@YarekTyshchenko
Copy link

YarekTyshchenko commented Jul 8, 2024

FWIW, assert!(matches!(c, Ok(x) | Err(x) if x.len() < 100)); is already valid.

I came looking for assert_eq! equivalent but with matches! funcionality: Assert eq prints left and right side when there's an inequality, and is very useful is tests. the functionaly equivalent assert!(matches!(...)); does not print a nice error message :(

Come to think of it, maybe what I want would be better named as assert_eq_matches!(...) ?

@RmStorm
Copy link

RmStorm commented Jan 8, 2025

Hello, there are two unchecked boxes in the top message:

[ ] Update the must_use message on is_none (per Add messages to Option's and Result's must_use annotation for is_* #62431 (review))*
[ ] Mention this macro in the documentation of matches!()

The first is done since that PR is merged? And the second one also looks done?
https://github.com/rust-lang/rust/blob/master/library/core/src/macros/mod.rs#L457

Does this mean that assert_matches! can be stabilized? What does that process look like and what's the timeline? I'm kinda new here 😅

@Voultapher
Copy link
Contributor

#121732 is the PR (by me) that improved the documentation which also included "Mention this macro in the documentation of matches!()".

@huntc
Copy link
Contributor

huntc commented Feb 2, 2025

So are we able to tick those two remaining boxes then? Thanks.

@tyilo
Copy link
Contributor

tyilo commented Feb 19, 2025

#121732 is the PR (by me) that improved the documentation which also included "Mention this macro in the documentation of matches!()".

Seems weird that the current stable documentation recommends using a nightly only feature:

When testing that a value matches a pattern, it’s generally preferable to use assert_matches! as it will print the debug representation of the value if the assertion fails.

Wouldn't it be better to have released the documentation change to stable together with the stabilization of assert_matches!?

@Voultapher
Copy link
Contributor

@tyilo honestly based on this comment a year ago where the libs team voted yes to stabilize this, with the only requirement being that an appropriate module would be found to put it in, I had assumed it was stabilized already. When writing that PR it never occurred to me to check that it was actually stabilized.

I agree the link to assert_matches should only be made once it's stabilized. The PR mainly focused on the confusing behavior I complained about, and that part is fine and independent of stabilization.

@tmccombs
Copy link
Contributor

It has the finished-final-comment-period label. Do we just need a PR to stabilize it? If so, I could make one.

@safinaskar
Copy link
Contributor

I think rust-lang/rfcs#3573 fully subsumes assert_matches. And rust-lang/rfcs#3573 is more general! Instead of assert_matches!(x, Some(_)) we can write assert!(x is Some(_)), which is shorter and nicer.

So, I think rust-lang/rfcs#3573 should be implemented and then assert_matches! should be removed

@ssokolow
Copy link

RFC 3573 is controversial, as you can see from the nature of the opposition to the idea in the discussion... though given that "detracts from Rust's features being orthogonal and well-factored now that the v1.0 stability promise is blocking removing something else" is one of the complaints, I suppose it is the responsible thing to ask to block stabilizing this until a ruling is made on that.

@nsunderland1
Copy link
Contributor

nsunderland1 commented Feb 20, 2025

Wouldn't assert!(x is Some(_)) have the same limitations as assert!(matches!(x, Some(_))) in that it can't give you a Debug printout?

thread 'main' panicked at src/main.rs:3:5:
assertion failed: matches!(x, Some(_))

As opposed to assert_matches

thread 'main' panicked at src/main.rs:6:5:
assertion `left matches right` failed
  left: None
 right: Some(_)

@teohhanhui
Copy link

teohhanhui commented Feb 20, 2025

same limitations as assert!(matches!(x, Some(_))) in that it can't give you a Debug printout?

There was #110382

It'd seem generic_assert (RFC 2011) and assert_matches are at odds with one another? At least in terms of the message it's sending to the users:

  1. Use assert!(...) for everything.
  2. But use assert_matches!(...) instead of assert!(matches!(...)) 🤔

@tmccombs
Copy link
Contributor

I wonder if it would be better for this to be part of the generic asset functionality. So you could write something like:

assert!(matches!(x, Some(_)))

and get a useful error message. Or maybe some assert-specific syntax like:

assert!(match x => Some(_))

or

assert!(let Some(_) = x)

Voultapher added a commit to Voultapher/rust that referenced this issue Feb 23, 2025
Closes rust-lang#82775

This is a revive of rust-lang#120234, with the
suggested move from the public assert_matches module to macros. This
necessitates the rename of the internal macros modules to core_macros and
std_macros respectively.
Voultapher added a commit to Voultapher/rust that referenced this issue Feb 23, 2025
Closes rust-lang#82775

This is a revive of rust-lang#120234, with the
suggested move from the public assert_matches module to macros. This
necessitates the rename of the internal macros modules to core_macros and
std_macros respectively.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. finished-final-comment-period The final comment period is finished for this PR / Issue. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet