446 lines
11 KiB
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")
|
|
}
|
|
}
|