flutter_platform.dart 35.6 KB
Newer Older
1 2 3 4 5 6 7
// Copyright 2015 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';

8
import 'package:meta/meta.dart';
9 10
import 'package:stream_channel/stream_channel.dart';

11 12
import 'package:test/src/backend/runtime.dart'; // ignore: implementation_imports
import 'package:test/src/backend/suite_platform.dart'; // ignore: implementation_imports
Ian Hickson's avatar
Ian Hickson committed
13 14
import 'package:test/src/runner/plugin/platform.dart'; // ignore: implementation_imports
import 'package:test/src/runner/plugin/hack_register_platform.dart' as hack; // ignore: implementation_imports
15

16
import '../artifacts.dart';
17
import '../base/common.dart';
18
import '../base/file_system.dart';
19
import '../base/io.dart';
20
import '../base/process_manager.dart';
21
import '../compile.dart';
22
import '../dart/package_map.dart';
23
import '../globals.dart';
24
import 'watcher.dart';
25

26 27
/// The timeout we give the test process to connect to the test harness
/// once the process has entered its main method.
28
const Duration _kTestStartupTimeout = Duration(minutes: 1);
29 30 31 32

/// The timeout we give the test process to start executing Dart code. When the
/// CPU is under severe load, this can take a while, but it's not indicative of
/// any problem with Flutter, so we give it a large timeout.
33
const Duration _kTestProcessTimeout = Duration(minutes: 5);
34 35 36 37 38 39 40 41 42 43

/// Message logged by the test process to signal that its main method has begun
/// execution.
///
/// The test harness responds by starting the [_kTestStartupTimeout] countdown.
/// The CPU may be throttled, which can cause a long delay in between when the
/// process is spawned and when dart code execution begins; we don't want to
/// hold that against the test.
const String _kStartTimeoutTimerMessage = 'sky_shell test process has entered main method';

44 45 46 47 48 49 50 51
/// The name of the test configuration file that will be discovered by the
/// test harness if it exists in the project directory hierarchy.
const String _kTestConfigFileName = 'flutter_test_config.dart';

/// The name of the file that signals the root of the project and that will
/// cause the test harness to stop scanning for configuration files.
const String _kProjectRootSentinel = 'pubspec.yaml';

52 53
/// The address at which our WebSocket server resides and at which the sky_shell
/// processes will host the Observatory server.
54
final Map<InternetAddressType, InternetAddress> _kHosts = <InternetAddressType, InternetAddress>{
55 56
  InternetAddressType.IPv4: InternetAddress.loopbackIPv4,
  InternetAddressType.IPv6: InternetAddress.loopbackIPv6,
57
};
58

59 60
/// Configure the `test` package to work with Flutter.
///
61
/// On systems where each [_FlutterPlatform] is only used to run one test suite
62
/// (that is, one Dart file with a `*_test.dart` file name and a single `void
63
/// main()`), you can set an observatory port explicitly.
64 65
void installHook({
  @required String shellPath,
66
  TestWatcher watcher,
67 68 69
  bool enableObservatory = false,
  bool machine = false,
  bool startPaused = false,
70
  bool previewDart2 = false,
71
  int port = 0,
72
  String precompiledDillPath,
73 74
  bool trackWidgetCreation = false,
  bool updateGoldens = false,
75
  int observatoryPort,
76
  InternetAddressType serverType = InternetAddressType.IPv4,
77
}) {
78
  assert(!enableObservatory || (!startPaused && observatoryPort == null));
79
  hack.registerPlatformPlugin(
80
    <Runtime>[Runtime.vm],
81
    () => new _FlutterPlatform(
82
      shellPath: shellPath,
83
      watcher: watcher,
84
      machine: machine,
85 86
      enableObservatory: enableObservatory,
      startPaused: startPaused,
87
      explicitObservatoryPort: observatoryPort,
88
      host: _kHosts[serverType],
89
      previewDart2: previewDart2,
90
      port: port,
91
      precompiledDillPath: precompiledDillPath,
92
      trackWidgetCreation: trackWidgetCreation,
93
      updateGoldens: updateGoldens,
94
    ),
95
  );
96 97
}

