Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,32 @@ typedef SetLogLevel = void Function({
/// A function type for tracking events.
typedef OnAnalyticsEvent = void Function(String event);

/// A dummy to replicate the usage-text of upstream private `HelpCommand`
final class _HelpCommandDummy extends Command {
_HelpCommandDummy({required this.runner});

static const label = 'help';

static const Null exitCode = null;

@override
final name = _HelpCommandDummy.label;

@override
final BetterCommandRunner runner;

@override
String get description =>
'Display help information for ${runner.executableName}.';

@override
String get invocation => '${runner.executableName} $name [command]';

@override
Never run() => throw StateError(
'This class is meant to only obtain the Usage Text for `$name` command');
}

/// An extension of [CommandRunner] with additional features.
///
/// This class extends the [CommandRunner] class from the `args` package and adds
Expand All @@ -72,7 +98,7 @@ class BetterCommandRunner<O extends OptionDefinition, T>
/// The environment variables used for configuration resolution.
final Map<String, String> envVariables;

/// The gloabl option definitions.
/// The global option definitions.
late final List<O> _globalOptions;

Configuration<O>? _globalConfiguration;
Expand Down Expand Up @@ -346,6 +372,10 @@ class BetterCommandRunner<O extends OptionDefinition, T>
await _onBeforeRunCommand?.call(this);

try {
if (_isUsageOfHelpCommandRequested(topLevelResults)) {
messageOutput?.logUsage(_HelpCommandDummy(runner: this).usage);
return _HelpCommandDummy.exitCode;
}
return await super.runCommand(topLevelResults);
} on UsageException catch (e) {
messageOutput?.logUsageException(e);
Expand All @@ -367,6 +397,32 @@ class BetterCommandRunner<O extends OptionDefinition, T>
);
}

static bool _isUsageOfHelpCommandRequested(final ArgResults topLevelResults) {
// check whether Help Command is chosen
final topLevelCommand = topLevelResults.command;
if (topLevelCommand == null) {
return false;
}
if (topLevelCommand.name != _HelpCommandDummy.label) {
return false;
}
final helpCommand = topLevelCommand;
// check whether it's allowed to get the usage-text for `help`
if (!helpCommand.options.contains(_HelpCommandDummy.label)) {
throw StateError('Upstream `package:args` has a breaking change');
}
// case: `mock help -h`
if (helpCommand.flag(_HelpCommandDummy.label)) {
return true;
}
// case: `mock help help`
if ((helpCommand.arguments.contains(_HelpCommandDummy.label))) {
return true;
}
// aside: more cases may be added if necessary in future
return false;
}

static CommandRunnerLogLevel _determineLogLevel(final Configuration config) {
if (config.findValueOf(argName: BetterCommandRunnerFlags.verbose) == true) {
return CommandRunnerLogLevel.verbose;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import 'dart:async' show ZoneSpecification, runZoned;

import 'package:args/command_runner.dart' show CommandRunner, UsageException;
import 'package:cli_tools/better_command_runner.dart'
show BetterCommandRunner, MessageOutput;
import 'package:test/test.dart';

void main() => _runTests();

const _exeName = 'mock';
const _exeDescription = 'A mock command to test `help -h` with MessageOutput.';
const _moMockText = 'SUCCESS: `MessageOutput.usageLogger`';
const _moExceptionText = 'SUCCESS: `MessageOutput.usageExceptionLogger`';
const _invalidOption = '-z';

CommandRunner _buildUpstreamRunner() => CommandRunner(
_exeName,
_exeDescription,
);

BetterCommandRunner _buildBetterRunner() => BetterCommandRunner(
_exeName,
_exeDescription,
messageOutput: MessageOutput(
usageLogger: (final usage) {
print(_moMockText);
print(usage);
},
usageExceptionLogger: (final exception) {
print(_moExceptionText);
print(exception.message);
print(exception.usage);
},
),
);

void _runTests() {
group('Given a BetterCommandRunner with custom MessageOutput', () {
for (final args in [
['help', '-h'],
['help', 'help'],
['help', '-h', 'help'],
['help', '-h', 'help', '-h'],
['help', '-h', 'help', '-h', 'help'],
['help', '-h', 'help', '-h', 'help', '-h'],
]) {
_testsForValidHelpUsageRequests(args);
}
for (final args in [
['help', '-h', _invalidOption],
['help', 'help', _invalidOption],
['help', _invalidOption, 'help'],
['help', 'help', '-h', _invalidOption],
['help', _invalidOption, 'help', '-h', _invalidOption],
]) {
_testsForInvalidHelpUsageRequests(args);
}
});
}

void _testsForValidHelpUsageRequests(final List<String> args) {
group('when $args is received', () {
final betterRunnerOutput = StringBuffer();
final upstreamRunnerOutput = StringBuffer();
late final Object? betterRunnerExitCode;
late final Object? upstreamRunnerExitCode;
setUpAll(() async {
betterRunnerExitCode = await runZoned(
() async => await _buildBetterRunner().run(args),
zoneSpecification: ZoneSpecification(
print: (final _, final __, final ___, final String line) {
betterRunnerOutput.writeln(line);
}),
);
upstreamRunnerExitCode = await runZoned(
() async => await _buildUpstreamRunner().run(args),
zoneSpecification: ZoneSpecification(
print: (final _, final __, final ___, final String line) {
upstreamRunnerOutput.writeln(line);
}),
);
});
test('then MessageOutput is not bypassed', () {
expect(
betterRunnerOutput.toString(),
contains(_moMockText),
);
});
test('then it can subsume upstream HelpCommand output', () {
expect(
betterRunnerOutput.toString(),
stringContainsInOrder([
_moMockText,
upstreamRunnerOutput.toString(),
]),
);
});
test('then Exit Code (null) matches that of upstream HelpCommand', () {
expect(betterRunnerExitCode, equals(null));
expect(betterRunnerExitCode, equals(upstreamRunnerExitCode));
});
});
}

void _testsForInvalidHelpUsageRequests(final List<String> args) {
group('when $args is received', () {
final betterRunnerOutput = StringBuffer();
final upstreamRunnerOutput = StringBuffer();
UsageException? betterRunnerException;
UsageException? upstreamRunnerException;
setUpAll(() async {
try {
await runZoned(
() async => await _buildBetterRunner().run(args),
zoneSpecification: ZoneSpecification(
print: (final _, final __, final ___, final String line) {
betterRunnerOutput.writeln(line);
},
),
);
} on UsageException catch (e) {
betterRunnerException = e;
}
try {
await runZoned(
() async => await _buildUpstreamRunner().run(args),
zoneSpecification: ZoneSpecification(
print: (final _, final __, final ___, final String line) {
upstreamRunnerOutput.writeln(line);
},
),
);
} on UsageException catch (e) {
upstreamRunnerException = e;
}
});
test('then it throws UsageException exactly like CommandRunner', () {
expect(betterRunnerException, isA<UsageException>());
expect(upstreamRunnerException, isA<UsageException>());
expect(
betterRunnerException!.message,
equals(upstreamRunnerException!.message),
);
expect(
betterRunnerException!.usage,
equals(upstreamRunnerException!.usage),
);
});
test('then MessageOutput is not bypassed', () {
expect(
betterRunnerOutput.toString(),
stringContainsInOrder(
[
_moExceptionText,
betterRunnerException!.message,
betterRunnerException!.usage,
],
),
);
});
test('then it can subsume upstream HelpCommand output', () {
expect(
betterRunnerOutput.toString(),
contains(upstreamRunnerOutput.toString()),
);
});
});
}