blob: 3036c93c36a3d1a39cf7361234774fbe84299ed9 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. 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/coverage.dart' as coverage;
import 'package:meta/meta.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/process.dart';
import '../globals.dart' as globals;
import '../vmservice.dart';
import 'test_device.dart';
import 'test_time_recorder.dart';
import 'watcher.dart';
/// A class that collects code coverage data during test runs.
class CoverageCollector extends TestWatcher {
this.libraryNames, this.verbose = true, required this.packagesPath,
this.resolver, this.testTimeRecorder, this.branchCoverage = false});
/// True when log messages should be emitted.
final bool verbose;
/// The path to the package_config.json of the package for which code
/// coverage is computed.
final String packagesPath;
/// Map of file path to coverage hit map for that file.
Map<String, coverage.HitMap>? _globalHitmap;
/// The names of the libraries to gather coverage for. If null, all libraries
/// will be accepted.
Set<String>? libraryNames;
final coverage.Resolver? resolver;
final Map<String, List<List<int>>?> _ignoredLinesInFilesCache = <String, List<List<int>>?>{};
final TestTimeRecorder? testTimeRecorder;
/// Whether to collect branch coverage information.
bool branchCoverage;
static Future<coverage.Resolver> getResolver(String? packagesPath) async {
try {
return await coverage.Resolver.create(packagesPath: packagesPath);
} on FileSystemException {
// When given a bad packages path (as for instance done in some tests)
// just ignore it and return one without a packages path.
return coverage.Resolver.create();
Future<void> handleFinishedTest(TestDevice testDevice) async {
_logMessage('Starting coverage collection');
await collectCoverage(testDevice);
void _logMessage(String line, { bool error = false }) {
if (!verbose) {
if (error) {
} else {
void _addHitmap(Map<String, coverage.HitMap> hitmap) {
final Stopwatch? stopwatch = testTimeRecorder?.start(TestTimePhases.CoverageAddHitmap);
if (_globalHitmap == null) {
_globalHitmap = hitmap;
} else {
testTimeRecorder?.stop(TestTimePhases.CoverageAddHitmap, stopwatch!);
/// The directory of the package for which coverage is being collected.
String get packageDirectory {
// The coverage package expects the directory of the package itself, and
// uses that to locate the package_info.json file, which it treats as a
// private implementation detail. In general, the package_info.json file is
// located in `.dart_tool/package_info.json` relative to the package
// directory, so we return the grandparent directory of that file.
// This may not be a safe assumption in non-standard environments, such as
// when building under build systems such as Bazel. In those cases, this
// getter should be overridden.
/// Collects coverage for an isolate using the given `port`.
/// This should be called when the code whose coverage data is being collected
/// has been run to completion so that all coverage data has been recorded.
/// The returned [Future] completes when the coverage is collected.
Future<void> collectCoverageIsolate(Uri observatoryUri) async {
_logMessage('collecting coverage data from $observatoryUri...');
final Map<String, dynamic> data = await collect(
observatoryUri, libraryNames, branchCoverage: branchCoverage);
if (data == null) {
throw Exception('Failed to collect coverage.');
_logMessage('($observatoryUri): collected coverage data; merging...');
_addHitmap(await coverage.HitMap.parseJson(
data['coverage'] as List<Map<String, dynamic>>,
packagePath: packageDirectory,
checkIgnoredLines: true,
_logMessage('($observatoryUri): done merging coverage data into global coverage map.');
/// Collects coverage for the given [Process] using the given `port`.
/// This should be called when the code whose coverage data is being collected
/// has been run to completion so that all coverage data has been recorded.
/// The returned [Future] completes when the coverage is collected.
Future<void> collectCoverage(TestDevice testDevice, {
@visibleForTesting FlutterVmService? serviceOverride,
}) async {
final Stopwatch? totalTestTimeRecorderStopwatch = testTimeRecorder?.start(TestTimePhases.CoverageTotal);
late Map<String, dynamic> data;
final Stopwatch? collectTestTimeRecorderStopwatch = testTimeRecorder?.start(TestTimePhases.CoverageCollect);
final Future<void> processComplete = testDevice.finished.catchError(
(Object error) => throw Exception(
'Failed to collect coverage, test device terminated prematurely with '
'error: ${(error as TestDeviceException).message}.'),
test: (Object error) => error is TestDeviceException,
final Future<void> collectionComplete = testDevice.observatoryUri
.then((Uri? observatoryUri) {
_logMessage('collecting coverage data from $testDevice at $observatoryUri...');
return collect(
observatoryUri!, libraryNames, serviceOverride: serviceOverride,
branchCoverage: branchCoverage)
.then<void>((Map<String, dynamic> result) {
if (result == null) {
throw Exception('Failed to collect coverage.');
_logMessage('Collected coverage data.');
data = result;
await Future.any<void>(<Future<void>>[ processComplete, collectionComplete ]);
testTimeRecorder?.stop(TestTimePhases.CoverageCollect, collectTestTimeRecorderStopwatch!);
_logMessage('Merging coverage data...');
final Stopwatch? parseTestTimeRecorderStopwatch = testTimeRecorder?.start(TestTimePhases.CoverageParseJson);
final Map<String, coverage.HitMap> hitmap = coverage.HitMap.parseJsonSync(
data['coverage'] as List<Map<String, dynamic>>,
checkIgnoredLines: true,
resolver: resolver ?? await CoverageCollector.getResolver(packageDirectory),
ignoredLinesInFilesCache: _ignoredLinesInFilesCache);
testTimeRecorder?.stop(TestTimePhases.CoverageParseJson, parseTestTimeRecorderStopwatch!);
_logMessage('Done merging coverage data into global coverage map.');
testTimeRecorder?.stop(TestTimePhases.CoverageTotal, totalTestTimeRecorderStopwatch!);
/// Returns formatted coverage data once all coverage data has been collected.
/// This will not start any collection tasks. It us up to the caller of to
/// call [collectCoverage] for each process first.
Future<String?> finalizeCoverage({
String Function(Map<String, coverage.HitMap> hitmap)? formatter,
coverage.Resolver? resolver,
Directory? coverageDirectory,
}) async {
if (_globalHitmap == null) {
return null;
if (formatter == null) {
final coverage.Resolver usedResolver = resolver ?? this.resolver ?? await CoverageCollector.getResolver(packagesPath);
final String packagePath = globals.fs.currentDirectory.path;
final List<String> reportOn = coverageDirectory == null
? <String>[globals.fs.path.join(packagePath, 'lib')]
: <String>[coverageDirectory.path];
formatter = (Map<String, coverage.HitMap> hitmap) => hitmap
.formatLcov(usedResolver, reportOn: reportOn, basePath: packagePath);
final String result = formatter(_globalHitmap!);
_globalHitmap = null;
return result;
Future<bool> collectCoverageData(String? coveragePath, { bool mergeCoverageData = false, Directory? coverageDirectory }) async {
final String? coverageData = await finalizeCoverage(
coverageDirectory: coverageDirectory,
_logMessage('coverage information collection complete');
if (coverageData == null) {
return false;
final File coverageFile = globals.fs.file(coveragePath)
..createSync(recursive: true)
..writeAsStringSync(coverageData, flush: true);
_logMessage('wrote coverage data to $coveragePath (size=${coverageData.length})');
const String baseCoverageData = 'coverage/';
if (mergeCoverageData) {
if (!globals.fs.isFileSync(baseCoverageData)) {
_logMessage('Missing "$baseCoverageData". Unable to merge coverage data.', error: true);
return false;
if (globals.os.which('lcov') == null) {
String installMessage = 'Please install lcov.';
if (globals.platform.isLinux) {
installMessage = 'Consider running "sudo apt-get install lcov".';
} else if (globals.platform.isMacOS) {
installMessage = 'Consider running "brew install lcov".';
_logMessage('Missing "lcov" tool. Unable to merge coverage data.\n$installMessage', error: true);
return false;
final Directory tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_test_coverage.');
try {
final File sourceFile = coverageFile.copySync(globals.fs.path.join(tempDir.path, ''));
final RunResult result = globals.processUtils.runSync(<String>[
'--add-tracefile', baseCoverageData,
'--add-tracefile', sourceFile.path,
'--output-file', coverageFile.path,
if (result.exitCode != 0) {
return false;
} finally {
tempDir.deleteSync(recursive: true);
return true;
Future<void> handleTestCrashed(TestDevice testDevice) async { }
Future<void> handleTestTimedOut(TestDevice testDevice) async { }
Future<Map<String, dynamic>> collect(Uri serviceUri, Set<String>? libraryNames, {
bool waitPaused = false,
String? debugName,
@visibleForTesting bool forceSequential = false,
@visibleForTesting FlutterVmService? serviceOverride,
bool branchCoverage = false,
}) {
return coverage.collect(
serviceUri, false, false, false, libraryNames,
serviceOverrideForTesting: serviceOverride?.service,
branchCoverage: branchCoverage);