test_driver.dart 32.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// 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';
7
import 'dart:io' as io; // flutter_ignore: dart_io_import
8 9 10 11

import 'package:file/file.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
12
import 'package:flutter_tools/src/base/utils.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

import '../src/common.dart';
18
import 'test_utils.dart';
19

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

final DateTime startTime = DateTime.now();

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

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

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

57
  VmService? _vmService;
58
  String get lastErrorInfo => _errorBuffer.toString();
59
  Stream<String> get stdout => _stdout.stream;
60
  Stream<String> get stderr => _stderr.stream;
61
  int? get vmServicePort => _vmServiceWsUri?.port;
62
  bool get hasExited => _hasExited;
63
  Uri? get vmServiceWsUri => _vmServiceWsUri;
64

65 66 67
  String lastTime = '';
  void _debugPrint(String message, { String topic = '' }) {
    const int maxLength = 2500;
68
    final String truncatedMessage = message.length > maxLength ? '${message.substring(0, maxLength)}...' : message;
69 70 71
    final String line = '${topic.padRight(10)} $truncatedMessage';
    _allMessages.add(line);
    final int timeInSeconds = DateTime.now().difference(startTime).inSeconds;
72
    String time = '${timeInSeconds.toString().padLeft(5)}s ';
73 74 75 76
    if (time == lastTime) {
      time = ' ' * time.length;
    } else {
      lastTime = time;
77
    }
78
    if (_printDebugOutputToStdOut) {
79 80 81 82
      // This is the one place in this file that can call print. It is gated by
      // _printDebugOutputToStdOut which should not be set to true in CI; it is
      // intended only for use in local debugging.
      // ignore: avoid_print
83
      print('$time$_logPrefix$line');
84
    }
85
  }
86

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

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

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

    // 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.
133 134
    _stdout.stream.listen((String message) => _debugPrint(message, topic: '<=stdout='));
    _stderr.stream.listen((String message) => _debugPrint(message, topic: '<=stderr='));
135 136
  }

137
  Future<void> get done async => _process?.exitCode;
138

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

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

    await Future.wait(<Future<Success>>[
156 157
      _vmService!.streamListen('Isolate'),
      _vmService!.streamListen('Debug'),
158 159
    ]);

160
    if ((await _vmService!.getVM()).isolates?.isEmpty ?? true) {
161 162 163
      await isolateStarted.future;
    }

164 165
    await waitForPause();
    if (pauseOnExceptions) {
166
      await _vmService!.setIsolatePauseMode(
167
        await _getFlutterIsolateId(),
168
        exceptionPauseMode: ExceptionPauseMode.kUnhandled,
169 170
      );
    }
171 172
  }

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

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

189
  Future<int> _killGracefully() async {
190
    if (_processPid == null) {
191
      return -1;
192
    }
193 194 195 196
    // 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)
197 198 199 200
        .catchError((Object e) {
          _debugPrint('Ignoring failure to resume during shutdown');
          return null;
        });
201

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

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

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

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

229 230 231 232 233 234 235 236 237 238
  /// 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 {
239 240
    final String isolateId = await _getFlutterIsolateId();
    final Future<Event> event = subscribeToPauseEvent(isolateId);
241
    await addBreakpoint(uri, line);
242
    await waitForPauseEvent(isolateId, event);
243 244
  }

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

254 255 256 257 258 259 260 261 262 263 264
  Future<Event> subscribeToPauseEvent(String isolateId) => subscribeToDebugEvent('Pause', isolateId);
  Future<Event> subscribeToResumeEvent(String isolateId) => subscribeToDebugEvent('Resume', isolateId);

  Future<Isolate> waitForPauseEvent(String isolateId, Future<Event> event) =>
      waitForDebugEvent('Pause', isolateId, event);
  Future<Isolate> waitForResumeEvent(String isolateId, Future<Event> event) =>
      waitForDebugEvent('Resume', isolateId, event);

  Future<Isolate> waitForPause() async => subscribeAndWaitForDebugEvent('Pause', await _getFlutterIsolateId());
  Future<Isolate> waitForResume() async => subscribeAndWaitForDebugEvent('Resume', await _getFlutterIsolateId());

