lumiera_documentation/tests/test/test-tracking-test.cpp
Ichthyostega a0d83b4fe8 Library: investigate design for an opaque inline functor
This is a design study to find out what would be required
to build an opaque function wrapper, similar to `std::function`,
yet without using any heap memory ever.

Such a building block would allow for some interesting options
for a well structured design of elaborate data structures.

This changeset provides an implementation draft.

__Two important findings to note__:
 * it is impossible to get away with just a single function pointer
 * because at the very least we need to invoke the destructor of the
   embedded delegate. Thus something akin to a "manager" is required.
 * this implies that the component would not be so efficient as I hoped,
   since it requires two pointers and requires to emit code for each
   type of delegate.
 * by making the component **NonCopyable**, I avoided the complexities
   of virtual copy support. In case the latter is desired, it would be
   much simpler to switch to a classical OO approach with a VTable
   and use our `PolymorphicValue` wrapper to handle copy and cloning.
2026-04-08 01:53:53 +02:00

423 lines
18 KiB
C++
Raw Permalink 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.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
TestTracking(Test) - demonstrate test helpers for tracking automated clean-up
Copyright (C)
2024, 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 test-tracking-test.cpp
** unit test \ref TestTracking_test
*/
#include "test/run.hpp"
#include "test/test-helper.hpp"
#include "test/tracking-dummy.hpp"
#include "test/tracking-allocator.hpp"
#include "lib/allocator-handle.hpp"
#include "lib/format-cout.hpp"
#include "lib/format-util.hpp"
#include <string>
#include <utility>
using std::move;
using std::string;
using util::toString;
using util::join;
using test::TrackingAllocator;
using test::TrackingFactory;
namespace test{
namespace test{
/***************************************************//**
* @test verify proper working of test helpers to track
* automated clean-up and memory deallocation.
* @see TestHelper_test
* @see tracking-dummy.hpp
* @see tracking-allocator.hpp
*/
class TestTracking_test : public Test
{
void
run (Arg)
{
demonstrate_logObject();
demonstrate_checkObject();
demonstrate_checkAllocator();
}
/** @test capture object lifecycle events in the EventLog.
* @see EventLog_test
* @see LateBindInstance_test
*/
void
demonstrate_logObject ()
{
auto& log = Tracker::log;
log.clear (this);
Tracker alpha; // (1) create α
auto randomAlpha = toString(alpha.val);
log.event("ID",alpha.val); // (2) α has an random ID
{
Tracker beta{55}; // (3) create β
alpha = beta; // (4) assign α ≔ β
}
log.event ("ID",alpha.val); // (5) thus α now also bears the ID 55 of β
Tracker gamma = move(alpha); // (6) create γ by move-defuncting α
{
Tracker delta(23); // (7) create δ with ID 23
delta = move(gamma); // (8) move-assign δ ⟵ γ
log.event ("ID",delta.val); // (9) thus δ now bears the ID 55 (moved α ⟶ γ ⟶ δ)
CHECK (delta.val == 55);
}
log.event("ID",alpha.val); // (X) and thus α is now a zombie object
CHECK (alpha.val == Tracker::DEFUNCT);
cout << "____Tracker-Log_______________\n"
<< util::join(Tracker::log, "\n")
<< "\n───╼━━━━━━━━━━━╾──────────────"<<endl;
CHECK (log.verify("EventLogHeader").on("TestTracking_test")
.beforeCall("ctor").on(&alpha) // (1) create α
.beforeEvent("ID", randomAlpha) // (2) α has an random ID
.beforeCall("ctor").arg(55) // (3) create β
.beforeCall("assign-copy").on(&alpha).arg("Track{55}") // (4) assign α ≔ β
.beforeCall("dtor").arg(55)
.beforeEvent("ID", "55") // (5) thus α now also bears the ID 55 of β
.beforeCall("ctor-move").on(&gamma).arg("Track{55}") // (6) create γ by move-defuncting α
.beforeCall("ctor").arg(23) // (7) create δ with ID 23
.beforeCall("assign-move").arg("Track{55}") // (8) move-assign δ ⟵ γ
.beforeEvent("ID", "55") // (9) thus δ now bears the ID 55 (moved α ⟶ γ ⟶ δ)
.beforeCall("dtor").arg(55)
.beforeEvent("ID", toString(Tracker::DEFUNCT)) // (X) and thus α is now a zombie object
);
}
/** @test dummy object with a tracking checksum.
*/
void
demonstrate_checkObject ()
{
CHECK (Dummy::checksum() == 0);
{
Dummy dum1; // picks a random positive int by default...
CHECK (0 < dum1.getID() and dum1.getID() <= 100'000'000);
CHECK (Dummy::checksum() == dum1.getID());
Dummy dum2{55};
CHECK (dum2.getID() == 55);
CHECK (Dummy::checksum() == dum1.getID() + 55);
Dummy dum3{move (dum2)};
CHECK (dum3.getID() == 55);
CHECK (dum2.getID() == Dummy::DEFUNCT);
dum3.setID (23);
CHECK (dum3.getID() == 23);
dum1 = move (dum3);
CHECK (dum1.getID() == 23 );
CHECK (dum2.getID() == Dummy::DEFUNCT);
CHECK (dum3.getID() == Dummy::DEFUNCT);
CHECK (Dummy::checksum() == 23 );
Dummy::activateCtorFailure (true);
try {
Dummy kabooom;
}
catch (int v)
{
CHECK (0 < v and v <= 100'000'000);
CHECK (Dummy::checksum() == 23 + v);
Dummy::checksum() -= v;
}
Dummy::activateCtorFailure (false);
CHECK (dum1.getID() == 23 );
CHECK (dum2.getID() == Dummy::DEFUNCT);
CHECK (dum3.getID() == Dummy::DEFUNCT);
CHECK (Dummy::checksum() == 23 );
}
CHECK (Dummy::checksum() == 0);
}
/** @test custom allocator to track memory handling
* - use the base allocator to perform raw memory allocation
* - demonstrate checksum and diagnostic functions
* - use a standard adapter to create objects with `unique_ptr`
* - use as _custom allocator_ within STL containers
* - can use several distinct pools
* - swapping containers will move allocators alongside
*/
void
demonstrate_checkAllocator()
{
// setup a common event-log for the tracking objects and -allocator
auto& log = TrackingAllocator::log;
Tracker::log.clear("Tracking-Allocator-Test");
Tracker::log.joinInto(log);
// everything is safe and sound initially....
CHECK (TrackingAllocator::checksum() == 0, "Testsuite is broken");
CHECK (TrackingAllocator::use_count() == 0);
{ // Test-1 : raw allocations....
log.event("Test-1");
TrackingAllocator allo;
CHECK (TrackingAllocator::use_count() == 1);
CHECK (TrackingAllocator::numAlloc() == 0);
CHECK (TrackingAllocator::numBytes() == 0);
void* mem = allo.allocate (55);
CHECK (TrackingAllocator::numAlloc() == 1);
CHECK (TrackingAllocator::numBytes() == 55);
CHECK (allo.manages (mem));
CHECK (allo.getSize (mem) == 55); // individual registration recalls the allocation's size
HashVal memID = allo.getID (mem);
CHECK (0 < memID);
CHECK (TrackingAllocator::checksum() == memID*55);
allo.deallocate (mem, 42); // note: passing a wrong number here is marked as ERROR in the log
CHECK (not allo.manages (mem));
CHECK (allo.getSize (mem) == 0);
CHECK (allo.getID (mem) == 0);
CHECK (TrackingAllocator::use_count() == 1);
CHECK (TrackingAllocator::numAlloc() == 0);
CHECK (TrackingAllocator::numBytes() == 0);
}
CHECK (log.verify("EventLogHeader").on("Tracking-Allocator-Test")
.before("logJoin")
.beforeEvent("Test-1")
.beforeCall("allocate").on(GLOBAL).argPos(0, 55)
.beforeEvent("error", "SizeMismatch-42-≠-55")
.beforeCall("deallocate").on(GLOBAL).argPos(0, 42)
);
CHECK (TrackingAllocator::checksum() == 0);
{ // Test-2 : attach scoped-ownership-front-End
log.event("Test-2");
lib::allo::OwnUniqueAdapter<TrackingFactory> uniFab;
CHECK (sizeof(uniFab) == sizeof(TrackingFactory));
CHECK (sizeof(uniFab) == sizeof(std::shared_ptr<byte>));
CHECK (not lib::allo::is_Stateless_v<decltype(uniFab)>);
CHECK (TrackingAllocator::use_count() == 1);
CHECK (TrackingAllocator::numAlloc() == 0);
CHECK (TrackingAllocator::numBytes() == 0);
{
log.event("fabricate unique");
auto uniqueHandle = uniFab.make_unique<Tracker> (77);
CHECK (uniqueHandle);
CHECK (uniqueHandle->val == 77);
CHECK (TrackingAllocator::use_count() == 2);
CHECK (TrackingAllocator::numAlloc() == 1);
CHECK (TrackingAllocator::numBytes() == sizeof(Tracker));
// all the default tracking allocators indeed attach to the same pool
TrackingAllocator allo;
void* mem = uniqueHandle.get();
CHECK (allo.manages (mem));
HashVal memID = allo.getID (mem);
CHECK (0 < memID);
CHECK (TrackingAllocator::checksum() == memID*sizeof(Tracker));
}// and it's gone...
CHECK (TrackingAllocator::use_count() == 1);
CHECK (TrackingAllocator::numAlloc() == 0);
CHECK (TrackingAllocator::numBytes() == 0);
}
CHECK (log.verifyEvent("Test-2")
.beforeEvent("fabricate unique")
.beforeCall("allocate").on(GLOBAL).argPos(0, sizeof(Tracker))
.beforeCall("create-Tracker").on(GLOBAL).arg(77)
.beforeCall("ctor").on("Tracker").arg(77)
.beforeCall("destroy-Tracker").on(GLOBAL)
.beforeCall("dtor").on("Tracker").arg(77)
.beforeCall("deallocate").on(GLOBAL).argPos(0, sizeof(Tracker))
);
CHECK (TrackingAllocator::checksum() == 0);
// define a vector type to use the TrackingAllocator internally
using TrackVec = std::vector<Tracker, TrackAlloc<Tracker>>;
// the following pointers are used later to identify log entries...
Tracker *t1, *t2, *t3, *t4, *t5, *t6;
{ // Test-3 : use as STL allocator
log.event("Test-3");
log.event("fill with 3 default instances");
TrackVec vec1(3);
int v3 = vec1.back().val;
TrackVec vec2;
log.event("move last instance over into other vector");
vec2.emplace_back (move (vec1[2]));
CHECK (vec2.back().val == v3);
CHECK (vec1.back().val == Tracker::DEFUNCT);
// capture object locations for log verification
t1 = & vec1[0];
t2 = & vec1[1];
t3 = & vec1[2];
t4 = & vec2.front();
log.event("leave scope");
}
CHECK (log.verifyEvent("Test-3")
.beforeEvent("fill with 3 default instances")
.beforeCall("allocate").on(GLOBAL)
.beforeCall("ctor").on(t1)
.beforeCall("ctor").on(t2)
.beforeCall("ctor").on(t3)
.beforeEvent("move last instance over into other vector")
.beforeCall("allocate").on(GLOBAL)
.beforeCall("ctor-move").on(t4)
.beforeEvent("leave scope")
.beforeCall("dtor").on(t4)
.beforeCall("deallocate").on(GLOBAL)
.beforeCall("dtor").on(t1) // (problematic test? order may be implementation dependent)
.beforeCall("dtor").on(t2)
.beforeCall("dtor").on(t3)
.beforeCall("deallocate").on(GLOBAL)
);
CHECK (TrackingAllocator::checksum() == 0);
{ // Test-4 : intermingled use of several pools
log.event("Test-4");
TrackAlloc<Tracker> allo1{"POOL-1"};
TrackAlloc<Tracker> allo2{"POOL-2"};
CHECK (allo1 != allo2);
CHECK (TrackingAllocator::use_count(GLOBAL) == 0);
CHECK (TrackingAllocator::use_count("POOL-1") == 1); // referred by allo1
CHECK (TrackingAllocator::use_count("POOL-2") == 1); // referred by allo2
CHECK (TrackingAllocator::checksum ("POOL-1") == 0);
CHECK (TrackingAllocator::checksum ("POOL-2") == 0);
TrackVec vec1{allo1};
TrackVec vec2{allo2};
CHECK (TrackingAllocator::use_count("POOL-1") == 2); // now also referred by the copy in the vectors
CHECK (TrackingAllocator::use_count("POOL-2") == 2);
log.event("reserve space in vectors");
vec1.reserve(20);
vec2.reserve(2);
CHECK (TrackingAllocator::numBytes("POOL-1") == 20*sizeof(Tracker));
CHECK (TrackingAllocator::numBytes("POOL-2") == 2*sizeof(Tracker));
CHECK (TrackingAllocator::numBytes(GLOBAL) == 0);
CHECK (TrackingAllocator::numBytes() == 0);
log.event("create elements in vec1");
vec1.resize(5);
vec1.back().val = 11;
log.event("add element to vec2");
vec2.push_back (Tracker{22});
// capture object locations for log verification later
t1 = & vec1[0];
t2 = & vec1[1];
t3 = & vec1[2];
t4 = & vec1[3];
t5 = & vec1[4];
t6 = & vec2.front();
log.event ("swap vectors");
std::swap (vec1, vec2);
CHECK (vec1.back().val == 22);
CHECK (vec2.back().val == 11);
CHECK (vec1.size() == 1);
CHECK (vec2.size() == 5);
// the allocators were migrated alongside with the swap
CHECK (TrackingAllocator::numBytes("POOL-1") == 20*sizeof(Tracker));
CHECK (TrackingAllocator::numBytes("POOL-2") == 2*sizeof(Tracker));
// this can be demonstrated....
log.event ("clear the elements migrated to vec2");
vec2.clear();
vec2.shrink_to_fit();
CHECK (vec2.capacity() == 0);
CHECK (TrackingAllocator::numBytes("POOL-1") == 0 );
CHECK (TrackingAllocator::numBytes("POOL-2") == 2*sizeof(Tracker));
CHECK (vec1.size() == 1);
CHECK (vec1.capacity() == 2); // unaffected
log.event ("leave scope");
}
CHECK (log.verifyEvent("Test-4")
.beforeEvent("reserve space in vectors")
.beforeCall("allocate").on("POOL-1").argPos(0, 20*sizeof(Tracker))
.beforeCall("allocate").on("POOL-2").argPos(0, 2*sizeof(Tracker))
.beforeEvent("create elements in vec1")
.beforeCall("ctor").on(t1)
.beforeCall("ctor").on(t2)
.beforeCall("ctor").on(t3)
.beforeCall("ctor").on(t4)
.beforeCall("ctor").on(t5)
.beforeEvent("add element to vec2")
.beforeCall("ctor").arg(22)
.beforeCall("ctor-move").on(t6).arg("Track{22}")
.beforeCall("dtor").arg(Tracker::DEFUNCT)
.beforeEvent("swap vectors")
.beforeEvent("clear the elements migrated to vec2")
.beforeCall("dtor").on(t1)
.beforeCall("dtor").on(t2)
.beforeCall("dtor").on(t3)
.beforeCall("dtor").on(t4)
.beforeCall("dtor").on(t5).arg(11)
.beforeCall("deallocate").on("POOL-1").argPos(0, 20*sizeof(Tracker))
.beforeEvent("leave scope")
.beforeCall("dtor").on(t6).arg(22)
.beforeCall("deallocate").on("POOL-2").argPos(0, 2*sizeof(Tracker))
);
// everything clean and all pools empty again...
CHECK (TrackingAllocator::use_count(GLOBAL) == 0);
CHECK (TrackingAllocator::use_count("POOL-1") == 0);
CHECK (TrackingAllocator::use_count("POOL-2") == 0);
CHECK (TrackingAllocator::checksum("POOL-1") == 0);
CHECK (TrackingAllocator::checksum("POOL-2") == 0);
CHECK (TrackingAllocator::checksum(GLOBAL) == 0);
cout << "____Tracking-Allo-Log_________\n"
<< util::join(TrackingAllocator::log,"\n")
<< "\n───╼━━━━━━━━━━━━━━━━━╾────────"<<endl;
}
};
LAUNCHER (TestTracking_test, "unit common");
}} // namespace test::test