FlutterDriver: allow customizing timeouts using a multiplier (#24066)
FlutterDriver: allow customizing timeouts using a multiplier
diff --git a/packages/flutter_driver/lib/src/driver/driver.dart b/packages/flutter_driver/lib/src/driver/driver.dart
index 3dbbc47..a70bb24 100644
--- a/packages/flutter_driver/lib/src/driver/driver.dart
+++ b/packages/flutter_driver/lib/src/driver/driver.dart
@@ -61,25 +61,38 @@
const List<TimelineStream> _defaultStreams = <TimelineStream>[TimelineStream.all];
+/// Multiplies the timeout values used when establishing a connection to the
+/// Flutter app under test and obtain an instance of [FlutterDriver].
+///
+/// This multiplier applies automatically when using the default implementation
+/// of the [vmServiceConnectFunction].
+///
+/// See also:
+///
+/// * [FlutterDriver.timeoutMultiplier], which multiplies all command timeouts by this number.
+double connectionTimeoutMultiplier = _kDefaultTimeoutMultiplier;
+
+const double _kDefaultTimeoutMultiplier = 1.0;
+
/// Default timeout for short-running RPCs.
-const Duration _kShortTimeout = Duration(seconds: 5);
+Duration _shortTimeout(double multiplier) => const Duration(seconds: 5) * multiplier;
/// Default timeout for awaiting an Isolate to become runnable.
-const Duration _kIsolateLoadRunnableTimeout = Duration(minutes: 1);
+Duration _isolateLoadRunnableTimeout(double multiplier) => const Duration(minutes: 1) * multiplier;
/// Time to delay before driving a Fuchsia module.
-const Duration _kFuchsiaDriveDelay = Duration(milliseconds: 500);
+Duration _fuchsiaDriveDelay(double multiplier) => const Duration(milliseconds: 500) * multiplier;
/// Default timeout for long-running RPCs.
-final Duration _kLongTimeout = _kShortTimeout * 6;
+Duration _longTimeout(double multiplier) => _shortTimeout(multiplier) * 6;
/// Additional amount of time we give the command to finish or timeout remotely
/// before timing out locally.
-final Duration _kRpcGraceTime = _kShortTimeout ~/ 2;
+Duration _rpcGraceTime(double multiplier) => _shortTimeout(multiplier) ~/ 2;
/// The amount of time we wait prior to making the next attempt to connect to
/// the VM service.
-final Duration _kPauseBetweenReconnectAttempts = _kShortTimeout ~/ 5;
+Duration _pauseBetweenReconnectAttempts(double multiplier) => _shortTimeout(multiplier) ~/ 5;
// See https://github.com/dart-lang/sdk/blob/master/runtime/vm/timeline.cc#L32
String _timelineStreamsToString(List<TimelineStream> streams) {
@@ -129,6 +142,7 @@
this._appIsolate, {
bool printCommunication = false,
bool logCommunicationToFile = true,
+ this.timeoutMultiplier = _kDefaultTimeoutMultiplier,
}) : _printCommunication = printCommunication,
_logCommunicationToFile = logCommunicationToFile,
_driverId = _nextDriverId++;
@@ -155,12 +169,15 @@
/// [logCommunicationToFile] determines whether the command communication
/// between the test and the app should be logged to `flutter_driver_commands.log`.
///
+ /// [FlutterDriver] multiplies all command timeouts by [timeoutMultiplier].
+ ///
/// [isolateNumber] (optional) determines the specific isolate to connect to.
/// If this is left as `null`, will connect to the first isolate found
/// running on [dartVmServiceUrl].
///
/// [isolateReadyTimeout] determines how long after we connect to the VM
- /// service we will wait for the first isolate to become runnable.
+ /// service we will wait for the first isolate to become runnable. Explicitly
+ /// specified non-null values are not affected by [timeoutMultiplier].
///
/// [fuchsiaModuleTarget] (optional) If running on a Fuchsia Device, either
/// this or the environment variable `FUCHSIA_MODULE_TARGET` must be set. This
@@ -170,10 +187,12 @@
String dartVmServiceUrl,
bool printCommunication = false,
bool logCommunicationToFile = true,
+ double timeoutMultiplier = _kDefaultTimeoutMultiplier,
int isolateNumber,
- Duration isolateReadyTimeout = _kIsolateLoadRunnableTimeout,
+ Duration isolateReadyTimeout,
Pattern fuchsiaModuleTarget,
}) async {
+ isolateReadyTimeout ??= _isolateLoadRunnableTimeout(timeoutMultiplier);
// If running on a Fuchsia device, connect to the first Isolate whose name
// matches FUCHSIA_MODULE_TARGET.
//
@@ -191,7 +210,7 @@
final List<fuchsia.IsolateRef> refs =
await fuchsiaConnection.getMainIsolatesByPattern(fuchsiaModuleTarget);
final fuchsia.IsolateRef ref = refs.first;
- await Future<void>.delayed(_kFuchsiaDriveDelay);
+ await Future<void>.delayed(_fuchsiaDriveDelay(timeoutMultiplier));
isolateNumber = ref.number;
dartVmServiceUrl = ref.dartVm.uri.toString();
await fuchsiaConnection.stop();
@@ -213,6 +232,7 @@
// Connect to Dart VM services
_log.info('Connecting to Flutter application at $dartVmServiceUrl');
+ connectionTimeoutMultiplier = timeoutMultiplier;
final VMServiceClientConnection connection =
await vmServiceConnectFunction(dartVmServiceUrl);
final VMServiceClient client = connection.client;
@@ -243,7 +263,7 @@
isolate.pauseEvent is! VMPauseExceptionEvent &&
isolate.pauseEvent is! VMPauseInterruptedEvent &&
isolate.pauseEvent is! VMResumeEvent) {
- await Future<void>.delayed(_kShortTimeout ~/ 10);
+ await Future<void>.delayed(_shortTimeout(timeoutMultiplier) ~/ 10);
isolate = await isolateRef.loadRunnable();
}
@@ -251,6 +271,7 @@
client, connection.peer, isolate,
printCommunication: printCommunication,
logCommunicationToFile: logCommunicationToFile,
+ timeoutMultiplier: timeoutMultiplier,
);
// Attempts to resume the isolate, but does not crash if it fails because
@@ -311,7 +332,7 @@
_log.trace('Waiting for service extension');
// We will never receive the extension event if the user does not
// register it. If that happens time out.
- await whenServiceExtensionReady.timeout(_kLongTimeout * 2);
+ await whenServiceExtensionReady.timeout(_longTimeout(timeoutMultiplier) * 2);
} on TimeoutException catch (_) {
throw DriverError(
'Timed out waiting for Flutter Driver extension to become available. '
@@ -352,7 +373,7 @@
'registered.'
);
await enableIsolateStreams();
- await waitForServiceExtension().timeout(_kLongTimeout * 2);
+ await waitForServiceExtension().timeout(_longTimeout(timeoutMultiplier) * 2);
return driver.checkHealth();
}
}
@@ -369,17 +390,30 @@
/// The unique ID of this driver instance.
final int _driverId;
+
/// Client connected to the Dart VM running the Flutter application
final VMServiceClient _serviceClient;
+
/// JSON-RPC client useful for sending raw JSON requests.
final rpc.Peer _peer;
+
/// The main isolate hosting the Flutter application
final VMIsolate _appIsolate;
+
/// Whether to print communication between host and app to `stdout`.
final bool _printCommunication;
+
/// Whether to log communication between host and app to `flutter_driver_commands.log`.
final bool _logCommunicationToFile;
+ /// [FlutterDriver] multiplies all command timeouts by this number.
+ ///
+ /// The right amount of time a driver command should be given to complete
+ /// depends on various environmental factors, such as the speed of the
+ /// device or the emulator, connection speed and latency, and others. Use
+ /// this multiplier to tailor the timeouts to your environment.
+ final double timeoutMultiplier;
+
Future<Map<String, dynamic>> _sendCommand(Command command) async {
Map<String, dynamic> response;
try {
@@ -387,7 +421,7 @@
_logCommunication('>>> $serialized');
response = await _appIsolate
.invokeExtension(_flutterExtensionMethodName, serialized)
- .timeout(command.timeout + _kRpcGraceTime);
+ .timeout(command.timeout + _rpcGraceTime(timeoutMultiplier));
_logCommunication('<<< $response');
} on TimeoutException catch (error, stackTrace) {
throw DriverError(
@@ -419,26 +453,31 @@
/// Checks the status of the Flutter Driver extension.
Future<Health> checkHealth({Duration timeout}) async {
+ timeout ??= _shortTimeout(timeoutMultiplier);
return Health.fromJson(await _sendCommand(GetHealth(timeout: timeout)));
}
/// Returns a dump of the render tree.
Future<RenderTree> getRenderTree({Duration timeout}) async {
+ timeout ??= _shortTimeout(timeoutMultiplier);
return RenderTree.fromJson(await _sendCommand(GetRenderTree(timeout: timeout)));
}
/// Taps at the center of the widget located by [finder].
Future<void> tap(SerializableFinder finder, {Duration timeout}) async {
+ timeout ??= _shortTimeout(timeoutMultiplier);
await _sendCommand(Tap(finder, timeout: timeout));
}
/// Waits until [finder] locates the target.
Future<void> waitFor(SerializableFinder finder, {Duration timeout}) async {
+ timeout ??= _shortTimeout(timeoutMultiplier);
await _sendCommand(WaitFor(finder, timeout: timeout));
}
/// Waits until [finder] can no longer locate the target.
Future<void> waitForAbsent(SerializableFinder finder, {Duration timeout}) async {
+ timeout ??= _shortTimeout(timeoutMultiplier);
await _sendCommand(WaitForAbsent(finder, timeout: timeout));
}
@@ -447,6 +486,7 @@
/// Use this method when you need to wait for the moment when the application
/// becomes "stable", for example, prior to taking a [screenshot].
Future<void> waitUntilNoTransientCallbacks({Duration timeout}) async {
+ timeout ??= _shortTimeout(timeoutMultiplier);
await _sendCommand(WaitUntilNoTransientCallbacks(timeout: timeout));
}
@@ -464,6 +504,7 @@
/// The move events are generated at a given [frequency] in Hz (or events per
/// second). It defaults to 60Hz.
Future<void> scroll(SerializableFinder finder, double dx, double dy, Duration duration, { int frequency = 60, Duration timeout }) async {
+ timeout ??= _shortTimeout(timeoutMultiplier);
await _sendCommand(Scroll(finder, dx, dy, duration, frequency, timeout: timeout));
}
@@ -475,6 +516,7 @@
/// then this method may fail because [finder] doesn't actually exist.
/// The [scrollUntilVisible] method can be used in this case.
Future<void> scrollIntoView(SerializableFinder finder, { double alignment = 0.0, Duration timeout }) async {
+ timeout ??= _shortTimeout(timeoutMultiplier);
await _sendCommand(ScrollIntoView(finder, alignment: alignment, timeout: timeout));
}
@@ -531,6 +573,7 @@
/// Returns the text in the `Text` widget located by [finder].
Future<String> getText(SerializableFinder finder, { Duration timeout }) async {
+ timeout ??= _shortTimeout(timeoutMultiplier);
return GetTextResult.fromJson(await _sendCommand(GetText(finder, timeout: timeout))).text;
}
@@ -567,6 +610,7 @@
/// });
/// ```
Future<void> enterText(String text, { Duration timeout }) async {
+ timeout ??= _shortTimeout(timeoutMultiplier);
await _sendCommand(EnterText(text, timeout: timeout));
}
@@ -585,6 +629,7 @@
/// channel will be mocked out.
Future<void> setTextEntryEmulation({ @required bool enabled, Duration timeout }) async {
assert(enabled != null);
+ timeout ??= _shortTimeout(timeoutMultiplier);
await _sendCommand(SetTextEntryEmulation(enabled, timeout: timeout));
}
@@ -595,6 +640,7 @@
/// callback in [enableFlutterDriverExtension] that can successfully handle
/// these requests.
Future<String> requestData(String message, { Duration timeout }) async {
+ timeout ??= _shortTimeout(timeoutMultiplier);
return RequestDataResult.fromJson(await _sendCommand(RequestData(message, timeout: timeout))).message;
}
@@ -602,7 +648,8 @@
///
/// Returns true when the call actually changed the state from on to off or
/// vice versa.
- Future<bool> setSemantics(bool enabled, { Duration timeout = _kShortTimeout }) async {
+ Future<bool> setSemantics(bool enabled, { Duration timeout }) async {
+ timeout ??= _shortTimeout(timeoutMultiplier);
final SetSemanticsResult result = SetSemanticsResult.fromJson(await _sendCommand(SetSemantics(enabled, timeout: timeout)));
return result.changedState;
}
@@ -615,7 +662,8 @@
///
/// Semantics must be enabled to use this method, either using a platform
/// specific shell command or [setSemantics].
- Future<int> getSemanticsId(SerializableFinder finder, { Duration timeout = _kShortTimeout}) async {
+ Future<int> getSemanticsId(SerializableFinder finder, { Duration timeout }) async {
+ timeout ??= _shortTimeout(timeoutMultiplier);
final Map<String, dynamic> jsonResponse = await _sendCommand(GetSemanticsId(finder, timeout: timeout));
final GetSemanticsIdResult result = GetSemanticsIdResult.fromJson(jsonResponse);
return result.id;
@@ -623,7 +671,7 @@
/// Take a screenshot. The image will be returned as a PNG.
Future<List<int>> screenshot({ Duration timeout }) async {
- timeout ??= _kLongTimeout;
+ timeout ??= _longTimeout(timeoutMultiplier);
// HACK: this artificial delay here is to deal with a race between the
// driver script and the GPU thread. The issue is that driver API
@@ -683,7 +731,8 @@
/// ]
///
/// [getFlagList]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#getflaglist
- Future<List<Map<String, dynamic>>> getVmFlags({ Duration timeout = _kShortTimeout }) async {
+ Future<List<Map<String, dynamic>>> getVmFlags({ Duration timeout }) async {
+ timeout ??= _shortTimeout(timeoutMultiplier);
final Map<String, dynamic> result = await _peer.sendRequest('getFlagList').timeout(timeout);
return result['flags'];
}
@@ -691,8 +740,9 @@
/// Starts recording performance traces.
Future<void> startTracing({
List<TimelineStream> streams = _defaultStreams,
- Duration timeout = _kShortTimeout,
+ Duration timeout,
}) async {
+ timeout ??= _shortTimeout(timeoutMultiplier);
assert(streams != null && streams.isNotEmpty);
try {
await _peer.sendRequest(_setVMTimelineFlagsMethodName, <String, String>{
@@ -708,7 +758,8 @@
}
/// Stops recording performance traces and downloads the timeline.
- Future<Timeline> stopTracingAndDownloadTimeline({ Duration timeout = _kShortTimeout }) async {
+ Future<Timeline> stopTracingAndDownloadTimeline({ Duration timeout }) async {
+ timeout ??= _shortTimeout(timeoutMultiplier);
try {
await _peer
.sendRequest(_setVMTimelineFlagsMethodName, <String, String>{'recordedStreams': '[]'})
@@ -751,7 +802,8 @@
}
/// Clears all timeline events recorded up until now.
- Future<void> clearTimeline({ Duration timeout = _kShortTimeout }) async {
+ Future<void> clearTimeline({ Duration timeout }) async {
+ timeout ??= _shortTimeout(timeoutMultiplier);
try {
await _peer
.sendRequest(_clearVMTimelineMethodName, <String, String>{})
@@ -782,6 +834,7 @@
/// ensure that no action is performed while the app is undergoing a
/// transition to avoid flakiness.
Future<T> runUnsynchronized<T>(Future<T> action(), { Duration timeout }) async {
+ timeout ??= _shortTimeout(timeoutMultiplier);
await _sendCommand(SetFrameSync(false, timeout: timeout));
T result;
try {
@@ -841,6 +894,11 @@
///
/// Overwrite this function if you require a custom method for connecting to
/// the VM service.
+///
+/// See also:
+///
+/// * [connectionTimeoutMultiplier], which controls the timeouts while
+/// establishing a connection using the default connection function.
VMServiceConnectFunction vmServiceConnectFunction = _waitAndConnect;
/// Restores [vmServiceConnectFunction] to its default value.
@@ -863,8 +921,8 @@
WebSocket ws1;
WebSocket ws2;
try {
- ws1 = await WebSocket.connect(uri.toString()).timeout(_kShortTimeout);
- ws2 = await WebSocket.connect(uri.toString()).timeout(_kShortTimeout);
+ ws1 = await WebSocket.connect(uri.toString()).timeout(_shortTimeout(connectionTimeoutMultiplier));
+ ws2 = await WebSocket.connect(uri.toString()).timeout(_shortTimeout(connectionTimeoutMultiplier));
return VMServiceClientConnection(
VMServiceClient(IOWebSocketChannel(ws1).cast()),
rpc.Peer(IOWebSocketChannel(ws2).cast())..listen()
@@ -873,9 +931,9 @@
await ws1?.close();
await ws2?.close();
- if (timer.elapsed < _kLongTimeout * 2) {
+ if (timer.elapsed < _longTimeout(connectionTimeoutMultiplier) * 2) {
_log.info('Waiting for application to start');
- await Future<void>.delayed(_kPauseBetweenReconnectAttempts);
+ await Future<void>.delayed(_pauseBetweenReconnectAttempts(connectionTimeoutMultiplier));
return attemptConnection();
} else {
_log.critical(
diff --git a/packages/flutter_driver/test/flutter_driver_test.dart b/packages/flutter_driver/test/flutter_driver_test.dart
index fb0318ae..4a72a16 100644
--- a/packages/flutter_driver/test/flutter_driver_test.dart
+++ b/packages/flutter_driver/test/flutter_driver_test.dart
@@ -17,6 +17,7 @@
/// Magical timeout value that's different from the default.
const Duration _kTestTimeout = Duration(milliseconds: 1234);
const String _kSerializedTestTimeout = '1234';
+const Duration _kDefaultCommandTimeout = Duration(seconds: 5);
void main() {
group('FlutterDriver.connect', () {
@@ -386,6 +387,43 @@
});
});
});
+
+ group('FlutterDriver with custom timeout', () {
+ const double kTestMultiplier = 3.0;
+ MockVMServiceClient mockClient;
+ MockPeer mockPeer;
+ MockIsolate mockIsolate;
+ FlutterDriver driver;
+
+ setUp(() {
+ mockClient = MockVMServiceClient();
+ mockPeer = MockPeer();
+ mockIsolate = MockIsolate();
+ driver = FlutterDriver.connectedTo(mockClient, mockPeer, mockIsolate, timeoutMultiplier: kTestMultiplier);
+ });
+
+ test('multiplies the timeout', () async {
+ when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
+ expect(i.positionalArguments[1], <String, String>{
+ 'command': 'get_health',
+ 'timeout': '${(_kDefaultCommandTimeout * kTestMultiplier).inMilliseconds}',
+ });
+ return makeMockResponse(<String, dynamic>{'status': 'ok'});
+ });
+ await driver.checkHealth();
+ });
+
+ test('does not multiply explicit timeouts', () async {
+ when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
+ expect(i.positionalArguments[1], <String, String>{
+ 'command': 'get_health',
+ 'timeout': _kSerializedTestTimeout,
+ });
+ return makeMockResponse(<String, dynamic>{'status': 'ok'});
+ });
+ await driver.checkHealth(timeout: _kTestTimeout);
+ });
+ });
}
Future<Map<String, dynamic>> makeMockResponse(