Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
12ea6ae
implemented Group Option Name
indraneel12 Oct 7, 2025
31a938e
added Unit Tests for Group Name
indraneel12 Oct 7, 2025
aaa430e
style: Consistent usage of `final`
indraneel12 Oct 7, 2025
c9ed4a4
perf: Minimize condition-checks
indraneel12 Oct 7, 2025
6146a2e
perf: Temporary reallocation instances avoided via Cascade Operator
indraneel12 Oct 7, 2025
a5561b7
refactor: Satisfy the nitpicky Review Bot
indraneel12 Oct 7, 2025
abba30e
refactor (potential fix): Backwards Compatibility
indraneel12 Oct 8, 2025
48e440b
refactor: Choice to customize default Group Name
indraneel12 Oct 8, 2025
2eaea17
fix: Upstream Bug detected and escalated
indraneel12 Oct 8, 2025
67c1c7a
test: Improvement to `option_group_usage_text_test.dart`
indraneel12 Oct 9, 2025
ff9368e
test: Ensures no automatic count-based Group Name when only one Group…
indraneel12 Oct 9, 2025
6e522d8
fix: Consistency with the existing Wrapper around Group Name
indraneel12 Oct 9, 2025
51abfbd
docs: Improvement to minor inline documentation
indraneel12 Oct 9, 2025
173d6de
test: Unit Test for jumbled Option Groups
indraneel12 Oct 9, 2025
293ac9e
docs(OptionGroup): Public Comment updated
indraneel12 Oct 9, 2025
67ec08d
refactor: Consistent processing
indraneel12 Oct 9, 2025
f08dc50
refactor: Improved readability of `addOptionsToParser`
indraneel12 Oct 10, 2025
4cda148
fix: Empty Headers avoided
indraneel12 Oct 10, 2025
78dc901
test: Hidden Groups are avoided as Group Name Separator
indraneel12 Oct 10, 2025
98cd918
test: Combined Behavior check made more rigorous
indraneel12 Oct 10, 2025
2f49719
refactor: Readability of Test Cases (for Group Name) improved
indraneel12 Oct 10, 2025
2a4bbae
style: usage of CAPS and Caps
indraneel12 Oct 10, 2025
24c1b28
test: Group Name Separators uniqueness
indraneel12 Oct 10, 2025
0c947b2
docs: Public Documentation added to cover the new behavior
indraneel12 Oct 10, 2025
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
@@ -0,0 +1,342 @@
import 'package:cli_tools/better_command_runner.dart';
import 'package:config/config.dart';
import 'package:test/test.dart';

