- README.md with full project documentation, architecture, setup guide - Automotive/industrial dark theme (theme.py) inspired by Vector CANoe - Dark blue-black background with amber/orange accents - Styled buttons (green Start, red Stop), orange hover effects - Custom scrollbars, tooltips, group box titles, tree widget headers - Updated highlight colors to match theme palette Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
190 lines
6.8 KiB
Python
190 lines
6.8 KiB
Python
"""
|
|
test_rx_realtime.py — Tests for Step 4: Rx panel real-time display.
|
|
|
|
Tests frame reception, timestamp updates, signal unpacking,
|
|
change highlighting, auto-scroll, and clear functionality.
|
|
"""
|
|
|
|
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
|
|
from PyQt6.QtGui import QBrush, QColor
|
|
|
|
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 TestRxFrameReception:
|
|
"""Test that receive_rx_frame updates the Rx panel correctly."""
|
|
|
|
def test_timestamp_updates(self, window):
|
|
"""Receiving a frame should set the timestamp."""
|
|
window.receive_rx_frame(0x20, [0x03, 0xC8])
|
|
frame_item = window.rx_table.topLevelItem(0)
|
|
ts = frame_item.text(0)
|
|
assert ts != "—"
|
|
assert ":" in ts # Should be HH:mm:ss.zzz format
|
|
|
|
def test_frame_bytes_stored(self, window):
|
|
"""Received bytes should be stored in the frame row."""
|
|
window.receive_rx_frame(0x20, [0x03, 0xC8])
|
|
frame_item = window.rx_table.topLevelItem(0)
|
|
stored = frame_item.data(4, Qt.ItemDataRole.UserRole)
|
|
assert stored == [0x03, 0xC8]
|
|
|
|
def test_frame_value_displayed(self, window):
|
|
"""Frame value column should show received bytes."""
|
|
window.receive_rx_frame(0x20, [0x03, 0xC8])
|
|
frame_item = window.rx_table.topLevelItem(0)
|
|
val = frame_item.text(4)
|
|
# Default hex mode
|
|
assert "03" in val
|
|
assert "C8" in val
|
|
|
|
def test_signal_values_unpacked(self, window):
|
|
"""Signal children should have unpacked values from received bytes."""
|
|
# Motor_Status: MotorStatus (bit 0, w2), MotorTemp (bit 8, w8)
|
|
window.receive_rx_frame(0x20, [0x03, 0xC8])
|
|
frame_item = window.rx_table.topLevelItem(0)
|
|
|
|
# MotorStatus (bit 0, width 2): byte0 & 0x03 = 3
|
|
status_val = frame_item.child(0).data(4, Qt.ItemDataRole.UserRole)
|
|
assert status_val == 3
|
|
|
|
# MotorTemp (bit 8, width 8): byte1 = 0xC8 = 200
|
|
temp_val = frame_item.child(1).data(4, Qt.ItemDataRole.UserRole)
|
|
assert temp_val == 200
|
|
|
|
def test_unknown_frame_id_ignored(self, window):
|
|
"""Receiving an unknown frame ID should not crash."""
|
|
window.receive_rx_frame(0xFF, [0x00])
|
|
# No assertion — just verify no crash
|
|
|
|
def test_update_same_frame_twice(self, window):
|
|
"""Second reception of same frame should update in-place."""
|
|
window.receive_rx_frame(0x20, [0x01, 0x10])
|
|
window.receive_rx_frame(0x20, [0x02, 0x20])
|
|
|
|
frame_item = window.rx_table.topLevelItem(0)
|
|
stored = frame_item.data(4, Qt.ItemDataRole.UserRole)
|
|
assert stored == [0x02, 0x20] # Updated, not appended
|
|
|
|
|
|
class TestChangeHighlighting:
|
|
"""Test that changed signal values get highlighted."""
|
|
|
|
def test_changed_signal_highlighted(self, window):
|
|
"""When a signal value changes, it should be highlighted yellow."""
|
|
# First reception
|
|
window.receive_rx_frame(0x20, [0x01, 0x10])
|
|
# Second reception with different MotorTemp
|
|
window.receive_rx_frame(0x20, [0x01, 0x20])
|
|
|
|
frame_item = window.rx_table.topLevelItem(0)
|
|
temp_sig = frame_item.child(1) # MotorTemp changed: 0x10 → 0x20
|
|
bg = temp_sig.background(4)
|
|
# Should be highlighted (amber/yellow with alpha)
|
|
assert bg.color().red() > 200
|
|
assert bg.color().green() > 150
|
|
|
|
def test_unchanged_signal_not_highlighted(self, window):
|
|
"""Signal that didn't change should not be highlighted."""
|
|
window.receive_rx_frame(0x20, [0x01, 0x10])
|
|
window.receive_rx_frame(0x20, [0x01, 0x20])
|
|
|
|
frame_item = window.rx_table.topLevelItem(0)
|
|
status_sig = frame_item.child(0) # MotorStatus unchanged: still 1
|
|
bg = status_sig.background(4)
|
|
# Should NOT be yellow — default/empty brush
|
|
assert bg.style() == Qt.BrushStyle.NoBrush
|
|
|
|
def test_first_reception_no_highlight(self, window):
|
|
"""First reception should not highlight anything (no previous value)."""
|
|
window.receive_rx_frame(0x20, [0x01, 0x10])
|
|
frame_item = window.rx_table.topLevelItem(0)
|
|
for j in range(frame_item.childCount()):
|
|
bg = frame_item.child(j).background(4)
|
|
assert bg.style() == Qt.BrushStyle.NoBrush
|
|
|
|
|
|
class TestAutoScroll:
|
|
"""Test auto-scroll behavior."""
|
|
|
|
def test_auto_scroll_default_on(self, window):
|
|
assert window.chk_auto_scroll.isChecked()
|
|
|
|
def test_auto_scroll_can_be_disabled(self, window):
|
|
window.chk_auto_scroll.setChecked(False)
|
|
assert not window.chk_auto_scroll.isChecked()
|
|
|
|
|
|
class TestClearRx:
|
|
"""Test the Clear button."""
|
|
|
|
def test_clear_resets_timestamps(self, window):
|
|
window.receive_rx_frame(0x20, [0x01, 0x10])
|
|
window._on_clear_rx()
|
|
|
|
for i in range(window.rx_table.topLevelItemCount()):
|
|
assert window.rx_table.topLevelItem(i).text(0) == "—"
|
|
|
|
def test_clear_resets_values(self, window):
|
|
window.receive_rx_frame(0x20, [0x01, 0x10])
|
|
window._on_clear_rx()
|
|
|
|
for i in range(window.rx_table.topLevelItemCount()):
|
|
assert window.rx_table.topLevelItem(i).text(4) == "—"
|
|
|
|
def test_clear_resets_signal_highlights(self, window):
|
|
window.receive_rx_frame(0x20, [0x01, 0x10])
|
|
window.receive_rx_frame(0x20, [0x02, 0x20])
|
|
window._on_clear_rx()
|
|
|
|
frame_item = window.rx_table.topLevelItem(0)
|
|
for j in range(frame_item.childCount()):
|
|
bg = frame_item.child(j).background(4)
|
|
assert bg.style() == Qt.BrushStyle.NoBrush
|
|
|
|
def test_clear_resets_last_values_tracking(self, window):
|
|
window.receive_rx_frame(0x20, [0x01, 0x10])
|
|
window._on_clear_rx()
|
|
assert len(window._rx_last_values) == 0
|
|
|
|
|
|
class TestRxHexDec:
|
|
"""Test hex/dec mode applies to received Rx data."""
|
|
|
|
def test_rx_hex_mode(self, window):
|
|
window.receive_rx_frame(0x20, [0x03, 0xC8])
|
|
frame_item = window.rx_table.topLevelItem(0)
|
|
# Hex mode (default)
|
|
assert "C8" in frame_item.text(4)
|
|
assert frame_item.child(1).text(4).startswith("0x")
|
|
|
|
def test_rx_dec_mode(self, window):
|
|
window.chk_hex_mode.setChecked(False)
|
|
window.receive_rx_frame(0x20, [0x03, 0xC8])
|
|
frame_item = window.rx_table.topLevelItem(0)
|
|
# Dec mode
|
|
assert "200" in frame_item.text(4)
|
|
assert not frame_item.child(1).text(4).startswith("0x")
|
|
window.chk_hex_mode.setChecked(True) # Restore
|