Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions score/launch_manager/src/daemon/src/control/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
load("@rules_cc//cc:defs.bzl", "cc_library")
load("//tests/utils/bazel:unit_test.bzl", "lm_cc_test")

cc_library(
name = "control_provider",
Expand All @@ -26,3 +27,21 @@ cc_library(
"//score/launch_manager/src/lm_control",
],
)

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"],
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",
],
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
#include "score/mw/launch_manager/common/log.hpp"
#include "score/mw/launch_manager/osal/ipc_comms.hpp"

#include <memory>

namespace score::mw::lifecycle::internal
{

Expand All @@ -36,33 +38,35 @@ Result<ControlProvider*> ControlProvider::Create(IRunTargetControl* graph) noexc
}
LmControlSkeleton skeleton = std::move(skeleton_result).value();

auto* control_provider = new ControlProvider{std::move(skeleton), graph};
// Freed automatically via `guard` on any of the failure paths below; released to the
// caller only once every setup step below has succeeded.
std::unique_ptr<ControlProvider> guard{new ControlProvider{std::move(skeleton), graph}};

const Result<void> setup_activate_run_target_result = control_provider->setupActivateRunTarget();
const Result<void> setup_activate_run_target_result = guard->setupActivateRunTarget();
if (!setup_activate_run_target_result.has_value())
{
return MakeUnexpected(static_cast<ExecErrc>(*setup_activate_run_target_result.error()));
}

const Result<void> setup_get_active_run_target_result = control_provider->setupGetActiveRunTarget();
const Result<void> setup_get_active_run_target_result = guard->setupGetActiveRunTarget();
if (!setup_get_active_run_target_result.has_value())
{
return MakeUnexpected(static_cast<ExecErrc>(*setup_get_active_run_target_result.error()));
}

const Result<void> setup_activation_result_result = control_provider->setupActivationResult();
const Result<void> setup_activation_result_result = guard->setupActivationResult();
if (!setup_activation_result_result.has_value())
{
return MakeUnexpected(static_cast<ExecErrc>(*setup_activation_result_result.error()));
}

const Result<void> offer_service_result = control_provider->offerService();
const Result<void> offer_service_result = guard->offerService();
if (!offer_service_result.has_value())
{
return MakeUnexpected(static_cast<ExecErrc>(*offer_service_result.error()));
}

return control_provider;
return guard.release();
}

ControlProvider::ControlProvider(LmControlSkeleton skeleton, IRunTargetControl* graph) noexcept
Expand Down
104 changes: 104 additions & 0 deletions score/launch_manager/src/daemon/src/control/control_provider_UT.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/********************************************************************************
* 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/process_group_manager/irun_target_control.hpp"

#include "score/mw/com/runtime.h"
#include "score/string_manipulation/arguments/arguments.h"

#include <gtest/gtest.h>

namespace score::mw::lifecycle::internal
{
namespace
{

// `Create()` never calls back into `graph` before returning, so a no-op stub is sufficient for
// exercising `Create()` itself: none of these bodies run in the tests below.
class FakeRunTargetControl : public IRunTargetControl
{
public:
[[nodiscard]] score::Result<IdentifierHash> getActiveRunTarget() const noexcept override
{
return MakeUnexpected(ExecErrc::kCommunicationError);
}

[[nodiscard]] score::Result<void> setRequestedRunTarget(IdentifierHash /*run_target*/) noexcept override
{
return {};
}

void registerActiveRunTargetCallback(ActivationCallbackT /*callback*/) noexcept override {}
};

class ControlProviderUT : public ::testing::Test
{
protected:
FakeRunTargetControl graph_;
};

TEST_F(ControlProviderUT, CreateSucceeds)
{
RecordProperty("Description", "ControlProvider::Create returns a valid instance when the instance is free.");

const Result<ControlProvider*> result = ControlProvider::Create(&graph_);

ASSERT_TRUE(result.has_value());
EXPECT_NE(result.value(), nullptr);

// `ControlProvider` is intentionally never destroyed in production (see the class-level
// comment: the mw::com callbacks it registers reference it for the daemon's whole lifetime).
// Free it explicitly here so this test doesn't report that deliberate, process-lifetime
// "leak" as a `--config=asan_ubsan_lsan` finding of its own, and so the instance below is
// free to reuse the same instance specifier.
delete result.value();
}

TEST_F(ControlProviderUT, SecondCreateForSameInstanceFailsCleanly)
{
RecordProperty("Description",
"A second ControlProvider::Create for an already-offered instance fails "
"cleanly instead of crashing or hanging.");

// NOTE: this fails at LmControlSkeleton::Create() itself (an flock on a marker file), before
// Create() ever reaches the `new ControlProvider{...}` this fix wraps in a unique_ptr guard.
// It does not exercise that guard's cleanup path. Traced why no config-based trigger exists
// for the other three setup steps: SkeletonMethod::RegisterHandler (communication's
// score/mw/com/impl/bindings/lola/skeleton_method.cpp) unconditionally does
// `type_erased_callback_ = std::move(...); return {};` -- it cannot fail on this binding, so
// setupActivateRunTarget/setupGetActiveRunTarget can't either, and setupActivationResult's
// own body is an unconditional `return {};`. That leaves offerService(), whose only reachable
// failure mode here is genuine OS resource exhaustion (e.g. an artificially lowered FD
// rlimit) during SHM event-slot allocation -- deliberately not done here, since it'd depend on
// the binding's internal FD-consumption pattern and risk CI flakiness for little benefit. This
// path's correctness rests on unique_ptr's RAII guarantee rather than on an executable test.
const Result<ControlProvider*> first_result = ControlProvider::Create(&graph_);
ASSERT_TRUE(first_result.has_value());

const Result<ControlProvider*> second_result = ControlProvider::Create(&graph_);
EXPECT_FALSE(second_result.has_value());

delete first_result.value();
}

} // namespace
} // namespace score::mw::lifecycle::internal

int main(int argc, char** argv)
{
::testing::InitGoogleTest(&argc, argv);
score::mw::com::runtime::InitializeRuntime(
score::string_manipulation::GetArguments(argc, const_cast<const char**>(argv)));
return RUN_ALL_TESTS();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"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"
}
}
Loading