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(