test(node): pin transport restart backoff and BLE start re-entrancy

- Pure-policy tests on PendingTransport: backoff doubling, cap saturation
  (checked_shl/saturating_mul, never overflows or panics), never-gives-up
  past the cap, and the is_due boundary
- BleTransport::start_async re-entrancy after a listen failure: first call
  errs and leaves state Failed, second call succeeds and leaves state Up —
  the claim the whole supervisor rests on
- Document the operational-but-dark case (advertise/scan failures are
  logged and swallowed) as a Limitations note on start_async; only a
  listener failure is recovered by restart supervision
This commit is contained in:
Arjen
2026-08-05 10:36:28 +01:00
parent 8104849f09
commit 59028d6ece
2 changed files with 39 additions and 6 deletions
+10 -6
View File
@@ -268,10 +268,12 @@ mod node_integration_tests {
let io = MockBleIo::new("hci0", test_addr(1));
io.fail_next_listens(1);
let mut config = BleConfig::default();
config.accept_connections = Some(true);
config.scan = Some(false);
config.advertise = Some(false);
let config = BleConfig {
accept_connections: Some(true),
scan: Some(false),
advertise: Some(false),
..Default::default()
};
let (tx, _rx) = tokio::sync::mpsc::channel(64);
let mut transport = BleTransport::new(TransportId::new(1), None, config, io, tx);
@@ -309,8 +311,10 @@ mod node_integration_tests {
// errors out on bind-address parsing — reproduce that exact shape
// with an unparseable bind address, then feed the failed handle to
// `quarantine_transport` and confirm it is dropped, not retained.
let mut udp_config = crate::config::UdpConfig::default();
udp_config.bind_addr = Some("not-an-address".to_string());
let udp_config = crate::config::UdpConfig {
bind_addr: Some("not-an-address".to_string()),
..Default::default()
};
let (packet_tx, _packet_rx) = crate::transport::packet_channel(64);
let mut udp = crate::transport::udp::UdpTransport::new(
TransportId::new(3),
+29
View File
@@ -177,6 +177,17 @@ impl<I: BleIo> BleTransport<I> {
/// Start the transport asynchronously.
pub async fn start_async(&mut self) -> Result<(), TransportError> {
// Limitations: only a failure to begin listening marks the transport
// `Failed` and returns `Err` — that is the one case restart
// supervision (`node::transport_restart`) can recover. A failure to
// begin advertising or scanning, below, is logged and swallowed, and
// the transport still proceeds to the operational state — so a
// transport can report itself operational while neither advertising
// nor scanning is actually running underneath it. Closing this gap
// would mean re-running the advertise/scan calls on a transport that
// already owns a live acceptor and a spawned accept loop, which is a
// larger change than this fix covers, so it is deferred rather than
// attempted here.
if !self.state.can_start() {
return Err(TransportError::AlreadyStarted);
}
@@ -1163,6 +1174,24 @@ mod tests {
assert_eq!(transport.state(), TransportState::Down);
}
#[tokio::test]
async fn test_start_async_is_reentrant_after_listen_failure() {
// The whole restart supervisor rests on this: `can_start()`
// permitting a retry is worthless if `start_async` cannot actually
// run twice on the same transport.
let io = MockBleIo::new("hci0", test_addr(1));
io.fail_next_listens(1);
let (mut transport, _rx) = make_transport(io);
assert!(transport.start_async().await.is_err());
assert_eq!(transport.state(), TransportState::Failed);
// The injected failure was one-shot — the second call is a real
// retry, not a repeat of the same failure.
transport.start_async().await.unwrap();
assert_eq!(transport.state(), TransportState::Up);
}
#[tokio::test(start_paused = true)]
async fn test_scan_discovers_peers() {
let io = MockBleIo::new("hci0", test_addr(1));