Unverified Commit 342c6286 authored by Jia Hao's avatar Jia Hao Committed by GitHub

Eagerly wait for the driver extension on FlutterDriver.connect() (#56428)

parent 4b814cf8
...@@ -8,7 +8,6 @@ import 'dart:io'; ...@@ -8,7 +8,6 @@ import 'dart:io';
import 'package:file/file.dart' as f; import 'package:file/file.dart' as f;
import 'package:fuchsia_remote_debug_protocol/fuchsia_remote_debug_protocol.dart' as fuchsia; import 'package:fuchsia_remote_debug_protocol/fuchsia_remote_debug_protocol.dart' as fuchsia;
import 'package:json_rpc_2/error_code.dart' as error_code;
import 'package:json_rpc_2/json_rpc_2.dart' as rpc; import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
...@@ -150,11 +149,37 @@ class VMServiceFlutterDriver extends FlutterDriver { ...@@ -150,11 +149,37 @@ class VMServiceFlutterDriver extends FlutterDriver {
} }
/// Waits for a signal from the VM service that the extension is registered. /// Waits for a signal from the VM service that the extension is registered.
/// Returns [_flutterExtensionMethodName] ///
Future<String> waitForServiceExtension() { /// Looks at the list of loaded extensions for the current [isolateRef], as
return isolate.onExtensionAdded.firstWhere((String extension) { /// well as the stream of added extensions.
return extension == _flutterExtensionMethodName; Future<void> waitForServiceExtension() async {
}); final Future<void> extensionAlreadyAdded = isolateRef
.loadRunnable()
.then((VMIsolate isolate) async {
if (isolate.extensionRpcs.contains(_flutterExtensionMethodName)) {
return;
}
// Never complete. Rely on the stream listener to find the service
// extension instead.
return Completer<void>().future;
});
final Completer<void> extensionAdded = Completer<void>();
StreamSubscription<String> isolateAddedSubscription;
isolateAddedSubscription = isolate.onExtensionAdded.listen(
(String extensionName) {
if (extensionName == _flutterExtensionMethodName) {
extensionAdded.complete();
isolateAddedSubscription.cancel();
}
},
onError: extensionAdded.completeError,
cancelOnError: true);
await Future.any(<Future<void>>[
extensionAlreadyAdded,
extensionAdded.future,
]);
} }
/// Tells the Dart VM Service to notify us about "Isolate" events. /// Tells the Dart VM Service to notify us about "Isolate" events.
...@@ -174,25 +199,7 @@ class VMServiceFlutterDriver extends FlutterDriver { ...@@ -174,25 +199,7 @@ class VMServiceFlutterDriver extends FlutterDriver {
if (isolate.pauseEvent is VMPauseStartEvent) { if (isolate.pauseEvent is VMPauseStartEvent) {
_log('Isolate is paused at start.'); _log('Isolate is paused at start.');
// If the isolate is paused at the start, e.g. via the --start-paused await resumeLeniently();
// option, then the VM service extension is not registered yet. Wait for
// it to be registered.
await enableIsolateStreams();
final Future<String> whenServiceExtensionReady = waitForServiceExtension();
final Future<dynamic> whenResumed = resumeLeniently();
await whenResumed;
_log('Waiting for service extension');
// We will never receive the extension event if the user does not
// register it. If that happens, show a message but continue waiting.
await _warnIfSlow<String>(
future: whenServiceExtensionReady,
timeout: kUnusuallyLongTimeout,
message: 'Flutter Driver extension is taking a long time to become available. '
'Ensure your test app (often "lib/main.dart") imports '
'"package:flutter_driver/driver_extension.dart" and '
'calls enableFlutterDriverExtension() as the first call in main().',
);
} else if (isolate.pauseEvent is VMPauseExitEvent || } else if (isolate.pauseEvent is VMPauseExitEvent ||
isolate.pauseEvent is VMPauseBreakpointEvent || isolate.pauseEvent is VMPauseBreakpointEvent ||
isolate.pauseEvent is VMPauseExceptionEvent || isolate.pauseEvent is VMPauseExceptionEvent ||
...@@ -210,27 +217,20 @@ class VMServiceFlutterDriver extends FlutterDriver { ...@@ -210,27 +217,20 @@ class VMServiceFlutterDriver extends FlutterDriver {
); );
} }
// Invoked checkHealth and try to fix delays in the registration of Service await enableIsolateStreams();
// extensions
Future<Health> checkHealth() async { // We will never receive the extension event if the user does not register
try { // it. If that happens, show a message but continue waiting.
// At this point the service extension must be installed. Verify it. await _warnIfSlow<void>(
return await driver.checkHealth(); future: waitForServiceExtension(),
} on rpc.RpcException catch (e) { timeout: kUnusuallyLongTimeout,
if (e.code != error_code.METHOD_NOT_FOUND) { message: 'Flutter Driver extension is taking a long time to become available. '
rethrow; 'Ensure your test app (often "lib/main.dart") imports '
} '"package:flutter_driver/driver_extension.dart" and '
_log( 'calls enableFlutterDriverExtension() as the first call in main().',
'Check Health failed, try to wait for the service extensions to be ' );
'registered.'
);
await enableIsolateStreams();
await waitForServiceExtension();
return driver.checkHealth();
}
}
final Health health = await checkHealth(); final Health health = await driver.checkHealth();
if (health.status != HealthStatus.ok) { if (health.status != HealthStatus.ok) {
await client.close(); await client.close();
throw DriverError('Flutter application health check failed.'); throw DriverError('Flutter application health check failed.');
......
...@@ -49,6 +49,10 @@ void main() { ...@@ -49,6 +49,10 @@ void main() {
when(mockClient.getVM()).thenAnswer((_) => Future<MockVM>.value(mockVM)); when(mockClient.getVM()).thenAnswer((_) => Future<MockVM>.value(mockVM));
when(mockVM.isolates).thenReturn(<VMRunnableIsolate>[mockIsolate]); when(mockVM.isolates).thenReturn(<VMRunnableIsolate>[mockIsolate]);
when(mockIsolate.loadRunnable()).thenAnswer((_) => Future<MockIsolate>.value(mockIsolate)); when(mockIsolate.loadRunnable()).thenAnswer((_) => Future<MockIsolate>.value(mockIsolate));
when(mockIsolate.extensionRpcs).thenReturn(<String>[]);
when(mockIsolate.onExtensionAdded).thenAnswer((Invocation invocation) {
return Stream<String>.fromIterable(<String>['ext.flutter.driver']);
});
when(mockIsolate.invokeExtension(any, any)).thenAnswer( when(mockIsolate.invokeExtension(any, any)).thenAnswer(
(Invocation invocation) => makeMockResponse(<String, dynamic>{'status': 'ok'})); (Invocation invocation) => makeMockResponse(<String, dynamic>{'status': 'ok'}));
vmServiceConnectFunction = (String url, {Map<String, dynamic> headers}) { vmServiceConnectFunction = (String url, {Map<String, dynamic> headers}) {
...@@ -81,7 +85,7 @@ void main() { ...@@ -81,7 +85,7 @@ void main() {
final FlutterDriver driver = await FlutterDriver.connect(dartVmServiceUrl: ''); final FlutterDriver driver = await FlutterDriver.connect(dartVmServiceUrl: '');
expect(driver, isNotNull); expect(driver, isNotNull);
expectLogContains('Isolate is paused at start'); expectLogContains('Isolate is paused at start');
expect(connectionLog, <String>['streamListen', 'onExtensionAdded', 'resume']); expect(connectionLog, <String>['resume', 'streamListen', 'onExtensionAdded']);
}); });
test('connects to isolate paused mid-flight', () async { test('connects to isolate paused mid-flight', () async {
...@@ -117,6 +121,18 @@ void main() { ...@@ -117,6 +121,18 @@ void main() {
expectLogContains('Isolate is not paused. Assuming application is ready.'); expectLogContains('Isolate is not paused. Assuming application is ready.');
}); });
test('connects to unpaused when onExtensionAdded does not contain the '
'driver extension', () async {
when(mockIsolate.pauseEvent).thenReturn(MockVMResumeEvent());
when(mockIsolate.extensionRpcs).thenReturn(<String>['ext.flutter.driver']);
when(mockIsolate.onExtensionAdded).thenAnswer((Invocation invocation) {
return const Stream<String>.empty();
});
final FlutterDriver driver = await FlutterDriver.connect(dartVmServiceUrl: '');
expect(driver, isNotNull);
expectLogContains('Isolate is not paused. Assuming application is ready.');
});
test('connects with headers', () async { test('connects with headers', () async {
Map<String, dynamic> actualHeaders; Map<String, dynamic> actualHeaders;
vmServiceConnectFunction = (String url, {Map<String, dynamic> headers}) { vmServiceConnectFunction = (String url, {Map<String, dynamic> headers}) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment