diff --git a/Cargo.lock b/Cargo.lock index 71b110c..283b285 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -278,6 +278,46 @@ dependencies = [ "num-traits", ] +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "code0-flow" version = "0.0.29" @@ -987,6 +1027,24 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "manual" +version = "0.1.0" +dependencies = [ + "async-nats 0.46.0", + "clap", + "env_logger", + "log", + "prost", + "serde", + "serde_json", + "taurus-core", + "tests-core", + "tokio", + "tonic", + "tucana", +] + [[package]] name = "matchit" version = "0.8.4" @@ -1742,6 +1800,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -1823,6 +1887,19 @@ dependencies = [ [[package]] name = "tests" version = "0.1.0" +dependencies = [ + "env_logger", + "log", + "serde", + "serde_json", + "taurus-core", + "tests-core", + "tucana", +] + +[[package]] +name = "tests-core" +version = "0.1.0" dependencies = [ "env_logger", "log", diff --git a/Cargo.toml b/Cargo.toml index 1104924..2374da2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [ "crates/core", "crates/taurus", "crates/tests" ] +members = [ "crates/core", "crates/manual", "crates/taurus", "crates/tests" , "crates/tests-core"] resolver = "3" [workspace.package] @@ -25,3 +25,6 @@ serde = "1.0.228" [workspace.dependencies.taurus-core] path = "./crates/core" + +[workspace.dependencies.tests-core] +path = "./crates/tests-core" diff --git a/crates/core/src/context/executor.rs b/crates/core/src/context/executor.rs index 478f1c7..48c7ad1 100644 --- a/crates/core/src/context/executor.rs +++ b/crates/core/src/context/executor.rs @@ -32,8 +32,7 @@ use crate::runtime::remote::RemoteRuntime; use futures_lite::future::block_on; use std::collections::HashMap; -use tucana::aquila::execution_result::Result as ExecutionOutcome; -use tucana::aquila::{ActionRuntimeError, ExecutionRequest, ExecutionResult}; +use tucana::aquila::ExecutionRequest; use tucana::shared::reference_value::Target; use tucana::shared::value::Kind; use tucana::shared::{NodeFunction, Struct, Value}; @@ -56,6 +55,14 @@ pub struct Executor<'a> { /// The current policy treats any node whose `definition_source` is not `"taurus"` /// as a remote node. fn is_remote(node: &NodeFunction) -> bool { + if node.definition_source == "" { + log::warn!( + "Found empty definition source, taking runtime as origin for node id: {}", + node.database_id + ); + return false; + } + node.definition_source != "taurus" } diff --git a/crates/manual/Cargo.toml b/crates/manual/Cargo.toml new file mode 100644 index 0000000..3682f73 --- /dev/null +++ b/crates/manual/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "manual" +version.workspace = true +edition.workspace = true + +[dependencies] +tests-core = { workspace = true } +tucana = { workspace = true } +taurus-core = { workspace = true } +log = { workspace = true } +env_logger = { workspace = true } +serde_json = { workspace = true } +serde = { workspace = true } +prost = { workspace = true } +tonic = { workspace = true } +tokio = { workspace = true } +async-nats = { workspace = true } +clap ={ version = "4.6.0", features= ["derive"] } diff --git a/crates/manual/src/main.rs b/crates/manual/src/main.rs new file mode 100644 index 0000000..ba0559e --- /dev/null +++ b/crates/manual/src/main.rs @@ -0,0 +1,160 @@ +use std::collections::HashMap; + +use async_nats::Client; +use clap::{Parser, arg, command}; +use prost::Message; +use taurus_core::context::{context::Context, executor::Executor, registry::FunctionStore}; +use taurus_core::runtime::{error::RuntimeError, remote::RemoteRuntime}; +use tests_core::Case; +use tonic::async_trait; +use tucana::shared::helper::value::to_json_value; +use tucana::shared::{NodeFunction, helper::value::from_json_value}; +use tucana::{ + aquila::{ExecutionRequest, ExecutionResult}, + shared::Value, +}; + +pub struct RemoteNatsClient { + client: Client, +} + +impl RemoteNatsClient { + pub fn new(client: Client) -> Self { + RemoteNatsClient { client } + } +} + +#[async_trait] +impl RemoteRuntime for RemoteNatsClient { + async fn execute_remote( + &self, + remote_name: String, + request: ExecutionRequest, + ) -> Result { + let topic = format!("action.{}.{}", remote_name, request.execution_identifier); + let payload = request.encode_to_vec(); + let res = self.client.request(topic, payload.into()).await; + let message = match res { + Ok(r) => r, + Err(_) => { + return Err(RuntimeError::simple_str( + "RemoteRuntimeExeption", + "Failed to handle NATS message", + )); + } + }; + + let decode_result = ExecutionResult::decode(message.payload); + let execution_result = match decode_result { + Ok(r) => r, + Err(_) => { + return Err(RuntimeError::simple_str( + "RemoteRuntimeExeption", + "Failed to decode NATS message", + )); + } + }; + + match execution_result.result { + Some(result) => match result { + tucana::aquila::execution_result::Result::Success(value) => Ok(value), + tucana::aquila::execution_result::Result::Error(err) => { + let name = err.code.to_string(); + let description = match err.description { + Some(string) => string, + None => "Unknown Error".to_string(), + }; + let error = RuntimeError::new(name, description, None); + Err(error) + } + }, + None => Err(RuntimeError::simple_str( + "RemoteRuntimeExeption", + "Result of Remote Response was empty.", + )), + } + } +} + +#[derive(clap::Parser, Debug)] +#[command(author, version, about)] +struct Args { + /// Index value + #[arg(short, long, default_value_t = 0)] + index: i32, + + /// NATS server URL + #[arg(short, long, default_value_t = String::from("nats://127.0.0.1:4222"))] + nats_url: String, + + /// Path value + #[arg(short, long)] + path: String, +} + +#[tokio::main] +async fn main() { + env_logger::Builder::from_default_env() + .filter_level(log::LevelFilter::Info) + .init(); + + let args = Args::parse(); + let index = args.index; + let nats_url = args.nats_url; + let path = args.path; + let case = Case::from_path(&path); + + let store = FunctionStore::default(); + + let node_functions: HashMap = case + .clone() + .flow + .node_functions + .into_iter() + .map(|node| (node.database_id, node)) + .collect(); + + let mut context = match case.inputs.get(index as usize) { + Some(inp) => match inp.input.clone() { + Some(json_input) => Context::new(from_json_value(json_input)), + None => Context::default(), + }, + None => Context::default(), + }; + + let client = match async_nats::connect(nats_url).await { + Ok(client) => { + log::info!("Connected to nats server"); + client + } + Err(err) => { + panic!("Failed to connect to NATS server: {}", err); + } + }; + let remote = RemoteNatsClient::new(client); + let result = Executor::new(&store, node_functions.clone()) + .with_remote_runtime(&remote) + .execute(case.flow.starting_node_id, &mut context, true); + + match result { + taurus_core::context::signal::Signal::Success(value) => { + let json = to_json_value(value); + let pretty = serde_json::to_string_pretty(&json).unwrap(); + println!("{}", pretty); + } + taurus_core::context::signal::Signal::Return(value) => { + let json = to_json_value(value); + let pretty = serde_json::to_string_pretty(&json).unwrap(); + println!("{}", pretty); + } + taurus_core::context::signal::Signal::Respond(value) => { + let json = to_json_value(value); + let pretty = serde_json::to_string_pretty(&json).unwrap(); + println!("{}", pretty); + } + taurus_core::context::signal::Signal::Stop => println!("Received Stop signal"), + taurus_core::context::signal::Signal::Failure(runtime_error) => { + println!("RuntimeError: {:?}", runtime_error); + } + } +} diff --git a/crates/tests-core/Cargo.toml b/crates/tests-core/Cargo.toml new file mode 100644 index 0000000..cc6b9ff --- /dev/null +++ b/crates/tests-core/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "tests-core" +version.workspace = true +edition.workspace = true + +[dependencies] +tucana = { workspace = true } +taurus-core = { workspace = true } +log = { workspace = true } +env_logger = { workspace = true } +serde_json = { workspace = true } +serde = { workspace = true } + diff --git a/crates/tests-core/src/lib.rs b/crates/tests-core/src/lib.rs new file mode 100644 index 0000000..b50afb7 --- /dev/null +++ b/crates/tests-core/src/lib.rs @@ -0,0 +1,107 @@ +use std::path::Path; + +use log::{error, info}; +use serde::Deserialize; +use tucana::shared::ValidationFlow; + +#[derive(Clone, Deserialize)] +pub struct Input { + pub input: Option, + pub expected_result: serde_json::Value, +} + +#[derive(Clone, Deserialize)] +pub struct Case { + pub name: String, + pub description: String, + pub inputs: Vec, + pub flow: ValidationFlow, +} + +pub enum CaseResult { + Success, + Failure(Input, serde_json::Value), +} + +pub trait Testable { + fn run(&self) -> CaseResult; +} + +#[derive(Clone, Deserialize)] +pub struct Cases { + pub cases: Vec, +} + +pub fn print_success(case: &Case) { + info!("test {} ... ok", case.name); +} + +pub fn print_failure(case: &Case, input: &Input, result: serde_json::Value) { + error!("test {} ... FAILED", case.name); + error!(" input: {:?}", input.input); + error!(" expected: {:?}", input.expected_result); + error!(" real_value: {:?}", result); + error!(" message: {}", case.description); +} + +fn get_test_case + std::fmt::Debug>(path: P) -> Option { + let content = match std::fs::read_to_string(&path) { + Ok(it) => it, + Err(err) => { + log::error!("Cannot read file ({:?}): {:?}", path, err); + return None; + } + }; + + match serde_json::from_str(&content) { + Ok(it) => it, + Err(err) => { + log::error!("Cannot read json ({:?}): {:?}", path, err); + return None; + } + } +} + +fn get_test_cases(path: &str) -> Cases { + let mut items = Vec::new(); + let dir = match std::fs::read_dir(path) { + Ok(d) => d, + Err(err) => { + panic!("Cannot open path: {:?}", err) + } + }; + + for entry in dir { + let entry = match entry { + Ok(it) => it, + Err(err) => { + log::error!("Cannot read entry: {:?}", err); + continue; + } + }; + let file_path = entry.path(); + items.push(match get_test_case(&file_path) { + Some(it) => it, + None => { + continue; + } + }); + } + + Cases { cases: items } +} + +impl Case { + pub fn from_path(path: &str) -> Self { + match get_test_case(path) { + Some(s) => s, + None => panic!("flow was not found"), + } + } +} + +impl Cases { + pub fn from_path(path: &str) -> Self { + get_test_cases(path) + } +} diff --git a/crates/tests/Cargo.toml b/crates/tests/Cargo.toml index 0e4f08a..8ac7443 100644 --- a/crates/tests/Cargo.toml +++ b/crates/tests/Cargo.toml @@ -9,4 +9,5 @@ taurus-core = { workspace = true } log = { workspace = true } env_logger = { workspace = true } serde_json = { workspace = true } +tests-core = { workspace = true } serde = { workspace = true } diff --git a/crates/tests/src/main.rs b/crates/tests/src/main.rs index 5eb1e37..c46c474 100644 --- a/crates/tests/src/main.rs +++ b/crates/tests/src/main.rs @@ -3,102 +3,27 @@ use serde::Deserialize; use serde_json::json; use std::collections::HashMap; use taurus_core::context::{context::Context, executor::Executor, registry::FunctionStore}; +use tests_core::{Case, CaseResult, Cases, print_failure, print_success}; use tucana::shared::{ - NodeFunction, ValidationFlow, + NodeFunction, helper::value::{from_json_value, to_json_value}, }; -#[derive(Clone, Deserialize)] -struct Input { - input: Option, - expected_result: serde_json::Value, +pub trait Testable { + fn run(&self) -> CaseResult; } -#[derive(Clone, Deserialize)] -struct Case { - name: String, - description: String, - inputs: Vec, - flow: ValidationFlow, -} - -#[derive(Clone, Deserialize)] -struct TestCases { - cases: Vec, -} - -fn print_success(case: &Case) { - info!("test {} ... ok", case.name); -} - -fn print_failure(case: &Case, input: &Input, result: serde_json::Value) { - error!("test {} ... FAILED", case.name); - error!(" input: {:?}", input.input); - error!(" expected: {:?}", input.expected_result); - error!(" real_value: {:?}", result); - error!(" message: {}", case.description); -} - -fn get_test_cases(path: &str) -> TestCases { - let mut items = Vec::new(); - let dir = match std::fs::read_dir(path) { - Ok(d) => d, - Err(err) => { - panic!("Cannot open path: {:?}", err) +fn run_tests(cases: Cases) { + for case in &cases.cases { + match case.run() { + CaseResult::Success => print_success(case), + CaseResult::Failure(input, result) => print_failure(case, &input, result), } - }; - - for entry in dir { - let entry = match entry { - Ok(it) => it, - Err(err) => { - log::error!("Cannot read entry: {:?}", err); - continue; - } - }; - let path = entry.path(); - - let content = match std::fs::read_to_string(&path) { - Ok(it) => it, - Err(err) => { - log::error!("Cannot read file ({:?}): {:?}", path, err); - continue; - } - }; - items.push(match serde_json::from_str(&content) { - Ok(it) => it, - Err(err) => { - log::error!("Cannot read json ({:?}): {:?}", path, err); - continue; - } - }); - } - - TestCases { cases: items } -} - -impl TestCases { - pub fn from_path(path: &str) -> Self { - get_test_cases(path) } - - pub fn run_tests(&self) { - for case in self.cases.clone() { - match case.run() { - CaseResult::Success => print_success(&case), - CaseResult::Failure(input, result) => print_failure(&case, &input, result), - } - } - } -} - -enum CaseResult { - Success, - Failure(Input, serde_json::Value), } -impl Case { +impl Testable for Case { fn run(&self) -> CaseResult { let store = FunctionStore::default(); @@ -167,6 +92,6 @@ fn main() { .filter_level(log::LevelFilter::Info) .init(); - let cases = TestCases::from_path("./crates/tests/flows/"); - cases.run_tests(); + let cases = Cases::from_path("./flows/"); + run_tests(cases); } diff --git a/crates/tests/flows/01_return_object.json b/flows/01_return_object.json similarity index 95% rename from crates/tests/flows/01_return_object.json rename to flows/01_return_object.json index 34c28c8..cd5b3f3 100644 --- a/crates/tests/flows/01_return_object.json +++ b/flows/01_return_object.json @@ -17,6 +17,7 @@ "starting_node_id": "1", "node_functions": [ { + "definition_source": "taurus", "databaseId": "2", "runtimeFunctionId": "rest::control::respond", "parameters": [ @@ -33,6 +34,7 @@ }, { "databaseId": "1", + "definition_source": "taurus", "runtimeFunctionId": "http::response::create", "parameters": [ { diff --git a/crates/tests/flows/02_return_flow_input.json b/flows/02_return_flow_input.json similarity index 97% rename from crates/tests/flows/02_return_flow_input.json rename to flows/02_return_flow_input.json index ef8402d..9a44b8c 100644 --- a/crates/tests/flows/02_return_flow_input.json +++ b/flows/02_return_flow_input.json @@ -45,6 +45,7 @@ "startingNodeId": "3", "nodeFunctions": [ { + "definition_source": "taurus", "databaseId": "3", "runtimeFunctionId": "http::response::create", "parameters": [ diff --git a/crates/tests/flows/03_for_each.json b/flows/03_for_each.json similarity index 96% rename from crates/tests/flows/03_for_each.json rename to flows/03_for_each.json index 231d005..383688f 100644 --- a/crates/tests/flows/03_for_each.json +++ b/flows/03_for_each.json @@ -11,6 +11,7 @@ "startingNodeId": "5", "nodeFunctions": [ { + "definition_source": "taurus", "databaseId": "5", "runtimeFunctionId": "std::list::for_each", "parameters": [ @@ -67,6 +68,7 @@ }, { "databaseId": "6", + "definition_source": "taurus", "runtimeFunctionId": "std::number::add", "parameters": [ {