From 96302af0d693f89efe2c44794ebc93d394ebbbfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Sun, 12 Jan 2025 23:46:35 +0100 Subject: [PATCH 1/5] Update to latest xunit version --- Directory.Packages.props | 8 ++-- .../AsyncSequence/CycleRangeTest.cs | 4 +- .../AsyncEnumerableExtensions/ShuffleTest.cs | 4 +- .../TestUtilities/AsyncAssert.cs | 30 +++--------- .../EnumerableExtensions/MaterializeTest.cs | 2 +- Funcky.Xunit/Funcky.Xunit.csproj | 4 +- Funcky.Xunit/FunctionalAssert/EqualOrThrow.cs | 22 +++++++++ Funcky.Xunit/FunctionalAssert/Error.cs | 11 +++-- Funcky.Xunit/FunctionalAssert/Left.cs | 33 ++++++++----- Funcky.Xunit/FunctionalAssert/None.cs | 13 +++-- Funcky.Xunit/FunctionalAssert/Ok.cs | 48 +++++++++++++------ Funcky.Xunit/FunctionalAssert/Right.cs | 33 ++++++++----- Funcky.Xunit/FunctionalAssert/Some.cs | 33 ++++++++----- Funcky.Xunit/FunctionalAssertException.cs | 23 +++++++++ 14 files changed, 168 insertions(+), 100 deletions(-) create mode 100644 Funcky.Xunit/FunctionalAssert/EqualOrThrow.cs create mode 100644 Funcky.Xunit/FunctionalAssertException.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index d25dc544..6e899d57 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,8 +7,8 @@ - - + + @@ -19,8 +19,8 @@ - - + + diff --git a/Funcky.Async.Test/AsyncSequence/CycleRangeTest.cs b/Funcky.Async.Test/AsyncSequence/CycleRangeTest.cs index 0ae05a43..7e71637d 100644 --- a/Funcky.Async.Test/AsyncSequence/CycleRangeTest.cs +++ b/Funcky.Async.Test/AsyncSequence/CycleRangeTest.cs @@ -16,8 +16,8 @@ public async Task CycleRangeIsEnumeratedLazilyAsync() } [Fact] - public void CyclingAnEmptySetThrowsAnArgumentException() - => Assert.ThrowsAsync(CycleEmptySequenceAsync); + public async Task CyclingAnEmptySetThrowsAnArgumentException() + => await Assert.ThrowsAsync(CycleEmptySequenceAsync); [Property] public Property CycleRangeCanProduceArbitraryManyItemsAsync(NonEmptySet sequence, PositiveInt arbitraryElements) diff --git a/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/ShuffleTest.cs b/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/ShuffleTest.cs index 1afdf5b2..1d2bf783 100644 --- a/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/ShuffleTest.cs +++ b/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/ShuffleTest.cs @@ -8,13 +8,13 @@ namespace Funcky.Async.Test.Extensions.AsyncEnumerableExtensions; public sealed class ShuffleTest { [Fact] - public void AShuffleIsEnumeratedLazilyAsync() + public async Task AShuffleIsEnumeratedLazilyAsync() { var doNotEnumerate = new FailOnEnumerateAsyncSequence(); var shuffled = doNotEnumerate.ShuffleAsync(); - Assert.ThrowsAsync(async () => await shuffled); + await Assert.ThrowsAsync(async () => await shuffled); } [Fact] diff --git a/Funcky.Async.Test/TestUtilities/AsyncAssert.cs b/Funcky.Async.Test/TestUtilities/AsyncAssert.cs index 29fbbc41..13563511 100644 --- a/Funcky.Async.Test/TestUtilities/AsyncAssert.cs +++ b/Funcky.Async.Test/TestUtilities/AsyncAssert.cs @@ -11,7 +11,8 @@ public static async Task Empty(IAsyncEnumerable asyncSequenc { if (await asyncEnumerator.MoveNextAsync()) { - throw new EmptyException(await asyncSequence.ToListAsync()); + var actual = await asyncSequence.ToListAsync(); + throw EmptyException.ForNonEmptyCollection(collection: "TODO"); } } finally @@ -27,7 +28,7 @@ public static async Task NotEmpty(IAsyncEnumerable asyncSequ { if (!await asyncEnumerator.MoveNextAsync()) { - throw new NotEmptyException(); + throw NotEmptyException.ForNonEmptyCollection(); } } finally @@ -39,25 +40,7 @@ public static async Task NotEmpty(IAsyncEnumerable asyncSequ public static async Task Collection(IAsyncEnumerable asyncSequence, params Action[] elementInspectors) { var elements = await asyncSequence.ToListAsync(); - var elementInspectorsLength = elementInspectors.Length; - var elementsLength = elements.Count; - - if (elementInspectorsLength != elementsLength) - { - throw new CollectionException(asyncSequence.ToListAsync(), elementInspectorsLength, elementsLength); - } - - foreach (var ((elementInspector, element), indexFailurePoint) in elementInspectors.Zip(elements).WithIndex()) - { - try - { - elementInspector(element); - } - catch (Exception ex) - { - throw new CollectionException(asyncSequence.ToListAsync(), elementInspectorsLength, elementsLength, indexFailurePoint, ex); - } - } + Assert.Collection(elements, elementInspectors); } public static async Task Single(IAsyncEnumerable asyncSequence) @@ -66,14 +49,15 @@ public static async Task Single(IAsyncEnumerable asyncSequence) if (await asyncEnumerator.MoveNextAsync() is false) { - SingleException.Empty(null); + throw SingleException.Empty(expected: null, collection: "TODO"); } var result = asyncEnumerator.Current; if (await asyncEnumerator.MoveNextAsync()) { - SingleException.MoreThanOne(await asyncSequence.CountAsync(), null); + var actual = await asyncSequence.ToListAsync(); + throw SingleException.MoreThanOne(expected: null, collection: "TODO", count: actual.Count, matchIndices: Array.Empty()); } return result; diff --git a/Funcky.Test/Extensions/EnumerableExtensions/MaterializeTest.cs b/Funcky.Test/Extensions/EnumerableExtensions/MaterializeTest.cs index 1135d1bc..8976ed8c 100644 --- a/Funcky.Test/Extensions/EnumerableExtensions/MaterializeTest.cs +++ b/Funcky.Test/Extensions/EnumerableExtensions/MaterializeTest.cs @@ -45,7 +45,7 @@ public void MaterializeWithMaterializationReturnsCorrectCollectionWhenEnumerate( public void MaterializeDoesNotEnumerableEnumerableReturnedByRepeat() { var sequence = Enumerable.Repeat("Hello world!", 3); - var materialized = sequence.Materialize>(_ => throw new FailException("Materialization should never be called")); + var materialized = sequence.Materialize>(_ => throw FailException.ForFailure("Materialization should never be called")); Assert.Same(sequence, materialized); } #endif diff --git a/Funcky.Xunit/Funcky.Xunit.csproj b/Funcky.Xunit/Funcky.Xunit.csproj index a03975a0..5824dfbe 100644 --- a/Funcky.Xunit/Funcky.Xunit.csproj +++ b/Funcky.Xunit/Funcky.Xunit.csproj @@ -18,9 +18,7 @@ $(DefineConstants);STACK_TRACE_HIDDEN_SUPPORTED - - - + diff --git a/Funcky.Xunit/FunctionalAssert/EqualOrThrow.cs b/Funcky.Xunit/FunctionalAssert/EqualOrThrow.cs new file mode 100644 index 00000000..2cb79499 --- /dev/null +++ b/Funcky.Xunit/FunctionalAssert/EqualOrThrow.cs @@ -0,0 +1,22 @@ +using Xunit; +using Xunit.Sdk; + +namespace Funcky; + +public static partial class FunctionalAssert +{ + private static void EqualOrThrow( + T expected, + T actual, + Action @throw) + { + try + { + Assert.Equal(expected, actual); + } + catch (XunitException) + { + @throw(); + } + } +} diff --git a/Funcky.Xunit/FunctionalAssert/Error.cs b/Funcky.Xunit/FunctionalAssert/Error.cs index fb80d1d1..a31f6c62 100644 --- a/Funcky.Xunit/FunctionalAssert/Error.cs +++ b/Funcky.Xunit/FunctionalAssert/Error.cs @@ -1,18 +1,20 @@ using System.Diagnostics.CodeAnalysis; using Xunit.Sdk; +using static Xunit.Sdk.ArgumentFormatter; namespace Funcky; public static partial class FunctionalAssert { /// Asserts that the given is Error. - /// Thrown when is Ok. + /// Thrown when is Ok. #if STACK_TRACE_HIDDEN_SUPPORTED [System.Diagnostics.StackTraceHidden] #else [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] #endif [SuppressMessage("Microsoft.Usage", "CA2200", Justification = "Stack trace erasure intentional.")] + [SuppressMessage("ReSharper", "PossibleIntendedRethrow", Justification = "Stack trace erasure intentional.")] public static Exception Error(Result result) where TValidResult : notnull { @@ -20,12 +22,11 @@ public static Exception Error(Result result) { return result.Match( error: Identity, - ok: static value => throw new AssertActualExpectedException( + ok: static value => throw FunctionalAssertException.ForMismatchedValues( expected: "Error(...)", - actual: $"Ok({value})", - userMessage: $"{nameof(FunctionalAssert)}.{nameof(Error)}() Failure")); + actual: $"Ok({Format(value)})")); } - catch (AssertActualExpectedException exception) + catch (XunitException exception) { throw exception; } diff --git a/Funcky.Xunit/FunctionalAssert/Left.cs b/Funcky.Xunit/FunctionalAssert/Left.cs index 4d4c92f9..561a3477 100644 --- a/Funcky.Xunit/FunctionalAssert/Left.cs +++ b/Funcky.Xunit/FunctionalAssert/Left.cs @@ -1,37 +1,45 @@ using System.Diagnostics.CodeAnalysis; -using Xunit; using Xunit.Sdk; +using static Xunit.Sdk.ArgumentFormatter; namespace Funcky; public static partial class FunctionalAssert { /// Asserts that the given is Left and contains the given . - /// Thrown when is Right. + /// Thrown when is Right. #if STACK_TRACE_HIDDEN_SUPPORTED [System.Diagnostics.StackTraceHidden] #else [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] #endif + [SuppressMessage("Microsoft.Usage", "CA2200", Justification = "Stack trace erasure intentional.")] + [SuppressMessage("ReSharper", "PossibleIntendedRethrow", Justification = "Stack trace erasure intentional.")] public static void Left(TLeft expectedLeft, Either either) where TLeft : notnull where TRight : notnull { try { - Assert.Equal(Either.Left(expectedLeft), either); + either.Switch( + right: right => throw FunctionalAssertException.ForMismatchedValues( + expected: $"Left({Format(expectedLeft)})", + actual: $"Right({Format(right)})"), + left: left => EqualOrThrow( + expected: expectedLeft, + actual: left, + () => throw FunctionalAssertException.ForMismatchedValues( + expected: $"Left({Format(expectedLeft)})", + actual: $"Left({Format(left)})"))); } - catch (EqualException exception) + catch (XunitException exception) { - throw new AssertActualExpectedException( - expected: exception.Expected, - actual: exception.Actual, - userMessage: $"{nameof(FunctionalAssert)}.{nameof(Left)}() Failure"); + throw exception; } } /// Asserts that the given is Left. - /// Thrown when is Right. + /// Thrown when is Right. /// Returns the value in if it was Left. #if STACK_TRACE_HIDDEN_SUPPORTED [System.Diagnostics.StackTraceHidden] @@ -48,12 +56,11 @@ public static TLeft Left(Either either) { return either.Match( left: Identity, - right: static right => throw new AssertActualExpectedException( + right: static right => throw FunctionalAssertException.ForMismatchedValues( expected: "Left(...)", - actual: $"Right({right})", - userMessage: $"{nameof(FunctionalAssert)}.{nameof(Left)}() Failure")); + actual: $"Right({Format(right)})")); } - catch (AssertActualExpectedException exception) + catch (XunitException exception) { throw exception; } diff --git a/Funcky.Xunit/FunctionalAssert/None.cs b/Funcky.Xunit/FunctionalAssert/None.cs index fbac3cf6..f8851923 100644 --- a/Funcky.Xunit/FunctionalAssert/None.cs +++ b/Funcky.Xunit/FunctionalAssert/None.cs @@ -1,12 +1,13 @@ using System.Diagnostics.CodeAnalysis; using Xunit.Sdk; +using static Xunit.Sdk.ArgumentFormatter; namespace Funcky; public static partial class FunctionalAssert { /// Asserts that the given is None. - /// Thrown when is Some. + /// Thrown when is Some. #if STACK_TRACE_HIDDEN_SUPPORTED [System.Diagnostics.StackTraceHidden] #else @@ -19,14 +20,12 @@ public static void None(Option option) { try { - option.Switch( - none: NoOperation, - some: static value => throw new AssertActualExpectedException( + option.AndThen( + static value => throw FunctionalAssertException.ForMismatchedValues( expected: "None", - actual: $"Some({value})", - userMessage: $"{nameof(FunctionalAssert)}.{nameof(None)}() Failure")); + actual: $"Some({Format(value)})")); } - catch (AssertActualExpectedException exception) + catch (XunitException exception) { throw exception; } diff --git a/Funcky.Xunit/FunctionalAssert/Ok.cs b/Funcky.Xunit/FunctionalAssert/Ok.cs index 3b6efd19..5a040c64 100644 --- a/Funcky.Xunit/FunctionalAssert/Ok.cs +++ b/Funcky.Xunit/FunctionalAssert/Ok.cs @@ -1,36 +1,44 @@ using System.Diagnostics.CodeAnalysis; -using Xunit; using Xunit.Sdk; +using static Xunit.Sdk.ArgumentFormatter; namespace Funcky; public static partial class FunctionalAssert { /// Asserts that the given is Ok and contains the given . - /// Thrown when is Error. + /// Thrown when is Error. #if STACK_TRACE_HIDDEN_SUPPORTED [System.Diagnostics.StackTraceHidden] #else [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] #endif + [SuppressMessage("Microsoft.Usage", "CA2200", Justification = "Stack trace erasure intentional.")] + [SuppressMessage("ReSharper", "PossibleIntendedRethrow", Justification = "Stack trace erasure intentional.")] public static void Ok(TValidResult expectedResult, Result result) where TValidResult : notnull { try { - Assert.Equal(expectedResult, result); + result.Switch( + error: exception => throw FunctionalAssertException.ForMismatchedValues( + expected: $"Ok({Format(expectedResult)})", + actual: $"Error({FormatException(exception)})"), + ok: value => EqualOrThrow( + expected: expectedResult, + actual: value, + () => throw FunctionalAssertException.ForMismatchedValues( + expected: $"Ok({Format(expectedResult)})", + actual: $"Ok({Format(value)})"))); } - catch (EqualException exception) + catch (XunitException exception) { - throw new AssertActualExpectedException( - expected: exception.Expected, - actual: exception.Actual, - userMessage: $"{nameof(FunctionalAssert)}.{nameof(Ok)}() Failure"); + throw exception; } } /// Asserts that the given is Ok. - /// Thrown when is Error. + /// Thrown when is Error. /// Returns the value in if it was Ok. #if STACK_TRACE_HIDDEN_SUPPORTED [System.Diagnostics.StackTraceHidden] @@ -45,17 +53,29 @@ public static TValidResult Ok(Result result) try { return result.GetOrElse( - static exception => throw new AssertActualExpectedException( + static exception => throw FunctionalAssertException.ForMismatchedValues( expected: "Ok(...)", - actual: $"Error({FormatException(exception)})", - userMessage: $"{nameof(FunctionalAssert)}.{nameof(Ok)}() Failure")); + actual: $"Error({FormatException(exception)})")); } - catch (AssertActualExpectedException exception) + catch (XunitException exception) { throw exception; } } private static string FormatException(Exception exception) - => $"{exception.GetType().FullName}: {exception.Message}"; + => $"{FormatTypeName(exception.GetType())}: {Format(exception.Message)}"; + + private static string FormatTypeName(Type type) + { + // Xunit's `Format` takes care of niceties like C# type names, + // array types, nullable, etc. but adds typeof(...) around the name + // which is unsuitable for our case. + const string prefix = "typeof("; + const string suffix = ")"; + var formatted = Format(type); + return formatted.StartsWith(prefix) && formatted.EndsWith(suffix) + ? formatted[prefix.Length..^suffix.Length] + : formatted; + } } diff --git a/Funcky.Xunit/FunctionalAssert/Right.cs b/Funcky.Xunit/FunctionalAssert/Right.cs index 505b5d10..a0fc2871 100644 --- a/Funcky.Xunit/FunctionalAssert/Right.cs +++ b/Funcky.Xunit/FunctionalAssert/Right.cs @@ -1,37 +1,45 @@ using System.Diagnostics.CodeAnalysis; -using Xunit; using Xunit.Sdk; +using static Xunit.Sdk.ArgumentFormatter; namespace Funcky; public static partial class FunctionalAssert { /// Asserts that the given is Right and contains the given . - /// Thrown when is Left. + /// Thrown when is Left. #if STACK_TRACE_HIDDEN_SUPPORTED [System.Diagnostics.StackTraceHidden] #else [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] #endif + [SuppressMessage("Microsoft.Usage", "CA2200", Justification = "Stack trace erasure intentional.")] + [SuppressMessage("ReSharper", "PossibleIntendedRethrow", Justification = "Stack trace erasure intentional.")] public static void Right(TRight expectedRight, Either either) where TLeft : notnull where TRight : notnull { try { - Assert.Equal(Either.Right(expectedRight), either); + either.Switch( + left: left => throw FunctionalAssertException.ForMismatchedValues( + expected: $"Right({Format(expectedRight)})", + actual: $"Left({Format(left)})"), + right: right => EqualOrThrow( + expected: expectedRight, + actual: right, + () => throw FunctionalAssertException.ForMismatchedValues( + expected: $"Right({Format(expectedRight)})", + actual: $"Right({Format(right)})"))); } - catch (EqualException exception) + catch (XunitException exception) { - throw new AssertActualExpectedException( - expected: exception.Expected, - actual: exception.Actual, - userMessage: $"{nameof(FunctionalAssert)}.{nameof(Right)}() Failure"); + throw exception; } } /// Asserts that the given is Right. - /// Thrown when is Left. + /// Thrown when is Left. /// Returns the value in if it was Right. #if STACK_TRACE_HIDDEN_SUPPORTED [System.Diagnostics.StackTraceHidden] @@ -47,12 +55,11 @@ public static TRight Right(Either either) try { return either.GetOrElse( - static left => throw new AssertActualExpectedException( + static left => throw FunctionalAssertException.ForMismatchedValues( expected: "Right(...)", - actual: $"Left({left})", - userMessage: $"{nameof(FunctionalAssert)}.{nameof(Right)}() Failure")); + actual: $"Left({Format(left)})")); } - catch (AssertActualExpectedException exception) + catch (XunitException exception) { throw exception; } diff --git a/Funcky.Xunit/FunctionalAssert/Some.cs b/Funcky.Xunit/FunctionalAssert/Some.cs index bf17666b..c3533455 100644 --- a/Funcky.Xunit/FunctionalAssert/Some.cs +++ b/Funcky.Xunit/FunctionalAssert/Some.cs @@ -1,36 +1,44 @@ using System.Diagnostics.CodeAnalysis; -using Xunit; using Xunit.Sdk; +using static Xunit.Sdk.ArgumentFormatter; namespace Funcky; public static partial class FunctionalAssert { /// Asserts that the given is Some and contains the given . - /// Thrown when the option is None. + /// Thrown when the option is None. #if STACK_TRACE_HIDDEN_SUPPORTED [System.Diagnostics.StackTraceHidden] #else [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] #endif + [SuppressMessage("Microsoft.Usage", "CA2200", Justification = "Stack trace erasure intentional.")] + [SuppressMessage("ReSharper", "PossibleIntendedRethrow", Justification = "Stack trace erasure intentional.")] public static void Some(TItem expectedValue, Option option) where TItem : notnull { try { - Assert.Equal(Option.Some(expectedValue), option); + option.Switch( + none: () => throw FunctionalAssertException.ForMismatchedValues( + expected: $"Some({Format(expectedValue)})", + actual: "None"), + some: value => EqualOrThrow( + expected: expectedValue, + actual: value, + () => throw FunctionalAssertException.ForMismatchedValues( + expected: $"Some({Format(expectedValue)})", + actual: $"Some({Format(value)})"))); } - catch (EqualException exception) + catch (XunitException exception) { - throw new AssertActualExpectedException( - expected: exception.Expected, - actual: exception.Actual, - userMessage: $"{nameof(FunctionalAssert)}.{nameof(Some)}() Failure"); + throw exception; } } /// Asserts that the given is Some. - /// Thrown when is None. + /// Thrown when is None. /// Returns the value in if it was Some. [Pure] #if STACK_TRACE_HIDDEN_SUPPORTED @@ -45,12 +53,11 @@ public static TItem Some(Option option) { try { - return option.GetOrElse(static () => throw new AssertActualExpectedException( + return option.GetOrElse(static () => throw FunctionalAssertException.ForMismatchedValues( expected: "Some(...)", - actual: "None", - userMessage: $"{nameof(FunctionalAssert)}.{nameof(Some)}() Failure")); + actual: "None")); } - catch (AssertActualExpectedException exception) + catch (XunitException exception) { throw exception; } diff --git a/Funcky.Xunit/FunctionalAssertException.cs b/Funcky.Xunit/FunctionalAssertException.cs new file mode 100644 index 00000000..79afb1ab --- /dev/null +++ b/Funcky.Xunit/FunctionalAssertException.cs @@ -0,0 +1,23 @@ +using System.Runtime.CompilerServices; +using Xunit.Sdk; + +namespace Funcky; + +internal static class FunctionalAssertException +{ + private static readonly string NewLineAndIndent = Environment.NewLine + new string(' ', 10); // Length of "Expected: " and "Actual: " + + public static XunitException ForMismatchedValues( + string expected, + string actual, + [CallerMemberName] string? assertionName = null) + { + var assertionLabel = assertionName is not null + ? $"{nameof(FunctionalAssert)}.{assertionName}()" + : nameof(FunctionalAssert); + return new XunitException( + $"{assertionLabel} Failure: Values differ{Environment.NewLine}" + + $"Expected: {expected.Replace(Environment.NewLine, NewLineAndIndent)}{Environment.NewLine}" + + $"Actual: {actual.Replace(Environment.NewLine, NewLineAndIndent)}"); + } +} From 5dbc1c4b1490ad74b258283c11e7bb28776684ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Sun, 12 Jan 2025 23:47:08 +0100 Subject: [PATCH 2/5] Add unit tests for `FunctionalAssert` --- .../FunctionalAssertClass/ErrorTest.cs | 25 ++++++++ .../FunctionalAssertClass/LeftTest.cs | 57 +++++++++++++++++++ .../FunctionalAssertClass/NoneTest.cs | 25 ++++++++ .../FunctionalAssertClass/OkTest.cs | 57 +++++++++++++++++++ .../FunctionalAssertClass/RightTest.cs | 57 +++++++++++++++++++ .../FunctionalAssertClass/SomeTest.cs | 57 +++++++++++++++++++ 6 files changed, 278 insertions(+) create mode 100644 Funcky.Xunit.Test/FunctionalAssertClass/ErrorTest.cs create mode 100644 Funcky.Xunit.Test/FunctionalAssertClass/LeftTest.cs create mode 100644 Funcky.Xunit.Test/FunctionalAssertClass/NoneTest.cs create mode 100644 Funcky.Xunit.Test/FunctionalAssertClass/OkTest.cs create mode 100644 Funcky.Xunit.Test/FunctionalAssertClass/RightTest.cs create mode 100644 Funcky.Xunit.Test/FunctionalAssertClass/SomeTest.cs diff --git a/Funcky.Xunit.Test/FunctionalAssertClass/ErrorTest.cs b/Funcky.Xunit.Test/FunctionalAssertClass/ErrorTest.cs new file mode 100644 index 00000000..31da2db9 --- /dev/null +++ b/Funcky.Xunit.Test/FunctionalAssertClass/ErrorTest.cs @@ -0,0 +1,25 @@ +using Xunit.Sdk; + +namespace Funcky.Xunit.Test.FunctionalAssertClass; + +public sealed class ErrorTest +{ + [Fact] + public void DoesNotThrowForError() + { + FunctionalAssert.Error(Result.Error(new ArgumentException("foo"))); + } + + [Fact] + public void ThrowsForOk() + { + const string expectedMessage = + """ + FunctionalAssert.Error() Failure: Values differ + Expected: Error(...) + Actual: Ok(42) + """; + var exception = Assert.ThrowsAny(() => FunctionalAssert.Error(Result.Ok(42))); + Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); + } +} diff --git a/Funcky.Xunit.Test/FunctionalAssertClass/LeftTest.cs b/Funcky.Xunit.Test/FunctionalAssertClass/LeftTest.cs new file mode 100644 index 00000000..7ad8d2ba --- /dev/null +++ b/Funcky.Xunit.Test/FunctionalAssertClass/LeftTest.cs @@ -0,0 +1,57 @@ +using Xunit.Sdk; + +namespace Funcky.Xunit.Test.FunctionalAssertClass; + +public sealed class LeftTest +{ + [Fact] + public void DoesNotThrowForMatchingLeft() + { + FunctionalAssert.Left(10, Either.Left(10)); + } + + [Fact] + public void DoesNotThrowForLeft() + { + FunctionalAssert.Left(Either.Left(10)); + } + + [Fact] + public void ThrowsForMismatchingLeft() + { + const string expectedMessage = + """ + FunctionalAssert.Left() Failure: Values differ + Expected: Left(10) + Actual: Left(11) + """; + var exception = Assert.ThrowsAny(() => FunctionalAssert.Left(10, Either.Left(11))); + Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); + } + + [Fact] + public void ThrowsForRightWithExpectedValue() + { + const string expectedMessage = + """ + FunctionalAssert.Left() Failure: Values differ + Expected: Left(10) + Actual: Right("right") + """; + var exception = Assert.ThrowsAny(() => FunctionalAssert.Left(10, Either.Right("right"))); + Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); + } + + [Fact] + public void ThrowsForRight() + { + const string expectedMessage = + """ + FunctionalAssert.Left() Failure: Values differ + Expected: Left(...) + Actual: Right("right") + """; + var exception = Assert.ThrowsAny(() => FunctionalAssert.Left(Either.Right("right"))); + Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); + } +} diff --git a/Funcky.Xunit.Test/FunctionalAssertClass/NoneTest.cs b/Funcky.Xunit.Test/FunctionalAssertClass/NoneTest.cs new file mode 100644 index 00000000..400b3628 --- /dev/null +++ b/Funcky.Xunit.Test/FunctionalAssertClass/NoneTest.cs @@ -0,0 +1,25 @@ +using Xunit.Sdk; + +namespace Funcky.Xunit.Test.FunctionalAssertClass; + +public sealed class NoneTest +{ + [Fact] + public void DoesNotThrowForNone() + { + FunctionalAssert.None(Option.None); + } + + [Fact] + public void ThrowsForSome() + { + const string expectedMessage = + """ + FunctionalAssert.None() Failure: Values differ + Expected: None + Actual: Some("something") + """; + var exception = Assert.ThrowsAny(() => FunctionalAssert.None(Option.Some("something"))); + Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); + } +} diff --git a/Funcky.Xunit.Test/FunctionalAssertClass/OkTest.cs b/Funcky.Xunit.Test/FunctionalAssertClass/OkTest.cs new file mode 100644 index 00000000..25063dcf --- /dev/null +++ b/Funcky.Xunit.Test/FunctionalAssertClass/OkTest.cs @@ -0,0 +1,57 @@ +using Xunit.Sdk; + +namespace Funcky.Xunit.Test.FunctionalAssertClass; + +public sealed class OkTest +{ + [Fact] + public void DoesNotThrowForMatchingOk() + { + FunctionalAssert.Ok(10, Result.Ok(10)); + } + + [Fact] + public void DoesNotThrowForOk() + { + FunctionalAssert.Ok(Result.Ok(10)); + } + + [Fact] + public void ThrowsForMismatchingOk() + { + const string expectedMessage = + """ + FunctionalAssert.Ok() Failure: Values differ + Expected: Ok(10) + Actual: Ok(11) + """; + var exception = Assert.ThrowsAny(() => FunctionalAssert.Ok(10, Result.Ok(11))); + Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); + } + + [Fact] + public void ThrowsForErrorWithExpectedValue() + { + const string expectedMessage = + """ + FunctionalAssert.Ok() Failure: Values differ + Expected: Ok(10) + Actual: Error(System.ArgumentException: "message") + """; + var exception = Assert.ThrowsAny(() => FunctionalAssert.Ok(10, Result.Error(new ArgumentException("message")))); + Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); + } + + [Fact] + public void ThrowsForError() + { + const string expectedMessage = + """ + FunctionalAssert.Ok() Failure: Values differ + Expected: Ok(...) + Actual: Error(System.ArgumentException: "message") + """; + var exception = Assert.ThrowsAny(() => FunctionalAssert.Ok(Result.Error(new ArgumentException("message")))); + Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); + } +} diff --git a/Funcky.Xunit.Test/FunctionalAssertClass/RightTest.cs b/Funcky.Xunit.Test/FunctionalAssertClass/RightTest.cs new file mode 100644 index 00000000..79efbf33 --- /dev/null +++ b/Funcky.Xunit.Test/FunctionalAssertClass/RightTest.cs @@ -0,0 +1,57 @@ +using Xunit.Sdk; + +namespace Funcky.Xunit.Test.FunctionalAssertClass; + +public sealed class RightTest +{ + [Fact] + public void DoesNotThrowForMatchingRight() + { + FunctionalAssert.Right(10, Either.Right(10)); + } + + [Fact] + public void DoesNotThrowForRight() + { + FunctionalAssert.Right(Either.Right(10)); + } + + [Fact] + public void ThrowsForMismatchingRight() + { + const string expectedMessage = + """ + FunctionalAssert.Right() Failure: Values differ + Expected: Right(10) + Actual: Right(11) + """; + var exception = Assert.ThrowsAny(() => FunctionalAssert.Right(10, Either.Right(11))); + Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); + } + + [Fact] + public void ThrowsForLeftWithExpectedValue() + { + const string expectedMessage = + """ + FunctionalAssert.Right() Failure: Values differ + Expected: Right(10) + Actual: Left("left") + """; + var exception = Assert.ThrowsAny(() => FunctionalAssert.Right(10, Either.Left("left"))); + Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); + } + + [Fact] + public void ThrowsForLeft() + { + const string expectedMessage = + """ + FunctionalAssert.Right() Failure: Values differ + Expected: Right(...) + Actual: Left("left") + """; + var exception = Assert.ThrowsAny(() => FunctionalAssert.Right(Either.Left("left"))); + Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); + } +} diff --git a/Funcky.Xunit.Test/FunctionalAssertClass/SomeTest.cs b/Funcky.Xunit.Test/FunctionalAssertClass/SomeTest.cs new file mode 100644 index 00000000..379173c5 --- /dev/null +++ b/Funcky.Xunit.Test/FunctionalAssertClass/SomeTest.cs @@ -0,0 +1,57 @@ +using Xunit.Sdk; + +namespace Funcky.Xunit.Test.FunctionalAssertClass; + +public sealed class SomeTest +{ + [Fact] + public void DoesNotThrowForMatchingSome() + { + FunctionalAssert.Some(10, Option.Some(10)); + } + + [Fact] + public void DoesNotThrowForRight() + { + _ = FunctionalAssert.Some(Option.Some(10)); + } + + [Fact] + public void ThrowsForMismatchingSome() + { + const string expectedMessage = + """ + FunctionalAssert.Some() Failure: Values differ + Expected: Some(10) + Actual: Some(11) + """; + var exception = Assert.ThrowsAny(() => FunctionalAssert.Some(10, Option.Some(11))); + Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); + } + + [Fact] + public void ThrowsForNoneWithExpectedValue() + { + const string expectedMessage = + """ + FunctionalAssert.Some() Failure: Values differ + Expected: Some(10) + Actual: None + """; + var exception = Assert.ThrowsAny(() => FunctionalAssert.Some(10, Option.None)); + Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); + } + + [Fact] + public void ThrowsForNone() + { + const string expectedMessage = + """ + FunctionalAssert.Some() Failure: Values differ + Expected: Some(...) + Actual: None + """; + var exception = Assert.ThrowsAny(() => FunctionalAssert.Some(Option.None)); + Assert.Equal(expectedMessage.ReplaceLineEndings(Environment.NewLine), exception.Message); + } +} From 28c5df7ba69fc5050fa0fedabd87d2e0abcfbff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Mon, 13 Jan 2025 00:01:07 +0100 Subject: [PATCH 3/5] Fix error messages in `AsyncAssert` --- .../TestUtilities/AsyncAssert.cs | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/Funcky.Async.Test/TestUtilities/AsyncAssert.cs b/Funcky.Async.Test/TestUtilities/AsyncAssert.cs index 13563511..b8d0eb0d 100644 --- a/Funcky.Async.Test/TestUtilities/AsyncAssert.cs +++ b/Funcky.Async.Test/TestUtilities/AsyncAssert.cs @@ -11,8 +11,7 @@ public static async Task Empty(IAsyncEnumerable asyncSequenc { if (await asyncEnumerator.MoveNextAsync()) { - var actual = await asyncSequence.ToListAsync(); - throw EmptyException.ForNonEmptyCollection(collection: "TODO"); + throw EmptyException.ForNonEmptyCollection(collection: await FormatCollectionStart(asyncSequence)); } } finally @@ -49,15 +48,15 @@ public static async Task Single(IAsyncEnumerable asyncSequence) if (await asyncEnumerator.MoveNextAsync() is false) { - throw SingleException.Empty(expected: null, collection: "TODO"); + throw SingleException.Empty(expected: null, collection: string.Empty); } var result = asyncEnumerator.Current; if (await asyncEnumerator.MoveNextAsync()) { - var actual = await asyncSequence.ToListAsync(); - throw SingleException.MoreThanOne(expected: null, collection: "TODO", count: actual.Count, matchIndices: Array.Empty()); + var actual = await MaterializeCollectionStart(asyncSequence); + throw SingleException.MoreThanOne(expected: null, collection: FormatCollectionStart(actual), count: actual.Count, matchIndices: Array.Empty()); } return result; @@ -65,4 +64,20 @@ public static async Task Single(IAsyncEnumerable asyncSequence) public static async Task Equal(IAsyncEnumerable expectedResult, IAsyncEnumerable actual) => Assert.Equal(await expectedResult.ToListAsync(), await actual.ToListAsync()); + + private static async Task> MaterializeCollectionStart(IAsyncEnumerable asyncSequence) + { + // This should *ideally* be kept in sync with XUnit's `ArgumentFormatter.MAX_ENUMERABLE_LENGTH + 1` (which is private). + const int maxEnumerableLength = 6; + return await asyncSequence.Take(maxEnumerableLength).ToListAsync(); + } + + private static async Task FormatCollectionStart(IAsyncEnumerable asyncSequence) + => FormatCollectionStart(await MaterializeCollectionStart(asyncSequence)); + + private static string FormatCollectionStart(IEnumerable sequence) + { + using var tracker = sequence.AsTracker(); + return tracker.FormatStart(); + } } From 8fa2af359a0a29ce9deb5ab4da98d4a226d1cf9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Mon, 13 Jan 2025 10:20:11 +0100 Subject: [PATCH 4/5] Remove .ConfigureAwait(false) from test --- Funcky.Async.Test/AsyncSequence/RepeatRangeTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Funcky.Async.Test/AsyncSequence/RepeatRangeTest.cs b/Funcky.Async.Test/AsyncSequence/RepeatRangeTest.cs index 2932f3c3..c61ed5f2 100644 --- a/Funcky.Async.Test/AsyncSequence/RepeatRangeTest.cs +++ b/Funcky.Async.Test/AsyncSequence/RepeatRangeTest.cs @@ -41,7 +41,7 @@ public async Task RepeatRangeThrowsWhenAlreadyDisposedEvenIfYouDisposeBetweenMov var repeatRange = AsyncSequence.RepeatRange(list, repeats); await using var enumerator = repeatRange.GetAsyncEnumerator(); - Assert.True(await AsyncEnumerable.Range(0, i).AllAwaitAsync(async _ => await enumerator.MoveNextAsync()).ConfigureAwait(false)); + Assert.True(await AsyncEnumerable.Range(0, i).AllAwaitAsync(async _ => await enumerator.MoveNextAsync())); #pragma warning disable IDISP016 // we test behaviour after Dispose #pragma warning disable IDISP017 // we test behaviour after Dispose From c4c54ed343ad17eeafcf7f9f83c6f24b35e108e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Mon, 13 Jan 2025 10:30:36 +0100 Subject: [PATCH 5/5] Don't test against EOL target frameworks xunit.runner.visualstudio only supports >= net6.0 and .NET Framework --- .github/workflows/build.yml | 8 -------- Funcky.Test/Funcky.Test.csproj | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 013eb12b..dd78d7e1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,14 +23,6 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v4 name: Install Current .NET SDK - - uses: actions/setup-dotnet@v4 - name: 'Install .NET SDK 3.1' - with: - dotnet-version: '3.1.x' - - uses: actions/setup-dotnet@v4 - name: 'Install .NET SDK 5.0' - with: - dotnet-version: '5.0.x' - uses: actions/setup-dotnet@v4 name: 'Install .NET SDK 7.0' with: diff --git a/Funcky.Test/Funcky.Test.csproj b/Funcky.Test/Funcky.Test.csproj index 9c8ba337..081441d2 100644 --- a/Funcky.Test/Funcky.Test.csproj +++ b/Funcky.Test/Funcky.Test.csproj @@ -1,6 +1,6 @@ - net8.0;net7.0;net6.0;net5.0;netcoreapp3.1 + net8.0;net7.0;net6.0 $(TargetFrameworks);net4.8 preview enable