265

266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
  Future<Isolate> subscribeAndWaitForDebugEvent(String kind, String isolateId) {
    final Future<Event> event = subscribeToDebugEvent(kind, isolateId);
    return waitForDebugEvent(kind, isolateId, event);
  }

  /// Subscribes to debug events containing [kind].
  ///
  /// Returns a future that completes when the [kind] event is received.
  ///
  /// Note that this method should be called before the command that triggers
  /// the event to subscribe to the event in time, for example:
  ///
  /// ```
  ///  var event = subscribeToDebugEvent('Pause', id); // Subscribe to 'pause' events.
  ///  ...                                             // Code that pauses the app.
  ///  await waitForDebugEvent('Pause', id, event);    // Isolate is paused now.
  /// ```
  Future<Event> subscribeToDebugEvent(String kind, String isolateId) {
    _debugPrint('Start listening for $kind events');

286
    return _vmService!.onDebugEvent
287
      .where((Event event) {
288
        return event.isolate?.id == isolateId
289
            && (event.kind?.startsWith(kind) ?? false);
290 291 292 293 294 295 296
      }).first;
  }

  /// Wait for the [event] if needed.
  ///
  /// Return immediately if the isolate is already in the desired state.
  Future<Isolate> waitForDebugEvent(String kind, String isolateId, Future<Event> event) {
297 298
    return _timeoutWithMessages<Isolate>(
      () async {
299 300 301
        // But also check if the isolate was already at the state we need (only after we've
        // set up the subscription) to avoid races. If it already in the desired state, we
        // don't need to wait for the event.
302 303
        final VmService vmService = _vmService!;
        final Isolate isolate = await vmService.getIsolate(isolateId);
304
        if (isolate.pauseEvent?.kind?.startsWith(kind) ?? false) {
305
          _debugPrint('Isolate was already at "$kind" (${isolate.pauseEvent!.kind}).');
306
          event.ignore();
307
        } else {
308
          _debugPrint('Waiting for "$kind" event to arrive...');
309
          await event;
310
        }
311

312
        return vmService.getIsolate(isolateId);
313
      },
314
      task: 'Waiting for isolate to $kind',
315
    );
316
  }
317

318 319 320 321 322
  Future<Isolate?> resume({ bool waitForNextPause = false }) => _resume(null, waitForNextPause);
  Future<Isolate?> stepOver({ bool waitForNextPause = true }) => _resume(StepOption.kOver, waitForNextPause);
  Future<Isolate?> stepOverAsync({ bool waitForNextPause = true }) => _resume(StepOption.kOverAsyncSuspension, waitForNextPause);
  Future<Isolate?> stepInto({ bool waitForNextPause = true }) => _resume(StepOption.kInto, waitForNextPause);
  Future<Isolate?> stepOut({ bool waitForNextPause = true }) => _resume(StepOption.kOut, waitForNextPause);
323

324
  Future<bool> isAtAsyncSuspension() async {
325
    final Isolate isolate = await getFlutterIsolate();
326
    return isolate.pauseEvent?.atAsyncSuspension ?? false;
327 328
  }

329
  Future<Isolate?> stepOverOrOverAsyncSuspension({ bool waitForNextPause = true }) async {
330
    if (await isAtAsyncSuspension()) {
331
      return stepOverAsync(waitForNextPause: waitForNextPause);
332
    }
333
    return stepOver(waitForNextPause: waitForNextPause);
334
  }
335

336
  Future<Isolate?> _resume(String? step, bool waitForNextPause) async {
337
    assert(waitForNextPause != null);
338 339 340 341 342
    final String isolateId = await _getFlutterIsolateId();

    final Future<Event> resume = subscribeToResumeEvent(isolateId);
    final Future<Event> pause = subscribeToPauseEvent(isolateId);

343 344
    await _timeoutWithMessages<Object?>(
      () async => _vmService!.resume(isolateId, step: step),
345 346
      task: 'Resuming isolate (step=$step)',
    );
347
    await waitForResumeEvent(isolateId, resume);
348
    return waitForNextPause ? waitForPauseEvent(isolateId, pause) : null;
349 350
  }

