| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 |
- """
- Unit tests for connection_manager parsing functions.
- Tests the pure functions that parse GRBL responses:
- - Machine position parsing (MPos and WPos formats)
- - Serial port listing/filtering
- """
- import pytest
- from unittest.mock import patch, MagicMock
- class TestParseMachinePosition:
- """Tests for parse_machine_position function."""
- def test_parse_machine_position_mpos_format(self):
- """Test parsing MPos format from GRBL status response."""
- from modules.connection.connection_manager import parse_machine_position
- response = "<Idle|MPos:100.500,-50.250,0.000|Bf:15,128>"
- result = parse_machine_position(response)
- assert result is not None
- assert result == (100.5, -50.25)
- def test_parse_machine_position_wpos_format(self):
- """Test parsing WPos format from GRBL status response."""
- from modules.connection.connection_manager import parse_machine_position
- response = "<Idle|WPos:0.000,19.000,0.000|Bf:15,128>"
- result = parse_machine_position(response)
- assert result is not None
- assert result == (0.0, 19.0)
- def test_parse_machine_position_prefers_mpos(self):
- """Test that MPos is preferred when both are present (rare but possible)."""
- from modules.connection.connection_manager import parse_machine_position
- # This response has both MPos and WPos - MPos should be used first
- response = "<Idle|MPos:10.0,20.0,0.0|WPos:5.0,10.0,0.0|Bf:15,128>"
- result = parse_machine_position(response)
- assert result is not None
- assert result == (10.0, 20.0)
- def test_parse_machine_position_invalid(self):
- """Test parsing returns None for invalid response."""
- from modules.connection.connection_manager import parse_machine_position
- # No position info
- result = parse_machine_position("ok")
- assert result is None
- # Empty string
- result = parse_machine_position("")
- assert result is None
- # Malformed response
- result = parse_machine_position("<Idle|Bf:15,128>")
- assert result is None
- def test_parse_machine_position_run_state(self):
- """Test parsing position during run state."""
- from modules.connection.connection_manager import parse_machine_position
- response = "<Run|MPos:-994.869,-321.861,0.000|Bf:15,127>"
- result = parse_machine_position(response)
- assert result is not None
- assert result[0] == pytest.approx(-994.869)
- assert result[1] == pytest.approx(-321.861)
- def test_parse_machine_position_alarm_state(self):
- """Test parsing position during alarm state."""
- from modules.connection.connection_manager import parse_machine_position
- response = "<Alarm|MPos:0.000,0.000,0.000|Bf:15,128|Pn:XY>"
- result = parse_machine_position(response)
- assert result is not None
- assert result == (0.0, 0.0)
- def test_parse_machine_position_with_extra_info(self):
- """Test parsing position with extra fields in response."""
- from modules.connection.connection_manager import parse_machine_position
- # Response with WCO (Work Coordinate Offset)
- response = "<Idle|MPos:5.0,10.0,0.0|FS:0,0|WCO:0,0,0>"
- result = parse_machine_position(response)
- assert result is not None
- assert result == (5.0, 10.0)
- def test_parse_machine_position_negative_coords(self):
- """Test parsing negative coordinates."""
- from modules.connection.connection_manager import parse_machine_position
- response = "<Idle|MPos:-100.123,-200.456,0.000|Bf:15,128>"
- result = parse_machine_position(response)
- assert result is not None
- assert result[0] == pytest.approx(-100.123)
- assert result[1] == pytest.approx(-200.456)
- def test_parse_machine_position_high_precision(self):
- """Test parsing high precision coordinates."""
- from modules.connection.connection_manager import parse_machine_position
- response = "<Idle|MPos:123.456789,987.654321,0.000000|Bf:15,128>"
- result = parse_machine_position(response)
- assert result is not None
- assert result[0] == pytest.approx(123.456789)
- assert result[1] == pytest.approx(987.654321)
- class TestListSerialPorts:
- """Tests for list_serial_ports function."""
- def test_list_serial_ports_filters_ignored(self):
- """Test that ignored ports are filtered out."""
- # Create mock port objects
- mock_port1 = MagicMock()
- mock_port1.device = "/dev/ttyUSB0"
- mock_port2 = MagicMock()
- mock_port2.device = "/dev/cu.debug-console" # Should be filtered
- mock_port3 = MagicMock()
- mock_port3.device = "/dev/cu.Bluetooth-Incoming-Port" # Should be filtered
- mock_port4 = MagicMock()
- mock_port4.device = "/dev/ttyACM0"
- with patch("serial.tools.list_ports.comports", return_value=[mock_port1, mock_port2, mock_port3, mock_port4]):
- from modules.connection.connection_manager import list_serial_ports
- ports = list_serial_ports()
- assert "/dev/ttyUSB0" in ports
- assert "/dev/ttyACM0" in ports
- assert "/dev/cu.debug-console" not in ports
- assert "/dev/cu.Bluetooth-Incoming-Port" not in ports
- assert len(ports) == 2
- def test_list_serial_ports_empty(self):
- """Test list_serial_ports returns empty when no ports available."""
- with patch("serial.tools.list_ports.comports", return_value=[]):
- from modules.connection.connection_manager import list_serial_ports
- ports = list_serial_ports()
- assert ports == []
- def test_list_serial_ports_all_ignored(self):
- """Test list_serial_ports when all ports are ignored."""
- mock_port1 = MagicMock()
- mock_port1.device = "/dev/cu.debug-console"
- mock_port2 = MagicMock()
- mock_port2.device = "/dev/cu.Bluetooth-Incoming-Port"
- with patch("serial.tools.list_ports.comports", return_value=[mock_port1, mock_port2]):
- from modules.connection.connection_manager import list_serial_ports
- ports = list_serial_ports()
- assert ports == []
- class TestConnectionClasses:
- """Tests for connection class structure (no hardware required)."""
- def test_base_connection_interface(self):
- """Test that BaseConnection defines required interface."""
- from modules.connection.connection_manager import BaseConnection
- # BaseConnection should have these abstract methods
- base = BaseConnection()
- with pytest.raises(NotImplementedError):
- base.send("test")
- with pytest.raises(NotImplementedError):
- base.flush()
- with pytest.raises(NotImplementedError):
- base.readline()
- with pytest.raises(NotImplementedError):
- base.in_waiting()
- with pytest.raises(NotImplementedError):
- base.is_connected()
- with pytest.raises(NotImplementedError):
- base.close()
- def test_serial_connection_inherits_base(self):
- """Test SerialConnection inherits from BaseConnection."""
- from modules.connection.connection_manager import SerialConnection, BaseConnection
- assert issubclass(SerialConnection, BaseConnection)
- def test_websocket_connection_inherits_base(self):
- """Test WebSocketConnection inherits from BaseConnection."""
- from modules.connection.connection_manager import WebSocketConnection, BaseConnection
- assert issubclass(WebSocketConnection, BaseConnection)
- class TestIgnorePorts:
- """Tests for IGNORE_PORTS and DEPRIORITIZED_PORTS constants."""
- def test_ignore_ports_defined(self):
- """Test that IGNORE_PORTS constant is defined."""
- from modules.connection.connection_manager import IGNORE_PORTS
- assert isinstance(IGNORE_PORTS, list)
- assert "/dev/cu.debug-console" in IGNORE_PORTS
- assert "/dev/cu.Bluetooth-Incoming-Port" in IGNORE_PORTS
- def test_deprioritized_ports_defined(self):
- """Test that DEPRIORITIZED_PORTS constant is defined."""
- from modules.connection.connection_manager import DEPRIORITIZED_PORTS
- assert isinstance(DEPRIORITIZED_PORTS, list)
- # ttyS0 is typically the Pi hardware UART - should be deprioritized
- assert "/dev/ttyS0" in DEPRIORITIZED_PORTS
- class TestIsMachineIdle:
- """Tests for is_machine_idle function."""
- def test_is_machine_idle_no_connection(self, mock_state):
- """Test is_machine_idle returns False when no connection."""
- mock_state.conn = None
- with patch("modules.connection.connection_manager.state", mock_state):
- from modules.connection.connection_manager import is_machine_idle
- result = is_machine_idle()
- assert result is False
- def test_is_machine_idle_disconnected(self, mock_state):
- """Test is_machine_idle returns False when disconnected."""
- mock_state.conn.is_connected.return_value = False
- with patch("modules.connection.connection_manager.state", mock_state):
- from modules.connection.connection_manager import is_machine_idle
- result = is_machine_idle()
- assert result is False
- def test_is_machine_idle_when_idle(self, mock_state):
- """Test is_machine_idle returns True when machine is idle."""
- mock_state.conn.is_connected.return_value = True
- mock_state.conn.send = MagicMock()
- mock_state.conn.readline.return_value = "<Idle|MPos:0,0,0|Bf:15,128>"
- with patch("modules.connection.connection_manager.state", mock_state):
- from modules.connection.connection_manager import is_machine_idle
- result = is_machine_idle()
- assert result is True
- mock_state.conn.send.assert_called_with('?')
- def test_is_machine_idle_when_running(self, mock_state):
- """Test is_machine_idle returns False when machine is running."""
- mock_state.conn.is_connected.return_value = True
- mock_state.conn.send = MagicMock()
- mock_state.conn.readline.return_value = "<Run|MPos:0,0,0|Bf:15,128>"
- with patch("modules.connection.connection_manager.state", mock_state):
- from modules.connection.connection_manager import is_machine_idle
- result = is_machine_idle()
- assert result is False
|