Skip to content
This repository was archived by the owner on Aug 28, 2024. It is now read-only.

Commit 908b55d

Browse files
authored
Add coverableLineCache param to collect (#466)
* Add coverableLineCache param to collect * Debugging collect_coverage_api_test * fmt * Add integration test * Bump min SDK version * Ben's comments
1 parent bcfd888 commit 908b55d

File tree

6 files changed

+295
-25
lines changed

6 files changed

+295
-25
lines changed

.github/workflows/test-package.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646
fail-fast: false
4747
matrix:
4848
os: [ubuntu-latest, macos-latest, windows-latest]
49-
sdk: [2.19.0, dev]
49+
sdk: [3.0.0, dev]
5050
steps:
5151
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
5252
- uses: dart-lang/setup-dart@8a4b97ea2017cc079571daec46542f76189836b1

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## 1.7.0
2+
3+
- Require Dart 3.0.0
4+
- Update `package:vm_service` constraints to '^12.0.0'.
5+
- Add `coverableLineCache` parameter to `collect`. This allows the set of
6+
coverable lines to be cached between calls to `collect`, avoiding the need to
7+
force compile the same libraries repeatedly. This is only useful when running
8+
multiple coverage collections over the same libraries.
9+
110
## 1.6.4
211

312
- allow omitting space between `//` and `coverage` in coverage ignore comments

lib/src/collect.dart

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,15 @@ const _debugTokenPositions = bool.fromEnvironment('DEBUG_COVERAGE');
4141
/// If [scopedOutput] is non-empty, coverage will be restricted so that only
4242
/// scripts that start with any of the provided paths are considered.
4343
///
44-
/// if [isolateIds] is set, the coverage gathering will be restricted to only
44+
/// If [isolateIds] is set, the coverage gathering will be restricted to only
4545
/// those VM isolates.
4646
///
47+
/// If [coverableLineCache] is set, the collector will avoid recompiling
48+
/// libraries it has already seen (see VmService.getSourceReport's
49+
/// librariesAlreadyCompiled parameter). This is only useful when doing more
50+
/// than one [collect] call over the same libraries. Pass an empty map to the
51+
/// first call, and then pass the same map to all subsequent calls.
52+
///
4753
/// [serviceOverrideForTesting] is for internal testing only, and should not be
4854
/// set by users.
4955
Future<Map<String, dynamic>> collect(Uri serviceUri, bool resume,
@@ -52,6 +58,7 @@ Future<Map<String, dynamic>> collect(Uri serviceUri, bool resume,
5258
Duration? timeout,
5359
bool functionCoverage = false,
5460
bool branchCoverage = false,
61+
Map<String, Set<int>>? coverableLineCache,
5562
VmService? serviceOverrideForTesting}) async {
5663
scopedOutput ??= <String>{};
5764

@@ -92,7 +99,7 @@ Future<Map<String, dynamic>> collect(Uri serviceUri, bool resume,
9299
}
93100

94101
return await _getAllCoverage(service, includeDart, functionCoverage,
95-
branchCoverage, scopedOutput, isolateIds);
102+
branchCoverage, scopedOutput, isolateIds, coverableLineCache);
96103
} finally {
97104
if (resume) {
98105
await _resumeIsolates(service);
@@ -115,7 +122,8 @@ Future<Map<String, dynamic>> _getAllCoverage(
115122
bool functionCoverage,
116123
bool branchCoverage,
117124
Set<String>? scopedOutput,
118-
Set<String>? isolateIds) async {
125+
Set<String>? isolateIds,
126+
Map<String, Set<int>>? coverableLineCache) async {
119127
scopedOutput ??= <String>{};
120128
final vm = await service.getVM();
121129
final allCoverage = <Map<String, dynamic>>[];
@@ -124,16 +132,22 @@ Future<Map<String, dynamic>> _getAllCoverage(
124132
final branchCoverageSupported = _versionCheck(version, 3, 56);
125133
final libraryFilters = _versionCheck(version, 3, 57);
126134
final fastIsoGroups = _versionCheck(version, 3, 61);
135+
final lineCacheSupported = _versionCheck(version, 4, 13);
136+
127137
if (branchCoverage && !branchCoverageSupported) {
128138
branchCoverage = false;
129139
stderr.writeln('Branch coverage was requested, but is not supported'
130140
' by the VM version. Try updating to a newer version of Dart');
131141
}
142+
132143
final sourceReportKinds = [
133144
SourceReportKind.kCoverage,
134145
if (branchCoverage) SourceReportKind.kBranchCoverage,
135146
];
136147

148+
final librariesAlreadyCompiled =
149+
lineCacheSupported ? coverableLineCache?.keys.toList() : null;
150+
137151
// Program counters are shared between isolates in the same group. So we need
138152
// to make sure we're only gathering coverage data for one isolate in each
139153
// group, otherwise we'll double count the hits.
@@ -173,15 +187,24 @@ Future<Map<String, dynamic>> _getAllCoverage(
173187
late final SourceReport scriptReport;
174188
try {
175189
scriptReport = await service.getSourceReport(
176-
isolateRef.id!, sourceReportKinds,
177-
forceCompile: true,
178-
scriptId: script.id,
179-
reportLines: reportLines ? true : null);
190+
isolateRef.id!,
191+
sourceReportKinds,
192+
forceCompile: true,
193+
scriptId: script.id,
194+
reportLines: reportLines ? true : null,
195+
librariesAlreadyCompiled: librariesAlreadyCompiled,
196+
);
180197
} on SentinelException {
181198
continue;
182199
}
183-
final coverage = await _getCoverageJson(service, isolateRef,
184-
scriptReport, includeDart, functionCoverage, reportLines);
200+
final coverage = await _processSourceReport(
201+
service,
202+
isolateRef,
203+
scriptReport,
204+
includeDart,
205+
functionCoverage,
206+
reportLines,
207+
coverableLineCache);
185208
allCoverage.addAll(coverage);
186209
}
187210
} else {
@@ -195,12 +218,19 @@ Future<Map<String, dynamic>> _getAllCoverage(
195218
libraryFilters: scopedOutput.isNotEmpty && libraryFilters
196219
? List.from(scopedOutput.map((filter) => 'package:$filter/'))
197220
: null,
221+
librariesAlreadyCompiled: librariesAlreadyCompiled,
198222
);
199223
} on SentinelException {
200224
continue;
201225
}
202-
final coverage = await _getCoverageJson(service, isolateRef,
203-
isolateReport, includeDart, functionCoverage, reportLines);
226+
final coverage = await _processSourceReport(
227+
service,
228+
isolateRef,
229+
isolateReport,
230+
includeDart,
231+
functionCoverage,
232+
reportLines,
233+
coverableLineCache);
204234
allCoverage.addAll(coverage);
205235
}
206236
}
@@ -276,13 +306,14 @@ int? _getLineFromTokenPos(Script script, int tokenPos) {
276306
}
277307

