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>
193 lines
6.4 KiB
Python
193 lines
6.4 KiB
Python
"""
|
|
test_ldf_handler.py — Tests for the LDF parsing module.
|
|
|
|
Tests the ldf_handler adapter layer that converts ldfparser's output
|
|
into our simplified data structures. We test:
|
|
1. Parsing a valid LDF file
|
|
2. Correct frame/signal extraction
|
|
3. Master vs slave frame classification
|
|
4. Schedule table extraction
|
|
5. Error handling for missing/invalid files
|
|
"""
|
|
|
|
import sys
|
|
import pytest
|
|
from pathlib import Path
|
|
|
|
# Add src to path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
|
|
from ldf_handler import parse_ldf, LdfData, FrameInfo, SignalInfo, ScheduleEntryInfo
|
|
|
|
|
|
# Path to the sample LDF file
|
|
SAMPLE_LDF = str(Path(__file__).parent.parent.parent / "resources" / "sample.ldf")
|
|
|
|
|
|
class TestParseLdf:
|
|
"""Test basic LDF parsing."""
|
|
|
|
def test_returns_ldf_data(self):
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
assert isinstance(result, LdfData)
|
|
|
|
def test_protocol_version(self):
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
assert result.protocol_version == "2.1"
|
|
|
|
def test_baudrate(self):
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
assert result.baudrate == 19200
|
|
|
|
def test_master_name(self):
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
assert result.master_name == "ECU_Master"
|
|
|
|
def test_slave_names(self):
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
assert "Motor_Control" in result.slave_names
|
|
assert "Door_Module" in result.slave_names
|
|
|
|
def test_file_path_stored(self):
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
assert result.file_path == SAMPLE_LDF
|
|
|
|
|
|
class TestFrameClassification:
|
|
"""Test that frames are correctly split into Tx and Rx."""
|
|
|
|
def test_tx_frame_count(self):
|
|
"""Master publishes 2 frames: Motor_Command, Door_Command."""
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
assert len(result.tx_frames) == 2
|
|
|
|
def test_rx_frame_count(self):
|
|
"""Slaves publish 2 frames: Motor_Status, Door_Status."""
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
assert len(result.rx_frames) == 2
|
|
|
|
def test_tx_frames_are_master(self):
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
for frame in result.tx_frames:
|
|
assert frame.is_master_tx is True
|
|
|
|
def test_rx_frames_are_slave(self):
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
for frame in result.rx_frames:
|
|
assert frame.is_master_tx is False
|
|
|
|
def test_tx_frame_names(self):
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
names = [f.name for f in result.tx_frames]
|
|
assert "Motor_Command" in names
|
|
assert "Door_Command" in names
|
|
|
|
def test_rx_frame_names(self):
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
names = [f.name for f in result.rx_frames]
|
|
assert "Motor_Status" in names
|
|
assert "Door_Status" in names
|
|
|
|
|
|
class TestFrameDetails:
|
|
"""Test frame metadata extraction."""
|
|
|
|
def test_motor_command_id(self):
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
frame = next(f for f in result.tx_frames if f.name == "Motor_Command")
|
|
assert frame.frame_id == 0x10
|
|
|
|
def test_motor_command_length(self):
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
frame = next(f for f in result.tx_frames if f.name == "Motor_Command")
|
|
assert frame.length == 2
|
|
|
|
def test_motor_command_publisher(self):
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
frame = next(f for f in result.tx_frames if f.name == "Motor_Command")
|
|
assert frame.publisher == "ECU_Master"
|
|
|
|
|
|
class TestSignalExtraction:
|
|
"""Test signal details within frames."""
|
|
|
|
def test_motor_command_signals(self):
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
frame = next(f for f in result.tx_frames if f.name == "Motor_Command")
|
|
sig_names = [s.name for s in frame.signals]
|
|
assert "MotorEnable" in sig_names
|
|
assert "MotorDirection" in sig_names
|
|
assert "MotorSpeed" in sig_names
|
|
|
|
def test_signal_bit_offset(self):
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
frame = next(f for f in result.tx_frames if f.name == "Motor_Command")
|
|
enable = next(s for s in frame.signals if s.name == "MotorEnable")
|
|
assert enable.bit_offset == 0
|
|
|
|
def test_signal_width(self):
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
frame = next(f for f in result.tx_frames if f.name == "Motor_Command")
|
|
speed = next(s for s in frame.signals if s.name == "MotorSpeed")
|
|
assert speed.width == 8
|
|
|
|
def test_signal_init_value(self):
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
frame = next(f for f in result.tx_frames if f.name == "Motor_Command")
|
|
enable = next(s for s in frame.signals if s.name == "MotorEnable")
|
|
assert enable.init_value == 0
|
|
|
|
|
|
class TestScheduleTables:
|
|
"""Test schedule table extraction."""
|
|
|
|
def test_schedule_count(self):
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
assert len(result.schedule_tables) == 2
|
|
|
|
def test_schedule_names(self):
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
names = [st.name for st in result.schedule_tables]
|
|
assert "NormalSchedule" in names
|
|
assert "FastSchedule" in names
|
|
|
|
def test_normal_schedule_entries(self):
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
normal = next(st for st in result.schedule_tables if st.name == "NormalSchedule")
|
|
assert len(normal.entries) == 4
|
|
|
|
def test_normal_schedule_delay(self):
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
normal = next(st for st in result.schedule_tables if st.name == "NormalSchedule")
|
|
# All entries in NormalSchedule have 10ms delay
|
|
for entry in normal.entries:
|
|
assert entry.delay_ms == 10
|
|
|
|
def test_fast_schedule_delay(self):
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
fast = next(st for st in result.schedule_tables if st.name == "FastSchedule")
|
|
for entry in fast.entries:
|
|
assert entry.delay_ms == 5
|
|
|
|
def test_frame_entries_have_no_data(self):
|
|
"""Regular frame entries should not have raw data."""
|
|
result = parse_ldf(SAMPLE_LDF)
|
|
normal = next(st for st in result.schedule_tables if st.name == "NormalSchedule")
|
|
for entry in normal.entries:
|
|
assert entry.data is None
|
|
|
|
|
|
class TestErrorHandling:
|
|
"""Test error cases."""
|
|
|
|
def test_file_not_found(self):
|
|
with pytest.raises(FileNotFoundError):
|
|
parse_ldf("/nonexistent/path/fake.ldf")
|
|
|
|
def test_invalid_file(self, tmp_path):
|
|
"""A file that exists but isn't valid LDF should raise an error."""
|
|
bad_file = tmp_path / "bad.ldf"
|
|
bad_file.write_text("this is not a valid LDF file")
|
|
with pytest.raises(Exception):
|
|
parse_ldf(str(bad_file))
|