use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BackpressureConfig {
pub commands: Vec<String>,
#[serde(default = "default_stop_on_failure")]
pub stop_on_failure: bool,
#[serde(default = "default_timeout")]
pub timeout_secs: u64,
}
fn default_stop_on_failure() -> bool {
true
}
fn default_timeout() -> u64 {
300 }
impl BackpressureConfig {
pub fn load(project_root: Option<&PathBuf>) -> Result<Self> {
let root = project_root
.cloned()
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
let config_path = root.join(".scud").join("config.toml");
if !config_path.exists() {
return Ok(Self::auto_detect(&root));
}
let content = std::fs::read_to_string(&config_path)?;
let config: toml::Value = toml::from_str(&content)?;
if let Some(swarm) = config.get("swarm") {
if let Some(bp) = swarm.get("backpressure") {
let bp_config: BackpressureConfig = bp.clone().try_into()?;
return Ok(bp_config);
}
}
Ok(Self::auto_detect(&root))
}
fn auto_detect(root: &Path) -> Self {
let mut commands = Vec::new();
if root.join("Cargo.toml").exists() {
commands.push("cargo build".to_string());
commands.push("cargo test".to_string());
}
if root.join("package.json").exists() {
if let Ok(content) = std::fs::read_to_string(root.join("package.json")) {
if content.contains("\"build\"") {
commands.push("npm run build".to_string());
}
if content.contains("\"test\"") {
commands.push("npm test".to_string());
}
if content.contains("\"lint\"") {
commands.push("npm run lint".to_string());
}
if content.contains("\"typecheck\"") {
commands.push("npm run typecheck".to_string());
}
}
}
if (root.join("pyproject.toml").exists() || root.join("setup.py").exists())
&& (root.join("pytest.ini").exists() || root.join("pyproject.toml").exists())
{
commands.push("pytest".to_string());
}
if root.join("go.mod").exists() {
commands.push("go build ./...".to_string());
commands.push("go test ./...".to_string());
}
Self {
commands,
stop_on_failure: true,
timeout_secs: 300,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationResult {
pub all_passed: bool,
pub failures: Vec<String>,
pub results: Vec<CommandResult>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandResult {
pub command: String,
pub passed: bool,
pub exit_code: Option<i32>,
pub stdout: String,
pub stderr: String,
pub duration_secs: f64,
}
pub fn run_validation(working_dir: &Path, config: &BackpressureConfig) -> Result<ValidationResult> {
let mut results = Vec::new();
let mut failures = Vec::new();
let mut all_passed = true;
for cmd_str in &config.commands {
println!(" Running: {}", cmd_str);
let start = std::time::Instant::now();
let result = run_command(working_dir, cmd_str, config.timeout_secs);
let duration = start.elapsed().as_secs_f64();
match result {
Ok((exit_code, stdout, stderr)) => {
let passed = exit_code == 0;
if !passed {
all_passed = false;
failures.push(cmd_str.clone());
}
results.push(CommandResult {
command: cmd_str.clone(),
passed,
exit_code: Some(exit_code),
stdout: truncate_output(&stdout, 1000),
stderr: truncate_output(&stderr, 1000),
duration_secs: duration,
});
if !passed && config.stop_on_failure {
break;
}
}
Err(e) => {
all_passed = false;
failures.push(format!("{} (error: {})", cmd_str, e));
results.push(CommandResult {
command: cmd_str.clone(),
passed: false,
exit_code: None,
stdout: String::new(),
stderr: e.to_string(),
duration_secs: duration,
});
if config.stop_on_failure {
break;
}
}
}
}
Ok(ValidationResult {
all_passed,
failures,
results,
})
}
fn run_command(
working_dir: &Path,
cmd_str: &str,
timeout_secs: u64,
) -> Result<(i32, String, String)> {
use std::io::Read;
use std::process::Stdio;
use std::time::{Duration, Instant};
if cmd_str.trim().is_empty() {
anyhow::bail!("Empty command");
}
let mut child = Command::new("sh")
.arg("-c")
.arg(cmd_str)
.current_dir(working_dir)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let timeout = Duration::from_secs(timeout_secs);
let start = Instant::now();
let poll_interval = Duration::from_millis(100);
loop {
match child.try_wait()? {
Some(status) => {
let mut stdout = String::new();
let mut stderr = String::new();
if let Some(mut stdout_pipe) = child.stdout.take() {
let _ = stdout_pipe.read_to_string(&mut stdout);
}
if let Some(mut stderr_pipe) = child.stderr.take() {
let _ = stderr_pipe.read_to_string(&mut stderr);
}
let exit_code = status.code().unwrap_or(-1);
return Ok((exit_code, stdout, stderr));
}
None => {
if start.elapsed() > timeout {
let _ = child.kill();
let _ = child.wait(); anyhow::bail!(
"Command timed out after {} seconds: {}",
timeout_secs,
cmd_str
);
}
std::thread::sleep(poll_interval);
}
}
}
}
fn truncate_output(output: &str, max_len: usize) -> String {
if output.len() <= max_len {
output.to_string()
} else {
format!("{}...[truncated]", &output[..max_len])
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_auto_detect_rust() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
let config = BackpressureConfig::auto_detect(tmp.path());
assert!(config.commands.contains(&"cargo build".to_string()));
assert!(config.commands.contains(&"cargo test".to_string()));
}
#[test]
fn test_auto_detect_empty() {
let tmp = TempDir::new().unwrap();
let config = BackpressureConfig::auto_detect(tmp.path());
assert!(config.commands.is_empty());
}
#[test]
fn test_truncate_output() {
assert_eq!(truncate_output("short", 100), "short");
let long = "a".repeat(200);
let truncated = truncate_output(&long, 50);
assert!(truncated.contains("truncated"));
assert!(truncated.len() < 200);
}
#[test]
fn test_run_command_simple() {
let tmp = TempDir::new().unwrap();
let result = run_command(tmp.path(), "echo hello", 60);
assert!(result.is_ok());
let (exit_code, stdout, _stderr) = result.unwrap();
assert_eq!(exit_code, 0);
assert!(stdout.contains("hello"));
}
#[test]
fn test_run_command_with_quotes() {
let tmp = TempDir::new().unwrap();
let result = run_command(tmp.path(), "echo 'hello world'", 60);
assert!(result.is_ok());
let (exit_code, stdout, _stderr) = result.unwrap();
assert_eq!(exit_code, 0);
assert!(stdout.contains("hello world"));
}
#[test]
fn test_run_command_with_pipe() {
let tmp = TempDir::new().unwrap();
let result = run_command(tmp.path(), "echo hello | cat", 60);
assert!(result.is_ok());
let (exit_code, stdout, _stderr) = result.unwrap();
assert_eq!(exit_code, 0);
assert!(stdout.contains("hello"));
}
#[test]
fn test_run_command_empty() {
let tmp = TempDir::new().unwrap();
let result = run_command(tmp.path(), "", 60);
assert!(result.is_err());
}
#[test]
fn test_run_command_whitespace_only() {
let tmp = TempDir::new().unwrap();
let result = run_command(tmp.path(), " ", 60);
assert!(result.is_err());
}
#[test]
fn test_run_command_timeout() {
let tmp = TempDir::new().unwrap();
let result = run_command(tmp.path(), "sleep 5", 1);
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("timed out"));
}
}