summaryrefslogtreecommitdiff
path: root/indra/llcommon/tests/workqueue_test.cpp
blob: 7655a7aa1f7ab24ad2b695a34ccdca820c13aed9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
/**
 * @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();
        // 2s seems like a long time to wait, since it directly impacts the
        // duration of this test program. Unfortunately GitHub's Mac runners
        // are pretty wimpy, and we're getting spurious "too late" errors just
        // because the thread doesn't wake up as soon as we want.
        auto interval = 2s;
        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 have exhausted the iterations
        // or we time out waiting
        for (auto finish = start + 10*interval;
             WorkQueue::TimePoint::clock::now() < finish &&
             data.get([](const Shared& data){ return data.size(); }) < 3; )
        {
            queue.runPending();
            std::this_thread::sleep_for(interval/10);
        }
        // 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