driver.dart 26.2 KB
Newer Older
1 2 3 4 5
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6
import 'dart:convert';
7
import 'dart:io';
8 9

import 'package:file/file.dart' as f;
10
import 'package:json_rpc_2/error_code.dart' as error_code;
11
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
12
import 'package:meta/meta.dart';
13
import 'package:path/path.dart' as p;
14 15
import 'package:vm_service_client/vm_service_client.dart';
import 'package:web_socket_channel/io.dart';
16

17
import 'common.dart';
18 19
import 'error.dart';
import 'find.dart';
20
import 'frame_sync.dart';
21 22 23
import 'gesture.dart';
import 'health.dart';
import 'message.dart';
24
import 'render_tree.dart';
25
import 'request_data.dart';
26
import 'semantics.dart';
27
import 'timeline.dart';
28

29
/// Timeline stream identifier.
30
enum TimelineStream {
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
  /// A meta-identifier that instructs the Dart VM to record all streams.
  all,

  /// Marks events related to calls made via Dart's C API.
  api,

  /// Marks events from the Dart VM's JIT compiler.
  compiler,

  /// Marks events emitted using the `dart:developer` API.
  dart,

  /// Marks events from the Dart VM debugger.
  debugger,

  /// Marks events emitted using the `dart_tools_api.h` C API.
  embedder,

  /// Marks events from the garbage collector.
  gc,

  /// Marks events related to message passing between Dart isolates.
  isolate,

  /// Marks internal VM events.
  vm,
57 58
}

59
const List<TimelineStream> _defaultStreams = const <TimelineStream>[TimelineStream.all];
60

61 62 63
/// Default timeout for short-running RPCs.
const Duration _kShortTimeout = const Duration(seconds: 5);

64 65 66 67 68 69 70 71 72 73 74
/// Default timeout for long-running RPCs.
final Duration _kLongTimeout = _kShortTimeout * 6;

/// Additional amount of time we give the command to finish or timeout remotely
/// before timing out locally.
final Duration _kRpcGraceTime = _kShortTimeout ~/ 2;

/// The amount of time we wait prior to making the next attempt to connect to
/// the VM service.
final Duration _kPauseBetweenReconnectAttempts = _kShortTimeout ~/ 5;

75
// See https://github.com/dart-lang/sdk/blob/master/runtime/vm/timeline.cc#L32
76 77
String _timelineStreamsToString(List<TimelineStream> streams) {
  final String contents = streams.map((TimelineStream stream) {
78
    switch (stream) {
79 80 81 82 83 84 85 86 87
      case TimelineStream.all: return 'all';
      case TimelineStream.api: return 'API';
      case TimelineStream.compiler: return 'Compiler';
      case TimelineStream.dart: return 'Dart';
      case TimelineStream.debugger: return 'Debugger';
      case TimelineStream.embedder: return 'Embedder';
      case TimelineStream.gc: return 'GC';
      case TimelineStream.isolate: return 'Isolate';
      case TimelineStream.vm: return 'VM';
88
      default:
89
        throw 'Unknown timeline stream $stream';
90 91 92 93 94
    }
  }).join(', ');
  return '[$contents]';
}

yjbanov's avatar
yjbanov committed
95
final Logger _log = new Logger('FlutterDriver');
96

97 98 99 100
/// A convenient accessor to frequently used finders.
///
/// Examples:
///
101
///     driver.tap(find.text('Save'));
102 103 104
///     driver.scroll(find.byValueKey(42));
const CommonFinders find = const CommonFinders._();

105 106 107 108 109 110 111
/// Computes a value.
///
/// If computation is asynchronous, the function may return a [Future].
///
/// See also [FlutterDriver.waitFor].
typedef dynamic EvaluatorFunction();

