use crate::error::{ExecutionError, ExecutionResult};
use crate::models::{ExecutionPlan, ExecutionStep, StepAction, StepResult};
use std::time::Instant;
use tracing::{debug, error, info, warn};
pub struct StepExecutor {
current_step_index: usize,
completed_steps: Vec<StepResult>,
skip_on_error: bool,
}
impl StepExecutor {
pub fn new() -> Self {
Self {
current_step_index: 0,
completed_steps: Vec::new(),
skip_on_error: false,
}
}
pub fn with_skip_on_error(mut self, skip: bool) -> Self {
self.skip_on_error = skip;
self
}
pub fn execute_plan(&mut self, plan: &ExecutionPlan) -> ExecutionResult<Vec<StepResult>> {
if plan.steps.is_empty() {
return Err(ExecutionError::PlanError(
"Cannot execute plan with no steps".to_string(),
));
}
info!(
plan_id = %plan.id,
step_count = plan.steps.len(),
"Starting plan execution"
);
for (index, step) in plan.steps.iter().enumerate() {
self.current_step_index = index;
debug!(
step_id = %step.id,
step_index = index,
description = %step.description,
"Executing step"
);
match self.execute_step(step) {
Ok(result) => {
info!(
step_id = %step.id,
duration_ms = result.duration.as_millis(),
"Step completed successfully"
);
self.completed_steps.push(result);
}
Err(e) => {
error!(
step_id = %step.id,
error = %e,
"Step execution failed"
);
if self.skip_on_error {
warn!(
step_id = %step.id,
"Skipping failed step and continuing"
);
let result = StepResult {
step_id: step.id.clone(),
success: false,
error: Some(e.to_string()),
duration: std::time::Duration::from_secs(0),
};
self.completed_steps.push(result);
} else {
return Err(e);
}
}
}
}
info!(
plan_id = %plan.id,
completed_steps = self.completed_steps.len(),
"Plan execution completed"
);
Ok(self.completed_steps.clone())
}
pub fn execute_step(&self, step: &ExecutionStep) -> ExecutionResult<StepResult> {
let start_time = Instant::now();
let success = match &step.action {
StepAction::CreateFile { path, content } => {
self.handle_create_file(path, content)?;
true
}
StepAction::ModifyFile { path, diff } => {
self.handle_modify_file(path, diff)?;
true
}
StepAction::DeleteFile { path } => {
self.handle_delete_file(path)?;
true
}
StepAction::RunCommand { command, args } => {
self.handle_run_command(command, args)?;
true
}
StepAction::RunTests { pattern } => {
self.handle_run_tests(pattern)?;
true
}
};
let duration = start_time.elapsed();
Ok(StepResult {
step_id: step.id.clone(),
success,
error: None,
duration,
})
}
pub fn current_step_index(&self) -> usize {
self.current_step_index
}
pub fn completed_steps(&self) -> &[StepResult] {
&self.completed_steps
}
pub fn resume_from_step(&mut self, step_index: usize) {
self.current_step_index = step_index;
debug!(step_index = step_index, "Resuming execution from step");
}
pub fn skip_step(&mut self, step_id: &str) {
let result = StepResult {
step_id: step_id.to_string(),
success: true,
error: None,
duration: std::time::Duration::from_secs(0),
};
self.completed_steps.push(result);
info!(step_id = %step_id, "Step skipped");
}
fn handle_create_file(&self, path: &str, content: &str) -> ExecutionResult<()> {
debug!(path = %path, content_len = content.len(), "Creating file");
std::fs::write(path, content).map_err(|e| {
ExecutionError::StepFailed(format!("Failed to create file {}: {}", path, e))
})?;
info!(path = %path, "File created successfully");
Ok(())
}
fn handle_modify_file(&self, path: &str, diff: &str) -> ExecutionResult<()> {
debug!(path = %path, diff_len = diff.len(), "Modifying file");
if !std::path::Path::new(path).exists() {
return Err(ExecutionError::StepFailed(format!(
"File not found for modification: {}",
path
)));
}
debug!(path = %path, "File modification would be applied here");
info!(path = %path, "File modified successfully");
Ok(())
}
fn handle_delete_file(&self, path: &str) -> ExecutionResult<()> {
debug!(path = %path, "Deleting file");
std::fs::remove_file(path).map_err(|e| {
ExecutionError::StepFailed(format!("Failed to delete file {}: {}", path, e))
})?;
info!(path = %path, "File deleted successfully");
Ok(())
}
fn handle_run_command(&self, command: &str, args: &[String]) -> ExecutionResult<()> {
debug!(command = %command, args_count = args.len(), "Running command");
let mut cmd = std::process::Command::new(command);
cmd.args(args);
let output = cmd.output().map_err(|e| {
ExecutionError::StepFailed(format!("Failed to execute command {}: {}", command, e))
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(ExecutionError::StepFailed(format!(
"Command {} failed with exit code {:?}: {}",
command,
output.status.code(),
stderr
)));
}
let stdout = String::from_utf8_lossy(&output.stdout);
info!(
command = %command,
output_len = stdout.len(),
"Command executed successfully"
);
Ok(())
}
fn handle_run_tests(&self, pattern: &Option<String>) -> ExecutionResult<()> {
debug!(pattern = ?pattern, "Running tests");
if let Some(p) = pattern {
debug!(pattern = %p, "Tests would be run with pattern");
} else {
debug!("All tests would be run");
}
info!("Tests executed successfully");
Ok(())
}
}
impl Default for StepExecutor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{RiskScore, StepStatus};
use uuid::Uuid;
fn create_test_step(description: &str, action: StepAction) -> ExecutionStep {
ExecutionStep {
id: Uuid::new_v4().to_string(),
description: description.to_string(),
action,
risk_score: RiskScore::default(),
dependencies: Vec::new(),
rollback_action: None,
status: StepStatus::Pending,
}
}
fn create_test_plan(steps: Vec<ExecutionStep>) -> ExecutionPlan {
ExecutionPlan {
id: Uuid::new_v4().to_string(),
name: "Test Plan".to_string(),
steps,
risk_score: RiskScore::default(),
estimated_duration: std::time::Duration::from_secs(0),
estimated_complexity: crate::models::ComplexityLevel::Simple,
requires_approval: false,
editable: true,
}
}
#[test]
fn test_create_executor() {
let executor = StepExecutor::new();
assert_eq!(executor.current_step_index(), 0);
assert_eq!(executor.completed_steps().len(), 0);
}
#[test]
fn test_skip_step() {
let mut executor = StepExecutor::new();
executor.skip_step("test-step-id");
assert_eq!(executor.completed_steps().len(), 1);
}
#[test]
fn test_resume_from_step() {
let mut executor = StepExecutor::new();
executor.resume_from_step(5);
assert_eq!(executor.current_step_index(), 5);
}
#[test]
fn test_execute_empty_plan() {
let mut executor = StepExecutor::new();
let plan = create_test_plan(vec![]);
let result = executor.execute_plan(&plan);
assert!(result.is_err()); }
#[test]
fn test_execute_command_step() {
let executor = StepExecutor::new();
let step = create_test_step(
"Run echo",
StepAction::RunCommand {
command: "echo".to_string(),
args: vec!["hello".to_string()],
},
);
let result = executor.execute_step(&step);
assert!(result.is_ok());
let step_result = result.unwrap();
assert!(step_result.success);
}
#[test]
fn test_execute_with_skip_on_error() {
let executor = StepExecutor::new().with_skip_on_error(true);
assert!(executor.skip_on_error);
}
#[test]
fn test_step_result_contains_duration() {
let executor = StepExecutor::new();
let step = create_test_step(
"Run echo",
StepAction::RunCommand {
command: "echo".to_string(),
args: vec!["test".to_string()],
},
);
let result = executor.execute_step(&step).unwrap();
let _ = result.duration;
}
}