The detailed analysis of corner cases indicated a lot of hidden dangers when stretching the `IterExplorer` design beyond its limits. By deliberate choice, this design was kept open so that advanced cohesive implementation structures can be built. It is even possible ''and tolerated'' to rely on ''references'' to some degree -- assuming however that the referred memory location remains stable. As it turned out, these basic assumptions can be broken by a combination of read-ahead processing (as is necessary for grouping and filtering), together with adaptor objects embedded as value, especially when the resulting pipeline object is then moved or copied during evaluation. While my general attitude towards such extremely clever implementation constructs is "don't shoot yourself into your foot unless you need to" -- it seems prudent to clean-up some crappy code from the first attempts towards an iterator pipeline -- notably some of the »Itertools«.
445 lines
22 KiB
C++
445 lines
22 KiB
C++
/*
|
||
MockSupport(Test) - verify test support for fixture and job dispatch
|
||
|
||
Copyright (C)
|
||
2023, Hermann Vosseler <Ichthyostega@web.de>
|
||
|
||
**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 "test/run.hpp"
|
||
#include "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 ::test::randTime;
|
||
using steam::fixture::Segment;
|
||
using lib::singletonIter;
|
||
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{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()
|
||
{
|
||
Time frameTime{randTime()};
|
||
|
||
// build a render job to do nothing....
|
||
Job nopJob = JobTicket::NOP.createJobFor (frameTime);
|
||
CHECK (INSTANCEOF (vault::gear::NopJobFunctor, static_cast<JobClosure*> (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{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{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 = singletonIter (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);
|
||
UNIMPLEMENTED ("Identity of a DataSink");
|
||
#if false //////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : disabled code to allow refactoring of OutputSlot
|
||
CHECK (sink0 != sink1);
|
||
#endif /////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : (end) disabled code
|
||
CHECK (port0.isValid());
|
||
CHECK (port1.isValid());
|
||
#if false //////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : disabled code to allow refactoring of OutputSlot
|
||
/////////////////////////OOO seemingly this is no longer possible (and unnecessary), since DataSink participates in the ref-count now
|
||
CHECK (sink0.isValid());
|
||
CHECK (sink1.isValid());
|
||
#endif /////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : (end) disabled code
|
||
CHECK (not ModelPort().isValid());
|
||
#if false //////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : disabled code to allow refactoring of OutputSlot
|
||
CHECK (not DataSink().isValid());
|
||
#endif /////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1410 : (end) disabled code
|
||
|
||
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
|