diff --git a/Akka.sln b/Akka.sln index 4838b6b2d75..e305fa35188 100644 --- a/Akka.sln +++ b/Akka.sln @@ -279,6 +279,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.TestKit.Xunit2.Tests", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.TestKit.Xunit.Tests", "src\contrib\testkits\Akka.TestKit.Xunit.Tests\Akka.TestKit.Xunit.Tests.csproj", "{F80F41E6-E5C7-4C92-B1CF-42539ECFBE68}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Persistence.TestKit.Xunit", "src\core\Akka.Persistence.TestKit.Xunit\Akka.Persistence.TestKit.Xunit.csproj", "{20C4E600-CF2C-4589-9005-BA82D962B312}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1318,6 +1320,18 @@ Global {F80F41E6-E5C7-4C92-B1CF-42539ECFBE68}.Release|x64.Build.0 = Release|Any CPU {F80F41E6-E5C7-4C92-B1CF-42539ECFBE68}.Release|x86.ActiveCfg = Release|Any CPU {F80F41E6-E5C7-4C92-B1CF-42539ECFBE68}.Release|x86.Build.0 = Release|Any CPU + {20C4E600-CF2C-4589-9005-BA82D962B312}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20C4E600-CF2C-4589-9005-BA82D962B312}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20C4E600-CF2C-4589-9005-BA82D962B312}.Debug|x64.ActiveCfg = Debug|Any CPU + {20C4E600-CF2C-4589-9005-BA82D962B312}.Debug|x64.Build.0 = Debug|Any CPU + {20C4E600-CF2C-4589-9005-BA82D962B312}.Debug|x86.ActiveCfg = Debug|Any CPU + {20C4E600-CF2C-4589-9005-BA82D962B312}.Debug|x86.Build.0 = Debug|Any CPU + {20C4E600-CF2C-4589-9005-BA82D962B312}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20C4E600-CF2C-4589-9005-BA82D962B312}.Release|Any CPU.Build.0 = Release|Any CPU + {20C4E600-CF2C-4589-9005-BA82D962B312}.Release|x64.ActiveCfg = Release|Any CPU + {20C4E600-CF2C-4589-9005-BA82D962B312}.Release|x64.Build.0 = Release|Any CPU + {20C4E600-CF2C-4589-9005-BA82D962B312}.Release|x86.ActiveCfg = Release|Any CPU + {20C4E600-CF2C-4589-9005-BA82D962B312}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1441,6 +1455,7 @@ Global {337A85B5-4A7C-4883-8634-46E7E52A765F} = {7735F35A-E7B7-44DE-B6FB-C770B53EB69C} {95017C99-E960-44E5-83AD-BF21461DF06F} = {7625FD95-4B2C-4A5B-BDD5-94B1493FAC8E} {F80F41E6-E5C7-4C92-B1CF-42539ECFBE68} = {7625FD95-4B2C-4A5B-BDD5-94B1493FAC8E} + {20C4E600-CF2C-4589-9005-BA82D962B312} = {01167D3C-49C4-4CDE-9787-C176D139ACDD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {03AD8E21-7507-4E68-A4E9-F4A7E7273164} diff --git a/src/core/Akka.Persistence.TestKit.Xunit/Akka.Persistence.TestKit.Xunit.csproj b/src/core/Akka.Persistence.TestKit.Xunit/Akka.Persistence.TestKit.Xunit.csproj new file mode 100644 index 00000000000..f03af3156d8 --- /dev/null +++ b/src/core/Akka.Persistence.TestKit.Xunit/Akka.Persistence.TestKit.Xunit.csproj @@ -0,0 +1,17 @@ + + + + TestKit for writing tests for Akka.NET Persistance module using xUnit + $(NetStandardLibVersion);$(NetLibVersion) + $(AkkaPackageTags);testkit;persistance;xunit + true + true + + + + + + + + + diff --git a/src/core/Akka.Persistence.TestKit.Xunit/PersistenceTestKit.cs b/src/core/Akka.Persistence.TestKit.Xunit/PersistenceTestKit.cs new file mode 100644 index 00000000000..54ecf8b88f6 --- /dev/null +++ b/src/core/Akka.Persistence.TestKit.Xunit/PersistenceTestKit.cs @@ -0,0 +1,364 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +//----------------------------------------------------------------------- + +using Akka.Actor.Setup; + +namespace Akka.Persistence.TestKit; + +using System; +using System.Threading.Tasks; +using Actor; +using Akka.TestKit.Xunit; +using Configuration; +using Xunit; + +/// +/// This class represents an Akka.NET Persistence TestKit that uses xUnit +/// as its testing framework. +/// +public class PersistenceTestKit : TestKit +{ + /// + /// Create a new instance of the class. + /// A new system with the specified configuration will be created. + /// + /// Test ActorSystem configuration + /// Optional: The name of the actor system + /// TBD + public PersistenceTestKit(ActorSystemSetup setup, string actorSystemName = null, ITestOutputHelper output = null) + : base(GetConfig(setup), actorSystemName, output) + { + var persistenceExtension = Persistence.Instance.Apply(Sys); + + JournalActorRef = persistenceExtension.JournalFor(null); + Journal = TestJournal.FromRef(JournalActorRef); + + SnapshotsActorRef = persistenceExtension.SnapshotStoreFor(null); + Snapshots = TestSnapshotStore.FromRef(SnapshotsActorRef); + } + + /// + /// Create a new instance of the class. + /// A new system with the specified configuration will be created. + /// + /// Test ActorSystem configuration + /// Optional: The name of the actor system + /// TBD + public PersistenceTestKit(Config config, string actorSystemName = null, ITestOutputHelper output = null) + : base(GetConfig(config), actorSystemName, output) + { + var persistenceExtension = Persistence.Instance.Apply(Sys); + + JournalActorRef = persistenceExtension.JournalFor(null); + Journal = TestJournal.FromRef(JournalActorRef); + + SnapshotsActorRef = persistenceExtension.SnapshotStoreFor(null); + Snapshots = TestSnapshotStore.FromRef(SnapshotsActorRef); + } + + public PersistenceTestKit(ActorSystem actorSystem, ITestOutputHelper output = null) + : base(actorSystem, output) + { + var persistenceExtension = Persistence.Instance.Apply(Sys); + + JournalActorRef = persistenceExtension.JournalFor(null); + Journal = TestJournal.FromRef(JournalActorRef); + + SnapshotsActorRef = persistenceExtension.SnapshotStoreFor(null); + Snapshots = TestSnapshotStore.FromRef(SnapshotsActorRef); + } + + /// + /// Create a new instance of the class. + /// A new system with the default configuration will be created. + /// + /// Optional: The name of the actor system + /// TBD + public PersistenceTestKit(string actorSystemName = null, ITestOutputHelper output = null) + : this(Config.Empty, actorSystemName, output) + { + } + + /// + /// Actor reference to persistence Journal used by current actor system. + /// + public IActorRef JournalActorRef { get; } + + /// + /// Actor reference to persistence Snapshot Store used by current actor system. + /// + public IActorRef SnapshotsActorRef { get; } + + /// + /// + /// + public ITestJournal Journal { get; } + + /// + /// + /// + public ITestSnapshotStore Snapshots { get; } + + /// + /// Execute delegate with Journal Behavior applied to Recovery operation. + /// + /// + /// After will be executed, Recovery behavior will be reverted back to normal. + /// + /// Delegate which will select Journal behavior. + /// Async delegate which will be executed with applied Journal behavior. + /// which must be awaited. + public async Task WithJournalRecovery(Func behaviorSelector, Func execution) + { + if (behaviorSelector == null) throw new ArgumentNullException(nameof(behaviorSelector)); + if (execution == null) throw new ArgumentNullException(nameof(execution)); + + try + { + await behaviorSelector(Journal.OnRecovery); + await execution(); + } + finally + { + await Journal.OnRecovery.Pass(); + } + } + + /// + /// Execute delegate with Journal Behavior applied to Write operation. + /// + /// + /// After will be executed, Write behavior will be reverted back to normal. + /// + /// Delegate which will select Journal behavior. + /// Async delegate which will be executed with applied Journal behavior. + /// which must be awaited. + public async Task WithJournalWrite(Func behaviorSelector, Func execution) + { + if (behaviorSelector == null) throw new ArgumentNullException(nameof(behaviorSelector)); + if (execution == null) throw new ArgumentNullException(nameof(execution)); + + try + { + await behaviorSelector(Journal.OnWrite); + await execution(); + } + finally + { + await Journal.OnWrite.Pass(); + } + } + + /// + /// Execute delegate with Journal Behavior applied to Recovery operation. + /// + /// + /// After will be executed, Recovery behavior will be reverted back to normal. + /// + /// Delegate which will select Journal behavior. + /// Delegate which will be executed with applied Journal behavior. + /// which must be awaited. + public Task WithJournalRecovery(Func behaviorSelector, Action execution) + => WithJournalRecovery(behaviorSelector, () => + { + if (execution == null) throw new ArgumentNullException(nameof(execution)); + + execution(); + return Task.FromResult(new object()); + }); + + /// + /// Execute delegate with Journal Behavior applied to Write operation. + /// + /// + /// After will be executed, Write behavior will be reverted back to normal. + /// + /// Delegate which will select Journal behavior. + /// Delegate which will be executed with applied Journal behavior. + /// which must be awaited. + public Task WithJournalWrite(Func behaviorSelector, Action execution) + => WithJournalWrite(behaviorSelector, () => + { + if (execution == null) throw new ArgumentNullException(nameof(execution)); + + execution(); + return Task.FromResult(new object()); + }); + + /// + /// Execute delegate with Snapshot Store Behavior applied to Save operation. + /// + /// + /// After will be executed, Save behavior will be reverted back to normal. + /// + /// Delegate which will select Snapshot Store behavior. + /// Async delegate which will be executed with applied Journal behavior. + /// which must be awaited. + public async Task WithSnapshotSave(Func behaviorSelector, Func execution) + { + if (behaviorSelector == null) throw new ArgumentNullException(nameof(behaviorSelector)); + if (execution == null) throw new ArgumentNullException(nameof(execution)); + + try + { + await behaviorSelector(Snapshots.OnSave); + await execution(); + } + finally + { + await Snapshots.OnSave.Pass(); + } + } + + /// + /// Execute delegate with Snapshot Store Behavior applied to Load operation. + /// + /// + /// After will be executed, Load behavior will be reverted back to normal. + /// + /// Delegate which will select Snapshot Store behavior. + /// Async delegate which will be executed with applied Journal behavior. + /// which must be awaited. + public async Task WithSnapshotLoad(Func behaviorSelector, Func execution) + { + if (behaviorSelector == null) throw new ArgumentNullException(nameof(behaviorSelector)); + if (execution == null) throw new ArgumentNullException(nameof(execution)); + + try + { + await behaviorSelector(Snapshots.OnLoad); + await execution(); + } + finally + { + await Snapshots.OnLoad.Pass(); + } + } + + /// + /// Execute delegate with Snapshot Store Behavior applied to Delete operation. + /// + /// + /// After will be executed, Delete behavior will be reverted back to normal. + /// + /// Delegate which will select Snapshot Store behavior. + /// Async delegate which will be executed with applied Journal behavior. + /// which must be awaited. + public async Task WithSnapshotDelete(Func behaviorSelector, Func execution) + { + if (behaviorSelector == null) throw new ArgumentNullException(nameof(behaviorSelector)); + if (execution == null) throw new ArgumentNullException(nameof(execution)); + + try + { + await behaviorSelector(Snapshots.OnDelete); + await execution(); + } + finally + { + await Snapshots.OnDelete.Pass(); + } + } + + /// + /// Execute delegate with Snapshot Store Behavior applied to Save operation. + /// + /// + /// After will be executed, Save behavior will be reverted back to normal. + /// + /// Delegate which will select Snapshot Store behavior. + /// Delegate which will be executed with applied Journal behavior. + /// which must be awaited. + public Task WithSnapshotSave(Func behaviorSelector, Action execution) + => WithSnapshotSave(behaviorSelector, () => + { + if (execution == null) throw new ArgumentNullException(nameof(execution)); + + execution(); + return Task.FromResult(true); + }); + + /// + /// Execute delegate with Snapshot Store Behavior applied to Load operation. + /// + /// + /// After will be executed, Load behavior will be reverted back to normal. + /// + /// Delegate which will select Snapshot Store behavior. + /// Async delegate which will be executed with applied Journal behavior. + /// which must be awaited. + public Task WithSnapshotLoad(Func behaviorSelector, Action execution) + => WithSnapshotLoad(behaviorSelector, () => + { + if (execution == null) throw new ArgumentNullException(nameof(execution)); + + execution(); + return Task.FromResult(true); + }); + + /// + /// Execute delegate with Snapshot Store Behavior applied to Delete operation. + /// + /// + /// After will be executed, Delete behavior will be reverted back to normal. + /// + /// Delegate which will select Snapshot Store behavior. + /// Async delegate which will be executed with applied Journal behavior. + /// which must be awaited. + public Task WithSnapshotDelete(Func behaviorSelector, Action execution) + => WithSnapshotDelete(behaviorSelector, () => + { + if (execution == null) throw new ArgumentNullException(nameof(execution)); + + execution(); + return Task.FromResult(true); + }); + + /// + /// Loads from embedded resources actor system persistence configuration with and + /// configured as default persistence plugins. + /// + /// Custom configuration that was passed in the constructor. + /// Actor system configuration object. + /// + private static ActorSystemSetup GetConfig(ActorSystemSetup customConfig) + { + var bootstrapSetup = customConfig.Get(); + var config = bootstrapSetup.FlatSelect(x => x.Config); + var actorProvider = bootstrapSetup.FlatSelect(x => x.ActorRefProvider); + var newSetup = BootstrapSetup.Create(); + if (config.HasValue) + { + newSetup = newSetup.WithConfig(GetConfig(config.Value)); + } + else + { + newSetup = newSetup.WithConfig(GetConfig(Config.Empty)); + } + + if (actorProvider.HasValue) + { + newSetup = newSetup.WithActorRefProvider(actorProvider.Value); + } + + return customConfig.WithSetup(newSetup); + } + + /// + /// Loads from embedded resources actor system persistence configuration with and + /// configured as default persistence plugins. + /// + /// Custom configuration that was passed in the constructor. + /// Actor system configuration object. + /// + private static Config GetConfig(Config customConfig) + { + var defaultConfig = ConfigurationFactory.FromResource("Akka.Persistence.TestKit.config.conf"); + if (customConfig == Config.Empty) return defaultConfig; + else return defaultConfig.SafeWithFallback(customConfig); + } +} \ No newline at end of file diff --git a/src/core/Akka.Persistence.TestKit/Akka.Persistence.TestKit.csproj b/src/core/Akka.Persistence.TestKit/Akka.Persistence.TestKit.csproj index b09dc51fc99..20e579d629a 100644 --- a/src/core/Akka.Persistence.TestKit/Akka.Persistence.TestKit.csproj +++ b/src/core/Akka.Persistence.TestKit/Akka.Persistence.TestKit.csproj @@ -12,7 +12,6 @@ -