351 352
  Future<ObjRef> evaluateInFrame(String expression) async {
    return _timeoutWithMessages<ObjRef>(
353
      () async => await _vmService!.evaluateInFrame(await _getFlutterIsolateId(), 0, expression) as ObjRef,
354 355
      task: 'Evaluating expression ($expression)',
    );
356 357
  }

358 359 360
  Future<ObjRef> evaluate(String targetId, String expression) async {
    return _timeoutWithMessages<ObjRef>(
      () async => await _vmService!.evaluate(await _getFlutterIsolateId(), targetId, expression) as ObjRef,
361 362
      task: 'Evaluating expression ($expression for $targetId)',
    );
363 364 365 366
  }

  Future<Frame> getTopStackFrame() async {
    final String flutterIsolateId = await _getFlutterIsolateId();
367 368 369
    final Stack stack = await _vmService!.getStack(flutterIsolateId);
    final List<Frame>? frames = stack.frames;
    if (frames == null || frames.isEmpty) {
370
      throw Exception('Stack is empty');
371
    }
372
    return frames.first;
373 374
  }

375
  Future<SourcePosition?> getSourceLocation() async {
376 377
    final String flutterIsolateId = await _getFlutterIsolateId();
    final Frame frame = await getTopStackFrame();
378 379
    final Script script = await _vmService!.getObject(flutterIsolateId, frame.location!.script!.id!) as Script;
    return _lookupTokenPos(script.tokenPosTable!, frame.location!.tokenPos!);
380 381
  }

382
  SourcePosition? _lookupTokenPos(List<List<int>> table, int tokenPos) {
383
    for (final List<int> row in table) {
384 385 386 387 388 389 390 391 392 393 394
      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;
395 396
  }

397 398 399
  Future<Map<String, Object?>> _waitFor({
    String? event,
    int? id,
400
    Duration timeout = defaultTimeout,
401 402
    bool ignoreAppStopEvent = false,
  }) async {
403 404 405 406
    assert(timeout != null);
    assert(event != null || id != null);
    assert(event == null || id == null);
    final String interestingOccurrence = event != null ? '$event event' : 'response to request $id';
407 408
    final Completer<Map<String, Object?>> response = Completer<Map<String, Object?>>();
    StreamSubscription<String>? subscription;
409
    subscription = _stdout.stream.listen((String line) async {
410
      final Map<String, Object?>? json = parseFlutterResponse(line);
411
      _lastResponse = line;
412
      if (json == null) {
413
        return;
414
      }
415
      if ((event != null && json['event'] == event) ||
416
          (id != null && json['id'] == id)) {
417
        await subscription?.cancel();
418
        _debugPrint('OK ($interestingOccurrence)');
419
        response.complete(json);
420
      } else if (!ignoreAppStopEvent && json['event'] == 'app.stop') {
421
        await subscription?.cancel();
422
        final StringBuffer error = StringBuffer();
423
        error.write('Received app.stop event while waiting for $interestingOccurrence\n\n$_errorBuffer');
424 425 426 427
        final Object? jsonParams = json['params'];
        if (jsonParams is Map<String, Object?>) {
          if (jsonParams['error'] != null) {
            error.write('${jsonParams['error']}\n\n');
428
          }
429 430 431
          final Object? trace = jsonParams['trace'];
          if (trace != null) {
            error.write('$trace\n\n');
432
          }
433
        }
434
        response.completeError(Exception(error.toString()));
435 436
      }
    });
437

438 439 440 441 442
    return _timeoutWithMessages(
      () => response.future,
      timeout: timeout,
      task: 'Expecting $interestingOccurrence',
    ).whenComplete(subscription.cancel);
443 444
  }

445 446
  Future<T> _timeoutWithMessages<T>(
    Future<T> Function() callback, {
447
    required String task,
448 449 450 451 452 453 454
    Duration timeout = defaultTimeout,
  }) {
    assert(task != null);
    assert(timeout != null);

    if (_printDebugOutputToStdOut) {
      _debugPrint('$task...');
455 456
      final Timer longWarning = Timer(timeout, () => _debugPrint('$task is taking longer than usual...'));
      return callback().whenComplete(longWarning.cancel);
457 458 459 460 461 462
    }

    // 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');
463
    final DateTime start = DateTime.now();
464 465
    bool timeoutExpired = false;
    void logMessage(String logLine) {
466
      final int ms = DateTime.now().difference(start).inMilliseconds;
467 468
      final String formattedLine = '[+ ${ms.toString().padLeft(5)}] $logLine';
      messages.writeln(formattedLine);
469
    }
470
    final StreamSubscription<String> subscription = _allMessages.stream.listen(logMessage);
471

472
    final Timer longWarning = Timer(timeout, () {
473
      _debugPrint(messages.toString());
474
      timeoutExpired = true;
475
      _debugPrint('$task is taking longer than usual...');
476
    });
477
    final Future<T> future = callback().whenComplete(longWarning.cancel);
478

479
    return future.catchError((Object error) {
480 481
      if (!timeoutExpired) {
        timeoutExpired = true;
482
        _debugPrint(messages.toString());
483
      }
484
      throw error; // ignore: only_throw_errors
485
    }).whenComplete(() => subscription.cancel());
486
  }
487 488 489
}