98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
/// Generates the bootstrap entry point script that will be used to launch an
/// individual test file.
///
/// The [testUrl] argument specifies the path to the test file that is being
/// launched.
///
/// The [host] argument specifies the address at which the test harness is
/// running.
///
/// If [testConfigFile] is specified, it must follow the conventions of test
/// configuration files as outlined in the [flutter_test] library. By default,
/// the test file will be launched directly.
///
/// The [updateGoldens] argument will set the [autoUpdateGoldens] global
/// variable in the [flutter_test] package before invoking the test.
String generateTestBootstrap({
  @required Uri testUrl,
  @required InternetAddress host,
  File testConfigFile,
117
  bool updateGoldens = false,
118 119 120 121 122
}) {
  assert(testUrl != null);
  assert(host != null);
  assert(updateGoldens != null);

123
  final String websocketUrl = host.type == InternetAddressType.IPv4
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
      ? 'ws://${host.address}'
      : 'ws://[${host.address}]';
  final String encodedWebsocketUrl = Uri.encodeComponent(websocketUrl);

  final StringBuffer buffer = new StringBuffer();
  buffer.write('''
import 'dart:convert';
import 'dart:io';  // ignore: dart_io_import

// We import this library first in order to trigger an import error for
// package:test (rather than package:stream_channel) when the developer forgets
// to add a dependency on package:test.
import 'package:test/src/runner/plugin/remote_platform_helpers.dart';

import 'package:flutter_test/flutter_test.dart';
import 'package:stream_channel/stream_channel.dart';
import 'package:test/src/runner/vm/catch_isolate_errors.dart';

import '$testUrl' as test;
'''
  );
  if (testConfigFile != null) {
    buffer.write('''
import '${new Uri.file(testConfigFile.path)}' as test_config;
'''
    );
  }
  buffer.write('''

void main() {
  print('$_kStartTimeoutTimerMessage');
  String serverPort = Platform.environment['SERVER_PORT'];
  String server = Uri.decodeComponent('$encodedWebsocketUrl:\$serverPort');
  StreamChannel channel = serializeSuite(() {
    catchIsolateErrors();
    goldenFileComparator = new LocalFileComparator(Uri.parse('$testUrl'));
    autoUpdateGoldenFiles = $updateGoldens;
'''
  );
  if (testConfigFile != null) {
    buffer.write('''
    return () => test_config.main(test.main);
''');
  } else {
    buffer.write('''
    return test.main;
''');
  }
  buffer.write('''
  });
  WebSocket.connect(server).then((WebSocket socket) {
    socket.map((dynamic x) {
      assert(x is String);
      return json.decode(x);
    }).pipe(channel.sink);
    socket.addStream(channel.stream.map(json.encode));
  });
}
'''
  );
  return buffer.toString();
}

187
enum _InitialResult { crashed, timedOut, connected }
188
enum _TestResult { crashed, harnessBailed, testBailed }
189
typedef Future<Null> _Finalizer();
190

191
class _CompilationRequest {
192 193 194
  String path;
  Completer<String> result;

195
  _CompilationRequest(this.path, this.result);
196 197
}

198 199 200
// This class is a wrapper around compiler that allows multiple isolates to
// enqueue compilation requests, but ensures only one compilation at a time.
class _Compiler {
201
  _Compiler(bool trackWidgetCreation) {
202 203 204 205 206 207 208
    // Compiler maintains and updates single incremental dill file.
    // Incremental compilation requests done for each test copy that file away
    // for independent execution.
    final Directory outputDillDirectory = fs.systemTempDirectory
        .createTempSync('output_dill');
    final File outputDill = outputDillDirectory.childFile('output.dill');

209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
    bool suppressOutput = false;
    void reportCompilerMessage(String message) {
      if (suppressOutput)
        return;

      if (message.startsWith('compiler message: Error: Could not resolve the package \'test\'')) {
        printTrace(message);
        printError('\n\nFailed to load test harness. Are you missing a dependency on flutter_test?\n');
        suppressOutput = true;
        return;
      }

      printError('$message');
    }

    ResidentCompiler createCompiler() {
      return new ResidentCompiler(
        artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath),
        packagesPath: PackageMap.globalPackagesPath,
        trackWidgetCreation: trackWidgetCreation,
        compilerMessageConsumer: reportCompilerMessage,
      );
    }

233
    compilerController.stream.listen((_CompilationRequest request) async {
234 235 236 237 238 239 240
      final bool isEmpty = compilationQueue.isEmpty;
      compilationQueue.add(request);
      // Only trigger processing if queue was empty - i.e. no other requests
      // are currently being processed. This effectively enforces "one
      // compilation request at a time".
      if (isEmpty) {
        while (compilationQueue.isNotEmpty) {
241
          final _CompilationRequest request = compilationQueue.first;
242
          printTrace('Compiling ${request.path}');
243 244
          compiler ??= createCompiler();
          suppressOutput = false;
245 246 247 248 249 250 251
          final CompilerOutput compilerOutput = await handleTimeout<CompilerOutput>(
              compiler.recompile(
                  request.path,
                  <String>[request.path],
                  outputPath: outputDill.path),
              request.path);
          final String outputPath = compilerOutput?.outputFilename;
252

253 254 255 256 257
          // In case compiler didn't produce output or reported compilation
          // errors, pass [null] upwards to the consumer and shutdown the
          // compiler to avoid reusing compiler that might have gotten into
          // a weird state.
          if (outputPath == null || compilerOutput.errorCount > 0) {
258 259 260 261 262 263 264 265 266
            request.result.complete(null);
            await shutdown();
          } else {
            final File kernelReadyToRun =
                await fs.file(outputPath).copy('${request.path}.dill');
            request.result.complete(kernelReadyToRun.path);
            compiler.accept();
            compiler.reset();
          }
267 268 269 270
          // Only remove now when we finished processing the element
          compilationQueue.removeAt(0);
        }
      }
271
    }, onDone: () {
272
      outputDillDirectory.deleteSync(recursive: true);
273 274
    });
  }
