dk-runner 0.2.70

dkod verification runner — CI/CD pipeline execution
Documentation
use std::collections::HashMap;
use std::path::Path;
use std::time::{Duration, Instant};
use super::{Executor, StepOutput, StepStatus};

pub struct ProcessExecutor;

impl ProcessExecutor {
    #[allow(clippy::new_without_default)]
    pub fn new() -> Self { Self }
}

const SAFE_ENV_VARS: &[&str] = &["PATH", "HOME", "LANG", "TERM", "USER", "SHELL"];

#[async_trait::async_trait]
impl Executor for ProcessExecutor {
    async fn run_command(
        &self,
        command: &str,
        work_dir: &Path,
        timeout: Duration,
        env: &HashMap<String, String>,
    ) -> StepOutput {
        let start = Instant::now();
        let mut cmd = tokio::process::Command::new("sh");
        cmd.arg("-c").arg(command);
        cmd.current_dir(work_dir);
        cmd.env_clear();
        for var in SAFE_ENV_VARS {
            if let Ok(val) = std::env::var(var) {
                cmd.env(var, val);
            }
        }
        for (k, v) in env {
            cmd.env(k, v);
        }
        let result = tokio::time::timeout(timeout, cmd.output()).await;
        let duration = start.elapsed();
        match result {
            Ok(Ok(output)) => {
                let stdout = String::from_utf8_lossy(&output.stdout).to_string();
                let stderr = String::from_utf8_lossy(&output.stderr).to_string();
                let status = if output.status.success() { StepStatus::Pass } else { StepStatus::Fail };
                StepOutput { status, stdout, stderr, duration }
            }
            Ok(Err(e)) => StepOutput {
                status: StepStatus::Fail, stdout: String::new(),
                stderr: format!("command error: {e}"), duration,
            },
            Err(_) => StepOutput {
                status: StepStatus::Timeout, stdout: String::new(),
                stderr: format!("command timed out after {}s", timeout.as_secs()), duration,
            },
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_echo_passes() {
        let exec = ProcessExecutor::new();
        let dir = std::env::temp_dir();
        let out = exec.run_command("echo hello", &dir, Duration::from_secs(5), &HashMap::new()).await;
        assert_eq!(out.status, StepStatus::Pass);
        assert!(out.stdout.contains("hello"));
    }

    #[tokio::test]
    async fn test_false_fails() {
        let exec = ProcessExecutor::new();
        let dir = std::env::temp_dir();
        let out = exec.run_command("false", &dir, Duration::from_secs(5), &HashMap::new()).await;
        assert_eq!(out.status, StepStatus::Fail);
    }

    #[tokio::test]
    async fn test_timeout() {
        let exec = ProcessExecutor::new();
        let dir = std::env::temp_dir();
        let out = exec.run_command("sleep 10", &dir, Duration::from_millis(100), &HashMap::new()).await;
        assert_eq!(out.status, StepStatus::Timeout);
    }

    #[tokio::test]
    async fn test_env_injection() {
        let exec = ProcessExecutor::new();
        let dir = std::env::temp_dir();
        let mut env = HashMap::new();
        env.insert("DKOD_TEST".to_string(), "yes".to_string());
        let out = exec.run_command("echo $DKOD_TEST", &dir, Duration::from_secs(5), &env).await;
        assert_eq!(out.status, StepStatus::Pass);
        assert!(out.stdout.contains("yes"));
    }
}