"""Tests for the SDT (structured data type) parser and type enrichment.""" from __future__ import annotations from pathlib import Path import pytest from mitsubishi_label_converter.excel_reader import read_workbook from mitsubishi_label_converter.excel import write_workbook from mitsubishi_label_converter.models import ( GlobalVarSection, ProjectMetadata, StructMember, Variable, ) from mitsubishi_label_converter.parser import parse_global_vars from mitsubishi_label_converter.sdt_parser import ( apply_struct_types, parse_struct_definitions, ) NS = "http://www.plcopen.org/xml/tc6_0201" SDT_XML = f""" """ GLOBAL_XML = f""" """ @pytest.fixture() def sdt_file(tmp_path: Path) -> Path: path = tmp_path / "SDT.xml" path.write_bytes(SDT_XML.encode("utf-16")) return path @pytest.fixture() def global_file(tmp_path: Path) -> Path: path = tmp_path / "Global.xml" path.write_bytes(GLOBAL_XML.encode("utf-16")) return path def test_parse_struct_definitions(sdt_file: Path) -> None: definitions = parse_struct_definitions(sdt_file) assert definitions["ST_Din_STA"] == { "Value": "BOOL", "Status": "WORD", "Name": "STRING[16]", "Levels": "ARRAY[1..4] OF INT", "Sub": "ST_Sub", } assert definitions["ST_Sub"] == {"Flag": "BOOL"} def test_apply_struct_types(global_file: Path, sdt_file: Path) -> None: sections = parse_global_vars(global_file) definitions = parse_struct_definitions(sdt_file) apply_struct_types(sections, definitions) di4 = sections[0].variables[0] assert [(m.name, m.type_name) for m in di4.members] == [ ("Value", "BOOL"), ("Status", "WORD"), ("Name", "STRING[16]"), ("Levels", "ARRAY[1..4] OF INT"), ("Sub", "ST_Sub"), ("Unknown", ""), # not part of the struct definition ] # FB_Din is not defined in the SDT file: members stay untyped fb = sections[0].variables[1] assert [(m.name, m.type_name) for m in fb.members] == [("Value", "")] def test_excel_round_trip_with_member_types( global_file: Path, sdt_file: Path, tmp_path: Path ) -> None: """Members with types survive the Excel write/read round-trip.""" sections = parse_global_vars(global_file) definitions = parse_struct_definitions(sdt_file) apply_struct_types(sections, definitions) out = tmp_path / "out.xlsx" write_workbook(sections, out, metadata=ProjectMetadata()) read_sections, _ = read_workbook(out) assert read_sections == sections def test_real_files_sdt_enrichment() -> None: """Enrich the real Global.xml with the real SDT.xml and verify.""" assets = Path(__file__).parents[1] / "assets" global_xml = assets / "Global.xml" sdt_xml = assets / "SDT.xml" if not global_xml.is_file() or not sdt_xml.is_file(): pytest.skip("assets not present") sections = parse_global_vars(global_xml) definitions = parse_struct_definitions(sdt_xml) # All 31 struct types carrying device assignments are ST_* and defined # in SDT.xml (FB_* instances have no device-assigned members). used = {v.type_name for s in sections for v in s.variables if v.members} assert len(used) == 31 assert all(name.startswith("ST_") for name in used) assert used <= set(definitions) apply_struct_types(sections, definitions) typed = sum( 1 for s in sections for v in s.variables for m in v.members if m.type_name ) total = sum(len(v.members) for s in sections for v in s.variables) # Every device-assigned member belongs to an ST_* type defined in the # SDT file, so every member gets a type. assert typed == total def test_real_sdt_definition_sample() -> None: assets = Path(__file__).parents[1] / "assets" sdt_xml = assets / "SDT.xml" if not sdt_xml.is_file(): pytest.skip("assets/SDT.xml not present") definitions = parse_struct_definitions(sdt_xml) assert len(definitions) == 164 assert definitions["ST_Din_STA"]["Value"] == "BOOL" assert definitions["ST_Din_STA"]["Status"] == "WORD"