275

276 277 278 279 280 281 282 283
  final StreamController<_CompilationRequest> compilerController =
      new StreamController<_CompilationRequest>();
  final List<_CompilationRequest> compilationQueue = <_CompilationRequest>[];
  ResidentCompiler compiler;

  Future<String> compile(String mainDart) {
    final Completer<String> completer = new Completer<String>();
    compilerController.add(new _CompilationRequest(mainDart, completer));
284
    return handleTimeout<String>(completer.future, mainDart);
285
  }
286 287 288 289 290

  Future<dynamic> shutdown() async {
    await compiler.shutdown();
    compiler = null;
  }
291

292
  static Future<T> handleTimeout<T>(Future<T> value, String path) {
293 294 295 296 297
    return value.timeout(const Duration(minutes: 5), onTimeout: () {
      printError('Compilation of $path timed out after 5 minutes.');
      return null;
    });
  }
298 299 300 301 302 303 304 305 306 307 308
}

class _FlutterPlatform extends PlatformPlugin {
  _FlutterPlatform({
    @required this.shellPath,
    this.watcher,
    this.enableObservatory,
    this.machine,
    this.startPaused,
    this.explicitObservatoryPort,
    this.host,
309
    this.previewDart2,
310 311
    this.port,
    this.precompiledDillPath,
312
    this.trackWidgetCreation,
313
    this.updateGoldens,
314 315
  }) : assert(shellPath != null);

316
  final String shellPath;
317 318
  final TestWatcher watcher;
  final bool enableObservatory;
319
  final bool machine;
320
  final bool startPaused;
321
  final int explicitObservatoryPort;
322
  final InternetAddress host;
323
  final bool previewDart2;
324
  final int port;
325
  final String precompiledDillPath;
326
  final bool trackWidgetCreation;
327
  final bool updateGoldens;
328 329

