// 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:glob/glob.dart'; import 'package:path/path.dart' as p; import 'hitmap.dart'; import 'resolver.dart'; @Deprecated('Migrate to FileHitMapsFormatter') abstract class Formatter { /// Returns the formatted coverage data. Future format(Map> 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. @Deprecated('Migrate to FileHitMapsFormatter.formatLcov') 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? reportOn; @override Future format(Map> hitmap) { return Future.value(hitmap .map((key, value) => MapEntry(key, HitMap(value))) .formatLcov(resolver, basePath: basePath, reportOn: reportOn)); } } /// 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. @Deprecated('Migrate to FileHitMapsFormatter.prettyPrint') 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? reportOn; final bool reportFuncs; @override Future format(Map> hitmap) { return hitmap.map((key, value) => MapEntry(key, HitMap(value))).prettyPrint( resolver, loader, reportOn: reportOn, reportFuncs: reportFuncs); } } extension FileHitMapsFormatter on Map { /// Converts the given hitmap to lcov format. /// /// 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. String formatLcov( Resolver resolver, { String? basePath, List? reportOn, Set? ignoreGlobs, }) { final pathFilter = _getPathFilter( reportOn: reportOn, ignoreGlobs: ignoreGlobs, ); final buf = StringBuffer(); for (final entry in entries) { final v = entry.value; 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; } if (!pathFilter(source)) { continue; } if (basePath != null) { source = p.relative(source, from: basePath); } buf.write('SF:$source\n'); if (funcHits != null && funcNames != null) { for (final k in funcNames.keys.toList()..sort()) { buf.write('FN:$k,${funcNames[k]}\n'); } for (final 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 (final 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'); 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'); } return buf.toString(); } /// Converts the given hitmap to a pretty-print format. /// /// If [reportOn] is provided, coverage report output is limited to files /// prefixed with one of the paths included. If [reportFuncs] is provided, /// only function coverage information will be shown. Future prettyPrint( Resolver resolver, Loader loader, { List? reportOn, Set? ignoreGlobs, bool reportFuncs = false, bool reportBranches = false, }) async { final pathFilter = _getPathFilter( reportOn: reportOn, ignoreGlobs: ignoreGlobs, ); final buf = StringBuffer(); for (final entry in entries) { final source = resolver.resolve(entry.key); if (source == null) { continue; } if (!pathFilter(source)) { continue; } final lines = await loader.load(source); if (lines == null) { continue; } final v = entry.value; if (reportFuncs && v.funcHits == null) { throw StateError( 'Function coverage formatting was requested, but the hit map is ' 'missing function coverage information. Did you run ' 'collect_coverage with the --function-coverage flag?', ); } if (reportBranches && v.branchHits == null) { throw StateError( '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; 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? reportOn, Set? ignoreGlobs}) { if (reportOn == null && ignoreGlobs == null) return (String path) => true; final absolutePaths = reportOn?.map(p.canonicalize).toList(); return (String path) { final canonicalizedPath = p.canonicalize(path); if (absolutePaths != null && !absolutePaths.any(canonicalizedPath.startsWith)) { return false; } if (ignoreGlobs != null && ignoreGlobs.any((glob) => glob.matches(canonicalizedPath))) { return false; } return true; }; }