-
Notifications
You must be signed in to change notification settings - Fork 236
feat: use very_good_test_runner to improve test output #308
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
61f28fc
298410e
a294760
159cfda
fb55ee0
8a44612
1933181
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,14 +28,11 @@ class Flutter { | |
| 'Running "flutter packages get" in $cwd', | ||
| ); | ||
| try { | ||
| final result = await _Cmd.run( | ||
| await _Cmd.run( | ||
| 'flutter', | ||
| ['packages', 'get'], | ||
| workingDirectory: cwd, | ||
| ); | ||
| return result; | ||
| } catch (_) { | ||
| rethrow; | ||
| } finally { | ||
| installDone?.call(); | ||
| } | ||
|
|
@@ -65,55 +62,178 @@ class Flutter { | |
| static Future<void> test({ | ||
| String cwd = '.', | ||
| bool recursive = false, | ||
| void Function([String?]) Function(String message)? progress, | ||
| }) async { | ||
| await _runCommand( | ||
| cmd: (cwd) async { | ||
| final installDone = progress?.call( | ||
| 'Running "flutter test" in $cwd', | ||
| void Function(String)? stdout, | ||
| void Function(String)? stderr, | ||
| }) { | ||
| return _runCommand( | ||
| cmd: (cwd) { | ||
| void noop(String? _) {} | ||
| stdout?.call('Running "flutter test" in $cwd...\n'); | ||
| return _flutterTest( | ||
| cwd: cwd, | ||
| stdout: stdout ?? noop, | ||
| stderr: stderr ?? noop, | ||
| ); | ||
| try { | ||
| final result = await _Cmd.run( | ||
| 'flutter', | ||
| ['test'], | ||
| workingDirectory: cwd, | ||
| ); | ||
| return result; | ||
| } catch (_) { | ||
| rethrow; | ||
| } finally { | ||
| installDone?.call(); | ||
| } | ||
| }, | ||
| cwd: cwd, | ||
| recursive: recursive, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /// Run a command on directories with a `pubspec.yaml`. | ||
| Future<void> _runCommand<T>({ | ||
| required Future<T> Function(String cwd) cmd, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: maybe have a type def for this?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do you feel it's necessary? This is a private API and it's only used in this one spot. By providing a typedef developers would have to jump to the typedef to understand the function signature.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just for the sake of DRY, I've noticed this is repeated along the file. Either way this is just a nit, I am fine either way
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where else do you see it repeated? I only see
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, this isn't repeated, I guess I got into the flow because of the other comments |
||
| required String cwd, | ||
| required bool recursive, | ||
| }) async { | ||
| if (!recursive) { | ||
| final pubspec = File(p.join(cwd, 'pubspec.yaml')); | ||
| if (!pubspec.existsSync()) throw PubspecNotFound(); | ||
|
|
||
| await cmd(cwd); | ||
| return; | ||
| } | ||
|
|
||
| /// Run a command on directories with a `pubspec.yaml`. | ||
| static Future<void> _runCommand({ | ||
| required Future<ProcessResult> Function(String cwd) cmd, | ||
| required String cwd, | ||
| required bool recursive, | ||
| }) async { | ||
| if (!recursive) { | ||
| final pubspec = File(p.join(cwd, 'pubspec.yaml')); | ||
| if (!pubspec.existsSync()) throw PubspecNotFound(); | ||
| final processes = _Cmd.runWhere( | ||
| run: (entity) => cmd(entity.parent.path), | ||
| where: _isPubspec, | ||
| cwd: cwd, | ||
| ); | ||
|
|
||
| await cmd(cwd); | ||
| return; | ||
| } | ||
| if (processes.isEmpty) throw PubspecNotFound(); | ||
|
|
||
| final processes = _Cmd.runWhere( | ||
| run: (entity) => cmd(entity.parent.path), | ||
| where: _isPubspec, | ||
| cwd: cwd, | ||
| ); | ||
| for (final process in processes) { | ||
erickzanardo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| await process; | ||
| } | ||
| } | ||
|
|
||
| if (processes.isEmpty) throw PubspecNotFound(); | ||
| Future<void> _flutterTest({ | ||
| String cwd = '.', | ||
| required void Function(String) stdout, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: reuse the typedef here
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same question here about whether you feel this is necessary? I'd prefer to leave it inline unless it's a public API (related to https://dart-lang.github.io/linter/lints/avoid_private_typedef_functions.html).
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting lint rule, never thought about it that way, makes sense! Let's keep online, we can ignore all the similar comments
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The only reason I slightly prefer to leave it as is is because let's say we create a typedef like: typedef ArgumentCallback<T> = void Function(T argument);Then the new signature will look like: Future<void> _flutterTest({
String cwd = '.',
required ArgumentCallback<String>? stdout,
required ArgumentCallback<String>? stderr,
}) {...}I don't really see the benefit of the typedef in this case because as a developer, I have to look at what Let me know what you think 👍
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it, I think it makes sense, I kind find the inline function type a little bit confusing on dart, hence why I prefer to use typedef, and as a bonus we DRY. But I agree with you, being a private method, we have have freedom to leave like this, and it is easier to spot the require method contract. Lets keep like that 👍 |
||
| required void Function(String) stderr, | ||
| }) { | ||
| const clearLine = '\u001B[2K\r'; | ||
|
|
||
| for (final process in processes) { | ||
| await process; | ||
| } | ||
| final completer = Completer<void>(); | ||
| final suites = <int, TestSuite>{}; | ||
| final groups = <int, TestGroup>{}; | ||
| final tests = <int, Test>{}; | ||
|
|
||
| var successCount = 0; | ||
| var skipCount = 0; | ||
| var failureCount = 0; | ||
|
|
||
| String computeStats() { | ||
| final passingTests = successCount.formatSuccess(); | ||
| final failingTests = failureCount.formatFailure(); | ||
| final skippedTests = skipCount.formatSkipped(); | ||
| final result = [passingTests, failingTests, skippedTests] | ||
| ..removeWhere((element) => element.isEmpty); | ||
| return result.join(' '); | ||
| } | ||
|
|
||
| final timerSubscription = | ||
| Stream.periodic(const Duration(seconds: 1), (_) => _).listen( | ||
| (tick) { | ||
| if (completer.isCompleted) return; | ||
| final timeElapsed = Duration(seconds: tick).formatted(); | ||
| stdout('$clearLine$timeElapsed ...'); | ||
| }, | ||
| ); | ||
|
|
||
| flutterTest(workingDirectory: cwd).listen( | ||
| (event) { | ||
| if (event.shouldCancelTimer()) timerSubscription.cancel(); | ||
| if (event is SuiteTestEvent) suites[event.suite.id] = event.suite; | ||
| if (event is GroupTestEvent) groups[event.group.id] = event.group; | ||
| if (event is TestStartEvent) tests[event.test.id] = event.test; | ||
|
|
||
| if (event is MessageTestEvent) { | ||
| if (event.message.startsWith('Skip:')) { | ||
| stdout('$clearLine${lightYellow.wrap(event.message)}\n'); | ||
| } else if (event.message.contains('EXCEPTION')) { | ||
| stderr('$clearLine${event.message}'); | ||
| } else { | ||
| stdout('$clearLine${event.message}\n'); | ||
| } | ||
| } | ||
|
|
||
| if (event is ErrorTestEvent) { | ||
| stderr(event.error); | ||
| if (event.stackTrace.trim().isNotEmpty) stderr(event.stackTrace); | ||
| } | ||
|
|
||
| if (event is TestDoneEvent) { | ||
| if (event.hidden) return; | ||
|
|
||
| final test = tests[event.testID]!; | ||
| final suite = suites[test.suiteID]!; | ||
|
|
||
| if (event.skipped) { | ||
| stdout( | ||
| '''$clearLine${lightYellow.wrap('${test.name} ${suite.path} (SKIPPED)')}\n''', | ||
| ); | ||
| skipCount++; | ||
| } else if (event.result == TestResult.success) { | ||
| successCount++; | ||
| } else { | ||
| stderr('$clearLine${test.name} ${suite.path} (FAILED)'); | ||
| failureCount++; | ||
| } | ||
|
|
||
| final timeElapsed = Duration(milliseconds: event.time).formatted(); | ||
| final stats = computeStats(); | ||
| stdout('$clearLine$timeElapsed $stats: ${test.name}'); | ||
| } | ||
|
|
||
| if (event is DoneTestEvent) { | ||
| final timeElapsed = Duration(milliseconds: event.time).formatted(); | ||
| final stats = computeStats(); | ||
| final summary = event.success == true | ||
| ? lightGreen.wrap('All tests passed!')! | ||
| : lightRed.wrap('Some tests failed.')!; | ||
|
|
||
| stdout('$clearLine${darkGray.wrap(timeElapsed)} $stats: $summary\n'); | ||
| completer.complete(); | ||
| } | ||
| }, | ||
| onError: completer.completeError, | ||
| ); | ||
|
|
||
| return completer.future; | ||
| } | ||
|
|
||
| extension on TestEvent { | ||
| bool shouldCancelTimer() { | ||
| final event = this; | ||
| if (event is MessageTestEvent) return true; | ||
| if (event is ErrorTestEvent) return true; | ||
| if (event is DoneTestEvent) return true; | ||
| if (event is TestDoneEvent) return !event.hidden; | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| extension on Duration { | ||
| String formatted() { | ||
| String twoDigits(int n) => n.toString().padLeft(2, '0'); | ||
| final twoDigitMinutes = twoDigits(inMinutes.remainder(60)); | ||
| final twoDigitSeconds = twoDigits(inSeconds.remainder(60)); | ||
| return darkGray.wrap('$twoDigitMinutes:$twoDigitSeconds')!; | ||
| } | ||
| } | ||
|
|
||
| extension on int { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👌 |
||
| String formatSuccess() { | ||
| return this > 0 ? lightGreen.wrap('+$this')! : ''; | ||
| } | ||
|
|
||
| String formatFailure() { | ||
| return this > 0 ? lightRed.wrap('-$this')! : ''; | ||
| } | ||
|
|
||
| String formatSkipped() { | ||
| return this > 0 ? lightYellow.wrap('~$this')! : ''; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Can we create a typedef for this?