diff --git a/.claude/skills/prepare-release/SKILL.md b/.claude/skills/prepare-release/SKILL.md index d22fa5f2..cd24b4fc 100644 --- a/.claude/skills/prepare-release/SKILL.md +++ b/.claude/skills/prepare-release/SKILL.md @@ -23,16 +23,20 @@ Run git commands to analyze commits since last release: - `git log --grep` for PR merges - Look for conventional commit patterns: `fix:`, `feat:`, `break:`, `docs:`, etc. -### 3. Categorize Changes +### 3. Filter and Categorize Changes -Group into categories (in this order): +**Exclude from release notes:** +- Dependabot / automated dependency bump commits (e.g., `chore(deps): Bump ...`). These are routine maintenance and not user-facing. +- CI/tooling-only changes (e.g., updating GitHub Actions workflows, claude workflows) unless they affect the shipped package. + +Group remaining changes into categories (in this order): 1. **Breaking changes** (major versions only) - API removals, behavior changes 2. **Features** - New functionality or APIs 3. **Fixes** - Bug fixes and corrections 4. **Improvements** - Performance or usability enhancements 5. **Docs** - Documentation-only changes -6. **Other** - Infrastructure, tooling, CI/CD +6. **Other** - Infrastructure, tooling, CI/CD (only if user-facing or noteworthy) ### 4. Generate releasenotes.props Entry @@ -57,15 +61,31 @@ Fixes: ``` **Patch versions:** + +Append the patch notes directly at the end of the existing parent version's `StartsWith('X.Y.')` block. Do NOT create a separate conditional block. Add the patch section just before the closing `` tag of the parent version: + ```xml - -$(PackageReleaseNotes) + +...existing X.Y.0 release notes... -X.Y.Z patch: +Updates in X.Y.Z patch: * @user: fix description (#123) ``` +For multiple patches, append each one in order at the end of the same block: + +```xml +...existing X.Y.0 release notes... + +Updates in X.Y.1 patch: +* @user: fix description (#123) + +Updates in X.Y.2 patch: +* @user: another fix (#456) + +``` + ### 5. Generate CHANGELOG.md Entry **Format:** `* [@contributor]: description ([#PR])` @@ -196,10 +216,7 @@ See https://natemcmaster.github.io/CommandLineUtils/vX.0/upgrade-guide.html ### Patch Versions -Use `$(PackageReleaseNotes)` to inherit parent version's notes. - -For first patch (X.Y.1), create new conditional entry after parent. -For subsequent patches, add BEFORE existing patches but AFTER minor version. +Append patch notes directly into the existing parent version's `StartsWith('X.Y.')` block in `releasenotes.props`. Do NOT create a separate conditional block or use `$(PackageReleaseNotes)` inheritance. Each patch gets an "Updates in X.Y.Z patch:" section appended at the end of the parent block. ## Quality Checklist diff --git a/CHANGELOG.md b/CHANGELOG.md index a4ca9a14..5c428df7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [v5.0.1](https://github.com/natemcmaster/CommandLineUtils/compare/v5.0.0...v5.0.1) + +### Features +* [@sensslen]: Restore target framework compilation for .NET Framework ([#591]) + +[#591]: https://github.com/natemcmaster/CommandLineUtils/pull/591 + ## [v5.0.0](https://github.com/natemcmaster/CommandLineUtils/compare/v4.1.1...v5.0.0) ### Breaking changes diff --git a/Directory.Build.props b/Directory.Build.props index 2e972097..aa9c9601 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -31,6 +31,8 @@ $(WarningsNotAsErrors);1591 true enable + + annotations $(MSBuildThisFileDirectory)src\StrongName.snk true @@ -41,7 +43,7 @@ - 5.0.0 + 5.0.1 beta true $(GITHUB_RUN_NUMBER) @@ -53,6 +55,11 @@ $(PackageVersion)+$(RepositoryCommit) + + + + + diff --git a/README.md b/README.md index 0e5061a8..bab675ad 100644 --- a/README.md +++ b/README.md @@ -138,3 +138,15 @@ If you need help with this project, please ... This is a fork of [Microsoft.Extensions.CommandLineUtils](https://github.com/aspnet/Common), which was [completely abandoned by Microsoft](https://github.com/aspnet/Common/issues/257). This project [forked in 2017](https://github.com/natemcmaster/CommandLineUtils/commit/f039360e4e51bbf8b8eb6236894b626ec7944cec) and continued to make improvements. From 2017 to 2021, over 30 contributors added new features and fixed bugs. As of 2022, the project has entered maintenance mode, so no major changes are planned. [See this issue for details on latest project status.](https://github.com/natemcmaster/CommandLineUtils/issues/485) This project is not abandoned -- I believe this library provides a stable API and rich feature set good enough for most developers to create command line apps in .NET -- but only the most critical of bugs will be fixed (such as security issues). +## Supported .NET Versions + +Framework | Version | Reason +---------------|---------|-------------------- +`dotnet` | 8.0 | Lowest Microsoft LTS version at time of release. See +.NET Framework | 4.7.2 | Lowest .NET Framework version fully compatible with [.NET Standard 2.0][netstandard-guidance] + +_Why not directly compile for .NET Standard?_ + +Microsoft guidance says ".NET 5 and later versions adopt a different approach to establishing uniformity that eliminates the need for .NET Standard in most scenarios." Compiling for 2 frameworks appears to be sufficient, so we avoid added complexity. + +[netstandard-guidance]: https://learn.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0 diff --git a/build.ps1 b/build.ps1 index 989f4708..a0b26fc9 100755 --- a/build.ps1 +++ b/build.ps1 @@ -32,6 +32,14 @@ exec dotnet tool run dotnet-format -- -v detailed @formatArgs "$PSScriptRoot/doc exec dotnet build --configuration $Configuration '-warnaserror:CS1591' exec dotnet pack --no-build --configuration $Configuration -o $artifacts exec dotnet build --configuration $Configuration "$PSScriptRoot/docs/samples/samples.sln" -exec dotnet test --no-build --configuration $Configuration --collect:"XPlat Code Coverage" + +[string[]] $testArgs = @() +if (-not $IsWindows) { + $testArgs += '-p:TestFullFramework=false' +} + +exec dotnet test --no-build --configuration $Configuration ` + --collect:"XPlat Code Coverage" ` + @testArgs write-host -f green 'BUILD SUCCEEDED' diff --git a/src/CommandLineUtils.Generators/McMaster.Extensions.CommandLineUtils.Generators.csproj b/src/CommandLineUtils.Generators/McMaster.Extensions.CommandLineUtils.Generators.csproj index cfd7d030..cc21eccc 100644 --- a/src/CommandLineUtils.Generators/McMaster.Extensions.CommandLineUtils.Generators.csproj +++ b/src/CommandLineUtils.Generators/McMaster.Extensions.CommandLineUtils.Generators.csproj @@ -2,7 +2,6 @@ netstandard2.0 - latest enable true true diff --git a/src/CommandLineUtils/Conventions/SubcommandAttributeConvention.cs b/src/CommandLineUtils/Conventions/SubcommandAttributeConvention.cs index f615fc1a..4c2e5839 100644 --- a/src/CommandLineUtils/Conventions/SubcommandAttributeConvention.cs +++ b/src/CommandLineUtils/Conventions/SubcommandAttributeConvention.cs @@ -58,7 +58,13 @@ private static string GetSubcommandName(Type subcommandType, ICommandMetadataPro private void AddSubcommandFromMetadata( ConventionContext context, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type subcommandType, +#if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] +#elif NET472_OR_GREATER +#else +#error Target framework misconfiguration +#endif + Type subcommandType, ICommandMetadataProvider provider, string name) { diff --git a/src/CommandLineUtils/IO/Pager.cs b/src/CommandLineUtils/IO/Pager.cs index 31559ec4..a1a0307a 100644 --- a/src/CommandLineUtils/IO/Pager.cs +++ b/src/CommandLineUtils/IO/Pager.cs @@ -122,8 +122,11 @@ public void Kill() FileName = "less", Arguments = ArgumentEscaper.EscapeAndConcatenate(args), RedirectStandardInput = true, -#if NET46_OR_GREATER +#if NET472_OR_GREATER UseShellExecute = false, +#elif NET6_0_OR_GREATER +#else +#error Target framework misconfiguration #endif } }; diff --git a/src/CommandLineUtils/Internal/DictionaryExtensions.cs b/src/CommandLineUtils/Internal/DictionaryExtensions.cs new file mode 100644 index 00000000..5918f826 --- /dev/null +++ b/src/CommandLineUtils/Internal/DictionaryExtensions.cs @@ -0,0 +1,25 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace McMaster.Extensions.CommandLineUtils +{ + internal static class DictionaryExtensions + { +#if NET6_0_OR_GREATER +#elif NET472_OR_GREATER + public static bool TryAdd(this IDictionary dictionary, TKey key, TValue value) + { + if (dictionary.ContainsKey(key)) + { + return false; + } + dictionary.Add(key, value); + return true; + } +#else +#error Target framework misconfiguration +#endif + } +} diff --git a/src/CommandLineUtils/Internal/ReflectionHelper.cs b/src/CommandLineUtils/Internal/ReflectionHelper.cs index 3567560a..2f9cdef5 100644 --- a/src/CommandLineUtils/Internal/ReflectionHelper.cs +++ b/src/CommandLineUtils/Internal/ReflectionHelper.cs @@ -188,12 +188,41 @@ public bool Equals(MethodInfo? x, MethodInfo? y) return true; } - return x != null && y != null && x.HasSameMetadataDefinitionAs(y); + if (x == null || y == null) + { + return false; + } + +#if NET6_0_OR_GREATER + return x.HasSameMetadataDefinitionAs(y); +#elif NET472_OR_GREATER + return x.MetadataToken == y.MetadataToken && x.Module.Equals(y.Module); +#else +#error Target framework misconfiguration +#endif } public int GetHashCode(MethodInfo obj) { +#if NET6_0_OR_GREATER return obj.HasMetadataToken() ? obj.GetMetadataToken().GetHashCode() : 0; +#elif NET472_OR_GREATER + // see https://github.com/dotnet/dotnet/blob/b0f34d51fccc69fd334253924abd8d6853fad7aa/src/runtime/src/libraries/System.Reflection.TypeExtensions/src/System/Reflection/TypeExtensions.cs#L496 + int token = obj.MetadataToken; + + // Tokens have MSB = table index, 3 LSBs = row index + // row index of 0 is a nil token + const int rowMask = 0x00FFFFFF; + if ((token & rowMask) == 0) + { + // Nil token is returned for edge cases like typeof(byte[]).MetadataToken. + return 0; + } + + return token; +#else +#error Target framework misconfiguration +#endif } } diff --git a/src/CommandLineUtils/McMaster.Extensions.CommandLineUtils.csproj b/src/CommandLineUtils/McMaster.Extensions.CommandLineUtils.csproj index ac9b2874..236eb6f2 100644 --- a/src/CommandLineUtils/McMaster.Extensions.CommandLineUtils.csproj +++ b/src/CommandLineUtils/McMaster.Extensions.CommandLineUtils.csproj @@ -1,7 +1,7 @@ - + - net8.0 + net8.0;net472 true true Command-line parsing API. @@ -30,6 +30,10 @@ McMaster.Extensions.CommandLineUtils.ArgumentEscaper + + + + - diff --git a/src/CommandLineUtils/Properties/NullabilityHelpers.cs b/src/CommandLineUtils/Properties/NullabilityHelpers.cs deleted file mode 100644 index 2ba73d2a..00000000 --- a/src/CommandLineUtils/Properties/NullabilityHelpers.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Nate McMaster. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -// Files here are for simplify annotations of nullable code and are not functional in .NET Standard 2.0 -#if NETSTANDARD2_0 || NET46_OR_GREATER -namespace System.Diagnostics.CodeAnalysis -{ - // https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.notnullwhenattribute? - [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] - internal sealed class NotNullWhenAttribute : Attribute - { - public NotNullWhenAttribute(bool returnValue) - { - } - } - - // https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.allownullattribute - [System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Parameter | System.AttributeTargets.Property, Inherited=false)] - internal sealed class AllowNullAttribute : Attribute { } -} -#endif diff --git a/src/CommandLineUtils/SourceGeneration/ActivatorModelFactory.cs b/src/CommandLineUtils/SourceGeneration/ActivatorModelFactory.cs index 2ec3c8d9..b9967031 100644 --- a/src/CommandLineUtils/SourceGeneration/ActivatorModelFactory.cs +++ b/src/CommandLineUtils/SourceGeneration/ActivatorModelFactory.cs @@ -11,7 +11,12 @@ namespace McMaster.Extensions.CommandLineUtils.SourceGeneration /// /// Model factory that uses Activator.CreateInstance or DI with constructor injection. /// +#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Uses Activator.CreateInstance or DI with constructor injection")] +#elif NET472_OR_GREATER +#else +#error Target framework misconfiguration +#endif internal sealed class ActivatorModelFactory : IModelFactory { private readonly Type _modelType; diff --git a/src/CommandLineUtils/SourceGeneration/DefaultMetadataResolver.cs b/src/CommandLineUtils/SourceGeneration/DefaultMetadataResolver.cs index 302a4a4d..1e6f95d9 100644 --- a/src/CommandLineUtils/SourceGeneration/DefaultMetadataResolver.cs +++ b/src/CommandLineUtils/SourceGeneration/DefaultMetadataResolver.cs @@ -30,7 +30,12 @@ private DefaultMetadataResolver() /// For full AOT compatibility, ensure the CommandLineUtils.Generators package is referenced /// and the source generator runs during compilation. /// +#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Falls back to reflection when no generated metadata is available. Use the source generator for AOT compatibility.")] +#elif NET472_OR_GREATER +#else +#error Target framework misconfiguration +#endif public ICommandMetadataProvider GetProvider(Type modelType) { // Check for generated metadata first (AOT-safe path) @@ -50,7 +55,12 @@ public ICommandMetadataProvider GetProvider(Type modelType) /// For full AOT compatibility, ensure the CommandLineUtils.Generators package is referenced /// and the source generator runs during compilation. /// +#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Falls back to reflection when no generated metadata is available. Use the source generator for AOT compatibility.")] +#elif NET472_OR_GREATER +#else +#error Target framework misconfiguration +#endif public ICommandMetadataProvider GetProvider() where TModel : class { // Check for generated metadata first (AOT-safe path) @@ -78,7 +88,12 @@ public bool HasGeneratedMetadata(Type modelType) return CommandMetadataRegistry.HasMetadata(modelType); } +#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Uses reflection to analyze the model type")] +#elif NET472_OR_GREATER +#else +#error Target framework misconfiguration +#endif private static ICommandMetadataProvider CreateReflectionProvider(Type modelType) { // This creates a reflection-based implementation of ICommandMetadataProvider diff --git a/src/CommandLineUtils/SourceGeneration/ReflectionExecuteHandler.cs b/src/CommandLineUtils/SourceGeneration/ReflectionExecuteHandler.cs index 7ed1237c..f3e26c38 100644 --- a/src/CommandLineUtils/SourceGeneration/ReflectionExecuteHandler.cs +++ b/src/CommandLineUtils/SourceGeneration/ReflectionExecuteHandler.cs @@ -12,7 +12,12 @@ namespace McMaster.Extensions.CommandLineUtils.SourceGeneration /// /// Execute handler that uses reflection to invoke OnExecute/OnExecuteAsync. /// +#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Uses reflection to invoke method")] +#elif NET472_OR_GREATER +#else +#error Target framework misconfiguration +#endif internal sealed class ReflectionExecuteHandler : IExecuteHandler { private readonly MethodInfo _method; diff --git a/src/CommandLineUtils/SourceGeneration/ReflectionMetadataProvider.cs b/src/CommandLineUtils/SourceGeneration/ReflectionMetadataProvider.cs index e5b26f7d..3659f241 100644 --- a/src/CommandLineUtils/SourceGeneration/ReflectionMetadataProvider.cs +++ b/src/CommandLineUtils/SourceGeneration/ReflectionMetadataProvider.cs @@ -15,7 +15,12 @@ namespace McMaster.Extensions.CommandLineUtils.SourceGeneration /// Provides command metadata by analyzing a type using reflection. /// This is the fallback when generated metadata is not available. /// +#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Uses reflection to analyze the model type")] +#elif NET472_OR_GREATER +#else +#error Target framework misconfiguration +#endif internal sealed class ReflectionMetadataProvider : ICommandMetadataProvider { private const BindingFlags MethodLookup = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public; diff --git a/src/CommandLineUtils/SourceGeneration/ReflectionValidateHandler.cs b/src/CommandLineUtils/SourceGeneration/ReflectionValidateHandler.cs index bb473e9f..3ead1679 100644 --- a/src/CommandLineUtils/SourceGeneration/ReflectionValidateHandler.cs +++ b/src/CommandLineUtils/SourceGeneration/ReflectionValidateHandler.cs @@ -12,7 +12,12 @@ namespace McMaster.Extensions.CommandLineUtils.SourceGeneration /// /// Validate handler that uses reflection to invoke OnValidate. /// +#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Uses reflection to invoke method")] +#elif NET472_OR_GREATER +#else +#error Target framework misconfiguration +#endif internal sealed class ReflectionValidateHandler : IValidateHandler { private readonly MethodInfo _method; diff --git a/src/CommandLineUtils/SourceGeneration/ReflectionValidationErrorHandler.cs b/src/CommandLineUtils/SourceGeneration/ReflectionValidationErrorHandler.cs index 0619433f..68dbf57f 100644 --- a/src/CommandLineUtils/SourceGeneration/ReflectionValidationErrorHandler.cs +++ b/src/CommandLineUtils/SourceGeneration/ReflectionValidationErrorHandler.cs @@ -11,7 +11,12 @@ namespace McMaster.Extensions.CommandLineUtils.SourceGeneration /// /// Validation error handler that uses reflection to invoke OnValidationError. /// +#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Uses reflection to invoke method")] +#elif NET472_OR_GREATER +#else +#error Target framework misconfiguration +#endif internal sealed class ReflectionValidationErrorHandler : IValidationErrorHandler { private readonly MethodInfo _method; diff --git a/src/CommandLineUtils/releasenotes.props b/src/CommandLineUtils/releasenotes.props index 0e5f7489..b857d9fc 100644 --- a/src/CommandLineUtils/releasenotes.props +++ b/src/CommandLineUtils/releasenotes.props @@ -26,6 +26,9 @@ Other: * @natemcmaster: Use NuGet trusted publishing with OIDC * @dependabot: Update GitHub Actions (#568) * @natemcmaster: Upgrade docfx to 2.78.4 + +Updates in 5.0.1 patch: +* @sensslen: Restore target framework compilation for .NET Framework (#591) Changes since 4.0: diff --git a/src/Hosting.CommandLine/McMaster.Extensions.Hosting.CommandLine.csproj b/src/Hosting.CommandLine/McMaster.Extensions.Hosting.CommandLine.csproj index 42eafd8a..ec78fa0d 100644 --- a/src/Hosting.CommandLine/McMaster.Extensions.Hosting.CommandLine.csproj +++ b/src/Hosting.CommandLine/McMaster.Extensions.Hosting.CommandLine.csproj @@ -1,7 +1,7 @@  - net8.0 + net8.0;net472 true true Provides command-line parsing API integration with the generic host API (Microsoft.Extensions.Hosting). diff --git a/test/.runsettings b/test/.runsettings new file mode 100644 index 00000000..29e4ea44 --- /dev/null +++ b/test/.runsettings @@ -0,0 +1,11 @@ + + + + + + True + + diff --git a/test/CommandLineUtils.Tests/AppNameFromEntryAssemblyConventionTests.cs b/test/CommandLineUtils.Tests/AppNameFromEntryAssemblyConventionTests.cs index a3e49d34..77b7d19d 100644 --- a/test/CommandLineUtils.Tests/AppNameFromEntryAssemblyConventionTests.cs +++ b/test/CommandLineUtils.Tests/AppNameFromEntryAssemblyConventionTests.cs @@ -16,7 +16,7 @@ public void ItSetsAppNameToEntryAssemblyIfNotSpecified() return; } - var expected = Assembly.GetEntryAssembly().GetName().Name; + var expected = Assembly.GetEntryAssembly()?.GetName().Name; var app = new CommandLineApplication(); app.Conventions.SetAppNameFromEntryAssembly(); Assert.Equal(expected, app.Name); diff --git a/test/CommandLineUtils.Tests/ArgumentAttributeTests.cs b/test/CommandLineUtils.Tests/ArgumentAttributeTests.cs index 7bf5aa87..71926578 100644 --- a/test/CommandLineUtils.Tests/ArgumentAttributeTests.cs +++ b/test/CommandLineUtils.Tests/ArgumentAttributeTests.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Linq; +using System.Reflection; using Xunit; using Xunit.Abstractions; @@ -43,8 +43,8 @@ public void ThrowsWhenDuplicateArgumentPositionsAreSpecified() Assert.Equal( Strings.DuplicateArgumentPosition( 0, - typeof(DuplicateArguments).GetProperty("AlsoFirst"), - typeof(DuplicateArguments).GetProperty("First")), + Assert.IsAssignableFrom(typeof(DuplicateArguments).GetProperty("AlsoFirst")), + Assert.IsAssignableFrom(typeof(DuplicateArguments).GetProperty("First"))), ex.Message); } diff --git a/test/CommandLineUtils.Tests/AttributeValidatorTests.cs b/test/CommandLineUtils.Tests/AttributeValidatorTests.cs index 2cfad77f..99febd92 100644 --- a/test/CommandLineUtils.Tests/AttributeValidatorTests.cs +++ b/test/CommandLineUtils.Tests/AttributeValidatorTests.cs @@ -39,7 +39,7 @@ public void ItOnlyInvokesAttributeIfValueExists() [InlineData(typeof(PhoneAttribute), "(800) 555-5555", "xyz")] public void ItExecutesValidationAttribute(Type attributeType, string validValue, string invalidValue) { - var attr = (ValidationAttribute)Activator.CreateInstance(attributeType); + var attr = Assert.IsAssignableFrom(Activator.CreateInstance(attributeType)); var app = new CommandLineApplication(); var arg = app.Argument("arg", "arg"); var validator = new AttributeValidator(attr); @@ -53,7 +53,7 @@ public void ItExecutesValidationAttribute(Type attributeType, string validValue, arg.Reset(); arg.TryParse(invalidValue); var result = validator.GetValidationResult(arg, context); - Assert.NotNull(result); + Assert.NotNull(result?.ErrorMessage); Assert.NotEmpty(result.ErrorMessage); } @@ -61,7 +61,7 @@ public void ItExecutesValidationAttribute(Type attributeType, string validValue, [InlineData(typeof(ClassLevelValidationAttribute), "good", "also good", "bad", "also bad")] public void ItExecutesClassLevelValidationAttribute(Type attributeType, string validProp1Value, string validProp2Value, string invalidProp1Value, string invalidProp2Value) { - var attr = (ValidationAttribute)Activator.CreateInstance(attributeType); + var attr = Assert.IsAssignableFrom(Activator.CreateInstance(attributeType)); var app = new CommandLineApplication(); var validator = new AttributeValidator(attr); var factory = new CommandLineValidationContextFactory(app); @@ -76,7 +76,7 @@ public void ItExecutesClassLevelValidationAttribute(Type attributeType, string v app.Model.Arg2 = invalidProp2Value; var result = validator.GetValidationResult(app, context); - Assert.NotNull(result); + Assert.NotNull(result?.ErrorMessage); Assert.NotEmpty(result.ErrorMessage); } @@ -96,7 +96,7 @@ private void OnExecute() { } [InlineData("email@example.com", 0)] public void ValidatesEmailArgument(string? email, int exitCode) { - Assert.Equal(exitCode, CommandLineApplication.Execute(new TestConsole(_output), email)); + Assert.Equal(exitCode, CommandLineApplication.Execute(new TestConsole(_output), email!)); } private class OptionBuilderApp : CommandLineApplication @@ -165,7 +165,7 @@ public void ValidatesAttributesOnOption(string[] args, int exitCode) private sealed class ThrowingValidationAttribute : ValidationAttribute { - public override bool IsValid(object value) + public override bool IsValid(object? value) { throw new InvalidOperationException(); } @@ -182,7 +182,7 @@ private sealed class ClassLevelValidationApp [AttributeUsage(AttributeTargets.Class)] private sealed class ClassLevelValidationAttribute : ValidationAttribute { - public override bool IsValid(object value) + public override bool IsValid(object? value) => value is ClassLevelValidationApp app && app.Arg1 != null && app.Arg1.Contains("good") && app.Arg2 != null && app.Arg2.Contains("good"); @@ -191,7 +191,7 @@ public override bool IsValid(object value) [AttributeUsage(AttributeTargets.Property)] private sealed class ModeValidationAttribute : ValidationAttribute { - public override bool IsValid(object value) + public override bool IsValid(object? value) { return value is string text && text.Contains("mode"); } diff --git a/test/CommandLineUtils.Tests/CommandLineApplicationExecutorTests.cs b/test/CommandLineUtils.Tests/CommandLineApplicationExecutorTests.cs index 99b6cbfd..d94d0773 100755 --- a/test/CommandLineUtils.Tests/CommandLineApplicationExecutorTests.cs +++ b/test/CommandLineUtils.Tests/CommandLineApplicationExecutorTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Nate McMaster. +// Copyright (c) Nate McMaster. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -158,6 +158,7 @@ public void ThrowsForUnknownOnExecuteTypes() var ex = Assert.Throws( () => CommandLineApplication.Execute()); var method = typeof(ExecuteWithUnknownTypes).GetMethod("OnExecute", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(method); var param = Assert.Single(method.GetParameters()); Assert.Equal(Strings.UnsupportedParameterTypeOnMethod(method.Name, param), ex.Message); } @@ -312,7 +313,7 @@ public void Dispose() [Command("sub")] private class Subcommand { - public DisposableParentCommand Parent { get; } + public DisposableParentCommand? Parent { get; } public void OnExecute() { diff --git a/test/CommandLineUtils.Tests/CommandLineApplicationTests.cs b/test/CommandLineUtils.Tests/CommandLineApplicationTests.cs index c7d3d5c8..e8b0013f 100644 --- a/test/CommandLineUtils.Tests/CommandLineApplicationTests.cs +++ b/test/CommandLineUtils.Tests/CommandLineApplicationTests.cs @@ -492,7 +492,8 @@ public void AllowNoThrowBehaviorOnUnexpectedOptionAfterSubcommand() // (does not throw) app.Execute("k", "run", unexpectedOption); Assert.Empty(testCmd.RemainingArguments); - var arg = Assert.Single(subCmd?.RemainingArguments); + Assert.NotNull(subCmd); + var arg = Assert.Single(subCmd.RemainingArguments); Assert.Equal(unexpectedOption, arg); } @@ -697,9 +698,10 @@ public void NestedInheritedOptions() Assert.Contains(subcmd1.GetOptions(), o => o.LongName == "nest1"); Assert.Contains(subcmd1.GetOptions(), o => o.LongName == "global"); - Assert.Contains(subcmd2?.GetOptions(), o => o.LongName == "nest2"); - Assert.Contains(subcmd2?.GetOptions(), o => o.LongName == "nest1"); - Assert.Contains(subcmd2?.GetOptions(), o => o.LongName == "global"); + Assert.NotNull(subcmd2); + Assert.Contains(subcmd2.GetOptions(), o => o.LongName == "nest2"); + Assert.Contains(subcmd2.GetOptions(), o => o.LongName == "nest1"); + Assert.Contains(subcmd2.GetOptions(), o => o.LongName == "global"); Assert.ThrowsAny(() => app.Execute("--nest2", "N2", "--nest1", "N1", "-g", "G")); Assert.ThrowsAny(() => app.Execute("lvl1", "--nest2", "N2", "--nest1", "N1", "-g", "G")); @@ -1051,7 +1053,7 @@ public void ThrowsExceptionOnInvalidArgument(string? inputOption) { var app = new CommandLineApplication(); - var exception = Assert.ThrowsAny(() => app.Execute(inputOption)); + var exception = Assert.ThrowsAny(() => app.Execute(inputOption!)); Assert.Equal($"Unrecognized command or argument '{inputOption}'", exception.Message); } diff --git a/test/CommandLineUtils.Tests/CustomValidationAttributeTest.cs b/test/CommandLineUtils.Tests/CustomValidationAttributeTest.cs index 0cd119f2..15226d80 100644 --- a/test/CommandLineUtils.Tests/CustomValidationAttributeTest.cs +++ b/test/CommandLineUtils.Tests/CustomValidationAttributeTest.cs @@ -12,7 +12,7 @@ public class CustomValidationAttributeTest [InlineData(null)] [InlineData("-c", "red")] [InlineData("-c", "blue")] - public void CustomValidationAttributePasses(params string?[] args) + public void CustomValidationAttributePasses(params string[]? args) { var app = new CommandLineApplication(); app.Conventions.UseDefaultConventions(); @@ -34,7 +34,7 @@ public void CustomValidationAttributeFails(params string?[] args) { var app = new CommandLineApplication(); app.Conventions.UseAttributes(); - var result = app.Parse(args); + var result = app.Parse(args!); var validationResult = result.SelectedCommand.GetValidationResult(); Assert.NotEqual(ValidationResult.Success, validationResult); var program = Assert.IsType>(result.SelectedCommand); @@ -43,7 +43,7 @@ public void CustomValidationAttributeFails(params string?[] args) { Assert.Equal(args[1], app.Model.Color); } - Assert.Equal("The value for --color must be 'red' or 'blue'", validationResult.ErrorMessage); + Assert.Equal("The value for --color must be 'red' or 'blue'", validationResult?.ErrorMessage); } private class RedBlueProgram diff --git a/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs b/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs index 7f46adf4..e2084052 100644 --- a/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs +++ b/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs @@ -79,7 +79,7 @@ public void DoesNotOrderCommandsByName() Assert.True(indexOfA > indexOfB); } - private string GetHelpText(CommandLineApplication app, DefaultHelpTextGenerator generator, string helpOption = null) + private string GetHelpText(CommandLineApplication app, DefaultHelpTextGenerator generator, string? helpOption = null) { var sb = new StringBuilder(); app.Out = new StringWriter(sb); @@ -90,7 +90,7 @@ private string GetHelpText(CommandLineApplication app, DefaultHelpTextGenerator return helpText; } - private string GetHelpText(CommandLineApplication app, string helpOption = null) + private string GetHelpText(CommandLineApplication app, string? helpOption = null) { var generator = new DefaultHelpTextGenerator { @@ -232,12 +232,12 @@ SomeNullableEnumArgument nullable enum arg desc. public class MyApp { [Option(ShortName = "strOpt", ValueName = "STR_OPT", Description = "str option desc.")] - public string strOpt { get; set; } + public string? strOpt { get; set; } [Option(ShortName = "rStrOpt", ValueName = "STR_OPT", Description = "restricted str option desc.")] [Required] [AllowedValues("Foo", "Bar")] - public string rStrOpt { get; set; } + public string? rStrOpt { get; set; } [Option(ShortName = "dStrOpt", ValueName = "STR_OPT", Description = "str option with default value desc.")] public string dStrOpt { get; set; } = "Foo"; @@ -265,12 +265,12 @@ public class MyApp public SomeEnum Verb5 { get; set; } [Argument(0, Description = "string arg desc.")] - public string SomeStringArgument { get; set; } + public string? SomeStringArgument { get; set; } [Argument(1, Description = "restricted string arg desc.")] [Required] [AllowedValues("Foo", "Bar")] - public string RestrictedStringArgument { get; set; } + public string? RestrictedStringArgument { get; set; } [Argument(2, Description = "string arg with default value desc.")] public string DefaultValStringArgument { get; set; } = "Foo"; diff --git a/test/CommandLineUtils.Tests/DotNetExeTests.cs b/test/CommandLineUtils.Tests/DotNetExeTests.cs index 4e3797b3..14c7cdb0 100644 --- a/test/CommandLineUtils.Tests/DotNetExeTests.cs +++ b/test/CommandLineUtils.Tests/DotNetExeTests.cs @@ -3,7 +3,7 @@ // This file has been modified from the original form. See Notice.txt in the project root for more information. -#if NETCOREAPP3_1_OR_GREATER +#if NET6_0_OR_GREATER using System.IO; using Xunit; @@ -22,7 +22,7 @@ public void FindsTheDotNetPath() } } } -#elif NET472 +#elif NET472_OR_GREATER #else #error Update target frameworks #endif diff --git a/test/CommandLineUtils.Tests/FilePathExistsAttributeTests.cs b/test/CommandLineUtils.Tests/FilePathExistsAttributeTests.cs index 63fd189c..22f06da0 100644 --- a/test/CommandLineUtils.Tests/FilePathExistsAttributeTests.cs +++ b/test/CommandLineUtils.Tests/FilePathExistsAttributeTests.cs @@ -45,7 +45,7 @@ public void ValidatesFilesMustExist(string? filePath) .GetValidationResult(); Assert.NotEqual(ValidationResult.Success, result); - Assert.Equal($"The file path '{filePath}' does not exist.", result.ErrorMessage); + Assert.Equal($"The file path '{filePath}' does not exist.", result?.ErrorMessage); var console = new TestConsole(_output); Assert.NotEqual(0, CommandLineApplication.Execute(console, filePath!)); @@ -95,7 +95,7 @@ public void ValidatesFilesRelativeToAppContext() Assert.Equal(ValidationResult.Success, success); Assert.NotEqual(ValidationResult.Success, fails); - Assert.Equal("The file path 'exists.txt' does not exist.", fails.ErrorMessage); + Assert.Equal("The file path 'exists.txt' does not exist.", fails?.ErrorMessage); var console = new TestConsole(_output); var context = new DefaultCommandLineContext(console, appNotInBaseDir.WorkingDirectory, new[] { "exists.txt" }); diff --git a/test/CommandLineUtils.Tests/FilePathNotExistsAttributeTests.cs b/test/CommandLineUtils.Tests/FilePathNotExistsAttributeTests.cs index 79fff038..9a0374c4 100644 --- a/test/CommandLineUtils.Tests/FilePathNotExistsAttributeTests.cs +++ b/test/CommandLineUtils.Tests/FilePathNotExistsAttributeTests.cs @@ -51,7 +51,7 @@ public void ValidatesFilesMustNotExist(string filePath) .GetValidationResult(); Assert.NotEqual(ValidationResult.Success, result); - Assert.Equal($"The file path '{filePath}' already exists.", result.ErrorMessage); + Assert.Equal($"The file path '{filePath}' already exists.", result?.ErrorMessage); var console = new TestConsole(_output); Assert.NotEqual(0, CommandLineApplication.Execute(console, filePath)); @@ -90,7 +90,7 @@ public void ValidatesFilesRelativeToAppContext() .GetValidationResult(); Assert.NotEqual(ValidationResult.Success, fails); - Assert.Equal("The file path 'exists.txt' already exists.", fails.ErrorMessage); + Assert.Equal("The file path 'exists.txt' already exists.", fails?.ErrorMessage); Assert.Equal(ValidationResult.Success, success); diff --git a/test/CommandLineUtils.Tests/HelpOptionAttributeTests.cs b/test/CommandLineUtils.Tests/HelpOptionAttributeTests.cs index 7e40b5f5..b3fab333 100644 --- a/test/CommandLineUtils.Tests/HelpOptionAttributeTests.cs +++ b/test/CommandLineUtils.Tests/HelpOptionAttributeTests.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Reflection; using System.Text; using Xunit; using Xunit.Abstractions; @@ -90,7 +91,7 @@ public void ThrowsIfMultipleAttributesApplied() { var ex = Assert.Throws(() => new CommandLineApplication().Conventions.UseHelpOptionAttribute()); - var prop = typeof(DuplicateOptionAttributes).GetProperty(nameof(DuplicateOptionAttributes.IsHelpOption)); + var prop = Assert.IsAssignableFrom(typeof(DuplicateOptionAttributes).GetProperty(nameof(DuplicateOptionAttributes.IsHelpOption))); Assert.Equal(Strings.BothOptionAndHelpOptionAttributesCannotBeSpecified(prop), ex.Message); } diff --git a/test/CommandLineUtils.Tests/LegalFilePathAttributeTests.cs b/test/CommandLineUtils.Tests/LegalFilePathAttributeTests.cs index 3f2bf4ea..2bd7b02c 100644 --- a/test/CommandLineUtils.Tests/LegalFilePathAttributeTests.cs +++ b/test/CommandLineUtils.Tests/LegalFilePathAttributeTests.cs @@ -49,7 +49,7 @@ public void ValidatesLegalFilePaths(string filePath) public void FailsInvalidLegalFilePaths(string? filePath) { var console = new TestConsole(_output); - Assert.NotEqual(0, CommandLineApplication.Execute(console, filePath)); + Assert.NotEqual(0, CommandLineApplication.Execute(console, filePath!)); } } } diff --git a/test/CommandLineUtils.Tests/McMaster.Extensions.CommandLineUtils.Tests.csproj b/test/CommandLineUtils.Tests/McMaster.Extensions.CommandLineUtils.Tests.csproj index 317c17cd..d40eec30 100644 --- a/test/CommandLineUtils.Tests/McMaster.Extensions.CommandLineUtils.Tests.csproj +++ b/test/CommandLineUtils.Tests/McMaster.Extensions.CommandLineUtils.Tests.csproj @@ -2,8 +2,7 @@ net8.0;net10.0 - - annotations + $(TargetFrameworks);net472 @@ -27,6 +26,10 @@ + + + + diff --git a/test/CommandLineUtils.Tests/OptionAttributeTests.cs b/test/CommandLineUtils.Tests/OptionAttributeTests.cs index feb8f475..beeca0f9 100644 --- a/test/CommandLineUtils.Tests/OptionAttributeTests.cs +++ b/test/CommandLineUtils.Tests/OptionAttributeTests.cs @@ -31,7 +31,7 @@ public void ThrowsWhenOptionTypeCannotBeDetermined() var ex = Assert.Throws( () => Create()); Assert.Equal( - Strings.CannotDetermineOptionType(typeof(AppWithUnknownOptionType).GetProperty("Option")), + Strings.CannotDetermineOptionType(Assert.IsAssignableFrom(typeof(AppWithUnknownOptionType).GetProperty("Option"))), ex.Message); } @@ -71,7 +71,7 @@ private class EmptyShortName public void CanSetShortNameToEmptyString() { var app = Create(); - Assert.All(app.Options, o => Assert.Empty(o.ShortName)); + Assert.All(app.Options, o => Assert.True(o.ShortName is null or "")); } private class AmbiguousShortOptionName @@ -91,8 +91,8 @@ public void ThrowsWhenShortOptionNamesAreAmbiguous() Assert.Equal( Strings.OptionNameIsAmbiguous("m", - typeof(AmbiguousShortOptionName).GetProperty("Mode"), - typeof(AmbiguousShortOptionName).GetProperty("Message")), + Assert.IsAssignableFrom(typeof(AmbiguousShortOptionName).GetProperty("Mode")), + Assert.IsAssignableFrom(typeof(AmbiguousShortOptionName).GetProperty("Message"))), ex.Message); } @@ -113,8 +113,8 @@ public void ThrowsWhenLongOptionNamesAreAmbiguous() Assert.Equal( Strings.OptionNameIsAmbiguous("no-edit", - typeof(AmbiguousLongOptionName).GetProperty("NoEdit"), - typeof(AmbiguousLongOptionName).GetProperty("ManuallySetToNoEdit")), + Assert.IsAssignableFrom(typeof(AmbiguousLongOptionName).GetProperty("NoEdit")), + Assert.IsAssignableFrom(typeof(AmbiguousLongOptionName).GetProperty("ManuallySetToNoEdit"))), ex.Message); } @@ -133,7 +133,7 @@ public void ThrowsWhenOptionAndArgumentAreSpecified() Assert.Equal( Strings.BothOptionAndArgumentAttributesCannotBeSpecified( - typeof(BothOptionAndArgument).GetProperty("NotPossible")), + Assert.IsAssignableFrom(typeof(BothOptionAndArgument).GetProperty("NotPossible"))), ex.Message); } @@ -203,7 +203,7 @@ public void KeepsDefaultValues() private class AppWithMultiValueStringOption { [Option("-o1")] - string[] Opt1 { get; } + string[]? Opt1 { get; } [Option("-o2")] string[] Opt2 { get; } = Array.Empty(); @@ -341,12 +341,12 @@ private CommandOption CreateOption(Type propType, string propName) var tb = mb.DefineType("Program"); var pb = tb.DefineProperty(propName, PropertyAttributes.None, propType, Array.Empty()); tb.DefineField($"<{propName}>k__BackingField", propType, FieldAttributes.Private); - var ctor = typeof(OptionAttribute).GetConstructor(Array.Empty()); + var ctor = Assert.IsAssignableFrom(typeof(OptionAttribute).GetConstructor(Array.Empty())); var ab = new CustomAttributeBuilder(ctor, Array.Empty()); pb.SetCustomAttribute(ab); var program = tb.CreateType(); var appBuilder = typeof(CommandLineApplication<>).MakeGenericType(program); - var app = (CommandLineApplication)Activator.CreateInstance(appBuilder, Array.Empty()); + var app = Assert.IsAssignableFrom(Activator.CreateInstance(appBuilder, Array.Empty())); app.Conventions.UseOptionAttributes(); return app.Options.First(); } @@ -465,7 +465,7 @@ public void ApplyingOptionConventionTwice_WithLongOnlyOptions_DoesNotThrow() Assert.Single(app.Options, o => o.LongName == "count"); // Verify short names are empty - Assert.All(app.Options, o => Assert.Empty(o.ShortName)); + Assert.All(app.Options, o => Assert.True(o.ShortName is null or "")); } #endregion diff --git a/test/CommandLineUtils.Tests/ResponseFileTests.cs b/test/CommandLineUtils.Tests/ResponseFileTests.cs index 90621e12..bb5867c9 100644 --- a/test/CommandLineUtils.Tests/ResponseFileTests.cs +++ b/test/CommandLineUtils.Tests/ResponseFileTests.cs @@ -269,7 +269,7 @@ public void SubcommandsCanResponseFileOptions() }); var rspFile = CreateResponseFile(" 'lorem ipsum' ", "dolor sit amet"); app.Execute("save", "@" + rspFile); - Assert.Collection(wordArgs?.Values, + Assert.Collection(wordArgs?.Values ?? [], a => Assert.Equal("lorem ipsum", a), a => Assert.Equal("dolor", a), a => Assert.Equal("sit", a), diff --git a/test/CommandLineUtils.Tests/StringExtensionsTests.cs b/test/CommandLineUtils.Tests/StringExtensionsTests.cs index ce107778..b435ae47 100644 --- a/test/CommandLineUtils.Tests/StringExtensionsTests.cs +++ b/test/CommandLineUtils.Tests/StringExtensionsTests.cs @@ -29,7 +29,7 @@ public class StringExtensionsTests [InlineData("m_Field", "m-field")] public void ToKebabCase(string? input, string? expected) { - Assert.Equal(expected, input.ToKebabCase()); + Assert.Equal(expected, input!.ToKebabCase()); } [Theory] @@ -41,7 +41,7 @@ public void ToKebabCase(string? input, string? expected) [InlineData("MSBuildTask", "MSBUILD_TASK")] public void ToConstantCase(string? input, string? expected) { - Assert.Equal(expected, input.ToConstantCase()); + Assert.Equal(expected, input!.ToConstantCase()); } } } diff --git a/test/CommandLineUtils.Tests/ValidateMethodConventionTests.cs b/test/CommandLineUtils.Tests/ValidateMethodConventionTests.cs index a37097eb..09609a2f 100644 --- a/test/CommandLineUtils.Tests/ValidateMethodConventionTests.cs +++ b/test/CommandLineUtils.Tests/ValidateMethodConventionTests.cs @@ -25,7 +25,7 @@ public void ValidatorAddedViaConvention() app.Conventions.UseOnValidateMethodFromModel(); var result = app.GetValidationResult(); Assert.NotEqual(ValidationResult.Success, result); - Assert.Equal("Failed", result.ErrorMessage); + Assert.Equal("Failed", result?.ErrorMessage); } private class ProgramWithBadOnValidate @@ -49,7 +49,7 @@ private class MainValidate [Option] public int? Middle { get; } - private ValidationResult OnValidate(ValidationContext context, CommandLineContext appContext) + private ValidationResult? OnValidate(ValidationContext context, CommandLineContext appContext) { if (this.Middle.HasValue && this.Middle < 0) { @@ -71,7 +71,7 @@ private class SubcommandValidate [Option] public int End { get; private set; } = Int32.MaxValue; - private ValidationResult OnValidate(ValidationContext context, CommandLineContext appContext) + private ValidationResult? OnValidate(ValidationContext context, CommandLineContext appContext) { if (this.Start >= this.End) { @@ -119,7 +119,7 @@ public void ValidatorShouldGetDeserializedModelInSubcommands(string args, string else { Assert.NotEqual(ValidationResult.Success, result); - Assert.Equal(error, result.ErrorMessage); + Assert.Equal(error, result?.ErrorMessage); } } } diff --git a/test/CommandLineUtils.Tests/ValidationTests.cs b/test/CommandLineUtils.Tests/ValidationTests.cs index b4cfee74..1797c971 100644 --- a/test/CommandLineUtils.Tests/ValidationTests.cs +++ b/test/CommandLineUtils.Tests/ValidationTests.cs @@ -50,7 +50,7 @@ public void ValidatorInvoked() app.OnValidate(_ => { called = true; - return ValidationResult.Success; + return ValidationResult.Success!; }); Assert.Equal(0, app.Execute()); Assert.True(called); diff --git a/test/CommandLineUtils.Tests/ValueParserProviderCustomTests.cs b/test/CommandLineUtils.Tests/ValueParserProviderCustomTests.cs index 77edfb54..a5295fae 100644 --- a/test/CommandLineUtils.Tests/ValueParserProviderCustomTests.cs +++ b/test/CommandLineUtils.Tests/ValueParserProviderCustomTests.cs @@ -144,7 +144,7 @@ public void DefaultCultureCanBeChanged(string property, string test, string cult app.Conventions.UseAttributes(); app.Parse(test); - var actual = (DateTimeOffset)typeof(DateParserProgram).GetProperty(property).GetMethod.Invoke(app.Model, null); + var actual = Assert.IsAssignableFrom(typeof(DateParserProgram).GetProperty(property)?.GetMethod?.Invoke(app.Model, null)); Assert.Equal(expected, actual); } diff --git a/test/CommandLineUtils.Tests/VersionOptionAttributeTests.cs b/test/CommandLineUtils.Tests/VersionOptionAttributeTests.cs index 2afe5da4..b5b7b54d 100644 --- a/test/CommandLineUtils.Tests/VersionOptionAttributeTests.cs +++ b/test/CommandLineUtils.Tests/VersionOptionAttributeTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Reflection; using Xunit; using Xunit.Abstractions; @@ -88,7 +89,7 @@ public void ThrowsIfMultipleAttributesApplied() { var ex = Assert.Throws(() => new CommandLineApplication().Conventions.UseVersionOptionAttribute()); - var prop = typeof(DuplicateOptionAttributes).GetProperty(nameof(DuplicateOptionAttributes.IsVersionOption)); + var prop = Assert.IsAssignableFrom(typeof(DuplicateOptionAttributes).GetProperty(nameof(DuplicateOptionAttributes.IsVersionOption))); Assert.Equal(Strings.BothOptionAndVersionOptionAttributesCannotBeSpecified(prop), ex.Message); } @@ -104,8 +105,7 @@ public void ThrowsIfHelpAndVersionAttributesApplied() { var ex = Assert.Throws(() => new CommandLineApplication().Conventions.UseVersionOptionAttribute()); - var prop = typeof(DuplicateOptionAttributes2).GetProperty(nameof(DuplicateOptionAttributes - .IsVersionOption)); + var prop = Assert.IsAssignableFrom(typeof(DuplicateOptionAttributes2).GetProperty(nameof(DuplicateOptionAttributes.IsVersionOption))); Assert.Equal(Strings.BothHelpOptionAndVersionOptionAttributesCannotBeSpecified(prop), ex.Message); } diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 358cb1a6..ab31da95 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -2,5 +2,6 @@ $(DefaultItemExcludes);TestResults\** + $(MSBuildThisFileDirectory)\.runsettings diff --git a/test/Hosting.CommandLine.Tests/McMaster.Extensions.Hosting.CommandLine.Tests.csproj b/test/Hosting.CommandLine.Tests/McMaster.Extensions.Hosting.CommandLine.Tests.csproj index 5989a3de..4f29a0a9 100644 --- a/test/Hosting.CommandLine.Tests/McMaster.Extensions.Hosting.CommandLine.Tests.csproj +++ b/test/Hosting.CommandLine.Tests/McMaster.Extensions.Hosting.CommandLine.Tests.csproj @@ -2,6 +2,7 @@ net8.0;net10.0 + $(TargetFrameworks);net472