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
15 changes: 15 additions & 0 deletions packages/shorebird_tools/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# See https://www.dartlang.org/guides/libraries/private-files

# Files and directories created by pub
.dart_tool/
.packages
build/
pubspec.lock

# Files generated during tests
.test_coverage.dart
coverage/
.test_runner.dart

# Android studio and IntelliJ
.idea
3 changes: 3 additions & 0 deletions packages/shorebird_tools/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## shorebird_tools

Tools for shorebird which need to be pinned to a specific flutter version.
4 changes: 4 additions & 0 deletions packages/shorebird_tools/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
include: package:very_good_analysis/analysis_options.5.1.0.yaml
linter:
rules:
public_member_api_docs: false
18 changes: 18 additions & 0 deletions packages/shorebird_tools/bin/shorebird_tools.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'dart:io';

import 'package:shorebird_tools/src/command_runner.dart';

Future<void> main(List<String> args) async {
await _flushThenExit(await ShorebirdToolsCommandRunner().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<void> _flushThenExit(int status) {
return Future.wait<void>([stdout.close(), stderr.close()])
.then<void>((_) => exit(status));
}
3 changes: 3 additions & 0 deletions packages/shorebird_tools/dart_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
tags:
version-verify:
skip: "Should only be run during pull request. Verifies if version file is updated."
10 changes: 10 additions & 0 deletions packages/shorebird_tools/lib/shorebird_tools.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/// shorebird_tools, Shorebird tools for Flutter
///
/// ```sh
/// # activate shorebird_tools
/// dart pub global activate shorebird_tools
///
/// # see usage
/// shorebird_tools --help
/// ```
library;
108 changes: 108 additions & 0 deletions packages/shorebird_tools/lib/src/command_runner.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:shorebird_tools/src/commands/commands.dart';
import 'package:shorebird_tools/src/version.dart';

const executableName = 'shorebird_tools';
const packageName = 'shorebird_tools';
const description = 'Shorebird tools for Flutter';

/// {@template shorebird_tools_command_runner}
/// A [CommandRunner] for the CLI.
///
/// ```
/// $ shorebird_tools --version
/// ```
/// {@endtemplate}
class ShorebirdToolsCommandRunner extends CommandRunner<int> {
/// {@macro shorebird_tools_command_runner}
ShorebirdToolsCommandRunner({
Logger? logger,
}) : _logger = logger ?? Logger(),
super(executableName, description) {
// Add root options and flags
argParser
..addFlag(
'version',
abbr: 'v',
negatable: false,
help: 'Print the current version.',
)
..addFlag(
'verbose',
help: 'Noisy logging, including all shell commands executed.',
);

// Add sub commands
addCommand(SampleCommand(logger: _logger));
}

@override
void printUsage() => _logger.info(usage);

final Logger _logger;

@override
Future<int> run(Iterable<String> args) async {
try {
final topLevelResults = parse(args);
if (topLevelResults['verbose'] == true) {
_logger.level = Level.verbose;
}
return await runCommand(topLevelResults) ?? ExitCode.success.code;
} on FormatException catch (e, stackTrace) {
// On format errors, show the commands error message, root usage and
// exit with an error code
_logger
..err(e.message)
..err('$stackTrace')
..info('')
..info(usage);
return ExitCode.usage.code;
} on UsageException catch (e) {
// On usage errors, show the commands usage message and
// exit with an error code
_logger
..err(e.message)
..info('')
..info(e.usage);
return ExitCode.usage.code;
}
}

@override
Future<int?> runCommand(ArgResults topLevelResults) async {
// Verbose logs
_logger
..detail('Argument information:')
..detail(' Top level options:');
for (final option in topLevelResults.options) {
if (topLevelResults.wasParsed(option)) {
_logger.detail(' - $option: ${topLevelResults[option]}');
}
}
if (topLevelResults.command != null) {
final commandResult = topLevelResults.command!;
_logger
..detail(' Command: ${commandResult.name}')
..detail(' Command options:');
for (final option in commandResult.options) {
if (commandResult.wasParsed(option)) {
_logger.detail(' - $option: ${commandResult[option]}');
}
}
}

// Run the command or show version
final int? exitCode;
if (topLevelResults['version'] == true) {
_logger.info(packageVersion);
exitCode = ExitCode.success.code;
} else {
exitCode = await super.runCommand(topLevelResults);
}

return exitCode;
}
}
1 change: 1 addition & 0 deletions packages/shorebird_tools/lib/src/commands/commands.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'sample_command.dart';
39 changes: 39 additions & 0 deletions packages/shorebird_tools/lib/src/commands/sample_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'package:args/command_runner.dart';
import 'package:mason_logger/mason_logger.dart';

/// {@template sample_command}
///
/// `shorebird_tools sample`
/// A [Command] to exemplify a sub command
/// {@endtemplate}
class SampleCommand extends Command<int> {
/// {@macro sample_command}
SampleCommand({
required Logger logger,
}) : _logger = logger {
argParser.addFlag(
'cyan',
abbr: 'c',
help: 'Prints the same joke, but in cyan',
negatable: false,
);
}

@override
String get description => 'A sample sub command that just prints one joke';

@override
String get name => 'sample';

final Logger _logger;

@override
Future<int> run() async {
var output = 'Which unicorn has a cold? The Achoo-nicorn!';
if (argResults?['cyan'] == true) {
output = lightCyan.wrap(output)!;
}
_logger.info(output);
return ExitCode.success.code;
}
}
2 changes: 2 additions & 0 deletions packages/shorebird_tools/lib/src/version.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions packages/shorebird_tools/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: shorebird_tools
description: Shorebird tools for Flutter
version: 0.0.1
publish_to: none

environment:
sdk: "^3.3.0"

dependencies:
args: ^2.4.2
mason_logger: ^0.2.12

dev_dependencies:
build_runner: ^2.4.8
build_verify: ^3.1.0
build_version: ^2.1.1
mocktail: ^1.0.3
test: ^1.25.2
very_good_analysis: ^5.1.0

executables:
shorebird_tools:
9 changes: 9 additions & 0 deletions packages/shorebird_tools/test/ensure_build_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@Tags(['version-verify'])
library;

import 'package:build_verify/build_verify.dart';
import 'package:test/test.dart';

void main() {
test('ensure_build', expectBuildClean);
}
103 changes: 103 additions & 0 deletions packages/shorebird_tools/test/src/command_runner_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import 'dart:io';

import 'package:args/command_runner.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:shorebird_tools/src/command_runner.dart';
import 'package:shorebird_tools/src/version.dart';
import 'package:test/test.dart';

class _MockLogger extends Mock implements Logger {}

class _MockProgress extends Mock implements Progress {}


const latestVersion = '0.0.0';


void main() {
group('ShorebirdToolsCommandRunner', () {
late Logger logger;
late ShorebirdToolsCommandRunner commandRunner;

setUp(() {
logger = _MockLogger();

commandRunner = ShorebirdToolsCommandRunner(
logger: logger,
);
});

test('can be instantiated without an explicit analytics/logger instance',
() {
final commandRunner = ShorebirdToolsCommandRunner();
expect(commandRunner, isNotNull);
});

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!', 'exception 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('exception usage')).called(1);
});

group('--version', () {
test('outputs current version', () async {
final result = await commandRunner.run(['--version']);
expect(result, equals(ExitCode.success.code));
verify(() => logger.info(packageVersion)).called(1);
});
});

group('--verbose', () {
test('enables verbose logging', () async {
final result = await commandRunner.run(['--verbose']);
expect(result, equals(ExitCode.success.code));

verify(() => logger.detail('Argument information:')).called(1);
verify(() => logger.detail(' Top level options:')).called(1);
verify(() => logger.detail(' - verbose: true')).called(1);
verifyNever(() => logger.detail(' Command options:'));
});

test('enables verbose logging for sub commands', () async {
final result = await commandRunner.run([
'--verbose',
'sample',
'--cyan',
]);
expect(result, equals(ExitCode.success.code));

verify(() => logger.detail('Argument information:')).called(1);
verify(() => logger.detail(' Top level options:')).called(1);
verify(() => logger.detail(' - verbose: true')).called(1);
verify(() => logger.detail(' Command: sample')).called(1);
verify(() => logger.detail(' Command options:')).called(1);
verify(() => logger.detail(' - cyan: true')).called(1);
});
});
});
}
Loading