aboutsummaryrefslogtreecommitdiff
path: root/internal/game/correct_owner_panic_subscription_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/correct_owner_panic_subscription_test.go')
-rw-r--r--internal/game/correct_owner_panic_subscription_test.go85
1 files changed, 85 insertions, 0 deletions
diff --git a/internal/game/correct_owner_panic_subscription_test.go b/internal/game/correct_owner_panic_subscription_test.go
new file mode 100644
index 0000000..f535dcc
--- /dev/null
+++ b/internal/game/correct_owner_panic_subscription_test.go
@@ -0,0 +1,85 @@
+package game
+
+// Regression: the owner loop contains a panic in a tick callback, so the tick
+// engine no longer sees it. SubscribeTicks must therefore treat "the callback
+// did not run to completion" as KEEP - a false return unsubscribes the system,
+// and the master subscriber main.go registers (RunTickSystems) is what drains
+// queued commands, settles casino rounds, and cleans up disconnects. Likewise
+// RunAdminOnOwner must not report a contained panic as "game is shutting down".
+
+import (
+ "testing"
+
+ "thehouseoficarus/internal/engine"
+)
+
+func TestSubscribeTicksKeepsSubscriptionWhenCallbackPanics(t *testing.T) {
+ g := &Game{owner: newOwner()}
+ g.Ticks = engine.New()
+ t.Cleanup(g.stopOwner)
+
+ calls := 0
+ g.SubscribeTicks(1, func() bool {
+ calls++
+ if calls == 1 {
+ panic("tick system failure")
+ }
+ return false
+ })
+
+ g.Ticks.Tick()
+ // The panicking task's runTask (recover, log, counter) has not finished
+ // when the blocked caller is released, so drain the queue before reading
+ // the counter: the owner processes tasks FIFO.
+ g.run(func() {})
+ if got := g.owner.panics.Load(); got != 1 {
+ t.Fatalf("contained panics = %d, want 1", got)
+ }
+ if calls != 1 {
+ t.Fatalf("callback calls = %d, want 1", calls)
+ }
+
+ // The subscription must still be live: a deleted one never fires again,
+ // which would leave the world frozen while the process keeps running.
+ g.Ticks.Tick()
+ if calls != 2 {
+ t.Fatalf("a contained panic unsubscribed the system: calls = %d, want 2", calls)
+ }
+
+ // An explicit false must still unsubscribe.
+ g.Ticks.Tick()
+ if calls != 2 {
+ t.Fatalf("explicit false did not unsubscribe: calls = %d, want 2", calls)
+ }
+}
+
+func TestRunAdminOnOwnerReportsContainedPanicToCaller(t *testing.T) {
+ g := &Game{owner: newOwner()}
+ t.Cleanup(g.stopOwner)
+
+ func() {
+ defer func() {
+ if r := recover(); r != adminTaskPanic {
+ t.Errorf("recovered %v, want the contained-task-panic value", r)
+ }
+ }()
+ if g.RunAdminOnOwner(func() { panic("editor task failure") }) {
+ t.Error("a panicking editor task was reported as accepted")
+ }
+ t.Fatal("a contained panic was reported as a normal result")
+ }()
+
+ g.run(func() {})
+ if got := g.owner.panics.Load(); got != 1 {
+ t.Errorf("contained panics = %d, want 1", got)
+ }
+ // The owner loop must still be draining, and the game must not have been
+ // flagged as shutting down by the panic.
+ ran := false
+ if !g.RunAdminOnOwner(func() { ran = true }) {
+ t.Fatal("the owner stopped accepting editor tasks after a contained panic")
+ }
+ if !ran {
+ t.Fatal("the owner loop did not execute later editor tasks")
+ }
+}