Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion tests/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
19 changes: 19 additions & 0 deletions tests/Light.GuardClauses.Tests/CodeCoverage.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Code coverage settings for Microsoft.Testing.Extensions.CodeCoverage.
See tests/AGENTS.md for the command line that applies this file.

Excludes code that is not part of Light.GuardClauses itself:
- JetBrains.Annotations attribute classes that are compiled into the assembly
- the Regex source generator output for the email address pattern
-->
<Configuration>
<CodeCoverage>
<Functions>
<Exclude>
<Function>^JetBrains\.Annotations\..*</Function>
<Function>^System\.Text\.RegularExpressions\.Generated\..*</Function>
</Exclude>
</Functions>
</CodeCoverage>
</Configuration>
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using FluentAssertions;
using Light.GuardClauses.ExceptionFactory;
using Light.GuardClauses.Exceptions;
using Xunit;

Expand Down Expand Up @@ -41,9 +42,9 @@ public static void EmptyGuardsCaptureCallerExpressions()
spanAct.Should().Throw<EmptyCollectionException>().WithParameterName("emptySpan");
readOnlySpanAct.Should().Throw<EmptyCollectionException>().WithParameterName("emptyReadOnlySpan");
((Action) (() => emptyMemory.MustNotBeEmpty())).Should().Throw<EmptyCollectionException>()
.WithParameterName(nameof(emptyMemory));
.WithParameterName(nameof(emptyMemory));
((Action) (() => emptyReadOnlyMemory.MustNotBeEmpty())).Should().Throw<EmptyCollectionException>()
.WithParameterName(nameof(emptyReadOnlyMemory));
.WithParameterName(nameof(emptyReadOnlyMemory));
}

[Fact]
Expand All @@ -55,6 +56,22 @@ public static void EmptyGuardFactoriesReceiveEveryShape()
Test.CustomMemoryException(ReadOnlyMemory<int>.Empty, (value, factory) => value.MustNotBeEmpty(factory));
}

