test_driver.dart 30.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// @dart = 2.8

7 8
import 'dart:async';
import 'dart:convert';
9
import 'dart:io' as io; // flutter_ignore: dart_io_import
10 11

import 'package:file/file.dart';
12
import 'package:flutter_tools/src/base/common.dart';
13 14
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
15
import 'package:flutter_tools/src/base/utils.dart';
16
import 'package:meta/meta.dart';
17
import 'package:process/process.dart';
18 19
import 'package:vm_service/vm_service.dart';
import 'package:vm_service/vm_service_io.dart';
20 21

import '../src/common.dart';
22
import 'test_utils.dart';
23

24 25 26 27 28 29 30 31 32 33
// Set this to true for debugging to get verbose logs written to stdout.
// The logs include the following:
//   <=stdout= data that the flutter tool running in --verbose mode wrote to stdout.
//   <=stderr= data that the flutter tool running in --verbose mode wrote to stderr.
//   =stdin=> data that the test sent to the flutter tool over stdin.
//   =vm=> data that was sent over the VM service channel to the app running on the test device.
//   <=vm= data that was sent from the app on the test device over the VM service channel.
//   Messages regarding what the test is doing.
// If this is false, then only critical errors and logs when things appear to be
// taking a long time are printed to the console.
34
const bool _printDebugOutputToStdOut = false;
35 36 37 38

final DateTime startTime = DateTime.now();

const Duration defaultTimeout = Duration(seconds: 5);
39 40
const Duration appStartTimeout = Duration(seconds: 120);
const Duration quitTimeout = Duration(seconds: 10);
41

