From 79715daacf2e4ec5b2825a1aab6ab8d2e8a1f134 Mon Sep 17 00:00:00 2001 From: Ken Swan <49496839+kenswan@users.noreply.github.com> Date: Thu, 13 Nov 2025 09:42:04 -0600 Subject: [PATCH] Add Root Commmand Support --- .../CommandLineSample.Standalone/Program.cs | 18 +- .../Properties/launchSettings.json | 4 + .../ReportCommandInitializer.cs | 15 +- samples/CommandLineSample/ReportCommand.cs | 17 +- .../AutomationConsoleApplication.cs | 2 +- src/CommandLine/src/AutomationCommand.cs | 20 +- .../src/AutomationCommandExtensions.cs | 34 ++- src/CommandLine/src/AutomationConsole.cs | 13 +- src/CommandLine/src/AutomationRootCommand.cs | 32 +++ .../src/Builder/AutomationConsoleBuilder.cs | 6 +- src/CommandLine/src/IAutomationCommand.cs | 46 ++++ .../src/IAutomationCommandInitializer.cs | 2 +- .../test/AutomationCommandExtensionsTests.cs | 187 +++++++++++++++ .../test/AutomationCommandTests.cs | 16 +- .../test/AutomationIoConsoleTests.Basic.cs | 4 +- .../test/AutomationRootCommandTests.cs | 213 ++++++++++++++++++ .../Builder/AutomationConsoleBuilderTests.cs | 11 +- .../Commands/BasicTestCommandInitializer.cs | 8 +- .../Commands/FullTestCommandInitializer.cs | 4 +- .../TestServiceWithAsyncCommandInitializer.cs | 4 +- ...tServiceWithExceptionCommandInitializer.cs | 4 +- ...rviceWithoutExceptionCommandInitializer.cs | 4 +- 22 files changed, 587 insertions(+), 77 deletions(-) create mode 100644 src/CommandLine/src/AutomationRootCommand.cs create mode 100644 src/CommandLine/src/IAutomationCommand.cs create mode 100644 src/CommandLine/test/AutomationCommandExtensionsTests.cs create mode 100644 src/CommandLine/test/AutomationRootCommandTests.cs diff --git a/samples/CommandLineSample.Standalone/Program.cs b/samples/CommandLineSample.Standalone/Program.cs index 3e4f73f..0d3761e 100644 --- a/samples/CommandLineSample.Standalone/Program.cs +++ b/samples/CommandLineSample.Standalone/Program.cs @@ -54,7 +54,6 @@ Option limitOption = new("--limit", "-l") { Description = "Max amount of products to display.", - }; Option headerOption = new("--header", "-h") @@ -62,7 +61,7 @@ Description = "Header to display above product results." }; -AutomationCommand rootCommand = +AutomationRootCommand rootCommand = AutomationConsole.CreateRootCommand( commandAutomationContext, "Sample CommandLine Example (with standalone commands)"); @@ -75,16 +74,23 @@ // Manual Command Building Example AutomationCommand subCommand = rootCommand.AddCommand("report", "Generate various reports"); -subCommand.Options.Add(reportTypeOption); -subCommand.Options.Add(limitOption); -subCommand.Options.Add(headerOption); +subCommand.Add(reportTypeOption); +subCommand.Add(limitOption); +subCommand.Add(headerOption); subCommand.SetAction((result, automationContext) => { + // Report Type and Limit are mapped to configuration via + // the AutomationContext ConfigurationMapping set in SetConfigurationMapping (above) string reportType = result.GetValue(reportTypeOption); string limit = result.GetValue(limitOption); - string headerText = result.GetValue(headerOption); + Console.WriteLine($"Report Type (Override): {reportType}"); + Console.WriteLine($"Limit (Override): {limit}"); + Console.WriteLine("Generating report..."); + + string headerText = result.GetValue(headerOption); + UpdateGreeting(headerText, automationContext.ServiceProvider.GetService()); }); diff --git a/samples/CommandLineSample.Standalone/Properties/launchSettings.json b/samples/CommandLineSample.Standalone/Properties/launchSettings.json index 1824ce0..ade2c3a 100644 --- a/samples/CommandLineSample.Standalone/Properties/launchSettings.json +++ b/samples/CommandLineSample.Standalone/Properties/launchSettings.json @@ -10,6 +10,10 @@ "CommandLineSample (Standalone) - Automation": { "commandName": "Project", "commandLineArgs": "report-automated --header test --limit 5 -t Company" + }, + "CommandLineSample (Standalone) - Help": { + "commandName": "Project", + "commandLineArgs": "report --help" } } } diff --git a/samples/CommandLineSample.Standalone/ReportCommandInitializer.cs b/samples/CommandLineSample.Standalone/ReportCommandInitializer.cs index 41028f5..d54d7ad 100644 --- a/samples/CommandLineSample.Standalone/ReportCommandInitializer.cs +++ b/samples/CommandLineSample.Standalone/ReportCommandInitializer.cs @@ -12,7 +12,7 @@ namespace CommandLineSample.Standalone; internal class ReportCommandInitializer : IAutomationCommandInitializer { - public void Initialize(AutomationCommand command) + public void Initialize(IAutomationCommand command) { Option reportTypeOption = new("--optionOne", "-t") { @@ -30,14 +30,21 @@ public void Initialize(AutomationCommand command) Description = "Header to display above product results." }; - command.Options.Add(reportTypeOption); - command.Options.Add(limitOption); - command.Options.Add(headerOption); + command.Add(reportTypeOption); + command.Add(limitOption); + command.Add(headerOption); command.SetAction((result, automationContext) => { + // Report Type and Limit are mapped to configuration via + // the AutomationContext ConfigurationMapping set up in Program.cs string reportType = result.GetValue(reportTypeOption); string limit = result.GetValue(limitOption); + + Console.WriteLine($"Report Type (Override): {reportType}"); + Console.WriteLine($"Limit (Override): {limit}"); + Console.WriteLine("Generating report..."); + string headerText = result.GetValue(headerOption); UpdateGreeting(headerText, automationContext.ServiceProvider.GetService()); diff --git a/samples/CommandLineSample/ReportCommand.cs b/samples/CommandLineSample/ReportCommand.cs index a0709f8..4eea3b7 100644 --- a/samples/CommandLineSample/ReportCommand.cs +++ b/samples/CommandLineSample/ReportCommand.cs @@ -12,7 +12,7 @@ namespace CommandLineSample; internal class ReportCommand : IAutomationCommandInitializer { - public void Initialize(AutomationCommand command) + public void Initialize(IAutomationCommand command) { Option reportTypeOption = new("--optionOne", "-t") { @@ -30,16 +30,23 @@ public void Initialize(AutomationCommand command) Description = "Header to display above product results." }; - command.Options.Add(reportTypeOption); - command.Options.Add(limitOption); - command.Options.Add(headerOption); + command.Add(reportTypeOption); + command.Add(limitOption); + command.Add(headerOption); command.SetAction((result, automationContext) => { + // Report Type and Limit are mapped to configuration via + // the AutomationContext ConfigurationMapping set up in Program.cs string reportType = result.GetValue(reportTypeOption); string limit = result.GetValue(limitOption); - string headerText = result.GetValue(headerOption); + Console.WriteLine($"Report Type (Override): {reportType}"); + Console.WriteLine($"Limit (Override): {limit}"); + Console.WriteLine("Generating report..."); + + string headerText = result.GetValue(headerOption); + UpdateGreeting(headerText, automationContext.ServiceProvider.GetService()); }); } diff --git a/src/CommandLine/src/Application/AutomationConsoleApplication.cs b/src/CommandLine/src/Application/AutomationConsoleApplication.cs index a2725e8..c115581 100644 --- a/src/CommandLine/src/Application/AutomationConsoleApplication.cs +++ b/src/CommandLine/src/Application/AutomationConsoleApplication.cs @@ -6,7 +6,7 @@ namespace AutomationIoC.CommandLine.Application; internal class AutomationConsoleApplication( - AutomationCommand rootCommand, + AutomationRootCommand rootCommand, string[]? arguments = null) : IAutomationConsole { private readonly string[] arguments = arguments ?? Environment.GetCommandLineArgs(); diff --git a/src/CommandLine/src/AutomationCommand.cs b/src/CommandLine/src/AutomationCommand.cs index a212aa8..c93646e 100644 --- a/src/CommandLine/src/AutomationCommand.cs +++ b/src/CommandLine/src/AutomationCommand.cs @@ -16,27 +16,17 @@ namespace AutomationIoC.CommandLine; /// Automation Services and configurations public class AutomationCommand( string name, - string? description, - IAutomationContext automationContext) : Command(name, description) + IAutomationContext automationContext, + string? description = null) : Command(name, description), IAutomationCommand { - /// - /// Services and configurations for dependency injection. - /// + /// public IAutomationContext Context => automationContext; - /// - /// Set the action to be executed when the command is invoked (synchronous). - /// The action receives the parsed result and the automation context, allowing for dependency injection. - /// - /// Dependency Injection Action for synchronous command invocation + /// public void SetAction(Action action) => SetAction(parsedResult => action(parsedResult, automationContext)); - /// - /// Set the action to be executed when the command is invoked (asynchronous). - /// The action receives the parsed result and the automation context, allowing for dependency injection. - /// - /// Dependency Injection Action for asynchronous command invocation + /// public void SetAction(Func action) => SetAction(async (parsedResult, cancellationToken) => { diff --git a/src/CommandLine/src/AutomationCommandExtensions.cs b/src/CommandLine/src/AutomationCommandExtensions.cs index 733d6ec..0f4236b 100644 --- a/src/CommandLine/src/AutomationCommandExtensions.cs +++ b/src/CommandLine/src/AutomationCommandExtensions.cs @@ -3,6 +3,8 @@ // Licensed under the MIT License // ------------------------------------------------------- +using System.CommandLine; + namespace AutomationIoC.CommandLine; /// @@ -20,7 +22,7 @@ public static class AutomationCommandExtensions /// New Child Command /// Thrown if parent does not have an established automation context public static AutomationCommand AddCommand( - this AutomationCommand parentCommand, + this IAutomationCommand parentCommand, string name, string description) where T : IAutomationCommandInitializer, new() @@ -30,10 +32,19 @@ public static AutomationCommand AddCommand( throw new InvalidOperationException("Parent Command is missing automation context."); } - var childAutomationCommand = new AutomationCommand(name, description, parentCommand.Context); + var childAutomationCommand = new AutomationCommand(name, parentCommand.Context, description); var commandInitializer = new T(); commandInitializer.Initialize(childAutomationCommand); - parentCommand.Subcommands.Add(childAutomationCommand); + + if (parentCommand is Command parentAsCommand) + { + parentAsCommand.Add(childAutomationCommand); + } + else + { + throw new InvalidOperationException( + "Parent Command must inherit from System.CommandLine.Command to add subcommands."); + } return childAutomationCommand; } @@ -47,17 +58,26 @@ public static AutomationCommand AddCommand( /// New Child Command /// public static AutomationCommand AddCommand( - this AutomationCommand parentCommand, + this IAutomationCommand parentCommand, string name, string description) { - if (parentCommand.Context is null) + if (parentCommand?.Context is null) { throw new InvalidOperationException("Parent Command is missing automation context."); } - var childAutomationCommand = new AutomationCommand(name, description, parentCommand.Context); - parentCommand.Subcommands.Add(childAutomationCommand); + var childAutomationCommand = new AutomationCommand(name, parentCommand.Context, description); + + if (parentCommand is Command parentAsCommand) + { + parentAsCommand.Add(childAutomationCommand); + } + else + { + throw new InvalidOperationException( + "Parent Command must inherit from System.CommandLine.Command to add subcommands."); + } return childAutomationCommand; } diff --git a/src/CommandLine/src/AutomationConsole.cs b/src/CommandLine/src/AutomationConsole.cs index 9b81e00..43dd889 100644 --- a/src/CommandLine/src/AutomationConsole.cs +++ b/src/CommandLine/src/AutomationConsole.cs @@ -5,7 +5,6 @@ using AutomationIoC.CommandLine.Builder; using AutomationIoC.Runtime; -using System.CommandLine; namespace AutomationIoC.CommandLine; @@ -26,7 +25,7 @@ public static IAutomationConsoleBuilder CreateDefaultBuilder(string? appDescript var automationContext = new AutomationContext(); var newAutomationCommand = - new AutomationCommand(RootCommand.ExecutableName, appDescription ?? string.Empty, automationContext); + new AutomationRootCommand(automationContext, appDescription ?? string.Empty); return new AutomationConsoleBuilder(newAutomationCommand, automationContext, args); } @@ -49,7 +48,7 @@ public static IAutomationConsoleBuilder CreateDefaultBuilder( var automationCommandInitializer = new T(); var newAutomationCommand = - new AutomationCommand(RootCommand.ExecutableName, appDescription ?? string.Empty, automationContext); + new AutomationRootCommand(automationContext, appDescription ?? string.Empty); automationCommandInitializer.Initialize(newAutomationCommand); @@ -63,12 +62,12 @@ public static IAutomationConsoleBuilder CreateDefaultBuilder( /// Automation context responsible for services /// Description of application tooling /// Root Automation Command - public static AutomationCommand CreateRootCommand( + public static AutomationRootCommand CreateRootCommand( IAutomationContext automationContext, string? appDescription = null) { var rootCommand = - new AutomationCommand(RootCommand.ExecutableName, appDescription ?? string.Empty, automationContext); + new AutomationRootCommand(automationContext, appDescription ?? string.Empty); return rootCommand; } @@ -81,7 +80,7 @@ public static AutomationCommand CreateRootCommand( /// Description of application tooling /// /// Root Automation Command - public static AutomationCommand CreateRootCommand( + public static AutomationRootCommand CreateRootCommand( IAutomationContext automationContext, string? appDescription = null) where T : IAutomationCommandInitializer, new() @@ -89,7 +88,7 @@ public static AutomationCommand CreateRootCommand( var automationCommandInitializer = new T(); var rootCommand = - new AutomationCommand(RootCommand.ExecutableName, appDescription ?? string.Empty, automationContext); + new AutomationRootCommand(automationContext, appDescription ?? string.Empty); automationCommandInitializer.Initialize(rootCommand); diff --git a/src/CommandLine/src/AutomationRootCommand.cs b/src/CommandLine/src/AutomationRootCommand.cs new file mode 100644 index 0000000..40f29e6 --- /dev/null +++ b/src/CommandLine/src/AutomationRootCommand.cs @@ -0,0 +1,32 @@ +// ------------------------------------------------------- +// Copyright (c) Ken Swan. All rights reserved. +// Licensed under the MIT License +// ------------------------------------------------------- + +using AutomationIoC.Runtime; +using System.CommandLine; + +namespace AutomationIoC.CommandLine; + +/// +/// Extends root command class with built-in dependency injection support for actions. +/// All sub-commands should inherit from AutomationCommand to leverage dependency injection. +/// +public class AutomationRootCommand( + IAutomationContext automationContext, + string description = "") : RootCommand(description), IAutomationCommand +{ + /// + public IAutomationContext Context => automationContext; + + /// + public void SetAction(Action action) => + SetAction(parsedResult => action(parsedResult, automationContext)); + + /// + public void SetAction(Func action) => + SetAction(async (parsedResult, cancellationToken) => + { + await action(parsedResult, automationContext, cancellationToken).ConfigureAwait(false); + }); +} diff --git a/src/CommandLine/src/Builder/AutomationConsoleBuilder.cs b/src/CommandLine/src/Builder/AutomationConsoleBuilder.cs index 3af1fbe..0d86f25 100644 --- a/src/CommandLine/src/Builder/AutomationConsoleBuilder.cs +++ b/src/CommandLine/src/Builder/AutomationConsoleBuilder.cs @@ -13,7 +13,7 @@ namespace AutomationIoC.CommandLine.Builder; internal class AutomationConsoleBuilder( - AutomationCommand rootCommand, + AutomationRootCommand rootCommand, AutomationContext automationContext, string[]? args = null) : IAutomationConsoleBuilder { @@ -50,7 +50,7 @@ public IAutomationConsoleBuilder AddCommand(params string[] commandPath) var automationCommandInitializer = new T(); var newAutomationCommand = - new AutomationCommand(addedCommandName, null, automationContext); + new AutomationCommand(addedCommandName, automationContext); automationCommandInitializer.Initialize(newAutomationCommand); @@ -88,5 +88,5 @@ public IAutomationConsoleBuilder WithConfigurationMapping(IDictionary new AutomationConsoleApplication(rootCommand, args); - internal AutomationCommand GetRootCommand() => rootCommand; + internal AutomationRootCommand GetRootCommand() => rootCommand; } diff --git a/src/CommandLine/src/IAutomationCommand.cs b/src/CommandLine/src/IAutomationCommand.cs new file mode 100644 index 0000000..b15acdb --- /dev/null +++ b/src/CommandLine/src/IAutomationCommand.cs @@ -0,0 +1,46 @@ +// ------------------------------------------------------- +// Copyright (c) Ken Swan. All rights reserved. +// Licensed under the MIT License +// ------------------------------------------------------- + +using AutomationIoC.Runtime; +using System.CommandLine; + +namespace AutomationIoC.CommandLine; + +/// +/// Extends command classes with built-in dependency injection support for actions. +/// +public interface IAutomationCommand +{ + /// + /// Services and configurations for dependency injection. + /// + IAutomationContext Context { get; } + + /// + /// Adds a to the command. + /// + /// The option to add to the command. + public void Add(Argument argument); + + /// + /// Adds a to the command. + /// + /// The option to add to the command. + public void Add(Option option); + + /// + /// Set the action to be executed when the command is invoked (synchronous). + /// The action receives the parsed result and the automation context, allowing for dependency injection. + /// + /// Dependency Injection Action for synchronous command invocation + void SetAction(Action action); + + /// + /// Set the action to be executed when the command is invoked (asynchronous). + /// The action receives the parsed result and the automation context, allowing for dependency injection. + /// + /// Dependency Injection Action for asynchronous command invocation + void SetAction(Func action); +} diff --git a/src/CommandLine/src/IAutomationCommandInitializer.cs b/src/CommandLine/src/IAutomationCommandInitializer.cs index dc12fa8..e604d89 100644 --- a/src/CommandLine/src/IAutomationCommandInitializer.cs +++ b/src/CommandLine/src/IAutomationCommandInitializer.cs @@ -21,5 +21,5 @@ public interface IAutomationCommandInitializer /// allowing it to be executed as part of the command line interface. /// /// Automation Command being configured for use - public void Initialize(AutomationCommand command); + public void Initialize(IAutomationCommand command); } diff --git a/src/CommandLine/test/AutomationCommandExtensionsTests.cs b/src/CommandLine/test/AutomationCommandExtensionsTests.cs new file mode 100644 index 0000000..adf7624 --- /dev/null +++ b/src/CommandLine/test/AutomationCommandExtensionsTests.cs @@ -0,0 +1,187 @@ +// ------------------------------------------------------- +// Copyright (c) Ken Swan. All rights reserved. +// Licensed under the MIT License +// ------------------------------------------------------- + +using AutomationIoC.Runtime; +using System.CommandLine; + +namespace AutomationIoC.CommandLine.Test; + +public class AutomationCommandExtensionsTests +{ + [Theory] + [InlineData(true, true)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(false, false)] + public void AddCommand_ShouldAddCommand(bool isRoot, bool hasInitializer) + { + const string expectedSubcommandName = "testCommand"; + const string expectedSubcommandDescription = "Test Command Description"; + + IAutomationCommand automationCommand = isRoot + ? new AutomationRootCommand( + description: "Test Root Command", + automationContext: new AutomationContext()) + : new AutomationCommand( + name: "ParentCommandName", + description: "Test Parent Command", + automationContext: new AutomationContext()); + + AutomationCommand subcommand = hasInitializer + ? automationCommand.AddCommand( + name: expectedSubcommandName, + description: expectedSubcommandDescription) + : automationCommand.AddCommand( + name: expectedSubcommandName, + description: expectedSubcommandDescription); + + Assert.IsType(subcommand, exactMatch: false); + + Command actualParentCommand = + Assert.IsType(automationCommand, exactMatch: false); + + Assert.Contains(subcommand, actualParentCommand.Children); + + Command actualSubcommand = Assert.Single(actualParentCommand.Subcommands); + Assert.Equal(expectedSubcommandName, actualSubcommand.Name); + Assert.Equal(expectedSubcommandDescription, actualSubcommand.Description); + } + + [Theory] + [InlineData(true, true)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(false, false)] + public void AddCommand_ShouldThrowExceptionWhenContextIsNull(bool isRoot, bool hasInitializer) + { + IAutomationContext automationContext = null; + + IAutomationCommand automationCommand = isRoot + ? new AutomationRootCommand( + description: "Test Root Command", + automationContext: automationContext) + : new AutomationCommand( + name: "ParentCommandName", + description: "Test Root Command", + automationContext: automationContext); + + InvalidOperationException exception = Assert.Throws(() => + { + AutomationCommand _ = hasInitializer + ? automationCommand.AddCommand( + name: "ShouldFail", + description: "This command should fail") + : automationCommand.AddCommand( + name: "ShouldFail", + description: "This command should fail"); + }); + + Assert.Equal("Parent Command is missing automation context.", exception.Message); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddCommand_ShouldThrowExceptionWhenNotBasedOnCommandType(bool hasInitializer) + { + IAutomationCommand automationCommand = new EmptyAutomationCommand(); + + InvalidOperationException exception = Assert.Throws(() => + { + AutomationCommand _ = hasInitializer + ? automationCommand.AddCommand( + name: "ShouldFail", + description: "This command should fail") + : automationCommand.AddCommand( + name: "ShouldFail", + description: "This command should fail"); + }); + + Assert.Equal( + expected: "Parent Command must inherit from System.CommandLine.Command to add subcommands.", + actual: exception.Message); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddCommand_ShouldInitializeNewCommandWithInitializer(bool isRoot) + { + const string expectedSubcommandName = "testCommand"; + const string expectedSubcommandDescription = "Test Command Description"; + + IAutomationCommand automationCommand = isRoot + ? new AutomationRootCommand( + description: "Test Root Command", + automationContext: new AutomationContext()) + : new AutomationCommand( + name: "ParentCommandName", + description: "Test Parent Command", + automationContext: new AutomationContext()); + + AutomationCommand subcommand = + automationCommand.AddCommand( + name: expectedSubcommandName, + description: expectedSubcommandDescription); + + Command actualParentCommand = + Assert.IsType(automationCommand, exactMatch: false); + + Assert.Contains(subcommand, actualParentCommand.Children); + Command actualSubcommand = Assert.Single(actualParentCommand.Subcommands); + + Argument argument = Assert.Single(actualSubcommand.Arguments); + Option option = Assert.Single(actualSubcommand.Options); + + Assert.Equal(DynamicAutomationCommandInitializer.ArgumentName, argument.Name); + Assert.Equal(DynamicAutomationCommandInitializer.ArgumentDescription, argument.Description); + Assert.Equal(DynamicAutomationCommandInitializer.OptionName, option.Name); + Assert.Equal(DynamicAutomationCommandInitializer.OptionDescription, option.Description); + } + + private class EmptyAutomationCommand : IAutomationCommand + { + public IAutomationContext Context => new AutomationContext(); + public void Add(Argument argument) => throw new NotImplementedException(); + + public void Add(Option option) => throw new NotImplementedException(); + + public void SetAction(Action action) => + throw new NotImplementedException(); + + public void SetAction(Func action) => + throw new NotImplementedException(); + } + + private class DynamicAutomationCommandInitializer : IAutomationCommandInitializer + { + public const string OptionName = "--testOption"; + public const string OptionDescription = "Description of a test option."; + public const string ArgumentName = "testArgument"; + public const string ArgumentDescription = "Description of a test argument."; + + public bool CommandInitialized { get; private set; } + public bool CommandWasCalled { get; private set; } + + public void Initialize(IAutomationCommand command) + { + Option option = new(name: OptionName) + { + Description = OptionDescription + }; + + Argument argument = new(ArgumentName) + { + Description = ArgumentDescription + }; + + command.Add(option); + command.Add(argument); + this.CommandInitialized = true; + + command.SetAction((parseResult, automationContext) => { this.CommandWasCalled = true; }); + } + } +} diff --git a/src/CommandLine/test/AutomationCommandTests.cs b/src/CommandLine/test/AutomationCommandTests.cs index a3e2601..85cb5cc 100644 --- a/src/CommandLine/test/AutomationCommandTests.cs +++ b/src/CommandLine/test/AutomationCommandTests.cs @@ -59,8 +59,8 @@ public void CreateCommand_ShouldCreateCommandWithAutomationContext() var command = new AutomationCommand( name: commandName, - description: "Test Command Description", - automationContext); + automationContext, + description: "Test Command Description"); string actualConfigurationValue = string.Empty; @@ -106,8 +106,8 @@ public async Task CreateCommand_ShouldCreateCommandWithAutomationContextAsyncAct var command = new AutomationCommand( name: commandName, - description: "Test Command Description", - automationContext); + automationContext, + description: "Test Command Description"); string actualConfigurationValue = string.Empty; @@ -162,13 +162,13 @@ public async Task Clone_ShouldCloneCommands() var commandOne = new AutomationCommand( name: commandOneName, - description: "Test Command Description", - automationContext); + automationContext, + description: "Test Command Description"); var commandOneSubCommand = new AutomationCommand( name: commandOneName, - description: "Test Command Description", - automationContext); + automationContext, + description: "Test Command Description"); commandOne.Subcommands.Add(commandOneSubCommand); diff --git a/src/CommandLine/test/AutomationIoConsoleTests.Basic.cs b/src/CommandLine/test/AutomationIoConsoleTests.Basic.cs index 68d4831..47b43d2 100644 --- a/src/CommandLine/test/AutomationIoConsoleTests.Basic.cs +++ b/src/CommandLine/test/AutomationIoConsoleTests.Basic.cs @@ -72,7 +72,7 @@ public void CreateRootCommand_ShouldCreateStandaloneRootCommandForManualConfigur string[] args = ["--optionOne", "testOption1"]; IAutomationContext context = new AutomationContext(); - AutomationCommand rootCommand = + AutomationRootCommand rootCommand = AutomationConsole.CreateRootCommand( automationContext: context, appDescription: "Test Application"); @@ -92,7 +92,7 @@ public void CreateRootCommand_ShouldCreateStandaloneRootCommandWithoutConfigurat string actualOptionOneValue = string.Empty; IAutomationContext context = new AutomationContext(); - AutomationCommand rootCommand = + AutomationRootCommand rootCommand = AutomationConsole.CreateRootCommand( automationContext: context, appDescription: "Test Application"); diff --git a/src/CommandLine/test/AutomationRootCommandTests.cs b/src/CommandLine/test/AutomationRootCommandTests.cs new file mode 100644 index 0000000..a19b915 --- /dev/null +++ b/src/CommandLine/test/AutomationRootCommandTests.cs @@ -0,0 +1,213 @@ +// ------------------------------------------------------- +// Copyright (c) Ken Swan. All rights reserved. +// Licensed under the MIT License +// ------------------------------------------------------- + +using AutomationIoC.CommandLine.Test.TestBed.Services; +using AutomationIoC.Runtime; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System.CommandLine; + +namespace AutomationIoC.CommandLine.Test; + +public class AutomationRootCommandTests +{ + [Fact] + public void CreateCommand_ShouldInitializeWithNameAndDescription() + { + // Arrange + const string commandDescription = "This is a test command"; + var automationContext = new AutomationContext(); + + // Act + var command = new AutomationRootCommand( + description: commandDescription, + automationContext: automationContext); + + // Assert + Assert.Equal(command.Name, RootCommand.ExecutableName); + Assert.Equal(commandDescription, command.Description); + } + + [Fact] + public void CreateCommand_ShouldCreateCommandWithAutomationContext() + { + // Arrange + const string configurationKey = "keyOne"; + const string configurationValue = "valueOne"; + var automationContext = new AutomationContext(); + + automationContext.SetConfigure((context, configurationBuilder) => + { + var appSettings = new Dictionary + { + { + configurationKey, configurationValue + } + }; + configurationBuilder.AddInMemoryCollection(appSettings); + }); + + automationContext.SetConfigureServices((context, services) => + { + services.AddTransient(); + }); + + var command = new AutomationRootCommand( + automationContext, + description: "Test Command Description"); + + string actualConfigurationValue = string.Empty; + + // Act + command.SetAction((parseResult, context) => + { + IServiceProvider serviceProvider = context.ServiceProvider; + TestConfigurationService testService = serviceProvider.GetRequiredService(); + actualConfigurationValue = testService.GetConfigurationValue(configurationKey); + }); + + int result = command.Parse(RootCommand.ExecutableName).Invoke(); + + // Assert + Assert.Equal(0, result); + Assert.Equal(configurationValue, actualConfigurationValue); + } + + [Fact] + public async Task CreateCommand_ShouldCreateCommandWithAutomationContextAsyncAction() + { + // Arrange + const string configurationKey = "keyOne"; + const string configurationValue = "valueOne"; + var automationContext = new AutomationContext(); + + automationContext.SetConfigure((context, configurationBuilder) => + { + var appSettings = new Dictionary + { + { + configurationKey, configurationValue + } + }; + configurationBuilder.AddInMemoryCollection(appSettings); + }); + + automationContext.SetConfigureServices((context, services) => + { + services.AddTransient(); + }); + + var command = new AutomationRootCommand( + automationContext, + description: "Test Command Description"); + + string actualConfigurationValue = string.Empty; + + // Act + command.SetAction(async (parseResult, context, cancellationToken) => + { + IServiceProvider serviceProvider = context.ServiceProvider; + + TestConfigurationService testService = + serviceProvider.GetRequiredService(); + + actualConfigurationValue = + await testService + .GetConfigurationValueAsync(configurationKey, cancellationToken) + .ConfigureAwait(false); + }); + + int result = await command.Parse(RootCommand.ExecutableName) + .InvokeAsync(cancellationToken: TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(0, result); + Assert.Equal(configurationValue, actualConfigurationValue); + } + + [Fact] + public async Task Clone_ShouldCloneCommands() + { + // Arrange + const string commandOneName = "testCommandOne"; + const string commandTwoName = "testCommandTwo"; + const string commandOptionValue = "test-option-value"; + const string configurationKey = "keyOne"; + const string configurationValue = "valueOne"; + var automationContext = new AutomationContext(); + + automationContext.SetConfigure((context, configurationBuilder) => + { + var appSettings = new Dictionary + { + { + configurationKey, configurationValue + } + }; + configurationBuilder.AddInMemoryCollection(appSettings); + }); + + automationContext.SetConfigureServices((context, services) => + { + services.AddTransient(); + }); + + var commandOne = new AutomationRootCommand( + automationContext, + description: "Test Command Description"); + + var commandOneSubCommand = new AutomationCommand( + name: commandOneName, + automationContext, + description: "Test Command Description"); + + commandOne.Add(commandOneSubCommand); + + var commandTwo = new Command( + name: commandTwoName, + description: "Test Command Description"); + + string actualConfigurationValue = string.Empty; + string actualOptionValue = string.Empty; + + Option passedInOption = new(name: "--optionOne") + { + Description = "Description of option one field." + }; + + commandOne.Add(passedInOption); + + commandOne.SetAction(async (parseResult, context, cancellationToken) => + { + actualOptionValue = parseResult.GetValue(passedInOption); + + IServiceProvider serviceProvider = context.ServiceProvider; + + TestConfigurationService testService = + serviceProvider.GetRequiredService(); + + actualConfigurationValue = + await testService + .GetConfigurationValueAsync(configurationKey, cancellationToken) + .ConfigureAwait(false); + }); + + // Act + AutomationCommand.Clone(source: commandOne, target: commandTwo); + + int result = await commandTwo + .Parse([commandTwoName, "--optionOne", commandOptionValue]) + .InvokeAsync(cancellationToken: TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(0, result); + Assert.Equal(configurationValue, actualConfigurationValue); + Assert.Equal(commandOptionValue, actualOptionValue); + Assert.Equivalent(commandOne.Options, commandTwo.Options); + Assert.Equivalent(commandOne.Subcommands, commandTwo.Subcommands); + Assert.Equivalent(commandOne.Arguments, commandTwo.Arguments); + Assert.Equivalent(commandOne.Aliases, commandTwo.Aliases); + } +} diff --git a/src/CommandLine/test/Builder/AutomationConsoleBuilderTests.cs b/src/CommandLine/test/Builder/AutomationConsoleBuilderTests.cs index d18de90..d4a0cc3 100644 --- a/src/CommandLine/test/Builder/AutomationConsoleBuilderTests.cs +++ b/src/CommandLine/test/Builder/AutomationConsoleBuilderTests.cs @@ -10,7 +10,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Moq; -using System.CommandLine; namespace AutomationIoC.CommandLine.Test.Builder; @@ -21,12 +20,12 @@ public void ShouldBuildCommandsWithAddCommand() { // Arrange var automationContext = new AutomationContext(); - var rootCommand = new AutomationCommand(RootCommand.ExecutableName, "Test Application", automationContext); + var rootCommand = new AutomationRootCommand(automationContext, "Test Application"); var automationConsoleBuilder = new AutomationConsoleBuilder(rootCommand, automationContext); automationConsoleBuilder.AddCommand("test", "subcommand"); - AutomationCommand configuredRootCommand = automationConsoleBuilder.GetRootCommand(); + AutomationRootCommand configuredRootCommand = automationConsoleBuilder.GetRootCommand(); // Act int invocationResult = @@ -44,7 +43,7 @@ public void ShouldBuildCommandsWithAutomationContext() const string expectedKey = "TestTheConfigKey"; string expectedKeyValue = Guid.NewGuid().ToString(); var automationContext = new AutomationContext(); - var rootCommand = new AutomationCommand(RootCommand.ExecutableName, "Test Application", automationContext); + var rootCommand = new AutomationRootCommand(automationContext, "Test Application"); var automationConsoleBuilder = new AutomationConsoleBuilder(rootCommand, automationContext); var testServiceMock = new Mock(); @@ -67,7 +66,7 @@ public void ShouldBuildCommandsWithAutomationContext() services.AddTransient(); }); - AutomationCommand configuredRootCommand = automationConsoleBuilder.GetRootCommand(); + AutomationRootCommand configuredRootCommand = automationConsoleBuilder.GetRootCommand(); // Act int invocationResult = @@ -88,7 +87,7 @@ public void ShouldBuildAutomationConsoleApplication() var automationContext = new AutomationContext(); var rootCommand = - new AutomationCommand(RootCommand.ExecutableName, "Test Application", automationContext); + new AutomationRootCommand(automationContext, "Test Application"); var automationConsoleBuilder = new AutomationConsoleBuilder(rootCommand, automationContext, ["full-test", "--key", expectedKey]); diff --git a/src/CommandLine/test/TestBed/Commands/BasicTestCommandInitializer.cs b/src/CommandLine/test/TestBed/Commands/BasicTestCommandInitializer.cs index 144d461..a6d384a 100644 --- a/src/CommandLine/test/TestBed/Commands/BasicTestCommandInitializer.cs +++ b/src/CommandLine/test/TestBed/Commands/BasicTestCommandInitializer.cs @@ -11,7 +11,7 @@ internal class BasicTestCommandInitializer : IAutomationCommandInitializer { private const string InternalTestData = "test"; - public void Initialize(AutomationCommand command) + public void Initialize(IAutomationCommand command) { Option passedInOption = new(name: "--optionOne") { @@ -24,10 +24,10 @@ public void Initialize(AutomationCommand command) DefaultValueFactory = _ => InternalTestData }; - command.Options.Add(passedInOption); - command.Options.Add(internalOption); + command.Add(passedInOption); + command.Add(internalOption); - command.SetAction(parseResult => + command.SetAction((parseResult, automationContext) => { string passedInOptionString = parseResult.GetValue(passedInOption); string internalOptionString = parseResult.GetValue(internalOption); diff --git a/src/CommandLine/test/TestBed/Commands/FullTestCommandInitializer.cs b/src/CommandLine/test/TestBed/Commands/FullTestCommandInitializer.cs index 9fda110..2dd5d1c 100644 --- a/src/CommandLine/test/TestBed/Commands/FullTestCommandInitializer.cs +++ b/src/CommandLine/test/TestBed/Commands/FullTestCommandInitializer.cs @@ -11,14 +11,14 @@ namespace AutomationIoC.CommandLine.Test.TestBed.Commands; public class FullTestCommandInitializer : IAutomationCommandInitializer { - public void Initialize(AutomationCommand command) + public void Initialize(IAutomationCommand command) { Option configurationKeyOption = new(name: "--key") { Description = "Name of key to pull from configuration." }; - command.Options.Add(configurationKeyOption); + command.Add(configurationKeyOption); command.SetAction(async (parseResult, context, cancellationToken) => { diff --git a/src/CommandLine/test/TestBed/Commands/TestServiceWithAsyncCommandInitializer.cs b/src/CommandLine/test/TestBed/Commands/TestServiceWithAsyncCommandInitializer.cs index cf67aa0..39b78f2 100644 --- a/src/CommandLine/test/TestBed/Commands/TestServiceWithAsyncCommandInitializer.cs +++ b/src/CommandLine/test/TestBed/Commands/TestServiceWithAsyncCommandInitializer.cs @@ -11,14 +11,14 @@ namespace AutomationIoC.CommandLine.Test.TestBed.Commands; internal class TestServiceWithAsyncCommandInitializer : IAutomationCommandInitializer { - public void Initialize(AutomationCommand command) + public void Initialize(IAutomationCommand command) { Option passedInOption = new(name: "--test") { Description = "Description of test option field." }; - command.Options.Add(passedInOption); + command.Add(passedInOption); command.SetAction(async (parseResult, automationContext, cancellationToken) => { diff --git a/src/CommandLine/test/TestBed/Commands/TestServiceWithExceptionCommandInitializer.cs b/src/CommandLine/test/TestBed/Commands/TestServiceWithExceptionCommandInitializer.cs index 2caa821..65dcda0 100644 --- a/src/CommandLine/test/TestBed/Commands/TestServiceWithExceptionCommandInitializer.cs +++ b/src/CommandLine/test/TestBed/Commands/TestServiceWithExceptionCommandInitializer.cs @@ -9,14 +9,14 @@ namespace AutomationIoC.CommandLine.Test.TestBed.Commands; internal class TestServiceWithExceptionCommandInitializer : IAutomationCommandInitializer { - public void Initialize(AutomationCommand command) + public void Initialize(IAutomationCommand command) { Option passedInOption = new(name: "--test") { Description = "Description of test option field." }; - command.Options.Add(passedInOption); + command.Add(passedInOption); command.SetAction((parseResult, automationContext) => throw new NotImplementedException()); } diff --git a/src/CommandLine/test/TestBed/Commands/TestServiceWithoutExceptionCommandInitializer.cs b/src/CommandLine/test/TestBed/Commands/TestServiceWithoutExceptionCommandInitializer.cs index a565ffb..fc317a6 100644 --- a/src/CommandLine/test/TestBed/Commands/TestServiceWithoutExceptionCommandInitializer.cs +++ b/src/CommandLine/test/TestBed/Commands/TestServiceWithoutExceptionCommandInitializer.cs @@ -11,14 +11,14 @@ namespace AutomationIoC.CommandLine.Test.TestBed.Commands; internal class TestServiceWithoutExceptionCommandInitializer : IAutomationCommandInitializer { - public void Initialize(AutomationCommand command) + public void Initialize(IAutomationCommand command) { Option passedInOption = new(name: "--test") { Description = "Description of test." }; - command.Options.Add(passedInOption); + command.Add(passedInOption); command.SetAction((parseResult, automationContext) => {