test_driver.dart 24 KB
Newer Older
1 2 3 4 5 6 7 8
// Copyright 2018 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';
import 'dart:convert';

import 'package:file/file.dart';
9
import 'package:flutter_tools/src/base/common.dart';
10 11
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
12
import 'package:meta/meta.dart';
13
import 'package:process/process.dart';
14 15
import 'package:vm_service/vm_service.dart';
import 'package:vm_service/vm_service_io.dart';
16 17 18

import '../src/common.dart';

19 20 21 22 23 24 25 26 27 28
// 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.
29
const bool _printDebugOutputToStdOut = false;
30 31 32 33

final DateTime startTime = DateTime.now();

const Duration defaultTimeout = Duration(seconds: 5);
34 35
const Duration appStartTimeout = Duration(seconds: 120);
const Duration quitTimeout = Duration(seconds: 10);
36

37
abstract class FlutterTestDriver {
38 39 40 41
  FlutterTestDriver(
    this._projectFolder, {
    String logPrefix,
  }) : _logPrefix = logPrefix != null ? '$logPrefix: ' : '';
42

43
  final Directory _projectFolder;
44
  final String _logPrefix;
45 46
  Process _process;
  int _processPid;
47 48 49 50
  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();
51
  String _lastResponse;
52
  Uri _vmServiceWsUri;
53
  bool _hasExited = false;
54

55
  VmService _vmService;
56
  String get lastErrorInfo => _errorBuffer.toString();
57
  Stream<String> get stdout => _stdout.stream;
58
  int get vmServicePort => _vmServiceWsUri.port;
59
  bool get hasExited => _hasExited;
60

61 62 63 64 65 66 67 68 69 70 71 72
  String lastTime = '';
  void _debugPrint(String message, { String topic = '' }) {
    const int maxLength = 2500;
    final String truncatedMessage = message.length > maxLength ? message.substring(0, maxLength) + '...' : message;
    final String line = '${topic.padRight(10)} $truncatedMessage';
    _allMessages.add(line);
    final int timeInSeconds = DateTime.now().difference(startTime).inSeconds;
    String time = timeInSeconds.toString().padLeft(5) + 's ';
    if (time == lastTime) {
      time = ' ' * time.length;
    } else {
      lastTime = time;
73
    }
74 75
    if (_printDebugOutputToStdOut)
      print('$time$_logPrefix$line');
76
  }
77

78
  Future<void> _setupProcess(
79
    List<String> arguments, {
80
    String script,
81 82 83
    bool withDebugger = false,
    File pidFile,
  }) async {
84
    final String flutterBin = fs.path.join(getFlutterRoot(), 'bin', 'flutter');
85
    if (withDebugger)
86
      arguments.add('--start-paused');
87
    if (_printDebugOutputToStdOut)
88
      arguments.add('--verbose');
89
    if (pidFile != null) {
90
      arguments.addAll(<String>['--pid-file', pidFile.path]);
91
    }
92
    if (script != null) {
93
      arguments.add(script);
94
    }
95
    _debugPrint('Spawning flutter $arguments in ${_projectFolder.path}');
96

97
    const ProcessManager _processManager = LocalProcessManager();
98 99 100 101 102 103 104
    _process = await _processManager.start(
      <String>[flutterBin]
        .followedBy(arguments)
        .toList(),
      workingDirectory: _projectFolder.path,
      environment: <String, String>{'FLUTTER_TEST': 'true'},
    );
105

106 107
    // This class doesn't use the result of the future. It's made available
    // via a getter for external uses.
108
    unawaited(_process.exitCode.then((int code) {
109 110
      _debugPrint('Process exited ($code)');
      _hasExited = true;
111
    }));
112 113
    transformToLines(_process.stdout).listen((String line) => _stdout.add(line));
    transformToLines(_process.stderr).listen((String line) => _stderr.add(line));
114 115 116 117 118

    // 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.
119 120
    _stdout.stream.listen((String message) => _debugPrint(message, topic: '<=stdout='));
    _stderr.stream.listen((String message) => _debugPrint(message, topic: '<=stderr='));
121 122
  }

123
  Future<void> connectToVmService({ bool pauseOnExceptions = false }) async {
124 125 126 127 128 129 130 131
    _vmService = await vmServiceConnectUri('$_vmServiceWsUri');
    _vmService.onSend.listen((String s) => _debugPrint(s, topic: '=vm=>'));
    _vmService.onReceive.listen((String s) => _debugPrint(s, topic: '<=vm='));
    _vmService.onIsolateEvent.listen((Event event) {
      if (event.kind == EventKind.kIsolateExit && event.isolate.id == _flutterIsolateId) {
        // Hot restarts cause all the isolates to exit, so we need to refresh
        // our idea of what the Flutter isolate ID is.
        _flutterIsolateId = null;
132
      }
133 134 135 136 137 138 139 140 141 142 143 144 145 146
    });

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

    await waitForPause();
    if (pauseOnExceptions) {
      await _vmService.setExceptionPauseMode(
        await _getFlutterIsolateId(),
        ExceptionPauseMode.kUnhandled,
      );
    }
147 148
  }

149 150
  Future<int> quit() => _killGracefully();

151
  Future<int> _killGracefully() async {
152
    if (_processPid == null)
153
      return -1;
154
    _debugPrint('Sending SIGTERM to $_processPid..');
155
    ProcessSignal.SIGTERM.send(_processPid);
156
    return _process.exitCode.timeout(quitTimeout, onTimeout: _killForcefully);
157 158 159
  }