42
abstract class FlutterTestDriver {
43 44 45 46
  FlutterTestDriver(
    this._projectFolder, {
    String logPrefix,
  }) : _logPrefix = logPrefix != null ? '$logPrefix: ' : '';
47

48
  final Directory _projectFolder;
49
  final String _logPrefix;
50 51
  Process _process;
  int _processPid;
52 53 54 55
  final StreamController<String> _stdout = StreamController<String>.broadcast();
  final StreamController<String> _stderr = StreamController<String>.broadcast();
  final StreamController<String> _allMessages = StreamController<String>.broadcast();
  final StringBuffer _errorBuffer = StringBuffer();
56
  String _lastResponse;
57
  Uri _vmServiceWsUri;
58
  int _attachPort;
59
  bool _hasExited = false;
60

61
  VmService _vmService;
62
  String get lastErrorInfo => _errorBuffer.toString();
63
  Stream<String> get stdout => _stdout.stream;
64
  int get vmServicePort => _vmServiceWsUri.port;
65
  bool get hasExited => _hasExited;
66
  Uri get vmServiceWsUri => _vmServiceWsUri;
67

68 69 70
  String lastTime = '';
  void _debugPrint(String message, { String topic = '' }) {
    const int maxLength = 2500;
71
    final String truncatedMessage = message.length > maxLength ? '${message.substring(0, maxLength)}...' : message;
72 73 74
    final String line = '${topic.padRight(10)} $truncatedMessage';
    _allMessages.add(line);
    final int timeInSeconds = DateTime.now().difference(startTime).inSeconds;
75
    String time = '${timeInSeconds.toString().padLeft(5)}s ';
76 77 78 79
    if (time == lastTime) {
      time = ' ' * time.length;
    } else {
      lastTime = time;
80
    }
81
    if (_printDebugOutputToStdOut) {
82
      print('$time$_logPrefix$line');
83
    }
84
  }
85

86
  Future<void> _setupProcess(
87
    List<String> arguments, {
88
    String script,
89
    bool withDebugger = false,
90
    bool singleWidgetReloads = false,
91
  }) async {
92
    final String flutterBin = fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter');
93
    if (withDebugger) {
94
      arguments.add('--start-paused');
95 96
    }
    if (_printDebugOutputToStdOut) {
97
      arguments.add('--verbose');
98
    }
99
    if (script != null) {
100
      arguments.add(script);
101
    }
102
    _debugPrint('Spawning flutter $arguments in ${_projectFolder.path}');
103

104 105
    const ProcessManager _processManager = LocalProcessManager();
    _process = await _processManager.start(
106 107 108 109
      <String>[flutterBin]
        .followedBy(arguments)
        .toList(),
      workingDirectory: _projectFolder.path,
110
      // The web environment variable has the same effect as `flutter config --enable-web`.
111 112 113 114 115 116
      environment: <String, String>{
        'FLUTTER_TEST': 'true',
        'FLUTTER_WEB': 'true',
        if (singleWidgetReloads)
          'FLUTTER_SINGLE_WIDGET_RELOAD': 'true',
      },
117
    );
118

119 120
    // This class doesn't use the result of the future. It's made available
    // via a getter for external uses.
121
    unawaited(_process.exitCode.then((int code) {
122 123
      _debugPrint('Process exited ($code)');
      _hasExited = true;
124
    }));
125 126
    transformToLines(_process.stdout).listen(_stdout.add);
    transformToLines(_process.stderr).listen(_stderr.add);
127 128 129 130 131

    // Capture stderr to a buffer so we can show it all if any requests fail.
    _stderr.stream.listen(_errorBuffer.writeln);

    // This is just debug printing to aid running/debugging tests locally.
132 133
    _stdout.stream.listen((String message) => _debugPrint(message, topic: '<=stdout='));
    _stderr.stream.listen((String message) => _debugPrint(message, topic: '<=stderr='));
134 135
  }

136 137
  Future<void> get done => _process.exitCode;

138
  Future<void> connectToVmService({ bool pauseOnExceptions = false }) async {
139 140 141
    _vmService = await vmServiceConnectUri('$_vmServiceWsUri');
    _vmService.onSend.listen((String s) => _debugPrint(s, topic: '=vm=>'));
    _vmService.onReceive.listen((String s) => _debugPrint(s, topic: '<=vm='));
142 143

    final Completer<void> isolateStarted = Completer<void>();
144
    _vmService.onIsolateEvent.listen((Event event) {
145 146 147
      if (event.kind == EventKind.kIsolateStart) {
        isolateStarted.complete();
      } else if (event.kind == EventKind.kIsolateExit && event.isolate.id == _flutterIsolateId) {
148 149 150
        // Hot restarts cause all the isolates to exit, so we need to refresh
        // our idea of what the Flutter isolate ID is.
        _flutterIsolateId = null;
151
      }
152 153 154 155 156 157 158
    });

    await Future.wait(<Future<Success>>[
      _vmService.streamListen('Isolate'),
      _vmService.streamListen('Debug'),
    ]);

159 160 161 162
    if ((await _vmService.getVM()).isolates.isEmpty) {
      await isolateStarted.future;
    }

163 164 165 166 167 168 169
    await waitForPause();
    if (pauseOnExceptions) {
      await _vmService.setExceptionPauseMode(
        await _getFlutterIsolateId(),
        ExceptionPauseMode.kUnhandled,
      );
    }
170 171
  }

172 173 174 175
  Future<Response> callServiceExtension(
    String extension, {
    Map<String, dynamic> args = const <String, dynamic>{},
  }) async {
176 177 178
    final int port = _vmServiceWsUri != null ? vmServicePort : _attachPort;
    final VmService vmService = await vmServiceConnectUri('ws://localhost:$port/ws');
    final Isolate isolate = await waitForExtension(vmService, extension);
179
    return vmService.callServiceExtension(
180 181 182 183 184 185
      extension,
      isolateId: isolate.id,
      args: args,
    );
  }

186 187
  Future<int> quit() => _killGracefully();

188
  Future<int> _killGracefully() async {
189
    if (_processPid == null) {
190
      return -1;
191
    }
192 193 194 195
    // If we try to kill the process while it's paused, we'll end up terminating
    // it forcefully and it won't terminate child processes, so we need to ensure
    // it's running before terminating.
    await resume().timeout(defaultTimeout)
196 197 198 199
        .catchError((Object e) {
          _debugPrint('Ignoring failure to resume during shutdown');
          return null;
        });
200

201
    _debugPrint('Sending SIGTERM to $_processPid..');
202
    io.Process.killPid(_processPid, io.ProcessSignal.sigterm);
203
    return _process.exitCode.timeout(quitTimeout, onTimeout: _killForcefully);
204 205 206
  }

