* using real EventSourcedBehaviorImpl * using new inmem journal (PersistenceTestKit) * advantages compared to a "fake" driver * no difference in implementation details from real thing * no limitations * less maintance * added internal messsages to EventSourcedBehaviorImpl to be able to grab state and persistenceId * GetState as InternalProtocol instead of Signal so that it is stashed * serialization checks, using SerializationTestKit * better testKitGuardian naming to allow multiple PersistenceTestKit * support testing of restart * support failure testing by using PersistenceTestKit * update doc sample * apidoc, reference docs, and javadsl
This commit is contained in:
parent
a910bee99f
commit
ef79738373
21 changed files with 1254 additions and 235 deletions
|
|
@ -5,6 +5,7 @@
|
||||||
package akka.actor.testkit.typed.scaladsl
|
package akka.actor.testkit.typed.scaladsl
|
||||||
|
|
||||||
import java.util.concurrent.TimeoutException
|
import java.util.concurrent.TimeoutException
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
import scala.concurrent.Await
|
import scala.concurrent.Await
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
|
|
@ -34,6 +35,8 @@ import akka.util.Timeout
|
||||||
|
|
||||||
object ActorTestKit {
|
object ActorTestKit {
|
||||||
|
|
||||||
|
private val testKitGuardianCounter = new AtomicInteger(0)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a testkit named from the ActorTestKit class.
|
* Create a testkit named from the ActorTestKit class.
|
||||||
*
|
*
|
||||||
|
|
@ -62,7 +65,12 @@ object ActorTestKit {
|
||||||
* using default configuration from the reference.conf resources that ship with the Akka libraries.
|
* using default configuration from the reference.conf resources that ship with the Akka libraries.
|
||||||
*/
|
*/
|
||||||
def apply(system: ActorSystem[_]): ActorTestKit = {
|
def apply(system: ActorSystem[_]): ActorTestKit = {
|
||||||
val testKitGuardian = system.systemActorOf(ActorTestKitGuardian.testKitGuardian, "test")
|
val name = testKitGuardianCounter.incrementAndGet() match {
|
||||||
|
case 1 => "test"
|
||||||
|
case n => s"test-$n"
|
||||||
|
}
|
||||||
|
val testKitGuardian =
|
||||||
|
system.systemActorOf(ActorTestKitGuardian.testKitGuardian, name)
|
||||||
new ActorTestKit(system, testKitGuardian, settings = None)
|
new ActorTestKit(system, testKitGuardian, settings = None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,33 +4,28 @@
|
||||||
|
|
||||||
package jdocs.akka.cluster.sharding.typed;
|
package jdocs.akka.cluster.sharding.typed;
|
||||||
|
|
||||||
|
import org.scalatestplus.junit.JUnitSuite;
|
||||||
|
|
||||||
|
import static jdocs.akka.cluster.sharding.typed.AccountExampleWithEventHandlersInState.AccountEntity;
|
||||||
|
|
||||||
// #test
|
// #test
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.UUID;
|
import akka.actor.testkit.typed.javadsl.LogCapturing;
|
||||||
|
import akka.actor.testkit.typed.javadsl.TestKitJunitResource;
|
||||||
|
import akka.actor.typed.ActorRef;
|
||||||
|
import akka.persistence.testkit.javadsl.EventSourcedBehaviorTestKit;
|
||||||
|
import akka.persistence.testkit.javadsl.EventSourcedBehaviorTestKit.CommandResultWithReply;
|
||||||
|
import akka.persistence.typed.PersistenceId;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
import org.junit.ClassRule;
|
import org.junit.ClassRule;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
import akka.actor.testkit.typed.javadsl.LogCapturing;
|
|
||||||
import akka.actor.testkit.typed.javadsl.TestKitJunitResource;
|
|
||||||
import akka.actor.testkit.typed.javadsl.TestProbe;
|
|
||||||
import akka.actor.typed.ActorRef;
|
|
||||||
import akka.persistence.typed.PersistenceId;
|
|
||||||
|
|
||||||
// #test
|
// #test
|
||||||
|
|
||||||
// #test-events
|
|
||||||
import akka.actor.typed.eventstream.EventStream;
|
|
||||||
import akka.persistence.journal.inmem.InmemJournal;
|
|
||||||
|
|
||||||
// #test-events
|
|
||||||
|
|
||||||
import org.scalatestplus.junit.JUnitSuite;
|
|
||||||
|
|
||||||
import static jdocs.akka.cluster.sharding.typed.AccountExampleWithEventHandlersInState.AccountEntity;
|
|
||||||
|
|
||||||
// #test
|
// #test
|
||||||
public class AccountExampleDocTest
|
public class AccountExampleDocTest
|
||||||
// #test
|
// #test
|
||||||
|
|
@ -38,100 +33,103 @@ public class AccountExampleDocTest
|
||||||
// #test
|
// #test
|
||||||
{
|
{
|
||||||
|
|
||||||
// #inmem-config
|
// #testkit
|
||||||
private static final String inmemConfig =
|
@ClassRule
|
||||||
"akka.persistence.journal.plugin = \"akka.persistence.journal.inmem\" \n"
|
public static final TestKitJunitResource testKit =
|
||||||
+ "akka.persistence.journal.inmem.test-serialization = on \n";
|
new TestKitJunitResource(EventSourcedBehaviorTestKit.config());
|
||||||
|
|
||||||
// #inmem-config
|
private EventSourcedBehaviorTestKit<
|
||||||
|
AccountEntity.Command, AccountEntity.Event, AccountEntity.Account>
|
||||||
// #snapshot-store-config
|
eventSourcedTestKit =
|
||||||
private static final String snapshotConfig =
|
EventSourcedBehaviorTestKit.create(
|
||||||
"akka.persistence.snapshot-store.plugin = \"akka.persistence.snapshot-store.local\" \n"
|
testKit.system(), AccountEntity.create("1", PersistenceId.of("Account", "1")));
|
||||||
+ "akka.persistence.snapshot-store.local.dir = \"target/snapshot-"
|
// #testkit
|
||||||
+ UUID.randomUUID().toString()
|
|
||||||
+ "\" \n";
|
|
||||||
// #snapshot-store-config
|
|
||||||
|
|
||||||
private static final String config = inmemConfig + snapshotConfig;
|
|
||||||
|
|
||||||
@ClassRule public static final TestKitJunitResource testKit = new TestKitJunitResource(config);
|
|
||||||
|
|
||||||
@Rule public final LogCapturing logCapturing = new LogCapturing();
|
@Rule public final LogCapturing logCapturing = new LogCapturing();
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void beforeEach() {
|
||||||
|
eventSourcedTestKit.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createWithEmptyBalance() {
|
||||||
|
CommandResultWithReply<
|
||||||
|
AccountEntity.Command,
|
||||||
|
AccountEntity.Event,
|
||||||
|
AccountEntity.Account,
|
||||||
|
AccountEntity.OperationResult>
|
||||||
|
result = eventSourcedTestKit.runCommand(AccountEntity.CreateAccount::new);
|
||||||
|
assertEquals(AccountEntity.Confirmed.INSTANCE, result.reply());
|
||||||
|
assertEquals(AccountEntity.AccountCreated.INSTANCE, result.event());
|
||||||
|
assertEquals(BigDecimal.ZERO, result.stateOfType(AccountEntity.OpenedAccount.class).balance);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void handleWithdraw() {
|
public void handleWithdraw() {
|
||||||
ActorRef<AccountEntity.Command> ref =
|
eventSourcedTestKit.runCommand(AccountEntity.CreateAccount::new);
|
||||||
testKit.spawn(AccountEntity.create("1", PersistenceId.of("Account", "1")));
|
|
||||||
TestProbe<AccountEntity.OperationResult> probe =
|
CommandResultWithReply<
|
||||||
testKit.createTestProbe(AccountEntity.OperationResult.class);
|
AccountEntity.Command,
|
||||||
ref.tell(new AccountEntity.CreateAccount(probe.getRef()));
|
AccountEntity.Event,
|
||||||
probe.expectMessage(AccountEntity.Confirmed.INSTANCE);
|
AccountEntity.Account,
|
||||||
ref.tell(new AccountEntity.Deposit(BigDecimal.valueOf(100), probe.getRef()));
|
AccountEntity.OperationResult>
|
||||||
probe.expectMessage(AccountEntity.Confirmed.INSTANCE);
|
result1 =
|
||||||
ref.tell(new AccountEntity.Withdraw(BigDecimal.valueOf(10), probe.getRef()));
|
eventSourcedTestKit.runCommand(
|
||||||
probe.expectMessage(AccountEntity.Confirmed.INSTANCE);
|
replyTo -> new AccountEntity.Deposit(BigDecimal.valueOf(100), replyTo));
|
||||||
|
assertEquals(AccountEntity.Confirmed.INSTANCE, result1.reply());
|
||||||
|
assertEquals(
|
||||||
|
BigDecimal.valueOf(100), result1.eventOfType(AccountEntity.Deposited.class).amount);
|
||||||
|
assertEquals(
|
||||||
|
BigDecimal.valueOf(100), result1.stateOfType(AccountEntity.OpenedAccount.class).balance);
|
||||||
|
|
||||||
|
CommandResultWithReply<
|
||||||
|
AccountEntity.Command,
|
||||||
|
AccountEntity.Event,
|
||||||
|
AccountEntity.Account,
|
||||||
|
AccountEntity.OperationResult>
|
||||||
|
result2 =
|
||||||
|
eventSourcedTestKit.runCommand(
|
||||||
|
replyTo -> new AccountEntity.Withdraw(BigDecimal.valueOf(10), replyTo));
|
||||||
|
assertEquals(AccountEntity.Confirmed.INSTANCE, result2.reply());
|
||||||
|
assertEquals(BigDecimal.valueOf(10), result2.eventOfType(AccountEntity.Withdrawn.class).amount);
|
||||||
|
assertEquals(
|
||||||
|
BigDecimal.valueOf(90), result2.stateOfType(AccountEntity.OpenedAccount.class).balance);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void rejectWithdrawOverdraft() {
|
public void rejectWithdrawOverdraft() {
|
||||||
ActorRef<AccountEntity.Command> ref =
|
eventSourcedTestKit.runCommand(AccountEntity.CreateAccount::new);
|
||||||
testKit.spawn(AccountEntity.create("2", PersistenceId.of("Account", "2")));
|
eventSourcedTestKit.runCommand(
|
||||||
TestProbe<AccountEntity.OperationResult> probe =
|
(ActorRef<AccountEntity.OperationResult> replyTo) ->
|
||||||
testKit.createTestProbe(AccountEntity.OperationResult.class);
|
new AccountEntity.Deposit(BigDecimal.valueOf(100), replyTo));
|
||||||
ref.tell(new AccountEntity.CreateAccount(probe.getRef()));
|
|
||||||
probe.expectMessage(AccountEntity.Confirmed.INSTANCE);
|
CommandResultWithReply<
|
||||||
ref.tell(new AccountEntity.Deposit(BigDecimal.valueOf(100), probe.getRef()));
|
AccountEntity.Command,
|
||||||
probe.expectMessage(AccountEntity.Confirmed.INSTANCE);
|
AccountEntity.Event,
|
||||||
ref.tell(new AccountEntity.Withdraw(BigDecimal.valueOf(110), probe.getRef()));
|
AccountEntity.Account,
|
||||||
probe.expectMessageClass(AccountEntity.Rejected.class);
|
AccountEntity.OperationResult>
|
||||||
|
result =
|
||||||
|
eventSourcedTestKit.runCommand(
|
||||||
|
replyTo -> new AccountEntity.Withdraw(BigDecimal.valueOf(110), replyTo));
|
||||||
|
result.replyOfType(AccountEntity.Rejected.class);
|
||||||
|
assertTrue(result.hasNoEvents());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void handleGetBalance() {
|
public void handleGetBalance() {
|
||||||
ActorRef<AccountEntity.Command> ref =
|
eventSourcedTestKit.runCommand(AccountEntity.CreateAccount::new);
|
||||||
testKit.spawn(AccountEntity.create("3", PersistenceId.of("Account", "3")));
|
eventSourcedTestKit.runCommand(
|
||||||
TestProbe<AccountEntity.OperationResult> opProbe =
|
(ActorRef<AccountEntity.OperationResult> replyTo) ->
|
||||||
testKit.createTestProbe(AccountEntity.OperationResult.class);
|
new AccountEntity.Deposit(BigDecimal.valueOf(100), replyTo));
|
||||||
ref.tell(new AccountEntity.CreateAccount(opProbe.getRef()));
|
|
||||||
opProbe.expectMessage(AccountEntity.Confirmed.INSTANCE);
|
|
||||||
ref.tell(new AccountEntity.Deposit(BigDecimal.valueOf(100), opProbe.getRef()));
|
|
||||||
opProbe.expectMessage(AccountEntity.Confirmed.INSTANCE);
|
|
||||||
|
|
||||||
TestProbe<AccountEntity.CurrentBalance> getProbe =
|
CommandResultWithReply<
|
||||||
testKit.createTestProbe(AccountEntity.CurrentBalance.class);
|
AccountEntity.Command,
|
||||||
ref.tell(new AccountEntity.GetBalance(getProbe.getRef()));
|
AccountEntity.Event,
|
||||||
assertEquals(
|
AccountEntity.Account,
|
||||||
BigDecimal.valueOf(100),
|
AccountEntity.CurrentBalance>
|
||||||
getProbe.expectMessageClass(AccountEntity.CurrentBalance.class).balance);
|
result = eventSourcedTestKit.runCommand(AccountEntity.GetBalance::new);
|
||||||
|
assertEquals(BigDecimal.valueOf(100), result.reply().balance);
|
||||||
}
|
}
|
||||||
|
|
||||||
// #test
|
|
||||||
// #test-events
|
|
||||||
@Test
|
|
||||||
public void storeEvents() {
|
|
||||||
TestProbe<InmemJournal.Operation> eventProbe = testKit.createTestProbe();
|
|
||||||
testKit
|
|
||||||
.system()
|
|
||||||
.eventStream()
|
|
||||||
.tell(new EventStream.Subscribe<>(InmemJournal.Operation.class, eventProbe.getRef()));
|
|
||||||
|
|
||||||
ActorRef<AccountEntity.Command> ref =
|
|
||||||
testKit.spawn(AccountEntity.create("4", PersistenceId.of("Account", "4")));
|
|
||||||
TestProbe<AccountEntity.OperationResult> probe =
|
|
||||||
testKit.createTestProbe(AccountEntity.OperationResult.class);
|
|
||||||
ref.tell(new AccountEntity.CreateAccount(probe.getRef()));
|
|
||||||
assertEquals(
|
|
||||||
AccountEntity.AccountCreated.INSTANCE,
|
|
||||||
eventProbe.expectMessageClass(InmemJournal.Write.class).event());
|
|
||||||
|
|
||||||
ref.tell(new AccountEntity.Deposit(BigDecimal.valueOf(100), probe.getRef()));
|
|
||||||
assertEquals(
|
|
||||||
BigDecimal.valueOf(100),
|
|
||||||
((AccountEntity.Deposited) eventProbe.expectMessageClass(InmemJournal.Write.class).event())
|
|
||||||
.amount);
|
|
||||||
}
|
|
||||||
// #test
|
|
||||||
// #test-events
|
|
||||||
}
|
}
|
||||||
// #test
|
// #test
|
||||||
|
|
|
||||||
|
|
@ -5,102 +5,76 @@
|
||||||
package docs.akka.cluster.sharding.typed
|
package docs.akka.cluster.sharding.typed
|
||||||
|
|
||||||
//#test
|
//#test
|
||||||
import java.util.UUID
|
import akka.persistence.testkit.scaladsl.EventSourcedBehaviorTestKit
|
||||||
|
import akka.persistence.typed.PersistenceId
|
||||||
import akka.actor.testkit.typed.scaladsl.LogCapturing
|
import akka.actor.testkit.typed.scaladsl.LogCapturing
|
||||||
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
|
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
|
||||||
import akka.persistence.typed.PersistenceId
|
import org.scalatest.BeforeAndAfterEach
|
||||||
import org.scalatest.wordspec.AnyWordSpecLike
|
import org.scalatest.wordspec.AnyWordSpecLike
|
||||||
|
|
||||||
//#test
|
//#test
|
||||||
|
|
||||||
//#test-events
|
|
||||||
import akka.persistence.journal.inmem.InmemJournal
|
|
||||||
import akka.actor.typed.eventstream.EventStream
|
|
||||||
|
|
||||||
//#test-events
|
|
||||||
|
|
||||||
import docs.akka.cluster.sharding.typed.AccountExampleWithEventHandlersInState.AccountEntity
|
import docs.akka.cluster.sharding.typed.AccountExampleWithEventHandlersInState.AccountEntity
|
||||||
|
|
||||||
object AccountExampleDocSpec {
|
|
||||||
val inmemConfig =
|
|
||||||
//#inmem-config
|
|
||||||
"""
|
|
||||||
akka.persistence.journal.plugin = "akka.persistence.journal.inmem"
|
|
||||||
akka.persistence.journal.inmem.test-serialization = on
|
|
||||||
"""
|
|
||||||
//#inmem-config
|
|
||||||
|
|
||||||
val snapshotConfig =
|
|
||||||
//#snapshot-store-config
|
|
||||||
s"""
|
|
||||||
akka.persistence.snapshot-store.plugin = "akka.persistence.snapshot-store.local"
|
|
||||||
akka.persistence.snapshot-store.local.dir = "target/snapshot-${UUID.randomUUID().toString}"
|
|
||||||
"""
|
|
||||||
//#snapshot-store-config
|
|
||||||
}
|
|
||||||
|
|
||||||
//#test
|
//#test
|
||||||
class AccountExampleDocSpec extends ScalaTestWithActorTestKit(s"""
|
//#testkit
|
||||||
akka.persistence.journal.plugin = "akka.persistence.journal.inmem"
|
class AccountExampleDocSpec
|
||||||
akka.persistence.snapshot-store.plugin = "akka.persistence.snapshot-store.local"
|
extends ScalaTestWithActorTestKit(EventSourcedBehaviorTestKit.config)
|
||||||
akka.persistence.snapshot-store.local.dir = "target/snapshot-${UUID.randomUUID().toString}"
|
//#testkit
|
||||||
""") with AnyWordSpecLike with LogCapturing {
|
with AnyWordSpecLike
|
||||||
|
with BeforeAndAfterEach
|
||||||
|
with LogCapturing {
|
||||||
|
|
||||||
|
private val eventSourcedTestKit =
|
||||||
|
EventSourcedBehaviorTestKit[AccountEntity.Command[_], AccountEntity.Event, AccountEntity.Account](
|
||||||
|
system,
|
||||||
|
AccountEntity("1", PersistenceId("Account", "1")))
|
||||||
|
|
||||||
|
override protected def beforeEach(): Unit = {
|
||||||
|
super.beforeEach()
|
||||||
|
eventSourcedTestKit.clear()
|
||||||
|
}
|
||||||
|
|
||||||
"Account" must {
|
"Account" must {
|
||||||
|
|
||||||
|
"be created with zero balance" in {
|
||||||
|
val result = eventSourcedTestKit.runCommand[AccountEntity.OperationResult](AccountEntity.CreateAccount(_))
|
||||||
|
result.reply shouldBe AccountEntity.Confirmed
|
||||||
|
result.event shouldBe AccountEntity.AccountCreated
|
||||||
|
result.stateOfType[AccountEntity.OpenedAccount].balance shouldBe 0
|
||||||
|
}
|
||||||
|
|
||||||
"handle Withdraw" in {
|
"handle Withdraw" in {
|
||||||
val probe = createTestProbe[AccountEntity.OperationResult]()
|
eventSourcedTestKit.runCommand[AccountEntity.OperationResult](AccountEntity.CreateAccount(_))
|
||||||
val ref = spawn(AccountEntity("1", PersistenceId("Account", "1")))
|
|
||||||
ref ! AccountEntity.CreateAccount(probe.ref)
|
val result1 = eventSourcedTestKit.runCommand[AccountEntity.OperationResult](AccountEntity.Deposit(100, _))
|
||||||
probe.expectMessage(AccountEntity.Confirmed)
|
result1.reply shouldBe AccountEntity.Confirmed
|
||||||
ref ! AccountEntity.Deposit(100, probe.ref)
|
result1.event shouldBe AccountEntity.Deposited(100)
|
||||||
probe.expectMessage(AccountEntity.Confirmed)
|
result1.stateOfType[AccountEntity.OpenedAccount].balance shouldBe 100
|
||||||
ref ! AccountEntity.Withdraw(10, probe.ref)
|
|
||||||
probe.expectMessage(AccountEntity.Confirmed)
|
val result2 = eventSourcedTestKit.runCommand[AccountEntity.OperationResult](AccountEntity.Withdraw(10, _))
|
||||||
|
result2.reply shouldBe AccountEntity.Confirmed
|
||||||
|
result2.event shouldBe AccountEntity.Withdrawn(10)
|
||||||
|
result2.stateOfType[AccountEntity.OpenedAccount].balance shouldBe 90
|
||||||
}
|
}
|
||||||
|
|
||||||
"reject Withdraw overdraft" in {
|
"reject Withdraw overdraft" in {
|
||||||
val probe = createTestProbe[AccountEntity.OperationResult]()
|
eventSourcedTestKit.runCommand[AccountEntity.OperationResult](AccountEntity.CreateAccount(_))
|
||||||
val ref = spawn(AccountEntity("2", PersistenceId("Account", "2")))
|
eventSourcedTestKit.runCommand[AccountEntity.OperationResult](AccountEntity.Deposit(100, _))
|
||||||
ref ! AccountEntity.CreateAccount(probe.ref)
|
|
||||||
probe.expectMessage(AccountEntity.Confirmed)
|
val result = eventSourcedTestKit.runCommand[AccountEntity.OperationResult](AccountEntity.Withdraw(110, _))
|
||||||
ref ! AccountEntity.Deposit(100, probe.ref)
|
result.replyOfType[AccountEntity.Rejected]
|
||||||
probe.expectMessage(AccountEntity.Confirmed)
|
result.hasNoEvents shouldBe true
|
||||||
ref ! AccountEntity.Withdraw(110, probe.ref)
|
|
||||||
probe.expectMessageType[AccountEntity.Rejected]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
"handle GetBalance" in {
|
"handle GetBalance" in {
|
||||||
val opProbe = createTestProbe[AccountEntity.OperationResult]()
|
eventSourcedTestKit.runCommand[AccountEntity.OperationResult](AccountEntity.CreateAccount(_))
|
||||||
val ref = spawn(AccountEntity("3", PersistenceId("Account", "3")))
|
eventSourcedTestKit.runCommand[AccountEntity.OperationResult](AccountEntity.Deposit(100, _))
|
||||||
ref ! AccountEntity.CreateAccount(opProbe.ref)
|
|
||||||
opProbe.expectMessage(AccountEntity.Confirmed)
|
|
||||||
ref ! AccountEntity.Deposit(100, opProbe.ref)
|
|
||||||
opProbe.expectMessage(AccountEntity.Confirmed)
|
|
||||||
|
|
||||||
val getProbe = createTestProbe[AccountEntity.CurrentBalance]()
|
val result = eventSourcedTestKit.runCommand[AccountEntity.CurrentBalance](AccountEntity.GetBalance(_))
|
||||||
ref ! AccountEntity.GetBalance(getProbe.ref)
|
result.reply.balance shouldBe 100
|
||||||
getProbe.expectMessage(AccountEntity.CurrentBalance(100))
|
result.hasNoEvents shouldBe true
|
||||||
}
|
}
|
||||||
|
|
||||||
//#test
|
|
||||||
//#test-events
|
|
||||||
"store events" in {
|
|
||||||
val eventProbe = createTestProbe[InmemJournal.Operation]()
|
|
||||||
system.eventStream ! EventStream.Subscribe(eventProbe.ref)
|
|
||||||
|
|
||||||
val probe = createTestProbe[AccountEntity.OperationResult]()
|
|
||||||
val ref = spawn(AccountEntity("4", PersistenceId("Account", "4")))
|
|
||||||
ref ! AccountEntity.CreateAccount(probe.ref)
|
|
||||||
eventProbe.expectMessageType[InmemJournal.Write].event should ===(AccountEntity.AccountCreated)
|
|
||||||
|
|
||||||
ref ! AccountEntity.Deposit(100, probe.ref)
|
|
||||||
probe.expectMessage(AccountEntity.Confirmed)
|
|
||||||
eventProbe.expectMessageType[InmemJournal.Write].event should ===(AccountEntity.Deposited(100))
|
|
||||||
}
|
|
||||||
//#test-events
|
|
||||||
//#test
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//#test
|
//#test
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,38 @@
|
||||||
# Testing
|
# Testing
|
||||||
|
|
||||||
## Dependency
|
## Module info
|
||||||
|
|
||||||
To use Akka Persistence and Actor TestKit, add the module to your project:
|
To use Akka Persistence TestKit, add the module to your project:
|
||||||
|
|
||||||
@@dependency[sbt,Maven,Gradle] {
|
@@dependency[sbt,Maven,Gradle] {
|
||||||
group1=com.typesafe.akka
|
group1=com.typesafe.akka
|
||||||
artifact1=akka-persistence-typed_$scala.binary_version$
|
artifact1=akka-persistence-typed_$scala.binary_version$
|
||||||
version1=$akka.version$
|
version1=$akka.version$
|
||||||
group2=com.typesafe.akka
|
group2=com.typesafe.akka
|
||||||
artifact2=akka-actor-testkit-typed_$scala.binary_version$
|
artifact2=akka-persistence-testkit_$scala.binary_version$
|
||||||
version2=$akka.version$
|
version2=$akka.version$
|
||||||
scope2=test
|
scope2=test
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@project-info{ projectId="akka-persistence-testkit" }
|
||||||
|
|
||||||
## Unit testing
|
## Unit testing
|
||||||
|
|
||||||
Unit testing of `EventSourcedBehavior` can be done with the @ref:[ActorTestKit](testing-async.md)
|
**Note!** The `EventSourcedBehaviorTestKit` is a new feature, api may have changes breaking source compatibility in future versions.
|
||||||
in the same way as other behaviors.
|
|
||||||
|
|
||||||
@ref:[Synchronous behavior testing](testing-sync.md) for `EventSourcedBehavior` is not supported yet, but
|
Unit testing of `EventSourcedBehavior` can be done with the @apidoc[EventSourcedBehaviorTestKit]. It supports running
|
||||||
tracked in @github[issue #23712](#23712).
|
one command at a time and you can assert that the synchronously returned result is as expected. The result contains the
|
||||||
|
events emitted by the command and the new state after applying the events. It also has support for verifying the reply
|
||||||
|
to a command.
|
||||||
|
|
||||||
You need to configure a journal, and the in-memory journal is sufficient for unit testing. To enable the
|
You need to configure the `ActorSystem` with the `EventSourcedBehaviorTestKit.config`. The configuration enables
|
||||||
in-memory journal you need to pass the following configuration to the @scala[`ScalaTestWithActorTestKit`]@java[`TestKitJunitResource`].
|
the in-memory journal and snapshot storage.
|
||||||
|
|
||||||
Scala
|
Scala
|
||||||
: @@snip [AccountExampleDocSpec.scala](/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleDocSpec.scala) { #inmem-config }
|
: @@snip [AccountExampleDocSpec.scala](/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleDocSpec.scala) { #testkit }
|
||||||
|
|
||||||
Java
|
Java
|
||||||
: @@snip [AccountExampleDocTest.java](/akka-cluster-sharding-typed/src/test/java/jdocs/akka/cluster/sharding/typed/AccountExampleDocTest.java) { #inmem-config }
|
: @@snip [AccountExampleDocTest.java](/akka-cluster-sharding-typed/src/test/java/jdocs/akka/cluster/sharding/typed/AccountExampleDocTest.java) { #testkit }
|
||||||
|
|
||||||
The `test-serialization = on` configuration of the `InmemJournal` will verify that persisted events can be serialized and deserialized.
|
|
||||||
|
|
||||||
Optionally you can also configure a snapshot store. To enable the file based snapshot store you need to pass the
|
|
||||||
following configuration to the @scala[`ScalaTestWithActorTestKit`]@java[`TestKitJunitResource`].
|
|
||||||
|
|
||||||
Scala
|
|
||||||
: @@snip [AccountExampleDocSpec.scala](/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleDocSpec.scala) { #snapshot-store-config }
|
|
||||||
|
|
||||||
Java
|
|
||||||
: @@snip [AccountExampleDocTest.java](/akka-cluster-sharding-typed/src/test/java/jdocs/akka/cluster/sharding/typed/AccountExampleDocTest.java) { #snapshot-store-config }
|
|
||||||
|
|
||||||
Then you can `spawn` the `EventSourcedBehavior` and verify the outcome of sending commands to the actor using
|
|
||||||
the facilities of the @ref:[ActorTestKit](testing-async.md).
|
|
||||||
|
|
||||||
A full test for the `AccountEntity`, which is shown in the @ref:[Persistence Style Guide](persistence-style.md), may look like this:
|
A full test for the `AccountEntity`, which is shown in the @ref:[Persistence Style Guide](persistence-style.md), may look like this:
|
||||||
|
|
||||||
|
|
@ -53,21 +42,19 @@ Scala
|
||||||
Java
|
Java
|
||||||
: @@snip [AccountExampleDocTest.java](/akka-cluster-sharding-typed/src/test/java/jdocs/akka/cluster/sharding/typed/AccountExampleDocTest.java) { #test }
|
: @@snip [AccountExampleDocTest.java](/akka-cluster-sharding-typed/src/test/java/jdocs/akka/cluster/sharding/typed/AccountExampleDocTest.java) { #test }
|
||||||
|
|
||||||
Note that each test case is using a different `PersistenceId` to not interfere with each other.
|
Serialization of commands, events and state are verified automatically. The serialization checks can be
|
||||||
|
customized with the `SerializationSettings` when creating the `EventSourcedBehaviorTestKit`. By default,
|
||||||
|
the serialization roundtrip is checked but the equality of the result of the serialization is not checked.
|
||||||
|
`equals` must be implemented @scala[(or using `case class`)] in the commands, events and state if `verifyEquality`
|
||||||
|
is enabled.
|
||||||
|
|
||||||
The @apidoc[akka.persistence.journal.inmem.InmemJournal$] publishes `Write` and `Delete` operations to the
|
To test recovery the `restart` method of the `EventSourcedBehaviorTestKit` can be used. It will restart the
|
||||||
`eventStream`, which makes it possible to verify that the expected events have been emitted and stored by the
|
behavior, which will then recover from stored snapshot and events from previous commands. It's also possible
|
||||||
`EventSourcedBehavior`. You can subscribe to to the `eventStream` with a `TestProbe` like this:
|
to populate the storage with events or simulate failures by using the underlying @apidoc[PersistenceTestKit].
|
||||||
|
|
||||||
Scala
|
|
||||||
: @@snip [AccountExampleDocSpec.scala](/akka-cluster-sharding-typed/src/test/scala/docs/akka/cluster/sharding/typed/AccountExampleDocSpec.scala) { #test-events }
|
|
||||||
|
|
||||||
Java
|
|
||||||
: @@snip [AccountExampleDocTest.java](/akka-cluster-sharding-typed/src/test/java/jdocs/akka/cluster/sharding/typed/AccountExampleDocTest.java) { #test-events }
|
|
||||||
|
|
||||||
## Persistence TestKit
|
## Persistence TestKit
|
||||||
|
|
||||||
**Note!** The testkit is a new feature, api may have changes breaking source compatibility in future versions.
|
**Note!** The `PersistenceTestKit` is a new feature, api may have changes breaking source compatibility in future versions.
|
||||||
|
|
||||||
Persistence testkit allows to check events saved in a storage, emulate storage operations and exceptions.
|
Persistence testkit allows to check events saved in a storage, emulate storage operations and exceptions.
|
||||||
To use the testkit you need to add the following dependency in your project:
|
To use the testkit you need to add the following dependency in your project:
|
||||||
|
|
@ -187,8 +174,10 @@ to the @ref:[reference configuration](../general/configuration-reference.md#conf
|
||||||
|
|
||||||
## Integration testing
|
## Integration testing
|
||||||
|
|
||||||
The in-memory journal and file based snapshot store can be used also for integration style testing of a single
|
`EventSourcedBehavior` actors can be tested with the @ref:[ActorTestKit](testing-async.md) together with
|
||||||
`ActorSystem`, for example when using Cluster Sharding with a single Cluster node.
|
other actors. The in-memory journal and snapshot storage from the @ref:[Persistence TestKit](#persistence-testkit)
|
||||||
|
can be used also for integration style testing of a single `ActorSystem`, for example when using Cluster Sharding
|
||||||
|
with a single Cluster node.
|
||||||
|
|
||||||
For tests that involve more than one Cluster node you have to use another journal and snapshot store.
|
For tests that involve more than one Cluster node you have to use another journal and snapshot store.
|
||||||
While it's possible to use the @ref:[Persistence Plugin Proxy](../persistence-plugins.md#persistence-plugin-proxy)
|
While it's possible to use the @ref:[Persistence Plugin Proxy](../persistence-plugins.md#persistence-plugin-proxy)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Lightbend Inc. <https://www.lightbend.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.persistence.testkit.internal
|
||||||
|
|
||||||
|
import scala.collection.immutable
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
import scala.reflect.ClassTag
|
||||||
|
import scala.util.control.NonFatal
|
||||||
|
|
||||||
|
import akka.actor.testkit.typed.scaladsl.ActorTestKit
|
||||||
|
import akka.actor.testkit.typed.scaladsl.SerializationTestKit
|
||||||
|
import akka.actor.typed.ActorRef
|
||||||
|
import akka.actor.typed.ActorSystem
|
||||||
|
import akka.actor.typed.Behavior
|
||||||
|
import akka.annotation.InternalApi
|
||||||
|
import akka.persistence.testkit.scaladsl.EventSourcedBehaviorTestKit
|
||||||
|
import akka.persistence.testkit.scaladsl.EventSourcedBehaviorTestKit.CommandResult
|
||||||
|
import akka.persistence.testkit.scaladsl.EventSourcedBehaviorTestKit.CommandResultWithReply
|
||||||
|
import akka.persistence.testkit.scaladsl.EventSourcedBehaviorTestKit.RestartResult
|
||||||
|
import akka.persistence.testkit.scaladsl.EventSourcedBehaviorTestKit.SerializationSettings
|
||||||
|
import akka.persistence.testkit.scaladsl.PersistenceTestKit
|
||||||
|
import akka.persistence.typed.PersistenceId
|
||||||
|
import akka.persistence.typed.internal.EventSourcedBehaviorImpl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* INTERNAL API
|
||||||
|
*/
|
||||||
|
@InternalApi private[akka] object EventSourcedBehaviorTestKitImpl {
|
||||||
|
final case class CommandResultImpl[Command, Event, State, Reply](
|
||||||
|
command: Command,
|
||||||
|
events: immutable.Seq[Event],
|
||||||
|
state: State,
|
||||||
|
replyOption: Option[Reply])
|
||||||
|
extends CommandResultWithReply[Command, Event, State, Reply] {
|
||||||
|
|
||||||
|
override def hasNoEvents: Boolean = events.isEmpty
|
||||||
|
|
||||||
|
override def event: Event = {
|
||||||
|
if (events.nonEmpty) events.head else throw new AssertionError("No events")
|
||||||
|
}
|
||||||
|
|
||||||
|
override def eventOfType[E <: Event: ClassTag]: E =
|
||||||
|
ofType(event, "event")
|
||||||
|
|
||||||
|
override def stateOfType[S <: State: ClassTag]: S =
|
||||||
|
ofType(state, "state")
|
||||||
|
|
||||||
|
override def reply: Reply = replyOption.getOrElse(throw new AssertionError("No reply"))
|
||||||
|
|
||||||
|
override def replyOfType[R <: Reply: ClassTag]: R =
|
||||||
|
ofType(reply, "reply")
|
||||||
|
|
||||||
|
// cast with nice error message
|
||||||
|
private def ofType[A: ClassTag](obj: Any, errorParam: String): A = {
|
||||||
|
obj match {
|
||||||
|
case a: A => a
|
||||||
|
case other =>
|
||||||
|
val expectedClass = implicitly[ClassTag[A]].runtimeClass
|
||||||
|
throw new AssertionError(
|
||||||
|
s"Expected $errorParam class [${expectedClass.getName}], " +
|
||||||
|
s"but was [${other.getClass.getName}]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class RestartResultImpl[State](state: State) extends RestartResult[State]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* INTERNAL API
|
||||||
|
*/
|
||||||
|
@InternalApi private[akka] class EventSourcedBehaviorTestKitImpl[Command, Event, State](
|
||||||
|
actorTestKit: ActorTestKit,
|
||||||
|
behavior: Behavior[Command],
|
||||||
|
serializationSettings: SerializationSettings)
|
||||||
|
extends EventSourcedBehaviorTestKit[Command, Event, State] {
|
||||||
|
|
||||||
|
import EventSourcedBehaviorTestKitImpl._
|
||||||
|
|
||||||
|
private def system: ActorSystem[_] = actorTestKit.system
|
||||||
|
|
||||||
|
override val persistenceTestKit: PersistenceTestKit = PersistenceTestKit(system)
|
||||||
|
|
||||||
|
private val probe = actorTestKit.createTestProbe[Any]()
|
||||||
|
private val stateProbe = actorTestKit.createTestProbe[State]()
|
||||||
|
private var actor: ActorRef[Command] = actorTestKit.spawn(behavior)
|
||||||
|
private def internalActor = actor.unsafeUpcast[Any]
|
||||||
|
private val persistenceId: PersistenceId = {
|
||||||
|
internalActor ! EventSourcedBehaviorImpl.GetPersistenceId(probe.ref)
|
||||||
|
try {
|
||||||
|
probe.expectMessageType[PersistenceId]
|
||||||
|
} catch {
|
||||||
|
case NonFatal(_) =>
|
||||||
|
throw new IllegalArgumentException("Only EventSourcedBehavior, or nested EventSourcedBehavior allowed.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val serializationTestKit = new SerializationTestKit(system)
|
||||||
|
|
||||||
|
private var emptyStateVerified = false
|
||||||
|
|
||||||
|
persistenceTestKit.clearByPersistenceId(persistenceId.id)
|
||||||
|
|
||||||
|
override def runCommand(command: Command): CommandResult[Command, Event, State] = {
|
||||||
|
if (serializationSettings.enabled && serializationSettings.verifyCommands)
|
||||||
|
verifySerializationAndThrow(command, "Command")
|
||||||
|
|
||||||
|
if (serializationSettings.enabled && !emptyStateVerified) {
|
||||||
|
internalActor ! EventSourcedBehaviorImpl.GetState(stateProbe.ref)
|
||||||
|
val emptyState = stateProbe.receiveMessage()
|
||||||
|
verifySerializationAndThrow(emptyState, "Empty State")
|
||||||
|
emptyStateVerified = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME we can expand the api of persistenceTestKit to read from storage from a seqNr instead
|
||||||
|
val oldEvents =
|
||||||
|
persistenceTestKit.persistedInStorage(persistenceId.id).map(_.asInstanceOf[Event])
|
||||||
|
|
||||||
|
actor ! command
|
||||||
|
|
||||||
|
internalActor ! EventSourcedBehaviorImpl.GetState(stateProbe.ref)
|
||||||
|
val newState = stateProbe.receiveMessage()
|
||||||
|
|
||||||
|
val newEvents =
|
||||||
|
persistenceTestKit.persistedInStorage(persistenceId.id).map(_.asInstanceOf[Event]).drop(oldEvents.size)
|
||||||
|
|
||||||
|
if (serializationSettings.enabled) {
|
||||||
|
if (serializationSettings.verifyEvents) {
|
||||||
|
newEvents.foreach(verifySerializationAndThrow(_, "Event"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serializationSettings.verifyState)
|
||||||
|
verifySerializationAndThrow(newState, "State")
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandResultImpl[Command, Event, State, Nothing](command, newEvents, newState, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def runCommand[R](creator: ActorRef[R] => Command): CommandResultWithReply[Command, Event, State, R] = {
|
||||||
|
val replyProbe = actorTestKit.createTestProbe[R]()
|
||||||
|
val command = creator(replyProbe.ref)
|
||||||
|
val result = runCommand(command)
|
||||||
|
|
||||||
|
val reply = try {
|
||||||
|
replyProbe.receiveMessage(Duration.Zero)
|
||||||
|
} catch {
|
||||||
|
case NonFatal(_) =>
|
||||||
|
throw new AssertionError(s"Missing expected reply for command [$command].")
|
||||||
|
} finally {
|
||||||
|
replyProbe.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serializationSettings.enabled && serializationSettings.verifyCommands)
|
||||||
|
verifySerializationAndThrow(reply, "Reply")
|
||||||
|
|
||||||
|
CommandResultImpl[Command, Event, State, R](result.command, result.events, result.state, Some(reply))
|
||||||
|
}
|
||||||
|
|
||||||
|
override def restart(): RestartResult[State] = {
|
||||||
|
actorTestKit.stop(actor)
|
||||||
|
actor = actorTestKit.spawn(behavior)
|
||||||
|
internalActor ! EventSourcedBehaviorImpl.GetState(stateProbe.ref)
|
||||||
|
try {
|
||||||
|
val state = stateProbe.receiveMessage()
|
||||||
|
RestartResultImpl(state)
|
||||||
|
} catch {
|
||||||
|
case NonFatal(_) =>
|
||||||
|
throw new IllegalStateException("Could not restart. Maybe exception from event handler. See logs.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def clear(): Unit = {
|
||||||
|
persistenceTestKit.clearByPersistenceId(persistenceId.id)
|
||||||
|
restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
private def verifySerializationAndThrow(obj: Any, errorMessagePrefix: String): Unit = {
|
||||||
|
try {
|
||||||
|
serializationTestKit.verifySerialization(obj, serializationSettings.verifyEquality)
|
||||||
|
} catch {
|
||||||
|
case NonFatal(exc) =>
|
||||||
|
throw new IllegalArgumentException(s"$errorMessagePrefix [$obj] isn't serializable.", exc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,246 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Lightbend Inc. <https://www.lightbend.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.persistence.testkit.javadsl
|
||||||
|
|
||||||
|
import java.util.function.{ Function => JFunction }
|
||||||
|
import java.util.{ List => JList }
|
||||||
|
|
||||||
|
import scala.reflect.ClassTag
|
||||||
|
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
|
||||||
|
import akka.actor.typed.ActorRef
|
||||||
|
import akka.actor.typed.ActorSystem
|
||||||
|
import akka.actor.typed.Behavior
|
||||||
|
import akka.annotation.ApiMayChange
|
||||||
|
import akka.annotation.DoNotInherit
|
||||||
|
import akka.persistence.testkit.scaladsl
|
||||||
|
import akka.util.ccompat.JavaConverters._
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testing of [[akka.persistence.typed.javadsl.EventSourcedBehavior]] implementations.
|
||||||
|
* It supports running one command at a time and you can assert that the synchronously returned result is as expected.
|
||||||
|
* The result contains the events emitted by the command and the new state after applying the events.
|
||||||
|
* It also has support for verifying the reply to a command.
|
||||||
|
*
|
||||||
|
* Serialization of commands, events and state are verified automatically.
|
||||||
|
*/
|
||||||
|
@ApiMayChange
|
||||||
|
object EventSourcedBehaviorTestKit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configuration to be included in the configuration of the `ActorSystem`. Typically used as
|
||||||
|
* constructor parameter to `TestKitJunitResource`. The configuration enables the in-memory
|
||||||
|
* journal and snapshot storage.
|
||||||
|
*/
|
||||||
|
val config: Config = scaladsl.EventSourcedBehaviorTestKit.config
|
||||||
|
|
||||||
|
val enabledSerializationSettings: SerializationSettings = new SerializationSettings(
|
||||||
|
enabled = true,
|
||||||
|
verifyEquality = false,
|
||||||
|
verifyCommands = true,
|
||||||
|
verifyEvents = true,
|
||||||
|
verifyState = true)
|
||||||
|
|
||||||
|
val disabledSerializationSettings: SerializationSettings = new SerializationSettings(
|
||||||
|
enabled = false,
|
||||||
|
verifyEquality = false,
|
||||||
|
verifyCommands = false,
|
||||||
|
verifyEvents = false,
|
||||||
|
verifyState = false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customization of which serialization checks that are performed.
|
||||||
|
* `equals` must be implemented (or using `case class`) when `verifyEquality` is enabled.
|
||||||
|
*/
|
||||||
|
final class SerializationSettings(
|
||||||
|
val enabled: Boolean,
|
||||||
|
val verifyEquality: Boolean,
|
||||||
|
val verifyCommands: Boolean,
|
||||||
|
val verifyEvents: Boolean,
|
||||||
|
val verifyState: Boolean) {
|
||||||
|
|
||||||
|
def withEnabled(value: Boolean): SerializationSettings =
|
||||||
|
copy(enabled = value)
|
||||||
|
|
||||||
|
def withVerifyEquality(value: Boolean): SerializationSettings =
|
||||||
|
copy(verifyEquality = value)
|
||||||
|
|
||||||
|
def withVerifyCommands(value: Boolean): SerializationSettings =
|
||||||
|
copy(verifyCommands = value)
|
||||||
|
|
||||||
|
def withVerifyEvents(value: Boolean): SerializationSettings =
|
||||||
|
copy(verifyEvents = value)
|
||||||
|
|
||||||
|
def withVerifyState(value: Boolean): SerializationSettings =
|
||||||
|
copy(verifyState = value)
|
||||||
|
|
||||||
|
private def copy(
|
||||||
|
enabled: Boolean = this.enabled,
|
||||||
|
verifyEquality: Boolean = this.verifyEquality,
|
||||||
|
verifyCommands: Boolean = this.verifyCommands,
|
||||||
|
verifyEvents: Boolean = this.verifyEvents,
|
||||||
|
verifyState: Boolean = this.verifyState): SerializationSettings = {
|
||||||
|
new SerializationSettings(enabled, verifyEquality, verifyCommands, verifyEvents, verifyState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory method to create a new EventSourcedBehaviorTestKit.
|
||||||
|
*/
|
||||||
|
def create[Command, Event, State](
|
||||||
|
system: ActorSystem[_],
|
||||||
|
behavior: Behavior[Command]): EventSourcedBehaviorTestKit[Command, Event, State] =
|
||||||
|
create(system, behavior, enabledSerializationSettings)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory method to create a new EventSourcedBehaviorTestKit with custom [[SerializationSettings]].
|
||||||
|
*
|
||||||
|
* Note that `equals` must be implemented in the commands, events and state if `verifyEquality` is enabled.
|
||||||
|
*/
|
||||||
|
def create[Command, Event, State](
|
||||||
|
system: ActorSystem[_],
|
||||||
|
behavior: Behavior[Command],
|
||||||
|
serializationSettings: SerializationSettings): EventSourcedBehaviorTestKit[Command, Event, State] = {
|
||||||
|
val scaladslSettings = new scaladsl.EventSourcedBehaviorTestKit.SerializationSettings(
|
||||||
|
enabled = serializationSettings.enabled,
|
||||||
|
verifyEquality = serializationSettings.verifyEquality,
|
||||||
|
verifyCommands = serializationSettings.verifyCommands,
|
||||||
|
verifyEvents = serializationSettings.verifyEvents,
|
||||||
|
verifyState = serializationSettings.verifyState)
|
||||||
|
new EventSourcedBehaviorTestKit(scaladsl.EventSourcedBehaviorTestKit(system, behavior, scaladslSettings))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of running a command.
|
||||||
|
*/
|
||||||
|
@DoNotInherit class CommandResult[Command, Event, State](
|
||||||
|
delegate: scaladsl.EventSourcedBehaviorTestKit.CommandResult[Command, Event, State]) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The command that was run.
|
||||||
|
*/
|
||||||
|
def command: Command =
|
||||||
|
delegate.command
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The events that were emitted by the command, and persisted.
|
||||||
|
* In many cases only one event is emitted and then it's more convenient to use [[CommandResult.event]]
|
||||||
|
* or [[CommandResult.eventOfType]].
|
||||||
|
*/
|
||||||
|
def events: JList[Event] =
|
||||||
|
delegate.events.asJava
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `true` if no events were emitted by the command.
|
||||||
|
*/
|
||||||
|
def hasNoEvents: Boolean =
|
||||||
|
delegate.hasNoEvents
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The first event. It will throw `AssertionError` if there is no event.
|
||||||
|
*/
|
||||||
|
def event: Event =
|
||||||
|
delegate.event
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The first event as a given expected type. It will throw `AssertionError` if there is no event or
|
||||||
|
* if the event is of a different type.
|
||||||
|
*/
|
||||||
|
def eventOfType[E <: Event](eventClass: Class[E]): E =
|
||||||
|
delegate.eventOfType(ClassTag[E](eventClass))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state after applying the events.
|
||||||
|
*/
|
||||||
|
def state: State =
|
||||||
|
delegate.state
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state as a given expected type. It will throw `AssertionError` if the state is of a different type.
|
||||||
|
*/
|
||||||
|
def stateOfType[S <: State](stateClass: Class[S]): S =
|
||||||
|
delegate.stateOfType(ClassTag[S](stateClass))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of running a command with a `ActorRef<R> replyTo`, i.e. the `runCommand` with a
|
||||||
|
* `Function<ActorRef<R>, Command>` parameter.
|
||||||
|
*/
|
||||||
|
final class CommandResultWithReply[Command, Event, State, Reply](
|
||||||
|
delegate: scaladsl.EventSourcedBehaviorTestKit.CommandResultWithReply[Command, Event, State, Reply])
|
||||||
|
extends CommandResult[Command, Event, State](delegate) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reply. It will throw `AssertionError` if there was no reply.
|
||||||
|
*/
|
||||||
|
def reply: Reply =
|
||||||
|
delegate.reply
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reply as a given expected type. It will throw `AssertionError` if there is no reply or
|
||||||
|
* if the reply is of a different type.
|
||||||
|
*/
|
||||||
|
def replyOfType[R <: Reply](replyClass: Class[R]): R =
|
||||||
|
delegate.replyOfType(ClassTag[R](replyClass))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of restarting the behavior.
|
||||||
|
*/
|
||||||
|
final class RestartResult[State](delegate: scaladsl.EventSourcedBehaviorTestKit.RestartResult[State]) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state after recovery.
|
||||||
|
*/
|
||||||
|
def state: State =
|
||||||
|
delegate.state
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiMayChange
|
||||||
|
final class EventSourcedBehaviorTestKit[Command, Event, State](
|
||||||
|
delegate: scaladsl.EventSourcedBehaviorTestKit[Command, Event, State]) {
|
||||||
|
|
||||||
|
import EventSourcedBehaviorTestKit._
|
||||||
|
|
||||||
|
private val _persistenceTestKit = new PersistenceTestKit(delegate.persistenceTestKit)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run one command through the behavior. The returned result contains emitted events and the state
|
||||||
|
* after applying the events.
|
||||||
|
*/
|
||||||
|
def runCommand(command: Command): CommandResult[Command, Event, State] =
|
||||||
|
new CommandResult(delegate.runCommand(command))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run one command with a `replyTo: ActorRef` through the behavior. The returned result contains emitted events,
|
||||||
|
* the state after applying the events, and the reply.
|
||||||
|
*/
|
||||||
|
def runCommand[R](creator: JFunction[ActorRef[R], Command]): CommandResultWithReply[Command, Event, State, R] =
|
||||||
|
new CommandResultWithReply(delegate.runCommand(replyTo => creator.apply(replyTo)))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restart the behavior, which will then recover from stored snapshot and events. Can be used for testing
|
||||||
|
* that the recovery is correct.
|
||||||
|
*/
|
||||||
|
def restart(): RestartResult[State] =
|
||||||
|
new RestartResult(delegate.restart())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the in-memory journal and snapshot storage and restarts the behavior.
|
||||||
|
*/
|
||||||
|
def clear(): Unit =
|
||||||
|
delegate.clear()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The underlying `PersistenceTestKit` for the in-memory journal and snapshot storage.
|
||||||
|
* Can be useful for advanced testing scenarios, such as simulating failures or
|
||||||
|
* populating the journal with events that are used for replay.
|
||||||
|
*/
|
||||||
|
def persistenceTestKit: PersistenceTestKit =
|
||||||
|
_persistenceTestKit
|
||||||
|
}
|
||||||
|
|
@ -19,9 +19,9 @@ import akka.util.ccompat.JavaConverters._
|
||||||
* Class for testing persisted events in persistent actors.
|
* Class for testing persisted events in persistent actors.
|
||||||
*/
|
*/
|
||||||
@ApiMayChange
|
@ApiMayChange
|
||||||
class PersistenceTestKit(system: ActorSystem) {
|
class PersistenceTestKit(scalaTestkit: ScalaTestKit) {
|
||||||
|
|
||||||
private val scalaTestkit = new ScalaTestKit(system)
|
def this(system: ActorSystem) = this(new ScalaTestKit(system))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check that nothing has been saved in the storage.
|
* Check that nothing has been saved in the storage.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,224 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Lightbend Inc. <https://www.lightbend.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.persistence.testkit.scaladsl
|
||||||
|
|
||||||
|
import scala.collection.immutable
|
||||||
|
import scala.reflect.ClassTag
|
||||||
|
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import com.typesafe.config.ConfigFactory
|
||||||
|
|
||||||
|
import akka.actor.testkit.typed.scaladsl.ActorTestKit
|
||||||
|
import akka.actor.typed.ActorRef
|
||||||
|
import akka.actor.typed.ActorSystem
|
||||||
|
import akka.actor.typed.Behavior
|
||||||
|
import akka.annotation.ApiMayChange
|
||||||
|
import akka.annotation.DoNotInherit
|
||||||
|
import akka.persistence.testkit.PersistenceTestKitPlugin
|
||||||
|
import akka.persistence.testkit.internal.EventSourcedBehaviorTestKitImpl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testing of [[akka.persistence.typed.scaladsl.EventSourcedBehavior]] implementations.
|
||||||
|
* It supports running one command at a time and you can assert that the synchronously returned result is as expected.
|
||||||
|
* The result contains the events emitted by the command and the new state after applying the events.
|
||||||
|
* It also has support for verifying the reply to a command.
|
||||||
|
*
|
||||||
|
* Serialization of commands, events and state are verified automatically.
|
||||||
|
*/
|
||||||
|
@ApiMayChange
|
||||||
|
object EventSourcedBehaviorTestKit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configuration to be included in the configuration of the `ActorSystem`. Typically used as
|
||||||
|
* constructor parameter to `ScalaTestWithActorTestKit`. The configuration enables the in-memory
|
||||||
|
* journal and snapshot storage.
|
||||||
|
*/
|
||||||
|
val config: Config = ConfigFactory.parseString("""
|
||||||
|
akka.persistence.testkit.events.serialize = off
|
||||||
|
""").withFallback(PersistenceTestKitPlugin.config)
|
||||||
|
|
||||||
|
object SerializationSettings {
|
||||||
|
val enabled: SerializationSettings = new SerializationSettings(
|
||||||
|
enabled = true,
|
||||||
|
verifyEquality = false,
|
||||||
|
verifyCommands = true,
|
||||||
|
verifyEvents = true,
|
||||||
|
verifyState = true)
|
||||||
|
|
||||||
|
val disabled: SerializationSettings = new SerializationSettings(
|
||||||
|
enabled = false,
|
||||||
|
verifyEquality = false,
|
||||||
|
verifyCommands = false,
|
||||||
|
verifyEvents = false,
|
||||||
|
verifyState = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customization of which serialization checks that are performed.
|
||||||
|
* `equals` must be implemented (or using `case class`) when `verifyEquality` is enabled.
|
||||||
|
*/
|
||||||
|
final class SerializationSettings private[akka] (
|
||||||
|
val enabled: Boolean,
|
||||||
|
val verifyEquality: Boolean,
|
||||||
|
val verifyCommands: Boolean,
|
||||||
|
val verifyEvents: Boolean,
|
||||||
|
val verifyState: Boolean) {
|
||||||
|
|
||||||
|
def withEnabled(value: Boolean): SerializationSettings =
|
||||||
|
copy(enabled = value)
|
||||||
|
|
||||||
|
def withVerifyEquality(value: Boolean): SerializationSettings =
|
||||||
|
copy(verifyEquality = value)
|
||||||
|
|
||||||
|
def withVerifyCommands(value: Boolean): SerializationSettings =
|
||||||
|
copy(verifyCommands = value)
|
||||||
|
|
||||||
|
def withVerifyEvents(value: Boolean): SerializationSettings =
|
||||||
|
copy(verifyEvents = value)
|
||||||
|
|
||||||
|
def withVerifyState(value: Boolean): SerializationSettings =
|
||||||
|
copy(verifyState = value)
|
||||||
|
|
||||||
|
private def copy(
|
||||||
|
enabled: Boolean = this.enabled,
|
||||||
|
verifyEquality: Boolean = this.verifyEquality,
|
||||||
|
verifyCommands: Boolean = this.verifyCommands,
|
||||||
|
verifyEvents: Boolean = this.verifyEvents,
|
||||||
|
verifyState: Boolean = this.verifyState): SerializationSettings = {
|
||||||
|
new SerializationSettings(enabled, verifyEquality, verifyCommands, verifyEvents, verifyState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory method to create a new EventSourcedBehaviorTestKit.
|
||||||
|
*/
|
||||||
|
def apply[Command, Event, State](
|
||||||
|
system: ActorSystem[_],
|
||||||
|
behavior: Behavior[Command]): EventSourcedBehaviorTestKit[Command, Event, State] =
|
||||||
|
apply(system, behavior, SerializationSettings.enabled)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory method to create a new EventSourcedBehaviorTestKit with custom [[SerializationSettings]].
|
||||||
|
*
|
||||||
|
* Note that `equals` must be implemented (or using `case class`) in the commands, events and state if
|
||||||
|
* `verifyEquality` is enabled.
|
||||||
|
*/
|
||||||
|
def apply[Command, Event, State](
|
||||||
|
system: ActorSystem[_],
|
||||||
|
behavior: Behavior[Command],
|
||||||
|
serializationSettings: SerializationSettings): EventSourcedBehaviorTestKit[Command, Event, State] =
|
||||||
|
new EventSourcedBehaviorTestKitImpl(ActorTestKit(system), behavior, serializationSettings)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of running a command.
|
||||||
|
*/
|
||||||
|
@DoNotInherit trait CommandResult[Command, Event, State] {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The command that was run.
|
||||||
|
*/
|
||||||
|
def command: Command
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The events that were emitted by the command, and persisted.
|
||||||
|
* In many cases only one event is emitted and then it's more convenient to use [[CommandResult.event]]
|
||||||
|
* or [[CommandResult.eventOfType]].
|
||||||
|
*/
|
||||||
|
def events: immutable.Seq[Event]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `true` if no events were emitted by the command.
|
||||||
|
*/
|
||||||
|
def hasNoEvents: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The first event. It will throw `AssertionError` if there is no event.
|
||||||
|
*/
|
||||||
|
def event: Event
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The first event as a given expected type. It will throw `AssertionError` if there is no event or
|
||||||
|
* if the event is of a different type.
|
||||||
|
*/
|
||||||
|
def eventOfType[E <: Event: ClassTag]: E
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state after applying the events.
|
||||||
|
*/
|
||||||
|
def state: State
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state as a given expected type. It will throw `AssertionError` if the state is of a different type.
|
||||||
|
*/
|
||||||
|
def stateOfType[S <: State: ClassTag]: S
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of running a command with a `replyTo: ActorRef[R]`, i.e. the `runCommand` with a
|
||||||
|
* `ActorRef[R] => Command` parameter.
|
||||||
|
*/
|
||||||
|
@DoNotInherit trait CommandResultWithReply[Command, Event, State, Reply]
|
||||||
|
extends CommandResult[Command, Event, State] {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reply. It will throw `AssertionError` if there was no reply.
|
||||||
|
*/
|
||||||
|
def reply: Reply
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reply as a given expected type. It will throw `AssertionError` if there is no reply or
|
||||||
|
* if the reply is of a different type.
|
||||||
|
*/
|
||||||
|
def replyOfType[R <: Reply: ClassTag]: R
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of restarting the behavior.
|
||||||
|
*/
|
||||||
|
@DoNotInherit trait RestartResult[State] {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state after recovery.
|
||||||
|
*/
|
||||||
|
def state: State
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiMayChange
|
||||||
|
@DoNotInherit trait EventSourcedBehaviorTestKit[Command, Event, State] {
|
||||||
|
|
||||||
|
import EventSourcedBehaviorTestKit._
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run one command through the behavior. The returned result contains emitted events and the state
|
||||||
|
* after applying the events.
|
||||||
|
*/
|
||||||
|
def runCommand(command: Command): CommandResult[Command, Event, State]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run one command with a `replyTo: ActorRef[R]` through the behavior. The returned result contains emitted events,
|
||||||
|
* the state after applying the events, and the reply.
|
||||||
|
*/
|
||||||
|
def runCommand[R](creator: ActorRef[R] => Command): CommandResultWithReply[Command, Event, State, R]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restart the behavior, which will then recover from stored snapshot and events. Can be used for testing
|
||||||
|
* that the recovery is correct.
|
||||||
|
*/
|
||||||
|
def restart(): RestartResult[State]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the in-memory journal and snapshot storage and restarts the behavior.
|
||||||
|
*/
|
||||||
|
def clear(): Unit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The underlying `PersistenceTestKit` for the in-memory journal and snapshot storage.
|
||||||
|
* Can be useful for advanced testing scenarios, such as simulating failures or
|
||||||
|
* populating the journal with events that are used for replay.
|
||||||
|
*/
|
||||||
|
def persistenceTestKit: PersistenceTestKit
|
||||||
|
}
|
||||||
|
|
@ -10,12 +10,19 @@ import scala.util.Try
|
||||||
|
|
||||||
import com.typesafe.config.Config
|
import com.typesafe.config.Config
|
||||||
|
|
||||||
import akka.actor.{ ActorSystem, ExtendedActorSystem, Extension, ExtensionId }
|
import akka.actor.ActorSystem
|
||||||
|
import akka.actor.ClassicActorSystemProvider
|
||||||
|
import akka.actor.ExtendedActorSystem
|
||||||
|
import akka.actor.Extension
|
||||||
|
import akka.actor.ExtensionId
|
||||||
import akka.actor.typed.{ ActorSystem => TypedActorSystem }
|
import akka.actor.typed.{ ActorSystem => TypedActorSystem }
|
||||||
import akka.annotation.ApiMayChange
|
import akka.annotation.ApiMayChange
|
||||||
import akka.persistence.{ Persistence, PersistentRepr, SnapshotMetadata }
|
import akka.persistence.Persistence
|
||||||
|
import akka.persistence.PersistentRepr
|
||||||
|
import akka.persistence.SnapshotMetadata
|
||||||
import akka.persistence.testkit._
|
import akka.persistence.testkit._
|
||||||
import akka.persistence.testkit.internal.{ InMemStorageExtension, SnapshotStorageEmulatorExtension }
|
import akka.persistence.testkit.internal.InMemStorageExtension
|
||||||
|
import akka.persistence.testkit.internal.SnapshotStorageEmulatorExtension
|
||||||
import akka.testkit.TestProbe
|
import akka.testkit.TestProbe
|
||||||
|
|
||||||
private[testkit] trait CommonTestKitOps[S, P] extends ClearOps with PolicyOpsTestKit[P] {
|
private[testkit] trait CommonTestKitOps[S, P] extends ClearOps with PolicyOpsTestKit[P] {
|
||||||
|
|
@ -293,7 +300,7 @@ private[testkit] trait PersistenceTestKitOps[S, P]
|
||||||
/**
|
/**
|
||||||
* Persist `snapshots` into storage in order.
|
* Persist `snapshots` into storage in order.
|
||||||
*/
|
*/
|
||||||
def persistForRecovery(persistenceId: String, snapshots: immutable.Seq[Any]): Unit
|
def persistForRecovery(persistenceId: String, events: immutable.Seq[Any]): Unit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve all snapshots saved in storage by persistence id.
|
* Retrieve all snapshots saved in storage by persistence id.
|
||||||
|
|
@ -480,9 +487,9 @@ class PersistenceTestKit(system: ActorSystem)
|
||||||
override def failNextNDeletes(persistenceId: String, n: Int, cause: Throwable): Unit =
|
override def failNextNDeletes(persistenceId: String, n: Int, cause: Throwable): Unit =
|
||||||
failNextNOpsCond((pid, op) => pid == persistenceId && op.isInstanceOf[DeleteEvents], n, cause)
|
failNextNOpsCond((pid, op) => pid == persistenceId && op.isInstanceOf[DeleteEvents], n, cause)
|
||||||
|
|
||||||
def persistForRecovery(persistenceId: String, snapshots: immutable.Seq[Any]): Unit = {
|
def persistForRecovery(persistenceId: String, events: immutable.Seq[Any]): Unit = {
|
||||||
storage.addAny(persistenceId, snapshots)
|
storage.addAny(persistenceId, events)
|
||||||
addToIndex(persistenceId, snapshots.size)
|
addToIndex(persistenceId, events.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
def persistedInStorage(persistenceId: String): immutable.Seq[Any] =
|
def persistedInStorage(persistenceId: String): immutable.Seq[Any] =
|
||||||
|
|
@ -494,9 +501,7 @@ class PersistenceTestKit(system: ActorSystem)
|
||||||
@ApiMayChange
|
@ApiMayChange
|
||||||
object PersistenceTestKit {
|
object PersistenceTestKit {
|
||||||
|
|
||||||
def apply(system: ActorSystem): PersistenceTestKit = new PersistenceTestKit(system)
|
def apply(system: ClassicActorSystemProvider): PersistenceTestKit = new PersistenceTestKit(system.classicSystem)
|
||||||
|
|
||||||
def apply(system: TypedActorSystem[_]): PersistenceTestKit = apply(system.classicSystem)
|
|
||||||
|
|
||||||
object Settings extends ExtensionId[Settings] {
|
object Settings extends ExtensionId[Settings] {
|
||||||
|
|
||||||
|
|
|
||||||
31
akka-persistence-testkit/src/test/resources/logback-test.xml
Normal file
31
akka-persistence-testkit/src/test/resources/logback-test.xml
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration>
|
||||||
|
<!-- Silence initial setup logging from Logback -->
|
||||||
|
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
|
||||||
|
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%date{ISO8601} %-5level %logger %marker - %msg MDC: {%mdc}%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Logging from tests are silenced by this appender. When there is a test failure
|
||||||
|
the captured logging events are flushed to the appenders defined for the
|
||||||
|
akka.actor.testkit.typed.internal.CapturingAppenderDelegate logger.
|
||||||
|
-->
|
||||||
|
<appender name="CapturingAppender" class="akka.actor.testkit.typed.internal.CapturingAppender" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The appenders defined for this CapturingAppenderDelegate logger are used
|
||||||
|
when there is a test failure and all logging events from the test are
|
||||||
|
flushed to these appenders.
|
||||||
|
-->
|
||||||
|
<logger name="akka.actor.testkit.typed.internal.CapturingAppenderDelegate" >
|
||||||
|
<appender-ref ref="STDOUT"/>
|
||||||
|
</logger>
|
||||||
|
|
||||||
|
<root level="TRACE">
|
||||||
|
<appender-ref ref="CapturingAppender"/>
|
||||||
|
</root>
|
||||||
|
</configuration>
|
||||||
|
|
@ -0,0 +1,302 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Lightbend Inc. <https://www.lightbend.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.persistence.testkit.scaladsl
|
||||||
|
|
||||||
|
import java.io.NotSerializableException
|
||||||
|
|
||||||
|
import org.scalatest.wordspec.AnyWordSpecLike
|
||||||
|
|
||||||
|
import akka.Done
|
||||||
|
import akka.actor.testkit.typed.TestException
|
||||||
|
import akka.actor.testkit.typed.scaladsl.LogCapturing
|
||||||
|
import akka.actor.testkit.typed.scaladsl.LoggingTestKit
|
||||||
|
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
|
||||||
|
import akka.actor.typed.ActorRef
|
||||||
|
import akka.actor.typed.Behavior
|
||||||
|
import akka.actor.typed.scaladsl.ActorContext
|
||||||
|
import akka.actor.typed.scaladsl.Behaviors
|
||||||
|
import akka.persistence.testkit.scaladsl.EventSourcedBehaviorTestKitSpec.TestCounter.NotSerializableState
|
||||||
|
import akka.persistence.typed.PersistenceId
|
||||||
|
import akka.persistence.typed.internal.JournalFailureException
|
||||||
|
import akka.persistence.typed.scaladsl.Effect
|
||||||
|
import akka.persistence.typed.scaladsl.EventSourcedBehavior
|
||||||
|
import akka.serialization.DisabledJavaSerializer
|
||||||
|
import akka.serialization.jackson.CborSerializable
|
||||||
|
import akka.util.unused
|
||||||
|
|
||||||
|
object EventSourcedBehaviorTestKitSpec {
|
||||||
|
|
||||||
|
object TestCounter {
|
||||||
|
sealed trait Command
|
||||||
|
case object Increment extends Command with CborSerializable
|
||||||
|
final case class IncrementWithConfirmation(replyTo: ActorRef[Done]) extends Command with CborSerializable
|
||||||
|
case class IncrementSeveral(n: Int) extends Command with CborSerializable
|
||||||
|
final case class GetValue(replyTo: ActorRef[State]) extends Command with CborSerializable
|
||||||
|
|
||||||
|
sealed trait Event
|
||||||
|
final case class Incremented(delta: Int) extends Event with CborSerializable
|
||||||
|
|
||||||
|
sealed trait State
|
||||||
|
final case class RealState(value: Int, history: Vector[Int]) extends State with CborSerializable
|
||||||
|
|
||||||
|
case object IncrementWithNotSerializableEvent extends Command with CborSerializable
|
||||||
|
final case class NotSerializableEvent(delta: Int) extends Event
|
||||||
|
|
||||||
|
case object IncrementWithNotSerializableState extends Command with CborSerializable
|
||||||
|
final case class IncrementedWithNotSerializableState(delta: Int) extends Event with CborSerializable
|
||||||
|
final case class NotSerializableState(value: Int, history: Vector[Int]) extends State
|
||||||
|
|
||||||
|
case object NotSerializableCommand extends Command
|
||||||
|
|
||||||
|
final case class IncrementWithNotSerializableReply(replyTo: ActorRef[NotSerializableReply.type])
|
||||||
|
extends Command
|
||||||
|
with CborSerializable
|
||||||
|
object NotSerializableReply
|
||||||
|
|
||||||
|
def apply(persistenceId: PersistenceId): Behavior[Command] =
|
||||||
|
apply(persistenceId, RealState(0, Vector.empty))
|
||||||
|
|
||||||
|
def apply(persistenceId: PersistenceId, emptyState: State): Behavior[Command] =
|
||||||
|
Behaviors.setup(ctx => counter(ctx, persistenceId, emptyState))
|
||||||
|
|
||||||
|
private def counter(
|
||||||
|
@unused ctx: ActorContext[Command],
|
||||||
|
persistenceId: PersistenceId,
|
||||||
|
emptyState: State): EventSourcedBehavior[Command, Event, State] = {
|
||||||
|
EventSourcedBehavior.withEnforcedReplies[Command, Event, State](
|
||||||
|
persistenceId,
|
||||||
|
emptyState,
|
||||||
|
commandHandler = (state, command) =>
|
||||||
|
command match {
|
||||||
|
case Increment =>
|
||||||
|
Effect.persist(Incremented(1)).thenNoReply()
|
||||||
|
|
||||||
|
case IncrementWithConfirmation(replyTo) =>
|
||||||
|
Effect.persist(Incremented(1)).thenReply(replyTo)(_ => Done)
|
||||||
|
|
||||||
|
case IncrementSeveral(n: Int) =>
|
||||||
|
val events = (1 to n).map(_ => Incremented(1))
|
||||||
|
Effect.persist(events).thenNoReply()
|
||||||
|
|
||||||
|
case IncrementWithNotSerializableEvent =>
|
||||||
|
Effect.persist(NotSerializableEvent(1)).thenNoReply()
|
||||||
|
|
||||||
|
case IncrementWithNotSerializableState =>
|
||||||
|
Effect.persist(IncrementedWithNotSerializableState(1)).thenNoReply()
|
||||||
|
|
||||||
|
case IncrementWithNotSerializableReply(replyTo) =>
|
||||||
|
Effect.persist(Incremented(1)).thenReply(replyTo)(_ => NotSerializableReply)
|
||||||
|
|
||||||
|
case NotSerializableCommand =>
|
||||||
|
Effect.noReply
|
||||||
|
|
||||||
|
case GetValue(replyTo) =>
|
||||||
|
Effect.reply(replyTo)(state)
|
||||||
|
|
||||||
|
},
|
||||||
|
eventHandler = {
|
||||||
|
case (RealState(value, history), Incremented(delta)) =>
|
||||||
|
if (delta <= 0)
|
||||||
|
throw new IllegalStateException("Delta must be positive")
|
||||||
|
RealState(value + delta, history :+ value)
|
||||||
|
case (RealState(value, history), NotSerializableEvent(delta)) =>
|
||||||
|
RealState(value + delta, history :+ value)
|
||||||
|
case (RealState(value, history), IncrementedWithNotSerializableState(delta)) =>
|
||||||
|
NotSerializableState(value + delta, history :+ value)
|
||||||
|
case (state: NotSerializableState, _) =>
|
||||||
|
throw new IllegalStateException(state.toString)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventSourcedBehaviorTestKitSpec
|
||||||
|
extends ScalaTestWithActorTestKit(EventSourcedBehaviorTestKit.config)
|
||||||
|
with AnyWordSpecLike
|
||||||
|
with LogCapturing {
|
||||||
|
import EventSourcedBehaviorTestKitSpec._
|
||||||
|
|
||||||
|
private val persistenceId = PersistenceId.ofUniqueId("test")
|
||||||
|
private val behavior = TestCounter(persistenceId)
|
||||||
|
|
||||||
|
private def createTestKit() = {
|
||||||
|
EventSourcedBehaviorTestKit[TestCounter.Command, TestCounter.Event, TestCounter.State](system, behavior)
|
||||||
|
}
|
||||||
|
|
||||||
|
"EventSourcedBehaviorTestKit" must {
|
||||||
|
|
||||||
|
"run commands" in {
|
||||||
|
val eventSourcedTestKit = createTestKit()
|
||||||
|
|
||||||
|
val result1 = eventSourcedTestKit.runCommand(TestCounter.Increment)
|
||||||
|
result1.event should ===(TestCounter.Incremented(1))
|
||||||
|
result1.state should ===(TestCounter.RealState(1, Vector(0)))
|
||||||
|
|
||||||
|
val result2 = eventSourcedTestKit.runCommand(TestCounter.Increment)
|
||||||
|
result2.event should ===(TestCounter.Incremented(1))
|
||||||
|
result2.state should ===(TestCounter.RealState(2, Vector(0, 1)))
|
||||||
|
|
||||||
|
result2.eventOfType[TestCounter.Incremented].delta should ===(1)
|
||||||
|
intercept[AssertionError] {
|
||||||
|
// wrong event type
|
||||||
|
result2.eventOfType[TestCounter.NotSerializableEvent].delta should ===(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"run command emitting several events" in {
|
||||||
|
val eventSourcedTestKit = createTestKit()
|
||||||
|
|
||||||
|
val result = eventSourcedTestKit.runCommand(TestCounter.IncrementSeveral(3))
|
||||||
|
result.events should ===(List(TestCounter.Incremented(1), TestCounter.Incremented(1), TestCounter.Incremented(1)))
|
||||||
|
result.state should ===(TestCounter.RealState(3, Vector(0, 1, 2)))
|
||||||
|
}
|
||||||
|
|
||||||
|
"run commands with reply" in {
|
||||||
|
val eventSourcedTestKit = createTestKit()
|
||||||
|
|
||||||
|
val result1 = eventSourcedTestKit.runCommand[Done](TestCounter.IncrementWithConfirmation(_))
|
||||||
|
result1.event should ===(TestCounter.Incremented(1))
|
||||||
|
result1.state should ===(TestCounter.RealState(1, Vector(0)))
|
||||||
|
result1.reply should ===(Done)
|
||||||
|
|
||||||
|
val result2 = eventSourcedTestKit.runCommand[Done](TestCounter.IncrementWithConfirmation(_))
|
||||||
|
result2.event should ===(TestCounter.Incremented(1))
|
||||||
|
result2.state should ===(TestCounter.RealState(2, Vector(0, 1)))
|
||||||
|
result2.reply should ===(Done)
|
||||||
|
}
|
||||||
|
|
||||||
|
"detect missing reply" in {
|
||||||
|
val eventSourcedTestKit = createTestKit()
|
||||||
|
|
||||||
|
intercept[AssertionError] {
|
||||||
|
eventSourcedTestKit.runCommand[Done](_ => TestCounter.Increment)
|
||||||
|
}.getMessage should include("Missing expected reply")
|
||||||
|
}
|
||||||
|
|
||||||
|
"run command with reply that is not emitting events" in {
|
||||||
|
val eventSourcedTestKit = createTestKit()
|
||||||
|
|
||||||
|
eventSourcedTestKit.runCommand(TestCounter.Increment)
|
||||||
|
val result = eventSourcedTestKit.runCommand[TestCounter.State](TestCounter.GetValue(_))
|
||||||
|
result.hasNoEvents should ===(true)
|
||||||
|
intercept[AssertionError] {
|
||||||
|
result.event
|
||||||
|
}
|
||||||
|
result.state should ===(TestCounter.RealState(1, Vector(0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
"detect non-serializable events" in {
|
||||||
|
val eventSourcedTestKit = createTestKit()
|
||||||
|
|
||||||
|
val exc = intercept[IllegalArgumentException] {
|
||||||
|
eventSourcedTestKit.runCommand(TestCounter.IncrementWithNotSerializableEvent)
|
||||||
|
}
|
||||||
|
(exc.getMessage should startWith).regex("Event.*isn't serializable")
|
||||||
|
exc.getCause.getClass should ===(classOf[DisabledJavaSerializer.JavaSerializationException])
|
||||||
|
}
|
||||||
|
|
||||||
|
"detect non-serializable state" in {
|
||||||
|
val eventSourcedTestKit = createTestKit()
|
||||||
|
|
||||||
|
val exc = intercept[IllegalArgumentException] {
|
||||||
|
eventSourcedTestKit.runCommand(TestCounter.IncrementWithNotSerializableState)
|
||||||
|
}
|
||||||
|
(exc.getMessage should include).regex("State.*isn't serializable")
|
||||||
|
exc.getCause.getClass should ===(classOf[DisabledJavaSerializer.JavaSerializationException])
|
||||||
|
}
|
||||||
|
|
||||||
|
"detect non-serializable empty state" in {
|
||||||
|
val eventSourcedTestKit =
|
||||||
|
EventSourcedBehaviorTestKit[TestCounter.Command, TestCounter.Event, TestCounter.State](
|
||||||
|
system,
|
||||||
|
TestCounter(persistenceId, NotSerializableState(0, Vector.empty)))
|
||||||
|
|
||||||
|
val exc = intercept[IllegalArgumentException] {
|
||||||
|
eventSourcedTestKit.runCommand(TestCounter.Increment)
|
||||||
|
}
|
||||||
|
(exc.getMessage should include).regex("Empty State.*isn't serializable")
|
||||||
|
exc.getCause.getClass should ===(classOf[DisabledJavaSerializer.JavaSerializationException])
|
||||||
|
}
|
||||||
|
|
||||||
|
"detect non-serializable command" in {
|
||||||
|
val eventSourcedTestKit = createTestKit()
|
||||||
|
|
||||||
|
val exc = intercept[IllegalArgumentException] {
|
||||||
|
eventSourcedTestKit.runCommand(TestCounter.NotSerializableCommand)
|
||||||
|
}
|
||||||
|
(exc.getMessage should include).regex("Command.*isn't serializable")
|
||||||
|
exc.getCause.getClass should ===(classOf[DisabledJavaSerializer.JavaSerializationException])
|
||||||
|
}
|
||||||
|
|
||||||
|
"detect non-serializable reply" in {
|
||||||
|
val eventSourcedTestKit = createTestKit()
|
||||||
|
|
||||||
|
val exc = intercept[IllegalArgumentException] {
|
||||||
|
eventSourcedTestKit.runCommand(TestCounter.IncrementWithNotSerializableReply(_))
|
||||||
|
}
|
||||||
|
(exc.getMessage should include).regex("Reply.*isn't serializable")
|
||||||
|
exc.getCause.getClass should ===(classOf[NotSerializableException])
|
||||||
|
}
|
||||||
|
|
||||||
|
"support test of replay" in {
|
||||||
|
val eventSourcedTestKit = createTestKit()
|
||||||
|
|
||||||
|
eventSourcedTestKit.runCommand(TestCounter.Increment)
|
||||||
|
eventSourcedTestKit.runCommand(TestCounter.Increment)
|
||||||
|
val expectedState = TestCounter.RealState(3, Vector(0, 1, 2))
|
||||||
|
eventSourcedTestKit.runCommand(TestCounter.Increment).state should ===(expectedState)
|
||||||
|
}
|
||||||
|
|
||||||
|
"support test of replay from stored events" in {
|
||||||
|
val eventSourcedTestKit = createTestKit()
|
||||||
|
eventSourcedTestKit.persistenceTestKit
|
||||||
|
.persistForRecovery(persistenceId.id, List(TestCounter.Incremented(1), TestCounter.Incremented(1)))
|
||||||
|
eventSourcedTestKit.restart().state should ===(TestCounter.RealState(2, Vector(0, 1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
"support test of invalid events" in {
|
||||||
|
val eventSourcedTestKit = createTestKit()
|
||||||
|
eventSourcedTestKit.persistenceTestKit
|
||||||
|
.persistForRecovery(persistenceId.id, List(TestCounter.Incremented(1), TestCounter.Incremented(-1)))
|
||||||
|
intercept[IllegalStateException] {
|
||||||
|
eventSourcedTestKit.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
"only allow EventSourcedBehavior" in {
|
||||||
|
intercept[IllegalArgumentException] {
|
||||||
|
EventSourcedBehaviorTestKit[TestCounter.Command, TestCounter.Event, TestCounter.State](
|
||||||
|
system,
|
||||||
|
Behaviors.empty[TestCounter.Command])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"support test of failures" in {
|
||||||
|
val eventSourcedTestKit = createTestKit()
|
||||||
|
eventSourcedTestKit.runCommand(TestCounter.Increment)
|
||||||
|
eventSourcedTestKit.runCommand(TestCounter.Increment)
|
||||||
|
eventSourcedTestKit.persistenceTestKit.failNextPersisted(persistenceId.id, TestException("DB err"))
|
||||||
|
LoggingTestKit.error[JournalFailureException].expect {
|
||||||
|
intercept[AssertionError] {
|
||||||
|
eventSourcedTestKit.runCommand(TestCounter.Increment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventSourcedTestKit.restart().state should ===(TestCounter.RealState(2, Vector(0, 1)))
|
||||||
|
eventSourcedTestKit.runCommand(TestCounter.Increment).state should ===(TestCounter.RealState(3, Vector(0, 1, 2)))
|
||||||
|
}
|
||||||
|
|
||||||
|
"have possibility to clear" in {
|
||||||
|
val eventSourcedTestKit = createTestKit()
|
||||||
|
eventSourcedTestKit.runCommand(TestCounter.Increment)
|
||||||
|
eventSourcedTestKit.runCommand(TestCounter.Increment).state should ===(TestCounter.RealState(2, Vector(0, 1)))
|
||||||
|
|
||||||
|
eventSourcedTestKit.clear()
|
||||||
|
eventSourcedTestKit.runCommand(TestCounter.Increment).state should ===(TestCounter.RealState(1, Vector(0)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
# 23712 Internals for EventSourcedBehaviorTestKit
|
||||||
|
ProblemFilters.exclude[IncompatibleResultTypeProblem]("akka.persistence.typed.internal.RequestingRecoveryPermit.stashInternal")
|
||||||
|
ProblemFilters.exclude[IncompatibleResultTypeProblem]("akka.persistence.typed.internal.ReplayingSnapshot.stashInternal")
|
||||||
|
ProblemFilters.exclude[IncompatibleResultTypeProblem]("akka.persistence.typed.internal.ReplayingEvents.stashInternal")
|
||||||
|
ProblemFilters.exclude[IncompatibleResultTypeProblem]("akka.persistence.typed.internal.StashManagement.stashInternal")
|
||||||
|
ProblemFilters.exclude[IncompatibleResultTypeProblem]("akka.persistence.typed.internal.Running.stashInternal")
|
||||||
|
|
@ -8,6 +8,7 @@ import java.util.UUID
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
import akka.actor.typed
|
import akka.actor.typed
|
||||||
|
import akka.actor.typed.ActorRef
|
||||||
import akka.actor.typed.BackoffSupervisorStrategy
|
import akka.actor.typed.BackoffSupervisorStrategy
|
||||||
import akka.actor.typed.Behavior
|
import akka.actor.typed.Behavior
|
||||||
import akka.actor.typed.BehaviorInterceptor
|
import akka.actor.typed.BehaviorInterceptor
|
||||||
|
|
@ -57,6 +58,17 @@ private[akka] object EventSourcedBehaviorImpl {
|
||||||
}
|
}
|
||||||
final case class WriterIdentity(instanceId: Int, writerUuid: String)
|
final case class WriterIdentity(instanceId: Int, writerUuid: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by EventSourcedBehaviorTestKit to retrieve the `persistenceId`.
|
||||||
|
*/
|
||||||
|
final case class GetPersistenceId(replyTo: ActorRef[PersistenceId]) extends Signal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by EventSourcedBehaviorTestKit to retrieve the state.
|
||||||
|
* Can't be a Signal because those are not stashed.
|
||||||
|
*/
|
||||||
|
final case class GetState[State](replyTo: ActorRef[State]) extends InternalProtocol
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@InternalApi
|
@InternalApi
|
||||||
|
|
@ -113,6 +125,7 @@ private[akka] final case class EventSourcedBehaviorImpl[Command, Event, State](
|
||||||
ctx.log.debug("Events successfully deleted to sequence number [{}].", toSequenceNr)
|
ctx.log.debug("Events successfully deleted to sequence number [{}].", toSequenceNr)
|
||||||
case (_, DeleteEventsFailed(toSequenceNr, failure)) =>
|
case (_, DeleteEventsFailed(toSequenceNr, failure)) =>
|
||||||
ctx.log.warn2("Failed to delete events to sequence number [{}] due to: {}", toSequenceNr, failure.getMessage)
|
ctx.log.warn2("Failed to delete events to sequence number [{}] due to: {}", toSequenceNr, failure.getMessage)
|
||||||
|
case (_, EventSourcedBehaviorImpl.GetPersistenceId(replyTo)) => replyTo ! persistenceId
|
||||||
}
|
}
|
||||||
|
|
||||||
// do this once, even if the actor is restarted
|
// do this once, even if the actor is restarted
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import akka.persistence.typed.EventsSeq
|
||||||
import akka.persistence.typed.RecoveryCompleted
|
import akka.persistence.typed.RecoveryCompleted
|
||||||
import akka.persistence.typed.RecoveryFailed
|
import akka.persistence.typed.RecoveryFailed
|
||||||
import akka.persistence.typed.SingleEventSeq
|
import akka.persistence.typed.SingleEventSeq
|
||||||
|
import akka.persistence.typed.internal.EventSourcedBehaviorImpl.GetState
|
||||||
import akka.persistence.typed.internal.ReplayingEvents.ReplayingState
|
import akka.persistence.typed.internal.ReplayingEvents.ReplayingState
|
||||||
import akka.persistence.typed.internal.Running.WithSeqNrAccessible
|
import akka.persistence.typed.internal.Running.WithSeqNrAccessible
|
||||||
import akka.util.OptionVal
|
import akka.util.OptionVal
|
||||||
|
|
@ -87,11 +88,12 @@ private[akka] final class ReplayingEvents[C, E, S](
|
||||||
|
|
||||||
override def onMessage(msg: InternalProtocol): Behavior[InternalProtocol] = {
|
override def onMessage(msg: InternalProtocol): Behavior[InternalProtocol] = {
|
||||||
msg match {
|
msg match {
|
||||||
case JournalResponse(r) => onJournalResponse(r)
|
case JournalResponse(r) => onJournalResponse(r)
|
||||||
case SnapshotterResponse(r) => onSnapshotterResponse(r)
|
case SnapshotterResponse(r) => onSnapshotterResponse(r)
|
||||||
case RecoveryTickEvent(snap) => onRecoveryTick(snap)
|
case RecoveryTickEvent(snap) => onRecoveryTick(snap)
|
||||||
case cmd: IncomingCommand[C] => onCommand(cmd)
|
case cmd: IncomingCommand[C] => onCommand(cmd)
|
||||||
case RecoveryPermitGranted => Behaviors.unhandled // should not happen, we already have the permit
|
case get: GetState[S @unchecked] => stashInternal(get)
|
||||||
|
case RecoveryPermitGranted => Behaviors.unhandled // should not happen, we already have the permit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,7 +164,6 @@ private[akka] final class ReplayingEvents[C, E, S](
|
||||||
Behaviors.unhandled
|
Behaviors.unhandled
|
||||||
} else {
|
} else {
|
||||||
stashInternal(cmd)
|
stashInternal(cmd)
|
||||||
Behaviors.same
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import akka.persistence._
|
||||||
import akka.persistence.SnapshotProtocol.LoadSnapshotFailed
|
import akka.persistence.SnapshotProtocol.LoadSnapshotFailed
|
||||||
import akka.persistence.SnapshotProtocol.LoadSnapshotResult
|
import akka.persistence.SnapshotProtocol.LoadSnapshotResult
|
||||||
import akka.persistence.typed.RecoveryFailed
|
import akka.persistence.typed.RecoveryFailed
|
||||||
|
import akka.persistence.typed.internal.EventSourcedBehaviorImpl.GetState
|
||||||
import akka.util.unused
|
import akka.util.unused
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -66,7 +67,8 @@ private[akka] class ReplayingSnapshot[C, E, S](override val setup: BehaviorSetup
|
||||||
Behaviors.unhandled
|
Behaviors.unhandled
|
||||||
} else
|
} else
|
||||||
onCommand(cmd)
|
onCommand(cmd)
|
||||||
case RecoveryPermitGranted => Behaviors.unhandled // should not happen, we already have the permit
|
case get: GetState[S @unchecked] => stashInternal(get)
|
||||||
|
case RecoveryPermitGranted => Behaviors.unhandled // should not happen, we already have the permit
|
||||||
}
|
}
|
||||||
.receiveSignal(returnPermitOnStop.orElse {
|
.receiveSignal(returnPermitOnStop.orElse {
|
||||||
case (_, PoisonPill) =>
|
case (_, PoisonPill) =>
|
||||||
|
|
@ -118,7 +120,6 @@ private[akka] class ReplayingSnapshot[C, E, S](override val setup: BehaviorSetup
|
||||||
def onCommand(cmd: IncomingCommand[C]): Behavior[InternalProtocol] = {
|
def onCommand(cmd: IncomingCommand[C]): Behavior[InternalProtocol] = {
|
||||||
// during recovery, stash all incoming commands
|
// during recovery, stash all incoming commands
|
||||||
stashInternal(cmd)
|
stashInternal(cmd)
|
||||||
Behaviors.same
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def onJournalResponse(response: JournalProtocol.Response): Behavior[InternalProtocol] = {
|
def onJournalResponse(response: JournalProtocol.Response): Behavior[InternalProtocol] = {
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@ private[akka] class RequestingRecoveryPermit[C, E, S](override val setup: Behavi
|
||||||
Behaviors.unhandled
|
Behaviors.unhandled
|
||||||
} else {
|
} else {
|
||||||
stashInternal(other)
|
stashInternal(other)
|
||||||
Behaviors.same
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import akka.persistence.typed.SnapshotCompleted
|
||||||
import akka.persistence.typed.SnapshotFailed
|
import akka.persistence.typed.SnapshotFailed
|
||||||
import akka.persistence.typed.SnapshotMetadata
|
import akka.persistence.typed.SnapshotMetadata
|
||||||
import akka.persistence.typed.SnapshotSelectionCriteria
|
import akka.persistence.typed.SnapshotSelectionCriteria
|
||||||
|
import akka.persistence.typed.internal.EventSourcedBehaviorImpl.GetState
|
||||||
import akka.persistence.typed.internal.Running.WithSeqNrAccessible
|
import akka.persistence.typed.internal.Running.WithSeqNrAccessible
|
||||||
import akka.persistence.typed.scaladsl.Effect
|
import akka.persistence.typed.scaladsl.Effect
|
||||||
import akka.util.unused
|
import akka.util.unused
|
||||||
|
|
@ -104,6 +105,7 @@ private[akka] object Running {
|
||||||
case IncomingCommand(c: C @unchecked) => onCommand(state, c)
|
case IncomingCommand(c: C @unchecked) => onCommand(state, c)
|
||||||
case JournalResponse(r) => onDeleteEventsJournalResponse(r, state.state)
|
case JournalResponse(r) => onDeleteEventsJournalResponse(r, state.state)
|
||||||
case SnapshotterResponse(r) => onDeleteSnapshotResponse(r, state.state)
|
case SnapshotterResponse(r) => onDeleteSnapshotResponse(r, state.state)
|
||||||
|
case get: GetState[S @unchecked] => onGetState(get)
|
||||||
case _ => Behaviors.unhandled
|
case _ => Behaviors.unhandled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,6 +123,12 @@ private[akka] object Running {
|
||||||
applyEffects(cmd, state, effect.asInstanceOf[EffectImpl[E, S]]) // TODO can we avoid the cast?
|
applyEffects(cmd, state, effect.asInstanceOf[EffectImpl[E, S]]) // TODO can we avoid the cast?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used by EventSourcedBehaviorTestKit to retrieve the state.
|
||||||
|
def onGetState(get: GetState[S]): Behavior[InternalProtocol] = {
|
||||||
|
get.replyTo ! state.state
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
@tailrec def applyEffects(
|
@tailrec def applyEffects(
|
||||||
msg: Any,
|
msg: Any,
|
||||||
state: RunningState[S],
|
state: RunningState[S],
|
||||||
|
|
@ -236,6 +244,7 @@ private[akka] object Running {
|
||||||
msg match {
|
msg match {
|
||||||
case JournalResponse(r) => onJournalResponse(r)
|
case JournalResponse(r) => onJournalResponse(r)
|
||||||
case in: IncomingCommand[C @unchecked] => onCommand(in)
|
case in: IncomingCommand[C @unchecked] => onCommand(in)
|
||||||
|
case get: GetState[S @unchecked] => stashInternal(get)
|
||||||
case SnapshotterResponse(r) => onDeleteSnapshotResponse(r, visibleState.state)
|
case SnapshotterResponse(r) => onDeleteSnapshotResponse(r, visibleState.state)
|
||||||
case RecoveryTickEvent(_) => Behaviors.unhandled
|
case RecoveryTickEvent(_) => Behaviors.unhandled
|
||||||
case RecoveryPermitGranted => Behaviors.unhandled
|
case RecoveryPermitGranted => Behaviors.unhandled
|
||||||
|
|
@ -249,7 +258,6 @@ private[akka] object Running {
|
||||||
Behaviors.unhandled
|
Behaviors.unhandled
|
||||||
} else {
|
} else {
|
||||||
stashInternal(cmd)
|
stashInternal(cmd)
|
||||||
this
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -348,7 +356,6 @@ private[akka] object Running {
|
||||||
Behaviors.unhandled
|
Behaviors.unhandled
|
||||||
} else {
|
} else {
|
||||||
stashInternal(cmd)
|
stashInternal(cmd)
|
||||||
Behaviors.same
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -406,6 +413,8 @@ private[akka] object Running {
|
||||||
case _ =>
|
case _ =>
|
||||||
onDeleteSnapshotResponse(response, state.state)
|
onDeleteSnapshotResponse(response, state.state)
|
||||||
}
|
}
|
||||||
|
case get: GetState[S @unchecked] =>
|
||||||
|
stashInternal(get)
|
||||||
case _ =>
|
case _ =>
|
||||||
Behaviors.unhandled
|
Behaviors.unhandled
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ package akka.persistence.typed.internal
|
||||||
import akka.actor.Dropped
|
import akka.actor.Dropped
|
||||||
import akka.actor.typed.Behavior
|
import akka.actor.typed.Behavior
|
||||||
import akka.actor.typed.scaladsl.ActorContext
|
import akka.actor.typed.scaladsl.ActorContext
|
||||||
|
import akka.actor.typed.scaladsl.Behaviors
|
||||||
import akka.actor.typed.scaladsl.LoggerOps
|
import akka.actor.typed.scaladsl.LoggerOps
|
||||||
import akka.actor.typed.scaladsl.StashBuffer
|
import akka.actor.typed.scaladsl.StashBuffer
|
||||||
import akka.actor.typed.scaladsl.StashOverflowException
|
import akka.actor.typed.scaladsl.StashOverflowException
|
||||||
|
|
@ -29,8 +30,10 @@ private[akka] trait StashManagement[C, E, S] {
|
||||||
/**
|
/**
|
||||||
* Stash a command to the internal stash buffer, which is used while waiting for persist to be completed.
|
* Stash a command to the internal stash buffer, which is used while waiting for persist to be completed.
|
||||||
*/
|
*/
|
||||||
protected def stashInternal(msg: InternalProtocol): Unit =
|
protected def stashInternal(msg: InternalProtocol): Behavior[InternalProtocol] = {
|
||||||
stash(msg, stashState.internalStashBuffer)
|
stash(msg, stashState.internalStashBuffer)
|
||||||
|
Behaviors.same
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stash a command to the user stash buffer, which is used when `Stash` effect is used.
|
* Stash a command to the user stash buffer, which is used when `Stash` effect is used.
|
||||||
|
|
|
||||||
|
|
@ -281,6 +281,7 @@ lazy val persistenceTestkit = akkaModule("akka-persistence-testkit")
|
||||||
.dependsOn(
|
.dependsOn(
|
||||||
persistenceTyped % "compile->compile;provided->provided;test->test",
|
persistenceTyped % "compile->compile;provided->provided;test->test",
|
||||||
testkit % "compile->compile;test->test",
|
testkit % "compile->compile;test->test",
|
||||||
|
actorTestkitTyped,
|
||||||
persistenceTck % "test")
|
persistenceTck % "test")
|
||||||
.settings(Dependencies.persistenceTestKit)
|
.settings(Dependencies.persistenceTestKit)
|
||||||
.settings(AutomaticModuleName.settings("akka.persistence.testkit"))
|
.settings(AutomaticModuleName.settings("akka.persistence.testkit"))
|
||||||
|
|
@ -450,6 +451,7 @@ lazy val clusterShardingTyped = akkaModule("akka-cluster-sharding-typed")
|
||||||
actorTestkitTyped % "test->test",
|
actorTestkitTyped % "test->test",
|
||||||
actorTypedTests % "test->test",
|
actorTypedTests % "test->test",
|
||||||
persistenceTyped % "test->test",
|
persistenceTyped % "test->test",
|
||||||
|
persistenceTestkit % "test->test",
|
||||||
remote % "compile->CompileJdk9;test->test",
|
remote % "compile->CompileJdk9;test->test",
|
||||||
remoteTests % "test->test",
|
remoteTests % "test->test",
|
||||||
remoteTests % "test->test;multi-jvm->multi-jvm",
|
remoteTests % "test->test;multi-jvm->multi-jvm",
|
||||||
|
|
|
||||||
|
|
@ -243,7 +243,7 @@ object Dependencies {
|
||||||
Provided.levelDB,
|
Provided.levelDB,
|
||||||
Provided.levelDBNative)
|
Provided.levelDBNative)
|
||||||
|
|
||||||
val persistenceTestKit = l ++= Seq(Test.scalatest)
|
val persistenceTestKit = l ++= Seq(Test.scalatest, Test.logback)
|
||||||
|
|
||||||
val persistenceShared = l ++= Seq(Provided.levelDB, Provided.levelDBNative)
|
val persistenceShared = l ++= Seq(Provided.levelDB, Provided.levelDBNative)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -407,6 +407,27 @@ project-info {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
akka-persistence-testkit: ${project-info.shared-info} {
|
||||||
|
title: "Akka Persistence Testkit"
|
||||||
|
jpms-name: "akka.persistence.testkit"
|
||||||
|
levels: [
|
||||||
|
{
|
||||||
|
readiness: Incubating
|
||||||
|
since: "2020-04-30"
|
||||||
|
since-version: "2.6.5"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
api-docs: [
|
||||||
|
{
|
||||||
|
url: ${project-info.scaladoc}"persistence/testkit/scaladsl/index.html"
|
||||||
|
text: "API (Scaladoc)"
|
||||||
|
}
|
||||||
|
{
|
||||||
|
url: ${project-info.javadoc}"?akka/persistence/testkit/javadsl/package-summary.html"
|
||||||
|
text: "API (Javadoc)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
akka-remote: ${project-info.shared-info} {
|
akka-remote: ${project-info.shared-info} {
|
||||||
title: "Akka Remoting"
|
title: "Akka Remoting"
|
||||||
jpms-name: "akka.remote"
|
jpms-name: "akka.remote"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue