diff --git a/Akka.Hosting.sln b/Akka.Hosting.sln index ce8668b3..28ff68a4 100644 --- a/Akka.Hosting.sln +++ b/Akka.Hosting.sln @@ -41,6 +41,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Persistence.Hosting.Te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Hosting.LoggingDemo", "src\Examples\Akka.Hosting.LoggingDemo\Akka.Hosting.LoggingDemo.csproj", "{4F79325B-9EE7-4501-800F-7A1F8DFBCC80}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Hosting.TestKit", "src\Akka.Hosting.TestKit\Akka.Hosting.TestKit.csproj", "{E28D4F3C-6C34-497B-BDC8-F2B3EA8BA309}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Hosting.TestKit.Tests", "src\Akka.Hosting.TestKit.Tests\Akka.Hosting.TestKit.Tests.csproj", "{3883AD08-B981-4943-8153-1E7FFD2C3127}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -105,6 +109,14 @@ Global {4F79325B-9EE7-4501-800F-7A1F8DFBCC80}.Debug|Any CPU.Build.0 = Debug|Any CPU {4F79325B-9EE7-4501-800F-7A1F8DFBCC80}.Release|Any CPU.ActiveCfg = Release|Any CPU {4F79325B-9EE7-4501-800F-7A1F8DFBCC80}.Release|Any CPU.Build.0 = Release|Any CPU + {E28D4F3C-6C34-497B-BDC8-F2B3EA8BA309}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E28D4F3C-6C34-497B-BDC8-F2B3EA8BA309}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E28D4F3C-6C34-497B-BDC8-F2B3EA8BA309}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E28D4F3C-6C34-497B-BDC8-F2B3EA8BA309}.Release|Any CPU.Build.0 = Release|Any CPU + {3883AD08-B981-4943-8153-1E7FFD2C3127}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3883AD08-B981-4943-8153-1E7FFD2C3127}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3883AD08-B981-4943-8153-1E7FFD2C3127}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3883AD08-B981-4943-8153-1E7FFD2C3127}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Akka.Hosting.TestKit.Tests/Akka.Hosting.TestKit.Tests.csproj b/src/Akka.Hosting.TestKit.Tests/Akka.Hosting.TestKit.Tests.csproj new file mode 100644 index 00000000..920e8b5e --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/Akka.Hosting.TestKit.Tests.csproj @@ -0,0 +1,28 @@ + + + + $(TestsNetCoreFramework) + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + Always + + + + diff --git a/src/Akka.Hosting.TestKit.Tests/HostingSpecSpec.cs b/src/Akka.Hosting.TestKit.Tests/HostingSpecSpec.cs new file mode 100644 index 00000000..46248177 --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/HostingSpecSpec.cs @@ -0,0 +1,64 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2022 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Event; +using Akka.TestKit.TestActors; +using Xunit; +using Xunit.Abstractions; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace Akka.Hosting.TestKit.Tests +{ + public class HostingSpecSpec: TestKit + { + private enum Echo + { } + + public HostingSpecSpec(ITestOutputHelper output) + : base(nameof(HostingSpecSpec), output, logLevel: LogLevel.Debug) + { + } + + protected override Task ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider) + { + builder.WithActors((system, registry) => + { + var echo = system.ActorOf(Props.Create(() => new SimpleEchoActor())); + registry.Register(echo); + }); + return Task.CompletedTask; + } + + [Fact] + public void ActorTest() + { + var echo = ActorRegistry.Get(); + var probe = CreateTestProbe(); + + echo.Tell("TestMessage", probe); + var msg = probe.ExpectMsg("TestMessage"); + Log.Info(msg); + } + + private class SimpleEchoActor : ReceiveActor + { + public SimpleEchoActor() + { + var log = Context.GetLogger(); + + ReceiveAny(msg => + { + log.Info($"Received {msg}"); + Sender.Tell(msg); + }); + } + } + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/NoImplicitSenderSpec.cs b/src/Akka.Hosting.TestKit.Tests/NoImplicitSenderSpec.cs new file mode 100644 index 00000000..23192787 --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/NoImplicitSenderSpec.cs @@ -0,0 +1,75 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Actor.Dsl; +using Akka.TestKit; +using FluentAssertions; +using Xunit; + +namespace Akka.Hosting.TestKit.Tests; + +public class NoImplicitSenderSpec : TestKit, INoImplicitSender +{ + protected override Task ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider) + { + return Task.CompletedTask; + } + + [Fact] + public void When_Not_ImplicitSender_then_testActor_is_not_sender() + { + var echoActor = Sys.ActorOf(c => c.ReceiveAny((m, ctx) => TestActor.Tell(ctx.Sender))); + echoActor.Tell("message"); + var actorRef = ExpectMsg(); + actorRef.Should().Be(Sys.DeadLetters); + } + +} + +public class ImplicitSenderSpec : TestKit +{ + protected override Task ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider) + { + return Task.CompletedTask; + } + + [Fact] + public void ImplicitSender_should_have_testActor_as_sender() + { + var echoActor = Sys.ActorOf(c => c.ReceiveAny((m, ctx) => TestActor.Tell(ctx.Sender))); + echoActor.Tell("message"); + ExpectMsg(actorRef => Equals(actorRef, TestActor)); + + //Test that it works after we know that context has been changed + echoActor.Tell("message"); + ExpectMsg(actorRef => Equals(actorRef, TestActor)); + + } + + + [Fact] + public void ImplicitSender_should_not_change_when_creating_Testprobes() + { + //Verifies that bug #459 has been fixed + var testProbe = CreateTestProbe(); + TestActor.Tell("message"); + ReceiveOne(); + LastSender.Should().Be(TestActor); + } + + [Fact] + public void ImplicitSender_should_not_change_when_creating_TestActors() + { + var testActor2 = CreateTestActor("test2"); + TestActor.Tell("message"); + ReceiveOne(); + LastSender.Should().Be(TestActor); + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/Properties/AssemblyInfo.cs b/src/Akka.Hosting.TestKit.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..e2336be8 --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .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. +using Xunit; + +// 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("b21496c0-a536-4953-9253-d2d0d526e42d")] + +// 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.*")] + +[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true)] diff --git a/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/BossActor.cs b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/BossActor.cs new file mode 100644 index 00000000..5c5e9f1d --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/BossActor.cs @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Akka.Actor; +using Akka.TestKit; + +namespace Akka.Hosting.TestKit.Tests.TestActorRefTests; + +public class BossActor : TActorBase +{ + private TestActorRef _child; + + public BossActor() + { + _child = new TestActorRef(Context.System, Props.Create(), Self, "child"); + } + + protected override SupervisorStrategy SupervisorStrategy() + { + return new OneForOneStrategy(maxNrOfRetries: 5, withinTimeRange: TimeSpan.FromSeconds(1), localOnlyDecider: ex => ex is ActorKilledException ? Directive.Restart : Directive.Escalate); + } + + protected override bool ReceiveMessage(object message) + { + if(message is string && ((string)message) == "sendKill") + { + _child.Tell(Kill.Instance); + return true; + } + return false; + } + + private class InternalActor : TActorBase + { + protected override void PreRestart(Exception reason, object message) + { + TestActorRefSpec.Counter--; + } + + protected override void PostRestart(Exception reason) + { + TestActorRefSpec.Counter--; + } + + protected override bool ReceiveMessage(object message) + { + return true; + } + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/FsmActor.cs b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/FsmActor.cs new file mode 100644 index 00000000..297f82ec --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/FsmActor.cs @@ -0,0 +1,56 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using Akka.Actor; + +namespace Akka.Hosting.TestKit.Tests.TestActorRefTests; + +public enum TestFsmState +{ + First, + Last +} + +public class FsmActor : FSM +{ + private readonly IActorRef _replyActor; + + public FsmActor(IActorRef replyActor) + { + _replyActor = replyActor; + + When(TestFsmState.First, e => + { + if (e.FsmEvent.Equals("check")) + { + _replyActor.Tell("first"); + } + else if (e.FsmEvent.Equals("next")) + { + return GoTo(TestFsmState.Last); + } + + return Stay(); + }); + + When(TestFsmState.Last, e => + { + if (e.FsmEvent.Equals("check")) + { + _replyActor.Tell("last"); + } + else if (e.FsmEvent.Equals("next")) + { + return GoTo(TestFsmState.First); + } + + return Stay(); + }); + + StartWith(TestFsmState.First, "foo"); + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/Logger.cs b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/Logger.cs new file mode 100644 index 00000000..33eb78e2 --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/Logger.cs @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using Akka.Actor; +using Akka.Event; + +namespace Akka.Hosting.TestKit.Tests.TestActorRefTests; + +public class Logger : ActorBase +{ + private int _count; + private string _msg; + protected override bool Receive(object message) + { + var warning = message as Warning; + if(warning != null && warning.Message is string) + { + _count++; + _msg = (string)warning.Message; + return true; + } + return false; + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/NestingActor.cs b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/NestingActor.cs new file mode 100644 index 00000000..64558707 --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/NestingActor.cs @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using Akka.Actor; +using Akka.TestKit; + +namespace Akka.Hosting.TestKit.Tests.TestActorRefTests; + +public class NestingActor : ActorBase +{ + private readonly IActorRef _nested; + + public NestingActor(bool createTestActorRef) + { + _nested = createTestActorRef ? Context.System.ActorOf() : new TestActorRef(Context.System, Props.Create(), null, null); + } + + protected override bool Receive(object message) + { + Sender.Tell(_nested, Self); + return true; + } + + private class NestedActor : ActorBase + { + protected override bool Receive(object message) + { + return true; + } + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/ReplyActor.cs b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/ReplyActor.cs new file mode 100644 index 00000000..e2c111f9 --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/ReplyActor.cs @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using Akka.Actor; +using Akka.TestKit; + +namespace Akka.Hosting.TestKit.Tests.TestActorRefTests; + +public class ReplyActor : TActorBase +{ + private IActorRef _replyTo; + + protected override bool ReceiveMessage(object message) + { + var strMessage = message as string; + switch(strMessage) + { + case "complexRequest": + _replyTo = Sender; + var worker = new TestActorRef(System, Props.Create()); + worker.Tell("work"); + return true; + case "complexRequest2": + var worker2 = new TestActorRef(System, Props.Create()); + worker2.Tell(Sender, Self); + return true; + case "workDone": + _replyTo.Tell("complexReply", Self); + return true; + case "simpleRequest": + Sender.Tell("simpleReply", Self); + return true; + } + return false; + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/SenderActor.cs b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/SenderActor.cs new file mode 100644 index 00000000..420ec2d0 --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/SenderActor.cs @@ -0,0 +1,44 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using Akka.Actor; + +namespace Akka.Hosting.TestKit.Tests.TestActorRefTests; + +public class SenderActor : TActorBase +{ + private readonly IActorRef _replyActor; + + public SenderActor(IActorRef replyActor) + { + _replyActor = replyActor; + } + + protected override bool ReceiveMessage(object message) + { + var strMessage = message as string; + switch(strMessage) + { + case "complex": + _replyActor.Tell("complexRequest", Self); + return true; + case "complex2": + _replyActor.Tell("complexRequest2", Self); + return true; + case "simple": + _replyActor.Tell("simpleRequest", Self); + return true; + case "complexReply": + TestActorRefSpec.Counter--; + return true; + case "simpleReply": + TestActorRefSpec.Counter--; + return true; + } + return false; + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/TActorBase.cs b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/TActorBase.cs new file mode 100644 index 00000000..4bf03ac4 --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/TActorBase.cs @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System.Threading; +using Akka.Actor; + +namespace Akka.Hosting.TestKit.Tests.TestActorRefTests; + +// ReSharper disable once InconsistentNaming +public abstract class TActorBase : ActorBase +{ + protected sealed override bool Receive(object message) + { + var currentThread = Thread.CurrentThread; + if(currentThread != TestActorRefSpec.Thread) + TestActorRefSpec.OtherThread = currentThread; + return ReceiveMessage(message); + } + + protected abstract bool ReceiveMessage(object message); + + protected ActorSystem System + { + get { return ((LocalActorRef)Self).Cell.System; } + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/TestActorRefSpec.cs b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/TestActorRefSpec.cs new file mode 100644 index 00000000..2be76e17 --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/TestActorRefSpec.cs @@ -0,0 +1,235 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Threading; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Configuration; +using Akka.Dispatch; +using Akka.TestKit; +using Akka.TestKit.Internal; +using FluentAssertions; +using Xunit; + +namespace Akka.Hosting.TestKit.Tests.TestActorRefTests +{ + public class TestActorRefSpec : TestKit + { + public static int Counter = 4; + public static readonly Thread Thread = Thread.CurrentThread; + public static Thread OtherThread; + + + public TestActorRefSpec() + { + } + + private TimeSpan DefaultTimeout => Dilated(TestKitSettings.DefaultTimeout); + + protected override Config Config => "test-dispatcher1.type=\"Akka.Dispatch.PinnedDispatcherConfigurator, Akka\""; + + private void AssertThread() + { + Assert.True(OtherThread == null || OtherThread == Thread, "Thread"); + } + + protected override Task ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider) + { + return Task.CompletedTask; + } + + protected override async Task BeforeTestStart() + { + await base.BeforeTestStart(); + OtherThread = null; + } + + [Fact] + public void TestActorRef_name_must_start_with_double_dollar_sign() + { + //Looking at the scala code, this might not be obvious that the name starts with $$ + //object TestActorRef (TestActorRef.scala) contain this code: + // private[testkit] def randomName: String = { + // val l = number.getAndIncrement() + // "$" + akka.util.Helpers.base64(l) + // } + //So it adds one $. The second is added by akka.util.Helpers.base64(l) which by default + //creates a StringBuilder and adds adds $. Hence, 2 $$ + var testActorRef = new TestActorRef(Sys, Props.Create()); + + Assert.Equal("$$", testActorRef.Path.Name.Substring(0, 2)); + } + + [Fact] + public void TestActorRef_must_support_nested_Actor_creation_when_used_with_TestActorRef() + { + var a = new TestActorRef(Sys, Props.Create(() => new NestingActor(true))); + Assert.NotNull(a); + var nested = a.Ask("any", DefaultTimeout).Result; + Assert.NotNull(nested); + Assert.NotSame(a, nested); + } + + [Fact] + public void TestActorRef_must_support_nested_Actor_creation_when_used_with_ActorRef() + { + var a = new TestActorRef(Sys, Props.Create(() => new NestingActor(false))); + Assert.NotNull(a); + var nested = a.Ask("any", DefaultTimeout).Result; + Assert.NotNull(nested); + Assert.NotSame(a, nested); + } + + [Fact] + public void TestActorRef_must_support_reply_via_sender() + { + var serverRef = new TestActorRef(Sys, Props.Create()); + var clientRef = new TestActorRef(Sys, Props.Create(() => new SenderActor(serverRef))); + + Counter = 4; + clientRef.Tell("complex"); + clientRef.Tell("simple"); + clientRef.Tell("simple"); + clientRef.Tell("simple"); + Counter.Should().Be(0); + + Counter = 4; + clientRef.Tell("complex2"); + clientRef.Tell("simple"); + clientRef.Tell("simple"); + clientRef.Tell("simple"); + Counter.Should().Be(0); + + AssertThread(); + } + + [Fact] + public void TestActorRef_must_stop_when_sent_a_PoisonPill() + { + //TODO: Should have this surrounding all code EventFilter[ActorKilledException]() intercept { + var a = new TestActorRef(Sys, Props.Create(), null, "will-be-killed"); + Sys.ActorOf(Props.Create(() => new WatchAndForwardActor(a, TestActor)), "forwarder"); + a.Tell(PoisonPill.Instance); + ExpectMsg(w => w.Terminated.ActorRef == a, TimeSpan.FromSeconds(10), string.Format("that the terminated actor was the one killed, i.e. {0}", a.Path)); + var actorRef = (InternalTestActorRef)a.Ref; + actorRef.IsTerminated.Should().Be(true); + AssertThread(); + } + + [Fact] + public void TestActorRef_must_restart_when_killed() + { + //TODO: Should have this surrounding all code EventFilter[ActorKilledException]() intercept { + Counter = 2; + var boss = new TestActorRef(Sys, Props.Create()); + + boss.Tell("sendKill"); + Assert.Equal(0, Counter); + AssertThread(); + } + + [Fact] + public void TestActorRef_must_support_futures() + { + var worker = new TestActorRef(Sys, Props.Create()); + var task = worker.Ask("work"); + Assert.True(task.IsCompleted, "Task should be completed"); + if(!task.Wait(DefaultTimeout)) throw new TimeoutException("Timed out"); //Using a timeout to stop the test if there is something wrong with the code + Assert.Equal("workDone", task.Result); + } + + [Fact] + public void TestActorRef_must_allow_access_to_internals() + { + var actorRef = new TestActorRef(Sys, Props.Create()); + actorRef.Tell("Hejsan!"); + var actor = actorRef.UnderlyingActor; + Assert.Equal("Hejsan!", actor.ReceivedString); + } + + [Fact] + public void TestActorRef_must_set_ReceiveTimeout_to_None() + { + var a = new TestActorRef(Sys, Props.Create()); + ((IInternalActor)a.UnderlyingActor).ActorContext.ReceiveTimeout.Should().Be(null); + } + + [Fact] + public void TestActorRef_must_set_CallingThreadDispatcher() + { + var a = new TestActorRef(Sys, Props.Create()); + var actorRef = (InternalTestActorRef)a.Ref; + Assert.IsType(actorRef.Cell.Dispatcher); + } + + [Fact] + public void TestActorRef_must_allow_override_of_dispatcher() + { + var a = new TestActorRef(Sys, Props.Create().WithDispatcher("test-dispatcher1")); + var actorRef = (InternalTestActorRef)a.Ref; + Assert.IsType(actorRef.Cell.Dispatcher); + } + + [Fact] + public void TestActorRef_must_proxy_receive_for_the_underlying_actor_without_sender() + { + var a = new TestActorRef(Sys, Props.Create()); + a.Receive("work"); + var actorRef = (InternalTestActorRef)a.Ref; + Assert.True(actorRef.IsTerminated); + } + + [Fact] + public void TestActorRef_must_proxy_receive_for_the_underlying_actor_with_sender() + { + var a = new TestActorRef(Sys, Props.Create()); + a.Receive("work", TestActor); //This will stop the actor + var actorRef = (InternalTestActorRef)a.Ref; + Assert.True(actorRef.IsTerminated); + ExpectMsg("workDone"); + } + + [Fact] + public void TestFsmActorRef_must_proxy_receive_for_underlying_actor_with_sender() + { + var a = new TestFSMRef(Sys, Props.Create(() => new FsmActor(TestActor))); + a.Receive("check"); + ExpectMsg("first"); + + // verify that we can change state + a.SetState(TestFsmState.Last); + a.Receive("check"); + ExpectMsg("last"); + } + + [Fact] + public void BugFix1709_TestFsmActorRef_must_work_with_Fsms_with_constructor_arguments() + { + var a = ActorOfAsTestFSMRef(Props.Create(() => new FsmActor(TestActor))); + a.Receive("check"); + ExpectMsg("first"); + + // verify that we can change state + a.SetState(TestFsmState.Last); + a.Receive("check"); + ExpectMsg("last"); + } + + private class SaveStringActor : TActorBase + { + public string ReceivedString { get; private set; } + + protected override bool ReceiveMessage(object message) + { + ReceivedString = message as string; + return true; + } + } + } +} + diff --git a/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/TestProbeSpec.cs b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/TestProbeSpec.cs new file mode 100644 index 00000000..ce64f64a --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/TestProbeSpec.cs @@ -0,0 +1,120 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.TestKit; +using Akka.TestKit.TestActors; +using Akka.Util.Internal; +using FluentAssertions; +using Xunit; + +namespace Akka.Hosting.TestKit.Tests.TestActorRefTests +{ + public class TestProbeSpec : TestKit + { + [Fact] + public void TestProbe_should_equal_underlying_Ref() + { + var p = CreateTestProbe(); + p.Equals(p.Ref).Should().BeTrue(); + p.Ref.Equals(p).Should().BeTrue(); + var hs = new HashSet {p, p.Ref}; + hs.Count.Should().Be(1); + } + + protected override Task ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider) + { + return Task.CompletedTask; + } + + /// + /// Should be able to receive a message from a + /// if we're deathwatching it and it terminates. + /// + [Fact] + public void TestProbe_should_send_Terminated_when_killed() + { + var p = CreateTestProbe(); + Watch(p); + Sys.Stop(p); + ExpectTerminated(p); + } + + /// + /// If we deathwatch the underlying actor ref or TestProbe itself, it shouldn't matter. + /// + /// They should be equivalent either way. + /// + [Fact] + public void TestProbe_underlying_Ref_should_be_equivalent_to_TestProbe() + { + var p = CreateTestProbe(); + Watch(p.Ref); + Sys.Stop(p); + ExpectTerminated(p); + } + + /// + /// Should be able to receive a message from a + /// if we're deathwatching it and it terminates. + /// + [Fact] + public void TestProbe_underlying_Ref_should_send_Terminated_when_killed() + { + var p = CreateTestProbe(); + Watch(p.Ref); + Sys.Stop(p.Ref); + ExpectTerminated(p.Ref); + } + + [Fact] + public void TestProbe_should_create_a_child_when_invoking_ChildActorOf() + { + var probe = CreateTestProbe(); + var child = probe.ChildActorOf(Props.Create()); + child.Path.Parent.Should().Be(probe.Ref.Path); + var namedChild = probe.ChildActorOf("actorName"); + namedChild.Path.Name.Should().Be("actorName"); + } + + [Fact] + public void TestProbe_restart_a_failing_child_if_the_given_supervisor_says_so() + { + var restarts = new AtomicCounter(0); + var probe = CreateTestProbe(); + var child = probe.ChildActorOf(Props.Create(() => new FailingActor(restarts)), SupervisorStrategy.DefaultStrategy); + AwaitAssert(() => + { + child.Tell("hello"); + restarts.Current.Should().BeGreaterThan(1); + }); + } + + class FailingActor : ActorBase + { + private AtomicCounter Restarts { get; } + + public FailingActor(AtomicCounter restarts) + { + Restarts = restarts; + } + + protected override bool Receive(object message) + { + throw new Exception("Simulated failure"); + } + + protected override void PostRestart(Exception reason) + { + Restarts.IncrementAndGet(); + } + } + } +} diff --git a/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/WatchAndForwardActor.cs b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/WatchAndForwardActor.cs new file mode 100644 index 00000000..0306bc9e --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/WatchAndForwardActor.cs @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using Akka.Actor; + +namespace Akka.Hosting.TestKit.Tests.TestActorRefTests; + +public class WatchAndForwardActor : ActorBase +{ + private readonly IActorRef _forwardToActor; + + public WatchAndForwardActor(IActorRef watchedActor, IActorRef forwardToActor) + { + _forwardToActor = forwardToActor; + Context.Watch(watchedActor); + } + + protected override bool Receive(object message) + { + var terminated = message as Terminated; + if(terminated != null) + _forwardToActor.Tell(new WrappedTerminated(terminated), Sender); + else + _forwardToActor.Tell(message, Sender); + return true; + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/WorkerActor.cs b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/WorkerActor.cs new file mode 100644 index 00000000..832849b2 --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/WorkerActor.cs @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using Akka.Actor; + +namespace Akka.Hosting.TestKit.Tests.TestActorRefTests; + +public class WorkerActor : TActorBase +{ + protected override bool ReceiveMessage(object message) + { + if((message as string) == "work") + { + Sender.Tell("workDone"); + Context.Stop(Self); + return true; + + } + //TODO: case replyTo: Promise[_] ⇒ replyTo.asInstanceOf[Promise[Any]].success("complexReply") + if(message is IActorRef) + { + ((IActorRef)message).Tell("complexReply", Self); + return true; + } + return false; + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/WrappedTerminated.cs b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/WrappedTerminated.cs new file mode 100644 index 00000000..b940dedd --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestActorRefTests/WrappedTerminated.cs @@ -0,0 +1,22 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using Akka.Actor; + +namespace Akka.Hosting.TestKit.Tests.TestActorRefTests; + +public class WrappedTerminated +{ + private readonly Terminated _terminated; + + public WrappedTerminated(Terminated terminated) + { + _terminated = terminated; + } + + public Terminated Terminated { get { return _terminated; } } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/AllTestForEventFilterBase.cs b/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/AllTestForEventFilterBase.cs new file mode 100644 index 00000000..dd4878c4 --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/AllTestForEventFilterBase.cs @@ -0,0 +1,293 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using Akka.Event; +using Akka.TestKit; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; +using static FluentAssertions.FluentActions; + +namespace Akka.Hosting.TestKit.Tests.TestEventListenerTests +{ + public abstract class AllTestForEventFilterBase : EventFilterTestBase where TLogEvent : LogEvent + { + // ReSharper disable ConvertToLambdaExpression + private EventFilterFactory _testingEventFilter; + + protected AllTestForEventFilterBase(LogLevel logLevel, ITestOutputHelper output = null) + : base(logLevel, output) + { + } + + protected override async Task BeforeTestStart() + { + await base.BeforeTestStart(); + LogLevel = Event.Logging.LogLevelFor(); + // ReSharper disable once VirtualMemberCallInContructor + _testingEventFilter = CreateTestingEventFilter(); + } + + protected new LogLevel LogLevel { get; private set; } + protected abstract EventFilterFactory CreateTestingEventFilter(); + + protected void LogMessage(string message) + { + Log.Log(LogLevel, message); + } + + protected override void SendRawLogEventMessage(object message) + { + PublishMessage(message, "test"); + } + + protected abstract void PublishMessage(object message, string source); + + [Fact] + public void Single_message_is_intercepted() + { + _testingEventFilter.ForLogLevel(LogLevel).ExpectOne(() => LogMessage("whatever")); + TestSuccessful = true; + } + + + [Fact] + public void Can_intercept_messages_when_start_is_specified() + { + _testingEventFilter.ForLogLevel(LogLevel, start: "what").ExpectOne(() => LogMessage("whatever")); + TestSuccessful = true; + } + + [Fact] + public void Do_not_intercept_messages_when_start_does_not_match() + { + _testingEventFilter.ForLogLevel(LogLevel, start: "what").ExpectOne(() => + { + LogMessage("let-me-thru"); + LogMessage("whatever"); + }); + ExpectMsg(err => (string)err.Message == "let-me-thru"); + TestSuccessful = true; + } + + [Fact] + public void Can_intercept_messages_when_message_is_specified() + { + _testingEventFilter.ForLogLevel(LogLevel, message: "whatever").ExpectOne(() => LogMessage("whatever")); + TestSuccessful = true; + } + + [Fact] + public void Do_not_intercept_messages_when_message_does_not_match() + { + EventFilter.ForLogLevel(LogLevel, message: "whatever").ExpectOne(() => + { + LogMessage("let-me-thru"); + LogMessage("whatever"); + }); + ExpectMsg(err => (string)err.Message == "let-me-thru"); + TestSuccessful = true; + } + + [Fact] + public void Can_intercept_messages_when_contains_is_specified() + { + _testingEventFilter.ForLogLevel(LogLevel, contains: "ate").ExpectOne(() => LogMessage("whatever")); + TestSuccessful = true; + } + + [Fact] + public void Do_not_intercept_messages_when_contains_does_not_match() + { + _testingEventFilter.ForLogLevel(LogLevel, contains: "eve").ExpectOne(() => + { + LogMessage("let-me-thru"); + LogMessage("whatever"); + }); + ExpectMsg(err => (string)err.Message == "let-me-thru"); + TestSuccessful = true; + } + + + [Fact] + public void Can_intercept_messages_when_source_is_specified() + { + _testingEventFilter.ForLogLevel(LogLevel, source: LogSource.FromType(GetType(), Sys)).ExpectOne(() => LogMessage("whatever")); + TestSuccessful = true; + } + + [Fact] + public void Do_not_intercept_messages_when_source_does_not_match() + { + _testingEventFilter.ForLogLevel(LogLevel, source: "expected-source").ExpectOne(() => + { + PublishMessage("message", source: "expected-source"); + PublishMessage("message", source: "let-me-thru"); + }); + ExpectMsg(err => err.LogSource == "let-me-thru"); + TestSuccessful = true; + } + + [Fact] + public void Specified_numbers_of_messagesan_be_intercepted() + { + _testingEventFilter.ForLogLevel(LogLevel).Expect(2, () => + { + LogMessage("whatever"); + LogMessage("whatever"); + }); + TestSuccessful = true; + } + + [Fact] + public void Expect_0_events_Should_work() + { + this.Invoking(_ => + { + EventFilter.Error().Expect(0, () => + { + Log.Error("something"); + }); + }).Should().Throw("Expected 0 events"); + } + + [Fact] + public async Task ExpectAsync_0_events_Should_work() + { + Exception ex = null; + try + { + await EventFilter.Error().ExpectAsync(0, async () => + { + await Task.Delay(100); // bug only happens when error is not logged instantly + Log.Error("something"); + }); + } + catch (Exception e) + { + ex = e; + } + + ex.Should().NotBeNull("Expected 0 errors logged, but there are error logs"); + } + + /// issue: InternalExpectAsync does not await actionAsync() - causing actionAsync to run as a detached task #5537 + [Fact] + public async Task ExpectAsync_should_await_actionAsync() + { + await Assert.ThrowsAnyAsync(async () => + { + await _testingEventFilter.ForLogLevel(LogLevel).ExpectAsync(0, actionAsync: async () => + { + Assert.False(true); + await Task.CompletedTask; + }); + }); + } + + // issue: InterceptAsync seems to run func() as a detached task #5586 + [Fact] + public async Task InterceptAsync_should_await_func() + { + await Assert.ThrowsAnyAsync(async () => + { + await _testingEventFilter.ForLogLevel(LogLevel).ExpectAsync(0, async () => + { + Assert.False(true); + await Task.CompletedTask; + }, TimeSpan.FromSeconds(.1)); + }); + } + + [Fact] + public void Messages_can_be_muted() + { + _testingEventFilter.ForLogLevel(LogLevel).Mute(() => + { + LogMessage("whatever"); + LogMessage("whatever"); + }); + TestSuccessful = true; + } + + + [Fact] + public void Messages_can_be_muted_from_now_on() + { + var unmutableFilter = _testingEventFilter.ForLogLevel(LogLevel).Mute(); + LogMessage("whatever"); + LogMessage("whatever"); + unmutableFilter.Unmute(); + TestSuccessful = true; + } + + [Fact] + public void Messages_can_be_muted_from_now_on_with_using() + { + using(_testingEventFilter.ForLogLevel(LogLevel).Mute()) + { + LogMessage("whatever"); + LogMessage("whatever"); + } + TestSuccessful = true; + } + + + [Fact] + public void Make_sure_async_works() + { + _testingEventFilter.ForLogLevel(LogLevel).Expect(1, TimeSpan.FromSeconds(2), () => + { + Task.Delay(TimeSpan.FromMilliseconds(10)).ContinueWith(t => { LogMessage("whatever"); }); + }); + } + + [Fact] + public void Chain_many_filters() + { + _testingEventFilter + .ForLogLevel(LogLevel,message:"Message 1").And + .ForLogLevel(LogLevel,message:"Message 3") + .Expect(2,() => + { + LogMessage("Message 1"); + LogMessage("Message 2"); + LogMessage("Message 3"); + + }); + ExpectMsg(m => (string) m.Message == "Message 2"); + } + + + [Fact] + public void Should_timeout_if_too_few_messages() + { + Invoking(() => + { + _testingEventFilter.ForLogLevel(LogLevel).Expect(2, TimeSpan.FromMilliseconds(50), () => + { + LogMessage("whatever"); + }); + }).Should().Throw().WithMessage("timeout*"); + } + + [Fact] + public void Should_log_when_not_muting() + { + const string message = "This should end up in the log since it's not filtered"; + LogMessage(message); + ExpectMsg( msg => (string)msg.Message == message); + } + + // ReSharper restore ConvertToLambdaExpression + + } +} + diff --git a/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/AllTestForEventFilterBase_Instances.cs b/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/AllTestForEventFilterBase_Instances.cs new file mode 100644 index 00000000..0aa33b9e --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/AllTestForEventFilterBase_Instances.cs @@ -0,0 +1,131 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using Akka.Event; +using Akka.TestKit; + +namespace Akka.Hosting.TestKit.Tests.TestEventListenerTests; + +public class EventFilterDebugTests : AllTestForEventFilterBase +{ + public EventFilterDebugTests() : base(LogLevel.DebugLevel){} + + protected override EventFilterFactory CreateTestingEventFilter() + { + return EventFilter; + } + + protected override void PublishMessage(object message, string source) + { + Sys.EventStream.Publish(new Debug(source,GetType(),message)); + } +} + +public class CustomEventFilterDebugTests : AllTestForEventFilterBase +{ + public CustomEventFilterDebugTests() : base(LogLevel.DebugLevel) { } + + protected override EventFilterFactory CreateTestingEventFilter() + { + return CreateEventFilter(Sys); + } + + protected override void PublishMessage(object message, string source) + { + Sys.EventStream.Publish(new Debug(source, GetType(), message)); + } +} + +public class EventFilterInfoTests : AllTestForEventFilterBase +{ + public EventFilterInfoTests() : base(LogLevel.InfoLevel) { } + + protected override EventFilterFactory CreateTestingEventFilter() + { + return EventFilter; + } + + protected override void PublishMessage(object message, string source) + { + Sys.EventStream.Publish(new Info(source, GetType(), message)); + } +} + +public class CustomEventFilterInfoTests : AllTestForEventFilterBase +{ + public CustomEventFilterInfoTests() : base(LogLevel.InfoLevel) { } + + protected override EventFilterFactory CreateTestingEventFilter() + { + return CreateEventFilter(Sys); + } + + protected override void PublishMessage(object message, string source) + { + Sys.EventStream.Publish(new Info(source, GetType(), message)); + } +} + + +public class EventFilterWarningTests : AllTestForEventFilterBase +{ + public EventFilterWarningTests() : base(LogLevel.WarningLevel) { } + + protected override EventFilterFactory CreateTestingEventFilter() + { + return EventFilter; + } + + protected override void PublishMessage(object message, string source) + { + Sys.EventStream.Publish(new Warning(source, GetType(), message)); + } +} + +public class CustomEventFilterWarningTests : AllTestForEventFilterBase +{ + public CustomEventFilterWarningTests() : base(LogLevel.WarningLevel) { } + + protected override EventFilterFactory CreateTestingEventFilter() + { + return CreateEventFilter(Sys); + } + + protected override void PublishMessage(object message, string source) + { + Sys.EventStream.Publish(new Warning(source, GetType(), message)); + } +} + +public class EventFilterErrorTests : AllTestForEventFilterBase +{ + public EventFilterErrorTests() : base(LogLevel.ErrorLevel) { } + + protected override EventFilterFactory CreateTestingEventFilter() + { + return EventFilter; + } + + protected override void PublishMessage(object message, string source) + { + Sys.EventStream.Publish(new Error(null, source, GetType(), message)); + } +} + +public class CustomEventFilterErrorTests : AllTestForEventFilterBase +{ + public CustomEventFilterErrorTests() : base(LogLevel.ErrorLevel) { } + + protected override EventFilterFactory CreateTestingEventFilter() + { + return CreateEventFilter(Sys); + } + + protected override void PublishMessage(object message, string source) + { + Sys.EventStream.Publish(new Error(null, source, GetType(), message)); + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/ConfigTests.cs b/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/ConfigTests.cs new file mode 100644 index 00000000..6d5bfef0 --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/ConfigTests.cs @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace Akka.Hosting.TestKit.Tests.TestEventListenerTests +{ + public class ConfigTests : TestKit + { + protected override Task ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider) + { + return Task.CompletedTask; + } + + [Fact] + public void TestEventListener_is_in_config_by_default() + { + var configLoggers = Sys.Settings.Config.GetStringList("akka.loggers", new string[] { }); + configLoggers.Any(logger => logger.Contains("Akka.TestKit.TestEventListener")).Should().BeTrue(); + configLoggers.Any(logger => logger.Contains("Akka.Event.DefaultLogger")).Should().BeFalse(); + } + } +} + diff --git a/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/CustomEventFilterTests.cs b/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/CustomEventFilterTests.cs new file mode 100644 index 00000000..0e1a1f63 --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/CustomEventFilterTests.cs @@ -0,0 +1,62 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using Akka.Event; +using Akka.TestKit; +using Xunit; + +namespace Akka.Hosting.TestKit.Tests.TestEventListenerTests; + +public abstract class CustomEventFilterTestsBase : EventFilterTestBase +{ + // ReSharper disable ConvertToLambdaExpression + public CustomEventFilterTestsBase() : base(Event.LogLevel.ErrorLevel) { } + + protected override void SendRawLogEventMessage(object message) + { + Sys.EventStream.Publish(new Error(null, "CustomEventFilterTests", GetType(), message)); + } + + protected abstract EventFilterFactory CreateTestingEventFilter(); + + [Fact] + public void Custom_filter_should_match() + { + var eventFilter = CreateTestingEventFilter(); + eventFilter.Custom(logEvent => logEvent is Error && (string) logEvent.Message == "whatever").ExpectOne(() => + { + Log.Error("whatever"); + }); + } + + [Fact] + public void Custom_filter_should_match2() + { + var eventFilter = CreateTestingEventFilter(); + eventFilter.Custom(logEvent => (string)logEvent.Message == "whatever").ExpectOne(() => + { + Log.Error("whatever"); + }); + } + // ReSharper restore ConvertToLambdaExpression +} + +public class CustomEventFilterTests : CustomEventFilterTestsBase +{ + protected override EventFilterFactory CreateTestingEventFilter() + { + return EventFilter; + } +} + +public class CustomEventFilterCustomFilterTests : CustomEventFilterTestsBase +{ + protected override EventFilterFactory CreateTestingEventFilter() + { + return CreateEventFilter(Sys); + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/DeadLettersEventFilterTests.cs b/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/DeadLettersEventFilterTests.cs new file mode 100644 index 00000000..86f0416a --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/DeadLettersEventFilterTests.cs @@ -0,0 +1,70 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Event; +using Akka.TestKit; +using Akka.TestKit.TestActors; +using Xunit; + +namespace Akka.Hosting.TestKit.Tests.TestEventListenerTests; + +public abstract class DeadLettersEventFilterTestsBase : EventFilterTestBase +{ + private IActorRef _deadActor; + + // ReSharper disable ConvertToLambdaExpression + protected DeadLettersEventFilterTestsBase() : base(Event.LogLevel.ErrorLevel) + { + } + + protected override async Task BeforeTestStart() + { + await base.BeforeTestStart(); + _deadActor = Sys.ActorOf(BlackHoleActor.Props, "dead-actor"); + Watch(_deadActor); + Sys.Stop(_deadActor); + ExpectTerminated(_deadActor); + } + + protected override void SendRawLogEventMessage(object message) + { + Sys.EventStream.Publish(new Error(null, "DeadLettersEventFilterTests", GetType(), message)); + } + + protected abstract EventFilterFactory CreateTestingEventFilter(); + + [Fact] + public void Should_be_able_to_filter_dead_letters() + { + var eventFilter = CreateTestingEventFilter(); + eventFilter.DeadLetter().ExpectOne(() => + { + _deadActor.Tell("whatever"); + }); + } + + + // ReSharper restore ConvertToLambdaExpression +} + +public class DeadLettersEventFilterTests : DeadLettersEventFilterTestsBase +{ + protected override EventFilterFactory CreateTestingEventFilter() + { + return EventFilter; + } +} + +public class DeadLettersCustomEventFilterTests : DeadLettersEventFilterTestsBase +{ + protected override EventFilterFactory CreateTestingEventFilter() + { + return CreateEventFilter(Sys); + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/EventFilterTestBase.cs b/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/EventFilterTestBase.cs new file mode 100644 index 00000000..ce5fdac8 --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/EventFilterTestBase.cs @@ -0,0 +1,77 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using Akka.Event; +using Xunit.Abstractions; + +namespace Akka.Hosting.TestKit.Tests.TestEventListenerTests +{ + public abstract class EventFilterTestBase : TestKit + { + private readonly LogLevel _logLevel; + + /// + /// Used to signal that the test was successful and that we should ensure no more messages were logged + /// + protected bool TestSuccessful; + + protected EventFilterTestBase(LogLevel logLevel, ITestOutputHelper output = null) : base(output: output) + { + _logLevel = logLevel; + } + + protected abstract void SendRawLogEventMessage(object message); + + protected override Task ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider) + { + builder.ConfigureLoggers(logger => + { + logger.LogLevel = _logLevel; + logger.ClearLoggers(); + logger.AddLogger(); + }); + + return Task.CompletedTask; + } + + protected override async Task BeforeTestStart() + { + await base.BeforeTestStart(); + + //We send a ForwardAllEventsTo containing message to the TestEventListenerToForwarder logger (configured as a logger above). + //It should respond with an "OK" message when it has received the message. + var initLoggerMessage = new ForwardAllEventsTestEventListener.ForwardAllEventsTo(TestActor); + // ReSharper disable once DoNotCallOverridableMethodsInConstructor + SendRawLogEventMessage(initLoggerMessage); + ExpectMsg("OK"); + //From now on we know that all messages will be forwarded to TestActor + } + + protected override async Task AfterAllAsync() + { + //After every test we make sure no uncatched messages have been logged + if(TestSuccessful) + { + EnsureNoMoreLoggedMessages(); + } + await base.AfterAllAsync(); + } + + private void EnsureNoMoreLoggedMessages() + { + //We log a Finished message. When it arrives to TestActor we know no other message has been logged. + //If we receive something else it means another message was logged, and ExpectMsg will fail + const string message = "<>"; + SendRawLogEventMessage(message); + ExpectMsg(err => (string) err.Message == message,hint: "message to be \"" + message + "\""); + } + + } +} + diff --git a/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/ExceptionEventFilterTests.cs b/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/ExceptionEventFilterTests.cs new file mode 100644 index 00000000..b4d2b12f --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/ExceptionEventFilterTests.cs @@ -0,0 +1,174 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Akka.Actor; +using Akka.Event; +using FluentAssertions; +using Xunit; +using Xunit.Sdk; +using static FluentAssertions.FluentActions; + +namespace Akka.Hosting.TestKit.Tests.TestEventListenerTests; + +public class ExceptionEventFilterTests : EventFilterTestBase +{ + public ExceptionEventFilterTests() + : base(Event.LogLevel.ErrorLevel) + { + } + + public class SomeException : Exception { } + + protected override void SendRawLogEventMessage(object message) + { + Sys.EventStream.Publish(new Error(null, nameof(ExceptionEventFilterTests), GetType(), message)); + } + + [Fact] + public void SingleExceptionIsIntercepted() + { + EventFilter.Exception() + .ExpectOne(() => Log.Error(new SomeException(), "whatever")); + ExpectNoMsg(TimeSpan.FromMilliseconds(100)); + } + + [Fact] + public void CanInterceptMessagesWhenStartIsSpecified() + { + EventFilter.Exception(start: "what") + .ExpectOne(() => Log.Error(new SomeException(), "whatever")); + ExpectNoMsg(TimeSpan.FromMilliseconds(100)); + } + + [Fact] + public void DoNotInterceptMessagesWhenStartDoesNotMatch() + { + EventFilter.Exception(start: "this is clearly not in message"); + Log.Error(new SomeException(), "whatever"); + ExpectMsg(err => (string)err.Message == "whatever"); + } + + [Fact] + public void CanInterceptMessagesWhenMessageIsSpecified() + { + EventFilter.Exception(message: "whatever") + .ExpectOne(() => Log.Error(new SomeException(), "whatever")); + ExpectNoMsg(TimeSpan.FromMilliseconds(100)); + } + + [Fact] + public void DoNotInterceptMessagesWhenMessageDoesNotMatch() + { + EventFilter.Exception(message: "this is clearly not the message"); + Log.Error(new SomeException(), "whatever"); + ExpectMsg(err => (string)err.Message == "whatever"); + } + + [Fact] + public void CanInterceptMessagesWhenContainsIsSpecified() + { + EventFilter.Exception(contains: "ate") + .ExpectOne(() => Log.Error(new SomeException(), "whatever")); + ExpectNoMsg(TimeSpan.FromMilliseconds(100)); + } + + [Fact] + public void DoNotInterceptMessagesWhenContainsDoesNotMatch() + { + EventFilter.Exception(contains: "this is clearly not in the message"); + Log.Error(new SomeException(), "whatever"); + ExpectMsg(err => (string)err.Message == "whatever"); + } + + + [Fact] + public void CanInterceptMessagesWhenSourceIsSpecified() + { + EventFilter.Exception(source: LogSource.Create(this, Sys).Source) + .ExpectOne(() => + { + Log.Error(new SomeException(), "whatever"); + }); + ExpectNoMsg(TimeSpan.FromMilliseconds(100)); + } + + [Fact] + public void DoNotInterceptMessagesWhenSourceDoesNotMatch() + { + EventFilter.Exception(source: "this is clearly not the source"); + Log.Error(new SomeException(), "whatever"); + ExpectMsg(err => (string)err.Message == "whatever"); + } + + + [Fact] + public void SpecifiedNumbersOfExceptionsCanBeIntercepted() + { + EventFilter.Exception() + .Expect(2, () => + { + Log.Error(new SomeException(), "whatever"); + Log.Error(new SomeException(), "whatever"); + }); + ExpectNoMsg(TimeSpan.FromMilliseconds(100)); + } + + [Fact] + public void ShouldFailIfMoreExceptionsThenSpecifiedAreLogged() + { + Invoking(() => + EventFilter.Exception().Expect(2, () => + { + Log.Error(new SomeException(), "whatever"); + Log.Error(new SomeException(), "whatever"); + Log.Error(new SomeException(), "whatever"); + })) + .Should().Throw().WithMessage("*1 message too many*"); + } + + [Fact] + public void ShouldReportCorrectMessageCount() + { + var toSend = "Eric Cartman"; + var actor = ActorOf( ExceptionTestActor.Props() ); + + EventFilter + .Exception(source: actor.Path.ToString()) + // expecting 2 because the same exception is logged in PostRestart + .Expect(2, () => actor.Tell( toSend )); + } + + internal sealed class ExceptionTestActor : UntypedActor + { + private ILoggingAdapter Log { get; } = Context.GetLogger(); + + protected override void PostRestart(Exception reason) + { + Log.Error(reason, "[PostRestart]"); + base.PostRestart(reason); + } + + protected override void OnReceive( object message ) + { + switch (message) + { + case string msg: + throw new InvalidOperationException( "I'm sailing away. Set an open course" ); + + default: + Unhandled( message ); + break; + } + } + + public static Props Props() + { + return Actor.Props.Create( () => new ExceptionTestActor() ); + } + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/ForwardAllEventsTestEventListener.cs b/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/ForwardAllEventsTestEventListener.cs new file mode 100644 index 00000000..30644a63 --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestEventListenerTests/ForwardAllEventsTestEventListener.cs @@ -0,0 +1,46 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using Akka.Actor; +using Akka.Event; +using Akka.TestKit; + +namespace Akka.Hosting.TestKit.Tests.TestEventListenerTests; + +public class ForwardAllEventsTestEventListener : TestEventListener +{ + private IActorRef _forwarder; + + protected override void Print(LogEvent m) + { + if(m.Message is ForwardAllEventsTo) + { + _forwarder = ((ForwardAllEventsTo)m.Message).Forwarder; + _forwarder.Tell("OK"); + } + else if(_forwarder != null) + { + _forwarder.Forward(m); + } + else + { + base.Print(m); + } + } + + public class ForwardAllEventsTo + { + private readonly IActorRef _forwarder; + + public ForwardAllEventsTo(IActorRef forwarder) + { + _forwarder = forwarder; + } + + public IActorRef Forwarder { get { return _forwarder; } } + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestFSMRefTests/TestFSMRefSpec.cs b/src/Akka.Hosting.TestKit.Tests/TestFSMRefTests/TestFSMRefSpec.cs new file mode 100644 index 00000000..8f099a43 --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestFSMRefTests/TestFSMRefSpec.cs @@ -0,0 +1,91 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using Akka.Actor; +using FluentAssertions; +using Xunit; + +namespace Akka.Hosting.TestKit.Tests.TestFSMRefTests; + +public class TestFSMRefSpec : TestKit +{ + protected override Task ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider) + { + return Task.CompletedTask; + } + + [Fact] + public void A_TestFSMRef_must_allow_access_to_internal_state() + { + var fsm = ActorOfAsTestFSMRef("test-fsm-ref-1"); + + fsm.StateName.Should().Be(1); + fsm.StateData.Should().Be(""); + + fsm.Tell("go"); + fsm.StateName.Should().Be(2); + fsm.StateData.Should().Be("go"); + + fsm.SetState(1); + fsm.StateName.Should().Be(1); + fsm.StateData.Should().Be("go"); + + fsm.SetStateData("buh"); + fsm.StateName.Should().Be(1); + fsm.StateData.Should().Be("buh"); + + fsm.SetStateTimeout(TimeSpan.FromMilliseconds(100)); + Within(TimeSpan.FromMilliseconds(80), TimeSpan.FromMilliseconds(500), () => + AwaitCondition(() => fsm.StateName == 2 && fsm.StateData == "timeout") + ); + } + + [Fact] + public void A_TestFSMRef_must_allow_access_to_timers() + { + var fsm = ActorOfAsTestFSMRef("test-fsm-ref-2"); + fsm.IsTimerActive("test").Should().Be(false); + fsm.SetTimer("test", 12, TimeSpan.FromMilliseconds(10), true); + fsm.IsTimerActive("test").Should().Be(true); + fsm.CancelTimer("test"); + fsm.IsTimerActive("test").Should().Be(false); + } + + private class StateTestFsm : FSM + { + public StateTestFsm() + { + StartWith(1, ""); + When(1, e => + { + var fsmEvent = e.FsmEvent; + if(Equals(fsmEvent, "go")) + return GoTo(2, "go"); + if(fsmEvent is StateTimeout) + return GoTo(2, "timeout"); + return null; + }); + When(2, e => + { + var fsmEvent = e.FsmEvent; + if(Equals(fsmEvent, "back")) + return GoTo(1, "back"); + return null; + }); + } + } + private class TimerTestFsm : FSM + { + public TimerTestFsm() + { + StartWith(1, null); + When(1, e => Stay()); + } + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestKitBaseTests/AwaitAssertTests.cs b/src/Akka.Hosting.TestKit.Tests/TestKitBaseTests/AwaitAssertTests.cs new file mode 100644 index 00000000..32b1a12b --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestKitBaseTests/AwaitAssertTests.cs @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using Akka.Configuration; +using Xunit; +using Xunit.Sdk; + +namespace Akka.Hosting.TestKit.Tests.TestKitBaseTests; + +public class AwaitAssertTests : TestKit +{ + protected override Config Config { get; } = "akka.test.timefactor=2"; + + protected override Task ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider) + { + return Task.CompletedTask; + } + + [Fact] + public void AwaitAssert_must_not_throw_any_exception_when_assertion_is_valid() + { + AwaitAssert(() => Assert.Equal("foo", "foo")); + } + + [Fact] + public void AwaitAssert_must_throw_exception_when_assertion_is_invalid() + { + Within(TimeSpan.FromMilliseconds(300), TimeSpan.FromSeconds(1), () => + { + Assert.Throws(() => + AwaitAssert(() => Assert.Equal("foo", "bar"), TimeSpan.FromMilliseconds(500), TimeSpan.FromMilliseconds(300))); + }); + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestKitBaseTests/DilatedTests.cs b/src/Akka.Hosting.TestKit.Tests/TestKitBaseTests/DilatedTests.cs new file mode 100644 index 00000000..54ec0a3c --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestKitBaseTests/DilatedTests.cs @@ -0,0 +1,85 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Akka.Configuration; +using Xunit; +using Xunit.Sdk; +using FluentAssertions; +using static FluentAssertions.FluentActions; + +namespace Akka.Hosting.TestKit.Tests.TestKitBaseTests; + +public class DilatedTests : TestKit +{ + private const int TimeFactor = 4; + private const int Timeout = 1000; + private const int ExpectedTimeout = Timeout * TimeFactor; + private const int Margin = 1000; // margin for GC + private const int DiffDelta = 100; + + protected override Config Config { get; } = $"akka.test.timefactor={TimeFactor}"; + + protected override Task ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider) + { + return Task.CompletedTask; + } + + [Fact] + public void Dilates_correctly_using_timeFactor() + { + Assert.Equal(Dilated(TimeSpan.FromMilliseconds(Timeout)), TimeSpan.FromMilliseconds(ExpectedTimeout)); + } + + [Fact] + public void AwaitCondition_should_dilate_timeout() + { + var stopwatch = Stopwatch.StartNew(); + Invoking(() => AwaitCondition(() => false, TimeSpan.FromMilliseconds(Timeout))) + .Should().Throw(); + stopwatch.Stop(); + AssertDilated(stopwatch.ElapsedMilliseconds, $"Expected the timeout to be {ExpectedTimeout} but in fact it was {stopwatch.ElapsedMilliseconds}."); + } + + [Fact] + public void ReceiveN_should_dilate_timeout() + { + var stopwatch = Stopwatch.StartNew(); + Invoking(() => ReceiveN(42, TimeSpan.FromMilliseconds(Timeout))) + .Should().Throw(); + stopwatch.Stop(); + AssertDilated(stopwatch.ElapsedMilliseconds, $"Expected the timeout to be {ExpectedTimeout} but in fact it was {stopwatch.ElapsedMilliseconds}."); + } + + [Fact] + public void ExpectMsgAllOf_should_dilate_timeout() + { + var stopwatch = Stopwatch.StartNew(); + Invoking(() => ExpectMsgAllOf(TimeSpan.FromMilliseconds(Timeout), "1", "2")) + .Should().Throw(); + stopwatch.Stop(); + AssertDilated(stopwatch.ElapsedMilliseconds, $"Expected the timeout to be {ExpectedTimeout} but in fact it was {stopwatch.ElapsedMilliseconds}."); + } + + [Fact] + public void FishForMessage_should_dilate_timeout() + { + var stopwatch = Stopwatch.StartNew(); + Invoking(() => FishForMessage(_=>false, TimeSpan.FromMilliseconds(Timeout))) + .Should().Throw(); + stopwatch.Stop(); + AssertDilated(stopwatch.ElapsedMilliseconds, $"Expected the timeout to be {ExpectedTimeout} but in fact it was {stopwatch.ElapsedMilliseconds}."); + } + + private static void AssertDilated(double diff, string message = null) + { + Assert.True(diff >= ExpectedTimeout - DiffDelta, message); + Assert.True(diff < ExpectedTimeout + Margin, message); // margin for GC + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestKitBaseTests/ExpectTests.cs b/src/Akka.Hosting.TestKit.Tests/TestKitBaseTests/ExpectTests.cs new file mode 100644 index 00000000..0ca717ac --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestKitBaseTests/ExpectTests.cs @@ -0,0 +1,62 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using Akka.Actor; +using FluentAssertions; +using Xunit; +using static FluentAssertions.FluentActions; + +namespace Akka.Hosting.TestKit.Tests.TestKitBaseTests; + +public class ExpectTests : TestKit +{ + protected override Task ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider) + { + return Task.CompletedTask; + } + + [Fact] + public void ExpectMsgAllOf_should_receive_correct_messages() + { + TestActor.Tell("1"); + TestActor.Tell("2"); + TestActor.Tell("3"); + TestActor.Tell("4"); + ExpectMsgAllOf("3", "1", "4", "2").Should() + .BeEquivalentTo(new[] { "1", "2", "3", "4" }, opt => opt.WithStrictOrdering()); + } + + [Fact] + public void ExpectMsgAllOf_should_fail_when_receiving_unexpected() + { + TestActor.Tell("1"); + TestActor.Tell("2"); + TestActor.Tell("Totally unexpected"); + TestActor.Tell("3"); + Invoking(() => ExpectMsgAllOf("3", "1", "2")) + .Should().Throw(); + } + + [Fact] + public void ExpectMsgAllOf_should_timeout_when_not_receiving_any_messages() + { + Invoking(() => ExpectMsgAllOf(TimeSpan.FromMilliseconds(100), "3", "1", "2")) + .Should().Throw(); + } + + [Fact] + public void ExpectMsgAllOf_should_timeout_if_to_few_messages() + { + TestActor.Tell("1"); + TestActor.Tell("2"); + Invoking(() => ExpectMsgAllOf(TimeSpan.FromMilliseconds(100), "3", "1", "2")) + .Should().Throw(); + } + +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestKitBaseTests/IgnoreMessagesTests.cs b/src/Akka.Hosting.TestKit.Tests/TestKitBaseTests/IgnoreMessagesTests.cs new file mode 100644 index 00000000..ad22590d --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestKitBaseTests/IgnoreMessagesTests.cs @@ -0,0 +1,68 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using Akka.Actor; +using FluentAssertions; +using Xunit; + +namespace Akka.Hosting.TestKit.Tests.TestKitBaseTests; + +public class IgnoreMessagesTests : TestKit +{ + public class IgnoredMessage + { + public IgnoredMessage(string ignoreMe = null) + { + IgnoreMe = ignoreMe; + } + + public string IgnoreMe { get; } + } + + protected override Task ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider) + { + return Task.CompletedTask; + } + + [Fact] + public void IgnoreMessages_should_ignore_messages() + { + IgnoreMessages(o => o is int && (int)o == 1); + TestActor.Tell(1); + TestActor.Tell("1"); + string.Equals((string)ReceiveOne(), "1").Should().BeTrue(); + HasMessages.Should().BeFalse(); + } + + [Fact] + public void IgnoreMessages_should_ignore_messages_T() + { + IgnoreMessages(); + + TestActor.Tell("1"); + TestActor.Tell(new IgnoredMessage(), TestActor); + TestActor.Tell("2"); + ReceiveN(2).Should().BeEquivalentTo(new[] { "1", "2" }, opt => opt.WithStrictOrdering()); + HasMessages.Should().BeFalse(); + } + + [Fact] + public void IgnoreMessages_should_ignore_messages_T_with_Func() + { + IgnoreMessages(m => String.IsNullOrWhiteSpace(m.IgnoreMe)); + + var msg = new IgnoredMessage("not ignored!"); + + TestActor.Tell("1"); + TestActor.Tell(msg, TestActor); + TestActor.Tell("2"); + ReceiveN(3).Should().BeEquivalentTo(new object[] { "1", msg, "2" }, opt => opt.WithStrictOrdering()); + HasMessages.Should().BeFalse(); + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestKitBaseTests/ReceiveTests.cs b/src/Akka.Hosting.TestKit.Tests/TestKitBaseTests/ReceiveTests.cs new file mode 100644 index 00000000..2bd7c342 --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestKitBaseTests/ReceiveTests.cs @@ -0,0 +1,283 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Threading.Tasks; +using Akka.Actor; +using FluentAssertions; +using Xunit; +using Xunit.Sdk; +using static FluentAssertions.FluentActions; + +namespace Akka.Hosting.TestKit.Tests.TestKitBaseTests; + +public class ReceiveTests : TestKit +{ + protected override Task ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider) + { + return Task.CompletedTask; + } + + [Fact] + public void ReceiveN_should_receive_correct_number_of_messages() + { + TestActor.Tell("1"); + TestActor.Tell("2"); + TestActor.Tell("3"); + TestActor.Tell("4"); + ReceiveN(3).Should().BeEquivalentTo(new[] { "1", "2", "3" }, opt => opt.WithStrictOrdering()); + ReceiveN(1).Should().BeEquivalentTo(new[] { "4" }); + } + + [Fact] + public void ReceiveN_should_timeout_if_no_messages() + { + Invoking(() => ReceiveN(3, TimeSpan.FromMilliseconds(10))) + .Should().Throw(); + } + + [Fact] + public void ReceiveN_should_timeout_if_to_few_messages() + { + TestActor.Tell("1"); + TestActor.Tell("2"); + Invoking(() => ReceiveN(3, TimeSpan.FromMilliseconds(100))) + .Should().Throw(); + } + + + [Fact] + public void FishForMessage_should_return_matched_message() + { + TestActor.Tell(1); + TestActor.Tell(2); + TestActor.Tell(10); + TestActor.Tell(20); + FishForMessage(i => i >= 10).Should().Be(10); + } + + [Fact] + public void FishForMessage_should_timeout_if_no_messages() + { + Invoking(() => FishForMessage(_ => false, TimeSpan.FromMilliseconds(10))) + .Should().Throw(); + } + + [Fact] + public void FishForMessage_should_timeout_if_to_few_messages() + { + TestActor.Tell("1"); + TestActor.Tell("2"); + Invoking(() => FishForMessage(_ => false, TimeSpan.FromMilliseconds(100))) + .Should().Throw(); + } + + [Fact] + public async Task FishForMessage_should_fill_the_all_messages_param_if_not_null() + { + await Task.Run(delegate + { + var probe = base.CreateTestProbe("probe"); + probe.Tell("1"); + probe.Tell(2); + probe.Tell("3"); + probe.Tell(4); + var allMessages = new ArrayList(); + probe.FishForMessage(isMessage: s => s == "3", allMessages: allMessages); + allMessages.Should().BeEquivalentTo(new ArrayList { "1", 2 }); + }); + } + + [Fact] + public async Task FishForMessage_should_clear_the_all_messages_param_if_not_null_before_filling_it() + { + await Task.Run(delegate + { + var probe = base.CreateTestProbe("probe"); + probe.Tell("1"); + probe.Tell(2); + probe.Tell("3"); + probe.Tell(4); + var allMessages = new ArrayList() { "pre filled data" }; + probe.FishForMessage(isMessage: x => x == "3", allMessages: allMessages); + allMessages.Should().BeEquivalentTo(new ArrayList { "1", 2 }); + }); + } + + [Fact] + public async Task FishUntilMessageAsync_should_succeed_with_good_input() + { + var probe = CreateTestProbe("probe"); + probe.Ref.Tell(1d, TestActor); + await probe.FishUntilMessageAsync(max: TimeSpan.FromMilliseconds(10)); + } + + + [Fact] + public async Task FishUntilMessageAsync_should_fail_with_bad_input() + { + var probe = CreateTestProbe("probe"); + probe.Ref.Tell(3, TestActor); + Func func = () => probe.FishUntilMessageAsync(max: TimeSpan.FromMilliseconds(10)); + await func.Should().ThrowAsync(); + } + + [Fact] + public async Task WaitForRadioSilenceAsync_should_succeed_immediately_with_null_good_input() + { + var probe = CreateTestProbe("probe"); + var messages = await probe.WaitForRadioSilenceAsync(max: TimeSpan.FromMilliseconds(0)); + messages.Should().BeEquivalentTo(new ArrayList()); + } + + [Fact] + public async Task WaitForRadioSilenceAsync_should_succeed_immediately_with_good_pre_input() + { + var probe = CreateTestProbe("probe"); + probe.Ref.Tell(1, TestActor); + var messages = await probe.WaitForRadioSilenceAsync(max: TimeSpan.FromMilliseconds(0)); + messages.Should().BeEquivalentTo(new ArrayList { 1 }); + } + + [Fact] + public async Task WaitForRadioSilenceAsync_should_succeed_later_with_good_post_input() + { + var probe = CreateTestProbe("probe"); + var task = probe.WaitForRadioSilenceAsync(); + probe.Ref.Tell(1, TestActor); + var messages = await task; + messages.Should().BeEquivalentTo(new ArrayList { 1 }); + } + + [Fact] + public async Task WaitForRadioSilenceAsync_should_reset_timer_twice_only() + { + var probe = CreateTestProbe("probe"); + var max = TimeSpan.FromMilliseconds(3000); + var halfMax = TimeSpan.FromMilliseconds(max.TotalMilliseconds / 2); + var doubleMax = TimeSpan.FromMilliseconds(max.TotalMilliseconds * 2); + var task = probe.WaitForRadioSilenceAsync(max: max, maxMessages: 2); + await Task.Delay(halfMax); + probe.Ref.Tell(1, TestActor); + await Task.Delay(halfMax); + probe.Ref.Tell(2, TestActor); + await Task.Delay(doubleMax); + probe.Ref.Tell(3, TestActor); + var messages = await task; + messages.Should().BeEquivalentTo(new ArrayList { 1, 2 }); + } + + [Fact] + public async Task WaitForRadioSilenceAsync_should_fail_immediately_with_bad_input() + { + var probe = CreateTestProbe("probe"); + probe.Ref.Tell(3, TestActor); + try + { + await probe.WaitForRadioSilenceAsync(max: TimeSpan.FromMilliseconds(0), maxMessages: 0); + Assert.True(false, "we should never get here"); + } + catch (XunitException) { } + } + + [Fact] + public void ReceiveWhile_Filter_should_on_a_timeout_return_no_messages() + { + ReceiveWhile(_ => _, TimeSpan.FromMilliseconds(10)).Count.Should().Be(0); + } + + [Fact] + public void ReceiveWhile_Filter_should_break_on_function_returning_null_and_return_correct_messages() + { + TestActor.Tell("1"); + TestActor.Tell(2); + TestActor.Tell("3"); + TestActor.Tell(99999.0); + TestActor.Tell(4); + ReceiveWhile(_ => _ is double ? null : _.ToString()) + .Should().BeEquivalentTo(new[] { "1", "2", "3" }, opt => opt.WithStrictOrdering()); + } + + [Fact] + public void ReceiveWhile_Filter_should_not_consume_last_message_that_didnt_match() + { + TestActor.Tell("1"); + TestActor.Tell("2"); + TestActor.Tell(4711); + ReceiveWhile(_ => _ is string ? _ : null); + ExpectMsg(4711); + } + + [Fact] + public void ReceiveWhile_Predicate_should_on_a_timeout_return_no_messages() + { + ReceiveWhile(_ => false, TimeSpan.FromMilliseconds(10)).Count.Should().Be(0); + } + + [Fact] + public void ReceiveWhile_Predicate_should_break_when_predicate_returns_false_and_return_correct_messages() + { + TestActor.Tell("1"); + TestActor.Tell("2"); + TestActor.Tell("3"); + TestActor.Tell("-----------"); + TestActor.Tell("4"); + ReceiveWhile(s => s.Length == 1) + .Should().BeEquivalentTo(new[] { "1", "2", "3" }, opt => opt.WithStrictOrdering()); + } + + [Fact] + public void + ReceiveWhile_Predicate_should_break_when_type_is_wrong_and_we_dont_ignore_those_and_return_correct_messages() + { + TestActor.Tell("1"); + TestActor.Tell("2"); + TestActor.Tell("3"); + TestActor.Tell(4); + TestActor.Tell("5"); + ReceiveWhile(s => s.Length == 1, shouldIgnoreOtherMessageTypes: false) + .Should().BeEquivalentTo(new[] { "1", "2", "3" }, opt => opt.WithStrictOrdering()); + } + + [Fact] + public void + ReceiveWhile_Predicate_should_continue_when_type_is_other_but_we_ignore_other_types_and_return_correct_messages() + { + TestActor.Tell("1"); + TestActor.Tell("2"); + TestActor.Tell("3"); + TestActor.Tell(4); + TestActor.Tell("5"); + ReceiveWhile(s => s.Length == 1, shouldIgnoreOtherMessageTypes: true) + .Should().BeEquivalentTo(new[] { "1", "2", "3", "5" }, opt => opt.WithStrictOrdering()); + } + + [Fact] + public void ReceiveWhile_Predicate_should_not_consume_last_message_that_didnt_match() + { + TestActor.Tell("1"); + TestActor.Tell("2"); + TestActor.Tell(4711); + TestActor.Tell("3"); + TestActor.Tell("4"); + TestActor.Tell("5"); + TestActor.Tell(6); + TestActor.Tell("7"); + TestActor.Tell("8"); + + var received = ReceiveWhile(_ => _ is string); + received.Should().BeEquivalentTo(new[] { "1", "2" }, opt => opt.WithStrictOrdering()); + + ExpectMsg(4711); + + received = ReceiveWhile(_ => _ is string); + received.Should().BeEquivalentTo(new[] { "3", "4", "5" }, opt => opt.WithStrictOrdering()); + + ExpectMsg(6); + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestKitBaseTests/RemainingTests.cs b/src/Akka.Hosting.TestKit.Tests/TestKitBaseTests/RemainingTests.cs new file mode 100644 index 00000000..3efd2ff4 --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestKitBaseTests/RemainingTests.cs @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Akka.Hosting.TestKit.Tests.TestKitBaseTests; + +public class RemainingTests : TestKit +{ + protected override Task ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider) + { + return Task.CompletedTask; + } + + [Fact] + public void Throw_if_remaining_is_called_outside_Within() + { + Assert.Throws(() => Remaining); + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestKitBaseTests/WithinTests.cs b/src/Akka.Hosting.TestKit.Tests/TestKitBaseTests/WithinTests.cs new file mode 100644 index 00000000..2a927f0a --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestKitBaseTests/WithinTests.cs @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Akka.Hosting.TestKit.Tests.TestKitBaseTests; + +public class WithinTests : TestKit +{ + protected override Task ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider) + { + return Task.CompletedTask; + } + + [Fact] + public void Within_should_increase_max_timeout_by_the_provided_epsilon_value() + { + Within(TimeSpan.FromSeconds(1), () => ExpectNoMsg(), TimeSpan.FromMilliseconds(50)); + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestKit_Config_Tests.cs b/src/Akka.Hosting.TestKit.Tests/TestKit_Config_Tests.cs new file mode 100644 index 00000000..c0f4cf74 --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestKit_Config_Tests.cs @@ -0,0 +1,38 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Reflection; +using System.Threading.Tasks; +using Akka.TestKit; +using FluentAssertions; +using Xunit; + +namespace Akka.Hosting.TestKit.Tests; + +// ReSharper disable once InconsistentNaming +public class TestKit_Config_Tests : TestKit +{ + protected override Task ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider) + { + return Task.CompletedTask; + } + + [Fact] + public void DefaultValues_should_be_correct() + { + TestKitSettings.DefaultTimeout.Should().Be(TimeSpan.FromSeconds(5)); + TestKitSettings.SingleExpectDefault.Should().Be(TimeSpan.FromSeconds(3)); + TestKitSettings.TestEventFilterLeeway.Should().Be(TimeSpan.FromSeconds(3)); + TestKitSettings.TestTimeFactor.Should().Be(1); + var callingThreadDispatcherTypeName = typeof(CallingThreadDispatcherConfigurator).FullName + ", " + typeof(CallingThreadDispatcher).GetTypeInfo().Assembly.GetName().Name; + Assert.False(Sys.Settings.Config.IsEmpty); + Sys.Settings.Config.GetString("akka.test.calling-thread-dispatcher.type", null).Should().Be(callingThreadDispatcherTypeName); + Sys.Settings.Config.GetString("akka.test.test-actor.dispatcher.type", null).Should().Be(callingThreadDispatcherTypeName); + CallingThreadDispatcher.Id.Should().Be("akka.test.calling-thread-dispatcher"); + } +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/TestSchedulerTests.cs b/src/Akka.Hosting.TestKit.Tests/TestSchedulerTests.cs new file mode 100644 index 00000000..973bd119 --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/TestSchedulerTests.cs @@ -0,0 +1,203 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Configuration; +using Akka.TestKit; +using Akka.TestKit.Configs; +using FluentAssertions; +using Xunit; + +namespace Akka.Hosting.TestKit.Tests; + +public class TestSchedulerTests : TestKit +{ + private IActorRef _testReceiveActor; + + protected override Task ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider) + { + return Task.CompletedTask; + } + + protected override async Task BeforeTestStart() + { + await base.BeforeTestStart(); + _testReceiveActor = Sys.ActorOf(Props.Create(() => new TestReceiveActor()) + .WithDispatcher(CallingThreadDispatcher.Id)); + } + + protected override Config Config { get; } = TestConfigs.TestSchedulerConfig; + + [Fact] + public void Delivers_message_when_scheduled_time_reached() + { + _testReceiveActor.Tell(new ScheduleOnceMessage(TimeSpan.FromSeconds(1))); + _testReceiveActor.Ask(new Identify(null), RemainingOrDefault) + .Wait(RemainingOrDefault).Should().BeTrue(); // verify that the ActorCell has started + + Scheduler.Advance(TimeSpan.FromSeconds(1)); + ExpectMsg(); + } + + [Fact] + public void Does_not_deliver_message_prematurely() + { + _testReceiveActor.Tell(new ScheduleOnceMessage(TimeSpan.FromSeconds(1))); + _testReceiveActor.Ask(new Identify(null), RemainingOrDefault) + .Wait(RemainingOrDefault).Should().BeTrue(); // verify that the ActorCell has started + + Scheduler.Advance(TimeSpan.FromMilliseconds(999)); + ExpectNoMsg(TimeSpan.FromMilliseconds(20)); + } + + [Fact] + public void Delivers_messages_scheduled_for_same_time_in_order_they_were_added() + { + _testReceiveActor.Tell(new ScheduleOnceMessage(TimeSpan.FromSeconds(1), 1)); + _testReceiveActor.Tell(new ScheduleOnceMessage(TimeSpan.FromSeconds(1), 2)); + _testReceiveActor.Ask(new Identify(null), RemainingOrDefault) + .Wait(RemainingOrDefault).Should().BeTrue(); // verify that the ActorCell has started + + Scheduler.Advance(TimeSpan.FromSeconds(1)); + var firstId = ExpectMsg().Id; + var secondId = ExpectMsg().Id; + Assert.Equal(1, firstId); + Assert.Equal(2, secondId); + } + + [Fact] + public void Keeps_delivering_rescheduled_message() + { + _testReceiveActor.Tell(new RescheduleMessage(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5))); + _testReceiveActor.Ask(new Identify(null), RemainingOrDefault) + .Wait(RemainingOrDefault).Should().BeTrue(); // verify that the ActorCell has started + + for (int i = 0; i < 500; i ++) + { + Scheduler.Advance(TimeSpan.FromSeconds(5)); + ExpectMsg(); + } + } + + [Fact] + public void Uses_initial_delay_to_schedule_first_rescheduled_message() + { + _testReceiveActor.Tell(new RescheduleMessage(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5))); + _testReceiveActor.Ask(new Identify(null), RemainingOrDefault) + .Wait(RemainingOrDefault).Should().BeTrue(); // verify that the ActorCell has started + + Scheduler.Advance(TimeSpan.FromSeconds(1)); + ExpectMsg(); + } + + [Fact] + public void Doesnt_reschedule_cancelled() + { + _testReceiveActor.Tell(new CancelableMessage(TimeSpan.FromSeconds(1))); + _testReceiveActor.Ask(new Identify(null), RemainingOrDefault) + .Wait(RemainingOrDefault).Should().BeTrue(); // verify that the ActorCell has started + + Scheduler.Advance(TimeSpan.FromSeconds(1)); + ExpectMsg(); + _testReceiveActor.Tell(new CancelMessage()); + Scheduler.Advance(TimeSpan.FromSeconds(1)); + ExpectNoMsg(TimeSpan.FromMilliseconds(20)); + } + + + [Fact] + public void Advance_to_takes_us_to_correct_time() + { + _testReceiveActor.Tell(new ScheduleOnceMessage(TimeSpan.FromSeconds(1), 1)); + _testReceiveActor.Tell(new ScheduleOnceMessage(TimeSpan.FromSeconds(2), 2)); + _testReceiveActor.Tell(new ScheduleOnceMessage(TimeSpan.FromSeconds(3), 3)); + _testReceiveActor.Ask(new Identify(null), RemainingOrDefault) + .Wait(RemainingOrDefault).Should().BeTrue(); // verify that the ActorCell has started + + Scheduler.AdvanceTo(Scheduler.Now.AddSeconds(2)); + var firstId = ExpectMsg().Id; + var secondId = ExpectMsg().Id; + ExpectNoMsg(TimeSpan.FromMilliseconds(20)); + Assert.Equal(1, firstId); + Assert.Equal(2, secondId); + } + + private class TestReceiveActor : ReceiveActor + { + private Cancelable _cancelable; + + public TestReceiveActor() + { + Receive(x => + { + Context.System.Scheduler.ScheduleTellOnce(x.ScheduleOffset, Sender, x, Self); + }); + + Receive(x => + { + Context.System.Scheduler.ScheduleTellRepeatedly(x.InitialOffset, x.ScheduleOffset, Sender, x, Self); + }); + + Receive(x => + { + _cancelable = new Cancelable(Context.System.Scheduler); + Context.System.Scheduler.ScheduleTellRepeatedly(x.ScheduleOffset, x.ScheduleOffset, Sender, x, Self, _cancelable); + }); + + Receive(_ => + { + _cancelable.Cancel(); + }); + + } + } + + private class CancelableMessage + { + public TimeSpan ScheduleOffset { get; } + public int Id { get; } + + public CancelableMessage(TimeSpan scheduleOffset, int id = 1) + { + ScheduleOffset = scheduleOffset; + Id = id; + } + } + + private class CancelMessage { } + + private class ScheduleOnceMessage + { + public TimeSpan ScheduleOffset { get; } + public int Id { get; } + + public ScheduleOnceMessage(TimeSpan scheduleOffset, int id = 1) + { + ScheduleOffset = scheduleOffset; + Id = id; + } + } + + private class RescheduleMessage + { + public TimeSpan InitialOffset { get; } + public TimeSpan ScheduleOffset { get; } + public int Id { get; } + + public RescheduleMessage(TimeSpan initialOffset, TimeSpan scheduleOffset, int id = 1) + { + InitialOffset = initialOffset; + ScheduleOffset = scheduleOffset; + Id = id; + } + } + + + private TestScheduler Scheduler => (TestScheduler)Sys.Scheduler; +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit.Tests/xunit.runner.json b/src/Akka.Hosting.TestKit.Tests/xunit.runner.json new file mode 100644 index 00000000..4a73b1e5 --- /dev/null +++ b/src/Akka.Hosting.TestKit.Tests/xunit.runner.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://xunit.github.io/schema/current/xunit.runner.schema.json", + "longRunningTestSeconds": 60, + "parallelizeAssembly": false, + "parallelizeTestCollections": false +} \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit/ActorCellKeepingSynchronizationContext.cs b/src/Akka.Hosting.TestKit/ActorCellKeepingSynchronizationContext.cs new file mode 100644 index 00000000..3af04257 --- /dev/null +++ b/src/Akka.Hosting.TestKit/ActorCellKeepingSynchronizationContext.cs @@ -0,0 +1,83 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Threading; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Actor.Internal; + +namespace Akka.Hosting.TestKit +{ + /// + /// TBD + /// + class ActorCellKeepingSynchronizationContext : SynchronizationContext + { + private readonly ActorCell _cell; + + internal static ActorCell AsyncCache { get; set; } + + /// + /// TBD + /// + /// TBD + public ActorCellKeepingSynchronizationContext(ActorCell cell) + { + _cell = cell; + } + + /// + /// TBD + /// + /// TBD + /// TBD + public override void Post(SendOrPostCallback d, object state) + { + ThreadPool.QueueUserWorkItem(_ => + { + var oldCell = InternalCurrentActorCellKeeper.Current; + var oldContext = Current; + SetSynchronizationContext(this); + InternalCurrentActorCellKeeper.Current = AsyncCache ?? _cell; + + try + { + d(state); + } + finally + { + InternalCurrentActorCellKeeper.Current = oldCell; + SetSynchronizationContext(oldContext); + } + }, state); + } + + /// + /// TBD + /// + /// TBD + /// TBD + public override void Send(SendOrPostCallback d, object state) + { + var tcs = new TaskCompletionSource(); + Post(_ => + { + try + { + d(state); + tcs.SetResult(0); + } + catch (Exception e) + { + tcs.TrySetException(e); + } + }, state); + tcs.Task.Wait(); + } + } +} diff --git a/src/Akka.Hosting.TestKit/Akka.Hosting.TestKit.csproj b/src/Akka.Hosting.TestKit/Akka.Hosting.TestKit.csproj new file mode 100644 index 00000000..ab6944ed --- /dev/null +++ b/src/Akka.Hosting.TestKit/Akka.Hosting.TestKit.csproj @@ -0,0 +1,19 @@ + + + + TestKit for writing tests for Akka.NET using Akka.Hosting and xUnit. + $(LibraryFramework) + true + 8.0 + + + + + + + + + + + + diff --git a/src/Akka.Hosting.TestKit/Akka.Hosting.TestKit.csproj.DotSettings b/src/Akka.Hosting.TestKit/Akka.Hosting.TestKit.csproj.DotSettings new file mode 100644 index 00000000..00152058 --- /dev/null +++ b/src/Akka.Hosting.TestKit/Akka.Hosting.TestKit.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/Akka.Hosting.TestKit/Internals/XUnitLogger.cs b/src/Akka.Hosting.TestKit/Internals/XUnitLogger.cs new file mode 100644 index 00000000..f822c5dd --- /dev/null +++ b/src/Akka.Hosting.TestKit/Internals/XUnitLogger.cs @@ -0,0 +1,86 @@ +using System; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace Akka.Hosting.TestKit.Internals +{ + public class XUnitLogger: ILogger + { + private const string NullFormatted = "[null]"; + + private readonly string _category; + private readonly ITestOutputHelper _helper; + private readonly LogLevel _logLevel; + + public XUnitLogger(string category, ITestOutputHelper helper, LogLevel logLevel) + { + _category = category; + _helper = helper; + _logLevel = logLevel; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (!IsEnabled(logLevel)) + return; + + if (!TryFormatMessage(state, exception, formatter, out var formattedMessage)) + return; + + WriteLogEntry(logLevel, eventId, formattedMessage, exception); + } + + private void WriteLogEntry(LogLevel logLevel, EventId eventId, string message, Exception exception) + { + var level = logLevel switch + { + LogLevel.Critical => "CRT", + LogLevel.Debug => "DBG", + LogLevel.Error => "ERR", + LogLevel.Information => "INF", + LogLevel.Warning => "WRN", + LogLevel.Trace => "DBG", + _ => "???" + }; + + var msg = $"{DateTime.Now}:{level}:{_category}:{eventId} {message}"; + if (exception != null) + msg += $"\n{exception.GetType()} {exception.Message}\n{exception.StackTrace}"; + _helper.WriteLine(msg); + } + + public bool IsEnabled(LogLevel logLevel) + { + return logLevel switch + { + LogLevel.None => false, + _ => logLevel >= _logLevel + }; + } + + public IDisposable BeginScope(TState state) + { + throw new NotImplementedException(); + } + + private static bool TryFormatMessage( + TState state, + Exception exception, + Func formatter, + out string result) + { + formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); + + var formattedMessage = formatter(state, exception); + if (formattedMessage == NullFormatted) + { + result = null; + return false; + } + + result = formattedMessage; + return true; + } + } +} + diff --git a/src/Akka.Hosting.TestKit/Internals/XUnitLoggerProvider.cs b/src/Akka.Hosting.TestKit/Internals/XUnitLoggerProvider.cs new file mode 100644 index 00000000..024d312c --- /dev/null +++ b/src/Akka.Hosting.TestKit/Internals/XUnitLoggerProvider.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace Akka.Hosting.TestKit.Internals +{ + public class XUnitLoggerProvider : ILoggerProvider + { + private readonly ITestOutputHelper _helper; + private readonly LogLevel _logLevel; + + public XUnitLoggerProvider(ITestOutputHelper helper, LogLevel logLevel) + { + _helper = helper; + _logLevel = logLevel; + } + + public void Dispose() + { + // no-op + } + + public ILogger CreateLogger(string categoryName) + { + return new XUnitLogger(categoryName, _helper, _logLevel); + } + } +} + diff --git a/src/Akka.Hosting.TestKit/TestKit.cs b/src/Akka.Hosting.TestKit/TestKit.cs new file mode 100644 index 00000000..15515ccc --- /dev/null +++ b/src/Akka.Hosting.TestKit/TestKit.cs @@ -0,0 +1,211 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2013-2022 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System; +using System.Threading; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Actor.Setup; +using Akka.Annotations; +using Akka.Configuration; +using Akka.Event; +using Akka.Hosting.Logging; +using Akka.Hosting.TestKit.Internals; +using Akka.TestKit; +using Akka.TestKit.Xunit2; +using Akka.TestKit.Xunit2.Internals; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +#nullable enable +namespace Akka.Hosting.TestKit +{ + public abstract class TestKit: TestKitBase, IAsyncLifetime + { + /// + /// Commonly used assertions used throughout the testkit. + /// + protected static XunitAssertions Assertions { get; } = new XunitAssertions(); + + private IHost? _host; + public IHost Host + { + get + { + if(_host is null) + throw new XunitException("Test has not been initialized yet"); + return _host; + } + } + + public ActorRegistry ActorRegistry => Host.Services.GetRequiredService(); + + public TimeSpan StartupTimeout { get; } + public string ActorSystemName { get; } + public ITestOutputHelper? Output { get; } + public LogLevel LogLevel { get; } + + private TaskCompletionSource _initialized = new TaskCompletionSource(); + + protected TestKit(string? actorSystemName = null, ITestOutputHelper? output = null, TimeSpan? startupTimeout = null, LogLevel logLevel = LogLevel.Information) + : base(Assertions) + { + ActorSystemName = actorSystemName ?? "test"; + Output = output; + LogLevel = logLevel; + StartupTimeout = startupTimeout ?? TimeSpan.FromSeconds(10); + } + + protected virtual void ConfigureHostConfiguration(IConfigurationBuilder builder) + { } + + protected virtual void ConfigureAppConfiguration(HostBuilderContext context, IConfigurationBuilder builder) + { } + + protected virtual void ConfigureServices(HostBuilderContext context, IServiceCollection services) + { } + + private void InternalConfigureServices(HostBuilderContext context, IServiceCollection services) + { + ConfigureServices(context, services); + + services.AddAkka(ActorSystemName, async (builder, provider) => + { + builder.AddHocon(DefaultConfig, HoconAddMode.Prepend); + if (Config is { }) + builder.AddHocon(Config, HoconAddMode.Prepend); + + builder.ConfigureLoggers(logger => + { + logger.LogLevel = ToAkkaLogLevel(LogLevel); + logger.ClearLoggers(); + logger.AddLogger(); + }); + + if (Output is { }) + { + builder.StartActors(async (system, registry) => + { + var extSystem = (ExtendedActorSystem)system; + var logger = extSystem.SystemActorOf(Props.Create(() => new LoggerFactoryLogger()), "log-test"); + await logger.Ask(new InitializeLogger(system.EventStream)); + }); + } + + await ConfigureAkka(builder, provider); + + builder.AddStartup((system, registry) => + { + base.InitializeTest(system, (ActorSystemSetup)null!, null, null); + _initialized.SetResult(Done.Instance); + }); + }); + } + + protected virtual Config? Config { get; } = null; + + protected virtual void ConfigureLogging(ILoggingBuilder builder) + { } + + protected abstract Task ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider); + + [InternalApi] + public async Task InitializeAsync() + { + var hostBuilder = new HostBuilder(); + if (Output != null) + hostBuilder.ConfigureLogging(logger => + { + logger.ClearProviders(); + logger.AddProvider(new XUnitLoggerProvider(Output, LogLevel)); + logger.AddFilter("Akka.*", LogLevel); + ConfigureLogging(logger); + }); + hostBuilder + .ConfigureHostConfiguration(ConfigureHostConfiguration) + .ConfigureAppConfiguration(ConfigureAppConfiguration) + .ConfigureServices(InternalConfigureServices); + + _host = hostBuilder.Build(); + + var cts = new CancellationTokenSource(StartupTimeout); + cts.Token.Register(() => + throw new TimeoutException($"Host failed to start within {StartupTimeout.Seconds} seconds")); + try + { + await _host.StartAsync(cts.Token); + } + finally + { + cts.Dispose(); + } + + await _initialized.Task; + await BeforeTestStart(); + } + + protected sealed override void InitializeTest(ActorSystem system, ActorSystemSetup config, string actorSystemName, string testActorName) + { + // no-op, deferring InitializeTest after Host have ran + } + + protected virtual Task BeforeTestStart() + { + return Task.CompletedTask; + } + + /// + /// This method is called when a test ends. + /// + /// + /// If you override this, then make sure you either call base.AfterAllAsync() + /// to shut down the system. Otherwise a memory leak will occur. + /// + /// + protected virtual Task AfterAllAsync() + { + Shutdown(); + return Task.CompletedTask; + } + + public async Task DisposeAsync() + { + await AfterAllAsync(); + if(_host != null) + { + await _host.StopAsync(); + if (_host is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync(); + } + else + { + _host.Dispose(); + } + } + } + + private static Event.LogLevel ToAkkaLogLevel(LogLevel logLevel) + => logLevel switch + { + LogLevel.Trace => Event.LogLevel.DebugLevel, + LogLevel.Debug => Event.LogLevel.DebugLevel, + LogLevel.Information => Event.LogLevel.InfoLevel, + LogLevel.Warning => Event.LogLevel.WarningLevel, + LogLevel.Error => Event.LogLevel.ErrorLevel, + LogLevel.Critical => Event.LogLevel.ErrorLevel, + _ => Event.LogLevel.ErrorLevel + }; + + } +} + diff --git a/src/Akka.Persistence.Hosting.Tests/Akka.Persistence.Hosting.Tests.csproj b/src/Akka.Persistence.Hosting.Tests/Akka.Persistence.Hosting.Tests.csproj index b06789e6..d503b8db 100644 --- a/src/Akka.Persistence.Hosting.Tests/Akka.Persistence.Hosting.Tests.csproj +++ b/src/Akka.Persistence.Hosting.Tests/Akka.Persistence.Hosting.Tests.csproj @@ -16,6 +16,7 @@ + diff --git a/src/Akka.Persistence.Hosting.Tests/EventAdapterSpecs.cs b/src/Akka.Persistence.Hosting.Tests/EventAdapterSpecs.cs index 6ed7a5ee..4dee92a4 100644 --- a/src/Akka.Persistence.Hosting.Tests/EventAdapterSpecs.cs +++ b/src/Akka.Persistence.Hosting.Tests/EventAdapterSpecs.cs @@ -11,7 +11,7 @@ namespace Akka.Persistence.Hosting.Tests; -public class EventAdapterSpecs +public class EventAdapterSpecs: Akka.Hosting.TestKit.TestKit { public static async Task StartHost(Action testSetup) { @@ -79,25 +79,25 @@ public IEventSequence FromJournal(object evt, string manifest) } } - [Fact] - public async Task Should_use_correct_EventAdapter_bindings() + protected override Task ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider) { - // arrange - using var host = await StartHost(collection => collection.AddAkka("MySys", builder => + builder.WithJournal("sql-server", journalBuilder => { - builder.WithJournal("sql-server", journalBuilder => - { - journalBuilder.AddWriteEventAdapter("mapper1", new Type[] { typeof(Event1) }); - journalBuilder.AddReadEventAdapter("reader1", new Type[] { typeof(Event1) }); - journalBuilder.AddEventAdapter("combo", boundTypes: new Type[] { typeof(Event2) }); - journalBuilder.AddWriteEventAdapter("tagger", - boundTypes: new Type[] { typeof(Event1), typeof(Event2) }); - }); - })); - + journalBuilder.AddWriteEventAdapter("mapper1", new Type[] { typeof(Event1) }); + journalBuilder.AddReadEventAdapter("reader1", new Type[] { typeof(Event1) }); + journalBuilder.AddEventAdapter("combo", boundTypes: new Type[] { typeof(Event2) }); + journalBuilder.AddWriteEventAdapter("tagger", + boundTypes: new Type[] { typeof(Event1), typeof(Event2) }); + }); + + return Task.CompletedTask; + } + + [Fact] + public void Should_use_correct_EventAdapter_bindings() + { // act - var sys = host.Services.GetRequiredService(); - var config = sys.Settings.Config; + var config = Sys.Settings.Config; var sqlPersistenceJournal = config.GetConfig("akka.persistence.journal.sql-server"); // assert diff --git a/src/Akka.Persistence.Hosting.Tests/InMemoryPersistenceSpecs.cs b/src/Akka.Persistence.Hosting.Tests/InMemoryPersistenceSpecs.cs index c20dac63..f770da67 100644 --- a/src/Akka.Persistence.Hosting.Tests/InMemoryPersistenceSpecs.cs +++ b/src/Akka.Persistence.Hosting.Tests/InMemoryPersistenceSpecs.cs @@ -13,7 +13,7 @@ namespace Akka.Persistence.Hosting.Tests { - public class InMemoryPersistenceSpecs + public class InMemoryPersistenceSpecs: Akka.Hosting.TestKit.TestKit { private readonly ITestOutputHelper _output; @@ -76,30 +76,26 @@ public static async Task StartHost(Action testSetup) await host.StartAsync(); return host; } + + protected override Task ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider) + { + builder + .WithInMemoryJournal() + .WithInMemorySnapshotStore() + .StartActors((system, registry) => + { + var myActor = system.ActorOf(Props.Create(() => new MyPersistenceActor("ac1")), "actor1"); + registry.Register(myActor); + }); + + return Task.CompletedTask; + } [Fact] public async Task Should_Start_ActorSystem_wth_InMemory_Persistence() { // arrange - using var host = await StartHost(collection => collection.AddAkka("MySys", builder => - { - builder.WithInMemoryJournal().WithInMemorySnapshotStore() - .StartActors((system, registry) => - { - var myActor = system.ActorOf(Props.Create(() => new MyPersistenceActor("ac1")), "actor1"); - registry.Register(myActor); - }) - .WithActors((system, registry) => - { - var extSystem = (ExtendedActorSystem)system; - var logger = extSystem.SystemActorOf(Props.Create(() => new TestOutputLogger(_output)), "log-test"); - logger.Tell(new InitializeLogger(system.EventStream)); - });; - })); - - var actorSystem = host.Services.GetRequiredService(); - var actorRegistry = host.Services.GetRequiredService(); - var myPersistentActor = actorRegistry.Get(); + var myPersistentActor = ActorRegistry.Get(); // act var resp1 = await myPersistentActor.Ask(1, TimeSpan.FromSeconds(3)); @@ -111,13 +107,13 @@ public async Task Should_Start_ActorSystem_wth_InMemory_Persistence() // kill + recreate actor with same PersistentId await myPersistentActor.GracefulStop(TimeSpan.FromSeconds(3)); - var myPersistentActor2 = actorSystem.ActorOf(Props.Create(() => new MyPersistenceActor("ac1")), "actor1a"); + var myPersistentActor2 = Sys.ActorOf(Props.Create(() => new MyPersistenceActor("ac1")), "actor1a"); var snapshot2 = await myPersistentActor2.Ask("getall", TimeSpan.FromSeconds(3)); snapshot2.Should().BeEquivalentTo(new[] {1, 2}); // validate configs - var config = actorSystem.Settings.Config; + var config = Sys.Settings.Config; config.GetString("akka.persistence.journal.plugin").Should().Be("akka.persistence.journal.inmem"); config.GetString("akka.persistence.snapshot-store.plugin").Should().Be("akka.persistence.snapshot-store.inmem"); }