* annoying test failures of deprecated feature * keeping example that is used in docs
This commit is contained in:
parent
2f5c19eb02
commit
066bd2a91b
1 changed files with 3 additions and 378 deletions
|
|
@ -5,381 +5,17 @@
|
||||||
package akka.persistence.fsm;
|
package akka.persistence.fsm;
|
||||||
|
|
||||||
import akka.actor.*;
|
import akka.actor.*;
|
||||||
import akka.japi.Option;
|
|
||||||
import akka.persistence.PersistenceSpec;
|
|
||||||
import akka.testkit.AkkaJUnitActorSystemResource;
|
|
||||||
import akka.testkit.javadsl.TestKit;
|
|
||||||
import akka.testkit.TestProbe;
|
|
||||||
import org.junit.ClassRule;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
||||||
import org.junit.Ignore;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.scalatestplus.junit.JUnitSuite;
|
|
||||||
|
|
||||||
import static akka.persistence.fsm.AbstractPersistentFSMTest.WebStoreCustomerFSM.UserState;
|
|
||||||
import static akka.persistence.fsm.AbstractPersistentFSMTest.WebStoreCustomerFSM.ShoppingCart;
|
|
||||||
import static akka.persistence.fsm.AbstractPersistentFSMTest.WebStoreCustomerFSM.Item;
|
|
||||||
|
|
||||||
import static akka.persistence.fsm.AbstractPersistentFSMTest.WebStoreCustomerFSM.GetCurrentCart;
|
|
||||||
import static akka.persistence.fsm.AbstractPersistentFSMTest.WebStoreCustomerFSM.AddItem;
|
|
||||||
import static akka.persistence.fsm.AbstractPersistentFSMTest.WebStoreCustomerFSM.Buy;
|
|
||||||
import static akka.persistence.fsm.AbstractPersistentFSMTest.WebStoreCustomerFSM.Leave;
|
|
||||||
|
|
||||||
import static akka.persistence.fsm.AbstractPersistentFSMTest.WebStoreCustomerFSM.PurchaseWasMade;
|
|
||||||
import static akka.persistence.fsm.AbstractPersistentFSMTest.WebStoreCustomerFSM.ShoppingCardDiscarded;
|
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.equalTo;
|
|
||||||
import static org.hamcrest.CoreMatchers.hasItems;
|
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
import static org.junit.Assert.assertThat;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
|
||||||
|
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public class AbstractPersistentFSMTest extends JUnitSuite {
|
public class AbstractPersistentFSMTest {
|
||||||
private static Option<String> none = Option.none();
|
// tests have been removed because of flaky test failures, see PR
|
||||||
|
// https://github.com/akka/akka/pull/31128
|
||||||
@ClassRule
|
|
||||||
public static AkkaJUnitActorSystemResource actorSystemResource =
|
|
||||||
new AkkaJUnitActorSystemResource(
|
|
||||||
"PersistentFSMJavaTest",
|
|
||||||
PersistenceSpec.config("leveldb", "AbstractPersistentFSMTest", "off", none.asScala()));
|
|
||||||
|
|
||||||
private final ActorSystem system = actorSystemResource.getSystem();
|
|
||||||
|
|
||||||
// Dummy report actor, for tests that don't need it
|
|
||||||
private final ActorRef dummyReportActorRef = new TestProbe(system).ref();
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void fsmFunctionalTest() throws Exception {
|
|
||||||
new TestKit(system) {
|
|
||||||
{
|
|
||||||
String persistenceId = generateId();
|
|
||||||
ActorRef fsmRef =
|
|
||||||
system.actorOf(WebStoreCustomerFSM.props(persistenceId, dummyReportActorRef));
|
|
||||||
|
|
||||||
watch(fsmRef);
|
|
||||||
fsmRef.tell(new PersistentFSM.SubscribeTransitionCallBack(getRef()), getRef());
|
|
||||||
|
|
||||||
Item shirt = new Item("1", "Shirt", 19.99F);
|
|
||||||
Item shoes = new Item("2", "Shoes", 18.99F);
|
|
||||||
Item coat = new Item("3", "Coat", 119.99F);
|
|
||||||
|
|
||||||
fsmRef.tell(GetCurrentCart.INSTANCE, getRef());
|
|
||||||
fsmRef.tell(new AddItem(shirt), getRef());
|
|
||||||
fsmRef.tell(GetCurrentCart.INSTANCE, getRef());
|
|
||||||
fsmRef.tell(new AddItem(shoes), getRef());
|
|
||||||
fsmRef.tell(GetCurrentCart.INSTANCE, getRef());
|
|
||||||
fsmRef.tell(new AddItem(coat), getRef());
|
|
||||||
fsmRef.tell(GetCurrentCart.INSTANCE, getRef());
|
|
||||||
fsmRef.tell(Buy.INSTANCE, getRef());
|
|
||||||
fsmRef.tell(GetCurrentCart.INSTANCE, getRef());
|
|
||||||
fsmRef.tell(Leave.INSTANCE, getRef());
|
|
||||||
|
|
||||||
PersistentFSM.CurrentState currentState =
|
|
||||||
expectMsgClass(akka.persistence.fsm.PersistentFSM.CurrentState.class);
|
|
||||||
assertEquals(currentState.state(), UserState.LOOKING_AROUND);
|
|
||||||
|
|
||||||
ShoppingCart shoppingCart = expectMsgClass(ShoppingCart.class);
|
|
||||||
assertTrue(shoppingCart.getItems().isEmpty());
|
|
||||||
|
|
||||||
PersistentFSM.Transition stateTransition = expectMsgClass(PersistentFSM.Transition.class);
|
|
||||||
assertTransition(stateTransition, fsmRef, UserState.LOOKING_AROUND, UserState.SHOPPING);
|
|
||||||
|
|
||||||
shoppingCart = expectMsgClass(ShoppingCart.class);
|
|
||||||
assertThat(shoppingCart.getItems(), hasItems(shirt));
|
|
||||||
|
|
||||||
shoppingCart = expectMsgClass(ShoppingCart.class);
|
|
||||||
assertThat(shoppingCart.getItems(), hasItems(shirt, shoes));
|
|
||||||
|
|
||||||
shoppingCart = expectMsgClass(ShoppingCart.class);
|
|
||||||
assertThat(shoppingCart.getItems(), hasItems(shirt, shoes, coat));
|
|
||||||
|
|
||||||
stateTransition = expectMsgClass(PersistentFSM.Transition.class);
|
|
||||||
assertTransition(stateTransition, fsmRef, UserState.SHOPPING, UserState.PAID);
|
|
||||||
|
|
||||||
shoppingCart = expectMsgClass(ShoppingCart.class);
|
|
||||||
assertThat(shoppingCart.getItems(), hasItems(shirt, shoes, coat));
|
|
||||||
|
|
||||||
Terminated terminated = expectMsgClass(Terminated.class);
|
|
||||||
assertEquals(fsmRef, terminated.getActor());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void fsmTimeoutTest() throws Exception {
|
|
||||||
new TestKit(system) {
|
|
||||||
{
|
|
||||||
String persistenceId = generateId();
|
|
||||||
ActorRef fsmRef =
|
|
||||||
system.actorOf(WebStoreCustomerFSM.props(persistenceId, dummyReportActorRef));
|
|
||||||
|
|
||||||
watch(fsmRef);
|
|
||||||
fsmRef.tell(new PersistentFSM.SubscribeTransitionCallBack(getRef()), getRef());
|
|
||||||
|
|
||||||
Item shirt = new Item("1", "Shirt", 29.99F);
|
|
||||||
|
|
||||||
fsmRef.tell(new AddItem(shirt), getRef());
|
|
||||||
|
|
||||||
PersistentFSM.CurrentState currentState =
|
|
||||||
expectMsgClass(akka.persistence.fsm.PersistentFSM.CurrentState.class);
|
|
||||||
assertEquals(currentState.state(), UserState.LOOKING_AROUND);
|
|
||||||
|
|
||||||
PersistentFSM.Transition stateTransition = expectMsgClass(PersistentFSM.Transition.class);
|
|
||||||
assertTransition(stateTransition, fsmRef, UserState.LOOKING_AROUND, UserState.SHOPPING);
|
|
||||||
|
|
||||||
within(
|
|
||||||
Duration.ofMillis(900),
|
|
||||||
getRemainingOrDefault(),
|
|
||||||
() -> {
|
|
||||||
PersistentFSM.Transition st = expectMsgClass(PersistentFSM.Transition.class);
|
|
||||||
assertTransition(st, fsmRef, UserState.SHOPPING, UserState.INACTIVE);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
within(Duration.ofMillis(1900), getRemainingOrDefault(), () -> expectTerminated(fsmRef));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test is flaky (https://github.com/akka/akka/issues/24723) and that failure issue was
|
|
||||||
// already deemed obsolete. Plus, the whole test is marked as deprecated. Ignoring...
|
|
||||||
@Test
|
|
||||||
@Ignore
|
|
||||||
public void testSuccessfulRecoveryWithCorrectStateData() {
|
|
||||||
new TestKit(system) {
|
|
||||||
{
|
|
||||||
String persistenceId = generateId();
|
|
||||||
ActorRef fsmRef =
|
|
||||||
system.actorOf(WebStoreCustomerFSM.props(persistenceId, dummyReportActorRef));
|
|
||||||
|
|
||||||
watch(fsmRef);
|
|
||||||
fsmRef.tell(new PersistentFSM.SubscribeTransitionCallBack(getRef()), getRef());
|
|
||||||
|
|
||||||
Item shirt = new Item("1", "Shirt", 38.99F);
|
|
||||||
Item shoes = new Item("2", "Shoes", 39.99F);
|
|
||||||
Item coat = new Item("3", "Coat", 139.99F);
|
|
||||||
|
|
||||||
fsmRef.tell(GetCurrentCart.INSTANCE, getRef());
|
|
||||||
fsmRef.tell(new AddItem(shirt), getRef());
|
|
||||||
fsmRef.tell(GetCurrentCart.INSTANCE, getRef());
|
|
||||||
fsmRef.tell(new AddItem(shoes), getRef());
|
|
||||||
fsmRef.tell(GetCurrentCart.INSTANCE, getRef());
|
|
||||||
|
|
||||||
PersistentFSM.CurrentState currentState =
|
|
||||||
expectMsgClass(akka.persistence.fsm.PersistentFSM.CurrentState.class);
|
|
||||||
assertEquals(currentState.state(), UserState.LOOKING_AROUND);
|
|
||||||
|
|
||||||
ShoppingCart shoppingCart = expectMsgClass(ShoppingCart.class);
|
|
||||||
assertThat(shoppingCart.getItems(), equalTo(Collections.emptyList()));
|
|
||||||
|
|
||||||
PersistentFSM.Transition stateTransition = expectMsgClass(PersistentFSM.Transition.class);
|
|
||||||
assertTransition(stateTransition, fsmRef, UserState.LOOKING_AROUND, UserState.SHOPPING);
|
|
||||||
|
|
||||||
shoppingCart = expectMsgClass(ShoppingCart.class);
|
|
||||||
assertThat(shoppingCart.getItems(), hasItems(shirt));
|
|
||||||
|
|
||||||
shoppingCart = expectMsgClass(ShoppingCart.class);
|
|
||||||
assertThat(shoppingCart.getItems(), hasItems(shirt, shoes));
|
|
||||||
|
|
||||||
fsmRef.tell(PoisonPill.getInstance(), ActorRef.noSender());
|
|
||||||
expectTerminated(fsmRef);
|
|
||||||
|
|
||||||
ActorRef recoveredFsmRef =
|
|
||||||
system.actorOf(WebStoreCustomerFSM.props(persistenceId, dummyReportActorRef));
|
|
||||||
watch(recoveredFsmRef);
|
|
||||||
recoveredFsmRef.tell(new PersistentFSM.SubscribeTransitionCallBack(getRef()), getRef());
|
|
||||||
|
|
||||||
recoveredFsmRef.tell(GetCurrentCart.INSTANCE, getRef());
|
|
||||||
|
|
||||||
recoveredFsmRef.tell(new AddItem(coat), getRef());
|
|
||||||
recoveredFsmRef.tell(GetCurrentCart.INSTANCE, getRef());
|
|
||||||
|
|
||||||
recoveredFsmRef.tell(Buy.INSTANCE, getRef());
|
|
||||||
recoveredFsmRef.tell(GetCurrentCart.INSTANCE, getRef());
|
|
||||||
recoveredFsmRef.tell(Leave.INSTANCE, getRef());
|
|
||||||
|
|
||||||
currentState = expectMsgClass(akka.persistence.fsm.PersistentFSM.CurrentState.class);
|
|
||||||
assertEquals(currentState.state(), UserState.SHOPPING);
|
|
||||||
|
|
||||||
shoppingCart = expectMsgClass(ShoppingCart.class);
|
|
||||||
assertThat(shoppingCart.getItems(), hasItems(shirt, shoes));
|
|
||||||
|
|
||||||
shoppingCart = expectMsgClass(ShoppingCart.class);
|
|
||||||
assertThat(shoppingCart.getItems(), hasItems(shirt, shoes, coat));
|
|
||||||
|
|
||||||
stateTransition = expectMsgClass(PersistentFSM.Transition.class);
|
|
||||||
assertTransition(stateTransition, recoveredFsmRef, UserState.SHOPPING, UserState.PAID);
|
|
||||||
|
|
||||||
shoppingCart = expectMsgClass(ShoppingCart.class);
|
|
||||||
assertThat(shoppingCart.getItems(), hasItems(shirt, shoes, coat));
|
|
||||||
|
|
||||||
expectTerminated(recoveredFsmRef);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testExecutionOfDefinedActionsFollowingSuccessfulPersistence() {
|
|
||||||
new TestKit(system) {
|
|
||||||
{
|
|
||||||
String persistenceId = generateId();
|
|
||||||
|
|
||||||
TestProbe reportActorProbe = new TestProbe(system);
|
|
||||||
ActorRef fsmRef =
|
|
||||||
system.actorOf(WebStoreCustomerFSM.props(persistenceId, reportActorProbe.ref()));
|
|
||||||
|
|
||||||
watch(fsmRef);
|
|
||||||
fsmRef.tell(new PersistentFSM.SubscribeTransitionCallBack(getRef()), getRef());
|
|
||||||
|
|
||||||
Item shirt = new Item("1", "Shirt", 49.99F);
|
|
||||||
Item shoes = new Item("2", "Shoes", 49.99F);
|
|
||||||
Item coat = new Item("3", "Coat", 149.99F);
|
|
||||||
|
|
||||||
fsmRef.tell(new AddItem(shirt), getRef());
|
|
||||||
fsmRef.tell(new AddItem(shoes), getRef());
|
|
||||||
fsmRef.tell(new AddItem(coat), getRef());
|
|
||||||
fsmRef.tell(Buy.INSTANCE, getRef());
|
|
||||||
fsmRef.tell(Leave.INSTANCE, getRef());
|
|
||||||
|
|
||||||
PersistentFSM.CurrentState currentState =
|
|
||||||
expectMsgClass(akka.persistence.fsm.PersistentFSM.CurrentState.class);
|
|
||||||
assertEquals(currentState.state(), UserState.LOOKING_AROUND);
|
|
||||||
|
|
||||||
PersistentFSM.Transition stateTransition = expectMsgClass(PersistentFSM.Transition.class);
|
|
||||||
assertTransition(stateTransition, fsmRef, UserState.LOOKING_AROUND, UserState.SHOPPING);
|
|
||||||
|
|
||||||
stateTransition = expectMsgClass(PersistentFSM.Transition.class);
|
|
||||||
assertTransition(stateTransition, fsmRef, UserState.SHOPPING, UserState.PAID);
|
|
||||||
|
|
||||||
PurchaseWasMade purchaseWasMade = reportActorProbe.expectMsgClass(PurchaseWasMade.class);
|
|
||||||
assertThat(purchaseWasMade.getItems(), hasItems(shirt, shoes, coat));
|
|
||||||
|
|
||||||
expectTerminated(fsmRef);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testExecutionOfDefinedActionsFollowingSuccessfulPersistenceOfFSMStop() {
|
|
||||||
new TestKit(system) {
|
|
||||||
{
|
|
||||||
String persistenceId = generateId();
|
|
||||||
|
|
||||||
TestProbe reportActorProbe = new TestProbe(system);
|
|
||||||
ActorRef fsmRef =
|
|
||||||
system.actorOf(WebStoreCustomerFSM.props(persistenceId, reportActorProbe.ref()));
|
|
||||||
|
|
||||||
watch(fsmRef);
|
|
||||||
fsmRef.tell(new PersistentFSM.SubscribeTransitionCallBack(getRef()), getRef());
|
|
||||||
|
|
||||||
Item shirt = new Item("1", "Shirt", 59.99F);
|
|
||||||
Item shoes = new Item("2", "Shoes", 58.99F);
|
|
||||||
Item coat = new Item("3", "Coat", 159.99F);
|
|
||||||
|
|
||||||
fsmRef.tell(new AddItem(shirt), getRef());
|
|
||||||
fsmRef.tell(new AddItem(shoes), getRef());
|
|
||||||
fsmRef.tell(new AddItem(coat), getRef());
|
|
||||||
fsmRef.tell(Leave.INSTANCE, getRef());
|
|
||||||
|
|
||||||
PersistentFSM.CurrentState currentState =
|
|
||||||
expectMsgClass(akka.persistence.fsm.PersistentFSM.CurrentState.class);
|
|
||||||
assertEquals(currentState.state(), UserState.LOOKING_AROUND);
|
|
||||||
|
|
||||||
PersistentFSM.Transition stateTransition = expectMsgClass(PersistentFSM.Transition.class);
|
|
||||||
assertTransition(stateTransition, fsmRef, UserState.LOOKING_AROUND, UserState.SHOPPING);
|
|
||||||
|
|
||||||
reportActorProbe.expectMsgClass(ShoppingCardDiscarded.class);
|
|
||||||
|
|
||||||
expectTerminated(fsmRef);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testCorrectStateTimeoutFollowingRecovery() {
|
|
||||||
new TestKit(system) {
|
|
||||||
{
|
|
||||||
String persistenceId = generateId();
|
|
||||||
ActorRef fsmRef =
|
|
||||||
system.actorOf(WebStoreCustomerFSM.props(persistenceId, dummyReportActorRef));
|
|
||||||
|
|
||||||
watch(fsmRef);
|
|
||||||
fsmRef.tell(new PersistentFSM.SubscribeTransitionCallBack(getRef()), getRef());
|
|
||||||
|
|
||||||
Item shirt = new Item("1", "Shirt", 69.99F);
|
|
||||||
|
|
||||||
fsmRef.tell(new AddItem(shirt), getRef());
|
|
||||||
|
|
||||||
PersistentFSM.CurrentState currentState =
|
|
||||||
expectMsgClass(akka.persistence.fsm.PersistentFSM.CurrentState.class);
|
|
||||||
assertEquals(currentState.state(), UserState.LOOKING_AROUND);
|
|
||||||
|
|
||||||
PersistentFSM.Transition stateTransition = expectMsgClass(PersistentFSM.Transition.class);
|
|
||||||
assertTransition(stateTransition, fsmRef, UserState.LOOKING_AROUND, UserState.SHOPPING);
|
|
||||||
|
|
||||||
expectNoMessage(
|
|
||||||
Duration.ofMillis(
|
|
||||||
600)); // randomly chosen delay, less than the timeout, before stopping the FSM
|
|
||||||
fsmRef.tell(PoisonPill.getInstance(), ActorRef.noSender());
|
|
||||||
expectTerminated(fsmRef);
|
|
||||||
|
|
||||||
final ActorRef recoveredFsmRef =
|
|
||||||
system.actorOf(WebStoreCustomerFSM.props(persistenceId, dummyReportActorRef));
|
|
||||||
watch(recoveredFsmRef);
|
|
||||||
recoveredFsmRef.tell(new PersistentFSM.SubscribeTransitionCallBack(getRef()), getRef());
|
|
||||||
|
|
||||||
currentState = expectMsgClass(akka.persistence.fsm.PersistentFSM.CurrentState.class);
|
|
||||||
assertEquals(currentState.state(), UserState.SHOPPING);
|
|
||||||
|
|
||||||
within(
|
|
||||||
Duration.ofMillis(900),
|
|
||||||
getRemainingOrDefault(),
|
|
||||||
() -> {
|
|
||||||
PersistentFSM.Transition st = expectMsgClass(PersistentFSM.Transition.class);
|
|
||||||
assertTransition(st, recoveredFsmRef, UserState.SHOPPING, UserState.INACTIVE);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
expectNoMessage(
|
|
||||||
Duration.ofMillis(
|
|
||||||
900)); // randomly chosen delay, less than the timeout, before stopping the FSM
|
|
||||||
recoveredFsmRef.tell(PoisonPill.getInstance(), ActorRef.noSender());
|
|
||||||
expectTerminated(recoveredFsmRef);
|
|
||||||
|
|
||||||
final ActorRef recoveredFsmRef2 =
|
|
||||||
system.actorOf(WebStoreCustomerFSM.props(persistenceId, dummyReportActorRef));
|
|
||||||
watch(recoveredFsmRef2);
|
|
||||||
recoveredFsmRef2.tell(new PersistentFSM.SubscribeTransitionCallBack(getRef()), getRef());
|
|
||||||
|
|
||||||
currentState = expectMsgClass(akka.persistence.fsm.PersistentFSM.CurrentState.class);
|
|
||||||
assertEquals(currentState.state(), UserState.INACTIVE);
|
|
||||||
|
|
||||||
within(
|
|
||||||
Duration.ofMillis(1900),
|
|
||||||
getRemainingOrDefault(),
|
|
||||||
() -> expectTerminated(recoveredFsmRef2));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static <State, From extends State, To extends State> void assertTransition(
|
|
||||||
PersistentFSM.Transition transition, ActorRef ref, From from, To to) {
|
|
||||||
assertEquals(ref, transition.fsmRef());
|
|
||||||
assertEquals(from, transition.from());
|
|
||||||
assertEquals(to, transition.to());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String generateId() {
|
|
||||||
return UUID.randomUUID().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class WebStoreCustomerFSM
|
public static class WebStoreCustomerFSM
|
||||||
extends AbstractPersistentFSM<
|
extends AbstractPersistentFSM<
|
||||||
|
|
@ -711,15 +347,4 @@ public class AbstractPersistentFSMTest extends JUnitSuite {
|
||||||
return "id";
|
return "id";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testCreationOfActorCallingOnTransitionWithVoidFunction() {
|
|
||||||
new TestKit(system) {
|
|
||||||
{
|
|
||||||
ActorRef persistentActor = system.actorOf(Props.create(PFSMwithLog.class));
|
|
||||||
persistentActor.tell("check", getRef());
|
|
||||||
expectMsg(Duration.ofSeconds(1), "started");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue