-
Notifications
You must be signed in to change notification settings - Fork 32
Add test for comp_req__launch_man__fast_shutdown_support #573
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2835e63
0582fae
7f29515
8342435
c8ef2d6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # ******************************************************************************* | ||
| # Copyright (c) 2026 Contributors to the Eclipse Foundation | ||
| # | ||
| # See the NOTICE file(s) distributed with this work for additional | ||
| # information regarding copyright ownership. | ||
| # | ||
| # This program and the accompanying materials are made available under the | ||
| # terms of the Apache License Version 2.0 which is available at | ||
| # https://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # ******************************************************************************* | ||
| load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library") | ||
| load("//tests/utils/bazel:integration.bzl", "integration_test") | ||
|
|
||
| cc_library( | ||
| name = "lm_sigkill_children_survive_common", | ||
| hdrs = ["common.hpp"], | ||
| deps = [ | ||
| "@googletest//:gtest_main", | ||
| ], | ||
| ) | ||
|
|
||
| cc_binary( | ||
| name = "control_client_test_driver", | ||
| srcs = ["control_client_test_driver.cpp"], | ||
| deps = [ | ||
| ":lm_sigkill_children_survive_common", | ||
| "//score/launch_manager:control_cc", | ||
| "//score/launch_manager:lifecycle_cc", | ||
| "//tests/utils/test_helper", | ||
| "@googletest//:gtest_main", | ||
| ], | ||
| ) | ||
|
|
||
| cc_binary( | ||
| name = "application_process", | ||
| srcs = ["application_process.cpp"], | ||
| deps = [ | ||
| ":lm_sigkill_children_survive_common", | ||
| "//score/launch_manager:control_cc", | ||
| "//score/launch_manager:lifecycle_cc", | ||
| "//tests/utils/test_helper", | ||
| "@googletest//:gtest_main", | ||
| ], | ||
| ) | ||
|
|
||
| integration_test( | ||
| name = "lm_sigkill_children_survive", | ||
| timeout = "short", | ||
| srcs = ["lm_sigkill_children_survive.py"], | ||
| binaries = [ | ||
| ":control_client_test_driver", | ||
| ":application_process", | ||
| "//score/launch_manager", | ||
| ], | ||
| config = ":lm_sigkill_children_survive.json", | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <gtest/gtest.h> | ||
|
|
||
| #include "common.hpp" | ||
| #include "tests/utils/test_helper/test_helper.hpp" | ||
| #include <score/mw/lifecycle/report_running.h> | ||
|
|
||
| // Application process started by the Launch Manager. It publishes its PID and | ||
| // reports running, then blocks (in the TestRunner destructor) so it stays alive | ||
| // after the Launch Manager is killed. | ||
| TEST(LmSigkillChildrenSurvive, ApplicationProcess) | ||
| { | ||
| // Publish our PID before reporting running: once the "Running" run target is | ||
| // active the test is guaranteed to find this file. | ||
| ASSERT_TRUE(write_pid(app_pid_file)); | ||
|
|
||
| score::mw::lifecycle::report_running(); | ||
| } | ||
|
|
||
| int main() | ||
| { | ||
| return TestRunner(__FILE__).RunTests(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ********************************************************************************/ | ||
| #ifndef SCORE_TESTS_INTEGRATION_LM_SIGKILL_CHILDREN_SURVIVE_COMMON_HPP | ||
| #define SCORE_TESTS_INTEGRATION_LM_SIGKILL_CHILDREN_SURVIVE_COMMON_HPP | ||
|
|
||
| #include <gtest/gtest.h> | ||
| #include <unistd.h> | ||
| #include <fstream> | ||
| #include <string> | ||
| #include <string_view> | ||
|
|
||
| /// @brief PID files written by the managed processes (as decimal text) so the | ||
| /// test can track them by PID after the Launch Manager has been SIGKILLed. | ||
| constexpr std::string_view daemon_pid_file = "daemon_pid"; | ||
| constexpr std::string_view app_pid_file = "app_pid"; | ||
|
|
||
| /// @brief Touched by the control daemon once both managed processes are running, | ||
| /// signalling the test that it may SIGKILL the Launch Manager. | ||
| constexpr std::string_view children_ready_file = "children_ready"; | ||
|
|
||
| /// @brief Writes the current PID as decimal text to @p path. | ||
| /// @return AssertionSuccess if the PID was written successfully. | ||
| inline testing::AssertionResult write_pid(const std::string_view path) | ||
| { | ||
| std::ofstream out{std::string{path}, std::ios::trunc}; | ||
| out << getpid(); | ||
| if (!out) | ||
| { | ||
| return testing::AssertionFailure() << "Failed to write PID to " << path; | ||
| } | ||
| return testing::AssertionSuccess(); | ||
| } | ||
|
|
||
| #endif // SCORE_TESTS_INTEGRATION_LM_SIGKILL_CHILDREN_SURVIVE_COMMON_HPP |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| /******************************************************************************** | ||
| * Copyright (c) 2026 Contributors to the Eclipse Foundation | ||
| * | ||
| * See the NOTICE file(s) distributed with this work for additional | ||
| * information regarding copyright ownership. | ||
| * | ||
| * This program and the accompanying materials are made available under the | ||
| * terms of the Apache License Version 2.0 which is available at | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| ********************************************************************************/ | ||
| #include <gtest/gtest.h> | ||
|
|
||
| #include "common.hpp" | ||
| #include "tests/utils/test_helper/test_helper.hpp" | ||
| #include <score/mw/lifecycle/control_client.h> | ||
| #include <score/mw/lifecycle/report_running.h> | ||
|
|
||
| // Control daemon: starts the application process by activating the "Running" run | ||
| // target, then publishes readiness and blocks. It never switches away from | ||
| // "Running", so both it and the application process are still children of the | ||
| // Launch Manager when the test SIGKILLs it. | ||
| TEST(LmSigkillChildrenSurvive, ControlDaemon) | ||
| { | ||
| score::mw::lifecycle::ControlClient client{}; | ||
|
|
||
| ASSERT_TRUE(check_clean({daemon_pid_file, app_pid_file, children_ready_file}, /*strict=*/false)); | ||
|
|
||
| TEST_STEP("Control daemon report running") | ||
| { | ||
| score::mw::lifecycle::report_running(); | ||
| } | ||
|
|
||
| TEST_STEP("Activate RunTarget Running") | ||
| { | ||
| score::cpp::stop_token stop_token; | ||
| auto result = client.ActivateRunTarget("Running").Get(stop_token); | ||
| ASSERT_TRUE(result.has_value()) << "Activating target Running failed: " << result.error().Message(); | ||
| } | ||
|
|
||
| // Publish our own PID and signal the test, which then SIGKILLs the Launch Manager. | ||
| TEST_STEP("Signal readiness") | ||
| { | ||
| ASSERT_TRUE(write_pid(daemon_pid_file)); | ||
| ASSERT_TRUE(touch_file(children_ready_file)); | ||
| } | ||
| } | ||
|
|
||
| int main() | ||
| { | ||
| return TestRunner(__FILE__).RunTests(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| { | ||
| "schema_version": 1, | ||
| "defaults": { | ||
| "deployment_config": { | ||
| "bin_dir": "/tmp/tests/lm_sigkill_children_survive", | ||
| "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_daemon": { | ||
| "component_properties": { | ||
| "binary_name": "control_client_test_driver", | ||
| "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_daemon" | ||
| } | ||
| } | ||
| }, | ||
| "application_process": { | ||
| "component_properties": { | ||
| "binary_name": "application_process", | ||
| "application_profile": { | ||
| "application_type": "Reporting" | ||
| } | ||
| }, | ||
| "deployment_config": { | ||
| "environmental_variables": { | ||
| "PROCESSIDENTIFIER": "DefaultPG_app0" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "run_targets": { | ||
| "Startup": { | ||
| "depends_on": [ | ||
| "control_daemon" | ||
| ], | ||
| "recovery_action": { | ||
| "switch_run_target": { | ||
| "run_target": "fallback_run_target" | ||
| } | ||
| } | ||
| }, | ||
| "Running": { | ||
| "depends_on": [ | ||
| "control_daemon", | ||
| "application_process" | ||
| ], | ||
| "recovery_action": { | ||
| "switch_run_target": { | ||
| "run_target": "fallback_run_target" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "initial_run_target": "Startup", | ||
| "alive_supervision": { | ||
| "evaluation_cycle": 0.05 | ||
| }, | ||
| "fallback_run_target": { | ||
| "depends_on": [] | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| # ******************************************************************************* | ||
| # Copyright (c) 2026 Contributors to the Eclipse Foundation | ||
| # | ||
| # See the NOTICE file(s) distributed with this work for additional | ||
| # information regarding copyright ownership. | ||
| # | ||
| # This program and the accompanying materials are made available under the | ||
| # terms of the Apache License Version 2.0 which is available at | ||
| # https://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # ******************************************************************************* | ||
| from tests.utils.testing_utils.run_until_file_deployed import run_until_file_deployed | ||
| from tests.utils.testing_utils.setup_test import setup_test | ||
| from tests.utils.testing_utils.test_results import assert_test_results | ||
| from attribute_plugin import add_test_properties | ||
|
|
||
|
|
||
| def _read_pid(target, pid_file): | ||
| code, out = target.execute(f"cat {pid_file}") | ||
| assert code == 0, f"Failed to read {pid_file}: {out!r}" | ||
| return int(out.decode().strip()) | ||
|
|
||
|
|
||
| def _pid_alive(target, pid): | ||
| return target.execute(f"kill -0 {pid}")[0] == 0 | ||
|
|
||
|
|
||
| @add_test_properties( | ||
| partially_verifies=[], | ||
| fully_verifies=["comp_req__launch_man__fast_shutdown_support"], | ||
| test_type="interface-test", | ||
| derivation_technique="explorative-testing", | ||
| ) | ||
| def test_lm_sigkill_children_survive( | ||
| target, setup_test, assert_test_results, remote_test_dir | ||
| ): | ||
| """ | ||
| Objective: Verifies that processes started by the Launch Manager keep running | ||
| when the Launch Manager itself is killed with SIGKILL, i.e. without any chance | ||
| to tear its children down. | ||
|
|
||
| The control daemon activates the "Running" run target, which starts the managed | ||
| application process, and then signals readiness. The test SIGKILLs only the | ||
| Launch Manager and checks that both the control daemon and the application | ||
| process are still alive afterwards. | ||
|
|
||
| Expected Behaviour: After the Launch Manager is SIGKILLed, both child processes | ||
| remain running. | ||
| """ | ||
|
|
||
| config_path = str(remote_test_dir / "etc/lm_sigkill_children_survive.bin") | ||
| ready_file = remote_test_dir / "children_ready" | ||
| daemon_pid_file = remote_test_dir / "daemon_pid" | ||
| app_pid_file = remote_test_dir / "app_pid" | ||
|
|
||
| # Both children are up and reporting once the daemon touches the ready file. | ||
| # Keep the launch manager running so the test can SIGKILL it below. | ||
| proc = run_until_file_deployed( | ||
| target=target, | ||
| binary_path=str(remote_test_dir / "launch_manager"), | ||
| file_path=ready_file, | ||
| cwd=str(remote_test_dir), | ||
| args=["-c", config_path], | ||
| timeout_s=5.0, | ||
| stop_on_file=False, | ||
| ) | ||
|
|
||
| daemon_pid = None | ||
| app_pid = None | ||
| try: | ||
| daemon_pid = _read_pid(target, daemon_pid_file) | ||
| app_pid = _read_pid(target, app_pid_file) | ||
|
|
||
| # Kill the Launch Manager - and only the Launch Manager - via SIGKILL. | ||
| assert proc.is_running(), "Launch manager exited before it could be killed" | ||
| code, out = target.execute(f"kill -9 {proc.pid()}") | ||
| assert code == 0, f"Failed to SIGKILL launch manager (pid {proc.pid()}): {out!r}" | ||
| proc.wait(timeout_s=3.0) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this really the intended implementation of "fast shutdown" by killing the launch manager?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At least this is how I have understood it. This is what I have noted:
|
||
|
|
||
| # The child processes must survive the death of their parent. | ||
| assert _pid_alive(target, daemon_pid), ( | ||
| f"Control daemon (pid {daemon_pid}) died with the launch manager" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we test that they survive? Not sure it's relevant for the requirement. |
||
| ) | ||
| assert _pid_alive(target, app_pid), ( | ||
| f"Application process (pid {app_pid}) died with the launch manager" | ||
| ) | ||
|
|
||
| assert_test_results( | ||
| {"control_client_test_driver.xml", "application_process.xml"} | ||
| ) | ||
| finally: | ||
| # Clean up the now-orphaned children so they do not leak on the target. | ||
| for pid in (daemon_pid, app_pid): | ||
| if pid is not None: | ||
| target.execute(f"kill -9 {pid}") | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if the fast shutdown should be a sigkill, in that case I'm not sure if there is anything to test really as the LCM doesn't really have anything to say about getting killed.
In that case I also don't know if we should even have a requirement for this, as there is no code we write that changes the behavior.
But if this improves the component req coverage then I guess we might as well test it?
I think we need to clarify why the req is required.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see your point, we can do so again today.