diff --git a/CommandLineParser.Tests/Command/MultipleCommandTests.cs b/CommandLineParser.Tests/Command/MultipleCommandTests.cs index bc8842e..61a1782 100644 --- a/CommandLineParser.Tests/Command/MultipleCommandTests.cs +++ b/CommandLineParser.Tests/Command/MultipleCommandTests.cs @@ -14,12 +14,12 @@ public void NonRequiredCommandShouldNotSetResultInErrorStateWhenRequiredOptionsA { var parser = new CommandLineParser(); - parser.AddCommand() + parser.AddCommand() .Name("cmd1") .Required(false) .Description("cmd1"); - parser.AddCommand() + parser.AddCommand() .Name("cmd2") .Required(false) .Description("cmd2"); @@ -29,7 +29,7 @@ public void NonRequiredCommandShouldNotSetResultInErrorStateWhenRequiredOptionsA result.AssertNoErrors(); } - private class MultipleCOmmandTestsOptions + private class MultipleCommandTestsOptions { [Required, Name("x", "bla"), Description("some description")] public int Option { get; set; } diff --git a/CommandLineParser.Tests/Command/SubCommandTests.cs b/CommandLineParser.Tests/Command/SubCommandTests.cs index fc9b034..8cfa571 100644 --- a/CommandLineParser.Tests/Command/SubCommandTests.cs +++ b/CommandLineParser.Tests/Command/SubCommandTests.cs @@ -6,6 +6,7 @@ using System; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Xunit; namespace MatthiWare.CommandLine.Tests.Command @@ -42,6 +43,36 @@ public void TestSubCommandWorksCorrectlyInModel(bool autoExecute, string bla, in Assert.All(result.CommandResults.Select(r => r.Executed), Assert.True); } + [Theory] + [InlineData(true, "something", 15, -1)] + [InlineData(false, "something", 15, -1)] + [InlineData(true, "", 15, -1)] + public async Task TestSubCommandWorksCorrectlyInModelAsync(bool autoExecute, string bla, int i, int n) + { + var lock1 = new ManualResetEventSlim(); + var lock2 = new ManualResetEventSlim(); + + var containerResolver = new CustomInstantiator(lock1, lock2, autoExecute, bla, i, n); + + var parser = new CommandLineParser(containerResolver); + + var result = await parser.ParseAsync(new[] { "main", "-b", bla, "sub", "-i", i.ToString(), "-n", n.ToString() }); + + result.AssertNoErrors(); + + if (!autoExecute) + { + Assert.All(result.CommandResults.Select(r => r.Executed), Assert.False); + + result.ExecuteCommands(); + } + + Assert.True(lock1.Wait(1000), "MainCommand didn't execute in time."); + Assert.True(lock2.Wait(1000), "SubCommand didn't execute in time."); + + Assert.All(result.CommandResults.Select(r => r.Executed), Assert.True); + } + private class CustomInstantiator : DefaultContainerResolver { private readonly ManualResetEventSlim lock1; @@ -108,6 +139,18 @@ public override void OnExecute(MainModel options, SubModel commandOptions) locker.Set(); } + + public override Task OnExecuteAsync(MainModel options, SubModel commandOptions, CancellationToken cancellationToken) + { + base.OnExecuteAsync(options, commandOptions, cancellationToken); + + Assert.Equal(bla, options.Bla); + Assert.Equal(i, commandOptions.Item); + + locker.Set(); + + return Task.CompletedTask; + } } public class SubCommand : Command @@ -144,6 +187,18 @@ public override void OnExecute(MainModel options, SubSubModel commandOptions) locker.Set(); } + + public override Task OnExecuteAsync(MainModel options, SubSubModel commandOptions, CancellationToken cancellationToken) + { + base.OnExecuteAsync(options, commandOptions, cancellationToken); + + Assert.Equal(bla, options.Bla); + Assert.Equal(n, commandOptions.Nothing); + + locker.Set(); + + return Task.CompletedTask; + } } public class MainModel diff --git a/CommandLineParser.Tests/CommandLineParserTests.cs b/CommandLineParser.Tests/CommandLineParserTests.cs index f91bce3..343a275 100644 --- a/CommandLineParser.Tests/CommandLineParserTests.cs +++ b/CommandLineParser.Tests/CommandLineParserTests.cs @@ -7,6 +7,7 @@ using System; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Xunit; namespace MatthiWare.CommandLine.Tests @@ -84,6 +85,36 @@ public void CommandLineParserUsesContainerCorrectly(bool generic) containerMock.VerifyAll(); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CommandLineParserUsesContainerCorrectlyAsync(bool generic) + { + var commandMock = new Mock(); + commandMock.Setup( + c => c.OnConfigure(It.IsAny>())) + .CallBase().Verifiable("OnConfigure not called"); + + commandMock.Setup(c => c.OnExecuteAsync(It.IsAny(), It.IsAny(), It.IsAny())).Verifiable("OnExecute not called"); + + var containerMock = new Mock(); + containerMock.Setup(c => c.Resolve()).Returns(commandMock.Object).Verifiable(); + + var parser = new CommandLineParser(containerMock.Object); + + if (generic) + parser.RegisterCommand(); + else + parser.RegisterCommand(typeof(MyCommand), typeof(object)); + + var result = await parser.ParseAsync(new[] { "app.exe", "my" }); + + result.AssertNoErrors(); + + commandMock.VerifyAll(); + containerMock.VerifyAll(); + } + [Fact] public void CommandLinerParserPassesContainerCorreclty() { @@ -150,6 +181,30 @@ public void AutoExecuteCommandsWithExceptionDoesntCrashTheParser() Assert.Equal(ex, result.Errors.First()); } + [Fact] + public async Task AutoExecuteCommandsWithExceptionDoesntCrashTheParserAsync() + { + var parser = new CommandLineParser(); + + var ex = new Exception("uh-oh"); + + parser.AddCommand() + .Name("test") + .InvokeCommand(true) + .Required(true) + .OnExecutingAsync(async (_, __) => + { + await Task.Delay(1); + throw ex; + }); + + var result = await parser.ParseAsync(new[] { "test" }); + + Assert.True(result.HasErrors); + + Assert.Equal(ex, result.Errors.First()); + } + [Fact] public void CommandLineParserUsesArgumentFactoryCorrectly() { @@ -347,6 +402,49 @@ public void ParseWithCommandTests() Assert.True(wait.WaitOne(2000)); } + [Fact] + public async Task ParseWithCommandTestsAsync() + { + var wait = new ManualResetEvent(false); + + var parser = new CommandLineParser(); + + parser.Configure(opt => opt.Option1) + .Name("o") + .Default("Default message") + .Required(); + + var addCmd = parser.AddCommand() + .Name("add") + .OnExecutingAsync(async (opt, cmdOpt, ctx) => + { + await Task.Delay(100); + + Assert.Equal("test", opt.Option1); + Assert.Equal("my message", cmdOpt.Message); + + await Task.Delay(100); + + wait.Set(); + + await Task.Delay(100); + }); + + addCmd.Configure(opt => opt.Message) + .Name("m", "message") + .Required(); + + var parsed = await parser.ParseAsync(new string[] { "app.exe", "-o", "test", "add", "-m=my message" }); + + parsed.AssertNoErrors(); + + Assert.Equal("test", parsed.Result.Option1); + + parsed.ExecuteCommands(); + + Assert.True(wait.WaitOne(2000)); + } + [Theory] [InlineData(new[] { "app.exe", "add", "-m", "message2", "-m", "message1" }, "message1", "message2")] [InlineData(new[] { "app.exe", "-m", "message1", "add", "-m", "message2" }, "message1", "message2")] @@ -383,6 +481,44 @@ public void ParseCommandTests(string[] args, string result1, string result2) Assert.True(wait.WaitOne(2000)); } + [Theory] + [InlineData(new[] { "app.exe", "add", "-m", "message2", "-m", "message1" }, "message1", "message2")] + [InlineData(new[] { "app.exe", "-m", "message1", "add", "-m", "message2" }, "message1", "message2")] + [InlineData(new[] { "add", "-m", "message2", "-m", "message1" }, "message1", "message2")] + [InlineData(new[] { "-m", "message1", "add", "-m", "message2" }, "message1", "message2")] + public async Task ParseCommandTestsAsync(string[] args, string result1, string result2) + { + var parser = new CommandLineParser(); + var wait = new ManualResetEvent(false); + + parser.AddCommand() + .Name("add") + .Required() + .OnExecutingAsync(async (opt1, opt2, ctx) => + { + await Task.Delay(100); + + wait.Set(); + + Assert.Equal(result2, opt2.Message); + }) + .Configure(c => c.Message) + .Name("m", "message") + .Required(); + + parser.Configure(opt => opt.Message) + .Name("m", "message") + .Required(); + + var result = await parser.ParseAsync(args); + + result.AssertNoErrors(); + + Assert.Equal(result1, result.Result.Message); + + Assert.True(wait.WaitOne(2000)); + } + [Theory] [InlineData(new string[] { "-x", "" }, true)] [InlineData(new string[] { "-x" }, true)] @@ -531,6 +667,47 @@ public void TransformationWorksAsExpectedForCommandOptions(string[] args, int ex Assert.Equal(expected, outcome); } + [Theory] + [InlineData(new string[] { "cmd" }, "", true)] + [InlineData(new string[] { "cmd", "-s", "-s2" }, "", true)] + [InlineData(new string[] { "cmd", "-s", "test", "-s2", "test" }, "test", false)] + [InlineData(new string[] { "cmd", "--string", "test", "-s2", "test" }, "test", false)] + public void CustomTypeWithStringTryParseGetsParsedCorrectly(string[] args, string expected, bool errors) + { + var parser = new CommandLineParser(); + + var result = parser.Parse(args); + + Assert.Equal(errors, result.AssertNoErrors(!errors)); + + if (!result.HasErrors) + { + Assert.Equal(expected, result.Result.String.Result); + Assert.Equal(expected, result.Result.String2.Result); + } + } + + [Theory] + [InlineData(new string[] { "cmd" }, "", true)] + [InlineData(new string[] { "cmd", "-s", "-s2", "-s3" }, "", true)] + [InlineData(new string[] { "cmd", "-s", "test", "-s2", "test", "-s3", "test" }, "test", false)] + [InlineData(new string[] { "cmd", "--string", "test", "-s2", "test", "-s3", "test" }, "test", false)] + public void CustomTypeWithStringConstructorGetsParsedCorrectly(string[] args, string expected, bool errors) + { + var parser = new CommandLineParser(); + + var result = parser.Parse(args); + + Assert.Equal(errors, result.AssertNoErrors(!errors)); + + if (!result.HasErrors) + { + Assert.Equal(expected, result.Result.String.Result); + Assert.Equal(expected, result.Result.String2.Result); + Assert.Equal(expected, result.Result.String3.Result); + } + } + private class ObjOption { [Name("p"), Required] @@ -570,5 +747,126 @@ private class IntOptions { public int SomeInt { get; set; } } + + private class StringTypeOptions + { + [Name("s", "string"), Required] + public StringType String { get; set; } + + [Name("s2"), Required] + public StringType4 String2 { get; set; } + + [Name("s3"), Required] + public StringType5 String3 { get; set; } + } + + private class StringTryParseTypeOptions + { + [Name("s", "string"), Required] + public StringType2 String { get; set; } + + [Name("s2"), Required] + public StringType3 String2 { get; set; } + } + + private class StringType + { + public StringType(string input) + { + Result = input; + } + + public StringType(string input, string input2) + { + Result = input; + } + + public string Result { get; } + } + + private class StringType2 + { + private StringType2(string input) + { + Result = input; + } + + public string Result { get; } + + public static bool TryParse(string input, IFormatProvider format, out StringType2 result) + { + result = new StringType2(input); + return true; + } + + public static bool TryParse() => false; + + public static void Tryparse(string input, IFormatProvider format, out StringType2 result) + { + result = default; + } + + public static bool TryParse(string input, StringType2 xd, out StringType2 stringType2) + { + stringType2 = default; + return false; + } + } + + private class StringType3 + { + private StringType3(string input) + { + Result = input; + } + + public string Result { get; } + + public static bool TryParse(string input, out StringType3 result) + { + result = new StringType3(input); + return true; + } + } + + private class StringType4 + { + private StringType4(string input) + { + Result = input; + } + + public string Result { get; } + + public static StringType4 Parse(string input) + { + return new StringType4(input); + } + } + + private class StringType5 + { + private StringType5(string input) + { + Result = input; + } + + public string Result { get; } + + public static StringType5 Parse(string input, IFormatProvider provider) + { + return new StringType5(input); + } + + public static StringType4 Parse(string input) + { + return null; + } + + public static StringType5 Parse(string input, IFormatProvider provider, IFormatProvider xd) + { + return null; + } + } } } diff --git a/CommandLineParser.Tests/Core/TypedInstanceCacheTests.cs b/CommandLineParser.Tests/Core/TypedInstanceCacheTests.cs new file mode 100644 index 0000000..6e12512 --- /dev/null +++ b/CommandLineParser.Tests/Core/TypedInstanceCacheTests.cs @@ -0,0 +1,70 @@ +using MatthiWare.CommandLine.Abstractions; +using MatthiWare.CommandLine.Core; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Xunit; + +namespace MatthiWare.CommandLine.Tests.Core +{ + public class TypedInstanceCacheTests + { + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddingItemsDoesNotTriggerResolve(bool doubleAdd) + { + var containerMock = new Mock(); + + var cache = new TypedInstanceCache(containerMock.Object); + + var type1 = new MyType(); + var type2 = new MyType(); + + cache.Add(type1); + + var result = cache.Get(); + + Assert.Equal(type1, result.First()); + + if (doubleAdd) + { + cache.Add(type2); + + result = cache.Get(); + + Assert.Equal(type2, result.First()); + } + + Assert.True(result.Count == 1); + + containerMock.Verify(c => c.Resolve(It.Is(t => t == typeof(MyType))), Times.Never()); + } + + [Fact] + public void AddingItemTypeDoesTriggerResolve() + { + var containerMock = new Mock(); + + var cache = new TypedInstanceCache(containerMock.Object); + + var type1 = new MyType(); + + containerMock.Setup(c => c.Resolve(It.Is(t => t == typeof(MyType)))).Returns(type1); + + cache.Add(typeof(MyType)); + + var result = cache.Get(); + + Assert.Equal(type1, result.First()); + + Assert.True(result.Count == 1); + + containerMock.Verify(c => c.Resolve(It.Is(t => t == typeof(MyType))), Times.Once()); + } + + private class MyType { } + } +} diff --git a/CommandLineParser.Tests/Exceptions/ExceptionsTest.cs b/CommandLineParser.Tests/Exceptions/ExceptionsTest.cs index 48bcf07..c7d4880 100644 --- a/CommandLineParser.Tests/Exceptions/ExceptionsTest.cs +++ b/CommandLineParser.Tests/Exceptions/ExceptionsTest.cs @@ -1,16 +1,46 @@ -using System; +using MatthiWare.CommandLine; +using MatthiWare.CommandLine.Abstractions.Command; +using MatthiWare.CommandLine.Core.Attributes; +using MatthiWare.CommandLine.Core.Exceptions; +using System; using System.Collections.Generic; using System.Linq; using System.Text; -using MatthiWare.CommandLine; -using MatthiWare.CommandLine.Core.Attributes; -using MatthiWare.CommandLine.Core.Exceptions; +using System.Threading.Tasks; using Xunit; namespace MatthiWare.CommandLine.Tests.Exceptions { public class ExceptionsTest { + [Fact] + public void SubCommandNotFoundTest() + { + var parser = new CommandLineParser(); + + var result = parser.Parse(new string[] { "cmd" }); + + Assert.True(result.HasErrors); + + Assert.IsType(result.Errors.First()); + + Assert.Same(parser.Commands.First(), result.Errors.Cast().First().Command); + } + + [Fact] + public async Task SubCommandNotFoundTestAsync() + { + var parser = new CommandLineParser(); + + var result = await parser.ParseAsync(new string[] { "cmd" }); + + Assert.True(result.HasErrors); + + Assert.IsType(result.Errors.First()); + + Assert.Same(parser.Commands.First(), result.Errors.Cast().First().Command); + } + [Fact] public void CommandNotFoundTest() { @@ -28,11 +58,27 @@ public void CommandNotFoundTest() } [Fact] - public void OptionNotFoundTest() + public async Task CommandNotFoundTestAsync() + { + var parser = new CommandLineParser(); + + parser.AddCommand().Name("missing").Required(); + + var result = await parser.ParseAsync(new string[] { }); + + Assert.True(result.HasErrors); + + Assert.IsType(result.Errors.First()); + + Assert.Same(parser.Commands.First(), result.Errors.Cast().First().Command); + } + + [Fact] + public async Task OptionNotFoundTestAsync() { var parser = new CommandLineParser(); - var result = parser.Parse(new string[] { }); + var result = await parser.ParseAsync(new string[] { }); Assert.True(result.HasErrors); @@ -62,6 +108,27 @@ public void CommandParseExceptionTest() Assert.Same(parser.Commands.First(), result.Errors.Cast().First().Command); } + [Fact] + public async Task CommandParseExceptionTestAsync() + { + var parser = new CommandLineParser(); + + parser.AddCommand() + .Name("missing") + .Required() + .Configure(opt => opt.MissingOption) + .Name("o") + .Required(); + + var result = await parser.ParseAsync(new string[] { "missing", "-o", "bla" }); + + Assert.True(result.HasErrors); + + Assert.IsType(result.Errors.First()); + + Assert.Same(parser.Commands.First(), result.Errors.Cast().First().Command); + } + [Fact] public void OptionParseExceptionTest() { @@ -76,10 +143,49 @@ public void OptionParseExceptionTest() Assert.Same(parser.Options.First(), result.Errors.Cast().First().Option); } + [Fact] + public async Task OptionParseExceptionTestAsync() + { + var parser = new CommandLineParser(); + + var result = await parser.ParseAsync(new string[] { "-m", "bla" }); + + Assert.True(result.HasErrors); + + Assert.IsType(result.Errors.First()); + + Assert.Same(parser.Options.First(), result.Errors.Cast().First().Option); + } + private class Options { [Required, Name("m", "missing")] public int MissingOption { get; set; } } + + private class Options2 + { + public SubCmd SubCmd { get; set; } + } + + private class Cmd : Command + { + public override void OnConfigure(ICommandConfigurationBuilder builder) + { + base.OnConfigure(builder); + + builder.Name("cmd").Required(); + } + } + + private class SubCmd : Command + { + public override void OnConfigure(ICommandConfigurationBuilder builder) + { + base.OnConfigure(builder); + + builder.Name("sub").Required(); + } + } } } diff --git a/CommandLineParser.Tests/OptionBuilderTest.cs b/CommandLineParser.Tests/OptionBuilderTest.cs index d1a7c88..12b6ee2 100644 --- a/CommandLineParser.Tests/OptionBuilderTest.cs +++ b/CommandLineParser.Tests/OptionBuilderTest.cs @@ -1,11 +1,9 @@ -using System; -using MatthiWare.CommandLine; +using MatthiWare.CommandLine; using MatthiWare.CommandLine.Abstractions; using MatthiWare.CommandLine.Abstractions.Parsing; using MatthiWare.CommandLine.Core; - using Moq; - +using System; using Xunit; namespace MatthiWare.CommandLine.Tests diff --git a/CommandLineParser.Tests/Parsing/ResolverFactoryTest.cs b/CommandLineParser.Tests/Parsing/ResolverFactoryTest.cs index 5a0cde4..f638538 100644 --- a/CommandLineParser.Tests/Parsing/ResolverFactoryTest.cs +++ b/CommandLineParser.Tests/Parsing/ResolverFactoryTest.cs @@ -1,13 +1,10 @@ -using System; - -using MatthiWare.CommandLine.Abstractions.Models; +using MatthiWare.CommandLine.Abstractions.Models; using MatthiWare.CommandLine.Abstractions.Parsing; using MatthiWare.CommandLine.Core; using MatthiWare.CommandLine.Core.Parsing; using MatthiWare.CommandLine.Core.Parsing.Resolvers; - using Moq; - +using System; using Xunit; namespace MatthiWare.CommandLine.Tests.Parsing @@ -84,6 +81,7 @@ public void RegisterOverrideWorks() factory.Register(typeof(string), mockResolver.Object.GetType(), true); factory.Register(true); + factory.Register(true); } [Fact] @@ -96,6 +94,14 @@ public void RegisterThrowsException() Assert.Throws(() => factory.Register()); } + [Fact] + public void NonAssignableTypeThrowsException() + { + var factory = new DefaultArgumentResolverFactory(new DefaultContainerResolver()); + + Assert.Throws(() => factory.Register(typeof(string), typeof(Mock), true)); + } + [Fact] public void RegisterObjectResolver() { diff --git a/CommandLineParser.Tests/Parsing/Resolvers/DefaultResolverTests.cs b/CommandLineParser.Tests/Parsing/Resolvers/DefaultResolverTests.cs new file mode 100644 index 0000000..934dde5 --- /dev/null +++ b/CommandLineParser.Tests/Parsing/Resolvers/DefaultResolverTests.cs @@ -0,0 +1,59 @@ +using MatthiWare.CommandLine.Abstractions.Models; +using MatthiWare.CommandLine.Core.Parsing.Resolvers; +using Xunit; + +namespace MatthiWare.CommandLine.Tests.Parsing.Resolvers +{ + public class DefaultResolverTests + { + [Theory] + [InlineData(true, "-m", "test")] + [InlineData(true, "-m", "my string")] + public void TestCanResolve(bool expected, string key, string value) + { + var resolver = new DefaultResolver(); + var model = new ArgumentModel(key, value); + + Assert.Equal(expected, resolver.CanResolve(model)); + } + + [Theory] + [InlineData(false, "-m", "test")] + [InlineData(false, "-m", "my string")] + public void TestCanResolveWithWrongCtor(bool expected, string key, string value) + { + var resolver = new DefaultResolver(); + var model = new ArgumentModel(key, value); + + Assert.Equal(expected, resolver.CanResolve(model)); + } + + [Theory] + [InlineData("test", "-m", "test")] + [InlineData("my string", "-m", "my string")] + public void TestResolve(string expected, string key, string value) + { + var resolver = new DefaultResolver(); + var model = new ArgumentModel(key, value); + + Assert.Equal(expected, resolver.Resolve(model).Result); + } + + public class MyTestType + { + public MyTestType(string ctor) + { + Result = ctor; + } + + public string Result { get; } + } + + public class MyTestType2 + { + public MyTestType2(int someInt) + { + } + } + } +} diff --git a/CommandLineParser.Tests/Parsing/Resolvers/EnumResolverTests.cs b/CommandLineParser.Tests/Parsing/Resolvers/EnumResolverTests.cs index d3adba8..25d93b8 100644 --- a/CommandLineParser.Tests/Parsing/Resolvers/EnumResolverTests.cs +++ b/CommandLineParser.Tests/Parsing/Resolvers/EnumResolverTests.cs @@ -21,7 +21,9 @@ public void TestCanResolve(bool expected, string key, string value) [Theory] [InlineData(TestEnum.Error, "-m", "Error")] + [InlineData(TestEnum.Error, "-m", "error")] [InlineData(TestEnum.Verbose, "-m", "Verbose")] + [InlineData(TestEnum.Verbose, "-m", "verbose")] public void TestResolve(TestEnum expected, string key, string value) { var resolver = new EnumResolver(); diff --git a/CommandLineParser.Tests/Parsing/Validation/ValidationAbstractionTests.cs b/CommandLineParser.Tests/Parsing/Validation/ValidationAbstractionTests.cs new file mode 100644 index 0000000..a5c81af --- /dev/null +++ b/CommandLineParser.Tests/Parsing/Validation/ValidationAbstractionTests.cs @@ -0,0 +1,109 @@ +using MatthiWare.CommandLine.Abstractions.Command; +using MatthiWare.CommandLine.Abstractions.Validations; +using MatthiWare.CommandLine.Core.Attributes; +using Moq; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace MatthiWare.CommandLine.Tests.Parsing.Validation +{ + public class ValidationAbstractionTests + { + [Fact] + public void ParsingCallsValidation() + { + var parser = new CommandLineParser(); + + var validValidationResultMock = new Mock(); + validValidationResultMock.SetupGet(v => v.IsValid).Returns(true); + + var optionWithCommandMockValidator = new Mock>(); + optionWithCommandMockValidator + .Setup(v => v.Validate(It.IsAny())) + .Returns(validValidationResultMock.Object) + .Verifiable(); + + var optionMockValidator = new Mock>(); + optionMockValidator + .Setup(v => v.Validate(It.IsAny())) + .Returns(validValidationResultMock.Object) + .Verifiable(); + + parser.Validators.AddValidator(optionWithCommandMockValidator.Object); + parser.Validators.AddValidator(optionMockValidator.Object); + + var result = parser.Parse(new[] { "-x", "true", "cmd", "-y", "true" }); + + result.AssertNoErrors(); + + optionMockValidator.Verify(); + optionWithCommandMockValidator.Verify(); + } + + [Fact] + public async Task ParsingCallsValidationAsync() + { + var parser = new CommandLineParser(); + + var validValidationResultMock = new Mock(); + validValidationResultMock.SetupGet(v => v.IsValid).Returns(true); + + var optionWithCommandMockValidator = new Mock>(); + optionWithCommandMockValidator + .Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(validValidationResultMock.Object)) + .Verifiable(); + + var optionMockValidator = new Mock>(); + optionMockValidator + .Setup(v => v.ValidateAsync(It.IsAny