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 HelpTextSpecs : SpecsBase { public HelpTextSpecs(ITestOutputHelper testOutput) : base(testOutput) { } [Fact] public async Task Help_text_is_printed_if_no_arguments_are_provided_and_the_default_command_is_not_defined() { // Arrange var application = new CliApplicationBuilder() .UseConsole(FakeConsole) .SetDescription("This will be in help text") .Build(); // Act var exitCode = await application.RunAsync( Array.Empty(), new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); // Assert exitCode.Should().Be(0); stdOut.Should().Contain("This will be in help text"); } [Fact] public async Task Help_text_is_printed_if_provided_arguments_contain_the_help_option() { // Arrange var commandType = DynamicCommandBuilder.Compile( // language=cs @" [Command] public class DefaultCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } "); var application = new CliApplicationBuilder() .AddCommand(commandType) .UseConsole(FakeConsole) .SetDescription("This will be in help text") .Build(); // Act var exitCode = await application.RunAsync( new[] {"--help"}, new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); // Assert exitCode.Should().Be(0); stdOut.Should().Contain("This will be in help text"); } [Fact] public async Task Help_text_is_printed_if_provided_arguments_contain_the_help_option_even_if_the_default_command_is_not_defined() { // Arrange var commandTypes = DynamicCommandBuilder.CompileMany( // language=cs @" [Command(""cmd"")] public class NamedCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } [Command(""cmd child"")] public class NamedChildCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } "); var application = new CliApplicationBuilder() .AddCommands(commandTypes) .UseConsole(FakeConsole) .SetDescription("This will be in help text") .Build(); // Act var exitCode = await application.RunAsync( new[] {"--help"}, new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); // Assert exitCode.Should().Be(0); stdOut.Should().Contain("This will be in help text"); } [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 commandTypes = DynamicCommandBuilder.CompileMany( // language=cs @" [Command] public class DefaultCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } [Command(""cmd"", Description = ""Description of a named command."")] public class NamedCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } [Command(""cmd child"")] public class NamedChildCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } "); var application = new CliApplicationBuilder() .AddCommands(commandTypes) .UseConsole(FakeConsole) .Build(); // Act var exitCode = await application.RunAsync( new[] {"cmd", "--help"}, new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); // Assert exitCode.Should().Be(0); stdOut.Should().Contain("Description of a named command."); } [Fact] public async Task Help_text_for_a_specific_named_child_command_is_printed_if_provided_arguments_match_its_name_and_contain_the_help_option() { // Arrange var commandTypes = DynamicCommandBuilder.CompileMany( // language=cs @" [Command] public class DefaultCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } [Command(""cmd"")] public class NamedCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } [Command(""cmd child"", Description = ""Description of a named child command."")] public class NamedChildCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } "); var application = new CliApplicationBuilder() .AddCommands(commandTypes) .UseConsole(FakeConsole) .Build(); // Act var exitCode = await application.RunAsync( new[] {"cmd", "sub", "--help"}, new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); // Assert exitCode.Should().Be(0); stdOut.Should().Contain("Description of a named child command."); } [Fact] public async Task Help_text_is_printed_on_invalid_user_input() { // Arrange var application = new CliApplicationBuilder() .AddCommand() .UseConsole(FakeConsole) .SetDescription("This will be in help text") .Build(); // Act var exitCode = await application.RunAsync( new[] {"invalid-command", "--invalid-option"}, new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); var stdErr = FakeConsole.ReadErrorString(); // Assert exitCode.Should().NotBe(0); stdOut.Should().Contain("This will be in help text"); stdErr.Should().NotBeNullOrWhiteSpace(); } [Fact] public async Task Help_text_shows_application_metadata() { // Arrange var application = new CliApplicationBuilder() .UseConsole(FakeConsole) .SetTitle("App title") .SetDescription("App description") .SetVersion("App version") .Build(); // Act var exitCode = await application.RunAsync( new[] {"--help"}, new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); // Assert exitCode.Should().Be(0); stdOut.Should().ContainAll( "App title", "App description", "App version" ); } [Fact] public async Task Help_text_shows_command_description() { // Arrange var commandType = DynamicCommandBuilder.Compile( // language=cs @" [Command(Description = ""Description of the default command."")] public class DefaultCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } "); var application = new CliApplicationBuilder() .AddCommand(commandType) .UseConsole(FakeConsole) .Build(); // Act var exitCode = await application.RunAsync( new[] {"--help"}, new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); // Assert exitCode.Should().Be(0); stdOut.Should().ContainAllInOrder( "DESCRIPTION", "Description of the default command." ); } [Fact] public async Task Help_text_shows_usage_format_which_indicates_how_to_execute_a_named_command() { // Arrange var commandTypes = DynamicCommandBuilder.CompileMany( // language=cs @" [Command] public class DefaultCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } [Command(""cmd"")] public class NamedCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } "); var application = new CliApplicationBuilder() .AddCommands(commandTypes) .UseConsole(FakeConsole) .Build(); // Act var exitCode = await application.RunAsync( new[] {"--help"}, new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); // Assert exitCode.Should().Be(0); stdOut.Should().ContainAllInOrder( "USAGE", "[command]", "[...]" ); } [Fact] public async Task Help_text_shows_usage_format_which_lists_all_parameters() { // Arrange var commandType = DynamicCommandBuilder.Compile( // language=cs @" [Command] public class Command : ICommand { [CommandParameter(0)] public string Foo { get; set; } [CommandParameter(1)] public string Bar { get; set; } [CommandParameter(2)] public IReadOnlyList Baz { get; set; } public ValueTask ExecuteAsync(IConsole console) => default; } "); var application = new CliApplicationBuilder() .AddCommand(commandType) .UseConsole(FakeConsole) .Build(); // Act var exitCode = await application.RunAsync( new[] {"--help"}, new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); // Assert exitCode.Should().Be(0); stdOut.Should().ContainAllInOrder( "USAGE", "", "", "" ); } [Fact] public async Task Help_text_shows_usage_format_which_lists_all_required_options() { // Arrange var commandType = DynamicCommandBuilder.Compile( // language=cs @" [Command] public class Command : ICommand { [CommandOption(""foo"", IsRequired = true)] public string Foo { get; set; } [CommandOption(""bar"")] public string Bar { get; set; } [CommandOption(""baz"", IsRequired = true)] public IReadOnlyList Baz { get; set; } public ValueTask ExecuteAsync(IConsole console) => default; } "); var application = new CliApplicationBuilder() .AddCommand(commandType) .UseConsole(FakeConsole) .Build(); // Act var exitCode = await application.RunAsync( new[] {"--help"}, new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); // Assert exitCode.Should().Be(0); stdOut.Should().ContainAllInOrder( "USAGE", "--foo ", "--baz ", "[options]" ); } [Fact] public async Task Help_text_shows_all_parameters_and_options() { // Arrange var commandType = DynamicCommandBuilder.Compile( // language=cs @" [Command] public class Command : ICommand { [CommandParameter(0, Name = ""foo"", Description = ""Description of foo."")] public string Foo { get; set; } [CommandOption(""bar"", Description = ""Description of bar."")] public string Bar { get; set; } public ValueTask ExecuteAsync(IConsole console) => default; } "); var application = new CliApplicationBuilder() .AddCommand(commandType) .UseConsole(FakeConsole) .Build(); // Act var exitCode = await application.RunAsync( new[] {"--help"}, new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); // Assert exitCode.Should().Be(0); stdOut.Should().ContainAllInOrder( "PARAMETERS", "foo", "Description of foo.", "OPTIONS", "--bar", "Description of bar." ); } [Fact] public async Task Help_text_shows_the_implicit_help_and_version_options_on_the_default_command() { // Arrange var commandType = DynamicCommandBuilder.Compile( // language=cs @" [Command] public class Command : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } "); var application = new CliApplicationBuilder() .AddCommand(commandType) .UseConsole(FakeConsole) .Build(); // Act var exitCode = await application.RunAsync( new[] {"--help"}, new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); // Assert exitCode.Should().Be(0); stdOut.Should().ContainAllInOrder( "OPTIONS", "-h", "--help", "Shows help text", "--version", "Shows version information" ); } [Fact] public async Task Help_text_shows_the_implicit_help_option_but_not_the_version_option_on_a_named_command() { // Arrange var commandType = DynamicCommandBuilder.Compile( // language=cs @" [Command(""cmd"")] public class Command : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } "); var application = new CliApplicationBuilder() .AddCommand(commandType) .UseConsole(FakeConsole) .Build(); // Act var exitCode = await application.RunAsync( new[] {"cmd", "--help"}, new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); // Assert exitCode.Should().Be(0); stdOut.Should().ContainAllInOrder( "OPTIONS", "-h", "--help", "Shows help text" ); stdOut.Should().NotContainAny( "--version", "Shows version information" ); } [Fact] public async Task Help_text_shows_all_valid_values_for_enum_parameters_and_options() { // Arrange var commandType = DynamicCommandBuilder.Compile( // language=cs @" public enum CustomEnum { One, Two, Three } [Command] public class Command : ICommand { [CommandParameter(0)] public CustomEnum Foo { get; set; } [CommandOption(""bar"")] public CustomEnum Bar { get; set; } public ValueTask ExecuteAsync(IConsole console) => default; } "); var application = new CliApplicationBuilder() .AddCommand(commandType) .UseConsole(FakeConsole) .Build(); // Act var exitCode = await application.RunAsync( new[] {"--help"}, new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); // Assert exitCode.Should().Be(0); stdOut.Should().ContainAllInOrder( "PARAMETERS", "foo", "Choices:", "One", "Two", "Three", "OPTIONS", "--bar", "Choices:", "One", "Two", "Three" ); } [Fact] public async Task Help_text_shows_all_valid_values_for_non_scalar_enum_parameters_and_options() { // Arrange var commandType = DynamicCommandBuilder.Compile( // language=cs @" public enum CustomEnum { One, Two, Three } [Command] public class Command : ICommand { [CommandParameter(0)] public IReadOnlyList Foo { get; set; } [CommandOption(""bar"")] public IReadOnlyList Bar { get; set; } public ValueTask ExecuteAsync(IConsole console) => default; } "); var application = new CliApplicationBuilder() .AddCommand(commandType) .UseConsole(FakeConsole) .Build(); // Act var exitCode = await application.RunAsync( new[] {"--help"}, new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); // Assert exitCode.Should().Be(0); stdOut.Should().ContainAllInOrder( "PARAMETERS", "foo", "Choices:", "One", "Two", "Three", "OPTIONS", "--bar", "Choices:", "One", "Two", "Three" ); } [Fact] public async Task Help_text_shows_all_valid_values_for_nullable_enum_parameters_and_options() { // Arrange var commandType = DynamicCommandBuilder.Compile( // language=cs @" public enum CustomEnum { One, Two, Three } [Command] public class Command : ICommand { [CommandParameter(0)] public CustomEnum? Foo { get; set; } [CommandOption(""bar"")] public IReadOnlyList Bar { get; set; } public ValueTask ExecuteAsync(IConsole console) => default; } "); var application = new CliApplicationBuilder() .AddCommand(commandType) .UseConsole(FakeConsole) .Build(); // Act var exitCode = await application.RunAsync( new[] {"--help"}, new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); // Assert exitCode.Should().Be(0); stdOut.Should().ContainAllInOrder( "PARAMETERS", "foo", "Choices:", "One", "Two", "Three", "OPTIONS", "--bar", "Choices:", "One", "Two", "Three" ); } [Fact] public async Task Help_text_shows_environment_variables_for_options_that_have_them_configured_as_fallback() { // Arrange var commandType = DynamicCommandBuilder.Compile( // language=cs @" public enum CustomEnum { One, Two, Three } [Command] public class Command : ICommand { [CommandOption(""foo"", EnvironmentVariable = ""ENV_FOO"")] public CustomEnum Foo { get; set; } [CommandOption(""bar"", EnvironmentVariable = ""ENV_BAR"")] public CustomEnum Bar { get; set; } public ValueTask ExecuteAsync(IConsole console) => default; } "); var application = new CliApplicationBuilder() .AddCommand(commandType) .UseConsole(FakeConsole) .Build(); // Act var exitCode = await application.RunAsync( new[] {"--help"}, new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); // Assert exitCode.Should().Be(0); stdOut.Should().ContainAllInOrder( "OPTIONS", "--foo", "Environment variable:", "ENV_FOO", "--bar", "Environment variable:", "ENV_BAR" ); } [Fact] public async Task Help_text_shows_default_values_for_non_required_options() { // Arrange var commandType = DynamicCommandBuilder.Compile( // language=cs @" public enum CustomEnum { One, Two, Three } [Command] public class Command : ICommand { [CommandOption(""foo"")] public object Foo { get; set; } = 42; [CommandOption(""bar"")] public string Bar { get; set; } = ""hello""; [CommandOption(""baz"")] public IReadOnlyList Baz { get; set; } = new[] {""one"", ""two"", ""three""}; [CommandOption(""qwe"")] public bool Qwe { get; set; } = true; [CommandOption(""qop"")] public int? Qop { get; set; } = 1337; [CommandOption(""zor"")] public TimeSpan Zor { get; set; } = TimeSpan.FromMinutes(123); [CommandOption(""lol"")] public CustomEnum Lol { get; set; } = CustomEnum.Two; [CommandOption(""hmm"", IsRequired = true)] public string Hmm { get; set; } = ""not printed""; public ValueTask ExecuteAsync(IConsole console) => default; } "); var application = new CliApplicationBuilder() .AddCommand(commandType) .UseConsole(FakeConsole) .Build(); // Act var exitCode = await application.RunAsync( new[] {"--help"}, new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); // Assert exitCode.Should().Be(0); stdOut.Should().ContainAllInOrder( "OPTIONS", "--foo", "Default:", "42", "--bar", "Default:", "hello", "--baz", "Default:", "one", "two", "three", "--qwe", "Default:", "True", "--qop", "Default:", "1337", "--zor", "Default:", "02:03:00", "--lol", "Default:", "Two" ); stdOut.Should().NotContain("not printed"); } [Fact] public async Task Help_text_shows_all_immediate_child_commands() { // Arrange var commandTypes = DynamicCommandBuilder.CompileMany( // language=cs @" [Command(""cmd1"", Description = ""Description of one command."")] public class FirstCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } [Command(""cmd2"", Description = ""Description of another command."")] public class SecondCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } [Command(""cmd2 child"", Description = ""Description of another command's child command."")] public class SecondCommandChildCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } "); var application = new CliApplicationBuilder() .AddCommands(commandTypes) .UseConsole(FakeConsole) .Build(); // Act var exitCode = await application.RunAsync( new[] {"--help"}, new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); // Assert exitCode.Should().Be(0); stdOut.Should().ContainAllInOrder( "COMMANDS", "cmd1", "Description of one command.", "cmd2", "Description of another command." ); // `cmd2 child` will still appear in the list of `cmd2` subcommands, // but its description will not be seen. stdOut.Should().NotContain( "Description of another command's child command." ); } [Fact] public async Task Help_text_shows_all_immediate_child_commands_of_each_child_command() { // Arrange var commandTypes = DynamicCommandBuilder.CompileMany( // language=cs @" [Command(""cmd1"")] public class FirstCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } [Command(""cmd1 child1"")] public class FirstCommandFirstChildCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } [Command(""cmd2"")] public class SecondCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } [Command(""cmd2 child11"")] public class SecondCommandFirstChildCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } [Command(""cmd2 child2"")] public class SecondCommandSecondChildCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } "); var application = new CliApplicationBuilder() .AddCommands(commandTypes) .UseConsole(FakeConsole) .Build(); // Act var exitCode = await application.RunAsync( new[] {"--help"}, new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); // Assert exitCode.Should().Be(0); stdOut.Should().ContainAllInOrder( "COMMANDS", "cmd1", "Subcommands:", "cmd1 child1", "cmd2", "Subcommands:", "cmd2 child1", "cmd2 child2" ); } [Fact] public async Task Help_text_shows_non_immediate_child_commands_if_they_do_not_have_a_more_specific_parent() { // Arrange var commandTypes = DynamicCommandBuilder.CompileMany( // language=cs @" [Command(""cmd1"")] public class FirstCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } [Command(""cmd2 child1"")] public class SecondCommandFirstChildCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } [Command(""cmd2 child2"")] public class SecondCommandSecondChildCommand : ICommand { public ValueTask ExecuteAsync(IConsole console) => default; } "); var application = new CliApplicationBuilder() .AddCommands(commandTypes) .UseConsole(FakeConsole) .Build(); // Act var exitCode = await application.RunAsync( new[] {"--help"}, new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); // Assert exitCode.Should().Be(0); stdOut.Should().ContainAllInOrder( "COMMANDS", "cmd1", "cmd2 child1", "cmd2 child2" ); } [Fact] public async Task Version_text_is_printed_if_provided_arguments_contain_the_version_option() { // Arrange var application = new CliApplicationBuilder() .AddCommand() .SetVersion("v6.9") .UseConsole(FakeConsole) .Build(); // Act var exitCode = await application.RunAsync( new[] {"--version"}, new Dictionary() ); var stdOut = FakeConsole.ReadOutputString(); // Assert exitCode.Should().Be(0); stdOut.Trim().Should().Be("v6.9"); } } }