use picante::debug::{CacheStats, DependencyGraph, TraceAnalysis, TraceCollector};
use picante::{
DerivedIngredient, HasRuntime, IngredientLookup, IngredientRegistry, InputIngredient,
QueryKindId, Runtime,
};
use std::sync::Arc;
#[derive(Default)]
struct TestDb {
runtime: Runtime,
ingredients: IngredientRegistry<TestDb>,
}
impl HasRuntime for TestDb {
fn runtime(&self) -> &Runtime {
&self.runtime
}
}
impl IngredientLookup for TestDb {
fn ingredient(&self, kind: QueryKindId) -> Option<&dyn picante::DynIngredient<Self>> {
self.ingredients.ingredient(kind)
}
}
#[tokio_test_lite::test]
async fn test_dependency_graph_visualization() {
let mut db = TestDb::default();
let input: Arc<InputIngredient<u32, String>> =
Arc::new(InputIngredient::new(QueryKindId(1), "input"));
let derived: Arc<DerivedIngredient<TestDb, u32, usize>> = {
let input = input.clone();
Arc::new(DerivedIngredient::new(
QueryKindId(2),
"derived",
move |db, key| {
let input = input.clone();
Box::pin(async move {
let s = input.get(db, &key)?.unwrap_or_default();
Ok(s.len())
})
},
))
};
db.ingredients.register(input.clone());
db.ingredients.register(derived.clone());
input.set(&db, 1, "hello".to_string());
input.set(&db, 2, "world".to_string());
let _ = derived.get(&db, 1).await.unwrap();
let _ = derived.get(&db, 2).await.unwrap();
let graph = DependencyGraph::from_runtime(db.runtime());
assert!(!graph.forward_deps.is_empty(), "Should have forward deps");
let mut output = Vec::new();
graph.write_dot_to(&mut output).unwrap();
let dot = String::from_utf8(output).unwrap();
assert!(dot.contains("digraph dependencies"));
assert!(dot.contains("->"));
}
#[tokio_test_lite::test]
async fn test_cache_statistics() {
let mut db = TestDb::default();
let input: Arc<InputIngredient<u32, String>> =
Arc::new(InputIngredient::new(QueryKindId(1), "input"));
let derived: Arc<DerivedIngredient<TestDb, u32, usize>> = {
let input = input.clone();
Arc::new(DerivedIngredient::new(
QueryKindId(2),
"derived",
move |db, key| {
let input = input.clone();
Box::pin(async move {
let s = input.get(db, &key)?.unwrap_or_default();
Ok(s.len())
})
},
))
};
db.ingredients.register(input.clone());
db.ingredients.register(derived.clone());
input.set(&db, 1, "hello".to_string());
input.set(&db, 2, "world".to_string());
input.set(&db, 3, "foo".to_string());
let _ = derived.get(&db, 1).await.unwrap();
let _ = derived.get(&db, 2).await.unwrap();
let _ = derived.get(&db, 3).await.unwrap();
let stats = CacheStats::collect(db.runtime());
assert!(stats.forward_deps_count > 0);
assert!(stats.total_dependency_edges > 0);
let formatted = stats.format();
assert!(formatted.contains("Cache Statistics"));
assert!(formatted.contains("Forward deps:"));
}
#[tokio_test_lite::test]
async fn test_trace_collector() {
let mut db = TestDb::default();
let input: Arc<InputIngredient<u32, String>> =
Arc::new(InputIngredient::new(QueryKindId(1), "input"));
let derived: Arc<DerivedIngredient<TestDb, u32, usize>> = {
let input = input.clone();
Arc::new(DerivedIngredient::new(
QueryKindId(2),
"derived",
move |db, key| {
let input = input.clone();
Box::pin(async move {
let s = input.get(db, &key)?.unwrap_or_default();
Ok(s.len())
})
},
))
};
db.ingredients.register(input.clone());
db.ingredients.register(derived.clone());
let collector = TraceCollector::start(db.runtime());
input.set(&db, 1, "hello".to_string());
let _ = derived.get(&db, 1).await.unwrap();
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
let trace = collector.stop().await;
assert!(!trace.is_empty(), "Should have recorded events");
let analysis = TraceAnalysis::from_trace(&trace);
assert!(analysis.total_events > 0);
assert!(analysis.input_changes > 0);
let formatted = analysis.format();
assert!(formatted.contains("Trace Analysis"));
assert!(formatted.contains("Total events:"));
}
#[tokio_test_lite::test]
async fn test_trace_analysis_with_invalidations() {
let mut db = TestDb::default();
let input: Arc<InputIngredient<u32, String>> =
Arc::new(InputIngredient::new(QueryKindId(1), "input"));
let derived: Arc<DerivedIngredient<TestDb, u32, usize>> = {
let input = input.clone();
Arc::new(DerivedIngredient::new(
QueryKindId(2),
"derived",
move |db, key| {
let input = input.clone();
Box::pin(async move {
let s = input.get(db, &key)?.unwrap_or_default();
Ok(s.len())
})
},
))
};
db.ingredients.register(input.clone());
db.ingredients.register(derived.clone());
let collector = TraceCollector::start(db.runtime());
input.set(&db, 1, "hello".to_string());
let result1 = derived.get(&db, 1).await.unwrap();
assert_eq!(result1, 5);
input.set(&db, 1, "world!".to_string());
let result2 = derived.get(&db, 1).await.unwrap();
assert_eq!(result2, 6);
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
let trace = collector.stop().await;
let analysis = TraceAnalysis::from_trace(&trace);
assert!(
analysis.input_changes >= 2,
"Should have at least 2 input changes"
);
assert!(analysis.invalidations > 0, "Should have invalidations");
}
#[tokio_test_lite::test]
async fn test_dependency_graph_path_finding() {
let mut db = TestDb::default();
let input: Arc<InputIngredient<u32, String>> =
Arc::new(InputIngredient::new(QueryKindId(1), "input"));
let derived1: Arc<DerivedIngredient<TestDb, u32, usize>> = {
let input = input.clone();
Arc::new(DerivedIngredient::new(
QueryKindId(2),
"derived1",
move |db, key| {
let input = input.clone();
Box::pin(async move {
let s = input.get(db, &key)?.unwrap_or_default();
Ok(s.len())
})
},
))
};
let derived2: Arc<DerivedIngredient<TestDb, u32, usize>> = {
let derived1 = derived1.clone();
Arc::new(DerivedIngredient::new(
QueryKindId(3),
"derived2",
move |db, key| {
let derived1 = derived1.clone();
Box::pin(async move {
let len = derived1.get(db, key).await?;
Ok(len * 2)
})
},
))
};
db.ingredients.register(input.clone());
db.ingredients.register(derived1.clone());
db.ingredients.register(derived2.clone());
input.set(&db, 1, "hello".to_string());
let _ = derived2.get(&db, 1).await.unwrap();
let graph = DependencyGraph::from_runtime(db.runtime());
assert!(
graph.forward_deps.len() >= 2,
"Should have chain of dependencies"
);
}