feedBack/tests/test_gp2rs_xml_encoding.py
ChrisBeWithYou 32ed564006
fix(gp2rs): write arrangement XML as UTF-8 so GP import survives non-ASCII metadata (#984)
The GP→arrangement-XML writers persisted their output with
`Path.write_text(xml_str)` and no explicit encoding. On Windows that uses
the cp1252 default, so a non-ASCII metadata character — e.g. the © in an
album name like "Chrysalis©1982" — was written as the lone byte 0xA9.
The XML is read back as UTF-8 (expat's default), where 0xA9 is an invalid
start byte, so `parse_arrangement` died with:

    xml.etree.ElementTree.ParseError: not well-formed (invalid token): line 10, column 22

and the whole Guitar Pro import failed (HTTP 500). All three arrangement
XML writes (gp2rs.py, gp2rs_gpx.py ×2) now pin encoding="utf-8".

CI runs on Linux (UTF-8 default) so the bug was invisible there; the new
test pins the locale-independent contract at the source level plus a
round-trip of a © album name.


Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q

Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 00:56:32 -05:00

54 lines
2.0 KiB
Python

"""Regression: the GP→arrangement-XML writers must pin UTF-8.
A bare ``Path.write_text(xml_str)`` uses the platform's *default* text
encoding. On Windows that is cp1252, which encodes a non-ASCII metadata
character — e.g. the © in an album name like "Chrysalis©1982" — as the lone
byte 0xA9. The XML is then read back as UTF-8 (expat's default), where 0xA9
is an invalid start byte, so parsing dies with
not well-formed (invalid token): line N, column 22
CI runs on Linux (UTF-8 default), so the bug is invisible there and a plain
functional test would pass on the old code too. These assertions instead pin
the locale-independent contract directly.
"""
import inspect
import re
import xml.etree.ElementTree as ET
import gp2rs
import gp2rs_gpx
def test_arrangement_xml_writes_specify_utf8():
# Every write of the arrangement XML string must pass encoding="utf-8"
# so non-ASCII metadata survives regardless of the host locale.
for mod in (gp2rs, gp2rs_gpx):
src = inspect.getsource(mod)
bare = re.findall(r"\.write_text\(\s*xml_str\s*\)", src)
assert not bare, (
f"{mod.__name__}: XML write must pass encoding=\"utf-8\" — a bare "
f"write_text() uses the platform default (cp1252 on Windows) and "
f"mangles non-ASCII metadata into invalid UTF-8"
)
assert 'write_text(xml_str, encoding="utf-8")' in src, (
f"{mod.__name__}: expected a UTF-8-pinned arrangement XML write"
)
def test_utf8_write_round_trips_non_ascii_album():
# The behavioural end of the contract: a © album name written as UTF-8
# parses cleanly and reads back intact (the cp1252 write does not).
from pathlib import Path
import tempfile
xml_str = (
'<?xml version="1.0"?>\n<song>\n'
" <albumName>Chrysalis©1982</albumName>\n</song>\n"
)
path = Path(tempfile.mkdtemp()) / "arr.xml"
path.write_text(xml_str, encoding="utf-8")
root = ET.parse(path).getroot()
assert root.findtext("albumName") == "Chrysalis©1982"