From be3a00d4a23c400d1fab214aa0e540971f45e02e Mon Sep 17 00:00:00 2001 From: Vipin Kumar Date: Fri, 9 Jan 2026 20:55:09 +0530 Subject: [PATCH 1/9] Add gtest-based unit test for EventLoop lifecycle --- CMakeLists.txt | 40 +++++++++++++++++++++++++++++++++++- tests/EventLoopBasicTest.cpp | 27 ++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 tests/EventLoopBasicTest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 1184ad6..670e007 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,18 @@ cmake_minimum_required(VERSION 3.1.0) project(EventLoop VERSION 2.0.0) +include(FetchContent) + +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip +) + +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) + +enable_testing() + find_package(Threads REQUIRED) include_directories(include) @@ -30,4 +42,30 @@ set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD_REQUIRED YES ) target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_11) -target_link_libraries(${PROJECT_NAME} ${CMAKE_THREAD_LIBS_INIT}) \ No newline at end of file +target_link_libraries(${PROJECT_NAME} ${CMAKE_THREAD_LIBS_INIT}) + +# -------------------- +# Tests +# -------------------- + +add_executable(EventLoopTests + tests/EventLoopBasicTest.cpp + tests/EventLoopMultipleHandlersTest.cpp + tests/EventLoopDataTest.cpp + tests/EventLoopThreadingTest.cpp + tests/EventLoopShutdownTest.cpp + tests/EventLoopTimerTest.cpp +) + +target_link_libraries(EventLoopTests + PRIVATE + gtest + gtest_main + EventLoop +) + +add_test( + NAME EventLoopBasicTest + COMMAND EventLoopTests +) + diff --git a/tests/EventLoopBasicTest.cpp b/tests/EventLoopBasicTest.cpp new file mode 100644 index 0000000..0f4a414 --- /dev/null +++ b/tests/EventLoopBasicTest.cpp @@ -0,0 +1,27 @@ +#include +#include +#include +#include + +TEST(EventLoopBasicTest, BasicEventLoopFlow) +{ + std::atomic handlerCalled{false}; + int value = 42; + + EventLoop::RegisterEvent("TestEvent", [&](EventLoop::Event*) { + handlerCalled = true; + }); + + EventLoop::RegisterEvent("DataEvent", [&](EventLoop::Event* evt) { + int* data = static_cast(evt->getData()); + EXPECT_EQ(*data, 42); + EventLoop::Halt(); + }); + + EventLoop::TriggerEvent("TestEvent"); + EventLoop::TriggerEvent("DataEvent", &value); + + EventLoop::Run(); + + EXPECT_TRUE(handlerCalled.load()); +} From 725bb2a465ecf9649a199e97486b875626ebf6df Mon Sep 17 00:00:00 2001 From: Vipin Kumar Date: Sun, 18 Jan 2026 18:23:11 +0530 Subject: [PATCH 2/9] Add gtest-based unit tests and improve coverage to ~67% --- CMakeLists.txt | 7 +----- tests/EventLoopBasicTest.cpp | 43 +++++++++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 670e007..b6ab742 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,12 +50,7 @@ target_link_libraries(${PROJECT_NAME} ${CMAKE_THREAD_LIBS_INIT}) add_executable(EventLoopTests tests/EventLoopBasicTest.cpp - tests/EventLoopMultipleHandlersTest.cpp - tests/EventLoopDataTest.cpp - tests/EventLoopThreadingTest.cpp - tests/EventLoopShutdownTest.cpp - tests/EventLoopTimerTest.cpp -) +) target_link_libraries(EventLoopTests PRIVATE diff --git a/tests/EventLoopBasicTest.cpp b/tests/EventLoopBasicTest.cpp index 0f4a414..f19da9b 100644 --- a/tests/EventLoopBasicTest.cpp +++ b/tests/EventLoopBasicTest.cpp @@ -3,25 +3,56 @@ #include #include -TEST(EventLoopBasicTest, BasicEventLoopFlow) +TEST(EventLoopTest, FullLifecycleCoverage) { - std::atomic handlerCalled{false}; + std::atomic count{0}; + std::atomic delayedCalled{false}; int value = 42; + // RegisterEvent (basic) EventLoop::RegisterEvent("TestEvent", [&](EventLoop::Event*) { - handlerCalled = true; + count++; }); + // Data + getName EventLoop::RegisterEvent("DataEvent", [&](EventLoop::Event* evt) { - int* data = static_cast(evt->getData()); - EXPECT_EQ(*data, 42); + EXPECT_EQ(evt->getName(), "DataEvent"); + EXPECT_EQ(*static_cast(evt->getData()), 42); + count++; + }); + + // RegisterEvents (vector) + EventLoop::RegisterEvents({"A", "B"}, [&](EventLoop::Event*) { + count++; + }); + + // Multiple handlers + EventLoop::RegisterEvent("Multi", [&](EventLoop::Event*) { count++; }); + EventLoop::RegisterEvent("Multi", [&](EventLoop::Event*) { count++; }); + + // DeregisterEvent + EventLoop::RegisterEvent("X", [&](EventLoop::Event*) { + FAIL() << "DeregisterEvent failed"; + }); + EventLoop::DeregisterEvent("X"); + + // Delayed event (used to stop loop safely) + EventLoop::RegisterEvent("Stop", [&](EventLoop::Event*) { + delayedCalled = true; EventLoop::Halt(); }); + // Trigger everything EventLoop::TriggerEvent("TestEvent"); EventLoop::TriggerEvent("DataEvent", &value); + EventLoop::TriggerEvent("A"); + EventLoop::TriggerEvent("B"); + EventLoop::TriggerEvent("Multi"); + EventLoop::TriggerEvent("X"); // should do nothing + EventLoop::TriggerEvent("Stop", 10); // delayed halt EventLoop::Run(); - EXPECT_TRUE(handlerCalled.load()); + EXPECT_TRUE(delayedCalled.load()); + EXPECT_GE(count.load(), 6); } From 6790f11eafd4140ac69721424d44fe4e3c86f01c Mon Sep 17 00:00:00 2001 From: Amol Dhamale Date: Sun, 15 Feb 2026 19:47:11 +0000 Subject: [PATCH 3/9] restructure to keep test cmake separate, add functionality to test and generate coverage in build script --- CMakeLists.txt | 37 ++++++------------------------------- build.sh | 29 ++++++++++++++++++++++++++--- tests/CMakeLists.txt | 21 +++++++++++++++++++++ 3 files changed, 53 insertions(+), 34 deletions(-) mode change 100755 => 100644 build.sh create mode 100644 tests/CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index b6ab742..7a67008 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,18 +2,6 @@ cmake_minimum_required(VERSION 3.1.0) project(EventLoop VERSION 2.0.0) -include(FetchContent) - -FetchContent_Declare( - googletest - URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip -) - -set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) -FetchContent_MakeAvailable(googletest) - -enable_testing() - find_package(Threads REQUIRED) include_directories(include) @@ -44,23 +32,10 @@ set_target_properties(${PROJECT_NAME} PROPERTIES target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_11) target_link_libraries(${PROJECT_NAME} ${CMAKE_THREAD_LIBS_INIT}) -# -------------------- -# Tests -# -------------------- - -add_executable(EventLoopTests - tests/EventLoopBasicTest.cpp -) - -target_link_libraries(EventLoopTests - PRIVATE - gtest - gtest_main - EventLoop -) - -add_test( - NAME EventLoopBasicTest - COMMAND EventLoopTests -) +if(TEST_COVERAGE) + target_compile_options(${PROJECT_NAME} PRIVATE -fprofile-arcs -ftest-coverage) + target_link_options(${PROJECT_NAME} PRIVATE -fprofile-arcs -ftest-coverage) + enable_testing() + add_subdirectory(tests) +endif() diff --git a/build.sh b/build.sh old mode 100755 new mode 100644 index dee6acf..55d6b74 --- a/build.sh +++ b/build.sh @@ -1,13 +1,13 @@ #!/bin/bash -usage() { echo "usage: $0 [-a|c {a} (all|clean)] [-r|d {d} (release|debug)] [-g {Ninja}]" 2>&1; exit 0; } +usage() { echo "usage: $0 [-a|c {a} (all|clean)] [-r|d {d} (release|debug)] [-g {Ninja}] [-t (test and coverage)]" 2>&1; exit 0; } BUILD_TYPE="Debug" GENERATOR="Ninja" TARGET="all" ACTION="Build" -while getopts ":acrdg:h" arg; do +while getopts ":acrdtg:h" arg; do case "${arg}" in a) TARGET="all" @@ -23,6 +23,9 @@ while getopts ":acrdg:h" arg; do d) BUILD_TYPE="Debug" ;; + t) + COVERAGE="ON" + ;; g) GENERATOR=${OPTARG} ;; @@ -42,7 +45,7 @@ if [ ${TARGET} = "all" ]; then rm -rf build/{.[!.]*,*} fi echo "Configuring Event Loop in ${BUILD_TYPE} mode..." - cmake -DCMAKE_BUILD_TYPE:STRING=${BUILD_TYPE} -S . -B ${PWD}/build -G "${GENERATOR}" + cmake -DCMAKE_BUILD_TYPE:STRING=${BUILD_TYPE} -DTEST_COVERAGE:BOOL=${COVERAGE} -S . -B ${PWD}/build -G "${GENERATOR}" fi echo "${ACTION}ing Event Loop..." @@ -50,6 +53,26 @@ if [ ! -z "$(ls -A ${PWD}/build)" ]; then cmake --build ${PWD}/build --config ${BUILD_TYPE} --target ${TARGET} fi +# Run tests and generate coverage report if coverage is enabled +if [ "${COVERAGE}" = "ON" ]; then + echo "Running unit tests..." + BUILD_DIR="$(pwd)/build" + (cd "${BUILD_DIR}" && ctest --output-on-failure) || { echo "Tests failed."; exit 1; } + + echo "Tests passed. Generating coverage report..." + COVERAGE_DIR="${BUILD_DIR}/coverage" + mkdir -p ${COVERAGE_DIR} + + lcov --directory "${BUILD_DIR}" --capture --output-file ${COVERAGE_DIR}/coverage.info \ + --rc branch_coverage=1 --exclude '/usr/*' --exclude '*/tests/*' --exclude '*/_deps/*' \ + --quiet + + genhtml --output-directory ${COVERAGE_DIR}/html --title "Event Loop Code Coverage" \ + --prefix "${PWD}" --rc branch_coverage=1 --quiet ${COVERAGE_DIR}/coverage.info + + echo "Coverage report generated at: ${COVERAGE_DIR}/html/index.html" +fi + if [ ${TARGET} = "clean" ]; then if [ -d ${PWD}/build ]; then rm -rf build/{.[!.]*,*} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..36dcb82 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,21 @@ +include(FetchContent) + +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip +) +FetchContent_MakeAvailable(googletest) + +add_executable(EventLoopTests EventLoopBasicTest.cpp) + +target_link_libraries(EventLoopTests + PRIVATE + gtest + gtest_main + EventLoop +) + +target_compile_options(EventLoopTests PRIVATE -fprofile-arcs -ftest-coverage) +target_link_options(EventLoopTests PRIVATE -fprofile-arcs -ftest-coverage) + +add_test(NAME EventLoopBasicTest COMMAND EventLoopTests) \ No newline at end of file From e2a786ebb778b3ad9db28226308cb5c70b341e84 Mon Sep 17 00:00:00 2001 From: Amol Dhamale Date: Mon, 16 Feb 2026 04:55:55 +0000 Subject: [PATCH 4/9] use a more concise and generic coverage option --- CMakeLists.txt | 4 ++-- tests/CMakeLists.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a67008..5adc19d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,8 +33,8 @@ target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_11) target_link_libraries(${PROJECT_NAME} ${CMAKE_THREAD_LIBS_INIT}) if(TEST_COVERAGE) - target_compile_options(${PROJECT_NAME} PRIVATE -fprofile-arcs -ftest-coverage) - target_link_options(${PROJECT_NAME} PRIVATE -fprofile-arcs -ftest-coverage) + target_compile_options(${PROJECT_NAME} PRIVATE -coverage) + target_link_options(${PROJECT_NAME} PRIVATE -coverage) enable_testing() add_subdirectory(tests) endif() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 36dcb82..d430f70 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,7 +15,7 @@ target_link_libraries(EventLoopTests EventLoop ) -target_compile_options(EventLoopTests PRIVATE -fprofile-arcs -ftest-coverage) -target_link_options(EventLoopTests PRIVATE -fprofile-arcs -ftest-coverage) +target_compile_options(EventLoopTests PRIVATE -coverage) +target_link_options(EventLoopTests PRIVATE -coverage) add_test(NAME EventLoopBasicTest COMMAND EventLoopTests) \ No newline at end of file From 035dab4f4ae95e4c7a26ca86fb7e3808af06fbaf Mon Sep 17 00:00:00 2001 From: Vipin Kumar Date: Tue, 17 Feb 2026 01:02:25 +0530 Subject: [PATCH 5/9] Refactor: Sync with upstream and add Windows/MSVC compatibility --- CMakeLists.txt | 21 +++++++++++++-------- tests/CMakeLists.txt | 11 +++++++++-- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5adc19d..47ad472 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1.0) +cmake_minimum_required(VERSION 3.14) project(EventLoop VERSION 2.0.0) @@ -6,7 +6,6 @@ find_package(Threads REQUIRED) include_directories(include) -# INCLUDES variable defines the headers required by apps using this library set(INCLUDES include/Event.h include/EventLoop.h) @@ -17,9 +16,9 @@ set(SOURCES src/EventSender.cpp src/EventReceiver.cpp) -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib) +# set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib) -add_library(${PROJECT_NAME} SHARED +add_library(${PROJECT_NAME} STATIC ${INCLUDES} ${SOURCES}) @@ -32,10 +31,16 @@ set_target_properties(${PROJECT_NAME} PROPERTIES target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_11) target_link_libraries(${PROJECT_NAME} ${CMAKE_THREAD_LIBS_INIT}) +if(MSVC) + set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +endif() + if(TEST_COVERAGE) - target_compile_options(${PROJECT_NAME} PRIVATE -coverage) - target_link_options(${PROJECT_NAME} PRIVATE -coverage) + if(NOT MSVC) + target_compile_options(${PROJECT_NAME} PRIVATE -coverage) + target_link_options(${PROJECT_NAME} PRIVATE -coverage) + endif() + enable_testing() add_subdirectory(tests) -endif() - +endif() \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d430f70..86bb017 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,6 +4,11 @@ FetchContent_Declare( googletest URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip ) + +if(MSVC) + set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +endif() + FetchContent_MakeAvailable(googletest) add_executable(EventLoopTests EventLoopBasicTest.cpp) @@ -15,7 +20,9 @@ target_link_libraries(EventLoopTests EventLoop ) -target_compile_options(EventLoopTests PRIVATE -coverage) -target_link_options(EventLoopTests PRIVATE -coverage) +if(NOT MSVC) + target_compile_options(EventLoopTests PRIVATE -coverage) + target_link_options(EventLoopTests PRIVATE -coverage) +endif() add_test(NAME EventLoopBasicTest COMMAND EventLoopTests) \ No newline at end of file From 68eaa86f29333bc65f268d2fe81bea7e57d63185 Mon Sep 17 00:00:00 2001 From: Vipin Kumar Date: Wed, 18 Feb 2026 20:31:40 +0530 Subject: [PATCH 6/9] Fix: Enable symbol export and configure test environment for Windows DLLs --- CMakeLists.txt | 8 +++++--- tests/CMakeLists.txt | 8 +++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 47ad472..6a16329 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,10 +15,10 @@ set(SOURCES src/EventManager.cpp src/EventSender.cpp src/EventReceiver.cpp) + +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib) -# set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib) - -add_library(${PROJECT_NAME} STATIC +add_library(${PROJECT_NAME} SHARED ${INCLUDES} ${SOURCES}) @@ -27,7 +27,9 @@ set_target_properties(${PROJECT_NAME} PROPERTIES SOVERSION ${PROJECT_VERSION_MAJOR} OUTPUT_NAME ${PROJECT_NAME} CXX_STANDARD_REQUIRED YES + WINDOWS_EXPORT_ALL_SYMBOLS ON ) + target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_11) target_link_libraries(${PROJECT_NAME} ${CMAKE_THREAD_LIBS_INIT}) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 86bb017..af5317e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -25,4 +25,10 @@ if(NOT MSVC) target_link_options(EventLoopTests PRIVATE -coverage) endif() -add_test(NAME EventLoopBasicTest COMMAND EventLoopTests) \ No newline at end of file +add_test(NAME EventLoopBasicTest COMMAND EventLoopTests) + +if(WIN32) + set_tests_properties(EventLoopBasicTest PROPERTIES + ENVIRONMENT "PATH=$;$ENV{PATH}" + ) +endif() \ No newline at end of file From 9d089d28657923db808609a9b8cb051afba17e57 Mon Sep 17 00:00:00 2001 From: Vipin Kumar Date: Thu, 19 Feb 2026 01:41:31 +0530 Subject: [PATCH 7/9] Fix: Address review comments (formatting, conditional export, cmake policy) --- CMakeLists.txt | 13 +++++++------ tests/CMakeLists.txt | 2 ++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a16329..554e5dc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,7 @@ find_package(Threads REQUIRED) include_directories(include) +# A INCLUDES variable defines the headers required by apps using this library set(INCLUDES include/Event.h include/EventLoop.h) @@ -15,7 +16,7 @@ set(SOURCES src/EventManager.cpp src/EventSender.cpp src/EventReceiver.cpp) - + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib) add_library(${PROJECT_NAME} SHARED @@ -27,16 +28,16 @@ set_target_properties(${PROJECT_NAME} PROPERTIES SOVERSION ${PROJECT_VERSION_MAJOR} OUTPUT_NAME ${PROJECT_NAME} CXX_STANDARD_REQUIRED YES - WINDOWS_EXPORT_ALL_SYMBOLS ON ) -target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_11) -target_link_libraries(${PROJECT_NAME} ${CMAKE_THREAD_LIBS_INIT}) - if(MSVC) - set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) + set_target_properties(${PROJECT_NAME} PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) + set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) endif() +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_11) +target_link_libraries(${PROJECT_NAME} ${CMAKE_THREAD_LIBS_INIT}) + if(TEST_COVERAGE) if(NOT MSVC) target_compile_options(${PROJECT_NAME} PRIVATE -coverage) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index af5317e..0f36275 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,3 +1,5 @@ +cmake_policy(SET CMP0135 NEW) + include(FetchContent) FetchContent_Declare( From f06d1058d3134344b93921886b48bc5337805f97 Mon Sep 17 00:00:00 2001 From: Vipin Kumar Date: Wed, 11 Mar 2026 23:40:57 +0530 Subject: [PATCH 8/9] Improve coverage to 100% functions/88% lines and add Windows coverage report --- .gitignore | 2 + build.sh | 64 ++++++++-- tests/EventLoopBasicTest.cpp | 239 ++++++++++++++++++++++++++++++----- 3 files changed, 264 insertions(+), 41 deletions(-) diff --git a/.gitignore b/.gitignore index 2881192..d1185d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ lib/* build/* +CoverageReport-*/ +LastCoverageResults.log \ No newline at end of file diff --git a/build.sh b/build.sh index 55d6b74..db7507e 100644 --- a/build.sh +++ b/build.sh @@ -58,19 +58,59 @@ if [ "${COVERAGE}" = "ON" ]; then echo "Running unit tests..." BUILD_DIR="$(pwd)/build" (cd "${BUILD_DIR}" && ctest --output-on-failure) || { echo "Tests failed."; exit 1; } - + echo "Tests passed. Generating coverage report..." COVERAGE_DIR="${BUILD_DIR}/coverage" - mkdir -p ${COVERAGE_DIR} - - lcov --directory "${BUILD_DIR}" --capture --output-file ${COVERAGE_DIR}/coverage.info \ - --rc branch_coverage=1 --exclude '/usr/*' --exclude '*/tests/*' --exclude '*/_deps/*' \ - --quiet - - genhtml --output-directory ${COVERAGE_DIR}/html --title "Event Loop Code Coverage" \ - --prefix "${PWD}" --rc branch_coverage=1 --quiet ${COVERAGE_DIR}/coverage.info - - echo "Coverage report generated at: ${COVERAGE_DIR}/html/index.html" + mkdir -p "${COVERAGE_DIR}" + + OS="$(uname -s)" + if [[ "${OS}" == MINGW* ]] || [[ "${OS}" == MSYS* ]] || [[ "${OS}" == CYGWIN* ]]; then + # ── Windows: OpenCppCoverage ────────────────────────────────────────── + # Requires OpenCppCoverage on PATH. + # Install via: winget install OpenCppCoverage OR choco install opencppcoverage + if ! command -v OpenCppCoverage &>/dev/null; then + echo "WARNING: OpenCppCoverage not found on PATH. Skipping Windows coverage report." + echo "Install it with: winget install OpenCppCoverage" + else + # Support both multi-config generators (MSVC) and single-config (Ninja/MinGW). + # Multi-config puts binaries under build/tests//; single-config does not. + if [ -f "${BUILD_DIR}/tests/${BUILD_TYPE}/EventLoopTests.exe" ]; then + TEST_EXE="${BUILD_DIR}/tests/${BUILD_TYPE}/EventLoopTests.exe" + LIB_DLL="${BUILD_DIR}/${BUILD_TYPE}/EventLoop.dll" + else + TEST_EXE="${BUILD_DIR}/tests/EventLoopTests.exe" + LIB_DLL="${BUILD_DIR}/EventLoop.dll" + fi + + echo "Generating Windows coverage report with OpenCppCoverage..." + OpenCppCoverage \ + --sources "${PWD}/src" \ + --sources "${PWD}/include" \ + --modules "${LIB_DLL}" \ + --export_type "html:${COVERAGE_DIR}/html" \ + --export_type "cobertura:${COVERAGE_DIR}/coverage.xml" \ + -- "${TEST_EXE}" + + echo "Coverage report generated at: ${COVERAGE_DIR}/html/index.html" + fi + else + # ── Linux / macOS: lcov + genhtml ───────────────────────────────────── + # --ignore-errors unused: suppresses harmless warning when _deps/ does not + # exist (e.g. when gtest is installed system-wide instead of via FetchContent) + lcov --directory "${BUILD_DIR}" --capture \ + --output-file "${COVERAGE_DIR}/coverage.info" \ + --rc branch_coverage=1 \ + --exclude '/usr/*' --exclude '*/tests/*' --exclude '*/_deps/*' \ + --ignore-errors mismatch,empty,unused \ + --quiet + + genhtml --output-directory "${COVERAGE_DIR}/html" \ + --title "Event Loop Code Coverage" \ + --prefix "${PWD}" --rc branch_coverage=1 --quiet \ + "${COVERAGE_DIR}/coverage.info" + + echo "Coverage report generated at: ${COVERAGE_DIR}/html/index.html" + fi fi if [ ${TARGET} = "clean" ]; then @@ -80,4 +120,4 @@ if [ ${TARGET} = "clean" ]; then if [ -d ${PWD}/lib ]; then rm -rf lib/* fi -fi \ No newline at end of file +fi diff --git a/tests/EventLoopBasicTest.cpp b/tests/EventLoopBasicTest.cpp index f19da9b..16d7327 100644 --- a/tests/EventLoopBasicTest.cpp +++ b/tests/EventLoopBasicTest.cpp @@ -1,58 +1,239 @@ +/** + * @file EventLoopBasicTest.cpp + * @brief Comprehensive unit tests targeting 100% function coverage and maximum line coverage. + * + * Coverage strategy: + * - Test 1 (DirectCoverage): exercises classes directly without the event-loop lifecycle. + * Covers Event single-arg constructor, EventLoopException constructor, + * EventReceiver::notifyAndDequeue / recevierQueueEmpty, EventSender throw path, + * and EventCompare::operator() via a local EventSender multiset insertion. + * - Test 2 (FullLifecycleCoverage): single NON_BLOCK lifecycle that covers all reachable + * runtime paths: SetMode + blockPrimaryThread, Halt no-op, all empty-name error branches, + * TriggerEvent with timeoutMS==0, two scheduled events (forces EventCompare ordering), + * SetMode / Run no-ops while running, and post-halt queue/schedule entries that trigger + * EventManager destructor cleanup (lines 34 and 38-41). + * NON_BLOCK mode also causes m_mainLoop to be joinable at program exit, covering the + * destructor join path (line 47). + * + * Known unreachable lines (defensive/dead code – cannot be covered by black-box tests): + * - EventManager::eventLoop(): throw after while(!m_shutdown) – dead code; the loop + * exits only when m_shutdown==true so !m_shutdown is always false there. + * - EventManager::start() and eventScheduler() catch blocks – only reachable if OS-level + * thread creation fails (resource exhaustion). + * - EventManager destructor line 45 (stop()) – requires the loop to be alive at program + * exit, which conflicts with safe test teardown. + * - EventManager line 149 (direct eventLoop() BLOCK path) – the NON_BLOCK path on + * line 151 is exercised instead. Testing both modes in one binary is unsafe because + * the static EventManager singleton cannot be restarted after Halt() without joining + * the previous scheduler thread. + */ + #include -#include #include +#include +#include + +// Public API +#include #include +// Internal headers – unit tests are permitted to include these to reach +// private implementation details for thorough coverage. +#include +#include +#include + +// ============================================================================= +// Test 1: Direct class coverage (no event-loop lifecycle required) +// ============================================================================= +TEST(EventLoopTest, DirectCoverage) +{ + // ── Event: single-argument constructor ────────────────────────────────── + { + EventLoop::Event e("singleArgEvent"); + EXPECT_EQ(e.getName(), "singleArgEvent"); + EXPECT_EQ(e.getData(), nullptr); + } + + // ── EventLoopException: constructor ───────────────────────────────────── + { + EventLoopException ex("test error message"); + EXPECT_STREQ(ex.what(), "test error message"); + } + + // ── EventSender: nextEventSchedule() throw on empty schedule ──────────── + // This covers EventSender.cpp line 63 (throw path) as well as confirming + // the EventLoopException constructor is exercised through a real throw. + { + EventSender sender; + EXPECT_TRUE(sender.eventScheduleEmpty()); + EXPECT_THROW(sender.nextEventSchedule(), EventLoopException); + } + + // ── EventSender: EventCompare::operator() via two multiset insertions ─── + // Inserting a second scheduled event with an earlier wakeup time causes + // the multiset to invoke EventCompare::operator() to maintain ordering. + { + EventSender sender; + auto now = std::chrono::system_clock::now(); + EventLoop::Event* e1 = new EventLoop::Event("scheduled1"); + EventLoop::Event* e2 = new EventLoop::Event("scheduled2"); + sender.addScheduledEvent(e1, now + std::chrono::milliseconds(200)); + // Earlier timestamp forces a comparison against e1 during insertion + sender.addScheduledEvent(e2, now + std::chrono::milliseconds(50)); + EXPECT_FALSE(sender.eventScheduleEmpty()); + + // Drain the queue to avoid leaks + while (!sender.eventScheduleEmpty()) { + EventLoop::Event* evt = sender.nextEventSchedule().first; + delete evt; + sender.removeEventSchedule(); + } + EXPECT_TRUE(sender.eventScheduleEmpty()); + } + + // ── EventReceiver: recevierQueueEmpty – both branches ─────────────────── + { + EventReceiver recv; + + // Else-branch: key absent → isEmpty = true + EXPECT_TRUE(recv.recevierQueueEmpty("absent")); + + recv.enqueue("existingEvt", [](EventLoop::Event*) {}); + + // If-branch: key present, non-empty list → isEmpty = false + EXPECT_FALSE(recv.recevierQueueEmpty("existingEvt")); + } + + // ── EventReceiver: notifyAndDequeue – key found and key absent ─────────── + { + EventReceiver recv; + bool called = false; + recv.enqueue("myEvent", [&](EventLoop::Event*) { called = true; }); + + // Key found, queue non-empty: callback is invoked + EventLoop::Event e("myEvent"); + recv.notifyAndDequeue(&e); + EXPECT_TRUE(called); + + // Key absent: no-op, must not crash + EventLoop::Event unknown("unknown"); + recv.notifyAndDequeue(&unknown); + } +} + +// ============================================================================= +// Test 2: Full lifecycle coverage (NON_BLOCK mode) +// ============================================================================= TEST(EventLoopTest, FullLifecycleCoverage) { - std::atomic count{0}; - std::atomic delayedCalled{false}; + std::atomic count{0}; + std::atomic done{false}; int value = 42; - // RegisterEvent (basic) - EventLoop::RegisterEvent("TestEvent", [&](EventLoop::Event*) { - count++; - }); + // ── Error paths: empty event name guards (before Run) ──────────────────── + // Each call hits the `if (evtName.empty())` true branch, prints to stderr, + // and returns early – covering lines 49-50, 66-67, 75-76, 85-86 of EventLoop.cpp. + EventLoop::RegisterEvent("", [](EventLoop::Event*) {}); + EventLoop::DeregisterEvent(""); + EventLoop::TriggerEvent(""); + EventLoop::TriggerEvent("", static_cast(100)); // delayed overload, empty name + + // ── SetMode: covers EventLoop::SetMode() and EventManager::blockPrimaryThread() + EventLoop::SetMode(EventLoop::NON_BLOCK); // blockPrimaryThread(false) + + // ── Halt when not running: no-op guard (isRunning() == false) ──────────── + EventLoop::Halt(); - // Data + getName + // ── TriggerEvent with timeoutMS == 0: falls through to instant TriggerEvent + // (covers the `if (timeoutMS == 0)` true branch, lines 89-90 of EventLoop.cpp) + EventLoop::RegisterEvent("ZeroTimeout", [&](EventLoop::Event*) { count++; }); + EventLoop::TriggerEvent("ZeroTimeout", static_cast(0)); + + // ── Register events for the main lifecycle ──────────────────────────────── EventLoop::RegisterEvent("DataEvent", [&](EventLoop::Event* evt) { EXPECT_EQ(evt->getName(), "DataEvent"); EXPECT_EQ(*static_cast(evt->getData()), 42); count++; }); - // RegisterEvents (vector) - EventLoop::RegisterEvents({"A", "B"}, [&](EventLoop::Event*) { - count++; - }); + // RegisterEvents: common handler for multiple events + EventLoop::RegisterEvents({"EvtA", "EvtB"}, [&](EventLoop::Event*) { count++; }); - // Multiple handlers + // Multiple handlers on one event EventLoop::RegisterEvent("Multi", [&](EventLoop::Event*) { count++; }); EventLoop::RegisterEvent("Multi", [&](EventLoop::Event*) { count++; }); - // DeregisterEvent - EventLoop::RegisterEvent("X", [&](EventLoop::Event*) { - FAIL() << "DeregisterEvent failed"; + // Register then immediately deregister – handler must never fire + EventLoop::RegisterEvent("Removed", [&](EventLoop::Event*) { + FAIL() << "DeregisterEvent failed: this handler should never be called"; + }); + EventLoop::DeregisterEvent("Removed"); + + // Handler that exercises the SetMode / Run no-op paths while running + EventLoop::RegisterEvent("NoOpPaths", [&](EventLoop::Event*) { + EventLoop::SetMode(EventLoop::BLOCK); // no-op: loop is running + EventLoop::Run(); // no-op: loop is running }); - EventLoop::DeregisterEvent("X"); - // Delayed event (used to stop loop safely) + // Two scheduled events: second insertion forces EventCompare::operator() + EventLoop::RegisterEvent("Sched1", [&](EventLoop::Event*) { count++; }); + EventLoop::RegisterEvent("Sched2", [&](EventLoop::Event*) { count++; }); + + // Stop event: halts the loop when delivered EventLoop::RegisterEvent("Stop", [&](EventLoop::Event*) { - delayedCalled = true; + done = true; EventLoop::Halt(); }); - // Trigger everything - EventLoop::TriggerEvent("TestEvent"); + // ── Start the event loop (NON_BLOCK) ───────────────────────────────────── + // EventManager line 151: m_mainLoop = std::thread(&EventManager::eventLoop, this) + EventLoop::Run(); + + // ── No-op Run / SetMode while loop is running ───────────────────────────── + EventLoop::Run(); // no-op: isRunning() == true + EventLoop::SetMode(EventLoop::BLOCK); // no-op: isRunning() == true + + // ── Trigger events ──────────────────────────────────────────────────────── EventLoop::TriggerEvent("DataEvent", &value); - EventLoop::TriggerEvent("A"); - EventLoop::TriggerEvent("B"); + EventLoop::TriggerEvent("EvtA"); + EventLoop::TriggerEvent("EvtB"); EventLoop::TriggerEvent("Multi"); - EventLoop::TriggerEvent("X"); // should do nothing - EventLoop::TriggerEvent("Stop", 10); // delayed halt + EventLoop::TriggerEvent("Removed"); // deregistered → silently dropped + EventLoop::TriggerEvent("NoOpPaths"); - EventLoop::Run(); + // Two delayed events – second one exercises EventCompare via the public API + EventLoop::TriggerEvent("Sched1", static_cast(5)); + EventLoop::TriggerEvent("Sched2", static_cast(10)); - EXPECT_TRUE(delayedCalled.load()); - EXPECT_GE(count.load(), 6); -} + // Schedule the halt + EventLoop::TriggerEvent("Stop", static_cast(50)); + + // ── Wait for the Stop handler ───────────────────────────────────────────── + for (int i = 0; i < 300 && !done.load(); ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + + EXPECT_TRUE(done.load()) << "Event loop did not halt within 300 ms"; + + // Allow background threads (m_mainLoop, m_scheduler) to finish + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + + // ── Post-halt: leave entries in queues for destructor cleanup coverage ──── + // + // An instant event after Halt goes into m_sender's queue but is never + // dispatched (loop stopped). The static EventManager destructor will + // call dequeue() on it at program exit (EventManager.cpp line 34). + EventLoop::RegisterEvent("PostHalt", [](EventLoop::Event*) {}); + EventLoop::TriggerEvent("PostHalt"); + + // A long-timeout scheduled event remains in m_scheduledEvts because the + // scheduler thread has already exited. The destructor will iterate and + // delete it (EventManager.cpp lines 38-41). + EventLoop::TriggerEvent("PostHalt", static_cast(120'000)); // 2-minute timeout + + // ── Assertions ─────────────────────────────────────────────────────────── + // Minimum expected count: + // ZeroTimeout(1) + DataEvent(1) + EvtA(1) + EvtB(1) + // + Multi×2(2) + Sched1(1) + Sched2(1) = 8 + EXPECT_GE(count.load(), 8); +} \ No newline at end of file From 3ac4b71c7aa4bb4f7172d3e0e8bf07b496240d9e Mon Sep 17 00:00:00 2001 From: Vipin Kumar Date: Thu, 30 Jul 2026 02:43:37 +0530 Subject: [PATCH 9/9] Split tests into named scenarios, fix thread restart bug Broke the two giant TEST() blocks into individual TEST/TEST_F cases with proper _test_scenario names. Moved event setup/teardown into fixtures (SetUp/TearDown, SetUpTestSuite for the shared loop) instead of building everything inline in one function. Also added a real assertion that halt happens within its scheduled delay instead of just waiting up to 300ms and hoping, and made the DataEvent test actually check the handler fired. While adding a BLOCK-mode test I hit a real bug: start() would call std::thread(...) on top of a thread that was still joinable from a previous run, since Halt() signals shutdown but never joins. Fixed by joining m_mainLoop/m_scheduler at the top of start() if needed. Cleaned up the coverage-percentage comments in the test file and build.sh since that stuff belongs in the coverage report, not code. Not doing in this PR: the dead-code cleanup in eventLoop() (will be its own PR) or mocking thread-creation failures for the catch blocks (needs a factory seam in EventManager, also separate). --- build.sh | 2 - src/EventManager.cpp | 9 + tests/EventLoopBasicTest.cpp | 468 +++++++++++++++++++++++------------ 3 files changed, 322 insertions(+), 157 deletions(-) mode change 100644 => 100755 build.sh mode change 100755 => 100644 src/EventManager.cpp diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 index db7507e..140956f --- a/build.sh +++ b/build.sh @@ -66,8 +66,6 @@ if [ "${COVERAGE}" = "ON" ]; then OS="$(uname -s)" if [[ "${OS}" == MINGW* ]] || [[ "${OS}" == MSYS* ]] || [[ "${OS}" == CYGWIN* ]]; then # ── Windows: OpenCppCoverage ────────────────────────────────────────── - # Requires OpenCppCoverage on PATH. - # Install via: winget install OpenCppCoverage OR choco install opencppcoverage if ! command -v OpenCppCoverage &>/dev/null; then echo "WARNING: OpenCppCoverage not found on PATH. Skipping Windows coverage report." echo "Install it with: winget install OpenCppCoverage" diff --git a/src/EventManager.cpp b/src/EventManager.cpp old mode 100755 new mode 100644 index 8f9d22a..dea0102 --- a/src/EventManager.cpp +++ b/src/EventManager.cpp @@ -143,6 +143,15 @@ void EventManager::start() { try { + // A previous run's threads may still be joinable here (stop() signals + // shutdown but does not join). Reassigning a std::thread that already + // represents a joinable thread calls std::terminate(), so clean those + // up first to allow the loop to be safely restarted after a Halt(). + if (m_mainLoop.joinable()) + m_mainLoop.join(); + if (m_scheduler.joinable()) + m_scheduler.join(); + m_shutdown = false; m_scheduler = std::thread(&EventManager::eventScheduler, this); if (m_blockPrimary) diff --git a/tests/EventLoopBasicTest.cpp b/tests/EventLoopBasicTest.cpp index 16d7327..960f31d 100644 --- a/tests/EventLoopBasicTest.cpp +++ b/tests/EventLoopBasicTest.cpp @@ -1,37 +1,31 @@ /** * @file EventLoopBasicTest.cpp - * @brief Comprehensive unit tests targeting 100% function coverage and maximum line coverage. + * @brief Unit tests for the EventLoop public API and its internal helper classes + * (Event, EventLoopException, EventSender, EventReceiver, EventManager). * - * Coverage strategy: - * - Test 1 (DirectCoverage): exercises classes directly without the event-loop lifecycle. - * Covers Event single-arg constructor, EventLoopException constructor, - * EventReceiver::notifyAndDequeue / recevierQueueEmpty, EventSender throw path, - * and EventCompare::operator() via a local EventSender multiset insertion. - * - Test 2 (FullLifecycleCoverage): single NON_BLOCK lifecycle that covers all reachable - * runtime paths: SetMode + blockPrimaryThread, Halt no-op, all empty-name error branches, - * TriggerEvent with timeoutMS==0, two scheduled events (forces EventCompare ordering), - * SetMode / Run no-ops while running, and post-halt queue/schedule entries that trigger - * EventManager destructor cleanup (lines 34 and 38-41). - * NON_BLOCK mode also causes m_mainLoop to be joinable at program exit, covering the - * destructor join path (line 47). + * Each test case has a short comment directly above it describing what it covers. * - * Known unreachable lines (defensive/dead code – cannot be covered by black-box tests): - * - EventManager::eventLoop(): throw after while(!m_shutdown) – dead code; the loop - * exits only when m_shutdown==true so !m_shutdown is always false there. - * - EventManager::start() and eventScheduler() catch blocks – only reachable if OS-level - * thread creation fails (resource exhaustion). - * - EventManager destructor line 45 (stop()) – requires the loop to be alive at program - * exit, which conflicts with safe test teardown. - * - EventManager line 149 (direct eventLoop() BLOCK path) – the NON_BLOCK path on - * line 151 is exercised instead. Testing both modes in one binary is unsafe because - * the static EventManager singleton cannot be restarted after Halt() without joining - * the previous scheduler thread. + * IMPORTANT - test ordering: + * EventManager is instantiated as a single static/global object (see the + * `evtManager` instance in EventLoop.cpp), so every test case that touches the + * event loop lifecycle shares that one instance and is order-dependent: + * - EventLoopLifecycleTest uses SetUpTestSuite() to start the loop once in + * NON_BLOCK mode; its test cases run in declaration order and the loop is + * halted by the last two of them. + * - blockMode_haltFromAnotherThread_test_scenario then runs the BLOCK-mode + * path and halts the loop again before returning. + * - staticDestructor_stopsRunningLoopAtProcessExit_test_scenario deliberately + * never halts the loop, so it must remain the LAST test in this file. */ #include #include #include +#include +#include +#include #include +#include // Public API #include @@ -43,197 +37,361 @@ #include #include -// ============================================================================= -// Test 1: Direct class coverage (no event-loop lifecycle required) -// ============================================================================= -TEST(EventLoopTest, DirectCoverage) +namespace { + +// Polls `predicate` at 1ms intervals for up to `timeoutMs`, returning true as +// soon as it succeeds. Used instead of a single fixed sleep so tests fail fast +// on success and only pay the full timeout when something is actually wrong. +bool waitUntil(const std::function& predicate, int timeoutMs) { - // ── Event: single-argument constructor ────────────────────────────────── + for (int waited = 0; waited < timeoutMs; ++waited) { - EventLoop::Event e("singleArgEvent"); - EXPECT_EQ(e.getName(), "singleArgEvent"); - EXPECT_EQ(e.getData(), nullptr); + if (predicate()) + return true; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); } + return predicate(); +} - // ── EventLoopException: constructor ───────────────────────────────────── +} + +// Covers EventLoop::Event's single-argument constructor. +TEST(EventTest, singleArgConstructor_test_scenario) +{ + EventLoop::Event e("singleArgEvent"); + EXPECT_EQ(e.getName(), "singleArgEvent"); + EXPECT_EQ(e.getData(), nullptr); +} + +// Covers EventLoopException's constructor and what(). +TEST(EventLoopExceptionTest, constructor_test_scenario) +{ + EventLoopException ex("test error message"); + EXPECT_STREQ(ex.what(), "test error message"); +} + +// Covers EventSender::nextEventSchedule() throwing when the schedule is empty. +TEST(EventSenderTest, nextEventSchedule_throwsOnEmptySchedule_test_scenario) +{ + EventSender sender; + EXPECT_TRUE(sender.eventScheduleEmpty()); + EXPECT_THROW(sender.nextEventSchedule(), EventLoopException); +} + +// Fixture owning the two scheduled Event objects used by the ordering test below. +class EventSenderSchedulingTest : public ::testing::Test +{ +protected: + void SetUp() override { - EventLoopException ex("test error message"); - EXPECT_STREQ(ex.what(), "test error message"); + m_event1 = new EventLoop::Event("scheduled1"); + m_event2 = new EventLoop::Event("scheduled2"); } - // ── EventSender: nextEventSchedule() throw on empty schedule ──────────── - // This covers EventSender.cpp line 63 (throw path) as well as confirming - // the EventLoopException constructor is exercised through a real throw. + void TearDown() override { - EventSender sender; - EXPECT_TRUE(sender.eventScheduleEmpty()); - EXPECT_THROW(sender.nextEventSchedule(), EventLoopException); + // Safety net only. The test itself takes ownership back from the + // sender, deletes both events, and nulls these members out. They are + // only still non-null here if the test body failed/returned early. + delete m_event1; + delete m_event2; } - // ── EventSender: EventCompare::operator() via two multiset insertions ─── - // Inserting a second scheduled event with an earlier wakeup time causes - // the multiset to invoke EventCompare::operator() to maintain ordering. + EventLoop::Event* m_event1 = nullptr; + EventLoop::Event* m_event2 = nullptr; +}; + +// Covers EventCompare::operator(), invoked when the multiset backing the +// schedule compares two entries during insertion. +TEST_F(EventSenderSchedulingTest, addScheduledEvent_invokesEventCompare_test_scenario) +{ + EventSender sender; + auto now = std::chrono::system_clock::now(); + + sender.addScheduledEvent(m_event1, now + std::chrono::milliseconds(200)); + // Earlier timestamp forces a comparison against m_event1 during insertion. + sender.addScheduledEvent(m_event2, now + std::chrono::milliseconds(50)); + EXPECT_FALSE(sender.eventScheduleEmpty()); + + while (!sender.eventScheduleEmpty()) { - EventSender sender; - auto now = std::chrono::system_clock::now(); - EventLoop::Event* e1 = new EventLoop::Event("scheduled1"); - EventLoop::Event* e2 = new EventLoop::Event("scheduled2"); - sender.addScheduledEvent(e1, now + std::chrono::milliseconds(200)); - // Earlier timestamp forces a comparison against e1 during insertion - sender.addScheduledEvent(e2, now + std::chrono::milliseconds(50)); - EXPECT_FALSE(sender.eventScheduleEmpty()); - - // Drain the queue to avoid leaks - while (!sender.eventScheduleEmpty()) { - EventLoop::Event* evt = sender.nextEventSchedule().first; - delete evt; - sender.removeEventSchedule(); - } - EXPECT_TRUE(sender.eventScheduleEmpty()); + EventLoop::Event* evt = sender.nextEventSchedule().first; + delete evt; + sender.removeEventSchedule(); } + m_event1 = nullptr; + m_event2 = nullptr; + EXPECT_TRUE(sender.eventScheduleEmpty()); +} - // ── EventReceiver: recevierQueueEmpty – both branches ─────────────────── - { - EventReceiver recv; +// Covers EventReceiver::recevierQueueEmpty() for both the absent-key branch +// and the present-key branch. +TEST(EventReceiverTest, recevierQueueEmpty_test_scenario) +{ + EventReceiver recv; - // Else-branch: key absent → isEmpty = true - EXPECT_TRUE(recv.recevierQueueEmpty("absent")); + EXPECT_TRUE(recv.recevierQueueEmpty("absent")); - recv.enqueue("existingEvt", [](EventLoop::Event*) {}); + recv.enqueue("existingEvt", [](EventLoop::Event*) {}); + EXPECT_FALSE(recv.recevierQueueEmpty("existingEvt")); +} - // If-branch: key present, non-empty list → isEmpty = false - EXPECT_FALSE(recv.recevierQueueEmpty("existingEvt")); - } +// Covers EventReceiver::notifyAndDequeue() for a registered key (callback +// fires) and an unregistered key (silently ignored, must not crash). +TEST(EventReceiverTest, notifyAndDequeue_test_scenario) +{ + EventReceiver recv; + bool called = false; + recv.enqueue("myEvent", [&](EventLoop::Event*) { called = true; }); - // ── EventReceiver: notifyAndDequeue – key found and key absent ─────────── - { - EventReceiver recv; - bool called = false; - recv.enqueue("myEvent", [&](EventLoop::Event*) { called = true; }); - - // Key found, queue non-empty: callback is invoked - EventLoop::Event e("myEvent"); - recv.notifyAndDequeue(&e); - EXPECT_TRUE(called); - - // Key absent: no-op, must not crash - EventLoop::Event unknown("unknown"); - recv.notifyAndDequeue(&unknown); - } + EventLoop::Event e("myEvent"); + recv.notifyAndDequeue(&e); + EXPECT_TRUE(called); + + EventLoop::Event unknown("unknown"); + recv.notifyAndDequeue(&unknown); } -// ============================================================================= -// Test 2: Full lifecycle coverage (NON_BLOCK mode) -// ============================================================================= -TEST(EventLoopTest, FullLifecycleCoverage) +// Fixture for the NON_BLOCK event-loop lifecycle. The loop is started once +// for the whole suite since EventManager is a single global instance; each +// TEST_F below then registers/triggers its own uniquely-named event(s) and +// polls for the result, so the suite's tests stay independent of each other +// even though they share the running loop. +class EventLoopLifecycleTest : public ::testing::Test { - std::atomic count{0}; - std::atomic done{false}; - int value = 42; +protected: + static void SetUpTestSuite() + { + EventLoop::SetMode(EventLoop::NON_BLOCK); + EventLoop::Run(); + } +}; - // ── Error paths: empty event name guards (before Run) ──────────────────── - // Each call hits the `if (evtName.empty())` true branch, prints to stderr, - // and returns early – covering lines 49-50, 66-67, 75-76, 85-86 of EventLoop.cpp. +// Covers the `if (evtName.empty())` guard on every public entry point that +// has one: each call must return early without registering/triggering anything. +TEST_F(EventLoopLifecycleTest, emptyEventNameGuards_returnEarlyWithoutAction_test_scenario) +{ EventLoop::RegisterEvent("", [](EventLoop::Event*) {}); EventLoop::DeregisterEvent(""); EventLoop::TriggerEvent(""); - EventLoop::TriggerEvent("", static_cast(100)); // delayed overload, empty name + EventLoop::TriggerEvent("", static_cast(100)); - // ── SetMode: covers EventLoop::SetMode() and EventManager::blockPrimaryThread() - EventLoop::SetMode(EventLoop::NON_BLOCK); // blockPrimaryThread(false) + // Give the loop a moment to prove it's still alive and unaffected. + std::this_thread::sleep_for(std::chrono::milliseconds(20)); +} - // ── Halt when not running: no-op guard (isRunning() == false) ──────────── - EventLoop::Halt(); +// Covers TriggerEvent's `if (timeoutMS == 0)` branch, which falls through to +// the instant-trigger path instead of scheduling anything. +TEST_F(EventLoopLifecycleTest, zeroTimeoutTrigger_fallsThroughToInstantTrigger_test_scenario) +{ + std::atomic handled{false}; + EventLoop::RegisterEvent("ZeroTimeout", [&](EventLoop::Event*) { handled = true; }); - // ── TriggerEvent with timeoutMS == 0: falls through to instant TriggerEvent - // (covers the `if (timeoutMS == 0)` true branch, lines 89-90 of EventLoop.cpp) - EventLoop::RegisterEvent("ZeroTimeout", [&](EventLoop::Event*) { count++; }); EventLoop::TriggerEvent("ZeroTimeout", static_cast(0)); - // ── Register events for the main lifecycle ──────────────────────────────── + EXPECT_TRUE(waitUntil([&] { return handled.load(); }, 200)) + << "ZeroTimeout handler was not invoked"; +} + +// Covers TriggerEvent(evtName, data): the data pointer passed at the trigger +// site must be the exact one the registered handler receives. +TEST_F(EventLoopLifecycleTest, dataEvent_deliversAssociatedData_test_scenario) +{ + std::atomic handled{false}; + int value = 42; + EventLoop::RegisterEvent("DataEvent", [&](EventLoop::Event* evt) { EXPECT_EQ(evt->getName(), "DataEvent"); EXPECT_EQ(*static_cast(evt->getData()), 42); - count++; + handled = true; }); - // RegisterEvents: common handler for multiple events + EventLoop::TriggerEvent("DataEvent", &value); + + EXPECT_TRUE(waitUntil([&] { return handled.load(); }, 200)) + << "DataEvent handler was not invoked"; +} + +// Covers RegisterEvents(): one handler shared across multiple event names, +// invoked once per name triggered. +TEST_F(EventLoopLifecycleTest, registerEvents_sharedHandlerAcrossMultipleNames_test_scenario) +{ + std::atomic count{0}; EventLoop::RegisterEvents({"EvtA", "EvtB"}, [&](EventLoop::Event*) { count++; }); - // Multiple handlers on one event + EventLoop::TriggerEvent("EvtA"); + EventLoop::TriggerEvent("EvtB"); + + ASSERT_TRUE(waitUntil([&] { return count.load() >= 2; }, 200)) + << "Only " << count.load() << "/2 handlers fired"; + EXPECT_EQ(count.load(), 2); +} + +// Covers registering multiple independent handlers for the same event name: +// triggering it once must invoke all of them. +TEST_F(EventLoopLifecycleTest, multipleHandlers_bothInvokedForSameEvent_test_scenario) +{ + std::atomic count{0}; EventLoop::RegisterEvent("Multi", [&](EventLoop::Event*) { count++; }); EventLoop::RegisterEvent("Multi", [&](EventLoop::Event*) { count++; }); - // Register then immediately deregister – handler must never fire + EventLoop::TriggerEvent("Multi"); + + ASSERT_TRUE(waitUntil([&] { return count.load() >= 2; }, 200)) + << "Only " << count.load() << "/2 handlers fired"; + EXPECT_EQ(count.load(), 2); +} + +// Covers DeregisterEvent(): a handler removed before the event is triggered +// must never be invoked. +TEST_F(EventLoopLifecycleTest, deregisterEvent_preventsHandlerInvocation_test_scenario) +{ EventLoop::RegisterEvent("Removed", [&](EventLoop::Event*) { FAIL() << "DeregisterEvent failed: this handler should never be called"; }); EventLoop::DeregisterEvent("Removed"); - // Handler that exercises the SetMode / Run no-op paths while running - EventLoop::RegisterEvent("NoOpPaths", [&](EventLoop::Event*) { - EventLoop::SetMode(EventLoop::BLOCK); // no-op: loop is running - EventLoop::Run(); // no-op: loop is running - }); + EventLoop::TriggerEvent("Removed"); - // Two scheduled events: second insertion forces EventCompare::operator() - EventLoop::RegisterEvent("Sched1", [&](EventLoop::Event*) { count++; }); - EventLoop::RegisterEvent("Sched2", [&](EventLoop::Event*) { count++; }); + // Nothing to poll for: success means the FAIL() above never runs. Give the + // loop a moment to have processed (and silently dropped) the event. + std::this_thread::sleep_for(std::chrono::milliseconds(50)); +} - // Stop event: halts the loop when delivered - EventLoop::RegisterEvent("Stop", [&](EventLoop::Event*) { - done = true; - EventLoop::Halt(); - }); +// Covers SetMode()/Run() being no-ops once the loop is already running: they +// must neither crash nor disrupt event delivery. +TEST_F(EventLoopLifecycleTest, noOpGuards_whileLoopIsRunning_test_scenario) +{ + std::atomic before{false}, after{false}; + EventLoop::RegisterEvent("BeforeNoOp", [&](EventLoop::Event*) { before = true; }); + EventLoop::RegisterEvent("AfterNoOp", [&](EventLoop::Event*) { after = true; }); - // ── Start the event loop (NON_BLOCK) ───────────────────────────────────── - // EventManager line 151: m_mainLoop = std::thread(&EventManager::eventLoop, this) + EventLoop::TriggerEvent("BeforeNoOp"); + ASSERT_TRUE(waitUntil([&] { return before.load(); }, 200)); + + // Both must be no-ops here (isRunning() == true) - if they weren't, this + // would attempt to relaunch the background threads mid-flight. + EventLoop::SetMode(EventLoop::BLOCK); EventLoop::Run(); - // ── No-op Run / SetMode while loop is running ───────────────────────────── - EventLoop::Run(); // no-op: isRunning() == true - EventLoop::SetMode(EventLoop::BLOCK); // no-op: isRunning() == true + EventLoop::TriggerEvent("AfterNoOp"); + EXPECT_TRUE(waitUntil([&] { return after.load(); }, 200)) + << "Loop stopped processing events after redundant SetMode()/Run() calls"; +} - // ── Trigger events ──────────────────────────────────────────────────────── - EventLoop::TriggerEvent("DataEvent", &value); - EventLoop::TriggerEvent("EvtA"); - EventLoop::TriggerEvent("EvtB"); - EventLoop::TriggerEvent("Multi"); - EventLoop::TriggerEvent("Removed"); // deregistered → silently dropped - EventLoop::TriggerEvent("NoOpPaths"); +// Covers EventCompare::operator() through the public API: two scheduled +// events must fire in wakeup-time order regardless of trigger order. +TEST_F(EventLoopLifecycleTest, scheduledEvents_deliveredInWakeupOrder_test_scenario) +{ + std::mutex orderMutex; + std::vector order; + + EventLoop::RegisterEvent("Sched1", [&](EventLoop::Event*) { + std::lock_guard lock(orderMutex); + order.push_back("Sched1"); + }); + EventLoop::RegisterEvent("Sched2", [&](EventLoop::Event*) { + std::lock_guard lock(orderMutex); + order.push_back("Sched2"); + }); - // Two delayed events – second one exercises EventCompare via the public API - EventLoop::TriggerEvent("Sched1", static_cast(5)); + // Sched1 has the longer delay; Sched2's earlier wakeup forces the + // schedule to reorder them, so Sched2 must be delivered first. + EventLoop::TriggerEvent("Sched1", static_cast(60)); EventLoop::TriggerEvent("Sched2", static_cast(10)); - // Schedule the halt - EventLoop::TriggerEvent("Stop", static_cast(50)); + ASSERT_TRUE(waitUntil([&] { + std::lock_guard lock(orderMutex); + return order.size() == 2; + }, 300)); - // ── Wait for the Stop handler ───────────────────────────────────────────── - for (int i = 0; i < 300 && !done.load(); ++i) - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + std::lock_guard lock(orderMutex); + ASSERT_EQ(order.size(), 2u); + EXPECT_EQ(order[0], "Sched2"); + EXPECT_EQ(order[1], "Sched1"); +} - EXPECT_TRUE(done.load()) << "Event loop did not halt within 300 ms"; +// Covers halting the loop from within a handler, and that the halt happens +// promptly relative to its scheduled delay, not just at some point before a +// long timeout. +TEST_F(EventLoopLifecycleTest, haltTiming_stopsLoopPromptly_test_scenario) +{ + std::atomic done{false}; + EventLoop::RegisterEvent("Stop", [&](EventLoop::Event*) { + done = true; + EventLoop::Halt(); + }); + + constexpr long long delayMs = 50; + auto start = std::chrono::steady_clock::now(); + EventLoop::TriggerEvent("Stop", static_cast(delayMs)); - // Allow background threads (m_mainLoop, m_scheduler) to finish + ASSERT_TRUE(waitUntil([&] { return done.load(); }, 500)) + << "Event loop did not halt within 500 ms"; + + auto elapsedMs = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start).count(); + + EXPECT_GE(elapsedMs, delayMs) + << "Stop fired earlier than its scheduled " << delayMs << " ms delay"; + EXPECT_LT(elapsedMs, delayMs + 150) + << "Stop took " << elapsedMs << " ms, well beyond its " << delayMs << " ms delay"; + + // Allow the background threads (m_mainLoop, m_scheduler) to finish before + // the next test in this suite touches EventManager again. std::this_thread::sleep_for(std::chrono::milliseconds(30)); +} - // ── Post-halt: leave entries in queues for destructor cleanup coverage ──── - // - // An instant event after Halt goes into m_sender's queue but is never - // dispatched (loop stopped). The static EventManager destructor will - // call dequeue() on it at program exit (EventManager.cpp line 34). +// Runs immediately after the loop has been halted above. Leaves an instant +// event (lands in m_sender's queue but is never dispatched) and a long-delay +// scheduled event (left in m_scheduledEvts since the scheduler thread has +// already exited) so the EventManager destructor's unconditional queue/ +// schedule cleanup loops have something to clean up when the process exits. +TEST_F(EventLoopLifecycleTest, postHalt_leavesEntriesForDestructorCleanup_test_scenario) +{ EventLoop::RegisterEvent("PostHalt", [](EventLoop::Event*) {}); EventLoop::TriggerEvent("PostHalt"); + EventLoop::TriggerEvent("PostHalt", static_cast(120000)); +} - // A long-timeout scheduled event remains in m_scheduledEvts because the - // scheduler thread has already exited. The destructor will iterate and - // delete it (EventManager.cpp lines 38-41). - EventLoop::TriggerEvent("PostHalt", static_cast(120'000)); // 2-minute timeout +// Covers the BLOCK-mode branch of EventManager::start(): eventLoop() runs +// synchronously on the calling thread, so Run() only returns once Halt() is +// called from elsewhere. +TEST(EventLoopTest, blockMode_haltFromAnotherThread_test_scenario) +{ + std::atomic eventHandled{false}; + + EventLoop::SetMode(EventLoop::BLOCK); + EventLoop::RegisterEvent("BlockModeEvent", [&](EventLoop::Event*) { eventHandled = true; }); + + std::thread stopper([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + EventLoop::TriggerEvent("BlockModeEvent"); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + EventLoop::Halt(); + }); + + EventLoop::Run(); // blocks here until the stopper thread calls Halt() + stopper.join(); - // ── Assertions ─────────────────────────────────────────────────────────── - // Minimum expected count: - // ZeroTimeout(1) + DataEvent(1) + EvtA(1) + EvtB(1) - // + Multi×2(2) + Sched1(1) + Sched2(1) = 8 - EXPECT_GE(count.load(), 8); + EXPECT_TRUE(eventHandled.load()); +} + +// Covers the EventManager destructor path taken when the loop is still +// running at process exit: m_shutdown is false, so the destructor itself +// calls stop() and joins the background thread. +// +// This test intentionally never calls EventLoop::Halt(), leaving the static +// EventManager instance running until the test binary exits. It must remain +// the LAST test in this file - see the ordering note at the top. +TEST(EventLoopTest, staticDestructor_stopsRunningLoopAtProcessExit_test_scenario) +{ + EventLoop::SetMode(EventLoop::NON_BLOCK); + EventLoop::RegisterEvent("NeverHalted", [](EventLoop::Event*) {}); + EventLoop::Run(); + EventLoop::TriggerEvent("NeverHalted"); + + // Give the background thread a moment to start looping before the + // process exits and the static destructor takes over cleanup. + std::this_thread::sleep_for(std::chrono::milliseconds(30)); } \ No newline at end of file