From 88c4d94f876abeb49f7ee5ecaa9fc8e9c1563198 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Wed, 15 Jul 2026 06:18:26 +0200 Subject: [PATCH] test: increase coverage and quality Signed-off-by: Kenny Pflug --- tests/AGENTS.md | 3 +- .../CodeCoverage.config | 19 ++ .../AdditionalSpanAndMemoryGuardsTests.cs | 125 ++++++-- .../MustContainKeyTests.cs | 16 +- .../CollectionAssertions/MustContainTests.cs | 53 +++- .../MustHaveLengthInTests.cs | 14 +- .../MustHaveSameCountAsTests.cs | 15 + .../MustNotContainKeyTests.cs | 16 +- .../MustNotContainNullOrWhiteSpaceTests.cs | 24 ++ .../MustNotContainNullTests.cs | 12 + .../MustNotContainTests.cs | 67 ++++- .../CommonAssertions/FiniteTests.cs | 104 +++++-- .../CommonAssertions/IsEmptyGuidTests.cs | 14 + .../CommonAssertions/IsValidEnumValueTests.cs | 54 +++- .../CommonAssertions/MustBeTests.cs | 63 ++-- .../MustNotBeEmptyGuidTests.cs | 15 +- .../CommonAssertions/MustNotBeSameAsTests.cs | 20 +- .../CommonAssertions/UuidVersion7Tests.cs | 7 +- .../MustBeApproximatelyTests.cs | 40 ++- .../MustNotBeApproximatelyTests.cs | 18 +- .../NumericCustomFactorySuccessTests.cs | 204 +++++++++---- .../ComparableAssertions/RangeTests.cs | 122 ++++++-- .../DateTimeAssertions/MustBeUtcTests.cs | 30 +- .../InvalidConfigurationExceptionTests.cs | 20 ++ .../StringContainsTests.cs | 41 +++ .../StreamAssertions/StreamGuardTests.cs | 15 +- .../StringAssertions/EqualsTests.cs | 18 +- .../StringAssertions/IsEmailAddressTests.cs | 45 +++ .../MustBeFileExtensionTests.cs | 53 +++- .../StringAssertions/MustBeTests.cs | 57 ++-- .../StringAssertions/MustNotBeTests.cs | 61 ++-- .../StringInspectionGuardTests.cs | 273 ++++++++++++------ .../StringInspectionPredicateTests.cs | 267 ++++++++++------- tests/Light.GuardClauses.Tests/Test.cs | 16 +- .../TypeAssertions/DerivesFromTests.cs | 16 +- .../EquivalentTypeComparerTests.cs | 47 +++ .../EquivalentTypeCompererTests.cs | 51 ---- .../TypeAssertions/ImplementsTests.cs | 18 +- .../MustBeHttpOrHttpsUrlTests.cs | 51 ++++ .../UriAssertions/MustHaveOneSchemeOfTests.cs | 73 ++++- .../UriAssertions/MustHaveSchemeTests.cs | 58 ++-- 41 files changed, 1636 insertions(+), 599 deletions(-) create mode 100644 tests/Light.GuardClauses.Tests/CodeCoverage.config create mode 100644 tests/Light.GuardClauses.Tests/CommonAssertions/IsEmptyGuidTests.cs create mode 100644 tests/Light.GuardClauses.Tests/Exceptions/InvalidConfigurationExceptionTests.cs create mode 100644 tests/Light.GuardClauses.Tests/FrameworkExtensions/StringContainsTests.cs create mode 100644 tests/Light.GuardClauses.Tests/TypeAssertions/EquivalentTypeComparerTests.cs delete mode 100644 tests/Light.GuardClauses.Tests/TypeAssertions/EquivalentTypeCompererTests.cs create mode 100644 tests/Light.GuardClauses.Tests/UriAssertions/MustBeHttpOrHttpsUrlTests.cs diff --git a/tests/AGENTS.md b/tests/AGENTS.md index c68ee7b9..8ca3d97e 100644 --- a/tests/AGENTS.md +++ b/tests/AGENTS.md @@ -10,4 +10,5 @@ # How to run tests - `dotnet test` for usual test runs. -- `dotnet test -- --coverage --coverage-output-format cobertura` for test coverage metrics. +- `dotnet test -- --coverage --coverage-output-format cobertura --coverage-settings CodeCoverage.config` for test coverage metrics (run from tests/Light.GuardClauses.Tests). The settings file excludes JetBrains.Annotations and Regex source generator output from the report. +- Line coverage of 100% is not achievable: the closing brace after a call to a `Throw.*` helper is an unreachable sequence point because these helpers never return (285 such lines as of 2026-07-15). diff --git a/tests/Light.GuardClauses.Tests/CodeCoverage.config b/tests/Light.GuardClauses.Tests/CodeCoverage.config new file mode 100644 index 00000000..9d05672c --- /dev/null +++ b/tests/Light.GuardClauses.Tests/CodeCoverage.config @@ -0,0 +1,19 @@ + + + + + + + ^JetBrains\.Annotations\..* + ^System\.Text\.RegularExpressions\.Generated\..* + + + + diff --git a/tests/Light.GuardClauses.Tests/CollectionAssertions/AdditionalSpanAndMemoryGuardsTests.cs b/tests/Light.GuardClauses.Tests/CollectionAssertions/AdditionalSpanAndMemoryGuardsTests.cs index df77b042..d34c83d5 100644 --- a/tests/Light.GuardClauses.Tests/CollectionAssertions/AdditionalSpanAndMemoryGuardsTests.cs +++ b/tests/Light.GuardClauses.Tests/CollectionAssertions/AdditionalSpanAndMemoryGuardsTests.cs @@ -1,5 +1,6 @@ using System; using FluentAssertions; +using Light.GuardClauses.ExceptionFactory; using Light.GuardClauses.Exceptions; using Xunit; @@ -41,9 +42,9 @@ public static void EmptyGuardsCaptureCallerExpressions() spanAct.Should().Throw().WithParameterName("emptySpan"); readOnlySpanAct.Should().Throw().WithParameterName("emptyReadOnlySpan"); ((Action) (() => emptyMemory.MustNotBeEmpty())).Should().Throw() - .WithParameterName(nameof(emptyMemory)); + .WithParameterName(nameof(emptyMemory)); ((Action) (() => emptyReadOnlyMemory.MustNotBeEmpty())).Should().Throw() - .WithParameterName(nameof(emptyReadOnlyMemory)); + .WithParameterName(nameof(emptyReadOnlyMemory)); } [Fact] @@ -55,6 +56,22 @@ public static void EmptyGuardFactoriesReceiveEveryShape() Test.CustomMemoryException(ReadOnlyMemory.Empty, (value, factory) => value.MustNotBeEmpty(factory)); } + [Fact] + public static void NonEmptyFactoriesDoNotThrowForEveryValidShape() + { + var array = new[] { 1, 2 }; + var span = array.AsSpan(); + ReadOnlySpan readOnlySpan = array; + var memory = array.AsMemory(); + ReadOnlyMemory readOnlyMemory = array; + ReadOnlySpanExceptionFactory factory = _ => new ("The factory should not be invoked."); + + (span.MustNotBeEmpty(factory) == span).Should().BeTrue(); + (readOnlySpan.MustNotBeEmpty(factory) == readOnlySpan).Should().BeTrue(); + memory.MustNotBeEmpty(factory).Should().Be(memory); + readOnlyMemory.MustNotBeEmpty(factory).Should().Be(readOnlyMemory); + } + [Fact] public static void RangedLengthGuardsPreserveBoundariesAndShapes() { @@ -81,14 +98,44 @@ public static void RangedLengthGuardsPreserveBoundariesAndShapes() public static void RangedLengthFactoriesReceiveEveryShape() { var invalidRange = Range.InclusiveBetween(2, 3); - Test.CustomSpanException(Span.Empty, invalidRange, - (value, range, factory) => value.MustHaveLengthIn(range, factory)); - Test.CustomSpanException(ReadOnlySpan.Empty, invalidRange, - (value, range, factory) => value.MustHaveLengthIn(range, factory)); - Test.CustomMemoryException(Memory.Empty, invalidRange, - (value, range, factory) => value.MustHaveLengthIn(range, factory)); - Test.CustomMemoryException(ReadOnlyMemory.Empty, invalidRange, - (value, range, factory) => value.MustHaveLengthIn(range, factory)); + Test.CustomSpanException( + Span.Empty, + invalidRange, + (value, range, factory) => value.MustHaveLengthIn(range, factory) + ); + Test.CustomSpanException( + ReadOnlySpan.Empty, + invalidRange, + (value, range, factory) => value.MustHaveLengthIn(range, factory) + ); + Test.CustomMemoryException( + Memory.Empty, + invalidRange, + (value, range, factory) => value.MustHaveLengthIn(range, factory) + ); + Test.CustomMemoryException( + ReadOnlyMemory.Empty, + invalidRange, + (value, range, factory) => value.MustHaveLengthIn(range, factory) + ); + } + + [Fact] + public static void RangedLengthFactoriesDoNotThrowForEveryValidShape() + { + var array = new[] { 1, 2, 3 }; + var validRange = Range.InclusiveBetween(3, 3); + var span = array.AsSpan(); + ReadOnlySpan readOnlySpan = array; + var memory = array.AsMemory(); + ReadOnlyMemory readOnlyMemory = array; + ReadOnlySpanExceptionFactory> factory = + (_, _) => new ("The factory should not be invoked."); + + (span.MustHaveLengthIn(validRange, factory) == span).Should().BeTrue(); + (readOnlySpan.MustHaveLengthIn(validRange, factory) == readOnlySpan).Should().BeTrue(); + memory.MustHaveLengthIn(validRange, factory).Should().Be(memory); + readOnlyMemory.MustHaveLengthIn(validRange, factory).Should().Be(readOnlyMemory); } [Fact] @@ -112,8 +159,22 @@ public static void MemoryExactLengthFailuresSupportMessagesExpressionsAndFactori .WithParameterName(nameof(memory)) .WithMessage("*custom*"); Test.CustomMemoryException(memory, 1, (value, length, factory) => value.MustHaveLength(length, factory)); - Test.CustomMemoryException(readOnlyMemory, 1, - (value, length, factory) => value.MustHaveLength(length, factory)); + Test.CustomMemoryException( + readOnlyMemory, + 1, + (value, length, factory) => value.MustHaveLength(length, factory) + ); + } + + [Fact] + public static void MemoryExactLengthFactoriesDoNotThrowForValidLengths() + { + var memory = new[] { 1, 2 }.AsMemory(); + ReadOnlyMemory readOnlyMemory = memory; + ReadOnlySpanExceptionFactory factory = (_, _) => new ("The factory should not be invoked."); + + memory.MustHaveLength(2, factory).Should().Be(memory); + readOnlyMemory.MustHaveLength(2, factory).Should().Be(readOnlyMemory); } [Fact] @@ -152,16 +213,40 @@ public static void EmptyAndWhitespaceFailuresUseExistingStringExceptions() .WithMessage("*custom*"); } + [Fact] + public static void WhitespaceFactoriesDoNotThrowForEveryValidShape() + { + var characters = " a".ToCharArray(); + var span = characters.AsSpan(); + ReadOnlySpan readOnlySpan = characters; + var memory = characters.AsMemory(); + ReadOnlyMemory readOnlyMemory = characters; + ReadOnlySpanExceptionFactory factory = _ => new ("The factory should not be invoked."); + + (span.MustNotBeEmptyOrWhiteSpace(factory) == span).Should().BeTrue(); + (readOnlySpan.MustNotBeEmptyOrWhiteSpace(factory) == readOnlySpan).Should().BeTrue(); + memory.MustNotBeEmptyOrWhiteSpace(factory).Should().Be(memory); + readOnlyMemory.MustNotBeEmptyOrWhiteSpace(factory).Should().Be(readOnlyMemory); + } + [Fact] public static void WhitespaceFactoriesReceiveEveryShape() { - Test.CustomSpanException(" ".ToCharArray().AsSpan(), - (value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory)); - Test.CustomSpanException((ReadOnlySpan) " ".ToCharArray(), - (value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory)); - Test.CustomMemoryException(" ".ToCharArray().AsMemory(), - (value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory)); - Test.CustomMemoryException((ReadOnlyMemory) " ".ToCharArray(), - (value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory)); + Test.CustomSpanException( + " ".ToCharArray().AsSpan(), + (value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory) + ); + Test.CustomSpanException( + (ReadOnlySpan) " ".ToCharArray(), + (value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory) + ); + Test.CustomMemoryException( + " ".ToCharArray().AsMemory(), + (value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory) + ); + Test.CustomMemoryException( + (ReadOnlyMemory) " ".ToCharArray(), + (value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory) + ); } } diff --git a/tests/Light.GuardClauses.Tests/CollectionAssertions/MustContainKeyTests.cs b/tests/Light.GuardClauses.Tests/CollectionAssertions/MustContainKeyTests.cs index dc5fd195..bbd50d42 100644 --- a/tests/Light.GuardClauses.Tests/CollectionAssertions/MustContainKeyTests.cs +++ b/tests/Light.GuardClauses.Tests/CollectionAssertions/MustContainKeyTests.cs @@ -19,7 +19,7 @@ public static void KeyNotPresent() { var dictionary = new Dictionary { ["Foo"] = 1, ["Bar"] = 2 }; - Action act = () => dictionary.MustContainKey("Baz", nameof(dictionary)); + Action act = () => dictionary.MustContainKey("Baz"); var assertion = act.Should().Throw().Which; assertion.Message.Should() @@ -41,7 +41,7 @@ public static void InterfaceKeyNotPresent() { IReadOnlyDictionary dictionary = new Dictionary { ["Foo"] = 1 }; - Action act = () => dictionary.MustContainKey("Bar", nameof(dictionary)); + Action act = () => dictionary.MustContainKey("Bar"); act.Should().Throw() .And.Message.Should() @@ -127,7 +127,15 @@ public static void CustomExceptionNotThrown() { var dictionary = new Dictionary { ["Foo"] = 1 }; - dictionary.MustContainKey("Foo", (_, _) => new Exception()).Should().BeSameAs(dictionary); + dictionary.MustContainKey("Foo", (_, _) => new ()).Should().BeSameAs(dictionary); + } + + [Fact] + public static void InterfaceCustomExceptionNotThrown() + { + IReadOnlyDictionary dictionary = new Dictionary { ["Foo"] = 1 }; + + dictionary.MustContainKey("Foo", (_, _) => new ()).Should().BeSameAs(dictionary); } [Fact] @@ -158,7 +166,7 @@ public static void DictionaryShapeIsPreservedInFluentChains() { var map = new Dictionary { ["endpoint"] = "https://example.com" }; - Dictionary result = map.MustNotBeNull().MustContainKey("endpoint"); + var result = map.MustNotBeNull().MustContainKey("endpoint"); result.Should().BeSameAs(map); } diff --git a/tests/Light.GuardClauses.Tests/CollectionAssertions/MustContainTests.cs b/tests/Light.GuardClauses.Tests/CollectionAssertions/MustContainTests.cs index e450f768..9777d4d3 100644 --- a/tests/Light.GuardClauses.Tests/CollectionAssertions/MustContainTests.cs +++ b/tests/Light.GuardClauses.Tests/CollectionAssertions/MustContainTests.cs @@ -15,7 +15,7 @@ public static class MustContainTests [InlineData(new[] { -5491, 6199 }, 42)] public static void ItemNotPartOf(int[] collection, int item) { - Action act = () => collection.MustContain(item, nameof(collection)); + Action act = () => collection.MustContain(item); var assertion = act.Should().Throw().Which; assertion.Message.Should().Contain($"{nameof(collection)} must contain {item}, but it actually does not."); @@ -41,15 +41,44 @@ public static void CollectionNull() [InlineData(new[] { long.MinValue, long.MaxValue }, 42L)] [InlineData(null, 42L)] public static void CustomException(long[] array, long item) => - Test.CustomException(array, - item, - (collection, i, exceptionFactory) => collection.MustContain(i, exceptionFactory)); + Test.CustomException( + array, + item, + (collection, i, exceptionFactory) => collection.MustContain(i, exceptionFactory) + ); [Fact] public static void CustomExceptionNotThrown() { var collection = new List { 1, 2, 3 }; - collection.MustContain(2, (_, _) => new Exception()).Should().BeSameAs(collection); + collection.MustContain(2, (_, _) => new ()).Should().BeSameAs(collection); + } + + [Fact] + public static void LazyEnumerableItemPartOf() + { + var enumerable = new TrackingEnumerable([1, 2, 3], false); + + enumerable.MustContain(2).Should().BeSameAs(enumerable); + } + + [Fact] + public static void LazyEnumerableItemNotPartOf() + { + var enumerable = new TrackingEnumerable([1, 2, 3], false); + + var act = () => enumerable.MustContain(42); + + act.Should().Throw() + .WithParameterName(nameof(enumerable)); + } + + [Fact] + public static void CustomExceptionNotThrownLazyEnumerable() + { + var enumerable = new TrackingEnumerable([1, 2, 3], false); + + enumerable.MustContain(3, (_, _) => new ()).Should().BeSameAs(enumerable); } [Fact] @@ -57,13 +86,15 @@ public static void CustomMessage() => Test.CustomMessage(message => new List().MustContain("Foo", message: message)); [Fact] - public static void CustomMessageCollectionNull() => - Test.CustomMessage(message => ((ObservableCollection) null).MustContain("Foo", message: message)); + public static void CustomMessageCollectionNull() => + Test.CustomMessage( + message => ((ObservableCollection) null).MustContain("Foo", message: message) + ); [Fact] public static void CallerArgumentExpression() { - var array = new [] { "Foo", "Bar" }; + var array = new[] { "Foo", "Bar" }; var act = () => array.MustContain("Baz"); @@ -77,7 +108,7 @@ public static void CallerArgumentExpression() public static void ImmutableArrayItemNotPartOf(int[] source, int item) { var immutableArray = source.ToImmutableArray(); - Action act = () => immutableArray.MustContain(item, nameof(immutableArray)); + Action act = () => immutableArray.MustContain(item); var assertion = act.Should().Throw().Which; assertion.Message.Should().Contain($"{nameof(immutableArray)} must contain {item}, but it actually does not."); @@ -119,7 +150,7 @@ public static void ImmutableArrayCustomException(long[] source, long item) public static void ImmutableArrayCustomExceptionNotThrown() { var immutableArray = ImmutableArray.Create(1, 2, 3); - immutableArray.MustContain(2, (_, _) => new Exception()).Should().Equal(immutableArray); + immutableArray.MustContain(2, (_, _) => new ()).Should().Equal(immutableArray); } [Fact] @@ -138,4 +169,4 @@ public static void ImmutableArrayCallerArgumentExpression() act.Should().Throw() .WithParameterName(nameof(immutableArray)); } -} \ No newline at end of file +} diff --git a/tests/Light.GuardClauses.Tests/CollectionAssertions/MustHaveLengthInTests.cs b/tests/Light.GuardClauses.Tests/CollectionAssertions/MustHaveLengthInTests.cs index 4c1e15e6..c5c2c552 100644 --- a/tests/Light.GuardClauses.Tests/CollectionAssertions/MustHaveLengthInTests.cs +++ b/tests/Light.GuardClauses.Tests/CollectionAssertions/MustHaveLengthInTests.cs @@ -25,7 +25,7 @@ public static void LengthInRange(ImmutableArray array, Range range) => [MemberData(nameof(LengthNotInRangeData))] public static void LengthNotInRange(ImmutableArray array, Range range) { - var act = () => array.MustHaveLengthIn(range, nameof(array)); + var act = () => array.MustHaveLengthIn(range); act.Should().Throw() .And.Message.Should().Contain($"must have its length in between {range.CreateRangeDescriptionText("and")}") @@ -49,6 +49,16 @@ public static void CustomException() => (array, r, exceptionFactory) => array.MustHaveLengthIn(r, exceptionFactory) ); + [Fact] + public static void CustomExceptionNotThrown() + { + var array = ImmutableArray.Create(1, 2, 3); + + var result = array.MustHaveLengthIn(Range.InclusiveBetween(1, 3), (_, _) => new ()); + + result.Should().Equal(array); + } + [Fact] public static void CustomMessage() => Test.CustomMessage( @@ -84,7 +94,7 @@ public static void DefaultImmutableArrayNotInRange() { var defaultArray = default(ImmutableArray); - var act = () => defaultArray.MustHaveLengthIn(Range.FromInclusive(1).ToInclusive(5), nameof(defaultArray)); + var act = () => defaultArray.MustHaveLengthIn(Range.FromInclusive(1).ToInclusive(5)); act.Should().Throw() .And.Message.Should().Contain("must have its length in between 1 (inclusive) and 5 (inclusive)") diff --git a/tests/Light.GuardClauses.Tests/CollectionAssertions/MustHaveSameCountAsTests.cs b/tests/Light.GuardClauses.Tests/CollectionAssertions/MustHaveSameCountAsTests.cs index 352ccdc9..058f085e 100644 --- a/tests/Light.GuardClauses.Tests/CollectionAssertions/MustHaveSameCountAsTests.cs +++ b/tests/Light.GuardClauses.Tests/CollectionAssertions/MustHaveSameCountAsTests.cs @@ -54,6 +54,7 @@ public static void NullReceiverUsesCapturedReceiverExpression() { int[] values = null; + // ReSharper disable once ExpressionIsAlwaysNull Action act = () => values.MustHaveSameCountAs(Array.Empty()); act.Should().Throw().WithParameterName(nameof(values)); @@ -65,6 +66,7 @@ public static void NullComparisonCollectionUsesSecondaryArgumentNameWithoutObser var values = new TrackingEnumerable([1]); string[] other = null; + // ReSharper disable once ExpressionIsAlwaysNull Action act = () => values.MustHaveSameCountAs(other); act.Should().Throw().WithParameterName("otherCollection"); @@ -113,6 +115,19 @@ public static void CustomFactoryReceivesOriginalValuesForEveryFailure(bool recei invocationCount.Should().Be(1); } + [Fact] + public static void CustomFactoryIsNotInvokedWhenComparingACollectionWithItself() + { + var collection = new TrackingEnumerable([1, 2]); + + collection.MustHaveSameCountAs( + collection, + (_, _) => throw new InvalidOperationException("The factory must not be invoked.") + ) + .Should().BeSameAs(collection); + collection.EnumeratorCount.Should().Be(0); + } + [Fact] public static void CustomFactoryIsNotInvokedForEqualCounts() { diff --git a/tests/Light.GuardClauses.Tests/CollectionAssertions/MustNotContainKeyTests.cs b/tests/Light.GuardClauses.Tests/CollectionAssertions/MustNotContainKeyTests.cs index 3972f770..dba42e24 100644 --- a/tests/Light.GuardClauses.Tests/CollectionAssertions/MustNotContainKeyTests.cs +++ b/tests/Light.GuardClauses.Tests/CollectionAssertions/MustNotContainKeyTests.cs @@ -19,7 +19,7 @@ public static void KeyPresent() { var dictionary = new Dictionary { ["Foo"] = 1, ["Bar"] = 2 }; - Action act = () => dictionary.MustNotContainKey("Foo", nameof(dictionary)); + Action act = () => dictionary.MustNotContainKey("Foo"); act.Should().Throw() .And.Message.Should() @@ -39,7 +39,7 @@ public static void InterfaceKeyPresent() { IReadOnlyDictionary dictionary = new Dictionary { ["Foo"] = 1 }; - Action act = () => dictionary.MustNotContainKey("Foo", nameof(dictionary)); + Action act = () => dictionary.MustNotContainKey("Foo"); act.Should().Throw() .And.Message.Should() @@ -125,7 +125,15 @@ public static void CustomExceptionNotThrown() { var dictionary = new Dictionary { ["Foo"] = 1 }; - dictionary.MustNotContainKey("Bar", (_, _) => new Exception()).Should().BeSameAs(dictionary); + dictionary.MustNotContainKey("Bar", (_, _) => new ()).Should().BeSameAs(dictionary); + } + + [Fact] + public static void InterfaceCustomExceptionNotThrown() + { + IReadOnlyDictionary dictionary = new Dictionary { ["Foo"] = 1 }; + + dictionary.MustNotContainKey("Bar", (_, _) => new ()).Should().BeSameAs(dictionary); } [Fact] @@ -156,7 +164,7 @@ public static void DictionaryShapeIsPreservedInFluentChains() { var map = new Dictionary { ["endpoint"] = "https://example.com" }; - Dictionary result = map.MustNotBeNull().MustNotContainKey("proxy"); + var result = map.MustNotBeNull().MustNotContainKey("proxy"); result.Should().BeSameAs(map); } diff --git a/tests/Light.GuardClauses.Tests/CollectionAssertions/MustNotContainNullOrWhiteSpaceTests.cs b/tests/Light.GuardClauses.Tests/CollectionAssertions/MustNotContainNullOrWhiteSpaceTests.cs index 5d0f9861..9ac66980 100644 --- a/tests/Light.GuardClauses.Tests/CollectionAssertions/MustNotContainNullOrWhiteSpaceTests.cs +++ b/tests/Light.GuardClauses.Tests/CollectionAssertions/MustNotContainNullOrWhiteSpaceTests.cs @@ -159,6 +159,18 @@ public static void InvalidImmutableArrayThrowsAtItsPosition() .WithMessage("*position 1*"); } + [Fact] + public static void InvalidReadOnlyIndexableReceiverThrowsAtItsPosition() + { + var values = new ReadOnlyIndexableOnlyList(["Alpha", " ", "Gamma"]); + + Action act = () => values.MustNotContainNullOrWhiteSpace(); + + act.Should().Throw() + .WithParameterName(nameof(values)) + .WithMessage("*position 1*"); + } + [Fact] public static void ImmutableArrayCustomFactoryReceivesOriginalShape() => Test.CustomException( @@ -166,6 +178,18 @@ public static void ImmutableArrayCustomFactoryReceivesOriginalShape() => (values, factory) => values.MustNotContainNullOrWhiteSpace(factory) ); + [Fact] + public static void ValidImmutableArrayDoesNotInvokeCustomFactory() + { + var values = ImmutableArray.Create("Alpha", "Beta"); + + var result = values.MustNotContainNullOrWhiteSpace( + _ => new InvalidOperationException("The factory must not be invoked.") + ); + + result.Should().Equal(values); + } + [Fact] public static void DefaultImmutableArrayDoesNotInvokeCustomFactory() { diff --git a/tests/Light.GuardClauses.Tests/CollectionAssertions/MustNotContainNullTests.cs b/tests/Light.GuardClauses.Tests/CollectionAssertions/MustNotContainNullTests.cs index 7d355b19..bd8e1891 100644 --- a/tests/Light.GuardClauses.Tests/CollectionAssertions/MustNotContainNullTests.cs +++ b/tests/Light.GuardClauses.Tests/CollectionAssertions/MustNotContainNullTests.cs @@ -179,6 +179,18 @@ public static void ImmutableArrayCustomFactoryReceivesOriginalShape() => (values, factory) => values.MustNotContainNull(factory) ); + [Fact] + public static void ValidImmutableArrayDoesNotInvokeCustomFactory() + { + var values = ImmutableArray.Create("Alpha", "Beta"); + + var result = values.MustNotContainNull( + _ => new InvalidOperationException("The factory must not be invoked.") + ); + + result.Should().Equal(values); + } + [Fact] public static void DefaultImmutableArrayDoesNotInvokeCustomFactory() { diff --git a/tests/Light.GuardClauses.Tests/CollectionAssertions/MustNotContainTests.cs b/tests/Light.GuardClauses.Tests/CollectionAssertions/MustNotContainTests.cs index 3f4b8172..fee70ac7 100644 --- a/tests/Light.GuardClauses.Tests/CollectionAssertions/MustNotContainTests.cs +++ b/tests/Light.GuardClauses.Tests/CollectionAssertions/MustNotContainTests.cs @@ -17,10 +17,12 @@ public static class MustNotContainTests [InlineData(new[] { "Corge", "Grault", null }, null)] public static void ItemExists(string[] collection, string item) { - Action act = () => collection.MustNotContain(item, nameof(collection)); + Action act = () => collection.MustNotContain(item); var assertions = act.Should().Throw().Which; - assertions.Message.Should().Contain($"{nameof(collection)} must not contain {item.ToStringOrNull()}, but it actually does."); + assertions.Message.Should().Contain( + $"{nameof(collection)} must not contain {item.ToStringOrNull()}, but it actually does." + ); assertions.ParamName.Should().BeSameAs(nameof(collection)); } @@ -34,37 +36,72 @@ public static void ItemExistsNot(int[] collection, int item) => [Fact] public static void CollectionNull() { - Action act = () => ((ObservableCollection)null).MustNotContain(new object()); + Action act = () => ((ObservableCollection) null).MustNotContain(new object()); act.Should().Throw(); } [Fact] public static void CustomException() => - Test.CustomException(new List { "Foo" }, - "Foo", - (collection, value, exceptionFactory) => collection.MustNotContain(value, exceptionFactory)); + Test.CustomException( + new List { "Foo" }, + "Foo", + (collection, value, exceptionFactory) => collection.MustNotContain(value, exceptionFactory) + ); [Fact] public static void CustomExceptionCollectionNull() => - Test.CustomException((Collection)null, - 42, - (collection, i, exceptionFactory) => collection.MustNotContain(i, exceptionFactory)); + Test.CustomException( + (Collection) null, + 42, + (collection, i, exceptionFactory) => collection.MustNotContain(i, exceptionFactory) + ); [Fact] public static void NoCustomExceptionThrown() { var collection = new[] { 1, 2 }; - collection.MustNotContain(3, (_, _) => new Exception()).Should().BeSameAs(collection); + collection.MustNotContain(3, (_, _) => new ()).Should().BeSameAs(collection); + } + + [Fact] + public static void LazyEnumerableItemExists() + { + var enumerable = new TrackingEnumerable([1, 2, 3], false); + + var act = () => enumerable.MustNotContain(2); + + act.Should().Throw() + .WithParameterName(nameof(enumerable)); + } + + [Fact] + public static void LazyEnumerableItemExistsNot() + { + var enumerable = new TrackingEnumerable([1, 2, 3], false); + + enumerable.MustNotContain(42).Should().BeSameAs(enumerable); + } + + [Fact] + public static void NoCustomExceptionThrownLazyEnumerable() + { + var enumerable = new TrackingEnumerable([1, 2, 3], false); + + enumerable.MustNotContain(42, (_, _) => new ()).Should().BeSameAs(enumerable); } [Fact] public static void CustomMessage() => - Test.CustomMessage(message => new HashSet { 42 }.MustNotContain(42, message: message)); + Test.CustomMessage( + message => new HashSet { 42 }.MustNotContain(42, message: message) + ); [Fact] public static void CustomMessageCollectionNull() => - Test.CustomMessage(message => ((List)null).MustNotContain(false, message: message)); + Test.CustomMessage( + message => ((List) null).MustNotContain(false, message: message) + ); [Fact] public static void CallerArgumentExpression() @@ -85,7 +122,7 @@ public static void ImmutableArrayItemExists(string[] items, string item) { var array = ImmutableArray.Create(items); - Action act = () => array.MustNotContain(item, nameof(array)); + Action act = () => array.MustNotContain(item); var assertions = act.Should().Throw().Which; assertions.Message.Should() @@ -172,7 +209,7 @@ public static void ImmutableArrayDefaultInstanceCustomMessage() var defaultArray = default(ImmutableArray); // Default instance should not throw even with custom message - var result = defaultArray.MustNotContain(new object(), message: "Custom message"); + var result = defaultArray.MustNotContain(new (), message: "Custom message"); result.IsDefault.Should().BeTrue(); } @@ -187,4 +224,4 @@ public static void ImmutableArrayDefaultInstanceCallerArgumentExpression() result.IsDefault.Should().BeTrue(); } -} \ No newline at end of file +} diff --git a/tests/Light.GuardClauses.Tests/CommonAssertions/FiniteTests.cs b/tests/Light.GuardClauses.Tests/CommonAssertions/FiniteTests.cs index 8e53920d..93eb8373 100644 --- a/tests/Light.GuardClauses.Tests/CommonAssertions/FiniteTests.cs +++ b/tests/Light.GuardClauses.Tests/CommonAssertions/FiniteTests.cs @@ -11,41 +11,48 @@ public static class FiniteTests [InlineData(double.Epsilon)] [InlineData(double.MinValue)] [InlineData(double.MaxValue)] - public static void FiniteDoublesAreAccepted(double value) - { - value.IsFinite().Should().BeTrue(); - value.MustBeFinite().Should().Be(value); - } + public static void IsFinite_FiniteDoublesAreAccepted(double value) => value.IsFinite().Should().BeTrue(); + + [Fact] + public static void IsFinite_NegativeZeroDoubleIsAccepted() => (-0d).IsFinite().Should().BeTrue(); [Theory] [InlineData(double.NaN)] [InlineData(double.PositiveInfinity)] [InlineData(double.NegativeInfinity)] - public static void NonFiniteDoublesAreRejected(double value) => value.IsFinite().Should().BeFalse(); + public static void IsFinite_NonFiniteDoublesAreRejected(double value) => value.IsFinite().Should().BeFalse(); [Theory] [InlineData(0f)] [InlineData(float.Epsilon)] [InlineData(float.MinValue)] [InlineData(float.MaxValue)] - public static void FiniteFloatsAreAccepted(float value) - { - value.IsFinite().Should().BeTrue(); - value.MustBeFinite().Should().Be(value); - } + public static void IsFinite_FiniteFloatsAreAccepted(float value) => value.IsFinite().Should().BeTrue(); + + [Fact] + public static void IsFinite_NegativeZeroFloatIsAccepted() => (-0f).IsFinite().Should().BeTrue(); [Theory] [InlineData(float.NaN)] [InlineData(float.PositiveInfinity)] [InlineData(float.NegativeInfinity)] - public static void NonFiniteFloatsAreRejected(float value) => value.IsFinite().Should().BeFalse(); + public static void IsFinite_NonFiniteFloatsAreRejected(float value) => value.IsFinite().Should().BeFalse(); - [Fact] - public static void NegativeZerosAreFinite() - { - (-0d).IsFinite().Should().BeTrue(); - (-0f).IsFinite().Should().BeTrue(); - } + [Theory] + [InlineData(0d)] + [InlineData(double.Epsilon)] + [InlineData(double.MinValue)] + [InlineData(double.MaxValue)] + public static void MustBeFinite_FiniteDoublesAreReturned(double value) => + value.MustBeFinite().Should().Be(value); + + [Theory] + [InlineData(0f)] + [InlineData(float.Epsilon)] + [InlineData(float.MinValue)] + [InlineData(float.MaxValue)] + public static void MustBeFinite_FiniteFloatsAreReturned(float value) => + value.MustBeFinite().Should().Be(value); [Fact] public static void DefaultExceptionCapturesExpressionAndValue() @@ -61,26 +68,65 @@ public static void DefaultExceptionCapturesExpressionAndValue() [Fact] public static void CustomMessage() => - Test.CustomMessage(message => float.PositiveInfinity.MustBeFinite(message: message)); + Test.CustomMessage( + message => float.PositiveInfinity.MustBeFinite(message: message) + ); [Fact] - public static void CustomFactoriesReceiveValues() - { + public static void CustomFactoryReceivesValue_Float() => Test.CustomException(float.NaN, (value, factory) => value.MustBeFinite(factory)); + + [Fact] + public static void CustomFactoryReceivesValue_Double() => Test.CustomException(double.NegativeInfinity, (value, factory) => value.MustBeFinite(factory)); - } + + [Fact] + public static void NoCustomExceptionThrown_Double() => + 42.5.MustBeFinite(_ => null).Should().Be(42.5); + + [Fact] + public static void NoCustomExceptionThrown_Float() => + 42.5f.MustBeFinite(_ => null).Should().Be(42.5f); #if NET8_0_OR_GREATER [Fact] - public static void GenericHalfOverloadsAreAvailable() + public static void IsFinite_FiniteHalfIsAccepted() => + ((Half) 1.5f).IsFinite().Should().BeTrue(); + + [Fact] + public static void IsFinite_NonFiniteHalfIsRejected() => + Half.PositiveInfinity.IsFinite().Should().BeFalse(); + + [Fact] + public static void MustBeFinite_FiniteHalfIsReturned() + { + var finiteHalf = (Half) 1.5f; + + finiteHalf.MustBeFinite().Should().Be(finiteHalf); + } + + [Fact] + public static void CustomFactoryReceivesValue_Generic() => + Test.CustomException(Half.PositiveInfinity, (value, factory) => value.MustBeFinite(factory)); + + [Fact] + public static void DefaultExceptionCapturesExpressionAndValue_Generic() + { + var nonFiniteHalf = Half.NaN; + + var act = () => nonFiniteHalf.MustBeFinite(); + + act.Should().Throw() + .WithParameterName(nameof(nonFiniteHalf)) + .WithMessage("*must be finite*"); + } + + [Fact] + public static void NoCustomExceptionThrown_Generic() { - var finite = (Half) 1.5f; - var nonFinite = Half.PositiveInfinity; + var finiteHalf = (Half) 1.5f; - Check.IsFinite(finite).Should().BeTrue(); - Check.IsFinite(nonFinite).Should().BeFalse(); - Check.MustBeFinite(finite).Should().Be(finite); - Test.CustomException(nonFinite, (value, factory) => Check.MustBeFinite(value, factory)); + finiteHalf.MustBeFinite(_ => null).Should().Be(finiteHalf); } #endif } diff --git a/tests/Light.GuardClauses.Tests/CommonAssertions/IsEmptyGuidTests.cs b/tests/Light.GuardClauses.Tests/CommonAssertions/IsEmptyGuidTests.cs new file mode 100644 index 00000000..a3e581aa --- /dev/null +++ b/tests/Light.GuardClauses.Tests/CommonAssertions/IsEmptyGuidTests.cs @@ -0,0 +1,14 @@ +using System; +using FluentAssertions; +using Xunit; + +namespace Light.GuardClauses.Tests.CommonAssertions; + +public static class IsEmptyGuidTests +{ + [Fact] + public static void EmptyGuid() => Guid.Empty.IsEmpty().Should().BeTrue(); + + [Fact] + public static void NonEmptyGuid() => Guid.NewGuid().IsEmpty().Should().BeFalse(); +} diff --git a/tests/Light.GuardClauses.Tests/CommonAssertions/IsValidEnumValueTests.cs b/tests/Light.GuardClauses.Tests/CommonAssertions/IsValidEnumValueTests.cs index aae83741..4802d79f 100644 --- a/tests/Light.GuardClauses.Tests/CommonAssertions/IsValidEnumValueTests.cs +++ b/tests/Light.GuardClauses.Tests/CommonAssertions/IsValidEnumValueTests.cs @@ -20,20 +20,14 @@ public static class IsValidEnumValueTests [InlineData(UInt64Enum.AllLow | UInt64Enum.High1)] [InlineData(UInt64Enum.AllHigh)] [InlineData(UInt64Enum.MaxValue)] - public static void EnumValueValid(T enumValue) where T : struct, Enum, IComparable - { - if (enumValue is BindingFlags) - { - - } - + public static void EnumValueValid(T enumValue) where T : struct, Enum, IComparable => enumValue.IsValidEnumValue().Should().BeTrue(); - } [Theory] [InlineData(2000)] [InlineData(-5)] - public static void EnumValueInvalid(int invalidDateTimeKindValue) => ((DateTimeKind) invalidDateTimeKindValue).IsValidEnumValue().Should().BeFalse(); + public static void EnumValueInvalid(int invalidDateTimeKindValue) => + ((DateTimeKind) invalidDateTimeKindValue).IsValidEnumValue().Should().BeFalse(); [Theory] [InlineData(-1)] @@ -44,7 +38,8 @@ public static void EnumValueValid(T enumValue) where T : struct, Enum, ICompa [InlineData(2055)] [InlineData(4096)] [InlineData(int.MaxValue)] - public static void InvalidNumberStyles(int invalidValue) => ((NumberStyles) invalidValue).IsValidEnumValue().Should().BeFalse(); + public static void InvalidNumberStyles(int invalidValue) => + ((NumberStyles) invalidValue).IsValidEnumValue().Should().BeFalse(); [Flags] public enum UInt64Enum : ulong @@ -59,7 +54,41 @@ public enum UInt64Enum : ulong High3 = 1 << 61, High4 = 1 << 62, AllHigh = High1 | High2 | High3 | High4, - MaxValue = ulong.MaxValue + MaxValue = ulong.MaxValue, + } + + [Theory] + [InlineData(ByteFlags.One)] + [InlineData(ByteFlags.One | ByteFlags.Two)] + [InlineData(ByteFlags.One | ByteFlags.Four)] + public static void ByteSizedFlagsEnumValueValid(ByteFlags value) => value.IsValidEnumValue().Should().BeTrue(); + + [Fact] + public static void ByteSizedFlagsEnumValueInvalid() => ((ByteFlags) 8).IsValidEnumValue().Should().BeFalse(); + + [Flags] + public enum ByteFlags : byte + { + One = 1, + Two = 2, + Four = 4, + } + + [Theory] + [InlineData(UInt16Flags.One)] + [InlineData(UInt16Flags.One | UInt16Flags.Two)] + [InlineData(UInt16Flags.Two | UInt16Flags.Four)] + public static void UInt16SizedFlagsEnumValueValid(UInt16Flags value) => value.IsValidEnumValue().Should().BeTrue(); + + [Fact] + public static void UInt16SizedFlagsEnumValueInvalid() => ((UInt16Flags) 8).IsValidEnumValue().Should().BeFalse(); + + [Flags] + public enum UInt16Flags : ushort + { + One = 1, + Two = 2, + Four = 4, } [Fact] @@ -72,5 +101,4 @@ public enum EmptyEnum { } [Flags] public enum EmptyFlagsEnum { } - -} \ No newline at end of file +} diff --git a/tests/Light.GuardClauses.Tests/CommonAssertions/MustBeTests.cs b/tests/Light.GuardClauses.Tests/CommonAssertions/MustBeTests.cs index 063c031a..87f03489 100644 --- a/tests/Light.GuardClauses.Tests/CommonAssertions/MustBeTests.cs +++ b/tests/Light.GuardClauses.Tests/CommonAssertions/MustBeTests.cs @@ -14,10 +14,12 @@ public static class MustBeTests [InlineData("Baz", "Qux")] public static void ValuesNotEqual(string x, string y) { - Action act = () => x.MustBe(y, nameof(x)); + Action act = () => x.MustBe(y); act.Should().Throw() - .And.Message.Should().Contain($"{nameof(x)} must be equal to {y.ToStringOrNull()}, but it actually is {x.ToStringOrNull()}."); + .And.Message.Should().Contain( + $"{nameof(x)} must be equal to {y.ToStringOrNull()}, but it actually is {x.ToStringOrNull()}." + ); } [Theory] @@ -28,13 +30,15 @@ public static void ValuesNotEqual(string x, string y) [Fact] public static void CustomException() => - Test.CustomException("Foo", - "Bar", - (x, y, exceptionFactory) => x.MustBe(y, exceptionFactory)); + Test.CustomException( + "Foo", + "Bar", + (x, y, exceptionFactory) => x.MustBe(y, exceptionFactory) + ); [Fact] - public static void CustomExceptionValuesEqual() => - 87.MustBe(87, (_, _) => new Exception()).MustBe(87); + public static void CustomExceptionValuesEqual() => + 87.MustBe(87, (_, _) => new ()).MustBe(87); [Fact] public static void CustomMessage() => @@ -48,34 +52,49 @@ public static void ValuesNotEqualCustomEqualityComparer() Action act = () => myString.MustBe("Bar", new EqualityComparerStub(false)); act.Should().Throw() - .And.Message.Should().Contain($"myString must be equal to {"Bar".ToStringOrNull()}, but it actually is {"Foo".ToStringOrNull()}."); + .And.Message.Should().Contain( + $"myString must be equal to {"Bar".ToStringOrNull()}, but it actually is {"Foo".ToStringOrNull()}." + ); } [Fact] - public static void ValuesEqualCustomEqualityComparer() => 42.MustBe(42, new EqualityComparerStub(true)).Should().Be(42); + public static void ValuesEqualCustomEqualityComparer() => + 42.MustBe(42, new EqualityComparerStub(true)).Should().Be(42); [Fact] public static void CustomExceptionEqualityComparer() => - Test.CustomException("Foo", - "Bar", - (IEqualityComparer) new EqualityComparerStub(false), - (x, y, comparer, exceptionFactory) => x.MustBe(y, comparer, exceptionFactory)); + Test.CustomException( + "Foo", + "Bar", + (IEqualityComparer) new EqualityComparerStub(false), + (x, y, comparer, exceptionFactory) => x.MustBe(y, comparer, exceptionFactory) + ); [Fact] - public static void CustomExceptionEqualityComparerNull() => - Test.CustomException(35L, - 22L, - (IEqualityComparer) null, - (x, y, comparer, exceptionFactory) => x.MustBe(y, comparer, exceptionFactory)); + public static void CustomExceptionEqualityComparerNull() => + Test.CustomException( + 35L, + 22L, + (IEqualityComparer) null, + (x, y, comparer, exceptionFactory) => x.MustBe(y, comparer, exceptionFactory) + ); + + [Fact] + public static void CustomExceptionEqualityComparerValuesEqual() => + 42.MustBe(42, new EqualityComparerStub(true), (_, _, _) => null).Should().Be(42); [Fact] public static void CustomMessageEqualityComparer() => - Test.CustomMessage(message => 99m.MustBe(100m, new EqualityComparerStub(false), message: message)); + Test.CustomMessage( + message => 99m.MustBe(100m, new EqualityComparerStub(false), message: message) + ); [Fact] - public static void CustomMessageEqualityComparerNull() => + public static void CustomMessageEqualityComparerNull() => // ReSharper disable once AssignNullToNotNullAttribute - Test.CustomMessage(message => 42.MustBe(42, (IEqualityComparer) null, message: message)); + Test.CustomMessage( + message => 42.MustBe(42, (IEqualityComparer) null, message: message) + ); [Fact] public static void CallerArgumentExpression() @@ -98,4 +117,4 @@ public static void CallerArgumentExpressionForEqualityComparerOverload() act.Should().Throw() .WithParameterName(nameof(seven)); } -} \ No newline at end of file +} diff --git a/tests/Light.GuardClauses.Tests/CommonAssertions/MustNotBeEmptyGuidTests.cs b/tests/Light.GuardClauses.Tests/CommonAssertions/MustNotBeEmptyGuidTests.cs index 5aabdb15..c9aeb779 100644 --- a/tests/Light.GuardClauses.Tests/CommonAssertions/MustNotBeEmptyGuidTests.cs +++ b/tests/Light.GuardClauses.Tests/CommonAssertions/MustNotBeEmptyGuidTests.cs @@ -12,7 +12,7 @@ public static void GuidEmpty() { var emptyGuid = Guid.Empty; - Action act = () => emptyGuid.MustNotBeEmpty(nameof(emptyGuid)); + Action act = () => emptyGuid.MustNotBeEmpty(); act.Should().Throw() .WithParameterName(nameof(emptyGuid)); @@ -32,6 +32,14 @@ public static void GuidNotEmpty() public static void CustomException() => Test.CustomException(exceptionFactory => Guid.Empty.MustNotBeEmpty(exceptionFactory)); + [Fact] + public static void CustomExceptionGuidNotEmpty() + { + var validGuid = Guid.NewGuid(); + + validGuid.MustNotBeEmpty(() => null).Should().Be(validGuid); + } + [Fact] public static void CustomMessage() => Test.CustomMessage(message => Guid.Empty.MustNotBeEmpty(message: message)); @@ -47,11 +55,11 @@ public static void CallerArgumentExpression() .WithParameterName(nameof(emptyGuid)); } - public class Entity(Guid id) + private sealed class Entity(Guid id) { public Guid Id { get; } = id.MustNotBeEmpty(); } - + [Fact] public static void PrimaryConstructorValidArgument() { @@ -70,4 +78,3 @@ public static void PrimaryConstructorInvalidArgument() .And.Message.Should().StartWith("id must be a valid GUID, but it actually is an empty one."); } } - diff --git a/tests/Light.GuardClauses.Tests/CommonAssertions/MustNotBeSameAsTests.cs b/tests/Light.GuardClauses.Tests/CommonAssertions/MustNotBeSameAsTests.cs index 17543cc3..aaef2979 100644 --- a/tests/Light.GuardClauses.Tests/CommonAssertions/MustNotBeSameAsTests.cs +++ b/tests/Light.GuardClauses.Tests/CommonAssertions/MustNotBeSameAsTests.cs @@ -12,10 +12,12 @@ public static class MustNotBeSameAsTests [InlineData("Bar")] public static void ReferencesEqual(string reference) { - Action act = () => reference.MustNotBeSameAs(reference, nameof(reference)); + Action act = () => reference.MustNotBeSameAs(reference); act.Should().Throw() - .And.Message.Should().Contain($"{nameof(reference)} must not point to object \"{reference}\", but it actually does."); + .And.Message.Should().Contain( + $"{nameof(reference)} must not point to object \"{reference}\", but it actually does." + ); } [Theory] @@ -29,9 +31,15 @@ public static void ReferencesDifferent(string first, string second) } [Fact] - public static void CustomException() => - Test.CustomException("Baz", - (reference, exceptionFactory) => reference.MustNotBeSameAs(reference, exceptionFactory)); + public static void CustomException() => + Test.CustomException( + "Baz", + (reference, exceptionFactory) => reference.MustNotBeSameAs(reference, exceptionFactory) + ); + + [Fact] + public static void CustomExceptionReferencesDifferent() => + "Foo".MustNotBeSameAs("Bar", _ => null).Should().Be("Foo"); [Fact] public static void CustomMessage() => @@ -47,4 +55,4 @@ public static void CallerArgumentExpression() act.Should().Throw() .WithParameterName(nameof(object1)); } -} \ No newline at end of file +} diff --git a/tests/Light.GuardClauses.Tests/CommonAssertions/UuidVersion7Tests.cs b/tests/Light.GuardClauses.Tests/CommonAssertions/UuidVersion7Tests.cs index b6898a5d..7888f180 100644 --- a/tests/Light.GuardClauses.Tests/CommonAssertions/UuidVersion7Tests.cs +++ b/tests/Light.GuardClauses.Tests/CommonAssertions/UuidVersion7Tests.cs @@ -14,7 +14,8 @@ public static class UuidVersion7Tests #if NET9_0_OR_GREATER [Fact] - public static void FrameworkGeneratedValueIsUuidVersion7() => Guid.CreateVersion7().IsUuidVersion7().Should().BeTrue(); + public static void FrameworkGeneratedValueIsUuidVersion7() => + Guid.CreateVersion7().IsUuidVersion7().Should().BeTrue(); #endif [Theory] @@ -45,4 +46,8 @@ public static void CustomMessage() => [Fact] public static void CustomFactoryReceivesValue() => Test.CustomException(WrongVariant, (value, factory) => value.MustBeUuidVersion7(factory)); + + [Fact] + public static void CustomFactoryNotInvokedForValidValue() => + RfcExample.MustBeUuidVersion7(_ => null).Should().Be(RfcExample); } diff --git a/tests/Light.GuardClauses.Tests/ComparableAssertions/MustBeApproximatelyTests.cs b/tests/Light.GuardClauses.Tests/ComparableAssertions/MustBeApproximatelyTests.cs index 20e87f24..ddf2668e 100644 --- a/tests/Light.GuardClauses.Tests/ComparableAssertions/MustBeApproximatelyTests.cs +++ b/tests/Light.GuardClauses.Tests/ComparableAssertions/MustBeApproximatelyTests.cs @@ -29,7 +29,7 @@ public static void ValuesApproximatelyEqual_Float(float value, float other, floa [InlineData(0.0001, 0.0002, 0.00005)] public static void ValuesNotApproximatelyEqual_Double(double value, double other, double tolerance) { - var act = () => value.MustBeApproximately(other, tolerance, nameof(value)); + var act = () => value.MustBeApproximately(other, tolerance); var exceptionAssertion = act.Should().Throw().Which; exceptionAssertion.Message.Should().Contain( @@ -45,7 +45,7 @@ public static void ValuesNotApproximatelyEqual_Double(double value, double other [InlineData(0.0001f, 0.0002f, 0.00005f)] public static void ValuesNotApproximatelyEqual_Float(float value, float other, float tolerance) { - var act = () => value.MustBeApproximately(other, tolerance, nameof(value)); + var act = () => value.MustBeApproximately(other, tolerance); var exceptionAssertion = act.Should().Throw().Which; exceptionAssertion.Message.Should().Contain( @@ -55,27 +55,39 @@ public static void ValuesNotApproximatelyEqual_Float(float value, float other, f } [Fact] - public static void DefaultTolerance_Double() + public static void DifferenceWithinDefaultTolerance_Double() { - // Should pass - difference is 0.00005 which is less than default tolerance 0.0001 + // The difference 0.00005 is less than the default tolerance 0.0001 const double value = 1.00005; + value.MustBeApproximately(1.0).Should().Be(value); + } - // Should throw - difference is 0.0002 which is greater than default tolerance 0.0001 + [Fact] + public static void DifferenceExceedsDefaultTolerance_Double() + { + // The difference 0.0002 is greater than the default tolerance 0.0001 Action act = () => 1.0002.MustBeApproximately(1.0, "parameter"); + act.Should().Throw() .WithParameterName("parameter"); } [Fact] - public static void DefaultTolerance_Float() + public static void DifferenceWithinDefaultTolerance_Float() { - // Should pass - difference is 0.00005f which is less than default tolerance 0.0001f + // The difference 0.00005f is less than the default tolerance 0.0001f const float value = 1.00005f; + value.MustBeApproximately(1.0f).Should().Be(value); + } - // Should throw - difference is 0.0002f which is greater than default tolerance 0.0001f + [Fact] + public static void DifferenceExceedsDefaultTolerance_Float() + { + // The difference 0.0002f is greater than the default tolerance 0.0001f Action act = () => 1.0002f.MustBeApproximately(1.0f, "parameter"); + act.Should().Throw() .WithParameterName("parameter"); } @@ -122,6 +134,14 @@ public static void NoCustomExceptionThrown_Double() => public static void NoCustomExceptionThrown_Float() => 5.0f.MustBeApproximately(5.05f, 0.1f, (_, _, _) => null).Should().Be(5.0f); + [Fact] + public static void NoCustomExceptionThrownWithDefaultTolerance_Double() => + 5.0.MustBeApproximately(5.00005, (_, _) => null).Should().Be(5.0); + + [Fact] + public static void NoCustomExceptionThrownWithDefaultTolerance_Float() => + 5.0f.MustBeApproximately(5.00005f, (_, _) => null).Should().Be(5.0f); + [Fact] public static void CustomMessage_Double() => Test.CustomMessage( @@ -194,7 +214,7 @@ public static void ValuesApproximatelyEqual_Generic(double value, double other, [InlineData(0.0001, 0.0002, 0.00005)] public static void ValuesNotApproximatelyEqual_Generic(double value, double other, double tolerance) { - var act = () => value.MustBeApproximately(other, tolerance, nameof(value)); + var act = () => value.MustBeApproximately(other, tolerance); var exceptionAssertion = act.Should().Throw().Which; exceptionAssertion.Message.Should().Contain( @@ -215,7 +235,7 @@ public static void CustomExceptionWithTolerance_Generic() => [Fact] public static void NoCustomExceptionThrown_Generic() => 5.0.MustBeApproximately(5.05, 0.1, (_, _, _) => null).Should().Be(5.0); - + [Fact] public static void CustomMessage_Generic() => Test.CustomMessage( diff --git a/tests/Light.GuardClauses.Tests/ComparableAssertions/MustNotBeApproximatelyTests.cs b/tests/Light.GuardClauses.Tests/ComparableAssertions/MustNotBeApproximatelyTests.cs index 52764929..59dae7a5 100644 --- a/tests/Light.GuardClauses.Tests/ComparableAssertions/MustNotBeApproximatelyTests.cs +++ b/tests/Light.GuardClauses.Tests/ComparableAssertions/MustNotBeApproximatelyTests.cs @@ -29,7 +29,7 @@ public static void ValuesNotApproximatelyEqual_Float(float value, float other, f [InlineData(0.0001, 0.00015, 0.0001)] public static void ValuesApproximatelyEqual_Double(double value, double other, double tolerance) { - var act = () => value.MustNotBeApproximately(other, tolerance, nameof(value)); + var act = () => value.MustNotBeApproximately(other, tolerance); var exceptionAssertion = act.Should().Throw().Which; exceptionAssertion.Message.Should().Contain( @@ -45,7 +45,7 @@ public static void ValuesApproximatelyEqual_Double(double value, double other, d [InlineData(0.0001f, 0.00015f, 0.0001f)] public static void ValuesApproximatelyEqual_Float(float value, float other, float tolerance) { - var act = () => value.MustNotBeApproximately(other, tolerance, nameof(value)); + var act = () => value.MustNotBeApproximately(other, tolerance); var exceptionAssertion = act.Should().Throw().Which; exceptionAssertion.Message.Should().Contain( @@ -180,6 +180,14 @@ public static void CallerArgumentExpressionWithTolerance_Float() .WithParameterName(nameof(pi)); } + [Fact] + public static void NoCustomExceptionThrownWithDefaultTolerance_Double() => + 5.1.MustNotBeApproximately(5.0, (_, _) => null).Should().Be(5.1); + + [Fact] + public static void NoCustomExceptionThrownWithDefaultTolerance_Float() => + 5.1f.MustNotBeApproximately(5.0f, (_, _) => null).Should().Be(5.1f); + #if NET8_0_OR_GREATER [Theory] [InlineData(5.3, 5.0, 0.2)] @@ -196,7 +204,7 @@ public static void ValuesNotApproximatelyEqual_Generic(double value, double othe [InlineData(0.0001, 0.00015, 0.0001)] public static void ValuesApproximatelyEqual_Generic(double value, double other, double tolerance) { - var act = () => value.MustNotBeApproximately(other, tolerance, nameof(value)); + var act = () => value.MustNotBeApproximately(other, tolerance); var exceptionAssertion = act.Should().Throw().Which; exceptionAssertion.Message.Should().Contain( @@ -217,7 +225,7 @@ public static void CustomExceptionWithTolerance_Generic() => [Fact] public static void NoCustomExceptionThrown_Generic() => 5.2.MustNotBeApproximately(5.0, 0.1, (_, _, _) => null).Should().Be(5.2); - + [Fact] public static void CustomMessage_Generic() => Test.CustomMessage( @@ -235,4 +243,4 @@ public static void CallerArgumentExpressionWithTolerance_Generic() .WithParameterName(nameof(seventyEightO1)); } #endif -} \ No newline at end of file +} diff --git a/tests/Light.GuardClauses.Tests/ComparableAssertions/NumericCustomFactorySuccessTests.cs b/tests/Light.GuardClauses.Tests/ComparableAssertions/NumericCustomFactorySuccessTests.cs index 24271fb9..08774454 100644 --- a/tests/Light.GuardClauses.Tests/ComparableAssertions/NumericCustomFactorySuccessTests.cs +++ b/tests/Light.GuardClauses.Tests/ComparableAssertions/NumericCustomFactorySuccessTests.cs @@ -4,68 +4,150 @@ namespace Light.GuardClauses.Tests.ComparableAssertions; +// Each test targets exactly one numeric overload so that a failing test +// immediately identifies the defective production method. public static class NumericCustomFactorySuccessTests { + private static InvalidOperationException FactoryMustNotBeInvoked() => + throw new InvalidOperationException("The factory must not be invoked."); + + [Fact] + public static void MustBePositive_Int() => + 1.MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(1); + + [Fact] + public static void MustBePositive_Long() => + 2L.MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(2L); + + [Fact] + public static void MustBePositive_Decimal() => + 3m.MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(3m); + + [Fact] + public static void MustBePositive_Float() => + 4f.MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(4f); + + [Fact] + public static void MustBePositive_Double() => + 5d.MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(5d); + + [Fact] + public static void MustBePositive_TimeSpan() => + TimeSpan.FromTicks(6).MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(TimeSpan.FromTicks(6)); + + [Fact] + public static void MustBePositive_Generic() => + ((short) 7).MustBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(7); + + [Fact] + public static void MustBeNegative_Int() => + (-1).MustBeNegative(_ => FactoryMustNotBeInvoked()).Should().Be(-1); + + [Fact] + public static void MustBeNegative_Long() => + (-2L).MustBeNegative(_ => FactoryMustNotBeInvoked()).Should().Be(-2L); + + [Fact] + public static void MustBeNegative_Decimal() => + (-3m).MustBeNegative(_ => FactoryMustNotBeInvoked()).Should().Be(-3m); + + [Fact] + public static void MustBeNegative_Float() => + (-4f).MustBeNegative(_ => FactoryMustNotBeInvoked()).Should().Be(-4f); + + [Fact] + public static void MustBeNegative_Double() => + (-5d).MustBeNegative(_ => FactoryMustNotBeInvoked()).Should().Be(-5d); + + [Fact] + public static void MustBeNegative_TimeSpan() => + TimeSpan.FromTicks(-6).MustBeNegative(_ => FactoryMustNotBeInvoked()).Should().Be(TimeSpan.FromTicks(-6)); + + [Fact] + public static void MustBeNegative_Generic() => + ((short) -7).MustBeNegative(_ => FactoryMustNotBeInvoked()).Should().Be(-7); + + [Fact] + public static void MustNotBePositive_Int() => + 0.MustNotBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(0); + + [Fact] + public static void MustNotBePositive_Long() => + 0L.MustNotBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(0L); + + [Fact] + public static void MustNotBePositive_Decimal() => + 0m.MustNotBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(0m); + + [Fact] + public static void MustNotBePositive_Float() => + 0f.MustNotBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(0f); + + [Fact] + public static void MustNotBePositive_Double() => + 0d.MustNotBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(0d); + + [Fact] + public static void MustNotBePositive_TimeSpan() => + TimeSpan.Zero.MustNotBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(TimeSpan.Zero); + + [Fact] + public static void MustNotBePositive_Generic() => + ((short) 0).MustNotBePositive(_ => FactoryMustNotBeInvoked()).Should().Be(0); + + [Fact] + public static void MustNotBeNegative_Int() => + 0.MustNotBeNegative(_ => FactoryMustNotBeInvoked()).Should().Be(0); + + [Fact] + public static void MustNotBeNegative_Long() => + 0L.MustNotBeNegative(_ => FactoryMustNotBeInvoked()).Should().Be(0L); + + [Fact] + public static void MustNotBeNegative_Decimal() => + 0m.MustNotBeNegative(_ => FactoryMustNotBeInvoked()).Should().Be(0m); + + [Fact] + public static void MustNotBeNegative_Float() => + 0f.MustNotBeNegative(_ => FactoryMustNotBeInvoked()).Should().Be(0f); + + [Fact] + public static void MustNotBeNegative_Double() => + 0d.MustNotBeNegative(_ => FactoryMustNotBeInvoked()).Should().Be(0d); + + [Fact] + public static void MustNotBeNegative_TimeSpan() => + TimeSpan.Zero.MustNotBeNegative(_ => FactoryMustNotBeInvoked()).Should().Be(TimeSpan.Zero); + + [Fact] + public static void MustNotBeNegative_Generic() => + ((short) 0).MustNotBeNegative(_ => FactoryMustNotBeInvoked()).Should().Be(0); + + [Fact] + public static void MustNotBeZero_Int() => + 1.MustNotBeZero(_ => FactoryMustNotBeInvoked()).Should().Be(1); + + [Fact] + public static void MustNotBeZero_Long() => + 2L.MustNotBeZero(_ => FactoryMustNotBeInvoked()).Should().Be(2L); + + [Fact] + public static void MustNotBeZero_Decimal() => + 3m.MustNotBeZero(_ => FactoryMustNotBeInvoked()).Should().Be(3m); + + [Fact] + public static void MustNotBeZero_Float() => + 4f.MustNotBeZero(_ => FactoryMustNotBeInvoked()).Should().Be(4f); + + [Fact] + public static void MustNotBeZero_Double() => + 5d.MustNotBeZero(_ => FactoryMustNotBeInvoked()).Should().Be(5d); + + [Fact] + public static void MustNotBeZero_TimeSpan() => + TimeSpan.FromTicks(6).MustNotBeZero(_ => FactoryMustNotBeInvoked()).Should().Be(TimeSpan.FromTicks(6)); + [Fact] - public static void MustBePositiveReturnsValidValuesWithoutInvokingFactories() - { - 1.MustBePositive(_ => throw new InvalidOperationException()).Should().Be(1); - 2L.MustBePositive(_ => throw new InvalidOperationException()).Should().Be(2L); - 3m.MustBePositive(_ => throw new InvalidOperationException()).Should().Be(3m); - 4f.MustBePositive(_ => throw new InvalidOperationException()).Should().Be(4f); - 5d.MustBePositive(_ => throw new InvalidOperationException()).Should().Be(5d); - TimeSpan.FromTicks(6).MustBePositive(_ => throw new InvalidOperationException()).Should() - .Be(TimeSpan.FromTicks(6)); - ((short) 7).MustBePositive(_ => throw new InvalidOperationException()).Should().Be(7); - } - - [Fact] - public static void MustBeNegativeReturnsValidValuesWithoutInvokingFactories() - { - (-1).MustBeNegative(_ => throw new InvalidOperationException()).Should().Be(-1); - (-2L).MustBeNegative(_ => throw new InvalidOperationException()).Should().Be(-2L); - (-3m).MustBeNegative(_ => throw new InvalidOperationException()).Should().Be(-3m); - (-4f).MustBeNegative(_ => throw new InvalidOperationException()).Should().Be(-4f); - (-5d).MustBeNegative(_ => throw new InvalidOperationException()).Should().Be(-5d); - TimeSpan.FromTicks(-6).MustBeNegative(_ => throw new InvalidOperationException()).Should() - .Be(TimeSpan.FromTicks(-6)); - ((short) -7).MustBeNegative(_ => throw new InvalidOperationException()).Should().Be(-7); - } - - [Fact] - public static void MustNotBePositiveReturnsValidValuesWithoutInvokingFactories() - { - 0.MustNotBePositive(_ => throw new InvalidOperationException()).Should().Be(0); - 0L.MustNotBePositive(_ => throw new InvalidOperationException()).Should().Be(0L); - 0m.MustNotBePositive(_ => throw new InvalidOperationException()).Should().Be(0m); - 0f.MustNotBePositive(_ => throw new InvalidOperationException()).Should().Be(0f); - 0d.MustNotBePositive(_ => throw new InvalidOperationException()).Should().Be(0d); - TimeSpan.Zero.MustNotBePositive(_ => throw new InvalidOperationException()).Should().Be(TimeSpan.Zero); - ((short) 0).MustNotBePositive(_ => throw new InvalidOperationException()).Should().Be(0); - } - - [Fact] - public static void MustNotBeNegativeReturnsValidValuesWithoutInvokingFactories() - { - 0.MustNotBeNegative(_ => throw new InvalidOperationException()).Should().Be(0); - 0L.MustNotBeNegative(_ => throw new InvalidOperationException()).Should().Be(0L); - 0m.MustNotBeNegative(_ => throw new InvalidOperationException()).Should().Be(0m); - 0f.MustNotBeNegative(_ => throw new InvalidOperationException()).Should().Be(0f); - 0d.MustNotBeNegative(_ => throw new InvalidOperationException()).Should().Be(0d); - TimeSpan.Zero.MustNotBeNegative(_ => throw new InvalidOperationException()).Should().Be(TimeSpan.Zero); - ((short) 0).MustNotBeNegative(_ => throw new InvalidOperationException()).Should().Be(0); - } - - [Fact] - public static void MustNotBeZeroReturnsValidValuesWithoutInvokingFactories() - { - 1.MustNotBeZero(_ => throw new InvalidOperationException()).Should().Be(1); - 2L.MustNotBeZero(_ => throw new InvalidOperationException()).Should().Be(2L); - 3m.MustNotBeZero(_ => throw new InvalidOperationException()).Should().Be(3m); - 4f.MustNotBeZero(_ => throw new InvalidOperationException()).Should().Be(4f); - 5d.MustNotBeZero(_ => throw new InvalidOperationException()).Should().Be(5d); - TimeSpan.FromTicks(6).MustNotBeZero(_ => throw new InvalidOperationException()).Should() - .Be(TimeSpan.FromTicks(6)); - ((short) 7).MustNotBeZero(_ => throw new InvalidOperationException()).Should().Be(7); - } + public static void MustNotBeZero_Generic() => + ((short) 7).MustNotBeZero(_ => FactoryMustNotBeInvoked()).Should().Be(7); } diff --git a/tests/Light.GuardClauses.Tests/ComparableAssertions/RangeTests.cs b/tests/Light.GuardClauses.Tests/ComparableAssertions/RangeTests.cs index e2a848bf..7ea1cf2a 100644 --- a/tests/Light.GuardClauses.Tests/ComparableAssertions/RangeTests.cs +++ b/tests/Light.GuardClauses.Tests/ComparableAssertions/RangeTests.cs @@ -56,8 +56,8 @@ public static void ConstructorException(int from, int to) } [Theory] - [InlineData(0, 10, 0)] // Lower boundary - [InlineData(0, 10, 5)] // Middle value + [InlineData(0, 10, 0)] // Lower boundary + [InlineData(0, 10, 5)] // Middle value [InlineData(0, 10, 10)] // Upper boundary public static void InclusiveBetween_ValuesInRange(int from, int to, int value) { @@ -69,8 +69,8 @@ public static void InclusiveBetween_ValuesInRange(int from, int to, int value) } [Theory] - [InlineData(0, 10, -1)] // Below range - [InlineData(0, 10, 11)] // Above range + [InlineData(0, 10, -1)] // Below range + [InlineData(0, 10, 11)] // Above range public static void InclusiveBetween_ValuesOutOfRange(int from, int to, int value) { var range = Range.InclusiveBetween(from, to); @@ -81,9 +81,9 @@ public static void InclusiveBetween_ValuesOutOfRange(int from, int to, int value } [Theory] - [InlineData(0, 10, 1)] // Just above lower boundary - [InlineData(0, 10, 5)] // Middle value - [InlineData(0, 10, 9)] // Just below upper boundary + [InlineData(0, 10, 1)] // Just above lower boundary + [InlineData(0, 10, 5)] // Middle value + [InlineData(0, 10, 9)] // Just below upper boundary public static void ExclusiveBetween_ValuesInRange(int from, int to, int value) { var range = Range.ExclusiveBetween(from, to); @@ -94,10 +94,10 @@ public static void ExclusiveBetween_ValuesInRange(int from, int to, int value) } [Theory] - [InlineData(0, 10, 0)] // Lower boundary - [InlineData(0, 10, 10)] // Upper boundary - [InlineData(0, 10, -1)] // Below range - [InlineData(0, 10, 11)] // Above range + [InlineData(0, 10, 0)] // Lower boundary + [InlineData(0, 10, 10)] // Upper boundary + [InlineData(0, 10, -1)] // Below range + [InlineData(0, 10, 11)] // Above range public static void ExclusiveBetween_ValuesOutOfRange(int from, int to, int value) { var range = Range.ExclusiveBetween(from, to); @@ -107,24 +107,24 @@ public static void ExclusiveBetween_ValuesOutOfRange(int from, int to, int value result.Should().BeFalse(); } - [Theory] - [InlineData(10, 5)] // To less than From - [InlineData(-5, -10)] // Negative To less than From + [Theory] + [InlineData(10, 5)] // To less than From + [InlineData(-5, -10)] // Negative To less than From public static void InclusiveBetween_InvalidRange_ThrowsException(int from, int to) { Action inclusiveAct = () => Range.InclusiveBetween(from, to); inclusiveAct.Should().Throw() - .And.Message.Should().Contain($"{nameof(to)} must not be less than {from}"); + .And.Message.Should().Contain($"{nameof(to)} must not be less than {from}"); } - [Theory] - [InlineData(10, 5)] // To less than From - [InlineData(-5, -10)] // Negative To less than From + [Theory] + [InlineData(10, 5)] // To less than From + [InlineData(-5, -10)] // Negative To less than From public static void ExclusiveBetween_InvalidRange_ThrowsException(int from, int to) { Action exclusiveAct = () => Range.ExclusiveBetween(from, to); exclusiveAct.Should().Throw() - .And.Message.Should().Contain($"{nameof(to)} must not be less than {from}"); + .And.Message.Should().Contain($"{nameof(to)} must not be less than {from}"); } [Theory] @@ -140,14 +140,13 @@ public static void RangeForCollections(IEnumerable enumerable) } public static readonly TheoryData Collections = - new () - { - new List { 1, 2, 3, 4 }, - "This is a long string", - new[] { 'a', 'b', 'c', 'd' }, - new ObservableCollection { 1, -1 }, - new ArrayList() - }; + [ + new List { 1, 2, 3, 4 }, + "This is a long string", + new[] { 'a', 'b', 'c', 'd' }, + new ObservableCollection { 1, -1 }, + new ArrayList(), + ]; [Fact] public static void EnumerableNull() @@ -176,7 +175,7 @@ public static void RangeForGenericCollections(IEnumerable enumerable) Array.Empty(), new List { 'a', 'b', 'c' }, "007", - new ArraySegment(new[] { 'a' }) + new ArraySegment(new[] { 'a' }), }; [Fact] @@ -234,6 +233,71 @@ public static void RangeForReadOnlySpan(Memory memory) { new[] { 1, 2, 3, 4 }, Enumerable.Range(1, 500).ToArray(), - Array.Empty() + Array.Empty(), + }; + + [Theory] + [MemberData(nameof(ArraySegments))] + public static void RangeForArraySegment(ArraySegment segment) + { + var range = Range.For(segment); + + var expectedRange = new Range(0, segment.Count, true, false); + range.Should().Be(expectedRange); + } + + public static readonly TheoryData> ArraySegments = + new () + { + new ArraySegment(new[] { 'a', 'b', 'c' }), + new ArraySegment(new[] { 'a', 'b', 'c', 'd' }, 1, 2), + new ArraySegment(Array.Empty()), }; + + [Fact] + public static void EqualityOperatorWithEqualRanges() => + (Range.InclusiveBetween(1, 5) == Range.InclusiveBetween(1, 5)).Should().BeTrue(); + + [Fact] + public static void EqualityOperatorWithDifferentRanges() => + (Range.InclusiveBetween(1, 5) == Range.InclusiveBetween(1, 6)).Should().BeFalse(); + + [Fact] + public static void InequalityOperatorWithEqualRanges() => + (Range.ExclusiveBetween(1, 5) != Range.ExclusiveBetween(1, 5)).Should().BeFalse(); + + [Fact] + public static void InequalityOperatorWithDifferentRanges() => + (Range.ExclusiveBetween(1, 5) != Range.ExclusiveBetween(2, 5)).Should().BeTrue(); + + [Theory] + [InlineData(true, false)] + [InlineData(false, true)] + public static void RangesWithDifferentInclusivenessAreNotEqual(bool isFromInclusive, bool isToInclusive) + { + var range = new Range(1, 5, isFromInclusive, isToInclusive); + var otherRange = new Range(1, 5, !isFromInclusive, isToInclusive); + + range.Equals(otherRange).Should().BeFalse(); + } + + [Fact] + public static void RangeDoesNotEqualNull() => + Range.InclusiveBetween(1, 5).Equals(null).Should().BeFalse(); + + [Fact] + public static void RangeDoesNotEqualInstanceOfOtherType() => + Range.InclusiveBetween(1, 5).Equals("no range").Should().BeFalse(); + + [Fact] + public static void RangeEqualsBoxedEqualRange() => + Range.InclusiveBetween(1, 5).Equals((object) Range.InclusiveBetween(1, 5)).Should().BeTrue(); + + [Fact] + public static void EqualRangesHaveEqualHashCodes() => + Range.InclusiveBetween(1, 5).GetHashCode().Should().Be(Range.InclusiveBetween(1, 5).GetHashCode()); + + [Fact] + public static void DifferentRangesHaveDifferentHashCodes() => + Range.InclusiveBetween(1, 5).GetHashCode().Should().NotBe(Range.ExclusiveBetween(1, 5).GetHashCode()); } diff --git a/tests/Light.GuardClauses.Tests/DateTimeAssertions/MustBeUtcTests.cs b/tests/Light.GuardClauses.Tests/DateTimeAssertions/MustBeUtcTests.cs index af41130b..90bdd629 100644 --- a/tests/Light.GuardClauses.Tests/DateTimeAssertions/MustBeUtcTests.cs +++ b/tests/Light.GuardClauses.Tests/DateTimeAssertions/MustBeUtcTests.cs @@ -12,10 +12,12 @@ public static void NotUtc() { var invalidDateTime = DateTime.Now; - Action act = () => invalidDateTime.MustBeUtc(nameof(invalidDateTime)); + Action act = () => invalidDateTime.MustBeUtc(); act.Should().Throw() - .And.Message.Should().Contain($"{nameof(invalidDateTime)} must use kind \"{DateTimeKind.Utc}\", but it actually uses \"{invalidDateTime.Kind}\" and is \"{invalidDateTime:O}\"."); + .And.Message.Should().Contain( + $"{nameof(invalidDateTime)} must use kind \"{DateTimeKind.Utc}\", but it actually uses \"{invalidDateTime.Kind}\" and is \"{invalidDateTime:O}\"." + ); } [Fact] @@ -30,8 +32,10 @@ public static void Utc() [Fact] public static void CustomException() => - Test.CustomException(new DateTime(2018, 2, 18, 16, 51, 00, DateTimeKind.Local), - (value, exceptionFactory) => value.MustBeUtc(exceptionFactory)); + Test.CustomException( + new DateTime(2018, 2, 18, 16, 51, 00, DateTimeKind.Local), + (value, exceptionFactory) => value.MustBeUtc(exceptionFactory) + ); [Fact] public static void NoCustomException() @@ -45,7 +49,7 @@ public static void NoCustomException() [Fact] public static void CustomMessage() => - Test.CustomMessage(message => DateTime.Now.MustBeUtc(message:message)); + Test.CustomMessage(message => DateTime.Now.MustBeUtc(message: message)); [Fact] public static void CallerArgumentExpression() @@ -83,8 +87,10 @@ public static void NonZeroOffsetIsRejectedWithoutNormalization() [Fact] public static void DateTimeOffsetCustomMessage() => - Test.CustomMessage(message => - new DateTimeOffset(2026, 7, 13, 12, 30, 0, TimeSpan.FromHours(2)).MustBeUtc(message: message)); + Test.CustomMessage( + message => + new DateTimeOffset(2026, 7, 13, 12, 30, 0, TimeSpan.FromHours(2)).MustBeUtc(message: message) + ); [Fact] public static void DateTimeOffsetCustomFactoryReceivesValue() @@ -93,4 +99,14 @@ public static void DateTimeOffsetCustomFactoryReceivesValue() Test.CustomException(value, (dateTimeOffset, factory) => dateTimeOffset.MustBeUtc(factory)); } + + [Fact] + public static void DateTimeOffsetNoCustomException() + { + var value = new DateTimeOffset(2026, 7, 13, 10, 30, 0, TimeSpan.Zero); + + var result = value.MustBeUtc(_ => null); + + result.Should().Be(value); + } } diff --git a/tests/Light.GuardClauses.Tests/Exceptions/InvalidConfigurationExceptionTests.cs b/tests/Light.GuardClauses.Tests/Exceptions/InvalidConfigurationExceptionTests.cs new file mode 100644 index 00000000..9cef5fdb --- /dev/null +++ b/tests/Light.GuardClauses.Tests/Exceptions/InvalidConfigurationExceptionTests.cs @@ -0,0 +1,20 @@ +using System; +using FluentAssertions; +using Light.GuardClauses.Exceptions; +using Xunit; + +namespace Light.GuardClauses.Tests.Exceptions; + +public static class InvalidConfigurationExceptionTests +{ + [Fact] + public static void MessageAndInnerExceptionArePassedToBaseClass() + { + var innerException = new ArgumentException("inner"); + + var exception = new InvalidConfigurationException("outer", innerException); + + exception.Message.Should().Be("outer"); + exception.InnerException.Should().BeSameAs(innerException); + } +} diff --git a/tests/Light.GuardClauses.Tests/FrameworkExtensions/StringContainsTests.cs b/tests/Light.GuardClauses.Tests/FrameworkExtensions/StringContainsTests.cs new file mode 100644 index 00000000..8b8353d3 --- /dev/null +++ b/tests/Light.GuardClauses.Tests/FrameworkExtensions/StringContainsTests.cs @@ -0,0 +1,41 @@ +using System; +using FluentAssertions; +using Light.GuardClauses.FrameworkExtensions; +using Xunit; + +namespace Light.GuardClauses.Tests.FrameworkExtensions; + +// The extension method is called explicitly via StringExtensions because modern TFMs +// provide string.Contains(string, StringComparison) as an instance method that would +// otherwise always win overload resolution. +public static class StringContainsTests +{ + [Theory] + [InlineData("Hello, World", "World", StringComparison.Ordinal, true)] + [InlineData("Hello, World", "world", StringComparison.Ordinal, false)] + [InlineData("Hello, World", "world", StringComparison.OrdinalIgnoreCase, true)] + [InlineData("Hello, World", "Foo", StringComparison.OrdinalIgnoreCase, false)] + public static void ContainsWithComparisonType( + string @string, + string value, + StringComparison comparisonType, + bool expected + ) => + StringExtensions.Contains(@string, value, comparisonType).Should().Be(expected); + + [Fact] + public static void StringNull() + { + var act = () => StringExtensions.Contains(null!, "foo", StringComparison.Ordinal); + + act.Should().Throw().WithParameterName("string"); + } + + [Fact] + public static void ValueNull() + { + var act = () => StringExtensions.Contains("foo", null!, StringComparison.Ordinal); + + act.Should().Throw().WithParameterName("value"); + } +} diff --git a/tests/Light.GuardClauses.Tests/StreamAssertions/StreamGuardTests.cs b/tests/Light.GuardClauses.Tests/StreamAssertions/StreamGuardTests.cs index ffb40910..6649a9b0 100644 --- a/tests/Light.GuardClauses.Tests/StreamAssertions/StreamGuardTests.cs +++ b/tests/Light.GuardClauses.Tests/StreamAssertions/StreamGuardTests.cs @@ -60,18 +60,15 @@ public static IEnumerable CapabilityStates() } } - [Fact] - public static void GuardsPreserveConcreteTypeAndOriginalInstance() + [Theory] + [InlineData(Capability.Readable)] + [InlineData(Capability.Writable)] + [InlineData(Capability.Seekable)] + public static void GuardsPreserveConcreteTypeAndOriginalInstance(Capability capability) { var stream = new TrackingStream(true, true, true); - var readable = stream.MustBeReadable(); - var writable = stream.MustBeWritable(); - var seekable = stream.MustBeSeekable(); - - readable.Should().BeSameAs(stream); - writable.Should().BeSameAs(stream); - seekable.Should().BeSameAs(stream); + InvokeDefault(stream, capability).Should().BeSameAs(stream); } [Theory] diff --git a/tests/Light.GuardClauses.Tests/StringAssertions/EqualsTests.cs b/tests/Light.GuardClauses.Tests/StringAssertions/EqualsTests.cs index 8d107c1d..cda27249 100644 --- a/tests/Light.GuardClauses.Tests/StringAssertions/EqualsTests.cs +++ b/tests/Light.GuardClauses.Tests/StringAssertions/EqualsTests.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using Light.GuardClauses.Exceptions; using Xunit; namespace Light.GuardClauses.Tests.StringAssertions; @@ -34,19 +35,19 @@ public static void OrdinalIgnoreWhiteSpace(string x, string y, bool expected) => [InlineData(null, null, true)] [InlineData(null, "Foo", false)] [InlineData("Bar", null, false)] - public static void OrdinalIgnoreCaseIgnoreWhiteSpace(string x, string y, bool expected) => + public static void OrdinalIgnoreCaseIgnoreWhiteSpace(string x, string y, bool expected) => x.Equals(y, StringComparisonType.OrdinalIgnoreCaseIgnoreWhiteSpace).Should().Be(expected); [Theory] [InlineData("Foo", "Foo", true)] [InlineData("Bar", "bar", false)] - public static void FallbackToOrdinal(string x, string y, bool expected) => + public static void FallbackToOrdinal(string x, string y, bool expected) => x.Equals(y, StringComparisonType.Ordinal).Should().Be(expected); [Theory] [InlineData("Foo", "foo", true)] [InlineData("Foo", "Bar", false)] - public static void FallbackToOrdinalIgnoreCase(string x, string y, bool expected) => + public static void FallbackToOrdinalIgnoreCase(string x, string y, bool expected) => x.Equals(y, StringComparisonType.OrdinalIgnoreCase).Should().Be(expected); [Theory] @@ -72,4 +73,13 @@ public static void FallbackToInvariantCulture(string x, string y, bool expected) [InlineData("Foo", "Bar", false)] public static void FallbackToInvariantCultureIgnoreCase(string x, string y, bool expected) => x.Equals(y, StringComparisonType.InvariantCultureIgnoreCase).Should().Be(expected); -} \ No newline at end of file + + [Fact] + public static void InvalidComparisonType() + { + var act = () => "Foo".Equals("Bar", (StringComparisonType) 42); + + act.Should().Throw() + .WithParameterName("comparisonType"); + } +} diff --git a/tests/Light.GuardClauses.Tests/StringAssertions/IsEmailAddressTests.cs b/tests/Light.GuardClauses.Tests/StringAssertions/IsEmailAddressTests.cs index c252cbf4..48cd2dc1 100644 --- a/tests/Light.GuardClauses.Tests/StringAssertions/IsEmailAddressTests.cs +++ b/tests/Light.GuardClauses.Tests/StringAssertions/IsEmailAddressTests.cs @@ -1,4 +1,5 @@ using System; +using System.Text.RegularExpressions; using FluentAssertions; using Xunit; @@ -104,5 +105,49 @@ public void IsValidEmailAddress_ReadOnlyMemory(string email) isValid.Should().BeTrue(); } + + private static readonly Regex CustomPattern = new (@"\A[a-z]+@[a-z]+\.com\z"); + + [Theory] + [InlineData("kenny@feo.com", true)] + [InlineData("Kenny@Feo.com", false)] + public void CustomPattern_Span(string email, bool expected) => + new Span(email.ToCharArray()).IsEmailAddress(CustomPattern).Should().Be(expected); + + [Theory] + [InlineData("kenny@feo.com", true)] + [InlineData("Kenny@Feo.com", false)] + public void CustomPattern_Memory(string email, bool expected) => + email.ToCharArray().AsMemory().IsEmailAddress(CustomPattern).Should().Be(expected); + + [Theory] + [InlineData("kenny@feo.com", true)] + [InlineData("Kenny@Feo.com", false)] + public void CustomPattern_ReadOnlyMemory(string email, bool expected) => + new ReadOnlyMemory(email.ToCharArray()).IsEmailAddress(CustomPattern).Should().Be(expected); + + [Fact] + public void CustomPatternNull_Span() + { + var act = () => new Span("foo@bar.com".ToCharArray()).IsEmailAddress(null!); + + act.Should().Throw().WithParameterName("emailAddressPattern"); + } + + [Fact] + public void CustomPatternNull_Memory() + { + var act = () => "foo@bar.com".ToCharArray().AsMemory().IsEmailAddress(null!); + + act.Should().Throw().WithParameterName("emailAddressPattern"); + } + + [Fact] + public void CustomPatternNull_ReadOnlyMemory() + { + var act = () => new ReadOnlyMemory("foo@bar.com".ToCharArray()).IsEmailAddress(null!); + + act.Should().Throw().WithParameterName("emailAddressPattern"); + } #endif } diff --git a/tests/Light.GuardClauses.Tests/StringAssertions/MustBeFileExtensionTests.cs b/tests/Light.GuardClauses.Tests/StringAssertions/MustBeFileExtensionTests.cs index a5df0736..761aa6f0 100644 --- a/tests/Light.GuardClauses.Tests/StringAssertions/MustBeFileExtensionTests.cs +++ b/tests/Light.GuardClauses.Tests/StringAssertions/MustBeFileExtensionTests.cs @@ -37,7 +37,7 @@ public static void StringIsNull() var nullString = default(string); // ReSharper disable once ExpressionIsAlwaysNull - var act = () => nullString.MustBeFileExtension(nameof(nullString)); + var act = () => nullString.MustBeFileExtension(); act.Should().Throw() .WithParameterName(nameof(nullString)); @@ -47,7 +47,7 @@ public static void StringIsNull() [MemberData(nameof(InvalidFileExtensionsData))] public static void InvalidFileExtensions(string invalidString) { - var act = () => invalidString.MustBeFileExtension(nameof(invalidString)); + var act = () => invalidString.MustBeFileExtension(); act.Should().Throw() .And.Message.Should().Contain( @@ -81,6 +81,55 @@ public static void CallerArgumentExpression() .WithParameterName(nameof(invalidString)); } + [Theory] + [MemberData(nameof(ValidFileExtensionsData))] + public static void CustomExceptionNotThrownForValidFileExtensions(string input) => + input.MustBeFileExtension(_ => new ()).Should().BeSameAs(input); + + [Theory] + [MemberData(nameof(ValidFileExtensionsData))] + public static void CustomExceptionNotThrownForValidFileExtensions_ReadOnlySpan(string input) + { + var span = input.AsSpan(); + + var result = span.MustBeFileExtension(_ => new ()); + + result.Equals(span, StringComparison.Ordinal).Should().BeTrue(); + } + + [Theory] + [MemberData(nameof(ValidFileExtensionsData))] + public static void CustomExceptionNotThrownForValidFileExtensions_Span(string input) + { + var span = input.ToCharArray().AsSpan(); + + var result = span.MustBeFileExtension(_ => new ()); + + result.ToString().Should().Be(input); + } + + [Theory] + [MemberData(nameof(ValidFileExtensionsData))] + public static void CustomExceptionNotThrownForValidFileExtensions_Memory(string input) + { + var memory = input.ToCharArray().AsMemory(); + + var result = memory.MustBeFileExtension(_ => new ()); + + result.ToString().Should().Be(input); + } + + [Theory] + [MemberData(nameof(ValidFileExtensionsData))] + public static void CustomExceptionNotThrownForValidFileExtensions_ReadOnlyMemory(string input) + { + var memory = input.AsMemory(); + + var result = memory.MustBeFileExtension(_ => new ()); + + result.Equals(memory).Should().BeTrue(); + } + [Theory] [MemberData(nameof(ValidFileExtensionsData))] public static void ValidFileExtensions_ReadOnlySpan(string input) diff --git a/tests/Light.GuardClauses.Tests/StringAssertions/MustBeTests.cs b/tests/Light.GuardClauses.Tests/StringAssertions/MustBeTests.cs index 6466858c..c211c08a 100644 --- a/tests/Light.GuardClauses.Tests/StringAssertions/MustBeTests.cs +++ b/tests/Light.GuardClauses.Tests/StringAssertions/MustBeTests.cs @@ -15,17 +15,20 @@ public static class MustBeTests [InlineData(null, "Qux", StringComparison.OrdinalIgnoreCase)] public static void StringsNotEqual(string first, string second, StringComparison comparisonType) { - Action act = () => first.MustBe(second, comparisonType, nameof(first)); + Action act = () => first.MustBe(second, comparisonType); act.Should().Throw() - .And.Message.Should().Contain($"{nameof(first)} must be equal to {second.ToStringOrNull()}, but it actually is {first.ToStringOrNull()}."); + .And.Message.Should().Contain( + $"{nameof(first)} must be equal to {second.ToStringOrNull()}, but it actually is {first.ToStringOrNull()}." + ); } [Theory] [InlineData("Foo", "Foo", StringComparison.Ordinal)] [InlineData("Foo", "foo", StringComparison.OrdinalIgnoreCase)] [InlineData(null, null, StringComparison.CurrentCulture)] - public static void StringsEqual(string first, string second, StringComparison comparisonType) => first.MustBe(second, comparisonType).Should().Be(first); + public static void StringsEqual(string first, string second, StringComparison comparisonType) => + first.MustBe(second, comparisonType).Should().Be(first); [Theory] [InlineData("Foo", " foo ", StringComparisonType.OrdinalIgnoreWhiteSpace)] @@ -34,39 +37,59 @@ public static void StringsNotEqual(string first, string second, StringComparison [InlineData(null, "Qux\r\n", StringComparisonType.OrdinalIgnoreWhiteSpace)] public static void StringsNotEqualIgnoreWhiteSpace(string first, string second, StringComparisonType comparisonType) { - Action act = () => first.MustBe(second, comparisonType, nameof(first)); + Action act = () => first.MustBe(second, comparisonType); act.Should().Throw() - .And.Message.Should().Contain($"{nameof(first)} must be equal to {second.ToStringOrNull()}, but it actually is {first.ToStringOrNull()}."); + .And.Message.Should().Contain( + $"{nameof(first)} must be equal to {second.ToStringOrNull()}, but it actually is {first.ToStringOrNull()}." + ); } [Theory] [InlineData("Foo", "\tfoo", StringComparisonType.OrdinalIgnoreCaseIgnoreWhiteSpace)] [InlineData("Bar", " Bar", StringComparisonType.OrdinalIgnoreWhiteSpace)] [InlineData(null, null, StringComparisonType.OrdinalIgnoreWhiteSpace)] - public static void StringsEqualIgnoreWhiteSpace(string x, string y, StringComparisonType comparisonType) => x.MustBe(y, comparisonType).Should().BeSameAs(x); + public static void StringsEqualIgnoreWhiteSpace(string x, string y, StringComparisonType comparisonType) => + x.MustBe(y, comparisonType).Should().BeSameAs(x); [Fact] public static void CustomException() => - Test.CustomException("Foo", - "Bar", - StringComparison.Ordinal, - (x, y, ct, exceptionFactory) => x.MustBe(y, ct, exceptionFactory)); + Test.CustomException( + "Foo", + "Bar", + StringComparison.Ordinal, + (x, y, ct, exceptionFactory) => x.MustBe(y, ct, exceptionFactory) + ); + + [Fact] + public static void CustomExceptionStringsEqual() => + "Foo".MustBe("foo", StringComparison.OrdinalIgnoreCase, (_, _, _) => null).Should().Be("Foo"); [Fact] public static void CustomMessage() => - Test.CustomMessage(message => "Foo".MustBe("Bar", StringComparison.Ordinal, message: message)); + Test.CustomMessage( + message => "Foo".MustBe("Bar", StringComparison.Ordinal, message: message) + ); [Fact] public static void CustomExceptionIgnoreWhiteSpace() => - Test.CustomException("Foo", - " foo", - StringComparisonType.OrdinalIgnoreWhiteSpace, - (x, y, ct, exceptionFactory) => x.MustBe(y, ct, exceptionFactory)); + Test.CustomException( + "Foo", + " foo", + StringComparisonType.OrdinalIgnoreWhiteSpace, + (x, y, ct, exceptionFactory) => x.MustBe(y, ct, exceptionFactory) + ); + + [Fact] + public static void CustomExceptionIgnoreWhiteSpaceStringsEqual() => + "Foo".MustBe(" foo", StringComparisonType.OrdinalIgnoreCaseIgnoreWhiteSpace, (_, _, _) => null) + .Should().Be("Foo"); [Fact] public static void CustomMessageIgnoreWhiteSpace() => - Test.CustomMessage(message => "foo".MustBe("bar", StringComparisonType.Ordinal, message: message)); + Test.CustomMessage( + message => "foo".MustBe("bar", StringComparisonType.Ordinal, message: message) + ); [Fact] public static void CallerArgumentExpression() @@ -78,4 +101,4 @@ public static void CallerArgumentExpression() act.Should().Throw() .WithParameterName(nameof(myString)); } -} \ No newline at end of file +} diff --git a/tests/Light.GuardClauses.Tests/StringAssertions/MustNotBeTests.cs b/tests/Light.GuardClauses.Tests/StringAssertions/MustNotBeTests.cs index b412c7cf..16aad3ab 100644 --- a/tests/Light.GuardClauses.Tests/StringAssertions/MustNotBeTests.cs +++ b/tests/Light.GuardClauses.Tests/StringAssertions/MustNotBeTests.cs @@ -14,10 +14,12 @@ public static class MustNotBeTests [InlineData(null, null, StringComparison.CurrentCulture)] public static void ValuesEqual(string x, string y, StringComparison comparisonType) { - Action act = () => x.MustNotBe(y, comparisonType, nameof(x)); + Action act = () => x.MustNotBe(y, comparisonType); act.Should().Throw() - .And.Message.Should().Contain($"{nameof(x)} must not be equal to {y.ToStringOrNull()}, but it actually is {x.ToStringOrNull()}"); + .And.Message.Should().Contain( + $"{nameof(x)} must not be equal to {y.ToStringOrNull()}, but it actually is {x.ToStringOrNull()}" + ); } [Theory] @@ -25,7 +27,8 @@ public static void ValuesEqual(string x, string y, StringComparison comparisonTy [InlineData("Baz", "BAZ", StringComparison.CurrentCulture)] [InlineData(null, "Qux", StringComparison.Ordinal)] [InlineData("Quux", null, StringComparison.OrdinalIgnoreCase)] - public static void ValuesNotEqual(string x, string y, StringComparison comparisonType) => x.MustNotBe(y, comparisonType).Should().Be(x); + public static void ValuesNotEqual(string x, string y, StringComparison comparisonType) => + x.MustNotBe(y, comparisonType).Should().Be(x); [Theory] [InlineData("Foo", " Foo ", StringComparisonType.OrdinalIgnoreWhiteSpace)] @@ -33,39 +36,61 @@ public static void ValuesEqual(string x, string y, StringComparison comparisonTy [InlineData(null, null, StringComparisonType.OrdinalIgnoreCaseIgnoreWhiteSpace)] public static void StringsEqualIgnoreWhiteSpace(string x, string y, StringComparisonType comparisonType) { - Action act = () => x.MustNotBe(y, comparisonType, nameof(x)); + Action act = () => x.MustNotBe(y, comparisonType); act.Should().Throw() - .And.Message.Should().Contain($"{nameof(x)} must not be equal to {y.ToStringOrNull()}, but it actually is {x.ToStringOrNull()}"); + .And.Message.Should().Contain( + $"{nameof(x)} must not be equal to {y.ToStringOrNull()}, but it actually is {x.ToStringOrNull()}" + ); } [Theory] [InlineData("Foo", "Bar", StringComparisonType.OrdinalIgnoreWhiteSpace)] [InlineData("Baz", "", StringComparisonType.OrdinalIgnoreCaseIgnoreWhiteSpace)] [InlineData(null, "Qux", StringComparisonType.OrdinalIgnoreCaseIgnoreWhiteSpace)] - public static void StringsNotEqualIgnoreWhiteSpace(string x, string y, StringComparisonType comparisonType) => + public static void StringsNotEqualIgnoreWhiteSpace(string x, string y, StringComparisonType comparisonType) => x.MustNotBe(y, comparisonType).Should().BeSameAs(x); [Fact] public static void CustomException() => - Test.CustomException("Foo", - "Foo", - (x, y, exceptionFactory) => x.MustNotBe(y, StringComparison.CurrentCulture, exceptionFactory)); + Test.CustomException( + "Foo", + "Foo", + (x, y, exceptionFactory) => x.MustNotBe(y, StringComparison.CurrentCulture, exceptionFactory) + ); [Fact] - public static void CustomExceptionIgnoreWhiteSpace() => - Test.CustomException("Foo", - " Foo", - StringComparisonType.OrdinalIgnoreWhiteSpace, - (x, y, ct, exceptionFactory) => x.MustNotBe(y, ct, exceptionFactory)); + public static void CustomExceptionIgnoreWhiteSpace() => + Test.CustomException( + "Foo", + " Foo", + StringComparisonType.OrdinalIgnoreWhiteSpace, + (x, y, ct, exceptionFactory) => x.MustNotBe(y, ct, exceptionFactory) + ); + + [Fact] + public static void CustomExceptionStringsNotEqual() => + "Foo".MustNotBe("Bar", StringComparison.Ordinal, (_, _) => null).Should().Be("Foo"); + + [Fact] + public static void CustomExceptionIgnoreWhiteSpaceStringsNotEqual() => + "Foo".MustNotBe("Bar", StringComparisonType.OrdinalIgnoreWhiteSpace, (_, _, _) => null).Should().Be("Foo"); [Fact] public static void CustomMessage() => - Test.CustomMessage(message => "Foo".MustNotBe("Foo", StringComparison.CurrentCulture, message: message)); + Test.CustomMessage( + message => "Foo".MustNotBe("Foo", StringComparison.CurrentCulture, message: message) + ); [Fact] - public static void CustomMessageIgnoreWhiteSpace() => - Test.CustomMessage(message => "Bar".MustNotBe("bar ", StringComparisonType.OrdinalIgnoreCaseIgnoreWhiteSpace, message: message)); + public static void CustomMessageIgnoreWhiteSpace() => + Test.CustomMessage( + message => "Bar".MustNotBe( + "bar ", + StringComparisonType.OrdinalIgnoreCaseIgnoreWhiteSpace, + message: message + ) + ); [Fact] public static void CallerArgumentExpression() @@ -77,4 +102,4 @@ public static void CallerArgumentExpression() act.Should().Throw() .WithParameterName(nameof(message)); } -} \ No newline at end of file +} diff --git a/tests/Light.GuardClauses.Tests/StringAssertions/StringInspectionGuardTests.cs b/tests/Light.GuardClauses.Tests/StringAssertions/StringInspectionGuardTests.cs index 92d994df..77e54d81 100644 --- a/tests/Light.GuardClauses.Tests/StringAssertions/StringInspectionGuardTests.cs +++ b/tests/Light.GuardClauses.Tests/StringAssertions/StringInspectionGuardTests.cs @@ -6,66 +6,99 @@ namespace Light.GuardClauses.Tests.StringAssertions; +// The string-inspection guards exist for seven families (digits, letters-or-digits, upper case, +// lower case, Base64, hexadecimal, and no-white-space) across five receiver shapes (string, +// Span, ReadOnlySpan, Memory, ReadOnlyMemory). The theories below run one test case per family +// so that a failing case immediately identifies the defective guard family, while the shape +// helpers keep the five receiver shapes in a Single Point of Truth. public static class StringInspectionGuardTests { - [Fact] - public static void EveryGuardReturnsEveryOriginalReceiverShape() - { - AssertDigitReturns("1٢3"); - AssertLetterOrDigitReturns("AzΩ٢"); - AssertUpperCaseReturns("AΩ 123!"); - AssertLowerCaseReturns("aω 123!"); - AssertBase64Returns("T W\tF\ru\n"); - AssertHexadecimalReturns("09aAfF"); - AssertNoWhiteSpaceReturns("Az-09"); + [Theory] + [InlineData(GuardFamily.Digits, "1٢3")] + [InlineData(GuardFamily.LettersOrDigits, "AzΩ٢")] + [InlineData(GuardFamily.UpperCase, "AΩ 123!")] + [InlineData(GuardFamily.LowerCase, "aω 123!")] + [InlineData(GuardFamily.Base64, "T W\tF\ru\n")] + [InlineData(GuardFamily.Hexadecimal, "09aAfF")] + [InlineData(GuardFamily.NoWhiteSpace, "Az-09")] + [InlineData(GuardFamily.Digits, "")] + [InlineData(GuardFamily.LettersOrDigits, "")] + [InlineData(GuardFamily.UpperCase, "")] + [InlineData(GuardFamily.LowerCase, "")] + [InlineData(GuardFamily.Base64, "")] + [InlineData(GuardFamily.Hexadecimal, "")] + [InlineData(GuardFamily.NoWhiteSpace, "")] + public static void GuardReturnsEveryOriginalReceiverShape(GuardFamily family, string validValue) => + AssertEveryShape(validValue, family); - string.Empty.MustContainOnlyDigits().Should().BeEmpty(); - Span.Empty.MustContainOnlyLettersOrDigits().IsEmpty.Should().BeTrue(); - ReadOnlySpan.Empty.MustBeUpperCase().IsEmpty.Should().BeTrue(); - Memory.Empty.MustBeLowerCase().IsEmpty.Should().BeTrue(); - ReadOnlyMemory.Empty.MustBeBase64().IsEmpty.Should().BeTrue(); - Span.Empty.MustBeHexadecimal().IsEmpty.Should().BeTrue(); - ReadOnlySpan.Empty.MustNotContainWhiteSpace().IsEmpty.Should().BeTrue(); - } + [Theory] + [InlineData(GuardFamily.Digits, "1٢3")] + [InlineData(GuardFamily.LettersOrDigits, "AzΩ٢")] + [InlineData(GuardFamily.UpperCase, "AΩ 123!")] + [InlineData(GuardFamily.LowerCase, "aω 123!")] + [InlineData(GuardFamily.Base64, "T W\tF\ru\n")] + [InlineData(GuardFamily.Hexadecimal, "09aAfF")] + [InlineData(GuardFamily.NoWhiteSpace, "Az-09")] + public static void FactoryGuardReturnsEveryOriginalReceiverShape(GuardFamily family, string validValue) => + AssertEveryFactoryShape(validValue, family); - [Fact] - public static void EveryFactoryGuardReturnsEveryOriginalReceiverShape() + [Theory] + [InlineData(GuardFamily.Digits)] + [InlineData(GuardFamily.LettersOrDigits)] + [InlineData(GuardFamily.UpperCase)] + [InlineData(GuardFamily.LowerCase)] + [InlineData(GuardFamily.Base64)] + [InlineData(GuardFamily.Hexadecimal)] + [InlineData(GuardFamily.NoWhiteSpace)] + public static void NullStringThrowsArgumentNullException(GuardFamily family) { - AssertEveryFactoryShape("1٢3", GuardFamily.Digits); - AssertEveryFactoryShape("AzΩ٢", GuardFamily.LettersOrDigits); - AssertEveryFactoryShape("AΩ 123!", GuardFamily.UpperCase); - AssertEveryFactoryShape("aω 123!", GuardFamily.LowerCase); - AssertEveryFactoryShape("T W\tF\ru\n", GuardFamily.Base64); - AssertEveryFactoryShape("09aAfF", GuardFamily.Hexadecimal); - AssertEveryFactoryShape("Az-09", GuardFamily.NoWhiteSpace); + var act = () => InvokeStringGuard(family, null); + + act.Should().ThrowExactly() + .WithParameterName("value"); } - [Fact] - public static void NullStringsThrowArgumentNullExceptionForEveryDefaultGuard() + [Theory] + [InlineData(GuardFamily.Digits, "a")] + [InlineData(GuardFamily.LettersOrDigits, "!")] + [InlineData(GuardFamily.UpperCase, "a")] + [InlineData(GuardFamily.LowerCase, "A")] + [InlineData(GuardFamily.Base64, "_")] + [InlineData(GuardFamily.Hexadecimal, "g")] + [InlineData(GuardFamily.NoWhiteSpace, " ")] + public static void InvalidStringContentThrowsStringException(GuardFamily family, string invalidValue) { - string nullText = null; + var act = () => InvokeStringGuard(family, invalidValue); - ((Action) (() => nullText.MustContainOnlyDigits())).Should().ThrowExactly() - .WithParameterName(nameof(nullText)); - ((Action) (() => nullText.MustContainOnlyLettersOrDigits())).Should().ThrowExactly(); - ((Action) (() => nullText.MustBeUpperCase())).Should().ThrowExactly(); - ((Action) (() => nullText.MustBeLowerCase())).Should().ThrowExactly(); - ((Action) (() => nullText.MustBeBase64())).Should().ThrowExactly(); - ((Action) (() => nullText.MustBeHexadecimal())).Should().ThrowExactly(); - ((Action) (() => nullText.MustNotContainWhiteSpace())).Should().ThrowExactly(); + act.Should().ThrowExactly(); } - [Fact] - public static void InvalidStringContentThrowsStringExceptionForEveryGuard() - { - AssertStringFailure("a", value => value.MustContainOnlyDigits()); - AssertStringFailure("!", value => value.MustContainOnlyLettersOrDigits()); - AssertStringFailure("a", value => value.MustBeUpperCase()); - AssertStringFailure("A", value => value.MustBeLowerCase()); - AssertStringFailure("_", value => value.MustBeBase64()); - AssertStringFailure("g", value => value.MustBeHexadecimal()); - AssertStringFailure(" ", value => value.MustNotContainWhiteSpace()); - } + [Theory] + [InlineData(GuardFamily.Digits, null)] + [InlineData(GuardFamily.Digits, "a")] + [InlineData(GuardFamily.LettersOrDigits, "!")] + [InlineData(GuardFamily.UpperCase, "a")] + [InlineData(GuardFamily.LowerCase, "A")] + [InlineData(GuardFamily.Base64, "_")] + [InlineData(GuardFamily.Hexadecimal, "g")] + [InlineData(GuardFamily.NoWhiteSpace, null)] + [InlineData(GuardFamily.NoWhiteSpace, " ")] + public static void StringFactoryReceivesNullOrInvalidContent(GuardFamily family, string invalidValue) => + Test.CustomException( + invalidValue, + (value, factory) => InvokeStringFactoryGuard(family, value, factory) + ); + + [Theory] + [InlineData(GuardFamily.Digits, "a")] + [InlineData(GuardFamily.LettersOrDigits, "!")] + [InlineData(GuardFamily.UpperCase, "a")] + [InlineData(GuardFamily.LowerCase, "A")] + [InlineData(GuardFamily.Base64, "_")] + [InlineData(GuardFamily.Hexadecimal, "g")] + [InlineData(GuardFamily.NoWhiteSpace, " ")] + public static void BufferFactoryReceivesEveryInvalidReceiverShape(GuardFamily family, string invalidValue) => + AssertEveryBufferFactoryFailure(invalidValue, family); [Fact] public static void DefaultBufferFailuresCaptureExpressionsAndSupportCustomMessages() @@ -95,55 +128,92 @@ public static void DefaultBufferFailuresCaptureExpressionsAndSupportCustomMessag ); } - [Fact] - public static void EveryStringFactoryReceivesNullAndInvalidContent() - { - string nullText = null; - Test.CustomException(nullText, (value, factory) => value.MustContainOnlyDigits(factory)); - Test.CustomException("a", (value, factory) => value.MustContainOnlyDigits(factory)); - Test.CustomException("!", (value, factory) => value.MustContainOnlyLettersOrDigits(factory)); - Test.CustomException("a", (value, factory) => value.MustBeUpperCase(factory)); - Test.CustomException("A", (value, factory) => value.MustBeLowerCase(factory)); - Test.CustomException("_", (value, factory) => value.MustBeBase64(factory)); - Test.CustomException("g", (value, factory) => value.MustBeHexadecimal(factory)); - Test.CustomException(" ", (value, factory) => value.MustNotContainWhiteSpace(factory)); - } + [Theory] + [InlineData("!", "invalidReadOnlySpan must contain only Unicode letters or decimal digits.")] + public static void DefaultReadOnlySpanFailure_LettersOrDigits(string invalidValue, string expectedMessage) => + AssertReadOnlySpanFailure( + invalidValue, + expectedMessage, + (invalidReadOnlySpan, parameterName) => invalidReadOnlySpan.MustContainOnlyLettersOrDigits(parameterName) + ); - [Fact] - public static void EveryBufferFactoryReceivesEveryInvalidReceiverShape() - { - AssertEveryBufferFactoryFailure("a", GuardFamily.Digits); - AssertEveryBufferFactoryFailure("!", GuardFamily.LettersOrDigits); - AssertEveryBufferFactoryFailure("a", GuardFamily.UpperCase); - AssertEveryBufferFactoryFailure("A", GuardFamily.LowerCase); - AssertEveryBufferFactoryFailure("_", GuardFamily.Base64); - AssertEveryBufferFactoryFailure("g", GuardFamily.Hexadecimal); - AssertEveryBufferFactoryFailure(" ", GuardFamily.NoWhiteSpace); - } + [Theory] + [InlineData("a", "invalidReadOnlySpan must contain no Unicode lowercase characters.")] + public static void DefaultReadOnlySpanFailure_UpperCase(string invalidValue, string expectedMessage) => + AssertReadOnlySpanFailure( + invalidValue, + expectedMessage, + (invalidReadOnlySpan, parameterName) => invalidReadOnlySpan.MustBeUpperCase(parameterName) + ); - [Fact] - public static void WhiteSpaceGuardUsesUnicodeClassificationAtEveryPosition() - { - "\u00A0abc".Invoking(value => value.MustNotContainWhiteSpace()).Should().ThrowExactly(); - "ab\u2003cd".Invoking(value => value.MustNotContainWhiteSpace()).Should().ThrowExactly(); - "abc\u2029".Invoking(value => value.MustNotContainWhiteSpace()).Should().ThrowExactly(); - "abc-def".MustNotContainWhiteSpace().Should().Be("abc-def"); - } - - private static void AssertDigitReturns(string value) => AssertEveryShape(value, GuardFamily.Digits); + [Theory] + [InlineData("A", "invalidReadOnlySpan must contain no Unicode uppercase characters.")] + public static void DefaultReadOnlySpanFailure_LowerCase(string invalidValue, string expectedMessage) => + AssertReadOnlySpanFailure( + invalidValue, + expectedMessage, + (invalidReadOnlySpan, parameterName) => invalidReadOnlySpan.MustBeLowerCase(parameterName) + ); - private static void AssertLetterOrDigitReturns(string value) => - AssertEveryShape(value, GuardFamily.LettersOrDigits); + [Theory] + [InlineData("_", "invalidReadOnlySpan must be valid standard Base64.")] + public static void DefaultReadOnlySpanFailure_Base64(string invalidValue, string expectedMessage) => + AssertReadOnlySpanFailure( + invalidValue, + expectedMessage, + (invalidReadOnlySpan, parameterName) => invalidReadOnlySpan.MustBeBase64(parameterName) + ); - private static void AssertUpperCaseReturns(string value) => AssertEveryShape(value, GuardFamily.UpperCase); + [Theory] + [InlineData("g", "invalidReadOnlySpan must contain only ASCII hexadecimal characters.")] + public static void DefaultReadOnlySpanFailure_Hexadecimal(string invalidValue, string expectedMessage) => + AssertReadOnlySpanFailure( + invalidValue, + expectedMessage, + (invalidReadOnlySpan, parameterName) => invalidReadOnlySpan.MustBeHexadecimal(parameterName) + ); - private static void AssertLowerCaseReturns(string value) => AssertEveryShape(value, GuardFamily.LowerCase); + [Theory] + [InlineData("\u00A0abc")] + [InlineData("ab\u2003cd")] + [InlineData("abc\u2029")] + public static void WhiteSpaceGuardUsesUnicodeClassificationAtEveryPosition(string invalidValue) => + invalidValue.Invoking(value => value.MustNotContainWhiteSpace()) + .Should().ThrowExactly(); - private static void AssertBase64Returns(string value) => AssertEveryShape(value, GuardFamily.Base64); + [Fact] + public static void WhiteSpaceGuardAcceptsContentWithoutWhiteSpace() => + "abc-def".MustNotContainWhiteSpace().Should().Be("abc-def"); - private static void AssertHexadecimalReturns(string value) => AssertEveryShape(value, GuardFamily.Hexadecimal); + private static string InvokeStringGuard(GuardFamily family, string value) => + family switch + { + GuardFamily.Digits => value.MustContainOnlyDigits(), + GuardFamily.LettersOrDigits => value.MustContainOnlyLettersOrDigits(), + GuardFamily.UpperCase => value.MustBeUpperCase(), + GuardFamily.LowerCase => value.MustBeLowerCase(), + GuardFamily.Base64 => value.MustBeBase64(), + GuardFamily.Hexadecimal => value.MustBeHexadecimal(), + GuardFamily.NoWhiteSpace => value.MustNotContainWhiteSpace(), + _ => throw new ArgumentOutOfRangeException(nameof(family), family, null), + }; - private static void AssertNoWhiteSpaceReturns(string value) => AssertEveryShape(value, GuardFamily.NoWhiteSpace); + private static string InvokeStringFactoryGuard( + GuardFamily family, + string value, + Func exceptionFactory + ) => + family switch + { + GuardFamily.Digits => value.MustContainOnlyDigits(exceptionFactory), + GuardFamily.LettersOrDigits => value.MustContainOnlyLettersOrDigits(exceptionFactory), + GuardFamily.UpperCase => value.MustBeUpperCase(exceptionFactory), + GuardFamily.LowerCase => value.MustBeLowerCase(exceptionFactory), + GuardFamily.Base64 => value.MustBeBase64(exceptionFactory), + GuardFamily.Hexadecimal => value.MustBeHexadecimal(exceptionFactory), + GuardFamily.NoWhiteSpace => value.MustNotContainWhiteSpace(exceptionFactory), + _ => throw new ArgumentOutOfRangeException(nameof(family), family, null), + }; private static void AssertEveryShape(string value, GuardFamily family) { @@ -382,11 +452,26 @@ private static void AssertEveryBufferFactoryFailure(string invalidValue, GuardFa } } - private static void AssertStringFailure(string invalidValue, Action guard) => - guard.Invoking(assertion => assertion(invalidValue)) - .Should().ThrowExactly(); + private static void AssertReadOnlySpanFailure( + string invalidValue, + string expectedMessage, + ReadOnlySpanGuard guard + ) + { + var act = () => + { + ReadOnlySpan invalidReadOnlySpan = invalidValue; + guard(invalidReadOnlySpan, nameof(invalidReadOnlySpan)); + }; + + act.Should().ThrowExactly() + .WithParameterName("invalidReadOnlySpan") + .WithMessage($"{expectedMessage}*"); + } + + private delegate ReadOnlySpan ReadOnlySpanGuard(ReadOnlySpan value, string parameterName); - private enum GuardFamily + public enum GuardFamily { Digits, LettersOrDigits, diff --git a/tests/Light.GuardClauses.Tests/StringAssertions/StringInspectionPredicateTests.cs b/tests/Light.GuardClauses.Tests/StringAssertions/StringInspectionPredicateTests.cs index 4f82f9b8..be21db49 100644 --- a/tests/Light.GuardClauses.Tests/StringAssertions/StringInspectionPredicateTests.cs +++ b/tests/Light.GuardClauses.Tests/StringAssertions/StringInspectionPredicateTests.cs @@ -5,118 +5,83 @@ using Light.GuardClauses.Exceptions; using Xunit; using Xunit.Sdk; +using GuardFamily = Light.GuardClauses.Tests.StringAssertions.StringInspectionGuardTests.GuardFamily; namespace Light.GuardClauses.Tests.StringAssertions; +// The string-inspection predicates exist for six families (digits, letters-or-digits, upper case, +// lower case, Base64, and hexadecimal) across five receiver shapes (string, Span, ReadOnlySpan, +// Memory, ReadOnlyMemory). The theories below run one test case per family so that a failing case +// immediately identifies the defective predicate family, while the shape helper keeps the five +// receiver shapes in a Single Point of Truth. public static class StringInspectionPredicateTests { private static readonly Base64Validator PortableBase64Validator = typeof(Check).GetMethod("IsBase64Portable", BindingFlags.NonPublic | BindingFlags.Static)! .CreateDelegate(); - [Fact] - public static void NullAndEmptyInputsHaveTheDefinedSemantics() - { - string nullText = null; - - nullText.ContainsOnlyDigits().Should().BeFalse(); - nullText.ContainsOnlyLettersOrDigits().Should().BeFalse(); - nullText.IsUpperCase().Should().BeFalse(); - nullText.IsLowerCase().Should().BeFalse(); - nullText.IsBase64().Should().BeFalse(); - nullText.IsHexadecimal().Should().BeFalse(); - - string.Empty.ContainsOnlyDigits().Should().BeTrue(); - string.Empty.ContainsOnlyLettersOrDigits().Should().BeTrue(); - string.Empty.IsUpperCase().Should().BeTrue(); - string.Empty.IsLowerCase().Should().BeTrue(); - string.Empty.IsBase64().Should().BeTrue(); - string.Empty.IsHexadecimal().Should().BeTrue(); - - Span.Empty.ContainsOnlyDigits().Should().BeTrue(); - ReadOnlySpan.Empty.ContainsOnlyLettersOrDigits().Should().BeTrue(); - Memory.Empty.IsUpperCase().Should().BeTrue(); - ReadOnlyMemory.Empty.IsLowerCase().Should().BeTrue(); - Span.Empty.IsBase64().Should().BeTrue(); - ReadOnlyMemory.Empty.IsHexadecimal().Should().BeTrue(); - } - - [Fact] - public static void EveryPredicateSupportsEveryReceiverShape() - { - var digits = "1٢3".ToCharArray(); - var lettersOrDigits = "AzΩ٢".ToCharArray(); - var upperCase = "AΩ 123!".ToCharArray(); - var lowerCase = "aω 123!".ToCharArray(); - var base64 = "T W\tF\ru\n".ToCharArray(); - var hexadecimal = "09aAfF".ToCharArray(); - - digits.AsSpan().ContainsOnlyDigits().Should().BeTrue(); - ((ReadOnlySpan) digits).ContainsOnlyDigits().Should().BeTrue(); - digits.AsMemory().ContainsOnlyDigits().Should().BeTrue(); - ((ReadOnlyMemory) digits).ContainsOnlyDigits().Should().BeTrue(); - - lettersOrDigits.AsSpan().ContainsOnlyLettersOrDigits().Should().BeTrue(); - ((ReadOnlySpan) lettersOrDigits).ContainsOnlyLettersOrDigits().Should().BeTrue(); - lettersOrDigits.AsMemory().ContainsOnlyLettersOrDigits().Should().BeTrue(); - ((ReadOnlyMemory) lettersOrDigits).ContainsOnlyLettersOrDigits().Should().BeTrue(); - - upperCase.AsSpan().IsUpperCase().Should().BeTrue(); - ((ReadOnlySpan) upperCase).IsUpperCase().Should().BeTrue(); - upperCase.AsMemory().IsUpperCase().Should().BeTrue(); - ((ReadOnlyMemory) upperCase).IsUpperCase().Should().BeTrue(); - - lowerCase.AsSpan().IsLowerCase().Should().BeTrue(); - ((ReadOnlySpan) lowerCase).IsLowerCase().Should().BeTrue(); - lowerCase.AsMemory().IsLowerCase().Should().BeTrue(); - ((ReadOnlyMemory) lowerCase).IsLowerCase().Should().BeTrue(); - - base64.AsSpan().IsBase64().Should().BeTrue(); - ((ReadOnlySpan) base64).IsBase64().Should().BeTrue(); - base64.AsMemory().IsBase64().Should().BeTrue(); - ((ReadOnlyMemory) base64).IsBase64().Should().BeTrue(); - - hexadecimal.AsSpan().IsHexadecimal().Should().BeTrue(); - ((ReadOnlySpan) hexadecimal).IsHexadecimal().Should().BeTrue(); - hexadecimal.AsMemory().IsHexadecimal().Should().BeTrue(); - ((ReadOnlyMemory) hexadecimal).IsHexadecimal().Should().BeTrue(); - } - [Theory] - [InlineData("x12", "1x2", "12x")] - public static void InvalidContentIsDetectedAtTheBeginningMiddleAndEnd(string beginning, string middle, string end) - { - beginning.ContainsOnlyDigits().Should().BeFalse(); - middle.ContainsOnlyDigits().Should().BeFalse(); - end.ContainsOnlyDigits().Should().BeFalse(); - - "-Az".ContainsOnlyLettersOrDigits().Should().BeFalse(); - "A-z".ContainsOnlyLettersOrDigits().Should().BeFalse(); - "Az-".ContainsOnlyLettersOrDigits().Should().BeFalse(); + [InlineData(GuardFamily.Digits)] + [InlineData(GuardFamily.LettersOrDigits)] + [InlineData(GuardFamily.UpperCase)] + [InlineData(GuardFamily.LowerCase)] + [InlineData(GuardFamily.Base64)] + [InlineData(GuardFamily.Hexadecimal)] + public static void NullStringsFailEveryPredicate(GuardFamily family) => + InvokeStringPredicate(family, null).Should().BeFalse(); - "aAB".IsUpperCase().Should().BeFalse(); - "AaB".IsUpperCase().Should().BeFalse(); - "ABa".IsUpperCase().Should().BeFalse(); + [Theory] + [InlineData(GuardFamily.Digits)] + [InlineData(GuardFamily.LettersOrDigits)] + [InlineData(GuardFamily.UpperCase)] + [InlineData(GuardFamily.LowerCase)] + [InlineData(GuardFamily.Base64)] + [InlineData(GuardFamily.Hexadecimal)] + public static void EmptyInputsPassEveryPredicateAndReceiverShape(GuardFamily family) => + AssertEveryShape(family, string.Empty, true); - "Aab".IsLowerCase().Should().BeFalse(); - "aAb".IsLowerCase().Should().BeFalse(); - "abA".IsLowerCase().Should().BeFalse(); + [Theory] + [InlineData(GuardFamily.Digits, "1٢3")] + [InlineData(GuardFamily.LettersOrDigits, "AzΩ٢")] + [InlineData(GuardFamily.UpperCase, "AΩ 123!")] + [InlineData(GuardFamily.LowerCase, "aω 123!")] + [InlineData(GuardFamily.Base64, "T W\tF\ru\n")] + [InlineData(GuardFamily.Hexadecimal, "09aAfF")] + public static void ValidContentPassesEveryReceiverShape(GuardFamily family, string validValue) => + AssertEveryShape(family, validValue, true); - "x09A".IsHexadecimal().Should().BeFalse(); - "0x9A".IsHexadecimal().Should().BeFalse(); - "09Ax".IsHexadecimal().Should().BeFalse(); - } + [Theory] + [InlineData(GuardFamily.Digits, "x12")] + [InlineData(GuardFamily.Digits, "1x2")] + [InlineData(GuardFamily.Digits, "12x")] + [InlineData(GuardFamily.LettersOrDigits, "-Az")] + [InlineData(GuardFamily.LettersOrDigits, "A-z")] + [InlineData(GuardFamily.LettersOrDigits, "Az-")] + [InlineData(GuardFamily.UpperCase, "aAB")] + [InlineData(GuardFamily.UpperCase, "AaB")] + [InlineData(GuardFamily.UpperCase, "ABa")] + [InlineData(GuardFamily.LowerCase, "Aab")] + [InlineData(GuardFamily.LowerCase, "aAb")] + [InlineData(GuardFamily.LowerCase, "abA")] + [InlineData(GuardFamily.Hexadecimal, "x09A")] + [InlineData(GuardFamily.Hexadecimal, "0x9A")] + [InlineData(GuardFamily.Hexadecimal, "09Ax")] + public static void InvalidContentIsDetectedAtTheBeginningMiddleAndEnd(GuardFamily family, string invalidValue) => + InvokeStringPredicate(family, invalidValue).Should().BeFalse(); - [Fact] - public static void CasingAllowsUncasedContentAndUsesUnicodeClassification() - { - "123 !\t中".IsUpperCase().Should().BeTrue(); - "123 !\t中".IsLowerCase().Should().BeTrue(); - "ÄΩ".IsUpperCase().Should().BeTrue(); - "äω".IsLowerCase().Should().BeTrue(); - "Äω".IsUpperCase().Should().BeFalse(); - "äΩ".IsLowerCase().Should().BeFalse(); - } + [Theory] + [InlineData(GuardFamily.UpperCase, "123 !\t中", true)] + [InlineData(GuardFamily.LowerCase, "123 !\t中", true)] + [InlineData(GuardFamily.UpperCase, "ÄΩ", true)] + [InlineData(GuardFamily.LowerCase, "äω", true)] + [InlineData(GuardFamily.UpperCase, "Äω", false)] + [InlineData(GuardFamily.LowerCase, "äΩ", false)] + public static void CasingAllowsUncasedContentAndUsesUnicodeClassification( + GuardFamily family, + string value, + bool expected + ) => + InvokeStringPredicate(family, value).Should().Be(expected); [Theory] [InlineData("", true)] @@ -148,7 +113,8 @@ public static void Base64AcceptsOnlyTheDefinedAlphabetStructureAndWhiteSpace(str public static void PortableBase64ValidatorMatchesNet10OverDeterministicCorpus() { var random = new Random(153); - const string corpusCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=_- \t\r\n\v\u00A0!"; + const string corpusCharacters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=_- \t\r\n\v\u00A0!"; for (var iteration = 0; iteration < 10_000; ++iteration) { @@ -180,22 +146,104 @@ public static void EveryUtf16CodeUnitMatchesTheDefiningClassifiers() { var character = (char) codeUnit; value[0] = character; + AssertEqual(char.IsDigit(character), character.IsDigit(), character, "digit character"); + AssertEqual(char.IsLetter(character), character.IsLetter(), character, "letter character"); AssertEqual(char.IsDigit(character), ((ReadOnlySpan) value).ContainsOnlyDigits(), character, "digit"); - AssertEqual(char.IsLetterOrDigit(character), ((ReadOnlySpan) value).ContainsOnlyLettersOrDigits(), character, "letter or digit"); + AssertEqual( + char.IsLetterOrDigit(character), + ((ReadOnlySpan) value).ContainsOnlyLettersOrDigits(), + character, + "letter or digit" + ); AssertEqual(!char.IsLower(character), ((ReadOnlySpan) value).IsUpperCase(), character, "upper case"); AssertEqual(!char.IsUpper(character), ((ReadOnlySpan) value).IsLowerCase(), character, "lower case"); - AssertEqual(IsAsciiHexDigit(character), ((ReadOnlySpan) value).IsHexadecimal(), character, "hexadecimal"); + AssertEqual( + IsAsciiHexDigit(character), + ((ReadOnlySpan) value).IsHexadecimal(), + character, + "hexadecimal" + ); AssertEqual(!char.IsWhiteSpace(character), IsAcceptedByWhiteSpaceGuard(value), character, "white space"); } } + private static bool InvokeStringPredicate(GuardFamily family, string value) => + family switch + { + GuardFamily.Digits => value.ContainsOnlyDigits(), + GuardFamily.LettersOrDigits => value.ContainsOnlyLettersOrDigits(), + GuardFamily.UpperCase => value.IsUpperCase(), + GuardFamily.LowerCase => value.IsLowerCase(), + GuardFamily.Base64 => value.IsBase64(), + GuardFamily.Hexadecimal => value.IsHexadecimal(), + _ => throw new ArgumentOutOfRangeException(nameof(family), family, null), + }; + + private static void AssertEveryShape(GuardFamily family, string value, bool expected) + { + var characters = value.ToCharArray(); + var span = characters.AsSpan(); + ReadOnlySpan readOnlySpan = characters; + var memory = characters.AsMemory(); + ReadOnlyMemory readOnlyMemory = characters; + + switch (family) + { + case GuardFamily.Digits: + value.ContainsOnlyDigits().Should().Be(expected); + span.ContainsOnlyDigits().Should().Be(expected); + readOnlySpan.ContainsOnlyDigits().Should().Be(expected); + memory.ContainsOnlyDigits().Should().Be(expected); + readOnlyMemory.ContainsOnlyDigits().Should().Be(expected); + break; + case GuardFamily.LettersOrDigits: + value.ContainsOnlyLettersOrDigits().Should().Be(expected); + span.ContainsOnlyLettersOrDigits().Should().Be(expected); + readOnlySpan.ContainsOnlyLettersOrDigits().Should().Be(expected); + memory.ContainsOnlyLettersOrDigits().Should().Be(expected); + readOnlyMemory.ContainsOnlyLettersOrDigits().Should().Be(expected); + break; + case GuardFamily.UpperCase: + value.IsUpperCase().Should().Be(expected); + span.IsUpperCase().Should().Be(expected); + readOnlySpan.IsUpperCase().Should().Be(expected); + memory.IsUpperCase().Should().Be(expected); + readOnlyMemory.IsUpperCase().Should().Be(expected); + break; + case GuardFamily.LowerCase: + value.IsLowerCase().Should().Be(expected); + span.IsLowerCase().Should().Be(expected); + readOnlySpan.IsLowerCase().Should().Be(expected); + memory.IsLowerCase().Should().Be(expected); + readOnlyMemory.IsLowerCase().Should().Be(expected); + break; + case GuardFamily.Base64: + value.IsBase64().Should().Be(expected); + span.IsBase64().Should().Be(expected); + readOnlySpan.IsBase64().Should().Be(expected); + memory.IsBase64().Should().Be(expected); + readOnlyMemory.IsBase64().Should().Be(expected); + break; + case GuardFamily.Hexadecimal: + value.IsHexadecimal().Should().Be(expected); + span.IsHexadecimal().Should().Be(expected); + readOnlySpan.IsHexadecimal().Should().Be(expected); + memory.IsHexadecimal().Should().Be(expected); + readOnlyMemory.IsHexadecimal().Should().Be(expected); + break; + default: throw new ArgumentOutOfRangeException(nameof(family), family, null); + } + } + private static void AssertPortableBase64MatchesFramework(ReadOnlySpan value) { var expected = Base64.IsValid(value); var actual = PortableBase64Validator(value); if (actual != expected) { - throw new XunitException($"Portable Base64 mismatch for '{value.ToString()}': expected {expected}, actual {actual}."); + throw new XunitException( + $"Portable Base64 mismatch for '{value.ToString()}': expected {expected}, actual {actual}." + ); } } @@ -210,13 +258,14 @@ private static string InsertWhiteSpace(string value) for (var index = 0; index < value.Length; ++index) { result[index * 2] = value[index]; - result[index * 2 + 1] = (char) (index % 4 switch - { - 0 => ' ', - 1 => '\t', - 2 => '\r', - _ => '\n', - }); + result[index * 2 + 1] = (char) (index % + 4 switch + { + 0 => ' ', + 1 => '\t', + 2 => '\r', + _ => '\n', + }); } return new (result); diff --git a/tests/Light.GuardClauses.Tests/Test.cs b/tests/Light.GuardClauses.Tests/Test.cs index eadb215f..3776903e 100644 --- a/tests/Light.GuardClauses.Tests/Test.cs +++ b/tests/Light.GuardClauses.Tests/Test.cs @@ -69,7 +69,7 @@ Exception ExceptionFactory(ReadOnlySpan parameter) } catch (ExceptionDummy exception) { - exception.Should().BeSameAs(exception); + exception.Should().BeSameAs(Exception); capturedParameter.Should().Equal(invalidValue.ToArray()); } } @@ -91,7 +91,7 @@ Exception ExceptionFactory(ReadOnlySpan parameter) } catch (ExceptionDummy exception) { - exception.Should().BeSameAs(exception); + exception.Should().BeSameAs(Exception); capturedParameter.Should().Equal(invalidValue.ToArray()); } } @@ -116,7 +116,7 @@ Exception ExceptionFactory(ReadOnlySpan parameter) } catch (ExceptionDummy exception) { - exception.Should().BeSameAs(exception); + exception.Should().BeSameAs(Exception); capturedParameter.ToArray().Should().Equal(invalidValue.ToArray()); } } @@ -141,7 +141,7 @@ Exception ExceptionFactory(ReadOnlySpan parameter) } catch (ExceptionDummy exception) { - exception.Should().BeSameAs(exception); + exception.Should().BeSameAs(Exception); capturedParameter.ToArray().Should().Equal(invalidValue.ToArray()); } } @@ -198,7 +198,7 @@ Exception ExceptionFactory(ReadOnlySpan parameter, T2 second) } catch (ExceptionDummy exception) { - exception.Should().BeSameAs(exception); + exception.Should().BeSameAs(Exception); capturedFirst.Should().Equal(invalidValue.ToArray()); capturedSecond.Should().Be(additionalValue); } @@ -227,7 +227,7 @@ Exception ExceptionFactory(ReadOnlySpan parameter, T2 second) } catch (ExceptionDummy exception) { - exception.Should().BeSameAs(exception); + exception.Should().BeSameAs(Exception); capturedFirst.Should().Equal(invalidValue.ToArray()); capturedSecond.Should().Be(additionalValue); } @@ -256,7 +256,7 @@ Exception ExceptionFactory(ReadOnlySpan first, T2 second) } catch (ExceptionDummy exception) { - exception.Should().BeSameAs(exception); + exception.Should().BeSameAs(Exception); capturedFirst.ToArray().Should().Equal(invalidValue.ToArray()); capturedSecond.Should().Be(additionalValue); } @@ -285,7 +285,7 @@ Exception ExceptionFactory(ReadOnlySpan first, T2 second) } catch (ExceptionDummy exception) { - exception.Should().BeSameAs(exception); + exception.Should().BeSameAs(Exception); capturedFirst.ToArray().Should().Equal(invalidValue.ToArray()); capturedSecond.Should().Be(additionalValue); } diff --git a/tests/Light.GuardClauses.Tests/TypeAssertions/DerivesFromTests.cs b/tests/Light.GuardClauses.Tests/TypeAssertions/DerivesFromTests.cs index c9d75d0b..8cfc3a7f 100644 --- a/tests/Light.GuardClauses.Tests/TypeAssertions/DerivesFromTests.cs +++ b/tests/Light.GuardClauses.Tests/TypeAssertions/DerivesFromTests.cs @@ -62,6 +62,16 @@ public static void GenericTypeDefinitionOfSameType() => public static void WrongBaseType() => TestIsDerivingFrom(typeof(Exception), typeof(ArgumentException), false); + [Fact] + public static void WrongBaseTypeWithCustomTypeComparer() => + typeof(Exception).DerivesFrom(typeof(ArgumentException), EquivalentTypeComparer.Instance) + .Should().BeFalse(); + + [Fact] + public static void ValidBaseClassWithCustomTypeComparer() => + typeof(StringDictionary).DerivesFrom(typeof(Dictionary<,>), EquivalentTypeComparer.Instance) + .Should().BeTrue(); + private static void TestIsDerivingFrom(Type type, Type baseClass, bool expected) => type.DerivesFrom(baseClass).Should().Be(expected); @@ -70,7 +80,7 @@ public static void TypeNull() { var type = default(Type); - // ReSharper disable once ExpressionIsAlwaysNull + // ReSharper disable once AssignNullToNotNullAttribute Action act = () => type.DerivesFrom(typeof(object)); act.Should().Throw() @@ -82,7 +92,7 @@ public static void BaseClassNull() { var baseClass = default(Type); - // ReSharper disable once ExpressionIsAlwaysNull + // ReSharper disable once AssignNullToNotNullAttribute Action act = () => typeof(string).DerivesFrom(baseClass); act.Should().Throw() @@ -90,4 +100,4 @@ public static void BaseClassNull() } public sealed class StringDictionary : Dictionary { } -} \ No newline at end of file +} diff --git a/tests/Light.GuardClauses.Tests/TypeAssertions/EquivalentTypeComparerTests.cs b/tests/Light.GuardClauses.Tests/TypeAssertions/EquivalentTypeComparerTests.cs new file mode 100644 index 00000000..10c5d5b3 --- /dev/null +++ b/tests/Light.GuardClauses.Tests/TypeAssertions/EquivalentTypeComparerTests.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using FluentAssertions; +using Xunit; + +namespace Light.GuardClauses.Tests.TypeAssertions; + +public static class EquivalentTypeComparerTests +{ + [Theory] + [MemberData(nameof(EqualityData))] + public static void CheckEquality(Type x, Type y, bool expected) => + EquivalentTypeComparer.Instance.Equals(x, y).Should().Be(expected); + + public static readonly TheoryData EqualityData = + new () + { + { typeof(string), typeof(string), true }, + { typeof(IComparer), typeof(IEnumerator), false }, + { typeof(List), typeof(List), true }, + { typeof(List), typeof(List<>), true }, + { typeof(List<>), typeof(List), true }, + { typeof(IDictionary<,>), typeof(IDictionary<,>), true }, + { typeof(IDictionary<,>), typeof(Dictionary<,>), false }, + { null!, typeof(Dictionary<,>), false }, + { typeof(double), null!, false }, + { null!, null!, true }, + }; + + [Theory] + [MemberData(nameof(HashCodesShouldBeEqualData))] + public static void HashCodesOfEquivalentTypesAreEqual(Type x, Type y) => + EquivalentTypeComparer.Instance.GetHashCode(x).Should().Be(EquivalentTypeComparer.Instance.GetHashCode(y)); + + public static readonly TheoryData HashCodesShouldBeEqualData = + new () + { + { typeof(int), typeof(int) }, + { typeof(IList), typeof(IList<>) }, + { typeof(IDictionary), typeof(IDictionary<,>) }, + }; + + [Fact] + public static void HashCodeOfNullIsZero() => + EquivalentTypeComparer.Instance.GetHashCode(null!).Should().Be(0); +} diff --git a/tests/Light.GuardClauses.Tests/TypeAssertions/EquivalentTypeCompererTests.cs b/tests/Light.GuardClauses.Tests/TypeAssertions/EquivalentTypeCompererTests.cs deleted file mode 100644 index e58b22c9..00000000 --- a/tests/Light.GuardClauses.Tests/TypeAssertions/EquivalentTypeCompererTests.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using FluentAssertions; -using Xunit; - -namespace Light.GuardClauses.Tests.TypeAssertions; - -public static class EquivalentTypeCompererTests -{ - [Fact] - public static void CheckEquality() - { - CheckEquals(typeof(string), typeof(string), true); - CheckEquals(typeof(IComparer), typeof(IEnumerator), false); - CheckEquals(typeof(List), typeof(List), true); - CheckEquals(typeof(List), typeof(List<>), true); - CheckEquals(typeof(List<>), typeof(List), true); - CheckEquals(typeof(IDictionary<,>), typeof(IDictionary<,>), true); - CheckEquals(typeof(IDictionary<,>), typeof(Dictionary<,>), false); - CheckEquals(null, typeof(Dictionary<,>), false); - CheckEquals(typeof(double), null, false); - CheckEquals(null, null, true); - } - - private static void CheckEquals(Type x, Type y, bool expected) => - new EquivalentTypeComparer().Equals(x, y).Should().Be(expected); - - [Fact] - public static void HashCode() - { - CheckGetHashCode(typeof(int), typeof(int), true); - CheckGetHashCode(typeof(double), typeof(object), false); - CheckGetHashCode(typeof(IList), typeof(IList<>), true); - CheckGetHashCode(typeof(IDictionary), typeof(IDictionary<,>), true); - CheckGetHashCode(typeof(Dictionary), typeof(IDictionary), false); - CheckGetHashCode(typeof(Dictionary), typeof(IDictionary<,>), false); - CheckGetHashCode(null, typeof(int), false); - CheckGetHashCode(typeof(short), null, false); - CheckGetHashCode(null, null, false); - } - - private static void CheckGetHashCode(Type x, Type y, bool shouldBeEqualHashCodes) - { - var comparer = new EquivalentTypeComparer(); - var firstHashCode = comparer.GetHashCode(x); - var secondHashCode = comparer.GetHashCode(y); - if (shouldBeEqualHashCodes) - firstHashCode.Should().Be(secondHashCode); - } -} \ No newline at end of file diff --git a/tests/Light.GuardClauses.Tests/TypeAssertions/ImplementsTests.cs b/tests/Light.GuardClauses.Tests/TypeAssertions/ImplementsTests.cs index ce02836c..9d582d2d 100644 --- a/tests/Light.GuardClauses.Tests/TypeAssertions/ImplementsTests.cs +++ b/tests/Light.GuardClauses.Tests/TypeAssertions/ImplementsTests.cs @@ -38,7 +38,7 @@ public static void TypeNull() { var type = default(Type); - // ReSharper disable once ExpressionIsAlwaysNull + // ReSharper disable once AssignNullToNotNullAttribute Action act = () => type.Implements(typeof(IComparable)); act.Should().Throw() @@ -50,13 +50,23 @@ public static void InterfaceTypeNull() { var interfaceType = default(Type); - // ReSharper disable once ExpressionIsAlwaysNull + // ReSharper disable once AssignNullToNotNullAttribute Action act = () => typeof(List).Implements(interfaceType); act.Should().Throw() .WithParameterName(nameof(interfaceType)); } - private static void CheckImplements(Type type, Type interfaceType, bool expected) => + [Fact] + public static void InterfaceNotImplementedWithCustomTypeComparer() => + typeof(Exception).Implements(typeof(IObservable<>), EquivalentTypeComparer.Instance) + .Should().BeFalse(); + + [Fact] + public static void InterfaceImplementedWithCustomTypeComparer() => + typeof(List).Implements(typeof(IList<>), EquivalentTypeComparer.Instance) + .Should().BeTrue(); + + private static void CheckImplements(Type type, Type interfaceType, bool expected) => type.Implements(interfaceType).Should().Be(expected); -} \ No newline at end of file +} diff --git a/tests/Light.GuardClauses.Tests/UriAssertions/MustBeHttpOrHttpsUrlTests.cs b/tests/Light.GuardClauses.Tests/UriAssertions/MustBeHttpOrHttpsUrlTests.cs new file mode 100644 index 00000000..e5dc57b4 --- /dev/null +++ b/tests/Light.GuardClauses.Tests/UriAssertions/MustBeHttpOrHttpsUrlTests.cs @@ -0,0 +1,51 @@ +using System; +using FluentAssertions; +using Light.GuardClauses.Exceptions; +using Xunit; + +namespace Light.GuardClauses.Tests.UriAssertions; + +public static class MustBeHttpOrHttpsUrlTests +{ + [Theory] + [InlineData("http://example.com/")] + [InlineData("https://example.com/")] + public static void HttpAndHttpsUrlsAreAccepted(string url) + { + var uri = new Uri(url); + + uri.MustBeHttpOrHttpsUrl().Should().BeSameAs(uri); + } + + [Fact] + public static void OtherSchemeIsRejected() + { + var ftpUrl = new Uri("ftp://example.com"); + + var act = () => ftpUrl.MustBeHttpOrHttpsUrl(); + + act.Should().Throw() + .WithParameterName(nameof(ftpUrl)); + } + + [Fact] + public static void CustomMessage() => + Test.CustomMessage( + message => new Uri("ftp://example.com").MustBeHttpOrHttpsUrl(message: message) + ); + + [Fact] + public static void CustomException() => + Test.CustomException( + new Uri("ftp://example.com"), + (url, exceptionFactory) => url.MustBeHttpOrHttpsUrl(exceptionFactory) + ); + + [Fact] + public static void CustomExceptionNotThrown() + { + var url = new Uri("https://example.com"); + + url.MustBeHttpOrHttpsUrl(_ => new ()).Should().BeSameAs(url); + } +} diff --git a/tests/Light.GuardClauses.Tests/UriAssertions/MustHaveOneSchemeOfTests.cs b/tests/Light.GuardClauses.Tests/UriAssertions/MustHaveOneSchemeOfTests.cs index ae9201e0..7cfb5270 100644 --- a/tests/Light.GuardClauses.Tests/UriAssertions/MustHaveOneSchemeOfTests.cs +++ b/tests/Light.GuardClauses.Tests/UriAssertions/MustHaveOneSchemeOfTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; using FluentAssertions; using Light.GuardClauses.Exceptions; @@ -18,10 +19,12 @@ public static void SchemeNotPresent(string uri, string[] schemes) Action act = () => new Uri(uri).MustHaveOneSchemeOf(schemes, nameof(uri)); var exceptionAssertion = act.Should().Throw().Which; - exceptionAssertion.Message.Should().Contain(new StringBuilder().AppendLine($"{nameof(uri)} must use one of the following schemes") - .AppendItemsWithNewLine(schemes) - .AppendLine($"but it actually is \"{uri}\".") - .ToString()); + exceptionAssertion.Message.Should().Contain( + new StringBuilder().AppendLine($"{nameof(uri)} must use one of the following schemes") + .AppendItemsWithNewLine(schemes) + .AppendLine($"but it actually is \"{uri}\".") + .ToString() + ); exceptionAssertion.ParamName.Should().BeSameAs(nameof(uri)); } @@ -35,6 +38,36 @@ public static void SchemePresent(string uri, string[] schemes) instance.MustHaveOneSchemeOf(schemes).Should().BeSameAs(instance); } + [Fact] + public static void SchemePresentInLazySchemes() + { + var instance = new Uri("https://www.example.com"); + var schemes = new[] { "http", "https" }.Select(scheme => scheme); + + instance.MustHaveOneSchemeOf(schemes).Should().BeSameAs(instance); + } + + [Fact] + public static void SchemeNotPresentInLazySchemes() + { + var uri = new Uri("ftp://example.com"); + var schemes = new[] { "http", "https" }.Select(scheme => scheme); + + var act = () => uri.MustHaveOneSchemeOf(schemes); + + act.Should().Throw() + .WithParameterName(nameof(uri)); + } + + [Fact] + public static void CustomExceptionNotThrownForLazySchemes() + { + var url = new Uri("https://www.example.com"); + var schemes = new[] { "http", "https" }.Select(scheme => scheme); + + url.MustHaveOneSchemeOf(schemes, (_, _) => new ()).Should().BeSameAs(url); + } + [Fact] public static void UriNull() { @@ -54,33 +87,43 @@ public static void UriRelative() [Theory] [MemberData(nameof(CustomExceptionData))] public static void CustomException(Uri url, List urlSchemes) => - Test.CustomException(url, - urlSchemes, - (uri, schemes, exceptionFactory) => uri.MustHaveOneSchemeOf(schemes, exceptionFactory)); + Test.CustomException( + url, + urlSchemes, + (uri, schemes, exceptionFactory) => uri.MustHaveOneSchemeOf(schemes, exceptionFactory) + ); public static readonly TheoryData> CustomExceptionData = new () { - { new Uri("https://www.microsoft.com"), new List { "http", "ftp" } }, - { null, new List { "http", "https" } }, - { new Uri("https://github.com"), null } + { new ("https://www.microsoft.com"), new() { "http", "ftp" } }, + { null, new() { "http", "https" } }, + { new ("https://github.com"), null }, }; [Fact] public static void CustomMessage() => - Test.CustomMessage(message => new Uri("https://go.com").MustHaveOneSchemeOf(new[] { "http" }, message: message)); + Test.CustomMessage( + message => new Uri("https://go.com").MustHaveOneSchemeOf(new[] { "http" }, message: message) + ); [Fact] public static void CustomMessageUrlRelative() => - Test.CustomMessage(message => new Uri("/api", UriKind.Relative).MustHaveOneSchemeOf(new[] { "https" }, message: message)); + Test.CustomMessage( + message => new Uri("/api", UriKind.Relative).MustHaveOneSchemeOf(new[] { "https" }, message: message) + ); [Fact] public static void CustomMessageUrlNull() => - Test.CustomMessage(message => ((Uri) null).MustHaveOneSchemeOf(new[] { "http" }, message: message)); + Test.CustomMessage( + message => ((Uri) null).MustHaveOneSchemeOf(new[] { "http" }, message: message) + ); [Fact] public static void CustomMessageSchemesNull() => - Test.CustomMessage(message => new Uri("https://www.dict.cc").MustHaveOneSchemeOf(null!, message: message)); + Test.CustomMessage( + message => new Uri("https://www.dict.cc").MustHaveOneSchemeOf(null!, message: message) + ); [Fact] public static void CallerArgumentExpression() @@ -92,4 +135,4 @@ public static void CallerArgumentExpression() act.Should().Throw() .WithParameterName(nameof(myUrl)); } -} \ No newline at end of file +} diff --git a/tests/Light.GuardClauses.Tests/UriAssertions/MustHaveSchemeTests.cs b/tests/Light.GuardClauses.Tests/UriAssertions/MustHaveSchemeTests.cs index a2ff61e3..f276411d 100644 --- a/tests/Light.GuardClauses.Tests/UriAssertions/MustHaveSchemeTests.cs +++ b/tests/Light.GuardClauses.Tests/UriAssertions/MustHaveSchemeTests.cs @@ -7,21 +7,24 @@ namespace Light.GuardClauses.Tests.UriAssertions; public static class MustHaveSchemeTests { - [Theory(DisplayName = "MustHaveScheme must throw an InvalidUriSchemeException when the URI does not have the specified scheme.")] + [Theory( + DisplayName = + "MustHaveScheme must throw an InvalidUriSchemeException when the URI does not have the specified scheme." + )] [MemberData(nameof(InvalidSchemeData))] public static void InvalidScheme(Uri uri, string scheme) { - Action act = () => uri.MustHaveScheme(scheme, nameof(uri)); + Action act = () => uri.MustHaveScheme(scheme); act.Should().Throw() .And.Message.Should().Contain($"{nameof(uri)} must use the scheme \"{scheme}\""); } public static readonly TheoryData InvalidSchemeData = - new() + new () { - { new Uri("http://localhost:8080"), "https" }, - { new Uri("http://my.service.com/upload"), "ftp" } + { new ("http://localhost:8080"), "https" }, + { new ("http://my.service.com/upload"), "ftp" }, }; [Theory(DisplayName = "MustHaveScheme must not throw an exception when the specified scheme is used by the URI.")] @@ -34,10 +37,10 @@ public static void ValidScheme(Uri uri, string scheme) } public static readonly TheoryData ValidSchemeData = - new() + new () { - { new Uri("https://www.google.com"), "https" }, - { new Uri("ftps://192.168.177.2"), "ftps" } + { new ("https://www.google.com"), "https" }, + { new ("ftps://192.168.177.2"), "ftps" }, }; [Fact(DisplayName = "MustHaveScheme must throw an ArgumentNullException when the specified URI is null.")] @@ -59,28 +62,39 @@ public static void UriRelative() [Theory] [MemberData(nameof(CustomExceptionData))] public static void CustomException(Uri uri, string urlScheme) => - Test.CustomException(uri, - urlScheme, - (url, scheme, exceptionFactory) => url.MustHaveScheme(scheme, exceptionFactory)); + Test.CustomException( + uri, + urlScheme, + (url, scheme, exceptionFactory) => url.MustHaveScheme(scheme, exceptionFactory) + ); public static readonly TheoryData CustomExceptionData = - new() + new () { - { new Uri("https://www.microsoft.com"), "http" }, + { new ("https://www.microsoft.com"), "http" }, { null, "ftp" }, - { new Uri("https://duckduckgo.com/", UriKind.Absolute), null } + { new ("https://duckduckgo.com/", UriKind.Absolute), null }, }; [Fact] - public static void CustomExceptionNoScheme() => - Test.CustomException(new Uri("/contact", UriKind.Relative), - (url, exceptionFactory) => url.MustHaveScheme("ssl", exceptionFactory)); + public static void CustomExceptionNoScheme() => + Test.CustomException( + new Uri("/contact", UriKind.Relative), + (url, exceptionFactory) => url.MustHaveScheme("ssl", exceptionFactory) + ); + + [Fact] + public static void CustomExceptionNoSchemeInvalidScheme() => + Test.CustomException( + new Uri("http://www.example.com"), + (url, exceptionFactory) => url.MustHaveScheme("https", exceptionFactory) + ); [Fact] public static void CustomExceptionNoSchemeUriValid() { var url = new Uri("https://www.hbo.com/westworld"); - url.MustHaveScheme("https", _ => new Exception()).Should().BeSameAs(url); + url.MustHaveScheme("https", _ => new ()).Should().BeSameAs(url); } [Fact] @@ -92,10 +106,12 @@ public static void CustomExceptionSchemeIsValid() [Fact] public static void CustomMessage() => - Test.CustomMessage(message => new Uri("ftp://foo.com").MustHaveScheme("https", message: message)); + Test.CustomMessage( + message => new Uri("ftp://foo.com").MustHaveScheme("https", message: message) + ); [Fact] - public static void CustomMessageUriNull() => + public static void CustomMessageUriNull() => Test.CustomMessage(message => ((Uri) null).MustHaveScheme("http", message: message)); [Fact] @@ -108,4 +124,4 @@ public static void CallerArgumentExpression() act.Should().Throw() .WithParameterName(nameof(myUrl)); } -} \ No newline at end of file +}