class FlutterRunTestDriver extends FlutterTestDriver {
490
  FlutterRunTestDriver(
491 492
    super.projectFolder, {
    super.logPrefix,
493
    this.spawnDdsInstance = true,
494
  });
495

496
  String? _currentRunningAppId;
497

498
  Future<void> run({
499
    bool withDebugger = false,
500
    bool startPaused = false,
501
    bool pauseOnExceptions = false,
502
    bool chrome = false,
503
    bool expressionEvaluation = true,
504
    bool structuredErrors = false,
505
    bool singleWidgetReloads = false,
506 507
    String? script,
    List<String>? additionalCommandArgs,
508
  }) async {
509 510
    await _setupProcess(
      <String>[
511
        'run',
512 513
        if (!chrome)
          '--disable-service-auth-codes',
514
        '--machine',
515
        if (!spawnDdsInstance) '--no-dds',
516
        ...getLocalEngineArguments(),
517
        '-d',
518
        if (chrome)
519 520 521
          ...<String>[
            'chrome',
            '--web-run-headless',
522
            if (!expressionEvaluation) '--no-web-enable-expression-evaluation',
523
          ]
524 525
        else
          'flutter-tester',
526 527
        if (structuredErrors)
          '--dart-define=flutter.inspector.structuredErrors=true',
528
        ...?additionalCommandArgs,
529 530 531 532
      ],
      withDebugger: withDebugger,
      startPaused: startPaused,
      pauseOnExceptions: pauseOnExceptions,
533
      script: script,
534
      singleWidgetReloads: singleWidgetReloads,
535
    );
536 537 538 539 540
  }

  Future<void> attach(
    int port, {
    bool withDebugger = false,
541
    bool startPaused = false,
542
    bool pauseOnExceptions = false,
543
    bool singleWidgetReloads = false,
544
    List<String>? additionalCommandArgs,
545
  }) async {
546
    _attachPort = port;
547 548
    await _setupProcess(
      <String>[
549
        'attach',
550
        ...getLocalEngineArguments(),
551
        '--machine',
552
        if (!spawnDdsInstance)
553
          '--no-dds',
554 555 556 557
        '-d',
        'flutter-tester',
        '--debug-port',
        '$port',
558
        ...?additionalCommandArgs,
559 560 561 562
      ],
      withDebugger: withDebugger,
      startPaused: startPaused,
      pauseOnExceptions: pauseOnExceptions,
563
      singleWidgetReloads: singleWidgetReloads,
564
      attachPort: port,
565
    );
566 567 568 569 570
  }

