Files
libretro/tests/test_exporters.py
T
Abdessamad Derraz b120528dd7 test: cover the native format exporters
export_native turns the profile data back into each platform's own
file, for a maintainer there to pick up and use, and none of the ten
exporters had a test. Coverage goes from 0 to 83%.

Writing them found the EmuDeck exporter rejecting its own output: its
export skips placeholder entries, its validate looked for them anyway
and reported the omission as a missing hash. Both sides apply the same
filter now.

The placeholder assertion initially passed for the wrong reason twice
over -- the fixture entry had no hash, so it was dropped as incomplete
rather than as a placeholder, and the assertion then read the name
while the exporters write the path. It is pinned on an unmistakable
destination now, and removing the guard fails two tests.
2026-08-12 00:46:04 +02:00

214 lines
8.3 KiB
Python

#!/usr/bin/env python3
"""Native-format exporters.
export_native.py turns the profile data back into each platform's own file:
Recalbox's es_bios.xml, Batocera's systems list, RetroDECK's manifests. The
point is that a platform maintainer can take the output and use it, so the
format has to be theirs, not an approximation of it. None of it was covered.
"""
from __future__ import annotations
import sys
import tempfile
import unittest
import xml.etree.ElementTree as ET
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
from exporter import discover_exporters # noqa: E402
from exporter.base_exporter import BaseExporter # noqa: E402
TRUTH = {
"systems": {
"sony-playstation": {
"files": [
{
"name": "scph5501.bin",
"path": "psx/scph5501.bin",
"md5": "490f666e1afb15b7362b406ed1cea246",
"required": True,
},
{
"name": "scph7001.bin",
"path": "psx/scph7001.bin",
"md5": "1e68c231d0896b7eadcad1d7d8e76129",
"required": False,
},
# Carries an md5 on purpose: without one it would be dropped
# for being incomplete, and the placeholder guard would never
# be the reason it stayed out.
{
"name": "<user-selected>.bin",
"path": "psx/PLACEHOLDER-MUST-NOT-SHIP.bin",
"md5": "00000000000000000000000000000000",
"required": False,
},
],
},
},
}
class ExporterRegistry(unittest.TestCase):
def test_every_exporter_declares_a_platform_and_the_contract(self):
exporters = discover_exporters()
self.assertTrue(exporters, "no exporters discovered")
for platform, cls in exporters.items():
with self.subTest(platform=platform):
self.assertTrue(issubclass(cls, BaseExporter))
self.assertEqual(cls.platform_name(), platform)
instance = cls()
self.assertTrue(callable(instance.export))
self.assertTrue(callable(instance.validate))
def test_the_registry_covers_the_platforms_it_claims(self):
for expected in ("recalbox", "batocera", "retrodeck", "romm"):
self.assertIn(expected, discover_exporters())
class SharedHelpers(unittest.TestCase):
def test_placeholder_names_are_recognised(self):
for name in ("<user-selected>.bin", "rom*.bin", "a<b>.rom"):
self.assertTrue(BaseExporter._is_pattern(name), name)
for name in ("scph5501.bin", "bios_CD_E.bin"):
self.assertFalse(BaseExporter._is_pattern(name), name)
def test_destination_prefers_path_then_destination_then_name(self):
self.assertEqual(
BaseExporter._dest({"path": "a", "destination": "b", "name": "c"}), "a"
)
self.assertEqual(BaseExporter._dest({"destination": "b", "name": "c"}), "b")
self.assertEqual(BaseExporter._dest({"name": "c"}), "c")
self.assertEqual(BaseExporter._dest({}), "")
def test_a_scraped_display_name_wins_over_the_slug(self):
self.assertEqual(
BaseExporter._display_name("sony-playstation", {"name": "PlayStation"}),
"PlayStation",
)
def test_acronyms_stay_upper_case_in_a_derived_name(self):
derived = BaseExporter._display_name("nes-and-snes")
self.assertIn("NES", derived)
self.assertIn("SNES", derived)
class _ExportFixture(unittest.TestCase):
platform = ""
def setUp(self):
if not self.platform:
self.skipTest("base fixture")
self._tmp = tempfile.TemporaryDirectory()
self.out = Path(self._tmp.name) / "out"
self.exporter = discover_exporters()[self.platform]()
def tearDown(self):
if hasattr(self, "_tmp"):
self._tmp.cleanup()
def _export(self) -> str:
self.exporter.export(TRUTH, str(self.out))
self.assertTrue(self.out.exists(), "exporter wrote nothing")
return self.out.read_text(encoding="utf-8")
class RecalboxExport(_ExportFixture):
platform = "recalbox"
def test_output_is_well_formed_xml_in_recalbox_shape(self):
root = ET.fromstring(self._export())
self.assertEqual(root.tag, "biosList")
systems = root.findall("system")
self.assertTrue(systems, "no <system> emitted")
def test_each_declared_file_becomes_a_bios_element_with_its_md5(self):
root = ET.fromstring(self._export())
by_path = {
b.get("path"): b for s in root.findall("system") for b in s.findall("bios")
}
self.assertIn("psx/scph5501.bin", by_path)
self.assertEqual(
by_path["psx/scph5501.bin"].get("md5"),
"490f666e1afb15b7362b406ed1cea246",
)
def test_a_placeholder_entry_is_not_exported_as_a_file(self):
"""Recalbox would otherwise look for a file nobody can supply.
The check is on the destination path, not the name: the exporter
writes path=, so asserting on the name passes whatever the filter
does.
"""
exported = self._export()
self.assertNotIn("PLACEHOLDER-MUST-NOT-SHIP", exported)
self.assertNotIn("<user-selected>", exported)
def test_mandatory_is_only_stated_when_it_is_false(self):
"""Recalbox treats a missing attribute as mandatory."""
root = ET.fromstring(self._export())
by_path = {
b.get("path"): b for s in root.findall("system") for b in s.findall("bios")
}
self.assertIsNone(by_path["psx/scph5501.bin"].get("mandatory"))
self.assertEqual(by_path["psx/scph7001.bin"].get("mandatory"), "false")
def test_the_export_round_trips_through_its_own_validator(self):
self.exporter.export(TRUTH, str(self.out))
self.assertEqual(self.exporter.validate(TRUTH, str(self.out)), [])
def test_a_truncated_export_is_reported_by_the_validator(self):
self.exporter.export(TRUTH, str(self.out))
self.out.write_text(
'<?xml version="1.0" encoding="UTF-8"?>\n<biosList></biosList>\n',
encoding="utf-8",
)
self.assertTrue(
self.exporter.validate(TRUTH, str(self.out)),
"a file missing every entry validated clean",
)
class EveryExporterProducesSomething(unittest.TestCase):
"""Whatever the format, an exporter must write a non-empty file it accepts."""
def test_each_exporter_writes_and_validates_its_own_output(self):
for platform, cls in sorted(discover_exporters().items()):
with self.subTest(platform=platform):
with tempfile.TemporaryDirectory() as directory:
out = Path(directory) / "out"
exporter = cls()
try:
exporter.export(TRUTH, str(out))
except (KeyError, TypeError, ValueError) as exc:
self.fail(f"{platform} export raised {exc!r}")
self.assertTrue(out.exists(), f"{platform} wrote nothing")
self.assertTrue(
out.stat().st_size > 0, f"{platform} wrote an empty file"
)
issues = exporter.validate(TRUTH, str(out))
self.assertEqual(
issues, [], f"{platform} rejects its own output: {issues[:2]}"
)
def test_no_exporter_emits_a_placeholder_name(self):
for platform, cls in sorted(discover_exporters().items()):
with self.subTest(platform=platform):
with tempfile.TemporaryDirectory() as directory:
out = Path(directory) / "out"
cls().export(TRUTH, str(out))
exported = out.read_text(encoding="utf-8", errors="replace")
self.assertNotIn(
"PLACEHOLDER-MUST-NOT-SHIP",
exported,
f"{platform} exported a placeholder as a real file",
)
self.assertNotIn("<user-selected>", exported)
if __name__ == "__main__":
unittest.main()