tree-diff-application: unit test PASS

well... this was quite a piece of work
Added some documentation, but a complete documentation,
preferably to the website, would be desirable, as would
be a more complete test covering the negative corner cases
This commit is contained in:
Fischlurch 2015-11-01 07:03:47 +01:00
parent eb829e6994
commit 34d79ee8df
5 changed files with 236 additions and 75 deletions

View file

@ -40,16 +40,49 @@
** a switch from scope to scope, which adds a lot of complexity. So the list diff
** application strategy can be seen as blueprint and demonstration of principles.
**
** Another point in question is weather see the diff application as manipulating
** a target data structure, or rather building a reshaped copy. The fact that
** GenNode and Record are designed as immutable values seems to favour the latter,
** yet the very reason to engage into building this diff framework was how to
** handle partial updates within a expectedly very large UI model, reflecting
** Another point in question is weather to treat the diff application as
** manipulating a target data structure, or rather building a reshaped copy.
** The fact that GenNode and Record are designed as immutable values seems to favour
** the latter, yet the very reason to engage into building this diff framework was
** how to handle partial updates within a expectedly very large UI model, reflecting
** the actual session model in Proc-Layer. So we end up working on a Mutator,
** which clearly signals we're going to reshape and re-rig the target data.
**
** @see diff-list-application-test.cpp
** @see VerbToken
** \par State and nested scopes
** Within the level of a single #Record, our tree diff language works similar to
** the list diff (with the addition of the \c after(ID) verb, which is just a
** shortcut to accept parts of the contents unaltered). But after possibly rearranging
** the contents of an "object" (Record), the diff might open some of its child "objects"
** by entering a nested scope. This is done with the \c mut(ID)....emu(ID) bracketing
** construct. On the implementation side, this means we need to use a stack somehow.
** The decision was to manage this stack explicitly, as a std::stack (heap memory).
** Each entry on this stack is a "context frame" for list diff. Which makes the
** tree diff applicator a highly statefull component.
**
** Even more so, since -- for \em performance reasons -- we try to alter the
** tree shaped data structure \em in-place. We want to avoid the copy of possibly
** deep sub-trees, when in the end we might be just rearranging their sequence order.
** This design decision comes at a price tag though
** - it subverts the immutable nature of \c Record<GenNode> and leads to
** high dependency on data layout and implementation details of the latter.
** This is at least prominently marked by working on a diff::Record::Mutator,
** so the client has first to "open up" the otherwise immutable tree
** - the actual list diff on each level works by first \em moving the entire
** Record contents away into a temporary buffer and then \em moving them
** back into new shape one by one. In case of a diff conflict (i.e. a
** mismatch between the actual data structure and the assumptions made
** for the diff message on the sender / generator side), an exception
** is thrown, leaving the client with a possibly corrupted tree, where
** parts might even still be stashed away in the temporary buffer,
** and thus be lost.
** We consider this unfortunate, yet justified by the very nature of applying a diff.
** When the user needs safety or transactional behaviour, a deep copy should be made
** before attaching the #DiffApplicator
**
** @see DiffTreeApplication_test
** @see DiffListApplication_test
** @see GenNodeBasic_test
** @see tree-diff.hpp
**
*/
@ -169,7 +202,7 @@ namespace diff{
throw error::State(_Fmt("Unable locate position 'after(%s)'") % elm.idi
, LUMIERA_ERROR_DIFF_CONFLICT);
}
void
__expect_valid_parent_scope (GenNode::ID const& idi)
{

View file

@ -27,12 +27,29 @@
** this building block defines the set of operations to express both structural
** and content changes in a given data structure.
**
** @todo UNIMPLEMENTED as of 12/14
** This »tree diff language« does not rely on any concrete data structure or layout,
** just on some general assumptions regarding the ordering and structure of the data.
** - top level is a root record
** - a record has a type, a collection of named attributes, and a collection of children
** - all elements within a record are conceived as elements in ordered sequence, with the
** attributes first, followed by the children. The end of the attribute scope is given
** by the the appearance of the first unnamed entry, i.e the first child.
** - the individual elements in these sequences have a distinguishable identity and
** optionally a name (a named element is an attribute).
** - moreover, the elements carry a typed payload data element, which possibly is
** a \em nested record ("nested child object").
** - the typing of the elements is outside the scope of the diff language; it is
** assumed that the receiver knows what types to expect and how to deal with them.
** - only nested records may be \em mutated by the diff. All other elements can
** only be inserted, moved or deleted (like elements in list diff)
** By implementing the #TreeDiffInterpreter interface (visitor), a concrete usage
** can receive a diff description and possibly apply it to suitable target data.
**
** @see diff-language.cpp
** @see diff-tree-application-test.cpp
** @see tree-diff.cpp
** @see GenNode
** @see tree-diff-application.cpp
** @see DiffTreeApplication_test
** @see list-diff.cpp
** @see diff::GenNode
**
*/
@ -54,9 +71,52 @@ namespace diff{
* Interpreter interface to define the operations ("verbs"),
* which describe differences or changes in hierarchical data structure.
* The meaning of the verbs is as follows:
* - \c TODO
*
* @todo to be defined
* - \c ins prompts to insert the given argument element at the \em current
* processing position into the target sequence. This operation
* allows to inject new data
* - \c del requires to delete the \em next element at \em current position.
* For sake of verification, the ID of the argument payload is
* required to match the ID of the element about to be discarded.
* - \c pick just accepts the \em next element at \em current position into
* the resulting altered sequence. Again, the ID of the argument
* has to match the ID of the element to be picked, for sake
* of verification.
* - \c find effect a re-ordering of the target scope contents: it requires
* to \em search for the (next respective single occurrence of the)
* given element further down into the remainder of the current
* record scope (but not into nested child scopes). The designated
* element is to be retrieved and inserted as the next element
* at current position.
* - \c skip processing hint, emitted at the position where an element
* previously extracted by a \c find verb happened to sit within
* the old order. This allows an optimising implementation to fetch
* a copy and just drop or skip the original, thereby avoiding to
* shift any other elements.
* - \c after shortcut to \c pick existing elements up to the designated point.
* As a special notation, \c after(Ref::ATTRIBUTES) allows to fast forward
* to the first child element, while \c after(Ref::END) means to accept
* all of the existing data contents as-is (possibly to append further
* elements after that point.
* - \c mut bracketing construct to open a nested sub scope. The element
* designated by the ID of the argument needs to be a #Record
* ("nested child object"). Moreover, this element must have been
* mentioned with the preceding diff verbs at that level, which means
* that the element as such must already be present in the altered
* target structure. The \c mut(ID) verb then opens this nested
* record for diff handling, and all subsequent diff verbs are to be
* interpreted relative to this scope, until the corresponding
* \c emu(ID) verb is encountered. As a special notation, right
* after handling an element with the list diff verbs (i.e. \c ins
* or \c pick or \c find), it is allowed immediately to open the
* nested scope with \c mut(Ref::THIS) -- which circumvents the
* problem that it is sometimes difficult to know the precise ID,
* especially when hand-writing a diff to populate a data structure.
* - \c emu bracketing construct and counterpart to \c mut(ID). This verb
* must be given precisely at the end of the nested scope (it is
* not allowed to "return" from the middle of a scope, for sake
* of sanity). At this point, ths child scope is left and the
* parent scope with all existing diff state is popped from an
* internal stack
*/
class TreeDiffInterpreter
{

View file

@ -158,6 +158,11 @@ return: 0
END
TEST "Diff: reshape a tree data structure through diff" DiffTreeApplication_test <<END
return: 0
END
TEST "A Digxel (numeric component)" Digxel_test <<END
out-lit: empty____## +0.0 ##
out-lit: value____##-88.8 ##

View file

@ -51,13 +51,13 @@ namespace test{
const GenNode ATTRIB1("α", 1), // attribute α = 1
ATTRIB2("β", 2L), // attribute α = 2L (int64_t)
ATTRIB3("γ", 3.45), // attribute γ = 3.45 (double)
TYPE_X("type", "X"), // a "magic" type attribute "X"
TYPE_X("type", "X"), // a "magic" type attribute "X"
TYPE_Y("type", "Y"), //
CHILD_A("a"), // unnamed string child node
CHILD_B('b'), // unnamed char child node
CHILD_T(Time(12,34,56,78)), // unnamed time value child
SUB_NODE = MakeRec().genNode(), // empty anonymous node used to open a sub scope
ATTRIB_NODE = MakeRec().genNode("δ"), // empty named node to be attached as attribute δ
ATTRIB_NODE = MakeRec().genNode("δ"), // empty named node to be attached as attribute δ
CHILD_NODE = SUB_NODE; // yet another child node, same ID as SUB_NODE (!)
}//(End)Test fixture
@ -71,13 +71,29 @@ namespace test{
/***********************************************************************//**
* @test Demonstration/Concept: a description language for list differences.
* @test Demonstration/Concept: a description language for tree differences.
* The representation is given as a linearised sequence of verb tokens.
* This test demonstrates the application of such a diff representation
* to a given source list, transforming this list to hold the intended
* target list contents.
*
* @see session-structure-mapping-test.cpp
* In addition to the verbs used for list diffing, here we additionally
* have to deal with nested scopes, which can be entered thorough a
* bracketing construct \c mut(ID)...emu(ID).
* This test demonstrates the application of such diff sequences
* - in the first step, an empty root #Record<GenNode> is populated
* with a type-ID, three named attribute values, three child values
* and a nested child-Record.
* - the second step demonstrates various diff language constructs
* to alter, reshape and mutate this data structure
* After applying those two diff sequences, we verify the data
* is indeed in the expected shape.
* @remarks to follow this test, you should be familiar both with our
* \link diff::Record generic data record \endlink, as well as with
* the \link diff::GenNode variant data node \endlink. The key point
* to note is the usage of Record elements as payload within GenNode,
* which allows to represent tree shaped object like data structures.
* @see GenericRecordRepresentation_test
* @see GenNodeBasic_test
* @see DiffListApplication_test
* @see diff-tree-application.hpp
* @see tree-diff.hpp
*/
class DiffTreeApplication_test
: public Test
@ -115,7 +131,7 @@ namespace test{
, pick(Ref::CHILD) // pick a child anonymously
, mut(Ref::THIS) // mutate the current element (the one just picked)
, ins(ATTRIB3)
, ins(ATTRIB_NODE)
, ins(ATTRIB_NODE) // attributes can also be nested objects
, find(CHILD_A)
, del(CHILD_B)
, ins(CHILD_NODE)
@ -125,7 +141,7 @@ namespace test{
, ins(TYPE_Y)
, ins(ATTRIB2)
, emu(CHILD_NODE)
, mut(ATTRIB_NODE)
, mut(ATTRIB_NODE) // mutation can be out-of order, target found by ID
, ins(CHILD_A)
, ins(CHILD_A)
, ins(CHILD_A)
@ -141,41 +157,44 @@ namespace test{
Rec::Mutator target;
Rec& subject = target;
DiffApplicator<Rec::Mutator> application(target);
// Part I : apply diff to populate
application.consume(populationDiff());
CHECK (!isnil (subject)); // nonempty -- content has been added
CHECK ("X" == subject.getType()); // type was set to "X"
CHECK (1 == subject.get("α").data.get<int>()); // has gotten our int attribute "α"
CHECK (2L == subject.get("β").data.get<int64_t>()); // ... the long attribute "β"
CHECK (3.45 == subject.get("γ").data.get<double>()); // ... and double attribute "γ"
auto scope = subject.scope(); // look into the scope contents...
CHECK ( *scope == CHILD_A); // there is CHILD_A
CHECK (*++scope == CHILD_T); // followed by a copy of CHILD_T
CHECK (*++scope == CHILD_T); // and another copy of CHILD_T
CHECK (*++scope == MakeRec().appendChild(CHILD_B) // and there is a nested Record
.appendChild(CHILD_A) // with CHILD_B
.genNode(SUB_NODE.idi.getSym())); // and CHILD_A
CHECK (isnil(++scope)); // thats all -- no more children
CHECK (!isnil (subject)); // nonempty -- content has been added
CHECK ("X" == subject.getType()); // type was set to "X"
CHECK (1 == subject.get("α").data.get<int>()); // has gotten our int attribute "α"
CHECK (2L == subject.get("β").data.get<int64_t>()); // ... the long attribute "β"
CHECK (3.45 == subject.get("γ").data.get<double>()); // ... and double attribute "γ"
auto scope = subject.scope(); // look into the scope contents...
CHECK ( *scope == CHILD_A); // there is CHILD_A
CHECK (*++scope == CHILD_T); // followed by a copy of CHILD_T
CHECK (*++scope == CHILD_T); // and another copy of CHILD_T
CHECK (*++scope == MakeRec().appendChild(CHILD_B) // and there is a nested Record
.appendChild(CHILD_A) // with CHILD_B
.genNode(SUB_NODE.idi.getSym())); // and CHILD_A
CHECK (isnil(++scope)); // thats all -- no more children
application.consume(mutationDiff());
CHECK (join (subject.keys()) == "α, β, γ"); // the attributes weren't altered
scope = subject.scope(); // but the scope was reordered
CHECK ( *scope == CHILD_T); // CHILD_T
CHECK (*++scope == CHILD_A); // CHILD_A
Rec nested = (++scope)->data.get<Rec>(); // and our nested Record, which too has been altered:
CHECK (nested.get("γ").data.get<double>() == 3.45); // it carries now an attribute "δ", which is again
CHECK (nested.get("δ").data.get<Rec>() == MakeRec().appendChild(CHILD_A) // a nested Record with three children CHILD_A
.appendChild(CHILD_A) //
.appendChild(CHILD_A) //
.genNode("δ")); //
auto subScope = nested.scope(); // and within the nested sub-scope we find
CHECK ( *subScope == CHILD_A); // CHILD_A
CHECK (*++subScope == MakeRec().type("Y") // a yet-again nested sub-Record of type "Y"
.set("β", 2L ) // with just an attribute "β" == 2L
.genNode(CHILD_NODE.idi.getSym())); // (and an empty child scope)
CHECK (*++subScope == CHILD_T); // followed by another copy of CHILD_T
CHECK (isnil (++subScope)); //
CHECK (isnil (++scope)); // and nothing beyond that.
// Part II : apply the second diff
application.consume(mutationDiff());
CHECK (join (subject.keys()) == "α, β, γ"); // the attributes weren't altered
scope = subject.scope(); // but the scope was reordered
CHECK ( *scope == CHILD_T); // CHILD_T
CHECK (*++scope == CHILD_A); // CHILD_A
Rec nested = (++scope)->data.get<Rec>(); // and our nested Record, which too has been altered:
CHECK (nested.get("γ").data.get<double>() == 3.45); // it carries now an attribute "δ", which is again
CHECK (nested.get("δ") == MakeRec().appendChild(CHILD_A) // a nested Record with three children CHILD_A
.appendChild(CHILD_A) //
.appendChild(CHILD_A) //
.genNode("δ")); //
auto subScope = nested.scope(); // and within the nested sub-scope we find
CHECK ( *subScope == CHILD_A); // CHILD_A
CHECK (*++subScope == MakeRec().type("Y") // a yet-again nested sub-Record of type "Y"
.set("β", 2L ) // with just an attribute "β" == 2L
.genNode(CHILD_NODE.idi.getSym())); // (and an empty child scope)
CHECK (*++subScope == CHILD_T); // followed by another copy of CHILD_T
CHECK (isnil (++subScope)); //
CHECK (isnil (++scope)); // and nothing beyond that.
}
};

View file

@ -1072,10 +1072,10 @@
</node>
</node>
</node>
<node CREATED="1443733567706" HGAP="76" ID="ID_143203937" MODIFIED="1443741888841" TEXT="Diff-Language" VSHIFT="15">
<node CREATED="1443733567706" HGAP="76" ID="ID_143203937" MODIFIED="1446356861829" TEXT="Diff-Language" VSHIFT="15">
<cloud COLOR="#cfba9d"/>
<font NAME="SansSerif" SIZE="14"/>
<icon BUILTIN="prepare"/>
<icon BUILTIN="go"/>
<node CREATED="1443733726563" ID="ID_410650103" MODIFIED="1443733731422" TEXT="Grundlagen">
<node CREATED="1443733732938" ID="ID_73184558" MODIFIED="1443733737277" TEXT="Folge von Verben"/>
<node CREATED="1443733738296" ID="ID_994909291" MODIFIED="1443733743181" TEXT="konstante Gr&#xf6;&#xdf;e"/>
@ -1711,9 +1711,9 @@
<node CREATED="1443741923547" ID="ID_1978439060" MODIFIED="1443741930738" TEXT="Listen-Diff">
<icon BUILTIN="button_ok"/>
</node>
<node BACKGROUND_COLOR="#fdfdcf" COLOR="#990000" CREATED="1443741931858" ID="ID_484829805" MODIFIED="1443741945481" TEXT="Baum-Diff">
<icon BUILTIN="flag"/>
<node CREATED="1445295424277" ID="ID_1084177503" MODIFIED="1445644243570">
<node CREATED="1443741931858" ID="ID_484829805" MODIFIED="1446356837301" TEXT="Baum-Diff">
<icon BUILTIN="pencil"/>
<node CREATED="1445295424277" FOLDED="true" ID="ID_1084177503" MODIFIED="1446356540880">
<richcontent TYPE="NODE"><html>
<head>
@ -1846,8 +1846,8 @@
<node CREATED="1445389311690" ID="ID_1926972913" MODIFIED="1445389323819" TEXT="und dieses rekursiv..."/>
</node>
</node>
<node COLOR="#391f9e" CREATED="1445391990778" HGAP="148" ID="ID_464295846" MODIFIED="1445392206014" TEXT="Implementierung" VSHIFT="4">
<icon BUILTIN="pencil"/>
<node COLOR="#391f9e" CREATED="1445391990778" HGAP="148" ID="ID_464295846" MODIFIED="1446356481059" TEXT="Implementierung" VSHIFT="4">
<icon BUILTIN="button_ok"/>
<node CREATED="1445392080175" ID="ID_341024968" MODIFIED="1445392101243">
<richcontent TYPE="NODE"><html>
<head>
@ -1882,7 +1882,7 @@
<icon BUILTIN="yes"/>
</node>
</node>
<node CREATED="1446159438278" HGAP="29" ID="ID_563496669" MODIFIED="1446159498995" VSHIFT="8">
<node CREATED="1446159438278" FOLDED="true" HGAP="29" ID="ID_563496669" MODIFIED="1446356526122" VSHIFT="8">
<richcontent TYPE="NODE"><html>
<head>
@ -1892,8 +1892,8 @@
Problem: <b><font color="#ed1c02" size="4">Rekursion</font></b>
</p>
</body>
</html>
</richcontent>
</html></richcontent>
<icon BUILTIN="button_ok"/>
<icon BUILTIN="clanbomber"/>
<node CREATED="1446159517914" ID="ID_1749096609" MODIFIED="1446159530661" TEXT="wir m&#xfc;ssen rekursiv in Sub-Scope einsteigen"/>
<node CREATED="1446159531745" ID="ID_1660140345" MODIFIED="1446159550795" TEXT="eingeschachtelt wieder ein Record::Mutator"/>
@ -1920,8 +1920,7 @@
ist der gesammte Diff f&#252;r den eingeschachtelten Kontext konsumiert
</p>
</body>
</html>
</richcontent>
</html></richcontent>
<icon BUILTIN="idea"/>
</node>
<node CREATED="1446159744053" ID="ID_1735127502" MODIFIED="1446159765190" TEXT="pro">
@ -1956,8 +1955,7 @@
und legt einen neuen Mutator an f&#252;r den nested scope
</p>
</body>
</html>
</richcontent>
</html></richcontent>
<icon BUILTIN="idea"/>
</node>
<node CREATED="1446160209758" ID="ID_1949126696" MODIFIED="1446160211514" TEXT="pro">
@ -1984,8 +1982,7 @@
interner Stack
</p>
</body>
</html>
</richcontent>
</html></richcontent>
<richcontent TYPE="NOTE"><html>
<head>
@ -2004,12 +2001,37 @@
also den TreeMutator, vermutlich das andere Modell (rekursiv konsumieren) verwenden.
</p>
</body>
</html>
</richcontent>
</html></richcontent>
<icon BUILTIN="yes"/>
</node>
</node>
</node>
<node CREATED="1446356556349" ID="ID_1068649765" MODIFIED="1446356607845" TEXT="offen...">
<icon BUILTIN="flag-yellow"/>
<node CREATED="1446356564244" ID="ID_211629813" MODIFIED="1446356584315" TEXT="find nicht per == sondern per match">
<icon BUILTIN="messagebox_warning"/>
</node>
<node CREATED="1446356765393" ID="ID_293921295" MODIFIED="1446356827238" TEXT="IDs in Testdaten f&#xfc;r GenNodeBaisc_test">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
Problem sind mal wieder die automatisch generierten IDs.
</p>
<p>
Die sind nat&#252;rlich anders, wenn wir die ganze Testsuite ausf&#252;hren...
</p>
</body>
</html>
</richcontent>
<icon BUILTIN="flag"/>
</node>
<node CREATED="1446356588505" ID="ID_895292312" MODIFIED="1446356602250" TEXT="gr&#xfc;ndlicher Test">
<icon BUILTIN="yes"/>
</node>
</node>
</node>
</node>
<node CREATED="1444522895718" ID="ID_598781690" MODIFIED="1444522897802" TEXT="Generierung">
@ -2585,5 +2607,27 @@
<node CREATED="1439842379420" ID="ID_1336697213" MODIFIED="1439842385655" TEXT="gtk-Abh&#xe4;ngigkeiten"/>
</node>
</node>
<node CREATED="1446356359992" HGAP="9" ID="ID_1850896628" MODIFIED="1446356445623" POSITION="left" TEXT="QA" VSHIFT="69">
<icon BUILTIN="prepare"/>
<node BACKGROUND_COLOR="#fdfdcf" COLOR="#990000" CREATED="1446356368070" ID="ID_768449868" MODIFIED="1446356423988" TEXT="ouch">
<icon BUILTIN="flag-yellow"/>
<node CREATED="1446356379653" ID="ID_1354454865" MODIFIED="1446356404198" TEXT="CHECK: typed-counter-test.cpp:329: thread_1: simpleUsageTest: (1 == myCounter.size())">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
sporadischer Fehlschlag am 1.11.2015
</p>
<p>
wir kann das passieren?
</p>
</body>
</html>
</richcontent>
</node>
</node>
</node>
</node>
</map>