  @override
  Future<void> _setupProcess(
    List<String> args, {
571
    String? script,
572
    bool withDebugger = false,
573
    bool startPaused = false,
574
    bool pauseOnExceptions = false,
575
    bool singleWidgetReloads = false,
576
    int? attachPort,
577
  }) async {
578
    assert(!startPaused || withDebugger);
579 580
    await super._setupProcess(
      args,
581
      script: script,
582
      withDebugger: withDebugger,
583
      singleWidgetReloads: singleWidgetReloads,
584 585
    );

586 587 588 589 590 591
    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.
592
    unawaited(_process?.exitCode.then((_) {
593
      if (!prematureExitGuard.isCompleted) {
594
        prematureExitGuard.completeError(Exception('Process exited prematurely: ${args.join(' ')}: $_errorBuffer'));
595 596
      }
    }));
597

598 599 600 601 602
    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).
603 604
        final Map<String, Object?> connected = await _waitFor(event: 'daemon.connected');
        _processPid = (connected['params'] as Map<String, Object?>?)?['pid'] as int?;
605 606 607

        // 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.
608
        final Future<Map<String, Object?>> started = _waitFor(event: 'app.started', timeout: appStartTimeout);
609 610

        if (withDebugger) {
611 612
          final Map<String, Object?> debugPort = await _waitFor(event: 'app.debugPort', timeout: appStartTimeout);
          final String wsUriString = (debugPort['params']! as Map<String, Object?>)['wsUri']! as String;
613 614 615
          _vmServiceWsUri = Uri.parse(wsUriString);
          await connectToVmService(pauseOnExceptions: pauseOnExceptions);
          if (!startPaused) {
616
            await resume();
617 618
          }
        }
619

620 621 622 623 624 625 626
        // 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;
        }

627 628
        // Now await the started event; if it had already happened the future will
        // have already completed.
629
        _currentRunningAppId = ((await started)['params'] as Map<String, Object?>?)?['appId'] as String?;
630
        prematureExitGuard.complete();
631
      } on Exception catch (error, stackTrace) {
632
        prematureExitGuard.completeError(Exception(error.toString()), stackTrace);
633
      }
634
    }());
635

636
    return prematureExitGuard.future;
637 638
  }

639
  Future<void> hotRestart({ bool pause = false, bool debounce = false}) => _restart(fullRestart: true, pause: pause);
640
  Future<void> hotReload({ bool debounce = false, int? debounceDurationOverrideMs }) =>
641
      _restart(debounce: debounce, debounceDurationOverrideMs: debounceDurationOverrideMs);
642

643 644 645 646 647 648
  Future<void> scheduleFrame() async {
    if (_currentRunningAppId == null) {
      throw Exception('App has not started yet');
    }
    await _sendRequest(
      'app.callServiceExtension',
649
      <String, Object?>{'appId': _currentRunningAppId, 'methodName': 'ext.ui.window.scheduleFrame'},
650 651 652
    );
  }

