eq2go/internal/entity/concurrency_test.go

446 lines
11 KiB
Go

package entity
import (
"sync"
"sync/atomic"
"testing"
"time"
)
// TestEntityConcurrencyStress performs intensive concurrent operations to identify race conditions
func TestEntityConcurrencyStress(t *testing.T) {
entity := NewEntity()
info := entity.GetInfoStruct()
info.SetMaxConcentration(1000) // Large concentration pool
var wg sync.WaitGroup
numGoroutines := 100
operationsPerGoroutine := 100
// Test 1: Concurrent combat and casting state changes
t.Run("CombatCastingStates", func(t *testing.T) {
var combatOps, castingOps int64
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func() {
defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ {
// Combat state operations
entity.SetInCombat(true)
if entity.IsInCombat() {
atomic.AddInt64(&combatOps, 1)
}
entity.SetInCombat(false)
// Casting state operations
entity.SetCasting(true)
if entity.IsCasting() {
atomic.AddInt64(&castingOps, 1)
}
entity.SetCasting(false)
}
}()
}
wg.Wait()
t.Logf("Combat operations: %d, Casting operations: %d", combatOps, castingOps)
})
// Test 2: Concurrent spell effect operations
t.Run("SpellEffects", func(t *testing.T) {
var addOps, removeOps int64
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) {
defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ {
spellID := int32(goroutineID*1000 + j)
if entity.AddSpellEffect(spellID, int32(goroutineID), 30.0) {
atomic.AddInt64(&addOps, 1)
}
if entity.RemoveSpellEffect(spellID) {
atomic.AddInt64(&removeOps, 1)
}
}
}(i)
}
wg.Wait()
t.Logf("Spell effect adds: %d, removes: %d", addOps, removeOps)
})
// Test 3: Concurrent maintained spell operations with concentration management
t.Run("MaintainedSpells", func(t *testing.T) {
var addOps, removeOps, concentrationFailures int64
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) {
defer wg.Done()
for j := 0; j < operationsPerGoroutine/10; j++ { // Fewer ops due to concentration limits
spellID := int32(goroutineID*100 + j + 10000)
if entity.AddMaintainedSpell("Stress Test Spell", spellID, 60.0, 1) {
atomic.AddInt64(&addOps, 1)
// Small delay to increase contention
time.Sleep(time.Microsecond)
if entity.RemoveMaintainedSpell(spellID) {
atomic.AddInt64(&removeOps, 1)
}
} else {
atomic.AddInt64(&concentrationFailures, 1)
}
}
}(i)
}
wg.Wait()
t.Logf("Maintained spell adds: %d, removes: %d, concentration failures: %d",
addOps, removeOps, concentrationFailures)
// Verify concentration was properly managed
currentConc := info.GetCurConcentration()
if currentConc != 0 {
t.Errorf("Expected concentration to be 0 after all operations, got %d", currentConc)
}
})
// Test 4: Concurrent stat calculations with bonuses
t.Run("StatCalculations", func(t *testing.T) {
var statReads int64
// Set some base stats
info.SetStr(100.0)
info.SetSta(100.0)
info.SetAgi(100.0)
info.SetWis(100.0)
info.SetIntel(100.0)
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) {
defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ {
// Add some stat bonuses
entity.AddStatBonus(int32(goroutineID*1000+j), 1, float32(j%10))
// Read stats (these involve bonus calculations)
_ = entity.GetStr()
_ = entity.GetSta()
_ = entity.GetAgi()
_ = entity.GetWis()
_ = entity.GetIntel()
_ = entity.GetPrimaryStat()
atomic.AddInt64(&statReads, 6)
// Trigger bonus recalculation
entity.CalculateBonuses()
}
}(i)
}
wg.Wait()
t.Logf("Stat reads: %d", statReads)
})
// Test 5: Concurrent pet management
t.Run("PetManagement", func(t *testing.T) {
var petOps int64
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func() {
defer wg.Done()
for j := 0; j < operationsPerGoroutine/10; j++ {
pet := NewEntity()
// Test different pet types
switch j % 4 {
case 0:
entity.SetPet(pet)
_ = entity.GetPet()
entity.SetPet(nil)
case 1:
entity.SetCharmedPet(pet)
_ = entity.GetCharmedPet()
entity.SetCharmedPet(nil)
case 2:
entity.SetDeityPet(pet)
_ = entity.GetDeityPet()
entity.SetDeityPet(nil)
case 3:
entity.SetCosmeticPet(pet)
_ = entity.GetCosmeticPet()
entity.SetCosmeticPet(nil)
}
atomic.AddInt64(&petOps, 1)
}
}()
}
wg.Wait()
t.Logf("Pet operations: %d", petOps)
})
}
// TestInfoStructConcurrencyStress performs intensive concurrent operations on InfoStruct
func TestInfoStructConcurrencyStress(t *testing.T) {
info := NewInfoStruct()
info.SetMaxConcentration(1000)
var wg sync.WaitGroup
numGoroutines := 100
operationsPerGoroutine := 100
// Test 1: Concurrent basic property access
t.Run("BasicProperties", func(t *testing.T) {
var nameOps, levelOps, statOps int64
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) {
defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ {
// Name operations
info.SetName("TestChar" + string(rune('A'+goroutineID%26)))
_ = info.GetName()
atomic.AddInt64(&nameOps, 1)
// Level operations
info.SetLevel(int16(j % 100))
_ = info.GetLevel()
atomic.AddInt64(&levelOps, 1)
// Stat operations
info.SetStr(float32(j))
_ = info.GetStr()
atomic.AddInt64(&statOps, 1)
}
}(i)
}
wg.Wait()
t.Logf("Name ops: %d, Level ops: %d, Stat ops: %d", nameOps, levelOps, statOps)
})
// Test 2: Concurrent concentration management
t.Run("ConcentrationManagement", func(t *testing.T) {
var addSuccesses, addFailures, removes int64
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func() {
defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ {
amount := int16(j%5 + 1) // 1-5 concentration points
if info.AddConcentration(amount) {
atomic.AddInt64(&addSuccesses, 1)
// Small delay to increase contention
time.Sleep(time.Microsecond)
info.RemoveConcentration(amount)
atomic.AddInt64(&removes, 1)
} else {
atomic.AddInt64(&addFailures, 1)
}
}
}()
}
wg.Wait()
t.Logf("Concentration adds: %d, failures: %d, removes: %d",
addSuccesses, addFailures, removes)
// Verify final state
finalConc := info.GetCurConcentration()
if finalConc < 0 || finalConc > info.GetMaxConcentration() {
t.Errorf("Invalid final concentration: %d (max: %d)", finalConc, info.GetMaxConcentration())
}
})
// Test 3: Concurrent coin operations
t.Run("CoinOperations", func(t *testing.T) {
var addOps, removeSuccesses, removeFailures int64
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) {
defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ {
amount := int32((goroutineID*1000 + j) % 10000)
// Add coins
info.AddCoins(amount)
atomic.AddInt64(&addOps, 1)
// Try to remove some coins
removeAmount := amount / 2
if info.RemoveCoins(removeAmount) {
atomic.AddInt64(&removeSuccesses, 1)
} else {
atomic.AddInt64(&removeFailures, 1)
}
// Read total coins
_ = info.GetCoins()
}
}(i)
}
wg.Wait()
t.Logf("Coin adds: %d, remove successes: %d, failures: %d",
addOps, removeSuccesses, removeFailures)
// Verify coins are non-negative
finalCoins := info.GetCoins()
if finalCoins < 0 {
t.Errorf("Coins became negative: %d", finalCoins)
}
})
// Test 4: Concurrent resistance operations
t.Run("ResistanceOperations", func(t *testing.T) {
var resistOps int64
resistTypes := []string{"heat", "cold", "magic", "mental", "divine", "disease", "poison"}
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) {
defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ {
resistType := resistTypes[j%len(resistTypes)]
value := int16(j % 100)
info.SetResistance(resistType, value)
_ = info.GetResistance(resistType)
atomic.AddInt64(&resistOps, 1)
}
}(i)
}
wg.Wait()
t.Logf("Resistance operations: %d", resistOps)
})
// Test 5: Concurrent clone operations
t.Run("CloneOperations", func(t *testing.T) {
var cloneOps int64
// Set some initial state
info.SetName("Original")
info.SetLevel(50)
info.SetStr(100.0)
info.AddConcentration(5)
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func() {
defer wg.Done()
for j := 0; j < operationsPerGoroutine/10; j++ { // Fewer clones as they're expensive
clone := info.Clone()
if clone != nil {
// Verify clone independence
clone.SetName("Clone")
if info.GetName() == "Clone" {
t.Errorf("Clone modified original")
}
atomic.AddInt64(&cloneOps, 1)
}
}
}()
}
wg.Wait()
t.Logf("Clone operations: %d", cloneOps)
})
}
// TestRaceConditionDetection uses the race detector to find potential issues
func TestRaceConditionDetection(t *testing.T) {
if testing.Short() {
t.Skip("Skipping race condition test in short mode")
}
entity := NewEntity()
info := entity.GetInfoStruct()
info.SetMaxConcentration(100)
var wg sync.WaitGroup
numGoroutines := 50
// Create a scenario designed to trigger race conditions
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func(id int) {
defer wg.Done()
for j := 0; j < 50; j++ {
// Mix of read and write operations that could race
entity.SetInCombat(id%2 == 0)
isInCombat := entity.IsInCombat()
entity.SetCasting(j%2 == 0)
isCasting := entity.IsCasting()
// Stats with bonus calculations
info.SetStr(float32(id + j))
str := entity.GetStr()
// Concentration with potential for contention
if info.AddConcentration(1) {
info.RemoveConcentration(1)
}
// Use the values to prevent optimization
_ = isInCombat
_ = isCasting
_ = str
}
}(i)
}
wg.Wait()
}
// Cleanup test to run after stress tests
func TestConcurrencyCleanup(t *testing.T) {
entity := NewEntity()
info := entity.GetInfoStruct()
// Verify entity is in clean state after stress tests
if entity.IsInCombat() {
t.Error("Entity should not be in combat after tests")
}
if entity.IsCasting() {
t.Error("Entity should not be casting after tests")
}
if info.GetCurConcentration() < 0 {
t.Error("Concentration should not be negative")
}
if info.GetCoins() < 0 {
t.Error("Coins should not be negative")
}
}