mdbook_validator/
command.rs1use anyhow::{Context, Result};
7use std::io::Write;
8use std::process::{Command, Output, Stdio};
9
10pub trait CommandRunner: Send + Sync {
15 fn run_script(
27 &self,
28 script_path: &str,
29 stdin_content: &str,
30 env_vars: &[(&str, &str)],
31 ) -> Result<Output>;
32}
33
34#[derive(Debug, Default, Clone, Copy)]
38pub struct RealCommandRunner;
39
40impl CommandRunner for RealCommandRunner {
41 fn run_script(
42 &self,
43 script_path: &str,
44 stdin_content: &str,
45 env_vars: &[(&str, &str)],
46 ) -> Result<Output> {
47 let mut cmd = Command::new("bash");
48 cmd.arg(script_path)
49 .stdin(Stdio::piped())
50 .stdout(Stdio::piped())
51 .stderr(Stdio::piped());
52
53 for (key, value) in env_vars {
55 cmd.env(*key, *value);
56 }
57
58 let mut child = cmd
59 .spawn()
60 .with_context(|| format!("Failed to spawn validator: {script_path}"))?;
61
62 if let Some(mut stdin) = child.stdin.take() {
67 if let Err(e) = stdin.write_all(stdin_content.as_bytes()) {
68 if e.kind() != std::io::ErrorKind::BrokenPipe {
70 return Err(e).context("Failed to write to validator stdin");
71 }
72 }
74 }
75
76 child
77 .wait_with_output()
78 .context("Failed to wait for validator")
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 #![allow(clippy::panic, clippy::expect_used, clippy::unwrap_used)]
85
86 use super::*;
87
88 #[test]
89 fn test_real_command_runner_default() {
90 let runner = RealCommandRunner;
91 let _ = runner;
93 }
94
95 #[test]
96 fn test_real_command_runner_clone() {
97 let runner = RealCommandRunner;
98 let cloned = runner;
99 let _ = cloned;
100 }
101
102 #[test]
103 fn test_run_script_success() {
104 let runner = RealCommandRunner;
105 let result = runner.run_script("tests/fixtures/echo_validator.sh", "", &[]);
107 assert!(result.is_ok());
108 }
109
110 #[test]
111 fn test_run_script_with_stdin() {
112 let runner = RealCommandRunner;
113 let result = runner.run_script("tests/fixtures/echo_validator.sh", "test input", &[]);
115 assert!(result.is_ok());
116 }
117
118 #[test]
119 fn test_run_script_with_env_vars() {
120 let runner = RealCommandRunner;
121 let result = runner.run_script(
123 "tests/fixtures/echo_validator.sh",
124 "{}",
125 &[("VALIDATOR_ASSERTIONS", "rows >= 1")],
126 );
127 assert!(result.is_ok());
128 let output = result.expect("run_script should succeed");
129 let stdout = String::from_utf8_lossy(&output.stdout);
130 assert!(
131 stdout.contains("rows >= 1"),
132 "Expected 'rows >= 1' in stdout: {stdout}"
133 );
134 }
135
136 #[test]
137 fn test_run_script_nonexistent_script() {
138 let runner = RealCommandRunner;
139 let result = runner.run_script("/nonexistent/script.sh", "", &[]);
141 assert!(result.is_ok()); let output = result.expect("run_script should succeed");
143 assert!(!output.status.success()); }
145}