DRTVWR-476: Add LLEventLogProxy, LLEventLogProxyFor<T>.

LLEventLogProxy can be introduced to serve as a logging proxy for an existing
LLEventPump subclass instance. Access through the LLEventLogProxy will be
logged; access directly to the underlying LLEventPump will not.

LLEventLogProxyFor<LLEventPumpSubclass> functions as a drop-in replacement for
the original LLEventPumpSubclass instance. It internally instantiates
LLEventPumpSubclass and serves as a proxy for that instance.

Add unit tests for LLEventMailDrop and LLEventLogProxyFor<LLEventMailDrop>,
both "plain" (events only) and via lleventcoro.h synchronization.
master
Nat Goodspeed 2019-10-16 09:15:47 -04:00
parent 6945755e52
commit 53aeea4d82
4 changed files with 307 additions and 0 deletions

View File

@ -37,6 +37,7 @@
// other Linden headers
#include "llerror.h" // LL_ERRS
#include "llsdutil.h" // llsd_matches()
#include "stringize.h"
/*****************************************************************************
* LLEventFilter
@ -409,3 +410,61 @@ void LLEventBatchThrottle::setSize(std::size_t size)
flush();
}
}
/*****************************************************************************
* LLEventLogProxy
*****************************************************************************/
LLEventLogProxy::LLEventLogProxy(LLEventPump& source, const std::string& name, bool tweak):
// note: we are NOT using the constructor that implicitly connects!
LLEventFilter(name, tweak),
// instead we simply capture a reference to the subject LLEventPump
mPump(source)
{
}
bool LLEventLogProxy::post(const LLSD& event) /* override */
{
auto counter = mCounter++;
auto eventplus = event;
if (eventplus.type() == LLSD::TypeMap)
{
eventplus["_cnt"] = counter;
}
std::string hdr{STRINGIZE(getName() << ": post " << counter)};
LL_INFOS("LogProxy") << hdr << ": " << event << LL_ENDL;
bool result = mPump.post(eventplus);
LL_INFOS("LogProxy") << hdr << " => " << result << LL_ENDL;
return result;
}
LLBoundListener LLEventLogProxy::listen_impl(const std::string& name,
const LLEventListener& target,
const NameList& after,
const NameList& before)
{
LL_DEBUGS("LogProxy") << "LLEventLogProxy('" << getName() << "').listen('"
<< name << "')" << LL_ENDL;
return mPump.listen(name,
[this, name, target](const LLSD& event)->bool
{ return listener(name, target, event); },
after,
before);
}
bool LLEventLogProxy::listener(const std::string& name,
const LLEventListener& target,
const LLSD& event) const
{
auto eventminus = event;
std::string counter{"**"};
if (eventminus.has("_cnt"))
{
counter = stringize(eventminus["_cnt"].asInteger());
eventminus.erase("_cnt");
}
std::string hdr{STRINGIZE(getName() << " to " << name << " " << counter)};
LL_INFOS("LogProxy") << hdr << ": " << eventminus << LL_ENDL;
bool result = target(eventminus);
LL_INFOS("LogProxy") << hdr << " => " << result << LL_ENDL;
return result;
}

View File

@ -427,4 +427,99 @@ private:
const bool mConsume;
};
/*****************************************************************************
* LLEventLogProxy
*****************************************************************************/
/**
* LLEventLogProxy is a little different than the other LLEventFilter
* subclasses declared in this header file, in that it completely wraps the
* passed LLEventPump (both input and output) instead of simply processing its
* output. Of course, if someone directly posts to the wrapped LLEventPump by
* looking up its string name in LLEventPumps, LLEventLogProxy can't intercept
* that post() call. But as long as consuming code is willing to access the
* LLEventLogProxy instance instead of the wrapped LLEventPump, all event data
* both post()ed and received is logged.
*
* The proxy role means that LLEventLogProxy intercepts more of LLEventPump's
* API than a typical LLEventFilter subclass.
*/
class LLEventLogProxy: public LLEventFilter
{
typedef LLEventFilter super;
public:
/**
* Construct LLEventLogProxy, wrapping the specified LLEventPump.
* Unlike a typical LLEventFilter subclass, the name parameter is @emph
* not optional because typically you want LLEventLogProxy to completely
* replace the wrapped LLEventPump. So you give the subject LLEventPump
* some other name and give the LLEventLogProxy the name that would have
* been used for the subject LLEventPump.
*/
LLEventLogProxy(LLEventPump& source, const std::string& name, bool tweak=false);
/// register a new listener
LLBoundListener listen_impl(const std::string& name, const LLEventListener& target,
const NameList& after, const NameList& before);
/// Post an event to all listeners
virtual bool post(const LLSD& event) /* override */;
private:
/// This method intercepts each call to any target listener. We pass it
/// the listener name and the caller's intended target listener plus the
/// posted LLSD event.
bool listener(const std::string& name,
const LLEventListener& target,
const LLSD& event) const;
LLEventPump& mPump;
LLSD::Integer mCounter{0};
};
/**
* LLEventPumpHolder<T> is a helper for LLEventLogProxyFor<T>. It simply
* stores an instance of T, presumably a subclass of LLEventPump. We derive
* LLEventLogProxyFor<T> from LLEventPumpHolder<T>, ensuring that
* LLEventPumpHolder's contained mWrappedPump is fully constructed before
* passing it to LLEventLogProxyFor's LLEventLogProxy base class constructor.
* But since LLEventPumpHolder<T> presents none of the LLEventPump API,
* LLEventLogProxyFor<T> inherits its methods unambiguously from
* LLEventLogProxy.
*/
template <class T>
class LLEventPumpHolder
{
protected:
LLEventPumpHolder(const std::string& name, bool tweak=false):
mWrappedPump(name, tweak)
{}
T mWrappedPump;
};
/**
* LLEventLogProxyFor<T> is a wrapper around any of the LLEventPump subclasses.
* Instantiating an LLEventLogProxy<T> instantiates an internal T. Otherwise
* it behaves like LLEventLogProxy.
*/
template <class T>
class LLEventLogProxyFor: private LLEventPumpHolder<T>, public LLEventLogProxy
{
// We derive privately from LLEventPumpHolder because it's an
// implementation detail of LLEventLogProxyFor. The only reason it's a
// base class at all is to guarantee that it's constructed first so we can
// pass it to our LLEventLogProxy base class constructor.
typedef LLEventPumpHolder<T> holder;
typedef LLEventLogProxy super;
public:
LLEventLogProxyFor(const std::string& name, bool tweak=false):
// our wrapped LLEventPump subclass instance gets a name suffix
// because that's not the LLEventPump we want consumers to obtain when
// they ask LLEventPumps for this name
holder(name + "-", tweak),
// it's our LLEventLogProxy that gets the passed name
super(holder::mWrappedPump, name, tweak)
{}
};
#endif /* ! defined(LL_LLEVENTFILTER_H) */

View File

@ -37,12 +37,14 @@
#include <iostream>
#include <string>
#include <typeinfo>
#include "../test/lltut.h"
#include "llsd.h"
#include "llsdutil.h"
#include "llevents.h"
#include "llcoros.h"
#include "lleventfilter.h"
#include "lleventcoro.h"
#include "../test/debug.h"
#include "../test/sync.h"
@ -255,4 +257,80 @@ namespace tut
LLCoros::instance().launch("test<5>", [this](){ coroPumpPost(); });
ensure_equals(result.asInteger(), 18);
}
template <class PUMP>
void test()
{
PUMP pump(typeid(PUMP).name());
bool running{false};
LLSD data{LLSD::emptyArray()};
// start things off by posting once before even starting the listener
// coro
LL_DEBUGS() << "test() posting first" << LL_ENDL;
LLSD first{LLSDMap("desc", "first")("value", 0)};
bool consumed = pump.post(first);
ensure("should not have consumed first", ! consumed);
// now launch the coro
LL_DEBUGS() << "test() launching listener coro" << LL_ENDL;
running = true;
LLCoros::instance().launch(
"listener",
[&pump, &running, &data](){
// important for this test that we consume posted values
LLCoros::instance().set_consuming(true);
// should immediately retrieve 'first' without waiting
LL_DEBUGS() << "listener coro waiting for first" << LL_ENDL;
data.append(llcoro::suspendUntilEventOnWithTimeout(pump, 0.1, LLSD()));
// Don't use ensure() from within the coro -- ensure() failure
// throws tut::fail, which won't propagate out to the main
// test driver, which will result in an odd failure.
// Wait for 'second' because it's not already pending.
LL_DEBUGS() << "listener coro waiting for second" << LL_ENDL;
data.append(llcoro::suspendUntilEventOnWithTimeout(pump, 0.1, LLSD()));
// and wait for 'third', which should involve no further waiting
LL_DEBUGS() << "listener coro waiting for third" << LL_ENDL;
data.append(llcoro::suspendUntilEventOnWithTimeout(pump, 0.1, LLSD()));
LL_DEBUGS() << "listener coro done" << LL_ENDL;
running = false;
});
// back from coro at the point where it's waiting for 'second'
LL_DEBUGS() << "test() posting second" << LL_ENDL;
LLSD second{llsd::map("desc", "second", "value", 1)};
consumed = pump.post(second);
ensure("should have consumed second", consumed);
// This is a key point: even though we've post()ed the value for which
// the coroutine is waiting, it's actually still suspended until we
// pause for some other reason. The coroutine will only pick up one
// value at a time from our 'pump'. It's important to exercise the
// case when we post() two values before it picks up either.
LL_DEBUGS() << "test() posting third" << LL_ENDL;
LLSD third{llsd::map("desc", "third", "value", 2)};
consumed = pump.post(third);
ensure("should NOT yet have consumed third", ! consumed);
// now just wait for coro to finish -- which it eventually will, given
// that all its suspend calls have short timeouts.
while (running)
{
LL_DEBUGS() << "test() waiting for coro done" << LL_ENDL;
llcoro::suspendUntilTimeout(0.1);
}
// okay, verify expected results
ensure_equals("should have received three values", data,
llsd::array(first, second, third));
LL_DEBUGS() << "test() done" << LL_ENDL;
}
template<> template<>
void object::test<6>()
{
set_test_name("LLEventMailDrop");
tut::test<LLEventMailDrop>();
}
template<> template<>
void object::test<7>()
{
set_test_name("LLEventLogProxyFor<LLEventMailDrop>");
tut::test< LLEventLogProxyFor<LLEventMailDrop> >();
}
}

