Files
CliFx/CliFx.Tests/OptionBindingSpecs.cs
2023-12-10 22:51:57 +02:00

773 lines
22 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Tests.Utils;
using CliFx.Tests.Utils.Extensions;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;
namespace CliFx.Tests;
public class OptionBindingSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
{
[Fact]
public async Task I_can_bind_an_option_to_a_property_and_get_the_value_from_the_corresponding_argument_by_name()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandOption("foo")]
public bool Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(Foo);
return default;
}
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] { "--foo" },
new Dictionary<string, string>()
);
// Assert
exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Trim().Should().Be("True");
}
[Fact]
public async Task I_can_bind_an_option_to_a_property_and_get_the_value_from_the_corresponding_argument_by_short_name()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandOption('f')]
public bool Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(Foo);
return default;
}
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(new[] { "-f" }, new Dictionary<string, string>());
// Assert
exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Trim().Should().Be("True");
}
[Fact]
public async Task I_can_bind_an_option_to_a_property_and_get_the_value_from_the_corresponding_argument_set_by_name()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandOption("foo")]
public string? Foo { get; init; }
[CommandOption("bar")]
public string? Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine("Foo = " + Foo);
console.Output.WriteLine("Bar = " + Bar);
return default;
}
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] { "--foo", "one", "--bar", "two" },
new Dictionary<string, string>()
);
// Assert
exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Should().ConsistOfLines("Foo = one", "Bar = two");
}
[Fact]
public async Task I_can_bind_an_option_to_a_property_and_get_the_value_from_the_corresponding_argument_set_by_short_name()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandOption('f')]
public string? Foo { get; init; }
[CommandOption('b')]
public string? Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine("Foo = " + Foo);
console.Output.WriteLine("Bar = " + Bar);
return default;
}
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] { "-f", "one", "-b", "two" },
new Dictionary<string, string>()
);
// Assert
exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Should().ConsistOfLines("Foo = one", "Bar = two");
}
[Fact]
public async Task I_can_bind_an_option_to_a_property_and_get_the_value_from_the_corresponding_argument_stack_by_short_name()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandOption('f')]
public string? Foo { get; init; }
[CommandOption('b')]
public string? Bar { get; init; }
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine("Foo = " + Foo);
console.Output.WriteLine("Bar = " + Bar);
return default;
}
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] { "-fb", "value" },
new Dictionary<string, string>()
);
// Assert
exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Should().ConsistOfLines("Foo = ", "Bar = value");
}
[Fact]
public async Task I_can_bind_an_option_to_a_non_scalar_property_and_get_the_values_from_the_corresponding_arguments_by_name()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandOption("Foo")]
public IReadOnlyList<string>? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console)
{
foreach (var i in Foo)
console.Output.WriteLine(i);
return default;
}
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] { "--foo", "one", "two", "three" },
new Dictionary<string, string>()
);
// Assert
exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Should().ConsistOfLines("one", "two", "three");
}
[Fact]
public async Task I_can_bind_an_option_to_a_non_scalar_property_and_get_the_values_from_the_corresponding_arguments_by_short_name()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandOption('f')]
public IReadOnlyList<string>? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console)
{
foreach (var i in Foo)
console.Output.WriteLine(i);
return default;
}
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] { "-f", "one", "two", "three" },
new Dictionary<string, string>()
);
// Assert
exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Should().ConsistOfLines("one", "two", "three");
}
[Fact]
public async Task I_can_bind_an_option_to_a_non_scalar_property_and_get_the_values_from_the_corresponding_argument_sets_by_name()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandOption("foo")]
public IReadOnlyList<string>? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console)
{
foreach (var i in Foo)
console.Output.WriteLine(i);
return default;
}
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] { "--foo", "one", "--foo", "two", "--foo", "three" },
new Dictionary<string, string>()
);
// Assert
exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Should().ConsistOfLines("one", "two", "three");
}
[Fact]
public async Task I_can_bind_an_option_to_a_non_scalar_property_and_get_the_values_from_the_corresponding_argument_sets_by_short_name()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandOption('f')]
public IReadOnlyList<string>? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console)
{
foreach (var i in Foo)
console.Output.WriteLine(i);
return default;
}
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] { "-f", "one", "-f", "two", "-f", "three" },
new Dictionary<string, string>()
);
// Assert
exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Should().ConsistOfLines("one", "two", "three");
}
[Fact]
public async Task I_can_bind_an_option_to_a_non_scalar_property_and_get_the_values_from_the_corresponding_argument_sets_by_name_or_short_name()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandOption("foo", 'f')]
public IReadOnlyList<string>? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console)
{
foreach (var i in Foo)
console.Output.WriteLine(i);
return default;
}
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] { "--foo", "one", "-f", "two", "--foo", "three" },
new Dictionary<string, string>()
);
// Assert
exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Should().ConsistOfLines("one", "two", "three");
}
[Fact]
public async Task I_can_bind_an_option_to_a_property_and_get_no_value_if_the_user_does_not_provide_the_corresponding_argument()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandOption("foo")]
public string? Foo { get; init; }
[CommandOption("bar")]
public string? Bar { get; init; } = "hello";
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine("Foo = " + Foo);
console.Output.WriteLine("Bar = " + Bar);
return default;
}
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] { "--foo", "one" },
new Dictionary<string, string>()
);
// Assert
exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Should().ConsistOfLines("Foo = one", "Bar = hello");
}
[Fact]
public async Task I_can_bind_an_option_to_a_property_through_multiple_inheritance()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
"""
public static class SharedContext
{
public static int Foo { get; set; }
public static bool Bar { get; set; }
}
public interface IHasFoo : ICommand
{
[CommandOption("foo")]
public int Foo
{
get => SharedContext.Foo;
init => SharedContext.Foo = value;
}
}
public interface IHasBar : ICommand
{
[CommandOption("bar")]
public bool Bar
{
get => SharedContext.Bar;
init => SharedContext.Bar = value;
}
}
public interface IHasBaz : ICommand
{
public string? Baz { get; init; }
}
[Command]
public class Command : IHasFoo, IHasBar, IHasBaz
{
[CommandOption("baz")]
public string? Baz { get; init; }
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine("Foo = " + SharedContext.Foo);
console.Output.WriteLine("Bar = " + SharedContext.Bar);
console.Output.WriteLine("Baz = " + Baz);
return default;
}
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(new[] { "--foo", "42", "--bar", "--baz", "xyz" });
// Assert
exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Should().ConsistOfLines("Foo = 42", "Bar = True", "Baz = xyz");
}
[Fact]
public async Task I_can_bind_an_option_to_a_property_and_get_the_correct_value_if_the_user_provides_an_argument_containing_a_negative_number()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandOption("foo")]
public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(Foo);
return default;
}
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] { "--foo", "-13" },
new Dictionary<string, string>()
);
// Assert
exitCode.Should().Be(0);
var stdOut = FakeConsole.ReadOutputString();
stdOut.Trim().Should().Be("-13");
}
[Fact]
public async Task I_can_try_to_bind_a_required_option_to_a_property_and_get_an_error_if_the_user_does_not_provide_the_corresponding_argument()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandOption("foo")]
public required string Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
Array.Empty<string>(),
new Dictionary<string, string>()
);
// Assert
exitCode.Should().NotBe(0);
var stdErr = FakeConsole.ReadErrorString();
stdErr.Should().Contain("Missing required option(s)");
}
[Fact]
public async Task I_can_try_to_bind_a_required_option_to_a_property_and_get_an_error_if_the_user_provides_an_empty_argument()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandOption("foo")]
public required string Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] { "--foo" },
new Dictionary<string, string>()
);
// Assert
exitCode.Should().NotBe(0);
var stdErr = FakeConsole.ReadErrorString();
stdErr.Should().Contain("Missing required option(s)");
}
[Fact]
public async Task I_can_try_to_bind_an_option_to_a_non_scalar_property_and_get_an_error_if_the_user_does_not_provide_at_least_one_corresponding_argument()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandOption("foo")]
public required IReadOnlyList<string> Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] { "--foo" },
new Dictionary<string, string>()
);
// Assert
exitCode.Should().NotBe(0);
var stdErr = FakeConsole.ReadErrorString();
stdErr.Should().Contain("Missing required option(s)");
}
[Fact]
public async Task I_can_try_to_bind_options_and_get_an_error_if_the_user_provides_unrecognized_arguments()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandOption("foo")]
public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] { "--foo", "one", "--bar", "two" },
new Dictionary<string, string>()
);
// Assert
exitCode.Should().NotBe(0);
var stdErr = FakeConsole.ReadErrorString();
stdErr.Should().Contain("Unrecognized option(s)");
}
[Fact]
public async Task I_can_try_to_bind_an_option_to_a_scalar_property_and_get_an_error_if_the_user_provides_too_many_arguments()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// lang=csharp
"""
[Command]
public class Command : ICommand
{
[CommandOption("foo")]
public string? Foo { get; init; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
"""
);
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] { "--foo", "one", "two", "three" },
new Dictionary<string, string>()
);
// Assert
exitCode.Should().NotBe(0);
var stdErr = FakeConsole.ReadErrorString();
stdErr.Should().Contain("expects a single argument, but provided with multiple");
}
}