LUMIERA.clone/tests/lib/scoped-collection-test.cpp
Ichthyostega 7448b81d49 Library: improve initial population of ScopedCollection
This is a collection with unique-ownership,
and the ability to create all content objects in ''RAII style''
from the constructor call, using a ''population functor''.

It is often used to maintain a collection of resource handles,
and especially to start a series of threads (for testing).

With the variadic λ-captures from C++20, it is possible to rewrite
the most frequently used population functors to be easier to use.
Notably we can now bind constructor arguments, that will be passed
to each of the created elements — a prominent example being the
thread function when launching several threads. Furthermore,
I added a constructor variant that consumes a Lumiera Iterator.
Adding this kind of generic variants is possible now with C++20,
since overload resolution takes concepts into account.
2026-05-14 22:46:06 +02:00

497 lines
16 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.

/*
ScopedCollection(Test) - holding and owning a fixed collection of noncopyable objects
Copyright (C)
2012, 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 scoped-collection-test.cpp
** unit test \ref ScopedCollection_test
*/
#include "test/run.hpp"
#include "test/test-helper.hpp"
#include "test/tracking-dummy.hpp"
#include "lib/scoped-collection.hpp"
#include "lib/util.hpp"
using test::Dummy;
namespace lib {
namespace test{
namespace error = lumiera::error;
namespace { // our explosive special Dummy
LUMIERA_ERROR_DEFINE(SUBVERSIVE, "undercover action");
class SubDummy
: public Dummy
{
int trigger_;
/** special variant of the dummy API operation:
* @param i when zero, the trigger value will be revealed
*/
virtual long
calc (int i)
{
if (!i)
return getID() + trigger_;
else
return Dummy::calc(i);
}
public:
SubDummy (int id, int trigger)
: Dummy(id)
, trigger_(trigger)
{
if (trigger == getID())
throw error::Fatal ("Subversive Bomb", LUMIERA_ERROR_SUBVERSIVE);
}
SubDummy()
: Dummy()
, trigger_(-1)
{ }
};
inline uint
sum (uint n)
{
return n*(n+1) / 2;
}
}//(End) subversive test data
using util::isnil;
using LERR_(ITER_EXHAUST);
typedef ScopedCollection<Dummy, sizeof(SubDummy)> CollD;
/****************************************************************//**
* @test ScopedCollection manages a fixed set of objects, but these
* child objects are noncopyable, may be polymorphic, an can
* be created either all at once or chunk wise. The API is
* similar to a vector and allows for element access
* and iteration.
*/
class ScopedCollection_test : public Test
{
virtual void
run (Arg)
{
seedRand();
simpleUsage();
building_RAII_Style();
building_StackStyle();
iterating();
verify_defaultPopulator();
verify_iteratorPopulator();
verify_embeddedCollection();
}
void
simpleUsage()
{
CHECK (0 == Dummy::checksum());
{
CollD container(5);
CHECK (isnil (container));
CHECK (0 == container.size());
CHECK (0 == Dummy::checksum());
container.populate();
CHECK (!isnil (container));
CHECK (5 == container.size());
CHECK (0 != Dummy::checksum());
container.clear();
CHECK (isnil (container));
CHECK (0 == container.size());
CHECK (0 == Dummy::checksum());
container.populate();
CHECK (Dummy::checksum() == container[0].getID()
+ container[1].getID()
+ container[2].getID()
+ container[3].getID()
+ container[4].getID());
}
CHECK (0 == Dummy::checksum());
}
void
iterating()
{
CHECK (0 == Dummy::checksum());
{
CollD coll(50);
for (uint i=0; i<coll.capacity(); ++i)
coll.emplace<Dummy>(i);
int check=0;
CollD::iterator ii = coll.begin();
while (ii)
{
CHECK (check == ii->getID());
CHECK (check == ii->calc(+5) - 5);
++check;
++ii;
}
// Test the const iterator
CollD const& const_coll (coll);
check = 0;
CollD::const_iterator cii = const_coll.begin();
while (cii)
{
CHECK (check == cii->getID());
++check;
++cii;
}
// Test c++11 foreach iteration
check = 0;
for (auto& entry : coll)
{
CHECK (check == entry.getID());
++check;
}
check = 0;
for (auto const& entry : const_coll)
{
CHECK (check == entry.getID());
++check;
}
// Verify correct behaviour of iteration end
CHECK (not bool(coll.end()));
CHECK (isnil (coll.end()));
VERIFY_ERROR (ITER_EXHAUST, *coll.end() );
VERIFY_ERROR (ITER_EXHAUST, ++coll.end() );
CHECK (ii == coll.end());
CHECK (cii == coll.end());
VERIFY_ERROR (ITER_EXHAUST, ++ii );
VERIFY_ERROR (ITER_EXHAUST, ++cii );
}
CHECK (0 == Dummy::checksum());
}
/** @test using the ScopedCollection to hold a variable
* and possibly increasing number of elements, within the
* fixed limits of the maximum capacity defined by the
* ctor parameter. Any new elements will be created
* behind the already existing objects. In case
* of failure while creating an element, only
* this element gets destroyed, the rest of
* the container remains intact.
*/
void
building_StackStyle()
{
CHECK (0 == Dummy::checksum());
{
int rr = rani(100);
CollD coll(3);
CHECK (0 == coll.size());
CHECK (0 == Dummy::checksum());
Dummy& d0 = coll.emplaceElement();
CHECK (1 == coll.size());
Dummy& d1 = coll.emplace<Dummy> (rr);
CHECK (2 == coll.size());
int sum = Dummy::checksum();
// trigger the bomb
VERIFY_ERROR (SUBVERSIVE, coll.emplace<SubDummy>(rr,rr) );
CHECK ( 2 == coll.size()); // the other objects survived
CHECK (sum == Dummy::checksum());
Dummy& d2 = coll.emplace<SubDummy> (rr, rr+1);
CHECK (3 == coll.size());
CHECK (sum + rr == Dummy::checksum());
VERIFY_ERROR (CAPACITY, coll.emplaceElement());
VERIFY_ERROR (CAPACITY, coll.emplaceElement());
VERIFY_ERROR (CAPACITY, coll.emplaceElement());
CHECK (3 == coll.size());
CHECK (sum + rr == Dummy::checksum());
CHECK (d0.calc(11) == coll[0].getID() + 11 );
CHECK (d1.calc(22) == rr + 22);
CHECK (d2.calc(33) == rr + 33);
CHECK (d2.calc(0) == rr + (rr+1) ); // SubDummy's special implementation of the acc()-function
// returns the trigger value, when the argument is zero
coll.clear();
coll.emplace<SubDummy> (11,22);
CHECK ( 1 == coll.size());
CHECK (11 == Dummy::checksum());
// NOTE DANGEROUS:
// The previously obtained references just point into the object storage.
// Thus we're now accessing a different object, even a different type!
CHECK (d0.calc(0) == 11 + 22);
// The others even point into obsoleted storage holding zombie objects
CHECK (d1.getID() == Dummy::DEAD);
}
CHECK (0 == Dummy::checksum());
}
/** @test using the ScopedCollection according to the RAII pattern.
* For this usage style, the collection is filled right away, during
* construction. If anything goes wrong, the whole collection is
* cleared and invalidated. Consequently there is no tangible "lifecycle"
* at the usage site. Either the collection is fully usable, or not at all.
* This requires the client to provide a functor (callback) to define
* the actual objects to be created within the ScopedCollection. These
* may as well be subclasses of the base type I, provided the general
* element storage size #siz was chosen sufficiently large to hold
* those subclass instances.
*
* This test demonstrates the most elaborate usage pattern, where
* the client provides a full blown functor object #Populator,
* which even has embedded state. Generally speaking, anything
* exposing a suitable function call operator is acceptable.
*/
void
building_RAII_Style()
{
CHECK (0 == Dummy::checksum());
{
int rr = rani(100);
int trigger = 100 + 5 + 1; // prevents the bomb from exploding (since rr < 100)
CollD coll (6, Populator(rr, trigger));
CHECK (not isnil (coll));
CHECK (6 == coll.size());
CHECK (0 != Dummy::checksum());
CHECK (coll[0].calc(0) == 0 + rr);
CHECK (coll[1].calc(0) == 1 + rr + trigger);
CHECK (coll[2].calc(0) == 2 + rr);
CHECK (coll[3].calc(0) == 3 + rr + trigger);
CHECK (coll[4].calc(0) == 4 + rr);
CHECK (coll[5].calc(0) == 5 + rr + trigger);
// what does this check prove?
// - the container was indeed populated with SubDummy objects
// since the overridden version of Dummy::acc() did run and
// reveal the trigger value
// - the population was indeed done with the anonymous Populator
// instance fed to the ctor, since this object was "marked" with
// the random value rr, and adds this mark to the built values.
coll.clear();
CHECK (0 == Dummy::checksum());
// Verify Error handling while in creation:
// SubDummy explodes on equal ctor parameters
// which here happens for i==7
VERIFY_ERROR (SUBVERSIVE, CollD(10, Populator(0, 7)) );
// any already created object was properly destroyed
CHECK (0 == Dummy::checksum());
}
CHECK (0 == Dummy::checksum());
}
/** Functor to populate the Collection */
class Populator
{
uint i_;
int off_;
int trigg_;
public:
Populator (int baseOffset, int triggerCode)
: i_(0)
, off_(baseOffset)
, trigg_(triggerCode)
{ }
void
operator() (CollD::ElementHolder& storage)
{
switch (i_ % 2)
{
case 0:
storage.create<Dummy> (i_+off_);
break;
case 1:
storage.create<SubDummy> (i_+off_, trigg_);
break;
}
++i_;
}
};
/** @test for using ScopedCollection in RAII style,
* several pre-defined "populators" are provided.
* The most obvious one being just to fill the
* collection with default constructed objects.
*/
void
verify_defaultPopulator()
{
CHECK (0 == Dummy::checksum());
CollD coll (25, CollD::fill() );
CHECK (not isnil (coll));
CHECK (25 == coll.size());
CHECK (0 != Dummy::checksum());
for (CollD::iterator ii = coll.begin(); ii; ++ii)
{
CHECK ( INSTANCEOF (Dummy, & (*ii)));
CHECK (!INSTANCEOF (SubDummy, & (*ii)));
}
}
/** @test use a "populator" that creates a subclass and binds arguments */
void
verify_subclassPopulator()
{
CHECK (0 == Dummy::checksum());
const uint arg{42};
CollD coll (25, CollD::fillWith<SubDummy> (arg, 55) );
CHECK (not isnil (coll));
CHECK (25 == coll.size());
CHECK (0 != Dummy::checksum());
for (CollD::iterator ii = coll.begin(); ii; ++ii)
CHECK (INSTANCEOF (SubDummy, & (*ii)));
}
/** @test verify dynamic pulling of elements
* - the first step uses a "populator" that invokes a λ
* - the second step then uses the source container's iterator,
* which happens to be a _Lumiera Forward Iterator_ to pull
* copies into a second container.
* - even while the second container is dimensioned to be larger,
* only the number of elements yielded by the iterator are created.
*/
void
verify_iteratorPopulator()
{
typedef ScopedCollection<uint> CollI;
CollI source (20, CollI::invoke([i=0] mutable { return i++; }));
// filled with numbers 0..19
CollI coll (30, source.begin()); // this immediately pulls in the first 20 elements
CHECK (not isnil (coll));
CHECK (20 == coll.size());
CHECK (20 == source.size());
CHECK (30 == coll.capacity());
for (uint i=0; i < coll.size(); ++i)
{
CHECK (coll[i] == i );
CHECK (coll[i] == source[i]);
}
}
/** @test simulate the typical situation of a manager
* owning some embedded components. Here, our ManagerDemo
* instance owns a collection of numbers 50..1. They are
* created right while initialising the manager, and this
* initialisation is done by invoking a member function
* of the manager
*/
void
verify_embeddedCollection()
{
ManagerDemo object_with_embedded_Collection(50);
CHECK (sum(50) == object_with_embedded_Collection.useMyNumbers());
}
class ManagerDemo
{
typedef ScopedCollection<uint> CollI;
uint memberVar_;
const CollI my_own_Numbers_;
void
buildNumbers (CollI::ElementHolder& storage)
{
storage.create<uint>(memberVar_);
--memberVar_;
}
public:
ManagerDemo(uint cnt)
: memberVar_(cnt)
, my_own_Numbers_(cnt, &ManagerDemo::buildNumbers, this)
{
CHECK (0 == memberVar_);
CHECK (cnt == my_own_Numbers_.size());
}
uint
useMyNumbers()
{
uint sum(0);
for (CollI::const_iterator ii = my_own_Numbers_.begin(); ii; ++ii)
sum += *ii;
return sum;
}
};
};
LAUNCHER (ScopedCollection_test, "unit common");
}} // namespace lib::test