blob: 50e0f67aa97994beee321c5ab63c1f505cd765b4 [file] [log] [blame]
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// 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:path/path.dart' as p;
import 'resolver.dart';
import 'hitmap.dart';
abstract class Formatter {
/// Returns the formatted coverage data.
Future<String> format(Map<String, HitMap> hitmap);
}
/// Converts the given hitmap to lcov format and appends the result to
/// env.output.
///
/// Returns a [Future] that completes as soon as all map entries have been
/// emitted.
class LcovFormatter implements Formatter {
/// Creates a LCOV formatter.
///
/// If [reportOn] is provided, coverage report output is limited to files
/// prefixed with one of the paths included. If [basePath] is provided, paths
/// are reported relative to that path.
LcovFormatter(this.resolver, {this.reportOn, this.basePath});
final Resolver resolver;
final String? basePath;
final List<String>? reportOn;
@override
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;
}
if (!pathFilter(source)) {
continue;
}
if (basePath != null) {
source = p.relative(source, from: basePath);
}
buf.write('SF:$source\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');
}
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');
}
return buf.toString();
}
}
/// Converts the given hitmap to a pretty-print format and appends the result
/// to env.output.
///
/// Returns a [Future] that completes as soon as all map entries have been
/// emitted.
class PrettyPrintFormatter implements Formatter {
/// Creates a pretty-print formatter.
///
/// 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, this.reportFuncs = false});
final Resolver resolver;
final Loader loader;
final List<String>? reportOn;
final bool reportFuncs;
@override
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]!;
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;
}
if (!pathFilter(source)) {
continue;
}
final lines = await loader.load(source);
if (lines == null) {
continue;
}
buf.writeln(source);
for (var line = 1; line <= lines.length; line++) {
var prefix = _prefix;
if (hits.containsKey(line)) {
prefix = hits[line].toString().padLeft(_prefix.length);
}
buf.writeln('$prefix|${lines[line - 1]}');
}
}
return buf.toString();
}
}
const _prefix = ' ';
typedef _PathFilter = bool Function(String path);
_PathFilter _getPathFilter(List<String>? reportOn) {
if (reportOn == null) return (String path) => true;
final absolutePaths = reportOn.map(p.absolute).toList();
return (String path) => absolutePaths.any((item) => path.startsWith(item));
}