Rework tests from 1-to-1 mapping into specifications (#46)

This commit is contained in:
Alexey Golub
2020-03-16 01:03:03 +02:00
committed by GitHub
parent 79e1a2e3d7
commit 57f168723b
80 changed files with 3235 additions and 2718 deletions

View File

@@ -0,0 +1,23 @@
using System;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.Dummy.Commands
{
[Command("console-test")]
public class ConsoleTestCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
var input = console.Input.ReadToEnd();
console.WithColors(ConsoleColor.Black, ConsoleColor.White, () =>
{
console.Output.WriteLine(input);
console.Error.WriteLine(input);
});
return default;
}
}
}

View File

@@ -1,8 +1,16 @@
using System.Threading.Tasks;
using System.Reflection;
using System.Threading.Tasks;
namespace CliFx.Tests.Dummy
{
public class Program
public static partial class Program
{
public static Assembly Assembly { get; } = typeof(Program).Assembly;
public static string Location { get; } = Assembly.Location;
}
public static partial class Program
{
public static async Task Main() =>
await new CliApplicationBuilder()

View File

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

View File

@@ -0,0 +1,197 @@
using System;
using System.IO;
using CliFx.Domain;
using CliFx.Exceptions;
using FluentAssertions;
using Xunit;
namespace CliFx.Tests
{
public partial class ApplicationSpecs
{
[Fact]
public void Application_can_be_created_with_a_default_configuration()
{
// Act
var app = new CliApplicationBuilder()
.AddCommandsFromThisAssembly()
.Build();
// Assert
app.Should().NotBeNull();
}
[Fact]
public void Application_can_be_created_with_a_custom_configuration()
{
// Act
var app = new CliApplicationBuilder()
.AddCommand(typeof(ValidCommand))
.AddCommandsFrom(typeof(ValidCommand).Assembly)
.AddCommands(new[] {typeof(ValidCommand)})
.AddCommandsFrom(new[] {typeof(ValidCommand).Assembly})
.AddCommandsFromThisAssembly()
.AllowDebugMode()
.AllowPreviewMode()
.UseTitle("test")
.UseExecutableName("test")
.UseVersionText("test")
.UseDescription("test")
.UseConsole(new VirtualConsole(Stream.Null))
.UseTypeActivator(Activator.CreateInstance)
.Build();
// Assert
app.Should().NotBeNull();
}
[Fact]
public void At_least_one_command_must_be_defined_in_an_application()
{
// Arrange
var commandTypes = Array.Empty<Type>();
// Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
}
[Fact]
public void Commands_must_implement_the_corresponding_interface()
{
// Arrange
var commandTypes = new[] {typeof(NonImplementedCommand)};
// Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
}
[Fact]
public void Commands_must_be_annotated_by_an_attribute()
{
// Arrange
var commandTypes = new[] {typeof(NonAnnotatedCommand)};
// Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
}
[Fact]
public void Commands_must_have_unique_names()
{
// Arrange
var commandTypes = new[] {typeof(DuplicateNameCommandA), typeof(DuplicateNameCommandB)};
// Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
}
[Fact]
public void Command_parameters_must_have_unique_order()
{
// Arrange
var commandTypes = new[] {typeof(DuplicateParameterOrderCommand)};
// Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
}
[Fact]
public void Command_parameters_must_have_unique_names()
{
// Arrange
var commandTypes = new[] {typeof(DuplicateParameterNameCommand)};
// Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
}
[Fact]
public void Command_parameter_can_be_non_scalar_only_if_no_other_such_parameter_is_present()
{
// Arrange
var commandTypes = new[] {typeof(MultipleNonScalarParametersCommand)};
// Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
}
[Fact]
public void Command_parameter_can_be_non_scalar_only_if_it_is_the_last_in_order()
{
// Arrange
var commandTypes = new[] {typeof(NonLastNonScalarParameterCommand)};
// Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
}
[Fact]
public void Command_options_must_have_unique_names()
{
// Arrange
var commandTypes = new[] {typeof(DuplicateOptionNamesCommand)};
// Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
}
[Fact]
public void Command_options_must_have_unique_short_names()
{
// Arrange
var commandTypes = new[] {typeof(DuplicateOptionShortNamesCommand)};
// Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
}
[Fact]
public void Command_options_must_have_unique_environment_variable_names()
{
// Arrange
var commandTypes = new[] {typeof(DuplicateOptionEnvironmentVariableNamesCommand)};
// Act & assert
Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
}
[Fact]
public void Command_options_and_parameters_must_be_annotated_by_corresponding_attributes()
{
// Arrange
var commandTypes = new[] {typeof(HiddenPropertiesCommand)};
// Act
var schema = ApplicationSchema.Resolve(commandTypes);
// Assert
schema.Should().BeEquivalentTo(new ApplicationSchema(new[]
{
new CommandSchema(
typeof(HiddenPropertiesCommand),
"hidden",
"Description",
new[]
{
new CommandParameterSchema(
typeof(HiddenPropertiesCommand).GetProperty(nameof(HiddenPropertiesCommand.Parameter)),
13,
"param",
"Param description")
},
new[]
{
new CommandOptionSchema(
typeof(HiddenPropertiesCommand).GetProperty(nameof(HiddenPropertiesCommand.Option)),
"option",
'o',
"ENV",
false,
"Option description")
})
}));
schema.ToString().Should().NotBeNullOrWhiteSpace(); // this is only for coverage, I'm sorry
}
}
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,315 @@
using System;
using CliFx.Domain;
using FluentAssertions;
using Xunit;
namespace CliFx.Tests
{
public class ArgumentSyntaxSpecs
{
[Fact]
public void Input_is_empty_if_no_arguments_are_provided()
{
// Arrange
var args = Array.Empty<string>();
// Act
var input = CommandLineInput.Parse(args);
// Assert
input.Should().BeEquivalentTo(CommandLineInput.Empty);
}
public static object[][] DirectivesTestData => new[]
{
new object[]
{
new[] {"[preview]"},
new CommandLineInputBuilder()
.AddDirective("preview")
.Build()
},
new object[]
{
new[] {"[preview]", "[debug]"},
new CommandLineInputBuilder()
.AddDirective("preview")
.AddDirective("debug")
.Build()
}
};
[Theory]
[MemberData(nameof(DirectivesTestData))]
internal void Directive_can_be_enabled_by_specifying_its_name_in_square_brackets(string[] arguments, CommandLineInput expectedInput)
{
// Act
var input = CommandLineInput.Parse(arguments);
// Assert
input.Should().BeEquivalentTo(expectedInput);
}
public static object[][] OptionsTestData => new[]
{
new object[]
{
new[] {"--option"},
new CommandLineInputBuilder()
.AddOption("option")
.Build()
},
new object[]
{
new[] {"--option", "value"},
new CommandLineInputBuilder()
.AddOption("option", "value")
.Build()
},
new object[]
{
new[] {"--option", "value1", "value2"},
new CommandLineInputBuilder()
.AddOption("option", "value1", "value2")
.Build()
},
new object[]
{
new[] {"--option", "same value"},
new CommandLineInputBuilder()
.AddOption("option", "same value")
.Build()
},
new object[]
{
new[] {"--option1", "--option2"},
new CommandLineInputBuilder()
.AddOption("option1")
.AddOption("option2")
.Build()
},
new object[]
{
new[] {"--option1", "value1", "--option2", "value2"},
new CommandLineInputBuilder()
.AddOption("option1", "value1")
.AddOption("option2", "value2")
.Build()
},
new object[]
{
new[] {"--option1", "value1", "value2", "--option2", "value3", "value4"},
new CommandLineInputBuilder()
.AddOption("option1", "value1", "value2")
.AddOption("option2", "value3", "value4")
.Build()
},
new object[]
{
new[] {"--option1", "value1", "value2", "--option2"},
new CommandLineInputBuilder()
.AddOption("option1", "value1", "value2")
.AddOption("option2")
.Build()
}
};
[Theory]
[MemberData(nameof(OptionsTestData))]
internal void Option_can_be_set_by_specifying_its_name_after_two_dashes(string[] arguments, CommandLineInput expectedInput)
{
// Act
var input = CommandLineInput.Parse(arguments);
// Assert
input.Should().BeEquivalentTo(expectedInput);
}
public static object[][] ShortOptionsTestData => new[]
{
new object[]
{
new[] {"-o"},
new CommandLineInputBuilder()
.AddOption("o")
.Build()
},
new object[]
{
new[] {"-o", "value"},
new CommandLineInputBuilder()
.AddOption("o", "value")
.Build()
},
new object[]
{
new[] {"-o", "value1", "value2"},
new CommandLineInputBuilder()
.AddOption("o", "value1", "value2")
.Build()
},
new object[]
{
new[] {"-o", "same value"},
new CommandLineInputBuilder()
.AddOption("o", "same value")
.Build()
},
new object[]
{
new[] {"-a", "-b"},
new CommandLineInputBuilder()
.AddOption("a")
.AddOption("b")
.Build()
},
new object[]
{
new[] {"-a", "value1", "-b", "value2"},
new CommandLineInputBuilder()
.AddOption("a", "value1")
.AddOption("b", "value2")
.Build()
},
new object[]
{
new[] {"-a", "value1", "value2", "-b", "value3", "value4"},
new CommandLineInputBuilder()
.AddOption("a", "value1", "value2")
.AddOption("b", "value3", "value4")
.Build()
},
new object[]
{
new[] {"-a", "value1", "value2", "-b"},
new CommandLineInputBuilder()
.AddOption("a", "value1", "value2")
.AddOption("b")
.Build()
},
new object[]
{
new[] {"-abc"},
new CommandLineInputBuilder()
.AddOption("a")
.AddOption("b")
.AddOption("c")
.Build()
},
new object[]
{
new[] {"-abc", "value"},
new CommandLineInputBuilder()
.AddOption("a")
.AddOption("b")
.AddOption("c", "value")
.Build()
},
new object[]
{
new[] {"-abc", "value1", "value2"},
new CommandLineInputBuilder()
.AddOption("a")
.AddOption("b")
.AddOption("c", "value1", "value2")
.Build()
}
};
[Theory]
[MemberData(nameof(ShortOptionsTestData))]
internal void Option_can_be_set_by_specifying_its_short_name_after_a_single_dash(string[] arguments, CommandLineInput expectedInput)
{
// Act
var input = CommandLineInput.Parse(arguments);
// Assert
input.Should().BeEquivalentTo(expectedInput);
}
public static object[][] UnboundArgumentsTestData => new[]
{
new object[]
{
new[] {"foo"},
new CommandLineInputBuilder()
.AddUnboundArgument("foo")
.Build()
},
new object[]
{
new[] {"foo", "bar"},
new CommandLineInputBuilder()
.AddUnboundArgument("foo")
.AddUnboundArgument("bar")
.Build()
},
new object[]
{
new[] {"[preview]", "foo"},
new CommandLineInputBuilder()
.AddDirective("preview")
.AddUnboundArgument("foo")
.Build()
},
new object[]
{
new[] {"foo", "--option", "value", "-abc"},
new CommandLineInputBuilder()
.AddUnboundArgument("foo")
.AddOption("option", "value")
.AddOption("a")
.AddOption("b")
.AddOption("c")
.Build()
},
new object[]
{
new[] {"[preview]", "[debug]", "foo", "bar", "--option", "value", "-abc"},
new CommandLineInputBuilder()
.AddDirective("preview")
.AddDirective("debug")
.AddUnboundArgument("foo")
.AddUnboundArgument("bar")
.AddOption("option", "value")
.AddOption("a")
.AddOption("b")
.AddOption("c")
.Build()
}
};
[Theory]
[MemberData(nameof(UnboundArgumentsTestData))]
internal void Any_remaining_arguments_are_treated_as_unbound_arguments(string[] arguments, CommandLineInput expectedInput)
{
// Act
var input = CommandLineInput.Parse(arguments);
// Assert
input.Should().BeEquivalentTo(expectedInput);
}
}
}

View File

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

View File

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

View File

@@ -1,44 +0,0 @@
using NUnit.Framework;
using System;
using CliFx.Tests.TestCommands;
namespace CliFx.Tests
{
[TestFixture]
public class CliApplicationBuilderTests
{
[Test(Description = "All builder methods must return without exceptions")]
public void Smoke_Test()
{
// Arrange
var builder = new CliApplicationBuilder();
// Act
builder
.AddCommand(typeof(HelloWorldDefaultCommand))
.AddCommandsFrom(typeof(HelloWorldDefaultCommand).Assembly)
.AddCommands(new[] {typeof(HelloWorldDefaultCommand)})
.AddCommandsFrom(new[] {typeof(HelloWorldDefaultCommand).Assembly})
.AddCommandsFromThisAssembly()
.AllowDebugMode()
.AllowPreviewMode()
.UseTitle("test")
.UseExecutableName("test")
.UseVersionText("test")
.UseDescription("test")
.UseConsole(new VirtualConsole())
.UseTypeActivator(Activator.CreateInstance)
.Build();
}
[Test(Description = "Builder must be able to produce an application when no parameters are specified")]
public void Build_Test()
{
// Arrange
var builder = new CliApplicationBuilder();
// Act
builder.Build();
}
}
}

View File

@@ -1,445 +0,0 @@
using FluentAssertions;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Tests.TestCommands;
namespace CliFx.Tests
{
[TestFixture]
public class CliApplicationTests
{
private const string TestAppName = "TestApp";
private const string TestVersionText = "v1.0";
private static IEnumerable<TestCaseData> GetTestCases_RunAsync()
{
yield return new TestCaseData(
new[] {typeof(HelloWorldDefaultCommand)},
new string[0],
new Dictionary<string, string>(),
"Hello world."
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new[] {"concat", "-i", "foo", "-i", "bar", "-s", " "},
new Dictionary<string, string>(),
"foo bar"
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new[] {"concat", "-i", "one", "two", "three", "-s", ", "},
new Dictionary<string, string>(),
"one, two, three"
);
yield return new TestCaseData(
new[] {typeof(DivideCommand)},
new[] {"div", "-D", "24", "-d", "8"},
new Dictionary<string, string>(),
"3"
);
yield return new TestCaseData(
new[] {typeof(HelloWorldDefaultCommand)},
new[] {"--version"},
new Dictionary<string, string>(),
TestVersionText
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new[] {"--version"},
new Dictionary<string, string>(),
TestVersionText
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new string[0],
new Dictionary<string, string>(),
null
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new[] {"-h"},
new Dictionary<string, string>(),
null
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new[] {"--help"},
new Dictionary<string, string>(),
null
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new[] {"concat", "-h"},
new Dictionary<string, string>(),
null
);
yield return new TestCaseData(
new[] {typeof(ExceptionCommand)},
new[] {"exc", "-h"},
new Dictionary<string, string>(),
null
);
yield return new TestCaseData(
new[] {typeof(CommandExceptionCommand)},
new[] {"exc", "-h"},
new Dictionary<string, string>(),
null
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new[] {"[preview]"},
new Dictionary<string, string>(),
null
);
yield return new TestCaseData(
new[] {typeof(ExceptionCommand)},
new[] {"[preview]", "exc"},
new Dictionary<string, string>(),
null
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new[] {"[preview]", "concat", "-o", "value"},
new Dictionary<string, string>(),
null
);
}
private static IEnumerable<TestCaseData> GetTestCases_RunAsync_Negative()
{
yield return new TestCaseData(
new Type[0],
new string[0],
new Dictionary<string, string>(),
null, null
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new[] {"non-existing"},
new Dictionary<string, string>(),
null, null
);
yield return new TestCaseData(
new[] {typeof(ExceptionCommand)},
new[] {"exc"},
new Dictionary<string, string>(),
null, null
);
yield return new TestCaseData(
new[] {typeof(CommandExceptionCommand)},
new[] {"exc"},
new Dictionary<string, string>(),
null, null
);
yield return new TestCaseData(
new[] {typeof(CommandExceptionCommand)},
new[] {"exc"},
new Dictionary<string, string>(),
null, null
);
yield return new TestCaseData(
new[] {typeof(CommandExceptionCommand)},
new[] {"exc", "-m", "foo bar"},
new Dictionary<string, string>(),
"foo bar", null
);
yield return new TestCaseData(
new[] {typeof(CommandExceptionCommand)},
new[] {"exc", "-m", "foo bar", "-c", "666"},
new Dictionary<string, string>(),
"foo bar", 666
);
}
private static IEnumerable<TestCaseData> GetTestCases_RunAsync_Help()
{
yield return new TestCaseData(
new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)},
new[] {"--help"},
new[]
{
TestVersionText,
"Description",
"HelpDefaultCommand description.",
"Usage",
TestAppName, "[command]", "[options]",
"Options",
"-a|--option-a", "OptionA description.",
"-b|--option-b", "OptionB description.",
"-h|--help", "Shows help text.",
"--version", "Shows version information.",
"Commands",
"cmd", "HelpNamedCommand description.",
"You can run", "to show help on a specific command."
}
);
yield return new TestCaseData(
new[] {typeof(HelpSubCommand)},
new[] {"--help"},
new[]
{
TestVersionText,
"Usage",
TestAppName, "[command]",
"Options",
"-h|--help", "Shows help text.",
"--version", "Shows version information.",
"Commands",
"cmd sub", "HelpSubCommand description.",
"You can run", "to show help on a specific command."
}
);
yield return new TestCaseData(
new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)},
new[] {"cmd", "--help"},
new[]
{
"Description",
"HelpNamedCommand description.",
"Usage",
TestAppName, "cmd", "[command]", "[options]",
"Options",
"-c|--option-c", "OptionC description.",
"-d|--option-d", "OptionD description.",
"-h|--help", "Shows help text.",
"Commands",
"sub", "HelpSubCommand description.",
"You can run", "to show help on a specific command."
}
);
yield return new TestCaseData(
new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)},
new[] {"cmd", "sub", "--help"},
new[]
{
"Description",
"HelpSubCommand description.",
"Usage",
TestAppName, "cmd sub", "[options]",
"Options",
"-e|--option-e", "OptionE description.",
"-h|--help", "Shows help text."
}
);
yield return new TestCaseData(
new[] {typeof(ParameterCommand)},
new[] {"param", "cmd", "--help"},
new[]
{
"Description",
"Command using positional parameters",
"Usage",
TestAppName, "param cmd", "<first>", "<parameterb>", "<third list...>", "[options]",
"Parameters",
"* first",
"* parameterb",
"* third list", "A list of numbers",
"Options",
"-o|--option",
"-h|--help", "Shows help text."
}
);
yield return new TestCaseData(
new[] {typeof(AllRequiredOptionsCommand)},
new[] {"allrequired", "--help"},
new[]
{
"Description",
"AllRequiredOptionsCommand description.",
"Usage",
TestAppName, "allrequired --option-f <value> --option-g <value>"
}
);
yield return new TestCaseData(
new[] {typeof(SomeRequiredOptionsCommand)},
new[] {"somerequired", "--help"},
new[]
{
"Description",
"SomeRequiredOptionsCommand description.",
"Usage",
TestAppName, "somerequired --option-f <value> [options]"
}
);
yield return new TestCaseData(
new[] {typeof(EnvironmentVariableCommand)},
new[] {"--help"},
new[]
{
"Environment variable:", "ENV_SINGLE_VALUE"
}
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new[] {"concat", "--help"},
new[]
{
"Usage",
TestAppName, "concat", "-i", "<values...>", "[options]",
}
);
}
[TestCaseSource(nameof(GetTestCases_RunAsync))]
public async Task RunAsync_Test(
IReadOnlyList<Type> commandTypes,
IReadOnlyList<string> commandLineArguments,
IReadOnlyDictionary<string, string> environmentVariables,
string? expectedStdOut = null)
{
// Arrange
using var console = new VirtualConsole();
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
.UseTitle(TestAppName)
.UseExecutableName(TestAppName)
.UseVersionText(TestVersionText)
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
var stdOut = console.ReadOutputString().Trim();
// Assert
exitCode.Should().Be(0);
stdOut.Should().NotBeNullOrWhiteSpace();
if (expectedStdOut != null)
stdOut.Should().Be(expectedStdOut);
Console.WriteLine(stdOut);
}
[TestCaseSource(nameof(GetTestCases_RunAsync_Negative))]
public async Task RunAsync_Negative_Test(
IReadOnlyList<Type> commandTypes,
IReadOnlyList<string> commandLineArguments,
IReadOnlyDictionary<string, string> environmentVariables,
string? expectedStdErr = null,
int? expectedExitCode = null)
{
// Arrange
using var console = new VirtualConsole();
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
.UseTitle(TestAppName)
.UseExecutableName(TestAppName)
.UseVersionText(TestVersionText)
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
var stdErr = console.ReadErrorString().Trim();
// Assert
exitCode.Should().NotBe(0);
stdErr.Should().NotBeNullOrWhiteSpace();
if (expectedExitCode != null)
exitCode.Should().Be(expectedExitCode);
if (expectedStdErr != null)
stdErr.Should().Be(expectedStdErr);
Console.WriteLine(stdErr);
}
[TestCaseSource(nameof(GetTestCases_RunAsync_Help))]
public async Task RunAsync_Help_Test(
IReadOnlyList<Type> commandTypes,
IReadOnlyList<string> commandLineArguments,
IReadOnlyList<string>? expectedSubstrings = null)
{
// Arrange
using var console = new VirtualConsole();
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
.UseTitle(TestAppName)
.UseExecutableName(TestAppName)
.UseVersionText(TestVersionText)
.UseConsole(console)
.Build();
var environmentVariables = new Dictionary<string, string>();
// Act
var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
var stdOut = console.ReadOutputString().Trim();
// Assert
exitCode.Should().Be(0);
stdOut.Should().NotBeNullOrWhiteSpace();
if (expectedSubstrings != null)
stdOut.Should().ContainAll(expectedSubstrings);
Console.WriteLine(stdOut);
}
[Test]
public async Task RunAsync_Cancellation_Test()
{
// Arrange
using var console = new VirtualConsole();
var application = new CliApplicationBuilder()
.AddCommand(typeof(CancellableCommand))
.UseConsole(console)
.Build();
var commandLineArguments = new[] {"cancel"};
var environmentVariables = new Dictionary<string, string>();
// Act
console.CancelAfter(TimeSpan.FromSeconds(0.2));
var exitCode = await application.RunAsync(commandLineArguments, environmentVariables);
var stdOut = console.ReadOutputString().Trim();
var stdErr = console.ReadErrorString().Trim();
// Assert
exitCode.Should().NotBe(0);
stdOut.Should().BeNullOrWhiteSpace();
stdErr.Should().NotBeNullOrWhiteSpace();
Console.WriteLine(stdErr);
}
}
}

