Skip to main content

bn/commands/
verify.rs

1use std::io::Read;
2use std::path::Path;
3use std::process::{Command as ShellCommand, Stdio};
4use std::time::{Duration, Instant};
5
6use anyhow::{anyhow, Context, Result};
7
8use crate::bean::Bean;
9use crate::config::Config;
10use crate::discovery::find_bean_file;
11
12/// Run the verify command for a bean without closing it.
13///
14/// Returns `Ok(true)` if the command exits 0, `Ok(false)` if non-zero or timed out.
15/// If no verify command is set, prints a message and returns `Ok(true)`.
16/// Respects `verify_timeout` from the bean or project config.
17pub fn cmd_verify(beans_dir: &Path, id: &str) -> Result<bool> {
18    let bean_path = find_bean_file(beans_dir, id).map_err(|_| anyhow!("Bean not found: {}", id))?;
19
20    let bean =
21        Bean::from_file(&bean_path).with_context(|| format!("Failed to load bean: {}", id))?;
22
23    let verify_cmd = match &bean.verify {
24        Some(cmd) => cmd.clone(),
25        None => {
26            println!("no verify command set for bean {}", id);
27            return Ok(true);
28        }
29    };
30
31    // Determine effective timeout: bean overrides config.
32    let config = Config::load(beans_dir).ok();
33    let timeout_secs =
34        bean.effective_verify_timeout(config.as_ref().and_then(|c| c.verify_timeout));
35
36    // Run in the project root (parent of .beans/)
37    let project_root = beans_dir
38        .parent()
39        .ok_or_else(|| anyhow!("Cannot determine project root from beans dir"))?;
40
41    println!("Running: {}", verify_cmd);
42    if let Some(secs) = timeout_secs {
43        println!("Timeout: {}s", secs);
44    }
45
46    let mut child = ShellCommand::new("sh")
47        .args(["-c", &verify_cmd])
48        .current_dir(project_root)
49        .stdout(Stdio::piped())
50        .stderr(Stdio::piped())
51        .spawn()
52        .with_context(|| format!("Failed to spawn verify command: {}", verify_cmd))?;
53
54    // Drain output in background threads to prevent pipe deadlock.
55    let stdout_thread = {
56        let stdout = child.stdout.take().expect("stdout is piped");
57        std::thread::spawn(move || {
58            let mut buf = Vec::new();
59            let mut reader = std::io::BufReader::new(stdout);
60            let _ = reader.read_to_end(&mut buf);
61            String::from_utf8_lossy(&buf).to_string()
62        })
63    };
64    let stderr_thread = {
65        let stderr = child.stderr.take().expect("stderr is piped");
66        std::thread::spawn(move || {
67            let mut buf = Vec::new();
68            let mut reader = std::io::BufReader::new(stderr);
69            let _ = reader.read_to_end(&mut buf);
70            String::from_utf8_lossy(&buf).to_string()
71        })
72    };
73
74    let timeout = timeout_secs.map(Duration::from_secs);
75    let start = Instant::now();
76
77    let (timed_out, exit_status) = loop {
78        match child
79            .try_wait()
80            .with_context(|| "Failed to poll verify process")?
81        {
82            Some(status) => break (false, Some(status)),
83            None => {
84                if let Some(limit) = timeout {
85                    if start.elapsed() >= limit {
86                        let _ = child.kill();
87                        let _ = child.wait();
88                        break (true, None);
89                    }
90                }
91                std::thread::sleep(Duration::from_millis(50));
92            }
93        }
94    };
95
96    let stdout_str = stdout_thread.join().unwrap_or_default();
97    let stderr_str = stderr_thread.join().unwrap_or_default();
98
99    // Print captured output so the user can see what happened.
100    if !stdout_str.trim().is_empty() {
101        print!("{}", stdout_str);
102    }
103    if !stderr_str.trim().is_empty() {
104        eprint!("{}", stderr_str);
105    }
106
107    if timed_out {
108        println!(
109            "Verify timed out after {}s for bean {}",
110            timeout_secs.unwrap_or(0),
111            id
112        );
113        return Ok(false);
114    }
115
116    let status = exit_status.expect("exit_status is Some when not timed_out");
117    if status.success() {
118        println!("Verify passed for bean {}", id);
119        Ok(true)
120    } else {
121        println!("Verify failed for bean {}", id);
122        Ok(false)
123    }
124}