diff --git a/backend/src/runtime/actors/actor.rs b/backend/src/runtime/actors/actor.rs index cc194a0..3893025 100644 --- a/backend/src/runtime/actors/actor.rs +++ b/backend/src/runtime/actors/actor.rs @@ -155,3 +155,151 @@ impl<'a, X: SeqAct<'a, BuildParam=()>> Act<'a> for SeqActor<'a, X> { self.seq_act.run(self.param) } } + +#[cfg(test)] +pub struct SeqActTestHarness<'a, ParamT, ConfigT, ActorT, FnT> +where + ConfigT: 'a, + ActorT: SeqAct<'a, BuildParam=ParamT, Config=ConfigT>, + FnT: Fn(&'a ConfigT, ParamT) -> Result, +{ + actor_factory: FnT, + inputs: Vec<(&'a ConfigT, ParamT, Primitive)>, + outputs: std::collections::VecDeque, +} + +pub enum Expected { + Output(Primitive), + BuildErr(ActError), + OutputIdc, +} + +#[cfg(test)] +impl<'a, ParamT, ConfigT, ActorT, FnT> SeqActTestHarness<'a, ParamT, ConfigT, ActorT, FnT> +where + ConfigT: 'a, + ActorT: SeqAct<'a, BuildParam=ParamT, Config=ConfigT>, + FnT: Fn(&'a ConfigT, ParamT) -> Result, +{ + #[allow(dead_code)] + pub fn new(factory: FnT, inputs_vars: Vec<(&'a ConfigT, ParamT, Primitive)>, output_vars: std::collections::VecDeque) -> Self { + Self { + actor_factory: factory, + inputs: inputs_vars, + outputs: output_vars, + } + } + + pub fn builder(factory: FnT) -> Self { + Self { + actor_factory: factory, + inputs: Vec::new(), + outputs: std::collections::VecDeque::new(), + } + } + + #[allow(dead_code)] + pub fn with_input(mut self, conf: &'a ConfigT, build_param: ParamT, run_param: Primitive) -> Self { + self.inputs.push((conf, build_param, run_param)); + self + } + + #[allow(dead_code)] + pub fn expect(mut self, expected: Expected) -> Self { + self.outputs.push_back(expected); + self + } + + pub fn with_io(mut self, input: (&'a ConfigT, ParamT, Primitive), output: Expected) -> Self { + self.inputs.push(input); + self.outputs.push_back(output); + self + } + + pub fn run(mut self) { + for input in self.inputs { + let expected = self.outputs.pop_front().expect("Not enough outputs for available inputs"); + let actor_result = (self.actor_factory)(input.0, input.1); + match expected { + Expected::Output(primitive) => { + let debug_prim = crate::runtime::primitive_utils::debug(&input.2); + let actual = actor_result.expect("Expected Ok actor").run(input.2); + let debug_actual = crate::runtime::primitive_utils::debug(&actual); + match primitive { + Primitive::Empty => if let Primitive::Empty = actual { + // good! + } else { + panic!("Expected SeqAct.run({}) to output Empty, got `{}`", debug_prim, debug_actual); + }, + Primitive::String(s) => if let Primitive::String(actual) = actual { + assert_eq!(actual, s, "Expected SeqAct.run({}) to output Primitive::String(`{}`)", debug_prim, s); + } else { + panic!("Expected SeqAct.run({}) to output Primitive::String(`{}`), got `{}`", debug_prim, s, debug_actual); + }, + Primitive::Json(j) => if let Primitive::Json(actual) = actual { + assert_eq!(actual, j, "Expected SeqAct.run({}) to output Primitive::Json(`{}`)", debug_prim, j); + } else { + panic!("Expected SeqAct.run({}) to output Primitive::Json(`{}`), got `{}`", debug_prim, j, debug_actual); + }, + _ => todo!("NGNIUS!!! Complete your damn test harness!!"), + } + }, + Expected::BuildErr(err) => { + if let Err(actual) = actor_result { + assert_eq!(actual, err, "Expected and actual build error do not match"); + } else { + panic!("Expected build error `{}`, but result was ok", err); + } + }, + Expected::OutputIdc => { + actor_result.expect("Expected Ok actor").run(input.2); + }, + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::config::*; + + #[test] + fn full_actor_test() { + let (runtime_io, _result_rx, _js_rx) = crate::runtime::RuntimeIO::mock(); + SeqActTestHarness::builder(Actor::build) + .with_io( + (&ElementConfig::Button(ButtonConfig { + title: "Test Button".into(), + on_click: TopLevelActionConfig::Mirror(MirrorAction) + }), + (0, &runtime_io), + Primitive::Empty), + + Expected::Output(Primitive::Empty)) + .run(); + } + + #[test] + fn top_level_actor_test() { + let (runtime_io, _result_rx, _js_rx) = crate::runtime::RuntimeIO::mock(); + SeqActTestHarness::builder(TopLevelActorType::build) + .with_io( + (&TopLevelActionConfig::Mirror(MirrorAction), &runtime_io, Primitive::Empty), Expected::Output(Primitive::Empty)) + .run(); + } + + #[test] + fn std_actor_test() { + let (runtime_io, _result_rx, _js_rx) = crate::runtime::RuntimeIO::mock(); + SeqActTestHarness::builder(ActorType::build) + .with_io( + (&ActionConfig::Transform(TransformAction{ + transformer: TransformTypeAction::Log(LogTransformAction{level: LogLevel::DEBUG})}), + &runtime_io, + Primitive::String("Test log output".into())), + + Expected::OutputIdc) + .run(); + } +} diff --git a/backend/src/runtime/actors/json_actor.rs b/backend/src/runtime/actors/json_actor.rs index ce8d628..d97f727 100644 --- a/backend/src/runtime/actors/json_actor.rs +++ b/backend/src/runtime/actors/json_actor.rs @@ -75,3 +75,67 @@ impl<'a> SeqAct<'a> for JsonActor { } } } + +#[cfg(test)] +mod test { + use crate::runtime::actors::*; + use crate::config::*; + use usdpl_back::core::serdes::Primitive; + + #[test] + fn json_actor_test() { + //let (runtime_io, _result_rx, _js_rx) = crate::runtime::RuntimeIO::mock(); + SeqActTestHarness::builder(JsonActor::build) + // test 1 --- + .with_io( + (&JsonAction { + jmespath: r"locations[?state == 'WA'].name | sort(@) | {WashingtonCities: join(', ', @)}".into(), + }, + (), + Primitive::Json( +r#"{ + "locations": [ + {"name": "Seattle", "state": "WA"}, + {"name": "New York", "state": "NY"}, + {"name": "Bellevue", "state": "WA"}, + {"name": "Olympia", "state": "WA"} + ] +}"#.into() + )), + + Expected::Output(Primitive::Json( +r#"{"WashingtonCities":"Bellevue, Olympia, Seattle"}"#.into() + ))) + + // test 2 --- + .with_io( + (&JsonAction { + jmespath: r"locations[?state == 'WA'].name | sort(@) | {WashingtonCities: join(', ', @)}".into(), + }, + (), + Primitive::Bool(false) + ), + + Expected::Output(Primitive::Empty)) + + // test 3 --- + .with_io( + (&JsonAction { + jmespath: "garb@ge".into(), + }, + (), + Primitive::Json( +r#"{ + "locations": [ + {"name": "Seattle", "state": "WA"}, + {"name": "New York", "state": "NY"}, + {"name": "Bellevue", "state": "WA"}, + {"name": "Olympia", "state": "WA"} + ] +}"#.into() + )), + + Expected::BuildErr("Failed to compile jmespath `garb@ge`: Parse error: Did not parse the complete expression -- found At (line 0, column 4)\ngarb@ge\n ^\n".into())) + .run(); + } +} diff --git a/backend/src/runtime/actors/mod.rs b/backend/src/runtime/actors/mod.rs index 58b3b3e..12960e6 100644 --- a/backend/src/runtime/actors/mod.rs +++ b/backend/src/runtime/actors/mod.rs @@ -7,6 +7,8 @@ mod sequential_actor; mod transform_actor; pub use actor::{Actor, Act, ActError, ActorType, SeqAct, SeqActor, TopLevelActorType}; +#[cfg(test)] +pub use actor::{Expected, SeqActTestHarness, /*ActTestHarness*/}; pub use command_actor::CommandActor; pub use javascript_actor::JavascriptActor; pub use json_actor::JsonActor; diff --git a/backend/src/runtime/executor.rs b/backend/src/runtime/executor.rs index 30e1733..7289bf2 100644 --- a/backend/src/runtime/executor.rs +++ b/backend/src/runtime/executor.rs @@ -12,6 +12,22 @@ pub struct RuntimeIO { pub javascript: Sender, } +#[cfg(test)] +impl RuntimeIO { + pub fn mock() -> (Self, Receiver, Receiver) { + let (s1, r1) = mpsc::channel(); + let (s2, r2) = mpsc::channel(); + ( + Self { + result: s1, + javascript: s2, + }, + r1, + r2, + ) + } +} + pub struct RuntimeExecutor { config_data: BaseConfig, tasks_receiver: Receiver,