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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 301d577..ef3500d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,18 @@
-## 1.1.1-dev
+## 1.2.0-dev
+* Support branch level coverage information, when running tests in the Dart VM.
+ This is not supported for web tests yet.
+* Add flag `--branch-coverage` (abbr `-b`) to collect_coverage that collects
+ branch coverage information. The VM must also be run with the
+ `--branch-coverage` flag.
+* Add flag `--pretty-print-branch` to format_coverage that works
+ similarly to pretty print, but outputs branch level coverage, rather than
+ line level.
+* Update `--lcov` (abbr `-l`) in format_coverage to output branch level
+ coverage, in addition to line level.
+* Add an optional bool flag to `collect` that controls whether branch coverage
+ is collected.
+* Add a `branchHits` field to `HitMap`.
* Add support for scraping the service URI from the new Dart VM service message.
## 1.1.0 - 2022-1-18
diff --git a/README.md b/README.md
index 57ace83..3d038df 100644
--- a/README.md
+++ b/README.md
@@ -61,3 +61,30 @@
- `// coverage:ignore-line` to ignore one line.
- `// coverage:ignore-start` and `// coverage:ignore-end` to ignore range of lines inclusive.
- `// coverage:ignore-file` to ignore the whole file.
+
+#### Function and branch coverage
+
+To gather function level coverage information, pass `--function-coverage` to
+collect_coverage:
+
+```
+dart --pause-isolates-on-exit --disable-service-auth-codes --enable-vm-service=NNNN script.dart
+pub global run coverage:collect_coverage --uri=http://... -o coverage.json --resume-isolates --function-coverage
+```
+
+To gather branch level coverage information, pass `--branch-coverage` to *both*
+collect_coverage and the Dart command you're gathering coverage from:
+
+```
+dart --pause-isolates-on-exit --disable-service-auth-codes --enable-vm-service=NNNN --branch-coverage script.dart
+pub global run coverage:collect_coverage --uri=http://... -o coverage.json --resume-isolates --branch-coverage
+```
+
+Branch coverage requires Dart VM 2.17.0, with service API v3.56. Function,
+branch, and line coverage can all be gathered at the same time, by combining
+those flags:
+
+```
+dart --pause-isolates-on-exit --disable-service-auth-codes --enable-vm-service=NNNN --branch-coverage script.dart
+pub global run coverage:collect_coverage --uri=http://... -o coverage.json --resume-isolates --function-coverage --branch-coverage
+```
diff --git a/bin/collect_coverage.dart b/bin/collect_coverage.dart
index 47ab4e0..b02ac21 100644
--- a/bin/collect_coverage.dart
+++ b/bin/collect_coverage.dart
@@ -21,7 +21,9 @@
await Chain.capture(() async {
final coverage = await collect(options.serviceUri, options.resume,
options.waitPaused, options.includeDart, options.scopedOutput,
- timeout: options.timeout, functionCoverage: options.functionCoverage);
+ timeout: options.timeout,
+ functionCoverage: options.functionCoverage,
+ branchCoverage: options.branchCoverage);
options.out.write(json.encode(coverage));
await options.out.close();
}, onError: (dynamic error, Chain chain) {
@@ -34,8 +36,16 @@
}
class Options {
- Options(this.serviceUri, this.out, this.timeout, this.waitPaused, this.resume,
- this.includeDart, this.functionCoverage, this.scopedOutput);
+ Options(
+ this.serviceUri,
+ this.out,
+ this.timeout,
+ this.waitPaused,
+ this.resume,
+ this.includeDart,
+ this.functionCoverage,
+ this.branchCoverage,
+ this.scopedOutput);
final Uri serviceUri;
final IOSink out;
@@ -44,6 +54,7 @@
final bool resume;
final bool includeDart;
final bool functionCoverage;
+ final bool branchCoverage;
final Set<String> scopedOutput;
}
@@ -75,6 +86,11 @@
abbr: 'd', defaultsTo: false, help: 'include "dart:" libraries')
..addFlag('function-coverage',
abbr: 'f', defaultsTo: false, help: 'Collect function coverage info')
+ ..addFlag('branch-coverage',
+ abbr: 'b',
+ defaultsTo: false,
+ help: 'Collect branch coverage info (Dart VM must also be run with '
+ '--branch-coverage for this to work)')
..addFlag('help', abbr: 'h', negatable: false, help: 'show this help');
final args = parser.parse(arguments);
@@ -127,6 +143,7 @@
args['resume-isolates'] as bool,
args['include-dart'] as bool,
args['function-coverage'] as bool,
+ args['branch-coverage'] as bool,
scopedOutput.toSet(),
);
}
diff --git a/bin/format_coverage.dart b/bin/format_coverage.dart
index 27d2e71..63fc48c 100644
--- a/bin/format_coverage.dart
+++ b/bin/format_coverage.dart
@@ -21,6 +21,7 @@
required this.packagesPath,
required this.prettyPrint,
required this.prettyPrintFunc,
+ required this.prettyPrintBranch,
required this.reportOn,
required this.sdkRoot,
required this.verbose,
@@ -37,6 +38,7 @@
String? packagesPath;
bool prettyPrint;
bool prettyPrintFunc;
+ bool prettyPrintBranch;
List<String>? reportOn;
String? sdkRoot;
bool verbose;
@@ -74,9 +76,11 @@
? BazelResolver(workspacePath: env.bazelWorkspace)
: Resolver(packagesPath: env.packagesPath, sdkRoot: env.sdkRoot);
final loader = Loader();
- if (env.prettyPrint || env.prettyPrintFunc) {
+ if (env.prettyPrint) {
output = await hitmap.prettyPrint(resolver, loader,
- reportOn: env.reportOn, reportFuncs: env.prettyPrintFunc);
+ reportOn: env.reportOn,
+ reportFuncs: env.prettyPrintFunc,
+ reportBranches: env.prettyPrintBranch);
} else {
assert(env.lcov);
output = hitmap.formatLcov(resolver,
@@ -135,6 +139,9 @@
abbr: 'f',
negatable: false,
help: 'convert function coverage data to pretty print format');
+ parser.addFlag('pretty-print-branch',
+ negatable: false,
+ help: 'convert branch coverage data to pretty print format');
parser.addFlag('lcov',
abbr: 'l',
negatable: false,
@@ -217,12 +224,17 @@
final lcov = args['lcov'] as bool;
var prettyPrint = args['pretty-print'] as bool;
final prettyPrintFunc = args['pretty-print-func'] as bool;
- if ((prettyPrint ? 1 : 0) + (prettyPrintFunc ? 1 : 0) + (lcov ? 1 : 0) > 1) {
+ final prettyPrintBranch = args['pretty-print-branch'] as bool;
+ final numModesChosen = (prettyPrint ? 1 : 0) +
+ (prettyPrintFunc ? 1 : 0) +
+ (prettyPrintBranch ? 1 : 0) +
+ (lcov ? 1 : 0);
+ if (numModesChosen > 1) {
fail('Choose one of the pretty-print modes or lcov output');
}
- // Use pretty-print either explicitly or by default.
- if (!lcov && !prettyPrintFunc) prettyPrint = true;
+ // The pretty printer is used by all modes other than lcov.
+ if (!lcov) prettyPrint = true;
int workers;
try {
@@ -244,6 +256,7 @@
packagesPath: packagesPath,
prettyPrint: prettyPrint,
prettyPrintFunc: prettyPrintFunc,
+ prettyPrintBranch: prettyPrintBranch,
reportOn: reportOn,
sdkRoot: sdkRoot,
verbose: verbose,
diff --git a/lib/src/collect.dart b/lib/src/collect.dart
index 5cd6695..0a5fc24 100644
--- a/lib/src/collect.dart
+++ b/lib/src/collect.dart
@@ -33,6 +33,10 @@
/// If [functionCoverage] is true, function coverage information will be
/// collected.
///
+/// If [branchCoverage] is true, branch coverage information will be collected.
+/// This will only work correctly if the target VM was run with the
+/// --branch-coverage flag.
+///
/// If [scopedOutput] is non-empty, coverage will be restricted so that only
/// scripts that start with any of the provided paths are considered.
///
@@ -42,7 +46,8 @@
bool waitPaused, bool includeDart, Set<String>? scopedOutput,
{Set<String>? isolateIds,
Duration? timeout,
- bool functionCoverage = false}) async {
+ bool functionCoverage = false,
+ bool branchCoverage = false}) async {
scopedOutput ??= <String>{};
// Create websocket URI. Handle any trailing slashes.
@@ -76,8 +81,8 @@
await _waitIsolatesPaused(service, timeout: timeout);
}
- return await _getAllCoverage(
- service, includeDart, functionCoverage, scopedOutput, isolateIds);
+ return await _getAllCoverage(service, includeDart, functionCoverage,
+ branchCoverage, scopedOutput, isolateIds);
} finally {
if (resume) {
await _resumeIsolates(service);
@@ -88,19 +93,34 @@
}
}
+bool _versionCheck(Version version, int minMajor, int minMinor) {
+ final major = version.major ?? 0;
+ final minor = version.minor ?? 0;
+ return major > minMajor || (major == minMajor && minor >= minMinor);
+}
+
Future<Map<String, dynamic>> _getAllCoverage(
VmService service,
bool includeDart,
bool functionCoverage,
+ bool branchCoverage,
Set<String>? scopedOutput,
Set<String>? isolateIds) async {
scopedOutput ??= <String>{};
final vm = await service.getVM();
final allCoverage = <Map<String, dynamic>>[];
final version = await service.getVersion();
- final reportLines =
- (version.major == 3 && version.minor != null && version.minor! >= 51) ||
- (version.major != null && version.major! > 3);
+ final reportLines = _versionCheck(version, 3, 51);
+ final branchCoverageSupported = _versionCheck(version, 3, 56);
+ if (branchCoverage && !branchCoverageSupported) {
+ branchCoverage = false;
+ stderr.write('Branch coverage was requested, but is not supported'
+ ' by the VM version. Try updating to a newer version of Dart');
+ }
+ final sourceReportKinds = [
+ SourceReportKind.kCoverage,
+ if (branchCoverage) SourceReportKind.kBranchCoverage,
+ ];
// Program counters are shared between isolates in the same group. So we need
// to make sure we're only gathering coverage data for one isolate in each
@@ -130,7 +150,7 @@
// Skip scripts which should not be included in the report.
if (!scopedOutput.contains(scope)) continue;
final scriptReport = await service.getSourceReport(
- isolateRef.id!, <String>[SourceReportKind.kCoverage],
+ isolateRef.id!, sourceReportKinds,
forceCompile: true,
scriptId: script.id,
reportLines: reportLines ? true : null);
@@ -141,7 +161,7 @@
} else {
final isolateReport = await service.getSourceReport(
isolateRef.id!,
- <String>[SourceReportKind.kCoverage],
+ sourceReportKinds,
forceCompile: true,
reportLines: reportLines ? true : null,
);
@@ -231,7 +251,8 @@
final line = _getLineFromTokenPos(script, tokenPos);
if (line == null) {
- print('tokenPos $tokenPos has no line mapping for script ${script.uri!}');
+ stderr.write(
+ 'tokenPos $tokenPos has no line mapping for script ${script.uri!}');
return;
}
hits.funcNames![line] = funcName;
@@ -306,28 +327,41 @@
if (coverage == null) continue;
- for (final pos in coverage.hits!) {
- final line = reportLines ? pos : _getLineFromTokenPos(script!, pos);
- if (line == null) {
- print('tokenPos $pos has no line mapping for script $scriptUri');
- continue;
+ void forEachLine(List<int> tokenPositions, void Function(int line) body) {
+ for (final pos in tokenPositions) {
+ final line = reportLines ? pos : _getLineFromTokenPos(script!, pos);
+ if (line == null) {
+ stderr
+ .write('tokenPos $pos has no line mapping for script $scriptUri');
+ continue;
+ }
+ body(line);
}
+ }
+
+ forEachLine(coverage.hits!, (line) {
_incrementCountForKey(hits.lineHits, line);
if (hits.funcNames != null && hits.funcNames!.containsKey(line)) {
_incrementCountForKey(hits.funcHits!, line);
}
- }
- for (final pos in coverage.misses!) {
- final line = reportLines ? pos : _getLineFromTokenPos(script!, pos);
- if (line == null) {
- print('tokenPos $pos has no line mapping for script $scriptUri');
- continue;
- }
+ });
+ forEachLine(coverage.misses!, (line) {
hits.lineHits.putIfAbsent(line, () => 0);
- }
+ });
hits.funcNames?.forEach((line, funcName) {
hits.funcHits?.putIfAbsent(line, () => 0);
});
+
+ final branchCoverage = range.branchCoverage;
+ if (branchCoverage != null) {
+ hits.branchHits ??= <int, int>{};
+ forEachLine(branchCoverage.hits!, (line) {
+ _incrementCountForKey(hits.branchHits!, line);
+ });
+ forEachLine(branchCoverage.misses!, (line) {
+ hits.branchHits!.putIfAbsent(line, () => 0);
+ });
+ }
}
// Output JSON
diff --git a/lib/src/formatter.dart b/lib/src/formatter.dart
index 85b1e21..763fb52 100644
--- a/lib/src/formatter.dart
+++ b/lib/src/formatter.dart
@@ -84,6 +84,7 @@
final lineHits = v.lineHits;
final funcHits = v.funcHits;
final funcNames = v.funcNames;
+ final branchHits = v.branchHits;
var source = resolver.resolve(entry.key);
if (source == null) {
continue;
@@ -115,6 +116,11 @@
}
buf.write('LF:${lineHits.length}\n');
buf.write('LH:${lineHits.values.where((v) => v > 0).length}\n');
+ if (branchHits != null) {
+ for (final k in branchHits.keys.toList()..sort()) {
+ buf.write('BRDA:$k,0,0,${branchHits[k]}\n');
+ }
+ }
buf.write('end_of_record\n');
}
@@ -131,6 +137,7 @@
Loader loader, {
List<String>? reportOn,
bool reportFuncs = false,
+ bool reportBranches = false,
}) async {
final pathFilter = _getPathFilter(reportOn);
final buf = StringBuffer();
@@ -141,7 +148,16 @@
'missing function coverage information. Did you run '
'collect_coverage with the --function-coverage flag?';
}
- final hits = reportFuncs ? v.funcHits! : v.lineHits;
+ if (reportBranches && v.branchHits == null) {
+ throw 'Branch coverage formatting was requested, but the hit map is '
+ 'missing branch coverage information. Did you run '
+ 'collect_coverage with the --branch-coverage flag?';
+ }
+ final hits = reportFuncs
+ ? v.funcHits!
+ : reportBranches
+ ? v.branchHits!
+ : v.lineHits;
final source = resolver.resolve(entry.key);
if (source == null) {
continue;
diff --git a/lib/src/hitmap.dart b/lib/src/hitmap.dart
index c748b2b..f94754e 100644
--- a/lib/src/hitmap.dart
+++ b/lib/src/hitmap.dart
@@ -11,8 +11,12 @@
/// Contains line and function hit information for a single script.
class HitMap {
/// Constructs a HitMap.
- HitMap([Map<int, int>? lineHits, this.funcHits, this.funcNames])
- : lineHits = lineHits ?? {};
+ HitMap([
+ Map<int, int>? lineHits,
+ this.funcHits,
+ this.funcNames,
+ this.branchHits,
+ ]) : lineHits = lineHits ?? {};
/// Map from line to hit count for that line.
final Map<int, int> lineHits;
@@ -25,6 +29,10 @@
/// function coverage info was not gathered.
Map<int, String>? funcNames;
+ /// Map from branch line, to the hit count for that branch. Null if branch
+ /// coverage info was not gathered.
+ Map<int, int>? branchHits;
+
/// Creates a single hitmap from a raw json object.
///
/// Throws away all entries that are not resolvable.
@@ -139,6 +147,10 @@
funcNames[i + 1] as String;
}
}
+ if (e.containsKey('branchHits')) {
+ sourceHitMap.branchHits ??= <int, int>{};
+ fillHitMap(e['branchHits'] as List, sourceHitMap.branchHits!);
+ }
}
return globalHitMap;
}
@@ -183,6 +195,10 @@
fileResult.funcNames![line] = name;
});
}
+ if (v.branchHits != null) {
+ fileResult.branchHits ??= <int, int>{};
+ _mergeHitCounts(v.branchHits!, fileResult.branchHits!);
+ }
} else {
this[file] = v;
}
@@ -300,6 +316,9 @@
if (hitmap.funcNames != null) {
json['funcNames'] = _flattenMap<dynamic>(hitmap.funcNames!);
}
+ if (hitmap.branchHits != null) {
+ json['branchHits'] = _flattenMap<int>(hitmap.branchHits!);
+ }
return json;
}
diff --git a/pubspec.yaml b/pubspec.yaml
index e7a5bf9..d0ceadb 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: coverage
-version: 1.1.0
+version: 1.2.0-dev
description: Coverage data manipulation and formatting
homepage: https://github.com/dart-lang/coverage
@@ -13,7 +13,7 @@
path: ^1.8.0
source_maps: ^0.10.10
stack_trace: ^1.10.0
- vm_service: ">=8.1.0 <9.0.0"
+ vm_service: ">=8.2.0 <9.0.0"
dev_dependencies:
lints: ^1.0.0
test: ^1.16.0
diff --git a/test/collect_coverage_api_test.dart b/test/collect_coverage_api_test.dart
index 16cce21..3d0dcc7 100644
--- a/test/collect_coverage_api_test.dart
+++ b/test/collect_coverage_api_test.dart
@@ -98,12 +98,35 @@
expect(sampleCoverageData['funcHits'], isNotEmpty);
}
});
+
+ test('collect_coverage_api with branch coverage', () async {
+ final json = await _collectCoverage(branchCoverage: true);
+ expect(json.keys, unorderedEquals(<String>['type', 'coverage']));
+ expect(json, containsPair('type', 'CodeCoverage'));
+
+ final coverage = json['coverage'] as List;
+ expect(coverage, isNotEmpty);
+
+ final sources = coverage.cast<Map>().fold(<String, List<Map>>{},
+ (Map<String, List<Map>> map, value) {
+ final sourceUri = value['source'] as String;
+ map.putIfAbsent(sourceUri, () => <Map>[]).add(value);
+ return map;
+ });
+
+ // Dart VM versions before 2.17 don't support branch coverage.
+ expect(sources[_sampleAppFileUri],
+ everyElement(containsPair('branchHits', isNotEmpty)));
+ expect(sources[_isolateLibFileUri],
+ everyElement(containsPair('branchHits', isNotEmpty)));
+ }, skip: !platformVersionCheck(2, 17));
}
Future<Map<String, dynamic>> _collectCoverage(
{Set<String> scopedOutput = const {},
bool isolateIds = false,
- bool functionCoverage = false}) async {
+ bool functionCoverage = false,
+ bool branchCoverage = false}) async {
final openPort = await getOpenPort();
// run the sample app, with the right flags
@@ -129,5 +152,6 @@
return collect(serviceUri, true, true, false, scopedOutput,
timeout: timeout,
isolateIds: isolateIdSet,
- functionCoverage: functionCoverage);
+ functionCoverage: functionCoverage,
+ branchCoverage: branchCoverage);
}
diff --git a/test/collect_coverage_test.dart b/test/collect_coverage_test.dart
index 0537235..7261eb7 100644
--- a/test/collect_coverage_test.dart
+++ b/test/collect_coverage_test.dart
@@ -113,7 +113,7 @@
});
test('HitMap.parseJson', () async {
- final resultString = await _collectCoverage(true);
+ final resultString = await _collectCoverage(true, true);
final jsonResult = json.decode(resultString) as Map<String, dynamic>;
final coverage = jsonResult['coverage'] as List;
final hitMap = await HitMap.parseJson(
@@ -179,7 +179,78 @@
28: 'fooAsync',
38: 'isolateTask'
});
- });
+ expect(isolateFile?.branchHits,
+ {11: 1, 12: 1, 15: 0, 19: 1, 23: 1, 28: 1, 32: 0, 38: 1, 42: 1});
+ }, skip: !platformVersionCheck(2, 17));
+
+ test('HitMap.parseJson, old VM without branch coverage', () async {
+ final resultString = await _collectCoverage(true, true);
+ final jsonResult = json.decode(resultString) as Map<String, dynamic>;
+ final coverage = jsonResult['coverage'] as List;
+ final hitMap = await HitMap.parseJson(
+ coverage.cast<Map<String, dynamic>>(),
+ );
+ expect(hitMap, contains(_sampleAppFileUri));
+
+ final isolateFile = hitMap[_isolateLibFileUri];
+ final expectedHits = {
+ 11: 1,
+ 12: 1,
+ 13: 1,
+ 15: 0,
+ 19: 1,
+ 23: 1,
+ 24: 2,
+ 28: 1,
+ 29: 1,
+ 30: 1,
+ 32: 0,
+ 38: 1,
+ 39: 1,
+ 41: 1,
+ 42: 3,
+ 43: 1,
+ 44: 3,
+ 45: 1,
+ 48: 1,
+ 49: 1,
+ 51: 1,
+ 54: 1,
+ 55: 1,
+ 56: 1,
+ 59: 1,
+ 60: 1,
+ 62: 1,
+ 63: 1,
+ 64: 1,
+ 66: 1,
+ 67: 1,
+ 68: 1
+ };
+ if (Platform.version.startsWith('1.')) {
+ // Dart VMs prior to 2.0.0-dev.5.0 contain a bug that emits coverage on the
+ // closing brace of async function blocks.
+ // See: https://github.com/dart-lang/coverage/issues/196
+ expectedHits[23] = 0;
+ } else {
+ // Dart VMs version 2.0.0-dev.6.0 mark the opening brace of a function as
+ // coverable.
+ expectedHits[11] = 1;
+ expectedHits[28] = 1;
+ expectedHits[38] = 1;
+ expectedHits[42] = 3;
+ }
+ expect(isolateFile?.lineHits, expectedHits);
+ expect(isolateFile?.funcHits, {11: 1, 19: 1, 21: 0, 23: 1, 28: 1, 38: 1});
+ expect(isolateFile?.funcNames, {
+ 11: 'fooSync',
+ 19: 'BarClass.BarClass',
+ 21: 'BarClass.x=',
+ 23: 'BarClass.baz',
+ 28: 'fooAsync',
+ 38: 'isolateTask'
+ });
+ }, skip: platformVersionCheck(2, 17));
test('parseCoverage', () async {
final tempDir = await Directory.systemTemp.createTemp('coverage.test.');
@@ -289,9 +360,10 @@
String? _coverageData;
Future<String> _getCoverageResult() async =>
- _coverageData ??= await _collectCoverage(false);
+ _coverageData ??= await _collectCoverage(false, false);
-Future<String> _collectCoverage(bool functionCoverage) async {
+Future<String> _collectCoverage(
+ bool functionCoverage, bool branchCoverage) async {
expect(FileSystemEntity.isFileSync(testAppPath), isTrue);
final openPort = await getOpenPort();
@@ -316,9 +388,10 @@
// Run the collection tool.
// TODO: need to get all of this functionality in the lib
- final toolResult = await Process.run('dart', [
+ final toolResult = await Process.run(Platform.resolvedExecutable, [
_collectAppPath,
if (functionCoverage) '--function-coverage',
+ if (branchCoverage) '--branch-coverage',
'--uri',
'$serviceUri',
'--resume-isolates',
diff --git a/test/lcov_test.dart b/test/lcov_test.dart
index 30035d6..b699589 100644
--- a/test/lcov_test.dart
+++ b/test/lcov_test.dart
@@ -11,6 +11,8 @@
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
+import 'test_util.dart';
+
final _sampleAppPath = p.join('test', 'test_files', 'test_app.dart');
final _isolateLibPath = p.join('test', 'test_files', 'test_app_isolate.dart');
@@ -29,6 +31,35 @@
final sampleAppHitLines = sampleAppHitMap?.lineHits;
final sampleAppHitFuncs = sampleAppHitMap?.funcHits;
final sampleAppFuncNames = sampleAppHitMap?.funcNames;
+ final sampleAppBranchHits = sampleAppHitMap?.branchHits;
+
+ expect(sampleAppHitLines, containsPair(46, greaterThanOrEqualTo(1)),
+ reason: 'be careful if you modify the test file');
+ expect(sampleAppHitLines, containsPair(50, 0),
+ reason: 'be careful if you modify the test file');
+ expect(sampleAppHitLines, isNot(contains(32)),
+ reason: 'be careful if you modify the test file');
+ expect(sampleAppHitFuncs, containsPair(45, 1),
+ reason: 'be careful if you modify the test file');
+ expect(sampleAppHitFuncs, containsPair(49, 0),
+ reason: 'be careful if you modify the test file');
+ expect(sampleAppFuncNames, containsPair(45, 'usedMethod'),
+ reason: 'be careful if you modify the test file');
+ expect(sampleAppBranchHits, containsPair(41, 1),
+ reason: 'be careful if you modify the test file');
+ }, skip: !platformVersionCheck(2, 17));
+
+ test('validate hitMap, old VM without branch coverage', () async {
+ final hitmap = await _getHitMap();
+
+ expect(hitmap, contains(_sampleAppFileUri));
+ expect(hitmap, contains(_isolateLibFileUri));
+ expect(hitmap, contains('package:coverage/src/util.dart'));
+
+ final sampleAppHitMap = hitmap[_sampleAppFileUri];
+ final sampleAppHitLines = sampleAppHitMap?.lineHits;
+ final sampleAppHitFuncs = sampleAppHitMap?.funcHits;
+ final sampleAppFuncNames = sampleAppHitMap?.funcNames;
expect(sampleAppHitLines, containsPair(46, greaterThanOrEqualTo(1)),
reason: 'be careful if you modify the test file');
@@ -42,7 +73,7 @@
reason: 'be careful if you modify the test file');
expect(sampleAppFuncNames, containsPair(45, 'usedMethod'),
reason: 'be careful if you modify the test file');
- });
+ }, skip: platformVersionCheck(2, 17));
group('LcovFormatter', () {
test('format()', () async {
@@ -197,6 +228,23 @@
expect(res, contains(' 0|int unusedMethod(int a, int b) {'));
expect(res, contains(' | return a + b;'));
});
+
+ test('prettyPrint() branches', () async {
+ final hitmap = await _getHitMap();
+
+ final resolver = Resolver(packagesPath: '.packages');
+ final res =
+ await hitmap.prettyPrint(resolver, Loader(), reportBranches: true);
+
+ expect(res, contains(p.absolute(_sampleAppPath)));
+ expect(res, contains(p.absolute(_isolateLibPath)));
+ expect(res, contains(p.absolute(p.join('lib', 'src', 'util.dart'))));
+
+ // be very careful if you change the test file
+ expect(res, contains(' 1| if (x == answer) {'));
+ expect(res, contains(' 0| while (i < lines.length) {'));
+ expect(res, contains(' | bar.baz();'));
+ }, skip: !platformVersionCheck(2, 17));
});
}
@@ -210,9 +258,12 @@
final sampleAppArgs = [
'--pause-isolates-on-exit',
'--enable-vm-service=$port',
+ // Dart VM versions before 2.17 don't support branch coverage.
+ if (platformVersionCheck(2, 17)) '--branch-coverage',
_sampleAppPath
];
- final sampleProcess = await Process.start('dart', sampleAppArgs);
+ final sampleProcess =
+ await Process.start(Platform.resolvedExecutable, sampleAppArgs);
// Capture the VM service URI.
final serviceUriCompleter = Completer<Uri>();
@@ -231,14 +282,15 @@
// collect hit map.
final coverageJson = (await collect(serviceUri, true, true, false, <String>{},
- functionCoverage: true))['coverage'] as List<Map<String, dynamic>>;
+ functionCoverage: true,
+ branchCoverage: true))['coverage'] as List<Map<String, dynamic>>;
final hitMap = HitMap.parseJson(coverageJson);
// wait for sample app to terminate.
final exitCode = await sampleProcess.exitCode;
if (exitCode != 0) {
- throw ProcessException(
- 'dart', sampleAppArgs, 'Fatal error. Exit code: $exitCode', exitCode);
+ throw ProcessException(Platform.resolvedExecutable, sampleAppArgs,
+ 'Fatal error. Exit code: $exitCode', exitCode);
}
await sampleProcess.stderr.drain();
return hitMap;
diff --git a/test/run_and_collect_test.dart b/test/run_and_collect_test.dart
index 388921e..8aa7922 100644
--- a/test/run_and_collect_test.dart
+++ b/test/run_and_collect_test.dart
@@ -71,5 +71,6 @@
expect(actualLineHits, expectedLineHits);
expect(actualHitMap?.funcHits, isNull);
expect(actualHitMap?.funcNames, isNull);
+ expect(actualHitMap?.branchHits, isNull);
});
}
diff --git a/test/test_util.dart b/test/test_util.dart
index e6f0f83..6abf0d3 100644
--- a/test/test_util.dart
+++ b/test/test_util.dart
@@ -12,9 +12,21 @@
const Duration timeout = Duration(seconds: 20);
Future<Process> runTestApp(int openPort) async {
- return Process.start('dart', [
+ return Process.start(Platform.resolvedExecutable, [
'--enable-vm-service=$openPort',
'--pause_isolates_on_exit',
+ // Dart VM versions before 2.17 don't support branch coverage.
+ if (platformVersionCheck(2, 17)) '--branch-coverage',
testAppPath
]);
}
+
+final _versionPattern = RegExp('([0-9]+)\\.([0-9]+)\\.([0-9]+)');
+bool platformVersionCheck(int minMajor, int minMinor) {
+ final match = _versionPattern.matchAsPrefix(Platform.version);
+ if (match == null) return false;
+ if (match.groupCount < 3) return false;
+ final major = int.parse(match.group(1)!);
+ final minor = int.parse(match.group(2)!);
+ return major > minMajor || (major == minMajor && minor >= minMinor);
+}