Skip to content

Commit dd8e06c

Browse files
committed
test: add integration tests for fault-lib ↔ DFM interaction
Add tests/integration/ crate with 15 integration tests across 4 modules: - test_report_and_query: basic fault reporting → DFM processing → SOVD query - test_lifecycle_transitions: full lifecycle state machine (NotTested→PreFailed→Failed→Passed) - test_persistent_storage: KVS persistence across DFM restart, delete operations - test_multi_catalog: multi-tenant catalog isolation, cross-catalog independence Tests use shared KVS storage (process-wide global pool constraint) with serial_test for isolation and clean_catalogs() for inter-test cleanup.
1 parent 849361a commit dd8e06c

9 files changed

Lines changed: 894 additions & 2 deletions

File tree

Cargo.lock

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ members = [
66
"src/dfm_lib",
77
"src/fault_lib",
88
"src/xtask",
9+
"tests/integration",
910
]
1011

1112
[workspace.package]
@@ -17,8 +18,8 @@ readme = "README.md"
1718

1819
[workspace.dependencies]
1920
env_logger = "0.11.8"
20-
iceoryx2 = { git = "https://github.com/eclipse-iceoryx/iceoryx2.git", rev = "eba5da4b8d8cb03bccf1394d88a05e31f58838dc"}
21-
iceoryx2-bb-container = { git = "https://github.com/eclipse-iceoryx/iceoryx2.git", rev = "eba5da4b8d8cb03bccf1394d88a05e31f58838dc"}
21+
iceoryx2 = { git = "https://github.com/eclipse-iceoryx/iceoryx2.git", rev = "eba5da4b8d8cb03bccf1394d88a05e31f58838dc" }
22+
iceoryx2-bb-container = { git = "https://github.com/eclipse-iceoryx/iceoryx2.git", rev = "eba5da4b8d8cb03bccf1394d88a05e31f58838dc" }
2223
log = "0.4.22"
2324
mockall = "0.13.1"
2425
serial_test = "3.2"

tests/integration/Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "integration_tests"
3+
version.workspace = true
4+
edition.workspace = true
5+
publish = false
6+
7+
[lib]
8+
name = "integration_tests"
9+
path = "src/lib.rs"
10+
11+
[dev-dependencies]
12+
common = { path = "../../src/common" }
13+
dfm_lib = { path = "../../src/dfm_lib" }
14+
fault_lib = { path = "../../src/fault_lib" }
15+
tempfile = "3.20"
16+
env_logger = "0.11.8"
17+
log = "0.4.22"
18+
serial_test.workspace = true

