The test suite is now **green** again. The design rework and clean-up of unnecessary test-driven functionality in the `DiagnosticBufferProvider` and `DiagnosticOutputSlot` required to break some very old test-setup, using fake instances of the relevant context facilities. This setup however was used by the newer family of test support facilities, notable the `MockJob`, `MockSegmentation` and `MockDispatcher`. These required a set of `DataSink` handles to be present (even while they are not used currently). This could be fixed easily now, relying on the simplified internal structure of the `OutputSlot`: by re-arranging the types, the implied identity of the `DataSink` handles could be made explicit, allowing for equality comparison. __Remark__: even while `DataSink` with only a single abstracted function, using function notation turned out to be rather confusing at usage site. Naming this function as `lockFrame(frameNo)` clarifies its purpose nicely. That call is effectively abstracted by means of the ''buffer type'', relying on the VTable in the `BufferProvider` implementation.
434 lines
20 KiB
C++
434 lines
20 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);
|
||
CHECK (sink0 != sink1);
|
||
CHECK (port0.isValid());
|
||
CHECK (port1.isValid());
|
||
CHECK (not ModelPort().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
|