diff --git a/CliFx.Benchmarks/Benchmarks.cs b/CliFx.Benchmarks/Benchmarks.cs index 48950bf..37a49cd 100644 --- a/CliFx.Benchmarks/Benchmarks.cs +++ b/CliFx.Benchmarks/Benchmarks.cs @@ -18,7 +18,7 @@ namespace CliFx.Benchmarks [Benchmark(Description = "CliFx", Baseline = true)] public async ValueTask ExecuteWithCliFx() => - await new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments, new Dictionary()); + await new CliApplicationBuilder().AddCommand().Build().RunAsync(Arguments, new Dictionary()); [Benchmark(Description = "System.CommandLine")] public async Task ExecuteWithSystemCommandLine() => diff --git a/CliFx.Tests/ApplicationSpecs.Commands.cs b/CliFx.Tests/ApplicationSpecs.Commands.cs deleted file mode 100644 index 91a5e85..0000000 --- a/CliFx.Tests/ApplicationSpecs.Commands.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests -{ - public partial class ApplicationSpecs - { - [Command] - private class DefaultCommand : ICommand - { - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command] - private class AnotherDefaultCommand : ICommand - { - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command] - private class NonImplementedCommand - { - } - - private class NonAnnotatedCommand : ICommand - { - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command("dup")] - private class DuplicateNameCommandA : ICommand - { - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command("dup")] - private class DuplicateNameCommandB : ICommand - { - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command] - private class DuplicateParameterOrderCommand : ICommand - { - [CommandParameter(13)] - public string? ParameterA { get; set; } - - [CommandParameter(13)] - public string? ParameterB { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command] - private class DuplicateParameterNameCommand : ICommand - { - [CommandParameter(0, Name = "param")] - public string? ParameterA { get; set; } - - [CommandParameter(1, Name = "param")] - public string? ParameterB { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command] - private class MultipleNonScalarParametersCommand : ICommand - { - [CommandParameter(0)] - public IReadOnlyList? ParameterA { get; set; } - - [CommandParameter(1)] - public IReadOnlyList? ParameterB { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command] - private class NonLastNonScalarParameterCommand : ICommand - { - [CommandParameter(0)] - public IReadOnlyList? ParameterA { get; set; } - - [CommandParameter(1)] - public string? ParameterB { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command] - private class EmptyOptionNameCommand : ICommand - { - [CommandOption("")] - public string? Apples { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command] - private class SingleCharacterOptionNameCommand : ICommand - { - [CommandOption("a")] - public string? Apples { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command] - private class DuplicateOptionNamesCommand : ICommand - { - [CommandOption("fruits")] - public string? Apples { get; set; } - - [CommandOption("fruits")] - public string? Oranges { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command] - private class DuplicateOptionShortNamesCommand : ICommand - { - [CommandOption('x')] - public string? OptionA { get; set; } - - [CommandOption('x')] - public string? OptionB { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command] - private class ConflictWithHelpOptionCommand : ICommand - { - [CommandOption("option-h", 'h')] - public string? OptionH { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command] - private class ConflictWithVersionOptionCommand : ICommand - { - [CommandOption("version")] - public string? Version { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command] - private class DuplicateOptionEnvironmentVariableNamesCommand : ICommand - { - [CommandOption("option-a", EnvironmentVariableName = "ENV_VAR")] - public string? OptionA { get; set; } - - [CommandOption("option-b", EnvironmentVariableName = "ENV_VAR")] - public string? OptionB { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command("hidden", Description = "Description")] - private class HiddenPropertiesCommand : ICommand - { - [CommandParameter(13, Name = "param", Description = "Param description")] - public string? Parameter { get; set; } - - [CommandOption("option", 'o', Description = "Option description", EnvironmentVariableName = "ENV")] - public string? Option { get; set; } - - public string? HiddenA { get; set; } - - public bool? HiddenB { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/ApplicationSpecs.cs b/CliFx.Tests/ApplicationSpecs.cs index 7d2167c..b5360fa 100644 --- a/CliFx.Tests/ApplicationSpecs.cs +++ b/CliFx.Tests/ApplicationSpecs.cs @@ -1,14 +1,15 @@ using System; using System.IO; -using CliFx.Domain; -using CliFx.Exceptions; +using System.Threading.Tasks; +using CliFx.Tests.Commands; +using CliFx.Tests.Commands.Invalid; using FluentAssertions; using Xunit; using Xunit.Abstractions; namespace CliFx.Tests { - public partial class ApplicationSpecs + public class ApplicationSpecs { private readonly ITestOutputHelper _output; @@ -31,7 +32,7 @@ namespace CliFx.Tests { // Act var app = new CliApplicationBuilder() - .AddCommand(typeof(DefaultCommand)) + .AddCommand() .AddCommandsFrom(typeof(DefaultCommand).Assembly) .AddCommands(new[] {typeof(DefaultCommand)}) .AddCommandsFrom(new[] {typeof(DefaultCommand).Assembly}) @@ -51,219 +52,356 @@ namespace CliFx.Tests } [Fact] - public void At_least_one_command_must_be_defined_in_an_application() + public async Task At_least_one_command_must_be_defined_in_an_application() { - // Arrange - var commandTypes = Array.Empty(); + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); - // Act & assert - var ex = Assert.Throws(() => RootSchema.Resolve(commandTypes)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Commands_must_implement_the_corresponding_interface() - { - // Arrange - var commandTypes = new[] {typeof(NonImplementedCommand)}; - - // Act & assert - var ex = Assert.Throws(() => RootSchema.Resolve(commandTypes)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Commands_must_be_annotated_by_an_attribute() - { - // Arrange - var commandTypes = new[] {typeof(NonAnnotatedCommand)}; - - // Act & assert - var ex = Assert.Throws(() => RootSchema.Resolve(commandTypes)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Commands_must_have_unique_names() - { - // Arrange - var commandTypes = new[] {typeof(DuplicateNameCommandA), typeof(DuplicateNameCommandB)}; - - // Act & assert - var ex = Assert.Throws(() => RootSchema.Resolve(commandTypes)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Command_can_be_default_but_only_if_it_is_the_only_such_command() - { - // Arrange - var commandTypes = new[] {typeof(DefaultCommand), typeof(AnotherDefaultCommand)}; - - // Act & assert - var ex = Assert.Throws(() => RootSchema.Resolve(commandTypes)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Command_parameters_must_have_unique_order() - { - // Arrange - var commandTypes = new[] {typeof(DuplicateParameterOrderCommand)}; - - // Act & assert - var ex = Assert.Throws(() => RootSchema.Resolve(commandTypes)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Command_parameters_must_have_unique_names() - { - // Arrange - var commandTypes = new[] {typeof(DuplicateParameterNameCommand)}; - - // Act & assert - var ex = Assert.Throws(() => RootSchema.Resolve(commandTypes)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Command_parameter_can_be_non_scalar_only_if_no_other_such_parameter_is_present() - { - // Arrange - var commandTypes = new[] {typeof(MultipleNonScalarParametersCommand)}; - - // Act & assert - var ex = Assert.Throws(() => RootSchema.Resolve(commandTypes)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Command_parameter_can_be_non_scalar_only_if_it_is_the_last_in_order() - { - // Arrange - var commandTypes = new[] {typeof(NonLastNonScalarParameterCommand)}; - - // Act & assert - var ex = Assert.Throws(() => RootSchema.Resolve(commandTypes)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Command_options_must_have_names_that_are_not_empty() - { - // Arrange - var commandTypes = new[] {typeof(EmptyOptionNameCommand)}; - - // Act & assert - var ex = Assert.Throws(() => RootSchema.Resolve(commandTypes)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Command_options_must_have_names_that_are_longer_than_one_character() - { - // Arrange - var commandTypes = new[] {typeof(SingleCharacterOptionNameCommand)}; - - // Act & assert - var ex = Assert.Throws(() => RootSchema.Resolve(commandTypes)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Command_options_must_have_unique_names() - { - // Arrange - var commandTypes = new[] {typeof(DuplicateOptionNamesCommand)}; - - // Act & assert - var ex = Assert.Throws(() => RootSchema.Resolve(commandTypes)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Command_options_must_have_unique_short_names() - { - // Arrange - var commandTypes = new[] {typeof(DuplicateOptionShortNamesCommand)}; - - // Act & assert - var ex = Assert.Throws(() => RootSchema.Resolve(commandTypes)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Command_options_must_not_have_conflicts_with_the_implicit_help_option() - { - // Arrange - var commandTypes = new[] {typeof(ConflictWithHelpOptionCommand)}; - - // Act & assert - var ex = Assert.Throws(() => RootSchema.Resolve(commandTypes)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Command_options_must_not_have_conflicts_with_the_implicit_version_option() - { - // Arrange - var commandTypes = new[] {typeof(ConflictWithVersionOptionCommand)}; - - // Act & assert - var ex = Assert.Throws(() => RootSchema.Resolve(commandTypes)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Command_options_must_have_unique_environment_variable_names() - { - // Arrange - var commandTypes = new[] {typeof(DuplicateOptionEnvironmentVariableNamesCommand)}; - - // Act & assert - var ex = Assert.Throws(() => RootSchema.Resolve(commandTypes)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Command_options_and_parameters_must_be_annotated_by_corresponding_attributes() - { - // Arrange - var commandTypes = new[] {typeof(HiddenPropertiesCommand)}; + var application = new CliApplicationBuilder() + .UseConsole(console) + .Build(); // Act - var schema = RootSchema.Resolve(commandTypes); + var exitCode = await application.RunAsync(Array.Empty()); + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); // Assert - schema.Should().BeEquivalentTo(new RootSchema(new[] - { - new CommandSchema( - typeof(HiddenPropertiesCommand), - "hidden", - "Description", - new[] - { - new CommandParameterSchema( - typeof(HiddenPropertiesCommand).GetProperty(nameof(HiddenPropertiesCommand.Parameter))!, - 13, - "param", - "Param description") - }, - new[] - { - new CommandOptionSchema( - typeof(HiddenPropertiesCommand).GetProperty(nameof(HiddenPropertiesCommand.Option))!, - "option", - 'o', - "ENV", - false, - "Option description"), - CommandOptionSchema.HelpOption - }) - })); + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); - schema.ToString().Should().NotBeNullOrWhiteSpace(); // this is only for coverage, I'm sorry + _output.WriteLine(stdErrData); + } + + [Fact] + public async Task Commands_must_implement_the_corresponding_interface() + { + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand(typeof(NonImplementedCommand)) + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(Array.Empty()); + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); + } + + [Fact] + public async Task Commands_must_be_annotated_by_an_attribute() + { + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(Array.Empty()); + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); + } + + [Fact] + public async Task Commands_must_have_unique_names() + { + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(Array.Empty()); + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); + } + + [Fact] + public async Task Command_can_be_default_but_only_if_it_is_the_only_such_command() + { + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(Array.Empty()); + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); + } + + [Fact] + public async Task Command_parameters_must_have_unique_order() + { + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(Array.Empty()); + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); + } + + [Fact] + public async Task Command_parameters_must_have_unique_names() + { + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(Array.Empty()); + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); + } + + [Fact] + public async Task Command_parameter_can_be_non_scalar_only_if_no_other_such_parameter_is_present() + { + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(Array.Empty()); + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); + } + + [Fact] + public async Task Command_parameter_can_be_non_scalar_only_if_it_is_the_last_in_order() + { + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(Array.Empty()); + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); + } + + [Fact] + public async Task Command_options_must_have_names_that_are_not_empty() + { + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(Array.Empty()); + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); + } + + [Fact] + public async Task Command_options_must_have_names_that_are_longer_than_one_character() + { + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(Array.Empty()); + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); + } + + [Fact] + public async Task Command_options_must_have_unique_names() + { + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(Array.Empty()); + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); + } + + [Fact] + public async Task Command_options_must_have_unique_short_names() + { + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(Array.Empty()); + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); + } + + [Fact] + public async Task Command_options_must_have_unique_environment_variable_names() + { + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(Array.Empty()); + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); + } + + [Fact] + public async Task Command_options_must_not_have_conflicts_with_the_implicit_help_option() + { + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(Array.Empty()); + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); + } + + [Fact] + public async Task Command_options_must_not_have_conflicts_with_the_implicit_version_option() + { + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(Array.Empty()); + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); } } } \ No newline at end of file diff --git a/CliFx.Tests/ArgumentBindingSpecs.Commands.cs b/CliFx.Tests/ArgumentBindingSpecs.Commands.cs deleted file mode 100644 index 55e25d5..0000000 --- a/CliFx.Tests/ArgumentBindingSpecs.Commands.cs +++ /dev/null @@ -1,197 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests -{ - public partial class ArgumentBindingSpecs - { - [Command] - private class AllSupportedTypesCommand : ICommand - { - [CommandOption(nameof(Object))] - public object? Object { get; set; } = 42; - - [CommandOption(nameof(String))] - public string? String { get; set; } = "foo bar"; - - [CommandOption(nameof(Bool))] - public bool Bool { get; set; } - - [CommandOption(nameof(Char))] - public char Char { get; set; } - - [CommandOption(nameof(Sbyte))] - public sbyte Sbyte { get; set; } - - [CommandOption(nameof(Byte))] - public byte Byte { get; set; } - - [CommandOption(nameof(Short))] - public short Short { get; set; } - - [CommandOption(nameof(Ushort))] - public ushort Ushort { get; set; } - - [CommandOption(nameof(Int))] - public int Int { get; set; } - - [CommandOption(nameof(Uint))] - public uint Uint { get; set; } - - [CommandOption(nameof(Long))] - public long Long { get; set; } - - [CommandOption(nameof(Ulong))] - public ulong Ulong { get; set; } - - [CommandOption(nameof(Float))] - public float Float { get; set; } - - [CommandOption(nameof(Double))] - public double Double { get; set; } - - [CommandOption(nameof(Decimal))] - public decimal Decimal { get; set; } - - [CommandOption(nameof(DateTime))] - public DateTime DateTime { get; set; } - - [CommandOption(nameof(DateTimeOffset))] - public DateTimeOffset DateTimeOffset { get; set; } - - [CommandOption(nameof(TimeSpan))] - public TimeSpan TimeSpan { get; set; } - - [CommandOption(nameof(CustomEnum))] - public CustomEnum CustomEnum { get; set; } - - [CommandOption(nameof(IntNullable))] - public int? IntNullable { get; set; } - - [CommandOption(nameof(CustomEnumNullable))] - public CustomEnum? CustomEnumNullable { get; set; } - - [CommandOption(nameof(TimeSpanNullable))] - public TimeSpan? TimeSpanNullable { get; set; } - - [CommandOption(nameof(TestStringConstructable))] - public StringConstructable? TestStringConstructable { get; set; } - - [CommandOption(nameof(TestStringParseable))] - public StringParseable? TestStringParseable { get; set; } - - [CommandOption(nameof(TestStringParseableWithFormatProvider))] - public StringParseableWithFormatProvider? TestStringParseableWithFormatProvider { get; set; } - - [CommandOption(nameof(ObjectArray))] - public object[]? ObjectArray { get; set; } - - [CommandOption(nameof(StringArray))] - public string[]? StringArray { get; set; } - - [CommandOption(nameof(IntArray))] - public int[]? IntArray { get; set; } - - [CommandOption(nameof(CustomEnumArray))] - public CustomEnum[]? CustomEnumArray { get; set; } - - [CommandOption(nameof(IntNullableArray))] - public int?[]? IntNullableArray { get; set; } - - [CommandOption(nameof(TestStringConstructableArray))] - public StringConstructable[]? TestStringConstructableArray { get; set; } - - [CommandOption(nameof(Enumerable))] - public IEnumerable? Enumerable { get; set; } - - [CommandOption(nameof(StringEnumerable))] - public IEnumerable? StringEnumerable { get; set; } - - [CommandOption(nameof(StringReadOnlyList))] - public IReadOnlyList? StringReadOnlyList { get; set; } - - [CommandOption(nameof(StringList))] - public List? StringList { get; set; } - - [CommandOption(nameof(StringHashSet))] - public HashSet? StringHashSet { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command] - private class ArrayOptionCommand : ICommand - { - [CommandOption("option", 'o')] - public IReadOnlyList? Option { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command] - private class RequiredOptionCommand : ICommand - { - [CommandOption(nameof(Option), IsRequired = true)] - public string? Option { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command] - private class RequiredArrayOptionCommand : ICommand - { - [CommandOption(nameof(Option), IsRequired = true)] - public IReadOnlyList? Option { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command] - private class ParametersCommand : ICommand - { - [CommandParameter(0)] - public string? ParameterA { get; set; } - - [CommandParameter(1)] - public string? ParameterB { get; set; } - - [CommandParameter(2)] - public IReadOnlyList? ParameterC { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command] - private class UnsupportedPropertyTypeCommand : ICommand - { - [CommandOption(nameof(Option))] - public DummyType? Option { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command] - private class UnsupportedEnumerablePropertyTypeCommand : ICommand - { - [CommandOption(nameof(Option))] - public CustomEnumerable? Option { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command] - private class NoParameterCommand : ICommand - { - [CommandOption(nameof(OptionA))] - public string? OptionA { get; set; } - - [CommandOption(nameof(OptionB))] - public string? OptionB { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/ArgumentBindingSpecs.Types.cs b/CliFx.Tests/ArgumentBindingSpecs.Types.cs deleted file mode 100644 index 7d751ae..0000000 --- a/CliFx.Tests/ArgumentBindingSpecs.Types.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -namespace CliFx.Tests -{ - public partial class ArgumentBindingSpecs - { - private enum CustomEnum - { - Value1 = 1, - Value2 = 2, - Value3 = 3 - } - - private class StringConstructable - { - public string Value { get; } - - public StringConstructable(string value) => Value = value; - } - - private class StringParseable - { - public string Value { get; } - - private StringParseable(string value) => Value = value; - - public static StringParseable Parse(string value) => new StringParseable(value); - } - - private class StringParseableWithFormatProvider - { - public string Value { get; } - - private StringParseableWithFormatProvider(string value) => Value = value; - - public static StringParseableWithFormatProvider Parse(string value, IFormatProvider formatProvider) => - new StringParseableWithFormatProvider(value + " " + formatProvider); - } - - private class DummyType - { - } - - public class CustomEnumerable : IEnumerable - { - public IEnumerator GetEnumerator() => ((IEnumerable) Array.Empty()).GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/ArgumentBindingSpecs.cs b/CliFx.Tests/ArgumentBindingSpecs.cs index e014164..503f868 100644 --- a/CliFx.Tests/ArgumentBindingSpecs.cs +++ b/CliFx.Tests/ArgumentBindingSpecs.cs @@ -1,1018 +1,269 @@ -using System; -using System.Collections.Generic; -using System.Globalization; +using System.IO; +using System.Threading.Tasks; using CliFx.Exceptions; -using CliFx.Tests.Internal; +using CliFx.Tests.Commands; using FluentAssertions; +using Newtonsoft.Json; using Xunit; using Xunit.Abstractions; namespace CliFx.Tests { - public partial class ArgumentBindingSpecs + public class ArgumentBindingSpecs { private readonly ITestOutputHelper _output; public ArgumentBindingSpecs(ITestOutputHelper output) => _output = output; [Fact] - public void Property_of_type_object_is_bound_directly_from_the_argument_value() + public async Task Property_annotated_as_an_option_can_be_bound_from_multiple_values_even_if_the_inputs_use_mixed_naming() { // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.Object), "value") + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) .Build(); // Act - var instance = CommandHelper.ResolveCommand(input); + var exitCode = await application.RunAsync(new[] + { + "cmd", "--opt", "foo", "-o", "bar", "--opt", "baz" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new WithStringArrayOptionCommand { - Object = "value" + Opt = new[] {"foo", "bar", "baz"} }); } [Fact] - public void Property_of_type_object_array_is_bound_directly_from_the_argument_values() + public async Task Property_annotated_as_a_required_option_must_always_be_set() { // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.ObjectArray), "foo", "bar") + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) .Build(); // Act - var instance = CommandHelper.ResolveCommand(input); + var exitCode = await application.RunAsync(new[] + { + "cmd", "--opt-a", "foo" + }); + + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); + } + + [Fact] + public async Task Property_annotated_as_a_required_option_must_always_be_bound_to_some_value() + { + // Arrange + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] { - ObjectArray = new object[] {"foo", "bar"} + "cmd", "--opt-a" + }); + + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); + } + + [Fact] + public async Task Property_annotated_as_a_required_option_must_always_be_bound_to_at_least_one_value_if_it_expects_multiple_values() + { + // Arrange + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--opt-a", "foo" + }); + + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); + } + + [Fact] + public async Task Property_annotated_as_parameter_is_bound_directly_from_argument_value_according_to_the_order() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "foo", "13", "bar", "baz" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new WithParametersCommand + { + ParamA = "foo", + ParamB = 13, + ParamC = new[] {"bar", "baz"} }); } [Fact] - public void Property_of_type_non_generic_IEnumerable_is_bound_directly_from_the_argument_values() + public async Task Property_annotated_as_parameter_must_always_be_bound_to_some_value() { // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.Enumerable), "foo", "bar") + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) .Build(); // Act - var instance = CommandHelper.ResolveCommand(input); + var exitCode = await application.RunAsync(new[] + { + "cmd" + }); + + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - Enumerable = new object[] {"foo", "bar"} - }); + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); } [Fact] - public void Property_of_type_string_is_bound_directly_from_the_argument_value() + public async Task Property_annotated_as_parameter_must_always_be_bound_to_at_least_one_value_if_it_expects_multiple_values() { // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.String), "value") + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) .Build(); // Act - var instance = CommandHelper.ResolveCommand(input); + var exitCode = await application.RunAsync(new[] + { + "cmd", "foo", "13" + }); + + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - String = "value" - }); + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); } [Fact] - public void Property_of_type_string_array_is_bound_directly_from_the_argument_values() + public async Task All_provided_option_arguments_must_be_bound_to_corresponding_properties() { // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.StringArray), "foo", "bar") + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) .Build(); // Act - var instance = CommandHelper.ResolveCommand(input); + var exitCode = await application.RunAsync(new[] + { + "cmd", "--non-existing-option", "13" + }); + + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - StringArray = new[] {"foo", "bar"} - }); + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); } [Fact] - public void Property_of_type_string_IEnumerable_is_bound_directly_from_the_argument_values() + public async Task All_provided_parameter_arguments_must_be_bound_to_corresponding_properties() { // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.StringEnumerable), "foo", "bar") + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) .Build(); // Act - var instance = CommandHelper.ResolveCommand(input); + var exitCode = await application.RunAsync(new[] + { + "cnd", "non-existing-parameter" + }); + + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - StringEnumerable = new[] {"foo", "bar"} - }); - } + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); - [Fact] - public void Property_of_type_string_IReadOnlyList_is_bound_directly_from_the_argument_values() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.StringReadOnlyList), "foo", "bar") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - StringReadOnlyList = new[] {"foo", "bar"} - }); - } - - [Fact] - public void Property_of_type_string_List_is_bound_directly_from_the_argument_values() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.StringList), "foo", "bar") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - StringList = new List {"foo", "bar"} - }); - } - - [Fact] - public void Property_of_type_string_HashSet_is_bound_directly_from_the_argument_values() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.StringHashSet), "foo", "bar") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - StringHashSet = new HashSet(new[] {"foo", "bar"}) - }); - } - - [Fact] - public void Property_of_type_bool_is_bound_as_true_if_the_argument_value_is_true() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.Bool), "true") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - Bool = true - }); - } - - [Fact] - public void Property_of_type_bool_is_bound_as_false_if_the_argument_value_is_false() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.Bool), "false") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - Bool = false - }); - } - - [Fact] - public void Property_of_type_bool_is_bound_as_true_if_the_argument_value_is_not_set() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.Bool)) - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - Bool = true - }); - } - - [Fact] - public void Property_of_type_char_is_bound_directly_from_the_argument_value_if_it_contains_only_one_character() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.Char), "a") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - Char = 'a' - }); - } - - [Fact] - public void Property_of_type_sbyte_is_bound_by_parsing_the_argument_value() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.Sbyte), "15") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - Sbyte = 15 - }); - } - - [Fact] - public void Property_of_type_byte_is_bound_by_parsing_the_argument_value() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.Byte), "15") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - Byte = 15 - }); - } - - [Fact] - public void Property_of_type_short_is_bound_by_parsing_the_argument_value() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.Short), "15") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - Short = 15 - }); - } - - [Fact] - public void Property_of_type_ushort_is_bound_by_parsing_the_argument_value() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.Ushort), "15") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - Ushort = 15 - }); - } - - [Fact] - public void Property_of_type_int_is_bound_by_parsing_the_argument_value() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.Int), "15") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - Int = 15 - }); - } - - [Fact] - public void Property_of_type_nullable_int_is_bound_by_parsing_the_argument_value_if_it_is_set() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.IntNullable), "15") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - IntNullable = 15 - }); - } - - [Fact] - public void Property_of_type_nullable_int_is_bound_as_null_if_the_argument_value_is_not_set() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.IntNullable)) - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - IntNullable = null - }); - } - - [Fact] - public void Property_of_type_int_array_is_bound_by_parsing_the_argument_values() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.IntArray), "3", "14") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - IntArray = new[] {3, 14} - }); - } - - [Fact] - public void Property_of_type_nullable_int_array_is_bound_by_parsing_the_argument_values() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.IntNullableArray), "3", "14") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - IntNullableArray = new int?[] {3, 14} - }); - } - - [Fact] - public void Property_of_type_uint_is_bound_by_parsing_the_argument_value() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.Uint), "15") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - Uint = 15 - }); - } - - [Fact] - public void Property_of_type_long_is_bound_by_parsing_the_argument_value() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.Long), "15") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - Long = 15 - }); - } - - [Fact] - public void Property_of_type_ulong_is_bound_by_parsing_the_argument_value() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.Ulong), "15") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - Ulong = 15 - }); - } - - [Fact] - public void Property_of_type_float_is_bound_by_parsing_the_argument_value() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.Float), "123.45") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - Float = 123.45F - }); - } - - [Fact] - public void Property_of_type_double_is_bound_by_parsing_the_argument_value() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.Double), "123.45") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - Double = 123.45 - }); - } - - [Fact] - public void Property_of_type_decimal_is_bound_by_parsing_the_argument_value() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.Decimal), "123.45") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - Decimal = 123.45M - }); - } - - [Fact] - public void Property_of_type_DateTime_is_bound_by_parsing_the_argument_value() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.DateTime), "28 Apr 1995") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - DateTime = new DateTime(1995, 04, 28) - }); - } - - [Fact] - public void Property_of_type_DateTimeOffset_is_bound_by_parsing_the_argument_value() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.DateTimeOffset), "28 Apr 1995") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - DateTimeOffset = new DateTime(1995, 04, 28) - }); - } - - [Fact] - public void Property_of_type_TimeSpan_is_bound_by_parsing_the_argument_value() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.TimeSpan), "00:14:59") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - TimeSpan = new TimeSpan(00, 14, 59) - }); - } - - [Fact] - public void Property_of_type_nullable_TimeSpan_is_bound_by_parsing_the_argument_value_if_it_is_set() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.TimeSpanNullable), "00:14:59") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - TimeSpanNullable = new TimeSpan(00, 14, 59) - }); - } - - [Fact] - public void Property_of_type_nullable_TimeSpan_is_bound_as_null_if_the_argument_value_is_not_set() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.TimeSpanNullable)) - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - TimeSpanNullable = null - }); - } - - [Fact] - public void Property_of_an_enum_type_is_bound_by_parsing_the_argument_value_as_name() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.CustomEnum), "value2") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - CustomEnum = CustomEnum.Value2 - }); - } - - [Fact] - public void Property_of_an_enum_type_is_bound_by_parsing_the_argument_value_as_id() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.CustomEnum), "2") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - CustomEnum = CustomEnum.Value2 - }); - } - - [Fact] - public void Property_of_a_nullable_enum_type_is_bound_by_parsing_the_argument_value_as_name_if_it_is_set() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.CustomEnumNullable), "value3") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - CustomEnumNullable = CustomEnum.Value3 - }); - } - - [Fact] - public void Property_of_a_nullable_enum_type_is_bound_by_parsing_the_argument_value_as_id_if_it_is_set() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.CustomEnumNullable), "3") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - CustomEnumNullable = CustomEnum.Value3 - }); - } - - [Fact] - public void Property_of_a_nullable_enum_type_is_bound_as_null_if_the_argument_value_is_not_set() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.CustomEnumNullable)) - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - CustomEnumNullable = null - }); - } - - [Fact] - public void Property_of_an_enum_array_type_is_bound_by_parsing_the_argument_values_as_names() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.CustomEnumArray), "value1", "value3") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - CustomEnumArray = new[] {CustomEnum.Value1, CustomEnum.Value3} - }); - } - - [Fact] - public void Property_of_an_enum_array_type_is_bound_by_parsing_the_argument_values_as_ids() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.CustomEnumArray), "1", "3") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - CustomEnumArray = new[] {CustomEnum.Value1, CustomEnum.Value3} - }); - } - - [Fact] - public void Property_of_an_enum_array_type_is_bound_by_parsing_the_argument_values_as_either_names_or_ids() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.CustomEnumArray), "value1", "3") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - CustomEnumArray = new[] {CustomEnum.Value1, CustomEnum.Value3} - }); - } - - [Fact] - public void Property_of_a_type_that_has_a_constructor_accepting_a_string_is_bound_by_invoking_the_constructor_with_the_argument_value() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.TestStringConstructable), "foobar") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - TestStringConstructable = new StringConstructable("foobar") - }); - } - - [Fact] - public void Property_of_an_array_of_type_that_has_a_constructor_accepting_a_string_is_bound_by_invoking_the_constructor_with_the_argument_values() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.TestStringConstructableArray), "foo", "bar") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - TestStringConstructableArray = new[] {new StringConstructable("foo"), new StringConstructable("bar") } - }); - } - - [Fact] - public void Property_of_a_type_that_has_a_static_Parse_method_accepting_a_string_is_bound_by_invoking_the_method() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.TestStringParseable), "foobar") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - TestStringParseable = StringParseable.Parse("foobar") - }); - } - - [Fact] - public void Property_of_a_type_that_has_a_static_Parse_method_accepting_a_string_and_format_provider_is_bound_by_invoking_the_method() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.TestStringParseableWithFormatProvider), "foobar") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new AllSupportedTypesCommand - { - TestStringParseableWithFormatProvider = StringParseableWithFormatProvider.Parse("foobar", CultureInfo.InvariantCulture) - }); - } - - [Fact] - public void Property_of_custom_type_that_implements_IEnumerable_can_only_be_bound_if_that_type_has_a_constructor_accepting_an_array() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(UnsupportedEnumerablePropertyTypeCommand.Option), "foo", "bar") - .Build(); - - // Act & assert - var ex = Assert.Throws(() => CommandHelper.ResolveCommand(input)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Property_of_non_nullable_type_can_only_be_bound_if_the_argument_value_is_set() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.Int)) - .Build(); - - // Act & assert - var ex = Assert.Throws(() => CommandHelper.ResolveCommand(input)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Property_must_have_a_type_supported_by_the_framework_in_order_to_be_bound() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(UnsupportedPropertyTypeCommand.Option), "foo") - .Build(); - - // Act & assert - var ex = Assert.Throws(() => CommandHelper.ResolveCommand(input)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Property_must_have_a_type_that_implements_IEnumerable_in_order_to_be_bound_from_multiple_argument_values() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(AllSupportedTypesCommand.Int), "1", "2", "3") - .Build(); - - // Act & assert - var ex = Assert.Throws(() => CommandHelper.ResolveCommand(input)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Property_annotated_as_an_option_can_be_bound_from_multiple_values_even_if_the_inputs_use_mixed_naming() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption("option", "foo") - .AddOption("o", "bar") - .AddOption("option", "baz") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new ArrayOptionCommand - { - Option = new[] {"foo", "bar", "baz"} - }); - } - - [Fact] - public void Property_annotated_as_a_required_option_must_always_be_set() - { - // Arrange - var input = new CommandInputBuilder() - .Build(); - - // Act & assert - var ex = Assert.Throws(() => CommandHelper.ResolveCommand(input)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Property_annotated_as_a_required_option_must_always_be_bound_to_some_value() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(RequiredOptionCommand.Option)) - .Build(); - - // Act & assert - var ex = Assert.Throws(() => CommandHelper.ResolveCommand(input)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Property_annotated_as_a_required_option_must_always_be_bound_to_at_least_one_value_if_it_expects_multiple_values() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption(nameof(RequiredOptionCommand.Option)) - .Build(); - - // Act & assert - var ex = Assert.Throws(() => CommandHelper.ResolveCommand(input)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Property_annotated_as_parameter_is_bound_directly_from_argument_value_according_to_the_order() - { - // Arrange - var input = new CommandInputBuilder() - .AddParameter("foo") - .AddParameter("bar") - .AddParameter("hello") - .AddParameter("world") - .Build(); - - // Act - var instance = CommandHelper.ResolveCommand(input); - - // Assert - instance.Should().BeEquivalentTo(new ParametersCommand - { - ParameterA = "foo", - ParameterB = "bar", - ParameterC = new[] {"hello", "world"} - }); - } - - [Fact] - public void Property_annotated_as_parameter_must_always_be_bound_to_some_value() - { - // Arrange - var input = new CommandInputBuilder() - .AddParameter("foo") - .Build(); - - // Act & assert - var ex = Assert.Throws(() => CommandHelper.ResolveCommand(input)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void Property_annotated_as_parameter_must_always_be_bound_to_at_least_one_value_if_it_expects_multiple_values() - { - // Arrange - var input = new CommandInputBuilder() - .AddParameter("foo") - .AddParameter("bar") - .Build(); - - // Act & assert - var ex = Assert.Throws(() => CommandHelper.ResolveCommand(input)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void All_provided_option_arguments_must_be_bound_to_corresponding_properties() - { - // Arrange - var input = new CommandInputBuilder() - .AddOption("not-a-real-option", "boom") - .AddOption("fake-option", "poof") - .Build(); - - // Act & assert - var ex = Assert.Throws(() => CommandHelper.ResolveCommand(input)); - _output.WriteLine(ex.Message); - } - - [Fact] - public void All_provided_parameter_arguments_must_be_bound_to_corresponding_properties() - { - // Arrange - var input = new CommandInputBuilder() - .AddParameter("boom") - .AddParameter("poof") - .AddOption(nameof(NoParameterCommand.OptionA), "foo") - .AddOption(nameof(NoParameterCommand.OptionB), "bar") - .Build(); - - // Act & assert - var ex = Assert.Throws(() => CommandHelper.ResolveCommand(input)); - _output.WriteLine(ex.Message); + _output.WriteLine(stdErrData); } } } \ No newline at end of file diff --git a/CliFx.Tests/ArgumentConversionSpecs.cs b/CliFx.Tests/ArgumentConversionSpecs.cs new file mode 100644 index 0000000..62844dd --- /dev/null +++ b/CliFx.Tests/ArgumentConversionSpecs.cs @@ -0,0 +1,1453 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Threading.Tasks; +using CliFx.Tests.Commands; +using FluentAssertions; +using Newtonsoft.Json; +using Xunit; +using Xunit.Abstractions; + +namespace CliFx.Tests +{ + public class ArgumentConversionSpecs + { + private readonly ITestOutputHelper _output; + + public ArgumentConversionSpecs(ITestOutputHelper output) => _output = output; + + [Fact] + public async Task Property_of_type_object_is_bound_directly_from_the_argument_value() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--obj", "value" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + Object = "value" + }); + } + + [Fact] + public async Task Property_of_type_object_array_is_bound_directly_from_the_argument_values() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--obj-array", "foo", "bar" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + ObjectArray = new object[] {"foo", "bar"} + }); + } + + [Fact] + public async Task Property_of_type_string_is_bound_directly_from_the_argument_value() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--str", "value" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + String = "value" + }); + } + + [Fact] + public async Task Property_of_type_string_array_is_bound_directly_from_the_argument_values() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--str-array", "foo", "bar" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + StringArray = new[] {"foo", "bar"} + }); + } + + [Fact] + public async Task Property_of_type_string_IEnumerable_is_bound_directly_from_the_argument_values() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--str-enumerable", "foo", "bar" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + StringEnumerable = new[] {"foo", "bar"} + }); + } + + [Fact] + public async Task Property_of_type_string_IReadOnlyList_is_bound_directly_from_the_argument_values() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--str-read-only-list", "foo", "bar" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + StringReadOnlyList = new[] {"foo", "bar"} + }); + } + + [Fact] + public async Task Property_of_type_string_List_is_bound_directly_from_the_argument_values() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--str-list", "foo", "bar" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + StringList = new List {"foo", "bar"} + }); + } + + [Fact] + public async Task Property_of_type_string_HashSet_is_bound_directly_from_the_argument_values() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--str-set", "foo", "bar" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + StringHashSet = new HashSet {"foo", "bar"} + }); + } + + [Fact] + public async Task Property_of_type_bool_is_bound_as_true_if_the_argument_value_is_true() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--bool", "true" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + Bool = true + }); + } + + [Fact] + public async Task Property_of_type_bool_is_bound_as_false_if_the_argument_value_is_false() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--bool", "false" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + Bool = false + }); + } + + [Fact] + public async Task Property_of_type_bool_is_bound_as_true_if_the_argument_value_is_not_set() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--bool" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + Bool = true + }); + } + + [Fact] + public async Task Property_of_type_char_is_bound_directly_from_the_argument_value_if_it_contains_only_one_character() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--char", "a" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + Char = 'a' + }); + } + + [Fact] + public async Task Property_of_type_sbyte_is_bound_by_parsing_the_argument_value() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--sbyte", "15" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + Sbyte = 15 + }); + } + + [Fact] + public async Task Property_of_type_byte_is_bound_by_parsing_the_argument_value() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--byte", "15" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + Byte = 15 + }); + } + + [Fact] + public async Task Property_of_type_short_is_bound_by_parsing_the_argument_value() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--short", "15" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + Short = 15 + }); + } + + [Fact] + public async Task Property_of_type_ushort_is_bound_by_parsing_the_argument_value() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--ushort", "15" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + Ushort = 15 + }); + } + + [Fact] + public async Task Property_of_type_int_is_bound_by_parsing_the_argument_value() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--int", "15" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + Int = 15 + }); + } + + [Fact] + public async Task Property_of_type_nullable_int_is_bound_by_parsing_the_argument_value_if_it_is_set() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--int-nullable", "15" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + IntNullable = 15 + }); + } + + [Fact] + public async Task Property_of_type_nullable_int_is_bound_as_null_if_the_argument_value_is_not_set() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--int-nullable" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + IntNullable = null + }); + } + + [Fact] + public async Task Property_of_type_int_array_is_bound_by_parsing_the_argument_values() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--int-array", "3", "15" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + IntArray = new[] {3, 15} + }); + } + + [Fact] + public async Task Property_of_type_nullable_int_array_is_bound_by_parsing_the_argument_values() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--int-nullable-array", "3", "15" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + IntNullableArray = new int?[] {3, 15} + }); + } + + [Fact] + public async Task Property_of_type_uint_is_bound_by_parsing_the_argument_value() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--uint", "15" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + Uint = 15 + }); + } + + [Fact] + public async Task Property_of_type_long_is_bound_by_parsing_the_argument_value() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--long", "15" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + Long = 15 + }); + } + + [Fact] + public async Task Property_of_type_ulong_is_bound_by_parsing_the_argument_value() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--ulong", "15" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + Ulong = 15 + }); + } + + [Fact] + public async Task Property_of_type_float_is_bound_by_parsing_the_argument_value() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--float", "3.14" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + Float = 3.14f + }); + } + + [Fact] + public async Task Property_of_type_double_is_bound_by_parsing_the_argument_value() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--double", "3.14" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + Double = 3.14 + }); + } + + [Fact] + public async Task Property_of_type_decimal_is_bound_by_parsing_the_argument_value() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--decimal", "3.14" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + Decimal = 3.14m + }); + } + + [Fact] + public async Task Property_of_type_DateTime_is_bound_by_parsing_the_argument_value() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--datetime", "28 Apr 1995" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + DateTime = new DateTime(1995, 04, 28) + }); + } + + [Fact] + public async Task Property_of_type_DateTimeOffset_is_bound_by_parsing_the_argument_value() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--datetime-offset", "28 Apr 1995" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + DateTimeOffset = new DateTime(1995, 04, 28) + }); + } + + [Fact] + public async Task Property_of_type_TimeSpan_is_bound_by_parsing_the_argument_value() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--timespan", "00:14:59" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + TimeSpan = new TimeSpan(00, 14, 59) + }); + } + + [Fact] + public async Task Property_of_type_nullable_TimeSpan_is_bound_by_parsing_the_argument_value_if_it_is_set() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--timespan-nullable", "00:14:59" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + TimeSpanNullable = new TimeSpan(00, 14, 59) + }); + } + + [Fact] + public async Task Property_of_type_nullable_TimeSpan_is_bound_as_null_if_the_argument_value_is_not_set() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--timespan-nullable" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + TimeSpanNullable = null + }); + } + + [Fact] + public async Task Property_of_an_enum_type_is_bound_by_parsing_the_argument_value_as_name() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--enum", "value2" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + Enum = SupportedArgumentTypesCommand.CustomEnum.Value2 + }); + } + + [Fact] + public async Task Property_of_an_enum_type_is_bound_by_parsing_the_argument_value_as_id() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--enum", "2" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + Enum = SupportedArgumentTypesCommand.CustomEnum.Value2 + }); + } + + [Fact] + public async Task Property_of_a_nullable_enum_type_is_bound_by_parsing_the_argument_value_as_name_if_it_is_set() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--enum-nullable", "value3" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + EnumNullable = SupportedArgumentTypesCommand.CustomEnum.Value3 + }); + } + + [Fact] + public async Task Property_of_a_nullable_enum_type_is_bound_by_parsing_the_argument_value_as_id_if_it_is_set() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--enum-nullable", "3" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + EnumNullable = SupportedArgumentTypesCommand.CustomEnum.Value3 + }); + } + + [Fact] + public async Task Property_of_a_nullable_enum_type_is_bound_as_null_if_the_argument_value_is_not_set() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--enum-nullable" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + EnumNullable = null + }); + } + + [Fact] + public async Task Property_of_an_enum_array_type_is_bound_by_parsing_the_argument_values_as_names() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--enum-array", "value1", "value3" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + EnumArray = new[] {SupportedArgumentTypesCommand.CustomEnum.Value1, SupportedArgumentTypesCommand.CustomEnum.Value3} + }); + } + + [Fact] + public async Task Property_of_an_enum_array_type_is_bound_by_parsing_the_argument_values_as_ids() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--enum-array", "1", "3" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + EnumArray = new[] {SupportedArgumentTypesCommand.CustomEnum.Value1, SupportedArgumentTypesCommand.CustomEnum.Value3} + }); + } + + [Fact] + public async Task Property_of_an_enum_array_type_is_bound_by_parsing_the_argument_values_as_either_names_or_ids() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--enum-array", "1", "value3" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + EnumArray = new[] {SupportedArgumentTypesCommand.CustomEnum.Value1, SupportedArgumentTypesCommand.CustomEnum.Value3} + }); + } + + [Fact] + public async Task Property_of_a_type_that_has_a_constructor_accepting_a_string_is_bound_by_invoking_the_constructor_with_the_argument_value() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--str-constructible", "foobar" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + StringConstructible = new SupportedArgumentTypesCommand.CustomStringConstructible("foobar") + }); + } + + [Fact] + public async Task Property_of_an_array_of_type_that_has_a_constructor_accepting_a_string_is_bound_by_invoking_the_constructor_with_the_argument_values() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--str-constructible-array", "foo", "bar" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + StringConstructibleArray = new[] + { + new SupportedArgumentTypesCommand.CustomStringConstructible("foo"), + new SupportedArgumentTypesCommand.CustomStringConstructible("bar") + } + }); + } + + [Fact] + public async Task Property_of_a_type_that_has_a_static_Parse_method_accepting_a_string_is_bound_by_invoking_the_method() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--str-parseable", "foobar" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + StringParseable = SupportedArgumentTypesCommand.CustomStringParseable.Parse("foobar") + }); + } + + [Fact] + public async Task Property_of_a_type_that_has_a_static_Parse_method_accepting_a_string_and_format_provider_is_bound_by_invoking_the_method() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--str-parseable-format", "foobar" + }); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); + + // Assert + exitCode.Should().Be(0); + + commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand + { + StringParseableWithFormatProvider = + SupportedArgumentTypesCommand.CustomStringParseableWithFormatProvider.Parse("foobar", CultureInfo.InvariantCulture) + }); + } + + [Fact] + public async Task Property_of_custom_type_must_be_string_initializable_in_order_to_be_bound() + { + // Arrange + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--str-non-initializable", "foobar" + }); + + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); + } + + [Fact] + public async Task Property_of_custom_type_that_implements_IEnumerable_can_only_be_bound_if_that_type_has_a_constructor_accepting_an_array() + { + // Arrange + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--str-enumerable-non-initializable", "foobar" + }); + + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); + } + + [Fact] + public async Task Property_of_non_nullable_type_can_only_be_bound_if_the_argument_value_is_set() + { + // Arrange + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--int" + }); + + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); + } + + [Fact] + public async Task Property_must_have_a_type_that_implements_IEnumerable_in_order_to_be_bound_from_multiple_argument_values() + { + // Arrange + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--int", "1", "2", "3" + }); + + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/ArgumentSyntaxSpecs.cs b/CliFx.Tests/ArgumentSyntaxSpecs.cs deleted file mode 100644 index a17f992..0000000 --- a/CliFx.Tests/ArgumentSyntaxSpecs.cs +++ /dev/null @@ -1,378 +0,0 @@ -using System; -using System.Collections.Generic; -using CliFx.Domain; -using CliFx.Tests.Internal; -using FluentAssertions; -using Xunit; - -namespace CliFx.Tests -{ - public class ArgumentSyntaxSpecs - { - [Fact] - public void Input_is_empty_if_no_arguments_are_provided() - { - // Arrange - var arguments = Array.Empty(); - var commandNames = Array.Empty(); - - // Act - var input = CommandInput.Parse(arguments, commandNames); - - // Assert - input.Should().BeEquivalentTo(CommandInput.Empty); - } - - public static object[][] DirectivesTestData => new[] - { - new object[] - { - new[] {"[preview]"}, - new CommandInputBuilder() - .AddDirective("preview") - .Build() - }, - - new object[] - { - new[] {"[preview]", "[debug]"}, - new CommandInputBuilder() - .AddDirective("preview") - .AddDirective("debug") - .Build() - } - }; - - [Theory] - [MemberData(nameof(DirectivesTestData))] - internal void Directive_can_be_enabled_by_specifying_its_name_in_square_brackets(IReadOnlyList arguments, CommandInput expectedInput) - { - // Arrange - var commandNames = Array.Empty(); - - // Act - var input = CommandInput.Parse(arguments, commandNames); - - // Assert - input.Should().BeEquivalentTo(expectedInput); - } - - public static object[][] OptionsTestData => new[] - { - new object[] - { - new[] {"--option"}, - new CommandInputBuilder() - .AddOption("option") - .Build() - }, - - new object[] - { - new[] {"--option", "value"}, - new CommandInputBuilder() - .AddOption("option", "value") - .Build() - }, - - new object[] - { - new[] {"--option", "value1", "value2"}, - new CommandInputBuilder() - .AddOption("option", "value1", "value2") - .Build() - }, - - new object[] - { - new[] {"--option", "same value"}, - new CommandInputBuilder() - .AddOption("option", "same value") - .Build() - }, - - new object[] - { - new[] {"--option1", "--option2"}, - new CommandInputBuilder() - .AddOption("option1") - .AddOption("option2") - .Build() - }, - - new object[] - { - new[] {"--option1", "value1", "--option2", "value2"}, - new CommandInputBuilder() - .AddOption("option1", "value1") - .AddOption("option2", "value2") - .Build() - }, - - new object[] - { - new[] {"--option1", "value1", "value2", "--option2", "value3", "value4"}, - new CommandInputBuilder() - .AddOption("option1", "value1", "value2") - .AddOption("option2", "value3", "value4") - .Build() - }, - - new object[] - { - new[] {"--option1", "value1", "value2", "--option2"}, - new CommandInputBuilder() - .AddOption("option1", "value1", "value2") - .AddOption("option2") - .Build() - } - }; - - [Theory] - [MemberData(nameof(OptionsTestData))] - internal void Option_can_be_set_by_specifying_its_name_after_two_dashes(IReadOnlyList arguments, CommandInput expectedInput) - { - // Arrange - var commandNames = Array.Empty(); - - // Act - var input = CommandInput.Parse(arguments, commandNames); - - // Assert - input.Should().BeEquivalentTo(expectedInput); - } - - public static object[][] ShortOptionsTestData => new[] - { - new object[] - { - new[] {"-o"}, - new CommandInputBuilder() - .AddOption("o") - .Build() - }, - - new object[] - { - new[] {"-o", "value"}, - new CommandInputBuilder() - .AddOption("o", "value") - .Build() - }, - - new object[] - { - new[] {"-o", "value1", "value2"}, - new CommandInputBuilder() - .AddOption("o", "value1", "value2") - .Build() - }, - - new object[] - { - new[] {"-o", "same value"}, - new CommandInputBuilder() - .AddOption("o", "same value") - .Build() - }, - - new object[] - { - new[] {"-a", "-b"}, - new CommandInputBuilder() - .AddOption("a") - .AddOption("b") - .Build() - }, - - new object[] - { - new[] {"-a", "value1", "-b", "value2"}, - new CommandInputBuilder() - .AddOption("a", "value1") - .AddOption("b", "value2") - .Build() - }, - - new object[] - { - new[] {"-a", "value1", "value2", "-b", "value3", "value4"}, - new CommandInputBuilder() - .AddOption("a", "value1", "value2") - .AddOption("b", "value3", "value4") - .Build() - }, - - new object[] - { - new[] {"-a", "value1", "value2", "-b"}, - new CommandInputBuilder() - .AddOption("a", "value1", "value2") - .AddOption("b") - .Build() - }, - - new object[] - { - new[] {"-abc"}, - new CommandInputBuilder() - .AddOption("a") - .AddOption("b") - .AddOption("c") - .Build() - }, - - new object[] - { - new[] {"-abc", "value"}, - new CommandInputBuilder() - .AddOption("a") - .AddOption("b") - .AddOption("c", "value") - .Build() - }, - - new object[] - { - new[] {"-abc", "value1", "value2"}, - new CommandInputBuilder() - .AddOption("a") - .AddOption("b") - .AddOption("c", "value1", "value2") - .Build() - } - }; - - [Theory] - [MemberData(nameof(ShortOptionsTestData))] - internal void Option_can_be_set_by_specifying_its_short_name_after_a_single_dash(IReadOnlyList arguments, CommandInput expectedInput) - { - // Arrange - var commandNames = Array.Empty(); - - // Act - var input = CommandInput.Parse(arguments, commandNames); - - // Assert - input.Should().BeEquivalentTo(expectedInput); - } - - public static object[][] ParametersTestData => new[] - { - new object[] - { - new[] {"foo"}, - new CommandInputBuilder() - .AddParameter("foo") - .Build() - }, - - new object[] - { - new[] {"foo", "bar"}, - new CommandInputBuilder() - .AddParameter("foo") - .AddParameter("bar") - .Build() - }, - - new object[] - { - new[] {"[preview]", "foo"}, - new CommandInputBuilder() - .AddDirective("preview") - .AddParameter("foo") - .Build() - }, - - new object[] - { - new[] {"foo", "--option", "value", "-abc"}, - new CommandInputBuilder() - .AddParameter("foo") - .AddOption("option", "value") - .AddOption("a") - .AddOption("b") - .AddOption("c") - .Build() - }, - - new object[] - { - new[] {"[preview]", "[debug]", "foo", "bar", "--option", "value", "-abc"}, - new CommandInputBuilder() - .AddDirective("preview") - .AddDirective("debug") - .AddParameter("foo") - .AddParameter("bar") - .AddOption("option", "value") - .AddOption("a") - .AddOption("b") - .AddOption("c") - .Build() - } - }; - - [Theory] - [MemberData(nameof(ParametersTestData))] - internal void Parameter_can_be_set_by_specifying_the_value_directly(IReadOnlyList arguments, CommandInput expectedInput) - { - // Arrange - var commandNames = Array.Empty(); - - // Act - var input = CommandInput.Parse(arguments, commandNames); - - // Assert - input.Should().BeEquivalentTo(expectedInput); - } - - public static object[][] CommandNameTestData => new[] - { - new object[] - { - new[] {"cmd"}, - new[] {"cmd"}, - new CommandInputBuilder() - .SetCommandName("cmd") - .Build() - }, - - new object[] - { - new[] {"cmd"}, - new[] {"cmd", "foo", "bar", "-o", "value"}, - new CommandInputBuilder() - .SetCommandName("cmd") - .AddParameter("foo") - .AddParameter("bar") - .AddOption("o", "value") - .Build() - }, - - new object[] - { - new[] {"cmd", "cmd sub"}, - new[] {"cmd", "sub", "foo"}, - new CommandInputBuilder() - .SetCommandName("cmd sub") - .AddParameter("foo") - .Build() - } - }; - - [Theory] - [MemberData(nameof(CommandNameTestData))] - internal void Command_name_is_matched_from_arguments_that_come_before_parameters( - IReadOnlyList commandNames, - IReadOnlyList arguments, - CommandInput expectedInput) - { - // Act - var input = CommandInput.Parse(arguments, commandNames); - - // Assert - input.Should().BeEquivalentTo(expectedInput); - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/CancellationSpecs.Commands.cs b/CliFx.Tests/CancellationSpecs.Commands.cs deleted file mode 100644 index 6afc5d1..0000000 --- a/CliFx.Tests/CancellationSpecs.Commands.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests -{ - public partial class CancellationSpecs - { - [Command("cancel")] - private class CancellableCommand : ICommand - { - public async ValueTask ExecuteAsync(IConsole console) - { - try - { - await Task.Delay(TimeSpan.FromSeconds(3), console.GetCancellationToken()); - console.Output.WriteLine("Never printed"); - } - catch (OperationCanceledException) - { - console.Output.WriteLine("Cancellation requested"); - throw; - } - } - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/CancellationSpecs.cs b/CliFx.Tests/CancellationSpecs.cs index db96d49..0f38568 100644 --- a/CliFx.Tests/CancellationSpecs.cs +++ b/CliFx.Tests/CancellationSpecs.cs @@ -1,14 +1,14 @@ using System; -using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; +using CliFx.Tests.Commands; using FluentAssertions; using Xunit; namespace CliFx.Tests { - public partial class CancellationSpecs + public class CancellationSpecs { [Fact] public async Task Command_can_perform_additional_cleanup_if_cancellation_is_requested() @@ -22,22 +22,19 @@ namespace CliFx.Tests var console = new VirtualConsole(output: stdOut, cancellationToken: cts.Token); var application = new CliApplicationBuilder() - .AddCommand(typeof(CancellableCommand)) + .AddCommand() .UseConsole(console) .Build(); // Act cts.CancelAfter(TimeSpan.FromSeconds(0.2)); - var exitCode = await application.RunAsync( - new[] {"cancel"}, - new Dictionary()); - + var exitCode = await application.RunAsync(new[] {"cmd"}); var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); // Assert exitCode.Should().NotBe(0); - stdOutData.Should().Be("Cancellation requested"); + stdOutData.Should().Be(CancellableCommand.CancellationOutputText); } } } \ No newline at end of file diff --git a/CliFx.Tests/CliFx.Tests.csproj b/CliFx.Tests/CliFx.Tests.csproj index 0cec929..924ee24 100644 --- a/CliFx.Tests/CliFx.Tests.csproj +++ b/CliFx.Tests/CliFx.Tests.csproj @@ -19,6 +19,7 @@ + diff --git a/CliFx.Tests/Commands/CancellableCommand.cs b/CliFx.Tests/Commands/CancellableCommand.cs new file mode 100644 index 0000000..6eac83a --- /dev/null +++ b/CliFx.Tests/Commands/CancellableCommand.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; +using CliFx.Attributes; + +namespace CliFx.Tests.Commands +{ + [Command("cmd")] + public class CancellableCommand : ICommand + { + public const string CompletionOutputText = "Finished"; + public const string CancellationOutputText = "Canceled"; + + public async ValueTask ExecuteAsync(IConsole console) + { + try + { + await Task.Delay( + TimeSpan.FromSeconds(3), + console.GetCancellationToken() + ); + + console.Output.WriteLine(CompletionOutputText); + } + catch (OperationCanceledException) + { + console.Output.WriteLine(CancellationOutputText); + throw; + } + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/CommandExceptionCommand.cs b/CliFx.Tests/Commands/CommandExceptionCommand.cs new file mode 100644 index 0000000..3a1af20 --- /dev/null +++ b/CliFx.Tests/Commands/CommandExceptionCommand.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using CliFx.Attributes; +using CliFx.Exceptions; + +namespace CliFx.Tests.Commands +{ + [Command("cmd")] + public class CommandExceptionCommand : ICommand + { + [CommandOption("code", 'c')] + public int ExitCode { get; set; } = 133; + + [CommandOption("msg", 'm')] + public string? Message { get; set; } + + [CommandOption("show-help")] + public bool ShowHelp { get; set; } + + public ValueTask ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode, ShowHelp); + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/DefaultCommand.cs b/CliFx.Tests/Commands/DefaultCommand.cs new file mode 100644 index 0000000..4c58b99 --- /dev/null +++ b/CliFx.Tests/Commands/DefaultCommand.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using CliFx.Attributes; + +namespace CliFx.Tests.Commands +{ + [Command(Description = nameof(DefaultCommand))] + public class DefaultCommand : ICommand + { + public const string ExpectedOutputText = nameof(DefaultCommand); + + public ValueTask ExecuteAsync(IConsole console) + { + console.Output.WriteLine(ExpectedOutputText); + return default; + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/GenericExceptionCommand.cs b/CliFx.Tests/Commands/GenericExceptionCommand.cs new file mode 100644 index 0000000..aa8bbc1 --- /dev/null +++ b/CliFx.Tests/Commands/GenericExceptionCommand.cs @@ -0,0 +1,15 @@ +using System; +using System.Threading.Tasks; +using CliFx.Attributes; + +namespace CliFx.Tests.Commands +{ + [Command("cmd")] + public class GenericExceptionCommand : ICommand + { + [CommandOption("msg", 'm')] + public string? Message { get; set; } + + public ValueTask ExecuteAsync(IConsole console) => throw new Exception(Message); + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/Invalid/ConflictWithHelpOptionCommand.cs b/CliFx.Tests/Commands/Invalid/ConflictWithHelpOptionCommand.cs new file mode 100644 index 0000000..0f7f2c2 --- /dev/null +++ b/CliFx.Tests/Commands/Invalid/ConflictWithHelpOptionCommand.cs @@ -0,0 +1,11 @@ +using CliFx.Attributes; + +namespace CliFx.Tests.Commands.Invalid +{ + [Command("cmd")] + public class ConflictWithHelpOptionCommand : SelfSerializeCommandBase + { + [CommandOption("option-h", 'h')] + public string? OptionH { get; set; } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/Invalid/ConflictWithVersionOptionCommand.cs b/CliFx.Tests/Commands/Invalid/ConflictWithVersionOptionCommand.cs new file mode 100644 index 0000000..4c4f7f3 --- /dev/null +++ b/CliFx.Tests/Commands/Invalid/ConflictWithVersionOptionCommand.cs @@ -0,0 +1,12 @@ +using CliFx.Attributes; + +namespace CliFx.Tests.Commands.Invalid +{ + // Must be default because version option is available only on default commands + [Command] + public class ConflictWithVersionOptionCommand : SelfSerializeCommandBase + { + [CommandOption("version")] + public string? Version { get; set; } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/Invalid/DuplicateOptionEnvironmentVariableNamesCommand.cs b/CliFx.Tests/Commands/Invalid/DuplicateOptionEnvironmentVariableNamesCommand.cs new file mode 100644 index 0000000..0618184 --- /dev/null +++ b/CliFx.Tests/Commands/Invalid/DuplicateOptionEnvironmentVariableNamesCommand.cs @@ -0,0 +1,14 @@ +using CliFx.Attributes; + +namespace CliFx.Tests.Commands.Invalid +{ + [Command("cmd")] + public class DuplicateOptionEnvironmentVariableNamesCommand : SelfSerializeCommandBase + { + [CommandOption("option-a", EnvironmentVariableName = "ENV_VAR")] + public string? OptionA { get; set; } + + [CommandOption("option-b", EnvironmentVariableName = "ENV_VAR")] + public string? OptionB { get; set; } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/Invalid/DuplicateOptionNamesCommand.cs b/CliFx.Tests/Commands/Invalid/DuplicateOptionNamesCommand.cs new file mode 100644 index 0000000..b04d91b --- /dev/null +++ b/CliFx.Tests/Commands/Invalid/DuplicateOptionNamesCommand.cs @@ -0,0 +1,14 @@ +using CliFx.Attributes; + +namespace CliFx.Tests.Commands.Invalid +{ + [Command("cmd")] + public class DuplicateOptionNamesCommand : SelfSerializeCommandBase + { + [CommandOption("fruits")] + public string? Apples { get; set; } + + [CommandOption("fruits")] + public string? Oranges { get; set; } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/Invalid/DuplicateOptionShortNamesCommand.cs b/CliFx.Tests/Commands/Invalid/DuplicateOptionShortNamesCommand.cs new file mode 100644 index 0000000..d571139 --- /dev/null +++ b/CliFx.Tests/Commands/Invalid/DuplicateOptionShortNamesCommand.cs @@ -0,0 +1,14 @@ +using CliFx.Attributes; + +namespace CliFx.Tests.Commands.Invalid +{ + [Command("cmd")] + public class DuplicateOptionShortNamesCommand : SelfSerializeCommandBase + { + [CommandOption('x')] + public string? OptionA { get; set; } + + [CommandOption('x')] + public string? OptionB { get; set; } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/Invalid/DuplicateParameterNameCommand.cs b/CliFx.Tests/Commands/Invalid/DuplicateParameterNameCommand.cs new file mode 100644 index 0000000..1d79e0a --- /dev/null +++ b/CliFx.Tests/Commands/Invalid/DuplicateParameterNameCommand.cs @@ -0,0 +1,14 @@ +using CliFx.Attributes; + +namespace CliFx.Tests.Commands.Invalid +{ + [Command("cmd")] + public class DuplicateParameterNameCommand : SelfSerializeCommandBase + { + [CommandParameter(0, Name = "param")] + public string? ParamA { get; set; } + + [CommandParameter(1, Name = "param")] + public string? ParamB { get; set; } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/Invalid/DuplicateParameterOrderCommand.cs b/CliFx.Tests/Commands/Invalid/DuplicateParameterOrderCommand.cs new file mode 100644 index 0000000..f1ab130 --- /dev/null +++ b/CliFx.Tests/Commands/Invalid/DuplicateParameterOrderCommand.cs @@ -0,0 +1,14 @@ +using CliFx.Attributes; + +namespace CliFx.Tests.Commands.Invalid +{ + [Command("cmd")] + public class DuplicateParameterOrderCommand : SelfSerializeCommandBase + { + [CommandParameter(13)] + public string? ParamA { get; set; } + + [CommandParameter(13)] + public string? ParamB { get; set; } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/Invalid/EmptyOptionNameCommand.cs b/CliFx.Tests/Commands/Invalid/EmptyOptionNameCommand.cs new file mode 100644 index 0000000..5f4de5d --- /dev/null +++ b/CliFx.Tests/Commands/Invalid/EmptyOptionNameCommand.cs @@ -0,0 +1,11 @@ +using CliFx.Attributes; + +namespace CliFx.Tests.Commands.Invalid +{ + [Command("cmd")] + public class EmptyOptionNameCommand : SelfSerializeCommandBase + { + [CommandOption("")] + public string? Apples { get; set; } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/Invalid/MultipleNonScalarParametersCommand.cs b/CliFx.Tests/Commands/Invalid/MultipleNonScalarParametersCommand.cs new file mode 100644 index 0000000..5d641d0 --- /dev/null +++ b/CliFx.Tests/Commands/Invalid/MultipleNonScalarParametersCommand.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using CliFx.Attributes; + +namespace CliFx.Tests.Commands.Invalid +{ + [Command("cmd")] + public class MultipleNonScalarParametersCommand : SelfSerializeCommandBase + { + [CommandParameter(0)] + public IReadOnlyList? ParamA { get; set; } + + [CommandParameter(1)] + public IReadOnlyList? ParamB { get; set; } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/Invalid/NonAnnotatedCommand.cs b/CliFx.Tests/Commands/Invalid/NonAnnotatedCommand.cs new file mode 100644 index 0000000..8910807 --- /dev/null +++ b/CliFx.Tests/Commands/Invalid/NonAnnotatedCommand.cs @@ -0,0 +1,6 @@ +namespace CliFx.Tests.Commands.Invalid +{ + public class NonAnnotatedCommand : SelfSerializeCommandBase + { + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/Invalid/NonImplementedCommand.cs b/CliFx.Tests/Commands/Invalid/NonImplementedCommand.cs new file mode 100644 index 0000000..72a091f --- /dev/null +++ b/CliFx.Tests/Commands/Invalid/NonImplementedCommand.cs @@ -0,0 +1,9 @@ +using CliFx.Attributes; + +namespace CliFx.Tests.Commands.Invalid +{ + [Command] + public class NonImplementedCommand + { + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/Invalid/NonLastNonScalarParameterCommand.cs b/CliFx.Tests/Commands/Invalid/NonLastNonScalarParameterCommand.cs new file mode 100644 index 0000000..3576d88 --- /dev/null +++ b/CliFx.Tests/Commands/Invalid/NonLastNonScalarParameterCommand.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using CliFx.Attributes; + +namespace CliFx.Tests.Commands.Invalid +{ + [Command("cmd")] + public class NonLastNonScalarParameterCommand : SelfSerializeCommandBase + { + [CommandParameter(0)] + public IReadOnlyList? ParamA { get; set; } + + [CommandParameter(1)] + public string? ParamB { get; set; } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/Invalid/OtherDefaultCommand.cs b/CliFx.Tests/Commands/Invalid/OtherDefaultCommand.cs new file mode 100644 index 0000000..d8a0ee9 --- /dev/null +++ b/CliFx.Tests/Commands/Invalid/OtherDefaultCommand.cs @@ -0,0 +1,9 @@ +using CliFx.Attributes; + +namespace CliFx.Tests.Commands.Invalid +{ + [Command] + public class OtherDefaultCommand : SelfSerializeCommandBase + { + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/Invalid/SingleCharacterOptionNameCommand.cs b/CliFx.Tests/Commands/Invalid/SingleCharacterOptionNameCommand.cs new file mode 100644 index 0000000..f4b5117 --- /dev/null +++ b/CliFx.Tests/Commands/Invalid/SingleCharacterOptionNameCommand.cs @@ -0,0 +1,11 @@ +using CliFx.Attributes; + +namespace CliFx.Tests.Commands.Invalid +{ + [Command("cmd")] + public class SingleCharacterOptionNameCommand : SelfSerializeCommandBase + { + [CommandOption("a")] + public string? Apples { get; set; } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/NamedCommand.cs b/CliFx.Tests/Commands/NamedCommand.cs new file mode 100644 index 0000000..a3990db --- /dev/null +++ b/CliFx.Tests/Commands/NamedCommand.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using CliFx.Attributes; + +namespace CliFx.Tests.Commands +{ + [Command("named", Description = nameof(NamedCommand))] + public class NamedCommand : ICommand + { + public const string ExpectedOutputText = nameof(NamedCommand); + + public ValueTask ExecuteAsync(IConsole console) + { + console.Output.WriteLine(ExpectedOutputText); + return default; + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/NamedSubCommand.cs b/CliFx.Tests/Commands/NamedSubCommand.cs new file mode 100644 index 0000000..9ade0e0 --- /dev/null +++ b/CliFx.Tests/Commands/NamedSubCommand.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using CliFx.Attributes; + +namespace CliFx.Tests.Commands +{ + [Command("named sub", Description = nameof(NamedSubCommand))] + public class NamedSubCommand : ICommand + { + public const string ExpectedOutputText = nameof(NamedSubCommand); + + public ValueTask ExecuteAsync(IConsole console) + { + console.Output.WriteLine(ExpectedOutputText); + return default; + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/SelfSerializeCommandBase.cs b/CliFx.Tests/Commands/SelfSerializeCommandBase.cs new file mode 100644 index 0000000..c37abda --- /dev/null +++ b/CliFx.Tests/Commands/SelfSerializeCommandBase.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace CliFx.Tests.Commands +{ + public abstract class SelfSerializeCommandBase : ICommand + { + public ValueTask ExecuteAsync(IConsole console) + { + console.Output.WriteLine(JsonConvert.SerializeObject(this)); + return default; + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/SupportedArgumentTypesCommand.cs b/CliFx.Tests/Commands/SupportedArgumentTypesCommand.cs new file mode 100644 index 0000000..8258159 --- /dev/null +++ b/CliFx.Tests/Commands/SupportedArgumentTypesCommand.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using CliFx.Attributes; +using Newtonsoft.Json; + +namespace CliFx.Tests.Commands +{ + [Command("cmd")] + public partial class SupportedArgumentTypesCommand : SelfSerializeCommandBase + { + [CommandOption("obj")] + public object? Object { get; set; } = 42; + + [CommandOption("str")] + public string? String { get; set; } = "foo bar"; + + [CommandOption("bool")] + public bool Bool { get; set; } + + [CommandOption("char")] + public char Char { get; set; } + + [CommandOption("sbyte")] + public sbyte Sbyte { get; set; } + + [CommandOption("byte")] + public byte Byte { get; set; } + + [CommandOption("short")] + public short Short { get; set; } + + [CommandOption("ushort")] + public ushort Ushort { get; set; } + + [CommandOption("int")] + public int Int { get; set; } + + [CommandOption("uint")] + public uint Uint { get; set; } + + [CommandOption("long")] + public long Long { get; set; } + + [CommandOption("ulong")] + public ulong Ulong { get; set; } + + [CommandOption("float")] + public float Float { get; set; } + + [CommandOption("double")] + public double Double { get; set; } + + [CommandOption("decimal")] + public decimal Decimal { get; set; } + + [CommandOption("datetime")] + public DateTime DateTime { get; set; } + + [CommandOption("datetime-offset")] + public DateTimeOffset DateTimeOffset { get; set; } + + [CommandOption("timespan")] + public TimeSpan TimeSpan { get; set; } + + [CommandOption("enum")] + public CustomEnum Enum { get; set; } + + [CommandOption("int-nullable")] + public int? IntNullable { get; set; } + + [CommandOption("enum-nullable")] + public CustomEnum? EnumNullable { get; set; } + + [CommandOption("timespan-nullable")] + public TimeSpan? TimeSpanNullable { get; set; } + + [CommandOption("str-constructible")] + public CustomStringConstructible? StringConstructible { get; set; } + + [CommandOption("str-parseable")] + public CustomStringParseable? StringParseable { get; set; } + + [CommandOption("str-parseable-format")] + public CustomStringParseableWithFormatProvider? StringParseableWithFormatProvider { get; set; } + + [CommandOption("obj-array")] + public object[]? ObjectArray { get; set; } + + [CommandOption("str-array")] + public string[]? StringArray { get; set; } + + [CommandOption("int-array")] + public int[]? IntArray { get; set; } + + [CommandOption("enum-array")] + public CustomEnum[]? EnumArray { get; set; } + + [CommandOption("int-nullable-array")] + public int?[]? IntNullableArray { get; set; } + + [CommandOption("str-constructible-array")] + public CustomStringConstructible[]? StringConstructibleArray { get; set; } + + [CommandOption("str-enumerable")] + public IEnumerable? StringEnumerable { get; set; } + + [CommandOption("str-read-only-list")] + public IReadOnlyList? StringReadOnlyList { get; set; } + + [CommandOption("str-list")] + public List? StringList { get; set; } + + [CommandOption("str-set")] + public HashSet? StringHashSet { get; set; } + } + + public partial class SupportedArgumentTypesCommand + { + public enum CustomEnum + { + Value1 = 1, + Value2 = 2, + Value3 = 3 + } + + public class CustomStringConstructible + { + public string Value { get; } + + public CustomStringConstructible(string value) => Value = value; + } + + public class CustomStringParseable + { + public string Value { get; } + + [JsonConstructor] + private CustomStringParseable(string value) => Value = value; + + public static CustomStringParseable Parse(string value) => new CustomStringParseable(value); + } + + public class CustomStringParseableWithFormatProvider + { + public string Value { get; } + + [JsonConstructor] + private CustomStringParseableWithFormatProvider(string value) => Value = value; + + public static CustomStringParseableWithFormatProvider Parse(string value, IFormatProvider formatProvider) => + new CustomStringParseableWithFormatProvider(value + " " + formatProvider); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/UnsupportedArgumentTypesCommand.cs b/CliFx.Tests/Commands/UnsupportedArgumentTypesCommand.cs new file mode 100644 index 0000000..4dc9648 --- /dev/null +++ b/CliFx.Tests/Commands/UnsupportedArgumentTypesCommand.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using CliFx.Attributes; + +namespace CliFx.Tests.Commands +{ + [Command("cmd")] + public partial class UnsupportedArgumentTypesCommand : SelfSerializeCommandBase + { + [CommandOption("str-non-initializable")] + public CustomType? StringNonInitializable { get; set; } + + [CommandOption("str-enumerable-non-initializable")] + public CustomEnumerable? StringEnumerableNonInitializable { get; set; } + } + + public partial class UnsupportedArgumentTypesCommand + { + public class CustomType + { + } + + public class CustomEnumerable : IEnumerable + { + public IEnumerator GetEnumerator() => ((IEnumerable) Array.Empty()).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/WithDefaultValuesCommand.cs b/CliFx.Tests/Commands/WithDefaultValuesCommand.cs new file mode 100644 index 0000000..792febb --- /dev/null +++ b/CliFx.Tests/Commands/WithDefaultValuesCommand.cs @@ -0,0 +1,44 @@ +using System; +using CliFx.Attributes; + +namespace CliFx.Tests.Commands +{ + [Command("cmd")] + public class WithDefaultValuesCommand : SelfSerializeCommandBase + { + public enum CustomEnum { Value1, Value2, Value3 }; + + [CommandOption("obj")] + public object? Object { get; set; } = 42; + + [CommandOption("str")] + public string? String { get; set; } = "foo"; + + [CommandOption("str-empty")] + public string StringEmpty { get; set; } = ""; + + [CommandOption("str-array")] + public string[]? StringArray { get; set; } = { "foo", "bar", "baz" }; + + [CommandOption("bool")] + public bool Bool { get; set; } = true; + + [CommandOption("char")] + public char Char { get; set; } = 't'; + + [CommandOption("int")] + public int Int { get; set; } = 1337; + + [CommandOption("int-nullable")] + public int? IntNullable { get; set; } = 1337; + + [CommandOption("int-array")] + public int[]? IntArray { get; set; } = { 1, 2, 3 }; + + [CommandOption("timespan")] + public TimeSpan TimeSpan { get; set; } = TimeSpan.FromMinutes(123); + + [CommandOption("enum")] + public CustomEnum Enum { get; set; } = CustomEnum.Value2; + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/WithDependenciesCommand.cs b/CliFx.Tests/Commands/WithDependenciesCommand.cs new file mode 100644 index 0000000..4e92e75 --- /dev/null +++ b/CliFx.Tests/Commands/WithDependenciesCommand.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using CliFx.Attributes; + +namespace CliFx.Tests.Commands +{ + [Command("cmd")] + public class WithDependenciesCommand : ICommand + { + public class DependencyA + { + } + + public class DependencyB + { + } + + private readonly DependencyA _dependencyA; + private readonly DependencyB _dependencyB; + + public WithDependenciesCommand(DependencyA dependencyA, DependencyB dependencyB) + { + _dependencyA = dependencyA; + _dependencyB = dependencyB; + } + + public ValueTask ExecuteAsync(IConsole console) => default; + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/WithEnumArgumentsCommand.cs b/CliFx.Tests/Commands/WithEnumArgumentsCommand.cs new file mode 100644 index 0000000..26d1c5f --- /dev/null +++ b/CliFx.Tests/Commands/WithEnumArgumentsCommand.cs @@ -0,0 +1,19 @@ +using CliFx.Attributes; + +namespace CliFx.Tests.Commands +{ + [Command("cmd")] + public class WithEnumArgumentsCommand : SelfSerializeCommandBase + { + public enum CustomEnum { Value1, Value2, Value3 }; + + [CommandParameter(0, Name = "enum")] + public CustomEnum EnumParameter { get; set; } + + [CommandOption("enum")] + public CustomEnum? EnumOption { get; set; } + + [CommandOption("required-enum", IsRequired = true)] + public CustomEnum RequiredEnumOption { get; set; } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/WithEnvironmentVariablesCommand.cs b/CliFx.Tests/Commands/WithEnvironmentVariablesCommand.cs new file mode 100644 index 0000000..0630779 --- /dev/null +++ b/CliFx.Tests/Commands/WithEnvironmentVariablesCommand.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using CliFx.Attributes; + +namespace CliFx.Tests.Commands +{ + [Command("cmd")] + public class WithEnvironmentVariablesCommand : SelfSerializeCommandBase + { + [CommandOption("opt-a", 'a', EnvironmentVariableName = "ENV_OPT_A")] + public string? OptA { get; set; } + + [CommandOption("opt-b", 'b', EnvironmentVariableName = "ENV_OPT_B")] + public IReadOnlyList? OptB { get; set; } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/WithParametersCommand.cs b/CliFx.Tests/Commands/WithParametersCommand.cs new file mode 100644 index 0000000..d5b064a --- /dev/null +++ b/CliFx.Tests/Commands/WithParametersCommand.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using CliFx.Attributes; + +namespace CliFx.Tests.Commands +{ + [Command("cmd")] + public class WithParametersCommand : SelfSerializeCommandBase + { + [CommandParameter(0)] + public string? ParamA { get; set; } + + [CommandParameter(1)] + public int? ParamB { get; set; } + + [CommandParameter(2)] + public IReadOnlyList? ParamC { get; set; } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/WithRequiredOptionsCommand.cs b/CliFx.Tests/Commands/WithRequiredOptionsCommand.cs new file mode 100644 index 0000000..cbbce77 --- /dev/null +++ b/CliFx.Tests/Commands/WithRequiredOptionsCommand.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using CliFx.Attributes; + +namespace CliFx.Tests.Commands +{ + [Command("cmd")] + public class WithRequiredOptionsCommand : SelfSerializeCommandBase + { + [CommandOption("opt-a", 'a', IsRequired = true)] + public string? OptA { get; set; } + + [CommandOption("opt-b", 'b')] + public int? OptB { get; set; } + + [CommandOption("opt-c", 'c', IsRequired = true)] + public IReadOnlyList? OptC { get; set; } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/WithSingleParameterCommand.cs b/CliFx.Tests/Commands/WithSingleParameterCommand.cs new file mode 100644 index 0000000..e8ad78d --- /dev/null +++ b/CliFx.Tests/Commands/WithSingleParameterCommand.cs @@ -0,0 +1,11 @@ +using CliFx.Attributes; + +namespace CliFx.Tests.Commands +{ + [Command("cmd")] + public class WithSingleParameterCommand : SelfSerializeCommandBase + { + [CommandParameter(0)] + public string? ParamA { get; set; } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/WithSingleRequiredOptionCommand.cs b/CliFx.Tests/Commands/WithSingleRequiredOptionCommand.cs new file mode 100644 index 0000000..811e00f --- /dev/null +++ b/CliFx.Tests/Commands/WithSingleRequiredOptionCommand.cs @@ -0,0 +1,14 @@ +using CliFx.Attributes; + +namespace CliFx.Tests.Commands +{ + [Command("cmd")] + public class WithSingleRequiredOptionCommand : SelfSerializeCommandBase + { + [CommandOption("opt-a")] + public string? OptA { get; set; } + + [CommandOption("opt-b", IsRequired = true)] + public string? OptB { get; set; } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Commands/WithStringArrayOptionCommand.cs b/CliFx.Tests/Commands/WithStringArrayOptionCommand.cs new file mode 100644 index 0000000..bede424 --- /dev/null +++ b/CliFx.Tests/Commands/WithStringArrayOptionCommand.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using CliFx.Attributes; + +namespace CliFx.Tests.Commands +{ + [Command("cmd")] + public class WithStringArrayOptionCommand : SelfSerializeCommandBase + { + [CommandOption("opt", 'o')] + public IReadOnlyList? Opt { get; set; } + } +} \ No newline at end of file diff --git a/CliFx.Tests/ConsoleSpecs.cs b/CliFx.Tests/ConsoleSpecs.cs index f3ffc98..7c5050d 100644 --- a/CliFx.Tests/ConsoleSpecs.cs +++ b/CliFx.Tests/ConsoleSpecs.cs @@ -38,7 +38,8 @@ namespace CliFx.Tests var console = new VirtualConsole( input: stdIn, output: stdOut, - error: stdErr); + error: stdErr + ); // Act console.Output.Write("output"); diff --git a/CliFx.Tests/DependencyInjectionSpecs.Commands.cs b/CliFx.Tests/DependencyInjectionSpecs.Commands.cs deleted file mode 100644 index 6542b0f..0000000 --- a/CliFx.Tests/DependencyInjectionSpecs.Commands.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests -{ - public partial class DependencyInjectionSpecs - { - [Command] - private class WithoutDependenciesCommand : ICommand - { - public ValueTask ExecuteAsync(IConsole console) => default; - } - - private class DependencyA - { - } - - private class DependencyB - { - } - - [Command] - private class WithDependenciesCommand : ICommand - { - private readonly DependencyA _dependencyA; - private readonly DependencyB _dependencyB; - - public WithDependenciesCommand(DependencyA dependencyA, DependencyB dependencyB) - { - _dependencyA = dependencyA; - _dependencyB = dependencyB; - } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/DependencyInjectionSpecs.cs b/CliFx.Tests/DependencyInjectionSpecs.cs index e23a56e..05d1eff 100644 --- a/CliFx.Tests/DependencyInjectionSpecs.cs +++ b/CliFx.Tests/DependencyInjectionSpecs.cs @@ -1,11 +1,12 @@ using CliFx.Exceptions; +using CliFx.Tests.Commands; using FluentAssertions; using Xunit; using Xunit.Abstractions; namespace CliFx.Tests { - public partial class DependencyInjectionSpecs + public class DependencyInjectionSpecs { private readonly ITestOutputHelper _output; @@ -18,10 +19,10 @@ namespace CliFx.Tests var activator = new DefaultTypeActivator(); // Act - var obj = activator.CreateInstance(typeof(WithoutDependenciesCommand)); + var obj = activator.CreateInstance(typeof(DefaultCommand)); // Assert - obj.Should().BeOfType(); + obj.Should().BeOfType(); } [Fact] @@ -40,7 +41,10 @@ namespace CliFx.Tests { // Arrange var activator = new DelegateTypeActivator(_ => - new WithDependenciesCommand(new DependencyA(), new DependencyB())); + new WithDependenciesCommand( + new WithDependenciesCommand.DependencyA(), + new WithDependenciesCommand.DependencyB()) + ); // Act var obj = activator.CreateInstance(typeof(WithDependenciesCommand)); diff --git a/CliFx.Tests/DirectivesSpecs.Commands.cs b/CliFx.Tests/DirectivesSpecs.Commands.cs deleted file mode 100644 index 58c941e..0000000 --- a/CliFx.Tests/DirectivesSpecs.Commands.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests -{ - public partial class DirectivesSpecs - { - [Command("cmd")] - private class NamedCommand : ICommand - { - public ValueTask ExecuteAsync(IConsole console) => default; - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/DirectivesSpecs.cs b/CliFx.Tests/DirectivesSpecs.cs index 52d0812..65c02c3 100644 --- a/CliFx.Tests/DirectivesSpecs.cs +++ b/CliFx.Tests/DirectivesSpecs.cs @@ -1,13 +1,19 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; +using CliFx.Tests.Commands; using FluentAssertions; using Xunit; +using Xunit.Abstractions; namespace CliFx.Tests { - public partial class DirectivesSpecs + public class DirectivesSpecs { + private readonly ITestOutputHelper _output; + + public DirectivesSpecs(ITestOutputHelper output) => _output = output; + [Fact] public async Task Preview_directive_can_be_specified_to_print_provided_arguments_as_they_were_parsed() { @@ -16,21 +22,23 @@ namespace CliFx.Tests var console = new VirtualConsole(output: stdOut); var application = new CliApplicationBuilder() - .AddCommand(typeof(NamedCommand)) + .AddCommand() .UseConsole(console) .AllowPreviewMode() .Build(); // Act var exitCode = await application.RunAsync( - new[] {"[preview]", "cmd", "param", "-abc", "--option", "foo"}, + new[] {"[preview]", "named", "param", "-abc", "--option", "foo"}, new Dictionary()); var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); // Assert exitCode.Should().Be(0); - stdOutData.Should().ContainAll("cmd", "", "[-a]", "[-b]", "[-c]", "[--option \"foo\"]"); + stdOutData.Should().ContainAll("named", "", "[-a]", "[-b]", "[-c]", "[--option \"foo\"]"); + + _output.WriteLine(stdOutData); } } } \ No newline at end of file diff --git a/CliFx.Tests/EnvironmentVariablesSpecs.Commands.cs b/CliFx.Tests/EnvironmentVariablesSpecs.Commands.cs deleted file mode 100644 index 381e0b0..0000000 --- a/CliFx.Tests/EnvironmentVariablesSpecs.Commands.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests -{ - public partial class EnvironmentVariablesSpecs - { - [Command] - private class EnvironmentVariableCollectionCommand : ICommand - { - [CommandOption("opt", EnvironmentVariableName = "ENV_OPT")] - public IReadOnlyList? Option { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command] - private class EnvironmentVariableCommand : ICommand - { - [CommandOption("opt", EnvironmentVariableName = "ENV_OPT")] - public string? Option { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/EnvironmentVariablesSpecs.cs b/CliFx.Tests/EnvironmentVariablesSpecs.cs index f8d1b52..19e6cc6 100644 --- a/CliFx.Tests/EnvironmentVariablesSpecs.cs +++ b/CliFx.Tests/EnvironmentVariablesSpecs.cs @@ -1,20 +1,20 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; -using CliFx.Domain; -using CliFx.Tests.Internal; +using CliFx.Tests.Commands; using CliWrap; using CliWrap.Buffered; using FluentAssertions; +using Newtonsoft.Json; using Xunit; namespace CliFx.Tests { - public partial class EnvironmentVariablesSpecs + public class EnvironmentVariablesSpecs { // This test uses a real application to make sure environment variables are actually read correctly [Fact] - public async Task Option_can_use_a_specific_environment_variable_as_fallback() + public async Task Option_can_use_an_environment_variable_as_fallback() { // Arrange var command = Cli.Wrap("dotnet") @@ -32,7 +32,7 @@ namespace CliFx.Tests // This test uses a real application to make sure environment variables are actually read correctly [Fact] - public async Task Option_only_uses_environment_variable_as_fallback_if_the_value_was_not_directly_provided() + public async Task Option_only_uses_an_environment_variable_as_fallback_if_the_value_is_not_directly_provided() { // Arrange var command = Cli.Wrap("dotnet") @@ -51,65 +51,97 @@ namespace CliFx.Tests } [Fact] - public void Option_of_non_scalar_type_can_take_multiple_separated_values_from_an_environment_variable() + public async Task Option_only_uses_an_environment_variable_as_fallback_if_the_name_matches_case_sensitively() { - // Arrange - var input = CommandInput.Empty; - var envVars = new Dictionary - { - ["ENV_OPT"] = $"foo{Path.PathSeparator}bar" - }; + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); // Act - var instance = CommandHelper.ResolveCommand(input, envVars); + var exitCode = await application.RunAsync( + new[] {"cmd"}, + new Dictionary + { + ["ENV_opt_A"] = "incorrect", + ["ENV_OPT_A"] = "correct" + } + ); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); // Assert - instance.Should().BeEquivalentTo(new EnvironmentVariableCollectionCommand + exitCode.Should().Be(0); + commandInstance.Should().BeEquivalentTo(new WithEnvironmentVariablesCommand { - Option = new[] {"foo", "bar"} + OptA = "correct" }); } [Fact] - public void Option_of_scalar_type_can_only_take_a_single_value_from_an_environment_variable_even_if_it_contains_separators() + public async Task Option_of_non_scalar_type_can_use_an_environment_variable_as_fallback_and_extract_multiple_values() { - // Arrange - var input = CommandInput.Empty; - var envVars = new Dictionary - { - ["ENV_OPT"] = $"foo{Path.PathSeparator}bar" - }; + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); // Act - var instance = CommandHelper.ResolveCommand(input, envVars); + var exitCode = await application.RunAsync( + new[] {"cmd"}, + new Dictionary + { + ["ENV_OPT_B"] = $"foo{Path.PathSeparator}bar" + } + ); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); // Assert - instance.Should().BeEquivalentTo(new EnvironmentVariableCommand + exitCode.Should().Be(0); + commandInstance.Should().BeEquivalentTo(new WithEnvironmentVariablesCommand { - Option = $"foo{Path.PathSeparator}bar" + OptB = new[] {"foo", "bar"} }); } - + [Fact] - public void Option_can_use_a_specific_environment_variable_as_fallback_while_respecting_case() + public async Task Option_of_scalar_type_can_use_an_environment_variable_as_fallback_regardless_of_separators() { - // Arrange - const string expected = "foobar"; - var input = CommandInput.Empty; - var envVars = new Dictionary - { - ["ENV_OPT"] = expected, - ["env_opt"] = "2" - }; + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); // Act - var instance = CommandHelper.ResolveCommand(input, envVars); + var exitCode = await application.RunAsync( + new[] {"cmd"}, + new Dictionary + { + ["ENV_OPT_A"] = $"foo{Path.PathSeparator}bar" + } + ); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var commandInstance = JsonConvert.DeserializeObject(stdOutData); // Assert - instance.Should().BeEquivalentTo(new EnvironmentVariableCommand + exitCode.Should().Be(0); + commandInstance.Should().BeEquivalentTo(new WithEnvironmentVariablesCommand { - Option = expected + OptA = $"foo{Path.PathSeparator}bar" }); } } -} +} \ No newline at end of file diff --git a/CliFx.Tests/ErrorReportingSpecs.Commands.cs b/CliFx.Tests/ErrorReportingSpecs.Commands.cs deleted file mode 100644 index 28475b7..0000000 --- a/CliFx.Tests/ErrorReportingSpecs.Commands.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Threading.Tasks; -using CliFx.Attributes; -using CliFx.Exceptions; - -namespace CliFx.Tests -{ - public partial class ErrorReportingSpecs - { - [Command("exc")] - private class GenericExceptionCommand : ICommand - { - [CommandOption("msg", 'm')] - public string? Message { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => throw new Exception(Message); - } - - [Command("exc")] - private class CommandExceptionCommand : ICommand - { - [CommandOption("code", 'c')] - public int ExitCode { get; set; } = 133; - - [CommandOption("msg", 'm')] - public string? Message { get; set; } - - [CommandOption("show-help")] - public bool ShowHelp { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode, ShowHelp); - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/ErrorReportingSpecs.cs b/CliFx.Tests/ErrorReportingSpecs.cs index 7696030..84601bf 100644 --- a/CliFx.Tests/ErrorReportingSpecs.cs +++ b/CliFx.Tests/ErrorReportingSpecs.cs @@ -1,13 +1,13 @@ -using System.Collections.Generic; -using System.IO; +using System.IO; using System.Threading.Tasks; +using CliFx.Tests.Commands; using FluentAssertions; using Xunit; using Xunit.Abstractions; namespace CliFx.Tests { - public partial class ErrorReportingSpecs + public class ErrorReportingSpecs { private readonly ITestOutputHelper _output; @@ -17,28 +17,32 @@ namespace CliFx.Tests public async Task Command_may_throw_a_generic_exception_which_exits_and_prints_error_message_and_stack_trace() { // Arrange + await using var stdOut = new MemoryStream(); await using var stdErr = new MemoryStream(); - var console = new VirtualConsole(error: stdErr); + + var console = new VirtualConsole(output: stdOut, error: stdErr); var application = new CliApplicationBuilder() - .AddCommand(typeof(GenericExceptionCommand)) + .AddCommand() .UseConsole(console) .Build(); // Act - var exitCode = await application.RunAsync( - new[] {"exc", "-m", "Kaput"}, - new Dictionary()); + var exitCode = await application.RunAsync(new[] {"cmd", "-m", "Kaput"}); + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); // Assert exitCode.Should().NotBe(0); + stdOutData.Should().BeEmpty(); stdErrData.Should().ContainAll( "System.Exception:", "Kaput", "at", - "CliFx.Tests"); + "CliFx.Tests" + ); + _output.WriteLine(stdOutData); _output.WriteLine(stdErrData); } @@ -46,25 +50,28 @@ namespace CliFx.Tests public async Task Command_may_throw_a_specialized_exception_which_exits_with_custom_code_and_prints_minimal_error_details() { // Arrange + await using var stdOut = new MemoryStream(); await using var stdErr = new MemoryStream(); - var console = new VirtualConsole(error: stdErr); + + var console = new VirtualConsole(output: stdOut, error: stdErr); var application = new CliApplicationBuilder() - .AddCommand(typeof(CommandExceptionCommand)) + .AddCommand() .UseConsole(console) .Build(); // Act - var exitCode = await application.RunAsync( - new[] {"exc", "-m", "Kaput", "-c", "69"}, - new Dictionary()); + var exitCode = await application.RunAsync(new[] {"cmd", "-m", "Kaput", "-c", "69"}); + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); // Assert exitCode.Should().Be(69); + stdOutData.Should().BeEmpty(); stdErrData.Should().Be("Kaput"); + _output.WriteLine(stdOutData); _output.WriteLine(stdErrData); } @@ -72,28 +79,32 @@ namespace CliFx.Tests public async Task Command_may_throw_a_specialized_exception_without_error_message_which_exits_and_prints_full_error_details() { // Arrange + await using var stdOut = new MemoryStream(); await using var stdErr = new MemoryStream(); - var console = new VirtualConsole(error: stdErr); + + var console = new VirtualConsole(output: stdOut, error: stdErr); var application = new CliApplicationBuilder() - .AddCommand(typeof(CommandExceptionCommand)) + .AddCommand() .UseConsole(console) .Build(); // Act - var exitCode = await application.RunAsync( - new[] {"exc"}, - new Dictionary()); + var exitCode = await application.RunAsync(new[] {"cmd"}); + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); // Assert exitCode.Should().NotBe(0); + stdOutData.Should().BeEmpty(); stdErrData.Should().ContainAll( "CliFx.Exceptions.CommandException:", "at", - "CliFx.Tests"); + "CliFx.Tests" + ); + _output.WriteLine(stdOutData); _output.WriteLine(stdErrData); } @@ -107,30 +118,27 @@ namespace CliFx.Tests var console = new VirtualConsole(output: stdOut, error: stdErr); var application = new CliApplicationBuilder() - .AddCommand(typeof(CommandExceptionCommand)) + .AddCommand() .UseConsole(console) .Build(); // Act - var exitCode = await application.RunAsync( - new[] {"exc", "-m", "Kaput", "--show-help"}, - new Dictionary()); + var exitCode = await application.RunAsync(new[] {"cmd", "-m", "Kaput", "--show-help"}); - var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); // Assert exitCode.Should().NotBe(0); - stdErrData.Should().Be("Kaput"); - stdOutData.Should().ContainAll( "Usage", "Options", - "-h|--help", "Shows help text." + "-h|--help" ); + stdErrData.Should().Be("Kaput"); - _output.WriteLine(stdErrData); _output.WriteLine(stdOutData); + _output.WriteLine(stdErrData); } [Fact] @@ -139,34 +147,31 @@ namespace CliFx.Tests // Arrange await using var stdOut = new MemoryStream(); await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(output: stdOut, error: stdErr); var application = new CliApplicationBuilder() - .AddCommand(typeof(CommandExceptionCommand)) + .AddCommand() .UseConsole(console) .Build(); // Act - var exitCode = await application.RunAsync( - new[] {"not-a-valid-command", "-r", "foo"}, - new Dictionary()); + var exitCode = await application.RunAsync(new[] {"not-a-valid-command", "-r", "foo"}); - var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); // Assert exitCode.Should().NotBe(0); - stdErrData.Should().NotBeNullOrWhiteSpace(); - stdOutData.Should().ContainAll( "Usage", - "[command]", "Options", - "-h|--help", "Shows help text." + "-h|--help" ); + stdErrData.Should().NotBeNullOrWhiteSpace(); - _output.WriteLine(stdErrData); _output.WriteLine(stdOutData); + _output.WriteLine(stdErrData); } } } \ No newline at end of file diff --git a/CliFx.Tests/HelpTextSpecs.Commands.cs b/CliFx.Tests/HelpTextSpecs.Commands.cs deleted file mode 100644 index 430ee85..0000000 --- a/CliFx.Tests/HelpTextSpecs.Commands.cs +++ /dev/null @@ -1,155 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests -{ - public partial class HelpTextSpecs - { - [Command(Description = "DefaultCommand description.")] - private class DefaultCommand : ICommand - { - [CommandOption("option-a", 'a', Description = "OptionA description.")] - public string? OptionA { get; set; } - - [CommandOption("option-b", 'b', Description = "OptionB description.")] - public string? OptionB { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command("cmd", Description = "NamedCommand description.")] - private class NamedCommand : ICommand - { - [CommandParameter(0, Name = "param-a", Description = "ParameterA description.")] - public string? ParameterA { get; set; } - - [CommandOption("option-c", 'c', Description = "OptionC description.")] - public string? OptionC { get; set; } - - [CommandOption("option-d", 'd', Description = "OptionD description.")] - public string? OptionD { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command("cmd sub", Description = "NamedSubCommand description.")] - private class NamedSubCommand : ICommand - { - [CommandParameter(0, Name = "param-b", Description = "ParameterB description.")] - public string? ParameterB { get; set; } - - [CommandParameter(1, Name = "param-c", Description = "ParameterC description.")] - public string? ParameterC { get; set; } - - [CommandOption("option-e", 'e', Description = "OptionE description.")] - public string? OptionE { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command("cmd-with-params")] - private class ParametersCommand : ICommand - { - [CommandParameter(0, Name = "first")] - public string? ParameterA { get; set; } - - [CommandParameter(10)] - public int? ParameterB { get; set; } - - [CommandParameter(20, Description = "A list of numbers", Name = "third list")] - public IEnumerable? ParameterC { get; set; } - - [CommandOption("option", 'o')] - public string? Option { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command("cmd-with-req-opts")] - private class RequiredOptionsCommand : ICommand - { - [CommandOption("option-a", 'a', IsRequired = true)] - public string? OptionA { get; set; } - - [CommandOption("option-b", 'b', IsRequired = true)] - public IEnumerable? OptionB { get; set; } - - [CommandOption("option-c", 'c')] - public string? OptionC { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command("cmd-with-enum-args")] - private class EnumArgumentsCommand : ICommand - { - public enum CustomEnum { Value1, Value2, Value3 }; - - [CommandParameter(0, Name = "value", Description = "Enum parameter.")] - public CustomEnum ParamA { get; set; } - - [CommandOption("value", Description = "Enum option.", IsRequired = true)] - public CustomEnum OptionA { get; set; } = CustomEnum.Value1; - - [CommandOption("nullable-value", Description = "Nullable enum option.")] - public CustomEnum? OptionB { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command("cmd-with-env-vars")] - private class EnvironmentVariableCommand : ICommand - { - [CommandOption("option-a", 'a', IsRequired = true, EnvironmentVariableName = "ENV_OPT_A")] - public string? OptionA { get; set; } - - [CommandOption("option-b", 'b', EnvironmentVariableName = "ENV_OPT_B")] - public string? OptionB { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } - - [Command("cmd-with-defaults")] - private class ArgumentsWithDefaultValuesCommand : ICommand - { - public enum CustomEnum { Value1, Value2, Value3 }; - - [CommandOption(nameof(Object))] - public object? Object { get; set; } = 42; - - [CommandOption(nameof(String))] - public string? String { get; set; } = "foo"; - - [CommandOption(nameof(EmptyString))] - public string EmptyString { get; set; } = ""; - - [CommandOption(nameof(Bool))] - public bool Bool { get; set; } = true; - - [CommandOption(nameof(Char))] - public char Char { get; set; } = 't'; - - [CommandOption(nameof(Int))] - public int Int { get; set; } = 1337; - - [CommandOption(nameof(TimeSpan))] - public TimeSpan TimeSpan { get; set; } = TimeSpan.FromMinutes(123); - - [CommandOption(nameof(Enum))] - public CustomEnum Enum { get; set; } = CustomEnum.Value2; - - [CommandOption(nameof(IntNullable))] - public int? IntNullable { get; set; } = 1337; - - [CommandOption(nameof(StringArray))] - public string[]? StringArray { get; set; } = { "foo", "bar", "baz" }; - - [CommandOption(nameof(IntArray))] - public int[]? IntArray { get; set; } = { 1, 2, 3 }; - - public ValueTask ExecuteAsync(IConsole console) => default; - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/HelpTextSpecs.cs b/CliFx.Tests/HelpTextSpecs.cs index de8e06a..3358752 100644 --- a/CliFx.Tests/HelpTextSpecs.cs +++ b/CliFx.Tests/HelpTextSpecs.cs @@ -1,189 +1,18 @@ using System.IO; using System.Threading.Tasks; +using CliFx.Tests.Commands; using FluentAssertions; using Xunit; using Xunit.Abstractions; namespace CliFx.Tests { - public partial class HelpTextSpecs + public class HelpTextSpecs { private readonly ITestOutputHelper _output; public HelpTextSpecs(ITestOutputHelper output) => _output = output; - [Fact] - public async Task Version_information_can_be_requested_by_providing_the_version_option_without_other_arguments() - { - // Arrange - await using var stdOut = new MemoryStream(); - var console = new VirtualConsole(output: stdOut); - - var application = new CliApplicationBuilder() - .AddCommand(typeof(DefaultCommand)) - .AddCommand(typeof(NamedCommand)) - .AddCommand(typeof(NamedSubCommand)) - .UseVersionText("v6.9") - .UseConsole(console) - .Build(); - - // Act - var exitCode = await application.RunAsync(new[] {"--version"}); - var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); - - // Assert - exitCode.Should().Be(0); - stdOutData.Should().Be("v6.9"); - - _output.WriteLine(stdOutData); - } - - [Fact] - public async Task Help_text_can_be_requested_by_providing_the_help_option() - { - // Arrange - await using var stdOut = new MemoryStream(); - var console = new VirtualConsole(output: stdOut); - - var application = new CliApplicationBuilder() - .AddCommand(typeof(DefaultCommand)) - .AddCommand(typeof(NamedCommand)) - .AddCommand(typeof(NamedSubCommand)) - .UseTitle("AppTitle") - .UseVersionText("AppVer") - .UseDescription("AppDesc") - .UseExecutableName("AppExe") - .UseConsole(console) - .Build(); - - // Act - await application.RunAsync(new[] {"--help"}); - var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); - - // Assert - stdOutData.Should().ContainAll( - "AppTitle", "AppVer", - "AppDesc", - "Usage", - "AppExe", "[command]", "[options]", - "Options", - "-a|--option-a", "OptionA description.", - "-b|--option-b", "OptionB description.", - "-h|--help", "Shows help text.", - "--version", "Shows version information.", - "Commands", - "cmd", "NamedCommand description.", - "You can run", "to show help on a specific command." - ); - - _output.WriteLine(stdOutData); - } - - [Fact] - public async Task Help_text_can_be_requested_on_a_specific_named_command() - { - // Arrange - await using var stdOut = new MemoryStream(); - var console = new VirtualConsole(output: stdOut); - - var application = new CliApplicationBuilder() - .AddCommand(typeof(DefaultCommand)) - .AddCommand(typeof(NamedCommand)) - .AddCommand(typeof(NamedSubCommand)) - .UseConsole(console) - .Build(); - - // Act - await application.RunAsync(new[] {"cmd", "--help"}); - var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); - - // Assert - stdOutData.Should().ContainAll( - "Description", - "NamedCommand description.", - "Usage", - "cmd", "[command]", "", "[options]", - "Parameters", - "* param-a", "ParameterA description.", - "Options", - "-c|--option-c", "OptionC description.", - "-d|--option-d", "OptionD description.", - "-h|--help", "Shows help text.", - "Commands", - "sub", "SubCommand description.", - "You can run", "to show help on a specific command." - ); - - _output.WriteLine(stdOutData); - } - - [Fact] - public async Task Help_text_can_be_requested_on_a_specific_named_sub_command() - { - // Arrange - await using var stdOut = new MemoryStream(); - var console = new VirtualConsole(output: stdOut); - - var application = new CliApplicationBuilder() - .AddCommand(typeof(DefaultCommand)) - .AddCommand(typeof(NamedCommand)) - .AddCommand(typeof(NamedSubCommand)) - .UseConsole(console) - .Build(); - - // Act - await application.RunAsync(new[] {"cmd", "sub", "--help"}); - var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); - - // Assert - stdOutData.Should().ContainAll( - "Description", - "SubCommand description.", - "Usage", - "cmd sub", "", "", "[options]", - "Parameters", - "* param-b", "ParameterB description.", - "* param-c", "ParameterC description.", - "Options", - "-e|--option-e", "OptionE description.", - "-h|--help", "Shows help text." - ); - - _output.WriteLine(stdOutData); - } - - [Fact] - public async Task Help_text_can_be_requested_without_specifying_command_even_if_default_command_is_not_defined() - { - // Arrange - await using var stdOut = new MemoryStream(); - var console = new VirtualConsole(output: stdOut); - - var application = new CliApplicationBuilder() - .AddCommand(typeof(NamedCommand)) - .AddCommand(typeof(NamedSubCommand)) - .UseConsole(console) - .Build(); - - // Act - await application.RunAsync(new[] {"--help"}); - var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); - - // Assert - stdOutData.Should().ContainAll( - "Usage", - "[command]", - "Options", - "-h|--help", "Shows help text.", - "--version", "Shows version information.", - "Commands", - "cmd", "NamedCommand description.", - "You can run", "to show help on a specific command." - ); - - _output.WriteLine(stdOutData); - } - [Fact] public async Task Help_text_shows_usage_format_which_lists_all_parameters() { @@ -192,18 +21,19 @@ namespace CliFx.Tests var console = new VirtualConsole(output: stdOut); var application = new CliApplicationBuilder() - .AddCommand(typeof(ParametersCommand)) + .AddCommand() .UseConsole(console) .Build(); // Act - await application.RunAsync(new[] {"cmd-with-params", "--help"}); + var exitCode = await application.RunAsync(new[] {"cmd", "--help"}); var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); // Assert + exitCode.Should().Be(0); stdOutData.Should().ContainAll( "Usage", - "cmd-with-params", "", "", "", "[options]" + "cmd", "", "", "" ); _output.WriteLine(stdOutData); @@ -217,78 +47,79 @@ namespace CliFx.Tests var console = new VirtualConsole(output: stdOut); var application = new CliApplicationBuilder() - .AddCommand(typeof(RequiredOptionsCommand)) + .AddCommand() .UseConsole(console) .Build(); // Act - await application.RunAsync(new[] {"cmd-with-req-opts", "--help"}); + var exitCode = await application.RunAsync(new[] {"cmd", "--help"}); var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); // Assert + exitCode.Should().Be(0); stdOutData.Should().ContainAll( "Usage", - "cmd-with-req-opts", "--option-a ", "--option-b ", "[options]", + "cmd", "--opt-a ", "--opt-c ", "[options]", "Options", - "* -a|--option-a", - "* -b|--option-b", - "-c|--option-c" + "* -a|--opt-a", + "-b|--opt-b", + "* -c|--opt-c" ); _output.WriteLine(stdOutData); } [Fact] - public async Task Help_text_lists_all_valid_values_for_enum_arguments() + public async Task Help_text_shows_all_valid_values_for_enum_arguments() { // Arrange await using var stdOut = new MemoryStream(); var console = new VirtualConsole(output: stdOut); var application = new CliApplicationBuilder() - .AddCommand(typeof(EnumArgumentsCommand)) + .AddCommand() .UseConsole(console) .Build(); // Act - await application.RunAsync(new[] {"cmd-with-enum-args", "--help"}); + var exitCode = await application.RunAsync(new[] {"cmd", "--help"}); var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); // Assert + exitCode.Should().Be(0); stdOutData.Should().ContainAll( - "Usage", - "cmd-with-enum-args", "[options]", "Parameters", - "value", "Valid values: \"Value1\", \"Value2\", \"Value3\".", + "enum", "Valid values: \"Value1\", \"Value2\", \"Value3\".", "Options", - "* --value", "Enum option.", "Valid values: \"Value1\", \"Value2\", \"Value3\".", - "--nullable-value", "Nullable enum option.", "Valid values: \"Value1\", \"Value2\", \"Value3\"." + "--enum", "Valid values: \"Value1\", \"Value2\", \"Value3\".", + "* --required-enum", "Valid values: \"Value1\", \"Value2\", \"Value3\"." ); _output.WriteLine(stdOutData); } [Fact] - public async Task Help_text_lists_environment_variable_names_for_options_that_have_them_defined() + public async Task Help_text_shows_environment_variable_names_for_options_that_have_them_defined() { // Arrange await using var stdOut = new MemoryStream(); var console = new VirtualConsole(output: stdOut); var application = new CliApplicationBuilder() - .AddCommand(typeof(EnvironmentVariableCommand)) + .AddCommand() .UseConsole(console) .Build(); // Act - await application.RunAsync(new[] {"cmd-with-env-vars", "--help"}); + var exitCode = await application.RunAsync(new[] {"cmd", "--help"}); var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); // Assert + exitCode.Should().Be(0); stdOutData.Should().ContainAll( "Options", - "* -a|--option-a", "Environment variable:", "ENV_OPT_A", - "-b|--option-b", "Environment variable:", "ENV_OPT_B" + "-a|--opt-a", "Environment variable:", "ENV_OPT_A", + "-b|--opt-b", "Environment variable:", "ENV_OPT_B" ); _output.WriteLine(stdOutData); @@ -302,30 +133,29 @@ namespace CliFx.Tests var console = new VirtualConsole(output: stdOut); var application = new CliApplicationBuilder() - .AddCommand(typeof(ArgumentsWithDefaultValuesCommand)) + .AddCommand() .UseConsole(console) .Build(); // Act - await application.RunAsync(new[] {"cmd-with-defaults", "--help"}); + var exitCode = await application.RunAsync(new[] {"cmd", "--help"}); var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); // Assert + exitCode.Should().Be(0); stdOutData.Should().ContainAll( - "Usage", - "cmd-with-defaults", "[options]", "Options", - "--Object", "Default: \"42\"", - "--String", "Default: \"foo\"", - "--EmptyString", "Default: \"\"", - "--Bool", "Default: \"True\"", - "--Char", "Default: \"t\"", - "--Int", "Default: \"1337\"", - "--TimeSpan", "Default: \"02:03:00\"", - "--Enum", "Default: \"Value2\"", - "--IntNullable", "Default: \"1337\"", - "--StringArray", "Default: \"foo\" \"bar\" \"baz\"", - "--IntArray", "Default: \"1\" \"2\" \"3\"" + "--obj", "Default: \"42\"", + "--str", "Default: \"foo\"", + "--str-empty", "Default: \"\"", + "--str-array", "Default: \"foo\" \"bar\" \"baz\"", + "--bool", "Default: \"True\"", + "--char", "Default: \"t\"", + "--int", "Default: \"1337\"", + "--int-nullable", "Default: \"1337\"", + "--int-array", "Default: \"1\" \"2\" \"3\"", + "--timespan", "Default: \"02:03:00\"", + "--enum", "Default: \"Value2\"" ); _output.WriteLine(stdOutData); diff --git a/CliFx.Tests/Internal/CommandHelper.cs b/CliFx.Tests/Internal/CommandHelper.cs deleted file mode 100644 index 3b83741..0000000 --- a/CliFx.Tests/Internal/CommandHelper.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using CliFx.Domain; - -namespace CliFx.Tests.Internal -{ - internal static class CommandHelper - { - public static TCommand ResolveCommand(CommandInput input, IReadOnlyDictionary environmentVariables) - where TCommand : ICommand, new() - { - var schema = CommandSchema.TryResolve(typeof(TCommand))!; - - var instance = new TCommand(); - schema.Bind(instance, input, environmentVariables); - - return instance; - } - - public static TCommand ResolveCommand(CommandInput input) - where TCommand : ICommand, new() => - ResolveCommand(input, new Dictionary()); - } -} \ No newline at end of file diff --git a/CliFx.Tests/Internal/CommandInputBuilder.cs b/CliFx.Tests/Internal/CommandInputBuilder.cs deleted file mode 100644 index 519599f..0000000 --- a/CliFx.Tests/Internal/CommandInputBuilder.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Collections.Generic; -using CliFx.Domain; - -namespace CliFx.Tests.Internal -{ - internal class CommandInputBuilder - { - private readonly List _directives = new List(); - private readonly List _parameters = new List(); - private readonly List _options = new List(); - - private string? _commandName; - - public CommandInputBuilder SetCommandName(string commandName) - { - _commandName = commandName; - return this; - } - - public CommandInputBuilder AddDirective(string directive) - { - _directives.Add(new CommandDirectiveInput(directive)); - return this; - } - - public CommandInputBuilder AddParameter(string parameter) - { - _parameters.Add(new CommandParameterInput(parameter)); - return this; - } - - public CommandInputBuilder AddOption(string alias, params string[] values) - { - _options.Add(new CommandOptionInput(alias, values)); - return this; - } - - public CommandInput Build() => new CommandInput( - _directives, - _commandName, - _parameters, - _options - ); - } -} \ No newline at end of file diff --git a/CliFx.Tests/Internal/TaskExtensions.cs b/CliFx.Tests/Internal/TaskExtensions.cs deleted file mode 100644 index faab8fd..0000000 --- a/CliFx.Tests/Internal/TaskExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace CliFx.Tests.Internal -{ - internal static class TaskExtensions - { - public static async Task IgnoreCancellation(this Task task) - { - try - { - await task; - } - catch (OperationCanceledException) - { - } - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/RoutingSpecs.Commands.cs b/CliFx.Tests/RoutingSpecs.Commands.cs deleted file mode 100644 index f4d5bd1..0000000 --- a/CliFx.Tests/RoutingSpecs.Commands.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests -{ - public partial class RoutingSpecs - { - [Command] - private class DefaultCommand : ICommand - { - public ValueTask ExecuteAsync(IConsole console) - { - console.Output.WriteLine("Hello world!"); - return default; - } - } - - [Command("concat", Description = "Concatenate strings.")] - private class ConcatCommand : ICommand - { - [CommandOption('i', IsRequired = true, Description = "Input strings.")] - public IReadOnlyList Inputs { get; set; } = Array.Empty(); - - [CommandOption('s', Description = "String separator.")] - public string Separator { get; set; } = ""; - - public ValueTask ExecuteAsync(IConsole console) - { - console.Output.WriteLine(string.Join(Separator, Inputs)); - return default; - } - } - - [Command("div", Description = "Divide one number by another.")] - private class DivideCommand : ICommand - { - [CommandOption("dividend", 'D', IsRequired = true, Description = "The number to divide.")] - public double Dividend { get; set; } = 0; - - [CommandOption("divisor", 'd', IsRequired = true, Description = "The number to divide by.")] - public double Divisor { get; set; } = 0; - - public ValueTask ExecuteAsync(IConsole console) - { - console.Output.WriteLine(Dividend / Divisor); - return default; - } - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/RoutingSpecs.cs b/CliFx.Tests/RoutingSpecs.cs index e6b8807..3a8341b 100644 --- a/CliFx.Tests/RoutingSpecs.cs +++ b/CliFx.Tests/RoutingSpecs.cs @@ -1,14 +1,19 @@ using System; -using System.Collections.Generic; using System.IO; using System.Threading.Tasks; +using CliFx.Tests.Commands; using FluentAssertions; using Xunit; +using Xunit.Abstractions; namespace CliFx.Tests { - public partial class RoutingSpecs + public class RoutingSpecs { + private readonly ITestOutputHelper _output; + + public RoutingSpecs(ITestOutputHelper testOutput) => _output = testOutput; + [Fact] public async Task Default_command_is_executed_if_provided_arguments_do_not_match_any_named_command() { @@ -17,48 +22,21 @@ namespace CliFx.Tests var console = new VirtualConsole(output: stdOut); var application = new CliApplicationBuilder() - .AddCommand(typeof(DefaultCommand)) - .AddCommand(typeof(ConcatCommand)) - .AddCommand(typeof(DivideCommand)) + .AddCommand() + .AddCommand() + .AddCommand() .UseConsole(console) .Build(); // Act - var exitCode = await application.RunAsync( - Array.Empty(), - new Dictionary()); - + var exitCode = await application.RunAsync(Array.Empty()); var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); // Assert exitCode.Should().Be(0); - stdOutData.Should().Be("Hello world!"); - } + stdOutData.Should().Be(DefaultCommand.ExpectedOutputText); - [Fact] - public async Task Help_text_is_printed_if_no_arguments_were_provided_and_default_command_is_not_defined() - { - // Arrange - await using var stdOut = new MemoryStream(); - var console = new VirtualConsole(output: stdOut); - - var application = new CliApplicationBuilder() - .AddCommand(typeof(ConcatCommand)) - .AddCommand(typeof(DivideCommand)) - .UseConsole(console) - .UseDescription("This will be visible in help") - .Build(); - - // Act - var exitCode = await application.RunAsync( - Array.Empty(), - new Dictionary()); - - var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); - - // Assert - exitCode.Should().Be(0); - stdOutData.Should().Contain("This will be visible in help"); + _output.WriteLine(stdOutData); } [Fact] @@ -69,22 +47,208 @@ namespace CliFx.Tests var console = new VirtualConsole(output: stdOut); var application = new CliApplicationBuilder() - .AddCommand(typeof(DefaultCommand)) - .AddCommand(typeof(ConcatCommand)) - .AddCommand(typeof(DivideCommand)) + .AddCommand() + .AddCommand() + .AddCommand() .UseConsole(console) .Build(); // Act - var exitCode = await application.RunAsync( - new[] {"concat", "-i", "foo", "bar", "-s", ", "}, - new Dictionary()); - + var exitCode = await application.RunAsync(new[] {"named"}); var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); // Assert exitCode.Should().Be(0); - stdOutData.Should().Be("foo, bar"); + stdOutData.Should().Be(NamedCommand.ExpectedOutputText); + + _output.WriteLine(stdOutData); + } + + [Fact] + public async Task Specific_named_sub_command_is_executed_if_provided_arguments_match_its_name() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .AddCommand() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] {"named", "sub"}); + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().Be(0); + stdOutData.Should().Be(NamedSubCommand.ExpectedOutputText); + + _output.WriteLine(stdOutData); + } + + [Fact] + public async Task Help_text_is_printed_if_no_arguments_were_provided_and_default_command_is_not_defined() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .AddCommand() + .UseConsole(console) + .UseDescription("This will be visible in help") + .Build(); + + // Act + var exitCode = await application.RunAsync(Array.Empty()); + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().Be(0); + stdOutData.Should().Contain("This will be visible in help"); + + _output.WriteLine(stdOutData); + } + + [Fact] + public async Task Help_text_is_printed_if_provided_arguments_contain_the_help_option() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .AddCommand() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] {"--help"}); + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().Be(0); + stdOutData.Should().ContainAll( + nameof(DefaultCommand), + "Usage" + ); + + _output.WriteLine(stdOutData); + } + + [Fact] + public async Task Help_text_is_printed_if_provided_arguments_contain_the_help_option_even_if_default_command_is_not_defined() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .AddCommand() + .UseDescription("This will be visible in help") + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] {"--help"}); + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().Be(0); + stdOutData.Should().Contain("This will be visible in help"); + + _output.WriteLine(stdOutData); + } + + [Fact] + public async Task Help_text_for_a_specific_named_command_is_printed_if_provided_arguments_match_its_name_and_contain_the_help_option() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .AddCommand() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] {"named", "--help"}); + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().Be(0); + stdOutData.Should().ContainAll( + nameof(NamedCommand), + "Usage", + "named" + ); + + _output.WriteLine(stdOutData); + } + + [Fact] + public async Task Help_text_for_a_specific_named_sub_command_is_printed_if_provided_arguments_match_its_name_and_contain_the_help_option() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .AddCommand() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] {"named", "sub", "--help"}); + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().Be(0); + stdOutData.Should().ContainAll( + nameof(NamedSubCommand), + "Usage", + "named", "sub" + ); + + _output.WriteLine(stdOutData); + } + + [Fact] + public async Task Version_is_printed_if_the_only_provided_argument_is_the_version_option() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand() + .AddCommand() + .AddCommand() + .UseVersionText("v6.9") + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] {"--version"}); + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().Be(0); + stdOutData.Should().Be("v6.9"); + + _output.WriteLine(stdOutData); } } } \ No newline at end of file diff --git a/CliFx/CliApplicationBuilder.cs b/CliFx/CliApplicationBuilder.cs index d75c135..dcce7e5 100644 --- a/CliFx/CliApplicationBuilder.cs +++ b/CliFx/CliApplicationBuilder.cs @@ -34,6 +34,12 @@ namespace CliFx return this; } + /// + /// Adds a command of specified type to the application. + /// + public CliApplicationBuilder AddCommand() where TCommand : ICommand => + AddCommand(typeof(TCommand)); + /// /// Adds multiple commands to the application. /// diff --git a/CliFx/CliFx.csproj b/CliFx/CliFx.csproj index fed64c1..492c8b4 100644 --- a/CliFx/CliFx.csproj +++ b/CliFx/CliFx.csproj @@ -23,15 +23,6 @@ annotations - - - <_Parameter1>$(AssemblyName).Tests - - - <_Parameter1>$(AssemblyName).Analyzers - - - diff --git a/CliFx/Domain/CommandArgumentSchema.cs b/CliFx/Domain/CommandArgumentSchema.cs index 59f74b5..3dd0be1 100644 --- a/CliFx/Domain/CommandArgumentSchema.cs +++ b/CliFx/Domain/CommandArgumentSchema.cs @@ -48,7 +48,7 @@ namespace CliFx.Domain ? ConvertScalar(value, nullableUnderlyingType) : null; - // String-constructable + // String-constructible var stringConstructor = targetType.GetConstructor(new[] {typeof(string)}); if (stringConstructor != null) return stringConstructor.Invoke(new object[] {value!}); @@ -83,7 +83,7 @@ namespace CliFx.Domain if (targetEnumerableType.IsAssignableFrom(arrayType)) return array; - // Constructable from an array + // Constructible from an array var arrayConstructor = targetEnumerableType.GetConstructor(new[] {arrayType}); if (arrayConstructor != null) return arrayConstructor.Invoke(new object[] {array}); diff --git a/Readme.md b/Readme.md index d101dbd..b9258db 100644 --- a/Readme.md +++ b/Readme.md @@ -573,7 +573,7 @@ public async Task ConcatCommand_Test() var console = new VirtualConsole(output: stdOut); var app = new CliApplicationBuilder() - .AddCommand(typeof(ConcatCommand)) + .AddCommand() .UseConsole(console) .Build();