LUMIERA.clone/tests/core/steam/engine/mock-support-test.cpp
Ichthyostega f25ec2f5ef Dispatcher-Pipeline: switch JobTicket creation to use ExitNode directly
Up to now, a draft/mock implementation was used, relying on a »spec tuple«,
which was fabricated by MockJobTicket. But with the introduction of
NodeGraphAttachment, the MockSequence now generates a nested ExitNode structure,
and thus the JobTicket will be created through the "real" ctor, and
no longer via MockJobTicket.

Thus it is possible to skip this whole interspersed »spec tuple«,
since ExitNode *is* already this aggregated / abstracted Spec
2023-06-10 04:52:40 +02:00

388 lines
18 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/engine/nop-job-functor.hpp"
#include "vault/engine/dummy-job.hpp"
#include "lib/iter-tree-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 vault::engine::DummyJob;
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
* @see JobPlanningSetup_test
* @see Dispatcher
* @see vault::engine::Job
* @see steam::fixture::Segmentation
*/
class MockSupport_test : public Test
{
virtual void
run (Arg)
{
simpleUsage();
verify_MockJob();
verify_MockJobTicket();
verify_MockSegmentation();
verify_MockPrerequisites();
}
/** @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 const& ticket = seg.jobTicket(0);
FrameCoord coord;
coord.absoluteNominalTime = Time(0,15);
Job job = ticket.createJobFor(coord);
CHECK (MockJobTicket::isAssociated (job, ticket));
job.triggerJob();
CHECK (DummyJob::was_invoked (job));
}
/** @test document and verify usage of a mock render job */
void
verify_MockJob()
{
Time nominalTime = lib::test::randTime();
int additionalKey = rand() % 5000;
Job mockJob = DummyJob::build (nominalTime, additionalKey);
CHECK (mockJob.getNominalTime() == nominalTime);
CHECK (not DummyJob::was_invoked (mockJob));
mockJob.triggerJob();
CHECK (DummyJob::was_invoked (mockJob));
CHECK (RealClock::wasRecently (DummyJob::invocationTime (mockJob)));
CHECK (nominalTime == DummyJob::invocationNominalTime (mockJob) );
CHECK (additionalKey == DummyJob::invocationAdditionalKey(mockJob));
Time prevInvocation = DummyJob::invocationTime (mockJob);
mockJob.triggerJob();
CHECK (prevInvocation < DummyJob::invocationTime (mockJob)); // invoked again, recorded new invocation time
CHECK (nominalTime == DummyJob::invocationNominalTime (mockJob) ); // all other Job parameter recorded again unaltered
CHECK (additionalKey == DummyJob::invocationAdditionalKey(mockJob));
}
/** @test document and verify usage of a mock JobTicket for frame dispatch */
void
verify_MockJobTicket()
{
FrameCoord coord;
coord.absoluteNominalTime = lib::test::randTime();
// build a render job to do nothing....
Job nopJob = JobTicket::NOP.createJobFor(coord);
CHECK (INSTANCEOF (vault::engine::NopJobFunctor, static_cast<JobClosure*> (nopJob.jobClosure))); //////////TICKET #1287 : fix actual interface down to JobFunctor (after removing C structs)
CHECK (nopJob.parameter.nominalTime == coord.absoluteNominalTime);
InvocationInstanceID empty; ///////////////////////////////////////////////////////////////////////TICKET #1287 : temporary workaround until we get rid of the C base structs
CHECK (lumiera_invokey_eq (&nopJob.parameter.invoKey, &empty));
MockJobTicket mockTicket;
CHECK (mockTicket.discoverPrerequisites().empty());
Job mockJob = mockTicket.createJobFor (coord);
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
}
/** @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()
{
FrameCoord coord;
Time someTime = lib::test::randTime();
coord.absoluteNominalTime = someTime;
//
//-----------------------------------------------------------------/// 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 const& ticket = mockSegs[someTime].jobTicket(0);
CHECK (not util::isSameObject (ticket, JobTicket::NOP));
Job someJob = ticket.createJobFor(coord); // JobTicket uses, but does not check the time given in FrameCoord
CHECK (someJob.parameter.nominalTime == _raw(coord.absoluteNominalTime));
CHECK (MockJobTicket::isAssociated (someJob, ticket)); // but the generated Job is linked to the Closure backed by the JobTicket
CHECK (not DummyJob::was_invoked (someJob));
someJob.triggerJob();
CHECK (DummyJob::was_invoked (someJob));
CHECK (RealClock::wasRecently (DummyJob::invocationTime (someJob)));
CHECK (someTime == DummyJob::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 = rand() % 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(coord);
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 (DummyJob::was_invoked (job));
CHECK (RealClock::wasRecently (DummyJob::invocationTime (job)));
CHECK (marker == DummyJob::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 (coord);
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 = rand() % 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(coord);
job.triggerJob();
CHECK (marker == DummyJob::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(coord);
job.triggerJob();
CHECK (DummyJob::was_invoked (job));
CHECK (RealClock::wasRecently (DummyJob::invocationTime (job)));
return DummyJob::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()
{
FrameCoord coord;
//-----------------------------------------------------------------/// one Segment with one additional prerequisite
{
MockSegmentation mockSegs{MakeRec()
.attrib("mark", 11)
.scope(MakeRec()
.attrib("mark",23)
.genNode())
.genNode()};
CHECK (1 == mockSegs.size());
JobTicket const& ticket = mockSegs[Time::ZERO].jobTicket(0); // Model-PortNr.0
auto prereq = ticket.getPrerequisites();
CHECK (not isnil (prereq));
JobTicket const& preTicket = *prereq;
++prereq;
CHECK (isnil (prereq));
Job job1 = preTicket.createJobFor (coord);
Job job2 = ticket.createJobFor (coord);
job1.triggerJob();
job2.triggerJob();
CHECK (23 == DummyJob::invocationAdditionalKey (job1));
CHECK (11 == DummyJob::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 const& ticket)
{
return ticket.getPrerequisites();
})
.expandAll()
.transform ([&](JobTicket const& ticket)
{
return ticket.createJobFor(coord).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
};
/** Register this test class... */
LAUNCHER (MockSupport_test, "unit engine");
}}} // namespace steam::engine::test