112 113
/// Drives a Flutter Application running in another process.
class FlutterDriver {
114 115 116
  /// Creates a driver that uses a connection provided by the given
  /// [_serviceClient], [_peer] and [_appIsolate].
  @visibleForTesting
117 118 119 120 121 122 123 124 125
  FlutterDriver.connectedTo(
    this._serviceClient,
    this._peer,
    this._appIsolate, {
    bool printCommunication: false,
    bool logCommunicationToFile: true,
  }) : _printCommunication = printCommunication,
       _logCommunicationToFile = logCommunicationToFile,
       _driverId = _nextDriverId++;
126

127
  static const String _kFlutterExtensionMethod = 'ext.flutter.driver';
128 129
  static const String _kSetVMTimelineFlagsMethod = '_setVMTimelineFlags';
  static const String _kGetVMTimelineMethod = '_getVMTimeline';
130

131 132
  static int _nextDriverId = 0;

133 134 135 136
  /// Connects to a Flutter application.
  ///
  /// Resumes the application if it is currently paused (e.g. at a breakpoint).
  ///
137 138
  /// [dartVmServiceUrl] is the URL to Dart observatory (a.k.a. VM service). If
  /// not specified, the URL specified by the `VM_SERVICE_URL` environment
139
  /// variable is used. One or the other must be specified.
140 141 142 143 144 145 146 147 148
  ///
  /// [printCommunication] determines whether the command communication between
  /// the test and the app should be printed to stdout.
  ///
  /// [logCommunicationToFile] determines whether the command communication
  /// between the test and the app should be logged to `flutter_driver_commands.log`.
  static Future<FlutterDriver> connect({ String dartVmServiceUrl,
                                         bool printCommunication: false,
                                         bool logCommunicationToFile: true }) async {
149
    dartVmServiceUrl ??= Platform.environment['VM_SERVICE_URL'];
150 151 152 153 154 155 156 157

    if (dartVmServiceUrl == null) {
      throw new DriverError(
        'Could not determine URL to connect to application.\n'
        'Either the VM_SERVICE_URL environment variable should be set, or an explicit\n'
        'URL should be provided to the FlutterDriver.connect() method.'
      );
    }
158

159 160
    // Connect to Dart VM servcies
    _log.info('Connecting to Flutter application at $dartVmServiceUrl');
161 162 163
    final VMServiceClientConnection connection = await vmServiceConnectFunction(dartVmServiceUrl);
    final VMServiceClient client = connection.client;
    final VM vm = await client.getVM();
164
    _log.trace('Looking for the isolate');
165 166
    VMIsolate isolate = await vm.isolates.first.loadRunnable();

167
    // TODO(yjbanov): vm_service_client does not support "None" pause event yet.
168 169
    // It is currently reported as null, but we cannot rely on it because
    // eventually the event will be reported as a non-null object. For now,
170 171
    // list all the events we know about. Later we'll check for "None" event
    // explicitly.
172
    //
173 174 175 176 177 178 179
    // See: https://github.com/dart-lang/vm_service_client/issues/4
    if (isolate.pauseEvent is! VMPauseStartEvent &&
        isolate.pauseEvent is! VMPauseExitEvent &&
        isolate.pauseEvent is! VMPauseBreakpointEvent &&
        isolate.pauseEvent is! VMPauseExceptionEvent &&
        isolate.pauseEvent is! VMPauseInterruptedEvent &&
        isolate.pauseEvent is! VMResumeEvent) {
180
      await new Future<Null>.delayed(_kShortTimeout ~/ 10);
181 182 183
      isolate = await vm.isolates.first.loadRunnable();
    }

184
    final FlutterDriver driver = new FlutterDriver.connectedTo(
185 186 187 188
        client, connection.peer, isolate,
        printCommunication: printCommunication,
        logCommunicationToFile: logCommunicationToFile
    );
189 190 191 192

    // Attempts to resume the isolate, but does not crash if it fails because
    // the isolate is already resumed. There could be a race with other tools,
    // such as a debugger, any of which could have resumed the isolate.
Ian Hickson's avatar
Ian Hickson committed
193
    Future<dynamic> resumeLeniently() {
194
      _log.trace('Attempting to resume isolate');
195 196
      return isolate.resume().catchError((dynamic e) {
        const int vmMustBePausedCode = 101;
197 198 199 200 201 202 203 204 205 206 207 208 209 210
        if (e is rpc.RpcException && e.code == vmMustBePausedCode) {
          // No biggie; something else must have resumed the isolate
          _log.warning(
            'Attempted to resume an already resumed isolate. This may happen '
            'when we lose a race with another tool (usually a debugger) that '
            'is connected to the same isolate.'
          );
        } else {
          // Failed to resume due to another reason. Fail hard.
          throw e;
        }
      });
    }

211 212
    /// Waits for a signal from the VM service that the extension is registered.
    /// Returns [_kFlutterExtensionMethod]
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
    Future<String> waitForServiceExtension() {
      return isolate.onExtensionAdded.firstWhere((String extension) {
        return extension == _kFlutterExtensionMethod;
      });
    }

    /// Tells the Dart VM Service to notify us about "Isolate" events.
    ///
    /// This is a workaround for an issue in package:vm_service_client, which
    /// subscribes to the "Isolate" stream lazily upon subscription, which
    /// results in lost events.
    ///
    /// Details: https://github.com/dart-lang/vm_service_client/issues/17
    Future<Null> enableIsolateStreams() async {
      await connection.peer.sendRequest('streamListen', <String, String>{
        'streamId': 'Isolate',
      });
    }

232 233 234 235 236 237 238
    // Attempt to resume isolate if it was paused
    if (isolate.pauseEvent is VMPauseStartEvent) {
      _log.trace('Isolate is paused at start.');

      // If the isolate is paused at the start, e.g. via the --start-paused
      // option, then the VM service extension is not registered yet. Wait for
      // it to be registered.
239
      await enableIsolateStreams();
240
      final Future<dynamic> whenServiceExtensionReady = waitForServiceExtension();
241
      final Future<dynamic> whenResumed = resumeLeniently();
242 243 244 245
      await whenResumed;

      try {
        _log.trace('Waiting for service extension');
246 247
        // We will never receive the extension event if the user does not
        // register it. If that happens time out.
248 249
        await whenServiceExtensionReady.timeout(_kLongTimeout * 2);
      } on TimeoutException catch (_) {
250 251
        throw new DriverError(
          'Timed out waiting for Flutter Driver extension to become available. '
252 253 254
          'Ensure your test app (often: lib/main.dart) imports '
          '"package:flutter_driver/driver_extension.dart" and '
          'calls enableFlutterDriverExtension() as the first call in main().'
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
        );
      }
    } else if (isolate.pauseEvent is VMPauseExitEvent ||
               isolate.pauseEvent is VMPauseBreakpointEvent ||
               isolate.pauseEvent is VMPauseExceptionEvent ||
               isolate.pauseEvent is VMPauseInterruptedEvent) {
      // If the isolate is paused for any other reason, assume the extension is
      // already there.
      _log.trace('Isolate is paused mid-flight.');
      await resumeLeniently();
    } else if (isolate.pauseEvent is VMResumeEvent) {
      _log.trace('Isolate is not paused. Assuming application is ready.');
    } else {
      _log.warning(
        'Unknown pause event type ${isolate.pauseEvent.runtimeType}. '
        'Assuming application is ready.'
      );
    }

274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
    // Invoked checkHealth and try to fix delays in the registration of Service
    // extensions
    Future<Health> checkHealth() async {
      try {
        // At this point the service extension must be installed. Verify it.
        return await driver.checkHealth();
      } on rpc.RpcException catch (e) {
        if (e.code != error_code.METHOD_NOT_FOUND) {
          rethrow;
        }
        _log.trace(
          'Check Health failed, try to wait for the service extensions to be'
          'registered.'
        );
        await enableIsolateStreams();
        await waitForServiceExtension().timeout(_kLongTimeout * 2);
        return driver.checkHealth();
      }
    }

    final Health health = await checkHealth();
295
    if (health.status != HealthStatus.ok) {
296
      await client.close();
297 298 299 300 301 302 303
      throw new DriverError('Flutter application health check failed.');
    }

    _log.info('Connected to Flutter application.');
    return driver;
  }

304 305
  /// The unique ID of this driver instance.
  final int _driverId;
306 307
  /// Client connected to the Dart VM running the Flutter application
  final VMServiceClient _serviceClient;
308 309
  /// JSON-RPC client useful for sending raw JSON requests.
  final rpc.Peer _peer;
310 311
  /// The main isolate hosting the Flutter application
  final VMIsolateRef _appIsolate;
312 313 314 315
  /// 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;
316 317

