mod common;
use common::*;
use orka::test_util::MockPipeline;
use orka::{
CancelToken, ContextData, Orka, OrkaError, Pipeline, PipelineControl, PipelineResult, PlannedAction, RunOutcome,
SkipReason, TraceCollector, TraceEventKind,
};
use std::sync::Arc;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct RegistryContextAlpha {
val: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct RegistryContextBeta {
num: i32,
}
#[tokio::test]
async fn test_registry_run_correct_pipeline() {
setup_tracing();
let orka_registry = Orka::<TestError>::new();
let mut p_alpha = Pipeline::<RegistryContextAlpha, TestError>::new(["alpha_task"]);
p_alpha.on_root("alpha_task", |ctx| async move {
ctx.write().val = "alpha_processed".to_string();
Ok(PipelineControl::Continue)
});
orka_registry.register_pipeline(p_alpha).unwrap();
let mut p_beta = Pipeline::<RegistryContextBeta, TestError>::new(["beta_task"]);
p_beta.on_root("beta_task", |ctx| async move {
ctx.write().num = 100;
Ok(PipelineControl::Continue)
});
orka_registry.register_pipeline(p_beta).unwrap();
let ctx_alpha = ContextData::new(RegistryContextAlpha::default());
let res_alpha = orka_registry.run(ctx_alpha.clone()).await;
assert!(res_alpha.is_ok());
assert_eq!(res_alpha.unwrap(), PipelineResult::Completed);
assert_eq!(ctx_alpha.read().val, "alpha_processed");
let ctx_beta = ContextData::new(RegistryContextBeta::default());
let res_beta = orka_registry.run(ctx_beta.clone()).await;
assert!(res_beta.is_ok());
assert_eq!(res_beta.unwrap(), PipelineResult::Completed);
assert_eq!(ctx_beta.read().num, 100);
}
#[tokio::test]
async fn test_registry_pipeline_not_found() {
setup_tracing();
let orka_registry = Orka::<TestError>::new();
#[derive(Clone, Debug, Default)]
struct UnregisteredContext;
let ctx_unregistered = ContextData::new(UnregisteredContext);
let result = orka_registry.run(ctx_unregistered).await;
assert!(result.is_err());
if let Err(TestError::Orka(s)) = result {
assert!(s.contains("ConfigurationError"));
assert!(s.contains("No pipeline registered"));
assert!(s.contains("UnregisteredContext"));
} else {
panic!(
"Expected OrkaError(ConfigurationError) for unregistered pipeline, got {:?}",
result
);
}
}
#[tokio::test]
async fn test_registry_pipeline_itself_errors() {
setup_tracing();
let orka_registry = Orka::<TestError>::new();
let mut p_alpha = Pipeline::<RegistryContextAlpha, TestError>::new(["alpha_fail"]);
p_alpha.on_root("alpha_fail", |_ctx| async move {
Err(TestError::Handler("Alpha pipeline failed".to_string()))
});
orka_registry.register_pipeline(p_alpha).unwrap();
let ctx_alpha = ContextData::new(RegistryContextAlpha::default());
let res_alpha = orka_registry.run(ctx_alpha.clone()).await;
assert!(res_alpha.is_err());
assert_eq!(
res_alpha.err().unwrap(),
TestError::Handler("Alpha pipeline failed".to_string())
);
}
#[tokio::test]
async fn test_registry_with_orka_error_default() {
setup_tracing();
let orka_registry = Orka::<OrkaError>::new_default();
#[derive(Clone, Debug, Default)]
struct SimpleCtx {
count: i32,
}
let mut pipeline = Pipeline::<SimpleCtx, OrkaError>::new(["task"]);
pipeline.on_root("task", |ctx| async move {
ctx.write().count = 1;
Ok(PipelineControl::Continue)
});
orka_registry.register_pipeline(pipeline).unwrap();
let ctx = ContextData::new(SimpleCtx::default());
let result = orka_registry.run(ctx.clone()).await;
assert!(result.is_ok());
assert_eq!(ctx.read().count, 1);
}
fn build_alpha_registry() -> Orka<TestError> {
let orka_registry = Orka::<TestError>::new();
let mut p = Pipeline::<RegistryContextAlpha, TestError>::new(["prepare", "commit"]);
p.skip_if("prepare", |ctx| ctx.read().val == "already_prepared");
p.on_root("prepare", |ctx| async move {
ctx.write().val.push_str("prepared;");
Ok(PipelineControl::Continue)
})
.on_root("commit", |ctx| async move {
ctx.write().val.push_str("committed;");
Ok(PipelineControl::Continue)
})
.on_finish(|_ctx, _outcome| async { Ok(()) });
orka_registry.register_pipeline(p).unwrap();
orka_registry
}
#[tokio::test]
async fn pipeline_accessor_returns_the_registered_pipeline() {
setup_tracing();
let orka_registry = build_alpha_registry();
let p = orka_registry
.pipeline::<RegistryContextAlpha, TestError>()
.expect("registered as a concrete pipeline");
assert_eq!(p.step_names(), vec!["prepare", "commit"]);
#[derive(Clone, Debug, Default)]
struct NotRegistered;
assert!(orka_registry.pipeline::<NotRegistered, TestError>().is_none());
}
#[tokio::test]
async fn pipeline_accessor_returns_none_for_runner_registrations_and_wrong_err() {
setup_tracing();
let orka_registry = Orka::<TestError>::new();
orka_registry.register_runner::<RegistryContextAlpha, TestError>(Arc::new(MockPipeline::completed()));
assert!(orka_registry.pipeline::<RegistryContextAlpha, TestError>().is_none());
let orka_registry2 = build_alpha_registry();
assert!(orka_registry2.pipeline::<RegistryContextAlpha, OrkaError>().is_none());
}
#[tokio::test]
async fn observe_and_dry_run_the_registered_pipeline_through_the_front_door() {
setup_tracing();
let orka_registry = build_alpha_registry();
let p = orka_registry.pipeline::<RegistryContextAlpha, TestError>().unwrap();
let plan = p.resolve_plan(&ContextData::new(RegistryContextAlpha {
val: "already_prepared".into(),
}));
assert_eq!(plan[0].action, PlannedAction::Skip(SkipReason::SkipCondition { label: None }));
assert_eq!(plan[1].action, PlannedAction::Run);
let trace = TraceCollector::new();
p.set_tracer(trace.clone());
let ctx = ContextData::new(RegistryContextAlpha::default());
assert_eq!(orka_registry.run(ctx.clone()).await.unwrap(), PipelineResult::Completed);
assert_eq!(ctx.read().val, "prepared;committed;");
assert_eq!(trace.completed_steps(), vec!["prepare", "commit"]);
assert_eq!(trace.last_outcome(), Some(RunOutcome::Completed));
assert!(
trace
.events()
.iter()
.any(|e| matches!(e.kind, TraceEventKind::FinalizerFinished { .. })),
"on_finish ring must be visible through orka.run"
);
let step_ctx = ContextData::new(RegistryContextAlpha::default());
p.run_step("commit", step_ctx.clone()).await.unwrap();
assert_eq!(step_ctx.read().val, "committed;");
}
#[tokio::test]
async fn cancellation_survives_the_registry_type_erasure() {
setup_tracing();
let orka_registry = Orka::<TestError>::new();
let token = CancelToken::new();
let canceller = token.clone();
let mut p = Pipeline::<RegistryContextAlpha, TestError>::new(["first", "second"]);
p.on_root("first", move |ctx: ContextData<RegistryContextAlpha>| {
let canceller = canceller.clone();
async move {
ctx.write().val = "first".to_string();
canceller.cancel();
Ok(PipelineControl::Continue)
}
})
.on_root("second", |ctx: ContextData<RegistryContextAlpha>| async move {
ctx.write().val = "second".to_string();
Ok(PipelineControl::Continue)
});
orka_registry.register_pipeline(p).unwrap();
let ctx = ContextData::new(RegistryContextAlpha::default());
let (result, outcome) = orka_registry.run_with_cancel_and_outcome(ctx.clone(), token).await;
assert_eq!(result.unwrap(), PipelineResult::Cancelled);
assert_eq!(outcome, RunOutcome::Cancelled);
assert_eq!(ctx.read().val, "first", "the second step never ran");
}
#[tokio::test]
async fn a_registry_run_with_an_uncancelled_token_completes_normally() {
setup_tracing();
let orka_registry = Orka::<TestError>::new();
let mut p = Pipeline::<RegistryContextBeta, TestError>::new(["task"]);
p.on_root("task", |ctx: ContextData<RegistryContextBeta>| async move {
ctx.write().num = 7;
Ok(PipelineControl::Continue)
});
orka_registry.register_pipeline(p).unwrap();
let ctx = ContextData::new(RegistryContextBeta::default());
let result = orka_registry.run_with_cancel(ctx.clone(), CancelToken::new()).await;
assert_eq!(result.unwrap(), PipelineResult::Completed);
assert_eq!(ctx.read().num, 7);
}