  _Compiler compiler;
330

331 332 333 334 335 336 337
  // Each time loadChannel() is called, we spin up a local WebSocket server,
  // then spin up the engine in a subprocess. We pass the engine a Dart file
  // that connects to our WebSocket server, then we proxy JSON messages from
  // the test harness to the engine and back again. If at any time the engine
  // crashes, we inject an error into that stream. When the process closes,
  // we clean everything up.

338 339
  int _testCount = 0;

340
  @override
341
  StreamChannel<dynamic> loadChannel(String testPath, SuitePlatform platform) {
342 343 344
    if (_testCount > 0) {
      // Fail if there will be a port conflict.
      if (explicitObservatoryPort != null)
345
        throwToolExit('installHook() was called with an observatory port or debugger mode enabled, but then more than one test suite was run.');
346
      // Fail if we're passing in a precompiled entry-point.
347
      if (precompiledDillPath != null)
348
        throwToolExit('installHook() was called with a precompiled test entry-point, but then more than one test suite was run.');
349
    }
350
    final int ourTestCount = _testCount;
351
    _testCount += 1;
352 353 354 355
    final StreamController<dynamic> localController = new StreamController<dynamic>();
    final StreamController<dynamic> remoteController = new StreamController<dynamic>();
    final Completer<Null> testCompleteCompleter = new Completer<Null>();
    final _FlutterPlatformStreamSinkWrapper<dynamic> remoteSink = new _FlutterPlatformStreamSinkWrapper<dynamic>(
356 357 358
      remoteController.sink,
      testCompleteCompleter.future,
    );
359
    final StreamChannel<dynamic> localChannel = new StreamChannel<dynamic>.withGuarantees(
360 361 362
      remoteController.stream,
      localController.sink,
    );
363
    final StreamChannel<dynamic> remoteChannel = new StreamChannel<dynamic>.withGuarantees(
364 365 366
      localController.stream,
      remoteSink,
    );
367
    testCompleteCompleter.complete(_startTest(testPath, localChannel, ourTestCount));
368
    return remoteChannel;
369
  }
370

371 372 373 374
  Future<Null> _startTest(
    String testPath,
    StreamChannel<dynamic> controller,
    int ourTestCount) async {
375
    printTrace('test $ourTestCount: starting test $testPath');
376

377 378
    dynamic outOfBandError; // error that we couldn't send to the harness that we need to send via our future

379
    final List<_Finalizer> finalizers = <_Finalizer>[]; // Will be run in reverse order.
380 381 382
    bool subprocessActive = false;
    bool controllerSinkClosed = false;
    try {
383 384
      // Callback can't throw since it's just setting a variable.
      controller.sink.done.whenComplete(() { controllerSinkClosed = true; }); // ignore: unawaited_futures
385 386

      // Prepare our WebSocket server to talk to the engine subproces.
387
      final HttpServer server = await HttpServer.bind(host, port);
388 389 390
      finalizers.add(() async {
        printTrace('test $ourTestCount: shutting down test harness socket server');
        await server.close(force: true);
391
      });
392
      final Completer<WebSocket> webSocket = new Completer<WebSocket>();
393 394
      server.listen(
        (HttpRequest request) {
395 396
          if (!webSocket.isCompleted)
            webSocket.complete(WebSocketTransformer.upgrade(request));
397 398 399 400 401 402 403 404 405 406 407 408 409
        },
        onError: (dynamic error, dynamic stack) {
          // If you reach here, it's unlikely we're going to be able to really handle this well.
          printTrace('test $ourTestCount: test harness socket server experienced an unexpected error: $error');
          if (!controllerSinkClosed) {
            controller.sink.addError(error, stack);
            controller.sink.close();
          } else {
            printError('unexpected error from test harness socket server: $error');
          }
        },
        cancelOnError: true,
      );
410

411
      printTrace('test $ourTestCount: starting shell process${previewDart2? " in preview-dart-2 mode":""}');
412

413 414
      // [precompiledDillPath] can be set only if [previewDart2] is [true].
      assert(precompiledDillPath == null || previewDart2);
415 416 417 418 419 420
      // If a kernel file is given, then use that to launch the test.
      // Otherwise create a "listener" dart that invokes actual test.
      String mainDart = precompiledDillPath != null
          ? precompiledDillPath
          : _createListenerDart(finalizers, ourTestCount, testPath, server);

421
      if (previewDart2 && precompiledDillPath == null) {
422
        // Lazily instantiate compiler so it is built only if it is actually used.
423
        compiler ??= new _Compiler(trackWidgetCreation);
424 425 426 427 428 429
        mainDart = await compiler.compile(mainDart);

        if (mainDart == null) {
          controller.sink.addError(
              _getErrorMessage('Compilation failed', testPath, shellPath));
          return null;
430
        }
431
      }
432

433
      final Process process = await _startProcess(
434
        shellPath,
435
        mainDart,
436
        packages: PackageMap.globalPackagesPath,
437 438
        enableObservatory: enableObservatory,
        startPaused: startPaused,
439
        bundlePath: _getBundlePath(finalizers, ourTestCount),
440
        observatoryPort: explicitObservatoryPort,
441
        serverPort: server.port,
442 443 444
      );
      subprocessActive = true;
      finalizers.add(() async {
445 446
        if (subprocessActive) {
          printTrace('test $ourTestCount: ensuring end-of-process for shell');
447
          process.kill();
448 449
          final int exitCode = await process.exitCode;
          subprocessActive = false;
450 451 452
          if (!controllerSinkClosed && exitCode != -15) { // ProcessSignal.SIGTERM
            // We expect SIGTERM (15) because we tried to terminate it.
            // It's negative because signals are returned as negative exit codes.
453
            final String message = _getErrorMessage(_getExitCodeMessage(exitCode, 'after tests finished'), testPath, shellPath);
454 455
            controller.sink.addError(message);
          }
456 457
        }
      });
458

459
      final Completer<Null> timeout = new Completer<Null>();
460

461
      // Pipe stdout and stderr from the subprocess to our printStatus console.
462
      // We also keep track of what observatory port the engine used, if any.
463
      Uri processObservatoryUri;
464

465 466
      _pipeStandardStreamsToConsole(
        process,
467 468
        reportObservatoryUri: (Uri detectedUri) {
          assert(processObservatoryUri == null);
469
          assert(explicitObservatoryPort == null ||
470
                 explicitObservatoryPort == detectedUri.port);
471
          if (startPaused && !machine) {
472 473
            printStatus('The test process has been started.');
            printStatus('You can now connect to it using observatory. To connect, load the following Web site in your browser:');
474
            printStatus('  $detectedUri');
475 476
            printStatus('You should first set appropriate breakpoints, then resume the test in the debugger.');
          } else {
477 478 479 480
            printTrace('test $ourTestCount: using observatory uri $detectedUri from pid ${process.pid}');
          }
          if (watcher != null) {
            watcher.onStartedProcess(new ProcessEvent(ourTestCount, process, detectedUri));
481
          }
482
          processObservatoryUri = detectedUri;
483 484
        },
        startTimeoutTimer: () {
485
          new Future<_InitialResult>.delayed(_kTestStartupTimeout).then((_) => timeout.complete());
486 487
        },
      );
488

489 490 491 492 493
      // At this point, three things can happen next:
      // The engine could crash, in which case process.exitCode will complete.
      // The engine could connect to us, in which case webSocket.future will complete.
      // The local test harness could get bored of us.

494
      printTrace('test $ourTestCount: awaiting initial result for pid ${process.pid}');
495
      final _InitialResult initialResult = await Future.any(<Future<_InitialResult>>[
496 497 498 499
        process.exitCode.then<_InitialResult>((int exitCode) => _InitialResult.crashed),
        timeout.future.then<_InitialResult>((Null _) => _InitialResult.timedOut),
        new Future<_InitialResult>.delayed(_kTestProcessTimeout, () => _InitialResult.timedOut),
        webSocket.future.then<_InitialResult>((WebSocket webSocket) => _InitialResult.connected),
500 501 502 503
      ]);

      switch (initialResult) {
        case _InitialResult.crashed:
504
          printTrace('test $ourTestCount: process with pid ${process.pid} crashed before connecting to test harness');
505
          final int exitCode = await process.exitCode;
506
          subprocessActive = false;
507
          final String message = _getErrorMessage(_getExitCodeMessage(exitCode, 'before connecting to test harness'), testPath, shellPath);
508
          controller.sink.addError(message);
509 510
          // Awaited for with 'sink.done' below.
          controller.sink.close(); // ignore: unawaited_futures
511
          printTrace('test $ourTestCount: waiting for controller sink to close');
512 513 514
          await controller.sink.done;
          break;
        case _InitialResult.timedOut:
515 516 517
          // Could happen either if the process takes a long time starting
          // (_kTestProcessTimeout), or if once Dart code starts running, it takes a
          // long time to open the WebSocket connection (_kTestStartupTimeout).
518
          printTrace('test $ourTestCount: timed out waiting for process with pid ${process.pid} to connect to test harness');
519
          final String message = _getErrorMessage('Test never connected to test harness.', testPath, shellPath);
520
          controller.sink.addError(message);
521 522
          // Awaited for with 'sink.done' below.
          controller.sink.close(); // ignore: unawaited_futures
523
          printTrace('test $ourTestCount: waiting for controller sink to close');
524 525 526
          await controller.sink.done;
          break;
        case _InitialResult.connected:
527
          printTrace('test $ourTestCount: process with pid ${process.pid} connected to test harness');
528
          final WebSocket testSocket = await webSocket.future;
529

530 531
          final Completer<Null> harnessDone = new Completer<Null>();
          final StreamSubscription<dynamic> harnessToTest = controller.stream.listen(
532
            (dynamic event) { testSocket.add(json.encode(event)); },
533
            onDone: harnessDone.complete,
534 535 536 537 538 539 540 541 542 543 544
            onError: (dynamic error, dynamic stack) {
              // If you reach here, it's unlikely we're going to be able to really handle this well.
              printError('test harness controller stream experienced an unexpected error\ntest: $testPath\nerror: $error');
              if (!controllerSinkClosed) {
                controller.sink.addError(error, stack);
                controller.sink.close();
              } else {
                printError('unexpected error from test harness controller stream: $error');
              }
            },
            cancelOnError: true,
545 546
          );

547 548
          final Completer<Null> testDone = new Completer<Null>();
          final StreamSubscription<dynamic> testToHarness = testSocket.listen(
549 550
            (dynamic encodedEvent) {
              assert(encodedEvent is String); // we shouldn't ever get binary messages
551
              controller.sink.add(json.decode(encodedEvent));
552
            },
553
            onDone: testDone.complete,
554 555 556 557 558 559 560 561 562 563 564
            onError: (dynamic error, dynamic stack) {
              // If you reach here, it's unlikely we're going to be able to really handle this well.
              printError('test socket stream experienced an unexpected error\ntest: $testPath\nerror: $error');
              if (!controllerSinkClosed) {
                controller.sink.addError(error, stack);
                controller.sink.close();
              } else {
                printError('unexpected error from test socket stream: $error');
              }
            },
            cancelOnError: true,
565 566
          );

567
          printTrace('test $ourTestCount: awaiting test result for pid ${process.pid}');
568
          final _TestResult testResult = await Future.any(<Future<_TestResult>>[
569 570
            process.exitCode.then<_TestResult>((int exitCode) { return _TestResult.crashed; }),
            harnessDone.future.then<_TestResult>((Null _) { return _TestResult.harnessBailed; }),
571
            testDone.future.then<_TestResult>((Null _) { return _TestResult.testBailed; }),
572 573
          ]);

574 575 576 577
          await Future.wait(<Future<Null>>[
            harnessToTest.cancel(),
            testToHarness.cancel(),
          ]);
578 579 580

          switch (testResult) {
            case _TestResult.crashed:
581
              printTrace('test $ourTestCount: process with pid ${process.pid} crashed');
582
              final int exitCode = await process.exitCode;
583
              subprocessActive = false;
584
              final String message = _getErrorMessage(_getExitCodeMessage(exitCode, 'before test harness closed its WebSocket'), testPath, shellPath);
585
              controller.sink.addError(message);
586 587
              // Awaited for with 'sink.done' below.
              controller.sink.close(); // ignore: unawaited_futures
588
              printTrace('test $ourTestCount: waiting for controller sink to close');
589 590 591
              await controller.sink.done;
              break;
            case _TestResult.harnessBailed:
592 593 594 595
              printTrace('test $ourTestCount: process with pid ${process.pid} no longer needed by test harness');
              break;
            case _TestResult.testBailed:
              printTrace('test $ourTestCount: process with pid ${process.pid} no longer needs test harness');
596 597 598 599 600
              break;
          }
          break;
      }

601 602 603 604 605 606 607 608 609 610 611 612 613 614 615
      if (watcher != null) {
        switch (initialResult) {
          case _InitialResult.crashed:
            await watcher.onTestCrashed(new ProcessEvent(ourTestCount, process));
            break;
          case _InitialResult.timedOut:
            await watcher.onTestTimedOut(new ProcessEvent(ourTestCount, process));
            break;
          case _InitialResult.connected:
            if (subprocessActive) {
              await watcher.onFinishedTest(
                  new ProcessEvent(ourTestCount, process, processObservatoryUri));
            }
            break;
        }
616 617 618
      }
    } catch (error, stack) {
      printTrace('test $ourTestCount: error caught during test; ${controllerSinkClosed ? "reporting to console" : "sending to test framework"}');
619
      if (!controllerSinkClosed) {
620
        controller.sink.addError(error, stack);
621
      } else {
622
        printError('unhandled error during test:\n$testPath\n$error\n$stack');
623
        outOfBandError ??= error;
624 625
      }
    } finally {
626
      printTrace('test $ourTestCount: cleaning up...');
627 628
      // Finalizers are treated like a stack; run them in reverse order.
      for (_Finalizer finalizer in finalizers.reversed) {
629 630 631 632 633 634 635
        try {
          await finalizer();
        } catch (error, stack) {
          printTrace('test $ourTestCount: error while cleaning up; ${controllerSinkClosed ? "reporting to console" : "sending to test framework"}');
          if (!controllerSinkClosed) {
            controller.sink.addError(error, stack);
          } else {
636
            printError('unhandled error during finalization of test:\n$testPath\n$error\n$stack');
637
            outOfBandError ??= error;
638 639 640
          }
        }
      }
641
      if (!controllerSinkClosed) {
642 643
        // Waiting below with await.
        controller.sink.close(); // ignore: unawaited_futures
644
        printTrace('test $ourTestCount: waiting for controller sink to close');
645 646 647 648 649
        await controller.sink.done;
      }
    }
    assert(!subprocessActive);
    assert(controllerSinkClosed);
650 651 652 653
    if (outOfBandError != null) {
      printTrace('test $ourTestCount: finished with out-of-band failure');
      throw outOfBandError;
    }
654 655
    printTrace('test $ourTestCount: finished');
    return null;
656 657
  }

658 659 660 661 662 663 664 665 666 667 668 669 670 671
  String _createListenerDart(List<_Finalizer> finalizers, int ourTestCount,
      String testPath, HttpServer server) {
    // Prepare a temporary directory to store the Dart file that will talk to us.
    final Directory temporaryDirectory = fs.systemTempDirectory
        .createTempSync('dart_test_listener');
    finalizers.add(() async {
      printTrace('test $ourTestCount: deleting temporary directory');
      temporaryDirectory.deleteSync(recursive: true);
    });

    // Prepare the Dart file that will talk to us and start the test.
    final File listenerFile = fs.file('${temporaryDirectory.path}/listener.dart');
    listenerFile.createSync();
    listenerFile.writeAsStringSync(_generateTestMain(
672
      testUrl: fs.path.toUri(fs.path.absolute(testPath)),
673 674 675 676 677
    ));
    return listenerFile.path;
  }