tests/integration/src/helpers.rs

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// Copyright (c) 2025 Contributors to the Eclipse Foundation
2+
//
3+
// See the NOTICE file(s) distributed with this work for additional
4+
// information regarding copyright ownership.
5+
//
6+
// This program and the accompanying materials are made available under the
7+
// terms of the Apache License Version 2.0 which is available at
8+
// <https://www.apache.org/licenses/LICENSE-2.0>
9+
//
10+
// SPDX-License-Identifier: Apache-2.0
11+
//
12+
//! Shared helpers for integration tests.
13+
//!
14+
//! Provides a convenience [`TestHarness`] that wires up all DFM components
15+
//! (catalog, registry, processor, storage, SOVD manager) in a single call,
16+
//! matching the real deployment topology minus IPC transport.
17+
18+
use common::catalog::{FaultCatalogBuilder, FaultCatalogConfig};
19+
use common::debounce::DebounceMode;
20+
use common::fault::*;
21+
use common::types::*;
22+
use dfm_lib::fault_catalog_registry::FaultCatalogRegistry;
23+
use dfm_lib::fault_record_processor::FaultRecordProcessor;
24+
use dfm_lib::operation_cycle::OperationCycleTracker;
25+
use dfm_lib::sovd_fault_manager::SovdFaultManager;
26+
use dfm_lib::sovd_fault_storage::KvsSovdFaultStateStorage;
27+
use std::path::Path;
28+
use std::sync::{Arc, LazyLock, RwLock};
29+
use std::time::Duration;
30+
use tempfile::TempDir;
31+
32+
/// Shared KVS storage directory used by **all** integration tests.
33+
///
34+
/// KVS uses a process-wide global pool (`KVS_MAX_INSTANCES = 10`) that
35+
/// binds each instance ID to a specific backend path on first use.
36+
/// Subsequent calls with the same instance ID but a *different* path
37+
/// return `InstanceParametersMismatch`.
38+
///
39+
/// To work around this, every test uses the **same** directory for KVS
40+
/// instance 0. Tests are serialized with `#[serial]` to prevent parallel
41+
/// data corruption, and each test cleans the data via `delete_all_faults`.
42+
static SHARED_STORAGE_DIR: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().expect("failed to create shared storage dir"));
43+
44+
/// Returns the path of the shared KVS storage directory.
45+
pub fn shared_storage_path() -> &'static Path {
46+
SHARED_STORAGE_DIR.path()
47+
}
48+
49+
/// All-in-one test harness wiring DFM components together.
50+
///
51+
/// All instances share the same KVS backend directory (process-wide
52+
/// constraint). Tests must run serially (`#[serial]`).
53+
pub struct TestHarness {
54+
pub processor: FaultRecordProcessor<KvsSovdFaultStateStorage>,
55+
pub manager: SovdFaultManager<KvsSovdFaultStateStorage>,
56+
}
57+
58+
impl TestHarness {
59+
/// Build a harness from one or more [`FaultCatalogConfig`]s.
60+
///
61+
/// Each config represents a separate reporter application's fault catalog
62+
/// (e.g., HVAC, IVI), mirroring how multiple apps register with a single DFM.
63+
pub fn new(configs: Vec<FaultCatalogConfig>) -> Self {
64+
Self::with_storage_path(configs, shared_storage_path())
65+
}
66+
67+
/// Build a harness using an explicit storage path. Useful for persistence
68+
/// tests where the same directory is reused across harness instances.
69+
pub fn with_storage_path(configs: Vec<FaultCatalogConfig>, storage_path: &Path) -> Self {
70+
let storage = Arc::new(KvsSovdFaultStateStorage::new(storage_path, 0).expect("storage init"));
71+
let catalogs: Vec<_> = configs
72+
.into_iter()
73+
.map(|cfg| FaultCatalogBuilder::new().cfg_struct(cfg).expect("builder config").build())
74+
.collect();
75+
let registry = Arc::new(FaultCatalogRegistry::new(catalogs));
76+
let cycle_tracker = Arc::new(RwLock::new(OperationCycleTracker::new()));
77+
78+
let processor = FaultRecordProcessor::new(Arc::clone(&storage), Arc::clone(&registry), cycle_tracker);
79+
let manager = SovdFaultManager::new(storage, registry);
80+
81+
Self { processor, manager }
82+
}
83+
84+
/// Clean all fault data from the shared storage.
85+
///
86+
/// Call this at the start of each test to ensure a clean slate
87+
/// (defence-in-depth alongside `#[serial]`).
88+
pub fn clean_catalogs(&mut self, paths: &[&str]) {
89+
for path in paths {
90+
// Ignore errors — the path may not have data yet.
91+
let _ = self.manager.delete_all_faults(path);
92+
}
93+
}
94+
}
95+
96+
// ============================================================================
97+
// Catalog configs
98+
// ============================================================================
99+
100+
/// HVAC subsystem catalog with two faults:
101+
/// - `CabinTempSensorStuck` (Numeric 0x7001, reporter-side HoldTime debounce)
102+
/// - `BlowerSpeedMismatch` (Text, manager-side EdgeWithCooldown debounce)
103+
pub fn hvac_catalog_config() -> FaultCatalogConfig {
104+
FaultCatalogConfig {
105+
id: "hvac".into(),
106+
version: 3,
107+
faults: vec![
108+
FaultDescriptor {
109+
id: FaultId::Numeric(0x7001),
110+
name: to_static_short_string("CabinTempSensorStuck").unwrap(),
111+
summary: None,
112+
category: FaultType::Communication,
113+
severity: FaultSeverity::Error,
114+
compliance: ComplianceVec::try_from(&[ComplianceTag::EmissionRelevant][..]).unwrap(),
115+
reporter_side_debounce: Some(DebounceMode::HoldTime {
116+
duration: Duration::from_secs(60),
117+
}),
118+
reporter_side_reset: None,
119+
manager_side_debounce: None,
120+
manager_side_reset: None,
121+
},
122+
FaultDescriptor {
123+
id: FaultId::Text(to_static_short_string("hvac.blower.speed_sensor_mismatch").unwrap()),
124+
name: to_static_short_string("BlowerSpeedMismatch").unwrap(),
125+
summary: Some(to_static_long_string("Blower motor speed does not match commanded value").unwrap()),
126+
category: FaultType::Communication,
127+
severity: FaultSeverity::Error,
128+
compliance: ComplianceVec::try_from(&[ComplianceTag::SecurityRelevant, ComplianceTag::SafetyCritical][..]).unwrap(),
129+
reporter_side_debounce: None,
130+
reporter_side_reset: None,
131+
manager_side_debounce: Some(DebounceMode::EdgeWithCooldown {
132+
cooldown: Duration::from_millis(100),
133+
}),
134+
manager_side_reset: None,
135+
},
136+
],
137+
}
138+
}
139+
140+
/// IVI (In-Vehicle Infotainment) catalog with a single software fault.
141+
pub fn ivi_catalog_config() -> FaultCatalogConfig {
142+
FaultCatalogConfig {
143+
id: "ivi".into(),
144+
version: 1,
145+
faults: vec![FaultDescriptor {
146+
id: FaultId::Text(to_static_short_string("ivi.display.init_timeout").unwrap()),
147+
name: to_static_short_string("DisplayInitTimeout").unwrap(),
148+
summary: Some(to_static_long_string("Display initialization exceeded 5s timeout").unwrap()),
149+
category: FaultType::Software,
150+
severity: FaultSeverity::Warn,
151+
compliance: ComplianceVec::new(),
152+
reporter_side_debounce: None,
153+
reporter_side_reset: None,
154+
manager_side_debounce: None,
155+
manager_side_reset: None,
156+
}],
157+
}
158+
}
159+
160+
// ============================================================================
161+
// Record builders
162+
// ============================================================================
163+
164+
/// Build a [`FaultRecord`] simulating what a reporter would create.
165+
pub fn make_fault_record(fault_id: FaultId, stage: LifecycleStage) -> FaultRecord {
166+
FaultRecord {
167+
id: fault_id,
168+
time: IpcTimestamp::default(),
169+
source: common::SourceId {
170+
entity: to_static_short_string("test_reporter").unwrap(),
171+
ecu: Some(to_static_short_string("ECU-A").unwrap()),
172+
domain: Some(to_static_short_string("body").unwrap()),
173+
sw_component: Some(to_static_short_string("hvac_ctrl").unwrap()),
174+
instance: Some(to_static_short_string("0").unwrap()),
175+
},
176+
lifecycle_phase: LifecyclePhase::Running,
177+
lifecycle_stage: stage,
178+
env_data: MetadataVec::new(),
179+
}
180+
}
181+
182+
/// Build a [`FaultRecord`] with environment data attached.
183+
pub fn make_fault_record_with_env(fault_id: FaultId, stage: LifecycleStage, env: &[(&str, &str)]) -> FaultRecord {
184+
let env_data = MetadataVec::try_from(
185+
&env.iter()
186+
.map(|(k, v)| (to_static_short_string(k).unwrap(), to_static_short_string(v).unwrap()))
187+
.collect::<Vec<_>>()[..],
188+
)
189+
.unwrap();
190+
191+
FaultRecord {
192+
id: fault_id,
193+
time: IpcTimestamp::default(),
194+
source: common::SourceId {
195+
entity: to_static_short_string("test_reporter").unwrap(),
196+
ecu: Some(to_static_short_string("ECU-A").unwrap()),
197+
domain: Some(to_static_short_string("body").unwrap()),
198+
sw_component: Some(to_static_short_string("hvac_ctrl").unwrap()),
199+
instance: Some(to_static_short_string("0").unwrap()),
200+
},
201+
lifecycle_phase: LifecyclePhase::Running,
202+
lifecycle_stage: stage,
203+
env_data,
204+
}
205+
}
206+
207+
/// Helper to create a [`LongString`] path for DFM routing.
208+
pub fn make_path(path: &str) -> LongString {
209+
LongString::from_str_truncated(path).unwrap()
210+
}

