1use serde::Serialize;
2use std::path::Path;
3
4#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
5pub struct TestRunner {
6 pub name: String,
7 pub command: String,
8 pub args: Vec<String>,
9}
10
11impl TestRunner {
12 pub fn display_command(&self) -> String {
13 std::iter::once(self.command.as_str())
14 .chain(self.args.iter().map(String::as_str))
15 .collect::<Vec<_>>()
16 .join(" ")
17 }
18}
19
20pub fn detect_test_runner(root: &Path) -> Option<TestRunner> {
21 if root.join("Cargo.toml").exists() {
22 return Some(TestRunner {
23 name: "cargo".into(),
24 command: "cargo".into(),
25 args: vec!["test".into(), "--all-targets".into()],
26 });
27 }
28 if root.join("package.json").exists() {
29 return Some(TestRunner {
30 name: "npm".into(),
31 command: "npm".into(),
32 args: vec!["test".into()],
33 });
34 }
35 if root.join("pyproject.toml").exists()
36 || root.join("pytest.ini").exists()
37 || root.join("setup.cfg").exists()
38 {
39 return Some(TestRunner {
40 name: "pytest".into(),
41 command: "python".into(),
42 args: vec!["-m".into(), "pytest".into()],
43 });
44 }
45 None
46}
47
48#[derive(Debug, Clone)]
50pub struct VerificationOutcome {
51 pub passed: bool,
52 pub summary: String,
53 pub command: String,
54}
55
56fn tail(s: &str, max: usize) -> String {
57 if s.len() <= max {
58 s.to_string()
59 } else {
60 format!("…{}", &s[s.len() - max..])
61 }
62}
63
64pub async fn run_verification(
72 root: &Path,
73 verify_command: Option<&str>,
74) -> Option<VerificationOutcome> {
75 use crate::sandbox::{Command, Limits, LocalSandbox, Sandbox};
76 use std::collections::HashMap;
77
78 let (program, args, display) = match verify_command.map(str::trim).filter(|c| !c.is_empty()) {
79 Some(cmd) => {
80 let (sh, flag) = if cfg!(windows) {
81 ("cmd", "/c")
82 } else {
83 ("sh", "-c")
84 };
85 (
86 sh.to_string(),
87 vec![flag.to_string(), cmd.to_string()],
88 cmd.to_string(),
89 )
90 }
91 None => {
92 let runner = detect_test_runner(root)?;
93 (
94 runner.command.clone(),
95 runner.args.clone(),
96 runner.display_command(),
97 )
98 }
99 };
100
101 let sandbox = LocalSandbox::new(root.to_path_buf());
102 let command = Command {
103 program,
104 args,
105 env: HashMap::new(),
106 workdir: root.to_path_buf(),
107 };
108 let limits = Limits {
109 timeout_ms: 180_000,
110 max_output_bytes: 256 * 1024,
111 };
112 let result = sandbox.exec(&command, &limits).await.ok()?;
113
114 let mut summary = String::new();
115 if !result.stdout.trim().is_empty() {
116 summary.push_str(&tail(result.stdout.trim(), 1500));
117 }
118 if !result.stderr.trim().is_empty() {
119 if !summary.is_empty() {
120 summary.push('\n');
121 }
122 summary.push_str(&tail(result.stderr.trim(), 1500));
123 }
124
125 Some(VerificationOutcome {
126 passed: result.exit_code == 0,
127 summary: summary.trim().to_string(),
128 command: display,
129 })
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn detects_cargo_runner_first() {
138 let temp = tempfile::tempdir().unwrap();
139 std::fs::write(temp.path().join("Cargo.toml"), "[package]\nname='x'\n").unwrap();
140 let runner = detect_test_runner(temp.path()).unwrap();
141 assert_eq!(runner.name, "cargo");
142 assert_eq!(runner.display_command(), "cargo test --all-targets");
143 }
144
145 #[test]
146 fn detects_node_and_python_runners() {
147 let node = tempfile::tempdir().unwrap();
148 std::fs::write(node.path().join("package.json"), "{}").unwrap();
149 assert_eq!(detect_test_runner(node.path()).unwrap().name, "npm");
150
151 let py = tempfile::tempdir().unwrap();
152 std::fs::write(py.path().join("pyproject.toml"), "[project]\nname='x'\n").unwrap();
153 assert_eq!(detect_test_runner(py.path()).unwrap().name, "pytest");
154 }
155
156 #[tokio::test]
157 async fn verification_runs_and_reports_exit_status() {
158 let dir = tempfile::tempdir().unwrap();
159 let ok = run_verification(dir.path(), Some("exit 0"))
161 .await
162 .expect("command was run");
163 assert!(ok.passed, "exit 0 → passed");
164
165 let bad = run_verification(dir.path(), Some("exit 1"))
166 .await
167 .expect("command was run");
168 assert!(!bad.passed, "exit 1 → failed");
169
170 assert!(run_verification(dir.path(), None).await.is_none());
172
173 assert!(run_verification(dir.path(), Some(" ")).await.is_none());
175 }
176}