From 38c81ed08d4f3a804ab00a18c19364dbfa16cd3f Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Fri, 7 Feb 2020 17:13:26 +0900 Subject: [PATCH 1/3] Fix some parsing issue and add integration tests. --- ConsoleAppFramework.sln | 15 +- ConsoleAppFramework.sln.DotSettings | 3 + src/ConsoleAppFramework/ConsoleAppEngine.cs | 54 +++-- .../AssemblyInfo.cs | 4 + .../CaptureConsoleOutput.cs | 25 +++ ...onsoleAppFramework.Integration.Test.csproj | 21 ++ .../MultipleCommandTest.cs | 106 +++++++++ .../SingleCommandTest.Arguments.cs | 101 +++++++++ .../SingleCommandTest.Options.cs | 207 ++++++++++++++++++ .../SingleCommandTest.OptionsAndArguments.cs | 97 ++++++++ .../SingleCommandTest.cs | 26 +++ 11 files changed, 636 insertions(+), 23 deletions(-) create mode 100644 ConsoleAppFramework.sln.DotSettings create mode 100644 tests/ConsoleAppFramework.Integration.Test/AssemblyInfo.cs create mode 100644 tests/ConsoleAppFramework.Integration.Test/CaptureConsoleOutput.cs create mode 100644 tests/ConsoleAppFramework.Integration.Test/ConsoleAppFramework.Integration.Test.csproj create mode 100644 tests/ConsoleAppFramework.Integration.Test/MultipleCommandTest.cs create mode 100644 tests/ConsoleAppFramework.Integration.Test/SingleCommandTest.Arguments.cs create mode 100644 tests/ConsoleAppFramework.Integration.Test/SingleCommandTest.Options.cs create mode 100644 tests/ConsoleAppFramework.Integration.Test/SingleCommandTest.OptionsAndArguments.cs create mode 100644 tests/ConsoleAppFramework.Integration.Test/SingleCommandTest.cs diff --git a/ConsoleAppFramework.sln b/ConsoleAppFramework.sln index 23faea1..a3eb377 100644 --- a/ConsoleAppFramework.sln +++ b/ConsoleAppFramework.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28307.168 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29728.190 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1F399F98-7439-4F05-847B-CC1267B4B7F2}" EndProject @@ -24,9 +24,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".circleci", ".circleci", "{ .circleci\config.yml = .circleci\config.yml EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppFramework.WebHosting", "src\ConsoleAppFramework.WebHosting\ConsoleAppFramework.WebHosting.csproj", "{9AC1CAE2-E717-472A-BBFB-0FE5590E5C7A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleAppFramework.WebHosting", "src\ConsoleAppFramework.WebHosting\ConsoleAppFramework.WebHosting.csproj", "{9AC1CAE2-E717-472A-BBFB-0FE5590E5C7A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebHostingApp", "sandbox\WebHostingApp\WebHostingApp.csproj", "{2B7CDEFC-3D92-4B72-8898-2494D7B087AD}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebHostingApp", "sandbox\WebHostingApp\WebHostingApp.csproj", "{2B7CDEFC-3D92-4B72-8898-2494D7B087AD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppFramework.Integration.Test", "tests\ConsoleAppFramework.Integration.Test\ConsoleAppFramework.Integration.Test.csproj", "{6A39E146-8CDF-4B04-88ED-395C56A32722}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -62,6 +64,10 @@ Global {2B7CDEFC-3D92-4B72-8898-2494D7B087AD}.Debug|Any CPU.Build.0 = Debug|Any CPU {2B7CDEFC-3D92-4B72-8898-2494D7B087AD}.Release|Any CPU.ActiveCfg = Release|Any CPU {2B7CDEFC-3D92-4B72-8898-2494D7B087AD}.Release|Any CPU.Build.0 = Release|Any CPU + {6A39E146-8CDF-4B04-88ED-395C56A32722}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6A39E146-8CDF-4B04-88ED-395C56A32722}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6A39E146-8CDF-4B04-88ED-395C56A32722}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6A39E146-8CDF-4B04-88ED-395C56A32722}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -74,6 +80,7 @@ Global {AF15C841-5D45-4E61-BFCE-A6E6B7BA7629} = {AAD2D900-C305-4449-A9FC-6C7696FFEDFA} {9AC1CAE2-E717-472A-BBFB-0FE5590E5C7A} = {1F399F98-7439-4F05-847B-CC1267B4B7F2} {2B7CDEFC-3D92-4B72-8898-2494D7B087AD} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6} + {6A39E146-8CDF-4B04-88ED-395C56A32722} = {AAD2D900-C305-4449-A9FC-6C7696FFEDFA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7F3E353A-C125-4020-8481-11DC6496358C} diff --git a/ConsoleAppFramework.sln.DotSettings b/ConsoleAppFramework.sln.DotSettings new file mode 100644 index 0000000..025f9b6 --- /dev/null +++ b/ConsoleAppFramework.sln.DotSettings @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file diff --git a/src/ConsoleAppFramework/ConsoleAppEngine.cs b/src/ConsoleAppFramework/ConsoleAppEngine.cs index 3738924..acd7c03 100644 --- a/src/ConsoleAppFramework/ConsoleAppEngine.cs +++ b/src/ConsoleAppFramework/ConsoleAppEngine.cs @@ -82,7 +82,7 @@ public async Task RunAsync(Type type, string[] args) { if (method != null) { - await SetFailAsync(ctx, "Found two public methods(wihtout command). Type:" + type.FullName + " Method:" + method.Name + " and " + item.Name); + await SetFailAsync(ctx, "Found more than one public methods(without command). Type:" + type.FullName + " Method:" + method.Name + " and " + item.Name); return; } method = item; // found single public(non-command) method. @@ -205,18 +205,36 @@ bool TryGetInvokeArguments(ParameterInfo[] parameters, string?[] args, int argsO { var jsonOption = (JsonSerializerOptions)provider.GetService(typeof(JsonSerializerOptions)); - var argumentDictionary = ParseArgument(args, argsOffset); + // Collect option types for parsing command-line arguments. + var optionTypeByOptionName = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < parameters.Length; i++) + { + var item = parameters[i]; + var option = item.GetCustomAttribute(); + + optionTypeByOptionName[item.Name] = item.ParameterType; + if (!string.IsNullOrWhiteSpace(option?.ShortName)) + { + optionTypeByOptionName[option!.ShortName!] = item.ParameterType; + } + } + + var argumentDictionary = ParseArgument(args, argsOffset, optionTypeByOptionName); invokeArgs = new object[parameters.Length]; for (int i = 0; i < parameters.Length; i++) { var item = parameters[i]; var option = item.GetCustomAttribute(); + if (!string.IsNullOrWhiteSpace(option?.ShortName) && char.IsDigit(option!.ShortName, 0)) throw new InvalidOperationException($"Option '{item.Name}' has a short name, but the short name must start with A-Z or a-z."); var value = default(OptionParameter); if (option != null && option.Index != -1) { - value = new OptionParameter { Value = args[argsOffset + i] }; + if (argsOffset + i < args.Length) + { + value = new OptionParameter { Value = args[argsOffset + i] }; + } } if (value.Value != null || argumentDictionary.TryGetValue(item.Name, out value) || argumentDictionary.TryGetValue(option?.ShortName?.TrimStart('-') ?? "", out value)) @@ -277,7 +295,7 @@ bool TryGetInvokeArguments(ParameterInfo[] parameters, string?[] args, int argsO } catch { - errorMessage = "Parameter \"" + item.Name + "\"" + " fail on JSON deserialize, plaease check type or JSON escape or add double-quotation."; + errorMessage = "Parameter \"" + item.Name + "\"" + " fail on JSON deserialize, please check type or JSON escape or add double-quotation."; return false; } } @@ -290,7 +308,7 @@ bool TryGetInvokeArguments(ParameterInfo[] parameters, string?[] args, int argsO } catch { - errorMessage = "Parameter \"" + item.Name + "\"" + " fail on JSON deserialize, plaease check type or JSON escape or add double-quotation."; + errorMessage = "Parameter \"" + item.Name + "\"" + " fail on JSON deserialize, please check type or JSON escape or add double-quotation."; return false; } } @@ -335,7 +353,7 @@ bool TryGetInvokeArguments(ParameterInfo[] parameters, string?[] args, int argsO return null; } - static ReadOnlyDictionary ParseArgument(string?[] args, int argsOffset) + static ReadOnlyDictionary ParseArgument(string?[] args, int argsOffset, IReadOnlyDictionary optionTypeByName) { var dict = new Dictionary(args.Length, StringComparer.OrdinalIgnoreCase); for (int i = argsOffset; i < args.Length;) @@ -347,21 +365,19 @@ static ReadOnlyDictionary ParseArgument(string?[] args, } key = key.TrimStart('-'); - if (i >= args.Length) - { - dict.Add(key, new OptionParameter { BooleanSwitch = true }); // Last parameter - break; - } - var value = args[i]; - if (value != null && !value.StartsWith("-")) - { - dict.Add(key, new OptionParameter { Value = value }); - i++; - } - else + if (optionTypeByName.TryGetValue(key, out var optionType)) { - dict.Add(key, new OptionParameter { BooleanSwitch = true }); + if (optionType == typeof(bool)) + { + dict.Add(key, new OptionParameter { BooleanSwitch = true }); + } + else + { + var value = args[i]; + dict.Add(key, new OptionParameter { Value = value }); + i++; + } } } diff --git a/tests/ConsoleAppFramework.Integration.Test/AssemblyInfo.cs b/tests/ConsoleAppFramework.Integration.Test/AssemblyInfo.cs new file mode 100644 index 0000000..b4c10db --- /dev/null +++ b/tests/ConsoleAppFramework.Integration.Test/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using Xunit; + +// NOTE: This test project contains integration tests that use `Console.Out` directly. Therefore, the tests must be run sequentially. +[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true)] \ No newline at end of file diff --git a/tests/ConsoleAppFramework.Integration.Test/CaptureConsoleOutput.cs b/tests/ConsoleAppFramework.Integration.Test/CaptureConsoleOutput.cs new file mode 100644 index 0000000..3816e8a --- /dev/null +++ b/tests/ConsoleAppFramework.Integration.Test/CaptureConsoleOutput.cs @@ -0,0 +1,25 @@ +using System; +using System.IO; + +namespace ConsoleAppFramework.Integration.Test +{ + public class CaptureConsoleOutput : IDisposable + { + private readonly TextWriter _originalWriter; + private readonly StringWriter _stringWriter; + + public CaptureConsoleOutput() + { + _originalWriter = Console.Out; + _stringWriter = new StringWriter(); + Console.SetOut(_stringWriter); + } + + public string Output => _stringWriter.ToString(); + + public void Dispose() + { + Console.SetOut(_originalWriter); + } + } +} \ No newline at end of file diff --git a/tests/ConsoleAppFramework.Integration.Test/ConsoleAppFramework.Integration.Test.csproj b/tests/ConsoleAppFramework.Integration.Test/ConsoleAppFramework.Integration.Test.csproj new file mode 100644 index 0000000..588dd9e --- /dev/null +++ b/tests/ConsoleAppFramework.Integration.Test/ConsoleAppFramework.Integration.Test.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + + diff --git a/tests/ConsoleAppFramework.Integration.Test/MultipleCommandTest.cs b/tests/ConsoleAppFramework.Integration.Test/MultipleCommandTest.cs new file mode 100644 index 0000000..48bc8d1 --- /dev/null +++ b/tests/ConsoleAppFramework.Integration.Test/MultipleCommandTest.cs @@ -0,0 +1,106 @@ +using System; +using FluentAssertions; +using Microsoft.Extensions.Hosting; +using Xunit; + +// ReSharper disable InconsistentNaming + +namespace ConsoleAppFramework.Integration.Test +{ + public partial class MultipleCommandTest + { + [Fact] + public void NoCommandAttribute() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Found more than one public methods(without command)."); + } + + public class CommandTests_Multiple_NoCommandAttribute : ConsoleAppBase + { + public void Hello() => Console.WriteLine("Hello"); + public void Konnichiwa() => Console.WriteLine("Konnichiwa"); + } + + [Fact] + public void Commands() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Usage:"); + console.Output.Should().Contain("Commands:"); + console.Output.Should().Contain("hello"); + console.Output.Should().Contain("konnichiwa"); + } + + [Fact] + public void Commands_UnknownCommand() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "unknown-command" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Usage:"); + console.Output.Should().Contain("Commands:"); + console.Output.Should().Contain("hello"); + console.Output.Should().Contain("konnichiwa"); + } + + [Fact] + public void Commands_UnknownCommand_Help() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "help", "-foo", "-bar" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Usage:"); + console.Output.Should().Contain("Commands:"); + console.Output.Should().Contain("hello"); + console.Output.Should().Contain("konnichiwa"); + } + + public class CommandTests_Multiple_Commands : ConsoleAppBase + { + [Command("hello")] + public void Hello() => Console.WriteLine("Hello"); + [Command("konnichiwa")] + public void Konnichiwa() => Console.WriteLine("Konnichiwa"); + } + + [Fact] + public void OptionAndArg() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "hello", "Cysharp" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello Cysharp (18)"); + } + + [Fact] + public void OptionAndArg_Option() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "hello", "Cysharp", "-age", "-128" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello Cysharp (-128)"); + } + + [Fact] + public void OptionAndArg_HelpOptionLike() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "hello", "-help", "-age", "-128" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello -help (-128)"); + } + + public class CommandTests_Multiple_OptionAndArg : ConsoleAppBase + { + [Command("hello")] + public void Hello([Option(0)]string name, int age = 18) => Console.WriteLine($"Hello {name} ({age})"); + [Command("konnichiwa")] + public void Konnichiwa() => Console.WriteLine("Konnichiwa"); + } + } +} diff --git a/tests/ConsoleAppFramework.Integration.Test/SingleCommandTest.Arguments.cs b/tests/ConsoleAppFramework.Integration.Test/SingleCommandTest.Arguments.cs new file mode 100644 index 0000000..f522dbc --- /dev/null +++ b/tests/ConsoleAppFramework.Integration.Test/SingleCommandTest.Arguments.cs @@ -0,0 +1,101 @@ +using System; +using FluentAssertions; +using Microsoft.Extensions.Hosting; +using Xunit; + +// ReSharper disable InconsistentNaming + +namespace ConsoleAppFramework.Integration.Test +{ + public partial class SingleCommandTest + { + [Fact] + public void NoOptions_OneRequiredArg() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "Cysharp" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello Cysharp"); + } + + [Fact] + public void NoOptions_OneRequiredArg_ArgHelp() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "help" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello help"); + // console.GetOutputText().Should().Contain("Usage:"); + // console.GetOutputText().Should().Contain("Arguments:"); + } + + [Fact] + public void NoOptions_OneRequiredArg_Insufficient() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Usage:"); + console.Output.Should().Contain("Arguments:"); + } + + [Fact] + public void NoOptions_OneRequiredArg_Help() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "-help" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Usage:"); + console.Output.Should().Contain("Arguments:"); + } + + public class CommandTests_Single_NoOptions_OneRequiredArg : ConsoleAppBase + { + public void Hello([Option(0)]string name) => Console.WriteLine($"Hello {name}"); + } + + [Fact] + public void NoOptions_OneOptionalArg() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "Cysharp" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello Cysharp"); + } + + [Fact] + public void NoOptions_OneOptionalArg_ArgHelp() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "help" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello help"); + // console.GetOutputText().Should().Contain("Usage:"); + // console.GetOutputText().Should().Contain("Arguments:"); + } + + [Fact] + public void NoOptions_OneOptionalArg_NoInputArg() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello Anonymous"); + } + + [Fact] + public void NoOptions_OneOptionalArg_Help() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "-help" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Usage:"); + console.Output.Should().Contain("Arguments:"); + } + + public class CommandTests_Single_NoOptions_OneOptionalArgs : ConsoleAppBase + { + public void Hello([Option(0)]string name = "Anonymous") => Console.WriteLine($"Hello {name}"); + } + } +} diff --git a/tests/ConsoleAppFramework.Integration.Test/SingleCommandTest.Options.cs b/tests/ConsoleAppFramework.Integration.Test/SingleCommandTest.Options.cs new file mode 100644 index 0000000..a12f037 --- /dev/null +++ b/tests/ConsoleAppFramework.Integration.Test/SingleCommandTest.Options.cs @@ -0,0 +1,207 @@ +using System; +using FluentAssertions; +using Microsoft.Extensions.Hosting; +using Xunit; + +// ReSharper disable InconsistentNaming + +namespace ConsoleAppFramework.Integration.Test +{ + public partial class SingleCommandTest + { + [Fact] + public void OneRequiredOption_NoArgs() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "-name", "Cysharp" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello Cysharp"); + } + + [Fact] + public void OneRequiredOption_NoArgs_OptionLikeValue() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "-name", "-help" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello -help"); + } + + [Fact] + public void OneRequiredOption_NoArgs_Insufficient() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Usage:"); + console.Output.Should().Contain("Options:"); + } + + [Fact] + public void OneRequiredOption_NoArgs_Help() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "-help" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Usage:"); + console.Output.Should().Contain("Options:"); + } + + public class CommandTests_Single_OneRequiredOption_NoArgs : ConsoleAppBase + { + public void Hello(string name) => Console.WriteLine($"Hello {name}"); + } + + [Fact] + public void OneRequiredOneOptionalOptions_NoArgs_0() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "-name", "Cysharp" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello Cysharp (17)"); + } + + [Fact] + public void OneRequiredOneOptionalOptions_NoArgs_1() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "-name", "Cysharp", "-age", "256" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello Cysharp (256)"); + } + + [Fact] + public void OneRequiredOneOptionalOptions_NoArgs_OptionLikeValue() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "-name", "-help", "-age", "256" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello -help (256)"); + } + + [Fact] + public void OneRequiredOneOptionalOptions_NoArgs_Insufficient() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Usage:"); + console.Output.Should().Contain("Options:"); + } + + [Fact] + public void OneRequiredOneOptionalOptions_NoArgs_Help() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "-help" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Usage:"); + console.Output.Should().Contain("Options:"); + } + + public class CommandTests_Single_OneRequiredOneOptionalOptions_NoArgs : ConsoleAppBase + { + public void Hello(string name, int age = 17) => Console.WriteLine($"Hello {name} ({age})"); + } + + + [Fact] + public void TwoOptionalOptions_NoArgs_0() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "-name", "Cysharp" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello Cysharp (17)"); + } + + [Fact] + public void TwoOptionalOptions_NoArgs_1() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "-name", "Cysharp", "-age", "256" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello Cysharp (256)"); + } + + [Fact] + public void TwoOptionalOptions_NoArgs_2() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "-age", "-256" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello Anonymous (-256)"); + } + + [Fact] + public void TwoOptionalOptions_NoArgs_Ambiguous() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "-name", "-help", "-age", "256" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello -help (256)"); + // console.GetOutputText().Should().Contain("Usage:"); + // console.GetOutputText().Should().Contain("Options:"); + } + + [Fact] + public void TwoOptionalOptions_NoArgs_Help() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "-help" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Usage:"); + console.Output.Should().Contain("Options:"); + } + + [Fact] + public void TwoOptionalOptions_NoArgs_AllDefaultValue() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello Anonymous (17)"); + } + + public class CommandTests_Single_TwoOptionalOptions_NoArgs : ConsoleAppBase + { + public void Hello(string name = "Anonymous", int age = 17) => Console.WriteLine($"Hello {name} ({age})"); + } + + [Fact] + public void RequiredBoolAndOtherOption_NoArgs() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "-hello", "-name", "Cysharp" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello Cysharp"); + } + + public class CommandTests_Single_RequiredBoolAndOtherOption_NoArgs : ConsoleAppBase + { + public void Hello(bool hello, string name) => Console.WriteLine($"{(hello ? "Hello" : "Konnichiwa")} {name}"); + } + + [Fact] + public void OptionalBoolAndRequiredOtherOption_NoArgs() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "-name", "Cysharp" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Konnichiwa Cysharp"); + } + + [Fact] + public void OptionalBoolAndRequiredOtherOption_NoArgs_1() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "-hello", "-name", "Cysharp" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello Cysharp"); + } + + public class CommandTests_Single_OptionalBoolAndRequiredOtherOption_NoArgs : ConsoleAppBase + { + public void Hello(string name, bool hello = false) => Console.WriteLine($"{(hello ? "Hello" : "Konnichiwa")} {name}"); + } + } +} diff --git a/tests/ConsoleAppFramework.Integration.Test/SingleCommandTest.OptionsAndArguments.cs b/tests/ConsoleAppFramework.Integration.Test/SingleCommandTest.OptionsAndArguments.cs new file mode 100644 index 0000000..822463c --- /dev/null +++ b/tests/ConsoleAppFramework.Integration.Test/SingleCommandTest.OptionsAndArguments.cs @@ -0,0 +1,97 @@ +using System; +using FluentAssertions; +using Microsoft.Extensions.Hosting; +using Xunit; + +// ReSharper disable InconsistentNaming + +namespace ConsoleAppFramework.Integration.Test +{ + public partial class SingleCommandTest + { + [Fact] + public void OneRequiredOption_OneRequiredArg() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "Cysharp", "-age", "18" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Cysharp (18)"); + } + + [Fact] + public void OneRequiredOption_OneRequiredArg_OptionLikeValueArg() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "--C--", "-age", "18" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("--C-- (18)"); + } + + [Fact] + public void OneRequiredOption_OneRequiredArg_Insufficient() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Usage:"); + console.Output.Should().Contain("Options:"); + } + + [Fact] + public void OneRequiredOption_OneRequiredArg_Insufficient_Options() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "Cysharp" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Required parameter \"age\""); + } + + [Fact] + public void OneRequiredOption_OneRequiredArg_Help() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "-help" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Usage:"); + console.Output.Should().Contain("Options:"); + } + + public class CommandTests_OneRequiredOption_OneRequiredArg : ConsoleAppBase + { + public void Hello([Option(0)]string name, int age) => Console.WriteLine($"{name} ({age})"); + } + + [Fact] + public void OneOptionalOption_OneRequiredArg() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "Cysharp" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Cysharp (17)"); + } + + [Fact] + public void OneOptionalOption_OneRequiredArg_Option() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "Cysharp", "-age", "18" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Cysharp (18)"); + } + + [Fact] + public void OneOptionalOption_OneRequiredArg_Help() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "-help" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Usage:"); + console.Output.Should().Contain("Options:"); + } + + public class CommandTests_OneOptionalOption_OneRequiredArg : ConsoleAppBase + { + public void Hello([Option(0)]string name, int age = 17) => Console.WriteLine($"{name} ({age})"); + } + } +} diff --git a/tests/ConsoleAppFramework.Integration.Test/SingleCommandTest.cs b/tests/ConsoleAppFramework.Integration.Test/SingleCommandTest.cs new file mode 100644 index 0000000..6399fc6 --- /dev/null +++ b/tests/ConsoleAppFramework.Integration.Test/SingleCommandTest.cs @@ -0,0 +1,26 @@ +using System; +using FluentAssertions; +using Microsoft.Extensions.Hosting; +using Xunit; + +// ReSharper disable InconsistentNaming + +namespace ConsoleAppFramework.Integration.Test +{ + public partial class SingleCommandTest + { + [Fact] + public void NoOptions_NoArgs() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello"); + } + + public class CommandTests_Single_NoOptions_NoArgs : ConsoleAppBase + { + public void Hello() => Console.WriteLine("Hello"); + } + } +} From 93d6cf3f22c80d8c5a33ceab46287eafadbef728 Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Fri, 14 Feb 2020 17:25:11 +0900 Subject: [PATCH 2/3] `help` command is no longer supported if an app has only one command --- ReadMe.md | 8 +- .../ConsoleAppEngineHostBuilderExtensions.cs | 15 ++-- .../MultipleCommandTest.cs | 65 +++++++++++++++ .../NamedSingleCommandTest.cs | 83 +++++++++++++++++++ .../SingleCommandTest.Arguments.cs | 34 +++++++- .../SingleContainedTest.cs | 4 +- 6 files changed, 194 insertions(+), 15 deletions(-) create mode 100644 tests/ConsoleAppFramework.Integration.Test/NamedSingleCommandTest.cs diff --git a/ReadMe.md b/ReadMe.md index def9921..3400b40 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -58,10 +58,10 @@ public void Hello( { ``` -`help` command(or no argument to pass) shows there detail. This help format is same as `dotnet` command. +`-help` option (or no argument to pass) shows there detail. This help format is same as `dotnet` command. ``` -> SampleApp.exe help +> SampleApp.exe -help Usage: SampleApp [options...] Options: @@ -69,10 +69,10 @@ Options: -r, -repeat repeat count. (Default: 3) ``` -`version` command shows `AssemblyInformationalVersion` or `AssemblylVersion`. +`-version` option shows `AssemblyInformationalVersion` or `AssemblylVersion`. ``` -> SampleApp.exe version +> SampleApp.exe -version 1.0.0 ``` diff --git a/src/ConsoleAppFramework/ConsoleAppEngineHostBuilderExtensions.cs b/src/ConsoleAppFramework/ConsoleAppEngineHostBuilderExtensions.cs index a898818..33306ee 100644 --- a/src/ConsoleAppFramework/ConsoleAppEngineHostBuilderExtensions.cs +++ b/src/ConsoleAppFramework/ConsoleAppEngineHostBuilderExtensions.cs @@ -150,26 +150,26 @@ IHostBuilder ConfigureEmptyService() else { // override default Help - args = new string[] { "help" }; + args = new string[] { "--help" }; } } } - if (!hasHelp && args.Length == 1 && TrimEquals(args[0], HelpCommand)) + if (!hasHelp && args.Length == 1 && OptionEquals(args[0], HelpCommand)) { Console.Write(new CommandHelpBuilder().BuildHelpMessage(methods, defaultMethod)); ConfigureEmptyService(); return hostBuilder; } - if (args.Length == 1 && TrimEquals(args[0], VersionCommand)) + if (args.Length == 1 && OptionEquals(args[0], VersionCommand)) { ShowVersion(); ConfigureEmptyService(); return hostBuilder; } - if (args.Length == 2 && methods.Length != 1) + if (args.Length == 2 && methods.Length > 0 && defaultMethod == null) { int methodIndex = -1; @@ -179,7 +179,7 @@ IHostBuilder ConfigureEmptyService() methodIndex = 1; } // command -help - else if (TrimEquals(args[1], HelpCommand)) + else if (OptionEquals(args[1], HelpCommand)) { methodIndex = 0; } @@ -223,6 +223,11 @@ static bool TrimEquals(string arg, string command) return arg.Trim('-').Equals(command, StringComparison.OrdinalIgnoreCase); } + static bool OptionEquals(string arg, string command) + { + return arg.StartsWith("-") && arg.Trim('-').Equals(command, StringComparison.OrdinalIgnoreCase); + } + static void ShowVersion() { var asm = Assembly.GetEntryAssembly(); diff --git a/tests/ConsoleAppFramework.Integration.Test/MultipleCommandTest.cs b/tests/ConsoleAppFramework.Integration.Test/MultipleCommandTest.cs index 48bc8d1..4acf474 100644 --- a/tests/ConsoleAppFramework.Integration.Test/MultipleCommandTest.cs +++ b/tests/ConsoleAppFramework.Integration.Test/MultipleCommandTest.cs @@ -86,15 +86,58 @@ public void OptionAndArg_Option() console.Output.Should().Contain("Hello Cysharp (-128)"); } + [Fact] + public void OptionAndArg_Help() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "hello", "help" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello help (18)"); + } + + [Fact] + public void OptionAndArg_HelpAndOtherArgs() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "hello", "help", "-age", "-128" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + + console.Output.Should().Contain("Hello help (-128)"); + } + [Fact] public void OptionAndArg_HelpOptionLike() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "hello", "-help" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Usage:"); + console.Output.Should().Contain("Arguments:"); + + // NOTE: Currently, ConsoleAppFramework treats the first argument as special. If the argument is '-help', it is same as '-help' option. + //console.Output.Should().Contain("Hello -help (-128)"); + } + + [Fact] + public void OptionAndArg_HelpOptionLikeAndOtherOptions() { using var console = new CaptureConsoleOutput(); var args = new string[] { "hello", "-help", "-age", "-128" }; Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello -help (-128)"); } + [Fact] + public void CommandHelp_OptionAndArg() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "help", "hello" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Usage:"); + console.Output.Should().Contain("Arguments:"); + } + public class CommandTests_Multiple_OptionAndArg : ConsoleAppBase { [Command("hello")] @@ -102,5 +145,27 @@ public class CommandTests_Multiple_OptionAndArg : ConsoleAppBase [Command("konnichiwa")] public void Konnichiwa() => Console.WriteLine("Konnichiwa"); } + + [Fact] + public void OptionHelp() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "-help" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Usage:"); + console.Output.Should().Contain("Commands:"); + console.Output.Should().Contain("hello"); + console.Output.Should().Contain("konnichiwa"); + } + + [Fact] + public void OptionVersion() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "-version" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().MatchRegex(@"\d.\d.\d"); // NOTE: When running with unit test runner, it returns a version of the runner. + } + } } diff --git a/tests/ConsoleAppFramework.Integration.Test/NamedSingleCommandTest.cs b/tests/ConsoleAppFramework.Integration.Test/NamedSingleCommandTest.cs new file mode 100644 index 0000000..6401f75 --- /dev/null +++ b/tests/ConsoleAppFramework.Integration.Test/NamedSingleCommandTest.cs @@ -0,0 +1,83 @@ +using System; +using FluentAssertions; +using Microsoft.Extensions.Hosting; +using Xunit; + +// ReSharper disable InconsistentNaming + +namespace ConsoleAppFramework.Integration.Test +{ + public partial class NamedSingleCommandTest + { + [Fact] + public void NamedCommand_NoArgs_CommandIsNotSpecified() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Usage:"); + console.Output.Should().Contain("Commands:"); + } + + [Fact] + public void NamedCommand_NoArgs_Invoke() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "hello" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello"); + } + + [Fact] + public void NamedCommand_NoArgs_CommandHelp() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "help", "hello" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Usage:"); + console.Output.Should().Contain(" hello"); + } + + public class CommandTests_Single_Named_NoArgs : ConsoleAppBase + { + [Command("hello")] + public void Hello() => Console.WriteLine("Hello"); + } + + [Fact] + public void NamedCommand_OneArg_CommandIsNotSpecified() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Usage:"); + console.Output.Should().Contain("Commands:"); + } + + [Fact] + public void NamedCommand_OneArg_Invoke() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "hello", "Cysharp" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Hello Cysharp"); + } + + [Fact] + public void NamedCommand_OneArg_CommandHelp() + { + using var console = new CaptureConsoleOutput(); + var args = new string[] { "help", "hello" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("Usage:"); + console.Output.Should().Contain("Arguments:"); + } + + public class CommandTests_Single_Named_OneArg : ConsoleAppBase + { + [Command("hello")] + public void Hello([Option(0)]string name) => Console.WriteLine($"Hello {name}"); + } + + } +} diff --git a/tests/ConsoleAppFramework.Integration.Test/SingleCommandTest.Arguments.cs b/tests/ConsoleAppFramework.Integration.Test/SingleCommandTest.Arguments.cs index f522dbc..b5f99af 100644 --- a/tests/ConsoleAppFramework.Integration.Test/SingleCommandTest.Arguments.cs +++ b/tests/ConsoleAppFramework.Integration.Test/SingleCommandTest.Arguments.cs @@ -25,8 +25,6 @@ public void NoOptions_OneRequiredArg_ArgHelp() var args = new[] { "help" }; Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); console.Output.Should().Contain("Hello help"); - // console.GetOutputText().Should().Contain("Usage:"); - // console.GetOutputText().Should().Contain("Arguments:"); } [Fact] @@ -47,6 +45,21 @@ public void NoOptions_OneRequiredArg_Help() Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); console.Output.Should().Contain("Usage:"); console.Output.Should().Contain("Arguments:"); + + // NOTE: Currently, ConsoleAppFramework treats the first argument as special. If the argument is '-help', it is same as '-help' option. + //console.Output.Should().Contain("Hello -version"); + } + + [Fact] + public void NoOptions_OneRequiredArg_Version() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "-version" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().MatchRegex(@"\d.\d.\d"); // NOTE: When running with unit test runner, it returns a version of the runner. + + // NOTE: Currently, ConsoleAppFramework treats the first argument as special. If the argument is '-help', it is same as '-help' option. + //console.Output.Should().Contain("Hello -version"); } public class CommandTests_Single_NoOptions_OneRequiredArg : ConsoleAppBase @@ -70,8 +83,6 @@ public void NoOptions_OneOptionalArg_ArgHelp() var args = new[] { "help" }; Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); console.Output.Should().Contain("Hello help"); - // console.GetOutputText().Should().Contain("Usage:"); - // console.GetOutputText().Should().Contain("Arguments:"); } [Fact] @@ -91,6 +102,21 @@ public void NoOptions_OneOptionalArg_Help() Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); console.Output.Should().Contain("Usage:"); console.Output.Should().Contain("Arguments:"); + + // NOTE: Currently, ConsoleAppFramework treats the first argument as special. If the argument is '-help', it is same as '-help' option. + //console.Output.Should().Contain("Hello -help"); + } + + [Fact] + public void NoOptions_OneOptionalArg_Version() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "-version" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().MatchRegex(@"\d.\d.\d"); // NOTE: When running with unit test runner, it returns a version of the runner. + + // NOTE: Currently, ConsoleAppFramework treats the first argument as special. If the argument is '-help', it is same as '-help' option. + //console.Output.Should().Contain("Hello -version"); } public class CommandTests_Single_NoOptions_OneOptionalArgs : ConsoleAppBase diff --git a/tests/ConsoleAppFramework.Tests/SingleContainedTest.cs b/tests/ConsoleAppFramework.Tests/SingleContainedTest.cs index 5ce1927..f87ad35 100644 --- a/tests/ConsoleAppFramework.Tests/SingleContainedTest.cs +++ b/tests/ConsoleAppFramework.Tests/SingleContainedTest.cs @@ -116,8 +116,8 @@ public async Task SimpleComplexArgsTest() public class TwoArgsWithOption : ConsoleAppBase { public void Hello( - [Option("-n", "name of this")]string name, - [Option("-r", "repeat msg")]int repeat) + [Option("n", "name of this")]string name, + [Option("r", "repeat msg")]int repeat) { Context.Logger.LogInformation($"name:{name}"); Context.Logger.LogInformation($"repeat:{repeat}"); From a9c29120c129e18208d49d9792efa4a2fcb9311c Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Fri, 14 Feb 2020 17:39:18 +0900 Subject: [PATCH 3/3] ConsoleAppFramework.Integration.Test targets netcoreapp3.0 --- .../ConsoleAppFramework.Integration.Test.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ConsoleAppFramework.Integration.Test/ConsoleAppFramework.Integration.Test.csproj b/tests/ConsoleAppFramework.Integration.Test/ConsoleAppFramework.Integration.Test.csproj index 588dd9e..a067f97 100644 --- a/tests/ConsoleAppFramework.Integration.Test/ConsoleAppFramework.Integration.Test.csproj +++ b/tests/ConsoleAppFramework.Integration.Test/ConsoleAppFramework.Integration.Test.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + netcoreapp3.0 false