* Lumiera source code always was copyrighted by individual contributors * there is no entity "Lumiera.org" which holds any copyrights * Lumiera source code is provided under the GPL Version 2+ == Explanations == Lumiera as a whole is distributed under Copyleft, GNU General Public License Version 2 or above. For this to become legally effective, the ''File COPYING in the root directory is sufficient.'' The licensing header in each file is not strictly necessary, yet considered good practice; attaching a licence notice increases the likeliness that this information is retained in case someone extracts individual code files. However, it is not by the presence of some text, that legally binding licensing terms become effective; rather the fact matters that a given piece of code was provably copyrighted and published under a license. Even reformatting the code, renaming some variables or deleting parts of the code will not alter this legal situation, but rather creates a derivative work, which is likewise covered by the GPL! The most relevant information in the file header is the notice regarding the time of the first individual copyright claim. By virtue of this initial copyright, the first author is entitled to choose the terms of licensing. All further modifications are permitted and covered by the License. The specific wording or format of the copyright header is not legally relevant, as long as the intention to publish under the GPL remains clear. The extended wording was based on a recommendation by the FSF. It can be shortened, because the full terms of the license are provided alongside the distribution, in the file COPYING.
436 lines
21 KiB
C++
436 lines
21 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 "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
|