diff --git a/Akka.sln b/Akka.sln index 4838b6b2d75..46bd08a19e4 100644 --- a/Akka.sln +++ b/Akka.sln @@ -279,6 +279,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.TestKit.Xunit2.Tests", 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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Tests.Shared.Internals.Xunit3", "src\core\Akka.Tests.Shared.Internals.Xunit3\Akka.Tests.Shared.Internals.Xunit3.csproj", "{24DD484D-0F15-4D13-B603-F2E4A901CDBC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1318,6 +1320,18 @@ Global {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 + {24DD484D-0F15-4D13-B603-F2E4A901CDBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {24DD484D-0F15-4D13-B603-F2E4A901CDBC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {24DD484D-0F15-4D13-B603-F2E4A901CDBC}.Debug|x64.ActiveCfg = Debug|Any CPU + {24DD484D-0F15-4D13-B603-F2E4A901CDBC}.Debug|x64.Build.0 = Debug|Any CPU + {24DD484D-0F15-4D13-B603-F2E4A901CDBC}.Debug|x86.ActiveCfg = Debug|Any CPU + {24DD484D-0F15-4D13-B603-F2E4A901CDBC}.Debug|x86.Build.0 = Debug|Any CPU + {24DD484D-0F15-4D13-B603-F2E4A901CDBC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {24DD484D-0F15-4D13-B603-F2E4A901CDBC}.Release|Any CPU.Build.0 = Release|Any CPU + {24DD484D-0F15-4D13-B603-F2E4A901CDBC}.Release|x64.ActiveCfg = Release|Any CPU + {24DD484D-0F15-4D13-B603-F2E4A901CDBC}.Release|x64.Build.0 = Release|Any CPU + {24DD484D-0F15-4D13-B603-F2E4A901CDBC}.Release|x86.ActiveCfg = Release|Any CPU + {24DD484D-0F15-4D13-B603-F2E4A901CDBC}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1441,6 +1455,7 @@ Global {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} + {24DD484D-0F15-4D13-B603-F2E4A901CDBC} = {01167D3C-49C4-4CDE-9787-C176D139ACDD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {03AD8E21-7507-4E68-A4E9-F4A7E7273164} diff --git a/Directory.Build.props b/Directory.Build.props index 838680f9280..0d8329302b7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -37,6 +37,7 @@ 5.10.3 true 2.16.6 + 3.3.2 2.0.3 6.0.1 1.5.40 diff --git a/src/core/Akka.Tests.Shared.Internals.Xunit3/ActorDslExtensions.cs b/src/core/Akka.Tests.Shared.Internals.Xunit3/ActorDslExtensions.cs new file mode 100644 index 00000000000..0f4d1b7d7b0 --- /dev/null +++ b/src/core/Akka.Tests.Shared.Internals.Xunit3/ActorDslExtensions.cs @@ -0,0 +1,21 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Akka.Actor; +using Akka.Actor.Dsl; + +// ReSharper disable once CheckNamespace +namespace Akka.TestKit; + +public static class ActorDslExtensions +{ + public static void Receive(this IActorDsl config, string message, Action handler) + { + config.Receive(m=>string.Equals(m,message,StringComparison.Ordinal), handler); + } +} \ No newline at end of file diff --git a/src/core/Akka.Tests.Shared.Internals.Xunit3/Akka.Tests.Shared.Internals.Xunit3.csproj b/src/core/Akka.Tests.Shared.Internals.Xunit3/Akka.Tests.Shared.Internals.Xunit3.csproj new file mode 100644 index 00000000000..8cd8d03b5b9 --- /dev/null +++ b/src/core/Akka.Tests.Shared.Internals.Xunit3/Akka.Tests.Shared.Internals.Xunit3.csproj @@ -0,0 +1,20 @@ + + + $(NetStandardLibVersion) + false + + + + + + + + + + + + + + + + diff --git a/src/core/Akka.Tests.Shared.Internals.Xunit3/AkkaSpec.cs b/src/core/Akka.Tests.Shared.Internals.Xunit3/AkkaSpec.cs new file mode 100644 index 00000000000..99c7e7fa58d --- /dev/null +++ b/src/core/Akka.Tests.Shared.Internals.Xunit3/AkkaSpec.cs @@ -0,0 +1,351 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Actor.Setup; +using Akka.Configuration; +using Akka.TestKit.Internal.StringMatcher; +using Akka.TestKit.TestEvent; +using Akka.Util.Internal; +using Xunit; +using Xunit.Sdk; + +// ReSharper disable once CheckNamespace +namespace Akka.TestKit; + +public abstract class AkkaSpec : Xunit.TestKit //AkkaSpec is not part of TestKit +{ + private static Regex _nameReplaceRegex = new("[^a-zA-Z0-9]", RegexOptions.Compiled); + private static readonly Config _akkaSpecConfig = ConfigurationFactory.ParseString( + """ + akka { + loglevel = WARNING + stdout-loglevel = WARNING + serialize-messages = on + actor { + ask-timeout = 20s + } + } + # use random ports to avoid race conditions with binding contention + akka.remote.dot-netty.tcp.port = 0 + """); + + private static int _systemNumber = 0; + + private static ActorSystemSetup FromActorSystemSetup(ActorSystemSetup setup) + { + var bootstrapOptions = setup.Get(); + var bootstrap = bootstrapOptions.HasValue ? bootstrapOptions.Value : BootstrapSetup.Create(); + bootstrap = bootstrap.WithConfigFallback(_akkaSpecConfig); + return setup.And(bootstrap); + } + + public AkkaSpec(string config, ITestOutputHelper output = null) + : this(ConfigurationFactory.ParseString(config).WithFallback(_akkaSpecConfig), output) + { + } + + public AkkaSpec(Config config = null, ITestOutputHelper output = null) + : base(config.SafeWithFallback(_akkaSpecConfig), GetCallerName(), output) + { + BeforeAll(); + } + + public AkkaSpec(ActorSystemSetup setup, ITestOutputHelper output = null) + : base(FromActorSystemSetup(setup), GetCallerName(), output) + { + BeforeAll(); + } + + public AkkaSpec(ITestOutputHelper output, Config config = null) + : base(config.SafeWithFallback(_akkaSpecConfig), GetCallerName(), output) + { + BeforeAll(); + } + + public AkkaSpec(ActorSystem system, ITestOutputHelper output = null) + : base(system, output) + { + BeforeAll(); + } + + private void BeforeAll() + { + GC.Collect(); + AtStartup(); + } + + protected override void AfterAll() + { + BeforeTermination(); + base.AfterAll(); + AfterTermination(); + } + + protected virtual void AtStartup() { } + + protected virtual void BeforeTermination() + { + } + + protected virtual void AfterTermination() + { + } + + private static string GetCallerName() + { + var systemNumber = Interlocked.Increment(ref _systemNumber); + var stackTrace = new StackTrace(0); + var name = stackTrace.GetFrames()? + .Select(f => f.GetMethod()) + .Where(m => m.DeclaringType != null) + .SkipWhile(m => m.DeclaringType.Name == "AkkaSpec") + .Select(m => _nameReplaceRegex.Replace(m.DeclaringType.Name + "-" + systemNumber, "-")) + .FirstOrDefault() ?? "test"; + + return name; + } + + public static Config AkkaSpecConfig { get { return _akkaSpecConfig; } } + + protected T ExpectMsgOf( + TimeSpan? timeout, + string hint, + Func function, + CancellationToken cancellationToken = default) + => ExpectMsgOfAsync(timeout, hint, this, function, cancellationToken) + .ConfigureAwait(false).GetAwaiter().GetResult(); + + protected async Task ExpectMsgOfAsync( + TimeSpan? timeout, + string hint, + Func function, + CancellationToken cancellationToken = default) + => await ExpectMsgOfAsync(timeout, hint, this, function, cancellationToken) + .ConfigureAwait(false); + + protected async Task ExpectMsgOfAsync( + TimeSpan? timeout, + string hint, + Func> function, + CancellationToken cancellationToken = default) + => await ExpectMsgOfAsync(timeout, hint, this, function, cancellationToken) + .ConfigureAwait(false); + + protected T ExpectMsgOf( + TimeSpan? timeout, + string hint, + TestKitBase probe, + Func function, + CancellationToken cancellationToken = default) + => ExpectMsgOfAsync(timeout, hint, probe, function, cancellationToken) + .ConfigureAwait(false).GetAwaiter().GetResult(); + + protected async Task ExpectMsgOfAsync( + TimeSpan? timeout, + string hint, + TestKitBase probe, + Func function, + CancellationToken cancellationToken = default) + => await ExpectMsgOfAsync(timeout, hint, probe, o => Task.FromResult(function(o)), cancellationToken) + .ConfigureAwait(false); + + protected async Task ExpectMsgOfAsync( + TimeSpan? timeout, + string hint, + TestKitBase probe, + Func> function, + CancellationToken cancellationToken = default) + { + var (success, envelope) = await probe.TryReceiveOneAsync(timeout, cancellationToken) + .ConfigureAwait(false); + + if(!success) + Assertions.Fail($"expected message of type {typeof(T)} but timed out after {GetTimeoutOrDefault(timeout)}"); + + var message = envelope.Message; + Assertions.AssertTrue(message != null, $"expected {hint} but got null message"); + //TODO: Check next line. + Assertions.AssertTrue( + function.GetMethodInfo().GetParameters().Any(x => x.ParameterType.IsInstanceOfType(message)), + $"expected {hint} but got {message} instead"); + + return await function(message).ConfigureAwait(false); + } + + protected T ExpectMsgOf( + string hint, + TestKitBase probe, + Func pf, + CancellationToken cancellationToken = default) + => ExpectMsgOfAsync(hint, probe, pf, cancellationToken) + .ConfigureAwait(false).GetAwaiter().GetResult(); + + protected async Task ExpectMsgOfAsync( + string hint, + TestKitBase probe, + Func pf, + CancellationToken cancellationToken = default) + => await ExpectMsgOfAsync(hint, probe, o => Task.FromResult(pf(o)), cancellationToken) + .ConfigureAwait(false); + + protected async Task ExpectMsgOfAsync( + string hint, + TestKitBase probe, + Func> pf, + CancellationToken cancellationToken = default) + { + var t = await probe.ExpectMsgAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + //TODO: Check if this really is needed: + Assertions.AssertTrue(pf.GetMethodInfo().GetParameters().Any(x => x.ParameterType.IsInstanceOfType(t)), + $"expected {hint} but got {t} instead"); + return await pf(t); + } + + protected T ExpectMsgOf( + string hint, + Func pf, + CancellationToken cancellationToken = default) + => ExpectMsgOfAsync(hint, this, pf, cancellationToken) + .ConfigureAwait(false).GetAwaiter().GetResult(); + + protected async Task ExpectMsgOfAsync( + string hint, + Func pf, + CancellationToken cancellationToken = default) + => await ExpectMsgOfAsync(hint, this, pf, cancellationToken) + .ConfigureAwait(false); + + protected async Task ExpectMsgOfAsync( + string hint, + Func> pf, + CancellationToken cancellationToken = default) + => await ExpectMsgOfAsync(hint, this, pf, cancellationToken) + .ConfigureAwait(false); + + [Obsolete("Method name typo, please use ExpectMsgOf instead")] + protected T ExpectMsgPf(TimeSpan? timeout, string hint, Func function) + => ExpectMsgOf(timeout, hint, this, function); + + [Obsolete("Method name typo, please use ExpectMsgOf instead")] + protected T ExpectMsgPf(TimeSpan? timeout, string hint, TestKitBase probe, Func function) + => ExpectMsgOf(timeout, hint, probe, function); + + [Obsolete("Method name typo, please use ExpectMsgOf instead")] + protected T ExpectMsgPf(string hint, Func pf) + => ExpectMsgOf(hint, this, pf); + + [Obsolete("Method name typo, please use ExpectMsgOf instead")] + protected T ExpectMsgPf(string hint, TestKitBase probe, Func pf) + => ExpectMsgOf(hint, probe, pf); + + /// + /// Intercept and return an exception that's expected to be thrown by the passed function value. The thrown + /// exception must be an instance of the type specified by the type parameter of this method. This method + /// invokes the passed function. If the function throws an exception that's an instance of the specified type, + /// this method returns that exception. Else, whether the passed function returns normally or completes abruptly + /// with a different exception, this method throws . + /// + /// Also note that the difference between this method and is that this method + /// returns the expected exception, so it lets you perform further assertions on that exception. By contrast, + /// the indicates to the reader of the code that nothing further is expected + /// about the thrown exception other than its type. The recommended usage is to use + /// by default, intercept only when you need to inspect the caught exception further. + /// + /// + /// The action that should throw the expected exception + /// The intercepted exception, if it is of the expected type + /// If the passed action does not complete abruptly with an exception that's an instance of the specified type. + protected T Intercept(Action actionThatThrows) where T : Exception + { + return Assertions.AssertThrows(actionThatThrows); + } + + protected Task InterceptAsync(Func funcThatThrows) where T : Exception + { + return Assertions.AssertThrowsAsync(funcThatThrows); + } + + /// + /// Ensure that an expected exception is thrown by the passed function value. The thrown exception must be an + /// instance of the type specified by the type parameter of this method. This method invokes the passed + /// function. If the function throws an exception that's an instance of the specified type, this method returns + /// void. Else, whether the passed function returns normally or completes abruptly with a different + /// exception, this method throws . + /// + /// Also note that the difference between this method and is that this method + /// does not return the expected exception, so it does not let you perform further assertions on that exception. + /// It also indicates to the reader of the code that nothing further is expected about the thrown exception + /// other than its type. The recommended usage is to use by default, + /// only when you need to inspect the caught exception further. + /// + /// + /// The action that should throw the expected exception + /// If the passed action does not complete abruptly with an exception that's an instance of the specified type. + protected void AssertThrows(Action actionThatThrows) where T : Exception + { + Intercept(actionThatThrows); + } + + protected async Task AssertThrowsAsync(Func funcThatThrows, Exception expected) + { + var exception = await Assertions.AssertThrowsAsync(funcThatThrows); + + // NOTE: + // This is how FluentAssertions do exception equality, more or less + // It unwraps AggregateException and checks to see if any of the internal exceptions + // matches the expected exception. + // Without this code, some unit tests will fail. + if (exception is AggregateException ae) + { + exception = ae.InnerExceptions[0]; + } + Assertions.AssertEqual(expected, exception); + } + + protected Task AssertThrowsAsync(Func funcThatThrows) where T : Exception + { + return Assertions.AssertThrowsAsync(funcThatThrows); + } + + protected Task AssertThrowsAsync(Func funcThatThrows) + { + return Assertions.AssertThrowsAsync(funcThatThrows); + } + + protected void MuteDeadLetters(params Type[] messageClasses) + => MuteDeadLetters(Sys, messageClasses); + + protected void MuteDeadLetters(ActorSystem sys, params Type[] messageClasses) + { + if (!sys.Log.IsDebugEnabled) + return; + + Action mute = + clazz => + sys.EventStream.Publish( + new Mute(new DeadLettersFilter(new PredicateMatcher(_ => true), + new PredicateMatcher(_ => true), + letter => clazz == typeof(object) || letter.Message.GetType() == clazz))); + + if (messageClasses.Length == 0) + mute(typeof(object)); + else + messageClasses.ForEach(mute); + } +} + +// ReSharper disable once InconsistentNaming \ No newline at end of file diff --git a/src/core/Akka.Tests.Shared.Internals.Xunit3/AkkaSpecExtensions.cs b/src/core/Akka.Tests.Shared.Internals.Xunit3/AkkaSpecExtensions.cs new file mode 100644 index 00000000000..1f17c91dbb8 --- /dev/null +++ b/src/core/Akka.Tests.Shared.Internals.Xunit3/AkkaSpecExtensions.cs @@ -0,0 +1,294 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Akka.TestKit.Xunit.Internals; +using Xunit; + +// ReSharper disable once CheckNamespace +namespace Akka.TestKit; + +/// +/// TBD +/// +public static class AkkaSpecExtensions +{ + /// + /// TBD + /// + /// TBD + /// TBD + /// TBD + /// TBD + public static void Should(this T self, Func isValid, string message) + { + Assert.True(isValid(self), message ?? "Value did not meet criteria. Value: " + self); + } + + /// + /// TBD + /// + /// TBD + /// TBD + /// TBD + public static void ShouldHaveCount(this IReadOnlyCollection self, int expectedCount) + { + Assert.Equal(expectedCount, self.Count); + } + + /// + /// TBD + /// + /// TBD + /// TBD + /// TBD + public static void ShouldBe(this IEnumerable self, IEnumerable other) + { + var otherList = other.ToList(); + var selfList = self.ToList(); + var expected = string.Join(",", otherList.Select(i => $"'{i}'")); + var actual = string.Join(",", selfList.Select(i => $"'{i}'")); + + Assert.True(selfList.SequenceEqual(otherList), "Expected " + expected + " got " + actual); + } + + public static async Task ShouldBeAsync(this IAsyncEnumerable self, IEnumerable other) + { + if (self is null) + throw new ArgumentNullException(nameof(self)); + if (other is null) + throw new ArgumentNullException(nameof(other)); + + var l1 = new List(); + var l2 = new List(); + var index = 0; + + await using var e1 = self.GetAsyncEnumerator(); + using var e2 = other.GetEnumerator(); + + var comparer = EqualityComparer.Default; + while (await e1.MoveNextAsync()) + { + l1.Add($"'{e1.Current}'"); + if (!e2.MoveNext()) + throw AkkaEqualException.ForMismatchedValues( + l2, l1, $"Input has more elements than expected, differ at index {index}"); + + l2.Add($"'{e2.Current}'"); + if(!comparer.Equals(e1.Current, e2.Current)) + throw AkkaEqualException.ForMismatchedValues( + l2, l1, $"Input is not equal to expected, differ at index {index}"); + + index++; + } + + if (e2.MoveNext()) + { + l2.Add($"'{e2.Current}'"); + throw AkkaEqualException.ForMismatchedValues( + l2, l1, $"Input has less elements than expected, differ at index {index}"); + } + } + + /// + /// TBD + /// + /// TBD + /// TBD + /// TBD + /// TBD + public static void ShouldBe(this T self, T expected, string message = null) + { + Assert.Equal(expected, self); + } + + /// + /// TBD + /// + /// TBD + /// TBD + /// TBD + /// TBD + public static async Task ShouldBeAsync(this ValueTask self, T expected, string message = null) + { + Assert.Equal(expected, await self); + } + + /// + /// TBD + /// + /// TBD + /// TBD + /// TBD + /// TBD + public static void ShouldNotBe(this T self, T expected, string message = null) + { + Assert.NotEqual(expected, self); + } + + /// + /// TBD + /// + /// TBD + /// TBD + /// TBD + /// TBD + public static void ShouldBeSame(this T self, T expected, string message = null) + { + Assert.Equal(expected, self); + } + + /// + /// TBD + /// + /// TBD + /// TBD + /// TBD + /// TBD + public static void ShouldNotBeSame(this T self, T expected, string message = null) + { + Assert.NotEqual(expected, self); + } + + /// + /// TBD + /// + /// TBD + /// TBD + public static void ShouldBeTrue(this bool b, string message = null) + { + Assert.True(b, message); + } + + /// + /// TBD + /// + /// TBD + /// TBD + public static void ShouldBeFalse(this bool b, string message = null) + { + Assert.False(b, message); + } + + /// + /// TBD + /// + /// TBD + /// TBD + /// TBD + /// TBD + public static void ShouldBeLessThan(this T actual, T value, string message = null) where T : IComparable + { + var comparisonResult = actual.CompareTo(value); + Assert.True(comparisonResult < 0, message ?? "Expected Actual: " + actual + " to be less than " + value); + } + + /// + /// TBD + /// + /// TBD + /// TBD + /// TBD + /// TBD + public static void ShouldBeLessOrEqualTo(this T actual, T value, string message = null) where T : IComparable + { + var comparisonResult = actual.CompareTo(value); + Assert.True(comparisonResult <= 0, message ?? "Expected Actual: " + actual + " to be less than " + value); + } + + /// + /// TBD + /// + /// TBD + /// TBD + /// TBD + /// TBD + public static void ShouldBeGreaterThan(this T actual, T value, string message = null) where T : IComparable + { + var comparisonResult = actual.CompareTo(value); + Assert.True(comparisonResult > 0, message ?? "Expected Actual: " + actual + " to be less than " + value); + } + + /// + /// TBD + /// + /// TBD + /// TBD + /// TBD + /// TBD + public static void ShouldBeGreaterOrEqual(this T actual, T value, string message = null) where T : IComparable + { + var comparisonResult = actual.CompareTo(value); + Assert.True(comparisonResult >= 0, message ?? "Expected Actual: " + actual + " to be less than " + value); + } + + /// + /// TBD + /// + /// TBD + /// TBD + /// TBD + public static void ShouldStartWith(this string s, string start, string message = null) + { + Assert.Equal(s.Substring(0, Math.Min(s.Length, start.Length)), start); + } + + /// + /// TBD + /// + /// TBD + /// TBD + /// TBD + public static void ShouldOnlyContainInOrder(this IEnumerable actual, params T[] expected) + { + ShouldBe(actual, expected); + } + + /// + /// TBD + /// + /// TBD + /// TBD + /// TBD + public static async Task ShouldOnlyContainInOrderAsync(this IAsyncEnumerable actual, params T[] expected) + => await ShouldBeAsync(actual, expected).ConfigureAwait(false); + + /// + /// TBD + /// + /// TBD + /// TBD + /// TBD + public static void ShouldOnlyContainInOrder(this IEnumerable actual, IEnumerable expected) + { + ShouldBe(actual, expected); + } + + /// + /// TBD + /// + /// TBD + /// TBD + public static async Task ThrowsAsync(Func func) + { + var expected = typeof(TException); + Type actual = null; + try + { + await func(); + } + catch (Exception e) + { + actual = e.GetType(); + } + + Assert.Equal(expected, actual); + } +} \ No newline at end of file diff --git a/src/core/Akka.Tests.Shared.Internals.Xunit3/AskExtensions.cs b/src/core/Akka.Tests.Shared.Internals.Xunit3/AskExtensions.cs new file mode 100644 index 00000000000..7eceaf4a665 --- /dev/null +++ b/src/core/Akka.Tests.Shared.Internals.Xunit3/AskExtensions.cs @@ -0,0 +1,22 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Akka.Actor; + +// ReSharper disable once CheckNamespace +namespace Akka.TestKit; + +public static class AskExtensions +{ + public static TAnswer AskAndWait(this ICanTell self, object message, TimeSpan timeout) + { + var task = self.Ask(message,timeout); + task.Wait(); + return task.Result; + } +} \ No newline at end of file diff --git a/src/core/Akka.Tests.Shared.Internals.Xunit3/Helpers/FSharpDelegateHelper.cs b/src/core/Akka.Tests.Shared.Internals.Xunit3/Helpers/FSharpDelegateHelper.cs new file mode 100644 index 00000000000..e768da291f0 --- /dev/null +++ b/src/core/Akka.Tests.Shared.Internals.Xunit3/Helpers/FSharpDelegateHelper.cs @@ -0,0 +1,46 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Microsoft.FSharp.Core; + +// ReSharper disable once CheckNamespace +namespace Akka.Tests.Shared.Internals.Helpers; + +/// +/// Maps F# methods to C# delegates +/// +public static class FsharpDelegateHelper +{ + public static FSharpFunc Create(Func func) + { + return FSharpFunc.FromConverter(Conv); + + TResult Conv(T2 input) => func(input); + } + + public static FSharpFunc> Create(Func func) + { + return FSharpFunc>.FromConverter(Conv); + + FSharpFunc Conv(T1 value1) + { + return Create(value2 => func(value1, value2)); + } + } + + public static FSharpFunc>> Create( + Func func) + { + return FSharpFunc>>.FromConverter(Conv); + + FSharpFunc> Conv(T1 value1) + { + return Create((value2, value3) => func(value1, value2, value3)); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.Tests.Shared.Internals.Xunit3/Helpers/XAssert.cs b/src/core/Akka.Tests.Shared.Internals.Xunit3/Helpers/XAssert.cs new file mode 100644 index 00000000000..275c97c20f9 --- /dev/null +++ b/src/core/Akka.Tests.Shared.Internals.Xunit3/Helpers/XAssert.cs @@ -0,0 +1,80 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using Akka.TestKit.Xunit; +using Xunit; + +// ReSharper disable once CheckNamespace +namespace Akka.TestKit; + +public static class XAssert +{ + private static readonly XunitAssertions _assertions = new(); + /// + /// Fails the test with the specified reason. + /// + public static void Fail(string reason) + { + Assert.Fail(reason); + } + + /// + /// Asserts that both arguments are the same reference. + /// + public static void Same(T expected, T actual, string message) + { + Assert.True(ReferenceEquals(expected,actual),message); + } + + public static void Equal(T expected, T actual, string format, params object[] args) + { + _assertions.AssertEqual(expected,actual,format,args); + } + + public static T Throws(Action action) where T : Exception + { + Exception exception = null; + try + { + action(); + } + catch(AggregateException ex) //need to flatten AggregateExceptions + { + var any = ex.Flatten().InnerExceptions.FirstOrDefault(x => x is T); + if(any!=null) return (T) any; + exception = ex; + } + catch(Exception ex) + { + if(ex is T exception1) + { + return exception1; + } + exception = ex; + } + if(exception != null) + Fail("Expected exception of type " + typeof(T).FullName + ". Received " + exception); + else + Fail("Expected exception of type " + typeof(T).Name + " but no exceptions was thrown."); + return null; //We'll never reach this line, since calling Fail will throw an exception. + } + + /// + /// Assert passes if two sequences are equal, regardless of the ordering of the items. + /// + /// Equivalent of http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.testtools.unittesting.collectionassert.areequivalent.aspx + /// + public static void Equivalent(IEnumerable expected, IEnumerable actual) + { + var e = expected.ToArray(); + var a = actual.ToArray(); + Assert.True(e.All(x => a.Contains(x)) && a.All(y => e.Contains(y))); + } +} \ No newline at end of file diff --git a/src/core/Akka.Tests.Shared.Internals.Xunit3/README.md b/src/core/Akka.Tests.Shared.Internals.Xunit3/README.md new file mode 100644 index 00000000000..97a22307eed --- /dev/null +++ b/src/core/Akka.Tests.Shared.Internals.Xunit3/README.md @@ -0,0 +1,4 @@ +This assembly contains code used internally for writing tests. +This is not part of the public package. + +The code comes mainly from Akka JVM `/akka-testkit/src/test/scala/akka/testkit/` directory. \ No newline at end of file diff --git a/src/core/Akka.Tests.Shared.Internals.Xunit3/RepeatAttribute.cs b/src/core/Akka.Tests.Shared.Internals.Xunit3/RepeatAttribute.cs new file mode 100644 index 00000000000..401c92c5ff7 --- /dev/null +++ b/src/core/Akka.Tests.Shared.Internals.Xunit3/RepeatAttribute.cs @@ -0,0 +1,70 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; +using Xunit; +using Xunit.Sdk; +using Xunit.v3; + +// ReSharper disable once CheckNamespace +namespace Akka.Tests.Shared.Internals; + +/// +/// This is an internal utility to test flaky/racy unit tests. +/// It allows the test runner to run a single unit test repeatedly to test for flaky situations. +/// +/// NOTE: +/// Make sure that this attribute are _NOT_ used in the unit test when it is ready to be committed, +/// because it creates artificial load that can bind the CI/CD PR validation process. +/// +/// +/// // This will repeatedly run MyUnitTest 500 times +/// // Note that you NEED to use [Theory], and the unit test requires a single integer parameter. +/// [Theory] +/// [Repeat(500)] +/// public void MyUnitTest(int _) +/// { } +/// +public sealed class RepeatAttribute : DataAttribute +{ + private readonly int _count; + + public RepeatAttribute(int count) + { + if (count < 1) + { + throw new ArgumentOutOfRangeException(nameof(count), + "Repeat count must be greater than 0."); + } + _count = count; + } + + public override ValueTask> GetData(MethodInfo testMethod, DisposalTracker disposalTracker) + { + var rows = new List(_count); + + for (var i = 1; i <= _count; i++) + rows.Add(new RepeatTheoryDataRow(i)); + + return new ValueTask>(rows); } + + public override bool SupportsDiscoveryEnumeration() => true; +} + +public class RepeatTheoryDataRow(int count) : TheoryDataRowBase +{ + /// + /// Gets the row of data. + /// + public object?[] Data => [count]; + + /// + protected override object?[] GetData() => [count]; +} diff --git a/src/core/Akka.Tests.Shared.Internals.Xunit3/TestReceiveActor.cs b/src/core/Akka.Tests.Shared.Internals.Xunit3/TestReceiveActor.cs new file mode 100644 index 00000000000..b017f513fd2 --- /dev/null +++ b/src/core/Akka.Tests.Shared.Internals.Xunit3/TestReceiveActor.cs @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Akka.Actor; + +// ReSharper disable once CheckNamespace +namespace Akka.TestKit; + +/// +/// Just like . Adds a Receive-overload that allows you to write code like: +/// Receive("the message", m => ... ); +/// +public class TestReceiveActor : ReceiveActor +{ + public void Receive(T value, Action handler) where T : IEquatable + { + Receive(m => Equals(value, m), handler); + } +} \ No newline at end of file