lumiera_/tests/library/lazy-init-test.cpp
Ichthyostega 806db414dd Copyright: clarify and simplify the file headers
* Lumiera source code always was copyrighted by individual contributors
 * there is no entity "Lumiera.org" which holds any copyrights
 * Lumiera source code is provided under the GPL Version 2+

== Explanations ==
Lumiera as a whole is distributed under Copyleft, GNU General Public License Version 2 or above.
For this to become legally effective, the ''File COPYING in the root directory is sufficient.''

The licensing header in each file is not strictly necessary, yet considered good practice;
attaching a licence notice increases the likeliness that this information is retained
in case someone extracts individual code files. However, it is not by the presence of some
text, that legally binding licensing terms become effective; rather the fact matters that a
given piece of code was provably copyrighted and published under a license. Even reformatting
the code, renaming some variables or deleting parts of the code will not alter this legal
situation, but rather creates a derivative work, which is likewise covered by the GPL!

The most relevant information in the file header is the notice regarding the
time of the first individual copyright claim. By virtue of this initial copyright,
the first author is entitled to choose the terms of licensing. All further
modifications are permitted and covered by the License. The specific wording
or format of the copyright header is not legally relevant, as long as the
intention to publish under the GPL remains clear. The extended wording was
based on a recommendation by the FSF. It can be shortened, because the full terms
of the license are provided alongside the distribution, in the file COPYING.
2024-11-17 23:42:55 +01:00

