LeechCraft  0.6.70-18450-gabe19ee3b0
Modular cross-platform feature rich live environment.
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 #ifdef QT_DBUS_LIB
13 #include <QDBusInterface>
14 #endif
15 #include <coro/future.h>
16 #include <coro.h>
17 #include <coro/getresult.h>
18 #include <coro/inparallel.h>
19 #include <coro/throttle.h>
20 #ifdef QT_DBUS_LIB
21 #include <coro/dbus.h>
22 #endif
23 #include <util/threads/futures.h>
24 #include <util/sll/debugprinters.h>
25 #include <util/sll/qtutil.h>
26 
27 QTEST_GUILESS_MAIN (LC::Util::CoroTaskTest)
28 
29 using namespace std::chrono_literals;
30 
31 namespace LC::Util
32 {
33  void CoroTaskTest::testReturn ()
34  {
35  auto task = [] () -> Task<int> { co_return 42; } ();
36  auto result = GetTaskResult (task);
37  QCOMPARE (result, 42);
38  }
39 
40  void CoroTaskTest::testWait ()
41  {
42  QElapsedTimer timer;
43  timer.start ();
44 
45  auto task = [] () -> Task<int>
46  {
47  co_await 50ms;
48  co_await Precisely { 10ms };
49  co_return 42;
50  } ();
51 
52  auto result = GetTaskResult (task);
53  QCOMPARE (result, 42);
54  QCOMPARE_GT (timer.elapsed (), 50);
55  }
56 
57  void CoroTaskTest::testTaskDestr ()
58  {
59  bool continued = false;
60 
61  [] (auto& continued) -> Task<void>
62  {
63  co_await 10ms;
64  continued = true;
65  } (continued);
66 
67  QTRY_VERIFY_WITH_TIMEOUT (continued, 20);
68  }
69 
70  namespace
71  {
72  // almost the Public Morozov pattern
73  class MockReply : public QNetworkReply
74  {
75  QBuffer Buffer_;
76  public:
77  using QNetworkReply::QNetworkReply;
78 
79  using QNetworkReply::setAttribute;
80  using QNetworkReply::setError;
81  using QNetworkReply::setFinished;
82  using QNetworkReply::setHeader;
83  using QNetworkReply::setOperation;
84  using QNetworkReply::setRawHeader;
85  using QNetworkReply::setRequest;
86  using QNetworkReply::setUrl;
87 
88  void SetData (const QByteArray& data)
89  {
90  Buffer_.setData (data);
91  Buffer_.open (QIODevice::ReadOnly);
92  open (QIODevice::ReadOnly);
93  }
94  protected:
95  qint64 readData (char *data, qint64 maxSize) override
96  {
97  return Buffer_.read (data, maxSize);
98  }
99 
100  void abort () override
101  {
102  }
103  };
104 
105  class MockNAM : public QNetworkAccessManager
106  {
107  QPointer<MockReply> Reply_;
108  public:
109  explicit MockNAM (MockReply *reply)
110  : Reply_ { reply }
111  {
112  }
113 
114  MockReply* GetReply ()
115  {
116  return Reply_;
117  }
118  protected:
119  QNetworkReply* createRequest (Operation op, const QNetworkRequest& req, QIODevice*) override
120  {
121  Reply_->setUrl (req.url ());
122  Reply_->setOperation (op);
123  Reply_->setRequest (req);
124  return Reply_;
125  }
126  };
127 
128  auto MkSuccessfulReply (const QByteArray& data)
129  {
130  auto reply = new MockReply;
131  reply->setAttribute (QNetworkRequest::HttpStatusCodeAttribute, 200);
132  reply->SetData (data);
133  return reply;
134  }
135 
136  auto MkErrorReply ()
137  {
138  auto reply = new MockReply;
139  reply->setAttribute (QNetworkRequest::HttpStatusCodeAttribute, 404);
140  reply->setError (QNetworkReply::NetworkError::ContentAccessDenied, "well, 404!"_qs);
141  return reply;
142  }
143 
144  void TestGoodReply (auto finishMarker)
145  {
146  const QByteArray data { "this is some test data" };
147  MockNAM nam { MkSuccessfulReply (data) };
148  finishMarker (*nam.GetReply ());
149 
150  auto task = [&nam] () -> Task<QByteArray>
151  {
152  auto reply = co_await *nam.get (QNetworkRequest { QUrl { "http://example.com/foo.txt"_qs } });
153  co_return reply.GetReplyData ();
154  } ();
155 
156  auto result = GetTaskResult (task);
157  QCOMPARE (result, data);
158  }
159 
160  void TestBadReply (auto finishMarker)
161  {
162  MockNAM nam { MkErrorReply () };
163  finishMarker (*nam.GetReply ());
164 
165  auto task = [&nam] () -> Task<QByteArray>
166  {
167  auto reply = co_await *nam.get (QNetworkRequest { QUrl { "http://example.com/foo.txt"_qs } });
168  co_return reply.GetReplyData ();
169  } ();
170 
171  QVERIFY_THROWS_EXCEPTION (LC::Util::NetworkReplyErrorException, GetTaskResult (task));
172  }
173 
174  void ImmediateFinishMarker (MockReply& reply)
175  {
176  reply.setFinished (true);
177  }
178 
179  void DelayedFinishMarker (MockReply& reply)
180  {
181  QTimer::singleShot (10ms,
182  [&]
183  {
184  reply.setFinished (true);
185  emit reply.finished ();
186  });
187  }
188  }
189 
190  void CoroTaskTest::testNetworkReplyGoodNoWait ()
191  {
192  TestGoodReply (&ImmediateFinishMarker);
193  }
194 
195  void CoroTaskTest::testNetworkReplyGoodWait ()
196  {
197  TestGoodReply (&DelayedFinishMarker);
198  }
199 
200  void CoroTaskTest::testNetworkReplyBadNoWait ()
201  {
202  TestBadReply (&ImmediateFinishMarker);
203  }
204 
205  void CoroTaskTest::testNetworkReplyBadWait ()
206  {
207  TestBadReply (&DelayedFinishMarker);
208  }
209 
210  void CoroTaskTest::testFutureAwaiter ()
211  {
212  auto delayed = [] () -> Task<int>
213  {
214  co_return co_await QtConcurrent::run ([]
215  {
216  QThread::msleep (1);
217  return 42;
218  });
219  } ();
220  QCOMPARE (GetTaskResult (delayed), 42);
221 
222  auto immediate = [] () -> Task<int>
223  {
224  co_return co_await QtConcurrent::run ([] { return 42; });
225  } ();
226  QCOMPARE (GetTaskResult (immediate), 42);
227 
228  auto ready = [] () -> Task<int>
229  {
230  co_return co_await MakeReadyFuture (42);
231  } ();
232  QCOMPARE (GetTaskResult (ready), 42);
233  }
234 
235  void CoroTaskTest::testWaitMany ()
236  {
237  constexpr auto max = 100;
238  auto mkTask = [] (int index) -> Task<int>
239  {
240  co_await Precisely { std::chrono::milliseconds { max - index } };
241  co_return index;
242  };
243 
244  QElapsedTimer timer;
245  timer.start ();
246  QVector<Task<int>> tasks;
247  QVector<int> expected;
248  for (int i = 0; i < max; ++i)
249  {
250  tasks << mkTask (i);
251  expected << i;
252  }
253  const auto creationElapsed = timer.elapsed ();
254 
255  timer.restart ();
256  auto result = GetTaskResult (InParallel (std::move (tasks)));
257  const auto executionElapsed = timer.elapsed ();
258 
259  QCOMPARE (result, expected);
260  QCOMPARE_LT (creationElapsed, 1);
261 
262  constexpr auto tolerance = 0.05;
263  QCOMPARE_GE (executionElapsed, max * (1 - tolerance));
264  const auto linearizedExecTime = max * (max + 1) / 2;
265  QCOMPARE_LT (executionElapsed, linearizedExecTime / 2);
266  }
267 
268  void CoroTaskTest::testWaitManyTuple ()
269  {
270  auto mkTask = [] (int delay) -> Task<int>
271  {
272  co_await Precisely { std::chrono::milliseconds { delay } };
273  co_return delay;
274  };
275 
276  QElapsedTimer timer;
277  timer.start ();
278  auto result = GetTaskResult (InParallel (mkTask (10), mkTask (9), mkTask (2), mkTask (1)));
279  const auto executionElapsed = timer.elapsed ();
280 
281  QCOMPARE (result, (std::tuple { 10, 9, 2, 1 }));
282 
283  QCOMPARE_GE (executionElapsed, 10);
284  QCOMPARE_LT (executionElapsed, (10 + 9 + 2 + 1) / 2);
285  }
286 
287  void CoroTaskTest::testWaitManyInvoking ()
288  {
289  constexpr auto max = 100;
290  auto mkTask = [] (int index) -> Task<int>
291  {
292  co_await Precisely { std::chrono::milliseconds { max - index } };
293  co_return index;
294  };
295 
296  QElapsedTimer timer;
297  timer.start ();
298  QVector<int> inputs;
299  QVector<int> expected;
300  for (int i = 0; i < max; ++i)
301  {
302  inputs << i;
303  expected << i;
304  }
305  const auto creationElapsed = timer.elapsed ();
306 
307  timer.restart ();
308  auto result = GetTaskResult (InParallel (inputs, mkTask));
309  const auto executionElapsed = timer.elapsed ();
310 
311  QCOMPARE (result, expected);
312  QCOMPARE_LT (creationElapsed, 1);
313 
314  constexpr auto tolerance = 0.05;
315  QCOMPARE_GE (executionElapsed, max * (1 - tolerance));
316  const auto linearizedExecTime = max * (max + 1) / 2;
317  QCOMPARE_LT (executionElapsed, linearizedExecTime / 2);
318  }
319 
320  void CoroTaskTest::testEither ()
321  {
322  using Result_t = Either<QString, bool>;
323 
324  auto immediatelyFailing = [] () -> Task<Result_t>
325  {
326  const auto theInt = co_await Either<QString, int> { "meh" };
327  co_return theInt > 420;
328  } ();
329  QCOMPARE (GetTaskResult (immediatelyFailing), Result_t { Left { "meh" } });
330 
331  auto earlyFailing = [] () -> Task<Result_t>
332  {
333  const auto theInt = co_await Either<QString, int> { "meh" };
334  co_await 10ms;
335  co_return theInt > 420;
336  } ();
337  QCOMPARE (GetTaskResult (earlyFailing), Result_t { Left { "meh" } });
338 
339  auto successful = [] () -> Task<Result_t>
340  {
341  const auto theInt = co_await Either<QString, int> { 42 };
342  co_await 10ms;
343  co_return theInt > 420;
344  } ();
345  QCOMPARE (GetTaskResult (successful), Result_t { false });
346  }
347 
348  void CoroTaskTest::testEitherIgnoreLeft ()
349  {
350  auto immediatelyFailing = [] () -> Task<Either<QString, int>>
351  {
352  const auto theInt = co_await WithHandler (Either<QString, int> { "meh" }, IgnoreLeft {});
353  co_return theInt;
354  } ();
355  QCOMPARE (GetTaskResult (immediatelyFailing), (Either<QString, int> { 0 }));
356  }
357 
358  void CoroTaskTest::testThrottleSameCoro ()
359  {
360  Throttle t { 10ms };
361  constexpr auto count = 10;
362 
363  QElapsedTimer timer;
364  timer.start ();
365  auto task = [] (auto& t) -> Task<int>
366  {
367  int result = 0;
368  for (int i = 0; i < count; ++i)
369  {
370  co_await t;
371  result += i;
372  }
373  co_return result;
374  } (t);
375  const auto result = GetTaskResult (task);
376  const auto time = timer.elapsed ();
377 
378  QCOMPARE (result, count * (count - 1) / 2);
379  QCOMPARE_GE (time, count * t.GetInterval ().count ());
380  }
381 
382  void CoroTaskTest::testThrottleSameCoroSlow ()
383  {
384  Throttle t { 10ms };
385  constexpr auto count = 10;
386  constexpr static auto intraDelay = 9ms;
387 
388  QElapsedTimer timer;
389  timer.start ();
390  auto task = [] (auto& t) -> Task<void>
391  {
392  for (int i = 0; i < count; ++i)
393  {
394  co_await t;
395  if (i != count - 1)
396  co_await Precisely { intraDelay };
397  }
398  } (t);
399  GetTaskResult (task);
400  const auto time = timer.elapsed ();
401 
402  const auto expectedMinTime = count * t.GetInterval ().count ();
403  QCOMPARE_GE (time, expectedMinTime);
404 
405  const auto delaysTime = (count - 1) * intraDelay.count ();
406  QCOMPARE_LE (time - expectedMinTime, delaysTime / 2);
407  }
408 
409  void CoroTaskTest::testThrottleSameCoroVerySlow ()
410  {
411  Throttle t { 10ms };
412  constexpr auto count = 10;
413  constexpr static auto intraDelay = 20ms;
414 
415  QElapsedTimer timer;
416  timer.start ();
417  auto task = [] (auto& t) -> Task<void>
418  {
419  for (int i = 0; i < count; ++i)
420  {
421  co_await t;
422  if (i != count - 1)
423  co_await Precisely { intraDelay };
424  }
425  } (t);
426  GetTaskResult (task);
427  const auto time = timer.elapsed ();
428 
429  const auto expectedMinTime = (count - 1) * intraDelay.count ();
430  QCOMPARE_GE (time, expectedMinTime);
431 
432  const auto throttlesTime = count * t.GetInterval ().count ();
433  QCOMPARE_LE (time - expectedMinTime, throttlesTime / 2);
434  }
435 
436  void CoroTaskTest::testThrottleManyCoros ()
437  {
438  Throttle t { 1ms, Qt::TimerType::PreciseTimer };
439  constexpr auto count = 10;
440 
441  QElapsedTimer timer;
442  timer.start ();
443  auto mkTask = [] (auto& t) -> Task<void>
444  {
445  for (int i = 0; i < count; ++i)
446  co_await t;
447  };
448  QVector tasks { mkTask (t), mkTask (t), mkTask (t) };
449  for (auto& task : tasks)
450  GetTaskResult (task);
451  const auto time = timer.elapsed ();
452 
453  QCOMPARE_GE (time, count * tasks.size () * t.GetInterval ().count ());
454  }
455 
456  constexpr auto LongDelay = 500ms;
457  constexpr auto ShortDelay = 10ms;
458  constexpr auto DelayThreshold = std::chrono::duration_cast<std::chrono::milliseconds> ((ShortDelay + LongDelay) / 2);
459 
460  void CoroTaskTest::testContextDestrBeforeFinish ()
461  {
462  auto context = std::make_unique<QObject> ();
463  auto task = [] (QObject *context) -> ContextTask<int>
464  {
465  co_await AddContextObject { *context };
466  co_await LongDelay;
467  co_return context->children ().size ();
468  } (&*context);
469  context.reset ();
470 
471  QVERIFY_THROWS_EXCEPTION (LC::Util::ContextDeadException, GetTaskResult (task));
472  }
473 
474  void CoroTaskTest::testContextDestrAfterFinish ()
475  {
476  auto context = std::make_unique<QObject> ();
477  auto task = [] (QObject *context) -> ContextTask<int>
478  {
479  co_await AddContextObject { *context };
480  co_await ShortDelay;
481  co_return context->children ().size ();
482  } (&*context);
483 
484  QCOMPARE (GetTaskResult (task), 0);
485  }
486 
487  void CoroTaskTest::testContextDestrAwaitedSubtask ()
488  {
489  auto context = std::make_unique<QObject> ();
490  auto task = [] (QObject *context) -> ContextTask<int>
491  {
492  co_await AddContextObject { *context };
493 
494  const auto nestedSubtask = [] -> ContextTask<int>
495  {
496  co_await ShortDelay;
497  co_return 42;
498  } ();
499 
500  co_await nestedSubtask;
501  co_return context->children ().size ();
502  } (&*context);
503  context.reset ();
504 
505  QVERIFY_THROWS_EXCEPTION (LC::Util::ContextDeadException, GetTaskResult (task));
506  }
507 
508  namespace
509  {
510  template<typename... Ts>
511  auto WithContext (auto&& taskGen, Ts&&... taskArgs)
512  {
513  auto context = std::make_unique<QObject> ();
514  auto task = taskGen (&*context, std::forward<Ts> (taskArgs)...);
515  QTimer::singleShot (ShortDelay, [context = std::move (context)] () mutable { context.reset (); });
516  return task;
517  }
518 
519  void WithDestroyTimer (auto task)
520  {
521  QElapsedTimer timer;
522  timer.start ();
523  QVERIFY_THROWS_EXCEPTION (LC::Util::ContextDeadException, GetTaskResult (task));
524  QCOMPARE_LT (timer.elapsed (), DelayThreshold.count ());
525  }
526  }
527 
528  void CoroTaskTest::testContextDestrDoesntWaitTimer ()
529  {
530  WithDestroyTimer (WithContext ([] (QObject *context) -> ContextTask<void>
531  {
532  co_await AddContextObject { *context };
533  co_await LongDelay;
534  }));
535  }
536 
537  void CoroTaskTest::testContextDestrDoesntWaitNetwork ()
538  {
539  const QByteArray data { "this is some test data" };
540  auto nam = std::make_shared<MockNAM> (MkSuccessfulReply (data));
541  QTimer::singleShot (LongDelay,
542  [nam]
543  {
544  if (const auto reply = nam->GetReply ())
545  {
546  reply->setFinished (true);
547  emit reply->finished ();
548  }
549  });
550 
551  WithDestroyTimer (WithContext ([] (QObject *context, QNetworkAccessManager *nam) -> ContextTask<QByteArray>
552  {
553  co_await AddContextObject { *context };
554  const auto reply = co_await *nam->get (QNetworkRequest { QUrl { "http://example.com/foo.txt"_qs } });
555  co_return reply.GetReplyData ();
556  }, &*nam));
557  }
558 
559  void CoroTaskTest::testContextDestrDoesntWaitProcess ()
560  {
561  WithDestroyTimer (WithContext ([] (QObject *context) -> ContextTask<>
562  {
563  co_await AddContextObject { *context };
564 
565  const auto process = new QProcess {};
566  const auto delay = std::chrono::duration_cast<std::chrono::milliseconds> (LongDelay);
567  process->start ("sleep", { QString::number (delay.count () / 1000.0) });
568  connect (process,
569  &QProcess::finished,
570  process,
571  &QObject::deleteLater);
572 
573  co_await *process;
574  }));
575  }
576 
577  void CoroTaskTest::testContextDestrDoesntWaitFuture ()
578  {
579  WithDestroyTimer (WithContext ([] (QObject *context) -> ContextTask<>
580  {
581  co_await AddContextObject { *context };
582  co_await QtConcurrent::run ([] { QThread::sleep (LongDelay); });
583  }));
584  }
585 
586 #ifdef QT_DBUS_LIB
587  void CoroTaskTest::testDBus ()
588  {
589  const auto& bus = QDBusConnection::systemBus ();
590  if (!bus.isConnected ())
591  QSKIP ("D-Bus system bus is not available");
592 
593  auto task = [] () -> Task<Either<QDBusError::ErrorType, bool>>
594  {
595  const auto& bus = QDBusConnection::systemBus ();
596  QDBusInterface dbusIface { "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", bus };
597  const auto result = co_await Typed<bool> (dbusIface.asyncCall ("NameHasOwner", "org.freedesktop.DBus"_qs));
598  co_return result.MapLeft (&QDBusError::type);
599  } ();
600 
601  QCOMPARE (GetTaskResult (task), true);
602  }
603 #endif
604 
605  void CoroTaskTest::cleanupTestCase ()
606  {
607  bool done = false;
608  QTimer::singleShot (LongDelay * 2, [&done] { done = true; });
609  QTRY_VERIFY (done);
610  }
611 }
constexpr detail::AggregateType< detail::AggregateFunction::Count, Ptr > count
Definition: oral.h:1104
WithPrecision< Qt::PreciseTimer > Precisely
Definition: timer.h:43
requires std::invocable< F, const L & > detail::EitherAwaiter< L, R, F > WithHandler(const Either< L, R > &either, F &&errorHandler)
Definition: either.h:82
Task< QVector< T >, Exts... > InParallel(Cont< Task< T, Exts... >> tasks)
Definition: inparallel.h:21
constexpr detail::AggregateType< detail::AggregateFunction::Max, Ptr > max
Definition: oral.h:1110
constexpr detail::ExprTree< detail::ExprType::LeafStaticPlaceholder, detail::MemberPtrs< Ptrs... > > tuple
Definition: oral.h:1086
T GetTaskResult(Task< T, Extensions... > task)
Definition: getresult.h:19
constexpr auto LongDelay
constexpr auto ShortDelay
constexpr auto DelayThreshold