From 8b65f505f6eb628a5d8993a90fc99f169951a465 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Tue, 14 Jul 2026 20:18:54 +0200 Subject: [PATCH 1/4] chore: add AGENTS.md for testing Signed-off-by: Kenny Pflug --- AGENTS.md | 4 ++++ Light.GuardClauses.slnx | 3 +++ tests/AGENTS.md | 13 +++++++++++++ 3 files changed, 20 insertions(+) create mode 100644 tests/AGENTS.md diff --git a/AGENTS.md b/AGENTS.md index df13986f..a5672420 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,6 +10,10 @@ Plans typically have acceptance criteria with check boxes. Check each box when y Read ./ai-plans/AGENTS.md for details on how to write plans. +## Test Rules + +Read ./tests/AGENTS.md for details on how to write tests. + ## Here is Your Space If you encounter something worth noting while you are working on this code base, write it down here in this section. Once you are finished, I will discuss it with you, and we can decide where to put your notes. diff --git a/Light.GuardClauses.slnx b/Light.GuardClauses.slnx index 8f69c999..411b4adf 100644 --- a/Light.GuardClauses.slnx +++ b/Light.GuardClauses.slnx @@ -15,9 +15,12 @@ + + + diff --git a/tests/AGENTS.md b/tests/AGENTS.md new file mode 100644 index 00000000..c68ee7b9 --- /dev/null +++ b/tests/AGENTS.md @@ -0,0 +1,13 @@ +# AGENTS.md for Testing + +## How to structure tests + +- Do not use mocking frameworks like Moq or NSubstitute for test doubles, use hand-written test doubles instead. +- Use FluentAssertions instead of xunit's `Assert` class. The library is pinned to 7.x.x to avoid licensing issues. +- Keep Test Coverage at least at 93%. Microsoft.Testing.Extensions.CodeCoverage is available to get test coverage metrics. +- Prefer Sociable Tests as proposed by Martin Fowler. Only use Solitary Tests as a last resort. + +# How to run tests + +- `dotnet test` for usual test runs. +- `dotnet test -- --coverage --coverage-output-format cobertura` for test coverage metrics. From a5040bdd7c11dd53a24640b45d7ece222c8f4a1a Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Tue, 14 Jul 2026 21:24:48 +0200 Subject: [PATCH 2/4] test: increase code coverage for Light.GuardClauses project Signed-off-by: Kenny Pflug --- .../NumericCustomFactorySuccessTests.cs | 71 ++++++++ .../EnumerableCountTests.cs | 55 ++++++ .../MultiplyAddHashTests.cs | 164 +++++++++++++++++- .../TextExtensionsTests.cs | 25 +++ .../ToStringRepresentationTests.cs | 45 ++++- .../StringAssertions/AsciiTests.cs | 76 ++++++-- .../MustBeEmailAddressTests.cs | 46 ++++- .../StringAssertions/MustNotStartWithTests.cs | 60 ++++++- 8 files changed, 505 insertions(+), 37 deletions(-) create mode 100644 tests/Light.GuardClauses.Tests/ComparableAssertions/NumericCustomFactorySuccessTests.cs create mode 100644 tests/Light.GuardClauses.Tests/FrameworkExtensions/EnumerableCountTests.cs create mode 100644 tests/Light.GuardClauses.Tests/FrameworkExtensions/TextExtensionsTests.cs diff --git a/tests/Light.GuardClauses.Tests/ComparableAssertions/NumericCustomFactorySuccessTests.cs b/tests/Light.GuardClauses.Tests/ComparableAssertions/NumericCustomFactorySuccessTests.cs new file mode 100644 index 00000000..24271fb9 --- /dev/null +++ b/tests/Light.GuardClauses.Tests/ComparableAssertions/NumericCustomFactorySuccessTests.cs @@ -0,0 +1,71 @@ +using System; +using FluentAssertions; +using Xunit; + +namespace Light.GuardClauses.Tests.ComparableAssertions; + +public static class NumericCustomFactorySuccessTests +{ + [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); + } +} diff --git a/tests/Light.GuardClauses.Tests/FrameworkExtensions/EnumerableCountTests.cs b/tests/Light.GuardClauses.Tests/FrameworkExtensions/EnumerableCountTests.cs new file mode 100644 index 00000000..15296511 --- /dev/null +++ b/tests/Light.GuardClauses.Tests/FrameworkExtensions/EnumerableCountTests.cs @@ -0,0 +1,55 @@ +using System.Collections; +using System.Collections.Generic; +using FluentAssertions; +using Light.GuardClauses.FrameworkExtensions; +using Xunit; + +namespace Light.GuardClauses.Tests.FrameworkExtensions; + +public static class EnumerableCountTests +{ + [Fact] + public static void CountWithExceptionDetailsUsesStringLengthWithoutEnumerating() + { + IEnumerable value = "text"; + + value.Count("value", "message").Should().Be(4); + } + + [Fact] + public static void GenericCountWithExceptionDetailsUsesEveryAvailableStrategy() + { + IEnumerable text = "text"; + var collection = new HashSet { 1, 2, 3 }; + var readOnlyCollection = new ReadOnlyCollectionOnly([1, 2, 3, 4]); + + text.GetCount("text", "message").Should().Be(4); + collection.GetCount("collection", "message").Should().Be(3); + readOnlyCollection.GetCount("readOnlyCollection", "message").Should().Be(4); + Yield(1, 2, 3, 4, 5).GetCount("items", "message").Should().Be(5); + } + + [Fact] + public static void IsOneOfEnumeratesSourcesThatAreNotCollections() + { + 2.IsOneOf(Yield(1, 2, 3)).Should().BeTrue(); + 4.IsOneOf(Yield(1, 2, 3)).Should().BeFalse(); + } + + private static IEnumerable Yield(params T[] items) + { + foreach (var item in items) + { + yield return item; + } + } + + private sealed class ReadOnlyCollectionOnly(IReadOnlyCollection items) : IReadOnlyCollection + { + public int Count => items.Count; + + public IEnumerator GetEnumerator() => items.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/tests/Light.GuardClauses.Tests/FrameworkExtensions/MultiplyAddHashTests.cs b/tests/Light.GuardClauses.Tests/FrameworkExtensions/MultiplyAddHashTests.cs index 8b735e66..ce85eb1d 100644 --- a/tests/Light.GuardClauses.Tests/FrameworkExtensions/MultiplyAddHashTests.cs +++ b/tests/Light.GuardClauses.Tests/FrameworkExtensions/MultiplyAddHashTests.cs @@ -40,7 +40,7 @@ public static void ThreeParameters(string value1, int value2, char value3) [Theory] [InlineData("Foo", 42, 'a', 87.73665)] [InlineData("Bar", -177422, 'Y', -15.25)] - [InlineData(null, 0, default(char), 0.0)] + [InlineData(null, 0, '\0', 0.0)] public static void FourParameters(string value1, int value2, char value3, double value4) { var hashCode1 = MultiplyAddHash.CreateHashCode(value1, value2, value3, value4); @@ -53,4 +53,164 @@ public static void FourParameters(string value1, int value2, char value3, double hashCode1.Should().Be(hashCode2); } -} \ No newline at end of file + + [Fact] + public static void EverySupportedArityMatchesTheBuilder() + { + var values = new object[] + { 1, "two", 3m, '4', 5L, 6f, 7d, (short) 8, (byte) 9, 10u, 11ul, null, 13, 14, 15, 16 }; + + MultiplyAddHash.CreateHashCode(values[0], values[1], values[2], values[3], values[4]) + .Should().Be(CreateExpectedHash(values, 5)); + MultiplyAddHash.CreateHashCode(values[0], values[1], values[2], values[3], values[4], values[5]) + .Should().Be(CreateExpectedHash(values, 6)); + MultiplyAddHash.CreateHashCode(values[0], values[1], values[2], values[3], values[4], values[5], values[6]) + .Should().Be(CreateExpectedHash(values, 7)); + MultiplyAddHash.CreateHashCode( + values[0], + values[1], + values[2], + values[3], + values[4], + values[5], + values[6], + values[7] + ) + .Should().Be(CreateExpectedHash(values, 8)); + MultiplyAddHash.CreateHashCode( + values[0], + values[1], + values[2], + values[3], + values[4], + values[5], + values[6], + values[7], + values[8] + ) + .Should().Be(CreateExpectedHash(values, 9)); + MultiplyAddHash.CreateHashCode( + values[0], + values[1], + values[2], + values[3], + values[4], + values[5], + values[6], + values[7], + values[8], + values[9] + ) + .Should().Be(CreateExpectedHash(values, 10)); + MultiplyAddHash.CreateHashCode( + values[0], + values[1], + values[2], + values[3], + values[4], + values[5], + values[6], + values[7], + values[8], + values[9], + values[10] + ) + .Should().Be(CreateExpectedHash(values, 11)); + MultiplyAddHash.CreateHashCode( + values[0], + values[1], + values[2], + values[3], + values[4], + values[5], + values[6], + values[7], + values[8], + values[9], + values[10], + values[11] + ) + .Should().Be(CreateExpectedHash(values, 12)); + MultiplyAddHash.CreateHashCode( + values[0], + values[1], + values[2], + values[3], + values[4], + values[5], + values[6], + values[7], + values[8], + values[9], + values[10], + values[11], + values[12] + ) + .Should().Be(CreateExpectedHash(values, 13)); + MultiplyAddHash.CreateHashCode( + values[0], + values[1], + values[2], + values[3], + values[4], + values[5], + values[6], + values[7], + values[8], + values[9], + values[10], + values[11], + values[12], + values[13] + ) + .Should().Be(CreateExpectedHash(values, 14)); + MultiplyAddHash.CreateHashCode( + values[0], + values[1], + values[2], + values[3], + values[4], + values[5], + values[6], + values[7], + values[8], + values[9], + values[10], + values[11], + values[12], + values[13], + values[14] + ) + .Should().Be(CreateExpectedHash(values, 15)); + MultiplyAddHash.CreateHashCode( + values[0], + values[1], + values[2], + values[3], + values[4], + values[5], + values[6], + values[7], + values[8], + values[9], + values[10], + values[11], + values[12], + values[13], + values[14], + values[15] + ) + .Should().Be(CreateExpectedHash(values, 16)); + } + + private static int CreateExpectedHash(object[] values, int count) + { + var builder = MultiplyAddHashBuilder.Create(); + for (var i = 0; i < count; i++) + { + builder = builder.CombineIntoHash(values[i]); + } + + return builder.BuildHash(); + } +} diff --git a/tests/Light.GuardClauses.Tests/FrameworkExtensions/TextExtensionsTests.cs b/tests/Light.GuardClauses.Tests/FrameworkExtensions/TextExtensionsTests.cs new file mode 100644 index 00000000..6f57e1a0 --- /dev/null +++ b/tests/Light.GuardClauses.Tests/FrameworkExtensions/TextExtensionsTests.cs @@ -0,0 +1,25 @@ +using System; +using System.Text; +using FluentAssertions; +using Light.GuardClauses.FrameworkExtensions; +using Xunit; + +namespace Light.GuardClauses.Tests.FrameworkExtensions; + +public static class TextExtensionsTests +{ + [Fact] + public static void NestedExceptionMessagesAreAppendedInOrder() + { + var exception = new InvalidOperationException("outer", new ArgumentException("inner")); + + var result = new StringBuilder().AppendExceptionMessages(exception); + + result.ToString().Should().Be($"outer{Environment.NewLine}{Environment.NewLine}inner{Environment.NewLine}"); + exception.GetAllExceptionMessages().Should().Be(result.ToString()); + } + + [Fact] + public static void EmptyTextDoesNotEqualNonEmptyTextWhenIgnoringWhiteSpace() => + string.Empty.EqualsOrdinalIgnoreWhiteSpace("content").Should().BeFalse(); +} diff --git a/tests/Light.GuardClauses.Tests/FrameworkExtensions/ToStringRepresentationTests.cs b/tests/Light.GuardClauses.Tests/FrameworkExtensions/ToStringRepresentationTests.cs index 637402a5..121be2db 100644 --- a/tests/Light.GuardClauses.Tests/FrameworkExtensions/ToStringRepresentationTests.cs +++ b/tests/Light.GuardClauses.Tests/FrameworkExtensions/ToStringRepresentationTests.cs @@ -1,4 +1,5 @@ -using FluentAssertions; +using System; +using FluentAssertions; using Light.GuardClauses.FrameworkExtensions; using Xunit; @@ -15,4 +16,44 @@ public static class ToStringRepresentationTests [Theory] [DefaultVariablesData] public static void QuotedValues(string value) => value.ToStringRepresentation().Should().Be($"\"{value}\""); -} \ No newline at end of file + + [Fact] + public static void EveryPrimitiveTypeConfiguredAsUnquotedIsNotQuoted() + { + 1L.ToStringRepresentation().Should().Be("1"); + ((short) 2).ToStringRepresentation().Should().Be("2"); + ((sbyte) 3).ToStringRepresentation().Should().Be("3"); + 4u.ToStringRepresentation().Should().Be("4"); + 5ul.ToStringRepresentation().Should().Be("5"); + ((ushort) 6).ToStringRepresentation().Should().Be("6"); + ((byte) 7).ToStringRepresentation().Should().Be("7"); + true.ToStringRepresentation().Should().Be(bool.TrueString); + 8d.ToStringRepresentation().Should().Be("8"); + 9m.ToStringRepresentation().Should().Be("9"); + 10f.ToStringRepresentation().Should().Be("10"); + } + + [Fact] + public static void EmptyAndLongRepresentationsAreHandled() + { + new EmptyRepresentation().ToStringRepresentation().Should().BeEmpty(); + + var longText = new string('a', 127); + longText.ToStringRepresentation().Should().Be($"\"{longText}\""); + } + + [Fact] + public static void NullValuesAreHandled() + { + string value = null; + + value.ToStringOrNull("missing").Should().Be("missing"); + ((Action) (() => value.ToStringRepresentation())).Should().Throw() + .WithParameterName(nameof(value)); + } + + private sealed class EmptyRepresentation + { + public override string ToString() => string.Empty; + } +} diff --git a/tests/Light.GuardClauses.Tests/StringAssertions/AsciiTests.cs b/tests/Light.GuardClauses.Tests/StringAssertions/AsciiTests.cs index 6182c8ea..16dc666c 100644 --- a/tests/Light.GuardClauses.Tests/StringAssertions/AsciiTests.cs +++ b/tests/Light.GuardClauses.Tests/StringAssertions/AsciiTests.cs @@ -80,14 +80,14 @@ public static void ScalarAndStringFailuresSupportDefaultMessagesFactoriesAndNull string nullText = null; ((Action) (() => invalidCharacter.MustBeAscii())).Should().ThrowExactly() - .WithParameterName(nameof(invalidCharacter)); + .WithParameterName(nameof(invalidCharacter)); ((Action) (() => invalidByte.MustBeAscii(message: "custom"))).Should().ThrowExactly() - .WithParameterName(nameof(invalidByte)) - .WithMessage("*custom*"); + .WithParameterName(nameof(invalidByte)) + .WithMessage("*custom*"); ((Action) (() => invalidText.MustBeAscii())).Should().ThrowExactly() - .WithParameterName(nameof(invalidText)); + .WithParameterName(nameof(invalidText)); ((Action) (() => nullText.MustBeAscii())).Should().Throw() - .WithParameterName(nameof(nullText)); + .WithParameterName(nameof(nullText)); Test.CustomException(invalidCharacter, (value, factory) => value.MustBeAscii(factory)); Test.CustomException(invalidByte, (value, factory) => value.MustBeAscii(factory)); Test.CustomException(nullText, (value, factory) => value.MustBeAscii(factory)); @@ -127,7 +127,7 @@ public static void BufferDefaultFailuresCaptureExpressionsAndMessages() }; var readOnlySpanAct = () => { - ReadOnlySpan invalidByteSpan = new byte[] { 128 }; + ReadOnlySpan invalidByteSpan = [128]; invalidByteSpan.MustBeAscii(message: "custom"); }; var readOnlyCharacterSpanAct = () => @@ -142,30 +142,70 @@ public static void BufferDefaultFailuresCaptureExpressionsAndMessages() spanAct.Should().ThrowExactly().WithParameterName("invalidCharacterSpan"); readOnlyCharacterSpanAct.Should().ThrowExactly() .WithParameterName("invalidCharacterSpan"); - readOnlySpanAct.Should().ThrowExactly().WithParameterName("invalidByteSpan").WithMessage("*custom*"); + readOnlySpanAct.Should().ThrowExactly().WithParameterName("invalidByteSpan") + .WithMessage("*custom*"); ((Action) (() => invalidCharacterMemory.MustBeAscii())).Should().ThrowExactly() - .WithParameterName(nameof(invalidCharacterMemory)); + .WithParameterName(nameof(invalidCharacterMemory)); ((Action) (() => invalidReadOnlyCharacterMemory.MustBeAscii())).Should().ThrowExactly() - .WithParameterName(nameof(invalidReadOnlyCharacterMemory)); + .WithParameterName( + nameof(invalidReadOnlyCharacterMemory) + ); ((Action) (() => invalidByteMemory.MustBeAscii())).Should().ThrowExactly() - .WithParameterName(nameof(invalidByteMemory)); + .WithParameterName(nameof(invalidByteMemory)); } [Fact] public static void BufferFactoriesReceiveEveryShape() { Test.CustomSpanException("é".ToCharArray().AsSpan(), (value, factory) => value.MustBeAscii(factory)); - Test.CustomSpanException((ReadOnlySpan) "é".ToCharArray(), - (value, factory) => value.MustBeAscii(factory)); + Test.CustomSpanException( + (ReadOnlySpan) "é".ToCharArray(), + (value, factory) => value.MustBeAscii(factory) + ); Test.CustomMemoryException("é".ToCharArray().AsMemory(), (value, factory) => value.MustBeAscii(factory)); - Test.CustomMemoryException((ReadOnlyMemory) "é".ToCharArray(), - (value, factory) => value.MustBeAscii(factory)); + Test.CustomMemoryException( + (ReadOnlyMemory) "é".ToCharArray(), + (value, factory) => value.MustBeAscii(factory) + ); Test.CustomSpanException(new byte[] { 128 }.AsSpan(), (value, factory) => value.MustBeAscii(factory)); - Test.CustomSpanException((ReadOnlySpan) new byte[] { 128 }, - (value, factory) => value.MustBeAscii(factory)); + Test.CustomSpanException( + (ReadOnlySpan) new byte[] { 128 }, + (value, factory) => value.MustBeAscii(factory) + ); Test.CustomMemoryException(new byte[] { 128 }.AsMemory(), (value, factory) => value.MustBeAscii(factory)); - Test.CustomMemoryException((ReadOnlyMemory) new byte[] { 128 }, - (value, factory) => value.MustBeAscii(factory)); + Test.CustomMemoryException( + (ReadOnlyMemory) new byte[] { 128 }, + (value, factory) => value.MustBeAscii(factory) + ); + } + + [Fact] + public static void CustomFactoriesAreNotInvokedForAsciiValues() + { + var characters = "ASCII".ToCharArray(); + var bytes = "ASCII"u8.ToArray(); + var characterSpan = characters.AsSpan(); + ReadOnlySpan readOnlyCharacterSpan = characters; + var byteSpan = bytes.AsSpan(); + ReadOnlySpan readOnlyByteSpan = bytes; + + 'A'.MustBeAscii(_ => throw new InvalidOperationException()).Should().Be('A'); + ((byte) 127).MustBeAscii(_ => throw new InvalidOperationException()).Should().Be(127); + "ASCII".MustBeAscii(_ => throw new InvalidOperationException()).Should().Be("ASCII"); + characterSpan.MustBeAscii(_ => throw new InvalidOperationException()).SequenceEqual(characterSpan).Should() + .BeTrue(); + readOnlyCharacterSpan.MustBeAscii(_ => throw new InvalidOperationException()) + .SequenceEqual(readOnlyCharacterSpan).Should().BeTrue(); + characters.AsMemory().MustBeAscii(_ => throw new InvalidOperationException()).Should() + .Be(characters.AsMemory()); + ((ReadOnlyMemory) characters).MustBeAscii(_ => throw new InvalidOperationException()) + .Should().Be((ReadOnlyMemory) characters); + byteSpan.MustBeAscii(_ => throw new InvalidOperationException()).SequenceEqual(byteSpan).Should().BeTrue(); + readOnlyByteSpan.MustBeAscii(_ => throw new InvalidOperationException()).SequenceEqual(readOnlyByteSpan) + .Should().BeTrue(); + bytes.AsMemory().MustBeAscii(_ => throw new InvalidOperationException()).Should().Be(bytes.AsMemory()); + ((ReadOnlyMemory) bytes).MustBeAscii(_ => throw new InvalidOperationException()) + .Should().Be((ReadOnlyMemory) bytes); } } diff --git a/tests/Light.GuardClauses.Tests/StringAssertions/MustBeEmailAddressTests.cs b/tests/Light.GuardClauses.Tests/StringAssertions/MustBeEmailAddressTests.cs index 81c4c617..fd768856 100644 --- a/tests/Light.GuardClauses.Tests/StringAssertions/MustBeEmailAddressTests.cs +++ b/tests/Light.GuardClauses.Tests/StringAssertions/MustBeEmailAddressTests.cs @@ -29,7 +29,7 @@ public static void InvalidEmailAddress(string emailAddress) [ClassData(typeof(InvalidEmailAddresses))] public static void InvalidEmailAddressArgumentName(string emailAddress) { - Action act = () => emailAddress.MustBeEmailAddress(nameof(emailAddress)); + Action act = () => emailAddress.MustBeEmailAddress(); act.Should().Throw() .And.Message.Should().Contain( @@ -43,7 +43,7 @@ public static void InvalidEmailAddressCustomMessage(string emailAddress) { const string customMessage = "This email address is not valid"; - Action act = () => emailAddress.MustBeEmailAddress(nameof(emailAddress), customMessage); + Action act = () => emailAddress.MustBeEmailAddress(customMessage); act.Should().Throw() .And.Message.Should().Contain(customMessage); @@ -232,7 +232,7 @@ public static void InvalidEmailAddressArgumentName_Span(string email) var act = () => { var span = new Span(emailChars); - span.MustBeEmailAddress(nameof(span)); + span.MustBeEmailAddress(); }; act.Should().Throw() .And.Message.Should().Contain("span"); @@ -247,7 +247,7 @@ public static void InvalidEmailAddressCustomMessage_Span(string email) var act = () => { var span = new Span(emailChars); - span.MustBeEmailAddress(nameof(span), customMessage); + span.MustBeEmailAddress(customMessage); }; act.Should().Throw() .And.Message.Should().Contain(customMessage); @@ -324,7 +324,7 @@ public static void InvalidEmailAddress_Memory(string email) public static void InvalidEmailAddressArgumentName_Memory(string email) { var memory = email.ToCharArray().AsMemory(); - Action act = () => memory.MustBeEmailAddress(nameof(memory)); + Action act = () => memory.MustBeEmailAddress(); act.Should().Throw() .And.Message.Should().Contain(nameof(memory)); } @@ -335,7 +335,7 @@ public static void InvalidEmailAddressCustomMessage_Memory(string email) { const string customMessage = "This email address is not valid"; var memory = email.ToCharArray().AsMemory(); - Action act = () => memory.MustBeEmailAddress(nameof(memory), customMessage); + Action act = () => memory.MustBeEmailAddress(customMessage); act.Should().Throw() .And.Message.Should().Contain(customMessage); } @@ -407,7 +407,7 @@ public static void InvalidEmailAddress_ReadOnlyMemory(string email) public static void InvalidEmailAddressArgumentName_ReadOnlyMemory(string email) { var readOnlyMemory = email.AsMemory(); - Action act = () => readOnlyMemory.MustBeEmailAddress(nameof(readOnlyMemory)); + Action act = () => readOnlyMemory.MustBeEmailAddress(); act.Should().Throw() .And.Message.Should().Contain(nameof(readOnlyMemory)); } @@ -418,7 +418,7 @@ public static void InvalidEmailAddressCustomMessage_ReadOnlyMemory(string email) { const string customMessage = "This email address is not valid"; var readOnlyMemory = email.AsMemory(); - Action act = () => readOnlyMemory.MustBeEmailAddress(nameof(readOnlyMemory), customMessage); + Action act = () => readOnlyMemory.MustBeEmailAddress(customMessage); act.Should().Throw() .And.Message.Should().Contain(customMessage); } @@ -464,5 +464,35 @@ public static void CustomMessageCustomRegex_ReadOnlyMemory() message => readOnlyMemory.MustBeEmailAddress(CustomRegex, message: message) ); } + + [Fact] + public static void EveryBufferOverloadReturnsValidEmailAddressesWithoutInvokingFactories() + { + const string email = "user@example.com"; + var characters = email.ToCharArray(); + var span = characters.AsSpan(); + ReadOnlySpan readOnlySpan = characters; + var memory = characters.AsMemory(); + ReadOnlyMemory readOnlyMemory = characters; + + readOnlySpan.MustBeEmailAddress(_ => throw new InvalidOperationException()).ToString().Should().Be(email); + readOnlySpan.MustBeEmailAddress(CustomRegex).ToString().Should().Be(email); + readOnlySpan.MustBeEmailAddress(CustomRegex, (_, _) => throw new InvalidOperationException()) + .ToString().Should().Be(email); + + span.MustBeEmailAddress(_ => throw new InvalidOperationException()).ToString().Should().Be(email); + span.MustBeEmailAddress(CustomRegex).ToString().Should().Be(email); + span.MustBeEmailAddress(CustomRegex, (_, _) => throw new InvalidOperationException()).ToString().Should() + .Be(email); + + memory.MustBeEmailAddress(_ => throw new InvalidOperationException()).Should().Be(memory); + memory.MustBeEmailAddress(CustomRegex).Should().Be(memory); + memory.MustBeEmailAddress(CustomRegex, (_, _) => throw new InvalidOperationException()).Should().Be(memory); + + readOnlyMemory.MustBeEmailAddress(_ => throw new InvalidOperationException()).Should().Be(readOnlyMemory); + readOnlyMemory.MustBeEmailAddress(CustomRegex).Should().Be(readOnlyMemory); + readOnlyMemory.MustBeEmailAddress(CustomRegex, (_, _) => throw new InvalidOperationException()) + .Should().Be(readOnlyMemory); + } #endif } diff --git a/tests/Light.GuardClauses.Tests/StringAssertions/MustNotStartWithTests.cs b/tests/Light.GuardClauses.Tests/StringAssertions/MustNotStartWithTests.cs index 5ad5160c..9d74d758 100644 --- a/tests/Light.GuardClauses.Tests/StringAssertions/MustNotStartWithTests.cs +++ b/tests/Light.GuardClauses.Tests/StringAssertions/MustNotStartWithTests.cs @@ -21,7 +21,9 @@ public static void StartsWith(string x, string y) var act = () => x.MustNotStartWith(y); var exception = act.Should().Throw().Which; - exception.Message.Should().StartWith($"{nameof(x)} must not start with \"{y}\" (CurrentCulture), but it actually is \"{x}\""); + exception.Message.Should().StartWith( + $"{nameof(x)} must not start with \"{y}\" (CurrentCulture), but it actually is \"{x}\"" + ); exception.ParamName.Should().BeSameAs(nameof(x)); } @@ -33,14 +35,16 @@ public static void StartsWithComparisonType(string x, string y, StringComparison var act = () => x.MustNotStartWith(y, comparisonType); var exception = act.Should().Throw().Which; - exception.Message.Should().StartWith($"{nameof(x)} must not start with \"{y}\" ({comparisonType}), but it actually is \"{x}\""); + exception.Message.Should().StartWith( + $"{nameof(x)} must not start with \"{y}\" ({comparisonType}), but it actually is \"{x}\"" + ); exception.ParamName.Should().BeSameAs(nameof(x)); } [Fact] public static void ParameterNull() { - var act = () => ((string)null).MustNotStartWith("Foo"); + var act = () => ((string) null).MustNotStartWith("Foo"); act.Should().Throw(); } @@ -59,15 +63,15 @@ public static void CustomMessage() => [Fact] public static void CustomMessageParameterNull() => - Test.CustomMessage(message => ((string)null).MustNotStartWith("Bar", message: message)); + Test.CustomMessage(message => ((string) null).MustNotStartWith("Bar", message: message)); [Fact] public static void NotStartsWithCustomException() => - "Bar".MustNotStartWith("Foo", (_, _) => new Exception()).Should().Be("Bar"); + "Bar".MustNotStartWith("Foo", (_, _) => new ()).Should().Be("Bar"); [Fact] public static void NotStartsWithCustomExceptionAndComparisonType() => - "Bar".MustNotStartWith("foo", StringComparison.OrdinalIgnoreCase, (_, _, _) => new Exception()).Should().Be("Bar"); + "Bar".MustNotStartWith("foo", StringComparison.OrdinalIgnoreCase, (_, _, _) => new ()).Should().Be("Bar"); [Theory] [InlineData("Foo", "Foo")] @@ -81,5 +85,47 @@ public static void CustomException(string first, string second) => [InlineData(null, "Bar", StringComparison.Ordinal)] [InlineData("Baz", null, StringComparison.Ordinal)] public static void CustomExceptionWithComparisonType(string a, string b, StringComparison comparisonType) => - Test.CustomException(a, b, comparisonType, (s1, s2, ct, exceptionFactory) => s1.MustNotStartWith(s2, ct, exceptionFactory)); + Test.CustomException( + a, + b, + comparisonType, + (s1, s2, ct, exceptionFactory) => s1.MustNotStartWith(s2, ct, exceptionFactory) + ); + + [Fact] + public static void SpanOverloadsSupportSuccessAndFailurePaths() + { + ReadOnlySpan parameter = "value"; + ReadOnlySpan other = "prefix"; + + parameter.MustNotStartWith(other, StringComparison.Ordinal).ToString().Should().Be("value"); + parameter.MustNotStartWith(other, (_, _) => throw new InvalidOperationException()).ToString().Should() + .Be("value"); + parameter.MustNotStartWith(other, StringComparison.Ordinal, (_, _, _) => throw new InvalidOperationException()) + .ToString().Should().Be("value"); + + var defaultFailure = () => + { + ReadOnlySpan invalid = "prefix-value"; + invalid.MustNotStartWith("prefix", StringComparison.Ordinal); + }; + var factoryFailure = () => + { + ReadOnlySpan invalid = "prefix-value"; + invalid.MustNotStartWith("prefix", (_, _) => new InvalidOperationException()); + }; + var comparisonFactoryFailure = () => + { + ReadOnlySpan invalid = "PREFIX-value"; + invalid.MustNotStartWith( + "prefix", + StringComparison.OrdinalIgnoreCase, + (_, _, _) => new InvalidOperationException() + ); + }; + + defaultFailure.Should().Throw(); + factoryFailure.Should().Throw(); + comparisonFactoryFailure.Should().Throw(); + } } From f3eb70b32c8e31a5ef37bea013a800afabe43cbf Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Tue, 14 Jul 2026 21:35:31 +0200 Subject: [PATCH 3/4] test: fix custom message tests in MustBeEmailAddressTests Signed-off-by: Kenny Pflug --- .../StringAssertions/MustBeEmailAddressTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Light.GuardClauses.Tests/StringAssertions/MustBeEmailAddressTests.cs b/tests/Light.GuardClauses.Tests/StringAssertions/MustBeEmailAddressTests.cs index fd768856..6251290a 100644 --- a/tests/Light.GuardClauses.Tests/StringAssertions/MustBeEmailAddressTests.cs +++ b/tests/Light.GuardClauses.Tests/StringAssertions/MustBeEmailAddressTests.cs @@ -43,7 +43,7 @@ public static void InvalidEmailAddressCustomMessage(string emailAddress) { const string customMessage = "This email address is not valid"; - Action act = () => emailAddress.MustBeEmailAddress(customMessage); + Action act = () => emailAddress.MustBeEmailAddress(message: customMessage); act.Should().Throw() .And.Message.Should().Contain(customMessage); @@ -247,7 +247,7 @@ public static void InvalidEmailAddressCustomMessage_Span(string email) var act = () => { var span = new Span(emailChars); - span.MustBeEmailAddress(customMessage); + span.MustBeEmailAddress(message: customMessage); }; act.Should().Throw() .And.Message.Should().Contain(customMessage); @@ -335,7 +335,7 @@ public static void InvalidEmailAddressCustomMessage_Memory(string email) { const string customMessage = "This email address is not valid"; var memory = email.ToCharArray().AsMemory(); - Action act = () => memory.MustBeEmailAddress(customMessage); + Action act = () => memory.MustBeEmailAddress(message: customMessage); act.Should().Throw() .And.Message.Should().Contain(customMessage); } @@ -418,7 +418,7 @@ public static void InvalidEmailAddressCustomMessage_ReadOnlyMemory(string email) { const string customMessage = "This email address is not valid"; var readOnlyMemory = email.AsMemory(); - Action act = () => readOnlyMemory.MustBeEmailAddress(customMessage); + Action act = () => readOnlyMemory.MustBeEmailAddress(message: customMessage); act.Should().Throw() .And.Message.Should().Contain(customMessage); } From 5ca4d58d1f6272ad9e5437e3e3b3894d8a8d40e9 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Tue, 14 Jul 2026 21:36:27 +0200 Subject: [PATCH 4/4] chore: Light.GuardClauses.slnx now uses forward-slashes consistently Signed-off-by: Kenny Pflug --- Light.GuardClauses.slnx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Light.GuardClauses.slnx b/Light.GuardClauses.slnx index 411b4adf..8e7cab72 100644 --- a/Light.GuardClauses.slnx +++ b/Light.GuardClauses.slnx @@ -15,12 +15,12 @@ - + - - + +