Skip to content

Commit ff2de80

Browse files
committed
fix: event tests
1 parent 0e45e0f commit ff2de80

File tree

3 files changed

+146
-214
lines changed

3 files changed

+146
-214
lines changed

pkg/cardinal/cardinal.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func NewWorld(opts WorldOptions) (*World, error) {
6464
world := &World{
6565
world: ecs.NewWorld(),
6666
commands: command.NewManager(),
67-
events: event.NewManager(),
67+
events: event.NewManager(1024), // Default event channel capacity
6868
address: micro.GetAddress(
6969
options.Region, micro.RealmWorld, options.Organization, options.Project, options.ShardID),
7070
currentTick: Tick{height: 0}, // timestamp will be set by cardinal.Tick

pkg/cardinal/internal/event/event.go

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@ const (
3030
// Handler is a function called to handle emitted events.
3131
type Handler func(Event) error
3232

33-
// TODO: figure out whether to make this configurable.
34-
// defaultEventChannelCapacity is the default size of the event channel.
35-
const defaultEventChannelCapacity = 1024
36-
3733
// initialCommandBufferCapacity is the starting capacity of command buffers.
3834
const initialEventBufferCapacity = 128
3935

@@ -46,11 +42,11 @@ type Manager struct {
4642
mu sync.Mutex // Mutex for buffer access during flush
4743
}
4844

49-
// NewManager creates a new event manager.
50-
func NewManager() Manager {
45+
// NewManager creates a new event manager with the specified channel capacity.
46+
func NewManager(channelCapacity int) Manager {
5147
return Manager{
5248
handlers: make([]Handler, math.MaxUint8+1),
53-
channel: make(chan Event, defaultEventChannelCapacity),
49+
channel: make(chan Event, channelCapacity),
5450
buffer: make([]Event, 0, initialEventBufferCapacity),
5551
}
5652
}
Lines changed: 142 additions & 206 deletions
Original file line numberDiff line numberDiff line change
@@ -1,208 +1,144 @@
11
package event_test
22

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

Comments
 (0)