From 0c5075781cc0c515364facf722c1abc01f7ed6c1 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Sat, 3 May 2025 21:27:00 +0200 Subject: [PATCH 1/6] Init --- .../Commands.Tests.Console/Program.cs | 2 +- src/Commands/Commands.csproj | 5 + .../Abstractions/IExecutionProvider.cs | 2 +- .../Abstractions/ITest.cs} | 2 +- .../Abstractions/ITestCollection.cs | 22 +++ .../Components/Abstractions/ITestProvider.cs | 17 +++ .../Abstractions/TestResultType.cs | 0 .../Attributes}/TestAttribute.cs | 2 +- src/Commands/Testing/Components/Test.cs | 30 ++++ .../Testing/Components/TestCollection.cs | 98 +++++++++++++ src/Commands/Testing/Components/TestGroup.cs | 109 ++++++++++++++ .../Testing/Components/TestProperties.cs | 51 +++++++ .../Testing/Execution/TestCollection.cs | 138 ------------------ .../Execution/TestCollectionProperties.cs | 94 ------------ .../Testing/{ => Results}/TestResult.cs | 0 src/Commands/Testing/TestProvider.cs | 37 ----- .../Testing/TestProviderProperties.cs | 69 --------- src/Commands/Testing/TestUtilities.cs | 3 +- 18 files changed, 338 insertions(+), 343 deletions(-) rename src/Commands/Testing/{Abstractions/ITestProvider.cs => Components/Abstractions/ITest.cs} (95%) create mode 100644 src/Commands/Testing/Components/Abstractions/ITestCollection.cs create mode 100644 src/Commands/Testing/Components/Abstractions/ITestProvider.cs rename src/Commands/Testing/{ => Components}/Abstractions/TestResultType.cs (100%) rename src/Commands/Testing/{ => Components/Attributes}/TestAttribute.cs (86%) create mode 100644 src/Commands/Testing/Components/Test.cs create mode 100644 src/Commands/Testing/Components/TestCollection.cs create mode 100644 src/Commands/Testing/Components/TestGroup.cs create mode 100644 src/Commands/Testing/Components/TestProperties.cs delete mode 100644 src/Commands/Testing/Execution/TestCollection.cs delete mode 100644 src/Commands/Testing/Execution/TestCollectionProperties.cs rename src/Commands/Testing/{ => Results}/TestResult.cs (100%) delete mode 100644 src/Commands/Testing/TestProvider.cs delete mode 100644 src/Commands/Testing/TestProviderProperties.cs diff --git a/src/Commands.Tests/Commands.Tests.Console/Program.cs b/src/Commands.Tests/Commands.Tests.Console/Program.cs index 0c79554f..16698421 100644 --- a/src/Commands.Tests/Commands.Tests.Console/Program.cs +++ b/src/Commands.Tests/Commands.Tests.Console/Program.cs @@ -13,7 +13,7 @@ }, "help")) .ToCollection(); -var tests = TestCollection.From([.. components.GetCommands()]) +var tests = TestCollection.From(components.GetCommands()) .ToCollection(); var results = await tests.Execute((str) => new TestContext(str)); diff --git a/src/Commands/Commands.csproj b/src/Commands/Commands.csproj index e2ef75bf..e74312eb 100644 --- a/src/Commands/Commands.csproj +++ b/src/Commands/Commands.csproj @@ -52,5 +52,10 @@ \ + + + + + diff --git a/src/Commands/Core/Components/Abstractions/IExecutionProvider.cs b/src/Commands/Core/Components/Abstractions/IExecutionProvider.cs index d802ee99..0d1ba7e8 100644 --- a/src/Commands/Core/Components/Abstractions/IExecutionProvider.cs +++ b/src/Commands/Core/Components/Abstractions/IExecutionProvider.cs @@ -1,7 +1,7 @@ namespace Commands; /// -/// Defines mechanisms for executing commands based on a set of arguments. +/// Defines a mechanism for executing commands based on a set of arguments. /// public interface IExecutionProvider : IComponentCollection { diff --git a/src/Commands/Testing/Abstractions/ITestProvider.cs b/src/Commands/Testing/Components/Abstractions/ITest.cs similarity index 95% rename from src/Commands/Testing/Abstractions/ITestProvider.cs rename to src/Commands/Testing/Components/Abstractions/ITest.cs index 80f51059..d7e19b16 100644 --- a/src/Commands/Testing/Abstractions/ITestProvider.cs +++ b/src/Commands/Testing/Components/Abstractions/ITest.cs @@ -3,7 +3,7 @@ /// /// Represents a test provider that can be used to test a command. /// -public interface ITestProvider +public interface ITest { /// /// Gets or sets the result that the test should return. If the test does not return this result, it will be considered a failure. diff --git a/src/Commands/Testing/Components/Abstractions/ITestCollection.cs b/src/Commands/Testing/Components/Abstractions/ITestCollection.cs new file mode 100644 index 00000000..1952f791 --- /dev/null +++ b/src/Commands/Testing/Components/Abstractions/ITestCollection.cs @@ -0,0 +1,22 @@ +namespace Commands.Testing; + +/// +/// +/// +/// +public interface ITestCollection : ICollection, IEnumerable +{ + /// + /// + /// + /// + /// + //public int AddRange(IEnumerable items); + + /// + /// + /// + /// + /// + //public int RemoveRange(IEnumerable items); +} diff --git a/src/Commands/Testing/Components/Abstractions/ITestProvider.cs b/src/Commands/Testing/Components/Abstractions/ITestProvider.cs new file mode 100644 index 00000000..314d87fb --- /dev/null +++ b/src/Commands/Testing/Components/Abstractions/ITestProvider.cs @@ -0,0 +1,17 @@ +namespace Commands.Testing; + +/// +/// +/// +public interface ITestProvider : ITestCollection +{ + /// + /// + /// + /// + /// + /// + /// + public ValueTask> Execute(Func callerCreation, CommandOptions? options = null) + where TContext : class, ICallerContext; +} diff --git a/src/Commands/Testing/Abstractions/TestResultType.cs b/src/Commands/Testing/Components/Abstractions/TestResultType.cs similarity index 100% rename from src/Commands/Testing/Abstractions/TestResultType.cs rename to src/Commands/Testing/Components/Abstractions/TestResultType.cs diff --git a/src/Commands/Testing/TestAttribute.cs b/src/Commands/Testing/Components/Attributes/TestAttribute.cs similarity index 86% rename from src/Commands/Testing/TestAttribute.cs rename to src/Commands/Testing/Components/Attributes/TestAttribute.cs index bd454377..d2d04785 100644 --- a/src/Commands/Testing/TestAttribute.cs +++ b/src/Commands/Testing/Components/Attributes/TestAttribute.cs @@ -4,7 +4,7 @@ /// An attribute that is used to define a test for a command. /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class TestAttribute : Attribute, ITestProvider +public sealed class TestAttribute : Attribute, ITest { /// public TestResultType ShouldEvaluateTo { get; set; } = TestResultType.Success; diff --git a/src/Commands/Testing/Components/Test.cs b/src/Commands/Testing/Components/Test.cs new file mode 100644 index 00000000..65ad62f5 --- /dev/null +++ b/src/Commands/Testing/Components/Test.cs @@ -0,0 +1,30 @@ +namespace Commands.Testing; + +/// +public sealed class Test : ITest +{ + /// + public TestResultType ShouldEvaluateTo { get; } + + /// + public string Arguments { get; } + + internal Test(string arguments, TestResultType shouldEvaluateTo) + { + Arguments = arguments; + ShouldEvaluateTo = shouldEvaluateTo; + } + + #region Initializers + + /// + /// Defines a collection of properties to configure and convert into a new instance of . + /// + /// The arguments to test with. + /// The result to test for. + /// A fluent-pattern property object that can be converted into an instance when configured. + public static TestProperties From(string? arguments = null, TestResultType testResult = TestResultType.Success) + => new TestProperties().AddArguments(arguments).AddResult(testResult); + + #endregion +} \ No newline at end of file diff --git a/src/Commands/Testing/Components/TestCollection.cs b/src/Commands/Testing/Components/TestCollection.cs new file mode 100644 index 00000000..343e350c --- /dev/null +++ b/src/Commands/Testing/Components/TestCollection.cs @@ -0,0 +1,98 @@ +namespace Commands.Testing; + +/// +/// +/// +public sealed class TestCollection : ITestProvider +{ + private TestGroup[] _tests; + + /// + /// Gets the number of groups contained in this collection. + /// + public int Count + => _tests.Length; + + /// + /// + /// + /// + public TestCollection(params TestGroup[] tests) + { + _tests = tests; + } + + /// + /// + /// + /// + /// + /// + /// + public async ValueTask> Execute(Func callerCreation, CommandOptions? options = null) + where TContext : class, ICallerContext + { + options ??= new CommandOptions(); + + var results = new IEnumerable[_tests.Length]; + + for (var i = 0; i < _tests.Length; i++) + results[i] = await _tests[i].Run(callerCreation, options).ConfigureAwait(false); + + return results.SelectMany(x => x); + } + + /// + public void Add(TestGroup item) + { + Assert.NotNull(item, nameof(item)); + + Array.Resize(ref _tests, _tests.Length); + + _tests[_tests.Length] = item; + } + + /// + public void Clear() + => _tests = []; + + /// + public bool Contains(TestGroup item) + { + Assert.NotNull(item, nameof(item)); + + return _tests.Contains(item); + } + + /// + public void CopyTo(TestGroup[] array, int arrayIndex) + => _tests.CopyTo(array, arrayIndex); + + /// + public bool Remove(TestGroup item) + { + Assert.NotNull(item, nameof(item)); + + var indexOf = Array.IndexOf(_tests, item); + + if (indexOf == -1) + return false; + + for (var i = indexOf; i < _tests.Length - 1; i++) + _tests[i] = _tests[i + 1]; + + Array.Resize(ref _tests, _tests.Length - 1); + + return true; + } + + /// + public IEnumerator GetEnumerator() + => ((IEnumerable)_tests).GetEnumerator(); + + bool ICollection.IsReadOnly + => false; + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); +} diff --git a/src/Commands/Testing/Components/TestGroup.cs b/src/Commands/Testing/Components/TestGroup.cs new file mode 100644 index 00000000..bc8974d9 --- /dev/null +++ b/src/Commands/Testing/Components/TestGroup.cs @@ -0,0 +1,109 @@ +namespace Commands.Testing; + +/// +/// Represents a group of implementations to be tested against the that this group targets. +/// +public sealed class TestGroup : ITestCollection +{ + private ITest[] _tests; + + /// + /// Gets the command which the tests contained in this should be tested against. + /// + public Command Command { get; } + + /// + /// Gets the number of implementations contained in this group. + /// + public int Count + => _tests.Length; + + /// + /// Creates a new targetting the provided command, including the provided tests. + /// + /// + /// + public TestGroup(Command command, params ITest[] tests) + { + Assert.NotNull(command, nameof(command)); + Assert.NotNull(tests, nameof(tests)); + + Command = command; + + _tests = tests; + } + + /// + /// + /// + /// + /// + /// + public async ValueTask> Run(Func callerCreation, CommandOptions options) + where TContext : class, ICallerContext + { + Assert.NotNull(callerCreation, nameof(callerCreation)); + Assert.NotNull(options, nameof(options)); + + var results = new TestResult[_tests.Length]; + + for (var i = 0; i < _tests.Length; i++) + results[i] = await Command.TestAgainst(callerCreation, _tests[i], options).ConfigureAwait(false); + + return results; + } + + /// + public void Add(ITest item) + { + Assert.NotNull(item, nameof(item)); + + Array.Resize(ref _tests, _tests.Length); + + _tests[_tests.Length] = item; + } + + /// + public void Clear() + => _tests = []; + + /// + public bool Contains(ITest item) + { + Assert.NotNull(item, nameof(item)); + + return _tests.Contains(item); + } + + /// + public void CopyTo(ITest[] array, int arrayIndex) + => _tests.CopyTo(array, arrayIndex); + + /// + public bool Remove(ITest item) + { + Assert.NotNull(item, nameof(item)); + + var indexOf = Array.IndexOf(_tests, item); + + if (indexOf == -1) + return false; + + for (var i = indexOf; i < _tests.Length - 1; i++) + _tests[i] = _tests[i + 1]; + + Array.Resize(ref _tests, _tests.Length - 1); + + return true; + } + + /// + public IEnumerator GetEnumerator() + => ((IEnumerable)_tests).GetEnumerator(); + + bool ICollection.IsReadOnly + => false; + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); +} diff --git a/src/Commands/Testing/Components/TestProperties.cs b/src/Commands/Testing/Components/TestProperties.cs new file mode 100644 index 00000000..92448f57 --- /dev/null +++ b/src/Commands/Testing/Components/TestProperties.cs @@ -0,0 +1,51 @@ +namespace Commands.Testing; + +/// +/// A set of properties for a test provider. +/// +public sealed class TestProperties +{ + private string? _arguments; + private TestResultType _result; + + /// + /// Creates a new instance of . + /// + public TestProperties() + { + _result = TestResultType.Success; + } + + /// + /// Sets the arguments this provider should test with. + /// + /// The arguments to set. + /// The same for call-chaining. + public TestProperties AddArguments(string? arguments) + { + _arguments = arguments; + + return this; + } + + /// + /// Sets the result this provider should return. + /// + /// The result to set. + /// The same for call-chaining. + public TestProperties AddResult(TestResultType result) + { + _result = result; + + return this; + } + + /// + /// Converts the properties to a new instance of . + /// + /// A new instance of . + public Test ToTest() + { + return new Test(_arguments ?? string.Empty, _result); + } +} diff --git a/src/Commands/Testing/Execution/TestCollection.cs b/src/Commands/Testing/Execution/TestCollection.cs deleted file mode 100644 index 27fb0b31..00000000 --- a/src/Commands/Testing/Execution/TestCollection.cs +++ /dev/null @@ -1,138 +0,0 @@ - -namespace Commands.Testing; - -/// -/// A test collection that can be ran and evaluated per command instance. This class cannot be inherited. -/// -public sealed class TestCollection : IDictionary -{ - private readonly Dictionary _tests; - - /// - /// Gets the number of tests present for this runner. - /// - public int Count - => _tests.Sum(x => x.Value.Length); - - /// - public ICollection Keys - => _tests.Keys; - - /// - public ICollection Values - => _tests.Values; - - /// - public bool IsReadOnly { get; } = false; - - /// - public ITestProvider[] this[Command key] - { - get => _tests[key]; - set => _tests[key] = value; - } - - /// - /// Creates a new instance of with no tests. - /// - public TestCollection() - { - _tests = []; - } - - /// - /// Creates a new instance of with the specified tests. - /// - /// The tests, grouped by command that should - public TestCollection(Dictionary tests) - { - _tests = tests; - } - - /// - /// Starts all contained tests sequentially and returns the total result. - /// - /// An action for creating new context when a command is executed. The inbound string represents the tested command name and arguments. - /// The options to use when running the tests. - /// An awaitable that represents testing operation. - public async Task Execute(Func creationAction, CommandOptions? options = null) - { - options ??= new CommandOptions(); - - var arr = new TestResult[Count]; - var i = 0; - - foreach (var test in _tests) - { - options.CancellationToken.ThrowIfCancellationRequested(); - - foreach (var provider in test.Value) - { - arr[i] = await test.Key.Test(creationAction, provider, options); - i++; - } - } - - return arr; - } - - /// - public bool ContainsKey(Command key) - => _tests.ContainsKey(key); - - /// - public bool TryGetValue(Command key, -#if NET8_0_OR_GREATER - [MaybeNullWhen(false)] -#endif - out ITestProvider[] value) - => _tests.TryGetValue(key, out value); - - /// - public void Add(Command key, ITestProvider[] value) - => _tests.Add(key, value); - - /// - public bool Remove(Command key) - => _tests.Remove(key); - - /// - public void Clear() - => _tests.Clear(); - - /// - public bool Remove(KeyValuePair item) - => _tests.Remove(item.Key); - - /// - public IEnumerator> GetEnumerator() - => _tests.GetEnumerator(); - - #region Initializers - - /// - /// Defines a collection of properties to configure and convert into a new instance of , with the specified commands. - /// - /// A collection of commands to evaluate. Commands marked with will have test providers automatically defined for them. - /// A fluent-pattern property object that can be converted into an instance when configured. - public static TestCollectionProperties From(params Command[] commands) - => new TestCollectionProperties().AddCommands(commands); - - #endregion - - #region IDictionary<> - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - void ICollection>.Add(KeyValuePair item) - => _tests.Add(item.Key, item.Value); - - bool ICollection>.Contains(KeyValuePair item) - => _tests.ContainsKey(item.Key) && _tests[item.Key].SequenceEqual(item.Value); - - void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) - => ((ICollection>)_tests).CopyTo(array, arrayIndex); - - #endregion -} \ No newline at end of file diff --git a/src/Commands/Testing/Execution/TestCollectionProperties.cs b/src/Commands/Testing/Execution/TestCollectionProperties.cs deleted file mode 100644 index b0a9dd0f..00000000 --- a/src/Commands/Testing/Execution/TestCollectionProperties.cs +++ /dev/null @@ -1,94 +0,0 @@ -namespace Commands.Testing; - -/// -/// A set of properties for a test runner. -/// -public sealed class TestCollectionProperties -{ - private readonly List _tests; - private readonly List _commands; - - /// - /// Creates a new instance of . - /// - public TestCollectionProperties() - { - _tests = []; - _commands = []; - } - - /// - /// Adds a test provider to the test runner. - /// - /// The provider to add. - /// The same for call-chaining. - public TestCollectionProperties AddTest(TestProviderProperties provider) - { - Assert.NotNull(provider, nameof(provider)); - - _tests.Add(provider); - - return this; - } - - /// - /// Adds multiple test providers to the test runner. - /// - /// The providers to add. - /// The same for call-chaining. - public TestCollectionProperties AddTests(params TestProviderProperties[] providers) - { - foreach (var provider in providers) - AddTest(provider); - - return this; - } - - /// - /// Adds a command to the test runner. - /// - /// The command to add. - /// The same for call-chaining. - public TestCollectionProperties AddCommand(Command command) - { - Assert.NotNull(command, nameof(command)); - - _commands.Add(command); - - return this; - } - - /// - /// Adds multiple commands to the test runner. - /// - /// The commands to add. - /// The same for call-chaining. - public TestCollectionProperties AddCommands(params Command[] commands) - { - foreach (var command in commands) - AddCommand(command); - - return this; - } - - /// - /// Converts the properties to a new instance of . - /// - /// A new instance of . - public TestCollection ToCollection() - { - var tests = _commands.ToDictionary(x => x, x => x.Attributes.OfType().ToArray()); - - var runtimeDefined = _tests.Select(x => x.ToProvider()).GroupBy(x => x.Command); - - foreach (var group in runtimeDefined) - { - if (tests.TryGetValue(group.Key, out var value)) - tests[group.Key] = [.. value, .. group]; - else - tests[group.Key] = [.. group]; - } - - return new TestCollection(tests); - } -} \ No newline at end of file diff --git a/src/Commands/Testing/TestResult.cs b/src/Commands/Testing/Results/TestResult.cs similarity index 100% rename from src/Commands/Testing/TestResult.cs rename to src/Commands/Testing/Results/TestResult.cs diff --git a/src/Commands/Testing/TestProvider.cs b/src/Commands/Testing/TestProvider.cs deleted file mode 100644 index 05e19f83..00000000 --- a/src/Commands/Testing/TestProvider.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace Commands.Testing; - -/// -public class TestProvider : ITestProvider -{ - /// - public TestResultType ShouldEvaluateTo { get; } - - /// - public string Arguments { get; } - - /// - /// The command that this test provider is associated with. - /// - public Command Command { get; } - - internal TestProvider(Command command, string arguments, TestResultType shouldEvaluateTo) - { - Command = command; - Arguments = arguments; - ShouldEvaluateTo = shouldEvaluateTo; - } - - #region Initializers - - /// - /// Defines a collection of properties to configure and convert into a new instance of . - /// - /// The command to test. - /// The arguments to test with. - /// The result to test for. - /// A fluent-pattern property object that can be converted into an instance when configured. - public static TestProviderProperties From(Command command, string? arguments = null, TestResultType testResult = TestResultType.Success) - => new TestProviderProperties().AddCommand(command).AddArguments(arguments).AddResult(testResult); - - #endregion -} \ No newline at end of file diff --git a/src/Commands/Testing/TestProviderProperties.cs b/src/Commands/Testing/TestProviderProperties.cs deleted file mode 100644 index aa6ad7ad..00000000 --- a/src/Commands/Testing/TestProviderProperties.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace Commands.Testing; - -/// -/// A set of properties for a test provider. -/// -public sealed class TestProviderProperties -{ - private string? _arguments; - private Command? _command; - private TestResultType _result; - - /// - /// Creates a new instance of . - /// - public TestProviderProperties() - { - _command = null; - _result = TestResultType.Success; - } - - /// - /// Sets the command this provider should test. - /// - /// The command to set. - /// The same for call-chaining. - public TestProviderProperties AddCommand(Command command) - { - Assert.NotNull(command, nameof(command)); - - _command = command; - - return this; - } - - /// - /// Sets the arguments this provider should test with. - /// - /// The arguments to set. - /// The same for call-chaining. - public TestProviderProperties AddArguments(string? arguments) - { - _arguments = arguments; - - return this; - } - - /// - /// Sets the result this provider should return. - /// - /// The result to set. - /// The same for call-chaining. - public TestProviderProperties AddResult(TestResultType result) - { - _result = result; - - return this; - } - - /// - /// Converts the properties to a new instance of . - /// - /// A new instance of . - public TestProvider ToProvider() - { - Assert.NotNull(_command, nameof(_command)); - - return new TestProvider(_command!, _arguments ?? string.Empty, _result); - } -} diff --git a/src/Commands/Testing/TestUtilities.cs b/src/Commands/Testing/TestUtilities.cs index 355bf8f0..a47ecda3 100644 --- a/src/Commands/Testing/TestUtilities.cs +++ b/src/Commands/Testing/TestUtilities.cs @@ -2,7 +2,8 @@ internal static class TestUtilities { - public static async ValueTask Test(this Command command, Func callerCreation, ITestProvider provider, CommandOptions options) + public static async ValueTask TestAgainst(this Command command, Func callerCreation, ITest provider, CommandOptions options) + where TContext : class, ICallerContext { TestResult GetResult(IResult result) { From 7062cb965d130fbedf722c8c3a38f8bbe4aec6b5 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Sun, 4 May 2025 16:23:17 +0200 Subject: [PATCH 2/6] Update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5fccc8a5..26615651 100644 --- a/.gitignore +++ b/.gitignore @@ -362,3 +362,4 @@ MigrationBackup/ # Fody - auto-generated XML schema FodyWeavers.xsd /Visual Studio 2022/Visualizers +/src/Commands.Samples/Commands.Samples.Console/Properties From d255a7f952daf0552e3f2931b162b808c87c4ecc Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Sun, 4 May 2025 16:23:29 +0200 Subject: [PATCH 3/6] Roll out reiterations --- .../Hosting/Execution/HostedCommandOptions.cs | 2 +- src/Commands.Hosting/Hosting/HostUtilities.cs | 14 +- .../Hosting/ServiceUtilities.cs | 16 +-- .../Commands.Samples.Console/Program.cs | 4 +- .../Commands.Samples.Core/Program.cs | 4 +- .../Commands.Tests.Aot/Program.cs | 2 +- .../Commands.Tests.Benchmarks/Program.cs | 12 +- .../Commands.Tests.Console/Program.cs | 16 ++- src/Commands/Commands.csproj | 5 - .../Components/ComponentCollectionBase.cs | 4 +- ...nentCollection.cs => ComponentProvider.cs} | 16 +-- ...ties.cs => ComponentProviderProperties.cs} | 112 +++++++-------- .../Core/Execution/ArgumentDictionary.cs | 70 +++++----- src/Commands/Core/Execution/CommandContext.cs | 4 +- src/Commands/Core/Execution/CommandModule.cs | 4 +- src/Commands/Core/Execution/CommandOptions.cs | 4 +- .../Core/Execution/ConsoleCallerContext.cs | 6 +- .../{Components => }/Abstractions/ITest.cs | 0 .../Testing/Abstractions/ITestProvider.cs | 26 ++++ .../Abstractions/TestResultType.cs | 0 .../Abstractions/ITestCollection.cs | 22 --- .../Components/Abstractions/ITestProvider.cs | 17 --- .../Testing/Components/TestCollection.cs | 98 ------------- src/Commands/Testing/Components/TestGroup.cs | 109 --------------- src/Commands/Testing/Execution/TestContext.cs | 14 -- src/Commands/Testing/{Components => }/Test.cs | 0 .../Attributes => }/TestAttribute.cs | 3 +- .../{Components => }/TestProperties.cs | 0 src/Commands/Testing/TestProvider.cs | 131 ++++++++++++++++++ .../Testing/TestProviderProperties.cs | 119 ++++++++++++++++ .../Testing/{Results => }/TestResult.cs | 27 ++-- src/Commands/Testing/TestUtilities.cs | 12 +- 32 files changed, 446 insertions(+), 427 deletions(-) rename src/Commands/Core/Components/{ComponentCollection.cs => ComponentProvider.cs} (87%) rename src/Commands/Core/Components/{ComponentCollectionProperties.cs => ComponentProviderProperties.cs} (51%) rename src/Commands/Testing/{Components => }/Abstractions/ITest.cs (100%) create mode 100644 src/Commands/Testing/Abstractions/ITestProvider.cs rename src/Commands/Testing/{Components => }/Abstractions/TestResultType.cs (100%) delete mode 100644 src/Commands/Testing/Components/Abstractions/ITestCollection.cs delete mode 100644 src/Commands/Testing/Components/Abstractions/ITestProvider.cs delete mode 100644 src/Commands/Testing/Components/TestCollection.cs delete mode 100644 src/Commands/Testing/Components/TestGroup.cs delete mode 100644 src/Commands/Testing/Execution/TestContext.cs rename src/Commands/Testing/{Components => }/Test.cs (100%) rename src/Commands/Testing/{Components/Attributes => }/TestAttribute.cs (92%) rename src/Commands/Testing/{Components => }/TestProperties.cs (100%) create mode 100644 src/Commands/Testing/TestProvider.cs create mode 100644 src/Commands/Testing/TestProviderProperties.cs rename src/Commands/Testing/{Results => }/TestResult.cs (63%) diff --git a/src/Commands.Hosting/Hosting/Execution/HostedCommandOptions.cs b/src/Commands.Hosting/Hosting/Execution/HostedCommandOptions.cs index 5e09e912..324d9f12 100644 --- a/src/Commands.Hosting/Hosting/Execution/HostedCommandOptions.cs +++ b/src/Commands.Hosting/Hosting/Execution/HostedCommandOptions.cs @@ -30,7 +30,7 @@ public sealed class HostedCommandOptions /// This behavior drastically changes execution flow, and as such, there are a few things to consider when using it: /// /// - /// The end-user must provide an implementation of to the that is used to execute the command, in order to handle the result of the command. + /// The end-user must provide an implementation of to the that is used to execute the command, in order to handle the result of the command. /// /// /// Objects, specifically those scoped to more than a single command must be made thread-safe, meaning they must be able to handle multiple requests at once. diff --git a/src/Commands.Hosting/Hosting/HostUtilities.cs b/src/Commands.Hosting/Hosting/HostUtilities.cs index 4607d35a..26672c12 100644 --- a/src/Commands.Hosting/Hosting/HostUtilities.cs +++ b/src/Commands.Hosting/Hosting/HostUtilities.cs @@ -19,31 +19,31 @@ public static IHostBuilder ConfigureComponents(this IHostBuilder builder) /// /// - /// An action responsible for configuring a newly created instance of in preparation for building an implementation of to execute commands with. + /// An action responsible for configuring a newly created instance of in preparation for building an implementation of to execute commands with. /// The same for call-chaining. - public static IHostBuilder ConfigureComponents(this IHostBuilder builder, Action configureAction) + public static IHostBuilder ConfigureComponents(this IHostBuilder builder, Action configureAction) => ConfigureComponents(builder, (ctx, props) => configureAction(props)); /// /// - /// An action responsible for configuring a newly created instance of in preparation for building an implementation of to execute commands with. + /// An action responsible for configuring a newly created instance of in preparation for building an implementation of to execute commands with. /// The same for call-chaining. - public static IHostBuilder ConfigureComponents(this IHostBuilder builder, Action configureAction) + public static IHostBuilder ConfigureComponents(this IHostBuilder builder, Action configureAction) => ConfigureComponents(builder, configureAction); /// /// The implementation of to consider the factory for executing commands using this host as the lifetime. /// - /// An action responsible for configuring a newly created instance of in preparation for building an implementation of to execute commands with. + /// An action responsible for configuring a newly created instance of in preparation for building an implementation of to execute commands with. /// The same for call-chaining. public static IHostBuilder ConfigureComponents<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TFactory> - (this IHostBuilder builder, Action configureAction) + (this IHostBuilder builder, Action configureAction) where TFactory : CommandExecutionFactory { Assert.NotNull(builder, nameof(builder)); Assert.NotNull(configureAction, nameof(configureAction)); - var properties = new ComponentCollectionProperties(); + var properties = new ComponentProviderProperties(); var services = builder.ConfigureServices((ctx, services) => { configureAction(ctx, properties); diff --git a/src/Commands.Hosting/Hosting/ServiceUtilities.cs b/src/Commands.Hosting/Hosting/ServiceUtilities.cs index c9aeeee7..199d1763 100644 --- a/src/Commands.Hosting/Hosting/ServiceUtilities.cs +++ b/src/Commands.Hosting/Hosting/ServiceUtilities.cs @@ -16,16 +16,16 @@ public static class ServiceUtilities /// Additionally, it provides a factory based execution mechanism for commands, implementing a singleton , scoped and transient . /// /// - /// An action responsible for configuring a newly created instance of in preparation for building an implementation of to execute commands with. + /// An action responsible for configuring a newly created instance of in preparation for building an implementation of to execute commands with. /// The same for call-chaining. public static IServiceCollection AddComponentCollection<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TFactory> - (this IServiceCollection services, Action configureAction) + (this IServiceCollection services, Action configureAction) where TFactory : class, IExecutionFactory { Assert.NotNull(services, nameof(services)); Assert.NotNull(configureAction, nameof(configureAction)); - var properties = new ComponentCollectionProperties(); + var properties = new ComponentProviderProperties(); configureAction(properties); @@ -34,7 +34,7 @@ public static class ServiceUtilities [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static IServiceCollection AddComponentCollection<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TFactory> - (IServiceCollection services, ComponentCollectionProperties properties) + (IServiceCollection services, ComponentProviderProperties properties) where TFactory : class, IExecutionFactory { if (services.Contains()) @@ -52,12 +52,12 @@ public static class ServiceUtilities services.AddScoped(); services.AddTransient(typeof(ICallerContextAccessor<>), typeof(CallerContextAccessor<>)); - var collectionDescriptor = ServiceDescriptor.Singleton(x => + var collectionDescriptor = ServiceDescriptor.Singleton(x => { // Implement global result handler to dispose of the execution scope. This must be done last, even if the properties are mutated anywhere before. properties.AddResultHandler(new ExecutionScopeResolver()); - var collection = properties.ToCollection(); + var collection = properties.ToProvider(); return collection; }); @@ -66,10 +66,10 @@ public static class ServiceUtilities { var provider = x.GetRequiredService(); - if (provider is ComponentCollection collection) + if (provider is ComponentProvider collection) return collection.Configuration; - throw new NotSupportedException($"The component collection is not available in the current context. Ensure that you are configuring an instance of {nameof(ComponentCollection)}."); + throw new NotSupportedException($"The component collection is not available in the current context. Ensure that you are configuring an instance of {nameof(ComponentProvider)}."); }); services.Add(collectionDescriptor); diff --git a/src/Commands.Samples/Commands.Samples.Console/Program.cs b/src/Commands.Samples/Commands.Samples.Console/Program.cs index d5e2807d..a0841a94 100644 --- a/src/Commands.Samples/Commands.Samples.Console/Program.cs +++ b/src/Commands.Samples/Commands.Samples.Console/Program.cs @@ -7,10 +7,10 @@ var results = ResultHandler.For() .AddDelegate((c, e, s) => c.Respond(e)); -var components = new ComponentCollectionProperties() +var components = new ComponentProviderProperties() .WithConfiguration(configuration) .AddResultHandler(results) - .ToCollection(); + .ToProvider(); var tests = new TestCollectionProperties() .AddCommands([.. components.GetCommands()]) diff --git a/src/Commands.Samples/Commands.Samples.Core/Program.cs b/src/Commands.Samples/Commands.Samples.Core/Program.cs index 030f162f..a3308a39 100644 --- a/src/Commands.Samples/Commands.Samples.Core/Program.cs +++ b/src/Commands.Samples/Commands.Samples.Core/Program.cs @@ -13,9 +13,9 @@ Command.From(Divide, "divide", "div") ); -var components = ComponentCollection.From(exit, mathCommands) +var components = ComponentProvider.From(exit, mathCommands) .AddComponentType() - .ToCollection(); + .ToProvider(); await components.Execute(new ConsoleCallerContext(args)); diff --git a/src/Commands.Tests/Commands.Tests.Aot/Program.cs b/src/Commands.Tests/Commands.Tests.Aot/Program.cs index 785a8f4b..d78e1b3c 100644 --- a/src/Commands.Tests/Commands.Tests.Aot/Program.cs +++ b/src/Commands.Tests/Commands.Tests.Aot/Program.cs @@ -1,7 +1,7 @@ using Commands; using Commands.Tests; -var manager = new ComponentCollection(new DelegateResultHandler((c, e, s) => c.Respond(e))) +var manager = new ComponentProvider(new DelegateResultHandler((c, e, s) => c.Respond(e))) { new CommandGroup(typeof(Module), configuration: new ComponentConfiguration()), new CommandGroup("commandgroup") diff --git a/src/Commands.Tests/Commands.Tests.Benchmarks/Program.cs b/src/Commands.Tests/Commands.Tests.Benchmarks/Program.cs index 8e8420b4..2cb581de 100644 --- a/src/Commands.Tests/Commands.Tests.Benchmarks/Program.cs +++ b/src/Commands.Tests/Commands.Tests.Benchmarks/Program.cs @@ -5,7 +5,7 @@ namespace Commands.Tests; public class BenchmarkCallerContext(string? input) : AsyncCallerContext { - public override ArgumentDictionary Arguments { get; } = ArgumentDictionary.From(input); + public override ArgumentDictionary Arguments { get; } = ArgumentDictionary.FromString(input); public override Task Respond(object? response) => Task.CompletedTask; @@ -27,18 +27,18 @@ public static void Command3() { } [MemoryDiagnoser] public class Program { - private static readonly ArgumentDictionary _args = ArgumentDictionary.From("command"); - private static readonly ComponentCollection _components = ComponentCollection.From() + private static readonly ArgumentDictionary _args = ArgumentDictionary.FromString("command"); + private static readonly ComponentProvider _components = ComponentProvider.From() .AddComponentType() .AddComponent(Command.From(() => { }, "command")) - .ToCollection(); + .ToProvider(); static void Main() => BenchmarkRunner.Run(); [Benchmark] public void CreateArguments() - => ArgumentDictionary.From("command"); + => ArgumentDictionary.FromString("command"); [Benchmark] public void FindCommands() @@ -56,7 +56,7 @@ public Task RunCommandNonBlocking() }); [Benchmark] - public ComponentCollection CollectionCreate() + public ComponentProvider CollectionCreate() => []; [Benchmark] diff --git a/src/Commands.Tests/Commands.Tests.Console/Program.cs b/src/Commands.Tests/Commands.Tests.Console/Program.cs index 16698421..b8ba76f1 100644 --- a/src/Commands.Tests/Commands.Tests.Console/Program.cs +++ b/src/Commands.Tests/Commands.Tests.Console/Program.cs @@ -1,7 +1,7 @@ using Commands; using Commands.Testing; -var components = new ComponentCollectionProperties() +var components = new ComponentProviderProperties() .AddComponentTypes(typeof(Program).Assembly.GetExportedTypes()) .AddResultHandler(ResultHandler.From((c, e, s) => c.Respond(e))) .AddComponent( @@ -11,15 +11,17 @@ c.Respond(command); }, "help")) - .ToCollection(); + .ToProvider(); -var tests = TestCollection.From(components.GetCommands()) - .ToCollection(); +var tests = components.GetCommands().Select(x => TestProvider.From(x).ToProvider()); -var results = await tests.Execute((str) => new TestContext(str)); +foreach (var test in tests) +{ + var result = await test.Test(x => new ConsoleCallerContext(x)); -if (results.Count(x => x.Success) == tests.Count) - Console.WriteLine("All tests ran succesfully."); + if (result.Any(x => !x.Success)) + throw new InvalidOperationException($"A command test failed to evaluate to success. Command: {test.Command}. Test: {result.FirstOrDefault(x => !x.Success).Test}"); +} while (true) await components.Execute(new ConsoleCallerContext(Console.ReadLine())); \ No newline at end of file diff --git a/src/Commands/Commands.csproj b/src/Commands/Commands.csproj index e74312eb..e2ef75bf 100644 --- a/src/Commands/Commands.csproj +++ b/src/Commands/Commands.csproj @@ -52,10 +52,5 @@ \ - - - - - diff --git a/src/Commands/Core/Components/ComponentCollectionBase.cs b/src/Commands/Core/Components/ComponentCollectionBase.cs index 23da0e8a..0d321c5a 100644 --- a/src/Commands/Core/Components/ComponentCollectionBase.cs +++ b/src/Commands/Core/Components/ComponentCollectionBase.cs @@ -235,7 +235,7 @@ private List FilterComponents(IEnumerable components) if (_items.Contains(component)) continue; - if (this is ComponentCollection manager) + if (this is ComponentProvider manager) { // When a component is not searchable it means it has no names. Between a manager and a group, a different restriction applies to how this should be done. if (!component.IsSearchable) @@ -261,7 +261,7 @@ private List FilterComponents(IEnumerable components) // Anything added to a group should be considered nested. // Because of the nature of this design, we want to avoid folding anything but top level. This means that nested groups must be named. if (component is not Command) - throw new InvalidOperationException($"{nameof(CommandGroup)} instances without names can only be added to a {nameof(ComponentCollection)}."); + throw new InvalidOperationException($"{nameof(CommandGroup)} instances without names can only be added to a {nameof(ComponentProvider)}."); discovered.Add(component); } diff --git a/src/Commands/Core/Components/ComponentCollection.cs b/src/Commands/Core/Components/ComponentProvider.cs similarity index 87% rename from src/Commands/Core/Components/ComponentCollection.cs rename to src/Commands/Core/Components/ComponentProvider.cs index dc3675ec..ecbd80fe 100644 --- a/src/Commands/Core/Components/ComponentCollection.cs +++ b/src/Commands/Core/Components/ComponentProvider.cs @@ -4,7 +4,7 @@ /// A concurrent implementation of the mechanism that allows commands to be executed using a provided set of arguments. This class cannot be inherited. /// [DebuggerDisplay("Count = {Count}")] -public sealed class ComponentCollection : ComponentCollectionBase, IExecutionProvider +public sealed class ComponentProvider : ComponentCollectionBase, IExecutionProvider { private readonly ResultHandler[] _handlers; @@ -20,14 +20,14 @@ public IReadOnlyCollection Handlers => _handlers; /// - /// Creates a new instance of with the specified handlers. + /// Creates a new instance of with the specified handlers. /// /// /// This overload supports enumerable service injection in order to create a manager from service definitions. /// /// The configuration for this component manager. /// A collection of handlers for post-execution processing of retrieved command input. - public ComponentCollection(ComponentConfiguration configuration, IEnumerable handlers) + public ComponentProvider(ComponentConfiguration configuration, IEnumerable handlers) { Configuration = configuration; @@ -36,10 +36,10 @@ public ComponentCollection(ComponentConfiguration configuration, IEnumerable - /// Creates a new instance of with the specified handlers. + /// Creates a new instance of with the specified handlers. /// /// A collection of handlers for post-execution processing of retrieved command input. - public ComponentCollection(params ResultHandler[] handlers) + public ComponentProvider(params ResultHandler[] handlers) { _handlers = handlers; @@ -141,12 +141,12 @@ private async Task WorkInternal(TContext context, CommandOpti #region Initializers /// - /// Defines a collection of properties to configure and convert into a new instance of . + /// Defines a collection of properties to configure and convert into a new instance of . /// /// The components to add. /// A fluent-pattern property object that can be converted into an instance when configured. - public static ComponentCollectionProperties From(params IComponentProperties[] components) - => new ComponentCollectionProperties().AddComponents(components); + public static ComponentProviderProperties From(params IComponentProperties[] components) + => new ComponentProviderProperties().AddComponents(components); #endregion } diff --git a/src/Commands/Core/Components/ComponentCollectionProperties.cs b/src/Commands/Core/Components/ComponentProviderProperties.cs similarity index 51% rename from src/Commands/Core/Components/ComponentCollectionProperties.cs rename to src/Commands/Core/Components/ComponentProviderProperties.cs index c547855a..7ebbfe20 100644 --- a/src/Commands/Core/Components/ComponentCollectionProperties.cs +++ b/src/Commands/Core/Components/ComponentProviderProperties.cs @@ -1,9 +1,9 @@ namespace Commands; /// -/// A set of properties for a component manager. +/// A set of properties for a component provider. /// -public sealed class ComponentCollectionProperties +public sealed class ComponentProviderProperties { private readonly List _dynamicTypes; @@ -13,9 +13,9 @@ public sealed class ComponentCollectionProperties private ComponentConfigurationProperties? _configuration; /// - /// Creates a new instance of . + /// Creates a new instance of . /// - public ComponentCollectionProperties() + public ComponentProviderProperties() { _dynamicTypes = []; _components = []; @@ -25,14 +25,14 @@ public ComponentCollectionProperties() } /// - /// Adds a type to the component manager. This operation can include non-command module types, but they will be ignored when the manager is created. + /// Adds a type to the component provider. This operation can include non-command module types, but they will be ignored when the provider is created. /// /// - /// Types are evaluated whether they implement , are not abstract, and have no open generic parameters when the manager is created. Any added types that do not match this constraint are ignored. + /// Types are evaluated whether they implement , are not abstract, and have no open generic parameters when the provider is created. Any added types that do not match this constraint are ignored. /// /// The type to add. If the type is already added, it is ignored. - /// The same for call-chaining. - public ComponentCollectionProperties AddComponentType( + /// The same for call-chaining. + public ComponentProviderProperties AddComponentType( #if NET8_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicNestedTypes)] # endif @@ -49,11 +49,11 @@ public ComponentCollectionProperties AddComponentType( } /// - /// Adds a type to the component manager. This operation can include non-command module types, but they will be ignored when the manager is created. + /// Adds a type to the component provider. This operation can include non-command module types, but they will be ignored when the provider is created. /// /// The type definition to add. If the type is already added, it is ignored. - /// The same for call-chaining. - public ComponentCollectionProperties AddComponentType< + /// The same for call-chaining. + public ComponentProviderProperties AddComponentType< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicNestedTypes)] #endif @@ -64,18 +64,18 @@ public ComponentCollectionProperties AddComponentType< } /// - /// Adds multiple types to the component manager. This operation can include non-command module types, but they will be ignored when the manager is created. + /// Adds multiple types to the component provider. This operation can include non-command module types, but they will be ignored when the provider is created. /// /// - /// When is called on the properties, all types added to the properties are checked if they implement or . + /// When is called on the properties, all types added to the properties are checked if they implement or . /// If any provided type does not implement said base type, it is ignored. /// /// The types to add. If any type is already added, it is ignored. - /// The same for call-chaining. + /// The same for call-chaining. #if NET8_0_OR_GREATER [UnconditionalSuppressMessage("AotAnalysis", "IL2072", Justification = "The types are supplied from user-facing implementation, it is up to the user to ensure that these types are available in AOT context.")] #endif - public ComponentCollectionProperties AddComponentTypes(params Type[] types) + public ComponentProviderProperties AddComponentTypes(params Type[] types) { foreach (var componentType in types) AddComponentType(componentType); @@ -84,14 +84,14 @@ public ComponentCollectionProperties AddComponentTypes(params Type[] types) } /// - /// Adds a component to the component manager. + /// Adds a component to the component provider. /// /// - /// Commands added to the manager must have at least one name. Groups that are added are not required to have a name. + /// Commands added to the provider must have at least one name. Groups that are added are not required to have a name. /// /// The component to add. - /// The same for call-chaining. - public ComponentCollectionProperties AddComponent(IComponentProperties component) + /// The same for call-chaining. + public ComponentProviderProperties AddComponent(IComponentProperties component) { Assert.NotNull(component, nameof(component)); @@ -101,14 +101,14 @@ public ComponentCollectionProperties AddComponent(IComponentProperties component } /// - /// Adds multiple components to the component manager. + /// Adds multiple components to the component provider. /// /// - /// Commands added to the manager must have at least one name. Groups that are added are not required to have a name. + /// Commands added to the provider must have at least one name. Groups that are added are not required to have a name. /// /// The components to add. - /// The same for call-chaining. - public ComponentCollectionProperties AddComponents(params IComponentProperties[] components) + /// The same for call-chaining. + public ComponentProviderProperties AddComponents(params IComponentProperties[] components) { foreach (var component in components) AddComponent(component); @@ -117,11 +117,11 @@ public ComponentCollectionProperties AddComponents(params IComponentProperties[] } /// - /// Adds a result handler to the component manager. + /// Adds a result handler to the component provider. /// /// The handler to add. - /// The same for call-chaining. - public ComponentCollectionProperties AddResultHandler(IResultHandlerProperties handler) + /// The same for call-chaining. + public ComponentProviderProperties AddResultHandler(IResultHandlerProperties handler) { Assert.NotNull(handler, nameof(handler)); @@ -131,39 +131,39 @@ public ComponentCollectionProperties AddResultHandler(IResultHandlerProperties h } /// - /// Adds a result handler to the component manager. + /// Adds a result handler to the component provider. /// /// The handler to add. - /// The same for call-chaining. - public ComponentCollectionProperties AddResultHandler(ResultHandler handler) + /// The same for call-chaining. + public ComponentProviderProperties AddResultHandler(ResultHandler handler) => AddResultHandler(new ResultHandlerProperties(handler)); /// - /// Adds a result handler to the component manager. + /// Adds a result handler to the component provider. /// /// The context type for the handler to handle. /// The delegate that is executed when the result of command execution is yielded. - /// The same for call-chaining. - public ComponentCollectionProperties AddResultHandler(Action executionDelegate) + /// The same for call-chaining. + public ComponentProviderProperties AddResultHandler(Action executionDelegate) where T : class, ICallerContext => AddResultHandler(new ResultHandlerProperties().AddDelegate(executionDelegate)); /// - /// Adds a result handler to the component manager. + /// Adds a result handler to the component provider. /// /// The context type for the handler to handle. /// The delegate that is executed when the result of command execution is yielded. - /// The same for call-chaining. - public ComponentCollectionProperties AddResultHandler(Func executionDelegate) + /// The same for call-chaining. + public ComponentProviderProperties AddResultHandler(Func executionDelegate) where T : class, ICallerContext => AddResultHandler(new ResultHandlerProperties().AddDelegate(executionDelegate)); /// - /// Adds multiple result handlers to the component manager. + /// Adds multiple result handlers to the component provider. /// /// The handlers to add. - /// The same for call-chaining. - public ComponentCollectionProperties AddResultHandlers(params IResultHandlerProperties[] handlers) + /// The same for call-chaining. + public ComponentProviderProperties AddResultHandlers(params IResultHandlerProperties[] handlers) { foreach (var handler in handlers) AddResultHandler(handler); @@ -172,11 +172,11 @@ public ComponentCollectionProperties AddResultHandlers(params IResultHandlerProp } /// - /// Adds multiple result handlers to the component manager. + /// Adds multiple result handlers to the component provider. /// /// The handlers to add. - /// The same for call-chaining. - public ComponentCollectionProperties AddResultHandlers(params ResultHandler[] handlers) + /// The same for call-chaining. + public ComponentProviderProperties AddResultHandlers(params ResultHandler[] handlers) { foreach (var handler in handlers) AddResultHandler(new ResultHandlerProperties(handler)); @@ -185,11 +185,11 @@ public ComponentCollectionProperties AddResultHandlers(params ResultHandler[] ha } /// - /// Sets the configuration for the component manager. + /// Sets the configuration for the component provider. /// - /// The configuration which should configure defined components that are to be built for this manager. - /// The same for call-chaining. - public ComponentCollectionProperties WithConfiguration(ComponentConfigurationProperties configuration) + /// The configuration which should configure defined components that are to be built for this provider. + /// The same for call-chaining. + public ComponentProviderProperties WithConfiguration(ComponentConfigurationProperties configuration) { Assert.NotNull(configuration, nameof(configuration)); @@ -199,11 +199,11 @@ public ComponentCollectionProperties WithConfiguration(ComponentConfigurationPro } /// - /// Sets the configuration for the component manager. + /// Sets the configuration for the component provider. /// - /// An action that configures a newly created instance of to be built for this manager. - /// The same for call-chaining. - public ComponentCollectionProperties WithConfiguration(Action configure) + /// An action that configures a newly created instance of to be built for this provider. + /// The same for call-chaining. + public ComponentProviderProperties WithConfiguration(Action configure) { Assert.NotNull(configure, nameof(configure)); @@ -216,20 +216,20 @@ public ComponentCollectionProperties WithConfiguration(Action - /// Converts this set of properties to a new instance of . + /// Converts this set of properties to a new instance of . /// - /// A new instance of . - public ComponentCollection ToCollection() + /// A new instance of . + public ComponentProvider ToProvider() { _configuration ??= ComponentConfigurationProperties.Default; var configuration = _configuration.ToConfiguration(); - var manager = new ComponentCollection(configuration, [.. _handlers.Select(handler => handler.ToHandler())]); + var provider = new ComponentProvider(configuration, [.. _handlers.Select(handler => handler.ToHandler())]); - manager.AddRange(_components.Select(component => component.ToComponent(configuration: configuration))); - manager.AddRange(ComponentUtilities.GetComponents(configuration, _dynamicTypes, null, false)); + provider.AddRange(_components.Select(component => component.ToComponent(configuration: configuration))); + provider.AddRange(ComponentUtilities.GetComponents(configuration, _dynamicTypes, null, false)); - return manager; + return provider; } } diff --git a/src/Commands/Core/Execution/ArgumentDictionary.cs b/src/Commands/Core/Execution/ArgumentDictionary.cs index c23e531d..0fdb321a 100644 --- a/src/Commands/Core/Execution/ArgumentDictionary.cs +++ b/src/Commands/Core/Execution/ArgumentDictionary.cs @@ -17,32 +17,32 @@ public struct ArgumentDictionary private int _index = 0; - private readonly List _unnamedArgs; + private readonly string[] _unnamedArgs; private readonly Dictionary _namedArgs; internal int AvailableLength { get; private set; } /// - /// Gets the number of arguments present in the set. + /// Gets the number of keys present in the dictionary. /// public readonly int Count - => _unnamedArgs.Count + _namedArgs.Count; + => _unnamedArgs.Length + _namedArgs.Count; /// - /// Gets a key-value pair from the set of arguments, known by the provided . + /// Gets the value from the set of arguments, known by the provided . /// /// The key under which this argument is known to the current array. - /// A + /// An object representing the value belonging to the specified key. If no value exists but the key is represented in the dictionary, is returned instead. /// Thrown when the provided is not found in the set. - public readonly KeyValuePair this[string key] + public readonly object? this[string key] { get { if (_namedArgs.TryGetValue(key, out var value)) - return new(key, value); + return value; if (_unnamedArgs.Contains(key, StringComparer.OrdinalIgnoreCase)) - return new(key, null); + return null; throw new KeyNotFoundException(); } @@ -53,22 +53,26 @@ public readonly int Count /// /// The range of named arguments to enumerate in this set. /// The comparer to evaluate keys in the inner named dictionary. - public ArgumentDictionary(StringComparer? comparer, IEnumerable> args) + public ArgumentDictionary(IEnumerable> args, StringComparer? comparer) { _namedArgs = new(comparer); - var unnamedFill = new List(); + var unnamedFill = Array.Empty(); foreach (var kvp in args) { if (kvp.Value == null) - unnamedFill.Add(kvp.Key); + { + Array.Resize(ref unnamedFill, unnamedFill.Length + 1); + + unnamedFill[unnamedFill.Length] = kvp.Key; + } else _namedArgs[kvp.Key] = kvp.Value; } _unnamedArgs = unnamedFill; - AvailableLength = _unnamedArgs.Count + _namedArgs.Count; + AvailableLength = _unnamedArgs.Length + _namedArgs.Count; } /// @@ -76,8 +80,8 @@ public ArgumentDictionary(StringComparer? comparer, IEnumerable public ArgumentDictionary() { - _namedArgs = null!; - _unnamedArgs = null!; + _namedArgs = []; + _unnamedArgs = []; AvailableLength = 0; } @@ -92,7 +96,7 @@ internal bool TryGetValue(string parameterName, out object? value) if (_namedArgs.TryGetValue(parameterName, out value!)) return true; - if (_index >= _unnamedArgs.Count) + if (_index >= _unnamedArgs.Length) return false; value = _unnamedArgs[_index++]; @@ -106,7 +110,7 @@ internal readonly bool TryGetElementAt(int index, [NotNullWhen(true)] out string internal readonly bool TryGetElementAt(int index, out string? value) #endif { - if (index < _unnamedArgs.Count) + if (index < _unnamedArgs.Length) { value = _unnamedArgs[index]; return true; @@ -141,21 +145,12 @@ internal void SetParseIndex(int index) #region Initializers - /// - public static ArgumentDictionary From(string? input, StringComparer? comparer = null) - => From(input, [' '], comparer); - - /// - public static ArgumentDictionary From(string[] input, StringComparer? comparer = null) - { - if (input.Length == 0) - return new(); - - return new(comparer, ReadInternal(input)); - } + /// + public static ArgumentDictionary FromString(string? input, StringComparer? comparer = null) + => FromString(input, [' '], comparer); /// - /// Reads the provided into an array of command arguments. This method will never throw, always returning a new . + /// Reads the provided into an array of command arguments. This operation will never throw, always returning a new . /// /// /// The implementation is defined by the following rules: @@ -175,13 +170,13 @@ public static ArgumentDictionary From(string[] input, StringComparer? comparer = /// /// /// - /// The caller input to parse into a set of arguments. + /// The caller's input to parse into a set of arguments. /// The characters to use as separators when splitting the input. /// The comparer to use when comparing argument names. /// - /// An array of arguments that can be used to search for a command or parse into a method. + /// An array of arguments that can be used to search for a command or parse into a delegate. /// - public static ArgumentDictionary From(string? input, char[] separators, StringComparer? comparer = null) + public static ArgumentDictionary FromString(string? input, char[] separators, StringComparer? comparer = null) { if (string.IsNullOrWhiteSpace(input)) return new(); @@ -191,7 +186,16 @@ public static ArgumentDictionary From(string? input, char[] separators, StringCo if (split.Length == 0) return new(); - return new(comparer, ReadInternal(split)); + return new(ReadInternal(split), comparer); + } + + /// + public static ArgumentDictionary FromArguments(string[] input, StringComparer? comparer = null) + { + if (input.Length == 0) + return new(); + + return new(ReadInternal(input), comparer); } private static IEnumerable> ReadInternal(string[] input) diff --git a/src/Commands/Core/Execution/CommandContext.cs b/src/Commands/Core/Execution/CommandContext.cs index 3d64b781..2ad21c4c 100644 --- a/src/Commands/Core/Execution/CommandContext.cs +++ b/src/Commands/Core/Execution/CommandContext.cs @@ -26,9 +26,9 @@ public class CommandContext(T caller, Command command, CommandOptions options public Command Command { get; } = command; /// - /// Gets the that triggered the command, if this command was invoked from one. + /// Gets the that triggered the command, if this command was invoked from one. /// - public ComponentCollection? Manager => Options.Manager; + public ComponentProvider? Manager => Options.Manager; /// /// Sends a response to the caller of the command. diff --git a/src/Commands/Core/Execution/CommandModule.cs b/src/Commands/Core/Execution/CommandModule.cs index e7d54531..b3285dcf 100644 --- a/src/Commands/Core/Execution/CommandModule.cs +++ b/src/Commands/Core/Execution/CommandModule.cs @@ -40,9 +40,9 @@ public abstract class CommandModule public Command Command { get; internal set; } = null!; /// - /// Gets the that invoked this command. This property is if the command was not invoked by a . + /// Gets the that invoked this command. This property is if the command was not invoked by a . /// - public ComponentCollection? Manager { get; internal set; } + public ComponentProvider? Manager { get; internal set; } /// /// Sends a response to the caller. diff --git a/src/Commands/Core/Execution/CommandOptions.cs b/src/Commands/Core/Execution/CommandOptions.cs index 8fef0834..4a5b8188 100644 --- a/src/Commands/Core/Execution/CommandOptions.cs +++ b/src/Commands/Core/Execution/CommandOptions.cs @@ -9,7 +9,7 @@ namespace Commands; public sealed class CommandOptions { // A reference to the component manager that called the command, if any. - internal ComponentCollection? Manager; + internal ComponentProvider? Manager; /// /// Gets or sets the services for running the request. @@ -50,7 +50,7 @@ public sealed class CommandOptions /// This behavior drastically changes execution flow, and as such, there are a few things to consider when using it: /// /// - /// The end-user must provide an implementation of to the that is used to execute the command, in order to handle the result of the command. + /// The end-user must provide an implementation of to the that is used to execute the command, in order to handle the result of the command. /// /// /// Objects, specifically those scoped to more than a single command must be made thread-safe, meaning they must be able to handle multiple requests at once. diff --git a/src/Commands/Core/Execution/ConsoleCallerContext.cs b/src/Commands/Core/Execution/ConsoleCallerContext.cs index d608629f..3824b2ce 100644 --- a/src/Commands/Core/Execution/ConsoleCallerContext.cs +++ b/src/Commands/Core/Execution/ConsoleCallerContext.cs @@ -16,7 +16,7 @@ public class ConsoleCallerContext : ICallerContext /// /// A raw string which will be parsed into a set of arguments. public ConsoleCallerContext(string? input) - => Arguments = ArgumentDictionary.From(input); + => Arguments = ArgumentDictionary.FromString(input); /// /// Creates a new instance of with the specified input. @@ -26,7 +26,7 @@ public ConsoleCallerContext(string? input) /// /// The CLI arguments passed to the application upon entry. public ConsoleCallerContext(string[] input) - => Arguments = ArgumentDictionary.From(input); + => Arguments = ArgumentDictionary.FromArguments(input); /// /// Sends a response to the console. @@ -34,7 +34,7 @@ public ConsoleCallerContext(string[] input) /// The message to send. public virtual void Respond(object? message) { - if (message is IEnumerable enumerable) + if (message is IEnumerable enumerable and not string) { foreach (var item in enumerable) Console.WriteLine(item); diff --git a/src/Commands/Testing/Components/Abstractions/ITest.cs b/src/Commands/Testing/Abstractions/ITest.cs similarity index 100% rename from src/Commands/Testing/Components/Abstractions/ITest.cs rename to src/Commands/Testing/Abstractions/ITest.cs diff --git a/src/Commands/Testing/Abstractions/ITestProvider.cs b/src/Commands/Testing/Abstractions/ITestProvider.cs new file mode 100644 index 00000000..636999a2 --- /dev/null +++ b/src/Commands/Testing/Abstractions/ITestProvider.cs @@ -0,0 +1,26 @@ +namespace Commands.Testing; + +/// +/// Implements a mechanism for testing a . +/// +public interface ITestProvider : ICollection, IEnumerable +{ + /// + /// Gets the command which the tests contained in this should be tested against. + /// + public Command Command { get; } + + /// + /// Sequentially tests all available instances aaginst the contained within this type using the provided and options. + /// + /// + /// When specifying the of this operation, the value of is ignored. + /// This is because the test execution expects to yield results directly back to the caller, and cannot do this in detached context. + /// + /// The type of that this test sequence should use to test with. + /// A delegate that yields an implementation of based on the input value for every new test. + /// A collection of options that determine how every test against this command is ran. + /// A containing an with the result of every test yielded by this operation. + public ValueTask> Test(Func callerCreation, CommandOptions? options = null) + where TContext : class, ICallerContext; +} diff --git a/src/Commands/Testing/Components/Abstractions/TestResultType.cs b/src/Commands/Testing/Abstractions/TestResultType.cs similarity index 100% rename from src/Commands/Testing/Components/Abstractions/TestResultType.cs rename to src/Commands/Testing/Abstractions/TestResultType.cs diff --git a/src/Commands/Testing/Components/Abstractions/ITestCollection.cs b/src/Commands/Testing/Components/Abstractions/ITestCollection.cs deleted file mode 100644 index 1952f791..00000000 --- a/src/Commands/Testing/Components/Abstractions/ITestCollection.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Commands.Testing; - -/// -/// -/// -/// -public interface ITestCollection : ICollection, IEnumerable -{ - /// - /// - /// - /// - /// - //public int AddRange(IEnumerable items); - - /// - /// - /// - /// - /// - //public int RemoveRange(IEnumerable items); -} diff --git a/src/Commands/Testing/Components/Abstractions/ITestProvider.cs b/src/Commands/Testing/Components/Abstractions/ITestProvider.cs deleted file mode 100644 index 314d87fb..00000000 --- a/src/Commands/Testing/Components/Abstractions/ITestProvider.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Commands.Testing; - -/// -/// -/// -public interface ITestProvider : ITestCollection -{ - /// - /// - /// - /// - /// - /// - /// - public ValueTask> Execute(Func callerCreation, CommandOptions? options = null) - where TContext : class, ICallerContext; -} diff --git a/src/Commands/Testing/Components/TestCollection.cs b/src/Commands/Testing/Components/TestCollection.cs deleted file mode 100644 index 343e350c..00000000 --- a/src/Commands/Testing/Components/TestCollection.cs +++ /dev/null @@ -1,98 +0,0 @@ -namespace Commands.Testing; - -/// -/// -/// -public sealed class TestCollection : ITestProvider -{ - private TestGroup[] _tests; - - /// - /// Gets the number of groups contained in this collection. - /// - public int Count - => _tests.Length; - - /// - /// - /// - /// - public TestCollection(params TestGroup[] tests) - { - _tests = tests; - } - - /// - /// - /// - /// - /// - /// - /// - public async ValueTask> Execute(Func callerCreation, CommandOptions? options = null) - where TContext : class, ICallerContext - { - options ??= new CommandOptions(); - - var results = new IEnumerable[_tests.Length]; - - for (var i = 0; i < _tests.Length; i++) - results[i] = await _tests[i].Run(callerCreation, options).ConfigureAwait(false); - - return results.SelectMany(x => x); - } - - /// - public void Add(TestGroup item) - { - Assert.NotNull(item, nameof(item)); - - Array.Resize(ref _tests, _tests.Length); - - _tests[_tests.Length] = item; - } - - /// - public void Clear() - => _tests = []; - - /// - public bool Contains(TestGroup item) - { - Assert.NotNull(item, nameof(item)); - - return _tests.Contains(item); - } - - /// - public void CopyTo(TestGroup[] array, int arrayIndex) - => _tests.CopyTo(array, arrayIndex); - - /// - public bool Remove(TestGroup item) - { - Assert.NotNull(item, nameof(item)); - - var indexOf = Array.IndexOf(_tests, item); - - if (indexOf == -1) - return false; - - for (var i = indexOf; i < _tests.Length - 1; i++) - _tests[i] = _tests[i + 1]; - - Array.Resize(ref _tests, _tests.Length - 1); - - return true; - } - - /// - public IEnumerator GetEnumerator() - => ((IEnumerable)_tests).GetEnumerator(); - - bool ICollection.IsReadOnly - => false; - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); -} diff --git a/src/Commands/Testing/Components/TestGroup.cs b/src/Commands/Testing/Components/TestGroup.cs deleted file mode 100644 index bc8974d9..00000000 --- a/src/Commands/Testing/Components/TestGroup.cs +++ /dev/null @@ -1,109 +0,0 @@ -namespace Commands.Testing; - -/// -/// Represents a group of implementations to be tested against the that this group targets. -/// -public sealed class TestGroup : ITestCollection -{ - private ITest[] _tests; - - /// - /// Gets the command which the tests contained in this should be tested against. - /// - public Command Command { get; } - - /// - /// Gets the number of implementations contained in this group. - /// - public int Count - => _tests.Length; - - /// - /// Creates a new targetting the provided command, including the provided tests. - /// - /// - /// - public TestGroup(Command command, params ITest[] tests) - { - Assert.NotNull(command, nameof(command)); - Assert.NotNull(tests, nameof(tests)); - - Command = command; - - _tests = tests; - } - - /// - /// - /// - /// - /// - /// - public async ValueTask> Run(Func callerCreation, CommandOptions options) - where TContext : class, ICallerContext - { - Assert.NotNull(callerCreation, nameof(callerCreation)); - Assert.NotNull(options, nameof(options)); - - var results = new TestResult[_tests.Length]; - - for (var i = 0; i < _tests.Length; i++) - results[i] = await Command.TestAgainst(callerCreation, _tests[i], options).ConfigureAwait(false); - - return results; - } - - /// - public void Add(ITest item) - { - Assert.NotNull(item, nameof(item)); - - Array.Resize(ref _tests, _tests.Length); - - _tests[_tests.Length] = item; - } - - /// - public void Clear() - => _tests = []; - - /// - public bool Contains(ITest item) - { - Assert.NotNull(item, nameof(item)); - - return _tests.Contains(item); - } - - /// - public void CopyTo(ITest[] array, int arrayIndex) - => _tests.CopyTo(array, arrayIndex); - - /// - public bool Remove(ITest item) - { - Assert.NotNull(item, nameof(item)); - - var indexOf = Array.IndexOf(_tests, item); - - if (indexOf == -1) - return false; - - for (var i = indexOf; i < _tests.Length - 1; i++) - _tests[i] = _tests[i + 1]; - - Array.Resize(ref _tests, _tests.Length - 1); - - return true; - } - - /// - public IEnumerator GetEnumerator() - => ((IEnumerable)_tests).GetEnumerator(); - - bool ICollection.IsReadOnly - => false; - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); -} diff --git a/src/Commands/Testing/Execution/TestContext.cs b/src/Commands/Testing/Execution/TestContext.cs deleted file mode 100644 index f983a03b..00000000 --- a/src/Commands/Testing/Execution/TestContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Commands.Testing; - -/// -/// A caller context for testing purposes. When responding, the message is ignored. -/// -/// The input to use for the context. -public class TestContext(string? input) : ICallerContext -{ - /// - public ArgumentDictionary Arguments { get; } = ArgumentDictionary.From(input); - - /// - public void Respond(object? message) { } // Deliberately emptied. -} diff --git a/src/Commands/Testing/Components/Test.cs b/src/Commands/Testing/Test.cs similarity index 100% rename from src/Commands/Testing/Components/Test.cs rename to src/Commands/Testing/Test.cs diff --git a/src/Commands/Testing/Components/Attributes/TestAttribute.cs b/src/Commands/Testing/TestAttribute.cs similarity index 92% rename from src/Commands/Testing/Components/Attributes/TestAttribute.cs rename to src/Commands/Testing/TestAttribute.cs index d2d04785..e14a1b04 100644 --- a/src/Commands/Testing/Components/Attributes/TestAttribute.cs +++ b/src/Commands/Testing/TestAttribute.cs @@ -1,4 +1,5 @@ -namespace Commands.Testing; + +namespace Commands.Testing; /// /// An attribute that is used to define a test for a command. diff --git a/src/Commands/Testing/Components/TestProperties.cs b/src/Commands/Testing/TestProperties.cs similarity index 100% rename from src/Commands/Testing/Components/TestProperties.cs rename to src/Commands/Testing/TestProperties.cs diff --git a/src/Commands/Testing/TestProvider.cs b/src/Commands/Testing/TestProvider.cs new file mode 100644 index 00000000..25140e43 --- /dev/null +++ b/src/Commands/Testing/TestProvider.cs @@ -0,0 +1,131 @@ +namespace Commands.Testing; + +/// +/// Represents a provider containing implementations to be tested against the that this group targets. +/// +[DebuggerDisplay("{ToString()}")] +public sealed class TestProvider : ITestProvider +{ + private ITest[] _tests; + + /// + public Command Command { get; } + + /// + /// Gets the number of implementations contained in this group. + /// + public int Count + => _tests.Length; + + /// + /// Creates a new targetting the provided command, including the provided tests. + /// + /// + /// This constructor adds all implementations of marked on defined execution delegates, alongside the provided . + /// + /// The command that should be tested against. + /// A variable collection of tests this command should be tested with. + public TestProvider(Command command, params ITest[] tests) + { + Assert.NotNull(command, nameof(command)); + Assert.NotNull(tests, nameof(tests)); + + Command = command; + + _tests = [.. tests, .. command.Attributes.OfType()]; + } + + /// + public async ValueTask> Test(Func callerCreation, CommandOptions? options = null) + where TContext : class, ICallerContext + { + Assert.NotNull(callerCreation, nameof(callerCreation)); + + // Permit a fast path for empty providers. + if (_tests.Length == 0) + return []; + + options ??= new CommandOptions(); + + var results = new TestResult[_tests.Length]; + + for (var i = 0; i < _tests.Length; i++) + results[i] = await Command.TestUsing(callerCreation, _tests[i], options).ConfigureAwait(false); + + return results; + } + + /// + public void Add(ITest item) + { + Assert.NotNull(item, nameof(item)); + + Array.Resize(ref _tests, _tests.Length + 1); + + _tests[_tests.Length] = item; + } + + /// + public void Clear() + => _tests = []; + + /// + public bool Contains(ITest item) + { + Assert.NotNull(item, nameof(item)); + + return _tests.Contains(item); + } + + /// + public void CopyTo(ITest[] array, int arrayIndex) + => _tests.CopyTo(array, arrayIndex); + + /// + public bool Remove(ITest item) + { + Assert.NotNull(item, nameof(item)); + + var indexOf = Array.IndexOf(_tests, item); + + if (indexOf == -1) + return false; + + for (var i = indexOf; i < _tests.Length - 1; i++) + _tests[i] = _tests[i + 1]; + + Array.Resize(ref _tests, _tests.Length - 1); + + return true; + } + + /// + public IEnumerator GetEnumerator() + => ((IEnumerable)_tests).GetEnumerator(); + + /// + public override string ToString() + => $"Count = {Count}\nCommand = {Command}"; + + #region Internals + + bool ICollection.IsReadOnly + => false; + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + #endregion + + #region Initializers + + /// + /// Defines a collection of properties from the provided command, to configure and construct a new instance of from. + /// + /// The command to be represented by the constructed type, being a testable interface to evaluate execution for. + /// A new instance of to configure and construct into a new instance of . + public static TestProviderProperties From(Command command) + => new TestProviderProperties().WithCommand(command); + + #endregion +} diff --git a/src/Commands/Testing/TestProviderProperties.cs b/src/Commands/Testing/TestProviderProperties.cs new file mode 100644 index 00000000..90c71926 --- /dev/null +++ b/src/Commands/Testing/TestProviderProperties.cs @@ -0,0 +1,119 @@ +namespace Commands.Testing; + +/// +/// Represents a collection of properties with which a new instance of can be constructed. +/// +public sealed class TestProviderProperties +{ + private Command _command; + private readonly List _tests; + + /// + /// Creates a new instance of to create a from. + /// + public TestProviderProperties() + { + _tests = []; + _command = null!; + } + + /// + /// Sets the . Calling this method multiple times replaces the old value with the new one. + /// + /// The to provide as the target of the these properties create. + /// The same for call-chaining. + public TestProviderProperties WithCommand(Command command) + { + Assert.NotNull(command, nameof(command)); + + _command = command; + + return this; + } + + /// + /// Adds an implementation of to the properties. + /// + /// The test to add. + /// The same for call-chaining. + public TestProviderProperties AddTest(ITest test) + { + Assert.NotNull(test, nameof(test)); + + _tests.Add(test); + + return this; + } + + /// + /// Adds the constructed to the properties. + /// + /// The properties to add. + /// The same for call-chaining. + public TestProviderProperties AddTest(TestProperties properties) + { + Assert.NotNull(properties, nameof(properties)); + + _tests.Add(properties.ToTest()); + + return this; + } + + /// + /// Creates a new instance of and configures it using the provided action, adding it to the properties. + /// + /// The configuration for the properties to add. + /// The same for call-chaining. + public TestProviderProperties AddTest(Action configureProperties) + { + Assert.NotNull(configureProperties, nameof(configureProperties)); + + var properties = new TestProperties(); + + configureProperties(properties); + + _tests.Add(properties.ToTest()); + + return this; + } + + /// + /// Adds a collection of implementations to the properties. + /// + /// The tests to add. + /// The same for call-chaining. + public TestProviderProperties AddTests(IEnumerable tests) + { + Assert.NotNull(tests, nameof(tests)); + + foreach (var test in tests) + AddTest(test); + + return this; + } + + /// + /// Adds a collection of implementations to the properties. + /// + /// The tests to add. + /// The same for call-chaining. + public TestProviderProperties AddTests(params ITest[] tests) + { + Assert.NotNull(tests, nameof(tests)); + + foreach (var test in tests) + AddTest(test); + + return this; + } + + /// + /// Creates a new instance of from the configured properties. + /// + /// + /// This operation adds all defined on defined execution delegates of any command to the tests of the resulting instance. + /// + /// A new instance of . + public TestProvider ToProvider() + => new(_command, [.. _tests]); +} diff --git a/src/Commands/Testing/Results/TestResult.cs b/src/Commands/Testing/TestResult.cs similarity index 63% rename from src/Commands/Testing/Results/TestResult.cs rename to src/Commands/Testing/TestResult.cs index aa731af8..cf159b4c 100644 --- a/src/Commands/Testing/Results/TestResult.cs +++ b/src/Commands/Testing/TestResult.cs @@ -3,20 +3,21 @@ /// /// A result type which contains the result of a test execution. /// +[DebuggerDisplay("{ToString()}")] public readonly struct TestResult : IResult { /// - /// The command that was tested. + /// Gets the executed test. /// - public Command Command { get; } + public ITest Test { get; } /// - /// The actual result of the test. If the test succeeded, this will be the same as . + /// Gets the actual result of the test. If the test succeeded, this will be the same as . /// public TestResultType ActualResult { get; } /// - /// The expected result of the test. + /// Gets the expected result of the test. /// public TestResultType ExpectedResult { get; } @@ -26,9 +27,9 @@ /// public bool Success { get; } - private TestResult(Command command, TestResultType expectedResult, TestResultType actualResult, Exception? exception, bool success) + private TestResult(ITest test, TestResultType expectedResult, TestResultType actualResult, Exception? exception, bool success) { - Command = command; + Test = test; ExpectedResult = expectedResult; ActualResult = actualResult; Exception = exception; @@ -37,7 +38,7 @@ private TestResult(Command command, TestResultType expectedResult, TestResultTyp /// public override string ToString() - => $"Command = {Command} \nSuccess = {(Exception == null ? "True" : $"False \nException = {Exception.Message}")}"; + => $"Test = {Test} \nSuccess = {(Exception == null ? "True" : $"False \nException = {Exception.Message}")}"; /// /// Gets a string representation of this result. @@ -50,20 +51,20 @@ public string ToString(bool inline) /// /// Creates a new representing a successful test execution. /// - /// The command that was tested. + /// The test that was tested. /// The result type of the test execution. /// - public static TestResult FromSuccess(Command command, TestResultType resultType) - => new(command, resultType, resultType, null, true); + public static TestResult FromSuccess(ITest test, TestResultType resultType) + => new(test, resultType, resultType, null, true); /// /// Creates a new representing a failed test execution. /// - /// The command that was tested. + /// The test that was tested. /// The expected result of the test. /// The actual result of the test execution. /// An exception that might have occurred during test execution. /// - public static TestResult FromError(Command command, TestResultType expectedResult, TestResultType actualResult, Exception exception) - => new(command, expectedResult, actualResult, exception, false); + public static TestResult FromError(ITest test, TestResultType expectedResult, TestResultType actualResult, Exception exception) + => new(test, expectedResult, actualResult, exception, false); } diff --git a/src/Commands/Testing/TestUtilities.cs b/src/Commands/Testing/TestUtilities.cs index a47ecda3..d7d937e6 100644 --- a/src/Commands/Testing/TestUtilities.cs +++ b/src/Commands/Testing/TestUtilities.cs @@ -2,16 +2,16 @@ internal static class TestUtilities { - public static async ValueTask TestAgainst(this Command command, Func callerCreation, ITest provider, CommandOptions options) + public static async ValueTask TestUsing(this Command command, Func callerCreation, ITest test, CommandOptions options) where TContext : class, ICallerContext { TestResult GetResult(IResult result) { TestResult CompareReturn(TestResultType targetType, Exception exception) { - return provider.ShouldEvaluateTo == targetType - ? TestResult.FromSuccess(command, provider.ShouldEvaluateTo) - : TestResult.FromError(command, provider.ShouldEvaluateTo, targetType, exception); + return test.ShouldEvaluateTo == targetType + ? TestResult.FromSuccess(test, test.ShouldEvaluateTo) + : TestResult.FromError(test, test.ShouldEvaluateTo, targetType, exception); } return result.Exception switch @@ -26,9 +26,9 @@ TestResult CompareReturn(TestResultType targetType, Exception exception) }; } - var fullName = string.IsNullOrWhiteSpace(provider.Arguments) + var fullName = string.IsNullOrWhiteSpace(test.Arguments) ? command.GetFullName(false) - : command.GetFullName(false) + ' ' + provider.Arguments; + : command.GetFullName(false) + ' ' + test.Arguments; var runResult = await command.Run(callerCreation(fullName), options).ConfigureAwait(false); From 8647e132984dc8b3f42ee806791d5979033f9406 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Sun, 4 May 2025 16:35:00 +0200 Subject: [PATCH 4/6] A few more minor adjustments to argumentdict --- src/Commands/Core/Components/Command.cs | 6 +++--- src/Commands/Core/Execution/ArgumentDictionary.cs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Commands/Core/Components/Command.cs b/src/Commands/Core/Components/Command.cs index f8c822e1..d454619b 100644 --- a/src/Commands/Core/Components/Command.cs +++ b/src/Commands/Core/Components/Command.cs @@ -167,9 +167,9 @@ public async ValueTask Run(TContext caller, CommandOptions op object?[] parameters; - if (!HasParameters && args.AvailableLength == 0) + if (!HasParameters && args.RemainingLength == 0) parameters = []; - else if (MaxLength == args.AvailableLength || (MaxLength <= args.AvailableLength && HasRemainder) || (MaxLength > args.AvailableLength && MinLength <= args.AvailableLength)) + else if (MaxLength == args.RemainingLength || (MaxLength <= args.RemainingLength && HasRemainder) || (MaxLength > args.RemainingLength && MinLength <= args.RemainingLength)) { var arguments = await ComponentUtilities.Parse(this, caller, args, options).ConfigureAwait(false); @@ -184,7 +184,7 @@ public async ValueTask Run(TContext caller, CommandOptions op } } else - return ParseResult.FromError(new CommandOutOfRangeException(this, args.AvailableLength)); + return ParseResult.FromError(new CommandOutOfRangeException(this, args.RemainingLength)); if (!options.SkipConditions) { diff --git a/src/Commands/Core/Execution/ArgumentDictionary.cs b/src/Commands/Core/Execution/ArgumentDictionary.cs index 0fdb321a..719b222d 100644 --- a/src/Commands/Core/Execution/ArgumentDictionary.cs +++ b/src/Commands/Core/Execution/ArgumentDictionary.cs @@ -20,7 +20,7 @@ public struct ArgumentDictionary private readonly string[] _unnamedArgs; private readonly Dictionary _namedArgs; - internal int AvailableLength { get; private set; } + internal int RemainingLength { get; private set; } /// /// Gets the number of keys present in the dictionary. @@ -72,7 +72,7 @@ public ArgumentDictionary(IEnumerable> args, Strin } _unnamedArgs = unnamedFill; - AvailableLength = _unnamedArgs.Length + _namedArgs.Count; + RemainingLength = _unnamedArgs.Length + _namedArgs.Count; } /// @@ -82,7 +82,7 @@ public ArgumentDictionary() { _namedArgs = []; _unnamedArgs = []; - AvailableLength = 0; + RemainingLength = 0; } #region Internals @@ -138,7 +138,7 @@ internal readonly IEnumerable TakeRemaining(string parameterName) internal void SetParseIndex(int index) { _index = index; - AvailableLength -= index; + RemainingLength -= index; } #endregion From 21319e7f433f2666c9c552b87378ab73be373648 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Sun, 4 May 2025 16:56:04 +0200 Subject: [PATCH 5/6] Update samples --- .../Commands/TestModule.cs | 4 ++-- .../Commands.Samples.Console/Program.cs | 15 ++++++++------- .../Commands.Samples.FSharp/Program.fs | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Commands.Samples/Commands.Samples.Console/Commands/TestModule.cs b/src/Commands.Samples/Commands.Samples.Console/Commands/TestModule.cs index 1d9e9f3b..5a52fce5 100644 --- a/src/Commands.Samples/Commands.Samples.Console/Commands/TestModule.cs +++ b/src/Commands.Samples/Commands.Samples.Console/Commands/TestModule.cs @@ -2,8 +2,8 @@ namespace Commands.Samples; -[RequireContext] -public sealed class TestModule : CommandModule +[RequireContext] +public sealed class TestModule : CommandModule { [Name("testcommand")] [Test] diff --git a/src/Commands.Samples/Commands.Samples.Console/Program.cs b/src/Commands.Samples/Commands.Samples.Console/Program.cs index a0841a94..7c6d6878 100644 --- a/src/Commands.Samples/Commands.Samples.Console/Program.cs +++ b/src/Commands.Samples/Commands.Samples.Console/Program.cs @@ -12,14 +12,15 @@ .AddResultHandler(results) .ToProvider(); -var tests = new TestCollectionProperties() - .AddCommands([.. components.GetCommands()]) - .ToCollection(); +var tests = components.GetCommands().Select(x => TestProvider.From(x).ToProvider()); -var testEvaluation = await tests.Execute((str) => new TestContext(str)); +foreach (var test in tests) +{ + var result = await test.Test(x => new ConsoleCallerContext(x)); -if (testEvaluation.Count(x => x.Success) == tests.Count) - Console.WriteLine("All tests ran successfully."); + if (result.Any(x => !x.Success)) + throw new InvalidOperationException($"A command test failed to evaluate to success. Command: {test.Command}. Test: {result.FirstOrDefault(x => !x.Success).Test}"); +} while (true) - await components.Execute(new SampleContext(username: "Peter", args: Console.ReadLine())); \ No newline at end of file + await components.Execute(new SampleContext(username: "Peter", args: Console.ReadLine())); diff --git a/src/Commands.Samples/Commands.Samples.FSharp/Program.fs b/src/Commands.Samples/Commands.Samples.FSharp/Program.fs index ff104b82..e4dce103 100644 --- a/src/Commands.Samples/Commands.Samples.FSharp/Program.fs +++ b/src/Commands.Samples/Commands.Samples.FSharp/Program.fs @@ -2,7 +2,7 @@ open Commands.Samples; open System -let components = new ComponentCollection() +let components = new ComponentProvider() printf "Added %i components." (components.AddRange(typeof.Assembly.GetExportedTypes())) From 2d30c2ddaf9b257eda90811e8c98670ae19c3b3a Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Sun, 4 May 2025 17:17:43 +0200 Subject: [PATCH 6/6] Update documentation --- README.md | 10 +++++----- wiki/v2/Results.md | 4 ++-- wiki/v2/Return-Types.md | 2 +- wiki/v2/Testing.md | 31 ++++++++++++++++++------------- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 87d129b0..0e3c1158 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ using Commands; var command = Command.From(() => "Hello world!", "greet"); -var collection = ComponentCollection.From(command).ToCollection(); +var collection = ComponentProvider.From(command).ToProvider(); await collection.Execute(new ConsoleCallerContext(args)); @@ -75,7 +75,7 @@ var mathCommands = CommandGroup.From("math") "divide", "div") ); -var collection = ComponentCollection.From(mathCommands).ToCollection(); +var collection = ComponentProvider.From(mathCommands).ToProvider(); await collection.Execute(new ConsoleCallerContext(args)); @@ -106,7 +106,7 @@ public class HelpModule : CommandModule ... -var collection = ComponentCollection.From(mathCommands).AddType().ToCollection(); +var collection = ComponentProvider.From(mathCommands).AddType().ToProvider(); await collection.Execute(new ConsoleCallerContext(args)); @@ -125,10 +125,10 @@ using Microsoft.Extensions.DependencyInjection; var services = new ServiceCollection() .AddSingleton() - .AddSingleton(ComponentCollection.From(mathCommands).AddType().ToCollection()); + .AddSingleton(ComponentProvider.From(mathCommands).AddType().ToProvider()); .BuildServiceProvider(); -var collection = services.GetRequiredService(); +var collection = services.GetRequiredService(); await collection.Execute(new ConsoleCallerContext(args), new CommandOptions() { Services = services }); ``` diff --git a/wiki/v2/Results.md b/wiki/v2/Results.md index 954b26b8..57db1fe5 100644 --- a/wiki/v2/Results.md +++ b/wiki/v2/Results.md @@ -85,10 +85,10 @@ public class CustomResultHandler : ResultHandler The `CommandNotFound` method is called when the search operation returns no commands or groups. -When you have succesfully constructed the logic for handling the result, you can pass it along when creating a new `ComponentCollection`: +When you have succesfully constructed the logic for handling the result, you can pass it along when creating a new `ComponentProvider`: ```cs -var collection = ComponentCollection.From(...).AddHandler(new CustomResultHandler()).Create(); +var collection = ComponentProvider.From(...).AddHandler(new CustomResultHandler()).Create(); ``` > [!NOTE] diff --git a/wiki/v2/Return-Types.md b/wiki/v2/Return-Types.md index 2b70f616..13052c6f 100644 --- a/wiki/v2/Return-Types.md +++ b/wiki/v2/Return-Types.md @@ -93,5 +93,5 @@ public class CustomResultHandler : ResultHandler ``` > [!NOTE] -> When no implementation of `ResultHandler` is provided to a `ComponentCollection`, the library will use a default implementation. +> When no implementation of `ResultHandler` is provided to a `ComponentProvider`, the library will use a default implementation. > This implementation does not handle failed results, only resolving the returned value by an `InvokeResult` as described above. \ No newline at end of file diff --git a/wiki/v2/Testing.md b/wiki/v2/Testing.md index 4b731ec0..7c7c364b 100644 --- a/wiki/v2/Testing.md +++ b/wiki/v2/Testing.md @@ -6,13 +6,13 @@ This is useful for ensuring that commands are functioning as expected, and for d ## Creating Tests -Tests are defined in one of two ways: By creating a new `TestProvider`, or by marking `Test` on modular command methods. +Tests are defined in one of two ways: By creating a new `Test`, or by marking `Test` on modular command methods. ### TestProvider -Alongside other components within Commands.NET, a creation pattern is used to create a new `TestProvider`. This pattern is used to ensure that the provider is correctly initialized. +Alongside other components within Commands.NET, a creation pattern is used to create a new `Test`. This pattern is used to ensure that the test is correctly initialized. ```cs -var provider = TestProvider.From(command, "arguments", TestResultType.Success); +var provider = Test.From(...).ToTest(); ``` ### Test Attribute @@ -31,28 +31,33 @@ public void TestMethod() ## Testing Commands -Collections of commands can be tested in bulk using `TestCollection`. The class can be initialized using a functional pattern: +Collections of commands can be tested individually using the `TestProvider`. +When making use of the `ComponentProvider`, some clever use of LINQ will allow us to make providers for every known command. ```cs -var tests = TestCollection.From(collection.GetCommands().ToArray()).ToCollection(); +var tests = components.GetCommands().Select(x => TestProvider.From(x).ToProvider()); ``` -This will create a new instance of `TestCollection`, which will be used to test commands. -The runner can be started and awaited, running all available tests made available to it: +> [!IMPORTANT] +> When creating a test provider in this way, all `TestAttribute` definitions present on the command's execution delegate will be included. +> Additionally, extra tests can be defined using its own respective properties. + +This will create a new instance of `TestProvider` for every command, which contains available tests. +The provider can be tested against and awaited. All known tests will be ran in sequence, and the results returned as an enumerable of `TestResult`: ```cs -var results = await tests.Execute((input) => new TestContext(input)); +var results = await provider.Test((input) => new ConsoleCallerContext(input)); ``` -When this method completes, all tests have been executed. -In order to evaluate whether all results ran, `results.Count(x => x.Success)` can be compared against `runner.Count`. +When this method completes, all tests have been executed for the specified command. +To verify whether the tests succeeded, the results contain a `Success` boolean. Again, some LINQ will help us get the values we want. ```cs -if (results.Count(x => x.Success) == tests.Count) +if (results.Any(x => !x.Success)) { - // All tests ran successfully. + // A test failed for the command. } ``` > [!NOTE] -> To ensure that the context is not shared between commands, `TestCollection` will create a new instance of `ICallerContext` using the provided delegate for every discovered test. \ No newline at end of file +> To ensure that the context is not shared between tests, the `TestProvider` will create a new instance of `ICallerContext` using the provided delegate for every test. \ No newline at end of file