Add support for --coverage to flutter test (#4679)
We need https://github.com/dart-lang/coverage/issues/100 to be fixed before
this will be useful.
Fixes #2342
diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart
index ca3c81e..d94b7a4 100644
--- a/packages/flutter_tools/lib/src/commands/test.dart
+++ b/packages/flutter_tools/lib/src/commands/test.dart
@@ -13,12 +13,21 @@
import '../dart/package_map.dart';
import '../globals.dart';
import '../runner/flutter_command.dart';
+import '../test/coverage_collector.dart';
import '../test/flutter_platform.dart' as loader;
import '../toolchain.dart';
class TestCommand extends FlutterCommand {
TestCommand() {
usesPubOption();
+ argParser.addFlag('coverage',
+ defaultsTo: false,
+ help: 'Whether to collect coverage information.'
+ );
+ argParser.addOption('coverage-path',
+ defaultsTo: 'coverage/lcov.info',
+ help: 'Where to store coverage information (if coverage is enabled).'
+ );
}
@override
@@ -67,6 +76,7 @@
printTrace('running test package with arguments: $testArgs');
await executable.main(testArgs);
printTrace('test package returned with exit code $exitCode');
+
return exitCode;
} finally {
Directory.current = currentDirectory;
@@ -96,6 +106,9 @@
if (!terminal.supportsColor)
testArgs.insert(0, '--no-color');
+ if (argResults['coverage'])
+ testArgs.insert(0, '--concurrency=1');
+
loader.installHook();
loader.shellPath = tools.getHostToolPath(HostTool.SkyShell);
if (!FileSystemEntity.isFileSync(loader.shellPath)) {
@@ -105,6 +118,23 @@
Cache.releaseLockEarly();
- return await _runTests(testArgs, testDir);
+ CoverageCollector collector = CoverageCollector.instance;
+ collector.enabled = argResults['coverage'];
+
+ int result = await _runTests(testArgs, testDir);
+
+ if (collector.enabled) {
+ Status status = logger.startProgress("Collecting coverage information...");
+ String coverageData = await collector.finalizeCoverage();
+ status.stop(showElapsedTime: true);
+
+ String coveragePath = argResults['coverage-path'];
+ new File(coveragePath)
+ ..createSync(recursive: true)
+ ..writeAsStringSync(coverageData, flush: true);
+ printTrace('wrote coverage data to $coveragePath (size=${coverageData.length})');
+ }
+
+ return result;
}
}
diff --git a/packages/flutter_tools/lib/src/test/coverage_collector.dart b/packages/flutter_tools/lib/src/test/coverage_collector.dart
new file mode 100644
index 0000000..64e67a6
--- /dev/null
+++ b/packages/flutter_tools/lib/src/test/coverage_collector.dart
@@ -0,0 +1,71 @@
+// 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 'dart:io';
+
+import 'package:coverage/coverage.dart';
+import 'package:path/path.dart' as path;
+
+import '../globals.dart';
+
+class CoverageCollector {
+ static final CoverageCollector instance = new CoverageCollector();
+
+ bool enabled = false;
+
+ void collectCoverage({
+ String host,
+ int port,
+ Process processToKill
+ }) {
+ if (enabled) {
+ assert(_jobs != null);
+ _jobs.add(_startJob(
+ host: host,
+ port: port,
+ processToKill: processToKill
+ ));
+ } else {
+ processToKill.kill();
+ }
+ }
+
+ Future<Null> _startJob({
+ String host,
+ int port,
+ Process processToKill
+ }) async {
+ int pid = processToKill.pid;
+ printTrace('collecting coverage data from pid $pid on port $port');
+ Map<String, dynamic> data = await collect(host, port, false, false);
+ printTrace('done collecting coverage data from pid $pid');
+ processToKill.kill();
+ Map<String, dynamic> hitmap = createHitmap(data['coverage']);
+ if (_globalHitmap == null)
+ _globalHitmap = hitmap;
+ else
+ mergeHitmaps(hitmap, _globalHitmap);
+ printTrace('done merging data from pid $pid into global coverage map');
+ }
+
+ Future<Null> finishPendingJobs() async {
+ await Future.wait(_jobs.toList(), eagerError: true);
+ }
+
+ List<Future<Null>> _jobs = <Future<Null>>[];
+ Map<String, dynamic> _globalHitmap;
+
+ Future<String> finalizeCoverage() async {
+ assert(enabled);
+ await finishPendingJobs();
+ printTrace('formating coverage data');
+ // TODO(abarth): Use PackageMap.globalPackagesPath once
+ // https://github.com/dart-lang/coverage/issues/100 is fixed.
+ Resolver resolver = new Resolver(packageRoot: path.absolute('packages'));
+ Formatter formater = new LcovFormatter(resolver);
+ List<String> reportOn = <String>[path.join(Directory.current.path, 'lib')];
+ return await formater.format(_globalHitmap, reportOn: reportOn);
+ }
+}
diff --git a/packages/flutter_tools/lib/src/test/flutter_platform.dart b/packages/flutter_tools/lib/src/test/flutter_platform.dart
index 28b1671..58a6a82 100644
--- a/packages/flutter_tools/lib/src/test/flutter_platform.dart
+++ b/packages/flutter_tools/lib/src/test/flutter_platform.dart
@@ -5,6 +5,7 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
+import 'dart:math' as math;
import 'package:async/async.dart';
import 'package:path/path.dart' as path;
@@ -16,6 +17,7 @@
import '../dart/package_map.dart';
import '../globals.dart';
+import 'coverage_collector.dart';
final String _kSkyShell = Platform.environment['SKY_SHELL'];
const String _kHost = '127.0.0.1';
@@ -45,15 +47,20 @@
return new _ServerInfo(server, 'ws://$_kHost:${server.port}$_kPath', socket.future);
}
-Future<Process> _startProcess(String mainPath, { String packages }) {
+Future<Process> _startProcess(String mainPath, { String packages, int observatoryPort }) {
assert(shellPath != null || _kSkyShell != null); // Please provide the path to the shell in the SKY_SHELL environment variable.
String executable = shellPath ?? _kSkyShell;
- List<String> arguments = <String>[
+ List<String> arguments = <String>[];
+ if (observatoryPort != null) {
+ arguments.add('--observatory-port=$observatoryPort');
+ } else {
+ arguments.add('--non-interactive');
+ }
+ arguments.addAll(<String>[
'--enable-checked-mode',
- '--non-interactive',
'--packages=$packages',
mainPath
- ];
+ ]);
printTrace('$executable ${arguments.join(' ')}');
return Process.start(executable, arguments, environment: <String, String>{ 'FLUTTER_TEST': 'true' });
}
@@ -105,8 +112,16 @@
}
''');
+ int observatoryPort;
+ if (CoverageCollector.instance.enabled) {
+ observatoryPort = new math.Random().nextInt(30000) + 2000;
+ await CoverageCollector.instance.finishPendingJobs();
+ }
+
Process process = await _startProcess(
- listenerFile.path, packages: PackageMap.globalPackagesPath
+ listenerFile.path,
+ packages: PackageMap.globalPackagesPath,
+ observatoryPort: observatoryPort
);
_attachStandardStreams(process);
@@ -115,7 +130,11 @@
if (process != null) {
Process processToKill = process;
process = null;
- processToKill.kill();
+ CoverageCollector.instance.collectCoverage(
+ host: _kHost,
+ port: observatoryPort,
+ processToKill: processToKill
+ );
}
if (tempDir != null) {
Directory dirToDelete = tempDir;
diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml
index fd2fd2a..cbc6747 100644
--- a/packages/flutter_tools/pubspec.yaml
+++ b/packages/flutter_tools/pubspec.yaml
@@ -10,6 +10,7 @@
dependencies:
archive: ^1.0.20
args: ^0.13.4
+ coverage: ^0.7.7
crypto: '>=1.1.1 <3.0.0'
file: ^0.1.0
http: ^0.11.3