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//!
8//! Each trial runs on its own tokio runtime, dropped when the trial ends:
9//! every task the test (or its fixtures' libraries) spawned is torn down
10//! before the next trial starts, so leaked tasks cannot hold sockets or
11//! other resources across tests. The [`Rig`] outlives all of them: it is
12//! acquired synchronously (no runtime in scope) and holds only
13//! runtime-independent resources — connections belong to the per-test
14//! fixtures.
15
16use crate::evidence::Evidence;
17use crate::rig::{Acquire, Rig};
18use libtest_mimic::{Arguments, Completion, Failed, Trial};
19use std::pin::Pin;
20use std::process::ExitCode;
21use std::sync::{Arc, OnceLock};
22
23pub struct TestCx {
24    pub rig: Arc<Rig>,
25    pub evidence: Evidence,
26}
27
28pub type TestFuture = Pin<Box<dyn Future<Output = Result<(), Failed>> + Send>>;
29
30pub struct BancTest {
31    name: String,
32    f: Box<dyn FnOnce(TestCx) -> TestFuture + Send>,
33}
34
35impl BancTest {
36    pub fn new(
37        name: impl Into<String>,
38        f: impl FnOnce(TestCx) -> TestFuture + Send + 'static,
39    ) -> Self {
40        BancTest { name: name.into(), f: Box::new(f) }
41    }
42}
43
44/// Outcome of the once-per-process rig acquisition, shared across trials.
45enum RigState {
46    Ready(Arc<Rig>),
47    Skip(String),
48    Fail(String),
49}
50
51pub fn run(tests: Vec<BancTest>) -> ExitCode {
52    let mut args = Arguments::from_args();
53    // Hardware is exclusive; never run trials concurrently in-process.
54    args.test_threads = Some(1);
55
56    let rig_state: Arc<OnceLock<RigState>> = Arc::new(OnceLock::new());
57
58    let trials: Vec<Trial> = tests
59        .into_iter()
60        .map(|test| {
61            let rig_state = rig_state.clone();
62            let name = test.name.clone();
63            Trial::ignorable_test(test.name, move || {
64                let state = rig_state.get_or_init(|| match Rig::acquire() {
65                    Ok(rig) => RigState::Ready(Arc::new(rig)),
66                    Err(Acquire::Skip(reason)) => RigState::Skip(reason),
67                    Err(Acquire::Fail(e)) => RigState::Fail(format!("{e:#}")),
68                });
69                let rig = match state {
70                    RigState::Ready(rig) => rig.clone(),
71                    RigState::Skip(reason) => return Ok(Completion::ignored_with(reason.clone())),
72                    RigState::Fail(e) => return Err(Failed::from(format!("rig unavailable: {e}"))),
73                };
74                let rt = tokio::runtime::Builder::new_multi_thread()
75                    .enable_all()
76                    .build()
77                    .expect("building tokio runtime");
78                let evidence = Evidence::new(&name);
79                let cx = TestCx { rig: rig.clone(), evidence: evidence.clone() };
80                let result = rt.block_on((test.f)(cx));
81                match result {
82                    Ok(()) => Ok(Completion::Completed),
83                    Err(failed) => {
84                        let mut msg = failed
85                            .message()
86                            .map(|m| m.to_string())
87                            .unwrap_or_else(|| "test failed".to_owned());
88                        if !evidence.is_empty() {
89                            let dir = artifacts_dir(&rig);
90                            match evidence.persist(&dir) {
91                                Ok(path) => {
92                                    msg.push_str(&format!(
93                                        "\n--- evidence (tail) ---\n{}full log: {}",
94                                        evidence.tail(40),
95                                        path.display()
96                                    ));
97                                }
98                                Err(e) => {
99                                    msg.push_str(&format!(
100                                        "\n--- evidence (tail; persist failed: {e}) ---\n{}",
101                                        evidence.tail(40)
102                                    ));
103                                }
104                            }
105                        }
106                        Err(Failed::from(msg))
107                    }
108                }
109            })
110        })
111        .collect();
112
113    let conclusion = libtest_mimic::run(&args, trials);
114    conclusion.exit_code()
115}
116
117fn artifacts_dir(rig: &Rig) -> std::path::PathBuf {
118    std::env::var_os("BANC_ARTIFACTS")
119        .map(std::path::PathBuf::from)
120        .unwrap_or_else(|| rig.base_dir.join("target").join("banc-artifacts"))
121}