  String _getBundlePath(List<_Finalizer> finalizers, int ourTestCount) {
678 679 680 681
    if (!previewDart2) {
      return null;
    }

682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698
    if (precompiledDillPath != null) {
      return artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath);
    }

    // bundlePath needs to point to a folder with `platform.dill` file.
    final Directory tempBundleDirectory = fs.systemTempDirectory
        .createTempSync('flutter_bundle_directory');
    finalizers.add(() async {
      printTrace(
          'test $ourTestCount: deleting temporary bundle directory');
      tempBundleDirectory.deleteSync(recursive: true);
    });

    // copy 'vm_platform_strong.dill' into 'platform.dill'
    final File vmPlatformStrongDill = fs.file(
      artifacts.getArtifactPath(Artifact.platformKernelDill),
    );
699
    printTrace('Copying platform.dill file from ${vmPlatformStrongDill.path}');
700 701 702 703 704 705 706 707 708 709 710 711
    final File platformDill = vmPlatformStrongDill.copySync(
      tempBundleDirectory
          .childFile('platform.dill')
          .path,
    );
    if (!platformDill.existsSync()) {
      printError('unexpected error copying platform kernel file');
    }

    return tempBundleDirectory.path;
  }

712
  String _generateTestMain({
713
    Uri testUrl,
714
  }) {
715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731
    assert(testUrl.scheme == 'file');
    File testConfigFile;
    Directory directory = fs.file(testUrl).parent;
    while (directory.path != directory.parent.path) {
      final File configFile = directory.childFile(_kTestConfigFileName);
      if (configFile.existsSync()) {
        printTrace('Discovered $_kTestConfigFileName in ${directory.path}');
        testConfigFile = configFile;
        break;
      }
      if (directory.childFile(_kProjectRootSentinel).existsSync()) {
        printTrace('Stopping scan for $_kTestConfigFileName; '
                   'found project root at ${directory.path}');
        break;
      }
      directory = directory.parent;
    }
732 733 734 735 736
    return generateTestBootstrap(
      testUrl: testUrl,
      testConfigFile: testConfigFile,
      host: host,
      updateGoldens: updateGoldens,
737
    );
738 739 740 741
  }

  File _cachedFontConfig;

