Skip to main content

banc_host/
runner.rs

1//! libtest-mimic glue: async tests with fixture injection, honest runtime
2//! self-skip when no rig is present, and evidence attached to failures.
3//!
4//! Suites are `harness = false` test binaries whose `main` calls [`run`].
5//! Works under plain `cargo test` and under cargo-nextest (which runs each
6//! test in its own process — hence the file lock in [`crate::rig`]).
7
8use crate::evidence::Evidence;
9use crate::rig::{Acquire, Rig};
10use libtest_mimic::{Arguments, Completion, Failed, Trial};
11use std::pin::Pin;
12use std::process::ExitCode;
13use std::sync::{Arc, OnceLock};
14
15pub struct TestCx {
16    pub rig: Arc<Rig>,
17    pub evidence: Evidence,
18}
19
20pub type TestFuture = Pin<Box<dyn Future<Output = Result<(), Failed>> + Send>>;
21
22pub struct BancTest {
23    name: String,
24    f: Box<dyn FnOnce(TestCx) -> TestFuture + Send>,
25}
26
27impl BancTest {
28    pub fn new(
29        name: impl Into<String>,
30        f: impl FnOnce(TestCx) -> TestFuture + Send + 'static,
31    ) -> Self {
32        BancTest { name: name.into(), f: Box::new(f) }
33    }
34}
35
36/// Outcome of the once-per-process rig acquisition, shared across trials.
37enum RigState {
38    Ready(Arc<Rig>),
39    Skip(String),
40    Fail(String),
41}
42
43pub fn run(tests: Vec<BancTest>) -> ExitCode {
44    let mut args = Arguments::from_args();
45    // Hardware is exclusive; never run trials concurrently in-process.
46    args.test_threads = Some(1);
47
48    let rt = Arc::new(
49        tokio::runtime::Builder::new_multi_thread()
50            .enable_all()
51            .build()
52            .expect("building tokio runtime"),
53    );
54    let rig_state: Arc<OnceLock<RigState>> = Arc::new(OnceLock::new());
55
56    let trials: Vec<Trial> = tests
57        .into_iter()
58        .map(|test| {
59            let rt = rt.clone();
60            let rig_state = rig_state.clone();
61            let name = test.name.clone();
62            Trial::ignorable_test(test.name, move || {
63                let state = rig_state.get_or_init(|| match rt.block_on(Rig::acquire()) {
64                    Ok(rig) => RigState::Ready(Arc::new(rig)),
65                    Err(Acquire::Skip(reason)) => RigState::Skip(reason),
66                    Err(Acquire::Fail(e)) => RigState::Fail(format!("{e:#}")),
67                });
68                let rig = match state {
69                    RigState::Ready(rig) => rig.clone(),
70                    RigState::Skip(reason) => return Ok(Completion::ignored_with(reason.clone())),
71                    RigState::Fail(e) => return Err(Failed::from(format!("rig unavailable: {e}"))),
72                };
73                let evidence = Evidence::new(&name);
74                let cx = TestCx { rig: rig.clone(), evidence: evidence.clone() };
75                let result = rt.block_on((test.f)(cx));
76                match result {
77                    Ok(()) => Ok(Completion::Completed),
78                    Err(failed) => {
79                        let mut msg = failed
80                            .message()
81                            .map(|m| m.to_string())
82                            .unwrap_or_else(|| "test failed".to_owned());
83                        if !evidence.is_empty() {
84                            let dir = artifacts_dir(&rig);
85                            match evidence.persist(&dir) {
86                                Ok(path) => {
87                                    msg.push_str(&format!(
88                                        "\n--- evidence (tail) ---\n{}full log: {}",
89                                        evidence.tail(40),
90                                        path.display()
91                                    ));
92                                }
93                                Err(e) => {
94                                    msg.push_str(&format!(
95                                        "\n--- evidence (tail; persist failed: {e}) ---\n{}",
96                                        evidence.tail(40)
97                                    ));
98                                }
99                            }
100                        }
101                        Err(Failed::from(msg))
102                    }
103                }
104            })
105        })
106        .collect();
107
108    let conclusion = libtest_mimic::run(&args, trials);
109    conclusion.exit_code()
110}
111
112fn artifacts_dir(rig: &Rig) -> std::path::PathBuf {
113    std::env::var_os("BANC_ARTIFACTS")
114        .map(std::path::PathBuf::from)
115        .unwrap_or_else(|| rig.base_dir.join("target").join("banc-artifacts"))
116}