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