View File

@ -36,9 +36,12 @@
// other Linden headers
#include "../test/lltut.h"
#include "stringize.h"
#include "llsdutil.h"
#include "listener.h"
#include "tests/wrapllerrs.h"
#include <typeinfo>
/*****************************************************************************
* Test classes
*****************************************************************************/
@ -401,6 +404,78 @@ namespace tut
throttle.post(";17");
ensure_equals("17", cat.result, "136;12;17"); // "17" delivered
}
template<class PUMP>
void test()
{
PUMP pump(typeid(PUMP).name());
LLSD data{LLSD::emptyArray()};
bool consumed{true};
// listener that appends to 'data'
// but that also returns the current value of 'consumed'
// Instantiate this separately because we're going to listen()
// multiple times with the same lambda: LLEventMailDrop only replays
// queued events on a new listen() call.
auto lambda =
[&data, &consumed](const LLSD& event)->bool
{
data.append(event);
return consumed;
};
{
LLTempBoundListener conn = pump.listen("lambda", lambda);
pump.post("first");
}
// first post() should certainly be received by listener
ensure_equals("first", data, llsd::array("first"));
// the question is, since consumed was true, did it queue the value?
data = LLSD::emptyArray();
{
// if it queued the value, it would be delivered on subsequent
// listen() call
LLTempBoundListener conn = pump.listen("lambda", lambda);
}
ensure_equals("empty1", data, LLSD::emptyArray());
data = LLSD::emptyArray();
// now let's NOT consume the posted data
consumed = false;
{
LLTempBoundListener conn = pump.listen("lambda", lambda);
pump.post("second");
pump.post("third");
}
// the two events still arrive
ensure_equals("second,third1", data, llsd::array("second", "third"));
data = LLSD::emptyArray();
{
// when we reconnect, these should be delivered again
// but this time they should be consumed
consumed = true;
LLTempBoundListener conn = pump.listen("lambda", lambda);
}
// unconsumed events were delivered again
ensure_equals("second,third2", data, llsd::array("second", "third"));
data = LLSD::emptyArray();
{
// when we reconnect this time, no more unconsumed events
LLTempBoundListener conn = pump.listen("lambda", lambda);
}
ensure_equals("empty2", data, LLSD::emptyArray());
}
template<> template<>
void filter_object::test<6>()
{
set_test_name("LLEventMailDrop");
tut::test<LLEventMailDrop>();
}
template<> template<>
void filter_object::test<7>()
{
set_test_name("LLEventLogProxyFor<LLEventMailDrop>");
tut::test< LLEventLogProxyFor<LLEventMailDrop> >();
}
} // namespace tut
/*****************************************************************************