tests/integration/src/lib.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) 2025 Contributors to the Eclipse Foundation
2+
//
3+
// See the NOTICE file(s) distributed with this work for additional
4+
// information regarding copyright ownership.
5+
//
6+
// This program and the accompanying materials are made available under the
7+
// terms of the Apache License Version 2.0 which is available at
8+
// <https://www.apache.org/licenses/LICENSE-2.0>
9+
//
10+
// SPDX-License-Identifier: Apache-2.0
11+
//
12+
//! Integration tests demonstrating the fault-lib ↔ DFM end-to-end flow.
13+
//!
14+
//! These tests exercise the full pipeline without IPC (iceoryx2), using
15+
//! in-process wiring instead:
16+
//!
17+
//! 1. Build a `FaultCatalog` from JSON config
18+
//! 2. Create a `FaultRecordProcessor` (DFM core) with persistent storage
19+
//! 3. Simulate reporter-side record creation
20+
//! 4. Feed records through the processor
21+
//! 5. Query results via `SovdFaultManager`
22+
//!
23+
//! This mirrors a real deployment where fault-lib reporters publish to DFM
24+
//! over IPC, but tests the logic without shared-memory transport.
25+
26+
#[cfg(test)]
27+
mod helpers;
28+
#[cfg(test)]
29+
mod test_lifecycle_transitions;
30+
#[cfg(test)]
31+
mod test_multi_catalog;
32+
#[cfg(test)]
33+
mod test_persistent_storage;
34+
#[cfg(test)]
35+
mod test_report_and_query;

0 commit comments

Comments
 (0)