diff --git a/src/Akka.sln b/src/Akka.sln index 1a5c4253abc..38eef547487 100644 --- a/src/Akka.sln +++ b/src/Akka.sln @@ -288,6 +288,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClusterToolsExample.Seed", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClusterToolsExample.Node", "examples\Cluster\ClusterTools\ClusterToolsExample.Node\ClusterToolsExample.Node.csproj", "{337A85B5-4A7C-4883-8634-46E7E52A765F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.TestKit.Xunit2.Tests", "contrib\testkits\Akka.TestKit.Xunit2.Tests\Akka.TestKit.Xunit2.Tests.csproj", "{95017C99-E960-44E5-83AD-BF21461DF06F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1343,6 +1345,18 @@ Global {337A85B5-4A7C-4883-8634-46E7E52A765F}.Release|x64.Build.0 = Release|Any CPU {337A85B5-4A7C-4883-8634-46E7E52A765F}.Release|x86.ActiveCfg = Release|Any CPU {337A85B5-4A7C-4883-8634-46E7E52A765F}.Release|x86.Build.0 = Release|Any CPU + {95017C99-E960-44E5-83AD-BF21461DF06F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {95017C99-E960-44E5-83AD-BF21461DF06F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {95017C99-E960-44E5-83AD-BF21461DF06F}.Debug|x64.ActiveCfg = Debug|Any CPU + {95017C99-E960-44E5-83AD-BF21461DF06F}.Debug|x64.Build.0 = Debug|Any CPU + {95017C99-E960-44E5-83AD-BF21461DF06F}.Debug|x86.ActiveCfg = Debug|Any CPU + {95017C99-E960-44E5-83AD-BF21461DF06F}.Debug|x86.Build.0 = Debug|Any CPU + {95017C99-E960-44E5-83AD-BF21461DF06F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {95017C99-E960-44E5-83AD-BF21461DF06F}.Release|Any CPU.Build.0 = Release|Any CPU + {95017C99-E960-44E5-83AD-BF21461DF06F}.Release|x64.ActiveCfg = Release|Any CPU + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1469,6 +1483,7 @@ Global {88D7D845-2F50-4D37-9026-B0A8353D0E8D} = {7735F35A-E7B7-44DE-B6FB-C770B53EB69C} {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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {03AD8E21-7507-4E68-A4E9-F4A7E7273164} diff --git a/src/contrib/testkits/Akka.TestKit.Xunit2.Tests/Akka.TestKit.Xunit2.Tests.csproj b/src/contrib/testkits/Akka.TestKit.Xunit2.Tests/Akka.TestKit.Xunit2.Tests.csproj new file mode 100644 index 00000000000..49f9906bac1 --- /dev/null +++ b/src/contrib/testkits/Akka.TestKit.Xunit2.Tests/Akka.TestKit.Xunit2.Tests.csproj @@ -0,0 +1,16 @@ + + + + + + $(NetFrameworkTestVersion);$(NetTestVersion) + + + + + + + + + + diff --git a/src/contrib/testkits/Akka.TestKit.Xunit2.Tests/Internals/AkkaEqualExceptionSpec.cs b/src/contrib/testkits/Akka.TestKit.Xunit2.Tests/Internals/AkkaEqualExceptionSpec.cs new file mode 100644 index 00000000000..a9f1b4b54ea --- /dev/null +++ b/src/contrib/testkits/Akka.TestKit.Xunit2.Tests/Internals/AkkaEqualExceptionSpec.cs @@ -0,0 +1,33 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2025 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +// ----------------------------------------------------------------------- + +using Akka.TestKit.Xunit2.Internals; +using Xunit; + +namespace Akka.TestKit.Xunit2.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.Xunit2.Tests/XunitAssertionsSpec.cs b/src/contrib/testkits/Akka.TestKit.Xunit2.Tests/XunitAssertionsSpec.cs new file mode 100644 index 00000000000..f96f64d6e43 --- /dev/null +++ b/src/contrib/testkits/Akka.TestKit.Xunit2.Tests/XunitAssertionsSpec.cs @@ -0,0 +1,81 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2025 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +// ----------------------------------------------------------------------- + +using Xunit; +using Xunit.Sdk; + +namespace Akka.TestKit.Xunit2.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.Xunit2/Internals/AkkaEqualException.cs b/src/contrib/testkits/Akka.TestKit.Xunit2/Internals/AkkaEqualException.cs index a545984b85c..64a69a22929 100644 --- a/src/contrib/testkits/Akka.TestKit.Xunit2/Internals/AkkaEqualException.cs +++ b/src/contrib/testkits/Akka.TestKit.Xunit2/Internals/AkkaEqualException.cs @@ -15,14 +15,12 @@ namespace Akka.TestKit.Xunit2.Internals /// /// TBD /// + [Serializable] public class AkkaEqualException : XunitException { // Length of "Expected: " and "Actual: " private static readonly string NewLineAndIndent = Environment.NewLine + new string(' ', 10); - private readonly string? _format; - private readonly object[] _args = Array.Empty(); - public static AkkaEqualException ForMismatchedValues( object? expected, object? actual, @@ -46,48 +44,46 @@ public static AkkaEqualException ForMismatchedValues( 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) - { - _format = format; - _args = args; - } + public AkkaEqualException(string format = "", params object[] args) + : base(BuildAssertionMessage(format, args)) { } /// /// 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) - { - } + protected AkkaEqualException(SerializationInfo info, StreamingContext context) + : base(info.GetString("Message")) { } /// - /// The message that describes the error. + /// Builds assertion message by applying specified arguments to the format string. + /// When no arguments are specified, format string is returned as-is. /// - public override string Message + internal static string? BuildAssertionMessage(string format, object[] args) { - get + if (string.IsNullOrEmpty(format)) { - if(string.IsNullOrEmpty(_format)) - return base.Message; + return null; + } - string message; - try - { - message = string.Format(_format!, _args); - } - catch(Exception) - { - message = $@"[Could not string.Format(""{_format}"", {string.Join(", ", _args)})]"; - } + if (args is not { Length: > 0 }) + { + return format; + } - return base.Message is not null ? $"{base.Message} {message}" : message; + try + { + return string.Format(format, args); + } + catch (Exception) + { + return $"""[Could not string.Format("{format}", {string.Join(", ", args)})]"""; } } } diff --git a/src/contrib/testkits/Akka.TestKit.Xunit2/XunitAssertions.cs b/src/contrib/testkits/Akka.TestKit.Xunit2/XunitAssertions.cs index d787cfbac7f..e12aeddf550 100644 --- a/src/contrib/testkits/Akka.TestKit.Xunit2/XunitAssertions.cs +++ b/src/contrib/testkits/Akka.TestKit.Xunit2/XunitAssertions.cs @@ -23,7 +23,7 @@ public class XunitAssertions : ITestKitAssertions /// 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)); } /// @@ -34,7 +34,7 @@ public void Fail(string format = "", params object[] args) /// 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)); + Assert.True(condition, AkkaEqualException.BuildAssertionMessage(format, args)); } /// @@ -45,7 +45,7 @@ public void AssertTrue(bool condition, string format = "", params object[] args) /// 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)); + Assert.False(condition, AkkaEqualException.BuildAssertionMessage(format, args)); } /// diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveTestKitXunit2.DotNet.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveTestKitXunit2.DotNet.verified.txt index 182f5e1bbf8..df82f81663c 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveTestKitXunit2.DotNet.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveTestKitXunit2.DotNet.verified.txt @@ -38,7 +38,6 @@ namespace Akka.TestKit.Xunit2.Internals { public AkkaEqualException(string format = "", params object[] args) { } protected AkkaEqualException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } - public override string Message { get; } [return: System.Runtime.CompilerServices.NullableAttribute(1)] public static Akka.TestKit.Xunit2.Internals.AkkaEqualException ForMismatchedValues(object expected, object actual, string format = null, [System.Runtime.CompilerServices.NullableAttribute(1)] params object[] args) { } } diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveTestKitXunit2.Net.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveTestKitXunit2.Net.verified.txt index c18b2d0c5fc..881e30f215f 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveTestKitXunit2.Net.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveTestKitXunit2.Net.verified.txt @@ -38,7 +38,6 @@ namespace Akka.TestKit.Xunit2.Internals { public AkkaEqualException(string format = "", params object[] args) { } protected AkkaEqualException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } - public override string Message { get; } [return: System.Runtime.CompilerServices.NullableAttribute(1)] public static Akka.TestKit.Xunit2.Internals.AkkaEqualException ForMismatchedValues(object expected, object actual, string format = null, [System.Runtime.CompilerServices.NullableAttribute(1)] params object[] args) { } } diff --git a/src/core/Akka.Persistence.TestKit.Tests/JournalInterceptorsSpecs.cs b/src/core/Akka.Persistence.TestKit.Tests/JournalInterceptorsSpecs.cs index f7638dad520..1d81c25da4d 100644 --- a/src/core/Akka.Persistence.TestKit.Tests/JournalInterceptorsSpecs.cs +++ b/src/core/Akka.Persistence.TestKit.Tests/JournalInterceptorsSpecs.cs @@ -73,7 +73,7 @@ public async Task cancelable_delay_must_call_next_interceptor_immediately_after_ var startedAt = DateTime.Now; var task = delay.InterceptAsync(null); - await Task.Delay(delayDuration - epsilon); + await Task.Delay(delayDuration); probe.WasCalled.Should().BeFalse(); cts.Cancel(); diff --git a/src/core/Akka.Persistence.TestKit.Tests/SnapshotStoreInterceptorsSpec.cs b/src/core/Akka.Persistence.TestKit.Tests/SnapshotStoreInterceptorsSpec.cs index 50820959094..5f22ac6b7f2 100644 --- a/src/core/Akka.Persistence.TestKit.Tests/SnapshotStoreInterceptorsSpec.cs +++ b/src/core/Akka.Persistence.TestKit.Tests/SnapshotStoreInterceptorsSpec.cs @@ -63,7 +63,7 @@ public async Task cancelable_delay_must_call_next_interceptor_immediately_after_ var startedAt = DateTime.Now; var task = delay.InterceptAsync(null, null); - await Task.Delay(delayDuration - epsilon); + await Task.Delay(delayDuration); probe.WasCalled.Should().BeFalse(); cts.Cancel();