| // 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<String> format(Map<String, Map<int, int>> 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<String>? reportOn; | |
| @override | |
| Future<String> format(Map<String, Map<int, int>> 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<String>? reportOn; | |
| final bool reportFuncs; | |
| @override | |
| Future<String> format(Map<String, Map<int, int>> hitmap) { | |
| return hitmap | |
| .map((key, value) => MapEntry(key, HitMap(value))) | |
| .prettyPrint( | |
| resolver, | |
| loader, | |
| reportOn: reportOn, | |
| reportFuncs: reportFuncs, | |
| ); | |
| } | |
| } | |
| extension FileHitMapsFormatter on Map<String, HitMap> { | |
| /// 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<String>? reportOn, | |
| Set<Glob>? 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<String> prettyPrint( | |
| Resolver resolver, | |
| Loader loader, { | |
| List<String>? reportOn, | |
| Set<Glob>? 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<String>? reportOn, Set<Glob>? 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; | |
| }; | |
| } |