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