From d574fc51c62e1f7b25640bdf2783d7d4e7d518c2 Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Wed, 19 Aug 2026 14:11:13 +0100 Subject: [PATCH 01/16] Handling variant rc --- .../details/process_info_node.cpp | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp index 754e9b1bf..d68bcc6c8 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp @@ -20,6 +20,7 @@ #include #include #include +#include namespace score::mw::lifecycle::internal { @@ -51,7 +52,7 @@ ProcessInfoNode::ProcessInfoNode( IComponent::RequestResult ProcessInfoNode::tryReportCompletion(score::mw::lifecycle::ProcessState new_state) { - ProcessState desired_state{}; + ProcessState desired_state{ProcessState::kRunning}; const auto& ready_condition = config_.component_properties.ready_condition; @@ -287,13 +288,27 @@ score::cpp::expected_blank ProcessInfoNode::handlePr const score::cpp::stop_token& stop_token) { static_cast(stop_token); // Not yet supported + const bool is_native = + configuration::ApplicationType::Native == config_.component_properties.application_profile.application_type; + bool ready_condition_met = false; - if (((configuration::ApplicationType::Native == - config_.component_properties.application_profile.application_type) || - (process_handling_.process_interface_->waitForkRunning( - sync_, std::chrono::milliseconds(config_.deployment_config.ready_timeout_ms)) == - osal::OsalReturnType::kSuccess)) && - (0 == status_)) + std::visit( + [this, &ready_condition_met](auto&& arg) { + using T = std::decay_t; + + if constexpr (std::is_same_v) + { + auto wait_res = process_handling_.process_interface_->waitForkRunning( + sync_, std::chrono::milliseconds(config_.deployment_config.ready_timeout_ms)); + ready_condition_met = wait_res == osal::OsalReturnType::kSuccess && 0 == status_; + } + else if constexpr (std::is_same_v) + { + } + }, + config_.component_properties.ready_condition.value()); + + if (is_native || ready_condition_met) { handleProcessRunning(); return {}; From acfdeaf5d48596b69a2e5ce21ff22bf344b55e8e Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Thu, 20 Aug 2026 08:12:45 +0100 Subject: [PATCH 02/16] some stuff --- .../launch_manager/src/daemon/src/osal/BUILD | 24 +++ .../src/osal/details/posix/wait_for_file.cpp | 93 ++++++++++++ .../osal/details/posix/wait_for_file_UT.cpp | 137 ++++++++++++++++++ .../src/daemon/src/osal/wait_for_file.hpp | 73 ++++++++++ 4 files changed, 327 insertions(+) create mode 100644 score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp create mode 100644 score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp create mode 100644 score/launch_manager/src/daemon/src/osal/wait_for_file.hpp diff --git a/score/launch_manager/src/daemon/src/osal/BUILD b/score/launch_manager/src/daemon/src/osal/BUILD index 39a4e849e..88d2fd254 100644 --- a/score/launch_manager/src/daemon/src/osal/BUILD +++ b/score/launch_manager/src/daemon/src/osal/BUILD @@ -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 = "return_types", @@ -33,6 +34,28 @@ cc_library( deps = [":return_types"], ) +cc_library( + name = "wait_for_file", + srcs = ["details/posix/wait_for_file.cpp"], + hdrs = [ + "return_types.hpp", + "wait_for_file.hpp", + ], + include_prefix = "score/mw/launch_manager/osal", + strip_include_prefix = "/score/launch_manager/src/daemon/src/osal", + visibility = ["//score:__subpackages__"], + deps = [":return_types"], +) + +lm_cc_test( + name = "wait_for_file_UT", + srcs = ["details/posix/wait_for_file_UT.cpp"], + deps = [ + ":wait_for_file", + "@googletest//:gtest_main", + ], +) + cc_library( name = "sys_exit", srcs = ["details/posix/sys_exit.cpp"], @@ -127,5 +150,6 @@ cc_library( ":set_affinity", ":set_groups", ":sys_exit", + ":wait_for_file", ], ) diff --git a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp new file mode 100644 index 000000000..ee496729f --- /dev/null +++ b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp @@ -0,0 +1,93 @@ +/******************************************************************************** + * 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 +#include + +#include +#include +#include +#include +#include +#include + +#include "score/mw/launch_manager/osal/wait_for_file.hpp" + +namespace score::mw::lifecycle::internal::osal +{ + +namespace +{ + +/// @brief Copy a possibly non null terminated path into a null terminated buffer. +/// @return true if the path fits into the buffer, false otherwise. +bool toNullTerminatedPath(std::string_view path, std::array& buffer) noexcept +{ + // One byte is reserved for the terminator, which is already in place because the buffer is value initialised. + if (path.empty() || (path.size() >= buffer.size())) + { + return false; + } + + static_cast(std::copy(path.cbegin(), path.cend(), buffer.begin())); + return true; +} + +} // namespace + +OsalReturnType wait_for_file( + std::chrono::milliseconds timeout, + std::string_view path, + std::chrono::milliseconds poll_interval) noexcept +{ + std::array path_buffer{}; + if (!toNullTerminatedPath(path, path_buffer)) + { + return OsalReturnType::kFail; + } + + // Calculate when the timeout will be reached. This avoids accumulating errors because `sleep_for` may block for + // longer than requested. + const auto deadline = std::chrono::steady_clock::now() + timeout; + + while (true) + { + struct stat info{}; + + if (stat(path_buffer.data(), &info) == 0) + { + return OsalReturnType::kSuccess; + } + + switch (errno) + { + // The path, or one of its parent directories, does not exist yet. This is what we are waiting for. + case (ENOENT): + case (ENOTDIR): + case (EINTR): + break; + + default: + return OsalReturnType::kFail; + } + + if (std::chrono::steady_clock::now() >= deadline) + { + return OsalReturnType::kTimeout; + } + + std::this_thread::sleep_for(poll_interval); + } +} + +} // namespace score::mw::lifecycle::internal::osal diff --git a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp new file mode 100644 index 000000000..85e3e77c1 --- /dev/null +++ b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp @@ -0,0 +1,137 @@ +/******************************************************************************** + * 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 + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "score/mw/launch_manager/osal/wait_for_file.hpp" + +using score::mw::lifecycle::internal::osal::OsalReturnType; +using score::mw::lifecycle::internal::osal::wait_for_file; + +namespace +{ + +constexpr std::chrono::milliseconds kPollInterval{1U}; + +class WaitForFileTest : public ::testing::Test +{ + protected: + void SetUp() override + { + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "explorative-testing"); + + path_ = std::string{::testing::TempDir()} + "wait_for_file_UT_" + std::to_string(getpid()); + static_cast(std::remove(path_.c_str())); + } + + void TearDown() override + { + static_cast(std::remove(path_.c_str())); + } + + void createFile() const + { + std::ofstream file{path_}; + ASSERT_TRUE(file.is_open()); + } + + std::string path_{}; +}; + +TEST_F(WaitForFileTest, ExistingFileIsReportedImmediately) +{ + RecordProperty("Description", "Verify that a file which already exists is detected without waiting."); + + createFile(); + + const auto start = std::chrono::steady_clock::now(); + EXPECT_EQ(wait_for_file(std::chrono::milliseconds{5000U}, path_, kPollInterval), OsalReturnType::kSuccess); + EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::milliseconds{1000U}); +} + +TEST_F(WaitForFileTest, ZeroTimeoutStillChecksOnce) +{ + RecordProperty("Description", "Verify that a timeout of zero performs a single existence check."); + + createFile(); + + EXPECT_EQ(wait_for_file(std::chrono::milliseconds{0U}, path_, kPollInterval), OsalReturnType::kSuccess); +} + +TEST_F(WaitForFileTest, MissingFileTimesOut) +{ + RecordProperty( + "Description", "Verify that waiting for a file which never appears returns kTimeout after the given duration."); + + const auto timeout = std::chrono::milliseconds{50U}; + const auto start = std::chrono::steady_clock::now(); + EXPECT_EQ(wait_for_file(timeout, path_, kPollInterval), OsalReturnType::kTimeout); + EXPECT_GE(std::chrono::steady_clock::now() - start, timeout); +} + +TEST_F(WaitForFileTest, MissingParentDirectoryTimesOut) +{ + RecordProperty("Description", "Verify that a path below a non existing directory is treated as not yet created."); + + const std::string path = path_ + "/no_such_directory/file"; + EXPECT_EQ(wait_for_file(std::chrono::milliseconds{10U}, path, kPollInterval), OsalReturnType::kTimeout); +} + +TEST_F(WaitForFileTest, FileCreatedWhileWaitingIsDetected) +{ + RecordProperty("Description", "Verify that a file created by another thread during the wait is detected."); + + std::thread creator{[this]() { + std::this_thread::sleep_for(std::chrono::milliseconds{20U}); + createFile(); + }}; + + EXPECT_EQ(wait_for_file(std::chrono::milliseconds{5000U}, path_, kPollInterval), OsalReturnType::kSuccess); + creator.join(); +} + +TEST_F(WaitForFileTest, DirectoryCountsAsExisting) +{ + RecordProperty("Description", "Verify that the check is not restricted to regular files."); + + EXPECT_EQ( + wait_for_file(std::chrono::milliseconds{0U}, ::testing::TempDir(), kPollInterval), OsalReturnType::kSuccess); +} + +TEST_F(WaitForFileTest, EmptyPathFails) +{ + RecordProperty("Description", "Verify that an empty path is rejected instead of being polled."); + + EXPECT_EQ( + wait_for_file(std::chrono::milliseconds{5000U}, std::string_view{}, kPollInterval), OsalReturnType::kFail); +} + +TEST_F(WaitForFileTest, TooLongPathFails) +{ + RecordProperty("Description", "Verify that a path which does not fit into PATH_MAX is rejected."); + + const std::string path(PATH_MAX + 1U, 'a'); + EXPECT_EQ(wait_for_file(std::chrono::milliseconds{5000U}, path, kPollInterval), OsalReturnType::kFail); +} + +} // namespace diff --git a/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp b/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp new file mode 100644 index 000000000..c0c499c38 --- /dev/null +++ b/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp @@ -0,0 +1,73 @@ +/******************************************************************************** + * 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 OSAL_WAIT_FOR_FILE_HPP_INCLUDED +#define OSAL_WAIT_FOR_FILE_HPP_INCLUDED + +#include +#include + +#include "return_types.hpp" + +namespace score +{ + +namespace mw::lifecycle +{ + +namespace internal +{ + +namespace osal +{ + +/// @brief Default interval between two consecutive `stat` calls performed by wait_for_file(). +// coverity[autosar_cpp14_m3_4_1_violation:INTENTIONAL] The value is used in a global context. +constexpr std::chrono::milliseconds kDefaultFilePollInterval{2U}; + +/// @brief Block until the given path exists or the timeout expires. +/// +/// The path is polled with `stat` because neither Linux nor QNX offer a portable way of waiting on the creation of a +/// single path. A monotonic clock (`std::chrono::steady_clock`) is used for the deadline so that a change of the +/// system clock by another thread cannot extend or shorten the wait. +/// - `stat` returns 0 if the path could be resolved, meaning the file exists. +/// - `stat` returns -1 with `errno` set to ENOENT/ENOTDIR while the path (or one of its parent directories) +/// does not exist yet, which is the case this function waits for. +/// - Any other `errno` (e.g. EACCES, ELOOP) is a permanent error and aborts the wait. +/// - https://pubs.opengroup.org/onlinepubs/9699919799/functions/stat.html +/// +/// The path is checked once before the first sleep, so a timeout of zero performs a single existence check. +/// No distinction is made between file types: a directory, a FIFO or a socket at @p path also counts as existing. +/// +/// @param timeout The maximum time to wait for the path to appear. +/// @param path The path to wait for. It does not need to be null terminated, but it must be shorter than PATH_MAX. +/// @param poll_interval The time to sleep between two consecutive `stat` calls. +/// @return An OsalReturnType indicating the result of the operation. +/// - `OsalReturnType::kSuccess`: The path exists. +/// - `OsalReturnType::kTimeout`: The path did not appear within the specified time. +/// - `OsalReturnType::kFail`: The path is empty, is too long, or could not be queried (e.g. permission +/// denied on one of the parent directories). +OsalReturnType wait_for_file( + std::chrono::milliseconds timeout, + std::string_view path, + std::chrono::milliseconds poll_interval = kDefaultFilePollInterval) noexcept; + +} // namespace osal + +} // namespace internal + +} // namespace mw::lifecycle + +} // namespace score + +#endif // OSAL_WAIT_FOR_FILE_HPP_INCLUDED From 7a1acd8ab146a2b1ec6e4ec5a763a81ae57cd239 Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Thu, 20 Aug 2026 12:28:05 +0100 Subject: [PATCH 03/16] inital impl --- .../launch_manager/src/daemon/src/osal/BUILD | 7 +- .../src/osal/details/posix/wait_for_file.cpp | 76 +++---- .../osal/details/posix/wait_for_file_UT.cpp | 208 ++++++++++++++++-- .../src/daemon/src/osal/wait_for_file.hpp | 58 ++--- .../src/process_group_manager/details/BUILD | 1 + .../details/process_info_node.cpp | 28 ++- tests/integration/ready_condition_file/BUILD | 42 ++++ .../file_creating_process.cpp | 55 +++++ .../ready_condition_file.json | 103 +++++++++ .../ready_condition_file.py | 47 ++++ .../ready_file_verification_process.cpp | 51 +++++ 11 files changed, 572 insertions(+), 104 deletions(-) create mode 100644 tests/integration/ready_condition_file/BUILD create mode 100644 tests/integration/ready_condition_file/file_creating_process.cpp create mode 100644 tests/integration/ready_condition_file/ready_condition_file.json create mode 100644 tests/integration/ready_condition_file/ready_condition_file.py create mode 100644 tests/integration/ready_condition_file/ready_file_verification_process.cpp diff --git a/score/launch_manager/src/daemon/src/osal/BUILD b/score/launch_manager/src/daemon/src/osal/BUILD index 88d2fd254..bc8b62e6e 100644 --- a/score/launch_manager/src/daemon/src/osal/BUILD +++ b/score/launch_manager/src/daemon/src/osal/BUILD @@ -44,7 +44,11 @@ cc_library( include_prefix = "score/mw/launch_manager/osal", strip_include_prefix = "/score/launch_manager/src/daemon/src/osal", visibility = ["//score:__subpackages__"], - deps = [":return_types"], + deps = [ + ":return_types", + "//score/launch_manager/src/daemon/src/configuration:component_config", + "@score_baselibs//score/language/futurecpp", + ], ) lm_cc_test( @@ -53,6 +57,7 @@ lm_cc_test( deps = [ ":wait_for_file", "@googletest//:gtest_main", + "@score_baselibs//score/language/futurecpp", ], ) diff --git a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp index ee496729f..bbc968b3c 100644 --- a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp +++ b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp @@ -12,10 +12,8 @@ ********************************************************************************/ #include -#include #include -#include #include #include #include @@ -26,67 +24,65 @@ namespace score::mw::lifecycle::internal::osal { -namespace -{ - -/// @brief Copy a possibly non null terminated path into a null terminated buffer. -/// @return true if the path fits into the buffer, false otherwise. -bool toNullTerminatedPath(std::string_view path, std::array& buffer) noexcept -{ - // One byte is reserved for the terminator, which is already in place because the buffer is value initialised. - if (path.empty() || (path.size() >= buffer.size())) - { - return false; - } - - static_cast(std::copy(path.cbegin(), path.cend(), buffer.begin())); - return true; -} - -} // namespace - OsalReturnType wait_for_file( - std::chrono::milliseconds timeout, std::string_view path, + configuration::FileExistenceState condition, + std::chrono::milliseconds timeout, std::chrono::milliseconds poll_interval) noexcept { - std::array path_buffer{}; - if (!toNullTerminatedPath(path, path_buffer)) + // note: QNX has a wait_for API, however this + + // The terminator is expected right behind the view, so it is not part of its size. + if (path.empty() || (path.data()[path.size()] != '\0')) { return OsalReturnType::kFail; } - // Calculate when the timeout will be reached. This avoids accumulating errors because `sleep_for` may block for - // longer than requested. + const bool wait_for_existence = (condition == configuration::FileExistenceState::Exists); const auto deadline = std::chrono::steady_clock::now() + timeout; while (true) { struct stat info{}; - if (stat(path_buffer.data(), &info) == 0) + if (stat(path.data(), &info) == 0) { - return OsalReturnType::kSuccess; + if (wait_for_existence) + { + return OsalReturnType::kSuccess; + } } - - switch (errno) + else { - // The path, or one of its parent directories, does not exist yet. This is what we are waiting for. - case (ENOENT): - case (ENOTDIR): - case (EINTR): - break; - - default: - return OsalReturnType::kFail; + switch (errno) + { + // The path, or one of its parent directories, does not exist. + case (ENOENT): + case (ENOTDIR): + if (!wait_for_existence) + { + return OsalReturnType::kSuccess; + } + break; + + // The query was interrupted, so it says nothing about the path. Simply retry it. + case (EINTR): + break; + + default: + return OsalReturnType::kFail; + } } - if (std::chrono::steady_clock::now() >= deadline) + const auto now = std::chrono::steady_clock::now(); + if (now >= deadline) { return OsalReturnType::kTimeout; } - std::this_thread::sleep_for(poll_interval); + // Never sleep past the deadline, so the timeout is honoured even with a coarse poll interval. + const auto remaining = deadline - now; + std::this_thread::sleep_for(std::min(poll_interval, remaining)); } } diff --git a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp index 85e3e77c1..8ec8936bd 100644 --- a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp +++ b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp @@ -15,6 +15,8 @@ #include #include +#include + #include #include #include @@ -24,6 +26,7 @@ #include "score/mw/launch_manager/osal/wait_for_file.hpp" +using score::mw::lifecycle::internal::osal::FileWaitCondition; using score::mw::lifecycle::internal::osal::OsalReturnType; using score::mw::lifecycle::internal::osal::wait_for_file; @@ -32,6 +35,12 @@ namespace constexpr std::chrono::milliseconds kPollInterval{1U}; +/// Long enough that the tests below never hit it unintentionally; the timeout itself is covered by dedicated tests. +constexpr std::chrono::milliseconds kWaitTimeout{60000U}; + +constexpr auto kExists = FileWaitCondition::kExists; +constexpr auto kNotExisting = FileWaitCondition::kNotExisting; + class WaitForFileTest : public ::testing::Test { protected: @@ -55,7 +64,13 @@ class WaitForFileTest : public ::testing::Test ASSERT_TRUE(file.is_open()); } + void removeFile() const + { + ASSERT_EQ(std::remove(path_.c_str()), 0); + } + std::string path_{}; + score::cpp::stop_source stop_source_{}; }; TEST_F(WaitForFileTest, ExistingFileIsReportedImmediately) @@ -65,28 +80,47 @@ TEST_F(WaitForFileTest, ExistingFileIsReportedImmediately) createFile(); const auto start = std::chrono::steady_clock::now(); - EXPECT_EQ(wait_for_file(std::chrono::milliseconds{5000U}, path_, kPollInterval), OsalReturnType::kSuccess); + EXPECT_EQ( + wait_for_file(stop_source_.get_token(), path_, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::milliseconds{1000U}); } -TEST_F(WaitForFileTest, ZeroTimeoutStillChecksOnce) +TEST_F(WaitForFileTest, AlreadyRequestedStopStillChecksOnce) { - RecordProperty("Description", "Verify that a timeout of zero performs a single existence check."); + RecordProperty("Description", "Verify that an already stopped token performs a single existence check."); createFile(); + static_cast(stop_source_.request_stop()); - EXPECT_EQ(wait_for_file(std::chrono::milliseconds{0U}, path_, kPollInterval), OsalReturnType::kSuccess); + EXPECT_EQ( + wait_for_file(stop_source_.get_token(), path_, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); } -TEST_F(WaitForFileTest, MissingFileTimesOut) +TEST_F(WaitForFileTest, MissingFileReturnsTimeoutOnStopRequest) { RecordProperty( - "Description", "Verify that waiting for a file which never appears returns kTimeout after the given duration."); + "Description", + "Verify that waiting for a file which never appears returns kTimeout once a stop is " + "requested."); - const auto timeout = std::chrono::milliseconds{50U}; - const auto start = std::chrono::steady_clock::now(); - EXPECT_EQ(wait_for_file(timeout, path_, kPollInterval), OsalReturnType::kTimeout); - EXPECT_GE(std::chrono::steady_clock::now() - start, timeout); + static_cast(stop_source_.request_stop()); + + EXPECT_EQ( + wait_for_file(stop_source_.get_token(), path_, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kTimeout); +} + +TEST_F(WaitForFileTest, StopRequestedWhileWaitingEndsTheWait) +{ + RecordProperty("Description", "Verify that a stop requested by another thread during the wait ends it."); + + std::thread stopper{[this]() { + std::this_thread::sleep_for(std::chrono::milliseconds{20U}); + static_cast(stop_source_.request_stop()); + }}; + + EXPECT_EQ( + wait_for_file(stop_source_.get_token(), path_, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kTimeout); + stopper.join(); } TEST_F(WaitForFileTest, MissingParentDirectoryTimesOut) @@ -94,7 +128,10 @@ TEST_F(WaitForFileTest, MissingParentDirectoryTimesOut) RecordProperty("Description", "Verify that a path below a non existing directory is treated as not yet created."); const std::string path = path_ + "/no_such_directory/file"; - EXPECT_EQ(wait_for_file(std::chrono::milliseconds{10U}, path, kPollInterval), OsalReturnType::kTimeout); + static_cast(stop_source_.request_stop()); + + EXPECT_EQ( + wait_for_file(stop_source_.get_token(), path, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kTimeout); } TEST_F(WaitForFileTest, FileCreatedWhileWaitingIsDetected) @@ -106,7 +143,8 @@ TEST_F(WaitForFileTest, FileCreatedWhileWaitingIsDetected) createFile(); }}; - EXPECT_EQ(wait_for_file(std::chrono::milliseconds{5000U}, path_, kPollInterval), OsalReturnType::kSuccess); + EXPECT_EQ( + wait_for_file(stop_source_.get_token(), path_, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); creator.join(); } @@ -115,7 +153,132 @@ TEST_F(WaitForFileTest, DirectoryCountsAsExisting) RecordProperty("Description", "Verify that the check is not restricted to regular files."); EXPECT_EQ( - wait_for_file(std::chrono::milliseconds{0U}, ::testing::TempDir(), kPollInterval), OsalReturnType::kSuccess); + wait_for_file(stop_source_.get_token(), ::testing::TempDir(), kExists, kWaitTimeout, kPollInterval), + OsalReturnType::kSuccess); +} + +TEST_F(WaitForFileTest, AbsentFileIsReportedImmediatelyForNotExisting) +{ + RecordProperty("Description", "Verify that a file which is already absent satisfies kNotExisting without waiting."); + + const auto start = std::chrono::steady_clock::now(); + EXPECT_EQ( + wait_for_file(stop_source_.get_token(), path_, kNotExisting, kWaitTimeout, kPollInterval), + OsalReturnType::kSuccess); + EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::milliseconds{1000U}); +} + +TEST_F(WaitForFileTest, FileRemovedWhileWaitingIsDetected) +{ + RecordProperty( + "Description", "Verify that a file removed by another thread during the wait satisfies kNotExisting."); + + createFile(); + + std::thread remover{[this]() { + std::this_thread::sleep_for(std::chrono::milliseconds{20U}); + removeFile(); + }}; + + EXPECT_EQ( + wait_for_file(stop_source_.get_token(), path_, kNotExisting, kWaitTimeout, kPollInterval), + OsalReturnType::kSuccess); + remover.join(); +} + +TEST_F(WaitForFileTest, ExistingFileTimesOutForNotExisting) +{ + RecordProperty("Description", "Verify that a file which never disappears returns kTimeout for kNotExisting."); + + createFile(); + constexpr std::chrono::milliseconds timeout{50U}; + + const auto start = std::chrono::steady_clock::now(); + EXPECT_EQ( + wait_for_file(stop_source_.get_token(), path_, kNotExisting, timeout, kPollInterval), OsalReturnType::kTimeout); + EXPECT_GE(std::chrono::steady_clock::now() - start, timeout); +} + +TEST_F(WaitForFileTest, MissingParentDirectoryCountsAsNotExisting) +{ + RecordProperty("Description", "Verify that a path below a non existing directory satisfies kNotExisting."); + + const std::string path = path_ + "/no_such_directory/file"; + + EXPECT_EQ( + wait_for_file(stop_source_.get_token(), path, kNotExisting, kWaitTimeout, kPollInterval), + OsalReturnType::kSuccess); +} + +TEST_F(WaitForFileTest, StopRequestedWhileWaitingForRemovalEndsTheWait) +{ + RecordProperty("Description", "Verify that a stop request also ends a wait for a file to disappear."); + + createFile(); + + std::thread stopper{[this]() { + std::this_thread::sleep_for(std::chrono::milliseconds{20U}); + static_cast(stop_source_.request_stop()); + }}; + + EXPECT_EQ( + wait_for_file(stop_source_.get_token(), path_, kNotExisting, kWaitTimeout, kPollInterval), + OsalReturnType::kTimeout); + stopper.join(); +} + +TEST_F(WaitForFileTest, EmptyPathFailsForNotExisting) +{ + RecordProperty("Description", "Verify that an empty path is rejected for kNotExisting instead of being polled."); + + EXPECT_EQ( + wait_for_file(stop_source_.get_token(), std::string_view{}, kNotExisting, kWaitTimeout, kPollInterval), + OsalReturnType::kFail); +} + +TEST_F(WaitForFileTest, MissingFileReturnsTimeoutWhenTheTimeoutElapses) +{ + RecordProperty("Description", "Verify that the wait ends with kTimeout once the given timeout has elapsed."); + + constexpr std::chrono::milliseconds timeout{50U}; + + const auto start = std::chrono::steady_clock::now(); + EXPECT_EQ( + wait_for_file(stop_source_.get_token(), path_, kExists, timeout, kPollInterval), OsalReturnType::kTimeout); + EXPECT_GE(std::chrono::steady_clock::now() - start, timeout); +} + +TEST_F(WaitForFileTest, ZeroTimeoutStillChecksOnce) +{ + RecordProperty("Description", "Verify that an existing file is detected even with a zero timeout."); + + createFile(); + + EXPECT_EQ( + wait_for_file(stop_source_.get_token(), path_, kExists, std::chrono::milliseconds{0U}, kPollInterval), + OsalReturnType::kSuccess); +} + +TEST_F(WaitForFileTest, ZeroTimeoutOnMissingFileReturnsTimeout) +{ + RecordProperty("Description", "Verify that a zero timeout does not wait for a file which does not exist yet."); + + EXPECT_EQ( + wait_for_file(stop_source_.get_token(), path_, kExists, std::chrono::milliseconds{0U}, kPollInterval), + OsalReturnType::kTimeout); +} + +TEST_F(WaitForFileTest, PollIntervalDoesNotExtendTheTimeout) +{ + RecordProperty("Description", "Verify that a poll interval longer than the timeout does not delay the kTimeout."); + + constexpr std::chrono::milliseconds timeout{20U}; + constexpr std::chrono::milliseconds poll_interval{5000U}; + + const auto start = std::chrono::steady_clock::now(); + EXPECT_EQ( + wait_for_file(stop_source_.get_token(), path_, kExists, timeout, poll_interval), OsalReturnType::kTimeout); + EXPECT_LT(std::chrono::steady_clock::now() - start, poll_interval); } TEST_F(WaitForFileTest, EmptyPathFails) @@ -123,7 +286,21 @@ TEST_F(WaitForFileTest, EmptyPathFails) RecordProperty("Description", "Verify that an empty path is rejected instead of being polled."); EXPECT_EQ( - wait_for_file(std::chrono::milliseconds{5000U}, std::string_view{}, kPollInterval), OsalReturnType::kFail); + wait_for_file(stop_source_.get_token(), std::string_view{}, kExists, kWaitTimeout, kPollInterval), + OsalReturnType::kFail); +} + +TEST_F(WaitForFileTest, NonNullTerminatedPathFails) +{ + RecordProperty("Description", "Verify that a path which is not null terminated is rejected instead of polled."); + + createFile(); + const std::string path = path_ + "x"; + const std::string_view not_terminated{path.data(), path.size() - 1U}; + + EXPECT_EQ( + wait_for_file(stop_source_.get_token(), not_terminated, kExists, kWaitTimeout, kPollInterval), + OsalReturnType::kFail); } TEST_F(WaitForFileTest, TooLongPathFails) @@ -131,7 +308,8 @@ TEST_F(WaitForFileTest, TooLongPathFails) RecordProperty("Description", "Verify that a path which does not fit into PATH_MAX is rejected."); const std::string path(PATH_MAX + 1U, 'a'); - EXPECT_EQ(wait_for_file(std::chrono::milliseconds{5000U}, path, kPollInterval), OsalReturnType::kFail); + EXPECT_EQ( + wait_for_file(stop_source_.get_token(), path, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kFail); } } // namespace diff --git a/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp b/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp index c0c499c38..3899dd8b8 100644 --- a/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp +++ b/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp @@ -14,60 +14,30 @@ #ifndef OSAL_WAIT_FOR_FILE_HPP_INCLUDED #define OSAL_WAIT_FOR_FILE_HPP_INCLUDED +#include + +#include "score/mw/launch_manager/configuration/component_config.hpp" #include +#include #include #include "return_types.hpp" -namespace score -{ - -namespace mw::lifecycle -{ - -namespace internal -{ - -namespace osal +namespace score::mw::lifecycle::internal::osal { -/// @brief Default interval between two consecutive `stat` calls performed by wait_for_file(). -// coverity[autosar_cpp14_m3_4_1_violation:INTENTIONAL] The value is used in a global context. -constexpr std::chrono::milliseconds kDefaultFilePollInterval{2U}; - -/// @brief Block until the given path exists or the timeout expires. +/// @brief Block until the given path reaches the requested state, the timeout elapses, or a stop is requested. /// -/// The path is polled with `stat` because neither Linux nor QNX offer a portable way of waiting on the creation of a -/// single path. A monotonic clock (`std::chrono::steady_clock`) is used for the deadline so that a change of the -/// system clock by another thread cannot extend or shorten the wait. -/// - `stat` returns 0 if the path could be resolved, meaning the file exists. -/// - `stat` returns -1 with `errno` set to ENOENT/ENOTDIR while the path (or one of its parent directories) -/// does not exist yet, which is the case this function waits for. -/// - Any other `errno` (e.g. EACCES, ELOOP) is a permanent error and aborts the wait. -/// - https://pubs.opengroup.org/onlinepubs/9699919799/functions/stat.html -/// -/// The path is checked once before the first sleep, so a timeout of zero performs a single existence check. -/// No distinction is made between file types: a directory, a FIFO or a socket at @p path also counts as existing. -/// -/// @param timeout The maximum time to wait for the path to appear. -/// @param path The path to wait for. It does not need to be null terminated, but it must be shorter than PATH_MAX. -/// @param poll_interval The time to sleep between two consecutive `stat` calls. -/// @return An OsalReturnType indicating the result of the operation. -/// - `OsalReturnType::kSuccess`: The path exists. -/// - `OsalReturnType::kTimeout`: The path did not appear within the specified time. -/// - `OsalReturnType::kFail`: The path is empty, is too long, or could not be queried (e.g. permission -/// denied on one of the parent directories). +/// @param path The path to wait for. It must be null terminated. +/// @param condition The path state to wait for. +/// @param timeout The maximum time to wait for the condition. +/// @param poll_interval The time between two consecutive existence checks. OsalReturnType wait_for_file( - std::chrono::milliseconds timeout, std::string_view path, - std::chrono::milliseconds poll_interval = kDefaultFilePollInterval) noexcept; - -} // namespace osal - -} // namespace internal - -} // namespace mw::lifecycle + configuration::FileExistenceState condition, + std::chrono::milliseconds timeout, + std::chrono::milliseconds poll_interval) noexcept; -} // namespace score +} // namespace score::mw::lifecycle::internal::osal #endif // OSAL_WAIT_FOR_FILE_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 de31199ee..869cdc90a 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 @@ -164,6 +164,7 @@ cc_library( "//score/launch_manager/src/daemon/src/control:control_client_channel", "//score/launch_manager/src/daemon/src/osal:ipc_comms", "//score/launch_manager/src/daemon/src/osal:semaphore", + "//score/launch_manager/src/daemon/src/osal:wait_for_file", "//score/launch_manager/src/daemon/src/process_group_manager:iprocess", "//score/launch_manager/src/daemon/src/process_group_manager:process_state", "//score/launch_manager/src/daemon/src/supervision_control_client:isupervision_event_publisher", diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp index d68bcc6c8..0199cad89 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp @@ -16,6 +16,7 @@ #include "score/mw/launch_manager/common/alive_interface_path.hpp" #include "score/mw/launch_manager/common/log.hpp" #include "score/mw/launch_manager/osal/ipc_comms.hpp" +#include "score/mw/launch_manager/osal/wait_for_file.hpp" #include "score/mw/launch_manager/process_group_manager/details/safe_process_map.hpp" #include #include @@ -287,28 +288,47 @@ void ProcessInfoNode::setupControlClientChannel() score::cpp::expected_blank ProcessInfoNode::handleProcessStillStarting( const score::cpp::stop_token& stop_token) { - static_cast(stop_token); // Not yet supported const bool is_native = configuration::ApplicationType::Native == config_.component_properties.application_profile.application_type; bool ready_condition_met = false; std::visit( - [this, &ready_condition_met](auto&& arg) { + [this, &ready_condition_met, is_native](auto&& arg) { using T = std::decay_t; if constexpr (std::is_same_v) { + if (is_native) + { + // A native process does not report kRunning, so its status is the only readiness indication. + ready_condition_met = (0 == status_); + return; + } + auto wait_res = process_handling_.process_interface_->waitForkRunning( sync_, std::chrono::milliseconds(config_.deployment_config.ready_timeout_ms)); ready_condition_met = wait_res == osal::OsalReturnType::kSuccess && 0 == status_; } else if constexpr (std::is_same_v) { + if (configuration::FileExistenceState::Exists != arg.state) + { + LM_LOG_WARN() << "Ready condition of process" << process_index_ << "(" << config_.name + << ") waits for" << arg.file_path << "to disappear, which is not supported"; + return; + } + + const auto wait_res = osal::wait_for_file( + arg.file_path, + arg.state, + std::chrono::milliseconds(config_.deployment_config.ready_timeout_ms), + arg.polling_interval); + ready_condition_met = (osal::OsalReturnType::kSuccess == wait_res) && (0 == status_); } }, - config_.component_properties.ready_condition.value()); + config_.component_properties.ready_condition); - if (is_native || ready_condition_met) + if (ready_condition_met) { handleProcessRunning(); return {}; diff --git a/tests/integration/ready_condition_file/BUILD b/tests/integration/ready_condition_file/BUILD new file mode 100644 index 000000000..4d487a445 --- /dev/null +++ b/tests/integration/ready_condition_file/BUILD @@ -0,0 +1,42 @@ +# ******************************************************************************* +# 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 = "file_creating_process", + srcs = ["file_creating_process.cpp"], + deps = [ + "//tests/utils/test_helper", + ], +) + +cc_binary( + name = "ready_file_verification_process", + srcs = ["ready_file_verification_process.cpp"], + deps = [ + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) + +integration_test( + name = "ready_condition_file", + srcs = ["ready_condition_file.py"], + binaries = [ + ":file_creating_process", + ":ready_file_verification_process", + "//score/launch_manager", + ], + config = ":ready_condition_file.json", +) diff --git a/tests/integration/ready_condition_file/file_creating_process.cpp b/tests/integration/ready_condition_file/file_creating_process.cpp new file mode 100644 index 000000000..69a5994d3 --- /dev/null +++ b/tests/integration/ready_condition_file/file_creating_process.cpp @@ -0,0 +1,55 @@ +/******************************************************************************** + * 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 "tests/utils/test_helper/test_helper.hpp" +#include +#include +#include +#include + +/// @file file_creating_process.cpp +/// @brief Test process that touches the file its component uses as a ready +/// condition. The path is passed as the first command line argument. +/// The file is only created after a delay, so a launch manager that does +/// not wait for it lets dependent components start too early. + +namespace +{ + +/// @brief Time between process start and the creation of the ready condition file. +constexpr std::chrono::milliseconds kCreationDelay{300}; + +} // namespace + +int main(int argc, char** argv) +{ + if (argc != 2) + { + std::cerr << "Expected the path of the ready condition file as the only argument" << std::endl; + return EXIT_FAILURE; + } + + std::this_thread::sleep_for(kCreationDelay); + + const auto res = touch_file(argv[1]); + if (!res) + { + std::cerr << res.failure_message() << std::endl; + return EXIT_FAILURE; + } + std::cout << "Touched ready condition file " << argv[1] << std::endl; + + // Keep providing the "service" until the launch manager terminates us. + pause(); + return EXIT_SUCCESS; +} diff --git a/tests/integration/ready_condition_file/ready_condition_file.json b/tests/integration/ready_condition_file/ready_condition_file.json new file mode 100644 index 000000000..a9a0c300e --- /dev/null +++ b/tests/integration/ready_condition_file/ready_condition_file.json @@ -0,0 +1,103 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "bin_dir": "/tmp/tests/ready_condition_file", + "ready_timeout": 1.0, + "shutdown_timeout": 1.0, + "ready_recovery_action": { + "restart": { + "number_of_attempts": 0 + } + }, + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + }, + "environmental_variables": { + "LD_LIBRARY_PATH": "/opt/lib" + }, + "sandbox": { + "uid": 0, + "gid": 0, + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0 + } + }, + "component_properties": { + "application_profile": { + "application_type": "Native", + "is_self_terminating": false + }, + "ready_condition": { + "process_state": "Running" + } + } + }, + "components": { + "file_creating_component": { + "component_properties": { + "binary_name": "file_creating_process", + "process_arguments": [ + "/tmp/tests/ready_condition_file/ready_file" + ], + "ready_condition": { + "file_state": { + "file_path": "/tmp/tests/ready_condition_file/ready_file", + "state": "Exists", + "polling_interval": 0.01 + } + } + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "file_creating_component" + } + } + }, + "verification_component": { + "component_properties": { + "binary_name": "ready_file_verification_process", + "process_arguments": [ + "/tmp/tests/ready_condition_file/ready_file" + ], + "depends_on": [ + "file_creating_component" + ], + "application_profile": { + "application_type": "Native", + "is_self_terminating": true + }, + "ready_condition": { + "process_state": "Terminated" + } + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "verification_component" + } + } + } + }, + "run_targets": { + "Startup": { + "depends_on": [ + "file_creating_component", + "verification_component" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + } + }, + "initial_run_target": "Startup", + "alive_supervision": { + "evaluation_cycle": 0.05 + }, + "fallback_run_target": { + "depends_on": [] + } +} diff --git a/tests/integration/ready_condition_file/ready_condition_file.py b/tests/integration/ready_condition_file/ready_condition_file.py new file mode 100644 index 000000000..a348019be --- /dev/null +++ b/tests/integration/ready_condition_file/ready_condition_file.py @@ -0,0 +1,47 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +from tests.utils.testing_utils.run_until_file_deployed import run_until_file_deployed +from tests.utils.testing_utils.setup_test import setup_test +from tests.utils.testing_utils.test_results import assert_test_results +from attribute_plugin import add_test_properties + + +@add_test_properties( + partially_verifies=[], + test_type="interface-test", + derivation_technique="explorative-testing", +) +def test_ready_condition_file(target, setup_test, assert_test_results, remote_test_dir): + """ + Objective: Verifies that a component with a file_state ready condition only reaches its ready state once the configured file exists. + + The initial run target contains a component that touches its ready condition file after a delay, and a second component depending on it. + Expected Behaviour: The launch manager polls for the file and only starts the dependent component after the file has been created. + """ + + config_path = str(remote_test_dir / "etc/ready_condition_file.bin") + ready_file = str(remote_test_dir / "ready_file") + + # A leftover file from a previous run would satisfy the ready condition immediately. + target.execute(f"rm -f {ready_file}") + + run_until_file_deployed( + target=target, + binary_path=str(remote_test_dir / "launch_manager"), + file_path=remote_test_dir.parent / "test_end", + cwd=str(remote_test_dir), + args=["-c", config_path], + timeout_s=3.0, + ) + + assert_test_results({"ready_file_verification_process.xml"}) diff --git a/tests/integration/ready_condition_file/ready_file_verification_process.cpp b/tests/integration/ready_condition_file/ready_file_verification_process.cpp new file mode 100644 index 000000000..ea5849aed --- /dev/null +++ b/tests/integration/ready_condition_file/ready_file_verification_process.cpp @@ -0,0 +1,51 @@ +/******************************************************************************** + * 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 + +#include +#include + +#include "tests/utils/test_helper/test_helper.hpp" + +// Given a correct configuration with: +// - An initial Run Target named "Startup" +// - Startup contains the Components "file_creating_component" and "verification_component" +// - file_creating_component has a file_state ready condition on a file it only creates after a delay +// - verification_component depends on file_creating_component + +// When launch manager is started + +std::string g_ready_file; + +TEST(ReadyConditionFile, ReadyFileExistsBeforeDependentStarts) +{ + // Then, this process is only started once the ready condition of file_creating_component is met: + TEST_STEP("Check that the ready condition file exists") + { + EXPECT_TRUE(std::filesystem::exists(g_ready_file)) + << "'" << g_ready_file << "' does not exist, the dependent component was started too early"; + } +} + +int main(int argc, char** argv) +{ + if (argc != 2) + { + std::cerr << "Expected the path of the ready condition file as the only argument" << std::endl; + return EXIT_FAILURE; + } + g_ready_file = argv[1]; + + TestRunner runner{__FILE__, TerminationBehavior::kContinue, TerminationNotification::kTestEnd}; + return runner.RunTests(); +} From 3c6e734c0affcaf185ee1619c13e3d3d1178c0d3 Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Thu, 20 Aug 2026 14:21:47 +0100 Subject: [PATCH 04/16] More tests --- .../details/process_info_node.cpp | 7 -- tests/integration/ready_condition_file/BUILD | 14 ++- .../file_deleting_process.cpp | 58 ++++++++++ .../ready_condition_file.json | 3 +- .../ready_condition_file.py | 48 +++++++- .../ready_condition_file_not_existing.json | 104 ++++++++++++++++++ .../ready_file_verification_process.cpp | 48 ++++++-- 7 files changed, 260 insertions(+), 22 deletions(-) create mode 100644 tests/integration/ready_condition_file/file_deleting_process.cpp create mode 100644 tests/integration/ready_condition_file/ready_condition_file_not_existing.json diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp index 0199cad89..6d5bdea40 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp @@ -311,13 +311,6 @@ score::cpp::expected_blank ProcessInfoNode::handlePr } else if constexpr (std::is_same_v) { - if (configuration::FileExistenceState::Exists != arg.state) - { - LM_LOG_WARN() << "Ready condition of process" << process_index_ << "(" << config_.name - << ") waits for" << arg.file_path << "to disappear, which is not supported"; - return; - } - const auto wait_res = osal::wait_for_file( arg.file_path, arg.state, diff --git a/tests/integration/ready_condition_file/BUILD b/tests/integration/ready_condition_file/BUILD index 4d487a445..1241cf978 100644 --- a/tests/integration/ready_condition_file/BUILD +++ b/tests/integration/ready_condition_file/BUILD @@ -21,6 +21,14 @@ cc_binary( ], ) +cc_binary( + name = "file_deleting_process", + srcs = ["file_deleting_process.cpp"], + deps = [ + "//tests/utils/test_helper", + ], +) + cc_binary( name = "ready_file_verification_process", srcs = ["ready_file_verification_process.cpp"], @@ -35,8 +43,12 @@ integration_test( srcs = ["ready_condition_file.py"], binaries = [ ":file_creating_process", + ":file_deleting_process", ":ready_file_verification_process", "//score/launch_manager", ], - config = ":ready_condition_file.json", + config = [ + ":ready_condition_file.json", + ":ready_condition_file_not_existing.json", + ], ) diff --git a/tests/integration/ready_condition_file/file_deleting_process.cpp b/tests/integration/ready_condition_file/file_deleting_process.cpp new file mode 100644 index 000000000..f27628042 --- /dev/null +++ b/tests/integration/ready_condition_file/file_deleting_process.cpp @@ -0,0 +1,58 @@ +/******************************************************************************** + * 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 "tests/utils/test_helper/test_helper.hpp" +#include +#include +#include +#include +#include + +/// @file file_deleting_process.cpp +/// @brief Test process that removes the file its component uses as a ready +/// condition. The path is passed as the first command line argument and +/// the file is expected to exist when the process starts. +/// The file is only removed after a delay, so a launch manager that does +/// not wait for it to disappear lets dependent components start too early. + +namespace +{ + +/// @brief Time between process start and the removal of the ready condition file. +constexpr std::chrono::milliseconds kRemovalDelay{300}; + +} // namespace + +int main(int argc, char** argv) +{ + if (argc != 2) + { + std::cerr << "Expected the path of the ready condition file as the only argument" << std::endl; + return EXIT_FAILURE; + } + + std::this_thread::sleep_for(kRemovalDelay); + + std::error_code error{}; + if (!std::filesystem::remove(argv[1], error)) + { + std::cerr << "Could not remove ready condition file " << argv[1] << ": " + << (error ? error.message() : "the file did not exist") << std::endl; + return EXIT_FAILURE; + } + std::cout << "Removed ready condition file " << argv[1] << std::endl; + + // Keep providing the "service" until the launch manager terminates us. + pause(); + return EXIT_SUCCESS; +} diff --git a/tests/integration/ready_condition_file/ready_condition_file.json b/tests/integration/ready_condition_file/ready_condition_file.json index a9a0c300e..1a38107d7 100644 --- a/tests/integration/ready_condition_file/ready_condition_file.json +++ b/tests/integration/ready_condition_file/ready_condition_file.json @@ -60,7 +60,8 @@ "component_properties": { "binary_name": "ready_file_verification_process", "process_arguments": [ - "/tmp/tests/ready_condition_file/ready_file" + "/tmp/tests/ready_condition_file/ready_file", + "Exists" ], "depends_on": [ "file_creating_component" diff --git a/tests/integration/ready_condition_file/ready_condition_file.py b/tests/integration/ready_condition_file/ready_condition_file.py index a348019be..9f7ddd9fa 100644 --- a/tests/integration/ready_condition_file/ready_condition_file.py +++ b/tests/integration/ready_condition_file/ready_condition_file.py @@ -23,10 +23,14 @@ ) def test_ready_condition_file(target, setup_test, assert_test_results, remote_test_dir): """ - Objective: Verifies that a component with a file_state ready condition only reaches its ready state once the configured file exists. + Objective: Verifies that a component with a file_state ready condition only + reaches its ready state once the configured file exists. - The initial run target contains a component that touches its ready condition file after a delay, and a second component depending on it. - Expected Behaviour: The launch manager polls for the file and only starts the dependent component after the file has been created. + The initial run target contains a component that touches its ready + condition file after a delay. + + Expected Behaviour: The launch manager polls for the file and only starts + the dependent component after the file has been created. """ config_path = str(remote_test_dir / "etc/ready_condition_file.bin") @@ -45,3 +49,41 @@ def test_ready_condition_file(target, setup_test, assert_test_results, remote_te ) assert_test_results({"ready_file_verification_process.xml"}) + + +@add_test_properties( + partially_verifies=[], + test_type="interface-test", + derivation_technique="explorative-testing", +) +def test_ready_condition_file_not_existing( + target, setup_test, assert_test_results, remote_test_dir +): + """ + Objective: Verifies that a component with a NotExisting file_state ready + condition only reaches its ready state once the configured file is gone. + + The initial run target contains a component that removes its ready + condition file after a delay, and a second component depending on it. + + Expected Behaviour: The launch manager polls for the file and only starts + the dependent component after the file has been removed. + """ + + config_path = str(remote_test_dir / "etc/ready_condition_file_not_existing.bin") + vanishing_file = str(remote_test_dir / "vanishing_file") + + # The file has to be there when the launch manager starts polling, otherwise the ready + # condition is satisfied right away and the test would pass without waiting for anything. + target.execute(f"touch {vanishing_file}") + + run_until_file_deployed( + target=target, + binary_path=str(remote_test_dir / "launch_manager"), + file_path=remote_test_dir.parent / "test_end", + cwd=str(remote_test_dir), + args=["-c", config_path], + timeout_s=3.0, + ) + + assert_test_results({"ready_file_verification_process.xml"}) diff --git a/tests/integration/ready_condition_file/ready_condition_file_not_existing.json b/tests/integration/ready_condition_file/ready_condition_file_not_existing.json new file mode 100644 index 000000000..917e126c7 --- /dev/null +++ b/tests/integration/ready_condition_file/ready_condition_file_not_existing.json @@ -0,0 +1,104 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "bin_dir": "/tmp/tests/ready_condition_file", + "ready_timeout": 1.0, + "shutdown_timeout": 1.0, + "ready_recovery_action": { + "restart": { + "number_of_attempts": 0 + } + }, + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + }, + "environmental_variables": { + "LD_LIBRARY_PATH": "/opt/lib" + }, + "sandbox": { + "uid": 0, + "gid": 0, + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0 + } + }, + "component_properties": { + "application_profile": { + "application_type": "Native", + "is_self_terminating": false + }, + "ready_condition": { + "process_state": "Running" + } + } + }, + "components": { + "file_deleting_component": { + "component_properties": { + "binary_name": "file_deleting_process", + "process_arguments": [ + "/tmp/tests/ready_condition_file/vanishing_file" + ], + "ready_condition": { + "file_state": { + "file_path": "/tmp/tests/ready_condition_file/vanishing_file", + "state": "NotExisting", + "polling_interval": 0.01 + } + } + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "file_deleting_component" + } + } + }, + "verification_component": { + "component_properties": { + "binary_name": "ready_file_verification_process", + "process_arguments": [ + "/tmp/tests/ready_condition_file/vanishing_file", + "NotExisting" + ], + "depends_on": [ + "file_deleting_component" + ], + "application_profile": { + "application_type": "Native", + "is_self_terminating": true + }, + "ready_condition": { + "process_state": "Terminated" + } + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "verification_component" + } + } + } + }, + "run_targets": { + "Startup": { + "depends_on": [ + "file_deleting_component", + "verification_component" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + } + }, + "initial_run_target": "Startup", + "alive_supervision": { + "evaluation_cycle": 0.05 + }, + "fallback_run_target": { + "depends_on": [] + } +} diff --git a/tests/integration/ready_condition_file/ready_file_verification_process.cpp b/tests/integration/ready_condition_file/ready_file_verification_process.cpp index ea5849aed..d09f93b52 100644 --- a/tests/integration/ready_condition_file/ready_file_verification_process.cpp +++ b/tests/integration/ready_condition_file/ready_file_verification_process.cpp @@ -14,38 +14,66 @@ #include #include +#include #include "tests/utils/test_helper/test_helper.hpp" // Given a correct configuration with: // - An initial Run Target named "Startup" -// - Startup contains the Components "file_creating_component" and "verification_component" -// - file_creating_component has a file_state ready condition on a file it only creates after a delay -// - verification_component depends on file_creating_component +// - Startup contains a component with a file_state ready condition on a file whose state it only +// changes after a delay, and the Component "verification_component" +// - verification_component depends on the component owning the file // When launch manager is started std::string g_ready_file; +bool g_expect_existing = true; -TEST(ReadyConditionFile, ReadyFileExistsBeforeDependentStarts) +TEST(ReadyConditionFile, ReadyConditionIsMetBeforeDependentStarts) { - // Then, this process is only started once the ready condition of file_creating_component is met: - TEST_STEP("Check that the ready condition file exists") + // Then, this process is only started once the ready condition of the component it depends on is met: + TEST_STEP("Check the state of the ready condition file") { - EXPECT_TRUE(std::filesystem::exists(g_ready_file)) - << "'" << g_ready_file << "' does not exist, the dependent component was started too early"; + if (g_expect_existing) + { + EXPECT_TRUE(std::filesystem::exists(g_ready_file)) + << "'" << g_ready_file << "' does not exist, the dependent component was started too early"; + } + else + { + EXPECT_FALSE(std::filesystem::exists(g_ready_file)) + << "'" << g_ready_file << "' still exists, the dependent component was started too early"; + } } } int main(int argc, char** argv) { - if (argc != 2) + if (argc != 3) { - std::cerr << "Expected the path of the ready condition file as the only argument" << std::endl; + std::cerr << "Expected the path of the ready condition file and its expected state " + "('Exists' or 'NotExisting') as arguments" + << std::endl; return EXIT_FAILURE; } g_ready_file = argv[1]; + const std::string_view expected_state{argv[2]}; + if (expected_state == "Exists") + { + g_expect_existing = true; + } + else if (expected_state == "NotExisting") + { + g_expect_existing = false; + } + else + { + std::cerr << "Unknown expected state '" << expected_state << "', expected 'Exists' or 'NotExisting'" + << std::endl; + return EXIT_FAILURE; + } + TestRunner runner{__FILE__, TerminationBehavior::kContinue, TerminationNotification::kTestEnd}; return runner.RunTests(); } From 2a9fdfec4a6a670c0b5990ab4591ebaea01cacb2 Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Fri, 21 Aug 2026 07:59:07 +0100 Subject: [PATCH 05/16] cleanup tests --- .../src/osal/details/posix/wait_for_file.cpp | 12 +-- .../file_creating_process.cpp | 55 ------------- .../file_deleting_process.cpp | 58 -------------- .../ready_conditions/file_state/common/BUILD | 35 ++++++++ .../file_state/common/file_modifier.cpp | 72 +++++++++++++++++ .../file_state/exists}/BUILD | 24 +----- .../exists}/ready_condition_file.json | 4 +- .../file_state/exists/ready_condition_file.py | 46 +++++++++++ .../ready_file_verification_process.cpp | 0 .../file_state/not_existing/BUILD | 34 ++++++++ .../ready_condition_file_not_existing.json | 10 +-- .../ready_condition_file_not_existing.py} | 40 +--------- .../ready_file_verification_process.cpp | 79 +++++++++++++++++++ 13 files changed, 285 insertions(+), 184 deletions(-) delete mode 100644 tests/integration/ready_condition_file/file_creating_process.cpp delete mode 100644 tests/integration/ready_condition_file/file_deleting_process.cpp create mode 100644 tests/integration/ready_conditions/file_state/common/BUILD create mode 100644 tests/integration/ready_conditions/file_state/common/file_modifier.cpp rename tests/integration/{ready_condition_file => ready_conditions/file_state/exists}/BUILD (69%) rename tests/integration/{ready_condition_file => ready_conditions/file_state/exists}/ready_condition_file.json (98%) create mode 100644 tests/integration/ready_conditions/file_state/exists/ready_condition_file.py rename tests/integration/{ready_condition_file => ready_conditions/file_state/exists}/ready_file_verification_process.cpp (100%) create mode 100644 tests/integration/ready_conditions/file_state/not_existing/BUILD rename tests/integration/{ready_condition_file => ready_conditions/file_state/not_existing}/ready_condition_file_not_existing.json (90%) rename tests/integration/{ready_condition_file/ready_condition_file.py => ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py} (61%) create mode 100644 tests/integration/ready_conditions/file_state/not_existing/ready_file_verification_process.cpp diff --git a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp index bbc968b3c..0591054fe 100644 --- a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp +++ b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp @@ -30,9 +30,11 @@ OsalReturnType wait_for_file( std::chrono::milliseconds timeout, std::chrono::milliseconds poll_interval) noexcept { - // note: QNX has a wait_for API, however this + // note: QNX has a wait_for API, however in the future a stop_token should + // be used, when using the API call we wouldn't be able to check the + // stop_token between stat calls. - // The terminator is expected right behind the view, so it is not part of its size. + // required null terminator if (path.empty() || (path.data()[path.size()] != '\0')) { return OsalReturnType::kFail; @@ -45,7 +47,7 @@ OsalReturnType wait_for_file( { struct stat info{}; - if (stat(path.data(), &info) == 0) + if (::stat(path.data(), &info) == 0) { if (wait_for_existence) { @@ -56,8 +58,8 @@ OsalReturnType wait_for_file( { switch (errno) { - // The path, or one of its parent directories, does not exist. case (ENOENT): + [[fallthrough]]; // threat file or dir not existing as the same case (ENOTDIR): if (!wait_for_existence) { @@ -65,8 +67,8 @@ OsalReturnType wait_for_file( } break; - // The query was interrupted, so it says nothing about the path. Simply retry it. case (EINTR): + // retry break; default: diff --git a/tests/integration/ready_condition_file/file_creating_process.cpp b/tests/integration/ready_condition_file/file_creating_process.cpp deleted file mode 100644 index 69a5994d3..000000000 --- a/tests/integration/ready_condition_file/file_creating_process.cpp +++ /dev/null @@ -1,55 +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 "tests/utils/test_helper/test_helper.hpp" -#include -#include -#include -#include - -/// @file file_creating_process.cpp -/// @brief Test process that touches the file its component uses as a ready -/// condition. The path is passed as the first command line argument. -/// The file is only created after a delay, so a launch manager that does -/// not wait for it lets dependent components start too early. - -namespace -{ - -/// @brief Time between process start and the creation of the ready condition file. -constexpr std::chrono::milliseconds kCreationDelay{300}; - -} // namespace - -int main(int argc, char** argv) -{ - if (argc != 2) - { - std::cerr << "Expected the path of the ready condition file as the only argument" << std::endl; - return EXIT_FAILURE; - } - - std::this_thread::sleep_for(kCreationDelay); - - const auto res = touch_file(argv[1]); - if (!res) - { - std::cerr << res.failure_message() << std::endl; - return EXIT_FAILURE; - } - std::cout << "Touched ready condition file " << argv[1] << std::endl; - - // Keep providing the "service" until the launch manager terminates us. - pause(); - return EXIT_SUCCESS; -} diff --git a/tests/integration/ready_condition_file/file_deleting_process.cpp b/tests/integration/ready_condition_file/file_deleting_process.cpp deleted file mode 100644 index f27628042..000000000 --- a/tests/integration/ready_condition_file/file_deleting_process.cpp +++ /dev/null @@ -1,58 +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 "tests/utils/test_helper/test_helper.hpp" -#include -#include -#include -#include -#include - -/// @file file_deleting_process.cpp -/// @brief Test process that removes the file its component uses as a ready -/// condition. The path is passed as the first command line argument and -/// the file is expected to exist when the process starts. -/// The file is only removed after a delay, so a launch manager that does -/// not wait for it to disappear lets dependent components start too early. - -namespace -{ - -/// @brief Time between process start and the removal of the ready condition file. -constexpr std::chrono::milliseconds kRemovalDelay{300}; - -} // namespace - -int main(int argc, char** argv) -{ - if (argc != 2) - { - std::cerr << "Expected the path of the ready condition file as the only argument" << std::endl; - return EXIT_FAILURE; - } - - std::this_thread::sleep_for(kRemovalDelay); - - std::error_code error{}; - if (!std::filesystem::remove(argv[1], error)) - { - std::cerr << "Could not remove ready condition file " << argv[1] << ": " - << (error ? error.message() : "the file did not exist") << std::endl; - return EXIT_FAILURE; - } - std::cout << "Removed ready condition file " << argv[1] << std::endl; - - // Keep providing the "service" until the launch manager terminates us. - pause(); - return EXIT_SUCCESS; -} diff --git a/tests/integration/ready_conditions/file_state/common/BUILD b/tests/integration/ready_conditions/file_state/common/BUILD new file mode 100644 index 000000000..914125777 --- /dev/null +++ b/tests/integration/ready_conditions/file_state/common/BUILD @@ -0,0 +1,35 @@ +# ******************************************************************************* +# 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 = "file_creator", + srcs = ["file_modifier.cpp"], + visibility = ["//tests/integration/ready_conditions/file_state:__subpackages__"], + deps = [ + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) + +cc_binary( + name = "file_deletor", + srcs = ["file_modifier.cpp"], + visibility = ["//tests/integration/ready_conditions/file_state:__subpackages__"], + deps = [ + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) diff --git a/tests/integration/ready_conditions/file_state/common/file_modifier.cpp b/tests/integration/ready_conditions/file_state/common/file_modifier.cpp new file mode 100644 index 000000000..f6305e5b8 --- /dev/null +++ b/tests/integration/ready_conditions/file_state/common/file_modifier.cpp @@ -0,0 +1,72 @@ +/******************************************************************************** + * 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 "tests/utils/test_helper/test_helper.hpp" +#include +#include +#include +#include +#include + +enum class Operation : std::uint8_t +{ + Create = 0, + Delete +}; +std::string_view g_file_path; +std::chrono::milliseconds g_modify_delay; +Operation g_operation; + +TEST(ModifyFile, ModifyFile) +{ + std::this_thread::sleep_for(g_modify_delay); + switch (g_operation) + { + case (Operation::Create): + ASSERT_TRUE(touch_file(g_file_path)); + break; + case (Operation::Delete): + std::error_code error{}; + ASSERT_TRUE(std::filesystem::remove(g_file_path, error)) + << "Could not remove file " << g_file_path << ": " + << (error ? error.message() : "the file did not exist"); + break; + } +} + +int main(int argc, char** argv) +{ + if (argc != 3) + { + std::cerr << "USAGE:" << argv[0] << "(file path) (milliseconds to wait before doing the operation)" + << std::endl; + return EXIT_FAILURE; + } + const std::string_view prog_name{argv[0]}; + + if (prog_name == "file_creator") + { + g_operation = Operation::Create; + } + else if (prog_name == "file_deletor") + { + g_operation = Operation::Delete; + } + + g_file_path = std::string_view{argv[1]}; + g_modify_delay = std::chrono::milliseconds{std::stoi(argv[2])}; + + std::string xml_name{"reporting_process_"}; + xml_name.append(prog_name); + return TestRunner(xml_name).RunTests(); +} diff --git a/tests/integration/ready_condition_file/BUILD b/tests/integration/ready_conditions/file_state/exists/BUILD similarity index 69% rename from tests/integration/ready_condition_file/BUILD rename to tests/integration/ready_conditions/file_state/exists/BUILD index 1241cf978..f451b4c0b 100644 --- a/tests/integration/ready_condition_file/BUILD +++ b/tests/integration/ready_conditions/file_state/exists/BUILD @@ -13,22 +13,6 @@ load("@rules_cc//cc:cc_binary.bzl", "cc_binary") load("//tests/utils/bazel:integration.bzl", "integration_test") -cc_binary( - name = "file_creating_process", - srcs = ["file_creating_process.cpp"], - deps = [ - "//tests/utils/test_helper", - ], -) - -cc_binary( - name = "file_deleting_process", - srcs = ["file_deleting_process.cpp"], - deps = [ - "//tests/utils/test_helper", - ], -) - cc_binary( name = "ready_file_verification_process", srcs = ["ready_file_verification_process.cpp"], @@ -42,13 +26,9 @@ integration_test( name = "ready_condition_file", srcs = ["ready_condition_file.py"], binaries = [ - ":file_creating_process", - ":file_deleting_process", + "//tests/integration/ready_conditions/file_state/common:file_creator", ":ready_file_verification_process", "//score/launch_manager", ], - config = [ - ":ready_condition_file.json", - ":ready_condition_file_not_existing.json", - ], + config = ":ready_condition_file.json", ) diff --git a/tests/integration/ready_condition_file/ready_condition_file.json b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.json similarity index 98% rename from tests/integration/ready_condition_file/ready_condition_file.json rename to tests/integration/ready_conditions/file_state/exists/ready_condition_file.json index 1a38107d7..0e3169216 100644 --- a/tests/integration/ready_condition_file/ready_condition_file.json +++ b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.json @@ -38,9 +38,9 @@ "components": { "file_creating_component": { "component_properties": { - "binary_name": "file_creating_process", + "binary_name": "file_creator", "process_arguments": [ - "/tmp/tests/ready_condition_file/ready_file" + "/tmp/tests/ready_condition_file/ready_file", "300" ], "ready_condition": { "file_state": { diff --git a/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py new file mode 100644 index 000000000..a8604ac33 --- /dev/null +++ b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py @@ -0,0 +1,46 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +from tests.utils.testing_utils.run_until_file_deployed import run_until_file_deployed +from tests.utils.testing_utils.setup_test import setup_test +from tests.utils.testing_utils.test_results import assert_test_results +from attribute_plugin import add_test_properties + + +@add_test_properties( + partially_verifies=[], + test_type="interface-test", + derivation_technique="explorative-testing", +) +def test_ready_condition_file(target, setup_test, assert_test_results, remote_test_dir): + """ + Objective: Verifies that a component with a file_state ready condition only + reaches its ready state once the configured file exists. + + The initial run target contains a component that touches its ready + condition file after a delay. + + Expected Behaviour: The launch manager polls for the file and only starts + the dependent component after the file has been created. + """ + + config_path = str(remote_test_dir / "etc/ready_condition_file.bin") + run_until_file_deployed( + target=target, + binary_path=str(remote_test_dir / "launch_manager"), + file_path=remote_test_dir.parent / "test_end", + cwd=str(remote_test_dir), + args=["-c", config_path], + timeout_s=3.0, + ) + + assert_test_results({"ready_file_verification_process.xml", "file_creator.xml"}) diff --git a/tests/integration/ready_condition_file/ready_file_verification_process.cpp b/tests/integration/ready_conditions/file_state/exists/ready_file_verification_process.cpp similarity index 100% rename from tests/integration/ready_condition_file/ready_file_verification_process.cpp rename to tests/integration/ready_conditions/file_state/exists/ready_file_verification_process.cpp diff --git a/tests/integration/ready_conditions/file_state/not_existing/BUILD b/tests/integration/ready_conditions/file_state/not_existing/BUILD new file mode 100644 index 000000000..6f41f4140 --- /dev/null +++ b/tests/integration/ready_conditions/file_state/not_existing/BUILD @@ -0,0 +1,34 @@ +# ******************************************************************************* +# 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 = "ready_file_verification_process", + srcs = ["ready_file_verification_process.cpp"], + deps = [ + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) + +integration_test( + name = "ready_condition_file_not_exists", + srcs = ["ready_condition_file_not_existing.py"], + binaries = [ + "//tests/integration/ready_conditions/file_state/common:file_deletor", + ":ready_file_verification_process", + "//score/launch_manager", + ], + config = ":ready_condition_file_not_existing.json", +) diff --git a/tests/integration/ready_condition_file/ready_condition_file_not_existing.json b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json similarity index 90% rename from tests/integration/ready_condition_file/ready_condition_file_not_existing.json rename to tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json index 917e126c7..2ae056fd7 100644 --- a/tests/integration/ready_condition_file/ready_condition_file_not_existing.json +++ b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json @@ -2,7 +2,7 @@ "schema_version": 1, "defaults": { "deployment_config": { - "bin_dir": "/tmp/tests/ready_condition_file", + "bin_dir": "/tmp/tests/ready_condition_file_not_exists", "ready_timeout": 1.0, "shutdown_timeout": 1.0, "ready_recovery_action": { @@ -38,13 +38,13 @@ "components": { "file_deleting_component": { "component_properties": { - "binary_name": "file_deleting_process", + "binary_name": "file_deletor", "process_arguments": [ - "/tmp/tests/ready_condition_file/vanishing_file" + "/tmp/tests/ready_condition_file_not_exists/vanishing_file", "300" ], "ready_condition": { "file_state": { - "file_path": "/tmp/tests/ready_condition_file/vanishing_file", + "file_path": "/tmp/tests/ready_condition_file_not_exists/vanishing_file", "state": "NotExisting", "polling_interval": 0.01 } @@ -60,7 +60,7 @@ "component_properties": { "binary_name": "ready_file_verification_process", "process_arguments": [ - "/tmp/tests/ready_condition_file/vanishing_file", + "/tmp/tests/ready_condition_file_not_exists/vanishing_file", "NotExisting" ], "depends_on": [ diff --git a/tests/integration/ready_condition_file/ready_condition_file.py b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py similarity index 61% rename from tests/integration/ready_condition_file/ready_condition_file.py rename to tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py index 9f7ddd9fa..273b8c722 100644 --- a/tests/integration/ready_condition_file/ready_condition_file.py +++ b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py @@ -16,41 +16,6 @@ from attribute_plugin import add_test_properties -@add_test_properties( - partially_verifies=[], - test_type="interface-test", - derivation_technique="explorative-testing", -) -def test_ready_condition_file(target, setup_test, assert_test_results, remote_test_dir): - """ - Objective: Verifies that a component with a file_state ready condition only - reaches its ready state once the configured file exists. - - The initial run target contains a component that touches its ready - condition file after a delay. - - Expected Behaviour: The launch manager polls for the file and only starts - the dependent component after the file has been created. - """ - - config_path = str(remote_test_dir / "etc/ready_condition_file.bin") - ready_file = str(remote_test_dir / "ready_file") - - # A leftover file from a previous run would satisfy the ready condition immediately. - target.execute(f"rm -f {ready_file}") - - run_until_file_deployed( - target=target, - binary_path=str(remote_test_dir / "launch_manager"), - file_path=remote_test_dir.parent / "test_end", - cwd=str(remote_test_dir), - args=["-c", config_path], - timeout_s=3.0, - ) - - assert_test_results({"ready_file_verification_process.xml"}) - - @add_test_properties( partially_verifies=[], test_type="interface-test", @@ -75,7 +40,8 @@ def test_ready_condition_file_not_existing( # The file has to be there when the launch manager starts polling, otherwise the ready # condition is satisfied right away and the test would pass without waiting for anything. - target.execute(f"touch {vanishing_file}") + res, stdout = target.execute(f"touch {vanishing_file}") + assert res == 0, stdout run_until_file_deployed( target=target, @@ -86,4 +52,4 @@ def test_ready_condition_file_not_existing( timeout_s=3.0, ) - assert_test_results({"ready_file_verification_process.xml"}) + assert_test_results({"ready_file_verification_process.xml", "file_deletor.xml"}) diff --git a/tests/integration/ready_conditions/file_state/not_existing/ready_file_verification_process.cpp b/tests/integration/ready_conditions/file_state/not_existing/ready_file_verification_process.cpp new file mode 100644 index 000000000..d09f93b52 --- /dev/null +++ b/tests/integration/ready_conditions/file_state/not_existing/ready_file_verification_process.cpp @@ -0,0 +1,79 @@ +/******************************************************************************** + * 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 + +#include +#include +#include + +#include "tests/utils/test_helper/test_helper.hpp" + +// Given a correct configuration with: +// - An initial Run Target named "Startup" +// - Startup contains a component with a file_state ready condition on a file whose state it only +// changes after a delay, and the Component "verification_component" +// - verification_component depends on the component owning the file + +// When launch manager is started + +std::string g_ready_file; +bool g_expect_existing = true; + +TEST(ReadyConditionFile, ReadyConditionIsMetBeforeDependentStarts) +{ + // Then, this process is only started once the ready condition of the component it depends on is met: + TEST_STEP("Check the state of the ready condition file") + { + if (g_expect_existing) + { + EXPECT_TRUE(std::filesystem::exists(g_ready_file)) + << "'" << g_ready_file << "' does not exist, the dependent component was started too early"; + } + else + { + EXPECT_FALSE(std::filesystem::exists(g_ready_file)) + << "'" << g_ready_file << "' still exists, the dependent component was started too early"; + } + } +} + +int main(int argc, char** argv) +{ + if (argc != 3) + { + std::cerr << "Expected the path of the ready condition file and its expected state " + "('Exists' or 'NotExisting') as arguments" + << std::endl; + return EXIT_FAILURE; + } + g_ready_file = argv[1]; + + const std::string_view expected_state{argv[2]}; + if (expected_state == "Exists") + { + g_expect_existing = true; + } + else if (expected_state == "NotExisting") + { + g_expect_existing = false; + } + else + { + std::cerr << "Unknown expected state '" << expected_state << "', expected 'Exists' or 'NotExisting'" + << std::endl; + return EXIT_FAILURE; + } + + TestRunner runner{__FILE__, TerminationBehavior::kContinue, TerminationNotification::kTestEnd}; + return runner.RunTests(); +} From f3abdfa44986807463e226f0bfa4500eb1ebaec9 Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Fri, 21 Aug 2026 08:58:24 +0100 Subject: [PATCH 06/16] Fix UT --- .../launch_manager/src/daemon/src/osal/BUILD | 2 +- .../osal/details/posix/wait_for_file_UT.cpp | 132 +++++------------- 2 files changed, 37 insertions(+), 97 deletions(-) diff --git a/score/launch_manager/src/daemon/src/osal/BUILD b/score/launch_manager/src/daemon/src/osal/BUILD index bc8b62e6e..05d1c5112 100644 --- a/score/launch_manager/src/daemon/src/osal/BUILD +++ b/score/launch_manager/src/daemon/src/osal/BUILD @@ -56,8 +56,8 @@ lm_cc_test( srcs = ["details/posix/wait_for_file_UT.cpp"], deps = [ ":wait_for_file", + "//score/launch_manager/src/daemon/src/configuration:component_config", "@googletest//:gtest_main", - "@score_baselibs//score/language/futurecpp", ], ) diff --git a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp index 8ec8936bd..9dbcd23dd 100644 --- a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp +++ b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp @@ -15,8 +15,6 @@ #include #include -#include - #include #include #include @@ -26,7 +24,7 @@ #include "score/mw/launch_manager/osal/wait_for_file.hpp" -using score::mw::lifecycle::internal::osal::FileWaitCondition; +using score::mw::lifecycle::internal::configuration::FileExistenceState; using score::mw::lifecycle::internal::osal::OsalReturnType; using score::mw::lifecycle::internal::osal::wait_for_file; @@ -38,8 +36,11 @@ constexpr std::chrono::milliseconds kPollInterval{1U}; /// Long enough that the tests below never hit it unintentionally; the timeout itself is covered by dedicated tests. constexpr std::chrono::milliseconds kWaitTimeout{60000U}; -constexpr auto kExists = FileWaitCondition::kExists; -constexpr auto kNotExisting = FileWaitCondition::kNotExisting; +/// Used where the condition can never be satisfied, so that the test does not wait for kWaitTimeout. +constexpr std::chrono::milliseconds kNoWait{0U}; + +constexpr auto kExists = FileExistenceState::Exists; +constexpr auto kNotExisting = FileExistenceState::NotExisting; class WaitForFileTest : public ::testing::Test { @@ -70,7 +71,6 @@ class WaitForFileTest : public ::testing::Test } std::string path_{}; - score::cpp::stop_source stop_source_{}; }; TEST_F(WaitForFileTest, ExistingFileIsReportedImmediately) @@ -80,58 +80,17 @@ TEST_F(WaitForFileTest, ExistingFileIsReportedImmediately) createFile(); const auto start = std::chrono::steady_clock::now(); - EXPECT_EQ( - wait_for_file(stop_source_.get_token(), path_, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); + EXPECT_EQ(wait_for_file(path_, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::milliseconds{1000U}); } -TEST_F(WaitForFileTest, AlreadyRequestedStopStillChecksOnce) -{ - RecordProperty("Description", "Verify that an already stopped token performs a single existence check."); - - createFile(); - static_cast(stop_source_.request_stop()); - - EXPECT_EQ( - wait_for_file(stop_source_.get_token(), path_, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); -} - -TEST_F(WaitForFileTest, MissingFileReturnsTimeoutOnStopRequest) -{ - RecordProperty( - "Description", - "Verify that waiting for a file which never appears returns kTimeout once a stop is " - "requested."); - - static_cast(stop_source_.request_stop()); - - EXPECT_EQ( - wait_for_file(stop_source_.get_token(), path_, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kTimeout); -} - -TEST_F(WaitForFileTest, StopRequestedWhileWaitingEndsTheWait) -{ - RecordProperty("Description", "Verify that a stop requested by another thread during the wait ends it."); - - std::thread stopper{[this]() { - std::this_thread::sleep_for(std::chrono::milliseconds{20U}); - static_cast(stop_source_.request_stop()); - }}; - - EXPECT_EQ( - wait_for_file(stop_source_.get_token(), path_, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kTimeout); - stopper.join(); -} - TEST_F(WaitForFileTest, MissingParentDirectoryTimesOut) { RecordProperty("Description", "Verify that a path below a non existing directory is treated as not yet created."); const std::string path = path_ + "/no_such_directory/file"; - static_cast(stop_source_.request_stop()); - EXPECT_EQ( - wait_for_file(stop_source_.get_token(), path, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kTimeout); + EXPECT_EQ(wait_for_file(path, kExists, kNoWait, kPollInterval), OsalReturnType::kTimeout); } TEST_F(WaitForFileTest, FileCreatedWhileWaitingIsDetected) @@ -143,8 +102,7 @@ TEST_F(WaitForFileTest, FileCreatedWhileWaitingIsDetected) createFile(); }}; - EXPECT_EQ( - wait_for_file(stop_source_.get_token(), path_, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); + EXPECT_EQ(wait_for_file(path_, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); creator.join(); } @@ -152,9 +110,7 @@ TEST_F(WaitForFileTest, DirectoryCountsAsExisting) { RecordProperty("Description", "Verify that the check is not restricted to regular files."); - EXPECT_EQ( - wait_for_file(stop_source_.get_token(), ::testing::TempDir(), kExists, kWaitTimeout, kPollInterval), - OsalReturnType::kSuccess); + EXPECT_EQ(wait_for_file(::testing::TempDir(), kExists, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); } TEST_F(WaitForFileTest, AbsentFileIsReportedImmediatelyForNotExisting) @@ -162,9 +118,7 @@ TEST_F(WaitForFileTest, AbsentFileIsReportedImmediatelyForNotExisting) RecordProperty("Description", "Verify that a file which is already absent satisfies kNotExisting without waiting."); const auto start = std::chrono::steady_clock::now(); - EXPECT_EQ( - wait_for_file(stop_source_.get_token(), path_, kNotExisting, kWaitTimeout, kPollInterval), - OsalReturnType::kSuccess); + EXPECT_EQ(wait_for_file(path_, kNotExisting, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::milliseconds{1000U}); } @@ -180,9 +134,7 @@ TEST_F(WaitForFileTest, FileRemovedWhileWaitingIsDetected) removeFile(); }}; - EXPECT_EQ( - wait_for_file(stop_source_.get_token(), path_, kNotExisting, kWaitTimeout, kPollInterval), - OsalReturnType::kSuccess); + EXPECT_EQ(wait_for_file(path_, kNotExisting, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); remover.join(); } @@ -194,8 +146,7 @@ TEST_F(WaitForFileTest, ExistingFileTimesOutForNotExisting) constexpr std::chrono::milliseconds timeout{50U}; const auto start = std::chrono::steady_clock::now(); - EXPECT_EQ( - wait_for_file(stop_source_.get_token(), path_, kNotExisting, timeout, kPollInterval), OsalReturnType::kTimeout); + EXPECT_EQ(wait_for_file(path_, kNotExisting, timeout, kPollInterval), OsalReturnType::kTimeout); EXPECT_GE(std::chrono::steady_clock::now() - start, timeout); } @@ -205,35 +156,35 @@ TEST_F(WaitForFileTest, MissingParentDirectoryCountsAsNotExisting) const std::string path = path_ + "/no_such_directory/file"; - EXPECT_EQ( - wait_for_file(stop_source_.get_token(), path, kNotExisting, kWaitTimeout, kPollInterval), - OsalReturnType::kSuccess); + EXPECT_EQ(wait_for_file(path, kNotExisting, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); } -TEST_F(WaitForFileTest, StopRequestedWhileWaitingForRemovalEndsTheWait) +TEST_F(WaitForFileTest, PathBelowARegularFileCountsAsNotExisting) { - RecordProperty("Description", "Verify that a stop request also ends a wait for a file to disappear."); + RecordProperty( + "Description", "Verify that ENOTDIR, raised by a regular file used as a directory, is not an error."); createFile(); + const std::string path = path_ + "/file"; - std::thread stopper{[this]() { - std::this_thread::sleep_for(std::chrono::milliseconds{20U}); - static_cast(stop_source_.request_stop()); - }}; + EXPECT_EQ(wait_for_file(path, kNotExisting, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); +} + +TEST_F(WaitForFileTest, PathBelowARegularFileTimesOutForExists) +{ + RecordProperty("Description", "Verify that ENOTDIR is treated as not yet created while waiting for kExists."); + + createFile(); + const std::string path = path_ + "/file"; - EXPECT_EQ( - wait_for_file(stop_source_.get_token(), path_, kNotExisting, kWaitTimeout, kPollInterval), - OsalReturnType::kTimeout); - stopper.join(); + EXPECT_EQ(wait_for_file(path, kExists, kNoWait, kPollInterval), OsalReturnType::kTimeout); } TEST_F(WaitForFileTest, EmptyPathFailsForNotExisting) { RecordProperty("Description", "Verify that an empty path is rejected for kNotExisting instead of being polled."); - EXPECT_EQ( - wait_for_file(stop_source_.get_token(), std::string_view{}, kNotExisting, kWaitTimeout, kPollInterval), - OsalReturnType::kFail); + EXPECT_EQ(wait_for_file(std::string_view{}, kNotExisting, kWaitTimeout, kPollInterval), OsalReturnType::kFail); } TEST_F(WaitForFileTest, MissingFileReturnsTimeoutWhenTheTimeoutElapses) @@ -243,8 +194,7 @@ TEST_F(WaitForFileTest, MissingFileReturnsTimeoutWhenTheTimeoutElapses) constexpr std::chrono::milliseconds timeout{50U}; const auto start = std::chrono::steady_clock::now(); - EXPECT_EQ( - wait_for_file(stop_source_.get_token(), path_, kExists, timeout, kPollInterval), OsalReturnType::kTimeout); + EXPECT_EQ(wait_for_file(path_, kExists, timeout, kPollInterval), OsalReturnType::kTimeout); EXPECT_GE(std::chrono::steady_clock::now() - start, timeout); } @@ -254,18 +204,14 @@ TEST_F(WaitForFileTest, ZeroTimeoutStillChecksOnce) createFile(); - EXPECT_EQ( - wait_for_file(stop_source_.get_token(), path_, kExists, std::chrono::milliseconds{0U}, kPollInterval), - OsalReturnType::kSuccess); + EXPECT_EQ(wait_for_file(path_, kExists, kNoWait, kPollInterval), OsalReturnType::kSuccess); } TEST_F(WaitForFileTest, ZeroTimeoutOnMissingFileReturnsTimeout) { RecordProperty("Description", "Verify that a zero timeout does not wait for a file which does not exist yet."); - EXPECT_EQ( - wait_for_file(stop_source_.get_token(), path_, kExists, std::chrono::milliseconds{0U}, kPollInterval), - OsalReturnType::kTimeout); + EXPECT_EQ(wait_for_file(path_, kExists, kNoWait, kPollInterval), OsalReturnType::kTimeout); } TEST_F(WaitForFileTest, PollIntervalDoesNotExtendTheTimeout) @@ -276,8 +222,7 @@ TEST_F(WaitForFileTest, PollIntervalDoesNotExtendTheTimeout) constexpr std::chrono::milliseconds poll_interval{5000U}; const auto start = std::chrono::steady_clock::now(); - EXPECT_EQ( - wait_for_file(stop_source_.get_token(), path_, kExists, timeout, poll_interval), OsalReturnType::kTimeout); + EXPECT_EQ(wait_for_file(path_, kExists, timeout, poll_interval), OsalReturnType::kTimeout); EXPECT_LT(std::chrono::steady_clock::now() - start, poll_interval); } @@ -285,9 +230,7 @@ TEST_F(WaitForFileTest, EmptyPathFails) { RecordProperty("Description", "Verify that an empty path is rejected instead of being polled."); - EXPECT_EQ( - wait_for_file(stop_source_.get_token(), std::string_view{}, kExists, kWaitTimeout, kPollInterval), - OsalReturnType::kFail); + EXPECT_EQ(wait_for_file(std::string_view{}, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kFail); } TEST_F(WaitForFileTest, NonNullTerminatedPathFails) @@ -298,9 +241,7 @@ TEST_F(WaitForFileTest, NonNullTerminatedPathFails) const std::string path = path_ + "x"; const std::string_view not_terminated{path.data(), path.size() - 1U}; - EXPECT_EQ( - wait_for_file(stop_source_.get_token(), not_terminated, kExists, kWaitTimeout, kPollInterval), - OsalReturnType::kFail); + EXPECT_EQ(wait_for_file(not_terminated, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kFail); } TEST_F(WaitForFileTest, TooLongPathFails) @@ -308,8 +249,7 @@ TEST_F(WaitForFileTest, TooLongPathFails) RecordProperty("Description", "Verify that a path which does not fit into PATH_MAX is rejected."); const std::string path(PATH_MAX + 1U, 'a'); - EXPECT_EQ( - wait_for_file(stop_source_.get_token(), path, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kFail); + EXPECT_EQ(wait_for_file(path, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kFail); } } // namespace From 100c965f534b0cd4f4f272d0ca62cbde6e74d48e Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Fri, 21 Aug 2026 10:12:33 +0100 Subject: [PATCH 07/16] Fixing tests --- .../launch_manager/src/daemon/src/osal/BUILD | 5 + .../src/osal/details/posix/wait_for_file.cpp | 47 ++++--- .../osal/details/posix/wait_for_file_UT.cpp | 116 +++++++++++++----- .../src/daemon/src/osal/wait_for_file.hpp | 5 +- .../file_state/common/file_modifier.cpp | 14 ++- .../file_state/exists/ready_condition_file.py | 4 +- .../ready_file_verification_process.cpp | 8 -- .../ready_condition_file_not_existing.json | 10 -- .../ready_condition_file_not_existing.py | 4 +- .../ready_file_verification_process.cpp | 8 -- 10 files changed, 138 insertions(+), 83 deletions(-) diff --git a/score/launch_manager/src/daemon/src/osal/BUILD b/score/launch_manager/src/daemon/src/osal/BUILD index 05d1c5112..8ae7f38b5 100644 --- a/score/launch_manager/src/daemon/src/osal/BUILD +++ b/score/launch_manager/src/daemon/src/osal/BUILD @@ -48,6 +48,8 @@ cc_library( ":return_types", "//score/launch_manager/src/daemon/src/configuration:component_config", "@score_baselibs//score/language/futurecpp", + "@score_baselibs//score/os:errno", + "@score_baselibs//score/os:stat", ], ) @@ -58,6 +60,9 @@ lm_cc_test( ":wait_for_file", "//score/launch_manager/src/daemon/src/configuration:component_config", "@googletest//:gtest_main", + "@score_baselibs//score/os:errno", + "@score_baselibs//score/os:stat", + "@score_baselibs//score/os/mocklib:stat_mock", ], ) diff --git a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp index 0591054fe..f0cb1dfb0 100644 --- a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp +++ b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp @@ -11,14 +11,14 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#include - #include -#include #include #include #include +#include "score/os/errno.h" +#include "score/os/stat.h" + #include "score/mw/launch_manager/osal/wait_for_file.hpp" namespace score::mw::lifecycle::internal::osal @@ -28,7 +28,8 @@ OsalReturnType wait_for_file( std::string_view path, configuration::FileExistenceState condition, std::chrono::milliseconds timeout, - std::chrono::milliseconds poll_interval) noexcept + std::chrono::milliseconds poll_interval, + const score::os::Stat& stat_os) noexcept { // note: QNX has a wait_for API, however in the future a stop_token should // be used, when using the API call we wouldn't be able to check the @@ -45,36 +46,34 @@ OsalReturnType wait_for_file( while (true) { - struct stat info{}; + score::os::StatBuffer info{}; - if (::stat(path.data(), &info) == 0) + const auto result = stat_os.stat(path.data(), info); + if (result.has_value()) { if (wait_for_existence) { return OsalReturnType::kSuccess; } } - else + // treat file or dir not existing as the same + else if ( + (result.error() == score::os::Error::Code::kNoSuchFileOrDirectory) || + (result.error() == score::os::Error::Code::kNotADirectory)) { - switch (errno) + if (!wait_for_existence) { - case (ENOENT): - [[fallthrough]]; // threat file or dir not existing as the same - case (ENOTDIR): - if (!wait_for_existence) - { - return OsalReturnType::kSuccess; - } - break; - - case (EINTR): - // retry - break; - - default: - return OsalReturnType::kFail; + return OsalReturnType::kSuccess; } } + else if (result.error() == score::os::Error::Code::kOperationWasInterruptedBySignal) + { + // retry + } + else + { + return OsalReturnType::kFail; + } const auto now = std::chrono::steady_clock::now(); if (now >= deadline) @@ -82,7 +81,7 @@ OsalReturnType wait_for_file( return OsalReturnType::kTimeout; } - // Never sleep past the deadline, so the timeout is honoured even with a coarse poll interval. + // never sleep past the deadline const auto remaining = deadline - now; std::this_thread::sleep_for(std::min(poll_interval, remaining)); } diff --git a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp index 9dbcd23dd..142fc5656 100644 --- a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp +++ b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp @@ -10,16 +10,26 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ +#include #include +#include #include #include +#include "score/os/errno.h" +#include "score/os/mocklib/stat_mock.h" + +#include +#include + #include #include +#include #include #include #include +#include #include #include "score/mw/launch_manager/osal/wait_for_file.hpp" @@ -34,7 +44,10 @@ namespace constexpr std::chrono::milliseconds kPollInterval{1U}; /// Long enough that the tests below never hit it unintentionally; the timeout itself is covered by dedicated tests. -constexpr std::chrono::milliseconds kWaitTimeout{60000U}; +constexpr std::chrono::milliseconds kWaitTimeout{1000U}; + +/// Upper bound for a call which must return on its first check instead of polling. +constexpr std::chrono::milliseconds kImmediate{200U}; /// Used where the condition can never be satisfied, so that the test does not wait for kWaitTimeout. constexpr std::chrono::milliseconds kNoWait{0U}; @@ -51,23 +64,26 @@ class WaitForFileTest : public ::testing::Test RecordProperty("DerivationTechnique", "explorative-testing"); path_ = std::string{::testing::TempDir()} + "wait_for_file_UT_" + std::to_string(getpid()); - static_cast(std::remove(path_.c_str())); + std::filesystem::create_directory(::testing::TempDir()); + static_cast(std::filesystem::remove(path_.c_str())); } void TearDown() override { - static_cast(std::remove(path_.c_str())); + std::filesystem::remove(path_.c_str()); } void createFile() const { - std::ofstream file{path_}; - ASSERT_TRUE(file.is_open()); + int fd = ::open(path_.c_str(), O_RDWR | O_CREAT); + ASSERT_TRUE(fd > 0) << "ERRNO: " << errno << " Desc: " << std::strerror(errno) << " OPENING PATH: " << path_; + ::close(fd); } void removeFile() const { - ASSERT_EQ(std::remove(path_.c_str()), 0); + std::error_code ec{}; + ASSERT_TRUE(std::filesystem::remove(path_.c_str(), ec)) << ec.message(); } std::string path_{}; @@ -81,7 +97,7 @@ TEST_F(WaitForFileTest, ExistingFileIsReportedImmediately) const auto start = std::chrono::steady_clock::now(); EXPECT_EQ(wait_for_file(path_, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); - EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::milliseconds{1000U}); + EXPECT_LT(std::chrono::steady_clock::now() - start, kImmediate); } TEST_F(WaitForFileTest, MissingParentDirectoryTimesOut) @@ -119,7 +135,7 @@ TEST_F(WaitForFileTest, AbsentFileIsReportedImmediatelyForNotExisting) const auto start = std::chrono::steady_clock::now(); EXPECT_EQ(wait_for_file(path_, kNotExisting, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); - EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::milliseconds{1000U}); + EXPECT_LT(std::chrono::steady_clock::now() - start, kImmediate); } TEST_F(WaitForFileTest, FileRemovedWhileWaitingIsDetected) @@ -159,27 +175,6 @@ TEST_F(WaitForFileTest, MissingParentDirectoryCountsAsNotExisting) EXPECT_EQ(wait_for_file(path, kNotExisting, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); } -TEST_F(WaitForFileTest, PathBelowARegularFileCountsAsNotExisting) -{ - RecordProperty( - "Description", "Verify that ENOTDIR, raised by a regular file used as a directory, is not an error."); - - createFile(); - const std::string path = path_ + "/file"; - - EXPECT_EQ(wait_for_file(path, kNotExisting, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); -} - -TEST_F(WaitForFileTest, PathBelowARegularFileTimesOutForExists) -{ - RecordProperty("Description", "Verify that ENOTDIR is treated as not yet created while waiting for kExists."); - - createFile(); - const std::string path = path_ + "/file"; - - EXPECT_EQ(wait_for_file(path, kExists, kNoWait, kPollInterval), OsalReturnType::kTimeout); -} - TEST_F(WaitForFileTest, EmptyPathFailsForNotExisting) { RecordProperty("Description", "Verify that an empty path is rejected for kNotExisting instead of being polled."); @@ -252,4 +247,67 @@ TEST_F(WaitForFileTest, TooLongPathFails) EXPECT_EQ(wait_for_file(path, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kFail); } +/// Errors which no filesystem reachable from a unit test raises reliably are injected through the score::os::Stat +/// seam. ENOTDIR in particular cannot be provoked on QNX, where /tmp is a flat shmem namespace that resolves +/// "/child" to the file itself instead of failing. +class WaitForFileMockTest : public ::testing::Test +{ + protected: + void SetUp() override + { + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "explorative-testing"); + } + + static score::cpp::expected_blank failWith(const std::int32_t error_number) + { + return score::cpp::make_unexpected(score::os::Error::createFromErrno(error_number)); + } + + static constexpr auto kPath = "/some/path"; + + score::os::StatMock stat_mock_{}; +}; + +TEST_F(WaitForFileMockTest, NotADirectoryCountsAsNotExisting) +{ + RecordProperty("Description", "Verify that ENOTDIR is treated as the path not existing."); + + EXPECT_CALL(stat_mock_, stat(::testing::StrEq(kPath), ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(failWith(ENOTDIR))); + + EXPECT_EQ(wait_for_file(kPath, kNotExisting, kWaitTimeout, kPollInterval, stat_mock_), OsalReturnType::kSuccess); +} + +TEST_F(WaitForFileMockTest, NotADirectoryTimesOutForExists) +{ + RecordProperty("Description", "Verify that ENOTDIR is treated as not yet created while waiting for kExists."); + + EXPECT_CALL(stat_mock_, stat(::testing::StrEq(kPath), ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(failWith(ENOTDIR))); + + EXPECT_EQ(wait_for_file(kPath, kExists, kNoWait, kPollInterval, stat_mock_), OsalReturnType::kTimeout); +} + +TEST_F(WaitForFileMockTest, InterruptedStatIsRetried) +{ + RecordProperty("Description", "Verify that EINTR neither ends the wait nor is reported as a failure."); + + EXPECT_CALL(stat_mock_, stat(::testing::StrEq(kPath), ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(failWith(EINTR))) + .WillOnce(::testing::Return(score::cpp::expected_blank{})); + + EXPECT_EQ(wait_for_file(kPath, kExists, kWaitTimeout, kPollInterval, stat_mock_), OsalReturnType::kSuccess); +} + +TEST_F(WaitForFileMockTest, UnexpectedErrorFails) +{ + RecordProperty("Description", "Verify that an error other than ENOENT, ENOTDIR or EINTR aborts the wait."); + + EXPECT_CALL(stat_mock_, stat(::testing::StrEq(kPath), ::testing::_, ::testing::_)) + .WillOnce(::testing::Return(failWith(EACCES))); + + EXPECT_EQ(wait_for_file(kPath, kExists, kWaitTimeout, kPollInterval, stat_mock_), OsalReturnType::kFail); +} + } // namespace diff --git a/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp b/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp index 3899dd8b8..b31fd9cc3 100644 --- a/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp +++ b/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp @@ -17,6 +17,7 @@ #include #include "score/mw/launch_manager/configuration/component_config.hpp" +#include "score/os/stat.h" #include #include #include @@ -32,11 +33,13 @@ namespace score::mw::lifecycle::internal::osal /// @param condition The path state to wait for. /// @param timeout The maximum time to wait for the condition. /// @param poll_interval The time between two consecutive existence checks. +/// @param stat_os Optional score::os::Stat instance used to query the path. OsalReturnType wait_for_file( std::string_view path, configuration::FileExistenceState condition, std::chrono::milliseconds timeout, - std::chrono::milliseconds poll_interval) noexcept; + std::chrono::milliseconds poll_interval, + const score::os::Stat& stat_os = score::os::Stat::instance()) noexcept; } // namespace score::mw::lifecycle::internal::osal diff --git a/tests/integration/ready_conditions/file_state/common/file_modifier.cpp b/tests/integration/ready_conditions/file_state/common/file_modifier.cpp index f6305e5b8..8a80a0377 100644 --- a/tests/integration/ready_conditions/file_state/common/file_modifier.cpp +++ b/tests/integration/ready_conditions/file_state/common/file_modifier.cpp @@ -14,8 +14,12 @@ #include "tests/utils/test_helper/test_helper.hpp" #include #include +#include #include +#include #include +#include +#include #include enum class Operation : std::uint8_t @@ -52,7 +56,11 @@ int main(int argc, char** argv) << std::endl; return EXIT_FAILURE; } - const std::string_view prog_name{argv[0]}; + + const std::string_view full_path{argv[0]}; + const std::size_t base_name_pos = full_path.rfind('/'); + assert(base_name_pos != std::string_view::npos); + const std::string_view prog_name{std::next(full_path.begin(), base_name_pos + 1)}; if (prog_name == "file_creator") { @@ -62,6 +70,10 @@ int main(int argc, char** argv) { g_operation = Operation::Delete; } + else + { + assert(false && "Program has to be called either file_creator or file_deletor"); + } g_file_path = std::string_view{argv[1]}; g_modify_delay = std::chrono::milliseconds{std::stoi(argv[2])}; diff --git a/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py index a8604ac33..d6a11198a 100644 --- a/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py +++ b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py @@ -43,4 +43,6 @@ def test_ready_condition_file(target, setup_test, assert_test_results, remote_te timeout_s=3.0, ) - assert_test_results({"ready_file_verification_process.xml", "file_creator.xml"}) + assert_test_results( + {"ready_file_verification_process.xml", "reporting_process_file_creator.xml"} + ) diff --git a/tests/integration/ready_conditions/file_state/exists/ready_file_verification_process.cpp b/tests/integration/ready_conditions/file_state/exists/ready_file_verification_process.cpp index d09f93b52..024deb651 100644 --- a/tests/integration/ready_conditions/file_state/exists/ready_file_verification_process.cpp +++ b/tests/integration/ready_conditions/file_state/exists/ready_file_verification_process.cpp @@ -18,14 +18,6 @@ #include "tests/utils/test_helper/test_helper.hpp" -// Given a correct configuration with: -// - An initial Run Target named "Startup" -// - Startup contains a component with a file_state ready condition on a file whose state it only -// changes after a delay, and the Component "verification_component" -// - verification_component depends on the component owning the file - -// When launch manager is started - std::string g_ready_file; bool g_expect_existing = true; diff --git a/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json index 2ae056fd7..fccebe8d5 100644 --- a/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json +++ b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json @@ -49,11 +49,6 @@ "polling_interval": 0.01 } } - }, - "deployment_config": { - "environmental_variables": { - "PROCESSIDENTIFIER": "file_deleting_component" - } } }, "verification_component": { @@ -73,11 +68,6 @@ "ready_condition": { "process_state": "Terminated" } - }, - "deployment_config": { - "environmental_variables": { - "PROCESSIDENTIFIER": "verification_component" - } } } }, diff --git a/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py index 273b8c722..3e734ae50 100644 --- a/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py +++ b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py @@ -52,4 +52,6 @@ def test_ready_condition_file_not_existing( timeout_s=3.0, ) - assert_test_results({"ready_file_verification_process.xml", "file_deletor.xml"}) + assert_test_results( + {"ready_file_verification_process.xml", "reporting_process_file_deletor.xml"} + ) diff --git a/tests/integration/ready_conditions/file_state/not_existing/ready_file_verification_process.cpp b/tests/integration/ready_conditions/file_state/not_existing/ready_file_verification_process.cpp index d09f93b52..024deb651 100644 --- a/tests/integration/ready_conditions/file_state/not_existing/ready_file_verification_process.cpp +++ b/tests/integration/ready_conditions/file_state/not_existing/ready_file_verification_process.cpp @@ -18,14 +18,6 @@ #include "tests/utils/test_helper/test_helper.hpp" -// Given a correct configuration with: -// - An initial Run Target named "Startup" -// - Startup contains a component with a file_state ready condition on a file whose state it only -// changes after a delay, and the Component "verification_component" -// - verification_component depends on the component owning the file - -// When launch manager is started - std::string g_ready_file; bool g_expect_existing = true; From af40eabd7eb24e4ad74cecc1a1d209e19611bb48 Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Wed, 26 Aug 2026 09:00:18 +0100 Subject: [PATCH 08/16] Doing comments --- .../launch_manager/src/daemon/src/osal/BUILD | 2 +- .../src/osal/details/posix/wait_for_file.cpp | 35 ++- .../osal/details/posix/wait_for_file_UT.cpp | 286 +++--------------- .../details/process_info_node.cpp | 18 +- .../file_state/common/file_modifier.cpp | 8 +- 5 files changed, 88 insertions(+), 261 deletions(-) diff --git a/score/launch_manager/src/daemon/src/osal/BUILD b/score/launch_manager/src/daemon/src/osal/BUILD index 8ae7f38b5..f9152d20a 100644 --- a/score/launch_manager/src/daemon/src/osal/BUILD +++ b/score/launch_manager/src/daemon/src/osal/BUILD @@ -47,9 +47,9 @@ cc_library( deps = [ ":return_types", "//score/launch_manager/src/daemon/src/configuration:component_config", - "@score_baselibs//score/language/futurecpp", "@score_baselibs//score/os:errno", "@score_baselibs//score/os:stat", + "@score_baselibs//score/result:error", ], ) diff --git a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp index f0cb1dfb0..a5f6b60dc 100644 --- a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp +++ b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp @@ -13,11 +13,13 @@ #include #include +#include #include #include #include "score/os/errno.h" #include "score/os/stat.h" +#include "score/result/error_code.h" #include "score/mw/launch_manager/osal/wait_for_file.hpp" @@ -56,24 +58,29 @@ OsalReturnType wait_for_file( return OsalReturnType::kSuccess; } } - // treat file or dir not existing as the same - else if ( - (result.error() == score::os::Error::Code::kNoSuchFileOrDirectory) || - (result.error() == score::os::Error::Code::kNotADirectory)) + else { - if (!wait_for_existence) + switch (result.error().GetOsDependentErrorCode()) { - return OsalReturnType::kSuccess; + case ENOENT: + // treat file or dir not existing as the same + [[fallthrough]]; + case ENOTDIR: + { + if (!wait_for_existence) + { + return OsalReturnType::kSuccess; + } + break; + }; + case EINTR: + break; + default: + { + return OsalReturnType::kFail; + } } } - else if (result.error() == score::os::Error::Code::kOperationWasInterruptedBySignal) - { - // retry - } - else - { - return OsalReturnType::kFail; - } const auto now = std::chrono::steady_clock::now(); if (now >= deadline) diff --git a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp index 142fc5656..56e168b05 100644 --- a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp +++ b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp @@ -13,10 +13,6 @@ #include #include -#include -#include -#include - #include "score/os/errno.h" #include "score/os/mocklib/stat_mock.h" @@ -24,36 +20,21 @@ #include #include -#include -#include -#include #include #include -#include -#include #include "score/mw/launch_manager/osal/wait_for_file.hpp" using score::mw::lifecycle::internal::configuration::FileExistenceState; using score::mw::lifecycle::internal::osal::OsalReturnType; using score::mw::lifecycle::internal::osal::wait_for_file; +using ::testing::_; namespace { constexpr std::chrono::milliseconds kPollInterval{1U}; - -/// Long enough that the tests below never hit it unintentionally; the timeout itself is covered by dedicated tests. -constexpr std::chrono::milliseconds kWaitTimeout{1000U}; - -/// Upper bound for a call which must return on its first check instead of polling. -constexpr std::chrono::milliseconds kImmediate{200U}; - -/// Used where the condition can never be satisfied, so that the test does not wait for kWaitTimeout. -constexpr std::chrono::milliseconds kNoWait{0U}; - -constexpr auto kExists = FileExistenceState::Exists; -constexpr auto kNotExisting = FileExistenceState::NotExisting; +constexpr std::chrono::milliseconds kWaitTimeout{2U}; class WaitForFileTest : public ::testing::Test { @@ -62,252 +43,85 @@ class WaitForFileTest : public ::testing::Test { RecordProperty("TestType", "interface-test"); RecordProperty("DerivationTechnique", "explorative-testing"); - - path_ = std::string{::testing::TempDir()} + "wait_for_file_UT_" + std::to_string(getpid()); - std::filesystem::create_directory(::testing::TempDir()); - static_cast(std::filesystem::remove(path_.c_str())); - } - - void TearDown() override - { - std::filesystem::remove(path_.c_str()); - } - - void createFile() const - { - int fd = ::open(path_.c_str(), O_RDWR | O_CREAT); - ASSERT_TRUE(fd > 0) << "ERRNO: " << errno << " Desc: " << std::strerror(errno) << " OPENING PATH: " << path_; - ::close(fd); - } - - void removeFile() const - { - std::error_code ec{}; - ASSERT_TRUE(std::filesystem::remove(path_.c_str(), ec)) << ec.message(); } - - std::string path_{}; }; -TEST_F(WaitForFileTest, ExistingFileIsReportedImmediately) -{ - RecordProperty("Description", "Verify that a file which already exists is detected without waiting."); - - createFile(); - - const auto start = std::chrono::steady_clock::now(); - EXPECT_EQ(wait_for_file(path_, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); - EXPECT_LT(std::chrono::steady_clock::now() - start, kImmediate); -} - -TEST_F(WaitForFileTest, MissingParentDirectoryTimesOut) -{ - RecordProperty("Description", "Verify that a path below a non existing directory is treated as not yet created."); - - const std::string path = path_ + "/no_such_directory/file"; - - EXPECT_EQ(wait_for_file(path, kExists, kNoWait, kPollInterval), OsalReturnType::kTimeout); -} - -TEST_F(WaitForFileTest, FileCreatedWhileWaitingIsDetected) -{ - RecordProperty("Description", "Verify that a file created by another thread during the wait is detected."); - - std::thread creator{[this]() { - std::this_thread::sleep_for(std::chrono::milliseconds{20U}); - createFile(); - }}; - - EXPECT_EQ(wait_for_file(path_, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); - creator.join(); -} - -TEST_F(WaitForFileTest, DirectoryCountsAsExisting) -{ - RecordProperty("Description", "Verify that the check is not restricted to regular files."); - - EXPECT_EQ(wait_for_file(::testing::TempDir(), kExists, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); -} - -TEST_F(WaitForFileTest, AbsentFileIsReportedImmediatelyForNotExisting) -{ - RecordProperty("Description", "Verify that a file which is already absent satisfies kNotExisting without waiting."); - - const auto start = std::chrono::steady_clock::now(); - EXPECT_EQ(wait_for_file(path_, kNotExisting, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); - EXPECT_LT(std::chrono::steady_clock::now() - start, kImmediate); -} - -TEST_F(WaitForFileTest, FileRemovedWhileWaitingIsDetected) +TEST_F(WaitForFileTest, FileExists) { RecordProperty( - "Description", "Verify that a file removed by another thread during the wait satisfies kNotExisting."); - - createFile(); - - std::thread remover{[this]() { - std::this_thread::sleep_for(std::chrono::milliseconds{20U}); - removeFile(); - }}; - - EXPECT_EQ(wait_for_file(path_, kNotExisting, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); - remover.join(); -} - -TEST_F(WaitForFileTest, ExistingFileTimesOutForNotExisting) -{ - RecordProperty("Description", "Verify that a file which never disappears returns kTimeout for kNotExisting."); - - createFile(); - constexpr std::chrono::milliseconds timeout{50U}; - - const auto start = std::chrono::steady_clock::now(); - EXPECT_EQ(wait_for_file(path_, kNotExisting, timeout, kPollInterval), OsalReturnType::kTimeout); - EXPECT_GE(std::chrono::steady_clock::now() - start, timeout); -} - -TEST_F(WaitForFileTest, MissingParentDirectoryCountsAsNotExisting) -{ - RecordProperty("Description", "Verify that a path below a non existing directory satisfies kNotExisting."); - - const std::string path = path_ + "/no_such_directory/file"; + "Description", "Verify that using FileExistenceState::Exists will return sucess if that stat returns success"); - EXPECT_EQ(wait_for_file(path, kNotExisting, kWaitTimeout, kPollInterval), OsalReturnType::kSuccess); + score::os::StatMock mock{}; + EXPECT_CALL(mock, stat(_, _, true)).WillOnce(testing::Return(score::cpp::expected_blank{})); + EXPECT_EQ( + wait_for_file("/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, mock), + OsalReturnType::kSuccess); } -TEST_F(WaitForFileTest, EmptyPathFailsForNotExisting) +TEST_F(WaitForFileTest, FileNotExisting) { - RecordProperty("Description", "Verify that an empty path is rejected for kNotExisting instead of being polled."); - - EXPECT_EQ(wait_for_file(std::string_view{}, kNotExisting, kWaitTimeout, kPollInterval), OsalReturnType::kFail); -} - -TEST_F(WaitForFileTest, MissingFileReturnsTimeoutWhenTheTimeoutElapses) -{ - RecordProperty("Description", "Verify that the wait ends with kTimeout once the given timeout has elapsed."); - - constexpr std::chrono::milliseconds timeout{50U}; - - const auto start = std::chrono::steady_clock::now(); - EXPECT_EQ(wait_for_file(path_, kExists, timeout, kPollInterval), OsalReturnType::kTimeout); - EXPECT_GE(std::chrono::steady_clock::now() - start, timeout); -} - -TEST_F(WaitForFileTest, ZeroTimeoutStillChecksOnce) -{ - RecordProperty("Description", "Verify that an existing file is detected even with a zero timeout."); - - createFile(); - - EXPECT_EQ(wait_for_file(path_, kExists, kNoWait, kPollInterval), OsalReturnType::kSuccess); -} - -TEST_F(WaitForFileTest, ZeroTimeoutOnMissingFileReturnsTimeout) -{ - RecordProperty("Description", "Verify that a zero timeout does not wait for a file which does not exist yet."); - - EXPECT_EQ(wait_for_file(path_, kExists, kNoWait, kPollInterval), OsalReturnType::kTimeout); -} - -TEST_F(WaitForFileTest, PollIntervalDoesNotExtendTheTimeout) -{ - RecordProperty("Description", "Verify that a poll interval longer than the timeout does not delay the kTimeout."); - - constexpr std::chrono::milliseconds timeout{20U}; - constexpr std::chrono::milliseconds poll_interval{5000U}; - - const auto start = std::chrono::steady_clock::now(); - EXPECT_EQ(wait_for_file(path_, kExists, timeout, poll_interval), OsalReturnType::kTimeout); - EXPECT_LT(std::chrono::steady_clock::now() - start, poll_interval); -} - -TEST_F(WaitForFileTest, EmptyPathFails) -{ - RecordProperty("Description", "Verify that an empty path is rejected instead of being polled."); + RecordProperty( + "Description", + "Verify that using FileExistenceState::NotExisting will return sucess if that stat returns ENOTDIR"); - EXPECT_EQ(wait_for_file(std::string_view{}, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kFail); + score::os::StatMock mock{}; + EXPECT_CALL(mock, stat(_, _, true)) + .WillOnce(testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOTDIR)))); + EXPECT_EQ( + wait_for_file("/some/path", FileExistenceState::NotExisting, kWaitTimeout, kPollInterval, mock), + OsalReturnType::kSuccess); } TEST_F(WaitForFileTest, NonNullTerminatedPathFails) { RecordProperty("Description", "Verify that a path which is not null terminated is rejected instead of polled."); - createFile(); - const std::string path = path_ + "x"; + score::os::StatMock mock{}; + const std::string path = "/some/pathx"; const std::string_view not_terminated{path.data(), path.size() - 1U}; - EXPECT_EQ(wait_for_file(not_terminated, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kFail); -} - -TEST_F(WaitForFileTest, TooLongPathFails) -{ - RecordProperty("Description", "Verify that a path which does not fit into PATH_MAX is rejected."); - - const std::string path(PATH_MAX + 1U, 'a'); - EXPECT_EQ(wait_for_file(path, kExists, kWaitTimeout, kPollInterval), OsalReturnType::kFail); + EXPECT_EQ( + wait_for_file(not_terminated, FileExistenceState::Exists, kWaitTimeout, kPollInterval, mock), + OsalReturnType::kFail); } -/// Errors which no filesystem reachable from a unit test raises reliably are injected through the score::os::Stat -/// seam. ENOTDIR in particular cannot be provoked on QNX, where /tmp is a flat shmem namespace that resolves -/// "/child" to the file itself instead of failing. -class WaitForFileMockTest : public ::testing::Test +TEST_F(WaitForFileTest, Timeout) { - protected: - void SetUp() override - { - RecordProperty("TestType", "interface-test"); - RecordProperty("DerivationTechnique", "explorative-testing"); - } - - static score::cpp::expected_blank failWith(const std::int32_t error_number) - { - return score::cpp::make_unexpected(score::os::Error::createFromErrno(error_number)); - } - - static constexpr auto kPath = "/some/path"; + RecordProperty("Description", "Verify that if an error is repeatedly given then the timeout fires."); - score::os::StatMock stat_mock_{}; -}; - -TEST_F(WaitForFileMockTest, NotADirectoryCountsAsNotExisting) -{ - RecordProperty("Description", "Verify that ENOTDIR is treated as the path not existing."); - - EXPECT_CALL(stat_mock_, stat(::testing::StrEq(kPath), ::testing::_, ::testing::_)) - .WillOnce(::testing::Return(failWith(ENOTDIR))); - - EXPECT_EQ(wait_for_file(kPath, kNotExisting, kWaitTimeout, kPollInterval, stat_mock_), OsalReturnType::kSuccess); -} - -TEST_F(WaitForFileMockTest, NotADirectoryTimesOutForExists) -{ - RecordProperty("Description", "Verify that ENOTDIR is treated as not yet created while waiting for kExists."); - - EXPECT_CALL(stat_mock_, stat(::testing::StrEq(kPath), ::testing::_, ::testing::_)) - .WillOnce(::testing::Return(failWith(ENOTDIR))); - - EXPECT_EQ(wait_for_file(kPath, kExists, kNoWait, kPollInterval, stat_mock_), OsalReturnType::kTimeout); + score::os::StatMock mock{}; + EXPECT_CALL(mock, stat(_, _, true)) + .WillRepeatedly(testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(EINTR)))); + EXPECT_EQ( + wait_for_file("/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, mock), + OsalReturnType::kTimeout); } -TEST_F(WaitForFileMockTest, InterruptedStatIsRetried) +TEST_F(WaitForFileTest, Error) { - RecordProperty("Description", "Verify that EINTR neither ends the wait nor is reported as a failure."); - - EXPECT_CALL(stat_mock_, stat(::testing::StrEq(kPath), ::testing::_, ::testing::_)) - .WillOnce(::testing::Return(failWith(EINTR))) - .WillOnce(::testing::Return(score::cpp::expected_blank{})); + RecordProperty("Description", "Verify if a unexpected error is recieved from the state call the wait will fail."); - EXPECT_EQ(wait_for_file(kPath, kExists, kWaitTimeout, kPollInterval, stat_mock_), OsalReturnType::kSuccess); + score::os::StatMock mock{}; + EXPECT_CALL(mock, stat(_, _, true)) + .WillRepeatedly(testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(EBADF)))); + EXPECT_EQ( + wait_for_file("/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, mock), + OsalReturnType::kFail); } -TEST_F(WaitForFileMockTest, UnexpectedErrorFails) +TEST_F(WaitForFileTest, NonNullTermindPathFails) { - RecordProperty("Description", "Verify that an error other than ENOENT, ENOTDIR or EINTR aborts the wait."); + RecordProperty( + "Description", "Verify if using FileExistenceState::Exists the stat is re-polled after the interval."); - EXPECT_CALL(stat_mock_, stat(::testing::StrEq(kPath), ::testing::_, ::testing::_)) - .WillOnce(::testing::Return(failWith(EACCES))); + score::os::StatMock mock{}; + EXPECT_CALL(mock, stat(_, _, true)) + .WillOnce(testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOTDIR)))) + .WillOnce(testing::Return(score::cpp::expected_blank{})); - EXPECT_EQ(wait_for_file(kPath, kExists, kWaitTimeout, kPollInterval, stat_mock_), OsalReturnType::kFail); + EXPECT_EQ( + wait_for_file("/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, mock), + OsalReturnType::kSuccess); } } // namespace diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp index 6d5bdea40..2e23aec0c 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp @@ -53,7 +53,7 @@ ProcessInfoNode::ProcessInfoNode( IComponent::RequestResult ProcessInfoNode::tryReportCompletion(score::mw::lifecycle::ProcessState new_state) { - ProcessState desired_state{ProcessState::kRunning}; + ProcessState desired_state; const auto& ready_condition = config_.component_properties.ready_condition; @@ -72,6 +72,10 @@ IComponent::RequestResult ProcessInfoNode::tryReportCompletion(score::mw::lifecy break; } } + else if constexpr (std::is_same_v) + { + desired_state = ProcessState::kRunning; + } }, ready_condition); @@ -290,10 +294,9 @@ score::cpp::expected_blank ProcessInfoNode::handlePr { const bool is_native = configuration::ApplicationType::Native == config_.component_properties.application_profile.application_type; - bool ready_condition_met = false; - std::visit( - [this, &ready_condition_met, is_native](auto&& arg) { + const bool ready_condition_met = std::visit( + [this, is_native](auto&& arg) -> bool { using T = std::decay_t; if constexpr (std::is_same_v) @@ -301,13 +304,12 @@ score::cpp::expected_blank ProcessInfoNode::handlePr if (is_native) { // A native process does not report kRunning, so its status is the only readiness indication. - ready_condition_met = (0 == status_); - return; + return status_ == 0; } auto wait_res = process_handling_.process_interface_->waitForkRunning( sync_, std::chrono::milliseconds(config_.deployment_config.ready_timeout_ms)); - ready_condition_met = wait_res == osal::OsalReturnType::kSuccess && 0 == status_; + return (wait_res == osal::OsalReturnType::kSuccess) && (status_ == 0); } else if constexpr (std::is_same_v) { @@ -316,7 +318,7 @@ score::cpp::expected_blank ProcessInfoNode::handlePr arg.state, std::chrono::milliseconds(config_.deployment_config.ready_timeout_ms), arg.polling_interval); - ready_condition_met = (osal::OsalReturnType::kSuccess == wait_res) && (0 == status_); + return (wait_res == osal::OsalReturnType::kSuccess) && (status_ == 0); } }, config_.component_properties.ready_condition); diff --git a/tests/integration/ready_conditions/file_state/common/file_modifier.cpp b/tests/integration/ready_conditions/file_state/common/file_modifier.cpp index 8a80a0377..b7acfe14e 100644 --- a/tests/integration/ready_conditions/file_state/common/file_modifier.cpp +++ b/tests/integration/ready_conditions/file_state/common/file_modifier.cpp @@ -59,7 +59,10 @@ int main(int argc, char** argv) const std::string_view full_path{argv[0]}; const std::size_t base_name_pos = full_path.rfind('/'); - assert(base_name_pos != std::string_view::npos); + if (base_name_pos == std::string_view::npos) + { + return EXIT_FAILURE; + }; const std::string_view prog_name{std::next(full_path.begin(), base_name_pos + 1)}; if (prog_name == "file_creator") @@ -72,7 +75,8 @@ int main(int argc, char** argv) } else { - assert(false && "Program has to be called either file_creator or file_deletor"); + std::cerr << "Program has to be called either file_creator or file_deletor" << std::endl; + return EXIT_FAILURE; } g_file_path = std::string_view{argv[1]}; From f4673a4593b294b6a04ae2a22a4c5d1ea3010c7e Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Thu, 27 Aug 2026 16:24:49 +0100 Subject: [PATCH 09/16] Adding timeout tests --- .../ready_conditions/file_state/exists/BUILD | 8 +- .../file_state/exists/fake_control_client.cpp | 53 ++++++++++++ .../exists/ready_condition_file.json | 81 +++++++++++-------- .../file_state/exists/ready_condition_file.py | 2 +- .../ready_file_verification_process.cpp | 71 ---------------- .../file_state/not_existing/BUILD | 8 +- .../not_existing/fake_control_client.cpp | 53 ++++++++++++ .../ready_condition_file_not_existing.json | 60 +++++++++----- .../ready_condition_file_not_existing.py | 3 +- .../ready_file_verification_process.cpp | 71 ---------------- 10 files changed, 206 insertions(+), 204 deletions(-) create mode 100644 tests/integration/ready_conditions/file_state/exists/fake_control_client.cpp delete mode 100644 tests/integration/ready_conditions/file_state/exists/ready_file_verification_process.cpp create mode 100644 tests/integration/ready_conditions/file_state/not_existing/fake_control_client.cpp delete mode 100644 tests/integration/ready_conditions/file_state/not_existing/ready_file_verification_process.cpp diff --git a/tests/integration/ready_conditions/file_state/exists/BUILD b/tests/integration/ready_conditions/file_state/exists/BUILD index f451b4c0b..e646a3503 100644 --- a/tests/integration/ready_conditions/file_state/exists/BUILD +++ b/tests/integration/ready_conditions/file_state/exists/BUILD @@ -14,9 +14,11 @@ load("@rules_cc//cc:cc_binary.bzl", "cc_binary") load("//tests/utils/bazel:integration.bzl", "integration_test") cc_binary( - name = "ready_file_verification_process", - srcs = ["ready_file_verification_process.cpp"], + name = "fake_control_client", + srcs = ["fake_control_client.cpp"], deps = [ + "//score/launch_manager:control_cc", + "//score/launch_manager:lifecycle_cc", "//tests/utils/test_helper", "@googletest//:gtest_main", ], @@ -27,7 +29,7 @@ integration_test( srcs = ["ready_condition_file.py"], binaries = [ "//tests/integration/ready_conditions/file_state/common:file_creator", - ":ready_file_verification_process", + ":fake_control_client", "//score/launch_manager", ], config = ":ready_condition_file.json", diff --git a/tests/integration/ready_conditions/file_state/exists/fake_control_client.cpp b/tests/integration/ready_conditions/file_state/exists/fake_control_client.cpp new file mode 100644 index 000000000..888c40a6f --- /dev/null +++ b/tests/integration/ready_conditions/file_state/exists/fake_control_client.cpp @@ -0,0 +1,53 @@ +/******************************************************************************** + * 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 + +#include "tests/utils/test_helper/test_helper.hpp" +#include +#include + +TEST(FileStateExist, ControlClientTestDriver) +{ + score::mw::lifecycle::ControlClient client; + + ASSERT_TRUE(check_clean({test_end_location, fallback_file})); + + TEST_STEP("Report kRunning from ControlClientTestDriver") + { + score::mw::lifecycle::report_running(); + } + + TEST_STEP("Activate RunTarget that works") + { + score::cpp::stop_token stop_token; + auto result = client.ActivateRunTarget("working").Get(stop_token); + EXPECT_TRUE(result.has_value()); + } + + TEST_STEP("Activate RunTarget that times out") + { + score::cpp::stop_token stop_token; + auto result = client.ActivateRunTarget("timeout").Get(stop_token); + EXPECT_FALSE(result.has_value()) << "Activation should timeout and error"; + } + + TEST_STEP("Activate RunTarget Off") + { + client.ActivateRunTarget("Off"); + } +} + +int main() +{ + return TestRunner(__FILE__, TerminationBehavior::kContinue, TerminationNotification::kTestEnd).RunTests(); +} diff --git a/tests/integration/ready_conditions/file_state/exists/ready_condition_file.json b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.json index 0e3169216..a4ece0d1e 100644 --- a/tests/integration/ready_conditions/file_state/exists/ready_condition_file.json +++ b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.json @@ -3,21 +3,13 @@ "defaults": { "deployment_config": { "bin_dir": "/tmp/tests/ready_condition_file", - "ready_timeout": 1.0, + "ready_timeout": 0.1, "shutdown_timeout": 1.0, - "ready_recovery_action": { - "restart": { - "number_of_attempts": 0 - } - }, "recovery_action": { "switch_run_target": { "run_target": "fallback_run_target" } }, - "environmental_variables": { - "LD_LIBRARY_PATH": "/opt/lib" - }, "sandbox": { "uid": 0, "gid": 0, @@ -36,56 +28,75 @@ } }, "components": { + "fake_control_client": { + "component_properties": { + "binary_name": "fake_control_client", + "ready_condition": { + "process_state": "Running" + }, + "application_profile": { + "application_type": "State_Manager", + "is_self_terminating": false + } + } + }, "file_creating_component": { "component_properties": { "binary_name": "file_creator", "process_arguments": [ - "/tmp/tests/ready_condition_file/ready_file", "300" + "/tmp/tests/ready_condition_file/ready_file", "0" ], "ready_condition": { "file_state": { "file_path": "/tmp/tests/ready_condition_file/ready_file", "state": "Exists", - "polling_interval": 0.01 + "polling_interval": 0.1 } } - }, - "deployment_config": { - "environmental_variables": { - "PROCESSIDENTIFIER": "file_creating_component" - } } }, - "verification_component": { + "file_creating_component_timeout": { "component_properties": { - "binary_name": "ready_file_verification_process", + "binary_name": "file_creator", "process_arguments": [ - "/tmp/tests/ready_condition_file/ready_file", - "Exists" + "/tmp/tests/ready_condition_file/ready_file_2", "1000" ], - "depends_on": [ - "file_creating_component" - ], - "application_profile": { - "application_type": "Native", - "is_self_terminating": true - }, "ready_condition": { - "process_state": "Terminated" - } - }, - "deployment_config": { - "environmental_variables": { - "PROCESSIDENTIFIER": "verification_component" + "file_state": { + "file_path": "/tmp/tests/ready_condition_file/ready_file_2", + "state": "Exists", + "polling_interval": 0.1 + } } } } }, "run_targets": { "Startup": { + "depends_on": [ + "fake_control_client" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "working": { "depends_on": [ "file_creating_component", - "verification_component" + "fake_control_client" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "timeout": { + "depends_on": [ + "file_creating_component_timeout", + "fake_control_client" ], "recovery_action": { "switch_run_target": { @@ -99,6 +110,6 @@ "evaluation_cycle": 0.05 }, "fallback_run_target": { - "depends_on": [] + "depends_on": ["fake_control_client"] } } diff --git a/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py index d6a11198a..276794579 100644 --- a/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py +++ b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py @@ -44,5 +44,5 @@ def test_ready_condition_file(target, setup_test, assert_test_results, remote_te ) assert_test_results( - {"ready_file_verification_process.xml", "reporting_process_file_creator.xml"} + {"fake_control_client.xml", "reporting_process_file_creator.xml"} ) diff --git a/tests/integration/ready_conditions/file_state/exists/ready_file_verification_process.cpp b/tests/integration/ready_conditions/file_state/exists/ready_file_verification_process.cpp deleted file mode 100644 index 024deb651..000000000 --- a/tests/integration/ready_conditions/file_state/exists/ready_file_verification_process.cpp +++ /dev/null @@ -1,71 +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 - -#include -#include -#include - -#include "tests/utils/test_helper/test_helper.hpp" - -std::string g_ready_file; -bool g_expect_existing = true; - -TEST(ReadyConditionFile, ReadyConditionIsMetBeforeDependentStarts) -{ - // Then, this process is only started once the ready condition of the component it depends on is met: - TEST_STEP("Check the state of the ready condition file") - { - if (g_expect_existing) - { - EXPECT_TRUE(std::filesystem::exists(g_ready_file)) - << "'" << g_ready_file << "' does not exist, the dependent component was started too early"; - } - else - { - EXPECT_FALSE(std::filesystem::exists(g_ready_file)) - << "'" << g_ready_file << "' still exists, the dependent component was started too early"; - } - } -} - -int main(int argc, char** argv) -{ - if (argc != 3) - { - std::cerr << "Expected the path of the ready condition file and its expected state " - "('Exists' or 'NotExisting') as arguments" - << std::endl; - return EXIT_FAILURE; - } - g_ready_file = argv[1]; - - const std::string_view expected_state{argv[2]}; - if (expected_state == "Exists") - { - g_expect_existing = true; - } - else if (expected_state == "NotExisting") - { - g_expect_existing = false; - } - else - { - std::cerr << "Unknown expected state '" << expected_state << "', expected 'Exists' or 'NotExisting'" - << std::endl; - return EXIT_FAILURE; - } - - TestRunner runner{__FILE__, TerminationBehavior::kContinue, TerminationNotification::kTestEnd}; - return runner.RunTests(); -} diff --git a/tests/integration/ready_conditions/file_state/not_existing/BUILD b/tests/integration/ready_conditions/file_state/not_existing/BUILD index 6f41f4140..826621132 100644 --- a/tests/integration/ready_conditions/file_state/not_existing/BUILD +++ b/tests/integration/ready_conditions/file_state/not_existing/BUILD @@ -14,9 +14,11 @@ load("@rules_cc//cc:cc_binary.bzl", "cc_binary") load("//tests/utils/bazel:integration.bzl", "integration_test") cc_binary( - name = "ready_file_verification_process", - srcs = ["ready_file_verification_process.cpp"], + name = "fake_control_client", + srcs = ["fake_control_client.cpp"], deps = [ + "//score/launch_manager:control_cc", + "//score/launch_manager:lifecycle_cc", "//tests/utils/test_helper", "@googletest//:gtest_main", ], @@ -27,7 +29,7 @@ integration_test( srcs = ["ready_condition_file_not_existing.py"], binaries = [ "//tests/integration/ready_conditions/file_state/common:file_deletor", - ":ready_file_verification_process", + ":fake_control_client", "//score/launch_manager", ], config = ":ready_condition_file_not_existing.json", diff --git a/tests/integration/ready_conditions/file_state/not_existing/fake_control_client.cpp b/tests/integration/ready_conditions/file_state/not_existing/fake_control_client.cpp new file mode 100644 index 000000000..33f510e52 --- /dev/null +++ b/tests/integration/ready_conditions/file_state/not_existing/fake_control_client.cpp @@ -0,0 +1,53 @@ +/******************************************************************************** + * 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 + +#include "tests/utils/test_helper/test_helper.hpp" +#include +#include + +TEST(FileStateNotExistng, ControlClientTestDriver) +{ + score::mw::lifecycle::ControlClient client; + + ASSERT_TRUE(check_clean({test_end_location, fallback_file})); + + TEST_STEP("Report kRunning from ControlClientTestDriver") + { + score::mw::lifecycle::report_running(); + } + + TEST_STEP("Activate RunTarget that works") + { + score::cpp::stop_token stop_token; + auto result = client.ActivateRunTarget("working").Get(stop_token); + EXPECT_TRUE(result.has_value()); + } + + TEST_STEP("Activate RunTarget that times out") + { + score::cpp::stop_token stop_token; + auto result = client.ActivateRunTarget("timeout").Get(stop_token); + EXPECT_FALSE(result.has_value()) << "Activation should timeout and error"; + } + + TEST_STEP("Activate RunTarget Off") + { + client.ActivateRunTarget("Off"); + } +} + +int main() +{ + return TestRunner(__FILE__, TerminationBehavior::kContinue, TerminationNotification::kTestEnd).RunTests(); +} diff --git a/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json index fccebe8d5..bbccc72ce 100644 --- a/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json +++ b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json @@ -3,7 +3,7 @@ "defaults": { "deployment_config": { "bin_dir": "/tmp/tests/ready_condition_file_not_exists", - "ready_timeout": 1.0, + "ready_timeout": 0.1, "shutdown_timeout": 1.0, "ready_recovery_action": { "restart": { @@ -15,9 +15,6 @@ "run_target": "fallback_run_target" } }, - "environmental_variables": { - "LD_LIBRARY_PATH": "/opt/lib" - }, "sandbox": { "uid": 0, "gid": 0, @@ -36,46 +33,71 @@ } }, "components": { + "fake_control_client": { + "component_properties": { + "binary_name": "fake_control_client", + "application_profile": { + "application_type": "State_Manager" + } + } + }, "file_deleting_component": { "component_properties": { "binary_name": "file_deletor", "process_arguments": [ - "/tmp/tests/ready_condition_file_not_exists/vanishing_file", "300" + "/tmp/tests/ready_condition_file_not_exists/vanishing_file", "0" ], "ready_condition": { "file_state": { "file_path": "/tmp/tests/ready_condition_file_not_exists/vanishing_file", "state": "NotExisting", - "polling_interval": 0.01 + "polling_interval": 0.1 } } } }, - "verification_component": { + "file_deleting_component_timeout": { "component_properties": { - "binary_name": "ready_file_verification_process", + "binary_name": "file_deletor", "process_arguments": [ - "/tmp/tests/ready_condition_file_not_exists/vanishing_file", - "NotExisting" + "/tmp/tests/ready_condition_file_not_exists/vanishing_file_2", "500" ], - "depends_on": [ - "file_deleting_component" - ], - "application_profile": { - "application_type": "Native", - "is_self_terminating": true - }, "ready_condition": { - "process_state": "Terminated" + "file_state": { + "file_path": "/tmp/tests/ready_condition_file_not_exists/vanishing_file_2", + "state": "NotExisting", + "polling_interval": 0.1 + } } } } }, "run_targets": { "Startup": { + "depends_on": [ + "fake_control_client" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "working": { "depends_on": [ "file_deleting_component", - "verification_component" + "fake_control_client" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "timeout": { + "depends_on": [ + "file_deleting_component_timeout", + "fake_control_client" ], "recovery_action": { "switch_run_target": { diff --git a/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py index 3e734ae50..9dbb0ed92 100644 --- a/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py +++ b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py @@ -41,6 +41,7 @@ def test_ready_condition_file_not_existing( # The file has to be there when the launch manager starts polling, otherwise the ready # condition is satisfied right away and the test would pass without waiting for anything. res, stdout = target.execute(f"touch {vanishing_file}") + res, stdout = target.execute(f"touch {vanishing_file}_2") assert res == 0, stdout run_until_file_deployed( @@ -53,5 +54,5 @@ def test_ready_condition_file_not_existing( ) assert_test_results( - {"ready_file_verification_process.xml", "reporting_process_file_deletor.xml"} + {"fake_control_client.xml", "reporting_process_file_deletor.xml"} ) diff --git a/tests/integration/ready_conditions/file_state/not_existing/ready_file_verification_process.cpp b/tests/integration/ready_conditions/file_state/not_existing/ready_file_verification_process.cpp deleted file mode 100644 index 024deb651..000000000 --- a/tests/integration/ready_conditions/file_state/not_existing/ready_file_verification_process.cpp +++ /dev/null @@ -1,71 +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 - -#include -#include -#include - -#include "tests/utils/test_helper/test_helper.hpp" - -std::string g_ready_file; -bool g_expect_existing = true; - -TEST(ReadyConditionFile, ReadyConditionIsMetBeforeDependentStarts) -{ - // Then, this process is only started once the ready condition of the component it depends on is met: - TEST_STEP("Check the state of the ready condition file") - { - if (g_expect_existing) - { - EXPECT_TRUE(std::filesystem::exists(g_ready_file)) - << "'" << g_ready_file << "' does not exist, the dependent component was started too early"; - } - else - { - EXPECT_FALSE(std::filesystem::exists(g_ready_file)) - << "'" << g_ready_file << "' still exists, the dependent component was started too early"; - } - } -} - -int main(int argc, char** argv) -{ - if (argc != 3) - { - std::cerr << "Expected the path of the ready condition file and its expected state " - "('Exists' or 'NotExisting') as arguments" - << std::endl; - return EXIT_FAILURE; - } - g_ready_file = argv[1]; - - const std::string_view expected_state{argv[2]}; - if (expected_state == "Exists") - { - g_expect_existing = true; - } - else if (expected_state == "NotExisting") - { - g_expect_existing = false; - } - else - { - std::cerr << "Unknown expected state '" << expected_state << "', expected 'Exists' or 'NotExisting'" - << std::endl; - return EXIT_FAILURE; - } - - TestRunner runner{__FILE__, TerminationBehavior::kContinue, TerminationNotification::kTestEnd}; - return runner.RunTests(); -} From 97ee19ac7402beedd46e9f27e255cf0d2778ada1 Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Thu, 27 Aug 2026 16:48:20 +0100 Subject: [PATCH 10/16] Fixing some stuff --- .../src/osal/details/posix/wait_for_file.cpp | 1 - .../details/process_info_node.cpp | 21 +++++++++---------- .../details/process_info_node.hpp | 4 ++-- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp index a5f6b60dc..0c19bcb3f 100644 --- a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp +++ b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp @@ -13,7 +13,6 @@ #include #include -#include #include #include diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp index 2e23aec0c..eab1a4d56 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp @@ -21,7 +21,6 @@ #include #include #include -#include namespace score::mw::lifecycle::internal { @@ -34,7 +33,7 @@ ProcessInfoNode::ProcessInfoNode( has_semaphore_(false), process_index_(index), pid_(0), - status_(0), + exit_code_(0), config_(std::move(config)), process_handling_(std::move(process_handling)) { @@ -168,7 +167,7 @@ IComponent::RequestResult ProcessInfoNode::tryHandleTermination(int32_t process_ { LM_LOG_DEBUG() << "Process" << process_index_ << "pid" << pid_ << "(" << config_.name << ") for node" << this << "terminated with status" << process_status; - status_ = process_status; + exit_code_ = process_status; IComponent::RequestResult res = {IComponent::RequestState::kWaiting}; if (has_semaphore_.exchange(false)) { @@ -195,7 +194,7 @@ IComponent::RequestResult ProcessInfoNode::tryHandleTermination(int32_t process_ else { LM_LOG_WARN() << "unexpected termination of process" << process_index_ << "pid" << pid_ << "(" - << config_.name << ")" << "( status" << status_ << ")"; + << config_.name << ")" << "( status" << exit_code_ << ")"; res = score::cpp::make_unexpected(IComponent::ComponentError::kErrorAfterReady); } } @@ -232,7 +231,7 @@ IComponent::RequestResult ProcessInfoNode::startProcess(score::cpp::stop_token s } pid_ = 0; - status_ = 0; + exit_code_ = 0; error = std::nullopt; static_cast(setState(score::mw::lifecycle::ProcessState::kStarting)); // Cannot fail by design @@ -295,7 +294,7 @@ score::cpp::expected_blank ProcessInfoNode::handlePr const bool is_native = configuration::ApplicationType::Native == config_.component_properties.application_profile.application_type; - const bool ready_condition_met = std::visit( + const bool startup_condition_met = std::visit( [this, is_native](auto&& arg) -> bool { using T = std::decay_t; @@ -304,12 +303,12 @@ score::cpp::expected_blank ProcessInfoNode::handlePr if (is_native) { // A native process does not report kRunning, so its status is the only readiness indication. - return status_ == 0; + return exit_code_ == 0; } auto wait_res = process_handling_.process_interface_->waitForkRunning( sync_, std::chrono::milliseconds(config_.deployment_config.ready_timeout_ms)); - return (wait_res == osal::OsalReturnType::kSuccess) && (status_ == 0); + return (wait_res == osal::OsalReturnType::kSuccess) && (exit_code_ == 0); } else if constexpr (std::is_same_v) { @@ -318,12 +317,12 @@ score::cpp::expected_blank ProcessInfoNode::handlePr arg.state, std::chrono::milliseconds(config_.deployment_config.ready_timeout_ms), arg.polling_interval); - return (wait_res == osal::OsalReturnType::kSuccess) && (status_ == 0); + return (wait_res == osal::OsalReturnType::kSuccess) && (exit_code_ == 0); } }, config_.component_properties.ready_condition); - if (ready_condition_met) + if (startup_condition_met) { handleProcessRunning(); return {}; @@ -341,7 +340,7 @@ score::cpp::expected_blank ProcessInfoNode::handlePr score::cpp::expected_blank ProcessInfoNode::handleProcessAlreadyTerminated() { - if ((0 != status_) || + if ((0 != exit_code_) || (configuration::ApplicationType::Native != config_.component_properties.application_profile.application_type)) { // Error. To get a legal terminated before kRunning the process must be self-terminating, non-reporting diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.hpp index 49fc16671..e6fd05cf4 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.hpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.hpp @@ -54,7 +54,7 @@ class ProcessInfoNode final : public IComponent has_semaphore_(other.has_semaphore_.load()), process_index_(other.process_index_), pid_(other.pid_), - status_(other.status_.load()), + exit_code_(other.exit_code_.load()), process_state_(other.process_state_.load()), reached_ready_(other.reached_ready_.load()), config_(std::move(other.config_)), @@ -166,7 +166,7 @@ class ProcessInfoNode final : public IComponent osal::ProcessID pid_ = 0; /// @brief The status reported by the operating system when the process terminated - std::atomic status_{0}; + std::atomic exit_code_{0}; /// @brief The current state of the OS process std::atomic process_state_{score::mw::lifecycle::ProcessState::kIdle}; From 89f113e53bc15894c6763a984f92983e8e77b2fe Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Fri, 28 Aug 2026 08:23:08 +0100 Subject: [PATCH 11/16] More stuff --- .../ready_conditions/file_state/common/BUILD | 13 +------ .../file_state/common/file_modifier.cpp | 38 ++++++++++--------- .../ready_conditions/file_state/exists/BUILD | 2 +- .../exists/ready_condition_file.json | 31 ++++++++++++--- .../file_state/exists/ready_condition_file.py | 2 +- .../file_state/not_existing/BUILD | 2 +- .../ready_condition_file_not_existing.json | 26 +++++++++++-- .../ready_condition_file_not_existing.py | 5 +-- 8 files changed, 75 insertions(+), 44 deletions(-) diff --git a/tests/integration/ready_conditions/file_state/common/BUILD b/tests/integration/ready_conditions/file_state/common/BUILD index 914125777..a097acd5a 100644 --- a/tests/integration/ready_conditions/file_state/common/BUILD +++ b/tests/integration/ready_conditions/file_state/common/BUILD @@ -15,20 +15,11 @@ load("@rules_cc//cc:cc_binary.bzl", "cc_binary") load("//tests/utils/bazel:integration.bzl", "integration_test") cc_binary( - name = "file_creator", - srcs = ["file_modifier.cpp"], - visibility = ["//tests/integration/ready_conditions/file_state:__subpackages__"], - deps = [ - "//tests/utils/test_helper", - "@googletest//:gtest_main", - ], -) - -cc_binary( - name = "file_deletor", + name = "file_modifier", srcs = ["file_modifier.cpp"], visibility = ["//tests/integration/ready_conditions/file_state:__subpackages__"], deps = [ + "//score/launch_manager:lifecycle_cc", "//tests/utils/test_helper", "@googletest//:gtest_main", ], diff --git a/tests/integration/ready_conditions/file_state/common/file_modifier.cpp b/tests/integration/ready_conditions/file_state/common/file_modifier.cpp index b7acfe14e..0b7c32e1d 100644 --- a/tests/integration/ready_conditions/file_state/common/file_modifier.cpp +++ b/tests/integration/ready_conditions/file_state/common/file_modifier.cpp @@ -13,6 +13,7 @@ #include "tests/utils/test_helper/test_helper.hpp" #include +#include #include #include #include @@ -50,26 +51,21 @@ TEST(ModifyFile, ModifyFile) int main(int argc, char** argv) { - if (argc != 3) + if (argc != 5) { - std::cerr << "USAGE:" << argv[0] << "(file path) (milliseconds to wait before doing the operation)" + std::cerr << "USAGE:" << argv[0] + << "(action [delete|create]) (file path) (milliseconds to wait before doing the operation) " + "(shall_report [0|1])" << std::endl; return EXIT_FAILURE; } - const std::string_view full_path{argv[0]}; - const std::size_t base_name_pos = full_path.rfind('/'); - if (base_name_pos == std::string_view::npos) - { - return EXIT_FAILURE; - }; - const std::string_view prog_name{std::next(full_path.begin(), base_name_pos + 1)}; - - if (prog_name == "file_creator") + const std::string_view action{argv[1]}; + if (action == "create") { g_operation = Operation::Create; } - else if (prog_name == "file_deletor") + else if (action == "delete") { g_operation = Operation::Delete; } @@ -79,10 +75,18 @@ int main(int argc, char** argv) return EXIT_FAILURE; } - g_file_path = std::string_view{argv[1]}; - g_modify_delay = std::chrono::milliseconds{std::stoi(argv[2])}; + g_file_path = std::string_view{argv[2]}; + g_modify_delay = std::chrono::milliseconds{std::stoi(argv[3])}; + + std::string xml_result{argv[0]}; + + const std::string_view shall_report{argv[4]}; + if (shall_report == "report") + { + std::cout << "REPORTING RUNNING from " << ::getpid() << std::endl; + score::mw::lifecycle::report_running(); + xml_result.append("_reporting"); + } - std::string xml_name{"reporting_process_"}; - xml_name.append(prog_name); - return TestRunner(xml_name).RunTests(); + return TestRunner(xml_result).RunTests(); } diff --git a/tests/integration/ready_conditions/file_state/exists/BUILD b/tests/integration/ready_conditions/file_state/exists/BUILD index e646a3503..f518ce9fb 100644 --- a/tests/integration/ready_conditions/file_state/exists/BUILD +++ b/tests/integration/ready_conditions/file_state/exists/BUILD @@ -28,7 +28,7 @@ integration_test( name = "ready_condition_file", srcs = ["ready_condition_file.py"], binaries = [ - "//tests/integration/ready_conditions/file_state/common:file_creator", + "//tests/integration/ready_conditions/file_state/common:file_modifier", ":fake_control_client", "//score/launch_manager", ], diff --git a/tests/integration/ready_conditions/file_state/exists/ready_condition_file.json b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.json index a4ece0d1e..6476cd38a 100644 --- a/tests/integration/ready_conditions/file_state/exists/ready_condition_file.json +++ b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.json @@ -3,7 +3,7 @@ "defaults": { "deployment_config": { "bin_dir": "/tmp/tests/ready_condition_file", - "ready_timeout": 0.1, + "ready_timeout": 0.2, "shutdown_timeout": 1.0, "recovery_action": { "switch_run_target": { @@ -42,24 +42,42 @@ }, "file_creating_component": { "component_properties": { - "binary_name": "file_creator", + "binary_name": "file_modifier", "process_arguments": [ - "/tmp/tests/ready_condition_file/ready_file", "0" + "create", "/tmp/tests/ready_condition_file/ready_file", "0", "native" ], "ready_condition": { "file_state": { "file_path": "/tmp/tests/ready_condition_file/ready_file", "state": "Exists", - "polling_interval": 0.1 + "polling_interval": 0.05 + } + } + } + }, + "file_creating_component_reporting": { + "component_properties": { + "binary_name": "file_modifier", + "process_arguments": [ + "create", "/tmp/tests/ready_condition_file/ready_file_reporting", "0", "report" + ], + "application_profile": { + "application_type": "Reporting" + }, + "ready_condition": { + "file_state": { + "file_path": "/tmp/tests/ready_condition_file/ready_file_reporting", + "state": "Exists", + "polling_interval": 0.05 } } } }, "file_creating_component_timeout": { "component_properties": { - "binary_name": "file_creator", + "binary_name": "file_modifier", "process_arguments": [ - "/tmp/tests/ready_condition_file/ready_file_2", "1000" + "create", "/tmp/tests/ready_condition_file/ready_file_2", "1000", "native" ], "ready_condition": { "file_state": { @@ -85,6 +103,7 @@ "working": { "depends_on": [ "file_creating_component", + "file_creating_component_reporting", "fake_control_client" ], "recovery_action": { diff --git a/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py index 276794579..07e83eeda 100644 --- a/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py +++ b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py @@ -44,5 +44,5 @@ def test_ready_condition_file(target, setup_test, assert_test_results, remote_te ) assert_test_results( - {"fake_control_client.xml", "reporting_process_file_creator.xml"} + {"fake_control_client.xml", "file_modifier.xml", "file_modifier_reporting.xml"} ) diff --git a/tests/integration/ready_conditions/file_state/not_existing/BUILD b/tests/integration/ready_conditions/file_state/not_existing/BUILD index 826621132..177882f46 100644 --- a/tests/integration/ready_conditions/file_state/not_existing/BUILD +++ b/tests/integration/ready_conditions/file_state/not_existing/BUILD @@ -28,7 +28,7 @@ integration_test( name = "ready_condition_file_not_exists", srcs = ["ready_condition_file_not_existing.py"], binaries = [ - "//tests/integration/ready_conditions/file_state/common:file_deletor", + "//tests/integration/ready_conditions/file_state/common:file_modifier", ":fake_control_client", "//score/launch_manager", ], diff --git a/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json index bbccc72ce..636f50d95 100644 --- a/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json +++ b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json @@ -43,9 +43,9 @@ }, "file_deleting_component": { "component_properties": { - "binary_name": "file_deletor", + "binary_name": "file_modifier", "process_arguments": [ - "/tmp/tests/ready_condition_file_not_exists/vanishing_file", "0" + "delete", "/tmp/tests/ready_condition_file_not_exists/vanishing_file", "0", "native" ], "ready_condition": { "file_state": { @@ -56,11 +56,29 @@ } } }, + "file_deleting_component_reporting": { + "component_properties": { + "binary_name": "file_modifier", + "process_arguments": [ + "delete", "/tmp/tests/ready_condition_file_not_exists/vanishing_file_report", "0", "report" + ], + "application_profile": { + "application_type": "Reporting" + }, + "ready_condition": { + "file_state": { + "file_path": "/tmp/tests/ready_condition_file_not_exists/vanishing_file_report", + "state": "NotExisting", + "polling_interval": 0.1 + } + } + } + }, "file_deleting_component_timeout": { "component_properties": { - "binary_name": "file_deletor", + "binary_name": "file_modifier", "process_arguments": [ - "/tmp/tests/ready_condition_file_not_exists/vanishing_file_2", "500" + "delete", "/tmp/tests/ready_condition_file_not_exists/vanishing_file_2", "500", "native" ], "ready_condition": { "file_state": { diff --git a/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py index 9dbb0ed92..cd7ef7420 100644 --- a/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py +++ b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py @@ -41,6 +41,7 @@ def test_ready_condition_file_not_existing( # The file has to be there when the launch manager starts polling, otherwise the ready # condition is satisfied right away and the test would pass without waiting for anything. res, stdout = target.execute(f"touch {vanishing_file}") + res, stdout = target.execute(f"touch {vanishing_file}_report") res, stdout = target.execute(f"touch {vanishing_file}_2") assert res == 0, stdout @@ -53,6 +54,4 @@ def test_ready_condition_file_not_existing( timeout_s=3.0, ) - assert_test_results( - {"fake_control_client.xml", "reporting_process_file_deletor.xml"} - ) + assert_test_results({"fake_control_client.xml", "file_modifier.xml"}) From a3cece038424e230aeced3601eaafbe8b7c4064e Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Fri, 28 Aug 2026 11:19:52 +0100 Subject: [PATCH 12/16] Working --- .../details/process_info_node.cpp | 23 +++++- .../details/process_launcher.cpp | 77 +++++++++++-------- .../details/process_launcher.hpp | 3 + .../src/process_group_manager/iprocess.hpp | 6 ++ .../process_group_manager/mock_iprocess.hpp | 1 + .../file_state/common/file_modifier.cpp | 5 ++ .../exists/ready_condition_file.json | 12 +-- .../file_state/exists/ready_condition_file.py | 6 +- .../ready_condition_file_not_existing.json | 3 + .../ready_condition_file_not_existing.py | 6 +- 10 files changed, 96 insertions(+), 46 deletions(-) diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp index eab1a4d56..51400b941 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp @@ -292,7 +292,7 @@ score::cpp::expected_blank ProcessInfoNode::handlePr const score::cpp::stop_token& stop_token) { const bool is_native = - configuration::ApplicationType::Native == config_.component_properties.application_profile.application_type; + config_.component_properties.application_profile.application_type == configuration::ApplicationType::Native; const bool startup_condition_met = std::visit( [this, is_native](auto&& arg) -> bool { @@ -310,13 +310,29 @@ score::cpp::expected_blank ProcessInfoNode::handlePr sync_, std::chrono::milliseconds(config_.deployment_config.ready_timeout_ms)); return (wait_res == osal::OsalReturnType::kSuccess) && (exit_code_ == 0); } + // req-id: comp_req__launch_man__path_condition_check else if constexpr (std::is_same_v) { + + if (!is_native) + { + // currently we do not support multiple ready conditions so we need + // to ignore the krunning signal. + auto wait_res = process_handling_.process_interface_->ignoreRunning(sync_); + static_cast(wait_res); + } + const auto wait_res = osal::wait_for_file( arg.file_path, arg.state, std::chrono::milliseconds(config_.deployment_config.ready_timeout_ms), arg.polling_interval); + + if (wait_res != osal::OsalReturnType::kSuccess) + { + LM_LOG_ERROR() << "Error Waiting for file"; + } + return (wait_res == osal::OsalReturnType::kSuccess) && (exit_code_ == 0); } }, @@ -333,7 +349,7 @@ score::cpp::expected_blank ProcessInfoNode::handlePr return score::cpp::make_unexpected(ComponentError::kErrorBeforeReady); } - LM_LOG_WARN() << "Got kRunning timeout for process" << process_index_ << "(" << config_.name << ")"; + LM_LOG_WARN() << "Got startup timeout for process" << process_index_ << "(" << config_.name << ")"; terminateProcess(stop_token); return score::cpp::make_unexpected(ComponentError::kActivationTimedOut); } @@ -380,8 +396,7 @@ void ProcessInfoNode::handleProcessRunning() { if (configuration::ApplicationType::Native == config_.component_properties.application_profile.application_type) { - LM_LOG_DEBUG() << "Considered kRunning for Non Reporting Process pid" << pid_ << "(" << config_.name - << ") process" << process_index_; + LM_LOG_DEBUG() << "Native process is running " << pid_ << "(" << config_.name << ") process" << process_index_; } else { diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp index 86b8e7caa..0722962c6 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp @@ -577,49 +577,66 @@ OsalReturnType ProcessLauncher::waitForTermination(osal::ProcessID& pid, int32_t return result; } +OsalReturnType ProcessLauncher::ignoreRunning(IpcCommsP sync) +{ + if (!sync) + { + LM_LOG_ERROR() << "Invalid shared memory pointer: The shared memory pointer is null."; + return OsalReturnType::kFail; + } + + const auto post_res = sync->reply_sync_.post(); + if (post_res == OsalReturnType::kFail) + { + LM_LOG_ERROR() << "Semaphore post failed"; + return OsalReturnType::kFail; + } + return OsalReturnType::kSuccess; +} + OsalReturnType ProcessLauncher::waitForkRunning(IpcCommsP sync, std::chrono::milliseconds timeout) { OsalReturnType result = OsalReturnType::kSuccess; - if (sync) + if (!sync) { - if ((sync->send_sync_.timedWait(timeout) == OsalReturnType::kFail) || - (sync->reply_sync_.post() == OsalReturnType::kFail)) - { - LM_LOG_ERROR() << "Semaphore timedWait or post failed: Unable to wait or post on semaphores within the " - "specified timeout."; - result = OsalReturnType::kFail; - } - else - { - result = sync->send_sync_.timedWait(std::chrono::milliseconds(100)); - } + LM_LOG_ERROR() << "Invalid shared memory pointer: The shared memory pointer is null."; + return OsalReturnType::kFail; + } - // We are not interested in the result of msync, just whether it worked or not. - // If it did not work, the child process has probably crashed and corrupted the shared memory - // so we should not try to deinitialize the semaphores. - // mincore would be more appropriate here, but is not available on QNX - if (msync(sync.get(), sizeof(IpcCommsSync), MS_ASYNC) == 0) + const auto time_res = sync->send_sync_.timedWait(timeout); + const auto post_res = sync->reply_sync_.post(); + + if ((time_res == OsalReturnType::kFail) || (post_res == OsalReturnType::kFail)) + { + LM_LOG_ERROR() << "Semaphore timedWait or post failed: Unable to wait or post on semaphores within the " + "specified timeout."; + result = OsalReturnType::kFail; + } + else + { + result = sync->send_sync_.timedWait(std::chrono::milliseconds(100)); + } + + // We are not interested in the result of msync, just whether it worked or not. + // If it did not work, the child process has probably crashed and corrupted the shared memory + // so we should not try to deinitialize the semaphores. + // mincore would be more appropriate here, but is not available on QNX + if (msync(sync.get(), sizeof(IpcCommsSync), MS_ASYNC) == 0) + { + if (sync->send_sync_.deinit() != OsalReturnType::kSuccess) { - if (sync->send_sync_.deinit() != OsalReturnType::kSuccess) - { - LM_LOG_WARN() << "Failed to deinitialize send_sync semaphore."; - } - if (sync->reply_sync_.deinit() != OsalReturnType::kSuccess) - { - LM_LOG_WARN() << "Failed to deinitialize reply_sync semaphore."; - } + LM_LOG_WARN() << "Failed to deinitialize send_sync semaphore."; } - else + if (sync->reply_sync_.deinit() != OsalReturnType::kSuccess) { - LM_LOG_WARN() << "Skipping semaphore deinitialization - shared memory region appears invalid: " - << errno_message(errno); + LM_LOG_WARN() << "Failed to deinitialize reply_sync semaphore."; } } else { - LM_LOG_ERROR() << "Invalid shared memory pointer: The shared memory pointer is null."; - result = OsalReturnType::kFail; + LM_LOG_WARN() << "Skipping semaphore deinitialization - shared memory region appears invalid: " + << errno_message(errno); } return result; diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.hpp index d93b361b2..f30eda374 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.hpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.hpp @@ -51,6 +51,9 @@ class ProcessLauncher final : public IProcess /// @see IProcess::waitForkRunning() for details OsalReturnType waitForkRunning(IpcCommsP sync, std::chrono::milliseconds timeout) override; + /// @see IProcess::waitForkRunning() for details + OsalReturnType ignoreRunning(IpcCommsP sync); + private: /// @brief Creates shared memory for communication between processes. /// @param[in,out] sync Pointer to a location to store a pointer to a structure containing diff --git a/score/launch_manager/src/daemon/src/process_group_manager/iprocess.hpp b/score/launch_manager/src/daemon/src/process_group_manager/iprocess.hpp index 2ec8374da..3831bf3fd 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/iprocess.hpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/iprocess.hpp @@ -123,6 +123,12 @@ class IProcess /// @return kFail if sync is NULL or a timeout occurs, kSuccess otherwise virtual OsalReturnType waitForkRunning(IpcCommsP sync, std::chrono::milliseconds timeout) = 0; + + /// @brief Ignores a kRunning signal. + /// @param sync The pointer returned from startProcess. + virtual OsalReturnType ignoreRunning(IpcCommsP sync) = 0; + + // virtual OsalReturnType respondToRunning(IpcCommsP sync, std::chrono::milliseconds timeout) = 0; }; } // namespace osal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/mock_iprocess.hpp b/score/launch_manager/src/daemon/src/process_group_manager/mock_iprocess.hpp index de14f16a6..d8864ee82 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/mock_iprocess.hpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/mock_iprocess.hpp @@ -33,6 +33,7 @@ class MockIProcess : public IProcess MOCK_METHOD(OsalReturnType, forceTermination, (ProcessID pid), (override)); MOCK_METHOD(OsalReturnType, waitForTermination, (ProcessID & pid, int32_t& status), (override)); MOCK_METHOD(OsalReturnType, waitForkRunning, (IpcCommsP sync, std::chrono::milliseconds timeout), (override)); + MOCK_METHOD(OsalReturnType, ignoreRunning, (IpcCommsP sync), (override)); }; } // namespace score::mw::lifecycle::internal::osal diff --git a/tests/integration/ready_conditions/file_state/common/file_modifier.cpp b/tests/integration/ready_conditions/file_state/common/file_modifier.cpp index 0b7c32e1d..de1b683bc 100644 --- a/tests/integration/ready_conditions/file_state/common/file_modifier.cpp +++ b/tests/integration/ready_conditions/file_state/common/file_modifier.cpp @@ -39,18 +39,22 @@ TEST(ModifyFile, ModifyFile) { case (Operation::Create): ASSERT_TRUE(touch_file(g_file_path)); + std::cout << "created file " << g_file_path << "By " << ::getpid() << std::endl; break; case (Operation::Delete): std::error_code error{}; ASSERT_TRUE(std::filesystem::remove(g_file_path, error)) << "Could not remove file " << g_file_path << ": " << (error ? error.message() : "the file did not exist"); + std::cout << "deleted file " << g_file_path << "By " << ::getpid() << std::endl; break; } + std::cout << "Operation complete!" << std::endl; } int main(int argc, char** argv) { + std::cout << "Starting" << std::endl; if (argc != 5) { std::cerr << "USAGE:" << argv[0] @@ -85,6 +89,7 @@ int main(int argc, char** argv) { std::cout << "REPORTING RUNNING from " << ::getpid() << std::endl; score::mw::lifecycle::report_running(); + std::cout << "REPORTED!" << ::getpid() << std::endl; xml_result.append("_reporting"); } diff --git a/tests/integration/ready_conditions/file_state/exists/ready_condition_file.json b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.json index 6476cd38a..0c9ef130e 100644 --- a/tests/integration/ready_conditions/file_state/exists/ready_condition_file.json +++ b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.json @@ -3,7 +3,7 @@ "defaults": { "deployment_config": { "bin_dir": "/tmp/tests/ready_condition_file", - "ready_timeout": 0.2, + "ready_timeout": 1.0, "shutdown_timeout": 1.0, "recovery_action": { "switch_run_target": { @@ -36,7 +36,10 @@ }, "application_profile": { "application_type": "State_Manager", - "is_self_terminating": false + "is_self_terminating": false, + "alive_supervision": { + "min_indications": 0 + } } } }, @@ -77,7 +80,7 @@ "component_properties": { "binary_name": "file_modifier", "process_arguments": [ - "create", "/tmp/tests/ready_condition_file/ready_file_2", "1000", "native" + "create", "/tmp/tests/ready_condition_file/ready_file_2", "1500", "native" ], "ready_condition": { "file_state": { @@ -125,9 +128,6 @@ } }, "initial_run_target": "Startup", - "alive_supervision": { - "evaluation_cycle": 0.05 - }, "fallback_run_target": { "depends_on": ["fake_control_client"] } diff --git a/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py index 07e83eeda..006bbb689 100644 --- a/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py +++ b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py @@ -17,9 +17,9 @@ @add_test_properties( - partially_verifies=[], - test_type="interface-test", - derivation_technique="explorative-testing", + partially_verifies=["comp_req__launch_man__path_condition_check"], + test_type="requirements-based", + derivation_technique="requirements-analysis", ) def test_ready_condition_file(target, setup_test, assert_test_results, remote_test_dir): """ diff --git a/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json index 636f50d95..2fd717fca 100644 --- a/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json +++ b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json @@ -47,6 +47,9 @@ "process_arguments": [ "delete", "/tmp/tests/ready_condition_file_not_exists/vanishing_file", "0", "native" ], + "application_profile": { + "application_type": "Native" + }, "ready_condition": { "file_state": { "file_path": "/tmp/tests/ready_condition_file_not_exists/vanishing_file", diff --git a/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py index cd7ef7420..05d998896 100644 --- a/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py +++ b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py @@ -17,9 +17,9 @@ @add_test_properties( - partially_verifies=[], - test_type="interface-test", - derivation_technique="explorative-testing", + partially_verifies=["comp_req__launch_man__path_condition_check"], + test_type="requirements-based", + derivation_technique="requirements-analysis", ) def test_ready_condition_file_not_existing( target, setup_test, assert_test_results, remote_test_dir From e96b6153af172d1dd7b1c66ca8cf5b8c9b032449 Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Fri, 28 Aug 2026 11:43:49 +0100 Subject: [PATCH 13/16] Adding stop token and zstring_View --- .../launch_manager/src/daemon/src/osal/BUILD | 2 + .../src/osal/details/posix/wait_for_file.cpp | 19 ++++---- .../osal/details/posix/wait_for_file_UT.cpp | 47 +++++++++++-------- .../src/daemon/src/osal/wait_for_file.hpp | 8 ++-- .../details/process_info_node.cpp | 5 +- 5 files changed, 46 insertions(+), 35 deletions(-) diff --git a/score/launch_manager/src/daemon/src/osal/BUILD b/score/launch_manager/src/daemon/src/osal/BUILD index f9152d20a..39a5737b3 100644 --- a/score/launch_manager/src/daemon/src/osal/BUILD +++ b/score/launch_manager/src/daemon/src/osal/BUILD @@ -47,6 +47,7 @@ cc_library( deps = [ ":return_types", "//score/launch_manager/src/daemon/src/configuration:component_config", + "@score_baselibs//score/language/safecpp/string_view:zstring_view", "@score_baselibs//score/os:errno", "@score_baselibs//score/os:stat", "@score_baselibs//score/result:error", @@ -60,6 +61,7 @@ lm_cc_test( ":wait_for_file", "//score/launch_manager/src/daemon/src/configuration:component_config", "@googletest//:gtest_main", + "@score_baselibs//score/language/safecpp/string_view:zstring_view", "@score_baselibs//score/os:errno", "@score_baselibs//score/os:stat", "@score_baselibs//score/os/mocklib:stat_mock", diff --git a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp index 0c19bcb3f..7966b5671 100644 --- a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp +++ b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp @@ -26,27 +26,26 @@ namespace score::mw::lifecycle::internal::osal { OsalReturnType wait_for_file( - std::string_view path, + score::safecpp::zstring_view path, configuration::FileExistenceState condition, std::chrono::milliseconds timeout, std::chrono::milliseconds poll_interval, + const score::cpp::stop_token& stop_token, const score::os::Stat& stat_os) noexcept { - // note: QNX has a wait_for API, however in the future a stop_token should - // be used, when using the API call we wouldn't be able to check the - // stop_token between stat calls. - - // required null terminator - if (path.empty() || (path.data()[path.size()] != '\0')) - { - return OsalReturnType::kFail; - } + // note: QNX has a wait_for API, however using the API call we wouldn't be + // able to check the stop_token between stat calls. const bool wait_for_existence = (condition == configuration::FileExistenceState::Exists); const auto deadline = std::chrono::steady_clock::now() + timeout; while (true) { + if (stop_token.stop_requested()) + { + return OsalReturnType::kFail; + } + score::os::StatBuffer info{}; const auto result = stat_os.stat(path.data(), info); diff --git a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp index 56e168b05..e8ee9fb6c 100644 --- a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp +++ b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp @@ -20,8 +20,8 @@ #include #include -#include -#include + +#include #include "score/mw/launch_manager/osal/wait_for_file.hpp" @@ -54,7 +54,8 @@ TEST_F(WaitForFileTest, FileExists) score::os::StatMock mock{}; EXPECT_CALL(mock, stat(_, _, true)).WillOnce(testing::Return(score::cpp::expected_blank{})); EXPECT_EQ( - wait_for_file("/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, mock), + wait_for_file( + "/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, score::cpp::stop_token{}, mock), OsalReturnType::kSuccess); } @@ -68,23 +69,11 @@ TEST_F(WaitForFileTest, FileNotExisting) EXPECT_CALL(mock, stat(_, _, true)) .WillOnce(testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOTDIR)))); EXPECT_EQ( - wait_for_file("/some/path", FileExistenceState::NotExisting, kWaitTimeout, kPollInterval, mock), + wait_for_file( + "/some/path", FileExistenceState::NotExisting, kWaitTimeout, kPollInterval, score::cpp::stop_token{}, mock), OsalReturnType::kSuccess); } -TEST_F(WaitForFileTest, NonNullTerminatedPathFails) -{ - RecordProperty("Description", "Verify that a path which is not null terminated is rejected instead of polled."); - - score::os::StatMock mock{}; - const std::string path = "/some/pathx"; - const std::string_view not_terminated{path.data(), path.size() - 1U}; - - EXPECT_EQ( - wait_for_file(not_terminated, FileExistenceState::Exists, kWaitTimeout, kPollInterval, mock), - OsalReturnType::kFail); -} - TEST_F(WaitForFileTest, Timeout) { RecordProperty("Description", "Verify that if an error is repeatedly given then the timeout fires."); @@ -93,7 +82,8 @@ TEST_F(WaitForFileTest, Timeout) EXPECT_CALL(mock, stat(_, _, true)) .WillRepeatedly(testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(EINTR)))); EXPECT_EQ( - wait_for_file("/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, mock), + wait_for_file( + "/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, score::cpp::stop_token{}, mock), OsalReturnType::kTimeout); } @@ -105,7 +95,8 @@ TEST_F(WaitForFileTest, Error) EXPECT_CALL(mock, stat(_, _, true)) .WillRepeatedly(testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(EBADF)))); EXPECT_EQ( - wait_for_file("/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, mock), + wait_for_file( + "/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, score::cpp::stop_token{}, mock), OsalReturnType::kFail); } @@ -120,8 +111,24 @@ TEST_F(WaitForFileTest, NonNullTermindPathFails) .WillOnce(testing::Return(score::cpp::expected_blank{})); EXPECT_EQ( - wait_for_file("/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, mock), + wait_for_file( + "/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, score::cpp::stop_token{}, mock), OsalReturnType::kSuccess); } +TEST_F(WaitForFileTest, StopRequested) +{ + RecordProperty("Description", "Verify that a requested stop causes an early kFail return."); + + score::os::StatMock mock{}; + score::cpp::stop_source stop_source{}; + static_cast(stop_source.request_stop()); + + EXPECT_CALL(mock, stat(_, _, true)).Times(0); + EXPECT_EQ( + wait_for_file( + "/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, stop_source.get_token(), mock), + OsalReturnType::kFail); +} + } // namespace diff --git a/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp b/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp index b31fd9cc3..390bbeb75 100644 --- a/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp +++ b/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp @@ -16,11 +16,11 @@ #include +#include "score/language/safecpp/string_view/zstring_view.h" #include "score/mw/launch_manager/configuration/component_config.hpp" #include "score/os/stat.h" #include #include -#include #include "return_types.hpp" @@ -29,16 +29,18 @@ namespace score::mw::lifecycle::internal::osal /// @brief Block until the given path reaches the requested state, the timeout elapses, or a stop is requested. /// -/// @param path The path to wait for. It must be null terminated. +/// @param path The path to wait for. /// @param condition The path state to wait for. /// @param timeout The maximum time to wait for the condition. /// @param poll_interval The time between two consecutive existence checks. +/// @param stop_token Checked between polls; a requested stop causes an early kFail return. /// @param stat_os Optional score::os::Stat instance used to query the path. OsalReturnType wait_for_file( - std::string_view path, + score::safecpp::zstring_view path, configuration::FileExistenceState condition, std::chrono::milliseconds timeout, std::chrono::milliseconds poll_interval, + const score::cpp::stop_token& stop_token, const score::os::Stat& stat_os = score::os::Stat::instance()) noexcept; } // namespace score::mw::lifecycle::internal::osal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp index 51400b941..6adff5668 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp @@ -295,7 +295,7 @@ score::cpp::expected_blank ProcessInfoNode::handlePr config_.component_properties.application_profile.application_type == configuration::ApplicationType::Native; const bool startup_condition_met = std::visit( - [this, is_native](auto&& arg) -> bool { + [this, is_native, &stop_token](auto&& arg) -> bool { using T = std::decay_t; if constexpr (std::is_same_v) @@ -326,7 +326,8 @@ score::cpp::expected_blank ProcessInfoNode::handlePr arg.file_path, arg.state, std::chrono::milliseconds(config_.deployment_config.ready_timeout_ms), - arg.polling_interval); + arg.polling_interval, + stop_token); if (wait_res != osal::OsalReturnType::kSuccess) { From 47d3fac1afbee1216e7a70b7978ed762ae8d13e8 Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Fri, 28 Aug 2026 14:19:20 +0100 Subject: [PATCH 14/16] Making wait for file mockable --- .../launch_manager/src/daemon/src/osal/BUILD | 30 +++++++++++++ .../src/osal/details/posix/wait_for_file.cpp | 7 ++- .../osal/details/posix/wait_for_file_UT.cpp | 26 +++++------ .../src/daemon/src/osal/ifile_waiter.hpp | 45 +++++++++++++++++++ .../src/daemon/src/osal/mock_ifile_waiter.hpp | 39 ++++++++++++++++ .../src/daemon/src/osal/wait_for_file.hpp | 41 ++++++++++------- .../daemon/src/process_group_manager/BUILD | 1 + .../src/process_group_manager/details/BUILD | 4 +- .../details/process_handling.hpp | 4 ++ .../details/process_info_node.cpp | 4 +- .../process_group_manager.cpp | 3 +- .../process_group_manager.hpp | 4 ++ 12 files changed, 172 insertions(+), 36 deletions(-) create mode 100644 score/launch_manager/src/daemon/src/osal/ifile_waiter.hpp create mode 100644 score/launch_manager/src/daemon/src/osal/mock_ifile_waiter.hpp diff --git a/score/launch_manager/src/daemon/src/osal/BUILD b/score/launch_manager/src/daemon/src/osal/BUILD index 39a5737b3..b5a576bf7 100644 --- a/score/launch_manager/src/daemon/src/osal/BUILD +++ b/score/launch_manager/src/daemon/src/osal/BUILD @@ -45,6 +45,7 @@ cc_library( strip_include_prefix = "/score/launch_manager/src/daemon/src/osal", visibility = ["//score:__subpackages__"], deps = [ + ":ifile_waiter", ":return_types", "//score/launch_manager/src/daemon/src/configuration:component_config", "@score_baselibs//score/language/safecpp/string_view:zstring_view", @@ -68,6 +69,35 @@ lm_cc_test( ], ) +cc_library( + name = "ifile_waiter", + hdrs = [ + "ifile_waiter.hpp", + "return_types.hpp", + ], + include_prefix = "score/mw/launch_manager/osal", + strip_include_prefix = "/score/launch_manager/src/daemon/src/osal", + visibility = ["//score:__subpackages__"], + deps = [ + ":return_types", + "//score/launch_manager/src/daemon/src/configuration:component_config", + "@score_baselibs//score/language/safecpp/string_view:zstring_view", + ], +) + +cc_library( + name = "mock_ifile_waiter", + testonly = True, + hdrs = ["mock_ifile_waiter.hpp"], + include_prefix = "score/mw/launch_manager/osal", + strip_include_prefix = "/score/launch_manager/src/daemon/src/osal", + visibility = ["//score:__subpackages__"], + deps = [ + ":ifile_waiter", + "@googletest//:gtest_main", + ], +) + cc_library( name = "sys_exit", srcs = ["details/posix/sys_exit.cpp"], diff --git a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp index 7966b5671..f61ee66f1 100644 --- a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp +++ b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp @@ -25,13 +25,12 @@ namespace score::mw::lifecycle::internal::osal { -OsalReturnType wait_for_file( +OsalReturnType FileWaiter::waitForFile( score::safecpp::zstring_view path, configuration::FileExistenceState condition, std::chrono::milliseconds timeout, std::chrono::milliseconds poll_interval, - const score::cpp::stop_token& stop_token, - const score::os::Stat& stat_os) noexcept + const score::cpp::stop_token& stop_token) const { // note: QNX has a wait_for API, however using the API call we wouldn't be // able to check the stop_token between stat calls. @@ -48,7 +47,7 @@ OsalReturnType wait_for_file( score::os::StatBuffer info{}; - const auto result = stat_os.stat(path.data(), info); + const auto result = stat_os_.stat(path.data(), info); if (result.has_value()) { if (wait_for_existence) diff --git a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp index e8ee9fb6c..eccc00017 100644 --- a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp +++ b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp @@ -26,8 +26,8 @@ #include "score/mw/launch_manager/osal/wait_for_file.hpp" using score::mw::lifecycle::internal::configuration::FileExistenceState; +using score::mw::lifecycle::internal::osal::FileWaiter; using score::mw::lifecycle::internal::osal::OsalReturnType; -using score::mw::lifecycle::internal::osal::wait_for_file; using ::testing::_; namespace @@ -54,8 +54,8 @@ TEST_F(WaitForFileTest, FileExists) score::os::StatMock mock{}; EXPECT_CALL(mock, stat(_, _, true)).WillOnce(testing::Return(score::cpp::expected_blank{})); EXPECT_EQ( - wait_for_file( - "/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, score::cpp::stop_token{}, mock), + FileWaiter{mock}.waitForFile( + "/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, score::cpp::stop_token{}), OsalReturnType::kSuccess); } @@ -69,8 +69,8 @@ TEST_F(WaitForFileTest, FileNotExisting) EXPECT_CALL(mock, stat(_, _, true)) .WillOnce(testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOTDIR)))); EXPECT_EQ( - wait_for_file( - "/some/path", FileExistenceState::NotExisting, kWaitTimeout, kPollInterval, score::cpp::stop_token{}, mock), + FileWaiter{mock}.waitForFile( + "/some/path", FileExistenceState::NotExisting, kWaitTimeout, kPollInterval, score::cpp::stop_token{}), OsalReturnType::kSuccess); } @@ -82,8 +82,8 @@ TEST_F(WaitForFileTest, Timeout) EXPECT_CALL(mock, stat(_, _, true)) .WillRepeatedly(testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(EINTR)))); EXPECT_EQ( - wait_for_file( - "/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, score::cpp::stop_token{}, mock), + FileWaiter{mock}.waitForFile( + "/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, score::cpp::stop_token{}), OsalReturnType::kTimeout); } @@ -95,8 +95,8 @@ TEST_F(WaitForFileTest, Error) EXPECT_CALL(mock, stat(_, _, true)) .WillRepeatedly(testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(EBADF)))); EXPECT_EQ( - wait_for_file( - "/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, score::cpp::stop_token{}, mock), + FileWaiter{mock}.waitForFile( + "/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, score::cpp::stop_token{}), OsalReturnType::kFail); } @@ -111,8 +111,8 @@ TEST_F(WaitForFileTest, NonNullTermindPathFails) .WillOnce(testing::Return(score::cpp::expected_blank{})); EXPECT_EQ( - wait_for_file( - "/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, score::cpp::stop_token{}, mock), + FileWaiter{mock}.waitForFile( + "/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, score::cpp::stop_token{}), OsalReturnType::kSuccess); } @@ -126,8 +126,8 @@ TEST_F(WaitForFileTest, StopRequested) EXPECT_CALL(mock, stat(_, _, true)).Times(0); EXPECT_EQ( - wait_for_file( - "/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, stop_source.get_token(), mock), + FileWaiter{mock}.waitForFile( + "/some/path", FileExistenceState::Exists, kWaitTimeout, kPollInterval, stop_source.get_token()), OsalReturnType::kFail); } diff --git a/score/launch_manager/src/daemon/src/osal/ifile_waiter.hpp b/score/launch_manager/src/daemon/src/osal/ifile_waiter.hpp new file mode 100644 index 000000000..e0bafa096 --- /dev/null +++ b/score/launch_manager/src/daemon/src/osal/ifile_waiter.hpp @@ -0,0 +1,45 @@ +/******************************************************************************** + * 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 OSAL_IFILE_WAITER_HPP_INCLUDED +#define OSAL_IFILE_WAITER_HPP_INCLUDED + +#include + +#include "score/language/safecpp/string_view/zstring_view.h" +#include "score/mw/launch_manager/configuration/component_config.hpp" +#include + +#include "return_types.hpp" + +namespace score::mw::lifecycle::internal::osal +{ + +/// @brief Abstraction over wait_for_file() for mocking. +class IFileWaiter +{ + public: + virtual ~IFileWaiter() = default; + + /// @see wait_for_file() for more info. + virtual OsalReturnType waitForFile( + score::safecpp::zstring_view path, + configuration::FileExistenceState condition, + std::chrono::milliseconds timeout, + std::chrono::milliseconds poll_interval, + const score::cpp::stop_token& stop_token) const = 0; +}; + +} // namespace score::mw::lifecycle::internal::osal + +#endif // OSAL_IFILE_WAITER_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/osal/mock_ifile_waiter.hpp b/score/launch_manager/src/daemon/src/osal/mock_ifile_waiter.hpp new file mode 100644 index 000000000..82b3b65f6 --- /dev/null +++ b/score/launch_manager/src/daemon/src/osal/mock_ifile_waiter.hpp @@ -0,0 +1,39 @@ +/******************************************************************************** + * 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 OSAL_MOCK_IFILE_WAITER_HPP_INCLUDED +#define OSAL_MOCK_IFILE_WAITER_HPP_INCLUDED + +#include "score/mw/launch_manager/osal/ifile_waiter.hpp" +#include + +namespace score::mw::lifecycle::internal::osal +{ + +class MockIFileWaiter : public IFileWaiter +{ + public: + MOCK_METHOD( + OsalReturnType, + waitForFile, + (score::safecpp::zstring_view path, + configuration::FileExistenceState condition, + std::chrono::milliseconds timeout, + std::chrono::milliseconds poll_interval, + const score::cpp::stop_token& stop_token), + (const, override)); +}; + +} // namespace score::mw::lifecycle::internal::osal + +#endif // OSAL_MOCK_IFILE_WAITER_HPP_INCLUDED diff --git a/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp b/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp index 390bbeb75..497913b02 100644 --- a/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp +++ b/score/launch_manager/src/daemon/src/osal/wait_for_file.hpp @@ -18,6 +18,7 @@ #include "score/language/safecpp/string_view/zstring_view.h" #include "score/mw/launch_manager/configuration/component_config.hpp" +#include "score/mw/launch_manager/osal/ifile_waiter.hpp" #include "score/os/stat.h" #include #include @@ -27,21 +28,31 @@ namespace score::mw::lifecycle::internal::osal { -/// @brief Block until the given path reaches the requested state, the timeout elapses, or a stop is requested. -/// -/// @param path The path to wait for. -/// @param condition The path state to wait for. -/// @param timeout The maximum time to wait for the condition. -/// @param poll_interval The time between two consecutive existence checks. -/// @param stop_token Checked between polls; a requested stop causes an early kFail return. -/// @param stat_os Optional score::os::Stat instance used to query the path. -OsalReturnType wait_for_file( - score::safecpp::zstring_view path, - configuration::FileExistenceState condition, - std::chrono::milliseconds timeout, - std::chrono::milliseconds poll_interval, - const score::cpp::stop_token& stop_token, - const score::os::Stat& stat_os = score::os::Stat::instance()) noexcept; +/// @brief IFileWaiter implementation that polls the real filesystem via score::os::Stat. +class FileWaiter final : public IFileWaiter +{ + public: + explicit FileWaiter(const score::os::Stat& stat_os = score::os::Stat::instance()) noexcept : stat_os_(stat_os) + { + } + + /// @brief Blocks until the given path reaches the requested state, the timeout elapses, or a stop is requested. + /// + /// @param path The path to wait for. + /// @param condition The path state to wait for. + /// @param timeout The maximum time to wait for the condition. + /// @param poll_interval The time between two consecutive existence checks. + /// @param stop_token Checked between polls; a requested stop causes an early kFail return. + OsalReturnType waitForFile( + score::safecpp::zstring_view path, + configuration::FileExistenceState condition, + std::chrono::milliseconds timeout, + std::chrono::milliseconds poll_interval, + const score::cpp::stop_token& stop_token) const override; + + private: + const score::os::Stat& stat_os_; +}; } // namespace score::mw::lifecycle::internal::osal 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 707cb5202..fba3d4a79 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/BUILD +++ b/score/launch_manager/src/daemon/src/process_group_manager/BUILD @@ -88,6 +88,7 @@ cc_library( "//score/launch_manager/src/daemon/src/configuration:config", "//score/launch_manager/src/daemon/src/control:control_client_channel", "//score/launch_manager/src/daemon/src/osal:ipc_comms", + "//score/launch_manager/src/daemon/src/osal:wait_for_file", "//score/launch_manager/src/daemon/src/process_group_manager/details:graph", "//score/launch_manager/src/daemon/src/process_group_manager/details:itransition_result_publisher", "//score/launch_manager/src/daemon/src/process_group_manager/details:os_handler", 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 869cdc90a..d36b17583 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 @@ -143,6 +143,7 @@ cc_library( visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__pkg__"], deps = [ ":safe_process_map", + "//score/launch_manager/src/daemon/src/osal:ifile_waiter", "//score/launch_manager/src/daemon/src/process_group_manager:iprocess", "//score/launch_manager/src/daemon/src/supervision_control_client:isupervision_event_publisher", ], @@ -162,9 +163,9 @@ cc_library( "//score/launch_manager/src/daemon/src/common:alive_interface_path", "//score/launch_manager/src/daemon/src/configuration:component_config", "//score/launch_manager/src/daemon/src/control:control_client_channel", + "//score/launch_manager/src/daemon/src/osal:ifile_waiter", "//score/launch_manager/src/daemon/src/osal:ipc_comms", "//score/launch_manager/src/daemon/src/osal:semaphore", - "//score/launch_manager/src/daemon/src/osal:wait_for_file", "//score/launch_manager/src/daemon/src/process_group_manager:iprocess", "//score/launch_manager/src/daemon/src/process_group_manager:process_state", "//score/launch_manager/src/daemon/src/supervision_control_client:isupervision_event_publisher", @@ -178,6 +179,7 @@ lm_cc_test( deps = [ ":process_info_node", ":safe_process_map", + "//score/launch_manager/src/daemon/src/osal:mock_ifile_waiter", "//score/launch_manager/src/daemon/src/process_group_manager:mock_iprocess", "//score/launch_manager/src/daemon/src/supervision_control_client:mock_supervision_event_publisher", "@googletest//:gtest_main", diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_handling.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_handling.hpp index 50cfbb37d..eba560a30 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_handling.hpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_handling.hpp @@ -14,6 +14,7 @@ #ifndef _INCLUDED_PROCESSHANDLING_ #define _INCLUDED_PROCESSHANDLING_ +#include "score/mw/launch_manager/osal/ifile_waiter.hpp" #include "score/mw/launch_manager/process_group_manager/details/safe_process_map.hpp" #include "score/mw/launch_manager/process_group_manager/iprocess.hpp" #include "score/mw/launch_manager/supervision_control_client/isupervision_event_publisher.hpp" @@ -33,6 +34,9 @@ struct ProcessHandling /// @brief Map to store the state of the process. std::shared_ptr process_map_; + + /// @brief Interface used to wait for a FileState ready condition. + osal::IFileWaiter* file_waiter_{nullptr}; }; } // namespace score::mw::lifecycle::internal diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp index 6adff5668..03d85063b 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp @@ -15,8 +15,8 @@ #include "score/launch_manager/src/daemon/src/configuration/component_config.hpp" #include "score/mw/launch_manager/common/alive_interface_path.hpp" #include "score/mw/launch_manager/common/log.hpp" +#include "score/mw/launch_manager/osal/ifile_waiter.hpp" #include "score/mw/launch_manager/osal/ipc_comms.hpp" -#include "score/mw/launch_manager/osal/wait_for_file.hpp" #include "score/mw/launch_manager/process_group_manager/details/safe_process_map.hpp" #include #include @@ -322,7 +322,7 @@ score::cpp::expected_blank ProcessInfoNode::handlePr static_cast(wait_res); } - const auto wait_res = osal::wait_for_file( + const auto wait_res = process_handling_.file_waiter_->waitForFile( arg.file_path, arg.state, std::chrono::milliseconds(config_.deployment_config.ready_timeout_ms), diff --git a/score/launch_manager/src/daemon/src/process_group_manager/process_group_manager.cpp b/score/launch_manager/src/daemon/src/process_group_manager/process_group_manager.cpp index ceb4f1ff6..495e9286a 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/process_group_manager.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/process_group_manager.cpp @@ -45,6 +45,7 @@ ProcessGroupManager::ProcessGroupManager( std::unique_ptr watchdog) : configuration_(std::move(config)), process_interface_(), + file_waiter_(), process_map_(nullptr), thread_pool_(nullptr), worker_jobs_(nullptr), @@ -210,7 +211,7 @@ bool ProcessGroupManager::initializeProcessGroups() configuration_.components().size() + configuration_.runTargets().size() + 2, configuration_, worker_jobs_, - ProcessHandling{*supervision_control_notifier_.get(), &process_interface_, process_map_}, + ProcessHandling{*supervision_control_notifier_.get(), &process_interface_, process_map_, &file_waiter_}, this); LM_LOG_DEBUG() << "Process group initialized successfully"; diff --git a/score/launch_manager/src/daemon/src/process_group_manager/process_group_manager.hpp b/score/launch_manager/src/daemon/src/process_group_manager/process_group_manager.hpp index 811cff93a..15977d364 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/process_group_manager.hpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/process_group_manager.hpp @@ -24,6 +24,7 @@ #include "score/mw/launch_manager/common/identifier_hash.hpp" #include "score/mw/launch_manager/configuration/config.hpp" #include "score/mw/launch_manager/control/control_client_channel.hpp" +#include "score/mw/launch_manager/osal/wait_for_file.hpp" #include "score/mw/launch_manager/process_group_manager/details/component_event_queue.hpp" #include "score/mw/launch_manager/process_group_manager/details/graph.hpp" #include "score/mw/launch_manager/process_group_manager/details/itransition_result_publisher.hpp" @@ -264,6 +265,9 @@ class ProcessGroupManager final : public ITransitionResultPublisher /// @brief The process interface object associated with the ProcessGroupManager. osal::ProcessLauncher process_interface_; + /// @brief Waits for FileState ready conditions; injected into ProcessInfoNode via ProcessHandling. + osal::FileWaiter file_waiter_; + /// @brief Shared pointer to the SafeProcessMap object. std::shared_ptr process_map_; From 6883f7b7c7fdaeee06f892b85129157043ad5e2d Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Fri, 28 Aug 2026 14:34:57 +0100 Subject: [PATCH 15/16] Adding UTs --- .../details/process_info_node_UT.cpp | 121 ++++++++++++++++++ .../details/process_launcher.hpp | 2 +- 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node_UT.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node_UT.cpp index 6c176d40a..d32d7fc24 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node_UT.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node_UT.cpp @@ -11,6 +11,7 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ +#include "score/mw/launch_manager/osal/mock_ifile_waiter.hpp" #include "score/mw/launch_manager/process_group_manager/details/process_info_node.hpp" #include "score/mw/launch_manager/process_group_manager/details/safe_process_map.hpp" #include "score/mw/launch_manager/process_group_manager/mock_iprocess.hpp" @@ -20,6 +21,7 @@ #include #include #include +#include #include #include @@ -71,6 +73,29 @@ class ProcessInfoNodeFixture : public ::testing::Test std::move(config), kProcessIndex, ProcessHandling{mock_publisher_, &mock_processIf_, process_map_}); } + /// @brief Helper method to create a ProcessInfoNode with a FileState ready condition. + std::unique_ptr createFileStateProcessInfoNode( + std::string file_path, + configuration::FileExistenceState state, + configuration::ApplicationType application_type = configuration::ApplicationType::Reporting, + std::chrono::milliseconds ready_timeout = std::chrono::milliseconds{50}, + std::chrono::milliseconds poll_interval = std::chrono::milliseconds{5}) + { + configuration::ComponentConfig config{}; + config.name = "test_process"; + config.component_properties.binary_name = "test_process"; + config.component_properties.application_profile.application_type = application_type; + config.component_properties.ready_condition = + configuration::ReadyCondition{configuration::FileState{std::move(file_path), state, poll_interval}}; + config.deployment_config.ready_timeout_ms = static_cast(ready_timeout.count()); + config.deployment_config.shutdown_timeout_ms = shutdown_timeout_ms_; + + return std::make_unique( + std::move(config), + kProcessIndex, + ProcessHandling{mock_publisher_, &mock_processIf_, process_map_, &mock_file_waiter_}); + } + /// @brief Helper method to create a ProcessInfoNode that is self-terminating. std::unique_ptr createSelfTerminatingProcessInfoNode( configuration::ApplicationType application_type = configuration::ApplicationType::Reporting, @@ -124,6 +149,7 @@ class ProcessInfoNodeFixture : public ::testing::Test score::cpp::stop_source stop_source_{}; std::shared_ptr process_map_{std::make_shared()}; StrictMock mock_processIf_{}; + StrictMock mock_file_waiter_{}; NiceMock mock_publisher_{}; }; @@ -597,3 +623,98 @@ TEST_F(ProcessInfoNodeDeactivationTest, ProcessIgnoresSigterm_ForcedWithSigkill) ASSERT_THAT(node->active(), IsFalse()); ASSERT_THAT(node->getState(), Eq(score::mw::lifecycle::ProcessState::kIdle)); } + +class ProcessInfoNodeFileStateTest : public ProcessInfoNodeFixture +{ +}; + +TEST_F(ProcessInfoNodeFileStateTest, ConditionAlreadyMet_ReturnsSuccess) +{ + RecordProperty( + "Description", + "A FileState ready condition with Exists that is satisfied lets activate() " + "return kSuccess."); + + auto node = createFileStateProcessInfoNode( + "/ready", + configuration::FileExistenceState::Exists, + configuration::ApplicationType::Reporting, + std::chrono::milliseconds{50}, + std::chrono::milliseconds{5}); + expectSuccessfulProcessLaunch(); + EXPECT_CALL(mock_processIf_, ignoreRunning(_)).WillOnce(Return(osal::OsalReturnType::kSuccess)); + EXPECT_CALL( + mock_file_waiter_, + waitForFile( + _, + Eq(configuration::FileExistenceState::Exists), + Eq(std::chrono::milliseconds{50}), + Eq(std::chrono::milliseconds{5}), + _)) + .WillOnce(Return(osal::OsalReturnType::kSuccess)); + EXPECT_CALL(mock_publisher_, reportActivation); + + 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::kRunning)); +} + +TEST_F(ProcessInfoNodeFileStateTest, NotExistingCondition_ReturnsSuccess) +{ + RecordProperty( + "Description", + "A FileState ready condition with NotExisting that is satisfied lets activate() " + "return kSuccess."); + + auto node = createFileStateProcessInfoNode("/var/run/gone", configuration::FileExistenceState::NotExisting); + expectSuccessfulProcessLaunch(); + EXPECT_CALL(mock_processIf_, ignoreRunning(_)).WillOnce(Return(osal::OsalReturnType::kSuccess)); + EXPECT_CALL(mock_file_waiter_, waitForFile(_, Eq(configuration::FileExistenceState::NotExisting), _, _, _)) + .WillOnce(Return(osal::OsalReturnType::kSuccess)); + EXPECT_CALL(mock_publisher_, reportActivation); + + 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::kRunning)); +} + +TEST_F(ProcessInfoNodeFileStateTest, NativeApplication_DoesNotIgnoreRunning_ReturnsSuccess) +{ + RecordProperty("Description", "A FileState ready condition with a native process hall not call ignoreRunning."); + + auto node = createFileStateProcessInfoNode( + "/var/run/ready", configuration::FileExistenceState::Exists, configuration::ApplicationType::Native); + expectSuccessfulProcessLaunch(); + EXPECT_CALL(mock_file_waiter_, waitForFile(_, _, _, _, _)).WillOnce(Return(osal::OsalReturnType::kSuccess)); + + 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::kRunning)); +} + +TEST_F(ProcessInfoNodeFileStateTest, WaitForFileTimesOut_ReturnsActivationTimedOut) +{ + RecordProperty( + "Description", + "If waitForFile() times out, activate() returns kActivationTimedOut and the process ends up terminated."); + + auto node = createFileStateProcessInfoNode("/var/run/ready", configuration::FileExistenceState::Exists); + expectSuccessfulProcessLaunch(); + EXPECT_CALL(mock_processIf_, ignoreRunning(_)).WillOnce(Return(osal::OsalReturnType::kSuccess)); + EXPECT_CALL(mock_file_waiter_, waitForFile(_, _, _, _, _)).WillOnce(Return(osal::OsalReturnType::kTimeout)); + // Simulate the OS handler reporting the killed process's exit once termination is requested. + expectOsAcknowledgesTermination(node.get()); + EXPECT_CALL(mock_publisher_, reportActivation).Times(0); + + auto result = node->activate(score::cpp::stop_token{}); + + ASSERT_THAT(result.has_value(), IsFalse()); + ASSERT_THAT(result.error(), Eq(IComponent::ComponentError::kActivationTimedOut)); + ASSERT_THAT(node->getState(), Eq(score::mw::lifecycle::ProcessState::kTerminated)); +} diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.hpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.hpp index f30eda374..363ca0a33 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.hpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.hpp @@ -52,7 +52,7 @@ class ProcessLauncher final : public IProcess OsalReturnType waitForkRunning(IpcCommsP sync, std::chrono::milliseconds timeout) override; /// @see IProcess::waitForkRunning() for details - OsalReturnType ignoreRunning(IpcCommsP sync); + OsalReturnType ignoreRunning(IpcCommsP sync) override; private: /// @brief Creates shared memory for communication between processes. From d94d64e1d235c9b2b0e86cc2a01712870488274d Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Fri, 28 Aug 2026 15:06:21 +0100 Subject: [PATCH 16/16] Renaming & getting 100% branch coverage --- .../src/osal/details/posix/wait_for_file.cpp | 6 +--- .../osal/details/posix/wait_for_file_UT.cpp | 32 +++++++++++++++++++ .../ready_conditions/file_state/exists/BUILD | 6 ++-- ...ent.cpp => control_client_test_driver.cpp} | 0 .../exists/ready_condition_file.json | 12 +++---- .../file_state/exists/ready_condition_file.py | 6 +++- .../file_state/not_existing/BUILD | 6 ++-- ...ent.cpp => control_client_test_driver.cpp} | 0 .../ready_condition_file_not_existing.json | 10 +++--- .../ready_condition_file_not_existing.py | 2 +- 10 files changed, 56 insertions(+), 24 deletions(-) rename tests/integration/ready_conditions/file_state/exists/{fake_control_client.cpp => control_client_test_driver.cpp} (100%) rename tests/integration/ready_conditions/file_state/not_existing/{fake_control_client.cpp => control_client_test_driver.cpp} (100%) diff --git a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp index f61ee66f1..b205f3760 100644 --- a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp +++ b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file.cpp @@ -63,19 +63,15 @@ OsalReturnType FileWaiter::waitForFile( // treat file or dir not existing as the same [[fallthrough]]; case ENOTDIR: - { if (!wait_for_existence) { return OsalReturnType::kSuccess; } break; - }; case EINTR: - break; + break; // retry default: - { return OsalReturnType::kFail; - } } } diff --git a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp index eccc00017..70e094ce3 100644 --- a/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp +++ b/score/launch_manager/src/daemon/src/osal/details/posix/wait_for_file_UT.cpp @@ -131,4 +131,36 @@ TEST_F(WaitForFileTest, StopRequested) OsalReturnType::kFail); } +TEST_F(WaitForFileTest, FileNotExistingViaEnoent) +{ + RecordProperty( + "Description", "Verify that using FileExistenceState::NotExisting will return success if stat returns ENOENT."); + + score::os::StatMock mock{}; + EXPECT_CALL(mock, stat(_, _, true)) + .WillOnce(testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOENT)))); + EXPECT_EQ( + FileWaiter{mock}.waitForFile( + "/some/path", FileExistenceState::NotExisting, kWaitTimeout, kPollInterval, score::cpp::stop_token{}), + OsalReturnType::kSuccess); +} + +TEST_F(WaitForFileTest, FileExistsWhileWaitingForNotExisting) +{ + RecordProperty( + "Description", + "Verify that if FileExistenceState::NotExisting is requested but stat still succeeds, the wait " + "is re-polled instead of returning immediately."); + + score::os::StatMock mock{}; + EXPECT_CALL(mock, stat(_, _, true)) + .WillOnce(testing::Return(score::cpp::expected_blank{})) + .WillOnce(testing::Return(score::cpp::make_unexpected(score::os::Error::createFromErrno(ENOENT)))); + + EXPECT_EQ( + FileWaiter{mock}.waitForFile( + "/some/path", FileExistenceState::NotExisting, kWaitTimeout, kPollInterval, score::cpp::stop_token{}), + OsalReturnType::kSuccess); +} + } // namespace diff --git a/tests/integration/ready_conditions/file_state/exists/BUILD b/tests/integration/ready_conditions/file_state/exists/BUILD index f518ce9fb..eec21764e 100644 --- a/tests/integration/ready_conditions/file_state/exists/BUILD +++ b/tests/integration/ready_conditions/file_state/exists/BUILD @@ -14,8 +14,8 @@ load("@rules_cc//cc:cc_binary.bzl", "cc_binary") load("//tests/utils/bazel:integration.bzl", "integration_test") cc_binary( - name = "fake_control_client", - srcs = ["fake_control_client.cpp"], + name = "control_client_test_driver", + srcs = ["control_client_test_driver.cpp"], deps = [ "//score/launch_manager:control_cc", "//score/launch_manager:lifecycle_cc", @@ -29,7 +29,7 @@ integration_test( srcs = ["ready_condition_file.py"], binaries = [ "//tests/integration/ready_conditions/file_state/common:file_modifier", - ":fake_control_client", + ":control_client_test_driver", "//score/launch_manager", ], config = ":ready_condition_file.json", diff --git a/tests/integration/ready_conditions/file_state/exists/fake_control_client.cpp b/tests/integration/ready_conditions/file_state/exists/control_client_test_driver.cpp similarity index 100% rename from tests/integration/ready_conditions/file_state/exists/fake_control_client.cpp rename to tests/integration/ready_conditions/file_state/exists/control_client_test_driver.cpp diff --git a/tests/integration/ready_conditions/file_state/exists/ready_condition_file.json b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.json index 0c9ef130e..06dc49145 100644 --- a/tests/integration/ready_conditions/file_state/exists/ready_condition_file.json +++ b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.json @@ -28,9 +28,9 @@ } }, "components": { - "fake_control_client": { + "control_client_test_driver": { "component_properties": { - "binary_name": "fake_control_client", + "binary_name": "control_client_test_driver", "ready_condition": { "process_state": "Running" }, @@ -95,7 +95,7 @@ "run_targets": { "Startup": { "depends_on": [ - "fake_control_client" + "control_client_test_driver" ], "recovery_action": { "switch_run_target": { @@ -107,7 +107,7 @@ "depends_on": [ "file_creating_component", "file_creating_component_reporting", - "fake_control_client" + "control_client_test_driver" ], "recovery_action": { "switch_run_target": { @@ -118,7 +118,7 @@ "timeout": { "depends_on": [ "file_creating_component_timeout", - "fake_control_client" + "control_client_test_driver" ], "recovery_action": { "switch_run_target": { @@ -129,6 +129,6 @@ }, "initial_run_target": "Startup", "fallback_run_target": { - "depends_on": ["fake_control_client"] + "depends_on": ["control_client_test_driver"] } } diff --git a/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py index 006bbb689..7d11879e7 100644 --- a/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py +++ b/tests/integration/ready_conditions/file_state/exists/ready_condition_file.py @@ -44,5 +44,9 @@ def test_ready_condition_file(target, setup_test, assert_test_results, remote_te ) assert_test_results( - {"fake_control_client.xml", "file_modifier.xml", "file_modifier_reporting.xml"} + { + "control_client_test_driver.xml", + "file_modifier.xml", + "file_modifier_reporting.xml", + } ) diff --git a/tests/integration/ready_conditions/file_state/not_existing/BUILD b/tests/integration/ready_conditions/file_state/not_existing/BUILD index 177882f46..29ddd8d53 100644 --- a/tests/integration/ready_conditions/file_state/not_existing/BUILD +++ b/tests/integration/ready_conditions/file_state/not_existing/BUILD @@ -14,8 +14,8 @@ load("@rules_cc//cc:cc_binary.bzl", "cc_binary") load("//tests/utils/bazel:integration.bzl", "integration_test") cc_binary( - name = "fake_control_client", - srcs = ["fake_control_client.cpp"], + name = "control_client_test_driver", + srcs = ["control_client_test_driver.cpp"], deps = [ "//score/launch_manager:control_cc", "//score/launch_manager:lifecycle_cc", @@ -29,7 +29,7 @@ integration_test( srcs = ["ready_condition_file_not_existing.py"], binaries = [ "//tests/integration/ready_conditions/file_state/common:file_modifier", - ":fake_control_client", + ":control_client_test_driver", "//score/launch_manager", ], config = ":ready_condition_file_not_existing.json", diff --git a/tests/integration/ready_conditions/file_state/not_existing/fake_control_client.cpp b/tests/integration/ready_conditions/file_state/not_existing/control_client_test_driver.cpp similarity index 100% rename from tests/integration/ready_conditions/file_state/not_existing/fake_control_client.cpp rename to tests/integration/ready_conditions/file_state/not_existing/control_client_test_driver.cpp diff --git a/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json index 2fd717fca..d3ac09185 100644 --- a/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json +++ b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.json @@ -33,9 +33,9 @@ } }, "components": { - "fake_control_client": { + "control_client_test_driver": { "component_properties": { - "binary_name": "fake_control_client", + "binary_name": "control_client_test_driver", "application_profile": { "application_type": "State_Manager" } @@ -96,7 +96,7 @@ "run_targets": { "Startup": { "depends_on": [ - "fake_control_client" + "control_client_test_driver" ], "recovery_action": { "switch_run_target": { @@ -107,7 +107,7 @@ "working": { "depends_on": [ "file_deleting_component", - "fake_control_client" + "control_client_test_driver" ], "recovery_action": { "switch_run_target": { @@ -118,7 +118,7 @@ "timeout": { "depends_on": [ "file_deleting_component_timeout", - "fake_control_client" + "control_client_test_driver" ], "recovery_action": { "switch_run_target": { diff --git a/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py index 05d998896..d8c63648a 100644 --- a/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py +++ b/tests/integration/ready_conditions/file_state/not_existing/ready_condition_file_not_existing.py @@ -54,4 +54,4 @@ def test_ready_condition_file_not_existing( timeout_s=3.0, ) - assert_test_results({"fake_control_client.xml", "file_modifier.xml"}) + assert_test_results({"control_client_test_driver.xml", "file_modifier.xml"})