From 9fce5557a25c3a2788a4527d58e3262b4081b699 Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Mon, 3 Aug 2026 08:40:02 +0100 Subject: [PATCH 1/5] Adding new config field --- .../docs/user_guide/configuration.rst | 15 +++ .../src/daemon/src/configuration/config.hpp | 16 ++- .../config_schema/launch_manager.schema.json | 30 ++++- .../configuration/configuration_adapter.cpp | 43 ++++--- .../configuration/configuration_adapter.hpp | 9 +- .../configuration_adapter_UT.cpp | 112 ++++++++++++++++++ .../details/flatbuffer_config_loader_UT.cpp | 49 +++++++- .../details/flatbuffer_type_converters.cpp | 73 +++++++++--- .../details/flatbuffer_type_converters.hpp | 7 +- .../details/flatbuffer_type_converters_UT.cpp | 95 ++++++++++++++- .../src/configuration/details/lm_flatcfg.fbs | 22 +++- 11 files changed, 428 insertions(+), 43 deletions(-) diff --git a/score/launch_manager/docs/user_guide/configuration.rst b/score/launch_manager/docs/user_guide/configuration.rst index a51caa11d..1678691a9 100644 --- a/score/launch_manager/docs/user_guide/configuration.rst +++ b/score/launch_manager/docs/user_guide/configuration.rst @@ -208,6 +208,21 @@ component_properties (object) * **Allowed Values:** * ``"Running"``: The process has started and reached its running state. * ``"Terminated"``: The process has started, reached its running state, and then terminated successfully. + * **file_state** (object, optional) + * **Description:** Specifies a ready condition based on the existence state of a file at a given path. + * **Properties:** + * **file_path** (string, required) + * **Description:** Specifies the absolute path to the file being watched. + * **state** (string, optional) + * **Description:** Specifies the required existence state of the file. + * **Allowed Values:** + * ``"Exists"``: The component is ready when the file at ``file_path`` exists. + * ``"Deleted"``: The component is ready when the file at ``file_path`` is deleted. + * **Default:** ``"Exists"`` + * **polling_interval** (integer, optional) + * **Description:** Specifies the time interval, in milliseconds, at which the **Launch Manager** checks the file existence state. + * **Constraint:** Must be greater than 0. + * **Default:** ``10`` .. _lm_conf_deployment_config_object_: diff --git a/score/launch_manager/src/daemon/src/configuration/config.hpp b/score/launch_manager/src/daemon/src/configuration/config.hpp index 12dbfdbd7..b4a2c09d0 100644 --- a/score/launch_manager/src/daemon/src/configuration/config.hpp +++ b/score/launch_manager/src/daemon/src/configuration/config.hpp @@ -14,10 +14,12 @@ #define CONFIG_HPP #include +#include #include #include #include #include +#include #include namespace score::mw::lifecycle::internal::configuration @@ -37,6 +39,12 @@ enum class ProcessState : uint8_t Terminated = 1 }; +enum class FileExistenceState : uint8_t +{ + Exists = 0, + Deleted, +}; + struct ComponentAliveSupervision { uint32_t reporting_cycle_ms{}; @@ -52,11 +60,15 @@ struct ApplicationProfile std::optional alive_supervision; }; -struct ReadyCondition +struct FileState { - ProcessState process_state{ProcessState::Running}; + std::string file_path; + FileExistenceState state{FileExistenceState::Exists}; + std::chrono::milliseconds polling_interval{10}; }; +using ReadyCondition = std::variant; + struct ComponentProperties { std::string binary_name; diff --git a/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json b/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json index 371630d73..990a9411d 100644 --- a/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json +++ b/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json @@ -89,6 +89,34 @@ "Terminated" ], "description": "Specifies the required state of the component's POSIX process. 'Running': the process has started and reached its running state. 'Terminated': the process has started, reached its running state, and then terminated successfully." + }, + "file_state": { + "type": "object", + "description": "Specifies a ready condition based on the existence state of a file at a given path.", + "properties": { + "file_path": { + "type": "string", + "pattern": "^/.*", + "description": "Specifies the absolute path to the file being watched." + }, + "state": { + "type": "string", + "enum": [ + "Exists", + "Deleted" + ], + "description": "Specifies the required existence state of the file. 'Exists': the file must be present at 'file_path'. 'Deleted': the file must be absent from 'file_path'. Defaults to 'Exists' if not specified." + }, + "polling_interval": { + "type": "integer", + "exclusiveMinimum": 0, + "description": "Specifies the time interval, in milliseconds, at which the Launch Manager checks the file existence state." + } + }, + "required": [ + "file_path" + ], + "additionalProperties": false } }, "required": [], @@ -488,4 +516,4 @@ "initial_run_target" ], "additionalProperties": false -} \ No newline at end of file +} diff --git a/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp b/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp index 20564194b..6688e4698 100644 --- a/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp +++ b/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -30,7 +31,7 @@ namespace constexpr const char* kAliveInterfaceEnvName = "LCM_ALIVE_INTERFACE_PATH"; constexpr uint32_t kDefaultProcessExecutionError = 1U; -uint64_t defaultProcessorAffinityMask() +[[maybe_unused]] uint64_t defaultProcessorAffinityMask() { return (1ULL << score::mw::lifecycle::internal::osal::getNumCores()) - 1ULL; } @@ -202,19 +203,34 @@ DependencyList ConfigurationAdapter::buildDependencyList(const ComponentProperti for (const auto& dep_name : props.depends_on) { + auto dep_it = component_by_name_.find(dep_name); + SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE( + dep_it != component_by_name_.end(), "Component's dependency points to a non-existent component"); + + const auto& dep_props = dep_it->second->component_properties; + Dependency dep{}; dep.process_state_ = score::mw::lifecycle::ProcessState::kRunning; - - auto dep_it = component_by_name_.find(dep_name); - if (dep_it != component_by_name_.end()) + if (dep_props.ready_condition.has_value()) { - const auto& dep_props = dep_it->second->component_properties; - if (dep_props.ready_condition.has_value()) - { - dep.process_state_ = dep_props.ready_condition->process_state == ProcessState::Running - ? score::mw::lifecycle::ProcessState::kRunning - : score::mw::lifecycle::ProcessState::kTerminated; - } + std::visit( + [&dep](auto&& arg) { + using argT = std::decay_t; + + if constexpr (std::is_same_v) + { + dep.process_state_ = arg == ProcessState::Running + ? score::mw::lifecycle::ProcessState::kRunning + : score::mw::lifecycle::ProcessState::kTerminated; + return; + } + else if constexpr (std::is_same_v) + { + SCORE_LANGUAGE_FUTURECPP_UNREACHABLE_MESSAGE("FileState is not yet supported"); + return; + } + }, + dep_props.ready_condition.value()); } dep.target_process_id_ = IdentifierHash{dep_name}; @@ -255,11 +271,9 @@ void ConfigurationAdapter::resolveDependsOnEntry( return; } - bool found = false; auto comp_it = component_to_process_index_.find(dep_name); if (comp_it != component_to_process_index_.end()) { - found = true; if (std::find(indexes.begin(), indexes.end(), comp_it->second) == indexes.end()) { indexes.push_back(comp_it->second); @@ -278,14 +292,11 @@ void ConfigurationAdapter::resolveDependsOnEntry( auto dep_it = depends_on_by_name.find(dep_name); if (dep_it != depends_on_by_name.end()) { - found = true; for (const auto& sub_dep : *dep_it->second) { resolveDependsOnEntry(sub_dep, depends_on_by_name, indexes, visited); } } - - assert(found && "depends_on references unknown component or run_target"); } ProcessGroupState ConfigurationAdapter::buildProcessGroupState( diff --git a/score/launch_manager/src/daemon/src/configuration/configuration_adapter.hpp b/score/launch_manager/src/daemon/src/configuration/configuration_adapter.hpp index 9cc87683d..3d6203cf1 100644 --- a/score/launch_manager/src/daemon/src/configuration/configuration_adapter.hpp +++ b/score/launch_manager/src/daemon/src/configuration/configuration_adapter.hpp @@ -107,21 +107,28 @@ class ConfigurationAdapter final bool buildFromConfig(const Config& config); OsProcess buildOsProcess(const ComponentConfig& comp, uint32_t process_index) const; + void fillStartupConfigFromDeployment( const ComponentConfig& comp, score::mw::lifecycle::internal::osal::OsalConfig& startup) const; + void fillStartupArguments( const ComponentProperties& props, score::mw::lifecycle::internal::osal::OsalConfig& startup) const; + size_t fillStartupEnvironment( const DeploymentConfig& deploy, score::mw::lifecycle::internal::osal::OsalConfig& startup) const; + void appendAliveInterfaceEnvironment( const ComponentConfig& comp, size_t& env_index, score::mw::lifecycle::internal::osal::OsalConfig& startup) const; + PgManagerConfig buildPgManagerConfig(const ComponentConfig& comp) const; - DependencyList buildDependencyList(const ComponentProperties& props) const; + + /// @brief Given a components properties, creates a list of dependencies. + [[nodiscard]] DependencyList buildDependencyList(const ComponentProperties& props) const; std::vector buildProcessGroupStates(const Config& config) const; ProcessGroupState buildProcessGroupState( diff --git a/score/launch_manager/src/daemon/src/configuration/configuration_adapter_UT.cpp b/score/launch_manager/src/daemon/src/configuration/configuration_adapter_UT.cpp index d07d4daae..b12a52ffc 100644 --- a/score/launch_manager/src/daemon/src/configuration/configuration_adapter_UT.cpp +++ b/score/launch_manager/src/daemon/src/configuration/configuration_adapter_UT.cpp @@ -571,5 +571,117 @@ TEST(ConfigurationAdapterFallbackTest, FallbackRunTargetResolvesDependenciesRecu adapter.deinitialize(); } +TEST(ConfigurationAdapterDependencyTest, DependencyOnNonExistentComponentIsIgnored) +{ + RecordProperty("Description", "When a component depends on a non-existent component, the dependency is skipped."); + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "explorative-testing"); + + ComponentConfig comp_a; + comp_a.name = "comp_a"; + comp_a.component_properties.application_profile.application_type = ApplicationType::Native; + comp_a.component_properties.application_profile.is_self_terminating = false; + comp_a.component_properties.depends_on = {"non_existent_component", "also_missing"}; + comp_a.deployment_config.bin_dir = "/opt"; + comp_a.component_properties.binary_name = "comp_a"; + comp_a.deployment_config.working_dir = "/tmp"; + comp_a.deployment_config.sandbox.uid = 0; + comp_a.deployment_config.sandbox.gid = 0; + comp_a.deployment_config.sandbox.scheduling_policy = SCHED_OTHER; + comp_a.deployment_config.sandbox.scheduling_priority = 0; + + std::vector components; + components.push_back(std::move(comp_a)); + + RunTargetConfig startup; + startup.name = "Startup"; + startup.depends_on = {"comp_a"}; + startup.transition_timeout_ms = 5000; + startup.recovery_action.run_target = "fallback_run_target"; + + std::vector run_targets; + run_targets.push_back(std::move(startup)); + + FallbackRunTargetConfig fallback; + fallback.transition_timeout_ms = 1500; + AliveSupervisionConfig alive; + alive.evaluation_cycle_ms = 500; + + auto config = ConfigBuilder{} + .setComponents(std::move(components)) + .setRunTargets(std::move(run_targets)) + .setInitialRunTarget("Startup") + .setFallbackRunTarget(std::move(fallback)) + .setAliveSupervision(alive) + .build(); + + ConfigurationAdapter adapter; + EXPECT_DEATH(adapter.initialize(config), "Component's dependency.*"); +} + +TEST(ConfigurationAdapterReadyConditionTest, FileStateReadyConditionTriggersAssert) +{ + RecordProperty("Description", "When a dependency target has FileState ready_condition, it triggers an assertion."); + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "explorative-testing"); + + ComponentConfig comp_a; + comp_a.name = "comp_a"; + comp_a.component_properties.application_profile.application_type = ApplicationType::Native; + comp_a.component_properties.application_profile.is_self_terminating = false; + FileState file_state{"/tmp/ready.txt", FileExistenceState::Exists, std::chrono::milliseconds{100}}; + comp_a.component_properties.ready_condition = ReadyCondition{file_state}; + comp_a.deployment_config.bin_dir = "/opt"; + comp_a.component_properties.binary_name = "comp_a"; + comp_a.deployment_config.working_dir = "/tmp"; + comp_a.deployment_config.sandbox.uid = 0; + comp_a.deployment_config.sandbox.gid = 0; + comp_a.deployment_config.sandbox.scheduling_policy = SCHED_OTHER; + comp_a.deployment_config.sandbox.scheduling_priority = 0; + + ComponentConfig comp_b; + comp_b.name = "comp_b"; + comp_b.component_properties.application_profile.application_type = ApplicationType::Native; + comp_b.component_properties.application_profile.is_self_terminating = false; + comp_b.component_properties.ready_condition = ReadyCondition{ProcessState::Running}; + comp_b.component_properties.depends_on = {"comp_a"}; + comp_b.deployment_config.bin_dir = "/opt"; + comp_b.component_properties.binary_name = "comp_b"; + comp_b.deployment_config.working_dir = "/tmp"; + comp_b.deployment_config.sandbox.uid = 0; + comp_b.deployment_config.sandbox.gid = 0; + comp_b.deployment_config.sandbox.scheduling_policy = SCHED_OTHER; + comp_b.deployment_config.sandbox.scheduling_priority = 0; + + std::vector components; + components.push_back(std::move(comp_a)); + components.push_back(std::move(comp_b)); + + RunTargetConfig startup; + startup.name = "Startup"; + startup.depends_on = {"comp_b"}; + startup.transition_timeout_ms = 5000; + startup.recovery_action.run_target = "fallback_run_target"; + + std::vector run_targets; + run_targets.push_back(std::move(startup)); + + FallbackRunTargetConfig fallback; + fallback.transition_timeout_ms = 1500; + AliveSupervisionConfig alive; + alive.evaluation_cycle_ms = 500; + + auto config = ConfigBuilder{} + .setComponents(std::move(components)) + .setRunTargets(std::move(run_targets)) + .setInitialRunTarget("Startup") + .setFallbackRunTarget(std::move(fallback)) + .setAliveSupervision(alive) + .build(); + + ConfigurationAdapter adapter; + EXPECT_DEATH(adapter.initialize(config), "FileState.*"); +} + } // namespace } // namespace score::mw::lifecycle::internal::configuration diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp index 85c4e1b3c..63398ae09 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp @@ -31,10 +31,12 @@ namespace namespace fb = score::mw::lifecycle::internal::configuration::fb; using ::testing::Eq; +using ::testing::FieldsAre; using ::testing::IsFalse; using ::testing::IsNull; using ::testing::IsTrue; using ::testing::StrEq; +using ::testing::VariantWith; const score::filesystem::Path kTestPath{"/tmp/test_config.bin"}; @@ -258,13 +260,58 @@ TEST_F(FlatbufferConfigLoaderTest, LoadSingleComponent) ASSERT_THAT(comp.component_properties.process_arguments.size(), Eq(1U)); EXPECT_THAT(comp.component_properties.process_arguments[0], Eq("--verbose")); ASSERT_THAT(comp.component_properties.ready_condition.has_value(), IsTrue()); - EXPECT_THAT(comp.component_properties.ready_condition->process_state, Eq(ProcessState::Running)); + EXPECT_THAT(*comp.component_properties.ready_condition, VariantWith(Eq(ProcessState::Running))); EXPECT_THAT(comp.deployment_config.ready_timeout_ms, Eq(1500U)); EXPECT_THAT(comp.deployment_config.shutdown_timeout_ms, Eq(2500U)); EXPECT_THAT(comp.deployment_config.bin_dir, Eq("/opt/bin")); EXPECT_THAT(comp.deployment_config.working_dir, Eq("/tmp")); } +TEST_F(FlatbufferConfigLoaderTest, LoadSingleComponentWithFileState) +{ + RecordProperty("Description", "Loads a component whose ready_condition includes a file_state."); + + ::flatbuffers::FlatBufferBuilder fbb; + + auto app_profile = fb::CreateApplicationProfile(fbb, fb::ApplicationType::Native, false /*is_self_terminating*/); + auto bin_name = fbb.CreateString("my_binary"); + auto file_state = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists); + auto ready_cond = fb::CreateReadyCondition(fbb, std::nullopt, file_state); + auto comp_props = fb::CreateComponentProperties( + fbb, bin_name, app_profile, 0 /*depends_on*/, 0 /*process_arguments*/, ready_cond); + + auto bin_dir = fbb.CreateString("/opt/bin"); + auto work_dir = fbb.CreateString("/tmp"); + auto sandbox = buildDefaultSandbox(fbb); + auto deploy = fb::CreateDeploymentConfig( + fbb, + 1.5 /*ready_timeout*/, + 2.5 /*shutdown_timeout*/, + 0 /*environmental_variables*/, + bin_dir, + work_dir, + 0 /*ready_recovery_action*/, + 0 /*recovery_action*/, + sandbox); + + auto comp_name = fbb.CreateString("TestComponent"); + auto comp_desc = fbb.CreateString("A test component"); + auto component = fb::CreateComponent(fbb, comp_name, comp_desc, comp_props, deploy); + auto comps = fbb.CreateVector(std::vector<::flatbuffers::Offset>{component}); + + auto result = loadBuffer(buildConfigWithComponents(fbb, comps)); + + ASSERT_THAT(result.has_value(), IsTrue()); + ASSERT_THAT(result->components().size(), Eq(1U)); + + const auto& comp = result->components()[0]; + ASSERT_THAT(comp.component_properties.ready_condition.has_value(), IsTrue()); + EXPECT_THAT( + *comp.component_properties.ready_condition, + VariantWith( + FieldsAre(Eq("/tmp/ready"), Eq(FileExistenceState::Exists), Eq(std::chrono::milliseconds{10})))); +} + TEST_F(FlatbufferConfigLoaderTest, LoadRunTargets) { RecordProperty("Description", "Loads run targets with dependencies and transition timeout."); diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp index 6c5c4f0fd..20e0bdfdd 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp @@ -108,6 +108,18 @@ ProcessState convertProcessState(fb::ProcessState fb_state) } } +FileExistenceState convertFileExistenceState(fb::FileExistenceState fb_state) +{ + switch (fb_state) + { + case fb::FileExistenceState::Deleted: + return FileExistenceState::Deleted; + case fb::FileExistenceState::Exists: + return FileExistenceState::Exists; + } + SCORE_LANGUAGE_FUTURECPP_UNREACHABLE(); +} + score::cpp::expected convertSchedulingPolicy(fb::SchedulingPolicy policy) { switch (policy) @@ -301,19 +313,57 @@ score::cpp::expected convertApplicatio return result; } -score::cpp::expected convertReadyCondition(const fb::ReadyCondition* fb_rc) +std::optional convertFileState(const fb::FileState* fb_fs) +{ + if (fb_fs == nullptr) + { + return std::nullopt; + } + SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE( + fb_fs->file_path(), "FileState::file_path must never be nullptr as it is required in the schema"); + + return FileState{ + fb_fs->file_path()->str(), + convertFileExistenceState(fb_fs->state()), + std::chrono::milliseconds{fb_fs->polling_interval()}}; +} + +std::optional convertReadyCondition(const fb::ReadyCondition* fb_rc) { - ReadyCondition result{}; - if (fb_rc != nullptr) + if (fb_rc == nullptr) + { + return std::nullopt; + } + + const bool has_process_state = fb_rc->process_state().has_value(); + const bool has_file_state = fb_rc->file_state() != nullptr; + + if (has_process_state && has_file_state) + { + LM_LOG_ERROR() << "ReadyCondition cannot have both process_state and file_state set"; + return std::nullopt; + } + + if (!has_process_state && !has_file_state) + { + LM_LOG_ERROR() << "ReadyCondition must have either process_state or file_state set"; + return std::nullopt; + } + + if (has_process_state) + { + return convertProcessState(*fb_rc->process_state()); + } + else { - auto process_state = requireScalarValue(fb_rc->process_state(), "ReadyCondition::process_state"); - if (!process_state.has_value()) + auto file_state = convertFileState(fb_rc->file_state()); + if (!file_state.has_value()) { - return score::cpp::make_unexpected(process_state.error()); + LM_LOG_ERROR() << "FileState conversion failed"; + return std::nullopt; } - result.process_state = convertProcessState(*process_state); + return *file_state; } - return result; } score::cpp::expected convertComponentProperties( @@ -339,12 +389,7 @@ score::cpp::expected convertComponent result.process_arguments = convertStringVector(fb_cp->process_arguments()); if (fb_cp->ready_condition() != nullptr) { - auto ready_cond = convertReadyCondition(fb_cp->ready_condition()); - if (!ready_cond.has_value()) - { - return score::cpp::make_unexpected(ready_cond.error()); - } - result.ready_condition = std::move(*ready_cond); + result.ready_condition = convertReadyCondition(fb_cp->ready_condition()); } } return result; diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.hpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.hpp index e22431ec7..67d836c81 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.hpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.hpp @@ -69,6 +69,10 @@ score::cpp::expected validateRange(int64_t value, [[nodiscard]] ApplicationType convertApplicationType(fb::ApplicationType fb_type); /// @brief Converts a FlatBuffer ProcessState enum to the config ProcessState. [[nodiscard]] ProcessState convertProcessState(fb::ProcessState fb_state); +/// @brief Converts a FlatBuffer FileState struct to the config equivalent. +std::optional convertFileState(const fb::FileState* fb_fs); +/// @brief Converts a FlatBuffer FileExistenceState enum to the config equivalent. +[[nodiscard]] FileExistenceState convertFileExistenceState(fb::FileExistenceState fb_state); /// @brief Converts a FlatBuffer SchedulingPolicy enum to a POSIX scheduling policy constant. [[nodiscard]] score::cpp::expected convertSchedulingPolicy(fb::SchedulingPolicy policy); @@ -105,8 +109,7 @@ score::cpp::expected validateRange(int64_t value, [[nodiscard]] score::cpp::expected convertApplicationProfile( const fb::ApplicationProfile* fb_ap); /// @brief Converts a FlatBuffer ReadyCondition to the config equivalent. -[[nodiscard]] score::cpp::expected convertReadyCondition( - const fb::ReadyCondition* fb_rc); +[[nodiscard]] std::optional convertReadyCondition(const fb::ReadyCondition* fb_rc); /// @brief Converts a FlatBuffer ComponentProperties to the config equivalent. [[nodiscard]] score::cpp::expected convertComponentProperties( const fb::ComponentProperties* fb_cp); diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp index 0fb421246..b50914bf9 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp @@ -562,7 +562,14 @@ TEST_F(ConverterTest, ConvertApplicationProfileMissingSelfTerminatingReturnsErro EXPECT_THAT(result.error(), Eq(IConfigLoader::Error::InvalidFormat)); } -TEST_F(ConverterTest, ConvertReadyConditionValid) +TEST_F(ConverterTest, ConvertReadyConditionNullReturnsNullopt) +{ + RecordProperty("Description", "convertReadyCondition with nullptr returns nullopt."); + auto result = details::convertReadyCondition(nullptr); + EXPECT_THAT(result.has_value(), IsFalse()); +} + +TEST_F(ConverterTest, ConvertReadyConditionWithProcessState) { RecordProperty("Description", "convertReadyCondition maps process_state correctly."); ::flatbuffers::FlatBufferBuilder fbb; @@ -572,12 +579,40 @@ TEST_F(ConverterTest, ConvertReadyConditionValid) auto result = details::convertReadyCondition(ptr); ASSERT_THAT(result.has_value(), IsTrue()); - EXPECT_THAT(result->process_state, Eq(ProcessState::Terminated)); + EXPECT_THAT(*result, ::testing::VariantWith(ProcessState::Terminated)); } -TEST_F(ConverterTest, ConvertReadyConditionMissingProcessStateReturnsError) +TEST_F(ConverterTest, ConvertReadyConditionWithFileState) { - RecordProperty("Description", "Missing process_state returns InvalidFormat."); + RecordProperty("Description", "convertReadyCondition maps file_state correctly."); + ::flatbuffers::FlatBufferBuilder fbb; + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists); + auto rc = fb::CreateReadyCondition(fbb, ::flatbuffers::nullopt /*process_state*/, fs); + fbb.Finish(rc); + const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); + + auto result = details::convertReadyCondition(ptr); + ASSERT_THAT(result.has_value(), IsTrue()); + EXPECT_THAT(*result, ::testing::VariantWith(::testing::Field(&FileState::file_path, Eq("/tmp/ready")))); +} + +TEST_F(ConverterTest, ConvertReadyConditionWithBothStatesReturnsNullopt) +{ + RecordProperty("Description", "convertReadyCondition with both process_state and file_state returns nullopt."); + ::flatbuffers::FlatBufferBuilder fbb; + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists); + auto rc = fb::CreateReadyCondition(fbb, fb::ProcessState::Running, fs); + fbb.Finish(rc); + const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); + + auto result = details::convertReadyCondition(ptr); + ASSERT_THAT(result.has_value(), IsFalse()); + EXPECT_EQ(result, std::nullopt); +} + +TEST_F(ConverterTest, ConvertReadyConditionWithNeitherStateReturnsNullopt) +{ + RecordProperty("Description", "convertReadyCondition with neither process_state nor file_state returns nullopt."); ::flatbuffers::FlatBufferBuilder fbb; auto rc = fb::CreateReadyCondition(fbb); fbb.Finish(rc); @@ -585,7 +620,57 @@ TEST_F(ConverterTest, ConvertReadyConditionMissingProcessStateReturnsError) auto result = details::convertReadyCondition(ptr); ASSERT_THAT(result.has_value(), IsFalse()); - EXPECT_THAT(result.error(), Eq(IConfigLoader::Error::InvalidFormat)); + EXPECT_EQ(result, std::nullopt); +} + +TEST_F(ConverterTest, ConvertFileExistenceStateMapsDeath) +{ + RecordProperty("Description", "convertFileExistenceState Fires an assertion if an undefined enum is given."); + EXPECT_DEATH( + static_cast(details::convertFileExistenceState( + static_cast(static_cast(fb::FileExistenceState::MAX) + 1))), + ".*"); +} + +TEST_F(ConverterTest, ConvertFileExistenceStateMapsBothValues) +{ + RecordProperty("Description", "convertFileExistenceState maps both enum values correctly."); + EXPECT_THAT(details::convertFileExistenceState(fb::FileExistenceState::Exists), Eq(FileExistenceState::Exists)); + EXPECT_THAT(details::convertFileExistenceState(fb::FileExistenceState::Deleted), Eq(FileExistenceState::Deleted)); +} + +TEST_F(ConverterTest, ConvertFileStateNullReturnsNullopt) +{ + RecordProperty("Description", "convertFileState returns nullopt when passed nullptr."); + auto result = details::convertFileState(nullptr); + EXPECT_THAT(result.has_value(), IsFalse()); +} + +TEST_F(ConverterTest, ConvertFileStateValid) +{ + RecordProperty("Description", "convertFileState maps file_path and an explicit state correctly."); + ::flatbuffers::FlatBufferBuilder fbb; + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Deleted); + fbb.Finish(fs); + const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); + + auto result = details::convertFileState(ptr); + ASSERT_THAT(result.has_value(), IsTrue()); + EXPECT_THAT(result->file_path, Eq("/tmp/ready")); + EXPECT_THAT(result->state, Eq(FileExistenceState::Deleted)); +} + +TEST_F(ConverterTest, ConvertFileStateDefaultsToExists) +{ + RecordProperty("Description", "convertFileState defaults state to Exists when not specified."); + ::flatbuffers::FlatBufferBuilder fbb; + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready"); + fbb.Finish(fs); + const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); + + auto result = details::convertFileState(ptr); + ASSERT_THAT(result.has_value(), IsTrue()); + EXPECT_THAT(result->state, Eq(FileExistenceState::Exists)); } TEST_F(ConverterTest, ConvertSandboxValid) diff --git a/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs b/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs index 83529b05c..76cdf9229 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs +++ b/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs @@ -26,6 +26,12 @@ enum ProcessState : byte { Terminated = 1 } +// Specifies the required existence state of a watched file. +enum FileExistenceState : byte { + Exists = 0, + Deleted = 1 +} + // Scheduling policy for a component's initial thread. enum SchedulingPolicy : byte { OTHER = 0, @@ -53,9 +59,23 @@ table ApplicationProfile { alive_supervision:ComponentAliveSupervision; // optional } +// Defines a ready condition based on the existence state of a file at a given path. +table FileState { + // Absolute path to the file being watched. + file_path:string (required); // required + // Existence state of the file. Defaults to Exists if not specified. + state:FileExistenceState = Exists; // optional, defaults to Exists + // Time in ms to wait between each poll if the file is present. + polling_interval: uint32 = 10; //optional, defaults to 10ms +} + // Defines the conditions that determine when the component enters the ready state. +// Either process_state or file_state should be set, but not both. table ReadyCondition { - process_state:ProcessState = null; // required + // Required state of the component's POSIX process. + process_state:ProcessState = null; // optional + // File existence state condition. + file_state:FileState; // optional } // Defines essential characteristics of a software component. From fa0646dff296c14341e846436757a6042251bc4d Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Thu, 13 Aug 2026 14:06:37 +0100 Subject: [PATCH 2/5] Using seconds --- .../docs/user_guide/configuration.rst | 10 ++++---- .../src/daemon/src/configuration/config.hpp | 2 +- .../config_schema/launch_manager.schema.json | 25 +++++++++++++------ .../details/flatbuffer_type_converters.cpp | 13 +++++----- .../details/flatbuffer_type_converters_UT.cpp | 7 +++--- .../src/configuration/details/lm_flatcfg.fbs | 6 ++--- 6 files changed, 38 insertions(+), 25 deletions(-) diff --git a/score/launch_manager/docs/user_guide/configuration.rst b/score/launch_manager/docs/user_guide/configuration.rst index 1678691a9..1aadffcf6 100644 --- a/score/launch_manager/docs/user_guide/configuration.rst +++ b/score/launch_manager/docs/user_guide/configuration.rst @@ -209,7 +209,7 @@ component_properties (object) * ``"Running"``: The process has started and reached its running state. * ``"Terminated"``: The process has started, reached its running state, and then terminated successfully. * **file_state** (object, optional) - * **Description:** Specifies a ready condition based on the existence state of a file at a given path. + * **Description:** Specifies a ready condition based on the existence of a file at a given path. * **Properties:** * **file_path** (string, required) * **Description:** Specifies the absolute path to the file being watched. @@ -217,12 +217,12 @@ component_properties (object) * **Description:** Specifies the required existence state of the file. * **Allowed Values:** * ``"Exists"``: The component is ready when the file at ``file_path`` exists. - * ``"Deleted"``: The component is ready when the file at ``file_path`` is deleted. + * ``"NotExisting"``: The component is ready when the file at ``file_path`` does not exist. * **Default:** ``"Exists"`` - * **polling_interval** (integer, optional) - * **Description:** Specifies the time interval, in milliseconds, at which the **Launch Manager** checks the file existence state. + * **polling_interval** (number, optional) + * **Description:** Specifies the time interval, in seconds (e.g., ``0.3`` for 300 milliseconds), at which the **Launch Manager** checks the file existence state. * **Constraint:** Must be greater than 0. - * **Default:** ``10`` + * **Default:** ``0.01`` .. _lm_conf_deployment_config_object_: diff --git a/score/launch_manager/src/daemon/src/configuration/config.hpp b/score/launch_manager/src/daemon/src/configuration/config.hpp index b4a2c09d0..a100daec4 100644 --- a/score/launch_manager/src/daemon/src/configuration/config.hpp +++ b/score/launch_manager/src/daemon/src/configuration/config.hpp @@ -42,7 +42,7 @@ enum class ProcessState : uint8_t enum class FileExistenceState : uint8_t { Exists = 0, - Deleted, + NotExisting, }; struct ComponentAliveSupervision diff --git a/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json b/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json index 990a9411d..98b598d8f 100644 --- a/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json +++ b/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json @@ -92,25 +92,25 @@ }, "file_state": { "type": "object", - "description": "Specifies a ready condition based on the existence state of a file at a given path.", + "description": "Specifies a ready condition based on the existence of a file at a given path.", "properties": { "file_path": { "type": "string", - "pattern": "^/.*", + "pattern": "^/(?:[^/]+(?:/[^/]+)*)$", "description": "Specifies the absolute path to the file being watched." }, "state": { "type": "string", "enum": [ "Exists", - "Deleted" + "NotExisting" ], - "description": "Specifies the required existence state of the file. 'Exists': the file must be present at 'file_path'. 'Deleted': the file must be absent from 'file_path'. Defaults to 'Exists' if not specified." + "description": "Specifies the required existence of the file. 'Exists': the file must be present at 'file_path'. 'NotExisting': the file must be absent from 'file_path'. Defaults to 'Exists' if not specified." }, "polling_interval": { - "type": "integer", + "type": "number", "exclusiveMinimum": 0, - "description": "Specifies the time interval, in milliseconds, at which the Launch Manager checks the file existence state." + "description": "Specifies the time interval, in seconds (e.g., '0.3' for 300 milliseconds), at which the Launch Manager checks the file existence. Defaults to 10 milliseconds." } }, "required": [ @@ -119,7 +119,18 @@ "additionalProperties": false } }, - "required": [], + "oneOf": [ + { + "required": [ + "process_state" + ] + }, + { + "required": [ + "file_state" + ] + } + ], "additionalProperties": false } }, diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp index 20e0bdfdd..05f9cdaa7 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp @@ -112,8 +112,8 @@ FileExistenceState convertFileExistenceState(fb::FileExistenceState fb_state) { switch (fb_state) { - case fb::FileExistenceState::Deleted: - return FileExistenceState::Deleted; + case fb::FileExistenceState::NotExisting: + return FileExistenceState::NotExisting; case fb::FileExistenceState::Exists: return FileExistenceState::Exists; } @@ -322,10 +322,11 @@ std::optional convertFileState(const fb::FileState* fb_fs) SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE( fb_fs->file_path(), "FileState::file_path must never be nullptr as it is required in the schema"); - return FileState{ - fb_fs->file_path()->str(), - convertFileExistenceState(fb_fs->state()), - std::chrono::milliseconds{fb_fs->polling_interval()}}; + const auto polling_interval_seconds = fb_fs->polling_interval(); + const auto polling_interval_ms = + std::chrono::duration_cast(std::chrono::duration(polling_interval_seconds)); + + return FileState{fb_fs->file_path()->str(), convertFileExistenceState(fb_fs->state()), polling_interval_ms}; } std::optional convertReadyCondition(const fb::ReadyCondition* fb_rc) diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp index b50914bf9..ac43e84d9 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp @@ -636,7 +636,8 @@ TEST_F(ConverterTest, ConvertFileExistenceStateMapsBothValues) { RecordProperty("Description", "convertFileExistenceState maps both enum values correctly."); EXPECT_THAT(details::convertFileExistenceState(fb::FileExistenceState::Exists), Eq(FileExistenceState::Exists)); - EXPECT_THAT(details::convertFileExistenceState(fb::FileExistenceState::Deleted), Eq(FileExistenceState::Deleted)); + EXPECT_THAT( + details::convertFileExistenceState(fb::FileExistenceState::NotExisting), Eq(FileExistenceState::NotExisting)); } TEST_F(ConverterTest, ConvertFileStateNullReturnsNullopt) @@ -650,14 +651,14 @@ TEST_F(ConverterTest, ConvertFileStateValid) { RecordProperty("Description", "convertFileState maps file_path and an explicit state correctly."); ::flatbuffers::FlatBufferBuilder fbb; - auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Deleted); + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::NotExisting); fbb.Finish(fs); const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); auto result = details::convertFileState(ptr); ASSERT_THAT(result.has_value(), IsTrue()); EXPECT_THAT(result->file_path, Eq("/tmp/ready")); - EXPECT_THAT(result->state, Eq(FileExistenceState::Deleted)); + EXPECT_THAT(result->state, Eq(FileExistenceState::NotExisting)); } TEST_F(ConverterTest, ConvertFileStateDefaultsToExists) diff --git a/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs b/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs index 76cdf9229..c5c636638 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs +++ b/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs @@ -29,7 +29,7 @@ enum ProcessState : byte { // Specifies the required existence state of a watched file. enum FileExistenceState : byte { Exists = 0, - Deleted = 1 + NotExisting = 1 } // Scheduling policy for a component's initial thread. @@ -65,8 +65,8 @@ table FileState { file_path:string (required); // required // Existence state of the file. Defaults to Exists if not specified. state:FileExistenceState = Exists; // optional, defaults to Exists - // Time in ms to wait between each poll if the file is present. - polling_interval: uint32 = 10; //optional, defaults to 10ms + // Time in seconds to wait between each poll if the file is present. + polling_interval: double = 0.01; //optional, defaults to 0.01s (10ms) } // Defines the conditions that determine when the component enters the ready state. From f69469af2fa4d26e5da92e07ad7371c55c0ce5c9 Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Tue, 18 Aug 2026 09:20:14 +0100 Subject: [PATCH 3/5] Making ReadyConditions non optional --- .../src/daemon/src/configuration/config.hpp | 2 +- .../configuration/configuration_adapter.cpp | 38 ++++---- .../configuration_adapter_UT.cpp | 4 +- .../details/flatbuffer_config_loader_UT.cpp | 13 +-- .../details/flatbuffer_type_converters.cpp | 65 +++++++------ .../details/flatbuffer_type_converters.hpp | 9 +- .../details/flatbuffer_type_converters_UT.cpp | 81 +++++++++++----- scripts/config_mapping/lifecycle_config.py | 47 ++++++++- scripts/config_mapping/unit_tests.py | 97 ++++++++++++++++++- 9 files changed, 267 insertions(+), 89 deletions(-) diff --git a/score/launch_manager/src/daemon/src/configuration/config.hpp b/score/launch_manager/src/daemon/src/configuration/config.hpp index a100daec4..8bb829a56 100644 --- a/score/launch_manager/src/daemon/src/configuration/config.hpp +++ b/score/launch_manager/src/daemon/src/configuration/config.hpp @@ -75,7 +75,7 @@ struct ComponentProperties ApplicationProfile application_profile; std::vector depends_on; std::vector process_arguments; - std::optional ready_condition; + ReadyCondition ready_condition; }; /// @brief A single environment variable diff --git a/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp b/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp index 6688e4698..1f779285d 100644 --- a/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp +++ b/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp @@ -211,27 +211,23 @@ DependencyList ConfigurationAdapter::buildDependencyList(const ComponentProperti Dependency dep{}; dep.process_state_ = score::mw::lifecycle::ProcessState::kRunning; - if (dep_props.ready_condition.has_value()) - { - std::visit( - [&dep](auto&& arg) { - using argT = std::decay_t; - - if constexpr (std::is_same_v) - { - dep.process_state_ = arg == ProcessState::Running - ? score::mw::lifecycle::ProcessState::kRunning - : score::mw::lifecycle::ProcessState::kTerminated; - return; - } - else if constexpr (std::is_same_v) - { - SCORE_LANGUAGE_FUTURECPP_UNREACHABLE_MESSAGE("FileState is not yet supported"); - return; - } - }, - dep_props.ready_condition.value()); - } + std::visit( + [&dep](auto&& arg) { + using argT = std::decay_t; + + if constexpr (std::is_same_v) + { + dep.process_state_ = arg == ProcessState::Running ? score::mw::lifecycle::ProcessState::kRunning + : score::mw::lifecycle::ProcessState::kTerminated; + return; + } + else if constexpr (std::is_same_v) + { + SCORE_LANGUAGE_FUTURECPP_UNREACHABLE_MESSAGE("FileState is not yet supported"); + return; + } + }, + dep_props.ready_condition); dep.target_process_id_ = IdentifierHash{dep_name}; dependencies.push_back(dep); diff --git a/score/launch_manager/src/daemon/src/configuration/configuration_adapter_UT.cpp b/score/launch_manager/src/daemon/src/configuration/configuration_adapter_UT.cpp index b12a52ffc..514365684 100644 --- a/score/launch_manager/src/daemon/src/configuration/configuration_adapter_UT.cpp +++ b/score/launch_manager/src/daemon/src/configuration/configuration_adapter_UT.cpp @@ -571,9 +571,9 @@ TEST(ConfigurationAdapterFallbackTest, FallbackRunTargetResolvesDependenciesRecu adapter.deinitialize(); } -TEST(ConfigurationAdapterDependencyTest, DependencyOnNonExistentComponentIsIgnored) +TEST(ConfigurationAdapterDependencyTest, DependencyOnNonExistentComponentAborts) { - RecordProperty("Description", "When a component depends on a non-existent component, the dependency is skipped."); + RecordProperty("Description", "When a component depends on a non-existent component, the program aborts."); RecordProperty("TestType", "interface-test"); RecordProperty("DerivationTechnique", "explorative-testing"); diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp index 63398ae09..7b448c370 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp @@ -259,8 +259,7 @@ TEST_F(FlatbufferConfigLoaderTest, LoadSingleComponent) EXPECT_THAT(comp.component_properties.depends_on[0], Eq("other_comp")); ASSERT_THAT(comp.component_properties.process_arguments.size(), Eq(1U)); EXPECT_THAT(comp.component_properties.process_arguments[0], Eq("--verbose")); - ASSERT_THAT(comp.component_properties.ready_condition.has_value(), IsTrue()); - EXPECT_THAT(*comp.component_properties.ready_condition, VariantWith(Eq(ProcessState::Running))); + EXPECT_THAT(comp.component_properties.ready_condition, VariantWith(Eq(ProcessState::Running))); EXPECT_THAT(comp.deployment_config.ready_timeout_ms, Eq(1500U)); EXPECT_THAT(comp.deployment_config.shutdown_timeout_ms, Eq(2500U)); EXPECT_THAT(comp.deployment_config.bin_dir, Eq("/opt/bin")); @@ -305,9 +304,8 @@ TEST_F(FlatbufferConfigLoaderTest, LoadSingleComponentWithFileState) ASSERT_THAT(result->components().size(), Eq(1U)); const auto& comp = result->components()[0]; - ASSERT_THAT(comp.component_properties.ready_condition.has_value(), IsTrue()); EXPECT_THAT( - *comp.component_properties.ready_condition, + comp.component_properties.ready_condition, VariantWith( FieldsAre(Eq("/tmp/ready"), Eq(FileExistenceState::Exists), Eq(std::chrono::milliseconds{10})))); } @@ -642,7 +640,8 @@ TEST_F(FlatbufferConfigLoaderTest, OptionalWatchdogAbsent) TEST_F(FlatbufferConfigLoaderTest, OptionalReadyConditionAbsent) { - RecordProperty("Description", "When no ready_condition is present on a component, it is nullopt."); + RecordProperty( + "Description", "When no ready_condition is present on a component, it defaults to ProcessState::Running."); ::flatbuffers::FlatBufferBuilder fbb; @@ -656,7 +655,9 @@ TEST_F(FlatbufferConfigLoaderTest, OptionalReadyConditionAbsent) auto result = loadBuffer(buildConfigWithComponents(fbb, comps)); ASSERT_THAT(result.has_value(), IsTrue()); - EXPECT_THAT(result->components()[0].component_properties.ready_condition.has_value(), IsFalse()); + EXPECT_THAT( + result->components()[0].component_properties.ready_condition, + VariantWith(Eq(ProcessState::Running))); } // ============================================================================ diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp index 05f9cdaa7..17e15a8a3 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp @@ -313,28 +313,27 @@ score::cpp::expected convertApplicatio return result; } -std::optional convertFileState(const fb::FileState* fb_fs) +score::cpp::expected convertFileState(const fb::FileState& fb_fs) { - if (fb_fs == nullptr) - { - return std::nullopt; - } SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE( - fb_fs->file_path(), "FileState::file_path must never be nullptr as it is required in the schema"); + fb_fs.file_path(), "FileState::file_path must never be nullptr as it is required in the schema"); - const auto polling_interval_seconds = fb_fs->polling_interval(); - const auto polling_interval_ms = - std::chrono::duration_cast(std::chrono::duration(polling_interval_seconds)); - - return FileState{fb_fs->file_path()->str(), convertFileExistenceState(fb_fs->state()), polling_interval_ms}; + auto polling_interval_ms = secondsToMs(fb_fs.polling_interval()); + if (!polling_interval_ms.has_value()) + { + LM_LOG_ERROR() << "Invalid value for FileState::polling_interval"; + return score::cpp::make_unexpected(polling_interval_ms.error()); + } + return FileState{ + fb_fs.file_path()->str(), + convertFileExistenceState(fb_fs.state()), + std::chrono::milliseconds{*polling_interval_ms}}; } -std::optional convertReadyCondition(const fb::ReadyCondition* fb_rc) +score::cpp::expected convertReadyCondition(const fb::ReadyCondition* fb_rc) { - if (fb_rc == nullptr) - { - return std::nullopt; - } + SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE( + fb_rc != nullptr, "No ReadyCondition is confitured, this should have been defaulted with the script."); const bool has_process_state = fb_rc->process_state().has_value(); const bool has_file_state = fb_rc->file_state() != nullptr; @@ -342,29 +341,28 @@ std::optional convertReadyCondition(const fb::ReadyCondition* fb if (has_process_state && has_file_state) { LM_LOG_ERROR() << "ReadyCondition cannot have both process_state and file_state set"; - return std::nullopt; - } - - if (!has_process_state && !has_file_state) - { - LM_LOG_ERROR() << "ReadyCondition must have either process_state or file_state set"; - return std::nullopt; + return score::cpp::make_unexpected(IConfigLoader::Error::InvalidFormat); } if (has_process_state) { - return convertProcessState(*fb_rc->process_state()); + return ReadyCondition{convertProcessState(*fb_rc->process_state())}; } - else + + if (has_file_state) { - auto file_state = convertFileState(fb_rc->file_state()); + auto file_state = convertFileState(*(fb_rc->file_state())); if (!file_state.has_value()) { - LM_LOG_ERROR() << "FileState conversion failed"; - return std::nullopt; + LM_LOG_ERROR() << "Invalid value for ReadyCondition::file_state"; + return score::cpp::make_unexpected(file_state.error()); } - return *file_state; - } + + // convertFileState only returns nullopt for a nullptr input, which is ruled out above + return ReadyCondition{*file_state}; + }; + + SCORE_LANGUAGE_FUTURECPP_UNREACHABLE(); } score::cpp::expected convertComponentProperties( @@ -390,7 +388,12 @@ score::cpp::expected convertComponent result.process_arguments = convertStringVector(fb_cp->process_arguments()); if (fb_cp->ready_condition() != nullptr) { - result.ready_condition = convertReadyCondition(fb_cp->ready_condition()); + auto ready_condition = convertReadyCondition(fb_cp->ready_condition()); + if (!ready_condition.has_value()) + { + return score::cpp::make_unexpected(ready_condition.error()); + } + result.ready_condition = std::move(ready_condition.value()); } } return result; diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.hpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.hpp index 67d836c81..b24cacd1c 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.hpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.hpp @@ -69,8 +69,8 @@ score::cpp::expected validateRange(int64_t value, [[nodiscard]] ApplicationType convertApplicationType(fb::ApplicationType fb_type); /// @brief Converts a FlatBuffer ProcessState enum to the config ProcessState. [[nodiscard]] ProcessState convertProcessState(fb::ProcessState fb_state); -/// @brief Converts a FlatBuffer FileState struct to the config equivalent. -std::optional convertFileState(const fb::FileState* fb_fs); +/// @brief Converts a FlatBuffer FileState table to the config equivalent, or nullopt if absent. +[[nodiscard]] score::cpp::expected convertFileState(const fb::FileState& fb_fs); /// @brief Converts a FlatBuffer FileExistenceState enum to the config equivalent. [[nodiscard]] FileExistenceState convertFileExistenceState(fb::FileExistenceState fb_state); /// @brief Converts a FlatBuffer SchedulingPolicy enum to a POSIX scheduling policy constant. @@ -108,8 +108,9 @@ std::optional convertFileState(const fb::FileState* fb_fs); /// @brief Converts a FlatBuffer ApplicationProfile to the config equivalent. [[nodiscard]] score::cpp::expected convertApplicationProfile( const fb::ApplicationProfile* fb_ap); -/// @brief Converts a FlatBuffer ReadyCondition to the config equivalent. -[[nodiscard]] std::optional convertReadyCondition(const fb::ReadyCondition* fb_rc); +/// @brief Converts a FlatBuffer ReadyCondition to the config equivalent, or nullopt if not configured. +[[nodiscard]] score::cpp::expected convertReadyCondition( + const fb::ReadyCondition* fb_rc); /// @brief Converts a FlatBuffer ComponentProperties to the config equivalent. [[nodiscard]] score::cpp::expected convertComponentProperties( const fb::ComponentProperties* fb_cp); diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp index ac43e84d9..464e52c02 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp @@ -562,11 +562,10 @@ TEST_F(ConverterTest, ConvertApplicationProfileMissingSelfTerminatingReturnsErro EXPECT_THAT(result.error(), Eq(IConfigLoader::Error::InvalidFormat)); } -TEST_F(ConverterTest, ConvertReadyConditionNullReturnsNullopt) +TEST_F(ConverterTest, ConvertReadyConditionNullDeath) { - RecordProperty("Description", "convertReadyCondition with nullptr returns nullopt."); - auto result = details::convertReadyCondition(nullptr); - EXPECT_THAT(result.has_value(), IsFalse()); + RecordProperty("Description", "convertReadyCondition fires an assertion when passed nullptr."); + EXPECT_DEATH(static_cast(details::convertReadyCondition(nullptr)), ".*"); } TEST_F(ConverterTest, ConvertReadyConditionWithProcessState) @@ -596,9 +595,10 @@ TEST_F(ConverterTest, ConvertReadyConditionWithFileState) EXPECT_THAT(*result, ::testing::VariantWith(::testing::Field(&FileState::file_path, Eq("/tmp/ready")))); } -TEST_F(ConverterTest, ConvertReadyConditionWithBothStatesReturnsNullopt) +TEST_F(ConverterTest, ConvertReadyConditionWithBothStatesReturnsError) { - RecordProperty("Description", "convertReadyCondition with both process_state and file_state returns nullopt."); + RecordProperty( + "Description", "convertReadyCondition with both process_state and file_state returns InvalidFormat."); ::flatbuffers::FlatBufferBuilder fbb; auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists); auto rc = fb::CreateReadyCondition(fbb, fb::ProcessState::Running, fs); @@ -607,20 +607,35 @@ TEST_F(ConverterTest, ConvertReadyConditionWithBothStatesReturnsNullopt) auto result = details::convertReadyCondition(ptr); ASSERT_THAT(result.has_value(), IsFalse()); - EXPECT_EQ(result, std::nullopt); + EXPECT_THAT(result.error(), Eq(IConfigLoader::Error::InvalidFormat)); } -TEST_F(ConverterTest, ConvertReadyConditionWithNeitherStateReturnsNullopt) +TEST_F(ConverterTest, ConvertReadyConditionWithNeitherStateDeath) { - RecordProperty("Description", "convertReadyCondition with neither process_state nor file_state returns nullopt."); + RecordProperty( + "Description", + "convertReadyCondition fires an assertion if neither process_state nor file_state is configured, as the " + "configuration script always defaults one of them."); ::flatbuffers::FlatBufferBuilder fbb; auto rc = fb::CreateReadyCondition(fbb); fbb.Finish(rc); const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); + EXPECT_DEATH(static_cast(details::convertReadyCondition(ptr)), ".*"); +} + +TEST_F(ConverterTest, ConvertReadyConditionWithInvalidPollingIntervalReturnsError) +{ + RecordProperty("Description", "convertReadyCondition propagates an invalid FileState::polling_interval."); + ::flatbuffers::FlatBufferBuilder fbb; + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists, -1.0 /*polling_interval*/); + auto rc = fb::CreateReadyCondition(fbb, ::flatbuffers::nullopt /*process_state*/, fs); + fbb.Finish(rc); + const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); + auto result = details::convertReadyCondition(ptr); ASSERT_THAT(result.has_value(), IsFalse()); - EXPECT_EQ(result, std::nullopt); + EXPECT_THAT(result.error(), Eq(IConfigLoader::Error::InvalidFormat)); } TEST_F(ConverterTest, ConvertFileExistenceStateMapsDeath) @@ -640,38 +655,60 @@ TEST_F(ConverterTest, ConvertFileExistenceStateMapsBothValues) details::convertFileExistenceState(fb::FileExistenceState::NotExisting), Eq(FileExistenceState::NotExisting)); } -TEST_F(ConverterTest, ConvertFileStateNullReturnsNullopt) -{ - RecordProperty("Description", "convertFileState returns nullopt when passed nullptr."); - auto result = details::convertFileState(nullptr); - EXPECT_THAT(result.has_value(), IsFalse()); -} - TEST_F(ConverterTest, ConvertFileStateValid) { - RecordProperty("Description", "convertFileState maps file_path and an explicit state correctly."); + RecordProperty("Description", "convertFileState maps file_path, an explicit state and polling_interval correctly."); ::flatbuffers::FlatBufferBuilder fbb; - auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::NotExisting); + auto fs = + fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::NotExisting, 0.3 /*polling_interval*/); fbb.Finish(fs); const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); - auto result = details::convertFileState(ptr); + auto result = details::convertFileState(*ptr); ASSERT_THAT(result.has_value(), IsTrue()); EXPECT_THAT(result->file_path, Eq("/tmp/ready")); EXPECT_THAT(result->state, Eq(FileExistenceState::NotExisting)); + EXPECT_THAT(result->polling_interval, Eq(std::chrono::milliseconds{300})); } TEST_F(ConverterTest, ConvertFileStateDefaultsToExists) { - RecordProperty("Description", "convertFileState defaults state to Exists when not specified."); + RecordProperty("Description", "convertFileState defaults state to Exists and polling_interval to 10ms."); ::flatbuffers::FlatBufferBuilder fbb; auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready"); fbb.Finish(fs); const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); - auto result = details::convertFileState(ptr); + auto result = details::convertFileState(*ptr); ASSERT_THAT(result.has_value(), IsTrue()); EXPECT_THAT(result->state, Eq(FileExistenceState::Exists)); + EXPECT_THAT(result->polling_interval, Eq(std::chrono::milliseconds{10})); +} + +TEST_F(ConverterTest, ConvertFileStateNegativePollingIntervalReturnsError) +{ + RecordProperty("Description", "convertFileState returns InvalidFormat for a negative polling_interval."); + ::flatbuffers::FlatBufferBuilder fbb; + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists, -0.5 /*polling_interval*/); + fbb.Finish(fs); + const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); + + auto result = details::convertFileState(*ptr); + ASSERT_THAT(result.has_value(), IsFalse()); + EXPECT_THAT(result.error(), Eq(IConfigLoader::Error::InvalidFormat)); +} + +TEST_F(ConverterTest, ConvertFileStateSubMillisecondPollingIntervalReturnsError) +{ + RecordProperty("Description", "convertFileState returns InvalidFormat for a sub-millisecond polling_interval."); + ::flatbuffers::FlatBufferBuilder fbb; + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists, 0.0001 /*polling_interval*/); + fbb.Finish(fs); + const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); + + auto result = details::convertFileState(*ptr); + ASSERT_THAT(result.has_value(), IsFalse()); + EXPECT_THAT(result.error(), Eq(IConfigLoader::Error::InvalidFormat)); } TEST_F(ConverterTest, ConvertSandboxValid) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index ba1416c08..1fd70d496 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -90,7 +90,16 @@ def report_error(message): # There are various dictionaries in the config where only a single entry is allowed. # We do not want to merge the defaults with the user specified values for these dictionaries. -not_merging_dicts = ["ready_recovery_action", "recovery_action"] +not_merging_dicts = ["ready_recovery_action", "recovery_action", "ready_condition"] + + +# Defaults for the optional fields of a "file_state" ready condition. +# note: these are only default when a file_state is configured so it's no in +# the main default config. +file_state_defaults: Dict[str, Any] = { + "state": "Exists", + "polling_interval": 0.01, +} def load_json_file(file_path: str) -> Dict[str, Any]: @@ -106,6 +115,21 @@ def get_working_dir(deployment_config): return deployment_config.get("working_dir", deployment_config["bin_dir"]) +def apply_file_state_defaults(ready_condition): + """Fill in the optional fields of a "file_state" ready condition with + their defaults. Only done if a "file_state" ready condition is configured. + """ + file_state = ready_condition.get("file_state") + is_configured = isinstance(file_state, dict) + if not is_configured: + return + + # user config takes precedence over the defaults + merged = dict(file_state_defaults) + merged.update(file_state) + ready_condition["file_state"] = {**merged} + + def preprocess_defaults(global_defaults, config): """ This function takes the input configuration and fills in any missing fields with default values. @@ -169,6 +193,12 @@ def dict_merge_recursive(dict_a, dict_b): component_config.get("deployment_config", {}), ) + apply_file_state_defaults( + new_config["components"][component_name]["component_properties"].get( + "ready_condition", {} + ) + ) + # If the application_type is not supervised, remove alive_supervision # from component_properties even if it was merged from defaults. app_type = new_config["components"][component_name]["component_properties"][ @@ -507,6 +537,21 @@ def custom_validations(config): ) success = False + # A ready condition is either process state or file state (right now), but + # never on both. + for component_name, component_config in config["components"].items(): + ready_condition = component_config["component_properties"].get( + "ready_condition", {} + ) + has_process_state = "process_state" in ready_condition + has_file_state = "file_state" in ready_condition + if has_process_state and has_file_state: + report_error( + f"Component '{component_name}': ready_condition must configure either " + '"process_state" or "file_state", but not both.' + ) + success = False + if "fallback_run_target" in config["run_targets"]: report_error( 'RunTarget name "fallback_run_target" is reserved, please choose a different name.' diff --git a/scripts/config_mapping/unit_tests.py b/scripts/config_mapping/unit_tests.py index 67ff1c97c..46161da2d 100644 --- a/scripts/config_mapping/unit_tests.py +++ b/scripts/config_mapping/unit_tests.py @@ -450,6 +450,81 @@ def test_preprocessing_no_defaults_section(): assert result["components"]["c1"]["deployment_config"]["bin_dir"] == "/opt" +def _config_with_file_state(file_state): + return { + "schema_version": 1, + "components": { + "c1": { + "component_properties": { + "binary_name": "c1", + "ready_condition": {"file_state": file_state}, + } + } + }, + "run_targets": {"Startup": {}}, + "initial_run_target": "Startup", + "fallback_run_target": {"transition_timeout": 1}, + } + + +def test_preprocessing_file_state_defaults(): + """ + A file_state ready condition only requires a file_path, state and + polling_interval are filled in with their defaults. + """ + config = _config_with_file_state({"file_path": "/tmp/ready"}) + result = preprocess_defaults(score_defaults, config) + ready_condition = result["components"]["c1"]["component_properties"][ + "ready_condition" + ] + assert ready_condition == { + "file_state": { + "file_path": "/tmp/ready", + "state": "Exists", + "polling_interval": 0.01, + } + } + + +def test_preprocessing_file_state_defaults_overridden(): + """ + User specified file_state values take precedence over the defaults. + """ + config = _config_with_file_state( + {"file_path": "/tmp/ready", "state": "NotExisting", "polling_interval": 0.5} + ) + result = preprocess_defaults(score_defaults, config) + file_state = result["components"]["c1"]["component_properties"]["ready_condition"][ + "file_state" + ] + assert file_state["state"] == "NotExisting" + assert file_state["polling_interval"] == 0.5 + + +def test_preprocessing_file_state_defaults_not_applied_for_process_state(): + """ + Without a file_state ready condition, no file_state defaults are added. + """ + config = { + "schema_version": 1, + "components": { + "c1": { + "component_properties": { + "binary_name": "c1", + "ready_condition": {"process_state": "Terminated"}, + } + } + }, + "run_targets": {"Startup": {}}, + "initial_run_target": "Startup", + "fallback_run_target": {"transition_timeout": 1}, + } + result = preprocess_defaults(score_defaults, config) + assert result["components"]["c1"]["component_properties"]["ready_condition"] == { + "process_state": "Terminated" + } + + # --------------------------------------------------------------------------- # check_cyclic_dependencies # --------------------------------------------------------------------------- @@ -592,7 +667,8 @@ def full_valid_config(): "components": { "app1": { "component_properties": { - "application_profile": {"application_type": "REPORTING"} + "application_profile": {"application_type": "REPORTING"}, + "ready_condition": {"process_state": "Running"}, } } }, @@ -635,6 +711,25 @@ def test_custom_validations_recovery_target_not_fallback(full_valid_config): assert custom_validations(full_valid_config) is False +def test_custom_validations_ready_condition_file_state(full_valid_config): + """A ready condition based on the file state alone is valid.""" + full_valid_config["components"]["app1"]["component_properties"][ + "ready_condition" + ] = {"file_state": {"file_path": "/tmp/ready"}} + assert custom_validations(full_valid_config) is True + + +def test_custom_validations_ready_condition_both_states(full_valid_config): + """process_state and file_state must not be configured at the same time.""" + full_valid_config["components"]["app1"]["component_properties"][ + "ready_condition" + ] = { + "process_state": "Running", + "file_state": {"file_path": "/tmp/ready"}, + } + assert custom_validations(full_valid_config) is False + + def test_custom_validations_missing_fallback_run_target(full_valid_config): """fallback_run_target is mandatory.""" del full_valid_config["fallback_run_target"] From 363517364e12d4ddfb5edb5fc6b42a631ec17e98 Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski <161459353+MaciejKaszynski@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:12:08 +0100 Subject: [PATCH 4/5] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nicolas Fußberger <145956508+NicolasFussberger@users.noreply.github.com> Signed-off-by: Maciej Kaszynski <161459353+MaciejKaszynski@users.noreply.github.com> --- .../src/configuration/details/flatbuffer_type_converters.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp index 17e15a8a3..50836d962 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp @@ -333,7 +333,7 @@ score::cpp::expected convertFileState(const fb: score::cpp::expected convertReadyCondition(const fb::ReadyCondition* fb_rc) { SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE( - fb_rc != nullptr, "No ReadyCondition is confitured, this should have been defaulted with the script."); + fb_rc != nullptr, "No ReadyCondition is configured, this should have been defaulted with the script."); const bool has_process_state = fb_rc->process_state().has_value(); const bool has_file_state = fb_rc->file_state() != nullptr; From 38139eb6e5c448429d7351be02c262aa00600f7f Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Wed, 19 Aug 2026 14:33:40 +0100 Subject: [PATCH 5/5] Addressing review comments --- .../src/daemon/src/configuration/config.hpp | 4 ++-- .../details/flatbuffer_config_loader_UT.cpp | 3 ++- .../details/flatbuffer_type_converters.cpp | 3 +++ .../details/flatbuffer_type_converters_UT.cpp | 21 ++++++++++++++++--- .../src/configuration/details/lm_flatcfg.fbs | 2 +- .../expected_output/lm_config_gen.json | 12 +++++++++-- .../full_config_test/input/lm_config.json | 16 ++++++++++++-- 7 files changed, 50 insertions(+), 11 deletions(-) diff --git a/score/launch_manager/src/daemon/src/configuration/config.hpp b/score/launch_manager/src/daemon/src/configuration/config.hpp index 8bb829a56..867e30079 100644 --- a/score/launch_manager/src/daemon/src/configuration/config.hpp +++ b/score/launch_manager/src/daemon/src/configuration/config.hpp @@ -63,8 +63,8 @@ struct ApplicationProfile struct FileState { std::string file_path; - FileExistenceState state{FileExistenceState::Exists}; - std::chrono::milliseconds polling_interval{10}; + FileExistenceState state; + std::chrono::milliseconds polling_interval; }; using ReadyCondition = std::variant; diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp index 7b448c370..5c9da752d 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp @@ -274,7 +274,8 @@ TEST_F(FlatbufferConfigLoaderTest, LoadSingleComponentWithFileState) auto app_profile = fb::CreateApplicationProfile(fbb, fb::ApplicationType::Native, false /*is_self_terminating*/); auto bin_name = fbb.CreateString("my_binary"); - auto file_state = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists); + auto file_state = + fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists, 0.01 /*polling_interval*/); auto ready_cond = fb::CreateReadyCondition(fbb, std::nullopt, file_state); auto comp_props = fb::CreateComponentProperties( fbb, bin_name, app_profile, 0 /*depends_on*/, 0 /*process_arguments*/, ready_cond); diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp index 50836d962..8a9906dcb 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp @@ -317,6 +317,9 @@ score::cpp::expected convertFileState(const fb: { SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE( fb_fs.file_path(), "FileState::file_path must never be nullptr as it is required in the schema"); + SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE( + fb_fs.polling_interval() != 0.0, + "No FileState::polling_interval is configured, this should have been defaulted with the script."); auto polling_interval_ms = secondsToMs(fb_fs.polling_interval()); if (!polling_interval_ms.has_value()) diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp index 464e52c02..5f5d0cd98 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp @@ -585,7 +585,7 @@ TEST_F(ConverterTest, ConvertReadyConditionWithFileState) { RecordProperty("Description", "convertReadyCondition maps file_state correctly."); ::flatbuffers::FlatBufferBuilder fbb; - auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists); + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists, 0.01 /*polling_interval*/); auto rc = fb::CreateReadyCondition(fbb, ::flatbuffers::nullopt /*process_state*/, fs); fbb.Finish(rc); const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); @@ -673,9 +673,10 @@ TEST_F(ConverterTest, ConvertFileStateValid) TEST_F(ConverterTest, ConvertFileStateDefaultsToExists) { - RecordProperty("Description", "convertFileState defaults state to Exists and polling_interval to 10ms."); + RecordProperty("Description", "convertFileState defaults state to Exists if it is not set."); ::flatbuffers::FlatBufferBuilder fbb; - auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready"); + // state is omitted from the buffer since it matches the schema default + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists, 0.01 /*polling_interval*/); fbb.Finish(fs); const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); @@ -685,6 +686,20 @@ TEST_F(ConverterTest, ConvertFileStateDefaultsToExists) EXPECT_THAT(result->polling_interval, Eq(std::chrono::milliseconds{10})); } +TEST_F(ConverterTest, ConvertFileStateWithoutPollingIntervalDeath) +{ + RecordProperty( + "Description", + "convertFileState fires an assertion if polling_interval is not configured, as the configuration script " + "always defaults it."); + ::flatbuffers::FlatBufferBuilder fbb; + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready"); + fbb.Finish(fs); + const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); + + EXPECT_DEATH(static_cast(details::convertFileState(*ptr)), ".*"); +} + TEST_F(ConverterTest, ConvertFileStateNegativePollingIntervalReturnsError) { RecordProperty("Description", "convertFileState returns InvalidFormat for a negative polling_interval."); diff --git a/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs b/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs index c5c636638..23d352c1a 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs +++ b/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs @@ -66,7 +66,7 @@ table FileState { // Existence state of the file. Defaults to Exists if not specified. state:FileExistenceState = Exists; // optional, defaults to Exists // Time in seconds to wait between each poll if the file is present. - polling_interval: double = 0.01; //optional, defaults to 0.01s (10ms) + polling_interval: double; // required } // Defines the conditions that determine when the component enters the ready state. diff --git a/scripts/config_mapping/tests/full_config_test/expected_output/lm_config_gen.json b/scripts/config_mapping/tests/full_config_test/expected_output/lm_config_gen.json index 24629f86b..16a8645ac 100644 --- a/scripts/config_mapping/tests/full_config_test/expected_output/lm_config_gen.json +++ b/scripts/config_mapping/tests/full_config_test/expected_output/lm_config_gen.json @@ -86,7 +86,11 @@ "--option" ], "ready_condition": { - "process_state": "Running" + "file_state": { + "state": "Exists", + "polling_interval": 0.01, + "file_path": "/var/run/b/ready" + } } }, "deployment_config": { @@ -143,7 +147,11 @@ ], "process_arguments": [], "ready_condition": { - "process_state": "Running" + "file_state": { + "state": "NotExisting", + "polling_interval": 0.5, + "file_path": "/var/run/c/startup.lock" + } } }, "deployment_config": { diff --git a/scripts/config_mapping/tests/full_config_test/input/lm_config.json b/scripts/config_mapping/tests/full_config_test/input/lm_config.json index 9ec08636a..33f2c2e70 100644 --- a/scripts/config_mapping/tests/full_config_test/input/lm_config.json +++ b/scripts/config_mapping/tests/full_config_test/input/lm_config.json @@ -127,7 +127,12 @@ "application_type": "Native", "is_self_terminating": true }, - "process_arguments": ["-b", "--option"] + "process_arguments": ["-b", "--option"], + "ready_condition": { + "file_state": { + "file_path": "/var/run/b/ready" + } + } }, "deployment_config": { "bin_dir": "/opt/apps/b", @@ -146,7 +151,14 @@ "application_type": "Reporting", "is_self_terminating": false }, - "depends_on": ["component_a"] + "depends_on": ["component_a"], + "ready_condition": { + "file_state": { + "file_path": "/var/run/c/startup.lock", + "state": "NotExisting", + "polling_interval": 0.5 + } + } }, "deployment_config": { "bin_dir": "/opt/apps/c",