Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* SPDX-License-Identifier: MIT
* Copyright (c) 2023 - 2026. The DeepCausality Authors and Contributors. All Rights Reserved.
*/

use deep_causality::utils_test::test_utils_graph;
use deep_causality::*;
use std::sync::Arc;
use std::sync::RwLock;

#[test]
fn test_causaloid_getters_singleton() {
let id = 1;
let description = "singleton getter test";

fn causal_fn(_input: ()) -> PropagatingEffect<bool> {
PropagatingEffect::from_value(true)
}

let causaloid = BaseCausaloid::<(), bool>::new(id, causal_fn, description);

// Test all getters for Singleton
assert_eq!(causaloid.id(), id);
assert!(matches!(causaloid.causal_type(), CausaloidType::Singleton));
assert!(causaloid.causal_fn().is_some());
assert!(causaloid.context_causal_fn().is_none());
assert!(causaloid.context().is_none());
assert!(causaloid.causal_collection().is_none());
assert!(causaloid.causal_graph().is_none());
assert_eq!(causaloid.description(), description);
assert!(causaloid.coll_aggregate_logic().is_none());
assert!(causaloid.coll_threshold_value().is_none());
}

#[test]
fn test_causaloid_getters_contextual() {
let id = 2;
let description = "contextual getter test";
let context = BaseContext::with_capacity(99, "test_ctx", 10);

fn context_fn(
_obs: EffectValue<()>,
_state: (),
_ctx: Option<Arc<RwLock<BaseContext>>>,
) -> PropagatingProcess<bool, (), Arc<RwLock<BaseContext>>> {
PropagatingProcess::pure(true)
}

let causaloid = BaseCausaloid::<(), bool>::new_with_context(
id,
context_fn,
Arc::new(RwLock::new(context)),
description,
);

// Test all getters for Contextual Singleton
assert_eq!(causaloid.id(), id);
assert!(matches!(causaloid.causal_type(), CausaloidType::Singleton));
assert!(causaloid.causal_fn().is_none());
assert!(causaloid.context_causal_fn().is_some());
assert!(causaloid.context().is_some());
assert!(causaloid.causal_collection().is_none());
assert!(causaloid.causal_graph().is_none());
assert_eq!(causaloid.description(), description);
}

#[test]
fn test_causaloid_getters_collection() {
let id = 3;
let description = "collection getter test";

// Create dummy children
fn causal_fn(_input: ()) -> PropagatingEffect<bool> {
PropagatingEffect::from_value(true)
}
let c1 = BaseCausaloid::<(), bool>::new(10, causal_fn, "c1");
let coll = Arc::new(vec![c1]);
let agg = AggregateLogic::All;
let thresh = 0.5;

let causaloid = BaseCausaloid::<(), bool>::from_causal_collection(
id,
coll.clone(),
description,
agg,
thresh,
);

// Test all getters for Collection
assert_eq!(causaloid.id(), id);
assert!(matches!(causaloid.causal_type(), CausaloidType::Collection));
assert!(causaloid.causal_fn().is_none());
assert!(causaloid.context_causal_fn().is_none());
assert!(causaloid.context().is_none());
assert!(causaloid.causal_collection().is_some());
// Verify collection content roughly
assert_eq!(causaloid.causal_collection().unwrap().len(), 1);

assert!(causaloid.causal_graph().is_none());
assert_eq!(causaloid.description(), description);

assert!(causaloid.coll_aggregate_logic().is_some());
assert!(matches!(
causaloid.coll_aggregate_logic().unwrap(),
AggregateLogic::All
));

assert!(causaloid.coll_threshold_value().is_some());
assert_eq!(*causaloid.coll_threshold_value().unwrap(), thresh);
}

#[test]
fn test_causaloid_getters_graph() {
let id = 4;
let description = "graph getter test";

let graph = test_utils_graph::build_multi_cause_graph();
let causaloid: BaseCausaloid<f64, f64> =
Causaloid::from_causal_graph(id, description, Arc::new(graph));

// Test all getters for Graph
assert_eq!(causaloid.id(), id);
assert!(matches!(causaloid.causal_type(), CausaloidType::Graph));
assert!(causaloid.causal_fn().is_none());
assert!(causaloid.context_causal_fn().is_none());
assert!(causaloid.context().is_none());
assert!(causaloid.causal_collection().is_none());
assert!(causaloid.causal_graph().is_some());
assert_eq!(causaloid.description(), description);
assert!(causaloid.coll_aggregate_logic().is_none());
assert!(causaloid.coll_threshold_value().is_none());
}
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,177 @@ fn test_evaluate_singleton_with_context() {
}

// Removed test_evaluate_singleton_err as it was testing compile-time type mismatch with runtime logic.

