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) 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"
32 #include "lib/iter-explorer.hpp"
33 #include "lib/util-tuple.hpp"
34 #include "lib/util.hpp"
35 
36 
37 using test::Test;
38 
39 
40 namespace steam {
41 namespace engine{
42 namespace test {
43 
46  using util::isSameObject;
47  using util::seqTuple;
48 
49 
50 
51  /**********************************************************************/
62  class MockSupport_test : public Test
63  {
64 
65  virtual void
66  run (Arg)
67  {
68  simpleUsage();
74  }
75 
76 
78  void
80  {
81  // Build a simple Segment at [10s ... 20s[
82  MockSegmentation mockSegs{MakeRec()
83  .attrib ("start", Time{0,10}
84  ,"after", Time{0,20})
85  .genNode()};
86  CHECK (3 == mockSegs.size());
87  fixture::Segment const& seg = mockSegs[Time{0,15}]; // access anywhere 10s <= t < 20s
88 
89  JobTicket& ticket = seg.jobTicket(0);
90 
91  Job job = ticket.createJobFor (Time{0,15});
92  CHECK (MockJobTicket::isAssociated (job, ticket));
93 
94  job.triggerJob();
95  CHECK (MockJob::was_invoked (job));
96  }
97 
98 
99 
100 
102  void
104  {
105  Time nominalTime = lib::test::randTime();
106  int additionalKey = rand() % 5000;
107  MockJob mockJob{nominalTime, additionalKey};
108  CHECK (mockJob.getNominalTime() == nominalTime);
109  CHECK (not MockJob::was_invoked (mockJob));
110 
111  mockJob.triggerJob();
112  CHECK (MockJob::was_invoked (mockJob));
113  CHECK (RealClock::wasRecently (MockJob::invocationTime (mockJob)));
114  CHECK (nominalTime == MockJob::invocationNominalTime (mockJob) );
115  CHECK (additionalKey == MockJob::invocationAdditionalKey(mockJob));
116 
117  Time prevInvocation = MockJob::invocationTime (mockJob);
118  mockJob.triggerJob();
119  CHECK (prevInvocation < MockJob::invocationTime (mockJob)); // invoked again, recorded new invocation time
120  CHECK (nominalTime == MockJob::invocationNominalTime (mockJob) ); // all other Job parameter recorded again unaltered
121  CHECK (additionalKey == MockJob::invocationAdditionalKey(mockJob));
122  }
123 
124 
126  void
128  {
129  auto frameTime = lib::test::randTime();
130 
131  // build a render job to do nothing....
132  Job nopJob = JobTicket::NOP.createJobFor (frameTime);
133  CHECK (INSTANCEOF (vault::gear::NopJobFunctor, static_cast<JobClosure*> (nopJob.jobClosure)));
134  CHECK (nopJob.parameter.nominalTime == frameTime);
135  InvocationInstanceID empty;
136  CHECK (lumiera_invokey_eq (&nopJob.parameter.invoKey, &empty));
137  CHECK (MockJob::isNopJob(nopJob)); // this diagnostic helper checks the same conditions as done here explicitly
138 
139  MockJobTicket mockTicket;
140  CHECK (not mockTicket.empty());
141  Job mockJob = mockTicket.createJobFor (frameTime);
142  CHECK ( mockTicket.verify_associated (mockJob)); // proof by invocation hash : is indeed backed by this JobTicket
143  CHECK (not mockTicket.verify_associated (nopJob)); // ...while some random other job is not related
144  CHECK (not MockJob::isNopJob(mockJob));
145  }
146 
147 
148 
157  void
159  {
160  Time someTime = lib::test::randTime();
161  //
162  //-----------------------------------------------------------------/// Empty default Segmentation
163  {
164  MockSegmentation mockSeg;
165  CHECK (1 == mockSeg.size());
166  JobTicket const& ticket = mockSeg[someTime].jobTicket(0); // just probe JobTicket generated for Model-Port-Nr.0
167  CHECK (util::isSameObject (ticket, JobTicket::NOP));
168  }
169  //-----------------------------------------------------------------/// Segmentation with one default segment spanning complete timeline
170  {
171  MockSegmentation mockSegs{MakeRec().genNode()};
172  CHECK (1 == mockSegs.size());
173  CHECK (Time::MIN == mockSegs[someTime].start());
174  CHECK (Time::MAX == mockSegs[someTime].after());
175  JobTicket& ticket = mockSegs[someTime].jobTicket(0);
176  CHECK (not util::isSameObject (ticket, JobTicket::NOP));
177 
178  Job someJob = ticket.createJobFor(someTime); // JobTicket uses, but does not check the time given
179  CHECK (someJob.parameter.nominalTime == someTime);
180  CHECK (MockJobTicket::isAssociated (someJob, ticket)); // but the generated Job is linked to the Closure backed by the JobTicket
181  CHECK (not MockJob::was_invoked (someJob));
182 
183  someJob.triggerJob();
184  CHECK (MockJob::was_invoked (someJob));
185  CHECK (RealClock::wasRecently (MockJob::invocationTime (someJob)));
186  CHECK (someTime == MockJob::invocationNominalTime (someJob));
187  }
188  //-----------------------------------------------------------------/// Segmentation with a segment spanning part of the timeline > 10s
189  {
190  // Marker to verify the job calls back into the right segment
191  int marker = rand() % 1000;
192  //
193  // Build a Segmentation partitioned at 10s
194  MockSegmentation mockSegs{MakeRec()
195  .attrib ("start", Time{0,10}
196  ,"mark", marker)
197  .genNode()};
198  CHECK (2 == mockSegs.size());
199  // since only start-time was given, the SplitSplice-Algo will attach
200  // the new Segment starting at 10s and expand towards +∞,
201  // while the left part of the axis is marked as NOP / empty
202  fixture::Segment const& seg1 = mockSegs[Time::ZERO]; // access anywhere < 10s
203  fixture::Segment const& seg2 = mockSegs[Time{0,20}]; // access anywhere >= 10s
204  CHECK ( util::isSameObject (seg1.jobTicket(0),JobTicket::NOP));
205  CHECK (not util::isSameObject (seg2.jobTicket(0),JobTicket::NOP));// this one is the active segment
206 
207  Job job = seg2.jobTicket(0).createJobFor(someTime);
208  CHECK (not MockJobTicket::isAssociated (job, seg1.jobTicket(0)));
209  CHECK ( MockJobTicket::isAssociated (job, seg2.jobTicket(0)));
210  CHECK (marker == job.parameter.invoKey.part.a);
211 
212  job.triggerJob();
213  CHECK (MockJob::was_invoked (job));
214  CHECK (RealClock::wasRecently (MockJob::invocationTime (job)));
215  CHECK (marker == MockJob::invocationAdditionalKey (job)); // DummyClosure is rigged such as to feed back the seed in `part.a`
216  // and thus we can prove this job really belongs to the marked segment
217  // create another job from the (empty) seg1
218  job = seg1.jobTicket(0).createJobFor (someTime);
219  InvocationInstanceID empty;
220  CHECK (lumiera_invokey_eq (&job.parameter.invoKey, &empty)); // indicates that it's just a placeholder to mark a "NOP"-Job
221  CHECK (seg1.jobTicket(0).empty());
222  CHECK (seg1.empty());
223  CHECK (not seg2.empty());
224  }
225  //-----------------------------------------------------------------/// Segmentation with one delineated segment, and otherwise empty
226  {
227  int marker = rand() % 1000;
228  // Build Segmentation with one fully defined segment
229  MockSegmentation mockSegs{MakeRec()
230  .attrib ("start", Time{0,10}
231  ,"after", Time{0,20}
232  ,"mark", marker)
233  .genNode()};
234  CHECK (3 == mockSegs.size());
235  auto const& [s1,s2,s3] = seqTuple<3> (mockSegs.eachSeg());
236  CHECK (s1.empty());
237  CHECK (not s2.empty());
238  CHECK (s3.empty());
239  CHECK (isSameObject (s2, mockSegs[Time{0,10}]));
240  CHECK (Time::MIN == s1.start());
241  CHECK (Time(0,10) == s1.after());
242  CHECK (Time(0,10) == s2.start());
243  CHECK (Time(0,20) == s2.after());
244  CHECK (Time(0,20) == s3.start());
245  CHECK (Time::MAX == s3.after());
246 
247  Job job = s2.jobTicket(0).createJobFor(someTime);
248  job.triggerJob();
249  CHECK (marker == MockJob::invocationAdditionalKey (job));
250  }
251  //-----------------------------------------------------------------/// Segmentation with several segments built in specific order
252  {
253  // Build Segmentation by partitioning in several steps
254  MockSegmentation mockSegs{MakeRec()
255  .attrib ("start", Time{0,20} // note: inverted segment definition is rectified automatically
256  ,"after", Time{0,10}
257  ,"mark", 1)
258  .genNode()
259  ,MakeRec()
260  .attrib ("after", Time::ZERO
261  ,"mark", 2)
262  .genNode()
263  ,MakeRec()
264  .attrib ("start", Time{FSecs{-5}}
265  ,"mark", 3)
266  .genNode()};
267 
268  CHECK (5 == mockSegs.size());
269  auto const& [s1,s2,s3,s4,s5] = seqTuple<5> (mockSegs.eachSeg());
270  CHECK (not s1.empty());
271  CHECK (not s2.empty());
272  CHECK ( s3.empty());
273  CHECK (not s4.empty());
274  CHECK ( s5.empty());
275  CHECK (Time::MIN == s1.start()); // the second added segment has covered the whole negative axis
276  CHECK (-Time(0,5) == s1.after()); // ..up to the partitioning point -5
277  CHECK (-Time(0,5) == s2.start()); // ...while the rest was taken up by the third added segment
278  CHECK (Time(0, 0) == s2.after());
279  CHECK (Time(0, 0) == s3.start()); // an empty gap remains between [0 ... 10[
280  CHECK (Time(0,10) == s3.after());
281  CHECK (Time(0,10) == s4.start()); // here is the first added segment
282  CHECK (Time(0,20) == s4.after());
283  CHECK (Time(0,20) == s5.start()); // and the remaining part of the positive axis is empty
284  CHECK (Time::MAX == s5.after());
285 
286  auto probeKey = [&](Segment const& segment)
287  {
288  if (segment.empty()) return 0;
289 
290  Job job = segment.jobTicket(0).createJobFor(someTime);
291  job.triggerJob();
292  CHECK (MockJob::was_invoked (job));
293  CHECK (RealClock::wasRecently (MockJob::invocationTime (job)));
294 
295  return MockJob::invocationAdditionalKey (job);
296  };
297  CHECK (2 == probeKey(s1)); // verify all generated jobs are wired back to the correct segment
298  CHECK (3 == probeKey(s2));
299  CHECK (0 == probeKey(s3));
300  CHECK (1 == probeKey(s4));
301  CHECK (0 == probeKey(s5));
302  }
303  }
304 
305 
306 
312  void
314  {
315  Time someTime = lib::test::randTime();
316  //-----------------------------------------------------------------/// one Segment with one additional prerequisite
317  {
318  MockSegmentation mockSegs{MakeRec()
319  .attrib("mark", 11)
320  .scope(MakeRec()
321  .attrib("mark",23)
322  .genNode())
323  .genNode()};
324  CHECK (1 == mockSegs.size());
325  JobTicket& ticket = mockSegs[Time::ZERO].jobTicket(0); // Model-PortNr.0
326  auto prereq = ticket.getPrerequisites();
327  CHECK (not isnil (prereq));
328 
329  JobTicket& preTicket = *prereq;
330  ++prereq;
331  CHECK (isnil (prereq));
332 
333  Job job1 = preTicket.createJobFor (someTime);
334  Job job2 = ticket.createJobFor (someTime);
335 
336  job1.triggerJob();
337  job2.triggerJob();
338  CHECK (23 == MockJob::invocationAdditionalKey (job1));
339  CHECK (11 == MockJob::invocationAdditionalKey (job2));
340  }
341  //-----------------------------------------------------------------/// a tree of deep nested prerequisites
342  {
343  MockSegmentation mockSegs{MakeRec()
344  .attrib("mark", 11)
345  .scope(MakeRec()
346  .attrib("mark",33)
347  .scope(MakeRec()
348  .attrib("mark",55)
349  .genNode()
350  ,MakeRec()
351  .attrib("mark",44)
352  .genNode()
353  )
354  .genNode()
355  ,MakeRec()
356  .attrib("mark",22)
357  .genNode())
358  .genNode()};
359 
360  auto start = singleValIterator (mockSegs[Time::ZERO].jobTicket(0));
361 
362  auto it = lib::explore(start)
363  .expand ([](JobTicket& ticket)
364  {
365  return ticket.getPrerequisites();
366  })
367  .expandAll()
368  .transform ([&](JobTicket& ticket)
369  {
370  return ticket.createJobFor(someTime).parameter.invoKey.part.a;
371  });
372 
373 
374  CHECK (util::join(it,"-") == "11-22-33-44-55"_expect);
375  } // Note: Prerequisites are prepended (LinkedElements)
376  } // thus at each level the last ones appear first
377 
378 
379 
389  void
391  {
392  {
393  MockDispatcher dispatcher;
394  // automatically generates some fake connection points...
395  auto [port0,sink0] = dispatcher.getDummyConnection(0);
396  auto [port1,sink1] = dispatcher.getDummyConnection(1);
397  CHECK (port0 != port1);
398  CHECK (sink0 != sink1);
399  CHECK (port0.isValid());
400  CHECK (port1.isValid());
401  CHECK (sink0.isValid());
402  CHECK (sink1.isValid());
403  CHECK (not ModelPort().isValid());
404  CHECK (not DataSink().isValid());
405 
406  CHECK (0 == dispatcher.resolveModelPort(port0));
407  CHECK (1 == dispatcher.resolveModelPort(port1));
408 
409  Time frameTime{0,30};
410  size_t modelPortIDX = 0;
411  Job job0 = dispatcher.createJobFor (modelPortIDX, frameTime);
412  modelPortIDX = 1;
413  Job job1 = dispatcher.createJobFor (modelPortIDX, frameTime);
414  CHECK (dispatcher.verify(job0, port0, sink0));
415  CHECK (dispatcher.verify(job1, port1, sink1));
416  }
417  //-----------------------------------------------------------------/// can define multiple Segments
418  {
419  MockDispatcher dispatcher{MakeRec()
420  .attrib("mark", 11)
421  .genNode()
422  ,MakeRec()
423  .attrib("mark", 22)
424  .attrib("start", Time{0,10}) // second segment covers 10s … +Time::MAX
425  .genNode()};
426 
427  size_t modelPortIDX = 1;
428  Job job0 = dispatcher.createJobFor (modelPortIDX, Time{0,5});
429  Job job1 = dispatcher.createJobFor (modelPortIDX, Time{0,25});
430 
431  CHECK (11 == job0.parameter.invoKey.part.a);
432  CHECK (22 == job1.parameter.invoKey.part.a);
433  }
434  }
435  };
436 
437 
439  LAUNCHER (MockSupport_test, "unit engine");
440 
441 
442 
443 }}} // 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:49
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:492
A mocked frame Dispatcher setup without any backing model.
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.
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:136
Lumiera&#39;s internal time value datatype.
Definition: timevalue.hpp:308
Abstract Base Class for all testcases.
Definition: run.hpp:62
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.
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:664
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:104
opaque ID attached to each individual job invocation.
Definition: job.h:112
For the purpose of building and rendering, the fixture (for each timeline) is partitioned such that e...
Definition: segment.hpp:68
static bool isNopJob(Job const &)
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
static const Time MAX
Definition: timevalue.hpp:318
auto getPrerequisites()
Core operation: iterate over the prerequisites, required to carry out a render operation based on thi...
Definition: job-ticket.hpp:164
execution plan for pulling a specific exit node.
Definition: job-ticket.hpp:87
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:79
bool isSameObject(A const &a, B const &b)
compare plain object identity, bypassing any custom comparison operators.
Definition: util.hpp:372