Lumiera  0.pre.03
»edit your freedom«
mock-support-test.cpp
Go to the documentation of this file.
1 /*
2  MockSupport(Test) - verify test support for fixture and job dispatch
3 
4  Copyright (C)
5  2023, Hermann Vosseler <Ichthyostega@web.de>
6 
7   **Lumiera** is free software; you can redistribute it and/or modify it
8   under the terms of the GNU General Public License as published by the
9   Free Software Foundation; either version 2 of the License, or (at your
10   option) any later version. See the file COPYING for further details.
11 
12 * *****************************************************************/
13 
19 #include "lib/test/run.hpp"
20 #include "lib/test/test-helper.hpp"
23 #include "lib/iter-explorer.hpp"
24 #include "lib/util-tuple.hpp"
25 #include "lib/util.hpp"
26 
27 
28 using test::Test;
29 
30 
31 namespace steam {
32 namespace engine{
33 namespace test {
34 
37  using util::isSameObject;
38  using util::seqTuple;
39 
40 
41 
42  /**********************************************************************/
53  class MockSupport_test : public Test
54  {
55 
56  virtual void
57  run (Arg)
58  {
59  seedRand();
60 
61  simpleUsage();
67  }
68 
69 
71  void
73  {
74  // Build a simple Segment at [10s ... 20s[
75  MockSegmentation mockSegs{MakeRec()
76  .attrib ("start", Time{0,10}
77  ,"after", Time{0,20})
78  .genNode()};
79  CHECK (3 == mockSegs.size());
80  fixture::Segment const& seg = mockSegs[Time{0,15}]; // access anywhere 10s <= t < 20s
81 
82  JobTicket& ticket = seg.jobTicket(0);
83 
84  Job job = ticket.createJobFor (Time{0,15});
85  CHECK (MockJobTicket::isAssociated (job, ticket));
86 
87  job.triggerJob();
88  CHECK (MockJob::was_invoked (job));
89  }
90 
91 
92 
93 
95  void
97  {
98  Time nominalTime = lib::test::randTime();
99  int additionalKey = rani(5000);
100  MockJob mockJob{nominalTime, additionalKey};
101  CHECK (mockJob.getNominalTime() == nominalTime);
102  CHECK (not MockJob::was_invoked (mockJob));
103 
104  mockJob.triggerJob();
105  CHECK (MockJob::was_invoked (mockJob));
106  CHECK (RealClock::wasRecently (MockJob::invocationTime (mockJob)));
107  CHECK (nominalTime == MockJob::invocationNominalTime (mockJob) );
108  CHECK (additionalKey == MockJob::invocationAdditionalKey(mockJob));
109 
110  Time prevInvocation = MockJob::invocationTime (mockJob);
111  mockJob.triggerJob();
112  CHECK (prevInvocation < MockJob::invocationTime (mockJob)); // invoked again, recorded new invocation time
113  CHECK (nominalTime == MockJob::invocationNominalTime (mockJob) ); // all other Job parameter recorded again unaltered
114  CHECK (additionalKey == MockJob::invocationAdditionalKey(mockJob));
115  }
116 
117 
119  void
121  {
122  auto frameTime = lib::test::randTime();
123 
124  // build a render job to do nothing....
125  Job nopJob = JobTicket::NOP.createJobFor (frameTime);
126  CHECK (INSTANCEOF (vault::gear::NopJobFunctor, static_cast<JobClosure*> (nopJob.jobClosure)));
127  CHECK (nopJob.parameter.nominalTime == frameTime);
128  InvocationInstanceID empty;
129  CHECK (lumiera_invokey_eq (&nopJob.parameter.invoKey, &empty));
130  CHECK (MockJob::isNopJob(nopJob)); // this diagnostic helper checks the same conditions as done here explicitly
131 
132  MockJobTicket mockTicket;
133  CHECK (not mockTicket.empty());
134  Job mockJob = mockTicket.createJobFor (frameTime);
135  CHECK ( mockTicket.verify_associated (mockJob)); // proof by invocation hash : is indeed backed by this JobTicket
136  CHECK (not mockTicket.verify_associated (nopJob)); // ...while some random other job is not related
137  CHECK (not MockJob::isNopJob(mockJob));
138  }
139 
140 
141 
150  void
152  {
153  Time someTime = lib::test::randTime();
154  //
155  //-----------------------------------------------------------------/// Empty default Segmentation
156  {
157  MockSegmentation mockSeg;
158  CHECK (1 == mockSeg.size());
159  JobTicket const& ticket = mockSeg[someTime].jobTicket(0); // just probe JobTicket generated for Model-Port-Nr.0
160  CHECK (util::isSameObject (ticket, JobTicket::NOP));
161  }
162  //-----------------------------------------------------------------/// Segmentation with one default segment spanning complete timeline
163  {
164  MockSegmentation mockSegs{MakeRec().genNode()};
165  CHECK (1 == mockSegs.size());
166  CHECK (Time::MIN == mockSegs[someTime].start());
167  CHECK (Time::MAX == mockSegs[someTime].after());
168  JobTicket& ticket = mockSegs[someTime].jobTicket(0);
169  CHECK (not util::isSameObject (ticket, JobTicket::NOP));
170 
171  Job someJob = ticket.createJobFor(someTime); // JobTicket uses, but does not check the time given
172  CHECK (someJob.parameter.nominalTime == someTime);
173  CHECK (MockJobTicket::isAssociated (someJob, ticket)); // but the generated Job is linked to the Closure backed by the JobTicket
174  CHECK (not MockJob::was_invoked (someJob));
175 
176  someJob.triggerJob();
177  CHECK (MockJob::was_invoked (someJob));
178  CHECK (RealClock::wasRecently (MockJob::invocationTime (someJob)));
179  CHECK (someTime == MockJob::invocationNominalTime (someJob));
180  }
181  //-----------------------------------------------------------------/// Segmentation with a segment spanning part of the timeline > 10s
182  {
183  // Marker to verify the job calls back into the right segment
184  int marker = rani(1000);
185  //
186  // Build a Segmentation partitioned at 10s
187  MockSegmentation mockSegs{MakeRec()
188  .attrib ("start", Time{0,10}
189  ,"mark", marker)
190  .genNode()};
191  CHECK (2 == mockSegs.size());
192  // since only start-time was given, the SplitSplice-Algo will attach
193  // the new Segment starting at 10s and expand towards +∞,
194  // while the left part of the axis is marked as NOP / empty
195  fixture::Segment const& seg1 = mockSegs[Time::ZERO]; // access anywhere < 10s
196  fixture::Segment const& seg2 = mockSegs[Time{0,20}]; // access anywhere >= 10s
197  CHECK ( util::isSameObject (seg1.jobTicket(0),JobTicket::NOP));
198  CHECK (not util::isSameObject (seg2.jobTicket(0),JobTicket::NOP));// this one is the active segment
199 
200  Job job = seg2.jobTicket(0).createJobFor(someTime);
201  CHECK (not MockJobTicket::isAssociated (job, seg1.jobTicket(0)));
202  CHECK ( MockJobTicket::isAssociated (job, seg2.jobTicket(0)));
203  CHECK (marker == job.parameter.invoKey.part.a);
204 
205  job.triggerJob();
206  CHECK (MockJob::was_invoked (job));
207  CHECK (RealClock::wasRecently (MockJob::invocationTime (job)));
208  CHECK (marker == MockJob::invocationAdditionalKey (job)); // DummyClosure is rigged such as to feed back the seed in `part.a`
209  // and thus we can prove this job really belongs to the marked segment
210  // create another job from the (empty) seg1
211  job = seg1.jobTicket(0).createJobFor (someTime);
212  InvocationInstanceID empty;
213  CHECK (lumiera_invokey_eq (&job.parameter.invoKey, &empty)); // indicates that it's just a placeholder to mark a "NOP"-Job
214  CHECK (seg1.jobTicket(0).empty());
215  CHECK (seg1.empty());
216  CHECK (not seg2.empty());
217  }
218  //-----------------------------------------------------------------/// Segmentation with one delineated segment, and otherwise empty
219  {
220  int marker = rani(1000);
221  // Build Segmentation with one fully defined segment
222  MockSegmentation mockSegs{MakeRec()
223  .attrib ("start", Time{0,10}
224  ,"after", Time{0,20}
225  ,"mark", marker)
226  .genNode()};
227  CHECK (3 == mockSegs.size());
228  auto const& [s1,s2,s3] = seqTuple<3> (mockSegs.eachSeg());
229  CHECK (s1.empty());
230  CHECK (not s2.empty());
231  CHECK (s3.empty());
232  CHECK (isSameObject (s2, mockSegs[Time{0,10}]));
233  CHECK (Time::MIN == s1.start());
234  CHECK (Time(0,10) == s1.after());
235  CHECK (Time(0,10) == s2.start());
236  CHECK (Time(0,20) == s2.after());
237  CHECK (Time(0,20) == s3.start());
238  CHECK (Time::MAX == s3.after());
239 
240  Job job = s2.jobTicket(0).createJobFor(someTime);
241  job.triggerJob();
242  CHECK (marker == MockJob::invocationAdditionalKey (job));
243  }
244  //-----------------------------------------------------------------/// Segmentation with several segments built in specific order
245  {
246  // Build Segmentation by partitioning in several steps
247  MockSegmentation mockSegs{MakeRec()
248  .attrib ("start", Time{0,20} // note: inverted segment definition is rectified automatically
249  ,"after", Time{0,10}
250  ,"mark", 1)
251  .genNode()
252  ,MakeRec()
253  .attrib ("after", Time::ZERO
254  ,"mark", 2)
255  .genNode()
256  ,MakeRec()
257  .attrib ("start", Time{FSecs{-5}}
258  ,"mark", 3)
259  .genNode()};
260 
261  CHECK (5 == mockSegs.size());
262  auto const& [s1,s2,s3,s4,s5] = seqTuple<5> (mockSegs.eachSeg());
263  CHECK (not s1.empty());
264  CHECK (not s2.empty());
265  CHECK ( s3.empty());
266  CHECK (not s4.empty());
267  CHECK ( s5.empty());
268  CHECK (Time::MIN == s1.start()); // the second added segment has covered the whole negative axis
269  CHECK (-Time(0,5) == s1.after()); // ..up to the partitioning point -5
270  CHECK (-Time(0,5) == s2.start()); // ...while the rest was taken up by the third added segment
271  CHECK (Time(0, 0) == s2.after());
272  CHECK (Time(0, 0) == s3.start()); // an empty gap remains between [0 ... 10[
273  CHECK (Time(0,10) == s3.after());
274  CHECK (Time(0,10) == s4.start()); // here is the first added segment
275  CHECK (Time(0,20) == s4.after());
276  CHECK (Time(0,20) == s5.start()); // and the remaining part of the positive axis is empty
277  CHECK (Time::MAX == s5.after());
278 
279  auto probeKey = [&](Segment const& segment)
280  {
281  if (segment.empty()) return 0;
282 
283  Job job = segment.jobTicket(0).createJobFor(someTime);
284  job.triggerJob();
285  CHECK (MockJob::was_invoked (job));
286  CHECK (RealClock::wasRecently (MockJob::invocationTime (job)));
287 
288  return MockJob::invocationAdditionalKey (job);
289  };
290  CHECK (2 == probeKey(s1)); // verify all generated jobs are wired back to the correct segment
291  CHECK (3 == probeKey(s2));
292  CHECK (0 == probeKey(s3));
293  CHECK (1 == probeKey(s4));
294  CHECK (0 == probeKey(s5));
295  }
296  }
297 
298 
299 
305  void
307  {
308  Time someTime = lib::test::randTime();
309  //-----------------------------------------------------------------/// one Segment with one additional prerequisite
310  {
311  MockSegmentation mockSegs{MakeRec()
312  .attrib("mark", 11)
313  .scope(MakeRec()
314  .attrib("mark",23)
315  .genNode())
316  .genNode()};
317  CHECK (1 == mockSegs.size());
318  JobTicket& ticket = mockSegs[Time::ZERO].jobTicket(0); // Model-PortNr.0
319  auto prereq = ticket.getPrerequisites();
320  CHECK (not isnil (prereq));
321 
322  JobTicket& preTicket = *prereq;
323  ++prereq;
324  CHECK (isnil (prereq));
325 
326  Job job1 = preTicket.createJobFor (someTime);
327  Job job2 = ticket.createJobFor (someTime);
328 
329  job1.triggerJob();
330  job2.triggerJob();
331  CHECK (23 == MockJob::invocationAdditionalKey (job1));
332  CHECK (11 == MockJob::invocationAdditionalKey (job2));
333  }
334  //-----------------------------------------------------------------/// a tree of deep nested prerequisites
335  {
336  MockSegmentation mockSegs{MakeRec()
337  .attrib("mark", 11)
338  .scope(MakeRec()
339  .attrib("mark",33)
340  .scope(MakeRec()
341  .attrib("mark",55)
342  .genNode()
343  ,MakeRec()
344  .attrib("mark",44)
345  .genNode()
346  )
347  .genNode()
348  ,MakeRec()
349  .attrib("mark",22)
350  .genNode())
351  .genNode()};
352 
353  auto start = singleValIterator (mockSegs[Time::ZERO].jobTicket(0));
354 
355  auto it = lib::explore(start)
356  .expand ([](JobTicket& ticket)
357  {
358  return ticket.getPrerequisites();
359  })
360  .expandAll()
361  .transform ([&](JobTicket& ticket)
362  {
363  return ticket.createJobFor(someTime).parameter.invoKey.part.a;
364  });
365 
366 
367  CHECK (util::join(it,"-") == "11-22-33-44-55"_expect);
368  } // Note: Prerequisites are prepended (LinkedElements)
369  } // thus at each level the last ones appear first
370 
371 
372 
382  void
384  {
385  {
386  MockDispatcher dispatcher;
387  // automatically generates some fake connection points...
388  auto [port0,sink0] = dispatcher.getDummyConnection(0);
389  auto [port1,sink1] = dispatcher.getDummyConnection(1);
390  CHECK (port0 != port1);
391  CHECK (sink0 != sink1);
392  CHECK (port0.isValid());
393  CHECK (port1.isValid());
394  CHECK (sink0.isValid());
395  CHECK (sink1.isValid());
396  CHECK (not ModelPort().isValid());
397  CHECK (not DataSink().isValid());
398 
399  CHECK (0 == dispatcher.resolveModelPort(port0));
400  CHECK (1 == dispatcher.resolveModelPort(port1));
401 
402  Time frameTime{0,30};
403  size_t modelPortIDX = 0;
404  Job job0 = dispatcher.createJobFor (modelPortIDX, frameTime);
405  modelPortIDX = 1;
406  Job job1 = dispatcher.createJobFor (modelPortIDX, frameTime);
407  CHECK (dispatcher.verify(job0, port0, sink0));
408  CHECK (dispatcher.verify(job1, port1, sink1));
409  }
410  //-----------------------------------------------------------------/// can define multiple Segments
411  {
412  MockDispatcher dispatcher{MakeRec()
413  .attrib("mark", 11)
414  .genNode()
415  ,MakeRec()
416  .attrib("mark", 22)
417  .attrib("start", Time{0,10}) // second segment covers 10s … +Time::MAX
418  .genNode()};
419 
420  size_t modelPortIDX = 1;
421  Job job0 = dispatcher.createJobFor (modelPortIDX, Time{0,5});
422  Job job1 = dispatcher.createJobFor (modelPortIDX, Time{0,25});
423 
424  CHECK (11 == job0.parameter.invoKey.part.a);
425  CHECK (22 == job1.parameter.invoKey.part.a);
426  }
427  }
428  };
429 
430 
432  LAUNCHER (MockSupport_test, "unit engine");
433 
434 
435 
436 }}} // namespace steam::engine::test
size_t resolveModelPort(ModelPort modelPort) override
translate a generic ModelPort spec into the specific index number applicable at the Timeline referred...
Mock data structures to support implementation testing of render job planning and frame dispatch...
Generic implementation of a JobFunctor to perform no calculations.
auto explore(IT &&srcSeq)
start building a IterExplorer by suitably wrapping the given iterable source.
Mock setup for a JobTicket to generate dummy render Job invocations.
Definition: run.hpp:40
bool verify_associated(Job const &) const
verify the given job instance was actually generated from this JobTicket.
#define INSTANCEOF(CLASS, EXPR)
shortcut for subclass test, intended for assertions only.
Definition: util.hpp:514
A mocked frame Dispatcher setup without any backing model.
int rani(uint bound=_iBOUND())
Definition: random.hpp:135
engine::JobTicket & jobTicket(size_t portNr) const
Access the JobTicket for this segment and the given portNr.
Definition: segment.hpp:122
Mock setup for a render Job with NO action but built-in diagnostics.
denotes an opened connection ready to receive media data for output.
play::test::DummyOutputLink getDummyConnection(uint index)
The faked builder/playback setup provides some preconfigured ModelPort and corresponding DataSink han...
Steam-Layer implementation namespace root.
static JobTicket NOP
special »do nothing« JobTicket marker
Definition: job-ticket.hpp:127
Lumiera&#39;s internal time value datatype.
Definition: timevalue.hpp:299
Abstract Base Class for all testcases.
Definition: run.hpp:53
bool verify(Job const &job, ModelPort const &port, play::DataSink const &sink)
Test support: verify the given Job is consistent with this Dispatcher.
Simplistic test class runner.
void seedRand()
draw a new random seed from a common nucleus, and re-seed the default-Gen.
Definition: suite.cpp:211
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.
auto singleValIterator(VAL &&something)
Build a SingleValIter: convenience free function shortcut, to pick up just any value and wrap it as L...
Definition: itertools.hpp:655
static bool isAssociated(Job const &, JobTicket const &)
convenience shortcut to perform this test on arbitrary JobTicket and Job instances.
Handle designating a point within the model, where actually output data can be pulled.
Definition: model-port.hpp:95
opaque ID attached to each individual job invocation.
Definition: job.h:103
For the purpose of building and rendering, the fixture (for each timeline) is partitioned such that e...
Definition: segment.hpp:59
static bool isNopJob(Job const &)
Job createJobFor(size_t portIDX, TimeValue nominalTime)
Convenience shortcut for tests: JobTicket ⟼ Job.
Definition: dispatcher.hpp:350
Building tree expanding and backtracking evaluations within hierarchical scopes.
Individual frame rendering task, forwarding to a closure.
Definition: job.h:268
static const Time MAX
Definition: timevalue.hpp:309
auto getPrerequisites()
Core operation: iterate over the prerequisites, required to carry out a render operation based on thi...
Definition: job-ticket.hpp:155
execution plan for pulling a specific exit node.
Definition: job-ticket.hpp:78
Stub/Test implementation of the JobFunctor interface for a render job to do nothing at all ...
Job createJobFor(Time nominalTime)
Core operation: build a concrete render job based on this blueprint.
Definition: job-ticket.cpp:71
bool isSameObject(A const &a, B const &b)
compare plain object identity, based directly on the referee&#39;s memory identities. ...
Definition: util.hpp:421