owenrusk.dev

casebench

test cases straight onto the board.

git clone https://owenrusk.dev/casebench.git

commit e12a67868ce846f0e8b72e67262a405d9c8810af
parent 434ced1b47718423bc8daa51ee23350e5913f348
author Owen Rusk <owen@papermothgames.com>
date   2025-04-23 12:14:58 -0500
casefile: say which item is broken

a KeyError from yaml halfway down tess's case wasn't telling her anything.
casebench/casefile.py+29-12
diff --git a/casebench/casefile.py b/casebench/casefile.py
index b28061c..243b26f 100644
--- a/casebench/casefile.py
+++ b/casebench/casefile.py
@@ -44,30 +44,47 @@ def load(folder: Path) -> Case:
     if not path.is_file():
         raise CaseError(f"{folder}: no case.yaml")
     raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
+    if not isinstance(raw, dict):
+        raise CaseError(f"{path}: not a mapping")
     items = []
-    for entry in raw.get("items") or []:
-        text = path.parent / entry["text"]
+    seen = set()
+    for n, entry in enumerate(raw.get("items") or []):
+        where = f"{path}: items[{n}]"
+        if not isinstance(entry, dict):
+            raise CaseError(f"{where}: not a mapping")
+        item_id = _id(entry.get("id"), f"{where}.id")
+        if item_id in seen:
+            raise CaseError(f"{where}: {item_id} twice")
+        seen.add(item_id)
+        text = path.parent / _text(entry.get("text"), f"{where}.text")
         if not text.is_file():
-            raise CaseError(f"{path}: no file {entry['text']}")
-        images = tuple(_image(path.parent / name, path) for name in entry.get("images") or [])
-        items.append(
-            Item(_id(entry.get("id"), path), entry["title"], text.read_text(encoding="utf-8"), images)
+            raise CaseError(f"{where}: no file {text.name}")
+        images = tuple(
+            _image(path.parent / _text(name, f"{where}.images"), where) for name in entry.get("images") or []
         )
+        title = _text(entry.get("title"), f"{where}.title")
+        items.append(Item(item_id, title, text.read_text(encoding="utf-8"), images))
     if not items:
         raise CaseError(f"{path}: no items")
-    return Case(_id(raw.get("id"), path), raw["title"], tuple(items))
+    return Case(_id(raw.get("id"), f"{path}: id"), _text(raw.get("title"), f"{path}: title"), tuple(items))
 
 
-def _id(value: object, path: Path) -> str:
+def _id(value: object, where: str) -> str:
     if not isinstance(value, str) or not ID.fullmatch(value):
-        raise CaseError(f"{path}: bad id {value!r}")
+        raise CaseError(f"{where}: bad id {value!r}")
     return value
 
 
-def _image(file: Path, path: Path) -> Image:
+def _text(value: object, where: str) -> str:
+    if not isinstance(value, str) or not value.strip():
+        raise CaseError(f"{where}: missing")
+    return value
+
+
+def _image(file: Path, where: str) -> Image:
     mime = MEDIA.get(file.suffix.lower())
     if mime is None:
-        raise CaseError(f"{path}: can't serve {file.name}")
+        raise CaseError(f"{where}: can't serve {file.name}")
     if not file.is_file():
-        raise CaseError(f"{path}: no file {file.name}")
+        raise CaseError(f"{where}: no file {file.name}")
     return Image(file.name, mime, file.read_bytes())