owenrusk.dev

casebench

test cases straight onto the board.

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

commit 1f13f6d4cdf9b8149847efa986de18cb69babb20
parent 9e0262e94232b80d40de6d31b5743d82f7b3898c
author Owen Rusk <owen@papermothgames.com>
date   2026-01-20 14:33:02 -0600
--all, and --player more than once

the beta has more boards than i want to type. list takes --player more than once too.
casebench/cli.py+57-40
tests/test_commands.py+14-0
diff --git a/casebench/cli.py b/casebench/cli.py
index cc62d78..a9ea075 100644
--- a/casebench/cli.py
+++ b/casebench/cli.py
@@ -16,9 +16,9 @@ def parser() -> argparse.ArgumentParser:
     top = argparse.ArgumentParser(prog="casebench")
     sub = top.add_subparsers(dest="command", required=True)
 
-    put = sub.add_parser("put", help="put a case on a player's board")
+    put = sub.add_parser("put", help="put a case on a board")
     put.add_argument("folder", type=Path)
-    put.add_argument("--player", required=True)
+    boards(put)
     put.set_defaults(run=run_put)
 
     note = sub.add_parser("note", help="write a note on a case")
@@ -27,33 +27,43 @@ def parser() -> argparse.ArgumentParser:
     words.add_argument("text", nargs="?")
     words.add_argument("-f", "--file", type=Path, help="read the note from a file")
     note.add_argument("--item", help="the item to write on; the case's title if left out")
-    note.add_argument("--player", required=True)
+    boards(note)
     note.set_defaults(run=run_note)
 
     locker = sub.add_parser("lock", help="lock a case behind one or more answers")
     locker.add_argument("case")
     locker.add_argument("--answer", action="append", help="one per line of the lock; asked for if left out")
-    locker.add_argument("--rest", default="3/60", help="tries/minutes: after that many wrong tries in a row, take nothing for that long (default: 3/60)")
+    locker.add_argument(
+        "--rest",
+        default="3/60",
+        help="tries/minutes: after that many wrong tries in a row, take nothing for that long (default: 3/60)",
+    )
     locker.add_argument("--text", default="locked.", help="what the lock says (default: locked.)")
-    locker.add_argument("--player", required=True)
+    boards(locker)
     locker.set_defaults(run=run_lock)
 
     unlocker = sub.add_parser("unlock", help="take the lock off a case")
     unlocker.add_argument("case")
-    unlocker.add_argument("--player", required=True)
+    boards(unlocker)
     unlocker.set_defaults(run=run_unlock)
 
     lister = sub.add_parser("list", help="what the bench has put where")
-    lister.add_argument("--player")
+    lister.add_argument("--player", action="append", help="a player's handle; every player if left out")
     lister.set_defaults(run=run_list)
 
     puller = sub.add_parser("pull", help="take a test case off a board")
     puller.add_argument("case")
-    puller.add_argument("--player", required=True)
+    boards(puller)
     puller.set_defaults(run=run_pull)
     return top
 
 
+def boards(command: argparse.ArgumentParser) -> None:
+    which = command.add_mutually_exclusive_group(required=True)
+    which.add_argument("--player", action="append", help="a player's handle; more than once is fine")
+    which.add_argument("--all", action="store_true", help="every player's board")
+
+
 def main(argv: list[str] | None = None, store: Store | None = None) -> int:
     args = parser().parse_args(argv)
     opened = store is None
@@ -76,10 +86,15 @@ def main(argv: list[str] | None = None, store: Store | None = None) -> int:
     return 0
 
 
-def player(store: Store, handle: str) -> Player:
-    found = store.player(handle)
-    if found is None:
-        raise Fail(f"no player {handle}")
+def targets(store: Store, args: argparse.Namespace) -> list[Player]:
+    if getattr(args, "all", False) or not args.player:
+        return store.players()
+    found = []
+    for handle in args.player:
+        who = store.player(handle)
+        if who is None:
+            raise Fail(f"no player {handle}")
+        found.append(who)
     return found
 
 
@@ -92,30 +107,33 @@ def on_board(store: Store, who: Player, case_id: str) -> Placed:
 
 def run_put(store: Store, args: argparse.Namespace) -> None:
     case = casefile.load(args.folder)
