mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-12 22:43:23 -05:00
feat: report unsourceable entries apart from gaps
The per-emulator report counted an entry nobody can supply -- a per-user key, a slot the user fills, a dump that was never made -- as plainly missing. fpse-ng read 14 missing when twelve were documented as unobtainable, which invites the wrong repair: dropping the flag, deleting the entry, or chasing a vendor's whole install tree. They are listed with the reason the profile records rather than hidden, and the summary counts them apart. Platform reports are untouched.
This commit is contained in:
1 parent
221ca93dbf
commit
b28f8d12a3
2 files changed
+116
-3
No files matched your search
+29
-3
@@ -1378,6 +1378,16 @@ def verify_emulator(
|
||||
|
||||
if not local_path:
|
||||
result = {"name": name, "status": Status.MISSING, "required": required}
|
||||
# An entry the profile documents as unsourceable is absent by
|
||||
# design: a per-user key, a slot the user fills, a dump nobody
|
||||
# has made. Counting it beside a file somebody could still
|
||||
# find invites the wrong repair -- dropping the flag, or
|
||||
# chasing a vendor's whole install tree.
|
||||
unsourceable = file_entry.get("unsourceable")
|
||||
if unsourceable:
|
||||
result["unsourceable"] = (
|
||||
unsourceable if isinstance(unsourceable, str) else ""
|
||||
)
|
||||
else:
|
||||
# Apply emulator validation
|
||||
check = check_file_validation(local_path, name, validation_index)
|
||||
@@ -1533,11 +1543,19 @@ def print_emulator_result(result: dict, verbose: bool = False) -> None:
|
||||
sc = result.get("status_counts", {})
|
||||
untested = sc.get(Status.UNTESTED, 0)
|
||||
missing = sc.get(Status.MISSING, 0)
|
||||
unsourceable_names = {
|
||||
d["name"]
|
||||
for d in result["details"]
|
||||
if d["status"] == Status.MISSING and "unsourceable" in d
|
||||
}
|
||||
unsourceable = len(unsourceable_names)
|
||||
parts = [f"{ok_count}/{total} OK"]
|
||||
if untested:
|
||||
parts.append(f"{untested} untested")
|
||||
if missing:
|
||||
parts.append(f"{missing} missing")
|
||||
if missing - unsourceable > 0:
|
||||
parts.append(f"{missing - unsourceable} missing")
|
||||
if unsourceable:
|
||||
parts.append(f"{unsourceable} unsourceable")
|
||||
print(f"{label}: {', '.join(parts)} [{mode}]")
|
||||
|
||||
seen = set()
|
||||
@@ -1558,7 +1576,7 @@ def print_emulator_result(result: dict, verbose: bool = False) -> None:
|
||||
else:
|
||||
print(f" {_format_ground_truth_aggregate(gt)}")
|
||||
for d in result["details"]:
|
||||
if d["status"] == Status.MISSING:
|
||||
if d["status"] == Status.MISSING and "unsourceable" not in d:
|
||||
if d["name"] in seen:
|
||||
continue
|
||||
seen.add(d["name"])
|
||||
@@ -1572,6 +1590,14 @@ def print_emulator_result(result: dict, verbose: bool = False) -> None:
|
||||
print(f" {line}")
|
||||
else:
|
||||
print(f" {_format_ground_truth_aggregate(gt)}")
|
||||
for d in result["details"]:
|
||||
if d["status"] == Status.MISSING and "unsourceable" in d:
|
||||
if d["name"] in seen:
|
||||
continue
|
||||
seen.add(d["name"])
|
||||
why = d.get("unsourceable") or "documented as unobtainable"
|
||||
print(f" UNSOURCEABLE: {d['name']} -{why}")
|
||||
|
||||
for d in result["details"]:
|
||||
if d.get("note"):
|
||||
print(f" {d['note']}")
|
||||
|
||||
@@ -154,6 +154,93 @@ class HashMismatchIsNotCoverage(unittest.TestCase):
|
||||
self.assertEqual(detail["status"], verify.Status.MISSING)
|
||||
|
||||
|
||||
class UnsourceableIsNotAGap(unittest.TestCase):
|
||||
"""An entry nobody can supply is absent by design, not by omission.
|
||||
|
||||
Counting a per-user key or a slot the user fills beside a file somebody
|
||||
could still find invites the wrong repair: dropping the flag, deleting the
|
||||
entry, or chasing a vendor's whole install tree. One profile read
|
||||
"14 missing" when twelve of those were documented as unobtainable.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self._tmp = tempfile.TemporaryDirectory()
|
||||
self.root = Path(self._tmp.name)
|
||||
self.emulators = self.root / "emulators"
|
||||
self.emulators.mkdir()
|
||||
(self.emulators / "demo.yml").write_text(
|
||||
"emulator: demo\n"
|
||||
"type: standalone\n"
|
||||
"display_name: Demo\n"
|
||||
"systems: [demo-system]\n"
|
||||
"files:\n"
|
||||
" - name: findable.rom\n"
|
||||
" system: demo-system\n"
|
||||
" required: true\n"
|
||||
" - name: paid.so\n"
|
||||
" system: demo-system\n"
|
||||
" required: true\n"
|
||||
" unsourceable: \"ships inside the paid application\"\n"
|
||||
)
|
||||
self.db = {"files": {}, "indexes": {
|
||||
"by_name": {}, "by_md5": {}, "by_sha256": {},
|
||||
"by_crc32": {}, "by_path_suffix": {}}}
|
||||
from common import _emulator_profiles_cache
|
||||
|
||||
_emulator_profiles_cache.clear()
|
||||
|
||||
def tearDown(self):
|
||||
from common import _emulator_profiles_cache
|
||||
|
||||
_emulator_profiles_cache.clear()
|
||||
self._tmp.cleanup()
|
||||
|
||||
def _result(self):
|
||||
cwd = os.getcwd()
|
||||
os.chdir(self.root)
|
||||
try:
|
||||
return verify.verify_emulator(
|
||||
["demo"], str(self.emulators), self.db
|
||||
)
|
||||
finally:
|
||||
os.chdir(cwd)
|
||||
|
||||
def test_an_unsourceable_entry_carries_its_reason(self):
|
||||
detail = next(d for d in self._result()["details"] if d["name"] == "paid.so")
|
||||
self.assertEqual(detail["status"], verify.Status.MISSING)
|
||||
self.assertIn("unsourceable", detail)
|
||||
self.assertIn("paid", detail["unsourceable"])
|
||||
|
||||
def test_a_findable_entry_carries_no_such_marker(self):
|
||||
detail = next(
|
||||
d for d in self._result()["details"] if d["name"] == "findable.rom"
|
||||
)
|
||||
self.assertEqual(detail["status"], verify.Status.MISSING)
|
||||
self.assertNotIn("unsourceable", detail)
|
||||
|
||||
def test_the_summary_counts_the_two_apart(self):
|
||||
import contextlib
|
||||
import io
|
||||
|
||||
buffer = io.StringIO()
|
||||
with contextlib.redirect_stdout(buffer):
|
||||
verify.print_emulator_result(self._result())
|
||||
summary = buffer.getvalue().splitlines()[0]
|
||||
self.assertIn("1 missing", summary)
|
||||
self.assertIn("1 unsourceable", summary)
|
||||
|
||||
def test_the_reason_is_printed_rather_than_the_entry_hidden(self):
|
||||
import contextlib
|
||||
import io
|
||||
|
||||
buffer = io.StringIO()
|
||||
with contextlib.redirect_stdout(buffer):
|
||||
verify.print_emulator_result(self._result())
|
||||
out = buffer.getvalue()
|
||||
self.assertIn("UNSOURCEABLE: paid.so", out)
|
||||
self.assertIn("MISSING (required): findable.rom", out)
|
||||
|
||||
|
||||
class RepositoryWideEvidence(unittest.TestCase):
|
||||
"""The real collection, so the fix is measured and not just asserted."""
|
||||
|
||||
|
||||
Reference in new issue
Block a user