LeechCraft 0.6.70-19214-g121e5318a7
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 <algorithm>
11#include <csignal>
12#include <functional>
13#include <stdexcept>
14#include <string>
15#include <thread>
16#include <variant>
17#include <QProcess>
18#include <QProcessEnvironment>
19#include <QRegularExpression>
20#include <QtConcurrentRun>
21#include <QtTest>
22#ifdef QT_DBUS_LIB
23#include <QDBusInterface>
24#endif
25#include <coro/future.h>
26#include <coro.h>
27#include <coro/getresult.h>
28#include <coro/inparallel.h>
29#include <coro/sharedtask.h>
30#include <coro/throttle.h>
31#ifdef QT_DBUS_LIB
32#include <coro/dbus.h>
33#endif
36#include <util/sll/qtutil.h>
37#include <util/sll/util.h>
38
39using Scenario = std::function<void ()>;
41
42QTEST_GUILESS_MAIN (LC::Util::CoroTaskTest)
43
44using namespace std::chrono_literals;
45
46namespace LC::Util
47{
48 namespace
49 {
50 constexpr auto TimerTolerance = 0.05;
51
52 constexpr auto MinElapsed (std::chrono::milliseconds expected)
53 {
54 return expected.count () * (1 - TimerTolerance);
55 }
56 }
57
58 void CoroTaskTest::testReturn ()
59 {
60 auto task = [] () -> Task<int> { co_return 42; } ();
61 auto result = GetTaskResult (task);
62 QCOMPARE (result, 42);
63 }
64
65 void CoroTaskTest::testMoveOnlyReturn ()
66 {
67 auto direct = [] () -> Task<std::unique_ptr<int>>
68 {
69 co_return std::make_unique<int> (42);
70 } ();
71 auto directResult = GetTaskResult (direct);
72 QVERIFY (directResult);
73 QCOMPARE (*directResult, 42);
74
75 auto chained = [] () -> Task<std::unique_ptr<int>>
76 {
77 auto inner = [] () -> Task<std::unique_ptr<int>>
78 {
79 co_await 5ms;
80 co_return std::make_unique<int> (7);
81 } ();
82 auto value = co_await inner;
83 *value *= 6;
84 co_return std::move (value);
85 } ();
86 auto chainedResult = GetTaskResult (chained);
87 QVERIFY (chainedResult);
88 QCOMPARE (*chainedResult, 42);
89 }
90
91 void CoroTaskTest::testWait ()
92 {
93 QElapsedTimer timer;
94 timer.start ();
95
96 auto task = [] () -> Task<int>
97 {
98 co_await 50ms;
99 co_await Precisely { 10ms };
100 co_return 42;
101 } ();
102
103 auto result = GetTaskResult (task);
104 QCOMPARE (result, 42);
105 QCOMPARE_GT (timer.elapsed (), 50);
106 }
107
108 void CoroTaskTest::testTaskDestr ()
109 {
110 bool continued = false;
111
112 [] (auto& continued) -> Task<void>
113 {
114 co_await 10ms;
115 continued = true;
116 } (continued);
117
118 QTRY_VERIFY_WITH_TIMEOUT (continued, 20);
119 }
120
121 namespace
122 {
123 // almost the Public Morozov pattern
124 class MockReply : public QNetworkReply
125 {
126 QBuffer Buffer_;
127 public:
128 using QNetworkReply::QNetworkReply;
129
130 using QNetworkReply::setAttribute;
131 using QNetworkReply::setError;
132 using QNetworkReply::setFinished;
133 using QNetworkReply::setHeader;
134 using QNetworkReply::setOperation;
135 using QNetworkReply::setRawHeader;
136 using QNetworkReply::setRequest;
137 using QNetworkReply::setUrl;
138
139 void SetData (const QByteArray& data)
140 {
141 Buffer_.setData (data);
142 Buffer_.open (QIODevice::ReadOnly);
143 open (QIODevice::ReadOnly);
144 }
145 protected:
146 qint64 readData (char *data, qint64 maxSize) override
147 {
148 return Buffer_.read (data, maxSize);
149 }
150
151 void abort () override
152 {
153 }
154 };
155
156 class MockNAM : public QNetworkAccessManager
157 {
158 QPointer<MockReply> Reply_;
159 public:
160 explicit MockNAM (MockReply *reply)
161 : Reply_ { reply }
162 {
163 }
164
165 MockReply* GetReply ()
166 {
167 return Reply_;
168 }
169 protected:
170 QNetworkReply* createRequest (Operation op, const QNetworkRequest& req, QIODevice*) override
171 {
172 Reply_->setUrl (req.url ());
173 Reply_->setOperation (op);
174 Reply_->setRequest (req);
175 return Reply_;
176 }
177 };
178
179 auto MkSuccessfulReply (const QByteArray& data)
180 {
181 auto reply = new MockReply;
182 reply->setAttribute (QNetworkRequest::HttpStatusCodeAttribute, 200);
183 reply->SetData (data);
184 return reply;
185 }
186
187 auto MkErrorReply ()
188 {
189 auto reply = new MockReply;
190 reply->setAttribute (QNetworkRequest::HttpStatusCodeAttribute, 404);
191 reply->setError (QNetworkReply::NetworkError::ContentAccessDenied, "well, 404!"_qs);
192 return reply;
193 }
194
195 void TestGoodReply (auto finishMarker)
196 {
197 const QByteArray data { "this is some test data" };
198 MockNAM nam { MkSuccessfulReply (data) };
199 finishMarker (*nam.GetReply ());
200
201 auto task = [&nam] () -> Task<QByteArray>
202 {
203 auto reply = co_await *nam.get (QNetworkRequest { QUrl { "http://example.com/foo.txt"_qs } });
204 co_return reply.GetReplyData ();
205 } ();
206
207 auto result = GetTaskResult (task);
208 QCOMPARE (result, data);
209 }
210
211 void TestBadReply (auto finishMarker)
212 {
213 MockNAM nam { MkErrorReply () };
214 finishMarker (*nam.GetReply ());
215
216 auto task = [&nam] () -> Task<QByteArray>
217 {
218 auto reply = co_await *nam.get (QNetworkRequest { QUrl { "http://example.com/foo.txt"_qs } });
219 co_return reply.GetReplyData ();
220 } ();
221
222 QVERIFY_THROWS_EXCEPTION (LC::Util::NetworkReplyErrorException, GetTaskResult (task));
223 }
224
225 void ImmediateFinishMarker (MockReply& reply)
226 {
227 reply.setFinished (true);
228 }
229
230 void DelayedFinishMarker (MockReply& reply)
231 {
232 QTimer::singleShot (10ms,
233 [&]
234 {
235 reply.setFinished (true);
236 emit reply.finished ();
237 });
238 }
239 }
240
241 void CoroTaskTest::testNetworkReplyGoodNoWait ()
242 {
243 TestGoodReply (&ImmediateFinishMarker);
244 }
245
246 void CoroTaskTest::testNetworkReplyGoodWait ()
247 {
248 TestGoodReply (&DelayedFinishMarker);
249 }
250
251 void CoroTaskTest::testNetworkReplyBadNoWait ()
252 {
253 TestBadReply (&ImmediateFinishMarker);
254 }
255
256 void CoroTaskTest::testNetworkReplyBadWait ()
257 {
258 TestBadReply (&DelayedFinishMarker);
259 }
260
261 void CoroTaskTest::testFutureAwaiter ()
262 {
263 auto delayed = [] () -> Task<int>
264 {
265 co_return co_await QtConcurrent::run ([]
266 {
267 QThread::msleep (1);
268 return 42;
269 });
270 } ();
271 QCOMPARE (GetTaskResult (delayed), 42);
272
273 auto immediate = [] () -> Task<int>
274 {
275 co_return co_await QtConcurrent::run ([] { return 42; });
276 } ();
277 QCOMPARE (GetTaskResult (immediate), 42);
278
279 auto ready = [] () -> Task<int>
280 {
281 co_return co_await MakeReadyFuture (42);
282 } ();
283 QCOMPARE (GetTaskResult (ready), 42);
284 }
285
286 void CoroTaskTest::testWaitMany ()
287 {
288 constexpr auto max = 100;
289 auto mkTask = [] (int index) -> Task<int>
290 {
291 co_await Precisely { std::chrono::milliseconds { max - index } };
292 co_return index;
293 };
294
295 QElapsedTimer timer;
296 timer.start ();
297 QVector<Task<int>> tasks;
298 QVector<int> expected;
299 for (int i = 0; i < max; ++i)
300 {
301 tasks << mkTask (i);
302 expected << i;
303 }
304 const auto creationElapsed = timer.elapsed ();
305
306 timer.restart ();
307 auto result = GetTaskResult (InParallel (std::move (tasks)));
308 const auto executionElapsed = timer.elapsed ();
309
310 QCOMPARE (result, expected);
311 QCOMPARE_LT (creationElapsed, 1);
312
313 QCOMPARE_GE (executionElapsed, MinElapsed (max * 1ms));
314 const auto linearizedExecTime = max * (max + 1) / 2;
315 QCOMPARE_LT (executionElapsed, linearizedExecTime / 2);
316 }
317
318 void CoroTaskTest::testWaitManyTuple ()
319 {
320 auto mkTask = [] (int delay) -> Task<int>
321 {
322 co_await Precisely { std::chrono::milliseconds { delay } };
323 co_return delay;
324 };
325
326 QElapsedTimer timer;
327 timer.start ();
328 auto result = GetTaskResult (InParallel (mkTask (10), mkTask (9), mkTask (2), mkTask (1)));
329 const auto executionElapsed = timer.elapsed ();
330
331 QCOMPARE (result, (std::tuple { 10, 9, 2, 1 }));
332
333 QCOMPARE_GE (executionElapsed, 10);
334 QCOMPARE_LT (executionElapsed, (10 + 9 + 2 + 1) / 2);
335 }
336
337 void CoroTaskTest::testWaitManyInvoking ()
338 {
339 constexpr auto max = 100;
340 auto mkTask = [] (int index) -> Task<int>
341 {
342 co_await Precisely { std::chrono::milliseconds { max - index } };
343 co_return index;
344 };
345
346 QElapsedTimer timer;
347 timer.start ();
348 QVector<int> inputs;
349 QVector<int> expected;
350 for (int i = 0; i < max; ++i)
351 {
352 inputs << i;
353 expected << i;
354 }
355 const auto creationElapsed = timer.elapsed ();
356
357 timer.restart ();
358 auto result = GetTaskResult (InParallel (inputs, mkTask));
359 const auto executionElapsed = timer.elapsed ();
360
361 QCOMPARE (result, expected);
362 QCOMPARE_LT (creationElapsed, 1);
363
364 QCOMPARE_GE (executionElapsed, MinElapsed (max * 1ms));
365 const auto linearizedExecTime = max * (max + 1) / 2;
366 QCOMPARE_LT (executionElapsed, linearizedExecTime / 2);
367 }
368
369 void CoroTaskTest::testNCopies ()
370 {
371 constexpr auto count = 10;
372 static constexpr auto unit = 10ms;
373
374 auto mkTask = [] (int index) -> Task<int>
375 {
376 co_await Precisely { unit * (count - index) };
377 co_return index;
378 };
379
380 int created = 0;
381 QElapsedTimer timer;
382 timer.start ();
383 auto task = NCopies (count, [&] { return mkTask (created++); });
384 const auto creationElapsed = timer.elapsed ();
385
386 static_assert (std::is_same_v<decltype (task), Task<QVector<int>>>);
387 QCOMPARE (created, count);
388 QCOMPARE_LT (creationElapsed, unit.count ());
389
390 timer.restart ();
391 const auto result = GetTaskResult (task);
392 const auto executionElapsed = timer.elapsed ();
393
394 QVector<int> expected;
395 for (int i = 0; i < count; ++i)
396 expected << i;
397 QCOMPARE (result, expected);
398
399 QCOMPARE_GE (executionElapsed, MinElapsed (count * unit));
400 const auto linearizedExecTime = unit.count () * count * (count + 1) / 2;
401 QCOMPARE_LT (executionElapsed, linearizedExecTime / 2);
402 }
403
404 void CoroTaskTest::testNCopiesVoid ()
405 {
406 constexpr auto count = 5;
407 auto mkTask = [] (int& finished) -> Task<void>
408 {
409 co_await 10ms;
410 ++finished;
411 };
412
413 int finished = 0;
414 auto task = NCopies (count, [&] { return mkTask (finished); });
415 static_assert (std::is_same_v<decltype (task), Task<void>>);
416 QCOMPARE (finished, 0);
417
418 GetTaskResult (task);
419 QCOMPARE (finished, count);
420
421 [[maybe_unused]] auto mkContextTask = [] () -> ContextTask<void> { co_return; };
422 static_assert (std::is_same_v<decltype (NCopies (1, mkContextTask)), ContextTask<void>>,
423 "extensions did not survive the delegation");
424 }
425
426 void CoroTaskTest::testNCopiesZero ()
427 {
428 int created = 0;
429 auto mkTask = [] (int& created) -> Task<int>
430 {
431 ++created;
432 co_return 0;
433 };
434
435 const auto result = GetTaskResult (NCopies (0, [&] { return mkTask (created); }));
436 QVERIFY (result.isEmpty ());
437 QCOMPARE (created, 0);
438 }
439
440 namespace
441 {
442 Task<int> SlowSuccess (std::chrono::milliseconds delay, bool& done)
443 {
444 co_await Precisely { delay };
445 done = true;
446 co_return 0;
447 }
448
449 Task<int> SlowFailure (std::chrono::milliseconds delay, std::string what)
450 {
451 co_await Precisely { delay };
452 throw std::runtime_error { what };
453 }
454
455 QString ThrownMessage (auto&& f)
456 {
457 try
458 {
459 f ();
460 }
461 catch (const std::exception& e)
462 {
463 return QString::fromUtf8 (e.what ());
464 }
465 return {};
466 }
467 }
468
469 void CoroTaskTest::testWaitManyJoinsAfterFailure ()
470 {
471 constexpr auto slowDelay = 60ms;
472 bool slowDone = false;
473 QElapsedTimer timer;
474 timer.start ();
475 const auto message = ThrownMessage ([&] { GetTaskResult (InParallel ({ SlowFailure (10ms, "early"), SlowSuccess (slowDelay, slowDone) })); });
476 const auto elapsed = timer.elapsed ();
477
478 QCOMPARE (message, "early"_qs);
479 QVERIFY (slowDone);
480 QCOMPARE_GE (elapsed, MinElapsed (slowDelay));
481 }
482
483 void CoroTaskTest::testWaitManyVoidJoinsAfterFailure ()
484 {
485 auto slowSuccess = [] (bool& done) -> Task<void>
486 {
487 co_await Precisely { 50ms };
488 done = true;
489 };
490 auto earlyFailure = [] () -> Task<void>
491 {
492 co_await Precisely { 5ms };
493 throw std::runtime_error { "early" };
494 };
495
496 bool slowDone = false;
497 const auto message = ThrownMessage ([&] { GetTaskResult (InParallel ({ earlyFailure (), slowSuccess (slowDone) })); });
498 QCOMPARE (message, "early"_qs);
499 QVERIFY (slowDone);
500 }
501
502 void CoroTaskTest::testWaitManyFirstFailureInOrderWins ()
503 {
504 QTest::ignoreMessage (QtWarningMsg, "dropping a subsequent exception: first in time");
505
506 const auto message = ThrownMessage ([] { GetTaskResult (InParallel ({ SlowFailure (40ms, "first in order"), SlowFailure (10ms, "first in time") })); });
507 QCOMPARE (message, "first in order"_qs);
508 }
509
510 void CoroTaskTest::testWaitManyInvokingFactoryThrows ()
511 {
512 constexpr auto delay = 40ms;
513 QVector<bool> done (5, false);
514 int created = 0;
515 auto mkTask = [&] (int index)
516 {
517 if (index == 2)
518 throw std::runtime_error { "factory" };
519 ++created;
520 return SlowSuccess (delay, done [index]);
521 };
522
523 QElapsedTimer timer;
524 timer.start ();
525 const auto message = ThrownMessage ([&] { GetTaskResult (InParallel (QVector { 0, 1, 2, 3, 4 }, mkTask)); });
526 const auto elapsed = timer.elapsed ();
527
528 QCOMPARE (message, "factory"_qs);
529 QCOMPARE (created, 2);
530 QCOMPARE (done, (QVector { true, true, false, false, false }));
531 QCOMPARE_GE (elapsed, MinElapsed (delay));
532 }
533
534 void CoroTaskTest::testNCopiesFactoryThrows ()
535 {
536 bool done = false;
537 int calls = 0;
538 auto taskFactory = [&]
539 {
540 if (calls++ == 1)
541 throw std::runtime_error { "factory" };
542 return SlowSuccess (30ms, done);
543 };
544
545 const auto message = ThrownMessage ([&] { GetTaskResult (NCopies (3, taskFactory)); });
546 QCOMPARE (message, "factory"_qs);
547 QCOMPARE (calls, 2);
548 QVERIFY (done);
549 }
550
551 void CoroTaskTest::testWaitManyContextDeathSilenced ()
552 {
553 QTest::failOnWarning (QRegularExpression { "dropping a subsequent"_qs });
554
555 auto context = std::make_unique<CoroContext> ();
556 auto mkTask = [] (CoroContext *context) -> ContextTask<void>
557 {
558 co_await AddContext { *context };
559 co_await 500ms;
560 };
561 auto task = InParallel ({ mkTask (context.get ()), mkTask (context.get ()), mkTask (context.get ()) });
562 QTimer::singleShot (10ms, [context = std::move (context)] () mutable { context.reset (); });
563
564 QElapsedTimer timer;
565 timer.start ();
566 QVERIFY_THROWS_EXCEPTION (ContextDeadException, GetTaskResult (task));
567 QCOMPARE_LT (timer.elapsed (), 250);
568 }
569
570 void CoroTaskTest::testSharedTaskManyAwaiters ()
571 {
572 auto shared = [] () -> SharedTask<int>
573 {
574 co_await 10ms;
575 co_return 42;
576 } ();
577
578 auto mkAwaiter = [&shared] (int offset) -> Task<int>
579 {
580 co_return co_await shared + offset;
581 };
582
583 const auto result = GetTaskResult (InParallel (mkAwaiter (0), mkAwaiter (1)));
584 QCOMPARE (result, (std::tuple { 42, 43 }));
585 }
586
587 void CoroTaskTest::testSharedTaskAwaiterRemovedOnOuterDestruction ()
588 {
589 constexpr auto destructionDelay = 10ms;
590 auto shared = [] () -> SharedTask<int>
591 {
592 co_await 500ms;
593 co_return 42;
594 } ();
595
596 auto survivingAwaiter = [&shared] () -> Task<int>
597 {
598 co_return co_await shared;
599 } ();
600
601 auto context = std::make_unique<CoroContext> ();
602 auto cancelledAwaiter = [&shared] (CoroContext *context) -> ContextTask<int>
603 {
604 co_await AddContext { *context };
605 co_return co_await shared;
606 } (&*context);
607 QTimer::singleShot (destructionDelay, [context = std::move (context)] () mutable { context.reset (); });
608
609 QCOMPARE (GetTaskResult (survivingAwaiter), 42);
610 QVERIFY_THROWS_EXCEPTION (LC::Util::ContextDeadException, GetTaskResult (cancelledAwaiter));
611 }
612
613 void CoroTaskTest::testSharedTaskExceptionManyAwaiters ()
614 {
615 struct TestSharedTaskException : std::exception {};
616
617 auto shared = [] () -> SharedTask<>
618 {
619 co_await 10ms;
620 throw TestSharedTaskException {};
621 } ();
622
623 auto mkAwaiter = [&shared] () -> Task<bool>
624 {
625 try
626 {
627 co_await shared;
628 co_return false;
629 }
630 catch (const TestSharedTaskException&)
631 {
632 co_return true;
633 }
634 };
635
636 const auto result = GetTaskResult (InParallel (mkAwaiter (), mkAwaiter ()));
637 QCOMPARE (result, (std::tuple { true, true }));
638 }
639
640 void CoroTaskTest::testSharedTaskLastCopyDestroyedByAwaiter ()
641 {
642 std::optional<SharedTask<int>> shared = [] () -> SharedTask<int>
643 {
644 co_await 10ms;
645 co_return 42;
646 } ();
647
648 auto destructiveAwaiter = [] (std::optional<SharedTask<int>> *shared) -> Task<int>
649 {
650 auto result = co_await **shared;
651 shared->reset ();
652 co_return result;
653 } (&shared);
654
655 auto normalAwaiter = [] (std::optional<SharedTask<int>> *shared) -> Task<int>
656 {
657 co_return co_await **shared;
658 } (&shared);
659
660 const auto result = GetTaskResult (InParallel (destructiveAwaiter, normalAwaiter));
661 QCOMPARE (result, (std::tuple { 42, 42 }));
662 }
663
664 void CoroTaskTest::testSharedTaskNonTriviallyMovableReturn ()
665 {
666 // Long enough that a moved-from QString is different from the real one, avoiding any SSO shenanigans.
667 const auto payload = "0123456789abcdef"_qs.repeated (16);
668
669 auto shared = [] (QString p) -> SharedTask<QString>
670 {
671 co_await 10ms;
672 co_return p;
673 } (payload);
674
675 auto mkAwaiter = [&shared] () -> Task<QString>
676 {
677 co_return co_await shared;
678 };
679
680 const auto result = GetTaskResult (InParallel (mkAwaiter (), mkAwaiter ()));
681 QCOMPARE (result, (std::tuple { payload, payload }));
682 }
683
684 void CoroTaskTest::testSharedContextTaskManyAwaitersContextAlive ()
685 {
686 auto context = std::make_unique<CoroContext> ();
687 auto shared = [] (CoroContext *ctx) -> SharedContextTask<int>
688 {
689 co_await AddContext { *ctx };
690 co_await 10ms;
691 co_return 42;
692 } (&*context);
693
694 auto mkAwaiter = [&shared] (int offset) -> Task<int>
695 {
696 co_return co_await shared + offset;
697 };
698
699 const auto result = GetTaskResult (InParallel (mkAwaiter (0), mkAwaiter (1)));
700 QCOMPARE (result, (std::tuple { 42, 43 }));
701 }
702
703 void CoroTaskTest::testSharedContextTaskContextDeadFansOutToAll ()
704 {
705 auto context = std::make_unique<CoroContext> ();
706 auto shared = [] (CoroContext *ctx) -> SharedContextTask<int>
707 {
708 co_await AddContext { *ctx };
709 co_await 500ms;
710 co_return 42;
711 } (&*context);
712
713 auto mkAwaiter = [&shared] () -> Task<bool>
714 {
715 try
716 {
717 co_await shared;
718 co_return false;
719 }
720 catch (const ContextDeadException&)
721 {
722 co_return true;
723 }
724 };
725
726 QTimer::singleShot (10ms, [context = std::move (context)] () mutable { context.reset (); });
727
728 const auto result = GetTaskResult (InParallel (mkAwaiter (), mkAwaiter ()));
729 QCOMPARE (result, (std::tuple { true, true }));
730 }
731
732 void CoroTaskTest::testSharedContextTaskBodyExceptionFansOut ()
733 {
734 struct TestSharedContextException : std::exception {};
735
736 auto context = std::make_unique<CoroContext> ();
737 auto shared = [] (CoroContext *ctx) -> SharedContextTask<>
738 {
739 co_await AddContext { *ctx };
740 co_await 10ms;
741 throw TestSharedContextException {};
742 } (&*context);
743
744 auto mkAwaiter = [&shared] () -> Task<bool>
745 {
746 try
747 {
748 co_await shared;
749 co_return false;
750 }
751 catch (const TestSharedContextException&)
752 {
753 co_return true;
754 }
755 };
756
757 const auto result = GetTaskResult (InParallel (mkAwaiter (), mkAwaiter ()));
758 QCOMPARE (result, (std::tuple { true, true }));
759 }
760
761 void CoroTaskTest::testSharedContextTaskContextDeadDoesntWaitLong ()
762 {
763 auto context = std::make_unique<CoroContext> ();
764 auto shared = [] (CoroContext *ctx) -> SharedContextTask<>
765 {
766 co_await AddContext { *ctx };
767 co_await 500ms;
768 } (&*context);
769
770 auto awaiter = [&shared] () -> Task<void>
771 {
772 co_await shared;
773 } ();
774
775 QTimer::singleShot (10ms, [context = std::move (context)] () mutable { context.reset (); });
776
777 QElapsedTimer timer;
778 timer.start ();
779 QVERIFY_THROWS_EXCEPTION (LC::Util::ContextDeadException, GetTaskResult (awaiter));
780 QCOMPARE_LT (timer.elapsed (), 255);
781 }
782
783 void CoroTaskTest::testSharedContextTaskMixedAwaitersOwnContextDies ()
784 {
785 auto sharedCtx = std::make_unique<CoroContext> ();
786 auto aliveCtx = std::make_unique<CoroContext> ();
787 auto dyingCtx = std::make_unique<CoroContext> ();
788
789 auto shared = [] (CoroContext *ctx) -> SharedContextTask<int>
790 {
791 co_await AddContext { *ctx };
792 co_await 100ms;
793 co_return 42;
794 } (&*sharedCtx);
795
796 auto aliveAwaiter = [&shared] (CoroContext *ctx) -> ContextTask<int>
797 {
798 co_await AddContext { *ctx };
799 co_return co_await shared;
800 } (&*aliveCtx);
801
802 auto dyingAwaiter = [&shared] (CoroContext *ctx) -> ContextTask<int>
803 {
804 co_await AddContext { *ctx };
805 co_return co_await shared;
806 } (&*dyingCtx);
807
808 QTimer::singleShot (10ms, [dyingCtx = std::move (dyingCtx)] () mutable { dyingCtx.reset (); });
809
810 QVERIFY_THROWS_EXCEPTION (LC::Util::ContextDeadException, GetTaskResult (dyingAwaiter));
811 QCOMPARE (GetTaskResult (aliveAwaiter), 42);
812 }
813
814 void CoroTaskTest::testEither ()
815 {
816 using Result_t = Either<QString, bool>;
817
818 auto immediatelyFailing = [] () -> Task<Result_t>
819 {
820 const auto theInt = co_await Either<QString, int> { "meh" };
821 co_return theInt > 420;
822 } ();
823 QCOMPARE (GetTaskResult (immediatelyFailing), Result_t { Left { "meh" } });
824
825 auto earlyFailing = [] () -> Task<Result_t>
826 {
827 const auto theInt = co_await Either<QString, int> { "meh" };
828 co_await 10ms;
829 co_return theInt > 420;
830 } ();
831 QCOMPARE (GetTaskResult (earlyFailing), Result_t { Left { "meh" } });
832
833 auto successful = [] () -> Task<Result_t>
834 {
835 const auto theInt = co_await Either<QString, int> { 42 };
836 co_await 10ms;
837 co_return theInt > 420;
838 } ();
839 QCOMPARE (GetTaskResult (successful), Result_t { false });
840 }
841
842 void CoroTaskTest::testEitherIgnoreLeft ()
843 {
844 auto immediatelyFailing = [] () -> Task<Either<QString, int>>
845 {
846 const auto theInt = co_await WithHandler (Either<QString, int> { "meh" }, IgnoreLeft {});
847 co_return theInt;
848 } ();
849 QCOMPARE (GetTaskResult (immediatelyFailing), (Either<QString, int> { 0 }));
850 }
851
852 void CoroTaskTest::testThrottleSameCoro ()
853 {
854 Throttle t { 10ms };
855 constexpr auto count = 10;
856
857 QElapsedTimer timer;
858 timer.start ();
859 auto task = [] (auto& t) -> Task<int>
860 {
861 int result = 0;
862 for (int i = 0; i < count; ++i)
863 {
864 co_await t;
865 result += i;
866 }
867 co_return result;
868 } (t);
869 const auto result = GetTaskResult (task);
870 const auto time = timer.elapsed ();
871
872 QCOMPARE (result, count * (count - 1) / 2);
873 QCOMPARE_GE (time, count * t.GetInterval ().count ());
874 }
875
876 void CoroTaskTest::testThrottleSameCoroSlow ()
877 {
878 Throttle t { 10ms };
879 constexpr auto count = 10;
880 constexpr static auto intraDelay = 9ms;
881
882 QElapsedTimer timer;
883 timer.start ();
884 auto task = [] (auto& t) -> Task<void>
885 {
886 for (int i = 0; i < count; ++i)
887 {
888 co_await t;
889 if (i != count - 1)
890 co_await Precisely { intraDelay };
891 }
892 } (t);
893 GetTaskResult (task);
894 const auto time = timer.elapsed ();
895
896 const auto expectedMinTime = count * t.GetInterval ().count ();
897 QCOMPARE_GE (time, expectedMinTime);
898
899 const auto delaysTime = (count - 1) * intraDelay.count ();
900 QCOMPARE_LE (time - expectedMinTime, delaysTime / 2);
901 }
902
903 void CoroTaskTest::testThrottleSameCoroVerySlow ()
904 {
905 Throttle t { 10ms };
906 constexpr auto count = 10;
907 constexpr static auto intraDelay = 20ms;
908
909 QElapsedTimer timer;
910 timer.start ();
911 auto task = [] (auto& t) -> Task<void>
912 {
913 for (int i = 0; i < count; ++i)
914 {
915 co_await t;
916 if (i != count - 1)
917 co_await Precisely { intraDelay };
918 }
919 } (t);
920 GetTaskResult (task);
921 const auto time = timer.elapsed ();
922
923 const auto expectedMinTime = (count - 1) * intraDelay.count ();
924 QCOMPARE_GE (time, expectedMinTime);
925
926 const auto throttlesTime = count * t.GetInterval ().count ();
927 QCOMPARE_LE (time - expectedMinTime, throttlesTime / 2);
928 }
929
930 void CoroTaskTest::testThrottleManyCoros ()
931 {
932 Throttle t { 1ms, Qt::TimerType::PreciseTimer };
933 constexpr auto count = 10;
934
935 QElapsedTimer timer;
936 timer.start ();
937 auto mkTask = [] (auto& t) -> Task<void>
938 {
939 for (int i = 0; i < count; ++i)
940 co_await t;
941 };
942 QVector tasks { mkTask (t), mkTask (t), mkTask (t) };
943 for (auto& task : tasks)
944 GetTaskResult (task);
945 const auto time = timer.elapsed ();
946
947 QCOMPARE_GE (time, count * tasks.size () * t.GetInterval ().count ());
948 }
949
950 constexpr auto LongDelay = 500ms;
951 constexpr auto ShortDelay = 10ms;
952 constexpr auto DelayThreshold = std::chrono::duration_cast<std::chrono::milliseconds> ((ShortDelay + LongDelay) / 2);
953
954 void CoroTaskTest::testContextDestrBeforeFinish ()
955 {
956 auto context = std::make_unique<CoroContext> ();
957 auto task = [] (CoroContext *context) -> ContextTask<QString>
958 {
959 co_await AddContext { *context };
960 co_await LongDelay;
961 co_return context->GetName ();
962 } (&*context);
963 context.reset ();
964
965 QVERIFY_THROWS_EXCEPTION (LC::Util::ContextDeadException, GetTaskResult (task));
966 }
967
968 void CoroTaskTest::testContextDestrAfterFinish ()
969 {
970 const auto ctxName = "some name"_qs;
971 auto context = std::make_unique<CoroContext> (ctxName);
972 auto task = [] (CoroContext *context) -> ContextTask<QString>
973 {
974 co_await AddContext { *context };
975 co_await ShortDelay;
976 co_return context->GetName ();
977 } (&*context);
978
979 QCOMPARE (GetTaskResult (task), ctxName);
980 }
981
982 void CoroTaskTest::testContextDestrAwaitedSubtask ()
983 {
984 auto context = std::make_unique<CoroContext> ();
985 auto task = [] (CoroContext *context) -> ContextTask<QString>
986 {
987 co_await AddContext { *context };
988
989 const auto nestedSubtask = [] -> ContextTask<int>
990 {
991 co_await ShortDelay;
992 co_return 42;
993 } ();
994
995 co_await nestedSubtask;
996 co_return context->GetName ();
997 } (&*context);
998 context.reset ();
999
1000 QVERIFY_THROWS_EXCEPTION (LC::Util::ContextDeadException, GetTaskResult (task));
1001 }
1002
1003 namespace
1004 {
1005 template<typename... Ts>
1006 auto WithContext (auto&& taskGen, Ts&&... taskArgs)
1007 {
1008 auto context = std::make_unique<CoroContext> ();
1009 auto task = taskGen (&*context, std::forward<Ts> (taskArgs)...);
1010 QTimer::singleShot (ShortDelay, [context = std::move (context)] () mutable { context.reset (); });
1011 return task;
1012 }
1013
1014 void WithDestroyTimer (auto task)
1015 {
1016 QElapsedTimer timer;
1017 timer.start ();
1018 QVERIFY_THROWS_EXCEPTION (LC::Util::ContextDeadException, GetTaskResult (task));
1019 QCOMPARE_LT (timer.elapsed (), DelayThreshold.count ());
1020 }
1021 }
1022
1023 void CoroTaskTest::testContextDestrDoesntWaitTimer ()
1024 {
1025 WithDestroyTimer (WithContext ([] (CoroContext *context) -> ContextTask<void>
1026 {
1027 co_await AddContext { *context };
1028 co_await LongDelay;
1029 }));
1030 }
1031
1032 void CoroTaskTest::testContextDestrDoesntWaitNetwork ()
1033 {
1034 const QByteArray data { "this is some test data" };
1035 auto nam = std::make_shared<MockNAM> (MkSuccessfulReply (data));
1036 QTimer::singleShot (LongDelay,
1037 [nam]
1038 {
1039 if (const auto reply = nam->GetReply ())
1040 {
1041 reply->setFinished (true);
1042 emit reply->finished ();
1043 }
1044 });
1045
1046 WithDestroyTimer (WithContext ([] (CoroContext *context, QNetworkAccessManager *nam) -> ContextTask<QByteArray>
1047 {
1048 co_await AddContext { *context };
1049 const auto reply = co_await *nam->get (QNetworkRequest { QUrl { "http://example.com/foo.txt"_qs } });
1050 co_return reply.GetReplyData ();
1051 }, &*nam));
1052 }
1053
1054 void CoroTaskTest::testContextDestrDoesntWaitProcess ()
1055 {
1056 WithDestroyTimer (WithContext ([] (CoroContext *context) -> ContextTask<>
1057 {
1058 co_await AddContext { *context };
1059
1060 const auto process = new QProcess {};
1061 const auto delay = std::chrono::duration_cast<std::chrono::milliseconds> (LongDelay);
1062 process->start ("sleep", { QString::number (delay.count () / 1000.0) });
1063 connect (process,
1064 &QProcess::finished,
1065 process,
1066 &QObject::deleteLater);
1067
1068 co_await *process;
1069 }));
1070 }
1071
1072 void CoroTaskTest::testContextDestrDoesntWaitFuture ()
1073 {
1074 WithDestroyTimer (WithContext ([] (CoroContext *context) -> ContextTask<>
1075 {
1076 co_await AddContext { *context };
1077 co_await QtConcurrent::run ([] { QThread::sleep (LongDelay); });
1078 }));
1079 }
1080
1081 void CoroTaskTest::testContextDestrDoesntWaitPlainTask ()
1082 {
1083 // a plain Task child is parked, not running: the parent is cancelled now and the child detached
1084 WithDestroyTimer (WithContext ([] (CoroContext *context) -> ContextTask<void>
1085 {
1086 co_await AddContext { *context };
1087 co_await [] () -> Task<void> { co_await LongDelay; } ();
1088 }));
1089 }
1090
1091 namespace
1092 {
1093 using Event = CoroTaskTest::Event;
1094 }
1095
1096 void CoroTaskTest::testContextDestrParentOutlivesChild ()
1097 {
1098 QList<Event> destrLog;
1099 auto task = WithContext ([] (CoroContext *context, QList<Event>& log) -> ContextTask<void>
1100 {
1101 co_await AddContext { *context };
1102 const auto cleanup = MakeScopeGuard ([&log] { log << Event::Parent; });
1103 co_await [] (CoroContext *context, QList<Event>& log) -> ContextTask<void>
1104 {
1105 const auto childCleanup = MakeScopeGuard ([&log] { log << Event::Child; });
1106 co_await AddContext { *context };
1107 co_await LongDelay;
1108 } (context, log);
1109 }, destrLog);
1110 WithDestroyTimer (task);
1111
1112 QCOMPARE (destrLog, (QList { Event::Child, Event::Parent }));
1113 }
1114
1115 void CoroTaskTest::testContextDestrDoesntResumeFinishedSibling ()
1116 {
1117 QList<Event> log;
1118 auto task = WithContext ([] (CoroContext *context, QList<Event>& log) -> ContextTask<void>
1119 {
1120 co_await AddContext { *context };
1121 const auto cleanup = MakeScopeGuard ([&log] { log << Event::Parent; });
1122
1123 auto child = [] (CoroContext *context, QList<Event>& log, std::chrono::milliseconds delay, Event name) -> ContextTask<void>
1124 {
1125 const auto cleanup = MakeScopeGuard ([&log, name] { log << name; });
1126 co_await AddContext { *context };
1127 co_await delay;
1128 };
1129 co_await InParallel ({ child (context, log, 1ms, Event::FastChild), child (context, log, LongDelay, Event::SlowChild) });
1130 }, log);
1131 WithDestroyTimer (task);
1132
1133 std::ranges::sort (log); // threat the log as a set and ensure it contains all expected events
1134 QCOMPARE (log, (QList { Event::Parent, Event::FastChild, Event::SlowChild }));
1135 }
1136
1137 namespace
1138 {
1139 struct TouchRecordingAwaiter
1140 {
1141 bool& Touched_;
1142
1143 bool await_ready () noexcept
1144 {
1145 Touched_ = true;
1146 return true;
1147 }
1148
1149 void await_suspend (std::coroutine_handle<>) noexcept {}
1150 void await_resume () noexcept {}
1151 };
1152 }
1153
1154 void CoroTaskTest::testContextDestrSwallowedDoesntTouchNextAwaitable ()
1155 {
1156 bool touched = false;
1157 auto task = WithContext ([] (CoroContext *context, bool& touched) -> ContextTask<void>
1158 {
1159 co_await AddContext { *context };
1160 try
1161 {
1162 co_await LongDelay;
1163 }
1164 catch (const std::exception&)
1165 {
1166 }
1167 co_await TouchRecordingAwaiter { touched }; // the retry delay
1168 }, touched);
1169 WithDestroyTimer (task);
1170 QVERIFY (!touched);
1171 }
1172
1173 namespace
1174 {
1175 using Context_ptr = std::unique_ptr<CoroContext>;
1176
1177 ContextTask<void> DestroyingChild (Context_ptr& context)
1178 {
1179 co_await AddContext { *context };
1180 co_await ShortDelay;
1181 context.reset ();
1182 }
1183
1184 ContextTask<void> ImmediateDestroyingChild (Context_ptr& context)
1185 {
1186 co_await AddContext { *context };
1187 context.reset (); // the frame has never suspended at this point
1188 }
1189
1190 ContextTask<void> SleepingChild (Context_ptr& context)
1191 {
1192 co_await AddContext { *context };
1193 co_await LongDelay;
1194 }
1195
1196 Task<void> PlainMiddle (Context_ptr& context)
1197 {
1198 co_await DestroyingChild (context);
1199 }
1200
1201 auto ParallelChildren (Context_ptr& context)
1202 {
1203 return InParallel ({ DestroyingChild (context), SleepingChild (context) });
1204 }
1205
1206 ContextTask<void> Parent (Context_ptr& context, auto mkChild)
1207 {
1208 co_await AddContext { *context };
1209 co_await mkChild (context);
1210 }
1211
1212 // runs the coro to completion, whichever way it ends
1213 void RunCoro (auto mkTask)
1214 {
1215 auto context = std::make_unique<CoroContext> ("doomed"_qs);
1216 GetTaskResult (mkTask (context));
1217 }
1218
1219 void RunUnderParent (auto mkChild)
1220 {
1221 RunCoro ([&] (Context_ptr& context) { return Parent (context, mkChild); });
1222 }
1223
1224 constexpr auto DeathChildEnvVar = "LC_COROTASKTEST_DEATH_CHILD";
1225
1226 // gtest's "threadsafe" death test: the scenario runs in a fresh copy of this test binary invoked
1227 // for just the current test function (and data row), which is expected to die with the given message
1228 void ExpectFatal (const QString& message, auto scenario)
1229 {
1230 if (qEnvironmentVariableIsSet (DeathChildEnvVar))
1231 {
1232 scenario ();
1233 QFAIL ("the scenario was expected to abort");
1234 }
1235
1236 auto testName = QString::fromLatin1 (QTest::currentTestFunction ());
1237 if (const auto tag = QTest::currentDataTag ())
1238 testName += ':' + QString::fromLatin1 (tag);
1239
1240 auto env = QProcessEnvironment::systemEnvironment ();
1241 env.insert (DeathChildEnvVar, "1"_qs);
1242
1243 QProcess child;
1244 child.setProcessEnvironment (env);
1245 child.setProcessChannelMode (QProcess::MergedChannels);
1246 child.start (QCoreApplication::applicationFilePath (), { "-nocrashhandler"_qs, testName });
1247 QVERIFY (child.waitForFinished ());
1248
1249 const auto output = QString::fromUtf8 (child.readAll ());
1250 QVERIFY2 (child.exitStatus () == QProcess::CrashExit, qPrintable (output));
1251 QVERIFY2 (output.contains (message), qPrintable (output));
1252 }
1253 }
1254
1255 void CoroTaskTest::testContextDestrFromRunningCoroAborts_data ()
1256 {
1257 QTest::addColumn<Scenario> ("scenario");
1258
1259 QTest::newRow ("self") << Scenario { [] { RunCoro (&DestroyingChild); } };
1260 QTest::newRow ("child") << Scenario { [] { RunUnderParent (&DestroyingChild); } };
1261 QTest::newRow ("child via plain task") << Scenario { [] { RunUnderParent (&PlainMiddle); } };
1262 QTest::newRow ("child before first await") << Scenario { [] { RunUnderParent (&ImmediateDestroyingChild); } };
1263 QTest::newRow ("child in InParallel") << Scenario { [] { RunUnderParent (&ParallelChildren); } };
1264 }
1265
1266 void CoroTaskTest::testContextDestrFromRunningCoroAborts ()
1267 {
1268 QFETCH (const Scenario, scenario);
1269 ExpectFatal ("destroying the context \"doomed\" while its child coro is running"_qs, scenario);
1270 }
1271
1272 void CoroTaskTest::testContextOfIsPerObject ()
1273 {
1274 QObject first;
1275 QObject second;
1276 QVERIFY (&CoroContext::Of (first) == &CoroContext::Of (first));
1277 QVERIFY (&CoroContext::Of (first) != &CoroContext::Of (second));
1278 }
1279
1280 void CoroTaskTest::testContextOfDiesWithObject ()
1281 {
1282 auto object = std::make_unique<QObject> ();
1283 auto task = [] (QObject *object) -> ContextTask<void>
1284 {
1285 co_await AddContext { CoroContext::Of (*object) };
1286 co_await LongDelay;
1287 } (object.get ());
1288 QTimer::singleShot (ShortDelay, [object = std::move (object)] () mutable { object.reset (); });
1289 WithDestroyTimer (task);
1290 }
1291
1292 void CoroTaskTest::testContextOfRejectsForeignThread ()
1293 {
1294 QObject object;
1295 bool thrown = false;
1296 // a real thread: QtConcurrent::run () + result () may steal the runnable and run it right here
1297 std::thread { [&]
1298 {
1299 try
1300 {
1301 CoroContext::Of (object);
1302 }
1303 catch (const std::runtime_error&)
1304 {
1305 thrown = true;
1306 }
1307 } }.join ();
1308 QVERIFY (thrown);
1309 QVERIFY (&CoroContext::Of (object) == &CoroContext::Of (object));
1310 }
1311
1312 namespace
1313 {
1314 Task<ProcessOutcome> RunProcess (QString program, QStringList args)
1315 {
1316 QProcess process;
1317 process.start (program, args);
1318 co_return co_await process;
1319 }
1320 }
1321
1322 void CoroTaskTest::testProcessOutcomeExited ()
1323 {
1324 const auto outcome = GetTaskResult (RunProcess ("sh"_qs, { "-c"_qs, "exit 3"_qs }));
1325 QVERIFY (std::holds_alternative<ProcessExited> (outcome));
1326 QCOMPARE (std::get<ProcessExited> (outcome).Code_, 3);
1327 }
1328
1329 void CoroTaskTest::testProcessOutcomeFailedToStart ()
1330 {
1331 const auto outcome = GetTaskResult (RunProcess ("lc-definitely-not-an-existing-binary"_qs, {}));
1332 QVERIFY (std::holds_alternative<ProcessFailedToStart> (outcome));
1333 QVERIFY (!std::get<ProcessFailedToStart> (outcome).Error_.isEmpty ());
1334 }
1335
1336#ifdef Q_OS_UNIX
1337 void CoroTaskTest::testProcessOutcomeCrashed ()
1338 {
1339 const auto outcome = GetTaskResult (RunProcess ("sh"_qs, { "-c"_qs, "kill -SEGV $$"_qs }));
1340 QVERIFY (std::holds_alternative<ProcessCrashed> (outcome));
1341 QCOMPARE (std::get<ProcessCrashed> (outcome).Code_, SIGSEGV);
1342 }
1343#endif
1344
1345#ifdef QT_DBUS_LIB
1346 void CoroTaskTest::testDBus ()
1347 {
1348 const auto& bus = QDBusConnection::systemBus ();
1349 if (!bus.isConnected ())
1350 QSKIP ("D-Bus system bus is not available");
1351
1352 auto task = [] () -> Task<Either<QDBusError::ErrorType, bool>>
1353 {
1354 const auto& bus = QDBusConnection::systemBus ();
1355 QDBusInterface dbusIface { "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", bus };
1356 const auto result = co_await Typed<bool> (dbusIface.asyncCall ("NameHasOwner", "org.freedesktop.DBus"_qs));
1357 co_return result.MapLeft (&QDBusError::type);
1358 } ();
1359
1360 QCOMPARE (GetTaskResult (task), true);
1361 }
1362#endif
1363
1364 void CoroTaskTest::cleanupTestCase ()
1365 {
1366 bool done = false;
1367 QTimer::singleShot (LongDelay * 2, [&done] { done = true; });
1368 QTRY_VERIFY (done);
1369 }
1370}
static CoroContext & Of(QObject &obj)
Returns the context tied to the lifetime of obj.
std::function< void()> Scenario
Task< R, ContextExtension > ContextTask
Definition taskfwd.h:20
detail::DBusAwaiter< Rets... > Typed(const QDBusPendingCall &asyncCall)
Definition dbus.h:87
constexpr auto LongDelay
Task< T, SharedTaskExtension > SharedTask
Definition sharedtask.h:51
Task< T, SharedTaskExtension, ContextExtension > SharedContextTask
Definition sharedtask.h:54
detail::ScopeGuard< F > MakeScopeGuard(const F &f)
Returns an object performing passed function on scope exit.
Definition util.h:155
auto NCopies(qsizetype count, auto taskFactory)
Starts count tasks via taskFactory and awaits them all.
Definition inparallel.h:137
detail::EitherAwaiter< L, R, F > WithHandler(const Either< L, R > &either, F &&errorHandler)
Definition either.h:83
constexpr auto ShortDelay
WithPrecision< Qt::PreciseTimer > Precisely
Definition timer.h:44
T GetTaskResult(Task< T, Extensions... > task)
Definition getresult.h:19
constexpr auto DelayThreshold
auto InParallel(QVector< Task< T, Exts... > > tasks)
Awaits all the tasks, collecting their results in order.
Definition inparallel.h:77
Q_DECLARE_METATYPE(QVariantList *)