mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 23:54:33 +01:00
Add comprehensive tests for channelserver package: - handlers_character_test.go: CharacterSaveData, pointer constants - handlers_data_test.go: grpToGR function with boundary tests - handlers_quest_test.go: findSubSliceIndices, equal functions - handlers_simple_test.go: simple handlers, ack responses - handlers_util_test.go: stub handlers, ack helpers - sys_channel_server_test.go: Server, Raviente, stages, semaphores - sys_object_test.go: Object, Stage, stageBinaryKey structs All tests pass with race detection enabled.
398 lines
8.3 KiB
Go
398 lines
8.3 KiB
Go
package channelserver
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
func TestObjectStruct(t *testing.T) {
|
|
obj := &Object{
|
|
id: 12345,
|
|
ownerCharID: 67890,
|
|
x: 100.5,
|
|
y: 50.25,
|
|
z: -10.0,
|
|
}
|
|
|
|
if obj.id != 12345 {
|
|
t.Errorf("Object id = %d, want 12345", obj.id)
|
|
}
|
|
if obj.ownerCharID != 67890 {
|
|
t.Errorf("Object ownerCharID = %d, want 67890", obj.ownerCharID)
|
|
}
|
|
if obj.x != 100.5 {
|
|
t.Errorf("Object x = %f, want 100.5", obj.x)
|
|
}
|
|
if obj.y != 50.25 {
|
|
t.Errorf("Object y = %f, want 50.25", obj.y)
|
|
}
|
|
if obj.z != -10.0 {
|
|
t.Errorf("Object z = %f, want -10.0", obj.z)
|
|
}
|
|
}
|
|
|
|
func TestObjectRWMutex(t *testing.T) {
|
|
obj := &Object{
|
|
id: 1,
|
|
ownerCharID: 100,
|
|
x: 0,
|
|
y: 0,
|
|
z: 0,
|
|
}
|
|
|
|
// Test read lock
|
|
obj.RLock()
|
|
_ = obj.x
|
|
obj.RUnlock()
|
|
|
|
// Test write lock
|
|
obj.Lock()
|
|
obj.x = 100.0
|
|
obj.Unlock()
|
|
|
|
if obj.x != 100.0 {
|
|
t.Errorf("Object x = %f, want 100.0 after write", obj.x)
|
|
}
|
|
}
|
|
|
|
func TestObjectConcurrentAccess(t *testing.T) {
|
|
obj := &Object{
|
|
id: 1,
|
|
ownerCharID: 100,
|
|
x: 0,
|
|
y: 0,
|
|
z: 0,
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// Concurrent writers
|
|
for i := 0; i < 10; i++ {
|
|
wg.Add(1)
|
|
go func(val float32) {
|
|
defer wg.Done()
|
|
for j := 0; j < 100; j++ {
|
|
obj.Lock()
|
|
obj.x = val
|
|
obj.y = val
|
|
obj.z = val
|
|
obj.Unlock()
|
|
}
|
|
}(float32(i))
|
|
}
|
|
|
|
// Concurrent readers
|
|
for i := 0; i < 5; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < 100; j++ {
|
|
obj.RLock()
|
|
_ = obj.x
|
|
_ = obj.y
|
|
_ = obj.z
|
|
obj.RUnlock()
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestStageBinaryKeyStruct(t *testing.T) {
|
|
key1 := stageBinaryKey{id0: 1, id1: 2}
|
|
key2 := stageBinaryKey{id0: 1, id1: 3}
|
|
key3 := stageBinaryKey{id0: 1, id1: 2}
|
|
|
|
// Different keys
|
|
if key1 == key2 {
|
|
t.Error("key1 and key2 should be different")
|
|
}
|
|
|
|
// Same keys
|
|
if key1 != key3 {
|
|
t.Error("key1 and key3 should be equal")
|
|
}
|
|
}
|
|
|
|
func TestStageBinaryKeyAsMapKey(t *testing.T) {
|
|
data := make(map[stageBinaryKey][]byte)
|
|
|
|
key1 := stageBinaryKey{id0: 0, id1: 0}
|
|
key2 := stageBinaryKey{id0: 0, id1: 1}
|
|
key3 := stageBinaryKey{id0: 1, id1: 0}
|
|
|
|
data[key1] = []byte{0x01}
|
|
data[key2] = []byte{0x02}
|
|
data[key3] = []byte{0x03}
|
|
|
|
if len(data) != 3 {
|
|
t.Errorf("Expected 3 entries, got %d", len(data))
|
|
}
|
|
|
|
if data[key1][0] != 0x01 {
|
|
t.Errorf("data[key1] = 0x%02X, want 0x01", data[key1][0])
|
|
}
|
|
if data[key2][0] != 0x02 {
|
|
t.Errorf("data[key2] = 0x%02X, want 0x02", data[key2][0])
|
|
}
|
|
if data[key3][0] != 0x03 {
|
|
t.Errorf("data[key3] = 0x%02X, want 0x03", data[key3][0])
|
|
}
|
|
}
|
|
|
|
func TestNewStageDefaults(t *testing.T) {
|
|
stage := NewStage("test_stage_001")
|
|
|
|
if stage.id != "test_stage_001" {
|
|
t.Errorf("stage.id = %s, want test_stage_001", stage.id)
|
|
}
|
|
if stage.maxPlayers != 4 {
|
|
t.Errorf("stage.maxPlayers = %d, want 4 (default)", stage.maxPlayers)
|
|
}
|
|
if stage.objectIndex != 0 {
|
|
t.Errorf("stage.objectIndex = %d, want 0", stage.objectIndex)
|
|
}
|
|
if stage.clients == nil {
|
|
t.Error("stage.clients should be initialized")
|
|
}
|
|
if stage.reservedClientSlots == nil {
|
|
t.Error("stage.reservedClientSlots should be initialized")
|
|
}
|
|
if stage.objects == nil {
|
|
t.Error("stage.objects should be initialized")
|
|
}
|
|
if stage.rawBinaryData == nil {
|
|
t.Error("stage.rawBinaryData should be initialized")
|
|
}
|
|
if stage.host != nil {
|
|
t.Error("stage.host should be nil initially")
|
|
}
|
|
if stage.password != "" {
|
|
t.Errorf("stage.password should be empty, got %s", stage.password)
|
|
}
|
|
}
|
|
|
|
func TestStageNextObjectID(t *testing.T) {
|
|
stage := NewStage("test")
|
|
|
|
// First ID should be 1 (index 0 is skipped)
|
|
id1 := stage.NextObjectID()
|
|
if stage.objectIndex != 1 {
|
|
t.Errorf("objectIndex after first call = %d, want 1", stage.objectIndex)
|
|
}
|
|
|
|
// Get several IDs and ensure they increment
|
|
id2 := stage.NextObjectID()
|
|
id3 := stage.NextObjectID()
|
|
|
|
if id1 == id2 || id2 == id3 {
|
|
t.Error("Object IDs should be unique")
|
|
}
|
|
}
|
|
|
|
func TestStageNextObjectID_WrapAround(t *testing.T) {
|
|
stage := NewStage("test")
|
|
stage.objectIndex = 125
|
|
|
|
// Get ID at 126
|
|
stage.NextObjectID()
|
|
if stage.objectIndex != 126 {
|
|
t.Errorf("objectIndex = %d, want 126", stage.objectIndex)
|
|
}
|
|
|
|
// Next should wrap to 1 (skipping 0 and 127)
|
|
stage.NextObjectID()
|
|
if stage.objectIndex != 1 {
|
|
t.Errorf("objectIndex = %d, want 1 (wrapped around)", stage.objectIndex)
|
|
}
|
|
}
|
|
|
|
func TestStageIsQuest(t *testing.T) {
|
|
stage := NewStage("test")
|
|
|
|
// Initially not a quest
|
|
if stage.isQuest() {
|
|
t.Error("New stage should not be a quest")
|
|
}
|
|
|
|
// Add reserved slot
|
|
stage.reservedClientSlots[100] = true
|
|
|
|
if !stage.isQuest() {
|
|
t.Error("Stage with reserved slots should be a quest")
|
|
}
|
|
}
|
|
|
|
func TestStageReservedClientSlots(t *testing.T) {
|
|
stage := NewStage("test")
|
|
|
|
// Reserve some slots
|
|
stage.reservedClientSlots[100] = true
|
|
stage.reservedClientSlots[200] = false // ready status doesn't matter for presence
|
|
stage.reservedClientSlots[300] = true
|
|
|
|
if len(stage.reservedClientSlots) != 3 {
|
|
t.Errorf("reservedClientSlots count = %d, want 3", len(stage.reservedClientSlots))
|
|
}
|
|
|
|
// Check ready status
|
|
if !stage.reservedClientSlots[100] {
|
|
t.Error("charID 100 should be ready")
|
|
}
|
|
if stage.reservedClientSlots[200] {
|
|
t.Error("charID 200 should not be ready")
|
|
}
|
|
}
|
|
|
|
func TestStageRawBinaryData(t *testing.T) {
|
|
stage := NewStage("test")
|
|
|
|
key := stageBinaryKey{id0: 5, id1: 10}
|
|
data := []byte{0xDE, 0xAD, 0xBE, 0xEF}
|
|
|
|
stage.rawBinaryData[key] = data
|
|
|
|
retrieved := stage.rawBinaryData[key]
|
|
if len(retrieved) != 4 {
|
|
t.Fatalf("retrieved data len = %d, want 4", len(retrieved))
|
|
}
|
|
if retrieved[0] != 0xDE || retrieved[3] != 0xEF {
|
|
t.Error("retrieved data doesn't match stored data")
|
|
}
|
|
}
|
|
|
|
func TestStageObjects(t *testing.T) {
|
|
stage := NewStage("test")
|
|
|
|
obj := &Object{
|
|
id: stage.NextObjectID(),
|
|
ownerCharID: 12345,
|
|
x: 100.0,
|
|
y: 200.0,
|
|
z: 300.0,
|
|
}
|
|
|
|
stage.objects[obj.id] = obj
|
|
|
|
if len(stage.objects) != 1 {
|
|
t.Errorf("objects count = %d, want 1", len(stage.objects))
|
|
}
|
|
|
|
retrieved := stage.objects[obj.id]
|
|
if retrieved.ownerCharID != 12345 {
|
|
t.Errorf("retrieved object ownerCharID = %d, want 12345", retrieved.ownerCharID)
|
|
}
|
|
}
|
|
|
|
func TestStageHost(t *testing.T) {
|
|
server := createMockServer()
|
|
stage := NewStage("test")
|
|
|
|
// Set host
|
|
host := createMockSession(100, server)
|
|
stage.host = host
|
|
|
|
if stage.host != host {
|
|
t.Error("stage host not set correctly")
|
|
}
|
|
if stage.host.charID != 100 {
|
|
t.Errorf("stage host charID = %d, want 100", stage.host.charID)
|
|
}
|
|
}
|
|
|
|
func TestStagePassword(t *testing.T) {
|
|
stage := NewStage("test")
|
|
|
|
// Set password
|
|
stage.password = "secret123"
|
|
|
|
if stage.password != "secret123" {
|
|
t.Errorf("stage password = %s, want secret123", stage.password)
|
|
}
|
|
}
|
|
|
|
func TestStageMaxPlayers(t *testing.T) {
|
|
stage := NewStage("test")
|
|
|
|
// Change max players
|
|
stage.maxPlayers = 16
|
|
|
|
if stage.maxPlayers != 16 {
|
|
t.Errorf("stage maxPlayers = %d, want 16", stage.maxPlayers)
|
|
}
|
|
}
|
|
|
|
func TestStageConcurrentClientAccess(t *testing.T) {
|
|
server := createMockServer()
|
|
stage := NewStage("test")
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// Concurrent client additions
|
|
for i := 0; i < 10; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < 10; j++ {
|
|
session := createMockSession(uint32(id*100+j), server)
|
|
stage.Lock()
|
|
stage.clients[session] = session.charID
|
|
stage.Unlock()
|
|
|
|
stage.Lock()
|
|
delete(stage.clients, session)
|
|
stage.Unlock()
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Concurrent reads
|
|
for i := 0; i < 5; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < 50; j++ {
|
|
stage.RLock()
|
|
_ = len(stage.clients)
|
|
stage.RUnlock()
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestStageBroadcastMHF_EmptyStage(t *testing.T) {
|
|
stage := NewStage("test")
|
|
pkt := &mockPacket{opcode: 0x1234}
|
|
|
|
// Should not panic with empty stage
|
|
stage.BroadcastMHF(pkt, nil)
|
|
}
|
|
|
|
func TestStageBroadcastMHF_SkipsNilClientContext(t *testing.T) {
|
|
server := createMockServer()
|
|
stage := NewStage("test")
|
|
|
|
session1 := createMockSession(1, server)
|
|
session2 := createMockSession(2, server)
|
|
session2.clientContext = nil // Nil context should be skipped
|
|
|
|
stage.clients[session1] = session1.charID
|
|
stage.clients[session2] = session2.charID
|
|
|
|
pkt := &mockPacket{opcode: 0x1234}
|
|
|
|
// Should not panic
|
|
stage.BroadcastMHF(pkt, nil)
|
|
|
|
// Only session1 should receive
|
|
select {
|
|
case <-session1.sendPackets:
|
|
// Good
|
|
default:
|
|
t.Error("session1 should receive broadcast")
|
|
}
|
|
}
|