653
  Future<void> _restart({ bool fullRestart = false, bool pause = false, bool debounce = false, int? debounceDurationOverrideMs }) async {
654
    if (_currentRunningAppId == null) {
655
      throw Exception('App has not started yet');
656
    }
657

658
    _debugPrint('Performing ${ pause ? "paused " : "" }${ fullRestart ? "hot restart" : "hot reload" }...');
659
    final Map<String, Object?>? hotReloadResponse = await _sendRequest(
660
      'app.restart',
661 662
      <String, Object?>{'appId': _currentRunningAppId, 'fullRestart': fullRestart, 'pause': pause, 'debounce': debounce, 'debounceDurationOverrideMs': debounceDurationOverrideMs},
    ) as Map<String, Object?>?;
663
    _debugPrint('${fullRestart ? "Hot restart" : "Hot reload"} complete.');
664

665
    if (hotReloadResponse == null || hotReloadResponse['code'] != 0) {
666
      _throwErrorResponse('Hot ${fullRestart ? 'restart' : 'reload'} request failed');
667
    }
668 669 670
  }

  Future<int> detach() async {
671 672
    final Process? process = _process;
    if (process == null) {
673 674
      return 0;
    }
675 676
    final VmService? vmService = _vmService;
    if (vmService != null) {
677
      _debugPrint('Closing VM service...');
678
      await vmService.dispose();
679 680
    }
    if (_currentRunningAppId != null) {
681
      _debugPrint('Detaching from app...');
682
      await Future.any<void>(<Future<void>>[
683
        process.exitCode,
684 685
        _sendRequest(
          'app.detach',
686
          <String, Object?>{'appId': _currentRunningAppId},
687 688 689 690 691 692 693
        ),
      ]).timeout(
        quitTimeout,
        onTimeout: () { _debugPrint('app.detach did not return within $quitTimeout'); },
      );
      _currentRunningAppId = null;
    }
694
    _debugPrint('Waiting for process to end...');
695
    return process.exitCode.timeout(quitTimeout, onTimeout: _killGracefully);
696 697 698
  }

  Future<int> stop() async {
699 700
    final VmService? vmService = _vmService;
    if (vmService != null) {
701
      _debugPrint('Closing VM service...');
702
      await vmService.dispose();
703
    }
704
    final Process? process = _process;
705
    if (_currentRunningAppId != null) {
706
      _debugPrint('Stopping application...');
707
      await Future.any<void>(<Future<void>>[
708
        process!.exitCode,
709 710
        _sendRequest(
          'app.stop',
711
          <String, Object?>{'appId': _currentRunningAppId},
712 713 714 715 716 717 718
        ),
      ]).timeout(
        quitTimeout,
        onTimeout: () { _debugPrint('app.stop did not return within $quitTimeout'); },
      );
      _currentRunningAppId = null;
    }
719
    if (process != null) {
720
      _debugPrint('Waiting for process to end...');
721
      return process.exitCode.timeout(quitTimeout, onTimeout: _killGracefully);
722 723 724 725
    }
    return 0;
  }

726
  int id = 1;
727
  Future<Object?> _sendRequest(String method, Object? params) async {
728
    final int requestId = id++;
729
    final Map<String, Object?> request = <String, Object?>{
730 731
      'id': requestId,
      'method': method,
732
      'params': params,
733
    };
734
    final String jsonEncoded = json.encode(<Map<String, Object?>>[request]);
735
    _debugPrint(jsonEncoded, topic: '=stdin=>');
736

737
    // Set up the response future before we send the request to avoid any
738
    // races. If the method we're calling is app.stop then we tell _waitFor not
739
    // to throw if it sees an app.stop event before the response to this request.
740
    final Future<Map<String, Object?>> responseFuture = _waitFor(
741 742 743
      id: requestId,
      ignoreAppStopEvent: method == 'app.stop',
    );
744 745
    _process?.stdin.writeln(jsonEncoded);
    final Map<String, Object?> response = await responseFuture;
746

747
    if (response['error'] != null || response['result'] == null) {
748
      _throwErrorResponse('Unexpected error response');
749
    }
750

751
    return response['result'];
752 753
  }

754
  void _throwErrorResponse(String message) {
755
    throw Exception('$message\n\n$_lastResponse\n\n$_errorBuffer'.trim());
756
  }
757 758

  final bool spawnDdsInstance;
759 760
}

761
class FlutterTestTestDriver extends FlutterTestDriver {
762
  FlutterTestTestDriver(super.projectFolder, {super.logPrefix});
763 764 765 766 767

  Future<void> test({
    String testFile = 'test/test.dart',
    bool withDebugger = false,
    bool pauseOnExceptions = false,
768
    bool coverage = false,
769
    Future<void> Function()? beforeStart,
770 771
  }) async {
    await _setupProcess(<String>[
772
      'test',
773
      ...getLocalEngineArguments(),
774 775
      '--disable-service-auth-codes',
      '--machine',
776 777
      if (coverage)
        '--coverage',
778
    ], script: testFile, withDebugger: withDebugger, pauseOnExceptions: pauseOnExceptions, beforeStart: beforeStart);
779 780 781 782 783
  }

