From f40282b2ff1fac69880f2910e996a25b9a231dd1 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Fri, 24 Jul 2009 05:24:39 +0200 Subject: [PATCH] WIP devised various aspects of command execution, drafted as unit test --- tests/45controller.tests | 15 ++ tests/components/Makefile.am | 3 + .../proc/control/command-use1-test.cpp | 167 ++++++++++++-- .../proc/control/command-use2-test.cpp | 208 ++++++++++++++++++ .../proc/control/command-use3-test.cpp | 121 ++++++++++ .../proc/control/test-dummy-commands.hpp | 54 ++++- wiki/renderengine.html | 12 +- 7 files changed, 546 insertions(+), 34 deletions(-) create mode 100644 tests/components/proc/control/command-use2-test.cpp create mode 100644 tests/components/proc/control/command-use3-test.cpp diff --git a/tests/45controller.tests b/tests/45controller.tests index d93c7f5a0..a9f87cbf3 100644 --- a/tests/45controller.tests +++ b/tests/45controller.tests @@ -54,3 +54,18 @@ END TEST "Memento wiring and storage" MementoTie_test < @@ -22,7 +22,7 @@ #include "lib/test/run.hpp" -//#include "lib/test/test-helper.hpp" +#include "lib/test/test-helper.hpp" //#include "proc/asset/media.hpp" //#include "proc/mobject/session.hpp" //#include "proc/mobject/session/edl.hpp" @@ -70,15 +70,18 @@ namespace test { /*************************************************************************** - * @test command usage scenario 1: defining commands in various ways. + * @test command usage aspects I: defining commands in various ways, + * then re-accessing those definitions, create instances, + * invoke them and undo the effect. Clean up finally. * - * @todo WIP + * @see Command + * @see command-basic-test.cpp (simple usage example) */ class CommandUse1_test : public Test { int randVal; - int shuffle() { return randVal = 10 + (rand() % 40); } + int random() { return randVal = 10 + (rand() % 40); } @@ -86,11 +89,18 @@ namespace test { run (Arg) { command1::checksum_ = 0; + uint cnt_defs = Command::definition_count(); + uint cnt_inst = Command::instance_count(); + allInOneStep(); + standardUse(); definePrototype(); usePrototype(); + undef(); - ASSERT (0 == command1::checksum_); + ASSERT (0 == command1::check_); + ASSERT (cnt_defs == Command::definition_count()); + ASSERT (cnt_inst == Command::instance_count()); } @@ -102,41 +112,80 @@ namespace test { .operation (command1::operate) .captureUndo (command1::capture) .undoOperation (command1::undoIt) - .bind (shuffle()) - .exec() + .bind (random()) + .execSync() ; ASSERT (randVal == checksum_); Command::get("test.command1.1").undo(); - ASSERT ( 0 == command1::checksum_); + ASSERT ( 0 == command1::check_); + } + + + void + standardUse() + { + { + CommandDef ("test.command1.2") + .operation (command1::operate) + .captureUndo (command1::capture) + .undoOperation (command1::undoIt) + ; + } + ASSERT ( CommandDef("test.command1.2")); + ASSERT (!Command::get("test.command1.2")); + + Command com = Command::get("test.command1.2"); + ASSERT (!com); + ASSERT (!com.canUndo()); + VERIFY_ERROR (UNBOUND_ARGUMENTS, com() ); + + ASSERT ( 0 == command1::check_); + VERIFY_ERROR (INVALID_ARGUMENTS, com.bind ("foo") ); + com.bind (random()); // note: run-time type check only + com(); + ASSERT (randVal == command1::check_); + com.undo(); + ASSERT ( 0 == command1::check_); + + // the following shortcut does the same: + Command::invoke ("test.command1.2", 1234); + ASSERT ( 1234 == command1::check_); + + // another shortcut, with static type check: + invoke (command1::operate, 5678); + ASSERT ( 1234+5678 == command1::check_); + + com.undo(); + ASSERT ( 0 == command1::check_); } void definePrototype() { - CommandDef ("test.command1.2") + CommandDef ("test.command1.3") .operation (command1::operate) .captureUndo (command1::capture) .undoOperation (command1::undoIt) - .bind (shuffle()) + .bind (random()) ; - ASSERT (Command::get("test.command1.2").canExec()); + ASSERT (Command::get("test.command1.3").canExec()); } void usePrototype() { - Command c1 = Command::get("test.command1.2"); + Command c1 = Command::get("test.command1.3"); ASSERT (c1); ASSERT (c1.canExec()); ASSERT (!c1.canUndo()); - Command c2 = Command::get("test.command1.2"); + Command c2 = Command::get("test.command1.3"); ASSERT (c1); ASSERT (c1.canExec()); ASSERT (!c1.canUndo()); @@ -144,41 +193,111 @@ namespace test { ASSERT (c1 == c2); ASSERT (!isSameObject(c1, c2)); - ASSERT (0 == command1::checksum_); + ASSERT (0 == command1::check_); c1(); - ASSERT (randVal == command1::checksum_); + ASSERT (randVal == command1::check_); ASSERT (c1.canUndo()); ASSERT (c1 != c2); ASSERT (!c2.canUndo()); c2(); - ASSERT (randVal + randVal == command1::checksum_); + ASSERT (randVal + randVal == command1::check_); ASSERT (c2.canUndo()); ASSERT (c1 != c2); c1.undo(); - ASSERT (0 == command1::checksum_); + ASSERT (0 == command1::check_); c2.undo(); - ASSERT (randVal == command1::checksum_); + ASSERT (randVal == command1::check_); c2.bind(23); c2(); - ASSERT (randVal + 23 == command1::checksum_); + ASSERT (randVal + 23 == command1::check_); // you should not use a command more than once (but it works...) c1(); - ASSERT (randVal + 23 + randVal == command1::checksum_); + ASSERT (randVal + 23 + randVal == command1::check_); c1.undo(); - ASSERT (randVal + 23 == command1::checksum_); + ASSERT (randVal + 23 == command1::check_); // note we've overwritten the previous undo state // and get the sate captured on the second invocation c2.undo() - ASSERT (randVal == command1::checksum_); + ASSERT (randVal == command1::check_); c1.undo(); - ASSERT (randVal + 23 == command1::checksum_); + ASSERT (randVal + 23 == command1::check_); + + // use the current sate of c2 as Prototype for new command definition + c2.storeDef ("test.command1.4"); + Command c3 = Command::get("test.command1.4"); + ASSERT (c3); + ASSERT (!c3.canUndo()); + c3(); + ASSERT (randVal + 2*23 == command1::check_); + + c3.bind(-command1::check_); + c3(); + ASSERT (0 == command1::check_); + c2(); + ASSERT (23 == command1::check_); + c2.undo(); + ASSERT (0 == command1::check_); + } + + + void + undef() + { + ASSERT (CommandDef ("test.command1.1")) + ASSERT (CommandDef ("test.command1.2")) + ASSERT (CommandDef ("test.command1.3")) + ASSERT (CommandDef ("test.command1.4")) + + ASSERT (Command::get("test.command1.1")); + ASSERT (Command::get("test.command1.2")); + ASSERT (Command::get("test.command1.3")); + ASSERT (Command::get("test.command1.4")); + + VERIFY_ERROR (INVALID_COMMAND, Command::get("miracle")); + + CommandDef unbelievable ("miracle"); + ASSERT (!unbelievable); + + // but because the miracle isn't yet defined, any use throws + VERIFY_ERROR (INVALID_COMMAND, Command::get("miracle")); + VERIFY_ERROR (UNBOUND_ARGUMENTS, unbelievable.execSync() ); + VERIFY_ERROR (INVALID_COMMAND, unbelievable.bind("abracadabra")); + + ASSERT (Command::remove("test.command1.1")); + ASSERT (Command::remove("test.command1.2")); + ASSERT (Command::remove("test.command1.3")); + + ASSERT (!Command::remove("miracle")); // there is no such thing... + + // note, we didn't remove the *definitions* + // thus we're free to create new instances... + Command xxx = Command::get("test.command1.1"); + VERIFY_ERROR (UNBOUND_ARGUMENTS, xxx() ); + + // but this one is still there (we didn't remove it) + ASSERT (Command::get("test.command1.4")); + + // now kill the definitions too... + ASSERT (Command::undef ("test.command1.1")); + ASSERT (Command::undef ("test.command1.2")); + ASSERT (Command::undef ("test.command1.3")); + ASSERT (Command::undef ("test.command1.4")); + ASSERT (Command::undef ("miracle")); + + VERIFY_ERROR (INVALID_COMMAND, Command::get("test.command1.1")); + VERIFY_ERROR (INVALID_COMMAND, Command::get("test.command1.2")); + VERIFY_ERROR (INVALID_COMMAND, Command::get("test.command1.3")); + VERIFY_ERROR (INVALID_COMMAND, Command::get("miracle")); + + // note: removing the definition automatically killed the remaining instance: + VERIFY_ERROR (INVALID_COMMAND, Command::get("test.command1.4")); } }; diff --git a/tests/components/proc/control/command-use2-test.cpp b/tests/components/proc/control/command-use2-test.cpp new file mode 100644 index 000000000..924f9e8c5 --- /dev/null +++ b/tests/components/proc/control/command-use2-test.cpp @@ -0,0 +1,208 @@ +/* + CommandUse2(Test) - usage aspects II + + Copyright (C) Lumiera.org + 2009, Hermann Vosseler + + 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., 675 Mass Ave, Cambridge, MA 02139, USA. + +* *****************************************************/ + + +#include "lib/test/run.hpp" +#include "lib/test/test-helper.hpp" +//#include "proc/asset/media.hpp" +//#include "proc/mobject/session.hpp" +//#include "proc/mobject/session/edl.hpp" +//#include "proc/mobject/session/testclip.hpp" +//#include "proc/mobject/test-dummy-mobject.hpp" +//#include "lib/p.hpp" +//#include "proc/mobject/placement.hpp" +//#include "proc/mobject/placement-index.hpp" +//#include "proc/mobject/explicitplacement.hpp" +#include "proc/control/command-def.hpp" +//#include "lib/lumitime.hpp" +#include "lib/util.hpp" + +#include "proc/control/test-dummy-commands.hpp" + +#include +#include +#include +//#include +//#include +#include + + +namespace control { +namespace test { + + + using boost::format; + using boost::str; + //using lumiera::Time; + //using util::contains; + using std::tr1::function; + using std::tr1::bind; + using std::string; + //using std::rand; + //using std::cout; + //using std::endl; +// using lib::test::showSizeof; +// using util::isSameObject; + using util::contains; + using boost::ref; + +// using session::test::TestClip; +// using lumiera::P; + + + //using lumiera::typelist::BuildTupleAccessor; + using lumiera::error::LUMIERA_ERROR_EXTERNAL; + + + + + + /*************************************************************************** + * @test command usage aspects II: patterns of invoking commands. + * + * @see Command + * @see command-basic-test.cpp (simple usage example) + */ + class CommandUse2_test : public Test + { + + int randVal_; + + string randomTxt() + { + format fmt ("invoked( %2d )"); + + randVal_ = rand() % 100; + return str (fmt % randVal_); + } + + bool blowUp_ = false; + + + virtual void + run (Arg) + { + command2::check_.seekp(0); + uint cnt_defs = Command::definition_count(); + uint cnt_inst = Command::instance_count(); + + function randFun = bind (&CommandUse2_test::randomTxt, this); + + // prepare a command definition (prototype) + CommandDef ("test.command2") + .operation (command2::operate) + .captureUndo (command2::capture) + .undoOperation (command2::undoIt) + .bind (randFun, ref(blowUp_)); + + // note: blowUp_ is bound via reference_wrapper, + // so we can pull the trigger to provoke an exception + blowUp_ = false; + + + check_defaultHandlingPattern(); + check_ThrowOnError(); + + + Command::undef ("test.command2"); + Command::undef ("test.command2.1"); + ASSERT (cnt_defs == Command::definition_count()); + ASSERT (cnt_inst == Command::instance_count()); + } + + + + void + check_defaultHandlingPattern() + { + Command com = Command::get("test.command2"); + + ASSERT (!contains (command2::check_, "invoked")); + + bool res = com(); + + ASSERT (res); + ASSERT (contains (command2::check_, "invoked")); + ASSERT (contains (command2::check_, randVal_)); + + res = com.undo(); + ASSERT (res); // UNDO invoked successfully + ASSERT (!contains (command2::check_, randVal_)); + ASSERT (contains (command2::check_, "UNDO")); + + blowUp_ = true; + string current = command2::check_.str(); + + res = com(); + ASSERT (!res); // not executed successfully (exception thrown) + ASSERT (command2::check_.str() == current); + ASSERT (LUMIERA_ERROR_EXTERNAL == lumiera_error()); + + res = com.undo(); + ASSERT (!res); // UNDO failed (exception thrown) + ASSERT (command2::check_.str() == current); + ASSERT (LUMIERA_ERROR_EXTERNAL == lumiera_error()); + + blowUp_ = false; + } + + + + void + check_ThrowOnError() + { + Command com = Command::get("test.command2"); + + blowUp_ = false; + com.exec(HandlingPattern::THROW_SYNC); + ASSERT (contains (command2::check_, randVal_)); + + blowUp_ = true; + string current = command2::check_.str(); + VERIFY_ERROR( EXTERNAL, com.exec(HandlingPattern::THROW_SYNC) ); + ASSERT (command2::check_.str() == current); + + // we can achieve the same effect, + // after changing the default HandlingPatern for this command instance + com.setHandlingPattern(HandlingPattern::THROW_SYNC); + com.storeDef ("test.command2.1"); + + Command com2 = Command::get("test.command2.1"); + VERIFY_ERROR( EXTERNAL, com2() ); + ASSERT (command2::check_.str() == current); + + blowUp_ = false; + com2(); + ASSERT (command2::check_.str() > current); + ASSERT (contains (command2::check_, randVal_)); + + com2.undo(); + ASSERT (!contains (command2::check_, randVal_)); + } + }; + + + /** Register this test class... */ + LAUNCHER (CommandUse2_test, "function controller"); + + +}} // namespace control::test diff --git a/tests/components/proc/control/command-use3-test.cpp b/tests/components/proc/control/command-use3-test.cpp new file mode 100644 index 000000000..03048599c --- /dev/null +++ b/tests/components/proc/control/command-use3-test.cpp @@ -0,0 +1,121 @@ +/* + CommandUse3(Test) - usage aspects III + + Copyright (C) Lumiera.org + 2009, Hermann Vosseler + + 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., 675 Mass Ave, Cambridge, MA 02139, USA. + +* *****************************************************/ + + +#include "lib/test/run.hpp" +#include "lib/test/test-helper.hpp" +//#include "proc/asset/media.hpp" +//#include "proc/mobject/session.hpp" +//#include "proc/mobject/session/edl.hpp" +//#include "proc/mobject/session/testclip.hpp" +//#include "proc/mobject/test-dummy-mobject.hpp" +//#include "lib/p.hpp" +//#include "proc/mobject/placement.hpp" +//#include "proc/mobject/placement-index.hpp" +//#include "proc/mobject/explicitplacement.hpp" +#include "proc/control/command-def.hpp" +//#include "lib/lumitime.hpp" +#include "lib/util.hpp" + +#include "proc/control/test-dummy-commands.hpp" + +//#include +//#include +//#include +//#include +//#include +//#include + + +namespace control { +namespace test { + + +// using boost::format; +// using boost::str; + //using lumiera::Time; + //using util::contains; +// using std::tr1::function; +// using std::tr1::bind; +// using std::string; + //using std::rand; + //using std::cout; + //using std::endl; +// using lib::test::showSizeof; +// using util::isSameObject; +// using util::contains; + +// using session::test::TestClip; +// using lumiera::P; + + + //using lumiera::typelist::BuildTupleAccessor; +// using lumiera::error::LUMIERA_ERROR_EXTERNAL; + + + + + + /*************************************************************************** + * @test command usage aspects III: elaborate handling patterns, like e.g. + * asynchronous or repeated invocation and command sequence bundles. + * + * @todo planned but not implemented as of 7/09 + * + * @see HandlingPattern + */ + class CommandUse3_test : public Test + { + + + virtual void + run (Arg) + { + command1::check_ = 0; + uint cnt_defs = Command::definition_count(); + uint cnt_inst = Command::instance_count(); + + // prepare a command definition (prototype) + CommandDef ("test.command1.1") + .operation (command1::operate) + .captureUndo (command1::capture) + .undoOperation (command1::undoIt); + + UNIMPLEMENTED ("more elaborate command handling patterns") + ////////////////////////////////////////////////////////////////////////////////TODO: devise tests for async, repeated and compound sequences + + ASSERT (cnt_inst == Command::instance_count()); + + Command::undef ("test.command1.1"); + ASSERT (cnt_defs == Command::definition_count()); + } + + + + }; + + + /** Register this test class... */ + LAUNCHER (CommandUse3_test, "function controller"); + + +}} // namespace control::test diff --git a/tests/components/proc/control/test-dummy-commands.hpp b/tests/components/proc/control/test-dummy-commands.hpp index 9f94cc725..4bbf9ed0e 100644 --- a/tests/components/proc/control/test-dummy-commands.hpp +++ b/tests/components/proc/control/test-dummy-commands.hpp @@ -61,42 +61,88 @@ //#include //using boost::format; + //using lumiera::Time; //using util::contains; //using std::string; //using std::rand; //using std::cout; //using std::endl; +#include +#include +#include namespace control { namespace test { // using lib::test::showSizeof; + using std::ostringstream; + using std::tr1::function; + using std::string; namespace command1 { ///< test command just adding a given value - long checksum_ = 0; + long check_ = 0; void operate (int someVal) { - checksum_ += someVal; + check_ += someVal; } long capture (int) { - return checksum_; + return check_; } void undoIt (int, long oldVal) { - checksum_ = oldVal; + check_ = oldVal; + } + + } + + + + + + namespace command2 { ///< test command writing to protocol and possibly throwing + + using lumiera::error::External; + + + ostringstream check_; + + + typedef function FunS; + + void + operate (FunS func, bool fail) + { + if (fail) throw External("simulated exception"); + + check_ << func(); + } + + string + capture (FunS, bool) + { + return check_.str(); + } + + void + undoIt (FunS, bool fail, string previousProtocol) + { + if (fail) throw External("simulated exception in UNDO"); + + check_.seekp(0); + check_ << previousProtocol << "|UNDO|"; } } diff --git a/wiki/renderengine.html b/wiki/renderengine.html index 2e9bd3a53..0df78873b 100644 --- a/wiki/renderengine.html +++ b/wiki/renderengine.html @@ -1012,20 +1012,20 @@ A command may be [[defined|CommandDefinition]] completely from scratch, or it mi By design, commands are single-serving value objects; executing an operation repeatedly requires creating a collection of command objects, one for each invocation. While nothing prevents you from invoking the command operation functor several times, each invocation will overwrite the undo state captrued by the previous invocation. Thus, each command instance should bes seen as the promise (or later the trace) of a single operation execution. In a similar vein, the undo capturing should be defined as to be self sufficient, so that invoking just the undo functor of a single command performes any necessary steps to restore the situation found before invoking the corresponding mutation functor &mdash; of course only //with respect to the topic covered by this command.// So, while commands provide a lot of flexibility and allow to do a multitude of things, certainly there is an intended CommandLifecycle. &rarr; more on possible [[command usage scenarios|CommandUsage]] -
+
[<img[Structure of Commands|uml/fig135173.png]]
 While generally the command framework was designed to be flexible and allow a lot of different use cases, execution paths and to serve various goals, there is an ''intended lifecycle'' &mdash; commands are expected to go through several distinct states.
 
