diff --git a/CMakePresets.json b/CMakePresets.json index f2f01e677..d3ae34570 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -224,6 +224,9 @@ { "name": "ci-tests", "configuration": "Release", + "environment": { + "QT_QPA_PLATFORM":"offscreen" + }, "output": { "outputOnFailure": true }, diff --git a/cmake/dependencies.cmake b/cmake/dependencies.cmake index 9e3306e43..3bdf266c9 100644 --- a/cmake/dependencies.cmake +++ b/cmake/dependencies.cmake @@ -7,10 +7,11 @@ endif() # ==================== QT ========================== if (RPP_BUILD_QT_CODE AND (RPP_BUILD_TESTS OR RPP_BUILD_EXAMPLES)) - find_package(Qt6 COMPONENTS Widgets) + find_package(Qt6 COMPONENTS Widgets QUIET) if (Qt6_FOUND) SET(RPP_QT_TARGET Qt6) else() + message("-- RPP: Can't find Qt6, searching for Qt5...") find_package(Qt5 REQUIRED COMPONENTS Widgets) SET(RPP_QT_TARGET Qt5) endif() diff --git a/src/rpp/rpp/schedulers/immediate_scheduler.hpp b/src/rpp/rpp/schedulers/immediate_scheduler.hpp index 959cadc8e..568cb9072 100644 --- a/src/rpp/rpp/schedulers/immediate_scheduler.hpp +++ b/src/rpp/rpp/schedulers/immediate_scheduler.hpp @@ -17,7 +17,6 @@ #include #include -#include namespace rpp::schedulers { diff --git a/src/rppqt/rppqt/fwd.hpp b/src/rppqt/rppqt/fwd.hpp index fe87166d5..59f19315b 100644 --- a/src/rppqt/rppqt/fwd.hpp +++ b/src/rppqt/rppqt/fwd.hpp @@ -21,4 +21,12 @@ * \ingroup rppqt */ -#include \ No newline at end of file +#include + +/** + * \defgroup schedulers Schedulers + * \brief Scheduler is the way to introduce multi-threading in your application via RPP + * \see https://reactivex.io/documentation/scheduler.html + * \ingroup rpp + */ +#include diff --git a/src/rppqt/rppqt/rppqt.hpp b/src/rppqt/rppqt/rppqt.hpp index 0fefc0e95..4fcf1e0db 100644 --- a/src/rppqt/rppqt/rppqt.hpp +++ b/src/rppqt/rppqt/rppqt.hpp @@ -11,4 +11,5 @@ #pragma once #include -#include \ No newline at end of file +#include +#include \ No newline at end of file diff --git a/src/rppqt/rppqt/schedulers.hpp b/src/rppqt/rppqt/schedulers.hpp new file mode 100644 index 000000000..4a2e95043 --- /dev/null +++ b/src/rppqt/rppqt/schedulers.hpp @@ -0,0 +1,20 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +/** + * \defgroup qt_schedulers QT Schedulers + * \brief Scheduler is the way to introduce multi-threading in your application via RPP + * \see https://reactivex.io/documentation/scheduler.html + * \ingroup rppqt + */ + +#include \ No newline at end of file diff --git a/src/rppqt/rppqt/schedulers/fwd.hpp b/src/rppqt/rppqt/schedulers/fwd.hpp new file mode 100644 index 000000000..756c4be14 --- /dev/null +++ b/src/rppqt/rppqt/schedulers/fwd.hpp @@ -0,0 +1,16 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +namespace rppqt::schedulers +{ +class main_thread_scheduler; +} // namespace rppqt::schedulers diff --git a/src/rppqt/rppqt/schedulers/main_thread_scheduler.hpp b/src/rppqt/rppqt/schedulers/main_thread_scheduler.hpp new file mode 100644 index 000000000..103ab6bbf --- /dev/null +++ b/src/rppqt/rppqt/schedulers/main_thread_scheduler.hpp @@ -0,0 +1,74 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include // own forwarding +#include // worker +#include // lifetime +#include + +#include +#include + +#include +#include + +namespace rppqt::schedulers +{ +/** + * \brief Schedule provided schedulables to main GUI QT thread (where QApplication placed) + * \ingroup qt_schedulers + */ +class main_thread_scheduler final : public rpp::schedulers::details::scheduler_tag +{ +private: + class worker_strategy; + using main_thread_schedulable = rpp::schedulers::schedulable_wrapper; + + class worker_strategy + { + public: + worker_strategy(const rpp::subscription_base& sub) + : m_sub{sub} {} + + bool is_subscribed() const { return m_sub.is_subscribed(); } + + void defer_at(rpp::schedulers::time_point time_point, rpp::schedulers::constraint::schedulable_fn auto&& fn) const + { + defer_at(time_point, main_thread_schedulable{*this, time_point, std::forward(fn)}); + } + + void defer_at(rpp::schedulers::time_point time_point, main_thread_schedulable&& fn) const + { + if (!m_sub.is_subscribed()) + return; + + const auto application = QCoreApplication::instance(); + if (!application) + throw utils::no_active_qapplication{ + "Pointer to application is null. Create QApplication before using main_thread_scheduler!"}; + + const auto duration = std::chrono::duration_cast(now() - time_point).count(); + QTimer::singleShot(duration, application, std::move(fn)); + } + + static rpp::schedulers::time_point now() { return rpp::schedulers::clock_type::now(); } + private: + rpp::subscription_base m_sub; + }; + +public: + static auto create_worker(const rpp::subscription_base& sub = {}) + { + return rpp::schedulers::worker{sub}; + } +}; +} // namespace rppqt::schedulers diff --git a/src/rppqt/rppqt/utils/exceptions.hpp b/src/rppqt/rppqt/utils/exceptions.hpp new file mode 100644 index 000000000..24f9e1ddb --- /dev/null +++ b/src/rppqt/rppqt/utils/exceptions.hpp @@ -0,0 +1,21 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus +// + +#pragma once + +#include + +namespace rppqt::utils +{ +struct no_active_qapplication : std::runtime_error +{ + using std::runtime_error::runtime_error; +}; +} \ No newline at end of file diff --git a/src/tests/rppqt/test_main_thread_scheduler.cpp b/src/tests/rppqt/test_main_thread_scheduler.cpp new file mode 100644 index 000000000..ff18d3987 --- /dev/null +++ b/src/tests/rppqt/test_main_thread_scheduler.cpp @@ -0,0 +1,66 @@ +// ReactivePlusPlus library +// +// Copyright Aleksey Loginov 2022 - present. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) +// +// Project home: https://github.com/victimsnino/ReactivePlusPlus + +#include +#include + +#include + +#include +#include + +SCENARIO("main_thread_scheduler schedules actions to main thread", "[schedulers]") +{ + GIVEN("qapplication and scheduler") + { + int argc{}; + QCoreApplication application{argc, nullptr}; + WHEN("submitting action to main scheduler from another thread") + { + std::promise execution_thread{}; + std::thread{[&] + { + rppqt::schedulers::main_thread_scheduler{}.create_worker().schedule([&]()->rpp::schedulers::optional_duration + { + execution_thread.set_value(std::this_thread::get_id()); + return {}; + }); + }}.join(); + QTimer::singleShot(10, &application, [&]{application.exit();}); + application.exec(); + THEN("thread of exectuion of schedulable should be same as thread of application") + { + auto future = execution_thread.get_future(); + REQUIRE(future.wait_for(std::chrono::seconds{3}) == std::future_status::ready); + CHECK(future.get() == std::this_thread::get_id()); + } + } + WHEN("submitting action to main scheduler from another thread but with unsubscribed subscription after schedule") + { + std::promise execution_thread{}; + std::thread{[&] + { + rpp::composite_subscription sub{}; + rppqt::schedulers::main_thread_scheduler{}.create_worker(sub).schedule([&]()->rpp::schedulers::optional_duration + { + execution_thread.set_value(std::this_thread::get_id()); + return {}; + }); + sub.unsubscribe(); + }}.join(); + QTimer::singleShot(10, &application, [&]{application.exit();}); + application.exec(); + THEN("no schedule execution") + { + auto future = execution_thread.get_future(); + REQUIRE(future.wait_for(std::chrono::seconds{1}) == std::future_status::timeout); + } + } + } +} \ No newline at end of file