View File

@@ -11,12 +11,16 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CliWrap" Version="2.5.0" />
<PackageReference Include="FluentAssertions" Version="5.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1" />
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CliWrap" Version="3.0.0" />
<PackageReference Include="FluentAssertions" Version="5.10.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="coverlet.msbuild" Version="2.8.0" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>
<ItemGroup>
@@ -24,8 +28,12 @@
<ProjectReference Include="..\CliFx\CliFx.csproj" />
</ItemGroup>
<Target Name="Copy dummy's runtime config" AfterTargets="AfterBuild">
<Copy SourceFiles="../CliFx.Tests.Dummy/bin/$(Configuration)/$(TargetFramework)/CliFx.Tests.Dummy.runtimeconfig.json" DestinationFiles="$(OutputPath)CliFx.Tests.Dummy.runtimeconfig.json" />
</Target>
<ItemGroup>
<None Include="../CliFx.Tests.Dummy/bin/$(Configuration)/$(TargetFramework)/CliFx.Tests.Dummy.runtimeconfig.json">
<Link>CliFx.Tests.Dummy.runtimeconfig.json</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Visible>False</Visible>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,72 @@
using System;
using System.IO;
using System.Threading.Tasks;
using CliWrap;
using CliWrap.Buffered;
using FluentAssertions;
using Xunit;
namespace CliFx.Tests
{
public class ConsoleSpecs
{
[Fact]
public async Task Real_implementation_of_console_maps_directly_to_system_console()
{
// Arrange
var command = "Hello world" | Cli.Wrap("dotnet")
.WithArguments(a => a
.Add(Dummy.Program.Location)
.Add("console-test"));
// Act
var result = await command.ExecuteBufferedAsync();
// Assert
result.StandardOutput.TrimEnd().Should().Be("Hello world");
result.StandardError.TrimEnd().Should().Be("Hello world");
}
[Fact]
public void Fake_implementation_of_console_can_be_used_to_execute_commands_in_isolation()
{
// Arrange
using var stdIn = new MemoryStream(Console.InputEncoding.GetBytes("input"));
using var stdOut = new MemoryStream();
using var stdErr = new MemoryStream();
var console = new VirtualConsole(
input: stdIn,
output: stdOut,
error: stdErr);
// Act
console.Output.Write("output");
console.Error.Write("error");
var stdInData = console.Input.ReadToEnd();
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray());
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray());
console.ResetColor();
console.ForegroundColor = ConsoleColor.DarkMagenta;
console.BackgroundColor = ConsoleColor.DarkMagenta;
// Assert
stdInData.Should().Be("input");
stdOutData.Should().Be("output");
stdErrData.Should().Be("error");
console.Input.Should().NotBeSameAs(Console.In);
console.Output.Should().NotBeSameAs(Console.Out);
console.Error.Should().NotBeSameAs(Console.Error);
console.IsInputRedirected.Should().BeTrue();
console.IsOutputRedirected.Should().BeTrue();
console.IsErrorRedirected.Should().BeTrue();
console.ForegroundColor.Should().NotBe(Console.ForegroundColor);
console.BackgroundColor.Should().NotBe(Console.BackgroundColor);
}
}
}

View File

@@ -1,48 +0,0 @@
using System;
using System.Collections.Generic;
using CliFx.Exceptions;
using CliFx.Tests.TestCommands;
using CliFx.Tests.TestCustomTypes;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests
{
[TestFixture]
public class DefaultCommandFactoryTests
{
private static IEnumerable<TestCaseData> GetTestCases_CreateInstance()
{
yield return new TestCaseData(typeof(HelloWorldDefaultCommand));
}
private static IEnumerable<TestCaseData> GetTestCases_CreateInstance_Negative()
{
yield return new TestCaseData(typeof(TestNonStringParseable));
}
[TestCaseSource(nameof(GetTestCases_CreateInstance))]
public void CreateInstance_Test(Type type)
{
// Arrange
var activator = new DefaultTypeActivator();
// Act
var obj = activator.CreateInstance(type);
// Assert
obj.Should().BeOfType(type);
}
[TestCaseSource(nameof(GetTestCases_CreateInstance_Negative))]
public void CreateInstance_Negative_Test(Type type)
{
// Arrange
var activator = new DefaultTypeActivator();
// Act & Assert
var ex = Assert.Throws<CliFxException>(() => activator.CreateInstance(type));
Console.WriteLine(ex.Message);
}
}
}

View File

@@ -1,53 +0,0 @@
using System;
using System.Collections.Generic;
using CliFx.Exceptions;
using CliFx.Tests.TestCommands;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests
{
[TestFixture]
public class DelegateCommandFactoryTests
{
private static IEnumerable<TestCaseData> GetTestCases_CreateInstance()
{
yield return new TestCaseData(
new Func<Type, object>(Activator.CreateInstance),
typeof(HelloWorldDefaultCommand)
);
}
private static IEnumerable<TestCaseData> GetTestCases_CreateInstance_Negative()
{
yield return new TestCaseData(
new Func<Type, object>(_ => null),
typeof(HelloWorldDefaultCommand)
);
}
[TestCaseSource(nameof(GetTestCases_CreateInstance))]
public void CreateInstance_Test(Func<Type, object> activatorFunc, Type type)
{
// Arrange
var activator = new DelegateTypeActivator(activatorFunc);
// Act
var obj = activator.CreateInstance(type);
// Assert
obj.Should().BeOfType(type);
}
[TestCaseSource(nameof(GetTestCases_CreateInstance_Negative))]
public void CreateInstance_Negative_Test(Func<Type, object> activatorFunc, Type type)
{
// Arrange
var activator = new DelegateTypeActivator(activatorFunc);
// Act & Assert
var ex = Assert.Throws<CliFxException>(() => activator.CreateInstance(type));
Console.WriteLine(ex.Message);
}
}
}

View File

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

View File

@@ -0,0 +1,58 @@
using CliFx.Exceptions;
using FluentAssertions;
using Xunit;
namespace CliFx.Tests
{
public partial class DependencyInjectionSpecs
{
[Fact]
public void Default_type_activator_can_initialize_a_command_if_it_has_a_parameterless_constructor()
{
// Arrange
var activator = new DefaultTypeActivator();
// Act
var obj = activator.CreateInstance(typeof(WithoutDependenciesCommand));
// Assert
obj.Should().BeOfType<WithoutDependenciesCommand>();
}
[Fact]
public void Default_type_activator_cannot_initialize_a_command_if_it_does_not_have_a_parameterless_constructor()
{
// Arrange
var activator = new DefaultTypeActivator();
// Act & assert
Assert.Throws<CliFxException>(() =>
activator.CreateInstance(typeof(WithDependenciesCommand)));
}
[Fact]
public void Delegate_type_activator_can_initialize_a_command_using_a_custom_function()
{
// Arrange
var activator = new DelegateTypeActivator(_ =>
new WithDependenciesCommand(new DependencyA(), new DependencyB()));
// Act
var obj = activator.CreateInstance(typeof(WithDependenciesCommand));
// Assert
obj.Should().BeOfType<WithDependenciesCommand>();
}
[Fact]
public void Delegate_type_activator_throws_if_the_underlying_function_returns_null()
{
// Arrange
var activator = new DelegateTypeActivator(_ => null);
// Act & assert
Assert.Throws<CliFxException>(() =>
activator.CreateInstance(typeof(WithDependenciesCommand)));
}
}
}

View File

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

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;
namespace CliFx.Tests
{
public partial class DirectivesSpecs
{
[Fact]
public async Task Preview_directive_can_be_enabled_to_print_provided_arguments_as_they_were_parsed()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(NamedCommand))
.UseConsole(console)
.AllowPreviewMode()
.Build();
// Act
var exitCode = await application.RunAsync(
new[] {"[preview]", "cmd", "param", "-abc", "--option", "foo"},
new Dictionary<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]");
}
}
}

View File