742 743 744 745 746 747 748 749
  @override
  Future<dynamic> close() async {
    if (compiler != null) {
      await compiler.shutdown();
      compiler = null;
    }
  }

750 751 752 753 754
  /// Returns a Fontconfig config file that limits font fallback to the
  /// artifact cache directory.
  File get _fontConfigFile {
    if (_cachedFontConfig != null)
      return _cachedFontConfig;
755

756
    final StringBuffer sb = new StringBuffer();
757 758 759 760 761
    sb.writeln('<fontconfig>');
    sb.writeln('  <dir>${cache.getCacheArtifacts().path}</dir>');
    sb.writeln('  <cachedir>/var/cache/fontconfig</cachedir>');
    sb.writeln('</fontconfig>');

762
    final Directory fontsDir = fs.systemTempDirectory.createTempSync('flutter_fonts');
763
    _cachedFontConfig = fs.file('${fontsDir.path}/fonts.conf');
764 765 766 767 768
    _cachedFontConfig.createSync();
    _cachedFontConfig.writeAsStringSync(sb.toString());
    return _cachedFontConfig;
  }

769 770 771 772
  Future<Process> _startProcess(
    String executable,
    String testPath, {
    String packages,
773
    String bundlePath,
774 775
    bool enableObservatory = false,
    bool startPaused = false,
776
    int observatoryPort,
777
    int serverPort,
778
  }) {
779
    assert(executable != null); // Please provide the path to the shell in the SKY_SHELL environment variable.
780
    assert(!startPaused || enableObservatory);
781
    final List<String> command = <String>[executable];
782
    if (enableObservatory) {
783
      // Some systems drive the _FlutterPlatform class in an unusual way, where
784 785 786 787
      // only one test file is processed at a time, and the operating
      // environment hands out specific ports ahead of time in a cooperative
      // manner, where we're only allowed to open ports that were given to us in
      // advance like this. For those esoteric systems, we have this feature
788
      // whereby you can create _FlutterPlatform with a pair of ports.
789 790 791 792
      //
      // I mention this only so that you won't be tempted, as I was, to apply
      // the obvious simplification to this code and remove this entire feature.
      if (observatoryPort != null)
793
        command.add('--observatory-port=$observatoryPort');
794
      if (startPaused)
795
        command.add('--start-paused');
796
    } else {
797
      command.add('--disable-observatory');
798
    }
799
    if (host.type == InternetAddressType.IPv6)
800
      command.add('--ipv6');
801 802 803
    if (bundlePath != null) {
      command.add('--flutter-assets-dir=$bundlePath');
    }
804
    command.add('--enable-checked-mode');
805
    command.addAll(<String>[
806 807
      '--enable-software-rendering',
      '--skia-deterministic-rendering',
808 809
      '--enable-dart-profiling',
      '--non-interactive',
810
      '--use-test-fonts',
811 812 813
      '--packages=$packages',
      testPath,
    ]);
814
    printTrace(command.join(' '));
815
    final Map<String, String> environment = <String, String>{
816 817
      'FLUTTER_TEST': 'true',
      'FONTCONFIG_FILE': _fontConfigFile.path,
818
      'SERVER_PORT': serverPort.toString(),
819
    };
820
    return processManager.start(command, environment: environment);
821
  }
