blob: 1e78e9e041a6d5aa918f17cbacc182bb0e22a711 [file] [log] [blame]
// Copyright 2014 The Flutter 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 'package:meta/meta.dart';
import 'package:stream_channel/stream_channel.dart';
import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/runner_suite.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/plugin/platform_helpers.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/environment.dart'; // ignore: implementation_imports
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../build_info.dart';
import '../compile.dart';
import '../convert.dart';
import '../dart/package_map.dart';
import '../globals.dart' as globals;
import '../project.dart';
import '../test/test_wrapper.dart';
import '../vmservice.dart';
import 'test_compiler.dart';
import 'test_config.dart';
import 'watcher.dart';
/// The timeout we give the test process to connect to the test harness
/// once the process has entered its main method.
///
/// We time out test execution because we expect some tests to hang and we want
/// to know which test hung, rather than have the entire test harness just do
/// nothing for a few hours until the user (or CI environment) gets bored.
const Duration _kTestStartupTimeout = Duration(minutes: 5);
/// 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.
///
/// See comment under [_kTestStartupTimeout] regarding timeouts.
const Duration _kTestProcessTimeout = Duration(minutes: 5);
/// 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';
/// The address at which our WebSocket server resides and at which the sky_shell
/// processes will host the Observatory server.
final Map<InternetAddressType, InternetAddress> _kHosts = <InternetAddressType, InternetAddress>{
InternetAddressType.IPv4: InternetAddress.loopbackIPv4,
InternetAddressType.IPv6: InternetAddress.loopbackIPv6,
};
typedef PlatformPluginRegistration = void Function(FlutterPlatform platform);
/// Configure the `test` package to work with Flutter.
///
/// On systems where each [FlutterPlatform] is only used to run one test suite
/// (that is, one Dart file with a `*_test.dart` file name and a single `void
/// main()`), you can set an observatory port explicitly.
FlutterPlatform installHook({
TestWrapper testWrapper = const TestWrapper(),
@required String shellPath,
TestWatcher watcher,
bool enableObservatory = false,
bool machine = false,
bool startPaused = false,
bool disableServiceAuthCodes = false,
int port = 0,
String precompiledDillPath,
Map<String, String> precompiledDillFiles,
@required BuildMode buildMode,
bool trackWidgetCreation = false,
bool updateGoldens = false,
bool buildTestAssets = false,
int observatoryPort,
InternetAddressType serverType = InternetAddressType.IPv4,
Uri projectRootDirectory,
FlutterProject flutterProject,
String icudtlPath,
PlatformPluginRegistration platformPluginRegistration,
}) {
assert(testWrapper != null);
assert(enableObservatory || (!startPaused && observatoryPort == null));
// registerPlatformPlugin can be injected for testing since it's not very mock-friendly.
platformPluginRegistration ??= (FlutterPlatform platform) {
testWrapper.registerPlatformPlugin(
<Runtime>[Runtime.vm],
() {
return platform;
},
);
};
final FlutterPlatform platform = FlutterPlatform(
shellPath: shellPath,
watcher: watcher,
machine: machine,
enableObservatory: enableObservatory,
startPaused: startPaused,
disableServiceAuthCodes: disableServiceAuthCodes,
explicitObservatoryPort: observatoryPort,
host: _kHosts[serverType],
port: port,
precompiledDillPath: precompiledDillPath,
precompiledDillFiles: precompiledDillFiles,
buildMode: buildMode,
trackWidgetCreation: trackWidgetCreation,
updateGoldens: updateGoldens,
buildTestAssets: buildTestAssets,
projectRootDirectory: projectRootDirectory,
flutterProject: flutterProject,
icudtlPath: icudtlPath,
);
platformPluginRegistration(platform);
return platform;
}
/// 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,
bool updateGoldens = false,
}) {
assert(testUrl != null);
assert(host != null);
assert(updateGoldens != null);
final String websocketUrl = host.type == InternetAddressType.IPv4
? 'ws://${host.address}'
: 'ws://[${host.address}]';
final String encodedWebsocketUrl = Uri.encodeComponent(websocketUrl);
final StringBuffer buffer = StringBuffer();
buffer.write('''
import 'dart:async';
import 'dart:convert'; // ignore: dart_convert_import
import 'dart:io'; // ignore: dart_io_import
import 'dart:isolate';
import 'package:flutter_test/flutter_test.dart';
import 'package:test_api/src/remote_listener.dart';
import 'package:stream_channel/stream_channel.dart';
import 'package:stack_trace/stack_trace.dart';
import '$testUrl' as test;
''');
if (testConfigFile != null) {
buffer.write('''
import '${Uri.file(testConfigFile.path)}' as test_config;
''');
}
buffer.write('''
/// Returns a serialized test suite.
StreamChannel<dynamic> serializeSuite(Function getMain(),
{bool hidePrints = true, Future<dynamic> beforeLoad()}) {
return RemoteListener.start(getMain,
hidePrints: hidePrints, beforeLoad: beforeLoad);
}
/// Capture any top-level errors (mostly lazy syntax errors, since other are
/// caught below) and report them to the parent isolate.
void catchIsolateErrors() {
final ReceivePort errorPort = ReceivePort();
// Treat errors non-fatal because otherwise they'll be double-printed.
Isolate.current.setErrorsFatal(false);
Isolate.current.addErrorListener(errorPort.sendPort);
errorPort.listen((dynamic message) {
// Masquerade as an IsolateSpawnException because that's what this would
// be if the error had been detected statically.
final IsolateSpawnException error = IsolateSpawnException(message[0]);
final Trace stackTrace =
message[1] == null ? Trace(const <Frame>[]) : Trace.parse(message[1]);
Zone.current.handleUncaughtError(error, stackTrace);
});
}
void main() {
print('$_kStartTimeoutTimerMessage');
String serverPort = Platform.environment['SERVER_PORT'];
String server = Uri.decodeComponent('$encodedWebsocketUrl:\$serverPort');
StreamChannel<dynamic> 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();
}
enum InitialResult { crashed, timedOut, connected }
enum TestResult { crashed, harnessBailed, testBailed }
typedef Finalizer = Future<void> Function();
/// The flutter test platform used to integrate with package:test.
class FlutterPlatform extends PlatformPlugin {
FlutterPlatform({
@required this.shellPath,
this.watcher,
this.enableObservatory,
this.machine,
this.startPaused,
this.disableServiceAuthCodes,
this.explicitObservatoryPort,
this.host,
this.port,
this.precompiledDillPath,
this.precompiledDillFiles,
@required this.buildMode,
this.trackWidgetCreation,
this.updateGoldens,
this.buildTestAssets,
this.projectRootDirectory,
this.flutterProject,
this.icudtlPath,
}) : assert(shellPath != null);
final String shellPath;
final TestWatcher watcher;
final bool enableObservatory;
final bool machine;
final bool startPaused;
final bool disableServiceAuthCodes;
final int explicitObservatoryPort;
final InternetAddress host;
final int port;
final String precompiledDillPath;
final Map<String, String> precompiledDillFiles;
final BuildMode buildMode;
final bool trackWidgetCreation;
final bool updateGoldens;
final bool buildTestAssets;
final Uri projectRootDirectory;
final FlutterProject flutterProject;
final String icudtlPath;
Directory fontsDirectory;
/// The test compiler produces dill files for each test main.
///
/// To speed up compilation, each compile is initialized from an existing
/// dill file from previous runs, if possible.
TestCompiler compiler;
// 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.
int _testCount = 0;
@override
Future<RunnerSuite> load(
String path,
SuitePlatform platform,
SuiteConfiguration suiteConfig,
Object message,
) async {
// loadChannel may throw an exception. That's fine; it will cause the
// LoadSuite to emit an error, which will be presented to the user.
// Except for the Declarer error, which is a specific test incompatibility
// error we need to catch.
try {
final StreamChannel<dynamic> channel = loadChannel(path, platform);
final RunnerSuiteController controller = deserializeSuite(path, platform,
suiteConfig, const PluginEnvironment(), channel, message);
return await controller.suite;
} catch (err) {
/// Rethrow a less confusing error if it is a test incompatibility.
if (err.toString().contains("type 'Declarer' is not a subtype of type 'Declarer'")) {
throw UnsupportedError('Package incompatibility between flutter and test packages:\n'
' * flutter is incompatible with test <1.4.0.\n'
' * flutter is incompatible with mockito <4.0.0\n'
"To fix this error, update test to at least '^1.4.0' and mockito to at least '^4.0.0'\n"
);
}
// Guess it was a different error.
rethrow;
}
}
@override
StreamChannel<dynamic> loadChannel(String path, SuitePlatform platform) {
if (_testCount > 0) {
// Fail if there will be a port conflict.
if (explicitObservatoryPort != null) {
throwToolExit('installHook() was called with an observatory port or debugger mode enabled, but then more than one test suite was run.');
}
// Fail if we're passing in a precompiled entry-point.
if (precompiledDillPath != null) {
throwToolExit('installHook() was called with a precompiled test entry-point, but then more than one test suite was run.');
}
}
final int ourTestCount = _testCount;
_testCount += 1;
final StreamController<dynamic> localController = StreamController<dynamic>();
final StreamController<dynamic> remoteController = StreamController<dynamic>();
final Completer<_AsyncError> testCompleteCompleter = Completer<_AsyncError>();
final _FlutterPlatformStreamSinkWrapper<dynamic> remoteSink = _FlutterPlatformStreamSinkWrapper<dynamic>(
remoteController.sink,
testCompleteCompleter.future,
);
final StreamChannel<dynamic> localChannel = StreamChannel<dynamic>.withGuarantees(
remoteController.stream,
localController.sink,
);
final StreamChannel<dynamic> remoteChannel = StreamChannel<dynamic>.withGuarantees(
localController.stream,
remoteSink,
);
testCompleteCompleter.complete(_startTest(path, localChannel, ourTestCount));
return remoteChannel;
}
Future<String> _compileExpressionService(
String isolateId,
String expression,
List<String> definitions,
List<String> typeDefinitions,
String libraryUri,
String klass,
bool isStatic,
) async {
if (compiler == null || compiler.compiler == null) {
throw 'Compiler is not set up properly to compile $expression';
}
final CompilerOutput compilerOutput =
await compiler.compiler.compileExpression(expression, definitions,
typeDefinitions, libraryUri, klass, isStatic);
if (compilerOutput != null && compilerOutput.outputFilename != null) {
return base64.encode(globals.fs.file(compilerOutput.outputFilename).readAsBytesSync());
}
throw 'Failed to compile $expression';
}
/// Binds an [HttpServer] serving from `host` on `port`.
///
/// Only intended to be overridden in tests for [FlutterPlatform].
@protected
@visibleForTesting
Future<HttpServer> bind(InternetAddress host, int port) => HttpServer.bind(host, port);
Future<_AsyncError> _startTest(
String testPath,
StreamChannel<dynamic> controller,
int ourTestCount,
) async {
globals.printTrace('test $ourTestCount: starting test $testPath');
_AsyncError outOfBandError; // error that we couldn't send to the harness that we need to send via our future
final List<Finalizer> finalizers = <Finalizer>[]; // Will be run in reverse order.
bool subprocessActive = false;
bool controllerSinkClosed = false;
try {
// Callback can't throw since it's just setting a variable.
unawaited(controller.sink.done.whenComplete(() {
controllerSinkClosed = true;
}));
// Prepare our WebSocket server to talk to the engine subproces.
final HttpServer server = await bind(host, port);
finalizers.add(() async {
globals.printTrace('test $ourTestCount: shutting down test harness socket server');
await server.close(force: true);
});
final Completer<WebSocket> webSocket = Completer<WebSocket>();
server.listen(
(HttpRequest request) {
if (!webSocket.isCompleted) {
webSocket.complete(WebSocketTransformer.upgrade(request));
}
},
onError: (dynamic error, StackTrace stack) {
// If you reach here, it's unlikely we're going to be able to really handle this well.
globals.printTrace('test $ourTestCount: test harness socket server experienced an unexpected error: $error');
if (!controllerSinkClosed) {
controller.sink.addError(error, stack);
controller.sink.close();
} else {
globals.printError('unexpected error from test harness socket server: $error');
}
},
cancelOnError: true,
);
globals.printTrace('test $ourTestCount: starting shell process');
// If a kernel file is given, then use that to launch the test.
// If mapping is provided, look kernel file from mapping.
// If all fails, create a "listener" dart that invokes actual test.
String mainDart;
if (precompiledDillPath != null) {
mainDart = precompiledDillPath;
} else if (precompiledDillFiles != null) {
mainDart = precompiledDillFiles[testPath];
}
mainDart ??= _createListenerDart(finalizers, ourTestCount, testPath, server);
if (precompiledDillPath == null && precompiledDillFiles == null) {
// Lazily instantiate compiler so it is built only if it is actually used.
compiler ??= TestCompiler(buildMode, trackWidgetCreation, flutterProject);
mainDart = await compiler.compile(mainDart);
if (mainDart == null) {
controller.sink.addError(_getErrorMessage('Compilation failed', testPath, shellPath));
return null;
}
}
final Process process = await _startProcess(
shellPath,
mainDart,
packages: PackageMap.globalPackagesPath,
enableObservatory: enableObservatory,
startPaused: startPaused,
disableServiceAuthCodes: disableServiceAuthCodes,
observatoryPort: explicitObservatoryPort,
serverPort: server.port,
);
subprocessActive = true;
finalizers.add(() async {
if (subprocessActive) {
globals.printTrace('test $ourTestCount: ensuring end-of-process for shell');
process.kill();
final int exitCode = await process.exitCode;
subprocessActive = false;
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.
final String message = _getErrorMessage(
_getExitCodeMessage(exitCode, 'after tests finished'),
testPath,
shellPath);
controller.sink.addError(message);
}
}
});
final Completer<void> timeout = Completer<void>();
final Completer<void> gotProcessObservatoryUri = Completer<void>();
if (!enableObservatory) {
gotProcessObservatoryUri.complete();
}
// Pipe stdout and stderr from the subprocess to our printStatus console.
// We also keep track of what observatory port the engine used, if any.
Uri processObservatoryUri;
_pipeStandardStreamsToConsole(
process,
reportObservatoryUri: (Uri detectedUri) {
assert(processObservatoryUri == null);
assert(explicitObservatoryPort == null ||
explicitObservatoryPort == detectedUri.port);
if (startPaused && !machine) {
globals.printStatus('The test process has been started.');
globals.printStatus('You can now connect to it using observatory. To connect, load the following Web site in your browser:');
globals.printStatus(' $detectedUri');
globals.printStatus('You should first set appropriate breakpoints, then resume the test in the debugger.');
} else {
globals.printTrace('test $ourTestCount: using observatory uri $detectedUri from pid ${process.pid}');
}
processObservatoryUri = detectedUri;
{
globals.printTrace('Connecting to service protocol: $processObservatoryUri');
final Future<VMService> localVmService = VMService.connect(processObservatoryUri,
compileExpression: _compileExpressionService);
localVmService.then((VMService vmservice) {
globals.printTrace('Successfully connected to service protocol: $processObservatoryUri');
});
}
gotProcessObservatoryUri.complete();
watcher?.handleStartedProcess(
ProcessEvent(ourTestCount, process, processObservatoryUri));
},
startTimeoutTimer: () {
Future<InitialResult>.delayed(_kTestStartupTimeout)
.then<void>((_) => timeout.complete());
},
);
// 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.
globals.printTrace('test $ourTestCount: awaiting initial result for pid ${process.pid}');
final InitialResult initialResult = await Future.any<InitialResult>(<Future<InitialResult>>[
process.exitCode.then<InitialResult>((int exitCode) => InitialResult.crashed),
timeout.future.then<InitialResult>((void value) => InitialResult.timedOut),
Future<InitialResult>.delayed(_kTestProcessTimeout, () => InitialResult.timedOut),
gotProcessObservatoryUri.future.then<InitialResult>((void value) {
return webSocket.future.then<InitialResult>(
(WebSocket webSocket) => InitialResult.connected,
);
}),
]);
switch (initialResult) {
case InitialResult.crashed:
globals.printTrace('test $ourTestCount: process with pid ${process.pid} crashed before connecting to test harness');
final int exitCode = await process.exitCode;
subprocessActive = false;
final String message = _getErrorMessage(
_getExitCodeMessage(
exitCode, 'before connecting to test harness'),
testPath,
shellPath);
controller.sink.addError(message);
// Awaited for with 'sink.done' below.
unawaited(controller.sink.close());
globals.printTrace('test $ourTestCount: waiting for controller sink to close');
await controller.sink.done;
await watcher?.handleTestCrashed(ProcessEvent(ourTestCount, process));
break;
case InitialResult.timedOut:
// 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).
globals.printTrace('test $ourTestCount: timed out waiting for process with pid ${process.pid} to connect to test harness');
final String message = _getErrorMessage('Test never connected to test harness.', testPath, shellPath);
controller.sink.addError(message);
// Awaited for with 'sink.done' below.
unawaited(controller.sink.close());
globals.printTrace('test $ourTestCount: waiting for controller sink to close');
await controller.sink.done;
await watcher
?.handleTestTimedOut(ProcessEvent(ourTestCount, process));
break;
case InitialResult.connected:
globals.printTrace('test $ourTestCount: process with pid ${process.pid} connected to test harness');
final WebSocket testSocket = await webSocket.future;
final Completer<void> harnessDone = Completer<void>();
final StreamSubscription<dynamic> harnessToTest =
controller.stream.listen(
(dynamic event) {
testSocket.add(json.encode(event));
},
onDone: harnessDone.complete,
onError: (dynamic error, StackTrace stack) {
// If you reach here, it's unlikely we're going to be able to really handle this well.
globals.printError('test harness controller stream experienced an unexpected error\ntest: $testPath\nerror: $error');
if (!controllerSinkClosed) {
controller.sink.addError(error, stack);
controller.sink.close();
} else {
globals.printError('unexpected error from test harness controller stream: $error');
}
},
cancelOnError: true,
);
final Completer<void> testDone = Completer<void>();
final StreamSubscription<dynamic> testToHarness = testSocket.listen(
(dynamic encodedEvent) {
assert(encodedEvent is String); // we shouldn't ever get binary messages
controller.sink.add(json.decode(encodedEvent as String));
},
onDone: testDone.complete,
onError: (dynamic error, StackTrace stack) {
// If you reach here, it's unlikely we're going to be able to really handle this well.
globals.printError('test socket stream experienced an unexpected error\ntest: $testPath\nerror: $error');
if (!controllerSinkClosed) {
controller.sink.addError(error, stack);
controller.sink.close();
} else {
globals.printError('unexpected error from test socket stream: $error');
}
},
cancelOnError: true,
);
globals.printTrace('test $ourTestCount: awaiting test result for pid ${process.pid}');
final TestResult testResult = await Future.any<TestResult>(<Future<TestResult>>[
process.exitCode.then<TestResult>((int exitCode) {
return TestResult.crashed;
}),
harnessDone.future.then<TestResult>((void value) {
return TestResult.harnessBailed;
}),
testDone.future.then<TestResult>((void value) {
return TestResult.testBailed;
}),
]);
await Future.wait<void>(<Future<void>>[
harnessToTest.cancel(),
testToHarness.cancel(),
]);
switch (testResult) {
case TestResult.crashed:
globals.printTrace('test $ourTestCount: process with pid ${process.pid} crashed');
final int exitCode = await process.exitCode;
subprocessActive = false;
final String message = _getErrorMessage(
_getExitCodeMessage(
exitCode, 'before test harness closed its WebSocket'),
testPath,
shellPath);
controller.sink.addError(message);
// Awaited for with 'sink.done' below.
unawaited(controller.sink.close());
globals.printTrace('test $ourTestCount: waiting for controller sink to close');
await controller.sink.done;
break;
case TestResult.harnessBailed:
case TestResult.testBailed:
if (testResult == TestResult.harnessBailed) {
globals.printTrace('test $ourTestCount: process with pid ${process.pid} no longer needed by test harness');
} else {
assert(testResult == TestResult.testBailed);
globals.printTrace('test $ourTestCount: process with pid ${process.pid} no longer needs test harness');
}
await watcher?.handleFinishedTest(
ProcessEvent(ourTestCount, process, processObservatoryUri));
break;
}
break;
}
} catch (error, stack) {
globals.printTrace('test $ourTestCount: error caught during test; ${controllerSinkClosed ? "reporting to console" : "sending to test framework"}');
if (!controllerSinkClosed) {
controller.sink.addError(error, stack);
} else {
globals.printError('unhandled error during test:\n$testPath\n$error\n$stack');
outOfBandError ??= _AsyncError(error, stack);
}
} finally {
globals.printTrace('test $ourTestCount: cleaning up...');
// Finalizers are treated like a stack; run them in reverse order.
for (final Finalizer finalizer in finalizers.reversed) {
try {
await finalizer();
} catch (error, stack) {
globals.printTrace('test $ourTestCount: error while cleaning up; ${controllerSinkClosed ? "reporting to console" : "sending to test framework"}');
if (!controllerSinkClosed) {
controller.sink.addError(error, stack);
} else {
globals.printError('unhandled error during finalization of test:\n$testPath\n$error\n$stack');
outOfBandError ??= _AsyncError(error, stack);
}
}
}
if (!controllerSinkClosed) {
// Waiting below with await.
unawaited(controller.sink.close());
globals.printTrace('test $ourTestCount: waiting for controller sink to close');
await controller.sink.done;
}
}
assert(!subprocessActive);
assert(controllerSinkClosed);
if (outOfBandError != null) {
globals.printTrace('test $ourTestCount: finished with out-of-band failure');
} else {
globals.printTrace('test $ourTestCount: finished');
}
return outOfBandError;
}
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 tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_test_listener.');
finalizers.add(() async {
globals.printTrace('test $ourTestCount: deleting temporary directory');
tempDir.deleteSync(recursive: true);
});
// Prepare the Dart file that will talk to us and start the test.
final File listenerFile = globals.fs.file('${tempDir.path}/listener.dart');
listenerFile.createSync();
listenerFile.writeAsStringSync(_generateTestMain(
testUrl: globals.fs.path.toUri(globals.fs.path.absolute(testPath)),
));
return listenerFile.path;
}
String _generateTestMain({
Uri testUrl,
}) {
assert(testUrl.scheme == 'file');
return generateTestBootstrap(
testUrl: testUrl,
testConfigFile: findTestConfigFile(globals.fs.file(testUrl)),
host: host,
updateGoldens: updateGoldens,
);
}
File _cachedFontConfig;
@override
Future<dynamic> close() async {
if (compiler != null) {
await compiler.dispose();
compiler = null;
}
if (fontsDirectory != null) {
globals.printTrace('Deleting ${fontsDirectory.path}...');
fontsDirectory.deleteSync(recursive: true);
fontsDirectory = null;
}
}
/// Returns a Fontconfig config file that limits font fallback to the
/// artifact cache directory.
File get _fontConfigFile {
if (_cachedFontConfig != null) {
return _cachedFontConfig;
}
final StringBuffer sb = StringBuffer();
sb.writeln('<fontconfig>');
sb.writeln(' <dir>${globals.cache.getCacheArtifacts().path}</dir>');
sb.writeln(' <cachedir>/var/cache/fontconfig</cachedir>');
sb.writeln('</fontconfig>');
if (fontsDirectory == null) {
fontsDirectory = globals.fs.systemTempDirectory.createTempSync('flutter_test_fonts.');
globals.printTrace('Using this directory for fonts configuration: ${fontsDirectory.path}');
}
_cachedFontConfig = globals.fs.file('${fontsDirectory.path}/fonts.conf');
_cachedFontConfig.createSync();
_cachedFontConfig.writeAsStringSync(sb.toString());
return _cachedFontConfig;
}
Future<Process> _startProcess(
String executable,
String testPath, {
String packages,
bool enableObservatory = false,
bool startPaused = false,
bool disableServiceAuthCodes = false,
int observatoryPort,
int serverPort,
}) {
assert(executable != null); // Please provide the path to the shell in the SKY_SHELL environment variable.
assert(!startPaused || enableObservatory);
final List<String> command = <String>[
executable,
if (enableObservatory) ...<String>[
// Some systems drive the _FlutterPlatform class in an unusual way, where
// 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
// whereby you can create _FlutterPlatform with a pair of ports.
//
// 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) '--observatory-port=$observatoryPort',
if (startPaused) '--start-paused',
if (disableServiceAuthCodes) '--disable-service-auth-codes',
]
else
'--disable-observatory',
if (host.type == InternetAddressType.IPv6) '--ipv6',
if (icudtlPath != null) '--icu-data-file-path=$icudtlPath',
'--enable-checked-mode',
'--verify-entry-points',
'--enable-software-rendering',
'--skia-deterministic-rendering',
'--enable-dart-profiling',
'--non-interactive',
'--use-test-fonts',
'--packages=$packages',
testPath,
];
globals.printTrace(command.join(' '));
// If the FLUTTER_TEST environment variable has been set, then pass it on
// for package:flutter_test to handle the value.
//
// If FLUTTER_TEST has not been set, assume from this context that this
// call was invoked by the command 'flutter test'.
final String flutterTest = globals.platform.environment.containsKey('FLUTTER_TEST')
? globals.platform.environment['FLUTTER_TEST']
: 'true';
final Map<String, String> environment = <String, String>{
'FLUTTER_TEST': flutterTest,
'FONTCONFIG_FILE': _fontConfigFile.path,
'SERVER_PORT': serverPort.toString(),
'APP_NAME': flutterProject?.manifest?.appName ?? '',
if (buildTestAssets)
'UNIT_TEST_ASSETS': globals.fs.path.join(flutterProject?.directory?.path ?? '', 'build', 'unit_test_assets'),
};
return globals.processManager.start(command, environment: environment);
}
void _pipeStandardStreamsToConsole(
Process process, {
void startTimeoutTimer(),
void reportObservatoryUri(Uri uri),
}) {
const String observatoryString = 'Observatory listening on ';
for (final Stream<List<int>> stream in <Stream<List<int>>>[
process.stderr,
process.stdout,
]) {
stream
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen(
(String line) {
if (line == _kStartTimeoutTimerMessage) {
if (startTimeoutTimer != null) {
startTimeoutTimer();
}
} else if (line.startsWith("error: Unable to read Dart source 'package:test/")) {
globals.printTrace('Shell: $line');
globals.printError('\n\nFailed to load test harness. Are you missing a dependency on flutter_test?\n');
} else if (line.startsWith(observatoryString)) {
globals.printTrace('Shell: $line');
try {
final Uri uri = Uri.parse(line.substring(observatoryString.length));
if (reportObservatoryUri != null) {
reportObservatoryUri(uri);
}
} catch (error) {
globals.printError('Could not parse shell observatory port message: $error');
}
} else if (line != null) {
globals.printStatus('Shell: $line');
}
},
onError: (dynamic error) {
globals. printError('shell console stream for process pid ${process.pid} experienced an unexpected error: $error');
},
cancelOnError: true,
);
}
}
String _getErrorMessage(String what, String testPath, String shellPath) {
return '$what\nTest: $testPath\nShell: $shellPath\n\n';
}
String _getExitCodeMessage(int exitCode, String when) {
switch (exitCode) {
case 1:
return 'Shell subprocess cleanly reported an error $when. Check the logs above for an error message.';
case 0:
return 'Shell subprocess ended cleanly $when. Did main() call exit()?';
case -0x0f: // ProcessSignal.SIGTERM
return 'Shell subprocess crashed with SIGTERM ($exitCode) $when.';
case -0x0b: // ProcessSignal.SIGSEGV
return 'Shell subprocess crashed with segmentation fault $when.';
case -0x06: // ProcessSignal.SIGABRT
return 'Shell subprocess crashed with SIGABRT ($exitCode) $when.';
case -0x02: // ProcessSignal.SIGINT
return 'Shell subprocess terminated by ^C (SIGINT, $exitCode) $when.';
default:
return 'Shell subprocess crashed with unexpected exit code $exitCode $when.';
}
}
}
// The [_shellProcessClosed] future can't have errors thrown on it because it
// crosses zones (it's fed in a zone created by the test package, but listened
// to by a parent zone, the same zone that calls [close] below).
//
// This is because Dart won't let errors that were fed into a Future in one zone
// propagate to listeners in another zone. (Specifically, the zone in which the
// future was completed with the error, and the zone in which the listener was
// registered, are what matters.)
//
// Because of this, the [_shellProcessClosed] future takes an [_AsyncError]
// object as a result. If it's null, it's as if it had completed correctly; if
// it's non-null, it contains the error and stack trace of the actual error, as
// if it had completed with that error.
class _FlutterPlatformStreamSinkWrapper<S> implements StreamSink<S> {
_FlutterPlatformStreamSinkWrapper(this._parent, this._shellProcessClosed);
final StreamSink<S> _parent;
final Future<_AsyncError> _shellProcessClosed;
@override
Future<void> get done => _done.future;
final Completer<void> _done = Completer<void>();
@override
Future<dynamic> close() {
Future.wait<dynamic>(<Future<dynamic>>[
_parent.close(),
_shellProcessClosed,
]).then<void>(
(List<dynamic> futureResults) {
assert(futureResults.length == 2);
assert(futureResults.first == null);
final dynamic lastResult = futureResults.last;
if (lastResult is _AsyncError) {
_done.completeError(lastResult.error, lastResult.stack);
} else {
assert(lastResult == null);
_done.complete();
}
},
onError: _done.completeError,
);
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);
}
@immutable
class _AsyncError {
const _AsyncError(this.error, this.stack);
final dynamic error;
final StackTrace stack;
}