-    who = player(store, args.player)
-    placed = store.placed(who, case.id)
-    if placed is not None and placed.source != BENCH:
-        raise Fail(f"{who.handle} already has {case.id} from the runtime")
-    store.put(who, case)
-    print(f"{who.handle}: put {case.id}")
+    for who in targets(store, args):
+        placed = store.placed(who, case.id)
+        if placed is not None and placed.source != BENCH:
+            raise Fail(f"{who.handle} already has {case.id} from the runtime")
+        store.put(who, case)
+        print(f"{who.handle}: put {case.id}")
 
 
 def run_note(store: Store, args: argparse.Namespace) -> None:
-    who = player(store, args.player)
-    on_board(store, who, args.case)
-    if args.item and args.item not in store.items(who, args.case):
-        raise Fail(f"{args.case} has no item {args.item}")
     text = args.file.read_text(encoding="utf-8") if args.file else args.text
-    store.note(who, args.case, args.item, text)
-    print(f"{who.handle}: noted {args.case}" + (f" / {args.item}" if args.item else ""))
+    where = args.case + (f" / {args.item}" if args.item else "")
+    for who in targets(store, args):
+        on_board(store, who, args.case)
+        if args.item and args.item not in store.items(who, args.case):
+            raise Fail(f"{who.handle}: {args.case} has no item {args.item}")
+        store.note(who, args.case, args.item, text)
+        print(f"{who.handle}: noted {where}")
 
 
 def run_lock(store: Store, args: argparse.Namespace) -> None:
-    who = player(store, args.player)
-    on_board(store, who, args.case)
-    new = lock.make(args.answer or ask(), lock.parse_rest(args.rest), args.text)
-    store.lock(who, args.case, new)
-    print(f"{who.handle}: locked {args.case}")
+    rest = lock.parse_rest(args.rest)
+    players = targets(store, args)
+    new = lock.make(args.answer or ask(), rest, args.text)
+    for who in players:
+        on_board(store, who, args.case)
+        store.lock(who, args.case, new)
+        print(f"{who.handle}: locked {args.case}")
 
 
 def ask() -> list[str]:
@@ -127,23 +145,22 @@ def ask() -> list[str]:
 
 
 def run_unlock(store: Store, args: argparse.Namespace) -> None:
-    who = player(store, args.player)
-    if not store.unlock(who, args.case):
-        raise Fail(f"{args.case} isn't locked for {who.handle}")
-    print(f"{who.handle}: unlocked {args.case}")
+    for who in targets(store, args):
+        if not store.unlock(who, args.case):
+            raise Fail(f"{args.case} isn't locked for {who.handle}")
+        print(f"{who.handle}: unlocked {args.case}")
 
 
 def run_list(store: Store, args: argparse.Namespace) -> None:
-    players = [player(store, args.player)] if args.player else store.players()
-    for who in players:
+    for who in targets(store, args):
         for placed in store.board(who):
             if placed.source == BENCH:
                 print(f"{who.handle}\t{placed.case_id}\t{placed.title}")
 
 
 def run_pull(store: Store, args: argparse.Namespace) -> None:
-    who = player(store, args.player)
-    if on_board(store, who, args.case).source != BENCH:
-        raise Fail(f"{args.case} came from the runtime; the bench only pulls what it put there")
-    store.pull(who, args.case)
-    print(f"{who.handle}: pulled {args.case}")
+    for who in targets(store, args):
+        if on_board(store, who, args.case).source != BENCH:
+            raise Fail(f"{args.case} came from the runtime; the bench only pulls what it put there")
+        store.pull(who, args.case)
+        print(f"{who.handle}: pulled {args.case}")
diff --git a/tests/test_commands.py b/tests/test_commands.py
index 0d356f7..b77609b 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -25,6 +25,20 @@ class PutTest(unittest.TestCase):
         self.assertEqual(self.store.notes(self.owen, "test-lost-umbrella"), [(None, "keep me")])
         self.assertIsNotNone(self.store.get_lock(self.owen, "test-lost-umbrella"))
 
+    def test_every_board(self):
+        code, out, _ = run(self.store, "put", str(UMBRELLA), "--all")
+        self.assertEqual(code, 0)
+        self.assertEqual(out, "owen: put test-lost-umbrella\ntess: put test-lost-umbrella\n")
+
+    def test_more_than_one_player(self):
+        run(self.store, "put", str(UMBRELLA), "--player", "tess", "--player", "owen")
+        for who in self.store.players():
+            self.assertIsNotNone(self.store.placed(who, "test-lost-umbrella"))
+
+    def test_needs_a_board(self):
+        with self.assertRaises(SystemExit):
+            run(self.store, "put", str(UMBRELLA))
+
     def test_no_such_player(self):
         code, _, err = run(self.store, "put", str(UMBRELLA), "--player", "nobody")
         self.assertEqual(code, 1)