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'; ...@@ -15,6 +15,7 @@ import '../base/utils.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
import '../vmservice.dart'; import '../vmservice.dart';
import 'test_device.dart';
import 'watcher.dart'; import 'watcher.dart';
/// A class that's used to collect coverage data during tests. /// A class that's used to collect coverage data during tests.
...@@ -27,9 +28,9 @@ class CoverageCollector extends TestWatcher { ...@@ -27,9 +28,9 @@ class CoverageCollector extends TestWatcher {
bool Function(String) libraryPredicate; bool Function(String) libraryPredicate;
@override @override
Future<void> handleFinishedTest(ProcessEvent event) async { Future<void> handleFinishedTest(TestDevice testDevice) async {
_logMessage('test ${event.childIndex}: collecting coverage'); _logMessage('Starting coverage collection');
await collectCoverage(event.process, event.observatoryUri); await collectCoverage(testDevice);
} }
void _logMessage(String line, { bool error = false }) { void _logMessage(String line, { bool error = false }) {
...@@ -81,34 +82,41 @@ class CoverageCollector extends TestWatcher { ...@@ -81,34 +82,41 @@ class CoverageCollector extends TestWatcher {
/// has been run to completion so that all coverage data has been recorded. /// has been run to completion so that all coverage data has been recorded.
/// ///
/// The returned [Future] completes when the coverage is collected. /// The returned [Future] completes when the coverage is collected.
Future<void> collectCoverage(Process process, Uri observatoryUri) async { Future<void> collectCoverage(TestDevice testDevice) async {
assert(process != null); assert(testDevice != null);
assert(observatoryUri != null);
final int pid = process.pid;
_logMessage('pid $pid: collecting coverage data from $observatoryUri...');
Map<String, dynamic> data; Map<String, dynamic> data;
final Future<void> processComplete = process.exitCode
.then<void>((int code) { final Future<void> processComplete = testDevice.finished.catchError(
throw Exception('Failed to collect coverage, process terminated prematurely with exit code $code.'); (Object error) => throw Exception(
}); 'Failed to collect coverage, test device terminated prematurely with '
final Future<void> collectionComplete = collect(observatoryUri, libraryPredicate) 'error: ${(error as TestDeviceException).message}.'),
.then<void>((Map<String, dynamic> result) { test: (Object error) => error is TestDeviceException,
if (result == null) { );
throw Exception('Failed to collect coverage.');
} final Future<void> collectionComplete = testDevice.observatoryUri
data = result; .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 ]); await Future.any<void>(<Future<void>>[ processComplete, collectionComplete ]);
assert(data != null); assert(data != null);
_logMessage('pid $pid ($observatoryUri): collected coverage data; merging...'); _logMessage('Merging coverage data...');
_addHitmap(await coverage.createHitmap( _addHitmap(await coverage.createHitmap(
data['coverage'] as List<Map<String, dynamic>>, data['coverage'] as List<Map<String, dynamic>>,
packagesPath: packagesPath, packagesPath: packagesPath,
checkIgnoredLines: true, 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 /// Returns a future that will complete with the formatted coverage data
...@@ -188,10 +196,10 @@ class CoverageCollector extends TestWatcher { ...@@ -188,10 +196,10 @@ class CoverageCollector extends TestWatcher {
} }
@override @override
Future<void> handleTestCrashed(ProcessEvent event) async { } Future<void> handleTestCrashed(TestDevice testDevice) async { }
@override @override
Future<void> handleTestTimedOut(ProcessEvent event) async { } Future<void> handleTestTimedOut(TestDevice testDevice) async { }
} }
Future<vm_service.VmService> _defaultConnect(Uri serviceUri) { Future<vm_service.VmService> _defaultConnect(Uri serviceUri) {
......
...@@ -6,6 +6,8 @@ ...@@ -6,6 +6,8 @@
import '../convert.dart'; import '../convert.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
import 'test_device.dart';
import 'watcher.dart'; import 'watcher.dart';
/// Prints JSON events when running a test in --machine mode. /// Prints JSON events when running a test in --machine mode.
...@@ -18,25 +20,25 @@ class EventPrinter extends TestWatcher { ...@@ -18,25 +20,25 @@ class EventPrinter extends TestWatcher {
final TestWatcher _parent; final TestWatcher _parent;
@override @override
void handleStartedProcess(ProcessEvent event) { void handleStartedDevice(Uri observatoryUri) {
_sendEvent('test.startedProcess', _sendEvent('test.startedProcess',
<String, dynamic>{'observatoryUri': event.observatoryUri.toString()}); <String, dynamic>{'observatoryUri': observatoryUri.toString()});
_parent?.handleStartedProcess(event); _parent?.handleStartedDevice(observatoryUri);
} }
@override @override
Future<void> handleTestCrashed(ProcessEvent event) async { Future<void> handleTestCrashed(TestDevice testDevice) async {
return _parent?.handleTestCrashed(event); return _parent?.handleTestCrashed(testDevice);
} }
@override @override
Future<void> handleTestTimedOut(ProcessEvent event) async { Future<void> handleTestTimedOut(TestDevice testDevice) async {
return _parent?.handleTestTimedOut(event); return _parent?.handleTestTimedOut(testDevice);
} }
@override @override
Future<void> handleFinishedTest(ProcessEvent event) async { Future<void> handleFinishedTest(TestDevice testDevice) async {
return _parent?.handleFinishedTest(event); return _parent?.handleFinishedTest(testDevice);
} }
void _sendEvent(String name, [ dynamic params ]) { void _sendEvent(String name, [ dynamic params ]) {
......
...@@ -5,14 +5,11 @@ ...@@ -5,14 +5,11 @@
// @dart = 2.8 // @dart = 2.8
import 'dart:async'; import 'dart:async';
import 'dart:io' as io; // ignore: dart_io_import;
import 'package:dds/dds.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:package_config/package_config.dart'; import 'package:package_config/package_config.dart';
import 'package:stream_channel/stream_channel.dart'; import 'package:stream_channel/stream_channel.dart';
import 'package:test_core/src/platform.dart'; // ignore: implementation_imports 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/common.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
...@@ -24,9 +21,12 @@ import '../device.dart'; ...@@ -24,9 +21,12 @@ import '../device.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
import '../project.dart'; import '../project.dart';
import '../test/test_wrapper.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_compiler.dart';
import 'test_config.dart'; import 'test_config.dart';
import 'test_device.dart';
import 'watcher.dart'; import 'watcher.dart';
/// The address at which our WebSocket server resides and at which the sky_shell /// The address at which our WebSocket server resides and at which the sky_shell
...@@ -50,7 +50,6 @@ FlutterPlatform installHook({ ...@@ -50,7 +50,6 @@ FlutterPlatform installHook({
TestWatcher watcher, TestWatcher watcher,
bool enableObservatory = false, bool enableObservatory = false,
bool machine = false, bool machine = false,
int port = 0,
String precompiledDillPath, String precompiledDillPath,
Map<String, String> precompiledDillFiles, Map<String, String> precompiledDillFiles,
bool updateGoldens = false, bool updateGoldens = false,
...@@ -80,7 +79,6 @@ FlutterPlatform installHook({ ...@@ -80,7 +79,6 @@ FlutterPlatform installHook({
machine: machine, machine: machine,
enableObservatory: enableObservatory, enableObservatory: enableObservatory,
host: _kHosts[serverType], host: _kHosts[serverType],
port: port,
precompiledDillPath: precompiledDillPath, precompiledDillPath: precompiledDillPath,
precompiledDillFiles: precompiledDillFiles, precompiledDillFiles: precompiledDillFiles,
updateGoldens: updateGoldens, updateGoldens: updateGoldens,
...@@ -178,17 +176,16 @@ void catchIsolateErrors() { ...@@ -178,17 +176,16 @@ void catchIsolateErrors() {
}); });
} }
void main() { void main() {
String serverPort = Platform.environment['SERVER_PORT'] ?? ''; String serverPort = Platform.environment['SERVER_PORT'] ?? '';
String server = Uri.decodeComponent('$encodedWebsocketUrl:\$serverPort'); String server = Uri.decodeComponent('$encodedWebsocketUrl:\$serverPort');
StreamChannel<dynamic> channel = serializeSuite(() { StreamChannel<dynamic> testChannel = serializeSuite(() {
catchIsolateErrors(); catchIsolateErrors();
'''); ''');
if (flutterTestDep) { if (flutterTestDep) {
buffer.write(''' buffer.write('''
goldenFileComparator = LocalFileComparator(Uri.parse('$testUrl')); goldenFileComparator = LocalFileComparator(Uri.parse('$testUrl'));
autoUpdateGoldenFiles = $updateGoldens; autoUpdateGoldenFiles = $updateGoldens;
'''); ''');
} }
if (testConfigFile != null) { if (testConfigFile != null) {
...@@ -203,18 +200,17 @@ autoUpdateGoldenFiles = $updateGoldens; ...@@ -203,18 +200,17 @@ autoUpdateGoldenFiles = $updateGoldens;
buffer.write(''' buffer.write('''
}); });
WebSocket.connect(server).then((WebSocket socket) { WebSocket.connect(server).then((WebSocket socket) {
socket.map((dynamic x) { socket.map((dynamic message) {
return json.decode(x as String); // We're only communicating with string encoded JSON.
}).pipe(channel.sink); return json.decode(message as String);
socket.addStream(channel.stream.map(json.encode)); }).pipe(testChannel.sink);
socket.addStream(testChannel.stream.map(json.encode));
}); });
} }
'''); ''');
return buffer.toString(); return buffer.toString();
} }
enum _TestHarnessStatus { testerCrashed, finished }
typedef Finalizer = Future<void> Function(); typedef Finalizer = Future<void> Function();
/// The flutter test platform used to integrate with package:test. /// The flutter test platform used to integrate with package:test.
...@@ -226,7 +222,6 @@ class FlutterPlatform extends PlatformPlugin { ...@@ -226,7 +222,6 @@ class FlutterPlatform extends PlatformPlugin {
this.enableObservatory, this.enableObservatory,
this.machine, this.machine,
this.host, this.host,
this.port,
this.precompiledDillPath, this.precompiledDillPath,
this.precompiledDillFiles, this.precompiledDillFiles,
this.updateGoldens, this.updateGoldens,
...@@ -242,7 +237,6 @@ class FlutterPlatform extends PlatformPlugin { ...@@ -242,7 +237,6 @@ class FlutterPlatform extends PlatformPlugin {
final bool enableObservatory; final bool enableObservatory;
final bool machine; final bool machine;
final InternetAddress host; final InternetAddress host;
final int port;
final String precompiledDillPath; final String precompiledDillPath;
final Map<String, String> precompiledDillFiles; final Map<String, String> precompiledDillFiles;
final bool updateGoldens; final bool updateGoldens;
...@@ -251,7 +245,7 @@ class FlutterPlatform extends PlatformPlugin { ...@@ -251,7 +245,7 @@ class FlutterPlatform extends PlatformPlugin {
final FlutterProject flutterProject; final FlutterProject flutterProject;
final String icudtlPath; final String icudtlPath;
Directory fontsDirectory; final FontConfigManager _fontConfigManager = FontConfigManager();
/// The test compiler produces dill files for each test main. /// The test compiler produces dill files for each test main.
/// ///
...@@ -297,6 +291,7 @@ class FlutterPlatform extends PlatformPlugin { ...@@ -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.'); throwToolExit('installHook() was called with a precompiled test entry-point, but then more than one test suite was run.');
} }
} }
final int ourTestCount = _testCount; final int ourTestCount = _testCount;
_testCount += 1; _testCount += 1;
final StreamController<dynamic> localController = StreamController<dynamic>(); final StreamController<dynamic> localController = StreamController<dynamic>();
...@@ -339,16 +334,29 @@ class FlutterPlatform extends PlatformPlugin { ...@@ -339,16 +334,29 @@ class FlutterPlatform extends PlatformPlugin {
throw 'Failed to compile $expression'; throw 'Failed to compile $expression';
} }
/// Binds an [HttpServer] serving from `host` on `port`. TestDevice _createTestDevice(int ourTestCount) {
/// return FlutterTesterTestDevice(
/// Only intended to be overridden in tests for [FlutterPlatform]. id: ourTestCount,
@protected platform: globals.platform,
@visibleForTesting fileSystem: globals.fs,
Future<HttpServer> bind(InternetAddress host, int port) => HttpServer.bind(host, port); 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( Future<_AsyncError> _startTest(
String testPath, String testPath,
StreamChannel<dynamic> controller, StreamChannel<dynamic> testHarnessChannel,
int ourTestCount, int ourTestCount,
) async { ) async {
globals.printTrace('test $ourTestCount: starting test $testPath'); globals.printTrace('test $ourTestCount: starting test $testPath');
...@@ -356,42 +364,13 @@ class FlutterPlatform extends PlatformPlugin { ...@@ -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 _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. final List<Finalizer> finalizers = <Finalizer>[]; // Will be run in reverse order.
bool subprocessActive = false;
bool controllerSinkClosed = false; bool controllerSinkClosed = false;
try { try {
// Callback can't throw since it's just setting a variable. // Callback can't throw since it's just setting a variable.
unawaited(controller.sink.done.whenComplete(() { unawaited(testHarnessChannel.sink.done.whenComplete(() {
controllerSinkClosed = true; 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 a kernel file is given, then use that to launch the test.
// If mapping is provided, look kernel file from mapping. // If mapping is provided, look kernel file from mapping.
// If all fails, create a "listener" dart that invokes actual test. // If all fails, create a "listener" dart that invokes actual test.
...@@ -401,7 +380,7 @@ class FlutterPlatform extends PlatformPlugin { ...@@ -401,7 +380,7 @@ class FlutterPlatform extends PlatformPlugin {
} else if (precompiledDillFiles != null) { } else if (precompiledDillFiles != null) {
mainDart = precompiledDillFiles[testPath]; mainDart = precompiledDillFiles[testPath];
} }
mainDart ??= _createListenerDart(finalizers, ourTestCount, testPath, server); mainDart ??= _createListenerDart(finalizers, ourTestCount, testPath);
if (precompiledDillPath == null && precompiledDillFiles == null) { if (precompiledDillPath == null && precompiledDillFiles == null) {
// Lazily instantiate compiler so it is built only if it is actually used. // Lazily instantiate compiler so it is built only if it is actually used.
...@@ -409,141 +388,66 @@ class FlutterPlatform extends PlatformPlugin { ...@@ -409,141 +388,66 @@ class FlutterPlatform extends PlatformPlugin {
mainDart = await compiler.compile(globals.fs.file(mainDart).uri); mainDart = await compiler.compile(globals.fs.file(mainDart).uri);
if (mainDart == null) { if (mainDart == null) {
controller.sink.addError(_getErrorMessage('Compilation failed', testPath, shellPath)); testHarnessChannel.sink.addError('Compilation failed for testPath=$testPath');
return null; return null;
} }
} }
final Process process = await _startProcess( globals.printTrace('test $ourTestCount: starting test device');
shellPath,
mainDart, final TestDevice testDevice = _createTestDevice(ourTestCount);
enableObservatory: enableObservatory, final Future<StreamChannel<String>> remoteChannelFuture = testDevice.start(
serverPort: server.port, compiledEntrypointPath: mainDart,
); );
subprocessActive = true;
finalizers.add(() async { finalizers.add(() async {
if (subprocessActive) { globals.printTrace('test $ourTestCount: ensuring test device is terminated.');
globals.printTrace('test $ourTestCount: ensuring end-of-process for shell'); await testDevice.kill();
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);
}
}
}); });
final Completer<Uri> gotProcessObservatoryUri = Completer<Uri>(); // At this point, these things can happen:
if (!enableObservatory) { // A. The test device could crash, in which case [testDevice.finished]
gotProcessObservatoryUri.complete(); // will complete.
} // B. The test device could connect to us, in which case
// [remoteChannelFuture] will complete.
// Pipe stdout and stderr from the subprocess to our printStatus console. globals.printTrace('test $ourTestCount: awaiting connection to test device');
// We also keep track of what observatory port the engine used, if any. await Future.any<void>(<Future<void>>[
final Uri ddsServiceUri = getDdsServiceUri(); testDevice.finished,
_pipeStandardStreamsToConsole( () async {
process, final Uri processObservatoryUri = await testDevice.observatoryUri;
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) {
if (processObservatoryUri != null) { if (processObservatoryUri != null) {
globals.printTrace('test $ourTestCount: Observatory uri is available at $processObservatoryUri'); 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)); watcher?.handleStartedDevice(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;
});
})
]);
if (testHarnessStatus == _TestHarnessStatus.testerCrashed) { final StreamChannel<String> remoteChannel = await remoteChannelFuture;
globals.printTrace('test $ourTestCount: process with pid ${process.pid} crashed'); globals.printTrace('test $ourTestCount: connected to test device, now awaiting test result');
final int exitCode = await process.exitCode;
subprocessActive = false; await _pipeHarnessToRemote(
final String message = _getErrorMessage( id: ourTestCount,
_getExitCodeMessage(exitCode), harnessChannel: testHarnessChannel,
testPath, remoteChannel: remoteChannel,
shellPath); );
controller.sink.addError(message);
// Awaited for with 'sink.done' below in `finally`. globals.printTrace('test $ourTestCount: finished');
unawaited(controller.sink.close()); await watcher?.handleFinishedTest(testDevice);
globals.printTrace('test $ourTestCount: waiting for controller sink to close'); }()
await controller.sink.done; ]);
await watcher?.handleTestCrashed(ProcessEvent(ourTestCount, process)); } 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"}'); globals.printTrace('test $ourTestCount: error caught during test; ${controllerSinkClosed ? "reporting to console" : "sending to test framework"}');
if (!controllerSinkClosed) { if (!controllerSinkClosed) {
controller.sink.addError(error, stack); testHarnessChannel.sink.addError(reportedError, reportedStackTrace);
} else { } else {
globals.printError('unhandled error during test:\n$testPath\n$error\n$stack'); globals.printError('unhandled error during test:\n$testPath\n$reportedError\n$reportedStackTrace');
outOfBandError ??= _AsyncError(error, stack); outOfBandError ??= _AsyncError(reportedError, reportedStackTrace);
} }
} finally { } finally {
globals.printTrace('test $ourTestCount: cleaning up...'); globals.printTrace('test $ourTestCount: cleaning up...');
...@@ -554,7 +458,7 @@ class FlutterPlatform extends PlatformPlugin { ...@@ -554,7 +458,7 @@ class FlutterPlatform extends PlatformPlugin {
} on Exception catch (error, stack) { } on Exception catch (error, stack) {
globals.printTrace('test $ourTestCount: error while cleaning up; ${controllerSinkClosed ? "reporting to console" : "sending to test framework"}'); globals.printTrace('test $ourTestCount: error while cleaning up; ${controllerSinkClosed ? "reporting to console" : "sending to test framework"}');
if (!controllerSinkClosed) { if (!controllerSinkClosed) {
controller.sink.addError(error, stack); testHarnessChannel.sink.addError(error, stack);
} else { } else {
globals.printError('unhandled error during finalization of test:\n$testPath\n$error\n$stack'); globals.printError('unhandled error during finalization of test:\n$testPath\n$error\n$stack');
outOfBandError ??= _AsyncError(error, stack); outOfBandError ??= _AsyncError(error, stack);
...@@ -563,12 +467,11 @@ class FlutterPlatform extends PlatformPlugin { ...@@ -563,12 +467,11 @@ class FlutterPlatform extends PlatformPlugin {
} }
if (!controllerSinkClosed) { if (!controllerSinkClosed) {
// Waiting below with await. // Waiting below with await.
unawaited(controller.sink.close()); unawaited(testHarnessChannel.sink.close());
globals.printTrace('test $ourTestCount: waiting for controller sink to close'); globals.printTrace('test $ourTestCount: waiting for controller sink to close');
await controller.sink.done; await testHarnessChannel.sink.done;
} }
} }
assert(!subprocessActive);
assert(controllerSinkClosed); assert(controllerSinkClosed);
if (outOfBandError != null) { if (outOfBandError != null) {
globals.printTrace('test $ourTestCount: finished with out-of-band failure'); globals.printTrace('test $ourTestCount: finished with out-of-band failure');
...@@ -582,7 +485,6 @@ class FlutterPlatform extends PlatformPlugin { ...@@ -582,7 +485,6 @@ class FlutterPlatform extends PlatformPlugin {
List<Finalizer> finalizers, List<Finalizer> finalizers,
int ourTestCount, int ourTestCount,
String testPath, String testPath,
HttpServer server,
) { ) {
// Prepare a temporary directory to store the Dart file that will talk to us. // Prepare a temporary directory to store the Dart file that will talk to us.
final Directory tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_test_listener.'); final Directory tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_test_listener.');
...@@ -621,184 +523,13 @@ class FlutterPlatform extends PlatformPlugin { ...@@ -621,184 +523,13 @@ class FlutterPlatform extends PlatformPlugin {
); );
} }
File _cachedFontConfig;
@override @override
Future<dynamic> close() async { Future<dynamic> close() async {
if (compiler != null) { if (compiler != null) {
await compiler.dispose(); await compiler.dispose();
compiler = null; compiler = null;
} }
if (fontsDirectory != null) { await _fontConfigManager.dispose();
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,
);
} }
} }
...@@ -862,60 +593,29 @@ class _AsyncError { ...@@ -862,60 +593,29 @@ class _AsyncError {
final StackTrace stack; 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 /// The returned future completes when either side is closed, which also
/// a [remoteSocket] that runs the test. The returned future completes when /// indicates when the tests have finished.
/// either side is closed, which also indicates when the tests have finished. Future<void> _pipeHarnessToRemote({
Future<void> _controlTests({ @required int id,
@required @required StreamChannel<dynamic> harnessChannel,
StreamChannel<dynamic> controller, @required StreamChannel<String> remoteChannel,
@required
WebSocket remoteSocket,
@required
void Function(dynamic, StackTrace) onError,
}) async { }) async {
final Completer<void> harnessDone = Completer<void>(); globals.printTrace('test $id: Waiting for test harness or tests to finish');
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');
await Future.any<void>(<Future<void>>[ await Future.any<void>(<Future<void>>[
harnessDone.future.then<void>((void value) { harnessChannel.stream
globals.printTrace('test process is no longer needed by test harness'); .map<String>(json.encode)
}), .pipe(remoteChannel.sink)
testDone.future.then<void>((void value) { .then<void>((void value) {
globals.printTrace('test harness is no longer needed by test process'); globals.printTrace('test $id: Test process is no longer needed by test harness');
}), }),
]); remoteChannel.stream
.map<dynamic>(json.decode)
await Future.wait<void>(<Future<void>>[ .pipe(harnessChannel.sink)
harnessToTest.cancel(), .then<void>((void value) {
testToHarness.cancel(), 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 @@ ...@@ -4,43 +4,27 @@
// @dart = 2.8 // @dart = 2.8
import '../base/io.dart' show Process; import 'test_device.dart';
/// Callbacks for reporting progress while running tests. /// Callbacks for reporting progress while running tests.
abstract class TestWatcher { 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 /// If startPaused was true, the caller needs to resume in Observatory to
/// start running the tests. /// 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. /// The test device won't exit until this method completes.
/// Not called if the process died. /// Not called if the test device died.
Future<void> handleFinishedTest(ProcessEvent event); Future<void> handleFinishedTest(TestDevice testDevice);
/// Called when the test process crashed before connecting to test harness. /// Called when the test device crashed before it could be connected to the
Future<void> handleTestCrashed(ProcessEvent event); /// 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. /// harness.
Future<void> handleTestTimedOut(ProcessEvent event); Future<void> handleTestTimedOut(TestDevice testDevice);
}
/// 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;
} }
...@@ -4,16 +4,12 @@ ...@@ -4,16 +4,12 @@
// @dart = 2.8 // @dart = 2.8
import 'dart:async';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.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/build_info.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/test/flutter_platform.dart'; import 'package:flutter_tools/src/test/flutter_platform.dart';
import 'package:meta/meta.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:process/process.dart'; import 'package:process/process.dart';
import 'package:test_core/backend.dart'; // ignore: deprecated_member_use import 'package:test_core/backend.dart'; // ignore: deprecated_member_use
...@@ -41,6 +37,7 @@ void main() { ...@@ -41,6 +37,7 @@ void main() {
BuildInfo.debug, BuildInfo.debug,
hostVmServicePort: 1234, hostVmServicePort: 1234,
), ),
enableObservatory: false,
); );
flutterPlatform.loadChannel('test1.dart', MockSuitePlatform()); flutterPlatform.loadChannel('test1.dart', MockSuitePlatform());
...@@ -56,6 +53,7 @@ void main() { ...@@ -56,6 +53,7 @@ void main() {
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
shellPath: '/', shellPath: '/',
precompiledDillPath: 'example.dill', precompiledDillPath: 'example.dill',
enableObservatory: false,
); );
flutterPlatform.loadChannel('test1.dart', MockSuitePlatform()); flutterPlatform.loadChannel('test1.dart', MockSuitePlatform());
...@@ -65,115 +63,6 @@ void main() { ...@@ -65,115 +63,6 @@ void main() {
ProcessManager: () => FakeProcessManager.any(), 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', () { testUsingContext('installHook creates a FlutterPlatform', () {
expect(() => installHook( expect(() => installHook(
shellPath: 'abc', shellPath: 'abc',
...@@ -206,7 +95,6 @@ void main() { ...@@ -206,7 +95,6 @@ void main() {
), ),
enableObservatory: true, enableObservatory: true,
machine: true, machine: true,
port: 100,
precompiledDillPath: 'def', precompiledDillPath: 'def',
precompiledDillFiles: expectedPrecompiledDillFiles, precompiledDillFiles: expectedPrecompiledDillFiles,
updateGoldens: true, updateGoldens: true,
...@@ -225,7 +113,6 @@ void main() { ...@@ -225,7 +113,6 @@ void main() {
expect(flutterPlatform.debuggingOptions.hostVmServicePort, equals(200)); expect(flutterPlatform.debuggingOptions.hostVmServicePort, equals(200));
expect(flutterPlatform.enableObservatory, equals(true)); expect(flutterPlatform.enableObservatory, equals(true));
expect(flutterPlatform.machine, equals(true)); expect(flutterPlatform.machine, equals(true));
expect(flutterPlatform.port, equals(100));
expect(flutterPlatform.host, InternetAddress.loopbackIPv6); expect(flutterPlatform.host, InternetAddress.loopbackIPv6);
expect(flutterPlatform.precompiledDillPath, equals('def')); expect(flutterPlatform.precompiledDillPath, equals('def'));
expect(flutterPlatform.precompiledDillFiles, expectedPrecompiledDillFiles); expect(flutterPlatform.precompiledDillFiles, expectedPrecompiledDillFiles);
...@@ -234,113 +121,20 @@ void main() { ...@@ -234,113 +121,20 @@ void main() {
expect(flutterPlatform.icudtlPath, equals('ghi')); 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 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. // 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 { class TestFlutterPlatform extends FlutterPlatform {
TestFlutterPlatform([List<String> dartEntrypointArgs = const <String>[]]) : super( TestFlutterPlatform() : super(
shellPath: '/', shellPath: '/',
debuggingOptions: DebuggingOptions.enabled( debuggingOptions: DebuggingOptions.enabled(
const BuildInfo(BuildMode.debug, '', treeShakeIcons: false, packagesPath: '.dart_tool/package_config.json'), const BuildInfo(
startPaused: false, BuildMode.debug,
disableDds: true, '',
dartEntrypointArgs: dartEntrypointArgs, 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 @@ ...@@ -5,19 +5,22 @@
// @dart = 2.8 // @dart = 2.8
import 'package:flutter_tools/src/test/event_printer.dart'; 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/common.dart';
import '../../src/fakes.dart';
void main() { void main() {
testWithoutContext('EventPrinter handles a null parent', () { testWithoutContext('EventPrinter handles a null parent', () {
final EventPrinter eventPrinter = EventPrinter(out: StringBuffer()); 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.handleFinishedTest(device), returnsNormally);
expect(() => eventPrinter.handleStartedProcess(processEvent), returnsNormally); expect(() => eventPrinter.handleStartedDevice(observatoryUri), returnsNormally);
expect(() => eventPrinter.handleTestCrashed(processEvent), returnsNormally); expect(() => eventPrinter.handleTestCrashed(device), returnsNormally);
expect(() => eventPrinter.handleTestTimedOut(processEvent), returnsNormally); expect(() => eventPrinter.handleTestTimedOut(device), returnsNormally);
}); });
} }
class _Device extends Mock implements TestDevice {}
...@@ -168,7 +168,7 @@ void main() { ...@@ -168,7 +168,7 @@ void main() {
extraArguments: const <String>['--verbose']); extraArguments: const <String>['--verbose']);
final String stdout = result.stdout as String; final String stdout = result.stdout as String;
if ((!stdout.contains('+1: All tests passed')) || 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: deleting temporary directory')) ||
(!stdout.contains('test 0: finished')) || (!stdout.contains('test 0: finished')) ||
(!stdout.contains('test package returned with exit code 0'))) { (!stdout.contains('test package returned with exit code 0'))) {
...@@ -185,7 +185,7 @@ void main() { ...@@ -185,7 +185,7 @@ void main() {
extraArguments: const <String>['--verbose']); extraArguments: const <String>['--verbose']);
final String stdout = result.stdout as String; final String stdout = result.stdout as String;
if ((!stdout.contains('+2: All tests passed')) || 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: deleting temporary directory')) ||
(!stdout.contains('test 0: finished')) || (!stdout.contains('test 0: finished')) ||
(!stdout.contains('test package returned with exit code 0'))) { (!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