From cb9c5f7214f2fa056e2911bbb02c48fd5a370e08 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Wed, 23 Sep 2026 03:59:47 +0000 Subject: [PATCH] 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. --- src/node/mod.rs | 55 +++++++++++++++++++++++++++++----- src/node/tests/unit.rs | 68 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 8 deletions(-) diff --git a/src/node/mod.rs b/src/node/mod.rs index aff6a43a..4963b40d 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -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 { self.transports diff --git a/src/node/tests/unit.rs b/src/node/tests/unit.rs index 5bc4ca87..500547e9 100644 --- a/src/node/tests/unit.rs +++ b/src/node/tests/unit.rs @@ -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;