• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2015 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:meta/meta.dart';
8import 'package:stream_channel/stream_channel.dart';
9
10import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
11import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports
12import 'package:test_core/src/runner/platform.dart'; // ignore: implementation_imports
13import 'package:test_core/src/runner/hack_register_platform.dart' as hack; // ignore: implementation_imports
14import 'package:test_core/src/runner/runner_suite.dart'; // ignore: implementation_imports
15import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports
16import 'package:test_core/src/runner/plugin/platform_helpers.dart'; // ignore: implementation_imports
17import 'package:test_core/src/runner/environment.dart'; // ignore: implementation_imports
18
19import '../base/common.dart';
20import '../base/file_system.dart';
21import '../base/io.dart';
22import '../base/platform.dart';
23import '../base/process_manager.dart';
24import '../compile.dart';
25import '../convert.dart';
26import '../dart/package_map.dart';
27import '../globals.dart';
28import '../project.dart';
29import '../vmservice.dart';
30import 'test_compiler.dart';
31import 'watcher.dart';
32
33/// The timeout we give the test process to connect to the test harness
34/// once the process has entered its main method.
35///
36/// We time out test execution because we expect some tests to hang and we want
37/// to know which test hung, rather than have the entire test harness just do
38/// nothing for a few hours until the user (or CI environment) gets bored.
39const Duration _kTestStartupTimeout = Duration(minutes: 5);
40
41/// The timeout we give the test process to start executing Dart code. When the
42/// CPU is under severe load, this can take a while, but it's not indicative of
43/// any problem with Flutter, so we give it a large timeout.
44///
45/// See comment under [_kTestStartupTimeout] regarding timeouts.
46const Duration _kTestProcessTimeout = Duration(minutes: 5);
47
48/// Message logged by the test process to signal that its main method has begun
49/// execution.
50///
51/// The test harness responds by starting the [_kTestStartupTimeout] countdown.
52/// The CPU may be throttled, which can cause a long delay in between when the
53/// process is spawned and when dart code execution begins; we don't want to
54/// hold that against the test.
55const String _kStartTimeoutTimerMessage = 'sky_shell test process has entered main method';
56
57/// The name of the test configuration file that will be discovered by the
58/// test harness if it exists in the project directory hierarchy.
59const String _kTestConfigFileName = 'flutter_test_config.dart';
60
61/// The name of the file that signals the root of the project and that will
62/// cause the test harness to stop scanning for configuration files.
63const String _kProjectRootSentinel = 'pubspec.yaml';
64
65/// The address at which our WebSocket server resides and at which the sky_shell
66/// processes will host the Observatory server.
67final Map<InternetAddressType, InternetAddress> _kHosts = <InternetAddressType, InternetAddress>{
68  InternetAddressType.IPv4: InternetAddress.loopbackIPv4,
69  InternetAddressType.IPv6: InternetAddress.loopbackIPv6,
70};
71
72typedef PlatformPluginRegistration = void Function(FlutterPlatform platform);
73
74/// Configure the `test` package to work with Flutter.
75///
76/// On systems where each [FlutterPlatform] is only used to run one test suite
77/// (that is, one Dart file with a `*_test.dart` file name and a single `void
78/// main()`), you can set an observatory port explicitly.
79FlutterPlatform installHook({
80  @required String shellPath,
81  TestWatcher watcher,
82  bool enableObservatory = false,
83  bool machine = false,
84  bool startPaused = false,
85  bool disableServiceAuthCodes = false,
86  int port = 0,
87  String precompiledDillPath,
88  Map<String, String> precompiledDillFiles,
89  bool trackWidgetCreation = false,
90  bool updateGoldens = false,
91  bool buildTestAssets = false,
92  int observatoryPort,
93  InternetAddressType serverType = InternetAddressType.IPv4,
94  Uri projectRootDirectory,
95  FlutterProject flutterProject,
96  String icudtlPath,
97  PlatformPluginRegistration platformPluginRegistration
98}) {
99  assert(enableObservatory || (!startPaused && observatoryPort == null));
100
101  // registerPlatformPlugin can be injected for testing since it's not very mock-friendly.
102  platformPluginRegistration ??= (FlutterPlatform platform) {
103    hack.registerPlatformPlugin(
104      <Runtime>[Runtime.vm],
105        () {
106        return platform;
107      }
108    );
109  };
110  final FlutterPlatform platform = FlutterPlatform(
111    shellPath: shellPath,
112    watcher: watcher,
113    machine: machine,
114    enableObservatory: enableObservatory,
115    startPaused: startPaused,
116    disableServiceAuthCodes: disableServiceAuthCodes,
117    explicitObservatoryPort: observatoryPort,
118    host: _kHosts[serverType],
119    port: port,
120    precompiledDillPath: precompiledDillPath,
121    precompiledDillFiles: precompiledDillFiles,
122    trackWidgetCreation: trackWidgetCreation,
123    updateGoldens: updateGoldens,
124    buildTestAssets: buildTestAssets,
125    projectRootDirectory: projectRootDirectory,
126    flutterProject: flutterProject,
127    icudtlPath: icudtlPath,
128  );
129  platformPluginRegistration(platform);
130  return platform;
131}
132
133/// Generates the bootstrap entry point script that will be used to launch an
134/// individual test file.
135///
136/// The [testUrl] argument specifies the path to the test file that is being
137/// launched.
138///
139/// The [host] argument specifies the address at which the test harness is
140/// running.
141///
142/// If [testConfigFile] is specified, it must follow the conventions of test
143/// configuration files as outlined in the [flutter_test] library. By default,
144/// the test file will be launched directly.
145///
146/// The [updateGoldens] argument will set the [autoUpdateGoldens] global
147/// variable in the [flutter_test] package before invoking the test.
148String generateTestBootstrap({
149  @required Uri testUrl,
150  @required InternetAddress host,
151  File testConfigFile,
152  bool updateGoldens = false,
153}) {
154  assert(testUrl != null);
155  assert(host != null);
156  assert(updateGoldens != null);
157
158  final String websocketUrl = host.type == InternetAddressType.IPv4
159      ? 'ws://${host.address}'
160      : 'ws://[${host.address}]';
161  final String encodedWebsocketUrl = Uri.encodeComponent(websocketUrl);
162
163  final StringBuffer buffer = StringBuffer();
164  buffer.write('''
165import 'dart:async';
166import 'dart:convert';  // ignore: dart_convert_import
167import 'dart:io';  // ignore: dart_io_import
168import 'dart:isolate';
169
170import 'package:flutter_test/flutter_test.dart';
171import 'package:test_api/src/remote_listener.dart';
172import 'package:stream_channel/stream_channel.dart';
173import 'package:stack_trace/stack_trace.dart';
174
175import '$testUrl' as test;
176''');
177  if (testConfigFile != null) {
178    buffer.write('''
179import '${Uri.file(testConfigFile.path)}' as test_config;
180''');
181  }
182  buffer.write('''
183
184/// Returns a serialized test suite.
185StreamChannel<dynamic> serializeSuite(Function getMain(),
186    {bool hidePrints = true, Future<dynamic> beforeLoad()}) {
187  return RemoteListener.start(getMain,
188      hidePrints: hidePrints, beforeLoad: beforeLoad);
189}
190
191/// Capture any top-level errors (mostly lazy syntax errors, since other are
192/// caught below) and report them to the parent isolate.
193void catchIsolateErrors() {
194  final ReceivePort errorPort = ReceivePort();
195  // Treat errors non-fatal because otherwise they'll be double-printed.
196  Isolate.current.setErrorsFatal(false);
197  Isolate.current.addErrorListener(errorPort.sendPort);
198  errorPort.listen((dynamic message) {
199    // Masquerade as an IsolateSpawnException because that's what this would
200    // be if the error had been detected statically.
201    final IsolateSpawnException error = IsolateSpawnException(message[0]);
202    final Trace stackTrace =
203        message[1] == null ? Trace(const <Frame>[]) : Trace.parse(message[1]);
204    Zone.current.handleUncaughtError(error, stackTrace);
205  });
206}
207
208
209void main() {
210  print('$_kStartTimeoutTimerMessage');
211  String serverPort = Platform.environment['SERVER_PORT'];
212  String server = Uri.decodeComponent('$encodedWebsocketUrl:\$serverPort');
213  StreamChannel<dynamic> channel = serializeSuite(() {
214    catchIsolateErrors();
215    goldenFileComparator = new LocalFileComparator(Uri.parse('$testUrl'));
216    autoUpdateGoldenFiles = $updateGoldens;
217''');
218  if (testConfigFile != null) {
219    buffer.write('''
220    return () => test_config.main(test.main);
221''');
222  } else {
223    buffer.write('''
224    return test.main;
225''');
226  }
227  buffer.write('''
228  });
229  WebSocket.connect(server).then((WebSocket socket) {
230    socket.map((dynamic x) {
231      assert(x is String);
232      return json.decode(x);
233    }).pipe(channel.sink);
234    socket.addStream(channel.stream.map(json.encode));
235  });
236}
237''');
238  return buffer.toString();
239}
240
241enum InitialResult { crashed, timedOut, connected }
242
243enum TestResult { crashed, harnessBailed, testBailed }
244
245typedef Finalizer = Future<void> Function();
246
247/// The flutter test platform used to integrate with package:test.
248class FlutterPlatform extends PlatformPlugin {
249  FlutterPlatform({
250    @required this.shellPath,
251    this.watcher,
252    this.enableObservatory,
253    this.machine,
254    this.startPaused,
255    this.disableServiceAuthCodes,
256    this.explicitObservatoryPort,
257    this.host,
258    this.port,
259    this.precompiledDillPath,
260    this.precompiledDillFiles,
261    this.trackWidgetCreation,
262    this.updateGoldens,
263    this.buildTestAssets,
264    this.projectRootDirectory,
265    this.flutterProject,
266    this.icudtlPath,
267  }) : assert(shellPath != null);
268
269  final String shellPath;
270  final TestWatcher watcher;
271  final bool enableObservatory;
272  final bool machine;
273  final bool startPaused;
274  final bool disableServiceAuthCodes;
275  final int explicitObservatoryPort;
276  final InternetAddress host;
277  final int port;
278  final String precompiledDillPath;
279  final Map<String, String> precompiledDillFiles;
280  final bool trackWidgetCreation;
281  final bool updateGoldens;
282  final bool buildTestAssets;
283  final Uri projectRootDirectory;
284  final FlutterProject flutterProject;
285  final String icudtlPath;
286
287  Directory fontsDirectory;
288
289  /// The test compiler produces dill files for each test main.
290  ///
291  /// To speed up compilation, each compile is intialized from an existing
292  /// dill file from previous runs, if possible.
293  TestCompiler compiler;
294
295  // Each time loadChannel() is called, we spin up a local WebSocket server,
296  // then spin up the engine in a subprocess. We pass the engine a Dart file
297  // that connects to our WebSocket server, then we proxy JSON messages from
298  // the test harness to the engine and back again. If at any time the engine
299  // crashes, we inject an error into that stream. When the process closes,
300  // we clean everything up.
301
302  int _testCount = 0;
303
304  @override
305  Future<RunnerSuite> load(
306    String path,
307    SuitePlatform platform,
308    SuiteConfiguration suiteConfig,
309    Object message,
310  ) async {
311    // loadChannel may throw an exception. That's fine; it will cause the
312    // LoadSuite to emit an error, which will be presented to the user.
313    // Except for the Declarer error, which is a specific test incompatibility
314    // error we need to catch.
315    try {
316      final StreamChannel<dynamic> channel = loadChannel(path, platform);
317      final RunnerSuiteController controller = deserializeSuite(path, platform,
318        suiteConfig, const PluginEnvironment(), channel, message);
319      return await controller.suite;
320    } catch (err) {
321      /// Rethrow a less confusing error if it is a test incompatibility.
322      if (err.toString().contains('type \'Declarer\' is not a subtype of type \'Declarer\'')) {
323        throw UnsupportedError('Package incompatibility between flutter and test packages:\n'
324          '  * flutter is incompatible with test <1.4.0.\n'
325          '  * flutter is incompatible with mockito <4.0.0\n'
326          'To fix this error, update test to at least \'^1.4.0\' and mockito to at least \'^4.0.0\'\n'
327        );
328      }
329      // Guess it was a different error.
330      rethrow;
331    }
332  }
333
334  @override
335  StreamChannel<dynamic> loadChannel(String path, SuitePlatform platform) {
336    if (_testCount > 0) {
337      // Fail if there will be a port conflict.
338      if (explicitObservatoryPort != null) {
339        throwToolExit('installHook() was called with an observatory port or debugger mode enabled, but then more than one test suite was run.');
340      }
341      // Fail if we're passing in a precompiled entry-point.
342      if (precompiledDillPath != null) {
343        throwToolExit('installHook() was called with a precompiled test entry-point, but then more than one test suite was run.');
344      }
345    }
346    final int ourTestCount = _testCount;
347    _testCount += 1;
348    final StreamController<dynamic> localController = StreamController<dynamic>();
349    final StreamController<dynamic> remoteController = StreamController<dynamic>();
350    final Completer<_AsyncError> testCompleteCompleter = Completer<_AsyncError>();
351    final _FlutterPlatformStreamSinkWrapper<dynamic> remoteSink = _FlutterPlatformStreamSinkWrapper<dynamic>(
352      remoteController.sink,
353      testCompleteCompleter.future,
354    );
355    final StreamChannel<dynamic> localChannel = StreamChannel<dynamic>.withGuarantees(
356      remoteController.stream,
357      localController.sink,
358    );
359    final StreamChannel<dynamic> remoteChannel = StreamChannel<dynamic>.withGuarantees(
360      localController.stream,
361      remoteSink,
362    );
363    testCompleteCompleter.complete(_startTest(path, localChannel, ourTestCount));
364    return remoteChannel;
365  }
366
367  Future<String> _compileExpressionService(
368    String isolateId,
369    String expression,
370    List<String> definitions,
371    List<String> typeDefinitions,
372    String libraryUri,
373    String klass,
374    bool isStatic,
375  ) async {
376    if (compiler == null || compiler.compiler == null) {
377      throw 'Compiler is not set up properly to compile $expression';
378    }
379    final CompilerOutput compilerOutput =
380      await compiler.compiler.compileExpression(expression, definitions,
381        typeDefinitions, libraryUri, klass, isStatic);
382    if (compilerOutput != null && compilerOutput.outputFilename != null) {
383      return base64.encode(fs.file(compilerOutput.outputFilename).readAsBytesSync());
384    }
385    throw 'Failed to compile $expression';
386  }
387
388  /// Binds an [HttpServer] serving from `host` on `port`.
389  ///
390  /// Only intended to be overridden in tests for [FlutterPlatform].
391  @protected
392  @visibleForTesting
393  Future<HttpServer> bind(InternetAddress host, int port) => HttpServer.bind(host, port);
394
395  Future<_AsyncError> _startTest(
396    String testPath,
397    StreamChannel<dynamic> controller,
398    int ourTestCount,
399  ) async {
400    printTrace('test $ourTestCount: starting test $testPath');
401
402    _AsyncError outOfBandError; // error that we couldn't send to the harness that we need to send via our future
403
404    final List<Finalizer> finalizers = <Finalizer>[]; // Will be run in reverse order.
405    bool subprocessActive = false;
406    bool controllerSinkClosed = false;
407    try {
408      // Callback can't throw since it's just setting a variable.
409      unawaited(controller.sink.done.whenComplete(() {
410        controllerSinkClosed = true;
411      }));
412
413      // Prepare our WebSocket server to talk to the engine subproces.
414      final HttpServer server = await bind(host, port);
415      finalizers.add(() async {
416        printTrace('test $ourTestCount: shutting down test harness socket server');
417        await server.close(force: true);
418      });
419      final Completer<WebSocket> webSocket = Completer<WebSocket>();
420      server.listen((HttpRequest request) {
421        if (!webSocket.isCompleted)
422          webSocket.complete(WebSocketTransformer.upgrade(request));
423        },
424        onError: (dynamic error, dynamic stack) {
425          // If you reach here, it's unlikely we're going to be able to really handle this well.
426          printTrace('test $ourTestCount: test harness socket server experienced an unexpected error: $error');
427          if (!controllerSinkClosed) {
428            controller.sink.addError(error, stack);
429            controller.sink.close();
430          } else {
431            printError('unexpected error from test harness socket server: $error');
432          }
433        },
434        cancelOnError: true,
435      );
436
437      printTrace('test $ourTestCount: starting shell process');
438
439      // If a kernel file is given, then use that to launch the test.
440      // If mapping is provided, look kernel file from mapping.
441      // If all fails, create a "listener" dart that invokes actual test.
442      String mainDart;
443      if (precompiledDillPath != null) {
444        mainDart = precompiledDillPath;
445      } else if (precompiledDillFiles != null) {
446        mainDart = precompiledDillFiles[testPath];
447      }
448      mainDart ??= _createListenerDart(finalizers, ourTestCount, testPath, server);
449
450      if (precompiledDillPath == null && precompiledDillFiles == null) {
451        // Lazily instantiate compiler so it is built only if it is actually used.
452        compiler ??= TestCompiler(trackWidgetCreation, flutterProject);
453        mainDart = await compiler.compile(mainDart);
454
455        if (mainDart == null) {
456          controller.sink.addError(_getErrorMessage('Compilation failed', testPath, shellPath));
457          return null;
458        }
459      }
460
461      final Process process = await _startProcess(
462        shellPath,
463        mainDart,
464        packages: PackageMap.globalPackagesPath,
465        enableObservatory: enableObservatory,
466        startPaused: startPaused,
467        disableServiceAuthCodes: disableServiceAuthCodes,
468        observatoryPort: explicitObservatoryPort,
469        serverPort: server.port,
470      );
471      subprocessActive = true;
472      finalizers.add(() async {
473        if (subprocessActive) {
474          printTrace('test $ourTestCount: ensuring end-of-process for shell');
475          process.kill();
476          final int exitCode = await process.exitCode;
477          subprocessActive = false;
478          if (!controllerSinkClosed && exitCode != -15) {
479            // ProcessSignal.SIGTERM
480            // We expect SIGTERM (15) because we tried to terminate it.
481            // It's negative because signals are returned as negative exit codes.
482            final String message = _getErrorMessage(
483                _getExitCodeMessage(exitCode, 'after tests finished'),
484                testPath,
485                shellPath);
486            controller.sink.addError(message);
487          }
488        }
489      });
490
491      final Completer<void> timeout = Completer<void>();
492      final Completer<void> gotProcessObservatoryUri = Completer<void>();
493      if (!enableObservatory) {
494        gotProcessObservatoryUri.complete();
495      }
496
497      // Pipe stdout and stderr from the subprocess to our printStatus console.
498      // We also keep track of what observatory port the engine used, if any.
499      Uri processObservatoryUri;
500      _pipeStandardStreamsToConsole(
501        process,
502        reportObservatoryUri: (Uri detectedUri) {
503          assert(processObservatoryUri == null);
504          assert(explicitObservatoryPort == null ||
505              explicitObservatoryPort == detectedUri.port);
506          if (startPaused && !machine) {
507            printStatus('The test process has been started.');
508            printStatus('You can now connect to it using observatory. To connect, load the following Web site in your browser:');
509            printStatus('  $detectedUri');
510            printStatus('You should first set appropriate breakpoints, then resume the test in the debugger.');
511          } else {
512            printTrace('test $ourTestCount: using observatory uri $detectedUri from pid ${process.pid}');
513          }
514          processObservatoryUri = detectedUri;
515          {
516            printTrace('Connecting to service protocol: $processObservatoryUri');
517            final Future<VMService> localVmService = VMService.connect(processObservatoryUri,
518              compileExpression: _compileExpressionService);
519            localVmService.then((VMService vmservice) {
520              printTrace('Successfully connected to service protocol: $processObservatoryUri');
521            });
522          }
523          gotProcessObservatoryUri.complete();
524          watcher?.handleStartedProcess(
525              ProcessEvent(ourTestCount, process, processObservatoryUri));
526        },
527        startTimeoutTimer: () {
528          Future<InitialResult>.delayed(_kTestStartupTimeout)
529              .then<void>((_) => timeout.complete());
530        },
531      );
532
533      // At this point, three things can happen next:
534      // The engine could crash, in which case process.exitCode will complete.
535      // The engine could connect to us, in which case webSocket.future will complete.
536      // The local test harness could get bored of us.
537      printTrace('test $ourTestCount: awaiting initial result for pid ${process.pid}');
538      final InitialResult initialResult = await Future.any<InitialResult>(<Future<InitialResult>>[
539        process.exitCode.then<InitialResult>((int exitCode) => InitialResult.crashed),
540        timeout.future.then<InitialResult>((void value) => InitialResult.timedOut),
541        Future<InitialResult>.delayed(_kTestProcessTimeout, () => InitialResult.timedOut),
542        gotProcessObservatoryUri.future.then<InitialResult>((void value) {
543          return webSocket.future.then<InitialResult>(
544            (WebSocket webSocket) => InitialResult.connected,
545          );
546        }),
547      ]);
548
549      switch (initialResult) {
550        case InitialResult.crashed:
551          printTrace('test $ourTestCount: process with pid ${process.pid} crashed before connecting to test harness');
552          final int exitCode = await process.exitCode;
553          subprocessActive = false;
554          final String message = _getErrorMessage(
555              _getExitCodeMessage(
556                  exitCode, 'before connecting to test harness'),
557              testPath,
558              shellPath);
559          controller.sink.addError(message);
560          // Awaited for with 'sink.done' below.
561          unawaited(controller.sink.close());
562          printTrace('test $ourTestCount: waiting for controller sink to close');
563          await controller.sink.done;
564          await watcher?.handleTestCrashed(ProcessEvent(ourTestCount, process));
565          break;
566        case InitialResult.timedOut:
567          // Could happen either if the process takes a long time starting
568          // (_kTestProcessTimeout), or if once Dart code starts running, it takes a
569          // long time to open the WebSocket connection (_kTestStartupTimeout).
570          printTrace('test $ourTestCount: timed out waiting for process with pid ${process.pid} to connect to test harness');
571          final String message = _getErrorMessage('Test never connected to test harness.', testPath, shellPath);
572          controller.sink.addError(message);
573          // Awaited for with 'sink.done' below.
574          unawaited(controller.sink.close());
575          printTrace('test $ourTestCount: waiting for controller sink to close');
576          await controller.sink.done;
577          await watcher
578              ?.handleTestTimedOut(ProcessEvent(ourTestCount, process));
579          break;
580        case InitialResult.connected:
581          printTrace('test $ourTestCount: process with pid ${process.pid} connected to test harness');
582          final WebSocket testSocket = await webSocket.future;
583
584          final Completer<void> harnessDone = Completer<void>();
585          final StreamSubscription<dynamic> harnessToTest =
586              controller.stream.listen(
587            (dynamic event) {
588              testSocket.add(json.encode(event));
589            },
590            onDone: harnessDone.complete,
591            onError: (dynamic error, dynamic stack) {
592              // If you reach here, it's unlikely we're going to be able to really handle this well.
593              printError('test harness controller stream experienced an unexpected error\ntest: $testPath\nerror: $error');
594              if (!controllerSinkClosed) {
595                controller.sink.addError(error, stack);
596                controller.sink.close();
597              } else {
598                printError('unexpected error from test harness controller stream: $error');
599              }
600            },
601            cancelOnError: true,
602          );
603
604          final Completer<void> testDone = Completer<void>();
605          final StreamSubscription<dynamic> testToHarness = testSocket.listen(
606            (dynamic encodedEvent) {
607              assert(encodedEvent
608                  is String); // we shouldn't ever get binary messages
609              controller.sink.add(json.decode(encodedEvent));
610            },
611            onDone: testDone.complete,
612            onError: (dynamic error, dynamic stack) {
613              // If you reach here, it's unlikely we're going to be able to really handle this well.
614              printError('test socket stream experienced an unexpected error\ntest: $testPath\nerror: $error');
615              if (!controllerSinkClosed) {
616                controller.sink.addError(error, stack);
617                controller.sink.close();
618              } else {
619                printError('unexpected error from test socket stream: $error');
620              }
621            },
622            cancelOnError: true,
623          );
624
625          printTrace('test $ourTestCount: awaiting test result for pid ${process.pid}');
626          final TestResult testResult = await Future.any<TestResult>(<Future<TestResult>>[
627            process.exitCode.then<TestResult>((int exitCode) {
628              return TestResult.crashed;
629            }),
630            harnessDone.future.then<TestResult>((void value) {
631              return TestResult.harnessBailed;
632            }),
633            testDone.future.then<TestResult>((void value) {
634              return TestResult.testBailed;
635            }),
636          ]);
637
638          await Future.wait<void>(<Future<void>>[
639            harnessToTest.cancel(),
640            testToHarness.cancel(),
641          ]);
642
643          switch (testResult) {
644            case TestResult.crashed:
645              printTrace('test $ourTestCount: process with pid ${process.pid} crashed');
646              final int exitCode = await process.exitCode;
647              subprocessActive = false;
648              final String message = _getErrorMessage(
649                  _getExitCodeMessage(
650                      exitCode, 'before test harness closed its WebSocket'),
651                  testPath,
652                  shellPath);
653              controller.sink.addError(message);
654              // Awaited for with 'sink.done' below.
655              unawaited(controller.sink.close());
656              printTrace('test $ourTestCount: waiting for controller sink to close');
657              await controller.sink.done;
658              break;
659            case TestResult.harnessBailed:
660            case TestResult.testBailed:
661              if (testResult == TestResult.harnessBailed) {
662                printTrace('test $ourTestCount: process with pid ${process.pid} no longer needed by test harness');
663              } else {
664                assert(testResult == TestResult.testBailed);
665                printTrace('test $ourTestCount: process with pid ${process.pid} no longer needs test harness');
666              }
667              await watcher?.handleFinishedTest(
668                  ProcessEvent(ourTestCount, process, processObservatoryUri));
669              break;
670          }
671          break;
672      }
673    } catch (error, stack) {
674      printTrace('test $ourTestCount: error caught during test; ${controllerSinkClosed ? "reporting to console" : "sending to test framework"}');
675      if (!controllerSinkClosed) {
676        controller.sink.addError(error, stack);
677      } else {
678        printError('unhandled error during test:\n$testPath\n$error\n$stack');
679        outOfBandError ??= _AsyncError(error, stack);
680      }
681    } finally {
682      printTrace('test $ourTestCount: cleaning up...');
683      // Finalizers are treated like a stack; run them in reverse order.
684      for (Finalizer finalizer in finalizers.reversed) {
685        try {
686          await finalizer();
687        } catch (error, stack) {
688          printTrace('test $ourTestCount: error while cleaning up; ${controllerSinkClosed ? "reporting to console" : "sending to test framework"}');
689          if (!controllerSinkClosed) {
690            controller.sink.addError(error, stack);
691          } else {
692            printError('unhandled error during finalization of test:\n$testPath\n$error\n$stack');
693            outOfBandError ??= _AsyncError(error, stack);
694          }
695        }
696      }
697      if (!controllerSinkClosed) {
698        // Waiting below with await.
699        unawaited(controller.sink.close());
700        printTrace('test $ourTestCount: waiting for controller sink to close');
701        await controller.sink.done;
702      }
703    }
704    assert(!subprocessActive);
705    assert(controllerSinkClosed);
706    if (outOfBandError != null) {
707      printTrace('test $ourTestCount: finished with out-of-band failure');
708    } else {
709      printTrace('test $ourTestCount: finished');
710    }
711    return outOfBandError;
712  }
713
714  String _createListenerDart(
715    List<Finalizer> finalizers,
716    int ourTestCount,
717    String testPath,
718    HttpServer server,
719  ) {
720    // Prepare a temporary directory to store the Dart file that will talk to us.
721    final Directory tempDir = fs.systemTempDirectory.createTempSync('flutter_test_listener.');
722    finalizers.add(() async {
723      printTrace('test $ourTestCount: deleting temporary directory');
724      tempDir.deleteSync(recursive: true);
725    });
726
727    // Prepare the Dart file that will talk to us and start the test.
728    final File listenerFile = fs.file('${tempDir.path}/listener.dart');
729    listenerFile.createSync();
730    listenerFile.writeAsStringSync(_generateTestMain(
731      testUrl: fs.path.toUri(fs.path.absolute(testPath)),
732    ));
733    return listenerFile.path;
734  }
735
736  String _generateTestMain({
737    Uri testUrl,
738  }) {
739    assert(testUrl.scheme == 'file');
740    File testConfigFile;
741    Directory directory = fs.file(testUrl).parent;
742    while (directory.path != directory.parent.path) {
743      final File configFile = directory.childFile(_kTestConfigFileName);
744      if (configFile.existsSync()) {
745        printTrace('Discovered $_kTestConfigFileName in ${directory.path}');
746        testConfigFile = configFile;
747        break;
748      }
749      if (directory.childFile(_kProjectRootSentinel).existsSync()) {
750        printTrace('Stopping scan for $_kTestConfigFileName; '
751            'found project root at ${directory.path}');
752        break;
753      }
754      directory = directory.parent;
755    }
756    return generateTestBootstrap(
757      testUrl: testUrl,
758      testConfigFile: testConfigFile,
759      host: host,
760      updateGoldens: updateGoldens,
761    );
762  }
763
764  File _cachedFontConfig;
765
766  @override
767  Future<dynamic> close() async {
768    if (compiler != null) {
769      await compiler.dispose();
770      compiler = null;
771    }
772    if (fontsDirectory != null) {
773      printTrace('Deleting ${fontsDirectory.path}...');
774      fontsDirectory.deleteSync(recursive: true);
775      fontsDirectory = null;
776    }
777  }
778
779  /// Returns a Fontconfig config file that limits font fallback to the
780  /// artifact cache directory.
781  File get _fontConfigFile {
782    if (_cachedFontConfig != null) {
783      return _cachedFontConfig;
784    }
785
786    final StringBuffer sb = StringBuffer();
787    sb.writeln('<fontconfig>');
788    sb.writeln('  <dir>${cache.getCacheArtifacts().path}</dir>');
789    sb.writeln('  <cachedir>/var/cache/fontconfig</cachedir>');
790    sb.writeln('</fontconfig>');
791
792    if (fontsDirectory == null) {
793      fontsDirectory = fs.systemTempDirectory.createTempSync('flutter_test_fonts.');
794      printTrace('Using this directory for fonts configuration: ${fontsDirectory.path}');
795    }
796
797    _cachedFontConfig = fs.file('${fontsDirectory.path}/fonts.conf');
798    _cachedFontConfig.createSync();
799    _cachedFontConfig.writeAsStringSync(sb.toString());
800    return _cachedFontConfig;
801  }
802
803  Future<Process> _startProcess(
804    String executable,
805    String testPath, {
806    String packages,
807    bool enableObservatory = false,
808    bool startPaused = false,
809    bool disableServiceAuthCodes = false,
810    int observatoryPort,
811    int serverPort,
812  }) {
813    assert(executable != null); // Please provide the path to the shell in the SKY_SHELL environment variable.
814    assert(!startPaused || enableObservatory);
815    final List<String> command = <String>[executable];
816    if (enableObservatory) {
817      // Some systems drive the _FlutterPlatform class in an unusual way, where
818      // only one test file is processed at a time, and the operating
819      // environment hands out specific ports ahead of time in a cooperative
820      // manner, where we're only allowed to open ports that were given to us in
821      // advance like this. For those esoteric systems, we have this feature
822      // whereby you can create _FlutterPlatform with a pair of ports.
823      //
824      // I mention this only so that you won't be tempted, as I was, to apply
825      // the obvious simplification to this code and remove this entire feature.
826      if (observatoryPort != null)
827        command.add('--observatory-port=$observatoryPort');
828      if (startPaused) {
829        command.add('--start-paused');
830      }
831      if (disableServiceAuthCodes) {
832        command.add('--disable-service-auth-codes');
833      }
834    } else {
835      command.add('--disable-observatory');
836    }
837    if (host.type == InternetAddressType.IPv6) {
838      command.add('--ipv6');
839    }
840
841    if (icudtlPath != null) {
842      command.add('--icu-data-file-path=$icudtlPath');
843    }
844
845    command.addAll(<String>[
846      '--enable-checked-mode',
847      '--verify-entry-points',
848      '--enable-software-rendering',
849      '--skia-deterministic-rendering',
850      '--enable-dart-profiling',
851      '--non-interactive',
852      '--use-test-fonts',
853      '--packages=$packages',
854      testPath,
855    ]);
856    printTrace(command.join(' '));
857    // If the FLUTTER_TEST environment variable has been set, then pass it on
858    // for package:flutter_test to handle the value.
859    //
860    // If FLUTTER_TEST has not been set, assume from this context that this
861    // call was invoked by the command 'flutter test'.
862    final String flutterTest = platform.environment.containsKey('FLUTTER_TEST')
863        ? platform.environment['FLUTTER_TEST']
864        : 'true';
865    final Map<String, String> environment = <String, String>{
866      'FLUTTER_TEST': flutterTest,
867      'FONTCONFIG_FILE': _fontConfigFile.path,
868      'SERVER_PORT': serverPort.toString(),
869      'APP_NAME': flutterProject?.manifest?.appName ?? '',
870    };
871    if (buildTestAssets) {
872      environment['UNIT_TEST_ASSETS'] = fs.path.join(
873        flutterProject?.directory?.path ?? '', 'build', 'unit_test_assets');
874    }
875    return processManager.start(command, environment: environment);
876  }
877
878  void _pipeStandardStreamsToConsole(
879    Process process, {
880    void startTimeoutTimer(),
881    void reportObservatoryUri(Uri uri),
882  }) {
883    const String observatoryString = 'Observatory listening on ';
884    for (Stream<List<int>> stream in <Stream<List<int>>>[
885      process.stderr,
886      process.stdout,
887    ]) {
888      stream
889          .transform<String>(utf8.decoder)
890          .transform<String>(const LineSplitter())
891          .listen(
892        (String line) {
893          if (line == _kStartTimeoutTimerMessage) {
894            if (startTimeoutTimer != null) {
895              startTimeoutTimer();
896            }
897          } else if (line.startsWith('error: Unable to read Dart source \'package:test/')) {
898            printTrace('Shell: $line');
899            printError('\n\nFailed to load test harness. Are you missing a dependency on flutter_test?\n');
900          } else if (line.startsWith(observatoryString)) {
901            printTrace('Shell: $line');
902            try {
903              final Uri uri = Uri.parse(line.substring(observatoryString.length));
904              if (reportObservatoryUri != null) {
905                reportObservatoryUri(uri);
906              }
907            } catch (error) {
908              printError('Could not parse shell observatory port message: $error');
909            }
910          } else if (line != null) {
911            printStatus('Shell: $line');
912          }
913        },
914        onError: (dynamic error) {
915          printError('shell console stream for process pid ${process.pid} experienced an unexpected error: $error');
916        },
917        cancelOnError: true,
918      );
919    }
920  }
921
922  String _getErrorMessage(String what, String testPath, String shellPath) {
923    return '$what\nTest: $testPath\nShell: $shellPath\n\n';
924  }
925
926  String _getExitCodeMessage(int exitCode, String when) {
927    switch (exitCode) {
928      case 1:
929        return 'Shell subprocess cleanly reported an error $when. Check the logs above for an error message.';
930      case 0:
931        return 'Shell subprocess ended cleanly $when. Did main() call exit()?';
932      case -0x0f: // ProcessSignal.SIGTERM
933        return 'Shell subprocess crashed with SIGTERM ($exitCode) $when.';
934      case -0x0b: // ProcessSignal.SIGSEGV
935        return 'Shell subprocess crashed with segmentation fault $when.';
936      case -0x06: // ProcessSignal.SIGABRT
937        return 'Shell subprocess crashed with SIGABRT ($exitCode) $when.';
938      case -0x02: // ProcessSignal.SIGINT
939        return 'Shell subprocess terminated by ^C (SIGINT, $exitCode) $when.';
940      default:
941        return 'Shell subprocess crashed with unexpected exit code $exitCode $when.';
942    }
943  }
944}
945
946// The [_shellProcessClosed] future can't have errors thrown on it because it
947// crosses zones (it's fed in a zone created by the test package, but listened
948// to by a parent zone, the same zone that calls [close] below).
949//
950// This is because Dart won't let errors that were fed into a Future in one zone
951// propagate to listeners in another zone. (Specifically, the zone in which the
952// future was completed with the error, and the zone in which the listener was
953// registered, are what matters.)
954//
955// Because of this, the [_shellProcessClosed] future takes an [_AsyncError]
956// object as a result. If it's null, it's as if it had completed correctly; if
957// it's non-null, it contains the error and stack trace of the actual error, as
958// if it had completed with that error.
959class _FlutterPlatformStreamSinkWrapper<S> implements StreamSink<S> {
960  _FlutterPlatformStreamSinkWrapper(this._parent, this._shellProcessClosed);
961
962  final StreamSink<S> _parent;
963  final Future<_AsyncError> _shellProcessClosed;
964
965  @override
966  Future<void> get done => _done.future;
967  final Completer<void> _done = Completer<void>();
968
969  @override
970  Future<dynamic> close() {
971    Future.wait<dynamic>(<Future<dynamic>>[
972      _parent.close(),
973      _shellProcessClosed,
974    ]).then<void>(
975      (List<dynamic> futureResults) {
976        assert(futureResults.length == 2);
977        assert(futureResults.first == null);
978        if (futureResults.last is _AsyncError) {
979          _done.completeError(futureResults.last.error, futureResults.last.stack);
980        } else {
981          assert(futureResults.last == null);
982          _done.complete();
983        }
984      },
985      onError: _done.completeError,
986    );
987    return done;
988  }
989
990  @override
991  void add(S event) => _parent.add(event);
992  @override
993  void addError(dynamic errorEvent, [ StackTrace stackTrace ]) => _parent.addError(errorEvent, stackTrace);
994  @override
995  Future<dynamic> addStream(Stream<S> stream) => _parent.addStream(stream);
996}
997
998@immutable
999class _AsyncError {
1000  const _AsyncError(this.error, this.stack);
1001  final dynamic error;
1002  final StackTrace stack;
1003}
1004