Skip to content
11 changes: 10 additions & 1 deletion deep_causality/tests/errors/action_error_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Copyright (c) 2023 - 2026. The DeepCausality Authors and Contributors. All Rights Reserved.
*/

use deep_causality::ActionError;
use deep_causality::{ActionError, CausalityError};
use std::error::Error;

#[test]
Expand Down Expand Up @@ -43,3 +43,12 @@ fn test_from_string() {
let action_error: ActionError = String::from(error_message).into();
assert_eq!(action_error.0, error_message);
}

#[test]
fn test_into_causality_error() {
// `From<ActionError> for CausalityError` wraps the message in the
// `ActionError` variant of the core error enum.
let action_error = ActionError::new("boom".to_string());
let causality_error: CausalityError = action_error.into();
assert!(causality_error.to_string().contains("boom"));
}
4 changes: 4 additions & 0 deletions deep_causality/tests/errors/csm_error_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,8 @@ fn test_csm_error_from_uncertain_error() {
format!("{}", csm_err),
"CSM Uncertain Error: Unsupported type: error"
);

// Test source - the Uncertain variant carries a String, not a nested
// error, so `source()` must return None.
assert!(csm_err.source().is_none());
}
108 changes: 108 additions & 0 deletions deep_causality/tests/extensions/inferable/inferable_vec_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,54 @@ fn test_all_non_inferable() {
assert!(!col.all_non_inferable());
}

/// A mock `Inferable` whose `is_inferable` and `is_inverse_inferable` both
/// return `true`. A real `Inference` can never be both (observation cannot be
/// simultaneously above and below the threshold), so this mock is the only way
/// to drive the "undecidable, hence non-inferable" `true` arm of
/// `all_non_inferable`.
#[derive(Debug)]
struct UndecidableInferable {
id: u64,
}

impl Identifiable for UndecidableInferable {
fn id(&self) -> u64 {
self.id
}
}

impl Inferable for UndecidableInferable {
fn question(&self) -> DescriptionValue {
DescriptionValue::from("undecidable")
}
fn observation(&self) -> NumericalValue {
1.0
}
fn threshold(&self) -> NumericalValue {
0.5
}
fn effect(&self) -> NumericalValue {
1.0
}
fn target(&self) -> NumericalValue {
1.0
}
fn is_inferable(&self) -> bool {
true
}
fn is_inverse_inferable(&self) -> bool {
true
}
}

#[test]
fn test_all_non_inferable_true_for_undecidable_item() {
let col: Vec<UndecidableInferable> = vec![UndecidableInferable { id: 1 }];
// The single item is both inferable and inverse-inferable, so
// `all_non_inferable` short-circuits and returns true.
assert!(col.all_non_inferable());
}

