blob: 7c481b0d5f31ff31caa294082740e018aaaf38f3 [file] [log] [blame]
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. 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:vm_service/vm_service.dart';
import 'util.dart';
import 'hitmap.dart';
const _retryInterval = Duration(milliseconds: 200);
/// Collects coverage for all isolates in the running VM.
///
/// Collects a hit-map containing merged coverage for all isolates in the Dart
/// VM associated with the specified [serviceUri]. Returns a map suitable for
/// input to the coverage formatters that ship with this package.
///
/// [serviceUri] must specify the http/https URI of the service port of a
/// running Dart VM and must not be null.
///
/// If [resume] is true, all isolates will be resumed once coverage collection
/// is complete.
///
/// If [waitPaused] is true, collection will not begin until all isolates are
/// in the paused state.
///
/// If [includeDart] is true, code coverage for core `dart:*` libraries will be
/// collected.
///
/// If [functionCoverage] is true, function coverage information will be
/// collected.
///
/// If [scopedOutput] is non-empty, coverage will be restricted so that only
/// scripts that start with any of the provided paths are considered.
///
/// if [isolateIds] is set, the coverage gathering will be restricted to only
/// those VM isolates.
Future<Map<String, dynamic>> collect(Uri serviceUri, bool resume,
bool waitPaused, bool includeDart, Set<String>? scopedOutput,
{Set<String>? isolateIds,
Duration? timeout,
bool functionCoverage = false}) async {
scopedOutput ??= <String>{};
// Create websocket URI. Handle any trailing slashes.
final pathSegments =
serviceUri.pathSegments.where((c) => c.isNotEmpty).toList()..add('ws');
final uri = serviceUri.replace(scheme: 'ws', pathSegments: pathSegments);
late VmService service;
await retry(() async {
try {
final options = const CompressionOptions(enabled: false);
final socket = await WebSocket.connect('$uri', compression: options);
final controller = StreamController<String>();
socket.listen((data) => controller.add(data as String), onDone: () {
controller.close();
service.dispose();
});
service = VmService(
controller.stream, (String message) => socket.add(message),
log: StdoutLog(), disposeHandler: () => socket.close());
await service.getVM().timeout(_retryInterval);
} on TimeoutException {
// The signature changed in vm_service version 6.0.0.
// ignore: await_only_futures
await service.dispose();
rethrow;
}
}, _retryInterval, timeout: timeout);
try {
if (waitPaused) {
await _waitIsolatesPaused(service, timeout: timeout);
}
return await _getAllCoverage(
service, includeDart, functionCoverage, scopedOutput, isolateIds);
} finally {
if (resume) {
await _resumeIsolates(service);
}
// The signature changed in vm_service version 6.0.0.
// ignore: await_only_futures
await service.dispose();
}
}
Future<Map<String, dynamic>> _getAllCoverage(
VmService service,
bool includeDart,
bool functionCoverage,
Set<String>? scopedOutput,
Set<String>? isolateIds) async {
scopedOutput ??= <String>{};
final vm = await service.getVM();
final allCoverage = <Map<String, dynamic>>[];
for (var isolateRef in vm.isolates!) {
if (isolateIds != null && !isolateIds.contains(isolateRef.id)) continue;
if (scopedOutput.isNotEmpty) {
final scripts = await service.getScripts(isolateRef.id!);
for (var script in scripts.scripts!) {
final uri = Uri.parse(script.uri!);
if (uri.scheme != 'package') continue;
final scope = uri.path.split('/').first;
// Skip scripts which should not be included in the report.
if (!scopedOutput.contains(scope)) continue;
final scriptReport = await service.getSourceReport(
isolateRef.id!, <String>[SourceReportKind.kCoverage],
forceCompile: true, scriptId: script.id);
final coverage = await _getCoverageJson(
service, isolateRef, scriptReport, includeDart, functionCoverage);
allCoverage.addAll(coverage);
}
} else {
final isolateReport = await service.getSourceReport(
isolateRef.id!,
<String>[SourceReportKind.kCoverage],
forceCompile: true,
);
final coverage = await _getCoverageJson(
service, isolateRef, isolateReport, includeDart, functionCoverage);
allCoverage.addAll(coverage);
}
}
return <String, dynamic>{'type': 'CodeCoverage', 'coverage': allCoverage};
}
Future _resumeIsolates(VmService service) async {
final vm = await service.getVM();
final futures = <Future>[];
for (var isolateRef in vm.isolates!) {
// Guard against sync as well as async errors: sync - when we are writing
// message to the socket, the socket might be closed; async - when we are
// waiting for the response, the socket again closes.
futures.add(Future.sync(() async {
final isolate = await service.getIsolate(isolateRef.id!);
if (isolate.pauseEvent!.kind != EventKind.kResume) {
await service.resume(isolateRef.id!);
}
}));
}
try {
await Future.wait(futures);
} catch (_) {
// Ignore resume isolate failures
}
}
Future _waitIsolatesPaused(VmService service, {Duration? timeout}) async {
final pauseEvents = <String>{
EventKind.kPauseStart,
EventKind.kPauseException,
EventKind.kPauseExit,
EventKind.kPauseInterrupted,
EventKind.kPauseBreakpoint
};
Future allPaused() async {
final vm = await service.getVM();
if (vm.isolates!.isEmpty) throw 'No isolates.';
for (var isolateRef in vm.isolates!) {
final isolate = await service.getIsolate(isolateRef.id!);
if (!pauseEvents.contains(isolate.pauseEvent!.kind)) {
throw 'Unpaused isolates remaining.';
}
}
}
return retry(allPaused, _retryInterval, timeout: timeout);
}
/// Returns the line number to which the specified token position maps.
///
/// Performs a binary search within the script's token position table to locate
/// the line in question.
int? _getLineFromTokenPos(Script script, int tokenPos) {
// TODO(cbracken): investigate whether caching this lookup results in
// significant performance gains.
var min = 0;
var max = script.tokenPosTable!.length;
while (min < max) {
final mid = min + ((max - min) >> 1);
final row = script.tokenPosTable![mid];
if (row[1] > tokenPos) {
max = mid;
} else {
for (var i = 1; i < row.length; i += 2) {
if (row[i] == tokenPos) return row.first;
}
min = mid + 1;
}
}
return null;
}
Future<void> _processFunction(VmService service, IsolateRef isolateRef,
Script script, FuncRef funcRef, HitMap hits) async {
final func = await service.getObject(isolateRef.id!, funcRef.id!) as Func;
final location = func.location;
if (location != null) {
final funcName = await _getFuncName(service, isolateRef, func);
final tokenPos = location.tokenPos!;
final line = _getLineFromTokenPos(script, tokenPos);
if (line == null) {
print('tokenPos $tokenPos has no line mapping for script ${script.uri!}');
return;
}
hits.funcNames![line] = funcName;
}
}
/// Returns a JSON coverage list backward-compatible with pre-1.16.0 SDKs.
Future<List<Map<String, dynamic>>> _getCoverageJson(
VmService service,
IsolateRef isolateRef,
SourceReport report,
bool includeDart,
bool functionCoverage) async {
final hitMaps = <Uri, HitMap>{};
final scripts = <ScriptRef, Script>{};
final libraries = <LibraryRef>{};
for (var range in report.ranges!) {
final scriptRef = report.scripts![range.scriptIndex!];
final scriptUri = Uri.parse(report.scripts![range.scriptIndex!].uri!);
// Not returned in scripts section of source report.
if (scriptUri.scheme == 'evaluate') continue;
// Skip scripts from dart:.
if (!includeDart && scriptUri.scheme == 'dart') continue;
if (!scripts.containsKey(scriptRef)) {
scripts[scriptRef] =
await service.getObject(isolateRef.id!, scriptRef.id!) as Script;
}
final script = scripts[scriptRef];
if (script == null) continue;
// Look up the hit maps for this script (shared across isolates).
final hits = hitMaps.putIfAbsent(scriptUri, () => HitMap());
// If the script's library isn't loaded, load it then look up all its funcs.
final libRef = script.library;
if (functionCoverage && libRef != null && !libraries.contains(libRef)) {
hits.funcHits ??= <int, int>{};
hits.funcNames ??= <int, String>{};
libraries.add(libRef);
final library =
await service.getObject(isolateRef.id!, libRef.id!) as Library;
if (library.functions != null) {
for (var funcRef in library.functions!) {
await _processFunction(service, isolateRef, script, funcRef, hits);
}
}
if (library.classes != null) {
for (var classRef in library.classes!) {
final clazz =
await service.getObject(isolateRef.id!, classRef.id!) as Class;
if (clazz.functions != null) {
for (var funcRef in clazz.functions!) {
await _processFunction(
service, isolateRef, script, funcRef, hits);
}
}
}
}
}
// Collect hits and misses.
final coverage = range.coverage;
if (coverage == null) continue;
for (final tokenPos in coverage.hits!) {
final line = _getLineFromTokenPos(script, tokenPos);
if (line == null) {
print('tokenPos $tokenPos has no line mapping for script $scriptUri');
continue;
}
_incrementCountForKey(hits.lineHits, line);
if (hits.funcNames != null && hits.funcNames!.containsKey(line)) {
_incrementCountForKey(hits.funcHits!, line);
}
}
for (final tokenPos in coverage.misses!) {
final line = _getLineFromTokenPos(script, tokenPos);
if (line == null) {
print('tokenPos $tokenPos has no line mapping for script $scriptUri');
continue;
}
hits.lineHits.putIfAbsent(line, () => 0);
}
hits.funcNames?.forEach((line, funcName) {
hits.funcHits?.putIfAbsent(line, () => 0);
});
}
// Output JSON
final coverage = <Map<String, dynamic>>[];
hitMaps.forEach((uri, hits) {
coverage.add(toScriptCoverageJson(uri, hits));
});
return coverage;
}
void _incrementCountForKey(Map<int, int> counter, int key) {
counter[key] = counter.containsKey(key) ? counter[key]! + 1 : 1;
}
Future<String> _getFuncName(
VmService service, IsolateRef isolateRef, Func func) async {
if (func.name == null) {
return '${func.type}:${func.location!.tokenPos}';
}
final owner = func.owner;
if (owner is ClassRef) {
final cls = await service.getObject(isolateRef.id!, owner.id!) as Class;
if (cls.name != null) return '${cls.name}.${func.name}';
}
return func.name!;
}
class StdoutLog extends Log {
@override
void warning(String message) => print(message);
@override
void severe(String message) => print(message);
}