/* MockSupport(Test) - verify test support for fixture and job dispatch Copyright (C) 2023, Hermann Vosseler   **Lumiera** 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. See the file COPYING for further details. * *****************************************************************/ /** @file mock-support-test.cpp ** unit test \ref MockSupport_test */ #include "lib/test/run.hpp" #include "lib/test/test-helper.hpp" #include "steam/engine/mock-dispatcher.hpp" #include "vault/gear/nop-job-functor.hpp" #include "lib/iter-explorer.hpp" #include "lib/util-tuple.hpp" #include "lib/util.hpp" using test::Test; namespace steam { namespace engine{ namespace test { using steam::fixture::Segment; using lib::singleValIterator; using util::isSameObject; using util::seqTuple; /**********************************************************************//** * @test validate test support for render job planning and dispatch. * - creating and invoking mock render jobs * - a mocked JobTicket, generating mock render jobs * - configurable test setup for a mocked Segmentation datastructure * - configurable setup of a complete frame Dispatcher * @see JobPlanningPipeline_test * @see Dispatcher * @see vault::gear::Job * @see steam::fixture::Segmentation */ class MockSupport_test : public Test { virtual void run (Arg) { seedRand(); simpleUsage(); verify_MockJob(); verify_MockJobTicket(); verify_MockSegmentation(); verify_MockPrerequisites(); verify_MockDispatcherSetup(); } /** @test simple usage example of the test helpers */ void simpleUsage() { // Build a simple Segment at [10s ... 20s[ MockSegmentation mockSegs{MakeRec() .attrib ("start", Time{0,10} ,"after", Time{0,20}) .genNode()}; CHECK (3 == mockSegs.size()); fixture::Segment const& seg = mockSegs[Time{0,15}]; // access anywhere 10s <= t < 20s JobTicket& ticket = seg.jobTicket(0); Job job = ticket.createJobFor (Time{0,15}); CHECK (MockJobTicket::isAssociated (job, ticket)); job.triggerJob(); CHECK (MockJob::was_invoked (job)); } /** @test document and verify usage of a mock render job */ void verify_MockJob() { Time nominalTime = lib::test::randTime(); int additionalKey = rani(5000); MockJob mockJob{nominalTime, additionalKey}; CHECK (mockJob.getNominalTime() == nominalTime); CHECK (not MockJob::was_invoked (mockJob)); mockJob.triggerJob(); CHECK (MockJob::was_invoked (mockJob)); CHECK (RealClock::wasRecently (MockJob::invocationTime (mockJob))); CHECK (nominalTime == MockJob::invocationNominalTime (mockJob) ); CHECK (additionalKey == MockJob::invocationAdditionalKey(mockJob)); Time prevInvocation = MockJob::invocationTime (mockJob); mockJob.triggerJob(); CHECK (prevInvocation < MockJob::invocationTime (mockJob)); // invoked again, recorded new invocation time CHECK (nominalTime == MockJob::invocationNominalTime (mockJob) ); // all other Job parameter recorded again unaltered CHECK (additionalKey == MockJob::invocationAdditionalKey(mockJob)); } /** @test document and verify usage of a mock JobTicket for frame dispatch */ void verify_MockJobTicket() { auto frameTime = lib::test::randTime(); // build a render job to do nothing.... Job nopJob = JobTicket::NOP.createJobFor (frameTime); CHECK (INSTANCEOF (vault::gear::NopJobFunctor, static_cast (nopJob.jobClosure))); //////////TICKET #1287 : fix actual interface down to JobFunctor (after removing C structs) CHECK (nopJob.parameter.nominalTime == frameTime); InvocationInstanceID empty; ///////////////////////////////////////////////////////////////////////TICKET #1287 : temporary workaround until we get rid of the C base structs CHECK (lumiera_invokey_eq (&nopJob.parameter.invoKey, &empty)); CHECK (MockJob::isNopJob(nopJob)); // this diagnostic helper checks the same conditions as done here explicitly MockJobTicket mockTicket; CHECK (not mockTicket.empty()); Job mockJob = mockTicket.createJobFor (frameTime); CHECK ( mockTicket.verify_associated (mockJob)); // proof by invocation hash : is indeed backed by this JobTicket CHECK (not mockTicket.verify_associated (nopJob)); // ...while some random other job is not related CHECK (not MockJob::isNopJob(mockJob)); } /** @test document and verify usage of a complete mocked Segmentation * to back frame dispatch * - default constructed: empty Segmentation * - cover the whole axis with one segment * - partition axis and verify the association of generated jobs * - a fully defined segment within an otherwise empty axis * - complex partitioning (using the »split-splice« mechanism */ void verify_MockSegmentation() { Time someTime = lib::test::randTime(); // //-----------------------------------------------------------------/// Empty default Segmentation { MockSegmentation mockSeg; CHECK (1 == mockSeg.size()); JobTicket const& ticket = mockSeg[someTime].jobTicket(0); // just probe JobTicket generated for Model-Port-Nr.0 CHECK (util::isSameObject (ticket, JobTicket::NOP)); } //-----------------------------------------------------------------/// Segmentation with one default segment spanning complete timeline { MockSegmentation mockSegs{MakeRec().genNode()}; CHECK (1 == mockSegs.size()); CHECK (Time::MIN == mockSegs[someTime].start()); CHECK (Time::MAX == mockSegs[someTime].after()); JobTicket& ticket = mockSegs[someTime].jobTicket(0); CHECK (not util::isSameObject (ticket, JobTicket::NOP)); Job someJob = ticket.createJobFor(someTime); // JobTicket uses, but does not check the time given CHECK (someJob.parameter.nominalTime == someTime); CHECK (MockJobTicket::isAssociated (someJob, ticket)); // but the generated Job is linked to the Closure backed by the JobTicket CHECK (not MockJob::was_invoked (someJob)); someJob.triggerJob(); CHECK (MockJob::was_invoked (someJob)); CHECK (RealClock::wasRecently (MockJob::invocationTime (someJob))); CHECK (someTime == MockJob::invocationNominalTime (someJob)); } //-----------------------------------------------------------------/// Segmentation with a segment spanning part of the timeline > 10s { // Marker to verify the job calls back into the right segment int marker = rani(1000); // // Build a Segmentation partitioned at 10s MockSegmentation mockSegs{MakeRec() .attrib ("start", Time{0,10} ,"mark", marker) .genNode()}; CHECK (2 == mockSegs.size()); // since only start-time was given, the SplitSplice-Algo will attach // the new Segment starting at 10s and expand towards +∞, // while the left part of the axis is marked as NOP / empty fixture::Segment const& seg1 = mockSegs[Time::ZERO]; // access anywhere < 10s fixture::Segment const& seg2 = mockSegs[Time{0,20}]; // access anywhere >= 10s CHECK ( util::isSameObject (seg1.jobTicket(0),JobTicket::NOP)); CHECK (not util::isSameObject (seg2.jobTicket(0),JobTicket::NOP));// this one is the active segment Job job = seg2.jobTicket(0).createJobFor(someTime); CHECK (not MockJobTicket::isAssociated (job, seg1.jobTicket(0))); CHECK ( MockJobTicket::isAssociated (job, seg2.jobTicket(0))); CHECK (marker == job.parameter.invoKey.part.a); job.triggerJob(); CHECK (MockJob::was_invoked (job)); CHECK (RealClock::wasRecently (MockJob::invocationTime (job))); CHECK (marker == MockJob::invocationAdditionalKey (job)); // DummyClosure is rigged such as to feed back the seed in `part.a` // and thus we can prove this job really belongs to the marked segment // create another job from the (empty) seg1 job = seg1.jobTicket(0).createJobFor (someTime); InvocationInstanceID empty; /////////////////////////////////////////////////////////////////////TICKET #1287 : temporary workaround until we get rid of the C base structs CHECK (lumiera_invokey_eq (&job.parameter.invoKey, &empty)); // indicates that it's just a placeholder to mark a "NOP"-Job CHECK (seg1.jobTicket(0).empty()); CHECK (seg1.empty()); CHECK (not seg2.empty()); } //-----------------------------------------------------------------/// Segmentation with one delineated segment, and otherwise empty { int marker = rani(1000); // Build Segmentation with one fully defined segment MockSegmentation mockSegs{MakeRec() .attrib ("start", Time{0,10} ,"after", Time{0,20} ,"mark", marker) .genNode()}; CHECK (3 == mockSegs.size()); auto const& [s1,s2,s3] = seqTuple<3> (mockSegs.eachSeg()); CHECK (s1.empty()); CHECK (not s2.empty()); CHECK (s3.empty()); CHECK (isSameObject (s2, mockSegs[Time{0,10}])); CHECK (Time::MIN == s1.start()); CHECK (Time(0,10) == s1.after()); CHECK (Time(0,10) == s2.start()); CHECK (Time(0,20) == s2.after()); CHECK (Time(0,20) == s3.start()); CHECK (Time::MAX == s3.after()); Job job = s2.jobTicket(0).createJobFor(someTime); job.triggerJob(); CHECK (marker == MockJob::invocationAdditionalKey (job)); } //-----------------------------------------------------------------/// Segmentation with several segments built in specific order { // Build Segmentation by partitioning in several steps MockSegmentation mockSegs{MakeRec() .attrib ("start", Time{0,20} // note: inverted segment definition is rectified automatically ,"after", Time{0,10} ,"mark", 1) .genNode() ,MakeRec() .attrib ("after", Time::ZERO ,"mark", 2) .genNode() ,MakeRec() .attrib ("start", Time{FSecs{-5}} ,"mark", 3) .genNode()}; CHECK (5 == mockSegs.size()); auto const& [s1,s2,s3,s4,s5] = seqTuple<5> (mockSegs.eachSeg()); CHECK (not s1.empty()); CHECK (not s2.empty()); CHECK ( s3.empty()); CHECK (not s4.empty()); CHECK ( s5.empty()); CHECK (Time::MIN == s1.start()); // the second added segment has covered the whole negative axis CHECK (-Time(0,5) == s1.after()); // ..up to the partitioning point -5 CHECK (-Time(0,5) == s2.start()); // ...while the rest was taken up by the third added segment CHECK (Time(0, 0) == s2.after()); CHECK (Time(0, 0) == s3.start()); // an empty gap remains between [0 ... 10[ CHECK (Time(0,10) == s3.after()); CHECK (Time(0,10) == s4.start()); // here is the first added segment CHECK (Time(0,20) == s4.after()); CHECK (Time(0,20) == s5.start()); // and the remaining part of the positive axis is empty CHECK (Time::MAX == s5.after()); auto probeKey = [&](Segment const& segment) { if (segment.empty()) return 0; Job job = segment.jobTicket(0).createJobFor(someTime); job.triggerJob(); CHECK (MockJob::was_invoked (job)); CHECK (RealClock::wasRecently (MockJob::invocationTime (job))); return MockJob::invocationAdditionalKey (job); }; CHECK (2 == probeKey(s1)); // verify all generated jobs are wired back to the correct segment CHECK (3 == probeKey(s2)); CHECK (0 == probeKey(s3)); CHECK (1 == probeKey(s4)); CHECK (0 == probeKey(s5)); } } /** * @test build a Segment with additional prerequisites, * resulting in additional JobTickets to explore and * additional prerequisite Jobs to build for each frame. */ void verify_MockPrerequisites() { Time someTime = lib::test::randTime(); //-----------------------------------------------------------------/// one Segment with one additional prerequisite { MockSegmentation mockSegs{MakeRec() .attrib("mark", 11) .scope(MakeRec() .attrib("mark",23) .genNode()) .genNode()}; CHECK (1 == mockSegs.size()); JobTicket& ticket = mockSegs[Time::ZERO].jobTicket(0); // Model-PortNr.0 auto prereq = ticket.getPrerequisites(); CHECK (not isnil (prereq)); JobTicket& preTicket = *prereq; ++prereq; CHECK (isnil (prereq)); Job job1 = preTicket.createJobFor (someTime); Job job2 = ticket.createJobFor (someTime); job1.triggerJob(); job2.triggerJob(); CHECK (23 == MockJob::invocationAdditionalKey (job1)); CHECK (11 == MockJob::invocationAdditionalKey (job2)); } //-----------------------------------------------------------------/// a tree of deep nested prerequisites { MockSegmentation mockSegs{MakeRec() .attrib("mark", 11) .scope(MakeRec() .attrib("mark",33) .scope(MakeRec() .attrib("mark",55) .genNode() ,MakeRec() .attrib("mark",44) .genNode() ) .genNode() ,MakeRec() .attrib("mark",22) .genNode()) .genNode()}; auto start = singleValIterator (mockSegs[Time::ZERO].jobTicket(0)); auto it = lib::explore(start) .expand ([](JobTicket& ticket) { return ticket.getPrerequisites(); }) .expandAll() .transform ([&](JobTicket& ticket) { return ticket.createJobFor(someTime).parameter.invoKey.part.a; }); CHECK (util::join(it,"-") == "11-22-33-44-55"_expect); } // Note: Prerequisites are prepended (LinkedElements) } // thus at each level the last ones appear first /** @test verify setup of a mocked Dispatcher instance * - by default, MockDispatcher generates a single segment * to span the whole Time-axis and with some random yet valid pipeline-ID, * so that a single job ticket can be generated for each port everywhere * - in addition, it is possible to use the same specification language * as for Segmentation to define a more complex (mock)processing graph * @note lacklustre ModelPort handling: processing graph is just duplicated for * each valid model port — not clear yet if we ever need something better... */ void verify_MockDispatcherSetup() { { MockDispatcher dispatcher; // automatically generates some fake connection points... auto [port0,sink0] = dispatcher.getDummyConnection(0); auto [port1,sink1] = dispatcher.getDummyConnection(1); CHECK (port0 != port1); CHECK (sink0 != sink1); CHECK (port0.isValid()); CHECK (port1.isValid()); CHECK (sink0.isValid()); CHECK (sink1.isValid()); CHECK (not ModelPort().isValid()); CHECK (not DataSink().isValid()); CHECK (0 == dispatcher.resolveModelPort(port0)); CHECK (1 == dispatcher.resolveModelPort(port1)); Time frameTime{0,30}; size_t modelPortIDX = 0; Job job0 = dispatcher.createJobFor (modelPortIDX, frameTime); modelPortIDX = 1; Job job1 = dispatcher.createJobFor (modelPortIDX, frameTime); CHECK (dispatcher.verify(job0, port0, sink0)); CHECK (dispatcher.verify(job1, port1, sink1)); } //-----------------------------------------------------------------/// can define multiple Segments { MockDispatcher dispatcher{MakeRec() .attrib("mark", 11) .genNode() ,MakeRec() .attrib("mark", 22) .attrib("start", Time{0,10}) // second segment covers 10s … +Time::MAX .genNode()}; size_t modelPortIDX = 1; Job job0 = dispatcher.createJobFor (modelPortIDX, Time{0,5}); Job job1 = dispatcher.createJobFor (modelPortIDX, Time{0,25}); CHECK (11 == job0.parameter.invoKey.part.a); CHECK (22 == job1.parameter.invoKey.part.a); } } }; /** Register this test class... */ LAUNCHER (MockSupport_test, "unit engine"); }}} // namespace steam::engine::test