package alt_advancement import ( "sync" "sync/atomic" "testing" ) // TestAAManagerConcurrentPlayerAccess tests concurrent access to player states func TestAAManagerConcurrentPlayerAccess(t *testing.T) { config := DefaultAAManagerConfig() manager := NewAAManager(config) // Set up mock database mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) // Test concurrent access to the same player const numGoroutines = 100 const characterID = int32(123) var wg sync.WaitGroup var successCount int64 // Launch multiple goroutines trying to get the same player state for i := 0; i < numGoroutines; i++ { wg.Add(1) go func() { defer wg.Done() state, err := manager.GetPlayerAAState(characterID) if err != nil { t.Errorf("Failed to get player state: %v", err) return } if state == nil { t.Error("Got nil player state") return } if state.CharacterID != characterID { t.Errorf("Wrong character ID: expected %d, got %d", characterID, state.CharacterID) return } atomic.AddInt64(&successCount, 1) }() } wg.Wait() if atomic.LoadInt64(&successCount) != numGoroutines { t.Errorf("Expected %d successful operations, got %d", numGoroutines, successCount) } // Verify only one instance was created in cache manager.statesMutex.RLock() cachedStates := len(manager.playerStates) manager.statesMutex.RUnlock() if cachedStates != 1 { t.Errorf("Expected 1 cached state, got %d", cachedStates) } } // TestAAManagerConcurrentMultiplePlayer tests concurrent access to different players func TestAAManagerConcurrentMultiplePlayer(t *testing.T) { config := DefaultAAManagerConfig() manager := NewAAManager(config) // Set up mock database mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) const numPlayers = 50 const goroutinesPerPlayer = 10 var wg sync.WaitGroup var successCount int64 // Launch multiple goroutines for different players for playerID := int32(1); playerID <= numPlayers; playerID++ { for j := 0; j < goroutinesPerPlayer; j++ { wg.Add(1) go func(id int32) { defer wg.Done() state, err := manager.GetPlayerAAState(id) if err != nil { t.Errorf("Failed to get player state for %d: %v", id, err) return } if state == nil { t.Errorf("Got nil player state for %d", id) return } if state.CharacterID != id { t.Errorf("Wrong character ID: expected %d, got %d", id, state.CharacterID) return } atomic.AddInt64(&successCount, 1) }(playerID) } } wg.Wait() expectedSuccess := int64(numPlayers * goroutinesPerPlayer) if atomic.LoadInt64(&successCount) != expectedSuccess { t.Errorf("Expected %d successful operations, got %d", expectedSuccess, successCount) } // Verify correct number of cached states manager.statesMutex.RLock() cachedStates := len(manager.playerStates) manager.statesMutex.RUnlock() if cachedStates != numPlayers { t.Errorf("Expected %d cached states, got %d", numPlayers, cachedStates) } } // TestConcurrentAAPurchases tests concurrent AA purchases func TestConcurrentAAPurchases(t *testing.T) { config := DefaultAAManagerConfig() manager := NewAAManager(config) // Set up mock database mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) // Add test AAs for i := 1; i <= 10; i++ { aa := &AltAdvanceData{ SpellID: int32(i * 100), NodeID: int32(i * 200), Name: "Test AA", Group: AA_CLASS, MaxRank: 5, RankCost: 1, // Low cost for testing MinLevel: 1, } manager.masterAAList.AddAltAdvancement(aa) } // Get player state and give it points state, err := manager.GetPlayerAAState(123) if err != nil { t.Fatalf("Failed to get player state: %v", err) } // Give player plenty of points state.TotalPoints = 1000 state.AvailablePoints = 1000 const numGoroutines = 20 var wg sync.WaitGroup var successCount, errorCount int64 // Concurrent purchases for i := 0; i < numGoroutines; i++ { wg.Add(1) go func(goroutineID int) { defer wg.Done() // Try to purchase different AAs aaNodeID := int32(200 + (goroutineID%10)*200) // Spread across different AAs err := manager.PurchaseAA(123, aaNodeID, 1) if err != nil { atomic.AddInt64(&errorCount, 1) // Some errors expected due to race conditions or insufficient points } else { atomic.AddInt64(&successCount, 1) } }(i) } wg.Wait() t.Logf("Successful purchases: %d, Errors: %d", successCount, errorCount) // Verify final state consistency state.mutex.RLock() finalAvailable := state.AvailablePoints finalSpent := state.SpentPoints finalTotal := state.TotalPoints numProgress := len(state.AAProgress) state.mutex.RUnlock() // Basic consistency checks if finalAvailable+finalSpent != finalTotal { t.Errorf("Point consistency check failed: available(%d) + spent(%d) != total(%d)", finalAvailable, finalSpent, finalTotal) } if numProgress > int(successCount) { t.Errorf("More progress entries (%d) than successful purchases (%d)", numProgress, successCount) } t.Logf("Final state: Total=%d, Spent=%d, Available=%d, Progress entries=%d", finalTotal, finalSpent, finalAvailable, numProgress) } // TestConcurrentAAPointAwarding tests concurrent point awarding func TestConcurrentAAPointAwarding(t *testing.T) { config := DefaultAAManagerConfig() manager := NewAAManager(config) // Set up mock database mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) const characterID = int32(123) const numGoroutines = 100 const pointsPerAward = int32(10) var wg sync.WaitGroup var successCount int64 // Concurrent point awarding for i := 0; i < numGoroutines; i++ { wg.Add(1) go func(goroutineID int) { defer wg.Done() err := manager.AwardAAPoints(characterID, pointsPerAward, "Concurrent test") if err != nil { t.Errorf("Failed to award points: %v", err) return } atomic.AddInt64(&successCount, 1) }(i) } wg.Wait() if atomic.LoadInt64(&successCount) != numGoroutines { t.Errorf("Expected %d successful awards, got %d", numGoroutines, successCount) } // Verify final point total total, spent, available, err := manager.GetAAPoints(characterID) if err != nil { t.Fatalf("Failed to get AA points: %v", err) } expectedTotal := pointsPerAward * numGoroutines if total != expectedTotal { t.Errorf("Expected total points %d, got %d", expectedTotal, total) } if spent != 0 { t.Errorf("Expected 0 spent points, got %d", spent) } if available != expectedTotal { t.Errorf("Expected available points %d, got %d", expectedTotal, available) } } // TestMasterAAListConcurrentOperations tests thread safety of MasterAAList func TestMasterAAListConcurrentOperations(t *testing.T) { masterList := NewMasterAAList() // Pre-populate with some AAs for i := 1; i <= 100; i++ { aa := &AltAdvanceData{ SpellID: int32(i * 100), NodeID: int32(i * 200), Name: "Test AA", Group: AA_CLASS, MaxRank: 5, RankCost: 2, } masterList.AddAltAdvancement(aa) } const numReaders = 50 const numWriters = 10 const operationsPerGoroutine = 100 var wg sync.WaitGroup var readOps, writeOps int64 // Reader goroutines for i := 0; i < numReaders; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < operationsPerGoroutine; j++ { // Mix different read operations switch j % 5 { case 0: masterList.GetAltAdvancement(100) case 1: masterList.GetAltAdvancementByNodeID(200) case 2: masterList.GetAAsByGroup(AA_CLASS) case 3: masterList.Size() case 4: masterList.GetAllAAs() } atomic.AddInt64(&readOps, 1) } }() } // Writer goroutines (adding new AAs) for i := 0; i < numWriters; i++ { wg.Add(1) go func(writerID int) { defer wg.Done() for j := 0; j < operationsPerGoroutine; j++ { // Create unique AAs for each writer baseID := (writerID + 1000) * 1000 + j aa := &AltAdvanceData{ SpellID: int32(baseID), NodeID: int32(baseID + 100000), Name: "Concurrent AA", Group: AA_CLASS, MaxRank: 5, RankCost: 2, } err := masterList.AddAltAdvancement(aa) if err != nil { // Some errors expected due to potential duplicates continue } atomic.AddInt64(&writeOps, 1) } }(i) } wg.Wait() t.Logf("Read operations: %d, Write operations: %d", readOps, writeOps) // Verify final state finalSize := masterList.Size() if finalSize < 100 { t.Errorf("Expected at least 100 AAs, got %d", finalSize) } t.Logf("Final AA count: %d", finalSize) } // TestMasterAANodeListConcurrentOperations tests thread safety of MasterAANodeList func TestMasterAANodeListConcurrentOperations(t *testing.T) { nodeList := NewMasterAANodeList() // Pre-populate with some nodes for i := 1; i <= 50; i++ { node := &TreeNodeData{ ClassID: int32(i % 10 + 1), // Classes 1-10 TreeID: int32(i * 100), AATreeID: int32(i * 200), } nodeList.AddTreeNode(node) } const numReaders = 30 const numWriters = 5 const operationsPerGoroutine = 100 var wg sync.WaitGroup var readOps, writeOps int64 // Reader goroutines for i := 0; i < numReaders; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < operationsPerGoroutine; j++ { // Mix different read operations switch j % 4 { case 0: nodeList.GetTreeNode(100) case 1: nodeList.GetTreeNodesByClass(1) case 2: nodeList.Size() case 3: nodeList.GetTreeNodes() } atomic.AddInt64(&readOps, 1) } }() } // Writer goroutines for i := 0; i < numWriters; i++ { wg.Add(1) go func(writerID int) { defer wg.Done() for j := 0; j < operationsPerGoroutine; j++ { // Create unique nodes for each writer baseID := (writerID + 1000) * 1000 + j node := &TreeNodeData{ ClassID: int32(writerID%5 + 1), TreeID: int32(baseID), AATreeID: int32(baseID + 100000), } err := nodeList.AddTreeNode(node) if err != nil { // Some errors expected due to potential duplicates continue } atomic.AddInt64(&writeOps, 1) } }(i) } wg.Wait() t.Logf("Read operations: %d, Write operations: %d", readOps, writeOps) // Verify final state finalSize := nodeList.Size() if finalSize < 50 { t.Errorf("Expected at least 50 nodes, got %d", finalSize) } t.Logf("Final node count: %d", finalSize) } // TestAAPlayerStateConcurrentAccess tests thread safety of AAPlayerState func TestAAPlayerStateConcurrentAccess(t *testing.T) { playerState := NewAAPlayerState(123) // Give player some initial points playerState.TotalPoints = 1000 playerState.AvailablePoints = 1000 const numGoroutines = 100 var wg sync.WaitGroup // Concurrent operations on player state for i := 0; i < numGoroutines; i++ { wg.Add(1) go func(goroutineID int) { defer wg.Done() // Mix of different operations switch goroutineID % 4 { case 0: // Add AA progress progress := &PlayerAAData{ CharacterID: 123, NodeID: int32(goroutineID + 1000), CurrentRank: 1, PointsSpent: 2, } playerState.AddAAProgress(progress) case 1: // Update points playerState.UpdatePoints(1000, int32(goroutineID), 0) case 2: // Get AA progress playerState.GetAAProgress(int32(goroutineID + 1000)) case 3: // Calculate spent points playerState.CalculateSpentPoints() } }(i) } wg.Wait() // Verify state is still consistent playerState.mutex.RLock() totalPoints := playerState.TotalPoints progressCount := len(playerState.AAProgress) playerState.mutex.RUnlock() if totalPoints != 1000 { t.Errorf("Expected total points to remain 1000, got %d", totalPoints) } t.Logf("Final progress entries: %d", progressCount) } // TestConcurrentSystemOperations tests mixed system operations func TestConcurrentSystemOperations(t *testing.T) { config := DefaultAAManagerConfig() manager := NewAAManager(config) // Set up mock database mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) // Add some test AAs for i := 1; i <= 20; i++ { aa := &AltAdvanceData{ SpellID: int32(i * 100), NodeID: int32(i * 200), Name: "Test AA", Group: int8(i % 3), // Mix groups MaxRank: 5, RankCost: 2, MinLevel: 1, } manager.masterAAList.AddAltAdvancement(aa) } const numGoroutines = 50 var wg sync.WaitGroup var operations int64 for i := 0; i < numGoroutines; i++ { wg.Add(1) go func(goroutineID int) { defer wg.Done() playerID := int32(goroutineID%10 + 1) // 10 different players // Mix of operations switch goroutineID % 6 { case 0: // Get player state manager.GetPlayerAAState(playerID) case 1: // Award points manager.AwardAAPoints(playerID, 50, "Test") case 2: // Get AA points manager.GetAAPoints(playerID) case 3: // Get AAs by group manager.GetAAsByGroup(AA_CLASS) case 4: // Get system stats manager.GetSystemStats() case 5: // Try to purchase AA (might fail, that's ok) manager.PurchaseAA(playerID, 200, 1) } atomic.AddInt64(&operations, 1) }(i) } wg.Wait() if atomic.LoadInt64(&operations) != numGoroutines { t.Errorf("Expected %d operations, got %d", numGoroutines, operations) } t.Logf("Completed %d concurrent system operations", operations) }