570 lines
13 KiB
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)
|
|
} |