Skip to content

Commit 4535544

Browse files
Merge pull request #644 from marvin-hansen/main
Increased test coverage
2 parents b30327c + 4719e8a commit 4535544

173 files changed

Lines changed: 8892 additions & 37 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

deep_causality/tests/errors/action_error_tests.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Copyright (c) 2023 - 2026. The DeepCausality Authors and Contributors. All Rights Reserved.
44
*/
55

6-
use deep_causality::ActionError;
6+
use deep_causality::{ActionError, CausalityError};
77
use std::error::Error;
88

99
#[test]
@@ -43,3 +43,12 @@ fn test_from_string() {
4343
let action_error: ActionError = String::from(error_message).into();
4444
assert_eq!(action_error.0, error_message);
4545
}
46+
47+
#[test]
48+
fn test_into_causality_error() {
49+
// `From<ActionError> for CausalityError` wraps the message in the
50+
// `ActionError` variant of the core error enum.
51+
let action_error = ActionError::new("boom".to_string());
52+
let causality_error: CausalityError = action_error.into();
53+
assert!(causality_error.to_string().contains("boom"));
54+
}

deep_causality/tests/errors/csm_error_tests.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,8 @@ fn test_csm_error_from_uncertain_error() {
7171
format!("{}", csm_err),
7272
"CSM Uncertain Error: Unsupported type: error"
7373
);
74+
75+
// Test source - the Uncertain variant carries a String, not a nested
76+
// error, so `source()` must return None.
77+
assert!(csm_err.source().is_none());
7478
}

deep_causality/tests/extensions/inferable/inferable_vec_tests.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,54 @@ fn test_all_non_inferable() {
3737
assert!(!col.all_non_inferable());
3838
}
3939