  @override
  Future<void> _setupProcess(
    List<String> args, {
784
    String? script,
785 786
    bool withDebugger = false,
    bool pauseOnExceptions = false,
787
    Future<void> Function()? beforeStart,
788
    bool singleWidgetReloads = false,
789 790 791 792 793
  }) async {
    await super._setupProcess(
      args,
      script: script,
      withDebugger: withDebugger,
794
      singleWidgetReloads: singleWidgetReloads,
795 796 797 798 799
    );

    // 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).
800 801
    final Map<String, Object?>? version = await _waitForJson();
    _processPid = version?['pid'] as int?;
802 803

    if (withDebugger) {
804 805 806
      final Map<String, Object?> startedProcessParams =
          (await _waitFor(event: 'test.startedProcess', timeout: appStartTimeout))['params']! as Map<String, Object?>;
      final String vmServiceHttpString = startedProcessParams['observatoryUri']! as String;
807 808 809 810 811 812
      _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();
      }
813
      await resume();
814 815 816
    }
  }

817
  Future<Map<String, Object?>?> _waitForJson({
818
    Duration timeout = defaultTimeout,
819
  }) async {
820
    assert(timeout != null);
821 822 823
    return _timeoutWithMessages<Map<String, Object?>?>(
      () => _stdout.stream.map<Map<String, Object?>?>(_parseJsonResponse)
          .firstWhere((Map<String, Object?>? output) => output != null),
824
      timeout: timeout,
825
      task: 'Waiting for JSON',
826 827 828
    );
  }

829
  Map<String, Object?>? _parseJsonResponse(String line) {
830
    try {
831
      return castStringKeyedMap(json.decode(line));
832
    } on Exception {
833 834 835 836
      // Not valid JSON, so likely some other output.
      return null;
    }
  }
837 838 839 840 841 842 843

  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 {
844
          final Map<String, Object?>? json = _parseJsonResponse(line);
845 846 847 848 849 850 851
          if (json != null && json['type'] != null && json['success'] != null) {
            done.complete(json['type'] == 'done' && json['success'] == true);
          }
        });

    await resume();

852 853 854
    final Future<void> timeoutFuture =
        Future<void>.delayed(defaultTimeout);
    await Future.any<void>(<Future<void>>[done.future, timeoutFuture]);
855 856 857 858 859
    await subscription.cancel();
    if (!done.isCompleted) {
      await quit();
    }
  }
860 861
}

862
Stream<String> transformToLines(Stream<List<int>> byteStream) {
863
  return byteStream.transform<String>(utf8.decoder).transform<String>(const LineSplitter());
864
}
865

866
Map<String, Object?>? parseFlutterResponse(String line) {
867
  if (line.startsWith('[') && line.endsWith(']') && line.length > 2) {
868
    try {
869
      final Map<String, Object?>? response = castStringKeyedMap((json.decode(line) as List<Object?>)[0]);
870
      return response;
871
    } on FormatException {
872 873 874 875 876 877 878
      // Not valid JSON, so likely some other output that was surrounded by [brackets]
      return null;
    }
  }
  return null;
}

879 880 881 882 883 884
class SourcePosition {
  SourcePosition(this.line, this.column);

  final int line;
  final int column;
}
885

886
Future<Isolate> waitForExtension(VmService vmService, String extension) async {
887
  final Completer<void> completer = Completer<void>();
888 889 890 891 892
  try {
    await vmService.streamListen(EventStreams.kExtension);
  } on RPCError {
    // Do nothing, already subscribed.
  }
893
  vmService.onExtensionEvent.listen((Event event) {
894
    if (event.json?['extensionKind'] == 'Flutter.FrameworkInitialization') {
895 896 897
      completer.complete();
    }
  });
898 899 900
  final IsolateRef isolateRef = (await vmService.getVM()).isolates!.first;
  final Isolate isolate = await vmService.getIsolate(isolateRef.id!);
  if (isolate.extensionRPCs!.contains(extension)) {
901 902 903 904 905
    return isolate;
  }
  await completer.future;
  return isolate;
}