From c33cfcb706127e3eadd89338883bc9fefc5cb5f1 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Mon, 20 Jul 2026 17:15:29 +0200 Subject: [PATCH 01/37] Add stub for test sandbox options Copied smoke test and renamed accordingly. --- tests/integration/sandbox_options/BUILD | 53 ++++++++ .../sandbox_options/control_daemon_mock.cpp | 53 ++++++++ .../sandbox_options/sandbox_options.json | 124 ++++++++++++++++++ .../sandbox_options/sandbox_options.py | 53 ++++++++ .../sandbox_options/sandbox_process.cpp | 30 +++++ 5 files changed, 313 insertions(+) create mode 100644 tests/integration/sandbox_options/BUILD create mode 100644 tests/integration/sandbox_options/control_daemon_mock.cpp create mode 100644 tests/integration/sandbox_options/sandbox_options.json create mode 100644 tests/integration/sandbox_options/sandbox_options.py create mode 100644 tests/integration/sandbox_options/sandbox_process.cpp diff --git a/tests/integration/sandbox_options/BUILD b/tests/integration/sandbox_options/BUILD new file mode 100644 index 000000000..afb20b491 --- /dev/null +++ b/tests/integration/sandbox_options/BUILD @@ -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 +# ******************************************************************************* +load("@rules_cc//cc:defs.bzl", "cc_binary") +load("//tests/utils/bazel:integration.bzl", "integration_test") + +exports_files( + ["lm_demo.json"], + visibility = ["//examples:__subpackages__"], +) + +cc_binary( + name = "control_daemon_mock", + srcs = ["control_daemon_mock.cpp"], + data = [], + deps = [ + "//score/launch_manager:control_cc", + "//score/launch_manager:lifecycle_cc", + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) + +cc_binary( + name = "sandbox_process", + srcs = ["sandbox_process.cpp"], + deps = [ + "//score/launch_manager:control_cc", + "//score/launch_manager:lifecycle_cc", + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) + +integration_test( + name = "smoke", + srcs = ["smoke.py"], + binaries = [ + ":control_daemon_mock", + ":sandbox_process", + "//score/launch_manager", + ], + config = ":lifecycle_smoketest.json", +) diff --git a/tests/integration/sandbox_options/control_daemon_mock.cpp b/tests/integration/sandbox_options/control_daemon_mock.cpp new file mode 100644 index 000000000..7e6484bf9 --- /dev/null +++ b/tests/integration/sandbox_options/control_daemon_mock.cpp @@ -0,0 +1,53 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include +#include +#include +#include + +#include "tests/utils/test_helper/test_helper.hpp" +#include +#include + +TEST(Smoke, Daemon) +{ + score::mw::lifecycle::ControlClient client{}; + ASSERT_TRUE(check_clean({test_end_location})); + TEST_STEP("Control daemon report running") + { + // 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); + EXPECT_TRUE(result.has_value()) << "Activating target Running failed: " << result.error().Message(); + } + 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") + { + client.ActivateRunTarget("Off"); + } +} + +int main(int argc, char** argv) +{ + return TestRunner(__FILE__, TerminationBehavior::kWait, TerminationNotification::kTestEnd).RunTests(); +} diff --git a/tests/integration/sandbox_options/sandbox_options.json b/tests/integration/sandbox_options/sandbox_options.json new file mode 100644 index 000000000..a78ba7f49 --- /dev/null +++ b/tests/integration/sandbox_options/sandbox_options.json @@ -0,0 +1,124 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "bin_dir": "/tmp/tests/sandbox_options", + "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_daemon_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_daemon" + } + } + }, + "sandbox_process": { + "component_properties": { + "binary_name": "sandbox_process", + "working_dir": "/tmp/sandbox/test", + "application_profile": { + "application_type": "Reporting" + } + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "DefaultPG_app0" + }, + "sandbox": { + "uid": 666, + "gid": 666, + "supplementary_group_ids": [123, 321, 456], + "scheduling_policy": "SCHED_FIFO", + "scheduling_priority": 10 + } + } + } + }, + "run_targets": { + "Startup": { + "depends_on": [ + "control_daemon" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "Running": { + "depends_on": [ + "control_daemon", + "sandbox_process" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "Off": { + "depends_on": [], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + + } + } + } + }, + "initial_run_target": "Startup", + "alive_supervision": { + "evaluation_cycle": 0.05 + }, + "fallback_run_target": { + "depends_on": [] + } +} diff --git a/tests/integration/sandbox_options/sandbox_options.py b/tests/integration/sandbox_options/sandbox_options.py new file mode 100644 index 000000000..373323463 --- /dev/null +++ b/tests/integration/sandbox_options/sandbox_options.py @@ -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 +# ******************************************************************************* +import logging +from tests.utils.testing_utils.run_until_file_deployed import run_until_file_deployed +from tests.utils.testing_utils.setup_test import setup_test +from tests.utils.testing_utils.test_results import assert_test_results +from attribute_plugin import add_test_properties + + +@add_test_properties( + fully_verifies=[ + "feat_req__lifecycle__uid_gid_support", + "feat_req__lifecycle__launch_priority_support", + "feat_req__lifecycle__cwd_support", + "feat_req__lifecycle__supplementary_groups" + ], + partially_verifies=[], + test_type="interface-test", + derivation_technique="explorative-testing", +) +def test_sandbox_options(target, setup_test, assert_test_results, remote_test_dir): + """ + Objective: Verifies the effectiveness of sandbox-options as gid, uid, supplementary groups, and scheduling policy. + + The launch manager starts with an initial run target. The control daemon activates the "Running" run target (starting the managed process with the sandbox options applied), then transitions back to "Startup", and finally activates "Off". + Expected Behaviour: All run target transitions complete successfully and all processes report running. + """ + + # launch manager will simply ignore the arguments if run with --//config:use_new_configuration=False. + # the old configuration will be used, which is the default behavior. + # The new configuration will be used if run with --//config:use_new_configuration=True + new_config_path = str(remote_test_dir / "etc/sandbox_options.bin") + + run_until_file_deployed( + target=target, + binary_path=str(remote_test_dir / "launch_manager"), + file_path=remote_test_dir.parent / "test_end", + cwd=str(remote_test_dir), + args=["-c", new_config_path], + timeout_s=3.0, + ) + + assert_test_results({"control_daemon_mock.xml", "gtest_process.xml"}) diff --git a/tests/integration/sandbox_options/sandbox_process.cpp b/tests/integration/sandbox_options/sandbox_process.cpp new file mode 100644 index 000000000..3b7649ba1 --- /dev/null +++ b/tests/integration/sandbox_options/sandbox_process.cpp @@ -0,0 +1,30 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include +#include +#include + +#include +#include "tests/utils/test_helper/test_helper.hpp" + +TEST(SandboxOptions, RunAndVerify) { + // report running + score::mw::lifecycle::report_running(); + + +} + +int main() { + return TestRunner(__FILE__).RunTests(); +} From ad31e4af7ba75d71562c349c9f3e09d82ab3cd66 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Tue, 21 Jul 2026 09:25:31 +0200 Subject: [PATCH 02/37] First attempt with opencode --- .../configuration/configuration_adapter.cpp | 1 + .../details/process_launcher.cpp | 10 ++ .../src/process_group_manager/iprocess.hpp | 1 + tests/integration/sandbox_options/BUILD | 12 +- .../sandbox_options/sandbox_options.json | 10 +- .../sandbox_options/sandbox_options.py | 2 +- .../sandbox_options_process.cpp | 112 ++++++++++++++++++ .../sandbox_options/sandbox_process.cpp | 30 ----- 8 files changed, 136 insertions(+), 42 deletions(-) create mode 100644 tests/integration/sandbox_options/sandbox_options_process.cpp delete mode 100644 tests/integration/sandbox_options/sandbox_process.cpp 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 4b2ecd3c3..5f9ce161c 100644 --- a/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp +++ b/score/launch_manager/src/daemon/src/configuration/configuration_adapter.cpp @@ -77,6 +77,7 @@ void ConfigurationAdapter::fillStartupConfigFromDeployment( const auto& props = comp.component_properties; startup.executable_path_ = deploy.bin_dir + "/" + props.binary_name; + startup.working_dir_ = deploy.working_dir; startup.short_name_ = comp.name; startup.uid_ = deploy.sandbox.uid; diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp index 9535575eb..cded4120d 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp @@ -140,6 +140,16 @@ void handleComms(score::lcm::internal::osal::ChildProcessConfig& param) /// @details The implementation should be async signal safe. void changeCurrentWorkingDirectory(const score::lcm::internal::osal::OsalConfig& config) { + if (!config.working_dir_.empty()) + { + if (-1 == chdir(config.working_dir_.c_str())) + { + LM_LOG_ERROR() << "[New process] chdir(" << config.working_dir_ << ") failed:" << score::lcm::internal::errno_message(errno); + sysexit(EXIT_FAILURE); + } + return; + } + // Change current working directory to the same as the executable constexpr size_t string_size = static_cast(PATH_MAX); // Notice that this next static variable is duplicated by the fork() and so does not need diff --git a/score/launch_manager/src/daemon/src/process_group_manager/iprocess.hpp b/score/launch_manager/src/daemon/src/process_group_manager/iprocess.hpp index e50738951..be7a9f5fd 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/iprocess.hpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/iprocess.hpp @@ -67,6 +67,7 @@ struct OsalConfig int32_t scheduling_policy_; ///< Scheduling policy defined for this process int32_t scheduling_priority_; ///< Scheduling priority for this process OsalLimits resource_limits_; ///< Resource limits for this process + std::string working_dir_{}; ///< Working directory for the process }; /// @brief Struct to hold configuration parameters for the child process. diff --git a/tests/integration/sandbox_options/BUILD b/tests/integration/sandbox_options/BUILD index afb20b491..08bba667a 100644 --- a/tests/integration/sandbox_options/BUILD +++ b/tests/integration/sandbox_options/BUILD @@ -31,8 +31,8 @@ cc_binary( ) cc_binary( - name = "sandbox_process", - srcs = ["sandbox_process.cpp"], + name = "sandbox_options_process", + srcs = ["sandbox_options_process.cpp"], deps = [ "//score/launch_manager:control_cc", "//score/launch_manager:lifecycle_cc", @@ -42,12 +42,12 @@ cc_binary( ) integration_test( - name = "smoke", - srcs = ["smoke.py"], + name = "sandbox_options", + srcs = ["sandbox_options.py"], binaries = [ ":control_daemon_mock", - ":sandbox_process", + ":sandbox_options_process", "//score/launch_manager", ], - config = ":lifecycle_smoketest.json", + config = ":sandbox_options.json", ) diff --git a/tests/integration/sandbox_options/sandbox_options.json b/tests/integration/sandbox_options/sandbox_options.json index a78ba7f49..38dcfeac3 100644 --- a/tests/integration/sandbox_options/sandbox_options.json +++ b/tests/integration/sandbox_options/sandbox_options.json @@ -60,10 +60,9 @@ } } }, - "sandbox_process": { + "sandbox_options_process": { "component_properties": { - "binary_name": "sandbox_process", - "working_dir": "/tmp/sandbox/test", + "binary_name": "sandbox_options_process", "application_profile": { "application_type": "Reporting" } @@ -78,7 +77,8 @@ "supplementary_group_ids": [123, 321, 456], "scheduling_policy": "SCHED_FIFO", "scheduling_priority": 10 - } + }, + "working_dir": "/tmp" } } }, @@ -96,7 +96,7 @@ "Running": { "depends_on": [ "control_daemon", - "sandbox_process" + "sandbox_options_process" ], "recovery_action": { "switch_run_target": { diff --git a/tests/integration/sandbox_options/sandbox_options.py b/tests/integration/sandbox_options/sandbox_options.py index 373323463..7de834c62 100644 --- a/tests/integration/sandbox_options/sandbox_options.py +++ b/tests/integration/sandbox_options/sandbox_options.py @@ -50,4 +50,4 @@ def test_sandbox_options(target, setup_test, assert_test_results, remote_test_di timeout_s=3.0, ) - assert_test_results({"control_daemon_mock.xml", "gtest_process.xml"}) + assert_test_results({"control_daemon_mock.xml", "sandbox_options_process.xml"}) diff --git a/tests/integration/sandbox_options/sandbox_options_process.cpp b/tests/integration/sandbox_options/sandbox_options_process.cpp new file mode 100644 index 000000000..56d456728 --- /dev/null +++ b/tests/integration/sandbox_options/sandbox_options_process.cpp @@ -0,0 +1,112 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include "tests/utils/test_helper/test_helper.hpp" + +namespace { + +bool verify_sandbox_options() +{ + bool all_pass = true; + + const uid_t expected_uid = 666; + const gid_t expected_gid = 666; + const std::array expected_supp_groups = {123, 321, 456}; + const std::string expected_cwd = "/tmp"; + + TEST_STEP("Verify uid and gid") + { + const uid_t current_uid = getuid(); + const gid_t current_gid = getgid(); + + std::ofstream uid_file{"sandbox_uid.txt"}; + uid_file << current_uid; + std::ofstream gid_file{"sandbox_gid.txt"}; + gid_file << current_gid; + + EXPECT_EQ(current_uid, expected_uid) << "Expected uid=" << expected_uid << " but got uid=" << current_uid; + EXPECT_EQ(current_gid, expected_gid) << "Expected gid=" << expected_gid << " but got gid=" << current_gid; + + if (current_uid != expected_uid || current_gid != expected_gid) + { + all_pass = false; + } + } + + TEST_STEP("Verify supplementary groups") + { + std::vector groups(256); + const int count = getgroups(static_cast(groups.size()), groups.data()); + EXPECT_GE(count, 0) << "Failed to get supplementary groups"; + + std::ofstream supp_file{"sandbox_supp_groups.txt"}; + for (int i = 0; i < count; ++i) + { + supp_file << groups[i] << (i < count - 1 ? "," : ""); + } + + for (const gid_t expected_group : expected_supp_groups) + { + const bool found = std::find(groups.begin(), groups.begin() + count, expected_group) != groups.end(); + EXPECT_TRUE(found) << "Expected supplementary group " << expected_group << " not found (groups: [" + << count << " total)]"; + if (!found) + { + all_pass = false; + } + } + } + + TEST_STEP("Verify working directory") + { + char buf[PATH_MAX]; + char* result = getcwd(buf, sizeof(buf)); + EXPECT_NE(result, nullptr) << "Failed to get current working directory"; + + if (result) + { + std::ofstream cwd_file{"sandbox_cwd.txt"}; + cwd_file << result; + + EXPECT_EQ(std::string(result), expected_cwd) + << "Expected working_dir=" << expected_cwd << " but got cwd=" << result; + + if (std::string(result) != expected_cwd) + { + all_pass = false; + } + } + } + + return all_pass; +} + +} // namespace + +TEST(SandboxOptions, RunAndVerify) { + ASSERT_TRUE(verify_sandbox_options()) << "Sandbox options verification failed"; + score::mw::lifecycle::report_running(); +} + +int main() { + return TestRunner(__FILE__).RunTests(); +} diff --git a/tests/integration/sandbox_options/sandbox_process.cpp b/tests/integration/sandbox_options/sandbox_process.cpp deleted file mode 100644 index 3b7649ba1..000000000 --- a/tests/integration/sandbox_options/sandbox_process.cpp +++ /dev/null @@ -1,30 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - ********************************************************************************/ - -#include -#include -#include - -#include -#include "tests/utils/test_helper/test_helper.hpp" - -TEST(SandboxOptions, RunAndVerify) { - // report running - score::mw::lifecycle::report_running(); - - -} - -int main() { - return TestRunner(__FILE__).RunTests(); -} From d7ab821141bfec5541fc28f71234825af3ce1c1a Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Tue, 21 Jul 2026 11:30:06 +0200 Subject: [PATCH 03/37] Patch the docker environment Allow execution of sched_setscheduler call by adding SYS_NICE capability to the docker environment --- MODULE.bazel | 10 ++++++++++ patches/BUILD | 17 +++++++++++++++++ patches/score_itf_docker_cap_add.patch | 11 +++++++++++ .../sandbox_options/sandbox_options.py | 10 ++++++++++ 4 files changed, 48 insertions(+) create mode 100644 patches/score_itf_docker_cap_add.patch diff --git a/MODULE.bazel b/MODULE.bazel index 339213741..49eddd7a1 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -161,6 +161,16 @@ use_repo(pip, "score_lifecycle_pip") bazel_dep(name = "score_itf", version = "0.5.0", dev_dependency = True) +# Patch the Docker plugin so tests can request extra container privileges +# (e.g. cap_add=["SYS_NICE"], needed for SCHED_FIFO/sched_setscheduler) via the +# `docker_configuration` fixture. Remove once forwarding lands upstream. +single_version_override( + module_name = "score_itf", + patch_strip = 1, + patches = ["//patches:score_itf_docker_cap_add.patch"], + version = "0.3.0", +) + oci = use_extension("@rules_oci//oci:extensions.bzl", "oci", dev_dependency = True) oci.pull( name = "debian-test-runtime", diff --git a/patches/BUILD b/patches/BUILD index 64261f475..401257be0 100644 --- a/patches/BUILD +++ b/patches/BUILD @@ -1 +1,18 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + exports_files(["hedron_compile_commands_env_vars.patch"]) + +exports_files(["score_bazel_tools.patch"]) + +exports_files(["score_bazel_tools_python.patch"]) diff --git a/patches/score_itf_docker_cap_add.patch b/patches/score_itf_docker_cap_add.patch new file mode 100644 index 000000000..f2a372799 --- /dev/null +++ b/patches/score_itf_docker_cap_add.patch @@ -0,0 +1,11 @@ +--- a/score/itf/plugins/docker.py ++++ b/score/itf/plugins/docker.py +@@ -331,6 +331,8 @@ def target_init(request, _docker_configuration): + environment=_docker_configuration["environment"], + volumes=_docker_configuration["volumes"], + shm_size=_docker_configuration["shm_size"], ++ privileged=_docker_configuration.get("privileged", False), ++ cap_add=_docker_configuration.get("cap_add"), + ) + target = None + try: diff --git a/tests/integration/sandbox_options/sandbox_options.py b/tests/integration/sandbox_options/sandbox_options.py index 7de834c62..1cdadc6f2 100644 --- a/tests/integration/sandbox_options/sandbox_options.py +++ b/tests/integration/sandbox_options/sandbox_options.py @@ -11,12 +11,22 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* import logging +import pytest +from score.itf.plugins.core import determine_target_scope 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 +@pytest.fixture(scope=determine_target_scope) +def docker_configuration(): + """Grant the container CAP_SYS_NICE so the launch manager can apply the + SCHED_FIFO real-time scheduling policy configured in sandbox_options.json. + Without it, sched_setscheduler() fails with 'Operation not permitted'.""" + return {"cap_add": ["SYS_NICE"]} + + @add_test_properties( fully_verifies=[ "feat_req__lifecycle__uid_gid_support", From f8899a20d78066eb2c13cb5a90b0be1311625719 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 22 Jul 2026 15:00:29 +0200 Subject: [PATCH 04/37] Update known_limitations.rst --- .../docs/product_documentation/known_limitations.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/score/launch_manager/docs/product_documentation/known_limitations.rst b/score/launch_manager/docs/product_documentation/known_limitations.rst index 4cfc8e935..6fbcca996 100644 --- a/score/launch_manager/docs/product_documentation/known_limitations.rst +++ b/score/launch_manager/docs/product_documentation/known_limitations.rst @@ -33,8 +33,7 @@ Component ``run_target`` set to ``fallback_run_target``. * The ``ready_timeout`` is used as the timeout until process state Running is reached, even in case the ReadyCondition is ``process_state:Terminated``. -* The parameter ``deployment_config/working_dir`` is currently not supported - and is ignored. + Run target ---------- From 872b274cf627be7325bf00112890835c38a20ea6 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 22 Jul 2026 16:34:40 +0200 Subject: [PATCH 05/37] Finish support for setting the working dir Some unit tests are currently broken however. --- .../config_schema/lm_flatcfg.fbs | 1 + .../configuration/configuration_manager.cpp | 4 +- scripts/config_mapping/lifecycle_config.py | 11 +++- .../sandbox_options/control_daemon_mock.cpp | 3 +- .../sandbox_options/sandbox_options.py | 8 ++- .../sandbox_options_process.cpp | 23 ++++--- tests/utils/testing_utils/test_results.py | 61 +++++++++++++------ 7 files changed, 76 insertions(+), 35 deletions(-) diff --git a/score/launch_manager/src/daemon/src/configuration/config_schema/lm_flatcfg.fbs b/score/launch_manager/src/daemon/src/configuration/config_schema/lm_flatcfg.fbs index e7f6fad15..ae687008d 100644 --- a/score/launch_manager/src/daemon/src/configuration/config_schema/lm_flatcfg.fbs +++ b/score/launch_manager/src/daemon/src/configuration/config_schema/lm_flatcfg.fbs @@ -43,6 +43,7 @@ table Process { uid: uint (id:4); gid: uint (id:5); path: string (id:6); + working_dir: string (id:11); security_policy_details: string (id:7); executable_reporting_behavior: ExecutionStateReportingBehaviorEnum (id:8); startup_config: [ProcessStartupConfig] (id:9); diff --git a/score/launch_manager/src/daemon/src/configuration/configuration_manager.cpp b/score/launch_manager/src/daemon/src/configuration/configuration_manager.cpp index bc538918c..4ce25de8b 100644 --- a/score/launch_manager/src/daemon/src/configuration/configuration_manager.cpp +++ b/score/launch_manager/src/daemon/src/configuration/configuration_manager.cpp @@ -521,8 +521,10 @@ bool ConfigurationManager::parseProcessConfigurations(const Process* node) setSchedulingParameters(*node, *startup_config_node, instance); // Set executable path from node's path instance.startup_config_.executable_path_ = getStringFromFlatBuffer(node->path()); + instance.startup_config_.working_dir_ = getStringFromFlatBuffer(node->working_dir()); LM_LOG_DEBUG() << "parseProcessConfigurations: Process index:" << instance.process_number_ - << "executable_path_:" << instance.startup_config_.executable_path_; + << "executable_path_:" << instance.startup_config_.executable_path_ + << " working_dir_:" << instance.startup_config_.working_dir_; instance.startup_config_.short_name_ = node->identifier() ? node->identifier()->c_str() : "Unknown"; instance.startup_config_.uid_ = node->uid() & 0x7FFFFFFFU; diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 680ac4b1f..0bcebde10 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -27,7 +27,6 @@ "shutdown_timeout": 0.5, "environmental_variables": {}, "bin_dir": "/opt", - "working_dir": "/tmp", "ready_recovery_action": { "restart": { "number_of_attempts": 0, @@ -289,7 +288,11 @@ def gen_config(output_dir, config, input_filename, schema_version=None): "ready_timeout": depl_cfg["ready_timeout"], "shutdown_timeout": depl_cfg["shutdown_timeout"], "bin_dir": depl_cfg["bin_dir"], - "working_dir": depl_cfg["working_dir"], + # Default the working directory to bin_dir (the directory the + # executable lives in) when not set explicitly. This matches the + # launch manager's own fallback of chdir'ing to the executable's + # directory when no working_dir is configured. + "working_dir": depl_cfg.get("working_dir", depl_cfg["bin_dir"]), "sandbox": sandbox_out, } @@ -668,6 +671,10 @@ def get_terminating_behavior(component_config): process["number_of_restart_attempts"] = component_config["deployment_config"][ "ready_recovery_action" ]["restart"]["number_of_attempts"] + process["working_dir"] = component_config["deployment_config"].get( + "working_dir", + component_config["deployment_config"]["bin_dir"] + ) match component_config["component_properties"]["application_profile"][ "application_type" diff --git a/tests/integration/sandbox_options/control_daemon_mock.cpp b/tests/integration/sandbox_options/control_daemon_mock.cpp index 7e6484bf9..d0c684303 100644 --- a/tests/integration/sandbox_options/control_daemon_mock.cpp +++ b/tests/integration/sandbox_options/control_daemon_mock.cpp @@ -25,7 +25,6 @@ TEST(Smoke, Daemon) ASSERT_TRUE(check_clean({test_end_location})); TEST_STEP("Control daemon report running") { - // report running score::mw::lifecycle::report_running(); } @@ -47,7 +46,7 @@ TEST(Smoke, Daemon) } } -int main(int argc, char** argv) +int main() { return TestRunner(__FILE__, TerminationBehavior::kWait, TerminationNotification::kTestEnd).RunTests(); } diff --git a/tests/integration/sandbox_options/sandbox_options.py b/tests/integration/sandbox_options/sandbox_options.py index 1cdadc6f2..1828f9c55 100644 --- a/tests/integration/sandbox_options/sandbox_options.py +++ b/tests/integration/sandbox_options/sandbox_options.py @@ -60,4 +60,10 @@ def test_sandbox_options(target, setup_test, assert_test_results, remote_test_di timeout_s=3.0, ) - assert_test_results({"control_daemon_mock.xml", "sandbox_options_process.xml"}) + # The managed process is configured with a custom working directory (see the + # "working_dir" entries in sandbox_options.json), so its XML result file is + # written there rather than into remote_test_dir. Search those directories too. + assert_test_results( + {"control_daemon_mock.xml", "sandbox_options_process.xml"}, + additional_search_dirs=["/tmp/tests/sandbox_options", "/tmp"], + ) diff --git a/tests/integration/sandbox_options/sandbox_options_process.cpp b/tests/integration/sandbox_options/sandbox_options_process.cpp index 56d456728..eac5a95b8 100644 --- a/tests/integration/sandbox_options/sandbox_options_process.cpp +++ b/tests/integration/sandbox_options/sandbox_options_process.cpp @@ -12,17 +12,18 @@ ********************************************************************************/ #include -#include #include #include +#include #include +#include #include -#include -#include #include "tests/utils/test_helper/test_helper.hpp" +#include -namespace { +namespace +{ bool verify_sandbox_options() { @@ -67,8 +68,8 @@ bool verify_sandbox_options() for (const gid_t expected_group : expected_supp_groups) { const bool found = std::find(groups.begin(), groups.begin() + count, expected_group) != groups.end(); - EXPECT_TRUE(found) << "Expected supplementary group " << expected_group << " not found (groups: [" - << count << " total)]"; + EXPECT_TRUE(found) << "Expected supplementary group " << expected_group << " not found (groups: [" << count + << " total)]"; if (!found) { all_pass = false; @@ -100,13 +101,15 @@ bool verify_sandbox_options() return all_pass; } -} // namespace +} // namespace -TEST(SandboxOptions, RunAndVerify) { +TEST(SandboxOptions, RunAndVerify) +{ ASSERT_TRUE(verify_sandbox_options()) << "Sandbox options verification failed"; score::mw::lifecycle::report_running(); } -int main() { +int main() +{ return TestRunner(__FILE__).RunTests(); -} +} \ No newline at end of file diff --git a/tests/utils/testing_utils/test_results.py b/tests/utils/testing_utils/test_results.py index 15934db40..15e097a97 100644 --- a/tests/utils/testing_utils/test_results.py +++ b/tests/utils/testing_utils/test_results.py @@ -11,30 +11,45 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* from pathlib import Path -from typing import Set +from typing import Iterable, Sequence, Set, Union import pytest from junitparser import JUnitXml -def download_xml_results(target, remote_dir: Path, local_dir: Path): - """Glob all .xml files in remote_dir, download them to local_dir, and delete - them from the remote. +def download_xml_results( + target, remote_dirs: Union[Path, str, Sequence[Union[Path, str]]], local_dir: Path +): + """Glob all .xml files in each of `remote_dirs`, download them to `local_dir`, + and delete them from the remote. + + `remote_dirs` may be a single directory or an iterable of directories. Each + directory is searched at most once, even if listed multiple times. """ - res, stdout = target.execute(f"ls {remote_dir}/*.xml") - assert res == 0, "Couldn't get list of files" - remote_xml_files = stdout.decode().strip().splitlines() - for remote_path in remote_xml_files: - remote_path = remote_path.strip() - if not remote_path: + if isinstance(remote_dirs, (str, Path)): + remote_dirs = [remote_dirs] + + seen: Set[str] = set() + for remote_dir in remote_dirs: + remote_dir = str(remote_dir) + if remote_dir in seen: continue - xml_name = Path(remote_path).name - try: - target.download(remote_path, str(local_dir / xml_name)) - target.execute(f"rm {remote_path}") - except Exception: - pass + seen.add(remote_dir) + res, stdout = target.execute(f"ls {remote_dir}/*.xml 2>/dev/null") + if res != 0: + continue + remote_xml_files = stdout.decode().strip().splitlines() + for remote_path in remote_xml_files: + remote_path = remote_path.strip() + if not remote_path: + continue + xml_name = Path(remote_path).name + try: + target.download(remote_path, str(local_dir / xml_name)) + target.execute(f"rm {remote_path}") + except Exception: + pass def get_failing_files(path: Path): """Collects all produced xml files and returns that set as well as the subset of failing files""" @@ -55,17 +70,25 @@ def assert_test_results(target, remote_test_dir, test_output_dir): count with no failures. Takes `target`, `remote_test_dir`, and `test_output_dir` from fixtures automatically. + By default only `remote_test_dir` is searched for XML result files. Tests + that configure processes with a different working directory can pass + `additional_search_dirs` so those locations are searched as well. + Usage:: def test_foo(assert_test_results, ...): ... - assert_test_results(expected_count=2) + assert_test_results({"foo.xml"}) + assert_test_results({"foo.xml"}, additional_search_dirs=["/tmp"]) """ - def _assert(expected_xml_files: Set[str]): + def _assert( + expected_xml_files: Set[str], additional_search_dirs: Iterable[Union[Path, str]] = () + ): # Show the error as coming from the call in the test rather than here __tracebackhide__ = True - download_xml_results(target, remote_test_dir, test_output_dir) + search_dirs = [remote_test_dir, *additional_search_dirs] + download_xml_results(target, search_dirs, test_output_dir) all_files, failing_files = get_failing_files(test_output_dir) assert all_files == expected_xml_files, ( # Set equality f"Didn't find the expected files, found: {all_files}" From bb15acf6be731cbc2614227dde895764dbfef25a Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 22 Jul 2026 16:39:12 +0200 Subject: [PATCH 06/37] Stabilize utility function --- tests/utils/testing_utils/run_until_file_deployed.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/utils/testing_utils/run_until_file_deployed.py b/tests/utils/testing_utils/run_until_file_deployed.py index 348964d7b..5e713a376 100644 --- a/tests/utils/testing_utils/run_until_file_deployed.py +++ b/tests/utils/testing_utils/run_until_file_deployed.py @@ -25,6 +25,7 @@ def run_until_file_deployed( file_path: str, timeout_s: float = 30.0, poll_interval_s: float = 0.5, + stop_timeout_s: float = 2.0, args=None, cwd: str = "/", ) -> AsyncProcess: @@ -35,6 +36,8 @@ def run_until_file_deployed( :param file_path: path of the file to wait for on the target. :param timeout_s: maximum seconds to wait for the file (default: 30). :param poll_interval_s: seconds between file-existence checks (default: 0.5). + :param stop_timeout_s: maximum seconds to wait for the process to terminate + after SIGTERM (default: 2). :param args: optional list of arguments to pass to the binary. :param cwd: working directory on the target (default: "/"). :return: the stopped :class:`AsyncProcess` handle. From 9117e31b87c2b30106dada16e3e36bdda89baab8 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 22 Jul 2026 17:05:06 +0200 Subject: [PATCH 07/37] Fix config mapping tests --- scripts/config_mapping/integration_tests.py | 1 + scripts/config_mapping/lifecycle_config.py | 10 +++++++--- .../tests/basic_test/expected_output/lm_demo.json | 5 +++++ .../tests/lm_config_test/expected_output/lm_demo.json | 6 ++++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/scripts/config_mapping/integration_tests.py b/scripts/config_mapping/integration_tests.py index b393a6ac1..0e147a56f 100644 --- a/scripts/config_mapping/integration_tests.py +++ b/scripts/config_mapping/integration_tests.py @@ -28,6 +28,7 @@ script_dir.parent.parent / "score" / "launch_manager" + / "src" / "daemon" / "src" / "configuration" diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 0bcebde10..49b467655 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -184,9 +184,13 @@ def dict_merge_recursive(dict_a, dict_b): merged_defaults["watchdog"], config.get("watchdog", {}) ) - new_config["initial_run_target"] = config.get( - "initial_run_target", merged_defaults["initial_run_target"] - ) + # Only emit initial_run_target when it can be derived from the input config + # or the defaults. This keeps preprocess_defaults usable with minimal + # defaults that don't define run-target keys. + if "initial_run_target" in config or "initial_run_target" in merged_defaults: + new_config["initial_run_target"] = config.get( + "initial_run_target", merged_defaults.get("initial_run_target") + ) if "fallback_run_target" in config: fallback_defaults = { diff --git a/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json b/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json index 660a30feb..ea0e5c0e5 100644 --- a/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json +++ b/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json @@ -20,6 +20,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, + "working_dir": "/tmp", "executable_reporting_behavior": "DoesNotReportExecutionState", "startup_config": [ { @@ -84,6 +85,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, + "working_dir": "/tmp", "executable_reporting_behavior": "DoesNotReportExecutionState", "startup_config": [ { @@ -152,6 +154,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, + "working_dir": "/tmp", "executable_reporting_behavior": "ReportsExecutionState", "startup_config": [ { @@ -219,6 +222,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, + "working_dir": "/tmp", "executable_reporting_behavior": "ReportsExecutionState", "startup_config": [ { @@ -295,6 +299,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, + "working_dir": "/tmp", "executable_reporting_behavior": "ReportsExecutionState", "function_cluster_affiliation": "STATE_MANAGEMENT", "startup_config": [ diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json b/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json index c90079231..8b8e5a5fb 100644 --- a/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json +++ b/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json @@ -17,6 +17,7 @@ ], "security_policy_details": "policy_name_2", "number_of_restart_attempts": 1, + "working_dir": "/tmp", "executable_reporting_behavior": "ReportsExecutionState", "startup_config": [ { @@ -92,6 +93,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, + "working_dir": "/tmp", "executable_reporting_behavior": "DoesNotReportExecutionState", "startup_config": [ { @@ -161,6 +163,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, + "working_dir": "/tmp", "executable_reporting_behavior": "DoesNotReportExecutionState", "startup_config": [ { @@ -229,6 +232,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, + "working_dir": "/tmp", "executable_reporting_behavior": "ReportsExecutionState", "startup_config": [ { @@ -296,6 +300,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, + "working_dir": "/tmp", "executable_reporting_behavior": "ReportsExecutionState", "startup_config": [ { @@ -372,6 +377,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, + "working_dir": "/tmp", "executable_reporting_behavior": "ReportsExecutionState", "function_cluster_affiliation": "STATE_MANAGEMENT", "startup_config": [ From d4f8e9b71626b7ec814abfc6ec89efd15421dbbb Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 23 Jul 2026 09:28:59 +0200 Subject: [PATCH 08/37] Add config mapping tests to the pipeline --- .github/workflows/test_and_docs.yml | 2 +- scripts/config_mapping/tests/BUILD | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test_and_docs.yml b/.github/workflows/test_and_docs.yml index c4dd425e7..346c5c269 100644 --- a/.github/workflows/test_and_docs.yml +++ b/.github/workflows/test_and_docs.yml @@ -44,7 +44,7 @@ jobs: contents: read pull-requests: read with: - bazel-target: 'test --lockfile_mode=error //score/... //tests/... --config=x86_64-linux' + bazel-target: 'test --lockfile_mode=error //score/... //tests/... //scripts/... --config=x86_64-linux' upload-name: 'bazel-testlogs' packages: 'fakechroot' build-docs: diff --git a/scripts/config_mapping/tests/BUILD b/scripts/config_mapping/tests/BUILD index 9bf775d3c..825fd6e40 100644 --- a/scripts/config_mapping/tests/BUILD +++ b/scripts/config_mapping/tests/BUILD @@ -32,7 +32,6 @@ score_py_pytest( "//score/launch_manager/src/daemon/src/configuration/config_schema:launch_manager.schema.json", "//scripts/config_mapping:lifecycle_config.py", ], - tags = ["manual"], deps = [ requirement("pytest"), requirement("jsonschema"), From 4a0421b538cd0ab0d0b7ea16fe54b0cf698a9892 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 23 Jul 2026 14:30:23 +0200 Subject: [PATCH 09/37] Add check for scheduling priority / policy --- .../sandbox_options/sandbox_options.py | 1 + .../sandbox_options_process.cpp | 83 +++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/tests/integration/sandbox_options/sandbox_options.py b/tests/integration/sandbox_options/sandbox_options.py index 1828f9c55..1019f8f81 100644 --- a/tests/integration/sandbox_options/sandbox_options.py +++ b/tests/integration/sandbox_options/sandbox_options.py @@ -31,6 +31,7 @@ def docker_configuration(): fully_verifies=[ "feat_req__lifecycle__uid_gid_support", "feat_req__lifecycle__launch_priority_support", + "feat_req__lifecycle__scheduling_policy", "feat_req__lifecycle__cwd_support", "feat_req__lifecycle__supplementary_groups" ], diff --git a/tests/integration/sandbox_options/sandbox_options_process.cpp b/tests/integration/sandbox_options/sandbox_options_process.cpp index eac5a95b8..2da3366de 100644 --- a/tests/integration/sandbox_options/sandbox_options_process.cpp +++ b/tests/integration/sandbox_options/sandbox_options_process.cpp @@ -12,12 +12,17 @@ ********************************************************************************/ #include +#include +#include #include #include #include #include +#include #include #include +#include +#include #include "tests/utils/test_helper/test_helper.hpp" #include @@ -25,6 +30,60 @@ namespace { +// Values configured for sandbox_options_process in sandbox_options.json. +constexpr int expected_policy = SCHED_FIFO; +constexpr int expected_priority = 10; + +const char* policy_name(const int policy) +{ + switch (policy) + { + case SCHED_FIFO: + return "SCHED_FIFO"; + case SCHED_RR: + return "SCHED_RR"; + case SCHED_OTHER: + return "SCHED_OTHER"; + default: + return "UNKNOWN"; + } +} + +/// @brief Verify that the calling thread runs with the expected scheduling policy and priority. +/// @param[in] context Human readable name of the thread, used in failure messages. +/// @param[in] file_prefix Prefix for the result files written for debugging. +/// @return true if the policy and priority match the configured values. +bool verify_scheduling(const std::string& context, const std::string& file_prefix) +{ + int policy = -1; + sched_param param{}; + const int rc = pthread_getschedparam(pthread_self(), &policy, ¶m); + EXPECT_EQ(rc, 0) << context << ": pthread_getschedparam failed rc=" << rc; + + std::ofstream policy_file{file_prefix + "_policy.txt"}; + policy_file << policy; + std::ofstream prio_file{file_prefix + "_priority.txt"}; + prio_file << param.sched_priority; + + bool pass = (rc == 0); + + EXPECT_EQ(policy, expected_policy) << context << ": Expected scheduling policy=" << policy_name(expected_policy) + << " but got " << policy_name(policy); + if (policy != expected_policy) + { + pass = false; + } + + EXPECT_EQ(param.sched_priority, expected_priority) + << context << ": Expected scheduling priority=" << expected_priority << " but got " << param.sched_priority; + if (param.sched_priority != expected_priority) + { + pass = false; + } + + return pass; +} + bool verify_sandbox_options() { bool all_pass = true; @@ -98,6 +157,30 @@ bool verify_sandbox_options() } } + TEST_STEP("Verify scheduling policy and priority in the main thread") + { + if (!verify_scheduling("main thread", "sandbox_sched_main")) + { + all_pass = false; + } + } + + TEST_STEP("Verify scheduling policy and priority in a spawned thread") + { + // A thread created with default attributes inherits the scheduling policy and + // priority of its creating thread, so the configured real-time settings must + // apply here as well. + std::atomic thread_pass{false}; + std::thread worker( + [&thread_pass]() { thread_pass = verify_scheduling("spawned thread", "sandbox_sched_thread"); }); + worker.join(); + + if (!thread_pass) + { + all_pass = false; + } + } + return all_pass; } From 9785173e7c927494f6404ef69c8acebbedff5084 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 23 Jul 2026 14:55:22 +0200 Subject: [PATCH 10/37] Remove not needed debug files --- .../sandbox_options_process.cpp | 38 +++++-------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/tests/integration/sandbox_options/sandbox_options_process.cpp b/tests/integration/sandbox_options/sandbox_options_process.cpp index 2da3366de..338f5dd56 100644 --- a/tests/integration/sandbox_options/sandbox_options_process.cpp +++ b/tests/integration/sandbox_options/sandbox_options_process.cpp @@ -15,12 +15,11 @@ #include #include #include -#include #include #include #include +#include #include -#include #include #include @@ -51,20 +50,14 @@ const char* policy_name(const int policy) /// @brief Verify that the calling thread runs with the expected scheduling policy and priority. /// @param[in] context Human readable name of the thread, used in failure messages. -/// @param[in] file_prefix Prefix for the result files written for debugging. /// @return true if the policy and priority match the configured values. -bool verify_scheduling(const std::string& context, const std::string& file_prefix) +bool verify_scheduling(const std::string& context) { int policy = -1; sched_param param{}; const int rc = pthread_getschedparam(pthread_self(), &policy, ¶m); EXPECT_EQ(rc, 0) << context << ": pthread_getschedparam failed rc=" << rc; - std::ofstream policy_file{file_prefix + "_policy.txt"}; - policy_file << policy; - std::ofstream prio_file{file_prefix + "_priority.txt"}; - prio_file << param.sched_priority; - bool pass = (rc == 0); EXPECT_EQ(policy, expected_policy) << context << ": Expected scheduling policy=" << policy_name(expected_policy) @@ -98,11 +91,6 @@ bool verify_sandbox_options() const uid_t current_uid = getuid(); const gid_t current_gid = getgid(); - std::ofstream uid_file{"sandbox_uid.txt"}; - uid_file << current_uid; - std::ofstream gid_file{"sandbox_gid.txt"}; - gid_file << current_gid; - EXPECT_EQ(current_uid, expected_uid) << "Expected uid=" << expected_uid << " but got uid=" << current_uid; EXPECT_EQ(current_gid, expected_gid) << "Expected gid=" << expected_gid << " but got gid=" << current_gid; @@ -118,12 +106,6 @@ bool verify_sandbox_options() const int count = getgroups(static_cast(groups.size()), groups.data()); EXPECT_GE(count, 0) << "Failed to get supplementary groups"; - std::ofstream supp_file{"sandbox_supp_groups.txt"}; - for (int i = 0; i < count; ++i) - { - supp_file << groups[i] << (i < count - 1 ? "," : ""); - } - for (const gid_t expected_group : expected_supp_groups) { const bool found = std::find(groups.begin(), groups.begin() + count, expected_group) != groups.end(); @@ -138,15 +120,12 @@ bool verify_sandbox_options() TEST_STEP("Verify working directory") { - char buf[PATH_MAX]; - char* result = getcwd(buf, sizeof(buf)); + std::array buf{}; + char* result = getcwd(buf.data(), buf.size()); EXPECT_NE(result, nullptr) << "Failed to get current working directory"; if (result) { - std::ofstream cwd_file{"sandbox_cwd.txt"}; - cwd_file << result; - EXPECT_EQ(std::string(result), expected_cwd) << "Expected working_dir=" << expected_cwd << " but got cwd=" << result; @@ -159,7 +138,7 @@ bool verify_sandbox_options() TEST_STEP("Verify scheduling policy and priority in the main thread") { - if (!verify_scheduling("main thread", "sandbox_sched_main")) + if (!verify_scheduling("main thread")) { all_pass = false; } @@ -167,12 +146,13 @@ bool verify_sandbox_options() TEST_STEP("Verify scheduling policy and priority in a spawned thread") { - // A thread created with default attributes inherits the scheduling policy and + // A thread created with default attributes inherits the schedulng policy and // priority of its creating thread, so the configured real-time settings must // apply here as well. std::atomic thread_pass{false}; - std::thread worker( - [&thread_pass]() { thread_pass = verify_scheduling("spawned thread", "sandbox_sched_thread"); }); + std::thread worker([&thread_pass]() { + thread_pass = verify_scheduling("spawned thread"); + }); worker.join(); if (!thread_pass) From ea0c3eb6cb8df9e147bb0898b24f1248b176f8eb Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 23 Jul 2026 18:51:13 +0200 Subject: [PATCH 11/37] Tiny modifications and comment --- MODULE.bazel | 4 +++- tests/integration/sandbox_options/control_daemon_mock.cpp | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 49eddd7a1..0d9aa2034 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -163,7 +163,9 @@ bazel_dep(name = "score_itf", version = "0.5.0", dev_dependency = True) # Patch the Docker plugin so tests can request extra container privileges # (e.g. cap_add=["SYS_NICE"], needed for SCHED_FIFO/sched_setscheduler) via the -# `docker_configuration` fixture. Remove once forwarding lands upstream. +# `docker_configuration` fixture. +# FIXME: With next release, this is no longer needed. +# See also https://github.com/eclipse-score/itf/blob/54cab4f6ebc1fb40d0b93a8c1a451a03bda77b6a/score/itf/plugins/docker.py#L381 single_version_override( module_name = "score_itf", patch_strip = 1, diff --git a/tests/integration/sandbox_options/control_daemon_mock.cpp b/tests/integration/sandbox_options/control_daemon_mock.cpp index d0c684303..00a7e9dd8 100644 --- a/tests/integration/sandbox_options/control_daemon_mock.cpp +++ b/tests/integration/sandbox_options/control_daemon_mock.cpp @@ -19,7 +19,7 @@ #include #include -TEST(Smoke, Daemon) +TEST(SandboxOptions, Daemon) { score::mw::lifecycle::ControlClient client{}; ASSERT_TRUE(check_clean({test_end_location})); From 144a472730fc44269a073c475a49e9c657523dfe Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 23 Jul 2026 19:08:06 +0200 Subject: [PATCH 12/37] Remove double replacement if working_dir is not set Before, when working_dir is not set it has been replaced with binary dir in c++ code. As this workaround is now done in python code, there is no need to keep this in c++ code. --- MODULE.bazel.lock | 4 +- .../details/process_launcher.cpp | 78 ++++++++----------- scripts/config_mapping/lifecycle_config.py | 4 +- 3 files changed, 35 insertions(+), 51 deletions(-) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 7eb61df3c..400be6002 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -968,8 +968,8 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_docs_as_code/4.6.1/MODULE.bazel": "0dae734ea8a99970a7417829b3115af02717b29f0fc40a8ebd3c1afcc71e1e21", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_docs_as_code/4.6.1/source.json": "3299daf219b15ba4aff6ca801385abebe0aaf364f71e391cd76cf9d38f3741f6", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_format_checker/0.1.1/MODULE.bazel": "1acc254faa90e9f97b79ac69af25b6c21c561f8d6079914f6352b9b20d26bd37", - "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_itf/0.5.0/MODULE.bazel": "d3ae40173462419b4e14e3fea96a49eab8822be7292f94890beb8cc73173eaa1", - "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_itf/0.5.0/source.json": "3c0fcd761e221bc33e6ebbeb0f92ce71b5d6f1f946b60ccf4fe7cf3a4c00d211", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_itf/0.3.0/MODULE.bazel": "0d47fb1832834deb35876147554dabdbb7c6d4dbbdf56ccc4d0be34967d7c5ee", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_itf/0.3.0/source.json": "86a6a4ba15e6208ea4396f5585de457bc674efb87590c1c636896ee5ef4a7e15", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_platform/0.1.0/MODULE.bazel": "cc9eae86e76f2a930510ed6e50ec991bb5661687e24881685b39c322087adf6f", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_platform/0.1.1/MODULE.bazel": "eb086ba99f9319371fbbd0a9252dfd27b0817039b88bd4d691602974b1ada005", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_platform/0.6.2/MODULE.bazel": "e88e7d086a08e1b451a68b60038b8b1f3c028aae7efc0bc9d8b39b6261ba040d", diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp index cded4120d..d9a27e8a6 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp @@ -22,15 +22,15 @@ #include #include +#include "score/mw/launch_manager/common/log.hpp" #include "score/mw/launch_manager/common/signal_safe_log.hpp" -#include "score/mw/launch_manager/process_group_manager/iprocess.hpp" #include "score/mw/launch_manager/control/control_client_channel.hpp" -#include "score/mw/launch_manager/common/log.hpp" #include "score/mw/launch_manager/osal/ipc_comms.hpp" #include "score/mw/launch_manager/osal/security_policy.hpp" #include "score/mw/launch_manager/osal/set_affinity.hpp" #include "score/mw/launch_manager/osal/set_groups.hpp" #include "score/mw/launch_manager/osal/sys_exit.hpp" +#include "score/mw/launch_manager/process_group_manager/iprocess.hpp" #include #include #include @@ -106,8 +106,7 @@ void handleComms(score::lcm::internal::osal::ChildProcessConfig& param) case CommsType::kReporting: if (-1 == fcntl(IpcCommsSync::sync_fd, F_SETFD, 0)) { - static_cast(signal_safe_log_errno(errno, - "fcntl at line ", __LINE__, " failed")); + static_cast(signal_safe_log_errno(errno, "fcntl at line ", __LINE__, " failed")); sysexit(EXIT_FAILURE); } close(IpcCommsSync::control_client_handler_nudge_fd); @@ -115,14 +114,12 @@ void handleComms(score::lcm::internal::osal::ChildProcessConfig& param) case CommsType::kControlClient: if (-1 == fcntl(IpcCommsSync::sync_fd, F_SETFD, 0)) { - static_cast(signal_safe_log_errno(errno, - "fcntl at line ", __LINE__, " failed")); + static_cast(signal_safe_log_errno(errno, "fcntl at line ", __LINE__, " failed")); sysexit(EXIT_FAILURE); } if (-1 == fcntl(IpcCommsSync::control_client_handler_nudge_fd, F_SETFD, 0)) { - static_cast(signal_safe_log_errno(errno, - "fcntl at line ", __LINE__, " failed")); + static_cast(signal_safe_log_errno(errno, "fcntl at line ", __LINE__, " failed")); sysexit(EXIT_FAILURE); } break; @@ -130,7 +127,10 @@ void handleComms(score::lcm::internal::osal::ChildProcessConfig& param) // nothing to do here break; default: - static_cast(signal_safe_log("at line ", __LINE__, " unknown CommsType ", + static_cast(signal_safe_log( + "at line ", + __LINE__, + " unknown CommsType ", static_cast(param.shared_block->comms_type_))); sysexit(EXIT_FAILURE); break; @@ -140,33 +140,11 @@ void handleComms(score::lcm::internal::osal::ChildProcessConfig& param) /// @details The implementation should be async signal safe. void changeCurrentWorkingDirectory(const score::lcm::internal::osal::OsalConfig& config) { - if (!config.working_dir_.empty()) - { - if (-1 == chdir(config.working_dir_.c_str())) - { - LM_LOG_ERROR() << "[New process] chdir(" << config.working_dir_ << ") failed:" << score::lcm::internal::errno_message(errno); - sysexit(EXIT_FAILURE); - } - return; - } - - // Change current working directory to the same as the executable - constexpr size_t string_size = static_cast(PATH_MAX); - // Notice that this next static variable is duplicated by the fork() and so does not need - // any protection by a mutex although at first sight you may think it could need one. - static char path_copy[string_size + 1U] = {0}; - - if (config.executable_path_.size() >= string_size) + // working_dir_ is set by python configuration generator in lifecycle_config.py, so it should always be valid. + // If not, chdir will fail anyway and we will log an error and exit. + if (-1 == chdir(config.working_dir_.c_str())) { - static_cast(signal_safe_log("executable path longer than ", - string_size, " chars: ", config.executable_path_)); - sysexit(EXIT_FAILURE); - } - - if (-1 == chdir(dirname(strncpy(path_copy, config.executable_path_.c_str(), string_size)))) - { - static_cast(signal_safe_log_errno(errno, - "chdir(", config.executable_path_, ") failed")); + static_cast(signal_safe_log_errno(errno, "chdir(", config.working_dir_, ") failed.")); sysexit(EXIT_FAILURE); } } @@ -192,8 +170,8 @@ void changeSecurityPolicy(const score::lcm::internal::osal::OsalConfig& config) { if (score::lcm::internal::osal::setSecurityPolicy(config.security_policy_.c_str()) != 0) { - static_cast(signal_safe_log_errno(errno, - "changeSecurityPolicy(", config.security_policy_, ") failed")); + static_cast( + signal_safe_log_errno(errno, "changeSecurityPolicy(", config.security_policy_, ") failed")); sysexit(EXIT_FAILURE); } } @@ -221,8 +199,7 @@ OsalReturnType IProcess::startProcess(ProcessID* pid, IpcCommsP* block, const Os { if (access(config->executable_path_.c_str(), X_OK) != 0) { - static_cast(signal_safe_log( - "File does not exist or is not executable: ", config->executable_path_)); + static_cast(signal_safe_log("File does not exist or is not executable: ", config->executable_path_)); return result; } @@ -297,7 +274,8 @@ inline bool IProcess::setupComms(IpcCommsP& block, int& fd, const OsalConfig& co length += sizeof(ControlClientChannel); } - static_cast(snprintf(shm_name, + static_cast(snprintf( + shm_name, static_cast(score::lcm::internal::ProcessLimits::maxLocalBuffSize), "/ipc_shared_mem%u", shm_name_counter++)); @@ -399,14 +377,22 @@ OsalReturnType IProcess::setSchedulingAndSecurity(const OsalConfig& config) if (sch_param.sched_priority < sched_get_priority_min(config.scheduling_policy_)) { - static_cast(signal_safe_log("Scheduling priority ", sch_param.sched_priority, - " is below minimum for policy ", config.scheduling_policy_, ", setting to minimum")); + static_cast(signal_safe_log( + "Scheduling priority ", + sch_param.sched_priority, + " is below minimum for policy ", + config.scheduling_policy_, + ", setting to minimum")); sch_param.sched_priority = sched_get_priority_min(config.scheduling_policy_); } else if (sch_param.sched_priority > sched_get_priority_max(config.scheduling_policy_)) { - static_cast(signal_safe_log("Scheduling priority ", sch_param.sched_priority, - " is above maximum for policy ", config.scheduling_policy_, ", setting to maximum")); + static_cast(signal_safe_log( + "Scheduling priority ", + sch_param.sched_priority, + " is above maximum for policy ", + config.scheduling_policy_, + ", setting to maximum")); sch_param.sched_priority = sched_get_priority_max(config.scheduling_policy_); } @@ -470,8 +456,8 @@ inline void IProcess::handleChildProcess(ChildProcessConfig& param) // arguments.", true); if (-1 == execve(param.config->argv_[0], const_cast(param.config->argv_.data()), param.config->envp_)) { - static_cast(signal_safe_log_errno(errno, "execve failed: Unable to execute the ", - param.config->executable_path_, " app.")); + static_cast(signal_safe_log_errno( + errno, "execve failed: Unable to execute the ", param.config->executable_path_, " app.")); sysexit(EXIT_FAILURE); } } diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 49b467655..e0ffec16b 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -293,9 +293,7 @@ def gen_config(output_dir, config, input_filename, schema_version=None): "shutdown_timeout": depl_cfg["shutdown_timeout"], "bin_dir": depl_cfg["bin_dir"], # Default the working directory to bin_dir (the directory the - # executable lives in) when not set explicitly. This matches the - # launch manager's own fallback of chdir'ing to the executable's - # directory when no working_dir is configured. + # executable lives in) when not set explicitly. "working_dir": depl_cfg.get("working_dir", depl_cfg["bin_dir"]), "sandbox": sandbox_out, } From d91475c76c3e5da4846ad027d11c68ec58cfe260 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 24 Jul 2026 08:45:28 +0200 Subject: [PATCH 13/37] Fix formatting --- MODULE.bazel | 2 +- scripts/config_mapping/lifecycle_config.py | 3 +-- tests/integration/sandbox_options/sandbox_options.py | 4 ++-- tests/utils/testing_utils/test_results.py | 4 +++- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 0d9aa2034..9092e56e1 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -163,7 +163,7 @@ bazel_dep(name = "score_itf", version = "0.5.0", dev_dependency = True) # Patch the Docker plugin so tests can request extra container privileges # (e.g. cap_add=["SYS_NICE"], needed for SCHED_FIFO/sched_setscheduler) via the -# `docker_configuration` fixture. +# `docker_configuration` fixture. # FIXME: With next release, this is no longer needed. # See also https://github.com/eclipse-score/itf/blob/54cab4f6ebc1fb40d0b93a8c1a451a03bda77b6a/score/itf/plugins/docker.py#L381 single_version_override( diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index e0ffec16b..cf1fc8648 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -674,8 +674,7 @@ def get_terminating_behavior(component_config): "ready_recovery_action" ]["restart"]["number_of_attempts"] process["working_dir"] = component_config["deployment_config"].get( - "working_dir", - component_config["deployment_config"]["bin_dir"] + "working_dir", component_config["deployment_config"]["bin_dir"] ) match component_config["component_properties"]["application_profile"][ diff --git a/tests/integration/sandbox_options/sandbox_options.py b/tests/integration/sandbox_options/sandbox_options.py index 1019f8f81..068e0f5a6 100644 --- a/tests/integration/sandbox_options/sandbox_options.py +++ b/tests/integration/sandbox_options/sandbox_options.py @@ -33,7 +33,7 @@ def docker_configuration(): "feat_req__lifecycle__launch_priority_support", "feat_req__lifecycle__scheduling_policy", "feat_req__lifecycle__cwd_support", - "feat_req__lifecycle__supplementary_groups" + "feat_req__lifecycle__supplementary_groups", ], partially_verifies=[], test_type="interface-test", @@ -42,7 +42,7 @@ def docker_configuration(): def test_sandbox_options(target, setup_test, assert_test_results, remote_test_dir): """ Objective: Verifies the effectiveness of sandbox-options as gid, uid, supplementary groups, and scheduling policy. - + The launch manager starts with an initial run target. The control daemon activates the "Running" run target (starting the managed process with the sandbox options applied), then transitions back to "Startup", and finally activates "Off". Expected Behaviour: All run target transitions complete successfully and all processes report running. """ diff --git a/tests/utils/testing_utils/test_results.py b/tests/utils/testing_utils/test_results.py index 15e097a97..083a7aea0 100644 --- a/tests/utils/testing_utils/test_results.py +++ b/tests/utils/testing_utils/test_results.py @@ -51,6 +51,7 @@ def download_xml_results( except Exception: pass + def get_failing_files(path: Path): """Collects all produced xml files and returns that set as well as the subset of failing files""" failing_files = set() @@ -83,7 +84,8 @@ def test_foo(assert_test_results, ...): """ def _assert( - expected_xml_files: Set[str], additional_search_dirs: Iterable[Union[Path, str]] = () + expected_xml_files: Set[str], + additional_search_dirs: Iterable[Union[Path, str]] = (), ): # Show the error as coming from the call in the test rather than here __tracebackhide__ = True From a632a38a61c16c0a552b61108cef9efb7a09d5fe Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 24 Jul 2026 11:44:40 +0200 Subject: [PATCH 14/37] Revert no longer needed patch for score_itf With new version 0.5.0 this is no longer needed. --- MODULE.bazel | 12 ------------ patches/score_itf_docker_cap_add.patch | 11 ----------- 2 files changed, 23 deletions(-) delete mode 100644 patches/score_itf_docker_cap_add.patch diff --git a/MODULE.bazel b/MODULE.bazel index 9092e56e1..339213741 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -161,18 +161,6 @@ use_repo(pip, "score_lifecycle_pip") bazel_dep(name = "score_itf", version = "0.5.0", dev_dependency = True) -# Patch the Docker plugin so tests can request extra container privileges -# (e.g. cap_add=["SYS_NICE"], needed for SCHED_FIFO/sched_setscheduler) via the -# `docker_configuration` fixture. -# FIXME: With next release, this is no longer needed. -# See also https://github.com/eclipse-score/itf/blob/54cab4f6ebc1fb40d0b93a8c1a451a03bda77b6a/score/itf/plugins/docker.py#L381 -single_version_override( - module_name = "score_itf", - patch_strip = 1, - patches = ["//patches:score_itf_docker_cap_add.patch"], - version = "0.3.0", -) - oci = use_extension("@rules_oci//oci:extensions.bzl", "oci", dev_dependency = True) oci.pull( name = "debian-test-runtime", diff --git a/patches/score_itf_docker_cap_add.patch b/patches/score_itf_docker_cap_add.patch deleted file mode 100644 index f2a372799..000000000 --- a/patches/score_itf_docker_cap_add.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- a/score/itf/plugins/docker.py -+++ b/score/itf/plugins/docker.py -@@ -331,6 +331,8 @@ def target_init(request, _docker_configuration): - environment=_docker_configuration["environment"], - volumes=_docker_configuration["volumes"], - shm_size=_docker_configuration["shm_size"], -+ privileged=_docker_configuration.get("privileged", False), -+ cap_add=_docker_configuration.get("cap_add"), - ) - target = None - try: From 9d0bbcb10b19e12cae14dfad9cd86e2cb874b099 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 24 Jul 2026 14:53:33 +0200 Subject: [PATCH 15/37] Test different variations of working_dir presence Two test modifications: - If no working_dir is defined, bin directory is used. - A working dir defined on deployment config level can be overridden on component level --- .../tests/basic_test/expected_output/lm_demo.json | 10 +++++----- .../tests/basic_test/input/lm_config.json | 1 - .../tests/lm_config_test/expected_output/lm_demo.json | 2 +- .../tests/lm_config_test/input/lm_config.json | 3 ++- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json b/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json index ea0e5c0e5..1878015f6 100644 --- a/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json +++ b/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json @@ -20,7 +20,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, - "working_dir": "/tmp", + "working_dir": "/opt/scripts", "executable_reporting_behavior": "DoesNotReportExecutionState", "startup_config": [ { @@ -85,7 +85,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, - "working_dir": "/tmp", + "working_dir": "/opt/apps/dlt-daemon", "executable_reporting_behavior": "DoesNotReportExecutionState", "startup_config": [ { @@ -154,7 +154,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, - "working_dir": "/tmp", + "working_dir": "/opt/apps/someip", "executable_reporting_behavior": "ReportsExecutionState", "startup_config": [ { @@ -222,7 +222,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, - "working_dir": "/tmp", + "working_dir": "/opt/apps/test_app1", "executable_reporting_behavior": "ReportsExecutionState", "startup_config": [ { @@ -299,7 +299,7 @@ ], "security_policy_details": "policy_name", "number_of_restart_attempts": 1, - "working_dir": "/tmp", + "working_dir": "/opt/apps/state_manager", "executable_reporting_behavior": "ReportsExecutionState", "function_cluster_affiliation": "STATE_MANAGEMENT", "startup_config": [ diff --git a/scripts/config_mapping/tests/basic_test/input/lm_config.json b/scripts/config_mapping/tests/basic_test/input/lm_config.json index b35597f9e..ab8402597 100644 --- a/scripts/config_mapping/tests/basic_test/input/lm_config.json +++ b/scripts/config_mapping/tests/basic_test/input/lm_config.json @@ -10,7 +10,6 @@ "EMPTY_GLOBAL_ENV_VAR": "" }, "bin_dir": "/opt", - "working_dir": "/tmp", "ready_recovery_action": { "restart": { "number_of_attempts": 1, diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json b/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json index 8b8e5a5fb..9966f9104 100644 --- a/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json +++ b/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json @@ -17,7 +17,7 @@ ], "security_policy_details": "policy_name_2", "number_of_restart_attempts": 1, - "working_dir": "/tmp", + "working_dir": "/opt/apps/test_app2", "executable_reporting_behavior": "ReportsExecutionState", "startup_config": [ { diff --git a/scripts/config_mapping/tests/lm_config_test/input/lm_config.json b/scripts/config_mapping/tests/lm_config_test/input/lm_config.json index f825876f8..90007280a 100644 --- a/scripts/config_mapping/tests/lm_config_test/input/lm_config.json +++ b/scripts/config_mapping/tests/lm_config_test/input/lm_config.json @@ -85,7 +85,8 @@ "scheduling_priority": 99, "max_memory_usage": 2048, "max_cpu_usage": 50 - } + }, + "working_dir": "/opt/apps/test_app2" } }, "setup_filesystem_sh": { From 0f364a53718dd5a98521dd32356869c2242fae83 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 24 Jul 2026 15:37:52 +0200 Subject: [PATCH 16/37] Parameterize sandbox_options_process executable --- .../sandbox_options/sandbox_options.json | 10 +- .../sandbox_options_process.cpp | 157 +++++++++++++++--- 2 files changed, 145 insertions(+), 22 deletions(-) diff --git a/tests/integration/sandbox_options/sandbox_options.json b/tests/integration/sandbox_options/sandbox_options.json index 38dcfeac3..dc35e08d8 100644 --- a/tests/integration/sandbox_options/sandbox_options.json +++ b/tests/integration/sandbox_options/sandbox_options.json @@ -65,7 +65,15 @@ "binary_name": "sandbox_options_process", "application_profile": { "application_type": "Reporting" - } + }, + "process_arguments": [ + "--uid=666", + "--gid=666", + "--supplementary-groups=123,321,456", + "--scheduling-policy=SCHED_FIFO", + "--scheduling-priority=10", + "--working-dir=/tmp" + ] }, "deployment_config": { "environmental_variables": { diff --git a/tests/integration/sandbox_options/sandbox_options_process.cpp b/tests/integration/sandbox_options/sandbox_options_process.cpp index 338f5dd56..dfa3081a1 100644 --- a/tests/integration/sandbox_options/sandbox_options_process.cpp +++ b/tests/integration/sandbox_options/sandbox_options_process.cpp @@ -20,8 +20,12 @@ #include #include #include +#include +#include #include +#include #include +#include #include "tests/utils/test_helper/test_helper.hpp" #include @@ -29,9 +33,23 @@ namespace { -// Values configured for sandbox_options_process in sandbox_options.json. -constexpr int expected_policy = SCHED_FIFO; -constexpr int expected_priority = 10; +/// @brief Expected sandbox option values, supplied to this process on the command line. +/// +/// The launch manager applies the sandbox options from sandbox_options.json and passes the same +/// values to the managed process via 'process_arguments', so the test verifies the applied state +/// against the configured expectation without duplicating any literals here. +struct ExpectedValues +{ + int policy = SCHED_OTHER; + int priority = 0; + uid_t uid = 0; + gid_t gid = 0; + std::vector supplementary_groups{}; + std::string working_dir{}; +}; + +// Populated from argv in main() before the tests run. +ExpectedValues expected; const char* policy_name(const int policy) { @@ -48,6 +66,101 @@ const char* policy_name(const int policy) } } +int parse_policy(const std::string& name) +{ + if (name == "SCHED_FIFO") + { + return SCHED_FIFO; + } + if (name == "SCHED_RR") + { + return SCHED_RR; + } + if (name == "SCHED_OTHER") + { + return SCHED_OTHER; + } + return -1; +} + +// Parse a comma separated list of group ids, e.g. "123,321,456". +std::vector parse_groups(const std::string& csv) +{ + std::vector groups; + std::stringstream stream(csv); + std::string token; + while (std::getline(stream, token, ',')) + { + if (!token.empty()) + { + groups.push_back(static_cast(std::stoul(token))); + } + } + return groups; +} + +// If 'arg' has the form "--=", store the value in 'value' and return true. +bool match_option(const std::string& arg, const std::string_view key, std::string& value) +{ + const std::string prefix = "--" + std::string(key) + "="; + if (arg.rfind(prefix, 0) == 0) + { + value = arg.substr(prefix.size()); + return true; + } + return false; +} + +// Populate 'expected' from the command line arguments. Returns true if every expected value was +// provided. +bool parse_arguments(int argc, char** argv, ExpectedValues& out) +{ + bool has_policy = false; + bool has_priority = false; + bool has_uid = false; + bool has_gid = false; + bool has_groups = false; + bool has_working_dir = false; + + std::string value; + for (int i = 1; i < argc; ++i) + { + const std::string arg = argv[i]; + if (match_option(arg, "uid", value)) + { + out.uid = static_cast(std::stoul(value)); + has_uid = true; + } + else if (match_option(arg, "gid", value)) + { + out.gid = static_cast(std::stoul(value)); + has_gid = true; + } + else if (match_option(arg, "supplementary-groups", value)) + { + out.supplementary_groups = parse_groups(value); + has_groups = true; + } + else if (match_option(arg, "scheduling-policy", value)) + { + out.policy = parse_policy(value); + has_policy = true; + } + else if (match_option(arg, "scheduling-priority", value)) + { + out.priority = std::stoi(value); + has_priority = true; + } + else if (match_option(arg, "working-dir", value)) + { + out.working_dir = value; + has_working_dir = true; + } + } + + return has_policy && has_priority && has_uid && has_gid && has_groups && has_working_dir; +} + /// @brief Verify that the calling thread runs with the expected scheduling policy and priority. /// @param[in] context Human readable name of the thread, used in failure messages. /// @return true if the policy and priority match the configured values. @@ -60,16 +173,16 @@ bool verify_scheduling(const std::string& context) bool pass = (rc == 0); - EXPECT_EQ(policy, expected_policy) << context << ": Expected scheduling policy=" << policy_name(expected_policy) + EXPECT_EQ(policy, expected.policy) << context << ": Expected scheduling policy=" << policy_name(expected.policy) << " but got " << policy_name(policy); - if (policy != expected_policy) + if (policy != expected.policy) { pass = false; } - EXPECT_EQ(param.sched_priority, expected_priority) - << context << ": Expected scheduling priority=" << expected_priority << " but got " << param.sched_priority; - if (param.sched_priority != expected_priority) + EXPECT_EQ(param.sched_priority, expected.priority) + << context << ": Expected scheduling priority=" << expected.priority << " but got " << param.sched_priority; + if (param.sched_priority != expected.priority) { pass = false; } @@ -81,20 +194,15 @@ bool verify_sandbox_options() { bool all_pass = true; - const uid_t expected_uid = 666; - const gid_t expected_gid = 666; - const std::array expected_supp_groups = {123, 321, 456}; - const std::string expected_cwd = "/tmp"; - TEST_STEP("Verify uid and gid") { const uid_t current_uid = getuid(); const gid_t current_gid = getgid(); - EXPECT_EQ(current_uid, expected_uid) << "Expected uid=" << expected_uid << " but got uid=" << current_uid; - EXPECT_EQ(current_gid, expected_gid) << "Expected gid=" << expected_gid << " but got gid=" << current_gid; + EXPECT_EQ(current_uid, expected.uid) << "Expected uid=" << expected.uid << " but got uid=" << current_uid; + EXPECT_EQ(current_gid, expected.gid) << "Expected gid=" << expected.gid << " but got gid=" << current_gid; - if (current_uid != expected_uid || current_gid != expected_gid) + if (current_uid != expected.uid || current_gid != expected.gid) { all_pass = false; } @@ -106,7 +214,7 @@ bool verify_sandbox_options() const int count = getgroups(static_cast(groups.size()), groups.data()); EXPECT_GE(count, 0) << "Failed to get supplementary groups"; - for (const gid_t expected_group : expected_supp_groups) + for (const gid_t expected_group : expected.supplementary_groups) { const bool found = std::find(groups.begin(), groups.begin() + count, expected_group) != groups.end(); EXPECT_TRUE(found) << "Expected supplementary group " << expected_group << " not found (groups: [" << count @@ -126,10 +234,10 @@ bool verify_sandbox_options() if (result) { - EXPECT_EQ(std::string(result), expected_cwd) - << "Expected working_dir=" << expected_cwd << " but got cwd=" << result; + EXPECT_EQ(std::string(result), expected.working_dir) + << "Expected working_dir=" << expected.working_dir << " but got cwd=" << result; - if (std::string(result) != expected_cwd) + if (std::string(result) != expected.working_dir) { all_pass = false; } @@ -172,7 +280,14 @@ TEST(SandboxOptions, RunAndVerify) score::mw::lifecycle::report_running(); } -int main() +int main(int argc, char** argv) { + if (!parse_arguments(argc, argv, expected)) + { + std::cerr << "Missing expected sandbox option(s) on the command line. Required: --uid, --gid, " + "--supplementary-groups, --scheduling-policy, --scheduling-priority, --working-dir" + << std::endl; + return 1; + } return TestRunner(__FILE__).RunTests(); } \ No newline at end of file From aabaacf7c2638ee27d3651d642581badfa7f0745 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 24 Jul 2026 15:42:49 +0200 Subject: [PATCH 17/37] Remove exports_files, which has been added by fault --- tests/integration/sandbox_options/BUILD | 5 ----- tests/integration/smoke/BUILD | 5 ----- 2 files changed, 10 deletions(-) diff --git a/tests/integration/sandbox_options/BUILD b/tests/integration/sandbox_options/BUILD index 08bba667a..2857a0fed 100644 --- a/tests/integration/sandbox_options/BUILD +++ b/tests/integration/sandbox_options/BUILD @@ -13,11 +13,6 @@ load("@rules_cc//cc:defs.bzl", "cc_binary") load("//tests/utils/bazel:integration.bzl", "integration_test") -exports_files( - ["lm_demo.json"], - visibility = ["//examples:__subpackages__"], -) - cc_binary( name = "control_daemon_mock", srcs = ["control_daemon_mock.cpp"], diff --git a/tests/integration/smoke/BUILD b/tests/integration/smoke/BUILD index 506497c00..127c143a4 100644 --- a/tests/integration/smoke/BUILD +++ b/tests/integration/smoke/BUILD @@ -13,11 +13,6 @@ load("@rules_cc//cc:defs.bzl", "cc_binary") load("//tests/utils/bazel:integration.bzl", "integration_test") -exports_files( - ["lm_demo.json"], - visibility = ["//examples:__subpackages__"], -) - cc_binary( name = "control_daemon_mock", srcs = ["control_daemon_mock.cpp"], From 78a784e3c3262e3d35b8b278d7cc6f54632a2dab Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 24 Jul 2026 16:23:33 +0200 Subject: [PATCH 18/37] kill with SIGKILL if process does not react on SIGTERM --- tests/utils/testing_utils/run_until_file_deployed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/testing_utils/run_until_file_deployed.py b/tests/utils/testing_utils/run_until_file_deployed.py index 5e713a376..85efbc121 100644 --- a/tests/utils/testing_utils/run_until_file_deployed.py +++ b/tests/utils/testing_utils/run_until_file_deployed.py @@ -35,7 +35,7 @@ def run_until_file_deployed( :param binary_path: path to the binary to execute on the target. :param file_path: path of the file to wait for on the target. :param timeout_s: maximum seconds to wait for the file (default: 30). - :param poll_interval_s: seconds between file-existence checks (default: 0.5). + :param poll_interval_s: seconds between file and process checks (default: 0.5). :param stop_timeout_s: maximum seconds to wait for the process to terminate after SIGTERM (default: 2). :param args: optional list of arguments to pass to the binary. From 9f5f0f1bc100dec3b3c1703272c26f4c84b28e8d Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Mon, 27 Jul 2026 12:41:04 +0200 Subject: [PATCH 19/37] Test more scheduling params - add another process - parameterize sandbox_options_process.cpp --- tests/integration/sandbox_options/BUILD | 19 +- .../sandbox_options/sandbox_options.json | 32 ++- .../sandbox_options/sandbox_options.py | 13 +- .../sandbox_options_process.cpp | 213 +++++++++++------- 4 files changed, 186 insertions(+), 91 deletions(-) diff --git a/tests/integration/sandbox_options/BUILD b/tests/integration/sandbox_options/BUILD index 2857a0fed..018c71319 100644 --- a/tests/integration/sandbox_options/BUILD +++ b/tests/integration/sandbox_options/BUILD @@ -25,8 +25,22 @@ cc_binary( ], ) +# Two binaries are built from the same source. Each verifies the sandbox options passed to it on +# the command line (see sandbox_options.json), and derives its GTest XML result file name from its +# own executable name so the results do not collide. cc_binary( - name = "sandbox_options_process", + name = "sandbox_options_process_a", + srcs = ["sandbox_options_process.cpp"], + deps = [ + "//score/launch_manager:control_cc", + "//score/launch_manager:lifecycle_cc", + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) + +cc_binary( + name = "sandbox_options_process_b", srcs = ["sandbox_options_process.cpp"], deps = [ "//score/launch_manager:control_cc", @@ -41,7 +55,8 @@ integration_test( srcs = ["sandbox_options.py"], binaries = [ ":control_daemon_mock", - ":sandbox_options_process", + ":sandbox_options_process_a", + ":sandbox_options_process_b", "//score/launch_manager", ], config = ":sandbox_options.json", diff --git a/tests/integration/sandbox_options/sandbox_options.json b/tests/integration/sandbox_options/sandbox_options.json index dc35e08d8..5b22e9127 100644 --- a/tests/integration/sandbox_options/sandbox_options.json +++ b/tests/integration/sandbox_options/sandbox_options.json @@ -60,9 +60,9 @@ } } }, - "sandbox_options_process": { + "sandbox_options_process_a": { "component_properties": { - "binary_name": "sandbox_options_process", + "binary_name": "sandbox_options_process_a", "application_profile": { "application_type": "Reporting" }, @@ -88,6 +88,31 @@ }, "working_dir": "/tmp" } + }, + "sandbox_options_process_b": { + "component_properties": { + "binary_name": "sandbox_options_process_b", + "application_profile": { + "application_type": "Reporting" + }, + "process_arguments": [ + "--uid=0", + "--gid=0", + "--supplementary-groups=", + "--scheduling-policy=SCHED_RR" + ] + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "DefaultPG_app1" + }, + "sandbox": { + "uid": 0, + "gid": 0, + "supplementary_group_ids": [], + "scheduling_policy": "SCHED_RR" + } + } } }, "run_targets": { @@ -104,7 +129,8 @@ "Running": { "depends_on": [ "control_daemon", - "sandbox_options_process" + "sandbox_options_process_a", + "sandbox_options_process_b" ], "recovery_action": { "switch_run_target": { diff --git a/tests/integration/sandbox_options/sandbox_options.py b/tests/integration/sandbox_options/sandbox_options.py index 068e0f5a6..2a00c7bf6 100644 --- a/tests/integration/sandbox_options/sandbox_options.py +++ b/tests/integration/sandbox_options/sandbox_options.py @@ -61,10 +61,15 @@ def test_sandbox_options(target, setup_test, assert_test_results, remote_test_di timeout_s=3.0, ) - # The managed process is configured with a custom working directory (see the - # "working_dir" entries in sandbox_options.json), so its XML result file is - # written there rather than into remote_test_dir. Search those directories too. + # sandbox_options_process_a is configured with a custom working directory ("working_dir": "/tmp" + # in sandbox_options.json), so its XML result file is written there rather than into + # remote_test_dir. sandbox_options_process_b has no working directory configured and therefore + # writes into remote_test_dir. Search all relevant directories. assert_test_results( - {"control_daemon_mock.xml", "sandbox_options_process.xml"}, + { + "control_daemon_mock.xml", + "sandbox_options_process_a.xml", + "sandbox_options_process_b.xml", + }, additional_search_dirs=["/tmp/tests/sandbox_options", "/tmp"], ) diff --git a/tests/integration/sandbox_options/sandbox_options_process.cpp b/tests/integration/sandbox_options/sandbox_options_process.cpp index dfa3081a1..bae967e7c 100644 --- a/tests/integration/sandbox_options/sandbox_options_process.cpp +++ b/tests/integration/sandbox_options/sandbox_options_process.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -38,14 +39,17 @@ namespace /// The launch manager applies the sandbox options from sandbox_options.json and passes the same /// values to the managed process via 'process_arguments', so the test verifies the applied state /// against the configured expectation without duplicating any literals here. +/// +/// Every value is optional: a value that is not passed on the command line is not verified. This +/// lets a component leave an option unset (e.g. no working directory) without the test flagging it. struct ExpectedValues { - int policy = SCHED_OTHER; - int priority = 0; - uid_t uid = 0; - gid_t gid = 0; - std::vector supplementary_groups{}; - std::string working_dir{}; + std::optional policy; + std::optional priority; + std::optional uid; + std::optional gid; + std::optional> supplementary_groups; + std::optional working_dir; }; // Populated from argv in main() before the tests run. @@ -111,16 +115,12 @@ bool match_option(const std::string& arg, const std::string_view key, std::strin return false; } -// Populate 'expected' from the command line arguments. Returns true if every expected value was -// provided. +// Populate 'out' from the command line arguments. Only the options that are actually present are +// set; any option left out stays unset and is therefore not verified. Returns false if an +// unrecognized option is encountered. bool parse_arguments(int argc, char** argv, ExpectedValues& out) { - bool has_policy = false; - bool has_priority = false; - bool has_uid = false; - bool has_gid = false; - bool has_groups = false; - bool has_working_dir = false; + bool all_recognized = true; std::string value; for (int i = 1; i < argc; ++i) @@ -129,60 +129,77 @@ bool parse_arguments(int argc, char** argv, ExpectedValues& out) if (match_option(arg, "uid", value)) { out.uid = static_cast(std::stoul(value)); - has_uid = true; } else if (match_option(arg, "gid", value)) { out.gid = static_cast(std::stoul(value)); - has_gid = true; } else if (match_option(arg, "supplementary-groups", value)) { out.supplementary_groups = parse_groups(value); - has_groups = true; } else if (match_option(arg, "scheduling-policy", value)) { out.policy = parse_policy(value); - has_policy = true; } else if (match_option(arg, "scheduling-priority", value)) { out.priority = std::stoi(value); - has_priority = true; } else if (match_option(arg, "working-dir", value)) { out.working_dir = value; - has_working_dir = true; + } + else + { + std::cerr << "Unrecognized argument: " << arg << std::endl; + all_recognized = false; } } - return has_policy && has_priority && has_uid && has_gid && has_groups && has_working_dir; + return all_recognized; } /// @brief Verify that the calling thread runs with the expected scheduling policy and priority. /// @param[in] context Human readable name of the thread, used in failure messages. -/// @return true if the policy and priority match the configured values. +/// @return true if the policy (when provided) and the priority match. bool verify_scheduling(const std::string& context) { int policy = -1; sched_param param{}; - const int rc = pthread_getschedparam(pthread_self(), &policy, ¶m); - EXPECT_EQ(rc, 0) << context << ": pthread_getschedparam failed rc=" << rc; + const int result = pthread_getschedparam(pthread_self(), &policy, ¶m); + EXPECT_EQ(result, 0) << context << ": pthread_getschedparam failed rc=" << result; + if (result != 0) + { + // 'policy' was not written, so it is still the sentinel -1. Bail out before it can reach + // sched_get_priority_min() below (which would return -1 and set errno=EINVAL). + return false; + } - bool pass = (rc == 0); + bool pass = true; - EXPECT_EQ(policy, expected.policy) << context << ": Expected scheduling policy=" << policy_name(expected.policy) - << " but got " << policy_name(policy); - if (policy != expected.policy) + if (expected.policy.has_value()) { - pass = false; + EXPECT_EQ(policy, *expected.policy) + << context << ": Expected scheduling policy=" << policy_name(*expected.policy) << " but got " + << policy_name(policy); + if (policy != *expected.policy) + { + pass = false; + } } - EXPECT_EQ(param.sched_priority, expected.priority) - << context << ": Expected scheduling priority=" << expected.priority << " but got " << param.sched_priority; - if (param.sched_priority != expected.priority) + // The priority is always verified. When no explicit priority is provided on the command line, + // verify against the default the launch manager ends up applying: its configured default (0) + // clamped up to the policy minimum, i.e. sched_get_priority_min() for the effective policy + // (1 for SCHED_FIFO/SCHED_RR, 0 for SCHED_OTHER). 'policy' is the value retrieved above and is + // guaranteed valid here (the rc != 0 case returned early). + const int effective_policy = expected.policy.value_or(policy); + const int expected_priority = expected.priority.value_or(sched_get_priority_min(effective_policy)); + EXPECT_EQ(param.sched_priority, expected_priority) + << context << ": Expected scheduling priority=" << expected_priority + << (expected.priority.has_value() ? "" : " (default)") << " but got " << param.sched_priority; + if (param.sched_priority != expected_priority) { pass = false; } @@ -194,78 +211,105 @@ bool verify_sandbox_options() { bool all_pass = true; - TEST_STEP("Verify uid and gid") + if (expected.uid.has_value() || expected.gid.has_value()) { - const uid_t current_uid = getuid(); - const gid_t current_gid = getgid(); - - EXPECT_EQ(current_uid, expected.uid) << "Expected uid=" << expected.uid << " but got uid=" << current_uid; - EXPECT_EQ(current_gid, expected.gid) << "Expected gid=" << expected.gid << " but got gid=" << current_gid; - - if (current_uid != expected.uid || current_gid != expected.gid) + TEST_STEP("Verify uid and gid") { - all_pass = false; + if (expected.uid.has_value()) + { + const uid_t current_uid = getuid(); + EXPECT_EQ(current_uid, *expected.uid) + << "Expected uid=" << *expected.uid << " but got uid=" << current_uid; + if (current_uid != *expected.uid) + { + all_pass = false; + } + } + + if (expected.gid.has_value()) + { + const gid_t current_gid = getgid(); + EXPECT_EQ(current_gid, *expected.gid) + << "Expected gid=" << *expected.gid << " but got gid=" << current_gid; + if (current_gid != *expected.gid) + { + all_pass = false; + } + } } } - TEST_STEP("Verify supplementary groups") + if (expected.supplementary_groups.has_value()) { - std::vector groups(256); - const int count = getgroups(static_cast(groups.size()), groups.data()); - EXPECT_GE(count, 0) << "Failed to get supplementary groups"; - - for (const gid_t expected_group : expected.supplementary_groups) + TEST_STEP("Verify supplementary groups") { - const bool found = std::find(groups.begin(), groups.begin() + count, expected_group) != groups.end(); - EXPECT_TRUE(found) << "Expected supplementary group " << expected_group << " not found (groups: [" << count - << " total)]"; - if (!found) + std::vector groups(256); + const int count = getgroups(static_cast(groups.size()), groups.data()); + EXPECT_GE(count, 0) << "Failed to get supplementary groups"; + if (count >= 0) { - all_pass = false; + groups.resize(static_cast(count)); + } + + for (const gid_t expected_group : *expected.supplementary_groups) + { + const bool found = std::find(groups.begin(), groups.end(), expected_group) != groups.end(); + EXPECT_TRUE(found) << "Expected supplementary group " << expected_group << " not found (groups: [" + << count << " total)]"; + if (!found) + { + all_pass = false; + } } } } - TEST_STEP("Verify working directory") + if (expected.working_dir.has_value()) { - std::array buf{}; - char* result = getcwd(buf.data(), buf.size()); - EXPECT_NE(result, nullptr) << "Failed to get current working directory"; - - if (result) + TEST_STEP("Verify working directory") { - EXPECT_EQ(std::string(result), expected.working_dir) - << "Expected working_dir=" << expected.working_dir << " but got cwd=" << result; + std::array buf{}; + char* result = getcwd(buf.data(), buf.size()); + EXPECT_NE(result, nullptr) << "Failed to get current working directory"; - if (std::string(result) != expected.working_dir) + if (result) { - all_pass = false; + EXPECT_EQ(std::string(result), *expected.working_dir) + << "Expected working_dir=" << *expected.working_dir << " but got cwd=" << result; + + if (std::string(result) != *expected.working_dir) + { + all_pass = false; + } } } } - TEST_STEP("Verify scheduling policy and priority in the main thread") + if (expected.policy.has_value() || expected.priority.has_value()) { - if (!verify_scheduling("main thread")) + TEST_STEP("Verify scheduling policy and priority in the main thread") { - all_pass = false; + if (!verify_scheduling("main thread")) + { + all_pass = false; + } } - } - TEST_STEP("Verify scheduling policy and priority in a spawned thread") - { - // A thread created with default attributes inherits the schedulng policy and - // priority of its creating thread, so the configured real-time settings must - // apply here as well. - std::atomic thread_pass{false}; - std::thread worker([&thread_pass]() { - thread_pass = verify_scheduling("spawned thread"); - }); - worker.join(); - - if (!thread_pass) + TEST_STEP("Verify scheduling policy and priority in a spawned thread") { - all_pass = false; + // A thread created with default attributes inherits the schedulng policy and + // priority of its creating thread, so the configured real-time settings must + // apply here as well. + std::atomic thread_pass{false}; + std::thread worker([&thread_pass]() { + thread_pass = verify_scheduling("spawned thread"); + }); + worker.join(); + + if (!thread_pass) + { + all_pass = false; + } } } @@ -284,10 +328,15 @@ int main(int argc, char** argv) { if (!parse_arguments(argc, argv, expected)) { - std::cerr << "Missing expected sandbox option(s) on the command line. Required: --uid, --gid, " - "--supplementary-groups, --scheduling-policy, --scheduling-priority, --working-dir" + std::cerr << "Recognized sandbox options: --uid, --gid, --supplementary-groups, " + "--scheduling-policy, --scheduling-priority, --working-dir" << std::endl; return 1; } - return TestRunner(__FILE__).RunTests(); + // Derive the GTest XML result file name from the executable name (argv[0]) rather than the + // shared source file, so that the two binaries built from this source (sandbox_options_process_a + // and sandbox_options_process_b) write distinct result files. Dereference rather than index to + // avoid the no-pointer-arithmetic lint rule. + const char* const executable_path = (argc > 0) ? *argv : __FILE__; + return TestRunner(executable_path).RunTests(); } \ No newline at end of file From a87831126aec5d76500f621496af29a79afdaada Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Mon, 27 Jul 2026 13:23:33 +0200 Subject: [PATCH 20/37] Introduce get_working_dir --- scripts/config_mapping/lifecycle_config.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index cf1fc8648..a5f9deeab 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -107,6 +107,14 @@ def get_recovery_process_group_state(config): def sec_to_ms(sec: float) -> int: return int(sec * 1000) +def get_working_dir(deployment_config): + """ + Get the working directory for a component. If not specified, default to the bin_dir. + """ + return deployment_config.get( + "working_dir", deployment_config["bin_dir"] + ) + def preprocess_defaults(global_defaults, config): """ @@ -294,7 +302,7 @@ def gen_config(output_dir, config, input_filename, schema_version=None): "bin_dir": depl_cfg["bin_dir"], # Default the working directory to bin_dir (the directory the # executable lives in) when not set explicitly. - "working_dir": depl_cfg.get("working_dir", depl_cfg["bin_dir"]), + "working_dir": get_working_dir(depl_cfg), "sandbox": sandbox_out, } @@ -673,9 +681,7 @@ def get_terminating_behavior(component_config): process["number_of_restart_attempts"] = component_config["deployment_config"][ "ready_recovery_action" ]["restart"]["number_of_attempts"] - process["working_dir"] = component_config["deployment_config"].get( - "working_dir", component_config["deployment_config"]["bin_dir"] - ) + process["working_dir"] = get_working_dir(component_config["deployment_config"]) match component_config["component_properties"]["application_profile"][ "application_type" From f5845eeabf8924eea5d8f508b26d04d7602f15e9 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 29 Jul 2026 13:50:12 +0200 Subject: [PATCH 21/37] Adapt user guide for param working_dir --- score/launch_manager/docs/user_guide/configuration.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/score/launch_manager/docs/user_guide/configuration.rst b/score/launch_manager/docs/user_guide/configuration.rst index b6499c423..1a26657d8 100644 --- a/score/launch_manager/docs/user_guide/configuration.rst +++ b/score/launch_manager/docs/user_guide/configuration.rst @@ -231,7 +231,7 @@ deployment_config (object) * **bin_dir** (string, optional) * **Description:** Specifies the absolute filesystem path to the directory where the component's executable is installed. * **working_dir** (string, optional) - * **Description:** Specifies the directory to be used as the working directory for the component during execution. + * **Description:** Specifies the directory to be used as the working directory for the component during execution. If not defined, the binary's directory (``bin_dir``) is used as the working directory by default. * **ready_recovery_action** (object, optional) * **Description:** Specifies the recovery action to execute when the component fails to reach its **Ready State** within the configured ``ready_timeout``. This action is limited to ``restart`` operations. * **Reference:** This property refers to the ``recovery_action`` reusable type defined in this schema, specifically enforcing the ``restart`` option. From b76e7ee637bf0c2520399b774ef01cff766a2b69 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 29 Jul 2026 14:18:20 +0200 Subject: [PATCH 22/37] Drop not needed empty param --- tests/integration/sandbox_options/sandbox_options.json | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/sandbox_options/sandbox_options.json b/tests/integration/sandbox_options/sandbox_options.json index 5b22e9127..60f8bfaff 100644 --- a/tests/integration/sandbox_options/sandbox_options.json +++ b/tests/integration/sandbox_options/sandbox_options.json @@ -98,7 +98,6 @@ "process_arguments": [ "--uid=0", "--gid=0", - "--supplementary-groups=", "--scheduling-policy=SCHED_RR" ] }, From a4d29a97d4502467b62a7c2c888648b0f876151f Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 29 Jul 2026 14:20:28 +0200 Subject: [PATCH 23/37] Provide path to schema file as build parameter Instead of hardcoded path in python. --- scripts/config_mapping/integration_tests.py | 44 ++++++++------------- scripts/config_mapping/tests/BUILD | 2 + scripts/config_mapping/tests/conftest.py | 15 +++++++ 3 files changed, 34 insertions(+), 27 deletions(-) create mode 100644 scripts/config_mapping/tests/conftest.py diff --git a/scripts/config_mapping/integration_tests.py b/scripts/config_mapping/integration_tests.py index 0e147a56f..6ef58ab36 100644 --- a/scripts/config_mapping/integration_tests.py +++ b/scripts/config_mapping/integration_tests.py @@ -24,22 +24,11 @@ ) script_dir = Path(__file__).parent -schema_path = ( - script_dir.parent.parent - / "score" - / "launch_manager" - / "src" - / "daemon" - / "src" - / "configuration" - / "config_schema" - / "launch_manager.schema.json" -) tests_dir = script_dir / "tests" lifecycle_script = script_dir / "lifecycle_config.py" -def run(input_file: Path, test_name: str, compare_files_only=[], exclude_files=[]): +def run(input_file: Path, test_name: str, schema_file: Path, compare_files_only=[], exclude_files=[]): """ Execute the mapping script with the given input file and compare the generated output with the expected output. Input: @@ -67,7 +56,7 @@ def run(input_file: Path, test_name: str, compare_files_only=[], exclude_files=[ "-o", str(actual_output_dir), "--schema", - str(schema_path), + str(schema_file), ] try: @@ -128,17 +117,18 @@ def compare_files(dir1: Path, dir2: Path, files: list) -> bool: return True -def test_basic(): +def test_basic(schema_file): """ Basic Smoketest for generating both launch manager and health monitoring configuration """ + test_name = "basic_test" input_file = tests_dir / test_name / "input" / "lm_config.json" - run(input_file, test_name) + run(input_file, test_name, schema_file) -def test_health_config_mapping(): +def test_health_config_mapping(schema_file): """ Test generation of the health monitoring configuration with * Different application types @@ -148,20 +138,20 @@ def test_health_config_mapping(): test_name = "health_config_test" input_file = tests_dir / test_name / "input" / "lm_config.json" - run(input_file, test_name, exclude_files=["lm_demo.json"]) + run(input_file, test_name, schema_file, exclude_files=["lm_demo.json"]) -def test_empty_health_config_mapping(): +def test_empty_health_config_mapping(schema_file): """ Test generation of the health monitoring configuration with no supervised processes """ test_name = "empty_health_config_test" input_file = tests_dir / test_name / "input" / "lm_config.json" - run(input_file, test_name, exclude_files=["lm_demo.json"]) + run(input_file, test_name, schema_file, exclude_files=["lm_demo.json"]) -def test_launch_config_mapping(): +def test_launch_config_mapping(schema_file): """ Test generation of the launch manager configuration with * Different application types @@ -171,20 +161,20 @@ def test_launch_config_mapping(): test_name = "lm_config_test" input_file = tests_dir / test_name / "input" / "lm_config.json" - run(input_file, test_name, compare_files_only=["lm_demo.json"]) + run(input_file, test_name, schema_file, compare_files_only=["lm_demo.json"]) -def test_empty_launch_config_mapping(): +def test_empty_launch_config_mapping(schema_file): """ Test generation of the launch manager configuration with no processes defined """ test_name = "empty_lm_config_test" input_file = tests_dir / test_name / "input" / "lm_config.json" - run(input_file, test_name, compare_files_only=["lm_demo.json"]) + run(input_file, test_name, schema_file, compare_files_only=["lm_demo.json"]) -def test_custom_validation_failures(): +def test_custom_validation_failures(schema_file): """ Test that custom validation checks implemented in lifecycle_config.py are correctly identifying invalid configurations. The input configuration contains the following issues: @@ -198,7 +188,7 @@ def test_custom_validation_failures(): input_file = tests_dir / test_name / "input" / "lm_config.json" try: - run(input_file, test_name) + run(input_file, test_name, schema_file) raise AssertionError( "Expected an error due to custom validation failures, but the mapping script executed successfully." ) @@ -224,7 +214,7 @@ def test_custom_validation_failures(): ) -def test_schema_validation_failures(): +def test_schema_validation_failures(schema_file): """ Test that schema validation errors are correctly raised when the input configuration does not conform to the defined JSON schema. The input configuration contains the following issues: @@ -234,7 +224,7 @@ def test_schema_validation_failures(): input_file = tests_dir / test_name / "input" / "lm_config.json" try: - run(input_file, test_name) + run(input_file, test_name, schema_file) raise AssertionError( "Expected an error due to schema validation failures, but the mapping script executed successfully." ) diff --git a/scripts/config_mapping/tests/BUILD b/scripts/config_mapping/tests/BUILD index 825fd6e40..c117924e0 100644 --- a/scripts/config_mapping/tests/BUILD +++ b/scripts/config_mapping/tests/BUILD @@ -22,10 +22,12 @@ filegroup( score_py_pytest( name = "lifecycle_config_tests", srcs = [ + "conftest.py", "//scripts/config_mapping:integration_tests.py", "//scripts/config_mapping:unit_tests.py", ], args = [ + "--schema-file=$(location //score/launch_manager/src/daemon/src/configuration/config_schema:launch_manager.schema.json)" ], data = [ ":integration_test_files", diff --git a/scripts/config_mapping/tests/conftest.py b/scripts/config_mapping/tests/conftest.py new file mode 100644 index 000000000..5af451730 --- /dev/null +++ b/scripts/config_mapping/tests/conftest.py @@ -0,0 +1,15 @@ +import pytest + +def pytest_addoption(parser): + parser.addoption( + "--schema-file", + action="store", + required=True, + help="path to the schema file", + + ) + + +@pytest.fixture +def schema_file(request): + return request.config.getoption("--schema-file") From f3b90f29c814d5c8eb868f2d983c88abbca4cea2 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 29 Jul 2026 14:52:21 +0200 Subject: [PATCH 24/37] Add option is_self_terminating = true to sandbox_options processes --- tests/integration/sandbox_options/sandbox_options.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/integration/sandbox_options/sandbox_options.json b/tests/integration/sandbox_options/sandbox_options.json index 60f8bfaff..d3f53347b 100644 --- a/tests/integration/sandbox_options/sandbox_options.json +++ b/tests/integration/sandbox_options/sandbox_options.json @@ -64,7 +64,8 @@ "component_properties": { "binary_name": "sandbox_options_process_a", "application_profile": { - "application_type": "Reporting" + "application_type": "Reporting", + "is_self_terminating": true }, "process_arguments": [ "--uid=666", @@ -93,7 +94,8 @@ "component_properties": { "binary_name": "sandbox_options_process_b", "application_profile": { - "application_type": "Reporting" + "application_type": "Reporting", + "is_self_terminating": true }, "process_arguments": [ "--uid=0", From bac65beb24648246e15fa37763fce2f8c20b6123 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 29 Jul 2026 14:52:42 +0200 Subject: [PATCH 25/37] Drop switch back to startup --- tests/integration/sandbox_options/control_daemon_mock.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/integration/sandbox_options/control_daemon_mock.cpp b/tests/integration/sandbox_options/control_daemon_mock.cpp index 00a7e9dd8..2c4f6084b 100644 --- a/tests/integration/sandbox_options/control_daemon_mock.cpp +++ b/tests/integration/sandbox_options/control_daemon_mock.cpp @@ -34,12 +34,7 @@ TEST(SandboxOptions, Daemon) 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") - { - score::cpp::stop_token stop_token; - auto result = client.ActivateRunTarget("Startup").Get(stop_token); - EXPECT_TRUE(result.has_value()); - } + TEST_STEP("Activate RunTarget Off") { client.ActivateRunTarget("Off"); From e91ebb884e6934cf320a0c43bb0482bdcf0d3e85 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 29 Jul 2026 16:41:35 +0200 Subject: [PATCH 26/37] Fix formatting --- scripts/config_mapping/integration_tests.py | 8 +++++++- scripts/config_mapping/lifecycle_config.py | 5 ++--- scripts/config_mapping/tests/BUILD | 2 +- scripts/config_mapping/tests/conftest.py | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/scripts/config_mapping/integration_tests.py b/scripts/config_mapping/integration_tests.py index 6ef58ab36..5c820ce00 100644 --- a/scripts/config_mapping/integration_tests.py +++ b/scripts/config_mapping/integration_tests.py @@ -28,7 +28,13 @@ lifecycle_script = script_dir / "lifecycle_config.py" -def run(input_file: Path, test_name: str, schema_file: Path, compare_files_only=[], exclude_files=[]): +def run( + input_file: Path, + test_name: str, + schema_file: Path, + compare_files_only=[], + exclude_files=[], +): """ Execute the mapping script with the given input file and compare the generated output with the expected output. Input: diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index a5f9deeab..4776cff57 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -107,13 +107,12 @@ def get_recovery_process_group_state(config): def sec_to_ms(sec: float) -> int: return int(sec * 1000) + def get_working_dir(deployment_config): """ Get the working directory for a component. If not specified, default to the bin_dir. """ - return deployment_config.get( - "working_dir", deployment_config["bin_dir"] - ) + return deployment_config.get("working_dir", deployment_config["bin_dir"]) def preprocess_defaults(global_defaults, config): diff --git a/scripts/config_mapping/tests/BUILD b/scripts/config_mapping/tests/BUILD index c117924e0..a31b2927e 100644 --- a/scripts/config_mapping/tests/BUILD +++ b/scripts/config_mapping/tests/BUILD @@ -27,7 +27,7 @@ score_py_pytest( "//scripts/config_mapping:unit_tests.py", ], args = [ - "--schema-file=$(location //score/launch_manager/src/daemon/src/configuration/config_schema:launch_manager.schema.json)" + "--schema-file=$(location //score/launch_manager/src/daemon/src/configuration/config_schema:launch_manager.schema.json)", ], data = [ ":integration_test_files", diff --git a/scripts/config_mapping/tests/conftest.py b/scripts/config_mapping/tests/conftest.py index 5af451730..1f450659f 100644 --- a/scripts/config_mapping/tests/conftest.py +++ b/scripts/config_mapping/tests/conftest.py @@ -1,12 +1,12 @@ import pytest + def pytest_addoption(parser): parser.addoption( "--schema-file", action="store", required=True, help="path to the schema file", - ) From 948683d88032f29ef2f46c9a53030be369dceb28 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 30 Jul 2026 11:06:50 +0200 Subject: [PATCH 27/37] Add header guard --- tests/utils/test_helper/test_helper.hpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/utils/test_helper/test_helper.hpp b/tests/utils/test_helper/test_helper.hpp index 42b6c7047..d1dc38d2a 100644 --- a/tests/utils/test_helper/test_helper.hpp +++ b/tests/utils/test_helper/test_helper.hpp @@ -10,6 +10,9 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ +#ifndef TESTS_UTILS_TEST_HELPER_HPP +#define TESTS_UTILS_TEST_HELPER_HPP + #include #include #include @@ -170,3 +173,6 @@ class TestRunner return res; } }; + + +#endif // TESTS_UTILS_TEST_HELPER_HPP From 550fdd28b386a5663cc346aceff695acc2e99788 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 30 Jul 2026 11:07:59 +0200 Subject: [PATCH 28/37] Move verify function to verify_sandbox.hpp --- tests/integration/sandbox_options/BUILD | 10 +- .../sandbox_options_process.cpp | 208 +---------------- .../sandbox_options/verify_sandbox.hpp | 211 ++++++++++++++++++ 3 files changed, 229 insertions(+), 200 deletions(-) create mode 100644 tests/integration/sandbox_options/verify_sandbox.hpp diff --git a/tests/integration/sandbox_options/BUILD b/tests/integration/sandbox_options/BUILD index 018c71319..1a9b431d2 100644 --- a/tests/integration/sandbox_options/BUILD +++ b/tests/integration/sandbox_options/BUILD @@ -30,7 +30,10 @@ cc_binary( # own executable name so the results do not collide. cc_binary( name = "sandbox_options_process_a", - srcs = ["sandbox_options_process.cpp"], + srcs = [ + "sandbox_options_process.cpp", + "verify_sandbox.hpp", + ], deps = [ "//score/launch_manager:control_cc", "//score/launch_manager:lifecycle_cc", @@ -41,7 +44,10 @@ cc_binary( cc_binary( name = "sandbox_options_process_b", - srcs = ["sandbox_options_process.cpp"], + srcs = [ + "sandbox_options_process.cpp", + "verify_sandbox.hpp", + ], deps = [ "//score/launch_manager:control_cc", "//score/launch_manager:lifecycle_cc", diff --git a/tests/integration/sandbox_options/sandbox_options_process.cpp b/tests/integration/sandbox_options/sandbox_options_process.cpp index bae967e7c..b3193c7a3 100644 --- a/tests/integration/sandbox_options/sandbox_options_process.cpp +++ b/tests/integration/sandbox_options/sandbox_options_process.cpp @@ -12,64 +12,32 @@ ********************************************************************************/ #include -#include #include #include -#include -#include -#include -#include -#include #include -#include #include #include #include -#include #include +#include "tests/integration/sandbox_options/verify_sandbox.hpp" #include "tests/utils/test_helper/test_helper.hpp" #include namespace { -/// @brief Expected sandbox option values, supplied to this process on the command line. -/// -/// The launch manager applies the sandbox options from sandbox_options.json and passes the same -/// values to the managed process via 'process_arguments', so the test verifies the applied state -/// against the configured expectation without duplicating any literals here. -/// -/// Every value is optional: a value that is not passed on the command line is not verified. This -/// lets a component leave an option unset (e.g. no working directory) without the test flagging it. -struct ExpectedValues -{ - std::optional policy; - std::optional priority; - std::optional uid; - std::optional gid; - std::optional> supplementary_groups; - std::optional working_dir; -}; +using sandbox_options::ExpectedValues; +// Expected sandbox option values, supplied to this process on the command line. +// +// The launch manager applies the sandbox options from sandbox_options.json and passes the same +// values to the managed process via 'process_arguments', so the test verifies the applied state +// against the configured expectation without duplicating any literals here. +// // Populated from argv in main() before the tests run. ExpectedValues expected; -const char* policy_name(const int policy) -{ - switch (policy) - { - case SCHED_FIFO: - return "SCHED_FIFO"; - case SCHED_RR: - return "SCHED_RR"; - case SCHED_OTHER: - return "SCHED_OTHER"; - default: - return "UNKNOWN"; - } -} - int parse_policy(const std::string& name) { if (name == "SCHED_FIFO") @@ -160,167 +128,11 @@ bool parse_arguments(int argc, char** argv, ExpectedValues& out) return all_recognized; } -/// @brief Verify that the calling thread runs with the expected scheduling policy and priority. -/// @param[in] context Human readable name of the thread, used in failure messages. -/// @return true if the policy (when provided) and the priority match. -bool verify_scheduling(const std::string& context) -{ - int policy = -1; - sched_param param{}; - const int result = pthread_getschedparam(pthread_self(), &policy, ¶m); - EXPECT_EQ(result, 0) << context << ": pthread_getschedparam failed rc=" << result; - if (result != 0) - { - // 'policy' was not written, so it is still the sentinel -1. Bail out before it can reach - // sched_get_priority_min() below (which would return -1 and set errno=EINVAL). - return false; - } - - bool pass = true; - - if (expected.policy.has_value()) - { - EXPECT_EQ(policy, *expected.policy) - << context << ": Expected scheduling policy=" << policy_name(*expected.policy) << " but got " - << policy_name(policy); - if (policy != *expected.policy) - { - pass = false; - } - } - - // The priority is always verified. When no explicit priority is provided on the command line, - // verify against the default the launch manager ends up applying: its configured default (0) - // clamped up to the policy minimum, i.e. sched_get_priority_min() for the effective policy - // (1 for SCHED_FIFO/SCHED_RR, 0 for SCHED_OTHER). 'policy' is the value retrieved above and is - // guaranteed valid here (the rc != 0 case returned early). - const int effective_policy = expected.policy.value_or(policy); - const int expected_priority = expected.priority.value_or(sched_get_priority_min(effective_policy)); - EXPECT_EQ(param.sched_priority, expected_priority) - << context << ": Expected scheduling priority=" << expected_priority - << (expected.priority.has_value() ? "" : " (default)") << " but got " << param.sched_priority; - if (param.sched_priority != expected_priority) - { - pass = false; - } - - return pass; -} - -bool verify_sandbox_options() -{ - bool all_pass = true; - - if (expected.uid.has_value() || expected.gid.has_value()) - { - TEST_STEP("Verify uid and gid") - { - if (expected.uid.has_value()) - { - const uid_t current_uid = getuid(); - EXPECT_EQ(current_uid, *expected.uid) - << "Expected uid=" << *expected.uid << " but got uid=" << current_uid; - if (current_uid != *expected.uid) - { - all_pass = false; - } - } - - if (expected.gid.has_value()) - { - const gid_t current_gid = getgid(); - EXPECT_EQ(current_gid, *expected.gid) - << "Expected gid=" << *expected.gid << " but got gid=" << current_gid; - if (current_gid != *expected.gid) - { - all_pass = false; - } - } - } - } - - if (expected.supplementary_groups.has_value()) - { - TEST_STEP("Verify supplementary groups") - { - std::vector groups(256); - const int count = getgroups(static_cast(groups.size()), groups.data()); - EXPECT_GE(count, 0) << "Failed to get supplementary groups"; - if (count >= 0) - { - groups.resize(static_cast(count)); - } - - for (const gid_t expected_group : *expected.supplementary_groups) - { - const bool found = std::find(groups.begin(), groups.end(), expected_group) != groups.end(); - EXPECT_TRUE(found) << "Expected supplementary group " << expected_group << " not found (groups: [" - << count << " total)]"; - if (!found) - { - all_pass = false; - } - } - } - } - - if (expected.working_dir.has_value()) - { - TEST_STEP("Verify working directory") - { - std::array buf{}; - char* result = getcwd(buf.data(), buf.size()); - EXPECT_NE(result, nullptr) << "Failed to get current working directory"; - - if (result) - { - EXPECT_EQ(std::string(result), *expected.working_dir) - << "Expected working_dir=" << *expected.working_dir << " but got cwd=" << result; - - if (std::string(result) != *expected.working_dir) - { - all_pass = false; - } - } - } - } - - if (expected.policy.has_value() || expected.priority.has_value()) - { - TEST_STEP("Verify scheduling policy and priority in the main thread") - { - if (!verify_scheduling("main thread")) - { - all_pass = false; - } - } - - TEST_STEP("Verify scheduling policy and priority in a spawned thread") - { - // A thread created with default attributes inherits the schedulng policy and - // priority of its creating thread, so the configured real-time settings must - // apply here as well. - std::atomic thread_pass{false}; - std::thread worker([&thread_pass]() { - thread_pass = verify_scheduling("spawned thread"); - }); - worker.join(); - - if (!thread_pass) - { - all_pass = false; - } - } - } - - return all_pass; -} - } // namespace TEST(SandboxOptions, RunAndVerify) { - ASSERT_TRUE(verify_sandbox_options()) << "Sandbox options verification failed"; + EXPECT_TRUE(sandbox_options::verifySandbox(expected)) << "Sandbox options verification failed"; score::mw::lifecycle::report_running(); } @@ -339,4 +151,4 @@ int main(int argc, char** argv) // avoid the no-pointer-arithmetic lint rule. const char* const executable_path = (argc > 0) ? *argv : __FILE__; return TestRunner(executable_path).RunTests(); -} \ No newline at end of file +} diff --git a/tests/integration/sandbox_options/verify_sandbox.hpp b/tests/integration/sandbox_options/verify_sandbox.hpp new file mode 100644 index 000000000..81f192011 --- /dev/null +++ b/tests/integration/sandbox_options/verify_sandbox.hpp @@ -0,0 +1,211 @@ +/******************************************************************************** + * 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 TESTS_INTEGRATION_SANDBOX_OPTIONS_VERIFY_SANDBOX_HPP +#define TESTS_INTEGRATION_SANDBOX_OPTIONS_VERIFY_SANDBOX_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tests/utils/test_helper/test_helper.hpp" + +namespace sandbox_options +{ + +/// @brief Expected sandbox option values to verify against the process' applied state. +/// +/// Every value is optional: a value that is not set is not verified. This lets a component leave +/// an option unset (e.g. no working directory) without the verification flagging it. +struct ExpectedValues +{ + std::optional policy; + std::optional priority; + std::optional uid; + std::optional gid; + std::optional> supplementary_groups; + std::optional working_dir; +}; + +inline const char* policy_name(const int policy) +{ + switch (policy) + { + case SCHED_FIFO: + return "SCHED_FIFO"; + case SCHED_RR: + return "SCHED_RR"; + case SCHED_OTHER: + return "SCHED_OTHER"; + default: + return "UNKNOWN"; + } +} + +/// @brief Verify that the calling thread runs with the expected scheduling policy and priority. +/// @param[in] expected The expected sandbox option values. +/// @param[in] context Human readable name of the thread, used in failure messages. +/// @param[out] failures Stream that receives a description of every mismatch. +inline void verify_scheduling(const ExpectedValues& expected, + const std::string& context, + std::ostringstream& failures) +{ + int policy = -1; + sched_param param{}; + const int result = pthread_getschedparam(pthread_self(), &policy, ¶m); + if (result != 0) + { + // 'policy' was not written, so it is still the sentinel -1. Bail out before it can reach + // sched_get_priority_min() below (which would return -1 and set errno=EINVAL). + failures << context << ": pthread_getschedparam failed rc=" << result << "\n"; + return; + } + + if (expected.policy.has_value() && policy != *expected.policy) + { + failures << context << ": Expected scheduling policy=" << policy_name(*expected.policy) << " but got " + << policy_name(policy) << "\n"; + } + + // The priority is always verified. When no explicit priority is provided, verify against the + // default the launch manager ends up applying: its configured default (0) clamped up to the + // policy minimum, i.e. sched_get_priority_min() for the effective policy (1 for + // SCHED_FIFO/SCHED_RR, 0 for SCHED_OTHER). 'policy' is the value retrieved above and is + // guaranteed valid here (the rc != 0 case returned early). + const int effective_policy = expected.policy.value_or(policy); + const int expected_priority = expected.priority.value_or(sched_get_priority_min(effective_policy)); + if (param.sched_priority != expected_priority) + { + failures << context << ": Expected scheduling priority=" << expected_priority + << (expected.priority.has_value() ? "" : " (default)") << " but got " << param.sched_priority + << "\n"; + } +} + +/// @brief Verify that this process runs with the given sandbox options applied. +/// @param[in] expected The expected sandbox option values; unset options are not verified. +/// @return AssertionSuccess if every set expectation matches, otherwise AssertionFailure carrying +/// a description of all mismatches. +inline ::testing::AssertionResult verifySandbox(const ExpectedValues& expected) +{ + std::ostringstream failures; + + if (expected.uid.has_value() || expected.gid.has_value()) + { + TEST_STEP("Verify uid and gid") + { + if (expected.uid.has_value()) + { + const uid_t current_uid = getuid(); + if (current_uid != *expected.uid) + { + failures << "Expected uid=" << *expected.uid << " but got uid=" << current_uid << "\n"; + } + } + + if (expected.gid.has_value()) + { + const gid_t current_gid = getgid(); + if (current_gid != *expected.gid) + { + failures << "Expected gid=" << *expected.gid << " but got gid=" << current_gid << "\n"; + } + } + } + } + + if (expected.supplementary_groups.has_value()) + { + TEST_STEP("Verify supplementary groups") + { + std::vector groups(256); + const int count = getgroups(static_cast(groups.size()), groups.data()); + if (count < 0) + { + failures << "Failed to get supplementary groups\n"; + } + else + { + groups.resize(static_cast(count)); + for (const gid_t expected_group : *expected.supplementary_groups) + { + const bool found = std::find(groups.begin(), groups.end(), expected_group) != groups.end(); + if (!found) + { + failures << "Expected supplementary group " << expected_group << " not found (groups: [" + << count << " total)]\n"; + } + } + } + } + } + + if (expected.working_dir.has_value()) + { + TEST_STEP("Verify working directory") + { + std::array buf{}; + char* result = getcwd(buf.data(), buf.size()); + if (result == nullptr) + { + failures << "Failed to get current working directory\n"; + } + else if (std::string(result) != *expected.working_dir) + { + failures << "Expected working_dir=" << *expected.working_dir << " but got cwd=" << result << "\n"; + } + } + } + + if (expected.policy.has_value() || expected.priority.has_value()) + { + TEST_STEP("Verify scheduling policy and priority in the main thread") + { + verify_scheduling(expected, "main thread", failures); + } + + TEST_STEP("Verify scheduling policy and priority in a spawned thread") + { + // A thread created with default attributes inherits the scheduling policy and + // priority of its creating thread, so the configured real-time settings must + // apply here as well. The thread accumulates into its own stream to avoid a data + // race on 'failures', which is merged in after the join. + std::ostringstream thread_failures; + std::thread worker([&expected, &thread_failures]() { + verify_scheduling(expected, "spawned thread", thread_failures); + }); + worker.join(); + failures << thread_failures.str(); + } + } + + if (failures.tellp() != std::ostringstream::pos_type(0)) + { + return ::testing::AssertionFailure() << failures.str(); + } + return ::testing::AssertionSuccess(); +} + +} // namespace sandbox_options + +#endif // TESTS_INTEGRATION_SANDBOX_OPTIONS_VERIFY_SANDBOX_HPP From e68d955cd61e4202a8f9d26525973f12475b2c28 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 30 Jul 2026 16:33:07 +0200 Subject: [PATCH 29/37] Add scheduling priority to process b and add process c sandbox_options_process_b was stuck in rare cases, as it never got cpu time and ran into timeout. This was caused by the minimal priority. So priority has been added here. Furthermore process_c added to check default options. --- tests/integration/sandbox_options/BUILD | 15 ++++++ .../sandbox_options/sandbox_options.json | 27 ++++++++-- .../sandbox_options/sandbox_options.py | 5 +- .../sandbox_options/verify_sandbox.hpp | 52 ++++++++++--------- 4 files changed, 69 insertions(+), 30 deletions(-) diff --git a/tests/integration/sandbox_options/BUILD b/tests/integration/sandbox_options/BUILD index 1a9b431d2..7128913ca 100644 --- a/tests/integration/sandbox_options/BUILD +++ b/tests/integration/sandbox_options/BUILD @@ -56,6 +56,20 @@ cc_binary( ], ) +cc_binary( + name = "sandbox_options_process_c", + srcs = [ + "sandbox_options_process.cpp", + "verify_sandbox.hpp", + ], + deps = [ + "//score/launch_manager:control_cc", + "//score/launch_manager:lifecycle_cc", + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], +) + integration_test( name = "sandbox_options", srcs = ["sandbox_options.py"], @@ -63,6 +77,7 @@ integration_test( ":control_daemon_mock", ":sandbox_options_process_a", ":sandbox_options_process_b", + ":sandbox_options_process_c", "//score/launch_manager", ], config = ":sandbox_options.json", diff --git a/tests/integration/sandbox_options/sandbox_options.json b/tests/integration/sandbox_options/sandbox_options.json index d3f53347b..17c7e0c26 100644 --- a/tests/integration/sandbox_options/sandbox_options.json +++ b/tests/integration/sandbox_options/sandbox_options.json @@ -100,7 +100,8 @@ "process_arguments": [ "--uid=0", "--gid=0", - "--scheduling-policy=SCHED_RR" + "--scheduling-policy=SCHED_RR", + "--scheduling-priority=10" ] }, "deployment_config": { @@ -111,7 +112,26 @@ "uid": 0, "gid": 0, "supplementary_group_ids": [], - "scheduling_policy": "SCHED_RR" + "scheduling_policy": "SCHED_RR", + "scheduling_priority": 10 + } + } + }, + "sandbox_options_process_c": { + "component_properties": { + "binary_name": "sandbox_options_process_c", + "application_profile": { + "application_type": "Reporting", + "is_self_terminating": true + }, + "process_arguments": [ + "--uid=0", + "--gid=0" + ] + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "DefaultPG_app2" } } } @@ -131,7 +151,8 @@ "depends_on": [ "control_daemon", "sandbox_options_process_a", - "sandbox_options_process_b" + "sandbox_options_process_b", + "sandbox_options_process_c" ], "recovery_action": { "switch_run_target": { diff --git a/tests/integration/sandbox_options/sandbox_options.py b/tests/integration/sandbox_options/sandbox_options.py index 2a00c7bf6..e7fbad38b 100644 --- a/tests/integration/sandbox_options/sandbox_options.py +++ b/tests/integration/sandbox_options/sandbox_options.py @@ -36,8 +36,8 @@ def docker_configuration(): "feat_req__lifecycle__supplementary_groups", ], partially_verifies=[], - test_type="interface-test", - derivation_technique="explorative-testing", + test_type="requirements-based", + derivation_technique="requirements-analysis", ) def test_sandbox_options(target, setup_test, assert_test_results, remote_test_dir): """ @@ -70,6 +70,7 @@ def test_sandbox_options(target, setup_test, assert_test_results, remote_test_di "control_daemon_mock.xml", "sandbox_options_process_a.xml", "sandbox_options_process_b.xml", + "sandbox_options_process_c.xml", }, additional_search_dirs=["/tmp/tests/sandbox_options", "/tmp"], ) diff --git a/tests/integration/sandbox_options/verify_sandbox.hpp b/tests/integration/sandbox_options/verify_sandbox.hpp index 81f192011..de1ae21a9 100644 --- a/tests/integration/sandbox_options/verify_sandbox.hpp +++ b/tests/integration/sandbox_options/verify_sandbox.hpp @@ -81,19 +81,22 @@ inline void verify_scheduling(const ExpectedValues& expected, return; } - if (expected.policy.has_value() && policy != *expected.policy) + // When no explicit policy is configured, the launch manager applies the default scheduling + // policy (SCHED_OTHER). Verify against that default instead of accepting whatever the thread + // happens to run with, so a process launched with default options is still checked. + const int expected_policy = expected.policy.value_or(SCHED_OTHER); + if (policy != expected_policy) { - failures << context << ": Expected scheduling policy=" << policy_name(*expected.policy) << " but got " - << policy_name(policy) << "\n"; + failures << context << ": Expected scheduling policy=" << policy_name(expected_policy) + << (expected.policy.has_value() ? "" : " (default)") << " but got " << policy_name(policy) << "\n"; } // The priority is always verified. When no explicit priority is provided, verify against the // default the launch manager ends up applying: its configured default (0) clamped up to the // policy minimum, i.e. sched_get_priority_min() for the effective policy (1 for - // SCHED_FIFO/SCHED_RR, 0 for SCHED_OTHER). 'policy' is the value retrieved above and is - // guaranteed valid here (the rc != 0 case returned early). - const int effective_policy = expected.policy.value_or(policy); - const int expected_priority = expected.priority.value_or(sched_get_priority_min(effective_policy)); + // SCHED_FIFO/SCHED_RR, 0 for SCHED_OTHER). 'policy'/'expected_policy' are valid here (the + // rc != 0 case returned early). + const int expected_priority = expected.priority.value_or(sched_get_priority_min(expected_policy)); if (param.sched_priority != expected_priority) { failures << context << ": Expected scheduling priority=" << expected_priority @@ -177,26 +180,25 @@ inline ::testing::AssertionResult verifySandbox(const ExpectedValues& expected) } } - if (expected.policy.has_value() || expected.priority.has_value()) + // Scheduling is always verified. When no explicit policy/priority is configured the check runs + // against the launch manager's defaults (SCHED_OTHER at its minimum priority). + TEST_STEP("Verify scheduling policy and priority in the main thread") { - TEST_STEP("Verify scheduling policy and priority in the main thread") - { - verify_scheduling(expected, "main thread", failures); - } + verify_scheduling(expected, "main thread", failures); + } - TEST_STEP("Verify scheduling policy and priority in a spawned thread") - { - // A thread created with default attributes inherits the scheduling policy and - // priority of its creating thread, so the configured real-time settings must - // apply here as well. The thread accumulates into its own stream to avoid a data - // race on 'failures', which is merged in after the join. - std::ostringstream thread_failures; - std::thread worker([&expected, &thread_failures]() { - verify_scheduling(expected, "spawned thread", thread_failures); - }); - worker.join(); - failures << thread_failures.str(); - } + TEST_STEP("Verify scheduling policy and priority in a spawned thread") + { + // A thread created with default attributes inherits the scheduling policy and priority of + // its creating thread, so the configured settings must apply here as well. The thread + // accumulates into its own stream to avoid a data race on 'failures', which is merged in + // after the join. + std::ostringstream thread_failures; + std::thread worker([&expected, &thread_failures]() { + verify_scheduling(expected, "spawned thread", thread_failures); + }); + worker.join(); + failures << thread_failures.str(); } if (failures.tellp() != std::ostringstream::pos_type(0)) From 0fb587c38b7056258cf177471f78c827f34ecdb5 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 30 Jul 2026 18:52:26 +0200 Subject: [PATCH 30/37] Be more explicit during shutdown --- .../testing_utils/run_until_file_deployed.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/utils/testing_utils/run_until_file_deployed.py b/tests/utils/testing_utils/run_until_file_deployed.py index 85efbc121..eb79343e0 100644 --- a/tests/utils/testing_utils/run_until_file_deployed.py +++ b/tests/utils/testing_utils/run_until_file_deployed.py @@ -25,7 +25,7 @@ def run_until_file_deployed( file_path: str, timeout_s: float = 30.0, poll_interval_s: float = 0.5, - stop_timeout_s: float = 2.0, + stop_timeout_s: float = 3.0, args=None, cwd: str = "/", ) -> AsyncProcess: @@ -37,7 +37,7 @@ def run_until_file_deployed( :param timeout_s: maximum seconds to wait for the file (default: 30). :param poll_interval_s: seconds between file and process checks (default: 0.5). :param stop_timeout_s: maximum seconds to wait for the process to terminate - after SIGTERM (default: 2). + after SIGTERM (default: 3). :param args: optional list of arguments to pass to the binary. :param cwd: working directory on the target (default: "/"). :return: the stopped :class:`AsyncProcess` handle. @@ -67,8 +67,16 @@ def run_until_file_deployed( # run their cleanup code before exiting. kill_cmd = f"kill -15 -{proc.pid()}" res, _ = target.execute(kill_cmd) - assert res == 0, "Couldn't kill lcm" - exit_code = proc.wait() # Raises RuntimeError if it does not stop + assert res == 0, "Couldn't kill lcm with SIGTERM" + try: + # wait() raises RuntimeError if the process is still running after + # stop_timeout_s. + exit_code = proc.wait(timeout_s=stop_timeout_s) + except RuntimeError as exc: + proc.stop() # escalate to SIGKILL so we don't leak the process + assert False, ( + f"Process '{binary_path}' still running {stop_timeout_s}s after SIGTERM: {exc}" + ) assert exit_code == 0, ( f"LCM did not exit cleanly, it died with code {exit_code}" ) From 51621576500daa8600d63af9a788db419b76a2e7 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 30 Jul 2026 19:03:39 +0200 Subject: [PATCH 31/37] Fix copyright header --- scripts/config_mapping/tests/conftest.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/scripts/config_mapping/tests/conftest.py b/scripts/config_mapping/tests/conftest.py index 1f450659f..531237e10 100644 --- a/scripts/config_mapping/tests/conftest.py +++ b/scripts/config_mapping/tests/conftest.py @@ -1,3 +1,16 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + import pytest From b84c0467e0b92ad278762de6b0e976e9f8bcade6 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 31 Jul 2026 09:21:07 +0200 Subject: [PATCH 32/37] Assert default working_dir in sandbox_options_process_c process_c configures neither a component-level working_dir nor one in the defaults section, so the launch manager falls back to bin_dir. Pass the expected default via --working-dir so the test verifies the default working directory path, mirroring how process_a verifies an explicit one. Co-Authored-By: Claude Opus 4.8 --- tests/integration/sandbox_options/BUILD | 2 +- tests/integration/sandbox_options/sandbox_options.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/integration/sandbox_options/BUILD b/tests/integration/sandbox_options/BUILD index 7128913ca..3205015d8 100644 --- a/tests/integration/sandbox_options/BUILD +++ b/tests/integration/sandbox_options/BUILD @@ -25,7 +25,7 @@ cc_binary( ], ) -# Two binaries are built from the same source. Each verifies the sandbox options passed to it on +# Three binaries are built from the same source. Each verifies the sandbox options passed to it on # the command line (see sandbox_options.json), and derives its GTest XML result file name from its # own executable name so the results do not collide. cc_binary( diff --git a/tests/integration/sandbox_options/sandbox_options.json b/tests/integration/sandbox_options/sandbox_options.json index 17c7e0c26..5c879cb9e 100644 --- a/tests/integration/sandbox_options/sandbox_options.json +++ b/tests/integration/sandbox_options/sandbox_options.json @@ -126,7 +126,8 @@ }, "process_arguments": [ "--uid=0", - "--gid=0" + "--gid=0", + "--working-dir=/tmp/tests/sandbox_options" ] }, "deployment_config": { From d3e845ad581c2c80d1da07c941a0eb92603946f4 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 31 Jul 2026 11:58:54 +0200 Subject: [PATCH 33/37] Update to itf 0.5.0 --- MODULE.bazel.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 400be6002..7eb61df3c 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -968,8 +968,8 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_docs_as_code/4.6.1/MODULE.bazel": "0dae734ea8a99970a7417829b3115af02717b29f0fc40a8ebd3c1afcc71e1e21", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_docs_as_code/4.6.1/source.json": "3299daf219b15ba4aff6ca801385abebe0aaf364f71e391cd76cf9d38f3741f6", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_format_checker/0.1.1/MODULE.bazel": "1acc254faa90e9f97b79ac69af25b6c21c561f8d6079914f6352b9b20d26bd37", - "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_itf/0.3.0/MODULE.bazel": "0d47fb1832834deb35876147554dabdbb7c6d4dbbdf56ccc4d0be34967d7c5ee", - "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_itf/0.3.0/source.json": "86a6a4ba15e6208ea4396f5585de457bc674efb87590c1c636896ee5ef4a7e15", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_itf/0.5.0/MODULE.bazel": "d3ae40173462419b4e14e3fea96a49eab8822be7292f94890beb8cc73173eaa1", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_itf/0.5.0/source.json": "3c0fcd761e221bc33e6ebbeb0f92ce71b5d6f1f946b60ccf4fe7cf3a4c00d211", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_platform/0.1.0/MODULE.bazel": "cc9eae86e76f2a930510ed6e50ec991bb5661687e24881685b39c322087adf6f", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_platform/0.1.1/MODULE.bazel": "eb086ba99f9319371fbbd0a9252dfd27b0817039b88bd4d691602974b1ada005", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_platform/0.6.2/MODULE.bazel": "e88e7d086a08e1b451a68b60038b8b1f3c028aae7efc0bc9d8b39b6261ba040d", From d345e01a79669dafd7507cbccfcccde61b15d109 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 31 Jul 2026 12:28:33 +0200 Subject: [PATCH 34/37] Rework to have test steps visible in cpp file --- .../sandbox_options_process.cpp | 47 +++- .../sandbox_options/verify_sandbox.hpp | 214 ++++++++---------- 2 files changed, 146 insertions(+), 115 deletions(-) diff --git a/tests/integration/sandbox_options/sandbox_options_process.cpp b/tests/integration/sandbox_options/sandbox_options_process.cpp index b3193c7a3..f0d61094c 100644 --- a/tests/integration/sandbox_options/sandbox_options_process.cpp +++ b/tests/integration/sandbox_options/sandbox_options_process.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include "tests/integration/sandbox_options/verify_sandbox.hpp" @@ -132,7 +133,51 @@ bool parse_arguments(int argc, char** argv, ExpectedValues& out) TEST(SandboxOptions, RunAndVerify) { - EXPECT_TRUE(sandbox_options::verifySandbox(expected)) << "Sandbox options verification failed"; + // Each sandbox option is verified in its own step so the test log shows exactly what is checked. + // An option that is left unset (see ExpectedValues) is not configured for this process and is + // therefore skipped; scheduling is always verified against the launch manager's defaults. + if (expected.uid.has_value() || expected.gid.has_value()) + { + TEST_STEP("Verify uid and gid") + { + EXPECT_TRUE(sandbox_options::verifyUidGid(expected.uid, expected.gid)); + } + } + + if (expected.supplementary_groups.has_value()) + { + TEST_STEP("Verify supplementary groups") + { + EXPECT_TRUE(sandbox_options::verifySupplementaryGroups(*expected.supplementary_groups)); + } + } + + if (expected.working_dir.has_value()) + { + TEST_STEP("Verify working directory") + { + EXPECT_TRUE(sandbox_options::verifyWorkingDir(*expected.working_dir)); + } + } + + TEST_STEP("Verify scheduling policy and priority in the main thread") + { + EXPECT_TRUE(sandbox_options::verifyScheduling(expected.policy, expected.priority, "main thread")); + } + + TEST_STEP("Verify scheduling policy and priority in a spawned thread") + { + // A thread created with default attributes inherits the scheduling policy and priority of + // its creating thread, so the configured settings must apply here as well. The result is + // written in the worker and read after the join, which synchronizes the access. + ::testing::AssertionResult thread_result = ::testing::AssertionSuccess(); + std::thread worker([&thread_result]() { + thread_result = sandbox_options::verifyScheduling(expected.policy, expected.priority, "spawned thread"); + }); + worker.join(); + EXPECT_TRUE(thread_result); + } + score::mw::lifecycle::report_running(); } diff --git a/tests/integration/sandbox_options/verify_sandbox.hpp b/tests/integration/sandbox_options/verify_sandbox.hpp index de1ae21a9..d49dffba3 100644 --- a/tests/integration/sandbox_options/verify_sandbox.hpp +++ b/tests/integration/sandbox_options/verify_sandbox.hpp @@ -20,16 +20,12 @@ #include #include #include -#include #include #include #include #include -#include #include -#include "tests/utils/test_helper/test_helper.hpp" - namespace sandbox_options { @@ -62,150 +58,140 @@ inline const char* policy_name(const int policy) } } -/// @brief Verify that the calling thread runs with the expected scheduling policy and priority. -/// @param[in] expected The expected sandbox option values. -/// @param[in] context Human readable name of the thread, used in failure messages. -/// @param[out] failures Stream that receives a description of every mismatch. -inline void verify_scheduling(const ExpectedValues& expected, - const std::string& context, - std::ostringstream& failures) +/// @brief Turn an accumulated failure description into a gtest result. +/// @return AssertionSuccess if 'failures' is empty, otherwise AssertionFailure carrying its text. +inline ::testing::AssertionResult to_result(std::ostringstream& failures) { - int policy = -1; - sched_param param{}; - const int result = pthread_getschedparam(pthread_self(), &policy, ¶m); - if (result != 0) + if (failures.tellp() != std::ostringstream::pos_type(0)) { - // 'policy' was not written, so it is still the sentinel -1. Bail out before it can reach - // sched_get_priority_min() below (which would return -1 and set errno=EINVAL). - failures << context << ": pthread_getschedparam failed rc=" << result << "\n"; - return; + return ::testing::AssertionFailure() << failures.str(); } + return ::testing::AssertionSuccess(); +} + +/// @brief Verify that this process runs with the expected user and group id. +/// @param[in] expected_uid Expected user id; not verified when unset. +/// @param[in] expected_gid Expected group id; not verified when unset. +/// @return AssertionSuccess if every set expectation matches, otherwise AssertionFailure. +inline ::testing::AssertionResult verifyUidGid(const std::optional expected_uid, + const std::optional expected_gid) +{ + std::ostringstream failures; - // When no explicit policy is configured, the launch manager applies the default scheduling - // policy (SCHED_OTHER). Verify against that default instead of accepting whatever the thread - // happens to run with, so a process launched with default options is still checked. - const int expected_policy = expected.policy.value_or(SCHED_OTHER); - if (policy != expected_policy) + if (expected_uid.has_value()) { - failures << context << ": Expected scheduling policy=" << policy_name(expected_policy) - << (expected.policy.has_value() ? "" : " (default)") << " but got " << policy_name(policy) << "\n"; + const uid_t current_uid = getuid(); + if (current_uid != *expected_uid) + { + failures << "Expected uid=" << *expected_uid << " but got uid=" << current_uid << "\n"; + } } - // The priority is always verified. When no explicit priority is provided, verify against the - // default the launch manager ends up applying: its configured default (0) clamped up to the - // policy minimum, i.e. sched_get_priority_min() for the effective policy (1 for - // SCHED_FIFO/SCHED_RR, 0 for SCHED_OTHER). 'policy'/'expected_policy' are valid here (the - // rc != 0 case returned early). - const int expected_priority = expected.priority.value_or(sched_get_priority_min(expected_policy)); - if (param.sched_priority != expected_priority) + if (expected_gid.has_value()) { - failures << context << ": Expected scheduling priority=" << expected_priority - << (expected.priority.has_value() ? "" : " (default)") << " but got " << param.sched_priority - << "\n"; + const gid_t current_gid = getgid(); + if (current_gid != *expected_gid) + { + failures << "Expected gid=" << *expected_gid << " but got gid=" << current_gid << "\n"; + } } + + return to_result(failures); } -/// @brief Verify that this process runs with the given sandbox options applied. -/// @param[in] expected The expected sandbox option values; unset options are not verified. -/// @return AssertionSuccess if every set expectation matches, otherwise AssertionFailure carrying -/// a description of all mismatches. -inline ::testing::AssertionResult verifySandbox(const ExpectedValues& expected) +/// @brief Verify that every expected supplementary group is present in the process' group set. +/// @param[in] expected_groups Group ids that must all be present. +/// @return AssertionSuccess if every expected group is found, otherwise AssertionFailure. +inline ::testing::AssertionResult verifySupplementaryGroups(const std::vector& expected_groups) { std::ostringstream failures; - if (expected.uid.has_value() || expected.gid.has_value()) + std::vector groups(256); + const int count = getgroups(static_cast(groups.size()), groups.data()); + if (count < 0) { - TEST_STEP("Verify uid and gid") - { - if (expected.uid.has_value()) - { - const uid_t current_uid = getuid(); - if (current_uid != *expected.uid) - { - failures << "Expected uid=" << *expected.uid << " but got uid=" << current_uid << "\n"; - } - } - - if (expected.gid.has_value()) - { - const gid_t current_gid = getgid(); - if (current_gid != *expected.gid) - { - failures << "Expected gid=" << *expected.gid << " but got gid=" << current_gid << "\n"; - } - } - } + failures << "Failed to get supplementary groups\n"; } - - if (expected.supplementary_groups.has_value()) + else { - TEST_STEP("Verify supplementary groups") + groups.resize(static_cast(count)); + for (const gid_t expected_group : expected_groups) { - std::vector groups(256); - const int count = getgroups(static_cast(groups.size()), groups.data()); - if (count < 0) - { - failures << "Failed to get supplementary groups\n"; - } - else + const bool found = std::find(groups.begin(), groups.end(), expected_group) != groups.end(); + if (!found) { - groups.resize(static_cast(count)); - for (const gid_t expected_group : *expected.supplementary_groups) - { - const bool found = std::find(groups.begin(), groups.end(), expected_group) != groups.end(); - if (!found) - { - failures << "Expected supplementary group " << expected_group << " not found (groups: [" - << count << " total)]\n"; - } - } + failures << "Expected supplementary group " << expected_group << " not found (groups: [" << count + << " total)]\n"; } } } - if (expected.working_dir.has_value()) + return to_result(failures); +} + +/// @brief Verify that the process' current working directory matches the expectation. +/// @param[in] expected_working_dir Expected absolute working directory. +/// @return AssertionSuccess if the working directory matches, otherwise AssertionFailure. +inline ::testing::AssertionResult verifyWorkingDir(const std::string& expected_working_dir) +{ + std::ostringstream failures; + + std::array buf{}; + char* result = getcwd(buf.data(), buf.size()); + if (result == nullptr) { - TEST_STEP("Verify working directory") - { - std::array buf{}; - char* result = getcwd(buf.data(), buf.size()); - if (result == nullptr) - { - failures << "Failed to get current working directory\n"; - } - else if (std::string(result) != *expected.working_dir) - { - failures << "Expected working_dir=" << *expected.working_dir << " but got cwd=" << result << "\n"; - } - } + failures << "Failed to get current working directory\n"; + } + else if (std::string(result) != expected_working_dir) + { + failures << "Expected working_dir=" << expected_working_dir << " but got cwd=" << result << "\n"; } - // Scheduling is always verified. When no explicit policy/priority is configured the check runs - // against the launch manager's defaults (SCHED_OTHER at its minimum priority). - TEST_STEP("Verify scheduling policy and priority in the main thread") + return to_result(failures); +} + +/// @brief Verify that the calling thread runs with the expected scheduling policy and priority. +/// +/// Scheduling is always verified. When no explicit policy/priority is configured the check runs +/// against the launch manager's defaults: SCHED_OTHER, at the minimum priority for the effective +/// policy (sched_get_priority_min(): 1 for SCHED_FIFO/SCHED_RR, 0 for SCHED_OTHER). +/// +/// @param[in] expected_policy Expected scheduling policy; defaults to SCHED_OTHER when unset. +/// @param[in] expected_priority Expected scheduling priority; defaults to the policy minimum when unset. +/// @param[in] context Human readable name of the thread, used in failure messages. +/// @return AssertionSuccess if policy and priority match, otherwise AssertionFailure. +inline ::testing::AssertionResult verifyScheduling(const std::optional expected_policy, + const std::optional expected_priority, + const std::string& context) +{ + std::ostringstream failures; + + int policy = -1; + sched_param param{}; + const int result = pthread_getschedparam(pthread_self(), &policy, ¶m); + if (result != 0) { - verify_scheduling(expected, "main thread", failures); + // 'policy' was not written, so it is still the sentinel -1. Bail out before it can reach + // sched_get_priority_min() below (which would return -1 and set errno=EINVAL). + failures << context << ": pthread_getschedparam failed rc=" << result << "\n"; + return to_result(failures); } - TEST_STEP("Verify scheduling policy and priority in a spawned thread") + const int effective_policy = expected_policy.value_or(SCHED_OTHER); + if (policy != effective_policy) { - // A thread created with default attributes inherits the scheduling policy and priority of - // its creating thread, so the configured settings must apply here as well. The thread - // accumulates into its own stream to avoid a data race on 'failures', which is merged in - // after the join. - std::ostringstream thread_failures; - std::thread worker([&expected, &thread_failures]() { - verify_scheduling(expected, "spawned thread", thread_failures); - }); - worker.join(); - failures << thread_failures.str(); + failures << context << ": Expected scheduling policy=" << policy_name(effective_policy) + << (expected_policy.has_value() ? "" : " (default)") << " but got " << policy_name(policy) << "\n"; } - if (failures.tellp() != std::ostringstream::pos_type(0)) + const int effective_priority = expected_priority.value_or(sched_get_priority_min(effective_policy)); + if (param.sched_priority != effective_priority) { - return ::testing::AssertionFailure() << failures.str(); + failures << context << ": Expected scheduling priority=" << effective_priority + << (expected_priority.has_value() ? "" : " (default)") << " but got " << param.sched_priority << "\n"; } - return ::testing::AssertionSuccess(); + + return to_result(failures); } } // namespace sandbox_options From 12592d16af7eccc1da89266229b4e2c7221d6cac Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 31 Jul 2026 12:40:32 +0200 Subject: [PATCH 35/37] Add timeout option to prevent warning Before, this warning appeared: Test execution time ... outside of range for MODERATE tests. Consider setting timeout="short" or size="small". --- tests/integration/sandbox_options/BUILD | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/sandbox_options/BUILD b/tests/integration/sandbox_options/BUILD index 3205015d8..3d2ace9a5 100644 --- a/tests/integration/sandbox_options/BUILD +++ b/tests/integration/sandbox_options/BUILD @@ -72,6 +72,7 @@ cc_binary( integration_test( name = "sandbox_options", + timeout = "short", srcs = ["sandbox_options.py"], binaries = [ ":control_daemon_mock", From 120e54dfc44d22b901f7c4902624ced98e6aae2a Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 31 Jul 2026 15:54:05 +0200 Subject: [PATCH 36/37] Remove duplication in build file --- tests/integration/sandbox_options/BUILD | 57 +++++++------------------ 1 file changed, 16 insertions(+), 41 deletions(-) diff --git a/tests/integration/sandbox_options/BUILD b/tests/integration/sandbox_options/BUILD index 3d2ace9a5..2c7976f84 100644 --- a/tests/integration/sandbox_options/BUILD +++ b/tests/integration/sandbox_options/BUILD @@ -28,47 +28,22 @@ cc_binary( # Three binaries are built from the same source. Each verifies the sandbox options passed to it on # the command line (see sandbox_options.json), and derives its GTest XML result file name from its # own executable name so the results do not collide. -cc_binary( - name = "sandbox_options_process_a", - srcs = [ - "sandbox_options_process.cpp", - "verify_sandbox.hpp", - ], - deps = [ - "//score/launch_manager:control_cc", - "//score/launch_manager:lifecycle_cc", - "//tests/utils/test_helper", - "@googletest//:gtest_main", - ], -) - -cc_binary( - name = "sandbox_options_process_b", - srcs = [ - "sandbox_options_process.cpp", - "verify_sandbox.hpp", - ], - deps = [ - "//score/launch_manager:control_cc", - "//score/launch_manager:lifecycle_cc", - "//tests/utils/test_helper", - "@googletest//:gtest_main", - ], -) - -cc_binary( - name = "sandbox_options_process_c", - srcs = [ - "sandbox_options_process.cpp", - "verify_sandbox.hpp", - ], - deps = [ - "//score/launch_manager:control_cc", - "//score/launch_manager:lifecycle_cc", - "//tests/utils/test_helper", - "@googletest//:gtest_main", - ], -) +[ + cc_binary( + name = "sandbox_options_process_{}".format(suffix), + srcs = [ + "sandbox_options_process.cpp", + "verify_sandbox.hpp", + ], + deps = [ + "//score/launch_manager:control_cc", + "//score/launch_manager:lifecycle_cc", + "//tests/utils/test_helper", + "@googletest//:gtest_main", + ], + ) + for suffix in ("a", "b", "c") +] integration_test( name = "sandbox_options", From 2cb996f1dc24c0585079aa8bfd02f91fa7251dc3 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 31 Jul 2026 16:58:26 +0200 Subject: [PATCH 37/37] Add capability SYS_NICE to devcontainer --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b66f7824f..fc243f851 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -62,6 +62,6 @@ }, - "runArgs": ["--device=/dev/kvm", "--network", "host"], + "runArgs": ["--device=/dev/kvm", "--network", "host", "--cap-add=SYS_NICE"], "postCreateCommand": "sudo chmod 666 /dev/kvm && sudo usermod -aG kvm $USER" }