#[test]
fn test_evaluate_collection_error() {
fn causal_fn(_input: ()) -> PropagatingEffect<bool> {
PropagatingEffect::from_value(true)
}

let c1 = BaseCausaloid::<(), bool>::new(1, causal_fn, "c1");
let c_coll = BaseCausaloid::<(), bool>::from_causal_collection(
3,
Arc::new(vec![c1]),
"collection",
AggregateLogic::All,
0.0,
);

let effect = PropagatingEffect::from_value(());
let res = c_coll.evaluate(&effect);

assert!(res.is_err());
let err_msg = res.error.unwrap().to_string();
assert!(err_msg.contains("Collection evaluation is not available"));
}

#[test]
fn test_evaluate_graph_error() {
// Use existing test utility to build a valid causal graph
let graph = test_utils_graph::build_multi_cause_graph();
let c_graph: BaseCausaloid<f64, f64> =
Causaloid::from_causal_graph(4, "graph from utility", Arc::new(graph));

let effect = PropagatingEffect::from_value(1.0);
let res = c_graph.evaluate(&effect);

assert!(res.is_err());
let err_msg = res.error.unwrap().to_string();
assert!(err_msg.contains("Graph evaluation is not available"));
}

#[test]
fn test_context_error_paths() {
// Test case: Context is None
let _id: IdentificationValue = 1;
let _description = "context missing test";

// We can't easily construct a Causaloid with context_causal_fn but NO context using public API
// because new_with_context() requires passing a context.
// However, we can construct one manually if we really need to, or check if there's an internal path.
// Actually, looking at the struct, context is Option<CTX>.
// But new_with_context sets it to Some(context).
// Let's see if we can trick it or if we need to modify the test to be more intrusive/unit-testy or
// if there's a constructor I missed.
// Ah, Causaloid struct fields are private.
// Unless we use unsafe or have a constructor that allows None context.
// Wait, the code in causable_utils.rs checks `if let Some(context) = causaloid.context.as_ref()`.
// If we can't create such a state via public API, then that branch is unreachable in normal usage
// and might be dead code, OR we might need to use a builder pattern if one existed.
// Currently, `new_with_context` always sets internal context to Some.
// So that branch `else { PropagatingEffect::from_error(...) }` in `execute_causal_logic` might be technically unreachable
// via public types unless we implement a custom constructor for testing or if I missed something.

// Actually, let's look at `Causaloid` definition again.
// `context: Option<CTX>`.

// If we can't easily hit that branch via public API, maybe we skip it or accept it's dead code?
// User wants 100% coverage.
// Let's try to mock or see if we can create a causaloid and then somehow mutate it? No, immutable.
// Wait, maybe `from_causal_collection` or others leave context as None?
// `from_causal_collection` sets `context: None`, `context_causal_fn: None`.
// `execute_causal_logic` checks `if let Some(context_fn) = &causaloid.context_causal_fn`.
// So if `context_causal_fn` is None, it goes to `else if let Some(causal_fn)`.

// The specific branch in `causable_utils.rs` is:
// if let Some(context_fn) = &causaloid.context_causal_fn {
// if let Some(context) = causaloid.context.as_ref() { ... } else { ERROR }
// }

// To hit the else (ERROR), we need `context_causal_fn` to be Some, but `context` to be None.
// `new_with_context` sets both to Some.
// `new` sets both to None.
// `from_causal_collection` sets both to None.
// `from_causal_collection_with_context` sets `context` to Some, but `context_causal_fn` to None!

// There seems to be NO public constructor that sets `context_causal_fn` to Some and `context` to None.
// So that error path "Causaloid::evaluate: context is None" is likely unreachable with current constructors.
// However, I can't remove the code. I can try to use unsafe to force it for the test if I want 100% coverage.
// Or I can add a test-only constructor.
// Since I cannot modify src code easily to add test-only constructors without cluttering, maybe I can use `std::mem::transmute`
// or just accept I can't cover it if it's true unreachable.
// BUT, the user requested 100%.

// Wait, let's look at `execute_causal_logic` again.
// `else { let err_msg = format!("Causaloid {} is missing both ...") ... }`
// This path is reachable if both are None.
// But `evaluate` calls `execute_causal_logic` ONLY for `CausaloidType::Singleton`.

// `new` sets `causal_fn` to Some.
// `new_with_context` sets `context_causal_fn` to Some.
// So a Singleton ALWAYS has one of them.

// If I create a Singleton that has NEITHER, I hit the "missing both" error.
// If I create a Singleton with `context_causal_fn` but NO `context`, I hit the "context is None" error.

// How to create such a malformed Causaloid?
// Constructing it manually in the test module?
// The test module differs from the src module, it likely can't see private fields.
// Unless I make fields pub(crate) and the test is in the same crate?
// The test file `causaloid_singleton_tests.rs` is in `tests/`. It treats `deep_causality` as an external crate.
// So I cannot access private fields.

// Conclusion: These error paths are defensive programming against invalid internal state that ideally shouldn't exist.
// If I really want to test them, I might need to add a "Testing" constructor or similar, or just skip if unreachable.
// BUT coverage tools report it as missing.

// Let's verify if I can reach "missing both" error.
// `from_causal_collection` creates a `Collection` type. `evaluate` handles `Collection` separately (returns error).
// So `evaluate` won't call `execute_causal_logic` for Collection.

// So `execute_causal_logic` is ONLY called for Singleton.
// And Singletons result from `new` or `new_with_context`.
// Both ensure valid state.

// So those branches ARE unreachable via public API.
// I will write the tests that ARE possible (the Collection/Graph evaluation errors).
// For the reachable error paths:
// 1. `ctx.is_none()` inside `contextual_causal_fn` logic - wait, that's inside the user-provided function!
// We CAN test that if we pass a context function that fails.
// `execute_causal_logic` calls the user function.
// `let process = context_fn(ev, PS::default(), Some(context.clone()));`
// It ALWAYS passes Some(context). So user function receives Some.

// Wait, `execute_causal_logic` lines 44-55:
// `match process.value.into_value() { Some(val) => ..., None => error }`
// We CAN test this! If the user function returns a process with None value.

// Also `context_causal_fn` return type `PropagatingProcess` can contain error.
// If it contains error and None value, `execute_causal_logic` propagates it.
// If it contains None value and NO error, `execute_causal_logic` creates a custom error "context_fn returned None...".

// So I CAN test:
// 1. User function returning None value + No Error -> checks the synthetic error generation.
// 2. User function returning None value + Error -> checks error propagation.
}

