fix: atomic write, tag preservation, invalid preset error, additional tests

This commit is contained in:
pyr0ball 2026-05-25 19:39:56 -07:00
parent 47faae74f4
commit 2677ccc42b
3 changed files with 50 additions and 18 deletions

5
conftest.py Normal file
View file

@ -0,0 +1,5 @@
# conftest.py
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent / "scripts"))

View file

@ -8,7 +8,9 @@ Usage:
--preset cc|cs6 --preset cc|cs6
""" """
import argparse import argparse
import os
import shutil import shutil
import tempfile
from pathlib import Path from pathlib import Path
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
@ -24,14 +26,19 @@ PRESET_OVERRIDES = {
} }
def _find_or_create(parent: ET.Element, id_val: str) -> ET.Element: def _atomic_write(tree: ET.ElementTree, path: Path) -> None:
"""Return child with matching id, or create and append a new one.""" """Write XML tree atomically — prevents corruption on interrupt."""
for child in parent: fd, tmp = tempfile.mkstemp(dir=path.parent, suffix=".tmp")
if child.get("id") == id_val: try:
return child with os.fdopen(fd, "w", encoding="utf-8") as fh:
child = ET.SubElement(parent, "group") tree.write(fh, encoding="unicode", xml_declaration=True)
child.set("id", id_val) os.replace(tmp, path)
return child except Exception:
try:
os.unlink(tmp)
except OSError:
pass
raise
def _merge_nodes(live_parent: ET.Element, patch_parent: ET.Element) -> None: def _merge_nodes(live_parent: ET.Element, patch_parent: ET.Element) -> None:
@ -40,24 +47,30 @@ def _merge_nodes(live_parent: ET.Element, patch_parent: ET.Element) -> None:
child_id = patch_child.get("id") child_id = patch_child.get("id")
if child_id is None: if child_id is None:
continue continue
live_child = next(
live_child = _find_or_create(live_parent, child_id) (c for c in live_parent if c.get("id") == child_id), None
)
# Merge attributes: only set attrs present in patch; preserve unknown attrs if live_child is None:
live_child = ET.SubElement(live_parent, patch_child.tag)
live_child.set("id", child_id)
for attr, value in patch_child.attrib.items(): for attr, value in patch_child.attrib.items():
if attr != "id": if attr != "id":
live_child.set(attr, value) live_child.set(attr, value)
# Recurse into children
_merge_nodes(live_child, patch_child) _merge_nodes(live_child, patch_child)
def _apply_preset(root: ET.Element, preset: str) -> None: def _apply_preset(root: ET.Element, preset: str) -> None:
"""Apply preset-specific overrides (active keys file, document units).""" """Apply preset-specific overrides (active keys file, document units)."""
if preset not in PRESET_OVERRIDES:
raise ValueError(f"Unknown preset {preset!r}. Valid: {list(PRESET_OVERRIDES)}")
for id_path, attrs in PRESET_OVERRIDES[preset].items(): for id_path, attrs in PRESET_OVERRIDES[preset].items():
node = root node = root
for id_val in id_path: for id_val in id_path:
node = _find_or_create(node, id_val) child = next((c for c in node if c.get("id") == id_val), None)
if child is None:
child = ET.SubElement(node, "group")
child.set("id", id_val)
node = child
for attr, value in attrs.items(): for attr, value in attrs.items():
node.set(attr, value) node.set(attr, value)
@ -73,7 +86,7 @@ def merge(prefs_path: Path, patch_path: Path, preset: str) -> None:
shutil.copy(patch_path, prefs_path) shutil.copy(patch_path, prefs_path)
live_tree = ET.parse(prefs_path) live_tree = ET.parse(prefs_path)
_apply_preset(live_tree.getroot(), preset) _apply_preset(live_tree.getroot(), preset)
live_tree.write(str(prefs_path), encoding="unicode", xml_declaration=True) _atomic_write(live_tree, prefs_path)
return return
live_tree = ET.parse(prefs_path) # raises ParseError on malformed XML live_tree = ET.parse(prefs_path) # raises ParseError on malformed XML

View file

@ -1,12 +1,10 @@
"""Tests for scripts/merge_prefs.py""" """Tests for scripts/merge_prefs.py"""
import shutil import shutil
import sys
from pathlib import Path from pathlib import Path
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
import pytest import pytest
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
import merge_prefs import merge_prefs
FIXTURES = Path(__file__).parent / "fixtures" FIXTURES = Path(__file__).parent / "fixtures"
@ -138,3 +136,19 @@ def test_merge_raises_on_missing_patch(tmp_path):
shutil.copy(FIXTURES / "prefs-fresh.xml", prefs) shutil.copy(FIXTURES / "prefs-fresh.xml", prefs)
with pytest.raises(FileNotFoundError): with pytest.raises(FileNotFoundError):
merge_prefs.merge(prefs, tmp_path / "nonexistent-patch.xml", "cc") merge_prefs.merge(prefs, tmp_path / "nonexistent-patch.xml", "cc")
def test_merge_raises_on_malformed_patch(tmp_path):
prefs = tmp_path / "preferences.xml"
shutil.copy(FIXTURES / "prefs-fresh.xml", prefs)
bad_patch = tmp_path / "bad-patch.xml"
bad_patch.write_text("not xml at all")
with pytest.raises(ET.ParseError):
merge_prefs.merge(prefs, bad_patch, "cc")
def test_merge_raises_on_invalid_preset(tmp_path):
prefs = tmp_path / "preferences.xml"
shutil.copy(FIXTURES / "prefs-fresh.xml", prefs)
with pytest.raises(ValueError, match="Unknown preset"):
merge_prefs.merge(prefs, FIXTURES / "patch-minimal.xml", "bad-preset")