Skip to content

Commit 5923724

Browse files
authored
Version 0.3.1 (#67)
* Add Command Discovery (#66) * Add command disoverer * Add tests * Improve Usage Printer and Usage Builder API (#68) * Improve IUsageBuilder, IUsagePrinter - Add errors - Rework API Fixes #64 * Improve error output Update program * Fix errors * Make UsagePrinter virtual * Improve styling * Add NO_COLOR support (#69) * Add support for NO_COLOR fixes #52 * Update xml documentation * Add tests * Fix styling issue
1 parent ed262a6 commit 5923724

23 files changed

+882
-108
lines changed
10.5 KB
Binary file not shown.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using MatthiWare.CommandLine.Core.Command;
2+
using Xunit;
3+
using MatthiWare.CommandLine.Abstractions.Command;
4+
using System.Reflection;
5+
6+
namespace MatthiWare.CommandLine.Tests.Command
7+
{
8+
public class CommandDiscoveryTests
9+
{
10+
[Fact]
11+
public void DiscoverCommandFromAssemblyContainsCorrectTypes()
12+
{
13+
var cmdDiscovery = new CommandDiscoverer();
14+
15+
var resultTypes = cmdDiscovery.DiscoverCommandTypes(typeof(CommandDiscoveryTests), new[] { Assembly.GetExecutingAssembly() });
16+
17+
var invalidAbstractCommand = typeof(AbstractCommand);
18+
var wrongGenericTypeCommand = typeof(WrongGenericTypeCommand);
19+
var validCommand = typeof(ValidCommand);
20+
var validCommand2 = typeof(ValidCommand2);
21+
22+
Assert.DoesNotContain(invalidAbstractCommand, resultTypes);
23+
Assert.DoesNotContain(wrongGenericTypeCommand, resultTypes);
24+
Assert.Contains(validCommand, resultTypes);
25+
Assert.Contains(validCommand2, resultTypes);
26+
}
27+
28+
[Fact]
29+
public void DiscoveredCommandsAreRegisteredCorrectly()
30+
{
31+
var parser = new CommandLineParser<CommandDiscoveryTests>();
32+
33+
parser.DiscoverCommands(Assembly.GetExecutingAssembly());
34+
35+
Assert.Equal(2, parser.Commands.Count);
36+
}
37+
38+
public abstract class AbstractCommand : Command<CommandDiscoveryTests>
39+
{
40+
}
41+
42+
public abstract class WrongGenericTypeCommand : Command<object>
43+
{
44+
}
45+
46+
public class ValidCommand : Command<CommandDiscoveryTests>
47+
{
48+
}
49+
50+
public class ValidCommand2 : Command<CommandDiscoveryTests, object>
51+
{
52+
}
53+
}
54+
}

CommandLineParser.Tests/CommandLineParserTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ public void AutoExecuteCommandsWithExceptionDoesntCrashTheParser()
178178

179179
Assert.True(result.HasErrors);
180180

181-
Assert.Equal(ex, result.Errors.First());
181+
Assert.Equal(ex, result.Errors.First().GetBaseException());
182182
}
183183

184184
[Fact]
@@ -202,7 +202,7 @@ public async Task AutoExecuteCommandsWithExceptionDoesntCrashTheParserAsync()
202202

203203
Assert.True(result.HasErrors);
204204

205-
Assert.Equal(ex, result.Errors.First());
205+
Assert.Equal(ex, result.Errors.First().GetBaseException());
206206
}
207207

208208
[Fact]

CommandLineParser.Tests/Usage/HelpDisplayCommandTests.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,8 @@ public void TestHelpDisplayFiresCorrectly(string[] args, bool fires)
2626
var usagePrinterMock = new Mock<IUsagePrinter>();
2727

2828
usagePrinterMock.Setup(mock => mock.PrintUsage()).Callback(() => calledFlag = true);
29-
usagePrinterMock.Setup(mock => mock.PrintUsage(It.IsAny<IArgument>())).Callback(() => calledFlag = true);
30-
usagePrinterMock.Setup(mock => mock.PrintUsage(It.IsAny<ICommandLineCommand>())).Callback(() => calledFlag = true);
31-
usagePrinterMock.Setup(mock => mock.PrintUsage(It.IsAny<ICommandLineOption>())).Callback(() => calledFlag = true);
29+
usagePrinterMock.Setup(mock => mock.PrintCommandUsage(It.IsAny<ICommandLineCommand>())).Callback(() => calledFlag = true);
30+
usagePrinterMock.Setup(mock => mock.PrintOptionUsage(It.IsAny<ICommandLineOption>())).Callback(() => calledFlag = true);
3231

3332
var parser = new CommandLineParser<Options>
3433
{
@@ -41,7 +40,7 @@ public void TestHelpDisplayFiresCorrectly(string[] args, bool fires)
4140

4241
parser.Parse(args);
4342

44-
Assert.Equal<bool>(fires, calledFlag);
43+
Assert.Equal(fires, calledFlag);
4544
}
4645

