175 lines
6.0 KiB
Python
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]" |