LeechCraft 0.6.70-17609-g3dde4097dd
Modular cross-platform feature rich live environment.
Loading...
Searching...
No Matches
corotasktest.cpp
Go to the documentation of this file.
1/**********************************************************************
2 * LeechCraft - modular cross-platform feature rich internet client.
3 * Copyright (C) 2006-2014 Georg Rudoy
4 *
5 * Distributed under the Boost Software License, Version 1.0.
6 * (See accompanying file LICENSE or copy at https://www.boost.org/LICENSE_1_0.txt)
7 **********************************************************************/
8
9#include "corotasktest.h"
10#include <QtConcurrentRun>
11#include <QtTest>
12#include <coro/future.h>
13#include <coro.h>
14#include <coro/getresult.h>
15#include <coro/inparallel.h>
16#include <coro/throttle.h>
18#include <util/sll/qtutil.h>
19
20QTEST_GUILESS_MAIN (LC::Util::CoroTaskTest)
21
22using namespace std::chrono_literals;
23
24namespace LC::Util
25{
26 void CoroTaskTest::testReturn ()
27 {
28 auto task = [] () -> Task<int> { co_return 42; } ();
29 auto result = GetTaskResult (task);
30 QCOMPARE (result, 42);
31 }
32
33 void CoroTaskTest::testWait ()
34 {
35 QElapsedTimer timer;
36 timer.start ();
37
38 auto task = [] () -> Task<int>
39 {
40 co_await 50ms;
41 co_await Precisely { 10ms };
42 co_return 42;
43 } ();
44
45 auto result = GetTaskResult (task);
46 QCOMPARE (result, 42);
47 QCOMPARE_GT (timer.elapsed (), 50);
48 }
49
50 void CoroTaskTest::testTaskDestr ()
51 {
52 bool continued = false;
53
54 [] (auto& continued) -> Task<void>
55 {
56 co_await 10ms;
57 continued = true;
58 } (continued);
59
60 QTRY_VERIFY_WITH_TIMEOUT (continued, 20);
61 }
62
63 namespace
64 {
65 // almost the Public Morozov pattern
66 class MockReply : public QNetworkReply
67 {
68 QBuffer Buffer_;
69 public:
70 using QNetworkReply::QNetworkReply;
71
72 using QNetworkReply::setAttribute;
73 using QNetworkReply::setError;
74 using QNetworkReply::setFinished;
75 using QNetworkReply::setHeader;
76 using QNetworkReply::setOperation;
77 using QNetworkReply::setRawHeader;
78 using QNetworkReply::setRequest;
79 using QNetworkReply::setUrl;
80
81 void SetData (const QByteArray& data)
82 {
83 Buffer_.setData (data);
84 Buffer_.open (QIODevice::ReadOnly);
85 open (QIODevice::ReadOnly);
86 }
87 protected:
88 qint64 readData (char *data, qint64 maxSize) override
89 {
90 return Buffer_.read (data, maxSize);
91 }
92
93 void abort () override
94 {
95 }
96 };
97
98 class MockNAM : public QNetworkAccessManager
99 {
100 QPointer<MockReply> Reply_;
101 public:
102 explicit MockNAM (MockReply *reply)
103 : Reply_ { reply }
104 {
105 }
106
107 MockReply* GetReply ()
108 {
109 return Reply_;
110 }
111 protected:
112 QNetworkReply* createRequest (Operation op, const QNetworkRequest& req, QIODevice*) override
113 {
114 Reply_->setUrl (req.url ());
115 Reply_->setOperation (op);
116 Reply_->setRequest (req);
117 return Reply_;
118 }
119 };
120
121 auto MkSuccessfulReply (const QByteArray& data)
122 {
123 auto reply = new MockReply;
124 reply->setAttribute (QNetworkRequest::HttpStatusCodeAttribute, 200);
125 reply->SetData (data);
126 return reply;
127 }
128
129 auto MkErrorReply ()
130 {
131 auto reply = new MockReply;
132 reply->setAttribute (QNetworkRequest::HttpStatusCodeAttribute, 404);
133 reply->setError (QNetworkReply::NetworkError::ContentAccessDenied, "well, 404!"_qs);
134 return reply;
135 }
136
137 void TestGoodReply (auto finishMarker)
138 {
139 const QByteArray data { "this is some test data" };
140 MockNAM nam { MkSuccessfulReply (data) };
141 finishMarker (*nam.GetReply ());
142
143 auto task = [&nam] () -> Task<QByteArray>
144 {
145 auto reply = co_await *nam.get (QNetworkRequest { QUrl { "http://example.com/foo.txt"_qs } });
146 co_return reply.GetReplyData ();
147 } ();
148
149 auto result = GetTaskResult (task);
150 QCOMPARE (result, data);
151 }
152
153 void TestBadReply (auto finishMarker)
154 {
155 MockNAM nam { MkErrorReply () };
156 finishMarker (*nam.GetReply ());
157
158 auto task = [&nam] () -> Task<QByteArray>
159 {
160 auto reply = co_await *nam.get (QNetworkRequest { QUrl { "http://example.com/foo.txt"_qs } });
161 co_return reply.GetReplyData ();
162 } ();
163
164 QVERIFY_THROWS_EXCEPTION (LC::Util::NetworkReplyErrorException, GetTaskResult (task));
165 }
166
167 void ImmediateFinishMarker (MockReply& reply)
168 {
169 reply.setFinished (true);
170 }
171
172 void DelayedFinishMarker (MockReply& reply)
173 {
174 QTimer::singleShot (10ms,
175 [&]
176 {
177 reply.setFinished (true);
178 emit reply.finished ();
179 });
180 }
181 }
182
183 void CoroTaskTest::testNetworkReplyGoodNoWait ()
184 {
185 TestGoodReply (&ImmediateFinishMarker);
186 }
187
188 void CoroTaskTest::testNetworkReplyGoodWait ()
189 {
190 TestGoodReply (&DelayedFinishMarker);
191 }
192
193 void CoroTaskTest::testNetworkReplyBadNoWait ()
194 {
195 TestBadReply (&ImmediateFinishMarker);
196 }
197
198 void CoroTaskTest::testNetworkReplyBadWait ()
199 {
200 TestBadReply (&DelayedFinishMarker);
201 }
202
203 void CoroTaskTest::testFutureAwaiter ()
204 {
205 auto delayed = [] () -> Task<int>
206 {
207 co_return co_await QtConcurrent::run ([]
208 {
209 QThread::msleep (1);
210 return 42;
211 });
212 } ();
213 QCOMPARE (GetTaskResult (delayed), 42);
214
215 auto immediate = [] () -> Task<int>
216 {
217 co_return co_await QtConcurrent::run ([] { return 42; });
218 } ();
219 QCOMPARE (GetTaskResult (immediate), 42);
220
221 auto ready = [] () -> Task<int>
222 {
223 co_return co_await MakeReadyFuture (42);
224 } ();
225 QCOMPARE (GetTaskResult (ready), 42);
226 }
227
228 namespace
229 {
230 void CompareDouble (double actual, double expected, double delta)
231 {
232 const auto diff = std::abs (actual - expected);
233 const auto midpoint = (actual + expected) / 2;
234 if (diff / midpoint >= delta)
235 {
236 auto message = std::to_string (actual) + " is too different from the expected " + std::to_string (expected);
237 QFAIL (message.c_str ());
238 }
239 }
240 }
241
242 void CoroTaskTest::testWaitMany ()
243 {
244 constexpr auto max = 100;
245 auto mkTask = [] (int index) -> Task<int>
246 {
247 co_await Precisely { std::chrono::milliseconds { max - index } };
248 co_return index;
249 };
250
251 QElapsedTimer timer;
252 timer.start ();
253 QVector<Task<int>> tasks;
254 QVector<int> expected;
255 for (int i = 0; i < max; ++i)
256 {
257 tasks << mkTask (i);
258 expected << i;
259 }
260 const auto creationElapsed = timer.elapsed ();
261
262 timer.restart ();
263 auto result = GetTaskResult (InParallel (std::move (tasks)));
264 const auto executionElapsed = timer.elapsed ();
265
266 QCOMPARE (result, expected);
267 QCOMPARE_LT (creationElapsed, 1);
268 CompareDouble (executionElapsed, max, 0.05);
269 }
270
271 void CoroTaskTest::testWaitManyTuple ()
272 {
273 auto mkTask = [] (int delay) -> Task<int>
274 {
275 co_await Precisely { std::chrono::milliseconds { delay } };
276 co_return delay;
277 };
278
279 QElapsedTimer timer;
280 timer.start ();
281 auto result = GetTaskResult (InParallel (mkTask (10), mkTask (9), mkTask (2), mkTask (1)));
282 const auto executionElapsed = timer.elapsed ();
283
284 QCOMPARE (result, (std::tuple { 10, 9, 2, 1 }));
285 CompareDouble (executionElapsed, 10, 0.05);
286 }
287
288 void CoroTaskTest::testEither ()
289 {
290 using Result_t = Either<QString, bool>;
291
292 auto immediatelyFailing = [] () -> Task<Result_t>
293 {
294 const auto theInt = co_await Either<QString, int> { "meh" };
295 co_return { theInt > 420 };
296 } ();
297 QCOMPARE (GetTaskResult (immediatelyFailing), Result_t { Left { "meh" } });
298
299 auto earlyFailing = [] () -> Task<Result_t>
300 {
301 const auto theInt = co_await Either<QString, int> { "meh" };
302 co_await 10ms;
303 co_return { theInt > 420 };
304 } ();
305 QCOMPARE (GetTaskResult (earlyFailing), Result_t { Left { "meh" } });
306
307 auto successful = [] () -> Task<Result_t>
308 {
309 const auto theInt = co_await Either<QString, int> { 42 };
310 co_await 10ms;
311 co_return { theInt > 420 };
312 } ();
313 QCOMPARE (GetTaskResult (successful), Result_t { false });
314 }
315
316 void CoroTaskTest::testThrottleSameCoro ()
317 {
318 Throttle t { 10ms };
319 constexpr auto count = 10;
320
321 QElapsedTimer timer;
322 timer.start ();
323 auto task = [] (auto& t) -> Task<int>
324 {
325 int result = 0;
326 for (int i = 0; i < count; ++i)
327 {
328 co_await t;
329 result += i;
330 }
331 co_return result;
332 } (t);
333 const auto result = GetTaskResult (task);
334 const auto time = timer.elapsed ();
335
336 QCOMPARE (result, count * (count - 1) / 2);
337 QCOMPARE_GE (time, count * t.GetInterval ().count ());
338 }
339
340 void CoroTaskTest::testThrottleSameCoroSlow ()
341 {
342 Throttle t { 10ms };
343 constexpr auto count = 10;
344
345 QElapsedTimer timer;
346 timer.start ();
347 auto task = [] (auto& t) -> Task<void>
348 {
349 for (int i = 0; i < count; ++i)
350 {
351 co_await t;
352 if (i != count - 1)
353 co_await Precisely { 9ms };
354 }
355 } (t);
356 GetTaskResult (task);
357 const auto time = timer.elapsed ();
358
359 const auto expectedMinTime = count * t.GetInterval ().count ();
360 QCOMPARE_GE (time, expectedMinTime);
361 QCOMPARE_LE (time - expectedMinTime, expectedMinTime * 0.05);
362 }
363
364 void CoroTaskTest::testThrottleManyCoros ()
365 {
366 Throttle t { 1ms, Qt::TimerType::PreciseTimer };
367 constexpr auto count = 10;
368
369 QElapsedTimer timer;
370 timer.start ();
371 auto mkTask = [] (auto& t) -> Task<void>
372 {
373 for (int i = 0; i < count; ++i)
374 co_await t;
375 };
376 QVector tasks { mkTask (t), mkTask (t), mkTask (t) };
377 for (auto& task : tasks)
378 GetTaskResult (task);
379 const auto time = timer.elapsed ();
380
381 QCOMPARE_GE (time, count * tasks.size () * t.GetInterval ().count ());
382 }
383
384 constexpr auto LongDelay = 500ms;
385 constexpr auto ShortDelay = 10ms;
386 constexpr auto DelayThreshold = std::chrono::duration_cast<std::chrono::milliseconds> ((ShortDelay + LongDelay) / 2);
387
388 void CoroTaskTest::testContextDestrBeforeFinish ()
389 {
390 auto context = std::make_unique<QObject> ();
391 auto task = [] (QObject *context) -> ContextTask<int>
392 {
393 co_await AddContextObject { *context };
394 co_await LongDelay;
395 co_return context->children ().size ();
396 } (&*context);
397 context.reset ();
398
399 QVERIFY_THROWS_EXCEPTION (LC::Util::ContextDeadException, GetTaskResult (task));
400 }
401
402 void CoroTaskTest::testContextDestrAfterFinish ()
403 {
404 auto context = std::make_unique<QObject> ();
405 auto task = [] (QObject *context) -> ContextTask<int>
406 {
407 co_await AddContextObject { *context };
408 co_await ShortDelay;
409 co_return context->children ().size ();
410 } (&*context);
411
412 QCOMPARE (GetTaskResult (task), 0);
413 }
414
415 namespace
416 {
417 template<typename... Ts>
418 auto WithContext (auto&& taskGen, Ts&&... taskArgs)
419 {
420 auto context = std::make_unique<QObject> ();
421 auto task = taskGen (&*context, std::forward<Ts> (taskArgs)...);
422 QTimer::singleShot (ShortDelay, [context = std::move (context)] () mutable { context.reset (); });
423 return task;
424 }
425
426 void WithDestroyTimer (auto task)
427 {
428 QElapsedTimer timer;
429 timer.start ();
430 QVERIFY_THROWS_EXCEPTION (LC::Util::ContextDeadException, GetTaskResult (task));
431 QCOMPARE_LT (timer.elapsed (), DelayThreshold.count ());
432 }
433 }
434
435 void CoroTaskTest::testContextDestrDoesntWaitTimer ()
436 {
437 WithDestroyTimer (WithContext ([] (QObject *context) -> ContextTask<void>
438 {
439 co_await AddContextObject { *context };
440 co_await LongDelay;
441 }));
442 }
443
444 void CoroTaskTest::testContextDestrDoesntWaitNetwork ()
445 {
446 const QByteArray data { "this is some test data" };
447 auto nam = std::make_shared<MockNAM> (MkSuccessfulReply (data));
448 QTimer::singleShot (LongDelay,
449 [nam]
450 {
451 if (const auto reply = nam->GetReply ())
452 {
453 reply->setFinished (true);
454 emit reply->finished ();
455 }
456 });
457
458 WithDestroyTimer (WithContext ([] (QObject *context, QNetworkAccessManager *nam) -> ContextTask<QByteArray>
459 {
460 co_await AddContextObject { *context };
461 const auto reply = co_await *nam->get (QNetworkRequest { QUrl { "http://example.com/foo.txt"_qs } });
462 co_return reply.GetReplyData ();
463 }, &*nam));
464 }
465
466 void CoroTaskTest::testContextDestrDoesntWaitProcess ()
467 {
468 WithDestroyTimer (WithContext ([] (QObject *context) -> ContextTask<>
469 {
470 co_await AddContextObject { *context };
471
472 const auto process = new QProcess {};
473 const auto delay = std::chrono::duration_cast<std::chrono::milliseconds> (LongDelay);
474 process->start ("sleep", { QString::number (delay.count () / 1000.0) });
475 connect (process,
476 &QProcess::finished,
477 process,
478 &QObject::deleteLater);
479
480 co_await *process;
481 }));
482 }
483
484 void CoroTaskTest::testContextDestrDoesntWaitFuture ()
485 {
486 WithDestroyTimer (WithContext ([] (QObject *context) -> ContextTask<>
487 {
488 co_await AddContextObject { *context };
489 co_await QtConcurrent::run ([] { QThread::sleep (LongDelay); });
490 }));
491 }
492
493 void CoroTaskTest::cleanupTestCase ()
494 {
495 bool done = false;
496 QTimer::singleShot (LongDelay * 2, [&done] { done = true; });
497 QTRY_VERIFY (done);
498 }
499}
constexpr detail::AggregateType< detail::AggregateFunction::Count, Ptr > count
Definition oral.h:962
constexpr detail::AggregateType< detail::AggregateFunction::Max, Ptr > max
Definition oral.h:968
Task< QVector< T >, Exts... > InParallel(QVector< Task< T, Exts... > > tasks)
Definition inparallel.h:17
constexpr auto LongDelay
Task< R, ContextExtensions > ContextTask
Definition taskfwd.h:20
constexpr auto ShortDelay
WithPrecision< Qt::PreciseTimer > Precisely
Definition timer.h:43
T GetTaskResult(Task< T, Extensions... > task)
Definition getresult.h:18
constexpr auto DelayThreshold