  Future<Map<String, dynamic>> _sendCommand(Command command) async {
318
    Map<String, dynamic> response;
319
    try {
320
      final Map<String, String> serialized = command.serialize();
321 322 323
      _logCommunication('>>> $serialized');
      response = await _appIsolate
          .invokeExtension(_kFlutterExtensionMethod, serialized)
324
          .timeout(command.timeout + _kRpcGraceTime);
325
      _logCommunication('<<< $response');
326 327 328 329 330 331
    } on TimeoutException catch (error, stackTrace) {
      throw new DriverError(
        'Failed to fulfill ${command.runtimeType}: Flutter application not responding',
        error,
        stackTrace
      );
332 333 334 335 336 337 338
    } catch (error, stackTrace) {
      throw new DriverError(
        'Failed to fulfill ${command.runtimeType} due to remote error',
        error,
        stackTrace
      );
    }
339 340 341
    if (response['isError'])
      throw new DriverError('Error in Flutter application: ${response['response']}');
    return response['response'];
342 343
  }

344 345 346 347
  void _logCommunication(String message)  {
    if (_printCommunication)
      _log.info(message);
    if (_logCommunicationToFile) {
348
      final f.File file = fs.file(p.join(testOutputsDirectory, 'flutter_driver_commands_$_driverId.log'));
349 350 351 352 353
      file.createSync(recursive: true); // no-op if file exists
      file.writeAsStringSync('${new DateTime.now()} $message\n', mode: f.FileMode.APPEND, flush: true);
    }
  }

354
  /// Checks the status of the Flutter Driver extension.
355 356
  Future<Health> checkHealth({Duration timeout}) async {
    return Health.fromJson(await _sendCommand(new GetHealth(timeout: timeout)));
357 358
  }

359
  /// Returns a dump of the render tree.
360 361
  Future<RenderTree> getRenderTree({Duration timeout}) async {
    return RenderTree.fromJson(await _sendCommand(new GetRenderTree(timeout: timeout)));
362 363
  }

364
  /// Taps at the center of the widget located by [finder].
365 366
  Future<Null> tap(SerializableFinder finder, {Duration timeout}) async {
    await _sendCommand(new Tap(finder, timeout: timeout));
367
    return null;
368
  }
369

370
  /// Waits until [finder] locates the target.
371
  Future<Null> waitFor(SerializableFinder finder, {Duration timeout}) async {
372 373
    await _sendCommand(new WaitFor(finder, timeout: timeout));
    return null;
374 375
  }

376 377 378 379 380 381
  /// Waits until [finder] can no longer locate the target.
  Future<Null> waitForAbsent(SerializableFinder finder, {Duration timeout}) async {
    await _sendCommand(new WaitForAbsent(finder, timeout: timeout));
    return null;
  }

382 383 384 385 386 387 388 389 390
  /// Waits until there are no more transient callbacks in the queue.
  ///
  /// Use this method when you need to wait for the moment when the application
  /// becomes "stable", for example, prior to taking a [screenshot].
  Future<Null> waitUntilNoTransientCallbacks({Duration timeout}) async {
    await _sendCommand(new WaitUntilNoTransientCallbacks(timeout: timeout));
    return null;
  }

391 392 393 394 395 396 397 398 399
  /// Tell the driver to perform a scrolling action.
  ///
  /// A scrolling action begins with a "pointer down" event, which commonly maps
  /// to finger press on the touch screen or mouse button press. A series of
  /// "pointer move" events follow. The action is completed by a "pointer up"
  /// event.
  ///
  /// [dx] and [dy] specify the total offset for the entire scrolling action.
  ///
400
  /// [duration] specifies the length of the action.
401 402 403
  ///
  /// The move events are generated at a given [frequency] in Hz (or events per
  /// second). It defaults to 60Hz.
404 405
  Future<Null> scroll(SerializableFinder finder, double dx, double dy, Duration duration, { int frequency: 60, Duration timeout }) async {
    return await _sendCommand(new Scroll(finder, dx, dy, duration, frequency, timeout: timeout)).then((Map<String, dynamic> _) => null);
406 407
  }

408 409
  /// Scrolls the Scrollable ancestor of the widget located by [finder]
  /// until the widget is completely visible.
410 411
  Future<Null> scrollIntoView(SerializableFinder finder, { double alignment: 0.0, Duration timeout }) async {
    return await _sendCommand(new ScrollIntoView(finder, alignment: alignment, timeout: timeout)).then((Map<String, dynamic> _) => null);
412 413
  }

414
  /// Returns the text in the `Text` widget located by [finder].
415 416
  Future<String> getText(SerializableFinder finder, { Duration timeout }) async {
    return GetTextResult.fromJson(await _sendCommand(new GetText(finder, timeout: timeout))).text;
417 418
  }

419 420
  /// Sends a string and returns a string.
  ///
421 422 423 424
  /// This enables generic communication between the driver and the application.
  /// It's expected that the application has registered a [DataHandler]
  /// callback in [enableFlutterDriverExtension] that can successfully handle
  /// these requests.
425 426 427 428
  Future<String> requestData(String message, { Duration timeout }) async {
    return RequestDataResult.fromJson(await _sendCommand(new RequestData(message, timeout: timeout))).message;
  }

429 430
  /// Turns semantics on or off in the Flutter app under test.
  ///
431
  /// Returns true when the call actually changed the state from on to off or
432 433 434 435 436 437
  /// vice versa.
  Future<bool> setSemantics(bool enabled, { Duration timeout: _kShortTimeout }) async {
    final SetSemanticsResult result = SetSemanticsResult.fromJson(await _sendCommand(new SetSemantics(enabled, timeout: timeout)));
    return result.changedState;
  }

438
  /// Take a screenshot.  The image will be returned as a PNG.
439 440
  Future<List<int>> screenshot({ Duration timeout }) async {
    timeout ??= _kLongTimeout;
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476

    // 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
    //       synchronizes with the framework based on transient callbacks, which
    //       are out of sync with the GPU thread. Here's the timeline of events
    //       in ASCII art:
    //
    //       -------------------------------------------------------------------
    //       Before this change:
    //       -------------------------------------------------------------------
    //       UI    : <-- build -->
    //       GPU   :               <-- rasterize -->
    //       Gap   :              | random |
    //       Driver:                        <-- screenshot -->
    //
    //       In the diagram above, the gap is the time between the last driver
    //       action taken, such as a `tap()`, and the subsequent call to
    //       `screenshot()`. The gap is random because it is determined by the
    //       unpredictable network communication between the driver process and
    //       the application. If this gap is too short, the screenshot is taken
    //       before the GPU thread is done rasterizing the frame, so the
    //       screenshot of the previous frame is taken, which is wrong.
    //
    //       -------------------------------------------------------------------
    //       After this change:
    //       -------------------------------------------------------------------
    //       UI    : <-- build -->
    //       GPU   :               <-- rasterize -->
    //       Gap   :              |    2 seconds or more   |
    //       Driver:                                        <-- screenshot -->
    //
    //       The two-second gap should be long enough for the GPU thread to
    //       finish rasterizing the frame, but not longer than necessary to keep
    //       driver tests as fast a possible.
    await new Future<Null>.delayed(const Duration(seconds: 2));

477
    final Map<String, dynamic> result = await _peer.sendRequest('_flutter.screenshot').timeout(timeout);
478 479 480
    return BASE64.decode(result['screenshot']);
  }

481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499
  /// Returns the Flags set in the Dart VM as JSON.
  ///
  /// See the complete documentation for `getFlagList` Dart VM service method
  /// [here][getFlagList].
  ///
  /// Example return value:
  ///
  ///     [
  ///       {
  ///         "name": "timeline_recorder",
  ///         "comment": "Select the timeline recorder used. Valid values: ring, endless, startup, and systrace.",
  ///         "modified": false,
  ///         "_flagType": "String",
  ///         "valueAsString": "ring"
  ///       },
  ///       ...
  ///     ]
  ///
  /// [getFlagList]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#getflaglist
500 501
  Future<List<Map<String, dynamic>>> getVmFlags({ Duration timeout: _kShortTimeout }) async {
    final Map<String, dynamic> result = await _peer.sendRequest('getFlagList').timeout(timeout);
502 503 504
    return result['flags'];
  }

505
  /// Starts recording performance traces.
506
  Future<Null> startTracing({ List<TimelineStream> streams: _defaultStreams, Duration timeout: _kShortTimeout }) async {
507
    assert(streams != null && streams.isNotEmpty);
508
    try {
pq's avatar
pq committed
509
      await _peer.sendRequest(_kSetVMTimelineFlagsMethod, <String, String>{
510
        'recordedStreams': _timelineStreamsToString(streams)
511
      }).timeout(timeout);
512 513 514 515 516 517 518 519 520 521
      return null;
    } catch(error, stackTrace) {
      throw new DriverError(
        'Failed to start tracing due to remote error',
        error,
        stackTrace
      );
    }
  }

522
  /// Stops recording performance traces and downloads the timeline.
523
  Future<Timeline> stopTracingAndDownloadTimeline({ Duration timeout: _kShortTimeout }) async {
524
    try {
525 526 527
      await _peer
          .sendRequest(_kSetVMTimelineFlagsMethod, <String, String>{'recordedStreams': '[]'})
          .timeout(timeout);
528
      return new Timeline.fromJson(await _peer.sendRequest(_kGetVMTimelineMethod));
529 530
    } catch(error, stackTrace) {
      throw new DriverError(
531
        'Failed to stop tracing due to remote error',
532 533 534
        error,
        stackTrace
      );
535 536 537 538 539 540 541 542 543
    }
  }

