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:
Abdessamad Derraz committed 2026-08-12 05:20:25 +02:00
1 parent 221ca93dbf
commit b28f8d12a3
2 files changed
+116 -3

No files matched your search

+29 -3
View File
@@ -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']}")
+87
View File
@@ -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."""