Skip to content

Commit 35196de

Browse files
authored
More testing, minor usability improvements (#44)
1 parent 69093b5 commit 35196de

14 files changed

+234
-40
lines changed

src/NClap/Help/TwoColumnArgumentHelpLayout.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ private TwoColumnArgumentHelpLayout(TwoColumnArgumentHelpLayout other) : base(ot
3131
/// <summary>
3232
/// Optional maximum widths of columns; null indicates no preference.
3333
/// </summary>
34-
public int?[] ColumnWidths = new int?[2] { null, null };
34+
public int?[] ColumnWidths { get; } = new int?[2] { null, null };
3535

3636
/// <summary>
3737
/// Optionally specifies separator string to be used between columns,

src/NClap/Metadata/ArgumentBaseAttribute.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,14 @@ public string[] ConflictsWith
117117
/// </summary>
118118
public bool AllowEmpty { get; set; }
119119

120+
/// <summary>
121+
/// Optionally specifies the strings that may be used as element
122+
/// separators for multiple elements that have been expressed in the
123+
/// same token. Only relevant for parsing collection types, and
124+
/// ignored otherwise.
125+
/// </summary>
126+
public string[] ElementSeparators { get; set; } = new[] { "," };
127+
120128
/// <summary>
121129
/// Optionally provides a type that implements IStringParser, and which
122130
/// should be used for parsing strings for this argument instead of the

src/NClap/NClap.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<Description>NClap is a .NET library for parsing command-line arguments and building interactive command shells. It's driven by a declarative attribute syntax, and easy to extend.</Description>
2222
<Company>Microsoft Corporation</Company>
2323
<Copyright>Copyright © Microsoft Corporation. All right reserved.</Copyright>
24-
<Version>2.0.1</Version>
24+
<Version>2.1.0</Version>
2525
<AssemblyVersion>$(Version)</AssemblyVersion>
2626
<FileVersion>$(Version)</FileVersion>
2727
<PackageVersion>$(Version)</PackageVersion>

src/NClap/Parser/ArgumentParser.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ private static ArgumentParseContext CreateParseContext(ArgumentDefinition argume
365365
new ArgumentParseContext
366366
{
367367
NumberOptions = argument.Attribute.NumberOptions,
368+
ElementSeparators = argument.Attribute.ElementSeparators,
368369
AllowEmpty = argument.Attribute.AllowEmpty,
369370
FileSystemReader = options.FileSystemReader,
370371
ParserContext = options.Context,

src/NClap/Repl/Loop.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,7 @@ public IEnumerable<string> GetCompletions(IEnumerable<string> tokens, int tokenI
4242
/// for the argument set that will be dynamically created for this loop.</param>
4343
public Loop(Type commandType, ILoopClient loopClient, ArgumentSetAttribute argSetAttribute = null)
4444
{
45-
if (commandType == null)
46-
{
47-
throw new ArgumentNullException(nameof(commandType));
48-
}
45+
if (commandType == null) throw new ArgumentNullException(nameof(commandType));
4946

5047
_client = loopClient ?? throw new ArgumentNullException(nameof(loopClient));
5148
_client.TokenCompleter = new TokenCompleter(this);
@@ -219,7 +216,7 @@ private static Type ConstructCommandType(Type inputType, out Func<object> factor
219216
}
220217

221218
// See if it is an enum; if so, use it as the inner type for a command group.
222-
if (inputType.GetTypeInfo().IsEnum)
219+
else if (inputType.GetTypeInfo().IsEnum)
223220
{
224221
loopType = typeof(CommandGroup<>).MakeGenericType(new[] { inputType });
225222
}

src/NClap/Types/ArgumentParseContext.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
using NClap.Metadata;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
25

36
namespace NClap.Types
47
{
@@ -8,6 +11,7 @@ namespace NClap.Types
811
public class ArgumentParseContext
912
{
1013
private IFileSystemReader _fileSystemReader = Parser.FileSystemReader.Create();
14+
private IReadOnlyList<string> _elementSeparators = new List<string> { "," };
1115

1216
/// <summary>
1317
/// The default context for parsing.
@@ -35,18 +39,31 @@ public IFileSystemReader FileSystemReader
3539
/// <summary>
3640
/// Options for parsing numeric arguments.
3741
/// </summary>
38-
public NumberOptions NumberOptions { get; set; }
42+
public NumberOptions NumberOptions { get; set; } = NumberOptions.None;
3943

4044
/// <summary>
4145
/// True to allow "empty" arguments (e.g. empty strings); false to
4246
/// consider them invalid.
4347
/// </summary>
44-
public bool AllowEmpty { get; set; }
48+
public bool AllowEmpty { get; set; } = false;
4549

4650
/// <summary>
4751
/// True for parsing to be case sensitive; false to be case insensitive.
4852
/// </summary>
49-
public bool CaseSensitive { get; set; }
53+
public bool CaseSensitive { get; set; } = false;
54+
55+
/// <summary>
56+
/// Strings that may separate multiple elements stored in the same token.
57+
/// </summary>
58+
public IReadOnlyList<string> ElementSeparators
59+
{
60+
get => _elementSeparators;
61+
set
62+
{
63+
if (value == null) _elementSeparators = new string[] { };
64+
_elementSeparators = value.ToList();
65+
}
66+
}
5067

5168
/// <summary>
5269
/// Optionally provides a reference to the object containing the one to

src/NClap/Types/CollectionArgumentTypeBase.cs

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ namespace NClap.Types
1313
/// </summary>
1414
internal abstract class CollectionArgumentTypeBase : ArgumentTypeBase, ICollectionArgumentType
1515
{
16-
private const char ElementSeparatorChar = ',';
17-
1816
private readonly IArgumentType _elementArgumentType;
1917

2018
/// <summary>
@@ -73,16 +71,29 @@ public override IEnumerable<string> GetCompletions(ArgumentCompletionContext con
7371
throw new ArgumentNullException(nameof(valueToComplete));
7472
}
7573

76-
var tokens = valueToComplete.Split(ElementSeparatorChar);
74+
string[] tokens;
75+
if (context.ParseContext.ElementSeparators.Any())
76+
{
77+
tokens = valueToComplete.Split(
78+
context.ParseContext.ElementSeparators.ToArray(),
79+
StringSplitOptions.None);
80+
}
81+
else
82+
{
83+
tokens = new[] { valueToComplete };
84+
}
85+
7786
Debug.Assert(tokens.Length >= 1);
7887

7988
var currentTokenIndex = tokens.Length - 1;
8089
var currentToken = tokens[currentTokenIndex];
8190

8291
tokens[currentTokenIndex] = string.Empty;
8392

93+
var preferredSeparator = GetPreferredElementSeparatorOrDefault(context.ParseContext) ?? string.Empty;
94+
8495
return _elementArgumentType.GetCompletions(context, currentToken)
85-
.Select(completion => string.Join(ElementSeparatorChar.ToString(), tokens) + completion);
96+
.Select(completion => string.Join(preferredSeparator, tokens) + completion);
8697
}
8798

8899
/// <summary>
@@ -109,7 +120,17 @@ public override IEnumerable<string> GetCompletions(ArgumentCompletionContext con
109120
/// <returns>The parsed object.</returns>
110121
protected override object Parse(ArgumentParseContext context, string stringToParse)
111122
{
112-
var elementStrings = stringToParse.Split(ElementSeparatorChar);
123+
string[] elementStrings;
124+
if (context.ElementSeparators.Any())
125+
{
126+
elementStrings = stringToParse.Split(
127+
context.ElementSeparators.ToArray(),
128+
StringSplitOptions.None);
129+
}
130+
else
131+
{
132+
elementStrings = new[] { stringToParse };
133+
}
113134

114135
var parsedElements = elementStrings.Select(elementString =>
115136
{
@@ -136,5 +157,14 @@ protected override object Parse(ArgumentParseContext context, string stringToPar
136157
/// <param name="collection">Collection to enumerate.</param>
137158
/// <returns>The enumerated objects.</returns>
138159
protected abstract IEnumerable<object> GetElements(object collection);
160+
161+
/// <summary>
162+
/// Given a parser context, returns the preferred element separator. Returns null
163+
/// if no such separator exists.
164+
/// </summary>
165+
/// <param name="context">Parser context.</param>
166+
/// <returns>Preferred element separator, if one exists; null otherwise.</returns>
167+
private static string GetPreferredElementSeparatorOrDefault(ArgumentParseContext context) =>
168+
context.ElementSeparators.FirstOrDefault();
139169
}
140170
}
Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using System.Linq;
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
24
using System.Reflection;
35
using FluentAssertions;
46
using FluentAssertions.Types;
@@ -9,15 +11,17 @@ namespace NClap.Tests
911
[TestClass]
1012
public class AssemblyTests
1113
{
12-
public static readonly Assembly AssemblyUnderTest = typeof(CommandLineParser).GetTypeInfo().Assembly;
14+
public static readonly Type RepresentativeType = typeof(CommandLineParser);
15+
16+
public static readonly Assembly AssemblyUnderTest = RepresentativeType.GetTypeInfo().Assembly;
1317

1418
[TestMethod]
15-
public void VerifyNamespace()
19+
public void TestThatAllPublicTypesInAssemblyStartWithCorrectNamespacePrefix()
1620
{
1721
const string expectedNs = nameof(NClap);
1822
const string expectedNsWithDot = expectedNs + ".";
1923

20-
foreach (var type in AllTypes.From(AssemblyUnderTest).Where(t => t.GetTypeInfo().IsPublic))
24+
foreach (var type in AllPublicTypes())
2125
{
2226
var ns = type.Namespace;
2327
if (ns != expectedNs)
@@ -26,5 +30,29 @@ public void VerifyNamespace()
2630
}
2731
}
2832
}
33+
34+
[TestMethod]
35+
public void TestThatNoPublicTypeInAssemblyContainsNonPrivateField()
36+
{
37+
var noNonPrivateFields = true;
38+
foreach (var type in AllPublicTypes().Where(t => !t.IsEnum))
39+
{
40+
foreach (var member in type.GetTypeInfo().GetFields().Where(f => !IsConstant(f)))
41+
{
42+
if (!member.IsPrivate)
43+
{
44+
Console.Error.WriteLine($"Found non-private field in public type: {type.FullName} :: {member.Name}");
45+
noNonPrivateFields = false;
46+
}
47+
}
48+
}
49+
50+
noNonPrivateFields.Should().BeTrue();
51+
}
52+
53+
private static bool IsConstant(FieldInfo field) =>
54+
field.IsLiteral && !field.IsInitOnly;
55+
56+
private static IEnumerable<Type> AllPublicTypes() => AllTypes.From(AssemblyUnderTest).Where(t => t.GetTypeInfo().IsPublic);
2957
}
3058
}

src/Tests/UnitTests/Exceptions/InvalidArgumentSetExceptionTests.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using NClap.Exceptions;
44
using NClap.Metadata;
55
using NClap.Tests.Metadata;
6+
using System;
67

78
namespace NClap.Tests.Exceptions
89
{
@@ -32,5 +33,15 @@ public void ArgumentConstructor()
3233
exn.InnerMessage.Should().Be(innerMessage);
3334
exn.Message.Should().Contain(innerMessage);
3435
}
36+
37+
[TestMethod]
38+
public void TestThatProvidedInnerExceptionIsAccessibleInConstructedObject()
39+
{
40+
const string anyMessage = "Some message";
41+
var innerExn = new NotImplementedException();
42+
var exn = new InvalidArgumentSetException(anyMessage, innerExn);
43+
exn.InnerException.Should().BeSameAs(innerExn);
44+
exn.InnerMessage.Should().Be(anyMessage);
45+
}
3546
}
3647
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using FluentAssertions;
2+
using Microsoft.VisualStudio.TestTools.UnitTesting;
3+
using NClap.Exceptions;
4+
5+
namespace NClap.Tests.Exceptions
6+
{
7+
[TestClass]
8+
public class InvalidCommandExceptionTests
9+
{
10+
[TestMethod]
11+
public void TestThatInnerMessageIsEmbeddedInConstructedObject()
12+
{
13+
const string anyMessage = "Some message";
14+
var exn = new InvalidCommandException(anyMessage);
15+
exn.Message.Should().Be(anyMessage);
16+
}
17+
}
18+
}

0 commit comments

Comments
 (0)