904 lines
21 KiB
Go
904 lines
21 KiB
Go
package sign
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"eq2emu/internal/spawn"
|
|
)
|
|
|
|
// Mock implementations for testing
|
|
|
|
// MockLogger implements the Logger interface for testing
|
|
type MockLogger struct {
|
|
logs []string
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func (ml *MockLogger) LogInfo(message string, args ...any) {
|
|
ml.mu.Lock()
|
|
defer ml.mu.Unlock()
|
|
ml.logs = append(ml.logs, fmt.Sprintf("INFO: "+message, args...))
|
|
}
|
|
|
|
func (ml *MockLogger) LogError(message string, args ...any) {
|
|
ml.mu.Lock()
|
|
defer ml.mu.Unlock()
|
|
ml.logs = append(ml.logs, fmt.Sprintf("ERROR: "+message, args...))
|
|
}
|
|
|
|
func (ml *MockLogger) LogDebug(message string, args ...any) {
|
|
ml.mu.Lock()
|
|
defer ml.mu.Unlock()
|
|
ml.logs = append(ml.logs, fmt.Sprintf("DEBUG: "+message, args...))
|
|
}
|
|
|
|
func (ml *MockLogger) LogWarning(message string, args ...any) {
|
|
ml.mu.Lock()
|
|
defer ml.mu.Unlock()
|
|
ml.logs = append(ml.logs, fmt.Sprintf("WARNING: "+message, args...))
|
|
}
|
|
|
|
func (ml *MockLogger) GetLogs() []string {
|
|
ml.mu.Lock()
|
|
defer ml.mu.Unlock()
|
|
result := make([]string, len(ml.logs))
|
|
copy(result, ml.logs)
|
|
return result
|
|
}
|
|
|
|
func (ml *MockLogger) Clear() {
|
|
ml.mu.Lock()
|
|
defer ml.mu.Unlock()
|
|
ml.logs = ml.logs[:0]
|
|
}
|
|
|
|
// MockDatabase implements the Database interface for testing
|
|
type MockDatabase struct {
|
|
signs map[int32][]*Sign
|
|
zoneNames map[int32]string
|
|
charNames map[int32]string
|
|
signMarks map[int32]map[int32]string // charID -> widgetID -> charName
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func NewMockDatabase() *MockDatabase {
|
|
return &MockDatabase{
|
|
signs: make(map[int32][]*Sign),
|
|
zoneNames: make(map[int32]string),
|
|
charNames: make(map[int32]string),
|
|
signMarks: make(map[int32]map[int32]string),
|
|
}
|
|
}
|
|
|
|
func (md *MockDatabase) GetZoneName(zoneID int32) (string, error) {
|
|
md.mu.RLock()
|
|
defer md.mu.RUnlock()
|
|
if name, exists := md.zoneNames[zoneID]; exists {
|
|
return name, nil
|
|
}
|
|
return "", fmt.Errorf("zone %d not found", zoneID)
|
|
}
|
|
|
|
func (md *MockDatabase) GetCharacterName(charID int32) (string, error) {
|
|
md.mu.RLock()
|
|
defer md.mu.RUnlock()
|
|
if name, exists := md.charNames[charID]; exists {
|
|
return name, nil
|
|
}
|
|
return "", fmt.Errorf("character %d not found", charID)
|
|
}
|
|
|
|
func (md *MockDatabase) SaveSignMark(charID int32, widgetID int32, charName string, client Client) error {
|
|
md.mu.Lock()
|
|
defer md.mu.Unlock()
|
|
if md.signMarks[charID] == nil {
|
|
md.signMarks[charID] = make(map[int32]string)
|
|
}
|
|
md.signMarks[charID][widgetID] = charName
|
|
return nil
|
|
}
|
|
|
|
func (md *MockDatabase) LoadSigns(zoneID int32) ([]*Sign, error) {
|
|
md.mu.RLock()
|
|
defer md.mu.RUnlock()
|
|
return md.signs[zoneID], nil
|
|
}
|
|
|
|
func (md *MockDatabase) SaveSign(sign *Sign) error {
|
|
return nil // No-op for testing
|
|
}
|
|
|
|
func (md *MockDatabase) DeleteSign(signID int32) error {
|
|
return nil // No-op for testing
|
|
}
|
|
|
|
func (md *MockDatabase) AddZone(zoneID int32, name string) {
|
|
md.mu.Lock()
|
|
defer md.mu.Unlock()
|
|
md.zoneNames[zoneID] = name
|
|
}
|
|
|
|
func (md *MockDatabase) AddCharacter(charID int32, name string) {
|
|
md.mu.Lock()
|
|
defer md.mu.Unlock()
|
|
md.charNames[charID] = name
|
|
}
|
|
|
|
func (md *MockDatabase) AddSignToZone(zoneID int32, sign *Sign) {
|
|
md.mu.Lock()
|
|
defer md.mu.Unlock()
|
|
md.signs[zoneID] = append(md.signs[zoneID], sign)
|
|
}
|
|
|
|
// MockPlayer implements the Player interface for testing
|
|
type MockPlayer struct {
|
|
x, y, z, heading float32
|
|
zone Zone
|
|
target *spawn.Spawn
|
|
}
|
|
|
|
func (mp *MockPlayer) GetDistance(target *spawn.Spawn) float32 {
|
|
if target == nil {
|
|
return 0
|
|
}
|
|
return 5.0 // Mock distance
|
|
}
|
|
|
|
func (mp *MockPlayer) SetX(x float32) { mp.x = x }
|
|
func (mp *MockPlayer) SetY(y float32) { mp.y = y }
|
|
func (mp *MockPlayer) SetZ(z float32) { mp.z = z }
|
|
func (mp *MockPlayer) SetHeading(heading float32) { mp.heading = heading }
|
|
func (mp *MockPlayer) GetZone() Zone { return mp.zone }
|
|
func (mp *MockPlayer) GetTarget() *spawn.Spawn { return mp.target }
|
|
|
|
// MockZone implements the Zone interface for testing
|
|
type MockZone struct {
|
|
transporters map[int32][]TransportDestination
|
|
}
|
|
|
|
func (mz *MockZone) GetTransporters(client Client, transporterID int32) ([]TransportDestination, error) {
|
|
if transporters, exists := mz.transporters[transporterID]; exists {
|
|
return transporters, nil
|
|
}
|
|
return nil, fmt.Errorf("transporter %d not found", transporterID)
|
|
}
|
|
|
|
func (mz *MockZone) ProcessEntityCommand(command *EntityCommand, player Player, target *spawn.Spawn) error {
|
|
return nil // No-op for testing
|
|
}
|
|
|
|
// MockClient implements the Client interface for testing
|
|
type MockClient struct {
|
|
player Player
|
|
charID int32
|
|
database Database
|
|
zone Zone
|
|
messages []string
|
|
tempTransportID int32
|
|
zoneAccess map[string]bool
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func (mc *MockClient) GetPlayer() Player { return mc.player }
|
|
func (mc *MockClient) GetCharacterID() int32 { return mc.charID }
|
|
func (mc *MockClient) GetDatabase() Database { return mc.database }
|
|
func (mc *MockClient) GetCurrentZone() Zone { return mc.zone }
|
|
func (mc *MockClient) SetTemporaryTransportID(id int32) { mc.tempTransportID = id }
|
|
|
|
func (mc *MockClient) SimpleMessage(channel int32, message string) {
|
|
mc.mu.Lock()
|
|
defer mc.mu.Unlock()
|
|
mc.messages = append(mc.messages, message)
|
|
}
|
|
|
|
func (mc *MockClient) Message(channel int32, format string, args ...any) {
|
|
mc.mu.Lock()
|
|
defer mc.mu.Unlock()
|
|
mc.messages = append(mc.messages, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
func (mc *MockClient) CheckZoneAccess(zoneName string) bool {
|
|
return mc.zoneAccess[zoneName]
|
|
}
|
|
|
|
func (mc *MockClient) TryZoneInstance(zoneID int32, useDefaults bool) bool {
|
|
return false // Mock always uses regular zones
|
|
}
|
|
|
|
func (mc *MockClient) Zone(zoneName string, useDefaults bool) error {
|
|
return nil // No-op for testing
|
|
}
|
|
|
|
func (mc *MockClient) ProcessTeleport(sign *Sign, destinations []TransportDestination, transporterID int32) error {
|
|
return nil // No-op for testing
|
|
}
|
|
|
|
func (mc *MockClient) GetMessages() []string {
|
|
mc.mu.Lock()
|
|
defer mc.mu.Unlock()
|
|
result := make([]string, len(mc.messages))
|
|
copy(result, mc.messages)
|
|
return result
|
|
}
|
|
|
|
func (mc *MockClient) ClearMessages() {
|
|
mc.mu.Lock()
|
|
defer mc.mu.Unlock()
|
|
mc.messages = mc.messages[:0]
|
|
}
|
|
|
|
// MockEntity implements the Entity interface for testing
|
|
type MockEntity struct {
|
|
id int32
|
|
name string
|
|
databaseID int32
|
|
}
|
|
|
|
func (me *MockEntity) GetID() int32 { return me.id }
|
|
func (me *MockEntity) GetName() string { return me.name }
|
|
func (me *MockEntity) GetDatabaseID() int32 { return me.databaseID }
|
|
|
|
// Helper functions for testing
|
|
|
|
func createTestSign() *Sign {
|
|
sign := NewSign()
|
|
sign.Spawn.SetDatabaseID(1)
|
|
sign.Spawn.SetName("Test Sign")
|
|
sign.SetWidgetID(100)
|
|
sign.SetSignTitle("Welcome")
|
|
sign.SetSignDescription("This is a test sign")
|
|
return sign
|
|
}
|
|
|
|
func createZoneSign() *Sign {
|
|
sign := createTestSign()
|
|
sign.SetSignType(SignTypeZone)
|
|
sign.SetSignZoneID(2)
|
|
sign.SetSignZoneX(100.0)
|
|
sign.SetSignZoneY(200.0)
|
|
sign.SetSignZoneZ(300.0)
|
|
sign.SetSignZoneHeading(45.0)
|
|
return sign
|
|
}
|
|
|
|
// Tests
|
|
|
|
func TestNewSign(t *testing.T) {
|
|
sign := NewSign()
|
|
|
|
if sign == nil {
|
|
t.Fatal("NewSign() returned nil")
|
|
}
|
|
|
|
if sign.Spawn == nil {
|
|
t.Error("Sign spawn is nil")
|
|
}
|
|
|
|
if sign.GetSignType() != SignTypeGeneric {
|
|
t.Errorf("Expected sign type %d, got %d", SignTypeGeneric, sign.GetSignType())
|
|
}
|
|
|
|
if !sign.IsSign() {
|
|
t.Error("IsSign() should return true")
|
|
}
|
|
|
|
if sign.IsZoneSign() {
|
|
t.Error("IsZoneSign() should return false for generic sign")
|
|
}
|
|
|
|
if !sign.IsGenericSign() {
|
|
t.Error("IsGenericSign() should return true for generic sign")
|
|
}
|
|
}
|
|
|
|
func TestSignProperties(t *testing.T) {
|
|
sign := NewSign()
|
|
|
|
// Test widget properties
|
|
sign.SetWidgetID(123)
|
|
if sign.GetWidgetID() != 123 {
|
|
t.Errorf("Expected widget ID 123, got %d", sign.GetWidgetID())
|
|
}
|
|
|
|
sign.SetWidgetX(10.5)
|
|
if sign.GetWidgetX() != 10.5 {
|
|
t.Errorf("Expected widget X 10.5, got %f", sign.GetWidgetX())
|
|
}
|
|
|
|
// Test sign properties
|
|
sign.SetSignTitle("Test Title")
|
|
if sign.GetSignTitle() != "Test Title" {
|
|
t.Errorf("Expected title 'Test Title', got '%s'", sign.GetSignTitle())
|
|
}
|
|
|
|
sign.SetSignDescription("Test Description")
|
|
if sign.GetSignDescription() != "Test Description" {
|
|
t.Errorf("Expected description 'Test Description', got '%s'", sign.GetSignDescription())
|
|
}
|
|
|
|
// Test zone properties
|
|
sign.SetSignZoneID(456)
|
|
if sign.GetSignZoneID() != 456 {
|
|
t.Errorf("Expected zone ID 456, got %d", sign.GetSignZoneID())
|
|
}
|
|
|
|
sign.SetSignZoneX(100.0)
|
|
sign.SetSignZoneY(200.0)
|
|
sign.SetSignZoneZ(300.0)
|
|
sign.SetSignZoneHeading(45.0)
|
|
|
|
if !sign.HasZoneCoordinates() {
|
|
t.Error("Should have zone coordinates")
|
|
}
|
|
|
|
// Test display options
|
|
sign.SetIncludeLocation(true)
|
|
if !sign.GetIncludeLocation() {
|
|
t.Error("Include location should be true")
|
|
}
|
|
}
|
|
|
|
func TestSignValidation(t *testing.T) {
|
|
// Valid sign
|
|
sign := createTestSign()
|
|
if !sign.IsValid() {
|
|
t.Error("Sign should be valid")
|
|
}
|
|
|
|
issues := sign.Validate()
|
|
if len(issues) > 0 {
|
|
t.Errorf("Valid sign should have no issues, got: %v", issues)
|
|
}
|
|
|
|
// Invalid sign - no widget ID
|
|
sign.SetWidgetID(0)
|
|
if sign.IsValid() {
|
|
t.Error("Sign without widget ID should be invalid")
|
|
}
|
|
|
|
issues = sign.Validate()
|
|
if len(issues) == 0 {
|
|
t.Error("Sign without widget ID should have validation issues")
|
|
}
|
|
|
|
// Invalid sign - title too long
|
|
sign.SetWidgetID(100)
|
|
sign.SetSignTitle(strings.Repeat("a", MaxSignTitleLength+1))
|
|
|
|
issues = sign.Validate()
|
|
found := false
|
|
for _, issue := range issues {
|
|
if strings.Contains(issue, "title too long") {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("Should have title too long validation issue")
|
|
}
|
|
|
|
// Invalid zone sign - no zone ID
|
|
sign = createTestSign()
|
|
sign.SetSignType(SignTypeZone)
|
|
// Don't set zone ID
|
|
|
|
issues = sign.Validate()
|
|
found = false
|
|
for _, issue := range issues {
|
|
if strings.Contains(issue, "no zone ID") {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("Zone sign without zone ID should have validation issue")
|
|
}
|
|
}
|
|
|
|
func TestSignCopy(t *testing.T) {
|
|
original := createTestSign()
|
|
original.SetSignTitle("Original Title")
|
|
original.SetSignDescription("Original Description")
|
|
|
|
copy := original.Copy()
|
|
|
|
if copy == nil {
|
|
t.Fatal("Copy returned nil")
|
|
}
|
|
|
|
if copy == original {
|
|
t.Error("Copy should not be the same instance")
|
|
}
|
|
|
|
if copy.GetSignTitle() != original.GetSignTitle() {
|
|
t.Error("Copy should have same title")
|
|
}
|
|
|
|
if copy.GetSignDescription() != original.GetSignDescription() {
|
|
t.Error("Copy should have same description")
|
|
}
|
|
|
|
if copy.GetWidgetID() != original.GetWidgetID() {
|
|
t.Error("Copy should have same widget ID")
|
|
}
|
|
|
|
// Modify copy and ensure original is unchanged
|
|
copy.SetSignTitle("Modified Title")
|
|
if original.GetSignTitle() == "Modified Title" {
|
|
t.Error("Modifying copy should not affect original")
|
|
}
|
|
}
|
|
|
|
func TestSignDisplayText(t *testing.T) {
|
|
sign := NewSign()
|
|
|
|
// Empty sign
|
|
text := sign.GetDisplayText()
|
|
if len(text) > 0 {
|
|
t.Error("Empty sign should have no display text")
|
|
}
|
|
|
|
// Title only
|
|
sign.SetSignTitle("Test Title")
|
|
text = sign.GetDisplayText()
|
|
if text != "Test Title" {
|
|
t.Errorf("Expected 'Test Title', got '%s'", text)
|
|
}
|
|
|
|
// Title and description
|
|
sign.SetSignDescription("Test Description")
|
|
text = sign.GetDisplayText()
|
|
expected := "Test Title\nTest Description"
|
|
if text != expected {
|
|
t.Errorf("Expected '%s', got '%s'", expected, text)
|
|
}
|
|
|
|
// With location
|
|
sign.SetSignZoneX(100.0)
|
|
sign.SetSignZoneY(200.0)
|
|
sign.SetSignZoneZ(300.0)
|
|
sign.SetIncludeLocation(true)
|
|
text = sign.GetDisplayText()
|
|
|
|
if !strings.Contains(text, "Location: 100.00, 200.00, 300.00") {
|
|
t.Error("Display text should contain location information")
|
|
}
|
|
|
|
// With heading
|
|
sign.SetSignZoneHeading(45.0)
|
|
sign.SetIncludeHeading(true)
|
|
text = sign.GetDisplayText()
|
|
|
|
if !strings.Contains(text, "Heading: 45.00") {
|
|
t.Error("Display text should contain heading information")
|
|
}
|
|
}
|
|
|
|
func TestSignHandleUse(t *testing.T) {
|
|
// Setup test environment
|
|
database := NewMockDatabase()
|
|
database.AddZone(2, "Test Zone")
|
|
database.AddCharacter(1, "TestPlayer")
|
|
|
|
player := &MockPlayer{}
|
|
client := &MockClient{
|
|
player: player,
|
|
charID: 1,
|
|
database: database,
|
|
zoneAccess: map[string]bool{"Test Zone": true},
|
|
}
|
|
|
|
// Test zone transport sign
|
|
sign := createZoneSign()
|
|
sign.SetSignDistance(10.0) // Set distance limit
|
|
|
|
err := sign.HandleUse(client, "")
|
|
if err != nil {
|
|
t.Errorf("HandleUse failed: %v", err)
|
|
}
|
|
|
|
// Check if player position was set
|
|
if player.x != 100.0 || player.y != 200.0 || player.z != 300.0 {
|
|
t.Error("Player position should be updated for zone transport")
|
|
}
|
|
|
|
if player.heading != 45.0 {
|
|
t.Error("Player heading should be updated for zone transport")
|
|
}
|
|
|
|
// Test mark command
|
|
sign = createTestSign()
|
|
err = sign.HandleUse(client, "mark")
|
|
if err != nil {
|
|
t.Errorf("Mark command failed: %v", err)
|
|
}
|
|
|
|
// Test distance check
|
|
sign = createZoneSign()
|
|
sign.SetSignDistance(1.0) // Very short distance
|
|
client.ClearMessages()
|
|
|
|
err = sign.HandleUse(client, "")
|
|
if err != nil {
|
|
t.Errorf("HandleUse with distance check failed: %v", err)
|
|
}
|
|
|
|
messages := client.GetMessages()
|
|
found := false
|
|
for _, msg := range messages {
|
|
if strings.Contains(msg, "too far away") {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("Should get 'too far away' message when outside distance")
|
|
}
|
|
}
|
|
|
|
func TestSignSpawn(t *testing.T) {
|
|
baseSpawn := spawn.NewSpawn()
|
|
baseSpawn.SetName("Test Sign Spawn")
|
|
|
|
signSpawn := NewSignSpawn(baseSpawn)
|
|
|
|
if signSpawn == nil {
|
|
t.Fatal("NewSignSpawn returned nil")
|
|
}
|
|
|
|
if !signSpawn.IsSign() {
|
|
t.Error("SignSpawn should be a sign")
|
|
}
|
|
|
|
if signSpawn.Spawn != baseSpawn {
|
|
t.Error("SignSpawn should reference the base spawn")
|
|
}
|
|
|
|
// Test copy
|
|
copy := signSpawn.Copy()
|
|
if copy == nil {
|
|
t.Fatal("SignSpawn Copy returned nil")
|
|
}
|
|
|
|
if copy == signSpawn {
|
|
t.Error("SignSpawn Copy should create new instance")
|
|
}
|
|
|
|
if copy.Spawn.GetName() != baseSpawn.GetName() {
|
|
t.Error("Copied SignSpawn should have same spawn name")
|
|
}
|
|
}
|
|
|
|
func TestSignAdapter(t *testing.T) {
|
|
logger := &MockLogger{}
|
|
entity := &MockEntity{id: 1, name: "Test Entity", databaseID: 100}
|
|
|
|
adapter := NewSignAdapter(entity, logger)
|
|
|
|
if adapter == nil {
|
|
t.Fatal("NewSignAdapter returned nil")
|
|
}
|
|
|
|
if !adapter.IsSign() {
|
|
t.Error("SignAdapter should be a sign")
|
|
}
|
|
|
|
if adapter.GetSign() == nil {
|
|
t.Error("SignAdapter should have a sign")
|
|
}
|
|
|
|
// Test setting properties with logging
|
|
adapter.GetSign().SetWidgetID(500) // Set widget ID for validation
|
|
adapter.SetSignTitle("Test Title")
|
|
adapter.SetSignDescription("Test Description")
|
|
adapter.SetSignType(SignTypeZone)
|
|
adapter.SetZoneTransport(2, 100.0, 200.0, 300.0, 45.0)
|
|
adapter.SetSignDistance(15.0)
|
|
|
|
logs := logger.GetLogs()
|
|
if len(logs) == 0 {
|
|
t.Error("SignAdapter operations should generate log messages")
|
|
}
|
|
|
|
// Verify sign properties were set
|
|
sign := adapter.GetSign()
|
|
if sign.GetSignTitle() != "Test Title" {
|
|
t.Error("Sign title not set correctly")
|
|
}
|
|
|
|
if sign.GetSignType() != SignTypeZone {
|
|
t.Error("Sign type not set correctly")
|
|
}
|
|
|
|
if sign.GetSignZoneID() != 2 {
|
|
t.Error("Zone ID not set correctly")
|
|
}
|
|
|
|
// Test validation
|
|
if !adapter.IsValid() {
|
|
issues := adapter.Validate()
|
|
t.Errorf("SignAdapter should be valid, issues: %v", issues)
|
|
}
|
|
}
|
|
|
|
func TestManager(t *testing.T) {
|
|
database := NewMockDatabase()
|
|
logger := &MockLogger{}
|
|
|
|
manager := NewManager(database, logger)
|
|
|
|
if manager == nil {
|
|
t.Fatal("NewManager returned nil")
|
|
}
|
|
|
|
// Test initialization
|
|
err := manager.Initialize()
|
|
if err != nil {
|
|
t.Errorf("Manager initialization failed: %v", err)
|
|
}
|
|
|
|
// Test adding signs
|
|
sign1 := createTestSign()
|
|
sign1.Spawn.SetDatabaseID(1)
|
|
sign1.SetWidgetID(100)
|
|
|
|
err = manager.AddSign(sign1)
|
|
if err != nil {
|
|
t.Errorf("Adding sign failed: %v", err)
|
|
}
|
|
|
|
if manager.GetSignCount() != 1 {
|
|
t.Errorf("Expected 1 sign, got %d", manager.GetSignCount())
|
|
}
|
|
|
|
// Test retrieving signs
|
|
retrieved := manager.GetSign(1)
|
|
if retrieved == nil {
|
|
t.Error("Should be able to retrieve sign by ID")
|
|
}
|
|
|
|
retrieved = manager.GetSignByWidget(100)
|
|
if retrieved == nil {
|
|
t.Error("Should be able to retrieve sign by widget ID")
|
|
}
|
|
|
|
// Test zone functionality
|
|
sign2 := createZoneSign()
|
|
sign2.Spawn.SetDatabaseID(2)
|
|
sign2.SetWidgetID(200)
|
|
|
|
err = manager.AddSignToZone(sign2, 1)
|
|
if err != nil {
|
|
t.Errorf("Adding sign to zone failed: %v", err)
|
|
}
|
|
|
|
zoneSigns := manager.GetZoneSigns(1)
|
|
if len(zoneSigns) != 1 {
|
|
t.Errorf("Expected 1 zone sign, got %d", len(zoneSigns))
|
|
}
|
|
|
|
// Test statistics
|
|
stats := manager.GetStatistics()
|
|
if stats["total_signs"] != int64(2) {
|
|
t.Errorf("Expected 2 total signs in stats, got %v", stats["total_signs"])
|
|
}
|
|
|
|
typeStats := stats["signs_by_type"].(map[int8]int64)
|
|
if typeStats[SignTypeGeneric] != 1 {
|
|
t.Errorf("Expected 1 generic sign in stats, got %d", typeStats[SignTypeGeneric])
|
|
}
|
|
if typeStats[SignTypeZone] != 1 {
|
|
t.Errorf("Expected 1 zone sign in stats, got %d", typeStats[SignTypeZone])
|
|
}
|
|
|
|
// Test removing signs
|
|
if !manager.RemoveSign(1) {
|
|
t.Error("Should be able to remove sign")
|
|
}
|
|
|
|
if manager.GetSignCount() != 1 {
|
|
t.Errorf("Expected 1 sign after removal, got %d", manager.GetSignCount())
|
|
}
|
|
|
|
// Test validation
|
|
issues := manager.ValidateAllSigns()
|
|
if len(issues) > 0 {
|
|
t.Errorf("All signs should be valid, found issues: %v", issues)
|
|
}
|
|
|
|
// Test commands
|
|
result, err := manager.ProcessCommand("stats", nil)
|
|
if err != nil {
|
|
t.Errorf("Stats command failed: %v", err)
|
|
}
|
|
if !strings.Contains(result, "Sign System Statistics") {
|
|
t.Error("Stats command should return statistics")
|
|
}
|
|
|
|
result, err = manager.ProcessCommand("list", nil)
|
|
if err != nil {
|
|
t.Errorf("List command failed: %v", err)
|
|
}
|
|
if !strings.Contains(result, "Signs (1)") {
|
|
t.Error("List command should show sign count")
|
|
}
|
|
|
|
result, err = manager.ProcessCommand("validate", nil)
|
|
if err != nil {
|
|
t.Errorf("Validate command failed: %v", err)
|
|
}
|
|
if !strings.Contains(result, "All signs are valid") {
|
|
t.Error("Validate command should report all signs valid")
|
|
}
|
|
|
|
result, err = manager.ProcessCommand("info", []string{"2"})
|
|
if err != nil {
|
|
t.Errorf("Info command failed: %v", err)
|
|
}
|
|
if !strings.Contains(result, "Sign Information") {
|
|
t.Error("Info command should show sign information")
|
|
}
|
|
|
|
// Test unknown command
|
|
_, err = manager.ProcessCommand("unknown", nil)
|
|
if err == nil {
|
|
t.Error("Unknown command should return error")
|
|
}
|
|
}
|
|
|
|
func TestManagerConcurrency(t *testing.T) {
|
|
database := NewMockDatabase()
|
|
logger := &MockLogger{}
|
|
manager := NewManager(database, logger)
|
|
|
|
const numGoroutines = 10
|
|
const numOperations = 100
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(numGoroutines)
|
|
|
|
// Test concurrent sign additions
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < numOperations; j++ {
|
|
sign := createTestSign()
|
|
signID := int32(id*numOperations + j + 1)
|
|
sign.Spawn.SetDatabaseID(signID)
|
|
sign.SetWidgetID(signID)
|
|
|
|
err := manager.AddSign(sign)
|
|
if err != nil {
|
|
t.Errorf("Concurrent AddSign failed: %v", err)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
expectedCount := int64(numGoroutines * numOperations)
|
|
if manager.GetSignCount() != expectedCount {
|
|
t.Errorf("Expected %d signs after concurrent additions, got %d",
|
|
expectedCount, manager.GetSignCount())
|
|
}
|
|
|
|
// Test concurrent reads
|
|
wg.Add(numGoroutines)
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < numOperations; j++ {
|
|
stats := manager.GetStatistics()
|
|
if stats["total_signs"].(int64) <= 0 {
|
|
t.Error("Stats should show positive sign count")
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestSignSerialization(t *testing.T) {
|
|
sign := createTestSign()
|
|
player := &MockPlayer{}
|
|
|
|
// Test serialization (currently returns error due to incomplete implementation)
|
|
_, err := sign.Serialize(player, 1146)
|
|
if err == nil {
|
|
t.Error("Serialization should return error with current implementation")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "not yet implemented") {
|
|
t.Error("Should get 'not yet implemented' error message")
|
|
}
|
|
}
|
|
|
|
func TestSignConstants(t *testing.T) {
|
|
// Test sign type constants
|
|
if SignTypeGeneric != 0 {
|
|
t.Errorf("SignTypeGeneric should be 0, got %d", SignTypeGeneric)
|
|
}
|
|
|
|
if SignTypeZone != 1 {
|
|
t.Errorf("SignTypeZone should be 1, got %d", SignTypeZone)
|
|
}
|
|
|
|
// Test default constants
|
|
if DefaultSpawnType != 2 {
|
|
t.Errorf("DefaultSpawnType should be 2, got %d", DefaultSpawnType)
|
|
}
|
|
|
|
if DefaultActivityStatus != 64 {
|
|
t.Errorf("DefaultActivityStatus should be 64, got %d", DefaultActivityStatus)
|
|
}
|
|
|
|
if DefaultSignDistance != 0.0 {
|
|
t.Errorf("DefaultSignDistance should be 0.0, got %f", DefaultSignDistance)
|
|
}
|
|
}
|
|
|
|
// Benchmark tests
|
|
|
|
func BenchmarkNewSign(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
_ = NewSign()
|
|
}
|
|
}
|
|
|
|
func BenchmarkSignCopy(b *testing.B) {
|
|
sign := createTestSign()
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
_ = sign.Copy()
|
|
}
|
|
}
|
|
|
|
func BenchmarkSignValidation(b *testing.B) {
|
|
sign := createTestSign()
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
_ = sign.Validate()
|
|
}
|
|
}
|
|
|
|
func BenchmarkManagerAddSign(b *testing.B) {
|
|
database := NewMockDatabase()
|
|
logger := &MockLogger{}
|
|
manager := NewManager(database, logger)
|
|
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
sign := createTestSign()
|
|
sign.Spawn.SetDatabaseID(int32(i + 1))
|
|
sign.SetWidgetID(int32(i + 100))
|
|
_ = manager.AddSign(sign)
|
|
}
|
|
}
|
|
|
|
func BenchmarkManagerGetSign(b *testing.B) {
|
|
database := NewMockDatabase()
|
|
logger := &MockLogger{}
|
|
manager := NewManager(database, logger)
|
|
|
|
// Pre-populate with signs
|
|
for i := 0; i < 1000; i++ {
|
|
sign := createTestSign()
|
|
sign.Spawn.SetDatabaseID(int32(i + 1))
|
|
sign.SetWidgetID(int32(i + 100))
|
|
_ = manager.AddSign(sign)
|
|
}
|
|
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
_ = manager.GetSign(int32((i % 1000) + 1))
|
|
}
|
|
} |