diff --git a/tests/integration/complex_monitoring/BUILD b/tests/integration/complex_monitoring/BUILD new file mode 100644 index 00000000..b516c850 --- /dev/null +++ b/tests/integration/complex_monitoring/BUILD @@ -0,0 +1,89 @@ +# ******************************************************************************* +# 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_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files") +load("@rules_pkg//pkg:tar.bzl", "pkg_tar") +load("//:defs.bzl", "launch_manager_config") +load("//tests/utils/bazel:integration.bzl", "integration_test") + +launch_manager_config( + name = "lm_complex_monitoring_config", + config = "//tests/integration/complex_monitoring:complex_monitoring.json", + flatbuffer_out_dir = "etc", +) + +cc_binary( + name = "control_client_mock", + srcs = ["control_client_mock.cpp"], + deps = [ + "//src/control_client_lib", + "//src/launch_manager_daemon/lifecycle_client_lib:lifecycle_client", + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) + +cc_binary( + name = "component_complex_monitoring", + srcs = ["component_complex_monitoring.cpp"], + linkopts = select({ + "@platforms//os:qnx": [ + "-lsocket", + ], + "@platforms//os:linux": [ + "-lpthread", + "-lrt", + ], + }), + deps = [ + "//src/health_monitoring_lib:health_monitoring_lib_cc", + "//src/launch_manager_daemon/health_monitor_lib:hm_shared_lib", + "//src/launch_manager_daemon/lifecycle_client_lib:lifecycle_client", + "//tests/utils/test_helper", + "@googletest//:gtest_main", + "@score_baselibs_rust//src/log/stdout_logger_cpp_init", + ], +) + +pkg_files( + name = "complex_monitoring_main_files", + srcs = [ + ":component_complex_monitoring", + ":control_client_mock", + "//src/launch_manager_daemon:launch_manager", + "//tests/utils/test_helper:verification_process", + ], + attributes = pkg_attributes(mode = "0755"), + prefix = "tests/complex_monitoring", +) + +pkg_files( + name = "complex_monitoring_etc_files", + srcs = [":lm_complex_monitoring_config"], + prefix = "tests/complex_monitoring", +) + +pkg_tar( + name = "complex_monitoring_binaries", + srcs = [ + ":complex_monitoring_etc_files", + ":complex_monitoring_main_files", + ], +) + +integration_test( + name = "complex_monitoring", + srcs = ["complex_monitoring.py"], + tags = ["integration"], + test_binaries = ":complex_monitoring_binaries", + deps = ["//tests/utils/testing_utils"], +) diff --git a/tests/integration/complex_monitoring/complex_monitoring.json b/tests/integration/complex_monitoring/complex_monitoring.json new file mode 100644 index 00000000..7c1bdcbc --- /dev/null +++ b/tests/integration/complex_monitoring/complex_monitoring.json @@ -0,0 +1,120 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "bin_dir": "/tmp/tests/complex_monitoring", + "ready_timeout": 2.0, + "shutdown_timeout": 0.2, + "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": "Reporting", + "is_self_terminating": false + }, + "ready_condition": { + "process_state": "Running" + } + } + }, + "components": { + "control_client_mock": { + "component_properties": { + "binary_name": "control_client_mock", + "application_profile": { + "application_type": "State_Manager", + "alive_supervision": { + "min_indications": 0 + } + } + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "control_client_mock" + } + } + }, + "component_complex_monitoring": { + "component_properties": { + "binary_name": "component_complex_monitoring", + "application_profile": { + "application_type": "Reporting_And_Supervised", + "alive_supervision": { + "reporting_cycle": 0.1, + "min_indications": 1, + "max_indications": 3, + "failed_cycles_tolerance": 0 + } + } + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "component_complex_monitoring", + "IDENTIFIER": "component_complex_monitoring", + "CONFIG_PATH": "etc/hmproc_component_complex_monitoring.bin" + } + } + }, + "verification_component": { + "component_properties": { + "binary_name": "verification_process", + "application_profile": { + "application_type": "Native", + "is_self_terminating": true + }, + "ready_condition": { + "process_state": "Terminated" + } + } + } + }, + "run_targets": { + "Startup": { + "depends_on": [ + "control_client_mock" + ] + }, + "run_target_complex_monitoring": { + "depends_on": [ + "control_client_mock", + "component_complex_monitoring" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "Off": { + "depends_on": [] + } + }, + "initial_run_target": "Startup", + "alive_supervision": { + "evaluation_cycle": 0.05 + }, + "fallback_run_target": { + "depends_on": [ + "control_client_mock", + "verification_component" + ] + } +} diff --git a/tests/integration/complex_monitoring/complex_monitoring.py b/tests/integration/complex_monitoring/complex_monitoring.py new file mode 100644 index 00000000..ff3d9511 --- /dev/null +++ b/tests/integration/complex_monitoring/complex_monitoring.py @@ -0,0 +1,44 @@ +# ******************************************************************************* +# 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 ( + check_for_failures, + download_xml_results, +) +from attribute_plugin import add_test_properties + + +@add_test_properties( + partially_verifies=[ + # Health monitoring requirements not yet ready + "feat_req__lifecycle__ext_monitor_notify" + ], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +def test_complex_monitoring(target, setup_test, test_output_dir, remote_test_dir): + """Integration test for recovery actions triggered by heartbeat monitor failure. See component_complex_monitoring.cpp for detailed spec""" + + 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), + timeout_s=4.0, + ) + + download_xml_results(target, remote_test_dir, test_output_dir) + all_files, failing_files = check_for_failures(test_output_dir) + assert len(all_files) == 2, f"Didn't find the expected number of files {all_files}" + assert len(failing_files) == 0, f"Found failures in files {failing_files}" diff --git a/tests/integration/complex_monitoring/component_complex_monitoring.cpp b/tests/integration/complex_monitoring/component_complex_monitoring.cpp new file mode 100644 index 00000000..029b1978 --- /dev/null +++ b/tests/integration/complex_monitoring/component_complex_monitoring.cpp @@ -0,0 +1,75 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include +#include + +#include "score/lcm/lifecycle_client.h" +#include "score/mw/log/rust/stdout_logger_init.h" +#include "tests/utils/test_helper/test_helper.hpp" +#include +#include + +TEST(ComplexMonitoring, ComponentComplexMonitoring) +{ + // Rust logger required for hmon logs + score::mw::log::rust::StdoutLoggerBuilder builder; + builder.Context("APP").LogLevel(score::mw::log::rust::LogLevel::Verbose).SetAsDefaultLogger(); + + using namespace score::hm; + using namespace std::chrono_literals; + + auto hm_result = HealthMonitorBuilder() + .add_heartbeat_monitor(MonitorTag("complex_monitoring_monitor"), + heartbeat::HeartbeatMonitorBuilder(TimeRange(50ms, 150ms))) + .with_internal_processing_cycle(50ms) + .with_supervisor_api_cycle(50ms) + .build(); + ASSERT_TRUE(hm_result.has_value()) << "Failed to build HealthMonitor"; + + auto hm = std::move(*hm_result); + + auto heartbeat_monitor_result = hm.get_heartbeat_monitor(MonitorTag("complex_monitoring_monitor")); + ASSERT_TRUE(heartbeat_monitor_result.has_value()) << "Failed to get heartbeat monitor"; + auto heartbeat_monitor = std::move(*heartbeat_monitor_result); + + hm.start(); + + TEST_STEP("Report kRunning") + { + auto result = score::lcm::LifecycleClient{}.ReportExecutionState(score::lcm::ExecutionState::kRunning); + ASSERT_TRUE(result.has_value()) << "ReportExecutionState() failed: " << result.error().Message(); + } + + auto time_to_report_checkpoints_until = std::chrono::steady_clock::now() + 1s; + // Given that we can send heartbeats successfully... + TEST_STEP("Send heartbeats for 1 second") + { + while (std::chrono::steady_clock::now() < time_to_report_checkpoints_until) + { + std::this_thread::sleep_for(100ms); + heartbeat_monitor.heartbeat(); + } + EXPECT_FALSE(TestRunner::exitRequested) << "Process should not be terminated yet"; + } + // When heartbeats are no longer sent... +} + +int main(int argc, char** argv) +{ + TestRunner(__FILE__, false).RunTests(); + // Then expect kill due to recovery action (verified by control client) + while (true) // Stop reporting, wait for sigkill + { + pause(); + } +} diff --git a/tests/integration/complex_monitoring/control_client_mock.cpp b/tests/integration/complex_monitoring/control_client_mock.cpp new file mode 100644 index 00000000..500fb7d4 --- /dev/null +++ b/tests/integration/complex_monitoring/control_client_mock.cpp @@ -0,0 +1,54 @@ +/******************************************************************************** + * 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 "tests/utils/test_helper/test_helper.hpp" +#include +#include + +score::lcm::ControlClient client; + +TEST(ComplexMonitoring, ControlClientMock) +{ + ASSERT_TRUE(check_clean({test_end_location, fallback_file})); + + TEST_STEP("Report kRunning") + { + auto result = score::lcm::LifecycleClient{}.ReportExecutionState(score::lcm::ExecutionState::kRunning); + ASSERT_TRUE(result.has_value()) << "ReportExecutionState() failed: " << result.error().Message(); + } + TEST_STEP("Launch monitored process") + { + score::cpp::stop_token stop_token; + auto result = client.ActivateRunTarget("run_target_complex_monitoring").Get(stop_token); + EXPECT_TRUE(result.has_value()) << "Activating target run_target_complex_monitoring failed: " + << result.error().Message(); + } + // Wait for health monitoring to fail and recovery to trigger + sleep(2); + TEST_STEP("Verify state changed to fallback run target") + { + // workaround to detect we're in fallback + EXPECT_TRUE(std::filesystem::exists(fallback_file)) << "Fallback run target was not activated"; + } + TEST_STEP("Activate Off run target") + { + client.ActivateRunTarget("Off"); + } +} + +int main(int argc, char** argv) +{ + return TestRunner(__FILE__, true, true).RunTests(); +} diff --git a/tests/integration/crash_on_startup/BUILD b/tests/integration/crash_on_startup/BUILD new file mode 100644 index 00000000..87db0cfd --- /dev/null +++ b/tests/integration/crash_on_startup/BUILD @@ -0,0 +1,83 @@ +# ******************************************************************************* +# 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_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files") +load("@rules_pkg//pkg:tar.bzl", "pkg_tar") +load("//:defs.bzl", "launch_manager_config") +load("//tests/utils/bazel:integration.bzl", "integration_test") + +launch_manager_config( + name = "lm_crash_on_startup_config", + config = "//tests/integration/crash_on_startup:crash_on_startup.json", + flatbuffer_out_dir = "etc", +) + +cc_binary( + name = "control_client_mock", + srcs = ["control_client_mock.cpp"], + deps = [ + "//src/control_client_lib", + "//src/launch_manager_daemon/lifecycle_client_lib:lifecycle_client", + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) + +cc_binary( + name = "process_crashing_on_startup_twice", + srcs = ["process_crashing_on_startup_twice.cpp"], + deps = [ + "//src/launch_manager_daemon/lifecycle_client_lib:lifecycle_client", + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) + +cc_binary( + name = "process_crashing_on_startup_always", + srcs = ["process_crashing_on_startup_always.cpp"], +) + +pkg_files( + name = "crash_on_startup_main_files", + srcs = [ + ":control_client_mock", + ":process_crashing_on_startup_always", + ":process_crashing_on_startup_twice", + "//src/launch_manager_daemon:launch_manager", + "//tests/utils/test_helper:verification_process", + ], + attributes = pkg_attributes(mode = "0755"), + prefix = "tests/crash_on_startup", +) + +pkg_files( + name = "crash_on_startup_etc_files", + srcs = [":lm_crash_on_startup_config"], + prefix = "tests/crash_on_startup", +) + +pkg_tar( + name = "crash_on_startup_binaries", + srcs = [ + ":crash_on_startup_etc_files", + ":crash_on_startup_main_files", + ], +) + +integration_test( + name = "crash_on_startup", + srcs = ["crash_on_startup.py"], + tags = ["integration"], + test_binaries = ":crash_on_startup_binaries", + deps = ["//tests/utils/testing_utils"], +) diff --git a/tests/integration/crash_on_startup/control_client_mock.cpp b/tests/integration/crash_on_startup/control_client_mock.cpp new file mode 100644 index 00000000..08cc9eec --- /dev/null +++ b/tests/integration/crash_on_startup/control_client_mock.cpp @@ -0,0 +1,69 @@ +/******************************************************************************** + * 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 "tests/utils/test_helper/test_helper.hpp" +#include +#include + +score::lcm::ControlClient client; + +TEST(CrashOnStartup, ControlClientMock) +{ + TEST_STEP("Report kRunning") + { + auto result = score::lcm::LifecycleClient{}.ReportExecutionState(score::lcm::ExecutionState::kRunning); + ASSERT_TRUE(result.has_value()) << "ReportExecutionState() failed: " << result.error().Message(); + } + + // Given a process that crashes on startup twice + TEST_STEP("Launch process crashing on startup twice") + { + score::cpp::stop_token stop_token; + auto result = client.ActivateRunTarget("run_target_crash_on_startup_twice").Get(stop_token); + // Then, the LM should restart it and eventually succeed + EXPECT_TRUE(result.has_value()) << "Activating run_target_crash_on_startup_twice failed: " + << result.error().Message(); + } + + TEST_STEP("Verify fallback run target was not activated") + { + EXPECT_FALSE(std::filesystem::exists(fallback_file)) << "Fallback should not be activated yet"; + } + + // Given a process that crashes on startup more times than the configured restart attempts + TEST_STEP("Attempt to launch process crashing on startup always") + { + score::cpp::stop_token stop_token; + auto result = client.ActivateRunTarget("run_target_crash_on_startup_always").Get(stop_token); + EXPECT_FALSE(result.has_value()) << "Expected run_target_crash_on_startup_always activation to fail"; + } + // Limitation: we cannot wait for the transition to fallback to complete + sleep(1); + // Then, the LM should exhaust retries and trigger the fallback + TEST_STEP("Verify fallback run target was activated") + { + EXPECT_TRUE(std::filesystem::exists(fallback_file)) << "Fallback run target was not activated"; + } + + TEST_STEP("Activate RunTarget Off") + { + client.ActivateRunTarget("Off"); + } +} + +int main(int argc, char** argv) +{ + return TestRunner(__FILE__, true, true).RunTests(); +} diff --git a/tests/integration/crash_on_startup/crash_on_startup.json b/tests/integration/crash_on_startup/crash_on_startup.json new file mode 100644 index 00000000..1e436706 --- /dev/null +++ b/tests/integration/crash_on_startup/crash_on_startup.json @@ -0,0 +1,149 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "bin_dir": "/tmp/tests/crash_on_startup", + "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": "Reporting", + "is_self_terminating": false, + "alive_supervision": { + "reporting_cycle": 0.1, + "min_indications": 1, + "max_indications": 3, + "failed_cycles_tolerance": 1 + } + }, + "ready_condition": { + "process_state": "Running" + } + } + }, + "components": { + "control_client_mock": { + "component_properties": { + "binary_name": "control_client_mock", + "application_profile": { + "application_type": "State_Manager", + "alive_supervision": { + "min_indications": 0 + } + } + }, + "deployment_config": { + "ready_timeout": 1.0, + "shutdown_timeout": 1.0, + "environmental_variables": { + "PROCESSIDENTIFIER": "control_client_mock" + } + } + }, + "process_crashing_on_startup_twice": { + "component_properties": { + "binary_name": "process_crashing_on_startup_twice", + "application_profile": { + "application_type": "Reporting" + } + }, + "deployment_config": { + "ready_recovery_action": { + "restart": { + "number_of_attempts": 2 + } + }, + "environmental_variables": { + "PROCESSIDENTIFIER": "process_crashing_on_startup_twice" + } + } + }, + "process_crashing_on_startup_always": { + "component_properties": { + "binary_name": "process_crashing_on_startup_always", + "application_profile": { + "application_type": "Reporting" + } + }, + "deployment_config": { + "ready_recovery_action": { + "restart": { + "number_of_attempts": 2 + } + }, + "environmental_variables": { + "PROCESSIDENTIFIER": "process_crashing_on_startup_always" + } + } + }, + "verification_component": { + "component_properties": { + "binary_name": "verification_process", + "application_profile": { + "application_type": "Native", + "is_self_terminating": true + }, + "ready_condition": { + "process_state": "Terminated" + } + } + } + }, + "run_targets": { + "Startup": { + "depends_on": [ + "control_client_mock" + ] + }, + "run_target_crash_on_startup_twice": { + "depends_on": [ + "control_client_mock", + "process_crashing_on_startup_twice" + ] + }, + "run_target_crash_on_startup_always": { + "depends_on": [ + "control_client_mock", + "process_crashing_on_startup_always" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "Off": { + "depends_on": [] + } + }, + "initial_run_target": "Startup", + "alive_supervision": { + "evaluation_cycle": 0.05 + }, + "fallback_run_target": { + "depends_on": [ + "control_client_mock", + "verification_component" + ] + } +} diff --git a/tests/integration/crash_on_startup/crash_on_startup.py b/tests/integration/crash_on_startup/crash_on_startup.py new file mode 100644 index 00000000..592c8e5f --- /dev/null +++ b/tests/integration/crash_on_startup/crash_on_startup.py @@ -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 +# ******************************************************************************* +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 ( + check_for_failures, + download_xml_results, +) +from attribute_plugin import add_test_properties + + +@add_test_properties( + fully_verifies=["feat_req__lifecycle__failure_detect"], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +def test_crash_on_startup(target, setup_test, test_output_dir, remote_test_dir): + """Integration test verifying that the launch manager correctly handles processes + that crash before reporting kRunning.""" + + 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), + timeout_s=2.0, + ) + + download_xml_results(target, remote_test_dir, test_output_dir) + all_files, failing_files = check_for_failures(test_output_dir) + assert len(all_files) == 2, f"Didn't find the expected number of files {all_files}" + assert len(failing_files) == 0, f"Found failures in files {failing_files}" diff --git a/tests/integration/crash_on_startup/process_crashing_on_startup_always.cpp b/tests/integration/crash_on_startup/process_crashing_on_startup_always.cpp new file mode 100644 index 00000000..b0df4355 --- /dev/null +++ b/tests/integration/crash_on_startup/process_crashing_on_startup_always.cpp @@ -0,0 +1,19 @@ +/******************************************************************************** + * 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 + +int main() +{ + std::cout << "Process crashing on startup (always)..." << std::endl; + std::abort(); +} diff --git a/tests/integration/crash_on_startup/process_crashing_on_startup_twice.cpp b/tests/integration/crash_on_startup/process_crashing_on_startup_twice.cpp new file mode 100644 index 00000000..8a5bf98c --- /dev/null +++ b/tests/integration/crash_on_startup/process_crashing_on_startup_twice.cpp @@ -0,0 +1,49 @@ +/******************************************************************************** + * 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" +#include + +TEST(CrashOnStartup, ProcessCrashingOnStartupTwice) +{ + TEST_STEP("Report kRunning") + { + auto result = score::lcm::LifecycleClient{}.ReportExecutionState(score::lcm::ExecutionState::kRunning); + ASSERT_TRUE(result.has_value()) << "ReportExecutionState() failed: " << result.error().Message(); + } +} + +void deploy_and_crash_if_not_present(const std::string_view name) +{ + if (!std::filesystem::exists(name)) + { + std::cout << "Process crashing on startup..." << std::endl; + if (!touch_file(name)) + { + std::cout << "Failed to deploy marker file!" << std::endl; + } + std::abort(); + } +} + +int main() +{ + deploy_and_crash_if_not_present("crashed_once"); + deploy_and_crash_if_not_present("crashed_twice"); + + std::cout << "Process starting successfully..." << std::endl; + return TestRunner(__FILE__).RunTests(); +} diff --git a/tests/integration/process_crash_monitoring/BUILD b/tests/integration/process_crash_monitoring/BUILD new file mode 100644 index 00000000..4602f917 --- /dev/null +++ b/tests/integration/process_crash_monitoring/BUILD @@ -0,0 +1,77 @@ +# ******************************************************************************* +# 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_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files") +load("@rules_pkg//pkg:tar.bzl", "pkg_tar") +load("//:defs.bzl", "launch_manager_config") +load("//tests/utils/bazel:integration.bzl", "integration_test") + +launch_manager_config( + name = "lm_process_crash_monitoring_config", + config = "//tests/integration/process_crash_monitoring:process_crash_monitoring.json", + flatbuffer_out_dir = "etc", +) + +cc_binary( + name = "control_client_mock", + srcs = ["control_client_mock.cpp"], + deps = [ + "//src/control_client_lib", + "//src/launch_manager_daemon/lifecycle_client_lib:lifecycle_client", + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) + +cc_binary( + name = "process_crashing_on_runtime", + srcs = ["process_crashing_on_runtime.cpp"], + deps = [ + "//src/launch_manager_daemon/lifecycle_client_lib:lifecycle_client", + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) + +pkg_files( + name = "process_crash_monitoring_main_files", + srcs = [ + ":control_client_mock", + ":process_crashing_on_runtime", + "//src/launch_manager_daemon:launch_manager", + "//tests/utils/test_helper:verification_process", + ], + attributes = pkg_attributes(mode = "0755"), + prefix = "tests/process_crash_monitoring", +) + +pkg_files( + name = "process_crash_monitoring_etc_files", + srcs = [":lm_process_crash_monitoring_config"], + prefix = "tests/process_crash_monitoring", +) + +pkg_tar( + name = "process_crash_monitoring_binaries", + srcs = [ + ":process_crash_monitoring_etc_files", + ":process_crash_monitoring_main_files", + ], +) + +integration_test( + name = "process_crash_monitoring", + srcs = ["process_crash_monitoring.py"], + tags = ["integration"], + test_binaries = ":process_crash_monitoring_binaries", + deps = ["//tests/utils/testing_utils"], +) diff --git a/tests/integration/process_crash_monitoring/control_client_mock.cpp b/tests/integration/process_crash_monitoring/control_client_mock.cpp new file mode 100644 index 00000000..1781bdf2 --- /dev/null +++ b/tests/integration/process_crash_monitoring/control_client_mock.cpp @@ -0,0 +1,60 @@ +/******************************************************************************** + * 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 +#include + +score::lcm::ControlClient client; + +// Given a correct configuration with: +// - An initial Run Target named "Startup" containing "control_client_mock" +// - A Run Target named "run_target_crashing_app_on_runtime" containing "control_client_mock" and +// "component_crashing_on_runtime" + +TEST(ProcessCrashMonitoring, ControlClientMock) +{ + ASSERT_TRUE(check_clean({test_end_location, fallback_file})); + // Establish communication with launch manager + TEST_STEP("Report kRunning") + { + auto result = score::lcm::LifecycleClient{}.ReportExecutionState(score::lcm::ExecutionState::kRunning); + ASSERT_TRUE(result.has_value()) << "ReportExecutionState() failed: " << result.error().Message(); + } + TEST_STEP("Start crashing process") + { + score::cpp::stop_token stop_token; + auto result = client.ActivateRunTarget("run_target_crashing_app_on_runtime").Get(stop_token); + EXPECT_TRUE(result.has_value()) << "Activating target run_target_crashing_app_on_runtime failed: " + << result.error().Message(); + } + // When the process crashes + sleep(2); + // Then + TEST_STEP("Verify state changed to fallback run target") + { + // workaround to detect we're in fallback + EXPECT_TRUE(std::filesystem::exists(fallback_file)) << "Fallback run target was not activated"; + } + TEST_STEP("Activate RunTarget Off") + { + client.ActivateRunTarget("Off"); + } +} + +int main(int argc, char** argv) +{ + return TestRunner(__FILE__, true, true).RunTests(); +} diff --git a/tests/integration/process_crash_monitoring/process_crash_monitoring.json b/tests/integration/process_crash_monitoring/process_crash_monitoring.json new file mode 100644 index 00000000..443cd403 --- /dev/null +++ b/tests/integration/process_crash_monitoring/process_crash_monitoring.json @@ -0,0 +1,120 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "bin_dir": "/tmp/tests/process_crash_monitoring", + "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": "Reporting", + "is_self_terminating": false, + "alive_supervision": { + "reporting_cycle": 0.1, + "min_indications": 1, + "max_indications": 3, + "failed_cycles_tolerance": 1 + } + }, + "ready_condition": { + "process_state": "Running" + } + } + }, + "components": { + "control_client_mock": { + "component_properties": { + "binary_name": "control_client_mock", + "application_profile": { + "application_type": "State_Manager", + "alive_supervision": { + "min_indications": 0 + } + } + }, + "deployment_config": { + "ready_timeout": 1.0, + "shutdown_timeout": 1.0, + "environmental_variables": { + "PROCESSIDENTIFIER": "control_client_mock" + } + } + }, + "component_crashing_on_runtime": { + "component_properties": { + "binary_name": "process_crashing_on_runtime", + "application_profile": { + "application_type": "Reporting" + } + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "component_crashing_on_runtime" + } + } + }, + "verification_component": { + "component_properties": { + "binary_name": "verification_process", + "application_profile": { + "application_type": "Native", + "is_self_terminating": true + }, + "ready_condition": { + "process_state": "Terminated" + } + } + } + }, + "run_targets": { + "Startup": { + "depends_on": [ + "control_client_mock" + ] + }, + "run_target_crashing_app_on_runtime": { + "depends_on": [ + "control_client_mock", + "component_crashing_on_runtime" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "Off": { + "depends_on": [] + } + }, + "initial_run_target": "Startup", + "alive_supervision": { + "evaluation_cycle": 0.05 + }, + "fallback_run_target": { + "depends_on": [ + "control_client_mock", + "verification_component" + ] + } +} diff --git a/tests/integration/process_crash_monitoring/process_crash_monitoring.py b/tests/integration/process_crash_monitoring/process_crash_monitoring.py new file mode 100644 index 00000000..bf8425c5 --- /dev/null +++ b/tests/integration/process_crash_monitoring/process_crash_monitoring.py @@ -0,0 +1,41 @@ +# ******************************************************************************* +# 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 ( + check_for_failures, + download_xml_results, +) +from attribute_plugin import add_test_properties + + +@add_test_properties( + fully_verifies=["feat_req__lifecycle__monitor_abnormal_term"], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +def test_process_crash_monitoring(target, setup_test, test_output_dir, remote_test_dir): + """Integration test verifying that the launch manager correctly handles a process crash at runtime.""" + + 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), + timeout_s=4.0, + ) + + download_xml_results(target, remote_test_dir, test_output_dir) + all_files, failing_files = check_for_failures(test_output_dir) + assert len(all_files) == 2, f"Didn't find the expected number of files {all_files}" + assert len(failing_files) == 0, f"Found failures in files {failing_files}" diff --git a/tests/integration/process_crash_monitoring/process_crashing_on_runtime.cpp b/tests/integration/process_crash_monitoring/process_crashing_on_runtime.cpp new file mode 100644 index 00000000..5aa5b4b0 --- /dev/null +++ b/tests/integration/process_crash_monitoring/process_crashing_on_runtime.cpp @@ -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 + ********************************************************************************/ +#include + +#include "tests/utils/test_helper/test_helper.hpp" +#include + +TEST(ProcessCrashMonitoring, CrashingProcess) +{ + TEST_STEP("Report kRunning") + { + auto result = score::lcm::LifecycleClient{}.ReportExecutionState(score::lcm::ExecutionState::kRunning); + ASSERT_TRUE(result.has_value()) << "ReportExecutionState() failed: " << result.error().Message(); + } +} + +int main() +{ + TestRunner(__FILE__, false).RunTests(); + // Plenty of time to output the XML file and for LM to complete run target activation + sleep(1); + std::cout << "Process crashing..." << std::endl; + std::abort(); +} diff --git a/tests/integration/process_launch_args/BUILD b/tests/integration/process_launch_args/BUILD new file mode 100644 index 00000000..1c5c6e31 --- /dev/null +++ b/tests/integration/process_launch_args/BUILD @@ -0,0 +1,64 @@ +# ******************************************************************************* +# 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_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files") +load("@rules_pkg//pkg:tar.bzl", "pkg_tar") +load("//:defs.bzl", "launch_manager_config") +load("//tests/utils/bazel:integration.bzl", "integration_test") + +launch_manager_config( + name = "lm_process_launch_args_config", + config = "//tests/integration/process_launch_args:process_launch_args.json", + flatbuffer_out_dir = "etc", +) + +cc_binary( + name = "process_initial", + srcs = ["process_initial.cpp"], + deps = [ + "//src/launch_manager_daemon/lifecycle_client_lib:lifecycle_client", + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) + +pkg_files( + name = "process_launch_args_main_files", + srcs = [ + ":process_initial", + "//src/launch_manager_daemon:launch_manager", + ], + attributes = pkg_attributes(mode = "0755"), + prefix = "tests/process_launch_args", +) + +pkg_files( + name = "process_launch_args_etc_files", + srcs = [":lm_process_launch_args_config"], + prefix = "tests/process_launch_args", +) + +pkg_tar( + name = "process_launch_args_binaries", + srcs = [ + ":process_launch_args_etc_files", + ":process_launch_args_main_files", + ], +) + +integration_test( + name = "process_launch_args", + srcs = ["process_launch_args.py"], + tags = ["integration"], + test_binaries = ":process_launch_args_binaries", + deps = ["//tests/utils/testing_utils"], +) diff --git a/tests/integration/process_launch_args/process_initial.cpp b/tests/integration/process_launch_args/process_initial.cpp new file mode 100644 index 00000000..410441d7 --- /dev/null +++ b/tests/integration/process_launch_args/process_initial.cpp @@ -0,0 +1,50 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include + +#include "tests/utils/test_helper/test_helper.hpp" +#include + +int g_argc; +char** g_argv; + +// Given a correct configuration with: +// - An initial Run Target named "Startup" +// - Startup contains one Component named "component_initial" +// - component_initial has command line parameter "S-CORE rules!" + +// When launch manager is started + +TEST(ProcessLaunchArgs, ProcessInitial) +{ + ASSERT_TRUE(check_clean({test_end_location})); + // Then, the process is started and: + TEST_STEP("Report kRunning") + { + auto result = score::lcm::LifecycleClient{}.ReportExecutionState(score::lcm::ExecutionState::kRunning); + EXPECT_TRUE(result.has_value()) << "ReportExecutionState() failed: " << result.error().Message(); + } + TEST_STEP("Check args") + { + ASSERT_GT(g_argc, 1) << "Not enough arguments"; + EXPECT_STREQ(g_argv[1], "S-CORE rules!") << "Second argument was not as expected"; + } +} + +int main(int argc, char** argv) +{ + g_argc = argc; + g_argv = argv; + TestRunner runner{__FILE__, false, true}; + return runner.RunTests(); +} diff --git a/tests/integration/process_launch_args/process_launch_args.json b/tests/integration/process_launch_args/process_launch_args.json new file mode 100644 index 00000000..1d417bc3 --- /dev/null +++ b/tests/integration/process_launch_args/process_launch_args.json @@ -0,0 +1,73 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "bin_dir": "/tmp/tests/process_launch_args", + "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": "Reporting", + "is_self_terminating": true, + "alive_supervision": { + "reporting_cycle": 0.1, + "min_indications": 1, + "max_indications": 3, + "failed_cycles_tolerance": 1 + } + }, + "ready_condition": { + "process_state": "Running" + } + } + }, + "components": { + "component_initial": { + "component_properties": { + "binary_name": "process_initial", + "process_arguments": [ + "S-CORE rules!" + ] + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "component_initial" + } + } + } + }, + "run_targets": { + "Startup": { + "depends_on": [ + "component_initial" + ] + } + }, + "initial_run_target": "Startup", + "alive_supervision": { + "evaluation_cycle": 0.05 + }, + "fallback_run_target": { + "depends_on": [] + } +} diff --git a/tests/integration/process_launch_args/process_launch_args.py b/tests/integration/process_launch_args/process_launch_args.py new file mode 100644 index 00000000..80c6e1c8 --- /dev/null +++ b/tests/integration/process_launch_args/process_launch_args.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 ( + check_for_failures, + download_xml_results, +) +from attribute_plugin import add_test_properties + + +@add_test_properties( + fully_verifies=[ + "feat_req__lifecycle__start_named_run_target", + "feat_req__lifecycle__launch_support", + "feat_req__lifecycle__process_state_comm", + "feat_req__lifecycle__process_launch_args", + ], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +def test_process_launch_args(target, setup_test, test_output_dir, remote_test_dir): + """Integration test verifying that the launch manager correctly passes launch arguments to processes.""" + + 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), + timeout_s=2.0, + ) + + # That the process is started and an XML file is produced verifies feat_req__lifecycle__launch_support + download_xml_results(target, remote_test_dir, test_output_dir) + all_files, failing_files = check_for_failures(test_output_dir) + assert len(all_files) == 1, f"Didn't find the expected number of files {all_files}" + assert len(failing_files) == 0, f"Found failures in files {failing_files}" diff --git a/tests/integration/smoke/control_daemon_mock.cpp b/tests/integration/smoke/control_daemon_mock.cpp index 7b70bb49..efdca5d8 100644 --- a/tests/integration/smoke/control_daemon_mock.cpp +++ b/tests/integration/smoke/control_daemon_mock.cpp @@ -10,39 +10,46 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ +#include +#include #include #include -#include -#include -#include +#include "tests/utils/test_helper/test_helper.hpp" #include #include -#include "tests/utils/test_helper/test_helper.hpp" +#include score::lcm::ControlClient client; -TEST(Smoke, Daemon) { - TEST_STEP("Control daemon report kRunning") { +TEST(Smoke, Daemon) +{ + ASSERT_TRUE(check_clean({test_end_location})); + TEST_STEP("Control daemon report kRunning") + { // report kRunning auto result = score::lcm::LifecycleClient{}.ReportExecutionState(score::lcm::ExecutionState::kRunning); ASSERT_TRUE(result.has_value()) << "client.ReportExecutionState() failed: " << result.error().Message(); } - TEST_STEP("Activate RunTarget Running") { + TEST_STEP("Activate RunTarget Running") + { score::cpp::stop_token stop_token; auto result = client.ActivateRunTarget("Running").Get(stop_token); EXPECT_TRUE(result.has_value()) << "Activating target Running failed: " << result.error().Message(); } - TEST_STEP("Activate RunTarget Startup") { + TEST_STEP("Activate RunTarget Startup") + { score::cpp::stop_token stop_token; auto result = client.ActivateRunTarget("Startup").Get(stop_token); EXPECT_TRUE(result.has_value()); } - TEST_STEP("Activate RunTarget Off") { + TEST_STEP("Activate RunTarget Off") + { client.ActivateRunTarget("Off"); } } -int main(int argc, char** argv) { - return TestRunner(__FILE__, true).RunTests(); +int main(int argc, char** argv) +{ + return TestRunner(__FILE__, true, true).RunTests(); } diff --git a/tests/integration/switch_run_target/BUILD b/tests/integration/switch_run_target/BUILD new file mode 100644 index 00000000..3d2f9423 --- /dev/null +++ b/tests/integration/switch_run_target/BUILD @@ -0,0 +1,103 @@ +# ******************************************************************************* +# 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_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files") +load("@rules_pkg//pkg:tar.bzl", "pkg_tar") +load("//:defs.bzl", "launch_manager_config") +load("//tests/utils/bazel:integration.bzl", "integration_test") + +launch_manager_config( + name = "lm_switch_run_target_config", + config = "//tests/integration/switch_run_target:switch_run_target.json", + flatbuffer_out_dir = "etc", +) + +cc_library( + name = "switch_run_target_common", + hdrs = ["common.hpp"], +) + +cc_binary( + name = "control_client_mock", + srcs = ["control_client_mock.cpp"], + deps = [ + ":switch_run_target_common", + "//src/control_client_lib", + "//src/launch_manager_daemon/lifecycle_client_lib:lifecycle_client", + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) + +[ + cc_binary( + name = component, + srcs = [component + ".cpp"], + deps = [ + ":switch_run_target_common", + "//src/launch_manager_daemon/lifecycle_client_lib:lifecycle_client", + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], + ) + for component in [ + "component_a", + "component_b", + "component_d", + ] +] + +cc_binary( + name = "component_c", + srcs = ["//tests/utils/test_helper:reporting_process.cpp"], + deps = [ + "//src/launch_manager_daemon/lifecycle_client_lib:lifecycle_client", + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) + +pkg_files( + name = "switch_run_target_main_files", + srcs = [ + ":component_a", + ":component_b", + ":component_c", + ":component_d", + ":control_client_mock", + "//src/launch_manager_daemon:launch_manager", + ], + attributes = pkg_attributes(mode = "0755"), + prefix = "tests/switch_run_target", +) + +pkg_files( + name = "switch_run_target_etc_files", + srcs = [":lm_switch_run_target_config"], + prefix = "tests/switch_run_target", +) + +pkg_tar( + name = "switch_run_target_binaries", + srcs = [ + ":switch_run_target_etc_files", + ":switch_run_target_main_files", + ], +) + +integration_test( + name = "switch_run_target", + srcs = ["switch_run_target.py"], + tags = ["integration"], + test_binaries = ":switch_run_target_binaries", + deps = ["//tests/utils/testing_utils"], +) diff --git a/tests/integration/switch_run_target/common.hpp b/tests/integration/switch_run_target/common.hpp new file mode 100644 index 00000000..e500a27c --- /dev/null +++ b/tests/integration/switch_run_target/common.hpp @@ -0,0 +1,19 @@ +#ifndef SWITCH_RUN_TARGET_HEADER +#define SWITCH_RUN_TARGET_HEADER + +#include + +// Macros for consistent, constexpr names of marker files across test processes + +#define PROC_FILES(x) \ + constexpr std::string_view x##_started = "proc_" #x "_started"; \ + constexpr std::string_view x##_terminating = "proc_" #x "_terminating"; + +PROC_FILES(a) +PROC_FILES(b) +PROC_FILES(c) +PROC_FILES(d) + +#undef PROC_FILES + +#endif diff --git a/tests/integration/switch_run_target/component_a.cpp b/tests/integration/switch_run_target/component_a.cpp new file mode 100644 index 00000000..67d69e2b --- /dev/null +++ b/tests/integration/switch_run_target/component_a.cpp @@ -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 + ********************************************************************************/ +#include + +#include "common.hpp" +#include "tests/utils/test_helper/test_helper.hpp" +#include + +TEST(ComponentA, RunAndVerify) +{ + TEST_STEP("Report running") + { + EXPECT_TRUE(touch_file(a_started)) << "failed to deploy file"; + auto result = score::lcm::LifecycleClient{}.ReportExecutionState(score::lcm::ExecutionState::kRunning); + EXPECT_TRUE(result.has_value()) << "ReportExecutionState() failed: " << result.error().Message(); + } + TEST_STEP("Verify startup order") + { + EXPECT_TRUE(std::filesystem::exists(b_started)) << "Process B, depended on by process A, was not started!"; + } + while (!TestRunner::exitRequested) + { + pause(); + } + TEST_STEP("Verify termination order") + { + EXPECT_FALSE(std::filesystem::exists(b_terminating)) + << "Process B, depended on by process A, terminated before A!"; + } + EXPECT_TRUE(touch_file(a_terminating)) << "Failed to deploy file"; +} + +int main() +{ + return TestRunner(__FILE__).RunTests(); +} diff --git a/tests/integration/switch_run_target/component_b.cpp b/tests/integration/switch_run_target/component_b.cpp new file mode 100644 index 00000000..019c389e --- /dev/null +++ b/tests/integration/switch_run_target/component_b.cpp @@ -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 + ********************************************************************************/ +#include + +#include "common.hpp" +#include "tests/utils/test_helper/test_helper.hpp" +#include + +TEST(ComponentB, RunAndVerify) +{ + TEST_STEP("Verify startup order") + { + // D may or may not have started at this point + EXPECT_FALSE(std::filesystem::exists(a_started)) << "A started before B reported running!"; + } + TEST_STEP("Report running") + { + EXPECT_TRUE(touch_file(b_started)) << "failed to deploy file"; + auto result = score::lcm::LifecycleClient{}.ReportExecutionState(score::lcm::ExecutionState::kRunning); + EXPECT_TRUE(result.has_value()) << "ReportExecutionState() failed: " << result.error().Message(); + } + while (!TestRunner::exitRequested) + { + pause(); + } + TEST_STEP("Verify termination order") + { + EXPECT_TRUE(std::filesystem::exists(a_terminating)) << "B was terminated before A terminated!"; + } + EXPECT_TRUE(touch_file(b_terminating)) << "Failed to deploy file"; +} + +int main() +{ + return TestRunner{__FILE__}.RunTests(); +} diff --git a/tests/integration/switch_run_target/component_d.cpp b/tests/integration/switch_run_target/component_d.cpp new file mode 100644 index 00000000..7295ea6b --- /dev/null +++ b/tests/integration/switch_run_target/component_d.cpp @@ -0,0 +1,37 @@ +/******************************************************************************** + * 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 "common.hpp" +#include "tests/utils/test_helper/test_helper.hpp" +#include + +TEST(ComponentD, RunAndVerify) +{ + TEST_STEP("Report running") + { + EXPECT_TRUE(touch_file(d_started)) << "failed to deploy file"; + auto result = score::lcm::LifecycleClient{}.ReportExecutionState(score::lcm::ExecutionState::kRunning); + EXPECT_TRUE(result.has_value()) << "ReportExecutionState() failed: " << result.error().Message(); + } + while (!TestRunner::exitRequested) + { + pause(); + } + EXPECT_TRUE(touch_file(d_terminating)) << "Failed to deploy file"; +} + +int main() +{ + return TestRunner(__FILE__).RunTests(); +} diff --git a/tests/integration/switch_run_target/control_client_mock.cpp b/tests/integration/switch_run_target/control_client_mock.cpp new file mode 100644 index 00000000..bd4d3067 --- /dev/null +++ b/tests/integration/switch_run_target/control_client_mock.cpp @@ -0,0 +1,77 @@ +/******************************************************************************** + * 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 "common.hpp" +#include + +#include "tests/utils/test_helper/test_helper.hpp" +#include +#include + +score::lcm::ControlClient client; + +// Given a configuration with the following dependency tree: +// - Startup - which is the initial run target - depends on component component_initial +// - component_initial: No dependencies +// - run_target_a: Depends on run target run_target_c and component component_a +// - component_a: Depends on component_b +// - component_b: No dependencies +// - run_target_c: Depends on component component_d +// - component_d: No dependencies + +// The only constraint on process startup order is that A must start after B. +// This is because, even though run target A depends on run target C (where +// component D is contained), *component* A only depends on component B. + +TEST(SwitchRunTarget, ControlClientMock) +{ + ASSERT_TRUE(check_clean({test_end_location, a_started, b_started, c_started, d_started})); + TEST_STEP("Report kRunning") + { + auto result = score::lcm::LifecycleClient{}.ReportExecutionState(score::lcm::ExecutionState::kRunning); + EXPECT_TRUE(result.has_value()) << "ReportExecutionState() failed: " << result.error().Message(); + } + // When we switch run to run target A + // Then + // Processes A and B verify that B is started before A and terminated after A. + TEST_STEP("Activate run target A") + { + score::cpp::stop_token stop_token; + auto result = client.ActivateRunTarget("run_target_a").Get(stop_token); + EXPECT_TRUE(result.has_value()) << "Activating target run_target_a failed: " << result.error().Message(); + } + TEST_STEP("Verify running processes") + { + const auto running = {a_started, b_started, d_started}; + for (const auto proc : running) + { + EXPECT_TRUE(std::filesystem::exists(proc)) << "A process depended on by run target A was not started!"; + } + } + // Processes A and B verify that they have been shut down in the correct order. + TEST_STEP("Activate RunTarget Startup") + { + score::cpp::stop_token stop_token; + auto result = client.ActivateRunTarget("Startup").Get(stop_token); + EXPECT_TRUE(result.has_value()) << "Activating target Startup failed: " << result.error().Message(); + } + TEST_STEP("Activate RunTarget Off") + { + client.ActivateRunTarget("Off"); + EXPECT_FALSE(std::filesystem::exists(c_started)) << "Component C should not be launched"; + } +} + +int main(int argc, char** argv) +{ + return TestRunner(__FILE__, true, true).RunTests(); +} diff --git a/tests/integration/switch_run_target/switch_run_target.json b/tests/integration/switch_run_target/switch_run_target.json new file mode 100644 index 00000000..e72652e9 --- /dev/null +++ b/tests/integration/switch_run_target/switch_run_target.json @@ -0,0 +1,134 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "bin_dir": "/tmp/tests/switch_run_target", + "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": "Reporting", + "is_self_terminating": false, + "alive_supervision": { + "reporting_cycle": 0.1, + "min_indications": 1, + "max_indications": 3, + "failed_cycles_tolerance": 1 + } + }, + "ready_condition": { + "process_state": "Running" + } + } + }, + "components": { + "component_initial": { + "component_properties": { + "binary_name": "control_client_mock", + "application_profile": { + "application_type": "State_Manager", + "alive_supervision": { + "min_indications": 0 + } + } + }, + "deployment_config": { + "ready_timeout": 1.0, + "shutdown_timeout": 1.0, + "environmental_variables": { + "PROCESSIDENTIFIER": "control_client_mock" + } + } + }, + "component_a": { + "component_properties": { + "binary_name": "component_a", + "depends_on": ["component_b"] + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "component_a" + } + } + }, + "component_b": { + "component_properties": { + "binary_name": "component_b" + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "component_b" + } + } + }, + "component_c": { + "component_properties": { + "binary_name": "reporting_process" + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "component_c" + } + } + }, + "component_d": { + "component_properties": { + "binary_name": "component_d" + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "component_d" + } + } + } + }, + "run_targets": { + "Startup": { + "depends_on": [ + "component_initial" + ] + }, + "run_target_a": { + "depends_on": [ + "component_initial", + "run_target_c", + "component_a" + ] + }, + "run_target_c": { + "depends_on": [ + "component_d" + ] + }, + "Off": { + "depends_on": [] + } + }, + "initial_run_target": "Startup", + "alive_supervision": { + "evaluation_cycle": 0.05 + }, + "fallback_run_target": { + "depends_on": [] + } +} diff --git a/tests/integration/switch_run_target/switch_run_target.py b/tests/integration/switch_run_target/switch_run_target.py new file mode 100644 index 00000000..848f8b29 --- /dev/null +++ b/tests/integration/switch_run_target/switch_run_target.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 ( + check_for_failures, + download_xml_results, +) +from attribute_plugin import add_test_properties + + +@add_test_properties( + fully_verifies=[ + "feat_req__lifecycle__request_run_target_start", + "feat_req__lifecycle__switch_run_targets", + "feat_req__lifecycle__process_state_comm", + "feat_req__lifecycle__process_termination", + "feat_req__lifecycle__terminationn_dependency", + ], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +def test_switch_run_target(target, setup_test, test_output_dir, remote_test_dir): + """Integration test verifying that the launch manager respects dependencies when switching run targets.""" + 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), + timeout_s=2.0, + ) + + download_xml_results(target, remote_test_dir, test_output_dir) + all_files, failing_files = check_for_failures(test_output_dir) + # Process C never starts so there should only be 4 files + assert len(all_files) == 4, f"Didn't find the expected number of files {all_files}" + assert len(failing_files) == 0, f"Found failures in files {failing_files}" diff --git a/tests/utils/test_helper/BUILD b/tests/utils/test_helper/BUILD index 032d365e..f64d4204 100644 --- a/tests/utils/test_helper/BUILD +++ b/tests/utils/test_helper/BUILD @@ -10,6 +10,15 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* +exports_files( + # Export these to allow us to rename the binary if desired + [ + "reporting_process.cpp", + "verification_process.cpp", + ], + visibility = ["//tests:__subpackages__"], +) + cc_library( name = "test_helper", hdrs = ["test_helper.hpp"], @@ -18,3 +27,23 @@ cc_library( "@googletest//:gtest_main", ], ) + +cc_binary( + name = "reporting_process", + srcs = ["reporting_process.cpp"], + visibility = ["//tests:__subpackages__"], + deps = [ + ":test_helper", + "//src/launch_manager_daemon/lifecycle_client_lib:lifecycle_client", + "@googletest//:gtest_main", + ], +) + +cc_binary( + name = "verification_process", + srcs = ["verification_process.cpp"], + visibility = ["//tests:__subpackages__"], + deps = [ + ":test_helper", + ], +) diff --git a/tests/utils/test_helper/reporting_process.cpp b/tests/utils/test_helper/reporting_process.cpp new file mode 100644 index 00000000..12b214ee --- /dev/null +++ b/tests/utils/test_helper/reporting_process.cpp @@ -0,0 +1,32 @@ +/******************************************************************************** + * 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" +#include + +TEST(ReportingProcess, ReportsRunning) +{ + auto result = score::lcm::LifecycleClient{}.ReportExecutionState(score::lcm::ExecutionState::kRunning); + EXPECT_TRUE(result.has_value()) << "ReportExecutionState() failed: " << result.error().Message(); +} + +int main() +{ + // Prevent XML file naming collisions of different components running this binary + const char* process_id = std::getenv("PROCESSIDENTIFIER"); + const std::string xml_name = process_id ? (std::string{"reporting_process_"} + process_id) : __FILE__; + return TestRunner(xml_name).RunTests(); +} diff --git a/tests/utils/test_helper/test_helper.hpp b/tests/utils/test_helper/test_helper.hpp index 4bac753b..7b9030b3 100644 --- a/tests/utils/test_helper/test_helper.hpp +++ b/tests/utils/test_helper/test_helper.hpp @@ -10,19 +10,21 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ +#include #include #include #include -#include /// @return File path to an xml adjacent to the input file path -std::string xmlPath(const std::string_view file) { +std::string xmlPath(const std::string_view file) +{ return std::filesystem::path{file}.filename().stem().string() + ".xml"; } /// @brief Creates an empty file. /// @return AssertionSuccess if the file is correctly created. -inline testing::AssertionResult touch_file(const std::string_view file_path) { +inline testing::AssertionResult touch_file(const std::string_view file_path) +{ auto openRes = fopen(file_path.data(), "w+"); if (!openRes) return testing::AssertionFailure() @@ -34,45 +36,89 @@ inline testing::AssertionResult touch_file(const std::string_view file_path) { return testing::AssertionSuccess(); } -#define TEST_STEP(message) \ - for (bool once = \ - (std::cout << "[ STEP ] " << (message) << std::endl, \ - true); \ - once; \ - (std::cout << "[ END STEP ] " << (message) << std::endl), \ - once = false) +/// @brief Location to store a file signalling that the fallback state has been reached. +constexpr std::string_view fallback_file = "fallback_reached"; +/// @brief Where to store the test_end signal file. This must be kept consistent with where the test framework +/// searches for files. +constexpr std::string_view test_end_location = "../test_end"; + +/// @brief Call at the start of a test to check for leftover files from a previous run +/// Files can be leftover when running manually on the host system, but otherwise are cleaned up +/// by the test framework. +/// @param[in] files Files to check +/// @param[in] strict If true, return a failure if any files exist. Otherwise attempt to remove them. +[[nodiscard]] +inline testing::AssertionResult check_clean(const std::initializer_list files, + const bool strict = true) +{ + std::stringstream failures{}; + for (const auto file : files) + { + if (!std::filesystem::exists(file)) + { + continue; + } + + if (strict) + { + failures << "'" << file << "' already exists!\n"; + } + else if (!std::filesystem::remove(file)) + { + failures << "Failed to remove '" << file << "'!\n"; + } + } + if (failures.tellp() > 0) + { + return testing::AssertionFailure() << failures.str(); + } + return testing::AssertionSuccess(); +} + +#define TEST_STEP(message) \ + for (bool once = (std::cout << "[ STEP ] " << (message) << std::endl, true); once; \ + (std::cout << "[ END STEP ] " << (message) << std::endl), once = false) /// @brief Helper class to setup, run, and clean up GTEST tests -class TestRunner { - static void signalHandler(int) { +class TestRunner +{ + static void signalHandler(int) + { exitRequested = true; } bool signal_completion; + bool m_wait_for_termination; std::string_view m_test_path; -public: + public: /// @brief TestRunner constructor /// @param[in] test_path location to write the GTEST xml file (usually __FILE__) + /// @param[in] wait_for_termination whether to block on RunTests() until termination is requested /// @param[in] do_signal_completion whether this test should deploy a file signaling the test has completed /// Usually the control daemon should deploy this file. - TestRunner(std::string_view test_path, bool do_signal_completion=false) { + TestRunner(std::string_view test_path, bool wait_for_termination = true, bool do_signal_completion = false) + { m_test_path = test_path; + m_wait_for_termination = wait_for_termination; signal(SIGINT, signalHandler); signal(SIGTERM, signalHandler); signal_completion = do_signal_completion; } - ~TestRunner() { - if (!exitRequested) { + ~TestRunner() + { + if (m_wait_for_termination && !exitRequested) + { pause(); } - if (signal_completion) { - static_cast(touch_file("../test_end")); + if (signal_completion) + { + static_cast(touch_file(test_end_location)); } } @@ -80,12 +126,13 @@ class TestRunner { inline static std::atomic exitRequested = false; /// @brief Use this function in main() to run all tests. It returns 0 if all tests are successful, or 1 otherwise. - int RunTests() { + int RunTests() + { ::testing::GTEST_FLAG(output) = "xml:" + xmlPath(m_test_path); testing::InitGoogleTest(); - + auto res = RUN_ALL_TESTS(); return res; } -}; \ No newline at end of file +}; diff --git a/tests/utils/test_helper/verification_process.cpp b/tests/utils/test_helper/verification_process.cpp new file mode 100644 index 00000000..bd44732c --- /dev/null +++ b/tests/utils/test_helper/verification_process.cpp @@ -0,0 +1,27 @@ +/******************************************************************************** + * 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 + +int main() +{ + // This process must be active only in the fallback run target + if (!touch_file(fallback_file)) + { + std::cout << "Failed to write file!" << std::endl; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +}