243 lines
6.8 KiB
Python
243 lines
6.8 KiB
Python
"""Unit tests for the MUM LIN adapter using fake pylin/pymumclient modules.
|
|
|
|
These tests don't talk to real hardware — they inject lightweight fakes via
|
|
the adapter's `mum_module` / `pylin_module` constructor args to validate the
|
|
adapter's plumbing (connect/disconnect, send, receive, send_raw, power_*).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from ecu_framework.lin.base import LinFrame
|
|
from ecu_framework.lin.mum import MumLinInterface
|
|
|
|
|
|
# ---- fakes ---------------------------------------------------------------
|
|
|
|
|
|
class _FakePower:
|
|
def __init__(self):
|
|
self.up_calls = 0
|
|
self.down_calls = 0
|
|
|
|
def power_up(self):
|
|
self.up_calls += 1
|
|
|
|
def power_down(self):
|
|
self.down_calls += 1
|
|
|
|
|
|
class _FakeTransport:
|
|
def __init__(self):
|
|
self.raw_frames = []
|
|
|
|
def ld_put_raw(self, data, baudrate):
|
|
self.raw_frames.append((bytes(data), int(baudrate)))
|
|
|
|
|
|
class _FakeLinDev:
|
|
def __init__(self, transport):
|
|
self.baudrate = 0
|
|
self.tx = []
|
|
self._transport = transport
|
|
# Pre-canned slave responses keyed by frame_id
|
|
self.slave_responses = {0x11: [0x07, 0x00, 0x00, 0x00]}
|
|
self.fail_on_recv_id = None
|
|
|
|
def get_device(self, name):
|
|
if name == "bus/transport_layer":
|
|
return self._transport
|
|
raise KeyError(name)
|
|
|
|
def send_message(self, master_to_slave, frame_id, data_length, data=None):
|
|
if master_to_slave:
|
|
self.tx.append((int(frame_id), int(data_length), list(data or [])))
|
|
return None
|
|
# slave-to-master
|
|
if self.fail_on_recv_id == int(frame_id):
|
|
raise RuntimeError("simulated rx timeout")
|
|
return self.slave_responses.get(int(frame_id))
|
|
|
|
|
|
class _FakeLinMaster:
|
|
def __init__(self):
|
|
self.setup_calls = 0
|
|
self.teardown_calls = 0
|
|
|
|
def setup(self):
|
|
self.setup_calls += 1
|
|
|
|
def teardown(self):
|
|
self.teardown_calls += 1
|
|
|
|
|
|
class _FakeMUM:
|
|
"""Stand-in for pymumclient.MelexisUniversalMaster()."""
|
|
def __init__(self):
|
|
self.opened_with = None
|
|
self._lin_master = _FakeLinMaster()
|
|
self._power = _FakePower()
|
|
self._transport = _FakeTransport()
|
|
self._lin_dev = _FakeLinDev(self._transport)
|
|
|
|
def open_all(self, host):
|
|
self.opened_with = host
|
|
|
|
def get_device(self, name):
|
|
if name == "lin0":
|
|
return self._lin_master
|
|
if name == "power_out0":
|
|
return self._power
|
|
raise KeyError(name)
|
|
|
|
|
|
class _FakeMumModule:
|
|
def __init__(self):
|
|
self.last = None
|
|
|
|
def MelexisUniversalMaster(self): # noqa: N802 - matches vendor API
|
|
self.last = _FakeMUM()
|
|
return self.last
|
|
|
|
|
|
class _FakePylinModule:
|
|
"""Stand-in for pylin: provides LinBusManager and LinDevice22."""
|
|
def __init__(self, lin_dev_factory):
|
|
# lin_dev_factory(lin_bus) returns an object with the .get_device,
|
|
# .send_message and .baudrate API used by MumLinInterface.
|
|
self._lin_dev_factory = lin_dev_factory
|
|
|
|
def LinBusManager(self, linmaster): # noqa: N802
|
|
return ("bus_for", linmaster)
|
|
|
|
def LinDevice22(self, lin_bus): # noqa: N802
|
|
return self._lin_dev_factory(lin_bus)
|
|
|
|
|
|
# ---- helpers -------------------------------------------------------------
|
|
|
|
|
|
def _build_iface(boot_settle=0.0):
|
|
"""Construct a MumLinInterface wired to fake modules; return (iface, fakes)."""
|
|
mum_mod = _FakeMumModule()
|
|
|
|
# Pylin's LinDevice22 should hand back the same FakeLinDev that's
|
|
# attached to the MUM instance for this test, so assertions can read tx.
|
|
captured = {}
|
|
|
|
def lin_dev_factory(lin_bus):
|
|
# The mum module's get_device('lin0') will be called from connect();
|
|
# but pylin.LinDevice22(lin_bus) just needs to expose the same API.
|
|
# We pull the FakeLinDev off the FakeMUM that was constructed.
|
|
captured["lin_dev"] = mum_mod.last._lin_dev
|
|
return mum_mod.last._lin_dev
|
|
|
|
pylin_mod = _FakePylinModule(lin_dev_factory)
|
|
iface = MumLinInterface(
|
|
host="10.0.0.1",
|
|
boot_settle_seconds=boot_settle,
|
|
mum_module=mum_mod,
|
|
pylin_module=pylin_mod,
|
|
)
|
|
return iface, mum_mod, captured
|
|
|
|
|
|
# ---- tests ---------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_connect_opens_mum_and_powers_up():
|
|
iface, mum_mod, _ = _build_iface()
|
|
iface.connect()
|
|
try:
|
|
assert mum_mod.last.opened_with == "10.0.0.1"
|
|
assert mum_mod.last._lin_master.setup_calls == 1
|
|
assert mum_mod.last._power.up_calls == 1
|
|
assert iface._lin_dev.baudrate == 19200
|
|
finally:
|
|
iface.disconnect()
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_disconnect_powers_down_and_tears_down():
|
|
iface, mum_mod, _ = _build_iface()
|
|
iface.connect()
|
|
iface.disconnect()
|
|
assert mum_mod.last._power.down_calls == 1
|
|
assert mum_mod.last._lin_master.teardown_calls == 1
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_send_publishes_master_frame():
|
|
iface, mum_mod, _ = _build_iface()
|
|
iface.connect()
|
|
try:
|
|
iface.send(LinFrame(id=0x0A, data=bytes([1, 2, 3, 4, 5, 6, 7, 8])))
|
|
tx = mum_mod.last._lin_dev.tx
|
|
assert tx == [(0x0A, 8, [1, 2, 3, 4, 5, 6, 7, 8])]
|
|
finally:
|
|
iface.disconnect()
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_receive_uses_frame_lengths_default():
|
|
iface, _, _ = _build_iface()
|
|
iface.connect()
|
|
try:
|
|
frame = iface.receive(id=0x11, timeout=0.1)
|
|
assert frame is not None
|
|
assert frame.id == 0x11
|
|
# Default frame_lengths maps 0x11 -> 4
|
|
assert len(frame.data) == 4
|
|
assert frame.data[0] == 0x07
|
|
finally:
|
|
iface.disconnect()
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_receive_returns_none_on_pylin_exception():
|
|
iface, mum_mod, _ = _build_iface()
|
|
iface.connect()
|
|
try:
|
|
mum_mod.last._lin_dev.fail_on_recv_id = 0x11
|
|
assert iface.receive(id=0x11, timeout=0.1) is None
|
|
finally:
|
|
iface.disconnect()
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_receive_without_id_raises():
|
|
iface, _, _ = _build_iface()
|
|
iface.connect()
|
|
try:
|
|
with pytest.raises(NotImplementedError):
|
|
iface.receive(id=None)
|
|
finally:
|
|
iface.disconnect()
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_send_raw_uses_classic_checksum_path():
|
|
iface, mum_mod, _ = _build_iface()
|
|
iface.connect()
|
|
try:
|
|
iface.send_raw(b"\x7f\x06\xb5\xff\x7f\x01\x02\xff")
|
|
raw = mum_mod.last._transport.raw_frames
|
|
assert len(raw) == 1
|
|
assert raw[0][0] == b"\x7f\x06\xb5\xff\x7f\x01\x02\xff"
|
|
assert raw[0][1] == 19200
|
|
finally:
|
|
iface.disconnect()
|
|
|
|
|
|
@pytest.mark.unit
|
|
def test_power_cycle_calls_down_then_up():
|
|
iface, mum_mod, _ = _build_iface()
|
|
iface.connect()
|
|
try:
|
|
iface.power_cycle(wait=0.0)
|
|
finally:
|
|
iface.disconnect()
|
|
assert mum_mod.last._power.up_calls >= 2 # initial connect + cycle
|
|
assert mum_mod.last._power.down_calls >= 1
|