Unverified Commit 10aa41f0 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Fix coverage collection crash (#21015)

* Fix coverage collection crash

Based on Jason's patch in https://github.com/flutter/flutter/pull/19546/

This is more or less the same but I tried to avoid using `dynamic`.

* Improve argument and variable names in flutter_platform

* Don't bother with reduce, since the order is guaranteed.
parent 77095356
...@@ -45,35 +45,29 @@ class CoverageCollector extends TestWatcher { ...@@ -45,35 +45,29 @@ class CoverageCollector extends TestWatcher {
assert(observatoryUri != null); assert(observatoryUri != null);
final int pid = process.pid; final int pid = process.pid;
int exitCode;
// Synchronization is enforced by the API contract. Error handling
// synchronization is done in the code below where `exitCode` is checked.
// Callback cannot throw.
process.exitCode.then<Null>((int code) { // ignore: unawaited_futures
exitCode = code;
});
if (exitCode != null)
throw new Exception('Failed to collect coverage, process terminated before coverage could be collected.');
printTrace('pid $pid: collecting coverage data from $observatoryUri...'); printTrace('pid $pid: collecting coverage data from $observatoryUri...');
final Map<String, dynamic> data = await coverage
.collect(observatoryUri, false, false) Map<String, dynamic> data;
.timeout( final Future<void> processComplete = process.exitCode
const Duration(minutes: 2), .then<void>((int code) {
onTimeout: () { throw new Exception('Failed to collect coverage, process terminated prematurely with exit code $code.');
throw new Exception('Timed out while collecting coverage.'); });
}, final Future<void> collectionComplete = coverage.collect(observatoryUri, false, false)
); .then<void>((Map<String, dynamic> result) {
printTrace(() { if (result == null)
final StringBuffer buf = new StringBuffer() throw new Exception('Failed to collect coverage.');
..write('pid $pid ($observatoryUri): ') data = result;
..write(exitCode == null })
? 'collected coverage data; merging...' .timeout(
: 'process terminated prematurely with exit code $exitCode; aborting'); const Duration(minutes: 10),
return buf.toString(); onTimeout: () {
}()); throw new Exception('Timed out while collecting coverage.');
if (exitCode != null) },
throw new Exception('Failed to collect coverage, process terminated while coverage was being collected.'); );
await Future.any<void>(<Future<void>>[ processComplete, collectionComplete ]);
assert(data != null);
printTrace('pid $pid ($observatoryUri): collected coverage data; merging...');
_addHitmap(coverage.createHitmap(data['coverage'])); _addHitmap(coverage.createHitmap(data['coverage']));
printTrace('pid $pid ($observatoryUri): done merging coverage data into global coverage map.'); printTrace('pid $pid ($observatoryUri): done merging coverage data into global coverage map.');
} }
......
...@@ -367,7 +367,7 @@ class _FlutterPlatform extends PlatformPlugin { ...@@ -367,7 +367,7 @@ class _FlutterPlatform extends PlatformPlugin {
_testCount += 1; _testCount += 1;
final StreamController<dynamic> localController = new StreamController<dynamic>(); final StreamController<dynamic> localController = new StreamController<dynamic>();
final StreamController<dynamic> remoteController = new StreamController<dynamic>(); final StreamController<dynamic> remoteController = new StreamController<dynamic>();
final Completer<Null> testCompleteCompleter = new Completer<Null>(); final Completer<_AsyncError> testCompleteCompleter = new Completer<_AsyncError>();
final _FlutterPlatformStreamSinkWrapper<dynamic> remoteSink = new _FlutterPlatformStreamSinkWrapper<dynamic>( final _FlutterPlatformStreamSinkWrapper<dynamic> remoteSink = new _FlutterPlatformStreamSinkWrapper<dynamic>(
remoteController.sink, remoteController.sink,
testCompleteCompleter.future, testCompleteCompleter.future,
...@@ -384,13 +384,14 @@ class _FlutterPlatform extends PlatformPlugin { ...@@ -384,13 +384,14 @@ class _FlutterPlatform extends PlatformPlugin {
return remoteChannel; return remoteChannel;
} }
Future<Null> _startTest( Future<_AsyncError> _startTest(
String testPath, String testPath,
StreamChannel<dynamic> controller, StreamChannel<dynamic> controller,
int ourTestCount) async { int ourTestCount,
) async {
printTrace('test $ourTestCount: starting test $testPath'); printTrace('test $ourTestCount: starting test $testPath');
dynamic 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 subprocessActive = false;
...@@ -440,8 +441,7 @@ class _FlutterPlatform extends PlatformPlugin { ...@@ -440,8 +441,7 @@ class _FlutterPlatform extends PlatformPlugin {
mainDart = await compiler.compile(mainDart); mainDart = await compiler.compile(mainDart);
if (mainDart == null) { if (mainDart == null) {
controller.sink.addError( controller.sink.addError(_getErrorMessage('Compilation failed', testPath, shellPath));
_getErrorMessage('Compilation failed', testPath, shellPath));
return null; return null;
} }
} }
...@@ -630,7 +630,7 @@ class _FlutterPlatform extends PlatformPlugin { ...@@ -630,7 +630,7 @@ class _FlutterPlatform extends PlatformPlugin {
controller.sink.addError(error, stack); controller.sink.addError(error, stack);
} else { } else {
printError('unhandled error during test:\n$testPath\n$error\n$stack'); printError('unhandled error during test:\n$testPath\n$error\n$stack');
outOfBandError ??= error; outOfBandError ??= new _AsyncError(error, stack);
} }
} finally { } finally {
printTrace('test $ourTestCount: cleaning up...'); printTrace('test $ourTestCount: cleaning up...');
...@@ -644,7 +644,7 @@ class _FlutterPlatform extends PlatformPlugin { ...@@ -644,7 +644,7 @@ class _FlutterPlatform extends PlatformPlugin {
controller.sink.addError(error, stack); controller.sink.addError(error, stack);
} else { } else {
printError('unhandled error during finalization of test:\n$testPath\n$error\n$stack'); printError('unhandled error during finalization of test:\n$testPath\n$error\n$stack');
outOfBandError ??= error; outOfBandError ??= new _AsyncError(error, stack);
} }
} }
} }
...@@ -659,10 +659,10 @@ class _FlutterPlatform extends PlatformPlugin { ...@@ -659,10 +659,10 @@ class _FlutterPlatform extends PlatformPlugin {
assert(controllerSinkClosed); assert(controllerSinkClosed);
if (outOfBandError != null) { if (outOfBandError != null) {
printTrace('test $ourTestCount: finished with out-of-band failure'); printTrace('test $ourTestCount: finished with out-of-band failure');
throw outOfBandError; } else {
printTrace('test $ourTestCount: finished');
} }
printTrace('test $ourTestCount: finished'); return outOfBandError;
return null;
} }
String _createListenerDart(List<_Finalizer> finalizers, int ourTestCount, String _createListenerDart(List<_Finalizer> finalizers, int ourTestCount,
...@@ -902,10 +902,24 @@ class _FlutterPlatform extends PlatformPlugin { ...@@ -902,10 +902,24 @@ class _FlutterPlatform extends PlatformPlugin {
} }
} }
// The [_shellProcessClosed] future can't have errors thrown on it because it
// crosses zones (it's fed in a zone created by the test package, but listened
// to by a parent zone, the same zone that calls [close] below).
//
// This is because Dart won't let errors that were fed into a Future in one zone
// propagate to listeners in another zone. (Specifically, the zone in which the
// future was completed with the error, and the zone in which the listener was
// registered, are what matters.)
//
// Because of this, the [_shellProcessClosed] future takes an [_AsyncError]
// object as a result. If it's null, it's as if it had completed correctly; if
// it's non-null, it contains the error and stack trace of the actual error, as
// if it had completed with that error.
class _FlutterPlatformStreamSinkWrapper<S> implements StreamSink<S> { class _FlutterPlatformStreamSinkWrapper<S> implements StreamSink<S> {
_FlutterPlatformStreamSinkWrapper(this._parent, this._shellProcessClosed); _FlutterPlatformStreamSinkWrapper(this._parent, this._shellProcessClosed);
final StreamSink<S> _parent; final StreamSink<S> _parent;
final Future<void> _shellProcessClosed; final Future<_AsyncError> _shellProcessClosed;
@override @override
Future<void> get done => _done.future; Future<void> get done => _done.future;
...@@ -913,12 +927,19 @@ class _FlutterPlatformStreamSinkWrapper<S> implements StreamSink<S> { ...@@ -913,12 +927,19 @@ class _FlutterPlatformStreamSinkWrapper<S> implements StreamSink<S> {
@override @override
Future<dynamic> close() { Future<dynamic> close() {
Future.wait<dynamic>(<Future<dynamic>>[ Future.wait<dynamic>(<Future<dynamic>>[
_parent.close(), _parent.close(),
_shellProcessClosed, _shellProcessClosed,
]).then<void>( ]).then<void>(
(List<dynamic> value) { (List<dynamic> futureResults) {
_done.complete(); assert(futureResults.length == 2);
assert(futureResults.first == null);
if (futureResults.last is _AsyncError) {
_done.completeError(futureResults.last.error, futureResults.last.stack);
} else {
assert(futureResults.last == null);
_done.complete();
}
}, },
onError: _done.completeError, onError: _done.completeError,
); );
...@@ -932,3 +953,10 @@ class _FlutterPlatformStreamSinkWrapper<S> implements StreamSink<S> { ...@@ -932,3 +953,10 @@ class _FlutterPlatformStreamSinkWrapper<S> implements StreamSink<S> {
@override @override
Future<dynamic> addStream(Stream<S> stream) => _parent.addStream(stream); Future<dynamic> addStream(Stream<S> stream) => _parent.addStream(stream);
} }
@immutable
class _AsyncError {
const _AsyncError(this.error, this.stack);
final dynamic error;
final StackTrace stack;
}
\ No newline at end of file
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