Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion feature_integration_tests/python_test_cases/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down
13 changes: 12 additions & 1 deletion feature_integration_tests/python_test_cases/fit_scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down Expand Up @@ -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:
Expand Down
10 changes: 10 additions & 0 deletions feature_integration_tests/python_test_cases/test_properties.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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


Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
// <https://www.apache.org/licenses/LICENSE-2.0>
//
// SPDX-License-Identifier: Apache-2.0
//
pub mod runtime_helper;
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
//
// SPDX-License-Identifier: Apache-2.0
//
pub mod runtime_helper;
pub mod kyron;
pub mod persistency;
Original file line number Diff line number Diff line change
@@ -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<Kvs, ErrorCode> {
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)
}
Original file line number Diff line number Diff line change
@@ -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<KvsDefaults>,
#[serde(default, deserialize_with = "deserialize_kvs_load")]
pub kvs_load: Option<KvsLoad>,
pub dir: Option<PathBuf>,
pub snapshot_max_count: Option<usize>,
}

impl KvsParameters {
/// Parse `KvsParameters` from `Value`.
/// `Value` is expected to contain `kvs_parameters` field.
pub fn from_value(value: &serde_json::Value) -> Result<Self, serde_json::Error> {
Comment on lines +22 to +24
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The from_value method expects a nested kvs_parameters field within the provided value, which creates an inconsistent API. The method already receives a value parameter, so requiring another kvs_parameters key inside it is confusing. Consider either removing the nested access or renaming the method to clarify this expectation.

Suggested change
/// Parse `KvsParameters` from `Value`.
/// `Value` is expected to contain `kvs_parameters` field.
pub fn from_value(value: &serde_json::Value) -> Result<Self, serde_json::Error> {
/// Parse `KvsParameters` directly from a JSON `Value`.
///
/// The provided `value` is expected to contain the fields of `KvsParameters`
/// at its top level (i.e., it should serialize exactly as `KvsParameters`).
pub fn from_value(value: &serde_json::Value) -> Result<Self, serde_json::Error> {
serde_json::from_value(value.clone())
}
/// Parse `KvsParameters` from a JSON `Value` that wraps it in a
/// `kvs_parameters` field.
///
/// This is useful when the configuration JSON has a structure like:
///
/// `{ "kvs_parameters": { ...fields of KvsParameters... } }`
pub fn from_wrapped_value(value: &serde_json::Value) -> Result<Self, serde_json::Error> {

Copilot uses AI. Check for mistakes.
serde_json::from_value(value["kvs_parameters"].clone())
}
}

fn deserialize_instance_id<'de, D>(deserializer: D) -> Result<InstanceId, D::Error>
where
D: Deserializer<'de>,
{
let value = usize::deserialize(deserializer)?;
Ok(InstanceId(value))
}

fn deserialize_defaults<'de, D>(deserializer: D) -> Result<Option<KvsDefaults>, D::Error>
where
D: Deserializer<'de>,
{
let value_opt: Option<String> = 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<Option<KvsLoad>, D::Error>
where
D: Deserializer<'de>,
{
let value_opt: Option<String> = 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)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod kvs_instance;
pub mod kvs_parameters;
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn ScenarioGroup> {
Box::new(ScenarioGroupImpl::new(
"basic",
vec![Box::new(
orchestration_with_persistency::OrchestrationWithPersistency,
)],
vec![Box::new(OrchestrationWithPersistency)],
vec![],
))
}
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn ScenarioGroup> {
Box::new(ScenarioGroupImpl::new(
"root",
vec![],
vec![basic_scenario_group()],
vec![basic_scenario_group(), persistency_group()],
))
}
Original file line number Diff line number Diff line change
@@ -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
// <https://www.apache.org/licenses/LICENSE-2.0>
//
// 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<dyn ScenarioGroup> {
Box::new(ScenarioGroupImpl::new(
"persistency",
vec![Box::new(MultipleInstanceIds)],
vec![],
))
}
Loading