@@ -28,14 +28,11 @@ class Flutter {
28
28
'Running "flutter packages get" in $cwd ' ,
29
29
);
30
30
try {
31
- final result = await _Cmd .run (
31
+ await _Cmd .run (
32
32
'flutter' ,
33
33
['packages' , 'get' ],
34
34
workingDirectory: cwd,
35
35
);
36
- return result;
37
- } catch (_) {
38
- rethrow ;
39
36
} finally {
40
37
installDone? .call ();
41
38
}
@@ -65,55 +62,178 @@ class Flutter {
65
62
static Future <void > test ({
66
63
String cwd = '.' ,
67
64
bool recursive = false ,
68
- void Function ([String ? ]) Function (String message)? progress,
69
- }) async {
70
- await _runCommand (
71
- cmd: (cwd) async {
72
- final installDone = progress? .call (
73
- 'Running "flutter test" in $cwd ' ,
65
+ void Function (String )? stdout,
66
+ void Function (String )? stderr,
67
+ }) {
68
+ return _runCommand (
69
+ cmd: (cwd) {
70
+ void noop (String ? _) {}
71
+ stdout? .call ('Running "flutter test" in $cwd ...\n ' );
72
+ return _flutterTest (
73
+ cwd: cwd,
74
+ stdout: stdout ?? noop,
75
+ stderr: stderr ?? noop,
74
76
);
75
- try {
76
- final result = await _Cmd .run (
77
- 'flutter' ,
78
- ['test' ],
79
- workingDirectory: cwd,
80
- );
81
- return result;
82
- } catch (_) {
83
- rethrow ;
84
- } finally {
85
- installDone? .call ();
86
- }
87
77
},
88
78
cwd: cwd,
89
79
recursive: recursive,
90
80
);
91
81
}
82
+ }
83
+
84
+ /// Run a command on directories with a `pubspec.yaml` .
85
+ Future <void > _runCommand <T >({
86
+ required Future <T > Function (String cwd) cmd,
87
+ required String cwd,
88
+ required bool recursive,
89
+ }) async {
90
+ if (! recursive) {
91
+ final pubspec = File (p.join (cwd, 'pubspec.yaml' ));
92
+ if (! pubspec.existsSync ()) throw PubspecNotFound ();
93
+
94
+ await cmd (cwd);
95
+ return ;
96
+ }
92
97
93
- /// Run a command on directories with a `pubspec.yaml` .
94
- static Future <void > _runCommand ({
95
- required Future <ProcessResult > Function (String cwd) cmd,
96
- required String cwd,
97
- required bool recursive,
98
- }) async {
99
- if (! recursive) {
100
- final pubspec = File (p.join (cwd, 'pubspec.yaml' ));
101
- if (! pubspec.existsSync ()) throw PubspecNotFound ();
98
+ final processes = _Cmd .runWhere (
99
+ run: (entity) => cmd (entity.parent.path),
100
+ where: _isPubspec,
101
+ cwd: cwd,
102
+ );
102
103
103
- await cmd (cwd);
104
- return ;
105
- }
104
+ if (processes.isEmpty) throw PubspecNotFound ();
106
105
107
- final processes = _Cmd .runWhere (
108
- run: (entity) => cmd (entity.parent.path),
109
- where: _isPubspec,
110
- cwd: cwd,
111
- );
106
+ for (final process in processes) {
107
+ await process;
108
+ }
109
+ }
112
110
113
- if (processes.isEmpty) throw PubspecNotFound ();
111
+ Future <void > _flutterTest ({
112
+ String cwd = '.' ,
113
+ required void Function (String ) stdout,
114
+ required void Function (String ) stderr,
115
+ }) {
116
+ const clearLine = '\u 001B[2K\r ' ;
114
117
115
- for (final process in processes) {
116
- await process;
117
- }
118
+ final completer = Completer <void >();
119
+ final suites = < int , TestSuite > {};
120
+ final groups = < int , TestGroup > {};
121
+ final tests = < int , Test > {};
122
+
123
+ var successCount = 0 ;
124
+ var skipCount = 0 ;
125
+ var failureCount = 0 ;
126
+
127
+ String computeStats () {
128
+ final passingTests = successCount.formatSuccess ();
129
+ final failingTests = failureCount.formatFailure ();
130
+ final skippedTests = skipCount.formatSkipped ();
131
+ final result = [passingTests, failingTests, skippedTests]
132
+ ..removeWhere ((element) => element.isEmpty);
133
+ return result.join (' ' );
134
+ }
135
+
136
+ final timerSubscription =
137
+ Stream .periodic (const Duration (seconds: 1 ), (_) => _).listen (
138
+ (tick) {
139
+ if (completer.isCompleted) return ;
140
+ final timeElapsed = Duration (seconds: tick).formatted ();
141
+ stdout ('$clearLine $timeElapsed ...' );
142
+ },
143
+ );
144
+
145
+ flutterTest (workingDirectory: cwd).listen (
146
+ (event) {
147
+ if (event.shouldCancelTimer ()) timerSubscription.cancel ();
148
+ if (event is SuiteTestEvent ) suites[event.suite.id] = event.suite;
149
+ if (event is GroupTestEvent ) groups[event.group.id] = event.group;
150
+ if (event is TestStartEvent ) tests[event.test.id] = event.test;
151
+
152
+ if (event is MessageTestEvent ) {
153
+ if (event.message.startsWith ('Skip:' )) {
154
+ stdout ('$clearLine ${lightYellow .wrap (event .message )}\n ' );
155
+ } else if (event.message.contains ('EXCEPTION' )) {
156
+ stderr ('$clearLine ${event .message }' );
157
+ } else {
158
+ stdout ('$clearLine ${event .message }\n ' );
159
+ }
160
+ }
161
+
162
+ if (event is ErrorTestEvent ) {
163
+ stderr (event.error);
164
+ if (event.stackTrace.trim ().isNotEmpty) stderr (event.stackTrace);
165
+ }
166
+
167
+ if (event is TestDoneEvent ) {
168
+ if (event.hidden) return ;
169
+
170
+ final test = tests[event.testID]! ;
171
+ final suite = suites[test.suiteID]! ;
172
+
173
+ if (event.skipped) {
174
+ stdout (
175
+ '''$clearLine ${lightYellow .wrap ('${test .name } ${suite .path } (SKIPPED)' )}\n ''' ,
176
+ );
177
+ skipCount++ ;
178
+ } else if (event.result == TestResult .success) {
179
+ successCount++ ;
180
+ } else {
181
+ stderr ('$clearLine ${test .name } ${suite .path } (FAILED)' );
182
+ failureCount++ ;
183
+ }
184
+
185
+ final timeElapsed = Duration (milliseconds: event.time).formatted ();
186
+ final stats = computeStats ();
187
+ stdout ('$clearLine $timeElapsed $stats : ${test .name }' );
188
+ }
189
+
190
+ if (event is DoneTestEvent ) {
191
+ final timeElapsed = Duration (milliseconds: event.time).formatted ();
192
+ final stats = computeStats ();
193
+ final summary = event.success == true
194
+ ? lightGreen.wrap ('All tests passed!' )!
195
+ : lightRed.wrap ('Some tests failed.' )! ;
196
+
197
+ stdout ('$clearLine ${darkGray .wrap (timeElapsed )} $stats : $summary \n ' );
198
+ completer.complete ();
199
+ }
200
+ },
201
+ onError: completer.completeError,
202
+ );
203
+
204
+ return completer.future;
205
+ }
206
+
207
+ extension on TestEvent {
208
+ bool shouldCancelTimer () {
209
+ final event = this ;
210
+ if (event is MessageTestEvent ) return true ;
211
+ if (event is ErrorTestEvent ) return true ;
212
+ if (event is DoneTestEvent ) return true ;
213
+ if (event is TestDoneEvent ) return ! event.hidden;
214
+ return false ;
215
+ }
216
+ }
217
+
218
+ extension on Duration {
219
+ String formatted () {
220
+ String twoDigits (int n) => n.toString ().padLeft (2 , '0' );
221
+ final twoDigitMinutes = twoDigits (inMinutes.remainder (60 ));
222
+ final twoDigitSeconds = twoDigits (inSeconds.remainder (60 ));
223
+ return darkGray.wrap ('$twoDigitMinutes :$twoDigitSeconds ' )! ;
224
+ }
225
+ }
226
+
227
+ extension on int {
228
+ String formatSuccess () {
229
+ return this > 0 ? lightGreen.wrap ('+$this ' )! : '' ;
230
+ }
231
+
232
+ String formatFailure () {
233
+ return this > 0 ? lightRed.wrap ('-$this ' )! : '' ;
234
+ }
235
+
236
+ String formatSkipped () {
237
+ return this > 0 ? lightYellow.wrap ('~$this ' )! : '' ;
118
238
}
119
239
}
0 commit comments