Skip to content

Commit 58b945d

Browse files
committed
feat: added execution test
1 parent 78ea0ce commit 58b945d

1 file changed

Lines changed: 193 additions & 0 deletions

File tree

crates/taurus/src/app/worker.rs

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,3 +342,196 @@ fn now_unix_ms() -> i64 {
342342
.unwrap_or(0)
343343
}
344344

345+
#[cfg(test)]
346+
mod tests {
347+
use super::*;
348+
use std::sync::Mutex;
349+
use std::time::Duration;
350+
351+
use prost::Message;
352+
use serde::Deserialize;
353+
use taurus_core::runtime::engine::ExecutionEngine;
354+
use taurus_provider::providers::emitter::nats_emitter::NATSRespondEmitter;
355+
use taurus_provider::providers::remote::nats_remote_runtime::NATSRemoteRuntime;
356+
use tucana::shared::{
357+
ValidationFlow, execution_result,
358+
helper::value::{from_json_value, to_json_value},
359+
};
360+
361+
#[derive(Deserialize)]
362+
struct FixtureInput {
363+
input: Option<serde_json::Value>,
364+
expected_result: serde_json::Value,
365+
}
366+
367+
#[derive(Deserialize)]
368+
struct FlowFixture {
369+
inputs: Vec<FixtureInput>,
370+
flow: ValidationFlow,
371+
}
372+
373+
static NATS_TEST_LOCK: Mutex<()> = Mutex::new(());
374+
375+
#[test]
376+
#[ignore = "requires a running NATS server at NATS_URL or nats://127.0.0.1:4222"]
377+
fn test_execution_request_returns_execution_result_over_nats() {
378+
let _lock = NATS_TEST_LOCK
379+
.lock()
380+
.expect("NATS test lock should not be poisoned");
381+
runtime().block_on(async {
382+
let client = connect_test_nats().await;
383+
let worker = spawn_test_worker(client.clone());
384+
wait_for_worker_subscription().await;
385+
386+
let execution_id = ExecutionId::new_v4();
387+
let fixture = load_fixture("flows/01_return_object.json");
388+
let expected_result = fixture.inputs[0].expected_result.clone();
389+
let flow = execution_flow_from_fixture(fixture);
390+
let response = request_execution_result(&client, execution_id, flow.encode_to_vec())
391+
.await
392+
.expect("test execution request should receive an ExecutionResult response");
393+
394+
worker.abort();
395+
396+
assert_eq!(response.execution_identifier, execution_id.to_string());
397+
assert_eq!(response.flow_id, flow.flow_id);
398+
assert!(response.started_at > 0);
399+
assert!(response.finished_at >= response.started_at);
400+
assert_eq!(response.input, None);
401+
assert!(response.node_execution_results.is_empty());
402+
403+
match response.result {
404+
Some(execution_result::Result::Success(value)) => {
405+
assert_eq!(to_json_value(value), expected_result);
406+
}
407+
other => panic!("expected successful test execution result, got {:?}", other),
408+
}
409+
});
410+
}
411+
412+
#[test]
413+
#[ignore = "requires a running NATS server at NATS_URL or nats://127.0.0.1:4222"]
414+
fn test_execution_request_returns_decode_error_over_nats() {
415+
let _lock = NATS_TEST_LOCK
416+
.lock()
417+
.expect("NATS test lock should not be poisoned");
418+
runtime().block_on(async {
419+
let client = connect_test_nats().await;
420+
let worker = spawn_test_worker(client.clone());
421+
wait_for_worker_subscription().await;
422+
423+
let execution_id = ExecutionId::new_v4();
424+
let response =
425+
request_execution_result(&client, execution_id, b"not protobuf".to_vec())
426+
.await
427+
.expect("malformed test execution request should receive an error response");
428+
429+
worker.abort();
430+
431+
assert_eq!(response.execution_identifier, execution_id.to_string());
432+
assert_eq!(response.flow_id, 0);
433+
assert!(response.started_at > 0);
434+
assert!(response.finished_at >= response.started_at);
435+
assert_eq!(response.input, None);
436+
assert!(response.node_execution_results.is_empty());
437+
438+
match response.result {
439+
Some(execution_result::Result::Error(error)) => {
440+
assert_eq!(error.code, "T-TAURUS-000001");
441+
assert_eq!(error.category, "ExecutionFlowDecodeError");
442+
assert_eq!(
443+
error.message,
444+
"Failed to decode test execution flow payload"
445+
);
446+
}
447+
other => panic!("expected decode error result, got {:?}", other),
448+
}
449+
});
450+
}
451+
452+
fn runtime() -> tokio::runtime::Runtime {
453+
tokio::runtime::Builder::new_multi_thread()
454+
.enable_all()
455+
.build()
456+
.expect("test runtime should build")
457+
}
458+
459+
async fn connect_test_nats() -> async_nats::Client {
460+
let nats_url =
461+
std::env::var("NATS_URL").unwrap_or_else(|_| "nats://127.0.0.1:4222".to_string());
462+
async_nats::connect(&nats_url)
463+
.await
464+
.unwrap_or_else(|err| panic!("failed to connect to NATS at {nats_url}: {err}"))
465+
}
466+
467+
fn spawn_test_worker(client: async_nats::Client) -> tokio::task::JoinHandle<()> {
468+
let engine = ExecutionEngine::new();
469+
let nats_remote = NATSRemoteRuntime::new(client.clone());
470+
let runtime_emitter = NATSRespondEmitter::new(client.clone());
471+
spawn_worker(client, engine, nats_remote, runtime_emitter, None)
472+
}
473+
474+
async fn wait_for_worker_subscription() {
475+
tokio::time::sleep(Duration::from_millis(250)).await;
476+
}
477+
478+
async fn request_execution_result(
479+
client: &async_nats::Client,
480+
execution_id: ExecutionId,
481+
payload: Vec<u8>,
482+
) -> Result<ExecutionResult, String> {
483+
let subject = format!("test_executions.{execution_id}");
484+
485+
for attempt in 1..=10 {
486+
match tokio::time::timeout(
487+
Duration::from_secs(2),
488+
client.request(subject.clone(), payload.clone().into()),
489+
)
490+
.await
491+
{
492+
Ok(Ok(message)) => {
493+
return ExecutionResult::decode(&*message.payload)
494+
.map_err(|err| format!("failed to decode ExecutionResult: {err}"));
495+
}
496+
Ok(Err(err)) if attempt < 10 => {
497+
log::debug!(
498+
"test execution request attempt {} failed before subscription was ready: {:?}",
499+
attempt,
500+
err
501+
);
502+
tokio::time::sleep(Duration::from_millis(100)).await;
503+
}
504+
Ok(Err(err)) => return Err(format!("NATS request failed: {err}")),
505+
Err(_) if attempt < 10 => {
506+
tokio::time::sleep(Duration::from_millis(100)).await;
507+
}
508+
Err(_) => return Err("timed out waiting for test execution response".to_string()),
509+
}
510+
}
511+
512+
Err("test execution request did not complete".to_string())
513+
}
514+
515+
fn load_fixture(path: &str) -> FlowFixture {
516+
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
517+
.join("../..")
518+
.join(path);
519+
let content = std::fs::read_to_string(&path)
520+
.unwrap_or_else(|err| panic!("failed to read fixture {}: {err}", path.display()));
521+
serde_json::from_str(&content)
522+
.unwrap_or_else(|err| panic!("failed to parse fixture {}: {err}", path.display()))
523+
}
524+
525+
fn execution_flow_from_fixture(fixture: FlowFixture) -> ExecutionFlow {
526+
ExecutionFlow {
527+
flow_id: fixture.flow.flow_id,
528+
project_id: fixture.flow.project_id,
529+
starting_node_id: fixture.flow.starting_node_id,
530+
node_functions: fixture.flow.node_functions,
531+
input_value: fixture
532+
.inputs
533+
.first()
534+
.and_then(|input| input.input.clone().map(from_json_value)),
535+
}
536+
}
537+
}

0 commit comments

Comments
 (0)