Skip to content

Commit c6e4ae4

Browse files
authored
feat: use very_good_test_runner to improve test output (#308)
1 parent 4865a91 commit c6e4ae4

File tree

7 files changed

+453
-55
lines changed

7 files changed

+453
-55
lines changed

lib/src/cli/cli.dart

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
import 'dart:async';
2+
3+
import 'package:mason/mason.dart';
14
import 'package:path/path.dart' as p;
25
import 'package:universal_io/io.dart';
6+
import 'package:very_good_test_runner/very_good_test_runner.dart';
37

48
part 'dart_cli.dart';
59
part 'flutter_cli.dart';
@@ -26,8 +30,8 @@ class _Cmd {
2630
return result;
2731
}
2832

29-
static Iterable<Future<ProcessResult>> runWhere({
30-
required Future<ProcessResult> Function(FileSystemEntity) run,
33+
static Iterable<Future> runWhere<T>({
34+
required Future<T> Function(FileSystemEntity) run,
3135
required bool Function(FileSystemEntity) where,
3236
String cwd = '.',
3337
}) {

lib/src/cli/dart_cli.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,6 @@ class Dart {
3737

3838
if (processes.isEmpty) throw PubspecNotFound();
3939

40-
await Future.wait(processes);
40+
await Future.wait<void>(processes);
4141
}
4242
}

lib/src/cli/flutter_cli.dart

+163-43
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,11 @@ class Flutter {
2828
'Running "flutter packages get" in $cwd',
2929
);
3030
try {
31-
final result = await _Cmd.run(
31+
await _Cmd.run(
3232
'flutter',
3333
['packages', 'get'],
3434
workingDirectory: cwd,
3535
);
36-
return result;
37-
} catch (_) {
38-
rethrow;
3936
} finally {
4037
installDone?.call();
4138
}
@@ -65,55 +62,178 @@ class Flutter {
6562
static Future<void> test({
6663
String cwd = '.',
6764
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,
7476
);
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-
}
8777
},
8878
cwd: cwd,
8979
recursive: recursive,
9080
);
9181
}
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+
}
9297

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+
);
102103

103-
await cmd(cwd);
104-
return;
105-
}
104+
if (processes.isEmpty) throw PubspecNotFound();
106105

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+
}
112110

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 = '\u001B[2K\r';
114117

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')! : '';
118238
}
119239
}

lib/src/commands/test.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ class TestCommand extends Command<int> {
4949
await Flutter.test(
5050
cwd: targetPath,
5151
recursive: recursive,
52-
progress: _logger.progress,
52+
stdout: _logger.write,
53+
stderr: _logger.err,
5354
);
5455
} on PubspecNotFound catch (_) {
5556
_logger.err('Could not find a pubspec.yaml in $targetPath');

pubspec.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ environment:
99
dependencies:
1010
args: ^2.1.0
1111
mason: ">=0.1.0-dev.9 <0.1.0-dev.10"
12+
mason_logger: ^0.1.0-dev.6
1213
meta: ^1.3.0
1314
path: ^1.8.0
1415
pub_updater: ^0.2.1
1516
universal_io: ^2.0.4
1617
usage: ^4.0.2
1718
very_good_analysis: ^2.4.0
19+
very_good_test_runner: ^0.1.1
1820

1921
dev_dependencies:
2022
build_runner: ^2.0.0

0 commit comments

Comments
 (0)