Skip to content
This repository was archived by the owner on Nov 21, 2024. It is now read-only.
Draft
12 changes: 12 additions & 0 deletions packages/patrol/lib/src/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,15 @@ import 'package:meta/meta.dart';
/// Whether Hot Restart is enabled.
@internal
const bool hotRestartEnabled = bool.fromEnvironment('PATROL_HOT_RESTART');
/// Whether coverage is enabled.
@internal
const bool coverage = bool.fromEnvironment('PATROL_COVERAGE');
/// Collect function coverage info
@internal
const bool functionCoverage = bool.fromEnvironment('PATROL_FUNCTION_COVERAGE');
/// A regular expression matching packages names to include in the coverage report.
@internal
const String coveragePackageList = String.fromEnvironment('PATROL_COVERAGE_PACKAGES');
/// The package config contents.
@internal
const String packageConfig = String.fromEnvironment('PATROL_PACKAGE_CONFIG');
169 changes: 169 additions & 0 deletions packages/patrol/lib/src/coverage/coverage_collector.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// ignore_for_file: avoid_print

// TODO: Use a logger instead of print

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:coverage/coverage.dart' as coverage;
import 'package:path_provider/path_provider.dart';
import 'package:patrol/src/coverage/coverage_options.dart';
import 'package:vm_service/vm_service.dart';
import 'package:vm_service/vm_service_io.dart';

import 'server_uri_processor.dart';

/// A singleton class responsible for collecting and managing code coverage data.
class CoverageCollector {

/// Returns the singleton instance of [CoverageCollector].
factory CoverageCollector() => _instance;
CoverageCollector._internal();
static final CoverageCollector _instance = CoverageCollector._internal();

late final CoverageOptions _options;

VmService? _service;
Set<String>? _libraryNames;

bool _isInitialized = false;
bool _isRunning = false;
final _completer = Completer<void>();

String? _currentObservatoryUrlWs;
String? _currentObservatoryUrlHttp;

late ServerUriProcessor _serverUriProcessor;

/// Initializes the CoverageCollector with required dependencies.
Future<void> initialize({CoverageOptions? options}) async {
if (_isInitialized) {
return;
}
_options = options ?? const CoverageOptions();
_isInitialized = true;
_libraryNames = await _options.getCoveragePackages();

// Initialize ServerUriProcessor
_serverUriProcessor = ServerUriProcessor(_handleServerUri);
}

/// Starts the coverage collection process in the background.
Future<void> startInBackground() async {
_ensureInitialized();
_isRunning = true;

unawaited(_serverUriProcessor.start());
unawaited(_runCoverageCollection());
}

/// Stops the coverage collection process and writes the collected data.
Future<void> stop() async {
if (!_isRunning) {
return;
}

_isRunning = false;
await _serverUriProcessor.stop();
await _service?.dispose();
_completer.complete();
}

/// Collects coverage data for a completed test.
Future<void> handleTestCompletion(String testName) async {
if (!_isRunning || _service == null) {
print('Not running or service is null');
return;
}
final vm = await _service!.getVM();
final isolateId = vm.isolates!.first.id!;
await collectCoverage(isolateId);
await stop();
print('Collecting coverage for test: $testName');
}

void _ensureInitialized() {
if (!_isInitialized) {
throw StateError('CoverageCollector is not initialized');
}
}

Future<void> _runCoverageCollection() async {
try {
await _completer.future;
} catch (e) {
print('Error running coverage collection: $e');
}
}

/// Handles the server URI when it becomes available.
void _handleServerUri(Uri serverUri) {
_currentObservatoryUrlHttp = serverUri.toString();
_currentObservatoryUrlWs = _convertToWebSocketUrl(_currentObservatoryUrlHttp!);
_connectToVmService();
}

Future<void> __writeCoverageDataToJsonFile(String coverageJsonData) async {
try {
Directory? downloadsDir;
if (Platform.isAndroid) {
downloadsDir = Directory('/sdcard/Download');
} else {
downloadsDir = await getDownloadsDirectory();
}

final file = File('${downloadsDir!.path}/coverage_${DateTime.now().millisecondsSinceEpoch}.json')
..createSync(recursive: true)
..writeAsStringSync(coverageJsonData, flush: true);

print('Wrote coverage data to ${file.path}');
} catch (err) {
print('Error writing coverage data: $err');
}
}

/// Collects coverage data for a specific isolate.
Future<void> collectCoverage(String isolateId) async {
final libraryNamesList = _libraryNames?.toList();
if (libraryNamesList == null || libraryNamesList.isEmpty) {
return;
}

final data = await _collectCoverageData(libraryNamesList);
await __writeCoverageDataToJsonFile(jsonEncode(data));
}

Future<void> _connectToVmService() async {
final wsUrl = _convertToWebSocketUrl(_currentObservatoryUrlWs!);

try {
_service = await vmServiceConnectUri(wsUrl);
} catch (e) {
_isRunning = false;
_completer.complete();
rethrow;
}
}

Future<Map<String, dynamic>> _collectCoverageData(List<String> libraryNamesList) async {
return coverage.collect(
Uri.parse(_currentObservatoryUrlHttp!),
true,
false,
false,
libraryNamesList.toSet(),
branchCoverage: _options.branchCoverage,
functionCoverage: _options.functionCoverage,
timeout: _options.timeout,
);
}

String _convertToWebSocketUrl(String observatoryUri) {
var observatoryUriWs = observatoryUri.replaceFirst('http://', 'ws://');
if (!observatoryUriWs.endsWith('/ws')) {
observatoryUriWs += 'ws';
}
return observatoryUriWs;
}
}
34 changes: 34 additions & 0 deletions packages/patrol/lib/src/coverage/coverage_options.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import '../constants.dart' as constants;