-The handling of a command starts out with a ''command ID'' provided by the client code. Command IDs are unique (human readable) identifiers and should be organised in a hierarchical fashion. When provided with an ID, the CommandRegistry tries to fetch an existing command definition. In case this fails, we enter the [[command definition stage|CommandDefinition]], which includes specifying functions to implement the operation, state capturing and UNDO. When all these informations are available, the entity is called a ''command definition''. It is comparable to a //class// or //meta object.//
+The handling of a command starts out with a ''command ID'' provided by the client code. Command IDs are unique (human readable) identifiers and should be organised in a hierarchical fashion. When provided with an ID, the CommandRegistry tries to fetch an existing command definition. In case this fails, we enter the [[command definition stage|CommandDefinition]], which includes specifying functions to implement the operation, state capturing and UNDO. When all these informations are available, the entity is called a ''command definition''. Conceptually, it is comparable to a //class// or //meta object.//
 
-By ''binding'' to specific operation arguments, the definition is //armed// and becomes a real ''command''. This is similar to creating an instance from a class
+By ''binding'' to specific operation arguments, the definition is //armed up//&nbsp; and becomes a real ''command''. This is similar to creating an instance from a class. Behind the scenes, storage is allocated to hold the argument values and any state captured to create the ability to UNDO the command's effect later on.
 
 A command is operated or executed by passing it to an ''execution pattern'' &mdash; there is a multitude of possible execution patterns to choose from, depending on the situation. 
 {{red{WIP}}}
-When a command has been executed (and maybe undone), it's best to leave it alone, because the UndoManager might hold a reference. At any time, c ''clone of the command'' could be created, maybe bound with different arguments and treated separately from the original command.
+When a command has been executed (and maybe undone), it's best to leave it alone, because the UndoManager might hold a reference. Anyway, a ''clone of the command'' could be created, maybe bound with different arguments and treated separately from the original command.
 
-
+
//for now (7/09) I'll use this page to collect ideas how commands might be used...//
 
 * use a command for getting a log entry and an undo possibility automatically
@@ -1052,7 +1052,7 @@ When a command has been executed (and maybe undone), it's best to leave it alone
 
 !!!an execution pattern....
 * can only be defined in code by a class definition, not at runtime
-* subclasses the !HandlingPattern interface, which automatically creates an registration.
+* subclasses the ~HandlingPattern interface, which automatically creates an registration.
 * a singleton instance is created on demand, triggered by referring the pattern's ID
 * is conceptually //stateless// &mdash; of course there can be common configuration values
 * is always invoked providing a concrete command instance to execute