"""Unit tests for LdfDatabase / Frame using the 4SEVEN LDF as fixture data.""" from __future__ import annotations import pathlib import pytest # Skip the whole module if ldfparser isn't installed. pytest.importorskip("ldfparser", reason="ldfparser is required for LDF unit tests") from ecu_framework.lin.ldf import Frame, FrameNotFound, LdfDatabase WORKSPACE_ROOT = pathlib.Path(__file__).resolve().parents[2] LDF_PATH = WORKSPACE_ROOT / "vendor" / "4SEVEN_color_lib_test.ldf" @pytest.fixture(scope="module") def db() -> LdfDatabase: return LdfDatabase(LDF_PATH) @pytest.mark.unit def test_loads_metadata(db: LdfDatabase): assert db.protocol_version in ("2.1", "2.0", "1.3") assert db.baudrate == 19200 @pytest.mark.unit def test_lookup_by_name_and_id(db: LdfDatabase): by_name = db.frame("ALM_Req_A") by_id = db.frame(0x0A) assert by_name.id == 0x0A == by_id.id assert by_name.name == "ALM_Req_A" == by_id.name assert by_name.length == 8 @pytest.mark.unit def test_unknown_frame_raises(db: LdfDatabase): with pytest.raises(FrameNotFound): db.frame("not_a_real_frame") @pytest.mark.unit def test_signal_layout_matches_ldf(db: LdfDatabase): layout = db.frame("ALM_Req_A").signal_layout() # spot-check a couple of entries from the LDF Frames block assert (0, "AmbLightColourRed", 8) in layout assert (32, "AmbLightUpdate", 2) in layout assert (34, "AmbLightMode", 6) in layout assert (56, "AmbLightLIDTo", 8) in layout @pytest.mark.unit def test_pack_kwargs_full_payload(db: LdfDatabase): frame = db.frame("ALM_Req_A") payload = frame.pack( AmbLightColourRed=0xFF, AmbLightColourGreen=0xFF, AmbLightColourBlue=0xFF, AmbLightIntensity=0xFF, AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=0, AmbLightLIDFrom=0x01, AmbLightLIDTo=0x01, ) assert isinstance(payload, bytes) assert len(payload) == 8 assert payload == bytes.fromhex("ffffffff00000101") @pytest.mark.unit def test_pack_unspecified_signals_use_init_value(db: LdfDatabase): """LDF defines non-zero init_values for ColorConfigFrameRed signals; pack() with no kwargs should fall back to those defaults.""" frame = db.frame("ColorConfigFrameRed") payload = frame.pack() decoded = frame.unpack(payload) # ColorConfigFrameRed_X init_value is 5665, _Y is 2396, _Z is 0, _Vf_Cal is 2031 assert decoded["ColorConfigFrameRed_X"] == 5665 assert decoded["ColorConfigFrameRed_Y"] == 2396 assert decoded["ColorConfigFrameRed_Z"] == 0 assert decoded["ColorConfigFrameRed_Vf_Cal"] == 2031 @pytest.mark.unit def test_pack_dict_argument(db: LdfDatabase): frame = db.frame("ALM_Req_A") a = frame.pack(AmbLightColourRed=0x12, AmbLightColourBlue=0x34) b = frame.pack({"AmbLightColourRed": 0x12, "AmbLightColourBlue": 0x34}) assert a == b @pytest.mark.unit def test_pack_rejects_args_and_kwargs_together(db: LdfDatabase): frame = db.frame("ALM_Req_A") with pytest.raises(TypeError): frame.pack({"AmbLightColourRed": 1}, AmbLightColourGreen=2) @pytest.mark.unit def test_unpack_round_trip(db: LdfDatabase): frame = db.frame("ALM_Req_A") values = { "AmbLightColourRed": 0xAB, "AmbLightColourGreen": 0xCD, "AmbLightColourBlue": 0x12, "AmbLightIntensity": 0x80, "AmbLightUpdate": 2, # 2 bits "AmbLightMode": 0x15, # 6 bits "AmbLightDuration": 0x40, "AmbLightLIDFrom": 0x01, "AmbLightLIDTo": 0xFE, } payload = frame.pack(**values) decoded = frame.unpack(payload) for k, v in values.items(): assert decoded[k] == v, f"signal {k} mismatch: {decoded[k]} vs {v}" @pytest.mark.unit def test_alm_status_decode_real_payload(db: LdfDatabase): """ALM_Status: byte 0 carries ALMNadNo (8 bits at offset 0).""" frame = db.frame("ALM_Status") assert frame.length == 4 decoded = frame.unpack(b"\x07\x00\x00\x00") assert decoded["ALMNadNo"] == 7 @pytest.mark.unit def test_frame_lengths_includes_all_unconditional_frames(db: LdfDatabase): lengths = db.frame_lengths() assert lengths[0x0A] == 8 # ALM_Req_A assert lengths[0x11] == 4 # ALM_Status assert lengths[0x06] == 3 # ConfigFrame # Every entry should map to a positive length assert all(l >= 1 for l in lengths.values()) @pytest.mark.unit def test_frames_returns_wrapped_frame_objects(db: LdfDatabase): frames = db.frames() assert all(isinstance(f, Frame) for f in frames) names = {f.name for f in frames} assert {"ALM_Req_A", "ALM_Status", "ConfigFrame"}.issubset(names) @pytest.mark.unit def test_ldf_repr_does_not_explode(db: LdfDatabase): s = repr(db) assert "LdfDatabase" in s @pytest.mark.unit def test_missing_file_raises_filenotfounderror(tmp_path): with pytest.raises(FileNotFoundError): LdfDatabase(tmp_path / "nope.ldf")