40+
/// A mock `Inferable` whose `is_inferable` and `is_inverse_inferable` both
41+
/// return `true`. A real `Inference` can never be both (observation cannot be
42+
/// simultaneously above and below the threshold), so this mock is the only way
43+
/// to drive the "undecidable, hence non-inferable" `true` arm of
44+
/// `all_non_inferable`.
45+
#[derive(Debug)]
46+
struct UndecidableInferable {
47+
id: u64,
48+
}
49+
50+
impl Identifiable for UndecidableInferable {
51+
fn id(&self) -> u64 {
52+
self.id
53+
}
54+
}
55+
56+
impl Inferable for UndecidableInferable {
57+
fn question(&self) -> DescriptionValue {
58+
DescriptionValue::from("undecidable")
59+
}
60+
fn observation(&self) -> NumericalValue {
61+
1.0
62+
}
63+
fn threshold(&self) -> NumericalValue {
64+
0.5
65+
}
66+
fn effect(&self) -> NumericalValue {
67+
1.0
68+
}
69+
fn target(&self) -> NumericalValue {
70+
1.0
71+
}
72+
fn is_inferable(&self) -> bool {
73+
true
74+
}
75+
fn is_inverse_inferable(&self) -> bool {
76+
true
77+
}
78+
}
79+
80+
#[test]
81+
fn test_all_non_inferable_true_for_undecidable_item() {
82+
let col: Vec<UndecidableInferable> = vec![UndecidableInferable { id: 1 }];
83+
// The single item is both inferable and inverse-inferable, so
84+
// `all_non_inferable` short-circuits and returns true.
85+
assert!(col.all_non_inferable());
86+
}
87+
4088
#[test]
4189
fn test_conjoint_delta() {
4290
let col = get_test_inf_vec();
@@ -126,3 +174,63 @@ fn test_is_empty() {
126174
let col = get_test_inf_vec();
127175
assert!(!InferableReasoning::is_empty(&col));
128176
}
177+
178+
// --- Per-item Inferable default-method coverage ---
179+
180+
#[test]
181+
fn test_item_conjoint_delta() {
182+
// Exercises the per-item `Inferable::conjoint_delta` default implementation.
183+
let inf = get_test_inferable(0, false);
184+
// conjoint_delta = abs(1.0 - observation)
185+
let expected = (1.0 - inf.observation()).abs();
186+
assert_eq!(inf.conjoint_delta(), expected);
187+
}
188+
189+
#[test]
190+
fn test_item_is_inferable_true() {
191+
let inf = get_test_inferable(0, false);
192+
assert!(inf.is_inferable());
193+
assert!(!inf.is_inverse_inferable());
194+
}
195+
196+
#[test]
197+
fn test_item_is_inverse_inferable_true() {
198+
let inf = get_test_inferable(0, true);
199+
assert!(inf.is_inverse_inferable());
200+
assert!(!inf.is_inferable());
201+
}
202+
203+
// --- Early-return branches in all_inferable / all_inverse_inferable ---
204+
205+
#[test]
206+
fn test_all_inferable_returns_false_on_mixed_collection() {
207+
// The first item is inverse-inferable (not inferable), so `all_inferable`
208+
// must short-circuit and return false on the first element.
209+
let col: Vec<Inference> =
210+
Vec::from_iter([get_test_inferable(0, true), get_test_inferable(1, false)]);
211+
assert!(!col.all_inferable());
212+
}
213+
214+
#[test]
215+
fn test_all_inverse_inferable_returns_false_on_mixed_collection() {
216+
// The first item is inferable (not inverse-inferable), so
217+
// `all_inverse_inferable` must short-circuit and return false.
218+
let col: Vec<Inference> =
219+
Vec::from_iter([get_test_inferable(0, false), get_test_inferable(1, true)]);
220+
assert!(!col.all_inverse_inferable());
221+
}
222+
223+
// --- Empty-collection guard branches ---
224+
225+
#[test]
226+
fn test_empty_collection_metrics_short_circuit() {
227+
let col: Vec<Inference> = Vec::new();
228+
229+
// conjoint_delta on an empty collection returns the neutral 1.0.
230+
assert_eq!(col.conjoint_delta(), 1.0);
231+
232+
// All percentage metrics return 0.0 on an empty collection.
233+
assert_eq!(col.percent_inferable(), 0.0);
234+
assert_eq!(col.percent_inverse_inferable(), 0.0);
235+
assert_eq!(col.percent_non_inferable(), 0.0);
236+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* SPDX-License-Identifier: MIT
3+
* Copyright (c) 2023 - 2026. The DeepCausality Authors and Contributors. All Rights Reserved.
4+
*/
5+
6+
//! Exercises the *default* method bodies of the `Adjustable<T>` and
7+
//! `UncertainAdjustable` traits.
8+
//!
9+
//! The default implementations intentionally do nothing and simply return
10+
//! `Ok(())` so that `update`/`adjust` are optional for node types. To cover
11+
//! those default bodies we declare two minimal types that implement the traits
12+
//! without overriding any method, then invoke the inherited defaults.
13+
14+
use deep_causality::utils_test::test_utils_array_grid;
15+
use deep_causality::{Adjustable, UncertainAdjustable};
16+
17+
/// A trivial type relying entirely on the default `Adjustable<i32>` impl.
18+
#[derive(Debug, Default, Clone, Copy, PartialEq)]
19+
struct PlainNode {
20+
value: i32,
21+
}
22+
23+
impl Adjustable<i32> for PlainNode {}
24+
25+
/// A trivial type relying entirely on the default `UncertainAdjustable` impl.
26+
#[derive(Debug, Default, Clone, PartialEq)]
27+
struct PlainUncertainNode;
28+
29+
impl UncertainAdjustable for PlainUncertainNode {
30+
type Data = f64;
31+
}
32+
33+
#[test]
34+
fn test_adjustable_default_update_is_noop_ok() {
35+
let mut node = PlainNode { value: 7 };
36+
let grid = test_utils_array_grid::get_1d_array_grid(99);
37+
38+
// The default body returns Ok(()) and leaves the node untouched.
39+
let res = node.update(&grid);
40+
assert!(res.is_ok());
41+
assert_eq!(node.value, 7);
42+
}
43+
44+
#[test]
45+
fn test_adjustable_default_adjust_is_noop_ok() {
46+
let mut node = PlainNode { value: 3 };
47+
let grid = test_utils_array_grid::get_1d_array_grid(123);
48+
49+
let res = node.adjust(&grid);
50+
assert!(res.is_ok());
51+
assert_eq!(node.value, 3);
52+
}
53+
54+
#[test]
55+
fn test_uncertain_adjustable_default_update_is_ok() {
56+
let mut node = PlainUncertainNode;
57+
let res = node.update(1.5_f64);
58+
assert!(res.is_ok());
59+
}
60+
61+
#[test]
62+
fn test_uncertain_adjustable_default_adjust_is_ok() {
63+
let mut node = PlainUncertainNode;
64+
let res = node.adjust(-2.5_f64);
65+
assert!(res.is_ok());
66+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/*
2+
* SPDX-License-Identifier: MIT
3+
* Copyright (c) 2023 - 2026. The DeepCausality Authors and Contributors. All Rights Reserved.
4+
*/
5+
#[cfg(test)]
6+
mod adjustable_default_tests;

deep_causality/tests/traits/causable_collection/collection_reasoning/stateful_monadic_collection_tests.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,93 @@ fn build_incoming() -> PropagatingProcess<u64, CounterState, ConfigCtx> {
8282
}
8383
}
8484

85+
fn item_uncertain_float(
86+
_obs: EffectValue<u64>,
87+
state: CounterState,
88+
ctx: Option<ConfigCtx>,
89+
) -> PropagatingProcess<deep_causality_uncertain::UncertainF64, CounterState, ConfigCtx> {
90+
PropagatingProcess {
91+
value: EffectValue::Value(deep_causality_uncertain::Uncertain::<f64>::point(1.0)),
92+
state,
93+
context: ctx,
94+
error: None,
95+
logs: EffectLog::new(),
96+
}
97+
}
98+
99+
#[test]
100+
fn evaluate_collection_stateful_short_circuits_on_incoming_error() {
101+
// An incoming process that already carries an error returns immediately
102+
// with that error and the incoming state preserved; no item runs.
103+
let items: Vec<Causaloid<u64, bool, CounterState, ConfigCtx>> =
104+
vec![Causaloid::new_with_context(
105+
1,
106+
item_true_increment,
107+
ConfigCtx { threshold: 1 },
108+
"a",
109+
)];
110+
111+
let incoming = PropagatingProcess {
112+
value: EffectValue::Value(7u64),
113+
state: CounterState { count: 9 },
114+
context: Some(ConfigCtx { threshold: 1 }),
115+
error: Some(CausalityError::new(CausalityErrorEnum::Custom(
116+
"pre-existing".into(),
117+
))),
118+
logs: EffectLog::new(),
119+
};
120+
121+
let out =
122+
items
123+
.as_slice()
124+
.evaluate_collection_stateful(&incoming, &AggregateLogic::All, Some(0.5));
125+
126+
assert!(out.error.is_some());
127+
assert_eq!(out.state.count, 9, "incoming state preserved, no item ran");
128+
}
129+
130+
#[test]
131+
fn evaluate_collection_stateful_empty_collection_errors() {
132+
let items: Vec<Causaloid<u64, bool, CounterState, ConfigCtx>> = vec![];
133+
134+
let out = items.as_slice().evaluate_collection_stateful(
135+
&build_incoming(),
136+
&AggregateLogic::All,
137+
Some(0.5),
138+
);
139+
140+
assert!(out.error.is_some());
141+
assert!(format!("{:?}", out.error).contains("Cannot evaluate an empty collection"));
142+
}
143+
144+
#[test]
145+
fn evaluate_collection_stateful_aggregation_error() {
146+
// Items evaluate successfully but produce UncertainF64 values, which the
147+
// aggregation helper cannot combine -> the `Err(e)` aggregation arm runs.
148+
let items: Vec<
149+
Causaloid<u64, deep_causality_uncertain::UncertainF64, CounterState, ConfigCtx>,
150+
> = vec![
151+
Causaloid::new_with_context(1, item_uncertain_float, ConfigCtx { threshold: 1 }, "a"),
152+
Causaloid::new_with_context(2, item_uncertain_float, ConfigCtx { threshold: 1 }, "b"),
153+
];
154+
155+
let incoming = PropagatingProcess {
156+
value: EffectValue::Value(7u64),
157+
state: CounterState::default(),
158+
context: Some(ConfigCtx { threshold: 1 }),
159+
error: None,
160+
logs: EffectLog::new(),
161+
};
162+
163+
let out =
164+
items
165+
.as_slice()
166+
.evaluate_collection_stateful(&incoming, &AggregateLogic::All, Some(0.5));
167+
168+
assert!(out.error.is_some());
169+
assert!(format!("{:?}", out.error).contains("not supported"));
170+
}
171+
85172
#[test]
86173
fn evaluate_collection_stateful_aggregates_and_threads_state() {
87174
// Three items each increment the counter; aggregator is "Some(2)" out of 3 trues.

deep_causality/tests/traits/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* SPDX-License-Identifier: MIT
33
* Copyright (c) 2023 - 2026. The DeepCausality Authors and Contributors. All Rights Reserved.
44
*/
5+
pub mod adjustable;
56
pub mod causable_collection;
67
pub mod causable_graph;
78
pub mod observable;

deep_causality/tests/types/causal_types/causaloid/causaloid_collection_tests.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,24 @@ fn test_evaluate_collection_with_sub_evaluation_error() {
144144
assert!(err.to_string().contains("Test error"));
145145
}
146146

147+
#[test]
148+
fn test_evaluate_collection_aggregation_error() {
149+
// Each item evaluates successfully to `Value(UncertainF64)`, but direct
150+
// aggregation of `UncertainF64` is unsupported, so the aggregation helper
151+
// returns an error which `evaluate_collection` propagates (the `Err(e)`
152+
// aggregation arm).
153+
let causal_coll = Arc::new(vec![
154+
test_utils::get_test_causaloid_uncertain_float(),
155+
test_utils::get_test_causaloid_uncertain_float(),
156+
]);
157+
158+
let effect = PropagatingEffect::from_value(0.99_f64);
159+
let res = causal_coll.evaluate_collection(&effect, &AggregateLogic::All, Some(0.5));
160+
161+
assert!(res.error.is_some());
162+
assert!(res.error.unwrap().to_string().contains("not supported"));
163+
}
164+
147165
#[test]
148166
fn test_evaluate_collection_without_true_effect() {
149167
// Setup: A collection with only 'false' causaloids.

deep_causality/tests/types/causal_types/causaloid_graph/causality_graph_nodes_tests.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,17 @@ fn test_remove_causaloid_error() {
193193
let res = g.remove_causaloid(99); // Invalid index
194194
assert!(res.is_err());
195195
}
196+
197+
#[test]
198+
fn test_add_causaloid_error_when_frozen() {
199+
// `add_causaloid` delegates to the underlying graph's `add_node`, which
200+
// returns an error once the graph is frozen. This drives the `Err(e)`
201+
// mapping arm in `add_causaloid`.
202+
let mut g = CausaloidGraph::new(0);
203+
g.add_root_causaloid(test_utils::get_test_causaloid_deterministic(0))
204+
.expect("root");
205+
g.freeze();
206+
207+
let res = g.add_causaloid(test_utils::get_test_causaloid_deterministic(1));
208+
assert!(res.is_err());
209+
}

0 commit comments

Comments
 (0)