Step 2 - LDF Loading: - ldfparser integration (Python) / custom regex parser (C++) - QTreeWidget with expandable signal rows, merged Value column - Hex/Dec toggle, FreeFormat schedule entries, auto-reload - Baud rate auto-detection from LDF Step 3 - Signal Editing: - Bit packing/unpacking (signal value ↔ frame bytes) - ReadOnlyColumnDelegate for per-column editability - Value clamping to signal width, recursion guard Step 4 - Rx Panel: - receive_rx_frame() API with timestamp, signal unpacking - Change highlighting (yellow), auto-scroll toggle, clear button - Dashboard view (in-place update per frame_id) Step 5 - Connection Panel: - ConnectionManager with state machine (Disconnected/Connecting/Connected/Error) - Port scanning (pyserial / QSerialPort), connect/disconnect with UI mapping Step 6 - BabyLIN Backend: - BabyLinBackend wrapping Lipowsky BabyLIN_library.py DLL - Mock mode for macOS/CI, device scan, SDF loading, signal access - Frame callbacks, raw command access Step 7 - Master Scheduler: - QTimer-based schedule execution with start/stop/pause - Frame sent callback with visual highlighting - Mock Rx simulation, manual send, global rate override Tests: Python 171 | C++ 124 (Steps 1-5 parity, Steps 6-7 Python-first) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
209 lines
7.1 KiB
Python
209 lines
7.1 KiB
Python
"""
|
|
test_main_window.py — Tests for the GUI skeleton (Step 1).
|
|
|
|
TESTING PyQt6 APPLICATIONS:
|
|
============================
|
|
GUI testing has a challenge: Qt widgets need a running QApplication.
|
|
We use a pytest fixture to create ONE QApplication for the entire test session.
|
|
|
|
Key things we verify:
|
|
1. Window creates without errors
|
|
2. All panels exist and are the correct widget types
|
|
3. Tables have the right columns
|
|
4. Buttons and controls are in the expected initial state
|
|
5. Window has a reasonable minimum size
|
|
|
|
We do NOT test visual appearance (that's manual testing).
|
|
We DO test that the widget tree is correctly assembled.
|
|
"""
|
|
|
|
import sys
|
|
import pytest
|
|
|
|
# Add src to path so we can import our modules
|
|
sys.path.insert(0, str(__import__("pathlib").Path(__file__).parent.parent / "src"))
|
|
|
|
from PyQt6.QtWidgets import QApplication, QDockWidget, QTreeWidget
|
|
from PyQt6.QtCore import Qt
|
|
|
|
|
|
# ─── Fixtures ──────────────────────────────────────────────────────────
|
|
# Fixtures are pytest's way of providing shared setup. The "session" scope
|
|
# means this QApplication is created once and reused across ALL tests.
|
|
|
|
@pytest.fixture(scope="session")
|
|
def app():
|
|
"""Create a QApplication instance for the test session."""
|
|
application = QApplication.instance() or QApplication(sys.argv)
|
|
yield application
|
|
|
|
|
|
@pytest.fixture
|
|
def window(app):
|
|
"""Create a fresh MainWindow for each test."""
|
|
from main_window import MainWindow
|
|
w = MainWindow()
|
|
yield w
|
|
w.close()
|
|
|
|
|
|
# ─── Tests ─────────────────────────────────────────────────────────────
|
|
|
|
class TestWindowBasics:
|
|
"""Test that the window initializes correctly."""
|
|
|
|
def test_window_title(self, window):
|
|
assert window.windowTitle() == "LIN Simulator"
|
|
|
|
def test_minimum_size(self, window):
|
|
assert window.minimumWidth() >= 1024
|
|
assert window.minimumHeight() >= 768
|
|
|
|
def test_central_widget_exists(self, window):
|
|
assert window.centralWidget() is not None
|
|
|
|
|
|
class TestMenuBar:
|
|
"""Test menu bar structure."""
|
|
|
|
def test_menu_bar_exists(self, window):
|
|
assert window.menuBar() is not None
|
|
|
|
def test_load_ldf_action_exists(self, window):
|
|
assert window.action_load_ldf is not None
|
|
assert window.action_load_ldf.text() == "&Load LDF..."
|
|
|
|
def test_load_ldf_shortcut(self, window):
|
|
assert window.action_load_ldf.shortcut().toString() == "Ctrl+O"
|
|
|
|
|
|
class TestLdfToolbar:
|
|
"""Test the LDF file toolbar."""
|
|
|
|
def test_ldf_path_field_exists(self, window):
|
|
assert window.ldf_path_edit is not None
|
|
assert window.ldf_path_edit.isReadOnly()
|
|
|
|
def test_ldf_path_placeholder(self, window):
|
|
assert window.ldf_path_edit.placeholderText() == "No LDF file loaded"
|
|
|
|
def test_browse_button_exists(self, window):
|
|
assert window.btn_browse_ldf is not None
|
|
|
|
def test_auto_reload_default_checked(self, window):
|
|
assert window.chk_auto_reload.isChecked()
|
|
|
|
|
|
class TestTxTable:
|
|
"""Test the Tx (transmit) tree widget structure."""
|
|
|
|
def test_tx_table_exists(self, window):
|
|
assert isinstance(window.tx_table, QTreeWidget)
|
|
|
|
def test_tx_table_columns(self, window):
|
|
assert window.tx_table.columnCount() == 6
|
|
headers = []
|
|
for i in range(window.tx_table.columnCount()):
|
|
headers.append(window.tx_table.headerItem().text(i))
|
|
assert headers == [
|
|
"Name", "ID / Bit", "Length / Width", "Interval (ms)",
|
|
"Value", "Action"
|
|
]
|
|
|
|
def test_tx_table_alternating_colors(self, window):
|
|
assert window.tx_table.alternatingRowColors()
|
|
|
|
def test_tx_table_is_decorated(self, window):
|
|
"""Tree should show expand/collapse arrows."""
|
|
assert window.tx_table.rootIsDecorated()
|
|
|
|
|
|
class TestRxTable:
|
|
"""Test the Rx (receive) tree widget structure."""
|
|
|
|
def test_rx_table_exists(self, window):
|
|
assert isinstance(window.rx_table, QTreeWidget)
|
|
|
|
def test_rx_table_columns(self, window):
|
|
assert window.rx_table.columnCount() == 5
|
|
headers = []
|
|
for i in range(window.rx_table.columnCount()):
|
|
headers.append(window.rx_table.headerItem().text(i))
|
|
assert headers == ["Timestamp", "Name", "ID / Bit", "Length / Width", "Value"]
|
|
|
|
def test_rx_table_is_decorated(self, window):
|
|
"""Tree should show expand/collapse arrows."""
|
|
assert window.rx_table.rootIsDecorated()
|
|
|
|
|
|
class TestConnectionDock:
|
|
"""Test the connection panel dock widget."""
|
|
|
|
def test_dock_exists(self, window):
|
|
docks = window.findChildren(QDockWidget)
|
|
assert len(docks) == 1
|
|
assert docks[0].windowTitle() == "Connection"
|
|
|
|
def test_device_combo_exists(self, window):
|
|
assert window.combo_device is not None
|
|
|
|
def test_connect_button_exists(self, window):
|
|
assert window.btn_connect is not None
|
|
assert window.btn_connect.isEnabled()
|
|
|
|
def test_disconnect_button_disabled_initially(self, window):
|
|
"""Disconnect should be disabled when no device is connected."""
|
|
assert not window.btn_disconnect.isEnabled()
|
|
|
|
def test_status_label_shows_disconnected(self, window):
|
|
assert "Disconnected" in window.lbl_conn_status.text()
|
|
|
|
def test_baud_rate_label_exists(self, window):
|
|
assert window.lbl_baud_rate is not None
|
|
|
|
def test_baud_rate_shows_placeholder_before_ldf(self, window):
|
|
"""Before loading an LDF, baud rate should show a placeholder."""
|
|
assert "load LDF" in window.lbl_baud_rate.text()
|
|
|
|
|
|
class TestControlBar:
|
|
"""Test the bottom control toolbar."""
|
|
|
|
def test_schedule_combo_exists(self, window):
|
|
assert window.combo_schedule is not None
|
|
|
|
def test_scheduler_buttons_disabled_initially(self, window):
|
|
"""Start/Stop/Pause should be disabled until LDF loaded + connected."""
|
|
assert not window.btn_start.isEnabled()
|
|
assert not window.btn_stop.isEnabled()
|
|
assert not window.btn_pause.isEnabled()
|
|
|
|
def test_manual_send_disabled_initially(self, window):
|
|
assert not window.btn_manual_send.isEnabled()
|
|
|
|
def test_global_rate_spinbox_exists(self, window):
|
|
assert window.spin_global_rate is not None
|
|
|
|
def test_global_rate_default_50ms(self, window):
|
|
"""50ms default = ~20 frames/sec, typical LIN schedule speed."""
|
|
assert window.spin_global_rate.value() == 50
|
|
|
|
def test_global_rate_range(self, window):
|
|
"""Range should be 1ms (fast stress test) to 10000ms (very slow)."""
|
|
assert window.spin_global_rate.minimum() == 1
|
|
assert window.spin_global_rate.maximum() == 10000
|
|
|
|
def test_global_rate_suffix(self, window):
|
|
"""Should display the unit 'ms' in the spinbox."""
|
|
assert window.spin_global_rate.suffix() == " ms"
|
|
|
|
|
|
class TestStatusBar:
|
|
"""Test the status bar."""
|
|
|
|
def test_status_bar_exists(self, window):
|
|
assert window.statusBar() is not None
|
|
|
|
def test_connection_status_label(self, window):
|
|
assert "Disconnected" in window.lbl_status_connection.text()
|