822

823 824 825
  void _pipeStandardStreamsToConsole(
    Process process, {
    void startTimeoutTimer(),
826
    void reportObservatoryUri(Uri uri),
827
  }) {
828
    const String observatoryString = 'Observatory listening on ';
829

830 831
    for (Stream<List<int>> stream in
        <Stream<List<int>>>[process.stderr, process.stdout]) {
832
      stream.transform(utf8.decoder)
833
        .transform(const LineSplitter())
834 835 836 837 838 839 840 841
        .listen(
          (String line) {
            if (line == _kStartTimeoutTimerMessage) {
              if (startTimeoutTimer != null)
                startTimeoutTimer();
            } else if (line.startsWith('error: Unable to read Dart source \'package:test/')) {
              printTrace('Shell: $line');
              printError('\n\nFailed to load test harness. Are you missing a dependency on flutter_test?\n');
842
            } else if (line.startsWith(observatoryString)) {
843 844
              printTrace('Shell: $line');
              try {
845 846 847
                final Uri uri = Uri.parse(line.substring(observatoryString.length));
                if (reportObservatoryUri != null)
                  reportObservatoryUri(uri);
848 849 850 851 852 853 854 855 856 857 858 859
              } catch (error) {
                printError('Could not parse shell observatory port message: $error');
              }
            } else if (line != null) {
              printStatus('Shell: $line');
            }
          },
          onError: (dynamic error) {
            printError('shell console stream for process pid ${process.pid} experienced an unexpected error: $error');
          },
          cancelOnError: true,
        );
860
    }
861
  }