  Future<int> _killForcefully() {
160
    _debugPrint('Sending SIGKILL to $_processPid..');
161
    ProcessSignal.SIGKILL.send(_processPid);
162
    return _process.exitCode;
163 164
  }

165 166
  String _flutterIsolateId;
  Future<String> _getFlutterIsolateId() async {
167 168
    // Currently these tests only have a single isolate. If this
    // ceases to be the case, this code will need changing.
169 170 171 172 173 174 175 176 177 178
    if (_flutterIsolateId == null) {
      final VM vm = await _vmService.getVM();
      _flutterIsolateId = vm.isolates.first.id;
    }
    return _flutterIsolateId;
  }

  Future<Isolate> _getFlutterIsolate() async {
    final Isolate isolate = await _vmService.getIsolate(await _getFlutterIsolateId());
    return isolate;
179 180
  }

181 182 183 184 185 186 187 188 189 190 191 192 193 194
  /// 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();
  }

195
  Future<void> addBreakpoint(Uri uri, int line) async {
196
    _debugPrint('Sending breakpoint for: $uri:$line');
197
    await _vmService.addBreakpointWithScriptUri(
198 199 200 201
      await _getFlutterIsolateId(),
      uri.toString(),
      line,
    );
202 203
  }

204 205
  // This method isn't racy. If the isolate is already paused,
  // it will immediately return.
206
  Future<Isolate> waitForPause() async {
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
    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) {
            if (!pauseEvent.isCompleted)
              pauseEvent.complete(event);
          });

        // 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.
        final Isolate isolate = await _vmService.getIsolate(flutterIsolate);
        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;
        }
233

234 235
        // Cancel the subscription on either of the above.
        await pauseSubscription.cancel();
236

237 238 239 240
        return _getFlutterIsolate();
      },
      task: 'Waiting for isolate to pause',
    );
241
  }
242

243 244
  Future<Isolate> resume({ bool waitForNextPause = false }) => _resume(null, waitForNextPause);
  Future<Isolate> stepOver({ bool waitForNextPause = true }) => _resume(StepOption.kOver, waitForNextPause);
245
  Future<Isolate> stepOverAsync({ bool waitForNextPause = true }) => _resume(StepOption.kOverAsyncSuspension, waitForNextPause);
