diff --git a/pkgs/coverage/CHANGELOG.md b/pkgs/coverage/CHANGELOG.md index 86e248e7d..7c880fbe2 100644 --- a/pkgs/coverage/CHANGELOG.md +++ b/pkgs/coverage/CHANGELOG.md @@ -1,6 +1,9 @@ ## 1.12.0-wip - Introduced support for specifying coverage flags through a YAML file. +- Fix a [bug](https://github.com/dart-lang/tools/issues/2049) where coverage + collection could hang forever if the package under test used background + isolates that were never shut down. ## 1.11.1 diff --git a/pkgs/coverage/lib/src/isolate_paused_listener.dart b/pkgs/coverage/lib/src/isolate_paused_listener.dart index 6f538a4e3..28894f772 100644 --- a/pkgs/coverage/lib/src/isolate_paused_listener.dart +++ b/pkgs/coverage/lib/src/isolate_paused_listener.dart @@ -28,30 +28,41 @@ class IsolatePausedListener { final _isolateGroups = {}; - int _numNonMainIsolates = 0; - final _allNonMainIsolatesExited = Completer(); - bool _finished = false; + int _numIsolates = 0; + bool _finishedListening = false; IsolateRef? _mainIsolate; - final _mainIsolatePaused = Completer(); + bool _hasMainIsolate = false; + + // Completes when either: + // - there is a main isolate, and it is paused or exited + // - there is no main isolate, and all isolates have exited + final _mainIsolatePausedOrAllIsolatesExited = Completer(); /// Starts listening and returns a future that completes when all isolates /// have exited. Future waitUntilAllExited() async { await listenToIsolateLifecycleEvents(_service, _onStart, _onPause, _onExit); - await _allNonMainIsolatesExited.future; - - // Resume the main isolate. - try { - if (_mainIsolate != null) { - if (await _mainIsolatePaused.future) { - await _runCallbackAndResume( - _mainIsolate!, !_getGroup(_mainIsolate!).collected); + await _mainIsolatePausedOrAllIsolatesExited.future; + _finishedListening = true; + + // Collect all remaining uncollected groups. + final collectionTasks = >[]; + for (final group in _isolateGroups.values) { + if (!group.collected) { + group.collected = true; + final iso = group.paused.firstOrNull ?? group.running.firstOrNull; + if (iso != null) { + collectionTasks.add(_onIsolatePaused(iso, true)); } } - } finally { - _finished = true; + } + await Future.wait(collectionTasks); + + // Resume the main isolate. + if (_mainIsolate != null) { + await _service.resume(_mainIsolate!.id!); } } @@ -59,70 +70,51 @@ class IsolatePausedListener { _isolateGroups[isolateRef.isolateGroupId!] ??= IsolateGroupState(); void _onStart(IsolateRef isolateRef) { - if (_finished) return; + if (_finishedListening) return; final group = _getGroup(isolateRef); - group.start(isolateRef.id!); - if (_mainIsolate == null && _isMainIsolate(isolateRef)) { + group.start(isolateRef); + ++_numIsolates; + if (!_hasMainIsolate && _isMainIsolate(isolateRef)) { _mainIsolate = isolateRef; - } else { - ++_numNonMainIsolates; + _hasMainIsolate = true; } } Future _onPause(IsolateRef isolateRef) async { - if (_finished) return; + if (_finishedListening) return; final group = _getGroup(isolateRef); - group.pause(isolateRef.id!); + group.pause(isolateRef); if (isolateRef.id! == _mainIsolate?.id) { - _mainIsolatePaused.complete(true); - - // If the main isolate is the only isolate, then _allNonMainIsolatesExited - // will never be completed. So check that case here. - _checkCompleted(); + _mainIsolatePausedOrAllIsolatesExited.safeComplete(); } else { - await _runCallbackAndResume(isolateRef, group.noRunningIsolates); - } - } - - Future _runCallbackAndResume( - IsolateRef isolateRef, bool isLastIsolateInGroup) async { - if (isLastIsolateInGroup) { - _getGroup(isolateRef).collected = true; - } - try { - await _onIsolatePaused(isolateRef, isLastIsolateInGroup); - } finally { - await _service.resume(isolateRef.id!); + final isLastIsolateInGroup = group.noRunningIsolates; + if (isLastIsolateInGroup) { + _getGroup(isolateRef).collected = true; + } + try { + await _onIsolatePaused(isolateRef, isLastIsolateInGroup); + } finally { + group.exit(isolateRef); + await _service.resume(isolateRef.id!); + } } } void _onExit(IsolateRef isolateRef) { - if (_finished) return; + if (_finishedListening) return; final group = _getGroup(isolateRef); - group.exit(isolateRef.id!); + group.exit(isolateRef); + --_numIsolates; if (group.noLiveIsolates && !group.collected) { _log('ERROR: An isolate exited without pausing, causing ' 'coverage data to be lost for group ${isolateRef.isolateGroupId!}.'); } if (isolateRef.id! == _mainIsolate?.id) { - if (!_mainIsolatePaused.isCompleted) { - // Main isolate exited without pausing. - _mainIsolatePaused.complete(false); - } - } else { - --_numNonMainIsolates; - _checkCompleted(); - } - } - - bool get _mainRunning => - _mainIsolate != null && !_mainIsolatePaused.isCompleted; - - void _checkCompleted() { - if (_numNonMainIsolates == 0 && - !_mainRunning && - !_allNonMainIsolatesExited.isCompleted) { - _allNonMainIsolatesExited.complete(); + // Main isolate exited without pausing. + _mainIsolate = null; + _mainIsolatePausedOrAllIsolatesExited.safeComplete(); + } else if (!_hasMainIsolate && _numIsolates == 0) { + _mainIsolatePausedOrAllIsolatesExited.safeComplete(); } } @@ -139,6 +131,12 @@ class IsolatePausedListener { } } +extension on Completer { + void safeComplete() { + if (!isCompleted) complete(); + } +} + /// Listens to isolate start and pause events, and backfills events for isolates /// that existed before listening started. /// @@ -220,30 +218,30 @@ Future listenToIsolateLifecycleEvents( class IsolateGroupState { // IDs of the isolates running in this group. @visibleForTesting - final running = {}; + final running = {}; // IDs of the isolates paused just before exiting in this group. @visibleForTesting - final paused = {}; + final paused = {}; bool collected = false; bool get noRunningIsolates => running.isEmpty; bool get noLiveIsolates => running.isEmpty && paused.isEmpty; - void start(String id) { - paused.remove(id); - running.add(id); + void start(IsolateRef iso) { + paused.remove(iso); + running.add(iso); } - void pause(String id) { - running.remove(id); - paused.add(id); + void pause(IsolateRef iso) { + running.remove(iso); + paused.add(iso); } - void exit(String id) { - running.remove(id); - paused.remove(id); + void exit(IsolateRef iso) { + running.remove(iso); + paused.remove(iso); } @override diff --git a/pkgs/coverage/test/collect_coverage_test.dart b/pkgs/coverage/test/collect_coverage_test.dart index 7262757be..476fee1c2 100644 --- a/pkgs/coverage/test/collect_coverage_test.dart +++ b/pkgs/coverage/test/collect_coverage_test.dart @@ -133,24 +133,25 @@ void main() { 39: 1, 41: 1, 42: 4, - 43: 1, - 44: 3, - 45: 1, - 48: 1, + 43: 2, + 44: 1, + 45: 3, + 46: 1, 49: 1, - 51: 1, - 54: 1, + 50: 1, + 52: 1, 55: 1, 56: 1, - 59: 1, + 57: 1, 60: 1, - 62: 1, + 61: 1, 63: 1, 64: 1, - 66: 1, + 65: 1, 67: 1, 68: 1, - 71: 3, + 69: 1, + 72: 3, }; expect(isolateFile?.lineHits, expectedHits); expect(isolateFile?.funcHits, {11: 1, 19: 1, 23: 1, 28: 1, 38: 1}); @@ -174,7 +175,7 @@ void main() { 32: 0, 38: 1, 42: 1, - 71: 1, + 72: 1, }, ); }); diff --git a/pkgs/coverage/test/isolate_paused_listener_test.dart b/pkgs/coverage/test/isolate_paused_listener_test.dart index b6016ce14..dc77408ef 100644 --- a/pkgs/coverage/test/isolate_paused_listener_test.dart +++ b/pkgs/coverage/test/isolate_paused_listener_test.dart @@ -129,73 +129,78 @@ void main() { expect(group.noRunningIsolates, isTrue); expect(group.noLiveIsolates, isTrue); - group.start('a'); - expect(group.running, unorderedEquals(['a'])); + group.start(IsolateRef(id: 'a')); + expect(group.running.map((iso) => iso.id).toList(), unorderedEquals(['a'])); expect(group.paused, isEmpty); expect(group.noRunningIsolates, isFalse); expect(group.noLiveIsolates, isFalse); - group.start('a'); - expect(group.running, unorderedEquals(['a'])); + group.start(IsolateRef(id: 'a')); + expect(group.running.map((iso) => iso.id).toList(), unorderedEquals(['a'])); expect(group.paused, isEmpty); expect(group.noRunningIsolates, isFalse); expect(group.noLiveIsolates, isFalse); - group.start('b'); - expect(group.running, unorderedEquals(['a', 'b'])); + group.start(IsolateRef(id: 'b')); + expect(group.running.map((iso) => iso.id).toList(), + unorderedEquals(['a', 'b'])); expect(group.paused, isEmpty); expect(group.noRunningIsolates, isFalse); expect(group.noLiveIsolates, isFalse); - group.pause('a'); - expect(group.running, unorderedEquals(['b'])); - expect(group.paused, unorderedEquals(['a'])); + group.pause(IsolateRef(id: 'a')); + expect(group.running.map((iso) => iso.id).toList(), unorderedEquals(['b'])); + expect(group.paused.map((iso) => iso.id).toList(), unorderedEquals(['a'])); expect(group.noRunningIsolates, isFalse); expect(group.noLiveIsolates, isFalse); - group.pause('a'); - expect(group.running, unorderedEquals(['b'])); - expect(group.paused, unorderedEquals(['a'])); + group.pause(IsolateRef(id: 'a')); + expect(group.running.map((iso) => iso.id).toList(), unorderedEquals(['b'])); + expect(group.paused.map((iso) => iso.id).toList(), unorderedEquals(['a'])); expect(group.noRunningIsolates, isFalse); expect(group.noLiveIsolates, isFalse); - group.pause('c'); - expect(group.running, unorderedEquals(['b'])); - expect(group.paused, unorderedEquals(['a', 'c'])); + group.pause(IsolateRef(id: 'c')); + expect(group.running.map((iso) => iso.id).toList(), unorderedEquals(['b'])); + expect(group.paused.map((iso) => iso.id).toList(), + unorderedEquals(['a', 'c'])); expect(group.noRunningIsolates, isFalse); expect(group.noLiveIsolates, isFalse); - group.start('c'); - expect(group.running, unorderedEquals(['b', 'c'])); - expect(group.paused, unorderedEquals(['a'])); + group.start(IsolateRef(id: 'c')); + expect(group.running.map((iso) => iso.id).toList(), + unorderedEquals(['b', 'c'])); + expect(group.paused.map((iso) => iso.id).toList(), unorderedEquals(['a'])); expect(group.noRunningIsolates, isFalse); expect(group.noLiveIsolates, isFalse); - group.pause('c'); - expect(group.running, unorderedEquals(['b'])); - expect(group.paused, unorderedEquals(['a', 'c'])); + group.pause(IsolateRef(id: 'c')); + expect(group.running.map((iso) => iso.id).toList(), unorderedEquals(['b'])); + expect(group.paused.map((iso) => iso.id).toList(), + unorderedEquals(['a', 'c'])); expect(group.noRunningIsolates, isFalse); expect(group.noLiveIsolates, isFalse); - group.exit('a'); - expect(group.running, unorderedEquals(['b'])); - expect(group.paused, unorderedEquals(['c'])); + group.exit(IsolateRef(id: 'a')); + expect(group.running.map((iso) => iso.id).toList(), unorderedEquals(['b'])); + expect(group.paused.map((iso) => iso.id).toList(), unorderedEquals(['c'])); expect(group.noRunningIsolates, isFalse); expect(group.noLiveIsolates, isFalse); - group.pause('b'); + group.pause(IsolateRef(id: 'b')); expect(group.running, isEmpty); - expect(group.paused, unorderedEquals(['b', 'c'])); + expect(group.paused.map((iso) => iso.id).toList(), + unorderedEquals(['b', 'c'])); expect(group.noRunningIsolates, isTrue); expect(group.noLiveIsolates, isFalse); - group.exit('b'); + group.exit(IsolateRef(id: 'b')); expect(group.running, isEmpty); - expect(group.paused, unorderedEquals(['c'])); + expect(group.paused.map((iso) => iso.id).toList(), unorderedEquals(['c'])); expect(group.noRunningIsolates, isTrue); expect(group.noLiveIsolates, isFalse); - group.exit('c'); + group.exit(IsolateRef(id: 'c')); expect(group.running, isEmpty); expect(group.paused, isEmpty); expect(group.noRunningIsolates, isTrue); @@ -528,7 +533,7 @@ void main() { service, (iso, isLastIsolateInGroup) async { expect(stopped, isFalse); - received.add('Pause ${iso.id}. Last in group ${iso.isolateGroupId}? ' + received.add('Pause ${iso.id}. Collect group ${iso.isolateGroupId}? ' '${isLastIsolateInGroup ? 'Yes' : 'No'}'); }, (message) => received.add(message), @@ -572,23 +577,23 @@ void main() { exitEvent('Z', '9'); expect(received, [ - 'Pause A. Last in group 1? No', + 'Pause A. Collect group 1? No', 'Resume A', - 'Pause B. Last in group 1? No', + 'Pause B. Collect group 1? No', 'Resume B', - 'Pause C. Last in group 1? Yes', + 'Pause C. Collect group 1? Yes', 'Resume C', - 'Pause F. Last in group 2? No', + 'Pause F. Collect group 2? No', 'Resume F', - 'Pause E. Last in group 2? No', + 'Pause E. Collect group 2? No', 'Resume E', - 'Pause I. Last in group 3? No', + 'Pause I. Collect group 3? No', 'Resume I', - 'Pause H. Last in group 3? No', + 'Pause H. Collect group 3? No', 'Resume H', - 'Pause D. Last in group 2? Yes', + 'Pause D. Collect group 2? Yes', 'Resume D', - 'Pause G. Last in group 3? Yes', + 'Pause G. Collect group 3? Yes', 'Resume G', ]); }); @@ -614,16 +619,16 @@ void main() { // B was paused correctly and was the last to exit isolate 1, so isolate 1 // was collected ok. expect(received, [ - 'Pause B. Last in group 1? Yes', + 'Pause B. Collect group 1? Yes', 'Resume B', - 'Pause D. Last in group 2? No', + 'Pause D. Collect group 2? No', 'Resume D', 'ERROR: An isolate exited without pausing, causing coverage data to ' 'be lost for group 2.', ]); }); - test('main isolate resumed last', () async { + test('subsequent main isolates are ignored', () async { startEvent('A', '1', 'main'); startEvent('B', '1', 'main'); // Second isolate named main, ignored. pauseEvent('B', '1', 'main'); @@ -639,17 +644,52 @@ void main() { await endTest(); expect(received, [ - 'Pause B. Last in group 1? No', + 'Pause B. Collect group 1? No', 'Resume B', - 'Pause C. Last in group 2? No', - 'Resume C', - 'Pause D. Last in group 2? Yes', - 'Resume D', - 'Pause A. Last in group 1? Yes', + 'Pause A. Collect group 1? Yes', + 'Pause C. Collect group 2? Yes', 'Resume A', ]); }); + test('all groups collected as soon as main isolate paused', () async { + startEvent('B', '1'); + startEvent('E', '2'); + startEvent('D', '2'); + startEvent('F', '2'); + pauseEvent('F', '2'); + startEvent('C', '2'); + startEvent('A', '1'); + pauseEvent('B', '1'); + pauseEvent('E', '2'); + startEvent('G', '3'); + pauseEvent('D', '2'); + startEvent('H', '3'); + exitEvent('F', '2'); + startEvent('I', '3'); + exitEvent('I', '3'); + + startEvent('M', '1', 'main'); + pauseEvent('M', '1', 'main'); + + await endTest(); + + expect(received, [ + 'Pause F. Collect group 2? No', + 'Resume F', + 'Pause B. Collect group 1? No', + 'Resume B', + 'Pause E. Collect group 2? No', + 'Resume E', + 'Pause D. Collect group 2? No', + 'Resume D', + 'Pause M. Collect group 1? Yes', + 'Pause C. Collect group 2? Yes', + 'Pause G. Collect group 3? Yes', + 'Resume M', + ]); + }); + test('main isolate exits without pausing', () async { startEvent('A', '1', 'main'); startEvent('B', '1'); @@ -660,7 +700,7 @@ void main() { await endTest(); expect(received, [ - 'Pause B. Last in group 1? No', + 'Pause B. Collect group 1? No', 'Resume B', 'ERROR: An isolate exited without pausing, causing coverage data to ' 'be lost for group 1.', @@ -674,7 +714,7 @@ void main() { await endTest(); expect(received, [ - 'Pause A. Last in group 1? Yes', + 'Pause A. Collect group 1? Yes', 'Resume A', ]); }); @@ -693,9 +733,9 @@ void main() { await endTest(); expect(received, [ - 'Pause B. Last in group 1? No', + 'Pause B. Collect group 1? No', 'Resume B', - 'Pause A. Last in group 1? Yes', + 'Pause A. Collect group 1? Yes', 'Resume A', ]); }); @@ -722,11 +762,11 @@ void main() { await endTest(); expect(received, [ - 'Pause B. Last in group 1? No', + 'Pause B. Collect group 1? No', 'Resume B', - 'Pause C. Last in group 1? No', + 'Pause C. Collect group 1? No', 'Resume C', - 'Pause A. Last in group 1? Yes', + 'Pause A. Collect group 1? Yes', 'Resume A', ]); }); @@ -753,11 +793,9 @@ void main() { await endTest(); expect(received, [ - 'Pause B. Last in group 1? No', + 'Pause B. Collect group 1? No', 'Resume B', - 'Pause C. Last in group 1? Yes', - 'Resume C', - 'Pause A. Last in group 1? No', + 'Pause A. Collect group 1? Yes', 'Resume A', ]); }); @@ -795,19 +833,19 @@ void main() { await endTest(); expect(received, [ - 'Pause Z. Last in group 9? Yes', + 'Pause Z. Collect group 9? Yes', 'Resume Z', - 'Pause A. Last in group 1? No', + 'Pause A. Collect group 1? No', 'Resume A', - 'Pause B. Last in group 1? Yes', + 'Pause B. Collect group 1? Yes', 'Resume B', - 'Pause E. Last in group 2? No', + 'Pause E. Collect group 2? No', 'Resume E', - 'Pause D. Last in group 2? Yes', + 'Pause D. Collect group 2? Yes', 'Resume D', - 'Pause F. Last in group 2? Yes', + 'Pause F. Collect group 2? Yes', 'Resume F', - 'Pause C. Last in group 1? Yes', + 'Pause C. Collect group 1? Yes', 'Resume C', ]); }); diff --git a/pkgs/coverage/test/run_and_collect_test.dart b/pkgs/coverage/test/run_and_collect_test.dart index e371f9000..a538a88a5 100644 --- a/pkgs/coverage/test/run_and_collect_test.dart +++ b/pkgs/coverage/test/run_and_collect_test.dart @@ -84,10 +84,10 @@ void checkIgnoredLinesInFilesCache( expect(ignoredLinesInFilesCache[packageUtilKey], isEmpty); expect(ignoredLinesInFilesCache[testAppKey], null /* means whole file */); expect(ignoredLinesInFilesCache[testAppIsolateKey], [ - [51, 51], - [53, 57], - [62, 65], - [66, 72] + [52, 52], + [54, 58], + [63, 66], + [67, 73] ]); } @@ -112,13 +112,14 @@ void checkHitmap(Map hitMap) { 39: 1, 41: 1, 42: 4, - 43: 1, - 44: 3, - 45: 1, - 48: 1, + 43: 2, + 44: 1, + 45: 3, + 46: 1, 49: 1, - 59: 1, - 60: 1 + 50: 1, + 60: 1, + 61: 1 }; expect(actualLineHits, expectedLineHits); diff --git a/pkgs/coverage/test/test_files/test_app_isolate.dart b/pkgs/coverage/test/test_files/test_app_isolate.dart index 8bbc6be31..22ccd75b3 100644 --- a/pkgs/coverage/test/test_files/test_app_isolate.dart +++ b/pkgs/coverage/test/test_files/test_app_isolate.dart @@ -40,6 +40,7 @@ void isolateTask(List threeThings) async { fooSync(answer); unawaited(fooAsync(answer).then((_) { + RawReceivePort().keepIsolateAlive = true; final port = threeThings.first as SendPort; final sum = (threeThings[1] as int) + (threeThings[2] as int); port.send(sum);