diff --git a/Akka.sln b/Akka.sln index 6d5dafd201e..4838b6b2d75 100644 --- a/Akka.sln +++ b/Akka.sln @@ -277,6 +277,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClusterToolsExample.Node", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.TestKit.Xunit2.Tests", "src\contrib\testkits\Akka.TestKit.Xunit2.Tests\Akka.TestKit.Xunit2.Tests.csproj", "{95017C99-E960-44E5-83AD-BF21461DF06F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.TestKit.Xunit.Tests", "src\contrib\testkits\Akka.TestKit.Xunit.Tests\Akka.TestKit.Xunit.Tests.csproj", "{F80F41E6-E5C7-4C92-B1CF-42539ECFBE68}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1304,6 +1306,18 @@ Global {95017C99-E960-44E5-83AD-BF21461DF06F}.Release|x64.Build.0 = Release|Any CPU {95017C99-E960-44E5-83AD-BF21461DF06F}.Release|x86.ActiveCfg = Release|Any CPU {95017C99-E960-44E5-83AD-BF21461DF06F}.Release|x86.Build.0 = Release|Any CPU + {F80F41E6-E5C7-4C92-B1CF-42539ECFBE68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F80F41E6-E5C7-4C92-B1CF-42539ECFBE68}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F80F41E6-E5C7-4C92-B1CF-42539ECFBE68}.Debug|x64.ActiveCfg = Debug|Any CPU + {F80F41E6-E5C7-4C92-B1CF-42539ECFBE68}.Debug|x64.Build.0 = Debug|Any CPU + {F80F41E6-E5C7-4C92-B1CF-42539ECFBE68}.Debug|x86.ActiveCfg = Debug|Any CPU + {F80F41E6-E5C7-4C92-B1CF-42539ECFBE68}.Debug|x86.Build.0 = Debug|Any CPU + {F80F41E6-E5C7-4C92-B1CF-42539ECFBE68}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F80F41E6-E5C7-4C92-B1CF-42539ECFBE68}.Release|Any CPU.Build.0 = Release|Any CPU + {F80F41E6-E5C7-4C92-B1CF-42539ECFBE68}.Release|x64.ActiveCfg = Release|Any CPU + {F80F41E6-E5C7-4C92-B1CF-42539ECFBE68}.Release|x64.Build.0 = Release|Any CPU + {F80F41E6-E5C7-4C92-B1CF-42539ECFBE68}.Release|x86.ActiveCfg = Release|Any CPU + {F80F41E6-E5C7-4C92-B1CF-42539ECFBE68}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1426,6 +1440,7 @@ Global {ED00E6F4-2B5C-4F16-ADE4-45E4A73C17B8} = {7735F35A-E7B7-44DE-B6FB-C770B53EB69C} {337A85B5-4A7C-4883-8634-46E7E52A765F} = {7735F35A-E7B7-44DE-B6FB-C770B53EB69C} {95017C99-E960-44E5-83AD-BF21461DF06F} = {7625FD95-4B2C-4A5B-BDD5-94B1493FAC8E} + {F80F41E6-E5C7-4C92-B1CF-42539ECFBE68} = {7625FD95-4B2C-4A5B-BDD5-94B1493FAC8E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {03AD8E21-7507-4E68-A4E9-F4A7E7273164} diff --git a/Directory.Build.props b/Directory.Build.props index 3ac9e59551c..61f92758949 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -19,6 +19,8 @@ 2.8.1 + 2.0.1 + 3.0.2 2.8.1 17.9.0 0.12.2 diff --git a/src/contrib/testkits/Akka.TestKit.Xunit.Tests/Akka.TestKit.Xunit.Tests.csproj b/src/contrib/testkits/Akka.TestKit.Xunit.Tests/Akka.TestKit.Xunit.Tests.csproj new file mode 100644 index 00000000000..18e1bcaf826 --- /dev/null +++ b/src/contrib/testkits/Akka.TestKit.Xunit.Tests/Akka.TestKit.Xunit.Tests.csproj @@ -0,0 +1,30 @@ + + + + + $(NetFrameworkTestVersion);net8.0 + Exe + enable + enable + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/contrib/testkits/Akka.TestKit.Xunit.Tests/Internals/AkkaEqualExceptionSpec.cs b/src/contrib/testkits/Akka.TestKit.Xunit.Tests/Internals/AkkaEqualExceptionSpec.cs new file mode 100644 index 00000000000..c56b0fd665a --- /dev/null +++ b/src/contrib/testkits/Akka.TestKit.Xunit.Tests/Internals/AkkaEqualExceptionSpec.cs @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +//----------------------------------------------------------------------- + +using Akka.TestKit.Xunit.Internals; +using Xunit; + +namespace Akka.TestKit.Xunit.Tests.Internals; + +public static class AkkaEqualExceptionSpec +{ +#if NETFRAMEWORK + [Fact] + public static void Constructor_deserializes_message() + { + var originalException = new AkkaEqualException("Test message"); + + AkkaEqualException deserializedException; + using (var memoryStream = new System.IO.MemoryStream()) + { + var formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter(); + formatter.Serialize(memoryStream, originalException); + memoryStream.Seek(0, System.IO.SeekOrigin.Begin); + deserializedException = (AkkaEqualException)formatter.Deserialize(memoryStream); + } + + Assert.Equal(originalException.Message, deserializedException.Message); + } +#endif +} diff --git a/src/contrib/testkits/Akka.TestKit.Xunit.Tests/XunitAssertionsSpec.cs b/src/contrib/testkits/Akka.TestKit.Xunit.Tests/XunitAssertionsSpec.cs new file mode 100644 index 00000000000..7bea4f94d57 --- /dev/null +++ b/src/contrib/testkits/Akka.TestKit.Xunit.Tests/XunitAssertionsSpec.cs @@ -0,0 +1,82 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +//----------------------------------------------------------------------- + +using Akka.TestKit.Xunit; +using Xunit; +using Xunit.Sdk; + +namespace Akka.TestKit.Xunit.Tests; + +public class XunitAssertionsSpec +{ + private readonly XunitAssertions _assertions = new(); + + [Fact] + public void Assert_does_not_format_message_when_no_arguments_are_specified() + { + const string testMessage = "{Value} with different format placeholders {0}"; + + var exception = Assert.ThrowsAny(() => _assertions.Fail(testMessage)); + Assert.Contains(testMessage, exception.Message); + + exception = Assert.ThrowsAny(() => _assertions.AssertTrue(false, testMessage)); + Assert.Contains(testMessage, exception.Message); + + exception = Assert.ThrowsAny(() => _assertions.AssertFalse(true, testMessage)); + Assert.Contains(testMessage, exception.Message); + + exception = Assert.ThrowsAny(() => _assertions.AssertEqual(4, 2, testMessage)); + Assert.Contains(testMessage, exception.Message); + + exception = Assert.ThrowsAny(() => _assertions.AssertEqual(4, 2, (_, _) => false, testMessage)); + Assert.Contains(testMessage, exception.Message); + } + + [Fact] + public void Assert_formats_message_when_arguments_are_specified() + { + const string testMessage = "Meaning: {0}"; + const string expectedMessage = "Meaning: 42"; + + var exception = Assert.ThrowsAny(() => _assertions.Fail(testMessage, 42)); + Assert.Contains(expectedMessage, exception.Message); + + exception = Assert.ThrowsAny(() => _assertions.AssertTrue(false, testMessage, 42)); + Assert.Contains(expectedMessage, exception.Message); + + exception = Assert.ThrowsAny(() => _assertions.AssertFalse(true, testMessage, 42)); + Assert.Contains(expectedMessage, exception.Message); + + exception = Assert.ThrowsAny(() => _assertions.AssertEqual(4, 2, testMessage, 42)); + Assert.Contains(expectedMessage, exception.Message); + + exception = Assert.ThrowsAny(() => _assertions.AssertEqual(4, 2, (_, _) => false, testMessage, 42)); + Assert.Contains(expectedMessage, exception.Message); + } + + [Fact] + public void Assert_catches_format_exceptions() + { + const string testMessage = "Meaning: {0} {1}"; + const string expectedMessage = "Could not string.Format"; + + var exception = Assert.ThrowsAny(() => _assertions.Fail(testMessage, 42)); + Assert.Contains(expectedMessage, exception.Message); + + exception = Assert.ThrowsAny(() => _assertions.AssertTrue(false, testMessage, 42)); + Assert.Contains(expectedMessage, exception.Message); + + exception = Assert.ThrowsAny(() => _assertions.AssertFalse(true, testMessage, 42)); + Assert.Contains(expectedMessage, exception.Message); + + exception = Assert.ThrowsAny(() => _assertions.AssertEqual(4, 2, testMessage, 42)); + Assert.Contains(expectedMessage, exception.Message); + + exception = Assert.ThrowsAny(() => _assertions.AssertEqual(4, 2, (_, _) => false, testMessage, 42)); + Assert.Contains(expectedMessage, exception.Message); + } +} diff --git a/src/contrib/testkits/Akka.TestKit.Xunit/Akka.TestKit.Xunit.csproj b/src/contrib/testkits/Akka.TestKit.Xunit/Akka.TestKit.Xunit.csproj index 96315b826d3..c51c54b72ed 100644 --- a/src/contrib/testkits/Akka.TestKit.Xunit/Akka.TestKit.Xunit.csproj +++ b/src/contrib/testkits/Akka.TestKit.Xunit/Akka.TestKit.Xunit.csproj @@ -2,14 +2,16 @@ TestKit for writing tests for Akka.NET using xUnit. - $(NetStandardLibVersion);$(NetLibVersion) + $(NetStandardLibVersion);net8.0 + enable $(AkkaPackageTags);testkit;xunit true - + + diff --git a/src/contrib/testkits/Akka.TestKit.Xunit/Attributes/LocalFactAttribute.cs b/src/contrib/testkits/Akka.TestKit.Xunit/Attributes/LocalFactAttribute.cs new file mode 100644 index 00000000000..97ef7802810 --- /dev/null +++ b/src/contrib/testkits/Akka.TestKit.Xunit/Attributes/LocalFactAttribute.cs @@ -0,0 +1,71 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Xunit; +using Xunit.v3; + +namespace Akka.TestKit.Xunit.Attributes; + +/// +/// +/// This custom XUnit Fact attribute will skip unit tests if the environment variable +/// "XUNIT_SKIP_LOCAL_FACT" exists and is set to the string "true" +/// +/// +/// Note that the original property takes precedence over this attribute, +/// any unit tests with with its property +/// set will always be skipped, regardless of the environment variable content. +/// +/// +[XunitTestCaseDiscoverer(typeof(FactDiscoverer))] +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +public class LocalFactAttribute: Attribute, IFactAttribute +{ + private const string EnvironmentVariableName = "XUNIT_SKIP_LOCAL_FACT"; + + private string? _skip; + + /// + public string? DisplayName { get; set; } + + /// + public bool Explicit { get; set; } + + /// + public Type[]? SkipExceptions { get; set; } + + /// + public Type? SkipType { get; set; } + + /// + public string? SkipUnless { get; set; } + + /// + public string? SkipWhen { get; set; } + + /// + public int Timeout { get; set; } + + /// + public string? Skip + { + get + { + var skipLocal = Environment.GetEnvironmentVariable(EnvironmentVariableName)? + .ToLowerInvariant(); + return skipLocal is "true" ? SkipLocal ?? "Local facts are being skipped" : _skip; + } + set => _skip = value; + } + + /// + /// The reason why this unit test is being skipped by the . + /// Note that the original property takes precedence over this message. + /// + public string? SkipLocal { get; set; } +} \ No newline at end of file diff --git a/src/contrib/testkits/Akka.TestKit.Xunit/Attributes/LocalTheoryAttribute.cs b/src/contrib/testkits/Akka.TestKit.Xunit/Attributes/LocalTheoryAttribute.cs new file mode 100644 index 00000000000..1e2a4552aeb --- /dev/null +++ b/src/contrib/testkits/Akka.TestKit.Xunit/Attributes/LocalTheoryAttribute.cs @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Xunit.v3; + +namespace Akka.TestKit.Xunit.Attributes +{ + /// + /// + /// This custom XUnit Fact attribute will skip unit tests if the environment variable + /// "XUNIT_SKIP_LOCAL_THEORY" exists and is set to the string "true" + /// + /// + /// Note that the original property takes precedence over this attribute, + /// any unit tests with with its property + /// set will always be skipped, regardless of the environment variable content. + /// + /// + [XunitTestCaseDiscoverer(typeof(TheoryDiscoverer))] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class LocalTheoryAttribute : LocalFactAttribute, ITheoryAttribute + { + /// + public bool DisableDiscoveryEnumeration { get; set; } + + /// + public bool SkipTestWithoutData { get; set; } + } +} diff --git a/src/contrib/testkits/Akka.TestKit.Xunit/Attributes/WindowsFactAttribute.cs b/src/contrib/testkits/Akka.TestKit.Xunit/Attributes/WindowsFactAttribute.cs new file mode 100644 index 00000000000..e7cfb8dcc38 --- /dev/null +++ b/src/contrib/testkits/Akka.TestKit.Xunit/Attributes/WindowsFactAttribute.cs @@ -0,0 +1,71 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Xunit.v3; + +namespace Akka.TestKit.Xunit.Attributes +{ + /// + /// + /// This custom XUnit Fact attribute will skip unit tests if the run-time environment is not windows + /// + /// + /// Note that the original property takes precedence over this attribute, + /// any unit tests with with its property + /// set will always be skipped, regardless of the environment variable content. + /// + /// + public class WindowsFactAttribute : Attribute, IFactAttribute + { + private string? _skip; + + + /// + public string? DisplayName { get; set; } + + /// + public bool Explicit { get; set; } + + /// + public Type[]? SkipExceptions { get; set; } + + /// + public Type? SkipType { get; set; } + + /// + public string? SkipUnless { get; set; } + + /// + public string? SkipWhen { get; set; } + + /// + public int Timeout { get; set; } + + /// + public string? Skip + { + get + { + if (_skip != null) + return _skip; + + var platform = Environment.OSVersion.Platform; + var notWindows = platform is PlatformID.MacOSX or PlatformID.Unix or PlatformID.Xbox; + return notWindows ? SkipUnix ?? "Skipped under Unix platforms" : null; + } + set => _skip = value; + } + + /// + /// The reason why this unit test is being skipped by the . + /// Note that the original property takes precedence over this message. + /// + public string? SkipUnix { get; set; } + } +} + diff --git a/src/contrib/testkits/Akka.TestKit.Xunit/Internals/AkkaAssertEqualityComparer.cs b/src/contrib/testkits/Akka.TestKit.Xunit/Internals/AkkaAssertEqualityComparer.cs index c288a6be60e..c731b580e97 100644 --- a/src/contrib/testkits/Akka.TestKit.Xunit/Internals/AkkaAssertEqualityComparer.cs +++ b/src/contrib/testkits/Akka.TestKit.Xunit/Internals/AkkaAssertEqualityComparer.cs @@ -8,90 +8,95 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Reflection; -namespace Akka.TestKit.Xunit.Internals +namespace Akka.TestKit.Xunit.Internals; + +internal static class AkkaAssertEqualityComparer { + public static readonly IEqualityComparer DefaultInnerComparer = new AkkaAssertEqualityComparerAdapter(new AkkaAssertEqualityComparer()); + public static readonly Type NullableType = typeof(Nullable<>); +} + +/// +/// Default implementation of used by the Akka's xUnit.net equality assertions. +/// Copy of xUnits code +/// https://github.com/xunit/xunit/blob/3e6ab94ca231a6d8c86e90d6e724631a0faa33b7/src/xunit.assert/Asserts/Sdk/AssertEqualityComparer.cs +/// Note! Part of internal API. Breaking changes may occur without notice. Use at own risk. +/// +/// The type that is being compared. +public class AkkaAssertEqualityComparer : IEqualityComparer +{ + private readonly Func _innerComparerFactory; + private readonly bool _skipTypeCheck; + /// - /// Default implementation of IEqualityComparer{T} used by the Akka's xUnit.net equality assertions. - /// Copy of xUnits code - /// https://github.com/xunit/xunit/blob/3e6ab94ca231a6d8c86e90d6e724631a0faa33b7/src/xunit.assert/Asserts/Sdk/AssertEqualityComparer.cs - /// Note! Part of internal API. Breaking changes may occur without notice. Use at own risk. + /// Initializes a new instance of the class. /// - /// The type that is being compared. - public class AkkaAssertEqualityComparer : IEqualityComparer + /// Set to true to skip type equality checks. + /// The inner comparer to be used when the compared objects are enumerable. + public AkkaAssertEqualityComparer(bool skipTypeCheck = false, IEqualityComparer? innerComparer = null) + { + _skipTypeCheck = skipTypeCheck; + + // Use a thunk to delay evaluation of DefaultInnerComparer + _innerComparerFactory = () => innerComparer ?? AkkaAssertEqualityComparer.DefaultInnerComparer; + } + + public bool Equals(T? x, T? y) { - private static readonly IEqualityComparer DefaultInnerComparer = new AkkaAssertEqualityComparerAdapter(new AkkaAssertEqualityComparer()); - private static readonly Type NullableType = typeof(Nullable<>); - - private readonly Func _innerComparerFactory; - private readonly bool _skipTypeCheck; - - /// - /// Initializes a new instance of the class. - /// - /// Set to true to skip type equality checks. - /// The inner comparer to be used when the compared objects are enumerable. - public AkkaAssertEqualityComparer(bool skipTypeCheck = false, IEqualityComparer innerComparer = null) + var type = typeof(T); + + // Null? + if(!type.IsValueType || (type.IsGenericType && type.GetGenericTypeDefinition().IsAssignableFrom(AkkaAssertEqualityComparer.NullableType))) { - _skipTypeCheck = skipTypeCheck; + if(object.Equals(x, default(T))) + return object.Equals(y, default(T)); - // Use a thunk to delay evaluation of DefaultInnerComparer - _innerComparerFactory = () => innerComparer ?? DefaultInnerComparer; + if(object.Equals(y, default(T))) + return false; } - - public bool Equals(T x, T y) + switch (x) { - var type = typeof(T); - - // Null? - if(!type.IsValueType || (type.IsGenericType && type.GetGenericTypeDefinition().IsAssignableFrom(NullableType))) - { - if(object.Equals(x, default(T))) - return object.Equals(y, default(T)); - - if(object.Equals(y, default(T))) - return false; - } - - switch (x) - { - case IEquatable equatable: - return equatable.Equals(y); - case IComparable comparableGeneric: - return comparableGeneric.CompareTo(y) == 0; - case IComparable comparable: - return comparable.CompareTo(y) == 0; - case IEnumerable enumerableX - when y is IEnumerable enumerableY: - - var enumeratorX = enumerableX.GetEnumerator(); - var enumeratorY = enumerableY.GetEnumerator(); - var equalityComparer = _innerComparerFactory(); - + case IEquatable equatable: + return equatable.Equals(y!); + case IComparable comparableGeneric: + return comparableGeneric.CompareTo(y!) == 0; + case IComparable comparable: + return comparable.CompareTo(y) == 0; + case IEnumerable enumerableX when y is IEnumerable enumerableY: + var enumeratorX = enumerableX.GetEnumerator(); + var enumeratorY = enumerableY.GetEnumerator(); + var equalityComparer = _innerComparerFactory(); + + try + { while (true) { var hasNextX = enumeratorX.MoveNext(); var hasNextY = enumeratorY.MoveNext(); if (!hasNextX || !hasNextY) - return hasNextX == hasNextY; + return (hasNextX == hasNextY); if (!equalityComparer.Equals(enumeratorX.Current, enumeratorY.Current)) return false; } - } - - // Same type? - if (!_skipTypeCheck && x.GetType() != y.GetType()) - return false; - - // Last case, rely on Object.Equals - return object.Equals(x, y); + } + finally + { + (enumeratorX as IDisposable)?.Dispose(); + (enumeratorY as IDisposable)?.Dispose(); + } } - - public int GetHashCode(T obj) => throw new NotImplementedException(); + // Same type? + if (!_skipTypeCheck && x!.GetType() != y!.GetType()) + return false; + + // Last case, rely on Object.Equals + return object.Equals(x, y); } -} + + public int GetHashCode(T obj) => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/src/contrib/testkits/Akka.TestKit.Xunit/Internals/AkkaAssertEqualityComparerAdapter.cs b/src/contrib/testkits/Akka.TestKit.Xunit/Internals/AkkaAssertEqualityComparerAdapter.cs index 6a2e7e9c736..c57524569d6 100644 --- a/src/contrib/testkits/Akka.TestKit.Xunit/Internals/AkkaAssertEqualityComparerAdapter.cs +++ b/src/contrib/testkits/Akka.TestKit.Xunit/Internals/AkkaAssertEqualityComparerAdapter.cs @@ -9,33 +9,31 @@ using System.Collections; using System.Collections.Generic; -namespace Akka.TestKit.Xunit.Internals +namespace Akka.TestKit.Xunit.Internals; + +/// +/// A class that wraps to create . +/// Copy of xUnits class: +/// https://github.com/xunit/xunit/blob/3e6ab94ca231a6d8c86e90d6e724631a0faa33b7/src/xunit.assert/Asserts/Sdk/AssertEqualityComparerAdapter.cs +/// Note! Part of internal API. Breaking changes may occur without notice. Use at own risk. +/// +/// The type that is being compared. +internal class AkkaAssertEqualityComparerAdapter : IEqualityComparer { + private readonly IEqualityComparer _innerComparer; + /// - /// A class that wraps to create . - /// Copy of xUnits class: - /// https://github.com/xunit/xunit/blob/3e6ab94ca231a6d8c86e90d6e724631a0faa33b7/src/xunit.assert/Asserts/Sdk/AssertEqualityComparerAdapter.cs - /// Note! Part of internal API. Breaking changes may occur without notice. Use at own risk. + /// Initializes a new instance of the class. /// - /// The type that is being compared. - internal class AkkaAssertEqualityComparerAdapter : IEqualityComparer + /// The comparer that is being adapted. + public AkkaAssertEqualityComparerAdapter(IEqualityComparer innerComparer) { - private readonly IEqualityComparer _innerComparer; - - /// - /// Initializes a new instance of the class. - /// - /// The comparer that is being adapted. - public AkkaAssertEqualityComparerAdapter(IEqualityComparer innerComparer) - { - _innerComparer = innerComparer; - } - - /// - public new bool Equals(object x, object y) => _innerComparer.Equals((T)x, (T)y); - - /// - public int GetHashCode(object obj) => throw new NotImplementedException(); + _innerComparer = innerComparer; } -} + /// + public new bool Equals(object? x, object? y) => _innerComparer.Equals((T?)x, (T?)y); + + /// + public int GetHashCode(object obj) => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/src/contrib/testkits/Akka.TestKit.Xunit/Internals/AkkaEqualException.cs b/src/contrib/testkits/Akka.TestKit.Xunit/Internals/AkkaEqualException.cs index ba7372e7e7a..021c75cdc66 100644 --- a/src/contrib/testkits/Akka.TestKit.Xunit/Internals/AkkaEqualException.cs +++ b/src/contrib/testkits/Akka.TestKit.Xunit/Internals/AkkaEqualException.cs @@ -9,85 +9,80 @@ using System.Runtime.Serialization; using Xunit.Sdk; -namespace Akka.TestKit.Xunit.Internals +namespace Akka.TestKit.Xunit.Internals; + +/// +/// TBD +/// +[Serializable] +public class AkkaEqualException : XunitException { - /// - /// TBD - /// - public class AkkaEqualException : XunitException - { - private static readonly string NewLineAndIndent = Environment.NewLine + new string(' ', 10); // Length of "Expected: " and "Actual: " + // Length of "Expected: " and "Actual: " + private static readonly string NewLineAndIndent = Environment.NewLine + new string(' ', 10); - private readonly string _format; - private readonly object[] _args; + public static AkkaEqualException ForMismatchedValues( + object? expected, + object? actual, + string? format = null, + params object[] args) + { + // Strings normally come through ForMismatchedStrings, so we want to make sure any + // string value that comes through here isn't re-formatted/truncated. This is for + // two reasons: (a) to support Assert.Equal(string1, string2) to get a full + // printout of the raw string values, which is useful when debugging; and (b) to + // allow the assertion functions to pre-format the value themselves, perhaps with + // additional information (like DateTime/DateTimeOffset when providing the precision + // of the comparison). + var expectedText = expected as string ?? ArgumentFormatter.Format(expected); + var actualText = actual as string ?? ArgumentFormatter.Format(actual); - public static AkkaEqualException ForMismatchedValues( - object expected, - object actual, - string format = null, - params object[] args) - { - // Strings normally come through ForMismatchedStrings, so we want to make sure any - // string value that comes through here isn't re-formatted/truncated. This is for - // two reasons: (a) to support Assert.Equal(string1, string2) to get a full - // printout of the raw string values, which is useful when debugging; and (b) to - // allow the assertion functions to pre-format the value themselves, perhaps with - // additional information (like DateTime/DateTimeOffset when providing the precision - // of the comparison). - var expectedText = expected as string ?? ArgumentFormatter.Format(expected); - var actualText = actual as string ?? ArgumentFormatter.Format(actual); + return new AkkaEqualException( + "Assert.Equal() Failure: " + (format ?? "Values differ") + Environment.NewLine + + "Expected: " + expectedText.Replace(Environment.NewLine, NewLineAndIndent) + Environment.NewLine + + "Actual: " + actualText.Replace(Environment.NewLine, NewLineAndIndent), + args + ); + } - return new AkkaEqualException( - @$"Assert.Equal() Failure: {format ?? "Values differ"}{Environment.NewLine} -Expected: {expectedText.Replace(Environment.NewLine, NewLineAndIndent)}{Environment.NewLine} -Actual: {actualText.Replace(Environment.NewLine, NewLineAndIndent)}", - args - ); + /// + /// Initializes a new instance of the class. + /// + /// A template string that describes the error. + /// An optional object array that contains zero or more objects to format. + public AkkaEqualException(string format = "", params object[] args) + : base(BuildAssertionMessage(format, args)) { } - } - - /// - /// Initializes a new instance of the class. - /// - /// A template string that describes the error. - /// An optional object array that contains zero or more objects to format. - public AkkaEqualException(string format = "", params object[] args): base(null) + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. + protected AkkaEqualException(SerializationInfo info, StreamingContext context) + : base(info.GetString("Message")) { } + + /// + /// Builds assertion message by applying specified arguments to the format string. + /// When no arguments are specified, format string is returned as-is. + /// + internal static string? BuildAssertionMessage(string format, object[] args) + { + if (string.IsNullOrEmpty(format)) { - _format = format; - _args = args; + return null; } - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected AkkaEqualException(SerializationInfo info, StreamingContext context): base(null) + if (args is not { Length: > 0 }) { + return format; } - /// - /// The message that describes the error. - /// - public override string Message + try { - get - { - if(string.IsNullOrEmpty(_format)) - return base.Message; - - string message; - try - { - message = string.Format(_format, _args); - } - catch(Exception) - { - message = $@"[Could not string.Format(""{_format}"", {string.Join(", ", _args)})]"; - } - - return base.Message is not null ? $"{base.Message} {message}" : message; - } + return string.Format(format, args); + } + catch (Exception) + { + return $"""[Could not string.Format("{format}", {string.Join(", ", args)})]"""; } } -} +} \ No newline at end of file diff --git a/src/contrib/testkits/Akka.TestKit.Xunit/Internals/Loggers.cs b/src/contrib/testkits/Akka.TestKit.Xunit/Internals/Loggers.cs index 4ae08dbabe5..5d4130dff38 100644 --- a/src/contrib/testkits/Akka.TestKit.Xunit/Internals/Loggers.cs +++ b/src/contrib/testkits/Akka.TestKit.Xunit/Internals/Loggers.cs @@ -8,53 +8,60 @@ using System; using Akka.Actor; using Akka.Event; -using Xunit.Abstractions; +using Akka.Util; +using Xunit; -namespace Akka.TestKit.Xunit.Internals +namespace Akka.TestKit.Xunit.Internals; + +/// +/// This class represents an actor that logs output from tests using an provider. +/// +public class TestOutputLogger : ReceiveActor { + private readonly ITestOutputHelper _output; + /// - /// This class represents an actor that logs output from tests using an ITestOutputHelper provider. + /// Initializes a new instance of the class. /// - public class TestOutputLogger : ReceiveActor + /// The provider used to write test output. + public TestOutputLogger(ITestOutputHelper output) { - private readonly ITestOutputHelper _output; - - /// - /// Initializes a new instance of the class. - /// - /// The provider used to write test output. - public TestOutputLogger(ITestOutputHelper output) + _output = output; + + Receive(HandleLogEvent); + Receive(HandleLogEvent); + Receive(HandleLogEvent); + Receive(HandleLogEvent); + Receive(e => { - _output = output; - - Receive(Write); - Receive(Write); - Receive(Write); - Receive(Write); - Receive(e => - { - e.LoggingBus.Subscribe(Self, typeof (LogEvent)); - }); - } + e.LoggingBus.Subscribe(Self, typeof (LogEvent)); + Sender.Tell(new LoggerInitialized()); + }); + } - private void Write(LogEvent e) + private void HandleLogEvent(LogEvent e) + { + try { - try - { - _output.WriteLine(e.ToString()); - } - catch (FormatException ex) + _output.WriteLine(e.ToString()); + } + catch (FormatException ex) + { + if (e.Message is LogMessage msg) { - if (e.Message is LogMessage msg) - { - var message = - $"Received a malformed formatted message. Log level: [{e.LogLevel()}], Template: [{msg.Format}], args: [{string.Join(",", msg.Unformatted())}]"; - if(e.Cause != null) - throw new AggregateException(message, ex, e.Cause); - throw new FormatException(message, ex); - } - throw; + var message = + $"Received a malformed formatted message. Log level: [{e.LogLevel()}], Template: [{msg.Format}], args: [{string.Join(",", msg.Unformatted())}]"; + if (e.Cause != null) + throw new AggregateException(message, ex, e.Cause); + throw new FormatException(message, ex); } + + throw; + } + catch (InvalidOperationException ie) + { + StandardOutWriter.WriteLine($"Received InvalidOperationException: {ie} - probably because the test had completed executing."); + Context.Stop(Self); // shut ourselves down, can't do our job any longer } } -} +} \ No newline at end of file diff --git a/src/contrib/testkits/Akka.TestKit.Xunit/Properties/AssemblyInfo.cs b/src/contrib/testkits/Akka.TestKit.Xunit/Properties/AssemblyInfo.cs deleted file mode 100644 index aeef4172d6c..00000000000 --- a/src/contrib/testkits/Akka.TestKit.Xunit/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,33 +0,0 @@ -//----------------------------------------------------------------------- -// -// Copyright (C) 2009-2022 Lightbend Inc. -// Copyright (C) 2013-2025 .NET Foundation -// -//----------------------------------------------------------------------- - -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("2b7e5487-4c34-4ef5-994f-b41766d175bd")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] - diff --git a/src/contrib/testkits/Akka.TestKit.Xunit/Properties/FriendsOf.cs b/src/contrib/testkits/Akka.TestKit.Xunit/Properties/FriendsOf.cs new file mode 100644 index 00000000000..8b60afea87c --- /dev/null +++ b/src/contrib/testkits/Akka.TestKit.Xunit/Properties/FriendsOf.cs @@ -0,0 +1,10 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Akka.TestKit.Xunit3.Tests")] \ No newline at end of file diff --git a/src/contrib/testkits/Akka.TestKit.Xunit/TestKit.cs b/src/contrib/testkits/Akka.TestKit.Xunit/TestKit.cs index cb52b2215f2..26272f21045 100644 --- a/src/contrib/testkits/Akka.TestKit.Xunit/TestKit.cs +++ b/src/contrib/testkits/Akka.TestKit.Xunit/TestKit.cs @@ -11,159 +11,211 @@ using Akka.Configuration; using Akka.Event; using Akka.TestKit.Xunit.Internals; -using Xunit.Abstractions; +using Xunit; -namespace Akka.TestKit.Xunit +namespace Akka.TestKit.Xunit; + +/// +/// This class represents an Akka.NET TestKit that uses xUnit +/// as its testing framework. +/// +public class TestKit : TestKitBase, IDisposable { - /// - /// This class represents an Akka.NET TestKit that uses xUnit - /// as its testing framework. - /// - public class TestKit : TestKitBase , IDisposable + private class PrefixedOutput : ITestOutputHelper { - private bool _isDisposed; //Automatically initialized to false; - - /// - /// The provider used to write test output. - /// - protected readonly ITestOutputHelper Output; - - /// - /// - /// Initializes a new instance of the class. - /// - /// - /// If no is passed in, a new system with - /// will be created. - /// - /// - /// The actor system to use for testing. The default value is . - /// The provider used to write test output. The default value is . - public TestKit(ActorSystem system = null, ITestOutputHelper output = null) - : base(Assertions, system) - { - Output = output; - InitializeLogger(Sys); - } + private static readonly string Newline = Environment.NewLine; + private readonly ITestOutputHelper _output; + private readonly string _prefix; + private bool _newLine; - /// - /// Initializes a new instance of the class. - /// - /// The to use for configuring the ActorSystem. - /// The name of the system. The default name is "test". - /// The provider used to write test output. The default value is . - public TestKit(ActorSystemSetup config, string actorSystemName = null, ITestOutputHelper output = null) - : base(Assertions, config, actorSystemName) + public PrefixedOutput(ITestOutputHelper output, string prefix) { - Output = output; - InitializeLogger(Sys); + _output = output; + _prefix = prefix; } - /// - /// Initializes a new instance of the class. - /// - /// The configuration to use for the system. - /// The name of the system. The default name is "test". - /// The provider used to write test output. The default value is . - public TestKit(Config config, string actorSystemName = null, ITestOutputHelper output = null) - : base(Assertions, config, actorSystemName) + public void Write(string message) { - Output = output; - InitializeLogger(Sys); + if(_newLine) + message = _prefix + Newline; + + var parts = message.Split([Newline], StringSplitOptions.None); + _output.Write(string.Join($"{Newline}{_prefix}", parts)); + _newLine = message.EndsWith(Newline); } - /// - /// Initializes a new instance of the class. - /// - /// The configuration to use for the system. - /// The provider used to write test output. The default value is . - public TestKit(string config, ITestOutputHelper output = null) - : base(Assertions, ConfigurationFactory.ParseString(config)) + public void Write(string format, params object[] args) { - Output = output; - InitializeLogger(Sys); + if(_newLine) + format = _prefix + format; + var parts = format.Split([Newline], StringSplitOptions.None); + format = string.Join($"{Newline}{_prefix}", parts); + + _output.Write(format, args); + _newLine = format.EndsWith(Newline); } - /// - /// A configuration that has just the default log settings enabled. The default settings can be found in - /// Akka.TestKit.Internal.Reference.conf. - /// - public new static Config DefaultConfig => TestKitBase.DefaultConfig; - - /// - /// A configuration that has all log settings enabled - /// - public new static Config FullDebugConfig => TestKitBase.FullDebugConfig; - - /// - /// Commonly used assertions used throughout the testkit. - /// - protected static XunitAssertions Assertions { get; } = new(); - - /// - /// This method is called when a test ends. - /// - /// - /// If you override this, then make sure you either call base.AfterTest() or - /// TestKitBase.Shutdown - /// to shut down the system. Otherwise a memory leak will occur. - /// - /// - protected virtual void AfterAll() + public void WriteLine(string message) { - Shutdown(); + if (_newLine) + message = _prefix + message; + _newLine = true; + _output.WriteLine(message); } - /// - /// Initializes a new used to log messages. - /// - /// The actor system used to attach the logger - protected void InitializeLogger(ActorSystem system) + public void WriteLine(string format, params object[] args) { - if (Output != null) - { - var extSystem = (ExtendedActorSystem)system; - var logger = extSystem.SystemActorOf(Props.Create(() => new TestOutputLogger(Output)), "log-test"); - logger.Tell(new InitializeLogger(system.EventStream)); - } + if (_newLine) + format = _prefix + format; + _newLine = true; + _output.WriteLine(format, args); } + public string Output => _output.Output; + } + + /// + /// The provider used to write test output. + /// + protected readonly ITestOutputHelper? Output; + + private bool _disposed; + private bool _disposing; - public void Dispose() + /// + /// + /// Initializes a new instance of the class. + /// + /// + /// If no is passed in, a new system with + /// will be created. + /// + /// + /// The actor system to use for testing. The default value is . + /// The provider used to write test output. The default value is . + public TestKit(ActorSystem? system = null, ITestOutputHelper? output = null) + : base(Assertions, system) + { + Output = output; + InitializeLogger(Sys); + } + + /// + /// Initializes a new instance of the class. + /// + /// The to use for configuring the ActorSystem. + /// The name of the system. The default name is "test". + /// The provider used to write test output. The default value is . + public TestKit(ActorSystemSetup config, string? actorSystemName = null, ITestOutputHelper? output = null) + : base(Assertions, config, actorSystemName) + { + Output = output; + InitializeLogger(Sys); + } + + /// + /// Initializes a new instance of the class. + /// + /// The configuration to use for the system. + /// The name of the system. The default name is "test". + /// The provider used to write test output. The default value is . + public TestKit(Config config, string? actorSystemName = null, ITestOutputHelper? output = null) + : base(Assertions, config, actorSystemName) + { + Output = output; + InitializeLogger(Sys); + } + + /// + /// Initializes a new instance of the class. + /// + /// The configuration to use for the system. + /// The provider used to write test output. The default value is . + public TestKit(string config, ITestOutputHelper? output = null) + : base(Assertions, ConfigurationFactory.ParseString(config)) + { + Output = output; + InitializeLogger(Sys); + } + + /// + /// A configuration that has just the default log settings enabled. The default settings can be found in + /// Akka.TestKit.Internal.Reference.conf. + /// + public new static Config DefaultConfig => TestKitBase.DefaultConfig; + + /// + /// A configuration that has all log settings enabled + /// + public new static Config FullDebugConfig => TestKitBase.FullDebugConfig; + + /// + /// Commonly used assertions used throughout the testkit. + /// + protected static XunitAssertions Assertions { get; } = new(); + + /// + /// This method is called when a test ends. + /// + protected virtual void AfterAll() + { + } + + /// + /// Initializes a new used to log messages. + /// + /// The actor system used to attach the logger + protected void InitializeLogger(ActorSystem system) + { + if (Output == null) + return; + + var extSystem = (ExtendedActorSystem)system; + var logger = extSystem.SystemActorOf(Props.Create(() => new TestOutputLogger(Output)), "log-test"); + logger.Ask(new InitializeLogger(system.EventStream), TestKitSettings.TestKitStartupTimeout) + .ConfigureAwait(false).GetAwaiter().GetResult(); + } + + protected void InitializeLogger(ActorSystem system, string prefix) + { + if (Output == null) + return; + + var extSystem = (ExtendedActorSystem)system; + var logger = extSystem.SystemActorOf(Props.Create(() => new TestOutputLogger( + string.IsNullOrEmpty(prefix) ? Output : new PrefixedOutput(Output, prefix))), "log-test"); + logger.Ask(new InitializeLogger(system.EventStream), TestKitSettings.TestKitStartupTimeout) + .ConfigureAwait(false).GetAwaiter().GetResult(); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// + /// if set to true the method has been called directly or indirectly by a user's code. + /// Managed and unmanaged resources will be disposed.
if set to false the method + /// has been called by the runtime from inside the finalizer and only unmanaged resources can + /// be disposed. + /// + protected virtual void Dispose(bool disposing) + { + if (_disposing || _disposed) + return; + + _disposing = true; + try { - Dispose(true); - //Take this object off the finalization queue and prevent finalization code for this object - //from executing a second time. - GC.SuppressFinalize(this); + AfterAll(); } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - /// - /// if set to true the method has been called directly or indirectly by a user's code. - /// Managed and unmanaged resources will be disposed.
if set to false the method - /// has been called by the runtime from inside the finalizer and only unmanaged resources can - /// be disposed. - /// - protected virtual void Dispose(bool disposing) + finally { - // If disposing equals false, the method has been called by the - // runtime from inside the finalizer and you should not reference - // other objects. Only unmanaged resources can be disposed. - - try - { - //Make sure Dispose does not get called more than once, by checking the disposed field - if(!_isDisposed && disposing) - { - AfterAll(); - } - _isDisposed = true; - } - finally - { - } + Shutdown(); + _disposed = true; } } -} + + public void Dispose() + { + Dispose(true); + } +} \ No newline at end of file diff --git a/src/contrib/testkits/Akka.TestKit.Xunit/XunitAssertions.cs b/src/contrib/testkits/Akka.TestKit.Xunit/XunitAssertions.cs index 133dd89a920..f938d1f4f9e 100644 --- a/src/contrib/testkits/Akka.TestKit.Xunit/XunitAssertions.cs +++ b/src/contrib/testkits/Akka.TestKit.Xunit/XunitAssertions.cs @@ -9,81 +9,80 @@ using Akka.TestKit.Xunit.Internals; using Xunit; -namespace Akka.TestKit.Xunit +namespace Akka.TestKit.Xunit; + +/// +/// This class contains several common assert patterns used throughout this testkit. +/// +public class XunitAssertions : ITestKitAssertions { /// - /// This class contains several common assert patterns used throughout this testkit. + /// Fails an assertion without checking any conditions. /// - public class XunitAssertions : ITestKitAssertions + /// A template string to display if the assertion fails. + /// An optional object array that contains zero or more objects to format. + public void Fail(string format = "", params object[] args) { - /// - /// Fails an assertion without checking any conditions. - /// - /// A template string to display if the assertion fails. - /// An optional object array that contains zero or more objects to format. - public void Fail(string format = "", params object[] args) - { - Assert.Fail(string.Format(format, args)); - } + Assert.Fail(AkkaEqualException.BuildAssertionMessage(format, args)); + } - /// - /// Verifies that a specified is true. - /// - /// The condition that is being verified. - /// A template string to display if the assertion fails. - /// An optional object array that contains zero or more objects to format. - public void AssertTrue(bool condition, string format = "", params object[] args) - { - Assert.True(condition, string.Format(format, args)); - } + /// + /// Verifies that a specified is true. + /// + /// The condition that is being verified. + /// A template string to display if the assertion fails. + /// An optional object array that contains zero or more objects to format. + public void AssertTrue(bool condition, string format = "", params object[] args) + { + Assert.True(condition, AkkaEqualException.BuildAssertionMessage(format, args)); + } - /// - /// Verifies that a specified is false. - /// - /// The condition that is being verified. - /// A template string to display if the assertion fails. - /// An optional object array that contains zero or more objects to format. - public void AssertFalse(bool condition, string format = "", params object[] args) - { - Assert.False(condition, string.Format(format, args)); - } + /// + /// Verifies that a specified is false. + /// + /// The condition that is being verified. + /// A template string to display if the assertion fails. + /// An optional object array that contains zero or more objects to format. + public void AssertFalse(bool condition, string format = "", params object[] args) + { + Assert.False(condition, AkkaEqualException.BuildAssertionMessage(format, args)); + } - /// - /// Verifies that the two specified values ( and - /// are equal using the built-in comparison function . - /// - /// The type that is being compared. - /// The expected value of the object - /// The actual value of the object - /// A template string to display if the assertion fails. - /// An optional object array that contains zero or more objects to format. - /// - /// This exception is thrown when the two specified values are not equal. - /// - public void AssertEqual(T expected, T actual, string format = "", params object[] args) - { - var comparer = new AkkaAssertEqualityComparer(); - if(!comparer.Equals(expected, actual)) - throw AkkaEqualException.ForMismatchedValues(expected, actual, format, args); - } + /// + /// Verifies that the two specified values ( and + /// are equal using the built-in comparison function . + /// + /// The type that is being compared. + /// The expected value of the object + /// The actual value of the object + /// A template string to display if the assertion fails. + /// An optional object array that contains zero or more objects to format. + /// + /// This exception is thrown when the two specified values are not equal. + /// + public void AssertEqual(T expected, T actual, string format = "", params object[] args) + { + var comparer = new AkkaAssertEqualityComparer(); + if(!comparer.Equals(expected, actual)) + throw AkkaEqualException.ForMismatchedValues(expected, actual, format, args); + } - /// - /// Verifies that the two specified values ( and - /// are equal using a specified comparison function . - /// - /// The type that is being compared. - /// The expected value of the object - /// The actual value of the object - /// The function used to compare the two specified values. - /// A template string to display if the assertion fails. - /// An optional object array that contains zero or more objects to format. - /// - /// This exception is thrown when the two specified values are not equal. - /// - public void AssertEqual(T expected, T actual, Func comparer, string format = "", params object[] args) - { - if(!comparer(expected, actual)) - throw AkkaEqualException.ForMismatchedValues(expected, actual, format, args); - } + /// + /// Verifies that the two specified values ( and + /// are equal using a specified comparison function . + /// + /// The type that is being compared. + /// The expected value of the object + /// The actual value of the object + /// The function used to compare the two specified values. + /// A template string to display if the assertion fails. + /// An optional object array that contains zero or more objects to format. + /// + /// This exception is thrown when the two specified values are not equal. + /// + public void AssertEqual(T expected, T actual, Func comparer, string format = "", params object[] args) + { + if(!comparer(expected, actual)) + throw AkkaEqualException.ForMismatchedValues(expected, actual, format, args); } -} +} \ No newline at end of file