🎉 Initial commit
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user