Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,23 @@ ProcessInfoNode::ProcessInfoNode(

IComponent::RequestResult ProcessInfoNode::tryReportCompletion(score::mw::lifecycle::ProcessState new_state)
{
if (new_state == ProcessState::kFailed)
{
// Didn't reach running or startup
return tryReportError(ComponentError::kErrorBeforeReady);
}

ProcessState desired_state{};
bool has_process_state_condition = false;

const auto& ready_condition = config_.component_properties.ready_condition;

std::visit(
[&desired_state](auto&& arg) {
[&desired_state, &has_process_state_condition](auto&& arg) {
using ReadyCondT = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<ReadyCondT, configuration::ProcessState>)
{
has_process_state_condition = true;
switch (arg)
{
case configuration::ProcessState::Running:
Expand All @@ -73,12 +81,9 @@ IComponent::RequestResult ProcessInfoNode::tryReportCompletion(score::mw::lifecy
},
ready_condition);

if (new_state == ProcessState::kFailed)
{
// Didn't reach running or startup
return tryReportError(ComponentError::kErrorBeforeReady);
}
if (new_state == desired_state)
// Reaching the desired state or beyond satisfies the ready condition: a self-terminating process
// may already have exited (kTerminated) by the time completion is reported.
if (has_process_state_condition && new_state >= desired_state)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A component can still want to go to a different state if it doesn't have a process state ready condition. This would mean that if a process_state ready condition is not set nothing will be started ever.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also comparing enums with >= shouldn't be done as it's very unclear.

{
return tryReportSuccess();
}
Expand Down Expand Up @@ -274,7 +279,12 @@ IComponent::RequestResult ProcessInfoNode::startProcess(score::cpp::stop_token s
}

setState(ProcessState::kRunning); // Can fail if we've terminated already
return tryReportCompletion(ProcessState::kRunning);

// A self-terminating process may already have exited before startup completed. tryHandleTermination()
// leaves such a node waiting for the startup thread, so report against the state actually reached.
const ProcessState reached_state =
(getState() == ProcessState::kTerminated) ? ProcessState::kTerminated : ProcessState::kRunning;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just put getState() into tryReportCompletion? This code reads as if we are in kTerminated then we've reached kTerminated, otherwise assume we reach kRunning. tryReportCompletion also has checks if it's kFailed but this would never happen then.

return tryReportCompletion(reached_state);
}

void ProcessInfoNode::setupControlClientChannel()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,32 @@ TEST_F(ProcessInfoNodeStartupTest, SelfTerminating_ExitsBeforeMapInsert_ReturnsS
ASSERT_THAT(node->getState(), Eq(score::mw::lifecycle::ProcessState::kTerminated));
}

TEST_F(ProcessInfoNodeStartupTest, SelfTerminating_TerminatedReadyCondition_ExitsBeforeMapInsert_ReturnsSuccess)
{
RecordProperty(
"Description",
"A self-terminating process whose ready condition is Terminated and that exits with status 0 before the map "
"insertion completes reports success from activate() instead of waiting forever.");

auto node = createProcessInfoNode(
configuration::ApplicationType::Native, 0U, true, configuration::ProcessState::Terminated);
// Simulate the process exiting before the map insertion happens.
EXPECT_CALL(mock_processIf_, startProcess(_, _, _))
.WillOnce(DoAll(
InvokeWithoutArgs([node = node.get()] {
static_cast<void>(node->tryHandleTermination(0));
}),
Return(osal::OsalReturnType::kSuccess)));
EXPECT_CALL(*process_map_, insertIfNotTerminated(_, _))
.WillOnce(Return(score::mw::lifecycle::internal::SafeProcessMapReturnType::kYield));

auto result = node->activate(score::cpp::stop_token{});

ASSERT_THAT(result.has_value(), IsTrue());
ASSERT_THAT(result.value(), Eq(IComponent::RequestState::kSuccess));
ASSERT_THAT(node->getState(), Eq(score::mw::lifecycle::ProcessState::kTerminated));
}

TEST_F(ProcessInfoNodeStartupTest, ActivateAlreadyActiveNode_ReturnsSuccess)
{
RecordProperty(
Expand Down
50 changes: 50 additions & 0 deletions tests/integration/rt_running_when_process_exits/BUILD

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should shorten the name to just rt. I think you could make a subfolder like tests/integration/run_target/running_when_process_exists/

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# *******************************************************************************
# 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
# *******************************************************************************
load("@rules_cc//cc:cc_binary.bzl", "cc_binary")
load("//tests/utils/bazel:integration.bzl", "integration_test")

cc_binary(
name = "filesystem_reader",
srcs = ["filesystem_reader.cpp"],
deps = [
"//score/launch_manager:lifecycle_cc",
"//tests/utils/test_helper",
"//tests/utils/test_helper:process_utils",
"@googletest//:gtest_main",
],
)

cc_binary(
name = "control_client_test_driver",
srcs = ["control_client_test_driver.cpp"],
deps = [
"//score/launch_manager:control_cc",
"//score/launch_manager:lifecycle_cc",
"//tests/utils/test_helper",
"@googletest//:gtest_main",
],
)

integration_test(
name = "rt_running_when_process_exits",
timeout = "short",
srcs = ["rt_running_when_process_exits.py"],
binaries = [
":control_client_test_driver",
":filesystem_reader",
":setup_filesystem.sh",
":slow_setup.sh",
"//score/launch_manager",
],
config = ":rt_running_when_process_exits.json",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/********************************************************************************
* 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 <gtest/gtest.h>

#include <filesystem>
#include <string_view>

#include "tests/utils/test_helper/test_helper.hpp"
#include <score/mw/lifecycle/control_client.h>
#include <score/mw/lifecycle/report_running.h>

namespace
{
/// @brief Marker file written by slow_setup.sh once it has finished (and is about to exit).
constexpr std::string_view kSlowSetupOutput = "slow_setup_output.txt";
} // namespace

// Given a configuration with two run targets, each pulling in a self-terminating component whose
// ready condition is "Terminated" but which differ in whether that component has a dependent:
//
// - run_target_reader: filesystem_reader (ready "Running") depends on setup_filesystem_sh
// (self-terminating, ready "Terminated"). The terminated-ready
// component HAS a dependent.
// - run_target_slow_setup: depends directly on slow_setup_sh (self-terminating, ready
// "Terminated") which has NO dependent component.
//
// In both cases the run target must only report success once the terminated-ready component's
// process has actually exited. Without the fix, graph accounting for such a node happens as soon as
// the process is *started*, so ActivateRunTarget(...).Get() returns while the script is still
// running and its marker file has not been written yet.
TEST(RtRunningWhenProcessExits, ControlClientTestDriver)
{
score::mw::lifecycle::ControlClient client;
score::cpp::stop_token stop_token;

// kSlowSetupOutput is checked too: its later presence must be a reliable signal that
// slow_setup.sh terminated during *this* run, not leftover from a previous one.
ASSERT_TRUE(check_clean({test_end_location, kSlowSetupOutput}));

TEST_STEP("Report running")
{
score::mw::lifecycle::report_running();
}

// The with-dependents case: filesystem_reader asserts on the prepared file and on the setup
// script process being gone, so the ordering is checked there.
TEST_STEP("Activate run target with a terminated-ready component that HAS a dependent")
{
auto result = client.ActivateRunTarget("run_target_reader").Get(stop_token);
EXPECT_TRUE(result.has_value()) << "Activating run_target_reader failed: " << result.error().Message();
}

// The no-dependents case: activation must only complete once slow_setup.sh has terminated.
TEST_STEP("Activate run target with a terminated-ready component that has NO dependent")
{
auto result = client.ActivateRunTarget("run_target_slow_setup").Get(stop_token);
EXPECT_TRUE(result.has_value()) << "Activating run_target_slow_setup failed: " << result.error().Message();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does << result.error() not work? I think some errors can have extra info that doing .Message hides.

}

TEST_STEP("Verify slow_setup.sh had terminated before activation completed")
{
EXPECT_TRUE(std::filesystem::exists(kSlowSetupOutput))
<< "run_target_slow_setup reported success while slow_setup.sh was still running: its "
"output file has not been written yet. A run target depending on a terminated-ready "
"component must only become ready once that component's process has actually exited.";
}

TEST_STEP("Activate run target Off")
{
client.ActivateRunTarget("Off");
}
}

int main()
{
return TestRunner(__FILE__, TerminationBehavior::kWait, TerminationNotification::kTestEnd).RunTests();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/********************************************************************************
* 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 <gtest/gtest.h>

#include <filesystem>
#include <fstream>
#include <string>
#include <string_view>

#include "tests/utils/test_helper/process_utils.hpp"
#include "tests/utils/test_helper/test_helper.hpp"
#include <score/mw/lifecycle/report_running.h>

namespace
{

constexpr std::string_view kSetupScriptName = "setup_filesystem.sh";
constexpr std::string_view kSetupOutputFile = "setup_filesystem_output.txt";

} // namespace

// Given a configuration with:
// - A self-terminating component "setup_filesystem_sh" (wrapping setup_filesystem.sh) whose
// ready condition is "Terminated".
// - A component "filesystem_reader" that depends on "setup_filesystem_sh".
// - An initial Run Target "Startup" that depends on "filesystem_reader".
//
// When the Launch Manager activates "Startup", it must first run setup_filesystem.sh to completion
// (the script writes a marker file and exits) before starting filesystem_reader.
TEST(RtRunningWhenProcessExits, FilesystemReader)
{
ASSERT_TRUE(check_clean({test_end_location}));

TEST_STEP("Report running")
{
score::mw::lifecycle::report_running();
}

TEST_STEP("Read file prepared by setup_filesystem.sh")
{
ASSERT_TRUE(std::filesystem::exists(kSetupOutputFile))
<< "The file prepared by setup_filesystem.sh does not exist; the dependency was not "
"started/finished before filesystem_reader";

std::ifstream output{std::filesystem::path{kSetupOutputFile}};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think initializing here a std::fs::path isn't really doing anything. This is just going to make the path and return the underlying string.

ASSERT_TRUE(output.is_open()) << "Could not open " << kSetupOutputFile;
std::string content;
std::getline(output, content);
EXPECT_EQ(content, "filesystem is ready") << "Unexpected content in " << kSetupOutputFile;
}

TEST_STEP("Verify setup_filesystem.sh process has already terminated")
{
EXPECT_FALSE(test_helper::process_is_running(kSetupScriptName))
<< "The setup_filesystem.sh process is still running; filesystem_reader was started "
"before its dependency terminated";
}
}

int main()
{
// test_end is signalled by control_client_test_driver, which orchestrates the run target switches.
TestRunner runner{__FILE__, TerminationBehavior::kContinue, TerminationNotification::kNone};
return runner.RunTests();
}
Loading
Loading