Files
2026-10-02 06:10:55 +02:00

197 lines
6.4 KiB
Python

"""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"""<?xml version="1.0" encoding="utf-16"?>
<project xmlns="{NS}">
<types>
<dataTypes>
<dataType name="ST_Din_STA">
<baseType>
<struct>
<variable name="Value"><type><BOOL/></type></variable>
<variable name="Status"><type><WORD/></type></variable>
<variable name="Name">
<type><string length="16"/></type>
</variable>
<variable name="Levels">
<type><array><dimension lower="1" upper="4"/>
<baseType><INT/></baseType></array></type>
</variable>
<variable name="Sub"><type><derived name="ST_Sub"/></type></variable>
</struct>
</baseType>
</dataType>
<dataType name="ST_Sub">
<baseType>
<struct>
<variable name="Flag"><type><BOOL/></type></variable>
</struct>
</baseType>
</dataType>
</dataTypes>
</types>
</project>
"""
GLOBAL_XML = f"""<?xml version="1.0" encoding="utf-16"?>
<project xmlns="{NS}">
<instances>
<configurations>
<configuration name="">
<globalVars name="Global">
<variable name="DI4">
<type><derived name="ST_Din_STA"/></type>
<addData>
<data name="http://www.mitsubishielectric.com/xml/variableStructDeviceAssignment"
handleUnknown="implementation">
<variableStructDeviceAssignment>
<member name="Value" address="D0.0"/>
<member name="Status" address="D1"/>
<member name="Name" address="D10"/>
<member name="Levels" address="D20"/>
<member name="Sub" address="D30"/>
<member name="Unknown" address="D40"/>
</variableStructDeviceAssignment>
</data>
</addData>
</variable>
<variable name="FB_Instance">
<type><derived name="FB_Din"/></type>
<addData>
<data name="http://www.mitsubishielectric.com/xml/variableStructDeviceAssignment"
handleUnknown="implementation">
<variableStructDeviceAssignment>
<member name="Value" address="R0.0"/>
</variableStructDeviceAssignment>
</data>
</addData>
</variable>
</globalVars>
</configuration>
</configurations>
</instances>
</project>
"""
@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"