@@ -4,6 +4,49 @@ part of 'cli.dart';
44/// is executed without a `pubspec.yaml` .
55class PubspecNotFound implements Exception {}
66
7+ /// {@template coverage_not_met}
8+ /// Thrown when `flutter test ---coverage --min-coverage`
9+ /// does not meet the provided minimum coverage threshold.
10+ /// {@endtemplate}
11+ class MinCoverageNotMet implements Exception {
12+ /// {@macro coverage_not_met}
13+ const MinCoverageNotMet (this .coverage);
14+
15+ /// The measured coverage percentage (total hits / total found * 100).
16+ final double coverage;
17+ }
18+
19+ /// Thrown when `flutter test ---coverage --min-coverage value`
20+ /// does not generate the coverage file within the timeout threshold.
21+ class GenerateCoverageTimeout implements Exception {
22+ @override
23+ String toString () => 'Timed out waiting for coverage to be generated.' ;
24+ }
25+
26+ class _CoverageMetrics {
27+ const _CoverageMetrics ._({this .totalHits = 0 , this .totalFound = 0 });
28+
29+ /// Generate coverage metrics from a list of lcov records.
30+ factory _CoverageMetrics .fromLcovRecords (List <Record > records) {
31+ return records.fold <_CoverageMetrics >(
32+ const _CoverageMetrics ._(),
33+ (current, record) {
34+ final found = record.lines? .found ?? 0 ;
35+ final hit = record.lines? .hit ?? 0 ;
36+ return _CoverageMetrics ._(
37+ totalFound: current.totalFound + found,
38+ totalHits: current.totalHits + hit,
39+ );
40+ },
41+ );
42+ }
43+
44+ final int totalHits;
45+ final int totalFound;
46+
47+ double get percentage => totalFound < 1 ? 0 : (totalHits / totalFound * 100 );
48+ }
49+
750/// Flutter CLI
851class Flutter {
952 /// Determine whether flutter is installed.
@@ -62,22 +105,40 @@ class Flutter {
62105 static Future <void > test ({
63106 String cwd = '.' ,
64107 bool recursive = false ,
108+ bool collectCoverage = false ,
109+ double ? minCoverage,
65110 void Function (String )? stdout,
66111 void Function (String )? stderr,
67- }) {
68- return _runCommand (
112+ }) async {
113+ final lcovPath = p.join (cwd, 'coverage' , 'lcov.info' );
114+ final lcovFile = File (lcovPath);
115+
116+ if (collectCoverage && lcovFile.existsSync ()) {
117+ await lcovFile.delete ();
118+ }
119+
120+ await _runCommand (
69121 cmd: (cwd) {
70122 void noop (String ? _) {}
71123 stdout? .call ('Running "flutter test" in $cwd ...\n ' );
72124 return _flutterTest (
73125 cwd: cwd,
126+ collectCoverage: collectCoverage,
74127 stdout: stdout ?? noop,
75128 stderr: stderr ?? noop,
76129 );
77130 },
78131 cwd: cwd,
79132 recursive: recursive,
80133 );
134+
135+ if (collectCoverage) await lcovFile.ensureCreated ();
136+ if (minCoverage != null ) {
137+ final records = await Parser .parse (lcovPath);
138+ final coverageMetrics = _CoverageMetrics .fromLcovRecords (records);
139+ final coverage = coverageMetrics.percentage;
140+ if (coverage < minCoverage) throw MinCoverageNotMet (coverage);
141+ }
81142 }
82143}
83144
@@ -110,6 +171,7 @@ Future<void> _runCommand<T>({
110171
111172Future <void > _flutterTest ({
112173 String cwd = '.' ,
174+ bool collectCoverage = false ,
113175 required void Function (String ) stdout,
114176 required void Function (String ) stderr,
115177}) {
@@ -142,7 +204,13 @@ Future<void> _flutterTest({
142204 },
143205 );
144206
145- flutterTest (workingDirectory: cwd, runInShell: true ).listen (
207+ flutterTest (
208+ workingDirectory: cwd,
209+ arguments: [
210+ if (collectCoverage) '--coverage' ,
211+ ],
212+ runInShell: true ,
213+ ).listen (
146214 (event) {
147215 if (event.shouldCancelTimer ()) timerSubscription.cancel ();
148216 if (event is SuiteTestEvent ) suites[event.suite.id] = event.suite;
@@ -217,6 +285,20 @@ final int _lineLength = () {
217285 }
218286}();
219287
288+ extension on File {
289+ Future <void > ensureCreated ({
290+ Duration timeout = const Duration (seconds: 1 ),
291+ Duration interval = const Duration (milliseconds: 50 ),
292+ }) async {
293+ var elapsedTime = Duration .zero;
294+ while (! existsSync ()) {
295+ await Future <void >.delayed (interval);
296+ elapsedTime += interval;
297+ if (elapsedTime >= timeout) throw GenerateCoverageTimeout ();
298+ }
299+ }
300+ }
301+
220302extension on TestEvent {
221303 bool shouldCancelTimer () {
222304 final event = this ;
0 commit comments