197 lines
6.4 KiB
Python
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" |