diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83bbb4b..3792fda 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,16 +2,40 @@ name: ci on: push: + branches: + - main pull_request: workflow_dispatch: jobs: - build-and-test-linux: - runs-on: ubuntu-22.04 + build-and-test: + name: build-and-test-${{ matrix.platform }} (${{ matrix.cxx_standard }}) + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - cxx_standard: [11, 14, 17] + include: + - platform: linux + os: ubuntu-22.04 + cxx_standard: 11 + - platform: linux + os: ubuntu-22.04 + cxx_standard: 14 + - platform: linux + os: ubuntu-22.04 + cxx_standard: 17 + - platform: windows + os: windows-latest + cxx_standard: 11 + - platform: windows + os: windows-latest + cxx_standard: 14 + - platform: windows + os: windows-latest + cxx_standard: 17 + - platform: macos + os: macos-latest + cxx_standard: 17 steps: - name: Checkout repository @@ -21,7 +45,7 @@ jobs: - name: Configure run: > - cmake -S . -B build + cmake -S . -B build -G Ninja -DCONSOLIX_CXX_STANDARD=${{ matrix.cxx_standard }} -DCONSOLIX_BUILD_EXAMPLES=ON -DCONSOLIX_BUILD_TESTS=ON @@ -31,3 +55,37 @@ jobs: - name: Test run: ctest --test-dir build --output-on-failure + + build-and-test-event-hub: + name: build-and-test-event-hub-${{ matrix.platform }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - platform: linux + os: ubuntu-22.04 + - platform: windows + os: windows-latest + - platform: macos + os: macos-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: true + + - name: Configure + run: > + cmake -S . -B build -G Ninja + -DCONSOLIX_CXX_STANDARD=17 + -DCONSOLIX_USE_EVENT_HUB=ON + -DCONSOLIX_BUILD_EXAMPLES=ON + -DCONSOLIX_BUILD_TESTS=ON + + - name: Build + run: cmake --build build --parallel + + - name: Test + run: ctest --test-dir build --output-on-failure diff --git a/.gitmodules b/.gitmodules index 3795344..2e9b2a2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,16 +1,21 @@ -[submodule "libs/log-it-cpp"] - path = libs/log-it-cpp +[submodule "external/log-it-cpp"] + path = external/log-it-cpp url = https://github.com/NewYaroslav/log-it-cpp.git -[submodule "libs/time-shield-cpp"] - path = libs/time-shield-cpp + branch = main +[submodule "external/time-shield-cpp"] + path = external/time-shield-cpp url = https://github.com/NewYaroslav/time-shield-cpp.git branch = main -[submodule "libs/cxxopts"] - path = libs/cxxopts +[submodule "external/cxxopts"] + path = external/cxxopts url = https://github.com/jarro2783/cxxopts.git -[submodule "libs/json"] - path = libs/json +[submodule "external/json"] + path = external/json url = https://github.com/nlohmann/json.git +[submodule "external/event-hub-cpp"] + path = external/event-hub-cpp + url = https://github.com/NewYaroslav/event-hub-cpp.git + branch = main [submodule ".github/doxygen-awesome-css"] path = .github/doxygen-awesome-css url = https://github.com/jothepro/doxygen-awesome-css.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d7d7c2..f560cb0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,9 +12,14 @@ endif() option(CONSOLIX_USE_LOGIT "Use LogIt++ logging library" ON) option(CONSOLIX_USE_JSON "Use nlohmann/json" ON) option(CONSOLIX_USE_CXXOPTS "Use cxxopts for argument parsing" ON) +option(CONSOLIX_USE_EVENT_HUB "Use optional event-hub-cpp integration" OFF) option(CONSOLIX_BUILD_EXAMPLES "Build Consolix examples" ON) option(CONSOLIX_BUILD_TESTS "Build Consolix tests" ON) +if(CONSOLIX_USE_EVENT_HUB AND CONSOLIX_CXX_STANDARD LESS 17) + message(FATAL_ERROR "CONSOLIX_USE_EVENT_HUB requires CONSOLIX_CXX_STANDARD=17") +endif() + set(CMAKE_CXX_STANDARD ${CONSOLIX_CXX_STANDARD}) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) @@ -34,29 +39,42 @@ target_compile_definitions(Consolix CONSOLIX_USE_LOGIT=$ CONSOLIX_USE_JSON=$ CONSOLIX_USE_CXXOPTS=$ + CONSOLIX_USE_EVENT_HUB=$ CONSOLIX_BASE_PATH="${CMAKE_CURRENT_SOURCE_DIR}" ) if(CONSOLIX_USE_LOGIT) target_include_directories(Consolix SYSTEM INTERFACE - $ - $ + $ + $ ) endif() if(CONSOLIX_USE_JSON) target_include_directories(Consolix SYSTEM INTERFACE - $ + $ ) endif() if(CONSOLIX_USE_CXXOPTS) target_include_directories(Consolix SYSTEM INTERFACE - $ + $ + ) +endif() + +if(CONSOLIX_USE_EVENT_HUB) + set(EVENT_HUB_CPP_BUILD_EXAMPLES OFF CACHE BOOL "Build event-hub-cpp examples" FORCE) + set(EVENT_HUB_CPP_BUILD_TESTS OFF CACHE BOOL "Build event-hub-cpp tests" FORCE) + set(EVENT_HUB_CPP_USE_TIME_SHIELD OFF CACHE BOOL "Enable optional time-shield-cpp integration" FORCE) + + add_subdirectory( + ${CMAKE_CURRENT_SOURCE_DIR}/external/event-hub-cpp + ${CMAKE_CURRENT_BINARY_DIR}/external/event-hub-cpp ) + target_link_libraries(Consolix INTERFACE event_hub::event_hub) endif() if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CONSOLIX_CXX_STANDARD LESS 17) @@ -125,4 +143,14 @@ if(CONSOLIX_BUILD_TESTS) target_compile_definitions(test_posix_signal_shutdown PRIVATE CONSOLIX_WAIT_ON_ERROR=0) set_tests_properties(test_posix_signal_shutdown PROPERTIES TIMEOUT 15) endif() + + if(CONSOLIX_USE_EVENT_HUB AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_event_hub_component.cpp") + consolix_add_test(test_event_hub_component "tests/test_event_hub_component.cpp") + set_tests_properties(test_event_hub_component PROPERTIES TIMEOUT 15) + endif() + + if(CONSOLIX_USE_EVENT_HUB AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_module_hub_component.cpp") + consolix_add_test(test_module_hub_component "tests/test_module_hub_component.cpp") + set_tests_properties(test_module_hub_component PROPERTIES TIMEOUT 15) + endif() endif() diff --git a/external/cxxopts b/external/cxxopts new file mode 160000 index 0000000..a3a21b3 --- /dev/null +++ b/external/cxxopts @@ -0,0 +1 @@ +Subproject commit a3a21b31ef2894b5a802f041f40d51059469a259 diff --git a/external/event-hub-cpp b/external/event-hub-cpp new file mode 160000 index 0000000..9e8b1a3 --- /dev/null +++ b/external/event-hub-cpp @@ -0,0 +1 @@ +Subproject commit 9e8b1a398192c3c69c9d52c9b7a82efe6721a845 diff --git a/external/json b/external/json new file mode 160000 index 0000000..e08b3ca --- /dev/null +++ b/external/json @@ -0,0 +1 @@ +Subproject commit e08b3cab6895835bc2e1f19079849bfc2422d0b6 diff --git a/external/log-it-cpp b/external/log-it-cpp new file mode 160000 index 0000000..3a8bd8a --- /dev/null +++ b/external/log-it-cpp @@ -0,0 +1 @@ +Subproject commit 3a8bd8a6fa21cbf50d8b56d12b8f075fdab377eb diff --git a/external/time-shield-cpp b/external/time-shield-cpp new file mode 160000 index 0000000..d3c251b --- /dev/null +++ b/external/time-shield-cpp @@ -0,0 +1 @@ +Subproject commit d3c251bf173ee6222f6e79186a34c4a0592d5767 diff --git a/include/consolix/components.hpp b/include/consolix/components.hpp index f4b45db..def8e93 100644 --- a/include/consolix/components.hpp +++ b/include/consolix/components.hpp @@ -31,12 +31,15 @@ /// - **ConfigComponent**: Loads and manages configuration from JSON files. /// - **BaseLoopComponent**: A base class for loop-based components. /// - **LoopComponent**: A component with customizable execution loops. +/// - **EventHubComponent**: Optional bridge to event-hub-cpp EventBus and TaskManager. +/// - **ModuleHubComponent**: Optional bridge to event-hub-cpp ModuleHub. /// /// ### Key Features: /// - Easy integration of logging and logo rendering. /// - Convenient handling of command-line arguments. /// - Lightweight JSON configuration management, including support for comments. /// - Support for custom execution loops using either a base class or the LoopComponent. +/// - Optional event-hub-cpp processing from the Consolix component loop. /// - UTF-8 support for program titles on all platforms: /// - Automatically converted to UTF-16 on Windows for proper Unicode rendering. /// - Displayed via ANSI escape sequences on Linux/macOS. @@ -54,6 +57,8 @@ /// - `components/ConfigComponent.hpp` /// - `components/BaseLoopComponent.hpp` /// - `components/LoopComponent.hpp` +/// - `components/EventHubComponent.hpp` when `CONSOLIX_USE_EVENT_HUB=1` +/// - `components/ModuleHubComponent.hpp` when `CONSOLIX_USE_EVENT_HUB=1` /// /// ### Example Usage: /// @@ -94,11 +99,17 @@ #include "core/service_utils.hpp" ///< Utility functions for working with services. // Core components of the Consolix framework -#include "components/TitleComponent.hpp" ///< Component for managing the console window title across platforms. +#include "components/TitleComponent.hpp" ///< Component for managing the console window title across platforms. #include "components/LoggerComponent.hpp" ///< Component for managing logging. #include "components/LogoComponent.hpp" ///< Component for rendering logos in the console. #include "components/BaseLoopComponent.hpp" ///< Base class for implementing loop-based components. #include "components/LoopComponent.hpp" ///< Component with configurable initialization, loop, and shutdown callbacks. + +#if CONSOLIX_USE_EVENT_HUB == 1 +#include "components/EventHubComponent.hpp" ///< Optional event-hub-cpp EventBus/TaskManager integration component. +#include "components/ModuleHubComponent.hpp" ///< Optional event-hub-cpp ModuleHub integration component. +#endif + #include "components/CliComponent.hpp" ///< Component for handling command-line arguments. // JSON configuration management diff --git a/include/consolix/components/EventHubComponent.hpp b/include/consolix/components/EventHubComponent.hpp new file mode 100644 index 0000000..1a821ef --- /dev/null +++ b/include/consolix/components/EventHubComponent.hpp @@ -0,0 +1,114 @@ +#pragma once +#ifndef _CONSOLIX_EVENT_HUB_COMPONENT_HPP_INCLUDED +#define _CONSOLIX_EVENT_HUB_COMPONENT_HPP_INCLUDED + +/// \file EventHubComponent.hpp +/// \brief Defines optional event-hub-cpp processing component. +/// \ingroup Components + +#if CONSOLIX_USE_EVENT_HUB == 1 + +#include + +#include + +namespace consolix { + + /// \class EventHubComponent + /// \brief Processes event-hub-cpp sources from the Consolix component loop. + /// + /// The component is intentionally passive. It does not own a thread and only + /// calls `EventBus::process()` and `TaskManager::process()` when Consolix + /// invokes the component's `process()` method. + class EventHubComponent : + public IAppComponent, + public IShutdownable { + public: + /// \brief Default maximum number of tasks processed per loop pass. + static constexpr std::size_t default_max_tasks_per_pass = 128; + + /// \brief Constructs an empty component. + EventHubComponent() = default; + + /// \brief Constructs component with optional non-owning sources. + /// \param event_bus Event bus to process, or nullptr. + /// \param task_manager Task manager to process, or nullptr. + /// \param max_tasks_per_pass Maximum ready tasks processed per pass. + EventHubComponent( + event_hub::EventBus* event_bus, + event_hub::TaskManager* task_manager = nullptr, + std::size_t max_tasks_per_pass = default_max_tasks_per_pass) : + m_event_bus(event_bus), + m_task_manager(task_manager), + m_max_tasks_per_pass(max_tasks_per_pass) { + } + + /// \brief Sets event bus processed by this component. + /// \param event_bus Event bus to process, or nullptr. + void set_event_bus(event_hub::EventBus* event_bus) { + m_event_bus = event_bus; + } + + /// \brief Sets task manager processed by this component. + /// \param task_manager Task manager to process, or nullptr. + void set_task_manager(event_hub::TaskManager* task_manager) { + m_task_manager = task_manager; + } + + /// \brief Sets maximum ready tasks processed per loop pass. + /// \param max_tasks_per_pass Maximum ready tasks processed per pass. + void set_max_tasks_per_pass(std::size_t max_tasks_per_pass) { + m_max_tasks_per_pass = max_tasks_per_pass; + } + + /// \brief Returns total work units processed by the last pass. + /// \return Number of processed events and tasks. + std::size_t last_work_count() const { + return m_last_work_count; + } + + protected: + bool initialize() override { + m_is_init = true; + return true; + } + + bool is_initialized() const override { + return m_is_init; + } + + void process() override { + std::size_t work_count = 0; + + if (m_event_bus) { + work_count += m_event_bus->process(); + } + if (m_task_manager) { + work_count += m_task_manager->process(m_max_tasks_per_pass); + } + + m_last_work_count = work_count; + } + + void shutdown(int /*signal*/) override { + if (m_event_bus) { + m_event_bus->clear_pending(); + } + if (m_task_manager) { + m_task_manager->clear_pending(); + } + } + + private: + event_hub::EventBus* m_event_bus{nullptr}; ///< Non-owning event bus pointer. + event_hub::TaskManager* m_task_manager{nullptr}; ///< Non-owning task manager pointer. + std::size_t m_max_tasks_per_pass{default_max_tasks_per_pass}; + std::size_t m_last_work_count{0}; + bool m_is_init{false}; + }; + +} // namespace consolix + +#endif // CONSOLIX_USE_EVENT_HUB == 1 + +#endif // _CONSOLIX_EVENT_HUB_COMPONENT_HPP_INCLUDED diff --git a/include/consolix/components/LoggerComponent.hpp b/include/consolix/components/LoggerComponent.hpp index 93df79c..670faed 100644 --- a/include/consolix/components/LoggerComponent.hpp +++ b/include/consolix/components/LoggerComponent.hpp @@ -109,7 +109,7 @@ #define CONSOLIX_SET_DEBUG_MODE(mode) \ LOGIT_SET_LOGGER_ENABLED(CONSOLIX_LOGIT_DEBUG_INDEX, mode) -#include +#include #include "LoggerComponent/MultiStream.hpp" namespace consolix { diff --git a/include/consolix/components/ModuleHubComponent.hpp b/include/consolix/components/ModuleHubComponent.hpp new file mode 100644 index 0000000..43a1efe --- /dev/null +++ b/include/consolix/components/ModuleHubComponent.hpp @@ -0,0 +1,87 @@ +#pragma once +#ifndef _CONSOLIX_MODULE_HUB_COMPONENT_HPP_INCLUDED +#define _CONSOLIX_MODULE_HUB_COMPONENT_HPP_INCLUDED + +/// \file ModuleHubComponent.hpp +/// \brief Defines optional event-hub-cpp ModuleHub processing component. +/// \ingroup Components + +#if CONSOLIX_USE_EVENT_HUB == 1 + +#include + +#include + +namespace consolix { + + /// \class ModuleHubComponent + /// \brief Processes an event_hub::ModuleHub from the Consolix component loop. + /// + /// The component does not own the hub. The referenced ModuleHub must outlive + /// this component and should not run its own active `run()` or `start()` loop + /// while it is processed by Consolix. + class ModuleHubComponent : + public IAppComponent, + public IShutdownable { + public: + /// \brief Constructs an empty component. + ModuleHubComponent() = default; + + /// \brief Constructs component with a non-owning ModuleHub pointer. + /// \param module_hub ModuleHub to process, or nullptr. + explicit ModuleHubComponent(event_hub::ModuleHub* module_hub) : + m_module_hub(module_hub) { + } + + /// \brief Sets ModuleHub processed by this component. + /// \param module_hub ModuleHub to process, or nullptr. + void set_module_hub(event_hub::ModuleHub* module_hub) { + m_module_hub = module_hub; + } + + /// \brief Returns work units processed by the last pass. + /// \return Number of processed events, tasks, and module hooks. + std::size_t last_work_count() const { + return m_last_work_count; + } + + protected: + bool initialize() override { + if (m_module_hub) { + m_module_hub->initialize(); + } + m_is_init = true; + return true; + } + + bool is_initialized() const override { + return m_is_init; + } + + void process() override { + if (!m_module_hub) { + m_last_work_count = 0; + return; + } + + m_last_work_count = m_module_hub->process(); + } + + void shutdown(int /*signal*/) override { + if (m_module_hub) { + m_module_hub->request_stop(); + m_module_hub->shutdown(); + } + } + + private: + event_hub::ModuleHub* m_module_hub{nullptr}; ///< Non-owning ModuleHub pointer. + std::size_t m_last_work_count{0}; + bool m_is_init{false}; + }; + +} // namespace consolix + +#endif // CONSOLIX_USE_EVENT_HUB == 1 + +#endif // _CONSOLIX_MODULE_HUB_COMPONENT_HPP_INCLUDED diff --git a/include/consolix/config_macros.hpp b/include/consolix/config_macros.hpp index c4c116f..f466a39 100644 --- a/include/consolix/config_macros.hpp +++ b/include/consolix/config_macros.hpp @@ -47,6 +47,14 @@ #define CONSOLIX_USE_JSON 0 #endif +/// \def CONSOLIX_USE_EVENT_HUB +/// \brief Enables or disables optional event-hub-cpp integration. +/// \details Set to `1` to enable EventHubComponent and the event-hub-cpp headers. +/// \default `0` +#ifndef CONSOLIX_USE_EVENT_HUB +#define CONSOLIX_USE_EVENT_HUB 0 +#endif + /// \def CONSOLIX_BASE_PATH /// \brief Defines the base path for resolving relative paths. /// \details Set to an empty object `{}` by default. diff --git a/include/consolix/core/ServiceLocator.hpp b/include/consolix/core/ServiceLocator.hpp index b47ec1a..4b24d38 100644 --- a/include/consolix/core/ServiceLocator.hpp +++ b/include/consolix/core/ServiceLocator.hpp @@ -7,7 +7,7 @@ /// \ingroup Core #if CONSOLIX_USE_LOGIT == 1 -#include +#include #endif #include "std_compat.hpp" diff --git a/libs/cxxopts b/libs/cxxopts deleted file mode 160000 index 10a7a64..0000000 --- a/libs/cxxopts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 10a7a647791fa3a24ec4f572f2573a6e0aaa881b diff --git a/libs/json b/libs/json deleted file mode 160000 index a006a7a..0000000 --- a/libs/json +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a006a7a48bb30a247f0344b788c62c2806edd90b diff --git a/libs/log-it-cpp b/libs/log-it-cpp deleted file mode 160000 index 874e0e0..0000000 --- a/libs/log-it-cpp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 874e0e01b02be310ac136c4f54a34324f1d752b7 diff --git a/libs/time-shield-cpp b/libs/time-shield-cpp deleted file mode 160000 index 3649f26..0000000 --- a/libs/time-shield-cpp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3649f26098420ad3a131fbf0fc82ab9f16f0e558 diff --git a/tests/test_event_hub_component.cpp b/tests/test_event_hub_component.cpp new file mode 100644 index 0000000..1778e01 --- /dev/null +++ b/tests/test_event_hub_component.cpp @@ -0,0 +1,61 @@ +#include +#include + +#include +#include + +#if CONSOLIX_USE_EVENT_HUB != 1 +#error "test_event_hub_component requires CONSOLIX_USE_EVENT_HUB=1" +#endif + +struct TestEvent { + int value; +}; + +int main() { + event_hub::EventBus bus; + event_hub::TaskManager tasks; + + int event_sum = 0; + bool task_ran = false; + + event_hub::EventEndpoint endpoint(bus); + endpoint.subscribe([&event_sum](const TestEvent& event) { + event_sum += event.value; + }); + + endpoint.post(7); + tasks.post([&task_ran] { + task_ran = true; + }); + + consolix::AppComponentManager manager; + auto component = manager.add(&bus, &tasks); + + if (!manager.initialize()) { + throw std::runtime_error("EventHubComponent failed to initialize"); + } + + manager.process(); + + if (event_sum != 7) { + throw std::runtime_error("EventHubComponent did not process EventBus queue"); + } + if (!task_ran) { + throw std::runtime_error("EventHubComponent did not process TaskManager queue"); + } + if (component->last_work_count() != 2) { + throw std::runtime_error("Unexpected EventHubComponent work count"); + } + + endpoint.post(5); + tasks.post([] {}); + manager.shutdown(0); + manager.process(); + + if (event_sum != 7) { + throw std::runtime_error("EventHubComponent did not clear pending events during shutdown"); + } + + return 0; +} diff --git a/tests/test_module_hub_component.cpp b/tests/test_module_hub_component.cpp new file mode 100644 index 0000000..530d77c --- /dev/null +++ b/tests/test_module_hub_component.cpp @@ -0,0 +1,76 @@ +#include + +#include +#include + +#if CONSOLIX_USE_EVENT_HUB != 1 +#error "test_module_hub_component requires CONSOLIX_USE_EVENT_HUB=1" +#endif + +class TestModule final : public event_hub::Module { +public: + explicit TestModule(event_hub::EventBus& bus, int& initialized, int& processed, int& shutdown) : + event_hub::Module(bus), + m_initialized(initialized), + m_processed(processed), + m_shutdown(shutdown) { + } + +protected: + void on_initialize() override { + ++m_initialized; + tasks().post([this] { + ++m_processed; + }); + } + + std::size_t on_process() override { + ++m_processed; + return 1; + } + + void on_shutdown() noexcept override { + ++m_shutdown; + } + +private: + int& m_initialized; + int& m_processed; + int& m_shutdown; +}; + +int main() { + int initialized = 0; + int processed = 0; + int shutdown = 0; + + event_hub::ModuleHub hub; + hub.emplace_module(initialized, processed, shutdown); + + consolix::AppComponentManager manager; + auto component = manager.add(&hub); + + if (!manager.initialize()) { + throw std::runtime_error("ModuleHubComponent failed to initialize"); + } + if (initialized != 1) { + throw std::runtime_error("ModuleHubComponent did not initialize ModuleHub"); + } + + manager.process(); + + if (processed < 2) { + throw std::runtime_error("ModuleHubComponent did not process ModuleHub work"); + } + if (component->last_work_count() == 0) { + throw std::runtime_error("ModuleHubComponent did not report processed work"); + } + + manager.shutdown(0); + + if (shutdown != 1) { + throw std::runtime_error("ModuleHubComponent did not shutdown ModuleHub exactly once"); + } + + return 0; +}