-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Expand file tree
/
Copy pathcoverage_check_command.dart
More file actions
151 lines (126 loc) · 4.79 KB
/
Copy pathcoverage_check_command.dart
File metadata and controls
151 lines (126 loc) · 4.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:io' as io;
import 'package:file/file.dart';
import 'package:yaml/yaml.dart';
import 'common/package_looping_command.dart';
import 'common/repository_package.dart';
/// A command to run code coverage checks on changed packages.
class CoverageCheckCommand extends PackageLoopingCommand {
/// Creates a coverage check command instance.
CoverageCheckCommand(super.packagesDir, {super.processRunner, super.platform, super.gitDir});
@override
final String name = 'coverage-check';
@override
final String description =
'Checks that code coverage meets the specified minimums '
'for opted-in packages.';
@override
PackageLoopingType get packageLoopingType => PackageLoopingType.includeAllSubpackages;
final Map<String, double> _customMinimums = <String, double>{};
@override
Future<void> initializeRun() async {
final File minimumsFile = packagesDir.parent
.childDirectory('script')
.childDirectory('configs')
.childFile('custom_coverage_minimums.yaml');
if (minimumsFile.existsSync()) {
final Object? yaml = loadYaml(minimumsFile.readAsStringSync());
if (yaml is YamlMap) {
final Object? packageMap = yaml['custom_coverage_minimums'];
if (packageMap is YamlMap) {
for (final MapEntry<dynamic, dynamic> entry in packageMap.entries) {
final Object? key = entry.key;
final Object? value = entry.value;
if (key is String && value is num) {
_customMinimums[key] = value.toDouble();
}
}
}
}
}
}
@override
Future<PackageResult> runForPackage(RepositoryPackage package) async {
// Only run for first-party packages.
final List<String> pathComponents = package.directory.fileSystem.path.split(package.path);
if (pathComponents.contains('third_party')) {
return PackageResult.skip('Not a first-party package.');
}
if (!package.testDirectory.existsSync()) {
return PackageResult.skip('No test/ directory.');
}
final String packageName = package.directory.basename;
if (!_customMinimums.containsKey(packageName)) {
return PackageResult.skip('Package not opted into coverage checks.');
}
// Collect code coverage for the package.
final double? currentCoverage = await _runCoverageAndParse(package);
if (currentCoverage == null) {
return PackageResult.fail(<String>['Failed to run tests or parse coverage']);
}
final double requiredCoverage = _customMinimums[packageName]!;
// Delete generated lcov.info.
final Directory coverageDir = package.directory.childDirectory('coverage');
if (coverageDir.existsSync()) {
coverageDir.deleteSync(recursive: true);
}
if (currentCoverage < requiredCoverage) {
return PackageResult.fail(<String>[
'Code coverage for $packageName is ${currentCoverage.toStringAsFixed(1)}%, which is below the required ${requiredCoverage.toStringAsFixed(1)}%.',
]);
}
return PackageResult.success();
}
Future<double?> _runCoverageAndParse(RepositoryPackage package) async {
final io.ProcessResult result = await processRunner.run(
'flutter',
<String>[
'test',
'--coverage',
],
workingDir: package.directory,
);
if (result.exitCode != 0) {
print('Test failed for ${package.directory.basename}:\n${result.stdout}\n${result.stderr}');
return null;
}
final File lcovFile = package.directory.childDirectory('coverage').childFile('lcov.info');
if (!lcovFile.existsSync()) {
print('Coverage file not found at ${lcovFile.path}.');
return null;
}
return _calculateCoverage(lcovFile);
}
/// Calculates code coverage for non-generated code files by finding
/// the percentage of covered lines of code over the total lines of code.
double _calculateCoverage(File lcovFile) {
var linesHit = 0;
var linesFound = 0;
var skipCurrentFile = false;
final List<String> lines = lcovFile.readAsLinesSync();
for (final line in lines) {
if (line.startsWith('SF:')) {
final String fileName = line.substring(3);
skipCurrentFile = _isGeneratedFile(fileName);
}
if (!skipCurrentFile) {
if (line.startsWith('LH:')) {
linesHit += int.parse(line.substring(3));
} else if (line.startsWith('LF:')) {
linesFound += int.parse(line.substring(3));
}
}
}
if (linesFound == 0) {
return 100.0;
}
return (linesHit / linesFound) * 100.0;
}
}
bool _isGeneratedFile(String fileName) {
return fileName.endsWith('.g.dart') ||
fileName.endsWith('.pb.dart') ||
fileName.endsWith('.mocks.dart');
}