
We don't actually care if we're on linux, we care only if the lcov tool is available. We check for that explicitly just below. Additionally there is code below which indicates that lcov is available for macOS. Before this change that code would never execute.
157 lines
5.9 KiB
Dart
157 lines
5.9 KiB
Dart
// Copyright 2016 The Chromium 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 'dart:async';
|
|
|
|
import 'package:coverage/coverage.dart' as coverage;
|
|
|
|
import '../base/file_system.dart';
|
|
import '../base/io.dart';
|
|
import '../base/logger.dart';
|
|
import '../base/os.dart';
|
|
import '../base/platform.dart';
|
|
import '../base/process_manager.dart';
|
|
import '../dart/package_map.dart';
|
|
import '../globals.dart';
|
|
|
|
import 'watcher.dart';
|
|
|
|
/// A class that's used to collect coverage data during tests.
|
|
class CoverageCollector extends TestWatcher {
|
|
Map<String, dynamic> _globalHitmap;
|
|
|
|
@override
|
|
Future<void> onFinishedTest(ProcessEvent event) async {
|
|
printTrace('test ${event.childIndex}: collecting coverage');
|
|
await collectCoverage(event.process, event.observatoryUri);
|
|
}
|
|
|
|
void _addHitmap(Map<String, dynamic> hitmap) {
|
|
if (_globalHitmap == null)
|
|
_globalHitmap = hitmap;
|
|
else
|
|
coverage.mergeHitmaps(hitmap, _globalHitmap);
|
|
}
|
|
|
|
/// 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<Null> collectCoverage(Process process, Uri observatoryUri) async {
|
|
assert(process != null);
|
|
assert(observatoryUri != null);
|
|
|
|
final int pid = process.pid;
|
|
int exitCode;
|
|
// Synchronization is enforced by the API contract. Error handling
|
|
// synchronization is done in the code below where `exitCode` is checked.
|
|
// Callback cannot throw.
|
|
process.exitCode.then<Null>((int code) { // ignore: unawaited_futures
|
|
exitCode = code;
|
|
});
|
|
if (exitCode != null)
|
|
throw new Exception('Failed to collect coverage, process terminated before coverage could be collected.');
|
|
|
|
printTrace('pid $pid: collecting coverage data from $observatoryUri...');
|
|
final Map<String, dynamic> data = await coverage
|
|
.collect(observatoryUri, false, false)
|
|
.timeout(
|
|
const Duration(minutes: 2),
|
|
onTimeout: () {
|
|
throw new Exception('Timed out while collecting coverage.');
|
|
},
|
|
);
|
|
printTrace(() {
|
|
final StringBuffer buf = new StringBuffer()
|
|
..write('pid $pid ($observatoryUri): ')
|
|
..write(exitCode == null
|
|
? 'collected coverage data; merging...'
|
|
: 'process terminated prematurely with exit code $exitCode; aborting');
|
|
return buf.toString();
|
|
}());
|
|
if (exitCode != null)
|
|
throw new Exception('Failed to collect coverage, process terminated while coverage was being collected.');
|
|
_addHitmap(coverage.createHitmap(data['coverage']));
|
|
printTrace('pid $pid ($observatoryUri): done merging coverage data into global coverage map.');
|
|
}
|
|
|
|
/// Returns a future that will complete with the formatted coverage data
|
|
/// (using [formatter]) 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.
|
|
///
|
|
/// If [timeout] is specified, the future will timeout (with a
|
|
/// [TimeoutException]) after the specified duration.
|
|
Future<String> finalizeCoverage({
|
|
coverage.Formatter formatter,
|
|
Duration timeout,
|
|
}) async {
|
|
printTrace('formating coverage data');
|
|
if (_globalHitmap == null)
|
|
return null;
|
|
if (formatter == null) {
|
|
final coverage.Resolver resolver = new coverage.Resolver(packagesPath: PackageMap.globalPackagesPath);
|
|
final String packagePath = fs.currentDirectory.path;
|
|
final List<String> reportOn = <String>[fs.path.join(packagePath, 'lib')];
|
|
formatter = new coverage.LcovFormatter(resolver, reportOn: reportOn, basePath: packagePath);
|
|
}
|
|
final String result = await formatter.format(_globalHitmap);
|
|
_globalHitmap = null;
|
|
return result;
|
|
}
|
|
|
|
Future<bool> collectCoverageData(String coveragePath, { bool mergeCoverageData = false }) async {
|
|
final Status status = logger.startProgress('Collecting coverage information...');
|
|
final String coverageData = await finalizeCoverage(
|
|
timeout: const Duration(seconds: 30),
|
|
);
|
|
status.stop();
|
|
printTrace('coverage information collection complete');
|
|
if (coverageData == null)
|
|
return false;
|
|
|
|
final File coverageFile = fs.file(coveragePath)
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync(coverageData, flush: true);
|
|
printTrace('wrote coverage data to $coveragePath (size=${coverageData.length})');
|
|
|
|
const String baseCoverageData = 'coverage/lcov.base.info';
|
|
if (mergeCoverageData) {
|
|
if (!fs.isFileSync(baseCoverageData)) {
|
|
printError('Missing "$baseCoverageData". Unable to merge coverage data.');
|
|
return false;
|
|
}
|
|
|
|
if (os.which('lcov') == null) {
|
|
String installMessage = 'Please install lcov.';
|
|
if (platform.isLinux)
|
|
installMessage = 'Consider running "sudo apt-get install lcov".';
|
|
else if (platform.isMacOS)
|
|
installMessage = 'Consider running "brew install lcov".';
|
|
printError('Missing "lcov" tool. Unable to merge coverage data.\n$installMessage');
|
|
return false;
|
|
}
|
|
|
|
final Directory tempDir = fs.systemTempDirectory.createTempSync('flutter_tools');
|
|
try {
|
|
final File sourceFile = coverageFile.copySync(fs.path.join(tempDir.path, 'lcov.source.info'));
|
|
final ProcessResult result = processManager.runSync(<String>[
|
|
'lcov',
|
|
'--add-tracefile', baseCoverageData,
|
|
'--add-tracefile', sourceFile.path,
|
|
'--output-file', coverageFile.path,
|
|
]);
|
|
if (result.exitCode != 0)
|
|
return false;
|
|
} finally {
|
|
tempDir.deleteSync(recursive: true);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|