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