862

863 864 865
  String _getErrorMessage(String what, String testPath, String shellPath) {
    return '$what\nTest: $testPath\nShell: $shellPath\n\n';
  }
866

867 868
  String _getExitCodeMessage(int exitCode, String when) {
    switch (exitCode) {
869 870
      case 1:
        return 'Shell subprocess cleanly reported an error $when. Check the logs above for an error message.';
871 872
      case 0:
        return 'Shell subprocess ended cleanly $when. Did main() call exit()?';
873
      case -0x0f: // ProcessSignal.SIGTERM
874
        return 'Shell subprocess crashed with SIGTERM ($exitCode) $when.';
875
      case -0x0b: // ProcessSignal.SIGSEGV
876
        return 'Shell subprocess crashed with segmentation fault $when.';
877
      case -0x06: // ProcessSignal.SIGABRT
878
        return 'Shell subprocess crashed with SIGABRT ($exitCode) $when.';
879
      case -0x02: // ProcessSignal.SIGINT
880
        return 'Shell subprocess terminated by ^C (SIGINT, $exitCode) $when.';
881 882
      default:
        return 'Shell subprocess crashed with unexpected exit code $exitCode $when.';
883 884 885
    }
  }
}
886 887 888 889 890 891 892 893 894 895 896 897

class _FlutterPlatformStreamSinkWrapper<S> implements StreamSink<S> {
  _FlutterPlatformStreamSinkWrapper(this._parent, this._shellProcessClosed);
  final StreamSink<S> _parent;
  final Future<Null> _shellProcessClosed;

  @override
  Future<Null> get done => _done.future;
  final Completer<Null> _done = new Completer<Null>();

  @override
  Future<dynamic> close() {
898
   Future.wait<dynamic>(<Future<dynamic>>[
899 900
      _parent.close(),
      _shellProcessClosed,
901
    ]).then<Null>(
902 903 904
      (List<dynamic> value) {
        _done.complete();
      },
905
      onError: _done.completeError,
906 907 908 909 910 911 912 913 914 915 916
    );
    return done;
  }

  @override
  void add(S event) => _parent.add(event);
  @override
  void addError(dynamic errorEvent, [ StackTrace stackTrace ]) => _parent.addError(errorEvent, stackTrace);
  @override
  Future<dynamic> addStream(Stream<S> stream) => _parent.addStream(stream);
}