4746
[Theory]
@@ -60,9 +59,8 @@ public async Task TestHelpDisplayFiresCorrectlyAsync(string[] args, bool fires)
6059
var usagePrinterMock = new Mock<IUsagePrinter>();
6160

6261
usagePrinterMock.Setup(mock => mock.PrintUsage()).Callback(() => calledFlag = true);
63-
usagePrinterMock.Setup(mock => mock.PrintUsage(It.IsAny<IArgument>())).Callback(() => calledFlag = true);
64-
usagePrinterMock.Setup(mock => mock.PrintUsage(It.IsAny<ICommandLineCommand>())).Callback(() => calledFlag = true);
65-
usagePrinterMock.Setup(mock => mock.PrintUsage(It.IsAny<ICommandLineOption>())).Callback(() => calledFlag = true);
62+
usagePrinterMock.Setup(mock => mock.PrintCommandUsage(It.IsAny<ICommandLineCommand>())).Callback(() => calledFlag = true);
63+
usagePrinterMock.Setup(mock => mock.PrintOptionUsage(It.IsAny<ICommandLineOption>())).Callback(() => calledFlag = true);
6664

6765
var parser = new CommandLineParser<Options>
6866
{
@@ -75,7 +73,7 @@ public async Task TestHelpDisplayFiresCorrectlyAsync(string[] args, bool fires)
7573

7674
await parser.ParseAsync(args);
7775

78-
Assert.Equal<bool>(fires, calledFlag);
76+
Assert.Equal(fires, calledFlag);
7977
}
8078

8179
public class Options
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using MatthiWare.CommandLine.Abstractions;
2+
using MatthiWare.CommandLine.Abstractions.Usage;
3+
using MatthiWare.CommandLine.Core.Attributes;
4+
using MatthiWare.CommandLine.Core.Usage;
5+
using Moq;
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Text;
9+
using Xunit;
10+
11+
namespace MatthiWare.CommandLine.Tests.Usage
12+
{
13+
[Collection("Non-Parallel Collection")]
14+
public class NoColorOutputTests
15+
{
16+
private readonly CommandLineParser<Options> parser;
17+
private readonly IEnvironmentVariablesService variablesService;
18+
private Action<ConsoleColor> consoleColorGetter;
19+
private bool variableServiceResult;
20+
private readonly UsagePrinter printer;
21+
22+
public NoColorOutputTests()
23+
{
24+
var envMock = new Mock<IEnvironmentVariablesService>();
25+
envMock.SetupGet(env => env.NoColorRequested).Returns(() => variableServiceResult);
26+
27+
variablesService = envMock.Object;
28+
29+
var usageBuilderMock = new Mock<IUsageBuilder>();
30+
usageBuilderMock.Setup(m => m.AddErrors(It.IsAny<IReadOnlyCollection<Exception>>())).Callback(() =>
31+
{
32+
consoleColorGetter(printer.m_currentConsoleColor);
33+
});
34+
35+
parser = new CommandLineParser<Options>();
36+
37+
printer = new UsagePrinter(parser, usageBuilderMock.Object, variablesService);
38+
39+
parser.Printer = printer;
40+
}
41+
42+
[Fact]
43+
public void CheckUsageOutputRespectsNoColor()
44+
{
45+
ParseAndCheckNoColor(false);
46+
ParseAndCheckNoColor(true);
47+
}
48+
49+
private void ParseAndCheckNoColor(bool noColorOuput)
50+
{
51+
consoleColorGetter = noColorOuput ? (Action<ConsoleColor>)AssertNoColor : AssertColor;
52+
53+
variableServiceResult = noColorOuput;
54+
55+
parser.Parse(new string[] { "alpha" });
56+
}
57+
58+
private void AssertNoColor(ConsoleColor color)
59+
{
60+
Assert.True(variablesService.NoColorRequested);
61+
Assert.NotEqual(ConsoleColor.Red, color);
62+
}
63+
64+
private void AssertColor(ConsoleColor color)
65+
{
66+
Assert.False(variablesService.NoColorRequested);
67+
Assert.Equal(ConsoleColor.Red, color);
68+
}
69+
70+
private class Options
71+
{
72+
[Required, Name("b")]
73+
public bool MyBool { get; set; }
74+
}
75+
}
76+
}

CommandLineParser.Tests/Usage/UsagePrinterTests.cs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public void UsagePrinterPrintsOptionCorrectly()
8686

8787
parser.Parse(new[] { "-o", "--help" });
8888

89-
printerMock.Verify(mock => mock.PrintUsage(It.IsAny<IArgument>()), Times.Once());
89+
printerMock.Verify(mock => mock.PrintOptionUsage(It.IsAny<ICommandLineOption>()), Times.Once());
9090
}
9191

9292
[Fact]
@@ -105,7 +105,7 @@ public void UsagePrinterPrintsCommandCorrectly()
105105

106106
parser.Parse(new[] { "-o", "bla", "cmd", "--help" });
107107

108-
printerMock.Verify(mock => mock.PrintUsage(It.IsAny<IArgument>()), Times.Once());
108+
printerMock.Verify(mock => mock.PrintCommandUsage(It.IsAny<ICommandLineCommand>()), Times.Once());
109109
}
110110

