use durare::{DurableContext, DurableEngine, Error, InMemoryProvider, Result, WorkflowOptions};
use std::sync::Arc;
use std::time::Duration;
#[tokio::test]
async fn write_close_then_read_in_order() -> Result<()> {
let mut engine = DurableEngine::new(Arc::new(InMemoryProvider::new())).await?;
engine.register("producer", |ctx: DurableContext, _: ()| async move {
for i in 0..3_i64 {
ctx.write_stream("nums", i).await?;
}
ctx.close_stream("nums").await?;
Ok::<_, Error>(())
});
engine
.start::<_, ()>("producer", (), WorkflowOptions::with_id("p"))
.await?
.result()
.await?;
let (values, closed): (Vec<i64>, bool) = engine.read_stream("p", "nums").await?;
assert_eq!(values, vec![0, 1, 2]);
assert!(closed);
let steps = engine.get_workflow_steps("p").await?;
let names: Vec<&str> = steps.iter().map(|s| s.name.as_str()).collect();
assert_eq!(
names,
vec![
"DBOS.writeStream",
"DBOS.writeStream",
"DBOS.writeStream",
"DBOS.closeStream",
]
);
Ok(())
}
#[tokio::test]
async fn read_stops_when_producer_finishes_without_close() -> Result<()> {
let mut engine = DurableEngine::new(Arc::new(InMemoryProvider::new())).await?;
engine.register("producer", |ctx: DurableContext, _: ()| async move {
ctx.write_stream("s", "a".to_string()).await?;
ctx.write_stream("s", "b".to_string()).await?;
Ok::<_, Error>(())
});
engine
.start::<_, ()>("producer", (), WorkflowOptions::with_id("p2"))
.await?
.result()
.await?;
let (values, closed): (Vec<String>, bool) = engine.read_stream("p2", "s").await?;
assert_eq!(values, vec!["a".to_string(), "b".to_string()]);
assert!(closed, "an inactive producer ends the stream");
Ok(())
}
#[tokio::test]
async fn snapshot_reads_available_from_offset() -> Result<()> {
let mut engine = DurableEngine::new(Arc::new(InMemoryProvider::new())).await?;
engine.register("producer", |ctx: DurableContext, _: ()| async move {
for i in 0..3_i64 {
ctx.write_stream("nums", i).await?;
}
ctx.close_stream("nums").await?;
Ok::<_, Error>(())
});
engine
.start::<_, ()>("producer", (), WorkflowOptions::with_id("p3"))
.await?
.result()
.await?;
let (head, closed): (Vec<i64>, bool) = engine.read_stream_snapshot("p3", "nums", 0).await?;
assert_eq!(head, vec![0, 1, 2]);
assert!(closed);
let (tail, closed): (Vec<i64>, bool) = engine.read_stream_snapshot("p3", "nums", 2).await?;
assert_eq!(tail, vec![2]);
assert!(closed);
Ok(())
}
#[tokio::test]
async fn writing_to_closed_stream_errors() -> Result<()> {
let mut engine = DurableEngine::new(Arc::new(InMemoryProvider::new())).await?;
engine.register("bad", |ctx: DurableContext, _: ()| async move {
ctx.close_stream("s").await?;
ctx.write_stream("s", 1_i64).await?;
Ok::<_, Error>(())
});
let res = engine
.start::<_, ()>("bad", (), WorkflowOptions::with_id("p4"))
.await?
.result()
.await;
assert!(res.is_err(), "writing after close must fail");
Ok(())
}
#[tokio::test]
async fn workflow_reads_another_workflows_stream() -> Result<()> {
let mut engine = DurableEngine::new(Arc::new(InMemoryProvider::new())).await?;
engine.register("producer", |ctx: DurableContext, _: ()| async move {
for i in 1..=3_i64 {
ctx.write_stream("nums", i).await?;
}
ctx.close_stream("nums").await?;
Ok::<_, Error>(())
});
engine.register(
"consumer",
|ctx: DurableContext, producer_id: String| async move {
let (values, _closed): (Vec<i64>, bool) = ctx.read_stream(&producer_id, "nums").await?;
Ok::<_, Error>(values)
},
);
engine
.start::<_, ()>("producer", (), WorkflowOptions::with_id("prod"))
.await?
.result()
.await?;
let values: Vec<i64> = engine
.start::<_, Vec<i64>>(
"consumer",
"prod".to_string(),
WorkflowOptions::with_id("cons"),
)
.await?
.result()
.await?;
assert_eq!(values, vec![1, 2, 3]);
let steps = engine.get_workflow_steps("cons").await?;
assert!(steps.is_empty(), "ctx.read_stream records no step");
Ok(())
}
#[tokio::test]
async fn async_stream_yields_values_incrementally() -> Result<()> {
use durare::StreamExt;
let mut engine = DurableEngine::new(Arc::new(InMemoryProvider::new())).await?;
engine.register("slow_producer", |ctx: DurableContext, _: ()| async move {
ctx.write_stream("s", 1_i64).await?;
ctx.sleep(Duration::from_millis(40)).await?;
ctx.write_stream("s", 2_i64).await?;
ctx.sleep(Duration::from_millis(40)).await?;
ctx.write_stream("s", 3_i64).await?;
ctx.close_stream("s").await?;
Ok::<_, Error>(())
});
let producer = engine
.start::<_, ()>("slow_producer", (), WorkflowOptions::with_id("p6"))
.await?;
let mut got = Vec::new();
let mut values = engine.read_stream_values::<i64>("p6", "s");
while let Some(item) = values.next().await {
got.push(item?);
}
assert_eq!(got, vec![1, 2, 3]);
producer.result().await?;
Ok(())
}
#[tokio::test]
async fn async_stream_errors_on_missing_workflow() -> Result<()> {
use durare::StreamExt;
let engine = DurableEngine::new(Arc::new(InMemoryProvider::new())).await?;
let mut values = engine.read_stream_values::<i64>("nope", "s");
let first = values.next().await;
assert!(
matches!(first, Some(Err(_))),
"missing workflow yields a terminal Err"
);
assert!(values.next().await.is_none(), "stream ends after the Err");
Ok(())
}
#[tokio::test]
async fn async_stream_drains_when_producer_finishes_without_close() -> Result<()> {
use durare::StreamExt;
let mut engine = DurableEngine::new(Arc::new(InMemoryProvider::new())).await?;
engine.register("p_noclose", |ctx: DurableContext, _: ()| async move {
ctx.write_stream("s", "a".to_string()).await?;
ctx.write_stream("s", "b".to_string()).await?;
Ok::<_, Error>(())
});
engine
.start::<_, ()>("p_noclose", (), WorkflowOptions::with_id("pnc"))
.await?
.result()
.await?;
let mut got = Vec::new();
let mut values = engine.read_stream_values::<String>("pnc", "s");
while let Some(item) = values.next().await {
got.push(item?);
}
assert_eq!(
got,
vec!["a".to_string(), "b".to_string()],
"no value dropped when the producer goes inactive without closing"
);
Ok(())
}
#[tokio::test]
async fn read_drains_while_producer_runs() -> Result<()> {
let mut engine = DurableEngine::new(Arc::new(InMemoryProvider::new())).await?;
engine.register("slow_producer", |ctx: DurableContext, _: ()| async move {
ctx.write_stream("s", 1_i64).await?;
ctx.sleep(Duration::from_millis(50)).await?;
ctx.write_stream("s", 2_i64).await?;
ctx.close_stream("s").await?;
Ok::<_, Error>(())
});
let producer = engine
.start::<_, ()>("slow_producer", (), WorkflowOptions::with_id("p5"))
.await?;
let (values, closed): (Vec<i64>, bool) = engine.read_stream("p5", "s").await?;
assert_eq!(values, vec![1, 2]);
assert!(closed);
producer.result().await?;
Ok(())
}