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
13pub 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 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 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 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 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}