483 lines
19 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
LazyInit(Test) - verify a mechanism to install a self-initialising functor
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 lazy-init-test.cpp
** unit test \ref LazyInit_test
*/
#include "lib/test/run.hpp"
#include "lib/test/test-helper.hpp"
#include "lib/lazy-init.hpp"
#include "lib/meta/util.hpp"
#include "lib/util.hpp"
#include <memory>
namespace lib {
namespace test{
using util::isSameObject;
using lib::meta::isFunMember;
using lib::meta::disable_if_self;
using err::LUMIERA_ERROR_LIFECYCLE;
using std::make_unique;
/***********************************************************************************//**
* @test Verify a mix-in to allow for lazy initialisation of complex infrastructure
* tied to a std::function; the intention is to have a »trap« hidden in the
* function itself to trigger on first use and perform the one-time
* initialisation, then finally lock the object at a fixed place.
* @see lazy-init.hpp
* @see lib::RandomDraw
*/
class LazyInit_test
: public Test
{
void
run (Arg)
{
seedRand();
verify_trojanLambda();
verify_inlineStorage();
verify_TargetRelocation();
verify_triggerMechanism();
verify_lazyInitialisation();
verify_complexUsageWithCopy();
}
/** @test verify construction of the »trap« front-end eventually to trigger initialisation
* - this test does not involve any std::function, rather a heap-allocated copy of a λ
* # the _target function_ finally to be invoked performs a verifiable computation
* # the _delegate_ receives an memory location and returns a reference to the target
* # the generated _»trojan λ«_ captures its own address, invokes the delegate,
* retrieves a reference to a target functor, and finally invokes this with actual arguments.
* @remark the purpose of this convoluted scheme is for the _delegate to perform initialisation,_
* taking into account the current memory location „sniffed“ by the trojan.
*/
void
verify_trojanLambda()
{
size_t beacon;
auto fun = [&](uint challenge){ return beacon+challenge; };
using Sig = size_t(uint);
CHECK (isFunMember<Sig> (&fun));
beacon = rani();
uint c = beacon % 42;
// verify we can invoke the target function
CHECK (beacon+c == fun(c));
// verify we can also invoke the target function through a reference
using FunType = decltype(fun);
FunType& funRef = fun;
CHECK (beacon+c == funRef(c));
// construct delegate function exposing the expected behaviour;
// additionally this function captures the passed-in address.
RawAddr location{nullptr};
auto delegate = [&](RawAddr adr) -> FunType&
{
location = adr;
return fun;
};
using Delegate = decltype(delegate);
auto delP = make_unique<Delegate> (delegate);
// verify the heap-allocated copy of the delegate behaves as expected
location = nullptr;
CHECK (beacon+c == (*delP)(this)(c));
CHECK (location == this);
// now (finally) build the »trap function«...
auto trojanLambda = TrojanFun<Sig>::generateTrap (delP.get());
CHECK (sizeof(trojanLambda) == sizeof(size_t));
// on invocation...
// - it captures its current location
// - passes this to the delegate
// - invokes the target function returned from the delegate
CHECK (beacon+c == trojanLambda(c));
CHECK (location == &trojanLambda);
// repeat same with a copy, and changed beacon value
auto trojanClone = trojanLambda;
beacon = rani();
c = beacon % 55;
CHECK (beacon+c == trojanClone(c));
CHECK (location == &trojanClone);
CHECK (beacon+c == trojanLambda(c));
CHECK (location == &trojanLambda);
}
/** @test verify that std::function indeed stores a simple functor inline.
* @remark The implementation of LazyInit relies crucially on a known optimisation
* in the standard library ─ which unfortunately is not guaranteed by the standard:
* Typically, std::function will apply _small object optimisation_ to place a very
* small functor directly into the wrapper, if the payload has a trivial copy-ctor.
* `Libstdc++` is known to be rather restrictive, while other implementations trade
* increased storage size of std::function against more optimisation possibilities.
* LazyInit exploits this optimisation to „spy“ about the current object location,
* allowing to execute the lazy initialisation on first use, without further help
* by client code. This trickery seems to be the only way, since λ-capture by reference
* is broken after copying or moving the host object (typically required for DSL use).
* In case this turns out to be fragile, LazyInit should become a "LateInit" and needs
* help by the client or the user to trigger initialisation; alternatively the DSL
* could be split off into a separate builder object distinct from RandomDraw.
*/
void
verify_inlineStorage()
{
// char payload[24];// ◁─────────────────────────────── use this to make the test fail....
const char* payload = "please look elsewhere";
auto lambda = [payload]{ return RawAddr(&payload); };
RawAddr location = lambda();
CHECK (location == &lambda);
std::function funWrap{lambda};
CHECK (funWrap);
CHECK (not isSameObject (funWrap, lambda));
location = funWrap();
CHECK (util::isCloseBy (location, funWrap));
// if »small object optimisation« was used,
// the lambda will be copied directly into the std:function;
// otherwise it will be heap allocated and this test fails.
// for context: these are considered "close by",
// since both are sitting right here in the same stack frame
CHECK (util::isCloseBy (funWrap, lambda));
}
/** @test verify navigating an object structure
* by applying known offsets consecutively
* from a starting point within an remote instance
* @remark in the real usage scenario, we know _only_ the offset
* and attempt to find home without knowing the layout.
*/
void
verify_TargetRelocation()
{
struct Nested
{
int unrelated{rani()};
int anchor{rani()};
};
struct Demo
{
Nested nested;
virtual ~Demo(){ };
virtual RawAddr peek()
{
return &nested.anchor;
}
};
// find out generic offset...
const ptrdiff_t offNested = []{
Nested probe;
return captureRawAddrOffset(&probe, &probe.anchor);
}();
Demo here;
// find out actual offset in existing object
const ptrdiff_t offBase = captureRawAddrOffset(&here, &here.nested);
CHECK (offBase > 0);
CHECK (offNested > 0);
// create a copy far far away...
auto farAway = make_unique<Demo> (here);
// reconstruct base address from starting point
RawAddr startPoint = farAway->peek();
Nested* farNested = relocate<Nested>(startPoint, -offNested);
CHECK (here.nested.unrelated == farNested->unrelated);
Demo* farSelf = relocate<Demo> (farNested, -offBase);
CHECK (here.nested.anchor == farSelf->nested.anchor);
CHECK (isSameObject (*farSelf, *farAway));
}
/** @test demonstrate the trigger mechanism in isolation
*/
void
verify_triggerMechanism()
{
using Fun = std::function<float(int)>;
Fun theFun;
CHECK (not theFun);
int report{0};
auto delegate = [&report](RawAddr insideFun) -> Fun&
{
auto realFun = [&report](int num)
{
report += num;
return num + 23.55f;
};
Fun& target = *relocate<Fun>(insideFun, -FUNCTOR_PAYLOAD_OFFSET);
report = -42; // as proof that the init-delegate was invoked
target = realFun;
return target;
};
CHECK (not theFun);
// install the init-»trap«
theFun = TrojanFun<float(int)>::generateTrap (&delegate);
CHECK (theFun);
CHECK (0 == report);
// invoke function
int feed{1 + rani (100)};
float res = theFun (feed);
// delegate *and* realFun were invoked
CHECK (feed == report + 42);
CHECK (res = feed -42 +23.55f);
// again...
report = 0;
feed = -1-rani(20);
res = theFun (feed);
// this time the delegate was *not* invoked,
// only the installed realFun
CHECK (feed == report);
CHECK (res = feed + 23.55f);
}
/** @test demonstrate a basic usage scenario
*/
void
verify_lazyInitialisation()
{
using Fun = std::function<float(int)>;
using Lazy = LazyInit<Fun>;
bool init{false};
uint invoked{0};
Lazy funny{funny, [&](Lazy* self)
{
Fun& thisFun = static_cast<Fun&> (*self);
thisFun = [&invoked](int num)
{
++invoked;
return num * 0.555f;
};
init = true;
}};
CHECK (not invoked);
CHECK (not init);
CHECK (funny);
int feed = 1 + rani(99);
CHECK (feed*0.555f == funny(feed));
CHECK (1 == invoked);
CHECK (init);
}
/** elaborate setup used for integration test */
struct LazyDemo
: LazyInit<>
{
using Fun = std::function<int(int)>;
int seed{0};
Fun fun; // ◁────────────────────────────────── this will be initialised lazily....
template<typename FUN>
auto
buildInit (FUN&& fun2install)
{
return [theFun = forward<FUN> (fun2install)]
(LazyDemo* self)
{ // this runs when init is actually performed....
CHECK (self);
if (self->fun)
// chain-up behind existing function
self->fun = [self, prevFun=self->fun, nextFun=theFun]
(int i)
{
return nextFun (prevFun (i));
};
else
// build new function chain, inject seed from object
self->fun = [self, newFun=theFun]
(int i)
{
return newFun (i + self->seed); // Note: binding to actual instance location
};
};
}
LazyDemo()
: LazyInit{MarkDisabled()}
, fun{}
{
installInitialiser(fun, buildInit([](int){ return 0; }));
}
// prevent this ctor from shadowing the copy ctors //////TICKET #963
template<typename FUN, typename =disable_if_self<LazyDemo, FUN>>
LazyDemo (FUN&& someFun)
: LazyInit{MarkDisabled()}
, fun{}
{
installInitialiser(fun, buildInit (forward<FUN> (someFun)));
}
template<typename FUN>
LazyDemo&&
attach (FUN&& someFun)
{
installInitialiser(fun, buildInit (forward<FUN> (someFun)));
return move(*this);
}
};
/**
* @test use an elaborately constructed example to cover more corner cases
* - the function to manage and initialise lazily is _a member_ of the _derived class_
* - the initialisation routine _adapts_ this function and links it with the current
* object location; thus, invoking this function on a copy would crash / corrupt memory.
* - however, as long as initialisation has not been triggered, LazyDemo instances can be
* copied; they may even be assigned to existing instances, overwriting their state.
* - a second given function will be chained behind the first one; this happens immediately
* if the first function was already invoked (and this initialised)
* - but when however both functions are attached immediately, prior to invocation,
* then an elaborate chain of initialisers is setup behind the scenes and played back
* in definition order once lazy initialisation is triggered
* - all the intermediary state is safe to copy and move and fork
* @remark 11/2023 memory allocations were verified using lib::test::Tracker and the EventLog
*/
void
verify_complexUsageWithCopy()
{
LazyDemo dd;
CHECK (not dd.isInit()); // not initialised, since function was not invoked yet
CHECK (dd.fun); // the functor is not empty anymore, since the »trap« was installed
dd.seed = 2;
CHECK (0 == dd.fun(22)); // d1 was default initialised and thus got the "return 0" function
CHECK (dd.isInit()); // first invocation also triggered the init-routine
// is »engaged« after init and rejects move / copy
VERIFY_ERROR (LIFECYCLE, LazyDemo dx{move(dd)} );
dd = LazyDemo{[](int i) // assign a fresh copy (discarding any state in d1)
{
return i + 1; // using a "return i+1" function
}};
CHECK (not dd.isInit());
CHECK (dd.seed == 0); // assignment indeed erased any existing settings (seed≔2)
CHECK (dd.fun);
CHECK (23 == dd.fun(22)); // new function was tied in (while also referring to self->seed)
CHECK (dd.isInit());
dd.seed = 3; // set the seed
CHECK (26 == dd.fun(22)); // seed value is picked up dynamically
VERIFY_ERROR (LIFECYCLE, LazyDemo dx{move(dd)} );
// attach a further function, to be chained-up
dd.attach([](int i)
{
return i / 2;
});
CHECK (dd.isInit());
CHECK (dd.seed == 3);
CHECK (12 == dd.fun(21)); // 21+3+1=25 / 2
CHECK (13 == dd.fun(22));
CHECK (13 == dd.fun(23));
dd.seed++;
CHECK (14 == dd.fun(23)); // 23+4+1=28 / 2
CHECK (14 == dd.fun(24));
CHECK (15 == dd.fun(25));
// ...use exactly the same configuration,
// but applied in one shot -> chained lazy-Init
dd = LazyDemo{[](int i){return i+1; }}
.attach([](int i){return i/2; });
dd.seed = 3;
CHECK (not dd.isInit());
CHECK (dd.seed == 3);
CHECK (dd.fun);
CHECK (12 == dd.fun(21));
CHECK (13 == dd.fun(22));
CHECK (13 == dd.fun(23));
dd.seed++;
CHECK (14 == dd.fun(23));
CHECK (14 == dd.fun(24));
CHECK (15 == dd.fun(25));
// create a nested graph of chained pending init
dd = LazyDemo{[](int i){return i+1; }};
LazyDemo d1{dd};
LazyDemo d2{move(dd)};
d2.seed = 3;
d2.attach ([](int i){return i/2; });
LazyDemo d3{d2};
d2.attach ([](int i){return i-1; });
// dd was left in defunct state by the move, and thus is locked
CHECK (not dd.fun);
CHECK (dd.isInit());
VERIFY_ERROR (LIFECYCLE, LazyDemo dx{move(dd)} );
// this can be amended by assigning another instance not yet engaged
dd = d2;
d2.seed = 5;
std::swap (d2,d3);
std::swap (d3,d1);
// confused?? ;-)
CHECK (not dd.isInit() and dd.seed == 3); // Seed≡3 {i+1} ⟶ {i/2} ⟶ {i-1}
CHECK (not d1.isInit() and d1.seed == 5); // Seed≡5 {i+1} ⟶ {i/2} ⟶ {i-1}
CHECK (not d2.isInit() and d2.seed == 3); // Seed≡3 {i+1} ⟶ {i/2}
CHECK (not d3.isInit() and d3.seed == 0); // Seed≡0 {i+1}
CHECK (12 == dd.fun(23)); // 23+3 +1 = 27/2 = 13 -1 = 12
CHECK (13 == d1.fun(23)); // 23+5 +1 = 29/2 = 14 -1 = 13
CHECK (13 == d2.fun(23)); // 23+3 +1 = 27/2 = 13 = 13
CHECK (24 == d3.fun(23)); // 23+0 +1 = 24
}
};
/** Register this test class... */
LAUNCHER (LazyInit_test, "unit common");
}} // namespace lib::test