add unit test, write documentation. Closes #420
This commit is contained in:
parent
89aacf385e
commit
12dc0e2c2d
5 changed files with 309 additions and 25 deletions
|
|
@ -24,17 +24,11 @@
|
|||
#ifndef MOBJECT_SESSION_QUERY_FOCUS_STACK_H
|
||||
#define MOBJECT_SESSION_QUERY_FOCUS_STACK_H
|
||||
|
||||
//#include "proc/mobject/mobject.hpp"
|
||||
//#include "proc/mobject/placement.hpp"
|
||||
#include "proc/mobject/session/scope-path.hpp"
|
||||
|
||||
//#include <vector>
|
||||
//#include <string>
|
||||
#include <boost/noncopyable.hpp>
|
||||
#include <list>
|
||||
|
||||
//using std::vector;
|
||||
//using std::string;
|
||||
using std::list;
|
||||
|
||||
|
||||
|
|
@ -44,7 +38,26 @@ namespace session {
|
|||
|
||||
|
||||
/**
|
||||
* TODO type comment
|
||||
* A custom stack holding ScopePath »frames«.
|
||||
* It is utilised by the ScopeLocator to establish the
|
||||
* \em current query focus location. Client code should
|
||||
* access this mechanism through QueryFocus instances
|
||||
* used as frontend. These QueryFocus objects incorporate
|
||||
* a boost::intrusive_ptr, which stores the ref-count within
|
||||
* the mentioned ScopePath frames located in the stack.
|
||||
*
|
||||
* \par automatic cleanup of unused frames
|
||||
*
|
||||
* The stack is aware of this ref-counting mechanism and will --
|
||||
* on each access -- automatically clean up any unused frames starting
|
||||
* from stack top, until encountering the first frame still in use.
|
||||
* This frame, by definition, is the <b>current focus location</b>.
|
||||
* The stack ensures there is always at least one ScopePath frame,
|
||||
* default-creating a new one if necessary.
|
||||
*
|
||||
* @see query-focus-stack-test.cpp
|
||||
* @see ScopeLocator
|
||||
* @see QueryFocus access point for client code
|
||||
*/
|
||||
class QueryFocusStack
|
||||
: boost::noncopyable
|
||||
|
|
@ -53,41 +66,55 @@ namespace session {
|
|||
std::list<ScopePath> paths_;
|
||||
|
||||
public:
|
||||
QueryFocusStack()
|
||||
QueryFocusStack ()
|
||||
: paths_()
|
||||
{
|
||||
openDefaultFrame();
|
||||
}
|
||||
|
||||
|
||||
bool empty() const;
|
||||
size_t size() const;
|
||||
bool empty () const;
|
||||
size_t size () const;
|
||||
|
||||
ScopePath& push (Scope const&);
|
||||
ScopePath& top ();
|
||||
void pop_unused ();
|
||||
void clear ();
|
||||
|
||||
|
||||
private:
|
||||
void openDefaultFrame();
|
||||
void openDefaultFrame ();
|
||||
};
|
||||
///////////////////////////TODO currently just fleshing the API
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* __implementation__ */
|
||||
|
||||
bool
|
||||
QueryFocusStack::empty() const
|
||||
QueryFocusStack::empty () const
|
||||
{
|
||||
return paths_.empty();
|
||||
}
|
||||
|
||||
|
||||
size_t
|
||||
QueryFocusStack::size() const
|
||||
QueryFocusStack::size () const
|
||||
{
|
||||
return paths_.size();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
QueryFocusStack::clear ()
|
||||
{
|
||||
paths_.clear();
|
||||
}
|
||||
|
||||
|
||||
/** Open a new path frame, pushing down the current frame.
|
||||
* The new frame tries to locate the given start scope
|
||||
* and navigates to this position.
|
||||
|
|
@ -120,7 +147,6 @@ namespace session {
|
|||
pop_unused();
|
||||
|
||||
ENSURE (!empty());
|
||||
ENSURE (!paths_.back().empty());
|
||||
return paths_.back();
|
||||
}
|
||||
|
||||
|
|
@ -130,30 +156,36 @@ namespace session {
|
|||
* ScopePath#use_count(). After executing this function
|
||||
* the topmost frame is either in use, or a new default
|
||||
* frame has been created at the bottom of an empty stack.
|
||||
* @note EXCEPTON_FREE ///////TODO prove!
|
||||
* @note EXCEPTION_FREE ///////TODO prove!
|
||||
*/
|
||||
void
|
||||
QueryFocusStack::pop_unused ()
|
||||
{
|
||||
if (1 == size() && !paths_.front().isValid())
|
||||
return; // unnecessary to evict a base frame repeatedly
|
||||
|
||||
while (size() && (0 == paths_.back().ref_count()))
|
||||
paths_.pop_back();
|
||||
|
||||
if (0 == size())
|
||||
openDefaultFrame();
|
||||
ENSURE (!empty());
|
||||
ENSURE (!paths_.back().empty());
|
||||
}
|
||||
|
||||
|
||||
/** @internal open a default path frame at the bottom
|
||||
* of an empty stack, locating to current model root
|
||||
* @note EXCEPTON_FREE ///////TODO prove!
|
||||
* @note EXCEPTION_FREE ///////TODO prove!
|
||||
*/
|
||||
void
|
||||
QueryFocusStack::openDefaultFrame()
|
||||
QueryFocusStack::openDefaultFrame ()
|
||||
{
|
||||
REQUIRE (0 == size());
|
||||
|
||||
paths_.resize(1);
|
||||
|
||||
ENSURE (!paths_.front().empty());
|
||||
ENSURE (!paths_.front().isValid()); // i.e. just root scope
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -188,14 +188,14 @@ namespace session {
|
|||
/** management function for boost::intrusive_ptr
|
||||
* to be picked up by ADL
|
||||
*/
|
||||
void
|
||||
inline void
|
||||
intrusive_ptr_add_ref (ScopePath* pathFrame)
|
||||
{
|
||||
REQUIRE (pathFrame);
|
||||
++(pathFrame->refcount_);
|
||||
}
|
||||
|
||||
void
|
||||
inline void
|
||||
intrusive_ptr_release (ScopePath* pathFrame)
|
||||
{
|
||||
REQUIRE (pathFrame);
|
||||
|
|
|
|||
|
|
@ -91,6 +91,10 @@ PLANNED "Query focus management" QueryFocus_test <<END
|
|||
END
|
||||
|
||||
|
||||
PLANNED "Query focus stack" QueryFocusStack_test <<END
|
||||
END
|
||||
|
||||
|
||||
PLANNED "RebuildFixture_test" RebuildFixture_test <<END
|
||||
END
|
||||
|
||||
|
|
|
|||
244
tests/components/proc/mobject/session/query-focus-stack-test.cpp
Normal file
244
tests/components/proc/mobject/session/query-focus-stack-test.cpp
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
/*
|
||||
QueryFocusStack(Test) - verify the stack of focus path frames
|
||||
|
||||
Copyright (C) Lumiera.org
|
||||
2009, 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.
|
||||
|
||||
* *****************************************************/
|
||||
|
||||
|
||||
#include "lib/test/run.hpp"
|
||||
#include "lib/test/test-helper.hpp"
|
||||
#include "proc/mobject/session/test-scopes.hpp"
|
||||
#include "proc/mobject/session/query-focus-stack.hpp"
|
||||
#include "lib/util.hpp"
|
||||
|
||||
|
||||
|
||||
|
||||
namespace mobject {
|
||||
namespace session {
|
||||
namespace test {
|
||||
|
||||
using util::isnil;
|
||||
using util::isSameObject;
|
||||
using lumiera::error::LUMIERA_ERROR_INVALID;
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* @test behaviour of the stack of focus location paths.
|
||||
* Basically this is just a stack, but has a somewhat unusual behaviour
|
||||
* on pop(), as it considers the (intrusive) ref-count maintained within
|
||||
* the stack frames (ScopePath instances) and cleans up unused frames.
|
||||
* Similar to the ScopePath_test, we use a pseudo-session to create
|
||||
* some path frames to play with.
|
||||
*
|
||||
* @see mobject::session::QueryFocusStack
|
||||
* @see mobject::session::ScopePath
|
||||
*/
|
||||
class QueryFocusStack_test : public Test
|
||||
{
|
||||
|
||||
virtual void
|
||||
run (Arg)
|
||||
{
|
||||
// Prepare an (test)Index backing the PlacementRefs
|
||||
PPIdx index = build_testScopes();
|
||||
|
||||
createStack();
|
||||
usePushedFrame();
|
||||
automaticFrameHandling();
|
||||
verify_errorHandling();
|
||||
clear();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
createStack ()
|
||||
{
|
||||
QueryFocusStack stack;
|
||||
|
||||
ASSERT (!isnil (stack));
|
||||
ASSERT (!isnil (stack.top()));
|
||||
ASSERT (stack.top().getLeaf().isRoot());
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
usePushedFrame ()
|
||||
{
|
||||
QueryFocusStack stack;
|
||||
PMO& startPoint = retrieve_startElm();
|
||||
|
||||
ScopePath& firstFrame = stack.top(); // remember for later
|
||||
intrusive_ptr_add_ref (&firstFrame);
|
||||
stack.top().navigate(startPoint);
|
||||
stack.top().moveUp();
|
||||
ASSERT (Scope(startPoint).getParent() == stack.top().getLeaf());
|
||||
ASSERT (1 == stack.size());
|
||||
|
||||
// now open a second path frame, pushing aside the initial one
|
||||
ScopePath& secondFrame = stack.push(startPoint);
|
||||
intrusive_ptr_add_ref (&secondFrame);
|
||||
ASSERT (2 == stack.size());
|
||||
ASSERT (secondFrame == stack.top());
|
||||
ASSERT (secondFrame.getLeaf() == startPoint);
|
||||
ASSERT (secondFrame.getLeaf() != firstFrame.getLeaf());
|
||||
|
||||
// can still reach and manipulate the ref-count of the first frame
|
||||
intrusive_ptr_add_ref (&firstFrame);
|
||||
ASSERT (2 == firstFrame.ref_count());
|
||||
ASSERT (1 == secondFrame.ref_count());
|
||||
|
||||
// can use/navigate the stack top frame
|
||||
stack.top().goRoot();
|
||||
ASSERT (isnil (stack.top())); // now indeed at root == empty path
|
||||
ASSERT (secondFrame.getLeaf().isRoot());
|
||||
ASSERT (secondFrame == stack.top());
|
||||
|
||||
// now drop back to the first frame:
|
||||
ASSERT (1 == secondFrame.ref_count());
|
||||
intrusive_ptr_release (&secondFrame);
|
||||
ASSERT (0 == secondFrame.ref_count());
|
||||
stack.pop_unused();
|
||||
ASSERT (1 == stack.size());
|
||||
ASSERT (firstFrame == stack.top());
|
||||
|
||||
// ...still pointing at the previous location
|
||||
ASSERT (Scope(startPoint).getParent() == stack.top().getLeaf());
|
||||
ASSERT (2 == firstFrame.ref_count());
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
automaticFrameHandling ()
|
||||
{
|
||||
QueryFocusStack stack;
|
||||
PMO& startPoint = retrieve_startElm();
|
||||
|
||||
ScopePath& firstFrame = stack.top(); // remember for later
|
||||
stack.top().navigate(startPoint);
|
||||
ASSERT (1 == stack.size());
|
||||
intrusive_ptr_add_ref (&firstFrame);
|
||||
|
||||
// now open two new frames, but don't add ref-counts on them
|
||||
ScopePath& secondFrame = stack.push(startPoint);
|
||||
ScopePath& thirdFrame = stack.push(startPoint);
|
||||
ASSERT (3 == stack.size());
|
||||
ASSERT (1 == firstFrame.ref_count());
|
||||
ASSERT (0 == secondFrame.ref_count());
|
||||
ASSERT (0 == thirdFrame.ref_count());
|
||||
|
||||
// any ref to top detects the non-referred-to state (by ref count==0)
|
||||
// and will automatically pop and clean up...
|
||||
ScopePath& newTop = stack.top();
|
||||
ASSERT (1 == stack.size());
|
||||
ASSERT (firstFrame == stack.top());
|
||||
ASSERT (isSameObject(newTop, firstFrame));
|
||||
ASSERT (stack.top().getLeaf() == startPoint);
|
||||
|
||||
// second exercise: a pop_unused may even completely empty the stack
|
||||
ScopePath& anotherFrame = stack.push(startPoint);
|
||||
ASSERT (0 == anotherFrame.ref_count());
|
||||
ASSERT (1 == firstFrame.ref_count());
|
||||
intrusive_ptr_release (&anotherFrame);
|
||||
ASSERT (0 == firstFrame.ref_count());
|
||||
ASSERT (firstFrame.getLeaf() == startPoint);
|
||||
|
||||
stack.pop_unused();
|
||||
ASSERT (1 == stack.size());
|
||||
// Note: don't use previously taken pointers
|
||||
// or references anymore, after the stack
|
||||
// triggered a cleanup!
|
||||
ScopePath& anotherFrame2 = stack.top();
|
||||
ASSERT (0 == anotherFrame2.ref_count());
|
||||
ASSERT (anotherFrame2.getLeaf().isRoot());
|
||||
anotherFrame2.navigate(startPoint);
|
||||
ASSERT (anotherFrame2.getLeaf() == startPoint);
|
||||
|
||||
stack.top();
|
||||
ASSERT (1 == stack.size());
|
||||
ASSERT (stack.top().getLeaf().isRoot());
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
verify_errorHandling ()
|
||||
{
|
||||
QueryFocusStack stack;
|
||||
PMO& startPoint = retrieve_startElm();
|
||||
|
||||
ScopePath& firstFrame = stack.top(); // remember for later
|
||||
stack.top().navigate(startPoint);
|
||||
ASSERT (1 == stack.size());
|
||||
intrusive_ptr_add_ref (&firstFrame);
|
||||
|
||||
ScopePath beforeInvalidNavigation = firstFrame;
|
||||
Scope unrelatedScope (TestPlacement<> (*new DummyMO));
|
||||
|
||||
// try to navigate to an invalid place
|
||||
VERIFY_ERROR (INVALID, stack.top().navigate (unrelatedScope) );
|
||||
ASSERT (1 == stack.size());
|
||||
ASSERT (1 == firstFrame.ref_count());
|
||||
ASSERT (stack.top().getLeaf() == startPoint);
|
||||
|
||||
// try to push an invalid place
|
||||
VERIFY_ERROR (INVALID, stack.push (unrelatedScope) );
|
||||
ASSERT (1 == stack.size());
|
||||
ASSERT (1 == firstFrame.ref_count());
|
||||
ASSERT (stack.top().getLeaf() == startPoint);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
clear ()
|
||||
{
|
||||
QueryFocusStack stack;
|
||||
intrusive_ptr_add_ref (&stack.top());
|
||||
stack.top().moveUp();
|
||||
ASSERT (stack.top().empty());
|
||||
|
||||
PMO& startPoint = retrieve_startElm();
|
||||
intrusive_ptr_add_ref ( & stack.push(startPoint) );
|
||||
intrusive_ptr_add_ref ( & stack.push(startPoint) );
|
||||
intrusive_ptr_add_ref ( & stack.push(startPoint) );
|
||||
intrusive_ptr_add_ref ( & stack.push(startPoint) );
|
||||
intrusive_ptr_add_ref ( & stack.push(startPoint) );
|
||||
intrusive_ptr_add_ref ( & stack.push(startPoint) );
|
||||
intrusive_ptr_add_ref ( & stack.push(startPoint) );
|
||||
intrusive_ptr_add_ref ( & stack.push(startPoint) );
|
||||
intrusive_ptr_add_ref ( & stack.push(startPoint) );
|
||||
ASSERT (10 == stack.size());
|
||||
stack.pop_unused();
|
||||
ASSERT (10 == stack.size());
|
||||
ASSERT (1 == stack.top().ref_count());
|
||||
|
||||
stack.clear();
|
||||
ASSERT (1 == stack.size());
|
||||
ASSERT (!stack.top().empty());
|
||||
ASSERT (stack.top().getLeaf().isRoot());
|
||||
ASSERT (0 == stack.top().ref_count());
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
/** Register this test class... */
|
||||
LAUNCHER (QueryFocusStack_test, "unit session");
|
||||
|
||||
|
||||
}}} // namespace mobject::session::test
|
||||
|
|
@ -3492,13 +3492,17 @@ The stack of scopes must not be confused with the ScopePath. Each single frame o
|
|||
The full implementation of this scope navigation is tricky, especially when it comes to determining the relation of two positions. It should be ''postponed'' and replaced by a ''dummy'' (no-op) implementation for the first integration round.
|
||||
</pre>
|
||||
</div>
|
||||
<div title="QueryFocusStack" modifier="Ichthyostega" modified="200910200159" created="200910200158" tags="SessionLogic spec dynamic" changecount="3">
|
||||
<pre>What is the ''current'' QueryFocus? There is a state-dependent part involved, inasmuch the effective ScopePath depends on how the invoking client has navigated the //current location// down into the HighLevelModel structures. Especially, when a VirtualClip is involved, there can be discrepancies between the paths resulting when descending down through different paths. (See &rarr; BindingScopeProblem).
|
||||
<div title="QueryFocusStack" modifier="Ichthyostega" modified="200911220509" created="200910200158" tags="SessionLogic spec dynamic" changecount="8">
|
||||
<pre>The ScopeLocator uses a special stack of ScopePath &raquo;frames&laquo; to maintain the //current focus.//
|
||||
What is the ''current'' QueryFocus and why is it necessary? There is a state-dependent part involved, inasmuch the effective ScopePath depends on how the invoking client has navigated the //current location// down into the HighLevelModel structures. Especially, when a VirtualClip is involved, there can be discrepancies between the paths resulting when descending down through different paths. (See &rarr; BindingScopeProblem).
|
||||
|
||||
Thus, doing something with the current location, and especially descending or querying adjacent scopes can modify this current path state. Thus we need a means of invoking a query in a way not interfering with the current path state, otherwise we wouldn't be able to provide side-effect free specific query operations.
|
||||
Thus, doing something with the current location, and especially descending or querying adjacent scopes can modify this current path state. Thus we need a means of invoking a query in a way not interfering with the current path state, otherwise we wouldn't be able to provide side-effect free query operations accessible on individual objects within the model.
|
||||
|
||||
!maintaining the current QueryFocus
|
||||
As long as client code is just interested to use the current query location, we can provide a handle referring to it. But when a query needs to be run without side effect on the current location, we //push//&nbsp; it aside and start using a new QueryFocus on top, which starts out as a clone copy of the current QueryFocus. Client code again gets a handle (smart-ptr) to this location, and additionally may access the new "current location". When all references are out of scope and gone, we'll drop back to the focus put aside previously.
|
||||
As long as client code is just interested to use the current query location, we can provide a handle referring to it. But when a query needs to be run without side effect on the current location, we //push//&nbsp; it aside and start using a new QueryFocus on top, which starts out at a new initial location. Client code again gets a handle (smart-ptr) to this location, and additionally may access the new //current location.// When all references are out of scope and gone, we'll drop back to the focus put aside previously.
|
||||
|
||||
!implementation of ref-counting and clean-up
|
||||
Actually, client code should use QueryFocus instances as frontend to access this &raquo;current focus&laquo;. Each ~QueryFocus instance incorporates a smart-ptr. But as in this case we're not managing objects allocated somewhere, we use an {{{boost::intrusive_ptr}}} and maintain the ref-count immediately within the target objects to be managed. These target objects are ScopePath instances and are living within the QueryFocusStack, which in turn in managed by the ScopeLocator singleton (see the UML diagram &rarr;[[here|QueryFocus]]). We use an (hand-written) stack implementation to ensure the memory locations of these ScopePath &raquo;frames&laquo; remain valid (and also to help with providing strong exception guarantees). The stack is aware of these ref-count and takes it into account on performing the {{{pop_unused()}}} operation: any unused frame on top will be evicted, stopping at the first frame still in use (which may be even just the old top). This cleanup also happens automatically when accessing the current top, re-initialising an potentially empty stack with a default-constructed new frame if necessary. This way, just accessing the stack top always yields the ''current focus location'', which thereby is //defined as the most recently used focus location still referred.//
|
||||
|
||||
!concurrency
|
||||
This concept deliberately ignores parallelism. But, as the current path state is already encapsulated (and ref-counting is in place), the only central access point is to reach the current scope. Instead of using a plain-flat singleton here, this access can easily be routed through thread local storage.
|
||||
|
|
|
|||
Loading…
Reference in a new issue