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

175 lines
6.0 KiB
Python

"""Tests for the PLCopen XML parser."""
from __future__ import annotations
from pathlib import Path
import pytest
from mitsubishi_label_converter.parser import (
parse_global_vars,
parse_project_metadata,
)
NS = "http://www.plcopen.org/xml/tc6_0201"
SAMPLE_XML = f"""<?xml version="1.0" encoding="utf-16"?>
<project xmlns="{NS}">
<instances>
<configurations>
<configuration name="">
<globalVars name="Global">
<variable name="Always_ON" address="SM400">
<type><BOOL/></type>
<addData>
<data name="http://www.mitsubishielectric.com/xml/variableLineNumber"
handleUnknown="implementation">
<variableLineNumber number="3"/>
</data>
</addData>
</variable>
<variable name="version" address="D450">
<type><string length="12"/></type>
</variable>
<variable name="PLC_FEL">
<type><derived name="FB_Din"/></type>
<addData>
<data name="http://www.mitsubishielectric.com/xml/variableComments"
handleUnknown="implementation">
<variableComments>
<comment number="1">
<html xml:space="preserve"
xmlns="http://www.w3.org/1999/xhtml">PLC error active</html>
</comment>
</variableComments>
</data>
</addData>
</variable>
<variable name="PLC_FEL_STA">
<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"/>
</variableStructDeviceAssignment>
</data>
</addData>
</variable>
</globalVars>
<globalVars name="Global" retain="true">
<variable name="PulseTot" address="D600">
<type><WORD/></type>
</variable>
</globalVars>
</configuration>
</configurations>
</instances>
</project>
"""
@pytest.fixture()
def sample_file(tmp_path: Path) -> Path:
"""Write the sample XML as UTF-16 LE, like GX Works3 does."""
xml_path = tmp_path / "Global.xml"
xml_path.write_bytes(SAMPLE_XML.encode("utf-16"))
return xml_path
def test_parses_two_sections(sample_file: Path) -> None:
sections = parse_global_vars(sample_file)
assert len(sections) == 2
assert all(section.name == "Global" for section in sections)
def test_retain_flag_is_parsed(sample_file: Path) -> None:
sections = parse_global_vars(sample_file)
assert sections[0].retain is False
assert sections[1].retain is True
def test_primitive_type(sample_file: Path) -> None:
sections = parse_global_vars(sample_file)
first = sections[0].variables[0]
assert first.name == "Always_ON"
assert first.address == "SM400"
assert first.type_name == "BOOL"
assert first.comment == ""
def test_string_type_includes_length(sample_file: Path) -> None:
sections = parse_global_vars(sample_file)
version = sections[0].variables[1]
assert version.name == "version"
assert version.type_name == "STRING[12]"
def test_derived_type_uses_name(sample_file: Path) -> None:
sections = parse_global_vars(sample_file)
plc_fel = sections[0].variables[2]
assert plc_fel.address == "" # derived FB variable without device address
assert plc_fel.type_name == "FB_Din"
sta = sections[0].variables[3]
assert sta.type_name == "ST_Din_STA"
assert [(m.name, m.address) for m in sta.members] == [
("Value", "D0.0"),
("Status", "D1"),
]
def test_comment_extraction(sample_file: Path) -> None:
sections = parse_global_vars(sample_file)
plc_fel = sections[0].variables[2]
assert plc_fel.comment == "PLC error active"
def test_struct_members_are_parsed(sample_file: Path) -> None:
sections = parse_global_vars(sample_file)
# PLC_FEL (plain derived FB) has no member assignments
assert sections[0].variables[2].members == ()
def test_struct_members_from_real_file() -> None:
asset = Path(__file__).parents[1] / "assets" / "Global.xml"
if not asset.is_file():
pytest.skip("assets/Global.xml not present")
sections = parse_global_vars(asset)
structured = [v for s in sections for v in s.variables if v.members]
assert len(structured) == 189
sta = next(v for v in structured if v.name == "PLC_FEL_STA")
assert sta.type_name == "ST_Din_STA"
assert sta.line_number == 39
assert (sta.members[0].name, sta.members[0].address) == ("Value", "D0.0")
def test_project_metadata_from_real_file() -> None:
asset = Path(__file__).parents[1] / "assets" / "Global.xml"
if not asset.is_file():
pytest.skip("assets/Global.xml not present")
meta = parse_project_metadata(asset)
assert meta.file_header["productName"] == "GX Works3"
assert meta.content_header["name"] == "21014JON-B14_20260930"
assert meta.project_add_data["projectPlcType"] == "FX5U"
def test_line_number_is_parsed(sample_file: Path) -> None:
sections = parse_global_vars(sample_file)
always_on = sections[0].variables[0]
assert always_on.line_number == 3
def test_real_world_global_xml() -> None:
"""End-to-end check against the real asset in the repository."""
asset = Path(__file__).parents[1] / "assets" / "Global.xml"
if not asset.is_file():
pytest.skip("assets/Global.xml not present")
sections = parse_global_vars(asset)
assert len(sections) == 2
# 174/118 raw child elements include one <addData> (varListProperties) each
assert [len(s.variables) for s in sections] == [173, 117]
version_row = sections[0].variables[0]
assert version_row.name == "version"
assert version_row.address == "D450"
assert version_row.type_name == "STRING[12]"