From 5f8b40a13b88b2a183450c2b67401415da47511a Mon Sep 17 00:00:00 2001 From: Andrej Mak Date: Mon, 6 Mar 2017 15:53:55 +0100 Subject: [PATCH] Add cds-access-client unit tests Change-Id: I0f330dfd02d1562d0965a9d3a27721970d5fece8 Signed-off-by: Andrej Mak --- .../access/client/ActorBehaviorTest.java | 194 ++++++++++++++++++ .../access/client/InversibleLockTest.java | 53 +++++ .../access/client/MockedSnapshotStore.java | 159 ++++++++++++++ .../src/test/resources/application.conf | 17 ++ 4 files changed, 423 insertions(+) create mode 100644 opendaylight/md-sal/cds-access-client/src/test/java/org/opendaylight/controller/cluster/access/client/ActorBehaviorTest.java create mode 100644 opendaylight/md-sal/cds-access-client/src/test/java/org/opendaylight/controller/cluster/access/client/InversibleLockTest.java create mode 100644 opendaylight/md-sal/cds-access-client/src/test/java/org/opendaylight/controller/cluster/access/client/MockedSnapshotStore.java create mode 100644 opendaylight/md-sal/cds-access-client/src/test/resources/application.conf diff --git a/opendaylight/md-sal/cds-access-client/src/test/java/org/opendaylight/controller/cluster/access/client/ActorBehaviorTest.java b/opendaylight/md-sal/cds-access-client/src/test/java/org/opendaylight/controller/cluster/access/client/ActorBehaviorTest.java new file mode 100644 index 0000000000..b0a6011222 --- /dev/null +++ b/opendaylight/md-sal/cds-access-client/src/test/java/org/opendaylight/controller/cluster/access/client/ActorBehaviorTest.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2017 Pantheon Technologies s.r.o. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.controller.cluster.access.client; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import akka.actor.ActorRef; +import akka.actor.ActorSystem; +import akka.actor.Props; +import akka.persistence.Persistence; +import akka.persistence.SelectedSnapshot; +import akka.persistence.SnapshotMetadata; +import akka.testkit.JavaTestKit; +import akka.testkit.TestProbe; +import java.lang.reflect.Field; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.opendaylight.controller.cluster.access.concepts.ClientIdentifier; +import org.opendaylight.controller.cluster.access.concepts.FrontendIdentifier; +import org.opendaylight.controller.cluster.access.concepts.FrontendType; +import org.opendaylight.controller.cluster.access.concepts.MemberName; +import scala.concurrent.duration.Duration; +import scala.concurrent.duration.FiniteDuration; + +public class ActorBehaviorTest { + + private static final String MEMBER_1_FRONTEND_TYPE_1 = "member-1-frontend-type-1"; + private static final FiniteDuration TIMEOUT = Duration.apply(5, TimeUnit.SECONDS); + + private ActorSystem system; + private TestProbe probe; + private ClientActorBehavior initialBehavior; + private MockedSnapshotStore.SaveRequest saveRequest; + private FrontendIdentifier id; + private ActorRef mockedActor; + + @Before + public void setUp() throws Exception { + initialBehavior = createInitialBehaviorMock(); + system = ActorSystem.apply("system1"); + final ActorRef storeRef = system.registerExtension(Persistence.lookup()).snapshotStoreFor(null); + probe = new TestProbe(system); + storeRef.tell(probe.ref(), ActorRef.noSender()); + final MemberName name = MemberName.forName("member-1"); + id = FrontendIdentifier.create(name, FrontendType.forName("type-1")); + mockedActor = system.actorOf(MockedActor.props(id, initialBehavior)); + //handle initial actor recovery + saveRequest = handleRecovery(null); + } + + @After + public void tearDown() throws Exception { + JavaTestKit.shutdownActorSystem(system); + } + + @Test + public void testInitialBehavior() throws Exception { + final InternalCommand cmd = mock(InternalCommand.class); + when(cmd.execute(any())).thenReturn(initialBehavior); + mockedActor.tell(cmd, ActorRef.noSender()); + verify(cmd, timeout(1000)).execute(initialBehavior); + } + + @Test + public void testCommandStashing() throws Exception { + system.stop(mockedActor); + mockedActor = system.actorOf(MockedActor.props(id, initialBehavior)); + final InternalCommand cmd = mock(InternalCommand.class); + when(cmd.execute(any())).thenReturn(initialBehavior); + //send messages before recovery is completed + mockedActor.tell(cmd, ActorRef.noSender()); + mockedActor.tell(cmd, ActorRef.noSender()); + mockedActor.tell(cmd, ActorRef.noSender()); + //complete recovery + handleRecovery(null); + verify(cmd, timeout(1000).times(3)).execute(initialBehavior); + } + + @Test + public void testRecoveryAfterRestart() throws Exception { + system.stop(mockedActor); + mockedActor = system.actorOf(MockedActor.props(id, initialBehavior)); + final MockedSnapshotStore.SaveRequest newSaveRequest = + handleRecovery(new SelectedSnapshot(saveRequest.getMetadata(), saveRequest.getSnapshot())); + Assert.assertEquals(MEMBER_1_FRONTEND_TYPE_1, newSaveRequest.getMetadata().persistenceId()); + } + + @Test + public void testRecoveryAfterRestartFrontendIdMismatch() throws Exception { + system.stop(mockedActor); + //start actor again + mockedActor = system.actorOf(MockedActor.props(id, initialBehavior)); + probe.expectMsgClass(MockedSnapshotStore.LoadRequest.class); + //offer snapshot with incorrect client id + final SnapshotMetadata metadata = saveRequest.getMetadata(); + final FrontendIdentifier anotherFrontend = FrontendIdentifier.create(MemberName.forName("another"), + FrontendType.forName("type-2")); + final ClientIdentifier incorrectClientId = ClientIdentifier.create(anotherFrontend, 0); + probe.watch(mockedActor); + probe.reply(Optional.of(new SelectedSnapshot(metadata, incorrectClientId))); + //actor should be stopped + probe.expectTerminated(mockedActor, TIMEOUT); + } + + @Test + public void testRecoveryAfterRestartSaveSnapshotFail() throws Exception { + system.stop(mockedActor); + mockedActor = system.actorOf(MockedActor.props(id, initialBehavior)); + probe.watch(mockedActor); + probe.expectMsgClass(MockedSnapshotStore.LoadRequest.class); + probe.reply(Optional.empty()); + probe.expectMsgClass(MockedSnapshotStore.SaveRequest.class); + probe.reply(new RuntimeException("save failed")); + probe.expectMsgClass(MockedSnapshotStore.DeleteByMetadataRequest.class); + probe.expectTerminated(mockedActor, TIMEOUT); + } + + @Test + public void testRecoveryAfterRestartDeleteSnapshotsFail() throws Exception { + system.stop(mockedActor); + mockedActor = system.actorOf(MockedActor.props(id, initialBehavior)); + probe.watch(mockedActor); + probe.expectMsgClass(MockedSnapshotStore.LoadRequest.class); + probe.reply(Optional.empty()); + probe.expectMsgClass(MockedSnapshotStore.SaveRequest.class); + probe.reply(Void.TYPE); + probe.expectMsgClass(MockedSnapshotStore.DeleteByCriteriaRequest.class); + probe.reply(new RuntimeException("delete failed")); + //actor shouldn't terminate + probe.expectNoMsg(); + } + + @SuppressWarnings("unchecked") + private ClientActorBehavior createInitialBehaviorMock() throws Exception { + final ClientActorBehavior initialBehavior = mock(ClientActorBehavior.class); + //persistenceId() in AbstractClientActorBehavior is final and can't be mocked + //use reflection to work around this + final Field context = AbstractClientActorBehavior.class.getDeclaredField("context"); + context.setAccessible(true); + final AbstractClientActorContext ctx = mock(AbstractClientActorContext.class); + context.set(initialBehavior, ctx); + final Field persistenceId = AbstractClientActorContext.class.getDeclaredField("persistenceId"); + persistenceId.setAccessible(true); + persistenceId.set(ctx, MEMBER_1_FRONTEND_TYPE_1); + return initialBehavior; + } + + private MockedSnapshotStore.SaveRequest handleRecovery(final SelectedSnapshot savedState) { + probe.expectMsgClass(MockedSnapshotStore.LoadRequest.class); + //offer snapshot + probe.reply(Optional.ofNullable(savedState)); + final MockedSnapshotStore.SaveRequest nextSaveRequest = + probe.expectMsgClass(MockedSnapshotStore.SaveRequest.class); + probe.reply(Void.TYPE); + //check old snapshots deleted + probe.expectMsgClass(MockedSnapshotStore.DeleteByCriteriaRequest.class); + probe.reply(Void.TYPE); + return nextSaveRequest; + } + + private static class MockedActor extends AbstractClientActor { + + private final ClientActorBehavior initialBehavior; + + private static Props props(final FrontendIdentifier frontendId, final ClientActorBehavior initialBehavior) { + return Props.create(MockedActor.class, () -> new MockedActor(frontendId, initialBehavior)); + } + + private MockedActor(final FrontendIdentifier frontendId, final ClientActorBehavior initialBehavior) { + super(frontendId); + this.initialBehavior = initialBehavior; + } + + @Override + protected ClientActorBehavior initialBehavior(final ClientActorContext context) { + return initialBehavior; + } + + } + +} \ No newline at end of file diff --git a/opendaylight/md-sal/cds-access-client/src/test/java/org/opendaylight/controller/cluster/access/client/InversibleLockTest.java b/opendaylight/md-sal/cds-access-client/src/test/java/org/opendaylight/controller/cluster/access/client/InversibleLockTest.java new file mode 100644 index 0000000000..56b3f54ac9 --- /dev/null +++ b/opendaylight/md-sal/cds-access-client/src/test/java/org/opendaylight/controller/cluster/access/client/InversibleLockTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2017 Pantheon Technologies s.r.o. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.controller.cluster.access.client; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class InversibleLockTest { + + private InversibleLock lock; + private ScheduledExecutorService executor; + + @Before + public void setUp() throws Exception { + lock = new InversibleLock(); + executor = Executors.newScheduledThreadPool(1); + } + + @After + public void tearDown() throws Exception { + executor.shutdownNow(); + } + + @Test(timeout = 2000) + public void testWriteLockUnlock() throws Exception { + final long stamp = lock.writeLock(); + Assert.assertTrue(lock.validate(stamp)); + executor.schedule(() -> lock.unlockWrite(stamp), 500, TimeUnit.MILLISECONDS); + try { + lock.optimisticRead(); + } catch (final InversibleLockException e) { + e.awaitResolution(); + } + } + + @Test + public void testLockAfterRead() throws Exception { + final long readStamp = lock.optimisticRead(); + lock.writeLock(); + Assert.assertFalse(lock.validate(readStamp)); + } +} \ No newline at end of file diff --git a/opendaylight/md-sal/cds-access-client/src/test/java/org/opendaylight/controller/cluster/access/client/MockedSnapshotStore.java b/opendaylight/md-sal/cds-access-client/src/test/java/org/opendaylight/controller/cluster/access/client/MockedSnapshotStore.java new file mode 100644 index 0000000000..0071a0b24b --- /dev/null +++ b/opendaylight/md-sal/cds-access-client/src/test/java/org/opendaylight/controller/cluster/access/client/MockedSnapshotStore.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2017 Pantheon Technologies s.r.o. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.controller.cluster.access.client; + +import akka.actor.ActorRef; +import akka.dispatch.OnComplete; +import akka.pattern.Patterns; +import akka.persistence.SelectedSnapshot; +import akka.persistence.SnapshotMetadata; +import akka.persistence.SnapshotSelectionCriteria; +import akka.persistence.snapshot.japi.SnapshotStore; +import com.google.common.base.Preconditions; +import java.util.Optional; +import scala.concurrent.Future; +import scala.concurrent.Promise; + +/** + * Instantiated by akka. MockedSnapshotStore forwards method calls as + * {@link MockedSnapshotStoreMessage} messages to delegate actor. Delegate reference + * must be sent as a message to this snapshot store. + */ +class MockedSnapshotStore extends SnapshotStore { + + private static final long TIMEOUT = 1000; + + private ActorRef delegate; + + /** + * Marker interface for messages produced by MockedSnapshotStore. + */ + interface MockedSnapshotStoreMessage { + } + + @Override + public Future> doLoadAsync(final String persistenceId, + final SnapshotSelectionCriteria criteria) { + return askDelegate(new LoadRequest(persistenceId, criteria)); + } + + @Override + public Future doSaveAsync(final SnapshotMetadata metadata, final Object snapshot) { + return askDelegate(new SaveRequest(metadata, snapshot)); + } + + @Override + public Future doDeleteAsync(final SnapshotMetadata metadata) { + return askDelegate(new DeleteByMetadataRequest(metadata)); + } + + @Override + public Future doDeleteAsync(final String persistenceId, final SnapshotSelectionCriteria criteria) { + return askDelegate(new DeleteByCriteriaRequest(persistenceId, criteria)); + } + + @Override + public void unhandled(final Object message) { + if (message instanceof ActorRef) { + delegate = (ActorRef) message; + return; + } + super.unhandled(message); + } + + private Future askDelegate(final MockedSnapshotStoreMessage message) { + Preconditions.checkNotNull(delegate, "Delegate ref wasn't sent"); + final Future ask = Patterns.ask(delegate, message, TIMEOUT); + return transform(ask); + } + + private Future transform(final Future future) { + final Promise promise = new scala.concurrent.impl.Promise.DefaultPromise<>(); + future.onComplete(new OnComplete() { + @Override + public void onComplete(final Throwable failure, final Object success) throws Throwable { + if (success instanceof Throwable) { + promise.failure((Throwable) success); + return; + } + if (success == Void.TYPE) { + promise.success(null); + return; + } + promise.success((T) success); + } + }, context().dispatcher()); + return promise.future(); + } + + class LoadRequest implements MockedSnapshotStoreMessage { + private final String persistenceId; + private final SnapshotSelectionCriteria criteria; + + LoadRequest(final String persistenceId, final SnapshotSelectionCriteria criteria) { + this.persistenceId = persistenceId; + this.criteria = criteria; + } + + public String getPersistenceId() { + return persistenceId; + } + + public SnapshotSelectionCriteria getCriteria() { + return criteria; + } + } + + class DeleteByCriteriaRequest implements MockedSnapshotStoreMessage { + private final String persistenceId; + private final SnapshotSelectionCriteria criteria; + + DeleteByCriteriaRequest(final String persistenceId, final SnapshotSelectionCriteria criteria) { + this.persistenceId = persistenceId; + this.criteria = criteria; + } + + public String getPersistenceId() { + return persistenceId; + } + + public SnapshotSelectionCriteria getCriteria() { + return criteria; + } + } + + class DeleteByMetadataRequest implements MockedSnapshotStoreMessage { + private final SnapshotMetadata metadata; + + DeleteByMetadataRequest(final SnapshotMetadata metadata) { + this.metadata = metadata; + } + + public SnapshotMetadata getMetadata() { + return metadata; + } + } + + class SaveRequest implements MockedSnapshotStoreMessage { + private final SnapshotMetadata metadata; + private final Object snapshot; + + SaveRequest(final SnapshotMetadata metadata, final Object snapshot) { + this.metadata = metadata; + this.snapshot = snapshot; + } + + public SnapshotMetadata getMetadata() { + return metadata; + } + + public Object getSnapshot() { + return snapshot; + } + } +} diff --git a/opendaylight/md-sal/cds-access-client/src/test/resources/application.conf b/opendaylight/md-sal/cds-access-client/src/test/resources/application.conf new file mode 100644 index 0000000000..b651f6a2b9 --- /dev/null +++ b/opendaylight/md-sal/cds-access-client/src/test/resources/application.conf @@ -0,0 +1,17 @@ +akka { + persistence.snapshot-store.plugin = "in-memory-snapshot-store" + persistence.journal.plugin = "in-memory-journal" + + loggers = ["akka.testkit.TestEventListener", "akka.event.slf4j.Slf4jLogger"] +} + +in-memory-journal { + class = "akka.persistence.journal.inmem.InmemJournal" +} + +in-memory-snapshot-store { + # Class name of the plugin. + class = "org.opendaylight.controller.cluster.access.client.MockedSnapshotStore" + # Dispatcher for the plugin actor. + plugin-dispatcher = "akka.persistence.dispatchers.default-plugin-dispatcher" +} \ No newline at end of file -- 2.36.6