@@ -1,888 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using CliFx.Domain;
using CliFx.Exceptions;
using CliFx.Tests.TestCommands;
using CliFx.Tests.TestCustomTypes;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests.Domain
{
[TestFixture]
internal partial class ApplicationSchemaTests
{
private static IEnumerable<TestCaseData> GetTestCases_Resolve()
{
yield return new TestCaseData(
new[]
{
typeof(DivideCommand),
typeof(ConcatCommand),
typeof(EnvironmentVariableCommand)
},
new[]
{
new CommandSchema(typeof(DivideCommand), "div", "Divide one number by another.",
new CommandParameterSchema[0], new[]
{
new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Dividend)),
"dividend", 'D', null, true, "The number to divide."),
new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Divisor)),
"divisor", 'd', null, true, "The number to divide by.")
}),
new CommandSchema(typeof(ConcatCommand), "concat", "Concatenate strings.",
new CommandParameterSchema[0],
new[]
{
new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Inputs)),
null, 'i', null, true, "Input strings."),
new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Separator)),
null, 's', null, false, "String separator.")
}),
new CommandSchema(typeof(EnvironmentVariableCommand), null, "Reads option values from environment variables.",
new CommandParameterSchema[0],
new[]
{
new CommandOptionSchema(typeof(EnvironmentVariableCommand).GetProperty(nameof(EnvironmentVariableCommand.Option)),
"opt", null, "ENV_SINGLE_VALUE", false, null)
}
)
}
);
yield return new TestCaseData(
new[] {typeof(SimpleParameterCommand)},
new[]
{
new CommandSchema(typeof(SimpleParameterCommand), "param cmd2", "Command using positional parameters",
new[]
{
new CommandParameterSchema(typeof(SimpleParameterCommand).GetProperty(nameof(SimpleParameterCommand.ParameterA)),
0, "first", null),
new CommandParameterSchema(typeof(SimpleParameterCommand).GetProperty(nameof(SimpleParameterCommand.ParameterB)),
10, null, null)
},
new[]
{
new CommandOptionSchema(typeof(SimpleParameterCommand).GetProperty(nameof(SimpleParameterCommand.OptionA)),
"option", 'o', null, false, null)
})
}
);
yield return new TestCaseData(
new[] {typeof(HelloWorldDefaultCommand)},
new[]
{
new CommandSchema(typeof(HelloWorldDefaultCommand), null, null,
new CommandParameterSchema[0],
new CommandOptionSchema[0])
}
);
}
private static IEnumerable<TestCaseData> GetTestCases_Resolve_Negative()
{
yield return new TestCaseData(new object[]
{
new Type[0]
});
// Command validation failure
yield return new TestCaseData(new object[]
{
new[] {typeof(NonImplementedCommand)}
});
yield return new TestCaseData(new object[]
{
// Same name
new[] {typeof(ExceptionCommand), typeof(CommandExceptionCommand)}
});
yield return new TestCaseData(new object[]
{
new[] {typeof(NonAnnotatedCommand)}
});
// Parameter validation failure
yield return new TestCaseData(new object[]
{
new[] {typeof(DuplicateParameterOrderCommand)}
});
yield return new TestCaseData(new object[]
{
new[] {typeof(DuplicateParameterNameCommand)}
});
yield return new TestCaseData(new object[]
{
new[] {typeof(MultipleNonScalarParametersCommand)}
});
yield return new TestCaseData(new object[]
{
new[] {typeof(NonLastNonScalarParameterCommand)}
});
// Option validation failure
yield return new TestCaseData(new object[]
{
new[] {typeof(DuplicateOptionNamesCommand)}
});
yield return new TestCaseData(new object[]
{
new[] {typeof(DuplicateOptionShortNamesCommand)}
});
yield return new TestCaseData(new object[]
{
new[] {typeof(DuplicateOptionEnvironmentVariableNamesCommand)}
});
}
[Test]
[TestCaseSource(nameof(GetTestCases_Resolve))]
public void Resolve_Test(
IReadOnlyList<Type> commandTypes,
IReadOnlyList<CommandSchema> expectedCommandSchemas)
{
// Act
var applicationSchema = ApplicationSchema.Resolve(commandTypes);
// Assert
applicationSchema.Commands.Should().BeEquivalentTo(expectedCommandSchemas);
}
[Test]
[TestCaseSource(nameof(GetTestCases_Resolve_Negative))]
public void Resolve_Negative_Test(IReadOnlyList<Type> commandTypes)
{
// Act & Assert
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
Console.WriteLine(ex.Message);
}
}
internal partial class ApplicationSchemaTests
{
private static IEnumerable<TestCaseData> GetTestCases_InitializeEntryPoint()
{
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Object), "value")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Object = "value"}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.String), "value")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {String = "value"}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Bool), "true")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Bool = true}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Bool), "false")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Bool = false}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Bool))
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Bool = true}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Char), "a")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Char = 'a'}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Sbyte), "15")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Sbyte = 15}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Byte), "15")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Byte = 15}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Short), "15")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Short = 15}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Ushort), "15")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Ushort = 15}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Int), "15")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Int = 15}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Uint), "15")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Uint = 15}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Long), "15")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Long = 15}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Ulong), "15")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Ulong = 15}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Float), "123.45")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Float = 123.45f}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Double), "123.45")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Double = 123.45}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Decimal), "123.45")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Decimal = 123.45m}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.DateTime), "28 Apr 1995")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {DateTime = new DateTime(1995, 04, 28)}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.DateTimeOffset), "28 Apr 1995")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {DateTimeOffset = new DateTime(1995, 04, 28)}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.TimeSpan), "00:14:59")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {TimeSpan = new TimeSpan(00, 14, 59)}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestEnum), "value2")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {TestEnum = TestEnum.Value2}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.IntNullable), "666")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {IntNullable = 666}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.IntNullable))
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {IntNullable = null}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestEnumNullable), "value3")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {TestEnumNullable = TestEnum.Value3}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestEnumNullable))
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {TestEnumNullable = null}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.TimeSpanNullable), "01:00:00")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {TimeSpanNullable = new TimeSpan(01, 00, 00)}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.TimeSpanNullable))
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {TimeSpanNullable = null}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestStringConstructable), "value")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {TestStringConstructable = new TestStringConstructable("value")}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestStringParseable), "value")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {TestStringParseable = TestStringParseable.Parse("value")}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestStringParseableWithFormatProvider), "value")
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand
{
TestStringParseableWithFormatProvider =
TestStringParseableWithFormatProvider.Parse("value", CultureInfo.InvariantCulture)
}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.ObjectArray), new[] {"value1", "value2"})
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {ObjectArray = new object[] {"value1", "value2"}}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.StringArray), new[] {"value1", "value2"})
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {StringArray = new[] {"value1", "value2"}}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.StringArray))
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {StringArray = new string[0]}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.IntArray), new[] {"47", "69"})
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {IntArray = new[] {47, 69}}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestEnumArray), new[] {"value1", "value3"})
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {TestEnumArray = new[] {TestEnum.Value1, TestEnum.Value3}}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.IntNullableArray), new[] {"1337", "2441"})
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {IntNullableArray = new int?[] {1337, 2441}}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.TestStringConstructableArray), new[] {"value1", "value2"})
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand
{
TestStringConstructableArray = new[]
{
new TestStringConstructable("value1"),
new TestStringConstructable("value2")
}
}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.Enumerable), new[] {"value1", "value3"})
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {Enumerable = new[] {"value1", "value3"}}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.StringEnumerable), new[] {"value1", "value3"})
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {StringEnumerable = new[] {"value1", "value3"}}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.StringReadOnlyList), new[] {"value1", "value3"})
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {StringReadOnlyList = new[] {"value1", "value3"}}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.StringList), new[] {"value1", "value3"})
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {StringList = new List<string> {"value1", "value3"}}
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[]
{
new CommandOptionInput(nameof(AllSupportedTypesCommand.StringHashSet), new[] {"value1", "value3"})
}),
new Dictionary<string, string>(),
new AllSupportedTypesCommand {StringHashSet = new HashSet<string> {"value1", "value3"}}
);
yield return new TestCaseData(
new[] {typeof(DivideCommand)},
new CommandLineInput(
new[] {"div"},
new[]
{
new CommandOptionInput("dividend", "13"),
new CommandOptionInput("divisor", "8"),
}),
new Dictionary<string, string>(),
new DivideCommand {Dividend = 13, Divisor = 8}
);
yield return new TestCaseData(
new[] {typeof(DivideCommand)},
new CommandLineInput(
new[] {"div"},
new[]
{
new CommandOptionInput("D", "13"),
new CommandOptionInput("d", "8"),
}),
new Dictionary<string, string>(),
new DivideCommand {Dividend = 13, Divisor = 8}
);
yield return new TestCaseData(
new[] {typeof(DivideCommand)},
new CommandLineInput(
new[] {"div"},
new[]
{
new CommandOptionInput("dividend", "13"),
new CommandOptionInput("d", "8"),
}),
new Dictionary<string, string>(),
new DivideCommand {Dividend = 13, Divisor = 8}
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new CommandLineInput(
new[] {"concat"},
new[] {new CommandOptionInput("i", new[] {"foo", " ", "bar"}),}),
new Dictionary<string, string>(),
new ConcatCommand {Inputs = new[] {"foo", " ", "bar"}}
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new CommandLineInput(
new[] {"concat"},
new[]
{
new CommandOptionInput("i", new[] {"foo", "bar"}),
new CommandOptionInput("s", " "),
}),
new Dictionary<string, string>(),
new ConcatCommand {Inputs = new[] {"foo", "bar"}, Separator = " "}
);
yield return new TestCaseData(
new[] {typeof(EnvironmentVariableCommand)},
CommandLineInput.Empty,
new Dictionary<string, string>
{
["ENV_SINGLE_VALUE"] = "A"
},
new EnvironmentVariableCommand {Option = "A"}
);
yield return new TestCaseData(
new[] {typeof(EnvironmentVariableWithMultipleValuesCommand)},
CommandLineInput.Empty,
new Dictionary<string, string>
{
["ENV_MULTIPLE_VALUES"] = string.Join(Path.PathSeparator, "A", "B", "C")
},
new EnvironmentVariableWithMultipleValuesCommand {Option = new[] {"A", "B", "C"}}
);
yield return new TestCaseData(
new[] {typeof(EnvironmentVariableCommand)},
new CommandLineInput(new[] {new CommandOptionInput("opt", "X")}),
new Dictionary<string, string>
{
["ENV_SINGLE_VALUE"] = "A"
},
new EnvironmentVariableCommand {Option = "X"}
);
yield return new TestCaseData(
new[] {typeof(EnvironmentVariableWithoutCollectionPropertyCommand)},
CommandLineInput.Empty,
new Dictionary<string, string>
{
["ENV_MULTIPLE_VALUES"] = string.Join(Path.PathSeparator, "A", "B", "C")
},
new EnvironmentVariableWithoutCollectionPropertyCommand {Option = string.Join(Path.PathSeparator, "A", "B", "C")}
);
yield return new TestCaseData(
new[] {typeof(ParameterCommand)},
new CommandLineInput(
new[] {"param", "cmd", "abc", "123", "1", "2"},
new[] {new CommandOptionInput("o", "option value")}),
new Dictionary<string, string>(),
new ParameterCommand
{
ParameterA = "abc",
ParameterB = 123,
ParameterC = new[] {1, 2},
OptionA = "option value"
}
);
}
private static IEnumerable<TestCaseData> GetTestCases_InitializeEntryPoint_Negative()
{
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[] {new CommandOptionInput(nameof(AllSupportedTypesCommand.Int), "1234.5")}),
new Dictionary<string, string>()
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[] {new CommandOptionInput(nameof(AllSupportedTypesCommand.Int), new[] {"123", "456"})}),
new Dictionary<string, string>()
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[] {new CommandOptionInput(nameof(AllSupportedTypesCommand.Int))}),
new Dictionary<string, string>()
);
yield return new TestCaseData(
new[] {typeof(AllSupportedTypesCommand)},
new CommandLineInput(
new[] {new CommandOptionInput(nameof(AllSupportedTypesCommand.NonConvertible), "123")}),
new Dictionary<string, string>()
);
yield return new TestCaseData(
new[] {typeof(DivideCommand)},
new CommandLineInput(new[] {"div"}),
new Dictionary<string, string>()
);
yield return new TestCaseData(
new[] {typeof(DivideCommand)},
new CommandLineInput(new[] {"div", "-D", "13"}),
new Dictionary<string, string>()
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new CommandLineInput(new[] {"concat"}),
new Dictionary<string, string>()
);
yield return new TestCaseData(
new[] {typeof(ConcatCommand)},
new CommandLineInput(
new[] {"concat"},
new[] {new CommandOptionInput("s", "_")}),
new Dictionary<string, string>()
);
yield return new TestCaseData(
new[] {typeof(ParameterCommand)},
new CommandLineInput(
new[] {"param", "cmd"},
new[] {new CommandOptionInput("o", "option value")}),
new Dictionary<string, string>()
);
yield return new TestCaseData(
new[] {typeof(ParameterCommand)},
new CommandLineInput(
new[] {"param", "cmd", "abc", "123", "invalid"},
new[] {new CommandOptionInput("o", "option value")}),
new Dictionary<string, string>()
);
yield return new TestCaseData(
new[] {typeof(DivideCommand)},
new CommandLineInput(new[] {"non-existing"}),
new Dictionary<string, string>()
);
yield return new TestCaseData(
new[] {typeof(BrokenEnumerableCommand)},
new CommandLineInput(new[] {"value1", "value2"}),
new Dictionary<string, string>()
);
}
[TestCaseSource(nameof(GetTestCases_InitializeEntryPoint))]
public void InitializeEntryPoint_Test(
IReadOnlyList<Type> commandTypes,
CommandLineInput commandLineInput,
IReadOnlyDictionary<string, string> environmentVariables,
ICommand expectedResult)
{
// Arrange
var applicationSchema = ApplicationSchema.Resolve(commandTypes);
var typeActivator = new DefaultTypeActivator();
// Act
var command = applicationSchema.InitializeEntryPoint(commandLineInput, environmentVariables, typeActivator);
// Assert
command.Should().BeEquivalentTo(expectedResult, o => o.RespectingRuntimeTypes());
}
[TestCaseSource(nameof(GetTestCases_InitializeEntryPoint_Negative))]
public void InitializeEntryPoint_Negative_Test(
IReadOnlyList<Type> commandTypes,
CommandLineInput commandLineInput,
IReadOnlyDictionary<string, string> environmentVariables)
{
// Arrange
var applicationSchema = ApplicationSchema.Resolve(commandTypes);
var typeActivator = new DefaultTypeActivator();
// Act & Assert
var ex = Assert.Throws<CliFxException>(() =>
applicationSchema.InitializeEntryPoint(commandLineInput, environmentVariables, typeActivator));
Console.WriteLine(ex.Message);
}
}
}

View File

@@ -1,264 +0,0 @@
using System.Collections.Generic;
using CliFx.Domain;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests.Domain
{
[TestFixture]
internal class CommandLineInputTests
{
private static IEnumerable<TestCaseData> GetTestCases_Parse()
{
yield return new TestCaseData(
new string[0],
CommandLineInput.Empty
);
yield return new TestCaseData(
new[] {"param"},
new CommandLineInput(
new[] {"param"})
);
yield return new TestCaseData(
new[] {"cmd", "param"},
new CommandLineInput(
new[] {"cmd", "param"})
);
yield return new TestCaseData(
new[] {"--option", "value"},
new CommandLineInput(
new[]
{
new CommandOptionInput("option", "value")
})
);
yield return new TestCaseData(
new[] {"--option1", "value1", "--option2", "value2"},
new CommandLineInput(
new[]
{
new CommandOptionInput("option1", "value1"),
new CommandOptionInput("option2", "value2")
})
);
yield return new TestCaseData(
new[] {"--option", "value1", "value2"},
new CommandLineInput(
new[]
{
new CommandOptionInput("option", new[] {"value1", "value2"})
})
);
yield return new TestCaseData(
new[] {"--option", "value1", "--option", "value2"},
new CommandLineInput(
new[]
{
new CommandOptionInput("option", new[] {"value1", "value2"})
})
);
yield return new TestCaseData(
new[] {"-a", "value"},
new CommandLineInput(
new[]
{
new CommandOptionInput("a", "value")
})
);
yield return new TestCaseData(
new[] {"-a", "value1", "-b", "value2"},
new CommandLineInput(
new[]
{
new CommandOptionInput("a", "value1"),
new CommandOptionInput("b", "value2")
})
);
yield return new TestCaseData(
new[] {"-a", "value1", "value2"},
new CommandLineInput(
new[]
{
new CommandOptionInput("a", new[] {"value1", "value2"})
})
);
yield return new TestCaseData(
new[] {"-a", "value1", "-a", "value2"},
new CommandLineInput(
new[]
{
new CommandOptionInput("a", new[] {"value1", "value2"})
})
);
yield return new TestCaseData(
new[] {"--option1", "value1", "-b", "value2"},
new CommandLineInput(
new[]
{
new CommandOptionInput("option1", "value1"),
new CommandOptionInput("b", "value2")
})
);
yield return new TestCaseData(
new[] {"--switch"},
new CommandLineInput(
new[]
{
new CommandOptionInput("switch")
})
);
yield return new TestCaseData(
new[] {"--switch1", "--switch2"},
new CommandLineInput(
new[]
{
new CommandOptionInput("switch1"),
new CommandOptionInput("switch2")
})
);
yield return new TestCaseData(
new[] {"-s"},
new CommandLineInput(
new[]
{
new CommandOptionInput("s")
})
);
yield return new TestCaseData(
new[] {"-a", "-b"},
new CommandLineInput(
new[]
{
new CommandOptionInput("a"),
new CommandOptionInput("b")
})
);
yield return new TestCaseData(
new[] {"-ab"},
new CommandLineInput(
new[]
{
new CommandOptionInput("a"),
new CommandOptionInput("b")
})
);
yield return new TestCaseData(
new[] {"-ab", "value"},
new CommandLineInput(
new[]
{
new CommandOptionInput("a"),
new CommandOptionInput("b", "value")
})
);
yield return new TestCaseData(
new[] {"cmd", "--option", "value"},
new CommandLineInput(
new[] {"cmd"},
new[]
{
new CommandOptionInput("option", "value")
})
);
yield return new TestCaseData(
new[] {"[debug]"},
new CommandLineInput(
new[] {"debug"},
new string[0],
new CommandOptionInput[0])
);
yield return new TestCaseData(
new[] {"[debug]", "[preview]"},
new CommandLineInput(
new[] {"debug", "preview"},
new string[0],
new CommandOptionInput[0])
);
yield return new TestCaseData(
new[] {"cmd", "param1", "param2", "--option", "value"},
new CommandLineInput(
new[] {"cmd", "param1", "param2"},
new[]
{
new CommandOptionInput("option", "value")
})
);
yield return new TestCaseData(
new[] {"[debug]", "[preview]", "-o", "value"},
new CommandLineInput(
new[] {"debug", "preview"},
new string[0],
new[]
{
new CommandOptionInput("o", "value")
})
);
yield return new TestCaseData(
new[] {"cmd", "[debug]", "[preview]", "-o", "value"},
new CommandLineInput(
new[] {"debug", "preview"},
new[] {"cmd"},
new[]
{
new CommandOptionInput("o", "value")
})
);
yield return new TestCaseData(
new[] {"cmd", "[debug]", "[preview]", "-o", "value"},
new CommandLineInput(
new[] {"debug", "preview"},
new[] {"cmd"},
new[]
{
new CommandOptionInput("o", "value")
})
);
yield return new TestCaseData(
new[] {"cmd", "param", "[debug]", "[preview]", "-o", "value"},
new CommandLineInput(
new[] {"debug", "preview"},
new[] {"cmd", "param"},
new[]
{
new CommandOptionInput("o", "value")
})
);
}
[Test]
[TestCaseSource(nameof(GetTestCases_Parse))]
public void Parse_Test(IReadOnlyList<string> commandLineArguments, CommandLineInput expectedResult)
{
// Act
var result = CommandLineInput.Parse(commandLineArguments);
// Assert
result.Should().BeEquivalentTo(expectedResult);
}
}
}

