blob: c6069316aa5d1811c0608c38f5b4ca5026aa8739 [file] [log] [blame]
// Copyright (c) 2022, 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:convert' show utf8, LineSplitter;
import 'dart:io';
import 'package:args/args.dart';
import 'package:coverage/src/util.dart' show extractVMServiceUri;
import 'package:package_config/package_config.dart';
import 'package:path/path.dart' as path;
import 'collect_coverage.dart' as collect_coverage;
import 'format_coverage.dart' as format_coverage;
final allProcesses = <Process>[];
Future<void> dartRun(List<String> args,
{void Function(String)? onStdout, String? workingDir}) async {
final process = await Process.start(
Platform.executable,
args,
workingDirectory: workingDir,
);
allProcesses.add(process);
final broadStdout = process.stdout.asBroadcastStream();
broadStdout.listen(stdout.add);
if (onStdout != null) {
broadStdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(onStdout);
}
process.stderr.listen(stderr.add);
final result = await process.exitCode;
if (result != 0) {
throw ProcessException(Platform.executable, args, '', result);
}
}
Future<String?> packageNameFromConfig(String packageDir) async {
final config = await findPackageConfig(Directory(packageDir));
return config?.packageOf(Uri.directory(packageDir))?.name;
}
void watchExitSignal(ProcessSignal signal) {
signal.watch().listen((sig) {
for (final process in allProcesses) {
process.kill(sig);
}
exit(1);
});
}
ArgParser createArgParser() {
final parser = ArgParser();
parser.addOption(
'package',
help: 'Root directory of the package to test.',
defaultsTo: '.',
);
parser.addOption(
'package-name',
help: 'Name of the package to test. '
'Deduced from --package if not provided.',
);
parser.addOption('port', help: 'VM service port.', defaultsTo: '8181');
parser.addOption('out',
abbr: 'o', help: 'Output directory. Defaults to <package-dir>/coverage.');
parser.addOption('test', help: 'Test script to run.', defaultsTo: 'test');
parser.addFlag(
'function-coverage',
abbr: 'f',
defaultsTo: false,
help: 'Collect function coverage info.',
);
parser.addFlag(
'branch-coverage',
abbr: 'b',
defaultsTo: false,
help: 'Collect branch coverage info.',
);
parser.addFlag('help', abbr: 'h', negatable: false, help: 'Show this help.');
return parser;
}
class Flags {
Flags(this.packageDir, this.packageName, this.outDir, this.port,
this.testScript, this.functionCoverage, this.branchCoverage);
final String packageDir;
final String packageName;
final String outDir;
final String port;
final String testScript;
final bool functionCoverage;
final bool branchCoverage;
}
Future<Flags> parseArgs(List<String> arguments) async {
final parser = createArgParser();
final args = parser.parse(arguments);
void printUsage() {
print('Runs tests and collects coverage for a package. By default this '
"script assumes it's being run from the root directory of a package, and "
'outputs a coverage.json and lcov.info to ./coverage/');
print('Usage: dart test_with_coverage.dart [OPTIONS...]\n');
print(parser.usage);
}
Never fail(String msg) {
print('\n$msg\n');
printUsage();
exit(1);
}
if (args['help'] as bool) {
printUsage();
exit(0);
}
final packageDir = path.canonicalize(args['package'] as String);
if (!FileSystemEntity.isDirectorySync(packageDir)) {
fail('--package is not a valid directory.');
}
final packageName = (args['package-name'] as String?) ??
await packageNameFromConfig(packageDir);
if (packageName == null) {
fail(
"Couldn't figure out package name from --package. Make sure this is a "
'package directory, or try passing --package-name explicitly.',
);
}
return Flags(
packageDir,
packageName,
(args['out'] as String?) ?? path.join(packageDir, 'coverage'),
args['port'] as String,
args['test'] as String,
args['function-coverage'] as bool,
args['branch-coverage'] as bool,
);
}
Future<void> main(List<String> arguments) async {
final flags = await parseArgs(arguments);
final outJson = path.join(flags.outDir, 'coverage.json');
final outLcov = path.join(flags.outDir, 'lcov.info');
if (!FileSystemEntity.isDirectorySync(flags.outDir)) {
await Directory(flags.outDir).create(recursive: true);
}
watchExitSignal(ProcessSignal.sighup);
watchExitSignal(ProcessSignal.sigint);
if (!Platform.isWindows) {
watchExitSignal(ProcessSignal.sigterm);
}
final serviceUriCompleter = Completer<Uri>();
final testProcess = dartRun([
if (flags.branchCoverage) '--branch-coverage',
'run',
'--pause-isolates-on-exit',
'--disable-service-auth-codes',
'--enable-vm-service=${flags.port}',
flags.testScript,
], onStdout: (line) {
if (!serviceUriCompleter.isCompleted) {
final uri = extractVMServiceUri(line);
if (uri != null) {
serviceUriCompleter.complete(uri);
}
}
});
final serviceUri = await serviceUriCompleter.future;
await collect_coverage.main([
'--wait-paused',
'--resume-isolates',
'--uri=$serviceUri',
'--scope-output=${flags.packageName}',
if (flags.branchCoverage) '--branch-coverage',
if (flags.functionCoverage) '--function-coverage',
'-o',
outJson,
]);
await testProcess;
await format_coverage.main([
'--lcov',
'--check-ignore',
'--package=${flags.packageDir}',
'-i',
outJson,
'-o',
outLcov,
]);
exit(0);
}