Lumiera  0.pre.03
»edit your freedom«
job-planning-pipeline-test.cpp
Go to the documentation of this file.
1 /*
2  JobPlanningPipeline(Test) - structure and setup of the job-planning pipeline
3 
4  Copyright (C) Lumiera.org
5  2023, Hermann Vosseler <Ichthyostega@web.de>
6 
7  This program is free software; you can redistribute it and/or
8  modify it under the terms of the GNU General Public License as
9  published by the Free Software Foundation; either version 2 of
10  the License, or (at your option) any later version.
11 
12  This program is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  GNU General Public License for more details.
16 
17  You should have received a copy of the GNU General Public License
18  along with this program; if not, write to the Free Software
19  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20 
21 * *****************************************************/
22 
28 #include "lib/test/run.hpp"
29 #include "lib/test/test-helper.hpp"
31 
32 #include "lib/iter-explorer.hpp"
33 #include "lib/format-string.hpp"
34 #include "lib/format-util.hpp"
35 #include "lib/util.hpp"
36 
37 
38 using test::Test;
39 using lib::eachNum;
40 using lib::explore;
41 using lib::time::PQuant;
43 using util::isnil;
44 using util::_Fmt;
45 
46 
47 namespace steam {
48 namespace engine{
49 namespace test {
50 
52 
53  namespace { // test fixture...
54 
56  template<class II>
57  inline string
58  materialise (II&& ii)
59  {
60  return util::join (std::forward<II> (ii), "-");
61  }
62 
63  inline PQuant
64  frameGrid (FrameRate fps)
65  {
66  return PQuant (new FixedFrameQuantiser (fps));
67  }
68 
69  } // (End) test fixture
70 
71 
72 
73  /****************************************************************************/
90  {
91 
92  virtual void
93  run (Arg)
94  {
99  integration();
100  }
101 
102 
104  void
106  {
107  Time nominalTime = lib::test::randTime();
108  int additionalKey = rand() % 5000;
109 
110  // (1) mocked render Job
111  MockJob mockJob{nominalTime, additionalKey};
112  mockJob.triggerJob();
113  CHECK (MockJob::was_invoked (mockJob));
114  CHECK (RealClock::wasRecently (MockJob::invocationTime (mockJob)));
115  CHECK (nominalTime == MockJob::invocationNominalTime (mockJob) );
116  CHECK (additionalKey == MockJob::invocationAdditionalKey(mockJob));
117 
118  // (2) Build a mocked Segment at [10s ... 20s[
119  MockSegmentation mockSegs{MakeRec()
120  .attrib ("start", Time{0,10} // start time (inclusive) of the Segment at 10sec
121  ,"after", Time{0,20} // the Segment ends *before* 20sec
122  ,"mark", 123) // marker-ID 123 (can be verified from Job invocation)
123  .scope(MakeRec() // this JobTicket also defines a prerequisite ticket
124  .attrib("mark",555) // using a different marker-ID 555
125  .genNode()
126  )
127  .genNode()};
128  fixture::Segment const& seg = mockSegs[Time{0,15}]; // access anywhere 10s <= t < 20s
129  JobTicket& ticket = seg.jobTicket(0); // get the master-JobTicket from this segment
130  JobTicket& prereq = *(ticket.getPrerequisites()); // pull a prerequisite JobTicket
131 
132  Job jobP = prereq.createJobFor(Time{0,15}); // create an instance of the prerequisites for some time(irrelevant)
133  Job jobM = ticket.createJobFor(Time{0,15}); // ...and an instance of the master job for the same time
134  CHECK (MockJobTicket::isAssociated (jobP, prereq));
135  CHECK (MockJobTicket::isAssociated (jobM, ticket));
136  CHECK (not MockJobTicket::isAssociated (jobP, ticket));
137  CHECK (not MockJobTicket::isAssociated (jobM, prereq));
138 
139  jobP.triggerJob();
140  jobM.triggerJob();
141  CHECK (123 == MockJob::invocationAdditionalKey (jobM)); // verify each job was invoked and linked to the correct spec,
142  CHECK (555 == MockJob::invocationAdditionalKey (jobP)); // indicating that in practice it will activate the proper render node
143 
144  // (3) demonstrate mocked frame dispatcher...
145  MockDispatcher dispatcher; // a complete dispatcher backed by a mock Segment for the whole timeline
146  auto [port1,sink1] = dispatcher.getDummyConnection(1); // also some fake ModelPort and DataSink entries are registered
147  Job jobD = dispatcher.createJobFor (1, Time{0,30});
148  CHECK (dispatcher.verify(jobD, port1, sink1)); // the generated job uses the associated ModelPort and DataSink and JobTicket
149  }
150 
151 
152 
159  void
161  {
162  auto grid = frameGrid(FrameRate::PAL); // one frame ≙ 40ms
163 
164  CHECK (materialise(
165  explore (eachNum(5,13))
166  .transform([&](FrameCnt frameNr)
167  {
168  return grid->timeOf (frameNr);
169  })
170  )
171  == "200ms-240ms-280ms-320ms-360ms-400ms-440ms-480ms"_expect);
172 
173 
174  MockDispatcher dispatcher;
175  play::Timings timings (FrameRate::PAL);
176 
177  CHECK (materialise (
178  dispatcher.forCalcStream(timings)
179  .timeRange(Time{200,0}, Time{500,0}) // Note: end point is exclusive
180  )
181  == "200ms-240ms-280ms-320ms-360ms-400ms-440ms-480ms"_expect);
182  }
183 
184 
188  void
190  {
191  MockDispatcher dispatcher;
192 
193  play::Timings timings (FrameRate::PAL);
194  auto [port,sink] = dispatcher.getDummyConnection(0);
195  auto pipeline = dispatcher.forCalcStream (timings)
196  .timeRange(Time{200,0}, Time{300,0})
197  .pullFrom (port);
198 
199  CHECK (not isnil (pipeline));
200  CHECK (pipeline->isTopLevel()); // is a top-level ticket
201  JobTicket& ticket = pipeline->ticket();
202 
203  Job job = ticket.createJobFor(Time::ZERO); // actual time point is irrelevant here
204  CHECK (dispatcher.verify(job, port, sink));
205  }
206 
207 
219  void
221  {
222  MockDispatcher dispatcher{MakeRec() // define a single segment for the complete time axis
223  .attrib("mark", 11) // the »master job« for each frame has pipeline-ID ≔ 11
224  .scope(MakeRec()
225  .attrib("mark",22) // add a »prerequisite job« marked with pipeline-ID ≔ 22
226  .genNode())
227  .genNode()};
228 
229  play::Timings timings (FrameRate::PAL);
230  auto [port,sink] = dispatcher.getDummyConnection(0);
231  auto pipeline = dispatcher.forCalcStream (timings)
232  .timeRange(Time{200,0}, Time{300,0})
233  .pullFrom (port)
234  .expandPrerequisites();
235 
236  // the first element is identical to previous test
237  CHECK (not isnil (pipeline));
238  CHECK (pipeline->isTopLevel());
239  Job job = pipeline->ticket().createJobFor (Time::ZERO);
240  CHECK (11 == job.parameter.invoKey.part.a);
241 
242  auto visualise = [](auto& pipeline) -> string
243  {
244  Time frame{pipeline.currPoint}; // can access the embedded PipeFrameTick core to get "currPoint" (nominal time)
245  Job job = pipeline->ticket().createJobFor(frame); // looking always at the second element, which is the current JobTicket
246  TimeValue nominalTime{job.parameter.nominalTime}; // job parameter holds the microseconds (gavl_time_t)
247  int32_t mark = job.parameter.invoKey.part.a; // the MockDispatcher places the given "mark" here
248  return _Fmt{"J(%d|%s)"} % mark % nominalTime;
249  };
250  CHECK (visualise(pipeline) == "J(11|200ms)"_expect); // first job in pipeline is at t=200ms and has mark=11 (it's the master Job for this frame)
251 
252  CHECK (materialise (pipeline.transform (visualise))
253  == "J(11|200ms)-J(22|200ms)-J(11|240ms)-J(22|240ms)-J(11|280ms)-J(22|280ms)"_expect);
254  }
255 
256 
257 
266  void
268  {
269  MockDispatcher dispatcher{MakeRec() // start with defining a first segment...
270  .attrib("mark", 11) // the »master job« for each frame has pipeline-ID ≔ 11
271  .attrib("runtime", Duration{Time{10,0}})
272  .scope(MakeRec()
273  .attrib("mark",22) // a »prerequisite job« marked with pipeline-ID ≔ 22
274  .attrib("runtime", Duration{Time{20,0}})
275  .scope(MakeRec()
276  .attrib("mark",33) // further »recursive prerequisite«
277  .attrib("runtime", Duration{Time{30,0}})
278  .genNode())
279  .genNode())
280  .genNode()
281  ,MakeRec() // add a second Segment with different calculation structure
282  .attrib("start", Time{250,0}) // partitioning the timeline at 250ms
283  .attrib("mark", 44)
284  .attrib("runtime", Duration{Time{70,0}})
285  .scope(MakeRec() // on 2nd level we have two independent prerequisites here
286  .attrib("mark", 55) // ...both will line up before the deadline of ticket No.44
287  .attrib("runtime", Duration{Time{60,0}})
288  .genNode()
289  ,MakeRec()
290  .attrib("mark", 66)
291  .attrib("runtime", Duration{Time{50,0}})
292  .genNode())
293  .genNode()};
294 
295 
296  play::Timings timings (FrameRate::PAL, Time{0,1}); // Timings anchored at wall-clock-time ≙ 1s
297  auto [port,sink] = dispatcher.getDummyConnection(0);
298  auto pipeline = dispatcher.forCalcStream (timings)
299  .timeRange(Time{200,0}, Time{300,0})
300  .pullFrom (port)
301  .expandPrerequisites()
302  .feedTo (sink);
303 
304  // this is the complete job-planning pipeline now
305  // and it is wrapped into a Dispatcher::PlanningPipeline front-end
306  CHECK (not isnil (pipeline));
307  CHECK (pipeline->isTopLevel());
308  // Invoking convenience functions on the PlanningPipeline front-end...
309  CHECK (5 == pipeline.currFrameNr());
310  CHECK (not pipeline.isBefore (Time{200,0}));
311  CHECK ( pipeline.isBefore (Time{220,0}));
312 
313  Job job = pipeline.buildJob(); // invoke the JobPlanning to build a Job for the first frame
314  CHECK (Time(200,0) == job.parameter.nominalTime);
315  CHECK (11 == job.parameter.invoKey.part.a);
316 
317  auto visualise = [](auto& pipeline) -> string
318  {
319  Job job = pipeline.buildJob(); // let the JobPlanning construct the »current job«
320  TimeValue nominalTime{job.parameter.nominalTime}; // job parameter holds the microseconds (gavl_time_t)
321  int32_t mark = job.parameter.invoKey.part.a; // the MockDispatcher places the given "mark" here
322  TimeValue deadline{pipeline.determineDeadline()};
323  return _Fmt{"J(%d|%s⧐%s)"}
324  % mark % nominalTime % deadline;
325  };
326  CHECK (visualise(pipeline) == "J(11|200ms⧐1s180ms)"_expect); // first job in pipeline: nominal t=200ms,
327  // .... 10ms engine latency + 10ms job runtime ⟶ deadline 1s180ms
328  CHECK (materialise(
329  explore(move(pipeline))
330  .transform(visualise)
331  )
332  == "J(11|200ms⧐1s180ms)-J(22|200ms⧐1s150ms)-J(33|200ms⧐1s110ms)-" // ... -(10+10) | -(10+10)-(10+20) | -(10+10)-(10+20)-(10+30)
333  "J(11|240ms⧐1s220ms)-J(22|240ms⧐1s190ms)-J(33|240ms⧐1s150ms)-"
334  "J(44|280ms⧐1s200ms)-J(66|280ms⧐1s140ms)-J(55|280ms⧐1s130ms)"_expect); // ... these call into the 2nd Segment
335  }
336  };
337 
338 
340  LAUNCHER (JobPlanningPipeline_test, "unit engine");
341 
342 
343 
344 }}} // namespace steam::engine::test
string materialise(II &&ii)
Diagnostic helper: join all the elements from some given container or iterable.
Mock data structures to support implementation testing of render job planning and frame dispatch...
auto explore(IT &&srcSeq)
start building a IterExplorer by suitably wrapping the given iterable source.
NumIter< INT > eachNum(INT start, INT end)
convenience function to iterate "each number"
Definition: run.hpp:49
Framerate specified as frames per second.
Definition: timevalue.hpp:664
Generic frame timing specification.
Definition: timings.hpp:95
A mocked frame Dispatcher setup without any backing model.
Front-end for printf-style string template interpolation.
engine::JobTicket & jobTicket(size_t portNr) const
Access the JobTicket for this segment and the given portNr.
Definition: segment.hpp:131
Mock setup for a render Job with NO action but built-in diagnostics.
play::test::DummyOutputLink getDummyConnection(uint index)
The faked builder/playback setup provides some preconfigured ModelPort and corresponding DataSink han...
Steam-Layer implementation namespace root.
A front-end for using printf-style formatting.
Lumiera&#39;s internal time value datatype.
Definition: timevalue.hpp:308
Abstract Base Class for all testcases.
Definition: run.hpp:62
PipelineBuilder< PipeFrameTick > forCalcStream(Timings timings)
Start a builder sequence to assemble a job-planning pipeline, backed by this Dispatcher.
Definition: dispatcher.hpp:353
bool verify(Job const &job, ModelPort const &port, play::DataSink const &sink)
Test support: verify the given Job is consistent with this Dispatcher.
Simple test class runner.
Tiny helper functions and shortcuts to be used everywhere Consider this header to be effectively incl...
Mock setup for a complete Segmentation to emulate the structure of the actual fixture, without the need of building a low-level Model.
A collection of frequently used helper functions to support unit testing.
static bool isAssociated(Job const &, JobTicket const &)
convenience shortcut to perform this test on arbitrary JobTicket and Job instances.
For the purpose of building and rendering, the fixture (for each timeline) is partitioned such that e...
Definition: segment.hpp:68
Collection of small helpers and convenience shortcuts for diagnostics & formatting.
Duration is the internal Lumiera time metric.
Definition: timevalue.hpp:477
Job createJobFor(size_t portIDX, TimeValue nominalTime)
Convenience shortcut for tests: JobTicket ⟼ Job.
Definition: dispatcher.hpp:359
Building tree expanding and backtracking evaluations within hierarchical scopes.
Individual frame rendering task, forwarding to a closure.
Definition: job.h:277
basic constant internal time value.
Definition: timevalue.hpp:142
auto getPrerequisites()
Core operation: iterate over the prerequisites, required to carry out a render operation based on thi...
Definition: job-ticket.hpp:164
Simple stand-alone Quantiser implementation based on a constant sized gird.
Definition: quantiser.hpp:144
static const FrameRate PAL
predefined constant for PAL framerate
Definition: timevalue.hpp:680
execution plan for pulling a specific exit node.
Definition: job-ticket.hpp:87
Job createJobFor(Time nominalTime)
Core operation: build a concrete render job based on this blueprint.
Definition: job-ticket.cpp:79