111111
[Theory]
@@ -123,41 +123,46 @@ public void CustomInvokedPrinterWorksCorrectly(string[] args, bool cmdPassed, bo
123123

124124
var parser = new CommandLineParser<UsagePrinterGetsCalledOptions>(parserOptions);
125125

126-
parser.Printer = new UsagePrinter(parserOptions, parser, builderMock.Object);
126+
parser.Printer = new UsagePrinter(parser, builderMock.Object);
127127

128128
parser.AddCommand<UsagePrinterCommandOptions>()
129129
.Name("cmd")
130130
.Required();
131131

132132
var result = parser.Parse(args);
133133

134-
builderMock.Verify(mock => mock.Print(), Times.Never());
135-
builderMock.Verify(mock => mock.PrintCommand(It.IsAny<string>(), It.IsAny<ICommandLineCommandContainer>()), Times.Never());
136-
builderMock.Verify(mock => mock.PrintOption(It.IsAny<ICommandLineOption>()), Times.Never());
134+
builderMock.Verify(mock => mock.Build(), Times.Never());
135+
builderMock.Verify(mock => mock.AddCommand(It.IsAny<string>(), It.IsAny<ICommandLineCommandContainer>()), Times.Never());
136+
builderMock.Verify(mock => mock.AddOption(It.IsAny<ICommandLineOption>()), Times.Never());
137137

138138
if (result.HelpRequested)
139+
{
139140
parser.Printer.PrintUsage(result.HelpRequestedFor);
141+
}
140142

141143
if (result.HasErrors)
142144
{
143145
foreach (var err in result.Errors)
144146
{
145-
if (!(err is BaseParserException baseParserException)) continue;
147+
if (!(err is BaseParserException baseParserException))
148+
{
149+
continue;
150+
}
146151

147152
parser.Printer.PrintUsage(baseParserException.Argument);
148153
}
149154
}
150155

151156
builderMock.Verify(
152-
mock => mock.Print(),
157+
mock => mock.Build(),
153158
ToTimes(result.HelpRequested || result.HasErrors));
154159

155160
builderMock.Verify(
156-
mock => mock.PrintCommand(It.IsAny<string>(), It.IsAny<ICommandLineCommandContainer>()),
161+
mock => mock.AddCommand(It.IsAny<string>(), It.IsAny<ICommandLineCommand>()),
157162
ToTimes(cmdPassed));
158163

159164
builderMock.Verify(
160-
mock => mock.PrintOption(It.IsAny<ICommandLineOption>()),
165+
mock => mock.AddOption(It.IsAny<ICommandLineOption>()),
161166
ToTimes(optPassed));
162167
}
163168

CommandLineParser.Tests/XUnitExtensions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Linq.Expressions;
33
using MatthiWare.CommandLine.Abstractions.Parsing;
4+
using Xunit;
45

56
namespace MatthiWare.CommandLine.Tests
67
{
@@ -31,4 +32,11 @@ public static bool AssertNoErrors<T>(this IParserResult<T> result, bool shouldTh
3132
return false;
3233
}
3334
}
35+
36+
#pragma warning disable SA1402 // FileMayOnlyContainASingleType
37+
[CollectionDefinition("Non-Parallel Collection", DisableParallelization = true)]
38+
public class NonParallelCollection
39+
{
40+
}
41+
#pragma warning restore SA1402 // FileMayOnlyContainASingleType
3442
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Reflection;
4+
5+
namespace MatthiWare.CommandLine.Abstractions.Command
6+
{
7+
public interface ICommandDiscoverer
8+
{
9+
IReadOnlyList<Type> DiscoverCommandTypes(Type optionType, Assembly[] assemblies);
10+
}
11+
}

CommandLineParser/Abstractions/ICommandLineParser'.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq.Expressions;
7+
using System.Reflection;
78
using System.Threading;
89
using System.Threading.Tasks;
910

@@ -57,6 +58,18 @@ public interface ICommandLineParser<TOption>
5758

5859
#region Configuration
5960

61+
/// <summary>
62+
/// Discovers commands and registers them from any given assembly
63+
/// </summary>
64+
/// <param name="assembly">Assembly containing the command types</param>
65+
void DiscoverCommands(Assembly assembly);
66+
67+
/// <summary>
68+
/// Discovers commands and registers them from any given assembly
69+
/// </summary>
70+
/// <param name="assemblies">Assemblies containing the command types</param>
71+
void DiscoverCommands(Assembly[] assemblies);
72+
6073
/// <summary>
6174
/// Configures a new or existing option
6275
/// </summary>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace MatthiWare.CommandLine.Abstractions.Usage
2+
{
3+
/// <summary>
4+
/// Environment variables
5+
/// </summary>
6+
public interface IEnvironmentVariablesService
7+
{
8+
/// <summary>
9+
/// Inidicates if NO_COLOR environment variable has been set
10+
/// https://no-color.org/
11+
/// </summary>
12+
bool NoColorRequested { get; }
13+
}
14+
}

0 commit comments

Comments
 (0)