Warn when a configured BLE instance cannot be built on this platform

The only warning for a BLE block that nothing could build sat under
contradictory cfg gates: it was compiled only in test builds, where its
warn! was compiled out. So macOS, Windows, FreeBSD, musl builds, and
Android without an armed radio dropped a configured ble: block silently
while the node reported healthy.

A cfg-selected helper now says why the build cannot construct a BLE
instance, and create_transports warns once per configured instance with
that reason, as it already does for Ethernet. The BlueZ path's adapter
initialisation warning is unchanged.
This commit is contained in:
Johnathan Corgan
2026-09-23 19:35:30 +00:00
parent 8fe1d16fac
commit cb9c5f7214
2 changed files with 115 additions and 8 deletions
+47 -8
View File
@@ -1164,7 +1164,7 @@ impl Node {
}
// Create BLE transport instances
#[cfg(bluer_available)]
#[cfg(all(bluer_available, not(test)))]
{
let ble_instances: Vec<_> = self
.config()
@@ -1174,7 +1174,6 @@ impl Node {
.map(|(name, config)| (name.map(|s| s.to_string()), config.clone()))
.collect();
#[cfg(all(bluer_available, not(test)))]
for (name, ble_config) in ble_instances {
let transport_id = self.allocate_transport_id();
let adapter = ble_config.adapter().to_string();
@@ -1196,12 +1195,6 @@ impl Node {
}
}
}
#[cfg(any(not(bluer_available), test))]
if !ble_instances.is_empty() {
#[cfg(not(test))]
tracing::warn!("BLE transport configured but this build lacks BlueZ support");
}
}
// Create BLE transport instances over an embedder-supplied radio.
@@ -1230,10 +1223,56 @@ impl Node {
transports.push(TransportHandle::Ble(ble));
}
}
// `BleConfig` always parses, so on a build that cannot construct a
// BLE transport a configured `ble:` block would otherwise be dropped
// silently and the node would report healthy without it.
if let Some(reason) = self.ble_blocker() {
for (name, _) in self.config().transports.ble.iter() {
tracing::warn!(
instance = name.unwrap_or("default"),
reason,
"BLE transport unavailable; ignoring configured instance"
);
}
}
transports
}
/// Why this build cannot construct a configured BLE instance, or `None`
/// when it can.
///
/// The three arms are disjoint and together cover every build, so a
/// target matching none or two of them fails to compile rather than
/// guessing.
#[cfg(all(bluer_available, not(test)))]
fn ble_blocker(&self) -> Option<&'static str> {
None
}
/// Why this build cannot construct a configured BLE instance, or `None`
/// when it can. The embedder-supplied backend needs its radio slot armed
/// before `start()`.
#[cfg(all(target_os = "android", not(bluer_available), not(test)))]
fn ble_blocker(&self) -> Option<&'static str> {
if self.ble_radio.is_some() {
None
} else {
Some("no BLE radio was armed before start")
}
}
/// Why this build cannot construct a configured BLE instance: it has no
/// backend at all. A test build lands here too, since its BLE transport
/// is the in-memory double and is never built from config.
#[cfg(not(any(
all(bluer_available, not(test)),
all(target_os = "android", not(bluer_available), not(test))
)))]
fn ble_blocker(&self) -> Option<&'static str> {
Some("this build has no BLE backend")
}
/// Find an operational transport that matches the given transport type name.
fn find_transport_for_type(&self, transport_type: &str) -> Option<TransportId> {
self.transports
+68
View File
@@ -3630,6 +3630,74 @@ fn app_owned_ble_radio_seam_is_absent_until_armed() {
assert!(node.ble_radio.is_none());
}
/// A configured `ble:` block that this build cannot turn into a transport is
/// named in a warning, once per instance, rather than dropped silently.
///
/// `BleConfig` parses on every platform, so a node on a build with no BLE
/// backend (macOS, Windows, FreeBSD, musl, and a test build) would otherwise
/// start and report healthy with the configured radio simply absent. Not
/// `cfg`-gated: the warning is for exactly the builds that lack BLE.
#[tokio::test]
async fn configured_ble_instances_each_draw_a_warning_when_no_backend_can_build_them() {
let mut config = crate::Config::new();
config.node.control.enabled = false;
config.transports.ble = crate::config::TransportInstances::Named(
[
("alpha".to_string(), crate::config::BleConfig::default()),
("beta".to_string(), crate::config::BleConfig::default()),
]
.into_iter()
.collect(),
);
let mut node = make_node_with(config);
let (tx, _rx) = packet_channel(8);
let (logs, guard) = crate::testutil::capture_logs_scoped();
let transports = node.create_transports(&tx).await;
drop(guard);
assert!(
transports.is_empty(),
"no transport can be built from a BLE block on this build",
);
let warnings = logs.warnings();
for instance in ["alpha", "beta"] {
let field = format!("instance=\"{instance}\"");
let hits = warnings
.iter()
.filter(|line| line.contains("ignoring configured instance") && line.contains(&field))
.count();
assert_eq!(
hits, 1,
"expected one warning naming BLE instance {instance}, got {warnings:?}",
);
}
}
/// The healthy side of the check above: with no `ble:` block there is nothing
/// to warn about, so the new warning must not fire on an ordinary config.
#[tokio::test]
async fn a_node_without_ble_config_draws_no_ble_warning() {
let mut config = crate::Config::new();
config.node.control.enabled = false;
let mut node = make_node_with(config);
let (tx, _rx) = packet_channel(8);
let (logs, guard) = crate::testutil::capture_logs_scoped();
let transports = node.create_transports(&tx).await;
drop(guard);
assert!(
transports.is_empty(),
"the default config builds no transport"
);
let warnings = logs.warnings();
assert!(
!warnings.iter().any(|line| line.contains("BLE transport")),
"no BLE block is configured, got {warnings:?}",
);
}
#[cfg(all(ble_available, any(target_os = "android", test)))]
mod test_radio {
use crate::transport::ble::addr::BleAddr;