1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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")
}
}
|