View File

@@ -1,82 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using CliWrap;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests
{
[TestFixture]
public class DummyTests
{
private static Assembly DummyAssembly { get; } = typeof(Dummy.Program).Assembly;
private static IEnumerable<TestCaseData> GetTestCases_RunAsync()
{
yield return new TestCaseData(
new[] {"--version"},
new Dictionary<string, string>(),
$"v{DummyAssembly.GetName().Version}"
);
yield return new TestCaseData(
new string[0],
new Dictionary<string, string>(),
"Hello World!"
);
yield return new TestCaseData(
new[] {"--target", "Earth"},
new Dictionary<string, string>(),
"Hello Earth!"
);
yield return new TestCaseData(
new string[0],
new Dictionary<string, string>
{
["ENV_TARGET"] = "Mars"
},
"Hello Mars!"
);
yield return new TestCaseData(
new[] {"--target", "Earth"},
new Dictionary<string, string>
{
["ENV_TARGET"] = "Mars"
},
"Hello Earth!"
);
}
[TestCaseSource(nameof(GetTestCases_RunAsync))]
public async Task RunAsync_Test(
IReadOnlyList<string> arguments,
IReadOnlyDictionary<string, string> environmentVariables,
string expectedStdOut)
{
// Arrange
var cli = Cli.Wrap("dotnet")
.SetArguments(arguments.Prepend(DummyAssembly.Location).ToArray())
.EnableExitCodeValidation()
.EnableStandardErrorValidation()
.SetStandardOutputCallback(Console.WriteLine)
.SetStandardErrorCallback(Console.WriteLine);
foreach (var (key, value) in environmentVariables)
cli.SetEnvironmentVariable(key, value);
// Act
var result = await cli.ExecuteAsync();
// Assert
result.ExitCode.Should().Be(0);
result.StandardError.Should().BeNullOrWhiteSpace();
result.StandardOutput.TrimEnd().Should().Be(expectedStdOut);
}
}
}

View File

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

@@ -0,0 +1,96 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using CliFx.Domain;
using CliWrap;
using CliWrap.Buffered;
using FluentAssertions;
using Xunit;
namespace CliFx.Tests
{
public partial class EnvironmentVariablesSpecs
{
// This test uses a real application to make sure environment variables are actually read correctly
[Fact]
public async Task Option_can_use_a_specific_environment_variable_as_fallback()
{
// Arrange
var command = Cli.Wrap("dotnet")
.WithArguments(a => a
.Add(Dummy.Program.Location))
.WithEnvironmentVariables(e => e
.Set("ENV_TARGET", "Mars"));
// Act
var stdOut = await command.ExecuteBufferedAsync().Select(r => r.StandardOutput);
// Assert
stdOut.TrimEnd().Should().Be("Hello Mars!");
}
// This test uses a real application to make sure environment variables are actually read correctly
[Fact]
public async Task Option_only_uses_environment_variable_as_fallback_if_the_value_was_not_directly_provided()
{
// Arrange
var command = Cli.Wrap("dotnet")
.WithArguments(a => a
.Add(Dummy.Program.Location)
.Add("--target")
.Add("Jupiter"))
.WithEnvironmentVariables(e => e
.Set("ENV_TARGET", "Mars"));
// Act
var stdOut = await command.ExecuteBufferedAsync().Select(r => r.StandardOutput);
// Assert
stdOut.TrimEnd().Should().Be("Hello Jupiter!");
}
[Fact]
public void Option_of_non_scalar_type_can_take_multiple_separated_values_from_an_environment_variable()
{
// Arrange
var schema = ApplicationSchema.Resolve(new[] {typeof(EnvironmentVariableCollectionCommand)});
var input = CommandLineInput.Empty;
var envVars = new Dictionary<string, string>
{
["ENV_OPT"] = $"foo{Path.PathSeparator}bar"
};
// Act
var command = schema.InitializeEntryPoint(input, envVars);
// Assert
command.Should().BeEquivalentTo(new EnvironmentVariableCollectionCommand
{
Option = new[] {"foo", "bar"}
});
}
[Fact]
public void Option_of_scalar_type_can_only_take_a_single_value_from_an_environment_variable_even_if_it_contains_separators()
{
// Arrange
var schema = ApplicationSchema.Resolve(new[] {typeof(EnvironmentVariableCommand)});
var input = CommandLineInput.Empty;
var envVars = new Dictionary<string, string>
{
["ENV_OPT"] = $"foo{Path.PathSeparator}bar"
};
// Act
var command = schema.InitializeEntryPoint(input, envVars);
// Assert
command.Should().BeEquivalentTo(new EnvironmentVariableCommand
{
Option = $"foo{Path.PathSeparator}bar"
});
}
}
}

View File

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

View File

@@ -0,0 +1,84 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;
namespace CliFx.Tests
{
public partial class ErrorReportingSpecs
{
[Fact]
public async Task Command_may_throw_a_generic_exception_which_exits_and_prints_full_error_details()
{
// Arrange
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand(typeof(GenericExceptionCommand))
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] {"exc", "-m", "Kaput"},
new Dictionary<string, string>());
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().NotBe(0);
stdErrData.Should().Contain("Kaput");
stdErrData.Length.Should().BeGreaterThan("Kaput".Length);
}
[Fact]
public async Task Command_may_throw_a_specialized_exception_which_exits_with_custom_code_and_prints_minimal_error_details()
{
// Arrange
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand(typeof(CommandExceptionCommand))
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] {"exc", "-m", "Kaput", "-c", "69"},
new Dictionary<string, string>());
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().Be(69);
stdErrData.Should().Be("Kaput");
}
[Fact]
public async Task Command_may_throw_a_specialized_exception_without_error_message_which_exits_and_prints_full_error_details()
{
// Arrange
await using var stdErr = new MemoryStream();
var console = new VirtualConsole(error: stdErr);
var application = new CliApplicationBuilder()
.AddCommand(typeof(CommandExceptionCommand))
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] {"exc", "-m", "Kaput"},
new Dictionary<string, string>());
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
// Assert
exitCode.Should().NotBe(0);
stdErrData.Should().NotBeEmpty();
}
}
}

View File

@@ -0,0 +1,96 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests
{
public partial class HelpTextSpecs
{
[Command(Description = "DefaultCommand description.")]
private class DefaultCommand : ICommand
{
[CommandOption("option-a", 'a', Description = "OptionA description.")]
public string? OptionA { get; set; }
[CommandOption("option-b", 'b', Description = "OptionB description.")]
public string? OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command("cmd", Description = "NamedCommand description.")]
private class NamedCommand : ICommand
{
[CommandParameter(0, Name = "param-a", Description = "ParameterA description.")]
public string? ParameterA { get; set; }
[CommandOption("option-c", 'c', Description = "OptionC description.")]
public string? OptionC { get; set; }
[CommandOption("option-d", 'd', Description = "OptionD description.")]
public string? OptionD { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command("cmd sub", Description = "NamedSubCommand description.")]
private class NamedSubCommand : ICommand
{
[CommandParameter(0, Name = "param-b", Description = "ParameterB description.")]
public string? ParameterB { get; set; }
[CommandParameter(1, Name = "param-c", Description = "ParameterC description.")]
public string? ParameterC { get; set; }
[CommandOption("option-e", 'e', Description = "OptionE description.")]
public string? OptionE { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command("cmd-with-params")]
private class ParametersCommand : ICommand
{
[CommandParameter(0, Name = "first")]
public string? ParameterA { get; set; }
[CommandParameter(10)]
public int? ParameterB { get; set; }
[CommandParameter(20, Description = "A list of numbers", Name = "third list")]
public IEnumerable<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-f", 'f', IsRequired = true)]
public string? OptionF { get; set; }
[CommandOption("option-g", 'g', IsRequired = true)]
public IEnumerable<int>? OptionG { get; set; }
[CommandOption("option-h", 'h')]
public string? OptionH { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command("cmd-with-env-vars")]
private class EnvironmentVariableCommand : ICommand
{
[CommandOption("option-a", 'a', IsRequired = true, EnvironmentVariableName = "ENV_OPT_A")]
public string? OptionA { get; set; }
[CommandOption("option-b", 'b', EnvironmentVariableName = "ENV_OPT_B")]
public string? OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}
}

View File

@@ -0,0 +1,246 @@
using System.IO;
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;
namespace CliFx.Tests
{
public partial class HelpTextSpecs
{
[Fact]
public async Task Version_information_can_be_requested_by_providing_the_version_option_without_other_arguments()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(DefaultCommand))
.AddCommand(typeof(NamedCommand))
.AddCommand(typeof(NamedSubCommand))
.UseVersionText("v6.9")
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(new[] {"--version"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
exitCode.Should().Be(0);
stdOutData.Should().Be("v6.9");
}
[Fact]
public async Task Help_text_can_be_requested_by_providing_the_help_option()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(DefaultCommand))
.AddCommand(typeof(NamedCommand))
.AddCommand(typeof(NamedSubCommand))
.UseTitle("AppTitle")
.UseVersionText("AppVer")
.UseDescription("AppDesc")
.UseExecutableName("AppExe")
.UseConsole(console)
.Build();
// Act
await application.RunAsync(new[] {"--help"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
stdOutData.Should().ContainAll(
"AppTitle", "AppVer",
"AppDesc",
"Usage",
"AppExe", "[command]", "[options]",
"Options",
"-a|--option-a", "OptionA description.",
"-b|--option-b", "OptionB description.",
"-h|--help", "Shows help text.",
"--version", "Shows version information.",
"Commands",
"cmd", "NamedCommand description.",
"You can run", "to show help on a specific command."
);
}
[Fact]
public async Task Help_text_can_be_requested_on_a_specific_named_command()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(DefaultCommand))
.AddCommand(typeof(NamedCommand))
.AddCommand(typeof(NamedSubCommand))
.UseConsole(console)
.Build();
// Act
await application.RunAsync(new[] {"cmd", "--help"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
stdOutData.Should().ContainAll(
"Description",
"NamedCommand description.",
"Usage",
"cmd", "[command]", "<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."
);
}
[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."
);
}
[Fact]
public async Task Help_text_can_be_requested_without_specifying_command_even_if_default_command_is_not_defined()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(NamedCommand))
.AddCommand(typeof(NamedSubCommand))
.UseConsole(console)
.Build();
// Act
await application.RunAsync(new[] {"--help"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
stdOutData.Should().ContainAll(
"Usage",
"[command]",
"Options",
"-h|--help", "Shows help text.",
"--version", "Shows version information.",
"Commands",
"cmd", "NamedCommand description.",
"You can run", "to show help on a specific command."
);
}
[Fact]
public async Task Help_text_shows_usage_format_which_lists_all_parameters()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(ParametersCommand))
.UseConsole(console)
.Build();
// Act
await application.RunAsync(new[] {"cmd-with-params", "--help"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
stdOutData.Should().ContainAll(
"Usage",
"cmd-with-params", "<first>", "<parameterb>", "<third list...>", "[options]"
);
}
[Fact]
public async Task Help_text_shows_usage_format_which_lists_all_required_options()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(RequiredOptionsCommand))
.UseConsole(console)
.Build();
// Act
await application.RunAsync(new[] {"cmd-with-req-opts", "--help"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
stdOutData.Should().ContainAll(
"Usage",
"cmd-with-req-opts", "--option-f <value>", "--option-g <values...>", "[options]",
"Options",
"* -f|--option-f",
"* -g|--option-g",
"-h|--option-h"
);
}
[Fact]
public async Task Help_text_lists_environment_variable_names_for_options_that_have_them_defined()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(EnvironmentVariableCommand))
.UseConsole(console)
.Build();
// Act
await application.RunAsync(new[] {"cmd-with-env-vars", "--help"});
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
stdOutData.Should().ContainAll(
"Options",
"* -a|--option-a", "Environment variable:", "ENV_OPT_A",
"-b|--option-b", "Environment variable:", "ENV_OPT_B"
);
}
}
}

View File

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

View File

@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;
namespace CliFx.Tests
{
public partial class RoutingSpecs
{
[Fact]
public async Task Default_command_is_executed_if_provided_arguments_do_not_match_any_named_command()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(DefaultCommand))
.AddCommand(typeof(ConcatCommand))
.AddCommand(typeof(DivideCommand))
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(
Array.Empty<string>(),
new Dictionary<string, string>());
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
exitCode.Should().Be(0);
stdOutData.Should().Be("Hello world!");
}
[Fact]
public async Task Help_text_is_printed_if_no_arguments_were_provided_and_default_command_is_not_defined()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(ConcatCommand))
.AddCommand(typeof(DivideCommand))
.UseConsole(console)
.UseDescription("This will be visible in help")
.Build();
// Act
var exitCode = await application.RunAsync(
Array.Empty<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");
}
[Fact]
public async Task Specific_named_command_is_executed_if_provided_arguments_match_its_name()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(DefaultCommand))
.AddCommand(typeof(ConcatCommand))
.AddCommand(typeof(DivideCommand))
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] {"concat", "-i", "foo", "bar", "-s", ", "},
new Dictionary<string, string>());
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
exitCode.Should().Be(0);
stdOutData.Should().Be("foo, bar");
}
}
}