246 247
  Future<Isolate> stepInto({ bool waitForNextPause = true }) => _resume(StepOption.kInto, waitForNextPause);
  Future<Isolate> stepOut({ bool waitForNextPause = true }) => _resume(StepOption.kOut, waitForNextPause);
248

249 250 251 252 253
  Future<bool> isAtAsyncSuspension() async {
    final Isolate isolate = await _getFlutterIsolate();
    return isolate.pauseEvent.atAsyncSuspension == true;
  }

254 255 256 257
  Future<Isolate> stepOverOrOverAsyncSuspension({ bool waitForNextPause = true }) async {
    if (await isAtAsyncSuspension())
      return await stepOverAsync(waitForNextPause: waitForNextPause);
    return await stepOver(waitForNextPause: waitForNextPause);
258
  }
259

260 261 262 263 264 265 266
  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;
267 268
  }

269 270
  Future<InstanceRef> evaluateInFrame(String expression) async {
    return _timeoutWithMessages<InstanceRef>(
271 272 273
      () async => await _vmService.evaluateInFrame(await _getFlutterIsolateId(), 0, expression),
      task: 'Evaluating expression ($expression)',
    );
274 275
  }

276 277
  Future<InstanceRef> evaluate(String targetId, String expression) async {
    return _timeoutWithMessages<InstanceRef>(
278 279 280
      () async => await _vmService.evaluate(await _getFlutterIsolateId(), targetId, expression),
      task: 'Evaluating expression ($expression for $targetId)',
    );
281 282 283 284 285
  }

  Future<Frame> getTopStackFrame() async {
    final String flutterIsolateId = await _getFlutterIsolateId();
    final Stack stack = await _vmService.getStack(flutterIsolateId);
286
    if (stack.frames.isEmpty) {
287
      throw Exception('Stack is empty');
288
    }
289 290 291
    return stack.frames.first;
  }

292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
  Future<SourcePosition> getSourceLocation() async {
    final String flutterIsolateId = await _getFlutterIsolateId();
    final Frame frame = await getTopStackFrame();
    final Script script = await _vmService.getObject(flutterIsolateId, frame.location.script.id);
    return _lookupTokenPos(script.tokenPosTable, frame.location.tokenPos);
  }

  SourcePosition _lookupTokenPos(List<List<int>> table, int tokenPos) {
    for (List<int> row in table) {
      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;
312 313
  }

314 315 316
  Future<Map<String, dynamic>> _waitFor({
    String event,
    int id,
317
    Duration timeout = defaultTimeout,
318 319
    bool ignoreAppStopEvent = false,
  }) async {
320 321 322 323
    assert(timeout != null);
    assert(event != null || id != null);
    assert(event == null || id == null);
    final String interestingOccurrence = event != null ? '$event event' : 'response to request $id';
324
    final Completer<Map<String, dynamic>> response = Completer<Map<String, dynamic>>();
325 326
    StreamSubscription<String> subscription;
    subscription = _stdout.stream.listen((String line) async {
327 328
      final dynamic json = parseFlutterResponse(line);
      _lastResponse = line;
329
      if (json == null)
330
        return;
331 332 333 334
      if ((event != null && json['event'] == event) ||
          (id    != null && json['id']    == id)) {
        await subscription.cancel();
        _debugPrint('OK ($interestingOccurrence)');
335
        response.complete(json);
336
      } else if (!ignoreAppStopEvent && json['event'] == 'app.stop') {
337
        await subscription.cancel();
338
        final StringBuffer error = StringBuffer();
339
        error.write('Received app.stop event while waiting for $interestingOccurrence\n\n');
340 341 342 343 344 345 346
        if (json['params'] != null && json['params']['error'] != null) {
          error.write('${json['params']['error']}\n\n');
        }
        if (json['params'] != null && json['params']['trace'] != null) {
          error.write('${json['params']['trace']}\n\n');
        }
        response.completeError(error.toString());
347 348
      }
    });
349

350 351 352 353 354
    return _timeoutWithMessages(
      () => response.future,
      timeout: timeout,
      task: 'Expecting $interestingOccurrence',
    ).whenComplete(subscription.cancel);
355 356
  }

357 358
  Future<T> _timeoutWithMessages<T>(
    Future<T> Function() callback, {
359 360 361 362 363 364 365 366 367 368
    @required String task,
    Duration timeout = defaultTimeout,
  }) {
    assert(task != null);
    assert(timeout != null);

    if (_printDebugOutputToStdOut) {
      _debugPrint('$task...');
      return callback()..timeout(timeout, onTimeout: () {
        _debugPrint('$task is taking longer than usual...');
369
        return null;
370 371 372 373 374 375 376
      });
    }

    // 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');
377
    final DateTime start = DateTime.now();
378 379
    bool timeoutExpired = false;
    void logMessage(String logLine) {
380
      final int ms = DateTime.now().difference(start).inMilliseconds;
381 382
      final String formattedLine = '[+ ${ms.toString().padLeft(5)}] $logLine';
      messages.writeln(formattedLine);
383
    }
384
    final StreamSubscription<String> subscription = _allMessages.stream.listen(logMessage);
385

386 387 388
    final Future<T> future = callback();

    future.timeout(timeout ?? defaultTimeout, onTimeout: () {
389
      _debugPrint(messages.toString());
390
      timeoutExpired = true;
391
      _debugPrint('$task is taking longer than usual...');
392
      return null;
393 394 395 396 397
    });

    return future.catchError((dynamic error) {
      if (!timeoutExpired) {
        timeoutExpired = true;
398
        _debugPrint(messages.toString());
399 400 401
      }
      throw error;
    }).whenComplete(() => subscription.cancel());
402
  }
403 404 405
}

