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