  Future<int> _killForcefully() {
207
    _debugPrint('Sending SIGKILL to $_processPid..');
208
    ProcessSignal.sigkill.send(_processPid);
209
    return _process.exitCode;
210 211
  }

212 213
  String _flutterIsolateId;
  Future<String> _getFlutterIsolateId() async {
214 215
    // Currently these tests only have a single isolate. If this
    // ceases to be the case, this code will need changing.
216 217
    if (_flutterIsolateId == null) {
      final VM vm = await _vmService.getVM();
218
      _flutterIsolateId = vm.isolates.single.id;
219 220 221 222
    }
    return _flutterIsolateId;
  }

223
  Future<Isolate> getFlutterIsolate() async {
224
    final Isolate isolate = await _vmService.getIsolate(await _getFlutterIsolateId());
225
    return isolate;
226 227
  }

228 229 230 231 232 233 234 235 236 237 238 239 240 241
  /// Add a breakpoint and wait for it to trip the program execution.
  ///
  /// Only call this when you are absolutely sure that the program under test
  /// will hit the breakpoint _in the future_.
  ///
  /// In particular, do not call this if the program is currently racing to pass
  /// the line of code you are breaking on. Pretend that calling this will take
  /// an hour before setting the breakpoint. Would the code still eventually hit
  /// the breakpoint and stop?
  Future<void> breakAt(Uri uri, int line) async {
    await addBreakpoint(uri, line);
    await waitForPause();
  }

242
  Future<void> addBreakpoint(Uri uri, int line) async {
243
    _debugPrint('Sending breakpoint for: $uri:$line');
244
    await _vmService.addBreakpointWithScriptUri(
245 246 247 248
      await _getFlutterIsolateId(),
      uri.toString(),
      line,
    );
249 250
  }

251 252
  // This method isn't racy. If the isolate is already paused,
  // it will immediately return.
253
  Future<Isolate> waitForPause() async {
254 255 256 257 258 259 260 261 262 263 264 265
    return _timeoutWithMessages<Isolate>(
      () async {
        final String flutterIsolate = await _getFlutterIsolateId();
        final Completer<Event> pauseEvent = Completer<Event>();

        // Start listening for pause events.
        final StreamSubscription<Event> pauseSubscription = _vmService.onDebugEvent
          .where((Event event) {
            return event.isolate.id == flutterIsolate
                && event.kind.startsWith('Pause');
          })
          .listen((Event event) {
266
            if (!pauseEvent.isCompleted) {
267
              pauseEvent.complete(event);
268
            }
269 270 271 272 273
          });

        // But also check if the isolate was already paused (only after we've set
        // up the subscription) to avoid races. If it was paused, we don't need to wait
        // for the event.
274
        final Isolate isolate = await _vmService.getIsolate(flutterIsolate);
275 276 277 278 279 280
        if (isolate.pauseEvent.kind.startsWith('Pause')) {
          _debugPrint('Isolate was already paused (${isolate.pauseEvent.kind}).');
        } else {
          _debugPrint('Isolate is not already paused, waiting for event to arrive...');
          await pauseEvent.future;
        }
281

282 283
        // Cancel the subscription on either of the above.
        await pauseSubscription.cancel();
284

285
        return getFlutterIsolate();
286 287 288
      },
      task: 'Waiting for isolate to pause',
    );
289
  }
290

291 292
  Future<Isolate> resume({ bool waitForNextPause = false }) => _resume(null, waitForNextPause);
  Future<Isolate> stepOver({ bool waitForNextPause = true }) => _resume(StepOption.kOver, waitForNextPause);
293
  Future<Isolate> stepOverAsync({ bool waitForNextPause = true }) => _resume(StepOption.kOverAsyncSuspension, waitForNextPause);
294 295
  Future<Isolate> stepInto({ bool waitForNextPause = true }) => _resume(StepOption.kInto, waitForNextPause);
  Future<Isolate> stepOut({ bool waitForNextPause = true }) => _resume(StepOption.kOut, waitForNextPause);
296

297
  Future<bool> isAtAsyncSuspension() async {
298
    final Isolate isolate = await getFlutterIsolate();
299 300 301
    return isolate.pauseEvent.atAsyncSuspension == true;
  }

302
  Future<Isolate> stepOverOrOverAsyncSuspension({ bool waitForNextPause = true }) async {
303
    if (await isAtAsyncSuspension()) {
304
      return stepOverAsync(waitForNextPause: waitForNextPause);
305
    }
306
    return stepOver(waitForNextPause: waitForNextPause);
307
  }
308

309 310 311 312 313 314 315
  Future<Isolate> _resume(String step, bool waitForNextPause) async {
    assert(waitForNextPause != null);
    await _timeoutWithMessages<dynamic>(
      () async => _vmService.resume(await _getFlutterIsolateId(), step: step),
      task: 'Resuming isolate (step=$step)',
    );
    return waitForNextPause ? waitForPause() : null;
316 317
  }

318 319 320
  Future<ObjRef> evaluateInFrame(String expression) async {
    return _timeoutWithMessages<ObjRef>(
      () async => await _vmService.evaluateInFrame(await _getFlutterIsolateId(), 0, expression) as ObjRef,
321 322
      task: 'Evaluating expression ($expression)',
    );
323 324
  }

325 326
  Future<InstanceRef> evaluate(String targetId, String expression) async {
    return _timeoutWithMessages<InstanceRef>(
327
      () async => await _vmService.evaluate(await _getFlutterIsolateId(), targetId, expression) as InstanceRef,
328 329
      task: 'Evaluating expression ($expression for $targetId)',
    );
330 331 332 333
  }

