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

Enable unawaited_futures #934

Merged
merged 14 commits into from
Oct 12, 2024
Merged

Enable unawaited_futures #934

merged 14 commits into from
Oct 12, 2024

Conversation

PIG208
Copy link
Member

@PIG208 PIG208 commented Sep 10, 2024

Fixes part of #731

@PIG208
Copy link
Member Author

PIG208 commented Sep 10, 2024

TODO: TestGlobalStore's doInsertAccount implementation has a race condition for checking the invariants. Concurrently adding accounts allows you to bypass the check.

@chrisbobbe
Copy link
Collaborator

This sounds helpful!! It looks like we have an issue for it: #731 🙂

@PIG208 PIG208 force-pushed the pr-async branch 4 times, most recently from c20a4a2 to c2a820b Compare September 12, 2024 04:44
@PIG208
Copy link
Member Author

PIG208 commented Sep 12, 2024

This is stacked on top of #937.

@PIG208 PIG208 force-pushed the pr-async branch 2 times, most recently from a9031aa to ecb1a36 Compare September 12, 2024 04:47
@PIG208 PIG208 added the maintainer review PR ready for review by Zulip maintainers label Sep 12, 2024
@PIG208 PIG208 marked this pull request as ready for review September 12, 2024 04:49
@PIG208 PIG208 changed the title WIP: Enable unawaited_futures/discarded_futures Enable unawaited_futures Sep 12, 2024
@PIG208 PIG208 removed the maintainer review PR ready for review by Zulip maintainers label Sep 17, 2024
@PIG208 PIG208 removed the request for review from chrisbobbe September 17, 2024 00:28
Comment on lines 158 to 160
showErrorDialog(context: context,
unawaited(showErrorDialog(context: context,
Copy link
Member

Choose a reason for hiding this comment

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

I just wrote down some thoughts on showErrorDialog's API on a related PR:
#937 (comment)

so adding a link to that here.

I think the many unawaited calls aroung showErrorDialog here get fairly noisy. That makes the diff bigger, which is undesirable but could be OK… but then I think it also leaves enough noise in the tree that it might start to undermine the effect of this lint. If we find ourselves scattering unawaited calls all over because there are some functions that just need it most times you call them, then the other more interesting unawaited calls don't stand out so much.

Probably a good fix for that is to have our own pair of showFooDialog wrappers provide an API that's more like the one I describe in that comment.

That would also be a better API for showDialog upstream to have. But it's a tough change to make, because the change in return type would break tons of downstream callers; I don't see a clear way to get to a great API given that constraint. So I wouldn't invest in trying to make the change upstream.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I like the showSnackBar API. Future is more primitive in the sense that it doesn't carry much contextual information. The wrapper class is a step up from simply using typedef, and offers good documentation discoverability.

@PIG208 PIG208 force-pushed the pr-async branch 4 times, most recently from da3c21c to 826124c Compare September 28, 2024 04:52
@PIG208 PIG208 added the maintainer review PR ready for review by Zulip maintainers label Sep 28, 2024
@PIG208
Copy link
Member Author

PIG208 commented Sep 28, 2024

This is stacked on top of #970 (dialog [nfc]: Wrap showErrorDialog's return value).

The PR is mostly small commits for refactors in a limited scope.

@PIG208 PIG208 removed the maintainer review PR ready for review by Zulip maintainers label Oct 3, 2024
@PIG208 PIG208 removed the request for review from chrisbobbe October 3, 2024 17:48
checkRequest({'x': [1, 2]}, '/api/v1/example/route?x=%5B1%2C2%5D');
checkRequest({'x': {'y': 1}}, '/api/v1/example/route?x=%7B%22y%22%3A1%7D');
checkRequest({'x': RawParameter('foo')},
await checkRequest(null, '/api/v1/example/route');
Copy link
Member

Choose a reason for hiding this comment

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

Are these needed? If I munge the expected value here, the test duly fails, even without await:

00:05 +0 -1: ApiConnection.get [E]                                                            
  Expected: a BaseRequest that:
    is a Request
    has method that:
      equals 'GET'
    has url that:
      has toString that:
        equals 'https://chat.example/api/v1/example/rote'
  Actual: a BaseRequest that:
    is a Request
    has method that:
      equals 'GET'
    has url that:
      has toString that:
      Actual: 'https://chat.example/api/v1/example/route'
      Which: differs at offset 38:
      ... example/rote
      ... example/route
                    ^
  package:checks/src/checks.dart 85:9               check.<fn>
  package:checks/src/checks.dart 708:12             _TestContext.expect
  package:checks/src/extensions/string.dart 108:13  StringChecks.equals
  test/api/core_test.dart 28:26                     main.<fn>.checkRequest.<fn>
  ===== asynchronous gap ===========================
  dart:async                                        _CustomZone.registerBinaryCallback
  test/api/fake_api.dart 189:14                     FakeApiConnection.with_
  test/api/core_test.dart 23:32                     main.<fn>.checkRequest
  test/api/core_test.dart 37:5                      main.<fn>
  
  This test failed after it had already completed.
  Make sure to use a matching library which informs the test runner
  of pending async work.
  package:checks/src/checks.dart 85:9               check.<fn>
  package:checks/src/checks.dart 708:12             _TestContext.expect
  package:checks/src/extensions/string.dart 108:13  StringChecks.equals
  test/api/core_test.dart 28:26                     main.<fn>.checkRequest.<fn>
  ===== asynchronous gap ===========================
  dart:async                                        _CustomZone.registerBinaryCallback
  test/api/fake_api.dart 189:14                     FakeApiConnection.with_
  test/api/core_test.dart 23:32                     main.<fn>.checkRequest
  test/api/core_test.dart 37:5                      main.<fn>
  
00:05 +17 -1: Some tests failed.                                                              

There is that warning message. But it points at a different solution: just make sure the test runner knows there's some pending async work.

That in turn can I think be done with check(…).completes(). For example:

   test('ApiConnection.get', () async {
-    Future<void> checkRequest(Map<String, dynamic>? params, String expectedRelativeUrl) {
-      return FakeApiConnection.with_(account: eg.selfAccount, (connection) async {
+    void checkRequest(Map<String, dynamic>? params, String expectedRelativeUrl) {
+      check(FakeApiConnection.with_(account: eg.selfAccount, (connection) async {
         connection.prepare(json: {});
         await connection.get(kExampleRouteName, (json) => json, 'example/route', params);
         check(connection.lastRequest!).isA<http.Request>()
           ..method.equals('GET')
           ..url.asString.equals('${eg.realmUrl.origin}$expectedRelativeUrl')
           ..headers.deepEquals({
             ...authHeader(email: eg.selfAccount.email, apiKey: eg.selfAccount.apiKey),
             ...kFallbackUserAgentHeader,
           })
           ..body.equals('');
-      });
+      })).completes();
     }

… In fact, looking at the implementation of completes, I think it can be a bit cleaner than that. I'll send a PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

Skimming on mobile, this should also apply to c516066, right? I recall that check(SomethingFuture) has some special handling.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, the new finish helper in #985 can probably apply to some other places as well. No need to go sweep through using it in all the existing tests, but worth using to simplify new tests or tests changed in this PR.

Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

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

Thanks @PIG208 for cleaning these up, and thanks @chrisbobbe for the previous review! Generally this all looks good; small comments below.

@@ -354,10 +355,9 @@ class NotificationDisplayManager {

assert(debugLog(' account: $account, narrow: $narrow'));
// TODO(nav): Better interact with existing nav stack on notif open
navigator.push(MaterialAccountWidgetRoute<void>(accountId: account.id,
unawaited(navigator.push(MaterialAccountWidgetRoute<void>(accountId: account.id,
Copy link
Member

Choose a reason for hiding this comment

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

nit in commit message: duplicate text

The `Future` that `Navigator.push` returns resolves when a
corresponding `Navigator.pop` is called.

The future Navgiator.push returns completes when the there is a
corresponding Navigator.pop call.  We don't use that as of now, so we
use unawaited to explicitly discard it.

markNarrowAsRead(context, narrow);
unawaited(markNarrowAsRead(context, narrow));
await tester.pump(Duration.zero);
Copy link
Member

Choose a reason for hiding this comment

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

Huh, odd. I wonder why this test is using tester.pump — is that just a substitute for awaiting the future?

What if we replaced both these lines with this?

      await markNarrowAsRead(context, narrow);

(Similarly for the other markNarrowAsRead calls in this file.)

Copy link
Member Author

@PIG208 PIG208 Oct 11, 2024

Choose a reason for hiding this comment

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

It's stuck forever, right before the connection.post call to "messages/flags/narrow". It's puzzling that it happens to a FakeApiConnection though. If I remove the connection.prepare call, the test would complete just fine, because updateMessageFlagsStartingFromAnchor catches all errors.

Copy link
Member Author

@PIG208 PIG208 Oct 11, 2024

Choose a reason for hiding this comment

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

I guess this is because we call it inside testWidgets. This issue is not specific to markNarrowAsRead. For example, a store.sendMessage call stucks as well. The FakeApiClient returns a Future.delayed(response.delay, computation); that does not complete in such tests.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, I see. So basically it's running into the problem that awaitFakeAsync solves.

It feels odd for these tests to not be awaiting that future — it means they're not ensuring that the requests have actually completed, and that the function has done the work it's going to do when it gets the responses, before they complete.

I'll send a quick separate PR that fixes these.

Copy link
Member

Choose a reason for hiding this comment

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

Comment on lines 157 to 158
finish(check(globalStore.updateAccount(eg.selfAccount.id, const AccountsCompanion(
id: Value(1234)))).throws());
Copy link
Member

Choose a reason for hiding this comment

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

For throws, I think the normal thing is to await the future:

Suggested change
finish(check(globalStore.updateAccount(eg.selfAccount.id, const AccountsCompanion(
id: Value(1234)))).throws());
await check(globalStore.updateAccount(eg.selfAccount.id, const AccountsCompanion(
id: Value(1234)))).throws();

From the docs linked in the commit message:

Expectation extension methods checking asynchronous behavior return a Future. The future should typically be awaited within the test body, […]

@@ -25,3 +25,4 @@ linter:
no_literal_bool_comparisons: true
prefer_relative_imports: true
unnecessary_statements: true
unawaited_futures: true
Copy link
Member

Choose a reason for hiding this comment

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

The commit message steps into a classic GitHub pitfall 🙂 :

Partially fixes: #731

GitHub doesn't understand the "partially", so that will close the issue.

If you don't mean "fixes", then the trick is to put some other word between it and the issue number. For example:

Fixes-partly: #731

Copy link
Member

Choose a reason for hiding this comment

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

nit in commit message: the convention for email-header-style lines at the end of a commit message is that the words before the colon are joined by hyphens -, not spaces

@PIG208 PIG208 force-pushed the pr-async branch 5 times, most recently from fe606f9 to f9306cd Compare October 11, 2024 01:51
@PIG208
Copy link
Member Author

PIG208 commented Oct 11, 2024

Thanks for the review! Updated the PR and fixed some more errors that appeared after rebasing (and applied #731)

PIG208 and others added 14 commits October 11, 2024 17:27
testBinding.globalStore gets modified by multiple tests here.

Signed-off-by: Zixuan James Li <[email protected]>
The future Navgiator.push returns completes when the there is a
corresponding Navigator.pop call.  We don't use that as of now, so we
use unawaited to explicitly discard it.

Other notes:

* _onSubmitted doesn't need to return a Future<void> because we
  don't intend to call it with await.

* This removes an unnecessary return in _navigateForNotification at
  the end of the method.
  It was added in commit 681a744.

Signed-off-by: Zixuan James Li <[email protected]>
The test helper receiveFcmMessage doesn't need to be asychronous
given what it currently does.

Signed-off-by: Zixuan James Li <[email protected]>
There may or may not be cases where later awaited calls to async
functions effectively wait for store.handleEvent.  We add await for
store.handleEvent calls anyway for explicitness.

Signed-off-by: Zixuan James Li <[email protected]>
These tests didn't break previously because the GlobalStore.add calls
finished before the per account store was ever needed.

A notable example is the test "find account among several" in
test/notifications/display_test.dart.  The accounts were added in
parallel, which actually allowed the GlobalStore.add calls to bypass
the invariant check performed by TestGlobalStore.  The test passed
anyway because it only requires the accounts to be there when some
other checks are performed.

The invariant check is to ensure that no accounts have the same email
address within a realm.  To fix this, we can potentially maintain a
store of accounts with TestGlobalStore, and check for the constraints
there.

Signed-off-by: Zixuan James Li <[email protected]>
These tests actually worked as expected, even though we didn't await
for the Future returned by FutureChecks.throws.

This is because checks ensure that the asychronous expectations are
complete before the test ends.

Nonetheless, we should use await so that our intention is explicit
(i.e. the future completes with an error as opposed to the future
never completes and can't be awaited).

See also:
  https://github.com/dart-lang/test/tree/master/pkgs/checks#checking-asynchronous-expectations

Signed-off-by: Zixuan James Li <[email protected]>
The later `await tester.idle` call helps wait for the earlier
mock platform message to be handled.  We can remove that call and use
await on the handlePlatformMessage call itself.

Signed-off-by: Zixuan James Li <[email protected]>
These are function calls that need to happen asynchronously until we
elapse the time with FakeAsync or WidgetTester.

Signed-off-by: Zixuan James Li <[email protected]>
This explicitly ensures that these checks complete
before the test ends.

We can keep these single-line helper calls without adding `await`'s
to all of them.

Signed-off-by: Zixuan James Li <[email protected]>
Fixes-partly: zulip#731

Signed-off-by: Zixuan James Li <[email protected]>
The `finish` helper is only needed when the future that needs to finish
isn't already one created by `check` (which takes care of the effect of
`finish` for itself).
@gnprice
Copy link
Member

gnprice commented Oct 12, 2024

Thanks for the revision! All looks good; merging, with one NFC commit on top:

1aef868 api test [nfc]: Simplify some unawaited checks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
integration review Added by maintainers when PR may be ready for integration
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants