Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<Project>

<PropertyGroup Label="Target frameworks">
<!-- Shared list for the libraries and their tests. Projects with other needs (e.g. an analyzer on netstandard2.0, the net10-only benchmarks) set their own. -->
<LibraryTargetFrameworks>net8.0;net9.0;net10.0</LibraryTargetFrameworks>
</PropertyGroup>

<PropertyGroup Label="Compiler">
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
</ItemGroup>

<ItemGroup Label="Test">
<PackageVersion Include="Shouldly" Version="4.3.0" />
<PackageVersion Include="xunit.v3" Version="4.0.0" />
</ItemGroup>

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ Breaking changes in 5.0:

## Tests

Unit tests use [xUnit.net](https://github.com/xunit/xunit) v3 on Microsoft.Testing.Platform and run on all three target frameworks. The suite includes a fuzz test that feeds random token sequences to every parser and asserts that user input never causes an exception, only reported errors.
Unit tests use [xUnit.net](https://github.com/xunit/xunit) v3 on Microsoft.Testing.Platform with [Shouldly](https://github.com/shouldly/shouldly) assertions and run on all three target frameworks. The suite includes a fuzz test that feeds random token sequences to every parser and asserts that user input never causes an exception, only reported errors.

```text
dotnet test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFrameworks>$(LibraryTargetFrameworks)</TargetFrameworks>
<Title>Command Line Parser Abstractions</Title>
<Description>Attributes and enums for holonsoft.CmdLineParser. Reference this package from assemblies that only define argument classes.</Description>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
Expand Down
37 changes: 18 additions & 19 deletions holonsoft.CmdLineParser.Tests/ArgumentAttributeTests.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
using System.Globalization;
using System.Reflection;
using holonsoft.CmdLineParser.Abstractions;
using Xunit;

namespace holonsoft.CmdLineParser.Tests;

public class ArgumentAttributeTests {
public sealed class ArgumentAttributeTests {
[Fact]
public void ArgumentTypeIsPreserved() {
Assert.Equal(ArgumentTypes.Required | ArgumentTypes.Multiple, new ArgumentAttribute(ArgumentTypes.Required | ArgumentTypes.Multiple).ArgumentType);
Assert.Equal(ArgumentTypes.Exclusive, new DefaultArgumentAttribute(ArgumentTypes.Exclusive).ArgumentType);
new ArgumentAttribute(ArgumentTypes.Required | ArgumentTypes.Multiple).ArgumentType.ShouldBe(ArgumentTypes.Required | ArgumentTypes.Multiple);
new DefaultArgumentAttribute(ArgumentTypes.Exclusive).ArgumentType.ShouldBe(ArgumentTypes.Exclusive);
}

[Theory]
Expand All @@ -19,7 +18,7 @@ public void ArgumentTypeIsPreserved() {
public void HasNoDefaultShortName(string? shortName, bool expected) {
var attribute = new ArgumentAttribute(ArgumentTypes.AtMostOnce) { ShortName = shortName };

Assert.Equal(expected, attribute.HasNoDefaultShortName);
attribute.HasNoDefaultShortName.ShouldBe(expected);
}

[Theory]
Expand All @@ -29,14 +28,14 @@ public void HasNoDefaultShortName(string? shortName, bool expected) {
public void HasNoDefaultLongName(string? longName, bool expected) {
var attribute = new ArgumentAttribute(ArgumentTypes.AtMostOnce) { LongName = longName };

Assert.Equal(expected, attribute.HasNoDefaultLongName);
attribute.HasNoDefaultLongName.ShouldBe(expected);
}

[Fact]
public void HasDefaultValueOnlyForNonNull() {
Assert.False(new ArgumentAttribute(ArgumentTypes.AtMostOnce).HasDefaultValue);
Assert.True(new ArgumentAttribute(ArgumentTypes.AtMostOnce) { DefaultValue = 0 }.HasDefaultValue);
Assert.True(new ArgumentAttribute(ArgumentTypes.AtMostOnce) { DefaultValue = "" }.HasDefaultValue);
new ArgumentAttribute(ArgumentTypes.AtMostOnce).HasDefaultValue.ShouldBeFalse();
new ArgumentAttribute(ArgumentTypes.AtMostOnce) { DefaultValue = 0 }.HasDefaultValue.ShouldBeTrue();
new ArgumentAttribute(ArgumentTypes.AtMostOnce) { DefaultValue = "" }.HasDefaultValue.ShouldBeTrue();
}

[Theory]
Expand All @@ -47,15 +46,15 @@ public void HasDefaultValueOnlyForNonNull() {
public void HasHelpTextIgnoresWhitespace(string? helpText, bool expected) {
var attribute = new ArgumentAttribute(ArgumentTypes.AtMostOnce) { HelpText = helpText };

Assert.Equal(expected, attribute.HasHelpText);
attribute.HasHelpText.ShouldBe(expected);
}

[Fact]
public void CultureNameResolvesToCultureInfo() {
var attribute = new ArgumentAttribute(ArgumentTypes.AtMostOnce) { Culture = "de-DE" };

Assert.Equal("de-DE", attribute.Culture);
Assert.Equal(CultureInfo.GetCultureInfo("de-DE"), attribute.CultureInfo);
attribute.Culture.ShouldBe("de-DE");
attribute.CultureInfo.ShouldBe(CultureInfo.GetCultureInfo("de-DE"));
}

[Theory]
Expand All @@ -65,28 +64,28 @@ public void CultureNameResolvesToCultureInfo() {
public void EmptyCultureMeansParserDefault(string? culture) {
var attribute = new ArgumentAttribute(ArgumentTypes.AtMostOnce) { Culture = culture };

Assert.Null(attribute.CultureInfo);
attribute.CultureInfo.ShouldBeNull();
}

[Fact]
public void CultureIsNullUntilSet() {
Assert.Null(new ArgumentAttribute(ArgumentTypes.AtMostOnce).CultureInfo);
Assert.Null(new ArgumentAttribute(ArgumentTypes.AtMostOnce).Culture);
new ArgumentAttribute(ArgumentTypes.AtMostOnce).CultureInfo.ShouldBeNull();
new ArgumentAttribute(ArgumentTypes.AtMostOnce).Culture.ShouldBeNull();
}

[Fact]
public void AttributesTargetFieldsAndPropertiesOnce() {
foreach (var type in new[] { typeof(ArgumentAttribute), typeof(DefaultArgumentAttribute) }) {
var usage = type.GetCustomAttribute<AttributeUsageAttribute>()!;

Assert.True(usage.ValidOn.HasFlag(AttributeTargets.Field), type.Name);
Assert.True(usage.ValidOn.HasFlag(AttributeTargets.Property), type.Name);
Assert.False(usage.AllowMultiple, type.Name);
usage.ValidOn.HasFlag(AttributeTargets.Field).ShouldBeTrue(type.Name);
usage.ValidOn.HasFlag(AttributeTargets.Property).ShouldBeTrue(type.Name);
usage.AllowMultiple.ShouldBeFalse(type.Name);
}
}

[Fact]
public void DefaultArgumentIsAnArgument() {
Assert.IsAssignableFrom<ArgumentAttribute>(new DefaultArgumentAttribute(ArgumentTypes.AtMostOnce));
new DefaultArgumentAttribute(ArgumentTypes.AtMostOnce).ShouldBeAssignableTo<ArgumentAttribute>();
}
}
49 changes: 24 additions & 25 deletions holonsoft.CmdLineParser.Tests/ArgumentLexerTests.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
using holonsoft.CmdLineParser.Internal;
using Xunit;

namespace holonsoft.CmdLineParser.Tests;

public class ArgumentLexerTests {
public sealed class ArgumentLexerTests {
private static ArgumentLexer Create(Action<CommandLineParserOptions>? configure = null) {
var options = new CommandLineParserOptions();
configure?.Invoke(options);
Expand All @@ -18,9 +17,9 @@ private static ArgumentLexer Create(Action<CommandLineParserOptions>? configure
public void OptionPrefixes(string raw, string expectedName) {
var token = Create().Classify(raw, afterEndOfOptions: false);

Assert.Equal(LexedTokenKind.Option, token.Kind);
Assert.Equal(expectedName, token.Name);
Assert.Null(token.Value);
token.Kind.ShouldBe(LexedTokenKind.Option);
token.Name.ShouldBe(expectedName);
token.Value.ShouldBeNull();
}

[Theory]
Expand All @@ -33,9 +32,9 @@ public void OptionPrefixes(string raw, string expectedName) {
public void InlineValues(string raw, string expectedName, string expectedValue) {
var token = Create().Classify(raw, afterEndOfOptions: false);

Assert.Equal(LexedTokenKind.Option, token.Kind);
Assert.Equal(expectedName, token.Name);
Assert.Equal(expectedValue, token.Value);
token.Kind.ShouldBe(LexedTokenKind.Option);
token.Name.ShouldBe(expectedName);
token.Value.ShouldBe(expectedValue);
}

[Theory]
Expand All @@ -55,32 +54,32 @@ public void InlineValues(string raw, string expectedName, string expectedValue)
public void Values(string raw, string expectedValue) {
var token = Create().Classify(raw, afterEndOfOptions: false);

Assert.Equal(LexedTokenKind.Value, token.Kind);
Assert.Equal(expectedValue, token.Value);
token.Kind.ShouldBe(LexedTokenKind.Value);
token.Value.ShouldBe(expectedValue);
}

[Fact]
public void EndOfOptionsMarker() {
var lexer = Create();

Assert.Equal(LexedTokenKind.EndOfOptions, lexer.Classify("--", afterEndOfOptions: false).Kind);
lexer.Classify("--", afterEndOfOptions: false).Kind.ShouldBe(LexedTokenKind.EndOfOptions);

var afterwards = lexer.Classify("-looks-like-option", afterEndOfOptions: true);
Assert.Equal(LexedTokenKind.Value, afterwards.Kind);
Assert.Equal("-looks-like-option", afterwards.Value);
afterwards.Kind.ShouldBe(LexedTokenKind.Value);
afterwards.Value.ShouldBe("-looks-like-option");
}

[Fact]
public void EndOfOptionsMarkerCanBeDisabled() {
var token = Create(o => o.RecognizeEndOfOptionsMarker = false).Classify("--", afterEndOfOptions: false);

Assert.Equal(LexedTokenKind.Value, token.Kind);
Assert.Equal("--", token.Value);
token.Kind.ShouldBe(LexedTokenKind.Value);
token.Value.ShouldBe("--");
}

[Fact]
public void SlashPrefixDefaultFollowsTheOperatingSystem() {
Assert.Equal(OperatingSystem.IsWindows(), new CommandLineParserOptions().AllowSlashPrefix);
new CommandLineParserOptions().AllowSlashPrefix.ShouldBe(OperatingSystem.IsWindows());
}

[Theory]
Expand All @@ -91,9 +90,9 @@ public void SlashPrefixDefaultFollowsTheOperatingSystem() {
public void SlashPrefixWhenEnabled(string raw, string expectedName, string? expectedValue) {
var token = Create(o => o.AllowSlashPrefix = true).Classify(raw, afterEndOfOptions: false);

Assert.Equal(LexedTokenKind.Option, token.Kind);
Assert.Equal(expectedName, token.Name);
Assert.Equal(expectedValue, token.Value);
token.Kind.ShouldBe(LexedTokenKind.Option);
token.Name.ShouldBe(expectedName);
token.Value.ShouldBe(expectedValue);
}

[Theory]
Expand All @@ -103,18 +102,18 @@ public void SlashPrefixWhenEnabled(string raw, string expectedName, string? expe
public void SlashPrefixWhenDisabled(string raw) {
var token = Create(o => o.AllowSlashPrefix = false).Classify(raw, afterEndOfOptions: false);

Assert.Equal(LexedTokenKind.Value, token.Kind);
Assert.Equal(raw, token.Value);
token.Kind.ShouldBe(LexedTokenKind.Value);
token.Value.ShouldBe(raw);
}

[Fact]
public void ValueSeparatorsAreConfigurable() {
var colonOnly = Create(o => o.ValueSeparators = [':']).Classify("-name=value", afterEndOfOptions: false);
Assert.Equal("name=value", colonOnly.Name);
Assert.Null(colonOnly.Value);
colonOnly.Name.ShouldBe("name=value");
colonOnly.Value.ShouldBeNull();

var none = Create(o => o.ValueSeparators = []).Classify("-name:value", afterEndOfOptions: false);
Assert.Equal("name:value", none.Name);
none.Name.ShouldBe("name:value");
}

[Theory]
Expand All @@ -128,6 +127,6 @@ public void ValueSeparatorsAreConfigurable() {
[InlineData("--5", false)]
[InlineData("5", false)]
public void NegativeNumberDetection(string raw, bool expected) {
Assert.Equal(expected, ArgumentLexer.LooksLikeNegativeNumber(raw));
ArgumentLexer.LooksLikeNegativeNumber(raw).ShouldBe(expected);
}
}
Loading