lumiera_/tests/library/split-splice-test.cpp

547 lines
22 KiB
C++
Raw Normal View History

/*
SplitSplice(Test) - verify interval splicing
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
Copyright (C)
2023, Hermann Vosseler <Ichthyostega@web.de>
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
  **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.
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
* *****************************************************************/
/** @file split-splice-test.cpp
** unit test \ref SplitSplice_test
** to verify proper working of the »SplitSplice« algorithm.
** This is a generic setup to modify a segmentation (partitioning)
** of an ordered axis; the axis is represented as a collection of _segments,_
** which are assumed to be ordered and seamless, with the start point inclusive
** and the end point exclusive (thus the start of the next segment is identical
** with the end point of the current segment).
**
** This test uses the natural number axis between -100 ... +100
** and establishes a binding for the generic algorithm with suitably rigged
** test data, to verify the algorithm properly inserts a new segment under all
** conceivable circumstances, since there are many possibilities of arrangement
** for two ordered segments of arbitrary length.
*/
#include "lib/test/run.hpp"
#include "lib/test/test-helper.hpp"
#include "lib/format-cout.hpp"
#include "lib/format-util.hpp"
#include "lib/format-string.hpp"
#include "lib/split-splice.hpp"
#include "lib/nocopy.hpp"
#include "lib/util.hpp"
2023-08-08 00:35:26 +02:00
#include <optional>
#include <utility>
#include <string>
#include <list>
namespace lib {
namespace test {
using util::_Fmt;
using util::isnil;
using util::getAdr;
using util::isSameObject;
using std::string;
using std::move;
namespace {// Test Fixture....
/**
* Test Dummy: a "segment" representing an integer interval.
* Memory management can be tracked since each instance gets a
* distinct ID number. Moreover, a Seg can be marked as "empty",
* which is visible on the `string` conversion
*/
struct Seg
: util::MoveOnly
{
int start;
int after;
bool empty;
~Seg()
{
check -= id;
if (id) --cnt;
}
Seg (int s, int a, bool nil=false)
: start{s}
, after{a}
, empty{nil}
, id{++idGen}
{
++cnt;
check += id;
}
/** create a clone, but modify bounds */
Seg (Seg const& ref, int s, int a)
: start{s}
, after{a}
, empty{ref.empty}
, id{ref.id}
{
++cnt;
check += id;
}
/** move-init: causes source-ref to be invalidated */
Seg (Seg&& rr)
: start{rr.start}
, after{rr.after}
, empty{rr.empty}
, id{0}
{
std::swap (id, rr.id);
}//transfer identity
operator string() const
{
return _Fmt{"[%i%s%i["}
% start
% (empty? "~":"_")
% after
;
}
//-- Diagnostics --
uint id;
static uint idGen;
static size_t cnt;
static size_t check;
};
// Storage for static ID-Generator
size_t Seg::check{0};
size_t Seg::cnt{0};
uint Seg::idGen{0};
const int SMIN = -100;
const int SMAX = +100;
/**
* Test-Segmentation comprised of a sequence of Seg entries.
* It can be checked for _validity_ according to the following conditions
* - the segmentation spans the complete range -100 ... +100
* - segments follow each other _seamlessly_
* - the bounds within each segment are ordered ascending
* - segments are not empty (i.e. start differs from end)
* The assessment of this validity conditions is appended on the string conversion.
*/
struct SegL
: std::list<Seg>
{
SegL (std::initializer_list<int> breaks)
{
int p = SMIN;
bool bound = true;
for (int b : breaks)
{
emplace_back (p,b, bound);
bound = false;
p = b;
}
emplace_back(p,SMAX, true);
}
SegL()
: SegL({})
{ }
// using standard copy
operator string() const
{
return renderContent() + assess();
}
bool
isValid() const
{
return isnil (this->assess());
}
string
renderContent() const
{
return ""+util::join(*this,"")+"";
}
string
assess() const
{
string diagnosis;
if (empty())
diagnosis = "!empty!";
else
{
if (front().start != SMIN)
diagnosis += "missing-lower-bound!";
if (back().after != SMAX)
diagnosis += "missing-upper-bound!";
int p = SMIN;
for (auto const& s : *this)
{
if (s.start != p)
diagnosis += _Fmt{"!gap_%i<>%i_!"} % p % s.start;
if (s.start == s.after)
diagnosis += _Fmt{"!degen_%i_!"} % s.start;
if (s.start > s.after)
diagnosis += _Fmt{"!order_%i>%i_!"} % s.start % s.after;
p = s.after;
}
}
return diagnosis;
}
};
/* ======= Split/Splice-Algo Setup ======= */
using OptInt = std::optional<int>;
using Iter = typename SegL::iterator;
/**
* Perform the »SplitSplice« Algorithm to splice a new Segment
* into the given [segmentation of the integer-axis](\ref SegL).
* A local λ-binding is setup to define the basic operations
* required by the algorithm implementation to work with this
* specific kind of data.
* @return Tuple `(s,n,e)` to indicate where changes happened
* - s the first changed element
* - n the new main segment (may be identical to s)
* - e the first unaltered element after the changed range (may be `end()`)
* @see lib::splitsplice::Algo
* @see [setup for the Fixture-Segmentation](\ref steam::fixture::Segmentation::splitSplice)
*/
auto
invokeSplitSplice (SegL& segs, OptInt startNew, OptInt afterNew)
{
/*---configure-contextually-bound-Functors--------------------*/
auto getStart = [](Iter elm) -> int { return elm->start; };
auto getAfter = [](Iter elm) -> int { return elm->after; };
auto createSeg= [&](Iter pos, int start, int after) -> Iter { return segs.emplace (pos, start, after); };
auto emptySeg = [&](Iter pos, int start, int after) -> Iter { return segs.emplace (pos, start, after, true); };
auto cloneSeg = [&](Iter pos, int start, int after, Iter src) -> Iter { return segs.emplace (pos, *src, start, after); };
auto discard = [&](Iter pos, Iter after) -> Iter { return segs.erase (pos,after); };
lib::splitsplice::Algo splicer{ getStart
, getAfter
, createSeg
, emptySeg
, cloneSeg
, discard
, SMAX
, segs.begin(),segs.end()
, startNew, afterNew
};
splicer.determineRelations();
return splicer.performSplitSplice();
}
}//(End)Test Fixture
/****************************************************************************//**
* @test verify proper working of a generic procedure to splice an interval
* into a complete segmentation of an ordered axis into seamless intervals.
* - demonstrate how to setup the invocation with custom data types
* - systematic coverage of all possible arrangements of intervals
* - handling of irregular cases
* @see split-splice.hpp
* @see steam::fixture::Segmentation::splitSplice
*/
class SplitSplice_test : public Test
{
virtual void
run (Arg)
{
demonstrate_usage();
verify_testFixture();
verify_standardCases();
verify_cornerCases();
verify_integrity();
// no memory leaked
CHECK (0 == Seg::check);
CHECK (0 == Seg::cnt);
}
/**
* @test demonstrate how to use the »Split-Splice« algorithm with custom data
*/
void
demonstrate_usage()
{
SegL segmentation;
CHECK (segmentation == "├[-100~100[┤"_expect);
OptInt startNew{5},
afterNew{23};
invokeSplitSplice (segmentation, startNew, afterNew);
// The given segmentation was modified by side-effect
// - a new segment [5...23[ has been inserted in the middle
// - suitably adapted empty predecessor and successor segments
CHECK (segmentation == "├[-100~5[[5_23[[23~100[┤"_expect);
// The modified segmentation still seamlessly covers the whole axis
CHECK (segmentation.isValid());
}
/** @test verify the fixture and self-diagnostics for this test */
void
verify_testFixture()
{
CHECK (0 == Seg::check);
Seg::idGen = 0;
{
Seg x{1,3}; // a segment 1 (inclusive) to 3 (exclusive)
Seg u{2,4,true}; // an "empty" segment 2 (incl) to 4 (excl)
CHECK (x == "[1_3["_expect);
CHECK (u == "[2~4["_expect); // "empty" interval is marked with '~' in string stylisation
CHECK (3 == Seg::check);
CHECK (2 == Seg::cnt);
Seg z{move(u)};
CHECK (z == "[2~4["_expect);
CHECK (3 == Seg::check);
CHECK (2 == Seg::cnt); // the "dead" instance u is not counted
CHECK (0 == u.id); // (its ID has been reset to zero in move-ctor)
CHECK (2 == z.id);
SegL l1; // default ctor always adds an empty base segment -100 ... +100
SegL l2{3};
SegL l3{5,-5,10};
CHECK (l1 == "├[-100~100[┤"_expect);
CHECK (l2 == "├[-100~3[[3~100[┤"_expect);
CHECK (l3 == "├[-100~5[[5_-5[[-5_10[[10~100[┤!order_5>-5_!"_expect);
CHECK (l1.isValid());
CHECK (l2.isValid());
CHECK (not l3.isValid()); // l3 violates validity condition, because segment [5 ... -5[ is reversed
CHECK (l3.assess() == "!order_5>-5_!"_expect);
CHECK ( 9 == Seg::cnt ); // 9 objects are alive
CHECK ( 9 == Seg::idGen); // ID generator sticks at 9
CHECK (45 == Seg::check); // checksum 1+..+9
l3.erase(l3.begin());
CHECK (l3.assess() == "missing-lower-bound!!gap_-100<>5_!!order_5>-5_!"_expect);
CHECK ( 8 == Seg::cnt ); // also one object less alive
l3.begin()->after = 5; // manipulate first segment to make it degenerate (empty
CHECK (l3.renderContent() == "├[5_5[[-5_10[[10~100[┤"_expect);
CHECK (l3.assess() == "missing-lower-bound!!gap_-100<>5_!!degen_5_!!gap_5<>-5_!"_expect);
l3.clear();
CHECK (l3.assess() == "!empty!"_expect);
CHECK ( 5 == Seg::cnt );
CHECK ( 9 == Seg::idGen);
CHECK (15 == Seg::check);
}
// all objects go out of scope
CHECK (0 == Seg::cnt );
CHECK (0 == Seg::check);
CHECK (9 == Seg::idGen);
}
/**
* @test cover all possible cases of splicing an interval
*/
void
verify_standardCases()
{
auto testCase = [](SegL segmentation
,int startNew
,int afterNew
,ExpectString expectedResult)
{
OptInt startSpec{startNew},
afterSpec{afterNew};
invokeSplitSplice (segmentation, startSpec, afterSpec);
CHECK (segmentation == expectedResult);
CHECK (segmentation.isValid());
};
////////
testCase (SegL{}, -23,24, "├[-100~-23[[-23_24[[24~100[┤"_expect); // simple segment into empty axis
// insert smaller segment
testCase (SegL{5,10}, 2,3, "├[-100~2[[2_3[[3~5[[5_10[[10~100[┤"_expect); // smaller segment left spaced off
testCase (SegL{5,10}, 4,5, "├[-100~4[[4_5[[5_10[[10~100[┤"_expect); // left adjacent
testCase (SegL{5,10}, 4,8, "├[-100~4[[4_8[[8_10[[10~100[┤"_expect); // left overlapping
testCase (SegL{5,10}, 5,8, "├[-100~5[[5_8[[8_10[[10~100[┤"_expect); // left inside justified
testCase (SegL{5,10}, 6,8, "├[-100~5[[5_6[[6_8[[8_10[[10~100[┤"_expect); // smaller segment complete inside
testCase (SegL{5,10}, 7,10, "├[-100~5[[5_7[[7_10[[10~100[┤"_expect); // right inside justified
testCase (SegL{5,10}, 9,13, "├[-100~5[[5_9[[9_13[[13~100[┤"_expect); // right overlapping
testCase (SegL{5,10}, 10,13, "├[-100~5[[5_10[[10_13[[13~100[┤"_expect); // right adjacent
testCase (SegL{5,10}, 13,23, "├[-100~5[[5_10[[10~13[[13_23[[23~100[┤"_expect); // right spaced off
// insert identical segment
testCase (SegL{5,10}, 5,10, "├[-100~5[[5_10[[10~100[┤"_expect); // identical size replacement
// insert larger segment
testCase (SegL{5,10}, 3,10, "├[-100~3[[3_10[[10~100[┤"_expect); // larger segment right aligned
testCase (SegL{5,10}, 3,23, "├[-100~3[[3_23[[23~100[┤"_expect); // larger segment overarching
testCase (SegL{5,10}, 5,23, "├[-100~5[[5_23[[23~100[┤"_expect); // larger segment left aligned
} //////
/**
* @test cover special and boundary cases
*/
void
verify_cornerCases()
{
auto testCase = [](SegL segmentation
,OptInt startNew // Note: these are optional...
,OptInt afterNew
,ExpectString expectedResult)
{
invokeSplitSplice (segmentation, startNew, afterNew);
CHECK (segmentation == expectedResult);
CHECK (segmentation.isValid());
};
auto x = std::nullopt;
//////
testCase (SegL{}, 3,2, "├[-100~2[[2_3[[3~100[┤"_expect); // flipped interval spec is reoriented
//////
testCase (SegL{}, 3,x, "├[-100~3[[3_100[┤"_expect); // expanded until domain end
testCase (SegL{}, x,5, "├[-100_5[[5~100[┤"_expect); // expanded to start of domain
/////
testCase (SegL{4,6}, 5,x, "├[-100~4[[4_5[[5_6[[6~100[┤"_expect); // expanded until end of enclosing segment
testCase (SegL{4,6}, x,5, "├[-100~4[[4_5[[5_6[[6~100[┤"_expect); // expanded to start of enclosing segment
/////
testCase (SegL{4,6}, 3,x, "├[-100~3[[3_4[[4_6[[6~100[┤"_expect); // expanded to fill gap to next segment
testCase (SegL{4,6}, x,3, "├[-100_3[[3~4[[4_6[[6~100[┤"_expect); // expanded to cover predecessor completely
testCase (SegL{4,6}, 4,x, "├[-100~4[[4_6[[6~100[┤"_expect); // expanded to cover (replace) successor
testCase (SegL{4,6}, x,4, "├[-100_4[[4_6[[6~100[┤"_expect); // expanded to cover (replace) predecessor
/////
testCase (SegL{4,6}, 7,x, "├[-100~4[[4_6[[6~7[[7_100[┤"_expect); // shorten successor and expand new segment to end of successor (=domain end)
testCase (SegL{4,6}, x,7, "├[-100~4[[4_6[[6_7[[7~100[┤"_expect); // fill gap between predecessor and given new segment end
testCase (SegL{4,6}, 6,x, "├[-100~4[[4_6[[6_100[┤"_expect); // expand to cover (replace) the following segment until domain end
testCase (SegL{4,6}, x,6, "├[-100~4[[4_6[[6~100[┤"_expect); // expanded to cover (replace) the preceding segment
/////
testCase (SegL{}, x,x, "├[-100_100[┤"_expect); // without any specification, the whole domain is covered
testCase (SegL{4}, x,x, "├[-100~4[[4_100[┤"_expect); // otherwise, without any spec the last segment is replaced
testCase (SegL{4,6}, x,x, "├[-100~4[[4_6[[6_100[┤"_expect);
/////
testCase (SegL{4,5,6,8}, 3,6, "├[-100~3[[3_6[[6_8[[8~100[┤"_expect); // spanning and thus replacing multiple segments
testCase (SegL{4,5,6,8}, 4,6, "├[-100~4[[4_6[[6_8[[8~100[┤"_expect);
testCase (SegL{4,5,6,8}, 4,7, "├[-100~4[[4_7[[7_8[[8~100[┤"_expect);
testCase (SegL{4,5,6,8}, 3,7, "├[-100~3[[3_7[[7_8[[8~100[┤"_expect);
testCase (SegL{4,5,6,8}, 3,8, "├[-100~3[[3_8[[8~100[┤"_expect);
testCase (SegL{4,5,6,8}, 4,8, "├[-100~4[[4_8[[8~100[┤"_expect);
testCase (SegL{4,5,6,8}, 4,9, "├[-100~4[[4_9[[9~100[┤"_expect);
testCase (SegL{4,5,6,8}, 5,9, "├[-100~4[[4_5[[5_9[[9~100[┤"_expect);
testCase (SegL{4,5,6,8}, 5,x, "├[-100~4[[4_5[[5_6[[6_8[[8~100[┤"_expect);
testCase (SegL{4,5,7,8}, x,6, "├[-100~4[[4_5[[5_6[[6_7[[7_8[[8~100[┤"_expect);
} /////
/**
* @test verify instance data is properly handled on _split operation_
* - segments not touched will stay at same memory location
* - any adapted segments will be new allocated objects
* - yet segments cloned / split will carry on all data
*/
void
verify_integrity()
{
SegL segs{2,6};
CHECK (segs == "├[-100~2[[2_6[[6~100[┤"_expect);
Iter s = segs.begin();
CHECK (s->start == -100);
CHECK (s->after == 2);
uint id1 = s->id;
void* adr1 = &(*s);
++s;
CHECK (s->start == 2);
CHECK (s->after == 6);
uint id2 = s->id;
void* adr2 = &(*s);
++s;
CHECK (s->start == 6);
CHECK (s->after == 100);
uint id3 = s->id;
void* adr3 = &(*s);
auto [p,n,a] = invokeSplitSplice (segs, 3,4);
CHECK (5 == segs.size());
CHECK (segs == "├[-100~2[[2_3[[3_4[[4_6[[6~100[┤"_expect);
s = segs.begin();
CHECK (s->start == -100);
CHECK (s->after == 2);
CHECK (s->id == id1);
CHECK (adr1 == getAdr(*s));
CHECK (s != p);
++s;
CHECK (s == p);
CHECK (s->start == 2);
CHECK (s->after == 3);
CHECK (s->id == id2);
CHECK (adr2 != getAdr(*s)); // this is the first part of the split segment (new allocation)
++s;
CHECK (s != p);
CHECK (s == n);
CHECK (s->start == 3);
CHECK (s->after == 4);
CHECK (s->id != id1);
CHECK (s->id != id2);
CHECK (s->id != id3);
CHECK (adr2 != getAdr(*s));
++s;
CHECK (s != n);
CHECK (s != a);
CHECK (s->start == 4);
CHECK (s->after == 6);
CHECK (s->id != id1);
CHECK (s->id == id2);
CHECK (s->id != id3);
CHECK (adr2 != getAdr(*s)); // this is the second part of the split segment (new allocation)
++s;
CHECK (s == a);
CHECK (s->start == 6);
CHECK (s->after == 100);
CHECK (s->id != id1);
CHECK (s->id != id2);
CHECK (s->id == id3);
CHECK (adr3 == getAdr(*s));
++s;
CHECK (s == segs.end());
}
};
LAUNCHER (SplitSplice_test, "unit common");
}} // namespace lib::test