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

Commit 84f5ef3

Browse files
authored
Branch coverage (#361)
* WIP: branch coverage * Pretty printing of branch coverage * WIP: testing * Finish testing * Changelog and pubspec * Warn user if their VM is too old to support branch coverage * Update readme to mention function and branch coverage * Fix some of the failing tests on older VM versions * Fix run_and_collect_test * Actually fix run_and_collect_test * Use skips instead of ifs in tests
1 parent 6e8a258 commit 84f5ef3

13 files changed

+351
-50
lines changed

CHANGELOG.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
1-
## 1.1.1-dev
2-
1+
## 1.2.0-dev
2+
3+
* Support branch level coverage information, when running tests in the Dart VM.
4+
This is not supported for web tests yet.
5+
* Add flag `--branch-coverage` (abbr `-b`) to collect_coverage that collects
6+
branch coverage information. The VM must also be run with the
7+
`--branch-coverage` flag.
8+
* Add flag `--pretty-print-branch` to format_coverage that works
9+
similarly to pretty print, but outputs branch level coverage, rather than
10+
line level.
11+
* Update `--lcov` (abbr `-l`) in format_coverage to output branch level
12+
coverage, in addition to line level.
13+
* Add an optional bool flag to `collect` that controls whether branch coverage
14+
is collected.
15+
* Add a `branchHits` field to `HitMap`.
316
* Add support for scraping the service URI from the new Dart VM service message.
417

518
## 1.1.0 - 2022-1-18

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,30 @@ collected. If `--sdk-root` is set, Dart SDK coverage will also be output.
6161
- `// coverage:ignore-line` to ignore one line.
6262
- `// coverage:ignore-start` and `// coverage:ignore-end` to ignore range of lines inclusive.
6363
- `// coverage:ignore-file` to ignore the whole file.
64+
65+
#### Function and branch coverage
66+
67+
To gather function level coverage information, pass `--function-coverage` to
68+
collect_coverage:
69+
70+
```
71+
dart --pause-isolates-on-exit --disable-service-auth-codes --enable-vm-service=NNNN script.dart
72+
pub global run coverage:collect_coverage --uri=http://... -o coverage.json --resume-isolates --function-coverage
73+
```
74+
75+
To gather branch level coverage information, pass `--branch-coverage` to *both*
76+
collect_coverage and the Dart command you're gathering coverage from:
77+
78+
```
79+
dart --pause-isolates-on-exit --disable-service-auth-codes --enable-vm-service=NNNN --branch-coverage script.dart
80+
pub global run coverage:collect_coverage --uri=http://... -o coverage.json --resume-isolates --branch-coverage
81+
```
82+
83+
Branch coverage requires Dart VM 2.17.0, with service API v3.56. Function,
84+
branch, and line coverage can all be gathered at the same time, by combining
85+
those flags:
86+
87+
```
88+
dart --pause-isolates-on-exit --disable-service-auth-codes --enable-vm-service=NNNN --branch-coverage script.dart
89+
pub global run coverage:collect_coverage --uri=http://... -o coverage.json --resume-isolates --function-coverage --branch-coverage
90+
```

bin/collect_coverage.dart

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ Future<void> main(List<String> arguments) async {
2121
await Chain.capture(() async {
2222
final coverage = await collect(options.serviceUri, options.resume,
2323
options.waitPaused, options.includeDart, options.scopedOutput,
24-
timeout: options.timeout, functionCoverage: options.functionCoverage);
24+
timeout: options.timeout,
25+
functionCoverage: options.functionCoverage,
26+
branchCoverage: options.branchCoverage);
2527
options.out.write(json.encode(coverage));
2628
await options.out.close();
2729
}, onError: (dynamic error, Chain chain) {
@@ -34,8 +36,16 @@ Future<void> main(List<String> arguments) async {
3436
}
3537

3638
class Options {
37-
Options(this.serviceUri, this.out, this.timeout, this.waitPaused, this.resume,
38-
this.includeDart, this.functionCoverage, this.scopedOutput);
39+
Options(
40+
this.serviceUri,
41+
this.out,
42+
this.timeout,
43+
this.waitPaused,
44+
this.resume,
45+
this.includeDart,
46+
this.functionCoverage,
47+
this.branchCoverage,
48+
this.scopedOutput);
3949

4050
final Uri serviceUri;
4151
final IOSink out;
@@ -44,6 +54,7 @@ class Options {
4454
final bool resume;
4555
final bool includeDart;
4656
final bool functionCoverage;
57+
final bool branchCoverage;
4758
final Set<String> scopedOutput;
4859
}
4960

@@ -75,6 +86,11 @@ Options _parseArgs(List<String> arguments) {
7586
abbr: 'd', defaultsTo: false, help: 'include "dart:" libraries')
7687
..addFlag('function-coverage',
7788
abbr: 'f', defaultsTo: false, help: 'Collect function coverage info')
89+
..addFlag('branch-coverage',
90+
abbr: 'b',
91+
defaultsTo: false,
92+
help: 'Collect branch coverage info (Dart VM must also be run with '
93+
'--branch-coverage for this to work)')
7894
..addFlag('help', abbr: 'h', negatable: false, help: 'show this help');
7995

8096
final args = parser.parse(arguments);
@@ -127,6 +143,7 @@ Options _parseArgs(List<String> arguments) {
127143
args['resume-isolates'] as bool,
128144
args['include-dart'] as bool,
129145
args['function-coverage'] as bool,
146+
args['branch-coverage'] as bool,
130147
scopedOutput.toSet(),
131148
);
132149
}

bin/format_coverage.dart

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Environment {
2121
required this.packagesPath,
2222
required this.prettyPrint,
2323
required this.prettyPrintFunc,
24+
required this.prettyPrintBranch,
2425
required this.reportOn,
2526
required this.sdkRoot,
2627
required this.verbose,
@@ -37,6 +38,7 @@ class Environment {
3738
String? packagesPath;
3839
bool prettyPrint;
3940
bool prettyPrintFunc;
41+
bool prettyPrintBranch;
4042
List<String>? reportOn;
4143
String? sdkRoot;
4244
bool verbose;
@@ -74,9 +76,11 @@ Future<void> main(List<String> arguments) async {
7476
? BazelResolver(workspacePath: env.bazelWorkspace)
7577
: Resolver(packagesPath: env.packagesPath, sdkRoot: env.sdkRoot);
7678
final loader = Loader();
77-
if (env.prettyPrint || env.prettyPrintFunc) {
79+
if (env.prettyPrint) {
7880
output = await hitmap.prettyPrint(resolver, loader,
79-
reportOn: env.reportOn, reportFuncs: env.prettyPrintFunc);
81+
reportOn: env.reportOn,
82+
reportFuncs: env.prettyPrintFunc,
83+
reportBranches: env.prettyPrintBranch);
8084
} else {
8185
assert(env.lcov);
8286
output = hitmap.formatLcov(resolver,
@@ -135,6 +139,9 @@ Environment parseArgs(List<String> arguments) {
135139
abbr: 'f',
136140
negatable: false,
137141
help: 'convert function coverage data to pretty print format');
142+
parser.addFlag('pretty-print-branch',
143+
negatable: false,
144+
help: 'convert branch coverage data to pretty print format');
138145
parser.addFlag('lcov',
139146
abbr: 'l',
140147
negatable: false,
@@ -217,12 +224,17 @@ Environment parseArgs(List<String> arguments) {
217224
final lcov = args['lcov'] as bool;
218225
var prettyPrint = args['pretty-print'] as bool;
219226
final prettyPrintFunc = args['pretty-print-func'] as bool;
220-
if ((prettyPrint ? 1 : 0) + (prettyPrintFunc ? 1 : 0) + (lcov ? 1 : 0) > 1) {
227+
final prettyPrintBranch = args['pretty-print-branch'] as bool;
228+
final numModesChosen = (prettyPrint ? 1 : 0) +
229+
(prettyPrintFunc ? 1 : 0) +
230+
(prettyPrintBranch ? 1 : 0) +
231+
(lcov ? 1 : 0);
232+
if (numModesChosen > 1) {
221233
fail('Choose one of the pretty-print modes or lcov output');
222234
}
223235

224-
// Use pretty-print either explicitly or by default.
225-
if (!lcov && !prettyPrintFunc) prettyPrint = true;
236+
// The pretty printer is used by all modes other than lcov.
237+
if (!lcov) prettyPrint = true;
226238

227239
int workers;
228240
try {
@@ -244,6 +256,7 @@ Environment parseArgs(List<String> arguments) {
244256
packagesPath: packagesPath,
245257
prettyPrint: prettyPrint,
246258
prettyPrintFunc: prettyPrintFunc,
259+
prettyPrintBranch: prettyPrintBranch,
247260
reportOn: reportOn,
248261
sdkRoot: sdkRoot,
249262
verbose: verbose,

lib/src/collect.dart

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ const _retryInterval = Duration(milliseconds: 200);
3333
/// If [functionCoverage] is true, function coverage information will be
3434
/// collected.
3535
///
36+
/// If [branchCoverage] is true, branch coverage information will be collected.
37+
/// This will only work correctly if the target VM was run with the
38+
/// --branch-coverage flag.
39+
///
3640
/// If [scopedOutput] is non-empty, coverage will be restricted so that only
3741
/// scripts that start with any of the provided paths are considered.
3842
///
@@ -42,7 +46,8 @@ Future<Map<String, dynamic>> collect(Uri serviceUri, bool resume,
4246
bool waitPaused, bool includeDart, Set<String>? scopedOutput,
4347
{Set<String>? isolateIds,
4448
Duration? timeout,
45-
bool functionCoverage = false}) async {
49+
bool functionCoverage = false,
50+
bool branchCoverage = false}) async {
4651
scopedOutput ??= <String>{};
4752

4853
// Create websocket URI. Handle any trailing slashes.
@@ -76,8 +81,8 @@ Future<Map<String, dynamic>> collect(Uri serviceUri, bool resume,
7681
await _waitIsolatesPaused(service, timeout: timeout);
7782
}
7883

79-
return await _getAllCoverage(
80-
service, includeDart, functionCoverage, scopedOutput, isolateIds);
84+
return await _getAllCoverage(service, includeDart, functionCoverage,
85+
branchCoverage, scopedOutput, isolateIds);
8186
} finally {
8287
if (resume) {
8388
await _resumeIsolates(service);
@@ -88,19 +93,34 @@ Future<Map<String, dynamic>> collect(Uri serviceUri, bool resume,
8893
}
8994
}
9095

96+
bool _versionCheck(Version version, int minMajor, int minMinor) {
97+
final major = version.major ?? 0;
98+
final minor = version.minor ?? 0;
99+
return major > minMajor || (major == minMajor && minor >= minMinor);
100+
}
101+
91102
Future<Map<String, dynamic>> _getAllCoverage(
92103
VmService service,
93104
bool includeDart,
94105
bool functionCoverage,
106+
bool branchCoverage,
95107
Set<String>? scopedOutput,
96108
Set<String>? isolateIds) async {
97109
scopedOutput ??= <String>{};
98110
final vm = await service.getVM();
99111
final allCoverage = <Map<String, dynamic>>[];
100112
final version = await service.getVersion();
101-
final reportLines =
102-
(version.major == 3 && version.minor != null && version.minor! >= 51) ||
103-
(version.major != null && version.major! > 3);
113+
final reportLines = _versionCheck(version, 3, 51);
114+
final branchCoverageSupported = _versionCheck(version, 3, 56);
115+
if (branchCoverage && !branchCoverageSupported) {
116+
branchCoverage = false;
117+
stderr.write('Branch coverage was requested, but is not supported'
118+
' by the VM version. Try updating to a newer version of Dart');
119+
}
120+
final sourceReportKinds = [
121+
SourceReportKind.kCoverage,
122+
if (branchCoverage) SourceReportKind.kBranchCoverage,
123+
];
104124

105125
// Program counters are shared between isolates in the same group. So we need
106126
// to make sure we're only gathering coverage data for one isolate in each
@@ -130,7 +150,7 @@ Future<Map<String, dynamic>> _getAllCoverage(
130150
// Skip scripts which should not be included in the report.
131151
if (!scopedOutput.contains(scope)) continue;
132152
final scriptReport = await service.getSourceReport(
133-
isolateRef.id!, <String>[SourceReportKind.kCoverage],
153+
isolateRef.id!, sourceReportKinds,
134154
forceCompile: true,
135155
scriptId: script.id,
136156
reportLines: reportLines ? true : null);
@@ -141,7 +161,7 @@ Future<Map<String, dynamic>> _getAllCoverage(
141161
} else {
142162
final isolateReport = await service.getSourceReport(
143163
isolateRef.id!,
144-
<String>[SourceReportKind.kCoverage],
164+
sourceReportKinds,
145165
forceCompile: true,
146166
reportLines: reportLines ? true : null,
147167
);
@@ -231,7 +251,8 @@ Future<void> _processFunction(VmService service, IsolateRef isolateRef,
231251
final line = _getLineFromTokenPos(script, tokenPos);
232252

233253
if (line == null) {
234-
print('tokenPos $tokenPos has no line mapping for script ${script.uri!}');
254+
stderr.write(
255+
'tokenPos $tokenPos has no line mapping for script ${script.uri!}');
235256
return;
236257
}
237258
hits.funcNames![line] = funcName;
@@ -306,28 +327,41 @@ Future<List<Map<String, dynamic>>> _getCoverageJson(
306327

307328
if (coverage == null) continue;
308329

309-
for (final pos in coverage.hits!) {
310-
final line = reportLines ? pos : _getLineFromTokenPos(script!, pos);
311-
if (line == null) {
312-
print('tokenPos $pos has no line mapping for script $scriptUri');
313-
continue;
330+
void forEachLine(List<int> tokenPositions, void Function(int line) body) {
331+
for (final pos in tokenPositions) {
332+
final line = reportLines ? pos : _getLineFromTokenPos(script!, pos);
333+
if (line == null) {
334+
stderr
335+
.write('tokenPos $pos has no line mapping for script $scriptUri');
336+
continue;
337+
}
338+
body(line);
314339
}
340+
}
341+
342+
forEachLine(coverage.hits!, (line) {
315343
_incrementCountForKey(hits.lineHits, line);
316344
if (hits.funcNames != null && hits.funcNames!.containsKey(line)) {
317345
_incrementCountForKey(hits.funcHits!, line);
318346
}
319-
}
320-
for (final pos in coverage.misses!) {
321-
final line = reportLines ? pos : _getLineFromTokenPos(script!, pos);
322-
if (line == null) {
323-
print('tokenPos $pos has no line mapping for script $scriptUri');
324-
continue;
325-
}
347+
});
348+
forEachLine(coverage.misses!, (line) {
326349
hits.lineHits.putIfAbsent(line, () => 0);
327-
}
350+
});
328351
hits.funcNames?.forEach((line, funcName) {
329352
hits.funcHits?.putIfAbsent(line, () => 0);
330353
});
354+
355+
final branchCoverage = range.branchCoverage;
356+
if (branchCoverage != null) {
357+
hits.branchHits ??= <int, int>{};
358+
forEachLine(branchCoverage.hits!, (line) {
359+
_incrementCountForKey(hits.branchHits!, line);
360+
});
361+
forEachLine(branchCoverage.misses!, (line) {
362+
hits.branchHits!.putIfAbsent(line, () => 0);
363+
});
364+
}
331365
}
332366

333367
// Output JSON

lib/src/formatter.dart

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ extension FileHitMapsFormatter on Map<String, HitMap> {
8484
final lineHits = v.lineHits;
8585
final funcHits = v.funcHits;
8686
final funcNames = v.funcNames;
87+
final branchHits = v.branchHits;
8788
var source = resolver.resolve(entry.key);
8889
if (source == null) {
8990
continue;
@@ -115,6 +116,11 @@ extension FileHitMapsFormatter on Map<String, HitMap> {
115116
}
116117
buf.write('LF:${lineHits.length}\n');
117118
buf.write('LH:${lineHits.values.where((v) => v > 0).length}\n');
119+
if (branchHits != null) {
120+
for (final k in branchHits.keys.toList()..sort()) {
121+
buf.write('BRDA:$k,0,0,${branchHits[k]}\n');
122+
}
123+
}
118124
buf.write('end_of_record\n');
119125
}
120126

@@ -131,6 +137,7 @@ extension FileHitMapsFormatter on Map<String, HitMap> {
131137
Loader loader, {
132138
List<String>? reportOn,
133139
bool reportFuncs = false,
140+
bool reportBranches = false,
134141
}) async {
135142
final pathFilter = _getPathFilter(reportOn);
136143
final buf = StringBuffer();
@@ -141,7 +148,16 @@ extension FileHitMapsFormatter on Map<String, HitMap> {
141148
'missing function coverage information. Did you run '
142149
'collect_coverage with the --function-coverage flag?';
143150
}
144-
final hits = reportFuncs ? v.funcHits! : v.lineHits;
151+
if (reportBranches && v.branchHits == null) {
152+
throw 'Branch coverage formatting was requested, but the hit map is '
153+
'missing branch coverage information. Did you run '
154+
'collect_coverage with the --branch-coverage flag?';
155+
}
156+
final hits = reportFuncs
157+
? v.funcHits!
158+
: reportBranches
159+
? v.branchHits!
160+
: v.lineHits;
145161
final source = resolver.resolve(entry.key);
146162
if (source == null) {
147163
continue;

0 commit comments

Comments
 (0)