278308
/// Returns a JSON coverage list backward-compatible with pre-1.16.0 SDKs.
279-
Future<List<Map<String, dynamic>>> _getCoverageJson(
309+
Future<List<Map<String, dynamic>>> _processSourceReport(
280310
VmService service,
281311
IsolateRef isolateRef,
282312
SourceReport report,
283313
bool includeDart,
284314
bool functionCoverage,
285-
bool reportLines) async {
315+
bool reportLines,
316+
Map<String, Set<int>>? coverableLineCache) async {
286317
final hitMaps = <Uri, HitMap>{};
287318
final scripts = <ScriptRef, Script>{};
288319
final libraries = <LibraryRef>{};
@@ -333,7 +364,16 @@ Future<List<Map<String, dynamic>>> _getCoverageJson(
333364

334365
for (var range in report.ranges!) {
335366
final scriptRef = report.scripts![range.scriptIndex!];
336-
final scriptUri = Uri.parse(scriptRef.uri!);
367+
final scriptUriString = scriptRef.uri!;
368+
final scriptUri = Uri.parse(scriptUriString);
369+
370+
// If we have a coverableLineCache, use it in the same way we use
371+
// SourceReportCoverage.misses: to add zeros to the coverage result for all
372+
// the lines that don't have a hit. Afterwards, add all the lines that were
373+
// hit or missed to the cache, so that the next coverage collection won't
374+
// need to compile this libarry.
375+
final coverableLines =
376+
coverableLineCache?.putIfAbsent(scriptUriString, () => <int>{});
337377

338378
// Not returned in scripts section of source report.
339379
if (scriptUri.scheme == 'evaluate') continue;
@@ -379,7 +419,8 @@ Future<List<Map<String, dynamic>>> _getCoverageJson(
379419

380420
if (coverage == null) continue;
381421

382-
void forEachLine(List<int> tokenPositions, void Function(int line) body) {
422+
void forEachLine(List<int>? tokenPositions, void Function(int line) body) {
423+
if (tokenPositions == null) return;
383424
for (final pos in tokenPositions) {
384425
final line = reportLines ? pos : _getLineFromTokenPos(script!, pos);
385426
if (line == null) {
@@ -393,14 +434,22 @@ Future<List<Map<String, dynamic>>> _getCoverageJson(
393434
}
394435
}
395436

396-
forEachLine(coverage.hits!, (line) {
437+
if (coverableLines != null) {
438+
for (final line in coverableLines) {
439+
hits.lineHits.putIfAbsent(line, () => 0);
440+
}
441+
}
442+
443+
forEachLine(coverage.hits, (line) {
397444
hits.lineHits.increment(line);
445+
coverableLines?.add(line);
398446
if (hits.funcNames != null && hits.funcNames!.containsKey(line)) {
399447
hits.funcHits!.increment(line);
400448
}
401449
});
402-
forEachLine(coverage.misses!, (line) {
450+
forEachLine(coverage.misses, (line) {
403451
hits.lineHits.putIfAbsent(line, () => 0);
452+
coverableLines?.add(line);
404453
});
405454
hits.funcNames?.forEach((line, funcName) {
406455
hits.funcHits?.putIfAbsent(line, () => 0);
@@ -409,10 +458,10 @@ Future<List<Map<String, dynamic>>> _getCoverageJson(
409458
final branchCoverage = range.branchCoverage;
410459
if (branchCoverage != null) {
411460
hits.branchHits ??= <int, int>{};
412-
forEachLine(branchCoverage.hits!, (line) {
461+
forEachLine(branchCoverage.hits, (line) {
413462
hits.branchHits!.increment(line);
414463
});
415-
forEachLine(branchCoverage.misses!, (line) {
464+
forEachLine(branchCoverage.misses, (line) {
416465
hits.branchHits!.putIfAbsent(line, () => 0);
417466
});
418467
}

pubspec.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
name: coverage
2-
version: 1.6.4
2+
version: 1.7.0
33
description: Coverage data manipulation and formatting
44
repository: https://github.com/dart-lang/coverage
55

66
environment:
7-
sdk: '>=2.18.0 <4.0.0'
7+
sdk: '^3.0.0'
88

99
dependencies:
1010
args: ^2.0.0
@@ -13,7 +13,7 @@ dependencies:
1313
path: ^1.8.0
1414
source_maps: ^0.10.10
1515
stack_trace: ^1.10.0
16-
vm_service: '>=11.9.0 <13.0.0'
16+
vm_service: '^12.0.0'
1717

1818
dev_dependencies:
1919
benchmark_harness: ^2.2.0

test/collect_coverage_api_test.dart

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,51 @@ void main() {
9595
expect(sources[_isolateLibFileUri],
9696
everyElement(containsPair('branchHits', isNotEmpty)));
9797
}, skip: !platformVersionCheck(2, 17));
98+
99+
test('collect_coverage_api with coverableLineCache', () async {
100+
final coverableLineCache = <String, Set<int>>{};
101+
final coverage =
102+
await _collectCoverage(coverableLineCache: coverableLineCache);
103+
final result = await HitMap.parseJson(
104+
coverage['coverage'] as List<Map<String, dynamic>>);
105+
106+
expect(coverableLineCache, contains(_sampleAppFileUri));
107+
expect(coverableLineCache, contains(_isolateLibFileUri));
108+
109+
// Expect that we have some missed lines.
110+
expect(result[_sampleAppFileUri]!.lineHits.containsValue(0), isTrue);
111+
expect(result[_isolateLibFileUri]!.lineHits.containsValue(0), isTrue);
112+
113+
// Clear _sampleAppFileUri's cache entry, then gather coverage again. We're
114+
// doing this to verify that force compilation is disabled for these
115+
// libraries. The result should be that _isolateLibFileUri should be the
116+
// same, but _sampleAppFileUri should be missing all its missed lines.
117+
coverableLineCache[_sampleAppFileUri] = {};
118+
final coverage2 =
119+
await _collectCoverage(coverableLineCache: coverableLineCache);
120+
final result2 = await HitMap.parseJson(
121+
coverage2['coverage'] as List<Map<String, dynamic>>);
122+
123+
// _isolateLibFileUri still has missed lines, but _sampleAppFileUri doesn't.
124+
expect(result2[_sampleAppFileUri]!.lineHits.containsValue(0), isFalse);
125+
expect(result2[_isolateLibFileUri]!.lineHits.containsValue(0), isTrue);
126+
127+
// _isolateLibFileUri is the same. _sampleAppFileUri is the same, but
128+
// without all its missed lines.
129+
expect(result2[_isolateLibFileUri]!.lineHits,
130+
result[_isolateLibFileUri]!.lineHits);
131+
result[_sampleAppFileUri]!.lineHits.removeWhere((line, hits) => hits == 0);
132+
expect(result2[_sampleAppFileUri]!.lineHits,
133+
result[_sampleAppFileUri]!.lineHits);
134+
}, skip: !platformVersionCheck(3, 2));
98135
}
99136

100137
Future<Map<String, dynamic>> _collectCoverage(
101138
{Set<String> scopedOutput = const {},
102139
bool isolateIds = false,
103140
bool functionCoverage = false,
104-
bool branchCoverage = false}) async {
141+
bool branchCoverage = false,
142+
Map<String, Set<int>>? coverableLineCache}) async {
105143
final openPort = await getOpenPort();
106144

107145
// run the sample app, with the right flags
@@ -114,5 +152,6 @@ Future<Map<String, dynamic>> _collectCoverage(
114152
timeout: timeout,
115153
isolateIds: isolateIdSet,
116154
functionCoverage: functionCoverage,
117-
branchCoverage: branchCoverage);
155+
branchCoverage: branchCoverage,
156+
coverableLineCache: coverableLineCache);
118157
}

0 commit comments

Comments
 (0)