Files
libretro/tests/test_severity.py
Abdessamad Derraz b9cdda07ee refactor: DRY consolidation + 83 unit tests
Moved shared functions to common.py (single source of truth):
- check_inside_zip (was in verify.py, imported by generate_pack)
- build_zip_contents_index (was duplicated in verify + generate_pack)
- load_emulator_profiles (was in verify, cross_reference, generate_site)
- group_identical_platforms (was in verify + generate_pack)

Added tests/ with 83 unit tests covering:
- resolve_local_file: SHA1, MD5, name, alias, truncated, zip_contents
- verify: existence, md5, zipped_file, multi-hash, severity mapping
- aliases: field parsing, by_name indexing, beetle_psx field rename
- pack: dedup, file_status, zipped_file inner check, EmuDeck entries
- severity: all 12 combinations, platform-native behavior

0 regressions: pipeline.py --all produces identical results.
2026-03-19 11:19:50 +01:00

185 lines
6.4 KiB
Python

"""Exhaustive severity mapping tests across all modes and statuses."""
from __future__ import annotations
import os
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
from verify import Status, Severity, compute_severity
class TestSeverityMappingExistence(unittest.TestCase):
"""Existence mode: RetroArch/Lakka/RetroPie behavior.
- OK = OK
- UNTESTED = OK (existence doesn't care about hash)
- MISSING + required = WARNING
- MISSING + optional = INFO
"""
MODE = "existence"
def test_ok_required(self):
self.assertEqual(compute_severity(Status.OK, True, self.MODE), Severity.OK)
def test_ok_optional(self):
self.assertEqual(compute_severity(Status.OK, False, self.MODE), Severity.OK)
def test_untested_required(self):
self.assertEqual(compute_severity(Status.UNTESTED, True, self.MODE), Severity.OK)
def test_untested_optional(self):
self.assertEqual(compute_severity(Status.UNTESTED, False, self.MODE), Severity.OK)
def test_missing_required(self):
self.assertEqual(compute_severity(Status.MISSING, True, self.MODE), Severity.WARNING)
def test_missing_optional(self):
self.assertEqual(compute_severity(Status.MISSING, False, self.MODE), Severity.INFO)
class TestSeverityMappingMd5(unittest.TestCase):
"""MD5 mode: Batocera/RetroBat/EmuDeck behavior.
- OK = OK
- UNTESTED + required = WARNING
- UNTESTED + optional = WARNING
- MISSING + required = CRITICAL
- MISSING + optional = WARNING
Batocera has no required/optional distinction in practice,
but the severity function handles it for Recalbox compatibility.
"""
MODE = "md5"
def test_ok_required(self):
self.assertEqual(compute_severity(Status.OK, True, self.MODE), Severity.OK)
def test_ok_optional(self):
self.assertEqual(compute_severity(Status.OK, False, self.MODE), Severity.OK)
def test_untested_required(self):
self.assertEqual(compute_severity(Status.UNTESTED, True, self.MODE), Severity.WARNING)
def test_untested_optional(self):
self.assertEqual(compute_severity(Status.UNTESTED, False, self.MODE), Severity.WARNING)
def test_missing_required(self):
self.assertEqual(compute_severity(Status.MISSING, True, self.MODE), Severity.CRITICAL)
def test_missing_optional(self):
self.assertEqual(compute_severity(Status.MISSING, False, self.MODE), Severity.WARNING)
class TestSeverityBatoceraBehavior(unittest.TestCase):
"""Batocera has no required distinction: all files are treated equally.
In practice, Batocera YAMLs don't set required=True/False,
so the default (True) applies. Both required and optional
untested files get WARNING severity.
"""
def test_batocera_no_required_distinction_for_untested(self):
sev_req = compute_severity(Status.UNTESTED, True, "md5")
sev_opt = compute_severity(Status.UNTESTED, False, "md5")
self.assertEqual(sev_req, sev_opt)
self.assertEqual(sev_req, Severity.WARNING)
class TestSeverityRecalboxBehavior(unittest.TestCase):
"""Recalbox has mandatory field: missing mandatory = CRITICAL (RED).
Recalbox uses md5 mode with mandatory (required) distinction.
Missing mandatory = CRITICAL (Bios.cpp RED)
Missing optional = WARNING (Bios.cpp YELLOW)
"""
def test_recalbox_mandatory_missing_is_critical(self):
self.assertEqual(
compute_severity(Status.MISSING, True, "md5"),
Severity.CRITICAL,
)
def test_recalbox_optional_missing_is_warning(self):
self.assertEqual(
compute_severity(Status.MISSING, False, "md5"),
Severity.WARNING,
)
def test_recalbox_ok_is_ok(self):
self.assertEqual(
compute_severity(Status.OK, True, "md5"),
Severity.OK,
)
class TestSeverityRetroArchBehavior(unittest.TestCase):
"""RetroArch existence mode: required missing = WARNING, optional = INFO."""
def test_retroarch_required_missing_is_warning(self):
self.assertEqual(
compute_severity(Status.MISSING, True, "existence"),
Severity.WARNING,
)
def test_retroarch_optional_missing_is_info(self):
self.assertEqual(
compute_severity(Status.MISSING, False, "existence"),
Severity.INFO,
)
def test_retroarch_untested_ignored(self):
"""Existence mode ignores untested (hash doesn't matter)."""
self.assertEqual(
compute_severity(Status.UNTESTED, True, "existence"),
Severity.OK,
)
class TestSeverityAllCombinations(unittest.TestCase):
"""Exhaustive matrix: all status x required x mode combinations."""
EXPECTED = {
# (status, required, mode): severity
(Status.OK, True, "existence"): Severity.OK,
(Status.OK, False, "existence"): Severity.OK,
(Status.OK, True, "md5"): Severity.OK,
(Status.OK, False, "md5"): Severity.OK,
(Status.UNTESTED, True, "existence"): Severity.OK,
(Status.UNTESTED, False, "existence"): Severity.OK,
(Status.UNTESTED, True, "md5"): Severity.WARNING,
(Status.UNTESTED, False, "md5"): Severity.WARNING,
(Status.MISSING, True, "existence"): Severity.WARNING,
(Status.MISSING, False, "existence"): Severity.INFO,
(Status.MISSING, True, "md5"): Severity.CRITICAL,
(Status.MISSING, False, "md5"): Severity.WARNING,
}
def test_all_combinations(self):
for (status, required, mode), expected_severity in self.EXPECTED.items():
with self.subTest(status=status, required=required, mode=mode):
actual = compute_severity(status, required, mode)
self.assertEqual(
actual,
expected_severity,
f"compute_severity({status!r}, {required}, {mode!r}) = "
f"{actual!r}, expected {expected_severity!r}",
)
def test_all_12_combinations_covered(self):
statuses = [Status.OK, Status.UNTESTED, Status.MISSING]
requireds = [True, False]
modes = ["existence", "md5"]
all_combos = {
(s, r, m) for s in statuses for r in requireds for m in modes
}
self.assertEqual(all_combos, set(self.EXPECTED.keys()))
if __name__ == "__main__":
unittest.main()