ecu-tests/tests/unit/test_mum_adapter_mocked.py

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