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