Merge pull request #348 from dart-lang/fncov
Function coverage
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 947de22..cd6880b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,20 @@
+## 1.1.0
+
+* Support function level coverage information, when running tests in the Dart
+ VM. This is not supported for web tests yet.
+* Add flag `--function-coverage` (abbr `-f`) to collect_coverage that collects
+ function coverage information.
+* Add flag `--pretty-print-func` (abbr `-f`) to format_coverage that works
+ similarly to pretty print, but outputs function level coverage, rather than
+ line level.
+* Update `--lcov` (abbr `-l`) in format_coverage to output function level
+ coverage, in addition to line level.
+* BREAKING CHANGE: The signatures of `createHitmap`, `mergeHitmaps`,
+ `parseCoverage`, `toScriptCoverageJson`, and `Formatter.format` have changed
+ from using `Map<int, int>` to represent line coverage to using `HitMap`
+ (which contains both line and function coverage). `collect` also has a new
+ optional bool flag controlling whether function coverage is collected.
+
## 1.0.4 - 2021-06-08
* Ensure `createHitmap` returns a sorted hitmap. This fixes a potential issue with
@@ -9,7 +26,7 @@
## 1.0.2 - 2021-03-15
-* Fix an issue where the `--packages` argument wasn't passed to `format_coverage`.
+* Fix an issue where the `--packages` argument wasn't passed to `format_coverage`.
## 1.0.1 - 2021-02-25
diff --git a/bin/collect_coverage.dart b/bin/collect_coverage.dart
index ccead74..50cf424 100644
--- a/bin/collect_coverage.dart
+++ b/bin/collect_coverage.dart
@@ -21,7 +21,7 @@
await Chain.capture(() async {
final coverage = await collect(options.serviceUri, options.resume,
options.waitPaused, options.includeDart, options.scopedOutput,
- timeout: options.timeout);
+ timeout: options.timeout, functionCoverage: options.functionCoverage);
options.out.write(json.encode(coverage));
await options.out.close();
}, onError: (dynamic error, Chain chain) {
@@ -35,7 +35,7 @@
class Options {
Options(this.serviceUri, this.out, this.timeout, this.waitPaused, this.resume,
- this.includeDart, this.scopedOutput);
+ this.includeDart, this.functionCoverage, this.scopedOutput);
final Uri serviceUri;
final IOSink out;
@@ -43,6 +43,7 @@
final bool waitPaused;
final bool resume;
final bool includeDart;
+ final bool functionCoverage;
final Set<String> scopedOutput;
}
@@ -72,6 +73,8 @@
abbr: 'r', defaultsTo: false, help: 'resume all isolates on exit')
..addFlag('include-dart',
abbr: 'd', defaultsTo: false, help: 'include "dart:" libraries')
+ ..addFlag('function-coverage',
+ abbr: 'f', defaultsTo: false, help: 'Collect function coverage info')
..addFlag('help', abbr: 'h', negatable: false, help: 'show this help');
final args = parser.parse(arguments);
@@ -123,6 +126,7 @@
args['wait-paused'] as bool,
args['resume-isolates'] as bool,
args['include-dart'] as bool,
+ args['function-coverage'] as bool,
scopedOutput.toSet(),
);
}
diff --git a/bin/format_coverage.dart b/bin/format_coverage.dart
index 2f32875..0ca57fb 100644
--- a/bin/format_coverage.dart
+++ b/bin/format_coverage.dart
@@ -20,6 +20,7 @@
required this.output,
required this.packagesPath,
required this.prettyPrint,
+ required this.prettyPrintFunc,
required this.reportOn,
required this.sdkRoot,
required this.verbose,
@@ -35,6 +36,7 @@
IOSink output;
String? packagesPath;
bool prettyPrint;
+ bool prettyPrintFunc;
List<String>? reportOn;
String? sdkRoot;
bool verbose;
@@ -73,10 +75,10 @@
? BazelResolver(workspacePath: env.bazelWorkspace)
: Resolver(packagesPath: env.packagesPath, sdkRoot: env.sdkRoot);
final loader = Loader();
- if (env.prettyPrint) {
- output =
- await PrettyPrintFormatter(resolver, loader, reportOn: env.reportOn)
- .format(hitmap);
+ if (env.prettyPrint || env.prettyPrintFunc) {
+ output = await PrettyPrintFormatter(resolver, loader,
+ reportOn: env.reportOn, reportFuncs: env.prettyPrintFunc)
+ .format(hitmap);
} else {
assert(env.lcov);
output = await LcovFormatter(resolver,
@@ -131,7 +133,11 @@
parser.addFlag('pretty-print',
abbr: 'r',
negatable: false,
- help: 'convert coverage data to pretty print format');
+ help: 'convert line coverage data to pretty print format');
+ parser.addFlag('pretty-print-func',
+ abbr: 'f',
+ negatable: false,
+ help: 'convert function coverage data to pretty print format');
parser.addFlag('lcov',
abbr: 'l',
negatable: false,
@@ -212,12 +218,14 @@
}
final lcov = args['lcov'] as bool;
- if (args['pretty-print'] as bool && lcov == true) {
- fail('Choose one of pretty-print or lcov output');
+ 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) {
+ fail('Choose one of the pretty-print modes or lcov output');
}
// Use pretty-print either explicitly or by default.
- final prettyPrint = !lcov;
+ if (!lcov && !prettyPrintFunc) prettyPrint = true;
int workers;
try {
@@ -238,6 +246,7 @@
output: output,
packagesPath: packagesPath,
prettyPrint: prettyPrint,
+ prettyPrintFunc: prettyPrintFunc,
reportOn: reportOn,
sdkRoot: sdkRoot,
verbose: verbose,
diff --git a/lib/src/chrome.dart b/lib/src/chrome.dart
index 0063d3b..ca429ba 100644
--- a/lib/src/chrome.dart
+++ b/lib/src/chrome.dart
@@ -2,6 +2,7 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
+import 'package:coverage/src/hitmap.dart';
import 'package:coverage/src/util.dart';
import 'package:source_maps/parser.dart';
@@ -70,11 +71,11 @@
}
}
- final coverageHitMaps = <Uri, Map<int, int>>{};
+ final coverageHitMaps = <Uri, HitMap>{};
coverageReport.forEach((uri, coverage) {
- final hitMap = <int, int>{};
+ final hitMap = HitMap();
for (var line in coverage.keys.toList()..sort()) {
- hitMap[line] = coverage[line]! ? 1 : 0;
+ hitMap.lineHits[line] = coverage[line]! ? 1 : 0;
}
coverageHitMaps[uri] = hitMap;
});
diff --git a/lib/src/collect.dart b/lib/src/collect.dart
index 29be493..7c481b0 100644
--- a/lib/src/collect.dart
+++ b/lib/src/collect.dart
@@ -8,6 +8,7 @@
import 'package:vm_service/vm_service.dart';
import 'util.dart';
+import 'hitmap.dart';
const _retryInterval = Duration(milliseconds: 200);
@@ -29,6 +30,9 @@
/// If [includeDart] is true, code coverage for core `dart:*` libraries will be
/// collected.
///
+/// If [functionCoverage] is true, function coverage information will be
+/// collected.
+///
/// If [scopedOutput] is non-empty, coverage will be restricted so that only
/// scripts that start with any of the provided paths are considered.
///
@@ -36,7 +40,9 @@
/// those VM isolates.
Future<Map<String, dynamic>> collect(Uri serviceUri, bool resume,
bool waitPaused, bool includeDart, Set<String>? scopedOutput,
- {Set<String>? isolateIds, Duration? timeout}) async {
+ {Set<String>? isolateIds,
+ Duration? timeout,
+ bool functionCoverage = false}) async {
scopedOutput ??= <String>{};
// Create websocket URI. Handle any trailing slashes.
@@ -71,7 +77,7 @@
}
return await _getAllCoverage(
- service, includeDart, scopedOutput, isolateIds);
+ service, includeDart, functionCoverage, scopedOutput, isolateIds);
} finally {
if (resume) {
await _resumeIsolates(service);
@@ -85,6 +91,7 @@
Future<Map<String, dynamic>> _getAllCoverage(
VmService service,
bool includeDart,
+ bool functionCoverage,
Set<String>? scopedOutput,
Set<String>? isolateIds) async {
scopedOutput ??= <String>{};
@@ -105,7 +112,7 @@
isolateRef.id!, <String>[SourceReportKind.kCoverage],
forceCompile: true, scriptId: script.id);
final coverage = await _getCoverageJson(
- service, isolateRef, scriptReport, includeDart);
+ service, isolateRef, scriptReport, includeDart, functionCoverage);
allCoverage.addAll(coverage);
}
} else {
@@ -115,7 +122,7 @@
forceCompile: true,
);
final coverage = await _getCoverageJson(
- service, isolateRef, isolateReport, includeDart);
+ service, isolateRef, isolateReport, includeDart, functionCoverage);
allCoverage.addAll(coverage);
}
}
@@ -190,12 +197,33 @@
return null;
}
+Future<void> _processFunction(VmService service, IsolateRef isolateRef,
+ Script script, FuncRef funcRef, HitMap hits) async {
+ final func = await service.getObject(isolateRef.id!, funcRef.id!) as Func;
+ final location = func.location;
+ if (location != null) {
+ final funcName = await _getFuncName(service, isolateRef, func);
+ final tokenPos = location.tokenPos!;
+ final line = _getLineFromTokenPos(script, tokenPos);
+
+ if (line == null) {
+ print('tokenPos $tokenPos has no line mapping for script ${script.uri!}');
+ return;
+ }
+ hits.funcNames![line] = funcName;
+ }
+}
+
/// Returns a JSON coverage list backward-compatible with pre-1.16.0 SDKs.
-Future<List<Map<String, dynamic>>> _getCoverageJson(VmService service,
- IsolateRef isolateRef, SourceReport report, bool includeDart) async {
- // script uri -> { line -> hit count }
- final hitMaps = <Uri, Map<int, int>>{};
+Future<List<Map<String, dynamic>>> _getCoverageJson(
+ VmService service,
+ IsolateRef isolateRef,
+ SourceReport report,
+ bool includeDart,
+ bool functionCoverage) async {
+ final hitMaps = <Uri, HitMap>{};
final scripts = <ScriptRef, Script>{};
+ final libraries = <LibraryRef>{};
for (var range in report.ranges!) {
final scriptRef = report.scripts![range.scriptIndex!];
final scriptUri = Uri.parse(report.scripts![range.scriptIndex!].uri!);
@@ -213,8 +241,35 @@
final script = scripts[scriptRef];
if (script == null) continue;
- // Look up the hit map for this script (shared across isolates).
- final hitMap = hitMaps.putIfAbsent(scriptUri, () => <int, int>{});
+ // Look up the hit maps for this script (shared across isolates).
+ final hits = hitMaps.putIfAbsent(scriptUri, () => HitMap());
+
+ // If the script's library isn't loaded, load it then look up all its funcs.
+ final libRef = script.library;
+ if (functionCoverage && libRef != null && !libraries.contains(libRef)) {
+ hits.funcHits ??= <int, int>{};
+ hits.funcNames ??= <int, String>{};
+ libraries.add(libRef);
+ final library =
+ await service.getObject(isolateRef.id!, libRef.id!) as Library;
+ if (library.functions != null) {
+ for (var funcRef in library.functions!) {
+ await _processFunction(service, isolateRef, script, funcRef, hits);
+ }
+ }
+ if (library.classes != null) {
+ for (var classRef in library.classes!) {
+ final clazz =
+ await service.getObject(isolateRef.id!, classRef.id!) as Class;
+ if (clazz.functions != null) {
+ for (var funcRef in clazz.functions!) {
+ await _processFunction(
+ service, isolateRef, script, funcRef, hits);
+ }
+ }
+ }
+ }
+ }
// Collect hits and misses.
final coverage = range.coverage;
@@ -227,7 +282,10 @@
print('tokenPos $tokenPos has no line mapping for script $scriptUri');
continue;
}
- hitMap[line] = hitMap.containsKey(line) ? hitMap[line]! + 1 : 1;
+ _incrementCountForKey(hits.lineHits, line);
+ if (hits.funcNames != null && hits.funcNames!.containsKey(line)) {
+ _incrementCountForKey(hits.funcHits!, line);
+ }
}
for (final tokenPos in coverage.misses!) {
final line = _getLineFromTokenPos(script, tokenPos);
@@ -235,18 +293,38 @@
print('tokenPos $tokenPos has no line mapping for script $scriptUri');
continue;
}
- hitMap.putIfAbsent(line, () => 0);
+ hits.lineHits.putIfAbsent(line, () => 0);
}
+ hits.funcNames?.forEach((line, funcName) {
+ hits.funcHits?.putIfAbsent(line, () => 0);
+ });
}
// Output JSON
final coverage = <Map<String, dynamic>>[];
- hitMaps.forEach((uri, hitMap) {
- coverage.add(toScriptCoverageJson(uri, hitMap));
+ hitMaps.forEach((uri, hits) {
+ coverage.add(toScriptCoverageJson(uri, hits));
});
return coverage;
}
+void _incrementCountForKey(Map<int, int> counter, int key) {
+ counter[key] = counter.containsKey(key) ? counter[key]! + 1 : 1;
+}
+
+Future<String> _getFuncName(
+ VmService service, IsolateRef isolateRef, Func func) async {
+ if (func.name == null) {
+ return '${func.type}:${func.location!.tokenPos}';
+ }
+ final owner = func.owner;
+ if (owner is ClassRef) {
+ final cls = await service.getObject(isolateRef.id!, owner.id!) as Class;
+ if (cls.name != null) return '${cls.name}.${func.name}';
+ }
+ return func.name!;
+}
+
class StdoutLog extends Log {
@override
void warning(String message) => print(message);
diff --git a/lib/src/formatter.dart b/lib/src/formatter.dart
index 679b8f3..50e0f67 100644
--- a/lib/src/formatter.dart
+++ b/lib/src/formatter.dart
@@ -5,10 +5,11 @@
import 'package:path/path.dart' as p;
import 'resolver.dart';
+import 'hitmap.dart';
abstract class Formatter {
/// Returns the formatted coverage data.
- Future<String> format(Map<String, Map<int, int>> hitmap);
+ Future<String> format(Map<String, HitMap> hitmap);
}
/// Converts the given hitmap to lcov format and appends the result to
@@ -29,11 +30,14 @@
final List<String>? reportOn;
@override
- Future<String> format(Map<String, Map<int, int>> hitmap) async {
+ Future<String> format(Map<String, HitMap> hitmap) async {
final pathFilter = _getPathFilter(reportOn);
final buf = StringBuffer();
for (var key in hitmap.keys) {
final v = hitmap[key]!;
+ final lineHits = v.lineHits;
+ final funcHits = v.funcHits;
+ final funcNames = v.funcNames;
var source = resolver.resolve(key);
if (source == null) {
continue;
@@ -48,12 +52,23 @@
}
buf.write('SF:$source\n');
- final lines = v.keys.toList()..sort();
- for (var k in lines) {
- buf.write('DA:$k,${v[k]}\n');
+ if (funcHits != null && funcNames != null) {
+ for (var k in funcNames.keys.toList()..sort()) {
+ buf.write('FN:$k,${funcNames[k]}\n');
+ }
+ for (var k in funcHits.keys.toList()..sort()) {
+ if (funcHits[k]! != 0) {
+ buf.write('FNDA:${funcHits[k]},${funcNames[k]}\n');
+ }
+ }
+ buf.write('FNF:${funcNames.length}\n');
+ buf.write('FNH:${funcHits.values.where((v) => v > 0).length}\n');
}
- buf.write('LF:${lines.length}\n');
- buf.write('LH:${lines.where((k) => v[k]! > 0).length}\n');
+ for (var k in lineHits.keys.toList()..sort()) {
+ buf.write('DA:$k,${lineHits[k]}\n');
+ }
+ buf.write('LF:${lineHits.length}\n');
+ buf.write('LH:${lineHits.values.where((v) => v > 0).length}\n');
buf.write('end_of_record\n');
}
@@ -71,18 +86,26 @@
///
/// If [reportOn] is provided, coverage report output is limited to files
/// prefixed with one of the paths included.
- PrettyPrintFormatter(this.resolver, this.loader, {this.reportOn});
+ PrettyPrintFormatter(this.resolver, this.loader,
+ {this.reportOn, this.reportFuncs = false});
final Resolver resolver;
final Loader loader;
final List<String>? reportOn;
+ final bool reportFuncs;
@override
- Future<String> format(Map<String, dynamic> hitmap) async {
+ Future<String> format(Map<String, HitMap> hitmap) async {
final pathFilter = _getPathFilter(reportOn);
final buf = StringBuffer();
for (var key in hitmap.keys) {
- final v = hitmap[key] as Map<int, int>;
+ final v = hitmap[key]!;
+ if (reportFuncs && v.funcHits == null) {
+ throw 'Function coverage formatting was requested, but the hit map is '
+ 'missing function coverage information. Did you run '
+ 'collect_coverage with the --function-coverage flag?';
+ }
+ final hits = reportFuncs ? v.funcHits! : v.lineHits;
final source = resolver.resolve(key);
if (source == null) {
continue;
@@ -99,8 +122,8 @@
buf.writeln(source);
for (var line = 1; line <= lines.length; line++) {
var prefix = _prefix;
- if (v.containsKey(line)) {
- prefix = v[line].toString().padLeft(_prefix.length);
+ if (hits.containsKey(line)) {
+ prefix = hits[line].toString().padLeft(_prefix.length);
}
buf.writeln('$prefix|${lines[line - 1]}');
}
diff --git a/lib/src/hitmap.dart b/lib/src/hitmap.dart
index 3cf6a32..7041e36 100644
--- a/lib/src/hitmap.dart
+++ b/lib/src/hitmap.dart
@@ -8,6 +8,20 @@
import 'package:coverage/src/resolver.dart';
import 'package:coverage/src/util.dart';
+/// Contains line and function hit information for a single script.
+class HitMap {
+ /// Map from line to hit count for that line.
+ final lineHits = <int, int>{};
+
+ /// Map from the first line of each function, to the hit count for that
+ /// function. Null if function coverage info was not gathered.
+ Map<int, int>? funcHits;
+
+ /// Map from the first line of each function, to the function name. Null if
+ /// function coverage info was not gathered.
+ Map<int, String>? funcNames;
+}
+
/// Class containing information about a coverage hit.
class _HitInfo {
_HitInfo(this.firstLine, this.hitRange, this.hitCount);
@@ -27,7 +41,7 @@
/// are not resolvable.
///
/// `jsonResult` is expected to be a List<Map<String, dynamic>>.
-Future<Map<String, Map<int, int>>> createHitmap(
+Future<Map<String, HitMap>> createHitmap(
List<Map<String, dynamic>> jsonResult, {
bool checkIgnoredLines = false,
String? packagesPath,
@@ -36,12 +50,7 @@
final loader = Loader();
// Map of source file to map of line to hit count for that line.
- final globalHitMap = <String, Map<int, int>>{};
-
- void addToMap(Map<int, int> map, int line, int count) {
- final oldCount = map.putIfAbsent(line, () => 0);
- map[line] = count + oldCount;
- }
+ final globalHitMap = <String, HitMap>{};
for (var e in jsonResult) {
final source = e['source'] as String?;
@@ -94,33 +103,53 @@
return false;
}
- final sourceHitMap = globalHitMap.putIfAbsent(source, () => <int, int>{});
- var hits = e['hits'] as List;
- // Ignore line annotations require hits to be sorted.
- hits = _sortHits(hits);
- // hits is a flat array of the following format:
- // [ <line|linerange>, <hitcount>,...]
- // line: number.
- // linerange: '<line>-<line>'.
- for (var i = 0; i < hits.length; i += 2) {
- final k = hits[i];
- if (k is int) {
- // Single line.
- if (_shouldIgnoreLine(ignoredLines, k)) continue;
+ void addToMap(Map<int, int> map, int line, int count) {
+ final oldCount = map.putIfAbsent(line, () => 0);
+ map[line] = count + oldCount;
+ }
- addToMap(sourceHitMap, k, hits[i + 1] as int);
- } else if (k is String) {
- // Linerange. We expand line ranges to actual lines at this point.
- final splitPos = k.indexOf('-');
- final start = int.parse(k.substring(0, splitPos));
- final end = int.parse(k.substring(splitPos + 1));
- for (var j = start; j <= end; j++) {
- if (_shouldIgnoreLine(ignoredLines, j)) continue;
+ void fillHitMap(List hits, Map<int, int> hitMap) {
+ // Ignore line annotations require hits to be sorted.
+ hits = _sortHits(hits);
+ // hits is a flat array of the following format:
+ // [ <line|linerange>, <hitcount>,...]
+ // line: number.
+ // linerange: '<line>-<line>'.
+ for (var i = 0; i < hits.length; i += 2) {
+ final k = hits[i];
+ if (k is int) {
+ // Single line.
+ if (_shouldIgnoreLine(ignoredLines, k)) continue;
- addToMap(sourceHitMap, j, hits[i + 1] as int);
+ addToMap(hitMap, k, hits[i + 1] as int);
+ } else if (k is String) {
+ // Linerange. We expand line ranges to actual lines at this point.
+ final splitPos = k.indexOf('-');
+ final start = int.parse(k.substring(0, splitPos));
+ final end = int.parse(k.substring(splitPos + 1));
+ for (var j = start; j <= end; j++) {
+ if (_shouldIgnoreLine(ignoredLines, j)) continue;
+
+ addToMap(hitMap, j, hits[i + 1] as int);
+ }
+ } else {
+ throw StateError('Expected value of type int or String');
}
- } else {
- throw StateError('Expected value of type int or String');
+ }
+ }
+
+ final sourceHitMap = globalHitMap.putIfAbsent(source, () => HitMap());
+ fillHitMap(e['hits'] as List, sourceHitMap.lineHits);
+ if (e.containsKey('funcHits')) {
+ sourceHitMap.funcHits ??= <int, int>{};
+ fillHitMap(e['funcHits'] as List, sourceHitMap.funcHits!);
+ }
+ if (e.containsKey('funcNames')) {
+ sourceHitMap.funcNames ??= <int, String>{};
+ final funcNames = e['funcNames'] as List;
+ for (var i = 0; i < funcNames.length; i += 2) {
+ sourceHitMap.funcNames![funcNames[i] as int] =
+ funcNames[i + 1] as String;
}
}
}
@@ -128,19 +157,32 @@
}
/// Merges [newMap] into [result].
-void mergeHitmaps(
- Map<String, Map<int, int>> newMap, Map<String, Map<int, int>> result) {
- newMap.forEach((String file, Map<int, int> v) {
+void mergeHitmaps(Map<String, HitMap> newMap, Map<String, HitMap> result) {
+ newMap.forEach((String file, HitMap v) {
final fileResult = result[file];
if (fileResult != null) {
- v.forEach((int line, int cnt) {
- final lineFileResult = fileResult[line];
- if (lineFileResult == null) {
- fileResult[line] = cnt;
- } else {
- fileResult[line] = lineFileResult + cnt;
- }
- });
+ void mergeHitCounts(Map<int, int> src, Map<int, int> dest) {
+ src.forEach((int line, int cnt) {
+ final lineFileResult = dest[line];
+ if (lineFileResult == null) {
+ dest[line] = cnt;
+ } else {
+ dest[line] = lineFileResult + cnt;
+ }
+ });
+ }
+
+ mergeHitCounts(v.lineHits, fileResult.lineHits);
+ if (v.funcHits != null) {
+ fileResult.funcHits ??= <int, int>{};
+ mergeHitCounts(v.funcHits!, fileResult.funcHits!);
+ }
+ if (v.funcNames != null) {
+ fileResult.funcNames ??= <int, String>{};
+ v.funcNames?.forEach((int line, String name) {
+ fileResult.funcNames![line] = name;
+ });
+ }
} else {
result[file] = v;
}
@@ -148,13 +190,13 @@
}
/// Generates a merged hitmap from a set of coverage JSON files.
-Future<Map<String, Map<int, int>>> parseCoverage(
+Future<Map<String, HitMap>> parseCoverage(
Iterable<File> files,
int _, {
bool checkIgnoredLines = false,
String? packagesPath,
}) async {
- final globalHitmap = <String, Map<int, int>>{};
+ final globalHitmap = <String, HitMap>{};
for (var file in files) {
final contents = file.readAsStringSync();
final jsonMap = json.decode(contents) as Map<String, dynamic>;
@@ -173,6 +215,36 @@
return globalHitmap;
}
+/// Returns a JSON hit map backward-compatible with pre-1.16.0 SDKs.
+Map<String, dynamic> toScriptCoverageJson(Uri scriptUri, HitMap hits) {
+ final json = <String, dynamic>{};
+ List<T> flattenMap<T>(Map map) {
+ final kvs = <T>[];
+ map.forEach((k, v) {
+ kvs.add(k as T);
+ kvs.add(v as T);
+ });
+ return kvs;
+ }
+
+ json['source'] = '$scriptUri';
+ json['script'] = {
+ 'type': '@Script',
+ 'fixedId': true,
+ 'id': 'libraries/1/scripts/${Uri.encodeComponent(scriptUri.toString())}',
+ 'uri': '$scriptUri',
+ '_kind': 'library',
+ };
+ json['hits'] = flattenMap<int>(hits.lineHits);
+ if (hits.funcHits != null) {
+ json['funcHits'] = flattenMap<int>(hits.funcHits!);
+ }
+ if (hits.funcNames != null) {
+ json['funcNames'] = flattenMap<dynamic>(hits.funcNames!);
+ }
+ return json;
+}
+
/// Sorts the hits array based on the line numbers.
List _sortHits(List hits) {
final structuredHits = <_HitInfo>[];
diff --git a/lib/src/util.dart b/lib/src/util.dart
index 45b2b9c..14ba7d5 100644
--- a/lib/src/util.dart
+++ b/lib/src/util.dart
@@ -73,26 +73,6 @@
}
}
-/// Returns a JSON hit map backward-compatible with pre-1.16.0 SDKs.
-Map<String, dynamic> toScriptCoverageJson(Uri scriptUri, Map<int, int> hitMap) {
- final json = <String, dynamic>{};
- final hits = <int>[];
- hitMap.forEach((line, hitCount) {
- hits.add(line);
- hits.add(hitCount);
- });
- json['source'] = '$scriptUri';
- json['script'] = {
- 'type': '@Script',
- 'fixedId': true,
- 'id': 'libraries/1/scripts/${Uri.encodeComponent(scriptUri.toString())}',
- 'uri': '$scriptUri',
- '_kind': 'library',
- };
- json['hits'] = hits;
- return json;
-}
-
/// Generates a hash code for two objects.
int hash2(dynamic a, dynamic b) =>
_finish(_combine(_combine(0, a.hashCode), b.hashCode));
diff --git a/test/collect_coverage_api_test.dart b/test/collect_coverage_api_test.dart
index 6bb6ce1..5f9d1a3 100644
--- a/test/collect_coverage_api_test.dart
+++ b/test/collect_coverage_api_test.dart
@@ -81,7 +81,7 @@
_getScriptCoverage(coverage, 'test_app_isolate.dart')!;
hits = isolateCoverage['hits'] as List<int>;
_expectHitCount(hits, 11, 1);
- _expectHitCount(hits, 18, 1);
+ _expectHitCount(hits, 28, 1);
});
}
@@ -115,7 +115,7 @@
final isolateIdSet = isolateIds ? {isolateId} : null;
return collect(serviceUri, true, true, false, scopedOutput,
- timeout: timeout, isolateIds: isolateIdSet);
+ timeout: timeout, isolateIds: isolateIdSet, functionCoverage: true);
}
// Returns the first coverage hitmap for the script with with the specified
diff --git a/test/collect_coverage_test.dart b/test/collect_coverage_test.dart
index b3a26a0..bc39bef 100644
--- a/test/collect_coverage_test.dart
+++ b/test/collect_coverage_test.dart
@@ -77,7 +77,7 @@
coverage.cast<Map<String, dynamic>>(),
);
final expectedHits = {15: 1, 16: 2, 17: 2, 45: 1, 46: 1, 49: 0, 50: 0};
- expect(hitMap['foo'], expectedHits);
+ expect(hitMap['foo']?.lineHits, expectedHits);
});
test('createHitmap', () async {
@@ -95,29 +95,34 @@
12: 1,
13: 1,
15: 0,
+ 19: 1,
+ 23: 1,
+ 24: 2,
28: 1,
29: 1,
- 31: 1,
- 32: 3,
+ 30: 1,
+ 32: 0,
38: 1,
+ 39: 1,
41: 1,
- 42: 1,
+ 42: 3,
43: 1,
- 46: 1,
- 47: 1,
+ 44: 3,
+ 45: 1,
+ 48: 1,
49: 1,
- 50: 1,
51: 1,
- 53: 1,
54: 1,
55: 1,
- 18: 1,
- 19: 1,
- 20: 1,
- 22: 0,
- 33: 1,
- 34: 3,
- 35: 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
@@ -128,11 +133,20 @@
// Dart VMs version 2.0.0-dev.6.0 mark the opening brace of a function as
// coverable.
expectedHits[11] = 1;
- expectedHits[18] = 1;
expectedHits[28] = 1;
- expectedHits[32] = 3;
+ expectedHits[38] = 1;
+ expectedHits[42] = 3;
}
- expect(isolateFile, expectedHits);
+ 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'
+ });
});
test('parseCoverage', () async {
@@ -206,6 +220,7 @@
// TODO: need to get all of this functionality in the lib
final toolResult = await Process.run('dart', [
_collectAppPath,
+ '--function-coverage',
'--uri',
'$serviceUri',
'--resume-isolates',
diff --git a/test/lcov_test.dart b/test/lcov_test.dart
index 6d28aac..1da9bfd 100644
--- a/test/lcov_test.dart
+++ b/test/lcov_test.dart
@@ -26,12 +26,21 @@
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(sampleAppHitMap, containsPair(46, greaterThanOrEqualTo(1)),
+ expect(sampleAppHitLines, containsPair(46, greaterThanOrEqualTo(1)),
reason: 'be careful if you modify the test file');
- expect(sampleAppHitMap, containsPair(50, 0),
+ expect(sampleAppHitLines, containsPair(50, 0),
reason: 'be careful if you modify the test file');
- expect(sampleAppHitMap, isNot(contains(32)),
+ 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');
});
@@ -142,10 +151,30 @@
expect(res, isNot(contains(p.absolute(_isolateLibPath))));
expect(res, contains(p.absolute(p.join('lib', 'src', 'util.dart'))));
});
+
+ test('format() functions', () async {
+ final hitmap = await _getHitMap();
+
+ final resolver = Resolver(packagesPath: '.packages');
+ final formatter =
+ PrettyPrintFormatter(resolver, Loader(), reportFuncs: true);
+
+ final res = await formatter.format(hitmap);
+
+ 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|Future<Null> main() async {'));
+ expect(res, contains(' 1|int usedMethod(int a, int b) {'));
+ expect(res, contains(' 0|int unusedMethod(int a, int b) {'));
+ expect(res, contains(' | return a + b;'));
+ });
});
}
-Future<Map<String, Map<int, int>>> _getHitMap() async {
+Future<Map<String, HitMap>> _getHitMap() async {
expect(FileSystemEntity.isFileSync(_sampleAppPath), isTrue);
// select service port.
@@ -175,9 +204,8 @@
final serviceUri = await serviceUriCompleter.future;
// collect hit map.
- final coverageJson =
- (await collect(serviceUri, true, true, false, <String>{}))['coverage']
- as List<Map<String, dynamic>>;
+ final coverageJson = (await collect(serviceUri, true, true, false, <String>{},
+ functionCoverage: true))['coverage'] as List<Map<String, dynamic>>;
final hitMap = createHitmap(coverageJson);
// wait for sample app to terminate.
diff --git a/test/run_and_collect_test.dart b/test/run_and_collect_test.dart
index 99b0e92..4095a13 100644
--- a/test/run_and_collect_test.dart
+++ b/test/run_and_collect_test.dart
@@ -41,27 +41,35 @@
final hitMap = await createHitmap(coverage, checkIgnoredLines: true);
expect(hitMap, isNot(contains(_sampleAppFileUri)));
- final actualHits = hitMap[_isolateLibFileUri];
- final expectedHits = {
+ final actualHitMap = hitMap[_isolateLibFileUri];
+ final actualLineHits = actualHitMap?.lineHits;
+ final expectedLineHits = {
11: 1,
12: 1,
13: 1,
15: 0,
+ 19: 1,
+ 23: 1,
+ 24: 2,
28: 1,
29: 1,
- 31: 1,
- 32: 3,
- 46: 1,
- 47: 1,
- 18: 1,
- 19: 1,
- 20: 1,
- 22: 0,
- 33: 1,
- 34: 3,
- 35: 1
+ 30: 1,
+ 32: 0,
+ 38: 1,
+ 39: 1,
+ 41: 1,
+ 42: 3,
+ 43: 1,
+ 44: 3,
+ 45: 1,
+ 48: 1,
+ 49: 1,
+ 59: 1,
+ 60: 1
};
- expect(actualHits, expectedHits);
+ expect(actualLineHits, expectedLineHits);
+ expect(actualHitMap?.funcHits, isNull);
+ expect(actualHitMap?.funcNames, isNull);
});
}
diff --git a/test/test_files/test_app_isolate.dart b/test/test_files/test_app_isolate.dart
index eac9c2d..fc8d723 100644
--- a/test/test_files/test_app_isolate.dart
+++ b/test/test_files/test_app_isolate.dart
@@ -15,6 +15,16 @@
return List.generate(x, (_) => 'xyzzy').join(' ');
}
+class BarClass {
+ BarClass(this.x);
+
+ int x;
+
+ void baz() {
+ print(x);
+ }
+}
+
Future<String> fooAsync(int x) async {
if (x == answer) {
return '*' * x;
@@ -35,6 +45,9 @@
port.send(sum);
});
+ final bar = BarClass(123);
+ bar.baz();
+
print('678'); // coverage:ignore-line
// coverage:ignore-start