blob: 8fdbff2d65436ec6ec5b4b3ebbee2da1feb26dcb [file] [log] [blame]
// ignore_for_file: public_member_api_docs, sort_constructors_first
// Copyright (c) 2023, 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:io';
import 'package:path/path.dart' as path;
import '../github.dart';
import '../repo.dart';
import '../utils.dart';
import 'lcov.dart';
class Coverage {
final bool coverageWeb;
Coverage(this.coverageWeb);
Future<CoverageResult> compareCoverages(Github github) async {
var files = await github.listFilesForPR();
var basePath = '../base_repo/';
return compareCoveragesFor(files, basePath);
}
CoverageResult compareCoveragesFor(List<GitFile> files, String basePath) {
var repository = Repository();
var packages = repository.locatePackages();
print('Found packages $packages at ${Directory.current}');
var filesOfInterest = files
.where((file) => path.extension(file.filename) == '.dart')
.where((file) => file.status != FileStatus.removed)
.where((file) => isInSomePackage(packages, file.relativePath))
.where((file) => isNotATest(packages, file.relativePath))
.toList();
print('The files of interest are $filesOfInterest');
var base = Directory(basePath);
var baseRepository = Repository(base);
var basePackages = baseRepository.locatePackages();
print('Found packages $basePackages at $base');
var changedPackages = packages
.where((package) =>
filesOfInterest.any((file) => file.isInPackage(package)))
.toList();
print('The packages of interest are $changedPackages');
var coverageResult = CoverageResult({});
for (var package in changedPackages) {
final newCoverages = getCoverage(package);
final basePackage = basePackages
.where((element) => element.name == package.name)
.firstOrNull;
final oldCoverages = getCoverage(basePackage);
for (var file in filesOfInterest.map((file) => file.relativePath)) {
var oldCoverage = oldCoverages[file];
var newCoverage = newCoverages[file];
print('Compage coverage for $file: $oldCoverage vs $newCoverage');
coverageResult[file] = Change(
oldCoverage: oldCoverage,
newCoverage: newCoverage,
);
}
}
return coverageResult;
}
bool isNotATest(List<Package> packages, String file) {
return packages.every((package) =>
!path.isWithin(path.join(package.directory.path, 'test'), file));
}
bool isInSomePackage(List<Package> packages, String file) {
return packages
.any((package) => path.isWithin(package.directory.path, file));
}
Map<String, double> getCoverage(Package? package) {
if (package != null) {
var hasTests =
Directory(path.join(package.directory.path, 'test')).existsSync();
if (hasTests) {
print('''
Get coverage for ${package.name} by running coverage in ${package.directory.path}''');
Process.runSync(
'dart',
['pub', 'get'],
workingDirectory: package.directory.path,
);
if (coverageWeb) {
print('Get test coverage for web');
var resultChrome = Process.runSync(
'dart',
['test', '-p', 'chrome', '--coverage=coverage'],
workingDirectory: package.directory.path,
);
print(resultChrome.stdout);
print(resultChrome.stderr);
}
print('Get test coverage for vm');
var resultVm = Process.runSync(
'dart',
['test', '--coverage=coverage'],
workingDirectory: package.directory.path,
);
print(resultVm.stdout);
print(resultVm.stderr);
var resultLcov = Process.runSync(
'dart',
[
'pub',
'global',
'run',
'coverage:format_coverage',
'--lcov',
'--check-ignore',
'--report-on lib/',
'-i',
'coverage/',
'-o',
'coverage/lcov.info'
],
workingDirectory: package.directory.path,
);
print(resultLcov.stdout);
print(resultLcov.stderr);
return parseLCOV(
path.join(package.directory.path, 'coverage/lcov.info'),
relativeTo: package.repository.baseDirectory.path,
);
}
}
return {};
}
}
class CoverageResult {
final Map<String, Change> coveragePerFile;
CoverageResult(this.coveragePerFile);
CoverageResult operator +(CoverageResult other) {
return CoverageResult({...coveragePerFile, ...other.coveragePerFile});
}
Change? operator [](String s) => coveragePerFile[s];
void operator []=(String s, Change d) => coveragePerFile[s] = d;
}
class Change {
final double? newCoverage;
final double? oldCoverage;
Change({this.newCoverage, this.oldCoverage});
double? get relativeChange => oldCoverage != null
? ((newCoverage ?? 0) - oldCoverage!) / oldCoverage!.abs()
: null;
double? get absoluteCoverage => newCoverage;
Severity get severity => _severityWithMessage().$1;
bool get existsNow => newCoverage != null;
bool get existedBefore => oldCoverage != null;
String toMarkdown() => _severityWithMessage().$2;
(Severity, String) _severityWithMessage() {
if (existedBefore || existsNow) {
String format(double? value) =>
'${((value ?? 0) * 100).abs().toStringAsFixed(0)} %';
var totalString = format(absoluteCoverage);
if (existedBefore && relativeChange != 0) {
var relativeString = '''
${relativeChange! >= 0 ? ':arrow_up:' : ':arrow_down:'} ${format(relativeChange)}''';
if (relativeChange! > 0) {
return (
Severity.success,
':green_heart: $totalString $relativeString',
);
} else {
return (
Severity.warning,
':broken_heart: $totalString $relativeString'
);
}
} else {
if (absoluteCoverage! > 0) {
return (Severity.success, ':green_heart: $totalString');
}
}
}
return (Severity.warning, ':broken_heart: Not covered');
}
@override
bool operator ==(covariant Change other) {
if (identical(this, other)) return true;
return other.newCoverage == newCoverage && other.oldCoverage == oldCoverage;
}
@override
int get hashCode => newCoverage.hashCode ^ oldCoverage.hashCode;
@override
String toString() =>
'Change(newCoverage: $newCoverage, oldCoverage: $oldCoverage)';
}