choir_ci_conform/
choir-ci-conform.rs1use 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 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 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}