Skip to main content

banc_host/
config.rs

1//! Rig topology configuration.
2//!
3//! A rig is described by a `banc-rig.toml`, located via the `BANC_RIG` env
4//! var or by searching from the current directory upward. Its absence is the
5//! signal that this machine has no hardware attached: suites self-skip.
6
7use serde::Deserialize;
8use std::path::{Path, PathBuf};
9
10pub const CONFIG_FILE: &str = "banc-rig.toml";
11pub const ENV_VAR: &str = "BANC_RIG";
12
13#[derive(Debug, Clone, Deserialize, Default)]
14#[serde(deny_unknown_fields)]
15pub struct RigConfig {
16    #[serde(default)]
17    pub rig: RigMeta,
18    pub target: Option<TargetConfig>,
19    #[serde(default, rename = "assistant")]
20    pub assistants: Vec<AssistantConfig>,
21    #[serde(default, rename = "instrument")]
22    pub instruments: Vec<InstrumentConfig>,
23}
24
25#[derive(Debug, Clone, Deserialize, Default)]
26#[serde(deny_unknown_fields)]
27pub struct RigMeta {
28    pub name: Option<String>,
29    /// Advisory lock file serializing rig access across processes (nextest
30    /// runs one process per test). Default: target/banc.lock next to the
31    /// config file.
32    pub lock_file: Option<PathBuf>,
33    /// Network lease server on the rig daemon. When set, it replaces the
34    /// flock entirely: runners on other machines contend for the same rig,
35    /// so a local file cannot be the arbiter.
36    pub lease: Option<LeaseConfig>,
37}
38
39/// Where and how to take the network rig lease.
40#[derive(Debug, Clone, Deserialize)]
41#[serde(deny_unknown_fields)]
42pub struct LeaseConfig {
43    /// host:port of the rig daemon.
44    pub addr: String,
45    /// File holding the shared token; relative paths resolve from the
46    /// rig-config directory.
47    pub token_file: PathBuf,
48}
49
50/// The device under test, driven via probe-rs.
51#[derive(Debug, Clone, Deserialize)]
52#[serde(deny_unknown_fields)]
53pub struct TargetConfig {
54    /// probe-rs target-database chip name, e.g. "STM32WL55JCIx".
55    pub chip: String,
56    /// Probe selector "VID:PID[:SERIAL]". None: the only probe attached.
57    pub probe: Option<String>,
58    /// probe-rs remote server (`probe-rs serve`), e.g. "https://pi:3000".
59    /// None: local USB probe via the probe-rs library.
60    pub probe_host: Option<String>,
61}
62
63/// An assistant node speaking banc-icd over postcard-rpc, reached over USB
64/// (serial/product match) or the network (addr + token).
65#[derive(Debug, Clone, Deserialize)]
66#[serde(deny_unknown_fields)]
67pub struct AssistantConfig {
68    /// Name tests use to look the node up, e.g. "a0".
69    pub name: String,
70    /// USB serial string (assistants surface their unique ID here).
71    pub serial: Option<String>,
72    /// USB product string to match when serial is not given.
73    pub product: Option<String>,
74    /// host:port of a network node (a rig daemon). Mutually exclusive with
75    /// the USB fields.
76    pub addr: Option<String>,
77    /// Token file for the network handshake; relative paths resolve from
78    /// the rig-config directory.
79    pub token_file: Option<PathBuf>,
80}
81
82/// A bench instrument. Drivers are matched on `kind` by the suite.
83#[derive(Debug, Clone, Deserialize)]
84#[serde(deny_unknown_fields)]
85pub struct InstrumentConfig {
86    pub name: String,
87    /// Driver key, e.g. "rcdat", "scpi".
88    pub kind: String,
89    /// Free-form address: host:port, VISA resource, hidraw path...
90    pub address: Option<String>,
91    /// Driver-specific settings, passed through untouched.
92    #[serde(default)]
93    pub params: toml::Table,
94}
95
96impl RigConfig {
97    /// Find the rig config for this machine/checkout. `Ok(None)` means "no
98    /// rig here" (the self-skip signal); `Err` means a config exists but is
99    /// unusable, which is a real failure, not a skip.
100    pub fn locate() -> anyhow::Result<Option<PathBuf>> {
101        if let Ok(path) = std::env::var(ENV_VAR) {
102            let path = PathBuf::from(path);
103            anyhow::ensure!(
104                path.is_file(),
105                "{ENV_VAR} points at {} which does not exist",
106                path.display()
107            );
108            return Ok(Some(path));
109        }
110        let mut dir = std::env::current_dir()?;
111        loop {
112            let candidate = dir.join(CONFIG_FILE);
113            if candidate.is_file() {
114                return Ok(Some(candidate));
115            }
116            if !dir.pop() {
117                return Ok(None);
118            }
119        }
120    }
121
122    pub fn load(path: &Path) -> anyhow::Result<Self> {
123        let text = std::fs::read_to_string(path)?;
124        let config: RigConfig = toml::from_str(&text)
125            .map_err(|e| anyhow::anyhow!("parsing {}: {e}", path.display()))?;
126        Ok(config)
127    }
128
129    pub fn assistant(&self, name: &str) -> Option<&AssistantConfig> {
130        self.assistants.iter().find(|a| a.name == name)
131    }
132
133    pub fn instrument(&self, name: &str) -> Option<&InstrumentConfig> {
134        self.instruments.iter().find(|i| i.name == name)
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn parses_full_config() {
144        let cfg: RigConfig = toml::from_str(
145            r#"
146            [rig]
147            name = "bench-1"
148
149            [target]
150            chip = "RP2350"
151            probe = "2e8a:000c"
152
153            [[assistant]]
154            name = "a0"
155            serial = "0123456789ABCDEF"
156
157            [[instrument]]
158            name = "att0"
159            kind = "rcdat"
160            address = "192.168.1.50:23"
161            params = { max_db = 90.0 }
162            "#,
163        )
164        .unwrap();
165        assert_eq!(cfg.rig.name.as_deref(), Some("bench-1"));
166        assert_eq!(cfg.target.as_ref().unwrap().chip, "RP2350");
167        assert_eq!(cfg.assistant("a0").unwrap().serial.as_deref(), Some("0123456789ABCDEF"));
168        assert_eq!(cfg.instrument("att0").unwrap().kind, "rcdat");
169        assert!(cfg.assistant("nope").is_none());
170    }
171
172    #[test]
173    fn empty_config_is_valid() {
174        let cfg: RigConfig = toml::from_str("").unwrap();
175        assert!(cfg.target.is_none());
176        assert!(cfg.assistants.is_empty());
177    }
178}