Skip to content

Reject asynchronous matchers inside isNot and anyOf - #2792

Merged
natebosch merged 1 commit into
masterfrom
matcher-reject-async-in-operators
Oct 13, 2026
Merged

natebosch merged 1 commit into
masterfrom
matcher-reject-async-in-operators

Conversation

@natebosch

@natebosch natebosch commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Closes #1637

Asynchronous matchers report a pending result as a synchronous match and
fail the test later. isNot inverts that placeholder into an immediate
failure, and anyOf short-circuits on it, so neither operator can combine
asynchronous results correctly. For example anyOf([completes, throwsA(x)])
passes synchronously no matter how the future completes, and the test may
fail later even when one of the matchers would have matched.

isNot and anyOf now match their operands in a zone that marks
asynchronous matchers as disallowed. AsyncMatcher.matches and
doesNotComplete throw an ArgumentError in that zone before starting any
asynchronous work, which also catches asynchronous matchers nested under
other operators. expect and expectLater clear the restriction, so
expectations made in callbacks run by an operator are unaffected.

AsyncMatcher subclasses can override mayMatchSynchronously to defer the
check until matchAsync returns a Future. The throws matchers opt in, so
common patterns like isNot(throwsA(...)) with a synchronous callback keep
working, while a Future or a callback returning a Future is still
rejected.

Asynchronous matchers report a pending result as a synchronous match and
fail the test later. `isNot` inverts that placeholder into an immediate
failure, and `anyOf` short-circuits on it, so neither operator can combine
asynchronous results correctly. For example `anyOf([completes, throwsA(x)])`
passes synchronously no matter how the future completes, and the test may
fail later even when one of the matchers would have matched.

`isNot` and `anyOf` now match their operands in a zone that marks
asynchronous matchers as disallowed. `AsyncMatcher.matches` and
`doesNotComplete` throw an `ArgumentError` in that zone before starting any
asynchronous work, which also catches asynchronous matchers nested under
other operators. `expect` and `expectLater` clear the restriction, so
expectations made in callbacks run by an operator are unaffected.

`AsyncMatcher` subclasses can override `mayMatchSynchronously` to defer the
check until `matchAsync` returns a `Future`. The `throws` matchers opt in, so
common patterns like `isNot(throwsA(...))` with a synchronous callback keep
working, while a `Future` or a callback returning a `Future` is still
rejected.
@natebosch

Copy link
Copy Markdown
Member Author

I found only 1 misleading test pattern that this catches copy/pasted 3 times in 1 repo. There may be more problems with this in external code which I haven't tried to survey. I think it's worth landing because there's really no reason to use the patterns this blocks.

@jakemac53 jakemac53 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this breaking, or are we fairly certain that all subtypes of AsyncMatcher extend the base type?

Since this would break those who are not, should we mark it as base now?

@natebosch

Copy link
Copy Markdown
Member Author

Is this breaking, or are we fairly certain that all subtypes of AsyncMatcher extend the base type?

We've never exposed it through a public library. I think there are some src imports in google3, either those are gone or they work with this.

Since this would break those who are not, should we mark it as base now?

That's a good idea, it's (practically) equally breaking and would protect us in the future.

@natebosch

Copy link
Copy Markdown
Member Author

Ah, I guess not quite equally breaking, adding base is much more likely to raise static errors because classes that already extends AsyncMatcher still have to be marked as base or final themselves. I'll kick off a bot to see whether google3 has any extends AsyncMatcher or not, but I think we shouldn't hold this PR for that.

@natebosch

Copy link
Copy Markdown
Member Author

There are uses of extends AsyncMatcher which would be affected.

In practice the design of this class only makes sense for use as a base class, and I think it's OK to rely on existing code doing that. There's nothing to be gained with implements AsyncMatcher.

If more changes were planned I'd consider going through the migration to add base, but currently I'm just trying to close up a few places where things might have been misused.

@github-actions

Copy link
Copy Markdown

PR Health

Changelog Entry ✔️
Package Changed Files

Changes to files need to be accounted for in their respective changelogs.

This check can be disabled by tagging the PR with skip-changelog-check (for example, for non-user-visible changes when the package version is already at -wip).

@jakemac53

Copy link
Copy Markdown
Contributor

Unfortunate about the transitive base/final requirement, but makes sense

@natebosch
natebosch merged commit 9bf69a8 into master Oct 13, 2026
71 checks passed
@natebosch
natebosch deleted the matcher-reject-async-in-operators branch October 13, 2026 21:33
kevmoo pushed a commit to firebase/flutterfire that referenced this pull request Oct 14, 2026
…18795)

The `testExceptionHandling` helpers in the firebase_auth and
firebase_performance platform interface tests have no callers. They also
contain an assertion that doesn't do what it appears to:

    await expectLater(
      () async => testMethod(),
      anyOf([
        completes,
        if (type == 'PLATFORM' || type == 'EXCEPTION')
          throwsA(isA<FirebaseException>()),
      ]),
    );

`completes` only matches a `Future`, and the value here is a closure, so
that branch can never match. When `type` is `'PLATFORM'` or `'EXCEPTION'`
the `anyOf` behaves like `throwsA(...)` alone, and for any other `type` the
assertion always fails. In addition, `anyOf` can't combine asynchronous
matchers: it treats the pending result from `throwsA` as a match, and
`expectLater` doesn't wait for the real result.

An upcoming change to `package:matcher` makes `anyOf` and `isNot` throw an
`ArgumentError` when they are used with asynchronous matchers like these,
which would break these helpers if they were ever called again:
dart-lang/test#2792

Remove the helpers and the imports they used.
copybara-service Bot pushed a commit to dart-lang/sdk that referenced this pull request Oct 15, 2026
Revisions updated by `dart tools/rev_sdk_deps.dart`.

native is rolled separately in
https://dart-review.googlesource.com/c/sdk/+/563524.

http (https://github.com/dart-lang/http/compare/23f8574..7d7aae2):
  7d7aae2  Mon Oct 12 20:39:25 2026 -0700  Kevin Moore  Clean up dependencies and expand Node and Wasm test coverage (dart-lang/http#1903)
  ab986ad  Mon Oct 12 18:59:24 2026 -0700  Kevin Moore  fix(http_parser): fix chunked size overflow and MediaType.toString quoting (dart-lang/http#2001)
  cb8113b  Mon Oct 12 17:35:26 2026 -0700  Kevin Moore  docs(http2): move post-3.1.0 security entries to 3.2.0-wip (dart-lang/http#2022)

test (https://github.com/dart-lang/test/compare/30acb69..9bf69a8):
  9bf69a88  Tue Oct 13 14:33:18 2026 -0700  Nate Bosch  Reject asynchronous matchers inside isNot and anyOf (dart-lang/test#2792)

tools (https://github.com/dart-lang/tools/compare/2b54c71..350bde5):
  350bde54f  Tue Oct 13 08:22:26 2026 -0700  Kevin Moore  chore(coverage): prepare for v1.16.0 release (dart-lang/tools#2643)
  a55965844  Tue Oct 13 13:29:52 2026 +0200  Sigurd Meldgaard  [markdown] Fix quadratic parsing of consecutive link reference definitions (dart-lang/tools#2662)

[email protected]
TEST=ci

Change-Id: I1065854cfbaf71ebb7a097d89f28902394a80c2e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/563365
Commit-Queue: Nate Bosch <[email protected]>
Reviewed-by: Brian Quinlan <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

isNot matcher is not negating the result of a wraped matcher

2 participants