From 47e42893f84e00f294a6ea5c5726ef58c78f6b6f Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Mon, 20 Jul 2026 17:15:29 +0200 Subject: [PATCH 01/47] 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 0f85f37997e280beff0ffc23194b53b331bd6a60 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Tue, 21 Jul 2026 09:25:31 +0200 Subject: [PATCH 02/47] 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 58aa1389c20a994e36be49ece156d0def5c6f42f Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Tue, 21 Jul 2026 11:30:06 +0200 Subject: [PATCH 03/47] 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 e6ededc57..1ea29a548 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 f1518e6ec757da1ed81c808a612c48ce38ccd916 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 22 Jul 2026 15:00:29 +0200 Subject: [PATCH 04/47] 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 734c4cdba2b2631165e1950dfd6337d103b68878 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 22 Jul 2026 16:34:40 +0200 Subject: [PATCH 05/47] 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 b60b14d85e8ffbf3a16fe4a27687243ec0868e20 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 22 Jul 2026 16:39:12 +0200 Subject: [PATCH 06/47] 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 cd3af3928c3627cd7e842cae89b9db1e714a5be7 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 22 Jul 2026 17:05:06 +0200 Subject: [PATCH 07/47] 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 80d622421e20c70fdf4dfcecd36f2b32777f7d0c Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 23 Jul 2026 09:28:59 +0200 Subject: [PATCH 08/47] 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 0b473cbd7a73222b78e4765bf63e656f2822ebcd Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 23 Jul 2026 14:30:23 +0200 Subject: [PATCH 09/47] 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 96a282a0a4264219a8db5cc07e582c5bde85cfc6 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 23 Jul 2026 14:55:22 +0200 Subject: [PATCH 10/47] 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 9b58c52e16d9222ab58c474ce04f48c36bd2fd11 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 23 Jul 2026 18:51:13 +0200 Subject: [PATCH 11/47] 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 1ea29a548..c4c65a1a1 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 93250f702e08f9368f612688f096a121519e911c Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 23 Jul 2026 19:08:06 +0200 Subject: [PATCH 12/47] 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 2b1c83d7a..d8406df6c 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -972,8 +972,8 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_docs_as_code/6.0.0/MODULE.bazel": "ab2af2d8fab73e4512d2e2bd399a64d10c5c5463388322f7025637b13ec7585c", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_docs_as_code/6.0.0/source.json": "c3992257800c4e3408be5e4656c0903fc19d3933011cc58e85d9a29c7214f374", "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.3/MODULE.bazel": "2fdb3cca15348f127fc356b27f84bd58d841530a94c58aedeefe09c47d8bfe8a", 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 af1c8888b416edc8b4475d7d23553e45ccb50ad5 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 24 Jul 2026 08:45:28 +0200 Subject: [PATCH 13/47] 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 c4c65a1a1..02fd3ecbb 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 d2e4381be8b5b12e58fddd373db9d5089aacac6d Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 24 Jul 2026 11:44:40 +0200 Subject: [PATCH 14/47] 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 02fd3ecbb..e6ededc57 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 f8476aaff209b2b86842410e989d5f64c265577e Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 24 Jul 2026 14:53:33 +0200 Subject: [PATCH 15/47] 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 73954206ef040e7922bad7051f43f44e619bcf29 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 24 Jul 2026 15:37:52 +0200 Subject: [PATCH 16/47] 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 2dcac910c86b37f5247eb9b3c097ef6458aa981c Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 24 Jul 2026 15:42:49 +0200 Subject: [PATCH 17/47] 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 af6274a36672012da48a30c6c60bd536d87f9bfc Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 24 Jul 2026 16:23:33 +0200 Subject: [PATCH 18/47] 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 c4ad1064096c0041eec365ab5a8f94e53efeeb83 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Mon, 27 Jul 2026 12:41:04 +0200 Subject: [PATCH 19/47] 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 58c475da89a694f096b9428118f4b37d3624c5cb Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Mon, 27 Jul 2026 13:23:33 +0200 Subject: [PATCH 20/47] 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 25e857b9cae6814e18983b658299851a3710c56a Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 29 Jul 2026 13:50:12 +0200 Subject: [PATCH 21/47] 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 df1651b101ceb4d9b2244e379a4c6bbe33183208 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 29 Jul 2026 14:18:20 +0200 Subject: [PATCH 22/47] 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 9d32c49e4beb5e196ff1d83de6b76c8908cf9bb6 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 29 Jul 2026 14:20:28 +0200 Subject: [PATCH 23/47] 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 66cac30fc75014311e4abcd655b203f167b52a3b Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 29 Jul 2026 14:52:21 +0200 Subject: [PATCH 24/47] 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 c5ce909a46f51dc3efc44b3411b47699b9e6d975 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 29 Jul 2026 14:52:42 +0200 Subject: [PATCH 25/47] 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 c8eee88d4df83c561093fd9bc153ff3551e70b3b Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Wed, 29 Jul 2026 16:41:35 +0200 Subject: [PATCH 26/47] 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 27040f592337513666b54c1df7922ed17698b197 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 30 Jul 2026 11:06:50 +0200 Subject: [PATCH 27/47] 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 36d60fcf7d4cb5b7e3da8036eecd28d8fd00a2a8 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 30 Jul 2026 11:07:59 +0200 Subject: [PATCH 28/47] 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 e82cb715eacca996a885ea63f53e614fca817d71 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 30 Jul 2026 16:33:07 +0200 Subject: [PATCH 29/47] 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 c4fd3d90e0f293cc1a92b828816a3d7d695848aa Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 30 Jul 2026 18:52:26 +0200 Subject: [PATCH 30/47] 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 bece241581ce32a6a61de4204cdd70c287a65566 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Thu, 30 Jul 2026 19:03:39 +0200 Subject: [PATCH 31/47] 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 7583f9151101e60c6d0660d8759806aa24b1190e Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 31 Jul 2026 09:21:07 +0200 Subject: [PATCH 32/47] 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 b2c97c4663feeef313546850b3f11bf7804b51c0 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 31 Jul 2026 11:58:54 +0200 Subject: [PATCH 33/47] 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 d8406df6c..2b1c83d7a 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -972,8 +972,8 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_docs_as_code/6.0.0/MODULE.bazel": "ab2af2d8fab73e4512d2e2bd399a64d10c5c5463388322f7025637b13ec7585c", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_docs_as_code/6.0.0/source.json": "c3992257800c4e3408be5e4656c0903fc19d3933011cc58e85d9a29c7214f374", "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.3/MODULE.bazel": "2fdb3cca15348f127fc356b27f84bd58d841530a94c58aedeefe09c47d8bfe8a", From d23869ae3b5a1bea4be15fddf8e4475648a3f420 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 31 Jul 2026 12:28:33 +0200 Subject: [PATCH 34/47] 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 ea9c7160769cdb45f598e4daac14d7ec236d6718 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 31 Jul 2026 12:40:32 +0200 Subject: [PATCH 35/47] 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 0769a622cbfcbd9137184f1befe39758639b373f Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 31 Jul 2026 15:54:05 +0200 Subject: [PATCH 36/47] 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 089aa026c0790f3381b8d3795ea79552cd1f5dd7 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Fri, 31 Jul 2026 16:58:26 +0200 Subject: [PATCH 37/47] 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" } From 64bc78572dce89b190097693a4bb190182b640fe Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Mon, 3 Aug 2026 07:40:22 +0000 Subject: [PATCH 38/47] Get rid of explicit file names Resp. remove outdated file names --- patches/BUILD | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/patches/BUILD b/patches/BUILD index 401257be0..a9fcc84b0 100644 --- a/patches/BUILD +++ b/patches/BUILD @@ -11,8 +11,4 @@ # 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"]) +exports_files(["*.patch"]) From 8b9acb0cb79697b46d872dd6b40cf2c056af3190 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Mon, 3 Aug 2026 12:20:13 +0200 Subject: [PATCH 39/47] Have proxy available at all steps Proxy was missing during sudo apt commands. Now config file for apt is written, which uses the content of the environment variable $http_proxy --- .devcontainer/devcontainer.json | 35 +++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index fc243f851..67a755d9c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,7 +5,20 @@ "name": "eclipse-s-core", "build": { - "dockerfile": "Dockerfile" + "dockerfile": "Dockerfile", + // runArgs (--network host) only applies to `docker run`. Build RUN steps + // (e.g. feature apt-get installs) run in an isolated build network where a + // loopback proxy at 127.0.0.1 is unreachable. Build on the host network and + // forward the proxy as build args so those steps can reach the internet. + "options": ["--network=host"], + "args": { + "HTTP_PROXY": "${localEnv:HTTP_PROXY}", + "HTTPS_PROXY": "${localEnv:HTTPS_PROXY}", + "http_proxy": "${localEnv:http_proxy}", + "https_proxy": "${localEnv:https_proxy}", + "NO_PROXY": "${localEnv:NO_PROXY}", + "no_proxy": "${localEnv:no_proxy}" + } }, "features": { @@ -22,6 +35,19 @@ }, "containerEnv": { + // Forward the host proxy into the *running* container. build.args only + // cover `docker build`; without these the running container (apt, git, + // curl, bazel, ...) has no proxy and falls back to direct DNS, which the + // corporate network blocks. Works at runtime because runArgs uses + // --network host, so proxy on the Windows host is reachable. + "HTTP_PROXY": "${localEnv:HTTP_PROXY}", + "HTTPS_PROXY": "${localEnv:HTTPS_PROXY}", + "http_proxy": "${localEnv:http_proxy}", + "https_proxy": "${localEnv:https_proxy}", + "ALL_PROXY": "${localEnv:ALL_PROXY}", + "all_proxy": "${localEnv:all_proxy}", + "NO_PROXY": "${localEnv:NO_PROXY}", + "no_proxy": "${localEnv:no_proxy}", "ANTHROPIC_FOUNDRY_API_KEY": "${localEnv:ANTHROPIC_FOUNDRY_API_KEY}", "ANTHROPIC_FOUNDRY_RESOURCE": "${localEnv:ANTHROPIC_FOUNDRY_RESOURCE}", "CLAUDE_CODE_USE_FOUNDRY": "${localEnv:CLAUDE_CODE_USE_FOUNDRY}", @@ -57,7 +83,12 @@ // Add your personal customizations "onCreateCommand": { - "update certificates & install dependencies": "sudo apt update && sudo apt install -y --no-install-recommends ca-certificates-java openjdk-17-jre-headless libacl1-dev tmux fakechroot libvirt-daemon-system libvirt-clients bridge-utils virt-manager qemu-system-x86 qemu-system-arm && sudo update-ca-certificates", + // Object-form lifecycle commands run in PARALLEL, so the apt proxy config is + // written inline (not as a separate entry) to guarantee it exists before apt runs. + // sudo resets the environment (env_reset), dropping http_proxy/https_proxy, so + // `sudo apt` would otherwise ignore the proxy vars from containerEnv. The apt.conf + // drop-in survives sudo and persists for later apt use. + "update certificates & install dependencies": "if [ -n \"$http_proxy\" ]; then printf 'Acquire::http::Proxy \"%s\";\\nAcquire::https::Proxy \"%s\";\\n' \"$http_proxy\" \"${https_proxy:-$http_proxy}\" | sudo tee /etc/apt/apt.conf.d/00proxy > /dev/null; fi && sudo apt update && sudo apt install -y --no-install-recommends ca-certificates-java openjdk-17-jre-headless libacl1-dev tmux fakechroot libvirt-daemon-system libvirt-clients bridge-utils virt-manager qemu-system-x86 qemu-system-arm && sudo update-ca-certificates", "bazel use system trust store": "echo 'startup --host_jvm_args=-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts --host_jvm_args=-Djavax.net.ssl.trustStorePassword=changeit' | sudo tee --append /etc/bazel.bazelrc" }, From 9e44e31dbf16af7f2e66a4f89dc02ecf12e08e67 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Mon, 3 Aug 2026 14:43:04 +0200 Subject: [PATCH 40/47] Get rid of outdated env variables Instead provide a way to keep environment variables needed on run time private. --- .devcontainer/devcontainer.json | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 67a755d9c..9c3f229a3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -10,8 +10,8 @@ // (e.g. feature apt-get installs) run in an isolated build network where a // loopback proxy at 127.0.0.1 is unreachable. Build on the host network and // forward the proxy as build args so those steps can reach the internet. - "options": ["--network=host"], - "args": { + "options": ["--network=${localEnv:DEVCONTAINER_NETWORK:bridge}"], + "args": { "HTTP_PROXY": "${localEnv:HTTP_PROXY}", "HTTPS_PROXY": "${localEnv:HTTPS_PROXY}", "http_proxy": "${localEnv:http_proxy}", @@ -47,20 +47,16 @@ "ALL_PROXY": "${localEnv:ALL_PROXY}", "all_proxy": "${localEnv:all_proxy}", "NO_PROXY": "${localEnv:NO_PROXY}", - "no_proxy": "${localEnv:no_proxy}", - "ANTHROPIC_FOUNDRY_API_KEY": "${localEnv:ANTHROPIC_FOUNDRY_API_KEY}", - "ANTHROPIC_FOUNDRY_RESOURCE": "${localEnv:ANTHROPIC_FOUNDRY_RESOURCE}", - "CLAUDE_CODE_USE_FOUNDRY": "${localEnv:CLAUDE_CODE_USE_FOUNDRY}", - "ANTHROPIC_DEFAULT_FABLE_MODEL": "${localEnv:ANTHROPIC_DEFAULT_FABLE_MODEL}", - "ANTHROPIC_DEFAULT_OPUS_MODEL": "${localEnv:ANTHROPIC_DEFAULT_OPUS_MODEL}", - "ANTHROPIC_DEFAULT_SONNET_MODEL": "${localEnv:ANTHROPIC_DEFAULT_SONNET_MODEL}", - "ANTHROPIC_DEFAULT_HAIKU_MODEL": "${localEnv:ANTHROPIC_DEFAULT_HAIKU_MODEL}" + "no_proxy": "${localEnv:no_proxy}" }, "initializeCommand": { "Initialize bazel cache on host": "mkdir -p ${localEnv:HOME}/.cache/bazel", "Make sure QNX licenses exists": "touch -a ~/.qnx/license/licenses", - "Make sure .netrc exists": "touch -a ~/.netrc"}, + "Make sure .netrc exists": "touch -a ~/.netrc", + // The env-file is used to pass custom environment variables into the container. + // Use format `KEY=VALUE` per line. The file is created if it doesn't exist. + "Make sure env-file exists": "touch -a ~/.config/score-lifecycle-devcontainer.env"}, // Mount ca-certificates from host "mounts": [ @@ -92,7 +88,6 @@ "bazel use system trust store": "echo 'startup --host_jvm_args=-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts --host_jvm_args=-Djavax.net.ssl.trustStorePassword=changeit' | sudo tee --append /etc/bazel.bazelrc" }, - - "runArgs": ["--device=/dev/kvm", "--network", "host", "--cap-add=SYS_NICE"], + "runArgs": ["--device=/dev/kvm", "--network=${localEnv:DEVCONTAINER_NETWORK:bridge}", "--cap-add=SYS_NICE", "--env-file", "${localEnv:HOME}/.config/score-lifecycle-devcontainer.env"], "postCreateCommand": "sudo chmod 666 /dev/kvm && sudo usermod -aG kvm $USER" } From 50d1321480b75989851e382730d33f6f450c0ccc Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Mon, 3 Aug 2026 18:39:03 +0200 Subject: [PATCH 41/47] Rework verification_process incl. debugging output To be used also to signal test_end. --- .../complex_monitoring.json | 3 ++ .../crash_on_startup/crash_on_startup.json | 3 ++ .../process_complex_rep_failure.json | 3 ++ .../process_crash_monitoring.json | 3 ++ .../process_simple_rep_failure.json | 3 ++ .../process_wrong_binary_failure.json | 3 ++ .../test_helper/verification_process.cpp | 44 +++++++++++++++++-- 7 files changed, 58 insertions(+), 4 deletions(-) diff --git a/tests/integration/complex_monitoring/complex_monitoring.json b/tests/integration/complex_monitoring/complex_monitoring.json index deaf9eaf8..f01be07ad 100644 --- a/tests/integration/complex_monitoring/complex_monitoring.json +++ b/tests/integration/complex_monitoring/complex_monitoring.json @@ -75,6 +75,9 @@ "verification_component": { "component_properties": { "binary_name": "verification_process", + "process_arguments": [ + "fallback" + ], "application_profile": { "application_type": "Native", "is_self_terminating": true diff --git a/tests/integration/crash_on_startup/crash_on_startup.json b/tests/integration/crash_on_startup/crash_on_startup.json index 1e4367065..4ee6fc2bc 100644 --- a/tests/integration/crash_on_startup/crash_on_startup.json +++ b/tests/integration/crash_on_startup/crash_on_startup.json @@ -99,6 +99,9 @@ "verification_component": { "component_properties": { "binary_name": "verification_process", + "process_arguments": [ + "fallback" + ], "application_profile": { "application_type": "Native", "is_self_terminating": true diff --git a/tests/integration/process_complex_rep_failure/process_complex_rep_failure.json b/tests/integration/process_complex_rep_failure/process_complex_rep_failure.json index 1d1bac8c9..05c04e395 100644 --- a/tests/integration/process_complex_rep_failure/process_complex_rep_failure.json +++ b/tests/integration/process_complex_rep_failure/process_complex_rep_failure.json @@ -93,6 +93,9 @@ "verification_component": { "component_properties": { "binary_name": "verification_process", + "process_arguments": [ + "fallback" + ], "application_profile": { "application_type": "Native", "is_self_terminating": true diff --git a/tests/integration/process_crash_monitoring/process_crash_monitoring.json b/tests/integration/process_crash_monitoring/process_crash_monitoring.json index 443cd4033..c3f518171 100644 --- a/tests/integration/process_crash_monitoring/process_crash_monitoring.json +++ b/tests/integration/process_crash_monitoring/process_crash_monitoring.json @@ -76,6 +76,9 @@ "verification_component": { "component_properties": { "binary_name": "verification_process", + "process_arguments": [ + "fallback" + ], "application_profile": { "application_type": "Native", "is_self_terminating": true diff --git a/tests/integration/process_simple_rep_failure/process_simple_rep_failure.json b/tests/integration/process_simple_rep_failure/process_simple_rep_failure.json index ae234cd2f..cfdf4a953 100644 --- a/tests/integration/process_simple_rep_failure/process_simple_rep_failure.json +++ b/tests/integration/process_simple_rep_failure/process_simple_rep_failure.json @@ -93,6 +93,9 @@ "verification_component": { "component_properties": { "binary_name": "verification_process", + "process_arguments": [ + "fallback" + ], "application_profile": { "application_type": "Native", "is_self_terminating": true diff --git a/tests/integration/process_wrong_binary_failure/process_wrong_binary_failure.json b/tests/integration/process_wrong_binary_failure/process_wrong_binary_failure.json index 1a0c13860..4a9c074a6 100644 --- a/tests/integration/process_wrong_binary_failure/process_wrong_binary_failure.json +++ b/tests/integration/process_wrong_binary_failure/process_wrong_binary_failure.json @@ -78,6 +78,9 @@ "verification_component": { "component_properties": { "binary_name": "verification_process", + "process_arguments": [ + "fallback" + ], "application_profile": { "application_type": "Native", "is_self_terminating": true diff --git a/tests/utils/test_helper/verification_process.cpp b/tests/utils/test_helper/verification_process.cpp index bd44732c2..8cc362334 100644 --- a/tests/utils/test_helper/verification_process.cpp +++ b/tests/utils/test_helper/verification_process.cpp @@ -12,14 +12,50 @@ ********************************************************************************/ #include "tests/utils/test_helper/test_helper.hpp" +#include #include +#include -int main() +/// @file verification_process.cpp +/// @brief Test process that touches signal files selected via command line +/// arguments. Pass "fallback" to touch the fallback file and/or +/// "test_end" to touch the test_end file. + +int main(int argc, char** argv) { - // This process must be active only in the fallback run target - if (!touch_file(fallback_file)) + std::cout << "verification_process: Working directory: " << std::filesystem::current_path() << std::endl; + + bool touch_fallback = false; + bool touch_test_end = false; + + for (int i = 1; i < argc; ++i) + { + const std::string_view arg{argv[i]}; + if (arg == "fallback") + { + touch_fallback = true; + } + else if (arg == "test_end") + { + std::cout << "Touching test_end file." << std::endl; + touch_test_end = true; + } + else + { + std::cerr << "Unknown argument '" << arg << "', expected 'fallback' and/or 'test_end'" << std::endl; + return EXIT_FAILURE; + } + } + + if (touch_fallback && !touch_file(fallback_file)) + { + std::cout << "Failed to write fallback file!" << std::endl; + return EXIT_FAILURE; + } + + if (touch_test_end && !touch_file(test_end_location)) { - std::cout << "Failed to write file!" << std::endl; + std::cout << "Failed to write test_end file!" << std::endl; return EXIT_FAILURE; } From 2552a5316acf2d0e5ed841fac812c508f417dbfc Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Mon, 3 Aug 2026 18:42:03 +0200 Subject: [PATCH 42/47] Get rid of control_daemon_mock.cpp Currently not passing. --- tests/integration/sandbox_options/BUILD | 14 +--- .../sandbox_options/control_daemon_mock.cpp | 47 ------------ .../sandbox_options/sandbox_options.json | 71 ++++++++++--------- .../sandbox_options/sandbox_options.py | 1 - 4 files changed, 37 insertions(+), 96 deletions(-) delete mode 100644 tests/integration/sandbox_options/control_daemon_mock.cpp diff --git a/tests/integration/sandbox_options/BUILD b/tests/integration/sandbox_options/BUILD index 2c7976f84..93ea67644 100644 --- a/tests/integration/sandbox_options/BUILD +++ b/tests/integration/sandbox_options/BUILD @@ -13,18 +13,6 @@ load("@rules_cc//cc:defs.bzl", "cc_binary") load("//tests/utils/bazel:integration.bzl", "integration_test") -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", - ], -) - # 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. @@ -50,11 +38,11 @@ integration_test( timeout = "short", srcs = ["sandbox_options.py"], binaries = [ - ":control_daemon_mock", ":sandbox_options_process_a", ":sandbox_options_process_b", ":sandbox_options_process_c", "//score/launch_manager", + "//tests/utils/test_helper:verification_process" ], config = ":sandbox_options.json", ) diff --git a/tests/integration/sandbox_options/control_daemon_mock.cpp b/tests/integration/sandbox_options/control_daemon_mock.cpp deleted file mode 100644 index 2c4f6084b..000000000 --- a/tests/integration/sandbox_options/control_daemon_mock.cpp +++ /dev/null @@ -1,47 +0,0 @@ -/******************************************************************************** - * 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(SandboxOptions, Daemon) -{ - score::mw::lifecycle::ControlClient client{}; - ASSERT_TRUE(check_clean({test_end_location})); - TEST_STEP("Control daemon report running") - { - score::mw::lifecycle::report_running(); - } - - TEST_STEP("Activate RunTarget Running") - { - score::cpp::stop_token stop_token; - auto result = client.ActivateRunTarget("Running").Get(stop_token); - EXPECT_TRUE(result.has_value()) << "Activating target Running failed: " << result.error().Message(); - } - - TEST_STEP("Activate RunTarget Off") - { - client.ActivateRunTarget("Off"); - } -} - -int main() -{ - 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 index 5c879cb9e..448f483f1 100644 --- a/tests/integration/sandbox_options/sandbox_options.json +++ b/tests/integration/sandbox_options/sandbox_options.json @@ -42,24 +42,6 @@ } }, "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_options_process_a": { "component_properties": { "binary_name": "sandbox_options_process_a", @@ -74,7 +56,10 @@ "--scheduling-policy=SCHED_FIFO", "--scheduling-priority=10", "--working-dir=/tmp" - ] + ], + "ready_condition": { + "process_state": "Terminated" + } }, "deployment_config": { "environmental_variables": { @@ -102,7 +87,10 @@ "--gid=0", "--scheduling-policy=SCHED_RR", "--scheduling-priority=10" - ] + ], + "ready_condition": { + "process_state": "Terminated" + } }, "deployment_config": { "environmental_variables": { @@ -128,32 +116,45 @@ "--uid=0", "--gid=0", "--working-dir=/tmp/tests/sandbox_options" - ] + ], + "ready_condition": { + "process_state": "Terminated" + } }, "deployment_config": { "environmental_variables": { "PROCESSIDENTIFIER": "DefaultPG_app2" } } + }, + "verification_component": { + "component_properties": { + "binary_name": "verification_process", + "depends_on": ["sandbox_options_process_a", "sandbox_options_process_b", "sandbox_options_process_c"], + "process_arguments": [ + "test_end" + ], + "application_profile": { + "application_type": "Native", + "is_self_terminating": true + }, + "ready_condition": { + "process_state": "Terminated" + } + }, + "deployment_config": { + "ready_timeout": 1.0, + "shutdown_timeout": 1.0, + "environmental_variables": { + "PROCESSIDENTIFIER": "verification_component" + } + } } }, "run_targets": { "Startup": { "depends_on": [ - "control_daemon" - ], - "recovery_action": { - "switch_run_target": { - "run_target": "fallback_run_target" - } - } - }, - "Running": { - "depends_on": [ - "control_daemon", - "sandbox_options_process_a", - "sandbox_options_process_b", - "sandbox_options_process_c" + "verification_component" ], "recovery_action": { "switch_run_target": { diff --git a/tests/integration/sandbox_options/sandbox_options.py b/tests/integration/sandbox_options/sandbox_options.py index e7fbad38b..2db4bcb6b 100644 --- a/tests/integration/sandbox_options/sandbox_options.py +++ b/tests/integration/sandbox_options/sandbox_options.py @@ -67,7 +67,6 @@ def test_sandbox_options(target, setup_test, assert_test_results, remote_test_di # writes into remote_test_dir. Search all relevant directories. assert_test_results( { - "control_daemon_mock.xml", "sandbox_options_process_a.xml", "sandbox_options_process_b.xml", "sandbox_options_process_c.xml", From 896e67e6b8d66b5a6c985e38367dc1c9c26224fd Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Mon, 3 Aug 2026 19:09:53 +0200 Subject: [PATCH 43/47] Fix and cleanup test --- tests/integration/sandbox_options/sandbox_options.json | 7 ------- .../sandbox_options/sandbox_options_process.cpp | 2 +- tests/utils/test_helper/verification_process.cpp | 3 --- 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/tests/integration/sandbox_options/sandbox_options.json b/tests/integration/sandbox_options/sandbox_options.json index 448f483f1..1aa03c937 100644 --- a/tests/integration/sandbox_options/sandbox_options.json +++ b/tests/integration/sandbox_options/sandbox_options.json @@ -141,13 +141,6 @@ "ready_condition": { "process_state": "Terminated" } - }, - "deployment_config": { - "ready_timeout": 1.0, - "shutdown_timeout": 1.0, - "environmental_variables": { - "PROCESSIDENTIFIER": "verification_component" - } } } }, diff --git a/tests/integration/sandbox_options/sandbox_options_process.cpp b/tests/integration/sandbox_options/sandbox_options_process.cpp index f0d61094c..d4c0d846d 100644 --- a/tests/integration/sandbox_options/sandbox_options_process.cpp +++ b/tests/integration/sandbox_options/sandbox_options_process.cpp @@ -195,5 +195,5 @@ int main(int argc, char** argv) // 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(); + return TestRunner(executable_path, TerminationBehavior::kContinue).RunTests(); } diff --git a/tests/utils/test_helper/verification_process.cpp b/tests/utils/test_helper/verification_process.cpp index 8cc362334..34845bccc 100644 --- a/tests/utils/test_helper/verification_process.cpp +++ b/tests/utils/test_helper/verification_process.cpp @@ -12,7 +12,6 @@ ********************************************************************************/ #include "tests/utils/test_helper/test_helper.hpp" -#include #include #include @@ -23,8 +22,6 @@ int main(int argc, char** argv) { - std::cout << "verification_process: Working directory: " << std::filesystem::current_path() << std::endl; - bool touch_fallback = false; bool touch_test_end = false; From 08a36df106dbb8d30628ea73bd02658f89fa9de1 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Mon, 3 Aug 2026 19:19:50 +0200 Subject: [PATCH 44/47] Fix documentation --- 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 eb79343e0..a16f9e3d1 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 and process checks (default: 0.5). + :param poll_interval_s: seconds between file checks (default: 0.5). :param stop_timeout_s: maximum seconds to wait for the process to terminate after SIGTERM (default: 3). :param args: optional list of arguments to pass to the binary. From 1a4c96413ce4ebc7230c1738ebbf394f20c4756e Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Mon, 3 Aug 2026 19:22:37 +0200 Subject: [PATCH 45/47] Drop Off Run Target --- tests/integration/sandbox_options/sandbox_options.json | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/integration/sandbox_options/sandbox_options.json b/tests/integration/sandbox_options/sandbox_options.json index 1aa03c937..5005139d8 100644 --- a/tests/integration/sandbox_options/sandbox_options.json +++ b/tests/integration/sandbox_options/sandbox_options.json @@ -154,15 +154,6 @@ "run_target": "fallback_run_target" } } - }, - "Off": { - "depends_on": [], - "recovery_action": { - "switch_run_target": { - "run_target": "fallback_run_target" - - } - } } }, "initial_run_target": "Startup", From 21c95c2007a567270271000e477998c54c1a904d Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Mon, 3 Aug 2026 19:25:18 +0200 Subject: [PATCH 46/47] Format fix --- tests/integration/sandbox_options/BUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/sandbox_options/BUILD b/tests/integration/sandbox_options/BUILD index 93ea67644..37029d146 100644 --- a/tests/integration/sandbox_options/BUILD +++ b/tests/integration/sandbox_options/BUILD @@ -42,7 +42,7 @@ integration_test( ":sandbox_options_process_b", ":sandbox_options_process_c", "//score/launch_manager", - "//tests/utils/test_helper:verification_process" + "//tests/utils/test_helper:verification_process", ], config = ":sandbox_options.json", ) From ca35c57f3ecf5aa9213d770532111104c00aad72 Mon Sep 17 00:00:00 2001 From: Timo Steuerwald Date: Mon, 3 Aug 2026 19:27:32 +0200 Subject: [PATCH 47/47] Revert heavy modifications in devcontainer.json This reverts commit 9e44e31dbf16af7f2e66a4f89dc02ecf12e08e67. --- .devcontainer/devcontainer.json | 50 ++++++++------------------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9c3f229a3..fc243f851 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,20 +5,7 @@ "name": "eclipse-s-core", "build": { - "dockerfile": "Dockerfile", - // runArgs (--network host) only applies to `docker run`. Build RUN steps - // (e.g. feature apt-get installs) run in an isolated build network where a - // loopback proxy at 127.0.0.1 is unreachable. Build on the host network and - // forward the proxy as build args so those steps can reach the internet. - "options": ["--network=${localEnv:DEVCONTAINER_NETWORK:bridge}"], - "args": { - "HTTP_PROXY": "${localEnv:HTTP_PROXY}", - "HTTPS_PROXY": "${localEnv:HTTPS_PROXY}", - "http_proxy": "${localEnv:http_proxy}", - "https_proxy": "${localEnv:https_proxy}", - "NO_PROXY": "${localEnv:NO_PROXY}", - "no_proxy": "${localEnv:no_proxy}" - } + "dockerfile": "Dockerfile" }, "features": { @@ -35,28 +22,19 @@ }, "containerEnv": { - // Forward the host proxy into the *running* container. build.args only - // cover `docker build`; without these the running container (apt, git, - // curl, bazel, ...) has no proxy and falls back to direct DNS, which the - // corporate network blocks. Works at runtime because runArgs uses - // --network host, so proxy on the Windows host is reachable. - "HTTP_PROXY": "${localEnv:HTTP_PROXY}", - "HTTPS_PROXY": "${localEnv:HTTPS_PROXY}", - "http_proxy": "${localEnv:http_proxy}", - "https_proxy": "${localEnv:https_proxy}", - "ALL_PROXY": "${localEnv:ALL_PROXY}", - "all_proxy": "${localEnv:all_proxy}", - "NO_PROXY": "${localEnv:NO_PROXY}", - "no_proxy": "${localEnv:no_proxy}" + "ANTHROPIC_FOUNDRY_API_KEY": "${localEnv:ANTHROPIC_FOUNDRY_API_KEY}", + "ANTHROPIC_FOUNDRY_RESOURCE": "${localEnv:ANTHROPIC_FOUNDRY_RESOURCE}", + "CLAUDE_CODE_USE_FOUNDRY": "${localEnv:CLAUDE_CODE_USE_FOUNDRY}", + "ANTHROPIC_DEFAULT_FABLE_MODEL": "${localEnv:ANTHROPIC_DEFAULT_FABLE_MODEL}", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "${localEnv:ANTHROPIC_DEFAULT_OPUS_MODEL}", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "${localEnv:ANTHROPIC_DEFAULT_SONNET_MODEL}", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "${localEnv:ANTHROPIC_DEFAULT_HAIKU_MODEL}" }, "initializeCommand": { "Initialize bazel cache on host": "mkdir -p ${localEnv:HOME}/.cache/bazel", "Make sure QNX licenses exists": "touch -a ~/.qnx/license/licenses", - "Make sure .netrc exists": "touch -a ~/.netrc", - // The env-file is used to pass custom environment variables into the container. - // Use format `KEY=VALUE` per line. The file is created if it doesn't exist. - "Make sure env-file exists": "touch -a ~/.config/score-lifecycle-devcontainer.env"}, + "Make sure .netrc exists": "touch -a ~/.netrc"}, // Mount ca-certificates from host "mounts": [ @@ -79,15 +57,11 @@ // Add your personal customizations "onCreateCommand": { - // Object-form lifecycle commands run in PARALLEL, so the apt proxy config is - // written inline (not as a separate entry) to guarantee it exists before apt runs. - // sudo resets the environment (env_reset), dropping http_proxy/https_proxy, so - // `sudo apt` would otherwise ignore the proxy vars from containerEnv. The apt.conf - // drop-in survives sudo and persists for later apt use. - "update certificates & install dependencies": "if [ -n \"$http_proxy\" ]; then printf 'Acquire::http::Proxy \"%s\";\\nAcquire::https::Proxy \"%s\";\\n' \"$http_proxy\" \"${https_proxy:-$http_proxy}\" | sudo tee /etc/apt/apt.conf.d/00proxy > /dev/null; fi && sudo apt update && sudo apt install -y --no-install-recommends ca-certificates-java openjdk-17-jre-headless libacl1-dev tmux fakechroot libvirt-daemon-system libvirt-clients bridge-utils virt-manager qemu-system-x86 qemu-system-arm && sudo update-ca-certificates", + "update certificates & install dependencies": "sudo apt update && sudo apt install -y --no-install-recommends ca-certificates-java openjdk-17-jre-headless libacl1-dev tmux fakechroot libvirt-daemon-system libvirt-clients bridge-utils virt-manager qemu-system-x86 qemu-system-arm && sudo update-ca-certificates", "bazel use system trust store": "echo 'startup --host_jvm_args=-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts --host_jvm_args=-Djavax.net.ssl.trustStorePassword=changeit' | sudo tee --append /etc/bazel.bazelrc" }, - "runArgs": ["--device=/dev/kvm", "--network=${localEnv:DEVCONTAINER_NETWORK:bridge}", "--cap-add=SYS_NICE", "--env-file", "${localEnv:HOME}/.config/score-lifecycle-devcontainer.env"], + + "runArgs": ["--device=/dev/kvm", "--network", "host", "--cap-add=SYS_NICE"], "postCreateCommand": "sudo chmod 666 /dev/kvm && sudo usermod -aG kvm $USER" }