package alma.acs.nc.sm; import static alma.acs.nc.sm.generated.EventSubscriberSignal.cleanUpEnvironment; import static alma.acs.nc.sm.generated.EventSubscriberSignal.resume; import static alma.acs.nc.sm.generated.EventSubscriberSignal.setUpEnvironment; import static alma.acs.nc.sm.generated.EventSubscriberSignal.startReceivingEvents; import static alma.acs.nc.sm.generated.EventSubscriberSignal.stopReceivingEvents; import static alma.acs.nc.sm.generated.EventSubscriberSignal.suspend; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.lessThan; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; import java.util.Collection; import java.util.Set; import org.apache.commons.scxml.ErrorReporter; import org.apache.commons.scxml.EventDispatcher; import org.apache.commons.scxml.SCInstance; import org.apache.commons.scxml.TriggerEvent; import org.junit.After; import org.junit.Before; import org.junit.Test; import alma.ACSErrTypeCommon.wrappers.AcsJIllegalStateEventEx; import alma.ACSErrTypeCommon.wrappers.AcsJStateMachineActionEx; import alma.acs.logging.ClientLogManager; import alma.acs.logging.config.LogConfig; import alma.acs.logging.level.AcsLogLevelDefinition; import alma.acs.logging.testsupport.JUnit4StandaloneTestBase; import alma.acs.nc.sm.generated.EventSubscriberActionDispatcher; import alma.acs.nc.sm.generated.EventSubscriberAllActionsHandler; import alma.acs.nc.sm.generated.EventSubscriberSignal; import alma.acs.nc.sm.generic.AcsScxmlEngine; import alma.acs.util.StopWatch; /** * A stand-alone test for the generated SCXML-based subscriber state machine. */ public class EventSubscriberSmEngineTest extends JUnit4StandaloneTestBase { private MyActionHandler actionHandler; private EventSubscriberStateMachine engine; /** * Optionally throws an exception in {@link #createEnvironmentAction(EventDispatcher, ErrorReporter, SCInstance, Collection)}, * which gets used in {@link EventSubscriberSmEngineTest#testActionException()}. */ private static class MyActionHandler implements EventSubscriberAllActionsHandler { public volatile boolean throwExceptionInCreateEnvironment = false; @Override public void createEnvironmentAction(EventDispatcher evtDispatcher, ErrorReporter errRep, SCInstance scInstance, Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx { if (throwExceptionInCreateEnvironment) { throw new AcsJStateMachineActionEx(); } } @Override public void destroyEnvironmentAction(EventDispatcher evtDispatcher, ErrorReporter errRep, SCInstance scInstance, Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx { // nothing } @Override public void createConnectionAction(EventDispatcher evtDispatcher, ErrorReporter errRep, SCInstance scInstance, Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx { // nothing } @Override public void destroyConnectionAction(EventDispatcher evtDispatcher, ErrorReporter errRep, SCInstance scInstance, Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx { // nothing } @Override public void suspendAction(EventDispatcher evtDispatcher, ErrorReporter errRep, SCInstance scInstance, Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx { // nothing } @Override public void resumeAction(EventDispatcher evtDispatcher, ErrorReporter errRep, SCInstance scInstance, Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx { // nothing } } @Before public void setUp() throws Exception { super.setUp(); configureLogging(AcsLogLevelDefinition.INFO); StopWatch sw = new StopWatch(); // we use empty handlers, except for environmentActionHandler actionHandler = new MyActionHandler(); engine = new EventSubscriberStateMachine(logger, new EventSubscriberActionDispatcher(actionHandler, logger)); // usually it's quite a bit faster, but for some reason the NRI test on te48 takes ~1.2 seconds assertThat("SM creation should take less than a 1.4 second.", sw.getLapTimeMillis(), lessThan(1400L)); } @After public void tearDown() throws Exception { // TODO: nothing to clean up in the engine?? super.tearDown(); } private void configureLogging(AcsLogLevelDefinition localLevel) { String scxmlLoggerName = "scxml@" + testMethodName; LogConfig logConfig = ClientLogManager.getAcsLogManager().getLogConfig(); logConfig.setMinLogLevelLocal(localLevel, scxmlLoggerName); logConfig.setMinLogLevel(AcsLogLevelDefinition.OFF, scxmlLoggerName); ClientLogManager.getAcsLogManager().suppressRemoteLogging(); } /** * Got through the SM's lifecycle. * Everything neat, no exceptions expected. */ @Test public void testSimpleTransitions() throws Exception { assertThat(engine.getCurrentState(), equalTo("EnvironmentUnknown")); assertThat("SM should be in a non-final state.", engine.setUpEnvironment(), is(false)); assertThat(engine.getCurrentState(), equalTo("EnvironmentCreated::Disconnected")); assertThat(engine.startReceivingEvents(), is(false)); assertThat(engine.getCurrentState(), equalTo("EnvironmentCreated::Connected::Active")); assertThat(engine.suspend(), is(false)); assertThat(engine.getCurrentState(), equalTo("EnvironmentCreated::Connected::Suspended")); assertThat(engine.resume(), is(false)); assertThat(engine.getCurrentState(), equalTo("EnvironmentCreated::Connected::Active")); assertThat(engine.suspend(), is(false)); // If the following fails because the states were renamed, make sure to fix also AcsEventSubscriberImplBase#isSuspended() assertThat(engine.getCurrentState(), equalTo("EnvironmentCreated::Connected::Suspended")); assertThat(engine.resume(), is(false)); assertThat(engine.getCurrentState(), equalTo("EnvironmentCreated::Connected::Active")); assertThat(engine.stopReceivingEvents(), is(false)); assertThat(engine.getCurrentState(), equalTo("EnvironmentCreated::Disconnected")); assertThat(engine.cleanUpEnvironment(), is(false)); assertThat(engine.getCurrentState(), equalTo("EnvironmentUnknown")); } /** * Verifies the behavior of the SM for signals not applicable for the current state: * <ul> * <li> {@link AcsScxmlEngine#fireSignal(Enum)} and its wrapper methods such as * {@link EventSubscriberStateMachine#stopReceivingEvents()} should silently ignore the event. * <li> {@link AcsScxmlEngine#fireSignalWithErrorFeedback(Enum)} should throw AcsJIllegalStateEventEx. * </ul> */ @Test public void testIllegalSignal() throws Exception { // here the wrong event should be just ignored assertThat(engine.getCurrentState(), equalTo("EnvironmentUnknown")); assertThat(engine.fireSignal(resume), is(false)); assertThat(engine.getCurrentState(), equalTo("EnvironmentUnknown")); // here we expect AcsJIllegalStateEventEx try { engine.fireSignalWithErrorFeedback(stopReceivingEvents); fail("AcsJIllegalStateEventEx expected."); } catch (AcsJIllegalStateEventEx ex) { assertThat(ex.getState(), equalTo("EnvironmentUnknown")); assertThat(ex.getEvent(), equalTo(stopReceivingEvents.name())); } assertThat(engine.getCurrentState(), equalTo("EnvironmentUnknown")); } /** * Tests {@link AcsScxmlEngine#getApplicableSignals()} for different states. */ @Test public void testGetApplicableSignals() { Set<EventSubscriberSignal> signals = engine.getScxmlEngine().getApplicableSignals(); assertThat(signals, contains(setUpEnvironment)); engine.fireSignal(setUpEnvironment); signals = engine.getScxmlEngine().getApplicableSignals(); assertThat(signals, containsInAnyOrder(cleanUpEnvironment, startReceivingEvents)); // verifies also set size == 2 } /** * Verifies the behavior of the SM for an action that throws AcsJStateMachineActionEx: * <ul> * <li> {@link AcsScxmlEngine#fireSignalWithErrorFeedback(Enum)} should throw this AcsJStateMachineActionEx * but nonetheless move on to the target state unless the SM foresees internal error transitions. * <li> {@link AcsScxmlEngine#fireSignalWith(Enum)} should log a warning message (not yet automatically verified). * </ul> */ @Test public void testActionException() throws Exception { // provoke an exception in the create action actionHandler.throwExceptionInCreateEnvironment = true; try { engine.fireSignalWithErrorFeedback(setUpEnvironment); fail("AcsJStateMachineActionEx expected"); } catch (AcsJStateMachineActionEx ex) { // good } // The state changes, even when the action fails. Restore to initial state. assertThat(engine.getCurrentState(), equalTo("EnvironmentCreated::Disconnected")); engine.fireSignal(cleanUpEnvironment); // Without exception feedback, we expect just a warning log: // WARNING [testActionException] Handler class alma.acs.nc.sm.EventSubscriberSmEngineTest$MyEnvironmentActionHandler failed to execute action createEnvironment // alma.acs.nc.sm.generic.AcsJStateMachineActionEx // at alma.acs.nc.sm.EventSubscriberSmEngineTest$MyEnvironmentActionHandler.create(EventSubscriberSmEngineTest.java:68) engine.fireSignal(setUpEnvironment); } /** * Tests {@link AcsScxmlEngine#isStateActive(String)} for different states. */ @Test public void testIsStateActive() { assertThat(engine.getScxmlEngine().isStateActive("EnvironmentUnknown"), is(true)); assertThat(engine.getScxmlEngine().isStateActive("EnvironmentCreated"), is(false)); assertThat(engine.getScxmlEngine().isStateActive("Disconnected"), is(false)); engine.fireSignal(setUpEnvironment); assertThat(engine.getScxmlEngine().isStateActive("EnvironmentUnknown"), is(false)); assertThat(engine.getScxmlEngine().isStateActive("EnvironmentCreated"), is(true)); assertThat(engine.getScxmlEngine().isStateActive("Disconnected"), is(true)); assertThat(engine.getScxmlEngine().isStateActive("EnvironmentCreated::Disconnected"), is(true)); } /** * Cycles 5.000 times between suspended and resumed state * and asserts that the state machine overhead be less than 0.6 ms per event. * This is done both with and without the check for allowed events. */ @Test public void testTransitionPerformance() throws Exception { // Configure logger "scxml@testTransitionPerformance" to higher than the INFO setting // used in the other tests, because otherwise the SM is much too slow. configureLogging(AcsLogLevelDefinition.WARNING); final int cycles = 5000; engine.fireSignal(setUpEnvironment); engine.fireSignal(startReceivingEvents); StopWatch sw = new StopWatch(); for (int i = 0; i < cycles; i++) { engine.fireSignal(suspend); engine.fireSignal(resume); } long elapsed = sw.getLapTimeMillis(); logger.info("suspend/resume cycles: cycles = " + cycles + ", elapsed time ms = " + elapsed); // 5.000 cycles mean 10.000 signals/transitions, so 6000 ms max means <= 0.6 ms per transition. // Usually its around 1000 ms, but NRI machine te48 has gotten slow and reports failures regularly. assertThat("SM transitions too slow.", elapsed, lessThan(6000L)); // Now do the same again, but including the check for applicable events // It seems that this second run is even faster, in spite of the additional check. // JIT optimization or some initial setup seem to distort the performance results. sw = new StopWatch(); engine.getScxmlEngine(); for (int i = 0; i < cycles; i++) { engine.fireSignalWithErrorFeedback(EventSubscriberSignal.suspend); engine.fireSignalWithErrorFeedback(EventSubscriberSignal.resume); } elapsed = sw.getLapTimeMillis(); logger.info("suspend/resume cycles with event checks: cycles = " + cycles + ", elapsed time ms = " + elapsed); assertThat("SM transitions too slow.", elapsed, lessThan(6000L)); } }