11import asyncio
2- from unittest import mock
2+ import logging
33
4+ from asynctest import CoroutineMock , mock
45import pytest
6+ import serial
7+ import zigpy .exceptions
58
69from zigpy_xbee import api as xbee_api , types as t , uart
710from zigpy_xbee .zigbee .application import ControllerApplication
@@ -25,9 +28,10 @@ async def test_connect(monkeypatch):
2528
2629
2730def test_close (api ):
28- api . _uart . close = mock . MagicMock ()
31+ uart = api . _uart
2932 api .close ()
30- assert api ._uart .close .call_count == 1
33+ assert api ._uart is None
34+ assert uart .close .call_count == 1
3135
3236
3337def test_commands ():
@@ -84,6 +88,22 @@ def mock_api_frame(name, *args):
8488 api ._uart .send .reset_mock ()
8589
8690
91+ @pytest .mark .asyncio
92+ async def test_command_not_connected (api ):
93+ api ._uart = None
94+
95+ def mock_api_frame (name , * args ):
96+ return mock .sentinel .api_frame_data , api ._seq
97+
98+ api ._api_frame = mock .MagicMock (side_effect = mock_api_frame )
99+
100+ for cmd , cmd_opts in xbee_api .COMMAND_REQUESTS .items ():
101+ with pytest .raises (zigpy .exceptions .APIException ):
102+ await api ._command (cmd , mock .sentinel .cmd_data )
103+ assert api ._api_frame .call_count == 0
104+ api ._api_frame .reset_mock ()
105+
106+
87107async def _test_at_or_queued_at_command (api , cmd , monkeypatch , do_reply = True ):
88108 monkeypatch .setattr (
89109 t , "serialize" , mock .MagicMock (return_value = mock .sentinel .serialize )
@@ -518,3 +538,131 @@ def test_handle_many_to_one_rri(api):
518538 ieee = t .EUI64 ([t .uint8_t (a ) for a in range (0 , 8 )])
519539 nwk = 0x1234
520540 api ._handle_many_to_one_rri (ieee , nwk , 0 )
541+
542+
543+ @pytest .mark .asyncio
544+ async def test_reconnect_multiple_disconnects (monkeypatch , caplog ):
545+ api = xbee_api .XBee ()
546+ dev = mock .sentinel .uart
547+ connect_mock = CoroutineMock ()
548+ connect_mock .return_value = asyncio .Future ()
549+ connect_mock .return_value .set_result (True )
550+ monkeypatch .setattr (uart , "connect" , connect_mock )
551+
552+ await api .connect (dev , 115200 )
553+
554+ caplog .set_level (logging .DEBUG )
555+ connected = asyncio .Future ()
556+ connected .set_result (mock .sentinel .uart_reconnect )
557+ connect_mock .reset_mock ()
558+ connect_mock .side_effect = [asyncio .Future (), connected ]
559+ api .connection_lost ("connection lost" )
560+ await asyncio .sleep (0.3 )
561+ api .connection_lost ("connection lost 2" )
562+ await asyncio .sleep (0.3 )
563+
564+ assert "Cancelling reconnection attempt" in caplog .messages
565+ assert api ._uart is mock .sentinel .uart_reconnect
566+ assert connect_mock .call_count == 2
567+
568+
569+ @pytest .mark .asyncio
570+ async def test_reconnect_multiple_attempts (monkeypatch , caplog ):
571+ api = xbee_api .XBee ()
572+ dev = mock .sentinel .uart
573+ connect_mock = CoroutineMock ()
574+ connect_mock .return_value = asyncio .Future ()
575+ connect_mock .return_value .set_result (True )
576+ monkeypatch .setattr (uart , "connect" , connect_mock )
577+
578+ await api .connect (dev , 115200 )
579+
580+ caplog .set_level (logging .DEBUG )
581+ connected = asyncio .Future ()
582+ connected .set_result (mock .sentinel .uart_reconnect )
583+ connect_mock .reset_mock ()
584+ connect_mock .side_effect = [asyncio .TimeoutError , OSError , connected ]
585+
586+ with mock .patch ("asyncio.sleep" ):
587+ api .connection_lost ("connection lost" )
588+ await api ._conn_lost_task
589+
590+ assert api ._uart is mock .sentinel .uart_reconnect
591+ assert connect_mock .call_count == 3
592+
593+
594+ @pytest .mark .asyncio
595+ @mock .patch .object (xbee_api .XBee , "_at_command" , new_callable = CoroutineMock )
596+ @mock .patch .object (uart , "connect" )
597+ async def test_probe_success (mock_connect , mock_at_cmd ):
598+ """Test device probing."""
599+
600+ res = await xbee_api .XBee .probe (mock .sentinel .uart , mock .sentinel .baud )
601+ assert res is True
602+ assert mock_connect .call_count == 1
603+ assert mock_connect .await_count == 1
604+ assert mock_connect .call_args [0 ][0 ] is mock .sentinel .uart
605+ assert mock_at_cmd .call_count == 1
606+ assert mock_connect .return_value .close .call_count == 1
607+
608+
609+ @pytest .mark .asyncio
610+ @mock .patch .object (xbee_api .XBee , "init_api_mode" , return_value = True )
611+ @mock .patch .object (xbee_api .XBee , "_at_command" , side_effect = asyncio .TimeoutError )
612+ @mock .patch .object (uart , "connect" )
613+ async def test_probe_success_api_mode (mock_connect , mock_at_cmd , mock_api_mode ):
614+ """Test device probing."""
615+
616+ res = await xbee_api .XBee .probe (mock .sentinel .uart , mock .sentinel .baud )
617+ assert res is True
618+ assert mock_connect .call_count == 1
619+ assert mock_connect .await_count == 1
620+ assert mock_connect .call_args [0 ][0 ] is mock .sentinel .uart
621+ assert mock_at_cmd .call_count == 1
622+ assert mock_api_mode .call_count == 1
623+ assert mock_connect .return_value .close .call_count == 1
624+
625+
626+ @pytest .mark .asyncio
627+ @mock .patch .object (xbee_api .XBee , "init_api_mode" )
628+ @mock .patch .object (xbee_api .XBee , "_at_command" , side_effect = asyncio .TimeoutError )
629+ @mock .patch .object (uart , "connect" )
630+ @pytest .mark .parametrize (
631+ "exception" ,
632+ (asyncio .TimeoutError , serial .SerialException , zigpy .exceptions .APIException ),
633+ )
634+ async def test_probe_fail (mock_connect , mock_at_cmd , mock_api_mode , exception ):
635+ """Test device probing fails."""
636+
637+ mock_api_mode .side_effect = exception
638+ mock_api_mode .reset_mock ()
639+ mock_at_cmd .reset_mock ()
640+ mock_connect .reset_mock ()
641+ res = await xbee_api .XBee .probe (mock .sentinel .uart , mock .sentinel .baud )
642+ assert res is False
643+ assert mock_connect .call_count == 1
644+ assert mock_connect .await_count == 1
645+ assert mock_connect .call_args [0 ][0 ] is mock .sentinel .uart
646+ assert mock_at_cmd .call_count == 1
647+ assert mock_api_mode .call_count == 1
648+ assert mock_connect .return_value .close .call_count == 1
649+
650+
651+ @pytest .mark .asyncio
652+ @mock .patch .object (xbee_api .XBee , "init_api_mode" , return_value = False )
653+ @mock .patch .object (xbee_api .XBee , "_at_command" , side_effect = asyncio .TimeoutError )
654+ @mock .patch .object (uart , "connect" )
655+ async def test_probe_fail_api_mode (mock_connect , mock_at_cmd , mock_api_mode ):
656+ """Test device probing fails."""
657+
658+ mock_api_mode .reset_mock ()
659+ mock_at_cmd .reset_mock ()
660+ mock_connect .reset_mock ()
661+ res = await xbee_api .XBee .probe (mock .sentinel .uart , mock .sentinel .baud )
662+ assert res is False
663+ assert mock_connect .call_count == 1
664+ assert mock_connect .await_count == 1
665+ assert mock_connect .call_args [0 ][0 ] is mock .sentinel .uart
666+ assert mock_at_cmd .call_count == 1
667+ assert mock_api_mode .call_count == 1
668+ assert mock_connect .return_value .close .call_count == 1
0 commit comments