[Fact]
public static void NonEmptyFactoriesDoNotThrowForEveryValidShape()
{
var array = new[] { 1, 2 };
var span = array.AsSpan();
ReadOnlySpan<int> readOnlySpan = array;
var memory = array.AsMemory();
ReadOnlyMemory<int> readOnlyMemory = array;
ReadOnlySpanExceptionFactory<int> 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()
{
Expand All @@ -81,14 +98,44 @@ public static void RangedLengthGuardsPreserveBoundariesAndShapes()
public static void RangedLengthFactoriesReceiveEveryShape()
{
var invalidRange = Range.InclusiveBetween(2, 3);
Test.CustomSpanException(Span<int>.Empty, invalidRange,
(value, range, factory) => value.MustHaveLengthIn(range, factory));
Test.CustomSpanException(ReadOnlySpan<int>.Empty, invalidRange,
(value, range, factory) => value.MustHaveLengthIn(range, factory));
Test.CustomMemoryException(Memory<int>.Empty, invalidRange,
(value, range, factory) => value.MustHaveLengthIn(range, factory));
Test.CustomMemoryException(ReadOnlyMemory<int>.Empty, invalidRange,
(value, range, factory) => value.MustHaveLengthIn(range, factory));
Test.CustomSpanException(
Span<int>.Empty,
invalidRange,
(value, range, factory) => value.MustHaveLengthIn(range, factory)
);
Test.CustomSpanException(
ReadOnlySpan<int>.Empty,
invalidRange,
(value, range, factory) => value.MustHaveLengthIn(range, factory)
);
Test.CustomMemoryException(
Memory<int>.Empty,
invalidRange,
(value, range, factory) => value.MustHaveLengthIn(range, factory)
);
Test.CustomMemoryException(
ReadOnlyMemory<int>.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<int> readOnlySpan = array;
var memory = array.AsMemory();
ReadOnlyMemory<int> readOnlyMemory = array;
ReadOnlySpanExceptionFactory<int, Range<int>> 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]
Expand All @@ -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<int> readOnlyMemory = memory;
ReadOnlySpanExceptionFactory<int, int> factory = (_, _) => new ("The factory should not be invoked.");

memory.MustHaveLength(2, factory).Should().Be(memory);
readOnlyMemory.MustHaveLength(2, factory).Should().Be(readOnlyMemory);
}

[Fact]
Expand Down Expand Up @@ -152,16 +213,40 @@ public static void EmptyAndWhitespaceFailuresUseExistingStringExceptions()
.WithMessage("*custom*");
}

[Fact]
public static void WhitespaceFactoriesDoNotThrowForEveryValidShape()
{
var characters = " a".ToCharArray();
var span = characters.AsSpan();
ReadOnlySpan<char> readOnlySpan = characters;
var memory = characters.AsMemory();
ReadOnlyMemory<char> readOnlyMemory = characters;
ReadOnlySpanExceptionFactory<char> 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<char>) " ".ToCharArray(),
(value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory));
Test.CustomMemoryException(" ".ToCharArray().AsMemory(),
(value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory));
Test.CustomMemoryException((ReadOnlyMemory<char>) " ".ToCharArray(),
(value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory));
Test.CustomSpanException(
" ".ToCharArray().AsSpan(),
(value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory)
);
Test.CustomSpanException(
(ReadOnlySpan<char>) " ".ToCharArray(),
(value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory)
);
Test.CustomMemoryException(
" ".ToCharArray().AsMemory(),
(value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory)
);
Test.CustomMemoryException(
(ReadOnlyMemory<char>) " ".ToCharArray(),
(value, factory) => value.MustNotBeEmptyOrWhiteSpace(factory)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static void KeyNotPresent()
{
var dictionary = new Dictionary<string, int> { ["Foo"] = 1, ["Bar"] = 2 };

Action act = () => dictionary.MustContainKey("Baz", nameof(dictionary));
Action act = () => dictionary.MustContainKey("Baz");

var assertion = act.Should().Throw<MissingKeyException>().Which;
assertion.Message.Should()
Expand All @@ -41,7 +41,7 @@ public static void InterfaceKeyNotPresent()
{
IReadOnlyDictionary<string, object> dictionary = new Dictionary<string, object> { ["Foo"] = 1 };

Action act = () => dictionary.MustContainKey("Bar", nameof(dictionary));
Action act = () => dictionary.MustContainKey("Bar");

act.Should().Throw<MissingKeyException>()
.And.Message.Should()
Expand Down Expand Up @@ -127,7 +127,15 @@ public static void CustomExceptionNotThrown()
{
var dictionary = new Dictionary<string, int> { ["Foo"] = 1 };

dictionary.MustContainKey("Foo", (_, _) => new Exception()).Should().BeSameAs(dictionary);
dictionary.MustContainKey("Foo", (_, _) => new ()).Should().BeSameAs(dictionary);
}

[Fact]
public static void InterfaceCustomExceptionNotThrown()
{
IReadOnlyDictionary<string, int> dictionary = new Dictionary<string, int> { ["Foo"] = 1 };

dictionary.MustContainKey("Foo", (_, _) => new ()).Should().BeSameAs(dictionary);
}

[Fact]
Expand Down Expand Up @@ -158,7 +166,7 @@ public static void DictionaryShapeIsPreservedInFluentChains()
{
var map = new Dictionary<string, string> { ["endpoint"] = "https://example.com" };

Dictionary<string, string> result = map.MustNotBeNull().MustContainKey("endpoint");
var result = map.MustNotBeNull().MustContainKey("endpoint");

result.Should().BeSameAs(map);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<MissingItemException>().Which;
assertion.Message.Should().Contain($"{nameof(collection)} must contain {item}, but it actually does not.");
Expand All @@ -41,29 +41,60 @@ 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<int> { 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<int>([1, 2, 3], false);

enumerable.MustContain(2).Should().BeSameAs(enumerable);
}

[Fact]
public static void LazyEnumerableItemNotPartOf()
{
var enumerable = new TrackingEnumerable<int>([1, 2, 3], false);

var act = () => enumerable.MustContain(42);

act.Should().Throw<MissingItemException>()
.WithParameterName(nameof(enumerable));
}

[Fact]
public static void CustomExceptionNotThrownLazyEnumerable()
{
var enumerable = new TrackingEnumerable<int>([1, 2, 3], false);

enumerable.MustContain(3, (_, _) => new ()).Should().BeSameAs(enumerable);
}

[Fact]
public static void CustomMessage() =>
Test.CustomMessage<MissingItemException>(message => new List<string>().MustContain("Foo", message: message));

[Fact]
public static void CustomMessageCollectionNull() =>
Test.CustomMessage<ArgumentNullException>(message => ((ObservableCollection<string>) null).MustContain("Foo", message: message));
public static void CustomMessageCollectionNull() =>
Test.CustomMessage<ArgumentNullException>(
message => ((ObservableCollection<string>) 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");

Expand All @@ -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<MissingItemException>().Which;
assertion.Message.Should().Contain($"{nameof(immutableArray)} must contain {item}, but it actually does not.");
Expand Down Expand Up @@ -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]
Expand All @@ -138,4 +169,4 @@ public static void ImmutableArrayCallerArgumentExpression()
act.Should().Throw<MissingItemException>()
.WithParameterName(nameof(immutableArray));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public static void LengthInRange(ImmutableArray<int> array, Range<int> range) =>
[MemberData(nameof(LengthNotInRangeData))]
public static void LengthNotInRange(ImmutableArray<int> array, Range<int> range)
{
var act = () => array.MustHaveLengthIn(range, nameof(array));
var act = () => array.MustHaveLengthIn(range);

act.Should().Throw<ArgumentOutOfRangeException>()
.And.Message.Should().Contain($"must have its length in between {range.CreateRangeDescriptionText("and")}")
Expand All @@ -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<ArgumentOutOfRangeException>(
Expand Down Expand Up @@ -84,7 +94,7 @@ public static void DefaultImmutableArrayNotInRange()
{
var defaultArray = default(ImmutableArray<int>);

var act = () => defaultArray.MustHaveLengthIn(Range.FromInclusive(1).ToInclusive(5), nameof(defaultArray));
var act = () => defaultArray.MustHaveLengthIn(Range.FromInclusive(1).ToInclusive(5));

act.Should().Throw<ArgumentOutOfRangeException>()
.And.Message.Should().Contain("must have its length in between 1 (inclusive) and 5 (inclusive)")
Expand Down
Loading
Loading