Unverified Commit 301eaa8c authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Make coverage, like, really fast (#30811)

parent 27b058a4
......@@ -18,6 +18,7 @@ import 'package:flutter_tools/src/dart/package_map.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/disabled_usage.dart';
import 'package:flutter_tools/src/globals.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/test/coverage_collector.dart';
import 'package:flutter_tools/src/test/runner.dart';
import 'package:flutter_tools/src/usage.dart';
......@@ -115,7 +116,7 @@ Future<void> run(List<String> args) async {
Directory testDirectory;
CoverageCollector collector;
if (argResults['coverage']) {
collector = CoverageCollector();
collector = CoverageCollector(await FlutterProject.current(), coverageDirectory: coverageDirectory);
if (!argResults.options.contains(_kOptionTestDirectory)) {
throwToolExit('Use of --coverage requires setting --test-directory');
}
......@@ -141,6 +142,7 @@ Future<void> run(List<String> args) async {
precompiledDillFiles: tests,
concurrency: math.max(1, platform.numberOfProcessors - 2),
icudtlPath: fs.path.absolute(argResults[_kOptionIcudtl]),
coverageDirectory: coverageDirectory,
);
if (collector != null) {
......
......@@ -37,6 +37,7 @@ import 'process.dart';
export 'dart:io'
show
BytesBuilder,
CompressionOptions,
// Directory NO! Use `file_system.dart`
exitCode,
// File NO! Use `file_system.dart`
......
......@@ -149,7 +149,7 @@ class TestCommand extends FastFlutterCommand {
CoverageCollector collector;
if (argResults['coverage'] || argResults['merge-coverage']) {
collector = CoverageCollector();
collector = CoverageCollector(await FlutterProject.current());
}
final bool machine = argResults['machine'];
......
......@@ -14,12 +14,18 @@ import '../base/platform.dart';
import '../base/process_manager.dart';
import '../dart/package_map.dart';
import '../globals.dart';
import '../project.dart';
import '../vmservice.dart';
import 'watcher.dart';
/// A class that's used to collect coverage data during tests.
class CoverageCollector extends TestWatcher {
CoverageCollector(this.flutterProject, {this.coverageDirectory});
Map<String, dynamic> _globalHitmap;
final Directory coverageDirectory;
final FlutterProject flutterProject;
@override
Future<void> handleFinishedTest(ProcessEvent event) async {
......@@ -43,7 +49,6 @@ class CoverageCollector extends TestWatcher {
Future<void> collectCoverage(Process process, Uri observatoryUri) async {
assert(process != null);
assert(observatoryUri != null);
final int pid = process.pid;
printTrace('pid $pid: collecting coverage data from $observatoryUri...');
......@@ -52,7 +57,14 @@ class CoverageCollector extends TestWatcher {
.then<void>((int code) {
throw Exception('Failed to collect coverage, process terminated prematurely with exit code $code.');
});
final Future<void> collectionComplete = coverage.collect(observatoryUri, false, false)
final Future<void> collectionComplete = collect(observatoryUri, (String libraryName) {
// If we have a specified coverage directory or could not find the package name, then
// accept all libraries.
if (coverageDirectory != null) {
return true;
}
return libraryName.contains(flutterProject.manifest.appName);
})
.then<void>((Map<String, dynamic> result) {
if (result == null)
throw Exception('Failed to collect coverage.');
......@@ -141,3 +153,136 @@ class CoverageCollector extends TestWatcher {
return true;
}
}
Future<Map<String, dynamic>> collect(Uri serviceUri, bool Function(String) libraryPredicate) async {
final VMService vmService = await VMService.connect(serviceUri, compression: CompressionOptions.compressionOff);
await vmService.getVM();
return _getAllCoverage(vmService, libraryPredicate);
}
Future<Map<String, dynamic>> _getAllCoverage(VMService service, bool Function(String) libraryPredicate) async {
await service.getVM();
final List<Map<String, dynamic>> coverage = <Map<String, dynamic>>[];
for (Isolate isolateRef in service.vm.isolates) {
await isolateRef.load();
final Map<String, dynamic> scriptList = await isolateRef.invokeRpcRaw('getScripts', params: <String, dynamic>{'isolateId': isolateRef.id});
final List<Future<void>> futures = <Future<void>>[];
final Map<String, Map<String, dynamic>> scripts = <String, Map<String, dynamic>>{};
final Map<String, Map<String, dynamic>> sourceReports = <String, Map<String, dynamic>>{};
// For each ScriptRef loaded into the VM, load the corresponding Script and
// SourceReport object.
for (Map<String, dynamic> script in scriptList['scripts']) {
if (!libraryPredicate(script['uri'])) {
continue;
}
final String scriptId = script['id'];
futures.add(
isolateRef.invokeRpcRaw('getSourceReport', params: <String, dynamic>{
'forceCompile': true,
'scriptId': scriptId,
'isolateId': isolateRef.id,
'reports': <String>['Coverage'],
})
.then((Map<String, dynamic> report) {
sourceReports[scriptId] = report;
})
);
futures.add(
isolateRef.invokeRpcRaw('getObject', params: <String, dynamic>{
'isolateId': isolateRef.id,
'objectId': scriptId,
})
.then((Map<String, dynamic> script) {
scripts[scriptId] = script;
})
);
}
await Future.wait(futures);
_buildCoverageMap(scripts, sourceReports, coverage);
}
return <String, dynamic>{'type': 'CodeCoverage', 'coverage': coverage};
}
// Build a hitmap of Uri -> Line -> Hit Count for each script object.
void _buildCoverageMap(
Map<String, Map<String, dynamic>> scripts,
Map<String, Map<String, dynamic>> sourceReports,
List<Map<String, dynamic>> coverage,
) {
final Map<String, Map<int, int>> hitMaps = <String, Map<int, int>>{};
for (String scriptId in scripts.keys) {
final Map<String, dynamic> sourceReport = sourceReports[scriptId];
for (Map<String, dynamic> range in sourceReport['ranges']) {
final Map<String, dynamic> coverage = range['coverage'];
final Map<String, dynamic> scriptRef = sourceReport['scripts'][range['scriptIndex']];
final String uri = scriptRef['uri'];
hitMaps[uri] ??= <int, int>{};
final Map<int, int> hitMap = hitMaps[uri];
final List<dynamic> hits = coverage['hits'];
final List<dynamic> misses = coverage['misses'];
final List<dynamic> tokenPositions = scripts[scriptRef['id']]['tokenPosTable'];
if (hits != null) {
for (int hit in hits) {
final int line = _lineAndColumn(hit, tokenPositions)[0];
final int current = hitMap[line] ?? 0;
hitMap[line] = current + 1;
}
}
if (misses != null) {
for (int miss in misses) {
final int line = _lineAndColumn(miss, tokenPositions)[0];
hitMap[line] ??= 0;
}
}
}
}
hitMaps.forEach((String uri, Map<int, int> hitMap) {
coverage.add(_toScriptCoverageJson(uri, hitMap));
});
}
// Binary search the token position table for the line and column which
// corresponds to each token position.
// The format of this table is described in https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#script
List<int> _lineAndColumn(int position, List<dynamic> tokenPositions) {
int min = 0;
int max = tokenPositions.length;
while (min < max) {
final int mid = min + ((max - min) >> 1);
final List<dynamic> row = tokenPositions[mid];
if (row[1] > position) {
max = mid;
} else {
for (int i = 1; i < row.length; i += 2) {
if (row[i] == position) {
return <int>[row.first, row[i + 1]];
}
}
min = mid + 1;
}
}
throw StateError('Unreachable');
}
// Returns a JSON hit map backward-compatible with pre-1.16.0 SDKs.
Map<String, dynamic> _toScriptCoverageJson(String scriptUri, Map<int, int> hitMap) {
final Map<String, dynamic> json = <String, dynamic>{};
final List<int> hits = <int>[];
hitMap.forEach((int line, int hitCount) {
hits.add(line);
hits.add(hitCount);
});
json['source'] = scriptUri;
json['script'] = <String, dynamic>{
'type': '@Script',
'fixedId': true,
'id': 'libraries/1/scripts/${Uri.encodeComponent(scriptUri)}',
'uri': scriptUri,
'_kind': 'library',
};
json['hits'] = hits;
return json;
}
......@@ -37,6 +37,7 @@ Future<int> runTests(
@required int concurrency,
FlutterProject flutterProject,
String icudtlPath,
Directory coverageDirectory,
}) async {
// Compute the command-line arguments for package:test.
final List<String> testArgs = <String>[];
......
......@@ -24,10 +24,10 @@ import 'vmservice_record_replay.dart';
/// Override `WebSocketConnector` in [context] to use a different constructor
/// for [WebSocket]s (used by tests).
typedef WebSocketConnector = Future<io.WebSocket> Function(String url);
typedef WebSocketConnector = Future<io.WebSocket> Function(String url, {io.CompressionOptions compression});
/// A function that opens a two-way communication channel to the specified [uri].
typedef _OpenChannel = Future<StreamChannel<String>> Function(Uri uri);
typedef _OpenChannel = Future<StreamChannel<String>> Function(Uri uri, {io.CompressionOptions compression});
_OpenChannel _openChannel = _defaultOpenChannel;
......@@ -62,7 +62,7 @@ typedef CompileExpression = Future<String> Function(
const String _kRecordingType = 'vmservice';
Future<StreamChannel<String>> _defaultOpenChannel(Uri uri) async {
Future<StreamChannel<String>> _defaultOpenChannel(Uri uri, {io.CompressionOptions compression = io.CompressionOptions.compressionDefault}) async {
Duration delay = const Duration(milliseconds: 100);
int attempts = 0;
io.WebSocket socket;
......@@ -86,7 +86,7 @@ Future<StreamChannel<String>> _defaultOpenChannel(Uri uri) async {
while (socket == null) {
attempts += 1;
try {
socket = await constructor(uri.toString());
socket = await constructor(uri.toString(), compression: compression);
} on io.WebSocketException catch (e) {
await handleError(e);
} on io.SocketException catch (e) {
......@@ -220,7 +220,7 @@ class VMService {
/// `"vmservice"` subdirectory.
static void enableRecordingConnection(String location) {
final Directory dir = getRecordingSink(location, _kRecordingType);
_openChannel = (Uri uri) async {
_openChannel = (Uri uri, {io.CompressionOptions compression}) async {
final StreamChannel<String> delegate = await _defaultOpenChannel(uri);
return RecordingVMServiceChannel(delegate, dir);
};
......@@ -233,7 +233,7 @@ class VMService {
/// passed to [enableRecordingConnection]), or a [ToolExit] will be thrown.
static void enableReplayConnection(String location) {
final Directory dir = getReplaySource(location, _kRecordingType);
_openChannel = (Uri uri) async => ReplayVMServiceChannel(dir);
_openChannel = (Uri uri, {io.CompressionOptions compression}) async => ReplayVMServiceChannel(dir);
}
/// Connect to a Dart VM Service at [httpUri].
......@@ -249,9 +249,10 @@ class VMService {
ReloadSources reloadSources,
Restart restart,
CompileExpression compileExpression,
io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
}) async {
final Uri wsUri = httpUri.replace(scheme: 'ws', path: fs.path.join(httpUri.path, 'ws'));
final StreamChannel<String> channel = await _openChannel(wsUri);
final StreamChannel<String> channel = await _openChannel(wsUri, compression: compression);
final rpc.Peer peer = rpc.Peer.withoutJson(jsonDocument.bind(channel));
final VMService service = VMService(peer, httpUri, wsUri, reloadSources, restart, compileExpression);
// This call is to ensure we are able to establish a connection instead of
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
......@@ -178,7 +179,7 @@ void main() {
}, overrides: <Type, Generator>{
Logger: () => StdoutLogger(),
Stdio: () => mockStdio,
WebSocketConnector: () => (String url) async => throw const SocketException('test'),
WebSocketConnector: () => (String url, {CompressionOptions compression}) async => throw const SocketException('test'),
});
testUsingContext('refreshViews', () {
......
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