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