""" test_signal_editing.py — Tests for Step 3: signal ↔ frame byte sync. Tests bit packing/unpacking: when a signal value changes, frame bytes update, and vice versa. """ import sys import pytest from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent / "src")) from PyQt6.QtWidgets import QApplication from PyQt6.QtCore import Qt SAMPLE_LDF = str(Path(__file__).parent.parent.parent / "resources" / "sample.ldf") @pytest.fixture(scope="session") def app(): application = QApplication.instance() or QApplication(sys.argv) yield application @pytest.fixture def window(app): from main_window import MainWindow w = MainWindow() w._load_ldf_file(SAMPLE_LDF) return w class TestBitPacking: """Test the static bit pack/unpack methods.""" def test_pack_single_bit(self, window): """Pack a 1-bit signal at bit 0.""" buf = [0, 0] window._pack_signal(buf, bit_offset=0, width=1, value=1) assert buf == [0x01, 0] def test_pack_byte_at_offset_8(self, window): """Pack an 8-bit signal at bit 8 (byte 1).""" buf = [0, 0] window._pack_signal(buf, bit_offset=8, width=8, value=0x80) assert buf == [0, 0x80] def test_pack_2bit_at_offset_1(self, window): """Pack a 2-bit signal at bit 1.""" buf = [0, 0] window._pack_signal(buf, bit_offset=1, width=2, value=3) # value 3 = 0b11, at bits 1-2 → byte 0 = 0b00000110 = 0x06 assert buf[0] == 0x06 def test_pack_multiple_signals(self, window): """Pack multiple signals without overwriting each other.""" buf = [0, 0] # MotorEnable: bit 0, width 1, value 1 window._pack_signal(buf, 0, 1, 1) # MotorDirection: bit 1, width 2, value 2 window._pack_signal(buf, 1, 2, 2) # MotorSpeed: bit 8, width 8, value 128 window._pack_signal(buf, 8, 8, 128) # byte 0: bit0=1 (Enable), bits1-2=10 (Dir=2) → 0b00000101 = 0x05 assert buf[0] == 0x05 assert buf[1] == 0x80 # 128 def test_extract_single_bit(self, window): buf = [0x01, 0] val = window._extract_signal(buf, bit_offset=0, width=1) assert val == 1 def test_extract_byte_at_offset_8(self, window): buf = [0, 0x80] val = window._extract_signal(buf, bit_offset=8, width=8) assert val == 0x80 def test_extract_2bit_at_offset_1(self, window): buf = [0x06, 0] # 0b00000110 → bits 1-2 = 0b11 = 3 val = window._extract_signal(buf, bit_offset=1, width=2) assert val == 3 def test_pack_then_extract_roundtrip(self, window): """Pack a value then extract it — should get the same value.""" buf = [0, 0] window._pack_signal(buf, 8, 8, 200) result = window._extract_signal(buf, 8, 8) assert result == 200 class TestSignalValueEditing: """Test that editing a signal value updates frame bytes.""" def test_edit_signal_updates_frame_bytes(self, window): """Changing MotorSpeed signal should update frame bytes.""" frame_item = window.tx_table.topLevelItem(0) # Motor_Command speed_sig = frame_item.child(2) # MotorSpeed (bit 8, width 8) # Simulate user edit: change the text in Value column # This triggers itemChanged → _on_tx_item_changed → repack speed_sig.setText(4, "128") # Frame bytes should now reflect MotorSpeed=128 frame_data = frame_item.data(0, Qt.ItemDataRole.UserRole) assert frame_data['bytes'][1] == 128 # byte 1 = 0x80 def test_edit_signal_preserves_other_signals(self, window): """Changing one signal shouldn't affect other signals in the same frame.""" frame_item = window.tx_table.topLevelItem(0) # Motor_Command enable_sig = frame_item.child(0) # MotorEnable (bit 0, width 1) speed_sig = frame_item.child(2) # MotorSpeed (bit 8, width 8) # Set MotorEnable = 1 via text edit enable_sig.setText(4, "1") # Set MotorSpeed = 255 via text edit speed_sig.setText(4, "255") # Both should be preserved frame_data = frame_item.data(0, Qt.ItemDataRole.UserRole) assert frame_data['bytes'][0] & 0x01 == 1 # MotorEnable still 1 assert frame_data['bytes'][1] == 255 # MotorSpeed = 255 def test_signal_value_clamped_to_width(self, window): """Signal value should be clamped to max for its width.""" frame_item = window.tx_table.topLevelItem(0) enable_sig = frame_item.child(0) # MotorEnable: width 1, max = 1 # Try to set value 5 on a 1-bit signal enable_sig.setText(4, "5") window._on_tx_item_changed(enable_sig, 4) # Should be clamped to 1 (max for 1-bit) stored = enable_sig.data(4, Qt.ItemDataRole.UserRole) assert stored <= 1 class TestFrameValueEditing: """Test that editing frame bytes updates signal values.""" def test_edit_frame_bytes_updates_signals(self, window): """Changing frame bytes should unpack to signal children.""" frame_item = window.tx_table.topLevelItem(0) # Motor_Command # Edit frame bytes to "05 C8" (hex mode) frame_item.setText(4, "05 C8") window._on_tx_item_changed(frame_item, 4) # MotorEnable (bit 0, width 1): byte0 & 0x01 = 1 enable_val = frame_item.child(0).data(4, Qt.ItemDataRole.UserRole) assert enable_val == 1 # MotorDirection (bit 1, width 2): (byte0 >> 1) & 0x03 = 2 dir_val = frame_item.child(1).data(4, Qt.ItemDataRole.UserRole) assert dir_val == 2 # MotorSpeed (bit 8, width 8): byte1 = 0xC8 = 200 speed_val = frame_item.child(2).data(4, Qt.ItemDataRole.UserRole) assert speed_val == 200 def test_edit_frame_bytes_invalid_reverts(self, window): """Invalid byte input should revert to previous values.""" frame_item = window.tx_table.topLevelItem(0) # Store current bytes old_data = frame_item.data(0, Qt.ItemDataRole.UserRole)['bytes'][:] # Enter invalid text frame_item.setText(4, "not valid hex") window._on_tx_item_changed(frame_item, 4) # Bytes should be unchanged new_data = frame_item.data(0, Qt.ItemDataRole.UserRole)['bytes'] assert new_data == old_data