diff --git a/score/launch_manager/src/daemon/src/control/BUILD b/score/launch_manager/src/daemon/src/control/BUILD index 18e684eaa4..dc954822c9 100644 --- a/score/launch_manager/src/daemon/src/control/BUILD +++ b/score/launch_manager/src/daemon/src/control/BUILD @@ -15,7 +15,6 @@ load("//tests/utils/bazel:unit_test.bzl", "lm_cc_test") cc_library( name = "control_provider", - srcs = ["control_provider.cpp"], hdrs = ["control_provider.hpp"], include_prefix = "score/mw/launch_manager/control", strip_include_prefix = "/score/launch_manager/src/daemon/src/control", @@ -31,17 +30,11 @@ cc_library( lm_cc_test( name = "control_provider_UT", srcs = ["control_provider_UT.cpp"], - args = [ - "--service_instance_manifest", - "$(rootpath control_provider_test_mw_com_config.json)", - ], - data = ["control_provider_test_mw_com_config.json"], + linkopts = ["-Wl,--wrap=fcntl"], deps = [ ":control_provider", "//score/launch_manager:error", "//score/launch_manager/src/daemon/src/process_group_manager:irun_target_control", - "@googletest//:gtest", - "@score_baselibs//score/string_manipulation/arguments", - "@score_communication//score/mw/com", + "@googletest//:gtest_main", ], ) diff --git a/score/launch_manager/src/daemon/src/control/control_provider.cpp b/score/launch_manager/src/daemon/src/control/control_provider.cpp deleted file mode 100644 index 20452af35a..0000000000 --- a/score/launch_manager/src/daemon/src/control/control_provider.cpp +++ /dev/null @@ -1,245 +0,0 @@ -/******************************************************************************** - * 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/control/control_provider.hpp" -#include "score/mw/launch_manager/common/log.hpp" -#include "score/mw/launch_manager/osal/ipc_comms.hpp" - -namespace score::mw::lifecycle::internal -{ - -Result> ControlProvider::Create(IRunTargetControl* graph) noexcept -{ - const Result instance_specifier_result = - com::InstanceSpecifier::Create(std::string{"LaunchManager/StateManager/Instance"}); - if (!instance_specifier_result.has_value()) - { - LM_LOG_ERROR() << "Failed to create mw::com instance specifier:" << instance_specifier_result.error().Message(); - return MakeUnexpected(ExecErrc::kCommunicationError); - } - - Result skeleton_result = LmControlSkeleton::Create(instance_specifier_result.value()); - if (!skeleton_result.has_value()) - { - LM_LOG_ERROR() << "Failed to create LmControlSkeleton:" << skeleton_result.error().Message(); - return MakeUnexpected(ExecErrc::kCommunicationError); - } - LmControlSkeleton skeleton = std::move(skeleton_result).value(); - - // Owned by a `unique_ptr` so that it is freed on any of the failure paths below. Not - // `std::make_unique`, because the constructor is private. - std::unique_ptr control_provider{new ControlProvider{std::move(skeleton), graph}}; - - const Result setup_activate_run_target_result = control_provider->setupActivateRunTarget(); - if (!setup_activate_run_target_result.has_value()) - { - return MakeUnexpected(static_cast(*setup_activate_run_target_result.error())); - } - - const Result setup_get_active_run_target_result = control_provider->setupGetActiveRunTarget(); - if (!setup_get_active_run_target_result.has_value()) - { - return MakeUnexpected(static_cast(*setup_get_active_run_target_result.error())); - } - - const Result setup_activation_result_result = control_provider->setupActivationResult(); - if (!setup_activation_result_result.has_value()) - { - return MakeUnexpected(static_cast(*setup_activation_result_result.error())); - } - - const Result offer_service_result = control_provider->offerService(); - if (!offer_service_result.has_value()) - { - return MakeUnexpected(static_cast(*offer_service_result.error())); - } - - return control_provider; -} - -ControlProvider::ControlProvider(LmControlSkeleton skeleton, IRunTargetControl* graph) noexcept - : skeleton_(std::move(skeleton)), graph_(graph) -{ -} - -Result ControlProvider::setupActivateRunTarget() noexcept -{ - const auto result = skeleton_.activate_run_target.RegisterHandler( - [this](ActivateRunTargetResponse& response, const ActivateRunTargetRequest& request) { - this->handleActivateRunTarget(response, request); - }); - - if (!result.has_value()) - { - LM_LOG_ERROR() << "Failed to register handler for activate_run_target:" << result.error().Message(); - return MakeUnexpected(ExecErrc::kCommunicationError); - } - - return {}; -} - -void ControlProvider::handleActivateRunTarget( - ActivateRunTargetResponse& response, - const ActivateRunTargetRequest& request) noexcept -{ - // See https://github.com/eclipse-score/lifecycle/issues/643. - if (request.mode != ActivationMode::kForced) - { - LM_LOG_ERROR() << "Activation request failed: queued activation is not yet implemented"; - - response = - ActivateRunTargetResponse{status : RequestStatus::kRejected, rejection_reason : ExecErrc::kNotImplemented}; - return; - } - - const std::optional new_state = IdentifierHash::if_exists(request.run_target_name.data()); - if (!new_state.has_value()) - { - LM_LOG_ERROR() << "Activation request failed: Run Target" << request.run_target_name << "does not exist"; - - response = ActivateRunTargetResponse{ - status : RequestStatus::kRejected, - rejection_reason : ExecErrc::kRunTargetDoesntExist - }; - return; - } - - const score::Result result = graph_->setRequestedRunTarget(new_state.value()); - if (!result.has_value()) - { - LM_LOG_ERROR() << "Activation request failed:" << result.error().Message(); - - response = ActivateRunTargetResponse{ - status : RequestStatus::kRejected, - rejection_reason : static_cast(*result.error()) - }; - return; - } - - response = ActivateRunTargetResponse{status : RequestStatus::kAccepted}; -} - -Result ControlProvider::setupGetActiveRunTarget() noexcept -{ - const auto result = skeleton_.get_active_run_target.RegisterHandler([this](GetActiveRunTargetResponse& response) { - this->handleGetActiveRunTarget(response); - }); - - if (!result.has_value()) - { - LM_LOG_ERROR() << "Failed to register handler for get_active_run_target:" << result.error().Message(); - return MakeUnexpected(ExecErrc::kCommunicationError); - } - - return {}; -} - -void ControlProvider::handleGetActiveRunTarget(GetActiveRunTargetResponse& response) noexcept -{ - const score::Result result = graph_->getActiveRunTarget(); - if (!result.has_value()) - { - SCORE_LANGUAGE_FUTURECPP_ASSERT_MESSAGE( - static_cast(*result.error()) == ExecErrc::kActivationInProgress, - "Impossible to communicate errors other than ExecErrc::kActivationInProgress to the client"); - - response = GetActiveRunTargetResponse{status : QueryStatus::kNotAvailable, run_target : RunTargetName("")}; - return; - } - - const RunTargetName run_target{result.value().get_name()}; - - response = GetActiveRunTargetResponse{status : QueryStatus::kAvailable, run_target}; -} - -Result ControlProvider::setupActivationResult() noexcept -{ - graph_->registerActiveRunTargetCallback([this](IdentifierHash state, RunTargetActivationSource source) { - this->handleActivationResult(state, source); - }); - - return {}; -} - -void ControlProvider::handleActivationResult(IdentifierHash state, RunTargetActivationSource source) noexcept -{ - auto allocate_result = skeleton_.activation_result.Allocate(); - if (!allocate_result.has_value()) - { - LM_LOG_ERROR() << "Failed to allocate space to send the activation result to the state manager:" - "check that the mw::com configuration is correct"; - return; - } - - ActivationResult* event = allocate_result.value().Get(); - { - const std::lock_guard lock(IdentifierHash::get_registry_mutex()); - const auto& registry = IdentifierHash::get_registry(); - const auto it = registry.find(state.data()); - SCORE_LANGUAGE_FUTURECPP_ASSERT_MESSAGE( - it != registry.end(), "IdentifierHash does not correspond to an existing name"); - event->activated_run_target = RunTargetName(it->second); - } - event->activation_source = source; - - const auto send_result = skeleton_.activation_result.Send(std::move(allocate_result.value())); - if (!send_result.has_value()) - { - LM_LOG_ERROR() << "Failed to send the activation result to the state manager"; - return; - } - - LM_LOG_DEBUG() << "Sent the activation result to the state manager"; -} - -Result ControlProvider::offerService() noexcept -{ - const auto result = skeleton_.OfferService(); - if (!result.has_value()) - { - LM_LOG_ERROR() << "Failed to offer mw::com service:" << result.error().Message(); - return MakeUnexpected(ExecErrc::kCommunicationError); - } - - // Workaround for https://github.com/eclipse-score/communication/issues/1064. - // This should be removed once the above issue is solved. - for (int fd = 0; fd < 16; fd++) - { - switch (fd) - { - case STDIN_FILENO: - case STDOUT_FILENO: - case STDERR_FILENO: - case osal::IpcCommsSync::sync_fd: - break; - default: - int flags = fcntl(fd, F_GETFD); - if (flags == -1) - { - SCORE_LANGUAGE_FUTURECPP_ASSERT_MESSAGE( - errno == EBADF, "fcntl F_GETFD failed with unexpected error"); - } - else - { - flags |= FD_CLOEXEC; - const auto result = fcntl(fd, F_SETFD, flags); - SCORE_LANGUAGE_FUTURECPP_ASSERT_MESSAGE(result != -1, "fcntl F_SETFD failed"); - } - break; - } - } - - return {}; -} - -} // namespace score::mw::lifecycle::internal diff --git a/score/launch_manager/src/daemon/src/control/control_provider.hpp b/score/launch_manager/src/daemon/src/control/control_provider.hpp index 58664ae92f..306d6bdb4b 100644 --- a/score/launch_manager/src/daemon/src/control/control_provider.hpp +++ b/score/launch_manager/src/daemon/src/control/control_provider.hpp @@ -14,63 +14,275 @@ #ifndef SCORE_LCM_CONTROL_PROVIDER #define SCORE_LCM_CONTROL_PROVIDER +#include "score/mw/launch_manager/common/log.hpp" +#include "score/mw/launch_manager/osal/ipc_comms.hpp" #include "score/mw/launch_manager/process_group_manager/irun_target_control.hpp" #include "score/mw/lifecycle/details/lm_control_service.h" #include +#include +#include namespace score::mw::lifecycle::internal { +/// @brief Default specifier factory that creates the production mw::com instance specifier. +struct LmControlSpecifierFactory +{ + static Result Create() + { + return com::InstanceSpecifier::Create(std::string{"LaunchManager/StateManager/Instance"}); + } +}; + /// @brief Provides the mw::com service for state managers to connect to. /// @details This cannot be moved, because the mw::com callbacks reference // the ControlProvider at its original location. It is therefore // handed out as a `std::unique_ptr`, which keeps that address fixed. -class ControlProvider +/// +/// @tparam SkeletonT The mw::com skeleton type. Defaults to LmControlSkeleton (production). +/// Tests inject a fake skeleton to exercise handler logic without a +/// real mw::com runtime. +/// @tparam SpecifierFactory A type with a static Create() that returns the mw::com instance +/// specifier. Defaults to LmControlSpecifierFactory (production). +template +class BasicControlProvider { public: - /// @brief Fallible constructor for ControllableGraph. - static Result> Create(IRunTargetControl* graph) noexcept; + /// @brief Named constructor for BasicControlProvider. + static Result> Create(IRunTargetControl* graph) noexcept + { + const Result instance_specifier_result = SpecifierFactory::Create(); + if (!instance_specifier_result.has_value()) + { + LM_LOG_ERROR() << "Failed to create mw::com instance specifier:" + << instance_specifier_result.error().Message(); + return MakeUnexpected(ExecErrc::kCommunicationError); + } + + Result skeleton_result = SkeletonT::Create(instance_specifier_result.value()); + if (!skeleton_result.has_value()) + { + LM_LOG_ERROR() << "Failed to create LmControlSkeleton:" << skeleton_result.error().Message(); + return MakeUnexpected(ExecErrc::kCommunicationError); + } + SkeletonT skeleton = std::move(skeleton_result).value(); + + // Owned by a `unique_ptr` so that it is freed on any of the failure paths below. Not + // `std::make_unique`, because the constructor is private. + std::unique_ptr control_provider{new BasicControlProvider{std::move(skeleton), graph}}; + + const Result setup_activate_run_target_result = control_provider->setupActivateRunTarget(); + if (!setup_activate_run_target_result.has_value()) + { + return MakeUnexpected(static_cast(*setup_activate_run_target_result.error())); + } + + const Result setup_get_active_run_target_result = control_provider->setupGetActiveRunTarget(); + if (!setup_get_active_run_target_result.has_value()) + { + return MakeUnexpected(static_cast(*setup_get_active_run_target_result.error())); + } + + control_provider->setupActivationResult(); - ~ControlProvider() = default; + const Result offer_service_result = control_provider->offerService(); + if (!offer_service_result.has_value()) + { + return MakeUnexpected(static_cast(*offer_service_result.error())); + } - // Cannot be moved because callbacks capture the ControlProvider by reference. - ControlProvider(const ControlProvider&) = delete; - ControlProvider(ControlProvider&&) = delete; - ControlProvider& operator=(const ControlProvider&) = delete; - ControlProvider operator=(ControlProvider&&) = delete; + return control_provider; + } + + ~BasicControlProvider() = default; + + // Cannot be moved because callbacks capture the BasicControlProvider by reference. + BasicControlProvider(const BasicControlProvider&) = delete; + BasicControlProvider(BasicControlProvider&&) = delete; + BasicControlProvider& operator=(const BasicControlProvider&) = delete; + BasicControlProvider operator=(BasicControlProvider&&) = delete; private: - explicit ControlProvider(LmControlSkeleton skeleton, IRunTargetControl* graph) noexcept; + explicit BasicControlProvider(SkeletonT skeleton, IRunTargetControl* graph) noexcept + : skeleton_(std::move(skeleton)), graph_(graph) + { + } /// @brief Register the handler for activate_run_target. - Result setupActivateRunTarget() noexcept; + Result setupActivateRunTarget() noexcept + { + const auto result = skeleton_.activate_run_target.RegisterHandler( + [this](ActivateRunTargetResponse& response, const ActivateRunTargetRequest& request) { + this->handleActivateRunTarget(response, request); + }); + + if (!result.has_value()) + { + LM_LOG_ERROR() << "Failed to register handler for activate_run_target:" << result.error().Message(); + return MakeUnexpected(ExecErrc::kCommunicationError); + } + + return {}; + } /// @brief Handle an activate_run_target request. - void handleActivateRunTarget(ActivateRunTargetResponse& response, const ActivateRunTargetRequest& request) noexcept; + void handleActivateRunTarget(ActivateRunTargetResponse& response, const ActivateRunTargetRequest& request) noexcept + { + // See https://github.com/eclipse-score/lifecycle/issues/643. + if (request.mode != ActivationMode::kForced) + { + LM_LOG_ERROR() << "Activation request failed: queued activation is not yet implemented"; + + response = + ActivateRunTargetResponse{status : RequestStatus::kRejected, rejection_reason : ExecErrc::kNotImplemented}; + return; + } + + const std::optional new_state = IdentifierHash::if_exists(request.run_target_name.data()); + if (!new_state.has_value()) + { + LM_LOG_ERROR() << "Activation request failed: Run Target" << request.run_target_name << "does not exist"; + + response = ActivateRunTargetResponse{ + status : RequestStatus::kRejected, + rejection_reason : ExecErrc::kRunTargetDoesntExist + }; + return; + } + + const score::Result result = graph_->setRequestedRunTarget(new_state.value()); + if (!result.has_value()) + { + LM_LOG_ERROR() << "Activation request failed:" << result.error().Message(); + + response = ActivateRunTargetResponse{ + status : RequestStatus::kRejected, + rejection_reason : static_cast(*result.error()) + }; + return; + } + + response = ActivateRunTargetResponse{status : RequestStatus::kAccepted}; + } /// @brief Register the handler for get_active_run_target. - Result setupGetActiveRunTarget() noexcept; + Result setupGetActiveRunTarget() noexcept + { + const auto result = + skeleton_.get_active_run_target.RegisterHandler([this](GetActiveRunTargetResponse& response) { + this->handleGetActiveRunTarget(response); + }); + + if (!result.has_value()) + { + LM_LOG_ERROR() << "Failed to register handler for get_active_run_target:" << result.error().Message(); + return MakeUnexpected(ExecErrc::kCommunicationError); + } + + return {}; + } /// @brief Handle a get_active_run_target request. - void handleGetActiveRunTarget(GetActiveRunTargetResponse& response) noexcept; + void handleGetActiveRunTarget(GetActiveRunTargetResponse& response) noexcept + { + const score::Result result = graph_->getActiveRunTarget(); + if (!result.has_value()) + { + SCORE_LANGUAGE_FUTURECPP_ASSERT_MESSAGE( + static_cast(*result.error()) == ExecErrc::kActivationInProgress, + "Impossible to communicate errors other than ExecErrc::kActivationInProgress to the client"); + + response = GetActiveRunTargetResponse{status : QueryStatus::kNotAvailable, run_target : RunTargetName("")}; + return; + } + + const RunTargetName run_target{result.value().get_name()}; + + response = GetActiveRunTargetResponse{status : QueryStatus::kAvailable, run_target}; + } /// @brief Register the handler for activation_result. - Result setupActivationResult() noexcept; + void setupActivationResult() noexcept + { + graph_->registerActiveRunTargetCallback([this](IdentifierHash state, RunTargetActivationSource source) { + this->handleActivationResult(state, source); + }); + } /// @brief Handle an activation_result event. - void handleActivationResult(IdentifierHash state, RunTargetActivationSource source) noexcept; + void handleActivationResult(IdentifierHash state, RunTargetActivationSource source) noexcept + { + auto allocate_result = skeleton_.activation_result.Allocate(); + if (!allocate_result.has_value()) + { + LM_LOG_ERROR() << "Failed to allocate space to send the activation result to the state manager:" + "check that the mw::com configuration is correct"; + return; + } + + ActivationResult* event = allocate_result.value().Get(); + event->activated_run_target = RunTargetName(state.get_name()); + event->activation_source = source; + + const auto send_result = skeleton_.activation_result.Send(std::move(allocate_result.value())); + if (!send_result.has_value()) + { + LM_LOG_ERROR() << "Failed to send the activation result to the state manager"; + return; + } + + LM_LOG_DEBUG() << "Sent the activation result to the state manager"; + } /// @brief Make the service available to clients. - Result offerService() noexcept; + Result offerService() noexcept + { + const auto result = skeleton_.OfferService(); + if (!result.has_value()) + { + LM_LOG_ERROR() << "Failed to offer mw::com service:" << result.error().Message(); + return MakeUnexpected(ExecErrc::kCommunicationError); + } + + // Workaround for https://github.com/eclipse-score/communication/issues/1064. + // This should be removed once the above issue is solved. + for (int fd = 0; fd < 16; fd++) + { + switch (fd) + { + case STDIN_FILENO: + case STDOUT_FILENO: + case STDERR_FILENO: + break; + default: + int flags = fcntl(fd, F_GETFD); + if (flags != -1) + { + flags |= FD_CLOEXEC; + const auto result = fcntl(fd, F_SETFD, flags); + if (result == -1) + { + LM_LOG_ERROR() << "fcntl F_SETFD failed for fd number:" << fd; + } + } + break; + } + } + + return {}; + } /// @brief The external `mw::com` interface. - LmControlSkeleton skeleton_; + SkeletonT skeleton_; /// @brief The underlying graph implementation. IRunTargetControl* graph_; }; +/// @brief Production alias: BasicControlProvider wired to the real mw::com skeleton. +using ControlProvider = BasicControlProvider; + } // namespace score::mw::lifecycle::internal #endif // SCORE_LCM_CONTROL_PROVIDER diff --git a/score/launch_manager/src/daemon/src/control/control_provider_UT.cpp b/score/launch_manager/src/daemon/src/control/control_provider_UT.cpp index e6c395aad4..4ce587b162 100644 --- a/score/launch_manager/src/daemon/src/control/control_provider_UT.cpp +++ b/score/launch_manager/src/daemon/src/control/control_provider_UT.cpp @@ -14,131 +14,715 @@ #include "score/mw/launch_manager/control/control_provider.hpp" #include "score/mw/launch_manager/process_group_manager/irun_target_control.hpp" -#include "score/mw/com/runtime.h" -#include "score/string_manipulation/arguments/arguments.h" - +#include #include +#include +#include +#include +#include + +#include #include +#include #include #include +// ============================================================================ +// fcntl() linker wrapper — offerService FD_CLOEXEC workaround test support +// +// These declarations must live at file scope: extern "C" gives functions +// external C linkage, which is incompatible with an anonymous namespace. +// +// Activated via --wrap=fcntl in the BUILD target's linkopts. The linker +// replaces every call to fcntl() with __wrap_fcntl(), and makes the original +// implementation available as __real_fcntl(). +// +// Only calls that match the currently injected (fd, cmd) pair are intercepted; +// all other calls are forwarded to __real_fcntl() transparently. +// +// ============================================================================ +// +// Why would we like to have a mixed setup where sometimes we use a real +// implementation and sometimes we use wrapper? +// +// The OfferService_OpenFd_SetsCloexec test checks that code correctly set the +// FD_CLOEXEC on a real file descriptor created by OS. +// +// The OfferService_FSetfdFails_LogsAndContinues tests a corner case where +// fcntl(fd, F_GETFD) succeed, but fcntl(fd, F_SETFD, flags) fails. +// To inject this type of the failure without a wrapper, we would need to +// close an fd after fcntl(fd, F_GETFD), but before fcntl(fd, F_SETFD, flags). +// +// This can be done with a bit of multi threading, but it would be flaky... +// Wrapper is more stable. +// +// And yes, the OfferService_OpenFd_SetsCloexec could also go through wrapper, +// but current implementation is much more close to the real life scenario. +// ============================================================================ + +/// @brief Controls whether and how a single fcntl() call is intercepted. +struct FcntlInjection +{ + bool active{false}; + int target_fd{-1}; + int target_cmd{-1}; + int errno_value{EINVAL}; +}; + +/// @brief Global injection state — written by ScopedFcntlError, read by __wrap_fcntl. +static FcntlInjection g_fcntl_injection{}; + +/// @brief The original fcntl() implementation, renamed by the linker. +extern "C" int __real_fcntl(int fd, int cmd, ...); + +/// @brief Intercepts every fcntl() call in the test binary. +/// Injects a configurable error for the targeted (fd, cmd) pair; +/// forwards everything else to the real implementation unchanged. +extern "C" int __wrap_fcntl(int fd, int cmd, ...) +{ + // Extract the optional third argument. fcntl() accepts either an int + // (F_SETFD, F_SETFL, …) or a pointer (F_GETLK, …). Extracting as int + // and forwarding is safe for the integer commands used in offerService(); + // non-integer commands are never intercepted so the forwarded value is + // ignored by __real_fcntl's own va_arg handling. + va_list args; + va_start(args, cmd); + const int third_arg = va_arg(args, int); + va_end(args); + + if (g_fcntl_injection.active && fd == g_fcntl_injection.target_fd && cmd == g_fcntl_injection.target_cmd) + { + // error injections through the wrapper + errno = g_fcntl_injection.errno_value; + return -1; + } + + // forwarding to the real implementation + return __real_fcntl(fd, cmd, third_arg); +} + namespace score::mw::lifecycle::internal { namespace { -// Locks in the contract from the class comment in control_provider.hpp: the registered callbacks -// capture the ControlProvider's own address, so it must stay neither movable nor copyable, and -// is handed out as a `std::unique_ptr` instead. A regression here (e.g. someone defaulting the -// move ops) would silently reintroduce a dangling-callback bug. -static_assert(!std::is_move_constructible_v); -static_assert(!std::is_move_assignable_v); -static_assert(!std::is_copy_constructible_v); -static_assert(!std::is_copy_assignable_v); +using ::testing::_; +using ::testing::DoAll; +using ::testing::Invoke; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::SaveArg; + +// ============================================================================ +// Fake infrastructure +// ============================================================================ + +/// @brief Minimal value type that mirrors the SampleAllocateePtr interface +/// used by the real skeleton event. Copyable so SaveArg works without complications. +struct FakeAllocateePtr +{ + ActivationResult data{}; + ActivationResult* Get() noexcept + { + return &data; + } +}; -// `Create()` never calls back into `graph` before returning, so a no-op stub is sufficient for -// `getActiveRunTarget`/`setRequestedRunTarget`. `registerActiveRunTargetCallback` is captured so -// the test below can invoke it directly, as if a real transition had just completed. -class FakeRunTargetControl : public IRunTargetControl +/// @brief Single gmock class that all fake skeleton operations are routed to. +class SkeletonMock { public: - [[nodiscard]] score::Result getActiveRunTarget() const noexcept override + MOCK_METHOD(score::Result, SkeletonCreate, ()); + MOCK_METHOD(score::Result, OfferService, ()); + MOCK_METHOD( + score::Result, + ActivateRunTargetRegisterHandler, + (std::function)); + MOCK_METHOD( + score::Result, + GetActiveRunTargetRegisterHandler, + (std::function)); + MOCK_METHOD(score::Result, Allocate, ()); + MOCK_METHOD(score::Result, Send, (FakeAllocateePtr)); +}; + +/// @brief Adapter for skeleton_.activate_run_target — delegates RegisterHandler to SkeletonMock. +struct FakeActivateMethod +{ + SkeletonMock* mock; + + template + score::Result RegisterHandler(Callable&& cb) + { + return mock->ActivateRunTargetRegisterHandler( + std::function( + std::forward(cb))); + } +}; + +/// @brief Adapter for skeleton_.get_active_run_target — delegates RegisterHandler to SkeletonMock. +struct FakeGetActiveMethod +{ + SkeletonMock* mock; + + template + score::Result RegisterHandler(Callable&& cb) { - return MakeUnexpected(ExecErrc::kCommunicationError); + return mock->GetActiveRunTargetRegisterHandler( + std::function(std::forward(cb))); } +}; - [[nodiscard]] score::Result setRequestedRunTarget(IdentifierHash /*run_target*/) noexcept override +/// @brief Adapter for skeleton_.activation_result — delegates Allocate/Send to SkeletonMock. +struct FakeActivationResultEvent +{ + SkeletonMock* mock; + + score::Result Allocate() + { + return mock->Allocate(); + } + score::Result Send(FakeAllocateePtr p) { - return {}; + return mock->Send(std::move(p)); } +}; + +/// @brief Drop-in replacement for LmControlSkeleton. Holds the three adapter members +/// and routes Create()/OfferService() through the static mock pointer. +struct FakeLmControlSkeleton +{ + static inline SkeletonMock* mock{nullptr}; - void registerActiveRunTargetCallback(ActivationCallbackT callback) noexcept override + FakeActivateMethod activate_run_target; + FakeGetActiveMethod get_active_run_target; + FakeActivationResultEvent activation_result; + + FakeLmControlSkeleton() : activate_run_target{mock}, get_active_run_target{mock}, activation_result{mock} { - callback_ = std::move(callback); } - void TriggerActivation(IdentifierHash state, RunTargetActivationSource source) + static score::Result Create(const com::InstanceSpecifier&) + { + const auto result = mock->SkeletonCreate(); + if (!result.has_value()) + { + return MakeUnexpected(ExecErrc::kCommunicationError); + } + return FakeLmControlSkeleton{}; + } + + score::Result OfferService() + { + return mock->OfferService(); + } +}; + +/// @brief Specifier factory whose failure mode can be toggled per test. +struct FakeSpecifierFactory +{ + static inline bool should_fail{false}; + + static score::Result Create() { - ASSERT_TRUE(static_cast(callback_)) << "registerActiveRunTargetCallback was never called"; - callback_(state, source); + if (should_fail) + { + return MakeUnexpected(ExecErrc::kCommunicationError); + } + return com::InstanceSpecifier::Create(std::string{"LaunchManager/StateManager/Instance"}); } +}; - private: - ActivationCallbackT callback_; +/// @brief gmock implementation of IRunTargetControl. +class MockRunTargetControl : public IRunTargetControl +{ + public: + MOCK_METHOD(score::Result, getActiveRunTarget, (), (const, noexcept, override)); + MOCK_METHOD(score::Result, setRequestedRunTarget, (IdentifierHash), (noexcept, override)); + MOCK_METHOD(void, registerActiveRunTargetCallback, (ActivationCallbackT), (noexcept, override)); }; +using TestControlProvider = BasicControlProvider; + +// ============================================================================ +// Fixture +// ============================================================================ + class ControlProviderUT : public ::testing::Test { protected: void SetUp() override { RecordProperty("TestType", "interface-test"); - RecordProperty("DerivationTechnique", "explorative-testing"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + + FakeLmControlSkeleton::mock = &skeleton_mock_; + FakeSpecifierFactory::should_fail = false; + + // Sensible defaults so tests only need to wire what they care about. + ON_CALL(skeleton_mock_, SkeletonCreate()).WillByDefault(Return(score::Result{})); + ON_CALL(skeleton_mock_, ActivateRunTargetRegisterHandler(_)) + .WillByDefault(DoAll(SaveArg<0>(&on_activate_), Return(score::Result{}))); + ON_CALL(skeleton_mock_, GetActiveRunTargetRegisterHandler(_)) + .WillByDefault(DoAll(SaveArg<0>(&on_get_active_), Return(score::Result{}))); + ON_CALL(skeleton_mock_, OfferService()).WillByDefault(Return(score::Result{})); + ON_CALL(skeleton_mock_, Allocate()).WillByDefault(Return(score::Result{FakeAllocateePtr{}})); + ON_CALL(skeleton_mock_, Send(_)).WillByDefault(Return(score::Result{})); + + ON_CALL(graph_, registerActiveRunTargetCallback(_)).WillByDefault(Invoke([this](ActivationCallbackT cb) { + activation_callback_ = std::move(cb); + })); + ON_CALL(graph_, getActiveRunTarget()).WillByDefault(Return(score::Result{default_run_target_})); + ON_CALL(graph_, setRequestedRunTarget(_)).WillByDefault(Return(score::Result{})); + } + + void TearDown() override + { + FakeLmControlSkeleton::mock = nullptr; + } + + /// @brief Build a successfully-constructed provider using fixture defaults. + std::unique_ptr MakeProvider() + { + auto result = TestControlProvider::Create(&graph_); + EXPECT_TRUE(result.has_value()); + return std::move(result).value(); } - FakeRunTargetControl graph_; + /// @brief Fire the activation callback captured during Create(), as if the graph just + /// completed a Run Target transition. + void TriggerActivation(IdentifierHash state, RunTargetActivationSource source) + { + ASSERT_TRUE(activation_callback_) << "registerActiveRunTargetCallback was never called"; + activation_callback_(state, source); + } + + // Registered by IdentifierHash constructor — stays in the global registry for the + // process lifetime. Use unique names per translation unit to avoid cross-test collisions. + const IdentifierHash default_run_target_{"cp_ut_default"}; + + NiceMock skeleton_mock_; + NiceMock graph_; + + // Handlers captured via SaveArg during Create() / MakeProvider(): + std::function on_activate_; + std::function on_get_active_; + ActivationCallbackT activation_callback_; }; +// ============================================================================ +// Static assertions — compile-time contract: no copy, no move. +// ============================================================================ + TEST_F(ControlProviderUT, StaticAssertionsCompiled) { RecordProperty( "Description", - "ControlProvider is neither movable nor copyable, so the address captured by its " - "registered callbacks can never change."); + "BasicControlProvider is neither movable nor copyable. Callbacks capture `this` " + "and the object must never relocate. A compile-time regression here would " + "silently reintroduce a dangling-callback bug."); + + static_assert(!std::is_move_constructible_v); + static_assert(!std::is_move_assignable_v); + static_assert(!std::is_copy_constructible_v); + static_assert(!std::is_copy_assignable_v); - // The interesting assertions above run at compile time; this keeps the test target from - // being empty and gives CI something to report. SUCCEED(); } -TEST_F(ControlProviderUT, CallbacksDispatchAfterOwnershipTransfer) +// ============================================================================ +// Create() — failure branches +// ============================================================================ + +TEST_F(ControlProviderUT, Create_SpecifierCreationFails_ReturnsCommunicationError) { RecordProperty( "Description", - "After the std::unique_ptr returned by Create() is moved to a different owner, the " - "registered callbacks still dispatch through the same, still-alive ControlProvider."); + "When the specifier factory fails, Create() returns kCommunicationError " + "without attempting skeleton creation."); - const IdentifierHash run_target_id{"control_provider_ut_run_target"}; + FakeSpecifierFactory::should_fail = true; + EXPECT_CALL(skeleton_mock_, SkeletonCreate()).Times(0); - std::unique_ptr owner; - { - Result> create_result = ControlProvider::Create(&graph_); - ASSERT_TRUE(create_result.has_value()); + const auto result = TestControlProvider::Create(&graph_); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kCommunicationError); +} + +TEST_F(ControlProviderUT, Create_SkeletonCreationFails_ReturnsCommunicationError) +{ + RecordProperty( + "Description", + "When skeleton creation fails, Create() returns kCommunicationError " + "without registering any handlers."); + + EXPECT_CALL(skeleton_mock_, SkeletonCreate()).WillOnce(Return(MakeUnexpected(ExecErrc::kCommunicationError))); + EXPECT_CALL(skeleton_mock_, ActivateRunTargetRegisterHandler(_)).Times(0); + + const auto result = TestControlProvider::Create(&graph_); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kCommunicationError); +} + +TEST_F(ControlProviderUT, Create_RegisterActivateHandlerFails_ReturnsCommunicationError) +{ + RecordProperty( + "Description", + "When registering the activate_run_target handler fails, Create() returns " + "kCommunicationError without proceeding to the get_active_run_target handler."); + + EXPECT_CALL(skeleton_mock_, ActivateRunTargetRegisterHandler(_)) + .WillOnce(Return(MakeUnexpected(ExecErrc::kCommunicationError))); + EXPECT_CALL(skeleton_mock_, GetActiveRunTargetRegisterHandler(_)).Times(0); + + const auto result = TestControlProvider::Create(&graph_); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kCommunicationError); +} + +TEST_F(ControlProviderUT, Create_RegisterGetActiveHandlerFails_ReturnsCommunicationError) +{ + RecordProperty( + "Description", + "When registering the get_active_run_target handler fails, Create() returns " + "kCommunicationError without offering the service."); + + EXPECT_CALL(skeleton_mock_, GetActiveRunTargetRegisterHandler(_)) + .WillOnce(Return(MakeUnexpected(ExecErrc::kCommunicationError))); + EXPECT_CALL(skeleton_mock_, OfferService()).Times(0); + + const auto result = TestControlProvider::Create(&graph_); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kCommunicationError); +} + +TEST_F(ControlProviderUT, Create_OfferServiceFails_ReturnsCommunicationError) +{ + RecordProperty("Description", "When OfferService fails, Create() returns kCommunicationError."); - // Only the pointer moves; the ControlProvider the callbacks captured stays where it is. - owner = std::move(create_result).value(); + EXPECT_CALL(skeleton_mock_, OfferService()).WillOnce(Return(MakeUnexpected(ExecErrc::kCommunicationError))); + + const auto result = TestControlProvider::Create(&graph_); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ExecErrc::kCommunicationError); +} + +TEST_F(ControlProviderUT, Create_Success_RegistersHandlersAndOffersService) +{ + RecordProperty( + "Description", + "On the happy path Create() registers both method handlers, registers the " + "activation callback with the graph, offers the service, and returns a " + "non-null provider."); + + EXPECT_CALL(skeleton_mock_, ActivateRunTargetRegisterHandler(_)).Times(1); + EXPECT_CALL(skeleton_mock_, GetActiveRunTargetRegisterHandler(_)).Times(1); + EXPECT_CALL(graph_, registerActiveRunTargetCallback(_)).Times(1); + EXPECT_CALL(skeleton_mock_, OfferService()).Times(1); + + const auto result = TestControlProvider::Create(&graph_); + ASSERT_TRUE(result.has_value()); + EXPECT_NE(result.value(), nullptr); +} + +// ============================================================================ +// handleActivateRunTarget +// ============================================================================ + +TEST_F(ControlProviderUT, ActivateRunTarget_QueuedMode_Rejected) +{ + RecordProperty( + "Description", + "A queued-mode activation request is rejected with kRejected/kNotImplemented; " + "the graph is never consulted."); + + auto provider = MakeProvider(); + ASSERT_TRUE(on_activate_) << "activate_run_target handler was not registered"; + + EXPECT_CALL(graph_, setRequestedRunTarget(_)).Times(0); + + ActivateRunTargetResponse response{}; + const ActivateRunTargetRequest request{RunTargetName{"any_target"}, ActivationMode::kQueued}; + on_activate_(response, request); + + EXPECT_EQ(response.status, RequestStatus::kRejected); + EXPECT_EQ(response.rejection_reason, ExecErrc::kNotImplemented); +} + +TEST_F(ControlProviderUT, ActivateRunTarget_UnknownRunTarget_Rejected) +{ + RecordProperty( + "Description", + "A forced activation for a name absent from the IdentifierHash registry is " + "rejected with kRejected/kRunTargetDoesntExist."); + + auto provider = MakeProvider(); + ASSERT_TRUE(on_activate_) << "activate_run_target handler was not registered"; + + EXPECT_CALL(graph_, setRequestedRunTarget(_)).Times(0); + + ActivateRunTargetResponse response{}; + const ActivateRunTargetRequest request{RunTargetName{"__cp_ut_does_not_exist__"}, ActivationMode::kForced}; + on_activate_(response, request); + + EXPECT_EQ(response.status, RequestStatus::kRejected); + EXPECT_EQ(response.rejection_reason, ExecErrc::kRunTargetDoesntExist); +} + +TEST_F(ControlProviderUT, ActivateRunTarget_SetRequestedRunTargetFails_Rejected) +{ + RecordProperty( + "Description", + "When setRequestedRunTarget returns an error, the response carries " + "kRejected with the same error code propagated from the graph."); + + auto provider = MakeProvider(); + ASSERT_TRUE(on_activate_) << "activate_run_target handler was not registered"; + + const IdentifierHash known{"cp_ut_known_target_fail"}; + EXPECT_CALL(graph_, setRequestedRunTarget(_)).WillOnce(Return(MakeUnexpected(ExecErrc::kFailed))); + + ActivateRunTargetResponse response{}; + const ActivateRunTargetRequest request{RunTargetName{known.get_name()}, ActivationMode::kForced}; + on_activate_(response, request); + + EXPECT_EQ(response.status, RequestStatus::kRejected); + EXPECT_EQ(response.rejection_reason, ExecErrc::kFailed); +} + +TEST_F(ControlProviderUT, ActivateRunTarget_ForcedSuccess_Accepted) +{ + RecordProperty( + "Description", + "A forced activation for a known Run Target that the graph accepts " + "is acknowledged with kAccepted."); + + auto provider = MakeProvider(); + ASSERT_TRUE(on_activate_) << "activate_run_target handler was not registered"; + + const IdentifierHash known{"cp_ut_known_target_ok"}; + EXPECT_CALL(graph_, setRequestedRunTarget(_)).WillOnce(Return(score::Result{})); + + ActivateRunTargetResponse response{}; + const ActivateRunTargetRequest request{RunTargetName{known.get_name()}, ActivationMode::kForced}; + on_activate_(response, request); + + EXPECT_EQ(response.status, RequestStatus::kAccepted); +} + +// ============================================================================ +// handleGetActiveRunTarget +// ============================================================================ + +TEST_F(ControlProviderUT, GetActiveRunTarget_ActivationInProgress_NotAvailable) +{ + RecordProperty( + "Description", + "When the graph reports kActivationInProgress, the response carries " + "kNotAvailable."); + + auto provider = MakeProvider(); + ASSERT_TRUE(on_get_active_) << "get_active_run_target handler was not registered"; + + EXPECT_CALL(graph_, getActiveRunTarget()).WillOnce(Return(MakeUnexpected(ExecErrc::kActivationInProgress))); + + GetActiveRunTargetResponse response{}; + on_get_active_(response); + + EXPECT_EQ(response.status, QueryStatus::kNotAvailable); +} + +TEST_F(ControlProviderUT, GetActiveRunTarget_Available) +{ + RecordProperty( + "Description", + "When the graph returns a valid IdentifierHash, the response carries " + "kAvailable and the corresponding Run Target name."); + + auto provider = MakeProvider(); + ASSERT_TRUE(on_get_active_) << "get_active_run_target handler was not registered"; + + const IdentifierHash active{"cp_ut_active_run_target"}; + EXPECT_CALL(graph_, getActiveRunTarget()).WillOnce(Return(score::Result{active})); + + GetActiveRunTargetResponse response{}; + on_get_active_(response); + + EXPECT_EQ(response.status, QueryStatus::kAvailable); + EXPECT_EQ(response.run_target, RunTargetName{"cp_ut_active_run_target"}); +} + +TEST_F(ControlProviderUT, GetActiveRunTarget_UnexpectedError_Aborts) +{ + RecordProperty( + "Description", + "If getActiveRunTarget() returns any error other than kActivationInProgress " + "the handler hits an internal assertion and the process terminates, because " + "no other error code can be communicated to the client."); + + auto provider = MakeProvider(); + ASSERT_TRUE(on_get_active_) << "get_active_run_target handler was not registered"; + + EXPECT_CALL(graph_, getActiveRunTarget()).WillRepeatedly(Return(MakeUnexpected(ExecErrc::kFailed))); + + EXPECT_DEATH( + { + GetActiveRunTargetResponse response{}; + on_get_active_(response); + }, + "Impossible to communicate errors other than ExecErrc::kActivationInProgress to the client"); +} + +// ============================================================================ +// handleActivationResult +// ============================================================================ + +TEST_F(ControlProviderUT, ActivationResult_AllocateFails_SendNotAttempted) +{ + RecordProperty("Description", "When Allocate() fails, the handler returns early without calling Send."); + + auto provider = MakeProvider(); + + const IdentifierHash known{"cp_ut_alloc_fail_target"}; + EXPECT_CALL(skeleton_mock_, Allocate()).WillOnce(Return(MakeUnexpected(ExecErrc::kCommunicationError))); + EXPECT_CALL(skeleton_mock_, Send(_)).Times(0); + + TriggerActivation(known, RunTargetActivationSource::kStateManagerRequest); +} + +TEST_F(ControlProviderUT, ActivationResult_SendFails_NocrashAfterAllocate) +{ + RecordProperty( + "Description", + "When Send() returns an error after a successful Allocate(), the handler " + "logs and returns cleanly without crashing."); + + auto provider = MakeProvider(); + + const IdentifierHash known{"cp_ut_send_fail_target"}; + EXPECT_CALL(skeleton_mock_, Allocate()).WillOnce(Return(score::Result{FakeAllocateePtr{}})); + EXPECT_CALL(skeleton_mock_, Send(_)).WillOnce(Return(MakeUnexpected(ExecErrc::kCommunicationError))); + + TriggerActivation(known, RunTargetActivationSource::kStateManagerRequest); +} + +TEST_F(ControlProviderUT, ActivationResult_Success_EventPopulatedAndSent) +{ + RecordProperty( + "Description", + "On the happy path the allocated event is populated with the correct Run " + "Target name and activation source, then sent via the skeleton."); + + auto provider = MakeProvider(); + + const IdentifierHash known{"cp_ut_success_target"}; + + FakeAllocateePtr sent{}; + EXPECT_CALL(skeleton_mock_, Allocate()).WillOnce(Return(score::Result{FakeAllocateePtr{}})); + EXPECT_CALL(skeleton_mock_, Send(_)).WillOnce(DoAll(SaveArg<0>(&sent), Return(score::Result{}))); + + TriggerActivation(known, RunTargetActivationSource::kStateManagerRequest); + + EXPECT_EQ(sent.data.activated_run_target, RunTargetName{"cp_ut_success_target"}); + EXPECT_EQ(sent.data.activation_source, RunTargetActivationSource::kStateManagerRequest); +} + +// ============================================================================ +// offerService — FD_CLOEXEC workaround +// Testing workaround for https://github.com/eclipse-score/communication/issues/1064 +// +// Uses the __wrap_fcntl linker wrapper defined at file scope above. +// ScopedFcntlError activates error injection for a specific (fd, cmd) pair +// and restores clean state on destruction. +// ============================================================================ + +/// @brief RAII guard that activates fcntl() error injection for one (fd, cmd) pair +/// and clears the injection state when it goes out of scope. +struct ScopedFcntlError +{ + ScopedFcntlError(int fd, int cmd, int errno_val = EINVAL) + { + g_fcntl_injection = {true, fd, cmd, errno_val}; } - ASSERT_NE(owner, nullptr); + ~ScopedFcntlError() + { + g_fcntl_injection = {}; + } +}; + +TEST_F(ControlProviderUT, OfferService_OpenFd_SetsCloexec) +{ + RecordProperty( + "Description", + "offerService() sets FD_CLOEXEC on open file descriptors in [0, 16) " + "except stdin, stdout and stderr, which are explicitly skipped. " + "A pipe opened before Create() must have the flag set afterwards; " + "the standard streams must be left completely unchanged."); + + const int stdin_flags_before = fcntl(STDIN_FILENO, F_GETFD); + const int stdout_flags_before = fcntl(STDOUT_FILENO, F_GETFD); + const int stderr_flags_before = fcntl(STDERR_FILENO, F_GETFD); + ASSERT_NE(stdin_flags_before, -1) << "fcntl F_GETFD failed for stdin"; + ASSERT_NE(stdout_flags_before, -1) << "fcntl F_GETFD failed for stdout"; + ASSERT_NE(stderr_flags_before, -1) << "fcntl F_GETFD failed for stderr"; + + int fds[2]{-1, -1}; + ASSERT_EQ(pipe(fds), 0) << "pipe() failed"; + ASSERT_LT(fds[0], 16) << "read end of pipe is outside the fd range covered by offerService()"; + ASSERT_LT(fds[1], 16) << "write end of pipe is outside the fd range covered by offerService()"; + + auto provider = MakeProvider(); - // Doesn't crash and doesn't trip ASan/UBSan iff the callback dispatches through the - // still-alive ControlProvider that `owner` now holds. - graph_.TriggerActivation(run_target_id, RunTargetActivationSource::kStateManagerRequest); + EXPECT_TRUE(fcntl(fds[0], F_GETFD) & FD_CLOEXEC) << "read end of pipe (fd " << fds[0] << ") did not get FD_CLOEXEC"; + EXPECT_TRUE(fcntl(fds[1], F_GETFD) & FD_CLOEXEC) + << "write end of pipe (fd " << fds[1] << ") did not get FD_CLOEXEC"; + + EXPECT_EQ(fcntl(STDIN_FILENO, F_GETFD), stdin_flags_before) << "offerService() modified stdin flags"; + EXPECT_EQ(fcntl(STDOUT_FILENO, F_GETFD), stdout_flags_before) << "offerService() modified stdout flags"; + EXPECT_EQ(fcntl(STDERR_FILENO, F_GETFD), stderr_flags_before) << "offerService() modified stderr flags"; + + close(fds[0]); + close(fds[1]); } -TEST_F(ControlProviderUT, DestructionReleasesService) +TEST_F(ControlProviderUT, OfferService_FSetfdFails_LogsAndContinues) { RecordProperty( "Description", - "Destroying the ControlProvider returned by Create() releases the offered service, so " - "a later Create() for the same instance succeeds again."); + "When F_SETFD fails for an open file descriptor, offerService() logs a " + "warning and continues rather than terminating. The failure is injected " + "via the __wrap_fcntl linker wrapper for the read end of a pipe."); - Result> first = ControlProvider::Create(&graph_); - ASSERT_TRUE(first.has_value()); - first.value().reset(); + int fds[2]{-1, -1}; + ASSERT_EQ(pipe(fds), 0) << "pipe() failed"; + ASSERT_LT(fds[0], 16) << "read end of pipe is outside the fd range covered by offerService()"; - FakeRunTargetControl second_graph; - Result> second = ControlProvider::Create(&second_graph); - EXPECT_TRUE(second.has_value()); + { + const ScopedFcntlError inject{fds[0], F_SETFD}; + auto provider = MakeProvider(); + } + + close(fds[0]); + close(fds[1]); } -} // namespace -} // namespace score::mw::lifecycle::internal +// ============================================================================ +// LmControlSpecifierFactory — production specifier factory +// ============================================================================ -int main(int argc, char** argv) +TEST(LmControlSpecifierFactoryUT, Create_ReturnsValidSpecifier) { - ::testing::InitGoogleTest(&argc, argv); - score::mw::com::runtime::InitializeRuntime( - score::string_manipulation::GetArguments(argc, const_cast(argv))); - return RUN_ALL_TESTS(); + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty( + "Description", + "LmControlSpecifierFactory::Create() returns a valid mw::com instance " + "specifier for the production service path without requiring a running runtime."); + + const auto result = LmControlSpecifierFactory::Create(); + EXPECT_TRUE(result.has_value()); } + +} // namespace +} // namespace score::mw::lifecycle::internal diff --git a/score/launch_manager/src/daemon/src/control/control_provider_test_mw_com_config.json b/score/launch_manager/src/daemon/src/control/control_provider_test_mw_com_config.json deleted file mode 100644 index 861d110c67..0000000000 --- a/score/launch_manager/src/daemon/src/control/control_provider_test_mw_com_config.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "serviceTypes": [ - { - "serviceTypeName": "/score/mw/lifecycle/LmControlService", - "version": { - "major": 1, - "minor": 0 - }, - "bindings": [ - { - "binding": "SHM", - "serviceId": 7101, - "events": [ - { - "eventName": "ActivationResult", - "eventId": 1 - } - ], - "methods": [ - { - "methodName": "ActivateRunTarget", - "methodId": 2 - }, - { - "methodName": "GetActiveRunTarget", - "methodId": 3 - } - ] - } - ] - } - ], - "serviceInstances": [ - { - "instanceSpecifier": "LaunchManager/StateManager/Instance", - "serviceTypeName": "/score/mw/lifecycle/LmControlService", - "version": { - "major": 1, - "minor": 0 - }, - "instances": [ - { - "instanceId": 1, - "asil-level": "QM", - "binding": "SHM", - "events": [ - { - "eventName": "ActivationResult", - "numberOfSampleSlots": 8, - "maxSubscribers": 1 - } - ], - "methods": [ - { - "methodName": "ActivateRunTarget", - "queueSize": 1 - }, - { - "methodName": "GetActiveRunTarget", - "queueSize": 1 - } - ] - } - ] - } - ], - "global": { - "asil-level": "QM" - } -}