/* ActivityDetector(Test) - verify diagnostic setup to watch scheduler activities Copyright (C) 2023, Hermann Vosseler   **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 activity-detector-test.cpp ** unit test \ref ActivityDetector_test */ #include "lib/test/run.hpp" #include "lib/test/test-helper.hpp" #include "activity-detector.hpp" #include "vault/real-clock.hpp" #include "lib/time/timevalue.hpp" #include "lib/format-cout.hpp" #include "lib/util.hpp" using lib::time::Time; using lib::time::FSecs; using util::isSameObject; using lib::test::randStr; using lib::test::randTime; namespace vault{ namespace gear { namespace test { /*****************************************************************//** * @test verify instrumentation setup to watch scheduler Activities. * @see SchedulerActivity_test * @see SchedulerUsage_test */ class ActivityDetector_test : public Test { virtual void run (Arg) { seedRand(); simpleUsage(); verifyMockInvocation(); verifyFakeInvocation(); verifyMockJobFunctor(); verifyFakeExeContext(); watch_ActivationProbe(); watch_ActivationTap(); insert_ActivationTap(); watch_notification(); watch_gate(); } /** @test demonstrate a simple usage scenario of this test support facility */ void simpleUsage() { ActivityDetector detector("spectre"); auto trap = detector.buildDiagnosticFun("trap") .returning(55); CHECK (55 == trap (1.23, Time{FSecs{3,2}})); CHECK (detector == "Rec(EventLogHeader| this = ActivityDetector(spectre) ), " "Rec(call| fun = trap, this = ActivityDetector(spectre), Seq = 0 |{1.23, 0:00:01.500})"_expect); } /** @test verify the setup and detection of instrumented invocations * - a _sequence number_ is embedded into the ActivityDetector * - this sequence number is recorded into an attribute at each invocation * - a DSL for verification is provided (based on the EventLog) * - arguments and sequence numbers can be explicitly checked */ void verifyMockInvocation() { ActivityDetector detector; auto fun = detector.buildDiagnosticFun ("funny"); uint rnd = rani(10000); detector.incrementSeq(); CHECK (1 == detector.currSeq()); CHECK (detector.ensureNoInvocation ("funny")); detector.incrementSeq(); CHECK (2 == detector.currSeq()); CHECK (detector.verifySeqIncrement(2)); fun (rnd); CHECK (detector.verifyInvocation ("funny")); CHECK (detector.verifyInvocation ("funny").arg(rnd)); CHECK (detector.verifyInvocation ("funny").seq(2)); CHECK (detector.verifyInvocation ("funny").arg(rnd).seq(2)); CHECK (detector.verifyInvocation ("funny").seq(2).arg(rnd)); CHECK (detector.ensureNoInvocation ("bunny")); // wrong name CHECK (detector.ensureNoInvocation ("funny").arg()); // fails since empty argument list expected CHECK (detector.ensureNoInvocation ("funny").arg(rnd+5)); // expecting wrong argument CHECK (detector.ensureNoInvocation ("funny").seq(5)); // expecting wrong sequence number CHECK (detector.ensureNoInvocation ("funny").arg(rnd).seq(1)); // expecting correct argument, but wrong sequence detector.incrementSeq(); fun (rnd+1); CHECK (detector.verifyInvocation ("funny").seq(2) .beforeSeqIncrement(3) .beforeInvocation ("funny").seq(3).arg(rnd+1)); CHECK (detector == "Rec(EventLogHeader| this = ActivityDetector )" ", Rec(event| ID = IncSeq |{1})" ", Rec(event| ID = IncSeq |{2})" ", Rec(call| fun = funny, this = ActivityDetector, Seq = 2 |{"+util::toString(rnd)+"})" ", Rec(event| ID = IncSeq |{3})" ", Rec(call| fun = funny, this = ActivityDetector, Seq = 3 |{"+util::toString(rnd+1)+"})"_expect); } /** @test verify a variation of the instrumented functor * to call into a custom provided _fake implementation._ */ void verifyFakeInvocation() { ActivityDetector detector; auto fun = detector.buildDiagnosticFun ("fakeFun"); uint rnd = rani(10000); CHECK (0 == fun (rnd)); fun.returning(42); detector.incrementSeq(); CHECK (42 == fun (rnd)); fun.implementedAs ([](uint i){ return -i; }); detector.incrementSeq(); CHECK (-int(rnd) == fun (rnd)); CHECK (detector.verifyInvocation("fakeFun").seq(0) .beforeInvocation("fakeFun").seq(1) .beforeInvocation("fakeFun").seq(2)); } /** @test diagnostic setup to detect a JobFunctor activation * - the ActivityDetector provides specifically rigged JobFunctor instances * - these capture all invocations, based on generic invocation logging * - special match qualifier to verify the job's nominal invocation time parameter * - event verification can be combined with other verifications to cover * complex invocation sequences */ void verifyMockJobFunctor() { ActivityDetector detector; InvocationInstanceID invoKey; Time nominal{FSecs{5,2}}; invoKey.part.a = 55; Job dummyJob{detector.buildMockJobFunctor ("mockJob") ,invoKey ,nominal}; CHECK (detector.ensureNoInvocation ("mockJob")); dummyJob.triggerJob(); CHECK (detector.verifyInvocation ("mockJob")); CHECK (detector.verifyInvocation ("mockJob").arg(nominal, invoKey.part.a)); CHECK (detector.verifyInvocation ("mockJob").timeArg(nominal)); detector.incrementSeq(); // note: sequence number incremented between invocations dummyJob.parameter.nominalTime += 5 * Time::SCALE; // different job parameter (later nominal time point) dummyJob.triggerJob(); CHECK (detector.verifyInvocation ("mockJob").timeArg(nominal).seq(0) .beforeInvocation ("mockJob").timeArg(nominal + Time{FSecs{5}}) // matching first invocation and then second... .afterSeqIncrement(1) // note: searching backwards from the 2nd invocation ); // cout << detector.showLog()<(); Time t = randTime(); Time td{t+Time(0,1)}; size_t x = rani(); Activity a; CHECK (detector.ensureNoInvocation(CTX_WORK)); CHECK (detector.ensureNoInvocation(CTX_POST)); CHECK (detector.ensureNoInvocation(CTX_DONE)); CHECK (detector.ensureNoInvocation(CTX_TICK)); ctx.work (t,x); CHECK (detector.verifyInvocation(CTX_WORK).arg(t,x)); ctx.done (t,x); CHECK (detector.verifyInvocation(CTX_DONE).arg(t,x)); CHECK (activity::PASS == ctx.post (t,td, &a, ctx)); CHECK (detector.verifyInvocation(CTX_POST).arg(t,td,&a,ctx)); CHECK (activity::PASS == ctx.tick(t)); CHECK (detector.verifyInvocation(CTX_TICK).arg(t)); detector.incrementSeq(); ctx.tick.returning(activity::KICK); CHECK (activity::KICK == ctx.tick(t)); CHECK (detector.verifyInvocation(CTX_TICK).timeArg(t)); CHECK (detector.verifyInvocation(CTX_WORK).timeArg(t) .beforeInvocation(CTX_DONE).timeArg(t) .beforeInvocation(CTX_POST).timeArg(t) .beforeInvocation(CTX_TICK).timeArg(t).seq(0) .beforeInvocation(CTX_TICK).timeArg(t).seq(1)); } /** @test a rigged diagnostic probe to detect Activity activation */ void watch_ActivationProbe() { ActivityDetector detector; auto someID = "trap-" + randStr(4); Activity& probe = detector.buildActivationProbe (someID); CHECK (probe.is (Activity::HOOK)); CHECK (not detector.wasInvoked (probe)); Time realTime = RealClock::now(); probe.activate (realTime, detector.executionCtx); CHECK (detector.verifyInvocation(someID).timeArg(realTime)); // Probe instance recalls last invocation "now" argument CHECK (realTime == detector.invokeTime (probe)); CHECK (detector.wasInvoked (probe)); } /** @test diagnostic adaptor to detect and pass-through Activity activation */ void watch_ActivationTap() { ActivityDetector detector; Time nomTime{99,11}; Activity feed{size_t{12},size_t{34}}; Activity feed2{size_t{56},size_t{78}}; feed.next = &feed2; string jobID = "job-" + randStr(4); Activity invoke{detector.buildMockJobFunctor(jobID), nomTime, feed}; Time t1{0,1,1}; CHECK (activity::PASS == invoke.activate (t1, detector.executionCtx)); CHECK (detector.verifyInvocation (jobID).arg(nomTime, 12)); // decorate the INVOKE-Activity with an ActivationTap Activity& tap = detector.buildActivationTap (invoke); CHECK (tap.next == invoke.next); detector.incrementSeq(); Time t2{0,2,2}; // now activate through the Tap.... tap.activate(t2, detector.executionCtx); CHECK (detector.verifySeqIncrement(1) // ==> the ActivationTap "tap-INVOKE" reports and passes activation .beforeInvocation("tap-INVOKE").seq(1).arg("JobFun-ActivityDetector."+jobID) .beforeInvocation(jobID).seq(1).arg(nomTime,12)); // WARNING: can still activate the watched subject directly... detector.incrementSeq(); Time t3{0,3,3}; invoke.activate (t3, detector.executionCtx); CHECK (detector.verifyInvocation(jobID).seq(2)); // subject invoked CHECK (detector.ensureNoInvocation("tap-INVOKE").seq(2) // but invocation not detected by ActivationTap .beforeInvocation(jobID).seq(2)); } /** @test inject (prepend) an ActivationTap into existing wiring */ void insert_ActivationTap() { ActivityDetector detector; Activity subject; Activity followUp{size_t(1), size_t(2)}; subject.next = &followUp; Activity* wiring = &subject; CHECK (isSameObject (*wiring, subject)); CHECK (wiring->verb_ == Activity::TICK); detector.insertActivationTap (wiring); CHECK (not isSameObject (*wiring, subject)); CHECK (wiring->verb_ == Activity::HOOK); CHECK (wiring->data_.callback.arg == size_t(&subject)); CHECK (wiring->next == subject.next); Time tt{1,1,1}; // now activate through the wiring.... wiring->activate(tt, detector.executionCtx); CHECK (detector.verifyInvocation("tap-TICK").arg("⧐ Act(TICK") .beforeInvocation("CTX-tick").timeArg(tt)); } /** @test diagnostic setup to detect and watch passing a notification * - setup a chain-Activity (here: a `TICK`) protected by a `GATE` * - configure the `GATE` to require one notification * - connect a `NOTIFY`-Activity to trigger the `GATE` * - inject a diagnostics Tap into the notification-connection * - dispatch of the notification can be verified * - notification has been passed through the Tap to the `GATE` * - `GATE` has been decremented to zero and triggers chain * - finally the chained `TICK`-Activity calls into the `executionCtx` */ void watch_notification() { Time tt{11,11}; // start time of the NOTIFY Time ts{22,22}; // start time of the target-chain Time td{33,33}; // deadline for the target-chain ActivityDetector detector; Activity chain; Activity gate{1, td}; gate.next = &chain; Activity notification{&gate, ts}; // note: follow-up start time `ts` injected here CHECK (gate.data_.condition.rest == 1); detector.insertActivationTap (notification.data_.notification.target); notification.activate (tt, detector.executionCtx); // dispatch time `tt` (is actually irrelevant here) // activating the NOTIFY causes it to POST its target, thereby setting the deadline from the GATE CHECK (detector.verifyInvocation("CTX-post").arg("22.022","33.033", "tap-GATE", "≺test::CTX≻")); detector.incrementSeq(); // to see the effect of the instrumentation, we need to mimic the behaviour of λ-post, // which is to call Activity::dispatch() on the given target notification.data_.notification.target->dispatch (ts, detector.executionCtx); // note: using `ts` for the follow-up chain CHECK (detector.verifyInvocation("tap-GATE").seq(1).arg("22.022 --notify-↯> Act(GATE")); CHECK (gate.data_.condition.rest == 0); } /** @test diagnostic setup to watch Activity::GATE activation * - when applied, Tap will be inserted before and after the * instrumented GATE-Activity * - it can thus be traced when the Gate is activated, * but also when the Gate condition is met and the `next` * Activity after the Gate is activated * - for this unit-test, a Gate and a follow-up Activity * is invoked directly, to verify the generated log entries */ void watch_gate() { ActivityDetector detector; Activity gate{0}; Activity followUp; gate.next = &followUp; Activity* wiring = &gate; detector.watchGate (wiring); Time tt{5,5}; wiring->activate(tt, detector.executionCtx); detector.incrementSeq(); wiring->next->activate(tt, detector.executionCtx); CHECK (detector.verifyInvocation("tap-GATE").seq(0).timeArg(tt) .beforeSeqIncrement(1) .beforeInvocation("after-GATE").seq(1).timeArg(tt) .beforeInvocation("CTX-tick").seq(1).timeArg(tt)); } }; /** Register this test class... */ LAUNCHER (ActivityDetector_test, "unit engine"); }}} // namespace vault::gear::test