class FlutterRunTestDriver extends FlutterTestDriver {
406 407 408 409
  FlutterRunTestDriver(
    Directory projectFolder, {
    String logPrefix,
  }) : super(projectFolder, logPrefix: logPrefix);
410 411 412

  String _currentRunningAppId;

413
  Future<void> run({
414
    bool withDebugger = false,
415
    bool startPaused = false,
416 417 418
    bool pauseOnExceptions = false,
    File pidFile,
  }) async {
419 420
    await _setupProcess(
      <String>[
421
        'run',
422
        '--disable-service-auth-codes',
423 424 425
        '--machine',
        '-d',
        'flutter-tester',
426 427 428 429 430 431
      ],
      withDebugger: withDebugger,
      startPaused: startPaused,
      pauseOnExceptions: pauseOnExceptions,
      pidFile: pidFile,
    );
432 433 434 435 436
  }

  Future<void> attach(
    int port, {
    bool withDebugger = false,
437
    bool startPaused = false,
438 439 440
    bool pauseOnExceptions = false,
    File pidFile,
  }) async {
441 442
    await _setupProcess(
      <String>[
443 444 445 446 447 448
        'attach',
        '--machine',
        '-d',
        'flutter-tester',
        '--debug-port',
        '$port',
449 450 451 452 453 454
      ],
      withDebugger: withDebugger,
      startPaused: startPaused,
      pauseOnExceptions: pauseOnExceptions,
      pidFile: pidFile,
    );
455 456 457 458 459
  }

