diff --git a/src/node/transport_restart.rs b/src/node/transport_restart.rs index 85412b0..270ea7c 100644 --- a/src/node/transport_restart.rs +++ b/src/node/transport_restart.rs @@ -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), diff --git a/src/transport/ble/mod.rs b/src/transport/ble/mod.rs index 9925f6a..94c885d 100644 --- a/src/transport/ble/mod.rs +++ b/src/transport/ble/mod.rs @@ -177,6 +177,17 @@ impl BleTransport { /// 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));