LUMIERA.clone/tests/core/steam/engine/mock-support-test.cpp
Ichthyostega 806db414dd Copyright: clarify and simplify the file headers
* 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.
2024-11-17 23:42:55 +01:00

436 lines
21 KiB
C++
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
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