pub mod defaults;
pub mod fixtures;
pub mod probe;
pub mod rows;
use bacnet_types::enums::ObjectType;
use defaults::Defaults;
use probe::{take, CallLog, Probe};
use rows::{rows, COMMANDS, QUERIES};
use std::collections::BTreeSet;
const TRAIT_SOURCE: &str = include_str!("../../../src/traits.rs");
pub struct TraitMethod {
pub name: &'static str,
pub shared: bool,
pub required: bool,
}
pub fn trait_methods() -> Vec<TraitMethod> {
let start = TRAIT_SOURCE
.find("pub trait BACnetObject")
.expect("trait declared");
let body = &TRAIT_SOURCE[start..];
let body = &body[..body.find("\n}\n").expect("trait closes")];
for line in body.lines().skip(1) {
let Some(item) = line.strip_prefix(" ") else {
continue;
};
assert!(
item.is_empty()
|| item.starts_with(' ')
|| ["fn ", "//", "#[", ")", "}"]
.iter()
.any(|start| item.starts_with(start)),
"the BACnetObject reader does not understand `{}`",
item.trim()
);
}
let methods: Vec<_> = body
.match_indices("\n fn ")
.map(|(offset, marker)| declared(&body[offset + marker.len()..]))
.collect();
let method = |name| {
methods
.iter()
.find(|method| method.name == name)
.unwrap_or_else(|| panic!("BACnetObject::{name} not read"))
};
assert!(method("object_identifier").shared && method("object_identifier").required);
assert!(!method("write_property").shared && method("write_property").required);
assert!(method("is_list_property").shared && !method("is_list_property").required);
assert!(!method("audit_policy_authority_internal").shared);
methods
}
fn declared(signature: &'static str) -> TraitMethod {
let name = signature
.split(|c: char| !(c.is_alphanumeric() || c == '_'))
.next()
.expect("method name");
let open = signature.find('(').expect("parameter list");
let receiver = signature[open + 1..]
.split([',', ')'])
.next()
.expect("first parameter")
.trim();
assert!(receiver.ends_with("self"), "{name} takes no receiver");
let mut depth = 0;
let end = signature
.chars()
.find(|&c| {
match c {
'(' | '[' => depth += 1,
')' | ']' => depth -= 1,
_ => {}
}
depth == 0 && matches!(c, ';' | '{')
})
.expect("declaration ends");
TraitMethod {
name,
shared: receiver.starts_with('&') && !receiver.contains("mut"),
required: end == ';',
}
}
pub fn assert_rows_cover_the_trait() {
let methods = trait_methods();
let tables: [(&str, bool, Vec<&str>); 2] = [
("QUERIES", true, QUERIES.iter().map(|row| row.0).collect()),
(
"COMMANDS",
false,
COMMANDS.iter().map(|row| row.0).collect(),
),
];
for (table, shared, rows) in tables {
let named: BTreeSet<_> = rows.iter().copied().collect();
assert_eq!(named.len(), rows.len(), "{table} has two rows for a method");
let declared: BTreeSet<_> = methods
.iter()
.filter(|method| method.shared == shared)
.map(|method| method.name)
.collect();
let missing: Vec<_> = declared.difference(&named).collect();
let unknown: Vec<_> = named.difference(&declared).collect();
assert!(
missing.is_empty() && unknown.is_empty(),
"{table} in bacnet-objects/tests/support/forwarding/rows.rs does not match \
BACnetObject: no row for {missing:?}, no such method for {unknown:?}. Give a \
new method a probe answer and a row, then forward it in SourceReporter and, \
if it takes &self, in DeviceReadView"
);
}
}
pub fn assert_probe_answers_every_row() {
let required: BTreeSet<_> = trait_methods()
.into_iter()
.filter(|method| method.required)
.map(|method| method.name)
.collect();
for (name, row) in rows() {
let log = CallLog::default();
let answer = row.call(&mut Probe::new(ObjectType::ANALOG_VALUE, log.clone()));
let prefix = format!("{name}(");
assert!(
take(&log).iter().any(|call| call.starts_with(&prefix)),
"{name}: the probe must answer its row itself"
);
if !required.contains(name) && answer != "()" {
let probe = Probe::new(ObjectType::ANALOG_VALUE, CallLog::default());
assert_ne!(
row.call(&mut Defaults(probe)),
answer,
"{name}: the probe must answer unlike the trait default"
);
}
}
}