  @override
  Future<void> _setupProcess(
    List<String> args, {
460
    String script,
461
    bool withDebugger = false,
462
    bool startPaused = false,
463 464 465
    bool pauseOnExceptions = false,
    File pidFile,
  }) async {
466
    assert(!startPaused || withDebugger);
467 468
    await super._setupProcess(
      args,
469
      script: script,
470 471 472 473 474
      withDebugger: withDebugger,
      pidFile: pidFile,
    );

    // Stash the PID so that we can terminate the VM more reliably than using
475 476
    // _process.kill() (`flutter` is a shell script so _process itself is a
    // shell, not the flutter tool's Dart process).
477
    final Map<String, dynamic> connected = await _waitFor(event: 'daemon.connected');
478
    _processPid = connected['params']['pid'];
479 480 481

    // 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.
482
    final Future<Map<String, dynamic>> started = _waitFor(event: 'app.started', timeout: appStartTimeout);
483 484

    if (withDebugger) {
485
      final Map<String, dynamic> debugPort = await _waitFor(event: 'app.debugPort', timeout: appStartTimeout);
486 487
      final String wsUriString = debugPort['params']['wsUri'];
      _vmServiceWsUri = Uri.parse(wsUriString);
488
      await connectToVmService(pauseOnExceptions: pauseOnExceptions);
489 490
      if (!startPaused)
        await resume(waitForNextPause: false);
491 492 493 494 495 496 497
    }

    // Now await the started event; if it had already happened the future will
    // have already completed.
    _currentRunningAppId = (await started)['params']['appId'];
  }

498
  Future<void> hotRestart({ bool pause = false }) => _restart(fullRestart: true, pause: pause);
499 500
  Future<void> hotReload() => _restart(fullRestart: false);

501
  Future<void> _restart({ bool fullRestart = false, bool pause = false }) async {
502 503 504
    if (_currentRunningAppId == null)
      throw Exception('App has not started yet');

505 506 507
    _debugPrint('Performing ${ pause ? "paused " : "" }${ fullRestart ? "hot restart" : "hot reload" }...');
    final dynamic hotReloadResponse = await _sendRequest(
      'app.restart',
508
      <String, dynamic>{'appId': _currentRunningAppId, 'fullRestart': fullRestart, 'pause': pause},
509
    );
510
    _debugPrint('${ fullRestart ? "Hot restart" : "Hot reload" } complete.');
511

512
    if (hotReloadResponse == null || hotReloadResponse['code'] != 0)
513 514 515 516
      _throwErrorResponse('Hot ${fullRestart ? 'restart' : 'reload'} request failed');
  }

  Future<int> detach() async {
517 518 519
    if (_process == null) {
      return 0;
    }
520
    if (_vmService != null) {
521
      _debugPrint('Closing VM service...');
522 523 524
      _vmService.dispose();
    }
    if (_currentRunningAppId != null) {
525
      _debugPrint('Detaching from app...');
526
      await Future.any<void>(<Future<void>>[
527
        _process.exitCode,
528 529 530 531 532 533 534 535 536 537
        _sendRequest(
          'app.detach',
          <String, dynamic>{'appId': _currentRunningAppId},
        ),
      ]).timeout(
        quitTimeout,
        onTimeout: () { _debugPrint('app.detach did not return within $quitTimeout'); },
      );
      _currentRunningAppId = null;
    }
538 539
    _debugPrint('Waiting for process to end...');
    return _process.exitCode.timeout(quitTimeout, onTimeout: _killGracefully);
540 541 542 543
  }

  Future<int> stop() async {
    if (_vmService != null) {
544
      _debugPrint('Closing VM service...');
545 546 547
      _vmService.dispose();
    }
    if (_currentRunningAppId != null) {
548
      _debugPrint('Stopping application...');
549
      await Future.any<void>(<Future<void>>[
550
        _process.exitCode,
551 552 553 554 555 556 557 558 559 560
        _sendRequest(
          'app.stop',
          <String, dynamic>{'appId': _currentRunningAppId},
        ),
      ]).timeout(
        quitTimeout,
        onTimeout: () { _debugPrint('app.stop did not return within $quitTimeout'); },
      );
      _currentRunningAppId = null;
    }
561 562 563
    if (_process != null) {
      _debugPrint('Waiting for process to end...');
      return _process.exitCode.timeout(quitTimeout, onTimeout: _killGracefully);
564 565 566 567
    }
    return 0;
  }

568 569 570
  int id = 1;
  Future<dynamic> _sendRequest(String method, dynamic params) async {
    final int requestId = id++;
571
    final Map<String, dynamic> request = <String, dynamic>{
572 573
      'id': requestId,
      'method': method,
574
      'params': params,
575
    };
576
    final String jsonEncoded = json.encode(<Map<String, dynamic>>[request]);
577
    _debugPrint(jsonEncoded, topic: '=stdin=>');
578

579
    // Set up the response future before we send the request to avoid any
580
    // races. If the method we're calling is app.stop then we tell _waitFor not
581 582 583 584 585
    // 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',
    );
586
    _process.stdin.writeln(jsonEncoded);
587
    final Map<String, dynamic> response = await responseFuture;
588

589
    if (response['error'] != null || response['result'] == null)
590
      _throwErrorResponse('Unexpected error response');
591

592
    return response['result'];
593 594
  }

