From: James Turner Date: Sun, 20 Jan 2013 13:57:20 +0000 (+0100) Subject: State-machine structure, initial work. X-Git-Url: https://git.mxchange.org/?a=commitdiff_plain;h=22ea8ebe258da45df9fc2b77904c2dcf79cd0f57;p=simgear.git State-machine structure, initial work. --- diff --git a/simgear/structure/CMakeLists.txt b/simgear/structure/CMakeLists.txt index ebe6be5d..0c5dc4f6 100644 --- a/simgear/structure/CMakeLists.txt +++ b/simgear/structure/CMakeLists.txt @@ -23,6 +23,7 @@ set(HEADERS exception.hxx intern.hxx subsystem_mgr.hxx + StateMachine.hxx ) set(SOURCES @@ -36,7 +37,15 @@ set(SOURCES commands.cxx event_mgr.cxx exception.cxx - subsystem_mgr.cxx + subsystem_mgr.cxx + StateMachine.cxx ) simgear_component(structure structure "${SOURCES}" "${HEADERS}") + +if(ENABLE_TESTS) +add_executable(test_state_machine state_machine_test.cxx) +target_link_libraries(test_state_machine SimGearCore) +add_test(test_state_machine ${EXECUTABLE_OUTPUT_PATH}/test_state_machine) + +endif(ENABLE_TESTS) diff --git a/simgear/structure/SGBinding.cxx b/simgear/structure/SGBinding.cxx index 8ca19034..db501d40 100644 --- a/simgear/structure/SGBinding.cxx +++ b/simgear/structure/SGBinding.cxx @@ -11,6 +11,7 @@ # include #endif +#include #include #include "SGBinding.hxx" @@ -23,6 +24,14 @@ SGBinding::SGBinding() { } +SGBinding::SGBinding(const std::string& commandName) + : _command(0), + _arg(0), + _setting(0) +{ + _command_name = commandName; +} + SGBinding::SGBinding(const SGPropertyNode* node, SGPropertyNode* root) : _command(0), _arg(0), @@ -98,3 +107,10 @@ SGBinding::fire (double setting) const fire(); } } + +void fireBindingList(const SGBindingList& aBindings) +{ + BOOST_FOREACH(SGBinding_ptr b, aBindings) { + b->fire(); + } +} diff --git a/simgear/structure/SGBinding.hxx b/simgear/structure/SGBinding.hxx index 25e1601e..23f38820 100644 --- a/simgear/structure/SGBinding.hxx +++ b/simgear/structure/SGBinding.hxx @@ -38,6 +38,12 @@ public: */ SGBinding (); + /** + * Convenience constructor. + * + * @param node The binding will be built from this node. + */ + SGBinding(const std::string& commandName); /** * Convenience constructor. @@ -119,7 +125,15 @@ private: mutable SGPropertyNode_ptr _setting; }; -typedef std::vector > SGBindingList; +typedef SGSharedPtr SGBinding_ptr; + +typedef std::vector SGBindingList; typedef std::map SGBindingMap; +/** + * fire every binding in a list, in sequence + + */ +void fireBindingList(const SGBindingList& aBindings); + #endif diff --git a/simgear/structure/StateMachine.cxx b/simgear/structure/StateMachine.cxx new file mode 100644 index 00000000..76a00b29 --- /dev/null +++ b/simgear/structure/StateMachine.cxx @@ -0,0 +1,456 @@ +/* -*-c++-*- + * + * Copyright (C) 2013 James Turner + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include "StateMachine.hxx" + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace simgear +{ + +typedef std::vector StatePtrVec; + +static void readBindingList(SGPropertyNode* desc, const std::string& name, + SGPropertyNode* root, SGBindingList& result) +{ + BOOST_FOREACH(SGPropertyNode* b, desc->getChildren(name)) { + SGBinding* bind = new SGBinding; + bind->read(b, root); + result.push_back(bind); + } +} + +/////////////////////////////////////////////////////////////////////////// + +class StateMachine::State::StatePrivate +{ +public: + std::string _name; + SGBindingList _updateBindings, + _entryBindings, + _exitBindings; +}; + +/////////////////////////////////////////////////////////////////////////// + +class StateMachine::Transition::TransitionPrivate +{ +public: + std::string _name; + SGBindingList _bindings; + std::set _sourceStates; ///< weak refs to source states + State* _target; + SGSharedPtr _condition; +}; + +/////////////////////////////////////////////////////////////////////////// + +class StateMachine::StateMachinePrivate : public SGPropertyChangeListener +{ +public: + StateMachinePrivate(StateMachine* p) : _p(p) { } + + void computeEligibleTransitions() + { + _eligible.clear(); + BOOST_FOREACH(Transition_ptr t, _transitions) { + if (t->applicableForState(_currentState)) { + _eligible.push_back(t.ptr()); + } + } + } + + StateMachine* _p; + State_ptr _currentState; + StatePtrVec _states; + std::vector _transitions; + std::vector _eligible; + SGTimeStamp _timeInState; + + bool _listenerLockout; ///< block our listener when self-updating props + virtual void valueChanged(SGPropertyNode* changed) + { + if (_listenerLockout) { + return; + } + + if (changed == _currentStateIndex) { + State_ptr s = _p->stateByIndex(changed->getIntValue()); + _p->changeToState(s); + } else if (changed == _currentStateName) { + _p->changeToStateName(changed->getStringValue()); + } + } + +// exposed properties + SGPropertyNode_ptr _root; + SGPropertyNode_ptr _currentStateIndex; + SGPropertyNode_ptr _currentStateName; + SGPropertyNode_ptr _timeInStateProp; +}; + +/////////////////////////////////////////////////////////////////////////// + +StateMachine::State::State(const std::string& aName) : + d(new StatePrivate) +{ + d->_name = aName; +} + +StateMachine::State::~State() +{ +} + +std::string StateMachine::State::name() const +{ + return d->_name; +} + +void StateMachine::State::update() +{ + fireBindingList(d->_updateBindings); +} + +void StateMachine::State::fireEntryBindings() +{ + fireBindingList(d->_entryBindings); +} + +void StateMachine::State::fireExitBindings() +{ + fireBindingList(d->_exitBindings); +} + +void StateMachine::State::addUpdateBinding(SGBinding* aBinding) +{ + d->_updateBindings.push_back(aBinding); +} + +void StateMachine::State::addEntryBinding(SGBinding* aBinding) +{ + d->_entryBindings.push_back(aBinding); +} + +void StateMachine::State::addExitBinding(SGBinding* aBinding) +{ + d->_exitBindings.push_back(aBinding); +} + +/////////////////////////////////////////////////////////////////////////// + +StateMachine::Transition::Transition(const std::string& aName, State* aTarget) : + d(new TransitionPrivate) +{ + assert(aTarget); + d->_name = aName; + d->_target = aTarget; +} + +StateMachine::Transition::~Transition() +{ +} + +StateMachine::State* StateMachine::Transition::target() const +{ + return d->_target; +} + +void StateMachine::Transition::addSourceState(State* aSource) +{ + if (aSource == d->_target) { // should this be disallowed outright? + SG_LOG(SG_GENERAL, SG_WARN, d->_name << ": adding target state as source"); + } + + d->_sourceStates.insert(aSource); +} + +bool StateMachine::Transition::applicableForState(State* aCurrent) const +{ + return d->_sourceStates.count(aCurrent); +} + +bool StateMachine::Transition::evaluate() const +{ + return d->_condition->test(); +} + +void StateMachine::Transition::fireBindings() +{ + fireBindingList(d->_bindings); +} + +std::string StateMachine::Transition::name() const +{ + return d->_name; +} + +void StateMachine::Transition::setTriggerCondition(SGCondition* aCondition) +{ + d->_condition = aCondition; +} + +void StateMachine::Transition::addBinding(SGBinding* aBinding) +{ + d->_bindings.push_back(aBinding); +} + +/////////////////////////////////////////////////////////////////////////// + +StateMachine::StateMachine() : + d(new StateMachinePrivate(this)) +{ + d->_root = new SGPropertyNode(); +} + +StateMachine::~StateMachine() +{ + +} + +void StateMachine::init() +{ + + + d->_currentStateIndex = d->_root->getChild("current-index", 0, true); + d->_currentStateIndex->setIntValue(0); + + d->_currentStateName = d->_root->getChild("current-name", 0, true); + d->_currentStateName->setStringValue(""); + + d->_currentStateIndex->addChangeListener(d.get()); + d->_currentStateName->addChangeListener(d.get()); + + d->_timeInStateProp = d->_root->getChild("elapsed-time-msec", 0, true); + d->_timeInStateProp->setIntValue(0); + + // TODO go to default state if found + d->computeEligibleTransitions(); + +} + +void StateMachine::shutdown() +{ + d->_currentStateIndex->removeChangeListener(d.get()); + d->_currentStateName->removeChangeListener(d.get()); + +} + +void StateMachine::innerChangeState(State_ptr aState, Transition_ptr aTrans) +{ + d->_currentState->fireExitBindings(); + +// fire bindings before we change the state, hmmmm + if (aTrans) { + aTrans->fireBindings(); + } + + // update our private state and properties + d->_listenerLockout = true; + d->_currentState = aState; + d->_timeInState.stamp(); + d->_currentStateName->setStringValue(d->_currentState->name()); + d->_currentStateIndex->setIntValue(indexOfState(aState)); + d->_timeInStateProp->setIntValue(0); + d->_listenerLockout = false; + + // fire bindings + d->_currentState->fireEntryBindings(); + d->_currentState->update(); + + d->computeEligibleTransitions(); +} + +void StateMachine::changeToState(State_ptr aState, bool aOnlyIfDifferent) +{ + assert(aState != NULL); + if (std::find(d->_states.begin(), d->_states.end(), aState) == d->_states.end()) { + throw sg_exception("Requested change to state not in machine"); + } + + if (aOnlyIfDifferent && (aState == d->_currentState)) { + return; + } + + innerChangeState(aState, NULL); +} + +void StateMachine::changeToStateName(const std::string& aName, bool aOnlyIfDifferent) +{ + State_ptr st = findStateByName(aName); + if (!st) { + throw sg_range_exception("unknown state:" + aName); + } + + changeToState(st, aOnlyIfDifferent); +} + +StateMachine::State_ptr StateMachine::state() const +{ + return d->_currentState; +} + +SGPropertyNode* StateMachine::root() +{ + return d->_root; +} + +void StateMachine::update(double aDt) +{ + // do this first, for triggers which depend on time in current state + // (spring-loaded transitions) + d->_timeInStateProp->setIntValue(d->_timeInState.elapsedMSec()); + + Transition_ptr trigger; + + BOOST_FOREACH(Transition* trans, d->_eligible) { + if (trans->evaluate()) { + if (trigger != Transition_ptr()) { + SG_LOG(SG_GENERAL, SG_WARN, "ambiguous transitions! " + << trans->name() << " or " << trigger->name()); + } + + trigger = trans; + } + } + + if (trigger != Transition_ptr()) { + SG_LOG(SG_GENERAL, SG_DEBUG, "firing transition:" << trigger->name()); + innerChangeState(trigger->target(), trigger); + } + + d->_currentState->update(); +} + +StateMachine::State_ptr StateMachine::findStateByName(const std::string& aName) const +{ + BOOST_FOREACH(State_ptr sp, d->_states) { + if (sp->name() == aName) { + return sp; + } + } + + SG_LOG(SG_GENERAL, SG_WARN, "unknown state:" << aName); + return State_ptr(); +} + +StateMachine::State_ptr StateMachine::stateByIndex(unsigned int aIndex) const +{ + if (aIndex >= d->_states.size()) { + throw sg_range_exception("invalid state index, out of bounds"); + } + + return d->_states[aIndex]; +} + +int StateMachine::indexOfState(State_ptr aState) const +{ + StatePtrVec::const_iterator it = std::find(d->_states.begin(), d->_states.end(), aState); + if (it == d->_states.end()) { + return -1; + } + + return it - d->_states.begin(); +} + +StateMachine::State_ptr StateMachine::createState(const std::string& aName) +{ + if (findStateByName(aName) != NULL) { + throw sg_range_exception("duplicate state name"); + } + + State_ptr st = new State(aName); + addState(st); + return st; +} + +StateMachine::Transition_ptr +StateMachine::createTransition(const std::string& aName, State_ptr aTarget) +{ + Transition_ptr t = new Transition(aName, aTarget); + addTransition(t); + return t; +} + +StateMachine* StateMachine::createFromPlist(SGPropertyNode* desc, SGPropertyNode* root) +{ + StateMachine* sm = new StateMachine; + + + BOOST_FOREACH(SGPropertyNode* stateDesc, desc->getChildren("state")) { + std::string nm = stateDesc->getStringValue("name"); + State_ptr st(new State(nm)); + + readBindingList(stateDesc, "enter", root, st->d->_updateBindings); + readBindingList(stateDesc, "exit", root, st->d->_entryBindings); + readBindingList(stateDesc, "update", root, st->d->_exitBindings); + + sm->addState(st); + } // of states iteration + + BOOST_FOREACH(SGPropertyNode* tDesc, desc->getChildren("transition")) { + std::string nm = tDesc->getStringValue("name"); + State_ptr target = sm->findStateByName(tDesc->getStringValue("target")); + + SGCondition* cond = sgReadCondition(root, tDesc->getChild("condition")); + + Transition_ptr t(new Transition(nm, target)); + t->setTriggerCondition(cond); + + BOOST_FOREACH(SGPropertyNode* src, desc->getChildren("source")) { + State_ptr srcState = sm->findStateByName(src->getStringValue()); + t->addSourceState(srcState); + } + + readBindingList(tDesc, "binding", root, t->d->_bindings); + + sm->addTransition(t); + } // of states iteration + + return sm; +} + +void StateMachine::addState(State_ptr aState) +{ + bool wasEmpty = d->_states.empty(); + d->_states.push_back(aState); + if (wasEmpty) { + d->_currentState = aState; + } +} + +void StateMachine::addTransition(Transition_ptr aTrans) +{ + d->_transitions.push_back(aTrans); +} + +} // of namespace simgear diff --git a/simgear/structure/StateMachine.hxx b/simgear/structure/StateMachine.hxx new file mode 100644 index 00000000..34fb84e1 --- /dev/null +++ b/simgear/structure/StateMachine.hxx @@ -0,0 +1,166 @@ +/* -*-c++-*- + * + * Copyright (C) 2013 James Turner + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + */ + +#ifndef SIMGEAR_STATE_MACHINE_H +#define SIMGEAR_STATE_MACHINE_H + +#include + +#include +#include + +// forward decls +class SGPropertyNode; +class SGBinding; +class SGCondition; + +namespace simgear +{ + +class StateMachine : public SGReferenced +{ +public: + StateMachine(); + virtual ~StateMachine(); + + class State : public SGReferenced + { + public: + virtual ~State(); + + std::string name() const; + + void addUpdateBinding(SGBinding* aBinding); + void addEntryBinding(SGBinding* aBinding); + void addExitBinding(SGBinding* aBinding); + + private: + friend class StateMachine; + + State(const std::string& name); + + void fireExitBindings(); + void fireEntryBindings(); + + void update(); + + class StatePrivate; + std::auto_ptr d; + }; + + class Transition : public SGReferenced + { + public: + virtual ~Transition(); + + std::string name() const; + + /** + * The state we end in, after this transition fires + */ + State* target() const; + + /** + * Add a state in which this transition is eligible to fire + */ + void addSourceState(State* aSource); + + /** + * Specify the transition trigger condition. Takes ownership + */ + void setTriggerCondition(SGCondition* aCondition); + + + void addBinding(SGBinding* aBinding); + private: + friend class StateMachine; + + Transition(const std::string& aName, State* aTarget); + + /** + * predicate to determine if this transition can fire given a + * current state. + */ + bool applicableForState(State* aCurrent) const; + + /** + * test if the transition should fire, based on current state + */ + bool evaluate() const; + + void fireBindings(); + + class TransitionPrivate; + std::auto_ptr d; + }; + + typedef SGSharedPtr State_ptr; + typedef SGSharedPtr Transition_ptr; + + /** + * create a state machine from a property list description + */ + static StateMachine* createFromPlist(SGPropertyNode* desc, SGPropertyNode* root); + + SGPropertyNode* root(); + + void init(); + void shutdown(); + + void update(double dt); + + State_ptr state() const; + + /** + * public API to force a change to a particular state. + * @param aOnlyIfDifferent - only make a transition if the new state is + * different from the current state. Otherwise, the existing state will + * be exited and re-entered. + */ + void changeToState(State_ptr aState, bool aOnlyIfDifferent=true); + + /// wrapper to change state by looking up a name + void changeToStateName(const std::string& aName, bool aOnlyIfDifferent=true); + + State_ptr findStateByName(const std::string& aName) const; + + State_ptr stateByIndex(unsigned int aIndex) const; + + int indexOfState(State_ptr aState) const; + + // programatic creation + State_ptr createState(const std::string& aName); + Transition_ptr createTransition(const std::string& aName, State_ptr aTarget); +private: + void addState(State_ptr aState); + void addTransition(Transition_ptr aTrans); + + void innerChangeState(State_ptr aState, Transition_ptr aTrans); + + class StateMachinePrivate; + std::auto_ptr d; +}; + +typedef SGSharedPtr StateMachine_ptr; + +} // of simgear namespace + +#endif // of SIMGEAR_STATE_MACHINE_H diff --git a/simgear/structure/state_machine_test.cxx b/simgear/structure/state_machine_test.cxx new file mode 100644 index 00000000..a22ae639 --- /dev/null +++ b/simgear/structure/state_machine_test.cxx @@ -0,0 +1,226 @@ +#ifdef HAVE_CONFIG_H +# include +#endif + +#ifdef NDEBUG +// Always enable DEBUG mode in test application, otherwise "assert" test +// statements have no effect and don't actually test anything (catch 17 ;-) ). +#undef NDEBUG +#endif + +#include + +#include +#include +#include +#include + +#include "StateMachine.hxx" + +#include +#include +#include +#include +#include +#include + +using std::string; +using std::cout; +using std::cerr; +using std::endl; + +// SGCondition subclass we can trivially manipulate from test code. +class DummyCondition : public SGCondition +{ +public: + DummyCondition(): _state(false) { } + + virtual bool test() const + { + return _state; + } + + bool _state; +}; + +static int dummy_cmd_state = 0; + +bool dummyCommand(const SGPropertyNode* arg) +{ + ++dummy_cmd_state; + return true; +} + +#define COMPARE(a, b) \ + if ((a) != (b)) { \ + cerr << "failed:" << #a << " != " << #b << endl; \ + cerr << "\tgot:'" << a << "'" << endl; \ + exit(1); \ + } + +#define VERIFY(a) \ + if (!(a)) { \ + cerr << "failed:" << #a << endl; \ + exit(1); \ + } + +using namespace simgear; + +#define BUILD_MACHINE_1() \ + StateMachine_ptr sm(new StateMachine); \ + StateMachine::State_ptr stateA = sm->createState("a"); \ + StateMachine::State_ptr stateB = sm->createState("b"); \ + StateMachine::State_ptr stateC = sm->createState("c"); \ + \ + DummyCondition* trigger1 = new DummyCondition; \ + StateMachine::Transition_ptr t1 = sm->createTransition(">b", stateB); \ + t1->addSourceState(stateA); \ + t1->setTriggerCondition(trigger1); \ + \ + DummyCondition* trigger2 = new DummyCondition; \ + StateMachine::Transition_ptr t2 = sm->createTransition(">c", stateC); \ + t2->addSourceState(stateB); \ + t2->setTriggerCondition(trigger2); \ + \ + DummyCondition* trigger3 = new DummyCondition; \ + StateMachine::Transition_ptr t3 = sm->createTransition(">a", stateA); \ + t3->addSourceState(stateC); \ + t3->addSourceState(stateB); \ + t3->setTriggerCondition(trigger3); \ + sm->init(); + +void testBasic() +{ + BUILD_MACHINE_1(); +//////////////////////////////////////////// + COMPARE(sm->state()->name(), "a"); + + COMPARE(sm->indexOfState(stateA), 0); + COMPARE(sm->findStateByName("c"), stateC); + + sm->changeToState(stateC); + COMPARE(sm->state(), stateC); + + trigger3->_state = true; + sm->update(1.0); + COMPARE(sm->state()->name(), "a"); + trigger3->_state = false; + + trigger1->_state = true; + sm->update(1.0); + trigger1->_state = false; + COMPARE(sm->state()->name(), "b"); + + trigger3->_state = true; + sm->update(1.0); + COMPARE(sm->state()->name(), "a"); + trigger3->_state = false; + + trigger1->_state = true; + sm->update(1.0); + trigger1->_state = false; + COMPARE(sm->state()->name(), "b"); + + trigger2->_state = true; + sm->update(1.0); + trigger2->_state = false; + COMPARE(sm->state()->name(), "c"); + +////////////////////////////////////////// + COMPARE(sm->root()->getIntValue("current-index"), 2); + COMPARE(sm->root()->getStringValue("current-name"), string("c")); + + sm->root()->setStringValue("current-name", "b"); + COMPARE(sm->state()->name(), "b"); + +//////////////////////////////////////// + COMPARE(sm->findStateByName("foo"), NULL); + COMPARE(sm->indexOfState(StateMachine::State_ptr()), -1); + + COMPARE(sm->stateByIndex(1), stateB); + + try { + sm->stateByIndex(44); + VERIFY(false && "should have raised an exception"); + } catch (sg_exception& e){ + // expected! + } +} + +void testBindings() +{ + SGCommandMgr* cmdMgr = SGCommandMgr::instance(); + cmdMgr->addCommand("dummy", dummyCommand); + BUILD_MACHINE_1(); + + t2->addBinding(new SGBinding("dummy")); + + stateA->addEntryBinding(new SGBinding("dummy")); + stateA->addExitBinding(new SGBinding("dummy")); + stateC->addEntryBinding(new SGBinding("dummy")); + +//////////////////////// + COMPARE(sm->state()->name(), "a"); + trigger1->_state = true; + sm->update(1.0); + trigger1->_state = false; + COMPARE(sm->state()->name(), "b"); + COMPARE(dummy_cmd_state, 1); // exit state A + + trigger2->_state = true; + sm->update(1.0); + trigger2->_state = false; + COMPARE(dummy_cmd_state, 3); // fire transition 2, enter state C + + dummy_cmd_state = 0; + sm->changeToState(stateA); + COMPARE(dummy_cmd_state, 1); // enter state A + trigger1->_state = true; + sm->update(1.0); + trigger1->_state = false; + COMPARE(dummy_cmd_state, 2); // exit state A + +//////////////////////// + t3->addBinding(new SGBinding("dummy")); + t3->addBinding(new SGBinding("dummy")); + t3->addBinding(new SGBinding("dummy")); + + sm->changeToStateName("b"); + dummy_cmd_state = 0; + trigger3->_state = true; + sm->update(1.0); + trigger3->_state = false; + COMPARE(dummy_cmd_state, 4); // three transition bindings, enter A +} + +void testParse() +{ + const char* xml = "" + "" + "" + "one" + "" + "" + "two" + "" + ""; + + SGPropertyNode* desc = new SGPropertyNode; + readProperties(xml, strlen(xml), desc); + + SGPropertyNode_ptr root(new SGPropertyNode); + StateMachine_ptr sm = StateMachine::createFromPlist(desc, root); + + VERIFY(sm->findStateByName("one") != NULL); + VERIFY(sm->findStateByName("two") != NULL); +} + +int main(int argc, char* argv[]) +{ + testBasic(); + testBindings(); + testParse(); + cout << __FILE__ << ": All tests passed" << endl; + return EXIT_SUCCESS; +} +