View File

@@ -1,17 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command("allrequired", Description = "AllRequiredOptionsCommand description.")]
public class AllRequiredOptionsCommand : ICommand
{
[CommandOption("option-f", 'f', IsRequired = true, Description = "OptionF description.")]
public string? OptionF { get; set; }
[CommandOption("option-g", 'g', IsRequired = true, Description = "OptionG description.")]
public string? OptionFG { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,126 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Tests.TestCustomTypes;
namespace CliFx.Tests.TestCommands
{
[Command]
public class AllSupportedTypesCommand : ICommand
{
[CommandOption(nameof(Object))]
public object? Object { get; set; } = 42;
[CommandOption(nameof(String))]
public string? String { get; set; } = "foo bar";
[CommandOption(nameof(Bool))]
public bool Bool { get; set; }
[CommandOption(nameof(Char))]
public char Char { get; set; }
[CommandOption(nameof(Sbyte))]
public sbyte Sbyte { get; set; }
[CommandOption(nameof(Byte))]
public byte Byte { get; set; }
[CommandOption(nameof(Short))]
public short Short { get; set; }
[CommandOption(nameof(Ushort))]
public ushort Ushort { get; set; }
[CommandOption(nameof(Int))]
public int Int { get; set; }
[CommandOption(nameof(Uint))]
public uint Uint { get; set; }
[CommandOption(nameof(Long))]
public long Long { get; set; }
[CommandOption(nameof(Ulong))]
public ulong Ulong { get; set; }
[CommandOption(nameof(Float))]
public float Float { get; set; }
[CommandOption(nameof(Double))]
public double Double { get; set; }
[CommandOption(nameof(Decimal))]
public decimal Decimal { get; set; }
[CommandOption(nameof(DateTime))]
public DateTime DateTime { get; set; }
[CommandOption(nameof(DateTimeOffset))]
public DateTimeOffset DateTimeOffset { get; set; }
[CommandOption(nameof(TimeSpan))]
public TimeSpan TimeSpan { get; set; }
[CommandOption(nameof(TestEnum))]
public TestEnum TestEnum { get; set; }
[CommandOption(nameof(IntNullable))]
public int? IntNullable { get; set; }
[CommandOption(nameof(TestEnumNullable))]
public TestEnum? TestEnumNullable { get; set; }
[CommandOption(nameof(TimeSpanNullable))]
public TimeSpan? TimeSpanNullable { get; set; }
[CommandOption(nameof(TestStringConstructable))]
public TestStringConstructable? TestStringConstructable { get; set; }
[CommandOption(nameof(TestStringParseable))]
public TestStringParseable? TestStringParseable { get; set; }
[CommandOption(nameof(TestStringParseableWithFormatProvider))]
public TestStringParseableWithFormatProvider? TestStringParseableWithFormatProvider { get; set; }
[CommandOption(nameof(ObjectArray))]
public object[]? ObjectArray { get; set; }
[CommandOption(nameof(StringArray))]
public string[]? StringArray { get; set; }
[CommandOption(nameof(IntArray))]
public int[]? IntArray { get; set; }
[CommandOption(nameof(TestEnumArray))]
public TestEnum[]? TestEnumArray { get; set; }
[CommandOption(nameof(IntNullableArray))]
public int?[]? IntNullableArray { get; set; }
[CommandOption(nameof(TestStringConstructableArray))]
public TestStringConstructable[]? TestStringConstructableArray { get; set; }
[CommandOption(nameof(Enumerable))]
public IEnumerable? Enumerable { get; set; }
[CommandOption(nameof(StringEnumerable))]
public IEnumerable<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; }
[CommandOption(nameof(NonConvertible))]
public TestNonStringParseable? NonConvertible { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,15 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Tests.TestCustomTypes;
namespace CliFx.Tests.TestCommands
{
[Command]
public class BrokenEnumerableCommand : ICommand
{
[CommandParameter(0)]
public TestCustomEnumerable<string>? Test { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,16 +0,0 @@
using System;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command("cancel")]
public class CancellableCommand : ICommand
{
public async ValueTask ExecuteAsync(IConsole console)
{
await Task.Delay(TimeSpan.FromSeconds(3), console.GetCancellationToken());
console.Output.WriteLine("Never printed");
}
}
}

View File

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

View File

@@ -1,22 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command("concat", Description = "Concatenate strings.")]
public class ConcatCommand : ICommand
{
[CommandOption('i', IsRequired = true, Description = "Input strings.")]
public IReadOnlyList<string> Inputs { get; set; }
[CommandOption('s', Description = "String separator.")]
public string Separator { get; set; } = "";
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(string.Join(Separator, Inputs));
return default;
}
}
}

View File

@@ -1,24 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command("div", Description = "Divide one number by another.")]
public class DivideCommand : ICommand
{
[CommandOption("dividend", 'D', IsRequired = true, Description = "The number to divide.")]
public double Dividend { get; set; }
[CommandOption("divisor", 'd', IsRequired = true, Description = "The number to divide by.")]
public double Divisor { get; set; }
// This property should be ignored by resolver
public bool NotAnOption { get; set; }
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(Dividend / Divisor);
return default;
}
}
}

View File

@@ -1,17 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command]
public class DuplicateOptionEnvironmentVariableNamesCommand : ICommand
{
[CommandOption("option-a", EnvironmentVariableName = "ENV_VAR")]
public string? OptionA { get; set; }
[CommandOption("option-b", EnvironmentVariableName = "ENV_VAR")]
public string? OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,17 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command]
public class DuplicateOptionNamesCommand : ICommand
{
[CommandOption("fruits")]
public string? Apples { get; set; }
[CommandOption("fruits")]
public string? Oranges { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,17 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command]
public class DuplicateOptionShortNamesCommand : ICommand
{
[CommandOption('x')]
public string? OptionA { get; set; }
[CommandOption('x')]
public string? OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,17 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command]
public class DuplicateParameterNameCommand : ICommand
{
[CommandParameter(0, Name = "param")]
public string? ParameterA { get; set; }
[CommandParameter(1, Name = "param")]
public string? ParameterB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,17 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command]
public class DuplicateParameterOrderCommand : ICommand
{
[CommandParameter(13)]
public string? ParameterA { get; set; }
[CommandParameter(13)]
public string? ParameterB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,14 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command(Description = "Reads option values from environment variables.")]
public class EnvironmentVariableCommand : ICommand
{
[CommandOption("opt", EnvironmentVariableName = "ENV_SINGLE_VALUE")]
public string? Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,15 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command(Description = "Reads multiple option values from environment variables.")]
public class EnvironmentVariableWithMultipleValuesCommand : ICommand
{
[CommandOption("opt", EnvironmentVariableName = "ENV_MULTIPLE_VALUES")]
public IEnumerable<string>? Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,14 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command(Description = "Reads one option value from environment variables because target property is not a collection.")]
public class EnvironmentVariableWithoutCollectionPropertyCommand : ICommand
{
[CommandOption("opt", EnvironmentVariableName = "ENV_MULTIPLE_VALUES")]
public string? Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

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

View File

@@ -1,15 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command]
public class HelloWorldDefaultCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine("Hello world.");
return default;
}
}
}

View File

