Skip to main content

max / makenotwork

4.8 KB · 173 lines History Blame Raw
1 use tokio::process::Command;
2 use tracing::instrument;
3
4 use crate::checks::parse;
5 use crate::config::TestsConfig;
6 use crate::types::{TestRun, TestSummary};
7
8 /// Returns `true` if every character in `filter` is in `[a-zA-Z0-9_:-]`.
9 /// An empty string is considered valid (no characters to reject).
10 pub fn validate_test_filter(filter: &str) -> bool {
11 filter.chars().all(|c| c.is_alphanumeric() || c == '_' || c == ':' || c == '-')
12 }
13
14 #[instrument(skip_all)]
15 pub async fn run_tests(
16 target_name: &str,
17 config: &TestsConfig,
18 filter: Option<&str>,
19 ) -> TestRun {
20 let started_at = chrono::Utc::now().to_rfc3339();
21 let start = std::time::Instant::now();
22
23 // Validate filter characters before appending to SSH command.
24 // Only allow alphanumeric, underscore, colon, dash — covers all valid Rust test filter patterns.
25 if let Some(f) = filter
26 && !validate_test_filter(f)
27 {
28 let finished_at = chrono::Utc::now().to_rfc3339();
29 let duration_secs = start.elapsed().as_secs() as i64;
30 return TestRun {
31 id: None,
32 target: target_name.to_string(),
33 started_at,
34 finished_at: Some(finished_at),
35 duration_secs: Some(duration_secs),
36 exit_code: None,
37 passed: false,
38 summary: TestSummary {
39 steps: vec![],
40 total_passed: None,
41 total_failed: None,
42 details: vec![],
43 },
44 raw_output: format!(
45 "Invalid filter: contains characters outside [a-zA-Z0-9_:-]. Got: {f}"
46 ),
47 filter: Some(f.to_string()),
48 };
49 }
50
51 let mut cmd_str = config.command.clone();
52 if let Some(f) = filter {
53 cmd_str.push(' ');
54 cmd_str.push_str(f);
55 }
56
57 let result = Command::new("ssh")
58 .arg("-o")
59 .arg("BatchMode=yes")
60 .arg("-o")
61 .arg(format!("ConnectTimeout={}", config.timeout_secs))
62 .arg(&config.ssh)
63 .arg("--")
64 .arg(&cmd_str)
65 .output()
66 .await;
67
68 let finished_at = chrono::Utc::now().to_rfc3339();
69 let duration_secs = start.elapsed().as_secs() as i64;
70
71 match result {
72 Ok(output) => {
73 let stdout = String::from_utf8_lossy(&output.stdout);
74 let stderr = String::from_utf8_lossy(&output.stderr);
75 let raw_output = format!("{stdout}{stderr}");
76
77 let exit_code = output.status.code();
78 let passed = output.status.success();
79 let summary = parse::parse_ci_output(&raw_output);
80
81 TestRun {
82 id: None,
83 target: target_name.to_string(),
84 started_at,
85 finished_at: Some(finished_at),
86 duration_secs: Some(duration_secs),
87 exit_code,
88 passed,
89 summary,
90 raw_output,
91 filter: filter.map(String::from),
92 }
93 }
94 Err(e) => TestRun {
95 id: None,
96 target: target_name.to_string(),
97 started_at,
98 finished_at: Some(finished_at),
99 duration_secs: Some(duration_secs),
100 exit_code: None,
101 passed: false,
102 summary: TestSummary {
103 steps: vec![],
104 total_passed: None,
105 total_failed: None,
106 details: vec![],
107 },
108 raw_output: format!("SSH connection failed: {e}"),
109 filter: filter.map(String::from),
110 },
111 }
112 }
113
114 #[cfg(test)]
115 mod tests {
116 use super::*;
117
118 #[test]
119 fn validate_test_filter_valid_simple() {
120 assert!(validate_test_filter("foo"));
121 }
122
123 #[test]
124 fn validate_test_filter_valid_module_path() {
125 assert!(validate_test_filter("foo::bar"));
126 }
127
128 #[test]
129 fn validate_test_filter_valid_underscore() {
130 assert!(validate_test_filter("foo_bar"));
131 }
132
133 #[test]
134 fn validate_test_filter_valid_dash() {
135 assert!(validate_test_filter("foo-bar"));
136 }
137
138 #[test]
139 fn validate_test_filter_valid_alphanumeric() {
140 assert!(validate_test_filter("a123"));
141 }
142
143 #[test]
144 fn validate_test_filter_empty_is_valid() {
145 assert!(validate_test_filter(""));
146 }
147
148 #[test]
149 fn validate_test_filter_rejects_semicolon() {
150 assert!(!validate_test_filter("foo;rm"));
151 }
152
153 #[test]
154 fn validate_test_filter_rejects_ampersand() {
155 assert!(!validate_test_filter("foo && bar"));
156 }
157
158 #[test]
159 fn validate_test_filter_rejects_pipe() {
160 assert!(!validate_test_filter("foo|bar"));
161 }
162
163 #[test]
164 fn validate_test_filter_rejects_subshell() {
165 assert!(!validate_test_filter("$(cmd)"));
166 }
167
168 #[test]
169 fn validate_test_filter_rejects_space() {
170 assert!(!validate_test_filter("foo bar"));
171 }
172 }
173