/* BusTerm(Test) - cover the building block of the UI-Bus Copyright (C) Lumiera.org 2015, 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. * *****************************************************/ /** @file bus-term-test.cpp ** unit test \ref BusTerm_test */ #include "lib/test/run.hpp" #include "lib/test/test-helper.hpp" #include "lib/thread.hpp" #include "lib/sync.hpp" #include "lib/sync-classlock.hpp" #include "include/ui-protocol.hpp" #include "stage/ctrl/bus-term.hpp" #include "stage/ctrl/state-manager.hpp" #include "steam/control/command.hpp" #include "test/test-nexus.hpp" #include "test/mock-elm.hpp" #include "lib/diff/gen-node.hpp" #include "lib/diff/mutation-message.hpp" #include "lib/idi/entry-id.hpp" #include "lib/iter-adapter-stl.hpp" #include "lib/iter-stack.hpp" #include "lib/call-queue.hpp" #include "lib/format-string.hpp" #include "lib/format-cout.hpp" #include "lib/time/timevalue.hpp" #include "lib/luid.h" #include "lib/util.hpp" #include using boost::lexical_cast; using lib::Sync; using lib::ClassLock; using lib::ThreadJoinable; using lib::iter_stl::dischargeToSnapshot; using lib::IterQueue; using lib::IterStack; using std::function; using util::contains; using util::isnil; using util::_Fmt; namespace stage { namespace model{ namespace test { using lib::idi::EntryID; using lib::idi::BareEntryID; using steam::control::Command; using stage::ctrl::StateManager; using stage::ctrl::BusTerm; using stage::test::MockElm; using lib::diff::MutationMessage; using lib::diff::TreeDiffLanguage; using lib::diff::DiffSource; using lib::diff::DiffStep; using lib::diff::GenNode; using lib::diff::MakeRec; using lib::diff::Rec; using lib::diff::Ref; using lib::time::Time; using lib::time::TimeSpan; using lib::hash::LuidH; using lib::test::EventLog; using lib::CallQueue; using LERR_(UNBOUND_ARGUMENTS); using LERR_(WRONG_TYPE); using ID = lib::idi::BareEntryID const&; namespace {// test data... // --------random-diff-test------ uint const MAX_RAND_BORGS = 100; // stay below 400, verification export grows quadratic uint const MAX_RAND_NUMBS = 500; uint const MAX_RAND_DELAY = 5000; // throttle generation, since diff application is slower // --------random-diff-test------ int generator_instances = 0; } /**************************************************************************//** * @test cover the standard node element (terminal element) within the UI-Bus, * with the help of an attached mock UI element. Contrary to the related * [ui-element test](\ref AbstractTangible_test), here we focus on the bus side * of the standard interactions. * * This test enacts the fundamental generic communication patterns * to verify the messaging behaviour * - attaching a \ref BusTerm * - detaching on element destruction * - generate a command invocation * - argument passing * - capture a _state mark_ * - replay a _state mark_ * - cast messages and error states downstream * - generic operating of interface states * - multithreaded integration test of diff mutation * * @see AbstractTangible_test * @see stage::model::Tangible * @see stage::ctrl::BusTerm */ class BusTerm_test : public Test { virtual void run (Arg) { seedRand(); attachNewBusTerm(); commandInvocation(); captureStateMark(); replayStateMark(); verifyNotifications(); clearStates(); pushDiff(); } /** @test build a new BusTerm and verify connectivity. * Every [tangible UI-element](\ref Tangible) bears an embedded BusTerm * member. Since the latter _requires another, up-link BusTerm_ on construction, * connection to the [UI-Bus](\ref ui-bus.hpp) is structurally ensured. Moreover, * when hooking up a new UI-element, the initialisation of the embedded BusTerm * will cause a down-link connection to be installed into the central routing * table within the \ref Nexus, the hub of the UI-Bus. Routing and addressing * is based on the UI-element's unique EntryID, destruction of the element, * through invocation of BusTerm's dtor, will ensure deregistration * from the Hub. */ void attachNewBusTerm() { MARK_TEST_FUN; // our dummy will be linked with this identity BareEntryID elmID = EntryID{"zeitgeist"}; // Access the log on the Test-Nexus hub EventLog nexusLog = stage::test::Nexus::startNewLog(); CHECK (nexusLog.ensureNot("zeitgeist")); MockElm mock(elmID); CHECK (nexusLog.verifyCall("routeAdd").on("TestNexus").arg(elmID,"Tangible") // Note: invoked from ctor, so it is just a tangible at the moment .beforeEvent("TestNexus", "added route to bID-zeitgeist")); EventLog elmLog = mock.getLog(); CHECK (elmLog.verifyCall("ctor").on(&mock) .beforeEvent("create", "zeitgeist")); // now verify there is indeed bidirectional connectivity... CHECK (elmLog.ensureNot("expanded")); CHECK (elmLog.ensureNot("doFlash")); CHECK (nexusLog.ensureNot("zeitgeist").arg("expand")); CHECK (nexusLog.ensureNot("zeitgeist").arg("Flash")); // invoke action on element to cause upstream message (with a "state mark") mock.slotExpand(); CHECK (elmLog.verifyEvent("expanded")); CHECK (nexusLog.verifyCall("note").on("TestNexus").arg(elmID, "GenNode-ID(\"expand\")-DataCap|«bool»|true")); // send a state mark down to the mock element stage::test::Nexus::testUI().mark (elmID, GenNode(string{MARK_Flash}, 23)); CHECK (nexusLog.verifyCall("mark").on("TestNexus").arg(elmID, MARK_Flash) .beforeEvent("TestNexus", "mark to bID-zeitgeist")); CHECK (elmLog.verifyCall("doFlash").on("zeitgeist")); // kill the zeitgeist and verify disconnection mock.kill(); CHECK (elmLog.verifyEvent("destroy","zeitgeist")); CHECK (nexusLog.verifyCall("routeDetach").on("TestNexus").arg(elmID) .beforeEvent("TestNexus", "removed route to bID-zeitgeist")); stage::test::Nexus::testUI().mark (elmID, GenNode({MARK_Flash}, 88)); CHECK (nexusLog.verify("removed route to bID-zeitgeist") .beforeCall("mark").on("TestNexus").arg(elmID, MARK_Flash) .beforeEvent("warn","discarding mark to unknown bID-zeitgeist")); CHECK (elmLog.ensureNot("Flash") .afterEvent("destroy","zeitgeist")); cout << "____Probe-Log_________________\n" << util::join(elmLog, "\n") << "\n───╼━━━━━━━━━╾────────────────"<(); MockElm mock("uiElm"); // random command arguments... string text {lib::test::randStr(12)}; TimeSpan clip (Time(1,2,3), lib::test::randTime()); LuidH luid; // we cannot invoke commands without binding required arguments VERIFY_ERROR (WRONG_TYPE, mock.invoke(cmd) ); // proper argument typing is ensured while dispatching the bind message. VERIFY_ERROR (WRONG_TYPE, mock.invoke(cmd, Rec({"lalala"})) ); // command can't be issued, since it's still unbound CHECK (not Command::canExec(cmd)); mock.invoke (cmd, text, clip, luid); CHECK (Command::canExec(cmd)); CHECK (stage::test::Nexus::wasBound(cmd, text, clip, luid)); CHECK (not stage::test::Nexus::wasBound(cmd, "lololo")); CHECK (stage::test::Nexus::wasInvoked(cmd)); CHECK (stage::test::Nexus::wasInvoked(cmd, text, clip, luid)); CHECK (not stage::test::Nexus::wasInvoked(cmd, " huh ", clip, luid)); CHECK (not stage::test::Nexus::wasInvoked(cmd, text, clip)); // Mock commands are automatically unique auto cmdX = stage::test::Nexus::prepareMockCmd<>(); auto cmdY = stage::test::Nexus::prepareMockCmd<>(); CHECK (cmd != cmdX); CHECK (cmd != cmdY); CHECK (not stage::test::Nexus::wasInvoked(cmdX)); CHECK (not stage::test::Nexus::wasInvoked(cmdY)); cout << "____Nexus-Log_________________\n" << util::join(stage::test::Nexus::getLog(), "\n") << "\n───╼━━━━━━━━━╾────────────────"< bruno("bruno"); CHECK (stateManager.currentState(bruno, "expand") == Ref::NO); // who knows bruno? mockC.slotExpand(); CHECK (stateManager.currentState(charly, "expand") == GenNode("expand", true )); // error states can be sticky mockC.markErr("overinflated"); CHECK (stateManager.currentState(charly, "Error") == GenNode("Error", "overinflated")); mockC.reset(); CHECK (stateManager.currentState(charly, "expand") == Ref::NO); // back to void cout << "____Nexus-Log_________________\n" << util::join(stage::test::Nexus::getLog(), "\n") << "\n───╼━━━━━━━━━╾────────────────"< , ThreadJoinable<> { // shared data uint64_t borgChecksum_ = 0; IterStack sessionBorgs_; // access to shared session data void scheduleBorg (uint id) { Lock sync{this}; borgChecksum_ += id; sessionBorgs_.push(id); } auto dispatchBorgs() { Lock sync{this}; return dischargeToSnapshot (sessionBorgs_); } /** * Independent heap allocated diff generator. * Implements the IterSource interface * and will be pulled from the GUI-Thread for actually * generating the diff. At this point, it needs to access * the shared session data with proper locking, and derive * a representation of the "changes" in diff format */ struct BorgGenerator : util::NonCopyable , TreeDiffLanguage , DiffSource { uint generatorID_; SessionThread& theCube_; IterQueue steps_; BorgGenerator (SessionThread& motherShip, uint id) : generatorID_{id} , theCube_{motherShip} { ClassLock sync; ++generator_instances; } ~BorgGenerator() { ClassLock sync; --generator_instances; } /* == Interface IterSource == */ virtual DiffStep* firstResult () override { REQUIRE (not steps_); auto plannedBorgs = theCube_.dispatchBorgs(); uint max = plannedBorgs.size(); uint cur = 0; _Fmt borgName{"%d of %d ≺%03d.gen%03d≻"}; steps_.feed (after(Ref::ATTRIBS)); // important: retain all existing attributes for (uint id : plannedBorgs) // Generate diff to inject a flock of Borg { GenNode borg = MakeRec().genNode(borgName % ++cur % max % id % generatorID_); steps_.feed (ins(borg)); steps_.feed (mut(borg)); // open nested scope for this Borg steps_.feed ( ins(GenNode{"borgID", int(id)})); steps_.feed (emu(borg)); // close nested scope } steps_.feed (after(Ref::END)); // important: fast-forward and accept already existing Borgs return & *steps_; // the IterSource protocol requires us to return a ptr to current element } virtual void nextResult (DiffStep*& pos) override { if (!pos) return; if (steps_) ++steps_; if (steps_) pos = & *steps_; // pointer to current element else pos = NULL; // signal iteration end } }; /** * launch the Session Thread and start injecting Borgs */ SessionThread(function notifyGUI) : ThreadJoinable{"BusTerm_test: asynchronous diff mutation" , [=] { uint cnt = randGen_.i(MAX_RAND_BORGS); for (uint i=0; i (borgID); string childID = borg.getID().getSym(); CHECK (contains (childID, borgID)); CHECK (contains (childID, " of ")); // e.g. "3 of 5" CHECK (nexusLog.verifyCall("routeAdd").arg(rootMock.getID(), memLocation(rootMock)) // rootMock was attached to Nexus .beforeCall("change") .argMatch(rootMock.getID(), // diff message sent via UI-Bus "after.+?_ATTRIBS_.+?" // verify diff pattern generated for each Borg "ins.+?"+childID+".+?" "mut.+?"+childID+".+?" "ins.+?borgID.+?"+borgID+".+?" "emu.+?"+childID) .beforeCall("routeAdd").arg(borg.getID(), memLocation(borg)) // Borg was inserted as child and attached to Nexus .beforeEvent("applied diff to "+string(rootMock.getID())) ); ////////////////////////////////////////////////////////TICKET #1158 } CHECK (rootMock.attrib["α"] == "Quadrant"); // attribute alpha was preserved while injecting all those Borg // sanity checks CHECK (borgChecksum == session.borgChecksum_); // no Borgs got lost CHECK (0 == generator_instances); // no generator instance leaks cout << "____Event-Log_________________\n" << util::join(rootMock.getLog(), "\n") << "\n───╼━━━━━━━━━╾────────────────"<