use std::sync::Arc;
use std::time::Duration;
use anyhow::{anyhow, Context as AnyhowContext, Result};
use crate::cook::environment::EnvironmentConfig;
use crate::cook::execution::MapPhase;
use crate::cook::workflow::{ExtendedWorkflowConfig, WorkflowStep};
use super::{
normalized, CheckpointCompletedStep, ExecutionEnvironment, StepResult, WorkflowContext,
};
#[cfg(test)]
use std::collections::HashMap;
pub async fn validate_mapreduce_dry_run(workflow: &ExtendedWorkflowConfig) -> Result<()> {
use crate::cook::execution::mapreduce::dry_run::{
DryRunConfig, DryRunValidator, OutputFormatter,
};
println!("[DRY RUN] MapReduce workflow execution simulation mode");
println!("[DRY RUN] Validating workflow configuration...");
let _dry_run_config = DryRunConfig {
show_work_items: true,
show_variables: true,
show_resources: true,
sample_size: Some(5),
};
let validator = DryRunValidator::new();
let validation_result = validator
.validate_workflow_phases(
workflow.setup_phase.clone(),
workflow
.map_phase
.as_ref()
.ok_or_else(|| anyhow!("MapReduce workflow requires map phase"))?
.clone(),
workflow.reduce_phase.clone(),
)
.await;
match validation_result {
Ok(report) => {
let formatter = OutputFormatter::new();
println!("{}", formatter.format_human(&report));
if report.errors.is_empty() {
println!("\n[DRY RUN] Validation successful! Workflow is ready to execute.");
Ok(())
} else {
println!(
"\n[DRY RUN] Validation failed with {} error(s)",
report.errors.len()
);
Err(anyhow!("Dry-run validation failed"))
}
}
Err(e) => {
println!("[DRY RUN] Validation failed: {}", e);
Err(anyhow!("Dry-run validation failed: {}", e))
}
}
}
pub fn prepare_mapreduce_environment(
env: &ExecutionEnvironment,
global_env_config: Option<&EnvironmentConfig>,
positional_args: Option<&[String]>,
) -> Result<(ExecutionEnvironment, WorkflowContext)> {
use crate::cook::environment::{EnvValue, EnvironmentContextBuilder};
let worktree_env = env.clone();
let mut builder = EnvironmentContextBuilder::new(env.working_dir.to_path_buf())
.with_config(global_env_config.unwrap_or(&EnvironmentConfig::default()))
.context("Failed to create immutable environment context")?;
if let Some(args) = positional_args {
builder = builder.with_positional_args(args);
}
let _worktree_context = builder.build();
let mut workflow_context = WorkflowContext::default();
if let Some(global_env_config) = global_env_config {
for (key, env_value) in &global_env_config.global_env {
if let EnvValue::Static(value) = env_value {
workflow_context
.variables
.insert(key.clone(), value.clone());
}
}
}
if let Some(args) = positional_args {
use crate::cook::environment::pure::inject_positional_args;
inject_positional_args(&mut workflow_context.variables, args);
}
Ok((worktree_env, workflow_context))
}
pub fn configure_map_phase(
workflow: &ExtendedWorkflowConfig,
generated_input: Option<String>,
context: &WorkflowContext,
) -> Result<MapPhase> {
let mut map_phase = workflow
.map_phase
.as_ref()
.ok_or_else(|| anyhow!("MapReduce workflow requires map phase configuration"))?
.clone();
if let Some(generated_file) = generated_input {
map_phase.config.input = generated_file;
}
let mut interpolated_input = map_phase.config.input.clone();
for (key, value) in &context.variables {
interpolated_input = interpolated_input.replace(&format!("${{{}}}", key), value);
interpolated_input = interpolated_input.replace(&format!("${}", key), value);
}
map_phase.config.input = interpolated_input;
map_phase.workflow_env = context.variables.clone();
tracing::debug!(
workflow_env = ?map_phase.workflow_env,
"MapReduce workflow environment configured"
);
Ok(map_phase)
}
pub fn build_session_step_result(
step_index: usize,
step_display: String,
step: &WorkflowStep,
step_result: &StepResult,
command_duration: Duration,
step_started_at: chrono::DateTime<chrono::Utc>,
step_completed_at: chrono::DateTime<chrono::Utc>,
) -> crate::cook::session::StepResult {
crate::cook::session::StepResult {
step_index,
command: step_display,
success: step_result.success,
output: if step.capture_output.is_enabled() {
Some(step_result.stdout.clone())
} else {
None
},
duration: command_duration,
error: if !step_result.success {
Some(step_result.stderr.clone())
} else {
None
},
started_at: step_started_at,
completed_at: step_completed_at,
exit_code: step_result.exit_code,
}
}
pub fn build_checkpoint_step(
step_index: usize,
step_display: String,
step: &WorkflowStep,
step_result: &StepResult,
workflow_context: &WorkflowContext,
command_duration: Duration,
step_completed_at: chrono::DateTime<chrono::Utc>,
) -> CheckpointCompletedStep {
CheckpointCompletedStep {
step_index,
command: step_display,
success: step_result.success,
output: if step.capture_output.is_enabled() {
Some(step_result.stdout.clone())
} else {
None
},
captured_variables: workflow_context.captured_outputs.clone(),
duration: command_duration,
completed_at: step_completed_at,
retry_state: None,
}
}
pub fn format_step_progress(step_index: usize, total_steps: usize, step_display: &str) -> String {
format!(
"Executing step {}/{}: {}",
step_index + 1,
total_steps,
step_display
)
}
pub fn format_iteration_progress(iteration: u32, max_iterations: u32) -> String {
format!("Starting iteration {}/{}", iteration, max_iterations)
}
pub fn format_workflow_start(workflow_name: &str, max_iterations: u32) -> String {
format!(
"Executing workflow: {} (max {} iterations)",
workflow_name, max_iterations
)
}
pub fn format_skip_step(step_index: usize, total_steps: usize, step_display: &str) -> String {
format!(
"Skipping already completed step {}/{}: {}",
step_index + 1,
total_steps,
step_display
)
}
#[allow(dead_code)] pub fn should_continue_iteration(
has_changes: bool,
is_iterative: bool,
current_iteration: u32,
max_iterations: u32,
) -> bool {
if current_iteration >= max_iterations {
return false;
}
if !is_iterative {
return current_iteration == 0;
}
has_changes
}
#[allow(dead_code)] pub fn calculate_progress_percentage(
current_iteration: u32,
max_iterations: u32,
current_step: usize,
total_steps: usize,
) -> u8 {
if max_iterations == 0 || total_steps == 0 {
return 0;
}
let iteration_progress = (current_iteration as f64 / max_iterations as f64) * 100.0;
let step_progress =
(current_step as f64 / total_steps as f64) * (100.0 / max_iterations as f64);
(iteration_progress + step_progress).min(100.0) as u8
}
pub fn create_workflow_hash(workflow_name: &str, step_count: usize) -> String {
format!("{}-{}", workflow_name, step_count)
}
pub fn create_normalized_workflow(
workflow_name: &str,
workflow_context: &WorkflowContext,
) -> normalized::NormalizedWorkflow {
normalized::NormalizedWorkflow {
name: Arc::from(workflow_name),
steps: Arc::from([]), execution_mode: normalized::ExecutionMode::Sequential,
variables: Arc::new(workflow_context.variables.clone()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_step_progress() {
let msg = format_step_progress(0, 5, "test command");
assert_eq!(msg, "Executing step 1/5: test command");
let msg = format_step_progress(4, 5, "last step");
assert_eq!(msg, "Executing step 5/5: last step");
}
#[test]
fn test_format_iteration_progress() {
let msg = format_iteration_progress(1, 10);
assert_eq!(msg, "Starting iteration 1/10");
let msg = format_iteration_progress(10, 10);
assert_eq!(msg, "Starting iteration 10/10");
}
#[test]
fn test_format_workflow_start() {
let msg = format_workflow_start("test-workflow", 5);
assert_eq!(msg, "Executing workflow: test-workflow (max 5 iterations)");
}
#[test]
fn test_format_skip_step() {
let msg = format_skip_step(2, 5, "skipped command");
assert_eq!(msg, "Skipping already completed step 3/5: skipped command");
}
#[test]
fn test_should_continue_iteration_non_iterative() {
assert!(should_continue_iteration(false, false, 0, 1));
assert!(!should_continue_iteration(false, false, 1, 1));
assert!(!should_continue_iteration(true, false, 1, 1));
}
#[test]
fn test_should_continue_iteration_iterative_with_changes() {
assert!(should_continue_iteration(true, true, 0, 10));
assert!(should_continue_iteration(true, true, 5, 10));
assert!(!should_continue_iteration(true, true, 10, 10));
}
#[test]
fn test_should_continue_iteration_iterative_no_changes() {
assert!(!should_continue_iteration(false, true, 1, 10));
assert!(!should_continue_iteration(false, true, 5, 10));
}
#[test]
fn test_should_continue_iteration_max_reached() {
assert!(!should_continue_iteration(true, true, 10, 10));
assert!(!should_continue_iteration(false, true, 10, 10));
}
#[test]
fn test_calculate_progress_percentage() {
assert_eq!(calculate_progress_percentage(1, 1, 0, 1), 100);
assert_eq!(calculate_progress_percentage(1, 10, 0, 5), 10);
assert_eq!(calculate_progress_percentage(5, 10, 0, 5), 50);
assert_eq!(calculate_progress_percentage(10, 10, 0, 5), 100);
assert_eq!(calculate_progress_percentage(0, 0, 0, 0), 0);
assert_eq!(calculate_progress_percentage(0, 10, 0, 0), 0);
}
#[test]
fn test_create_workflow_hash() {
let hash1 = create_workflow_hash("test-workflow", 5);
let hash2 = create_workflow_hash("test-workflow", 5);
assert_eq!(hash1, hash2);
let hash3 = create_workflow_hash("other-workflow", 5);
assert_ne!(hash1, hash3);
let hash4 = create_workflow_hash("test-workflow", 10);
assert_ne!(hash1, hash4); }
#[test]
fn test_create_normalized_workflow() {
use crate::cook::workflow::variables::VariableStore;
let context = WorkflowContext {
variables: HashMap::from([
("key1".to_string(), "value1".to_string()),
("key2".to_string(), "value2".to_string()),
]),
captured_outputs: HashMap::new(),
iteration_vars: HashMap::new(),
validation_results: HashMap::new(),
variable_store: Arc::new(VariableStore::new()),
git_tracker: None,
};
let normalized = create_normalized_workflow("test-workflow", &context);
assert_eq!(normalized.name.as_ref(), "test-workflow");
assert_eq!(
normalized.variables.get("key1"),
Some(&"value1".to_string())
);
assert_eq!(
normalized.variables.get("key2"),
Some(&"value2".to_string())
);
}
}