230 lines
8.2 KiB
C++
230 lines
8.2 KiB
C++
/**
|
|
* @file workqueue_test.cpp
|
|
* @author Nat Goodspeed
|
|
* @date 2021-10-07
|
|
* @brief Test for workqueue.
|
|
*
|
|
* $LicenseInfo:firstyear=2021&license=viewerlgpl$
|
|
* Copyright (c) 2021, Linden Research, Inc.
|
|
* $/LicenseInfo$
|
|
*/
|
|
|
|
// Precompiled header
|
|
#include "linden_common.h"
|
|
// associated header
|
|
#include "workqueue.h"
|
|
// STL headers
|
|
// std headers
|
|
#include <chrono>
|
|
#include <deque>
|
|
// external library headers
|
|
// other Linden headers
|
|
#include "../test/lltut.h"
|
|
#include "../test/catch_and_store_what_in.h"
|
|
#include "llcond.h"
|
|
#include "llcoros.h"
|
|
#include "lleventcoro.h"
|
|
#include "llstring.h"
|
|
#include "stringize.h"
|
|
|
|
using namespace LL;
|
|
using namespace std::literals::chrono_literals; // ms suffix
|
|
using namespace std::literals::string_literals; // s suffix
|
|
|
|
/*****************************************************************************
|
|
* TUT
|
|
*****************************************************************************/
|
|
namespace tut
|
|
{
|
|
struct workqueue_data
|
|
{
|
|
WorkQueue queue{"queue"};
|
|
};
|
|
typedef test_group<workqueue_data> workqueue_group;
|
|
typedef workqueue_group::object object;
|
|
workqueue_group workqueuegrp("workqueue");
|
|
|
|
template<> template<>
|
|
void object::test<1>()
|
|
{
|
|
set_test_name("name");
|
|
ensure_equals("didn't capture name", queue.getKey(), "queue");
|
|
ensure("not findable", WorkQueue::getInstance("queue") == queue.getWeak().lock());
|
|
WorkQueue q2;
|
|
ensure("has no name", LLStringUtil::startsWith(q2.getKey(), "WorkQueue"));
|
|
}
|
|
|
|
template<> template<>
|
|
void object::test<2>()
|
|
{
|
|
set_test_name("post");
|
|
bool wasRun{ false };
|
|
// We only get away with binding a simple bool because we're running
|
|
// the work on the same thread.
|
|
queue.post([&wasRun](){ wasRun = true; });
|
|
queue.close();
|
|
ensure("ran too soon", ! wasRun);
|
|
queue.runUntilClose();
|
|
ensure("didn't run", wasRun);
|
|
}
|
|
|
|
template<> template<>
|
|
void object::test<3>()
|
|
{
|
|
set_test_name("postEvery");
|
|
// record of runs
|
|
using Shared = std::deque<WorkQueue::TimePoint>;
|
|
// This is an example of how to share data between the originator of
|
|
// postEvery(work) and the work item itself, since usually a WorkQueue
|
|
// is used to dispatch work to a different thread. Neither of them
|
|
// should call any of LLCond's wait methods: you don't want to stall
|
|
// either the worker thread or the originating thread (conventionally
|
|
// main). Use LLCond or a subclass even if all you want to do is
|
|
// signal the work item that it can quit; consider LLOneShotCond.
|
|
LLCond<Shared> data;
|
|
auto start = WorkQueue::TimePoint::clock::now();
|
|
auto interval = 100ms;
|
|
queue.postEvery(
|
|
interval,
|
|
[&data, count = 0]
|
|
() mutable
|
|
{
|
|
// record the timestamp at which this instance is running
|
|
data.update_one(
|
|
[](Shared& data)
|
|
{
|
|
data.push_back(WorkQueue::TimePoint::clock::now());
|
|
});
|
|
// by the 3rd call, return false to stop
|
|
return (++count < 3);
|
|
});
|
|
// no convenient way to close() our queue while we've got a
|
|
// postEvery() running, so run until we think we should have exhausted
|
|
// the iterations
|
|
queue.runFor(10*interval);
|
|
// Take a copy of the captured deque.
|
|
Shared result = data.get();
|
|
ensure_equals("called wrong number of times", result.size(), 3);
|
|
// postEvery() assumes you want the first call to happen right away.
|
|
// Pretend our start time was (interval) earlier than that, to make
|
|
// our too early/too late tests uniform for all entries.
|
|
start -= interval;
|
|
for (size_t i = 0; i < result.size(); ++i)
|
|
{
|
|
auto diff = result[i] - start;
|
|
start += interval;
|
|
try
|
|
{
|
|
ensure(STRINGIZE("call " << i << " too soon"), diff >= interval);
|
|
ensure(STRINGIZE("call " << i << " too late"), diff < interval*1.5);
|
|
}
|
|
catch (const tut::failure&)
|
|
{
|
|
auto interval_ms = interval / 1ms;
|
|
auto diff_ms = diff / 1ms;
|
|
std::cerr << "interval " << interval_ms
|
|
<< "ms; diff " << diff_ms << "ms" << std::endl;
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
template<> template<>
|
|
void object::test<4>()
|
|
{
|
|
set_test_name("postTo");
|
|
WorkQueue main("main");
|
|
auto qptr = WorkQueue::getInstance("queue");
|
|
int result = 0;
|
|
main.postTo(
|
|
qptr,
|
|
[](){ return 17; },
|
|
// Note that a postTo() *callback* can safely bind a reference to
|
|
// a variable on the invoking thread, because the callback is run
|
|
// on the invoking thread. (Of course the bound variable must
|
|
// survive until the callback is called.)
|
|
[&result](int i){ result = i; });
|
|
// this should post the callback to main
|
|
qptr->runOne();
|
|
// this should run the callback
|
|
main.runOne();
|
|
ensure_equals("failed to run int callback", result, 17);
|
|
|
|
std::string alpha;
|
|
// postTo() handles arbitrary return types
|
|
main.postTo(
|
|
qptr,
|
|
[](){ return "abc"s; },
|
|
[&alpha](const std::string& s){ alpha = s; });
|
|
qptr->runPending();
|
|
main.runPending();
|
|
ensure_equals("failed to run string callback", alpha, "abc");
|
|
}
|
|
|
|
template<> template<>
|
|
void object::test<5>()
|
|
{
|
|
set_test_name("postTo with void return");
|
|
WorkQueue main("main");
|
|
auto qptr = WorkQueue::getInstance("queue");
|
|
std::string observe;
|
|
main.postTo(
|
|
qptr,
|
|
// The ONLY reason we can get away with binding a reference to
|
|
// 'observe' in our work callable is because we're directly
|
|
// calling qptr->runOne() on this same thread. It would be a
|
|
// mistake to do that if some other thread were servicing 'queue'.
|
|
[&observe](){ observe = "queue"; },
|
|
[&observe](){ observe.append(";main"); });
|
|
qptr->runOne();
|
|
main.runOne();
|
|
ensure_equals("failed to run both lambdas", observe, "queue;main");
|
|
}
|
|
|
|
template<> template<>
|
|
void object::test<6>()
|
|
{
|
|
set_test_name("waitForResult");
|
|
std::string stored;
|
|
// Try to call waitForResult() on this thread's main coroutine. It
|
|
// should throw because the main coroutine must service the queue.
|
|
auto what{ catch_what<WorkQueue::Error>(
|
|
[this, &stored](){ stored = queue.waitForResult(
|
|
[](){ return "should throw"; }); }) };
|
|
ensure("lambda should not have run", stored.empty());
|
|
ensure_not("waitForResult() should have thrown", what.empty());
|
|
ensure(STRINGIZE("should mention waitForResult: " << what),
|
|
what.find("waitForResult") != std::string::npos);
|
|
|
|
// Call waitForResult() on a coroutine, with a string result.
|
|
LLCoros::instance().launch(
|
|
"waitForResult string",
|
|
[this, &stored]()
|
|
{ stored = queue.waitForResult(
|
|
[](){ return "string result"; }); });
|
|
llcoro::suspend();
|
|
// Nothing will have happened yet because, even if the coroutine did
|
|
// run immediately, all it did was to queue the inner lambda on
|
|
// 'queue'. Service it.
|
|
queue.runOne();
|
|
llcoro::suspend();
|
|
ensure_equals("bad waitForResult return", stored, "string result");
|
|
|
|
// Call waitForResult() on a coroutine, with a void callable.
|
|
stored.clear();
|
|
bool done = false;
|
|
LLCoros::instance().launch(
|
|
"waitForResult void",
|
|
[this, &stored, &done]()
|
|
{
|
|
queue.waitForResult([&stored](){ stored = "ran"; });
|
|
done = true;
|
|
});
|
|
llcoro::suspend();
|
|
queue.runOne();
|
|
llcoro::suspend();
|
|
ensure_equals("didn't run coroutine", stored, "ran");
|
|
ensure("void waitForResult() didn't return", done);
|
|
}
|
|
} // namespace tut
|