| // Copyright (c) 2024, 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:convert'; |
| import 'dart:developer'; |
| import 'dart:io'; |
| |
| import 'vm_service_helper.dart'; |
| |
| Future<Coverage?> collectCoverage({ |
| required String displayName, |
| bool getKernelServiceCoverageToo = false, |
| bool forceCompile = false, |
| }) async { |
| ServiceProtocolInfo service = |
| await Service.controlWebServer(enable: true, silenceOutput: true); |
| if (service.serverUri == null) { |
| return null; |
| } |
| VMServiceHelper helper = new VMServiceHelper(); |
| await helper.connect(service.serverUri!); |
| |
| Coverage? result = await collectCoverageWithHelper( |
| helper: helper, |
| getKernelServiceCoverageToo: getKernelServiceCoverageToo, |
| displayName: displayName, |
| forceCompile: forceCompile, |
| ); |
| await helper.disconnect(); |
| return result; |
| } |
| |
| Future<Coverage?> collectCoverageWithHelper( |
| {required VMServiceHelper helper, |
| required final bool getKernelServiceCoverageToo, |
| required final String displayName, |
| bool forceCompile = false}) async { |
| VM vm = await helper.serviceClient.getVM(); |
| final List<String> isolateIds = []; |
| if (getKernelServiceCoverageToo) { |
| List<IsolateRef> kernelServiceIsolates = vm.systemIsolates! |
| .where((element) => element.name == "kernel-service") |
| .toList(); |
| |
| if (kernelServiceIsolates.length < 1) { |
| print("Expected (at least) 1 kernel-service isolate, " |
| "got ${kernelServiceIsolates.length}"); |
| return null; |
| } |
| |
| // TODO(jensj): I guess we should look at the isolate group; also we could |
| // just iterate through them to get all coverage... |
| if (kernelServiceIsolates.length != 1) { |
| print("Got ${kernelServiceIsolates.length} kernel-service isolates. " |
| "Picking the first one."); |
| } |
| isolateIds.add(kernelServiceIsolates.first.id!); |
| } |
| |
| if (vm.isolates!.isEmpty) { |
| print("Expected (at least) 1 isolate, got ${vm.isolates!.length}"); |
| return null; |
| } |
| |
| // TODO(jensj): I guess we should look at the isolate group; also we could |
| // just iterate through them to get all coverage... |
| if (vm.isolates!.length != 1) { |
| print("Got ${vm.isolates!.length} isolates. Picking the first one."); |
| } |
| isolateIds.add(vm.isolates!.first.id!); |
| |
| List<SourceReport> sourceReports = []; |
| for (String isolateId in isolateIds) { |
| final Stopwatch stopwatch = new Stopwatch()..start(); |
| sourceReports.add(await helper.serviceClient.getSourceReport( |
| isolateId, [SourceReportKind.kCoverage], |
| forceCompile: forceCompile)); |
| print("Got source report from VM in ${stopwatch.elapsedMilliseconds} ms"); |
| } |
| |
| bool includeCoverageFor(Uri uri) { |
| if (uri.isScheme("package")) { |
| return uri.pathSegments.first == "front_end" || |
| uri.pathSegments.first == "frontend_server" || |
| uri.pathSegments.first == "_fe_analyzer_shared" || |
| uri.pathSegments.first == "testing" || |
| uri.pathSegments.first == "kernel"; |
| } |
| return false; |
| } |
| |
| final Stopwatch stopwatch = new Stopwatch()..start(); |
| Coverage coverage = getCoverageFromSourceReport( |
| sourceReports, |
| includeCoverageFor, |
| displayName: displayName, |
| ); |
| print("Done in ${stopwatch.elapsed}"); |
| return coverage; |
| } |
| |
| Coverage getCoverageFromSourceReport( |
| List<SourceReport> sourceReports, |
| bool Function(Uri) shouldIncludeCoverageFor, { |
| String? displayName, |
| }) { |
| Coverage coverage = new Coverage(displayName ?? ""); |
| for (SourceReport sourceReport in sourceReports) { |
| for (SourceReportRange range in sourceReport.ranges!) { |
| ScriptRef script = sourceReport.scripts![range.scriptIndex!]; |
| Uri scriptUri = Uri.parse(script.uri!); |
| if (!shouldIncludeCoverageFor(scriptUri)) continue; |
| |
| final FileCoverage fileCoverage = |
| coverage.getOrAddFileCoverage(scriptUri); |
| SourceReportCoverage? sourceReportCoverage = range.coverage; |
| if (sourceReportCoverage == null) { |
| // Range not compiled. Record the range if provided. |
| assert(!range.compiled!); |
| if (range.startPos! >= 0 || range.endPos! >= 0) { |
| fileCoverage.notCompiled |
| .add(new StartEndPair(range.startPos!, range.endPos!)); |
| } |
| continue; |
| } |
| fileCoverage.hits.addAll(sourceReportCoverage.hits!); |
| fileCoverage.misses.addAll(sourceReportCoverage.misses!); |
| } |
| } |
| return coverage; |
| } |
| |
| class Coverage { |
| final Map<Uri, FileCoverage> _coverages = {}; |
| final String displayName; |
| |
| Coverage(this.displayName); |
| |
| FileCoverage getOrAddFileCoverage(Uri uri) { |
| return _coverages[uri] ??= new FileCoverage._(uri); |
| } |
| |
| List<FileCoverage> getAllFileCoverages() { |
| return _coverages.values.toList(); |
| } |
| |
| void printCoverage(bool printHits) { |
| for (FileCoverage fileCoverage in _coverages.values) { |
| if (fileCoverage.hits.isEmpty && |
| fileCoverage.misses.isEmpty && |
| fileCoverage.notCompiled.isEmpty) { |
| continue; |
| } |
| print(fileCoverage.uri); |
| if (printHits) { |
| print("Hits: ${fileCoverage.hits.toList()..sort()}"); |
| } |
| print("Misses: ${fileCoverage.misses.toList()..sort()}"); |
| print("Not compiled: ${fileCoverage.notCompiled.toList()..sort()}"); |
| print(""); |
| } |
| } |
| |
| Map<String, Object?> toJson() { |
| return { |
| "displayName": displayName, |
| "coverages": _coverages.values.toList(), |
| }; |
| } |
| |
| factory Coverage.fromJson(Map<String, Object?> json) { |
| Coverage result = new Coverage(json["displayName"] as String? ?? ""); |
| List coverages = json["coverages"] as List; |
| for (Map<String, dynamic> entry in coverages) { |
| FileCoverage fileCoverage = FileCoverage.fromJson(entry); |
| result._coverages[fileCoverage.uri] = fileCoverage; |
| } |
| return result; |
| } |
| |
| factory Coverage.loadFromFile(File f) { |
| return Coverage.fromJson(jsonDecode(f.readAsStringSync())); |
| } |
| |
| void writeToFile(File f) { |
| const JsonEncoder encoder = const JsonEncoder.withIndent(" "); |
| // We don't want it to fail because the parent dir didn't exist. |
| f.createSync(recursive: true); |
| f.writeAsStringSync(encoder.convert(this)); |
| print("Wrote coverage to $f"); |
| } |
| } |
| |
| class FileCoverage { |
| final Uri uri; |
| // File offset maps. |
| final Set<int> hits = {}; |
| final Set<int> misses = {}; |
| final Set<StartEndPair> notCompiled = {}; |
| |
| FileCoverage._(this.uri); |
| |
| Map<String, Object?> toJson() { |
| List<int> notCompiledRanges = []; |
| for (var pair in notCompiled.toList() |
| ..sort((a, b) => a.startPos.compareTo(b.startPos))) { |
| notCompiledRanges.add(pair.startPos); |
| notCompiledRanges.add(pair.endPos); |
| } |
| misses.removeAll(hits); |
| return { |
| "uri": uri.toString(), |
| "hits": hits.toList()..sort(), |
| "misses": misses.toList()..sort(), |
| "notCompiledRanges": notCompiledRanges, |
| }; |
| } |
| |
| factory FileCoverage.fromJson(Map<String, Object?> json) { |
| FileCoverage result = new FileCoverage._(Uri.parse(json["uri"] as String)); |
| List hits = json["hits"] as List; |
| for (int hit in hits) { |
| result.hits.add(hit); |
| } |
| List misses = json["misses"] as List; |
| for (int mis in misses) { |
| result.misses.add(mis); |
| } |
| List notCompiledRanges = json["notCompiledRanges"] as List; |
| for (int i = 0; i < notCompiledRanges.length; i += 2) { |
| result.notCompiled.add( |
| new StartEndPair(notCompiledRanges[i], notCompiledRanges[i + 1])); |
| } |
| return result; |
| } |
| } |
| |
| class StartEndPair implements Comparable { |
| final int startPos; |
| final int endPos; |
| |
| StartEndPair(this.startPos, this.endPos); |
| |
| @override |
| String toString() => "[$startPos - $endPos]"; |
| |
| @override |
| int compareTo(dynamic other) { |
| if (other is! StartEndPair) return -1; |
| StartEndPair o = other; |
| return startPos - o.startPos; |
| } |
| |
| @override |
| int get hashCode => Object.hash(startPos, endPos); |
| } |