-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Added a Driver API that waits until the Flutter app is idle #36599
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
Conversation
tvolkert
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the high level, my concern with this series of PRs is that we're adding APIs that all feel like they're getting at the same thing:
WaitUntilNoTransientCallbacksWaitUntilNoPendingFrameWaitUntilNoPendingChannelMessages
This is going to confuse callers - "Which one should I wait for and under which circumstances? Should I just wait for them all?" I think when faced with these questions, people are likely to just wait for them all. Do we want them to? Will doing so cause them problems in certain circumstances? If we want them to, we should just make this one API - WaitUntilIdle or something along those lines. Which in turn would argue for something similar to Espresso's IdlingResource.
|
|
||
| test('returns immediately when there is no pending channel message', | ||
| () async { | ||
| FakeAsync().run((FakeAsync async) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling a variable async is just asking for trouble - how about fakeAsync?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed to fakeAsync.
| if (result == null) { | ||
| throw MissingPluginException('No implementation found for method $method on channel $name'); | ||
| try { | ||
| _pendingChannelInvokeMethodCount += 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should go immediately before the try
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
| /// Creates a command that waits until there's no pending frame scheduled. | ||
| const WaitUntilNoPendingFrame({ Duration timeout }) : super(timeout: timeout); | ||
| /// A Flutter Driver command that waits until the Flutter app is idle. | ||
| class WaitUntilIdle extends Command { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tvolkert will this need to be announced as a breaking change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added this API a couple of days ago (which shouldn't be used anywhere yet...), and kept the waitUntilNoTransientCallbacks API. LMK if this is a breaking change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since that API is so young, maybe revert that change so nobody starts depending on it until we have agreement what API we actually want to add?
| /// should instead disable the frame sync using the `set_frame_sync` method. | ||
| /// See [FlutterDriver.runUnsynchronized] for more details on how to do this. | ||
| /// Note, disabling frame sync will require the test author to use some other method to avoid flakiness. | ||
| Future<Result> _waitUntilIdle(Command command) async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be a link to the other similar method waitUntilNoTransientCallbacks and an explanation when to use what.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shall we just deprecate the waitUntilNoTransientCallbacks after we add this waitUntilIdle API? I don't think a test author/developer actually understand what "transient callbacks" means.
| && !SchedulerBinding.instance.hasScheduledFrame; | ||
| /// should instead disable the frame sync using the `set_frame_sync` method. | ||
| /// See [FlutterDriver.runUnsynchronized] for more details on how to do this. | ||
| /// Note, disabling frame sync will require the test author to use some other method to avoid flakiness. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The doc comment will not appear in our API docs since the method is private. Maybe add the comment to the corresponding method in FlutterDriver?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ya... if you think it makes sense to expose this API on Driver, I can add it in a follow-up PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not in this one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My main goal for this PR is to make Espresso tests more reliable by calling this waitUntilIdle API.
As you noted in other comments, I think it makes sense to sort out the relationship between this one and the waitUntilNoTransientCallbacks before exposing it to Flutter Driver users... And I suspect it's going to take time and could be a breaking change. Also, considering I'm not familiar with Flutter's release process (e.g. how to handle breaking changes), I don't want to include all the changes in one PR. At the same time, it doesn't block Espresso.
Hope this works for you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tvolkert + Todd on this thread. Let me know what would be the best way to move forward.
| return SchedulerBinding.instance.transientCallbackCount == 0 | ||
| && !SchedulerBinding.instance.hasScheduledFrame; | ||
| /// should instead disable the frame sync using the `set_frame_sync` method. | ||
| /// See [FlutterDriver.runUnsynchronized] for more details on how to do this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we document on runUnsynchronized that it changes the behavior of waitUntilIdle?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On second thought, isn't this interaction kinda wired? Why does runUnsynchronized change the definition of what "idle" means? With this implementation, waiting for idle while running unsynchronized will in fact not wait for the app to be idle...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if I understand your question here... I think runUnsyncrhonized will disable "frame sync"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the current implementation, if you execute a waitUntilIdle command within runUnsynchronized you are not waiting until the app is idle. That seems odd to me given the method name. So, I was wondering why you decided to make waitUntilIdle depend on set_frame_sync.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK. Got your question.
IMHO, I think it should be fine to call this waitUntilIdle within runsynchronized and it waits until resources are idle except the frame sync. And if an app has animations running all the time, but developers want to test other parts of the app, they have the option to disable frame sync but call this waitUntilIdle to ensure everything else is idle.
On the other hand, since Driver/extension API has the notion of "frame sync" mechanism, I think it makes sense to respect that in a waitUntilIdle API. Otherwise, these APIs act inconsistently: _waitForElement API respects frame sync, but this one doesn't.
| /// Creates a command that waits until there's no pending frame scheduled. | ||
| const WaitUntilNoPendingFrame({ Duration timeout }) : super(timeout: timeout); | ||
| /// A Flutter Driver command that waits until the Flutter app is idle. | ||
| class WaitUntilIdle extends Command { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason we do not expose this command (like the other commands) in the FlutterDriver API? e.g.
flutter/packages/flutter_driver/lib/src/driver/driver.dart
Lines 475 to 477 in 5318670
| Future<RenderTree> getRenderTree({ Duration timeout }) async { | |
| return RenderTree.fromJson(await _sendCommand(GetRenderTree(timeout: timeout))); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ya... Espresso can just call this extension API but if you think it makes sense to expose to the Driver API, happy to make that change in a follow-up PR. But we might want to sort out the relationship between this one and the waitUntilNoTransientCallbacks: shall we simply deprecate the waitUntilNoTransientCallbacks method? After we have this waitUntilIdle API, I don't see there are cases that users would want to use the waitUntilNoTransientCallbacks API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably sort out that relationship before we add more API.
Codecov Report
@@ Coverage Diff @@
## master #36599 +/- ##
==========================================
- Coverage 55.95% 55.21% -0.75%
==========================================
Files 188 190 +2
Lines 17432 17603 +171
==========================================
- Hits 9754 9719 -35
- Misses 7678 7884 +206
Continue to review full report at Codecov.
|
|
This doesn't really wait for idleness (e.g. it doesn't wait for timers, network traffic, tasks that are started by a channel message that immediately replies and sends the result later, etc), so it might be better to name it something more descriptive of what it actually waits for. |
|
Right, the current implementation only checks a couple of idleness signals, not all of them, but I'd like to evolve the implementation gradually. Eventually I envision the Flutter synchronization API would be something close to:
I'm still experimenting with the synchronization mechanism & API (also, note, we probably don't want to ensure everything is idle but these that effect UI). And I think there are two ways to get to the end goal:
Comparing these two approaches, I decided to go with 2), as it actually aligns better: I can add comments to explain that this |
|
is there some way to solve this by composition? Having components for each mechanism, that you can combine? |
|
I didn't see there's a good extensible way but you may have suggestions. We probably shouldn't do this: Possible improvement by wrapping different conditions into separate methods. Something like: |
| /// | ||
| /// This method is useful for a testing framework like Espresso to monitor | ||
| /// ongoing platform channel messages and decide whether the app is idle. | ||
| static int get pendingChannelInvokeMethodCount => _pendingChannelInvokeMethodCount; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if this were exposed through a listener registration similar to SchedulerBinding.instance.addPostFrameCallback(), it would clean up some smells elsewhere in this PR, such as the 10ms polling.
It'd probably amount to adding API to SchedulerBinding - something like addMethodChannelsIdleCallback() (and moving the state variable into the binding, which also means it no longer needs to be static, which is nice)
Then again, this work isn't being scheduled in the way that fits perfectly into the existing SchedulerBinding model. @Hixie @goderbauer wdyt?
| } | ||
|
|
||
| Future<void> _waitUntilCondition(bool condition(), | ||
| [Duration duration = const Duration(milliseconds: 10)]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regardless of the duration, polling seems wrong here - see my comment above about making this callback based.
Sure, you'd want a solution a little more elaborate than this, where each waiter can say when it is met and when it is not, and you only go when all of them are met simultaneously. |
|
@adazh Can we close this PR in favor of the other PRs? |
|
Ya, I should.
|
Description
This PR includes:
Related Issues
In Espresso tests for the Flutter app, we noticed test flakiness when there are async method channel calls, especially for the ones that takes significant time to get the platform messages back. Thus, we propose to track the pending message channel calls and use it as a signal for the app to go idle.
There are certainly other types of async methods that could potentially lead to UI test flakiness, but we prefer to cover the common use cases that could lead to indeterministic UI state in Espresso tests (like message channels). For other async methods, if there are no common patterns, we can introduce similar IdlingResource to the Dart land.
Tests
I added the following tests:
flutter_driver/test/src/extension_test.dart
Checklist
Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes (
[x]). This will ensure a smooth and quick review process.///).flutter analyze --flutter-repo) does not report any problems on my PR.Breaking Change
Does your PR require Flutter developers to manually update their apps to accommodate your change?