Get rid of internal tests

Move all tests to e2e+ level
This commit is contained in:
Alexey Golub
2020-08-19 19:37:44 +03:00
parent b17341b56c
commit 3831cfc7c0
63 changed files with 3058 additions and 2751 deletions

View File

@@ -18,7 +18,7 @@ namespace CliFx.Benchmarks
[Benchmark(Description = "CliFx", Baseline = true)]
public async ValueTask<int> ExecuteWithCliFx() =>
await new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments, new Dictionary<string, string>());
await new CliApplicationBuilder().AddCommand<CliFxCommand>().Build().RunAsync(Arguments, new Dictionary<string, string>());
[Benchmark(Description = "System.CommandLine")]
public async Task<int> ExecuteWithSystemCommandLine() =>

View File

@@ -1,179 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests
{
public partial class ApplicationSpecs
{
[Command]
private class DefaultCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command]
private class AnotherDefaultCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command]
private class NonImplementedCommand
{
}
private class NonAnnotatedCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command("dup")]
private class DuplicateNameCommandA : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command("dup")]
private class DuplicateNameCommandB : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command]
private class DuplicateParameterOrderCommand : ICommand
{
[CommandParameter(13)]
public string? ParameterA { get; set; }
[CommandParameter(13)]
public string? ParameterB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command]
private class DuplicateParameterNameCommand : ICommand
{
[CommandParameter(0, Name = "param")]
public string? ParameterA { get; set; }
[CommandParameter(1, Name = "param")]
public string? ParameterB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command]
private class MultipleNonScalarParametersCommand : ICommand
{
[CommandParameter(0)]
public IReadOnlyList<string>? ParameterA { get; set; }
[CommandParameter(1)]
public IReadOnlyList<string>? ParameterB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command]
private class NonLastNonScalarParameterCommand : ICommand
{
[CommandParameter(0)]
public IReadOnlyList<string>? ParameterA { get; set; }
[CommandParameter(1)]
public string? ParameterB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command]
private class EmptyOptionNameCommand : ICommand
{
[CommandOption("")]
public string? Apples { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command]
private class SingleCharacterOptionNameCommand : ICommand
{
[CommandOption("a")]
public string? Apples { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command]
private class DuplicateOptionNamesCommand : ICommand
{
[CommandOption("fruits")]
public string? Apples { get; set; }
[CommandOption("fruits")]
public string? Oranges { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command]
private class DuplicateOptionShortNamesCommand : ICommand
{
[CommandOption('x')]
public string? OptionA { get; set; }
[CommandOption('x')]
public string? OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command]
private class ConflictWithHelpOptionCommand : ICommand
{
[CommandOption("option-h", 'h')]
public string? OptionH { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command]
private class ConflictWithVersionOptionCommand : ICommand
{
[CommandOption("version")]
public string? Version { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command]
private class DuplicateOptionEnvironmentVariableNamesCommand : ICommand
{
[CommandOption("option-a", EnvironmentVariableName = "ENV_VAR")]
public string? OptionA { get; set; }
[CommandOption("option-b", EnvironmentVariableName = "ENV_VAR")]
public string? OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command("hidden", Description = "Description")]
private class HiddenPropertiesCommand : ICommand
{
[CommandParameter(13, Name = "param", Description = "Param description")]
public string? Parameter { get; set; }
[CommandOption("option", 'o', Description = "Option description", EnvironmentVariableName = "ENV")]
public string? Option { get; set; }
public string? HiddenA { get; set; }
public bool? HiddenB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}
}

View File

@@ -1,14 +1,15 @@
using System;
using System.IO;
using CliFx.Domain;
using CliFx.Exceptions;
using System.Threading.Tasks;
using CliFx.Tests.Commands;
using CliFx.Tests.Commands.Invalid;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;
namespace CliFx.Tests
{
public partial class ApplicationSpecs
public class ApplicationSpecs
{
private readonly ITestOutputHelper _output;
@@ -31,7 +32,7 @@ namespace CliFx.Tests
{
// Act
var app = new CliApplicationBuilder()
.AddCommand(typeof(DefaultCommand))
.AddCommand<DefaultCommand>()
.AddCommandsFrom(typeof(DefaultCommand).Assembly)
.AddCommands(new[] {typeof(DefaultCommand)})
.AddCommandsFrom(new[] {typeof(DefaultCommand).Assembly})
@@ -51,219 +52,356 @@ namespace CliFx.Tests
}
[Fact]
public void At_least_one_command_must_be_defined_in_an_application()
public async Task At_least_one_command_must_be_defined_in_an_application()
{
// Arrange
var commandTypes = Array.Empty<Type>();
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(error: stdErr);
// Act & assert
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
}
[Fact]
public void Commands_must_implement_the_corresponding_interface()
{
// Arrange
var commandTypes = new[] {typeof(NonImplementedCommand)};
// Act & assert
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
}
[Fact]
public void Commands_must_be_annotated_by_an_attribute()
{
// Arrange
var commandTypes = new[] {typeof(NonAnnotatedCommand)};
// Act & assert
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
}
[Fact]
public void Commands_must_have_unique_names()
{
// Arrange
var commandTypes = new[] {typeof(DuplicateNameCommandA), typeof(DuplicateNameCommandB)};
// Act & assert
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
}
[Fact]
public void Command_can_be_default_but_only_if_it_is_the_only_such_command()
{
// Arrange
var commandTypes = new[] {typeof(DefaultCommand), typeof(AnotherDefaultCommand)};
// Act & assert
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
}
[Fact]
public void Command_parameters_must_have_unique_order()
{
// Arrange
var commandTypes = new[] {typeof(DuplicateParameterOrderCommand)};
// Act & assert
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
}
[Fact]
public void Command_parameters_must_have_unique_names()
{
// Arrange
var commandTypes = new[] {typeof(DuplicateParameterNameCommand)};
// Act & assert
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
}
[Fact]
public void Command_parameter_can_be_non_scalar_only_if_no_other_such_parameter_is_present()
{
// Arrange
var commandTypes = new[] {typeof(MultipleNonScalarParametersCommand)};
// Act & assert
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
}
[Fact]
public void Command_parameter_can_be_non_scalar_only_if_it_is_the_last_in_order()
{
// Arrange
var commandTypes = new[] {typeof(NonLastNonScalarParameterCommand)};
// Act & assert
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
}
[Fact]
public void Command_options_must_have_names_that_are_not_empty()
{
// Arrange
var commandTypes = new[] {typeof(EmptyOptionNameCommand)};
// Act & assert
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
}
[Fact]
public void Command_options_must_have_names_that_are_longer_than_one_character()
{
// Arrange
var commandTypes = new[] {typeof(SingleCharacterOptionNameCommand)};
// Act & assert
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
}
[Fact]
public void Command_options_must_have_unique_names()
{
// Arrange
var commandTypes = new[] {typeof(DuplicateOptionNamesCommand)};
// Act & assert
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
}
[Fact]
public void Command_options_must_have_unique_short_names()
{
// Arrange
var commandTypes = new[] {typeof(DuplicateOptionShortNamesCommand)};
// Act & assert
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
}
[Fact]
public void Command_options_must_not_have_conflicts_with_the_implicit_help_option()
{
// Arrange
var commandTypes = new[] {typeof(ConflictWithHelpOptionCommand)};
// Act & assert
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
}
[Fact]
public void Command_options_must_not_have_conflicts_with_the_implicit_version_option()
{
// Arrange
var commandTypes = new[] {typeof(ConflictWithVersionOptionCommand)};
// Act & assert
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
}
[Fact]
public void Command_options_must_have_unique_environment_variable_names()
{
// Arrange
var commandTypes = new[] {typeof(DuplicateOptionEnvironmentVariableNamesCommand)};
// Act & assert
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
_output.WriteLine(ex.Message);
}
[Fact]
public void Command_options_and_parameters_must_be_annotated_by_corresponding_attributes()
{
// Arrange
var commandTypes = new[] {typeof(HiddenPropertiesCommand)};
var application = new CliApplicationBuilder()
.UseConsole(console)
.Build();
// Act
var schema = RootSchema.Resolve(commandTypes);
var exitCode = await application.RunAsync(Array.Empty<string>());
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
schema.Should().BeEquivalentTo(new RootSchema(new[]
{
new CommandSchema(
typeof(HiddenPropertiesCommand),
"hidden",
"Description",
new[]
{
new CommandParameterSchema(
typeof(HiddenPropertiesCommand).GetProperty(nameof(HiddenPropertiesCommand.Parameter))!,
13,
"param",
"Param description")
},
new[]
{
new CommandOptionSchema(
typeof(HiddenPropertiesCommand).GetProperty(nameof(HiddenPropertiesCommand.Option))!,
"option",
'o',
"ENV",
false,
"Option description"),
CommandOptionSchema.HelpOption
})
}));
exitCode.Should().NotBe(0);
stdErrData.Should().NotBeEmpty();
schema.ToString().Should().NotBeNullOrWhiteSpace(); // this is only for coverage, I'm sorry
_output.WriteLine(stdErrData);
}
[Fact]
public async Task Commands_must_implement_the_corresponding_interface()
{
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand(typeof(NonImplementedCommand))
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().NotBe(0);
stdErrData.Should().NotBeEmpty();
_output.WriteLine(stdErrData);
}
[Fact]
public async Task Commands_must_be_annotated_by_an_attribute()
{
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand<NonAnnotatedCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().NotBe(0);
stdErrData.Should().NotBeEmpty();
_output.WriteLine(stdErrData);
}
[Fact]
public async Task Commands_must_have_unique_names()
{
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand<GenericExceptionCommand>()
.AddCommand<CommandExceptionCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().NotBe(0);
stdErrData.Should().NotBeEmpty();
_output.WriteLine(stdErrData);
}
[Fact]
public async Task Command_can_be_default_but_only_if_it_is_the_only_such_command()
{
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand<DefaultCommand>()
.AddCommand<OtherDefaultCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().NotBe(0);
stdErrData.Should().NotBeEmpty();
_output.WriteLine(stdErrData);
}
[Fact]
public async Task Command_parameters_must_have_unique_order()
{
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand<DuplicateParameterOrderCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().NotBe(0);
stdErrData.Should().NotBeEmpty();
_output.WriteLine(stdErrData);
}
[Fact]
public async Task Command_parameters_must_have_unique_names()
{
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand<DuplicateParameterNameCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().NotBe(0);
stdErrData.Should().NotBeEmpty();
_output.WriteLine(stdErrData);
}
[Fact]
public async Task Command_parameter_can_be_non_scalar_only_if_no_other_such_parameter_is_present()
{
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand<MultipleNonScalarParametersCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().NotBe(0);
stdErrData.Should().NotBeEmpty();
_output.WriteLine(stdErrData);
}
[Fact]
public async Task Command_parameter_can_be_non_scalar_only_if_it_is_the_last_in_order()
{
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand<NonLastNonScalarParameterCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().NotBe(0);
stdErrData.Should().NotBeEmpty();
_output.WriteLine(stdErrData);
}
[Fact]
public async Task Command_options_must_have_names_that_are_not_empty()
{
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand<EmptyOptionNameCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().NotBe(0);
stdErrData.Should().NotBeEmpty();
_output.WriteLine(stdErrData);
}
[Fact]
public async Task Command_options_must_have_names_that_are_longer_than_one_character()
{
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand<SingleCharacterOptionNameCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().NotBe(0);
stdErrData.Should().NotBeEmpty();
_output.WriteLine(stdErrData);
}
[Fact]
public async Task Command_options_must_have_unique_names()
{
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand<DuplicateOptionNamesCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().NotBe(0);
stdErrData.Should().NotBeEmpty();
_output.WriteLine(stdErrData);
}
[Fact]
public async Task Command_options_must_have_unique_short_names()
{
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand<DuplicateOptionShortNamesCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().NotBe(0);
stdErrData.Should().NotBeEmpty();
_output.WriteLine(stdErrData);
}
[Fact]
public async Task Command_options_must_have_unique_environment_variable_names()
{
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand<DuplicateOptionEnvironmentVariableNamesCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().NotBe(0);
stdErrData.Should().NotBeEmpty();
_output.WriteLine(stdErrData);
}
[Fact]
public async Task Command_options_must_not_have_conflicts_with_the_implicit_help_option()
{
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand<ConflictWithHelpOptionCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().NotBe(0);
stdErrData.Should().NotBeEmpty();
_output.WriteLine(stdErrData);
}
[Fact]
public async Task Command_options_must_not_have_conflicts_with_the_implicit_version_option()
{
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand<ConflictWithVersionOptionCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().NotBe(0);
stdErrData.Should().NotBeEmpty();
_output.WriteLine(stdErrData);
}
}
}

View File

@@ -1,197 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests
{
public partial class ArgumentBindingSpecs
{
[Command]
private class AllSupportedTypesCommand : ICommand
{
[CommandOption(nameof(Object))]
public object? Object { get; set; } = 42;
[CommandOption(nameof(String))]
public string? String { get; set; } = "foo bar";
[CommandOption(nameof(Bool))]
public bool Bool { get; set; }
[CommandOption(nameof(Char))]
public char Char { get; set; }
[CommandOption(nameof(Sbyte))]
public sbyte Sbyte { get; set; }
[CommandOption(nameof(Byte))]
public byte Byte { get; set; }
[CommandOption(nameof(Short))]
public short Short { get; set; }
[CommandOption(nameof(Ushort))]
public ushort Ushort { get; set; }
[CommandOption(nameof(Int))]
public int Int { get; set; }
[CommandOption(nameof(Uint))]
public uint Uint { get; set; }
[CommandOption(nameof(Long))]
public long Long { get; set; }
[CommandOption(nameof(Ulong))]
public ulong Ulong { get; set; }
[CommandOption(nameof(Float))]
public float Float { get; set; }
[CommandOption(nameof(Double))]
public double Double { get; set; }
[CommandOption(nameof(Decimal))]
public decimal Decimal { get; set; }
[CommandOption(nameof(DateTime))]
public DateTime DateTime { get; set; }
[CommandOption(nameof(DateTimeOffset))]
public DateTimeOffset DateTimeOffset { get; set; }
[CommandOption(nameof(TimeSpan))]
public TimeSpan TimeSpan { get; set; }
[CommandOption(nameof(CustomEnum))]
public CustomEnum CustomEnum { get; set; }
[CommandOption(nameof(IntNullable))]
public int? IntNullable { get; set; }
[CommandOption(nameof(CustomEnumNullable))]
public CustomEnum? CustomEnumNullable { get; set; }
[CommandOption(nameof(TimeSpanNullable))]
public TimeSpan? TimeSpanNullable { get; set; }
[CommandOption(nameof(TestStringConstructable))]
public StringConstructable? TestStringConstructable { get; set; }
[CommandOption(nameof(TestStringParseable))]
public StringParseable? TestStringParseable { get; set; }
[CommandOption(nameof(TestStringParseableWithFormatProvider))]
public StringParseableWithFormatProvider? TestStringParseableWithFormatProvider { get; set; }
[CommandOption(nameof(ObjectArray))]
public object[]? ObjectArray { get; set; }
[CommandOption(nameof(StringArray))]
public string[]? StringArray { get; set; }
[CommandOption(nameof(IntArray))]
public int[]? IntArray { get; set; }
[CommandOption(nameof(CustomEnumArray))]
public CustomEnum[]? CustomEnumArray { get; set; }
[CommandOption(nameof(IntNullableArray))]
public int?[]? IntNullableArray { get; set; }
[CommandOption(nameof(TestStringConstructableArray))]
public StringConstructable[]? TestStringConstructableArray { get; set; }
[CommandOption(nameof(Enumerable))]
public IEnumerable? Enumerable { get; set; }
[CommandOption(nameof(StringEnumerable))]
public IEnumerable<string>? StringEnumerable { get; set; }
[CommandOption(nameof(StringReadOnlyList))]
public IReadOnlyList<string>? StringReadOnlyList { get; set; }
[CommandOption(nameof(StringList))]
public List<string>? StringList { get; set; }
[CommandOption(nameof(StringHashSet))]
public HashSet<string>? StringHashSet { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command]
private class ArrayOptionCommand : ICommand
{
[CommandOption("option", 'o')]
public IReadOnlyList<string>? Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command]
private class RequiredOptionCommand : ICommand
{
[CommandOption(nameof(Option), IsRequired = true)]
public string? Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command]
private class RequiredArrayOptionCommand : ICommand
{
[CommandOption(nameof(Option), IsRequired = true)]
public IReadOnlyList<string>? Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command]
private class ParametersCommand : ICommand
{
[CommandParameter(0)]
public string? ParameterA { get; set; }
[CommandParameter(1)]
public string? ParameterB { get; set; }
[CommandParameter(2)]
public IReadOnlyList<string>? 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<string>? Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command]
private class NoParameterCommand : ICommand
{
[CommandOption(nameof(OptionA))]
public string? OptionA { get; set; }
[CommandOption(nameof(OptionB))]
public string? OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}
}

View File

@@ -1,53 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace CliFx.Tests
{
public partial class ArgumentBindingSpecs
{
private enum CustomEnum
{
Value1 = 1,
Value2 = 2,
Value3 = 3
}
private class StringConstructable
{
public string Value { get; }
public StringConstructable(string value) => Value = value;
}
private class StringParseable
{
public string Value { get; }
private StringParseable(string value) => Value = value;
public static StringParseable Parse(string value) => new StringParseable(value);
}
private class StringParseableWithFormatProvider
{
public string Value { get; }
private StringParseableWithFormatProvider(string value) => Value = value;
public static StringParseableWithFormatProvider Parse(string value, IFormatProvider formatProvider) =>
new StringParseableWithFormatProvider(value + " " + formatProvider);
}
private class DummyType
{
}
public class CustomEnumerable<T> : IEnumerable<T>
{
public IEnumerator<T> GetEnumerator() => ((IEnumerable<T>) Array.Empty<T>()).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,378 +0,0 @@
using System;
using System.Collections.Generic;
using CliFx.Domain;
using CliFx.Tests.Internal;
using FluentAssertions;
using Xunit;
namespace CliFx.Tests
{
public class ArgumentSyntaxSpecs
{
[Fact]
public void Input_is_empty_if_no_arguments_are_provided()
{
// Arrange
var arguments = Array.Empty<string>();
var commandNames = Array.Empty<string>();
// Act
var input = CommandInput.Parse(arguments, commandNames);
// Assert
input.Should().BeEquivalentTo(CommandInput.Empty);
}
public static object[][] DirectivesTestData => new[]
{
new object[]
{
new[] {"[preview]"},
new CommandInputBuilder()
.AddDirective("preview")
.Build()
},
new object[]
{
new[] {"[preview]", "[debug]"},
new CommandInputBuilder()
.AddDirective("preview")
.AddDirective("debug")
.Build()
}
};
[Theory]
[MemberData(nameof(DirectivesTestData))]
internal void Directive_can_be_enabled_by_specifying_its_name_in_square_brackets(IReadOnlyList<string> arguments, CommandInput expectedInput)
{
// Arrange
var commandNames = Array.Empty<string>();
// Act
var input = CommandInput.Parse(arguments, commandNames);
// Assert
input.Should().BeEquivalentTo(expectedInput);
}
public static object[][] OptionsTestData => new[]
{
new object[]
{
new[] {"--option"},
new CommandInputBuilder()
.AddOption("option")
.Build()
},
new object[]
{
new[] {"--option", "value"},
new CommandInputBuilder()
.AddOption("option", "value")
.Build()
},
new object[]
{
new[] {"--option", "value1", "value2"},
new CommandInputBuilder()
.AddOption("option", "value1", "value2")
.Build()
},
new object[]
{
new[] {"--option", "same value"},
new CommandInputBuilder()
.AddOption("option", "same value")
.Build()
},
new object[]
{
new[] {"--option1", "--option2"},
new CommandInputBuilder()
.AddOption("option1")
.AddOption("option2")
.Build()
},
new object[]
{
new[] {"--option1", "value1", "--option2", "value2"},
new CommandInputBuilder()
.AddOption("option1", "value1")
.AddOption("option2", "value2")
.Build()
},
new object[]
{
new[] {"--option1", "value1", "value2", "--option2", "value3", "value4"},
new CommandInputBuilder()
.AddOption("option1", "value1", "value2")
.AddOption("option2", "value3", "value4")
.Build()
},
new object[]
{
new[] {"--option1", "value1", "value2", "--option2"},
new CommandInputBuilder()
.AddOption("option1", "value1", "value2")
.AddOption("option2")
.Build()
}
};
[Theory]
[MemberData(nameof(OptionsTestData))]
internal void Option_can_be_set_by_specifying_its_name_after_two_dashes(IReadOnlyList<string> arguments, CommandInput expectedInput)
{
// Arrange
var commandNames = Array.Empty<string>();
// Act
var input = CommandInput.Parse(arguments, commandNames);
// Assert
input.Should().BeEquivalentTo(expectedInput);
}
public static object[][] ShortOptionsTestData => new[]
{
new object[]
{
new[] {"-o"},
new CommandInputBuilder()
.AddOption("o")
.Build()
},
new object[]
{
new[] {"-o", "value"},
new CommandInputBuilder()
.AddOption("o", "value")
.Build()
},
new object[]
{
new[] {"-o", "value1", "value2"},
new CommandInputBuilder()
.AddOption("o", "value1", "value2")
.Build()
},
new object[]
{
new[] {"-o", "same value"},
new CommandInputBuilder()
.AddOption("o", "same value")
.Build()
},
new object[]
{
new[] {"-a", "-b"},
new CommandInputBuilder()
.AddOption("a")
.AddOption("b")
.Build()
},
new object[]
{
new[] {"-a", "value1", "-b", "value2"},
new CommandInputBuilder()
.AddOption("a", "value1")
.AddOption("b", "value2")
.Build()
},
new object[]
{
new[] {"-a", "value1", "value2", "-b", "value3", "value4"},
new CommandInputBuilder()
.AddOption("a", "value1", "value2")
.AddOption("b", "value3", "value4")
.Build()
},
new object[]
{
new[] {"-a", "value1", "value2", "-b"},
new CommandInputBuilder()
.AddOption("a", "value1", "value2")
.AddOption("b")
.Build()
},
new object[]
{
new[] {"-abc"},
new CommandInputBuilder()
.AddOption("a")
.AddOption("b")
.AddOption("c")
.Build()
},
new object[]
{
new[] {"-abc", "value"},
new CommandInputBuilder()
.AddOption("a")
.AddOption("b")
.AddOption("c", "value")
.Build()
},
new object[]
{
new[] {"-abc", "value1", "value2"},
new CommandInputBuilder()
.AddOption("a")
.AddOption("b")
.AddOption("c", "value1", "value2")
.Build()
}
};
[Theory]
[MemberData(nameof(ShortOptionsTestData))]
internal void Option_can_be_set_by_specifying_its_short_name_after_a_single_dash(IReadOnlyList<string> arguments, CommandInput expectedInput)
{
// Arrange
var commandNames = Array.Empty<string>();
// Act
var input = CommandInput.Parse(arguments, commandNames);
// Assert
input.Should().BeEquivalentTo(expectedInput);
}
public static object[][] ParametersTestData => new[]
{
new object[]
{
new[] {"foo"},
new CommandInputBuilder()
.AddParameter("foo")
.Build()
},
new object[]
{
new[] {"foo", "bar"},
new CommandInputBuilder()
.AddParameter("foo")
.AddParameter("bar")
.Build()
},
new object[]
{
new[] {"[preview]", "foo"},
new CommandInputBuilder()
.AddDirective("preview")
.AddParameter("foo")
.Build()
},
new object[]
{
new[] {"foo", "--option", "value", "-abc"},
new CommandInputBuilder()
.AddParameter("foo")
.AddOption("option", "value")
.AddOption("a")
.AddOption("b")
.AddOption("c")
.Build()
},
new object[]
{
new[] {"[preview]", "[debug]", "foo", "bar", "--option", "value", "-abc"},
new CommandInputBuilder()
.AddDirective("preview")
.AddDirective("debug")
.AddParameter("foo")
.AddParameter("bar")
.AddOption("option", "value")
.AddOption("a")
.AddOption("b")
.AddOption("c")
.Build()
}
};
[Theory]
[MemberData(nameof(ParametersTestData))]
internal void Parameter_can_be_set_by_specifying_the_value_directly(IReadOnlyList<string> arguments, CommandInput expectedInput)
{
// Arrange
var commandNames = Array.Empty<string>();
// Act
var input = CommandInput.Parse(arguments, commandNames);
// Assert
input.Should().BeEquivalentTo(expectedInput);
}
public static object[][] CommandNameTestData => new[]
{
new object[]
{
new[] {"cmd"},
new[] {"cmd"},
new CommandInputBuilder()
.SetCommandName("cmd")
.Build()
},
new object[]
{
new[] {"cmd"},
new[] {"cmd", "foo", "bar", "-o", "value"},
new CommandInputBuilder()
.SetCommandName("cmd")
.AddParameter("foo")
.AddParameter("bar")
.AddOption("o", "value")
.Build()
},
new object[]
{
new[] {"cmd", "cmd sub"},
new[] {"cmd", "sub", "foo"},
new CommandInputBuilder()
.SetCommandName("cmd sub")
.AddParameter("foo")
.Build()
}
};
[Theory]
[MemberData(nameof(CommandNameTestData))]
internal void Command_name_is_matched_from_arguments_that_come_before_parameters(
IReadOnlyList<string> commandNames,
IReadOnlyList<string> arguments,
CommandInput expectedInput)
{
// Act
var input = CommandInput.Parse(arguments, commandNames);
// Assert
input.Should().BeEquivalentTo(expectedInput);
}
}
}

View File

@@ -1,27 +0,0 @@
using System;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests
{
public partial class CancellationSpecs
{
[Command("cancel")]
private class CancellableCommand : ICommand
{
public async ValueTask ExecuteAsync(IConsole console)
{
try
{
await Task.Delay(TimeSpan.FromSeconds(3), console.GetCancellationToken());
console.Output.WriteLine("Never printed");
}
catch (OperationCanceledException)
{
console.Output.WriteLine("Cancellation requested");
throw;
}
}
}
}
}

View File

@@ -1,14 +1,14 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Tests.Commands;
using FluentAssertions;
using Xunit;
namespace CliFx.Tests
{
public partial class CancellationSpecs
public class CancellationSpecs
{
[Fact]
public async Task Command_can_perform_additional_cleanup_if_cancellation_is_requested()
@@ -22,22 +22,19 @@ namespace CliFx.Tests
var console = new VirtualConsole(output: stdOut, cancellationToken: cts.Token);
var application = new CliApplicationBuilder()
.AddCommand(typeof(CancellableCommand))
.AddCommand<CancellableCommand>()
.UseConsole(console)
.Build();
// Act
cts.CancelAfter(TimeSpan.FromSeconds(0.2));
var exitCode = await application.RunAsync(
new[] {"cancel"},
new Dictionary<string, string>());
var exitCode = await application.RunAsync(new[] {"cmd"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
exitCode.Should().NotBe(0);
stdOutData.Should().Be("Cancellation requested");
stdOutData.Should().Be(CancellableCommand.CancellationOutputText);
}
}
}

View File

@@ -19,6 +19,7 @@
<PackageReference Include="FluentAssertions" Version="5.10.2" />
<PackageReference Include="GitHubActionsTestLogger" Version="1.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.msbuild" Version="2.8.0" PrivateAssets="all" />

View File

@@ -0,0 +1,31 @@
using System;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class CancellableCommand : ICommand
{
public const string CompletionOutputText = "Finished";
public const string CancellationOutputText = "Canceled";
public async ValueTask ExecuteAsync(IConsole console)
{
try
{
await Task.Delay(
TimeSpan.FromSeconds(3),
console.GetCancellationToken()
);
console.Output.WriteLine(CompletionOutputText);
}
catch (OperationCanceledException)
{
console.Output.WriteLine(CancellationOutputText);
throw;
}
}
}
}

View File

@@ -0,0 +1,21 @@
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Exceptions;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class CommandExceptionCommand : ICommand
{
[CommandOption("code", 'c')]
public int ExitCode { get; set; } = 133;
[CommandOption("msg", 'm')]
public string? Message { get; set; }
[CommandOption("show-help")]
public bool ShowHelp { get; set; }
public ValueTask ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode, ShowHelp);
}
}

View File

@@ -0,0 +1,17 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command(Description = nameof(DefaultCommand))]
public class DefaultCommand : ICommand
{
public const string ExpectedOutputText = nameof(DefaultCommand);
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(ExpectedOutputText);
return default;
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class GenericExceptionCommand : ICommand
{
[CommandOption("msg", 'm')]
public string? Message { get; set; }
public ValueTask ExecuteAsync(IConsole console) => throw new Exception(Message);
}
}

View File

@@ -0,0 +1,11 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command("cmd")]
public class ConflictWithHelpOptionCommand : SelfSerializeCommandBase
{
[CommandOption("option-h", 'h')]
public string? OptionH { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
// Must be default because version option is available only on default commands
[Command]
public class ConflictWithVersionOptionCommand : SelfSerializeCommandBase
{
[CommandOption("version")]
public string? Version { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command("cmd")]
public class DuplicateOptionEnvironmentVariableNamesCommand : SelfSerializeCommandBase
{
[CommandOption("option-a", EnvironmentVariableName = "ENV_VAR")]
public string? OptionA { get; set; }
[CommandOption("option-b", EnvironmentVariableName = "ENV_VAR")]
public string? OptionB { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command("cmd")]
public class DuplicateOptionNamesCommand : SelfSerializeCommandBase
{
[CommandOption("fruits")]
public string? Apples { get; set; }
[CommandOption("fruits")]
public string? Oranges { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command("cmd")]
public class DuplicateOptionShortNamesCommand : SelfSerializeCommandBase
{
[CommandOption('x')]
public string? OptionA { get; set; }
[CommandOption('x')]
public string? OptionB { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command("cmd")]
public class DuplicateParameterNameCommand : SelfSerializeCommandBase
{
[CommandParameter(0, Name = "param")]
public string? ParamA { get; set; }
[CommandParameter(1, Name = "param")]
public string? ParamB { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command("cmd")]
public class DuplicateParameterOrderCommand : SelfSerializeCommandBase
{
[CommandParameter(13)]
public string? ParamA { get; set; }
[CommandParameter(13)]
public string? ParamB { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command("cmd")]
public class EmptyOptionNameCommand : SelfSerializeCommandBase
{
[CommandOption("")]
public string? Apples { get; set; }
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command("cmd")]
public class MultipleNonScalarParametersCommand : SelfSerializeCommandBase
{
[CommandParameter(0)]
public IReadOnlyList<string>? ParamA { get; set; }
[CommandParameter(1)]
public IReadOnlyList<string>? ParamB { get; set; }
}
}

View File

@@ -0,0 +1,6 @@
namespace CliFx.Tests.Commands.Invalid
{
public class NonAnnotatedCommand : SelfSerializeCommandBase
{
}
}

View File

@@ -0,0 +1,9 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command]
public class NonImplementedCommand
{
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command("cmd")]
public class NonLastNonScalarParameterCommand : SelfSerializeCommandBase
{
[CommandParameter(0)]
public IReadOnlyList<string>? ParamA { get; set; }
[CommandParameter(1)]
public string? ParamB { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command]
public class OtherDefaultCommand : SelfSerializeCommandBase
{
}
}

View File

@@ -0,0 +1,11 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command("cmd")]
public class SingleCharacterOptionNameCommand : SelfSerializeCommandBase
{
[CommandOption("a")]
public string? Apples { get; set; }
}
}

View File

@@ -0,0 +1,17 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("named", Description = nameof(NamedCommand))]
public class NamedCommand : ICommand
{
public const string ExpectedOutputText = nameof(NamedCommand);
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(ExpectedOutputText);
return default;
}
}
}

View File

@@ -0,0 +1,17 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("named sub", Description = nameof(NamedSubCommand))]
public class NamedSubCommand : ICommand
{
public const string ExpectedOutputText = nameof(NamedSubCommand);
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(ExpectedOutputText);
return default;
}
}
}

View File

@@ -0,0 +1,14 @@
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace CliFx.Tests.Commands
{
public abstract class SelfSerializeCommandBase : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(JsonConvert.SerializeObject(this));
return default;
}
}
}

View File

@@ -0,0 +1,155 @@
using System;
using System.Collections;
using System.Collections.Generic;
using CliFx.Attributes;
using Newtonsoft.Json;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public partial class SupportedArgumentTypesCommand : SelfSerializeCommandBase
{
[CommandOption("obj")]
public object? Object { get; set; } = 42;
[CommandOption("str")]
public string? String { get; set; } = "foo bar";
[CommandOption("bool")]
public bool Bool { get; set; }
[CommandOption("char")]
public char Char { get; set; }
[CommandOption("sbyte")]
public sbyte Sbyte { get; set; }
[CommandOption("byte")]
public byte Byte { get; set; }
[CommandOption("short")]
public short Short { get; set; }
[CommandOption("ushort")]
public ushort Ushort { get; set; }
[CommandOption("int")]
public int Int { get; set; }
[CommandOption("uint")]
public uint Uint { get; set; }
[CommandOption("long")]
public long Long { get; set; }
[CommandOption("ulong")]
public ulong Ulong { get; set; }
[CommandOption("float")]
public float Float { get; set; }
[CommandOption("double")]
public double Double { get; set; }
[CommandOption("decimal")]
public decimal Decimal { get; set; }
[CommandOption("datetime")]
public DateTime DateTime { get; set; }
[CommandOption("datetime-offset")]
public DateTimeOffset DateTimeOffset { get; set; }
[CommandOption("timespan")]
public TimeSpan TimeSpan { get; set; }
[CommandOption("enum")]
public CustomEnum Enum { get; set; }
[CommandOption("int-nullable")]
public int? IntNullable { get; set; }
[CommandOption("enum-nullable")]
public CustomEnum? EnumNullable { get; set; }
[CommandOption("timespan-nullable")]
public TimeSpan? TimeSpanNullable { get; set; }
[CommandOption("str-constructible")]
public CustomStringConstructible? StringConstructible { get; set; }
[CommandOption("str-parseable")]
public CustomStringParseable? StringParseable { get; set; }
[CommandOption("str-parseable-format")]
public CustomStringParseableWithFormatProvider? StringParseableWithFormatProvider { get; set; }
[CommandOption("obj-array")]
public object[]? ObjectArray { get; set; }
[CommandOption("str-array")]
public string[]? StringArray { get; set; }
[CommandOption("int-array")]
public int[]? IntArray { get; set; }
[CommandOption("enum-array")]
public CustomEnum[]? EnumArray { get; set; }
[CommandOption("int-nullable-array")]
public int?[]? IntNullableArray { get; set; }
[CommandOption("str-constructible-array")]
public CustomStringConstructible[]? StringConstructibleArray { get; set; }
[CommandOption("str-enumerable")]
public IEnumerable<string>? StringEnumerable { get; set; }
[CommandOption("str-read-only-list")]
public IReadOnlyList<string>? StringReadOnlyList { get; set; }
[CommandOption("str-list")]
public List<string>? StringList { get; set; }
[CommandOption("str-set")]
public HashSet<string>? StringHashSet { get; set; }
}
public partial class SupportedArgumentTypesCommand
{
public enum CustomEnum
{
Value1 = 1,
Value2 = 2,
Value3 = 3
}
public class CustomStringConstructible
{
public string Value { get; }
public CustomStringConstructible(string value) => Value = value;
}
public class CustomStringParseable
{
public string Value { get; }
[JsonConstructor]
private CustomStringParseable(string value) => Value = value;
public static CustomStringParseable Parse(string value) => new CustomStringParseable(value);
}
public class CustomStringParseableWithFormatProvider
{
public string Value { get; }
[JsonConstructor]
private CustomStringParseableWithFormatProvider(string value) => Value = value;
public static CustomStringParseableWithFormatProvider Parse(string value, IFormatProvider formatProvider) =>
new CustomStringParseableWithFormatProvider(value + " " + formatProvider);
}
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Collections;
using System.Collections.Generic;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public partial class UnsupportedArgumentTypesCommand : SelfSerializeCommandBase
{
[CommandOption("str-non-initializable")]
public CustomType? StringNonInitializable { get; set; }
[CommandOption("str-enumerable-non-initializable")]
public CustomEnumerable<string>? StringEnumerableNonInitializable { get; set; }
}
public partial class UnsupportedArgumentTypesCommand
{
public class CustomType
{
}
public class CustomEnumerable<T> : IEnumerable<T>
{
public IEnumerator<T> GetEnumerator() => ((IEnumerable<T>) Array.Empty<T>()).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class WithDefaultValuesCommand : SelfSerializeCommandBase
{
public enum CustomEnum { Value1, Value2, Value3 };
[CommandOption("obj")]
public object? Object { get; set; } = 42;
[CommandOption("str")]
public string? String { get; set; } = "foo";
[CommandOption("str-empty")]
public string StringEmpty { get; set; } = "";
[CommandOption("str-array")]
public string[]? StringArray { get; set; } = { "foo", "bar", "baz" };
[CommandOption("bool")]
public bool Bool { get; set; } = true;
[CommandOption("char")]
public char Char { get; set; } = 't';
[CommandOption("int")]
public int Int { get; set; } = 1337;
[CommandOption("int-nullable")]
public int? IntNullable { get; set; } = 1337;
[CommandOption("int-array")]
public int[]? IntArray { get; set; } = { 1, 2, 3 };
[CommandOption("timespan")]
public TimeSpan TimeSpan { get; set; } = TimeSpan.FromMinutes(123);
[CommandOption("enum")]
public CustomEnum Enum { get; set; } = CustomEnum.Value2;
}
}

View File

@@ -0,0 +1,28 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class WithDependenciesCommand : ICommand
{
public class DependencyA
{
}
public class DependencyB
{
}
private readonly DependencyA _dependencyA;
private readonly DependencyB _dependencyB;
public WithDependenciesCommand(DependencyA dependencyA, DependencyB dependencyB)
{
_dependencyA = dependencyA;
_dependencyB = dependencyB;
}
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -0,0 +1,19 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class WithEnumArgumentsCommand : SelfSerializeCommandBase
{
public enum CustomEnum { Value1, Value2, Value3 };
[CommandParameter(0, Name = "enum")]
public CustomEnum EnumParameter { get; set; }
[CommandOption("enum")]
public CustomEnum? EnumOption { get; set; }
[CommandOption("required-enum", IsRequired = true)]
public CustomEnum RequiredEnumOption { get; set; }
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class WithEnvironmentVariablesCommand : SelfSerializeCommandBase
{
[CommandOption("opt-a", 'a', EnvironmentVariableName = "ENV_OPT_A")]
public string? OptA { get; set; }
[CommandOption("opt-b", 'b', EnvironmentVariableName = "ENV_OPT_B")]
public IReadOnlyList<string>? OptB { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class WithParametersCommand : SelfSerializeCommandBase
{
[CommandParameter(0)]
public string? ParamA { get; set; }
[CommandParameter(1)]
public int? ParamB { get; set; }
[CommandParameter(2)]
public IReadOnlyList<string>? ParamC { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class WithRequiredOptionsCommand : SelfSerializeCommandBase
{
[CommandOption("opt-a", 'a', IsRequired = true)]
public string? OptA { get; set; }
[CommandOption("opt-b", 'b')]
public int? OptB { get; set; }
[CommandOption("opt-c", 'c', IsRequired = true)]
public IReadOnlyList<char>? OptC { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class WithSingleParameterCommand : SelfSerializeCommandBase
{
[CommandParameter(0)]
public string? ParamA { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class WithSingleRequiredOptionCommand : SelfSerializeCommandBase
{
[CommandOption("opt-a")]
public string? OptA { get; set; }
[CommandOption("opt-b", IsRequired = true)]
public string? OptB { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
using System.Collections.Generic;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class WithStringArrayOptionCommand : SelfSerializeCommandBase
{
[CommandOption("opt", 'o')]
public IReadOnlyList<string>? Opt { get; set; }
}
}

View File

@@ -38,7 +38,8 @@ namespace CliFx.Tests
var console = new VirtualConsole(
input: stdIn,
output: stdOut,
error: stdErr);
error: stdErr
);
// Act
console.Output.Write("output");

View File

@@ -1,37 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests
{
public partial class DependencyInjectionSpecs
{
[Command]
private class WithoutDependenciesCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
private class DependencyA
{
}
private class DependencyB
{
}
[Command]
private class WithDependenciesCommand : ICommand
{
private readonly DependencyA _dependencyA;
private readonly DependencyB _dependencyB;
public WithDependenciesCommand(DependencyA dependencyA, DependencyB dependencyB)
{
_dependencyA = dependencyA;
_dependencyB = dependencyB;
}
public ValueTask ExecuteAsync(IConsole console) => default;
}
}
}

View File

@@ -1,11 +1,12 @@
using CliFx.Exceptions;
using CliFx.Tests.Commands;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;
namespace CliFx.Tests
{
public partial class DependencyInjectionSpecs
public class DependencyInjectionSpecs
{
private readonly ITestOutputHelper _output;
@@ -18,10 +19,10 @@ namespace CliFx.Tests
var activator = new DefaultTypeActivator();
// Act
var obj = activator.CreateInstance(typeof(WithoutDependenciesCommand));
var obj = activator.CreateInstance(typeof(DefaultCommand));
// Assert
obj.Should().BeOfType<WithoutDependenciesCommand>();
obj.Should().BeOfType<DefaultCommand>();
}
[Fact]
@@ -40,7 +41,10 @@ namespace CliFx.Tests
{
// Arrange
var activator = new DelegateTypeActivator(_ =>
new WithDependenciesCommand(new DependencyA(), new DependencyB()));
new WithDependenciesCommand(
new WithDependenciesCommand.DependencyA(),
new WithDependenciesCommand.DependencyB())
);
// Act
var obj = activator.CreateInstance(typeof(WithDependenciesCommand));

View File

@@ -1,14 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests
{
public partial class DirectivesSpecs
{
[Command("cmd")]
private class NamedCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
}
}

View File

@@ -1,13 +1,19 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using CliFx.Tests.Commands;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;
namespace CliFx.Tests
{
public partial class DirectivesSpecs
public class DirectivesSpecs
{
private readonly ITestOutputHelper _output;
public DirectivesSpecs(ITestOutputHelper output) => _output = output;
[Fact]
public async Task Preview_directive_can_be_specified_to_print_provided_arguments_as_they_were_parsed()
{
@@ -16,21 +22,23 @@ namespace CliFx.Tests
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(NamedCommand))
.AddCommand<NamedCommand>()
.UseConsole(console)
.AllowPreviewMode()
.Build();
// Act
var exitCode = await application.RunAsync(
new[] {"[preview]", "cmd", "param", "-abc", "--option", "foo"},
new[] {"[preview]", "named", "param", "-abc", "--option", "foo"},
new Dictionary<string, string>());
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
exitCode.Should().Be(0);
stdOutData.Should().ContainAll("cmd", "<param>", "[-a]", "[-b]", "[-c]", "[--option \"foo\"]");
stdOutData.Should().ContainAll("named", "<param>", "[-a]", "[-b]", "[-c]", "[--option \"foo\"]");
_output.WriteLine(stdOutData);
}
}
}

View File

@@ -1,27 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests
{
public partial class EnvironmentVariablesSpecs
{
[Command]
private class EnvironmentVariableCollectionCommand : ICommand
{
[CommandOption("opt", EnvironmentVariableName = "ENV_OPT")]
public IReadOnlyList<string>? 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;
}
}
}

View File

@@ -1,20 +1,20 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using CliFx.Domain;
using CliFx.Tests.Internal;
using CliFx.Tests.Commands;
using CliWrap;
using CliWrap.Buffered;
using FluentAssertions;
using Newtonsoft.Json;
using Xunit;
namespace CliFx.Tests
{
public partial class EnvironmentVariablesSpecs
public class EnvironmentVariablesSpecs
{
// This test uses a real application to make sure environment variables are actually read correctly
[Fact]
public async Task Option_can_use_a_specific_environment_variable_as_fallback()
public async Task Option_can_use_an_environment_variable_as_fallback()
{
// Arrange
var command = Cli.Wrap("dotnet")
@@ -32,7 +32,7 @@ namespace CliFx.Tests
// This test uses a real application to make sure environment variables are actually read correctly
[Fact]
public async Task Option_only_uses_environment_variable_as_fallback_if_the_value_was_not_directly_provided()
public async Task Option_only_uses_an_environment_variable_as_fallback_if_the_value_is_not_directly_provided()
{
// Arrange
var command = Cli.Wrap("dotnet")
@@ -51,65 +51,97 @@ namespace CliFx.Tests
}
[Fact]
public void Option_of_non_scalar_type_can_take_multiple_separated_values_from_an_environment_variable()
public async Task Option_only_uses_an_environment_variable_as_fallback_if_the_name_matches_case_sensitively()
{
// Arrange
var input = CommandInput.Empty;
var envVars = new Dictionary<string, string>
{
["ENV_OPT"] = $"foo{Path.PathSeparator}bar"
};
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand<WithEnvironmentVariablesCommand>()
.UseConsole(console)
.Build();
// Act
var instance = CommandHelper.ResolveCommand<EnvironmentVariableCollectionCommand>(input, envVars);
var exitCode = await application.RunAsync(
new[] {"cmd"},
new Dictionary<string, string>
{
["ENV_opt_A"] = "incorrect",
["ENV_OPT_A"] = "correct"
}
);
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
var commandInstance = JsonConvert.DeserializeObject<WithEnvironmentVariablesCommand>(stdOutData);
// Assert
instance.Should().BeEquivalentTo(new EnvironmentVariableCollectionCommand
exitCode.Should().Be(0);
commandInstance.Should().BeEquivalentTo(new WithEnvironmentVariablesCommand
{
Option = new[] {"foo", "bar"}
OptA = "correct"
});
}
[Fact]
public void Option_of_scalar_type_can_only_take_a_single_value_from_an_environment_variable_even_if_it_contains_separators()
public async Task Option_of_non_scalar_type_can_use_an_environment_variable_as_fallback_and_extract_multiple_values()
{
// Arrange
var input = CommandInput.Empty;
var envVars = new Dictionary<string, string>
{
["ENV_OPT"] = $"foo{Path.PathSeparator}bar"
};
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand<WithEnvironmentVariablesCommand>()
.UseConsole(console)
.Build();
// Act
var instance = CommandHelper.ResolveCommand<EnvironmentVariableCommand>(input, envVars);
var exitCode = await application.RunAsync(
new[] {"cmd"},
new Dictionary<string, string>
{
["ENV_OPT_B"] = $"foo{Path.PathSeparator}bar"
}
);
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
var commandInstance = JsonConvert.DeserializeObject<WithEnvironmentVariablesCommand>(stdOutData);
// Assert
instance.Should().BeEquivalentTo(new EnvironmentVariableCommand
exitCode.Should().Be(0);
commandInstance.Should().BeEquivalentTo(new WithEnvironmentVariablesCommand
{
Option = $"foo{Path.PathSeparator}bar"
OptB = new[] {"foo", "bar"}
});
}
[Fact]
public void Option_can_use_a_specific_environment_variable_as_fallback_while_respecting_case()
public async Task Option_of_scalar_type_can_use_an_environment_variable_as_fallback_regardless_of_separators()
{
// Arrange
const string expected = "foobar";
var input = CommandInput.Empty;
var envVars = new Dictionary<string, string>
{
["ENV_OPT"] = expected,
["env_opt"] = "2"
};
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand<WithEnvironmentVariablesCommand>()
.UseConsole(console)
.Build();
// Act
var instance = CommandHelper.ResolveCommand<EnvironmentVariableCommand>(input, envVars);
var exitCode = await application.RunAsync(
new[] {"cmd"},
new Dictionary<string, string>
{
["ENV_OPT_A"] = $"foo{Path.PathSeparator}bar"
}
);
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
var commandInstance = JsonConvert.DeserializeObject<WithEnvironmentVariablesCommand>(stdOutData);
// Assert
instance.Should().BeEquivalentTo(new EnvironmentVariableCommand
exitCode.Should().Be(0);
commandInstance.Should().BeEquivalentTo(new WithEnvironmentVariablesCommand
{
Option = expected
OptA = $"foo{Path.PathSeparator}bar"
});
}
}
}
}

View File

@@ -1,34 +0,0 @@
using System;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Exceptions;
namespace CliFx.Tests
{
public partial class ErrorReportingSpecs
{
[Command("exc")]
private class GenericExceptionCommand : ICommand
{
[CommandOption("msg", 'm')]
public string? Message { get; set; }
public ValueTask ExecuteAsync(IConsole console) => throw new Exception(Message);
}
[Command("exc")]
private class CommandExceptionCommand : ICommand
{
[CommandOption("code", 'c')]
public int ExitCode { get; set; } = 133;
[CommandOption("msg", 'm')]
public string? Message { get; set; }
[CommandOption("show-help")]
public bool ShowHelp { get; set; }
public ValueTask ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode, ShowHelp);
}
}
}

View File

@@ -1,13 +1,13 @@
using System.Collections.Generic;
using System.IO;
using System.IO;
using System.Threading.Tasks;
using CliFx.Tests.Commands;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;
namespace CliFx.Tests
{
public partial class ErrorReportingSpecs
public class ErrorReportingSpecs
{
private readonly ITestOutputHelper _output;
@@ -17,28 +17,32 @@ namespace CliFx.Tests
public async Task Command_may_throw_a_generic_exception_which_exits_and_prints_error_message_and_stack_trace()
{
// Arrange
await using var stdOut = new MemoryStream();
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(error: stdErr);
var console = new VirtualConsole(output: stdOut, error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand(typeof(GenericExceptionCommand))
.AddCommand<GenericExceptionCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] {"exc", "-m", "Kaput"},
new Dictionary<string, string>());
var exitCode = await application.RunAsync(new[] {"cmd", "-m", "Kaput"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().NotBe(0);
stdOutData.Should().BeEmpty();
stdErrData.Should().ContainAll(
"System.Exception:",
"Kaput", "at",
"CliFx.Tests");
"CliFx.Tests"
);
_output.WriteLine(stdOutData);
_output.WriteLine(stdErrData);
}
@@ -46,25 +50,28 @@ namespace CliFx.Tests
public async Task Command_may_throw_a_specialized_exception_which_exits_with_custom_code_and_prints_minimal_error_details()
{
// Arrange
await using var stdOut = new MemoryStream();
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(error: stdErr);
var console = new VirtualConsole(output: stdOut, error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand(typeof(CommandExceptionCommand))
.AddCommand<CommandExceptionCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] {"exc", "-m", "Kaput", "-c", "69"},
new Dictionary<string, string>());
var exitCode = await application.RunAsync(new[] {"cmd", "-m", "Kaput", "-c", "69"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().Be(69);
stdOutData.Should().BeEmpty();
stdErrData.Should().Be("Kaput");
_output.WriteLine(stdOutData);
_output.WriteLine(stdErrData);
}
@@ -72,28 +79,32 @@ namespace CliFx.Tests
public async Task Command_may_throw_a_specialized_exception_without_error_message_which_exits_and_prints_full_error_details()
{
// Arrange
await using var stdOut = new MemoryStream();
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(error: stdErr);
var console = new VirtualConsole(output: stdOut, error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand(typeof(CommandExceptionCommand))
.AddCommand<CommandExceptionCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] {"exc"},
new Dictionary<string, string>());
var exitCode = await application.RunAsync(new[] {"cmd"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().NotBe(0);
stdOutData.Should().BeEmpty();
stdErrData.Should().ContainAll(
"CliFx.Exceptions.CommandException:",
"at",
"CliFx.Tests");
"CliFx.Tests"
);
_output.WriteLine(stdOutData);
_output.WriteLine(stdErrData);
}
@@ -107,30 +118,27 @@ namespace CliFx.Tests
var console = new VirtualConsole(output: stdOut, error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand(typeof(CommandExceptionCommand))
.AddCommand<CommandExceptionCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] {"exc", "-m", "Kaput", "--show-help"},
new Dictionary<string, string>());
var exitCode = await application.RunAsync(new[] {"cmd", "-m", "Kaput", "--show-help"});
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().NotBe(0);
stdErrData.Should().Be("Kaput");
stdOutData.Should().ContainAll(
"Usage",
"Options",
"-h|--help", "Shows help text."
"-h|--help"
);
stdErrData.Should().Be("Kaput");
_output.WriteLine(stdErrData);
_output.WriteLine(stdOutData);
_output.WriteLine(stdErrData);
}
[Fact]
@@ -139,34 +147,31 @@ namespace CliFx.Tests
// Arrange
await using var stdOut = new MemoryStream();
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(output: stdOut, error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand(typeof(CommandExceptionCommand))
.AddCommand<DefaultCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] {"not-a-valid-command", "-r", "foo"},
new Dictionary<string, string>());
var exitCode = await application.RunAsync(new[] {"not-a-valid-command", "-r", "foo"});
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().NotBe(0);
stdErrData.Should().NotBeNullOrWhiteSpace();
stdOutData.Should().ContainAll(
"Usage",
"[command]",
"Options",
"-h|--help", "Shows help text."
"-h|--help"
);
stdErrData.Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErrData);
_output.WriteLine(stdOutData);
_output.WriteLine(stdErrData);
}
}
}

View File

@@ -1,155 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests
{
public partial class HelpTextSpecs
{
[Command(Description = "DefaultCommand description.")]
private class DefaultCommand : ICommand
{
[CommandOption("option-a", 'a', Description = "OptionA description.")]
public string? OptionA { get; set; }
[CommandOption("option-b", 'b', Description = "OptionB description.")]
public string? OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command("cmd", Description = "NamedCommand description.")]
private class NamedCommand : ICommand
{
[CommandParameter(0, Name = "param-a", Description = "ParameterA description.")]
public string? ParameterA { get; set; }
[CommandOption("option-c", 'c', Description = "OptionC description.")]
public string? OptionC { get; set; }
[CommandOption("option-d", 'd', Description = "OptionD description.")]
public string? OptionD { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command("cmd sub", Description = "NamedSubCommand description.")]
private class NamedSubCommand : ICommand
{
[CommandParameter(0, Name = "param-b", Description = "ParameterB description.")]
public string? ParameterB { get; set; }
[CommandParameter(1, Name = "param-c", Description = "ParameterC description.")]
public string? ParameterC { get; set; }
[CommandOption("option-e", 'e', Description = "OptionE description.")]
public string? OptionE { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command("cmd-with-params")]
private class ParametersCommand : ICommand
{
[CommandParameter(0, Name = "first")]
public string? ParameterA { get; set; }
[CommandParameter(10)]
public int? ParameterB { get; set; }
[CommandParameter(20, Description = "A list of numbers", Name = "third list")]
public IEnumerable<int>? ParameterC { get; set; }
[CommandOption("option", 'o')]
public string? Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command("cmd-with-req-opts")]
private class RequiredOptionsCommand : ICommand
{
[CommandOption("option-a", 'a', IsRequired = true)]
public string? OptionA { get; set; }
[CommandOption("option-b", 'b', IsRequired = true)]
public IEnumerable<int>? OptionB { get; set; }
[CommandOption("option-c", 'c')]
public string? OptionC { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command("cmd-with-enum-args")]
private class EnumArgumentsCommand : ICommand
{
public enum CustomEnum { Value1, Value2, Value3 };
[CommandParameter(0, Name = "value", Description = "Enum parameter.")]
public CustomEnum ParamA { get; set; }
[CommandOption("value", Description = "Enum option.", IsRequired = true)]
public CustomEnum OptionA { get; set; } = CustomEnum.Value1;
[CommandOption("nullable-value", Description = "Nullable enum option.")]
public CustomEnum? OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command("cmd-with-env-vars")]
private class EnvironmentVariableCommand : ICommand
{
[CommandOption("option-a", 'a', IsRequired = true, EnvironmentVariableName = "ENV_OPT_A")]
public string? OptionA { get; set; }
[CommandOption("option-b", 'b', EnvironmentVariableName = "ENV_OPT_B")]
public string? OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command("cmd-with-defaults")]
private class ArgumentsWithDefaultValuesCommand : ICommand
{
public enum CustomEnum { Value1, Value2, Value3 };
[CommandOption(nameof(Object))]
public object? Object { get; set; } = 42;
[CommandOption(nameof(String))]
public string? String { get; set; } = "foo";
[CommandOption(nameof(EmptyString))]
public string EmptyString { get; set; } = "";
[CommandOption(nameof(Bool))]
public bool Bool { get; set; } = true;
[CommandOption(nameof(Char))]
public char Char { get; set; } = 't';
[CommandOption(nameof(Int))]
public int Int { get; set; } = 1337;
[CommandOption(nameof(TimeSpan))]
public TimeSpan TimeSpan { get; set; } = TimeSpan.FromMinutes(123);
[CommandOption(nameof(Enum))]
public CustomEnum Enum { get; set; } = CustomEnum.Value2;
[CommandOption(nameof(IntNullable))]
public int? IntNullable { get; set; } = 1337;
[CommandOption(nameof(StringArray))]
public string[]? StringArray { get; set; } = { "foo", "bar", "baz" };
[CommandOption(nameof(IntArray))]
public int[]? IntArray { get; set; } = { 1, 2, 3 };
public ValueTask ExecuteAsync(IConsole console) => default;
}
}
}

View File

@@ -1,189 +1,18 @@
using System.IO;
using System.Threading.Tasks;
using CliFx.Tests.Commands;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;
namespace CliFx.Tests
{
public partial class HelpTextSpecs
public class HelpTextSpecs
{
private readonly ITestOutputHelper _output;
public HelpTextSpecs(ITestOutputHelper output) => _output = output;
[Fact]
public async Task Version_information_can_be_requested_by_providing_the_version_option_without_other_arguments()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(DefaultCommand))
.AddCommand(typeof(NamedCommand))
.AddCommand(typeof(NamedSubCommand))
.UseVersionText("v6.9")
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(new[] {"--version"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
exitCode.Should().Be(0);
stdOutData.Should().Be("v6.9");
_output.WriteLine(stdOutData);
}
[Fact]
public async Task Help_text_can_be_requested_by_providing_the_help_option()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(DefaultCommand))
.AddCommand(typeof(NamedCommand))
.AddCommand(typeof(NamedSubCommand))
.UseTitle("AppTitle")
.UseVersionText("AppVer")
.UseDescription("AppDesc")
.UseExecutableName("AppExe")
.UseConsole(console)
.Build();
// Act
await application.RunAsync(new[] {"--help"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
stdOutData.Should().ContainAll(
"AppTitle", "AppVer",
"AppDesc",
"Usage",
"AppExe", "[command]", "[options]",
"Options",
"-a|--option-a", "OptionA description.",
"-b|--option-b", "OptionB description.",
"-h|--help", "Shows help text.",
"--version", "Shows version information.",
"Commands",
"cmd", "NamedCommand description.",
"You can run", "to show help on a specific command."
);
_output.WriteLine(stdOutData);
}
[Fact]
public async Task Help_text_can_be_requested_on_a_specific_named_command()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(DefaultCommand))
.AddCommand(typeof(NamedCommand))
.AddCommand(typeof(NamedSubCommand))
.UseConsole(console)
.Build();
// Act
await application.RunAsync(new[] {"cmd", "--help"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
stdOutData.Should().ContainAll(
"Description",
"NamedCommand description.",
"Usage",
"cmd", "[command]", "<param-a>", "[options]",
"Parameters",
"* param-a", "ParameterA description.",
"Options",
"-c|--option-c", "OptionC description.",
"-d|--option-d", "OptionD description.",
"-h|--help", "Shows help text.",
"Commands",
"sub", "SubCommand description.",
"You can run", "to show help on a specific command."
);
_output.WriteLine(stdOutData);
}
[Fact]
public async Task Help_text_can_be_requested_on_a_specific_named_sub_command()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(DefaultCommand))
.AddCommand(typeof(NamedCommand))
.AddCommand(typeof(NamedSubCommand))
.UseConsole(console)
.Build();
// Act
await application.RunAsync(new[] {"cmd", "sub", "--help"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
stdOutData.Should().ContainAll(
"Description",
"SubCommand description.",
"Usage",
"cmd sub", "<param-b>", "<param-c>", "[options]",
"Parameters",
"* param-b", "ParameterB description.",
"* param-c", "ParameterC description.",
"Options",
"-e|--option-e", "OptionE description.",
"-h|--help", "Shows help text."
);
_output.WriteLine(stdOutData);
}
[Fact]
public async Task Help_text_can_be_requested_without_specifying_command_even_if_default_command_is_not_defined()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(NamedCommand))
.AddCommand(typeof(NamedSubCommand))
.UseConsole(console)
.Build();
// Act
await application.RunAsync(new[] {"--help"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
stdOutData.Should().ContainAll(
"Usage",
"[command]",
"Options",
"-h|--help", "Shows help text.",
"--version", "Shows version information.",
"Commands",
"cmd", "NamedCommand description.",
"You can run", "to show help on a specific command."
);
_output.WriteLine(stdOutData);
}
[Fact]
public async Task Help_text_shows_usage_format_which_lists_all_parameters()
{
@@ -192,18 +21,19 @@ namespace CliFx.Tests
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(ParametersCommand))
.AddCommand<WithParametersCommand>()
.UseConsole(console)
.Build();
// Act
await application.RunAsync(new[] {"cmd-with-params", "--help"});
var exitCode = await application.RunAsync(new[] {"cmd", "--help"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
exitCode.Should().Be(0);
stdOutData.Should().ContainAll(
"Usage",
"cmd-with-params", "<first>", "<parameterb>", "<third list...>", "[options]"
"cmd", "<parama>", "<paramb>", "<paramc...>"
);
_output.WriteLine(stdOutData);
@@ -217,78 +47,79 @@ namespace CliFx.Tests
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(RequiredOptionsCommand))
.AddCommand<WithRequiredOptionsCommand>()
.UseConsole(console)
.Build();
// Act
await application.RunAsync(new[] {"cmd-with-req-opts", "--help"});
var exitCode = await application.RunAsync(new[] {"cmd", "--help"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
exitCode.Should().Be(0);
stdOutData.Should().ContainAll(
"Usage",
"cmd-with-req-opts", "--option-a <value>", "--option-b <values...>", "[options]",
"cmd", "--opt-a <value>", "--opt-c <values...>", "[options]",
"Options",
"* -a|--option-a",
"* -b|--option-b",
"-c|--option-c"
"* -a|--opt-a",
"-b|--opt-b",
"* -c|--opt-c"
);
_output.WriteLine(stdOutData);
}
[Fact]
public async Task Help_text_lists_all_valid_values_for_enum_arguments()
public async Task Help_text_shows_all_valid_values_for_enum_arguments()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(EnumArgumentsCommand))
.AddCommand<WithEnumArgumentsCommand>()
.UseConsole(console)
.Build();
// Act
await application.RunAsync(new[] {"cmd-with-enum-args", "--help"});
var exitCode = await application.RunAsync(new[] {"cmd", "--help"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
exitCode.Should().Be(0);
stdOutData.Should().ContainAll(
"Usage",
"cmd-with-enum-args", "[options]",
"Parameters",
"value", "Valid values: \"Value1\", \"Value2\", \"Value3\".",
"enum", "Valid values: \"Value1\", \"Value2\", \"Value3\".",
"Options",
"* --value", "Enum option.", "Valid values: \"Value1\", \"Value2\", \"Value3\".",
"--nullable-value", "Nullable enum option.", "Valid values: \"Value1\", \"Value2\", \"Value3\"."
"--enum", "Valid values: \"Value1\", \"Value2\", \"Value3\".",
"* --required-enum", "Valid values: \"Value1\", \"Value2\", \"Value3\"."
);
_output.WriteLine(stdOutData);
}
[Fact]
public async Task Help_text_lists_environment_variable_names_for_options_that_have_them_defined()
public async Task Help_text_shows_environment_variable_names_for_options_that_have_them_defined()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(EnvironmentVariableCommand))
.AddCommand<WithEnvironmentVariablesCommand>()
.UseConsole(console)
.Build();
// Act
await application.RunAsync(new[] {"cmd-with-env-vars", "--help"});
var exitCode = await application.RunAsync(new[] {"cmd", "--help"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
exitCode.Should().Be(0);
stdOutData.Should().ContainAll(
"Options",
"* -a|--option-a", "Environment variable:", "ENV_OPT_A",
"-b|--option-b", "Environment variable:", "ENV_OPT_B"
"-a|--opt-a", "Environment variable:", "ENV_OPT_A",
"-b|--opt-b", "Environment variable:", "ENV_OPT_B"
);
_output.WriteLine(stdOutData);
@@ -302,30 +133,29 @@ namespace CliFx.Tests
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(ArgumentsWithDefaultValuesCommand))
.AddCommand<WithDefaultValuesCommand>()
.UseConsole(console)
.Build();
// Act
await application.RunAsync(new[] {"cmd-with-defaults", "--help"});
var exitCode = await application.RunAsync(new[] {"cmd", "--help"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
exitCode.Should().Be(0);
stdOutData.Should().ContainAll(
"Usage",
"cmd-with-defaults", "[options]",
"Options",
"--Object", "Default: \"42\"",
"--String", "Default: \"foo\"",
"--EmptyString", "Default: \"\"",
"--Bool", "Default: \"True\"",
"--Char", "Default: \"t\"",
"--Int", "Default: \"1337\"",
"--TimeSpan", "Default: \"02:03:00\"",
"--Enum", "Default: \"Value2\"",
"--IntNullable", "Default: \"1337\"",
"--StringArray", "Default: \"foo\" \"bar\" \"baz\"",
"--IntArray", "Default: \"1\" \"2\" \"3\""
"--obj", "Default: \"42\"",
"--str", "Default: \"foo\"",
"--str-empty", "Default: \"\"",
"--str-array", "Default: \"foo\" \"bar\" \"baz\"",
"--bool", "Default: \"True\"",
"--char", "Default: \"t\"",
"--int", "Default: \"1337\"",
"--int-nullable", "Default: \"1337\"",
"--int-array", "Default: \"1\" \"2\" \"3\"",
"--timespan", "Default: \"02:03:00\"",
"--enum", "Default: \"Value2\""
);
_output.WriteLine(stdOutData);

View File

@@ -1,23 +0,0 @@
using System.Collections.Generic;
using CliFx.Domain;
namespace CliFx.Tests.Internal
{
internal static class CommandHelper
{
public static TCommand ResolveCommand<TCommand>(CommandInput input, IReadOnlyDictionary<string, string> environmentVariables)
where TCommand : ICommand, new()
{
var schema = CommandSchema.TryResolve(typeof(TCommand))!;
var instance = new TCommand();
schema.Bind(instance, input, environmentVariables);
return instance;
}
public static TCommand ResolveCommand<TCommand>(CommandInput input)
where TCommand : ICommand, new() =>
ResolveCommand<TCommand>(input, new Dictionary<string, string>());
}
}

View File

@@ -1,45 +0,0 @@
using System.Collections.Generic;
using CliFx.Domain;
namespace CliFx.Tests.Internal
{
internal class CommandInputBuilder
{
private readonly List<CommandDirectiveInput> _directives = new List<CommandDirectiveInput>();
private readonly List<CommandParameterInput> _parameters = new List<CommandParameterInput>();
private readonly List<CommandOptionInput> _options = new List<CommandOptionInput>();
private string? _commandName;
public CommandInputBuilder SetCommandName(string commandName)
{
_commandName = commandName;
return this;
}
public CommandInputBuilder AddDirective(string directive)
{
_directives.Add(new CommandDirectiveInput(directive));
return this;
}
public CommandInputBuilder AddParameter(string parameter)
{
_parameters.Add(new CommandParameterInput(parameter));
return this;
}
public CommandInputBuilder AddOption(string alias, params string[] values)
{
_options.Add(new CommandOptionInput(alias, values));
return this;
}
public CommandInput Build() => new CommandInput(
_directives,
_commandName,
_parameters,
_options
);
}
}

View File

@@ -1,19 +0,0 @@
using System;
using System.Threading.Tasks;
namespace CliFx.Tests.Internal
{
internal static class TaskExtensions
{
public static async Task IgnoreCancellation(this Task task)
{
try
{
await task;
}
catch (OperationCanceledException)
{
}
}
}
}

View File

@@ -1,52 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests
{
public partial class RoutingSpecs
{
[Command]
private class DefaultCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine("Hello world!");
return default;
}
}
[Command("concat", Description = "Concatenate strings.")]
private class ConcatCommand : ICommand
{
[CommandOption('i', IsRequired = true, Description = "Input strings.")]
public IReadOnlyList<string> Inputs { get; set; } = Array.Empty<string>();
[CommandOption('s', Description = "String separator.")]
public string Separator { get; set; } = "";
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(string.Join(Separator, Inputs));
return default;
}
}
[Command("div", Description = "Divide one number by another.")]
private class DivideCommand : ICommand
{
[CommandOption("dividend", 'D', IsRequired = true, Description = "The number to divide.")]
public double Dividend { get; set; } = 0;
[CommandOption("divisor", 'd', IsRequired = true, Description = "The number to divide by.")]
public double Divisor { get; set; } = 0;
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(Dividend / Divisor);
return default;
}
}
}
}

View File

@@ -1,14 +1,19 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using CliFx.Tests.Commands;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;
namespace CliFx.Tests
{
public partial class RoutingSpecs
public class RoutingSpecs
{
private readonly ITestOutputHelper _output;
public RoutingSpecs(ITestOutputHelper testOutput) => _output = testOutput;
[Fact]
public async Task Default_command_is_executed_if_provided_arguments_do_not_match_any_named_command()
{
@@ -17,48 +22,21 @@ namespace CliFx.Tests
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(DefaultCommand))
.AddCommand(typeof(ConcatCommand))
.AddCommand(typeof(DivideCommand))
.AddCommand<DefaultCommand>()
.AddCommand<NamedCommand>()
.AddCommand<NamedSubCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(
Array.Empty<string>(),
new Dictionary<string, string>());
var exitCode = await application.RunAsync(Array.Empty<string>());
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
exitCode.Should().Be(0);
stdOutData.Should().Be("Hello world!");
}
stdOutData.Should().Be(DefaultCommand.ExpectedOutputText);
[Fact]
public async Task Help_text_is_printed_if_no_arguments_were_provided_and_default_command_is_not_defined()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(ConcatCommand))
.AddCommand(typeof(DivideCommand))
.UseConsole(console)
.UseDescription("This will be visible in help")
.Build();
// Act
var exitCode = await application.RunAsync(
Array.Empty<string>(),
new Dictionary<string, string>());
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
exitCode.Should().Be(0);
stdOutData.Should().Contain("This will be visible in help");
_output.WriteLine(stdOutData);
}
[Fact]
@@ -69,22 +47,208 @@ namespace CliFx.Tests
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(DefaultCommand))
.AddCommand(typeof(ConcatCommand))
.AddCommand(typeof(DivideCommand))
.AddCommand<DefaultCommand>()
.AddCommand<NamedCommand>()
.AddCommand<NamedSubCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] {"concat", "-i", "foo", "bar", "-s", ", "},
new Dictionary<string, string>());
var exitCode = await application.RunAsync(new[] {"named"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
exitCode.Should().Be(0);
stdOutData.Should().Be("foo, bar");
stdOutData.Should().Be(NamedCommand.ExpectedOutputText);
_output.WriteLine(stdOutData);
}
[Fact]
public async Task Specific_named_sub_command_is_executed_if_provided_arguments_match_its_name()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand<DefaultCommand>()
.AddCommand<NamedCommand>()
.AddCommand<NamedSubCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(new[] {"named", "sub"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
exitCode.Should().Be(0);
stdOutData.Should().Be(NamedSubCommand.ExpectedOutputText);
_output.WriteLine(stdOutData);
}
[Fact]
public async Task Help_text_is_printed_if_no_arguments_were_provided_and_default_command_is_not_defined()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand<NamedCommand>()
.AddCommand<NamedSubCommand>()
.UseConsole(console)
.UseDescription("This will be visible in help")
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
exitCode.Should().Be(0);
stdOutData.Should().Contain("This will be visible in help");
_output.WriteLine(stdOutData);
}
[Fact]
public async Task Help_text_is_printed_if_provided_arguments_contain_the_help_option()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand<DefaultCommand>()
.AddCommand<NamedCommand>()
.AddCommand<NamedSubCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(new[] {"--help"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
exitCode.Should().Be(0);
stdOutData.Should().ContainAll(
nameof(DefaultCommand),
"Usage"
);
_output.WriteLine(stdOutData);
}
[Fact]
public async Task Help_text_is_printed_if_provided_arguments_contain_the_help_option_even_if_default_command_is_not_defined()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand<NamedCommand>()
.AddCommand<NamedSubCommand>()
.UseDescription("This will be visible in help")
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(new[] {"--help"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
exitCode.Should().Be(0);
stdOutData.Should().Contain("This will be visible in help");
_output.WriteLine(stdOutData);
}
[Fact]
public async Task Help_text_for_a_specific_named_command_is_printed_if_provided_arguments_match_its_name_and_contain_the_help_option()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand<DefaultCommand>()
.AddCommand<NamedCommand>()
.AddCommand<NamedSubCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(new[] {"named", "--help"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
exitCode.Should().Be(0);
stdOutData.Should().ContainAll(
nameof(NamedCommand),
"Usage",
"named"
);
_output.WriteLine(stdOutData);
}
[Fact]
public async Task Help_text_for_a_specific_named_sub_command_is_printed_if_provided_arguments_match_its_name_and_contain_the_help_option()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand<DefaultCommand>()
.AddCommand<NamedCommand>()
.AddCommand<NamedSubCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(new[] {"named", "sub", "--help"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
exitCode.Should().Be(0);
stdOutData.Should().ContainAll(
nameof(NamedSubCommand),
"Usage",
"named", "sub"
);
_output.WriteLine(stdOutData);
}
[Fact]
public async Task Version_is_printed_if_the_only_provided_argument_is_the_version_option()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand<DefaultCommand>()
.AddCommand<NamedCommand>()
.AddCommand<NamedSubCommand>()
.UseVersionText("v6.9")
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(new[] {"--version"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
exitCode.Should().Be(0);
stdOutData.Should().Be("v6.9");
_output.WriteLine(stdOutData);
}
}
}

View File

@@ -34,6 +34,12 @@ namespace CliFx
return this;
}
/// <summary>
/// Adds a command of specified type to the application.
/// </summary>
public CliApplicationBuilder AddCommand<TCommand>() where TCommand : ICommand =>
AddCommand(typeof(TCommand));
/// <summary>
/// Adds multiple commands to the application.
/// </summary>

View File

@@ -23,15 +23,6 @@
<Nullable>annotations</Nullable>
</PropertyGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>$(AssemblyName).Tests</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>$(AssemblyName).Analyzers</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="all" />

View File

@@ -48,7 +48,7 @@ namespace CliFx.Domain
? ConvertScalar(value, nullableUnderlyingType)
: null;
// String-constructable
// String-constructible
var stringConstructor = targetType.GetConstructor(new[] {typeof(string)});
if (stringConstructor != null)
return stringConstructor.Invoke(new object[] {value!});
@@ -83,7 +83,7 @@ namespace CliFx.Domain
if (targetEnumerableType.IsAssignableFrom(arrayType))
return array;
// Constructable from an array
// Constructible from an array
var arrayConstructor = targetEnumerableType.GetConstructor(new[] {arrayType});
if (arrayConstructor != null)
return arrayConstructor.Invoke(new object[] {array});

View File

@@ -573,7 +573,7 @@ public async Task ConcatCommand_Test()
var console = new VirtualConsole(output: stdOut);
var app = new CliApplicationBuilder()
.AddCommand(typeof(ConcatCommand))
.AddCommand<ConcatCommand>()
.UseConsole(console)
.Build();