  Future<Frame> getTopStackFrame() async {
    final String flutterIsolateId = await _getFlutterIsolateId();
334
    final Stack stack = await _vmService.getStack(flutterIsolateId);
335
    if (stack.frames.isEmpty) {
336
      throw Exception('Stack is empty');
337
    }
338 339 340
    return stack.frames.first;
  }

341 342 343
  Future<SourcePosition> getSourceLocation() async {
    final String flutterIsolateId = await _getFlutterIsolateId();
    final Frame frame = await getTopStackFrame();
344
    final Script script = await _vmService.getObject(flutterIsolateId, frame.location.script.id) as Script;
345 346 347 348
    return _lookupTokenPos(script.tokenPosTable, frame.location.tokenPos);
  }

  SourcePosition _lookupTokenPos(List<List<int>> table, int tokenPos) {
349
    for (final List<int> row in table) {
350 351 352 353 354 355 356 357 358 359 360
      final int lineNumber = row[0];
      int index = 1;

      for (index = 1; index < row.length - 1; index += 2) {
        if (row[index] == tokenPos) {
          return SourcePosition(lineNumber, row[index + 1]);
        }
      }
    }

    return null;
361 362
  }

363 364 365
  Future<Map<String, dynamic>> _waitFor({
    String event,
    int id,
366
    Duration timeout = defaultTimeout,
367 368
    bool ignoreAppStopEvent = false,
  }) async {
369 370 371 372
    assert(timeout != null);
    assert(event != null || id != null);
    assert(event == null || id == null);
    final String interestingOccurrence = event != null ? '$event event' : 'response to request $id';
373
    final Completer<Map<String, dynamic>> response = Completer<Map<String, dynamic>>();
374 375
    StreamSubscription<String> subscription;
    subscription = _stdout.stream.listen((String line) async {
376
      final Map<String, dynamic> json = parseFlutterResponse(line);
377
      _lastResponse = line;
378
      if (json == null) {
379
        return;
380
      }
381
      if ((event != null && json['event'] == event) ||
382
          (id != null && json['id'] == id)) {
383 384
        await subscription.cancel();
        _debugPrint('OK ($interestingOccurrence)');
385
        response.complete(json);
386
      } else if (!ignoreAppStopEvent && json['event'] == 'app.stop') {
387
        await subscription.cancel();
388
        final StringBuffer error = StringBuffer();
389
        error.write('Received app.stop event while waiting for $interestingOccurrence\n\n$_errorBuffer');
390 391 392 393 394 395 396 397
        if (json['params'] != null) {
          final Map<String, dynamic> params = json['params'] as Map<String, dynamic>;
          if (params['error'] != null) {
            error.write('${params['error']}\n\n');
          }
          if (json['params'] != null && params['trace'] != null) {
            error.write('${params['trace']}\n\n');
          }
398
        }
399
        response.completeError(Exception(error.toString()));
400 401
      }
    });
402

403 404 405 406 407
    return _timeoutWithMessages(
      () => response.future,
      timeout: timeout,
      task: 'Expecting $interestingOccurrence',
    ).whenComplete(subscription.cancel);
408 409
  }

410 411
  Future<T> _timeoutWithMessages<T>(
    Future<T> Function() callback, {
412 413 414 415 416 417 418 419
    @required String task,
    Duration timeout = defaultTimeout,
  }) {
    assert(task != null);
    assert(timeout != null);

    if (_printDebugOutputToStdOut) {
      _debugPrint('$task...');
420 421
      final Timer longWarning = Timer(timeout, () => _debugPrint('$task is taking longer than usual...'));
      return callback().whenComplete(longWarning.cancel);
422 423 424 425 426 427
    }

    // We're not showing all output to the screen, so let's capture the output
    // that we would have printed if we were, and output it if we take longer
    // than the timeout or if we get an error.
    final StringBuffer messages = StringBuffer('$task\n');
428
    final DateTime start = DateTime.now();
429 430
    bool timeoutExpired = false;
    void logMessage(String logLine) {
431
      final int ms = DateTime.now().difference(start).inMilliseconds;
432 433
      final String formattedLine = '[+ ${ms.toString().padLeft(5)}] $logLine';
      messages.writeln(formattedLine);
434
    }
435
    final StreamSubscription<String> subscription = _allMessages.stream.listen(logMessage);
436

437
    final Timer longWarning = Timer(timeout, () {
438
      _debugPrint(messages.toString());
439
      timeoutExpired = true;
440
      _debugPrint('$task is taking longer than usual...');
441
    });
442
    final Future<T> future = callback().whenComplete(longWarning.cancel);
443 444 445 446

    return future.catchError((dynamic error) {
      if (!timeoutExpired) {
        timeoutExpired = true;
447
        _debugPrint(messages.toString());
448 449 450
      }
      throw error;
    }).whenComplete(() => subscription.cancel());
451
  }
452 453 454
}

