blob: a972620f25d2be5a8bfa20490a95d5f9483c3494 [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 'dart:io' as io; // ignore: dart_io_import;
import 'package:dds/dds.dart';
import 'package:meta/meta.dart';
import 'package:package_config/package_config.dart';
import 'package:stream_channel/stream_channel.dart';
import 'package:test_core/src/platform.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/language_version.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 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,
bool disableDds = false,
int port = 0,
String precompiledDillPath,
Map<String, String> precompiledDillFiles,
bool updateGoldens = false,
bool buildTestAssets = false,
int observatoryPort,
InternetAddressType serverType = InternetAddressType.IPv4,
Uri projectRootDirectory,
FlutterProject flutterProject,
String icudtlPath,
PlatformPluginRegistration platformPluginRegistration,
bool nullAssertions = false,
@required BuildInfo buildInfo,
List<String> additionalArguments,
}) {
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,
disableDds: disableDds,
explicitObservatoryPort: observatoryPort,
host: _kHosts[serverType],
port: port,
precompiledDillPath: precompiledDillPath,
precompiledDillFiles: precompiledDillFiles,
updateGoldens: updateGoldens,
buildTestAssets: buildTestAssets,
projectRootDirectory: projectRootDirectory,
flutterProject: flutterProject,
icudtlPath: icudtlPath,
nullAssertions: nullAssertions,
buildInfo: buildInfo,
additionalArguments: additionalArguments,
);
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.
// NOTE: this API is used by the fuchsia source tree, do not add new
// required or position parameters.
String generateTestBootstrap({
@required Uri testUrl,
@required InternetAddress host,
File testConfigFile,
bool updateGoldens = false,
String languageVersionHeader = '',
bool nullSafety = false,
bool flutterTestDep = true,
}) {
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('''
$languageVersionHeader
import 'dart:async';
import 'dart:convert'; // ignore: dart_convert_import
import 'dart:io'; // ignore: dart_io_import
import 'dart:isolate';
''');
if (flutterTestDep) {
buffer.write('''
import 'package:flutter_test/flutter_test.dart';
''');
}
buffer.write('''
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()) {
return RemoteListener.start(getMain);
}
/// 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] as String);
final Trace stackTrace = message[1] == null ?
Trace(const <Frame>[]) : Trace.parse(message[1] as String);
Zone.current.handleUncaughtError(error, stackTrace);
});
}
void main() {
String serverPort = Platform.environment['SERVER_PORT'] ?? '';
String server = Uri.decodeComponent('$encodedWebsocketUrl:\$serverPort');
StreamChannel<dynamic> channel = serializeSuite(() {
catchIsolateErrors();
''');
if (flutterTestDep) {
buffer.write('''
goldenFileComparator = LocalFileComparator(Uri.parse('$testUrl'));
autoUpdateGoldenFiles = $updateGoldens;
''');
}
if (testConfigFile != null) {
buffer.write('''
return () => test_config.testExecutable(test.main);
''');
} else {
buffer.write('''
return test.main;
''');
}
buffer.write('''
});
WebSocket.connect(server).then((WebSocket socket) {
socket.map((dynamic x) {
return json.decode(x as String);
}).pipe(channel.sink);
socket.addStream(channel.stream.map(json.encode));
});
}
''');
return buffer.toString();
}
enum InitialResult { crashed, 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.disableDds,
this.explicitObservatoryPort,
this.host,
this.port,
this.precompiledDillPath,
this.precompiledDillFiles,
this.updateGoldens,
this.buildTestAssets,
this.projectRootDirectory,
this.flutterProject,
this.icudtlPath,
this.nullAssertions = false,
this.additionalArguments,
@required this.buildInfo,
}) : assert(shellPath != null);
final String shellPath;
final TestWatcher watcher;
final bool enableObservatory;
final bool machine;
final bool startPaused;
final bool disableServiceAuthCodes;
final bool disableDds;
final int explicitObservatoryPort;
final InternetAddress host;
final int port;
final String precompiledDillPath;
final Map<String, String> precompiledDillFiles;
final bool updateGoldens;
final bool buildTestAssets;
final Uri projectRootDirectory;
final FlutterProject flutterProject;
final String icudtlPath;
final bool nullAssertions;
final BuildInfo buildInfo;
final List<String> additionalArguments;
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.
final StreamChannel<dynamic> channel = loadChannel(path, platform);
final RunnerSuiteController controller = deserializeSuite(path, platform,
suiteConfig, const PluginEnvironment(), channel, message);
return controller.suite;
}
@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);
PackageConfig _packageConfig;
Future<_AsyncError> _startTest(
String testPath,
StreamChannel<dynamic> controller,
int ourTestCount,
) async {
_packageConfig ??= buildInfo.packageConfig;
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 subprocess.
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(buildInfo, flutterProject);
mainDart = await compiler.compile(globals.fs.file(mainDart).uri);
if (mainDart == null) {
controller.sink.addError(_getErrorMessage('Compilation failed', testPath, shellPath));
return null;
}
}
final Process process = await _startProcess(
shellPath,
mainDart,
packages: buildInfo.packagesPath,
enableObservatory: enableObservatory,
startPaused: startPaused,
disableServiceAuthCodes: disableServiceAuthCodes,
observatoryPort: disableDds ? explicitObservatoryPort : 0,
serverPort: server.port,
additionalArguments: additionalArguments,
);
subprocessActive = true;
finalizers.add(() async {
if (subprocessActive) {
globals.printTrace('test $ourTestCount: ensuring end-of-process for shell');
process.kill(io.ProcessSignal.sigkill);
final int exitCode = await process.exitCode;
subprocessActive = false;
if (!controllerSinkClosed && exitCode != -9) {
// ProcessSignal.SIGKILL
// We expect SIGKILL (9) 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> 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;
final Uri ddsServiceUri = getDdsServiceUri();
_pipeStandardStreamsToConsole(
process,
reportObservatoryUri: (Uri detectedUri) async {
assert(processObservatoryUri == null);
assert(explicitObservatoryPort == null ||
explicitObservatoryPort == detectedUri.port);
if (!disableDds) {
final DartDevelopmentService dds = await DartDevelopmentService.startDartDevelopmentService(
detectedUri,
serviceUri: ddsServiceUri,
enableAuthCodes: !disableServiceAuthCodes,
ipv6: host.type == InternetAddressType.IPv6,
);
processObservatoryUri = dds.uri;
globals.printTrace('Dart Development Service started at ${dds.uri}, forwarding to VM service at ${dds.remoteVmServiceUri}.');
} else {
processObservatoryUri = detectedUri;
}
{
globals.printTrace('Connecting to service protocol: $processObservatoryUri');
final Future<FlutterVmService> localVmService = connectToVmService(processObservatoryUri,
compileExpression: _compileExpressionService);
unawaited(localVmService.then((FlutterVmService vmservice) {
globals.printTrace('Successfully connected to service protocol: $processObservatoryUri');
}));
}
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(' $processObservatoryUri');
globals.printStatus('You should first set appropriate breakpoints, then resume the test in the debugger.');
} else {
globals.printTrace('test $ourTestCount: using observatory uri $processObservatoryUri from pid ${process.pid}');
}
gotProcessObservatoryUri.complete();
watcher?.handleStartedProcess(
ProcessEvent(ourTestCount, process, processObservatoryUri));
},
);
// 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),
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.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;
}
} on Exception 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();
} on Exception 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');
final File file = globals.fs.file(testUrl);
final LanguageVersion languageVersion = determineLanguageVersion(
file,
_packageConfig[flutterProject?.manifest?.appName],
);
return generateTestBootstrap(
testUrl: testUrl,
testConfigFile: findTestConfigFile(globals.fs.file(testUrl)),
host: host,
updateGoldens: updateGoldens,
flutterTestDep: _packageConfig['flutter_test'] != null,
languageVersionHeader: '// @dart=${languageVersion.major}.${languageVersion.minor}'
);
}
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,
List<String> additionalArguments,
}) {
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',
if (nullAssertions)
'--dart-flags=--null_assertions',
...?additionalArguments,
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, {
Future<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) async {
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) {
await reportObservatoryUri(uri);
}
} on Exception 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.';
}
}
@visibleForTesting
@protected
Uri getDdsServiceUri() {
return Uri(
scheme: 'http',
host: (host.type == InternetAddressType.IPv6 ?
InternetAddress.loopbackIPv6 :
InternetAddress.loopbackIPv4
).host,
port: explicitObservatoryPort ?? 0,
);
}
}
// 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;
}