@@ -93,6 +93,53 @@ def fake_disconnect():
9393 with patch ("meshtastic.mesh_interface.MeshInterface.close" ), \
9494 patch .object (iface , "_disconnected" ):
9595 iface .close ()
96+ iface .close () # A completed close must also remain idempotent.
9697
9798 assert disconnect_count == 1 , "disconnect() should be called exactly once"
9899 assert iface .client is None , "client should be cleaned up"
100+
101+
102+ @pytest .mark .unit
103+ @pytest .mark .parametrize ("failure" , ["join" , "disconnect" , "close" , "notification" ])
104+ def test_ble_close_can_retry_after_cleanup_failure (failure ) -> None :
105+ """A failed cleanup must not permanently disable close()."""
106+ iface = object .__new__ (BLEInterface )
107+ iface ._closing = False
108+ iface ._want_receive = True
109+ receive_thread = MagicMock ()
110+ iface ._receiveThread = receive_thread
111+ client = MagicMock ()
112+ iface .client = client
113+ iface ._exit_handler = lambda : None
114+ notification = MagicMock ()
115+ iface ._disconnected = notification
116+ failing_step = {
117+ "join" : receive_thread .join ,
118+ "disconnect" : client .disconnect ,
119+ "close" : client .close ,
120+ "notification" : notification ,
121+ }[failure ]
122+ failing_step .side_effect = RuntimeError ("cleanup failed" )
123+
124+ with patch ("meshtastic.mesh_interface.MeshInterface.close" ), \
125+ patch ("meshtastic.ble_interface.atexit.unregister" ) as unregister :
126+ with pytest .raises (RuntimeError , match = "cleanup failed" ):
127+ iface .close ()
128+
129+ assert iface ._closing is False
130+ if failure != "notification" :
131+ assert iface .client is client
132+ unregister .assert_not_called ()
133+
134+ failing_step .side_effect = None
135+ iface .close ()
136+ iface .close ()
137+
138+ assert iface ._closing is True
139+ assert iface .client is None
140+ assert iface ._receiveThread is None
141+ assert iface ._want_receive is False
142+ assert failing_step .call_count == 2
143+ unregister .assert_called_once_with (iface ._exit_handler )
144+ client .close .assert_called ()
145+ notification .assert_called ()
0 commit comments