@@ -1,17 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command(Description = "HelpDefaultCommand description.")]
public class HelpDefaultCommand : ICommand
{
[CommandOption("option-a", 'a', Description = "OptionA description.")]
public string? OptionA { get; set; }
[CommandOption("option-b", 'b', Description = "OptionB description.")]
public string? OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,17 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command("cmd", Description = "HelpNamedCommand description.")]
public class HelpNamedCommand : ICommand
{
[CommandOption("option-c", 'c', Description = "OptionC description.")]
public string? OptionC { get; set; }
[CommandOption("option-d", 'd', Description = "OptionD description.")]
public string? OptionD { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,14 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command("cmd sub", Description = "HelpSubCommand description.")]
public class HelpSubCommand : ICommand
{
[CommandOption("option-e", 'e', Description = "OptionE description.")]
public string? OptionE { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,18 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command]
public class MultipleNonScalarParametersCommand : ICommand
{
[CommandParameter(0)]
public IReadOnlyList<string>? ParameterA { get; set; }
[CommandParameter(1)]
public IReadOnlyList<string>? ParameterB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,9 +0,0 @@
using System.Threading.Tasks;
namespace CliFx.Tests.TestCommands
{
public class NonAnnotatedCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

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

View File

@@ -1,18 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command]
public class NonLastNonScalarParameterCommand : ICommand
{
[CommandParameter(0)]
public IReadOnlyList<string>? ParameterA { get; set; }
[CommandParameter(1)]
public string? ParameterB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,24 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command("param cmd", Description = "Command using positional parameters")]
public class ParameterCommand : ICommand
{
[CommandParameter(0, Name = "first")]
public string? ParameterA { get; set; }
[CommandParameter(10)]
public int? ParameterB { get; set; }
[CommandParameter(20, Description = "A list of numbers", Name = "third list")]
public IEnumerable<int>? ParameterC { get; set; }
[CommandOption("option", 'o')]
public string? OptionA { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,20 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command("param cmd2", Description = "Command using positional parameters")]
public class SimpleParameterCommand : ICommand
{
[CommandParameter(0, Name = "first")]
public string? ParameterA { get; set; }
[CommandParameter(10)]
public int? ParameterB { get; set; }
[CommandOption("option", 'o')]
public string? OptionA { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,17 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.TestCommands
{
[Command("somerequired", Description = "SomeRequiredOptionsCommand description.")]
public class SomeRequiredOptionsCommand : ICommand
{
[CommandOption("option-f", 'f', IsRequired = true, Description = "OptionF description.")]
public string? OptionF { get; set; }
[CommandOption("option-g", 'g', Description = "OptionG description.")]
public string? OptionFG { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -1,14 +0,0 @@
using System.Collections;
using System.Collections.Generic;
namespace CliFx.Tests.TestCustomTypes
{
public class TestCustomEnumerable<T> : IEnumerable<T>
{
private readonly T[] _arr = new T[0];
public IEnumerator<T> GetEnumerator() => ((IEnumerable<T>) _arr).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

View File

@@ -1,9 +0,0 @@
namespace CliFx.Tests.TestCustomTypes
{
public enum TestEnum
{
Value1,
Value2,
Value3
}
}

View File

@@ -1,12 +0,0 @@
namespace CliFx.Tests.TestCustomTypes
{
public class TestNonStringParseable
{
public int Value { get; }
public TestNonStringParseable(int value)
{
Value = value;
}
}
}

View File

@@ -1,12 +0,0 @@
namespace CliFx.Tests.TestCustomTypes
{
public class TestStringConstructable
{
public string Value { get; }
public TestStringConstructable(string value)
{
Value = value;
}
}
}

View File

@@ -1,14 +0,0 @@
namespace CliFx.Tests.TestCustomTypes
{
public class TestStringParseable
{
public string Value { get; }
private TestStringParseable(string value)
{
Value = value;
}
public static TestStringParseable Parse(string value) => new TestStringParseable(value);
}
}

View File

@@ -1,17 +0,0 @@
using System;
namespace CliFx.Tests.TestCustomTypes
{
public class TestStringParseableWithFormatProvider
{
public string Value { get; }
private TestStringParseableWithFormatProvider(string value)
{
Value = value;
}
public static TestStringParseableWithFormatProvider Parse(string value, IFormatProvider formatProvider) =>
new TestStringParseableWithFormatProvider(value + " " + formatProvider);
}
}

View File

@@ -1,46 +0,0 @@
using System.Linq;
using CliFx.Utilities;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests.Utilities
{
[TestFixture]
public class ProgressTickerTests
{
[Test]
public void Report_Test()
{
// Arrange
using var console = new VirtualConsole(false);
var ticker = console.CreateProgressTicker();
var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray();
var progressStringValues = progressValues.Select(p => p.ToString("P2")).ToArray();
// Act
foreach (var progress in progressValues)
ticker.Report(progress);
// Assert
console.ReadOutputString().Should().ContainAll(progressStringValues);
}
[Test]
public void Report_Redirected_Test()
{
// Arrange
using var console = new VirtualConsole();
var ticker = console.CreateProgressTicker();
var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray();
// Act
foreach (var progress in progressValues)
ticker.Report(progress);
// Assert
console.ReadOutputString().Should().BeEmpty();
}
}
}

View File

@@ -0,0 +1,54 @@
using System.IO;
using System.Linq;
using CliFx.Utilities;
using FluentAssertions;
using Xunit;
namespace CliFx.Tests
{
public class UtilitiesSpecs
{
[Fact]
public void Progress_ticker_can_be_used_to_report_progress_to_console()
{
// Arrange
using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut, isOutputRedirected: false);
var ticker = console.CreateProgressTicker();
var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray();
var progressStringValues = progressValues.Select(p => p.ToString("P2")).ToArray();
// Act
foreach (var progress in progressValues)
ticker.Report(progress);
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray());
// Assert
stdOutData.Should().ContainAll(progressStringValues);
}
[Fact]
public void Progress_ticker_does_not_write_to_console_if_output_is_redirected()
{
// Arrange
using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var ticker = console.CreateProgressTicker();
var progressValues = Enumerable.Range(0, 100).Select(p => p / 100.0).ToArray();
// Act
foreach (var progress in progressValues)
ticker.Report(progress);
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray());
// Assert
stdOutData.Should().BeEmpty();
}
}
}

View File

@@ -1,33 +0,0 @@
using System;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests
{
[TestFixture]
public class VirtualConsoleTests
{
[Test(Description = "Must not leak to system console")]
public void Smoke_Test()
{
// Arrange
using var console = new VirtualConsole();
console.WriteInputString("hello world");
// Act
console.ResetColor();
console.ForegroundColor = ConsoleColor.DarkMagenta;
console.BackgroundColor = ConsoleColor.DarkMagenta;
// Assert
console.Input.Should().NotBeSameAs(Console.In);
console.IsInputRedirected.Should().BeTrue();
console.Output.Should().NotBeSameAs(Console.Out);
console.IsOutputRedirected.Should().BeTrue();
console.Error.Should().NotBeSameAs(Console.Error);
console.IsErrorRedirected.Should().BeTrue();
console.ForegroundColor.Should().NotBe(Console.ForegroundColor);
console.BackgroundColor.Should().NotBe(Console.BackgroundColor);
}
}
}

View File

@@ -0,0 +1,5 @@
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"methodDisplayOptions": "all",
"methodDisplay": "method"
}

View File

@@ -12,13 +12,15 @@ namespace CliFx
/// <summary>
/// Command line application facade.
/// </summary>
public partial class CliApplication
public class CliApplication
{
private readonly ApplicationMetadata _metadata;
private readonly ApplicationConfiguration _configuration;
private readonly IConsole _console;
private readonly ITypeActivator _typeActivator;
private readonly HelpTextWriter _helpTextWriter;
/// <summary>
/// Initializes an instance of <see cref="CliApplication"/>.
/// </summary>
@@ -30,6 +32,8 @@ namespace CliFx
_configuration = configuration;
_console = console;
_typeActivator = typeActivator;
_helpTextWriter = new HelpTextWriter(metadata, console);
}
private async ValueTask<int?> HandleDebugDirectiveAsync(CommandLineInput commandLineInput)
@@ -67,7 +71,7 @@ namespace CliFx
}
// Parameters
foreach (var parameter in commandLineInput.Arguments.Skip(argumentOffset))
foreach (var parameter in commandLineInput.UnboundArguments.Skip(argumentOffset))
{
_console.Output.Write('<');
@@ -98,7 +102,7 @@ namespace CliFx
private int? HandleVersionOption(CommandLineInput commandLineInput)
{
// Version option is available only on the default command (i.e. when arguments are not specified)
var shouldRenderVersion = !commandLineInput.Arguments.Any() && commandLineInput.IsVersionOptionSpecified;
var shouldRenderVersion = !commandLineInput.UnboundArguments.Any() && commandLineInput.IsVersionOptionSpecified;
if (!shouldRenderVersion)
return null;
@@ -112,7 +116,7 @@ namespace CliFx
// Help is rendered either when it's requested or when the user provides no arguments and there is no default command
var shouldRenderHelp =
commandLineInput.IsHelpOptionSpecified ||
!applicationSchema.Commands.Any(c => c.IsDefault) && !commandLineInput.Arguments.Any() && !commandLineInput.Options.Any();
!applicationSchema.Commands.Any(c => c.IsDefault) && !commandLineInput.UnboundArguments.Any() && !commandLineInput.Options.Any();
if (!shouldRenderHelp)
return null;
@@ -122,7 +126,7 @@ namespace CliFx
applicationSchema.TryFindCommand(commandLineInput) ??
CommandSchema.StubDefaultCommand;
RenderHelp(applicationSchema, commandSchema);
_helpTextWriter.Write(applicationSchema, commandSchema);
return 0;
}

View File

@@ -46,7 +46,7 @@ namespace CliFx
/// <summary>
/// Adds commands from the specified assembly to the application.
/// Only the public types are added.
/// Only adds public valid command types.
/// </summary>
public CliApplicationBuilder AddCommandsFrom(Assembly commandAssembly)
{
@@ -58,7 +58,7 @@ namespace CliFx
/// <summary>
/// Adds commands from the specified assemblies to the application.
/// Only the public types are added.
/// Only adds public valid command types.
/// </summary>
public CliApplicationBuilder AddCommandsFrom(IEnumerable<Assembly> commandAssemblies)
{
@@ -70,7 +70,7 @@ namespace CliFx
/// <summary>
/// Adds commands from the calling assembly to the application.
/// Only the public types are added.
/// Only adds public valid command types.
/// </summary>
public CliApplicationBuilder AddCommandsFromThisAssembly() => AddCommandsFrom(Assembly.GetCallingAssembly());

View File

@@ -22,6 +22,9 @@
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>$(AssemblyName).Tests</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>$(AssemblyName).Analyzers</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
@@ -31,7 +34,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="all" />
<PackageReference Include="Nullable" Version="1.2.0" PrivateAssets="all" />
<PackageReference Include="Nullable" Version="1.2.1" PrivateAssets="all" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'net45'">

View File

@@ -47,9 +47,9 @@ namespace CliFx.Domain
public CommandSchema? TryFindCommand(CommandLineInput commandLineInput, out int argumentOffset)
{
// Try to find the command that contains the most of the input arguments in its name
for (var i = commandLineInput.Arguments.Count; i >= 0; i--)
for (var i = commandLineInput.UnboundArguments.Count; i >= 0; i--)
{
var potentialCommandName = string.Join(" ", commandLineInput.Arguments.Take(i));
var potentialCommandName = string.Join(" ", commandLineInput.UnboundArguments.Take(i));
var matchingCommand = Commands.FirstOrDefault(c => c.MatchesName(potentialCommandName));
if (matchingCommand != null)
@@ -75,15 +75,25 @@ namespace CliFx.Domain
if (command == null)
{
throw new CliFxException(
$"Can't find a command that matches arguments [{string.Join(" ", commandLineInput.Arguments)}].");
$"Can't find a command that matches arguments [{string.Join(" ", commandLineInput.UnboundArguments)}].");
}
var parameterInputs = argumentOffset == 0
? commandLineInput.Arguments
: commandLineInput.Arguments.Skip(argumentOffset).ToArray();
var parameterValues = argumentOffset == 0
? commandLineInput.UnboundArguments.Select(a => a.Value).ToArray()
: commandLineInput.UnboundArguments.Skip(argumentOffset).Select(a => a.Value).ToArray();
return command.CreateInstance(parameterInputs, commandLineInput.Options, environmentVariables, activator);
return command.CreateInstance(parameterValues, commandLineInput.Options, environmentVariables, activator);
}
public ICommand InitializeEntryPoint(
CommandLineInput commandLineInput,
IReadOnlyDictionary<string, string> environmentVariables) =>
InitializeEntryPoint(commandLineInput, environmentVariables, new DefaultTypeActivator());
public ICommand InitializeEntryPoint(CommandLineInput commandLineInput) =>
InitializeEntryPoint(commandLineInput, new Dictionary<string, string>());
public override string ToString() => string.Join(Environment.NewLine, Commands);
}
internal partial class ApplicationSchema

View File

@@ -0,0 +1,20 @@
using System;
namespace CliFx.Domain
{
internal class CommandDirectiveInput
{
public string Name { get; }
public bool IsDebugDirective => string.Equals(Name, "debug", StringComparison.OrdinalIgnoreCase);
public bool IsPreviewDirective => string.Equals(Name, "preview", StringComparison.OrdinalIgnoreCase);
public CommandDirectiveInput(string name)
{
Name = name;
}
public override string ToString() => $"[{Name}]";
}
}

View File

@@ -8,49 +8,30 @@ namespace CliFx.Domain
{
internal partial class CommandLineInput
{
public IReadOnlyList<string> Directives { get; }
public IReadOnlyList<CommandDirectiveInput> Directives { get; }
public IReadOnlyList<string> Arguments { get; }
public IReadOnlyList<CommandUnboundArgumentInput> UnboundArguments { get; }
public IReadOnlyList<CommandOptionInput> Options { get; }
public bool IsDebugDirectiveSpecified => Directives.Contains("debug", StringComparer.OrdinalIgnoreCase);
public bool IsDebugDirectiveSpecified => Directives.Any(d => d.IsDebugDirective);
public bool IsPreviewDirectiveSpecified => Directives.Contains("preview", StringComparer.OrdinalIgnoreCase);
public bool IsPreviewDirectiveSpecified => Directives.Any(d => d.IsPreviewDirective);
public bool IsHelpOptionSpecified =>
Options.Any(o => CommandOptionSchema.HelpOption.MatchesNameOrShortName(o.Alias));
public bool IsHelpOptionSpecified => Options.Any(o => o.IsHelpOption);
public bool IsVersionOptionSpecified =>
Options.Any(o => CommandOptionSchema.VersionOption.MatchesNameOrShortName(o.Alias));
public bool IsVersionOptionSpecified => Options.Any(o => o.IsVersionOption);
public CommandLineInput(
IReadOnlyList<string> directives,
IReadOnlyList<string> arguments,
IReadOnlyList<CommandDirectiveInput> directives,
IReadOnlyList<CommandUnboundArgumentInput> unboundArguments,
IReadOnlyList<CommandOptionInput> options)
{
Directives = directives;
Arguments = arguments;
UnboundArguments = unboundArguments;
Options = options;
}
public CommandLineInput(
IReadOnlyList<string> arguments,
IReadOnlyList<CommandOptionInput> options)
: this(new string[0], arguments, options)
{
}
public CommandLineInput(IReadOnlyList<string> arguments)
: this(arguments, new CommandOptionInput[0])
{
}
public CommandLineInput(IReadOnlyList<CommandOptionInput> options)
: this(new string[0], options)
{
}
public override string ToString()
{
var buffer = new StringBuilder();
@@ -58,13 +39,10 @@ namespace CliFx.Domain
foreach (var directive in Directives)
{
buffer.AppendIfNotEmpty(' ');
buffer
.Append('[')
.Append(directive)
.Append(']');
buffer.Append(directive);
}
foreach (var argument in Arguments)
foreach (var argument in UnboundArguments)
{
buffer.AppendIfNotEmpty(' ');
buffer.Append(argument);
@@ -84,16 +62,14 @@ namespace CliFx.Domain
{
public static CommandLineInput Parse(IReadOnlyList<string> commandLineArguments)
{
var directives = new List<string>();
var arguments = new List<string>();
var optionsDic = new Dictionary<string, List<string>>();
var builder = new CommandLineInputBuilder();
// Option aliases and values are parsed in pairs so we need to keep track of last alias
var lastOptionAlias = "";
var currentOptionAlias = "";
var currentOptionValues = new List<string>();
bool TryParseDirective(string argument)
{
if (!string.IsNullOrWhiteSpace(lastOptionAlias))
if (!string.IsNullOrWhiteSpace(currentOptionAlias))
return false;
if (!argument.StartsWith("[", StringComparison.OrdinalIgnoreCase) ||
@@ -101,17 +77,17 @@ namespace CliFx.Domain
return false;
var directive = argument.Substring(1, argument.Length - 2);
directives.Add(directive);
builder.AddDirective(directive);
return true;
}
bool TryParseArgument(string argument)
{
if (!string.IsNullOrWhiteSpace(lastOptionAlias))
if (!string.IsNullOrWhiteSpace(currentOptionAlias))
return false;
arguments.Add(argument);
builder.AddUnboundArgument(argument);
return true;
}
@@ -121,10 +97,11 @@ namespace CliFx.Domain
if (!argument.StartsWith("--", StringComparison.OrdinalIgnoreCase))
return false;
lastOptionAlias = argument.Substring(2);
if (!string.IsNullOrWhiteSpace(currentOptionAlias))
builder.AddOption(currentOptionAlias, currentOptionValues);
if (!optionsDic.ContainsKey(lastOptionAlias))
optionsDic[lastOptionAlias] = new List<string>();
currentOptionAlias = argument.Substring(2);
currentOptionValues = new List<string>();
return true;
}
@@ -136,10 +113,11 @@ namespace CliFx.Domain
foreach (var c in argument.Substring(1))
{
lastOptionAlias = c.AsString();
if (!string.IsNullOrWhiteSpace(currentOptionAlias))
builder.AddOption(currentOptionAlias, currentOptionValues);
if (!optionsDic.ContainsKey(lastOptionAlias))
optionsDic[lastOptionAlias] = new List<string>();
currentOptionAlias = c.AsString();
currentOptionValues = new List<string>();
}
return true;
@@ -147,10 +125,10 @@ namespace CliFx.Domain
bool TryParseOptionValue(string argument)
{
if (string.IsNullOrWhiteSpace(lastOptionAlias))
if (string.IsNullOrWhiteSpace(currentOptionAlias))
return false;
optionsDic[lastOptionAlias].Add(argument);
currentOptionValues.Add(argument);
return true;
}
@@ -165,15 +143,21 @@ namespace CliFx.Domain
TryParseOptionValue(argument);
}
var options = optionsDic.Select(p => new CommandOptionInput(p.Key, p.Value)).ToArray();
if (!string.IsNullOrWhiteSpace(currentOptionAlias))
builder.AddOption(currentOptionAlias, currentOptionValues);
return new CommandLineInput(directives, arguments, options);
return builder.Build();
}
}
internal partial class CommandLineInput
{
public static CommandLineInput Empty { get; } =
new CommandLineInput(new string[0], new string[0], new CommandOptionInput[0]);
private static IReadOnlyList<CommandDirectiveInput> EmptyDirectives { get; } = new CommandDirectiveInput[0];
private static IReadOnlyList<CommandUnboundArgumentInput> EmptyUnboundArguments { get; } = new CommandUnboundArgumentInput[0];
private static IReadOnlyList<CommandOptionInput> EmptyOptions { get; } = new CommandOptionInput[0];
public static CommandLineInput Empty { get; } = new CommandLineInput(EmptyDirectives, EmptyUnboundArguments, EmptyOptions);
}
}

View File

@@ -0,0 +1,43 @@
using System.Collections.Generic;
namespace CliFx.Domain
{
internal class CommandLineInputBuilder
{
private readonly List<CommandDirectiveInput> _directives = new List<CommandDirectiveInput>();
private readonly List<CommandUnboundArgumentInput> _unboundArguments = new List<CommandUnboundArgumentInput>();
private readonly List<CommandOptionInput> _options = new List<CommandOptionInput>();
public CommandLineInputBuilder AddDirective(CommandDirectiveInput directive)
{
_directives.Add(directive);
return this;
}
public CommandLineInputBuilder AddDirective(string directive) =>
AddDirective(new CommandDirectiveInput(directive));
public CommandLineInputBuilder AddUnboundArgument(CommandUnboundArgumentInput unboundArgument)
{
_unboundArguments.Add(unboundArgument);
return this;
}
public CommandLineInputBuilder AddUnboundArgument(string unboundArgument) =>
AddUnboundArgument(new CommandUnboundArgumentInput(unboundArgument));
public CommandLineInputBuilder AddOption(CommandOptionInput option)
{
_options.Add(option);
return this;
}
public CommandLineInputBuilder AddOption(string optionAlias, IReadOnlyList<string> values) =>
AddOption(new CommandOptionInput(optionAlias, values));
public CommandLineInputBuilder AddOption(string optionAlias, params string[] values) =>
AddOption(optionAlias, (IReadOnlyList<string>) values);
public CommandLineInput Build() => new CommandLineInput(_directives, _unboundArguments, _options);
}
}

View File

@@ -10,22 +10,16 @@ namespace CliFx.Domain
public IReadOnlyList<string> Values { get; }
public bool IsHelpOption => CommandOptionSchema.HelpOption.MatchesNameOrShortName(Alias);
public bool IsVersionOption => CommandOptionSchema.VersionOption.MatchesNameOrShortName(Alias);
public CommandOptionInput(string alias, IReadOnlyList<string> values)
{
Alias = alias;
Values = values;
}
public CommandOptionInput(string alias, string value)
: this(alias, new[] {value})
{
}
public CommandOptionInput(string alias)
: this(alias, new string[0])
{
}
public override string ToString()
{
var buffer = new StringBuilder();

View File

@@ -0,0 +1,14 @@
namespace CliFx.Domain
{
internal class CommandUnboundArgumentInput
{
public string Value { get; }
public CommandUnboundArgumentInput(string value)
{
Value = value;
}
public override string ToString() => Value;
}
}

View File

@@ -1,13 +1,21 @@
using System;
using System.Linq;
using CliFx.Domain;
using CliFx.Internal;
namespace CliFx
namespace CliFx.Domain
{
public partial class CliApplication
internal class HelpTextWriter
{
private void RenderHelp(ApplicationSchema applicationSchema, CommandSchema command)
private readonly ApplicationMetadata _metadata;
private readonly IConsole _console;
public HelpTextWriter(ApplicationMetadata metadata, IConsole console)
{
_metadata = metadata;
_console = console;
}
public void Write(ApplicationSchema applicationSchema, CommandSchema command)
{
var column = 0;
var row = 0;

View File

@@ -1,4 +1,6 @@
#if NET45 || NETSTANDARD2_0
// ReSharper disable CheckNamespace
#if NET45 || NETSTANDARD2_0
using System.Collections.Generic;
using System.Text;

View File

@@ -9,78 +9,10 @@ namespace CliFx
/// Does not leak to system console in any way.
/// Use this class as a substitute for system console when running tests.
/// </summary>
public partial class VirtualConsole
{
private readonly MemoryStream _inputStream = new MemoryStream();
private readonly MemoryStream _outputStream = new MemoryStream();
private readonly MemoryStream _errorStream = new MemoryStream();
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
/// <summary>
/// Initializes an instance of <see cref="VirtualConsole"/>.
/// </summary>
public VirtualConsole(bool isRedirected)
{
Input = new StreamReader(_inputStream, Console.InputEncoding, false);
Output = new StreamWriter(_outputStream, Console.OutputEncoding) {AutoFlush = true};
Error = new StreamWriter(_errorStream, Console.OutputEncoding) {AutoFlush = true};
IsInputRedirected = isRedirected;
IsOutputRedirected = isRedirected;
IsErrorRedirected = isRedirected;
}
/// <summary>
/// Initializes an instance of <see cref="VirtualConsole"/>.
/// </summary>
public VirtualConsole()
: this(true)
{
}
/// <summary>
/// Writes raw data to input stream.
/// </summary>
public void WriteInputData(byte[] data) => _inputStream.Write(data, 0, data.Length);
/// <summary>
/// Writes text to input stream.
/// </summary>
public void WriteInputString(string str) => WriteInputData(Input.CurrentEncoding.GetBytes(str));
/// <summary>
/// Reads all data written to output stream thus far.
/// </summary>
public byte[] ReadOutputData() => _outputStream.ToArray();
/// <summary>
/// Reads all text written to output stream thus far.
/// </summary>
public string ReadOutputString() => Output.Encoding.GetString(ReadOutputData());
/// <summary>
/// Reads all data written to error stream thus far.
/// </summary>
public byte[] ReadErrorData() => _errorStream.ToArray();
/// <summary>
/// Reads all text written to error stream thus far.
/// </summary>
public string ReadErrorString() => Error.Encoding.GetString(ReadErrorData());
/// <summary>
/// Sends an interrupt signal.
/// </summary>
public void Cancel() => _cts.Cancel();
/// <summary>
/// Sends an interrupt signal after a delay.
/// </summary>
public void CancelAfter(TimeSpan delay) => _cts.CancelAfter(delay);
}
public partial class VirtualConsole : IConsole
{
private readonly CancellationToken _cancellationToken;
/// <inheritdoc />
public StreamReader Input { get; }
@@ -113,21 +45,55 @@ namespace CliFx
}
/// <inheritdoc />
public CancellationToken GetCancellationToken() => _cts.Token;
}
public CancellationToken GetCancellationToken() => _cancellationToken;
public partial class VirtualConsole : IDisposable
{
/// <inheritdoc />
public void Dispose()
/// <summary>
/// Initializes an instance of <see cref="VirtualConsole"/>.
/// Use named parameters to specify the streams you want to override.
/// </summary>
public VirtualConsole(
StreamReader? input = null, bool isInputRedirected = true,
StreamWriter? output = null, bool isOutputRedirected = true,
StreamWriter? error = null, bool isErrorRedirected = true,
CancellationToken cancellationToken = default)
{
Input = input ?? StreamReader.Null;
IsInputRedirected = isInputRedirected;
Output = output ?? StreamWriter.Null;
IsOutputRedirected = isOutputRedirected;
Error = error ?? StreamWriter.Null;
IsErrorRedirected = isErrorRedirected;
_cancellationToken = cancellationToken;
}
/// <summary>
/// Initializes an instance of <see cref="VirtualConsole"/>.
/// Use named parameters to specify the streams you want to override.
/// </summary>
public VirtualConsole(
Stream? input = null, bool isInputRedirected = true,
Stream? output = null, bool isOutputRedirected = true,
Stream? error = null, bool isErrorRedirected = true,
CancellationToken cancellationToken = default)
: this(
WrapInput(input), isInputRedirected,
WrapOutput(output), isOutputRedirected,
WrapOutput(error), isErrorRedirected,
cancellationToken)
{
_inputStream.Dispose();
_outputStream.Dispose();
_errorStream.Dispose();
_cts.Dispose();
Input.Dispose();
Output.Dispose();
Error.Dispose();
}
}
public partial class VirtualConsole
{
private static StreamReader WrapInput(Stream? stream) =>
stream != null
? new StreamReader(stream, Console.InputEncoding, false)
: StreamReader.Null;
private static StreamWriter WrapOutput(Stream? stream) =>
stream != null
? new StreamWriter(stream, Console.OutputEncoding) {AutoFlush = true}
: StreamWriter.Null;
}
}