From 72938620638e244023f4d67c6ed8b8fc104c058e Mon Sep 17 00:00:00 2001 From: smaillet Date: Thu, 4 Sep 2025 14:41:23 -0700 Subject: [PATCH] Added Unit tests for CommandLine * State of the testing is rather limited due to bugs in the underlying API and design [This library should minimize exposure to the underlying System.CommandLine so that it is easier to replace it with a different parsing technology as there are a LOT of issues/assumptions] --- .../AssemblyInfo.cs | 20 ++++ .../CommandLineTests.cs | 77 +++++++++++++ .../GlobalSuppressions.cs | 12 +++ .../ModuleFixtures.cs | 27 +++++ .../RawApiTests.cs | 101 ++++++++++++++++++ .../SettingsTestExtensions.cs | 21 ++++ .../TestOptions.cs | 10 ++ .../TestOptions.g.cs | 40 +++++++ .../Ubiquity.NET.CommandLine.UT.csproj | 18 ++++ src/Ubiquity.NET.Llvm.slnx | 7 +- 10 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 src/Ubiquity.NET.CommandLine.UT/AssemblyInfo.cs create mode 100644 src/Ubiquity.NET.CommandLine.UT/CommandLineTests.cs create mode 100644 src/Ubiquity.NET.CommandLine.UT/GlobalSuppressions.cs create mode 100644 src/Ubiquity.NET.CommandLine.UT/ModuleFixtures.cs create mode 100644 src/Ubiquity.NET.CommandLine.UT/RawApiTests.cs create mode 100644 src/Ubiquity.NET.CommandLine.UT/SettingsTestExtensions.cs create mode 100644 src/Ubiquity.NET.CommandLine.UT/TestOptions.cs create mode 100644 src/Ubiquity.NET.CommandLine.UT/TestOptions.g.cs create mode 100644 src/Ubiquity.NET.CommandLine.UT/Ubiquity.NET.CommandLine.UT.csproj diff --git a/src/Ubiquity.NET.CommandLine.UT/AssemblyInfo.cs b/src/Ubiquity.NET.CommandLine.UT/AssemblyInfo.cs new file mode 100644 index 000000000..739254e04 --- /dev/null +++ b/src/Ubiquity.NET.CommandLine.UT/AssemblyInfo.cs @@ -0,0 +1,20 @@ +// Copyright (c) Ubiquity.NET Contributors. All rights reserved. +// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information. + +using System; +using System.Runtime.InteropServices; + +// In SDK-style projects such as this one, several assembly attributes that were historically +// defined in this file are now automatically added during build and populated with +// values defined in project properties. For details of which attributes are included +// and how to customize this process see: https://aka.ms/assembly-info-properties + +// Setting ComVisible to false makes the types in this assembly not visible to COM +// components. If you need to access a type in this assembly from COM, set the ComVisible +// attribute to true on that type. +[assembly: ComVisible( false )] + +// The following GUID is for the ID of the typelib if this project is exposed to COM. +[assembly: Guid( "994905c9-5cea-480a-b9fa-1458c5fc04d5" )] + +[assembly: CLSCompliant( false )] diff --git a/src/Ubiquity.NET.CommandLine.UT/CommandLineTests.cs b/src/Ubiquity.NET.CommandLine.UT/CommandLineTests.cs new file mode 100644 index 000000000..b7751a270 --- /dev/null +++ b/src/Ubiquity.NET.CommandLine.UT/CommandLineTests.cs @@ -0,0 +1,77 @@ +// Copyright (c) Ubiquity.NET Contributors. All rights reserved. +// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information. + +using System.CommandLine; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Ubiquity.NET.CommandLine.UT +{ + [TestClass] + public class CommandLineTests + { + [TestMethod] + public void CommandLine_parse_with_version_option_only_succeeds( ) + { + var settings = CreateTestSettings(); + var result = ArgsParsing.Parse(["--version"], settings); + + // due to bug in underlying library this will fail, see: https://github.com/dotnet/command-line-api/issues/2659 + // bug is fixed in PR https://github.com/dotnet/command-line-api/pull/2644 but not available + // yet. (as of version 2.0.0-beta7.25380.108) + //Assert.HasCount( 0, result.Errors ); + Assert.HasCount( 1, result.Errors ); + + var versionOption = result.GetVersionOption(); + Assert.IsNotNull(versionOption); + Assert.AreEqual( versionOption.Action, result.Action); + } + + [TestMethod] + public void CommandLine_with_help_option_only_succeeds( ) + { + var settings = CreateTestSettings(); + + var result = ArgsParsing.Parse(["--help"], settings); + Assert.HasCount( 0, result.Errors ); + } + + [TestMethod] + public void CommandLine_with_unknown_option_has_errors( ) + { + var settings = CreateTestSettings(); + ParseResult result = ArgsParsing.Parse(["--FooBar"], settings ); + Assert.HasCount( 2, result.Errors, "Errors should include missing Required, and invalid param" ); + } + + [TestMethod] + [Ignore("https://github.com/dotnet/command-line-api/issues/2664")] + public void CommandLine_with_known_option_and_version_has_errors( ) + { + var settings = CreateTestSettings(); + ParseResult result = ArgsParsing.Parse(["--version", "--option1"], settings ); + + // until https://github.com/dotnet/command-line-api/issues/2664 is resolved this will fail + Assert.HasCount( 2, result.Errors, "Should be one error (--version must be set alone, missing arg for --option1)" ); + } + + [TestMethod] + [Ignore("https://github.com/dotnet/command-line-api/issues/2664")] + public void CommandLine_with_known_option_requiring_arg_and_version_has_errors( ) + { + var settings = CreateTestSettings(); + ParseResult result = ArgsParsing.Parse(["--option1", "--version"], settings ); + + // until https://github.com/dotnet/command-line-api/issues/2664 is resolved this will fail + Assert.HasCount( 2, result.Errors, "Should be one error (--version must be set alone, missing arg for --option1)" ); + } + + internal static CmdLineSettings CreateTestSettings( DefaultOption defaultOptions = DefaultOption.Help | DefaultOption.Version ) + { + return new CmdLineSettings() + { + DefaultOptions = defaultOptions, + }; + } + } +} diff --git a/src/Ubiquity.NET.CommandLine.UT/GlobalSuppressions.cs b/src/Ubiquity.NET.CommandLine.UT/GlobalSuppressions.cs new file mode 100644 index 000000000..a79c00f1d --- /dev/null +++ b/src/Ubiquity.NET.CommandLine.UT/GlobalSuppressions.cs @@ -0,0 +1,12 @@ +// Copyright (c) Ubiquity.NET Contributors. All rights reserved. +// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information. + +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage( "StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Unit Tests" )] +[assembly: SuppressMessage( "StyleCop.CSharp.DocumentationRules", "SA1652:Enable XML documentation output", Justification = "Unit Tests" )] diff --git a/src/Ubiquity.NET.CommandLine.UT/ModuleFixtures.cs b/src/Ubiquity.NET.CommandLine.UT/ModuleFixtures.cs new file mode 100644 index 000000000..d54a29323 --- /dev/null +++ b/src/Ubiquity.NET.CommandLine.UT/ModuleFixtures.cs @@ -0,0 +1,27 @@ +// Copyright (c) Ubiquity.NET Contributors. All rights reserved. +// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information. + +#if USE_MODULE_FIXTURES +using System; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Ubiquity.NET.CommandLine.UT +{ + // Provides common location for one time initialization for all tests in this assembly + [TestClass] + public static class ModuleFixtures + { + [AssemblyInitialize] + public static void AssemblyInitialize( TestContext ctx ) + { + ArgumentNullException.ThrowIfNull( ctx ); + } + + [AssemblyCleanup] + public static void AssemblyCleanup( ) + { + } + } +} +#endif diff --git a/src/Ubiquity.NET.CommandLine.UT/RawApiTests.cs b/src/Ubiquity.NET.CommandLine.UT/RawApiTests.cs new file mode 100644 index 000000000..18e373bf7 --- /dev/null +++ b/src/Ubiquity.NET.CommandLine.UT/RawApiTests.cs @@ -0,0 +1,101 @@ +// Copyright (c) Ubiquity.NET Contributors. All rights reserved. +// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information. + +using System.CommandLine; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Ubiquity.NET.CommandLine.UT +{ + /// This is mostly for validation.understanding of the underlying RAW API as well as a good place to put "samples" for bug reports + [TestClass] + public class RawApiTests + { + [TestMethod] + [Ignore( "https://github.com/dotnet/command-line-api/issues/2664" )] + public void RawApi_Version_Error_tests( ) + { + var rootCommand = new RootCommand("Test Root") + { + new Option("--option1") + { + Description = "Test option `", + Required = true, + }, + }; + + var result = rootCommand.Parse(["--FooBar", "--version"]); + Assert.HasCount( 3, result.Errors, "Errors should account for, bogus arg (`--FooBar`), missing required arg (`--option1`), AND that `--version` should be solo" ); + } + + [TestMethod] + [Ignore( "https://github.com/dotnet/command-line-api/issues/2664" )] + public void RawApi_Help_Error_tests( ) + { + var rootCommand = new RootCommand("Test Root") + { + new Option("--option1") + { + Description = "Test option `", + Required = true, + }, + }; + + var result = rootCommand.Parse(["--FooBar", "--help"]); + Assert.HasCount( 3, result.Errors, "Errors should account for bogus arg (`--FooBar`), missing required arg (`--option1`), AND that `--version` should be solo" ); + } + + [TestMethod] + [Ignore( "https://github.com/dotnet/command-line-api/issues/2659" )] + public void RawApi_Version_Only_with_required_has_no_errors( ) + { + var rootCommand = new RootCommand("Test Root") + { + new Option("--option1") + { + Description = "Test option `", + Required = true, + }, + }; + + var result = rootCommand.Parse(["--version"]); + Assert.HasCount( 0, result.Errors, "Should not be any errors" ); + } + + [TestMethod] + [Ignore("https://github.com/dotnet/command-line-api/issues/2664")] + public void RawApi_Version_with_required_option_has_errors( ) + { + var rootCommand = new RootCommand("Test Root") + { + new Option("--option1") + { + Description = "Test option `", + Required = true, + }, + }; + + var result = rootCommand.Parse(["--version", "--option1"]); + + // This assert will fail - result.Errors.Count == 3! + // result.Errors: + // [0]{--version option cannot be combined with other arguments.} + // [1]{Required argument missing for option: '--option1'.} + // [2]{Required argument missing for option: '--option1'.} // Why is this occurring twice? + //Assert.HasCount( 2, result.Errors, "Should be two errors (version not used solo, missing arg)" ); + + // try with arguments in reversed order (--version is later) + result = rootCommand.Parse(["--option1", "--version"]); + + // result.Action == null! [BUG] + // result.Errors.Count == 0! [BUG] + Assert.HasCount( 2, result.Errors, "Should be two errors (version not used solo, missing arg)" ); + + result = rootCommand.Parse("--option1 --version"); + + // result.Action == null! [BUG] + // result.Errors.Count == 0! [BUG] + Assert.HasCount( 2, result.Errors, "Should be two errors (version not used solo, missing arg)" ); + } + } +} diff --git a/src/Ubiquity.NET.CommandLine.UT/SettingsTestExtensions.cs b/src/Ubiquity.NET.CommandLine.UT/SettingsTestExtensions.cs new file mode 100644 index 000000000..5b17167ab --- /dev/null +++ b/src/Ubiquity.NET.CommandLine.UT/SettingsTestExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Ubiquity.NET Contributors. All rights reserved. +// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information. + +using System; +using System.Collections.Immutable; +using System.IO; + +namespace Ubiquity.NET.CommandLine.UT +{ + internal static class SettingsTestExtensions + { + public static ImmutableArray GetOutput( this StringWriter self ) + { + ArgumentNullException.ThrowIfNull( self ); + string underlyingString = self.ToString(); + return string.IsNullOrWhiteSpace( underlyingString ) + ? [] + : [ .. underlyingString.Split( self.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries ) ]; + } + } +} diff --git a/src/Ubiquity.NET.CommandLine.UT/TestOptions.cs b/src/Ubiquity.NET.CommandLine.UT/TestOptions.cs new file mode 100644 index 000000000..b87f1c62b --- /dev/null +++ b/src/Ubiquity.NET.CommandLine.UT/TestOptions.cs @@ -0,0 +1,10 @@ +// Copyright (c) Ubiquity.NET Contributors. All rights reserved. +// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information. + +namespace Ubiquity.NET.CommandLine.UT +{ + internal partial class TestOptions + { + public string Option1 { get; init; } = string.Empty; + } +} diff --git a/src/Ubiquity.NET.CommandLine.UT/TestOptions.g.cs b/src/Ubiquity.NET.CommandLine.UT/TestOptions.g.cs new file mode 100644 index 000000000..531e436fb --- /dev/null +++ b/src/Ubiquity.NET.CommandLine.UT/TestOptions.g.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ubiquity.NET.CommandLine.UT +{ + // FUTURE: Generate this with source generator from attributes in other partial declaration + internal partial class TestOptions + : ICommandLineOptions + { + public static TestOptions Bind( ParseResult parseResult ) + { + return new() + { + Option1 = parseResult.GetValue(Descriptors.Option1), + }; + } + + public static AppControlledDefaultsRootCommand BuildRootCommand( CmdLineSettings settings ) + { + return new( settings, "Test option root command") + { + Descriptors.Option1, + }; + } + + internal static class Descriptors + { + internal static Option Option1 + = new("--option1") + { + Description = "Test Option", + Required = true, // Should have no impact on Help/Version + }; + } + } +} diff --git a/src/Ubiquity.NET.CommandLine.UT/Ubiquity.NET.CommandLine.UT.csproj b/src/Ubiquity.NET.CommandLine.UT/Ubiquity.NET.CommandLine.UT.csproj new file mode 100644 index 000000000..0422636c1 --- /dev/null +++ b/src/Ubiquity.NET.CommandLine.UT/Ubiquity.NET.CommandLine.UT.csproj @@ -0,0 +1,18 @@ + + + + net9.0 + false + True + + + + + + + + + + + + diff --git a/src/Ubiquity.NET.Llvm.slnx b/src/Ubiquity.NET.Llvm.slnx index a1ea7ed5f..a5ec26b19 100644 --- a/src/Ubiquity.NET.Llvm.slnx +++ b/src/Ubiquity.NET.Llvm.slnx @@ -64,14 +64,17 @@ + + + + + - -