max / makenotwork
1 file changed,
+33 insertions,
-5 deletions
| @@ -54,7 +54,13 @@ pub async fn run_tests( | |||
| 54 | 54 | cmd_str.push_str(f); | |
| 55 | 55 | } | |
| 56 | 56 | ||
| 57 | - | let result = Command::new("ssh") | |
| 57 | + | // `timeout_secs` is documented as "max seconds before killing the test | |
| 58 | + | // command," but was only wired to SSH ConnectTimeout — which bounds the | |
| 59 | + | // handshake, not a connected-then-hung remote command (fuzz-2026-07-06). Wrap | |
| 60 | + | // the whole run in a total timeout and `kill_on_drop` so the ssh child is | |
| 61 | + | // reaped when it elapses, honoring the field's contract. | |
| 62 | + | let mut command = Command::new("ssh"); | |
| 63 | + | command | |
| 58 | 64 | .arg("-o") | |
| 59 | 65 | .arg("BatchMode=yes") | |
| 60 | 66 | .arg("-o") | |
| @@ -62,14 +68,36 @@ pub async fn run_tests( | |||
| 62 | 68 | .arg(&config.ssh) | |
| 63 | 69 | .arg("--") | |
| 64 | 70 | .arg(&cmd_str) | |
| 65 | - | .output() | |
| 66 | - | .await; | |
| 71 | + | .kill_on_drop(true); | |
| 72 | + | ||
| 73 | + | let total_timeout = std::time::Duration::from_secs(config.timeout_secs.max(1)); | |
| 74 | + | let result = tokio::time::timeout(total_timeout, command.output()).await; | |
| 67 | 75 | ||
| 68 | 76 | let finished_at = chrono::Utc::now().to_rfc3339(); | |
| 69 | 77 | let duration_secs = start.elapsed().as_secs() as i64; | |
| 70 | 78 | ||
| 71 | 79 | match result { | |
| 72 | - | Ok(output) => { | |
| 80 | + | Err(_elapsed) => TestRun { | |
| 81 | + | id: None, | |
| 82 | + | target: target_name.to_string(), | |
| 83 | + | started_at, | |
| 84 | + | finished_at: Some(finished_at), | |
| 85 | + | duration_secs: Some(duration_secs), | |
| 86 | + | exit_code: None, | |
| 87 | + | passed: false, | |
| 88 | + | summary: TestSummary { | |
| 89 | + | steps: vec![], | |
| 90 | + | total_passed: None, | |
| 91 | + | total_failed: None, | |
| 92 | + | details: vec![], | |
| 93 | + | }, | |
| 94 | + | raw_output: format!( | |
| 95 | + | "SSH test command timed out after {}s (killed)", | |
| 96 | + | config.timeout_secs | |
| 97 | + | ), | |
| 98 | + | filter: filter.map(String::from), | |
| 99 | + | }, | |
| 100 | + | Ok(Ok(output)) => { | |
| 73 | 101 | let stdout = String::from_utf8_lossy(&output.stdout); | |
| 74 | 102 | let stderr = String::from_utf8_lossy(&output.stderr); | |
| 75 | 103 | let raw_output = format!("{stdout}{stderr}"); | |
| @@ -91,7 +119,7 @@ pub async fn run_tests( | |||
| 91 | 119 | filter: filter.map(String::from), | |
| 92 | 120 | } | |
| 93 | 121 | } | |
| 94 | - | Err(e) => TestRun { | |
| 122 | + | Ok(Err(e)) => TestRun { | |
| 95 | 123 | id: None, | |
| 96 | 124 | target: target_name.to_string(), | |
| 97 | 125 | started_at, |