Skip to main content

choir_ci_conform/
choir-ci-conform.rs

1//! Runs the D18 executor conformance suite against a helper argv.
2//!
3//! `choir-ci-local` is gated by `cargo test`, because cargo can build
4//! it. The helper the seam exists for cannot be gated that way: a
5//! Firecracker or Cloud Hypervisor driver needs KVM, so it lives on a
6//! Linux machine and quite possibly in another repository, and a suite
7//! that only runs against what this workspace links would let it ship
8//! ungated. This binary is the suite with the helper as an argument.
9//!
10//! ```text
11//! choir-ci-conform [options] -- <helper> [helper args...]
12//!
13//!   --subject <hex>          subject for every fixture job, so a helper
14//!                            that materializes the subject is given one
15//!                            it can materialize (default: a blake3 hash
16//!                            of "conform", which suits a helper that
17//!                            ignores the field)
18//!   --shell <path>           what runs the fixture scripts on the far
19//!                            side (default: /bin/sh)
20//!   --deadline-ms <n>        deadline for the jobs that must finish
21//!                            (default: 600000); raise it for a helper
22//!                            that boots a machine per job
23//!   --slow-deadline-ms <n>   deadline for the job that must time out
24//!                            (default: 100)
25//!   --probe-dir <dir>        a directory the far side can see, holding
26//!                            nothing; enables the Job::directory check,
27//!                            which is skipped without it
28//! ```
29//!
30//! Exit 0 when every check that ran passed, 1 when any failed, 2 on a
31//! usage error. Skipped checks do not fail the run and are printed as
32//! what they are: the report says what it could not establish rather
33//! than counting it as evidence.
34
35use choir_queue::conform::{conform, Fixtures, Outcome};
36use choir_queue::executor::Job;
37use choir_queue::remote::ProtocolRunner;
38use std::time::Duration;
39
40const MARKER: &str = "choir-conform-marker";
41
42fn main() {
43    let args: Vec<String> = std::env::args().skip(1).collect();
44    let opts = match parse(&args) {
45        Ok(o) => o,
46        Err(why) => {
47            eprintln!("choir-ci-conform: {why}");
48            eprintln!("usage: choir-ci-conform [options] -- <helper> [args...]");
49            std::process::exit(2);
50        }
51    };
52
53    let sh = |script: &str| {
54        let mut job = Job::new(
55            opts.subject.clone(),
56            vec![opts.shell.clone(), "-c".into(), script.into()],
57        );
58        job.deadline = opts.deadline;
59        job
60    };
61    let mut slow = sh("sleep 30");
62    slow.deadline = opts.slow_deadline;
63
64    let in_directory = opts.probe_dir.as_ref().map(|dir| {
65        if let Err(e) = std::fs::write(dir.join(MARKER), b"here") {
66            eprintln!("choir-ci-conform: cannot write the probe marker in {dir:?}: {e}");
67            std::process::exit(2);
68        }
69        // The probe passes only from inside that directory, so the
70        // verdict *is* the assertion about where the job ran.
71        let mut job = sh(&format!("test -e {MARKER}"));
72        job.directory = Some(dir.clone());
73        job
74    });
75
76    let checks = conform(
77        &mut ProtocolRunner::new(opts.helper.clone()),
78        Fixtures {
79            passing: sh("exit 0"),
80            failing: sh("exit 3"),
81            // A path that cannot exist, so the failure is at spawn.
82            erroring: Job::new(
83                opts.subject.clone(),
84                vec!["/nonexistent/choir-not-a-command".into()],
85            ),
86            slow,
87            in_directory,
88        },
89    );
90
91    let mut failed = 0;
92    let mut skipped = 0;
93    for check in &checks {
94        println!("{check}");
95        match check.outcome {
96            Outcome::Failed(_) => failed += 1,
97            Outcome::Skipped(_) => skipped += 1,
98            Outcome::Passed => {}
99        }
100    }
101    println!(
102        "{}: {} passed, {failed} failed, {skipped} skipped",
103        opts.helper[0],
104        checks.len() - failed - skipped
105    );
106    std::process::exit(i32::from(failed > 0));
107}
108
109struct Options {
110    helper: Vec<String>,
111    subject: choir_hash::ContentHash,
112    shell: String,
113    deadline: Duration,
114    slow_deadline: Duration,
115    probe_dir: Option<std::path::PathBuf>,
116}
117
118fn parse(args: &[String]) -> Result<Options, String> {
119    let mut opts = Options {
120        helper: Vec::new(),
121        subject: choir_hash::ContentHash::blake3(b"conform"),
122        shell: "/bin/sh".to_string(),
123        deadline: choir_queue::executor::DEFAULT_DEADLINE,
124        slow_deadline: Duration::from_millis(100),
125        probe_dir: None,
126    };
127    let mut i = 0;
128    while i < args.len() {
129        let value = |name: &str| {
130            args.get(i + 1)
131                .cloned()
132                .ok_or_else(|| format!("{name} needs a value"))
133        };
134        match args[i].as_str() {
135            "--" => {
136                opts.helper = args[i + 1..].to_vec();
137                break;
138            }
139            "--subject" => {
140                let hex = value("--subject")?;
141                opts.subject = choir_hash::ContentHash::from_hex(&hex)
142                    .ok_or_else(|| format!("--subject {hex} is not a content hash"))?;
143                i += 2;
144            }
145            "--shell" => {
146                opts.shell = value("--shell")?;
147                i += 2;
148            }
149            "--deadline-ms" => {
150                opts.deadline = Duration::from_millis(millis(&value("--deadline-ms")?)?);
151                i += 2;
152            }
153            "--slow-deadline-ms" => {
154                opts.slow_deadline = Duration::from_millis(millis(&value("--slow-deadline-ms")?)?);
155                i += 2;
156            }
157            "--probe-dir" => {
158                let dir = std::path::PathBuf::from(value("--probe-dir")?);
159                if !dir.is_dir() {
160                    return Err(format!("--probe-dir {dir:?} is not a directory"));
161                }
162                opts.probe_dir = Some(dir);
163                i += 2;
164            }
165            other => return Err(format!("unknown argument `{other}`")),
166        }
167    }
168    if opts.helper.is_empty() {
169        return Err("no helper: pass it after `--`".to_string());
170    }
171    Ok(opts)
172}
173
174fn millis(raw: &str) -> Result<u64, String> {
175    raw.parse()
176        .map_err(|_| format!("`{raw}` is not a number of milliseconds"))
177}