Skip to content

Commit 4a653b4

Browse files
Add test for feat_req__persistency__multiple_kvs (#73)
1 parent 31ed67c commit 4a653b4

16 files changed

Lines changed: 363 additions & 24 deletions

File tree

feature_integration_tests/python_test_cases/BUILD

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ score_virtualenv(
2828
# Tests targets
2929
score_py_pytest(
3030
name = "fit",
31-
srcs = glob(["tests/**/*.py"]) + ["conftest.py", "fit_scenario.py"],
31+
srcs = glob(["tests/**/*.py"]) + ["conftest.py", "fit_scenario.py", "test_properties.py"],
3232
args = [
3333
"--traces=all",
3434
"--rust-target-path=$(rootpath //feature_integration_tests/rust_test_scenarios)",

feature_integration_tests/python_test_cases/fit_scenario.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@
1212
)
1313

1414

15+
class ResultCode:
16+
"""
17+
Test scenario exit codes.
18+
"""
19+
20+
SUCCESS = 0
21+
PANIC = 101
22+
SIGKILL = -9
23+
SIGABRT = -6
24+
25+
1526
def temp_dir_common(
1627
tmp_path_factory: pytest.TempPathFactory, base_name: str, *args: str
1728
) -> Generator[Path, None, None]:
@@ -63,7 +74,7 @@ def results(
6374
**kwargs,
6475
) -> ScenarioResult:
6576
result = self._run_command(command, execution_timeout, args, kwargs)
66-
success = result.return_code == 0 and not result.hang
77+
success = result.return_code == ResultCode.SUCCESS and not result.hang
6778
if self.expect_command_failure() and success:
6879
raise RuntimeError(f"Command execution succeeded unexpectedly: {result=}")
6980
if not self.expect_command_failure() and not success:
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
try:
2+
from attribute_plugin import add_test_properties # type: ignore[import-untyped]
3+
except ImportError:
4+
# Define no-op decorator if attribute_plugin is not available (outside bazel)
5+
# Keeps IDE debugging functionality
6+
def add_test_properties(*args, **kwargs):
7+
def decorator(func):
8+
return func # No-op decorator
9+
10+
return decorator

feature_integration_tests/python_test_cases/tests/basic/test_orchestartion_with_persistency.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,11 @@
11
import json
2+
from collections.abc import Generator
23
from pathlib import Path
3-
from typing import Any, Generator
4+
from typing import Any
45

56
import pytest
6-
7-
try:
8-
from attribute_plugin import add_test_properties # type: ignore[import-untyped]
9-
except ImportError:
10-
# Define no-op decorator if attribute_plugin is not available (outside bazel)
11-
# Keeps IDE debugging functionality
12-
def add_test_properties(*args, **kwargs):
13-
def decorator(func):
14-
return func # No-op decorator
15-
16-
return decorator
17-
18-
197
from fit_scenario import FitScenario, temp_dir_common
8+
from test_properties import add_test_properties
209
from testing_utils import LogContainer
2110

2211

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# *******************************************************************************
2+
# Copyright (c) 2025 Contributors to the Eclipse Foundation
3+
#
4+
# See the NOTICE file(s) distributed with this work for additional
5+
# information regarding copyright ownership.
6+
#
7+
# This program and the accompanying materials are made available under the
8+
# terms of the Apache License Version 2.0 which is available at
9+
# https://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# SPDX-License-Identifier: Apache-2.0
12+
# *******************************************************************************
13+
14+
import json
15+
from pathlib import Path
16+
from typing import Any, Generator
17+
18+
import pytest
19+
from fit_scenario import FitScenario, temp_dir_common
20+
from test_properties import add_test_properties
21+
from testing_utils import LogContainer
22+
23+
24+
@add_test_properties(
25+
partially_verifies=["feat_req__persistency__multiple_kvs"],
26+
test_type="requirements-based",
27+
derivation_technique="requirements-analysis",
28+
)
29+
class TestMultipleInstanceIds(FitScenario):
30+
"""
31+
Verifies that multiple KVS instances with different IDs store and retrieve independent values without interference.
32+
"""
33+
34+
@pytest.fixture(scope="class")
35+
def scenario_name(self) -> str:
36+
return "persistency.multiple_kvs_per_app"
37+
38+
@pytest.fixture(scope="class")
39+
def kvs_key(self) -> str:
40+
return "number"
41+
42+
@pytest.fixture(scope="class")
43+
def kvs_value_1(self) -> float:
44+
return 111.1
45+
46+
@pytest.fixture(scope="class")
47+
def kvs_value_2(self) -> float:
48+
return 222.2
49+
50+
@pytest.fixture(scope="class")
51+
def temp_dir(
52+
self,
53+
tmp_path_factory: pytest.TempPathFactory,
54+
) -> Generator[Path, None, None]:
55+
yield from temp_dir_common(tmp_path_factory, self.__class__.__name__)
56+
57+
@pytest.fixture(scope="class")
58+
def test_config(
59+
self,
60+
temp_dir: Path,
61+
kvs_key: str,
62+
kvs_value_1: float,
63+
kvs_value_2: float,
64+
) -> dict[str, Any]:
65+
return {
66+
"kvs_parameters_1": {
67+
"kvs_parameters": {"instance_id": 1, "dir": str(temp_dir)},
68+
},
69+
"kvs_parameters_2": {
70+
"kvs_parameters": {"instance_id": 2, "dir": str(temp_dir)},
71+
},
72+
"test": {"key": kvs_key, "value_1": kvs_value_1, "value_2": kvs_value_2},
73+
}
74+
75+
def test_logged_execution(
76+
self,
77+
kvs_key: str,
78+
kvs_value_1: float,
79+
kvs_value_2: float,
80+
logs_info_level: LogContainer,
81+
):
82+
log1 = logs_info_level.find_log("instance", value="InstanceId(1)")
83+
assert log1 is not None
84+
assert log1.key == kvs_key
85+
assert log1.value == kvs_value_1
86+
87+
log2 = logs_info_level.find_log("instance", value="InstanceId(2)")
88+
assert log2 is not None
89+
assert log2.key == kvs_key
90+
assert log2.value == kvs_value_2
91+
92+
def test_kvs_write_results(
93+
self,
94+
temp_dir: Path,
95+
kvs_key: str,
96+
kvs_value_1: float,
97+
kvs_value_2: float,
98+
):
99+
# Verify KVS Instance(1)
100+
kvs1_file = temp_dir / "kvs_1_0.json"
101+
data1 = json.loads(kvs1_file.read_text())
102+
assert data1["v"][kvs_key]["v"] == kvs_value_1
103+
104+
# Verify KVS Instance(2)
105+
kvs2_file = temp_dir / "kvs_2_0.json"
106+
data2 = json.loads(kvs2_file.read_text())
107+
assert data2["v"][kvs_key]["v"] == kvs_value_2
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//
2+
// Copyright (c) 2025 Contributors to the Eclipse Foundation
3+
//
4+
// See the NOTICE file(s) distributed with this work for additional
5+
// information regarding copyright ownership.
6+
//
7+
// This program and the accompanying materials are made available under the
8+
// terms of the Apache License Version 2.0 which is available at
9+
// <https://www.apache.org/licenses/LICENSE-2.0>
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
pub mod runtime_helper;

feature_integration_tests/rust_test_scenarios/src/internals/runtime_helper.rs renamed to feature_integration_tests/rust_test_scenarios/src/internals/kyron/runtime_helper.rs

File renamed without changes.

feature_integration_tests/rust_test_scenarios/src/internals/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@
1010
//
1111
// SPDX-License-Identifier: Apache-2.0
1212
//
13-
pub mod runtime_helper;
13+
pub mod kyron;
14+
pub mod persistency;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//! KVS instance test helpers.
2+
3+
use crate::internals::persistency::kvs_parameters::KvsParameters;
4+
use rust_kvs::prelude::{ErrorCode, Kvs, KvsBuilder};
5+
6+
/// Create KVS instance based on provided parameters.
7+
pub fn kvs_instance(kvs_parameters: KvsParameters) -> Result<Kvs, ErrorCode> {
8+
let mut builder = KvsBuilder::new(kvs_parameters.instance_id);
9+
10+
if let Some(flag) = kvs_parameters.defaults {
11+
builder = builder.defaults(flag);
12+
}
13+
14+
if let Some(flag) = kvs_parameters.kvs_load {
15+
builder = builder.kvs_load(flag);
16+
}
17+
18+
if let Some(dir) = kvs_parameters.dir {
19+
builder = builder.dir(dir.to_string_lossy().to_string());
20+
}
21+
22+
if let Some(snapshot_max_count) = kvs_parameters.snapshot_max_count {
23+
builder = builder.snapshot_max_count(snapshot_max_count);
24+
}
25+
26+
let kvs: Kvs = builder.build()?;
27+
28+
Ok(kvs)
29+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//! KVS parameters test helpers.
2+
3+
use rust_kvs::prelude::{InstanceId, KvsDefaults, KvsLoad};
4+
use serde::{de, Deserialize, Deserializer};
5+
use std::path::PathBuf;
6+
7+
/// KVS parameters in serde-compatible format.
8+
#[derive(Deserialize, Debug, Clone)]
9+
#[serde(deny_unknown_fields)]
10+
pub struct KvsParameters {
11+
#[serde(deserialize_with = "deserialize_instance_id")]
12+
pub instance_id: InstanceId,
13+
#[serde(default, deserialize_with = "deserialize_defaults")]
14+
pub defaults: Option<KvsDefaults>,
15+
#[serde(default, deserialize_with = "deserialize_kvs_load")]
16+
pub kvs_load: Option<KvsLoad>,
17+
pub dir: Option<PathBuf>,
18+
pub snapshot_max_count: Option<usize>,
19+
}
20+
21+
impl KvsParameters {
22+
/// Parse `KvsParameters` from `Value`.
23+
/// `Value` is expected to contain `kvs_parameters` field.
24+
pub fn from_value(value: &serde_json::Value) -> Result<Self, serde_json::Error> {
25+
serde_json::from_value(value["kvs_parameters"].clone())
26+
}
27+
}
28+
29+
fn deserialize_instance_id<'de, D>(deserializer: D) -> Result<InstanceId, D::Error>
30+
where
31+
D: Deserializer<'de>,
32+
{
33+
let value = usize::deserialize(deserializer)?;
34+
Ok(InstanceId(value))
35+
}
36+
37+
fn deserialize_defaults<'de, D>(deserializer: D) -> Result<Option<KvsDefaults>, D::Error>
38+
where
39+
D: Deserializer<'de>,
40+
{
41+
let value_opt: Option<String> = Option::deserialize(deserializer)?;
42+
if let Some(value_str) = value_opt {
43+
let value = match value_str.as_str() {
44+
"ignored" => KvsDefaults::Ignored,
45+
"optional" => KvsDefaults::Optional,
46+
"required" => KvsDefaults::Required,
47+
_ => return Err(de::Error::custom("Invalid \"defaults\" mode")),
48+
};
49+
return Ok(Some(value));
50+
}
51+
52+
Ok(None)
53+
}
54+
55+
fn deserialize_kvs_load<'de, D>(deserializer: D) -> Result<Option<KvsLoad>, D::Error>
56+
where
57+
D: Deserializer<'de>,
58+
{
59+
let value_opt: Option<String> = Option::deserialize(deserializer)?;
60+
if let Some(value_str) = value_opt {
61+
let value = match value_str.as_str() {
62+
"ignored" => KvsLoad::Ignored,
63+
"optional" => KvsLoad::Optional,
64+
"required" => KvsLoad::Required,
65+
_ => return Err(de::Error::custom("Invalid \"kvs_load\" mode")),
66+
};
67+
return Ok(Some(value));
68+
}
69+
70+
Ok(None)
71+
}

0 commit comments

Comments
 (0)