/* SubsystemRunner(Test) - validate starting and stopping of dependent subsystems Copyright (C) Lumiera.org 2008, 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 "common/subsys.hpp" #include "common/subsystem-runner.hpp" #include "common/option.hpp" #include "lib/symbol.hpp" #include "backend/thread-wrapper.hpp" #include "lib/query-util.hpp" #include "lib/error.hpp" #include "lib/util.hpp" #include "lib/sync.hpp" #include #include using std::cout; using std::bind; using util::isnil; using util::cStr; using test::Test; using lib::Literal; using lib::query::extractID; using backend::Thread; namespace lumiera { namespace test { namespace { // private test classes and data... using lib::query::extractID; /** limit for the randomly selected duration of * subsystem's running phase (milliseconds) */ const uint MAX_RUNNING_TIME_ms = 80; const uint MIN_RUNNING_TIME_ms = 20; /** the "running" subsystem checks for a * shutdown request every XX milliseconds */ const uint TICK_DURATION_ms = 5; /** due to a shortcoming of this test fixture, * a floundering subsystem continues to run for * a short time after the sync barrier. * Relevant for singleSubsys_start_failure(). */ const uint DELAY_FOR_FLOUNDERING_THRAD_ms = 20; /** dummy options just to be ignored */ lib::Cmdline dummyArgs (""); lumiera::Option dummyOpt (dummyArgs); /** marker for simulated failure exceptions */ LUMIERA_ERROR_DEFINE( TEST, "simulated failure."); using error::LUMIERA_ERROR_LOGIC; using error::LUMIERA_ERROR_STATE; /** * A simulated "Lumiera Subsystem". * It is capable of starting a separate thread, which may terminate regularly * after a random time, or may fail in various ways. The behaviour is controlled * by a number of definitions, given at construction in logic predicate notation. */ class MockSys : public lumiera::Subsys { Literal id_; const string spec_; volatile bool isUp_; volatile bool didRun_; volatile bool started_; volatile bool termRequest_; int running_duration_; bool shouldStart (lumiera::Option&) { string startSpec (extractID ("start",spec_)); return "true" ==startSpec || "fail" ==startSpec || "throw"==startSpec; } bool start (lumiera::Option&, Subsys::SigTerm termination) { CHECK (!(isUp_|started_|didRun_), "attempt to start %s twice!", cStr(*this)); string startSpec (extractID ("start",spec_)); CHECK (!isnil (startSpec)); if ("true"==startSpec) //----simulate successful subsystem start { CHECK (!started_); Thread (id_, bind (&MockSys::run, this, termination)) .sync(); // run-status handshake CHECK (started_); } else if ("fail"==startSpec) //----not starting, incorrectly reporting success return true; else if ("throw"==startSpec) //---starting flounders throw error::Fatal("simulated failure to start the subsystem", LUMIERA_ERROR_TEST); return started_; } void triggerShutdown () throw() { // note: *not* locking here... termRequest_ = true; INFO (test, "triggerShutdown() --> %s....", cStr(*this)); } bool checkRunningState () throw() { // note: *not* locking here... return isUp_; } /** executes in a separate thread and * simulates a "running" subsystem. * Behaviour determined by run(XX) spec: * - run(true) : start, run, terminate normally * - run(throw): start, run, signal abnormal termination * - run(fail) : set didRun_, but abort, never enter running state * - run(false): just handshake, but then abort without further action */ void run (Subsys::SigTerm termination) { string runSpec (extractID ("run",spec_)); CHECK (!isnil (runSpec)); // run-status handshake started_ = true; isUp_ = ("true"==runSpec || "throw"==runSpec); didRun_ = ("false"!=runSpec); // includes "fail" and "throw" lumiera_thread_sync (); if (isUp_) //-------------actually enter running state for some time { running_duration_ = MIN_RUNNING_TIME_ms; running_duration_ += (rand() % (MAX_RUNNING_TIME_ms - MIN_RUNNING_TIME_ms)); INFO (test, "thread %s now running....", cStr(*this)); while (!shouldTerminate()) { usleep (1000*TICK_DURATION_ms); running_duration_ -= TICK_DURATION_ms; } INFO (test, "thread %s about to terminate...", cStr(*this)); isUp_ = false; } if ("fail" ==runSpec) return; // terminate without further notice if ("true" ==runSpec) termination(0); // signal regular termination if ("throw"==runSpec) { Error problemIndicator("simulated Problem terminating subsystem",LUMIERA_ERROR_TEST); lumiera_error(); // reset error state.... // Note: in real life this actually // would be an catched exception! string problemReport (problemIndicator.what()); termination (&problemReport); } } bool shouldTerminate () ///< simulates async termination, either on request or by timing { return termRequest_ || running_duration_ <= 0; } public: MockSys(Literal id, Literal spec) : id_(id), spec_(spec), isUp_(false), didRun_(false), started_(false), termRequest_(false), running_duration_(0) { } ~MockSys() { } operator string () const { return "MockSys(\""+id_+"\")"; } friend inline ostream& operator<< (ostream& os, MockSys const& subsys) { return os << string(subsys); } bool didRun () const { return didRun_; } }; } // (End) test classes and data.... /**********************************************************************//** * @test managing start and stop of several dependent "subsystems" * under various conditions. Using mock-subsystems, which actually * spawn a thread and finish by themselves and generally behave sane. * For each such MockSys, we can define a behaviour pattern, e.g. * weather the start succeeds and if the run terminates with error. * * @see lumiera::Subsys * @see lumiera::SubsystemRunner * @see lumiera::AppState * @see main.cpp */ class SubsystemRunner_test : public Test { virtual void run (Arg) { singleSubsys_complete_cycle(); singleSubsys_start_failure(); singleSubsys_emegency_exit(); dependentSubsys_complete_cycle(); dependentSubsys_start_failure(); } void singleSubsys_complete_cycle() { cout << "-----singleSubsys_complete_cycle-----\n"; MockSys unit ("one", "start(true), run(true)."); SubsystemRunner runner(dummyOpt); CHECK (!unit.isRunning()); CHECK (!unit.didRun()); runner.maybeRun (unit); bool emergency = runner.wait(); CHECK (!emergency); CHECK (!unit.isRunning()); CHECK (unit.didRun()); } /** @note as this test focuses on the SubsystemRunner, the mock subsystem * is implemented rather simplistic. Especially, there is a race when a * subsystem is configured to "fail" -- because in this case the starting * context may go away before the remainder of the subsystem thread has * executed after the sync() barrier. Especially in this case, no MockSys * actually starts without failure, and thus the SubsystemRunner::wait() * has no guarding effect. This can be considered a shortcoming of the * test fixture; a well behaved subsystem won't just go away... */ void singleSubsys_start_failure() { cout << "-----singleSubsys_start_failure-----\n"; MockSys unit1 ("U1", "start(false), run(false)."); MockSys unit2 ("U2", "start(throw), run(false)."); MockSys unit3 ("U3", "start(fail), run(false)."); // simulates incorrect behaviour MockSys unit4 ("U4", "start(true), run(fail)." ); // simulates failure immediately after start SubsystemRunner runner(dummyOpt); runner.maybeRun (unit1); // this one doesn't start at all, which isn't considered an error VERIFY_ERROR (TEST, runner.maybeRun (unit2) ); VERIFY_ERROR (LOGIC, runner.maybeRun (unit3) ); // incorrect behaviour trapped VERIFY_ERROR (LOGIC, runner.maybeRun (unit4) ); // detected that the subsystem didn't come up usleep (DELAY_FOR_FLOUNDERING_THRAD_ms * 1000); // preempt to allow unit4 to go away runner.wait(); CHECK (!unit1.isRunning()); CHECK (!unit2.isRunning()); CHECK (!unit3.isRunning()); CHECK (!unit4.isRunning()); CHECK (!unit1.didRun()); CHECK (!unit2.didRun()); CHECK (!unit3.didRun()); CHECK ( unit4.didRun()); // ...but it failed immediately } void singleSubsys_emegency_exit() { cout << "-----singleSubsys_emegency_exit-----\n"; MockSys unit ("one", "start(true), run(throw)."); SubsystemRunner runner(dummyOpt); runner.maybeRun (unit); bool emergency = runner.wait(); CHECK (emergency); // emergency state got propagated CHECK (!unit.isRunning()); CHECK (unit.didRun()); } void dependentSubsys_complete_cycle() { cout << "-----dependentSubsys_complete_cycle-----\n"; MockSys unit1 ("U1", "start(true), run(true)."); MockSys unit2 ("U2", "start(true), run(true)."); MockSys unit3 ("U3", "start(true), run(true)."); MockSys unit4 ("U4", "start(true), run(true)."); unit2.depends (unit1); unit4.depends (unit3); unit4.depends (unit1); unit3.depends (unit2); SubsystemRunner runner(dummyOpt); runner.maybeRun (unit4); CHECK (unit1.isRunning()); CHECK (unit2.isRunning()); CHECK (unit3.isRunning()); CHECK (unit4.isRunning()); bool emergency = runner.wait(); CHECK (!emergency); CHECK (!unit1.isRunning()); CHECK (!unit2.isRunning()); CHECK (!unit3.isRunning()); CHECK (!unit4.isRunning()); CHECK (unit1.didRun()); CHECK (unit2.didRun()); CHECK (unit3.didRun()); CHECK (unit4.didRun()); } void dependentSubsys_start_failure() { cout << "-----dependentSubsys_start_failure-----\n"; MockSys unit1 ("U1", "start(true), run(true)."); MockSys unit2 ("U2", "start(true), run(true)."); MockSys unit3 ("U3", "start(false),run(false)."); // note MockSys unit4 ("U4", "start(true), run(true)."); unit2.depends (unit1); unit4.depends (unit3); unit4.depends (unit1); unit3.depends (unit2); SubsystemRunner runner(dummyOpt); VERIFY_ERROR (STATE, runner.maybeRun (unit4) ); // failure to bring up prerequisites is detected CHECK ( unit1.isRunning()); CHECK ( unit2.isRunning()); CHECK (!unit3.isRunning()); // shutdown has been triggered for unit4, but may require some time bool emergency = runner.wait(); CHECK (!emergency); // no problems with the subsystems actually running... CHECK (!unit1.isRunning()); CHECK (!unit2.isRunning()); CHECK (!unit3.isRunning()); CHECK (!unit4.isRunning()); CHECK ( unit1.didRun()); CHECK ( unit2.didRun()); CHECK (!unit3.didRun()); // can't say for sure if unit4 actually did run } }; /** Register this test class... */ LAUNCHER (SubsystemRunner_test, "function common"); }} // namespace lumiera::test