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)