eq2go/internal/appearances/appearances_test.go

1551 lines
39 KiB
Go

package appearances
import (
"fmt"
"sync"
"testing"
)
func TestNewAppearance(t *testing.T) {
tests := []struct {
name string
id int32
appearanceName string
minClientVersion int16
wantNil bool
}{
{
name: "valid appearance",
id: 100,
appearanceName: "Test Appearance",
minClientVersion: 1096,
wantNil: false,
},
{
name: "empty name returns nil",
id: 200,
appearanceName: "",
minClientVersion: 1096,
wantNil: true,
},
{
name: "negative id allowed",
id: -50,
appearanceName: "Negative ID",
minClientVersion: 0,
wantNil: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
app := NewAppearance(tt.id, tt.appearanceName, tt.minClientVersion)
if tt.wantNil {
if app != nil {
t.Errorf("expected nil appearance, got %v", app)
}
return
}
if app == nil {
t.Fatal("expected non-nil appearance, got nil")
}
if app.GetID() != tt.id {
t.Errorf("ID = %v, want %v", app.GetID(), tt.id)
}
if app.GetName() != tt.appearanceName {
t.Errorf("Name = %v, want %v", app.GetName(), tt.appearanceName)
}
if app.GetMinClientVersion() != tt.minClientVersion {
t.Errorf("MinClientVersion = %v, want %v", app.GetMinClientVersion(), tt.minClientVersion)
}
})
}
}
func TestAppearanceGetters(t *testing.T) {
app := NewAppearance(123, "Test Appearance", 1096)
if id := app.GetID(); id != 123 {
t.Errorf("GetID() = %v, want 123", id)
}
if name := app.GetName(); name != "Test Appearance" {
t.Errorf("GetName() = %v, want Test Appearance", name)
}
if nameStr := app.GetNameString(); nameStr != "Test Appearance" {
t.Errorf("GetNameString() = %v, want Test Appearance", nameStr)
}
if minVer := app.GetMinClientVersion(); minVer != 1096 {
t.Errorf("GetMinClientVersion() = %v, want 1096", minVer)
}
}
func TestAppearanceSetters(t *testing.T) {
app := NewAppearance(100, "Original", 1000)
app.SetName("Modified Name")
if app.GetName() != "Modified Name" {
t.Errorf("SetName failed: got %v, want Modified Name", app.GetName())
}
app.SetMinClientVersion(2000)
if app.GetMinClientVersion() != 2000 {
t.Errorf("SetMinClientVersion failed: got %v, want 2000", app.GetMinClientVersion())
}
}
func TestIsCompatibleWithClient(t *testing.T) {
app := NewAppearance(100, "Test", 1096)
tests := []struct {
clientVersion int16
want bool
}{
{1095, false}, // Below minimum
{1096, true}, // Exact minimum
{1097, true}, // Above minimum
{2000, true}, // Well above minimum
{0, false}, // Zero version
}
for _, tt := range tests {
t.Run("", func(t *testing.T) {
if got := app.IsCompatibleWithClient(tt.clientVersion); got != tt.want {
t.Errorf("IsCompatibleWithClient(%v) = %v, want %v", tt.clientVersion, got, tt.want)
}
})
}
}
func TestAppearanceClone(t *testing.T) {
original := NewAppearance(500, "Original Appearance", 1200)
clone := original.Clone()
if clone == nil {
t.Fatal("Clone returned nil")
}
if clone == original {
t.Error("Clone returned same pointer as original")
}
if clone.GetID() != original.GetID() {
t.Errorf("Clone ID = %v, want %v", clone.GetID(), original.GetID())
}
if clone.GetName() != original.GetName() {
t.Errorf("Clone Name = %v, want %v", clone.GetName(), original.GetName())
}
if clone.GetMinClientVersion() != original.GetMinClientVersion() {
t.Errorf("Clone MinClientVersion = %v, want %v", clone.GetMinClientVersion(), original.GetMinClientVersion())
}
// Verify modification independence
clone.SetName("Modified Clone")
if original.GetName() == "Modified Clone" {
t.Error("Modifying clone affected original")
}
}
func TestNewAppearances(t *testing.T) {
apps := NewAppearances()
if apps == nil {
t.Fatal("NewAppearances returned nil")
}
if count := apps.GetAppearanceCount(); count != 0 {
t.Errorf("New appearances collection should be empty, got count %v", count)
}
}
func TestAppearancesInsertAndFind(t *testing.T) {
apps := NewAppearances()
// Test nil insertion
err := apps.InsertAppearance(nil)
if err == nil {
t.Error("InsertAppearance(nil) should return error")
}
// Insert valid appearances
app1 := NewAppearance(100, "Appearance 1", 1000)
app2 := NewAppearance(200, "Appearance 2", 1100)
if err := apps.InsertAppearance(app1); err != nil {
t.Errorf("InsertAppearance failed: %v", err)
}
if err := apps.InsertAppearance(app2); err != nil {
t.Errorf("InsertAppearance failed: %v", err)
}
// Test finding by ID
found := apps.FindAppearanceByID(100)
if found == nil {
t.Error("FindAppearanceByID(100) returned nil")
} else if found.GetName() != "Appearance 1" {
t.Errorf("FindAppearanceByID(100) returned wrong appearance: %v", found.GetName())
}
// Test finding non-existent ID
notFound := apps.FindAppearanceByID(999)
if notFound != nil {
t.Errorf("FindAppearanceByID(999) should return nil, got %v", notFound)
}
}
func TestAppearancesHasAppearance(t *testing.T) {
apps := NewAppearances()
app := NewAppearance(300, "Test", 1000)
apps.InsertAppearance(app)
if !apps.HasAppearance(300) {
t.Error("HasAppearance(300) should return true")
}
if apps.HasAppearance(999) {
t.Error("HasAppearance(999) should return false")
}
}
func TestAppearancesGetAllAndCount(t *testing.T) {
apps := NewAppearances()
// Add multiple appearances
for i := int32(1); i <= 5; i++ {
app := NewAppearance(i*100, "Appearance", 1000)
apps.InsertAppearance(app)
}
if count := apps.GetAppearanceCount(); count != 5 {
t.Errorf("GetAppearanceCount() = %v, want 5", count)
}
all := apps.GetAllAppearances()
if len(all) != 5 {
t.Errorf("GetAllAppearances() returned %v items, want 5", len(all))
}
// Verify it's a copy by modifying returned map
delete(all, 100)
if apps.GetAppearanceCount() != 5 {
t.Error("Modifying returned map affected internal state")
}
}
func TestAppearancesGetIDs(t *testing.T) {
apps := NewAppearances()
expectedIDs := []int32{100, 200, 300}
for _, id := range expectedIDs {
app := NewAppearance(id, "Test", 1000)
apps.InsertAppearance(app)
}
ids := apps.GetAppearanceIDs()
if len(ids) != len(expectedIDs) {
t.Errorf("GetAppearanceIDs() returned %v IDs, want %v", len(ids), len(expectedIDs))
}
// Check all expected IDs are present
idMap := make(map[int32]bool)
for _, id := range ids {
idMap[id] = true
}
for _, expected := range expectedIDs {
if !idMap[expected] {
t.Errorf("Expected ID %v not found in returned IDs", expected)
}
}
}
func TestAppearancesFindByName(t *testing.T) {
apps := NewAppearances()
apps.InsertAppearance(NewAppearance(1, "Human Male", 1000))
apps.InsertAppearance(NewAppearance(2, "Human Female", 1000))
apps.InsertAppearance(NewAppearance(3, "Elf Male", 1000))
apps.InsertAppearance(NewAppearance(4, "Dwarf Female", 1000))
// Test partial matching
humanApps := apps.FindAppearancesByName("Human")
if len(humanApps) != 2 {
t.Errorf("FindAppearancesByName('Human') returned %v results, want 2", len(humanApps))
}
// Test case sensitivity
maleApps := apps.FindAppearancesByName("Male")
if len(maleApps) != 2 {
t.Errorf("FindAppearancesByName('Male') returned %v results, want 2", len(maleApps))
}
// Test empty substring
allApps := apps.FindAppearancesByName("")
if len(allApps) != 4 {
t.Errorf("FindAppearancesByName('') returned %v results, want 4", len(allApps))
}
// Test no matches
noMatches := apps.FindAppearancesByName("Orc")
if len(noMatches) != 0 {
t.Errorf("FindAppearancesByName('Orc') returned %v results, want 0", len(noMatches))
}
}
func TestAppearancesFindByMinClient(t *testing.T) {
apps := NewAppearances()
apps.InsertAppearance(NewAppearance(1, "Old", 1000))
apps.InsertAppearance(NewAppearance(2, "Medium1", 1096))
apps.InsertAppearance(NewAppearance(3, "Medium2", 1096))
apps.InsertAppearance(NewAppearance(4, "New", 1200))
results := apps.FindAppearancesByMinClient(1096)
if len(results) != 2 {
t.Errorf("FindAppearancesByMinClient(1096) returned %v results, want 2", len(results))
}
results = apps.FindAppearancesByMinClient(999)
if len(results) != 0 {
t.Errorf("FindAppearancesByMinClient(999) returned %v results, want 0", len(results))
}
}
func TestAppearancesGetCompatible(t *testing.T) {
apps := NewAppearances()
apps.InsertAppearance(NewAppearance(1, "Old", 1000))
apps.InsertAppearance(NewAppearance(2, "Medium", 1096))
apps.InsertAppearance(NewAppearance(3, "New", 1200))
apps.InsertAppearance(NewAppearance(4, "Newer", 1300))
// Client version 1100 should get Old and Medium
compatible := apps.GetCompatibleAppearances(1100)
if len(compatible) != 2 {
t.Errorf("GetCompatibleAppearances(1100) returned %v results, want 2", len(compatible))
}
// Client version 1500 should get all
compatible = apps.GetCompatibleAppearances(1500)
if len(compatible) != 4 {
t.Errorf("GetCompatibleAppearances(1500) returned %v results, want 4", len(compatible))
}
// Client version 500 should get none
compatible = apps.GetCompatibleAppearances(500)
if len(compatible) != 0 {
t.Errorf("GetCompatibleAppearances(500) returned %v results, want 0", len(compatible))
}
}
func TestAppearancesRemove(t *testing.T) {
apps := NewAppearances()
app := NewAppearance(100, "Test", 1000)
apps.InsertAppearance(app)
// Remove existing
if !apps.RemoveAppearance(100) {
t.Error("RemoveAppearance(100) should return true")
}
if apps.HasAppearance(100) {
t.Error("Appearance 100 should have been removed")
}
// Remove non-existent
if apps.RemoveAppearance(100) {
t.Error("RemoveAppearance(100) should return false for non-existent ID")
}
}
func TestAppearancesUpdate(t *testing.T) {
apps := NewAppearances()
// Test updating nil
err := apps.UpdateAppearance(nil)
if err == nil {
t.Error("UpdateAppearance(nil) should return error")
}
// Insert and update
original := NewAppearance(100, "Original", 1000)
apps.InsertAppearance(original)
updated := NewAppearance(100, "Updated", 1100)
err = apps.UpdateAppearance(updated)
if err != nil {
t.Errorf("UpdateAppearance failed: %v", err)
}
found := apps.FindAppearanceByID(100)
if found.GetName() != "Updated" {
t.Errorf("Updated appearance name = %v, want Updated", found.GetName())
}
// Update non-existent (should insert)
new := NewAppearance(200, "New", 1200)
err = apps.UpdateAppearance(new)
if err != nil {
t.Errorf("UpdateAppearance failed: %v", err)
}
if !apps.HasAppearance(200) {
t.Error("UpdateAppearance should insert non-existent appearance")
}
}
func TestAppearancesGetByIDRange(t *testing.T) {
apps := NewAppearances()
// Insert appearances with various IDs
for _, id := range []int32{5, 10, 15, 20, 25, 30} {
apps.InsertAppearance(NewAppearance(id, "Test", 1000))
}
results := apps.GetAppearancesByIDRange(10, 20)
if len(results) != 3 { // Should get 10, 15, 20
t.Errorf("GetAppearancesByIDRange(10, 20) returned %v results, want 3", len(results))
}
// Verify correct IDs
idMap := make(map[int32]bool)
for _, app := range results {
idMap[app.GetID()] = true
}
for _, expectedID := range []int32{10, 15, 20} {
if !idMap[expectedID] {
t.Errorf("Expected ID %v not found in range results", expectedID)
}
}
// Test empty range
results = apps.GetAppearancesByIDRange(100, 200)
if len(results) != 0 {
t.Errorf("GetAppearancesByIDRange(100, 200) returned %v results, want 0", len(results))
}
}
func TestAppearancesValidate(t *testing.T) {
apps := NewAppearances()
// Valid appearances
apps.InsertAppearance(NewAppearance(100, "Valid", 1000))
issues := apps.ValidateAppearances()
if len(issues) != 0 {
t.Errorf("ValidateAppearances() returned issues for valid data: %v", issues)
}
if !apps.IsValid() {
t.Error("IsValid() should return true for valid data")
}
// Force invalid state by directly modifying map
apps.mutex.Lock()
apps.appearanceMap[200] = nil
apps.appearanceMap[300] = NewAppearance(301, "", 1000) // ID mismatch and empty name
apps.appearanceMap[400] = NewAppearance(400, "Negative", -100)
apps.mutex.Unlock()
issues = apps.ValidateAppearances()
if len(issues) < 3 {
t.Errorf("ValidateAppearances() should return at least 3 issues, got %v", len(issues))
}
if apps.IsValid() {
t.Error("IsValid() should return false for invalid data")
}
}
func TestAppearancesStatistics(t *testing.T) {
apps := NewAppearances()
// Add appearances with different client versions
apps.InsertAppearance(NewAppearance(10, "A", 1000))
apps.InsertAppearance(NewAppearance(20, "B", 1000))
apps.InsertAppearance(NewAppearance(30, "C", 1096))
apps.InsertAppearance(NewAppearance(40, "D", 1096))
apps.InsertAppearance(NewAppearance(50, "E", 1096))
stats := apps.GetStatistics()
if total, ok := stats["total_appearances"].(int); !ok || total != 5 {
t.Errorf("total_appearances = %v, want 5", stats["total_appearances"])
}
if minID, ok := stats["min_id"].(int32); !ok || minID != 10 {
t.Errorf("min_id = %v, want 10", stats["min_id"])
}
if maxID, ok := stats["max_id"].(int32); !ok || maxID != 50 {
t.Errorf("max_id = %v, want 50", stats["max_id"])
}
if idRange, ok := stats["id_range"].(int32); !ok || idRange != 40 {
t.Errorf("id_range = %v, want 40", stats["id_range"])
}
if versionCounts, ok := stats["appearances_by_min_client"].(map[int16]int); ok {
if versionCounts[1000] != 2 {
t.Errorf("appearances with min client 1000 = %v, want 2", versionCounts[1000])
}
if versionCounts[1096] != 3 {
t.Errorf("appearances with min client 1096 = %v, want 3", versionCounts[1096])
}
} else {
t.Error("appearances_by_min_client not found in statistics")
}
}
func TestAppearancesClearAndReset(t *testing.T) {
apps := NewAppearances()
// Add some appearances
for i := int32(1); i <= 3; i++ {
apps.InsertAppearance(NewAppearance(i*100, "Test", 1000))
}
if apps.GetAppearanceCount() != 3 {
t.Error("Setup failed: should have 3 appearances")
}
// Test ClearAppearances
apps.ClearAppearances()
if apps.GetAppearanceCount() != 0 {
t.Errorf("ClearAppearances() failed: count = %v, want 0", apps.GetAppearanceCount())
}
// Add again and test Reset
for i := int32(1); i <= 3; i++ {
apps.InsertAppearance(NewAppearance(i*100, "Test", 1000))
}
apps.Reset()
if apps.GetAppearanceCount() != 0 {
t.Errorf("Reset() failed: count = %v, want 0", apps.GetAppearanceCount())
}
}
func TestAppearancesConcurrency(t *testing.T) {
apps := NewAppearances()
var wg sync.WaitGroup
// Concurrent insertions
for i := 0; i < 100; i++ {
wg.Add(1)
go func(id int32) {
defer wg.Done()
app := NewAppearance(id, "Concurrent", 1000)
apps.InsertAppearance(app)
}(int32(i))
}
// Concurrent reads
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = apps.GetAppearanceCount()
_ = apps.GetAllAppearances()
_ = apps.FindAppearanceByID(25)
}()
}
// Concurrent searches
for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = apps.FindAppearancesByName("Concurrent")
_ = apps.GetCompatibleAppearances(1100)
}()
}
wg.Wait()
// Verify all insertions succeeded
if count := apps.GetAppearanceCount(); count != 100 {
t.Errorf("After concurrent operations, count = %v, want 100", count)
}
}
func TestContainsFunction(t *testing.T) {
tests := []struct {
str string
substr string
want bool
}{
{"hello world", "world", true},
{"hello world", "World", false}, // Case sensitive
{"hello", "hello world", false},
{"hello", "", true},
{"", "hello", false},
{"", "", true},
{"abcdef", "cde", true},
{"abcdef", "xyz", false},
}
for _, tt := range tests {
t.Run("", func(t *testing.T) {
if got := contains(tt.str, tt.substr); got != tt.want {
t.Errorf("contains(%q, %q) = %v, want %v", tt.str, tt.substr, got, tt.want)
}
})
}
}
// Benchmarks
func BenchmarkAppearanceInsert(b *testing.B) {
apps := NewAppearances()
b.ResetTimer()
for i := 0; i < b.N; i++ {
app := NewAppearance(int32(i), "Benchmark", 1000)
apps.InsertAppearance(app)
}
}
func BenchmarkAppearanceFindByID(b *testing.B) {
apps := NewAppearances()
// Pre-populate
for i := 0; i < 10000; i++ {
app := NewAppearance(int32(i), "Benchmark", 1000)
apps.InsertAppearance(app)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
apps.FindAppearanceByID(int32(i % 10000))
}
}
func BenchmarkAppearanceFindByName(b *testing.B) {
apps := NewAppearances()
// Pre-populate with varied names
names := []string{"Human Male", "Human Female", "Elf Male", "Elf Female", "Dwarf Male"}
for i := 0; i < 1000; i++ {
app := NewAppearance(int32(i), names[i%len(names)], 1000)
apps.InsertAppearance(app)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
apps.FindAppearancesByName("Male")
}
}
// Mock implementations
type MockDatabase struct {
appearances []*Appearance
saveError error
loadError error
deleteError error
}
func (m *MockDatabase) LoadAllAppearances() ([]*Appearance, error) {
if m.loadError != nil {
return nil, m.loadError
}
return m.appearances, nil
}
func (m *MockDatabase) SaveAppearance(appearance *Appearance) error {
return m.saveError
}
func (m *MockDatabase) DeleteAppearance(id int32) error {
return m.deleteError
}
func (m *MockDatabase) LoadAppearancesByClientVersion(minClientVersion int16) ([]*Appearance, error) {
var results []*Appearance
for _, app := range m.appearances {
if app.GetMinClientVersion() == minClientVersion {
results = append(results, app)
}
}
return results, nil
}
type MockLogger struct {
logs []string
}
func (m *MockLogger) LogInfo(message string, args ...any) {
m.logs = append(m.logs, fmt.Sprintf("INFO: "+message, args...))
}
func (m *MockLogger) LogError(message string, args ...any) {
m.logs = append(m.logs, fmt.Sprintf("ERROR: "+message, args...))
}
func (m *MockLogger) LogDebug(message string, args ...any) {
m.logs = append(m.logs, fmt.Sprintf("DEBUG: "+message, args...))
}
func (m *MockLogger) LogWarning(message string, args ...any) {
m.logs = append(m.logs, fmt.Sprintf("WARNING: "+message, args...))
}
type MockEntity struct {
id int32
name string
databaseID int32
}
func (m *MockEntity) GetID() int32 { return m.id }
func (m *MockEntity) GetName() string { return m.name }
func (m *MockEntity) GetDatabaseID() int32 { return m.databaseID }
type MockClient struct {
version int16
sendError error
}
func (m *MockClient) GetVersion() int16 { return m.version }
func (m *MockClient) SendAppearanceUpdate(appearanceID int32) error { return m.sendError }
// Manager tests
func TestNewManager(t *testing.T) {
db := &MockDatabase{}
logger := &MockLogger{}
manager := NewManager(db, logger)
if manager == nil {
t.Fatal("NewManager returned nil")
}
if manager.database != db {
t.Error("Manager database not set correctly")
}
if manager.logger != logger {
t.Error("Manager logger not set correctly")
}
if manager.appearances == nil {
t.Error("Manager appearances not initialized")
}
}
func TestManagerInitialize(t *testing.T) {
tests := []struct {
name string
database *MockDatabase
wantError bool
wantCount int
wantLogInfo bool
}{
{
name: "successful initialization",
database: &MockDatabase{
appearances: []*Appearance{
NewAppearance(1, "Test1", 1000),
NewAppearance(2, "Test2", 1096),
},
},
wantError: false,
wantCount: 2,
wantLogInfo: true,
},
{
name: "nil database",
database: nil,
wantError: false,
wantCount: 0,
wantLogInfo: true,
},
{
name: "database load error",
database: &MockDatabase{
loadError: fmt.Errorf("database error"),
},
wantError: true,
wantCount: 0,
wantLogInfo: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logger := &MockLogger{}
var manager *Manager
if tt.database != nil {
manager = NewManager(tt.database, logger)
} else {
manager = &Manager{
appearances: NewAppearances(),
database: nil,
logger: logger,
}
}
err := manager.Initialize()
if (err != nil) != tt.wantError {
t.Errorf("Initialize() error = %v, wantError %v", err, tt.wantError)
}
if count := manager.GetAppearanceCount(); count != tt.wantCount {
t.Errorf("GetAppearanceCount() = %v, want %v", count, tt.wantCount)
}
if tt.wantLogInfo && len(logger.logs) == 0 {
t.Error("Expected log messages, got none")
}
})
}
}
func TestManagerFindAppearanceByID(t *testing.T) {
db := &MockDatabase{
appearances: []*Appearance{
NewAppearance(100, "Test", 1000),
},
}
logger := &MockLogger{}
manager := NewManager(db, logger)
manager.Initialize()
// Test successful lookup
appearance := manager.FindAppearanceByID(100)
if appearance == nil {
t.Error("FindAppearanceByID(100) returned nil")
}
// Test failed lookup
appearance = manager.FindAppearanceByID(999)
if appearance != nil {
t.Error("FindAppearanceByID(999) should return nil")
}
// Check statistics
stats := manager.GetStatistics()
if totalLookups, ok := stats["total_lookups"].(int64); !ok || totalLookups != 2 {
t.Errorf("total_lookups = %v, want 2", stats["total_lookups"])
}
if successfulLookups, ok := stats["successful_lookups"].(int64); !ok || successfulLookups != 1 {
t.Errorf("successful_lookups = %v, want 1", stats["successful_lookups"])
}
if failedLookups, ok := stats["failed_lookups"].(int64); !ok || failedLookups != 1 {
t.Errorf("failed_lookups = %v, want 1", stats["failed_lookups"])
}
}
func TestManagerAddAppearance(t *testing.T) {
tests := []struct {
name string
appearance *Appearance
saveError error
existingID int32
wantError bool
errorContains string
}{
{
name: "successful add",
appearance: NewAppearance(100, "Test", 1000),
wantError: false,
},
{
name: "nil appearance",
appearance: nil,
wantError: true,
errorContains: "cannot be nil",
},
{
name: "empty name",
appearance: &Appearance{id: 100, name: "", minClient: 1000},
wantError: true,
errorContains: "name cannot be empty",
},
{
name: "invalid ID",
appearance: NewAppearance(0, "Test", 1000),
wantError: true,
errorContains: "must be positive",
},
{
name: "duplicate ID",
appearance: NewAppearance(100, "Test", 1000),
existingID: 100,
wantError: true,
errorContains: "already exists",
},
{
name: "database save error",
appearance: NewAppearance(200, "Test", 1000),
saveError: fmt.Errorf("save failed"),
wantError: true,
errorContains: "database",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db := &MockDatabase{saveError: tt.saveError}
logger := &MockLogger{}
manager := NewManager(db, logger)
if tt.existingID > 0 {
manager.appearances.InsertAppearance(NewAppearance(tt.existingID, "Existing", 1000))
}
err := manager.AddAppearance(tt.appearance)
if (err != nil) != tt.wantError {
t.Errorf("AddAppearance() error = %v, wantError %v", err, tt.wantError)
}
if err != nil && tt.errorContains != "" && !contains(err.Error(), tt.errorContains) {
t.Errorf("Error message %q doesn't contain %q", err.Error(), tt.errorContains)
}
// Verify appearance was added/not added
if !tt.wantError && tt.appearance != nil {
if !manager.appearances.HasAppearance(tt.appearance.GetID()) {
t.Error("Appearance was not added to collection")
}
}
})
}
}
func TestManagerUpdateAppearance(t *testing.T) {
db := &MockDatabase{}
logger := &MockLogger{}
manager := NewManager(db, logger)
// Add initial appearance
original := NewAppearance(100, "Original", 1000)
manager.AddAppearance(original)
// Test successful update
updated := NewAppearance(100, "Updated", 1100)
err := manager.UpdateAppearance(updated)
if err != nil {
t.Errorf("UpdateAppearance failed: %v", err)
}
found := manager.FindAppearanceByID(100)
if found.GetName() != "Updated" {
t.Error("Appearance was not updated")
}
// Test update non-existent
notExist := NewAppearance(999, "NotExist", 1000)
err = manager.UpdateAppearance(notExist)
if err == nil {
t.Error("UpdateAppearance should fail for non-existent appearance")
}
// Test nil appearance
err = manager.UpdateAppearance(nil)
if err == nil {
t.Error("UpdateAppearance should fail for nil appearance")
}
}
func TestManagerRemoveAppearance(t *testing.T) {
db := &MockDatabase{}
logger := &MockLogger{}
manager := NewManager(db, logger)
// Add appearance
app := NewAppearance(100, "Test", 1000)
manager.AddAppearance(app)
// Test successful removal
err := manager.RemoveAppearance(100)
if err != nil {
t.Errorf("RemoveAppearance failed: %v", err)
}
if manager.appearances.HasAppearance(100) {
t.Error("Appearance was not removed")
}
// Test removing non-existent
err = manager.RemoveAppearance(999)
if err == nil {
t.Error("RemoveAppearance should fail for non-existent appearance")
}
// Test database delete error
db.deleteError = fmt.Errorf("delete failed")
manager.AddAppearance(NewAppearance(200, "Test2", 1000))
err = manager.RemoveAppearance(200)
if err == nil {
t.Error("RemoveAppearance should fail when database delete fails")
}
}
func TestManagerCommands(t *testing.T) {
db := &MockDatabase{
appearances: []*Appearance{
NewAppearance(100, "Human Male", 1000),
NewAppearance(200, "Human Female", 1096),
},
}
logger := &MockLogger{}
manager := NewManager(db, logger)
manager.Initialize()
// Test stats command
result, err := manager.ProcessCommand("stats", nil)
if err != nil {
t.Errorf("ProcessCommand(stats) failed: %v", err)
}
if !contains(result, "Appearance System Statistics") {
t.Error("Stats command output incorrect")
}
// Test validate command
result, err = manager.ProcessCommand("validate", nil)
if err != nil {
t.Errorf("ProcessCommand(validate) failed: %v", err)
}
if !contains(result, "valid") {
t.Error("Validate command output incorrect")
}
// Test search command
result, err = manager.ProcessCommand("search", []string{"Human"})
if err != nil {
t.Errorf("ProcessCommand(search) failed: %v", err)
}
if !contains(result, "Found 2 appearances") {
t.Error("Search command output incorrect")
}
// Test search without args
_, err = manager.ProcessCommand("search", nil)
if err == nil {
t.Error("Search command should fail without arguments")
}
// Test info command
result, err = manager.ProcessCommand("info", []string{"100"})
if err != nil {
t.Errorf("ProcessCommand(info) failed: %v", err)
}
if !contains(result, "Human Male") {
t.Error("Info command output incorrect")
}
// Test info without args
_, err = manager.ProcessCommand("info", nil)
if err == nil {
t.Error("Info command should fail without arguments")
}
// Test unknown command
_, err = manager.ProcessCommand("unknown", nil)
if err == nil {
t.Error("Unknown command should return error")
}
}
func TestManagerResetStatistics(t *testing.T) {
manager := NewManager(nil, nil)
// Perform some lookups
manager.FindAppearanceByID(100)
manager.FindAppearanceByID(200)
stats := manager.GetStatistics()
if totalLookups, ok := stats["total_lookups"].(int64); !ok || totalLookups != 2 {
t.Error("Statistics not tracked correctly")
}
// Reset statistics
manager.ResetStatistics()
stats = manager.GetStatistics()
if totalLookups, ok := stats["total_lookups"].(int64); !ok || totalLookups != 0 {
t.Error("Statistics not reset correctly")
}
}
func TestManagerShutdown(t *testing.T) {
logger := &MockLogger{}
manager := NewManager(nil, logger)
manager.appearances.InsertAppearance(NewAppearance(100, "Test", 1000))
manager.Shutdown()
if manager.GetAppearanceCount() != 0 {
t.Error("Shutdown did not clear appearances")
}
// Check for shutdown log
found := false
for _, log := range logger.logs {
if contains(log, "Shutting down") {
found = true
break
}
}
if !found {
t.Error("Shutdown did not log message")
}
}
// EntityAppearanceAdapter tests
func TestEntityAppearanceAdapter(t *testing.T) {
entity := &MockEntity{id: 1, name: "TestEntity", databaseID: 100}
logger := &MockLogger{}
manager := NewManager(nil, logger)
// Add test appearance
app := NewAppearance(500, "TestAppearance", 1000)
manager.AddAppearance(app)
adapter := NewEntityAppearanceAdapter(entity, manager, logger)
// Test initial state
if adapter.GetAppearanceID() != 0 {
t.Error("Initial appearance ID should be 0")
}
if adapter.GetAppearance() != nil {
t.Error("Initial appearance should be nil")
}
// Test setting appearance ID
adapter.SetAppearanceID(500)
if adapter.GetAppearanceID() != 500 {
t.Error("SetAppearanceID failed")
}
// Test getting appearance
appearance := adapter.GetAppearance()
if appearance == nil || appearance.GetID() != 500 {
t.Error("GetAppearance failed")
}
// Test appearance name
if name := adapter.GetAppearanceName(); name != "TestAppearance" {
t.Errorf("GetAppearanceName() = %v, want TestAppearance", name)
}
// Test validation
if err := adapter.ValidateAppearance(); err != nil {
t.Errorf("ValidateAppearance() failed: %v", err)
}
// Test invalid appearance
adapter.SetAppearanceID(999)
if err := adapter.ValidateAppearance(); err == nil {
t.Error("ValidateAppearance() should fail for invalid ID")
}
}
func TestEntityAppearanceAdapterClientCompatibility(t *testing.T) {
entity := &MockEntity{id: 1, name: "TestEntity"}
manager := NewManager(nil, nil)
// Add appearance with version requirement
app := NewAppearance(100, "Test", 1096)
manager.AddAppearance(app)
adapter := NewEntityAppearanceAdapter(entity, manager, nil)
adapter.SetAppearanceID(100)
// Test compatible client
if !adapter.IsCompatibleWithClient(1100) {
t.Error("Should be compatible with client version 1100")
}
// Test incompatible client
if adapter.IsCompatibleWithClient(1000) {
t.Error("Should not be compatible with client version 1000")
}
// Test no appearance (always compatible)
adapter.SetAppearanceID(0)
if !adapter.IsCompatibleWithClient(500) {
t.Error("No appearance should be compatible with all clients")
}
}
func TestEntityAppearanceAdapterSendToClient(t *testing.T) {
entity := &MockEntity{id: 1, name: "TestEntity"}
logger := &MockLogger{}
manager := NewManager(nil, logger)
app := NewAppearance(100, "Test", 1096)
manager.AddAppearance(app)
adapter := NewEntityAppearanceAdapter(entity, manager, logger)
adapter.SetAppearanceID(100)
// Test successful send
client := &MockClient{version: 1100}
err := adapter.SendAppearanceToClient(client)
if err != nil {
t.Errorf("SendAppearanceToClient failed: %v", err)
}
// Test incompatible client
lowClient := &MockClient{version: 1000}
err = adapter.SendAppearanceToClient(lowClient)
if err == nil {
t.Error("SendAppearanceToClient should fail for incompatible client")
}
// Test client send error
errorClient := &MockClient{version: 1100, sendError: fmt.Errorf("send failed")}
err = adapter.SendAppearanceToClient(errorClient)
if err == nil {
t.Error("SendAppearanceToClient should propagate client error")
}
// Test nil client
err = adapter.SendAppearanceToClient(nil)
if err == nil {
t.Error("SendAppearanceToClient should fail for nil client")
}
}
// Cache tests
func TestSimpleAppearanceCache(t *testing.T) {
cache := NewSimpleAppearanceCache()
app1 := NewAppearance(100, "Test1", 1000)
app2 := NewAppearance(200, "Test2", 1096)
// Test Set and Get
cache.Set(100, app1)
cache.Set(200, app2)
if got := cache.Get(100); got != app1 {
t.Error("Cache Get(100) failed")
}
if got := cache.Get(999); got != nil {
t.Error("Cache Get(999) should return nil")
}
// Test GetSize
if size := cache.GetSize(); size != 2 {
t.Errorf("GetSize() = %v, want 2", size)
}
// Test Remove
cache.Remove(100)
if cache.Get(100) != nil {
t.Error("Remove(100) failed")
}
if size := cache.GetSize(); size != 1 {
t.Errorf("GetSize() after remove = %v, want 1", size)
}
// Test Clear
cache.Clear()
if size := cache.GetSize(); size != 0 {
t.Errorf("GetSize() after clear = %v, want 0", size)
}
}
func TestCachedAppearanceManager(t *testing.T) {
db := &MockDatabase{}
logger := &MockLogger{}
baseManager := NewManager(db, logger)
// Add test appearances
app1 := NewAppearance(100, "Test1", 1000)
app2 := NewAppearance(200, "Test2", 1096)
baseManager.AddAppearance(app1)
baseManager.AddAppearance(app2)
cache := NewSimpleAppearanceCache()
cachedManager := NewCachedAppearanceManager(baseManager, cache)
// Test FindAppearanceByID with caching
found := cachedManager.FindAppearanceByID(100)
if found == nil || found.GetID() != 100 {
t.Error("FindAppearanceByID(100) failed")
}
// Verify it was cached
if cache.GetSize() != 1 {
t.Error("Appearance was not cached")
}
// Test cache hit
found2 := cachedManager.FindAppearanceByID(100)
if found2 != found {
t.Error("Should have returned cached appearance")
}
// Test AddAppearance updates cache
app3 := NewAppearance(300, "Test3", 1200)
err := cachedManager.AddAppearance(app3)
if err != nil {
t.Errorf("AddAppearance failed: %v", err)
}
if cache.Get(300) == nil {
t.Error("Added appearance was not cached")
}
// Test UpdateAppearance updates cache
updated := NewAppearance(100, "Updated", 1100)
err = cachedManager.UpdateAppearance(updated)
if err != nil {
t.Errorf("UpdateAppearance failed: %v", err)
}
cached := cache.Get(100)
if cached == nil || cached.GetName() != "Updated" {
t.Error("Cache was not updated")
}
// Test RemoveAppearance updates cache
err = cachedManager.RemoveAppearance(200)
if err != nil {
t.Errorf("RemoveAppearance failed: %v", err)
}
if cache.Get(200) != nil {
t.Error("Removed appearance still in cache")
}
// Test ClearCache
cachedManager.ClearCache()
if cache.GetSize() != 0 {
t.Error("ClearCache failed")
}
}
func TestCacheConcurrency(t *testing.T) {
cache := NewSimpleAppearanceCache()
var wg sync.WaitGroup
// Concurrent operations
for i := 0; i < 100; i++ {
wg.Add(3)
// Writer
go func(id int32) {
defer wg.Done()
app := NewAppearance(id, "Test", 1000)
cache.Set(id, app)
}(int32(i))
// Reader
go func(id int32) {
defer wg.Done()
_ = cache.Get(id)
}(int32(i))
// Size checker
go func() {
defer wg.Done()
_ = cache.GetSize()
}()
}
wg.Wait()
// Final size should be predictable
if size := cache.GetSize(); size > 100 {
t.Errorf("Cache size %v is too large", size)
}
}
// Additional tests for uncovered code paths
func TestEntityAppearanceAdapterErrorCases(t *testing.T) {
entity := &MockEntity{id: 1, name: "TestEntity"}
logger := &MockLogger{}
// Test with nil manager
adapter := NewEntityAppearanceAdapter(entity, nil, logger)
adapter.SetAppearanceID(100)
// GetAppearance should return nil and log error
appearance := adapter.GetAppearance()
if appearance != nil {
t.Error("GetAppearance should return nil with nil manager")
}
// Test UpdateAppearance with nil manager
err := adapter.UpdateAppearance(100)
if err == nil {
t.Error("UpdateAppearance should fail with nil manager")
}
// Test UpdateAppearance with non-existent appearance
manager := NewManager(nil, logger)
adapter = NewEntityAppearanceAdapter(entity, manager, logger)
err = adapter.UpdateAppearance(999)
if err == nil {
t.Error("UpdateAppearance should fail for non-existent appearance")
}
// Test SendAppearanceToClient with no appearance set
client := &MockClient{version: 1100}
adapter.SetAppearanceID(0)
err = adapter.SendAppearanceToClient(client)
if err != nil {
t.Errorf("SendAppearanceToClient should succeed with no appearance: %v", err)
}
}
func TestManagerGetAppearances(t *testing.T) {
manager := NewManager(nil, nil)
appearances := manager.GetAppearances()
if appearances == nil {
t.Error("GetAppearances should not return nil")
}
if appearances != manager.appearances {
t.Error("GetAppearances should return internal appearances collection")
}
}
func TestManagerCompatibleAndSearch(t *testing.T) {
manager := NewManager(nil, nil)
// Add test appearances
app1 := NewAppearance(100, "Human Male", 1000)
app2 := NewAppearance(200, "Human Female", 1096)
manager.AddAppearance(app1)
manager.AddAppearance(app2)
// Test GetCompatibleAppearances
compatible := manager.GetCompatibleAppearances(1050)
if len(compatible) != 1 {
t.Errorf("GetCompatibleAppearances(1050) returned %v results, want 1", len(compatible))
}
// Test SearchAppearancesByName
results := manager.SearchAppearancesByName("Human")
if len(results) != 2 {
t.Errorf("SearchAppearancesByName('Human') returned %v results, want 2", len(results))
}
}
func TestManagerReloadFromDatabase(t *testing.T) {
// Test with nil database
manager := NewManager(nil, nil)
err := manager.ReloadFromDatabase()
if err == nil {
t.Error("ReloadFromDatabase should fail with nil database")
}
// Test successful reload
db := &MockDatabase{
appearances: []*Appearance{
NewAppearance(100, "Test", 1000),
},
}
manager = NewManager(db, nil)
// Add some appearances first
manager.AddAppearance(NewAppearance(200, "Existing", 1000))
err = manager.ReloadFromDatabase()
if err != nil {
t.Errorf("ReloadFromDatabase failed: %v", err)
}
// Should only have the database appearance now
if count := manager.GetAppearanceCount(); count != 1 {
t.Errorf("After reload, count = %v, want 1", count)
}
if !manager.appearances.HasAppearance(100) {
t.Error("Reloaded appearance not found")
}
if manager.appearances.HasAppearance(200) {
t.Error("Previous appearance should be cleared")
}
}
func TestManagerCommandEdgeCases(t *testing.T) {
manager := NewManager(nil, nil)
// Test reload command without database
result, err := manager.ProcessCommand("reload", nil)
if err == nil {
t.Error("Reload command should fail without database")
}
// Test info command with invalid ID
_, err = manager.ProcessCommand("info", []string{"invalid"})
if err == nil {
t.Error("Info command should fail with invalid ID")
}
// Test info command with non-existent ID
result, err = manager.ProcessCommand("info", []string{"999"})
if err != nil {
t.Errorf("Info command failed: %v", err)
}
if !contains(result, "not found") {
t.Error("Info command should indicate appearance not found")
}
// Test search with no results
result, err = manager.ProcessCommand("search", []string{"nonexistent"})
if err != nil {
t.Errorf("Search command failed: %v", err)
}
if !contains(result, "No appearances found") {
t.Error("Search command should indicate no results")
}
}
func TestManagerValidateAllAppearances(t *testing.T) {
manager := NewManager(nil, nil)
// Add valid appearance
manager.AddAppearance(NewAppearance(100, "Valid", 1000))
issues := manager.ValidateAllAppearances()
if len(issues) != 0 {
t.Errorf("ValidateAllAppearances returned issues for valid data: %v", issues)
}
}
func TestEntityAppearanceAdapterGetAppearanceName(t *testing.T) {
entity := &MockEntity{id: 1, name: "TestEntity"}
manager := NewManager(nil, nil)
adapter := NewEntityAppearanceAdapter(entity, manager, nil)
// Test with no appearance
if name := adapter.GetAppearanceName(); name != "" {
t.Errorf("GetAppearanceName() with no appearance = %v, want empty string", name)
}
// Test with appearance
app := NewAppearance(100, "TestName", 1000)
manager.AddAppearance(app)
adapter.SetAppearanceID(100)
if name := adapter.GetAppearanceName(); name != "TestName" {
t.Errorf("GetAppearanceName() = %v, want TestName", name)
}
}