Skip to main content

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    // exit == the configured green code is green; anything else (incl. signal kills) is red.
31    let result = if status.code() == Some(green_exit_code) {
32        "green"
33    } else {
34        "red"
35    };
36    Ok(Receipt {
37        test: reference.to_string(),
38        platform: platform.to_string(),
39        commit,
40        ran_at,
41        result: result.to_string(),
42        falsifiable: None,
43    })
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    // A fresh git repo (with one empty commit, so HEAD resolves) for resolve_sha.
51    fn git_repo() -> std::path::PathBuf {
52        use std::sync::atomic::{AtomicU64, Ordering};
53        static N: AtomicU64 = AtomicU64::new(0);
54        let p = std::env::temp_dir().join(format!(
55            "ev-runner-{}-{}",
56            std::process::id(),
57            N.fetch_add(1, Ordering::Relaxed)
58        ));
59        let _ = std::fs::remove_dir_all(&p);
60        std::fs::create_dir_all(&p).unwrap();
61        for args in [
62            ["init"].as_slice(),
63            ["config", "user.email", "t@e.st"].as_slice(),
64            ["config", "user.name", "Tester"].as_slice(),
65            ["commit", "--allow-empty", "-m", "init"].as_slice(),
66        ] {
67            Command::new("git")
68                .args(args)
69                .current_dir(&p)
70                .output()
71                .unwrap();
72        }
73        p
74    }
75
76    #[test]
77    fn run_check_should_record_green_when_the_command_exits_zero() {
78        // given: a git repo and a bound command that succeeds
79        let repo = git_repo();
80
81        // when: the bound ref is run on platform "local"
82        let r = run_check(&repo, "true", "local", 0).expect("ok");
83
84        // then: the receipt is green for that platform, test, and a 40-hex commit
85        assert_eq!(r.result, "green");
86        assert_eq!(r.platform, "local");
87        assert_eq!(r.test, "true");
88        assert_eq!(r.commit.len(), 40);
89    }
90
91    #[test]
92    fn run_check_should_record_red_when_the_command_exits_nonzero() {
93        // given: a git repo and a bound command that fails
94        let repo = git_repo();
95
96        // when: the bound ref is run
97        let r = run_check(&repo, "false", "local", 0).expect("ok");
98
99        // then: the receipt is red
100        assert_eq!(r.result, "red");
101    }
102}