diff --git a/Light.GuardClauses.SingleFile.cs b/Light.GuardClauses.SingleFile.cs index fedfcc0..c6d7283 100644 --- a/Light.GuardClauses.SingleFile.cs +++ b/Light.GuardClauses.SingleFile.cs @@ -4214,6 +4214,182 @@ public static TItem MustBeOneOf(this TItem parameter, [NotNu return parameter; } + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws an . + /// + /// The value to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when is zero or negative. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static sbyte MustBePositive(this sbyte parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (!(parameter > (sbyte)0)) + { + Throw.MustBePositive(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws your custom exception. + /// + /// The value to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// + /// Your custom exception thrown when is zero or negative. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static sbyte MustBePositive(this sbyte parameter, Func exceptionFactory) + { + if (!(parameter > (sbyte)0)) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws an . + /// + /// The value to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when is zero. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte MustBePositive(this byte parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (!(parameter > (byte)0)) + { + Throw.MustBePositive(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws your custom exception. + /// + /// The value to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// + /// Your custom exception thrown when is zero. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static byte MustBePositive(this byte parameter, Func exceptionFactory) + { + if (!(parameter > (byte)0)) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws an . + /// + /// The value to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when is zero or negative. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short MustBePositive(this short parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (!(parameter > (short)0)) + { + Throw.MustBePositive(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws your custom exception. + /// + /// The value to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// + /// Your custom exception thrown when is zero or negative. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static short MustBePositive(this short parameter, Func exceptionFactory) + { + if (!(parameter > (short)0)) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws an . + /// + /// The value to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when is zero. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort MustBePositive(this ushort parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (!(parameter > (ushort)0)) + { + Throw.MustBePositive(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws your custom exception. + /// + /// The value to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// + /// Your custom exception thrown when is zero. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static ushort MustBePositive(this ushort parameter, Func exceptionFactory) + { + if (!(parameter > (ushort)0)) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + /// /// Ensures that the specified is positive (greater than zero), or otherwise /// throws an . @@ -4258,6 +4434,50 @@ public static int MustBePositive(this int parameter, Func except return parameter; } + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws an . + /// + /// The value to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when is zero. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint MustBePositive(this uint parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (!(parameter > 0U)) + { + Throw.MustBePositive(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws your custom exception. + /// + /// The value to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// + /// Your custom exception thrown when is zero. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static uint MustBePositive(this uint parameter, Func exceptionFactory) + { + if (!(parameter > 0U)) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + /// /// Ensures that the specified is positive (greater than zero), or otherwise /// throws an . @@ -4302,6 +4522,50 @@ public static long MustBePositive(this long parameter, Func exc return parameter; } + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws an . + /// + /// The value to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when is zero. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong MustBePositive(this ulong parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null) + { + if (!(parameter > 0UL)) + { + Throw.MustBePositive(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws your custom exception. + /// + /// The value to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// + /// Your custom exception thrown when is zero. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static ulong MustBePositive(this ulong parameter, Func exceptionFactory) + { + if (!(parameter > 0UL)) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + /// /// Ensures that the specified is positive (greater than zero), or otherwise /// throws an . diff --git a/ai-plans/0168-must-be-positive-older-frameworks.md b/ai-plans/0168-must-be-positive-older-frameworks.md new file mode 100644 index 0000000..ccf035f --- /dev/null +++ b/ai-plans/0168-must-be-positive-older-frameworks.md @@ -0,0 +1,38 @@ +# MustBePositive Parity for Older Target Frameworks + +## Rationale + +Parent issue #162 (point 5) identifies a target-framework gap in `MustBePositive`: the .NET 10 generic `INumber` overload supports integral types such as `byte` and `ushort`, but the .NET Standard targets and their single-file source export expose only the existing concrete overloads. Consequently, BrilliantMessaging must express seven positive `byte` or `ushort` guards as the less-specific `MustBeGreaterThan(0)`. + +Add concrete overloads for the complete missing signed and unsigned integral set so `MustBePositive` has a symmetric API on every supported target framework without changing the modern generic overloads. + +## Acceptance Criteria + +- [x] `MustBePositive` has concrete overloads for `sbyte`, `byte`, `short`, `ushort`, `uint`, and `ulong` on .NET Standard 2.0, .NET Standard 2.1, and .NET 10; every overload returns the original value when it is greater than zero and throws `ArgumentOutOfRangeException` for zero and, for signed types, negative values. +- [x] Each new integral type has a custom-exception-factory overload accepting `Func`; it passes the original value to the factory, invokes the factory only when validation fails, and a null factory on a failing value throws `ArgumentNullException` via the existing `Throw.CustomException` convention. +- [x] Default failures preserve the existing `MustBePositive` exception contract, including caller-argument-expression parameter names, optional custom messages, actual values, and the standard generated message. +- [x] Automated tests cover positive boundary values, zero for every new type, negative boundary values for the signed types, return values, parameter-name and custom-message propagation, custom factory values and exceptions, factories not invoked on success, and null-factory behavior. +- [x] The existing `MustBePositive` source-export whitelist entry includes all new concrete overloads in portable and modern exports, preserves the generic `INumber` overloads only in the modern export, and continues to trim every custom-exception-factory overload when that option is disabled. +- [x] The committed .NET Standard 2.0 single-file distribution is regenerated with the new overloads and validates for both supported source-export targets. +- [x] The comparable/range documentation no longer describes the new concrete integral types as modern-target-only, and the package release notes mention the expanded `MustBePositive` support. +- [x] The complete solution restores and builds without warnings in Release configuration, and all automated tests pass on the pinned SDK. + +## Technical Details + +Add the twelve overloads to the existing `Check.MustBePositive.cs` outside the `NET8_0_OR_GREATER` region, following the exact structure, annotations, XML documentation, and comparison semantics of the `int` and `long` overload pairs. For each `T` in `sbyte`, `byte`, `short`, `ushort`, `uint`, and `ulong`, the public shape is: + +```csharp +public static T MustBePositive( + this T parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null +); + +public static T MustBePositive(this T parameter, Func exceptionFactory); +``` + +Compare each value directly with its type-appropriate zero. Do not widen values to `int` or `long`: the returned value, exception `ActualValue`, and custom-factory argument must retain the receiver's exact type. The existing generic `Throw.MustBePositive` and `Throw.CustomException` helpers already support all six types, so no new exception or throw-helper API is needed. + +Keep the `INumber` overloads under `NET8_0_OR_GREATER`. On modern targets, normal calls with the six integral types resolve to the new concrete overloads while explicitly generic calls remain available; both paths must have the same greater-than-zero semantics. This issue does not expand the other sign-guard families or add native-integer overloads. + +Extend `MustBePositiveTests` and `NumericCustomFactorySuccessTests` with the concrete integral matrix. Strengthen `SourceFileMergerWhitelistTests.SignGuardWhitelistsUseTargetSpecificSurface` to assert representative signed and unsigned overloads, exact factory signatures, portable omission of `INumber`, modern retention of `INumber`, and factory trimming. Regenerate `tools/source-export/Light.GuardClauses.SourceCodeTransformation/Light.GuardClauses.SingleFile.cs` using the committed source-export settings. diff --git a/docs/assertion-overview.md b/docs/assertion-overview.md index b79af37..61845aa 100644 --- a/docs/assertion-overview.md +++ b/docs/assertion-overview.md @@ -13,7 +13,7 @@ The package has .NET Standard 2.0, .NET Standard 2.1, and .NET 10 assets. The co The .NET 10 asset additionally provides: - generic `INumber` overloads for `IsApproximately`, `MustBeApproximately`, `MustNotBeApproximately`, `IsGreaterThanOrApproximately`, `MustBeGreaterThanOrApproximately`, `IsLessThanOrApproximately`, and `MustBeLessThanOrApproximately`; -- generic `INumber` overloads for `MustBePositive`, `MustBeNegative`, `MustNotBePositive`, `MustNotBeNegative`, and `MustNotBeZero`, covering numeric types without concrete overloads (such as `short`, `byte`, or `Half`); +- generic `INumber` overloads for `MustBePositive`, `MustBeNegative`, `MustNotBePositive`, `MustNotBeNegative`, and `MustNotBeZero`; for `MustBePositive`, these extend the common concrete integral overloads to remaining numeric types such as `Half`; - generic `IFloatingPointIeee754` overloads for `IsFinite` and `MustBeFinite`, including `Half` but excluding `decimal`; - `Span`, `ReadOnlySpan`, `Memory`, and `ReadOnlyMemory` overloads for `IsEmailAddress` and `MustBeEmailAddress`; and - trimming annotations on the type-relation helpers where supported by the framework. @@ -72,7 +72,7 @@ UUIDv7 validation checks the version-7 nibble and RFC/IETF `10xx` variant bits d | `IsGreaterThanOrApproximately`, `MustBeGreaterThanOrApproximately` | Accept values greater than or within tolerance of the comparison value | | `IsLessThanOrApproximately`, `MustBeLessThanOrApproximately` | Accept values less than or within tolerance of the comparison value | -The five sign guard families have concrete overloads for `int`, `long`, `decimal`, `float`, `double`, and `TimeSpan` on all package targets; the .NET 10 asset adds the generic `INumber` overloads listed above. All checks compare the value against zero with the type's comparison operators. Consequently, `NaN` is rejected by the four sign guards and accepted by `MustNotBeZero`, positive and negative infinity satisfy the guards matching their sign (compose with `MustBeFinite` to reject non-finite values), and negative zero — including `decimal`'s signed zero representations — behaves exactly like zero. `MustNotBeZero` uses exact equality; tolerance-based comparisons remain the domain of the approximation guards. +The five sign guard families have concrete overloads for `int`, `long`, `decimal`, `float`, `double`, and `TimeSpan` on all package targets. `MustBePositive` additionally has concrete `sbyte`, `byte`, `short`, `ushort`, `uint`, and `ulong` overloads on every target. The .NET 10 asset adds the generic `INumber` overloads listed above. All checks compare the value against zero with the type's comparison operators. Consequently, `NaN` is rejected by the four sign guards and accepted by `MustNotBeZero`, positive and negative infinity satisfy the guards matching their sign (compose with `MustBeFinite` to reject non-finite values), and negative zero — including `decimal`'s signed zero representations — behaves exactly like zero. `MustNotBeZero` uses exact equality; tolerance-based comparisons remain the domain of the approximation guards. Create ranges with the `Range` fluent API: diff --git a/src/Directory.Build.props b/src/Directory.Build.props index f84f8ec..4c4404a 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -21,6 +21,7 @@ -------------------------------- - new assertions: MustBeAssignableTo, MustBeUri, ObjectDisposed + - expanded MustBePositive support for sbyte, byte, short, ushort, uint, and ulong on all target frameworks diff --git a/src/Light.GuardClauses/Check.MustBePositive.cs b/src/Light.GuardClauses/Check.MustBePositive.cs index 0efd376..db440ff 100644 --- a/src/Light.GuardClauses/Check.MustBePositive.cs +++ b/src/Light.GuardClauses/Check.MustBePositive.cs @@ -10,6 +10,198 @@ namespace Light.GuardClauses; public static partial class Check { + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws an . + /// + /// The value to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when is zero or negative. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static sbyte MustBePositive( + this sbyte parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (!(parameter > (sbyte) 0)) + { + Throw.MustBePositive(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws your custom exception. + /// + /// The value to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// + /// Your custom exception thrown when is zero or negative. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static sbyte MustBePositive(this sbyte parameter, Func exceptionFactory) + { + if (!(parameter > (sbyte) 0)) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws an . + /// + /// The value to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when is zero. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte MustBePositive( + this byte parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (!(parameter > (byte) 0)) + { + Throw.MustBePositive(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws your custom exception. + /// + /// The value to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// + /// Your custom exception thrown when is zero. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static byte MustBePositive(this byte parameter, Func exceptionFactory) + { + if (!(parameter > (byte) 0)) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws an . + /// + /// The value to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when is zero or negative. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short MustBePositive( + this short parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (!(parameter > (short) 0)) + { + Throw.MustBePositive(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws your custom exception. + /// + /// The value to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// + /// Your custom exception thrown when is zero or negative. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static short MustBePositive(this short parameter, Func exceptionFactory) + { + if (!(parameter > (short) 0)) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws an . + /// + /// The value to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when is zero. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort MustBePositive( + this ushort parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (!(parameter > (ushort) 0)) + { + Throw.MustBePositive(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws your custom exception. + /// + /// The value to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// + /// Your custom exception thrown when is zero. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static ushort MustBePositive(this ushort parameter, Func exceptionFactory) + { + if (!(parameter > (ushort) 0)) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + /// /// Ensures that the specified is positive (greater than zero), or otherwise /// throws an . @@ -58,6 +250,54 @@ public static int MustBePositive(this int parameter, Func except return parameter; } + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws an . + /// + /// The value to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when is zero. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint MustBePositive( + this uint parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (!(parameter > 0U)) + { + Throw.MustBePositive(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws your custom exception. + /// + /// The value to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// + /// Your custom exception thrown when is zero. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static uint MustBePositive(this uint parameter, Func exceptionFactory) + { + if (!(parameter > 0U)) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + /// /// Ensures that the specified is positive (greater than zero), or otherwise /// throws an . @@ -106,6 +346,54 @@ public static long MustBePositive(this long parameter, Func exc return parameter; } + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws an . + /// + /// The value to be checked. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// + /// Thrown when is zero. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong MustBePositive( + this ulong parameter, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (!(parameter > 0UL)) + { + Throw.MustBePositive(parameter, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the specified is positive (greater than zero), or otherwise + /// throws your custom exception. + /// + /// The value to be checked. + /// + /// The delegate that creates your custom exception. is passed to this delegate. + /// + /// + /// Your custom exception thrown when is zero. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static ulong MustBePositive(this ulong parameter, Func exceptionFactory) + { + if (!(parameter > 0UL)) + { + Throw.CustomException(exceptionFactory, parameter); + } + + return parameter; + } + /// /// Ensures that the specified is positive (greater than zero), or otherwise /// throws an . diff --git a/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerWhitelistTests.cs b/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerWhitelistTests.cs index 675380c..1a91d73 100644 --- a/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerWhitelistTests.cs +++ b/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerWhitelistTests.cs @@ -248,6 +248,10 @@ public static void SignGuardWhitelistsUseTargetSpecificSurface() using var temporaryDirectory = new TemporaryDirectory(); var portableFile = Path.Combine(temporaryDirectory.DirectoryPath, "SignGuardsPortable.cs"); var modernFile = Path.Combine(temporaryDirectory.DirectoryPath, "SignGuardsModern.cs"); + var portableWithoutFactoriesFile = + Path.Combine(temporaryDirectory.DirectoryPath, "MustBePositivePortableWithoutFactories.cs"); + var modernWithoutFactoriesFile = + Path.Combine(temporaryDirectory.DirectoryPath, "MustBePositiveModernWithoutFactories.cs"); var whitelist = CreateWhitelist( includedAssertions: [ @@ -263,15 +267,68 @@ public static void SignGuardWhitelistsUseTargetSpecificSurface() SourceFileMerger.CreateSingleSourceFile( CreateOptions(modernFile, whitelist, SourceTargetFramework.Net10_0) ); + var withoutFactoriesWhitelist = CreateWhitelist( + includedAssertions: [new ("MustBePositive", false)] + ); + SourceFileMerger.CreateSingleSourceFile( + CreateOptions(portableWithoutFactoriesFile, withoutFactoriesWhitelist) + ); + SourceFileMerger.CreateSingleSourceFile( + CreateOptions( + modernWithoutFactoriesFile, + withoutFactoriesWhitelist, + SourceTargetFramework.Net10_0 + ) + ); var portableCode = File.ReadAllText(portableFile); var modernCode = File.ReadAllText(modernFile); + var portableWithoutFactoriesCode = File.ReadAllText(portableWithoutFactoriesFile); + var modernWithoutFactoriesCode = File.ReadAllText(modernWithoutFactoriesFile); + string[] concreteMustBePositiveTypes = + [ + "sbyte", + "byte", + "short", + "ushort", + "int", + "uint", + "long", + "ulong", + "decimal", + "float", + "double", + "TimeSpan", + ]; + + foreach (var type in concreteMustBePositiveTypes) + { + portableCode.Should().Contain($"public static {type} MustBePositive("); + modernCode.Should().Contain($"public static {type} MustBePositive("); + portableCode.Should() + .Contain( + $"MustBePositive(this {type} parameter, Func<{type}, Exception> exceptionFactory)" + ); + modernCode.Should() + .Contain( + $"MustBePositive(this {type} parameter, Func<{type}, Exception> exceptionFactory)" + ); + portableWithoutFactoriesCode.Should().Contain($"public static {type} MustBePositive("); + modernWithoutFactoriesCode.Should().Contain($"public static {type} MustBePositive("); + portableWithoutFactoriesCode.Should() + .NotContain( + $"MustBePositive(this {type} parameter, Func<{type}, Exception> exceptionFactory)" + ); + modernWithoutFactoriesCode.Should() + .NotContain( + $"MustBePositive(this {type} parameter, Func<{type}, Exception> exceptionFactory)" + ); + } - portableCode.Should().Contain("public static int MustBePositive("); portableCode.Should().Contain("public static decimal MustBeNegative("); portableCode.Should().Contain("public static TimeSpan MustNotBeNegative("); portableCode.Should().Contain("public static double MustNotBeZero("); - portableCode.Should().Contain("MustBePositive(this int parameter, Func exceptionFactory)"); portableCode.Should().NotContain("MustNotBeZero(this int parameter, Func exceptionFactory)"); + portableCode.Should().NotContain("public static T MustBePositive("); portableCode.Should().NotContain("INumber"); modernCode.Should().Contain("MustBePositive"); modernCode.Should().Contain("MustBeNegative"); @@ -279,6 +336,12 @@ public static void SignGuardWhitelistsUseTargetSpecificSurface() modernCode.Should().Contain("MustNotBeNegative"); modernCode.Should().Contain("MustNotBeZero"); modernCode.Should().Contain("INumber"); + portableWithoutFactoriesCode.Should().NotContain("public static T MustBePositive("); + modernWithoutFactoriesCode.Should().Contain("public static T MustBePositive("); + modernWithoutFactoriesCode.Should() + .NotContain( + "MustBePositive(this T parameter, Func exceptionFactory)" + ); } [Fact] diff --git a/tests/Light.GuardClauses.Tests/ComparableAssertions/MustBePositiveTests.cs b/tests/Light.GuardClauses.Tests/ComparableAssertions/MustBePositiveTests.cs index 88ea137..55269b3 100644 --- a/tests/Light.GuardClauses.Tests/ComparableAssertions/MustBePositiveTests.cs +++ b/tests/Light.GuardClauses.Tests/ComparableAssertions/MustBePositiveTests.cs @@ -6,6 +6,56 @@ namespace Light.GuardClauses.Tests.ComparableAssertions; public static class MustBePositiveTests { + [Fact] + public static void PositiveSBytesAreAccepted() + { + ((sbyte) 1).MustBePositive().Should().Be((sbyte) 1); + sbyte.MaxValue.MustBePositive().Should().Be(sbyte.MaxValue); + } + + [Fact] + public static void NonPositiveSBytesAreRejected() + { + CheckIntegralIsRejected((sbyte) 0, value => value.MustBePositive()); + CheckIntegralIsRejected(sbyte.MinValue, value => value.MustBePositive()); + } + + [Fact] + public static void PositiveBytesAreAccepted() + { + ((byte) 1).MustBePositive().Should().Be((byte) 1); + byte.MaxValue.MustBePositive().Should().Be(byte.MaxValue); + } + + [Fact] + public static void ZeroByteIsRejected() => + CheckIntegralIsRejected((byte) 0, value => value.MustBePositive()); + + [Fact] + public static void PositiveInt16sAreAccepted() + { + ((short) 1).MustBePositive().Should().Be((short) 1); + short.MaxValue.MustBePositive().Should().Be(short.MaxValue); + } + + [Fact] + public static void NonPositiveInt16sAreRejected() + { + CheckIntegralIsRejected((short) 0, value => value.MustBePositive()); + CheckIntegralIsRejected(short.MinValue, value => value.MustBePositive()); + } + + [Fact] + public static void PositiveUInt16sAreAccepted() + { + ((ushort) 1).MustBePositive().Should().Be((ushort) 1); + ushort.MaxValue.MustBePositive().Should().Be(ushort.MaxValue); + } + + [Fact] + public static void ZeroUInt16IsRejected() => + CheckIntegralIsRejected((ushort) 0, value => value.MustBePositive()); + [Theory] [InlineData(1)] [InlineData(42)] @@ -23,6 +73,17 @@ public static void NonPositiveInt32sAreRejected(int value) act.Should().Throw().WithMessage("*must be positive*"); } + [Fact] + public static void PositiveUInt32sAreAccepted() + { + 1U.MustBePositive().Should().Be(1U); + uint.MaxValue.MustBePositive().Should().Be(uint.MaxValue); + } + + [Fact] + public static void ZeroUInt32IsRejected() => + CheckIntegralIsRejected(0U, value => value.MustBePositive()); + [Theory] [InlineData(1L)] [InlineData(long.MaxValue)] @@ -39,6 +100,17 @@ public static void NonPositiveInt64sAreRejected(long value) act.Should().Throw().WithMessage("*must be positive*"); } + [Fact] + public static void PositiveUInt64sAreAccepted() + { + 1UL.MustBePositive().Should().Be(1UL); + ulong.MaxValue.MustBePositive().Should().Be(ulong.MaxValue); + } + + [Fact] + public static void ZeroUInt64IsRejected() => + CheckIntegralIsRejected(0UL, value => value.MustBePositive()); + [Fact] public static void PositiveDecimalsAreAccepted() { @@ -135,34 +207,79 @@ public static void DefaultExceptionCapturesExpressionAndValue() public static void CustomMessage() => Test.CustomMessage(message => (-1).MustBePositive(message: message)); + [Fact] + public static void NewIntegralOverloadPropagatesParameterNameAndCustomMessage() + { + const uint invalidValue = 0U; + + var act = () => invalidValue.MustBePositive("quantity", "A positive quantity is required."); + + act.Should().Throw() + .WithParameterName("quantity") + .WithMessage("A positive quantity is required.*"); + } + [Fact] public static void CustomFactoriesReceiveValues() { + Test.CustomException((sbyte) -1, (value, factory) => value.MustBePositive(factory)); + Test.CustomException((byte) 0, (value, factory) => value.MustBePositive(factory)); + Test.CustomException((short) -1, (value, factory) => value.MustBePositive(factory)); + Test.CustomException((ushort) 0, (value, factory) => value.MustBePositive(factory)); Test.CustomException(0, (value, factory) => value.MustBePositive(factory)); + Test.CustomException(0U, (value, factory) => value.MustBePositive(factory)); Test.CustomException(-1L, (value, factory) => value.MustBePositive(factory)); + Test.CustomException(0UL, (value, factory) => value.MustBePositive(factory)); Test.CustomException(0m, (value, factory) => value.MustBePositive(factory)); Test.CustomException(float.NaN, (value, factory) => value.MustBePositive(factory)); Test.CustomException(double.NegativeInfinity, (value, factory) => value.MustBePositive(factory)); Test.CustomException(TimeSpan.Zero, (value, factory) => value.MustBePositive(factory)); } + [Fact] + public static void NullFactoriesThrowArgumentNullExceptionForNewIntegralOverloads() + { + CheckNullFactory(() => ((sbyte) 0).MustBePositive((Func) null!)); + CheckNullFactory(() => ((byte) 0).MustBePositive((Func) null!)); + CheckNullFactory(() => ((short) 0).MustBePositive((Func) null!)); + CheckNullFactory(() => ((ushort) 0).MustBePositive((Func) null!)); + CheckNullFactory(() => 0U.MustBePositive((Func) null!)); + CheckNullFactory(() => 0UL.MustBePositive((Func) null!)); + } + #if NET8_0_OR_GREATER [Fact] - public static void GenericOverloadsCoverTypesWithoutConcreteOverloads() + public static void ExplicitGenericOverloadsRemainAvailable() { - ((short) 5).MustBePositive().Should().Be((short) 5); - ((byte) 3).MustBePositive().Should().Be((byte) 3); + Check.MustBePositive((short) 5).Should().Be((short) 5); + Check.MustBePositive((byte) 3).Should().Be((byte) 3); ((Half) 1.5f).MustBePositive().Should().Be((Half) 1.5f); - var zeroShort = () => ((short) 0).MustBePositive(); + var zeroShort = () => Check.MustBePositive((short) 0); var nanHalf = () => Half.NaN.MustBePositive(); zeroShort.Should().Throw().WithMessage("*must be positive*"); nanHalf.Should().Throw().WithMessage("*must be positive*"); - Test.CustomException((short) -3, (value, factory) => value.MustBePositive(factory)); + Test.CustomException( + (short) -3, + (value, factory) => Check.MustBePositive(value, factory) + ); } #endif + private static void CheckIntegralIsRejected(T value, Func guard) + { + var act = () => guard(value); + + act.Should().Throw() + .WithParameterName(nameof(value)) + .WithMessage($"*value must be positive, but it actually is {value}*"); + } + + private static void CheckNullFactory(Action act) => + act.Should().Throw() + .WithParameterName("exceptionFactory"); + private static void CheckDecimalIsRejected(decimal value) { var act = () => value.MustBePositive(); diff --git a/tests/Light.GuardClauses.Tests/ComparableAssertions/NumericCustomFactorySuccessTests.cs b/tests/Light.GuardClauses.Tests/ComparableAssertions/NumericCustomFactorySuccessTests.cs index 0877445..14098ab 100644 --- a/tests/Light.GuardClauses.Tests/ComparableAssertions/NumericCustomFactorySuccessTests.cs +++ b/tests/Light.GuardClauses.Tests/ComparableAssertions/NumericCustomFactorySuccessTests.cs @@ -11,33 +11,57 @@ public static class NumericCustomFactorySuccessTests private static InvalidOperationException FactoryMustNotBeInvoked() => throw new InvalidOperationException("The factory must not be invoked."); + [Fact] + public static void MustBePositive_SByte() => + ((sbyte) 1).MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be((sbyte) 1); + + [Fact] + public static void MustBePositive_Byte() => + ((byte) 2).MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be((byte) 2); + + [Fact] + public static void MustBePositive_Short() => + ((short) 3).MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be((short) 3); + + [Fact] + public static void MustBePositive_UShort() => + ((ushort) 4).MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be((ushort) 4); + [Fact] public static void MustBePositive_Int() => - 1.MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(1); + 5.MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(5); + + [Fact] + public static void MustBePositive_UInt() => + 6U.MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(6U); [Fact] public static void MustBePositive_Long() => - 2L.MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(2L); + 7L.MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(7L); + + [Fact] + public static void MustBePositive_ULong() => + 8UL.MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(8UL); [Fact] public static void MustBePositive_Decimal() => - 3m.MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(3m); + 9m.MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(9m); [Fact] public static void MustBePositive_Float() => - 4f.MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(4f); + 10f.MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(10f); [Fact] public static void MustBePositive_Double() => - 5d.MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(5d); + 11d.MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(11d); [Fact] public static void MustBePositive_TimeSpan() => - TimeSpan.FromTicks(6).MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(TimeSpan.FromTicks(6)); + TimeSpan.FromTicks(12).MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(TimeSpan.FromTicks(12)); [Fact] public static void MustBePositive_Generic() => - ((short) 7).MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(7); + Check.MustBePositive((short) 13, _ => FactoryMustNotBeInvoked()).Should().Be(13); [Fact] public static void MustBeNegative_Int() =>