use crate::core::context_data::ContextData;
use crate::core::control::{PipelineControl, PipelineResult};
use crate::core::trace::{RunOutcome, TraceCollector};
use crate::error::OrkaError;
use crate::pipeline::definition::Pipeline;
use crate::pipeline::runner::PipelineRunner;
use async_trait::async_trait;
use parking_lot::Mutex;
use std::collections::VecDeque;
use std::future::{ready, Ready};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum TestError {
#[error("Orka framework error: {0:?}")]
Orka(String),
#[error("Test handler failed: {0}")]
Handler(String),
#[error("Test extractor failed: {0}")]
Extractor(String),
#[error("Test pipeline provider failed: {0}")]
Provider(String),
#[error("Test scoped task failed: {0}")]
ScopedTask(String),
#[error("{0}")]
Other(String),
}
impl From<OrkaError> for TestError {
fn from(oe: OrkaError) -> Self {
TestError::Orka(format!("{:?}", oe))
}
}
#[derive(Clone, Debug, Default)]
pub struct ExecutionCounter(Arc<AtomicUsize>);
impl ExecutionCounter {
pub fn new() -> Self {
Self::default()
}
pub fn increment(&self) -> usize {
self.0.fetch_add(1, Ordering::SeqCst) + 1
}
pub fn get(&self) -> usize {
self.0.load(Ordering::SeqCst)
}
pub fn reset(&self) {
self.0.store(0, Ordering::SeqCst);
}
}
pub fn continue_handler<TData, Err>() -> impl Fn(ContextData<TData>) -> Ready<Result<PipelineControl, Err>> + Send + Sync + 'static
where
TData: 'static + Send + Sync,
{
|_ctx| ready(Ok(PipelineControl::Continue))
}
pub fn stop_handler<TData, Err>() -> impl Fn(ContextData<TData>) -> Ready<Result<PipelineControl, Err>> + Send + Sync + 'static
where
TData: 'static + Send + Sync,
{
|_ctx| ready(Ok(PipelineControl::Stop))
}
pub fn fail_handler<TData, Err>(
make_err: impl Fn() -> Err + Send + Sync + 'static,
) -> impl Fn(ContextData<TData>) -> Ready<Result<PipelineControl, Err>> + Send + Sync + 'static
where
TData: 'static + Send + Sync,
{
move |_ctx| ready(Err(make_err()))
}
pub fn counting_handler<TData, Err>(
counter: ExecutionCounter,
) -> impl Fn(ContextData<TData>) -> Ready<Result<PipelineControl, Err>> + Send + Sync + 'static
where
TData: 'static + Send + Sync,
{
move |_ctx| {
counter.increment();
ready(Ok(PipelineControl::Continue))
}
}
pub trait PipelineTestExt<TData, Err>
where
TData: 'static + Send + Sync,
Err: std::error::Error + From<OrkaError> + Send + Sync + 'static,
{
fn fail_at(&mut self, step_name: impl AsRef<str>, make_err: impl Fn() -> Err + Send + Sync + 'static) -> &mut Self;
}
impl<TData, Err> PipelineTestExt<TData, Err> for Pipeline<TData, Err>
where
TData: 'static + Send + Sync,
Err: std::error::Error + From<OrkaError> + Send + Sync + 'static,
{
fn fail_at(&mut self, step_name: impl AsRef<str>, make_err: impl Fn() -> Err + Send + Sync + 'static) -> &mut Self {
let step_name = step_name.as_ref();
self.replace_on_root(step_name, fail_handler(make_err))
}
}
pub fn noop_pipeline<TData, Err, I, S>(step_names: I) -> Pipeline<TData, Err>
where
TData: 'static + Send + Sync,
Err: std::error::Error + From<OrkaError> + Send + Sync + 'static,
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let names: Vec<String> = step_names.into_iter().map(|s| s.as_ref().to_string()).collect();
let mut pipeline = Pipeline::new(&names);
for name in &names {
pipeline.on_root(name, continue_handler());
}
pipeline
}
enum CannedResponse<Err> {
Completed,
Stopped,
Error(Box<dyn Fn() -> Err + Send + Sync>),
}
type BaseBehavior<TData, Err> = Box<dyn Fn(ContextData<TData>) -> Result<PipelineResult, Err> + Send + Sync>;
pub struct MockPipeline<TData, Err>
where
TData: 'static + Send + Sync,
{
base: BaseBehavior<TData, Err>,
queued: Mutex<VecDeque<CannedResponse<Err>>>,
seen_contexts: Mutex<Vec<ContextData<TData>>>,
}
impl<TData, Err> MockPipeline<TData, Err>
where
TData: 'static + Send + Sync,
Err: std::error::Error + From<OrkaError> + Send + Sync + 'static,
{
pub fn completed() -> Self {
Self::from_fn(|_ctx| Ok(PipelineResult::Completed))
}
pub fn stopped() -> Self {
Self::from_fn(|_ctx| Ok(PipelineResult::Stopped))
}
pub fn failing(make_err: impl Fn() -> Err + Send + Sync + 'static) -> Self {
Self::from_fn(move |_ctx| Err(make_err()))
}
pub fn from_fn(f: impl Fn(ContextData<TData>) -> Result<PipelineResult, Err> + Send + Sync + 'static) -> Self {
Self {
base: Box::new(f),
queued: Mutex::new(VecDeque::new()),
seen_contexts: Mutex::new(Vec::new()),
}
}
pub fn then_completed(&mut self) -> &mut Self {
self.queued.lock().push_back(CannedResponse::Completed);
self
}
pub fn then_stopped(&mut self) -> &mut Self {
self.queued.lock().push_back(CannedResponse::Stopped);
self
}
pub fn then_error(&mut self, make_err: impl Fn() -> Err + Send + Sync + 'static) -> &mut Self {
self.queued.lock().push_back(CannedResponse::Error(Box::new(make_err)));
self
}
pub fn run_count(&self) -> usize {
self.seen_contexts.lock().len()
}
pub fn contexts(&self) -> Vec<ContextData<TData>> {
self.seen_contexts.lock().clone()
}
}
#[async_trait]
impl<TData, Err> PipelineRunner<TData, Err> for MockPipeline<TData, Err>
where
TData: 'static + Send + Sync,
Err: std::error::Error + From<OrkaError> + Send + Sync + 'static,
{
async fn run(&self, ctx_data: ContextData<TData>) -> Result<PipelineResult, Err> {
self.seen_contexts.lock().push(ctx_data.clone());
let queued = self.queued.lock().pop_front();
match queued {
Some(CannedResponse::Completed) => Ok(PipelineResult::Completed),
Some(CannedResponse::Stopped) => Ok(PipelineResult::Stopped),
Some(CannedResponse::Error(make_err)) => Err(make_err()),
None => (self.base)(ctx_data),
}
}
}
#[track_caller]
pub fn assert_steps_completed(trace: &TraceCollector, expected: &[&str]) {
let completed = trace.completed_steps();
if completed != expected {
panic!(
"completed steps mismatch\n expected: {:?}\n actual: {:?}\n all events:\n{}",
expected,
completed,
format_events(trace)
);
}
}
#[track_caller]
pub fn assert_steps_skipped(trace: &TraceCollector, expected: &[&str]) {
let skipped = trace.skipped_steps();
if skipped != expected {
panic!(
"skipped steps mismatch\n expected: {:?}\n actual: {:?}\n all events:\n{}",
expected,
skipped,
format_events(trace)
);
}
}
#[track_caller]
pub fn assert_run_outcome(trace: &TraceCollector, expected: RunOutcome) {
let actual = trace.last_outcome();
if actual.as_ref() != Some(&expected) {
panic!(
"run outcome mismatch\n expected: {:?}\n actual: {:?}\n all events:\n{}",
expected,
actual,
format_events(trace)
);
}
}
#[track_caller]
pub fn assert_order(trace: &TraceCollector, expected: &[&str]) {
let completed = trace.completed_steps();
let mut remaining = completed.iter();
for want in expected {
if !remaining.any(|s| s == want) {
panic!(
"expected {:?} as an in-order subsequence of completed steps, but '{}' was not found in order\n completed: {:?}",
expected, want, completed
);
}
}
}
fn format_events(trace: &TraceCollector) -> String {
trace
.events()
.iter()
.map(|e| format!(" {}", e))
.collect::<Vec<_>>()
.join("\n")
}