Răsfoiți Sursa

test(backend-testing-01): add connection_manager parsing tests

- parse_machine_position tests (MPos, WPos formats)
- parse_machine_position edge cases (invalid, run/alarm state, negative, high precision)
- list_serial_ports tests (filter ignored, empty, all ignored)
- BaseConnection interface tests
- IGNORE_PORTS and DEPRIORITIZED_PORTS constant tests
- is_machine_idle tests (no connection, idle, running)

21 tests total, all passing
tuanchris 1 săptămână în urmă
părinte
comite
7eb375e780
1 a modificat fișierele cu 282 adăugiri și 0 ștergeri
  1. 282 0
      tests/unit/test_connection_manager.py

+ 282 - 0
tests/unit/test_connection_manager.py

@@ -0,0 +1,282 @@
+"""
+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