Skip to content

Conversation

@adazh
Copy link
Contributor

@adazh adazh commented Jul 20, 2019

Description

This PR includes:

  • Added a pendingChannelInvokeMethodCount variable that tracks the # of pending [MethodChannel.invokeMethod] calls.
  • Added a Driver API that checks and wait until pendingChannelInvokeMethodCount == 0, and considers there's no going on async method channel calls.
  • Added mock message handlers for the "flutter/platform" channel's "SystemChrome.setApplicationSwitcherDescription" since it's called at start of test initialization, and we'd like to have a default response so that the pendingChannelInvokeMethodCount is 0 when tests are run.

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.

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I signed the CLA.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I updated/added relevant documentation (doc comments with ///).
  • All existing and new tests are passing.
  • The analyzer (flutter analyze --flutter-repo) does not report any problems on my PR.
  • I am willing to follow-up on review comments in a timely manner.

Breaking Change

Does your PR require Flutter developers to manually update their apps to accommodate your change?

  • Yes, this is a breaking change (Please read Handling breaking changes). Replace this with a link to the e-mail where you asked for input on this proposed change.
  • No, this is not a breaking change.

@fluttergithubbot fluttergithubbot added a: tests "flutter test", flutter_test, or one of our tests framework flutter/packages/flutter repository. See also f: labels. labels Jul 20, 2019
@adazh
Copy link
Contributor Author

adazh commented Jul 20, 2019

cc @AlbertWang0116 @khandpur @Hixie

Copy link
Contributor

@tvolkert tvolkert left a 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:

  • WaitUntilNoTransientCallbacks
  • WaitUntilNoPendingFrame
  • WaitUntilNoPendingChannelMessages

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) {
Copy link
Contributor

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?

Copy link
Contributor Author

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;
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@adazh adazh requested a review from tvolkert July 22, 2019 01:12
/// 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 {
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Member

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?

@adazh adazh changed the title Added a Driver API that waits until there's no pending channel messages Added a Driver API that waits until the Flutter app is idle Jul 23, 2019
/// 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 {
Copy link
Member

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.

Copy link
Contributor Author

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.
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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?

Copy link
Contributor Author

@adazh adazh Jul 25, 2019

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.

Copy link
Contributor Author

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.
Copy link
Member

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?

Copy link
Member

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...

Copy link
Contributor Author

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"?

Copy link
Member

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.

Copy link
Contributor Author

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 {
Copy link
Member

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.

Future<RenderTree> getRenderTree({ Duration timeout }) async {
return RenderTree.fromJson(await _sendCommand(GetRenderTree(timeout: timeout)));
}

Copy link
Contributor Author

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.

Copy link
Member

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
Copy link

codecov bot commented Jul 25, 2019

Codecov Report

Merging #36599 into master will decrease coverage by 0.74%.
The diff coverage is 53.63%.

Impacted file tree graph

@@            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
Flag Coverage Δ
#flutter_tool 55.21% <53.63%> (-0.75%) ⬇️
Impacted Files Coverage Δ
packages/flutter_tools/lib/src/device.dart 65.06% <ø> (-2.56%) ⬇️
...lutter_tools/lib/src/commands/update_packages.dart 3.9% <ø> (ø) ⬆️
...ackages/flutter_tools/lib/src/commands/unpack.dart 6.93% <ø> (+0.06%) ⬆️
.../flutter_tools/lib/src/commands/build_fuchsia.dart 81.81% <ø> (ø) ⬆️
packages/flutter_tools/lib/src/desktop.dart 86.95% <ø> (+8.38%) ⬆️
packages/flutter_tools/lib/src/base/io.dart 64.86% <0%> (-5.73%) ⬇️
...es/flutter_tools/lib/src/commands/build_macos.dart 26.31% <0%> (-61.92%) ⬇️
...ackages/flutter_tools/lib/src/commands/create.dart 72.78% <0%> (ø) ⬆️
packages/flutter_tools/lib/src/android/aar.dart 0% <0%> (ø)
packages/flutter_tools/lib/src/features.dart 100% <100%> (ø) ⬆️
... and 54 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 6cdb4dd...fe7a519. Read the comment docs.

@Hixie
Copy link
Contributor

Hixie commented Jul 25, 2019

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.

@adazh
Copy link
Contributor Author

adazh commented Jul 26, 2019

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:

  1. Have a waitUntilIdle API that include common idleness checks that work for most of the cases; The testing framework or developers can call this method to check for idleness (without having to know all the details).
  2. Provide specific waiting APIs or a customizable framework for users to register for their custom checks if they need to. Something like the IdlingResource API in Espresso.

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:

  1. Define all the specific idle checks, and I actually started from there, but got three wait APIs quickly:
  • waitUntilNoTransientCallbacks;
  • waitUntilNoPendingFrames;
  • waitUntilNoPendingInvokeMethods;
    This list will go on and on as we discover more to check.
  1. Or start with the waitUntilIdle method, explain clearly what it checks in the documentation, and evolves this API as we understand more of the synchronization needs.

Comparing these two approaches, I decided to go with 2), as it actually aligns better:
Having a common waitUntilIdle method; but for approach 1), it's likely we'll have a long list of these APIs, and eventually throw them away if they turn out to be something that should be merged to the waitUntilIdle API.

I can add comments to explain that this waitUntilIdle is still experimental and hide it from the Driver API before it becomes more mature. WDYT?

@Hixie
Copy link
Contributor

Hixie commented Jul 26, 2019

is there some way to solve this by composition? Having components for each mechanism, that you can combine?

@adazh
Copy link
Contributor Author

adazh commented Jul 26, 2019

I didn't see there's a good extensible way but you may have suggestions.

We probably shouldn't do this:
waitUntilIdle() { return Futures.wait( _waitUntilNoTransientCallbacks(), _waitUntilNoPendingFrames(), _waitUntilNoPendingInvokeMethods()) }
As we want all the conditions hold at the same time when the waitUntilIdle future completes.

Possible improvement by wrapping different conditions into separate methods. Something like:
bool hasNoTransientCallbacks() { return SchedulingBinding.transientCallbacks == 0; } waitUntilIdle() { return _waitUntilCondition( hasNoTransientCallbacks() && hasNoPendingFrames() && ...); }

///
/// 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;
Copy link
Contributor

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)]) {
Copy link
Contributor

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.

@Hixie
Copy link
Contributor

Hixie commented Jul 30, 2019

We probably shouldn't do this:
waitUntilIdle() { return Futures.wait( _waitUntilNoTransientCallbacks(), _waitUntilNoPendingFrames(), _waitUntilNoPendingInvokeMethods()) }
As we want all the conditions hold at the same time when the waitUntilIdle future completes.

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.

@goderbauer
Copy link
Member

@adazh Can we close this PR in favor of the other PRs?

@adazh
Copy link
Contributor Author

adazh commented Aug 7, 2019

Ya, I should.

@adazh adazh closed this Aug 7, 2019
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 5, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

a: tests "flutter test", flutter_test, or one of our tests framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants