mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-20 00:11:34 +00:00
759 lines
24 KiB
Rust
759 lines
24 KiB
Rust
use std::time::Duration;
|
|
|
|
use async_trait::async_trait;
|
|
|
|
use crate::error::Result;
|
|
use crate::graph::Graph;
|
|
use crate::outcome::{NodeResult, Outcome, OutcomeMeta};
|
|
use crate::state::ExecutionState;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum NodeDecision<M: OutcomeMeta = ()> {
|
|
Continue,
|
|
Skip(Box<Outcome<M>>),
|
|
Block(String),
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum EdgeDecision {
|
|
Continue,
|
|
Override(String),
|
|
Block(String),
|
|
}
|
|
|
|
pub struct AttemptContext<'a, G: Graph> {
|
|
pub node: &'a G::Node,
|
|
pub attempt: u32,
|
|
pub max_attempts: u32,
|
|
}
|
|
|
|
pub struct AttemptResultContext<'a, G: Graph> {
|
|
pub node: &'a G::Node,
|
|
pub result: &'a NodeResult<G::Meta>,
|
|
pub attempt: u32,
|
|
pub will_retry: bool,
|
|
pub backoff_delay: Option<Duration>,
|
|
}
|
|
|
|
pub struct EdgeContext<'a, G: Graph> {
|
|
pub from: &'a str,
|
|
pub to: &'a str,
|
|
pub edge: Option<G::Edge>,
|
|
pub is_jump: bool,
|
|
pub outcome: &'a Outcome<G::Meta>,
|
|
pub reason: &'a str,
|
|
}
|
|
|
|
#[async_trait]
|
|
pub trait RunLifecycle<G: Graph>: Send + Sync {
|
|
async fn on_run_start(&self, _graph: &G, _state: &ExecutionState<G::Meta>) -> Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn on_terminal_reached(
|
|
&self,
|
|
_node: &G::Node,
|
|
_goal_gates_passed: bool,
|
|
_state: &ExecutionState<G::Meta>,
|
|
) {
|
|
}
|
|
|
|
async fn before_node(
|
|
&self,
|
|
_node: &G::Node,
|
|
_state: &ExecutionState<G::Meta>,
|
|
) -> Result<NodeDecision<G::Meta>> {
|
|
Ok(NodeDecision::Continue)
|
|
}
|
|
|
|
async fn before_attempt(
|
|
&self,
|
|
_ctx: &AttemptContext<'_, G>,
|
|
_state: &ExecutionState<G::Meta>,
|
|
) -> Result<NodeDecision<G::Meta>> {
|
|
Ok(NodeDecision::Continue)
|
|
}
|
|
|
|
async fn after_attempt(
|
|
&self,
|
|
_ctx: &AttemptResultContext<'_, G>,
|
|
_state: &ExecutionState<G::Meta>,
|
|
) -> Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn after_node(
|
|
&self,
|
|
_node: &G::Node,
|
|
_result: &mut NodeResult<G::Meta>,
|
|
_state: &ExecutionState<G::Meta>,
|
|
) -> Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn after_record(
|
|
&self,
|
|
_node: &G::Node,
|
|
_result: &NodeResult<G::Meta>,
|
|
_state: &ExecutionState<G::Meta>,
|
|
) -> Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn on_edge_selected(
|
|
&self,
|
|
_ctx: &EdgeContext<'_, G>,
|
|
_state: &ExecutionState<G::Meta>,
|
|
) -> Result<EdgeDecision> {
|
|
Ok(EdgeDecision::Continue)
|
|
}
|
|
|
|
async fn on_checkpoint(
|
|
&self,
|
|
_node: &G::Node,
|
|
_result: &NodeResult<G::Meta>,
|
|
_next_node_id: Option<&str>,
|
|
_state: &ExecutionState<G::Meta>,
|
|
) -> Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn on_run_end(&self, _outcome: &Outcome<G::Meta>, _state: &ExecutionState<G::Meta>) {}
|
|
}
|
|
|
|
/// No-op lifecycle that passes through everything.
|
|
pub struct NoopLifecycle;
|
|
|
|
#[async_trait]
|
|
impl<G: Graph> RunLifecycle<G> for NoopLifecycle {}
|
|
|
|
/// Composes multiple lifecycles, calling them in order. Useful for testing
|
|
/// and simple use cases where fixed ordering suffices.
|
|
pub struct CompositeLifecycle<G: Graph> {
|
|
children: Vec<Box<dyn RunLifecycle<G>>>,
|
|
}
|
|
|
|
impl<G: Graph> CompositeLifecycle<G> {
|
|
pub fn new(children: Vec<Box<dyn RunLifecycle<G>>>) -> Self {
|
|
Self { children }
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<G: Graph + 'static> RunLifecycle<G> for CompositeLifecycle<G> {
|
|
async fn on_run_start(&self, graph: &G, state: &ExecutionState<G::Meta>) -> Result<()> {
|
|
for child in &self.children {
|
|
child.on_run_start(graph, state).await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn on_terminal_reached(
|
|
&self,
|
|
node: &G::Node,
|
|
goal_gates_passed: bool,
|
|
state: &ExecutionState<G::Meta>,
|
|
) {
|
|
for child in &self.children {
|
|
child
|
|
.on_terminal_reached(node, goal_gates_passed, state)
|
|
.await;
|
|
}
|
|
}
|
|
|
|
async fn before_node(
|
|
&self,
|
|
node: &G::Node,
|
|
state: &ExecutionState<G::Meta>,
|
|
) -> Result<NodeDecision<G::Meta>> {
|
|
for child in &self.children {
|
|
match child.before_node(node, state).await? {
|
|
NodeDecision::Continue => {}
|
|
decision => return Ok(decision),
|
|
}
|
|
}
|
|
Ok(NodeDecision::Continue)
|
|
}
|
|
|
|
async fn before_attempt(
|
|
&self,
|
|
ctx: &AttemptContext<'_, G>,
|
|
state: &ExecutionState<G::Meta>,
|
|
) -> Result<NodeDecision<G::Meta>> {
|
|
for child in &self.children {
|
|
match child.before_attempt(ctx, state).await? {
|
|
NodeDecision::Continue => {}
|
|
decision => return Ok(decision),
|
|
}
|
|
}
|
|
Ok(NodeDecision::Continue)
|
|
}
|
|
|
|
async fn after_attempt(
|
|
&self,
|
|
ctx: &AttemptResultContext<'_, G>,
|
|
state: &ExecutionState<G::Meta>,
|
|
) -> Result<()> {
|
|
for child in &self.children {
|
|
child.after_attempt(ctx, state).await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn after_node(
|
|
&self,
|
|
node: &G::Node,
|
|
result: &mut NodeResult<G::Meta>,
|
|
state: &ExecutionState<G::Meta>,
|
|
) -> Result<()> {
|
|
for child in &self.children {
|
|
child.after_node(node, result, state).await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn after_record(
|
|
&self,
|
|
node: &G::Node,
|
|
result: &NodeResult<G::Meta>,
|
|
state: &ExecutionState<G::Meta>,
|
|
) -> Result<()> {
|
|
for child in &self.children {
|
|
child.after_record(node, result, state).await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn on_edge_selected(
|
|
&self,
|
|
ctx: &EdgeContext<'_, G>,
|
|
state: &ExecutionState<G::Meta>,
|
|
) -> Result<EdgeDecision> {
|
|
for child in &self.children {
|
|
match child.on_edge_selected(ctx, state).await? {
|
|
EdgeDecision::Continue => {}
|
|
decision => return Ok(decision),
|
|
}
|
|
}
|
|
Ok(EdgeDecision::Continue)
|
|
}
|
|
|
|
async fn on_checkpoint(
|
|
&self,
|
|
node: &G::Node,
|
|
result: &NodeResult<G::Meta>,
|
|
next_node_id: Option<&str>,
|
|
state: &ExecutionState<G::Meta>,
|
|
) -> Result<()> {
|
|
for child in &self.children {
|
|
child
|
|
.on_checkpoint(node, result, next_node_id, state)
|
|
.await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn on_run_end(&self, outcome: &Outcome<G::Meta>, state: &ExecutionState<G::Meta>) {
|
|
for child in &self.children {
|
|
child.on_run_end(outcome, state).await;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#![allow(
|
|
clippy::items_after_statements,
|
|
reason = "Local helper items keep the test setup readable."
|
|
)]
|
|
|
|
use std::sync::atomic::{AtomicU32, Ordering};
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use super::*;
|
|
use crate::test_fixtures::{TestGraph, TestNode, linear_graph};
|
|
|
|
/// A lifecycle that records which callbacks were called.
|
|
struct RecordingLifecycle {
|
|
name: String,
|
|
log: Arc<Mutex<Vec<String>>>,
|
|
before_node_decision: Mutex<Option<NodeDecision>>,
|
|
before_attempt_decision: Mutex<Option<NodeDecision>>,
|
|
edge_decision: Mutex<Option<EdgeDecision>>,
|
|
}
|
|
|
|
impl RecordingLifecycle {
|
|
fn new(name: &str, log: Arc<Mutex<Vec<String>>>) -> Self {
|
|
Self {
|
|
name: name.to_string(),
|
|
log,
|
|
before_node_decision: Mutex::new(None),
|
|
before_attempt_decision: Mutex::new(None),
|
|
edge_decision: Mutex::new(None),
|
|
}
|
|
}
|
|
|
|
fn with_before_node(self, decision: NodeDecision) -> Self {
|
|
*self.before_node_decision.lock().unwrap() = Some(decision);
|
|
self
|
|
}
|
|
|
|
fn with_before_attempt(self, decision: NodeDecision) -> Self {
|
|
*self.before_attempt_decision.lock().unwrap() = Some(decision);
|
|
self
|
|
}
|
|
|
|
fn with_edge_decision(self, decision: EdgeDecision) -> Self {
|
|
*self.edge_decision.lock().unwrap() = Some(decision);
|
|
self
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl RunLifecycle<TestGraph> for RecordingLifecycle {
|
|
async fn on_run_start(&self, _graph: &TestGraph, _state: &ExecutionState) -> Result<()> {
|
|
self.log
|
|
.lock()
|
|
.unwrap()
|
|
.push(format!("{}:on_run_start", self.name));
|
|
Ok(())
|
|
}
|
|
|
|
async fn on_terminal_reached(
|
|
&self,
|
|
_node: &TestNode,
|
|
_goal_gates_passed: bool,
|
|
_state: &ExecutionState,
|
|
) {
|
|
self.log
|
|
.lock()
|
|
.unwrap()
|
|
.push(format!("{}:on_terminal_reached", self.name));
|
|
}
|
|
|
|
async fn before_node(
|
|
&self,
|
|
_node: &TestNode,
|
|
_state: &ExecutionState,
|
|
) -> Result<NodeDecision> {
|
|
self.log
|
|
.lock()
|
|
.unwrap()
|
|
.push(format!("{}:before_node", self.name));
|
|
Ok(self
|
|
.before_node_decision
|
|
.lock()
|
|
.unwrap()
|
|
.take()
|
|
.unwrap_or(NodeDecision::Continue))
|
|
}
|
|
|
|
async fn before_attempt(
|
|
&self,
|
|
_ctx: &AttemptContext<'_, TestGraph>,
|
|
_state: &ExecutionState,
|
|
) -> Result<NodeDecision> {
|
|
self.log
|
|
.lock()
|
|
.unwrap()
|
|
.push(format!("{}:before_attempt", self.name));
|
|
Ok(self
|
|
.before_attempt_decision
|
|
.lock()
|
|
.unwrap()
|
|
.take()
|
|
.unwrap_or(NodeDecision::Continue))
|
|
}
|
|
|
|
async fn after_attempt(
|
|
&self,
|
|
_ctx: &AttemptResultContext<'_, TestGraph>,
|
|
_state: &ExecutionState,
|
|
) -> Result<()> {
|
|
self.log
|
|
.lock()
|
|
.unwrap()
|
|
.push(format!("{}:after_attempt", self.name));
|
|
Ok(())
|
|
}
|
|
|
|
async fn after_node(
|
|
&self,
|
|
_node: &TestNode,
|
|
_result: &mut NodeResult,
|
|
_state: &ExecutionState,
|
|
) -> Result<()> {
|
|
self.log
|
|
.lock()
|
|
.unwrap()
|
|
.push(format!("{}:after_node", self.name));
|
|
Ok(())
|
|
}
|
|
|
|
async fn after_record(
|
|
&self,
|
|
_node: &TestNode,
|
|
_result: &NodeResult,
|
|
_state: &ExecutionState,
|
|
) -> Result<()> {
|
|
self.log
|
|
.lock()
|
|
.unwrap()
|
|
.push(format!("{}:after_record", self.name));
|
|
Ok(())
|
|
}
|
|
|
|
async fn on_edge_selected(
|
|
&self,
|
|
_ctx: &EdgeContext<'_, TestGraph>,
|
|
_state: &ExecutionState,
|
|
) -> Result<EdgeDecision> {
|
|
self.log
|
|
.lock()
|
|
.unwrap()
|
|
.push(format!("{}:on_edge_selected", self.name));
|
|
Ok(self
|
|
.edge_decision
|
|
.lock()
|
|
.unwrap()
|
|
.take()
|
|
.unwrap_or(EdgeDecision::Continue))
|
|
}
|
|
|
|
async fn on_checkpoint(
|
|
&self,
|
|
_node: &TestNode,
|
|
_result: &NodeResult,
|
|
_next_node_id: Option<&str>,
|
|
_state: &ExecutionState,
|
|
) -> Result<()> {
|
|
self.log
|
|
.lock()
|
|
.unwrap()
|
|
.push(format!("{}:on_checkpoint", self.name));
|
|
Ok(())
|
|
}
|
|
|
|
async fn on_run_end(&self, _outcome: &Outcome, _state: &ExecutionState) {
|
|
self.log
|
|
.lock()
|
|
.unwrap()
|
|
.push(format!("{}:on_run_end", self.name));
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn default_lifecycle_is_noop() {
|
|
let lc = NoopLifecycle;
|
|
let g = linear_graph(&["start", "end"]);
|
|
let state = ExecutionState::new(&g).unwrap();
|
|
assert!(
|
|
<NoopLifecycle as RunLifecycle<TestGraph>>::on_run_start(&lc, &g, &state)
|
|
.await
|
|
.is_ok()
|
|
);
|
|
let node = g.get_node("start").unwrap();
|
|
assert!(matches!(
|
|
<NoopLifecycle as RunLifecycle<TestGraph>>::before_node(&lc, &node, &state)
|
|
.await
|
|
.unwrap(),
|
|
NodeDecision::Continue
|
|
));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn composite_calls_all_children_on_run_start() {
|
|
let log = Arc::new(Mutex::new(Vec::new()));
|
|
let lc = CompositeLifecycle::new(vec![
|
|
Box::new(RecordingLifecycle::new("a", log.clone())),
|
|
Box::new(RecordingLifecycle::new("b", log.clone())),
|
|
]);
|
|
let g = linear_graph(&["start", "end"]);
|
|
let state = ExecutionState::new(&g).unwrap();
|
|
lc.on_run_start(&g, &state).await.unwrap();
|
|
let calls = log.lock().unwrap().clone();
|
|
assert_eq!(calls, vec!["a:on_run_start", "b:on_run_start"]);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn composite_before_node_skip_short_circuits() {
|
|
let log = Arc::new(Mutex::new(Vec::new()));
|
|
let lc = CompositeLifecycle::new(vec![
|
|
Box::new(
|
|
RecordingLifecycle::new("a", log.clone())
|
|
.with_before_node(NodeDecision::Skip(Box::new(Outcome::skipped("hook")))),
|
|
),
|
|
Box::new(RecordingLifecycle::new("b", log.clone())),
|
|
]);
|
|
let g = linear_graph(&["start", "end"]);
|
|
let state = ExecutionState::new(&g).unwrap();
|
|
let node = g.get_node("start").unwrap();
|
|
let decision = lc.before_node(&node, &state).await.unwrap();
|
|
assert!(matches!(decision, NodeDecision::Skip(_)));
|
|
let calls = log.lock().unwrap().clone();
|
|
assert_eq!(calls, vec!["a:before_node"]);
|
|
// b was NOT called
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn composite_before_node_block_short_circuits() {
|
|
let log = Arc::new(Mutex::new(Vec::new()));
|
|
let lc = CompositeLifecycle::new(vec![
|
|
Box::new(
|
|
RecordingLifecycle::new("a", log.clone())
|
|
.with_before_node(NodeDecision::Block("denied".into())),
|
|
),
|
|
Box::new(RecordingLifecycle::new("b", log.clone())),
|
|
]);
|
|
let g = linear_graph(&["start", "end"]);
|
|
let state = ExecutionState::new(&g).unwrap();
|
|
let node = g.get_node("start").unwrap();
|
|
let decision = lc.before_node(&node, &state).await.unwrap();
|
|
assert!(matches!(decision, NodeDecision::Block(_)));
|
|
let calls = log.lock().unwrap().clone();
|
|
assert_eq!(calls, vec!["a:before_node"]);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn composite_before_attempt_skip_short_circuits() {
|
|
let log = Arc::new(Mutex::new(Vec::new()));
|
|
let lc = CompositeLifecycle::new(vec![
|
|
Box::new(
|
|
RecordingLifecycle::new("a", log.clone())
|
|
.with_before_attempt(NodeDecision::Skip(Box::new(Outcome::skipped("skip")))),
|
|
),
|
|
Box::new(RecordingLifecycle::new("b", log.clone())),
|
|
]);
|
|
let g = linear_graph(&["start", "end"]);
|
|
let state = ExecutionState::new(&g).unwrap();
|
|
let node = g.get_node("start").unwrap();
|
|
let ctx = AttemptContext {
|
|
node: &node,
|
|
attempt: 1,
|
|
max_attempts: 1,
|
|
};
|
|
let decision = lc.before_attempt(&ctx, &state).await.unwrap();
|
|
assert!(matches!(decision, NodeDecision::Skip(_)));
|
|
let calls = log.lock().unwrap().clone();
|
|
assert_eq!(calls, vec!["a:before_attempt"]);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn composite_before_attempt_block_short_circuits() {
|
|
let log = Arc::new(Mutex::new(Vec::new()));
|
|
let lc = CompositeLifecycle::new(vec![
|
|
Box::new(
|
|
RecordingLifecycle::new("a", log.clone())
|
|
.with_before_attempt(NodeDecision::Block("nope".into())),
|
|
),
|
|
Box::new(RecordingLifecycle::new("b", log.clone())),
|
|
]);
|
|
let g = linear_graph(&["start", "end"]);
|
|
let state = ExecutionState::new(&g).unwrap();
|
|
let node = g.get_node("start").unwrap();
|
|
let ctx = AttemptContext {
|
|
node: &node,
|
|
attempt: 1,
|
|
max_attempts: 1,
|
|
};
|
|
let decision = lc.before_attempt(&ctx, &state).await.unwrap();
|
|
assert!(matches!(decision, NodeDecision::Block(_)));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn composite_after_attempt_calls_all() {
|
|
let log = Arc::new(Mutex::new(Vec::new()));
|
|
let lc = CompositeLifecycle::new(vec![
|
|
Box::new(RecordingLifecycle::new("a", log.clone())),
|
|
Box::new(RecordingLifecycle::new("b", log.clone())),
|
|
]);
|
|
let g = linear_graph(&["start", "end"]);
|
|
let state = ExecutionState::new(&g).unwrap();
|
|
let node = g.get_node("start").unwrap();
|
|
let result = NodeResult::new(
|
|
Outcome::success(),
|
|
Duration::ZERO,
|
|
Duration::ZERO,
|
|
Duration::ZERO,
|
|
1,
|
|
1,
|
|
);
|
|
let ctx = AttemptResultContext {
|
|
node: &node,
|
|
result: &result,
|
|
attempt: 1,
|
|
will_retry: false,
|
|
backoff_delay: None,
|
|
};
|
|
lc.after_attempt(&ctx, &state).await.unwrap();
|
|
let calls = log.lock().unwrap().clone();
|
|
assert_eq!(calls, vec!["a:after_attempt", "b:after_attempt"]);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn composite_on_edge_selected_override_short_circuits() {
|
|
let log = Arc::new(Mutex::new(Vec::new()));
|
|
let lc = CompositeLifecycle::new(vec![
|
|
Box::new(
|
|
RecordingLifecycle::new("a", log.clone())
|
|
.with_edge_decision(EdgeDecision::Override("other".into())),
|
|
),
|
|
Box::new(RecordingLifecycle::new("b", log.clone())),
|
|
]);
|
|
let g = linear_graph(&["start", "end"]);
|
|
let state = ExecutionState::new(&g).unwrap();
|
|
let outcome = Outcome::success();
|
|
let edge = g.outgoing_edges("start").into_iter().next().unwrap();
|
|
let ctx = EdgeContext {
|
|
from: "start",
|
|
to: "end",
|
|
edge: Some(edge),
|
|
is_jump: false,
|
|
outcome: &outcome,
|
|
reason: "unconditional",
|
|
};
|
|
let decision = lc.on_edge_selected(&ctx, &state).await.unwrap();
|
|
assert!(matches!(decision, EdgeDecision::Override(ref t) if t == "other"));
|
|
let calls = log.lock().unwrap().clone();
|
|
assert_eq!(calls, vec!["a:on_edge_selected"]);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn composite_on_edge_selected_block_short_circuits() {
|
|
let log = Arc::new(Mutex::new(Vec::new()));
|
|
let lc = CompositeLifecycle::new(vec![
|
|
Box::new(
|
|
RecordingLifecycle::new("a", log.clone())
|
|
.with_edge_decision(EdgeDecision::Block("blocked".into())),
|
|
),
|
|
Box::new(RecordingLifecycle::new("b", log.clone())),
|
|
]);
|
|
let g = linear_graph(&["start", "end"]);
|
|
let state = ExecutionState::new(&g).unwrap();
|
|
let outcome = Outcome::success();
|
|
let ctx = EdgeContext {
|
|
from: "start",
|
|
to: "end",
|
|
edge: None,
|
|
is_jump: false,
|
|
outcome: &outcome,
|
|
reason: "unconditional",
|
|
};
|
|
let decision = lc.on_edge_selected(&ctx, &state).await.unwrap();
|
|
assert!(matches!(decision, EdgeDecision::Block(_)));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn composite_on_edge_selected_none_for_jumps() {
|
|
let log = Arc::new(Mutex::new(Vec::new()));
|
|
let lc = CompositeLifecycle::new(vec![Box::new(RecordingLifecycle::new("a", log.clone()))]);
|
|
let g = linear_graph(&["start", "end"]);
|
|
let state = ExecutionState::new(&g).unwrap();
|
|
let outcome = Outcome::success();
|
|
let ctx = EdgeContext::<TestGraph> {
|
|
from: "start",
|
|
to: "target",
|
|
edge: None,
|
|
is_jump: true,
|
|
outcome: &outcome,
|
|
reason: "jump",
|
|
};
|
|
let decision = lc.on_edge_selected(&ctx, &state).await.unwrap();
|
|
assert!(matches!(decision, EdgeDecision::Continue));
|
|
assert!(ctx.edge.is_none());
|
|
assert!(ctx.is_jump);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn composite_after_node_calls_all() {
|
|
let log = Arc::new(Mutex::new(Vec::new()));
|
|
let lc = CompositeLifecycle::new(vec![
|
|
Box::new(RecordingLifecycle::new("a", log.clone())),
|
|
Box::new(RecordingLifecycle::new("b", log.clone())),
|
|
]);
|
|
let g = linear_graph(&["start", "end"]);
|
|
let state = ExecutionState::new(&g).unwrap();
|
|
let node = g.get_node("start").unwrap();
|
|
let mut result = NodeResult::new(
|
|
Outcome::success(),
|
|
Duration::ZERO,
|
|
Duration::ZERO,
|
|
Duration::ZERO,
|
|
1,
|
|
1,
|
|
);
|
|
lc.after_node(&node, &mut result, &state).await.unwrap();
|
|
let calls = log.lock().unwrap().clone();
|
|
assert_eq!(calls, vec!["a:after_node", "b:after_node"]);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn composite_after_record_calls_all() {
|
|
let log = Arc::new(Mutex::new(Vec::new()));
|
|
let lc = CompositeLifecycle::new(vec![
|
|
Box::new(RecordingLifecycle::new("a", log.clone())),
|
|
Box::new(RecordingLifecycle::new("b", log.clone())),
|
|
]);
|
|
let g = linear_graph(&["start", "end"]);
|
|
let state = ExecutionState::new(&g).unwrap();
|
|
let node = g.get_node("start").unwrap();
|
|
let result = NodeResult::new(
|
|
Outcome::success(),
|
|
Duration::ZERO,
|
|
Duration::ZERO,
|
|
Duration::ZERO,
|
|
1,
|
|
1,
|
|
);
|
|
lc.after_record(&node, &result, &state).await.unwrap();
|
|
let calls = log.lock().unwrap().clone();
|
|
assert_eq!(calls, vec!["a:after_record", "b:after_record"]);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn composite_ordering_is_preserved() {
|
|
let log = Arc::new(Mutex::new(Vec::new()));
|
|
let counter = Arc::new(AtomicU32::new(0));
|
|
|
|
struct OrderedLifecycle {
|
|
name: String,
|
|
log: Arc<Mutex<Vec<String>>>,
|
|
counter: Arc<AtomicU32>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl RunLifecycle<TestGraph> for OrderedLifecycle {
|
|
async fn on_run_start(&self, _g: &TestGraph, _s: &ExecutionState) -> Result<()> {
|
|
let order = self.counter.fetch_add(1, Ordering::SeqCst);
|
|
self.log
|
|
.lock()
|
|
.unwrap()
|
|
.push(format!("{}:{}", self.name, order));
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
let lc = CompositeLifecycle::new(vec![
|
|
Box::new(OrderedLifecycle {
|
|
name: "first".into(),
|
|
log: log.clone(),
|
|
counter: counter.clone(),
|
|
}),
|
|
Box::new(OrderedLifecycle {
|
|
name: "second".into(),
|
|
log: log.clone(),
|
|
counter: counter.clone(),
|
|
}),
|
|
Box::new(OrderedLifecycle {
|
|
name: "third".into(),
|
|
log: log.clone(),
|
|
counter: counter.clone(),
|
|
}),
|
|
]);
|
|
let g = linear_graph(&["start", "end"]);
|
|
let state = ExecutionState::new(&g).unwrap();
|
|
lc.on_run_start(&g, &state).await.unwrap();
|
|
let calls = log.lock().unwrap().clone();
|
|
assert_eq!(calls, vec!["first:0", "second:1", "third:2"]);
|
|
}
|
|
}
|