void main() {
const mockCommandName = 'mock';
const mockCommandDescription = 'A mock CLI for Option Group Usage Text test.';

String buildSeparatorView(final String name) => '\n\n$name\n';

String buildSuffix([final Object? suffix = '', final String prefix = '']) =>
suffix != null && suffix != '' ? '$prefix${suffix.toString()}' : '';

String buildMockArgName([final Object? suffix = '']) =>
'mock-arg${buildSuffix(suffix, '-')}';

String buildMockGroupName([final Object? suffix = '']) =>
'Mock Group${buildSuffix(suffix, ' ')}';

OptionDefinition buildMockOption(
final String argName,
final String? groupName, {
final bool hide = false,
}) =>
FlagOption(
argName: argName,
hide: hide,
group: groupName != null ? OptionGroup(groupName) : null,
helpText: 'Help section for $argName.',
);

BetterCommandRunner buildRunner(final List<OptionDefinition> options) =>
BetterCommandRunner(
mockCommandName,
mockCommandDescription,
globalOptions: options,
);

int howManyMatches(final String pattern, final String target) =>
RegExp(pattern).allMatches(target).length;

group('Group Names (of visible Groups) are rendered as-is', () {
final grouplessOptions = <OptionDefinition>[
for (var i = 0; i < 5; ++i) buildMockOption(buildMockArgName(i), null),
];
final groupedOptions = <OptionDefinition>[
for (var i = 5; i < 10; ++i)
buildMockOption(buildMockArgName(i), buildMockGroupName(i)),
];
final expectation = allOf([
for (var i = 5; i < 10; ++i) contains(buildMockGroupName(i)),
]);
test(
'in the presence of Groupless Options',
() {
expect(
buildRunner(grouplessOptions + groupedOptions).usage,
expectation,
);
},
);
test(
'in the absence of Groupless Options',
() {
expect(
buildRunner(groupedOptions).usage,
expectation,
);
},
);
});

group('Group Names (of invisible Groups) are hidden', () {
var testOptionCount = 0;
var testGroupCount = 0;
final grouplessOptions = <OptionDefinition>[
for (var i = 0; i < 5; ++i)
buildMockOption(buildMockArgName(++testOptionCount), null),
];
final groupedOptions = <OptionDefinition>[
for (var i = 0; i < 5; ++i)
buildMockOption(
buildMockArgName(++testOptionCount),
buildMockGroupName(++testGroupCount),
),
];
final hiddenGroups = <OptionDefinition>[
for (var i = 0; i < 5; ++i)
buildMockOption(
buildMockArgName(++testOptionCount),
buildMockGroupName(++testGroupCount),
hide: true,
),
];
var expectationGroupCount = 0;
final expectation = allOf([
for (var i = 0; i < 5; ++i)
contains(buildMockGroupName(++expectationGroupCount)),
for (var i = 0; i < 5; ++i)
isNot(contains(buildMockGroupName(++expectationGroupCount))),
]);
test(
'in the presence of Groupless Options',
() {
expect(
buildRunner(grouplessOptions + groupedOptions + hiddenGroups).usage,
expectation,
);
},
);
test(
'in the absence of Groupless Options',
() {
expect(
buildRunner(groupedOptions + hiddenGroups).usage,
expectation,
);
},
);
});

group('Group Names are properly padded with newlines', () {
final grouplessOptions = <OptionDefinition>[
for (var i = 0; i < 5; ++i) buildMockOption(buildMockArgName(i), null),
];
final groupedOptions = <OptionDefinition>[
for (var i = 5; i < 10; ++i)
buildMockOption(buildMockArgName(i), buildMockGroupName(i)),
];
final expectation = allOf([
for (var i = 5; i < 10; ++i)
contains(buildSeparatorView(buildMockGroupName(i))),
]);
test(
'in the presence of Groupless Options',
() {
expect(
buildRunner(grouplessOptions + groupedOptions).usage,
expectation,
);
},
);
test(
'in the absence of Groupless Options',
() {
expect(
buildRunner(groupedOptions).usage,
expectation,
);
},
);
});

group('Only one Separator per unique Group Name', () {
final grouplessOptions = <OptionDefinition>[
for (var i = 0; i < 5; ++i) buildMockOption(buildMockArgName(i), null),
];
final groupedOptions = <OptionDefinition>[
for (var i = 5; i < 10; ++i)
buildMockOption(buildMockArgName(i), buildMockGroupName('A')),
for (var i = 10; i < 15; ++i)
buildMockOption(buildMockArgName(i), buildMockGroupName('B')),
for (var i = 15; i < 20; ++i)
buildMockOption(buildMockArgName(i), buildMockGroupName('A')),
];
void checkExpectation(final String usage) {
expect(
usage,
stringContainsInOrder([
buildSeparatorView(buildMockGroupName('A')),
for (var i = 5; i < 10; ++i) buildMockArgName(i),
for (var i = 15; i < 20; ++i) buildMockArgName(i),
buildSeparatorView(buildMockGroupName('B')),
for (var i = 10; i < 15; ++i) buildMockArgName(i),
]),
);
expect(
howManyMatches(buildSeparatorView(buildMockGroupName('A')), usage),
equals(1),
);
expect(
howManyMatches(buildSeparatorView(buildMockGroupName('B')), usage),
equals(1),
);
}

test(
'in the presence of Groupless Options',
() {
checkExpectation(buildRunner(grouplessOptions + groupedOptions).usage);
},
);
test(
'in the absence of Groupless Options',
() {
checkExpectation(buildRunner(groupedOptions).usage);
},
);
});

test(
'All Groupless Options are shown before Grouped Options',
() {
final groupedOptions = <OptionDefinition>[
for (var i = 0; i < 5; ++i)
buildMockOption(buildMockArgName(i), buildMockGroupName(i)),
];
final grouplessOptions = <OptionDefinition>[
for (var i = 5; i < 10; ++i) buildMockOption(buildMockArgName(i), null),
];
final expectation = stringContainsInOrder([
'\n',
for (var i = 5; i < 10; ++i) ...[
buildMockArgName(i),
'\n',
],
'\n',
for (var i = 0; i < 5; ++i) ...[
buildMockArgName(i),
'\n',
],
'\n',
]);
expect(
buildRunner(groupedOptions + grouplessOptions).usage,
expectation,
);
},
);

test(
'Relative order of all Options within a Group is preserved',
() {
var testOptionCount = 0;
final grouplessOptions = <OptionDefinition>[
for (var i = 0; i < 5; ++i)
buildMockOption(buildMockArgName(++testOptionCount), null),
];
final groupedOptions = <OptionDefinition>[
for (var i = 0; i < 3; ++i)
for (var j = 0; j < 5; ++j)
buildMockOption(
buildMockArgName(++testOptionCount),
buildMockGroupName(i),
),
];
var expectationOptionCount = 0;
final expectation = stringContainsInOrder([
'\n',
for (var i = 0; i < 5; ++i) ...[
buildMockArgName(++expectationOptionCount),
'\n',
],
'\n',
for (var i = 0; i < 3; ++i)
for (var j = 0; j < 5; ++j) ...[
buildMockArgName(++expectationOptionCount),
'\n',
],
'\n',
]);
expect(
buildRunner(grouplessOptions + groupedOptions).usage,
expectation,
);
},
);

test(
'Relative order of all Groups is preserved',
() {
var optionCount = 0;
var testGroupCount = 0;
final grouplessOptions = <OptionDefinition>[
for (var i = 0; i < 5; ++i)
buildMockOption(buildMockArgName(++optionCount), null),
];
final groupedOptions = <OptionDefinition>[
for (var i = 0; i < 3; ++i)
for (var j = 0; j < 5; ++j)
buildMockOption(
buildMockArgName(++optionCount),
buildMockGroupName(++testGroupCount),
),
];
var expectationGroupCount = 0;
final expectation = stringContainsInOrder([
for (var i = 0; i < testGroupCount; ++i)
buildSeparatorView(buildMockGroupName(++expectationGroupCount)),
]);
expect(
buildRunner(grouplessOptions + groupedOptions).usage,
expectation,
);
},
);

test(
'Combined Behavior check (Groupless Options, Grouped Options, Hidden Groups)',
() {
final usage = buildRunner(<OptionDefinition>[
buildMockOption('option-1', null),
buildMockOption('option-2', 'Group 1'),
buildMockOption('option-3', 'Group 2'),
buildMockOption('option-4', 'Group 1'),
buildMockOption('option-5', null),
buildMockOption('option-6', 'Group 2'),
buildMockOption('option-7', 'Group 3', hide: true),
buildMockOption('option-8', 'Group 4', hide: true),
buildMockOption('option-9', 'Group 4', hide: true),
buildMockOption('option-10', 'Group 5', hide: true),
buildMockOption('option-11', 'Group 5'),
]).usage;
expect(
usage,
allOf([
stringContainsInOrder([
'option-1',
'option-5',
'Group 1',
'option-2',
'option-4',
'Group 2',
'option-3',
'option-6',
'Group 5',
'option-11',
]),
isNot(contains('Group 3')),
isNot(contains('option-7')),
isNot(contains('Group 4')),
isNot(contains('option-8')),
isNot(contains('option-9')),
isNot(contains('option-10')),
]),
);
expect(howManyMatches('Group 1', usage), equals(1));
expect(howManyMatches('Group 2', usage), equals(1));
expect(howManyMatches('Group 5', usage), equals(1));
},
);
}
2 changes: 1 addition & 1 deletion packages/config/lib/src/config/config_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ class ConfigParser implements ArgParser {
void _addOption(final OptionDefinition opt) {
_optionDefinitions.add(opt);
// added continuously to the parser so separators are placed correctly:
addOptionsToParser([opt], _parser);
addOptionsToParser([opt], _parser, addGroupSeparators: false);
}

@override
Expand Down
Loading