diff --git a/score/launch_manager/src/daemon/src/process_group_manager/BUILD b/score/launch_manager/src/daemon/src/process_group_manager/BUILD index 4f1ba54df..407278ed0 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/BUILD +++ b/score/launch_manager/src/daemon/src/process_group_manager/BUILD @@ -45,6 +45,19 @@ cc_library( visibility = ["//score:__subpackages__"], ) +cc_library( + name = "ialive_monitor_thread_mock", + testonly = True, + hdrs = ["alive_monitor_thread_mock.hpp"], + include_prefix = "score/mw/launch_manager/process_group_manager", + strip_include_prefix = "/score/launch_manager/src/daemon/src/process_group_manager", + visibility = ["//score:__subpackages__"], + deps = [ + ":ialive_monitor_thread", + "@googletest//:gtest", + ], +) + cc_library( name = "alive_monitor_thread", srcs = ["alive_monitor_thread.cpp"], diff --git a/score/launch_manager/src/daemon/src/process_group_manager/alive_monitor_thread_mock.hpp b/score/launch_manager/src/daemon/src/process_group_manager/alive_monitor_thread_mock.hpp new file mode 100644 index 000000000..d8f28ee61 --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/alive_monitor_thread_mock.hpp @@ -0,0 +1,41 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_LCM_IALIVE_MONITOR_THREAD_MOCK_HPP_INCLUDED +#define SCORE_LCM_IALIVE_MONITOR_THREAD_MOCK_HPP_INCLUDED + +#include "score/mw/launch_manager/process_group_manager/ialive_monitor_thread.hpp" + +#include + +namespace score +{ +namespace lcm +{ +namespace internal +{ + +/// @brief Reusable gmock mock for IAliveMonitorThread, for use by tests of components that own an alive monitor +/// thread. +class MockAliveMonitorThread : public IAliveMonitorThread +{ + public: + MOCK_METHOD(bool, start, (), (override)); + MOCK_METHOD(void, stop, (), (override)); +}; + +} // namespace internal +} // namespace lcm +} // namespace score + +#endif // SCORE_LCM_IALIVE_MONITOR_THREAD_MOCK_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/BUILD b/score/launch_manager/src/daemon/src/process_group_manager/details/BUILD index 65bfaf79c..a25d42e3c 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/BUILD +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/BUILD @@ -184,7 +184,9 @@ cc_library( hdrs = ["graph.hpp"], include_prefix = "score/mw/launch_manager/process_group_manager/details", strip_include_prefix = "/score/launch_manager/src/daemon/src/process_group_manager/details", - visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__pkg__"], + visibility = [ + "//score/launch_manager/src/daemon/src/process_group_manager:__pkg__", + ], deps = [ ":component_event_queue", ":component_of", @@ -323,6 +325,21 @@ lm_cc_test( ], ) +lm_cc_test( + name = "process_group_manager_UT", + srcs = ["process_group_manager_UT.cpp"], + deps = [ + "//score/launch_manager/src/daemon/src/configuration:config", + "//score/launch_manager/src/daemon/src/process_group_manager", + "//score/launch_manager/src/daemon/src/process_group_manager:ialive_monitor_thread_mock", + "//score/launch_manager/src/daemon/src/recovery_client:recovery_client_mock", + "//score/launch_manager/src/daemon/src/supervision_control_client:isupervision_control_notifier", + "//score/launch_manager/src/daemon/src/supervision_control_client:mock_supervision_control_notifier", + "//score/launch_manager/src/daemon/src/watchdog:i_watchdog_if_mock", + "@googletest//:gtest_main", + ], +) + cc_library( name = "process_launcher", srcs = ["process_launcher.cpp"], diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_group_manager_UT.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_group_manager_UT.cpp new file mode 100644 index 000000000..2f3278a6e --- /dev/null +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_group_manager_UT.cpp @@ -0,0 +1,258 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/mw/launch_manager/process_group_manager/process_group_manager.hpp" + +#include "score/mw/launch_manager/process_group_manager/alive_monitor_thread_mock.hpp" +#include "score/mw/launch_manager/recovery_client/irecovery_client_mock.h" +#include "score/mw/launch_manager/supervision_control_client/mock_supervision_control_notifier.hpp" +#include "score/mw/launch_manager/watchdog/IWatchdogIfMock.hpp" + +#include +#include + +#include +#include +#include +#include +#include + +#include "score/mw/launch_manager/configuration/config.hpp" + +using namespace testing; + +namespace score::lcm::internal +{ +namespace +{ + +using score::lcm::MockRecoveryClient; +using score::lcm::MockSupervisionControlNotifier; +using score::lcm::watchdog::MockWatchdogIf; + +using score::mw::launch_manager::configuration::AliveSupervisionConfig; +using score::mw::launch_manager::configuration::ApplicationType; +using score::mw::launch_manager::configuration::ComponentConfig; +using score::mw::launch_manager::configuration::Config; +using score::mw::launch_manager::configuration::ConfigBuilder; +using score::mw::launch_manager::configuration::FallbackRunTargetConfig; +using score::mw::launch_manager::configuration::ProcessState; +using score::mw::launch_manager::configuration::ReadyCondition; +using score::mw::launch_manager::configuration::RunTargetConfig; +using score::mw::launch_manager::configuration::WatchdogConfig; + +Config makeMinimalConfig() +{ + ComponentConfig component; + component.name = "comp_a"; + component.description = "Component A"; + component.component_properties.binary_name = "true"; + component.component_properties.application_profile.application_type = ApplicationType::Native; + component.component_properties.application_profile.is_self_terminating = true; + component.component_properties.ready_condition = ReadyCondition{ProcessState::Running}; + component.deployment_config.ready_timeout_ms = 500U; + component.deployment_config.shutdown_timeout_ms = 500U; + component.deployment_config.bin_dir = "/bin"; + component.deployment_config.working_dir = "/workspaces/lifecycle"; + component.deployment_config.sandbox.uid = 1000U; + component.deployment_config.sandbox.gid = 1000U; + component.deployment_config.sandbox.scheduling_policy = SCHED_OTHER; + component.deployment_config.sandbox.scheduling_priority = 0; + + RunTargetConfig startup; + startup.name = "Startup"; + startup.description = "Initial run target"; + startup.depends_on = {"comp_a"}; + startup.transition_timeout_ms = 5000U; + startup.recovery_action.run_target = "fallback_run_target"; + + FallbackRunTargetConfig fallback; + fallback.description = "Safe state"; + fallback.transition_timeout_ms = 1500U; + + AliveSupervisionConfig alive; + alive.evaluation_cycle_ms = 500U; + + WatchdogConfig watchdog; + watchdog.device_file_path = "/dev/watchdog0"; + watchdog.max_timeout_ms = 5000U; + watchdog.deactivate_on_shutdown = true; + watchdog.require_magic_close = false; + + std::vector components; + components.push_back(std::move(component)); + + std::vector run_targets; + run_targets.push_back(std::move(startup)); + + return ConfigBuilder{} + .setComponents(std::move(components)) + .setRunTargets(std::move(run_targets)) + .setInitialRunTarget("Startup") + .setFallbackRunTarget(std::move(fallback)) + .setAliveSupervision(alive) + .setWatchdog(watchdog) + .build(); +} + +class ProcessGroupManagerWatchdogTest : public Test +{ + protected: + void expectNormalStartup() + { + EXPECT_CALL(*alive_monitor_thread_, start()).WillOnce(Return(true)); + EXPECT_CALL(*watchdog_, init(_, _)).WillOnce(Return(true)); + EXPECT_CALL(*watchdog_, enable()).WillOnce(Return(true)); + } + + void SetUp() override + { + RecordProperty("TestType", "unit-test"); + RecordProperty("DerivationTechnique", "explorative-testing"); + + auto alive_monitor_thread = std::make_unique>(); + alive_monitor_thread_ = alive_monitor_thread.get(); + ON_CALL(*alive_monitor_thread_, start()).WillByDefault(Return(true)); + + auto recovery_client = std::make_shared>(); + recovery_client_ = recovery_client.get(); + // Capture the callback the ProcessGroupManager registers so tests can inject recovery + // requests as if they came from the Alive Monitor. + ON_CALL(*recovery_client_, setRecoveryRequestCallback(_)).WillByDefault(SaveArg<0>(&recovery_callback_)); + ON_CALL(*recovery_client_, sendRecoveryRequest(_)).WillByDefault(Return(true)); + + auto supervision_control_notifier = std::make_unique>(); + supervision_control_notifier_ = supervision_control_notifier.get(); + ON_CALL(*supervision_control_notifier_, constructReceiver()) + .WillByDefault(Return(ByMove(std::unique_ptr{}))); + ON_CALL(*supervision_control_notifier_, reportActivation(_, _)).WillByDefault(Return(true)); + ON_CALL(*supervision_control_notifier_, reportDeactivation(_, _)).WillByDefault(Return(true)); + + auto watchdog = std::make_unique>(); + watchdog_ = watchdog.get(); + + process_group_manager_ = std::make_unique( + std::move(alive_monitor_thread), + std::move(recovery_client), + std::move(supervision_control_notifier), + std::move(watchdog)); + } + + void TearDown() override + { + process_group_manager_->deinitialize(); + } + + MockAliveMonitorThread* alive_monitor_thread_{}; + MockRecoveryClient* recovery_client_{}; + score::lcm::IRecoveryClient::RecoveryRequestCallback recovery_callback_{}; + MockSupervisionControlNotifier* supervision_control_notifier_{}; + MockWatchdogIf* watchdog_{}; + std::unique_ptr process_group_manager_; +}; + +TEST_F(ProcessGroupManagerWatchdogTest, GivenMinimalConfig_ExpectWatchdogMethodsCalledInSequence_WhenInitializeCalled) +{ + // Given + const auto config = makeMinimalConfig(); + + // Expected + InSequence sequence; + expectNormalStartup(); + EXPECT_CALL(*watchdog_, disable()).Times(1); + EXPECT_CALL(*alive_monitor_thread_, stop()).Times(1); + + // When + auto initialize_result = process_group_manager_->initialize(config); + + // Then + EXPECT_TRUE(initialize_result); +} + +TEST_F(ProcessGroupManagerWatchdogTest, GivenMinimalConfig_ExpectWatchdogServicePerCycle_WhenRunCalled) +{ + // Given + const auto config = makeMinimalConfig(); + + // Expected + expectNormalStartup(); + // Call cancel() to exit the run() loop after at least one cycle of serviceWatchdog() is called + EXPECT_CALL(*watchdog_, serviceWatchdog()).Times(AtLeast(1)).WillRepeatedly([this]() { + process_group_manager_->cancel(); + }); + // Called in deinitialize() after run() returns + EXPECT_CALL(*watchdog_, disable()).Times(1); + EXPECT_CALL(*alive_monitor_thread_, stop()).Times(1); + + // When + ASSERT_TRUE(process_group_manager_->initialize(config)); + auto run_result = process_group_manager_->run(); + + // Then + EXPECT_TRUE(run_result); +} + +TEST_F(ProcessGroupManagerWatchdogTest, GivenMinimalConfig_ExpectWatchdogFired_WhenRecoveryClientOverflowsDuringRunCall) +{ + // Given + const auto config = makeMinimalConfig(); + // More than the component event queue can hold (capacity == number of OS processes * 3; makeMinimalConfig has a + // single component) + constexpr int kNumRecoveryRequests = 16; + + // Expected + expectNormalStartup(); + EXPECT_CALL(*watchdog_, fireWatchdogReaction()).Times(AtLeast(1)); + EXPECT_CALL(*watchdog_, serviceWatchdog()).Times(AtLeast(1)).WillRepeatedly([this]() { + process_group_manager_->cancel(); + }); + EXPECT_CALL(*watchdog_, disable()).Times(1); + EXPECT_CALL(*alive_monitor_thread_, stop()).Times(1); + + // When + ASSERT_TRUE(process_group_manager_->initialize(config)); + + // Deliver more recovery requests than the component event queue can hold, + // forcing the queue to drop events and latch its sticky overflow flag. + // run() observes the overflow and fires the watchdog reaction. + ASSERT_TRUE(recovery_callback_); + for (int i = 0; i < kNumRecoveryRequests; ++i) + { + recovery_callback_(score::lcm::IdentifierHash{"overflow_probe"}); + } + + auto run_result = process_group_manager_->run(); + + // Then + EXPECT_TRUE(run_result); +} + +TEST_F(ProcessGroupManagerWatchdogTest, GivenMinimalConfig_ExpectWatchdogDisabled_WhenDeinitializeCalled) +{ + // Given + const auto config = makeMinimalConfig(); + + // Expected + expectNormalStartup(); + // We are explicitly calling deinitialize() in this test for readability, + // so disable() and stop() are expected to be called twice: once in deinitialize() and once in TearDown(). + EXPECT_CALL(*watchdog_, disable()).Times(2); + EXPECT_CALL(*alive_monitor_thread_, stop()).Times(2); + + // When + ASSERT_TRUE(process_group_manager_->initialize(config)); + process_group_manager_->deinitialize(); +} + +} // namespace +} // namespace score::lcm::internal diff --git a/score/launch_manager/src/daemon/src/recovery_client/BUILD b/score/launch_manager/src/daemon/src/recovery_client/BUILD index ed4574813..2066a6103 100644 --- a/score/launch_manager/src/daemon/src/recovery_client/BUILD +++ b/score/launch_manager/src/daemon/src/recovery_client/BUILD @@ -32,6 +32,19 @@ cc_library( ], ) +cc_library( + name = "recovery_client_mock", + testonly = True, + hdrs = ["irecovery_client_mock.h"], + include_prefix = "score/mw/launch_manager/recovery_client", + strip_include_prefix = "/score/launch_manager/src/daemon/src/recovery_client", + visibility = ["//score:__subpackages__"], + deps = [ + ":recovery_client", + "@googletest//:gtest", + ], +) + lm_cc_test( name = "recovery_client_UT", srcs = ["recovery_client_UT.cpp"], diff --git a/score/launch_manager/src/daemon/src/recovery_client/irecovery_client_mock.h b/score/launch_manager/src/daemon/src/recovery_client/irecovery_client_mock.h new file mode 100644 index 000000000..7c0a7f4b3 --- /dev/null +++ b/score/launch_manager/src/daemon/src/recovery_client/irecovery_client_mock.h @@ -0,0 +1,41 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_LCM_IRECOVERYCLIENT_MOCK_H_ +#define SCORE_LCM_IRECOVERYCLIENT_MOCK_H_ + +#include "score/mw/launch_manager/recovery_client/irecovery_client.h" + +#include + +namespace score +{ +namespace lcm +{ + +/// @brief Reusable gmock mock for IRecoveryClient, for use by tests of components that either request recovery +/// (e.g. Alive Monitor) or process recovery requests (e.g. ProcessGroupManager). +class MockRecoveryClient : public IRecoveryClient +{ + public: + MOCK_METHOD(void, setRecoveryRequestCallback, (RecoveryRequestCallback callback), (noexcept, override)); + MOCK_METHOD( + bool, + sendRecoveryRequest, + (const score::lcm::IdentifierHash& process_group_identifier), + (noexcept, override)); +}; + +} // namespace lcm +} // namespace score + +#endif // SCORE_LCM_IRECOVERYCLIENT_MOCK_H_ diff --git a/score/launch_manager/src/daemon/src/supervision_control_client/BUILD b/score/launch_manager/src/daemon/src/supervision_control_client/BUILD index 26c77445a..1a5e5a213 100644 --- a/score/launch_manager/src/daemon/src/supervision_control_client/BUILD +++ b/score/launch_manager/src/daemon/src/supervision_control_client/BUILD @@ -74,6 +74,19 @@ cc_library( ], ) +cc_library( + name = "mock_supervision_control_notifier", + testonly = True, + hdrs = ["mock_supervision_control_notifier.hpp"], + include_prefix = "score/mw/launch_manager/supervision_control_client", + strip_include_prefix = "/score/launch_manager/src/daemon/src/supervision_control_client", + visibility = ["//score:__subpackages__"], + deps = [ + ":isupervision_control_notifier", + "@googletest//:gtest_main", + ], +) + cc_library( name = "supervision_control_notifier", srcs = ["supervision_control_notifier.cpp"], diff --git a/score/launch_manager/src/daemon/src/supervision_control_client/iprocess_state_notifier_mock.hpp b/score/launch_manager/src/daemon/src/supervision_control_client/iprocess_state_notifier_mock.hpp new file mode 100644 index 000000000..52d8d7745 --- /dev/null +++ b/score/launch_manager/src/daemon/src/supervision_control_client/iprocess_state_notifier_mock.hpp @@ -0,0 +1,40 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef IPROCESSSTATE_NOTIFIER_MOCK_HPP_INCLUDED +#define IPROCESSSTATE_NOTIFIER_MOCK_HPP_INCLUDED + +#include "score/mw/launch_manager/process_state_client/iprocess_state_notifier.hpp" + +#include + +#include + +namespace score +{ +namespace lcm +{ + +/// @brief Reusable gmock mock for IProcessStateNotifier, for use by tests of components that notify PHM of process +/// state changes. +class MockProcessStateNotifier : public IProcessStateNotifier +{ + public: + MOCK_METHOD(std::unique_ptr, constructReceiver, (), (override)); + MOCK_METHOD(bool, queuePosixProcess, (const score::lcm::PosixProcess& f_posixProcess), (noexcept, override)); +}; + +} // namespace lcm +} // namespace score + +#endif // IPROCESSSTATE_NOTIFIER_MOCK_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/supervision_control_client/mock_supervision_control_notifier.hpp b/score/launch_manager/src/daemon/src/supervision_control_client/mock_supervision_control_notifier.hpp new file mode 100644 index 000000000..56a43b8e1 --- /dev/null +++ b/score/launch_manager/src/daemon/src/supervision_control_client/mock_supervision_control_notifier.hpp @@ -0,0 +1,32 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef MOCK_SUPERVISION_CONTROL_NOTIFIER_HPP_INCLUDED +#define MOCK_SUPERVISION_CONTROL_NOTIFIER_HPP_INCLUDED + +#include "score/mw/launch_manager/supervision_control_client/isupervision_control_notifier.hpp" +#include + +namespace score::lcm +{ + +class MockSupervisionControlNotifier : public ISupervisionControlNotifier +{ + public: + MOCK_METHOD(bool, reportActivation, (IdentifierHash id, timespec time), (override, noexcept)); + MOCK_METHOD(bool, reportDeactivation, (IdentifierHash id, timespec time), (override, noexcept)); + MOCK_METHOD(std::unique_ptr, constructReceiver, (), (override)); +}; + +} // namespace score::lcm + +#endif // MOCK_SUPERVISION_CONTROL_NOTIFIER_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/watchdog/BUILD b/score/launch_manager/src/daemon/src/watchdog/BUILD index 9628e9541..5fdf87cf9 100644 --- a/score/launch_manager/src/daemon/src/watchdog/BUILD +++ b/score/launch_manager/src/daemon/src/watchdog/BUILD @@ -46,3 +46,16 @@ lm_cc_test( "@googletest//:gtest_main", ], ) + +cc_library( + name = "i_watchdog_if_mock", + testonly = True, + hdrs = ["IWatchdogIfMock.hpp"], + include_prefix = "score/mw/launch_manager/watchdog", + strip_include_prefix = "/score/launch_manager/src/daemon/src/watchdog", + visibility = ["//score:__subpackages__"], + deps = [ + ":i_watchdog_if", + "@googletest//:gtest", + ], +) diff --git a/score/launch_manager/src/daemon/src/watchdog/IWatchdogIfMock.hpp b/score/launch_manager/src/daemon/src/watchdog/IWatchdogIfMock.hpp new file mode 100644 index 000000000..4509c823e --- /dev/null +++ b/score/launch_manager/src/daemon/src/watchdog/IWatchdogIfMock.hpp @@ -0,0 +1,49 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef IWATCHDOGIFMOCK_HPP_INCLUDED +#define IWATCHDOGIFMOCK_HPP_INCLUDED + +#include "score/mw/launch_manager/watchdog/IWatchdogIf.hpp" + +#include + +namespace score +{ +namespace lcm +{ +namespace watchdog +{ + +/// @brief Reusable gmock mock for IWatchdogIf, for use by tests of components that service the watchdog. +class MockWatchdogIf : public IWatchdogIf +{ + public: + MockWatchdogIf() = default; + + MOCK_METHOD( + bool, + init, + (const score::mw::launch_manager::configuration::WatchdogConfig& watchdog_config, std::int64_t cycle_time_ns), + (noexcept, override)); + MOCK_METHOD(bool, enable, (), (noexcept, override)); + MOCK_METHOD(void, disable, (), (noexcept, override)); + MOCK_METHOD(void, serviceWatchdog, (), (noexcept, override)); + MOCK_METHOD(void, fireWatchdogReaction, (), (noexcept, override)); +}; + +} // namespace watchdog +} // namespace lcm +} // namespace score + +#endif // IWATCHDOGIFMOCK_HPP_INCLUDED