class FlutterRunTestDriver extends FlutterTestDriver {
455 456 457
  FlutterRunTestDriver(
    Directory projectFolder, {
    String logPrefix,
458
    this.spawnDdsInstance = true,
459
  }) : super(projectFolder, logPrefix: logPrefix);
460 461 462

  String _currentRunningAppId;

463
  Future<void> run({
464
    bool withDebugger = false,
465
    bool startPaused = false,
466
    bool pauseOnExceptions = false,
467
    bool chrome = false,
468
    bool expressionEvaluation = true,
469
    bool structuredErrors = false,
470
    bool singleWidgetReloads = false,
471
    String script,
472
    List<String> additionalCommandArgs,
473
  }) async {
474 475
    await _setupProcess(
      <String>[
476
        'run',
477 478
        if (!chrome)
          '--disable-service-auth-codes',
479
        '--machine',
480
        if (!spawnDdsInstance) '--no-dds',
481
        ...getLocalEngineArguments(),
482
        '-d',
483
        if (chrome)
484 485 486 487 488
          ...<String>[
            'chrome',
            '--web-run-headless',
            if (!expressionEvaluation) '--no-web-enable-expression-evaluation'
          ]
489 490
        else
          'flutter-tester',
491 492
        if (structuredErrors)
          '--dart-define=flutter.inspector.structuredErrors=true',
493
        ...?additionalCommandArgs,
494 495 496 497
      ],
      withDebugger: withDebugger,
      startPaused: startPaused,
      pauseOnExceptions: pauseOnExceptions,
498
      script: script,
499
      singleWidgetReloads: singleWidgetReloads,
500
    );
501 502 503 504 505
  }

  Future<void> attach(
    int port, {
    bool withDebugger = false,
506
    bool startPaused = false,
507
    bool pauseOnExceptions = false,
508
    bool singleWidgetReloads = false,
509
    List<String> additionalCommandArgs,
510
  }) async {
511
    _attachPort = port;
512 513
    await _setupProcess(
      <String>[
514
        'attach',
515
        ...getLocalEngineArguments(),
516
        '--machine',
517
        if (!spawnDdsInstance)
518
          '--no-dds',
519 520 521 522
        '-d',
        'flutter-tester',
        '--debug-port',
        '$port',
523
        ...?additionalCommandArgs,
524 525 526 527
      ],
      withDebugger: withDebugger,
      startPaused: startPaused,
      pauseOnExceptions: pauseOnExceptions,
528
      singleWidgetReloads: singleWidgetReloads,
529
      attachPort: port,
530
    );
531 532 533 534 535
  }

