Commit 3f1d6d3b authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Remove randomness from port assignment during coverage collection. (#7548)

Also, defer to test package for throttling (this will require a test
package update as well).

Also, add a lot more instrumentation to --verbose mode for tests, and
fix minor trivial things here and there, and add error handling in
more places.

Also, refactor how coverage works to be simpler and not use statics.
parent b2a2ee72
...@@ -36,13 +36,16 @@ DART_SDK_PATH="$FLUTTER_ROOT/bin/cache/dart-sdk" ...@@ -36,13 +36,16 @@ DART_SDK_PATH="$FLUTTER_ROOT/bin/cache/dart-sdk"
DART="$DART_SDK_PATH/bin/dart" DART="$DART_SDK_PATH/bin/dart"
# To debug the tool, you can uncomment the following line to enable checked mode and set an observatory port:
# FLUTTER_TOOL_ARGS="--observe=65432 --checked"
( (
if hash flock 2>/dev/null; then if hash flock 2>/dev/null; then
flock 3 # ensures that we don't simultaneously update Dart in multiple parallel instances flock 3 # ensures that we don't simultaneously update Dart in multiple parallel instances
# some platforms (e.g. Mac) don't have flock or any reliable alternative # some platforms (e.g. Mac) don't have flock or any reliable alternative
fi fi
REVISION=`(cd "$FLUTTER_ROOT"; git rev-parse HEAD)` REVISION=`(cd "$FLUTTER_ROOT"; git rev-parse HEAD)`
if [ ! -f "$SNAPSHOT_PATH" ] || [ ! -f "$STAMP_PATH" ] || [ `cat "$STAMP_PATH"` != "$REVISION" ] || [ "$FLUTTER_TOOLS_DIR/pubspec.yaml" -nt "$FLUTTER_TOOLS_DIR/pubspec.lock" ]; then if [ ! -f "$SNAPSHOT_PATH" ] || [ ! -s "$STAMP_PATH" ] || [ `cat "$STAMP_PATH"` != "$REVISION" ] || [ "$FLUTTER_TOOLS_DIR/pubspec.yaml" -nt "$FLUTTER_TOOLS_DIR/pubspec.lock" ]; then
mkdir -p "$FLUTTER_ROOT/bin/cache" mkdir -p "$FLUTTER_ROOT/bin/cache"
touch "$FLUTTER_ROOT/bin/cache/.dartignore" touch "$FLUTTER_ROOT/bin/cache/.dartignore"
"$FLUTTER_ROOT/bin/internal/update_dart_sdk.sh" "$FLUTTER_ROOT/bin/internal/update_dart_sdk.sh"
...@@ -56,7 +59,7 @@ DART="$DART_SDK_PATH/bin/dart" ...@@ -56,7 +59,7 @@ DART="$DART_SDK_PATH/bin/dart"
) 3< "$PROG_NAME" ) 3< "$PROG_NAME"
set +e set +e
"$DART" "$SNAPSHOT_PATH" "$@" "$DART" $FLUTTER_TOOL_ARGS "$SNAPSHOT_PATH" "$@"
# The VM exits with code 253 if the snapshot version is out-of-date. # The VM exits with code 253 if the snapshot version is out-of-date.
# If it is, we need to snapshot it again. # If it is, we need to snapshot it again.
...@@ -67,4 +70,4 @@ fi ...@@ -67,4 +70,4 @@ fi
set -e set -e
"$DART" --snapshot="$SNAPSHOT_PATH" --packages="$FLUTTER_TOOLS_DIR/.packages" "$SCRIPT_PATH" "$DART" --snapshot="$SNAPSHOT_PATH" --packages="$FLUTTER_TOOLS_DIR/.packages" "$SCRIPT_PATH"
"$DART" "$SNAPSHOT_PATH" "$@" "$DART" $FLUTTER_TOOL_ARGS "$SNAPSHOT_PATH" "$@"
...@@ -32,7 +32,7 @@ class TestCommand extends FlutterCommand { ...@@ -32,7 +32,7 @@ class TestCommand extends FlutterCommand {
argParser.addFlag('merge-coverage', argParser.addFlag('merge-coverage',
defaultsTo: false, defaultsTo: false,
negatable: false, negatable: false,
help: 'Whether to merge converage data with "coverage/lcov.base.info". ' help: 'Whether to merge converage data with "coverage/lcov.base.info".\n'
'Implies collecting coverage data. (Requires lcov)' 'Implies collecting coverage data. (Requires lcov)'
); );
argParser.addOption('coverage-path', argParser.addOption('coverage-path',
...@@ -93,6 +93,7 @@ class TestCommand extends FlutterCommand { ...@@ -93,6 +93,7 @@ class TestCommand extends FlutterCommand {
timeout: new Duration(seconds: 30), timeout: new Duration(seconds: 30),
); );
status.stop(); status.stop();
printTrace('coverage information collection complete');
if (coverageData == null) if (coverageData == null)
return false; return false;
...@@ -146,40 +147,42 @@ class TestCommand extends FlutterCommand { ...@@ -146,40 +147,42 @@ class TestCommand extends FlutterCommand {
@override @override
Future<Null> runCommand() async { Future<Null> runCommand() async {
List<String> testArgs = argResults.rest.map((String testPath) => path.absolute(testPath)).toList(); List<String> testArgs = <String>[];
commandValidator(); commandValidator();
Directory testDir; if (!terminal.supportsColor)
testArgs.addAll(<String>['--no-color', '-rexpanded']);
CoverageCollector collector;
if (argResults['coverage'] || argResults['merge-coverage']) {
collector = new CoverageCollector();
testArgs.add('--concurrency=1');
}
testArgs.add('--');
if (testArgs.isEmpty) { Directory testDir;
List<String> files = argResults.rest.map((String testPath) => path.absolute(testPath)).toList();
if (files.isEmpty) {
testDir = _currentPackageTestDir; testDir = _currentPackageTestDir;
if (!testDir.existsSync()) if (!testDir.existsSync())
throwToolExit("Test directory '${testDir.path}' not found."); throwToolExit("Test directory '${testDir.path}' not found.");
testArgs.addAll(_findTests(testDir)); testArgs.addAll(_findTests(testDir));
} else {
testArgs.addAll(files);
} }
testArgs.insert(0, '--');
if (!terminal.supportsColor)
testArgs.insertAll(0, <String>['--no-color', '-rexpanded']);
if (argResults['coverage'])
testArgs.insert(0, '--concurrency=1');
final String shellPath = tools.getHostToolPath(HostTool.SkyShell) ?? Platform.environment['SKY_SHELL']; final String shellPath = tools.getHostToolPath(HostTool.SkyShell) ?? Platform.environment['SKY_SHELL'];
if (!fs.isFileSync(shellPath)) if (!fs.isFileSync(shellPath))
throwToolExit('Cannot find Flutter shell at $shellPath'); throwToolExit('Cannot find Flutter shell at $shellPath');
loader.installHook(shellPath: shellPath); loader.installHook(shellPath: shellPath, collector: collector);
Cache.releaseLockEarly(); Cache.releaseLockEarly();
CoverageCollector collector = CoverageCollector.instance;
collector.enabled = argResults['coverage'] || argResults['merge-coverage'];
int result = await _runTests(testArgs, testDir); int result = await _runTests(testArgs, testDir);
if (collector.enabled) { if (collector != null) {
if (!await _collectCoverageData(collector, mergeCoverageData: argResults['merge-coverage'])) if (!await _collectCoverageData(collector, mergeCoverageData: argResults['merge-coverage']))
throwToolExit(null); throwToolExit(null);
} }
......
...@@ -14,43 +14,6 @@ import '../globals.dart'; ...@@ -14,43 +14,6 @@ import '../globals.dart';
/// A class that's used to collect coverage data during tests. /// A class that's used to collect coverage data during tests.
class CoverageCollector { class CoverageCollector {
CoverageCollector._();
/// The singleton instance of the coverage collector.
static final CoverageCollector instance = new CoverageCollector._();
/// By default, coverage collection is not enabled. Set [enabled] to true
/// to turn on coverage collection.
bool enabled = false;
int observatoryPort;
/// Adds a coverage collection tasks to the pending queue. The task will not
/// begin collecting coverage data until [CoverageCollectionTask.start] is
/// called.
///
/// When a process is spawned to accumulate code coverage data, this method
/// should be called before the process terminates so that this collector
/// knows to wait for the coverage data in [finalizeCoverage].
///
/// If this collector is not [enabled], the task will still be added to the
/// pending queue. Only when the task is started will the enabled state of
/// the collector be consulted.
CoverageCollectionTask addTask({
String host,
int port,
Process processToKill,
}) {
final CoverageCollectionTask task = new CoverageCollectionTask._(
this,
host,
port,
processToKill,
);
_tasks.add(task._future);
return task;
}
List<Future<Null>> _tasks = <Future<Null>>[];
Map<String, dynamic> _globalHitmap; Map<String, dynamic> _globalHitmap;
void _addHitmap(Map<String, dynamic> hitmap) { void _addHitmap(Map<String, dynamic> hitmap) {
...@@ -60,36 +23,43 @@ class CoverageCollector { ...@@ -60,36 +23,43 @@ class CoverageCollector {
mergeHitmaps(hitmap, _globalHitmap); mergeHitmaps(hitmap, _globalHitmap);
} }
/// Returns a future that completes once all tasks have finished. /// Collects coverage for the given [Process] using the given `port`.
/// This will not start any tasks that were not already started.
/// ///
/// If [timeout] is specified, the future will timeout (with a /// This should be called when the code whose coverage data is being collected
/// [TimeoutException]) after the specified duration. /// has been run to completion so that all coverage data has been recorded.
Future<Null> finishPendingTasks({ Duration timeout }) { ///
Future<dynamic> future = Future.wait(_tasks, eagerError: true); /// The returned [Future] completes when the coverage is collected.
if (timeout != null) Future<Null> collectCoverage(Process process, InternetAddress host, int port) async {
future = future.timeout(timeout); assert(process != null);
return future; assert(port != null);
int pid = process.pid;
int exitCode;
process.exitCode.then((int code) {
exitCode = code;
});
printTrace('pid $pid (port $port): collecting coverage data...');
final Map<dynamic, dynamic> data = await collect(host.address, port, false, false);
printTrace('pid $pid (port $port): ${ exitCode != null ? "process terminated prematurely with exit code $exitCode; aborting" : "collected coverage data; merging..." }');
if (exitCode != null)
throw new Exception('Failed to collect coverage, process terminated prematurely.');
_addHitmap(createHitmap(data['coverage']));
printTrace('pid $pid (port $port): 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
/// (using [formatter]) once all coverage data has been collected. /// (using [formatter]) once all coverage data has been collected.
/// ///
/// This will not start any collection tasks. It us up to the caller of /// This will not start any collection tasks. It us up to the caller of to
/// [addTask] to maintain a reference to the [CoverageCollectionTask] and /// call [collectCoverage] for each process first.
/// call `start` on the task once the code in question has run. Failure to do
/// so will cause this method to wait indefinitely for the task.
/// ///
/// If [timeout] is specified, the future will timeout (with a /// If [timeout] is specified, the future will timeout (with a
/// [TimeoutException]) after the specified duration. /// [TimeoutException]) after the specified duration.
///
/// This must only be called if this collector is [enabled].
Future<String> finalizeCoverage({ Future<String> finalizeCoverage({
Formatter formatter, Formatter formatter,
Duration timeout, Duration timeout,
}) async { }) async {
assert(enabled);
await finishPendingTasks(timeout: timeout);
printTrace('formating coverage data'); printTrace('formating coverage data');
if (_globalHitmap == null) if (_globalHitmap == null)
return null; return null;
...@@ -99,68 +69,8 @@ class CoverageCollector { ...@@ -99,68 +69,8 @@ class CoverageCollector {
List<String> reportOn = <String>[path.join(packagePath, 'lib')]; List<String> reportOn = <String>[path.join(packagePath, 'lib')];
formatter = new LcovFormatter(resolver, reportOn: reportOn, basePath: packagePath); formatter = new LcovFormatter(resolver, reportOn: reportOn, basePath: packagePath);
} }
return await formatter.format(_globalHitmap); String result = await formatter.format(_globalHitmap);
} _globalHitmap = null;
} return result;
/// A class that represents a pending task of coverage data collection.
/// Instances of this class are obtained when a process starts running code
/// by calling [CoverageCollector.addTask]. Then, when the code has run to
/// completion (all the coverage data has been recorded), the task is started
/// to actually collect the coverage data.
class CoverageCollectionTask {
final Completer<Null> _completer = new Completer<Null>();
final CoverageCollector _collector;
final String _host;
final int _port;
final Process _processToKill;
CoverageCollectionTask._(
this._collector,
this._host,
this._port,
this._processToKill,
);
bool _started = false;
Future<Null> get _future => _completer.future;
/// Starts the task of collecting coverage.
///
/// This should be called when the code whose coverage data is being collected
/// has been run to completion so that all coverage data has been recorded.
/// Failure to do so will cause [CoverageCollector.finalizeCoverage] to wait
/// indefinitely for the task to complete.
///
/// Each task may only be started once.
void start() {
assert(!_started);
_started = true;
if (!_collector.enabled) {
_processToKill.kill();
_completer.complete();
return;
}
int pid = _processToKill.pid;
printTrace('collecting coverage data from pid $pid on port $_port');
collect(_host, _port, false, false).then(
(Map<dynamic, dynamic> data) {
printTrace('done collecting coverage data from pid $pid');
_processToKill.kill();
try {
_collector._addHitmap(createHitmap(data['coverage']));
printTrace('done merging data from pid $pid into global coverage map');
_completer.complete();
} catch (error, stackTrace) {
_completer.completeError(error, stackTrace);
}
},
onError: (dynamic error, StackTrace stackTrace) {
_completer.completeError(error, stackTrace);
},
);
} }
} }
...@@ -4,8 +4,8 @@ ...@@ -4,8 +4,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:math' as math;
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:stream_channel/stream_channel.dart'; import 'package:stream_channel/stream_channel.dart';
...@@ -38,23 +38,28 @@ const Duration _kTestProcessTimeout = const Duration(minutes: 5); ...@@ -38,23 +38,28 @@ const Duration _kTestProcessTimeout = const Duration(minutes: 5);
/// hold that against the test. /// hold that against the test.
const String _kStartTimeoutTimerMessage = 'sky_shell test process has entered main method'; const String _kStartTimeoutTimerMessage = 'sky_shell test process has entered main method';
/// The address at which our WebSocket server resides. /// The address at which our WebSocket server resides and at which the sky_shell
/// processes will host the Observatory server.
final InternetAddress _kHost = InternetAddress.LOOPBACK_IP_V4; final InternetAddress _kHost = InternetAddress.LOOPBACK_IP_V4;
void installHook({ String shellPath }) { void installHook({ @required String shellPath, CoverageCollector collector }) {
hack.registerPlatformPlugin(<TestPlatform>[TestPlatform.vm], () => new FlutterPlatform(shellPath: shellPath)); hack.registerPlatformPlugin(
<TestPlatform>[TestPlatform.vm],
() => new FlutterPlatform(shellPath: shellPath, collector: collector),
);
} }
enum _InitialResult { crashed, timedOut, connected } enum _InitialResult { crashed, timedOut, connected }
enum _TestResult { crashed, harnessBailed, completed } enum _TestResult { crashed, harnessBailed, testBailed }
typedef Future<Null> _Finalizer(); typedef Future<Null> _Finalizer();
class FlutterPlatform extends PlatformPlugin { class FlutterPlatform extends PlatformPlugin {
FlutterPlatform({ this.shellPath }) { FlutterPlatform({ this.shellPath, this.collector }) {
assert(shellPath != null); assert(shellPath != null);
} }
final String shellPath; final String shellPath;
final CoverageCollector collector;
// Each time loadChannel() is called, we spin up a local WebSocket server, // Each time loadChannel() is called, we spin up a local WebSocket server,
// then spin up the engine in a subprocess. We pass the engine a Dart file // then spin up the engine in a subprocess. We pass the engine a Dart file
...@@ -65,13 +70,33 @@ class FlutterPlatform extends PlatformPlugin { ...@@ -65,13 +70,33 @@ class FlutterPlatform extends PlatformPlugin {
@override @override
StreamChannel<dynamic> loadChannel(String testPath, TestPlatform platform) { StreamChannel<dynamic> loadChannel(String testPath, TestPlatform platform) {
final StreamChannelController<dynamic> controller = new StreamChannelController<dynamic>(allowForeignErrors: false); StreamController<dynamic> localController = new StreamController<dynamic>();
_startTest(testPath, controller.local); StreamController<dynamic> remoteController = new StreamController<dynamic>();
return controller.foreign; Completer<Null> testCompleteCompleter = new Completer<Null>();
_FlutterPlatformStreamSinkWrapper<dynamic> remoteSink = new _FlutterPlatformStreamSinkWrapper<dynamic>(
remoteController.sink,
testCompleteCompleter.future,
);
StreamChannel<dynamic> localChannel = new StreamChannel<dynamic>.withGuarantees(
remoteController.stream,
localController.sink,
);
StreamChannel<dynamic> remoteChannel = new StreamChannel<dynamic>.withGuarantees(
localController.stream,
remoteSink,
);
_startTest(testPath, localChannel).whenComplete(() {
testCompleteCompleter.complete();
});
return remoteChannel;
} }
int _testCount = 0;
Future<Null> _startTest(String testPath, StreamChannel<dynamic> controller) async { Future<Null> _startTest(String testPath, StreamChannel<dynamic> controller) async {
printTrace('starting test: $testPath'); int ourTestCount = _testCount;
_testCount += 1;
printTrace('test $ourTestCount: starting test $testPath');
final List<_Finalizer> finalizers = <_Finalizer>[]; final List<_Finalizer> finalizers = <_Finalizer>[];
bool subprocessActive = false; bool subprocessActive = false;
...@@ -81,15 +106,34 @@ class FlutterPlatform extends PlatformPlugin { ...@@ -81,15 +106,34 @@ class FlutterPlatform extends PlatformPlugin {
// Prepare our WebSocket server to talk to the engine subproces. // Prepare our WebSocket server to talk to the engine subproces.
HttpServer server = await HttpServer.bind(_kHost, 0); HttpServer server = await HttpServer.bind(_kHost, 0);
finalizers.add(() async { await server.close(force: true); }); finalizers.add(() async {
printTrace('test $ourTestCount: shutting down test harness socket server');
await server.close(force: true);
});
Completer<WebSocket> webSocket = new Completer<WebSocket>(); Completer<WebSocket> webSocket = new Completer<WebSocket>();
server.listen((HttpRequest request) { server.listen(
(HttpRequest request) {
webSocket.complete(WebSocketTransformer.upgrade(request)); webSocket.complete(WebSocketTransformer.upgrade(request));
}); },
onError: (dynamic error, dynamic stack) {
// If you reach here, it's unlikely we're going to be able to really handle this well.
printTrace('test $ourTestCount: test harness socket server experienced an unexpected error: $error');
if (!controllerSinkClosed) {
controller.sink.addError(error, stack);
controller.sink.close();
} else {
printError('unexpected error from test harness socket server: $error');
}
},
cancelOnError: true,
);
// 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.
Directory temporaryDirectory = fs.systemTempDirectory.createTempSync('dart_test_listener'); Directory temporaryDirectory = fs.systemTempDirectory.createTempSync('dart_test_listener');
finalizers.add(() async { temporaryDirectory.deleteSync(recursive: true); }); finalizers.add(() async {
printTrace('test $ourTestCount: deleting temporary directory');
temporaryDirectory.deleteSync(recursive: true);
});
// Prepare the Dart file that will talk to us and start the test. // Prepare the Dart file that will talk to us and start the test.
File listenerFile = fs.file('${temporaryDirectory.path}/listener.dart'); File listenerFile = fs.file('${temporaryDirectory.path}/listener.dart');
...@@ -99,52 +143,51 @@ class FlutterPlatform extends PlatformPlugin { ...@@ -99,52 +143,51 @@ class FlutterPlatform extends PlatformPlugin {
encodedWebsocketUrl: Uri.encodeComponent("ws://${_kHost.address}:${server.port}"), encodedWebsocketUrl: Uri.encodeComponent("ws://${_kHost.address}:${server.port}"),
)); ));
// If we are collecting coverage data, then set that up now.
int observatoryPort;
if (CoverageCollector.instance.enabled) {
// TODO(ianh): the random number on the next line is a landmine that will eventually
// cause a hard-to-find bug...
observatoryPort = CoverageCollector.instance.observatoryPort ?? new math.Random().nextInt(30000) + 2000;
await CoverageCollector.instance.finishPendingTasks();
}
// Start the engine subprocess. // Start the engine subprocess.
printTrace('test $ourTestCount: starting shell process');
Process process = await _startProcess( Process process = await _startProcess(
shellPath, shellPath,
listenerFile.path, listenerFile.path,
packages: PackageMap.globalPackagesPath, packages: PackageMap.globalPackagesPath,
observatoryPort: observatoryPort, enableObservatory: collector != null,
); );
subprocessActive = true; subprocessActive = true;
finalizers.add(() async { finalizers.add(() async {
if (subprocessActive) if (subprocessActive) {
printTrace('test $ourTestCount: ensuring end-of-process for shell');
process.kill(); process.kill();
int exitCode = await process.exitCode; final int exitCode = await process.exitCode;
subprocessActive = false; subprocessActive = false;
if (!controllerSinkClosed && exitCode != 0) { if (!controllerSinkClosed && exitCode != -15) {
String message = _getErrorMessage(_getExitCodeMessage(exitCode, 'after tests finished'), testPath, shellPath); String message = _getErrorMessage(_getExitCodeMessage(exitCode, 'after tests finished'), testPath, shellPath);
controller.sink.addError(message); controller.sink.addError(message);
} }
}
}); });
CoverageCollectionTask coverageTask = CoverageCollector.instance.addTask(
host: _kHost.address,
port: observatoryPort,
processToKill: process, // This kills the subprocess whether coverage is enabled or not.
);
Completer<Null> timeout = new Completer<Null>(); Completer<Null> timeout = new Completer<Null>();
// Pipe stdout and stderr from the subprocess to our printStatus console. // Pipe stdout and stderr from the subprocess to our printStatus console.
_pipeStandardStreamsToConsole(process, startTimeoutTimer: () { // We also keep track of what observatory port the engine used, if any.
int processObservatoryPort;
_pipeStandardStreamsToConsole(
process,
storeObservatoryPort: (int observatoryPort) {
assert(processObservatoryPort == null);
printTrace('test $ourTestCount: using observatory port $observatoryPort from pid ${process.pid} to collect coverage');
processObservatoryPort = observatoryPort;
},
startTimeoutTimer: () {
new Future<_InitialResult>.delayed(_kTestStartupTimeout, () => timeout.complete()); new Future<_InitialResult>.delayed(_kTestStartupTimeout, () => timeout.complete());
}); },
);
// At this point, three things can happen next: // At this point, three things can happen next:
// The engine could crash, in which case process.exitCode will complete. // 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 engine could connect to us, in which case webSocket.future will complete.
// The local test harness could get bored of us. // The local test harness could get bored of us.
printTrace('test $ourTestCount: awaiting initial result for pid ${process.pid}');
_InitialResult initialResult = await Future.any(<Future<_InitialResult>>[ _InitialResult initialResult = await Future.any(<Future<_InitialResult>>[
process.exitCode.then<_InitialResult>((int exitCode) => _InitialResult.crashed), process.exitCode.then<_InitialResult>((int exitCode) => _InitialResult.crashed),
timeout.future.then<_InitialResult>((Null _) => _InitialResult.timedOut), timeout.future.then<_InitialResult>((Null _) => _InitialResult.timedOut),
...@@ -154,40 +197,69 @@ class FlutterPlatform extends PlatformPlugin { ...@@ -154,40 +197,69 @@ class FlutterPlatform extends PlatformPlugin {
switch (initialResult) { switch (initialResult) {
case _InitialResult.crashed: case _InitialResult.crashed:
printTrace('test $ourTestCount: process with pid ${process.pid} crashed before connecting to test harness');
int exitCode = await process.exitCode; int exitCode = await process.exitCode;
subprocessActive = false;
String message = _getErrorMessage(_getExitCodeMessage(exitCode, 'before connecting to test harness'), testPath, shellPath); String message = _getErrorMessage(_getExitCodeMessage(exitCode, 'before connecting to test harness'), testPath, shellPath);
controller.sink.addError(message); controller.sink.addError(message);
controller.sink.close(); controller.sink.close();
printTrace('test $ourTestCount: waiting for controller sink to close');
await controller.sink.done; await controller.sink.done;
break; break;
case _InitialResult.timedOut: case _InitialResult.timedOut:
printTrace('test $ourTestCount: timed out waiting for process with pid ${process.pid} to connect to test harness');
String message = _getErrorMessage('Test never connected to test harness.', testPath, shellPath); String message = _getErrorMessage('Test never connected to test harness.', testPath, shellPath);
controller.sink.addError(message); controller.sink.addError(message);
controller.sink.close(); controller.sink.close();
printTrace('test $ourTestCount: waiting for controller sink to close');
await controller.sink.done; await controller.sink.done;
break; break;
case _InitialResult.connected: case _InitialResult.connected:
printTrace('test $ourTestCount: process with pid ${process.pid} connected to test harness');
WebSocket testSocket = await webSocket.future; WebSocket testSocket = await webSocket.future;
Completer<Null> harnessDone = new Completer<Null>(); Completer<Null> harnessDone = new Completer<Null>();
StreamSubscription<dynamic> harnessToTest = controller.stream.listen( StreamSubscription<dynamic> harnessToTest = controller.stream.listen(
(dynamic event) { testSocket.add(JSON.encode(event)); }, (dynamic event) { testSocket.add(JSON.encode(event)); },
onDone: () { harnessDone.complete(); }, onDone: () { harnessDone.complete(); },
onError: (dynamic error, dynamic stack) {
// If you reach here, it's unlikely we're going to be able to really handle this well.
printError('test harness controller stream experienced an unexpected error\ntest: $testPath\nerror: $error');
if (!controllerSinkClosed) {
controller.sink.addError(error, stack);
controller.sink.close();
} else {
printError('unexpected error from test harness controller stream: $error');
}
},
cancelOnError: true,
); );
Completer<Null> testDone = new Completer<Null>(); Completer<Null> testDone = new Completer<Null>();
StreamSubscription<dynamic> testToHarness = testSocket.listen( StreamSubscription<dynamic> testToHarness = testSocket.listen(
(dynamic event) { (dynamic encodedEvent) {
assert(event is String); // we shouldn't ever get binary messages assert(encodedEvent is String); // we shouldn't ever get binary messages
controller.sink.add(JSON.decode(event)); controller.sink.add(JSON.decode(encodedEvent));
}, },
onDone: () { testDone.complete(); }, onDone: () { testDone.complete(); },
onError: (dynamic error, dynamic stack) {
// If you reach here, it's unlikely we're going to be able to really handle this well.
printError('test socket stream experienced an unexpected error\ntest: $testPath\nerror: $error');
if (!controllerSinkClosed) {
controller.sink.addError(error, stack);
controller.sink.close();
} else {
printError('unexpected error from test socket stream: $error');
}
},
cancelOnError: true,
); );
printTrace('test $ourTestCount: awaiting test result for pid ${process.pid}');
_TestResult testResult = await Future.any(<Future<_TestResult>>[ _TestResult testResult = await Future.any(<Future<_TestResult>>[
process.exitCode.then<_TestResult>((int exitCode) { return _TestResult.crashed; }), process.exitCode.then<_TestResult>((int exitCode) { return _TestResult.crashed; }),
testDone.future.then<_TestResult>((Null _) { return _TestResult.completed; }),
harnessDone.future.then<_TestResult>((Null _) { return _TestResult.harnessBailed; }), harnessDone.future.then<_TestResult>((Null _) { return _TestResult.harnessBailed; }),
testDone.future.then<_TestResult>((Null _) { return _TestResult.testBailed; }),
]); ]);
harnessToTest.cancel(); harnessToTest.cancel();
...@@ -195,40 +267,60 @@ class FlutterPlatform extends PlatformPlugin { ...@@ -195,40 +267,60 @@ class FlutterPlatform extends PlatformPlugin {
switch (testResult) { switch (testResult) {
case _TestResult.crashed: case _TestResult.crashed:
printTrace('test $ourTestCount: process with pid ${process.pid} crashed');
int exitCode = await process.exitCode; int exitCode = await process.exitCode;
subprocessActive = false; subprocessActive = false;
String message = _getErrorMessage(_getExitCodeMessage(exitCode, 'before test harness closed its WebSocket'), testPath, shellPath); String message = _getErrorMessage(_getExitCodeMessage(exitCode, 'before test harness closed its WebSocket'), testPath, shellPath);
controller.sink.addError(message); controller.sink.addError(message);
controller.sink.close(); controller.sink.close();
printTrace('test $ourTestCount: waiting for controller sink to close');
await controller.sink.done; await controller.sink.done;
break; break;
case _TestResult.completed:
break;
case _TestResult.harnessBailed: case _TestResult.harnessBailed:
printTrace('test $ourTestCount: process with pid ${process.pid} no longer needed by test harness');
break;
case _TestResult.testBailed:
printTrace('test $ourTestCount: process with pid ${process.pid} no longer needs test harness');
break; break;
} }
break; break;
} }
coverageTask.start(); if (subprocessActive && collector != null) {
subprocessActive = false; printTrace('test $ourTestCount: collecting coverage');
} catch (e, stack) { await collector.collectCoverage(process, _kHost, processObservatoryPort);
}
} catch (error, stack) {
printTrace('test $ourTestCount: error caught during test; ${controllerSinkClosed ? "reporting to console" : "sending to test framework"}');
if (!controllerSinkClosed) { if (!controllerSinkClosed) {
controller.sink.addError(e, stack); controller.sink.addError(error, stack);
} else { } else {
printError('unhandled error during test:\n$e\n$stack'); printError('unhandled error during test:\n$testPath\n$error');
} }
} finally { } finally {
for (_Finalizer finalizer in finalizers) printTrace('test $ourTestCount: cleaning up...');
for (_Finalizer finalizer in finalizers) {
try {
await finalizer(); await finalizer();
} catch (error, stack) {
printTrace('test $ourTestCount: error while cleaning up; ${controllerSinkClosed ? "reporting to console" : "sending to test framework"}');
if (!controllerSinkClosed) {
controller.sink.addError(error, stack);
} else {
printError('unhandled error during finalization of test:\n$testPath\n$error');
}
}
}
if (!controllerSinkClosed) { if (!controllerSinkClosed) {
controller.sink.close(); controller.sink.close();
printTrace('test $ourTestCount: waiting for controller sink to close');
await controller.sink.done; await controller.sink.done;
} }
} }
assert(!subprocessActive); assert(!subprocessActive);
assert(controllerSinkClosed); assert(controllerSinkClosed);
printTrace('ending test: $testPath'); printTrace('test $ourTestCount: finished');
return null;
} }
String _generateTestMain({ String _generateTestMain({
...@@ -286,14 +378,11 @@ void main() { ...@@ -286,14 +378,11 @@ void main() {
} }
Future<Process> _startProcess(String executable, String testPath, { String packages, int observatoryPort }) { Future<Process> _startProcess(String executable, String testPath, { String packages, bool enableObservatory: false }) {
assert(executable != null); // Please provide the path to the shell in the SKY_SHELL environment variable. assert(executable != null); // Please provide the path to the shell in the SKY_SHELL environment variable.
List<String> arguments = <String>[]; List<String> arguments = <String>[];
if (observatoryPort != null) { if (!enableObservatory)
arguments.add('--observatory-port=$observatoryPort');
} else {
arguments.add('--disable-observatory'); arguments.add('--disable-observatory');
}
arguments.addAll(<String>[ arguments.addAll(<String>[
'--enable-dart-profiling', '--enable-dart-profiling',
'--non-interactive', '--non-interactive',
...@@ -309,22 +398,46 @@ void main() { ...@@ -309,22 +398,46 @@ void main() {
return processManager.start(executable, arguments, environment: environment); return processManager.start(executable, arguments, environment: environment);
} }
String get observatoryPortString => 'Observatory listening on http://${_kHost.address}:';
String get diagnosticPortString => 'Diagnostic server listening on http://${_kHost.address}:';
void _pipeStandardStreamsToConsole( void _pipeStandardStreamsToConsole(
Process process, { Process process, {
void startTimeoutTimer(), void startTimeoutTimer(),
void storeObservatoryPort(int port)
}) { }) {
for (Stream<List<int>> stream in for (Stream<List<int>> stream in
<Stream<List<int>>>[process.stderr, process.stdout]) { <Stream<List<int>>>[process.stderr, process.stdout]) {
stream.transform(UTF8.decoder) stream.transform(UTF8.decoder)
.transform(const LineSplitter()) .transform(const LineSplitter())
.listen((String line) { .listen(
if (line == _kStartTimeoutTimerMessage && startTimeoutTimer != null) (String line) {
if (line == _kStartTimeoutTimerMessage) {
if (startTimeoutTimer != null)
startTimeoutTimer(); startTimeoutTimer();
else if (line.startsWith('error: Unable to read Dart source \'package:test/')) } else if (line.startsWith('error: Unable to read Dart source \'package:test/')) {
printTrace('Shell: $line');
printError('\n\nFailed to load test harness. Are you missing a dependency on flutter_test?\n'); printError('\n\nFailed to load test harness. Are you missing a dependency on flutter_test?\n');
else if (line != null) } else if (line.startsWith(observatoryPortString)) {
printTrace('Shell: $line');
try {
int port = int.parse(line.substring(observatoryPortString.length, line.length - 1)); // last character is a slash
if (storeObservatoryPort != null)
storeObservatoryPort(port);
} catch (error) {
printError('Could not parse shell observatory port message: $error');
}
} else if (line.startsWith(diagnosticPortString)) {
printTrace('Shell: $line');
} else if (line != null) {
printStatus('Shell: $line'); printStatus('Shell: $line');
}); }
},
onError: (dynamic error) {
printError('shell console stream for process pid ${process.pid} experienced an unexpected error: $error');
},
cancelOnError: true,
);
} }
} }
...@@ -334,6 +447,8 @@ void main() { ...@@ -334,6 +447,8 @@ void main() {
String _getExitCodeMessage(int exitCode, String when) { String _getExitCodeMessage(int exitCode, String when) {
switch (exitCode) { switch (exitCode) {
case 1:
return 'Shell subprocess cleanly reported an error $when. Check the logs above for an error message.';
case 0: case 0:
return 'Shell subprocess ended cleanly $when. Did main() call exit()?'; return 'Shell subprocess ended cleanly $when. Did main() call exit()?';
case -0x0f: // ProcessSignal.SIGTERM case -0x0f: // ProcessSignal.SIGTERM
...@@ -342,8 +457,43 @@ void main() { ...@@ -342,8 +457,43 @@ void main() {
return 'Shell subprocess crashed with segmentation fault $when.'; return 'Shell subprocess crashed with segmentation fault $when.';
case -0x06: // ProcessSignal.SIGABRT case -0x06: // ProcessSignal.SIGABRT
return 'Shell subprocess crashed with SIGABRT ($exitCode) $when.'; return 'Shell subprocess crashed with SIGABRT ($exitCode) $when.';
case -0x02: // ProcessSignal.SIGINT
return 'Shell subprocess terminated by ^C (SIGINT, $exitCode) $when.';
default: default:
return 'Shell subprocess crashed with unexpected exit code $exitCode $when.'; return 'Shell subprocess crashed with unexpected exit code $exitCode $when.';
} }
} }
} }
class _FlutterPlatformStreamSinkWrapper<S> implements StreamSink<S> {
_FlutterPlatformStreamSinkWrapper(this._parent, this._shellProcessClosed);
final StreamSink<S> _parent;
final Future<Null> _shellProcessClosed;
@override
Future<Null> get done => _done.future;
final Completer<Null> _done = new Completer<Null>();
@override
Future<dynamic> close() {
Future.wait(<Future<dynamic>>[
_parent.close(),
_shellProcessClosed,
]).then(
(List<dynamic> value) {
_done.complete();
},
onError: (dynamic error, StackTrace stack) {
_done.completeError(error, stack);
},
);
return done;
}
@override
void add(S event) => _parent.add(event);
@override
void addError(dynamic errorEvent, [ StackTrace stackTrace ]) => _parent.addError(errorEvent, stackTrace);
@override
Future<dynamic> addStream(Stream<S> stream) => _parent.addStream(stream);
}
...@@ -104,11 +104,11 @@ class Usage { ...@@ -104,11 +104,11 @@ class Usage {
/// Returns when the last analytics event has been sent, or after a fixed /// Returns when the last analytics event has been sent, or after a fixed
/// (short) delay, whichever is less. /// (short) delay, whichever is less.
Future<Null> ensureAnalyticsSent() { Future<Null> ensureAnalyticsSent() async {
// TODO(devoncarew): This may delay tool exit and could cause some analytics // TODO(devoncarew): This may delay tool exit and could cause some analytics
// events to not be reported. Perhaps we could send the analytics pings // events to not be reported. Perhaps we could send the analytics pings
// out-of-process from flutter_tools? // out-of-process from flutter_tools?
return _analytics.waitForLastPing(timeout: new Duration(milliseconds: 250)); await _analytics.waitForLastPing(timeout: new Duration(milliseconds: 250));
} }
void printUsage() { void printUsage() {
......
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