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