LUMIERA.clone/tests/library/lazy-init-test.cpp
Ichthyostega 04ca79fd65 Chain-Load: verify re-initialisation and copy
...this is a more realistic demo example, which mimics
some of the patterns present in RandomDraw. The test also
uses lambdas linking to the actual storage location, so that
the invocation would crash on a copy; LazyInit was invented
to safeguard against this, while still allowing leeway
during the initialisation phase in a DSL.
2023-12-03 04:59:18 +01:00

415 lines
16 KiB
C++

/*
LazyInit(Test) - verify a mechanism to install a self-initialising functor
Copyright (C) Lumiera.org
2023, Hermann Vosseler <Ichthyostega@web.de>
This program 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.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
* *****************************************************/
/** @file lazy-init-test.cpp
** unit test \ref LazyInit_test
*/
#include "lib/test/run.hpp"
#include "lib/lazy-init.hpp"
//#include "lib/format-string.hpp"
//#include "lib/test/test-helper.hpp"
//#include "lib/test/testdummy.hpp"
#include "lib/test/diagnostic-output.hpp" /////////////////////TODO TODOH
#include "lib/util.hpp"
#include <memory>
namespace lib {
namespace test{
// using util::_Fmt;
using std::make_unique;
using util::isSameObject;
using lib::meta::isFunMember;
using err::LUMIERA_ERROR_LIFECYCLE;
namespace { // policy and configuration for test...
//
}//(End) Test config
/***********************************************************************************//**
* @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 in place.
* @see lazy-init.hpp
* @see lib::RandomDraw
*/
class LazyInit_test
: public Test
{
void
run (Arg)
{
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 = rand();
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 = rand();
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{rand()};
int anchor{rand()};
};
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+rand()%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-rand()%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 + rand()%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)
{
CHECK (self);
self->fun = [self, chain = move(theFun)]
(int i)
{
return chain (i + self->seed); // Note: binding to actual instance location
};
};
}
LazyDemo()
: LazyInit{MarkDisabled()}
, fun{}
{
installInitialiser(fun, buildInit([](int){ return 0; }));
}
template<typename FUN>
LazyDemo(FUN&& someFun)
: LazyInit{MarkDisabled()}
, fun{}
{
installInitialiser(fun, buildInit (forward<FUN> (someFun)));
}
};
/**
* @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.
*/
void
verify_complexUsageWithCopy()
{
LazyDemo d1;
CHECK (not d1.isInit()); // not initialised, since function was not invoked yet
CHECK (d1.fun); // the functor is not empty anymore, since the »trap« was installed
d1.seed = 2;
CHECK (0 == d1.fun(22)); // d1 was default initialised and thus got the "return 0" function
CHECK (d1.isInit()); // first invocation also triggered the init-routine
// is »engaged« after init and rejects move / copy
VERIFY_ERROR (LIFECYCLE, LazyDemo dx{move(d1)} );
d1 = LazyDemo{[](int i) // assign a fresh copy (discarding any state in d1)
{
return i + 1; // using a "return i+1" function
}};
CHECK (not d1.isInit());
CHECK (d1.seed == 0); // assignment indeed erased any existing settings (seed≔2)
CHECK (d1.fun);
CHECK (23 == d1.fun(22)); // new function was tied in (while also referring to self->seed)
CHECK (d1.isInit());
d1.seed = 3; // set the seed
CHECK (26 == d1.fun(22)); // seed value is picked up dynamically
VERIFY_ERROR (LIFECYCLE, LazyDemo dx{move(d1)} );
}
};
/** Register this test class... */
LAUNCHER (LazyInit_test, "unit common");
}} // namespace lib::test