  /// Runs [action] and outputs a performance trace for it.
  ///
  /// Waits for the `Future` returned by [action] to complete prior to stopping
  /// the trace.
  ///
  /// This is merely a convenience wrapper on top of [startTracing] and
544
  /// [stopTracingAndDownloadTimeline].
545 546 547
  ///
  /// [streams] limits the recorded timeline event streams to only the ones
  /// listed. By default, all streams are recorded.
548 549
  Future<Timeline> traceAction(Future<dynamic> action(), { List<TimelineStream> streams: _defaultStreams }) async {
    await startTracing(streams: streams);
550
    await action();
551
    return stopTracingAndDownloadTimeline();
552 553
  }

554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569
  /// [action] will be executed with the frame sync mechanism disabled.
  ///
  /// By default, Flutter Driver waits until there is no pending frame scheduled
  /// in the app under test before executing an action. This mechanism is called
  /// "frame sync". It greatly reduces flakiness because Flutter Driver will not
  /// execute an action while the app under test is undergoing a transition.
  ///
  /// Having said that, sometimes it is necessary to disable the frame sync
  /// mechanism (e.g. if there is an ongoing animation in the app, it will
  /// never reach a state where there are no pending frames scheduled and the
  /// action will time out). For these cases, the sync mechanism can be disabled
  /// by wrapping the actions to be performed by this [runUnsynchronized] method.
  ///
  /// With frame sync disabled, its the responsibility of the test author to
  /// ensure that no action is performed while the app is undergoing a
  /// transition to avoid flakiness.
570 571
  Future<T> runUnsynchronized<T>(Future<T> action(), { Duration timeout }) async {
    await _sendCommand(new SetFrameSync(false, timeout: timeout));
572
    T result;
573 574 575
    try {
      result = await action();
    } finally {
576
      await _sendCommand(new SetFrameSync(true, timeout: timeout));
577 578 579 580
    }
    return result;
  }

581 582 583
  /// Closes the underlying connection to the VM service.
  ///
  /// Returns a [Future] that fires once the connection has been closed.
584
  Future<Null> close() async {
585
    // Don't leak vm_service_client-specific objects, if any
586 587 588
    await _serviceClient.close();
    await _peer.close();
  }
589
}
yjbanov's avatar
yjbanov committed
590

