Skip to content

Commit 5a936d6

Browse files
committed
feat: create VeryGoodCommandRunner (#4)
1 parent d349389 commit 5a936d6

File tree

8 files changed

+262
-7
lines changed

8 files changed

+262
-7
lines changed

.github/workflows/main.yaml

+10-4
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,24 @@ jobs:
66
build:
77
runs-on: ubuntu-latest
88
container:
9-
image: google/dart:2.9.3
9+
image: google/dart:2.10.0
1010
steps:
1111
- uses: actions/checkout@v2
1212

1313
- name: Install Dependencies
1414
run: pub get
1515

1616
- name: Format
17-
run: dartfmt --dry-run --set-exit-if-changed .
17+
run: dart format --set-exit-if-changed .
1818

1919
- name: Analyze
20-
run: dartanalyzer --fatal-infos --fatal-warnings .
20+
run: dart analyze --fatal-infos --fatal-warnings .
2121

22-
- name: Ensure Build
22+
- name: Verify Build
2323
run: pub run test --run-skipped -t pull-request-only
24+
25+
- name: Run Tests
26+
run: dart test -x pull-request-only --coverage=coverage && pub run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --packages=.packages --report-on=lib
27+
28+
- name: Check Code Coverage
29+
uses: VeryGoodOpenSource/[email protected]

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ doc/api/
1111

1212
# Temporary Files
1313
.tmp/
14+
15+
# Files generated during tests
16+
.test_coverage.dart
17+
coverage/

README.md

+22-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
Developed with 💙 by [Very Good Ventures](very_good_ventures_link) 🦄
66

77
[![ci][ci_badge]][ci_link]
8+
[![coverage][coverage_badge]][ci_link]
89
[![License: MIT][license_badge]][license_link]
910
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
1011

@@ -14,14 +15,34 @@ A Very Good Command Line Interface for Dart.
1415

1516
## Commands
1617

17-
### `very_good create`
18+
### `$ very_good create`
1819

1920
Create a new very good flutter application in seconds.
2021

2122
![Very Good CLI][very_good_cli]
2223

24+
### `$ very_good --help`
25+
26+
See the complete list of commands and usage information.
27+
28+
```sh
29+
🦄 A Very Good Commandline Interface
30+
31+
Usage: very_good <command> [arguments]
32+
33+
Global options:
34+
-h, --help Print this usage information.
35+
--version Print the current version.
36+
37+
Available commands:
38+
help Display help information for very_good.
39+
40+
Run "very_good help <command>" for more information about a command.
41+
```
42+
2343
[ci_badge]: https://github.com/VeryGoodOpenSource/very_good_cli/workflows/ci/badge.svg
2444
[ci_link]: https://github.com/VeryGoodOpenSource/very_good_cli/actions
45+
[coverage_badge]: coverage_badge.svg
2546
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
2647
[license_link]: https://opensource.org/licenses/MIT
2748
[logo]: docs/assets/vgv_logo.png

bin/very_good.dart

+17-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,17 @@
1-
void main() {}
1+
import 'dart:io';
2+
import 'package:very_good_cli/src/command_runner.dart';
3+
4+
void main(List<String> args) async {
5+
await _flushThenExit(await VeryGoodCommandRunner().run(args));
6+
}
7+
8+
/// Flushes the stdout and stderr streams, then exits the program with the given
9+
/// status code.
10+
///
11+
/// This returns a Future that will never complete, since the program will have
12+
/// exited already. This is useful to prevent Future chains from proceeding
13+
/// after you've decided to exit.
14+
Future _flushThenExit(int status) {
15+
return Future.wait<void>([stdout.close(), stderr.close()])
16+
.then<void>((_) => exit(status));
17+
}

coverage_badge.svg

+20
Loading

lib/src/command_runner.dart

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import 'package:args/args.dart';
2+
import 'package:args/command_runner.dart';
3+
import 'package:io/io.dart';
4+
import 'package:mason/mason.dart';
5+
6+
import 'version.dart';
7+
8+
/// {@template very_good_command_runner}
9+
/// A [CommandRunner] for the Very Good CLI.
10+
/// {@endtemplate}
11+
class VeryGoodCommandRunner extends CommandRunner<int> {
12+
/// {@macro very_good_command_runner}
13+
VeryGoodCommandRunner({Logger logger})
14+
: _logger = logger ?? Logger(),
15+
super('very_good', '🦄 A Very Good Commandline Interface') {
16+
argParser.addFlag(
17+
'version',
18+
negatable: false,
19+
help: 'Print the current version.',
20+
);
21+
}
22+
23+
final Logger _logger;
24+
25+
@override
26+
Future<int> run(Iterable<String> args) async {
27+
try {
28+
final _argResults = parse(args);
29+
return await runCommand(_argResults) ?? ExitCode.success.code;
30+
} on FormatException catch (e) {
31+
_logger
32+
..err(e.message)
33+
..info('')
34+
..info(usage);
35+
return ExitCode.usage.code;
36+
} on UsageException catch (e) {
37+
_logger
38+
..err(e.message)
39+
..info('')
40+
..info(usage);
41+
return ExitCode.usage.code;
42+
}
43+
}
44+
45+
@override
46+
Future<int> runCommand(ArgResults topLevelResults) async {
47+
if (topLevelResults['version'] == true) {
48+
_logger.info('very_good version: $packageVersion');
49+
return ExitCode.success.code;
50+
}
51+
return super.runCommand(topLevelResults);
52+
}
53+
}

pubspec.yaml

+8-1
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,19 @@ version: 0.0.1-dev.0
44
homepage: https://github.com/VeryGoodOpenSource/very_good_cli
55

66
environment:
7-
sdk: ">=2.8.1 <3.0.0"
7+
sdk: ">=2.10.0 <3.0.0"
8+
9+
dependencies:
10+
args: ^1.6.0
11+
io: ^0.3.4
12+
mason: ^0.0.1-dev.22
813

914
dev_dependencies:
15+
coverage: ^0.13.4
1016
build_runner: ^1.10.0
1117
build_verify: ^1.1.1
1218
build_version: ^2.0.1
19+
mockito: ^4.0.0
1320
test: ^1.14.3
1421
very_good_analysis: ^1.0.4
1522

test/src/command_runner_test.dart

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// ignore_for_file: no_adjacent_strings_in_list
2+
import 'dart:async';
3+
4+
import 'package:args/command_runner.dart';
5+
import 'package:io/io.dart';
6+
import 'package:mason/mason.dart';
7+
import 'package:mockito/mockito.dart';
8+
import 'package:test/test.dart';
9+
import 'package:very_good_cli/src/command_runner.dart';
10+
import 'package:very_good_cli/src/version.dart';
11+
12+
class MockLogger extends Mock implements Logger {}
13+
14+
void main() {
15+
group('VeryGoodCommandRunner', () {
16+
List<String> printLogs;
17+
Logger logger;
18+
VeryGoodCommandRunner commandRunner;
19+
20+
void Function() overridePrint(void Function() fn) {
21+
return () {
22+
final spec = ZoneSpecification(print: (_, __, ___, String msg) {
23+
printLogs.add(msg);
24+
});
25+
return Zone.current.fork(specification: spec).run<void>(fn);
26+
};
27+
}
28+
29+
setUp(() {
30+
printLogs = [];
31+
logger = MockLogger();
32+
commandRunner = VeryGoodCommandRunner(logger: logger);
33+
});
34+
35+
test('can be instantiated without an explicit logger instance', () {
36+
final commandRunner = VeryGoodCommandRunner();
37+
expect(commandRunner, isNotNull);
38+
});
39+
40+
group('run', () {
41+
test('handles FormatException', () async {
42+
const exception = FormatException('oops!');
43+
var isFirstInvocation = true;
44+
when(logger.info(any)).thenAnswer((_) {
45+
if (isFirstInvocation) {
46+
isFirstInvocation = false;
47+
throw exception;
48+
}
49+
});
50+
final result = await commandRunner.run(['--version']);
51+
expect(result, equals(ExitCode.usage.code));
52+
verify(logger.err(exception.message)).called(1);
53+
verify(logger.info(commandRunner.usage)).called(1);
54+
});
55+
56+
test('handles UsageException', () async {
57+
final exception = UsageException('oops!', commandRunner.usage);
58+
var isFirstInvocation = true;
59+
when(logger.info(any)).thenAnswer((_) {
60+
if (isFirstInvocation) {
61+
isFirstInvocation = false;
62+
throw exception;
63+
}
64+
});
65+
final result = await commandRunner.run(['--version']);
66+
expect(result, equals(ExitCode.usage.code));
67+
verify(logger.err(exception.message)).called(1);
68+
verify(logger.info(commandRunner.usage)).called(1);
69+
});
70+
71+
test('handles no command', overridePrint(() async {
72+
const expectedPrintLogs = [
73+
'🦄 A Very Good Commandline Interface\n'
74+
'\n'
75+
'Usage: very_good <command> [arguments]\n'
76+
'\n'
77+
'Global options:\n'
78+
'-h, --help Print this usage information.\n'
79+
' --version Print the current version.\n'
80+
'\n'
81+
'Available commands:\n'
82+
' help Display help information for very_good.\n'
83+
'\n'
84+
'''Run "very_good help <command>" for more information about a command.'''
85+
];
86+
final result = await commandRunner.run([]);
87+
expect(printLogs, equals(expectedPrintLogs));
88+
expect(result, equals(ExitCode.success.code));
89+
}));
90+
91+
group('--help', () {
92+
test('outputs usage', overridePrint(() async {
93+
const expectedPrintLogs = [
94+
'🦄 A Very Good Commandline Interface\n'
95+
'\n'
96+
'Usage: very_good <command> [arguments]\n'
97+
'\n'
98+
'Global options:\n'
99+
'-h, --help Print this usage information.\n'
100+
' --version Print the current version.\n'
101+
'\n'
102+
'Available commands:\n'
103+
' help Display help information for very_good.\n'
104+
'\n'
105+
'''Run "very_good help <command>" for more information about a command.'''
106+
];
107+
final result = await commandRunner.run(['--help']);
108+
expect(printLogs, equals(expectedPrintLogs));
109+
expect(result, equals(ExitCode.success.code));
110+
111+
printLogs.clear();
112+
113+
final resultAbbr = await commandRunner.run(['-h']);
114+
expect(printLogs, equals(expectedPrintLogs));
115+
expect(resultAbbr, equals(ExitCode.success.code));
116+
}));
117+
});
118+
119+
group('--version', () {
120+
test('outputs current version', () async {
121+
final result = await commandRunner.run(['--version']);
122+
expect(result, equals(ExitCode.success.code));
123+
verify(logger.info('very_good version: $packageVersion'));
124+
});
125+
});
126+
});
127+
});
128+
}

0 commit comments

Comments
 (0)