diff options
Diffstat (limited to 'src/lib/util/tests/state_model_unittest.cc')
-rw-r--r-- | src/lib/util/tests/state_model_unittest.cc | 916 |
1 files changed, 916 insertions, 0 deletions
diff --git a/src/lib/util/tests/state_model_unittest.cc b/src/lib/util/tests/state_model_unittest.cc new file mode 100644 index 0000000..eaaba73 --- /dev/null +++ b/src/lib/util/tests/state_model_unittest.cc @@ -0,0 +1,916 @@ +// Copyright (C) 2013-2020 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <util/state_model.h> + +#include <gtest/gtest.h> + +using namespace std; +using namespace isc; +using namespace isc::util; + +namespace { + +/// @brief Test derivation of StateModel for exercising state model mechanics. +/// +/// This class facilitates testing by making non-public methods accessible so +/// they can be invoked directly in test routines. It implements a very +/// rudimentary state model, sufficient to test the state model mechanics +/// supplied by the base class. +class StateModelTest : public StateModel, public testing::Test { +public: + + ///@brief StateModelTest states + ///@brief Fake state used for handler mapping tests. + static const int DUMMY_ST = SM_DERIVED_STATE_MIN + 1; + + ///@brief Starting state for the test state model. + static const int READY_ST = SM_DERIVED_STATE_MIN + 2; + + ///@brief State which simulates doing asynchronous work. + static const int DO_WORK_ST = SM_DERIVED_STATE_MIN + 3; + + ///@brief State which finishes off processing. + static const int DONE_ST = SM_DERIVED_STATE_MIN + 4; + + ///@brief State in which model is always paused. + static const int PAUSE_ALWAYS_ST = SM_DERIVED_STATE_MIN + 5; + + ///@brief State in which model is paused at most once. + static const int PAUSE_ONCE_ST = SM_DERIVED_STATE_MIN + 6; + + // StateModelTest events + ///@brief Event used to trigger initiation of asynchronous work. + static const int WORK_START_EVT = SM_DERIVED_EVENT_MIN + 1; + + ///@brief Event issued when the asynchronous work "completes". + static const int WORK_DONE_EVT = SM_DERIVED_EVENT_MIN + 2; + + ///@brief Event issued when all the work is done. + static const int ALL_DONE_EVT = SM_DERIVED_EVENT_MIN + 3; + + ///@brief Event used to trigger an attempt to transition to bad state + static const int FORCE_UNDEFINED_ST_EVT = SM_DERIVED_EVENT_MIN + 4; + + ///@brief Event used to trigger an attempt to transition to bad state + static const int SIMULATE_ERROR_EVT = SM_DERIVED_EVENT_MIN + 5; + + ///@brief Event used to indicate that state machine is unpaused. + static const int UNPAUSED_EVT = SM_DERIVED_EVENT_MIN + 6; + + /// @brief Constructor + /// + /// Parameters match those needed by StateModel. + StateModelTest() : dummy_called_(false), work_completed_(false), + failure_explanation_("") { + } + /// @brief Destructor + virtual ~StateModelTest() { + } + + /// @brief Fetches the value of the dummy called flag. + bool getDummyCalled() { + return (dummy_called_); + } + + /// @brief StateHandler for fake state, DummyState. + /// + /// It simply sets the dummy called flag to indicate that this method + /// was invoked. + void dummyHandler() { + dummy_called_ = true; + } + + /// @brief Returns the failure explanation string. + /// + /// This value is set only via onModelFailure and it stores whatever + /// explanation that method was passed. + const std::string& getFailureExplanation() { + return (failure_explanation_); + } + + /// @brief Returns indication of whether or not the model succeeded. + /// + /// If true, this indicates that the test model executed correctly through + /// to completion. The flag is only by the DONE_ST handler. + bool getWorkCompleted() { + return (work_completed_); + } + + /// @brief State handler for the READY_ST. + /// + /// Serves as the starting state handler, it consumes the + /// START_EVT "transitioning" to the state, DO_WORK_ST and + /// sets the next event to WORK_START_EVT. + void readyHandler() { + switch(getNextEvent()) { + case START_EVT: + transition(DO_WORK_ST, WORK_START_EVT); + break; + default: + // its bogus + isc_throw(StateModelError, "readyHandler:invalid event: " + << getContextStr()); + } + } + + /// @brief State handler for the DO_WORK_ST. + /// + /// Simulates a state that starts some form of asynchronous work. + /// When next event is WORK_START_EVT it sets the status to pending + /// and signals the state model must "wait" for an event by setting + /// next event to NOP_EVT. + /// + /// When next event is IO_COMPLETED_EVT, it transitions to the state, + /// DONE_ST, and sets the next event to WORK_DONE_EVT. + void doWorkHandler() { + switch(getNextEvent()) { + case WORK_START_EVT: + postNextEvent(NOP_EVT); + break; + case WORK_DONE_EVT: + work_completed_ = true; + transition(DONE_ST, ALL_DONE_EVT); + break; + case FORCE_UNDEFINED_ST_EVT: + transition(9999, ALL_DONE_EVT); + break; + case SIMULATE_ERROR_EVT: + throw std::logic_error("Simulated Unexpected Error"); + break; + default: + // its bogus + isc_throw(StateModelError, "doWorkHandler:invalid event: " + << getContextStr()); + } + } + + /// @brief State handler for the DONE_ST. + /// + /// This is the last state in the model. Note that it sets the + /// status to completed and next event to NOP_EVT. + void doneWorkHandler() { + switch(getNextEvent()) { + case ALL_DONE_EVT: + endModel(); + break; + default: + // its bogus + isc_throw(StateModelError, "doneWorkHandler:invalid event: " + << getContextStr()); + } + } + + /// @brief State handler for PAUSE_ALWAYS_ST and PAUSE_ONCE_ST. + void pauseHandler() { + postNextEvent(NOP_EVT); + } + + /// @brief Construct the event dictionary. + virtual void defineEvents() { + // Invoke the base call implementation first. + StateModel::defineEvents(); + + // Define our events. + defineEvent(WORK_START_EVT, "WORK_START_EVT"); + defineEvent(WORK_DONE_EVT , "WORK_DONE_EVT"); + defineEvent(ALL_DONE_EVT, "ALL_DONE_EVT"); + defineEvent(FORCE_UNDEFINED_ST_EVT, "FORCE_UNDEFINED_ST_EVT"); + defineEvent(SIMULATE_ERROR_EVT, "SIMULATE_ERROR_EVT"); + defineEvent(UNPAUSED_EVT, "UNPAUSED_EVT"); + } + + /// @brief Verify the event dictionary. + virtual void verifyEvents() { + // Invoke the base call implementation first. + StateModel::verifyEvents(); + + // Verify our events. + getEvent(WORK_START_EVT); + getEvent(WORK_DONE_EVT); + getEvent(ALL_DONE_EVT); + getEvent(FORCE_UNDEFINED_ST_EVT); + getEvent(SIMULATE_ERROR_EVT); + getEvent(UNPAUSED_EVT); + } + + /// @brief Construct the state dictionary. + virtual void defineStates() { + // Invoke the base call implementation first. + StateModel::defineStates(); + + // Define our states. + defineState(DUMMY_ST, "DUMMY_ST", + std::bind(&StateModelTest::dummyHandler, this)); + + defineState(READY_ST, "READY_ST", + std::bind(&StateModelTest::readyHandler, this)); + + defineState(DO_WORK_ST, "DO_WORK_ST", + std::bind(&StateModelTest::doWorkHandler, this)); + + defineState(DONE_ST, "DONE_ST", + std::bind(&StateModelTest::doneWorkHandler, this)); + + defineState(PAUSE_ALWAYS_ST, "PAUSE_ALWAYS_ST", + std::bind(&StateModelTest::pauseHandler, this), + STATE_PAUSE_ALWAYS); + + defineState(PAUSE_ONCE_ST, "PAUSE_ONCE_ST", + std::bind(&StateModelTest::pauseHandler, this), + STATE_PAUSE_ONCE); + } + + /// @brief Verify the state dictionary. + virtual void verifyStates() { + // Invoke the base call implementation first. + StateModel::verifyStates(); + + // Verify our states. + getStateInternal(DUMMY_ST); + getStateInternal(READY_ST); + getStateInternal(DO_WORK_ST); + getStateInternal(DONE_ST); + getStateInternal(PAUSE_ALWAYS_ST); + getStateInternal(PAUSE_ONCE_ST); + } + + /// @brief Manually construct the event and state dictionaries. + /// This allows testing without running startModel. + void initDictionaries() { + ASSERT_NO_THROW(defineEvents()); + ASSERT_NO_THROW(verifyEvents()); + ASSERT_NO_THROW(defineStates()); + ASSERT_NO_THROW(verifyStates()); + } + + /// @brief Tests the event dictionary entry for the given event value. + bool checkEvent(const int value, const std::string& label) { + EventPtr event; + try { + event = getEvent(value); + EXPECT_TRUE(event); + EXPECT_EQ(value, event->getValue()); + EXPECT_EQ(label, event->getLabel()); + } catch (const std::exception& ex) { + return false; + } + + return true; + } + + /// @brief Tests the state dictionary entry for the given state value. + bool checkState(const int value, const std::string& label) { + EventPtr state; + try { + state = getState(value); + EXPECT_TRUE(state); + EXPECT_EQ(value, state->getValue()); + EXPECT_EQ(label, state->getLabel()); + } catch (const std::exception& ex) { + return false; + } + + return true; + } + + + /// @brief Handler called when the model suffers an execution error. + virtual void onModelFailure(const std::string& explanation) { + failure_explanation_ = explanation; + } + + /// @brief Indicator of whether or not the DUMMY_ST handler has been called. + bool dummy_called_; + + /// @brief Indicator of whether or not DONE_ST handler has been called. + bool work_completed_; + + /// @brief Stores the failure explanation + std::string failure_explanation_; +}; + +// Declare them so gtest can see them. +const int StateModelTest::DUMMY_ST; +const int StateModelTest::READY_ST; +const int StateModelTest::DO_WORK_ST; +const int StateModelTest::DONE_ST; +const int StateModelTest::WORK_START_EVT; +const int StateModelTest::WORK_DONE_EVT; +const int StateModelTest::ALL_DONE_EVT; +const int StateModelTest::PAUSE_ALWAYS_ST; +const int StateModelTest::PAUSE_ONCE_ST; + +/// @brief Checks the fundamentals of defining and retrieving events. +TEST_F(StateModelTest, eventDefinition) { + // After construction, the event dictionary should be empty. Verify that + // getEvent will throw when event is not defined. + EXPECT_THROW(getEvent(NOP_EVT), StateModelError); + + // Verify that we can add a handler to the map. + ASSERT_NO_THROW(defineEvent(NOP_EVT, "NOP_EVT")); + + // Verify that we can find the event by value and its content is correct. + EXPECT_TRUE(checkEvent(NOP_EVT, "NOP_EVT")); + + // Verify that we cannot add a duplicate. + ASSERT_THROW(defineEvent(NOP_EVT, "NOP_EVT"), StateModelError); + + // Verify that we can still find the event. + EXPECT_TRUE(checkEvent(NOP_EVT, "NOP_EVT")); +} + +/// @brief Tests event dictionary construction and verification. +TEST_F(StateModelTest, eventDictionary) { + // After construction, the event dictionary should be empty. + // Make sure that verifyEvents() throws. + EXPECT_THROW(verifyEvents(), StateModelError); + + // Construct the dictionary and verify it. + EXPECT_NO_THROW(defineEvents()); + EXPECT_NO_THROW(verifyEvents()); + + // Verify base class events are defined. + EXPECT_TRUE(checkEvent(NOP_EVT, "NOP_EVT")); + EXPECT_TRUE(checkEvent(START_EVT, "START_EVT")); + EXPECT_TRUE(checkEvent(END_EVT, "END_EVT")); + EXPECT_TRUE(checkEvent(FAIL_EVT, "FAIL_EVT")); + + // Verify stub class events are defined. + EXPECT_TRUE(checkEvent(WORK_START_EVT, "WORK_START_EVT")); + EXPECT_TRUE(checkEvent(WORK_DONE_EVT, "WORK_DONE_EVT")); + EXPECT_TRUE(checkEvent(ALL_DONE_EVT, "ALL_DONE_EVT")); + EXPECT_TRUE(checkEvent(FORCE_UNDEFINED_ST_EVT, "FORCE_UNDEFINED_ST_EVT")); + EXPECT_TRUE(checkEvent(SIMULATE_ERROR_EVT, "SIMULATE_ERROR_EVT")); + + // Verify that undefined events are handled correctly. + EXPECT_THROW(getEvent(9999), StateModelError); + EXPECT_EQ(LabeledValueSet::UNDEFINED_LABEL, getEventLabel(9999)); +} + +/// @brief General testing of event context accessors. +/// Most if not all of these are also tested as a byproduct off larger tests. +TEST_F(StateModelTest, eventContextAccessors) { + // Construct the event definitions, normally done by startModel. + ASSERT_NO_THROW(defineEvents()); + ASSERT_NO_THROW(verifyEvents()); + + // Verify the post-construction values. + EXPECT_EQ(NOP_EVT, getNextEvent()); + EXPECT_EQ(NOP_EVT, getLastEvent()); + + // Call setEvent which will update both next event and last event. + EXPECT_NO_THROW(postNextEvent(START_EVT)); + + // Verify the values are what we expect. + EXPECT_EQ(START_EVT, getNextEvent()); + EXPECT_EQ(NOP_EVT, getLastEvent()); + + // Call setEvent again. + EXPECT_NO_THROW(postNextEvent(WORK_START_EVT)); + + // Verify the values are what we expect. + EXPECT_EQ(WORK_START_EVT, getNextEvent()); + EXPECT_EQ(START_EVT, getLastEvent()); + + // Verify that posting an undefined event throws. + EXPECT_THROW(postNextEvent(9999), StateModelError); +} + +/// @brief Tests the fundamental methods used for state handler mapping. +/// Verifies the ability to search for and add entries in the state handler map. +TEST_F(StateModelTest, stateDefinition) { + // After construction, the state dictionary should be empty. Verify that + // getState will throw when, state is not defined. + EXPECT_THROW(getState(READY_ST), StateModelError); + + // Verify that we can add a state to the dictionary. + ASSERT_NO_THROW(defineState(READY_ST, "READY_ST", + std::bind(&StateModelTest::dummyHandler, + this))); + + // Verify that we can find the state by its value. + StatePtr state; + EXPECT_NO_THROW(state = getState(READY_ST)); + EXPECT_TRUE(state); + + // Verify the state's value and label. + EXPECT_EQ(READY_ST, state->getValue()); + EXPECT_EQ("READY_ST", state->getLabel()); + + // Now verify that retrieved state's handler executes the correct method. + // Make sure the dummy called flag is false prior to invocation. + EXPECT_FALSE(getDummyCalled()); + + // Invoke the state's handler. + EXPECT_NO_THROW(state->run()); + + // Verify the dummy called flag is now true. + EXPECT_TRUE(getDummyCalled()); + + // Verify that we cannot add a duplicate. + EXPECT_THROW(defineState(READY_ST, "READY_ST", + std::bind(&StateModelTest::readyHandler, this)), + StateModelError); + + // Verify that we can still find the state. + EXPECT_NO_THROW(getState(READY_ST)); +} + +/// @brief Tests state dictionary initialization and validation. +/// This tests the basic concept of state dictionary initialization and +/// verification by manually invoking the methods normally called by startModel. +TEST_F(StateModelTest, stateDictionary) { + // Verify that the map validation throws prior to the dictionary being + // initialized. + EXPECT_THROW(verifyStates(), StateModelError); + + // Construct the dictionary and verify it. + ASSERT_NO_THROW(defineStates()); + EXPECT_NO_THROW(verifyStates()); + + // Verify the base class states. + EXPECT_TRUE(checkState(NEW_ST, "NEW_ST")); + EXPECT_TRUE(checkState(END_ST, "END_ST")); + + // Verify stub class states. + EXPECT_TRUE(checkState(DUMMY_ST, "DUMMY_ST")); + EXPECT_TRUE(checkState(READY_ST, "READY_ST")); + EXPECT_TRUE(checkState(DO_WORK_ST, "DO_WORK_ST")); + EXPECT_TRUE(checkState(DONE_ST, "DONE_ST")); + + // Verify that undefined states are handled correctly. + EXPECT_THROW(getState(9999), StateModelError); + EXPECT_EQ(LabeledValueSet::UNDEFINED_LABEL, getStateLabel(9999)); +} + +/// @brief General testing of state context accessors. +/// Most if not all of these are also tested as a byproduct off larger tests. +TEST_F(StateModelTest, stateContextAccessors) { + // setState will throw unless we initialize the handler map. + ASSERT_NO_THROW(defineStates()); + ASSERT_NO_THROW(verifyStates()); + + // Verify post-construction state values. + EXPECT_EQ(NEW_ST, getCurrState()); + EXPECT_EQ(NEW_ST, getPrevState()); + + // Call setState which will update both state and previous state. + EXPECT_NO_THROW(setState(READY_ST)); + + // Verify the values are what we expect. + EXPECT_EQ(READY_ST, getCurrState()); + EXPECT_EQ(NEW_ST, getPrevState()); + + // Call setState again. + EXPECT_NO_THROW(setState(DO_WORK_ST)); + + // Verify the values are what we expect. + EXPECT_EQ(DO_WORK_ST, getCurrState()); + EXPECT_EQ(READY_ST, getPrevState()); + + // Verify that calling setState with an state that has no handler + // will throw. + EXPECT_THROW(setState(-1), StateModelError); + + // Verify that calling setState with NEW_ST is ok. + EXPECT_NO_THROW(setState(NEW_ST)); + + // Verify that calling setState with END_ST is ok. + EXPECT_NO_THROW(setState(END_ST)); + + // Verify that calling setState with an undefined state throws. + EXPECT_THROW(setState(9999), StateModelError); +} + +/// @brief Checks that invoking runModel prior to startModel is not allowed. +TEST_F(StateModelTest, runBeforeStart) { + // Verify that the failure explanation is empty and work is not done. + EXPECT_TRUE(getFailureExplanation().empty()); + + // Attempt to call runModel before startModel. This should result in an + // orderly model failure. + ASSERT_NO_THROW(runModel(START_EVT)); + + // Check that state and event are correct. + EXPECT_EQ(END_ST, getCurrState()); + EXPECT_EQ(FAIL_EVT, getNextEvent()); + + // Verify that failure explanation is not empty. + EXPECT_FALSE(getFailureExplanation().empty()); +} + +/// @brief Tests that the endModel may be used to transition the model to +/// a normal conclusion. +TEST_F(StateModelTest, transitionWithEnd) { + // Init dictionaries manually, normally done by startModel. + initDictionaries(); + + // call transition to move from NEW_ST to DUMMY_ST with START_EVT + EXPECT_NO_THROW(transition(DUMMY_ST, START_EVT)); + + // Verify that state and event members are as expected. + EXPECT_EQ(DUMMY_ST, getCurrState()); + EXPECT_EQ(NEW_ST, getPrevState()); + EXPECT_EQ(START_EVT, getNextEvent()); + EXPECT_EQ(NOP_EVT, getLastEvent()); + + // Call endModel to transition us to the end of the model. + EXPECT_NO_THROW(endModel()); + + // Verify state and event members are correctly set. + EXPECT_EQ(END_ST, getCurrState()); + EXPECT_EQ(DUMMY_ST, getPrevState()); + EXPECT_EQ(END_EVT, getNextEvent()); + EXPECT_EQ(START_EVT, getLastEvent()); +} + +/// @brief Tests that the abortModel may be used to transition the model to +/// failed conclusion. +TEST_F(StateModelTest, transitionWithAbort) { + // Init dictionaries manually, normally done by startModel. + initDictionaries(); + + // call transition to move from NEW_ST to DUMMY_ST with START_EVT + EXPECT_NO_THROW(transition(DUMMY_ST, START_EVT)); + + // Verify that state and event members are as expected. + EXPECT_EQ(DUMMY_ST, getCurrState()); + EXPECT_EQ(NEW_ST, getPrevState()); + EXPECT_EQ(START_EVT, getNextEvent()); + EXPECT_EQ(NOP_EVT, getLastEvent()); + + // Call endModel to transition us to the end of the model. + EXPECT_NO_THROW(abortModel("test invocation")); + + // Verify state and event members are correctly set. + EXPECT_EQ(END_ST, getCurrState()); + EXPECT_EQ(DUMMY_ST, getPrevState()); + EXPECT_EQ(FAIL_EVT, getNextEvent()); + EXPECT_EQ(START_EVT, getLastEvent()); +} + +/// @brief Tests that the boolean indicators for on state entry and exit +/// work properly. +TEST_F(StateModelTest, doFlags) { + // Init dictionaries manually, normally done by startModel. + initDictionaries(); + + // Verify that "do" flags are false. + EXPECT_FALSE(doOnEntry()); + EXPECT_FALSE(doOnExit()); + + // call transition to move from NEW_ST to DUMMY_ST with START_EVT + EXPECT_NO_THROW(transition(DUMMY_ST, START_EVT)); + + // We are transitioning states, so "do" flags should be true. + EXPECT_TRUE(doOnEntry()); + EXPECT_TRUE(doOnExit()); + + // "do" flags are one-shots, so they should now both be false. + EXPECT_FALSE(doOnEntry()); + EXPECT_FALSE(doOnExit()); + + // call transition to re-enter same state, "do" flags should be false. + EXPECT_NO_THROW(transition(DUMMY_ST, START_EVT)); + + // "do" flags should be false. + EXPECT_FALSE(doOnEntry()); + EXPECT_FALSE(doOnExit()); + +} + +/// @brief Verifies that the model status methods accurately reflect the model +/// status. It also verifies that the dictionaries can be modified before +/// the model is running but not after. +TEST_F(StateModelTest, statusMethods) { + // Init dictionaries manually, normally done by startModel. + initDictionaries(); + + // After construction, state model is "new", all others should be false. + EXPECT_TRUE(isModelNew()); + EXPECT_FALSE(isModelRunning()); + EXPECT_FALSE(isModelWaiting()); + EXPECT_FALSE(isModelDone()); + EXPECT_FALSE(didModelFail()); + + // Verify that events and states can be added before the model is started. + EXPECT_NO_THROW(defineEvent(9998, "9998")); + EXPECT_NO_THROW(defineState(9998, "9998", + std::bind(&StateModelTest::readyHandler, + this))); + + // "START" the model. + // Fake out starting the model by calling transition to move from NEW_ST + // to DUMMY_ST with START_EVT. If we used startModel this would blow by + // the status of "running" but not "waiting". + EXPECT_NO_THROW(transition(DUMMY_ST, START_EVT)); + + + // Verify that events and states cannot be added after the model is started. + EXPECT_THROW(defineEvent(9999, "9999"), StateModelError); + EXPECT_THROW(defineState(9999, "9999", + std::bind(&StateModelTest::readyHandler, this)), + StateModelError); + + // The state and event combos set above, should show the model as + // "running", all others should be false. + EXPECT_FALSE(isModelNew()); + EXPECT_TRUE(isModelRunning()); + EXPECT_FALSE(isModelWaiting()); + EXPECT_FALSE(isModelDone()); + EXPECT_FALSE(didModelFail()); + + // call transition to submit NOP_EVT to current state, DUMMY_ST. + EXPECT_NO_THROW(transition(DUMMY_ST, NOP_EVT)); + + // Verify the status methods are correct: with next event set to NOP_EVT, + // model should be "running" and "waiting". + EXPECT_FALSE(isModelNew()); + EXPECT_TRUE(isModelRunning()); + EXPECT_TRUE(isModelWaiting()); + EXPECT_FALSE(isModelDone()); + EXPECT_FALSE(didModelFail()); + + // Call endModel to transition us to the end of the model. + EXPECT_NO_THROW(endModel()); + + // With state set to END_ST, model should be done. + EXPECT_FALSE(isModelNew()); + EXPECT_FALSE(isModelRunning()); + EXPECT_FALSE(isModelWaiting()); + EXPECT_TRUE(isModelDone()); + EXPECT_FALSE(didModelFail()); +} + +/// @brief Tests that the model status methods are correct after a model +/// failure. +TEST_F(StateModelTest, statusMethodsOnFailure) { + // Construct the event and state definitions, normally done by startModel. + ASSERT_NO_THROW(defineEvents()); + ASSERT_NO_THROW(verifyEvents()); + ASSERT_NO_THROW(defineStates()); + ASSERT_NO_THROW(verifyStates()); + + // call transition to move from NEW_ST to DUMMY_ST with START_EVT + EXPECT_NO_THROW(transition(DUMMY_ST, START_EVT)); + + // Call endModel to transition us to the end of the model. + EXPECT_NO_THROW(abortModel("test invocation")); + + // With state set to END_ST, model should be done. + EXPECT_FALSE(isModelNew()); + EXPECT_FALSE(isModelRunning()); + EXPECT_FALSE(isModelWaiting()); + EXPECT_TRUE(isModelDone()); + EXPECT_TRUE(didModelFail()); +} + +/// @brief Checks that the context strings accurately reflect context and +/// are safe to invoke. +TEST_F(StateModelTest, contextStrs) { + // Verify context methods do not throw prior to dictionary init. + ASSERT_NO_THROW(getContextStr()); + ASSERT_NO_THROW(getPrevContextStr()); + + // Construct the event and state definitions, normally done by startModel. + ASSERT_NO_THROW(defineEvents()); + ASSERT_NO_THROW(verifyEvents()); + ASSERT_NO_THROW(defineStates()); + ASSERT_NO_THROW(verifyStates()); + + // transition uses setState and setEvent, testing it tests all three. + EXPECT_NO_THROW(transition(READY_ST, START_EVT)); + + // Verify the current context string depicts correct state and event. + std::string ctx_str; + ASSERT_NO_THROW(ctx_str = getContextStr()); + EXPECT_NE(std::string::npos, ctx_str.find(getStateLabel(READY_ST))); + EXPECT_NE(std::string::npos, ctx_str.find(getEventLabel(START_EVT))); + + // Verify the previous context string depicts correct state and event. + ASSERT_NO_THROW(ctx_str = getPrevContextStr()); + EXPECT_NE(std::string::npos, ctx_str.find(getStateLabel(NEW_ST))); + EXPECT_NE(std::string::npos, ctx_str.find(getEventLabel(NOP_EVT))); +} + +/// @brief Tests that undefined states are handled gracefully. +/// This test verifies that attempting to transition to an undefined state, +/// which constitutes a model violation, results in an orderly model failure. +TEST_F(StateModelTest, undefinedState) { + // Verify that the failure explanation is empty and work is not done. + EXPECT_TRUE(getFailureExplanation().empty()); + EXPECT_FALSE(getWorkCompleted()); + + // First, lets execute the state model to a known valid point, by + // calling startModel. This should run the model through to DO_WORK_ST. + ASSERT_NO_THROW(startModel(READY_ST)); + + // Verify we are in the state of DO_WORK_ST with event of NOP_EVT. + EXPECT_EQ(DO_WORK_ST, getCurrState()); + EXPECT_EQ(NOP_EVT, getNextEvent()); + + // Resume the model with next event set to cause the DO_WORK_ST handler + // to transition to an undefined state. This should cause it to return + // without throwing and yield a failed model. + EXPECT_NO_THROW(runModel(FORCE_UNDEFINED_ST_EVT)); + + // Verify that status methods are correct: model is done but failed. + EXPECT_FALSE(isModelNew()); + EXPECT_FALSE(isModelRunning()); + EXPECT_FALSE(isModelWaiting()); + EXPECT_TRUE(isModelDone()); + EXPECT_TRUE(didModelFail()); + + // Verify that failure explanation is not empty. + EXPECT_FALSE(getFailureExplanation().empty()); + + // Verify that work completed flag is still false. + EXPECT_FALSE(getWorkCompleted()); +} + +/// @brief Tests that an unexpected exception thrown by a state handler is +/// handled gracefully. State models are supposed to account for and handle +/// all errors that they actions (i.e. handlers) may cause. In the event they +/// do not, this constitutes a model violation. This test verifies such +/// violations are handled correctly and result in an orderly model failure. +TEST_F(StateModelTest, unexpectedError) { + // Verify that the failure explanation is empty and work is not done. + EXPECT_TRUE(getFailureExplanation().empty()); + EXPECT_FALSE(getWorkCompleted()); + + // First, lets execute the state model to a known valid point, by + // calling startModel with a start state of READY_ST. + // This should run the model through to DO_WORK_ST. + ASSERT_NO_THROW(startModel(READY_ST)); + + // Verify we are in the state of DO_WORK_ST with event of NOP_EVT. + EXPECT_EQ(DO_WORK_ST, getCurrState()); + EXPECT_EQ(NOP_EVT, getNextEvent()); + + // Resume the model with next event set to cause the DO_WORK_ST handler + // to transition to an undefined state. This should cause it to return + // without throwing and yield a failed model. + EXPECT_NO_THROW(runModel(SIMULATE_ERROR_EVT)); + + // Verify that status methods are correct: model is done but failed. + EXPECT_FALSE(isModelNew()); + EXPECT_FALSE(isModelRunning()); + EXPECT_FALSE(isModelWaiting()); + EXPECT_TRUE(isModelDone()); + EXPECT_TRUE(didModelFail()); + + // Verify that failure explanation is not empty. + EXPECT_FALSE(getFailureExplanation().empty()); + + // Verify that work completed flag is still false. + EXPECT_FALSE(getWorkCompleted()); +} + +/// @brief Tests that undefined events are handled gracefully. +/// This test verifies that submitting an undefined event to the state machine +/// results, which constitutes a model violation, results in an orderly model +/// failure. +TEST_F(StateModelTest, undefinedEvent) { + // Verify that the failure explanation is empty and work is not done. + EXPECT_TRUE(getFailureExplanation().empty()); + EXPECT_FALSE(getWorkCompleted()); + + // First, lets execute the state model to a known valid point, by + // calling startModel with a start state of READY_ST. + // This should run the model through to DO_WORK_ST. + ASSERT_NO_THROW(startModel(READY_ST)); + + // Verify we are in the state of DO_WORK_ST with event of NOP_EVT. + EXPECT_EQ(DO_WORK_ST, getCurrState()); + EXPECT_EQ(NOP_EVT, getNextEvent()); + + // Attempting to post an undefined event within runModel should cause it + // to return without throwing and yield a failed model. + EXPECT_NO_THROW(runModel(9999)); + + // Verify that status methods are correct: model is done but failed. + EXPECT_FALSE(isModelNew()); + EXPECT_FALSE(isModelRunning()); + EXPECT_FALSE(isModelWaiting()); + EXPECT_TRUE(isModelDone()); + EXPECT_TRUE(didModelFail()); + + // Verify that failure explanation is not empty. + EXPECT_FALSE(getFailureExplanation().empty()); + + // Verify that work completed flag is still false. + EXPECT_FALSE(getWorkCompleted()); +} + +/// @brief Test the basic mechanics of state model execution. +/// This test exercises the ability to execute state model from start to +/// finish, including the handling of a asynchronous IO operation. +TEST_F(StateModelTest, stateModelTest) { + // Verify that status methods are correct: model is new. + EXPECT_TRUE(isModelNew()); + EXPECT_FALSE(isModelRunning()); + EXPECT_FALSE(isModelWaiting()); + EXPECT_FALSE(isModelDone()); + + // Verify that the failure explanation is empty and work is not done. + EXPECT_TRUE(getFailureExplanation().empty()); + EXPECT_FALSE(getWorkCompleted()); + + // Launch the transaction by calling startModel. The state model + // should run up until the simulated async work operation is initiated + // in DO_WORK_ST. + ASSERT_NO_THROW(startModel(READY_ST)); + + // Verify that we are now in state of DO_WORK_ST, the last event was + // WORK_START_EVT, the next event is NOP_EVT. + EXPECT_EQ(DO_WORK_ST, getCurrState()); + EXPECT_EQ(WORK_START_EVT, getLastEvent()); + EXPECT_EQ(NOP_EVT, getNextEvent()); + + // Simulate completion of async work completion by resuming runModel with + // an event of WORK_DONE_EVT. + ASSERT_NO_THROW(runModel(WORK_DONE_EVT)); + + // Verify that the state model has progressed through to completion: + // it is in the DONE_ST, the status is ST_COMPLETED, and the next event + // is NOP_EVT. + EXPECT_EQ(END_ST, getCurrState()); + EXPECT_EQ(END_EVT, getNextEvent()); + + // Verify that status methods are correct: model done. + EXPECT_FALSE(isModelNew()); + EXPECT_FALSE(isModelRunning()); + EXPECT_FALSE(isModelWaiting()); + EXPECT_TRUE(isModelDone()); + + // Verify that failure explanation is empty. + EXPECT_TRUE(getFailureExplanation().empty()); + + // Verify that work completed flag is true. + EXPECT_TRUE(getWorkCompleted()); +} + +// This test verifies the pausing and un-pausing capabilities of the state +// model. +TEST_F(StateModelTest, stateModelPause) { + // Verify that status methods are correct: model is new. + EXPECT_TRUE(isModelNew()); + EXPECT_FALSE(isModelRunning()); + EXPECT_FALSE(isModelWaiting()); + EXPECT_FALSE(isModelDone()); + EXPECT_FALSE(isModelPaused()); + + // Verify that the failure explanation is empty and work is not done. + EXPECT_TRUE(getFailureExplanation().empty()); + EXPECT_FALSE(getWorkCompleted()); + + // Transition straight to the state in which the model should always + // pause. + ASSERT_NO_THROW(startModel(PAUSE_ALWAYS_ST)); + + // Verify it was successful and that the model is paused. + EXPECT_EQ(PAUSE_ALWAYS_ST, getCurrState()); + EXPECT_TRUE(isModelPaused()); + + // Run the model again. It should still be paused. + ASSERT_NO_THROW(runModel(NOP_EVT)); + EXPECT_TRUE(isModelPaused()); + + // Unpause the model and transition to the state in which the model + // should be paused at most once. + unpauseModel(); + transition(PAUSE_ONCE_ST, NOP_EVT); + EXPECT_EQ(PAUSE_ONCE_ST, getCurrState()); + EXPECT_TRUE(isModelPaused()); + + // The model should still be paused until explicitly unpaused. + ASSERT_NO_THROW(runModel(NOP_EVT)); + EXPECT_EQ(PAUSE_ONCE_ST, getCurrState()); + EXPECT_TRUE(isModelPaused()); + + unpauseModel(); + + // Transition back to the first state. The model should pause again. + transition(PAUSE_ALWAYS_ST, NOP_EVT); + EXPECT_EQ(PAUSE_ALWAYS_ST, getCurrState()); + EXPECT_TRUE(isModelPaused()); + + ASSERT_NO_THROW(runModel(NOP_EVT)); + EXPECT_EQ(PAUSE_ALWAYS_ST, getCurrState()); + EXPECT_TRUE(isModelPaused()); + + // Unpause the model and transition to the state in which the model + // should pause only once. This time it should not pause. + unpauseModel(); + transition(PAUSE_ONCE_ST, NOP_EVT); + EXPECT_EQ(PAUSE_ONCE_ST, getCurrState()); + EXPECT_FALSE(isModelPaused()); +} + +} |