591
/// Encapsulates connection information to an instance of a Flutter application.
592
@visibleForTesting
593 594 595 596 597 598 599 600 601 602
class VMServiceClientConnection {
  /// Use this for structured access to the VM service's public APIs.
  final VMServiceClient client;

  /// Use this to make arbitrary raw JSON-RPC calls.
  ///
  /// This object allows reaching into private VM service APIs. Use with
  /// caution.
  final rpc.Peer peer;

603
  /// Creates an instance of this class given a [client] and a [peer].
604 605 606
  VMServiceClientConnection(this.client, this.peer);
}

yjbanov's avatar
yjbanov committed
607
/// A function that connects to a Dart VM service given the [url].
608
typedef Future<VMServiceClientConnection> VMServiceConnectFunction(String url);
yjbanov's avatar
yjbanov committed
609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624

/// The connection function used by [FlutterDriver.connect].
///
/// Overwrite this function if you require a custom method for connecting to
/// the VM service.
VMServiceConnectFunction vmServiceConnectFunction = _waitAndConnect;

/// Restores [vmServiceConnectFunction] to its default value.
void restoreVmServiceConnectFunction() {
  vmServiceConnectFunction = _waitAndConnect;
}

/// Waits for a real Dart VM service to become available, then connects using
/// the [VMServiceClient].
///
/// Times out after 30 seconds.
625
Future<VMServiceClientConnection> _waitAndConnect(String url) async {
626
  final Stopwatch timer = new Stopwatch()..start();
627 628 629

  Future<VMServiceClientConnection> attemptConnection() async {
    Uri uri = Uri.parse(url);
630 631
    if (uri.scheme == 'http')
      uri = uri.replace(scheme: 'ws', path: '/ws');
632 633 634 635 636 637 638

    WebSocket ws1;
    WebSocket ws2;
    try {
      ws1 = await WebSocket.connect(uri.toString());
      ws2 = await WebSocket.connect(uri.toString());
      return new VMServiceClientConnection(
639 640
        new VMServiceClient(new IOWebSocketChannel(ws1).cast()),
        new rpc.Peer(new IOWebSocketChannel(ws2).cast())..listen()
641 642
      );
    } catch(e) {
643 644
      await ws1?.close();
      await ws2?.close();
645

646
      if (timer.elapsed < _kLongTimeout * 2) {
647
        _log.info('Waiting for application to start');
648
        await new Future<Null>.delayed(_kPauseBetweenReconnectAttempts);
649 650 651 652 653 654
        return attemptConnection();
      } else {
        _log.critical(
          'Application has not started in 30 seconds. '
          'Giving up.'
        );
655
        rethrow;
656 657
      }
    }
yjbanov's avatar
yjbanov committed
658
  }
659

yjbanov's avatar
yjbanov committed
660 661
  return attemptConnection();
}
662 663 664 665 666 667 668 669

/// Provides convenient accessors to frequently used finders.
class CommonFinders {
  const CommonFinders._();

  /// Finds [Text] widgets containing string equal to [text].
  SerializableFinder text(String text) => new ByText(text);

670
  /// Finds widgets by [key]. Only [String] and [int] values can be used.
671 672 673 674
  SerializableFinder byValueKey(dynamic key) => new ByValueKey(key);

  /// Finds widgets with a tooltip with the given [message].
  SerializableFinder byTooltip(String message) => new ByTooltipMessage(message);
675 676 677

  /// Finds widgets whose class name matches the given string.
  SerializableFinder byType(String type) => new ByType(type);
678
}