Commit b094fa6d authored by Adam Barth's avatar Adam Barth Committed by GitHub

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
parent 6c0b88aa
...@@ -13,12 +13,21 @@ import '../cache.dart'; ...@@ -13,12 +13,21 @@ import '../cache.dart';
import '../dart/package_map.dart'; import '../dart/package_map.dart';
import '../globals.dart'; import '../globals.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import '../test/coverage_collector.dart';
import '../test/flutter_platform.dart' as loader; import '../test/flutter_platform.dart' as loader;
import '../toolchain.dart'; import '../toolchain.dart';
class TestCommand extends FlutterCommand { class TestCommand extends FlutterCommand {
TestCommand() { TestCommand() {
usesPubOption(); 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 @override
...@@ -67,6 +76,7 @@ class TestCommand extends FlutterCommand { ...@@ -67,6 +76,7 @@ class TestCommand extends FlutterCommand {
printTrace('running test package with arguments: $testArgs'); printTrace('running test package with arguments: $testArgs');
await executable.main(testArgs); await executable.main(testArgs);
printTrace('test package returned with exit code $exitCode'); printTrace('test package returned with exit code $exitCode');
return exitCode; return exitCode;
} finally { } finally {
Directory.current = currentDirectory; Directory.current = currentDirectory;
...@@ -96,6 +106,9 @@ class TestCommand extends FlutterCommand { ...@@ -96,6 +106,9 @@ class TestCommand extends FlutterCommand {
if (!terminal.supportsColor) if (!terminal.supportsColor)
testArgs.insert(0, '--no-color'); testArgs.insert(0, '--no-color');
if (argResults['coverage'])
testArgs.insert(0, '--concurrency=1');
loader.installHook(); loader.installHook();
loader.shellPath = tools.getHostToolPath(HostTool.SkyShell); loader.shellPath = tools.getHostToolPath(HostTool.SkyShell);
if (!FileSystemEntity.isFileSync(loader.shellPath)) { if (!FileSystemEntity.isFileSync(loader.shellPath)) {
...@@ -105,6 +118,23 @@ class TestCommand extends FlutterCommand { ...@@ -105,6 +118,23 @@ class TestCommand extends FlutterCommand {
Cache.releaseLockEarly(); 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;
} }
} }
// 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);
}
}
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math' as math;
import 'package:async/async.dart'; import 'package:async/async.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
...@@ -16,6 +17,7 @@ import 'package:test/src/runner/plugin/hack_register_platform.dart' as hack; // ...@@ -16,6 +17,7 @@ import 'package:test/src/runner/plugin/hack_register_platform.dart' as hack; //
import '../dart/package_map.dart'; import '../dart/package_map.dart';
import '../globals.dart'; import '../globals.dart';
import 'coverage_collector.dart';
final String _kSkyShell = Platform.environment['SKY_SHELL']; final String _kSkyShell = Platform.environment['SKY_SHELL'];
const String _kHost = '127.0.0.1'; const String _kHost = '127.0.0.1';
...@@ -45,15 +47,20 @@ Future<_ServerInfo> _startServer() async { ...@@ -45,15 +47,20 @@ Future<_ServerInfo> _startServer() async {
return new _ServerInfo(server, 'ws://$_kHost:${server.port}$_kPath', socket.future); 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. assert(shellPath != null || _kSkyShell != null); // Please provide the path to the shell in the SKY_SHELL environment variable.
String executable = shellPath ?? _kSkyShell; 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', '--enable-checked-mode',
'--non-interactive',
'--packages=$packages', '--packages=$packages',
mainPath mainPath
]; ]);
printTrace('$executable ${arguments.join(' ')}'); printTrace('$executable ${arguments.join(' ')}');
return Process.start(executable, arguments, environment: <String, String>{ 'FLUTTER_TEST': 'true' }); return Process.start(executable, arguments, environment: <String, String>{ 'FLUTTER_TEST': 'true' });
} }
...@@ -105,8 +112,16 @@ void main() { ...@@ -105,8 +112,16 @@ void main() {
} }
'''); ''');
int observatoryPort;
if (CoverageCollector.instance.enabled) {
observatoryPort = new math.Random().nextInt(30000) + 2000;
await CoverageCollector.instance.finishPendingJobs();
}
Process process = await _startProcess( Process process = await _startProcess(
listenerFile.path, packages: PackageMap.globalPackagesPath listenerFile.path,
packages: PackageMap.globalPackagesPath,
observatoryPort: observatoryPort
); );
_attachStandardStreams(process); _attachStandardStreams(process);
...@@ -115,7 +130,11 @@ void main() { ...@@ -115,7 +130,11 @@ void main() {
if (process != null) { if (process != null) {
Process processToKill = process; Process processToKill = process;
process = null; process = null;
processToKill.kill(); CoverageCollector.instance.collectCoverage(
host: _kHost,
port: observatoryPort,
processToKill: processToKill
);
} }
if (tempDir != null) { if (tempDir != null) {
Directory dirToDelete = tempDir; Directory dirToDelete = tempDir;
......
...@@ -10,6 +10,7 @@ environment: ...@@ -10,6 +10,7 @@ environment:
dependencies: dependencies:
archive: ^1.0.20 archive: ^1.0.20
args: ^0.13.4 args: ^0.13.4
coverage: ^0.7.7
crypto: '>=1.1.1 <3.0.0' crypto: '>=1.1.1 <3.0.0'
file: ^0.1.0 file: ^0.1.0
http: ^0.11.3 http: ^0.11.3
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment