154 lines
5.1 KiB
Python
154 lines
5.1 KiB
Python
"""Tests for the XML writer (Excel data -> GX Works3 XML)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from mitsubishi_label_converter.models import (
|
|
GlobalVarSection,
|
|
ProjectMetadata,
|
|
StructMember,
|
|
Variable,
|
|
)
|
|
from mitsubishi_label_converter.parser import (
|
|
parse_global_vars,
|
|
parse_project_metadata,
|
|
)
|
|
from mitsubishi_label_converter.xml_writer import write_xml
|
|
|
|
|
|
def make_sections() -> list[GlobalVarSection]:
|
|
return [
|
|
GlobalVarSection(
|
|
name="Global",
|
|
retain=False,
|
|
variables=[
|
|
Variable("Always_ON", "SM400", "BOOL", "", line_number=3),
|
|
Variable("version", "D450", "STRING[12]", ""),
|
|
Variable(
|
|
"PLC_FEL_STA",
|
|
"",
|
|
"ST_Din_STA",
|
|
"",
|
|
line_number=39,
|
|
members=(
|
|
StructMember("Value", "D0.0"),
|
|
StructMember("Status", "D1"),
|
|
),
|
|
),
|
|
Variable(
|
|
"Switch_status",
|
|
"SD200",
|
|
"INT",
|
|
"CPU switch status (0: RUN, 1: STOP)",
|
|
line_number=14,
|
|
),
|
|
],
|
|
),
|
|
GlobalVarSection(
|
|
name="Global",
|
|
retain=True,
|
|
variables=[
|
|
Variable(
|
|
"PulseTot", "D600", "WORD", "", line_number=31, retain=True
|
|
),
|
|
],
|
|
),
|
|
]
|
|
|
|
|
|
def make_metadata() -> ProjectMetadata:
|
|
return ProjectMetadata(
|
|
file_header={
|
|
"companyName": "MITSUBISHI ELECTRIC CORPORATION",
|
|
"companyURL": "http://www.mitsubishielectric.com",
|
|
"productName": "GX Works3",
|
|
"productVersion": "1.126G",
|
|
"creationDateTime": "2026-10-01T20:59:19+02:00",
|
|
},
|
|
content_header={
|
|
"name": "21014JON-B14_20260930",
|
|
"modificationDateTime": "2026-10-01T20:57:58+02:00",
|
|
},
|
|
project_add_data={"projectPlcType": "FX5U"},
|
|
)
|
|
|
|
|
|
def test_full_round_trip(tmp_path: Path) -> None:
|
|
"""XML -> sections -> XML: the parsed data must be identical."""
|
|
out = tmp_path / "regenerated.xml"
|
|
sections = make_sections()
|
|
write_xml(sections, make_metadata(), out)
|
|
|
|
# The regenerated file must parse back to the same sections.
|
|
reparsed = parse_global_vars(out)
|
|
assert reparsed == sections
|
|
|
|
meta = parse_project_metadata(out)
|
|
assert meta.file_header == make_metadata().file_header
|
|
assert meta.content_header == make_metadata().content_header
|
|
assert meta.project_add_data == make_metadata().project_add_data
|
|
|
|
|
|
def test_output_is_utf16_le(tmp_path: Path) -> None:
|
|
out = tmp_path / "regenerated.xml"
|
|
write_xml(make_sections(), make_metadata(), out)
|
|
raw = out.read_bytes()
|
|
# UTF-16 LE BOM
|
|
assert raw[:2] == b"\xff\xfe"
|
|
# XML declaration must declare utf-16
|
|
assert raw.decode("utf-16").startswith('<?xml version="1.0" encoding="utf-16"?>')
|
|
|
|
|
|
def test_retain_flag_written(tmp_path: Path) -> None:
|
|
out = tmp_path / "regenerated.xml"
|
|
write_xml(make_sections(), make_metadata(), out)
|
|
text = out.read_text(encoding="utf-16")
|
|
assert '<globalVars name="Global" retain="true">' in text
|
|
# the plain section: retain attribute must NOT be present
|
|
assert '<globalVars name="Global">' in text
|
|
|
|
|
|
def test_member_addresses_written(tmp_path: Path) -> None:
|
|
out = tmp_path / "regenerated.xml"
|
|
write_xml(make_sections(), make_metadata(), out)
|
|
text = out.read_text(encoding="utf-16")
|
|
assert 'member name="Value" address="D0.0"' in text
|
|
assert 'member name="Status" address="D1"' in text
|
|
|
|
|
|
def test_comment_written(tmp_path: Path) -> None:
|
|
out = tmp_path / "regenerated.xml"
|
|
write_xml(make_sections(), make_metadata(), out)
|
|
reparsed = parse_global_vars(out)
|
|
switch_status = reparsed[0].variables[3]
|
|
assert switch_status.comment == "CPU switch status (0: RUN, 1: STOP)"
|
|
|
|
|
|
def test_line_numbers_written(tmp_path: Path) -> None:
|
|
out = tmp_path / "regenerated.xml"
|
|
write_xml(make_sections(), make_metadata(), out)
|
|
reparsed = parse_global_vars(out)
|
|
assert [v.line_number for v in reparsed[0].variables] == [3, None, 39, 14]
|
|
|
|
|
|
def test_real_file_full_round_trip(tmp_path: Path) -> None:
|
|
"""The real asset: XML -> sections -> XML -> sections must be stable."""
|
|
asset = Path(__file__).parents[1] / "assets" / "Global.xml"
|
|
if not asset.is_file():
|
|
import pytest
|
|
|
|
pytest.skip("assets/Global.xml not present")
|
|
|
|
original_sections = parse_global_vars(asset)
|
|
original_meta = parse_project_metadata(asset)
|
|
|
|
out = tmp_path / "regenerated.xml"
|
|
write_xml(original_sections, original_meta, out)
|
|
|
|
reparsed_sections = parse_global_vars(out)
|
|
reparsed_meta = parse_project_metadata(out)
|
|
assert reparsed_sections == original_sections
|
|
assert reparsed_meta.file_header == original_meta.file_header
|
|
assert reparsed_meta.content_header == original_meta.content_header
|
|
assert reparsed_meta.project_add_data == original_meta.project_add_data |