use crate::cook::workflow::normalized::{NormalizedStep, NormalizedWorkflow};
#[derive(Debug, Clone)]
pub struct StepPlan {
pub index: usize,
pub step: NormalizedStep,
pub commit_required: bool,
pub idempotent: bool,
pub depends_on: Vec<usize>,
}
pub fn plan_steps(workflow: &NormalizedWorkflow) -> Vec<StepPlan> {
workflow
.steps
.iter()
.enumerate()
.map(|(idx, step)| {
let commit_required = step.commit_required;
let idempotent = true;
StepPlan {
index: idx,
step: step.clone(),
commit_required,
idempotent,
depends_on: (0..idx).collect(),
}
})
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResumeDecision {
Skip,
Execute,
WarnAndExecute { warning: String },
}
pub fn is_safe_to_resume(plan: &StepPlan, was_completed: bool) -> ResumeDecision {
if was_completed {
ResumeDecision::Skip
} else if plan.idempotent {
ResumeDecision::Execute
} else {
ResumeDecision::WarnAndExecute {
warning: format!(
"Step {} is not marked as idempotent. Re-execution may have side effects.",
plan.index
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cook::workflow::normalized::{
ExecutionMode, NormalizedStep, NormalizedWorkflow, StepCommand, StepHandlers,
};
use std::collections::HashMap;
use std::sync::Arc;
fn create_test_step(id: &str, commit_required: bool) -> NormalizedStep {
NormalizedStep {
id: id.into(),
command: StepCommand::Shell("echo test".into()),
validation: None,
handlers: StepHandlers::default(),
timeout: None,
working_dir: None,
env: Arc::new(HashMap::new()),
outputs: None,
commit_required,
when: None,
}
}
fn create_test_workflow(num_steps: usize) -> NormalizedWorkflow {
let steps: Vec<NormalizedStep> = (0..num_steps)
.map(|i| create_test_step(&format!("step-{}", i), false))
.collect();
NormalizedWorkflow {
name: "test-workflow".into(),
steps: Arc::from(steps),
execution_mode: ExecutionMode::Sequential,
variables: Arc::new(HashMap::new()),
}
}
#[test]
fn test_plan_steps_sequential() {
let workflow = create_test_workflow(3);
let plans = plan_steps(&workflow);
assert_eq!(plans.len(), 3);
assert_eq!(plans[0].depends_on, Vec::<usize>::new());
assert_eq!(plans[1].depends_on, vec![0]);
assert_eq!(plans[2].depends_on, vec![0, 1]);
}
#[test]
fn test_plan_steps_empty_workflow() {
let workflow = create_test_workflow(0);
let plans = plan_steps(&workflow);
assert_eq!(plans.len(), 0);
}
#[test]
fn test_plan_steps_single_step() {
let workflow = create_test_workflow(1);
let plans = plan_steps(&workflow);
assert_eq!(plans.len(), 1);
assert_eq!(plans[0].depends_on, Vec::<usize>::new());
assert_eq!(plans[0].index, 0);
}
#[test]
fn test_plan_steps_preserves_commit_required() {
let steps = vec![
create_test_step("step-1", false),
create_test_step("step-2", true),
];
let workflow = NormalizedWorkflow {
name: "test-workflow".into(),
steps: Arc::from(steps),
execution_mode: ExecutionMode::Sequential,
variables: Arc::new(HashMap::new()),
};
let plans = plan_steps(&workflow);
assert!(!plans[0].commit_required);
assert!(plans[1].commit_required);
}
#[test]
fn test_is_safe_to_resume_completed() {
let plan = StepPlan {
index: 0,
step: create_test_step("test", false),
commit_required: false,
idempotent: true,
depends_on: vec![],
};
let decision = is_safe_to_resume(&plan, true);
assert_eq!(decision, ResumeDecision::Skip);
}
#[test]
fn test_is_safe_to_resume_idempotent() {
let plan = StepPlan {
index: 0,
step: create_test_step("test", false),
commit_required: false,
idempotent: true,
depends_on: vec![],
};
let decision = is_safe_to_resume(&plan, false);
assert_eq!(decision, ResumeDecision::Execute);
}
#[test]
fn test_is_safe_to_resume_non_idempotent() {
let plan = StepPlan {
index: 0,
step: create_test_step("test", false),
commit_required: false,
idempotent: false,
depends_on: vec![],
};
let decision = is_safe_to_resume(&plan, false);
match decision {
ResumeDecision::WarnAndExecute { warning } => {
assert!(warning.contains("not marked as idempotent"));
}
_ => panic!("Expected WarnAndExecute"),
}
}
}