diff options
3 files changed, 470 insertions, 26 deletions
diff --git a/feature-test-transaction/src/main/java/org/onap/policy/drools/testtransaction/TestTransaction.java b/feature-test-transaction/src/main/java/org/onap/policy/drools/testtransaction/TestTransaction.java index c778bf67..452825c0 100644 --- a/feature-test-transaction/src/main/java/org/onap/policy/drools/testtransaction/TestTransaction.java +++ b/feature-test-transaction/src/main/java/org/onap/policy/drools/testtransaction/TestTransaction.java @@ -65,29 +65,29 @@ class TTImpl implements TestTransaction { @Override public synchronized void register(PolicyController controller) { - if (this.controllers.containsValue(controller)) { - final TTControllerTask controllerTask = this.controllers.get(controller.getName()); - if (controllerTask.isAlive()) { - return; - } - - // continue : unregister, register operation + TTControllerTask controllerTask = this.controllers.get(controller.getName()); + if (controllerTask != null && controllerTask.isAlive()) { + return; } - final TTControllerTask controllerTask = new TTControllerTask(controller); + // continue : unregister, register operation + + controllerTask = makeControllerTask(controller); this.controllers.put(controller.getName(), controllerTask); } @Override public synchronized void unregister(PolicyController controller) { - if (!this.controllers.containsValue(controller)) { - return; + final TTControllerTask controllerTask = this.controllers.remove(controller.getName()); + if (controllerTask != null) { + controllerTask.stop(); } + } - final TTControllerTask controllerTask = this.controllers.get(controller.getName()); - controllerTask.stop(); + // these may be overridden by junit tests - this.controllers.remove(controller.getName()); + protected TTControllerTask makeControllerTask(PolicyController controller) { + return new TTControllerTask(controller); } } @@ -103,7 +103,7 @@ class TTControllerTask implements Runnable { protected final PolicyController controller; protected volatile boolean alive = true; - protected final Thread thread = new Thread(this); + protected final Thread thread = makeThread(this); public TTControllerTask(PolicyController controller) { this.controller = controller; @@ -123,7 +123,7 @@ class TTControllerTask implements Runnable { this.alive = false; this.thread.interrupt(); try { - this.thread.join(1000); + joinThread(1000); } catch (final InterruptedException e) { logger.error("TestTransaction thread threw", e); this.thread.interrupt(); @@ -163,13 +163,13 @@ class TTControllerTask implements Runnable { return; } - if (!Thread.currentThread().isInterrupted()) { - Thread.sleep(TestTransaction.DEFAULT_TT_TASK_SLEEP); + if (!getCurrentThread().isInterrupted()) { + doSleep(TestTransaction.DEFAULT_TT_TASK_SLEEP); } } } catch (final InterruptedException e) { logger.info("{}: stopping ...", this, e); - Thread.currentThread().interrupt(); + getCurrentThread().interrupt(); } catch (final IllegalArgumentException e) { logger.error( "{}: controller {} has not been enabled for testing: ", @@ -245,4 +245,22 @@ class TTControllerTask implements Runnable { builder.append("]"); return builder.toString(); } + + // these may be overridden by junit tests + + protected Thread makeThread(Runnable action) { + return new Thread(action); + } + + protected void joinThread(long waitTimeMs) throws InterruptedException { + this.thread.join(waitTimeMs); + } + + protected void doSleep(long sleepMs) throws InterruptedException { + Thread.sleep(sleepMs); + } + + protected Thread getCurrentThread() { + return Thread.currentThread(); + } } diff --git a/feature-test-transaction/src/test/java/org/onap/policy/drools/testtransaction/TestTransactionTest.java b/feature-test-transaction/src/test/java/org/onap/policy/drools/testtransaction/TestTransactionTest.java index 09be93f9..40b1ff9d 100644 --- a/feature-test-transaction/src/test/java/org/onap/policy/drools/testtransaction/TestTransactionTest.java +++ b/feature-test-transaction/src/test/java/org/onap/policy/drools/testtransaction/TestTransactionTest.java @@ -29,7 +29,8 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Properties; import java.util.Set; - +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import org.junit.BeforeClass; import org.junit.Test; import org.onap.policy.drools.persistence.SystemPersistence; @@ -75,30 +76,46 @@ public class TestTransactionTest { PolicyEngine.manager.createPolicyController(TEST_CONTROLLER_NAME, controllerProperties); assertNotNull(PolicyController.factory.get(TEST_CONTROLLER_NAME)); logger.info(controller.toString()); - - TestTransaction.manager.register(controller); + + CountDownLatch latch = new CountDownLatch(1); + + // use our own impl so we can decrement the latch when run() completes + TTImpl impl = new TTImpl() { + @Override + protected TTControllerTask makeControllerTask(PolicyController controller) { + return new TTControllerTask(controller) { + @Override + public void run() { + super.run(); + latch.countDown(); + } + }; + } + }; + + impl.register(controller); assertNotNull(TestTransaction.manager); /* * Unregistering the controller should terminate its TestTransaction thread if it hasn't already * been terminated */ - TestTransaction.manager.unregister(controller); + impl.unregister(controller); - Thread ttThread = this.getThread("tt-controller-task-" + TEST_CONTROLLER_NAME); + Thread ttThread = getThread(latch, "tt-controller-task-" + TEST_CONTROLLER_NAME); assertEquals(null, ttThread); } /** * Returns thread object based on String name. - * + * @param latch indicates when the thread has finished running * @param threadName thread name * @return the thread * @throws InterruptedException exception */ - public Thread getThread(String threadName) throws InterruptedException { + public Thread getThread(CountDownLatch latch, String threadName) throws InterruptedException { // give a chance to the transaction thread to be spawned/destroyed - Thread.sleep(5000L); + latch.await(5, TimeUnit.SECONDS); final Set<Thread> threadSet = Thread.getAllStackTraces().keySet(); for (final Thread thread : threadSet) { diff --git a/feature-test-transaction/src/test/java/org/onap/policy/drools/testtransaction/TestTransactionTest2.java b/feature-test-transaction/src/test/java/org/onap/policy/drools/testtransaction/TestTransactionTest2.java new file mode 100644 index 00000000..7c31ba98 --- /dev/null +++ b/feature-test-transaction/src/test/java/org/onap/policy/drools/testtransaction/TestTransactionTest2.java @@ -0,0 +1,409 @@ +/*- + * ============LICENSE_START======================================================= + * feature-test-transaction + * ================================================================================ + * Copyright (C) 2017-2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.testtransaction; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collections; +import java.util.EventObject; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Before; +import org.junit.Test; +import org.onap.policy.drools.controller.DroolsController; +import org.onap.policy.drools.core.PolicyContainer; +import org.onap.policy.drools.system.PolicyController; + +public class TestTransactionTest2 { + + private static final int MAX_SLEEP_COUNT = 3; + private static final String EXPECTED = "expected exception"; + private static final String CONTROLLER1 = "controller-a"; + private static final String CONTROLLER2 = "controller-b"; + private static final String CONTROLLER3 = "controller-c"; + private static final String SESSION1 = "session-a"; + private static final String SESSION2 = "session-b"; + private static final List<String> sessions = Arrays.asList(SESSION1, SESSION2); + private static final List<Object> facts = Arrays.asList(0L); + + private Thread theThread; + private PolicyController controller; + private PolicyController controller2; + private PolicyController controller3; + private Runnable theAction; + private long waitJoinMs; + private long doSleepMs; + private DroolsController drools; + private PolicyContainer container; + private Map<String, TTControllerTask> name2task; + private TTControllerTask task; + private TTControllerTask task2; + private TTControllerTask task3; + private TestTransTImplTester impl; + + /** + * Initialize objects for each test. + */ + @Before + public void setUp() { + theThread = mock(Thread.class); + controller = mock(PolicyController.class); + controller2 = mock(PolicyController.class); + controller3 = mock(PolicyController.class); + theAction = null; + waitJoinMs = -1; + doSleepMs = -1; + drools = mock(DroolsController.class); + container = mock(PolicyContainer.class); + task2 = mock(TTControllerTask.class); + task3 = mock(TTControllerTask.class); + name2task = new TreeMap<>(); + + when(drools.getSessionNames()).thenReturn(sessions); + when(drools.isBrained()).thenReturn(true); + when(drools.factQuery(anyString(), anyString(), anyString(), anyBoolean())).thenReturn(facts); + when(drools.getContainer()).thenReturn(container); + + when(controller.getName()).thenReturn(CONTROLLER1); + when(controller.getDrools()).thenReturn(drools); + when(controller.isAlive()).thenReturn(true); + + when(controller2.getName()).thenReturn(CONTROLLER2); + when(controller2.getDrools()).thenReturn(drools); + when(controller2.isAlive()).thenReturn(true); + + when(controller3.getName()).thenReturn(CONTROLLER3); + when(controller3.getDrools()).thenReturn(drools); + when(controller3.isAlive()).thenReturn(true); + + task = new TestTransControllerTaskTester(controller); + + name2task.put(CONTROLLER1, task); + name2task.put(CONTROLLER2, task2); + name2task.put(CONTROLLER3, task3); + + impl = new TestTransTImplTester(); + } + + @Test + public void testTestTransactionImpl() { + assertNotNull(TTImpl.manager); + } + + @Test + public void testTestTransactionImplRegister_testTestTransactionImplUnregister() { + task = mock(TTControllerTask.class); + when(task.isAlive()).thenReturn(true); + name2task.put(CONTROLLER1, task); + + impl.register(controller); + impl.register(controller2); + + // re-register + impl.register(controller); + + // re-register when task is not running + + // give controller3 same name as controller1 -> task3 replaces task + when(controller3.getName()).thenReturn(CONTROLLER1); + name2task.put(CONTROLLER1, task3); + when(task.isAlive()).thenReturn(false); + impl.register(controller3); + + impl.unregister(controller); + verify(task, never()).stop(); + verify(task2, never()).stop(); + verify(task3).stop(); + + impl.unregister(controller2); + verify(task2).stop(); + + // unregister again - stop() should not be called again + impl.unregister(controller3); + verify(task3).stop(); + + // unregister original controller - no stop() should be called again + impl.unregister(controller); + verify(task, never()).stop(); + verify(task2).stop(); + verify(task3).stop(); + } + + @Test + public void testTestTransactionControllerTaskFactory() throws Exception { + task = new TTControllerTask(controller) { + @Override + protected Thread makeThread(Runnable action) { + return theThread; + } + + @Override + protected void joinThread(long waitTimeMs) throws InterruptedException { + // do nothing + } + }; + + task.doSleep(1); + assertEquals(Thread.currentThread(), task.getCurrentThread()); + } + + @Test + public void testTestTransactionControllerTask() { + assertEquals(task, theAction); + assertTrue(task.isAlive()); + assertEquals(controller, task.getController()); + assertEquals(theThread, task.getThread()); + + verify(theThread).start(); + } + + @Test + public void testTestTransactionControllerTaskGetController() { + assertEquals(controller, task.getController()); + } + + @Test + public void testTestTransactionControllerTaskGetThread() { + assertEquals(theThread, task.getThread()); + } + + @Test + public void testTestTransactionControllerTaskStop() throws Exception { + task.stop(); + assertFalse(task.isAlive()); + verify(theThread).interrupt(); + assertTrue(waitJoinMs > 0); + + // throw interrupt during join() + setUp(); + task = new TestTransControllerTaskTester(controller) { + @Override + protected void joinThread(long waitTimeMs) throws InterruptedException { + waitJoinMs = waitTimeMs; + throw new InterruptedException(EXPECTED); + } + }; + task.stop(); + assertFalse(task.isAlive()); + verify(theThread, times(2)).interrupt(); + assertTrue(waitJoinMs > 0); + } + + @Test + public void testTestTransactionControllerTaskRun() { + task.run(); + assertFalse(task.isAlive()); + verify(theThread, never()).interrupt(); + verify(controller, times(MAX_SLEEP_COUNT + 1)).isAlive(); + assertTrue(doSleepMs > 0); + + // not brained + setUp(); + when(drools.isBrained()).thenReturn(false); + task.run(); + assertFalse(task.isAlive()); + verify(controller, never()).isAlive(); + assertEquals(-1, doSleepMs); + + // controller not running + setUp(); + when(controller.isAlive()).thenReturn(false); + task.run(); + assertFalse(task.isAlive()); + assertEquals(-1, doSleepMs); + + // controller is locked + setUp(); + when(controller.isLocked()).thenReturn(true); + task.run(); + assertFalse(task.isAlive()); + assertEquals(-1, doSleepMs); + + // un-brain during sleep + setUp(); + task = new TestTransControllerTaskTester(controller) { + @Override + protected void doSleep(long sleepMs) throws InterruptedException { + when(drools.isBrained()).thenReturn(false); + super.doSleep(sleepMs); + } + }; + task.run(); + assertFalse(task.isAlive()); + // only hit top of the loop twice + verify(controller, times(2)).isAlive(); + assertTrue(doSleepMs > 0); + + // stop during sleep + setUp(); + task = new TestTransControllerTaskTester(controller) { + @Override + protected void doSleep(long sleepMs) throws InterruptedException { + task.stop(); + super.doSleep(sleepMs); + } + }; + task.run(); + assertFalse(task.isAlive()); + // only hit top of the loop twice + verify(controller, times(2)).isAlive(); + assertTrue(doSleepMs > 0); + + // isInterrupted() returns true the first time, interrupt next time + setUp(); + AtomicInteger count = new AtomicInteger(1); + when(theThread.isInterrupted()).thenAnswer(args -> { + if (count.decrementAndGet() >= 0) { + return true; + } else { + throw new InterruptedException(EXPECTED); + } + }); + task.run(); + assertFalse(task.isAlive()); + verify(controller, times(2)).isAlive(); + // doSleep() should not be called + assertEquals(-1, doSleepMs); + + // interrupt during sleep + setUp(); + task = new TestTransControllerTaskTester(controller) { + @Override + protected void doSleep(long sleepMs) throws InterruptedException { + super.doSleep(sleepMs); + throw new InterruptedException(EXPECTED); + } + }; + task.run(); + assertFalse(task.isAlive()); + verify(theThread).interrupt(); + // only hit top of the loop once + verify(controller).isAlive(); + assertTrue(doSleepMs > 0); + + // stop() during factQuery() + setUp(); + when(drools.factQuery(anyString(), anyString(), anyString(), anyBoolean())).thenAnswer(args -> { + task.stop(); + return facts; + }); + task.run(); + assertFalse(task.isAlive()); + // only hit top of the loop once + verify(controller).isAlive(); + + // exception during isBrained() check + setUp(); + when(drools.isBrained()).thenThrow(new IllegalArgumentException(EXPECTED)); + task.run(); + assertFalse(task.isAlive()); + + // other exception during isBrained() check + setUp(); + when(drools.isBrained()).thenThrow(new RuntimeException(EXPECTED)); + task.run(); + assertFalse(task.isAlive()); + } + + @Test + public void testTestTransactionControllerTaskInjectTxIntoSessions() { + task.run(); + verify(container, times(MAX_SLEEP_COUNT * sessions.size())).insert(anyString(), any(EventObject.class)); + + // null facts + setUp(); + when(drools.factQuery(anyString(), anyString(), anyString(), anyBoolean())).thenReturn(null); + task.run(); + verify(container, never()).insert(anyString(), any()); + + // empty fact list + setUp(); + when(drools.factQuery(anyString(), anyString(), anyString(), anyBoolean())).thenReturn(Collections.emptyList()); + task.run(); + verify(container, never()).insert(anyString(), any()); + } + + @Test + public void testTestTransactionControllerTaskToString() { + assertTrue(task.toString().startsWith("TTControllerTask [")); + } + + /** + * TestTransaction with overridden methods. + */ + private class TestTransTImplTester extends TTImpl { + + @Override + protected TTControllerTask makeControllerTask(PolicyController controller) { + return name2task.get(controller.getName()); + } + } + + /** + * Controller task with overridden methods. + */ + private class TestTransControllerTaskTester extends TTControllerTask { + private int sleepCount = MAX_SLEEP_COUNT; + + public TestTransControllerTaskTester(PolicyController controller) { + super(controller); + } + + @Override + protected Thread makeThread(Runnable action) { + theAction = action; + return theThread; + } + + @Override + protected void joinThread(long waitTimeMs) throws InterruptedException { + waitJoinMs = waitTimeMs; + } + + @Override + protected void doSleep(long sleepMs) throws InterruptedException { + doSleepMs = sleepMs; + + if (--sleepCount <= 0) { + when(controller.isAlive()).thenReturn(false); + } + } + + @Override + protected Thread getCurrentThread() { + return thread; + } + } +} |