diff --git a/feature_integration_tests/python_test_cases/BUILD b/feature_integration_tests/python_test_cases/BUILD index 3aa8031e9b..8f9d2d3197 100644 --- a/feature_integration_tests/python_test_cases/BUILD +++ b/feature_integration_tests/python_test_cases/BUILD @@ -28,7 +28,7 @@ score_virtualenv( # Tests targets score_py_pytest( name = "fit", - srcs = glob(["tests/**/*.py"]) + ["conftest.py", "fit_scenario.py"], + srcs = glob(["tests/**/*.py"]) + ["conftest.py", "fit_scenario.py", "test_properties.py"], args = [ "--traces=all", "--rust-target-path=$(rootpath //feature_integration_tests/rust_test_scenarios)", diff --git a/feature_integration_tests/python_test_cases/fit_scenario.py b/feature_integration_tests/python_test_cases/fit_scenario.py index f76406d614..6038f37b47 100644 --- a/feature_integration_tests/python_test_cases/fit_scenario.py +++ b/feature_integration_tests/python_test_cases/fit_scenario.py @@ -12,6 +12,17 @@ ) +class ResultCode: + """ + Test scenario exit codes. + """ + + SUCCESS = 0 + PANIC = 101 + SIGKILL = -9 + SIGABRT = -6 + + def temp_dir_common( tmp_path_factory: pytest.TempPathFactory, base_name: str, *args: str ) -> Generator[Path, None, None]: @@ -63,7 +74,7 @@ def results( **kwargs, ) -> ScenarioResult: result = self._run_command(command, execution_timeout, args, kwargs) - success = result.return_code == 0 and not result.hang + success = result.return_code == ResultCode.SUCCESS and not result.hang if self.expect_command_failure() and success: raise RuntimeError(f"Command execution succeeded unexpectedly: {result=}") if not self.expect_command_failure() and not success: diff --git a/feature_integration_tests/python_test_cases/test_properties.py b/feature_integration_tests/python_test_cases/test_properties.py new file mode 100644 index 0000000000..2f30b7b7a1 --- /dev/null +++ b/feature_integration_tests/python_test_cases/test_properties.py @@ -0,0 +1,10 @@ +try: + from attribute_plugin import add_test_properties # type: ignore[import-untyped] +except ImportError: + # Define no-op decorator if attribute_plugin is not available (outside bazel) + # Keeps IDE debugging functionality + def add_test_properties(*args, **kwargs): + def decorator(func): + return func # No-op decorator + + return decorator diff --git a/feature_integration_tests/python_test_cases/tests/basic/test_orchestartion_with_persistency.py b/feature_integration_tests/python_test_cases/tests/basic/test_orchestartion_with_persistency.py index 182f9e3eec..8659bb90e3 100644 --- a/feature_integration_tests/python_test_cases/tests/basic/test_orchestartion_with_persistency.py +++ b/feature_integration_tests/python_test_cases/tests/basic/test_orchestartion_with_persistency.py @@ -1,22 +1,11 @@ import json +from collections.abc import Generator from pathlib import Path -from typing import Any, Generator +from typing import Any import pytest - -try: - from attribute_plugin import add_test_properties # type: ignore[import-untyped] -except ImportError: - # Define no-op decorator if attribute_plugin is not available (outside bazel) - # Keeps IDE debugging functionality - def add_test_properties(*args, **kwargs): - def decorator(func): - return func # No-op decorator - - return decorator - - from fit_scenario import FitScenario, temp_dir_common +from test_properties import add_test_properties from testing_utils import LogContainer diff --git a/feature_integration_tests/python_test_cases/tests/persistency/multiple_kvs_per_app.py b/feature_integration_tests/python_test_cases/tests/persistency/multiple_kvs_per_app.py new file mode 100644 index 0000000000..cbb941741b --- /dev/null +++ b/feature_integration_tests/python_test_cases/tests/persistency/multiple_kvs_per_app.py @@ -0,0 +1,89 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +from pathlib import Path +from typing import Any, Generator + +import pytest +from fit_scenario import FitScenario, temp_dir_common +from test_properties import add_test_properties +from testing_utils import LogContainer + + +@add_test_properties( + partially_verifies=["feat_req__persistency__multiple_kvs"], + test_type="requirements-based", + derivation_technique="requirements-analysis", +) +class TestMultipleInstanceIds(FitScenario): + """ + Verifies that multiple KVS instances with different IDs store and retrieve independent values without interference. + """ + + @pytest.fixture(scope="class") + def scenario_name(self) -> str: + return "persistency.multiple_instance_ids" + + @pytest.fixture(scope="class") + def kvs_key(self) -> str: + return "number" + + @pytest.fixture(scope="class") + def kvs_value_1(self) -> float: + return 111.1 + + @pytest.fixture(scope="class") + def kvs_value_2(self) -> float: + return 222.2 + + @pytest.fixture(scope="class") + def temp_dir( + self, + tmp_path_factory: pytest.TempPathFactory, + ) -> Generator[Path, None, None]: + yield from temp_dir_common(tmp_path_factory, self.__class__.__name__) + + @pytest.fixture(scope="class") + def test_config( + self, + temp_dir: Path, + kvs_key: str, + kvs_value_1: float, + kvs_value_2: float, + ) -> dict[str, Any]: + return { + "kvs_parameters_1": { + "kvs_parameters": {"instance_id": 1, "dir": str(temp_dir)}, + }, + "kvs_parameters_2": { + "kvs_parameters": {"instance_id": 2, "dir": str(temp_dir)}, + }, + "test": {"key": kvs_key, "value_1": kvs_value_1, "value_2": kvs_value_2}, + } + + def test_ok( + self, + kvs_key: str, + kvs_value_1: float, + kvs_value_2: float, + logs_info_level: LogContainer, + ): + log1 = logs_info_level.find_log("instance", value="kvs1") + assert log1 is not None + assert log1.key == kvs_key + assert log1.value == kvs_value_1 + + log2 = logs_info_level.find_log("instance", value="kvs2") + assert log2 is not None + assert log2.key == kvs_key + assert log2.value == kvs_value_2 diff --git a/feature_integration_tests/rust_test_scenarios/src/internals/kyron/mod.rs b/feature_integration_tests/rust_test_scenarios/src/internals/kyron/mod.rs new file mode 100644 index 0000000000..2938643029 --- /dev/null +++ b/feature_integration_tests/rust_test_scenarios/src/internals/kyron/mod.rs @@ -0,0 +1,13 @@ +// +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// +pub mod runtime_helper; diff --git a/feature_integration_tests/rust_test_scenarios/src/internals/runtime_helper.rs b/feature_integration_tests/rust_test_scenarios/src/internals/kyron/runtime_helper.rs similarity index 100% rename from feature_integration_tests/rust_test_scenarios/src/internals/runtime_helper.rs rename to feature_integration_tests/rust_test_scenarios/src/internals/kyron/runtime_helper.rs diff --git a/feature_integration_tests/rust_test_scenarios/src/internals/mod.rs b/feature_integration_tests/rust_test_scenarios/src/internals/mod.rs index 2938643029..117edcc98e 100644 --- a/feature_integration_tests/rust_test_scenarios/src/internals/mod.rs +++ b/feature_integration_tests/rust_test_scenarios/src/internals/mod.rs @@ -10,4 +10,5 @@ // // SPDX-License-Identifier: Apache-2.0 // -pub mod runtime_helper; +pub mod kyron; +pub mod persistency; diff --git a/feature_integration_tests/rust_test_scenarios/src/internals/persistency/kvs_instance.rs b/feature_integration_tests/rust_test_scenarios/src/internals/persistency/kvs_instance.rs new file mode 100644 index 0000000000..9771632a8f --- /dev/null +++ b/feature_integration_tests/rust_test_scenarios/src/internals/persistency/kvs_instance.rs @@ -0,0 +1,29 @@ +//! KVS instance test helpers. + +use crate::internals::persistency::kvs_parameters::KvsParameters; +use rust_kvs::prelude::{ErrorCode, Kvs, KvsBuilder}; + +/// Create KVS instance based on provided parameters. +pub fn kvs_instance(kvs_parameters: KvsParameters) -> Result { + let mut builder = KvsBuilder::new(kvs_parameters.instance_id); + + if let Some(flag) = kvs_parameters.defaults { + builder = builder.defaults(flag); + } + + if let Some(flag) = kvs_parameters.kvs_load { + builder = builder.kvs_load(flag); + } + + if let Some(dir) = kvs_parameters.dir { + builder = builder.dir(dir.to_string_lossy().to_string()); + } + + if let Some(snapshot_max_count) = kvs_parameters.snapshot_max_count { + builder = builder.snapshot_max_count(snapshot_max_count); + } + + let kvs: Kvs = builder.build()?; + + Ok(kvs) +} diff --git a/feature_integration_tests/rust_test_scenarios/src/internals/persistency/kvs_parameters.rs b/feature_integration_tests/rust_test_scenarios/src/internals/persistency/kvs_parameters.rs new file mode 100644 index 0000000000..cbdefeded3 --- /dev/null +++ b/feature_integration_tests/rust_test_scenarios/src/internals/persistency/kvs_parameters.rs @@ -0,0 +1,71 @@ +//! KVS parameters test helpers. + +use rust_kvs::prelude::{InstanceId, KvsDefaults, KvsLoad}; +use serde::{de, Deserialize, Deserializer}; +use std::path::PathBuf; + +/// KVS parameters in serde-compatible format. +#[derive(Deserialize, Debug, Clone)] +#[serde(deny_unknown_fields)] +pub struct KvsParameters { + #[serde(deserialize_with = "deserialize_instance_id")] + pub instance_id: InstanceId, + #[serde(default, deserialize_with = "deserialize_defaults")] + pub defaults: Option, + #[serde(default, deserialize_with = "deserialize_kvs_load")] + pub kvs_load: Option, + pub dir: Option, + pub snapshot_max_count: Option, +} + +impl KvsParameters { + /// Parse `KvsParameters` from `Value`. + /// `Value` is expected to contain `kvs_parameters` field. + pub fn from_value(value: &serde_json::Value) -> Result { + serde_json::from_value(value["kvs_parameters"].clone()) + } +} + +fn deserialize_instance_id<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let value = usize::deserialize(deserializer)?; + Ok(InstanceId(value)) +} + +fn deserialize_defaults<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value_opt: Option = Option::deserialize(deserializer)?; + if let Some(value_str) = value_opt { + let value = match value_str.as_str() { + "ignored" => KvsDefaults::Ignored, + "optional" => KvsDefaults::Optional, + "required" => KvsDefaults::Required, + _ => return Err(de::Error::custom("Invalid \"defaults\" mode")), + }; + return Ok(Some(value)); + } + + Ok(None) +} + +fn deserialize_kvs_load<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value_opt: Option = Option::deserialize(deserializer)?; + if let Some(value_str) = value_opt { + let value = match value_str.as_str() { + "ignored" => KvsLoad::Ignored, + "optional" => KvsLoad::Optional, + "required" => KvsLoad::Required, + _ => return Err(de::Error::custom("Invalid \"kvs_load\" mode")), + }; + return Ok(Some(value)); + } + + Ok(None) +} diff --git a/feature_integration_tests/rust_test_scenarios/src/internals/persistency/mod.rs b/feature_integration_tests/rust_test_scenarios/src/internals/persistency/mod.rs new file mode 100644 index 0000000000..db40645314 --- /dev/null +++ b/feature_integration_tests/rust_test_scenarios/src/internals/persistency/mod.rs @@ -0,0 +1,2 @@ +pub mod kvs_instance; +pub mod kvs_parameters; diff --git a/feature_integration_tests/rust_test_scenarios/src/scenarios/basic/mod.rs b/feature_integration_tests/rust_test_scenarios/src/scenarios/basic/mod.rs index 62667b93a6..cb871dd13a 100644 --- a/feature_integration_tests/rust_test_scenarios/src/scenarios/basic/mod.rs +++ b/feature_integration_tests/rust_test_scenarios/src/scenarios/basic/mod.rs @@ -9,16 +9,15 @@ // // SPDX-License-Identifier: Apache-2.0 // -use test_scenarios_rust::scenario::{ScenarioGroup, ScenarioGroupImpl}; - mod orchestration_with_persistency; +use orchestration_with_persistency::OrchestrationWithPersistency; +use test_scenarios_rust::scenario::{ScenarioGroup, ScenarioGroupImpl}; + pub fn basic_scenario_group() -> Box { Box::new(ScenarioGroupImpl::new( "basic", - vec![Box::new( - orchestration_with_persistency::OrchestrationWithPersistency, - )], + vec![Box::new(OrchestrationWithPersistency)], vec![], )) } diff --git a/feature_integration_tests/rust_test_scenarios/src/scenarios/basic/orchestration_with_persistency.rs b/feature_integration_tests/rust_test_scenarios/src/scenarios/basic/orchestration_with_persistency.rs index a9a7641a77..ceab4b4b15 100644 --- a/feature_integration_tests/rust_test_scenarios/src/scenarios/basic/orchestration_with_persistency.rs +++ b/feature_integration_tests/rust_test_scenarios/src/scenarios/basic/orchestration_with_persistency.rs @@ -10,7 +10,7 @@ // // SPDX-License-Identifier: Apache-2.0 // -use crate::internals::runtime_helper::Runtime; +use crate::internals::kyron::runtime_helper::Runtime; use kyron_foundation::containers::Vector; use kyron_foundation::prelude::CommonErrors; use orchestration::prelude::*; diff --git a/feature_integration_tests/rust_test_scenarios/src/scenarios/mod.rs b/feature_integration_tests/rust_test_scenarios/src/scenarios/mod.rs index 2a381791fa..8c7f224287 100644 --- a/feature_integration_tests/rust_test_scenarios/src/scenarios/mod.rs +++ b/feature_integration_tests/rust_test_scenarios/src/scenarios/mod.rs @@ -13,13 +13,15 @@ use test_scenarios_rust::scenario::{ScenarioGroup, ScenarioGroupImpl}; mod basic; +mod persistency; use basic::basic_scenario_group; +use persistency::persistency_group; pub fn root_scenario_group() -> Box { Box::new(ScenarioGroupImpl::new( "root", vec![], - vec![basic_scenario_group()], + vec![basic_scenario_group(), persistency_group()], )) } diff --git a/feature_integration_tests/rust_test_scenarios/src/scenarios/persistency/mod.rs b/feature_integration_tests/rust_test_scenarios/src/scenarios/persistency/mod.rs new file mode 100644 index 0000000000..8132008c12 --- /dev/null +++ b/feature_integration_tests/rust_test_scenarios/src/scenarios/persistency/mod.rs @@ -0,0 +1,23 @@ +// Copyright (c) 2025 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// +mod multiple_kvs_per_app; + +use multiple_kvs_per_app::MultipleInstanceIds; +use test_scenarios_rust::scenario::{ScenarioGroup, ScenarioGroupImpl}; + +pub fn persistency_group() -> Box { + Box::new(ScenarioGroupImpl::new( + "persistency", + vec![Box::new(MultipleInstanceIds)], + vec![], + )) +} diff --git a/feature_integration_tests/rust_test_scenarios/src/scenarios/persistency/multiple_kvs_per_app.rs b/feature_integration_tests/rust_test_scenarios/src/scenarios/persistency/multiple_kvs_per_app.rs new file mode 100644 index 0000000000..ee99f606c7 --- /dev/null +++ b/feature_integration_tests/rust_test_scenarios/src/scenarios/persistency/multiple_kvs_per_app.rs @@ -0,0 +1,74 @@ +use crate::internals::persistency::{kvs_instance::kvs_instance, kvs_parameters::KvsParameters}; +use rust_kvs::prelude::KvsApi; +use serde::Deserialize; +use serde_json::Value; +use test_scenarios_rust::scenario::Scenario; +use tracing::info; + +#[derive(Deserialize, Debug)] +pub struct TestInput { + pub key: String, + pub value_1: f64, + pub value_2: f64, +} + +impl TestInput { + /// Parse `TestInput` from JSON string. + /// JSON is expected to contain `test` field. + pub fn from_json(json_str: &str) -> Result { + let v: Value = serde_json::from_str(json_str)?; + serde_json::from_value(v["test"].clone()) + } +} + +pub struct MultipleInstanceIds; + +impl Scenario for MultipleInstanceIds { + fn name(&self) -> &str { + "multiple_instance_ids" + } + + fn run(&self, input: &str) -> Result<(), String> { + // Parameters. + let v: Value = serde_json::from_str(input).expect("Failed to parse input string"); + let params1 = + KvsParameters::from_value(&v["kvs_parameters_1"]).expect("Failed to parse parameters"); + let params2 = + KvsParameters::from_value(&v["kvs_parameters_2"]).expect("Failed to parse parameters"); + let logic = TestInput::from_json(input).expect("Failed to parse input string"); + { + // Create first KVS instance. + let kvs1 = kvs_instance(params1.clone()).expect("Failed to create KVS instance"); + + // Create second KVS instance. + let kvs2 = kvs_instance(params2.clone()).expect("Failed to create KVS instance"); + + // Set values to both KVS instances. + kvs1.set_value(&logic.key, logic.value_1) + .expect("Failed to set kvs1 value"); + kvs2.set_value(&logic.key, logic.value_2) + .expect("Failed to set kvs2 value"); + + // Flush KVS. + kvs1.flush().expect("Failed to flush first instance"); + kvs2.flush().expect("Failed to flush second instance"); + } + + { + // Second KVS run. + let kvs1 = kvs_instance(params1).expect("Failed to create KVS1 instance"); + let kvs2 = kvs_instance(params2).expect("Failed to create KVS2 instance"); + + let value1 = kvs1 + .get_value_as::(&logic.key) + .expect("Failed to read kvs1 value"); + info!(instance = "kvs1", key = logic.key, value = value1); + let value2 = kvs2 + .get_value_as::(&logic.key) + .expect("Failed to read kvs2 value"); + info!(instance = "kvs2", key = logic.key, value = value2); + } + + Ok(()) + } +}