Lin_Simulator/python/tests/test_ldf_handler.py
Mohamed Salem cb60c2ad5d Steps 2-7: LDF loading, signal editing, Rx display, connection, BabyLIN backend, scheduler
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>
2026-04-04 14:21:24 +02:00

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))