Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions lib/src/cli/cli.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import 'dart:async';

import 'package:mason/mason.dart';
import 'package:path/path.dart' as p;
import 'package:universal_io/io.dart';
import 'package:very_good_test_runner/very_good_test_runner.dart';

part 'dart_cli.dart';
part 'flutter_cli.dart';
Expand All @@ -26,8 +30,8 @@ class _Cmd {
return result;
}

static Iterable<Future<ProcessResult>> runWhere({
required Future<ProcessResult> Function(FileSystemEntity) run,
static Iterable<Future> runWhere<T>({
required Future<T> Function(FileSystemEntity) run,
required bool Function(FileSystemEntity) where,
String cwd = '.',
}) {
Expand Down
2 changes: 1 addition & 1 deletion lib/src/cli/dart_cli.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,6 @@ class Dart {

if (processes.isEmpty) throw PubspecNotFound();

await Future.wait(processes);
await Future.wait<void>(processes);
}
}
206 changes: 163 additions & 43 deletions lib/src/cli/flutter_cli.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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,
Copy link
Member

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?

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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: maybe have a type def for this?

Copy link
Contributor Author

@felangel felangel Mar 10, 2022

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where else do you see it repeated? I only see Future<T> Function(String cwd) in one spot.

Copy link
Member

Choose a reason for hiding this comment

The 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) {
await process;
}
}

if (processes.isEmpty) throw PubspecNotFound();
Future<void> _flutterTest({
String cwd = '.',
required void Function(String) stdout,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: reuse the typedef here

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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).

Copy link
Member

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 ArgumentCallback means above and, from a readability standpoint, I personally prefer seeing the type inline when it's simple like this especially if it's used in a private method.

Let me know what you think 👍

Copy link
Member

Choose a reason for hiding this comment

The 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 {
Copy link
Member

Choose a reason for hiding this comment

The 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')! : '';
}
}
3 changes: 2 additions & 1 deletion lib/src/commands/test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ class TestCommand extends Command<int> {
await Flutter.test(
cwd: targetPath,
recursive: recursive,
progress: _logger.progress,
stdout: _logger.write,
stderr: _logger.err,
);
} on PubspecNotFound catch (_) {
_logger.err('Could not find a pubspec.yaml in $targetPath');
Expand Down
2 changes: 2 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ environment:
dependencies:
args: ^2.1.0
mason: ">=0.1.0-dev.9 <0.1.0-dev.10"
mason_logger: ^0.1.0-dev.6
meta: ^1.3.0
path: ^1.8.0
pub_updater: ^0.2.1
universal_io: ^2.0.4
usage: ^4.0.2
very_good_analysis: ^2.4.0
very_good_test_runner: ^0.1.1

dev_dependencies:
build_runner: ^2.0.0
Expand Down
Loading