  @override
  Future<void> _setupProcess(
    List<String> args, {
536
    String script,
537
    bool withDebugger = false,
538
    bool startPaused = false,
539
    bool pauseOnExceptions = false,
540
    bool singleWidgetReloads = false,
541
    int attachPort,
542
  }) async {
543
    assert(!startPaused || withDebugger);
544 545
    await super._setupProcess(
      args,
546
      script: script,
547
      withDebugger: withDebugger,
548
      singleWidgetReloads: singleWidgetReloads,
549 550
    );

551 552 553 554 555 556 557 558
    final Completer<void> prematureExitGuard = Completer<void>();

    // If the process exits before all of the `await`s below are done, then it
    // exited prematurely. This causes the currently suspended `await` to
    // deadlock until the test times out. Instead, this causes the test to fail
    // fast.
    unawaited(_process.exitCode.then((_) {
      if (!prematureExitGuard.isCompleted) {
559
        prematureExitGuard.completeError(Exception('Process exited prematurely: ${args.join(' ')}: $_errorBuffer'));
560 561
      }
    }));
562

563 564 565 566 567 568
    unawaited(() async {
      try {
        // Stash the PID so that we can terminate the VM more reliably than using
        // _process.kill() (`flutter` is a shell script so _process itself is a
        // shell, not the flutter tool's Dart process).
        final Map<String, dynamic> connected = await _waitFor(event: 'daemon.connected');
569
        _processPid = (connected['params'] as Map<String, dynamic>)['pid'] as int;
570 571 572 573 574 575 576

        // Set this up now, but we don't wait it yet. We want to make sure we don't
        // miss it while waiting for debugPort below.
        final Future<Map<String, dynamic>> started = _waitFor(event: 'app.started', timeout: appStartTimeout);

        if (withDebugger) {
          final Map<String, dynamic> debugPort = await _waitFor(event: 'app.debugPort', timeout: appStartTimeout);
577
          final String wsUriString = (debugPort['params'] as Map<String, dynamic>)['wsUri'] as String;
578 579 580 581 582 583
          _vmServiceWsUri = Uri.parse(wsUriString);
          await connectToVmService(pauseOnExceptions: pauseOnExceptions);
          if (!startPaused) {
            await resume(waitForNextPause: false);
          }
        }
584

585 586 587 588 589 590 591
        // In order to call service extensions from test runners started with
        // attach, we need to store the port that the test runner was attached
        // to.
        if (_vmServiceWsUri == null && attachPort != null) {
          _attachPort = attachPort;
        }

592 593
        // Now await the started event; if it had already happened the future will
        // have already completed.
594
        _currentRunningAppId = ((await started)['params'] as Map<String, dynamic>)['appId'] as String;
595
        prematureExitGuard.complete();
596
      } on Exception catch (error, stackTrace) {
597
        prematureExitGuard.completeError(Exception(error.toString()), stackTrace);
598
      }
599
    }());
600

601
    return prematureExitGuard.future;
602 603
  }

604 605 606
  Future<void> hotRestart({ bool pause = false, bool debounce = false}) => _restart(fullRestart: true, pause: pause);
  Future<void> hotReload({ bool debounce = false, int debounceDurationOverrideMs }) =>
      _restart(fullRestart: false, debounce: debounce, debounceDurationOverrideMs: debounceDurationOverrideMs);
607

608 609 610 611 612 613 614 615 616 617
  Future<void> scheduleFrame() async {
    if (_currentRunningAppId == null) {
      throw Exception('App has not started yet');
    }
    await _sendRequest(
      'app.callServiceExtension',
      <String, dynamic>{'appId': _currentRunningAppId, 'methodName': 'ext.ui.window.scheduleFrame'},
    );
  }

618
  Future<void> _restart({ bool fullRestart = false, bool pause = false, bool debounce = false, int debounceDurationOverrideMs }) async {
619
    if (_currentRunningAppId == null) {
620
      throw Exception('App has not started yet');
621
    }
622

623
    _debugPrint('Performing ${ pause ? "paused " : "" }${ fullRestart ? "hot restart" : "hot reload" }...');
624
    final Map<String, dynamic> hotReloadResponse = await _sendRequest(
625
      'app.restart',
626
      <String, dynamic>{'appId': _currentRunningAppId, 'fullRestart': fullRestart, 'pause': pause, 'debounce': debounce, 'debounceDurationOverrideMs': debounceDurationOverrideMs},
627
    ) as Map<String, dynamic>;
628
    _debugPrint('${fullRestart ? "Hot restart" : "Hot reload"} complete.');
629

630
    if (hotReloadResponse == null || hotReloadResponse['code'] != 0) {
631
      _throwErrorResponse('Hot ${fullRestart ? 'restart' : 'reload'} request failed');
632
    }
633 634 635
  }

