diff --git a/CliFx.Tests.Dummy/Commands/ConsoleTestCommand.cs b/CliFx.Tests.Dummy/Commands/ConsoleTestCommand.cs new file mode 100644 index 0000000..90c561b --- /dev/null +++ b/CliFx.Tests.Dummy/Commands/ConsoleTestCommand.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading.Tasks; +using CliFx.Attributes; + +namespace CliFx.Tests.Dummy.Commands +{ + [Command("console-test")] + public class ConsoleTestCommand : ICommand + { + public ValueTask ExecuteAsync(IConsole console) + { + var input = console.Input.ReadToEnd(); + + console.WithColors(ConsoleColor.Black, ConsoleColor.White, () => + { + console.Output.WriteLine(input); + console.Error.WriteLine(input); + }); + + return default; + } + } +} \ No newline at end of file diff --git a/CliFx.Tests.Dummy/Program.cs b/CliFx.Tests.Dummy/Program.cs index 7ab41e5..5218a77 100644 --- a/CliFx.Tests.Dummy/Program.cs +++ b/CliFx.Tests.Dummy/Program.cs @@ -1,8 +1,16 @@ -using System.Threading.Tasks; +using System.Reflection; +using System.Threading.Tasks; namespace CliFx.Tests.Dummy { - public class Program + public static partial class Program + { + public static Assembly Assembly { get; } = typeof(Program).Assembly; + + public static string Location { get; } = Assembly.Location; + } + + public static partial class Program { public static async Task Main() => await new CliApplicationBuilder() diff --git a/CliFx.Tests/ApplicationSpecs.Commands.cs b/CliFx.Tests/ApplicationSpecs.Commands.cs new file mode 100644 index 0000000..24efa20 --- /dev/null +++ b/CliFx.Tests/ApplicationSpecs.Commands.cs @@ -0,0 +1,137 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using CliFx.Attributes; + +namespace CliFx.Tests +{ + public partial class ApplicationSpecs + { + [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 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 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] + private class ValidCommand : ICommand + { + 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 new file mode 100644 index 0000000..8fb086b --- /dev/null +++ b/CliFx.Tests/ApplicationSpecs.cs @@ -0,0 +1,197 @@ +using System; +using System.IO; +using CliFx.Domain; +using CliFx.Exceptions; +using FluentAssertions; +using Xunit; + +namespace CliFx.Tests +{ + public partial class ApplicationSpecs + { + [Fact] + public void Application_can_be_created_with_a_default_configuration() + { + // Act + var app = new CliApplicationBuilder() + .AddCommandsFromThisAssembly() + .Build(); + + // Assert + app.Should().NotBeNull(); + } + + [Fact] + public void Application_can_be_created_with_a_custom_configuration() + { + // Act + var app = new CliApplicationBuilder() + .AddCommand(typeof(ValidCommand)) + .AddCommandsFrom(typeof(ValidCommand).Assembly) + .AddCommands(new[] {typeof(ValidCommand)}) + .AddCommandsFrom(new[] {typeof(ValidCommand).Assembly}) + .AddCommandsFromThisAssembly() + .AllowDebugMode() + .AllowPreviewMode() + .UseTitle("test") + .UseExecutableName("test") + .UseVersionText("test") + .UseDescription("test") + .UseConsole(new VirtualConsole(Stream.Null)) + .UseTypeActivator(Activator.CreateInstance) + .Build(); + + // Assert + app.Should().NotBeNull(); + } + + [Fact] + public void At_least_one_command_must_be_defined_in_an_application() + { + // Arrange + var commandTypes = Array.Empty(); + + // Act & assert + Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + } + + [Fact] + public void Commands_must_implement_the_corresponding_interface() + { + // Arrange + var commandTypes = new[] {typeof(NonImplementedCommand)}; + + // Act & assert + Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + } + + [Fact] + public void Commands_must_be_annotated_by_an_attribute() + { + // Arrange + var commandTypes = new[] {typeof(NonAnnotatedCommand)}; + + // Act & assert + Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + } + + [Fact] + public void Commands_must_have_unique_names() + { + // Arrange + var commandTypes = new[] {typeof(DuplicateNameCommandA), typeof(DuplicateNameCommandB)}; + + // Act & assert + Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + } + + [Fact] + public void Command_parameters_must_have_unique_order() + { + // Arrange + var commandTypes = new[] {typeof(DuplicateParameterOrderCommand)}; + + // Act & assert + Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + } + + [Fact] + public void Command_parameters_must_have_unique_names() + { + // Arrange + var commandTypes = new[] {typeof(DuplicateParameterNameCommand)}; + + // Act & assert + Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + } + + [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 + Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + } + + [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 + Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + } + + [Fact] + public void Command_options_must_have_unique_names() + { + // Arrange + var commandTypes = new[] {typeof(DuplicateOptionNamesCommand)}; + + // Act & assert + Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + } + + [Fact] + public void Command_options_must_have_unique_short_names() + { + // Arrange + var commandTypes = new[] {typeof(DuplicateOptionShortNamesCommand)}; + + // Act & assert + Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + } + + [Fact] + public void Command_options_must_have_unique_environment_variable_names() + { + // Arrange + var commandTypes = new[] {typeof(DuplicateOptionEnvironmentVariableNamesCommand)}; + + // Act & assert + Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); + } + + [Fact] + public void Command_options_and_parameters_must_be_annotated_by_corresponding_attributes() + { + // Arrange + var commandTypes = new[] {typeof(HiddenPropertiesCommand)}; + + // Act + var schema = ApplicationSchema.Resolve(commandTypes); + + // Assert + schema.Should().BeEquivalentTo(new ApplicationSchema(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") + }) + })); + + schema.ToString().Should().NotBeNullOrWhiteSpace(); // this is only for coverage, I'm sorry + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/ArgumentBindingSpecs.Commands.cs b/CliFx.Tests/ArgumentBindingSpecs.Commands.cs new file mode 100644 index 0000000..23e7921 --- /dev/null +++ b/CliFx.Tests/ArgumentBindingSpecs.Commands.cs @@ -0,0 +1,170 @@ +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 RequiredOptionCommand : ICommand + { + [CommandOption(nameof(OptionA))] + public string? OptionA { get; set; } + + [CommandOption(nameof(OptionB), IsRequired = true)] + public string? OptionB { 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; + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/ArgumentBindingSpecs.Types.cs b/CliFx.Tests/ArgumentBindingSpecs.Types.cs new file mode 100644 index 0000000..ea83bb5 --- /dev/null +++ b/CliFx.Tests/ArgumentBindingSpecs.Types.cs @@ -0,0 +1,64 @@ +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 + { + private readonly T[] _arr = new T[0]; + + public IEnumerator GetEnumerator() => ((IEnumerable) _arr).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/ArgumentBindingSpecs.cs b/CliFx.Tests/ArgumentBindingSpecs.cs new file mode 100644 index 0000000..a1c13a4 --- /dev/null +++ b/CliFx.Tests/ArgumentBindingSpecs.cs @@ -0,0 +1,1022 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using CliFx.Domain; +using CliFx.Exceptions; +using FluentAssertions; +using Xunit; + +namespace CliFx.Tests +{ + public partial class ArgumentBindingSpecs + { + [Fact] + public void Property_of_type_object_is_bound_directly_from_the_argument_value() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.Object), "value") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.Should().BeEquivalentTo(new AllSupportedTypesCommand + { + Object = "value" + }); + } + + [Fact] + public void Property_of_type_object_array_is_bound_directly_from_the_argument_values() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.ObjectArray), "foo", "bar") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.Should().BeEquivalentTo(new AllSupportedTypesCommand + { + ObjectArray = new object[] {"foo", "bar"} + }); + } + + [Fact] + public void Property_of_type_non_generic_IEnumerable_is_bound_directly_from_the_argument_values() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.Enumerable), "foo", "bar") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.Should().BeEquivalentTo(new AllSupportedTypesCommand + { + Enumerable = new object[] {"foo", "bar"} + }); + } + + [Fact] + public void Property_of_type_string_is_bound_directly_from_the_argument_value() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.String), "value") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.Should().BeEquivalentTo(new AllSupportedTypesCommand + { + String = "value" + }); + } + + [Fact] + public void Property_of_type_string_array_is_bound_directly_from_the_argument_values() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.StringArray), "foo", "bar") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.Should().BeEquivalentTo(new AllSupportedTypesCommand + { + StringArray = new[] {"foo", "bar"} + }); + } + + [Fact] + public void Property_of_type_string_IEnumerable_is_bound_directly_from_the_argument_values() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.StringEnumerable), "foo", "bar") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.Should().BeEquivalentTo(new AllSupportedTypesCommand + { + StringEnumerable = new[] {"foo", "bar"} + }); + } + + [Fact] + public void Property_of_type_string_IReadOnlyList_is_bound_directly_from_the_argument_values() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.StringReadOnlyList), "foo", "bar") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.StringList), "foo", "bar") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.StringHashSet), "foo", "bar") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.Bool), "true") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.Bool), "false") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.Bool)) + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.Char), "a") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.Should().BeEquivalentTo(new AllSupportedTypesCommand + { + Char = 'a' + }); + } + + [Fact] + public void Property_of_type_sbyte_is_bound_by_parsing_the_argument_value() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.Sbyte), "15") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.Should().BeEquivalentTo(new AllSupportedTypesCommand + { + Sbyte = 15 + }); + } + + [Fact] + public void Property_of_type_byte_is_bound_by_parsing_the_argument_value() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.Byte), "15") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.Should().BeEquivalentTo(new AllSupportedTypesCommand + { + Byte = 15 + }); + } + + [Fact] + public void Property_of_type_short_is_bound_by_parsing_the_argument_value() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.Short), "15") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.Should().BeEquivalentTo(new AllSupportedTypesCommand + { + Short = 15 + }); + } + + [Fact] + public void Property_of_type_ushort_is_bound_by_parsing_the_argument_value() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.Ushort), "15") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.Should().BeEquivalentTo(new AllSupportedTypesCommand + { + Ushort = 15 + }); + } + + [Fact] + public void Property_of_type_int_is_bound_by_parsing_the_argument_value() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.Int), "15") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.IntNullable), "15") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.IntNullable)) + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.Should().BeEquivalentTo(new AllSupportedTypesCommand + { + IntNullable = null + }); + } + + [Fact] + public void Property_of_type_int_array_is_bound_by_parsing_the_argument_values() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.IntArray), "3", "14") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.IntNullableArray), "3", "14") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.Uint), "15") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.Should().BeEquivalentTo(new AllSupportedTypesCommand + { + Uint = 15 + }); + } + + [Fact] + public void Property_of_type_long_is_bound_by_parsing_the_argument_value() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.Long), "15") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.Should().BeEquivalentTo(new AllSupportedTypesCommand + { + Long = 15 + }); + } + + [Fact] + public void Property_of_type_ulong_is_bound_by_parsing_the_argument_value() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.Ulong), "15") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.Should().BeEquivalentTo(new AllSupportedTypesCommand + { + Ulong = 15 + }); + } + + [Fact] + public void Property_of_type_float_is_bound_by_parsing_the_argument_value() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.Float), "123.45") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.Should().BeEquivalentTo(new AllSupportedTypesCommand + { + Float = 123.45F + }); + } + + [Fact] + public void Property_of_type_double_is_bound_by_parsing_the_argument_value() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.Double), "123.45") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.Should().BeEquivalentTo(new AllSupportedTypesCommand + { + Double = 123.45 + }); + } + + [Fact] + public void Property_of_type_decimal_is_bound_by_parsing_the_argument_value() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.Decimal), "123.45") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.Should().BeEquivalentTo(new AllSupportedTypesCommand + { + Decimal = 123.45M + }); + } + + [Fact] + public void Property_of_type_DateTime_is_bound_by_parsing_the_argument_value() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.DateTime), "28 Apr 1995") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.DateTimeOffset), "28 Apr 1995") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.TimeSpan), "00:14:59") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.TimeSpanNullable), "00:14:59") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.TimeSpanNullable)) + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.CustomEnum), "value2") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.CustomEnum), "2") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.CustomEnumNullable), "value3") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.CustomEnumNullable), "3") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.CustomEnumNullable)) + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.CustomEnumArray), "value1", "value3") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.CustomEnumArray), "1", "3") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.CustomEnumArray), "value1", "3") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.TestStringConstructable), "foobar") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.TestStringConstructableArray), "foo", "bar") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.TestStringParseable), "foobar") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.TestStringParseableWithFormatProvider), "foobar") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.Should().BeEquivalentTo(new AllSupportedTypesCommand + { + TestStringParseableWithFormatProvider = StringParseableWithFormatProvider.Parse("foobar", CultureInfo.InvariantCulture) + }); + } + + [Fact] + public void Property_annotated_as_a_required_option_must_always_be_bound_to_some_value() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(RequiredOptionCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(RequiredOptionCommand.OptionA), "foo") + .Build(); + + // Act & assert + Assert.Throws(() => schema.InitializeEntryPoint(input)); + } + + [Fact] + public void Property_annotated_as_parameter_is_bound_directly_from_argument_value_according_to_the_order() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(ParametersCommand)}); + + var input = new CommandLineInputBuilder() + .AddUnboundArgument("foo") + .AddUnboundArgument("bar") + .AddUnboundArgument("hello") + .AddUnboundArgument("world") + .Build(); + + // Act + var command = schema.InitializeEntryPoint(input); + + // Assert + command.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 schema = ApplicationSchema.Resolve(new[] {typeof(ParametersCommand)}); + + var input = new CommandLineInputBuilder() + .AddUnboundArgument("foo") + .Build(); + + // Act & assert + Assert.Throws(() => schema.InitializeEntryPoint(input)); + } + + [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 schema = ApplicationSchema.Resolve(new[] {typeof(UnsupportedEnumerablePropertyTypeCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(UnsupportedEnumerablePropertyTypeCommand.Option), "foo", "bar") + .Build(); + + // Act & assert + Assert.Throws(() => schema.InitializeEntryPoint(input)); + } + + [Fact] + public void Property_of_non_nullable_type_can_only_be_bound_if_the_argument_value_is_set() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.Int)) + .Build(); + + // Act & assert + Assert.Throws(() => schema.InitializeEntryPoint(input)); + } + + [Fact] + public void Property_must_have_a_type_supported_by_the_framework_in_order_to_be_bound() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(UnsupportedPropertyTypeCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(UnsupportedPropertyTypeCommand.Option), "foo") + .Build(); + + // Act & assert + Assert.Throws(() => schema.InitializeEntryPoint(input)); + } + + [Fact] + public void Property_must_have_a_type_that_implements_IEnumerable_in_order_to_be_bound_from_multiple_argument_values() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(AllSupportedTypesCommand)}); + + var input = new CommandLineInputBuilder() + .AddOption(nameof(AllSupportedTypesCommand.Int), "1", "2", "3") + .Build(); + + // Act & assert + Assert.Throws(() => schema.InitializeEntryPoint(input)); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/ArgumentSyntaxSpecs.cs b/CliFx.Tests/ArgumentSyntaxSpecs.cs new file mode 100644 index 0000000..9984b8c --- /dev/null +++ b/CliFx.Tests/ArgumentSyntaxSpecs.cs @@ -0,0 +1,315 @@ +using System; +using CliFx.Domain; +using FluentAssertions; +using Xunit; + +namespace CliFx.Tests +{ + public class ArgumentSyntaxSpecs + { + [Fact] + public void Input_is_empty_if_no_arguments_are_provided() + { + // Arrange + var args = Array.Empty(); + + // Act + var input = CommandLineInput.Parse(args); + + // Assert + input.Should().BeEquivalentTo(CommandLineInput.Empty); + } + + public static object[][] DirectivesTestData => new[] + { + new object[] + { + new[] {"[preview]"}, + new CommandLineInputBuilder() + .AddDirective("preview") + .Build() + }, + + new object[] + { + new[] {"[preview]", "[debug]"}, + new CommandLineInputBuilder() + .AddDirective("preview") + .AddDirective("debug") + .Build() + } + }; + + [Theory] + [MemberData(nameof(DirectivesTestData))] + internal void Directive_can_be_enabled_by_specifying_its_name_in_square_brackets(string[] arguments, CommandLineInput expectedInput) + { + // Act + var input = CommandLineInput.Parse(arguments); + + // Assert + input.Should().BeEquivalentTo(expectedInput); + } + + public static object[][] OptionsTestData => new[] + { + new object[] + { + new[] {"--option"}, + new CommandLineInputBuilder() + .AddOption("option") + .Build() + }, + + new object[] + { + new[] {"--option", "value"}, + new CommandLineInputBuilder() + .AddOption("option", "value") + .Build() + }, + + new object[] + { + new[] {"--option", "value1", "value2"}, + new CommandLineInputBuilder() + .AddOption("option", "value1", "value2") + .Build() + }, + + new object[] + { + new[] {"--option", "same value"}, + new CommandLineInputBuilder() + .AddOption("option", "same value") + .Build() + }, + + new object[] + { + new[] {"--option1", "--option2"}, + new CommandLineInputBuilder() + .AddOption("option1") + .AddOption("option2") + .Build() + }, + + new object[] + { + new[] {"--option1", "value1", "--option2", "value2"}, + new CommandLineInputBuilder() + .AddOption("option1", "value1") + .AddOption("option2", "value2") + .Build() + }, + + new object[] + { + new[] {"--option1", "value1", "value2", "--option2", "value3", "value4"}, + new CommandLineInputBuilder() + .AddOption("option1", "value1", "value2") + .AddOption("option2", "value3", "value4") + .Build() + }, + + new object[] + { + new[] {"--option1", "value1", "value2", "--option2"}, + new CommandLineInputBuilder() + .AddOption("option1", "value1", "value2") + .AddOption("option2") + .Build() + } + }; + + [Theory] + [MemberData(nameof(OptionsTestData))] + internal void Option_can_be_set_by_specifying_its_name_after_two_dashes(string[] arguments, CommandLineInput expectedInput) + { + // Act + var input = CommandLineInput.Parse(arguments); + + // Assert + input.Should().BeEquivalentTo(expectedInput); + } + + public static object[][] ShortOptionsTestData => new[] + { + new object[] + { + new[] {"-o"}, + new CommandLineInputBuilder() + .AddOption("o") + .Build() + }, + + new object[] + { + new[] {"-o", "value"}, + new CommandLineInputBuilder() + .AddOption("o", "value") + .Build() + }, + + new object[] + { + new[] {"-o", "value1", "value2"}, + new CommandLineInputBuilder() + .AddOption("o", "value1", "value2") + .Build() + }, + + new object[] + { + new[] {"-o", "same value"}, + new CommandLineInputBuilder() + .AddOption("o", "same value") + .Build() + }, + + new object[] + { + new[] {"-a", "-b"}, + new CommandLineInputBuilder() + .AddOption("a") + .AddOption("b") + .Build() + }, + + new object[] + { + new[] {"-a", "value1", "-b", "value2"}, + new CommandLineInputBuilder() + .AddOption("a", "value1") + .AddOption("b", "value2") + .Build() + }, + + new object[] + { + new[] {"-a", "value1", "value2", "-b", "value3", "value4"}, + new CommandLineInputBuilder() + .AddOption("a", "value1", "value2") + .AddOption("b", "value3", "value4") + .Build() + }, + + new object[] + { + new[] {"-a", "value1", "value2", "-b"}, + new CommandLineInputBuilder() + .AddOption("a", "value1", "value2") + .AddOption("b") + .Build() + }, + + new object[] + { + new[] {"-abc"}, + new CommandLineInputBuilder() + .AddOption("a") + .AddOption("b") + .AddOption("c") + .Build() + }, + + new object[] + { + new[] {"-abc", "value"}, + new CommandLineInputBuilder() + .AddOption("a") + .AddOption("b") + .AddOption("c", "value") + .Build() + }, + + new object[] + { + new[] {"-abc", "value1", "value2"}, + new CommandLineInputBuilder() + .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(string[] arguments, CommandLineInput expectedInput) + { + // Act + var input = CommandLineInput.Parse(arguments); + + // Assert + input.Should().BeEquivalentTo(expectedInput); + } + + public static object[][] UnboundArgumentsTestData => new[] + { + new object[] + { + new[] {"foo"}, + new CommandLineInputBuilder() + .AddUnboundArgument("foo") + .Build() + }, + + new object[] + { + new[] {"foo", "bar"}, + new CommandLineInputBuilder() + .AddUnboundArgument("foo") + .AddUnboundArgument("bar") + .Build() + }, + + new object[] + { + new[] {"[preview]", "foo"}, + new CommandLineInputBuilder() + .AddDirective("preview") + .AddUnboundArgument("foo") + .Build() + }, + + new object[] + { + new[] {"foo", "--option", "value", "-abc"}, + new CommandLineInputBuilder() + .AddUnboundArgument("foo") + .AddOption("option", "value") + .AddOption("a") + .AddOption("b") + .AddOption("c") + .Build() + }, + + new object[] + { + new[] {"[preview]", "[debug]", "foo", "bar", "--option", "value", "-abc"}, + new CommandLineInputBuilder() + .AddDirective("preview") + .AddDirective("debug") + .AddUnboundArgument("foo") + .AddUnboundArgument("bar") + .AddOption("option", "value") + .AddOption("a") + .AddOption("b") + .AddOption("c") + .Build() + } + }; + + [Theory] + [MemberData(nameof(UnboundArgumentsTestData))] + internal void Any_remaining_arguments_are_treated_as_unbound_arguments(string[] arguments, CommandLineInput expectedInput) + { + // Act + var input = CommandLineInput.Parse(arguments); + + // 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 new file mode 100644 index 0000000..6afc5d1 --- /dev/null +++ b/CliFx.Tests/CancellationSpecs.Commands.cs @@ -0,0 +1,27 @@ +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 new file mode 100644 index 0000000..1b336b5 --- /dev/null +++ b/CliFx.Tests/CancellationSpecs.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace CliFx.Tests +{ + public partial class CancellationSpecs + { + [Fact] + public async Task Command_can_perform_additional_cleanup_if_cancellation_is_requested() + { + // Arrange + using var cts = new CancellationTokenSource(); + + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut, cancellationToken: cts.Token); + + var application = new CliApplicationBuilder() + .AddCommand(typeof(CancellableCommand)) + .UseConsole(console) + .Build(); + + // Act + cts.CancelAfter(TimeSpan.FromSeconds(0.2)); + + var exitCode = await application.RunAsync( + new[] {"cancel"}, + new Dictionary()); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdOutData.Should().Be("Cancellation requested"); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/CliApplicationBuilderTests.cs b/CliFx.Tests/CliApplicationBuilderTests.cs deleted file mode 100644 index 7f02908..0000000 --- a/CliFx.Tests/CliApplicationBuilderTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -using NUnit.Framework; -using System; -using CliFx.Tests.TestCommands; - -namespace CliFx.Tests -{ - [TestFixture] - public class CliApplicationBuilderTests - { - [Test(Description = "All builder methods must return without exceptions")] - public void Smoke_Test() - { - // Arrange - var builder = new CliApplicationBuilder(); - - // Act - builder - .AddCommand(typeof(HelloWorldDefaultCommand)) - .AddCommandsFrom(typeof(HelloWorldDefaultCommand).Assembly) - .AddCommands(new[] {typeof(HelloWorldDefaultCommand)}) - .AddCommandsFrom(new[] {typeof(HelloWorldDefaultCommand).Assembly}) - .AddCommandsFromThisAssembly() - .AllowDebugMode() - .AllowPreviewMode() - .UseTitle("test") - .UseExecutableName("test") - .UseVersionText("test") - .UseDescription("test") - .UseConsole(new VirtualConsole()) - .UseTypeActivator(Activator.CreateInstance) - .Build(); - } - - [Test(Description = "Builder must be able to produce an application when no parameters are specified")] - public void Build_Test() - { - // Arrange - var builder = new CliApplicationBuilder(); - - // Act - builder.Build(); - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/CliApplicationTests.cs b/CliFx.Tests/CliApplicationTests.cs deleted file mode 100644 index d2cc44a..0000000 --- a/CliFx.Tests/CliApplicationTests.cs +++ /dev/null @@ -1,445 +0,0 @@ -using FluentAssertions; -using NUnit.Framework; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using CliFx.Tests.TestCommands; - -namespace CliFx.Tests -{ - [TestFixture] - public class CliApplicationTests - { - private const string TestAppName = "TestApp"; - private const string TestVersionText = "v1.0"; - - private static IEnumerable GetTestCases_RunAsync() - { - yield return new TestCaseData( - new[] {typeof(HelloWorldDefaultCommand)}, - new string[0], - new Dictionary(), - "Hello world." - ); - - yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new[] {"concat", "-i", "foo", "-i", "bar", "-s", " "}, - new Dictionary(), - "foo bar" - ); - - yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new[] {"concat", "-i", "one", "two", "three", "-s", ", "}, - new Dictionary(), - "one, two, three" - ); - - yield return new TestCaseData( - new[] {typeof(DivideCommand)}, - new[] {"div", "-D", "24", "-d", "8"}, - new Dictionary(), - "3" - ); - - yield return new TestCaseData( - new[] {typeof(HelloWorldDefaultCommand)}, - new[] {"--version"}, - new Dictionary(), - TestVersionText - ); - - yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new[] {"--version"}, - new Dictionary(), - TestVersionText - ); - - yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new string[0], - new Dictionary(), - null - ); - - yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new[] {"-h"}, - new Dictionary(), - null - ); - - yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new[] {"--help"}, - new Dictionary(), - null - ); - - yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new[] {"concat", "-h"}, - new Dictionary(), - null - ); - - yield return new TestCaseData( - new[] {typeof(ExceptionCommand)}, - new[] {"exc", "-h"}, - new Dictionary(), - null - ); - - yield return new TestCaseData( - new[] {typeof(CommandExceptionCommand)}, - new[] {"exc", "-h"}, - new Dictionary(), - null - ); - - yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new[] {"[preview]"}, - new Dictionary(), - null - ); - - yield return new TestCaseData( - new[] {typeof(ExceptionCommand)}, - new[] {"[preview]", "exc"}, - new Dictionary(), - null - ); - - yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new[] {"[preview]", "concat", "-o", "value"}, - new Dictionary(), - null - ); - } - - private static IEnumerable GetTestCases_RunAsync_Negative() - { - yield return new TestCaseData( - new Type[0], - new string[0], - new Dictionary(), - null, null - ); - - yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new[] {"non-existing"}, - new Dictionary(), - null, null - ); - - yield return new TestCaseData( - new[] {typeof(ExceptionCommand)}, - new[] {"exc"}, - new Dictionary(), - null, null - ); - - yield return new TestCaseData( - new[] {typeof(CommandExceptionCommand)}, - new[] {"exc"}, - new Dictionary(), - null, null - ); - - yield return new TestCaseData( - new[] {typeof(CommandExceptionCommand)}, - new[] {"exc"}, - new Dictionary(), - null, null - ); - - yield return new TestCaseData( - new[] {typeof(CommandExceptionCommand)}, - new[] {"exc", "-m", "foo bar"}, - new Dictionary(), - "foo bar", null - ); - - yield return new TestCaseData( - new[] {typeof(CommandExceptionCommand)}, - new[] {"exc", "-m", "foo bar", "-c", "666"}, - new Dictionary(), - "foo bar", 666 - ); - } - - private static IEnumerable GetTestCases_RunAsync_Help() - { - yield return new TestCaseData( - new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)}, - new[] {"--help"}, - new[] - { - TestVersionText, - "Description", - "HelpDefaultCommand description.", - "Usage", - TestAppName, "[command]", "[options]", - "Options", - "-a|--option-a", "OptionA description.", - "-b|--option-b", "OptionB description.", - "-h|--help", "Shows help text.", - "--version", "Shows version information.", - "Commands", - "cmd", "HelpNamedCommand description.", - "You can run", "to show help on a specific command." - } - ); - - yield return new TestCaseData( - new[] {typeof(HelpSubCommand)}, - new[] {"--help"}, - new[] - { - TestVersionText, - "Usage", - TestAppName, "[command]", - "Options", - "-h|--help", "Shows help text.", - "--version", "Shows version information.", - "Commands", - "cmd sub", "HelpSubCommand description.", - "You can run", "to show help on a specific command." - } - ); - - yield return new TestCaseData( - new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)}, - new[] {"cmd", "--help"}, - new[] - { - "Description", - "HelpNamedCommand description.", - "Usage", - TestAppName, "cmd", "[command]", "[options]", - "Options", - "-c|--option-c", "OptionC description.", - "-d|--option-d", "OptionD description.", - "-h|--help", "Shows help text.", - "Commands", - "sub", "HelpSubCommand description.", - "You can run", "to show help on a specific command." - } - ); - - yield return new TestCaseData( - new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)}, - new[] {"cmd", "sub", "--help"}, - new[] - { - "Description", - "HelpSubCommand description.", - "Usage", - TestAppName, "cmd sub", "[options]", - "Options", - "-e|--option-e", "OptionE description.", - "-h|--help", "Shows help text." - } - ); - - yield return new TestCaseData( - new[] {typeof(ParameterCommand)}, - new[] {"param", "cmd", "--help"}, - new[] - { - "Description", - "Command using positional parameters", - "Usage", - TestAppName, "param cmd", "", "", "", "[options]", - "Parameters", - "* first", - "* parameterb", - "* third list", "A list of numbers", - "Options", - "-o|--option", - "-h|--help", "Shows help text." - } - ); - - yield return new TestCaseData( - new[] {typeof(AllRequiredOptionsCommand)}, - new[] {"allrequired", "--help"}, - new[] - { - "Description", - "AllRequiredOptionsCommand description.", - "Usage", - TestAppName, "allrequired --option-f --option-g " - } - ); - - yield return new TestCaseData( - new[] {typeof(SomeRequiredOptionsCommand)}, - new[] {"somerequired", "--help"}, - new[] - { - "Description", - "SomeRequiredOptionsCommand description.", - "Usage", - TestAppName, "somerequired --option-f [options]" - } - ); - - yield return new TestCaseData( - new[] {typeof(EnvironmentVariableCommand)}, - new[] {"--help"}, - new[] - { - "Environment variable:", "ENV_SINGLE_VALUE" - } - ); - - yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new[] {"concat", "--help"}, - new[] - { - "Usage", - TestAppName, "concat", "-i", "", "[options]", - } - ); - } - - [TestCaseSource(nameof(GetTestCases_RunAsync))] - public async Task RunAsync_Test( - IReadOnlyList commandTypes, - IReadOnlyList commandLineArguments, - IReadOnlyDictionary environmentVariables, - string? expectedStdOut = null) - { - // Arrange - using var console = new VirtualConsole(); - - var application = new CliApplicationBuilder() - .AddCommands(commandTypes) - .UseTitle(TestAppName) - .UseExecutableName(TestAppName) - .UseVersionText(TestVersionText) - .UseConsole(console) - .Build(); - - // Act - var exitCode = await application.RunAsync(commandLineArguments, environmentVariables); - var stdOut = console.ReadOutputString().Trim(); - - // Assert - exitCode.Should().Be(0); - stdOut.Should().NotBeNullOrWhiteSpace(); - - if (expectedStdOut != null) - stdOut.Should().Be(expectedStdOut); - - Console.WriteLine(stdOut); - } - - [TestCaseSource(nameof(GetTestCases_RunAsync_Negative))] - public async Task RunAsync_Negative_Test( - IReadOnlyList commandTypes, - IReadOnlyList commandLineArguments, - IReadOnlyDictionary environmentVariables, - string? expectedStdErr = null, - int? expectedExitCode = null) - { - // Arrange - using var console = new VirtualConsole(); - - var application = new CliApplicationBuilder() - .AddCommands(commandTypes) - .UseTitle(TestAppName) - .UseExecutableName(TestAppName) - .UseVersionText(TestVersionText) - .UseConsole(console) - .Build(); - - // Act - var exitCode = await application.RunAsync(commandLineArguments, environmentVariables); - var stdErr = console.ReadErrorString().Trim(); - - // Assert - exitCode.Should().NotBe(0); - stdErr.Should().NotBeNullOrWhiteSpace(); - - if (expectedExitCode != null) - exitCode.Should().Be(expectedExitCode); - - if (expectedStdErr != null) - stdErr.Should().Be(expectedStdErr); - - Console.WriteLine(stdErr); - } - - [TestCaseSource(nameof(GetTestCases_RunAsync_Help))] - public async Task RunAsync_Help_Test( - IReadOnlyList commandTypes, - IReadOnlyList commandLineArguments, - IReadOnlyList? expectedSubstrings = null) - { - // Arrange - using var console = new VirtualConsole(); - - var application = new CliApplicationBuilder() - .AddCommands(commandTypes) - .UseTitle(TestAppName) - .UseExecutableName(TestAppName) - .UseVersionText(TestVersionText) - .UseConsole(console) - .Build(); - - var environmentVariables = new Dictionary(); - - // Act - var exitCode = await application.RunAsync(commandLineArguments, environmentVariables); - var stdOut = console.ReadOutputString().Trim(); - - // Assert - exitCode.Should().Be(0); - stdOut.Should().NotBeNullOrWhiteSpace(); - - if (expectedSubstrings != null) - stdOut.Should().ContainAll(expectedSubstrings); - - Console.WriteLine(stdOut); - } - - [Test] - public async Task RunAsync_Cancellation_Test() - { - // Arrange - using var console = new VirtualConsole(); - - var application = new CliApplicationBuilder() - .AddCommand(typeof(CancellableCommand)) - .UseConsole(console) - .Build(); - - var commandLineArguments = new[] {"cancel"}; - var environmentVariables = new Dictionary(); - - // Act - console.CancelAfter(TimeSpan.FromSeconds(0.2)); - - var exitCode = await application.RunAsync(commandLineArguments, environmentVariables); - var stdOut = console.ReadOutputString().Trim(); - var stdErr = console.ReadErrorString().Trim(); - - // Assert - exitCode.Should().NotBe(0); - stdOut.Should().BeNullOrWhiteSpace(); - stdErr.Should().NotBeNullOrWhiteSpace(); - - Console.WriteLine(stdErr); - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/CliFx.Tests.csproj b/CliFx.Tests/CliFx.Tests.csproj index ab5fc4e..547c921 100644 --- a/CliFx.Tests/CliFx.Tests.csproj +++ b/CliFx.Tests/CliFx.Tests.csproj @@ -11,12 +11,16 @@ - - - - - + + + + + + + + + @@ -24,8 +28,12 @@ - - - + + + CliFx.Tests.Dummy.runtimeconfig.json + PreserveNewest + False + + \ No newline at end of file diff --git a/CliFx.Tests/ConsoleSpecs.cs b/CliFx.Tests/ConsoleSpecs.cs new file mode 100644 index 0000000..e3574f6 --- /dev/null +++ b/CliFx.Tests/ConsoleSpecs.cs @@ -0,0 +1,72 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using CliWrap; +using CliWrap.Buffered; +using FluentAssertions; +using Xunit; + +namespace CliFx.Tests +{ + public class ConsoleSpecs + { + [Fact] + public async Task Real_implementation_of_console_maps_directly_to_system_console() + { + // Arrange + var command = "Hello world" | Cli.Wrap("dotnet") + .WithArguments(a => a + .Add(Dummy.Program.Location) + .Add("console-test")); + + // Act + var result = await command.ExecuteBufferedAsync(); + + // Assert + result.StandardOutput.TrimEnd().Should().Be("Hello world"); + result.StandardError.TrimEnd().Should().Be("Hello world"); + } + + [Fact] + public void Fake_implementation_of_console_can_be_used_to_execute_commands_in_isolation() + { + // Arrange + using var stdIn = new MemoryStream(Console.InputEncoding.GetBytes("input")); + using var stdOut = new MemoryStream(); + using var stdErr = new MemoryStream(); + + var console = new VirtualConsole( + input: stdIn, + output: stdOut, + error: stdErr); + + // Act + console.Output.Write("output"); + console.Error.Write("error"); + + var stdInData = console.Input.ReadToEnd(); + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()); + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()); + + console.ResetColor(); + console.ForegroundColor = ConsoleColor.DarkMagenta; + console.BackgroundColor = ConsoleColor.DarkMagenta; + + // Assert + stdInData.Should().Be("input"); + stdOutData.Should().Be("output"); + stdErrData.Should().Be("error"); + + console.Input.Should().NotBeSameAs(Console.In); + console.Output.Should().NotBeSameAs(Console.Out); + console.Error.Should().NotBeSameAs(Console.Error); + + console.IsInputRedirected.Should().BeTrue(); + console.IsOutputRedirected.Should().BeTrue(); + console.IsErrorRedirected.Should().BeTrue(); + + console.ForegroundColor.Should().NotBe(Console.ForegroundColor); + console.BackgroundColor.Should().NotBe(Console.BackgroundColor); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/DefaultCommandFactoryTests.cs b/CliFx.Tests/DefaultCommandFactoryTests.cs deleted file mode 100644 index 8977875..0000000 --- a/CliFx.Tests/DefaultCommandFactoryTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; -using CliFx.Exceptions; -using CliFx.Tests.TestCommands; -using CliFx.Tests.TestCustomTypes; -using FluentAssertions; -using NUnit.Framework; - -namespace CliFx.Tests -{ - [TestFixture] - public class DefaultCommandFactoryTests - { - private static IEnumerable GetTestCases_CreateInstance() - { - yield return new TestCaseData(typeof(HelloWorldDefaultCommand)); - } - - private static IEnumerable GetTestCases_CreateInstance_Negative() - { - yield return new TestCaseData(typeof(TestNonStringParseable)); - } - - [TestCaseSource(nameof(GetTestCases_CreateInstance))] - public void CreateInstance_Test(Type type) - { - // Arrange - var activator = new DefaultTypeActivator(); - - // Act - var obj = activator.CreateInstance(type); - - // Assert - obj.Should().BeOfType(type); - } - - [TestCaseSource(nameof(GetTestCases_CreateInstance_Negative))] - public void CreateInstance_Negative_Test(Type type) - { - // Arrange - var activator = new DefaultTypeActivator(); - - // Act & Assert - var ex = Assert.Throws(() => activator.CreateInstance(type)); - Console.WriteLine(ex.Message); - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/DelegateCommandFactoryTests.cs b/CliFx.Tests/DelegateCommandFactoryTests.cs deleted file mode 100644 index d195f10..0000000 --- a/CliFx.Tests/DelegateCommandFactoryTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using CliFx.Exceptions; -using CliFx.Tests.TestCommands; -using FluentAssertions; -using NUnit.Framework; - -namespace CliFx.Tests -{ - [TestFixture] - public class DelegateCommandFactoryTests - { - private static IEnumerable GetTestCases_CreateInstance() - { - yield return new TestCaseData( - new Func(Activator.CreateInstance), - typeof(HelloWorldDefaultCommand) - ); - } - - private static IEnumerable GetTestCases_CreateInstance_Negative() - { - yield return new TestCaseData( - new Func(_ => null), - typeof(HelloWorldDefaultCommand) - ); - } - - [TestCaseSource(nameof(GetTestCases_CreateInstance))] - public void CreateInstance_Test(Func activatorFunc, Type type) - { - // Arrange - var activator = new DelegateTypeActivator(activatorFunc); - - // Act - var obj = activator.CreateInstance(type); - - // Assert - obj.Should().BeOfType(type); - } - - [TestCaseSource(nameof(GetTestCases_CreateInstance_Negative))] - public void CreateInstance_Negative_Test(Func activatorFunc, Type type) - { - // Arrange - var activator = new DelegateTypeActivator(activatorFunc); - - // Act & Assert - var ex = Assert.Throws(() => activator.CreateInstance(type)); - Console.WriteLine(ex.Message); - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/DependencyInjectionSpecs.Commands.cs b/CliFx.Tests/DependencyInjectionSpecs.Commands.cs new file mode 100644 index 0000000..6542b0f --- /dev/null +++ b/CliFx.Tests/DependencyInjectionSpecs.Commands.cs @@ -0,0 +1,37 @@ +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 new file mode 100644 index 0000000..3100a20 --- /dev/null +++ b/CliFx.Tests/DependencyInjectionSpecs.cs @@ -0,0 +1,58 @@ +using CliFx.Exceptions; +using FluentAssertions; +using Xunit; + +namespace CliFx.Tests +{ + public partial class DependencyInjectionSpecs + { + [Fact] + public void Default_type_activator_can_initialize_a_command_if_it_has_a_parameterless_constructor() + { + // Arrange + var activator = new DefaultTypeActivator(); + + // Act + var obj = activator.CreateInstance(typeof(WithoutDependenciesCommand)); + + // Assert + obj.Should().BeOfType(); + } + + [Fact] + public void Default_type_activator_cannot_initialize_a_command_if_it_does_not_have_a_parameterless_constructor() + { + // Arrange + var activator = new DefaultTypeActivator(); + + // Act & assert + Assert.Throws(() => + activator.CreateInstance(typeof(WithDependenciesCommand))); + } + + [Fact] + public void Delegate_type_activator_can_initialize_a_command_using_a_custom_function() + { + // Arrange + var activator = new DelegateTypeActivator(_ => + new WithDependenciesCommand(new DependencyA(), new DependencyB())); + + // Act + var obj = activator.CreateInstance(typeof(WithDependenciesCommand)); + + // Assert + obj.Should().BeOfType(); + } + + [Fact] + public void Delegate_type_activator_throws_if_the_underlying_function_returns_null() + { + // Arrange + var activator = new DelegateTypeActivator(_ => null); + + // Act & assert + Assert.Throws(() => + activator.CreateInstance(typeof(WithDependenciesCommand))); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/DirectivesSpecs.Commands.cs b/CliFx.Tests/DirectivesSpecs.Commands.cs new file mode 100644 index 0000000..58c941e --- /dev/null +++ b/CliFx.Tests/DirectivesSpecs.Commands.cs @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..4249aa2 --- /dev/null +++ b/CliFx.Tests/DirectivesSpecs.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace CliFx.Tests +{ + public partial class DirectivesSpecs + { + [Fact] + public async Task Preview_directive_can_be_enabled_to_print_provided_arguments_as_they_were_parsed() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand(typeof(NamedCommand)) + .UseConsole(console) + .AllowPreviewMode() + .Build(); + + // Act + var exitCode = await application.RunAsync( + new[] {"[preview]", "cmd", "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]"); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/Domain/ApplicationSchemaTests.cs b/CliFx.Tests/Domain/ApplicationSchemaTests.cs deleted file mode 100644 index 14a99bd..0000000 --- a/CliFx.Tests/Domain/ApplicationSchemaTests.cs +++ /dev/null @@ -1,888 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using CliFx.Domain; -using CliFx.Exceptions; -using CliFx.Tests.TestCommands; -using CliFx.Tests.TestCustomTypes; -using FluentAssertions; -using NUnit.Framework; - -namespace CliFx.Tests.Domain -{ - [TestFixture] - internal partial class ApplicationSchemaTests - { - private static IEnumerable GetTestCases_Resolve() - { - yield return new TestCaseData( - new[] - { - typeof(DivideCommand), - typeof(ConcatCommand), - typeof(EnvironmentVariableCommand) - }, - new[] - { - new CommandSchema(typeof(DivideCommand), "div", "Divide one number by another.", - new CommandParameterSchema[0], new[] - { - new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Dividend)), - "dividend", 'D', null, true, "The number to divide."), - new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Divisor)), - "divisor", 'd', null, true, "The number to divide by.") - }), - new CommandSchema(typeof(ConcatCommand), "concat", "Concatenate strings.", - new CommandParameterSchema[0], - new[] - { - new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Inputs)), - null, 'i', null, true, "Input strings."), - new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Separator)), - null, 's', null, false, "String separator.") - }), - new CommandSchema(typeof(EnvironmentVariableCommand), null, "Reads option values from environment variables.", - new CommandParameterSchema[0], - new[] - { - new CommandOptionSchema(typeof(EnvironmentVariableCommand).GetProperty(nameof(EnvironmentVariableCommand.Option)), - "opt", null, "ENV_SINGLE_VALUE", false, null) - } - ) - } - ); - - yield return new TestCaseData( - new[] {typeof(SimpleParameterCommand)}, - new[] - { - new CommandSchema(typeof(SimpleParameterCommand), "param cmd2", "Command using positional parameters", - new[] - { - new CommandParameterSchema(typeof(SimpleParameterCommand).GetProperty(nameof(SimpleParameterCommand.ParameterA)), - 0, "first", null), - new CommandParameterSchema(typeof(SimpleParameterCommand).GetProperty(nameof(SimpleParameterCommand.ParameterB)), - 10, null, null) - }, - new[] - { - new CommandOptionSchema(typeof(SimpleParameterCommand).GetProperty(nameof(SimpleParameterCommand.OptionA)), - "option", 'o', null, false, null) - }) - } - ); - - yield return new TestCaseData( - new[] {typeof(HelloWorldDefaultCommand)}, - new[] - { - new CommandSchema(typeof(HelloWorldDefaultCommand), null, null, - new CommandParameterSchema[0], - new CommandOptionSchema[0]) - } - ); - } - - private static IEnumerable GetTestCases_Resolve_Negative() - { - yield return new TestCaseData(new object[] - { - new Type[0] - }); - - // Command validation failure - - yield return new TestCaseData(new object[] - { - new[] {typeof(NonImplementedCommand)} - }); - - yield return new TestCaseData(new object[] - { - // Same name - new[] {typeof(ExceptionCommand), typeof(CommandExceptionCommand)} - }); - - yield return new TestCaseData(new object[] - { - new[] {typeof(NonAnnotatedCommand)} - }); - - // Parameter validation failure - - yield return new TestCaseData(new object[] - { - new[] {typeof(DuplicateParameterOrderCommand)} - }); - - yield return new TestCaseData(new object[] - { - new[] {typeof(DuplicateParameterNameCommand)} - }); - - yield return new TestCaseData(new object[] - { - new[] {typeof(MultipleNonScalarParametersCommand)} - }); - - yield return new TestCaseData(new object[] - { - new[] {typeof(NonLastNonScalarParameterCommand)} - }); - - // Option validation failure - - yield return new TestCaseData(new object[] - { - new[] {typeof(DuplicateOptionNamesCommand)} - }); - - yield return new TestCaseData(new object[] - { - new[] {typeof(DuplicateOptionShortNamesCommand)} - }); - - yield return new TestCaseData(new object[] - { - new[] {typeof(DuplicateOptionEnvironmentVariableNamesCommand)} - }); - } - - [Test] - [TestCaseSource(nameof(GetTestCases_Resolve))] - public void Resolve_Test( - IReadOnlyList commandTypes, - IReadOnlyList expectedCommandSchemas) - { - // Act - var applicationSchema = ApplicationSchema.Resolve(commandTypes); - - // Assert - applicationSchema.Commands.Should().BeEquivalentTo(expectedCommandSchemas); - } - - [Test] - [TestCaseSource(nameof(GetTestCases_Resolve_Negative))] - public void Resolve_Negative_Test(IReadOnlyList commandTypes) - { - // Act & Assert - var ex = Assert.Throws(() => ApplicationSchema.Resolve(commandTypes)); - Console.WriteLine(ex.Message); - } - } - - internal partial class ApplicationSchemaTests - { - private static IEnumerable GetTestCases_InitializeEntryPoint() - { - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.Object), "value") - }), - new Dictionary(), - new AllSupportedTypesCommand {Object = "value"} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.String), "value") - }), - new Dictionary(), - new AllSupportedTypesCommand {String = "value"} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.Bool), "true") - }), - new Dictionary(), - new AllSupportedTypesCommand {Bool = true} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.Bool), "false") - }), - new Dictionary(), - new AllSupportedTypesCommand {Bool = false} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.Bool)) - }), - new Dictionary(), - new AllSupportedTypesCommand {Bool = true} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.Char), "a") - }), - new Dictionary(), - new AllSupportedTypesCommand {Char = 'a'} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.Sbyte), "15") - }), - new Dictionary(), - new AllSupportedTypesCommand {Sbyte = 15} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.Byte), "15") - }), - new Dictionary(), - new AllSupportedTypesCommand {Byte = 15} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.Short), "15") - }), - new Dictionary(), - new AllSupportedTypesCommand {Short = 15} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.Ushort), "15") - }), - new Dictionary(), - new AllSupportedTypesCommand {Ushort = 15} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.Int), "15") - }), - new Dictionary(), - new AllSupportedTypesCommand {Int = 15} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.Uint), "15") - }), - new Dictionary(), - new AllSupportedTypesCommand {Uint = 15} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.Long), "15") - }), - new Dictionary(), - new AllSupportedTypesCommand {Long = 15} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.Ulong), "15") - }), - new Dictionary(), - new AllSupportedTypesCommand {Ulong = 15} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.Float), "123.45") - }), - new Dictionary(), - new AllSupportedTypesCommand {Float = 123.45f} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.Double), "123.45") - }), - new Dictionary(), - new AllSupportedTypesCommand {Double = 123.45} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.Decimal), "123.45") - }), - new Dictionary(), - new AllSupportedTypesCommand {Decimal = 123.45m} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.DateTime), "28 Apr 1995") - }), - new Dictionary(), - new AllSupportedTypesCommand {DateTime = new DateTime(1995, 04, 28)} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.DateTimeOffset), "28 Apr 1995") - }), - new Dictionary(), - new AllSupportedTypesCommand {DateTimeOffset = new DateTime(1995, 04, 28)} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.TimeSpan), "00:14:59") - }), - new Dictionary(), - new AllSupportedTypesCommand {TimeSpan = new TimeSpan(00, 14, 59)} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.TestEnum), "value2") - }), - new Dictionary(), - new AllSupportedTypesCommand {TestEnum = TestEnum.Value2} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.IntNullable), "666") - }), - new Dictionary(), - new AllSupportedTypesCommand {IntNullable = 666} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.IntNullable)) - }), - new Dictionary(), - new AllSupportedTypesCommand {IntNullable = null} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.TestEnumNullable), "value3") - }), - new Dictionary(), - new AllSupportedTypesCommand {TestEnumNullable = TestEnum.Value3} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.TestEnumNullable)) - }), - new Dictionary(), - new AllSupportedTypesCommand {TestEnumNullable = null} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.TimeSpanNullable), "01:00:00") - }), - new Dictionary(), - new AllSupportedTypesCommand {TimeSpanNullable = new TimeSpan(01, 00, 00)} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.TimeSpanNullable)) - }), - new Dictionary(), - new AllSupportedTypesCommand {TimeSpanNullable = null} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.TestStringConstructable), "value") - }), - new Dictionary(), - new AllSupportedTypesCommand {TestStringConstructable = new TestStringConstructable("value")} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.TestStringParseable), "value") - }), - new Dictionary(), - new AllSupportedTypesCommand {TestStringParseable = TestStringParseable.Parse("value")} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.TestStringParseableWithFormatProvider), "value") - }), - new Dictionary(), - new AllSupportedTypesCommand - { - TestStringParseableWithFormatProvider = - TestStringParseableWithFormatProvider.Parse("value", CultureInfo.InvariantCulture) - } - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.ObjectArray), new[] {"value1", "value2"}) - }), - new Dictionary(), - new AllSupportedTypesCommand {ObjectArray = new object[] {"value1", "value2"}} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.StringArray), new[] {"value1", "value2"}) - }), - new Dictionary(), - new AllSupportedTypesCommand {StringArray = new[] {"value1", "value2"}} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.StringArray)) - }), - new Dictionary(), - new AllSupportedTypesCommand {StringArray = new string[0]} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.IntArray), new[] {"47", "69"}) - }), - new Dictionary(), - new AllSupportedTypesCommand {IntArray = new[] {47, 69}} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.TestEnumArray), new[] {"value1", "value3"}) - }), - new Dictionary(), - new AllSupportedTypesCommand {TestEnumArray = new[] {TestEnum.Value1, TestEnum.Value3}} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.IntNullableArray), new[] {"1337", "2441"}) - }), - new Dictionary(), - new AllSupportedTypesCommand {IntNullableArray = new int?[] {1337, 2441}} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.TestStringConstructableArray), new[] {"value1", "value2"}) - }), - new Dictionary(), - new AllSupportedTypesCommand - { - TestStringConstructableArray = new[] - { - new TestStringConstructable("value1"), - new TestStringConstructable("value2") - } - } - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.Enumerable), new[] {"value1", "value3"}) - }), - new Dictionary(), - new AllSupportedTypesCommand {Enumerable = new[] {"value1", "value3"}} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.StringEnumerable), new[] {"value1", "value3"}) - }), - new Dictionary(), - new AllSupportedTypesCommand {StringEnumerable = new[] {"value1", "value3"}} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.StringReadOnlyList), new[] {"value1", "value3"}) - }), - new Dictionary(), - new AllSupportedTypesCommand {StringReadOnlyList = new[] {"value1", "value3"}} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.StringList), new[] {"value1", "value3"}) - }), - new Dictionary(), - new AllSupportedTypesCommand {StringList = new List {"value1", "value3"}} - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] - { - new CommandOptionInput(nameof(AllSupportedTypesCommand.StringHashSet), new[] {"value1", "value3"}) - }), - new Dictionary(), - new AllSupportedTypesCommand {StringHashSet = new HashSet {"value1", "value3"}} - ); - - yield return new TestCaseData( - new[] {typeof(DivideCommand)}, - new CommandLineInput( - new[] {"div"}, - new[] - { - new CommandOptionInput("dividend", "13"), - new CommandOptionInput("divisor", "8"), - }), - new Dictionary(), - new DivideCommand {Dividend = 13, Divisor = 8} - ); - - yield return new TestCaseData( - new[] {typeof(DivideCommand)}, - new CommandLineInput( - new[] {"div"}, - new[] - { - new CommandOptionInput("D", "13"), - new CommandOptionInput("d", "8"), - }), - new Dictionary(), - new DivideCommand {Dividend = 13, Divisor = 8} - ); - - yield return new TestCaseData( - new[] {typeof(DivideCommand)}, - new CommandLineInput( - new[] {"div"}, - new[] - { - new CommandOptionInput("dividend", "13"), - new CommandOptionInput("d", "8"), - }), - new Dictionary(), - new DivideCommand {Dividend = 13, Divisor = 8} - ); - - yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new CommandLineInput( - new[] {"concat"}, - new[] {new CommandOptionInput("i", new[] {"foo", " ", "bar"}),}), - new Dictionary(), - new ConcatCommand {Inputs = new[] {"foo", " ", "bar"}} - ); - - yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new CommandLineInput( - new[] {"concat"}, - new[] - { - new CommandOptionInput("i", new[] {"foo", "bar"}), - new CommandOptionInput("s", " "), - }), - new Dictionary(), - new ConcatCommand {Inputs = new[] {"foo", "bar"}, Separator = " "} - ); - - yield return new TestCaseData( - new[] {typeof(EnvironmentVariableCommand)}, - CommandLineInput.Empty, - new Dictionary - { - ["ENV_SINGLE_VALUE"] = "A" - }, - new EnvironmentVariableCommand {Option = "A"} - ); - - yield return new TestCaseData( - new[] {typeof(EnvironmentVariableWithMultipleValuesCommand)}, - CommandLineInput.Empty, - new Dictionary - { - ["ENV_MULTIPLE_VALUES"] = string.Join(Path.PathSeparator, "A", "B", "C") - }, - new EnvironmentVariableWithMultipleValuesCommand {Option = new[] {"A", "B", "C"}} - ); - - yield return new TestCaseData( - new[] {typeof(EnvironmentVariableCommand)}, - new CommandLineInput(new[] {new CommandOptionInput("opt", "X")}), - new Dictionary - { - ["ENV_SINGLE_VALUE"] = "A" - }, - new EnvironmentVariableCommand {Option = "X"} - ); - - yield return new TestCaseData( - new[] {typeof(EnvironmentVariableWithoutCollectionPropertyCommand)}, - CommandLineInput.Empty, - new Dictionary - { - ["ENV_MULTIPLE_VALUES"] = string.Join(Path.PathSeparator, "A", "B", "C") - }, - new EnvironmentVariableWithoutCollectionPropertyCommand {Option = string.Join(Path.PathSeparator, "A", "B", "C")} - ); - - yield return new TestCaseData( - new[] {typeof(ParameterCommand)}, - new CommandLineInput( - new[] {"param", "cmd", "abc", "123", "1", "2"}, - new[] {new CommandOptionInput("o", "option value")}), - new Dictionary(), - new ParameterCommand - { - ParameterA = "abc", - ParameterB = 123, - ParameterC = new[] {1, 2}, - OptionA = "option value" - } - ); - } - - private static IEnumerable GetTestCases_InitializeEntryPoint_Negative() - { - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] {new CommandOptionInput(nameof(AllSupportedTypesCommand.Int), "1234.5")}), - new Dictionary() - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] {new CommandOptionInput(nameof(AllSupportedTypesCommand.Int), new[] {"123", "456"})}), - new Dictionary() - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] {new CommandOptionInput(nameof(AllSupportedTypesCommand.Int))}), - new Dictionary() - ); - - yield return new TestCaseData( - new[] {typeof(AllSupportedTypesCommand)}, - new CommandLineInput( - new[] {new CommandOptionInput(nameof(AllSupportedTypesCommand.NonConvertible), "123")}), - new Dictionary() - ); - - yield return new TestCaseData( - new[] {typeof(DivideCommand)}, - new CommandLineInput(new[] {"div"}), - new Dictionary() - ); - - yield return new TestCaseData( - new[] {typeof(DivideCommand)}, - new CommandLineInput(new[] {"div", "-D", "13"}), - new Dictionary() - ); - - yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new CommandLineInput(new[] {"concat"}), - new Dictionary() - ); - - yield return new TestCaseData( - new[] {typeof(ConcatCommand)}, - new CommandLineInput( - new[] {"concat"}, - new[] {new CommandOptionInput("s", "_")}), - new Dictionary() - ); - - yield return new TestCaseData( - new[] {typeof(ParameterCommand)}, - new CommandLineInput( - new[] {"param", "cmd"}, - new[] {new CommandOptionInput("o", "option value")}), - new Dictionary() - ); - - yield return new TestCaseData( - new[] {typeof(ParameterCommand)}, - new CommandLineInput( - new[] {"param", "cmd", "abc", "123", "invalid"}, - new[] {new CommandOptionInput("o", "option value")}), - new Dictionary() - ); - - yield return new TestCaseData( - new[] {typeof(DivideCommand)}, - new CommandLineInput(new[] {"non-existing"}), - new Dictionary() - ); - - yield return new TestCaseData( - new[] {typeof(BrokenEnumerableCommand)}, - new CommandLineInput(new[] {"value1", "value2"}), - new Dictionary() - ); - } - - [TestCaseSource(nameof(GetTestCases_InitializeEntryPoint))] - public void InitializeEntryPoint_Test( - IReadOnlyList commandTypes, - CommandLineInput commandLineInput, - IReadOnlyDictionary environmentVariables, - ICommand expectedResult) - { - // Arrange - var applicationSchema = ApplicationSchema.Resolve(commandTypes); - var typeActivator = new DefaultTypeActivator(); - - // Act - var command = applicationSchema.InitializeEntryPoint(commandLineInput, environmentVariables, typeActivator); - - // Assert - command.Should().BeEquivalentTo(expectedResult, o => o.RespectingRuntimeTypes()); - } - - [TestCaseSource(nameof(GetTestCases_InitializeEntryPoint_Negative))] - public void InitializeEntryPoint_Negative_Test( - IReadOnlyList commandTypes, - CommandLineInput commandLineInput, - IReadOnlyDictionary environmentVariables) - { - // Arrange - var applicationSchema = ApplicationSchema.Resolve(commandTypes); - var typeActivator = new DefaultTypeActivator(); - - // Act & Assert - var ex = Assert.Throws(() => - applicationSchema.InitializeEntryPoint(commandLineInput, environmentVariables, typeActivator)); - Console.WriteLine(ex.Message); - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/Domain/CommandLineInputTests.cs b/CliFx.Tests/Domain/CommandLineInputTests.cs deleted file mode 100644 index 3075e14..0000000 --- a/CliFx.Tests/Domain/CommandLineInputTests.cs +++ /dev/null @@ -1,264 +0,0 @@ -using System.Collections.Generic; -using CliFx.Domain; -using FluentAssertions; -using NUnit.Framework; - -namespace CliFx.Tests.Domain -{ - [TestFixture] - internal class CommandLineInputTests - { - private static IEnumerable GetTestCases_Parse() - { - yield return new TestCaseData( - new string[0], - CommandLineInput.Empty - ); - - yield return new TestCaseData( - new[] {"param"}, - new CommandLineInput( - new[] {"param"}) - ); - - yield return new TestCaseData( - new[] {"cmd", "param"}, - new CommandLineInput( - new[] {"cmd", "param"}) - ); - - yield return new TestCaseData( - new[] {"--option", "value"}, - new CommandLineInput( - new[] - { - new CommandOptionInput("option", "value") - }) - ); - - yield return new TestCaseData( - new[] {"--option1", "value1", "--option2", "value2"}, - new CommandLineInput( - new[] - { - new CommandOptionInput("option1", "value1"), - new CommandOptionInput("option2", "value2") - }) - ); - - yield return new TestCaseData( - new[] {"--option", "value1", "value2"}, - new CommandLineInput( - new[] - { - new CommandOptionInput("option", new[] {"value1", "value2"}) - }) - ); - - yield return new TestCaseData( - new[] {"--option", "value1", "--option", "value2"}, - new CommandLineInput( - new[] - { - new CommandOptionInput("option", new[] {"value1", "value2"}) - }) - ); - - yield return new TestCaseData( - new[] {"-a", "value"}, - new CommandLineInput( - new[] - { - new CommandOptionInput("a", "value") - }) - ); - - yield return new TestCaseData( - new[] {"-a", "value1", "-b", "value2"}, - new CommandLineInput( - new[] - { - new CommandOptionInput("a", "value1"), - new CommandOptionInput("b", "value2") - }) - ); - - yield return new TestCaseData( - new[] {"-a", "value1", "value2"}, - new CommandLineInput( - new[] - { - new CommandOptionInput("a", new[] {"value1", "value2"}) - }) - ); - - yield return new TestCaseData( - new[] {"-a", "value1", "-a", "value2"}, - new CommandLineInput( - new[] - { - new CommandOptionInput("a", new[] {"value1", "value2"}) - }) - ); - - yield return new TestCaseData( - new[] {"--option1", "value1", "-b", "value2"}, - new CommandLineInput( - new[] - { - new CommandOptionInput("option1", "value1"), - new CommandOptionInput("b", "value2") - }) - ); - - yield return new TestCaseData( - new[] {"--switch"}, - new CommandLineInput( - new[] - { - new CommandOptionInput("switch") - }) - ); - - yield return new TestCaseData( - new[] {"--switch1", "--switch2"}, - new CommandLineInput( - new[] - { - new CommandOptionInput("switch1"), - new CommandOptionInput("switch2") - }) - ); - - yield return new TestCaseData( - new[] {"-s"}, - new CommandLineInput( - new[] - { - new CommandOptionInput("s") - }) - ); - - yield return new TestCaseData( - new[] {"-a", "-b"}, - new CommandLineInput( - new[] - { - new CommandOptionInput("a"), - new CommandOptionInput("b") - }) - ); - - yield return new TestCaseData( - new[] {"-ab"}, - new CommandLineInput( - new[] - { - new CommandOptionInput("a"), - new CommandOptionInput("b") - }) - ); - - yield return new TestCaseData( - new[] {"-ab", "value"}, - new CommandLineInput( - new[] - { - new CommandOptionInput("a"), - new CommandOptionInput("b", "value") - }) - ); - - yield return new TestCaseData( - new[] {"cmd", "--option", "value"}, - new CommandLineInput( - new[] {"cmd"}, - new[] - { - new CommandOptionInput("option", "value") - }) - ); - - yield return new TestCaseData( - new[] {"[debug]"}, - new CommandLineInput( - new[] {"debug"}, - new string[0], - new CommandOptionInput[0]) - ); - - yield return new TestCaseData( - new[] {"[debug]", "[preview]"}, - new CommandLineInput( - new[] {"debug", "preview"}, - new string[0], - new CommandOptionInput[0]) - ); - - yield return new TestCaseData( - new[] {"cmd", "param1", "param2", "--option", "value"}, - new CommandLineInput( - new[] {"cmd", "param1", "param2"}, - new[] - { - new CommandOptionInput("option", "value") - }) - ); - - yield return new TestCaseData( - new[] {"[debug]", "[preview]", "-o", "value"}, - new CommandLineInput( - new[] {"debug", "preview"}, - new string[0], - new[] - { - new CommandOptionInput("o", "value") - }) - ); - - yield return new TestCaseData( - new[] {"cmd", "[debug]", "[preview]", "-o", "value"}, - new CommandLineInput( - new[] {"debug", "preview"}, - new[] {"cmd"}, - new[] - { - new CommandOptionInput("o", "value") - }) - ); - - yield return new TestCaseData( - new[] {"cmd", "[debug]", "[preview]", "-o", "value"}, - new CommandLineInput( - new[] {"debug", "preview"}, - new[] {"cmd"}, - new[] - { - new CommandOptionInput("o", "value") - }) - ); - - yield return new TestCaseData( - new[] {"cmd", "param", "[debug]", "[preview]", "-o", "value"}, - new CommandLineInput( - new[] {"debug", "preview"}, - new[] {"cmd", "param"}, - new[] - { - new CommandOptionInput("o", "value") - }) - ); - } - - [Test] - [TestCaseSource(nameof(GetTestCases_Parse))] - public void Parse_Test(IReadOnlyList commandLineArguments, CommandLineInput expectedResult) - { - // Act - var result = CommandLineInput.Parse(commandLineArguments); - - // Assert - result.Should().BeEquivalentTo(expectedResult); - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/DummyTests.cs b/CliFx.Tests/DummyTests.cs deleted file mode 100644 index a18a888..0000000 --- a/CliFx.Tests/DummyTests.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using CliWrap; -using FluentAssertions; -using NUnit.Framework; - -namespace CliFx.Tests -{ - [TestFixture] - public class DummyTests - { - private static Assembly DummyAssembly { get; } = typeof(Dummy.Program).Assembly; - - private static IEnumerable GetTestCases_RunAsync() - { - yield return new TestCaseData( - new[] {"--version"}, - new Dictionary(), - $"v{DummyAssembly.GetName().Version}" - ); - - yield return new TestCaseData( - new string[0], - new Dictionary(), - "Hello World!" - ); - - yield return new TestCaseData( - new[] {"--target", "Earth"}, - new Dictionary(), - "Hello Earth!" - ); - - yield return new TestCaseData( - new string[0], - new Dictionary - { - ["ENV_TARGET"] = "Mars" - }, - "Hello Mars!" - ); - - yield return new TestCaseData( - new[] {"--target", "Earth"}, - new Dictionary - { - ["ENV_TARGET"] = "Mars" - }, - "Hello Earth!" - ); - } - - [TestCaseSource(nameof(GetTestCases_RunAsync))] - public async Task RunAsync_Test( - IReadOnlyList arguments, - IReadOnlyDictionary environmentVariables, - string expectedStdOut) - { - // Arrange - var cli = Cli.Wrap("dotnet") - .SetArguments(arguments.Prepend(DummyAssembly.Location).ToArray()) - .EnableExitCodeValidation() - .EnableStandardErrorValidation() - .SetStandardOutputCallback(Console.WriteLine) - .SetStandardErrorCallback(Console.WriteLine); - - foreach (var (key, value) in environmentVariables) - cli.SetEnvironmentVariable(key, value); - - // Act - var result = await cli.ExecuteAsync(); - - // Assert - result.ExitCode.Should().Be(0); - result.StandardError.Should().BeNullOrWhiteSpace(); - result.StandardOutput.TrimEnd().Should().Be(expectedStdOut); - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/EnvironmentVariablesSpecs.Commands.cs b/CliFx.Tests/EnvironmentVariablesSpecs.Commands.cs new file mode 100644 index 0000000..381e0b0 --- /dev/null +++ b/CliFx.Tests/EnvironmentVariablesSpecs.Commands.cs @@ -0,0 +1,27 @@ +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 new file mode 100644 index 0000000..c270ccb --- /dev/null +++ b/CliFx.Tests/EnvironmentVariablesSpecs.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using CliFx.Domain; +using CliWrap; +using CliWrap.Buffered; +using FluentAssertions; +using Xunit; + +namespace CliFx.Tests +{ + public partial 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() + { + // Arrange + var command = Cli.Wrap("dotnet") + .WithArguments(a => a + .Add(Dummy.Program.Location)) + .WithEnvironmentVariables(e => e + .Set("ENV_TARGET", "Mars")); + + // Act + var stdOut = await command.ExecuteBufferedAsync().Select(r => r.StandardOutput); + + // Assert + stdOut.TrimEnd().Should().Be("Hello Mars!"); + } + + // 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() + { + // Arrange + var command = Cli.Wrap("dotnet") + .WithArguments(a => a + .Add(Dummy.Program.Location) + .Add("--target") + .Add("Jupiter")) + .WithEnvironmentVariables(e => e + .Set("ENV_TARGET", "Mars")); + + // Act + var stdOut = await command.ExecuteBufferedAsync().Select(r => r.StandardOutput); + + // Assert + stdOut.TrimEnd().Should().Be("Hello Jupiter!"); + } + + [Fact] + public void Option_of_non_scalar_type_can_take_multiple_separated_values_from_an_environment_variable() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(EnvironmentVariableCollectionCommand)}); + + var input = CommandLineInput.Empty; + var envVars = new Dictionary + { + ["ENV_OPT"] = $"foo{Path.PathSeparator}bar" + }; + + // Act + var command = schema.InitializeEntryPoint(input, envVars); + + // Assert + command.Should().BeEquivalentTo(new EnvironmentVariableCollectionCommand + { + Option = new[] {"foo", "bar"} + }); + } + + [Fact] + public void Option_of_scalar_type_can_only_take_a_single_value_from_an_environment_variable_even_if_it_contains_separators() + { + // Arrange + var schema = ApplicationSchema.Resolve(new[] {typeof(EnvironmentVariableCommand)}); + + var input = CommandLineInput.Empty; + var envVars = new Dictionary + { + ["ENV_OPT"] = $"foo{Path.PathSeparator}bar" + }; + + // Act + var command = schema.InitializeEntryPoint(input, envVars); + + // Assert + command.Should().BeEquivalentTo(new EnvironmentVariableCommand + { + Option = $"foo{Path.PathSeparator}bar" + }); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/ErrorReportingSpecs.Commands.cs b/CliFx.Tests/ErrorReportingSpecs.Commands.cs new file mode 100644 index 0000000..c4e5a5f --- /dev/null +++ b/CliFx.Tests/ErrorReportingSpecs.Commands.cs @@ -0,0 +1,31 @@ +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; } = 1337; + + [CommandOption("msg", 'm')] + public string? Message { get; set; } + + public ValueTask ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/ErrorReportingSpecs.cs b/CliFx.Tests/ErrorReportingSpecs.cs new file mode 100644 index 0000000..9515d7d --- /dev/null +++ b/CliFx.Tests/ErrorReportingSpecs.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace CliFx.Tests +{ + public partial class ErrorReportingSpecs + { + [Fact] + public async Task Command_may_throw_a_generic_exception_which_exits_and_prints_full_error_details() + { + // Arrange + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand(typeof(GenericExceptionCommand)) + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync( + new[] {"exc", "-m", "Kaput"}, + new Dictionary()); + + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().Contain("Kaput"); + stdErrData.Length.Should().BeGreaterThan("Kaput".Length); + } + + [Fact] + public async Task Command_may_throw_a_specialized_exception_which_exits_with_custom_code_and_prints_minimal_error_details() + { + // Arrange + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand(typeof(CommandExceptionCommand)) + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync( + new[] {"exc", "-m", "Kaput", "-c", "69"}, + new Dictionary()); + + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().Be(69); + stdErrData.Should().Be("Kaput"); + } + + [Fact] + public async Task Command_may_throw_a_specialized_exception_without_error_message_which_exits_and_prints_full_error_details() + { + // Arrange + await using var stdErr = new MemoryStream(); + var console = new VirtualConsole(error: stdErr); + + var application = new CliApplicationBuilder() + .AddCommand(typeof(CommandExceptionCommand)) + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync( + new[] {"exc", "-m", "Kaput"}, + new Dictionary()); + + var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().NotBe(0); + stdErrData.Should().NotBeEmpty(); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/HelpTextSpecs.Commands.cs b/CliFx.Tests/HelpTextSpecs.Commands.cs new file mode 100644 index 0000000..dccc2a8 --- /dev/null +++ b/CliFx.Tests/HelpTextSpecs.Commands.cs @@ -0,0 +1,96 @@ +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-f", 'f', IsRequired = true)] + public string? OptionF { get; set; } + + [CommandOption("option-g", 'g', IsRequired = true)] + public IEnumerable? OptionG { get; set; } + + [CommandOption("option-h", 'h')] + public string? OptionH { 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; + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/HelpTextSpecs.cs b/CliFx.Tests/HelpTextSpecs.cs new file mode 100644 index 0000000..c688048 --- /dev/null +++ b/CliFx.Tests/HelpTextSpecs.cs @@ -0,0 +1,246 @@ +using System.IO; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace CliFx.Tests +{ + public partial class HelpTextSpecs + { + [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"); + } + + [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." + ); + } + + [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." + ); + } + + [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." + ); + } + + [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." + ); + } + + [Fact] + public async Task Help_text_shows_usage_format_which_lists_all_parameters() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand(typeof(ParametersCommand)) + .UseConsole(console) + .Build(); + + // Act + await application.RunAsync(new[] {"cmd-with-params", "--help"}); + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + + // Assert + stdOutData.Should().ContainAll( + "Usage", + "cmd-with-params", "", "", "", "[options]" + ); + } + + [Fact] + public async Task Help_text_shows_usage_format_which_lists_all_required_options() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand(typeof(RequiredOptionsCommand)) + .UseConsole(console) + .Build(); + + // Act + await application.RunAsync(new[] {"cmd-with-req-opts", "--help"}); + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + + // Assert + stdOutData.Should().ContainAll( + "Usage", + "cmd-with-req-opts", "--option-f ", "--option-g ", "[options]", + "Options", + "* -f|--option-f", + "* -g|--option-g", + "-h|--option-h" + ); + } + + [Fact] + public async Task Help_text_lists_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)) + .UseConsole(console) + .Build(); + + // Act + await application.RunAsync(new[] {"cmd-with-env-vars", "--help"}); + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + + // Assert + stdOutData.Should().ContainAll( + "Options", + "* -a|--option-a", "Environment variable:", "ENV_OPT_A", + "-b|--option-b", "Environment variable:", "ENV_OPT_B" + ); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/RoutingSpecs.Commands.cs b/CliFx.Tests/RoutingSpecs.Commands.cs new file mode 100644 index 0000000..3b61921 --- /dev/null +++ b/CliFx.Tests/RoutingSpecs.Commands.cs @@ -0,0 +1,51 @@ +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; } + + [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; } + + [CommandOption("divisor", 'd', IsRequired = true, Description = "The number to divide by.")] + public double Divisor { get; set; } + + 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 new file mode 100644 index 0000000..e6b8807 --- /dev/null +++ b/CliFx.Tests/RoutingSpecs.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace CliFx.Tests +{ + public partial class RoutingSpecs + { + [Fact] + public async Task Default_command_is_executed_if_provided_arguments_do_not_match_any_named_command() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand(typeof(DefaultCommand)) + .AddCommand(typeof(ConcatCommand)) + .AddCommand(typeof(DivideCommand)) + .UseConsole(console) + .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().Be("Hello world!"); + } + + [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"); + } + + [Fact] + public async Task Specific_named_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(typeof(DefaultCommand)) + .AddCommand(typeof(ConcatCommand)) + .AddCommand(typeof(DivideCommand)) + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync( + new[] {"concat", "-i", "foo", "bar", "-s", ", "}, + new Dictionary()); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + + // Assert + exitCode.Should().Be(0); + stdOutData.Should().Be("foo, bar"); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/AllRequiredOptionsCommand.cs b/CliFx.Tests/TestCommands/AllRequiredOptionsCommand.cs deleted file mode 100644 index 98b6036..0000000 --- a/CliFx.Tests/TestCommands/AllRequiredOptionsCommand.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command("allrequired", Description = "AllRequiredOptionsCommand description.")] - public class AllRequiredOptionsCommand : ICommand - { - [CommandOption("option-f", 'f', IsRequired = true, Description = "OptionF description.")] - public string? OptionF { get; set; } - - [CommandOption("option-g", 'g', IsRequired = true, Description = "OptionG description.")] - public string? OptionFG { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } -} diff --git a/CliFx.Tests/TestCommands/AllSupportedTypesCommand.cs b/CliFx.Tests/TestCommands/AllSupportedTypesCommand.cs deleted file mode 100644 index 291e6f4..0000000 --- a/CliFx.Tests/TestCommands/AllSupportedTypesCommand.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Threading.Tasks; -using CliFx.Attributes; -using CliFx.Tests.TestCustomTypes; - -namespace CliFx.Tests.TestCommands -{ - [Command] - public 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(TestEnum))] - public TestEnum TestEnum { get; set; } - - [CommandOption(nameof(IntNullable))] - public int? IntNullable { get; set; } - - [CommandOption(nameof(TestEnumNullable))] - public TestEnum? TestEnumNullable { get; set; } - - [CommandOption(nameof(TimeSpanNullable))] - public TimeSpan? TimeSpanNullable { get; set; } - - [CommandOption(nameof(TestStringConstructable))] - public TestStringConstructable? TestStringConstructable { get; set; } - - [CommandOption(nameof(TestStringParseable))] - public TestStringParseable? TestStringParseable { get; set; } - - [CommandOption(nameof(TestStringParseableWithFormatProvider))] - public TestStringParseableWithFormatProvider? 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(TestEnumArray))] - public TestEnum[]? TestEnumArray { get; set; } - - [CommandOption(nameof(IntNullableArray))] - public int?[]? IntNullableArray { get; set; } - - [CommandOption(nameof(TestStringConstructableArray))] - public TestStringConstructable[]? 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; } - - [CommandOption(nameof(NonConvertible))] - public TestNonStringParseable? NonConvertible { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/BrokenEnumerableCommand.cs b/CliFx.Tests/TestCommands/BrokenEnumerableCommand.cs deleted file mode 100644 index 0485403..0000000 --- a/CliFx.Tests/TestCommands/BrokenEnumerableCommand.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Threading.Tasks; -using CliFx.Attributes; -using CliFx.Tests.TestCustomTypes; - -namespace CliFx.Tests.TestCommands -{ - [Command] - public class BrokenEnumerableCommand : ICommand - { - [CommandParameter(0)] - public TestCustomEnumerable? Test { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/CancellableCommand.cs b/CliFx.Tests/TestCommands/CancellableCommand.cs deleted file mode 100644 index 0beb0eb..0000000 --- a/CliFx.Tests/TestCommands/CancellableCommand.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command("cancel")] - public class CancellableCommand : ICommand - { - public async ValueTask ExecuteAsync(IConsole console) - { - await Task.Delay(TimeSpan.FromSeconds(3), console.GetCancellationToken()); - console.Output.WriteLine("Never printed"); - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/CommandExceptionCommand.cs b/CliFx.Tests/TestCommands/CommandExceptionCommand.cs deleted file mode 100644 index adf9475..0000000 --- a/CliFx.Tests/TestCommands/CommandExceptionCommand.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Threading.Tasks; -using CliFx.Attributes; -using CliFx.Exceptions; - -namespace CliFx.Tests.TestCommands -{ - [Command("exc")] - public class CommandExceptionCommand : ICommand - { - [CommandOption("code", 'c')] - public int ExitCode { get; set; } = 1337; - - [CommandOption("msg", 'm')] - public string? Message { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode); - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/ConcatCommand.cs b/CliFx.Tests/TestCommands/ConcatCommand.cs deleted file mode 100644 index d75ff67..0000000 --- a/CliFx.Tests/TestCommands/ConcatCommand.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command("concat", Description = "Concatenate strings.")] - public class ConcatCommand : ICommand - { - [CommandOption('i', IsRequired = true, Description = "Input strings.")] - public IReadOnlyList Inputs { get; set; } - - [CommandOption('s', Description = "String separator.")] - public string Separator { get; set; } = ""; - - public ValueTask ExecuteAsync(IConsole console) - { - console.Output.WriteLine(string.Join(Separator, Inputs)); - return default; - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/DivideCommand.cs b/CliFx.Tests/TestCommands/DivideCommand.cs deleted file mode 100644 index 8653cb8..0000000 --- a/CliFx.Tests/TestCommands/DivideCommand.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command("div", Description = "Divide one number by another.")] - public class DivideCommand : ICommand - { - [CommandOption("dividend", 'D', IsRequired = true, Description = "The number to divide.")] - public double Dividend { get; set; } - - [CommandOption("divisor", 'd', IsRequired = true, Description = "The number to divide by.")] - public double Divisor { get; set; } - - // This property should be ignored by resolver - public bool NotAnOption { get; set; } - - public ValueTask ExecuteAsync(IConsole console) - { - console.Output.WriteLine(Dividend / Divisor); - return default; - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/DuplicateOptionEnvironmentVariableNamesCommand.cs b/CliFx.Tests/TestCommands/DuplicateOptionEnvironmentVariableNamesCommand.cs deleted file mode 100644 index 19a87bd..0000000 --- a/CliFx.Tests/TestCommands/DuplicateOptionEnvironmentVariableNamesCommand.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command] - public 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; - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/DuplicateOptionNamesCommand.cs b/CliFx.Tests/TestCommands/DuplicateOptionNamesCommand.cs deleted file mode 100644 index d5bd654..0000000 --- a/CliFx.Tests/TestCommands/DuplicateOptionNamesCommand.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command] - public class DuplicateOptionNamesCommand : ICommand - { - [CommandOption("fruits")] - public string? Apples { get; set; } - - [CommandOption("fruits")] - public string? Oranges { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/DuplicateOptionShortNamesCommand.cs b/CliFx.Tests/TestCommands/DuplicateOptionShortNamesCommand.cs deleted file mode 100644 index 6d5fa34..0000000 --- a/CliFx.Tests/TestCommands/DuplicateOptionShortNamesCommand.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command] - public class DuplicateOptionShortNamesCommand : ICommand - { - [CommandOption('x')] - public string? OptionA { get; set; } - - [CommandOption('x')] - public string? OptionB { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/DuplicateParameterNameCommand.cs b/CliFx.Tests/TestCommands/DuplicateParameterNameCommand.cs deleted file mode 100644 index 30c9f36..0000000 --- a/CliFx.Tests/TestCommands/DuplicateParameterNameCommand.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command] - public 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; - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/DuplicateParameterOrderCommand.cs b/CliFx.Tests/TestCommands/DuplicateParameterOrderCommand.cs deleted file mode 100644 index b193a6b..0000000 --- a/CliFx.Tests/TestCommands/DuplicateParameterOrderCommand.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command] - public class DuplicateParameterOrderCommand : ICommand - { - [CommandParameter(13)] - public string? ParameterA { get; set; } - - [CommandParameter(13)] - public string? ParameterB { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/EnvironmentVariableCommand.cs b/CliFx.Tests/TestCommands/EnvironmentVariableCommand.cs deleted file mode 100644 index 4ac9bc1..0000000 --- a/CliFx.Tests/TestCommands/EnvironmentVariableCommand.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command(Description = "Reads option values from environment variables.")] - public class EnvironmentVariableCommand : ICommand - { - [CommandOption("opt", EnvironmentVariableName = "ENV_SINGLE_VALUE")] - public string? Option { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } -} diff --git a/CliFx.Tests/TestCommands/EnvironmentVariableWithMultipleValuesCommand.cs b/CliFx.Tests/TestCommands/EnvironmentVariableWithMultipleValuesCommand.cs deleted file mode 100644 index ab6d836..0000000 --- a/CliFx.Tests/TestCommands/EnvironmentVariableWithMultipleValuesCommand.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command(Description = "Reads multiple option values from environment variables.")] - public class EnvironmentVariableWithMultipleValuesCommand : ICommand - { - [CommandOption("opt", EnvironmentVariableName = "ENV_MULTIPLE_VALUES")] - public IEnumerable? Option { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } -} diff --git a/CliFx.Tests/TestCommands/EnvironmentVariableWithoutCollectionPropertyCommand.cs b/CliFx.Tests/TestCommands/EnvironmentVariableWithoutCollectionPropertyCommand.cs deleted file mode 100644 index 6ef7386..0000000 --- a/CliFx.Tests/TestCommands/EnvironmentVariableWithoutCollectionPropertyCommand.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command(Description = "Reads one option value from environment variables because target property is not a collection.")] - public class EnvironmentVariableWithoutCollectionPropertyCommand : ICommand - { - [CommandOption("opt", EnvironmentVariableName = "ENV_MULTIPLE_VALUES")] - public string? Option { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } -} diff --git a/CliFx.Tests/TestCommands/ExceptionCommand.cs b/CliFx.Tests/TestCommands/ExceptionCommand.cs deleted file mode 100644 index 7f8f5ad..0000000 --- a/CliFx.Tests/TestCommands/ExceptionCommand.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command("exc")] - public class ExceptionCommand : 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/TestCommands/HelloWorldDefaultCommand.cs b/CliFx.Tests/TestCommands/HelloWorldDefaultCommand.cs deleted file mode 100644 index 2ee07b7..0000000 --- a/CliFx.Tests/TestCommands/HelloWorldDefaultCommand.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command] - public class HelloWorldDefaultCommand : ICommand - { - public ValueTask ExecuteAsync(IConsole console) - { - console.Output.WriteLine("Hello world."); - return default; - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/HelpDefaultCommand.cs b/CliFx.Tests/TestCommands/HelpDefaultCommand.cs deleted file mode 100644 index 78018fc..0000000 --- a/CliFx.Tests/TestCommands/HelpDefaultCommand.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command(Description = "HelpDefaultCommand description.")] - public class HelpDefaultCommand : 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; - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/HelpNamedCommand.cs b/CliFx.Tests/TestCommands/HelpNamedCommand.cs deleted file mode 100644 index 29f3a0d..0000000 --- a/CliFx.Tests/TestCommands/HelpNamedCommand.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command("cmd", Description = "HelpNamedCommand description.")] - public class HelpNamedCommand : ICommand - { - [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; - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/HelpSubCommand.cs b/CliFx.Tests/TestCommands/HelpSubCommand.cs deleted file mode 100644 index 7dba17f..0000000 --- a/CliFx.Tests/TestCommands/HelpSubCommand.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command("cmd sub", Description = "HelpSubCommand description.")] - public class HelpSubCommand : ICommand - { - [CommandOption("option-e", 'e', Description = "OptionE description.")] - public string? OptionE { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/MultipleNonScalarParametersCommand.cs b/CliFx.Tests/TestCommands/MultipleNonScalarParametersCommand.cs deleted file mode 100644 index a2a5c9d..0000000 --- a/CliFx.Tests/TestCommands/MultipleNonScalarParametersCommand.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command] - public class MultipleNonScalarParametersCommand : ICommand - { - [CommandParameter(0)] - public IReadOnlyList? ParameterA { get; set; } - - [CommandParameter(1)] - public IReadOnlyList? ParameterB { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/NonAnnotatedCommand.cs b/CliFx.Tests/TestCommands/NonAnnotatedCommand.cs deleted file mode 100644 index 7fe2798..0000000 --- a/CliFx.Tests/TestCommands/NonAnnotatedCommand.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace CliFx.Tests.TestCommands -{ - public class NonAnnotatedCommand : ICommand - { - public ValueTask ExecuteAsync(IConsole console) => default; - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/NonImplementedCommand.cs b/CliFx.Tests/TestCommands/NonImplementedCommand.cs deleted file mode 100644 index a21047d..0000000 --- a/CliFx.Tests/TestCommands/NonImplementedCommand.cs +++ /dev/null @@ -1,9 +0,0 @@ -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command] - public class NonImplementedCommand - { - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/NonLastNonScalarParameterCommand.cs b/CliFx.Tests/TestCommands/NonLastNonScalarParameterCommand.cs deleted file mode 100644 index 81e5c0b..0000000 --- a/CliFx.Tests/TestCommands/NonLastNonScalarParameterCommand.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command] - public class NonLastNonScalarParameterCommand : ICommand - { - [CommandParameter(0)] - public IReadOnlyList? ParameterA { get; set; } - - [CommandParameter(1)] - public string? ParameterB { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/ParameterCommand.cs b/CliFx.Tests/TestCommands/ParameterCommand.cs deleted file mode 100644 index 3170f1f..0000000 --- a/CliFx.Tests/TestCommands/ParameterCommand.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command("param cmd", Description = "Command using positional parameters")] - public class ParameterCommand : 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? OptionA { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/SimpleParameterCommand.cs b/CliFx.Tests/TestCommands/SimpleParameterCommand.cs deleted file mode 100644 index 59cfd07..0000000 --- a/CliFx.Tests/TestCommands/SimpleParameterCommand.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command("param cmd2", Description = "Command using positional parameters")] - public class SimpleParameterCommand : ICommand - { - [CommandParameter(0, Name = "first")] - public string? ParameterA { get; set; } - - [CommandParameter(10)] - public int? ParameterB { get; set; } - - [CommandOption("option", 'o')] - public string? OptionA { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCommands/SomeRequiredOptionsCommand.cs b/CliFx.Tests/TestCommands/SomeRequiredOptionsCommand.cs deleted file mode 100644 index a41d4fb..0000000 --- a/CliFx.Tests/TestCommands/SomeRequiredOptionsCommand.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Threading.Tasks; -using CliFx.Attributes; - -namespace CliFx.Tests.TestCommands -{ - [Command("somerequired", Description = "SomeRequiredOptionsCommand description.")] - public class SomeRequiredOptionsCommand : ICommand - { - [CommandOption("option-f", 'f', IsRequired = true, Description = "OptionF description.")] - public string? OptionF { get; set; } - - [CommandOption("option-g", 'g', Description = "OptionG description.")] - public string? OptionFG { get; set; } - - public ValueTask ExecuteAsync(IConsole console) => default; - } -} diff --git a/CliFx.Tests/TestCustomTypes/TestCustomEnumerable.cs b/CliFx.Tests/TestCustomTypes/TestCustomEnumerable.cs deleted file mode 100644 index 55ac197..0000000 --- a/CliFx.Tests/TestCustomTypes/TestCustomEnumerable.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections; -using System.Collections.Generic; - -namespace CliFx.Tests.TestCustomTypes -{ - public class TestCustomEnumerable : IEnumerable - { - private readonly T[] _arr = new T[0]; - - public IEnumerator GetEnumerator() => ((IEnumerable) _arr).GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCustomTypes/TestEnum.cs b/CliFx.Tests/TestCustomTypes/TestEnum.cs deleted file mode 100644 index 95d121b..0000000 --- a/CliFx.Tests/TestCustomTypes/TestEnum.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace CliFx.Tests.TestCustomTypes -{ - public enum TestEnum - { - Value1, - Value2, - Value3 - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCustomTypes/TestNonStringParseable.cs b/CliFx.Tests/TestCustomTypes/TestNonStringParseable.cs deleted file mode 100644 index 8162902..0000000 --- a/CliFx.Tests/TestCustomTypes/TestNonStringParseable.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace CliFx.Tests.TestCustomTypes -{ - public class TestNonStringParseable - { - public int Value { get; } - - public TestNonStringParseable(int value) - { - Value = value; - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCustomTypes/TestStringConstructable.cs b/CliFx.Tests/TestCustomTypes/TestStringConstructable.cs deleted file mode 100644 index 8e03f20..0000000 --- a/CliFx.Tests/TestCustomTypes/TestStringConstructable.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace CliFx.Tests.TestCustomTypes -{ - public class TestStringConstructable - { - public string Value { get; } - - public TestStringConstructable(string value) - { - Value = value; - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCustomTypes/TestStringParseable.cs b/CliFx.Tests/TestCustomTypes/TestStringParseable.cs deleted file mode 100644 index 6682e2b..0000000 --- a/CliFx.Tests/TestCustomTypes/TestStringParseable.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace CliFx.Tests.TestCustomTypes -{ - public class TestStringParseable - { - public string Value { get; } - - private TestStringParseable(string value) - { - Value = value; - } - - public static TestStringParseable Parse(string value) => new TestStringParseable(value); - } -} \ No newline at end of file diff --git a/CliFx.Tests/TestCustomTypes/TestStringParseableWithFormatProvider.cs b/CliFx.Tests/TestCustomTypes/TestStringParseableWithFormatProvider.cs deleted file mode 100644 index 27d7eeb..0000000 --- a/CliFx.Tests/TestCustomTypes/TestStringParseableWithFormatProvider.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace CliFx.Tests.TestCustomTypes -{ - public class TestStringParseableWithFormatProvider - { - public string Value { get; } - - private TestStringParseableWithFormatProvider(string value) - { - Value = value; - } - - public static TestStringParseableWithFormatProvider Parse(string value, IFormatProvider formatProvider) => - new TestStringParseableWithFormatProvider(value + " " + formatProvider); - } -} \ No newline at end of file diff --git a/CliFx.Tests/Utilities/ProgressTickerTests.cs b/CliFx.Tests/Utilities/ProgressTickerTests.cs deleted file mode 100644 index f831d39..0000000 --- a/CliFx.Tests/Utilities/ProgressTickerTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Linq; -using CliFx.Utilities; -using FluentAssertions; -using NUnit.Framework; - -namespace CliFx.Tests.Utilities -{ - [TestFixture] - public class ProgressTickerTests - { - [Test] - public void Report_Test() - { - // Arrange - using var console = new VirtualConsole(false); - var ticker = console.CreateProgressTicker(); - - var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray(); - var progressStringValues = progressValues.Select(p => p.ToString("P2")).ToArray(); - - // Act - foreach (var progress in progressValues) - ticker.Report(progress); - - // Assert - console.ReadOutputString().Should().ContainAll(progressStringValues); - } - - [Test] - public void Report_Redirected_Test() - { - // Arrange - using var console = new VirtualConsole(); - var ticker = console.CreateProgressTicker(); - - var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray(); - - // Act - foreach (var progress in progressValues) - ticker.Report(progress); - - // Assert - console.ReadOutputString().Should().BeEmpty(); - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/UtilitiesSpecs.cs b/CliFx.Tests/UtilitiesSpecs.cs new file mode 100644 index 0000000..ed055de --- /dev/null +++ b/CliFx.Tests/UtilitiesSpecs.cs @@ -0,0 +1,54 @@ +using System.IO; +using System.Linq; +using CliFx.Utilities; +using FluentAssertions; +using Xunit; + +namespace CliFx.Tests +{ + public class UtilitiesSpecs + { + [Fact] + public void Progress_ticker_can_be_used_to_report_progress_to_console() + { + // Arrange + using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut, isOutputRedirected: false); + + var ticker = console.CreateProgressTicker(); + + var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray(); + var progressStringValues = progressValues.Select(p => p.ToString("P2")).ToArray(); + + // Act + foreach (var progress in progressValues) + ticker.Report(progress); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()); + + // Assert + stdOutData.Should().ContainAll(progressStringValues); + } + + [Fact] + public void Progress_ticker_does_not_write_to_console_if_output_is_redirected() + { + // Arrange + using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var ticker = console.CreateProgressTicker(); + + var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray(); + + // Act + foreach (var progress in progressValues) + ticker.Report(progress); + + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()); + + // Assert + stdOutData.Should().BeEmpty(); + } + } +} \ No newline at end of file diff --git a/CliFx.Tests/VirtualConsoleTests.cs b/CliFx.Tests/VirtualConsoleTests.cs deleted file mode 100644 index 09f42eb..0000000 --- a/CliFx.Tests/VirtualConsoleTests.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using FluentAssertions; -using NUnit.Framework; - -namespace CliFx.Tests -{ - [TestFixture] - public class VirtualConsoleTests - { - [Test(Description = "Must not leak to system console")] - public void Smoke_Test() - { - // Arrange - using var console = new VirtualConsole(); - console.WriteInputString("hello world"); - - // Act - console.ResetColor(); - console.ForegroundColor = ConsoleColor.DarkMagenta; - console.BackgroundColor = ConsoleColor.DarkMagenta; - - // Assert - console.Input.Should().NotBeSameAs(Console.In); - console.IsInputRedirected.Should().BeTrue(); - console.Output.Should().NotBeSameAs(Console.Out); - console.IsOutputRedirected.Should().BeTrue(); - console.Error.Should().NotBeSameAs(Console.Error); - console.IsErrorRedirected.Should().BeTrue(); - console.ForegroundColor.Should().NotBe(Console.ForegroundColor); - console.BackgroundColor.Should().NotBe(Console.BackgroundColor); - } - } -} \ No newline at end of file diff --git a/CliFx.Tests/xunit.runner.json b/CliFx.Tests/xunit.runner.json new file mode 100644 index 0000000..186540e --- /dev/null +++ b/CliFx.Tests/xunit.runner.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "methodDisplayOptions": "all", + "methodDisplay": "method" +} \ No newline at end of file diff --git a/CliFx/CliApplication.cs b/CliFx/CliApplication.cs index 97c732c..d4b48b4 100644 --- a/CliFx/CliApplication.cs +++ b/CliFx/CliApplication.cs @@ -12,13 +12,15 @@ namespace CliFx /// /// Command line application facade. /// - public partial class CliApplication + public class CliApplication { private readonly ApplicationMetadata _metadata; private readonly ApplicationConfiguration _configuration; private readonly IConsole _console; private readonly ITypeActivator _typeActivator; + private readonly HelpTextWriter _helpTextWriter; + /// /// Initializes an instance of . /// @@ -30,6 +32,8 @@ namespace CliFx _configuration = configuration; _console = console; _typeActivator = typeActivator; + + _helpTextWriter = new HelpTextWriter(metadata, console); } private async ValueTask HandleDebugDirectiveAsync(CommandLineInput commandLineInput) @@ -67,7 +71,7 @@ namespace CliFx } // Parameters - foreach (var parameter in commandLineInput.Arguments.Skip(argumentOffset)) + foreach (var parameter in commandLineInput.UnboundArguments.Skip(argumentOffset)) { _console.Output.Write('<'); @@ -98,7 +102,7 @@ namespace CliFx private int? HandleVersionOption(CommandLineInput commandLineInput) { // Version option is available only on the default command (i.e. when arguments are not specified) - var shouldRenderVersion = !commandLineInput.Arguments.Any() && commandLineInput.IsVersionOptionSpecified; + var shouldRenderVersion = !commandLineInput.UnboundArguments.Any() && commandLineInput.IsVersionOptionSpecified; if (!shouldRenderVersion) return null; @@ -112,7 +116,7 @@ namespace CliFx // Help is rendered either when it's requested or when the user provides no arguments and there is no default command var shouldRenderHelp = commandLineInput.IsHelpOptionSpecified || - !applicationSchema.Commands.Any(c => c.IsDefault) && !commandLineInput.Arguments.Any() && !commandLineInput.Options.Any(); + !applicationSchema.Commands.Any(c => c.IsDefault) && !commandLineInput.UnboundArguments.Any() && !commandLineInput.Options.Any(); if (!shouldRenderHelp) return null; @@ -122,7 +126,7 @@ namespace CliFx applicationSchema.TryFindCommand(commandLineInput) ?? CommandSchema.StubDefaultCommand; - RenderHelp(applicationSchema, commandSchema); + _helpTextWriter.Write(applicationSchema, commandSchema); return 0; } diff --git a/CliFx/CliApplicationBuilder.cs b/CliFx/CliApplicationBuilder.cs index 83986a2..48b708d 100644 --- a/CliFx/CliApplicationBuilder.cs +++ b/CliFx/CliApplicationBuilder.cs @@ -46,7 +46,7 @@ namespace CliFx /// /// Adds commands from the specified assembly to the application. - /// Only the public types are added. + /// Only adds public valid command types. /// public CliApplicationBuilder AddCommandsFrom(Assembly commandAssembly) { @@ -58,7 +58,7 @@ namespace CliFx /// /// Adds commands from the specified assemblies to the application. - /// Only the public types are added. + /// Only adds public valid command types. /// public CliApplicationBuilder AddCommandsFrom(IEnumerable commandAssemblies) { @@ -70,7 +70,7 @@ namespace CliFx /// /// Adds commands from the calling assembly to the application. - /// Only the public types are added. + /// Only adds public valid command types. /// public CliApplicationBuilder AddCommandsFromThisAssembly() => AddCommandsFrom(Assembly.GetCallingAssembly()); diff --git a/CliFx/CliFx.csproj b/CliFx/CliFx.csproj index 121cc42..c9733a3 100644 --- a/CliFx/CliFx.csproj +++ b/CliFx/CliFx.csproj @@ -22,6 +22,9 @@ <_Parameter1>$(AssemblyName).Tests + + <_Parameter1>$(AssemblyName).Analyzers + @@ -31,7 +34,7 @@ - + diff --git a/CliFx/Domain/ApplicationSchema.cs b/CliFx/Domain/ApplicationSchema.cs index 640848d..5623996 100644 --- a/CliFx/Domain/ApplicationSchema.cs +++ b/CliFx/Domain/ApplicationSchema.cs @@ -47,9 +47,9 @@ namespace CliFx.Domain public CommandSchema? TryFindCommand(CommandLineInput commandLineInput, out int argumentOffset) { // Try to find the command that contains the most of the input arguments in its name - for (var i = commandLineInput.Arguments.Count; i >= 0; i--) + for (var i = commandLineInput.UnboundArguments.Count; i >= 0; i--) { - var potentialCommandName = string.Join(" ", commandLineInput.Arguments.Take(i)); + var potentialCommandName = string.Join(" ", commandLineInput.UnboundArguments.Take(i)); var matchingCommand = Commands.FirstOrDefault(c => c.MatchesName(potentialCommandName)); if (matchingCommand != null) @@ -75,15 +75,25 @@ namespace CliFx.Domain if (command == null) { throw new CliFxException( - $"Can't find a command that matches arguments [{string.Join(" ", commandLineInput.Arguments)}]."); + $"Can't find a command that matches arguments [{string.Join(" ", commandLineInput.UnboundArguments)}]."); } - var parameterInputs = argumentOffset == 0 - ? commandLineInput.Arguments - : commandLineInput.Arguments.Skip(argumentOffset).ToArray(); + var parameterValues = argumentOffset == 0 + ? commandLineInput.UnboundArguments.Select(a => a.Value).ToArray() + : commandLineInput.UnboundArguments.Skip(argumentOffset).Select(a => a.Value).ToArray(); - return command.CreateInstance(parameterInputs, commandLineInput.Options, environmentVariables, activator); + return command.CreateInstance(parameterValues, commandLineInput.Options, environmentVariables, activator); } + + public ICommand InitializeEntryPoint( + CommandLineInput commandLineInput, + IReadOnlyDictionary environmentVariables) => + InitializeEntryPoint(commandLineInput, environmentVariables, new DefaultTypeActivator()); + + public ICommand InitializeEntryPoint(CommandLineInput commandLineInput) => + InitializeEntryPoint(commandLineInput, new Dictionary()); + + public override string ToString() => string.Join(Environment.NewLine, Commands); } internal partial class ApplicationSchema diff --git a/CliFx/Domain/CommandDirectiveInput.cs b/CliFx/Domain/CommandDirectiveInput.cs new file mode 100644 index 0000000..4245ac0 --- /dev/null +++ b/CliFx/Domain/CommandDirectiveInput.cs @@ -0,0 +1,20 @@ +using System; + +namespace CliFx.Domain +{ + internal class CommandDirectiveInput + { + public string Name { get; } + + public bool IsDebugDirective => string.Equals(Name, "debug", StringComparison.OrdinalIgnoreCase); + + public bool IsPreviewDirective => string.Equals(Name, "preview", StringComparison.OrdinalIgnoreCase); + + public CommandDirectiveInput(string name) + { + Name = name; + } + + public override string ToString() => $"[{Name}]"; + } +} \ No newline at end of file diff --git a/CliFx/Domain/CommandLineInput.cs b/CliFx/Domain/CommandLineInput.cs index 647b82d..a914a89 100644 --- a/CliFx/Domain/CommandLineInput.cs +++ b/CliFx/Domain/CommandLineInput.cs @@ -8,49 +8,30 @@ namespace CliFx.Domain { internal partial class CommandLineInput { - public IReadOnlyList Directives { get; } + public IReadOnlyList Directives { get; } - public IReadOnlyList Arguments { get; } + public IReadOnlyList UnboundArguments { get; } public IReadOnlyList Options { get; } - public bool IsDebugDirectiveSpecified => Directives.Contains("debug", StringComparer.OrdinalIgnoreCase); + public bool IsDebugDirectiveSpecified => Directives.Any(d => d.IsDebugDirective); - public bool IsPreviewDirectiveSpecified => Directives.Contains("preview", StringComparer.OrdinalIgnoreCase); + public bool IsPreviewDirectiveSpecified => Directives.Any(d => d.IsPreviewDirective); - public bool IsHelpOptionSpecified => - Options.Any(o => CommandOptionSchema.HelpOption.MatchesNameOrShortName(o.Alias)); + public bool IsHelpOptionSpecified => Options.Any(o => o.IsHelpOption); - public bool IsVersionOptionSpecified => - Options.Any(o => CommandOptionSchema.VersionOption.MatchesNameOrShortName(o.Alias)); + public bool IsVersionOptionSpecified => Options.Any(o => o.IsVersionOption); public CommandLineInput( - IReadOnlyList directives, - IReadOnlyList arguments, + IReadOnlyList directives, + IReadOnlyList unboundArguments, IReadOnlyList options) { Directives = directives; - Arguments = arguments; + UnboundArguments = unboundArguments; Options = options; } - public CommandLineInput( - IReadOnlyList arguments, - IReadOnlyList options) - : this(new string[0], arguments, options) - { - } - - public CommandLineInput(IReadOnlyList arguments) - : this(arguments, new CommandOptionInput[0]) - { - } - - public CommandLineInput(IReadOnlyList options) - : this(new string[0], options) - { - } - public override string ToString() { var buffer = new StringBuilder(); @@ -58,13 +39,10 @@ namespace CliFx.Domain foreach (var directive in Directives) { buffer.AppendIfNotEmpty(' '); - buffer - .Append('[') - .Append(directive) - .Append(']'); + buffer.Append(directive); } - foreach (var argument in Arguments) + foreach (var argument in UnboundArguments) { buffer.AppendIfNotEmpty(' '); buffer.Append(argument); @@ -84,16 +62,14 @@ namespace CliFx.Domain { public static CommandLineInput Parse(IReadOnlyList commandLineArguments) { - var directives = new List(); - var arguments = new List(); - var optionsDic = new Dictionary>(); + var builder = new CommandLineInputBuilder(); - // Option aliases and values are parsed in pairs so we need to keep track of last alias - var lastOptionAlias = ""; + var currentOptionAlias = ""; + var currentOptionValues = new List(); bool TryParseDirective(string argument) { - if (!string.IsNullOrWhiteSpace(lastOptionAlias)) + if (!string.IsNullOrWhiteSpace(currentOptionAlias)) return false; if (!argument.StartsWith("[", StringComparison.OrdinalIgnoreCase) || @@ -101,17 +77,17 @@ namespace CliFx.Domain return false; var directive = argument.Substring(1, argument.Length - 2); - directives.Add(directive); + builder.AddDirective(directive); return true; } bool TryParseArgument(string argument) { - if (!string.IsNullOrWhiteSpace(lastOptionAlias)) + if (!string.IsNullOrWhiteSpace(currentOptionAlias)) return false; - arguments.Add(argument); + builder.AddUnboundArgument(argument); return true; } @@ -121,10 +97,11 @@ namespace CliFx.Domain if (!argument.StartsWith("--", StringComparison.OrdinalIgnoreCase)) return false; - lastOptionAlias = argument.Substring(2); + if (!string.IsNullOrWhiteSpace(currentOptionAlias)) + builder.AddOption(currentOptionAlias, currentOptionValues); - if (!optionsDic.ContainsKey(lastOptionAlias)) - optionsDic[lastOptionAlias] = new List(); + currentOptionAlias = argument.Substring(2); + currentOptionValues = new List(); return true; } @@ -136,10 +113,11 @@ namespace CliFx.Domain foreach (var c in argument.Substring(1)) { - lastOptionAlias = c.AsString(); + if (!string.IsNullOrWhiteSpace(currentOptionAlias)) + builder.AddOption(currentOptionAlias, currentOptionValues); - if (!optionsDic.ContainsKey(lastOptionAlias)) - optionsDic[lastOptionAlias] = new List(); + currentOptionAlias = c.AsString(); + currentOptionValues = new List(); } return true; @@ -147,10 +125,10 @@ namespace CliFx.Domain bool TryParseOptionValue(string argument) { - if (string.IsNullOrWhiteSpace(lastOptionAlias)) + if (string.IsNullOrWhiteSpace(currentOptionAlias)) return false; - optionsDic[lastOptionAlias].Add(argument); + currentOptionValues.Add(argument); return true; } @@ -165,15 +143,21 @@ namespace CliFx.Domain TryParseOptionValue(argument); } - var options = optionsDic.Select(p => new CommandOptionInput(p.Key, p.Value)).ToArray(); + if (!string.IsNullOrWhiteSpace(currentOptionAlias)) + builder.AddOption(currentOptionAlias, currentOptionValues); - return new CommandLineInput(directives, arguments, options); + return builder.Build(); } } internal partial class CommandLineInput { - public static CommandLineInput Empty { get; } = - new CommandLineInput(new string[0], new string[0], new CommandOptionInput[0]); + private static IReadOnlyList EmptyDirectives { get; } = new CommandDirectiveInput[0]; + + private static IReadOnlyList EmptyUnboundArguments { get; } = new CommandUnboundArgumentInput[0]; + + private static IReadOnlyList EmptyOptions { get; } = new CommandOptionInput[0]; + + public static CommandLineInput Empty { get; } = new CommandLineInput(EmptyDirectives, EmptyUnboundArguments, EmptyOptions); } } \ No newline at end of file diff --git a/CliFx/Domain/CommandLineInputBuilder.cs b/CliFx/Domain/CommandLineInputBuilder.cs new file mode 100644 index 0000000..ec3aafa --- /dev/null +++ b/CliFx/Domain/CommandLineInputBuilder.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; + +namespace CliFx.Domain +{ + internal class CommandLineInputBuilder + { + private readonly List _directives = new List(); + private readonly List _unboundArguments = new List(); + private readonly List _options = new List(); + + public CommandLineInputBuilder AddDirective(CommandDirectiveInput directive) + { + _directives.Add(directive); + return this; + } + + public CommandLineInputBuilder AddDirective(string directive) => + AddDirective(new CommandDirectiveInput(directive)); + + public CommandLineInputBuilder AddUnboundArgument(CommandUnboundArgumentInput unboundArgument) + { + _unboundArguments.Add(unboundArgument); + return this; + } + + public CommandLineInputBuilder AddUnboundArgument(string unboundArgument) => + AddUnboundArgument(new CommandUnboundArgumentInput(unboundArgument)); + + public CommandLineInputBuilder AddOption(CommandOptionInput option) + { + _options.Add(option); + return this; + } + + public CommandLineInputBuilder AddOption(string optionAlias, IReadOnlyList values) => + AddOption(new CommandOptionInput(optionAlias, values)); + + public CommandLineInputBuilder AddOption(string optionAlias, params string[] values) => + AddOption(optionAlias, (IReadOnlyList) values); + + public CommandLineInput Build() => new CommandLineInput(_directives, _unboundArguments, _options); + } +} \ No newline at end of file diff --git a/CliFx/Domain/CommandOptionInput.cs b/CliFx/Domain/CommandOptionInput.cs index 8f4009a..adb2780 100644 --- a/CliFx/Domain/CommandOptionInput.cs +++ b/CliFx/Domain/CommandOptionInput.cs @@ -10,22 +10,16 @@ namespace CliFx.Domain public IReadOnlyList Values { get; } + public bool IsHelpOption => CommandOptionSchema.HelpOption.MatchesNameOrShortName(Alias); + + public bool IsVersionOption => CommandOptionSchema.VersionOption.MatchesNameOrShortName(Alias); + public CommandOptionInput(string alias, IReadOnlyList values) { Alias = alias; Values = values; } - public CommandOptionInput(string alias, string value) - : this(alias, new[] {value}) - { - } - - public CommandOptionInput(string alias) - : this(alias, new string[0]) - { - } - public override string ToString() { var buffer = new StringBuilder(); diff --git a/CliFx/Domain/CommandUnboundArgumentInput.cs b/CliFx/Domain/CommandUnboundArgumentInput.cs new file mode 100644 index 0000000..deed700 --- /dev/null +++ b/CliFx/Domain/CommandUnboundArgumentInput.cs @@ -0,0 +1,14 @@ +namespace CliFx.Domain +{ + internal class CommandUnboundArgumentInput + { + public string Value { get; } + + public CommandUnboundArgumentInput(string value) + { + Value = value; + } + + public override string ToString() => Value; + } +} \ No newline at end of file diff --git a/CliFx/CliApplication.Help.cs b/CliFx/Domain/HelpTextWriter.cs similarity index 95% rename from CliFx/CliApplication.Help.cs rename to CliFx/Domain/HelpTextWriter.cs index 2937ee2..cdf703a 100644 --- a/CliFx/CliApplication.Help.cs +++ b/CliFx/Domain/HelpTextWriter.cs @@ -1,13 +1,21 @@ using System; using System.Linq; -using CliFx.Domain; using CliFx.Internal; -namespace CliFx +namespace CliFx.Domain { - public partial class CliApplication + internal class HelpTextWriter { - private void RenderHelp(ApplicationSchema applicationSchema, CommandSchema command) + private readonly ApplicationMetadata _metadata; + private readonly IConsole _console; + + public HelpTextWriter(ApplicationMetadata metadata, IConsole console) + { + _metadata = metadata; + _console = console; + } + + public void Write(ApplicationSchema applicationSchema, CommandSchema command) { var column = 0; var row = 0; diff --git a/CliFx/Internal/Polyfills.cs b/CliFx/Internal/Polyfills.cs index 77dfcaa..126c272 100644 --- a/CliFx/Internal/Polyfills.cs +++ b/CliFx/Internal/Polyfills.cs @@ -1,4 +1,6 @@ -#if NET45 || NETSTANDARD2_0 +// ReSharper disable CheckNamespace + +#if NET45 || NETSTANDARD2_0 using System.Collections.Generic; using System.Text; diff --git a/CliFx/VirtualConsole.cs b/CliFx/VirtualConsole.cs index bab5e7b..b76ec5b 100644 --- a/CliFx/VirtualConsole.cs +++ b/CliFx/VirtualConsole.cs @@ -9,78 +9,10 @@ namespace CliFx /// Does not leak to system console in any way. /// Use this class as a substitute for system console when running tests. /// - public partial class VirtualConsole - { - private readonly MemoryStream _inputStream = new MemoryStream(); - private readonly MemoryStream _outputStream = new MemoryStream(); - private readonly MemoryStream _errorStream = new MemoryStream(); - private readonly CancellationTokenSource _cts = new CancellationTokenSource(); - - /// - /// Initializes an instance of . - /// - public VirtualConsole(bool isRedirected) - { - Input = new StreamReader(_inputStream, Console.InputEncoding, false); - Output = new StreamWriter(_outputStream, Console.OutputEncoding) {AutoFlush = true}; - Error = new StreamWriter(_errorStream, Console.OutputEncoding) {AutoFlush = true}; - - IsInputRedirected = isRedirected; - IsOutputRedirected = isRedirected; - IsErrorRedirected = isRedirected; - } - - /// - /// Initializes an instance of . - /// - public VirtualConsole() - : this(true) - { - } - - /// - /// Writes raw data to input stream. - /// - public void WriteInputData(byte[] data) => _inputStream.Write(data, 0, data.Length); - - /// - /// Writes text to input stream. - /// - public void WriteInputString(string str) => WriteInputData(Input.CurrentEncoding.GetBytes(str)); - - /// - /// Reads all data written to output stream thus far. - /// - public byte[] ReadOutputData() => _outputStream.ToArray(); - - /// - /// Reads all text written to output stream thus far. - /// - public string ReadOutputString() => Output.Encoding.GetString(ReadOutputData()); - - /// - /// Reads all data written to error stream thus far. - /// - public byte[] ReadErrorData() => _errorStream.ToArray(); - - /// - /// Reads all text written to error stream thus far. - /// - public string ReadErrorString() => Error.Encoding.GetString(ReadErrorData()); - - /// - /// Sends an interrupt signal. - /// - public void Cancel() => _cts.Cancel(); - - /// - /// Sends an interrupt signal after a delay. - /// - public void CancelAfter(TimeSpan delay) => _cts.CancelAfter(delay); - } - public partial class VirtualConsole : IConsole { + private readonly CancellationToken _cancellationToken; + /// public StreamReader Input { get; } @@ -113,21 +45,55 @@ namespace CliFx } /// - public CancellationToken GetCancellationToken() => _cts.Token; - } + public CancellationToken GetCancellationToken() => _cancellationToken; - public partial class VirtualConsole : IDisposable - { - /// - public void Dispose() + /// + /// Initializes an instance of . + /// Use named parameters to specify the streams you want to override. + /// + public VirtualConsole( + StreamReader? input = null, bool isInputRedirected = true, + StreamWriter? output = null, bool isOutputRedirected = true, + StreamWriter? error = null, bool isErrorRedirected = true, + CancellationToken cancellationToken = default) + { + Input = input ?? StreamReader.Null; + IsInputRedirected = isInputRedirected; + Output = output ?? StreamWriter.Null; + IsOutputRedirected = isOutputRedirected; + Error = error ?? StreamWriter.Null; + IsErrorRedirected = isErrorRedirected; + _cancellationToken = cancellationToken; + } + + /// + /// Initializes an instance of . + /// Use named parameters to specify the streams you want to override. + /// + public VirtualConsole( + Stream? input = null, bool isInputRedirected = true, + Stream? output = null, bool isOutputRedirected = true, + Stream? error = null, bool isErrorRedirected = true, + CancellationToken cancellationToken = default) + : this( + WrapInput(input), isInputRedirected, + WrapOutput(output), isOutputRedirected, + WrapOutput(error), isErrorRedirected, + cancellationToken) { - _inputStream.Dispose(); - _outputStream.Dispose(); - _errorStream.Dispose(); - _cts.Dispose(); - Input.Dispose(); - Output.Dispose(); - Error.Dispose(); } } + + public partial class VirtualConsole + { + private static StreamReader WrapInput(Stream? stream) => + stream != null + ? new StreamReader(stream, Console.InputEncoding, false) + : StreamReader.Null; + + private static StreamWriter WrapOutput(Stream? stream) => + stream != null + ? new StreamWriter(stream, Console.OutputEncoding) {AutoFlush = true} + : StreamWriter.Null; + } } \ No newline at end of file