Unverified Commit 9e55af52 authored by Jia Hao's avatar Jia Hao Committed by GitHub

[flutter_tools] Decouple FlutterPlatform from Process (#74236)

parent 6a8ba743
......@@ -15,6 +15,7 @@ import '../base/utils.dart';
import '../globals.dart' as globals;
import '../vmservice.dart';
import 'test_device.dart';
import 'watcher.dart';
/// A class that's used to collect coverage data during tests.
......@@ -27,9 +28,9 @@ class CoverageCollector extends TestWatcher {
bool Function(String) libraryPredicate;
@override
Future<void> handleFinishedTest(ProcessEvent event) async {
_logMessage('test ${event.childIndex}: collecting coverage');
await collectCoverage(event.process, event.observatoryUri);
Future<void> handleFinishedTest(TestDevice testDevice) async {
_logMessage('Starting coverage collection');
await collectCoverage(testDevice);
}
void _logMessage(String line, { bool error = false }) {
......@@ -81,34 +82,41 @@ class CoverageCollector extends TestWatcher {
/// has been run to completion so that all coverage data has been recorded.
///
/// The returned [Future] completes when the coverage is collected.
Future<void> collectCoverage(Process process, Uri observatoryUri) async {
assert(process != null);
assert(observatoryUri != null);
final int pid = process.pid;
_logMessage('pid $pid: collecting coverage data from $observatoryUri...');
Future<void> collectCoverage(TestDevice testDevice) async {
assert(testDevice != null);
Map<String, dynamic> data;
final Future<void> processComplete = process.exitCode
.then<void>((int code) {
throw Exception('Failed to collect coverage, process terminated prematurely with exit code $code.');
});
final Future<void> collectionComplete = collect(observatoryUri, libraryPredicate)
.then<void>((Map<String, dynamic> result) {
if (result == null) {
throw Exception('Failed to collect coverage.');
}
data = result;
final Future<void> processComplete = testDevice.finished.catchError(
(Object error) => throw Exception(
'Failed to collect coverage, test device terminated prematurely with '
'error: ${(error as TestDeviceException).message}.'),
test: (Object error) => error is TestDeviceException,
);
final Future<void> collectionComplete = testDevice.observatoryUri
.then((Uri observatoryUri) {
_logMessage('collecting coverage data from $testDevice at $observatoryUri...');
return collect(observatoryUri, libraryPredicate)
.then<void>((Map<String, dynamic> result) {
if (result == null) {
throw Exception('Failed to collect coverage.');
}
_logMessage('Collected coverage data.');
data = result;
});
});
await Future.any<void>(<Future<void>>[ processComplete, collectionComplete ]);
assert(data != null);
_logMessage('pid $pid ($observatoryUri): collected coverage data; merging...');
_logMessage('Merging coverage data...');
_addHitmap(await coverage.createHitmap(
data['coverage'] as List<Map<String, dynamic>>,
packagesPath: packagesPath,
checkIgnoredLines: true,
));
_logMessage('pid $pid ($observatoryUri): done merging coverage data into global coverage map.');
_logMessage('Done merging coverage data into global coverage map.');
}
/// Returns a future that will complete with the formatted coverage data
......@@ -188,10 +196,10 @@ class CoverageCollector extends TestWatcher {
}
@override
Future<void> handleTestCrashed(ProcessEvent event) async { }
Future<void> handleTestCrashed(TestDevice testDevice) async { }
@override
Future<void> handleTestTimedOut(ProcessEvent event) async { }
Future<void> handleTestTimedOut(TestDevice testDevice) async { }
}
Future<vm_service.VmService> _defaultConnect(Uri serviceUri) {
......
......@@ -6,6 +6,8 @@
import '../convert.dart';
import '../globals.dart' as globals;
import 'test_device.dart';
import 'watcher.dart';
/// Prints JSON events when running a test in --machine mode.
......@@ -18,25 +20,25 @@ class EventPrinter extends TestWatcher {
final TestWatcher _parent;
@override
void handleStartedProcess(ProcessEvent event) {
void handleStartedDevice(Uri observatoryUri) {
_sendEvent('test.startedProcess',
<String, dynamic>{'observatoryUri': event.observatoryUri.toString()});
_parent?.handleStartedProcess(event);
<String, dynamic>{'observatoryUri': observatoryUri.toString()});
_parent?.handleStartedDevice(observatoryUri);
}
@override
Future<void> handleTestCrashed(ProcessEvent event) async {
return _parent?.handleTestCrashed(event);
Future<void> handleTestCrashed(TestDevice testDevice) async {
return _parent?.handleTestCrashed(testDevice);
}
@override
Future<void> handleTestTimedOut(ProcessEvent event) async {
return _parent?.handleTestTimedOut(event);
Future<void> handleTestTimedOut(TestDevice testDevice) async {
return _parent?.handleTestTimedOut(testDevice);
}
@override
Future<void> handleFinishedTest(ProcessEvent event) async {
return _parent?.handleFinishedTest(event);
Future<void> handleFinishedTest(TestDevice testDevice) async {
return _parent?.handleFinishedTest(testDevice);
}
void _sendEvent(String name, [ dynamic params ]) {
......
......@@ -5,14 +5,11 @@
// @dart = 2.8
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 'package:vm_service/vm_service.dart' as vm_service;
import '../base/common.dart';
import '../base/file_system.dart';
......@@ -24,9 +21,12 @@ import '../device.dart';
import '../globals.dart' as globals;
import '../project.dart';
import '../test/test_wrapper.dart';
import '../vmservice.dart';
import 'flutter_tester_device.dart';
import 'font_config_manager.dart';
import 'test_compiler.dart';
import 'test_config.dart';
import 'test_device.dart';
import 'watcher.dart';
/// The address at which our WebSocket server resides and at which the sky_shell
......@@ -50,7 +50,6 @@ FlutterPlatform installHook({
TestWatcher watcher,
bool enableObservatory = false,
bool machine = false,
int port = 0,
String precompiledDillPath,
Map<String, String> precompiledDillFiles,
bool updateGoldens = false,
......@@ -80,7 +79,6 @@ FlutterPlatform installHook({
machine: machine,
enableObservatory: enableObservatory,
host: _kHosts[serverType],
port: port,
precompiledDillPath: precompiledDillPath,
precompiledDillFiles: precompiledDillFiles,
updateGoldens: updateGoldens,
......@@ -178,17 +176,16 @@ void catchIsolateErrors() {
});
}
void main() {
String serverPort = Platform.environment['SERVER_PORT'] ?? '';
String server = Uri.decodeComponent('$encodedWebsocketUrl:\$serverPort');
StreamChannel<dynamic> channel = serializeSuite(() {
StreamChannel<dynamic> testChannel = serializeSuite(() {
catchIsolateErrors();
''');
if (flutterTestDep) {
buffer.write('''
goldenFileComparator = LocalFileComparator(Uri.parse('$testUrl'));
autoUpdateGoldenFiles = $updateGoldens;
goldenFileComparator = LocalFileComparator(Uri.parse('$testUrl'));
autoUpdateGoldenFiles = $updateGoldens;
''');
}
if (testConfigFile != null) {
......@@ -203,18 +200,17 @@ autoUpdateGoldenFiles = $updateGoldens;
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));
socket.map((dynamic message) {
// We're only communicating with string encoded JSON.
return json.decode(message as String);
}).pipe(testChannel.sink);
socket.addStream(testChannel.stream.map(json.encode));
});
}
''');
return buffer.toString();
}
enum _TestHarnessStatus { testerCrashed, finished }
typedef Finalizer = Future<void> Function();
/// The flutter test platform used to integrate with package:test.
......@@ -226,7 +222,6 @@ class FlutterPlatform extends PlatformPlugin {
this.enableObservatory,
this.machine,
this.host,
this.port,
this.precompiledDillPath,
this.precompiledDillFiles,
this.updateGoldens,
......@@ -242,7 +237,6 @@ class FlutterPlatform extends PlatformPlugin {
final bool enableObservatory;
final bool machine;
final InternetAddress host;
final int port;
final String precompiledDillPath;
final Map<String, String> precompiledDillFiles;
final bool updateGoldens;
......@@ -251,7 +245,7 @@ class FlutterPlatform extends PlatformPlugin {
final FlutterProject flutterProject;
final String icudtlPath;
Directory fontsDirectory;
final FontConfigManager _fontConfigManager = FontConfigManager();
/// The test compiler produces dill files for each test main.
///
......@@ -297,6 +291,7 @@ class FlutterPlatform extends PlatformPlugin {
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>();
......@@ -339,16 +334,29 @@ class FlutterPlatform extends PlatformPlugin {
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);
TestDevice _createTestDevice(int ourTestCount) {
return FlutterTesterTestDevice(
id: ourTestCount,
platform: globals.platform,
fileSystem: globals.fs,
processManager: globals.processManager,
logger: globals.logger,
shellPath: shellPath,
enableObservatory: enableObservatory,
machine: machine,
debuggingOptions: debuggingOptions,
host: host,
buildTestAssets: buildTestAssets,
flutterProject: flutterProject,
icudtlPath: icudtlPath,
compileExpression: _compileExpressionService,
fontConfigManager: _fontConfigManager
);
}
Future<_AsyncError> _startTest(
String testPath,
StreamChannel<dynamic> controller,
StreamChannel<dynamic> testHarnessChannel,
int ourTestCount,
) async {
globals.printTrace('test $ourTestCount: starting test $testPath');
......@@ -356,42 +364,13 @@ class FlutterPlatform extends PlatformPlugin {
_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(() {
unawaited(testHarnessChannel.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.
......@@ -401,7 +380,7 @@ class FlutterPlatform extends PlatformPlugin {
} else if (precompiledDillFiles != null) {
mainDart = precompiledDillFiles[testPath];
}
mainDart ??= _createListenerDart(finalizers, ourTestCount, testPath, server);
mainDart ??= _createListenerDart(finalizers, ourTestCount, testPath);
if (precompiledDillPath == null && precompiledDillFiles == null) {
// Lazily instantiate compiler so it is built only if it is actually used.
......@@ -409,141 +388,66 @@ class FlutterPlatform extends PlatformPlugin {
mainDart = await compiler.compile(globals.fs.file(mainDart).uri);
if (mainDart == null) {
controller.sink.addError(_getErrorMessage('Compilation failed', testPath, shellPath));
testHarnessChannel.sink.addError('Compilation failed for testPath=$testPath');
return null;
}
}
final Process process = await _startProcess(
shellPath,
mainDart,
enableObservatory: enableObservatory,
serverPort: server.port,
globals.printTrace('test $ourTestCount: starting test device');
final TestDevice testDevice = _createTestDevice(ourTestCount);
final Future<StreamChannel<String>> remoteChannelFuture = testDevice.start(
compiledEntrypointPath: mainDart,
);
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),
testPath,
shellPath);
controller.sink.addError(message);
}
}
globals.printTrace('test $ourTestCount: ensuring test device is terminated.');
await testDevice.kill();
});
final Completer<Uri> gotProcessObservatoryUri = Completer<Uri>();
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.
final Uri ddsServiceUri = getDdsServiceUri();
_pipeStandardStreamsToConsole(
process,
reportObservatoryUri: (Uri detectedUri) async {
assert(!gotProcessObservatoryUri.isCompleted);
assert(debuggingOptions.hostVmServicePort == null ||
debuggingOptions.hostVmServicePort == detectedUri.port);
Uri forwardingUri;
if (!debuggingOptions.disableDds) {
final DartDevelopmentService dds = await DartDevelopmentService.startDartDevelopmentService(
detectedUri,
serviceUri: ddsServiceUri,
enableAuthCodes: !debuggingOptions.disableServiceAuthCodes,
ipv6: host.type == InternetAddressType.IPv6,
);
forwardingUri = dds.uri;
globals.printTrace('Dart Development Service started at ${dds.uri}, forwarding to VM service at ${dds.remoteVmServiceUri}.');
} else {
forwardingUri = detectedUri;
}
{
globals.printTrace('Connecting to service protocol: $forwardingUri');
final Future<vm_service.VmService> localVmService = connectToVmService(forwardingUri,
compileExpression: _compileExpressionService);
unawaited(localVmService.then((vm_service.VmService vmservice) {
globals.printTrace('Successfully connected to service protocol: $forwardingUri');
}));
}
if (debuggingOptions.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(' $forwardingUri');
globals.printStatus('You should first set appropriate breakpoints, then resume the test in the debugger.');
}
gotProcessObservatoryUri.complete(forwardingUri);
},
);
// 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 connection to result for test process at pid ${process.pid}');
final _TestHarnessStatus testHarnessStatus = await Future.any<_TestHarnessStatus>(<Future<_TestHarnessStatus>>[
process.exitCode.then<_TestHarnessStatus>((int exitCode) => _TestHarnessStatus.testerCrashed),
gotProcessObservatoryUri.future.then<_TestHarnessStatus>((Uri processObservatoryUri) {
// At this point, these things can happen:
// A. The test device could crash, in which case [testDevice.finished]
// will complete.
// B. The test device could connect to us, in which case
// [remoteChannelFuture] will complete.
globals.printTrace('test $ourTestCount: awaiting connection to test device');
await Future.any<void>(<Future<void>>[
testDevice.finished,
() async {
final Uri processObservatoryUri = await testDevice.observatoryUri;
if (processObservatoryUri != null) {
globals.printTrace('test $ourTestCount: Observatory uri is available at $processObservatoryUri');
} else {
globals.printTrace('test $ourTestCount: Observatory uri is not available');
}
watcher?.handleStartedProcess(ProcessEvent(ourTestCount, process, processObservatoryUri));
return webSocket.future.then<_TestHarnessStatus>((WebSocket remoteSocket) async {
globals.printTrace('test $ourTestCount: connected to test harness, now awaiting test result');
await _controlTests(
controller: controller,
remoteSocket: remoteSocket,
onError: (dynamic error, StackTrace stackTrace) {
// If you reach here, it's unlikely we're going to be able to really handle this well.
globals.printError('test: $testPath\nerror: $error');
if (!controllerSinkClosed) {
controller.sink.addError(error, stackTrace);
controller.sink.close();
} else {
globals.printError('unexpected error: $error');
}
}
);
await watcher?.handleFinishedTest(ProcessEvent(ourTestCount, process, processObservatoryUri));
return _TestHarnessStatus.finished;
});
})
]);
watcher?.handleStartedDevice(processObservatoryUri);
if (testHarnessStatus == _TestHarnessStatus.testerCrashed) {
globals.printTrace('test $ourTestCount: process with pid ${process.pid} crashed');
final int exitCode = await process.exitCode;
subprocessActive = false;
final String message = _getErrorMessage(
_getExitCodeMessage(exitCode),
testPath,
shellPath);
controller.sink.addError(message);
// Awaited for with 'sink.done' below in `finally`.
unawaited(controller.sink.close());
globals.printTrace('test $ourTestCount: waiting for controller sink to close');
await controller.sink.done;
await watcher?.handleTestCrashed(ProcessEvent(ourTestCount, process));
final StreamChannel<String> remoteChannel = await remoteChannelFuture;
globals.printTrace('test $ourTestCount: connected to test device, now awaiting test result');
await _pipeHarnessToRemote(
id: ourTestCount,
harnessChannel: testHarnessChannel,
remoteChannel: remoteChannel,
);
globals.printTrace('test $ourTestCount: finished');
await watcher?.handleFinishedTest(testDevice);
}()
]);
} on Exception catch (error, stackTrace) {
Object reportedError = error;
StackTrace reportedStackTrace = stackTrace;
if (error is TestDeviceException) {
reportedError = error.message;
reportedStackTrace = error.stackTrace;
}
} 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);
testHarnessChannel.sink.addError(reportedError, reportedStackTrace);
} else {
globals.printError('unhandled error during test:\n$testPath\n$error\n$stack');
outOfBandError ??= _AsyncError(error, stack);
globals.printError('unhandled error during test:\n$testPath\n$reportedError\n$reportedStackTrace');
outOfBandError ??= _AsyncError(reportedError, reportedStackTrace);
}
} finally {
globals.printTrace('test $ourTestCount: cleaning up...');
......@@ -554,7 +458,7 @@ class FlutterPlatform extends PlatformPlugin {
} 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);
testHarnessChannel.sink.addError(error, stack);
} else {
globals.printError('unhandled error during finalization of test:\n$testPath\n$error\n$stack');
outOfBandError ??= _AsyncError(error, stack);
......@@ -563,12 +467,11 @@ class FlutterPlatform extends PlatformPlugin {
}
if (!controllerSinkClosed) {
// Waiting below with await.
unawaited(controller.sink.close());
unawaited(testHarnessChannel.sink.close());
globals.printTrace('test $ourTestCount: waiting for controller sink to close');
await controller.sink.done;
await testHarnessChannel.sink.done;
}
}
assert(!subprocessActive);
assert(controllerSinkClosed);
if (outOfBandError != null) {
globals.printTrace('test $ourTestCount: finished with out-of-band failure');
......@@ -582,7 +485,6 @@ class FlutterPlatform extends PlatformPlugin {
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.');
......@@ -621,184 +523,13 @@ class FlutterPlatform extends PlatformPlugin {
);
}
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, {
bool enableObservatory = false,
int serverPort,
}) {
assert(executable != null); // Please provide the path to the shell in the SKY_SHELL environment variable.
assert(!debuggingOptions.startPaused || enableObservatory);
final int observatoryPort = debuggingOptions.disableDds ? debuggingOptions.hostVmServicePort : 0;
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 (debuggingOptions.startPaused) '--start-paused',
if (debuggingOptions.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=${debuggingOptions.buildInfo.packagesPath}',
if (debuggingOptions.nullAssertions)
'--dart-flags=--null_assertions',
...debuggingOptions.dartEntrypointArgs,
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) {
switch (exitCode) {
case 1:
return 'Shell subprocess cleanly reported an error. Check the logs above for an error message.';
case 0:
return 'Shell subprocess ended cleanly. Did main() call exit()?';
case -0x0f: // ProcessSignal.SIGTERM
return 'Shell subprocess crashed with SIGTERM ($exitCode).';
case -0x0b: // ProcessSignal.SIGSEGV
return 'Shell subprocess crashed with segmentation fault.';
case -0x06: // ProcessSignal.SIGABRT
return 'Shell subprocess crashed with SIGABRT ($exitCode).';
case -0x02: // ProcessSignal.SIGINT
return 'Shell subprocess terminated by ^C (SIGINT, $exitCode).';
default:
return 'Shell subprocess crashed with unexpected exit code $exitCode.';
}
}
@visibleForTesting
@protected
Uri getDdsServiceUri() {
return Uri(
scheme: 'http',
host: (host.type == InternetAddressType.IPv6 ?
InternetAddress.loopbackIPv6 :
InternetAddress.loopbackIPv4
).host,
port: debuggingOptions.hostVmServicePort ?? 0,
);
await _fontConfigManager.dispose();
}
}
......@@ -862,60 +593,29 @@ class _AsyncError {
final StackTrace stack;
}
/// Bridges the package:test controller and the remote tester.
/// Bridges the package:test harness and the remote device.
///
/// Sets up a that allows the package:test test [controller] to communicate with
/// a [remoteSocket] that runs the test. The returned future completes when
/// either side is closed, which also indicates when the tests have finished.
Future<void> _controlTests({
@required
StreamChannel<dynamic> controller,
@required
WebSocket remoteSocket,
@required
void Function(dynamic, StackTrace) onError,
/// The returned future completes when either side is closed, which also
/// indicates when the tests have finished.
Future<void> _pipeHarnessToRemote({
@required int id,
@required StreamChannel<dynamic> harnessChannel,
@required StreamChannel<String> remoteChannel,
}) async {
final Completer<void> harnessDone = Completer<void>();
final StreamSubscription<dynamic> harnessToTest =
controller.stream.listen(
(dynamic event) {
remoteSocket.add(json.encode(event));
},
onDone: harnessDone.complete,
onError: (dynamic error, StackTrace stack) {
globals.printError('test harness controller stream experienced an unexpected error');
onError(error, stack);
},
cancelOnError: true,
);
final Completer<void> testDone = Completer<void>();
final StreamSubscription<dynamic> testToHarness = remoteSocket.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) {
globals.printError('test socket stream experienced an unexpected error');
onError(error, stack);
},
cancelOnError: true,
);
globals.printTrace('waiting for test harness or tests to finish');
globals.printTrace('test $id: Waiting for test harness or tests to finish');
await Future.any<void>(<Future<void>>[
harnessDone.future.then<void>((void value) {
globals.printTrace('test process is no longer needed by test harness');
}),
testDone.future.then<void>((void value) {
globals.printTrace('test harness is no longer needed by test process');
}),
]);
await Future.wait<void>(<Future<void>>[
harnessToTest.cancel(),
testToHarness.cancel(),
harnessChannel.stream
.map<String>(json.encode)
.pipe(remoteChannel.sink)
.then<void>((void value) {
globals.printTrace('test $id: Test process is no longer needed by test harness');
}),
remoteChannel.stream
.map<dynamic>(json.decode)
.pipe(harnessChannel.sink)
.then<void>((void value) {
globals.printTrace('test $id: Test harness is no longer needed by test process');
}),
]);
}
// 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.
// @dart = 2.8
import 'dart:async';
import 'dart:io' as io; // ignore: dart_io_import;
import 'package:dds/dds.dart';
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import 'package:stream_channel/stream_channel.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/platform.dart';
import '../convert.dart';
import '../device.dart';
import '../project.dart';
import '../vmservice.dart';
import 'font_config_manager.dart';
import 'test_device.dart';
/// Implementation of [TestDevice] with the Flutter Tester over a [Process].
class FlutterTesterTestDevice extends TestDevice {
FlutterTesterTestDevice({
@required this.id,
@required this.platform,
@required this.fileSystem,
@required this.processManager,
@required this.logger,
@required this.shellPath,
@required this.debuggingOptions,
@required this.enableObservatory,
@required this.machine,
@required this.host,
@required this.buildTestAssets,
@required this.flutterProject,
@required this.icudtlPath,
@required this.compileExpression,
@required this.fontConfigManager,
}) : assert(shellPath != null), // Please provide the path to the shell in the SKY_SHELL environment variable.
assert(!debuggingOptions.startPaused || enableObservatory),
_gotProcessObservatoryUri = enableObservatory
? Completer<Uri>() : (Completer<Uri>()..complete(null));
/// Used for logging to identify the test that is currently being executed.
final int id;
final Platform platform;
final FileSystem fileSystem;
final ProcessManager processManager;
final Logger logger;
final String shellPath;
final DebuggingOptions debuggingOptions;
final bool enableObservatory;
final bool machine;
final InternetAddress host;
final bool buildTestAssets;
final FlutterProject flutterProject;
final String icudtlPath;
final CompileExpression compileExpression;
final FontConfigManager fontConfigManager;
final Completer<Uri> _gotProcessObservatoryUri;
final Completer<int> _exitCode = Completer<int>();
Process _process;
HttpServer _server;
@override
Future<StreamChannel<String>> start({@required String compiledEntrypointPath}) async {
assert(!_exitCode.isCompleted);
assert(_process == null);
assert(_server == null);
// Prepare our WebSocket server to talk to the engine subprocess.
// Let the server choose an unused port.
_server = await bind(host, /*port*/ 0);
logger.printTrace('test $id: test harness socket server is running at port:${_server.port}');
final List<String> command = <String>[
shellPath,
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.
'--observatory-port=${debuggingOptions.disableDds ? debuggingOptions.hostVmServicePort: 0}',
if (debuggingOptions.startPaused) '--start-paused',
if (debuggingOptions.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=${debuggingOptions.buildInfo.packagesPath}',
if (debuggingOptions.nullAssertions)
'--dart-flags=--null_assertions',
...debuggingOptions.dartEntrypointArgs,
compiledEntrypointPath,
];
// 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 = platform.environment.containsKey('FLUTTER_TEST')
? platform.environment['FLUTTER_TEST']
: 'true';
final Map<String, String> environment = <String, String>{
'FLUTTER_TEST': flutterTest,
'FONTCONFIG_FILE': fontConfigManager.fontConfigFile.path,
'SERVER_PORT': _server.port.toString(),
'APP_NAME': flutterProject?.manifest?.appName ?? '',
if (buildTestAssets)
'UNIT_TEST_ASSETS': fileSystem.path.join(flutterProject?.directory?.path ?? '', 'build', 'unit_test_assets'),
};
logger.printTrace('test $id: Starting flutter_tester process with command=$command, environment=$environment');
_process = await processManager.start(command, environment: environment);
// Unawaited to update state.
unawaited(_process.exitCode.then((int exitCode) {
logger.printTrace('test $id: flutter_tester process at pid ${_process.pid} exited with code=$exitCode');
_exitCode.complete(exitCode);
}));
logger.printTrace('test $id: Started flutter_tester process at pid ${_process.pid}');
// Pipe stdout and stderr from the subprocess to our printStatus console.
// We also keep track of what observatory port the engine used, if any.
_pipeStandardStreamsToConsole(
process: _process,
reportObservatoryUri: (Uri detectedUri) async {
assert(!_gotProcessObservatoryUri.isCompleted);
assert(debuggingOptions.hostVmServicePort == null ||
debuggingOptions.hostVmServicePort == detectedUri.port);
Uri forwardingUri;
if (!debuggingOptions.disableDds) {
logger.printTrace('test $id: Starting Dart Development Service');
final DartDevelopmentService dds = await startDds(detectedUri);
forwardingUri = dds.uri;
logger.printTrace('test $id: Dart Development Service started at ${dds.uri}, forwarding to VM service at ${dds.remoteVmServiceUri}.');
} else {
forwardingUri = detectedUri;
}
logger.printTrace('Connecting to service protocol: $forwardingUri');
final Future<vm_service.VmService> localVmService = connectToVmService(
forwardingUri,
compileExpression: compileExpression,
);
unawaited(localVmService.then((vm_service.VmService vmservice) {
logger.printTrace('test $id: Successfully connected to service protocol: $forwardingUri');
}));
if (debuggingOptions.startPaused && !machine) {
logger.printStatus('The test process has been started.');
logger.printStatus('You can now connect to it using observatory. To connect, load the following Web site in your browser:');
logger.printStatus(' $forwardingUri');
logger.printStatus('You should first set appropriate breakpoints, then resume the test in the debugger.');
}
_gotProcessObservatoryUri.complete(forwardingUri);
},
);
return remoteChannel;
}
@override
Future<Uri> get observatoryUri {
assert(_gotProcessObservatoryUri != null);
return _gotProcessObservatoryUri.future;
}
@override
Future<void> kill() async {
logger.printTrace('test $id: Terminating flutter_tester process');
_process?.kill(io.ProcessSignal.sigkill);
logger.printTrace('test $id: Shutting down test harness socket server');
await _server?.close(force: true);
await finished;
}
@override
Future<void> get finished async {
final int exitCode = await _exitCode.future;
// On Windows, the [exitCode] and the terminating signal have no correlation.
if (platform.isWindows) {
return;
}
// ProcessSignal.SIGKILL. Negative because signals are returned as negative
// exit codes.
if (exitCode == -9) {
// We expect SIGKILL (9) because we could have tried to [kill] it.
return;
}
throw TestDeviceException(_getExitCodeMessage(exitCode), StackTrace.current);
}
Uri get _ddsServiceUri {
return Uri(
scheme: 'http',
host: (host.type == InternetAddressType.IPv6 ?
InternetAddress.loopbackIPv6 :
InternetAddress.loopbackIPv4
).host,
port: debuggingOptions.hostVmServicePort ?? 0,
);
}
@visibleForTesting
@protected
Future<DartDevelopmentService> startDds(Uri uri) {
return DartDevelopmentService.startDartDevelopmentService(
uri,
serviceUri: _ddsServiceUri,
enableAuthCodes: !debuggingOptions.disableServiceAuthCodes,
ipv6: host.type == InternetAddressType.IPv6,
);
}
/// Binds an [HttpServer] serving from `host` on `port`.
///
/// Only intended to be overridden in tests.
@protected
@visibleForTesting
Future<HttpServer> bind(InternetAddress host, int port) => HttpServer.bind(host, port);
@protected
@visibleForTesting
Future<StreamChannel<String>> get remoteChannel async {
assert(_server != null);
try {
final HttpRequest firstRequest = await _server.first;
final WebSocket webSocket = await WebSocketTransformer.upgrade(firstRequest);
return _webSocketToStreamChannel(webSocket);
} on Exception catch (error, stackTrace) {
throw TestDeviceException('Unable to connect to flutter_tester process: $error', stackTrace);
}
}
@override
String toString() {
final String status = _process != null
? 'pid: ${_process.pid}, ${_exitCode.isCompleted ? 'exited' : 'running'}'
: 'not started';
return 'Flutter Tester ($status) for test $id';
}
void _pipeStandardStreamsToConsole({
@required Process process,
@required 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 {
logger.printTrace('test $id: Shell: $line');
if (line.startsWith(observatoryString)) {
try {
final Uri uri = Uri.parse(line.substring(observatoryString.length));
if (reportObservatoryUri != null) {
await reportObservatoryUri(uri);
}
} on Exception catch (error) {
logger.printError('Could not parse shell observatory port message: $error');
}
} else if (line != null) {
logger.printStatus('Shell: $line');
}
},
onError: (dynamic error) {
logger.printError('shell console stream for process pid ${process.pid} experienced an unexpected error: $error');
},
cancelOnError: true,
);
}
}
}
String _getExitCodeMessage(int exitCode) {
switch (exitCode) {
case 1:
return 'Shell subprocess cleanly reported an error. Check the logs above for an error message.';
case 0:
return 'Shell subprocess ended cleanly. Did main() call exit()?';
case -0x0f: // ProcessSignal.SIGTERM
return 'Shell subprocess crashed with SIGTERM ($exitCode).';
case -0x0b: // ProcessSignal.SIGSEGV
return 'Shell subprocess crashed with segmentation fault.';
case -0x06: // ProcessSignal.SIGABRT
return 'Shell subprocess crashed with SIGABRT ($exitCode).';
case -0x02: // ProcessSignal.SIGINT
return 'Shell subprocess terminated by ^C (SIGINT, $exitCode).';
default:
return 'Shell subprocess crashed with unexpected exit code $exitCode.';
}
}
StreamChannel<String> _webSocketToStreamChannel(WebSocket webSocket) {
final StreamChannelController<String> controller = StreamChannelController<String>();
controller.local.stream
.map<dynamic>((String message) => message as dynamic)
.pipe(webSocket);
webSocket
// We're only communicating with string encoded JSON.
.map<String>((dynamic message) => message as String)
.pipe(controller.local.sink);
return controller.foreign;
}
// 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.
// @dart = 2.8
import 'dart:async';
import '../base/file_system.dart';
import '../globals.dart' as globals;
/// Manages a Font configuration that can be shared across multiple tests.
class FontConfigManager {
Directory _fontsDirectory;
File _cachedFontConfig;
/// Returns a Font configuration 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<void> dispose() async {
if (_fontsDirectory != null) {
globals.printTrace('Deleting ${_fontsDirectory.path}...');
await _fontsDirectory.delete(recursive: true);
_fontsDirectory = null;
}
}
}
// 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.
// @dart = 2.8
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:stream_channel/stream_channel.dart';
/// A remote device where tests can be executed on.
///
/// Reusability of an instance across multiple runs is not guaranteed for all
/// implementations.
abstract class TestDevice {
/// Starts the test device with the provided entrypoint.
///
/// Returns a channel that can be used to communicate with the test process.
Future<StreamChannel<String>> start({@required String compiledEntrypointPath});
/// Should complete with null if the observatory is not enabled.
Future<Uri> get observatoryUri;
/// Terminates the test device.
///
/// A [TestDeviceException] can be thrown if it did not stop gracefully.
Future<void> kill();
/// Waits for the test device to stop.
///
/// A [TestDeviceException] can be thrown if it did not stop gracefully.
Future<void> get finished;
}
/// Thrown when the device encounters a problem.
class TestDeviceException implements Exception {
TestDeviceException(this.message, this.stackTrace);
final String message;
final StackTrace stackTrace;
@override
String toString() => 'TestDeviceException($message)';
}
......@@ -4,43 +4,27 @@
// @dart = 2.8
import '../base/io.dart' show Process;
import 'test_device.dart';
/// Callbacks for reporting progress while running tests.
abstract class TestWatcher {
/// Called after a child process starts.
/// Called after the test device starts.
///
/// If startPaused was true, the caller needs to resume in Observatory to
/// start running the tests.
void handleStartedProcess(ProcessEvent event) { }
void handleStartedDevice(Uri observatoryUri) { }
/// Called after the tests finish but before the process exits.
/// Called after the tests finish but before the test device exits.
///
/// The child process won't exit until this method completes.
/// Not called if the process died.
Future<void> handleFinishedTest(ProcessEvent event);
/// The test device won't exit until this method completes.
/// Not called if the test device died.
Future<void> handleFinishedTest(TestDevice testDevice);
/// Called when the test process crashed before connecting to test harness.
Future<void> handleTestCrashed(ProcessEvent event);
/// Called when the test device crashed before it could be connected to the
/// test harness.
Future<void> handleTestCrashed(TestDevice testDevice);
/// Called if we timed out waiting for the test process to connect to test
/// Called if we timed out waiting for the test device to connect to test
/// harness.
Future<void> handleTestTimedOut(ProcessEvent event);
}
/// Describes a child process started during testing.
class ProcessEvent {
ProcessEvent(this.childIndex, this.process, [this.observatoryUri]);
/// The index assigned when the child process was launched.
///
/// Indexes are assigned consecutively starting from zero.
/// When debugging, there should only be one child process so this will
/// always be zero.
final int childIndex;
final Process process;
/// The observatory URL or null if not debugging.
final Uri observatoryUri;
Future<void> handleTestTimedOut(TestDevice testDevice);
}
......@@ -4,16 +4,12 @@
// @dart = 2.8
import 'dart:async';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/test/flutter_platform.dart';
import 'package:meta/meta.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import 'package:test_core/backend.dart'; // ignore: deprecated_member_use
......@@ -41,6 +37,7 @@ void main() {
BuildInfo.debug,
hostVmServicePort: 1234,
),
enableObservatory: false,
);
flutterPlatform.loadChannel('test1.dart', MockSuitePlatform());
......@@ -56,6 +53,7 @@ void main() {
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
shellPath: '/',
precompiledDillPath: 'example.dill',
enableObservatory: false,
);
flutterPlatform.loadChannel('test1.dart', MockSuitePlatform());
......@@ -65,115 +63,6 @@ void main() {
ProcessManager: () => FakeProcessManager.any(),
});
group('Observatory and DDS setup', () {
Platform fakePlatform;
ProcessManager fakeProcessManager;
FlutterPlatform flutterPlatform;
final Map<Type, Generator> contextOverrides = <Type, Generator>{
Platform: () => fakePlatform,
ProcessManager: () => fakeProcessManager,
FileSystem: () => fileSystem,
};
setUp(() {
fakePlatform = FakePlatform(operatingSystem: 'linux', environment: <String, String>{});
fakeProcessManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>[
'/',
'--observatory-port=0',
'--ipv6',
'--enable-checked-mode',
'--verify-entry-points',
'--enable-software-rendering',
'--skia-deterministic-rendering',
'--enable-dart-profiling',
'--non-interactive',
'--use-test-fonts',
'--packages=.dart_tool/package_config.json',
'example.dill'
],
stdout: 'success',
stderr: 'failure',
exitCode: 0,
)
]);
flutterPlatform = TestObservatoryFlutterPlatform();
});
testUsingContext('skips setting observatory port and uses the input port for for DDS instead', () async {
flutterPlatform.loadChannel('test1.dart', MockSuitePlatform());
final TestObservatoryFlutterPlatform testPlatform = flutterPlatform as TestObservatoryFlutterPlatform;
await testPlatform.ddsServiceUriFuture().then((Uri uri) => expect(uri.port, 1234));
}, overrides: contextOverrides);
});
group('The FLUTTER_TEST environment variable is passed to the test process', () {
FakePlatform fakePlatform;
MockProcessManager mockProcessManager;
FlutterPlatform flutterPlatform;
final Map<Type, Generator> contextOverrides = <Type, Generator>{
Platform: () => fakePlatform,
ProcessManager: () => mockProcessManager,
FileSystem: () => fileSystem,
};
setUp(() {
// Not Windows
fakePlatform = FakePlatform(operatingSystem: 'linux');
mockProcessManager = MockProcessManager();
flutterPlatform = TestFlutterPlatform();
});
Future<Map<String, String>> captureEnvironment() async {
flutterPlatform.loadChannel('test1.dart', MockSuitePlatform());
when(mockProcessManager.start(
any,
environment: anyNamed('environment')),
).thenAnswer((_) {
return Future<Process>.value(MockProcess());
});
await untilCalled(mockProcessManager.start(any, environment: anyNamed('environment')));
final VerificationResult toVerify = verify(mockProcessManager.start(
any,
environment: captureAnyNamed('environment'),
));
expect(toVerify.captured, hasLength(1));
expect(toVerify.captured.first, isA<Map<String, String>>());
return toVerify.captured.first as Map<String, String>;
}
testUsingContext('as true when not originally set', () async {
fakePlatform.environment = <String, String>{};
final Map<String, String> capturedEnvironment = await captureEnvironment();
expect(capturedEnvironment['FLUTTER_TEST'], 'true');
}, overrides: contextOverrides);
testUsingContext('as true when set to true', () async {
fakePlatform.environment = <String, String>{'FLUTTER_TEST': 'true'};
final Map<String, String> capturedEnvironment = await captureEnvironment();
expect(capturedEnvironment['FLUTTER_TEST'], 'true');
}, overrides: contextOverrides);
testUsingContext('as false when set to false', () async {
fakePlatform.environment = <String, String>{'FLUTTER_TEST': 'false'};
final Map<String, String> capturedEnvironment = await captureEnvironment();
expect(capturedEnvironment['FLUTTER_TEST'], 'false');
}, overrides: contextOverrides);
testUsingContext('unchanged when set', () async {
fakePlatform.environment = <String, String>{'FLUTTER_TEST': 'neither true nor false'};
final Map<String, String> capturedEnvironment = await captureEnvironment();
expect(capturedEnvironment['FLUTTER_TEST'], 'neither true nor false');
}, overrides: contextOverrides);
testUsingContext('as null when set to null', () async {
fakePlatform.environment = <String, String>{'FLUTTER_TEST': null};
final Map<String, String> capturedEnvironment = await captureEnvironment();
expect(capturedEnvironment['FLUTTER_TEST'], null);
}, overrides: contextOverrides);
});
testUsingContext('installHook creates a FlutterPlatform', () {
expect(() => installHook(
shellPath: 'abc',
......@@ -206,7 +95,6 @@ void main() {
),
enableObservatory: true,
machine: true,
port: 100,
precompiledDillPath: 'def',
precompiledDillFiles: expectedPrecompiledDillFiles,
updateGoldens: true,
......@@ -225,7 +113,6 @@ void main() {
expect(flutterPlatform.debuggingOptions.hostVmServicePort, equals(200));
expect(flutterPlatform.enableObservatory, equals(true));
expect(flutterPlatform.machine, equals(true));
expect(flutterPlatform.port, equals(100));
expect(flutterPlatform.host, InternetAddress.loopbackIPv6);
expect(flutterPlatform.precompiledDillPath, equals('def'));
expect(flutterPlatform.precompiledDillFiles, expectedPrecompiledDillFiles);
......@@ -234,113 +121,20 @@ void main() {
expect(flutterPlatform.icudtlPath, equals('ghi'));
});
});
FakeProcessManager fakeProcessManager;
testUsingContext('Can pass additional arguments to tester binary', () async {
final TestFlutterPlatform platform = TestFlutterPlatform(<String>['--foo', '--bar']);
platform.loadChannel('test1.dart', MockSuitePlatform());
await null;
expect(fakeProcessManager.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () {
return fakeProcessManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>[
'/',
'--disable-observatory',
'--ipv6',
'--enable-checked-mode',
'--verify-entry-points',
'--enable-software-rendering',
'--skia-deterministic-rendering',
'--enable-dart-profiling',
'--non-interactive',
'--use-test-fonts',
'--packages=.dart_tool/package_config.json',
'--foo',
'--bar',
'example.dill'
],
stdout: 'success',
stderr: 'failure',
exitCode: 0,
)
]);
}
});
}
class MockSuitePlatform extends Mock implements SuitePlatform {}
class MockProcessManager extends Mock implements ProcessManager {}
class MockProcess extends Mock implements Process {}
class MockHttpServer extends Mock implements HttpServer {}
// A FlutterPlatform with enough fields set to load and start a test.
//
// Uses a mock HttpServer. We don't want to bind random ports in our CI hosts.
class TestFlutterPlatform extends FlutterPlatform {
TestFlutterPlatform([List<String> dartEntrypointArgs = const <String>[]]) : super(
TestFlutterPlatform() : super(
shellPath: '/',
debuggingOptions: DebuggingOptions.enabled(
const BuildInfo(BuildMode.debug, '', treeShakeIcons: false, packagesPath: '.dart_tool/package_config.json'),
startPaused: false,
disableDds: true,
dartEntrypointArgs: dartEntrypointArgs,
const BuildInfo(
BuildMode.debug,
'',
treeShakeIcons: false,
),
),
precompiledDillPath: 'example.dill',
host: InternetAddress.loopbackIPv6,
port: 0,
updateGoldens: false,
enableObservatory: false,
buildTestAssets: false,
);
@override
@protected
Future<HttpServer> bind(InternetAddress host, int port) async => MockHttpServer();
}
// A FlutterPlatform that enables observatory.
//
// Uses a mock HttpServer. We don't want to bind random ports in our CI hosts.
class TestObservatoryFlutterPlatform extends FlutterPlatform {
TestObservatoryFlutterPlatform() : super(
shellPath: '/',
debuggingOptions: DebuggingOptions.enabled(
const BuildInfo(BuildMode.debug, '', treeShakeIcons: false, packagesPath: '.dart_tool/package_config.json'),
startPaused: false,
disableDds: false,
disableServiceAuthCodes: false,
hostVmServicePort: 1234,
),
precompiledDillPath: 'example.dill',
host: InternetAddress.loopbackIPv6,
port: 0,
updateGoldens: false,
enableObservatory: true,
buildTestAssets: false,
);
final Completer<Uri> _ddsServiceUriCompleter = Completer<Uri>();
Future<Uri> ddsServiceUriFuture() {
return _ddsServiceUriCompleter.future;
}
@override
@protected
Future<HttpServer> bind(InternetAddress host, int port) async => MockHttpServer();
@override
Uri getDdsServiceUri() {
final Uri result = super.getDdsServiceUri();
_ddsServiceUriCompleter.complete(result);
return result;
}
}
// 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.
// @dart = 2.8
import 'dart:async';
import 'package:dds/dds.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/test/font_config_manager.dart';
import 'package:flutter_tools/src/test/flutter_tester_device.dart';
import 'package:meta/meta.dart';
import 'package:mockito/mockito.dart';
import 'package:stream_channel/stream_channel.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/platform.dart';
import '../src/common.dart';
import '../src/context.dart';
void main() {
FakePlatform platform;
FileSystem fileSystem;
ProcessManager processManager;
FlutterTesterTestDevice device;
setUp(() {
fileSystem = MemoryFileSystem.test();
// Not Windows.
platform = FakePlatform(
operatingSystem: 'linux',
environment: <String, String>{},
);
processManager = FakeProcessManager.any();
});
FlutterTesterTestDevice createDevice({
List<String> dartEntrypointArgs = const <String>[],
bool enableObservatory = false,
}) =>
TestFlutterTesterDevice(
platform: platform,
fileSystem: fileSystem,
processManager: processManager,
enableObservatory: enableObservatory,
dartEntrypointArgs: dartEntrypointArgs,
);
group('The FLUTTER_TEST environment variable is passed to the test process', () {
setUp(() {
processManager = MockProcessManager();
device = createDevice();
fileSystem
.file('.dart_tool/package_config.json')
..createSync(recursive: true)
..writeAsStringSync('{"configVersion":2,"packages":[]}');
});
Future<Map<String, String>> captureEnvironment() async {
final Future<StreamChannel<String>> deviceStarted = device.start(
compiledEntrypointPath: 'example.dill',
);
when(processManager.start(
any,
environment: anyNamed('environment')),
).thenAnswer((_) {
return Future<Process>.value(MockProcess());
});
await untilCalled(processManager.start(any, environment: anyNamed('environment')));
final VerificationResult toVerify = verify(processManager.start(
any,
environment: captureAnyNamed('environment'),
));
expect(toVerify.captured, hasLength(1));
expect(toVerify.captured.first, isA<Map<String, String>>());
await deviceStarted;
return toVerify.captured.first as Map<String, String>;
}
testUsingContext('as true when not originally set', () async {
final Map<String, String> capturedEnvironment = await captureEnvironment();
expect(capturedEnvironment['FLUTTER_TEST'], 'true');
});
testUsingContext('as true when set to true', () async {
platform.environment = <String, String>{'FLUTTER_TEST': 'true'};
final Map<String, String> capturedEnvironment = await captureEnvironment();
expect(capturedEnvironment['FLUTTER_TEST'], 'true');
});
testUsingContext('as false when set to false', () async {
platform.environment = <String, String>{'FLUTTER_TEST': 'false'};
final Map<String, String> capturedEnvironment = await captureEnvironment();
expect(capturedEnvironment['FLUTTER_TEST'], 'false');
});
testUsingContext('unchanged when set', () async {
platform.environment = <String, String>{'FLUTTER_TEST': 'neither true nor false'};
final Map<String, String> capturedEnvironment = await captureEnvironment();
expect(capturedEnvironment['FLUTTER_TEST'], 'neither true nor false');
});
testUsingContext('as null when set to null', () async {
platform.environment = <String, String>{'FLUTTER_TEST': null};
final Map<String, String> capturedEnvironment = await captureEnvironment();
expect(capturedEnvironment['FLUTTER_TEST'], null);
});
});
group('Dart Entrypoint Args', () {
setUp(() {
processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>[
'/',
'--disable-observatory',
'--ipv6',
'--enable-checked-mode',
'--verify-entry-points',
'--enable-software-rendering',
'--skia-deterministic-rendering',
'--enable-dart-profiling',
'--non-interactive',
'--use-test-fonts',
'--packages=.dart_tool/package_config.json',
'--foo',
'--bar',
'example.dill'
],
stdout: 'success',
stderr: 'failure',
exitCode: 0,
)
]);
device = createDevice(dartEntrypointArgs: <String>['--foo', '--bar']);
});
testUsingContext('Can pass additional arguments to tester binary', () async {
await device.start(compiledEntrypointPath: 'example.dill');
expect((processManager as FakeProcessManager).hasRemainingExpectations, false);
});
});
group('DDS', () {
setUp(() {
processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>[
'/',
'--observatory-port=0',
'--ipv6',
'--enable-checked-mode',
'--verify-entry-points',
'--enable-software-rendering',
'--skia-deterministic-rendering',
'--enable-dart-profiling',
'--non-interactive',
'--use-test-fonts',
'--packages=.dart_tool/package_config.json',
'example.dill'
],
stdout: 'Observatory listening on http://localhost:1234',
stderr: 'failure',
exitCode: 0,
)
]);
device = createDevice(enableObservatory: true);
});
testUsingContext('skips setting observatory port and uses the input port for for DDS instead', () async {
await device.start(compiledEntrypointPath: 'example.dill');
await device.observatoryUri;
final Uri uri = await (device as TestFlutterTesterDevice).ddsServiceUriFuture();
expect(uri.port, 1234);
});
});
}
/// A Flutter Tester device.
///
/// Uses a mock HttpServer. We don't want to bind random ports in our CI hosts.
class TestFlutterTesterDevice extends FlutterTesterTestDevice {
TestFlutterTesterDevice({
@required Platform platform,
@required FileSystem fileSystem,
@required ProcessManager processManager,
@required bool enableObservatory,
@required List<String> dartEntrypointArgs,
}) : super(
id: 999,
shellPath: '/',
platform: platform,
fileSystem: fileSystem,
processManager: processManager,
logger: MockLogger(),
debuggingOptions: DebuggingOptions.enabled(
const BuildInfo(
BuildMode.debug,
'',
treeShakeIcons: false,
packagesPath: '.dart_tool/package_config.json',
),
startPaused: false,
disableDds: false,
disableServiceAuthCodes: false,
hostVmServicePort: 1234,
nullAssertions: false,
dartEntrypointArgs: dartEntrypointArgs,
),
enableObservatory: enableObservatory,
machine: false,
host: InternetAddress.loopbackIPv6,
buildTestAssets: false,
flutterProject: null,
icudtlPath: null,
compileExpression: null,
fontConfigManager: FontConfigManager(),
);
final Completer<Uri> _ddsServiceUriCompleter = Completer<Uri>();
Future<Uri> ddsServiceUriFuture() => _ddsServiceUriCompleter.future;
@override
Future<DartDevelopmentService> startDds(Uri uri) async {
_ddsServiceUriCompleter.complete(uri);
final MockDartDevelopmentService mock = MockDartDevelopmentService();
when(mock.uri).thenReturn(Uri.parse('http://localhost:${debuggingOptions.hostVmServicePort}'));
return mock;
}
@override
Future<HttpServer> bind(InternetAddress host, int port) async => MockHttpServer();
@override
Future<StreamChannel<String>> get remoteChannel async => StreamChannelController<String>().foreign;
}
class MockDartDevelopmentService extends Mock implements DartDevelopmentService {}
class MockHttpServer extends Mock implements HttpServer {}
class MockLogger extends Mock implements Logger {}
class MockProcessManager extends Mock implements ProcessManager {}
class MockProcess extends Mock implements Process {
@override
Future<int> get exitCode async => 0;
@override
Stream<List<int>> get stdout => const Stream<List<int>>.empty();
@override
Stream<List<int>> get stderr => const Stream<List<int>>.empty();
}
......@@ -5,19 +5,22 @@
// @dart = 2.8
import 'package:flutter_tools/src/test/event_printer.dart';
import 'package:flutter_tools/src/test/watcher.dart';
import 'package:flutter_tools/src/test/test_device.dart';
import 'package:mockito/mockito.dart';
import '../../src/common.dart';
import '../../src/fakes.dart';
void main() {
testWithoutContext('EventPrinter handles a null parent', () {
final EventPrinter eventPrinter = EventPrinter(out: StringBuffer());
final ProcessEvent processEvent = ProcessEvent(0, FakeProcess());
final _Device device = _Device();
final Uri observatoryUri = Uri.parse('http://localhost:1234');
expect(() => eventPrinter.handleFinishedTest(processEvent), returnsNormally);
expect(() => eventPrinter.handleStartedProcess(processEvent), returnsNormally);
expect(() => eventPrinter.handleTestCrashed(processEvent), returnsNormally);
expect(() => eventPrinter.handleTestTimedOut(processEvent), returnsNormally);
expect(() => eventPrinter.handleFinishedTest(device), returnsNormally);
expect(() => eventPrinter.handleStartedDevice(observatoryUri), returnsNormally);
expect(() => eventPrinter.handleTestCrashed(device), returnsNormally);
expect(() => eventPrinter.handleTestTimedOut(device), returnsNormally);
});
}
class _Device extends Mock implements TestDevice {}
......@@ -168,7 +168,7 @@ void main() {
extraArguments: const <String>['--verbose']);
final String stdout = result.stdout as String;
if ((!stdout.contains('+1: All tests passed')) ||
(!stdout.contains('test 0: starting shell process')) ||
(!stdout.contains('test 0: Starting flutter_tester process with command')) ||
(!stdout.contains('test 0: deleting temporary directory')) ||
(!stdout.contains('test 0: finished')) ||
(!stdout.contains('test package returned with exit code 0'))) {
......@@ -185,7 +185,7 @@ void main() {
extraArguments: const <String>['--verbose']);
final String stdout = result.stdout as String;
if ((!stdout.contains('+2: All tests passed')) ||
(!stdout.contains('test 0: starting shell process')) ||
(!stdout.contains('test 0: Starting flutter_tester process with command')) ||
(!stdout.contains('test 0: deleting temporary directory')) ||
(!stdout.contains('test 0: finished')) ||
(!stdout.contains('test package returned with exit code 0'))) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment