Skip to content
Snippets Groups Projects
Commit d7c2e4a7 authored by Nat Goodspeed's avatar Nat Goodspeed
Browse files

DRTVWR-476: Add Sync class to help with stepwise coroutine tests.

Sync is specifically intended for test programs. It is based on an
LLScalarCond<int>. The idea is that each of two coroutines can watch for the
other to get a chance to run, indicated by incrementing the wrapped int and
notifying the wrapped condition_variable. This is less hand-wavy than calling
llcoro::suspend() and hoping that the other routine will have had a chance to
run.

Use Sync in lleventcoro_test.cpp.

Also refactor lleventcoro_test.cpp so that instead of a collection of static
data requiring a clear() call at start of each individual test function, the
relevant data is all part of the test_data struct common to all test
functions. Make the helper coroutine functions members of test_data too.

Introduce llcoro::logname(), a convenience function to log the name of the
currently executing coroutine or "main" if in the thread's main coroutine.
parent 98cfe13c
No related branches found
No related tags found
No related merge requests found
......@@ -239,4 +239,17 @@ class LL_COMMON_API LLCoros: public LLSingleton<LLCoros>
static void delete_CoroData(CoroData* cdptr);
};
namespace llcoro
{
inline
std::string logname()
{
static std::string main("main");
std::string name(LLCoros::instance().getName());
return name.empty()? main : name;
}
} // llcoro
#endif /* ! defined(LL_LLCOROS_H) */
......@@ -45,6 +45,7 @@
#include "llcoros.h"
#include "lleventcoro.h"
#include "../test/debug.h"
#include "../test/sync.h"
using namespace llcoro;
......@@ -58,8 +59,9 @@ using namespace llcoro;
class ImmediateAPI
{
public:
ImmediateAPI():
mPump("immediate", true)
ImmediateAPI(Sync& sync):
mPump("immediate", true),
mSync(sync)
{
mPump.listen("API", boost::bind(&ImmediateAPI::operator(), this, _1));
}
......@@ -68,22 +70,18 @@ class ImmediateAPI
// Invoke this with an LLSD map containing:
// ["value"]: Integer value. We will reply with ["value"] + 1.
// ["reply"]: Name of LLEventPump on which to send success response.
// ["error"]: Name of LLEventPump on which to send error response.
// ["fail"]: Presence of this key selects ["error"], else ["success"] as
// the name of the pump on which to send the response.
// ["reply"]: Name of LLEventPump on which to send response.
bool operator()(const LLSD& event) const
{
mSync.bump();
LLSD::Integer value(event["value"]);
LLSD::String replyPumpName(event.has("fail")? "error" : "reply");
LLEventPumps::instance().obtain(event[replyPumpName]).post(value + 1);
// give listener a chance to process
llcoro::suspend();
LLEventPumps::instance().obtain(event["reply"]).post(value + 1);
return false;
}
private:
LLEventStream mPump;
Sync& mSync;
};
/*****************************************************************************
......@@ -91,34 +89,29 @@ class ImmediateAPI
*****************************************************************************/
namespace tut
{
struct coroutine_data {};
typedef test_group<coroutine_data> coroutine_group;
struct test_data
{
Sync mSync;
ImmediateAPI immediateAPI{mSync};
std::string replyName, errorName, threw, stringdata;
LLSD result, errordata;
int which;
void explicit_wait(boost::shared_ptr<LLCoros::Promise<std::string>>& cbp);
void waitForEventOn1();
void coroPump();
void postAndWait1();
void coroPumpPost();
};
typedef test_group<test_data> coroutine_group;
typedef coroutine_group::object object;
coroutine_group coroutinegrp("coroutine");
// use static data so we can intersperse coroutine functions with the
// tests that engage them
ImmediateAPI immediateAPI;
std::string replyName, errorName, threw, stringdata;
LLSD result, errordata;
int which;
// reinit vars at the start of each test
void clear()
{
replyName.clear();
errorName.clear();
threw.clear();
stringdata.clear();
result = LLSD();
errordata = LLSD();
which = 0;
}
void explicit_wait(boost::shared_ptr<LLCoros::Promise<std::string>>& cbp)
void test_data::explicit_wait(boost::shared_ptr<LLCoros::Promise<std::string>>& cbp)
{
BEGIN
{
mSync.bump();
// The point of this test is to verify / illustrate suspending a
// coroutine for something other than an LLEventPump. In other
// words, this shows how to adapt to any async operation that
......@@ -136,6 +129,7 @@ namespace tut
// calling get() on the future causes us to suspend
debug("about to suspend");
stringdata = future.get();
mSync.bump();
ensure_equals("Got it", stringdata, "received");
}
END
......@@ -144,30 +138,32 @@ namespace tut
template<> template<>
void object::test<1>()
{
clear();
set_test_name("explicit_wait");
DEBUG;
// Construct the coroutine instance that will run explicit_wait.
boost::shared_ptr<LLCoros::Promise<std::string>> respond;
LLCoros::instance().launch("test<1>",
boost::bind(explicit_wait, boost::ref(respond)));
[this, &respond](){ explicit_wait(respond); });
mSync.bump();
// When the coroutine waits for the future, it returns here.
debug("about to respond");
// Now we're the I/O subsystem delivering a result. This should make
// the coroutine ready.
respond->set_value("received");
// but give it a chance to wake up
llcoro::suspend();
mSync.yield();
// ensure the coroutine ran and woke up again with the intended result
ensure_equals(stringdata, "received");
}
void waitForEventOn1()
void test_data::waitForEventOn1()
{
BEGIN
{
mSync.bump();
result = suspendUntilEventOn("source");
mSync.bump();
}
END
}
......@@ -175,25 +171,27 @@ namespace tut
template<> template<>
void object::test<2>()
{
clear();
set_test_name("waitForEventOn1");
DEBUG;
LLCoros::instance().launch("test<2>", waitForEventOn1);
LLCoros::instance().launch("test<2>", [this](){ waitForEventOn1(); });
mSync.bump();
debug("about to send");
LLEventPumps::instance().obtain("source").post("received");
// give waitForEventOn1() a chance to run
llcoro::suspend();
mSync.yield();
debug("back from send");
ensure_equals(result.asString(), "received");
}
void coroPump()
void test_data::coroPump()
{
BEGIN
{
mSync.bump();
LLCoroEventPump waiter;
replyName = waiter.getName();
result = waiter.suspend();
mSync.bump();
}
END
}
......@@ -201,26 +199,28 @@ namespace tut
template<> template<>
void object::test<3>()
{
clear();
set_test_name("coroPump");
DEBUG;
LLCoros::instance().launch("test<3>", coroPump);
LLCoros::instance().launch("test<3>", [this](){ coroPump(); });
mSync.bump();
debug("about to send");
LLEventPumps::instance().obtain(replyName).post("received");
// give coroPump() a chance to run
llcoro::suspend();
mSync.yield();
debug("back from send");
ensure_equals(result.asString(), "received");
}
void postAndWait1()
void test_data::postAndWait1()
{
BEGIN
{
mSync.bump();
result = postAndSuspend(LLSDMap("value", 17), // request event
immediateAPI.getPump(), // requestPump
"reply1", // replyPump
"reply"); // request["reply"] = name
mSync.bump();
}
END
}
......@@ -228,22 +228,21 @@ namespace tut
template<> template<>
void object::test<4>()
{
clear();
set_test_name("postAndWait1");
DEBUG;
LLCoros::instance().launch("test<4>", postAndWait1);
// give postAndWait1() a chance to run
llcoro::suspend();
LLCoros::instance().launch("test<4>", [this](){ postAndWait1(); });
ensure_equals(result.asInteger(), 18);
}
void coroPumpPost()
void test_data::coroPumpPost()
{
BEGIN
{
mSync.bump();
LLCoroEventPump waiter;
result = waiter.postAndSuspend(LLSDMap("value", 17),
immediateAPI.getPump(), "reply");
mSync.bump();
}
END
}
......@@ -251,12 +250,9 @@ namespace tut
template<> template<>
void object::test<5>()
{
clear();
set_test_name("coroPumpPost");
DEBUG;
LLCoros::instance().launch("test<5>", coroPumpPost);
// give coroPumpPost() a chance to run
llcoro::suspend();
LLCoros::instance().launch("test<5>", [this](){ coroPumpPost(); });
ensure_equals(result.asInteger(), 18);
}
}
......@@ -67,6 +67,7 @@ set(test_HEADER_FILES
llpipeutil.h
llsdtraits.h
lltut.h
sync.h
)
if (NOT WINDOWS)
......
/**
* @file sync.h
* @author Nat Goodspeed
* @date 2019-03-13
* @brief Synchronize coroutines within a test program so we can observe side
* effects. Certain test programs test coroutine synchronization
* mechanisms. Such tests usually want to interleave coroutine
* executions in strictly stepwise fashion. This class supports that
* paradigm.
*
* $LicenseInfo:firstyear=2019&license=viewerlgpl$
* Copyright (c) 2019, Linden Research, Inc.
* $/LicenseInfo$
*/
#if ! defined(LL_SYNC_H)
#define LL_SYNC_H
#include "llcond.h"
#include "lltut.h"
#include "stringize.h"
#include "llerror.h"
#include "llcoros.h"
/**
* Instantiate Sync in any test in which we need to suspend one coroutine
* until we're sure that another has had a chance to run. Simply calling
* llcoro::suspend() isn't necessarily enough; that provides a chance for the
* other to run, but doesn't guarantee that it has. If each coroutine is
* consistent about calling Sync::bump() every time it wakes from any
* suspension, Sync::yield() and yield_until() should at least ensure that
* somebody else has had a chance to run.
*/
class Sync
{
LLScalarCond<int> mCond{0};
F32Milliseconds mTimeout;
public:
Sync(F32Milliseconds timeout=F32Milliseconds(10.0f)):
mTimeout(timeout)
{}
/// Bump mCond by n steps -- ideally, do this every time a participating
/// coroutine wakes up from any suspension. The choice to bump() after
/// resumption rather than just before suspending is worth calling out:
/// this practice relies on the fact that condition_variable::notify_all()
/// merely marks a suspended coroutine ready to run, rather than
/// immediately resuming it. This way, though, even if a coroutine exits
/// before reaching its next suspend point, the other coroutine isn't
/// left waiting forever.
void bump(int n=1)
{
LL_DEBUGS() << llcoro::logname() << " bump(" << n << ") -> " << (mCond.get() + n) << LL_ENDL;
mCond.set_all(mCond.get() + n);
}
/// suspend until "somebody else" has bumped mCond by n steps
void yield(int n=1)
{
return yield_until(STRINGIZE("Sync::yield_for(" << n << ") timed out after "
<< int(mTimeout.value()) << "ms"),
mCond.get() + n);
}
/// suspend until "somebody else" has bumped mCond to a specific value
void yield_until(int until)
{
return yield_until(STRINGIZE("Sync::yield_until(" << until << ") timed out after "
<< int(mTimeout.value()) << "ms"),
until);
}
private:
void yield_until(const std::string& desc, int until)
{
std::string name(llcoro::logname());
LL_DEBUGS() << name << " yield_until(" << until << ") suspending" << LL_ENDL;
tut::ensure(name + ' ' + desc, mCond.wait_for_equal(mTimeout, until));
// each time we wake up, bump mCond
bump();
}
};
#endif /* ! defined(LL_SYNC_H) */
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment