diff --git a/core/engine/Cargo.toml b/core/engine/Cargo.toml index 14eeffcd..a5136d1e 100644 --- a/core/engine/Cargo.toml +++ b/core/engine/Cargo.toml @@ -25,7 +25,7 @@ v8 = { version = "0.66.0" } zen-expression = { path = "../expression", version = "0.4.1" } [dev-dependencies] -tokio-test = "0.4.2" +tokio = { version = "1.28", features = ["macros"] } criterion = { workspace = true, features = ["async_futures"] } [features] diff --git a/core/engine/src/engine.rs b/core/engine/src/engine.rs index 34116714..8bdfd155 100644 --- a/core/engine/src/engine.rs +++ b/core/engine/src/engine.rs @@ -97,118 +97,3 @@ impl DecisionEngine { Ok(self.create_decision(content)) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::loader::{FilesystemLoader, FilesystemLoaderOptions, MemoryLoader}; - use crate::model::DecisionContent; - use serde_json::json; - use std::ops::Deref; - use std::path::Path; - - #[test] - fn it_supports_memory_loader() { - let mem_loader = MemoryLoader::default(); - - mem_loader.add( - "table", - serde_json::from_str::(include_str!("../../../test-data/table.json")) - .unwrap(), - ); - - mem_loader.add( - "function", - serde_json::from_str::(include_str!( - "../../../test-data/function.json" - )) - .unwrap(), - ); - - let graph = DecisionEngine::new(mem_loader); - let res1 = tokio_test::block_on(graph.evaluate("table", &json!({ "input": 12 }))); - let res2 = tokio_test::block_on(graph.evaluate("aaa", &json!({ "input": 12 }))); - - assert_eq!(res1.unwrap().result, json!({"output": 10})); - assert!(res2.is_err()); - } - - #[test] - fn it_supports_filesystem_loader() { - let cargo_root = Path::new(env!("CARGO_MANIFEST_DIR")); - let test_data_root = cargo_root.join("../../").join("test-data"); - let fs_loader = FilesystemLoader::new(FilesystemLoaderOptions { - keep_in_memory: true, - root: test_data_root.to_str().unwrap(), - }); - - let graph = DecisionEngine::new(fs_loader); - let res1 = tokio_test::block_on(graph.evaluate("table.json", &json!({ "input": 12 }))); - let res2 = tokio_test::block_on(graph.evaluate("aaa", &json!({ "input": 12 }))); - - assert_eq!(res1.unwrap().result, json!({"output": 10})); - assert!(res2.is_err()); - } - - #[test] - fn it_supports_closure_loader() { - let graph = DecisionEngine::async_loader(|_| async { - let content = serde_json::from_str::(include_str!( - "../../../test-data/table.json" - )) - .unwrap(); - - Ok(Arc::new(content)) - }); - - let res1 = tokio_test::block_on(graph.evaluate("sample", &json!({ "input": 12 }))); - let res2 = tokio_test::block_on(graph.evaluate("1", &json!({ "input": 4 }))); - - assert_eq!(res1.unwrap().result, json!({"output": 10})); - assert_eq!(res2.unwrap().result, json!({"output": 0})) - } - - #[test] - #[cfg_attr(miri, ignore)] - fn it_throws_correct_error_type() { - let cargo_root = Path::new(env!("CARGO_MANIFEST_DIR")); - let test_data_root = cargo_root.join("../../").join("test-data"); - let fs_loader = FilesystemLoader::new(FilesystemLoaderOptions { - keep_in_memory: true, - root: test_data_root.to_str().unwrap(), - }); - - let graph = DecisionEngine::new(fs_loader); - - let infinite_fn = - tokio_test::block_on(graph.evaluate("infinite-function.json", &json!({}))); - - match infinite_fn.unwrap_err().deref() { - EvaluationError::NodeError(e) => { - assert_eq!(e.node_id, "e0fd96d0-44dc-4f0e-b825-06e56b442d78"); - assert_eq!(e.source.to_string(), "Timeout exceeded"); - } - _ => assert!(false, "Wrong error type"), - } - } - - #[test] - fn it_terminates_when_depth_limit_exceeded() { - let cargo_root = Path::new(env!("CARGO_MANIFEST_DIR")); - let test_data_root = cargo_root.join("../../").join("test-data"); - let fs_loader = FilesystemLoader::new(FilesystemLoaderOptions { - keep_in_memory: true, - root: test_data_root.to_str().unwrap(), - }); - - let graph = DecisionEngine::new(fs_loader); - let recursive = tokio_test::block_on(graph.evaluate("recursive-table1.json", &json!({}))); - - match recursive.unwrap_err().deref() { - EvaluationError::NodeError(e) => { - assert_eq!(e.source.to_string(), "Depth limit exceeded") - } - _ => assert!(false, "Depth limit not exceeded"), - } - } -} diff --git a/core/engine/src/handler/graph.rs b/core/engine/src/handler/graph.rs index 99989439..0c589460 100644 --- a/core/engine/src/handler/graph.rs +++ b/core/engine/src/handler/graph.rs @@ -271,8 +271,8 @@ mod tests { use serde_json::json; use std::sync::Arc; - #[test] - fn decision_table() { + #[tokio::test] + async fn decision_table() { let content = &serde_json::from_str(include_str!("../../../../test-data/table.json")).unwrap(); let tree = DecisionGraph::new(DecisionGraphConfig { @@ -283,15 +283,14 @@ mod tests { loader: Arc::new(MemoryLoader::default()), }); - let result = - tokio_test::block_on(async { tree.evaluate(&json!({ "input": 15 })).await.unwrap() }); + let result = tree.evaluate(&json!({ "input": 15 })).await.unwrap(); assert_eq!(result.result, json!({ "output": 10 })); } - #[test] + #[tokio::test] #[cfg_attr(miri, ignore)] - fn function() { + async fn function() { let content = &serde_json::from_str(include_str!("../../../../test-data/function.json")).unwrap(); let tree = DecisionGraph::new(DecisionGraphConfig { @@ -302,8 +301,7 @@ mod tests { loader: Arc::new(MemoryLoader::default()), }); - let result = - tokio_test::block_on(async { tree.evaluate(&json!({ "input": 15 })).await.unwrap() }); + let result = tree.evaluate(&json!({ "input": 15 })).await.unwrap(); assert_eq!(result.result, json!({ "output": 30 })); } diff --git a/core/engine/src/handler/table/mod.rs b/core/engine/src/handler/table/mod.rs index b04334b9..9b60d689 100644 --- a/core/engine/src/handler/table/mod.rs +++ b/core/engine/src/handler/table/mod.rs @@ -202,21 +202,21 @@ mod tests { assert_eq!(rk!("a.b.c").cmp(&rk!("a.b.c", 1)), Ordering::Less); } - #[test] - fn test_insert_order() { + #[tokio::test] + async fn test_insert_order() { let mut o = RowOutput::default(); o.push("a", RowOutputKind::Value(json!("abc"))); o.push("a.b", RowOutputKind::Value(json!("abc"))); o.push("a.b.c", RowOutputKind::Value(json!("abc"))); assert_eq!( - tokio_test::block_on(o.to_json()).unwrap(), + o.to_json().await.unwrap(), json!({ "a": { "b": { "c": "abc" } } }) ); } - #[test] - fn test_nested() { + #[tokio::test] + async fn test_nested() { let mut o = RowOutput::default(); o.push("a.first.deleted", RowOutputKind::Value(json!("deleted"))); o.push( @@ -228,7 +228,7 @@ mod tests { o.push("a.third.nested", RowOutputKind::Value(json!("nested"))); assert_eq!( - tokio_test::block_on(o.to_json()).unwrap(), + o.to_json().await.unwrap(), json!({ "a": { "first": "first", diff --git a/core/engine/tests/decision.rs b/core/engine/tests/decision.rs new file mode 100644 index 00000000..83ce11a0 --- /dev/null +++ b/core/engine/tests/decision.rs @@ -0,0 +1,45 @@ +use crate::support::{create_fs_loader, load_test_data}; +use serde_json::json; +use std::ops::Deref; +use std::sync::Arc; +use zen_engine::{Decision, EvaluationError}; + +mod support; + +#[tokio::test] +#[cfg_attr(miri, ignore)] +async fn decision_from_content() { + let table_content = load_test_data("table.json"); + let decision = Decision::from(table_content); + + let context = json!({ "input": 5 }); + let result = decision.evaluate(&context).await; + + assert_eq!(result.unwrap().result, json!({"output": 0})); +} + +#[tokio::test] +#[cfg_attr(miri, ignore)] +async fn decision_from_content_recursive() { + let recursive_content = load_test_data("recursive-table1.json"); + let decision = Decision::from(recursive_content); + + let context = json!({}); + let result = decision.evaluate(&context).await; + match result.unwrap_err().deref() { + EvaluationError::NodeError(e) => { + assert_eq!(e.node_id, "0b8dcf6b-fc04-47cb-bf82-bda764e6c09b"); + assert!(e.source.to_string().contains("Loader failed")); + } + _ => assert!(false, "Depth limit not exceeded"), + } + + let with_loader = decision.with_loader(Arc::new(create_fs_loader())); + let new_result = with_loader.evaluate(&context).await; + match new_result.unwrap_err().deref() { + EvaluationError::NodeError(e) => { + assert_eq!(e.source.to_string(), "Depth limit exceeded") + } + _ => assert!(false, "Depth limit not exceeded"), + } +} diff --git a/core/engine/tests/engine.rs b/core/engine/tests/engine.rs new file mode 100644 index 00000000..0f57d0d2 --- /dev/null +++ b/core/engine/tests/engine.rs @@ -0,0 +1,140 @@ +use crate::support::{create_fs_loader, load_test_data}; +use serde_json::json; +use std::ops::Deref; +use std::sync::Arc; + +use zen_engine::loader::{LoaderError, MemoryLoader}; +use zen_engine::{DecisionEngine, EvaluationError, EvaluationOptions}; + +mod support; + +#[tokio::test] +#[cfg_attr(miri, ignore)] +async fn engine_memory_loader() { + let memory_loader = Arc::new(MemoryLoader::default()); + memory_loader.add("table", load_test_data("table.json")); + memory_loader.add("function", load_test_data("function.json")); + + let engine = DecisionEngine::new_arc(memory_loader.clone()); + let table = engine.evaluate("table", &json!({ "input": 12 })).await; + let function = engine.evaluate("function", &json!({ "input": 12 })).await; + + memory_loader.remove("function"); + let not_found = engine.evaluate("function", &json!({})).await; + + assert_eq!(table.unwrap().result, json!({"output": 10})); + assert_eq!(function.unwrap().result, json!({"output": 24})); + assert_eq!(not_found.unwrap_err().to_string(), "Loader error"); +} + +#[tokio::test] +#[cfg_attr(miri, ignore)] +async fn engine_filesystem_loader() { + let engine = DecisionEngine::new(create_fs_loader()); + let table = engine.evaluate("table.json", &json!({ "input": 12 })).await; + let function = engine + .evaluate("function.json", &json!({ "input": 12 })) + .await; + let not_found = engine.evaluate("invalid_file", &json!({})).await; + + assert_eq!(table.unwrap().result, json!({"output": 10})); + assert_eq!(function.unwrap().result, json!({"output": 24})); + assert_eq!(not_found.unwrap_err().to_string(), "Loader error"); +} + +#[tokio::test] +#[cfg_attr(miri, ignore)] +async fn engine_closure_loader() { + let engine = DecisionEngine::async_loader(|key| { + // TODO: Improve once async closures become stable in Rust + let mv_key = key.to_string(); + + async move { + match mv_key.as_str() { + "function" => Ok(Arc::new(load_test_data("function.json"))), + "table" => Ok(Arc::new(load_test_data("table.json"))), + _ => Err(LoaderError::NotFound(mv_key).into()), + } + } + }); + + let table = engine.evaluate("table", &json!({ "input": 12 })).await; + let function = engine.evaluate("function", &json!({ "input": 12 })).await; + let not_found = engine.evaluate("invalid_file", &json!({})).await; + + assert_eq!(table.unwrap().result, json!({"output": 10})); + assert_eq!(function.unwrap().result, json!({"output": 24})); + assert_eq!(not_found.unwrap_err().to_string(), "Loader error"); +} + +#[tokio::test] +async fn engine_noop_loader() { + // Default engine is noop + let engine = DecisionEngine::default(); + let result = engine.evaluate("any.json", &json!({})).await; + + assert_eq!(result.unwrap_err().to_string(), "Loader error"); +} + +#[tokio::test] +async fn engine_get_decision() { + let engine = DecisionEngine::new(create_fs_loader()); + + assert!(engine.get_decision("table.json").await.is_ok()); + assert!(engine.get_decision("any.json").await.is_err()); +} + +#[tokio::test] +async fn engine_create_decision() { + let engine = DecisionEngine::default(); + engine.create_decision(load_test_data("table.json").into()); +} + +#[tokio::test] +#[cfg_attr(miri, ignore)] +async fn engine_errors() { + let engine = DecisionEngine::new(create_fs_loader()); + + let infinite_fn = engine.evaluate("infinite-function.json", &json!({})).await; + match infinite_fn.unwrap_err().deref() { + EvaluationError::NodeError(e) => { + assert_eq!(e.node_id, "e0fd96d0-44dc-4f0e-b825-06e56b442d78"); + assert_eq!(e.source.to_string(), "Timeout exceeded"); + } + _ => assert!(false, "Wrong error type"), + } + + let recursive = engine.evaluate("recursive-table1.json", &json!({})).await; + match recursive.unwrap_err().deref() { + EvaluationError::NodeError(e) => { + assert_eq!(e.source.to_string(), "Depth limit exceeded") + } + _ => assert!(false, "Depth limit not exceeded"), + } +} + +#[tokio::test] +async fn engine_with_trace() { + let engine = DecisionEngine::new(create_fs_loader()); + + let table_r = engine.evaluate("table.json", &json!({ "input": 12 })).await; + let table_opt_r = engine + .evaluate_with_opts( + "table.json", + &json!({ "input": 12 }), + EvaluationOptions { + trace: Some(true), + max_depth: None, + }, + ) + .await; + + let table = table_r.unwrap(); + let table_opt = table_opt_r.unwrap(); + + assert!(table.trace.is_none()); + assert!(table_opt.trace.is_some()); + + let trace = table_opt.trace.unwrap(); + assert_eq!(trace.len(), 3); // trace for each node +} diff --git a/core/engine/tests/model.rs b/core/engine/tests/model.rs new file mode 100644 index 00000000..ef722f2f --- /dev/null +++ b/core/engine/tests/model.rs @@ -0,0 +1,25 @@ +mod support; + +#[cfg(feature = "bincode")] +mod bincode_tests { + use crate::support::load_test_data; + use bincode::config; + use zen_engine::model::DecisionContent; + + #[test] + fn jdm_bincode() { + let decision_content = load_test_data("table.json"); + let cache_slice_r = bincode::encode_to_vec(&decision_content, config::standard()); + + assert!(cache_slice_r.is_ok(), "Bincode serialisation failed"); + + let cache_slice = cache_slice_r.unwrap(); + let decode_res = + bincode::decode_from_slice::(&cache_slice, config::standard()); + + assert!(decode_res.is_ok(), "Bincode deserialization failed"); + + let decoded_decision_content = decode_res.unwrap(); + assert_eq!(decoded_decision_content.0, decision_content); + } +} diff --git a/core/engine/tests/support/mod.rs b/core/engine/tests/support/mod.rs new file mode 100644 index 00000000..f7f816bf --- /dev/null +++ b/core/engine/tests/support/mod.rs @@ -0,0 +1,33 @@ +use std::fs::File; +use std::io::BufReader; +use std::path::Path; +use zen_engine::loader::{FilesystemLoader, FilesystemLoaderOptions}; +use zen_engine::model::DecisionContent; + +#[allow(dead_code)] +pub fn test_data_root() -> String { + let cargo_root = Path::new(env!("CARGO_MANIFEST_DIR")); + cargo_root + .parent() + .unwrap() + .parent() + .unwrap() + .join("test-data") + .to_string_lossy() + .to_string() +} + +#[allow(dead_code)] +pub fn load_test_data(key: &str) -> DecisionContent { + let file = File::open(Path::new(&test_data_root()).join(key)).unwrap(); + let reader = BufReader::new(file); + serde_json::from_reader(reader).unwrap() +} + +#[allow(dead_code)] +pub fn create_fs_loader() -> FilesystemLoader { + FilesystemLoader::new(FilesystemLoaderOptions { + keep_in_memory: false, + root: test_data_root(), + }) +}