eq2go/internal/alt_advancement/concurrency_test.go

570 lines
13 KiB
Go

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)
}