ev/runner.rs
1//! ev check --run: execute a bound test locally and produce a run-receipt. A THIN runner —
2//! the production receipt-writer is CI / a supervisor hook; --run is for local verification.
3//! exit == the configured green_exit_code => green, anything else => red (gray comes from
4//! external writers, never from --run).
5use crate::receipt::Receipt;
6use std::path::Path;
7use std::process::Command;
8use time::format_description::well_known::Rfc3339;
9use time::OffsetDateTime;
10
11/// Run the bound `reference` as a shell command in `repo`; return a receipt stamped for
12/// `platform`, the current git commit (HEAD), and now (UTC). exit == `green_exit_code` => green,
13/// else red.
14pub fn run_check(
15 repo: &Path,
16 reference: &str,
17 platform: &str,
18 green_exit_code: i32,
19) -> Result<Receipt, String> {
20 let commit = crate::capture::resolve_sha(repo, &None)?;
21 let ran_at = OffsetDateTime::now_utc()
22 .format(&Rfc3339)
23 .map_err(|e| format!("timestamp: {e}"))?;
24 let status = Command::new("sh")
25 .arg("-c")
26 .arg(reference)
27 .current_dir(repo)
28 .status()
29 .map_err(|e| format!("cannot run {reference:?}: {e}"))?;
30 // 126 (not executable) / 127 (command not found) from `sh -c` mean the selector could not be
31 // EXECUTED as a command — a typo, a missing binary. That is NOT a clean pass/fail: treating it
32 // as `red` would let a broken counter-test "flip" against a passing check and read as a proven
33 // green (a false-green). Surface it as an error so the caller records it honestly (the check
34 // path → not-run; the counter-test path → unproven), never as a meaningful result.
35 // LIMIT (honest, unavoidable under `sh -c`): a command that *intentionally* exits 126/127 is
36 // indistinguishable from a missing one, so it is treated as un-executed too. We err toward
37 // not-run/unproven (which GATE), never toward a false-green; a check should not use 126/127 as a
38 // meaningful exit code.
39 if matches!(status.code(), Some(126) | Some(127)) {
40 return Err(format!(
41 "{reference:?} could not be executed (exit {})",
42 status.code().unwrap_or(127)
43 ));
44 }
45 // exit == the configured green code is green; anything else (incl. signal kills) is red.
46 let result = if status.code() == Some(green_exit_code) {
47 "green"
48 } else {
49 "red"
50 };
51 Ok(Receipt {
52 test: reference.to_string(),
53 platform: platform.to_string(),
54 commit,
55 ran_at,
56 result: result.to_string(),
57 falsifiable: None,
58 })
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 // A fresh git repo (with one empty commit, so HEAD resolves) for resolve_sha.
66 fn git_repo() -> std::path::PathBuf {
67 use std::sync::atomic::{AtomicU64, Ordering};
68 static N: AtomicU64 = AtomicU64::new(0);
69 let p = std::env::temp_dir().join(format!(
70 "ev-runner-{}-{}",
71 std::process::id(),
72 N.fetch_add(1, Ordering::Relaxed)
73 ));
74 let _ = std::fs::remove_dir_all(&p);
75 std::fs::create_dir_all(&p).unwrap();
76 for args in [
77 ["init"].as_slice(),
78 ["config", "user.email", "t@e.st"].as_slice(),
79 ["config", "user.name", "Tester"].as_slice(),
80 ["commit", "--allow-empty", "-m", "init"].as_slice(),
81 ] {
82 Command::new("git")
83 .args(args)
84 .current_dir(&p)
85 .output()
86 .unwrap();
87 }
88 p
89 }
90
91 #[test]
92 fn run_check_should_record_green_when_the_command_exits_zero() {
93 // given: a git repo and a bound command that succeeds
94 let repo = git_repo();
95
96 // when: the bound ref is run on platform "local"
97 let r = run_check(&repo, "true", "local", 0).expect("ok");
98
99 // then: the receipt is green for that platform, test, and a 40-hex commit
100 assert_eq!(r.result, "green");
101 assert_eq!(r.platform, "local");
102 assert_eq!(r.test, "true");
103 assert_eq!(r.commit.len(), 40);
104 }
105
106 #[test]
107 fn run_check_should_record_red_when_the_command_exits_nonzero() {
108 // given: a git repo and a bound command that fails
109 let repo = git_repo();
110
111 // when: the bound ref is run
112 let r = run_check(&repo, "false", "local", 0).expect("ok");
113
114 // then: the receipt is red
115 assert_eq!(r.result, "red");
116 }
117
118 #[test]
119 fn run_check_should_error_when_the_command_cannot_execute() {
120 // given: a git repo and a selector that is not a runnable command (sh exits 127)
121 let repo = git_repo();
122
123 // when: the bound ref is run
124 let r = run_check(&repo, "this_is_not_a_real_command_xyz123", "local", 0);
125
126 // then: it is an Err (could-not-execute), NOT a clean red — so a green check is never paired
127 // with a counter that merely failed to run (which would read as a false-green "proven")
128 assert!(
129 r.is_err(),
130 "a non-runnable selector must error, not return a red receipt"
131 );
132 }
133}