#[test]
fn test_conjoint_delta() {
let col = get_test_inf_vec();
Expand Down Expand Up @@ -126,3 +174,63 @@ fn test_is_empty() {
let col = get_test_inf_vec();
assert!(!InferableReasoning::is_empty(&col));
}

// --- Per-item Inferable default-method coverage ---

#[test]
fn test_item_conjoint_delta() {
// Exercises the per-item `Inferable::conjoint_delta` default implementation.
let inf = get_test_inferable(0, false);
// conjoint_delta = abs(1.0 - observation)
let expected = (1.0 - inf.observation()).abs();
assert_eq!(inf.conjoint_delta(), expected);
}

#[test]
fn test_item_is_inferable_true() {
let inf = get_test_inferable(0, false);
assert!(inf.is_inferable());
assert!(!inf.is_inverse_inferable());
}

#[test]
fn test_item_is_inverse_inferable_true() {
let inf = get_test_inferable(0, true);
assert!(inf.is_inverse_inferable());
assert!(!inf.is_inferable());
}

// --- Early-return branches in all_inferable / all_inverse_inferable ---

#[test]
fn test_all_inferable_returns_false_on_mixed_collection() {
// The first item is inverse-inferable (not inferable), so `all_inferable`
// must short-circuit and return false on the first element.
let col: Vec<Inference> =
Vec::from_iter([get_test_inferable(0, true), get_test_inferable(1, false)]);
assert!(!col.all_inferable());
}

#[test]
fn test_all_inverse_inferable_returns_false_on_mixed_collection() {
// The first item is inferable (not inverse-inferable), so
// `all_inverse_inferable` must short-circuit and return false.
let col: Vec<Inference> =
Vec::from_iter([get_test_inferable(0, false), get_test_inferable(1, true)]);
assert!(!col.all_inverse_inferable());
}

// --- Empty-collection guard branches ---

#[test]
fn test_empty_collection_metrics_short_circuit() {
let col: Vec<Inference> = Vec::new();

// conjoint_delta on an empty collection returns the neutral 1.0.
assert_eq!(col.conjoint_delta(), 1.0);

// All percentage metrics return 0.0 on an empty collection.
assert_eq!(col.percent_inferable(), 0.0);
assert_eq!(col.percent_inverse_inferable(), 0.0);
assert_eq!(col.percent_non_inferable(), 0.0);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* SPDX-License-Identifier: MIT
* Copyright (c) 2023 - 2026. The DeepCausality Authors and Contributors. All Rights Reserved.
*/

//! Exercises the *default* method bodies of the `Adjustable<T>` and
//! `UncertainAdjustable` traits.
//!
//! The default implementations intentionally do nothing and simply return
//! `Ok(())` so that `update`/`adjust` are optional for node types. To cover
//! those default bodies we declare two minimal types that implement the traits
//! without overriding any method, then invoke the inherited defaults.

use deep_causality::utils_test::test_utils_array_grid;
use deep_causality::{Adjustable, UncertainAdjustable};

/// A trivial type relying entirely on the default `Adjustable<i32>` impl.
#[derive(Debug, Default, Clone, Copy, PartialEq)]
struct PlainNode {
value: i32,
}

impl Adjustable<i32> for PlainNode {}

/// A trivial type relying entirely on the default `UncertainAdjustable` impl.
#[derive(Debug, Default, Clone, PartialEq)]
struct PlainUncertainNode;

impl UncertainAdjustable for PlainUncertainNode {
type Data = f64;
}

#[test]
fn test_adjustable_default_update_is_noop_ok() {
let mut node = PlainNode { value: 7 };
let grid = test_utils_array_grid::get_1d_array_grid(99);

// The default body returns Ok(()) and leaves the node untouched.
let res = node.update(&grid);
assert!(res.is_ok());
assert_eq!(node.value, 7);
}

#[test]
fn test_adjustable_default_adjust_is_noop_ok() {
let mut node = PlainNode { value: 3 };
let grid = test_utils_array_grid::get_1d_array_grid(123);

let res = node.adjust(&grid);
assert!(res.is_ok());
assert_eq!(node.value, 3);
}

#[test]
fn test_uncertain_adjustable_default_update_is_ok() {
let mut node = PlainUncertainNode;
let res = node.update(1.5_f64);
assert!(res.is_ok());
}

#[test]
fn test_uncertain_adjustable_default_adjust_is_ok() {
let mut node = PlainUncertainNode;
let res = node.adjust(-2.5_f64);
assert!(res.is_ok());
}
6 changes: 6 additions & 0 deletions deep_causality/tests/traits/adjustable/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* SPDX-License-Identifier: MIT
* Copyright (c) 2023 - 2026. The DeepCausality Authors and Contributors. All Rights Reserved.
*/
#[cfg(test)]
mod adjustable_default_tests;
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,93 @@ fn build_incoming() -> PropagatingProcess<u64, CounterState, ConfigCtx> {
}
}

fn item_uncertain_float(
_obs: EffectValue<u64>,
state: CounterState,
ctx: Option<ConfigCtx>,
) -> PropagatingProcess<deep_causality_uncertain::UncertainF64, CounterState, ConfigCtx> {
PropagatingProcess {
value: EffectValue::Value(deep_causality_uncertain::Uncertain::<f64>::point(1.0)),
state,
context: ctx,
error: None,
logs: EffectLog::new(),
}
}

#[test]
fn evaluate_collection_stateful_short_circuits_on_incoming_error() {
// An incoming process that already carries an error returns immediately
// with that error and the incoming state preserved; no item runs.
let items: Vec<Causaloid<u64, bool, CounterState, ConfigCtx>> =
vec![Causaloid::new_with_context(
1,
item_true_increment,
ConfigCtx { threshold: 1 },
"a",
)];

let incoming = PropagatingProcess {
value: EffectValue::Value(7u64),
state: CounterState { count: 9 },
context: Some(ConfigCtx { threshold: 1 }),
error: Some(CausalityError::new(CausalityErrorEnum::Custom(
"pre-existing".into(),
))),
logs: EffectLog::new(),
};

let out =
items
.as_slice()
.evaluate_collection_stateful(&incoming, &AggregateLogic::All, Some(0.5));

assert!(out.error.is_some());
assert_eq!(out.state.count, 9, "incoming state preserved, no item ran");
}

#[test]
fn evaluate_collection_stateful_empty_collection_errors() {
let items: Vec<Causaloid<u64, bool, CounterState, ConfigCtx>> = vec![];

let out = items.as_slice().evaluate_collection_stateful(
&build_incoming(),
&AggregateLogic::All,
Some(0.5),
);

assert!(out.error.is_some());
assert!(format!("{:?}", out.error).contains("Cannot evaluate an empty collection"));
}

#[test]
fn evaluate_collection_stateful_aggregation_error() {
// Items evaluate successfully but produce UncertainF64 values, which the
// aggregation helper cannot combine -> the `Err(e)` aggregation arm runs.
let items: Vec<
Causaloid<u64, deep_causality_uncertain::UncertainF64, CounterState, ConfigCtx>,
> = vec![
Causaloid::new_with_context(1, item_uncertain_float, ConfigCtx { threshold: 1 }, "a"),
Causaloid::new_with_context(2, item_uncertain_float, ConfigCtx { threshold: 1 }, "b"),
];

let incoming = PropagatingProcess {
value: EffectValue::Value(7u64),
state: CounterState::default(),
context: Some(ConfigCtx { threshold: 1 }),
error: None,
logs: EffectLog::new(),
};

let out =
items
.as_slice()
.evaluate_collection_stateful(&incoming, &AggregateLogic::All, Some(0.5));

assert!(out.error.is_some());
assert!(format!("{:?}", out.error).contains("not supported"));
}

#[test]
fn evaluate_collection_stateful_aggregates_and_threads_state() {
// Three items each increment the counter; aggregator is "Some(2)" out of 3 trues.
Expand Down
1 change: 1 addition & 0 deletions deep_causality/tests/traits/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* SPDX-License-Identifier: MIT
* Copyright (c) 2023 - 2026. The DeepCausality Authors and Contributors. All Rights Reserved.
*/
pub mod adjustable;
pub mod causable_collection;
pub mod causable_graph;
pub mod observable;
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,24 @@ fn test_evaluate_collection_with_sub_evaluation_error() {
assert!(err.to_string().contains("Test error"));
}

#[test]
fn test_evaluate_collection_aggregation_error() {
// Each item evaluates successfully to `Value(UncertainF64)`, but direct
// aggregation of `UncertainF64` is unsupported, so the aggregation helper
// returns an error which `evaluate_collection` propagates (the `Err(e)`
// aggregation arm).
let causal_coll = Arc::new(vec![
test_utils::get_test_causaloid_uncertain_float(),
test_utils::get_test_causaloid_uncertain_float(),
]);

let effect = PropagatingEffect::from_value(0.99_f64);
let res = causal_coll.evaluate_collection(&effect, &AggregateLogic::All, Some(0.5));

assert!(res.error.is_some());
assert!(res.error.unwrap().to_string().contains("not supported"));
}

#[test]
fn test_evaluate_collection_without_true_effect() {
// Setup: A collection with only 'false' causaloids.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,17 @@ fn test_remove_causaloid_error() {
let res = g.remove_causaloid(99); // Invalid index
assert!(res.is_err());
}

#[test]
fn test_add_causaloid_error_when_frozen() {
// `add_causaloid` delegates to the underlying graph's `add_node`, which
// returns an error once the graph is frozen. This drives the `Err(e)`
// mapping arm in `add_causaloid`.
let mut g = CausaloidGraph::new(0);
g.add_root_causaloid(test_utils::get_test_causaloid_deterministic(0))
.expect("root");
g.freeze();

let res = g.add_causaloid(test_utils::get_test_causaloid_deterministic(1));
assert!(res.is_err());
}
Loading
Loading