/// Class representing coverage options for generating coverage reports.
class CoverageOptions {
/// Creates an instance of [CoverageOptions] with the given parameters.
const CoverageOptions({
this.coverage = true,
this.timeout = const Duration(minutes: 1),
this.functionCoverage = false,
this.branchCoverage = false,
this.coveragePackageConfig = '',
});

/// Whether to include coverage information.
final bool coverage;

/// Timeout duration for connecting, in seconds.
final Duration timeout;

/// Whether to include function coverage.
final bool functionCoverage;

/// Whether to include branch coverage.
final bool branchCoverage;

/// Path to the coverage package configuration file.
final String? coveragePackageConfig;

/// Returns the coverage packages to include in the coverage report.
Future<Set<String>> getCoveragePackages() async {
final packagesToInclude = constants.coveragePackageList.split(',');
return packagesToInclude.toSet();
}
}
41 changes: 41 additions & 0 deletions packages/patrol/lib/src/coverage/server_uri_processor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import 'dart:async';
import 'dart:developer' as developer;

/// A class that processes the server URI by periodically checking for it.
class ServerUriProcessor {
/// Creates an instance of [ServerUriProcessor] with a callback function to handle the server URI.
ServerUriProcessor(this.onServerUri);

/// A callback function that handles the server URI when it's found.
final void Function(Uri) onServerUri;

Timer? _checkTimer;

/// Starts the server URI processing by initiating periodic checks.
Future<void> start() async {
_startListening();
}

/// Stops the server URI processing by cancelling the periodic checks.
Future<void> stop() async {
_checkTimer?.cancel();
}

void _startListening() {
_checkTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
_checkForServerUri();
});
}

Future<void> _checkForServerUri() async {
try {
final info = await developer.Service.getInfo();
if (info.serverUri != null) {
onServerUri(info.serverUri!);
await stop();
}
} catch (e) {
developer.log('Error checking for server URI: $e');
}
}
}
26 changes: 26 additions & 0 deletions packages/patrol/lib/src/native/patrol_app_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
// TODO: Use a logger instead of print

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:patrol/src/common.dart';
import 'package:patrol/src/coverage/coverage_options.dart';
import 'package:patrol/src/native/contracts/contracts.dart';
import 'package:patrol/src/native/contracts/patrol_app_service_server.dart';
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as shelf_io;

import '../constants.dart' as constans;
import '../coverage/coverage_collector.dart';

const _idleTimeout = Duration(hours: 2);

class _TestExecutionResult {
Expand All @@ -37,11 +42,28 @@ Future<void> runAppService(PatrolAppService service) async {

final address = server.address;

if (constans.coverage) {
await _startCoverageService();
}

print(
'PatrolAppService started, address: ${address.address}, host: ${address.host}, port: ${server.port}',
);
}

Future<void> _startCoverageService() async {
print('Starting coverage service');
final packageConfig = utf8.decode(base64.decode(constans.packageConfig));
final coverageOptions = CoverageOptions(
// ignore: avoid_redundant_argument_values
functionCoverage: constans.functionCoverage,
coveragePackageConfig: packageConfig,
);

await CoverageCollector().initialize(options: coverageOptions);
await CoverageCollector().startInBackground();
}

/// Implements a stateful HTTP service for querying and executing Dart tests.
///
/// This is an internal class and you don't want to use it. It's public so that
Expand Down Expand Up @@ -101,6 +123,10 @@ class PatrolAppService extends PatrolAppServiceServer {
'that was most recently requested to run was $requestedDartTestName',
);

if(constans.coverage){
await CoverageCollector().handleTestCompletion(dartFileName);
}

_testExecutionCompleted.complete(
_TestExecutionResult(passed: passed, details: details),
);
Expand Down
3 changes: 3 additions & 0 deletions packages/patrol/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ environment:
flutter: '>=3.22.0'

dependencies:
coverage: ^1.8.0
equatable: ^2.0.5
flutter:
sdk: flutter
Expand All @@ -26,8 +27,10 @@ dependencies:
json_annotation: ^4.8.1
meta: ^1.10.0
patrol_finders: ^2.1.2
path_provider: ^2.0.11
shelf: ^1.4.1
test_api: '^0.7.0'
vm_service: ^14.2.1

dev_dependencies:
build_runner: ^2.4.6
Expand Down
Loading