mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6d2359d6b | ||
|
|
0d32876bad | ||
|
|
c063251d89 | ||
|
|
3831cfc7c0 | ||
|
|
b17341b56c | ||
|
|
5bda964fb5 | ||
|
|
432430489a | ||
|
|
9a20101f30 | ||
|
|
b491818779 | ||
|
|
69c24c8dfc | ||
|
|
004f906148 | ||
|
|
ac83233dc2 | ||
|
|
082910c968 |
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 24 KiB |
15
Changelog.md
15
Changelog.md
@@ -1,3 +1,18 @@
|
||||
### v1.4 (20-Aug-2020)
|
||||
|
||||
- Added `VirtualConsole.CreateBuffered()` method to simplify test setup when using in-memory backing stores for output and error streams. Please refer to the readme for updated recommendations on how to test applications built with CliFx.
|
||||
- Added generic `CliApplicationBuilder.AddCommand<TCommand>()`. This overload simplifies adding commands one-by-one as it also checks that the type implements `ICommand`.
|
||||
|
||||
### v1.3.2 (31-Jul-2020)
|
||||
|
||||
- Fixed an issue where a command was incorrectly allowed to execute when the user did not specify any value for a non-scalar parameter. Since they are always required, a parameter needs to be bound to (at least) one value. (Thanks [@Daniel Hix](https://github.com/ADustyOldMuffin))
|
||||
- Fixed an issue where `CliApplication.RunAsync(...)` threw `ArgumentException` if there were two environment variables, whose names differed only in case. Environment variable names are now treated case-sensitively. (Thanks [@Ron Myers](https://github.com/ron-myers))
|
||||
|
||||
### v1.3.1 (19-Jul-2020)
|
||||
|
||||
- Running the application with the debug directive (`myapp [debug]`) will now also try to launch a debugger instance. In most cases it will save time as you won't need to attach the debugger manually. (Thanks [@Volodymyr Shkolka](https://github.com/BlackGad))
|
||||
- Fixed an issue where unhandled generic exceptions (i.e. not `CommandException`) sometimes caused the application to incorrectly return successful exit code due to an overflow issue on Unix systems. Starting from this version, all unhandled generic exceptions will produce `1` as the exit code when thrown. Instances of `CommandException` can still be configured to return any specified exit code, but it's recommended to constrain the values between `1` and `255` to avoid overflow issues. (Thanks [@Ihor Nechyporuk](https://github.com/inech))
|
||||
|
||||
### v1.3 (23-May-2020)
|
||||
|
||||
- Changed analyzers to report errors instead of warnings. If you find that some analyzer works incorrectly, please report it on GitHub. You can also configure inspection severity overrides in your project if you need to.
|
||||
|
||||
@@ -206,7 +206,7 @@ namespace CliFx.Analyzers
|
||||
// Duplicate environment variable name
|
||||
var duplicateEnvironmentVariableNameOptions = options
|
||||
.Where(p => !string.IsNullOrWhiteSpace(p.EnvironmentVariableName))
|
||||
.GroupBy(p => p.EnvironmentVariableName, StringComparer.OrdinalIgnoreCase)
|
||||
.GroupBy(p => p.EnvironmentVariableName, StringComparer.Ordinal)
|
||||
.Where(g => g.Count() > 1)
|
||||
.SelectMany(g => g.AsEnumerable())
|
||||
.ToArray();
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace CliFx.Benchmarks
|
||||
|
||||
[Benchmark(Description = "CliFx", Baseline = true)]
|
||||
public async ValueTask<int> ExecuteWithCliFx() =>
|
||||
await new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments, new Dictionary<string, string>());
|
||||
await new CliApplicationBuilder().AddCommand<CliFxCommand>().Build().RunAsync(Arguments, new Dictionary<string, string>());
|
||||
|
||||
[Benchmark(Description = "System.CommandLine")]
|
||||
public async Task<int> ExecuteWithSystemCommandLine() =>
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class ApplicationSpecs
|
||||
{
|
||||
[Command]
|
||||
private class DefaultCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class AnotherDefaultCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class NonImplementedCommand
|
||||
{
|
||||
}
|
||||
|
||||
private class NonAnnotatedCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("dup")]
|
||||
private class DuplicateNameCommandA : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("dup")]
|
||||
private class DuplicateNameCommandB : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class DuplicateParameterOrderCommand : ICommand
|
||||
{
|
||||
[CommandParameter(13)]
|
||||
public string? ParameterA { get; set; }
|
||||
|
||||
[CommandParameter(13)]
|
||||
public string? ParameterB { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class DuplicateParameterNameCommand : ICommand
|
||||
{
|
||||
[CommandParameter(0, Name = "param")]
|
||||
public string? ParameterA { get; set; }
|
||||
|
||||
[CommandParameter(1, Name = "param")]
|
||||
public string? ParameterB { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class MultipleNonScalarParametersCommand : ICommand
|
||||
{
|
||||
[CommandParameter(0)]
|
||||
public IReadOnlyList<string>? ParameterA { get; set; }
|
||||
|
||||
[CommandParameter(1)]
|
||||
public IReadOnlyList<string>? ParameterB { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class NonLastNonScalarParameterCommand : ICommand
|
||||
{
|
||||
[CommandParameter(0)]
|
||||
public IReadOnlyList<string>? ParameterA { get; set; }
|
||||
|
||||
[CommandParameter(1)]
|
||||
public string? ParameterB { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class EmptyOptionNameCommand : ICommand
|
||||
{
|
||||
[CommandOption("")]
|
||||
public string? Apples { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class SingleCharacterOptionNameCommand : ICommand
|
||||
{
|
||||
[CommandOption("a")]
|
||||
public string? Apples { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class DuplicateOptionNamesCommand : ICommand
|
||||
{
|
||||
[CommandOption("fruits")]
|
||||
public string? Apples { get; set; }
|
||||
|
||||
[CommandOption("fruits")]
|
||||
public string? Oranges { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class DuplicateOptionShortNamesCommand : ICommand
|
||||
{
|
||||
[CommandOption('x')]
|
||||
public string? OptionA { get; set; }
|
||||
|
||||
[CommandOption('x')]
|
||||
public string? OptionB { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class ConflictWithHelpOptionCommand : ICommand
|
||||
{
|
||||
[CommandOption("option-h", 'h')]
|
||||
public string? OptionH { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class ConflictWithVersionOptionCommand : ICommand
|
||||
{
|
||||
[CommandOption("version")]
|
||||
public string? Version { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class DuplicateOptionEnvironmentVariableNamesCommand : ICommand
|
||||
{
|
||||
[CommandOption("option-a", EnvironmentVariableName = "ENV_VAR")]
|
||||
public string? OptionA { get; set; }
|
||||
|
||||
[CommandOption("option-b", EnvironmentVariableName = "ENV_VAR")]
|
||||
public string? OptionB { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("hidden", Description = "Description")]
|
||||
private class HiddenPropertiesCommand : ICommand
|
||||
{
|
||||
[CommandParameter(13, Name = "param", Description = "Param description")]
|
||||
public string? Parameter { get; set; }
|
||||
|
||||
[CommandOption("option", 'o', Description = "Option description", EnvironmentVariableName = "ENV")]
|
||||
public string? Option { get; set; }
|
||||
|
||||
public string? HiddenA { get; set; }
|
||||
|
||||
public bool? HiddenB { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using CliFx.Domain;
|
||||
using CliFx.Exceptions;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Tests.Commands;
|
||||
using CliFx.Tests.Commands.Invalid;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class ApplicationSpecs
|
||||
public class ApplicationSpecs
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
@@ -31,7 +32,7 @@ namespace CliFx.Tests
|
||||
{
|
||||
// Act
|
||||
var app = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(DefaultCommand))
|
||||
.AddCommand<DefaultCommand>()
|
||||
.AddCommandsFrom(typeof(DefaultCommand).Assembly)
|
||||
.AddCommands(new[] {typeof(DefaultCommand)})
|
||||
.AddCommandsFrom(new[] {typeof(DefaultCommand).Assembly})
|
||||
@@ -51,219 +52,324 @@ namespace CliFx.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void At_least_one_command_must_be_defined_in_an_application()
|
||||
public async Task At_least_one_command_must_be_defined_in_an_application()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = Array.Empty<Type>();
|
||||
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Commands_must_implement_the_corresponding_interface()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = new[] {typeof(NonImplementedCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Commands_must_be_annotated_by_an_attribute()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = new[] {typeof(NonAnnotatedCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Commands_must_have_unique_names()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = new[] {typeof(DuplicateNameCommandA), typeof(DuplicateNameCommandB)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Command_can_be_default_but_only_if_it_is_the_only_such_command()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = new[] {typeof(DefaultCommand), typeof(AnotherDefaultCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Command_parameters_must_have_unique_order()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = new[] {typeof(DuplicateParameterOrderCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Command_parameters_must_have_unique_names()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = new[] {typeof(DuplicateParameterNameCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Command_parameter_can_be_non_scalar_only_if_no_other_such_parameter_is_present()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = new[] {typeof(MultipleNonScalarParametersCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Command_parameter_can_be_non_scalar_only_if_it_is_the_last_in_order()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = new[] {typeof(NonLastNonScalarParameterCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Command_options_must_have_names_that_are_not_empty()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = new[] {typeof(EmptyOptionNameCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Command_options_must_have_names_that_are_longer_than_one_character()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = new[] {typeof(SingleCharacterOptionNameCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Command_options_must_have_unique_names()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = new[] {typeof(DuplicateOptionNamesCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Command_options_must_have_unique_short_names()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = new[] {typeof(DuplicateOptionShortNamesCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Command_options_must_not_have_conflicts_with_the_implicit_help_option()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = new[] {typeof(ConflictWithHelpOptionCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Command_options_must_not_have_conflicts_with_the_implicit_version_option()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = new[] {typeof(ConflictWithVersionOptionCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Command_options_must_have_unique_environment_variable_names()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = new[] {typeof(DuplicateOptionEnvironmentVariableNamesCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Command_options_and_parameters_must_be_annotated_by_corresponding_attributes()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = new[] {typeof(HiddenPropertiesCommand)};
|
||||
var application = new CliApplicationBuilder()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var schema = RootSchema.Resolve(commandTypes);
|
||||
var exitCode = await application.RunAsync(Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
schema.Should().BeEquivalentTo(new RootSchema(new[]
|
||||
{
|
||||
new CommandSchema(
|
||||
typeof(HiddenPropertiesCommand),
|
||||
"hidden",
|
||||
"Description",
|
||||
new[]
|
||||
{
|
||||
new CommandParameterSchema(
|
||||
typeof(HiddenPropertiesCommand).GetProperty(nameof(HiddenPropertiesCommand.Parameter))!,
|
||||
13,
|
||||
"param",
|
||||
"Param description")
|
||||
},
|
||||
new[]
|
||||
{
|
||||
new CommandOptionSchema(
|
||||
typeof(HiddenPropertiesCommand).GetProperty(nameof(HiddenPropertiesCommand.Option))!,
|
||||
"option",
|
||||
'o',
|
||||
"ENV",
|
||||
false,
|
||||
"Option description"),
|
||||
CommandOptionSchema.HelpOption
|
||||
})
|
||||
}));
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
schema.ToString().Should().NotBeNullOrWhiteSpace(); // this is only for coverage, I'm sorry
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Commands_must_implement_the_corresponding_interface()
|
||||
{
|
||||
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(NonImplementedCommand))
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Commands_must_be_annotated_by_an_attribute()
|
||||
{
|
||||
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<NonAnnotatedCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Commands_must_have_unique_names()
|
||||
{
|
||||
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<GenericExceptionCommand>()
|
||||
.AddCommand<CommandExceptionCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Command_can_be_default_but_only_if_it_is_the_only_such_command()
|
||||
{
|
||||
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<DefaultCommand>()
|
||||
.AddCommand<OtherDefaultCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Command_parameters_must_have_unique_order()
|
||||
{
|
||||
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<DuplicateParameterOrderCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Command_parameters_must_have_unique_names()
|
||||
{
|
||||
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<DuplicateParameterNameCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Command_parameter_can_be_non_scalar_only_if_no_other_such_parameter_is_present()
|
||||
{
|
||||
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<MultipleNonScalarParametersCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Command_parameter_can_be_non_scalar_only_if_it_is_the_last_in_order()
|
||||
{
|
||||
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<NonLastNonScalarParameterCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Command_options_must_have_names_that_are_not_empty()
|
||||
{
|
||||
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<EmptyOptionNameCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Command_options_must_have_names_that_are_longer_than_one_character()
|
||||
{
|
||||
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<SingleCharacterOptionNameCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Command_options_must_have_unique_names()
|
||||
{
|
||||
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<DuplicateOptionNamesCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Command_options_must_have_unique_short_names()
|
||||
{
|
||||
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<DuplicateOptionShortNamesCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Command_options_must_have_unique_environment_variable_names()
|
||||
{
|
||||
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<DuplicateOptionEnvironmentVariableNamesCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Command_options_must_not_have_conflicts_with_the_implicit_help_option()
|
||||
{
|
||||
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<ConflictWithHelpOptionCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Command_options_must_not_have_conflicts_with_the_implicit_version_option()
|
||||
{
|
||||
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<ConflictWithVersionOptionCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class ArgumentBindingSpecs
|
||||
{
|
||||
[Command]
|
||||
private class AllSupportedTypesCommand : ICommand
|
||||
{
|
||||
[CommandOption(nameof(Object))]
|
||||
public object? Object { get; set; } = 42;
|
||||
|
||||
[CommandOption(nameof(String))]
|
||||
public string? String { get; set; } = "foo bar";
|
||||
|
||||
[CommandOption(nameof(Bool))]
|
||||
public bool Bool { get; set; }
|
||||
|
||||
[CommandOption(nameof(Char))]
|
||||
public char Char { get; set; }
|
||||
|
||||
[CommandOption(nameof(Sbyte))]
|
||||
public sbyte Sbyte { get; set; }
|
||||
|
||||
[CommandOption(nameof(Byte))]
|
||||
public byte Byte { get; set; }
|
||||
|
||||
[CommandOption(nameof(Short))]
|
||||
public short Short { get; set; }
|
||||
|
||||
[CommandOption(nameof(Ushort))]
|
||||
public ushort Ushort { get; set; }
|
||||
|
||||
[CommandOption(nameof(Int))]
|
||||
public int Int { get; set; }
|
||||
|
||||
[CommandOption(nameof(Uint))]
|
||||
public uint Uint { get; set; }
|
||||
|
||||
[CommandOption(nameof(Long))]
|
||||
public long Long { get; set; }
|
||||
|
||||
[CommandOption(nameof(Ulong))]
|
||||
public ulong Ulong { get; set; }
|
||||
|
||||
[CommandOption(nameof(Float))]
|
||||
public float Float { get; set; }
|
||||
|
||||
[CommandOption(nameof(Double))]
|
||||
public double Double { get; set; }
|
||||
|
||||
[CommandOption(nameof(Decimal))]
|
||||
public decimal Decimal { get; set; }
|
||||
|
||||
[CommandOption(nameof(DateTime))]
|
||||
public DateTime DateTime { get; set; }
|
||||
|
||||
[CommandOption(nameof(DateTimeOffset))]
|
||||
public DateTimeOffset DateTimeOffset { get; set; }
|
||||
|
||||
[CommandOption(nameof(TimeSpan))]
|
||||
public TimeSpan TimeSpan { get; set; }
|
||||
|
||||
[CommandOption(nameof(CustomEnum))]
|
||||
public CustomEnum CustomEnum { get; set; }
|
||||
|
||||
[CommandOption(nameof(IntNullable))]
|
||||
public int? IntNullable { get; set; }
|
||||
|
||||
[CommandOption(nameof(CustomEnumNullable))]
|
||||
public CustomEnum? CustomEnumNullable { get; set; }
|
||||
|
||||
[CommandOption(nameof(TimeSpanNullable))]
|
||||
public TimeSpan? TimeSpanNullable { get; set; }
|
||||
|
||||
[CommandOption(nameof(TestStringConstructable))]
|
||||
public StringConstructable? TestStringConstructable { get; set; }
|
||||
|
||||
[CommandOption(nameof(TestStringParseable))]
|
||||
public StringParseable? TestStringParseable { get; set; }
|
||||
|
||||
[CommandOption(nameof(TestStringParseableWithFormatProvider))]
|
||||
public StringParseableWithFormatProvider? TestStringParseableWithFormatProvider { get; set; }
|
||||
|
||||
[CommandOption(nameof(ObjectArray))]
|
||||
public object[]? ObjectArray { get; set; }
|
||||
|
||||
[CommandOption(nameof(StringArray))]
|
||||
public string[]? StringArray { get; set; }
|
||||
|
||||
[CommandOption(nameof(IntArray))]
|
||||
public int[]? IntArray { get; set; }
|
||||
|
||||
[CommandOption(nameof(CustomEnumArray))]
|
||||
public CustomEnum[]? CustomEnumArray { get; set; }
|
||||
|
||||
[CommandOption(nameof(IntNullableArray))]
|
||||
public int?[]? IntNullableArray { get; set; }
|
||||
|
||||
[CommandOption(nameof(TestStringConstructableArray))]
|
||||
public StringConstructable[]? TestStringConstructableArray { get; set; }
|
||||
|
||||
[CommandOption(nameof(Enumerable))]
|
||||
public IEnumerable? Enumerable { get; set; }
|
||||
|
||||
[CommandOption(nameof(StringEnumerable))]
|
||||
public IEnumerable<string>? StringEnumerable { get; set; }
|
||||
|
||||
[CommandOption(nameof(StringReadOnlyList))]
|
||||
public IReadOnlyList<string>? StringReadOnlyList { get; set; }
|
||||
|
||||
[CommandOption(nameof(StringList))]
|
||||
public List<string>? StringList { get; set; }
|
||||
|
||||
[CommandOption(nameof(StringHashSet))]
|
||||
public HashSet<string>? StringHashSet { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class ArrayOptionCommand : ICommand
|
||||
{
|
||||
[CommandOption("option", 'o')]
|
||||
public IReadOnlyList<string>? Option { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class RequiredOptionCommand : ICommand
|
||||
{
|
||||
[CommandOption(nameof(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;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class NoParameterCommand : ICommand
|
||||
{
|
||||
[CommandOption(nameof(OptionA))]
|
||||
public string? OptionA { get; set; }
|
||||
|
||||
[CommandOption(nameof(OptionB))]
|
||||
public string? OptionB { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class ArgumentBindingSpecs
|
||||
{
|
||||
private enum CustomEnum
|
||||
{
|
||||
Value1 = 1,
|
||||
Value2 = 2,
|
||||
Value3 = 3
|
||||
}
|
||||
|
||||
private class StringConstructable
|
||||
{
|
||||
public string Value { get; }
|
||||
|
||||
public StringConstructable(string value) => Value = value;
|
||||
}
|
||||
|
||||
private class StringParseable
|
||||
{
|
||||
public string Value { get; }
|
||||
|
||||
private StringParseable(string value) => Value = value;
|
||||
|
||||
public static StringParseable Parse(string value) => new StringParseable(value);
|
||||
}
|
||||
|
||||
private class StringParseableWithFormatProvider
|
||||
{
|
||||
public string Value { get; }
|
||||
|
||||
private StringParseableWithFormatProvider(string value) => Value = value;
|
||||
|
||||
public static StringParseableWithFormatProvider Parse(string value, IFormatProvider formatProvider) =>
|
||||
new StringParseableWithFormatProvider(value + " " + formatProvider);
|
||||
}
|
||||
|
||||
private class DummyType
|
||||
{
|
||||
}
|
||||
|
||||
public class CustomEnumerable<T> : IEnumerable<T>
|
||||
{
|
||||
public IEnumerator<T> GetEnumerator() => ((IEnumerable<T>) Array.Empty<T>()).GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
1352
CliFx.Tests/ArgumentConversionSpecs.cs
Normal file
1352
CliFx.Tests/ArgumentConversionSpecs.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,378 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Domain;
|
||||
using CliFx.Tests.Internal;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public class ArgumentSyntaxSpecs
|
||||
{
|
||||
[Fact]
|
||||
public void Input_is_empty_if_no_arguments_are_provided()
|
||||
{
|
||||
// Arrange
|
||||
var arguments = Array.Empty<string>();
|
||||
var commandNames = Array.Empty<string>();
|
||||
|
||||
// Act
|
||||
var input = CommandInput.Parse(arguments, commandNames);
|
||||
|
||||
// Assert
|
||||
input.Should().BeEquivalentTo(CommandInput.Empty);
|
||||
}
|
||||
|
||||
public static object[][] DirectivesTestData => new[]
|
||||
{
|
||||
new object[]
|
||||
{
|
||||
new[] {"[preview]"},
|
||||
new CommandInputBuilder()
|
||||
.AddDirective("preview")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"[preview]", "[debug]"},
|
||||
new CommandInputBuilder()
|
||||
.AddDirective("preview")
|
||||
.AddDirective("debug")
|
||||
.Build()
|
||||
}
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(DirectivesTestData))]
|
||||
internal void Directive_can_be_enabled_by_specifying_its_name_in_square_brackets(IReadOnlyList<string> arguments, CommandInput expectedInput)
|
||||
{
|
||||
// Arrange
|
||||
var commandNames = Array.Empty<string>();
|
||||
|
||||
// Act
|
||||
var input = CommandInput.Parse(arguments, commandNames);
|
||||
|
||||
// Assert
|
||||
input.Should().BeEquivalentTo(expectedInput);
|
||||
}
|
||||
|
||||
public static object[][] OptionsTestData => new[]
|
||||
{
|
||||
new object[]
|
||||
{
|
||||
new[] {"--option"},
|
||||
new CommandInputBuilder()
|
||||
.AddOption("option")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"--option", "value"},
|
||||
new CommandInputBuilder()
|
||||
.AddOption("option", "value")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"--option", "value1", "value2"},
|
||||
new CommandInputBuilder()
|
||||
.AddOption("option", "value1", "value2")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"--option", "same value"},
|
||||
new CommandInputBuilder()
|
||||
.AddOption("option", "same value")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"--option1", "--option2"},
|
||||
new CommandInputBuilder()
|
||||
.AddOption("option1")
|
||||
.AddOption("option2")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"--option1", "value1", "--option2", "value2"},
|
||||
new CommandInputBuilder()
|
||||
.AddOption("option1", "value1")
|
||||
.AddOption("option2", "value2")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"--option1", "value1", "value2", "--option2", "value3", "value4"},
|
||||
new CommandInputBuilder()
|
||||
.AddOption("option1", "value1", "value2")
|
||||
.AddOption("option2", "value3", "value4")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"--option1", "value1", "value2", "--option2"},
|
||||
new CommandInputBuilder()
|
||||
.AddOption("option1", "value1", "value2")
|
||||
.AddOption("option2")
|
||||
.Build()
|
||||
}
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(OptionsTestData))]
|
||||
internal void Option_can_be_set_by_specifying_its_name_after_two_dashes(IReadOnlyList<string> arguments, CommandInput expectedInput)
|
||||
{
|
||||
// Arrange
|
||||
var commandNames = Array.Empty<string>();
|
||||
|
||||
// Act
|
||||
var input = CommandInput.Parse(arguments, commandNames);
|
||||
|
||||
// Assert
|
||||
input.Should().BeEquivalentTo(expectedInput);
|
||||
}
|
||||
|
||||
public static object[][] ShortOptionsTestData => new[]
|
||||
{
|
||||
new object[]
|
||||
{
|
||||
new[] {"-o"},
|
||||
new CommandInputBuilder()
|
||||
.AddOption("o")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"-o", "value"},
|
||||
new CommandInputBuilder()
|
||||
.AddOption("o", "value")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"-o", "value1", "value2"},
|
||||
new CommandInputBuilder()
|
||||
.AddOption("o", "value1", "value2")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"-o", "same value"},
|
||||
new CommandInputBuilder()
|
||||
.AddOption("o", "same value")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"-a", "-b"},
|
||||
new CommandInputBuilder()
|
||||
.AddOption("a")
|
||||
.AddOption("b")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"-a", "value1", "-b", "value2"},
|
||||
new CommandInputBuilder()
|
||||
.AddOption("a", "value1")
|
||||
.AddOption("b", "value2")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"-a", "value1", "value2", "-b", "value3", "value4"},
|
||||
new CommandInputBuilder()
|
||||
.AddOption("a", "value1", "value2")
|
||||
.AddOption("b", "value3", "value4")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"-a", "value1", "value2", "-b"},
|
||||
new CommandInputBuilder()
|
||||
.AddOption("a", "value1", "value2")
|
||||
.AddOption("b")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"-abc"},
|
||||
new CommandInputBuilder()
|
||||
.AddOption("a")
|
||||
.AddOption("b")
|
||||
.AddOption("c")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"-abc", "value"},
|
||||
new CommandInputBuilder()
|
||||
.AddOption("a")
|
||||
.AddOption("b")
|
||||
.AddOption("c", "value")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"-abc", "value1", "value2"},
|
||||
new CommandInputBuilder()
|
||||
.AddOption("a")
|
||||
.AddOption("b")
|
||||
.AddOption("c", "value1", "value2")
|
||||
.Build()
|
||||
}
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ShortOptionsTestData))]
|
||||
internal void Option_can_be_set_by_specifying_its_short_name_after_a_single_dash(IReadOnlyList<string> arguments, CommandInput expectedInput)
|
||||
{
|
||||
// Arrange
|
||||
var commandNames = Array.Empty<string>();
|
||||
|
||||
// Act
|
||||
var input = CommandInput.Parse(arguments, commandNames);
|
||||
|
||||
// Assert
|
||||
input.Should().BeEquivalentTo(expectedInput);
|
||||
}
|
||||
|
||||
public static object[][] ParametersTestData => new[]
|
||||
{
|
||||
new object[]
|
||||
{
|
||||
new[] {"foo"},
|
||||
new CommandInputBuilder()
|
||||
.AddParameter("foo")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"foo", "bar"},
|
||||
new CommandInputBuilder()
|
||||
.AddParameter("foo")
|
||||
.AddParameter("bar")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"[preview]", "foo"},
|
||||
new CommandInputBuilder()
|
||||
.AddDirective("preview")
|
||||
.AddParameter("foo")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"foo", "--option", "value", "-abc"},
|
||||
new CommandInputBuilder()
|
||||
.AddParameter("foo")
|
||||
.AddOption("option", "value")
|
||||
.AddOption("a")
|
||||
.AddOption("b")
|
||||
.AddOption("c")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"[preview]", "[debug]", "foo", "bar", "--option", "value", "-abc"},
|
||||
new CommandInputBuilder()
|
||||
.AddDirective("preview")
|
||||
.AddDirective("debug")
|
||||
.AddParameter("foo")
|
||||
.AddParameter("bar")
|
||||
.AddOption("option", "value")
|
||||
.AddOption("a")
|
||||
.AddOption("b")
|
||||
.AddOption("c")
|
||||
.Build()
|
||||
}
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ParametersTestData))]
|
||||
internal void Parameter_can_be_set_by_specifying_the_value_directly(IReadOnlyList<string> arguments, CommandInput expectedInput)
|
||||
{
|
||||
// Arrange
|
||||
var commandNames = Array.Empty<string>();
|
||||
|
||||
// Act
|
||||
var input = CommandInput.Parse(arguments, commandNames);
|
||||
|
||||
// Assert
|
||||
input.Should().BeEquivalentTo(expectedInput);
|
||||
}
|
||||
|
||||
public static object[][] CommandNameTestData => new[]
|
||||
{
|
||||
new object[]
|
||||
{
|
||||
new[] {"cmd"},
|
||||
new[] {"cmd"},
|
||||
new CommandInputBuilder()
|
||||
.SetCommandName("cmd")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"cmd"},
|
||||
new[] {"cmd", "foo", "bar", "-o", "value"},
|
||||
new CommandInputBuilder()
|
||||
.SetCommandName("cmd")
|
||||
.AddParameter("foo")
|
||||
.AddParameter("bar")
|
||||
.AddOption("o", "value")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"cmd", "cmd sub"},
|
||||
new[] {"cmd", "sub", "foo"},
|
||||
new CommandInputBuilder()
|
||||
.SetCommandName("cmd sub")
|
||||
.AddParameter("foo")
|
||||
.Build()
|
||||
}
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(CommandNameTestData))]
|
||||
internal void Command_name_is_matched_from_arguments_that_come_before_parameters(
|
||||
IReadOnlyList<string> commandNames,
|
||||
IReadOnlyList<string> arguments,
|
||||
CommandInput expectedInput)
|
||||
{
|
||||
// Act
|
||||
var input = CommandInput.Parse(arguments, commandNames);
|
||||
|
||||
// Assert
|
||||
input.Should().BeEquivalentTo(expectedInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class CancellationSpecs
|
||||
{
|
||||
[Command("cancel")]
|
||||
private class CancellableCommand : ICommand
|
||||
{
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(3), console.GetCancellationToken());
|
||||
console.Output.WriteLine("Never printed");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
console.Output.WriteLine("Cancellation requested");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Tests.Commands;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class CancellationSpecs
|
||||
public class CancellationSpecs
|
||||
{
|
||||
[Fact]
|
||||
public async Task Command_can_perform_additional_cleanup_if_cancellation_is_requested()
|
||||
@@ -17,27 +16,21 @@ namespace CliFx.Tests
|
||||
|
||||
// Arrange
|
||||
using var cts = new CancellationTokenSource();
|
||||
|
||||
await using var stdOut = new MemoryStream();
|
||||
var console = new VirtualConsole(output: stdOut, cancellationToken: cts.Token);
|
||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered(cts.Token);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(CancellableCommand))
|
||||
.AddCommand<CancellableCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(0.2));
|
||||
|
||||
var exitCode = await application.RunAsync(
|
||||
new[] {"cancel"},
|
||||
new Dictionary<string, string>());
|
||||
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
|
||||
var exitCode = await application.RunAsync(new[] {"cmd"});
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdOutData.Should().Be("Cancellation requested");
|
||||
stdOut.GetString().Trim().Should().Be(CancellableCommand.CancellationOutputText);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
<PackageReference Include="FluentAssertions" Version="5.10.2" />
|
||||
<PackageReference Include="GitHubActionsTestLogger" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
|
||||
<PackageReference Include="coverlet.msbuild" Version="2.8.0" PrivateAssets="all" />
|
||||
|
||||
31
CliFx.Tests/Commands/CancellableCommand.cs
Normal file
31
CliFx.Tests/Commands/CancellableCommand.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands
|
||||
{
|
||||
[Command("cmd")]
|
||||
public class CancellableCommand : ICommand
|
||||
{
|
||||
public const string CompletionOutputText = "Finished";
|
||||
public const string CancellationOutputText = "Canceled";
|
||||
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(
|
||||
TimeSpan.FromSeconds(3),
|
||||
console.GetCancellationToken()
|
||||
);
|
||||
|
||||
console.Output.WriteLine(CompletionOutputText);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
console.Output.WriteLine(CancellationOutputText);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
CliFx.Tests/Commands/CommandExceptionCommand.cs
Normal file
21
CliFx.Tests/Commands/CommandExceptionCommand.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Exceptions;
|
||||
|
||||
namespace CliFx.Tests.Commands
|
||||
{
|
||||
[Command("cmd")]
|
||||
public class CommandExceptionCommand : ICommand
|
||||
{
|
||||
[CommandOption("code", 'c')]
|
||||
public int ExitCode { get; set; } = 133;
|
||||
|
||||
[CommandOption("msg", 'm')]
|
||||
public string? Message { get; set; }
|
||||
|
||||
[CommandOption("show-help")]
|
||||
public bool ShowHelp { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode, ShowHelp);
|
||||
}
|
||||
}
|
||||
17
CliFx.Tests/Commands/DefaultCommand.cs
Normal file
17
CliFx.Tests/Commands/DefaultCommand.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands
|
||||
{
|
||||
[Command(Description = "Default command description")]
|
||||
public class DefaultCommand : ICommand
|
||||
{
|
||||
public const string ExpectedOutputText = nameof(DefaultCommand);
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.Output.WriteLine(ExpectedOutputText);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
CliFx.Tests/Commands/GenericExceptionCommand.cs
Normal file
15
CliFx.Tests/Commands/GenericExceptionCommand.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands
|
||||
{
|
||||
[Command("cmd")]
|
||||
public class GenericExceptionCommand : ICommand
|
||||
{
|
||||
[CommandOption("msg", 'm')]
|
||||
public string? Message { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => throw new Exception(Message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands.Invalid
|
||||
{
|
||||
[Command("cmd")]
|
||||
public class ConflictWithHelpOptionCommand : SelfSerializeCommandBase
|
||||
{
|
||||
[CommandOption("option-h", 'h')]
|
||||
public string? OptionH { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands.Invalid
|
||||
{
|
||||
// Must be default because version option is available only on default commands
|
||||
[Command]
|
||||
public class ConflictWithVersionOptionCommand : SelfSerializeCommandBase
|
||||
{
|
||||
[CommandOption("version")]
|
||||
public string? Version { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands.Invalid
|
||||
{
|
||||
[Command("cmd")]
|
||||
public class DuplicateOptionEnvironmentVariableNamesCommand : SelfSerializeCommandBase
|
||||
{
|
||||
[CommandOption("option-a", EnvironmentVariableName = "ENV_VAR")]
|
||||
public string? OptionA { get; set; }
|
||||
|
||||
[CommandOption("option-b", EnvironmentVariableName = "ENV_VAR")]
|
||||
public string? OptionB { get; set; }
|
||||
}
|
||||
}
|
||||
14
CliFx.Tests/Commands/Invalid/DuplicateOptionNamesCommand.cs
Normal file
14
CliFx.Tests/Commands/Invalid/DuplicateOptionNamesCommand.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands.Invalid
|
||||
{
|
||||
[Command("cmd")]
|
||||
public class DuplicateOptionNamesCommand : SelfSerializeCommandBase
|
||||
{
|
||||
[CommandOption("fruits")]
|
||||
public string? Apples { get; set; }
|
||||
|
||||
[CommandOption("fruits")]
|
||||
public string? Oranges { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands.Invalid
|
||||
{
|
||||
[Command("cmd")]
|
||||
public class DuplicateOptionShortNamesCommand : SelfSerializeCommandBase
|
||||
{
|
||||
[CommandOption('x')]
|
||||
public string? OptionA { get; set; }
|
||||
|
||||
[CommandOption('x')]
|
||||
public string? OptionB { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands.Invalid
|
||||
{
|
||||
[Command("cmd")]
|
||||
public class DuplicateParameterNameCommand : SelfSerializeCommandBase
|
||||
{
|
||||
[CommandParameter(0, Name = "param")]
|
||||
public string? ParamA { get; set; }
|
||||
|
||||
[CommandParameter(1, Name = "param")]
|
||||
public string? ParamB { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands.Invalid
|
||||
{
|
||||
[Command("cmd")]
|
||||
public class DuplicateParameterOrderCommand : SelfSerializeCommandBase
|
||||
{
|
||||
[CommandParameter(13)]
|
||||
public string? ParamA { get; set; }
|
||||
|
||||
[CommandParameter(13)]
|
||||
public string? ParamB { get; set; }
|
||||
}
|
||||
}
|
||||
11
CliFx.Tests/Commands/Invalid/EmptyOptionNameCommand.cs
Normal file
11
CliFx.Tests/Commands/Invalid/EmptyOptionNameCommand.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands.Invalid
|
||||
{
|
||||
[Command("cmd")]
|
||||
public class EmptyOptionNameCommand : SelfSerializeCommandBase
|
||||
{
|
||||
[CommandOption("")]
|
||||
public string? Apples { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands.Invalid
|
||||
{
|
||||
[Command("cmd")]
|
||||
public class MultipleNonScalarParametersCommand : SelfSerializeCommandBase
|
||||
{
|
||||
[CommandParameter(0)]
|
||||
public IReadOnlyList<string>? ParamA { get; set; }
|
||||
|
||||
[CommandParameter(1)]
|
||||
public IReadOnlyList<string>? ParamB { get; set; }
|
||||
}
|
||||
}
|
||||
6
CliFx.Tests/Commands/Invalid/NonAnnotatedCommand.cs
Normal file
6
CliFx.Tests/Commands/Invalid/NonAnnotatedCommand.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace CliFx.Tests.Commands.Invalid
|
||||
{
|
||||
public class NonAnnotatedCommand : SelfSerializeCommandBase
|
||||
{
|
||||
}
|
||||
}
|
||||
9
CliFx.Tests/Commands/Invalid/NonImplementedCommand.cs
Normal file
9
CliFx.Tests/Commands/Invalid/NonImplementedCommand.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands.Invalid
|
||||
{
|
||||
[Command]
|
||||
public class NonImplementedCommand
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands.Invalid
|
||||
{
|
||||
[Command("cmd")]
|
||||
public class NonLastNonScalarParameterCommand : SelfSerializeCommandBase
|
||||
{
|
||||
[CommandParameter(0)]
|
||||
public IReadOnlyList<string>? ParamA { get; set; }
|
||||
|
||||
[CommandParameter(1)]
|
||||
public string? ParamB { get; set; }
|
||||
}
|
||||
}
|
||||
9
CliFx.Tests/Commands/Invalid/OtherDefaultCommand.cs
Normal file
9
CliFx.Tests/Commands/Invalid/OtherDefaultCommand.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands.Invalid
|
||||
{
|
||||
[Command]
|
||||
public class OtherDefaultCommand : SelfSerializeCommandBase
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands.Invalid
|
||||
{
|
||||
[Command("cmd")]
|
||||
public class SingleCharacterOptionNameCommand : SelfSerializeCommandBase
|
||||
{
|
||||
[CommandOption("a")]
|
||||
public string? Apples { get; set; }
|
||||
}
|
||||
}
|
||||
17
CliFx.Tests/Commands/NamedCommand.cs
Normal file
17
CliFx.Tests/Commands/NamedCommand.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands
|
||||
{
|
||||
[Command("named", Description = "Named command description")]
|
||||
public class NamedCommand : ICommand
|
||||
{
|
||||
public const string ExpectedOutputText = nameof(NamedCommand);
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.Output.WriteLine(ExpectedOutputText);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
17
CliFx.Tests/Commands/NamedSubCommand.cs
Normal file
17
CliFx.Tests/Commands/NamedSubCommand.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands
|
||||
{
|
||||
[Command("named sub", Description = "Named sub command description")]
|
||||
public class NamedSubCommand : ICommand
|
||||
{
|
||||
public const string ExpectedOutputText = nameof(NamedSubCommand);
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.Output.WriteLine(ExpectedOutputText);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
CliFx.Tests/Commands/SelfSerializeCommandBase.cs
Normal file
14
CliFx.Tests/Commands/SelfSerializeCommandBase.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CliFx.Tests.Commands
|
||||
{
|
||||
public abstract class SelfSerializeCommandBase : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.Output.WriteLine(JsonConvert.SerializeObject(this));
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
155
CliFx.Tests/Commands/SupportedArgumentTypesCommand.cs
Normal file
155
CliFx.Tests/Commands/SupportedArgumentTypesCommand.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Attributes;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CliFx.Tests.Commands
|
||||
{
|
||||
[Command("cmd")]
|
||||
public partial class SupportedArgumentTypesCommand : SelfSerializeCommandBase
|
||||
{
|
||||
[CommandOption("obj")]
|
||||
public object? Object { get; set; } = 42;
|
||||
|
||||
[CommandOption("str")]
|
||||
public string? String { get; set; } = "foo bar";
|
||||
|
||||
[CommandOption("bool")]
|
||||
public bool Bool { get; set; }
|
||||
|
||||
[CommandOption("char")]
|
||||
public char Char { get; set; }
|
||||
|
||||
[CommandOption("sbyte")]
|
||||
public sbyte Sbyte { get; set; }
|
||||
|
||||
[CommandOption("byte")]
|
||||
public byte Byte { get; set; }
|
||||
|
||||
[CommandOption("short")]
|
||||
public short Short { get; set; }
|
||||
|
||||
[CommandOption("ushort")]
|
||||
public ushort Ushort { get; set; }
|
||||
|
||||
[CommandOption("int")]
|
||||
public int Int { get; set; }
|
||||
|
||||
[CommandOption("uint")]
|
||||
public uint Uint { get; set; }
|
||||
|
||||
[CommandOption("long")]
|
||||
public long Long { get; set; }
|
||||
|
||||
[CommandOption("ulong")]
|
||||
public ulong Ulong { get; set; }
|
||||
|
||||
[CommandOption("float")]
|
||||
public float Float { get; set; }
|
||||
|
||||
[CommandOption("double")]
|
||||
public double Double { get; set; }
|
||||
|
||||
[CommandOption("decimal")]
|
||||
public decimal Decimal { get; set; }
|
||||
|
||||
[CommandOption("datetime")]
|
||||
public DateTime DateTime { get; set; }
|
||||
|
||||
[CommandOption("datetime-offset")]
|
||||
public DateTimeOffset DateTimeOffset { get; set; }
|
||||
|
||||
[CommandOption("timespan")]
|
||||
public TimeSpan TimeSpan { get; set; }
|
||||
|
||||
[CommandOption("enum")]
|
||||
public CustomEnum Enum { get; set; }
|
||||
|
||||
[CommandOption("int-nullable")]
|
||||
public int? IntNullable { get; set; }
|
||||
|
||||
[CommandOption("enum-nullable")]
|
||||
public CustomEnum? EnumNullable { get; set; }
|
||||
|
||||
[CommandOption("timespan-nullable")]
|
||||
public TimeSpan? TimeSpanNullable { get; set; }
|
||||
|
||||
[CommandOption("str-constructible")]
|
||||
public CustomStringConstructible? StringConstructible { get; set; }
|
||||
|
||||
[CommandOption("str-parseable")]
|
||||
public CustomStringParseable? StringParseable { get; set; }
|
||||
|
||||
[CommandOption("str-parseable-format")]
|
||||
public CustomStringParseableWithFormatProvider? StringParseableWithFormatProvider { get; set; }
|
||||
|
||||
[CommandOption("obj-array")]
|
||||
public object[]? ObjectArray { get; set; }
|
||||
|
||||
[CommandOption("str-array")]
|
||||
public string[]? StringArray { get; set; }
|
||||
|
||||
[CommandOption("int-array")]
|
||||
public int[]? IntArray { get; set; }
|
||||
|
||||
[CommandOption("enum-array")]
|
||||
public CustomEnum[]? EnumArray { get; set; }
|
||||
|
||||
[CommandOption("int-nullable-array")]
|
||||
public int?[]? IntNullableArray { get; set; }
|
||||
|
||||
[CommandOption("str-constructible-array")]
|
||||
public CustomStringConstructible[]? StringConstructibleArray { get; set; }
|
||||
|
||||
[CommandOption("str-enumerable")]
|
||||
public IEnumerable<string>? StringEnumerable { get; set; }
|
||||
|
||||
[CommandOption("str-read-only-list")]
|
||||
public IReadOnlyList<string>? StringReadOnlyList { get; set; }
|
||||
|
||||
[CommandOption("str-list")]
|
||||
public List<string>? StringList { get; set; }
|
||||
|
||||
[CommandOption("str-set")]
|
||||
public HashSet<string>? StringHashSet { get; set; }
|
||||
}
|
||||
|
||||
public partial class SupportedArgumentTypesCommand
|
||||
{
|
||||
public enum CustomEnum
|
||||
{
|
||||
Value1 = 1,
|
||||
Value2 = 2,
|
||||
Value3 = 3
|
||||
}
|
||||
|
||||
public class CustomStringConstructible
|
||||
{
|
||||
public string Value { get; }
|
||||
|
||||
public CustomStringConstructible(string value) => Value = value;
|
||||
}
|
||||
|
||||
public class CustomStringParseable
|
||||
{
|
||||
public string Value { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
private CustomStringParseable(string value) => Value = value;
|
||||
|
||||
public static CustomStringParseable Parse(string value) => new CustomStringParseable(value);
|
||||
}
|
||||
|
||||
public class CustomStringParseableWithFormatProvider
|
||||
{
|
||||
public string Value { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
private CustomStringParseableWithFormatProvider(string value) => Value = value;
|
||||
|
||||
public static CustomStringParseableWithFormatProvider Parse(string value, IFormatProvider formatProvider) =>
|
||||
new CustomStringParseableWithFormatProvider(value + " " + formatProvider);
|
||||
}
|
||||
}
|
||||
}
|
||||
31
CliFx.Tests/Commands/UnsupportedArgumentTypesCommand.cs
Normal file
31
CliFx.Tests/Commands/UnsupportedArgumentTypesCommand.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands
|
||||
{
|
||||
[Command("cmd")]
|
||||
public partial class UnsupportedArgumentTypesCommand : SelfSerializeCommandBase
|
||||
{
|
||||
[CommandOption("str-non-initializable")]
|
||||
public CustomType? StringNonInitializable { get; set; }
|
||||
|
||||
[CommandOption("str-enumerable-non-initializable")]
|
||||
public CustomEnumerable<string>? StringEnumerableNonInitializable { get; set; }
|
||||
}
|
||||
|
||||
public partial class UnsupportedArgumentTypesCommand
|
||||
{
|
||||
public class CustomType
|
||||
{
|
||||
}
|
||||
|
||||
public class CustomEnumerable<T> : IEnumerable<T>
|
||||
{
|
||||
public IEnumerator<T> GetEnumerator() => ((IEnumerable<T>) Array.Empty<T>()).GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
||||
44
CliFx.Tests/Commands/WithDefaultValuesCommand.cs
Normal file
44
CliFx.Tests/Commands/WithDefaultValuesCommand.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands
|
||||
{
|
||||
[Command("cmd")]
|
||||
public class WithDefaultValuesCommand : SelfSerializeCommandBase
|
||||
{
|
||||
public enum CustomEnum { Value1, Value2, Value3 };
|
||||
|
||||
[CommandOption("obj")]
|
||||
public object? Object { get; set; } = 42;
|
||||
|
||||
[CommandOption("str")]
|
||||
public string? String { get; set; } = "foo";
|
||||
|
||||
[CommandOption("str-empty")]
|
||||
public string StringEmpty { get; set; } = "";
|
||||
|
||||
[CommandOption("str-array")]
|
||||
public string[]? StringArray { get; set; } = { "foo", "bar", "baz" };
|
||||
|
||||
[CommandOption("bool")]
|
||||
public bool Bool { get; set; } = true;
|
||||
|
||||
[CommandOption("char")]
|
||||
public char Char { get; set; } = 't';
|
||||
|
||||
[CommandOption("int")]
|
||||
public int Int { get; set; } = 1337;
|
||||
|
||||
[CommandOption("int-nullable")]
|
||||
public int? IntNullable { get; set; } = 1337;
|
||||
|
||||
[CommandOption("int-array")]
|
||||
public int[]? IntArray { get; set; } = { 1, 2, 3 };
|
||||
|
||||
[CommandOption("timespan")]
|
||||
public TimeSpan TimeSpan { get; set; } = TimeSpan.FromMinutes(123);
|
||||
|
||||
[CommandOption("enum")]
|
||||
public CustomEnum Enum { get; set; } = CustomEnum.Value2;
|
||||
}
|
||||
}
|
||||
28
CliFx.Tests/Commands/WithDependenciesCommand.cs
Normal file
28
CliFx.Tests/Commands/WithDependenciesCommand.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands
|
||||
{
|
||||
[Command("cmd")]
|
||||
public class WithDependenciesCommand : ICommand
|
||||
{
|
||||
public class DependencyA
|
||||
{
|
||||
}
|
||||
|
||||
public class DependencyB
|
||||
{
|
||||
}
|
||||
|
||||
private readonly DependencyA _dependencyA;
|
||||
private readonly DependencyB _dependencyB;
|
||||
|
||||
public WithDependenciesCommand(DependencyA dependencyA, DependencyB dependencyB)
|
||||
{
|
||||
_dependencyA = dependencyA;
|
||||
_dependencyB = dependencyB;
|
||||
}
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
}
|
||||
19
CliFx.Tests/Commands/WithEnumArgumentsCommand.cs
Normal file
19
CliFx.Tests/Commands/WithEnumArgumentsCommand.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands
|
||||
{
|
||||
[Command("cmd")]
|
||||
public class WithEnumArgumentsCommand : SelfSerializeCommandBase
|
||||
{
|
||||
public enum CustomEnum { Value1, Value2, Value3 };
|
||||
|
||||
[CommandParameter(0, Name = "enum")]
|
||||
public CustomEnum EnumParameter { get; set; }
|
||||
|
||||
[CommandOption("enum")]
|
||||
public CustomEnum? EnumOption { get; set; }
|
||||
|
||||
[CommandOption("required-enum", IsRequired = true)]
|
||||
public CustomEnum RequiredEnumOption { get; set; }
|
||||
}
|
||||
}
|
||||
15
CliFx.Tests/Commands/WithEnvironmentVariablesCommand.cs
Normal file
15
CliFx.Tests/Commands/WithEnvironmentVariablesCommand.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands
|
||||
{
|
||||
[Command("cmd")]
|
||||
public class WithEnvironmentVariablesCommand : SelfSerializeCommandBase
|
||||
{
|
||||
[CommandOption("opt-a", 'a', EnvironmentVariableName = "ENV_OPT_A")]
|
||||
public string? OptA { get; set; }
|
||||
|
||||
[CommandOption("opt-b", 'b', EnvironmentVariableName = "ENV_OPT_B")]
|
||||
public IReadOnlyList<string>? OptB { get; set; }
|
||||
}
|
||||
}
|
||||
18
CliFx.Tests/Commands/WithParametersCommand.cs
Normal file
18
CliFx.Tests/Commands/WithParametersCommand.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands
|
||||
{
|
||||
[Command("cmd")]
|
||||
public class WithParametersCommand : SelfSerializeCommandBase
|
||||
{
|
||||
[CommandParameter(0)]
|
||||
public string? ParamA { get; set; }
|
||||
|
||||
[CommandParameter(1)]
|
||||
public int? ParamB { get; set; }
|
||||
|
||||
[CommandParameter(2)]
|
||||
public IReadOnlyList<string>? ParamC { get; set; }
|
||||
}
|
||||
}
|
||||
18
CliFx.Tests/Commands/WithRequiredOptionsCommand.cs
Normal file
18
CliFx.Tests/Commands/WithRequiredOptionsCommand.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands
|
||||
{
|
||||
[Command("cmd")]
|
||||
public class WithRequiredOptionsCommand : SelfSerializeCommandBase
|
||||
{
|
||||
[CommandOption("opt-a", 'a', IsRequired = true)]
|
||||
public string? OptA { get; set; }
|
||||
|
||||
[CommandOption("opt-b", 'b')]
|
||||
public int? OptB { get; set; }
|
||||
|
||||
[CommandOption("opt-c", 'c', IsRequired = true)]
|
||||
public IReadOnlyList<char>? OptC { get; set; }
|
||||
}
|
||||
}
|
||||
11
CliFx.Tests/Commands/WithSingleParameterCommand.cs
Normal file
11
CliFx.Tests/Commands/WithSingleParameterCommand.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands
|
||||
{
|
||||
[Command("cmd")]
|
||||
public class WithSingleParameterCommand : SelfSerializeCommandBase
|
||||
{
|
||||
[CommandParameter(0)]
|
||||
public string? ParamA { get; set; }
|
||||
}
|
||||
}
|
||||
14
CliFx.Tests/Commands/WithSingleRequiredOptionCommand.cs
Normal file
14
CliFx.Tests/Commands/WithSingleRequiredOptionCommand.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands
|
||||
{
|
||||
[Command("cmd")]
|
||||
public class WithSingleRequiredOptionCommand : SelfSerializeCommandBase
|
||||
{
|
||||
[CommandOption("opt-a")]
|
||||
public string? OptA { get; set; }
|
||||
|
||||
[CommandOption("opt-b", IsRequired = true)]
|
||||
public string? OptB { get; set; }
|
||||
}
|
||||
}
|
||||
12
CliFx.Tests/Commands/WithStringArrayOptionCommand.cs
Normal file
12
CliFx.Tests/Commands/WithStringArrayOptionCommand.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests.Commands
|
||||
{
|
||||
[Command("cmd")]
|
||||
public class WithStringArrayOptionCommand : SelfSerializeCommandBase
|
||||
{
|
||||
[CommandOption("opt", 'o')]
|
||||
public IReadOnlyList<string>? Opt { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,8 @@ namespace CliFx.Tests
|
||||
var console = new VirtualConsole(
|
||||
input: stdIn,
|
||||
output: stdOut,
|
||||
error: stdErr);
|
||||
error: stdErr
|
||||
);
|
||||
|
||||
// Act
|
||||
console.Output.Write("output");
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class DependencyInjectionSpecs
|
||||
{
|
||||
[Command]
|
||||
private class WithoutDependenciesCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
private class DependencyA
|
||||
{
|
||||
}
|
||||
|
||||
private class DependencyB
|
||||
{
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class WithDependenciesCommand : ICommand
|
||||
{
|
||||
private readonly DependencyA _dependencyA;
|
||||
private readonly DependencyB _dependencyB;
|
||||
|
||||
public WithDependenciesCommand(DependencyA dependencyA, DependencyB dependencyB)
|
||||
{
|
||||
_dependencyA = dependencyA;
|
||||
_dependencyB = dependencyB;
|
||||
}
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Tests.Commands;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class DependencyInjectionSpecs
|
||||
public class DependencyInjectionSpecs
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
@@ -18,10 +19,10 @@ namespace CliFx.Tests
|
||||
var activator = new DefaultTypeActivator();
|
||||
|
||||
// Act
|
||||
var obj = activator.CreateInstance(typeof(WithoutDependenciesCommand));
|
||||
var obj = activator.CreateInstance(typeof(DefaultCommand));
|
||||
|
||||
// Assert
|
||||
obj.Should().BeOfType<WithoutDependenciesCommand>();
|
||||
obj.Should().BeOfType<DefaultCommand>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -40,7 +41,10 @@ namespace CliFx.Tests
|
||||
{
|
||||
// Arrange
|
||||
var activator = new DelegateTypeActivator(_ =>
|
||||
new WithDependenciesCommand(new DependencyA(), new DependencyB()));
|
||||
new WithDependenciesCommand(
|
||||
new WithDependenciesCommand.DependencyA(),
|
||||
new WithDependenciesCommand.DependencyB())
|
||||
);
|
||||
|
||||
// Act
|
||||
var obj = activator.CreateInstance(typeof(WithDependenciesCommand));
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class DirectivesSpecs
|
||||
{
|
||||
[Command("cmd")]
|
||||
private class NamedCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,66 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Tests.Internal;
|
||||
using CliWrap;
|
||||
using CliWrap.Buffered;
|
||||
using CliFx.Tests.Commands;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class DirectivesSpecs
|
||||
public class DirectivesSpecs
|
||||
{
|
||||
[Fact]
|
||||
public async Task Debug_directive_can_be_specified_to_have_the_application_wait_until_debugger_is_attached()
|
||||
{
|
||||
// We can't actually attach a debugger in tests, so instead just cancel execution after some time
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
// Arrange
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1));
|
||||
var stdOut = new StringBuilder();
|
||||
|
||||
var command = Cli.Wrap("dotnet")
|
||||
.WithArguments(a => a
|
||||
.Add(Dummy.Program.Location)
|
||||
.Add("[debug]"))
|
||||
.WithEnvironmentVariables(e => e
|
||||
.Set("ENV_TARGET", "Mars")) | stdOut;
|
||||
|
||||
// Act
|
||||
await command.ExecuteAsync(cts.Token).Task.IgnoreCancellation();
|
||||
var stdOutData = stdOut.ToString();
|
||||
|
||||
// Assert
|
||||
stdOutData.Should().Contain("Attach debugger to");
|
||||
}
|
||||
public DirectivesSpecs(ITestOutputHelper output) => _output = output;
|
||||
|
||||
[Fact]
|
||||
public async Task Preview_directive_can_be_specified_to_print_provided_arguments_as_they_were_parsed()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
var console = new VirtualConsole(output: stdOut);
|
||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(NamedCommand))
|
||||
.AddCommand<NamedCommand>()
|
||||
.UseConsole(console)
|
||||
.AllowPreviewMode()
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
new[] {"[preview]", "cmd", "param", "-abc", "--option", "foo"},
|
||||
new[] {"[preview]", "named", "param", "-abc", "--option", "foo"},
|
||||
new Dictionary<string, string>());
|
||||
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
stdOutData.Should().ContainAll("cmd", "<param>", "[-a]", "[-b]", "[-c]", "[--option \"foo\"]");
|
||||
stdOut.GetString().Should().ContainAll(
|
||||
"named", "<param>", "[-a]", "[-b]", "[-c]", "[--option \"foo\"]"
|
||||
);
|
||||
|
||||
_output.WriteLine(stdOut.GetString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class EnvironmentVariablesSpecs
|
||||
{
|
||||
[Command]
|
||||
private class EnvironmentVariableCollectionCommand : ICommand
|
||||
{
|
||||
[CommandOption("opt", EnvironmentVariableName = "ENV_OPT")]
|
||||
public IReadOnlyList<string>? Option { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class EnvironmentVariableCommand : ICommand
|
||||
{
|
||||
[CommandOption("opt", EnvironmentVariableName = "ENV_OPT")]
|
||||
public string? Option { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Domain;
|
||||
using CliFx.Tests.Commands;
|
||||
using CliFx.Tests.Internal;
|
||||
using CliWrap;
|
||||
using CliWrap.Buffered;
|
||||
@@ -10,11 +10,11 @@ using Xunit;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class EnvironmentVariablesSpecs
|
||||
public class EnvironmentVariablesSpecs
|
||||
{
|
||||
// This test uses a real application to make sure environment variables are actually read correctly
|
||||
[Fact]
|
||||
public async Task Option_can_use_a_specific_environment_variable_as_fallback()
|
||||
public async Task Option_can_use_an_environment_variable_as_fallback()
|
||||
{
|
||||
// Arrange
|
||||
var command = Cli.Wrap("dotnet")
|
||||
@@ -27,12 +27,12 @@ namespace CliFx.Tests
|
||||
var stdOut = await command.ExecuteBufferedAsync().Select(r => r.StandardOutput);
|
||||
|
||||
// Assert
|
||||
stdOut.TrimEnd().Should().Be("Hello Mars!");
|
||||
stdOut.Trim().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()
|
||||
public async Task Option_only_uses_an_environment_variable_as_fallback_if_the_value_is_not_directly_provided()
|
||||
{
|
||||
// Arrange
|
||||
var command = Cli.Wrap("dotnet")
|
||||
@@ -47,46 +47,94 @@ namespace CliFx.Tests
|
||||
var stdOut = await command.ExecuteBufferedAsync().Select(r => r.StandardOutput);
|
||||
|
||||
// Assert
|
||||
stdOut.TrimEnd().Should().Be("Hello Jupiter!");
|
||||
stdOut.Trim().Should().Be("Hello Jupiter!");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Option_of_non_scalar_type_can_take_multiple_separated_values_from_an_environment_variable()
|
||||
public async Task Option_only_uses_an_environment_variable_as_fallback_if_the_name_matches_case_sensitively()
|
||||
{
|
||||
// Arrange
|
||||
var input = CommandInput.Empty;
|
||||
var envVars = new Dictionary<string, string>
|
||||
{
|
||||
["ENV_OPT"] = $"foo{Path.PathSeparator}bar"
|
||||
};
|
||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<WithEnvironmentVariablesCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var instance = CommandHelper.ResolveCommand<EnvironmentVariableCollectionCommand>(input, envVars);
|
||||
var exitCode = await application.RunAsync(
|
||||
new[] {"cmd"},
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["ENV_opt_A"] = "incorrect",
|
||||
["ENV_OPT_A"] = "correct"
|
||||
}
|
||||
);
|
||||
|
||||
var commandInstance = stdOut.GetString().DeserializeJson<WithEnvironmentVariablesCommand>();
|
||||
|
||||
// Assert
|
||||
instance.Should().BeEquivalentTo(new EnvironmentVariableCollectionCommand
|
||||
exitCode.Should().Be(0);
|
||||
commandInstance.Should().BeEquivalentTo(new WithEnvironmentVariablesCommand
|
||||
{
|
||||
Option = new[] {"foo", "bar"}
|
||||
OptA = "correct"
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Option_of_scalar_type_can_only_take_a_single_value_from_an_environment_variable_even_if_it_contains_separators()
|
||||
public async Task Option_of_non_scalar_type_can_use_an_environment_variable_as_fallback_and_extract_multiple_values()
|
||||
{
|
||||
// Arrange
|
||||
var input = CommandInput.Empty;
|
||||
var envVars = new Dictionary<string, string>
|
||||
{
|
||||
["ENV_OPT"] = $"foo{Path.PathSeparator}bar"
|
||||
};
|
||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<WithEnvironmentVariablesCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var instance = CommandHelper.ResolveCommand<EnvironmentVariableCommand>(input, envVars);
|
||||
var exitCode = await application.RunAsync(
|
||||
new[] {"cmd"},
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["ENV_OPT_B"] = $"foo{Path.PathSeparator}bar"
|
||||
}
|
||||
);
|
||||
|
||||
var commandInstance = stdOut.GetString().DeserializeJson<WithEnvironmentVariablesCommand>();
|
||||
|
||||
// Assert
|
||||
instance.Should().BeEquivalentTo(new EnvironmentVariableCommand
|
||||
exitCode.Should().Be(0);
|
||||
commandInstance.Should().BeEquivalentTo(new WithEnvironmentVariablesCommand
|
||||
{
|
||||
Option = $"foo{Path.PathSeparator}bar"
|
||||
OptB = new[] {"foo", "bar"}
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Option_of_scalar_type_can_use_an_environment_variable_as_fallback_regardless_of_separators()
|
||||
{
|
||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<WithEnvironmentVariablesCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
new[] {"cmd"},
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["ENV_OPT_A"] = $"foo{Path.PathSeparator}bar"
|
||||
}
|
||||
);
|
||||
|
||||
var commandInstance = stdOut.GetString().DeserializeJson<WithEnvironmentVariablesCommand>();
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
commandInstance.Should().BeEquivalentTo(new WithEnvironmentVariablesCommand
|
||||
{
|
||||
OptA = $"foo{Path.PathSeparator}bar"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Exceptions;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class ErrorReportingSpecs
|
||||
{
|
||||
[Command("exc")]
|
||||
private class GenericExceptionCommand : ICommand
|
||||
{
|
||||
[CommandOption("msg", 'm')]
|
||||
public string? Message { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => throw new Exception(Message);
|
||||
}
|
||||
|
||||
[Command("exc")]
|
||||
private class CommandExceptionCommand : ICommand
|
||||
{
|
||||
[CommandOption("code", 'c')]
|
||||
public int ExitCode { get; set; } = 1337;
|
||||
|
||||
[CommandOption("msg", 'm')]
|
||||
public string? Message { get; set; }
|
||||
|
||||
[CommandOption("show-help")]
|
||||
public bool ShowHelp { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode, ShowHelp);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Tests.Commands;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class ErrorReportingSpecs
|
||||
public class ErrorReportingSpecs
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
@@ -17,156 +16,131 @@ namespace CliFx.Tests
|
||||
public async Task Command_may_throw_a_generic_exception_which_exits_and_prints_error_message_and_stack_trace()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdErr = new MemoryStream();
|
||||
var console = new VirtualConsole(error: stdErr);
|
||||
var (console, stdOut, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(GenericExceptionCommand))
|
||||
.AddCommand<GenericExceptionCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
new[] {"exc", "-m", "Kaput"},
|
||||
new Dictionary<string, string>());
|
||||
|
||||
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
|
||||
var exitCode = await application.RunAsync(new[] {"cmd", "-m", "Kaput"});
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErrData.Should().ContainAll(
|
||||
stdOut.GetString().Should().BeEmpty();
|
||||
stdErr.GetString().Should().ContainAll(
|
||||
"System.Exception:",
|
||||
"Kaput", "at",
|
||||
"CliFx.Tests");
|
||||
"CliFx.Tests"
|
||||
);
|
||||
|
||||
_output.WriteLine(stdErrData);
|
||||
_output.WriteLine(stdOut.GetString());
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
|
||||
[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 (console, stdOut, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(CommandExceptionCommand))
|
||||
.AddCommand<CommandExceptionCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
new[] {"exc", "-m", "Kaput", "-c", "69"},
|
||||
new Dictionary<string, string>());
|
||||
|
||||
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
|
||||
var exitCode = await application.RunAsync(new[] {"cmd", "-m", "Kaput", "-c", "69"});
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(69);
|
||||
stdErrData.Should().Be("Kaput");
|
||||
stdOut.GetString().Should().BeEmpty();
|
||||
stdErr.GetString().Trim().Should().Be("Kaput");
|
||||
|
||||
_output.WriteLine(stdErrData);
|
||||
_output.WriteLine(stdOut.GetString());
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
|
||||
[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 (console, stdOut, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(CommandExceptionCommand))
|
||||
.AddCommand<CommandExceptionCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
new[] {"exc"},
|
||||
new Dictionary<string, string>());
|
||||
|
||||
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
|
||||
var exitCode = await application.RunAsync(new[] {"cmd"});
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErrData.Should().ContainAll(
|
||||
stdOut.GetString().Should().BeEmpty();
|
||||
stdErr.GetString().Should().ContainAll(
|
||||
"CliFx.Exceptions.CommandException:",
|
||||
"at",
|
||||
"CliFx.Tests");
|
||||
"CliFx.Tests"
|
||||
);
|
||||
|
||||
_output.WriteLine(stdErrData);
|
||||
_output.WriteLine(stdOut.GetString());
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Command_may_throw_a_specialized_exception_which_exits_and_prints_help_text()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
await using var stdErr = new MemoryStream();
|
||||
|
||||
var console = new VirtualConsole(output: stdOut, error: stdErr);
|
||||
var (console, stdOut, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(CommandExceptionCommand))
|
||||
.AddCommand<CommandExceptionCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
new[] {"exc", "-m", "Kaput", "--show-help"},
|
||||
new Dictionary<string, string>());
|
||||
|
||||
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
|
||||
var exitCode = await application.RunAsync(new[] {"cmd", "-m", "Kaput", "--show-help"});
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErrData.Should().Be("Kaput");
|
||||
|
||||
stdOutData.Should().ContainAll(
|
||||
stdOut.GetString().Should().ContainAll(
|
||||
"Usage",
|
||||
"Options",
|
||||
"-h|--help", "Shows help text."
|
||||
"-h|--help"
|
||||
);
|
||||
stdErr.GetString().Trim().Should().Be("Kaput");
|
||||
|
||||
_output.WriteLine(stdErrData);
|
||||
_output.WriteLine(stdOutData);
|
||||
_output.WriteLine(stdOut.GetString());
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Command_shows_help_text_on_invalid_user_input()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
await using var stdErr = new MemoryStream();
|
||||
var console = new VirtualConsole(output: stdOut, error: stdErr);
|
||||
var (console, stdOut, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(CommandExceptionCommand))
|
||||
.AddCommand<DefaultCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
new[] {"not-a-valid-command", "-r", "foo"},
|
||||
new Dictionary<string, string>());
|
||||
|
||||
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
|
||||
var exitCode = await application.RunAsync(new[] {"not-a-valid-command", "-r", "foo"});
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErrData.Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
stdOutData.Should().ContainAll(
|
||||
stdOut.GetString().Should().ContainAll(
|
||||
"Usage",
|
||||
"[command]",
|
||||
"Options",
|
||||
"-h|--help", "Shows help text."
|
||||
"-h|--help"
|
||||
);
|
||||
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
_output.WriteLine(stdErrData);
|
||||
_output.WriteLine(stdOutData);
|
||||
_output.WriteLine(stdOut.GetString());
|
||||
_output.WriteLine(stdErr.GetString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class HelpTextSpecs
|
||||
{
|
||||
[Command(Description = "DefaultCommand description.")]
|
||||
private class DefaultCommand : ICommand
|
||||
{
|
||||
[CommandOption("option-a", 'a', Description = "OptionA description.")]
|
||||
public string? OptionA { get; set; }
|
||||
|
||||
[CommandOption("option-b", 'b', Description = "OptionB description.")]
|
||||
public string? OptionB { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("cmd", Description = "NamedCommand description.")]
|
||||
private class NamedCommand : ICommand
|
||||
{
|
||||
[CommandParameter(0, Name = "param-a", Description = "ParameterA description.")]
|
||||
public string? ParameterA { get; set; }
|
||||
|
||||
[CommandOption("option-c", 'c', Description = "OptionC description.")]
|
||||
public string? OptionC { get; set; }
|
||||
|
||||
[CommandOption("option-d", 'd', Description = "OptionD description.")]
|
||||
public string? OptionD { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("cmd sub", Description = "NamedSubCommand description.")]
|
||||
private class NamedSubCommand : ICommand
|
||||
{
|
||||
[CommandParameter(0, Name = "param-b", Description = "ParameterB description.")]
|
||||
public string? ParameterB { get; set; }
|
||||
|
||||
[CommandParameter(1, Name = "param-c", Description = "ParameterC description.")]
|
||||
public string? ParameterC { get; set; }
|
||||
|
||||
[CommandOption("option-e", 'e', Description = "OptionE description.")]
|
||||
public string? OptionE { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("cmd-with-params")]
|
||||
private class ParametersCommand : ICommand
|
||||
{
|
||||
[CommandParameter(0, Name = "first")]
|
||||
public string? ParameterA { get; set; }
|
||||
|
||||
[CommandParameter(10)]
|
||||
public int? ParameterB { get; set; }
|
||||
|
||||
[CommandParameter(20, Description = "A list of numbers", Name = "third list")]
|
||||
public IEnumerable<int>? ParameterC { get; set; }
|
||||
|
||||
[CommandOption("option", 'o')]
|
||||
public string? Option { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("cmd-with-req-opts")]
|
||||
private class RequiredOptionsCommand : ICommand
|
||||
{
|
||||
[CommandOption("option-a", 'a', IsRequired = true)]
|
||||
public string? OptionA { get; set; }
|
||||
|
||||
[CommandOption("option-b", 'b', IsRequired = true)]
|
||||
public IEnumerable<int>? OptionB { get; set; }
|
||||
|
||||
[CommandOption("option-c", 'c')]
|
||||
public string? OptionC { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("cmd-with-enum-args")]
|
||||
private class EnumArgumentsCommand : ICommand
|
||||
{
|
||||
public enum CustomEnum { Value1, Value2, Value3 };
|
||||
|
||||
[CommandParameter(0, Name = "value", Description = "Enum parameter.")]
|
||||
public CustomEnum ParamA { get; set; }
|
||||
|
||||
[CommandOption("value", Description = "Enum option.", IsRequired = true)]
|
||||
public CustomEnum OptionA { get; set; } = CustomEnum.Value1;
|
||||
|
||||
[CommandOption("nullable-value", Description = "Nullable enum option.")]
|
||||
public CustomEnum? OptionB { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("cmd-with-env-vars")]
|
||||
private class EnvironmentVariableCommand : ICommand
|
||||
{
|
||||
[CommandOption("option-a", 'a', IsRequired = true, EnvironmentVariableName = "ENV_OPT_A")]
|
||||
public string? OptionA { get; set; }
|
||||
|
||||
[CommandOption("option-b", 'b', EnvironmentVariableName = "ENV_OPT_B")]
|
||||
public string? OptionB { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("cmd-with-defaults")]
|
||||
private class ArgumentsWithDefaultValuesCommand : ICommand
|
||||
{
|
||||
public enum CustomEnum { Value1, Value2, Value3 };
|
||||
|
||||
[CommandOption(nameof(Object))]
|
||||
public object? Object { get; set; } = 42;
|
||||
|
||||
[CommandOption(nameof(String))]
|
||||
public string? String { get; set; } = "foo";
|
||||
|
||||
[CommandOption(nameof(EmptyString))]
|
||||
public string EmptyString { get; set; } = "";
|
||||
|
||||
[CommandOption(nameof(Bool))]
|
||||
public bool Bool { get; set; } = true;
|
||||
|
||||
[CommandOption(nameof(Char))]
|
||||
public char Char { get; set; } = 't';
|
||||
|
||||
[CommandOption(nameof(Int))]
|
||||
public int Int { get; set; } = 1337;
|
||||
|
||||
[CommandOption(nameof(TimeSpan))]
|
||||
public TimeSpan TimeSpan { get; set; } = TimeSpan.FromMinutes(123);
|
||||
|
||||
[CommandOption(nameof(Enum))]
|
||||
public CustomEnum Enum { get; set; } = CustomEnum.Value2;
|
||||
|
||||
[CommandOption(nameof(IntNullable))]
|
||||
public int? IntNullable { get; set; } = 1337;
|
||||
|
||||
[CommandOption(nameof(StringArray))]
|
||||
public string[]? StringArray { get; set; } = { "foo", "bar", "baz" };
|
||||
|
||||
[CommandOption(nameof(IntArray))]
|
||||
public int[]? IntArray { get; set; } = { 1, 2, 3 };
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,336 +1,153 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Tests.Commands;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class HelpTextSpecs
|
||||
public class HelpTextSpecs
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public HelpTextSpecs(ITestOutputHelper output) => _output = output;
|
||||
|
||||
[Fact]
|
||||
public async Task Version_information_can_be_requested_by_providing_the_version_option_without_other_arguments()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
var console = new VirtualConsole(output: stdOut);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(DefaultCommand))
|
||||
.AddCommand(typeof(NamedCommand))
|
||||
.AddCommand(typeof(NamedSubCommand))
|
||||
.UseVersionText("v6.9")
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(new[] {"--version"});
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
stdOutData.Should().Be("v6.9");
|
||||
|
||||
_output.WriteLine(stdOutData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Help_text_can_be_requested_by_providing_the_help_option()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
var console = new VirtualConsole(output: stdOut);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(DefaultCommand))
|
||||
.AddCommand(typeof(NamedCommand))
|
||||
.AddCommand(typeof(NamedSubCommand))
|
||||
.UseTitle("AppTitle")
|
||||
.UseVersionText("AppVer")
|
||||
.UseDescription("AppDesc")
|
||||
.UseExecutableName("AppExe")
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
await application.RunAsync(new[] {"--help"});
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
|
||||
|
||||
// Assert
|
||||
stdOutData.Should().ContainAll(
|
||||
"AppTitle", "AppVer",
|
||||
"AppDesc",
|
||||
"Usage",
|
||||
"AppExe", "[command]", "[options]",
|
||||
"Options",
|
||||
"-a|--option-a", "OptionA description.",
|
||||
"-b|--option-b", "OptionB description.",
|
||||
"-h|--help", "Shows help text.",
|
||||
"--version", "Shows version information.",
|
||||
"Commands",
|
||||
"cmd", "NamedCommand description.",
|
||||
"You can run", "to show help on a specific command."
|
||||
);
|
||||
|
||||
_output.WriteLine(stdOutData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Help_text_can_be_requested_on_a_specific_named_command()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
var console = new VirtualConsole(output: stdOut);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(DefaultCommand))
|
||||
.AddCommand(typeof(NamedCommand))
|
||||
.AddCommand(typeof(NamedSubCommand))
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
await application.RunAsync(new[] {"cmd", "--help"});
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
|
||||
|
||||
// Assert
|
||||
stdOutData.Should().ContainAll(
|
||||
"Description",
|
||||
"NamedCommand description.",
|
||||
"Usage",
|
||||
"cmd", "[command]", "<param-a>", "[options]",
|
||||
"Parameters",
|
||||
"* param-a", "ParameterA description.",
|
||||
"Options",
|
||||
"-c|--option-c", "OptionC description.",
|
||||
"-d|--option-d", "OptionD description.",
|
||||
"-h|--help", "Shows help text.",
|
||||
"Commands",
|
||||
"sub", "SubCommand description.",
|
||||
"You can run", "to show help on a specific command."
|
||||
);
|
||||
|
||||
_output.WriteLine(stdOutData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Help_text_can_be_requested_on_a_specific_named_sub_command()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
var console = new VirtualConsole(output: stdOut);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(DefaultCommand))
|
||||
.AddCommand(typeof(NamedCommand))
|
||||
.AddCommand(typeof(NamedSubCommand))
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
await application.RunAsync(new[] {"cmd", "sub", "--help"});
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
|
||||
|
||||
// Assert
|
||||
stdOutData.Should().ContainAll(
|
||||
"Description",
|
||||
"SubCommand description.",
|
||||
"Usage",
|
||||
"cmd sub", "<param-b>", "<param-c>", "[options]",
|
||||
"Parameters",
|
||||
"* param-b", "ParameterB description.",
|
||||
"* param-c", "ParameterC description.",
|
||||
"Options",
|
||||
"-e|--option-e", "OptionE description.",
|
||||
"-h|--help", "Shows help text."
|
||||
);
|
||||
|
||||
_output.WriteLine(stdOutData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Help_text_can_be_requested_without_specifying_command_even_if_default_command_is_not_defined()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
var console = new VirtualConsole(output: stdOut);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(NamedCommand))
|
||||
.AddCommand(typeof(NamedSubCommand))
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
await application.RunAsync(new[] {"--help"});
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
|
||||
|
||||
// Assert
|
||||
stdOutData.Should().ContainAll(
|
||||
"Usage",
|
||||
"[command]",
|
||||
"Options",
|
||||
"-h|--help", "Shows help text.",
|
||||
"--version", "Shows version information.",
|
||||
"Commands",
|
||||
"cmd", "NamedCommand description.",
|
||||
"You can run", "to show help on a specific command."
|
||||
);
|
||||
|
||||
_output.WriteLine(stdOutData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Help_text_shows_usage_format_which_lists_all_parameters()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
var console = new VirtualConsole(output: stdOut);
|
||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(ParametersCommand))
|
||||
.AddCommand<WithParametersCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
await application.RunAsync(new[] {"cmd-with-params", "--help"});
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
|
||||
var exitCode = await application.RunAsync(new[] {"cmd", "--help"});
|
||||
|
||||
// Assert
|
||||
stdOutData.Should().ContainAll(
|
||||
exitCode.Should().Be(0);
|
||||
stdOut.GetString().Should().ContainAll(
|
||||
"Usage",
|
||||
"cmd-with-params", "<first>", "<parameterb>", "<third list...>", "[options]"
|
||||
"cmd", "<parama>", "<paramb>", "<paramc...>"
|
||||
);
|
||||
|
||||
_output.WriteLine(stdOutData);
|
||||
_output.WriteLine(stdOut.GetString());
|
||||
}
|
||||
|
||||
[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 (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(RequiredOptionsCommand))
|
||||
.AddCommand<WithRequiredOptionsCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
await application.RunAsync(new[] {"cmd-with-req-opts", "--help"});
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
|
||||
var exitCode = await application.RunAsync(new[] {"cmd", "--help"});
|
||||
|
||||
// Assert
|
||||
stdOutData.Should().ContainAll(
|
||||
exitCode.Should().Be(0);
|
||||
stdOut.GetString().Should().ContainAll(
|
||||
"Usage",
|
||||
"cmd-with-req-opts", "--option-a <value>", "--option-b <values...>", "[options]",
|
||||
"cmd", "--opt-a <value>", "--opt-c <values...>", "[options]",
|
||||
"Options",
|
||||
"* -a|--option-a",
|
||||
"* -b|--option-b",
|
||||
"-c|--option-c"
|
||||
"* -a|--opt-a",
|
||||
"-b|--opt-b",
|
||||
"* -c|--opt-c"
|
||||
);
|
||||
|
||||
_output.WriteLine(stdOutData);
|
||||
_output.WriteLine(stdOut.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Help_text_lists_all_valid_values_for_enum_arguments()
|
||||
public async Task Help_text_shows_all_valid_values_for_enum_arguments()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
var console = new VirtualConsole(output: stdOut);
|
||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(EnumArgumentsCommand))
|
||||
.AddCommand<WithEnumArgumentsCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
await application.RunAsync(new[] {"cmd-with-enum-args", "--help"});
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
|
||||
var exitCode = await application.RunAsync(new[] {"cmd", "--help"});
|
||||
|
||||
// Assert
|
||||
stdOutData.Should().ContainAll(
|
||||
"Usage",
|
||||
"cmd-with-enum-args", "[options]",
|
||||
exitCode.Should().Be(0);
|
||||
stdOut.GetString().Should().ContainAll(
|
||||
"Parameters",
|
||||
"value", "Valid values: \"Value1\", \"Value2\", \"Value3\".",
|
||||
"enum", "Valid values: \"Value1\", \"Value2\", \"Value3\".",
|
||||
"Options",
|
||||
"* --value", "Enum option.", "Valid values: \"Value1\", \"Value2\", \"Value3\".",
|
||||
"--nullable-value", "Nullable enum option.", "Valid values: \"Value1\", \"Value2\", \"Value3\"."
|
||||
"--enum", "Valid values: \"Value1\", \"Value2\", \"Value3\".",
|
||||
"* --required-enum", "Valid values: \"Value1\", \"Value2\", \"Value3\"."
|
||||
);
|
||||
|
||||
_output.WriteLine(stdOutData);
|
||||
_output.WriteLine(stdOut.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Help_text_lists_environment_variable_names_for_options_that_have_them_defined()
|
||||
public async Task Help_text_shows_environment_variable_names_for_options_that_have_them_defined()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
var console = new VirtualConsole(output: stdOut);
|
||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(EnvironmentVariableCommand))
|
||||
.AddCommand<WithEnvironmentVariablesCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
await application.RunAsync(new[] {"cmd-with-env-vars", "--help"});
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
|
||||
var exitCode = await application.RunAsync(new[] {"cmd", "--help"});
|
||||
|
||||
// Assert
|
||||
stdOutData.Should().ContainAll(
|
||||
exitCode.Should().Be(0);
|
||||
stdOut.GetString().Should().ContainAll(
|
||||
"Options",
|
||||
"* -a|--option-a", "Environment variable:", "ENV_OPT_A",
|
||||
"-b|--option-b", "Environment variable:", "ENV_OPT_B"
|
||||
"-a|--opt-a", "Environment variable:", "ENV_OPT_A",
|
||||
"-b|--opt-b", "Environment variable:", "ENV_OPT_B"
|
||||
);
|
||||
|
||||
_output.WriteLine(stdOutData);
|
||||
_output.WriteLine(stdOut.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Help_text_shows_default_values_for_non_required_options()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
var console = new VirtualConsole(output: stdOut);
|
||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(ArgumentsWithDefaultValuesCommand))
|
||||
.AddCommand<WithDefaultValuesCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
await application.RunAsync(new[] {"cmd-with-defaults", "--help"});
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
|
||||
var exitCode = await application.RunAsync(new[] {"cmd", "--help"});
|
||||
|
||||
// Assert
|
||||
stdOutData.Should().ContainAll(
|
||||
"Usage",
|
||||
"cmd-with-defaults", "[options]",
|
||||
exitCode.Should().Be(0);
|
||||
stdOut.GetString().Should().ContainAll(
|
||||
"Options",
|
||||
"--Object", "Default: \"42\"",
|
||||
"--String", "Default: \"foo\"",
|
||||
"--EmptyString", "Default: \"\"",
|
||||
"--Bool", "Default: \"True\"",
|
||||
"--Char", "Default: \"t\"",
|
||||
"--Int", "Default: \"1337\"",
|
||||
"--TimeSpan", "Default: \"02:03:00\"",
|
||||
"--Enum", "Default: \"Value2\"",
|
||||
"--IntNullable", "Default: \"1337\"",
|
||||
"--StringArray", "Default: \"foo\" \"bar\" \"baz\"",
|
||||
"--IntArray", "Default: \"1\" \"2\" \"3\""
|
||||
"--obj", "Default: \"42\"",
|
||||
"--str", "Default: \"foo\"",
|
||||
"--str-empty", "Default: \"\"",
|
||||
"--str-array", "Default: \"foo\" \"bar\" \"baz\"",
|
||||
"--bool", "Default: \"True\"",
|
||||
"--char", "Default: \"t\"",
|
||||
"--int", "Default: \"1337\"",
|
||||
"--int-nullable", "Default: \"1337\"",
|
||||
"--int-array", "Default: \"1\" \"2\" \"3\"",
|
||||
"--timespan", "Default: \"02:03:00\"",
|
||||
"--enum", "Default: \"Value2\""
|
||||
);
|
||||
|
||||
_output.WriteLine(stdOutData);
|
||||
_output.WriteLine(stdOut.GetString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Domain;
|
||||
|
||||
namespace CliFx.Tests.Internal
|
||||
{
|
||||
internal static class CommandHelper
|
||||
{
|
||||
public static TCommand ResolveCommand<TCommand>(CommandInput input, IReadOnlyDictionary<string, string> environmentVariables)
|
||||
where TCommand : ICommand, new()
|
||||
{
|
||||
var schema = CommandSchema.TryResolve(typeof(TCommand))!;
|
||||
|
||||
var instance = new TCommand();
|
||||
schema.Bind(instance, input, environmentVariables);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static TCommand ResolveCommand<TCommand>(CommandInput input)
|
||||
where TCommand : ICommand, new() =>
|
||||
ResolveCommand<TCommand>(input, new Dictionary<string, string>());
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Domain;
|
||||
|
||||
namespace CliFx.Tests.Internal
|
||||
{
|
||||
internal class CommandInputBuilder
|
||||
{
|
||||
private readonly List<CommandDirectiveInput> _directives = new List<CommandDirectiveInput>();
|
||||
private readonly List<CommandParameterInput> _parameters = new List<CommandParameterInput>();
|
||||
private readonly List<CommandOptionInput> _options = new List<CommandOptionInput>();
|
||||
|
||||
private string? _commandName;
|
||||
|
||||
public CommandInputBuilder SetCommandName(string commandName)
|
||||
{
|
||||
_commandName = commandName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandInputBuilder AddDirective(string directive)
|
||||
{
|
||||
_directives.Add(new CommandDirectiveInput(directive));
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandInputBuilder AddParameter(string parameter)
|
||||
{
|
||||
_parameters.Add(new CommandParameterInput(parameter));
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandInputBuilder AddOption(string alias, params string[] values)
|
||||
{
|
||||
_options.Add(new CommandOptionInput(alias, values));
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandInput Build() => new CommandInput(
|
||||
_directives,
|
||||
_commandName,
|
||||
_parameters,
|
||||
_options
|
||||
);
|
||||
}
|
||||
}
|
||||
10
CliFx.Tests/Internal/JsonExtensions.cs
Normal file
10
CliFx.Tests/Internal/JsonExtensions.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CliFx.Tests.Internal
|
||||
{
|
||||
internal static class JsonExtensions
|
||||
{
|
||||
public static T DeserializeJson<T>(this string json) =>
|
||||
JsonConvert.DeserializeObject<T>(json);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CliFx.Tests.Internal
|
||||
{
|
||||
internal static class TaskExtensions
|
||||
{
|
||||
public static async Task IgnoreCancellation(this Task task)
|
||||
{
|
||||
try
|
||||
{
|
||||
await task;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class RoutingSpecs
|
||||
{
|
||||
[Command]
|
||||
private class DefaultCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.Output.WriteLine("Hello world!");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
[Command("concat", Description = "Concatenate strings.")]
|
||||
private class ConcatCommand : ICommand
|
||||
{
|
||||
[CommandOption('i', IsRequired = true, Description = "Input strings.")]
|
||||
public IReadOnlyList<string> Inputs { get; set; } = Array.Empty<string>();
|
||||
|
||||
[CommandOption('s', Description = "String separator.")]
|
||||
public string Separator { get; set; } = "";
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.Output.WriteLine(string.Join(Separator, Inputs));
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
[Command("div", Description = "Divide one number by another.")]
|
||||
private class DivideCommand : ICommand
|
||||
{
|
||||
[CommandOption("dividend", 'D', IsRequired = true, Description = "The number to divide.")]
|
||||
public double Dividend { get; set; } = 0;
|
||||
|
||||
[CommandOption("divisor", 'd', IsRequired = true, Description = "The number to divide by.")]
|
||||
public double Divisor { get; set; } = 0;
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.Output.WriteLine(Dividend / Divisor);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,90 +1,235 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Tests.Commands;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace CliFx.Tests
|
||||
{
|
||||
public partial class RoutingSpecs
|
||||
public class RoutingSpecs
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public RoutingSpecs(ITestOutputHelper testOutput) => _output = testOutput;
|
||||
|
||||
[Fact]
|
||||
public async Task Default_command_is_executed_if_provided_arguments_do_not_match_any_named_command()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
var console = new VirtualConsole(output: stdOut);
|
||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(DefaultCommand))
|
||||
.AddCommand(typeof(ConcatCommand))
|
||||
.AddCommand(typeof(DivideCommand))
|
||||
.AddCommand<DefaultCommand>()
|
||||
.AddCommand<NamedCommand>()
|
||||
.AddCommand<NamedSubCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
Array.Empty<string>(),
|
||||
new Dictionary<string, string>());
|
||||
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
|
||||
var exitCode = await application.RunAsync(Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
stdOutData.Should().Be("Hello world!");
|
||||
}
|
||||
stdOut.GetString().Trim().Should().Be(DefaultCommand.ExpectedOutputText);
|
||||
|
||||
[Fact]
|
||||
public async Task Help_text_is_printed_if_no_arguments_were_provided_and_default_command_is_not_defined()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
var console = new VirtualConsole(output: stdOut);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(ConcatCommand))
|
||||
.AddCommand(typeof(DivideCommand))
|
||||
.UseConsole(console)
|
||||
.UseDescription("This will be visible in help")
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
Array.Empty<string>(),
|
||||
new Dictionary<string, string>());
|
||||
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
stdOutData.Should().Contain("This will be visible in help");
|
||||
_output.WriteLine(stdOut.GetString());
|
||||
}
|
||||
|
||||
[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 (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(DefaultCommand))
|
||||
.AddCommand(typeof(ConcatCommand))
|
||||
.AddCommand(typeof(DivideCommand))
|
||||
.AddCommand<DefaultCommand>()
|
||||
.AddCommand<NamedCommand>()
|
||||
.AddCommand<NamedSubCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
new[] {"concat", "-i", "foo", "bar", "-s", ", "},
|
||||
new Dictionary<string, string>());
|
||||
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
|
||||
var exitCode = await application.RunAsync(new[] {"named"});
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
stdOutData.Should().Be("foo, bar");
|
||||
stdOut.GetString().Trim().Should().Be(NamedCommand.ExpectedOutputText);
|
||||
|
||||
_output.WriteLine(stdOut.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Specific_named_sub_command_is_executed_if_provided_arguments_match_its_name()
|
||||
{
|
||||
// Arrange
|
||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<DefaultCommand>()
|
||||
.AddCommand<NamedCommand>()
|
||||
.AddCommand<NamedSubCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(new[] {"named", "sub"});
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
stdOut.GetString().Trim().Should().Be(NamedSubCommand.ExpectedOutputText);
|
||||
|
||||
_output.WriteLine(stdOut.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Help_text_is_printed_if_no_arguments_were_provided_and_default_command_is_not_defined()
|
||||
{
|
||||
// Arrange
|
||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<NamedCommand>()
|
||||
.AddCommand<NamedSubCommand>()
|
||||
.UseConsole(console)
|
||||
.UseDescription("This will be visible in help")
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
stdOut.GetString().Should().Contain("This will be visible in help");
|
||||
|
||||
_output.WriteLine(stdOut.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Help_text_is_printed_if_provided_arguments_contain_the_help_option()
|
||||
{
|
||||
// Arrange
|
||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<DefaultCommand>()
|
||||
.AddCommand<NamedCommand>()
|
||||
.AddCommand<NamedSubCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(new[] {"--help"});
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
stdOut.GetString().Should().ContainAll(
|
||||
"Default command description",
|
||||
"Usage"
|
||||
);
|
||||
|
||||
_output.WriteLine(stdOut.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Help_text_is_printed_if_provided_arguments_contain_the_help_option_even_if_default_command_is_not_defined()
|
||||
{
|
||||
// Arrange
|
||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<NamedCommand>()
|
||||
.AddCommand<NamedSubCommand>()
|
||||
.UseDescription("This will be visible in help")
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(new[] {"--help"});
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
stdOut.GetString().Should().Contain("This will be visible in help");
|
||||
|
||||
_output.WriteLine(stdOut.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Help_text_for_a_specific_named_command_is_printed_if_provided_arguments_match_its_name_and_contain_the_help_option()
|
||||
{
|
||||
// Arrange
|
||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<DefaultCommand>()
|
||||
.AddCommand<NamedCommand>()
|
||||
.AddCommand<NamedSubCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(new[] {"named", "--help"});
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
stdOut.GetString().Should().ContainAll(
|
||||
"Named command description",
|
||||
"Usage",
|
||||
"named"
|
||||
);
|
||||
|
||||
_output.WriteLine(stdOut.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Help_text_for_a_specific_named_sub_command_is_printed_if_provided_arguments_match_its_name_and_contain_the_help_option()
|
||||
{
|
||||
// Arrange
|
||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<DefaultCommand>()
|
||||
.AddCommand<NamedCommand>()
|
||||
.AddCommand<NamedSubCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(new[] {"named", "sub", "--help"});
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
stdOut.GetString().Should().ContainAll(
|
||||
"Named sub command description",
|
||||
"Usage",
|
||||
"named", "sub"
|
||||
);
|
||||
|
||||
_output.WriteLine(stdOut.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Version_is_printed_if_the_only_provided_argument_is_the_version_option()
|
||||
{
|
||||
// Arrange
|
||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand<DefaultCommand>()
|
||||
.AddCommand<NamedCommand>()
|
||||
.AddCommand<NamedSubCommand>()
|
||||
.UseVersionText("v6.9")
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(new[] {"--version"});
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
stdOut.GetString().Trim().Should().Be("v6.9");
|
||||
|
||||
_output.WriteLine(stdOut.GetString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>1.3</Version>
|
||||
<Version>1.4</Version>
|
||||
<Company>Tyrrrz</Company>
|
||||
<Copyright>Copyright (C) Alexey Golub</Copyright>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
||||
@@ -28,7 +28,8 @@ namespace CliFx
|
||||
/// </summary>
|
||||
public ApplicationConfiguration(
|
||||
IReadOnlyList<Type> commandTypes,
|
||||
bool isDebugModeAllowed, bool isPreviewModeAllowed)
|
||||
bool isDebugModeAllowed,
|
||||
bool isPreviewModeAllowed)
|
||||
{
|
||||
CommandTypes = commandTypes;
|
||||
IsDebugModeAllowed = isDebugModeAllowed;
|
||||
|
||||
@@ -41,13 +41,15 @@ namespace CliFx
|
||||
private void WriteError(string message) => _console.WithForegroundColor(ConsoleColor.Red, () =>
|
||||
_console.Error.WriteLine(message));
|
||||
|
||||
private async ValueTask WaitForDebuggerAsync()
|
||||
private async ValueTask LaunchAndWaitForDebuggerAsync()
|
||||
{
|
||||
var processId = ProcessEx.GetCurrentProcessId();
|
||||
|
||||
_console.WithForegroundColor(ConsoleColor.Green, () =>
|
||||
_console.Output.WriteLine($"Attach debugger to PID {processId} to continue."));
|
||||
|
||||
Debugger.Launch();
|
||||
|
||||
while (!Debugger.IsAttached)
|
||||
await Task.Delay(100);
|
||||
}
|
||||
@@ -125,8 +127,7 @@ namespace CliFx
|
||||
// Debug mode
|
||||
if (_configuration.IsDebugModeAllowed && input.IsDebugDirectiveSpecified)
|
||||
{
|
||||
// Ensure debugger is attached and continue
|
||||
await WaitForDebuggerAsync();
|
||||
await LaunchAndWaitForDebuggerAsync();
|
||||
}
|
||||
|
||||
// Preview mode
|
||||
@@ -217,9 +218,10 @@ namespace CliFx
|
||||
/// </remarks>
|
||||
public async ValueTask<int> RunAsync(IReadOnlyList<string> commandLineArguments)
|
||||
{
|
||||
// Environment variable names are case-insensitive on Windows but are case-sensitive on Linux and macOS
|
||||
var environmentVariables = Environment.GetEnvironmentVariables()
|
||||
.Cast<DictionaryEntry>()
|
||||
.ToDictionary(e => (string) e.Key, e => (string) e.Value, StringComparer.OrdinalIgnoreCase);
|
||||
.ToDictionary(e => (string) e.Key, e => (string) e.Value, StringComparer.Ordinal);
|
||||
|
||||
return await RunAsync(commandLineArguments, environmentVariables);
|
||||
}
|
||||
@@ -252,7 +254,7 @@ namespace CliFx
|
||||
public static int FromException(Exception ex) =>
|
||||
ex is CommandException cmdEx
|
||||
? cmdEx.ExitCode
|
||||
: ex.HResult;
|
||||
: 1;
|
||||
}
|
||||
|
||||
[Command]
|
||||
|
||||
@@ -4,7 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using CliFx.Domain;
|
||||
using CliFx.Internal;
|
||||
using CliFx.Internal.Extensions;
|
||||
|
||||
namespace CliFx
|
||||
{
|
||||
@@ -34,6 +34,12 @@ namespace CliFx
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a command of specified type to the application.
|
||||
/// </summary>
|
||||
public CliApplicationBuilder AddCommand<TCommand>() where TCommand : ICommand =>
|
||||
AddCommand(typeof(TCommand));
|
||||
|
||||
/// <summary>
|
||||
/// Adds multiple commands to the application.
|
||||
/// </summary>
|
||||
|
||||
@@ -23,15 +23,6 @@
|
||||
<Nullable>annotations</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
||||
<_Parameter1>$(AssemblyName).Tests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
||||
<_Parameter1>$(AssemblyName).Analyzers</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="all" />
|
||||
|
||||
@@ -4,7 +4,7 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Internal;
|
||||
using CliFx.Internal.Extensions;
|
||||
|
||||
namespace CliFx.Domain
|
||||
{
|
||||
@@ -48,7 +48,7 @@ namespace CliFx.Domain
|
||||
? ConvertScalar(value, nullableUnderlyingType)
|
||||
: null;
|
||||
|
||||
// String-constructable
|
||||
// String-constructible
|
||||
var stringConstructor = targetType.GetConstructor(new[] {typeof(string)});
|
||||
if (stringConstructor != null)
|
||||
return stringConstructor.Invoke(new object[] {value!});
|
||||
@@ -83,7 +83,7 @@ namespace CliFx.Domain
|
||||
if (targetEnumerableType.IsAssignableFrom(arrayType))
|
||||
return array;
|
||||
|
||||
// Constructable from an array
|
||||
// Constructible from an array
|
||||
var arrayConstructor = targetEnumerableType.GetConstructor(new[] {arrayType});
|
||||
if (arrayConstructor != null)
|
||||
return arrayConstructor.Invoke(new object[] {array});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace CliFx.Domain
|
||||
{
|
||||
@@ -12,6 +13,7 @@ namespace CliFx.Domain
|
||||
|
||||
public CommandDirectiveInput(string name) => Name = name;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public override string ToString() => $"[{Name}]";
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using CliFx.Internal;
|
||||
using CliFx.Internal.Extensions;
|
||||
|
||||
namespace CliFx.Domain
|
||||
{
|
||||
@@ -36,6 +37,7 @@ namespace CliFx.Domain
|
||||
Options = options;
|
||||
}
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public override string ToString()
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using CliFx.Internal;
|
||||
using CliFx.Internal.Extensions;
|
||||
|
||||
namespace CliFx.Domain
|
||||
{
|
||||
@@ -29,6 +30,7 @@ namespace CliFx.Domain
|
||||
|
||||
public string GetRawValues() => Values.Select(v => v.Quote()).JoinToString(" ");
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public override string ToString() => $"{GetRawAlias()} {GetRawValues()}";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
@@ -45,7 +46,7 @@ namespace CliFx.Domain
|
||||
|
||||
public bool MatchesEnvironmentVariableName(string environmentVariableName) =>
|
||||
!string.IsNullOrWhiteSpace(EnvironmentVariableName) &&
|
||||
string.Equals(EnvironmentVariableName, environmentVariableName, StringComparison.OrdinalIgnoreCase);
|
||||
string.Equals(EnvironmentVariableName, environmentVariableName, StringComparison.Ordinal);
|
||||
|
||||
public string GetUserFacingDisplayString()
|
||||
{
|
||||
@@ -75,6 +76,7 @@ namespace CliFx.Domain
|
||||
|
||||
public string GetInternalDisplayString() => $"{Property?.Name ?? "<implicit>"} ('{GetUserFacingDisplayString()}')";
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public override string ToString() => GetInternalDisplayString();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace CliFx.Domain
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace CliFx.Domain
|
||||
{
|
||||
internal class CommandParameterInput
|
||||
{
|
||||
@@ -6,6 +8,7 @@
|
||||
|
||||
public CommandParameterInput(string value) => Value = value;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public override string ToString() => Value;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Reflection;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using CliFx.Attributes;
|
||||
|
||||
@@ -31,6 +32,7 @@ namespace CliFx.Domain
|
||||
|
||||
public string GetInternalDisplayString() => $"{Property?.Name ?? "<implicit>"} ([{Order}] {GetUserFacingDisplayString()})";
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public override string ToString() => GetInternalDisplayString();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Internal;
|
||||
using CliFx.Internal.Extensions;
|
||||
|
||||
namespace CliFx.Domain
|
||||
{
|
||||
@@ -103,12 +104,16 @@ namespace CliFx.Domain
|
||||
|
||||
if (nonScalarParameter != null)
|
||||
{
|
||||
// TODO: Should it verify that at least one value is passed?
|
||||
var nonScalarValues = parameterInputs
|
||||
.Skip(scalarParameters.Length)
|
||||
.Select(p => p.Value)
|
||||
.ToArray();
|
||||
|
||||
// Parameters are required by default and so a non-scalar parameter must
|
||||
// be bound to at least one value
|
||||
if(!nonScalarValues.Any())
|
||||
throw CliFxException.ParameterNotSet(nonScalarParameter);
|
||||
|
||||
nonScalarParameter.BindOn(instance, nonScalarValues);
|
||||
remainingParameterInputs.Clear();
|
||||
}
|
||||
@@ -203,6 +208,7 @@ namespace CliFx.Domain
|
||||
return buffer.ToString();
|
||||
}
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public override string ToString() => GetInternalDisplayString();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using CliFx.Internal;
|
||||
using CliFx.Internal.Extensions;
|
||||
|
||||
namespace CliFx.Domain
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Internal;
|
||||
using CliFx.Internal.Extensions;
|
||||
|
||||
namespace CliFx.Domain
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Domain;
|
||||
using CliFx.Internal;
|
||||
using CliFx.Internal.Extensions;
|
||||
|
||||
namespace CliFx.Exceptions
|
||||
{
|
||||
|
||||
@@ -9,12 +9,12 @@ namespace CliFx.Exceptions
|
||||
/// </summary>
|
||||
public class CommandException : Exception
|
||||
{
|
||||
private const int DefaultExitCode = -1;
|
||||
private const int DefaultExitCode = 1;
|
||||
|
||||
private readonly bool _isMessageSet;
|
||||
|
||||
/// <summary>
|
||||
/// Returns an exit code associated with this exception.
|
||||
/// Exit code returned by the application when this exception is handled.
|
||||
/// </summary>
|
||||
public int ExitCode { get; }
|
||||
|
||||
@@ -26,6 +26,9 @@ namespace CliFx.Exceptions
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="CommandException"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// On Unix systems an exit code is 8-bit unsigned integer so it's strongly recommended to use values between 1 and 255 to avoid overflow.
|
||||
/// </remarks>
|
||||
public CommandException(string? message, Exception? innerException, int exitCode = DefaultExitCode, bool showHelp = false)
|
||||
: base(message, innerException)
|
||||
{
|
||||
@@ -39,6 +42,9 @@ namespace CliFx.Exceptions
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="CommandException"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// On Unix systems an exit code is 8-bit unsigned integer so it's strongly recommended to use values between 1 and 255 to avoid overflow.
|
||||
/// </remarks>
|
||||
public CommandException(string? message, int exitCode = DefaultExitCode, bool showHelp = false)
|
||||
: this(message, null, exitCode, showHelp)
|
||||
{
|
||||
@@ -47,6 +53,9 @@ namespace CliFx.Exceptions
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="CommandException"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// On Unix systems an exit code is 8-bit unsigned integer so it's strongly recommended to use values between 1 and 255 to avoid overflow.
|
||||
/// </remarks>
|
||||
public CommandException(int exitCode = DefaultExitCode, bool showHelp = false)
|
||||
: this(null, exitCode, showHelp)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CliFx.Internal
|
||||
namespace CliFx.Internal.Extensions
|
||||
{
|
||||
internal static class CollectionExtensions
|
||||
{
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CliFx.Internal
|
||||
namespace CliFx.Internal.Extensions
|
||||
{
|
||||
internal static class StringExtensions
|
||||
{
|
||||
@@ -4,7 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace CliFx.Internal
|
||||
namespace CliFx.Internal.Extensions
|
||||
{
|
||||
internal static class TypeExtensions
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Internal
|
||||
namespace CliFx.Internal.Extensions
|
||||
{
|
||||
internal static class VersionExtensions
|
||||
{
|
||||
43
CliFx/Utilities/MemoryStreamWriter.cs
Normal file
43
CliFx/Utilities/MemoryStreamWriter.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace CliFx.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="StreamWriter"/> with a <see cref="MemoryStream"/> as a backing store.
|
||||
/// </summary>
|
||||
public class MemoryStreamWriter : StreamWriter
|
||||
{
|
||||
private new MemoryStream BaseStream => (MemoryStream) base.BaseStream;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="MemoryStreamWriter"/>.
|
||||
/// </summary>
|
||||
public MemoryStreamWriter(Encoding encoding)
|
||||
: base(new MemoryStream(), encoding)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="MemoryStreamWriter"/>.
|
||||
/// </summary>
|
||||
public MemoryStreamWriter()
|
||||
: base(new MemoryStream())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bytes written to the underlying stream.
|
||||
/// </summary>
|
||||
public byte[] GetBytes()
|
||||
{
|
||||
Flush();
|
||||
return BaseStream.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the string written to the underlying stream.
|
||||
/// </summary>
|
||||
public string GetString() => Encoding.GetString(GetBytes());
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using CliFx.Utilities;
|
||||
|
||||
namespace CliFx
|
||||
{
|
||||
@@ -101,5 +102,25 @@ namespace CliFx
|
||||
stream != null
|
||||
? new StreamWriter(Stream.Synchronized(stream), Console.OutputEncoding) {AutoFlush = true}
|
||||
: StreamWriter.Null;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="VirtualConsole"/> that uses in-memory output and error streams.
|
||||
/// Use the exposed streams to easily get the current output.
|
||||
/// </summary>
|
||||
public static (VirtualConsole console, MemoryStreamWriter output, MemoryStreamWriter error) CreateBuffered(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Memory streams don't need to be disposed
|
||||
var output = new MemoryStreamWriter(Console.OutputEncoding);
|
||||
var error = new MemoryStreamWriter(Console.OutputEncoding);
|
||||
|
||||
var console = new VirtualConsole(
|
||||
output: output,
|
||||
error: error,
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
|
||||
return (console, output, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
110
Readme.md
110
Readme.md
@@ -168,7 +168,7 @@ Parameters
|
||||
* value Value whose logarithm is to be found.
|
||||
|
||||
Options
|
||||
-b|--base Logarithm base.
|
||||
-b|--base Logarithm base. Default: "10".
|
||||
-h|--help Shows help text.
|
||||
--version Shows version information.
|
||||
```
|
||||
@@ -215,9 +215,9 @@ As a general guideline, prefer to use parameters for required inputs that the co
|
||||
|
||||
### Argument syntax
|
||||
|
||||
This library supports an argument syntax which is based on the POSIX standard. To be fair, nobody really knows what the standard is about and very few tools actually follow it as they're supposed to, so for the purpose of having dashes and spaces, CliFx is using the "standard command line syntax".
|
||||
This library supports an argument syntax which is based on the POSIX standard. To be fair, nobody really knows what the standard is about and very few tools actually follow it to the letter, so for the purpose of having dashes and spaces, CliFx is using the "standard command line syntax".
|
||||
|
||||
In more detail, the following examples are all valid:
|
||||
More specifically, the following examples are all valid:
|
||||
|
||||
- `myapp --foo bar` sets option `"foo"` to value `"bar"`
|
||||
- `myapp -f bar` sets option `'f'` to value `"bar"`
|
||||
@@ -227,32 +227,36 @@ In more detail, the following examples are all valid:
|
||||
- `myapp -xqf bar` sets options `'x'` and `'q'` without value, and option `'f'` to value `"bar"`
|
||||
- `myapp -i file1.txt file2.txt` sets option `'i'` to a sequence of values `"file1.txt"` and `"file2.txt"`
|
||||
- `myapp -i file1.txt -i file2.txt` sets option `'i'` to a sequence of values `"file1.txt"` and `"file2.txt"`
|
||||
- `myapp jar new -o cookie` sets option `'o'` to value `"cookie"` and retains two unbound arguments `"jar"` and `"new"`
|
||||
- `myapp cmd abc -o` routes to command `cmd` (assuming it exists) with parameter `abc` and sets option `'o'` without value
|
||||
|
||||
Note that CliFx purposely employs a context-free parser when consuming command line arguments. That means that every input is parsed the same way.
|
||||
Argument parsing in CliFx aims to be as deterministic as possible, ideally yielding the same result no matter the context. The only context-sensitive part in the parser is the command name resolution which needs to know what commands are available in order to discern between arguments that correspond to the command name and arguments which are parameters.
|
||||
|
||||
This also means that `myapp -i file1.txt file2.txt` will _always_ be parsed as an option with multiple values, even if the underlying bound property is not enumerable. For the same reason, unseparated arguments such as `myapp -ofile` will be treated as five distinct options `'o'`, `'f'`, `'i'`, `'l'`, `'e'`, instead of `'o'` being set to `"file"`.
|
||||
Options are always parsed the same way, disregarding the arity of the actual property it binds to. This means that `myapp -i file1.txt file2.txt` will _always_ be parsed as an option with multiple values, even if the underlying bound property is not enumerable. For the same reason, unseparated arguments such as `myapp -ofile` will be treated as five distinct options `'o'`, `'f'`, `'i'`, `'l'`, `'e'`, instead of `'o'` being set to `"file"`.
|
||||
|
||||
When it comes to command name and parameters, they must appear in a strict order, before any options. The parser can't distinguish between arguments that make up a part of the command name and arguments that belong to command parameters, which is why the non-option arguments are bound at a later stage. It is done by trying to find a command that matches the longest sequence of arguments starting from the first, binding any remaining arguments to positional parameters.
|
||||
Because of these rules, order of arguments is semantically important and it always goes like this:
|
||||
|
||||
The above design may seem like a deficiency, but it actually provides value in the fact that it's deterministic -- given a set of command line arguments, the semantics behind them always remain the same. This leads to a more consistent experience for both you as a developer, as well as for the users of your application.
|
||||
```ini
|
||||
{directives} {command name} {parameters} {options}
|
||||
```
|
||||
|
||||
The above design makes the usage of your applications a lot more intuitive and predictable, providing a better end-user experience.
|
||||
|
||||
### Value conversion
|
||||
|
||||
Parameters and options can have different underlying types:
|
||||
|
||||
- Standard types
|
||||
- Primitive types (`int`, `bool`, `double`, `ulong`, `char`, etc)
|
||||
- Primitive types (`int`, `bool`, `double`, `ulong`, `char`, etc.)
|
||||
- Date and time types (`DateTime`, `DateTimeOffset`, `TimeSpan`)
|
||||
- Enum types
|
||||
- Enum types (converted from either name or value)
|
||||
- String-initializable types
|
||||
- Types with a constructor that accepts a single `string` parameter (`FileInfo`, `DirectoryInfo`, etc)
|
||||
- Types with a constructor that accepts a single `string` parameter (`FileInfo`, `DirectoryInfo`, etc.)
|
||||
- Types with a static method `Parse` that accepts a single `string` parameter (and optionally `IFormatProvider`)
|
||||
- Nullable versions of all above types (`decimal?`, `TimeSpan?`, etc)
|
||||
- Nullable versions of all above types (`decimal?`, `TimeSpan?`, etc.)
|
||||
- Collections of all above types
|
||||
- Array types (`T[]`)
|
||||
- Types that are assignable from arrays (`IReadOnlyList<T>`, `ICollection<T>`, etc)
|
||||
- Types with a constructor that accepts a single `T[]` parameter (`HashSet<T>`, `List<T>`, etc)
|
||||
- Types that are assignable from arrays (`IReadOnlyList<T>`, `ICollection<T>`, etc.)
|
||||
- Types with a constructor that accepts a single `T[]` parameter (`HashSet<T>`, `List<T>`, etc.)
|
||||
|
||||
When defining a parameter of an enumerable type, keep in mind that it has to be the only such parameter and it must be the last in order. Options, on the other hand, don't have this limitation.
|
||||
|
||||
@@ -392,10 +396,12 @@ You can run `myapp.exe cmd1 [command] --help` to show help on a specific command
|
||||
|
||||
You may have noticed that commands in CliFx don't return exit codes. This is by design as exit codes are considered a higher-level concern and thus handled by `CliApplication`, not by individual commands.
|
||||
|
||||
Commands can report execution failure simply by throwing exceptions just like any other C# code. When an exception is thrown, `CliApplication` will catch it, print the error, and return an appropriate exit code to the calling process.
|
||||
Commands can report execution failure simply by throwing exceptions just like any other C# code. When an exception is thrown, `CliApplication` will catch it, print the error, and return `1` as the exit code to the calling process.
|
||||
|
||||
If you want to communicate a specific error through exit code, you can instead throw an instance of `CommandException` which takes an exit code as a parameter. When a command throws an exception of type `CommandException`, it is assumed that this was a result of a handled error and, as such, only the exception message will be printed to the error stream. If a command throws an exception of any other type, the full stack trace will be printed as well.
|
||||
|
||||
> Note: Unix systems rely on 8-bit unsigned integers for exit codes, so it's strongly recommended to use values between `1` and `255` to avoid potential overflow issues.
|
||||
|
||||
```c#
|
||||
[Command]
|
||||
public class DivideCommand : ICommand
|
||||
@@ -410,8 +416,8 @@ public class DivideCommand : ICommand
|
||||
{
|
||||
if (Math.Abs(Divisor) < double.Epsilon)
|
||||
{
|
||||
// This will print the error and set exit code to 1337
|
||||
throw new CommandException("Division by zero is not supported.", 1337);
|
||||
// This will print the error and set exit code to 133
|
||||
throw new CommandException("Division by zero is not supported.", 133);
|
||||
}
|
||||
|
||||
var result = Dividend / Divisor;
|
||||
@@ -430,10 +436,10 @@ Division by zero is not supported.
|
||||
|
||||
> $LastExitCode
|
||||
|
||||
1337
|
||||
133
|
||||
```
|
||||
|
||||
You can also specify the `showHelp` parameter to instruct whether to show the help text after printing the error:
|
||||
You can also specify the `showHelp` parameter to instruct whether to show the help text for the current command after printing the error:
|
||||
|
||||
```c#
|
||||
[Command]
|
||||
@@ -448,7 +454,7 @@ public class ExampleCommand : ICommand
|
||||
|
||||
### Graceful cancellation
|
||||
|
||||
It is possible to gracefully cancel execution of a command and preform any necessary cleanup. By default an app gets forcefully killed when it receives an interrupt signal (Ctrl+C or Ctrl+Break), but you can easily override this behavior.
|
||||
It is possible to gracefully cancel execution of a command and preform any necessary cleanup. By default an app gets forcefully killed when it receives an interrupt signal (Ctrl+C or Ctrl+Break), but you can override this behavior.
|
||||
|
||||
In order to make a command cancellation-aware, you need to call `console.GetCancellationToken()`. This method returns a `CancellationToken` that will trigger when the user issues an interrupt signal. Note that any code that comes before the first call to `GetCancellationToken()` will not be cancellation-aware and as such will terminate instantly. Any subsequent calls to this method will return the same token.
|
||||
|
||||
@@ -504,11 +510,29 @@ public static class Program
|
||||
|
||||
### Testing
|
||||
|
||||
CliFx provides an easy way to write functional tests for your commands thanks to the `IConsole` interface.
|
||||
CliFx provides a convenient way to write functional tests for your applications, thanks to the `IConsole` interface.
|
||||
|
||||
You can use `VirtualConsole` to replace the application's stdin, stdout and stderr with your own streams. It has multiple constructor overloads allowing you to specify the exact set of streams that you want. Streams that are not provided are replaced with stubs, i.e. `VirtualConsole` doesn't leak to `System.Console` in any way.
|
||||
Instead of interacting with the real console, you can use an instance of `VirtualConsole` to replace the application's stdin, stdout and stderr with your own streams. Using optional parameters you can also choose to substitute only some of the streams, in which case the remaining streams are replaced with no-op stubs:
|
||||
|
||||
Let's assume you want to test a simple command such as this one.
|
||||
```c#
|
||||
var console = new VirtualConsole(
|
||||
input: stdIn,
|
||||
output: stdOut,
|
||||
error: stdErr
|
||||
);
|
||||
```
|
||||
|
||||
Although `VirtualConsole` can be constructed with all kinds of streams, most of the time you will want to test against in-memory stores. To simplify setup in such scenarios, CliFx also provides a `CreateBuffered` factory method that returns an instance of `IConsole` along with in-memory streams that you can later read from:
|
||||
|
||||
```c#
|
||||
var (console, stdOut, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
// ...
|
||||
|
||||
var stdOutData = stdOut.GetString();
|
||||
```
|
||||
|
||||
To illustrate how to use all this, let's look at an example. Assume you want to test a simple command such as this one:
|
||||
|
||||
```c#
|
||||
[Command]
|
||||
@@ -531,15 +555,15 @@ public class ConcatCommand : ICommand
|
||||
}
|
||||
```
|
||||
|
||||
By substituting `IConsole` you can write your test cases like this:
|
||||
By substituting `IConsole` you can write your test cases like so:
|
||||
|
||||
```c#
|
||||
// Integration test at command level
|
||||
[Test]
|
||||
public async Task ConcatCommand_Test()
|
||||
public async Task ConcatCommand_executes_successfully()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
var console = new VirtualConsole(output: stdOut);
|
||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var command = new ConcatCommand
|
||||
{
|
||||
@@ -549,25 +573,24 @@ public async Task ConcatCommand_Test()
|
||||
|
||||
// Act
|
||||
await command.ExecuteAsync(console);
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray());
|
||||
|
||||
// Assert
|
||||
Assert.That(stdOutData, Is.EqualTo("foo bar"));
|
||||
Assert.That(stdOut.GetString(), Is.EqualTo("foo bar"));
|
||||
}
|
||||
```
|
||||
|
||||
And if you want, you can also test the whole application in a similar fashion:
|
||||
Similarly, you can also test the command end-to-end like so:
|
||||
|
||||
```c#
|
||||
// End-to-end test at application level
|
||||
[Test]
|
||||
public async Task ConcatCommand_Test()
|
||||
public async Task ConcatCommand_executes_successfully()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
var console = new VirtualConsole(output: stdOut);
|
||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||
|
||||
var app = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(ConcatCommand))
|
||||
.AddCommand<ConcatCommand>()
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
@@ -576,14 +599,25 @@ public async Task ConcatCommand_Test()
|
||||
|
||||
// Act
|
||||
await app.RunAsync(args, envVars);
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray());
|
||||
|
||||
// Assert
|
||||
Assert.That(stdOutData, Is.EqualTo("foo bar"));
|
||||
Assert.That(stdOut.GetString(), Is.EqualTo("foo bar"));
|
||||
}
|
||||
```
|
||||
|
||||
Note that CliFx applications have access to underlying binary streams that allows them to write binary data directly. By using the approach outlined above, we're making the assumption that the application is only expected to produce text output.
|
||||
As a general recommendation, it's nearly always more preferable to test at the application level. While you can validate your command's execution adequately simply by testing its `ExecuteAsync` method, testing end-to-end helps you to also catch bugs related to configuration, such as incorrect option names, parameter order, environment variable names, etc.
|
||||
|
||||
Additionally, it's important to remember that commands in CliFx are not constrained to text and can also produce binary data. In such cases, you can still use the above setup but use `GetBytes` instead of `GetString`:
|
||||
|
||||
```c#
|
||||
// Act
|
||||
await app.RunAsync(args, envVars);
|
||||
|
||||
// Assert
|
||||
Assert.That(stdOut.GetBytes(), Is.EqualTo(new byte[] {1, 2, 3, 4, 5}));
|
||||
```
|
||||
|
||||
In some scenarios the binary data may be too large to load in-memory. In situations like this, it's recommended to use `VirtualConsole` directly with custom streams.
|
||||
|
||||
### Debug and preview mode
|
||||
|
||||
@@ -633,7 +667,7 @@ for (var i = 0.0; i <= 1; i += 0.01)
|
||||
|
||||
### Environment variables
|
||||
|
||||
An option can be configured to use the value of a specific environment variable as a fallback.
|
||||
An option can be configured to use the value of an environment variable as a fallback. If an option was not specified by the user, the value will be extracted from that environment variable instead. This also works on options which are marked as required.
|
||||
|
||||
```c#
|
||||
[Command]
|
||||
|
||||
Reference in New Issue
Block a user