|
1 | 1 | package event_test |
2 | 2 |
|
3 | | -// TODO: adapt tests to new type. |
4 | | - |
5 | | -// package ecs |
6 | | -// |
7 | | -// import ( |
8 | | -// "math/rand/v2" |
9 | | -// "reflect" |
10 | | -// "testing" |
11 | | -// "testing/synctest" |
12 | | -// |
13 | | -// "github.com/argus-labs/world-engine/pkg/cardinal/internal/event" |
14 | | -// "github.com/argus-labs/world-engine/pkg/testutils" |
15 | | -// "github.com/stretchr/testify/assert" |
16 | | -// "github.com/stretchr/testify/require" |
17 | | -// ) |
18 | | -// |
19 | | -// // ------------------------------------------------------------------------------------------------- |
20 | | -// // Model-based fuzzing event manager operations |
21 | | -// // ------------------------------------------------------------------------------------------------- |
22 | | -// // This test verifies the eventManager implementation correctness using model-based testing. It |
23 | | -// // compares our implementation against two slices (inFlight and buffer) as the model by applying |
24 | | -// // random sequences of enqueue/getEvents/clear operations to both and asserting equivalence. |
25 | | -// // The model tracks events in two stages: inFlight (channel) and buffer (drained events). |
26 | | -// // ------------------------------------------------------------------------------------------------- |
27 | | -// |
28 | | -// func TestEvent_ModelFuzz(t *testing.T) { |
29 | | -// t.Parallel() |
30 | | -// prng := testutils.NewRand(t) |
31 | | -// |
32 | | -// const opsMax = 1 << 15 // 32_768 iterations |
33 | | -// |
34 | | -// impl := newEventManager() |
35 | | -// // Model: track in-flight (channel) and buffered events separately |
36 | | -// inFlight := make([]event.Event, 0) // events enqueued but not yet drained |
37 | | -// buffer := make([]event.Event, 0) // events in the buffer after getEvents |
38 | | -// |
39 | | -// for range opsMax { |
40 | | -// op := testutils.RandWeightedOp(prng, eventOps) |
41 | | -// switch op { |
42 | | -// case em_enqueue: |
43 | | -// n := prng.IntN(10) + 1 |
44 | | -// for range n { |
45 | | -// kind := event.Kind(prng.IntN(2 + 1)) |
46 | | -// payload := prng.Int() |
47 | | -// event := event.Event{Kind: kind, Payload: payload} |
48 | | -// |
49 | | -// impl.enqueue(kind, payload) |
50 | | -// inFlight = append(inFlight, event) |
51 | | -// } |
52 | | -// case em_get: |
53 | | -// implEvents := impl.getEvents() |
54 | | -// buffer = append(buffer, inFlight...) |
55 | | -// inFlight = inFlight[:0] |
56 | | -// assert.Equal(t, buffer, implEvents, "getEvents mismatch") |
57 | | -// case em_clear: |
58 | | -// impl.clear() |
59 | | -// buffer = buffer[:0] |
60 | | -// default: |
61 | | -// panic("unreachable") |
62 | | -// } |
63 | | -// } |
64 | | -// |
65 | | -// // Final state check: drain remaining and compare to model. |
66 | | -// implEvents := impl.getEvents() |
67 | | -// buffer = append(buffer, inFlight...) |
68 | | -// assert.Equal(t, buffer, implEvents, "final buffer mismatch") |
69 | | -// } |
70 | | -// |
71 | | -// type eventOp uint8 |
72 | | -// |
73 | | -// const ( |
74 | | -// em_enqueue eventOp = 75 |
75 | | -// em_get eventOp = 20 |
76 | | -// em_clear eventOp = 5 |
77 | | -// ) |
78 | | -// |
79 | | -// var eventOps = []eventOp{em_enqueue, em_get, em_clear} |
80 | | -// |
81 | | -// // ------------------------------------------------------------------------------------------------- |
82 | | -// // Channel overflow regression test |
83 | | -// // ------------------------------------------------------------------------------------------------- |
84 | | -// // This test verifies that enqueue does not block when the channel is full. Before the fix, |
85 | | -// // enqueue would block indefinitely when the channel capacity (1024) was exceeded, causing |
86 | | -// // a deadlock. After the fix, enqueue should flush the channel to the buffer when full. |
87 | | -// // ------------------------------------------------------------------------------------------------- |
88 | | -// |
89 | | -// func TestEvent_EnqueueChannelFull(t *testing.T) { |
90 | | -// t.Parallel() |
91 | | -// |
92 | | -// synctest.Test(t, func(t *testing.T) { |
93 | | -// const channelCapacity = 16 |
94 | | -// const totalEvents = channelCapacity * 3 // Well beyond channel capacity |
95 | | -// |
96 | | -// impl := newEventManager(withChannelCapacity(channelCapacity)) |
97 | | -// |
98 | | -// // Enqueue more events than channel capacity. |
99 | | -// // Before fix: this blocks forever after 16 events, causing deadlock. |
100 | | -// // After fix: this completes without blocking. |
101 | | -// done := false |
102 | | -// go func() { |
103 | | -// for i := range totalEvents { |
104 | | -// impl.enqueue(EventKindDefault, i) |
105 | | -// } |
106 | | -// done = true |
107 | | -// }() |
108 | | -// |
109 | | -// // Wait for all goroutines to complete or durably block. |
110 | | -// // If enqueue blocks, synctest.Test will detect deadlock and fail. |
111 | | -// synctest.Wait() |
112 | | -// |
113 | | -// if !done { |
114 | | -// t.Fatal("enqueue blocked: channel overflow not handled") |
115 | | -// } |
116 | | -// |
117 | | -// // Verify all events are captured. |
118 | | -// events := impl.getEvents() |
119 | | -// assert.Len(t, events, totalEvents, "expected all %d events to be captured", totalEvents) |
120 | | -// |
121 | | -// // Verify data integrity. |
122 | | -// for i, evt := range events { |
123 | | -// assert.Equal(t, event.KindDefault, evt.Kind, "event kind mismatch at index %d", i) |
124 | | -// assert.Equal(t, i, evt.Payload, "payload mismatch at index %d", i) |
125 | | -// } |
126 | | -// }) |
127 | | -// } |
128 | | -// |
129 | | -// // ------------------------------------------------------------------------------------------------- |
130 | | -// // Model-based fuzzing event registration |
131 | | -// // ------------------------------------------------------------------------------------------------- |
132 | | -// // This test verifies the eventManager registration correctness using model-based testing. It |
133 | | -// // compares our implementation against a map[string]uint32 as the model by applying random |
134 | | -// // register operations and asserting equivalence. We also verify structural invariants: |
135 | | -// // name-id bijection and ID uniqueness. |
136 | | -// // ------------------------------------------------------------------------------------------------- |
137 | | -// |
138 | | -// func TestEvent_RegisterModelFuzz(t *testing.T) { |
139 | | -// t.Parallel() |
140 | | -// prng := testutils.NewRand(t) |
141 | | -// |
142 | | -// const opsMax = 1 << 15 // 32_768 iterations |
143 | | -// |
144 | | -// impl := newEventManager() |
145 | | -// model := make(map[string]uint32) // name -> ID |
146 | | -// |
147 | | -// for range opsMax { |
148 | | -// name := randValidEventName(prng) |
149 | | -// implID, err := impl.register(name, reflect.TypeOf(name)) |
150 | | -// require.NoError(t, err) |
151 | | -// |
152 | | -// if modelID, exists := model[name]; exists { |
153 | | -// assert.Equal(t, modelID, implID, "ID mismatch for re-registered %q", name) |
154 | | -// } else { |
155 | | -// model[name] = implID |
156 | | -// } |
157 | | -// } |
158 | | -// |
159 | | -// // Property: bijection holds between names and IDs. |
160 | | -// seenIDs := make(map[uint32]string) |
161 | | -// for name, id := range impl.registry { |
162 | | -// if prevName, seen := seenIDs[id]; seen { |
163 | | -// t.Errorf("ID %d is mapped by both %q and %q", id, prevName, name) |
164 | | -// } |
165 | | -// seenIDs[id] = name |
166 | | -// } |
167 | | -// |
168 | | -// // Property: all IDs in registry are in range [0, nextID). |
169 | | -// for name, id := range impl.registry { |
170 | | -// assert.Less(t, id, impl.nextID, "ID for %q is out of range", name) |
171 | | -// } |
172 | | -// |
173 | | -// // Final state check: registry matches model. |
174 | | -// assert.Len(t, impl.registry, len(model), "registry length mismatch") |
175 | | -// for name, modelID := range model { |
176 | | -// implID, exists := impl.registry[name] |
177 | | -// require.True(t, exists, "event %q should be registered", name) |
178 | | -// assert.Equal(t, modelID, implID, "ID mismatch for %q", name) |
179 | | -// } |
180 | | -// |
181 | | -// // Simple test to confirm that registering the same name repeatedly is a no-op. |
182 | | -// t.Run("registration idempotence", func(t *testing.T) { |
183 | | -// t.Parallel() |
184 | | -// |
185 | | -// id1, err := impl.register("hello", reflect.TypeOf(nil)) |
186 | | -// require.NoError(t, err) |
187 | | -// |
188 | | -// id2, err := impl.register("hello", reflect.TypeOf(nil)) |
189 | | -// require.NoError(t, err) |
190 | | -// |
191 | | -// assert.Equal(t, id1, id2) |
192 | | -// |
193 | | -// id3, err := impl.register("a_different_name", reflect.TypeOf(nil)) |
194 | | -// require.NoError(t, err) |
195 | | -// |
196 | | -// assert.Equal(t, id1+1, id3) |
197 | | -// }) |
198 | | -// } |
199 | | -// |
200 | | -// func randValidEventName(prng *rand.Rand) string { |
201 | | -// const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" |
202 | | -// length := prng.IntN(50) + 1 // 1-50 characters |
203 | | -// b := make([]byte, length) |
204 | | -// for i := range b { |
205 | | -// b[i] = chars[prng.IntN(len(chars))] |
206 | | -// } |
207 | | -// return string(b) |
208 | | -// } |
| 3 | +import ( |
| 4 | + "sync" |
| 5 | + "testing" |
| 6 | + "testing/synctest" |
| 7 | + |
| 8 | + "github.com/argus-labs/world-engine/pkg/cardinal/internal/event" |
| 9 | + "github.com/argus-labs/world-engine/pkg/testutils" |
| 10 | + "github.com/stretchr/testify/assert" |
| 11 | + "github.com/stretchr/testify/require" |
| 12 | +) |
| 13 | + |
| 14 | +// ------------------------------------------------------------------------------------------------- |
| 15 | +// Model-based fuzzing event manager operations |
| 16 | +// ------------------------------------------------------------------------------------------------- |
| 17 | +// This test verifies the event manager implementation correctness by applying random sequences of |
| 18 | +// operations and comparing it against a Go slice as the model. |
| 19 | +// ------------------------------------------------------------------------------------------------- |
| 20 | + |
| 21 | +func TestEvent_ModelFuzz(t *testing.T) { |
| 22 | + t.Parallel() |
| 23 | + prng := testutils.NewRand(t) |
| 24 | + |
| 25 | + const ( |
| 26 | + opsMax = 1 << 15 // 32_768 iterations |
| 27 | + opEnqueue = "enqueue" |
| 28 | + opDispatch = "dispatch" |
| 29 | + ) |
| 30 | + |
| 31 | + impl := event.NewManager(1024) |
| 32 | + model := make([]event.Event, 0) // Queue of pending events |
| 33 | + |
| 34 | + // Slice to capture events dispatched by handlers. |
| 35 | + var dispatched []event.Event |
| 36 | + var mu sync.Mutex |
| 37 | + |
| 38 | + // Register handlers for kinds 0 to N-1. |
| 39 | + numKinds := prng.IntN(256) + 1 // 1-256 kinds |
| 40 | + for i := range numKinds { |
| 41 | + impl.RegisterHandler(event.Kind(i), func(e event.Event) error { |
| 42 | + mu.Lock() |
| 43 | + dispatched = append(dispatched, e) |
| 44 | + mu.Unlock() |
| 45 | + return nil |
| 46 | + }) |
| 47 | + } |
| 48 | + |
| 49 | + // Randomize operation weights. |
| 50 | + operations := []string{opEnqueue, opDispatch} |
| 51 | + weights := testutils.RandOpWeights(prng, operations) |
| 52 | + |
| 53 | + // Run opsMax iterations. |
| 54 | + for range opsMax { |
| 55 | + op := testutils.RandWeightedOp(prng, weights) |
| 56 | + switch op { |
| 57 | + case opEnqueue: |
| 58 | + // Pick a random registered event kind and create event with random payload. |
| 59 | + kind := event.Kind(prng.IntN(numKinds)) |
| 60 | + e := event.Event{Kind: kind, Payload: prng.Int()} |
| 61 | + |
| 62 | + impl.Enqueue(e) |
| 63 | + model = append(model, e) |
| 64 | + |
| 65 | + case opDispatch: |
| 66 | + err := impl.Dispatch() |
| 67 | + require.NoError(t, err) |
| 68 | + |
| 69 | + // Property: dispatched events must match model (pending queue). |
| 70 | + assert.ElementsMatch(t, model, dispatched, "dispatched events mismatch") |
| 71 | + |
| 72 | + // Clear model and dispatched slice. |
| 73 | + model = model[:0] |
| 74 | + dispatched = dispatched[:0] |
| 75 | + |
| 76 | + default: |
| 77 | + panic("unreachable") |
| 78 | + } |
| 79 | + } |
| 80 | + |
| 81 | + // Final state check. |
| 82 | + err := impl.Dispatch() |
| 83 | + require.NoError(t, err) |
| 84 | + |
| 85 | + // Property: all enqueued events must be dispatched. |
| 86 | + assert.ElementsMatch(t, model, dispatched, "final dispatched events mismatch") |
| 87 | +} |
| 88 | + |
| 89 | +// ------------------------------------------------------------------------------------------------- |
| 90 | +// Channel overflow regression test |
| 91 | +// ------------------------------------------------------------------------------------------------- |
| 92 | +// This test verifies that enqueue does not block when the channel is full. Before the fix, |
| 93 | +// enqueue would block indefinitely when the channel capacity (1024) was exceeded, causing |
| 94 | +// a deadlock. After the fix, enqueue should flush the channel to the buffer when full. |
| 95 | +// ------------------------------------------------------------------------------------------------- |
| 96 | + |
| 97 | +func TestEvent_EnqueueChannelFull(t *testing.T) { |
| 98 | + t.Parallel() |
| 99 | + |
| 100 | + synctest.Test(t, func(t *testing.T) { |
| 101 | + const channelCapacity = 16 |
| 102 | + const totalEvents = channelCapacity * 3 // Well beyond channel capacity |
| 103 | + |
| 104 | + impl := event.NewManager(channelCapacity) |
| 105 | + |
| 106 | + // Register a handler for the default kind. |
| 107 | + var dispatched []event.Event |
| 108 | + impl.RegisterHandler(event.KindDefault, func(e event.Event) error { |
| 109 | + dispatched = append(dispatched, e) |
| 110 | + return nil |
| 111 | + }) |
| 112 | + |
| 113 | + // Enqueue more events than channel capacity. |
| 114 | + // Before fix: this blocks forever after 16 events, causing deadlock. |
| 115 | + // After fix: this completes without blocking. |
| 116 | + done := false |
| 117 | + go func() { |
| 118 | + for i := range totalEvents { |
| 119 | + impl.Enqueue(event.Event{Kind: event.KindDefault, Payload: i}) |
| 120 | + } |
| 121 | + done = true |
| 122 | + }() |
| 123 | + |
| 124 | + // Wait for all goroutines to complete or durably block. |
| 125 | + // If enqueue blocks, synctest.Test will detect deadlock and fail. |
| 126 | + synctest.Wait() |
| 127 | + |
| 128 | + if !done { |
| 129 | + t.Fatal("enqueue blocked: channel overflow not handled") |
| 130 | + } |
| 131 | + |
| 132 | + // Verify all events are captured. |
| 133 | + err := impl.Dispatch() |
| 134 | + require.NoError(t, err) |
| 135 | + |
| 136 | + assert.Len(t, dispatched, totalEvents, "expected all %d events to be captured", totalEvents) |
| 137 | + |
| 138 | + // Verify data integrity. |
| 139 | + for i, evt := range dispatched { |
| 140 | + assert.Equal(t, event.KindDefault, evt.Kind, "event kind mismatch at index %d", i) |
| 141 | + assert.Equal(t, i, evt.Payload, "payload mismatch at index %d", i) |
| 142 | + } |
| 143 | + }) |
| 144 | +} |
0 commit comments