595 596
  void _throwErrorResponse(String message) {
    throw '$message\n\n$_lastResponse\n\n${_errorBuffer.toString()}'.trim();
597
  }
598 599
}

600
class FlutterTestTestDriver extends FlutterTestDriver {
601 602
  FlutterTestTestDriver(Directory _projectFolder, {String logPrefix})
    : super(_projectFolder, logPrefix: logPrefix);
603 604 605 606 607 608 609 610 611 612

  Future<void> test({
    String testFile = 'test/test.dart',
    bool withDebugger = false,
    bool pauseOnExceptions = false,
    File pidFile,
    Future<void> Function() beforeStart,
  }) async {
    await _setupProcess(<String>[
        'test',
613
        '--disable-service-auth-codes',
614 615
        '--machine',
        '-d',
616
        'flutter-tester',
617
    ], script: testFile, withDebugger: withDebugger, pauseOnExceptions: pauseOnExceptions, pidFile: pidFile, beforeStart: beforeStart);
618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639
  }

  @override
  Future<void> _setupProcess(
    List<String> args, {
    String script,
    bool withDebugger = false,
    bool pauseOnExceptions = false,
    File pidFile,
    Future<void> Function() beforeStart,
  }) async {
    await super._setupProcess(
      args,
      script: script,
      withDebugger: withDebugger,
      pidFile: pidFile,
    );

    // 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();
640
    _processPid = version['pid'];
641 642 643 644 645 646 647 648 649 650

    if (withDebugger) {
      final Map<String, dynamic> startedProcess = await _waitFor(event: 'test.startedProcess', timeout: appStartTimeout);
      final String vmServiceHttpString = startedProcess['params']['observatoryUri'];
      _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();
      }
651
      await resume(waitForNextPause: false);
652 653 654 655
    }
  }

  Future<Map<String, dynamic>> _waitForJson({
656
    Duration timeout = defaultTimeout,
657
  }) async {
658
    assert(timeout != null);
659
    return _timeoutWithMessages<Map<String, dynamic>>(
660 661
      () => _stdout.stream.map<Map<String, dynamic>>(_parseJsonResponse)
          .firstWhere((Map<String, dynamic> output) => output != null),
662
      timeout: timeout,
663
      task: 'Waiting for JSON',
664 665 666 667 668 669 670 671 672 673 674 675 676
    );
  }

  Map<String, dynamic> _parseJsonResponse(String line) {
    try {
      return json.decode(line);
    } catch (e) {
      // Not valid JSON, so likely some other output.
      return null;
    }
  }
}

677
Stream<String> transformToLines(Stream<List<int>> byteStream) {
678
  return byteStream.transform<String>(utf8.decoder).transform<String>(const LineSplitter());
679
}
680

681 682 683
Map<String, dynamic> parseFlutterResponse(String line) {
  if (line.startsWith('[') && line.endsWith(']')) {
    try {
684 685
      final Map<String, dynamic> response = json.decode(line)[0];
      return response;
686 687 688 689 690 691 692 693
    } catch (e) {
      // Not valid JSON, so likely some other output that was surrounded by [brackets]
      return null;
    }
  }
  return null;
}

694 695 696 697 698 699
class SourcePosition {
  SourcePosition(this.line, this.column);

  final int line;
  final int column;
}