  Future<int> detach() async {
636 637 638
    if (_process == null) {
      return 0;
    }
639
    if (_vmService != null) {
640
      _debugPrint('Closing VM service...');
641
      await _vmService.dispose();
642 643
    }
    if (_currentRunningAppId != null) {
644
      _debugPrint('Detaching from app...');
645
      await Future.any<void>(<Future<void>>[
646
        _process.exitCode,
647 648 649 650 651 652 653 654 655 656
        _sendRequest(
          'app.detach',
          <String, dynamic>{'appId': _currentRunningAppId},
        ),
      ]).timeout(
        quitTimeout,
        onTimeout: () { _debugPrint('app.detach did not return within $quitTimeout'); },
      );
      _currentRunningAppId = null;
    }
657 658
    _debugPrint('Waiting for process to end...');
    return _process.exitCode.timeout(quitTimeout, onTimeout: _killGracefully);
659 660 661 662
  }

  Future<int> stop() async {
    if (_vmService != null) {
663
      _debugPrint('Closing VM service...');
664
      await _vmService.dispose();
665 666
    }
    if (_currentRunningAppId != null) {
667
      _debugPrint('Stopping application...');
668
      await Future.any<void>(<Future<void>>[
669
        _process.exitCode,
670 671 672 673 674 675 676 677 678 679
        _sendRequest(
          'app.stop',
          <String, dynamic>{'appId': _currentRunningAppId},
        ),
      ]).timeout(
        quitTimeout,
        onTimeout: () { _debugPrint('app.stop did not return within $quitTimeout'); },
      );
      _currentRunningAppId = null;
    }
680 681 682
    if (_process != null) {
      _debugPrint('Waiting for process to end...');
      return _process.exitCode.timeout(quitTimeout, onTimeout: _killGracefully);
683 684 685 686
    }
    return 0;
  }

687 688 689
  int id = 1;
  Future<dynamic> _sendRequest(String method, dynamic params) async {
    final int requestId = id++;
690
    final Map<String, dynamic> request = <String, dynamic>{
691 692
      'id': requestId,
      'method': method,
693
      'params': params,
694
    };
695
    final String jsonEncoded = json.encode(<Map<String, dynamic>>[request]);
696
    _debugPrint(jsonEncoded, topic: '=stdin=>');
697

698
    // Set up the response future before we send the request to avoid any
699
    // races. If the method we're calling is app.stop then we tell _waitFor not
700 701 702 703 704
    // to throw if it sees an app.stop event before the response to this request.
    final Future<Map<String, dynamic>> responseFuture = _waitFor(
      id: requestId,
      ignoreAppStopEvent: method == 'app.stop',
    );
705
    _process.stdin.writeln(jsonEncoded);
706
    final Map<String, dynamic> response = await responseFuture;
707

708
    if (response['error'] != null || response['result'] == null) {
709
      _throwErrorResponse('Unexpected error response');
710
    }
711

712
    return response['result'];
713 714
  }

715 716
  void _throwErrorResponse(String message) {
    throw '$message\n\n$_lastResponse\n\n${_errorBuffer.toString()}'.trim();
717
  }
718 719

  final bool spawnDdsInstance;
720 721
}

722
class FlutterTestTestDriver extends FlutterTestDriver {
723 724
  FlutterTestTestDriver(Directory _projectFolder, {String logPrefix})
    : super(_projectFolder, logPrefix: logPrefix);
725 726 727 728 729

  Future<void> test({
    String testFile = 'test/test.dart',
    bool withDebugger = false,
    bool pauseOnExceptions = false,
730
    bool coverage = false,
731 732 733
    Future<void> Function() beforeStart,
  }) async {
    await _setupProcess(<String>[
734
      'test',
735
      ...getLocalEngineArguments(),
736 737
      '--disable-service-auth-codes',
      '--machine',
738 739
      if (coverage)
        '--coverage',
740
    ], script: testFile, withDebugger: withDebugger, pauseOnExceptions: pauseOnExceptions, beforeStart: beforeStart);
741 742 743 744 745 746 747 748 749
  }