#[test]
fn test_contextual_fn_returning_none() {
let id: IdentificationValue = 99;
let description = "test none return";
let context = get_base_context();

fn bad_fn(
_obs: EffectValue<NumericalValue>,
_state: (),
_ctx: Option<Arc<RwLock<BaseContext>>>,
) -> PropagatingProcess<bool, (), Arc<RwLock<BaseContext>>> {
// Return a process with None value and None error
PropagatingProcess::none()
}

let causaloid = BaseCausaloid::<NumericalValue, bool>::new_with_context(
id,
bad_fn,
Arc::new(RwLock::new(context)),
description,
);

let effect = PropagatingEffect::from_value(1.0);
// This should trigger "context_fn returned None value and no error"
let res = causaloid.evaluate(&effect);

assert!(res.is_err());
let err = res.error.unwrap().to_string();
assert!(err.contains("context_fn returned None value"));
}
2 changes: 2 additions & 0 deletions deep_causality/tests/types/causal_types/causaloid/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ mod causaloid_collection_tests;
#[cfg(test)]
mod causaloid_debug_tests;
#[cfg(test)]
mod causaloid_getters_tests;
#[cfg(test)]
mod causaloid_graph_tests;
#[cfg(test)]
mod causaloid_singleton_tests;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,33 @@
* Copyright (c) 2023 - 2026. The DeepCausality Authors and Contributors. All Rights Reserved.
*/

use deep_causality::utils_test::test_utils;
use deep_causality::*;
use ultragraph::*;

use deep_causality::utils_test::test_utils;
#[test]
fn test_add_edge_error() {
let mut g = CausaloidGraph::<BaseCausaloid<NumericalValue, bool>>::new(0);
// Try to add edge between non-existent nodes
let res = g.add_edge(0, 1);
assert!(res.is_err());
}

#[test]
fn test_add_edge_with_weight_error() {
let mut g = CausaloidGraph::<BaseCausaloid<NumericalValue, bool>>::new(0);
// Try to add edge between non-existent nodes
let res = g.add_edg_with_weight(0, 1, 10);
assert!(res.is_err());
}

#[test]
fn test_remove_edge_error() {
let mut g = CausaloidGraph::<BaseCausaloid<NumericalValue, bool>>::new(0);
// Try to remove non-existent edge
let res = g.remove_edge(0, 1);
assert!(res.is_err());
}

#[test]
fn test_new() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,38 @@ fn test_get_graph() {
assert_eq!(graph.number_edges(), 0);
assert_eq!(graph.number_nodes(), 0);
}

#[test]
fn test_contains_root_causaloid_empty() {
let g = CausaloidGraph::<BaseCausaloid<NumericalValue, bool>>::new(0);
assert!(!g.contains_root_causaloid());
}

#[test]
fn test_get_root_causaloid_empty() {
let g = CausaloidGraph::<BaseCausaloid<NumericalValue, bool>>::new(0);
assert!(g.get_root_causaloid().is_none());
}

#[test]
fn test_is_empty_and_clear() {
let mut g = CausaloidGraph::new(0);
assert!(g.is_empty());

let causaloid = test_utils::get_test_causaloid_deterministic(1);
g.add_causaloid(causaloid).unwrap();
assert!(!g.is_empty());
assert_eq!(g.size(), 1);
assert_eq!(g.number_nodes(), 1);

g.clear();
assert!(g.is_empty());
assert_eq!(g.size(), 0);
}

#[test]
fn test_remove_causaloid_error() {
let mut g = CausaloidGraph::<BaseCausaloid<NumericalValue, bool>>::new(0);
let res = g.remove_causaloid(99); // Invalid index
assert!(res.is_err());
}
Loading
Loading