513 lines
18 KiB
C++
513 lines
18 KiB
C++
/**
|
|
* @file llevents_tut.cpp
|
|
* @author Nat Goodspeed
|
|
* @date 2008-09-12
|
|
* @brief Test of llevents.h
|
|
*
|
|
* $LicenseInfo:firstyear=2008&license=viewerlgpl$
|
|
* Second Life Viewer Source Code
|
|
* Copyright (C) 2010, Linden Research, Inc.
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation;
|
|
* version 2.1 of the License only.
|
|
*
|
|
* This library 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
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*
|
|
* Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
|
|
* $/LicenseInfo$
|
|
*/
|
|
|
|
#if LL_WINDOWS
|
|
#pragma warning (disable : 4675) // "resolved by ADL" -- just as I want!
|
|
#endif
|
|
|
|
// Precompiled header
|
|
#include "linden_common.h"
|
|
// associated header
|
|
// UGLY HACK! We want to verify state internal to the classes without
|
|
// providing public accessors.
|
|
#define testable public
|
|
#include "llevents.h"
|
|
#undef testable
|
|
// STL headers
|
|
// std headers
|
|
#include <iostream>
|
|
#include <typeinfo>
|
|
// external library headers
|
|
#include <boost/bind.hpp>
|
|
#include <boost/shared_ptr.hpp>
|
|
#include <boost/assign/list_of.hpp>
|
|
// other Linden headers
|
|
#include "tests/listener.h" // must PRECEDE lltut.h
|
|
#include "lltut.h"
|
|
#include "catch_and_store_what_in.h"
|
|
#include "stringize.h"
|
|
|
|
using boost::assign::list_of;
|
|
|
|
template<typename T>
|
|
T make(const T& value)
|
|
{
|
|
return value;
|
|
}
|
|
|
|
/*****************************************************************************
|
|
* tut test group
|
|
*****************************************************************************/
|
|
namespace tut
|
|
{
|
|
struct events_data
|
|
{
|
|
events_data() :
|
|
pumps(LLEventPumps::instance()),
|
|
listener0("first"),
|
|
listener1("second")
|
|
{
|
|
}
|
|
LLEventPumps& pumps;
|
|
Listener listener0;
|
|
Listener listener1;
|
|
|
|
void check_listener(const std::string& desc, const Listener& listener, LLSD::Integer got)
|
|
{
|
|
ensure_equals(STRINGIZE(listener << ' ' << desc),
|
|
listener.getLastEvent().asInteger(), got);
|
|
}
|
|
};
|
|
typedef test_group<events_data> events_group;
|
|
typedef events_group::object events_object;
|
|
tut::events_group evgr("events");
|
|
|
|
template<> template<>
|
|
void events_object::test<1>()
|
|
{
|
|
set_test_name("basic operations");
|
|
// Having to modify this to track the statically-
|
|
// constructed pumps in other TUT modules in this giant monolithic test
|
|
// executable isn't such a hot idea.
|
|
// ensure_equals("initial pump", pumps.mPumpMap.size(), 1);
|
|
size_t initial_pumps(pumps.mPumpMap.size());
|
|
LLEventPump& per_frame(pumps.obtain("per-frame"));
|
|
ensure_equals("first explicit pump", pumps.mPumpMap.size(), initial_pumps + 1);
|
|
// Verify that per_frame was instantiated as an LLEventStream.
|
|
ensure("LLEventStream leaf class", dynamic_cast<LLEventStream*> (&per_frame));
|
|
ensure("enabled", per_frame.enabled());
|
|
// Trivial test, but posting an event to an EventPump with no
|
|
// listeners should not blow up. The test is relevant because defining
|
|
// a boost::signal with a non-void return signature, using the default
|
|
// combiner, blows up if there are no listeners. This is because the
|
|
// default combiner is defined to return the value returned by the
|
|
// last listener, which is meaningless if there were no listeners.
|
|
per_frame.post(0);
|
|
LLBoundListener connection = listener0.listenTo(per_frame);
|
|
ensure("connected", connection.connected());
|
|
ensure("not blocked", !connection.blocked());
|
|
per_frame.post(1);
|
|
check_listener("received", listener0, 1);
|
|
{ // block the connection
|
|
LLEventPump::Blocker block(connection);
|
|
ensure("blocked", connection.blocked());
|
|
per_frame.post(2);
|
|
check_listener("not updated", listener0, 1);
|
|
} // unblock
|
|
ensure("unblocked", !connection.blocked());
|
|
per_frame.post(3);
|
|
check_listener("unblocked", listener0, 3);
|
|
LLBoundListener sameConnection = per_frame.getListener(listener0.getName());
|
|
ensure("still connected", sameConnection.connected());
|
|
ensure("still not blocked", !sameConnection.blocked());
|
|
{ // block it again
|
|
LLEventPump::Blocker block(sameConnection);
|
|
ensure("re-blocked", sameConnection.blocked());
|
|
per_frame.post(4);
|
|
check_listener("re-blocked", listener0, 3);
|
|
} // unblock
|
|
std::string threw = catch_what<LLEventPump::DupListenerName>(
|
|
[&per_frame, this](){
|
|
// NOTE: boost::bind() saves its arguments by VALUE! If you pass
|
|
// an object instance rather than a pointer, you'll end up binding
|
|
// to an internal copy of that instance! Use boost::ref() to
|
|
// capture a reference instead.
|
|
per_frame.listen(listener0.getName(), // note bug, dup name
|
|
boost::bind(&Listener::call, boost::ref(listener1), _1));
|
|
});
|
|
ensure_equals(threw,
|
|
std::string("DupListenerName: "
|
|
"Attempt to register duplicate listener name '") +
|
|
listener0.getName() + "' on " + typeid(per_frame).name() +
|
|
" '" + per_frame.getName() + "'");
|
|
// do it right this time
|
|
listener1.listenTo(per_frame);
|
|
per_frame.post(5);
|
|
check_listener("got", listener0, 5);
|
|
check_listener("got", listener1, 5);
|
|
per_frame.enable(false);
|
|
per_frame.post(6);
|
|
check_listener("didn't get", listener0, 5);
|
|
check_listener("didn't get", listener1, 5);
|
|
per_frame.enable();
|
|
per_frame.post(7);
|
|
check_listener("got", listener0, 7);
|
|
check_listener("got", listener1, 7);
|
|
per_frame.stopListening(listener0.getName());
|
|
ensure("disconnected 0", ! connection.connected());
|
|
ensure("disconnected 1", ! sameConnection.connected());
|
|
per_frame.post(8);
|
|
check_listener("disconnected", listener0, 7);
|
|
check_listener("still connected", listener1, 8);
|
|
per_frame.stopListening(listener1.getName());
|
|
per_frame.post(9);
|
|
check_listener("disconnected", listener1, 8);
|
|
}
|
|
|
|
template<> template<>
|
|
void events_object::test<2>()
|
|
{
|
|
set_test_name("callstop() returning true");
|
|
LLEventPump& per_frame(pumps.obtain("per-frame"));
|
|
listener0.reset(0);
|
|
listener1.reset(0);
|
|
LLBoundListener bound0 = listener0.listenTo(per_frame, &Listener::callstop);
|
|
LLBoundListener bound1 = listener1.listenTo(per_frame, &Listener::call,
|
|
// after listener0
|
|
make<LLEventPump::NameList>(list_of(listener0.getName())));
|
|
ensure("enabled", per_frame.enabled());
|
|
ensure("connected 0", bound0.connected());
|
|
ensure("unblocked 0", !bound0.blocked());
|
|
ensure("connected 1", bound1.connected());
|
|
ensure("unblocked 1", !bound1.blocked());
|
|
per_frame.post(1);
|
|
check_listener("got", listener0, 1);
|
|
// Because listener0.callstop() returns true, control never reaches listener1.call().
|
|
check_listener("got", listener1, 0);
|
|
}
|
|
|
|
bool chainEvents(Listener& someListener, const LLSD& event)
|
|
{
|
|
// Make this call so we can watch for side effects for test purposes.
|
|
someListener.call(event);
|
|
// This function represents a recursive event chain -- or some other
|
|
// scenario in which an event handler raises additional events.
|
|
int value = event.asInteger();
|
|
if (value)
|
|
{
|
|
LLEventPumps::instance().obtain("login").post(value - 1);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
template<> template<>
|
|
void events_object::test<3>()
|
|
{
|
|
set_test_name("explicitly-instantiated LLEventStream");
|
|
// Explicitly instantiate an LLEventStream, and verify that it
|
|
// self-registers with LLEventPumps
|
|
size_t registered = pumps.mPumpMap.size();
|
|
size_t owned = pumps.mOurPumps.size();
|
|
LLEventPump* localInstance;
|
|
{
|
|
LLEventStream myEventStream("stream");
|
|
localInstance = &myEventStream;
|
|
LLEventPump& stream(pumps.obtain("stream"));
|
|
ensure("found named LLEventStream instance", &stream == localInstance);
|
|
ensure_equals("registered new instance", pumps.mPumpMap.size(), registered + 1);
|
|
ensure_equals("explicit instance not owned", pumps.mOurPumps.size(), owned);
|
|
} // destroy myEventStream -- should unregister
|
|
ensure_equals("destroyed instance unregistered", pumps.mPumpMap.size(), registered);
|
|
ensure_equals("destroyed instance not owned", pumps.mOurPumps.size(), owned);
|
|
LLEventPump& stream(pumps.obtain("stream"));
|
|
ensure("new LLEventStream instance", &stream != localInstance);
|
|
ensure_equals("obtain()ed instance registered", pumps.mPumpMap.size(), registered + 1);
|
|
ensure_equals("obtain()ed instance owned", pumps.mOurPumps.size(), owned + 1);
|
|
}
|
|
|
|
template<> template<>
|
|
void events_object::test<4>()
|
|
{
|
|
set_test_name("stopListening()");
|
|
LLEventPump& login(pumps.obtain("login"));
|
|
listener0.listenTo(login);
|
|
login.stopListening(listener0.getName());
|
|
// should not throw because stopListening() should have removed name
|
|
listener0.listenTo(login, &Listener::callstop);
|
|
LLBoundListener wrong = login.getListener("bogus");
|
|
ensure("bogus connection disconnected", !wrong.connected());
|
|
ensure("bogus connection blocked", wrong.blocked());
|
|
}
|
|
|
|
template<> template<>
|
|
void events_object::test<5>()
|
|
{
|
|
set_test_name("chaining LLEventPump instances");
|
|
LLEventPump& upstream(pumps.obtain("upstream"));
|
|
// One potentially-useful construct is to chain LLEventPumps together.
|
|
// Among other things, this allows you to turn subsets of listeners on
|
|
// and off in groups.
|
|
LLEventPump& filter0(pumps.obtain("filter0"));
|
|
LLEventPump& filter1(pumps.obtain("filter1"));
|
|
upstream.listen(filter0.getName(), boost::bind(&LLEventPump::post, boost::ref(filter0), _1));
|
|
upstream.listen(filter1.getName(), boost::bind(&LLEventPump::post, boost::ref(filter1), _1));
|
|
listener0.listenTo(filter0);
|
|
listener1.listenTo(filter1);
|
|
listener0.reset(0);
|
|
listener1.reset(0);
|
|
upstream.post(1);
|
|
check_listener("got unfiltered", listener0, 1);
|
|
check_listener("got unfiltered", listener1, 1);
|
|
filter0.enable(false);
|
|
upstream.post(2);
|
|
check_listener("didn't get filtered", listener0, 1);
|
|
check_listener("got filtered", listener1, 2);
|
|
}
|
|
|
|
template<> template<>
|
|
void events_object::test<6>()
|
|
{
|
|
set_test_name("listener dependency order");
|
|
typedef LLEventPump::NameList NameList;
|
|
LLEventPump& button(pumps.obtain("button"));
|
|
Collect collector;
|
|
button.listen("Mary",
|
|
boost::bind(&Collect::add, boost::ref(collector), "Mary", _1),
|
|
// state that "Mary" must come after "checked"
|
|
make<NameList> (list_of("checked")));
|
|
button.listen("checked",
|
|
boost::bind(&Collect::add, boost::ref(collector), "checked", _1),
|
|
// "checked" must come after "spot"
|
|
make<NameList> (list_of("spot")));
|
|
button.listen("spot",
|
|
boost::bind(&Collect::add, boost::ref(collector), "spot", _1));
|
|
button.post(1);
|
|
ensure_equals(collector.result, make<StringVec>(list_of("spot")("checked")("Mary")));
|
|
collector.clear();
|
|
button.stopListening("Mary");
|
|
button.listen("Mary",
|
|
boost::bind(&Collect::add, boost::ref(collector), "Mary", _1),
|
|
LLEventPump::empty, // no after dependencies
|
|
// now "Mary" must come before "spot"
|
|
make<NameList>(list_of("spot")));
|
|
button.post(2);
|
|
ensure_equals(collector.result, make<StringVec>(list_of("Mary")("spot")("checked")));
|
|
collector.clear();
|
|
button.stopListening("spot");
|
|
std::string threw = catch_what<LLEventPump::Cycle>(
|
|
[&button, &collector](){
|
|
button.listen("spot",
|
|
boost::bind(&Collect::add, boost::ref(collector), "spot", _1),
|
|
// after "Mary" and "checked" -- whoops!
|
|
make<NameList>(list_of("Mary")("checked")));
|
|
});
|
|
// Obviously the specific wording of the exception text can
|
|
// change; go ahead and change the test to match.
|
|
// Establish that it contains:
|
|
// - the name and runtime type of the LLEventPump
|
|
ensure_contains("LLEventPump type", threw, typeid(button).name());
|
|
ensure_contains("LLEventPump name", threw, "'button'");
|
|
// - the name of the new listener that caused the problem
|
|
ensure_contains("new listener name", threw, "'spot'");
|
|
// - a synopsis of the problematic dependencies.
|
|
ensure_contains("cyclic dependencies", threw,
|
|
"\"Mary\" -> before (\"spot\")");
|
|
ensure_contains("cyclic dependencies", threw,
|
|
"after (\"spot\") -> \"checked\"");
|
|
ensure_contains("cyclic dependencies", threw,
|
|
"after (\"Mary\", \"checked\") -> \"spot\"");
|
|
button.listen("yellow",
|
|
boost::bind(&Collect::add, boost::ref(collector), "yellow", _1),
|
|
make<NameList>(list_of("checked")));
|
|
button.listen("shoelaces",
|
|
boost::bind(&Collect::add, boost::ref(collector), "shoelaces", _1),
|
|
make<NameList>(list_of("checked")));
|
|
button.post(3);
|
|
ensure_equals(collector.result, make<StringVec>(list_of("Mary")("checked")("yellow")("shoelaces")));
|
|
collector.clear();
|
|
threw = catch_what<LLEventPump::OrderChange>(
|
|
[&button, &collector](){
|
|
button.listen("of",
|
|
boost::bind(&Collect::add, boost::ref(collector), "of", _1),
|
|
make<NameList>(list_of("shoelaces")),
|
|
make<NameList>(list_of("yellow")));
|
|
});
|
|
// Same remarks about the specific wording of the exception. Just
|
|
// ensure that it contains enough information to clarify the
|
|
// problem and what must be done to resolve it.
|
|
ensure_contains("LLEventPump type", threw, typeid(button).name());
|
|
ensure_contains("LLEventPump name", threw, "'button'");
|
|
ensure_contains("new listener name", threw, "'of'");
|
|
ensure_contains("prev listener name", threw, "'yellow'");
|
|
// std::cout << "Thrown Exception: " << threw << std::endl;
|
|
ensure_contains("old order", threw, "was: Mary, checked, yellow, shoelaces");
|
|
ensure_contains("new order", threw, "now: Mary, checked, shoelaces, of, yellow");
|
|
button.post(4);
|
|
ensure_equals(collector.result, make<StringVec>(list_of("Mary")("checked")("yellow")("shoelaces")));
|
|
}
|
|
|
|
template<> template<>
|
|
void events_object::test<7>()
|
|
{
|
|
set_test_name("tweaked and untweaked LLEventPump instance names");
|
|
{ // nested scope
|
|
// Hand-instantiate an LLEventStream...
|
|
LLEventStream bob("bob");
|
|
std::string threw = catch_what<LLEventPump::DupPumpName>(
|
|
[](){
|
|
// then another with a duplicate name.
|
|
LLEventStream bob2("bob");
|
|
});
|
|
ensure("Caught DupPumpName", !threw.empty());
|
|
} // delete first 'bob'
|
|
LLEventStream bob("bob"); // should work, previous one unregistered
|
|
LLEventStream bob1("bob", true);// allowed to tweak name
|
|
ensure_equals("tweaked LLEventStream name", bob1.getName(), "bob1");
|
|
std::vector<boost::shared_ptr<LLEventStream> > streams;
|
|
for (int i = 2; i <= 10; ++i)
|
|
{
|
|
streams.push_back(boost::shared_ptr<LLEventStream>(new LLEventStream("bob", true)));
|
|
}
|
|
ensure_equals("last tweaked LLEventStream name", streams.back()->getName(), "bob10");
|
|
}
|
|
|
|
// Define a function that accepts an LLListenerOrPumpName
|
|
void eventSource(const LLListenerOrPumpName& listener)
|
|
{
|
|
// Pretend that some time has elapsed. Call listener immediately.
|
|
listener(17);
|
|
}
|
|
|
|
template<> template<>
|
|
void events_object::test<8>()
|
|
{
|
|
set_test_name("LLListenerOrPumpName");
|
|
// Passing a boost::bind() expression to LLListenerOrPumpName
|
|
listener0.reset(0);
|
|
eventSource(boost::bind(&Listener::call, boost::ref(listener0), _1));
|
|
check_listener("got by listener", listener0, 17);
|
|
// Passing a string LLEventPump name to LLListenerOrPumpName
|
|
listener0.reset(0);
|
|
LLEventStream random("random");
|
|
listener0.listenTo(random);
|
|
eventSource("random");
|
|
check_listener("got by pump name", listener0, 17);
|
|
std::string threw = catch_what<LLListenerOrPumpName::Empty>(
|
|
[](){
|
|
LLListenerOrPumpName empty;
|
|
empty(17);
|
|
});
|
|
|
|
ensure("threw Empty", !threw.empty());
|
|
}
|
|
|
|
class TempListener: public Listener
|
|
{
|
|
public:
|
|
TempListener(const std::string& name, bool& liveFlag) :
|
|
Listener(name), mLiveFlag(liveFlag)
|
|
{
|
|
mLiveFlag = true;
|
|
}
|
|
|
|
virtual ~TempListener()
|
|
{
|
|
mLiveFlag = false;
|
|
}
|
|
|
|
private:
|
|
bool& mLiveFlag;
|
|
};
|
|
|
|
template<> template<>
|
|
void events_object::test<9>()
|
|
{
|
|
set_test_name("listen(boost::bind(...TempListener...))");
|
|
// listen() can't do anything about a plain TempListener instance:
|
|
// it's not managed with shared_ptr, nor is it an LLEventTrackable subclass
|
|
bool live = false;
|
|
LLEventPump& heaptest(pumps.obtain("heaptest"));
|
|
LLBoundListener connection;
|
|
{
|
|
TempListener tempListener("temp", live);
|
|
ensure("TempListener constructed", live);
|
|
connection = heaptest.listen(tempListener.getName(),
|
|
boost::bind(&Listener::call,
|
|
boost::ref(tempListener),
|
|
_1));
|
|
heaptest.post(1);
|
|
check_listener("received", tempListener, 1);
|
|
} // presumably this will make newListener go away?
|
|
// verify that
|
|
ensure("TempListener destroyed", !live);
|
|
// This is the case against which we can't defend. Don't even try to
|
|
// post to heaptest -- that would engage Undefined Behavior.
|
|
// Cautiously inspect connection...
|
|
ensure("misleadingly connected", connection.connected());
|
|
// then disconnect by hand.
|
|
heaptest.stopListening("temp");
|
|
}
|
|
|
|
class TempTrackableListener: public TempListener, public LLEventTrackable
|
|
{
|
|
public:
|
|
TempTrackableListener(const std::string& name, bool& liveFlag):
|
|
TempListener(name, liveFlag)
|
|
{}
|
|
};
|
|
|
|
template<> template<>
|
|
void events_object::test<10>()
|
|
{
|
|
set_test_name("listen(boost::bind(...TempTrackableListener ref...))");
|
|
bool live = false;
|
|
LLEventPump& heaptest(pumps.obtain("heaptest"));
|
|
LLBoundListener connection;
|
|
{
|
|
TempTrackableListener tempListener("temp", live);
|
|
ensure("TempTrackableListener constructed", live);
|
|
connection = heaptest.listen(tempListener.getName(),
|
|
boost::bind(&TempTrackableListener::call,
|
|
boost::ref(tempListener), _1));
|
|
heaptest.post(1);
|
|
check_listener("received", tempListener, 1);
|
|
} // presumably this will make tempListener go away?
|
|
// verify that
|
|
ensure("TempTrackableListener destroyed", ! live);
|
|
ensure("implicit disconnect", ! connection.connected());
|
|
// now just make sure we don't blow up trying to access a freed object!
|
|
heaptest.post(2);
|
|
}
|
|
|
|
template<> template<>
|
|
void events_object::test<11>()
|
|
{
|
|
set_test_name("listen(boost::bind(...TempTrackableListener pointer...))");
|
|
bool live = false;
|
|
LLEventPump& heaptest(pumps.obtain("heaptest"));
|
|
LLBoundListener connection;
|
|
{
|
|
TempTrackableListener* newListener(new TempTrackableListener("temp", live));
|
|
ensure("TempTrackableListener constructed", live);
|
|
connection = heaptest.listen(newListener->getName(),
|
|
boost::bind(&TempTrackableListener::call,
|
|
newListener, _1));
|
|
heaptest.post(1);
|
|
check_listener("received", *newListener, 1);
|
|
// explicitly destroy newListener
|
|
delete newListener;
|
|
}
|
|
// verify that
|
|
ensure("TempTrackableListener destroyed", ! live);
|
|
ensure("implicit disconnect", ! connection.connected());
|
|
// now just make sure we don't blow up trying to access a freed object!
|
|
heaptest.post(2);
|
|
}
|
|
|
|
} // namespace tut
|