  @override
  Future<void> _setupProcess(
    List<String> args, {
    String script,
    bool withDebugger = false,
    bool pauseOnExceptions = false,
    Future<void> Function() beforeStart,
750
    bool singleWidgetReloads = false,
751 752 753 754 755
  }) async {
    await super._setupProcess(
      args,
      script: script,
      withDebugger: withDebugger,
756
      singleWidgetReloads: singleWidgetReloads,
757 758 759 760 761 762
    );

    // Stash the PID so that we can terminate the VM more reliably than using
    // _proc.kill() (because _proc is a shell, because `flutter` is a shell
    // script).
    final Map<String, dynamic> version = await _waitForJson();
763
    _processPid = version['pid'] as int;
764 765

    if (withDebugger) {
766 767 768
      final Map<String, dynamic> startedProcessParams =
          (await _waitFor(event: 'test.startedProcess', timeout: appStartTimeout))['params'] as Map<String, dynamic>;
      final String vmServiceHttpString = startedProcessParams['observatoryUri'] as String;
769 770 771 772 773 774
      _vmServiceWsUri = Uri.parse(vmServiceHttpString).replace(scheme: 'ws', path: '/ws');
      await connectToVmService(pauseOnExceptions: pauseOnExceptions);
      // Allow us to run code before we start, eg. to set up breakpoints.
      if (beforeStart != null) {
        await beforeStart();
      }
775
      await resume(waitForNextPause: false);
776 777 778 779
    }
  }

  Future<Map<String, dynamic>> _waitForJson({
780
    Duration timeout = defaultTimeout,
781
  }) async {
782
    assert(timeout != null);
783
    return _timeoutWithMessages<Map<String, dynamic>>(
784 785
      () => _stdout.stream.map<Map<String, dynamic>>(_parseJsonResponse)
          .firstWhere((Map<String, dynamic> output) => output != null),
786
      timeout: timeout,
787
      task: 'Waiting for JSON',
788 789 790 791 792
    );
  }

  Map<String, dynamic> _parseJsonResponse(String line) {
    try {
793
      return castStringKeyedMap(json.decode(line));
794
    } on Exception {
795 796 797 798
      // Not valid JSON, so likely some other output.
      return null;
    }
  }
799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821

  Future<void> waitForCompletion() async {
    final Completer<bool> done = Completer<bool>();
    // Waiting for `{"success":true,"type":"done",...}` line indicating
    // end of test run.
    final StreamSubscription<String> subscription = _stdout.stream.listen(
        (String line) async {
          final Map<String, dynamic> json = _parseJsonResponse(line);
          if (json != null && json['type'] != null && json['success'] != null) {
            done.complete(json['type'] == 'done' && json['success'] == true);
          }
        });

    await resume();

    final Future<dynamic> timeoutFuture =
        Future<dynamic>.delayed(defaultTimeout);
    await Future.any<dynamic>(<Future<dynamic>>[done.future, timeoutFuture]);
    await subscription.cancel();
    if (!done.isCompleted) {
      await quit();
    }
  }
822 823
}

824
Stream<String> transformToLines(Stream<List<int>> byteStream) {
825
  return byteStream.transform<String>(utf8.decoder).transform<String>(const LineSplitter());
826
}
827

828
Map<String, dynamic> parseFlutterResponse(String line) {
829
  if (line.startsWith('[') && line.endsWith(']') && line.length > 2) {
830
    try {
831
      final Map<String, dynamic> response = castStringKeyedMap((json.decode(line) as List<dynamic>)[0]);
832
      return response;
833
    } on FormatException {
834 835 836 837 838 839 840
      // Not valid JSON, so likely some other output that was surrounded by [brackets]
      return null;
    }
  }
  return null;
}

841 842 843 844 845 846
class SourcePosition {
  SourcePosition(this.line, this.column);

  final int line;
  final int column;
}
847

848
Future<Isolate> waitForExtension(VmService vmService, String extension) async {
849
  final Completer<void> completer = Completer<void>();
850 851 852 853 854
  try {
    await vmService.streamListen(EventStreams.kExtension);
  } on RPCError {
    // Do nothing, already subscribed.
  }
855 856 857 858 859 860 861
  vmService.onExtensionEvent.listen((Event event) {
    if (event.json['extensionKind'] == 'Flutter.FrameworkInitialization') {
      completer.complete();
    }
  });
  final IsolateRef isolateRef = (await vmService.getVM()).isolates.first;
  final Isolate isolate = await vmService.getIsolate(isolateRef.id);
862
  if (isolate.extensionRPCs.contains(extension)) {
863 864 865 866 867
    return isolate;
  }
  await completer.future;
  return isolate;
}