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