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.
438 lines
15 KiB
C++
438 lines
15 KiB
C++
/*
|
||
MOCK-DISPATCHER.hpp - test scaffolding to verify render job planning and 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-dispatcher.hpp
|
||
** Mock data structures to support implementation testing of render job
|
||
** planning and frame dispatch. This specifically rigged test setup allows to
|
||
** investigate and verify designated functionality in isolation, without backing
|
||
** by the actual render engine and low-level-Model implementation.
|
||
** - a MockJob is a render Job, wired to a DummyFunctor, which does nothing,
|
||
** but records any invocation into an internal diagnostics Map.
|
||
** - MockJobTicket is a builder / adapter on top of the actual steam::engine::JobTicket,
|
||
** allowing to generate simple JobTicket instances with an embedded ExitNode and
|
||
** a (configurable) pipelineID. From this setup, »mock jobs« can be generated,
|
||
** which use the MockJob functor and thus record any invocation without performing
|
||
** actual work. The internal connection to the MockJobTicket can then be verified.
|
||
** - MockSegmentation is a mocked variant of the »Segmentation« datastructure,
|
||
** which forms the backbone of the Fixture and is the top-level attachment
|
||
** point for the »low-level-Model« (the render nodes network). It can be
|
||
** configured with a test specification of ExitNode(s) defined by a
|
||
** GenNode tree, and defining Segments of the timeline and prerequisites.
|
||
** - finally, the MockDispatcher combines all these facilities to emulate
|
||
** frame dispatch from the Fixture without actually using any data model.
|
||
** Similar to MockSegmentation, a GenNode-based specification is used.
|
||
**
|
||
** @remark in spring 2023, this setup was created as a means to define and
|
||
** then build the actual implementation of frame dispatch and scheduling.
|
||
** @see MockSupport_test
|
||
*/
|
||
|
||
|
||
#ifndef STEAM_ENGINE_TEST_MOCK_DISPATCHER_H
|
||
#define STEAM_ENGINE_TEST_MOCK_DISPATCHER_H
|
||
|
||
|
||
#include "test/test-helper.hpp"
|
||
#include "steam/play/dummy-play-connection.hpp"
|
||
#include "steam/fixture/node-graph-attachment.hpp"
|
||
#include "steam/fixture/segmentation.hpp"
|
||
#include "steam/mobject/model-port.hpp"
|
||
#include "steam/engine/dispatcher.hpp"
|
||
#include "steam/engine/job-ticket.hpp"
|
||
#include "vault/out/output-slot.hpp"
|
||
#include "vault/gear/job.h"
|
||
#include "vault/real-clock.hpp"
|
||
#include "lib/allocator-handle.hpp"
|
||
#include "lib/time/timevalue.hpp"
|
||
#include "lib/diff/gen-node.hpp"
|
||
#include "lib/linked-elements.hpp"
|
||
#include "lib/itertools.hpp"
|
||
#include "lib/depend.hpp"
|
||
#include "lib/util.hpp"
|
||
|
||
#include <tuple>
|
||
#include <map>
|
||
|
||
|
||
namespace steam {
|
||
namespace engine {
|
||
namespace test {
|
||
|
||
using std::make_tuple;
|
||
using lib::diff::GenNode;
|
||
using lib::diff::MakeRec;
|
||
using lib::time::TimeValue;
|
||
using lib::time::Time;
|
||
using lib::HashVal;
|
||
using lib::ranHash;
|
||
using lib::rani;
|
||
using util::isnil;
|
||
using util::isSameObject;
|
||
using fixture::Segmentation;
|
||
using vault::RealClock;
|
||
using vault::gear::Job;
|
||
using vault::gear::JobClosure;
|
||
|
||
using DataSink = vault::out::OutputSlot::DataSink;
|
||
|
||
|
||
|
||
|
||
|
||
/**
|
||
* Mock setup for a render Job with NO action but built-in diagnostics.
|
||
* Each invocation of such a MockJob will be logged internally
|
||
* and can be investigated and verified afterwards.
|
||
*/
|
||
class MockJob
|
||
: public Job
|
||
{
|
||
static Job build(); ///< uses random job definition values
|
||
static Job build (Time nominalTime, int additionalKey);
|
||
|
||
public:
|
||
MockJob()
|
||
: Job{build()}
|
||
{ }
|
||
|
||
MockJob (Time nominalTime, int additionalKey)
|
||
: Job{build (nominalTime,additionalKey)}
|
||
{ }
|
||
|
||
|
||
static bool was_invoked (Job const& job);
|
||
static Time invocationTime (Job const& job);
|
||
static Time invocationNominalTime (Job const& job);
|
||
static int invocationAdditionalKey (Job const& job);
|
||
|
||
static bool isNopJob (Job const&);
|
||
static JobClosure& getFunctor();
|
||
};
|
||
|
||
|
||
|
||
|
||
|
||
|
||
/**
|
||
* Mock setup for a JobTicket to generate dummy render Job invocations.
|
||
* Implemented as subclass, it provides a specification DSL for tests,
|
||
* and is able to probe some otherwise opaque internals of JobTicket.
|
||
* Beyond that, MockJobTicket has the same storage size; and behaves
|
||
* like the regular JobTicket after construction -- but any Job
|
||
* created by JobTicket::createJobFor(nominalTime) will be wired
|
||
* with the MockJob functor and can thus be related back to
|
||
* the test specification setup.
|
||
* @see JobPlanningPipeline_test
|
||
* @see DispatcherInterface_test
|
||
*/
|
||
class MockJobTicket
|
||
: private lib::AllocatorHandle<JobTicket>
|
||
, public JobTicket
|
||
{
|
||
auto&
|
||
allocator()
|
||
{
|
||
return static_cast<lib::AllocatorHandle<JobTicket>&> (*this);
|
||
}
|
||
|
||
/** provide a test specification wired to MockJob */
|
||
static ExitNode
|
||
defineSimpleSpec (HashVal seed =ranHash())
|
||
{
|
||
return ExitNode{seed, DUMMY_JOB_RUNTIME
|
||
,ExitNodes{}
|
||
,& MockJob::getFunctor()};
|
||
}
|
||
|
||
public:
|
||
MockJobTicket()
|
||
: JobTicket{defineSimpleSpec(), allocator()}
|
||
{ }
|
||
|
||
MockJobTicket (HashVal seed)
|
||
: JobTicket{defineSimpleSpec (seed), allocator()}
|
||
{ }
|
||
|
||
|
||
/* ===== Diagnostics ===== */
|
||
|
||
bool verify_associated (Job const&) const;
|
||
static bool isAssociated (Job const&, JobTicket const&);
|
||
};
|
||
|
||
|
||
|
||
/**
|
||
* Mock setup for a complete Segmentation to emulate the structure
|
||
* of the actual fixture, without the need of building a low-level Model.
|
||
* MockSegmentation instances can be instantiated directly within the
|
||
* test, by passing a test specification in »GenNode« notation to the
|
||
* constructor. This specification defines the segments to create
|
||
* and allows to associate a marker number, which can later be
|
||
* verified from the actual DummyClosure invocation.
|
||
* - the ctor accepts a sequence of GenNode elements,
|
||
* each corresponding to a segment to created
|
||
* - optionally, attributes "start" and "after" can be defined
|
||
* to provide the lib::time::Time values of segment start/end
|
||
* - in addition, optionally a "mark" attribute can be defined;
|
||
* the given integer number will be "hidden" in the job instance
|
||
* hash, and can be [verified](\ref MockJob::invocationAdditionalKey)
|
||
* - the _scope_ of each top-level GenNode may hold a sequence of
|
||
* nested nodes corresponding to _prerequisite_ JobTicket instances
|
||
* - these can in turn hold further nested prerequisites, and so on
|
||
* @see MockSetup_test::verify_MockSegmentation
|
||
*/
|
||
class MockSegmentation
|
||
: public Segmentation
|
||
{
|
||
|
||
public:
|
||
MockSegmentation()
|
||
: Segmentation{}
|
||
{ }
|
||
|
||
MockSegmentation (std::initializer_list<GenNode> specs)
|
||
: MockSegmentation{}
|
||
{
|
||
for (auto& spec : specs)
|
||
{
|
||
auto start = spec.retrieveAttribute<Time> ("start");
|
||
auto after = spec.retrieveAttribute<Time> ("after");
|
||
Segmentation::splitSplice (start, after
|
||
,ExitNodes{buildExitNodeFromSpec (spec)}
|
||
);
|
||
}
|
||
}
|
||
|
||
|
||
ExitNode
|
||
buildExitNodeFromSpec (GenNode const& spec)
|
||
{
|
||
return ExitNode{buildSeed (spec)
|
||
,buildRuntime (spec)
|
||
,buildPrerequisites (spec)
|
||
,& MockJob::getFunctor()};
|
||
}
|
||
|
||
/** @internal helper for MockDispatcher */
|
||
void duplicateExitNodeSpec (uint times);
|
||
|
||
|
||
private: /* ======== Implementation: build fake ExitNodes from test specification ==== */
|
||
|
||
HashVal
|
||
buildSeed (GenNode const& spec)
|
||
{
|
||
auto seed = spec.retrieveAttribute<int> ("mark");
|
||
return seed? HashVal(*seed) : HashVal(1 +rani(1000));
|
||
}
|
||
|
||
Duration
|
||
buildRuntime (GenNode const& spec)
|
||
{
|
||
auto runtime = spec.retrieveAttribute<Duration> ("runtime");
|
||
return runtime? *runtime : DUMMY_JOB_RUNTIME;
|
||
}
|
||
|
||
ExitNodes
|
||
buildPrerequisites (GenNode const& spec)
|
||
{
|
||
ExitNodes prerequisites;
|
||
for (auto& child : spec.getChildren())
|
||
prerequisites.emplace_back (
|
||
buildExitNodeFromSpec (child));
|
||
return prerequisites;
|
||
}
|
||
};
|
||
|
||
|
||
|
||
|
||
/**
|
||
* This is some trickery to allow handling of multiple ModelPort(s) in MockDispatcher;
|
||
* actually the code using this mock setup does not need any elaborate differentiation
|
||
* of the ExitNodes structure per port, thus the first entry of the existing configuration
|
||
* is just duplicated for the given number of further ModelPorts.
|
||
* @warning this manipulation must be done prior to generating any JobTicket
|
||
*/
|
||
inline void
|
||
MockSegmentation::duplicateExitNodeSpec (uint times)
|
||
{
|
||
using Spec = fixture::NodeGraphAttachment;
|
||
|
||
Segmentation::adaptSpecification ([times](Spec const& spec)
|
||
{
|
||
return Spec{ExitNodes(times, spec[0])};
|
||
}); // vector with <times> copies of spec[0]
|
||
} // (Warning: use parens, not braces for this ctor...)
|
||
|
||
|
||
/**
|
||
* verify the given job instance was actually generated from this JobTicket.
|
||
* @remark this test support function relies on some specific rigging,
|
||
* which typically is prepared by setup of a MockJobTicket.
|
||
*/
|
||
inline bool
|
||
MockJobTicket::verify_associated (Job const& job) const
|
||
{
|
||
JobFunctor& functor = dynamic_cast<JobFunctor&> (static_cast<JobClosure&> (*job.jobClosure));
|
||
Time nominalTime {TimeValue{job.parameter.nominalTime}};
|
||
InvocationInstanceID const& invoKey = job.parameter.invoKey;
|
||
return this->isValid()
|
||
and this->verifyInstance(functor, invoKey, nominalTime);
|
||
}
|
||
|
||
/**
|
||
* convenience shortcut to perform [this test](\ref MockJobTicket::verify_associated)
|
||
* on arbitrary JobTicket and Job instances.
|
||
* @warning a positive test result however relies on some casting trickery and there is no
|
||
* guarantee this test works if the JobTicket was not created from this mock framework.
|
||
*/
|
||
inline bool
|
||
MockJobTicket::isAssociated (Job const& job, JobTicket const& ticket)
|
||
{ // should work always, since storage is the same
|
||
MockJobTicket const& backdoor = static_cast<MockJobTicket const&> (ticket);
|
||
return backdoor.verify_associated (job);
|
||
}
|
||
|
||
|
||
|
||
namespace { // used internally by MockDispatcher....
|
||
using play::test::ModelPorts;
|
||
using play::test::PlayTestFrames_Strategy;
|
||
|
||
using DummyPlaybackSetup = play::test::DummyPlayConnection<PlayTestFrames_Strategy>;
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* A mocked frame Dispatcher setup without any backing model.
|
||
* Instantiating such a MockDispatcher will automatically create some fake
|
||
* model structures and some ModelPort and DisplaySink handles (and thereby
|
||
* push aside and shadow any existing ModelPort registry).
|
||
*
|
||
* The configuration is similar to MockSegmentation, using a test spec
|
||
* given as GenNode-tree to define Segments of the timeline and possibly
|
||
* pipeline-IDs and prerequisites. One notable difference is that here
|
||
* the default ctor always creates a single Segment covering the whole
|
||
* time axis, and that the ExitNode specification is automatically
|
||
* duplicated for all faked ModelPort(s).
|
||
*/
|
||
class MockDispatcher
|
||
: public Dispatcher
|
||
{
|
||
|
||
DummyPlaybackSetup dummySetup_;
|
||
MockSegmentation mockSeg_;
|
||
|
||
using PortIdxMap = std::map<ModelPort, size_t>;
|
||
|
||
const PortIdxMap portIdx_;
|
||
|
||
public:
|
||
/* == mock implementation of the Dispatcher interface == */
|
||
|
||
size_t
|
||
resolveModelPort (ModelPort modelPort) override
|
||
{
|
||
auto entry = portIdx_.find(modelPort);
|
||
if (entry == portIdx_.end())
|
||
throw error::Logic{"Invalid ModelPort for this Dispatcher"};
|
||
else
|
||
return entry->second;
|
||
}
|
||
|
||
|
||
JobTicket&
|
||
getJobTicketFor (size_t portIDX, TimeValue nominalTime) override
|
||
{
|
||
auto& seg = mockSeg_[nominalTime];
|
||
return seg.jobTicket(portIDX);
|
||
}
|
||
|
||
|
||
public:
|
||
MockDispatcher()
|
||
: dummySetup_{}
|
||
, mockSeg_{MakeRec().genNode()} // Node: generate a single active Segment to cover all
|
||
, portIdx_{buildPortIndex()}
|
||
{
|
||
mockSeg_.duplicateExitNodeSpec(portIdx_.size());
|
||
}
|
||
|
||
MockDispatcher (std::initializer_list<GenNode> specs)
|
||
: dummySetup_{}
|
||
, mockSeg_(specs)
|
||
, portIdx_{buildPortIndex()}
|
||
{
|
||
mockSeg_.duplicateExitNodeSpec(portIdx_.size());
|
||
}
|
||
|
||
|
||
ModelPort
|
||
provideMockModelPort()
|
||
{
|
||
ModelPorts mockModelPorts = dummySetup_.getAllModelPorts();
|
||
return *mockModelPorts; // using just the first dummy port
|
||
}
|
||
|
||
/**
|
||
* The faked builder/playback setup provides some preconfigured ModelPort and
|
||
* corresponding DataSink handles. These are stored into a dummy registry and only available
|
||
* during the lifetime of the DummyPlaybackSetup instance.
|
||
* @param index number of the distinct port / connection
|
||
* @return a `std::pair<ModelPort,DataSink>`
|
||
* @warning as of 5/2023, there are two preconfigured "slots",
|
||
* and they are not usable in any way other then referring to their identity
|
||
*/
|
||
play::test::DummyOutputLink
|
||
getDummyConnection(uint index)
|
||
{
|
||
return dummySetup_.getModelPort (index);
|
||
}
|
||
|
||
/**
|
||
* Test support: verify the given Job is consistent with this Dispatcher.
|
||
*/
|
||
bool verify(Job const& job, ModelPort const& port, DataSink const& sink)
|
||
{
|
||
if (not dummySetup_.isSupported (port, sink)) return false;
|
||
|
||
TimeValue nominalTime{job.parameter.nominalTime};
|
||
size_t portIDX = resolveModelPort (port);
|
||
JobTicket& ticket = getJobTicketFor (portIDX, nominalTime);
|
||
return isnil (ticket)? MockJob::isNopJob (job)
|
||
: MockJobTicket::isAssociated (job, ticket);
|
||
}
|
||
|
||
private:
|
||
PortIdxMap
|
||
buildPortIndex()
|
||
{
|
||
PortIdxMap newIndex;
|
||
uint i{0};
|
||
for (auto it=dummySetup_.getAllModelPorts()
|
||
; bool{it}
|
||
; ++it, ++i
|
||
)
|
||
newIndex[*it] = i;
|
||
|
||
return newIndex;
|
||
}
|
||
};
|
||
|
||
|
||
}}} // namespace steam::engine::test
|
||
#endif /*STEAM_ENGINE_TEST_MOCK_DISPATCHER_H*/
|