use std::collections::BTreeSet;
use std::path::PathBuf;
const ALLOWLIST: &[(&str, &str, &str)] = &[
(
"epid",
"Soft Channel",
"record-internal: is_soft_dtyp short-circuits device attach, so epid \
process() runs epid_soft::do_pid directly (epid.rs). The \
EpidSoftDeviceSupport DeviceSupport impl is vestigial for this row.",
),
(
"epid",
"Async Soft Channel",
"record-internal + KNOWN GAP (epid async TRIG readback): is_soft_dtyp \
short-circuits the device, so the sync do_pid runs with NO TRIG. The \
record's TRIG / ca_trig_pending readback is gated on the non-dbd DTYP \
string 'Epid Async Soft', so it is unreachable for this dbd DTYP. \
Tracked; not closed by registration.",
),
(
"epid",
"Fast Epid",
"app-parameterized: EpidFastDeviceSupport::new() builds an un-wired \
device; the asyn Float64 output port must be installed per-record via \
set_output_port(sync_io, reason) (an asyn handle, app-specific), like \
the Asyn Scaler / motor device support. Not a std_device_supports() \
drop-in.",
),
];
fn dbd_path() -> Option<PathBuf> {
if let Ok(modules) = std::env::var("EPICS_MODULES") {
let p = PathBuf::from(modules).join("std/stdApp/src/stdSupport.dbd");
if p.is_file() {
return Some(p);
}
}
let reference =
PathBuf::from("/Users/stevek/codes/epics-modules/std/stdApp/src/stdSupport.dbd");
reference.is_file().then_some(reference)
}
fn parse_device_rows(dbd: &str) -> Vec<(String, String)> {
let mut rows = Vec::new();
for line in dbd.lines() {
let line = line.trim();
let Some(rest) = line.strip_prefix("device(") else {
continue;
};
let Some(inner) = rest.strip_suffix(')') else {
continue;
};
let fields: Vec<&str> = inner.split(',').map(str::trim).collect();
if fields.len() != 4 {
continue;
}
let record_type = fields[0].to_string();
let dtyp = fields[3].trim_matches('"').to_string();
rows.push((record_type, dtyp));
}
rows
}
#[test]
fn stdsupport_device_rows_are_all_accounted_for() {
let Some(path) = dbd_path() else {
eprintln!(
"SKIP stdsupport_device_rows_are_all_accounted_for: stdSupport.dbd \
not found (set EPICS_MODULES or place the reference checkout). No \
device rows verified."
);
return;
};
let dbd = std::fs::read_to_string(&path).expect("read stdSupport.dbd");
let rows = parse_device_rows(&dbd);
assert!(
!rows.is_empty(),
"parsed zero device() rows from {} — parser or file format changed",
path.display()
);
let registered: BTreeSet<String> = std_rs::std_device_supports()
.into_iter()
.map(|(dtyp, _)| dtyp.to_string())
.collect();
let mut failures: Vec<String> = Vec::new();
let mut tracked: Vec<String> = Vec::new();
let mut covered_registered: BTreeSet<String> = BTreeSet::new();
for (record_type, dtyp) in &rows {
if let Some((_, _, reason)) = ALLOWLIST
.iter()
.find(|(rt, dt, _)| *rt == record_type.as_str() && *dt == dtyp.as_str())
{
tracked.push(format!("({record_type}, {dtyp:?}) — {reason}"));
} else if registered.contains(dtyp) {
covered_registered.insert(dtyp.clone());
} else {
failures.push(format!(
"({record_type}, {dtyp:?}) is neither registered in \
std_device_supports() nor on the (recordType, DTYP) allowlist \
— a std device row with no Rust coverage (the record's device \
never runs; VAL silently wrong). Register a factory or \
allowlist it with a reason."
));
}
}
for dtyp in ®istered {
if !covered_registered.contains(dtyp) {
failures.push(format!(
"std_device_supports() registers {dtyp:?} but no stdSupport.dbd \
device() row uses it — phantom registration or a renamed DTYP."
));
}
}
if !tracked.is_empty() {
eprintln!(
"stdsupport_device_rows_are_all_accounted_for: {} device row(s) \
covered outside std_device_supports() (tracked, with reasons):",
tracked.len()
);
for t in &tracked {
eprintln!(" - {t}");
}
}
assert!(
failures.is_empty(),
"stdSupport.dbd device rows unaccounted for:\n{}",
failures.join("\n")
);
}