eq2go/internal/appearances/appearance_test.go
2025-08-08 10:44:31 -05:00

480 lines
13 KiB
Go

package appearances
import (
"sync"
"testing"
"eq2emu/internal/database"
)
func TestNew(t *testing.T) {
db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared")
if err != nil {
t.Fatalf("Failed to create test database: %v", err)
}
defer db.Close()
// Test creating a new appearance
appearance := New(db)
if appearance == nil {
t.Fatal("New returned nil")
}
if !appearance.IsNew() {
t.Error("New appearance should be marked as new")
}
// Test setting values
appearance.ID = 1001
appearance.Name = "Test Appearance"
appearance.MinClient = 1096
if appearance.GetID() != 1001 {
t.Errorf("Expected GetID() to return 1001, got %d", appearance.GetID())
}
if appearance.GetName() != "Test Appearance" {
t.Errorf("Expected GetName() to return 'Test Appearance', got %s", appearance.GetName())
}
if appearance.GetMinClientVersion() != 1096 {
t.Errorf("Expected GetMinClientVersion() to return 1096, got %d", appearance.GetMinClientVersion())
}
}
func TestNewWithData(t *testing.T) {
db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared")
if err != nil {
t.Fatalf("Failed to create test database: %v", err)
}
defer db.Close()
appearance := NewWithData(100, "Human Male", 1096, db)
if appearance == nil {
t.Fatal("NewWithData returned nil")
}
if appearance.GetID() != 100 {
t.Errorf("Expected ID 100, got %d", appearance.GetID())
}
if appearance.GetName() != "Human Male" {
t.Errorf("Expected name 'Human Male', got '%s'", appearance.GetName())
}
if appearance.GetMinClientVersion() != 1096 {
t.Errorf("Expected min client 1096, got %d", appearance.GetMinClientVersion())
}
if !appearance.IsNew() {
t.Error("NewWithData should create new appearance")
}
}
func TestAppearanceGetters(t *testing.T) {
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
defer db.Close()
app := NewWithData(123, "Test Appearance", 1096, db)
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) {
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
defer db.Close()
app := NewWithData(100, "Original", 1000, db)
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) {
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
defer db.Close()
app := NewWithData(100, "Test", 1096, db)
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) {
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
defer db.Close()
original := NewWithData(500, "Original Appearance", 1200, db)
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())
}
if !clone.IsNew() {
t.Error("Clone should always be marked as new")
}
// Verify modification independence
clone.SetName("Modified Clone")
if original.GetName() == "Modified Clone" {
t.Error("Modifying clone affected original")
}
}
// Test appearance type functions
func TestGetAppearanceType(t *testing.T) {
tests := []struct {
typeName string
expected int8
}{
{"hair_color1", AppearanceHairColor1},
{"soga_hair_color1", AppearanceSOGAHairColor1},
{"skin_color", AppearanceSkinColor},
{"eye_color", AppearanceEyeColor},
{"unknown_type", -1},
}
for _, tt := range tests {
t.Run(tt.typeName, func(t *testing.T) {
result := GetAppearanceType(tt.typeName)
if result != tt.expected {
t.Errorf("GetAppearanceType(%q) = %d, want %d", tt.typeName, result, tt.expected)
}
})
}
}
func TestGetAppearanceTypeName(t *testing.T) {
tests := []struct {
typeConst int8
expected string
}{
{AppearanceHairColor1, "hair_color1"},
{AppearanceSOGAHairColor1, "soga_hair_color1"},
{AppearanceSkinColor, "skin_color"},
{AppearanceEyeColor, "eye_color"},
{-1, "unknown"},
{100, "unknown"},
}
for _, tt := range tests {
t.Run("", func(t *testing.T) {
result := GetAppearanceTypeName(tt.typeConst)
if result != tt.expected {
t.Errorf("GetAppearanceTypeName(%d) = %q, want %q", tt.typeConst, result, tt.expected)
}
})
}
}
// TestMasterList tests the bespoke master list implementation
func TestMasterList(t *testing.T) {
masterList := NewMasterList()
if masterList == nil {
t.Fatal("NewMasterList returned nil")
}
if masterList.Size() != 0 {
t.Errorf("Expected size 0, got %d", masterList.Size())
}
// Create test database
db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared")
if err != nil {
t.Fatalf("Failed to create test database: %v", err)
}
defer db.Close()
// Create appearances for testing
app1 := NewWithData(1001, "Human Male", 1096, db)
app2 := NewWithData(1002, "Elf Female", 1200, db)
app3 := NewWithData(1003, "Dwarf Warrior", 1096, db)
// Test adding
if !masterList.AddAppearance(app1) {
t.Error("Should successfully add app1")
}
if !masterList.AddAppearance(app2) {
t.Error("Should successfully add app2")
}
if !masterList.AddAppearance(app3) {
t.Error("Should successfully add app3")
}
if masterList.Size() != 3 {
t.Errorf("Expected size 3, got %d", masterList.Size())
}
// Test duplicate add (should fail)
if masterList.AddAppearance(app1) {
t.Error("Should not add duplicate appearance")
}
// Test retrieving
retrieved := masterList.GetAppearance(1001)
if retrieved == nil {
t.Error("Should retrieve added appearance")
}
if retrieved.Name != "Human Male" {
t.Errorf("Expected name 'Human Male', got '%s'", retrieved.Name)
}
// Test safe retrieval
retrievedSafe, exists := masterList.GetAppearanceSafe(1001)
if !exists {
t.Error("Should find existing appearance")
}
if retrievedSafe.Name != "Human Male" {
t.Errorf("Expected safe name 'Human Male', got '%s'", retrievedSafe.Name)
}
_, notExists := masterList.GetAppearanceSafe(9999)
if notExists {
t.Error("Should not find non-existent appearance")
}
// Test client version filtering
version1096 := masterList.FindAppearancesByMinClient(1096)
if len(version1096) != 2 {
t.Errorf("Expected 2 appearances with min client 1096, got %d", len(version1096))
}
version1200 := masterList.FindAppearancesByMinClient(1200)
if len(version1200) != 1 {
t.Errorf("Expected 1 appearance with min client 1200, got %d", len(version1200))
}
// Test compatible appearances
compatible1200 := masterList.GetCompatibleAppearances(1200)
if len(compatible1200) != 3 {
t.Errorf("Expected 3 appearances compatible with client 1200, got %d", len(compatible1200))
}
compatible1100 := masterList.GetCompatibleAppearances(1100)
if len(compatible1100) != 2 {
t.Errorf("Expected 2 appearances compatible with client 1100, got %d", len(compatible1100))
}
// Test name searching (case insensitive)
// Names: "Human Male", "Elf Female", "Dwarf Warrior"
humanApps := masterList.FindAppearancesByName("human")
if len(humanApps) != 1 {
t.Errorf("Expected 1 appearance with 'human' in name, got %d", len(humanApps))
}
maleApps := masterList.FindAppearancesByName("male")
if len(maleApps) != 1 { // Only "Human Male" contains "male"
t.Errorf("Expected 1 appearance with 'male' in name, got %d", len(maleApps))
}
// Test exact name match (indexed lookup)
humanMaleApps := masterList.FindAppearancesByName("human male")
if len(humanMaleApps) != 1 {
t.Errorf("Expected 1 appearance with exact name 'human male', got %d", len(humanMaleApps))
}
// Test ID range filtering
rangeApps := masterList.GetAppearancesByIDRange(1001, 1002)
if len(rangeApps) != 2 {
t.Errorf("Expected 2 appearances in range 1001-1002, got %d", len(rangeApps))
}
// Test client version range filtering
clientRangeApps := masterList.GetAppearancesByClientRange(1096, 1096)
if len(clientRangeApps) != 2 {
t.Errorf("Expected 2 appearances in client range 1096-1096, got %d", len(clientRangeApps))
}
// Test metadata caching
clientVersions := masterList.GetClientVersions()
if len(clientVersions) != 2 {
t.Errorf("Expected 2 unique client versions, got %d", len(clientVersions))
}
// Test clone
clone := masterList.GetAppearanceClone(1001)
if clone == nil {
t.Error("Should return cloned appearance")
}
if clone.Name != "Human Male" {
t.Errorf("Expected cloned name 'Human Male', got '%s'", clone.Name)
}
// Test GetAllAppearances
allApps := masterList.GetAllAppearances()
if len(allApps) != 3 {
t.Errorf("Expected 3 appearances in GetAll, got %d", len(allApps))
}
// Test GetAllAppearancesList
allAppsList := masterList.GetAllAppearancesList()
if len(allAppsList) != 3 {
t.Errorf("Expected 3 appearances in GetAllList, got %d", len(allAppsList))
}
// Test update
updatedApp := NewWithData(1001, "Updated Human", 1500, db)
if err := masterList.UpdateAppearance(updatedApp); err != nil {
t.Errorf("Update should succeed: %v", err)
}
// Verify update worked
retrievedUpdated := masterList.GetAppearance(1001)
if retrievedUpdated.Name != "Updated Human" {
t.Errorf("Expected updated name 'Updated Human', got '%s'", retrievedUpdated.Name)
}
// Verify client version index updated
version1500 := masterList.FindAppearancesByMinClient(1500)
if len(version1500) != 1 {
t.Errorf("Expected 1 appearance with min client 1500, got %d", len(version1500))
}
// Test removal
if !masterList.RemoveAppearance(1001) {
t.Error("Should successfully remove appearance")
}
if masterList.Size() != 2 {
t.Errorf("Expected size 2 after removal, got %d", masterList.Size())
}
// Test validation
issues := masterList.ValidateAppearances()
if len(issues) != 0 {
t.Errorf("Expected no validation issues, got %d", len(issues))
}
// Test statistics
stats := masterList.GetStatistics()
if stats["total_appearances"] != 2 {
t.Errorf("Expected statistics total 2, got %v", stats["total_appearances"])
}
// Test clear
masterList.Clear()
if masterList.Size() != 0 {
t.Errorf("Expected size 0 after clear, got %d", masterList.Size())
}
}
// TestMasterListConcurrency tests thread safety of the master list
func TestMasterListConcurrency(t *testing.T) {
masterList := NewMasterList()
// Create test database
db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared")
if err != nil {
t.Fatalf("Failed to create test database: %v", err)
}
defer db.Close()
const numWorkers = 10
const appsPerWorker = 100
var wg sync.WaitGroup
// Concurrently add appearances
wg.Add(numWorkers)
for i := 0; i < numWorkers; i++ {
go func(workerID int) {
defer wg.Done()
for j := 0; j < appsPerWorker; j++ {
app := NewWithData(
int32(workerID*appsPerWorker+j+1),
"Concurrent Test",
int16(1096+(workerID%3)*100),
db,
)
masterList.AddAppearance(app)
}
}(i)
}
// Concurrently read appearances
wg.Add(numWorkers)
for i := 0; i < numWorkers; i++ {
go func() {
defer wg.Done()
for j := 0; j < appsPerWorker; j++ {
// Random reads
_ = masterList.GetAppearance(int32(j + 1))
_ = masterList.FindAppearancesByMinClient(1096)
_ = masterList.GetCompatibleAppearances(1200)
_ = masterList.Size()
}
}()
}
wg.Wait()
// Verify final state
expectedSize := numWorkers * appsPerWorker
if masterList.Size() != expectedSize {
t.Errorf("Expected size %d, got %d", expectedSize, masterList.Size())
}
clientVersions := masterList.GetClientVersions()
if len(clientVersions) != 3 {
t.Errorf("Expected 3 client versions, got %d", len(clientVersions))
}
}