From 5a936d69cd9ff077564d080466621f699f381eab Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Sun, 31 Jan 2021 13:45:25 -0600 Subject: [PATCH] feat: create VeryGoodCommandRunner (#4) --- .github/workflows/main.yaml | 14 +++- .gitignore | 4 + README.md | 23 +++++- bin/very_good.dart | 18 ++++- coverage_badge.svg | 20 +++++ lib/src/command_runner.dart | 53 +++++++++++++ pubspec.yaml | 9 ++- test/src/command_runner_test.dart | 128 ++++++++++++++++++++++++++++++ 8 files changed, 262 insertions(+), 7 deletions(-) create mode 100644 coverage_badge.svg create mode 100644 lib/src/command_runner.dart create mode 100644 test/src/command_runner_test.dart diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 9823f81b9..0f70dff3e 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -6,7 +6,7 @@ jobs: build: runs-on: ubuntu-latest container: - image: google/dart:2.9.3 + image: google/dart:2.10.0 steps: - uses: actions/checkout@v2 @@ -14,10 +14,16 @@ jobs: run: pub get - name: Format - run: dartfmt --dry-run --set-exit-if-changed . + run: dart format --set-exit-if-changed . - name: Analyze - run: dartanalyzer --fatal-infos --fatal-warnings . + run: dart analyze --fatal-infos --fatal-warnings . - - name: Ensure Build + - name: Verify Build run: pub run test --run-skipped -t pull-request-only + + - name: Run Tests + 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 + + - name: Check Code Coverage + uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 diff --git a/.gitignore b/.gitignore index be6343d2f..0415fcd01 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,7 @@ doc/api/ # Temporary Files .tmp/ + +# Files generated during tests +.test_coverage.dart +coverage/ \ No newline at end of file diff --git a/README.md b/README.md index 352253b84..e0d27a580 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ Developed with 💙 by [Very Good Ventures](very_good_ventures_link) 🦄 [![ci][ci_badge]][ci_link] +[![coverage][coverage_badge]][ci_link] [![License: MIT][license_badge]][license_link] [![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] @@ -14,14 +15,34 @@ A Very Good Command Line Interface for Dart. ## Commands -### `very_good create` +### `$ very_good create` Create a new very good flutter application in seconds. ![Very Good CLI][very_good_cli] +### `$ very_good --help` + +See the complete list of commands and usage information. + +```sh +🦄 A Very Good Commandline Interface + +Usage: very_good [arguments] + +Global options: +-h, --help Print this usage information. + --version Print the current version. + +Available commands: + help Display help information for very_good. + +Run "very_good help " for more information about a command. +``` + [ci_badge]: https://github.com/VeryGoodOpenSource/very_good_cli/workflows/ci/badge.svg [ci_link]: https://github.com/VeryGoodOpenSource/very_good_cli/actions +[coverage_badge]: coverage_badge.svg [license_badge]: https://img.shields.io/badge/license-MIT-blue.svg [license_link]: https://opensource.org/licenses/MIT [logo]: docs/assets/vgv_logo.png diff --git a/bin/very_good.dart b/bin/very_good.dart index ab73b3a23..f73615e4e 100644 --- a/bin/very_good.dart +++ b/bin/very_good.dart @@ -1 +1,17 @@ -void main() {} +import 'dart:io'; +import 'package:very_good_cli/src/command_runner.dart'; + +void main(List args) async { + await _flushThenExit(await VeryGoodCommandRunner().run(args)); +} + +/// Flushes the stdout and stderr streams, then exits the program with the given +/// status code. +/// +/// This returns a Future that will never complete, since the program will have +/// exited already. This is useful to prevent Future chains from proceeding +/// after you've decided to exit. +Future _flushThenExit(int status) { + return Future.wait([stdout.close(), stderr.close()]) + .then((_) => exit(status)); +} diff --git a/coverage_badge.svg b/coverage_badge.svg new file mode 100644 index 000000000..88bfadfb4 --- /dev/null +++ b/coverage_badge.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + coverage + coverage + 100% + 100% + + \ No newline at end of file diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart new file mode 100644 index 000000000..4385d7861 --- /dev/null +++ b/lib/src/command_runner.dart @@ -0,0 +1,53 @@ +import 'package:args/args.dart'; +import 'package:args/command_runner.dart'; +import 'package:io/io.dart'; +import 'package:mason/mason.dart'; + +import 'version.dart'; + +/// {@template very_good_command_runner} +/// A [CommandRunner] for the Very Good CLI. +/// {@endtemplate} +class VeryGoodCommandRunner extends CommandRunner { + /// {@macro very_good_command_runner} + VeryGoodCommandRunner({Logger logger}) + : _logger = logger ?? Logger(), + super('very_good', '🦄 A Very Good Commandline Interface') { + argParser.addFlag( + 'version', + negatable: false, + help: 'Print the current version.', + ); + } + + final Logger _logger; + + @override + Future run(Iterable args) async { + try { + final _argResults = parse(args); + return await runCommand(_argResults) ?? ExitCode.success.code; + } on FormatException catch (e) { + _logger + ..err(e.message) + ..info('') + ..info(usage); + return ExitCode.usage.code; + } on UsageException catch (e) { + _logger + ..err(e.message) + ..info('') + ..info(usage); + return ExitCode.usage.code; + } + } + + @override + Future runCommand(ArgResults topLevelResults) async { + if (topLevelResults['version'] == true) { + _logger.info('very_good version: $packageVersion'); + return ExitCode.success.code; + } + return super.runCommand(topLevelResults); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index c11c19624..d7c169c19 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,12 +4,19 @@ version: 0.0.1-dev.0 homepage: https://github.com/VeryGoodOpenSource/very_good_cli environment: - sdk: ">=2.8.1 <3.0.0" + sdk: ">=2.10.0 <3.0.0" + +dependencies: + args: ^1.6.0 + io: ^0.3.4 + mason: ^0.0.1-dev.22 dev_dependencies: + coverage: ^0.13.4 build_runner: ^1.10.0 build_verify: ^1.1.1 build_version: ^2.0.1 + mockito: ^4.0.0 test: ^1.14.3 very_good_analysis: ^1.0.4 diff --git a/test/src/command_runner_test.dart b/test/src/command_runner_test.dart new file mode 100644 index 000000000..b3c850c7e --- /dev/null +++ b/test/src/command_runner_test.dart @@ -0,0 +1,128 @@ +// ignore_for_file: no_adjacent_strings_in_list +import 'dart:async'; + +import 'package:args/command_runner.dart'; +import 'package:io/io.dart'; +import 'package:mason/mason.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; +import 'package:very_good_cli/src/command_runner.dart'; +import 'package:very_good_cli/src/version.dart'; + +class MockLogger extends Mock implements Logger {} + +void main() { + group('VeryGoodCommandRunner', () { + List printLogs; + Logger logger; + VeryGoodCommandRunner commandRunner; + + void Function() overridePrint(void Function() fn) { + return () { + final spec = ZoneSpecification(print: (_, __, ___, String msg) { + printLogs.add(msg); + }); + return Zone.current.fork(specification: spec).run(fn); + }; + } + + setUp(() { + printLogs = []; + logger = MockLogger(); + commandRunner = VeryGoodCommandRunner(logger: logger); + }); + + test('can be instantiated without an explicit logger instance', () { + final commandRunner = VeryGoodCommandRunner(); + expect(commandRunner, isNotNull); + }); + + group('run', () { + test('handles FormatException', () async { + const exception = FormatException('oops!'); + var isFirstInvocation = true; + when(logger.info(any)).thenAnswer((_) { + if (isFirstInvocation) { + isFirstInvocation = false; + throw exception; + } + }); + final result = await commandRunner.run(['--version']); + expect(result, equals(ExitCode.usage.code)); + verify(logger.err(exception.message)).called(1); + verify(logger.info(commandRunner.usage)).called(1); + }); + + test('handles UsageException', () async { + final exception = UsageException('oops!', commandRunner.usage); + var isFirstInvocation = true; + when(logger.info(any)).thenAnswer((_) { + if (isFirstInvocation) { + isFirstInvocation = false; + throw exception; + } + }); + final result = await commandRunner.run(['--version']); + expect(result, equals(ExitCode.usage.code)); + verify(logger.err(exception.message)).called(1); + verify(logger.info(commandRunner.usage)).called(1); + }); + + test('handles no command', overridePrint(() async { + const expectedPrintLogs = [ + '🦄 A Very Good Commandline Interface\n' + '\n' + 'Usage: very_good [arguments]\n' + '\n' + 'Global options:\n' + '-h, --help Print this usage information.\n' + ' --version Print the current version.\n' + '\n' + 'Available commands:\n' + ' help Display help information for very_good.\n' + '\n' + '''Run "very_good help " for more information about a command.''' + ]; + final result = await commandRunner.run([]); + expect(printLogs, equals(expectedPrintLogs)); + expect(result, equals(ExitCode.success.code)); + })); + + group('--help', () { + test('outputs usage', overridePrint(() async { + const expectedPrintLogs = [ + '🦄 A Very Good Commandline Interface\n' + '\n' + 'Usage: very_good [arguments]\n' + '\n' + 'Global options:\n' + '-h, --help Print this usage information.\n' + ' --version Print the current version.\n' + '\n' + 'Available commands:\n' + ' help Display help information for very_good.\n' + '\n' + '''Run "very_good help " for more information about a command.''' + ]; + final result = await commandRunner.run(['--help']); + expect(printLogs, equals(expectedPrintLogs)); + expect(result, equals(ExitCode.success.code)); + + printLogs.clear(); + + final resultAbbr = await commandRunner.run(['-h']); + expect(printLogs, equals(expectedPrintLogs)); + expect(resultAbbr, equals(ExitCode.success.code)); + })); + }); + + group('--version', () { + test('outputs current version', () async { + final result = await commandRunner.run(['--version']); + expect(result, equals(ExitCode.success.code)); + verify(logger.info('very_good version: $packageVersion')); + }); + }); + }); + }); +}