From ca053f7f4e4cd32bfeca0e4bc894e53bf4ff57cf Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Fri, 8 Aug 2025 12:02:54 -0700 Subject: [PATCH 1/4] wip --- .../Invocation/InvocationTests.cs | 19 ++++++++++++++----- src/System.CommandLine/ParseResult.cs | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/System.CommandLine.Tests/Invocation/InvocationTests.cs b/src/System.CommandLine.Tests/Invocation/InvocationTests.cs index 745ccaed1f..17d736f9c3 100644 --- a/src/System.CommandLine.Tests/Invocation/InvocationTests.cs +++ b/src/System.CommandLine.Tests/Invocation/InvocationTests.cs @@ -266,15 +266,24 @@ public void Nonterminating_option_action_does_not_short_circuit_command_action() } [Fact] - public void When_multiple_options_with_actions_are_present_then_only_the_last_one_is_invoked() + public void When_multiple_options_with_terminating_actions_are_present_then_only_the_last_one_is_invoked() { bool optionAction1WasCalled = false; bool optionAction2WasCalled = false; bool optionAction3WasCalled = false; - SynchronousTestAction optionAction1 = new(_ => optionAction1WasCalled = true); - SynchronousTestAction optionAction2 = new(_ => optionAction2WasCalled = true); - SynchronousTestAction optionAction3 = new(_ => optionAction3WasCalled = true); + SynchronousTestAction optionAction1 = new(_ => + { + optionAction1WasCalled = true; + }, terminating: true); + SynchronousTestAction optionAction2 = new(_ => + { + optionAction2WasCalled = true; + }, terminating: true); + SynchronousTestAction optionAction3 = new(_ => + { + optionAction3WasCalled = true; + }, terminating: true); Command command = new Command("cmd") { @@ -283,7 +292,7 @@ public void When_multiple_options_with_actions_are_present_then_only_the_last_on new Option("--3") { Action = optionAction3 } }; - ParseResult parseResult = command.Parse("cmd --1 true --3 false --2 true"); + ParseResult parseResult = command.Parse("cmd --1 a --3 a --2 a"); using var _ = new AssertionScope(); diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 3153acaacf..666f8256ef 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -51,7 +51,7 @@ internal ParseResult( } else { - Tokens = Array.Empty(); + Tokens = []; } CommandLineText = commandLineText; From dd9e44e48a7bd9e374557582b0267d3e94ea4343 Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Fri, 8 Aug 2025 12:58:31 -0700 Subject: [PATCH 2/4] improve preaction testing --- .../Invocation/InvocationTests.cs | 91 +++++++++++++++++-- 1 file changed, 81 insertions(+), 10 deletions(-) diff --git a/src/System.CommandLine.Tests/Invocation/InvocationTests.cs b/src/System.CommandLine.Tests/Invocation/InvocationTests.cs index 17d736f9c3..73181c2479 100644 --- a/src/System.CommandLine.Tests/Invocation/InvocationTests.cs +++ b/src/System.CommandLine.Tests/Invocation/InvocationTests.cs @@ -265,8 +265,10 @@ public void Nonterminating_option_action_does_not_short_circuit_command_action() commandActionWasCalled.Should().BeTrue(); } - [Fact] - public void When_multiple_options_with_terminating_actions_are_present_then_only_the_last_one_is_invoked() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task When_multiple_options_with_terminating_actions_are_present_then_only_the_last_one_is_invoked(bool invokeAsync) { bool optionAction1WasCalled = false; bool optionAction2WasCalled = false; @@ -285,24 +287,90 @@ public void When_multiple_options_with_terminating_actions_are_present_then_only optionAction3WasCalled = true; }, terminating: true); - Command command = new Command("cmd") + var command = new RootCommand { - new Option("--1") { Action = optionAction1 }, - new Option("--2") { Action = optionAction2 }, - new Option("--3") { Action = optionAction3 } + Action = new AsynchronousTestAction(_ => {}), + Options = + { + new Option("--1") { Action = optionAction1 }, + new Option("--2") { Action = optionAction2 }, + new Option("--3") { Action = optionAction3 }, + } }; - ParseResult parseResult = command.Parse("cmd --1 a --3 a --2 a"); + ParseResult parseResult = command.Parse("--1 --3 --2"); using var _ = new AssertionScope(); parseResult.Action.Should().Be(optionAction2); - parseResult.Invoke().Should().Be(0); + + if (invokeAsync) + { + (await parseResult.InvokeAsync()).Should().Be(0); + } + else + { + parseResult.Invoke().Should().Be(0); + } + optionAction1WasCalled.Should().BeFalse(); optionAction2WasCalled.Should().BeTrue(); optionAction3WasCalled.Should().BeFalse(); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task When_multiple_options_with_nonterminating_actions_are_present_then_all_are_invoked(bool invokeAsync) + { + bool optionAction1WasCalled = false; + bool optionAction2WasCalled = false; + bool optionAction3WasCalled = false; + bool commandActionWasCalled = false; + + SynchronousTestAction optionAction1 = new(_ => + { + optionAction1WasCalled = true; + }, terminating: false); + SynchronousTestAction optionAction2 = new(_ => + { + optionAction2WasCalled = true; + }, terminating: false); + SynchronousTestAction optionAction3 = new(_ => + { + optionAction3WasCalled = true; + }, terminating: false); + + var command = new RootCommand + { + Action = new AsynchronousTestAction(_ => commandActionWasCalled = true), + Options = + { + new Option("--1") { Action = optionAction1 }, + new Option("--2") { Action = optionAction2 }, + new Option("--3") { Action = optionAction3 }, + } + }; + + ParseResult parseResult = command.Parse("--1 true --3 false --2 true"); + + using var _ = new AssertionScope(); + + if (invokeAsync) + { + (await parseResult.InvokeAsync()).Should().Be(0); + } + else + { + parseResult.Invoke().Should().Be(0); + } + + optionAction1WasCalled.Should().BeTrue(); + optionAction2WasCalled.Should().BeTrue(); + optionAction3WasCalled.Should().BeTrue(); + commandActionWasCalled.Should().BeTrue(); + } + [Fact] public void Directive_action_takes_precedence_over_option_action() { @@ -336,9 +404,12 @@ public void Directive_action_takes_precedence_over_option_action() [Theory] [InlineData(true)] [InlineData(false)] - public async Task Nontermninating_option_actions_handle_exceptions_and_return_an_error_return_code(bool invokeAsync) + public async Task Nonterminating_option_actions_handle_exceptions_and_return_an_error_return_code(bool invokeAsync) { - var nonexclusiveAction = new SynchronousTestAction(_ => throw new Exception("oops!"), terminating: false); + var nonexclusiveAction = new SynchronousTestAction(_ => + { + throw new Exception("oops!"); + }, terminating: false); var command = new RootCommand { From c928915e4c4dea58a7a41bd19f33f5b5e071c3dc Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Fri, 8 Aug 2025 13:28:48 -0700 Subject: [PATCH 3/4] fix #2128 --- ...s.Help_layout_has_not_changed.approved.txt | 18 ++++---- .../Help/HelpBuilderTests.Customization.cs | 6 +-- .../Help/HelpBuilderTests.cs | 32 ++++++++++---- .../HelpOptionTests.cs | 4 +- .../Help/HelpBuilder.Default.cs | 42 +++++++++++++------ 5 files changed, 67 insertions(+), 35 deletions(-) diff --git a/src/System.CommandLine.Tests/Help/Approvals/HelpBuilderTests.Help_layout_has_not_changed.approved.txt b/src/System.CommandLine.Tests/Help/Approvals/HelpBuilderTests.Help_layout_has_not_changed.approved.txt index 79cc177654..2fb90ef354 100644 --- a/src/System.CommandLine.Tests/Help/Approvals/HelpBuilderTests.Help_layout_has_not_changed.approved.txt +++ b/src/System.CommandLine.Tests/Help/Approvals/HelpBuilderTests.Help_layout_has_not_changed.approved.txt @@ -12,13 +12,13 @@ Arguments: the-root-arg-enum-default-description [default: Read] Options: - -trna, --the-root-option-no-arg (REQUIRED) the-root-option-no-arg-description - -trondda, --the-root-option-no-description-default-arg [default: the-root-option--no-description-default-arg-value] - -tronda, --the-root-option-no-default-arg (REQUIRED) the-root-option-no-default-description - -troda, --the-root-option-default-arg the-root-option-default-arg-description [default: the-root-option-arg-value] - -troea, --the-root-option-enum-arg the-root-option-description [default: Read] - -trorea, --the-root-option-required-enum-arg (REQUIRED) the-root-option-description [default: Read] - -tromld, --the-root-option-multi-line-description the-root-option - multi-line - description + -trna, --the-root-option-no-arg (REQUIRED) the-root-option-no-arg-description + -trondda, --the-root-option-no-description-default-arg [default: the-root-option--no-description-default-arg-value] + -tronda, --the-root-option-no-default-arg (REQUIRED) the-root-option-no-default-description + -troda, --the-root-option-default-arg the-root-option-default-arg-description [default: the-root-option-arg-value] + -troea, --the-root-option-enum-arg the-root-option-description [default: Read] + -trorea, --the-root-option-required-enum-arg (REQUIRED) the-root-option-description [default: Read] + -tromld, --the-root-option-multi-line-description the-root-option + multi-line + description diff --git a/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs b/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs index b103f18162..c3d73d818f 100644 --- a/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs +++ b/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs @@ -45,7 +45,7 @@ public void Option_can_customize_displayed_default_value() _helpBuilder.Write(command, _console); var expected = $"Options:{NewLine}" + - $"{_indentation}--the-option{_columnPadding}[default: 42]{NewLine}{NewLine}"; + $"{_indentation}--the-option {_columnPadding}[default: 42]{NewLine}{NewLine}"; _console.ToString().Should().Contain(expected); } @@ -245,9 +245,9 @@ public void Customize_throws_when_symbol_is_null() [Theory] - [InlineData(false, false, "--option \\s*description")] + [InlineData(false, false, "--option