LUMIERA.clone/tests/core/steam/engine/mock-support-test.cpp
Ichthyostega 0b9e184fa3 Library: replace usages of rand() in the whole code base
* most usages are drop-in replacements
 * occasionally the other convenience functions can be used
 * verify call-paths from core code to identify usages
 * ensure reseeding for all tests involving some kind of randomness...

__Note__: some tests were not yet converted,
since their usage of randomness is actually not thread-safe.
This problem existed previously, since also `rand()` is not thread safe,
albeit in most cases it is possible to ignore this problem, as
''garbled internal state'' is also somehow „random“
2024-11-13 04:23:46 +01:00

445 lines
21 KiB
C++

/*
MockSupport(Test) - verify test support for fixture and job dispatch
Copyright (C) Lumiera.org
2023, Hermann Vosseler <Ichthyostega@web.de>
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 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<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 = 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