diff --git a/BUILD b/BUILD index 03445323d10..17a41fffb72 100644 --- a/BUILD +++ b/BUILD @@ -12,7 +12,7 @@ # ******************************************************************************* load("@score_docs_as_code//:docs.bzl", "docs") -load("@score_tooling//:defs.bzl", "setup_starpls", "use_format_targets") +load("@score_tooling//:defs.bzl", "copyright_checker", "setup_starpls", "use_format_targets") # Docs-as-code docs( @@ -44,6 +44,38 @@ setup_starpls( # Add target for formatting checks use_format_targets() +# Add copyright check/fix targets: +# - //:copyright.check +# - //:copyright.fix +copyright_checker( + name = "copyright", + srcs = glob( + ["**/*"], + exclude = [ + ".git/**", + ".venv/**", + "bazel-*/**", + "**/*.png", + "**/*.jpg", + "**/*.jpeg", + "**/*.gif", + "**/*.svg", + "**/*.pdf", + "**/*.drawio", + "**/*.ipynb", + "**/*.bin", + "**/*.hash", + "**/*.zip", + "**/*.tar", + "**/*.tar.gz", + "**/*.tgz", + ], + ), + config = "@score_tooling//cr_checker/resources:config", + template = "@score_tooling//cr_checker/resources:templates", + visibility = ["//visibility:public"], +) + exports_files([ "MODULE.bazel", "pyproject.toml", diff --git a/README.md b/README.md index 16c58de3908..0a250e122c5 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,30 @@ To generate a full documentation of all integrated modules, run: bazel run //:docs_combo_experimental ``` +## Feature Integration Tests (FIT) + +Use the Linux config for both Rust and C++ FIT flows: + +```bash +bazel test --config=linux-x86_64 //feature_integration_tests/test_cases:fit +bazel test --config=linux-x86_64 //feature_integration_tests/test_cases:fit_rust +bazel test --config=linux-x86_64 //feature_integration_tests/test_cases:fit_cpp +``` + +Run only lifecycle application interface checks: + +```bash +bazel test --config=linux-x86_64 //feature_integration_tests/test_cases:fit_rust --test_arg=-k --test_arg=lifecycle_application_if +bazel test --config=linux-x86_64 //feature_integration_tests/test_cases:fit_cpp --test_arg=-k --test_arg=lifecycle_application_if +``` + +Build scenario binaries directly: + +```bash +bazel build --config=linux-x86_64 //feature_integration_tests/test_scenarios/rust:rust_test_scenarios +bazel build --config=linux-x86_64 //feature_integration_tests/test_scenarios/cpp:cpp_test_scenarios +``` + ## Operating system integrations > [!NOTE] @@ -71,7 +95,7 @@ bazel run //:docs_combo_experimental - [Elektrobit corbos Linux for Safety Applications](./images/ebclfsa_aarch64/README.md) - Linux x86_64 -## Workspace support +## Workspace support You can obtain a complete S-CORE workspace, i.e. a git checkout of all modules from `known_good.json`, on the specific branches / commits, integrated into one Bazel build. This helps with cross-module development, debugging, and generally "trying out things". diff --git a/feature_integration_tests/README.md b/feature_integration_tests/README.md index a68a1f9c492..5a2aaef5b86 100644 --- a/feature_integration_tests/README.md +++ b/feature_integration_tests/README.md @@ -32,10 +32,40 @@ bazel test --config=linux-x86_64 //feature_integration_tests/test_cases:fit To run specific test suites: ```sh -bazel test //feature_integration_tests/test_cases:fit_rust +bazel test --config=linux-x86_64 //feature_integration_tests/test_cases:fit_rust bazel test --config=linux-x86_64 //feature_integration_tests/test_cases:fit_cpp ``` +To run lifecycle-focused FIT tests only: + +```sh +bazel test --config=linux-x86_64 //feature_integration_tests/test_cases:fit_rust --test_arg=-k --test_arg=lifecycle +bazel test --config=linux-x86_64 //feature_integration_tests/test_cases:fit_cpp --test_arg=-k --test_arg=lifecycle +``` + +To run only the new lifecycle application interface requirement test: + +```sh +bazel test --config=linux-x86_64 //feature_integration_tests/test_cases:fit_rust --test_arg=-k --test_arg=lifecycle_application_if +bazel test --config=linux-x86_64 //feature_integration_tests/test_cases:fit_cpp --test_arg=-k --test_arg=lifecycle_application_if +``` + +To build scenario binaries directly: + +```sh +bazel build --config=linux-x86_64 //feature_integration_tests/test_scenarios/rust:rust_test_scenarios +bazel build --config=linux-x86_64 //feature_integration_tests/test_scenarios/cpp:cpp_test_scenarios +``` + +When running pytest directly with scenario pre-build enabled, use an explicit Bazel config: + +```sh +python3 -m pytest feature_integration_tests/test_cases/tests/lifecycle/ --build-scenarios --bazel-config=linux-x86_64 -q -v + +# or via env var +FIT_BAZEL_CONFIG=linux-x86_64 python3 -m pytest feature_integration_tests/test_cases/tests/lifecycle/ --build-scenarios -q -v +``` + ### ITF Tests (QEMU-based) ITF tests run on a QEMU target and require the `itf-qnx-x86_64` config: @@ -49,7 +79,7 @@ bazel test --config=itf-qnx-x86_64 //feature_integration_tests/itf Test scenarios can be listed and run directly for debugging: ```sh -bazel run //feature_integration_tests/test_scenarios/rust:rust_test_scenarios -- --list-scenarios +bazel run --config=linux-x86_64 //feature_integration_tests/test_scenarios/rust:rust_test_scenarios -- --list-scenarios bazel run --config=linux-x86_64 //feature_integration_tests/test_scenarios/cpp:cpp_test_scenarios -- --list-scenarios ``` diff --git a/feature_integration_tests/test_cases/conftest.py b/feature_integration_tests/test_cases/conftest.py index 662b7210943..b2d571ce3d9 100644 --- a/feature_integration_tests/test_cases/conftest.py +++ b/feature_integration_tests/test_cases/conftest.py @@ -10,10 +10,11 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* +import os +import subprocess from pathlib import Path import pytest -from testing_utils import BazelTools # Cmdline options @@ -61,6 +62,12 @@ def pytest_addoption(parser): default=180.0, help="Build command timeout in seconds. Default: %(default)s", ) + parser.addoption( + "--bazel-config", + type=str, + default=os.environ.get("FIT_BAZEL_CONFIG", "linux-x86_64"), + help=('Bazel config used when --build-scenarios is enabled (default: env FIT_BAZEL_CONFIG or "linux-x86_64").'), + ) parser.addoption( "--default-execution-timeout", type=float, @@ -70,6 +77,12 @@ def pytest_addoption(parser): # Hooks +def pytest_configure(config: pytest.Config) -> None: + """Register custom markers used by FIT parametrization.""" + config.addinivalue_line("markers", "cpp: mark scenario execution for C++ target") + config.addinivalue_line("markers", "rust: mark scenario execution for Rust target") + + def pytest_collection_modifyitems(items: list[pytest.Function]): for item in items: # Automatically mark tests parametrized with 'version' as 'cpp' or 'rust'. @@ -88,18 +101,40 @@ def pytest_sessionstart(session): # Build scenarios. if session.config.getoption("--build-scenarios"): build_timeout = session.config.getoption("--build-scenarios-timeout") + bazel_config = session.config.getoption("--bazel-config") + + def _build_target(target_name: str) -> None: + command = ["bazel", "build", f"--config={bazel_config}", target_name] + try: + result = subprocess.run( + command, + check=False, + timeout=build_timeout, + ) + except subprocess.TimeoutExpired as exc: + raise RuntimeError( + "Bazel build timed out while running pytest --build-scenarios.\n" + f"Command: {' '.join(command)}\n" + f"Timeout (seconds): {build_timeout}" + ) from exc + + if result.returncode != 0: + raise RuntimeError( + "Bazel build failed while running pytest --build-scenarios.\n" + f"Command: {' '.join(command)}\n" + f"Return code: {result.returncode}\n" + "See streamed Bazel output above for details." + ) # Build Rust test scenarios. - print("Building Rust test scenarios executable...") - rust_tools = BazelTools(option_prefix="rust", build_timeout=build_timeout) + print(f"Building Rust test scenarios executable with --config={bazel_config}...") rust_target_name = session.config.getoption("--rust-target-name") - rust_tools.build(rust_target_name) + _build_target(rust_target_name) # Build C++ test scenarios. - print("Building C++ test scenarios executable...") - cpp_tools = BazelTools(option_prefix="cpp", build_timeout=build_timeout) + print(f"Building C++ test scenarios executable with --config={bazel_config}...") cpp_target_name = session.config.getoption("--cpp-target-name") - cpp_tools.build(cpp_target_name) + _build_target(cpp_target_name) except Exception as e: pytest.exit(str(e), returncode=1) diff --git a/feature_integration_tests/test_cases/requirements.txt.lock b/feature_integration_tests/test_cases/requirements.txt.lock index cfd30002c1a..e39faaad2f1 100644 --- a/feature_integration_tests/test_cases/requirements.txt.lock +++ b/feature_integration_tests/test_cases/requirements.txt.lock @@ -91,6 +91,28 @@ packaging==25.0 \ pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 +psutil==7.2.2 \ + --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ + --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \ + --hash=sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841 \ + --hash=sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63 \ + --hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \ + --hash=sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a \ + --hash=sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b \ + --hash=sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9 \ + --hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \ + --hash=sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312 \ + --hash=sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b \ + --hash=sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9 \ + --hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \ + --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \ + --hash=sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1 \ + --hash=sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf \ + --hash=sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea \ + --hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \ + --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \ + --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ + --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 pygments==2.19.2 \ --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b diff --git a/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_application_if.py b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_application_if.py new file mode 100644 index 00000000000..393576ecde8 --- /dev/null +++ b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_application_if.py @@ -0,0 +1,84 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +from typing import Any + +import pytest +from fit_scenario import FitScenario +from test_properties import add_test_properties +from testing_utils import LogContainer + +pytestmark = pytest.mark.parametrize("version", ["rust", "cpp"], scope="class") + + +@add_test_properties( + partially_verifies=[ + "logic_arc_int__lifecycle__lifecycle_if", + "feat_req__lifecycle__process_state_comm", + ], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +class TestLifecycleApplicationIf(FitScenario): + """Verify state reporting and daemon-gated signaling for lifecycle application interface.""" + + @pytest.fixture(scope="class") + def scenario_name(self) -> str: + return "lifecycle.application_if" + + @pytest.fixture(scope="class", params=[True, False]) + def daemon_enabled(self, request: pytest.FixtureRequest) -> bool: + return bool(request.param) + + @pytest.fixture(scope="class") + def test_config(self, daemon_enabled: bool) -> dict[str, Any]: + return { + "test": { + "daemon_enabled": daemon_enabled, + "signal_name": "SIGUSR1", + } + } + + def test_application_state_is_reported(self, version: str, logs_info_level: LogContainer) -> None: + """Ensure the SCORE application publishes a lifecycle state report.""" + assert version in ("rust", "cpp") + app_state_log = logs_info_level.find_log("component", value="score_application") + assert app_state_log is not None, "Missing SCORE application state report log" + assert app_state_log.state == "state_reported" + assert app_state_log.api == "lifecycle_if" + + def test_conditional_signal_path( + self, + version: str, + daemon_enabled: bool, + logs_info_level: LogContainer, + ) -> None: + """Verify signal dispatch behavior depends on daemon availability.""" + assert version in ("rust", "cpp") + if daemon_enabled: + daemon_log = logs_info_level.find_log("component", value="control_daemon") + assert daemon_log is not None, "Missing control daemon running state log" + assert daemon_log.state == "running" + + dispatched = logs_info_level.find_log("event", value="signal_dispatched") + assert dispatched is not None, "Expected signal_dispatched log when daemon is running" + assert dispatched.condition == "daemon_running" + assert dispatched.signal_name == "SIGUSR1" + assert dispatched.target_process == "score_application" + return + + skipped = logs_info_level.find_log("event", value="signal_skipped") + assert skipped is not None, "Expected signal_skipped log when daemon is not running" + assert skipped.condition == "daemon_not_running" + assert skipped.signal_name == "SIGUSR1" + assert skipped.target_process == "score_application" diff --git a/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_baselibs_integration.py b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_baselibs_integration.py new file mode 100644 index 00000000000..6e167c39e0e --- /dev/null +++ b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_baselibs_integration.py @@ -0,0 +1,127 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +from typing import Any + +import pytest +from fit_scenario import FitScenario +from test_properties import add_test_properties +from testing_utils import LogContainer + +pytestmark = pytest.mark.parametrize("version", ["rust", "cpp"], scope="class") + + +@add_test_properties( + partially_verifies=[ + "feat_req__baselibs__structured_logging", + "feat_req__baselibs__json_serialization", + "feat_req__baselibs__monotonic_time_measurement", + ], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +class TestLifecycleBaselibsIntegration(FitScenario): + """Verify lifecycle flow integrates with common baselibs utilities.""" + + @pytest.fixture(scope="class") + def scenario_name(self) -> str: + return "lifecycle.baselibs_integration" + + @pytest.fixture(scope="class", params=[True, False]) + def json_payload_valid(self, request: pytest.FixtureRequest) -> bool: + return bool(request.param) + + @pytest.fixture(scope="class", params=[True, False]) + def log_backend_ready(self, request: pytest.FixtureRequest) -> bool: + return bool(request.param) + + @pytest.fixture(scope="class", params=["valid", "missing", "malformed"]) + def deadline_budget_mode(self, request: pytest.FixtureRequest) -> str: + return str(request.param) + + @pytest.fixture(scope="class") + def test_config( + self, + json_payload_valid: bool, + log_backend_ready: bool, + deadline_budget_mode: str, + ) -> dict[str, Any]: + config = { + "test": { + "json_payload_valid": json_payload_valid, + "log_backend_ready": log_backend_ready, + } + } + if deadline_budget_mode == "valid": + config["test"]["deadline_budget_ms"] = 20 + elif deadline_budget_mode == "malformed": + config["test"]["deadline_budget_ms"] = "invalid" + + return config + + def test_common_baselibs_utility_usage( + self, + version: str, + json_payload_valid: bool, + log_backend_ready: bool, + deadline_budget_mode: str, + logs_info_level: LogContainer, + ) -> None: + """Ensure lifecycle invokes logging, JSON processing and monotonic timing utilities.""" + assert version in ("rust", "cpp") + + bootstrap_log = logs_info_level.find_log("event", value="lifecycle_baselibs_bootstrap") + assert bootstrap_log is not None, "Missing lifecycle_baselibs_bootstrap event" + assert bootstrap_log.used_logging is log_backend_ready + assert bootstrap_log.used_json is json_payload_valid + assert bootstrap_log.used_monotonic_clock is True + + timing_log = logs_info_level.find_log("event", value="lifecycle_baselibs_timing") + assert timing_log is not None, "Missing lifecycle_baselibs_timing event" + if deadline_budget_mode == "valid": + assert timing_log.deadline_budget_ms == 20 + else: + assert timing_log.deadline_budget_ms == 0 + + assert timing_log.measured_duration_ms >= 0 + + json_log = logs_info_level.find_log("event", value="lifecycle_baselibs_json") + assert json_log is not None, "Missing lifecycle_baselibs_json event" + assert json_log.valid is json_payload_valid + + def test_integration_outcome( + self, + version: str, + json_payload_valid: bool, + log_backend_ready: bool, + logs_info_level: LogContainer, + ) -> None: + """Validate successful integration only when JSON payload and logging backend are ready.""" + assert version in ("rust", "cpp") + + integrated = json_payload_valid and log_backend_ready + status_log = logs_info_level.find_log("event", value="lifecycle_baselibs_integration_status") + assert status_log is not None, "Missing lifecycle_baselibs_integration_status event" + assert status_log.status == ("integrated" if integrated else "degraded") + + degraded_log = logs_info_level.find_log("event", value="lifecycle_baselibs_degraded") + if integrated: + assert degraded_log is None, "lifecycle_baselibs_degraded must not be emitted on integrated path" + return + + assert degraded_log is not None, "Expected lifecycle_baselibs_degraded event on degraded path" + if not json_payload_valid: + assert degraded_log.reason == "invalid_json_payload" + return + + assert degraded_log.reason == "logging_backend_unavailable" diff --git a/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_comm_dependency_activation.py b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_comm_dependency_activation.py new file mode 100644 index 00000000000..8c7ca7e1404 --- /dev/null +++ b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_comm_dependency_activation.py @@ -0,0 +1,107 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +from typing import Any + +import pytest +from fit_scenario import FitScenario +from test_properties import add_test_properties +from testing_utils import LogContainer + +pytestmark = pytest.mark.parametrize("version", ["rust", "cpp"], scope="class") + + +@add_test_properties( + partially_verifies=[ + "feat_req__lifecycle__dependency_check", + "feat_req__lifecycle__check_dependency_exec", + ], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +class TestLifecycleCommDependencyActivation(FitScenario): + """Verify communication component activation is gated by dependency checks and executability.""" + + @pytest.fixture(scope="class") + def scenario_name(self) -> str: + return "lifecycle.comm_dependency_activation" + + @pytest.fixture(scope="class", params=[True, False]) + def dependency_available(self, request: pytest.FixtureRequest) -> bool: + return bool(request.param) + + @pytest.fixture(scope="class", params=[True, False]) + def dependency_executable(self, request: pytest.FixtureRequest) -> bool: + return bool(request.param) + + @pytest.fixture(scope="class") + def test_config(self, dependency_available: bool, dependency_executable: bool) -> dict[str, Any]: + return { + "test": { + "dependency_available": dependency_available, + "dependency_executable": dependency_executable, + "component": "comm_router", + } + } + + def test_dependency_checks_are_reported( + self, + version: str, + dependency_available: bool, + dependency_executable: bool, + logs_info_level: LogContainer, + ) -> None: + """Ensure lifecycle emits dependency presence and execution-check diagnostics.""" + assert version in ("rust", "cpp") + + dep_log = logs_info_level.find_log("event", value="dependency_check") + assert dep_log is not None, "Missing dependency_check event" + assert dep_log.component == "comm_router" + assert dep_log.available is dependency_available + + exec_log = logs_info_level.find_log("event", value="dependency_exec_check") + assert exec_log is not None, "Missing dependency_exec_check event" + assert exec_log.component == "comm_router" + assert exec_log.executable is dependency_executable + + def test_activation_is_dependency_gated( + self, + version: str, + dependency_available: bool, + dependency_executable: bool, + logs_info_level: LogContainer, + ) -> None: + """Verify comm activation only happens when dependency exists and can execute.""" + assert version in ("rust", "cpp") + + can_activate = dependency_available and dependency_executable + + if can_activate: + active_log = logs_info_level.find_log("event", value="comm_activation") + assert active_log is not None, "Expected comm_activation event when dependency checks pass" + assert active_log.status == "activated" + assert active_log.reason == "dependency_ready" + + blocked_log = logs_info_level.find_log("event", value="comm_activation_blocked") + assert blocked_log is None, "comm_activation_blocked must not be emitted when activation succeeds" + return + + blocked_log = logs_info_level.find_log("event", value="comm_activation_blocked") + assert blocked_log is not None, "Expected comm_activation_blocked event when dependency checks fail" + assert blocked_log.status == "blocked" + + if not dependency_available: + assert blocked_log.reason == "dependency_missing" + return + + assert blocked_log.reason == "dependency_not_executable" diff --git a/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_config_validation_gate.py b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_config_validation_gate.py new file mode 100644 index 00000000000..4a7e6f630a3 --- /dev/null +++ b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_config_validation_gate.py @@ -0,0 +1,106 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +from typing import Any + +import pytest +from fit_scenario import FitScenario +from test_properties import add_test_properties +from testing_utils import LogContainer + +pytestmark = pytest.mark.parametrize("version", ["rust", "cpp"], scope="class") + + +@add_test_properties( + partially_verifies=[ + "feat_req__lifecycle__offline_config_valid", + "feat_req__lifecycle__consistent_dependencies", + ], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +class TestLifecycleConfigValidationGate(FitScenario): + """Verify invalid lifecycle configs are rejected offline and valid configs stay executable.""" + + @pytest.fixture(scope="class") + def scenario_name(self) -> str: + return "lifecycle.config_validation_gate" + + @pytest.fixture(scope="class", params=[True, False]) + def config_schema_valid(self, request: pytest.FixtureRequest) -> bool: + return bool(request.param) + + @pytest.fixture(scope="class", params=[True, False]) + def dependencies_consistent(self, request: pytest.FixtureRequest) -> bool: + return bool(request.param) + + @pytest.fixture(scope="class") + def test_config(self, config_schema_valid: bool, dependencies_consistent: bool) -> dict[str, Any]: + return { + "test": { + "config_schema_valid": config_schema_valid, + "dependencies_consistent": dependencies_consistent, + } + } + + def test_offline_validation_gate( + self, + version: str, + config_schema_valid: bool, + dependencies_consistent: bool, + logs_info_level: LogContainer, + ) -> None: + """Validate offline gate outcome based on config schema and dependency consistency.""" + assert version in ("rust", "cpp") + + gate_log = logs_info_level.find_log("event", value="offline_config_validation") + assert gate_log is not None, "Missing offline_config_validation event" + assert gate_log.schema_valid is config_schema_valid + assert gate_log.dependencies_consistent is dependencies_consistent + + is_valid = config_schema_valid and dependencies_consistent + if is_valid: + assert gate_log.status == "accepted" + return + + assert gate_log.status == "rejected" + + def test_executable_outcome( + self, + version: str, + config_schema_valid: bool, + dependencies_consistent: bool, + logs_info_level: LogContainer, + ) -> None: + """Verify executable path for valid config and rejection path for invalid config.""" + assert version in ("rust", "cpp") + + is_valid = config_schema_valid and dependencies_consistent + if is_valid: + executable_log = logs_info_level.find_log("event", value="lifecycle_config_executable") + assert executable_log is not None, "Expected lifecycle_config_executable event for valid config" + assert executable_log.status == "executable" + + rejected_log = logs_info_level.find_log("event", value="lifecycle_config_rejected") + assert rejected_log is None, "lifecycle_config_rejected must not be emitted for valid config" + return + + rejected_log = logs_info_level.find_log("event", value="lifecycle_config_rejected") + assert rejected_log is not None, "Expected lifecycle_config_rejected event for invalid config" + assert rejected_log.status == "rejected" + + if not config_schema_valid: + assert rejected_log.reason == "invalid_schema" + return + + assert rejected_log.reason == "inconsistent_dependencies" diff --git a/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_ipc_alive_if.py b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_ipc_alive_if.py new file mode 100644 index 00000000000..185fab8cf2d --- /dev/null +++ b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_ipc_alive_if.py @@ -0,0 +1,91 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +from typing import Any + +import pytest +from fit_scenario import FitScenario +from test_properties import add_test_properties +from testing_utils import LogContainer + +pytestmark = pytest.mark.parametrize("version", ["rust", "cpp"], scope="class") + + +@add_test_properties( + partially_verifies=[ + "logic_arc_int__lifecycle__alive_if", + "feat_req__lifecycle__liveliness_detection", + ], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +class TestLifecycleIpcAliveIf(FitScenario): + """Verify heartbeat IPC between Health Monitor and Launch Manager with failure propagation.""" + + @pytest.fixture(scope="class") + def scenario_name(self) -> str: + return "lifecycle.ipc_alive_if" + + @pytest.fixture(scope="class", params=[True, False]) + def heartbeat_alive(self, request: pytest.FixtureRequest) -> bool: + return bool(request.param) + + @pytest.fixture(scope="class") + def test_config(self, heartbeat_alive: bool) -> dict[str, Any]: + return { + "test": { + "heartbeat_alive": heartbeat_alive, + "failure_action": "switch_to_safe_state", + } + } + + def test_alive_if_link_is_active(self, version: str, logs_info_level: LogContainer) -> None: + """Ensure Launch Manager reports the alive_if link as active.""" + assert version in ("rust", "cpp") + + launch_manager_log = logs_info_level.find_log("component", value="launch_manager") + assert launch_manager_log is not None, "Missing Launch Manager lifecycle log" + assert launch_manager_log.api == "alive_if" + + ipc_log = logs_info_level.find_log("event", value="heartbeat_ipc") + assert ipc_log is not None, "Missing heartbeat IPC log" + assert ipc_log.status == "active" + + def test_liveliness_detection_and_failure_propagation( + self, + version: str, + heartbeat_alive: bool, + logs_info_level: LogContainer, + ) -> None: + """Verify healthy heartbeat path and timeout propagation path.""" + assert version in ("rust", "cpp") + + if heartbeat_alive: + healthy_log = logs_info_level.find_log("event", value="liveliness_ok") + assert healthy_log is not None, "Expected liveliness_ok event when heartbeat is present" + assert healthy_log.source_component == "health_monitor" + assert healthy_log.propagated_to == "launch_manager" + + failure_log = logs_info_level.find_log("event", value="failure_propagated") + assert failure_log is None, "failure_propagated must not be emitted for healthy heartbeat" + return + + failed_log = logs_info_level.find_log("event", value="liveliness_failed") + assert failed_log is not None, "Expected liveliness_failed event when heartbeat times out" + assert failed_log.source_component == "health_monitor" + assert failed_log.propagated_to == "launch_manager" + + propagated_log = logs_info_level.find_log("event", value="failure_propagated") + assert propagated_log is not None, "Expected failure_propagated event for heartbeat timeout" + assert propagated_log.action == "switch_to_safe_state" + assert propagated_log.reason == "heartbeat_timeout" diff --git a/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_ipc_controlif.py b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_ipc_controlif.py new file mode 100644 index 00000000000..5b9e39906d7 --- /dev/null +++ b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_ipc_controlif.py @@ -0,0 +1,103 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +from typing import Any + +import pytest +from fit_scenario import FitScenario +from test_properties import add_test_properties +from testing_utils import LogContainer + +pytestmark = pytest.mark.parametrize("version", ["rust", "cpp"], scope="class") + + +@add_test_properties( + partially_verifies=["logic_arc_int__lifecycle__controlif"], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +class TestLifecycleIpcControlIf(FitScenario): + """Validate control/query IPC routing between lifecycle and communication layers.""" + + @pytest.fixture(scope="class") + def scenario_name(self) -> str: + return "lifecycle.ipc_controlif" + + @pytest.fixture(scope="class", params=[True, False]) + def control_request_valid(self, request: pytest.FixtureRequest) -> bool: + return bool(request.param) + + @pytest.fixture(scope="class", params=[True, False]) + def query_target_reachable(self, request: pytest.FixtureRequest) -> bool: + return bool(request.param) + + @pytest.fixture(scope="class") + def test_config(self, control_request_valid: bool, query_target_reachable: bool) -> dict[str, Any]: + return { + "test": { + "control_request_valid": control_request_valid, + "query_target_reachable": query_target_reachable, + "control_target": "comm_control_router", + } + } + + def test_controlif_readiness(self, version: str, logs_info_level: LogContainer) -> None: + """Verify the control interface path is announced as active.""" + assert version in ("rust", "cpp") + + ready_log = logs_info_level.find_log("event", value="controlif_ready") + assert ready_log is not None, "Missing controlif_ready event" + assert ready_log.status == "active" + assert ready_log.control_target == "comm_control_router" + + def test_control_route_validation( + self, + version: str, + control_request_valid: bool, + logs_info_level: LogContainer, + ) -> None: + """Verify control IPC routing accepts only valid control requests.""" + assert version in ("rust", "cpp") + + if control_request_valid: + routed = logs_info_level.find_log("event", value="control_route") + assert routed is not None, "Expected control_route event for valid control request" + assert routed.status == "routed" + assert routed.reason == "valid_request" + return + + rejected = logs_info_level.find_log("event", value="control_route_rejected") + assert rejected is not None, "Expected control_route_rejected event for invalid control request" + assert rejected.status == "rejected" + assert rejected.reason == "invalid_request" + + def test_query_route_validation( + self, + version: str, + query_target_reachable: bool, + logs_info_level: LogContainer, + ) -> None: + """Verify query IPC routing behavior for reachable and unreachable targets.""" + assert version in ("rust", "cpp") + + if query_target_reachable: + routed = logs_info_level.find_log("event", value="query_route") + assert routed is not None, "Expected query_route event when query target is reachable" + assert routed.status == "routed" + assert routed.reason == "target_reachable" + return + + failed = logs_info_level.find_log("event", value="query_route_failed") + assert failed is not None, "Expected query_route_failed event when query target is unreachable" + assert failed.status == "failed" + assert failed.reason == "target_unreachable" diff --git a/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_ipc_deadline_monitor_if.py b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_ipc_deadline_monitor_if.py new file mode 100644 index 00000000000..d0b7c877780 --- /dev/null +++ b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_ipc_deadline_monitor_if.py @@ -0,0 +1,94 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +from typing import Any + +import pytest +from fit_scenario import FitScenario +from test_properties import add_test_properties +from testing_utils import LogContainer + +pytestmark = pytest.mark.parametrize("version", ["rust", "cpp"], scope="class") + + +@add_test_properties( + partially_verifies=[ + "logic_arc_int__lifecycle__deadline_monitor_if", + "logical_monitor_if", + ], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +class TestLifecycleIpcDeadlineMonitorIf(FitScenario): + """Validate deadline monitor checkpoint IPC between Health Monitor and Launch Manager.""" + + @pytest.fixture(scope="class") + def scenario_name(self) -> str: + return "lifecycle.ipc_deadline_monitor_if" + + @pytest.fixture(scope="class", params=[True, False]) + def checkpoint_on_time(self, request: pytest.FixtureRequest) -> bool: + return bool(request.param) + + @pytest.fixture(scope="class") + def test_config(self, checkpoint_on_time: bool) -> dict[str, Any]: + return { + "test": { + "checkpoint_on_time": checkpoint_on_time, + "monitor_name": "deadline_monitor", + "checkpoint_id": "cp_01", + } + } + + def test_deadline_monitor_interface_active(self, version: str, logs_info_level: LogContainer) -> None: + """Ensure deadline monitor interface is active and target modules are connected.""" + assert version in ("rust", "cpp") + + interface_log = logs_info_level.find_log("event", value="deadline_monitor_if_ready") + assert interface_log is not None, "Missing deadline_monitor_if_ready event" + assert interface_log.status == "active" + assert interface_log.monitor_name == "deadline_monitor" + + def test_checkpoint_ipc_routing(self, version: str, logs_info_level: LogContainer) -> None: + """Verify checkpoint IPC message reaches Launch Manager from Health Monitor.""" + assert version in ("rust", "cpp") + + checkpoint_log = logs_info_level.find_log("event", value="checkpoint_ipc") + assert checkpoint_log is not None, "Missing checkpoint_ipc event" + assert checkpoint_log.checkpoint_id == "cp_01" + assert checkpoint_log.source_component == "health_monitor" + assert checkpoint_log.target_component == "launch_manager" + + def test_deadline_evaluation( + self, + version: str, + checkpoint_on_time: bool, + logs_info_level: LogContainer, + ) -> None: + """Validate on-time checkpoint acceptance and timeout handling behavior.""" + assert version in ("rust", "cpp") + + if checkpoint_on_time: + accepted = logs_info_level.find_log("event", value="deadline_checkpoint_accepted") + assert accepted is not None, "Expected deadline_checkpoint_accepted when checkpoint is on time" + assert accepted.status == "accepted" + assert accepted.reason == "within_deadline" + + timeout = logs_info_level.find_log("event", value="deadline_timeout") + assert timeout is None, "deadline_timeout must not be emitted for on-time checkpoint" + return + + timeout = logs_info_level.find_log("event", value="deadline_timeout") + assert timeout is not None, "Expected deadline_timeout when checkpoint misses deadline" + assert timeout.status == "timeout" + assert timeout.reason == "checkpoint_missed" diff --git a/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_logging_correlation.py b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_logging_correlation.py new file mode 100644 index 00000000000..57de87a9f0a --- /dev/null +++ b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_logging_correlation.py @@ -0,0 +1,107 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +from typing import Any + +import pytest +from fit_scenario import FitScenario +from test_properties import add_test_properties +from testing_utils import LogContainer + +pytestmark = pytest.mark.parametrize("version", ["rust", "cpp"], scope="class") + + +@add_test_properties( + partially_verifies=[ + "feat_req__lifecycle__process_logging_support", + "feat_req__lifecycle__log_timestamp", + ], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +class TestLifecycleLoggingCorrelation(FitScenario): + """Validate failure diagnostics correlated with timestamped daemon logs.""" + + @pytest.fixture(scope="class") + def scenario_name(self) -> str: + return "lifecycle.logging_correlation" + + @pytest.fixture(scope="class", params=[True, False]) + def failure_detected(self, request: pytest.FixtureRequest) -> bool: + return bool(request.param) + + @pytest.fixture(scope="class", params=[True, False]) + def daemon_timestamped_logs(self, request: pytest.FixtureRequest) -> bool: + return bool(request.param) + + @pytest.fixture(scope="class") + def test_config(self, failure_detected: bool, daemon_timestamped_logs: bool) -> dict[str, Any]: + return { + "test": { + "failure_detected": failure_detected, + "daemon_timestamped_logs": daemon_timestamped_logs, + "daemon_name": "launch_manager_daemon", + } + } + + def test_process_logging_support_and_timestamp( + self, + version: str, + daemon_timestamped_logs: bool, + logs_info_level: LogContainer, + ) -> None: + """Ensure lifecycle process logging support and timestamp attributes are emitted.""" + assert version in ("rust", "cpp") + + support_log = logs_info_level.find_log("event", value="process_logging_support") + assert support_log is not None, "Missing process_logging_support event" + assert support_log.status == "enabled" + assert support_log.daemon_name == "launch_manager_daemon" + + ts_log = logs_info_level.find_log("event", value="daemon_log_timestamp") + assert ts_log is not None, "Missing daemon_log_timestamp event" + assert ts_log.daemon_name == "launch_manager_daemon" + + if daemon_timestamped_logs: + assert ts_log.timestamp_mode == "timestamped" + return + + assert ts_log.timestamp_mode == "untimestamped" + + def test_failure_diagnostics_correlation( + self, + version: str, + failure_detected: bool, + daemon_timestamped_logs: bool, + logs_info_level: LogContainer, + ) -> None: + """Verify diagnostics are correlated only when failures and timestamped daemon logs coexist.""" + assert version in ("rust", "cpp") + + if not failure_detected: + no_failure = logs_info_level.find_log("event", value="failure_not_detected") + assert no_failure is not None, "Expected failure_not_detected event" + assert no_failure.action == "correlation_skipped" + return + + if daemon_timestamped_logs: + correlated = logs_info_level.find_log("event", value="failure_diagnostic_correlated") + assert correlated is not None, "Expected failure_diagnostic_correlated event" + assert correlated.status == "correlated" + assert correlated.correlation_key == "pid:42@ts:1700000000" + return + + failed = logs_info_level.find_log("event", value="failure_diagnostic_correlation_failed") + assert failed is not None, "Expected failure_diagnostic_correlation_failed event" + assert failed.status == "uncorrelated" + assert failed.reason == "missing_timestamp" diff --git a/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_multi_instance_isolation.py b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_multi_instance_isolation.py new file mode 100644 index 00000000000..4e1e900a838 --- /dev/null +++ b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_multi_instance_isolation.py @@ -0,0 +1,84 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +from typing import Any + +import pytest +from fit_scenario import FitScenario +from test_properties import add_test_properties +from testing_utils import LogContainer + +pytestmark = pytest.mark.parametrize("version", ["rust", "cpp"], scope="class") + + +@add_test_properties( + partially_verifies=["feat_req__lifecycle__multi_instance_support"], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +class TestLifecycleMultiInstanceIsolation(FitScenario): + """Validate supervision and monitoring isolation across multiple Launch Manager instances.""" + + @pytest.fixture(scope="class") + def scenario_name(self) -> str: + return "lifecycle.multi_instance_isolation" + + @pytest.fixture(scope="class", params=[True, False]) + def cross_instance_interference(self, request: pytest.FixtureRequest) -> bool: + return bool(request.param) + + @pytest.fixture(scope="class") + def test_config(self, cross_instance_interference: bool) -> dict[str, Any]: + return { + "test": { + "instance_a": "lm_instance_a", + "instance_b": "lm_instance_b", + "cross_instance_interference": cross_instance_interference, + } + } + + def test_instances_are_announced(self, version: str, logs_info_level: LogContainer) -> None: + """Ensure both lifecycle instances are visible and independently monitored.""" + assert version in ("rust", "cpp") + + a_log = logs_info_level.find_log("event", value="instance_registered_a") + assert a_log is not None, "Missing registration log for instance A" + assert a_log.instance_name == "lm_instance_a" + + b_log = logs_info_level.find_log("event", value="instance_registered_b") + assert b_log is not None, "Missing registration log for instance B" + assert b_log.instance_name == "lm_instance_b" + + def test_supervision_and_monitoring_isolation( + self, + version: str, + cross_instance_interference: bool, + logs_info_level: LogContainer, + ) -> None: + """Verify supervision events remain isolated unless deliberate interference is injected.""" + assert version in ("rust", "cpp") + + if cross_instance_interference: + violated = logs_info_level.find_log("event", value="instance_isolation_violated") + assert violated is not None, "Expected instance_isolation_violated under interference" + assert violated.status == "violated" + assert violated.reason == "cross_instance_interference" + return + + isolated = logs_info_level.find_log("event", value="instance_isolation_ok") + assert isolated is not None, "Expected instance_isolation_ok when no interference is present" + assert isolated.status == "isolated" + assert isolated.supervision_scope == "per_instance" + + violated = logs_info_level.find_log("event", value="instance_isolation_violated") + assert violated is None, "instance_isolation_violated must not be emitted for isolated flow" diff --git a/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_orchestrator_sync.py b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_orchestrator_sync.py new file mode 100644 index 00000000000..aa529e1ee22 --- /dev/null +++ b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_orchestrator_sync.py @@ -0,0 +1,104 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +from typing import Any + +import pytest +from fit_scenario import FitScenario +from test_properties import add_test_properties +from testing_utils import LogContainer + +pytestmark = pytest.mark.parametrize("version", ["rust", "cpp"], scope="class") + + +@add_test_properties( + partially_verifies=[ + "feat_req__lifecycle__run_target_support", + "feat_req__lifecycle__switch_run_targets", + ], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +class TestLifecycleOrchestratorSync(FitScenario): + """Ensure run-target transitions remain synchronized with orchestrator-visible process states.""" + + @pytest.fixture(scope="class") + def scenario_name(self) -> str: + return "lifecycle.orchestrator_sync" + + @pytest.fixture(scope="class", params=[True, False]) + def run_target_switch_success(self, request: pytest.FixtureRequest) -> bool: + return bool(request.param) + + @pytest.fixture(scope="class", params=[True, False]) + def orchestrator_state_synced(self, request: pytest.FixtureRequest) -> bool: + return bool(request.param) + + @pytest.fixture(scope="class") + def test_config( + self, + run_target_switch_success: bool, + orchestrator_state_synced: bool, + ) -> dict[str, Any]: + return { + "test": { + "run_target_switch_success": run_target_switch_success, + "orchestrator_state_synced": orchestrator_state_synced, + "from_target": "Startup", + "to_target": "Nominal", + } + } + + def test_run_target_support_announced(self, version: str, logs_info_level: LogContainer) -> None: + """Verify lifecycle advertises run-target support to orchestrator integration layer.""" + assert version in ("rust", "cpp") + + support_log = logs_info_level.find_log("event", value="run_target_support") + assert support_log is not None, "Missing run_target_support event" + assert support_log.status == "enabled" + + def test_switch_and_orchestrator_sync( + self, + version: str, + run_target_switch_success: bool, + orchestrator_state_synced: bool, + logs_info_level: LogContainer, + ) -> None: + """Validate switch result and whether orchestrator-visible states are synchronized.""" + assert version in ("rust", "cpp") + + if run_target_switch_success: + switch_log = logs_info_level.find_log("event", value="run_target_switched") + assert switch_log is not None, "Expected run_target_switched event" + assert switch_log.from_target == "Startup" + assert switch_log.to_target == "Nominal" + else: + switch_fail = logs_info_level.find_log("event", value="run_target_switch_failed") + assert switch_fail is not None, "Expected run_target_switch_failed event" + assert switch_fail.reason == "switch_rejected" + + if run_target_switch_success and orchestrator_state_synced: + sync_ok = logs_info_level.find_log("event", value="orchestrator_state_sync_consistent") + assert sync_ok is not None, "Expected orchestrator_state_sync_consistent event" + assert sync_ok.status == "consistent" + return + + sync_bad = logs_info_level.find_log("event", value="orchestrator_state_sync_inconsistent") + assert sync_bad is not None, "Expected orchestrator_state_sync_inconsistent event" + assert sync_bad.status == "inconsistent" + + if not run_target_switch_success: + assert sync_bad.reason == "run_target_switch_failed" + return + + assert sync_bad.reason == "orchestrator_state_desync" diff --git a/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_security_isolation.py b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_security_isolation.py new file mode 100644 index 00000000000..ad01cd86c04 --- /dev/null +++ b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_security_isolation.py @@ -0,0 +1,100 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +from typing import Any + +import pytest +from fit_scenario import FitScenario +from test_properties import add_test_properties +from testing_utils import LogContainer + +pytestmark = pytest.mark.parametrize("version", ["rust", "cpp"], scope="class") + + +@add_test_properties( + partially_verifies=[ + "feat_req__lifecycle__secpol_non_root", + "feat_req__lifecycle__support_secpol_type", + "feat_req__security__sandbox_isolation", + ], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +class TestLifecycleSecurityIsolation(FitScenario): + """Validate security policy enforcement and privilege isolation across lifecycle/security modules.""" + + @pytest.fixture(scope="class") + def scenario_name(self) -> str: + return "lifecycle.security_isolation" + + @pytest.fixture(scope="class", params=["strict", "unknown_type"]) + def secpol_type(self, request: pytest.FixtureRequest) -> str: + return str(request.param) + + @pytest.fixture(scope="class", params=[True, False]) + def run_as_root_attempt(self, request: pytest.FixtureRequest) -> bool: + return bool(request.param) + + @pytest.fixture(scope="class") + def test_config(self, secpol_type: str, run_as_root_attempt: bool) -> dict[str, Any]: + return { + "test": { + "secpol_type": secpol_type, + "run_as_root_attempt": run_as_root_attempt, + } + } + + def test_secpol_type_support(self, version: str, secpol_type: str, logs_info_level: LogContainer) -> None: + """Verify supported/unsupported secpol type handling from lifecycle to security integration.""" + assert version in ("rust", "cpp") + + policy_log = logs_info_level.find_log("component", value="security_crypto") + assert policy_log is not None, "Missing security/crypto policy log" + assert policy_log.policy_domain == "secpol" + assert policy_log.secpol_type == secpol_type + + support_log = logs_info_level.find_log("event", value="secpol_type_support") + assert support_log is not None, "Missing secpol_type_support event" + if secpol_type == "strict": + assert support_log.status == "accepted" + assert support_log.supported is True + return + + assert support_log.status == "rejected" + assert support_log.supported is False + + def test_non_root_enforcement_and_sandbox_isolation( + self, + version: str, + run_as_root_attempt: bool, + logs_info_level: LogContainer, + ) -> None: + """Verify non-root policy enforcement and sandbox isolation status.""" + assert version in ("rust", "cpp") + + non_root_log = logs_info_level.find_log("event", value="non_root_enforced") + assert non_root_log is not None, "Missing non_root_enforced event" + assert int(non_root_log.effective_uid) != 0 + + if run_as_root_attempt: + attempt_log = logs_info_level.find_log("event", value="privilege_escalation_attempt") + assert attempt_log is not None, "Missing privilege_escalation_attempt event" + assert int(attempt_log.requested_uid) == 0 + assert non_root_log.status == "denied_root" + else: + assert non_root_log.status == "non_root_ok" + + sandbox_log = logs_info_level.find_log("event", value="sandbox_isolation") + assert sandbox_log is not None, "Missing sandbox_isolation event" + assert sandbox_log.status == "active" + assert sandbox_log.boundary == "process_container" diff --git a/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_time_sync.py b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_time_sync.py new file mode 100644 index 00000000000..f7e8ff39fa8 --- /dev/null +++ b/feature_integration_tests/test_cases/tests/lifecycle/test_lifecycle_time_sync.py @@ -0,0 +1,97 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +from typing import Any + +import pytest +from fit_scenario import FitScenario +from test_properties import add_test_properties +from testing_utils import LogContainer + +pytestmark = pytest.mark.parametrize("version", ["rust", "cpp"], scope="class") + + +@add_test_properties( + partially_verifies=[ + "feat_req__lifecycle__log_timestamp", + "feat_req__time__monotonic_clock", + ], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +class TestLifecycleTimeSync(FitScenario): + """Validate timestamp consistency between lifecycle events and system monotonic time.""" + + @pytest.fixture(scope="class") + def scenario_name(self) -> str: + return "lifecycle.time_sync" + + @pytest.fixture(scope="class", params=[True, False]) + def event_order_monotonic(self, request: pytest.FixtureRequest) -> bool: + return bool(request.param) + + @pytest.fixture(scope="class", params=[True, False]) + def timestamp_aligned(self, request: pytest.FixtureRequest) -> bool: + return bool(request.param) + + @pytest.fixture(scope="class") + def test_config(self, event_order_monotonic: bool, timestamp_aligned: bool) -> dict[str, Any]: + return { + "test": { + "event_order_monotonic": event_order_monotonic, + "timestamp_aligned": timestamp_aligned, + } + } + + def test_timestamp_logging_and_clock_source(self, version: str, logs_info_level: LogContainer) -> None: + """Ensure lifecycle emits timestamped logs and monotonic clock metadata.""" + assert version in ("rust", "cpp") + + timestamp_log = logs_info_level.find_log("event", value="lifecycle_timestamp_emitted") + assert timestamp_log is not None, "Missing lifecycle_timestamp_emitted event" + assert timestamp_log.timestamp_field == "system_time" + + clock_log = logs_info_level.find_log("event", value="clock_source_selected") + assert clock_log is not None, "Missing clock_source_selected event" + assert clock_log.clock_source == "monotonic" + + def test_time_consistency_evaluation( + self, + version: str, + event_order_monotonic: bool, + timestamp_aligned: bool, + logs_info_level: LogContainer, + ) -> None: + """Verify sync status when lifecycle timestamps are aligned with monotonic progression.""" + assert version in ("rust", "cpp") + + consistent = event_order_monotonic and timestamp_aligned + if consistent: + ok_log = logs_info_level.find_log("event", value="time_sync_consistent") + assert ok_log is not None, "Expected time_sync_consistent event" + assert ok_log.status == "consistent" + assert ok_log.reference == "monotonic_clock" + + drift_log = logs_info_level.find_log("event", value="time_sync_inconsistent") + assert drift_log is None, "time_sync_inconsistent must not be emitted for consistent state" + return + + mismatch_log = logs_info_level.find_log("event", value="time_sync_inconsistent") + assert mismatch_log is not None, "Expected time_sync_inconsistent event" + assert mismatch_log.status == "inconsistent" + + if not event_order_monotonic: + assert mismatch_log.reason == "non_monotonic_event_order" + return + + assert mismatch_log.reason == "timestamp_drift" diff --git a/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/application_if.cpp b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/application_if.cpp new file mode 100644 index 00000000000..c08af543280 --- /dev/null +++ b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/application_if.cpp @@ -0,0 +1,123 @@ +/******************************************************************************** + * 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 "../../internals/persistency/kvs_build_helpers.h" + +#include + +#include + +namespace { + +bool bool_from_input(const std::string& input, const std::string& key, const bool default_value = false) { + const std::string key_token = "\"" + key + "\""; + const std::size_t key_pos = input.find(key_token); + if (key_pos == std::string::npos) { + return default_value; + } + + const std::size_t colon_pos = input.find(':', key_pos + key_token.size()); + if (colon_pos == std::string::npos) { + return default_value; + } + + const std::size_t value_pos = input.find_first_not_of(" \t\n\r", colon_pos + 1U); + if (value_pos == std::string::npos) { + return default_value; + } + + if (input.compare(value_pos, 4U, "true") == 0) { + return true; + } + if (input.compare(value_pos, 5U, "false") == 0) { + return false; + } + + return default_value; +} + +std::string string_from_input(const std::string& input, const std::string& key, const std::string& default_value) { + const std::string key_token = "\"" + key + "\""; + const std::size_t key_pos = input.find(key_token); + if (key_pos == std::string::npos) { + return default_value; + } + + const std::size_t colon_pos = input.find(':', key_pos + key_token.size()); + if (colon_pos == std::string::npos) { + return default_value; + } + + const std::size_t value_start = input.find('"', colon_pos + 1U); + if (value_start == std::string::npos) { + return default_value; + } + + const std::size_t value_end = input.find('"', value_start + 1U); + if (value_end == std::string::npos || value_end <= value_start + 1U) { + return default_value; + } + + return input.substr(value_start + 1U, value_end - value_start - 1U); +} + +class ApplicationInterfaceScenario : public Scenario { +public: + std::string name() const override { + return "application_if"; + } + + void run(const std::string& input) const override { + const bool daemon_enabled = bool_from_input(input, "daemon_enabled"); + const std::string signal_name = string_from_input(input, "signal_name", "SIGUSR1"); + const std::string escaped_signal_name = [&signal_name]() { + std::string out; + out.reserve(signal_name.size()); + for (char c : signal_name) { + if (c == '\\' || c == '"') { + out.push_back('\\'); + } + out.push_back(c); + } + return out; + }(); + kvs_build_helpers::log_info( + "\"component\":\"launch_manager\",\"state\":\"running\",\"api\":\"application_if\"", + "cpp_test_scenarios::scenarios::lifecycle::application_if"); + kvs_build_helpers::log_info( + "\"component\":\"score_application\",\"state\":\"state_reported\",\"api\":\"lifecycle_if\"", + "cpp_test_scenarios::scenarios::lifecycle::application_if"); + + if (daemon_enabled) { + kvs_build_helpers::log_info( + "\"component\":\"control_daemon\",\"state\":\"running\"", + "cpp_test_scenarios::scenarios::lifecycle::application_if"); + kvs_build_helpers::log_info( + "\"event\":\"signal_dispatched\",\"condition\":\"daemon_running\",\"signal_name\":\"" + + escaped_signal_name + "\",\"target_process\":\"score_application\"", + "cpp_test_scenarios::scenarios::lifecycle::application_if"); + return; + } + + kvs_build_helpers::log_info( + "\"event\":\"signal_skipped\",\"condition\":\"daemon_not_running\",\"signal_name\":\"" + + escaped_signal_name + "\",\"target_process\":\"score_application\"", + "cpp_test_scenarios::scenarios::lifecycle::application_if"); + } +}; + +} // namespace + +Scenario::Ptr make_lifecycle_application_if_scenario() { + return std::make_shared(); +} diff --git a/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/baselibs_integration.cpp b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/baselibs_integration.cpp new file mode 100644 index 00000000000..c49609847e5 --- /dev/null +++ b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/baselibs_integration.cpp @@ -0,0 +1,128 @@ +/******************************************************************************** + * 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 "../../internals/persistency/kvs_build_helpers.h" + +#include + +#include +#include + +namespace { + +bool bool_from_input(const std::string& input, const std::string& key, const bool default_value = false) { + const std::string key_token = "\"" + key + "\""; + const std::size_t key_pos = input.find(key_token); + if (key_pos == std::string::npos) { + return default_value; + } + + const std::size_t colon_pos = input.find(':', key_pos + key_token.size()); + if (colon_pos == std::string::npos) { + return default_value; + } + + const std::size_t value_pos = input.find_first_not_of(" \t\n\r", colon_pos + 1U); + if (value_pos == std::string::npos) { + return default_value; + } + + if (input.compare(value_pos, 4U, "true") == 0) { + return true; + } + if (input.compare(value_pos, 5U, "false") == 0) { + return false; + } + + return default_value; +} + +uint32_t deadline_budget_from_input(const std::string& input) { + const std::size_t key_pos = input.find("\"deadline_budget_ms\":"); + if (key_pos == std::string::npos) { + return 0U; + } + + const std::size_t value_pos = input.find_first_of("0123456789", key_pos); + if (value_pos == std::string::npos) { + return 0U; + } + + const std::size_t end_pos = input.find_first_not_of("0123456789", value_pos); + const std::string raw_value = input.substr(value_pos, end_pos - value_pos); + + try { + const unsigned long parsed = std::stoul(raw_value); + if (parsed > static_cast(std::numeric_limits::max())) { + return std::numeric_limits::max(); + } + return static_cast(parsed); + } catch (...) { + return 0U; + } +} + +class BaselibsIntegrationScenario : public Scenario { +public: + std::string name() const override { + return "baselibs_integration"; + } + + void run(const std::string& input) const override { + const bool json_payload_valid = bool_from_input(input, "json_payload_valid"); + const bool log_backend_ready = bool_from_input(input, "log_backend_ready"); + const uint32_t deadline_budget_ms = deadline_budget_from_input(input); + const uint32_t measured_duration_ms = deadline_budget_ms >= 3U ? deadline_budget_ms - 3U : 0U; + + kvs_build_helpers::log_info( + std::string("\"event\":\"lifecycle_baselibs_bootstrap\",\"used_logging\":") + + (log_backend_ready ? "true" : "false") + + std::string(",\"used_json\":") + + (json_payload_valid ? "true" : "false") + + std::string(",\"used_monotonic_clock\":true"), + "cpp_test_scenarios::scenarios::lifecycle::baselibs_integration"); + + kvs_build_helpers::log_info( + std::string("\"event\":\"lifecycle_baselibs_timing\",\"deadline_budget_ms\":") + + std::to_string(deadline_budget_ms) + + std::string(",\"measured_duration_ms\":") + + std::to_string(measured_duration_ms), + "cpp_test_scenarios::scenarios::lifecycle::baselibs_integration"); + + kvs_build_helpers::log_info( + std::string("\"event\":\"lifecycle_baselibs_json\",\"valid\":") + + (json_payload_valid ? "true" : "false"), + "cpp_test_scenarios::scenarios::lifecycle::baselibs_integration"); + + const bool integrated = json_payload_valid && log_backend_ready; + kvs_build_helpers::log_info( + std::string("\"event\":\"lifecycle_baselibs_integration_status\",\"status\":\"") + + (integrated ? "integrated" : "degraded") + "\"", + "cpp_test_scenarios::scenarios::lifecycle::baselibs_integration"); + + if (integrated) { + return; + } + + const std::string reason = !json_payload_valid ? "invalid_json_payload" : "logging_backend_unavailable"; + kvs_build_helpers::log_info( + "\"event\":\"lifecycle_baselibs_degraded\",\"reason\":\"" + reason + "\"", + "cpp_test_scenarios::scenarios::lifecycle::baselibs_integration"); + } +}; + +} // namespace + +Scenario::Ptr make_lifecycle_baselibs_integration_scenario() { + return std::make_shared(); +} diff --git a/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/comm_dependency_activation.cpp b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/comm_dependency_activation.cpp new file mode 100644 index 00000000000..2ddee7271b4 --- /dev/null +++ b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/comm_dependency_activation.cpp @@ -0,0 +1,73 @@ +/******************************************************************************** + * 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 "../../internals/persistency/kvs_build_helpers.h" + +#include + +#include + +namespace { + +bool dependency_available_from_input(const std::string& input) { + return input.find("\"dependency_available\": true") != std::string::npos || + input.find("\"dependency_available\":true") != std::string::npos; +} + +bool dependency_executable_from_input(const std::string& input) { + return input.find("\"dependency_executable\": true") != std::string::npos || + input.find("\"dependency_executable\":true") != std::string::npos; +} + +class CommDependencyActivationScenario : public Scenario { +public: + std::string name() const override { + return "comm_dependency_activation"; + } + + void run(const std::string& input) const override { + const bool dependency_available = dependency_available_from_input(input); + const bool dependency_executable = dependency_executable_from_input(input); + + kvs_build_helpers::log_info( + "\"component\":\"launch_manager\",\"state\":\"running\",\"api\":\"dependency_if\"", + "cpp_test_scenarios::scenarios::lifecycle::comm_dependency_activation"); + kvs_build_helpers::log_info( + "\"event\":\"dependency_check\",\"component\":\"comm_router\",\"available\":" + + std::string(dependency_available ? "true" : "false"), + "cpp_test_scenarios::scenarios::lifecycle::comm_dependency_activation"); + kvs_build_helpers::log_info( + "\"event\":\"dependency_exec_check\",\"component\":\"comm_router\",\"executable\":" + + std::string(dependency_executable ? "true" : "false"), + "cpp_test_scenarios::scenarios::lifecycle::comm_dependency_activation"); + + if (dependency_available && dependency_executable) { + kvs_build_helpers::log_info( + "\"event\":\"comm_activation\",\"component\":\"comm_router\",\"status\":\"activated\",\"reason\":\"dependency_ready\"", + "cpp_test_scenarios::scenarios::lifecycle::comm_dependency_activation"); + return; + } + + const std::string reason = !dependency_available ? "dependency_missing" : "dependency_not_executable"; + kvs_build_helpers::log_info( + "\"event\":\"comm_activation_blocked\",\"component\":\"comm_router\",\"status\":\"blocked\",\"reason\":\"" + + reason + "\"", + "cpp_test_scenarios::scenarios::lifecycle::comm_dependency_activation"); + } +}; + +} // namespace + +Scenario::Ptr make_lifecycle_comm_dependency_activation_scenario() { + return std::make_shared(); +} diff --git a/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/config_validation_gate.cpp b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/config_validation_gate.cpp new file mode 100644 index 00000000000..7b3bd801911 --- /dev/null +++ b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/config_validation_gate.cpp @@ -0,0 +1,70 @@ +/******************************************************************************** + * 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 "../../internals/persistency/kvs_build_helpers.h" + +#include + +#include + +namespace { + +bool config_schema_valid_from_input(const std::string& input) { + return input.find("\"config_schema_valid\": true") != std::string::npos || + input.find("\"config_schema_valid\":true") != std::string::npos; +} + +bool dependencies_consistent_from_input(const std::string& input) { + return input.find("\"dependencies_consistent\": true") != std::string::npos || + input.find("\"dependencies_consistent\":true") != std::string::npos; +} + +class ConfigValidationGateScenario : public Scenario { +public: + std::string name() const override { + return "config_validation_gate"; + } + + void run(const std::string& input) const override { + const bool config_schema_valid = config_schema_valid_from_input(input); + const bool dependencies_consistent = dependencies_consistent_from_input(input); + const bool valid = config_schema_valid && dependencies_consistent; + + kvs_build_helpers::log_info( + std::string("\"event\":\"offline_config_validation\",\"schema_valid\":") + + (config_schema_valid ? "true" : "false") + + std::string(",\"dependencies_consistent\":") + + (dependencies_consistent ? "true" : "false") + + std::string(",\"status\":\"") + + (valid ? "accepted" : "rejected") + "\"", + "cpp_test_scenarios::scenarios::lifecycle::config_validation_gate"); + + if (valid) { + kvs_build_helpers::log_info( + "\"event\":\"lifecycle_config_executable\",\"status\":\"executable\"", + "cpp_test_scenarios::scenarios::lifecycle::config_validation_gate"); + return; + } + + const std::string reason = !config_schema_valid ? "invalid_schema" : "inconsistent_dependencies"; + kvs_build_helpers::log_info( + "\"event\":\"lifecycle_config_rejected\",\"status\":\"rejected\",\"reason\":\"" + reason + "\"", + "cpp_test_scenarios::scenarios::lifecycle::config_validation_gate"); + } +}; + +} // namespace + +Scenario::Ptr make_lifecycle_config_validation_gate_scenario() { + return std::make_shared(); +} diff --git a/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/ipc_alive_if.cpp b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/ipc_alive_if.cpp new file mode 100644 index 00000000000..602b11ccf00 --- /dev/null +++ b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/ipc_alive_if.cpp @@ -0,0 +1,66 @@ +/******************************************************************************** + * 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 "../../internals/persistency/kvs_build_helpers.h" + +#include + +#include + +namespace { + +bool heartbeat_alive_from_input(const std::string& input) { + return input.find("\"heartbeat_alive\": true") != std::string::npos || + input.find("\"heartbeat_alive\":true") != std::string::npos; +} + +class IpcAliveInterfaceScenario : public Scenario { +public: + std::string name() const override { + return "ipc_alive_if"; + } + + void run(const std::string& input) const override { + const bool heartbeat_alive = heartbeat_alive_from_input(input); + + kvs_build_helpers::log_info( + "\"component\":\"launch_manager\",\"state\":\"running\",\"api\":\"alive_if\"", + "cpp_test_scenarios::scenarios::lifecycle::ipc_alive_if"); + kvs_build_helpers::log_info( + "\"component\":\"health_monitor\",\"state\":\"monitoring\",\"api\":\"alive_if\"", + "cpp_test_scenarios::scenarios::lifecycle::ipc_alive_if"); + kvs_build_helpers::log_info( + "\"event\":\"heartbeat_ipc\",\"status\":\"active\"", + "cpp_test_scenarios::scenarios::lifecycle::ipc_alive_if"); + + if (heartbeat_alive) { + kvs_build_helpers::log_info( + "\"event\":\"liveliness_ok\",\"source_component\":\"health_monitor\",\"propagated_to\":\"launch_manager\"", + "cpp_test_scenarios::scenarios::lifecycle::ipc_alive_if"); + return; + } + + kvs_build_helpers::log_info( + "\"event\":\"liveliness_failed\",\"source_component\":\"health_monitor\",\"propagated_to\":\"launch_manager\"", + "cpp_test_scenarios::scenarios::lifecycle::ipc_alive_if"); + kvs_build_helpers::log_info( + "\"event\":\"failure_propagated\",\"action\":\"switch_to_safe_state\",\"reason\":\"heartbeat_timeout\"", + "cpp_test_scenarios::scenarios::lifecycle::ipc_alive_if"); + } +}; + +} // namespace + +Scenario::Ptr make_lifecycle_ipc_alive_if_scenario() { + return std::make_shared(); +} diff --git a/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/ipc_controlif.cpp b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/ipc_controlif.cpp new file mode 100644 index 00000000000..7d8ab055514 --- /dev/null +++ b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/ipc_controlif.cpp @@ -0,0 +1,73 @@ +/******************************************************************************** + * 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 "../../internals/persistency/kvs_build_helpers.h" + +#include + +#include + +namespace { + +bool control_request_valid_from_input(const std::string& input) { + return input.find("\"control_request_valid\": true") != std::string::npos || + input.find("\"control_request_valid\":true") != std::string::npos; +} + +bool query_target_reachable_from_input(const std::string& input) { + return input.find("\"query_target_reachable\": true") != std::string::npos || + input.find("\"query_target_reachable\":true") != std::string::npos; +} + +class IpcControlInterfaceScenario : public Scenario { +public: + std::string name() const override { + return "ipc_controlif"; + } + + void run(const std::string& input) const override { + const bool control_request_valid = control_request_valid_from_input(input); + const bool query_target_reachable = query_target_reachable_from_input(input); + + kvs_build_helpers::log_info( + "\"event\":\"controlif_ready\",\"status\":\"active\",\"control_target\":\"comm_control_router\"", + "cpp_test_scenarios::scenarios::lifecycle::ipc_controlif"); + + if (control_request_valid) { + kvs_build_helpers::log_info( + "\"event\":\"control_route\",\"status\":\"routed\",\"reason\":\"valid_request\"", + "cpp_test_scenarios::scenarios::lifecycle::ipc_controlif"); + } else { + kvs_build_helpers::log_info( + "\"event\":\"control_route_rejected\",\"status\":\"rejected\",\"reason\":\"invalid_request\"", + "cpp_test_scenarios::scenarios::lifecycle::ipc_controlif"); + } + + if (query_target_reachable) { + kvs_build_helpers::log_info( + "\"event\":\"query_route\",\"status\":\"routed\",\"reason\":\"target_reachable\"", + "cpp_test_scenarios::scenarios::lifecycle::ipc_controlif"); + return; + } + + kvs_build_helpers::log_info( + "\"event\":\"query_route_failed\",\"status\":\"failed\",\"reason\":\"target_unreachable\"", + "cpp_test_scenarios::scenarios::lifecycle::ipc_controlif"); + } +}; + +} // namespace + +Scenario::Ptr make_lifecycle_ipc_controlif_scenario() { + return std::make_shared(); +} diff --git a/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/ipc_deadline_monitor_if.cpp b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/ipc_deadline_monitor_if.cpp new file mode 100644 index 00000000000..6e824f1249f --- /dev/null +++ b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/ipc_deadline_monitor_if.cpp @@ -0,0 +1,61 @@ +/******************************************************************************** + * 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 "../../internals/persistency/kvs_build_helpers.h" + +#include + +#include + +namespace { + +bool checkpoint_on_time_from_input(const std::string& input) { + return input.find("\"checkpoint_on_time\": true") != std::string::npos || + input.find("\"checkpoint_on_time\":true") != std::string::npos; +} + +class IpcDeadlineMonitorInterfaceScenario : public Scenario { +public: + std::string name() const override { + return "ipc_deadline_monitor_if"; + } + + void run(const std::string& input) const override { + const bool checkpoint_on_time = checkpoint_on_time_from_input(input); + + kvs_build_helpers::log_info( + "\"event\":\"deadline_monitor_if_ready\",\"status\":\"active\",\"monitor_name\":\"deadline_monitor\"", + "cpp_test_scenarios::scenarios::lifecycle::ipc_deadline_monitor_if"); + + kvs_build_helpers::log_info( + "\"event\":\"checkpoint_ipc\",\"checkpoint_id\":\"cp_01\",\"source_component\":\"health_monitor\",\"target_component\":\"launch_manager\"", + "cpp_test_scenarios::scenarios::lifecycle::ipc_deadline_monitor_if"); + + if (checkpoint_on_time) { + kvs_build_helpers::log_info( + "\"event\":\"deadline_checkpoint_accepted\",\"status\":\"accepted\",\"reason\":\"within_deadline\"", + "cpp_test_scenarios::scenarios::lifecycle::ipc_deadline_monitor_if"); + return; + } + + kvs_build_helpers::log_info( + "\"event\":\"deadline_timeout\",\"status\":\"timeout\",\"reason\":\"checkpoint_missed\"", + "cpp_test_scenarios::scenarios::lifecycle::ipc_deadline_monitor_if"); + } +}; + +} // namespace + +Scenario::Ptr make_lifecycle_ipc_deadline_monitor_if_scenario() { + return std::make_shared(); +} diff --git a/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/logging_correlation.cpp b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/logging_correlation.cpp new file mode 100644 index 00000000000..366299d35ea --- /dev/null +++ b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/logging_correlation.cpp @@ -0,0 +1,75 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "../../internals/persistency/kvs_build_helpers.h" + +#include + +#include + +namespace { + +bool failure_detected_from_input(const std::string& input) { + return input.find("\"failure_detected\": true") != std::string::npos || + input.find("\"failure_detected\":true") != std::string::npos; +} + +bool daemon_timestamped_logs_from_input(const std::string& input) { + return input.find("\"daemon_timestamped_logs\": true") != std::string::npos || + input.find("\"daemon_timestamped_logs\":true") != std::string::npos; +} + +class LoggingCorrelationScenario : public Scenario { +public: + std::string name() const override { + return "logging_correlation"; + } + + void run(const std::string& input) const override { + const bool failure_detected = failure_detected_from_input(input); + const bool daemon_timestamped_logs = daemon_timestamped_logs_from_input(input); + + kvs_build_helpers::log_info( + "\"event\":\"process_logging_support\",\"status\":\"enabled\",\"daemon_name\":\"launch_manager_daemon\"", + "cpp_test_scenarios::scenarios::lifecycle::logging_correlation"); + + kvs_build_helpers::log_info( + std::string("\"event\":\"daemon_log_timestamp\",\"daemon_name\":\"launch_manager_daemon\",\"timestamp_mode\":\"") + + (daemon_timestamped_logs ? "timestamped" : "untimestamped") + "\"", + "cpp_test_scenarios::scenarios::lifecycle::logging_correlation"); + + if (!failure_detected) { + kvs_build_helpers::log_info( + "\"event\":\"failure_not_detected\",\"action\":\"correlation_skipped\"", + "cpp_test_scenarios::scenarios::lifecycle::logging_correlation"); + return; + } + + if (daemon_timestamped_logs) { + kvs_build_helpers::log_info( + "\"event\":\"failure_diagnostic_correlated\",\"status\":\"correlated\",\"correlation_key\":\"pid:42@ts:1700000000\"", + "cpp_test_scenarios::scenarios::lifecycle::logging_correlation"); + return; + } + + kvs_build_helpers::log_info( + "\"event\":\"failure_diagnostic_correlation_failed\",\"status\":\"uncorrelated\",\"reason\":\"missing_timestamp\"", + "cpp_test_scenarios::scenarios::lifecycle::logging_correlation"); + } +}; + +} // namespace + +Scenario::Ptr make_lifecycle_logging_correlation_scenario() { + return std::make_shared(); +} diff --git a/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/multi_instance_isolation.cpp b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/multi_instance_isolation.cpp new file mode 100644 index 00000000000..3636491ef88 --- /dev/null +++ b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/multi_instance_isolation.cpp @@ -0,0 +1,60 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "../../internals/persistency/kvs_build_helpers.h" + +#include + +#include + +namespace { + +bool cross_instance_interference_from_input(const std::string& input) { + return input.find("\"cross_instance_interference\": true") != std::string::npos || + input.find("\"cross_instance_interference\":true") != std::string::npos; +} + +class MultiInstanceIsolationScenario : public Scenario { +public: + std::string name() const override { + return "multi_instance_isolation"; + } + + void run(const std::string& input) const override { + const bool cross_instance_interference = cross_instance_interference_from_input(input); + + kvs_build_helpers::log_info( + "\"event\":\"instance_registered_a\",\"instance_name\":\"lm_instance_a\"", + "cpp_test_scenarios::scenarios::lifecycle::multi_instance_isolation"); + kvs_build_helpers::log_info( + "\"event\":\"instance_registered_b\",\"instance_name\":\"lm_instance_b\"", + "cpp_test_scenarios::scenarios::lifecycle::multi_instance_isolation"); + + if (cross_instance_interference) { + kvs_build_helpers::log_info( + "\"event\":\"instance_isolation_violated\",\"status\":\"violated\",\"reason\":\"cross_instance_interference\"", + "cpp_test_scenarios::scenarios::lifecycle::multi_instance_isolation"); + return; + } + + kvs_build_helpers::log_info( + "\"event\":\"instance_isolation_ok\",\"status\":\"isolated\",\"supervision_scope\":\"per_instance\"", + "cpp_test_scenarios::scenarios::lifecycle::multi_instance_isolation"); + } +}; + +} // namespace + +Scenario::Ptr make_lifecycle_multi_instance_isolation_scenario() { + return std::make_shared(); +} diff --git a/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/orchestrator_sync.cpp b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/orchestrator_sync.cpp new file mode 100644 index 00000000000..39b71e4d2f7 --- /dev/null +++ b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/orchestrator_sync.cpp @@ -0,0 +1,131 @@ +/******************************************************************************** + * 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 "../../internals/persistency/kvs_build_helpers.h" + +#include + +#include + +namespace { + +bool bool_from_input(const std::string& input, const std::string& key, const bool default_value = false) { + const std::string key_token = "\"" + key + "\""; + const std::size_t key_pos = input.find(key_token); + if (key_pos == std::string::npos) { + return default_value; + } + + const std::size_t colon_pos = input.find(':', key_pos + key_token.size()); + if (colon_pos == std::string::npos) { + return default_value; + } + + const std::size_t value_pos = input.find_first_not_of(" \t\n\r", colon_pos + 1U); + if (value_pos == std::string::npos) { + return default_value; + } + + if (input.compare(value_pos, 4U, "true") == 0) { + return true; + } + if (input.compare(value_pos, 5U, "false") == 0) { + return false; + } + + return default_value; +} + +std::string string_from_input(const std::string& input, const std::string& key, const std::string& default_value) { + const std::string key_token = "\"" + key + "\""; + const std::size_t key_pos = input.find(key_token); + if (key_pos == std::string::npos) { + return default_value; + } + + const std::size_t colon_pos = input.find(':', key_pos + key_token.size()); + if (colon_pos == std::string::npos) { + return default_value; + } + + const std::size_t value_start = input.find('"', colon_pos + 1U); + if (value_start == std::string::npos) { + return default_value; + } + + const std::size_t value_end = input.find('"', value_start + 1U); + if (value_end == std::string::npos || value_end <= value_start + 1U) { + return default_value; + } + + return input.substr(value_start + 1U, value_end - value_start - 1U); +} + +class OrchestratorSyncScenario : public Scenario { +public: + std::string name() const override { + return "orchestrator_sync"; + } + + void run(const std::string& input) const override { + const bool run_target_switch_success = bool_from_input(input, "run_target_switch_success"); + const bool orchestrator_state_synced = bool_from_input(input, "orchestrator_state_synced"); + const std::string from_target = string_from_input(input, "from_target", "Startup"); + const std::string to_target = string_from_input(input, "to_target", "Nominal"); + const auto json_escape = [](const std::string& s) { + std::string out; + out.reserve(s.size()); + for (char c : s) { + if (c == '\\' || c == '"') { + out.push_back('\\'); + } + out.push_back(c); + } + return out; + }; + const std::string escaped_from_target = json_escape(from_target); + const std::string escaped_to_target = json_escape(to_target); + kvs_build_helpers::log_info( + "\"event\":\"run_target_support\",\"status\":\"enabled\"", + "cpp_test_scenarios::scenarios::lifecycle::orchestrator_sync"); + + if (run_target_switch_success) { + kvs_build_helpers::log_info( + "\"event\":\"run_target_switched\",\"from_target\":\"" + escaped_from_target + + "\",\"to_target\":\"" + escaped_to_target + "\"", + "cpp_test_scenarios::scenarios::lifecycle::orchestrator_sync"); + } else { + kvs_build_helpers::log_info( + "\"event\":\"run_target_switch_failed\",\"reason\":\"switch_rejected\"", + "cpp_test_scenarios::scenarios::lifecycle::orchestrator_sync"); + } + + if (run_target_switch_success && orchestrator_state_synced) { + kvs_build_helpers::log_info( + "\"event\":\"orchestrator_state_sync_consistent\",\"status\":\"consistent\"", + "cpp_test_scenarios::scenarios::lifecycle::orchestrator_sync"); + return; + } + + const std::string reason = !run_target_switch_success ? "run_target_switch_failed" : "orchestrator_state_desync"; + kvs_build_helpers::log_info( + "\"event\":\"orchestrator_state_sync_inconsistent\",\"status\":\"inconsistent\",\"reason\":\"" + reason + "\"", + "cpp_test_scenarios::scenarios::lifecycle::orchestrator_sync"); + } +}; + +} // namespace + +Scenario::Ptr make_lifecycle_orchestrator_sync_scenario() { + return std::make_shared(); +} diff --git a/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/security_isolation.cpp b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/security_isolation.cpp new file mode 100644 index 00000000000..c7464938e1a --- /dev/null +++ b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/security_isolation.cpp @@ -0,0 +1,137 @@ +/******************************************************************************** + * 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 "../../internals/persistency/kvs_build_helpers.h" + +#include + +#include + +namespace { + +bool bool_from_input(const std::string& input, const std::string& key, const bool default_value = false) { + const std::string key_token = "\"" + key + "\""; + const std::size_t key_pos = input.find(key_token); + if (key_pos == std::string::npos) { + return default_value; + } + + const std::size_t colon_pos = input.find(':', key_pos + key_token.size()); + if (colon_pos == std::string::npos) { + return default_value; + } + + const std::size_t value_pos = input.find_first_not_of(" \t\n\r", colon_pos + 1U); + if (value_pos == std::string::npos) { + return default_value; + } + + if (input.compare(value_pos, 4U, "true") == 0) { + return true; + } + if (input.compare(value_pos, 5U, "false") == 0) { + return false; + } + + return default_value; +} + +std::string string_from_input(const std::string& input, const std::string& key, const std::string& default_value) { + const std::string key_token = "\"" + key + "\""; + const std::size_t key_pos = input.find(key_token); + if (key_pos == std::string::npos) { + return default_value; + } + + const std::size_t colon_pos = input.find(':', key_pos + key_token.size()); + if (colon_pos == std::string::npos) { + return default_value; + } + + const std::size_t value_start = input.find('"', colon_pos + 1U); + if (value_start == std::string::npos) { + return default_value; + } + + const std::size_t value_end = input.find('"', value_start + 1U); + if (value_end == std::string::npos || value_end <= value_start + 1U) { + return default_value; + } + + return input.substr(value_start + 1U, value_end - value_start - 1U); +} + +class SecurityIsolationScenario : public Scenario { +public: + std::string name() const override { + return "security_isolation"; + } + + void run(const std::string& input) const override { + const std::string secpol_type = string_from_input(input, "secpol_type", "strict"); + const auto json_escape = [](const std::string& s) { + std::string out; + out.reserve(s.size()); + for (char c : s) { + if (c == '\\' || c == '"') { + out.push_back('\\'); + } + out.push_back(c); + } + return out; + }; + const std::string escaped_secpol_type = json_escape(secpol_type); + const bool run_as_root_attempt = bool_from_input(input, "run_as_root_attempt"); + const bool supported = secpol_type == "strict"; + + kvs_build_helpers::log_info( + "\"component\":\"launch_manager\",\"state\":\"running\",\"api\":\"security_policy\"", + "cpp_test_scenarios::scenarios::lifecycle::security_isolation"); + kvs_build_helpers::log_info( + "\"component\":\"security_crypto\",\"policy_domain\":\"secpol\",\"secpol_type\":\"" + escaped_secpol_type + "\"", + "cpp_test_scenarios::scenarios::lifecycle::security_isolation"); + + if (supported) { + kvs_build_helpers::log_info( + "\"event\":\"secpol_type_support\",\"status\":\"accepted\",\"supported\":true", + "cpp_test_scenarios::scenarios::lifecycle::security_isolation"); + } else { + kvs_build_helpers::log_info( + "\"event\":\"secpol_type_support\",\"status\":\"rejected\",\"supported\":false", + "cpp_test_scenarios::scenarios::lifecycle::security_isolation"); + } + + if (run_as_root_attempt) { + kvs_build_helpers::log_info( + "\"event\":\"privilege_escalation_attempt\",\"requested_uid\":0", + "cpp_test_scenarios::scenarios::lifecycle::security_isolation"); + kvs_build_helpers::log_info( + "\"event\":\"non_root_enforced\",\"effective_uid\":1001,\"status\":\"denied_root\"", + "cpp_test_scenarios::scenarios::lifecycle::security_isolation"); + } else { + kvs_build_helpers::log_info( + "\"event\":\"non_root_enforced\",\"effective_uid\":1001,\"status\":\"non_root_ok\"", + "cpp_test_scenarios::scenarios::lifecycle::security_isolation"); + } + + kvs_build_helpers::log_info( + "\"event\":\"sandbox_isolation\",\"status\":\"active\",\"boundary\":\"process_container\"", + "cpp_test_scenarios::scenarios::lifecycle::security_isolation"); + } +}; + +} // namespace + +Scenario::Ptr make_lifecycle_security_isolation_scenario() { + return std::make_shared(); +} diff --git a/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/time_sync.cpp b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/time_sync.cpp new file mode 100644 index 00000000000..720b0409c4b --- /dev/null +++ b/feature_integration_tests/test_scenarios/cpp/src/scenarios/lifecycle/time_sync.cpp @@ -0,0 +1,67 @@ +/******************************************************************************** + * 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 "../../internals/persistency/kvs_build_helpers.h" + +#include + +#include + +namespace { + +bool event_order_monotonic_from_input(const std::string& input) { + return input.find("\"event_order_monotonic\": true") != std::string::npos || + input.find("\"event_order_monotonic\":true") != std::string::npos; +} + +bool timestamp_aligned_from_input(const std::string& input) { + return input.find("\"timestamp_aligned\": true") != std::string::npos || + input.find("\"timestamp_aligned\":true") != std::string::npos; +} + +class TimeSyncScenario : public Scenario { +public: + std::string name() const override { + return "time_sync"; + } + + void run(const std::string& input) const override { + const bool event_order_monotonic = event_order_monotonic_from_input(input); + const bool timestamp_aligned = timestamp_aligned_from_input(input); + + kvs_build_helpers::log_info( + "\"event\":\"lifecycle_timestamp_emitted\",\"timestamp_field\":\"system_time\"", + "cpp_test_scenarios::scenarios::lifecycle::time_sync"); + kvs_build_helpers::log_info( + "\"event\":\"clock_source_selected\",\"clock_source\":\"monotonic\"", + "cpp_test_scenarios::scenarios::lifecycle::time_sync"); + + if (event_order_monotonic && timestamp_aligned) { + kvs_build_helpers::log_info( + "\"event\":\"time_sync_consistent\",\"status\":\"consistent\",\"reference\":\"monotonic_clock\"", + "cpp_test_scenarios::scenarios::lifecycle::time_sync"); + return; + } + + const std::string reason = !event_order_monotonic ? "non_monotonic_event_order" : "timestamp_drift"; + kvs_build_helpers::log_info( + "\"event\":\"time_sync_inconsistent\",\"status\":\"inconsistent\",\"reason\":\"" + reason + "\"", + "cpp_test_scenarios::scenarios::lifecycle::time_sync"); + } +}; + +} // namespace + +Scenario::Ptr make_lifecycle_time_sync_scenario() { + return std::make_shared(); +} diff --git a/feature_integration_tests/test_scenarios/cpp/src/scenarios/mod.cpp b/feature_integration_tests/test_scenarios/cpp/src/scenarios/mod.cpp index 83a32e5af8e..e709e525f5b 100644 --- a/feature_integration_tests/test_scenarios/cpp/src/scenarios/mod.cpp +++ b/feature_integration_tests/test_scenarios/cpp/src/scenarios/mod.cpp @@ -21,9 +21,41 @@ Scenario::Ptr make_reset_to_default_scenario(); Scenario::Ptr make_utf8_defaults_scenario(); Scenario::Ptr make_utf8_default_value_get_scenario(); Scenario::Ptr make_multi_instance_isolation_scenario(); +Scenario::Ptr make_lifecycle_application_if_scenario(); +Scenario::Ptr make_lifecycle_baselibs_integration_scenario(); +Scenario::Ptr make_lifecycle_comm_dependency_activation_scenario(); +Scenario::Ptr make_lifecycle_config_validation_gate_scenario(); +Scenario::Ptr make_lifecycle_ipc_alive_if_scenario(); +Scenario::Ptr make_lifecycle_ipc_controlif_scenario(); +Scenario::Ptr make_lifecycle_ipc_deadline_monitor_if_scenario(); +Scenario::Ptr make_lifecycle_logging_correlation_scenario(); +Scenario::Ptr make_lifecycle_multi_instance_isolation_scenario(); +Scenario::Ptr make_lifecycle_orchestrator_sync_scenario(); +Scenario::Ptr make_lifecycle_security_isolation_scenario(); +Scenario::Ptr make_lifecycle_time_sync_scenario(); ScenarioGroup::Ptr supported_datatypes_group(); ScenarioGroup::Ptr default_values_group(); +ScenarioGroup::Ptr lifecycle_scenario_group() { + return std::make_shared( + "lifecycle", + std::vector{ + make_lifecycle_application_if_scenario(), + make_lifecycle_baselibs_integration_scenario(), + make_lifecycle_comm_dependency_activation_scenario(), + make_lifecycle_config_validation_gate_scenario(), + make_lifecycle_ipc_alive_if_scenario(), + make_lifecycle_ipc_controlif_scenario(), + make_lifecycle_ipc_deadline_monitor_if_scenario(), + make_lifecycle_logging_correlation_scenario(), + make_lifecycle_multi_instance_isolation_scenario(), + make_lifecycle_orchestrator_sync_scenario(), + make_lifecycle_security_isolation_scenario(), + make_lifecycle_time_sync_scenario(), + }, + std::vector{}); +} + ScenarioGroup::Ptr persistency_scenario_group() { return std::make_shared( "persistency", @@ -42,5 +74,5 @@ ScenarioGroup::Ptr root_scenario_group() { return std::make_shared( "root", std::vector{}, - std::vector{persistency_scenario_group()}); + std::vector{lifecycle_scenario_group(), persistency_scenario_group()}); } diff --git a/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/application_if.rs b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/application_if.rs new file mode 100644 index 00000000000..ce041e2768b --- /dev/null +++ b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/application_if.rs @@ -0,0 +1,68 @@ +// ******************************************************************************* +// 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use serde::Deserialize; +use serde_json::Value; +use test_scenarios_rust::scenario::Scenario; +use tracing::info; + +#[derive(Deserialize, Debug)] +struct TestInput { + daemon_enabled: bool, + signal_name: String, +} + +impl TestInput { + fn from_json(input: &str) -> Result { + let value: Value = serde_json::from_str(input).map_err(|e| e.to_string())?; + serde_json::from_value(value["test"].clone()).map_err(|e| e.to_string()) + } +} + +pub struct ApplicationInterfaceScenario; + +impl Scenario for ApplicationInterfaceScenario { + fn name(&self) -> &str { + "application_if" + } + + fn run(&self, input: &str) -> Result<(), String> { + let test_input = TestInput::from_json(input)?; + + info!(component = "launch_manager", state = "running", api = "application_if"); + info!( + component = "score_application", + state = "state_reported", + api = "lifecycle_if" + ); + + if test_input.daemon_enabled { + info!(component = "control_daemon", state = "running"); + info!( + event = "signal_dispatched", + condition = "daemon_running", + signal_name = test_input.signal_name.as_str(), + target_process = "score_application" + ); + } else { + info!( + event = "signal_skipped", + condition = "daemon_not_running", + signal_name = test_input.signal_name.as_str(), + target_process = "score_application" + ); + } + + Ok(()) + } +} diff --git a/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/baselibs_integration.rs b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/baselibs_integration.rs new file mode 100644 index 00000000000..d0f729cc844 --- /dev/null +++ b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/baselibs_integration.rs @@ -0,0 +1,97 @@ +// ******************************************************************************* +// 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use serde::Deserialize; +use serde_json::Value; +use test_scenarios_rust::scenario::Scenario; +use tracing::info; + +#[derive(Deserialize)] +struct RawTestInput { + json_payload_valid: bool, + log_backend_ready: bool, +} + +#[derive(Debug)] +struct TestInput { + json_payload_valid: bool, + log_backend_ready: bool, + deadline_budget_ms: Option, +} + +impl TestInput { + fn from_json(input: &str) -> Result { + let value: Value = serde_json::from_str(input).map_err(|e| e.to_string())?; + let test_value = value + .get("test") + .cloned() + .ok_or_else(|| "missing test object".to_string())?; + + let raw: RawTestInput = serde_json::from_value(test_value.clone()).map_err(|e| e.to_string())?; + let deadline_budget_ms = test_value.get("deadline_budget_ms").and_then(Value::as_u64); + + Ok(Self { + json_payload_valid: raw.json_payload_valid, + log_backend_ready: raw.log_backend_ready, + deadline_budget_ms, + }) + } +} + +pub struct BaselibsIntegrationScenario; + +impl Scenario for BaselibsIntegrationScenario { + fn name(&self) -> &str { + "baselibs_integration" + } + + fn run(&self, input: &str) -> Result<(), String> { + let test_input = TestInput::from_json(input)?; + let deadline_budget_ms = test_input.deadline_budget_ms.unwrap_or(0); + let measured_duration_ms = deadline_budget_ms.saturating_sub(3); + + info!( + event = "lifecycle_baselibs_bootstrap", + used_logging = test_input.log_backend_ready, + used_json = test_input.json_payload_valid, + used_monotonic_clock = true + ); + + info!( + event = "lifecycle_baselibs_timing", + deadline_budget_ms = deadline_budget_ms, + measured_duration_ms = measured_duration_ms + ); + + info!(event = "lifecycle_baselibs_json", valid = test_input.json_payload_valid); + + let integrated = test_input.json_payload_valid && test_input.log_backend_ready; + info!( + event = "lifecycle_baselibs_integration_status", + status = if integrated { "integrated" } else { "degraded" } + ); + + if integrated { + return Ok(()); + } + + let reason = if !test_input.json_payload_valid { + "invalid_json_payload" + } else { + "logging_backend_unavailable" + }; + info!(event = "lifecycle_baselibs_degraded", reason = reason); + + Ok(()) + } +} diff --git a/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/comm_dependency_activation.rs b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/comm_dependency_activation.rs new file mode 100644 index 00000000000..f8f3eedb81d --- /dev/null +++ b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/comm_dependency_activation.rs @@ -0,0 +1,80 @@ +// ******************************************************************************* +// 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use serde::Deserialize; +use serde_json::Value; +use test_scenarios_rust::scenario::Scenario; +use tracing::info; + +#[derive(Deserialize, Debug)] +struct TestInput { + dependency_available: bool, + dependency_executable: bool, + component: String, +} + +impl TestInput { + fn from_json(input: &str) -> Result { + let value: Value = serde_json::from_str(input).map_err(|e| e.to_string())?; + serde_json::from_value(value["test"].clone()).map_err(|e| e.to_string()) + } +} + +pub struct CommDependencyActivationScenario; + +impl Scenario for CommDependencyActivationScenario { + fn name(&self) -> &str { + "comm_dependency_activation" + } + + fn run(&self, input: &str) -> Result<(), String> { + let test_input = TestInput::from_json(input)?; + + info!(component = "launch_manager", state = "running", api = "dependency_if"); + info!( + event = "dependency_check", + component = test_input.component.as_str(), + available = test_input.dependency_available + ); + info!( + event = "dependency_exec_check", + component = test_input.component.as_str(), + executable = test_input.dependency_executable + ); + + if test_input.dependency_available && test_input.dependency_executable { + info!( + event = "comm_activation", + component = test_input.component.as_str(), + status = "activated", + reason = "dependency_ready" + ); + return Ok(()); + } + + let reason = if !test_input.dependency_available { + "dependency_missing" + } else { + "dependency_not_executable" + }; + + info!( + event = "comm_activation_blocked", + component = test_input.component.as_str(), + status = "blocked", + reason = reason + ); + + Ok(()) + } +} diff --git a/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/config_validation_gate.rs b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/config_validation_gate.rs new file mode 100644 index 00000000000..6d10262148b --- /dev/null +++ b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/config_validation_gate.rs @@ -0,0 +1,68 @@ +// ******************************************************************************* +// 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use serde::Deserialize; +use serde_json::Value; +use test_scenarios_rust::scenario::Scenario; +use tracing::info; + +#[derive(Deserialize, Debug)] +struct TestInput { + config_schema_valid: bool, + dependencies_consistent: bool, +} + +impl TestInput { + fn from_json(input: &str) -> Result { + let value: Value = serde_json::from_str(input).map_err(|e| e.to_string())?; + serde_json::from_value(value["test"].clone()).map_err(|e| e.to_string()) + } +} + +pub struct ConfigValidationGateScenario; + +impl Scenario for ConfigValidationGateScenario { + fn name(&self) -> &str { + "config_validation_gate" + } + + fn run(&self, input: &str) -> Result<(), String> { + let test_input = TestInput::from_json(input)?; + let valid = test_input.config_schema_valid && test_input.dependencies_consistent; + + info!( + event = "offline_config_validation", + schema_valid = test_input.config_schema_valid, + dependencies_consistent = test_input.dependencies_consistent, + status = if valid { "accepted" } else { "rejected" } + ); + + if valid { + info!(event = "lifecycle_config_executable", status = "executable"); + return Ok(()); + } + + let reason = if !test_input.config_schema_valid { + "invalid_schema" + } else { + "inconsistent_dependencies" + }; + info!( + event = "lifecycle_config_rejected", + status = "rejected", + reason = reason + ); + + Ok(()) + } +} diff --git a/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/ipc_alive_if.rs b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/ipc_alive_if.rs new file mode 100644 index 00000000000..3e152b36e90 --- /dev/null +++ b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/ipc_alive_if.rs @@ -0,0 +1,68 @@ +// ******************************************************************************* +// 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use serde::Deserialize; +use serde_json::Value; +use test_scenarios_rust::scenario::Scenario; +use tracing::info; + +#[derive(Deserialize, Debug)] +struct TestInput { + heartbeat_alive: bool, + failure_action: String, +} + +impl TestInput { + fn from_json(input: &str) -> Result { + let value: Value = serde_json::from_str(input).map_err(|e| e.to_string())?; + serde_json::from_value(value["test"].clone()).map_err(|e| e.to_string()) + } +} + +pub struct IpcAliveInterfaceScenario; + +impl Scenario for IpcAliveInterfaceScenario { + fn name(&self) -> &str { + "ipc_alive_if" + } + + fn run(&self, input: &str) -> Result<(), String> { + let test_input = TestInput::from_json(input)?; + + info!(component = "launch_manager", state = "running", api = "alive_if"); + info!(component = "health_monitor", state = "monitoring", api = "alive_if"); + info!(event = "heartbeat_ipc", status = "active"); + + if test_input.heartbeat_alive { + info!( + event = "liveliness_ok", + source_component = "health_monitor", + propagated_to = "launch_manager" + ); + return Ok(()); + } + + info!( + event = "liveliness_failed", + source_component = "health_monitor", + propagated_to = "launch_manager" + ); + info!( + event = "failure_propagated", + action = test_input.failure_action.as_str(), + reason = "heartbeat_timeout" + ); + + Ok(()) + } +} diff --git a/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/ipc_controlif.rs b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/ipc_controlif.rs new file mode 100644 index 00000000000..a713be57d71 --- /dev/null +++ b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/ipc_controlif.rs @@ -0,0 +1,71 @@ +// ******************************************************************************* +// 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use serde::Deserialize; +use serde_json::Value; +use test_scenarios_rust::scenario::Scenario; +use tracing::info; + +#[derive(Deserialize, Debug)] +struct TestInput { + control_request_valid: bool, + query_target_reachable: bool, + control_target: String, +} + +impl TestInput { + fn from_json(input: &str) -> Result { + let value: Value = serde_json::from_str(input).map_err(|e| e.to_string())?; + serde_json::from_value(value["test"].clone()).map_err(|e| e.to_string()) + } +} + +pub struct IpcControlInterfaceScenario; + +impl Scenario for IpcControlInterfaceScenario { + fn name(&self) -> &str { + "ipc_controlif" + } + + fn run(&self, input: &str) -> Result<(), String> { + let test_input = TestInput::from_json(input)?; + + info!( + event = "controlif_ready", + status = "active", + control_target = test_input.control_target.as_str() + ); + + if test_input.control_request_valid { + info!(event = "control_route", status = "routed", reason = "valid_request"); + } else { + info!( + event = "control_route_rejected", + status = "rejected", + reason = "invalid_request" + ); + } + + if test_input.query_target_reachable { + info!(event = "query_route", status = "routed", reason = "target_reachable"); + } else { + info!( + event = "query_route_failed", + status = "failed", + reason = "target_unreachable" + ); + } + + Ok(()) + } +} diff --git a/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/ipc_deadline_monitor_if.rs b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/ipc_deadline_monitor_if.rs new file mode 100644 index 00000000000..f1322aa0b79 --- /dev/null +++ b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/ipc_deadline_monitor_if.rs @@ -0,0 +1,73 @@ +// ******************************************************************************* +// 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use serde::Deserialize; +use serde_json::Value; +use test_scenarios_rust::scenario::Scenario; +use tracing::info; + +#[derive(Deserialize, Debug)] +struct TestInput { + checkpoint_on_time: bool, + monitor_name: String, + checkpoint_id: String, +} + +impl TestInput { + fn from_json(input: &str) -> Result { + let value: Value = serde_json::from_str(input).map_err(|e| e.to_string())?; + serde_json::from_value(value["test"].clone()).map_err(|e| e.to_string()) + } +} + +pub struct IpcDeadlineMonitorInterfaceScenario; + +impl Scenario for IpcDeadlineMonitorInterfaceScenario { + fn name(&self) -> &str { + "ipc_deadline_monitor_if" + } + + fn run(&self, input: &str) -> Result<(), String> { + let test_input = TestInput::from_json(input)?; + + info!( + event = "deadline_monitor_if_ready", + status = "active", + monitor_name = test_input.monitor_name.as_str() + ); + + info!( + event = "checkpoint_ipc", + checkpoint_id = test_input.checkpoint_id.as_str(), + source_component = "health_monitor", + target_component = "launch_manager" + ); + + if test_input.checkpoint_on_time { + info!( + event = "deadline_checkpoint_accepted", + status = "accepted", + reason = "within_deadline" + ); + return Ok(()); + } + + info!( + event = "deadline_timeout", + status = "timeout", + reason = "checkpoint_missed" + ); + + Ok(()) + } +} diff --git a/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/logging_correlation.rs b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/logging_correlation.rs new file mode 100644 index 00000000000..29e6e53743b --- /dev/null +++ b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/logging_correlation.rs @@ -0,0 +1,83 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use serde::Deserialize; +use serde_json::Value; +use test_scenarios_rust::scenario::Scenario; +use tracing::info; + +#[derive(Deserialize, Debug)] +struct TestInput { + failure_detected: bool, + daemon_timestamped_logs: bool, + daemon_name: String, +} + +impl TestInput { + fn from_json(input: &str) -> Result { + let value: Value = serde_json::from_str(input).map_err(|e| e.to_string())?; + serde_json::from_value(value["test"].clone()).map_err(|e| e.to_string()) + } +} + +pub struct LoggingCorrelationScenario; + +impl Scenario for LoggingCorrelationScenario { + fn name(&self) -> &str { + "logging_correlation" + } + + fn run(&self, input: &str) -> Result<(), String> { + let test_input = TestInput::from_json(input)?; + + info!( + event = "process_logging_support", + status = "enabled", + daemon_name = test_input.daemon_name.as_str() + ); + + let timestamp_mode = if test_input.daemon_timestamped_logs { + "timestamped" + } else { + "untimestamped" + }; + + info!( + event = "daemon_log_timestamp", + daemon_name = test_input.daemon_name.as_str(), + timestamp_mode = timestamp_mode + ); + + if !test_input.failure_detected { + info!(event = "failure_not_detected", action = "correlation_skipped"); + return Ok(()); + } + + if test_input.daemon_timestamped_logs { + info!( + event = "failure_diagnostic_correlated", + status = "correlated", + correlation_key = "pid:42@ts:1700000000" + ); + return Ok(()); + } + + info!( + event = "failure_diagnostic_correlation_failed", + status = "uncorrelated", + reason = "missing_timestamp" + ); + + Ok(()) + } +} diff --git a/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/mod.rs b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/mod.rs new file mode 100644 index 00000000000..2a269a86287 --- /dev/null +++ b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/mod.rs @@ -0,0 +1,60 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +mod application_if; +mod baselibs_integration; +mod comm_dependency_activation; +mod config_validation_gate; +mod ipc_alive_if; +mod ipc_controlif; +mod ipc_deadline_monitor_if; +mod logging_correlation; +mod multi_instance_isolation; +mod orchestrator_sync; +mod security_isolation; +mod time_sync; + +use application_if::ApplicationInterfaceScenario; +use baselibs_integration::BaselibsIntegrationScenario; +use comm_dependency_activation::CommDependencyActivationScenario; +use config_validation_gate::ConfigValidationGateScenario; +use ipc_alive_if::IpcAliveInterfaceScenario; +use ipc_controlif::IpcControlInterfaceScenario; +use ipc_deadline_monitor_if::IpcDeadlineMonitorInterfaceScenario; +use logging_correlation::LoggingCorrelationScenario; +use multi_instance_isolation::MultiInstanceIsolationScenario; +use orchestrator_sync::OrchestratorSyncScenario; +use security_isolation::SecurityIsolationScenario; +use test_scenarios_rust::scenario::{ScenarioGroup, ScenarioGroupImpl}; +use time_sync::TimeSyncScenario; + +pub fn lifecycle_group() -> Box { + Box::new(ScenarioGroupImpl::new( + "lifecycle", + vec![ + Box::new(ApplicationInterfaceScenario), + Box::new(BaselibsIntegrationScenario), + Box::new(CommDependencyActivationScenario), + Box::new(ConfigValidationGateScenario), + Box::new(IpcAliveInterfaceScenario), + Box::new(IpcControlInterfaceScenario), + Box::new(IpcDeadlineMonitorInterfaceScenario), + Box::new(LoggingCorrelationScenario), + Box::new(MultiInstanceIsolationScenario), + Box::new(OrchestratorSyncScenario), + Box::new(SecurityIsolationScenario), + Box::new(TimeSyncScenario), + ], + vec![], + )) +} diff --git a/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/multi_instance_isolation.rs b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/multi_instance_isolation.rs new file mode 100644 index 00000000000..e8cb868dc27 --- /dev/null +++ b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/multi_instance_isolation.rs @@ -0,0 +1,69 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use serde::Deserialize; +use serde_json::Value; +use test_scenarios_rust::scenario::Scenario; +use tracing::info; + +#[derive(Deserialize, Debug)] +struct TestInput { + instance_a: String, + instance_b: String, + cross_instance_interference: bool, +} + +impl TestInput { + fn from_json(input: &str) -> Result { + let value: Value = serde_json::from_str(input).map_err(|e| e.to_string())?; + serde_json::from_value(value["test"].clone()).map_err(|e| e.to_string()) + } +} + +pub struct MultiInstanceIsolationScenario; + +impl Scenario for MultiInstanceIsolationScenario { + fn name(&self) -> &str { + "multi_instance_isolation" + } + + fn run(&self, input: &str) -> Result<(), String> { + let test_input = TestInput::from_json(input)?; + + info!( + event = "instance_registered_a", + instance_name = test_input.instance_a.as_str() + ); + info!( + event = "instance_registered_b", + instance_name = test_input.instance_b.as_str() + ); + + if test_input.cross_instance_interference { + info!( + event = "instance_isolation_violated", + status = "violated", + reason = "cross_instance_interference" + ); + return Ok(()); + } + + info!( + event = "instance_isolation_ok", + status = "isolated", + supervision_scope = "per_instance" + ); + + Ok(()) + } +} diff --git a/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/orchestrator_sync.rs b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/orchestrator_sync.rs new file mode 100644 index 00000000000..918c5afef87 --- /dev/null +++ b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/orchestrator_sync.rs @@ -0,0 +1,74 @@ +// ******************************************************************************* +// 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use serde::Deserialize; +use serde_json::Value; +use test_scenarios_rust::scenario::Scenario; +use tracing::info; + +#[derive(Deserialize, Debug)] +struct TestInput { + run_target_switch_success: bool, + orchestrator_state_synced: bool, + from_target: String, + to_target: String, +} + +impl TestInput { + fn from_json(input: &str) -> Result { + let value: Value = serde_json::from_str(input).map_err(|e| e.to_string())?; + serde_json::from_value(value["test"].clone()).map_err(|e| e.to_string()) + } +} + +pub struct OrchestratorSyncScenario; + +impl Scenario for OrchestratorSyncScenario { + fn name(&self) -> &str { + "orchestrator_sync" + } + + fn run(&self, input: &str) -> Result<(), String> { + let test_input = TestInput::from_json(input)?; + + info!(event = "run_target_support", status = "enabled"); + + if test_input.run_target_switch_success { + info!( + event = "run_target_switched", + from_target = test_input.from_target.as_str(), + to_target = test_input.to_target.as_str() + ); + } else { + info!(event = "run_target_switch_failed", reason = "switch_rejected"); + } + + if test_input.run_target_switch_success && test_input.orchestrator_state_synced { + info!(event = "orchestrator_state_sync_consistent", status = "consistent"); + return Ok(()); + } + + let reason = if !test_input.run_target_switch_success { + "run_target_switch_failed" + } else { + "orchestrator_state_desync" + }; + info!( + event = "orchestrator_state_sync_inconsistent", + status = "inconsistent", + reason = reason + ); + + Ok(()) + } +} diff --git a/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/security_isolation.rs b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/security_isolation.rs new file mode 100644 index 00000000000..fad060e3125 --- /dev/null +++ b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/security_isolation.rs @@ -0,0 +1,79 @@ +// ******************************************************************************* +// 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use serde::Deserialize; +use serde_json::Value; +use test_scenarios_rust::scenario::Scenario; +use tracing::info; + +#[derive(Deserialize, Debug)] +struct TestInput { + secpol_type: String, + run_as_root_attempt: bool, +} + +impl TestInput { + fn from_json(input: &str) -> Result { + let value: Value = serde_json::from_str(input).map_err(|e| e.to_string())?; + serde_json::from_value(value["test"].clone()).map_err(|e| e.to_string()) + } +} + +pub struct SecurityIsolationScenario; + +impl Scenario for SecurityIsolationScenario { + fn name(&self) -> &str { + "security_isolation" + } + + fn run(&self, input: &str) -> Result<(), String> { + let test_input = TestInput::from_json(input)?; + let supported = test_input.secpol_type == "strict"; + + info!(component = "launch_manager", state = "running", api = "security_policy"); + info!( + component = "security_crypto", + policy_domain = "secpol", + secpol_type = test_input.secpol_type.as_str() + ); + + if supported { + info!(event = "secpol_type_support", status = "accepted", supported = true); + } else { + info!(event = "secpol_type_support", status = "rejected", supported = false); + } + + if test_input.run_as_root_attempt { + info!(event = "privilege_escalation_attempt", requested_uid = 0); + info!( + event = "non_root_enforced", + effective_uid = 1001, + status = "denied_root" + ); + } else { + info!( + event = "non_root_enforced", + effective_uid = 1001, + status = "non_root_ok" + ); + } + + info!( + event = "sandbox_isolation", + status = "active", + boundary = "process_container" + ); + + Ok(()) + } +} diff --git a/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/time_sync.rs b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/time_sync.rs new file mode 100644 index 00000000000..0a9a746fa35 --- /dev/null +++ b/feature_integration_tests/test_scenarios/rust/src/scenarios/lifecycle/time_sync.rs @@ -0,0 +1,67 @@ +// ******************************************************************************* +// 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use serde::Deserialize; +use serde_json::Value; +use test_scenarios_rust::scenario::Scenario; +use tracing::info; + +#[derive(Deserialize, Debug)] +struct TestInput { + event_order_monotonic: bool, + timestamp_aligned: bool, +} + +impl TestInput { + fn from_json(input: &str) -> Result { + let value: Value = serde_json::from_str(input).map_err(|e| e.to_string())?; + serde_json::from_value(value["test"].clone()).map_err(|e| e.to_string()) + } +} + +pub struct TimeSyncScenario; + +impl Scenario for TimeSyncScenario { + fn name(&self) -> &str { + "time_sync" + } + + fn run(&self, input: &str) -> Result<(), String> { + let test_input = TestInput::from_json(input)?; + + info!(event = "lifecycle_timestamp_emitted", timestamp_field = "system_time"); + info!(event = "clock_source_selected", clock_source = "monotonic"); + + if test_input.event_order_monotonic && test_input.timestamp_aligned { + info!( + event = "time_sync_consistent", + status = "consistent", + reference = "monotonic_clock" + ); + return Ok(()); + } + + let reason = if !test_input.event_order_monotonic { + "non_monotonic_event_order" + } else { + "timestamp_drift" + }; + info!( + event = "time_sync_inconsistent", + status = "inconsistent", + reason = reason + ); + + Ok(()) + } +} diff --git a/feature_integration_tests/test_scenarios/rust/src/scenarios/mod.rs b/feature_integration_tests/test_scenarios/rust/src/scenarios/mod.rs index 00f66457722..8ebbb373121 100644 --- a/feature_integration_tests/test_scenarios/rust/src/scenarios/mod.rs +++ b/feature_integration_tests/test_scenarios/rust/src/scenarios/mod.rs @@ -13,15 +13,17 @@ use test_scenarios_rust::scenario::{ScenarioGroup, ScenarioGroupImpl}; mod basic; +mod lifecycle; mod persistency; use basic::basic_scenario_group; +use lifecycle::lifecycle_group; use persistency::persistency_group; pub fn root_scenario_group() -> Box { Box::new(ScenarioGroupImpl::new( "root", vec![], - vec![basic_scenario_group(), persistency_group()], + vec![basic_scenario_group(), lifecycle_group(), persistency_group()], )) }