package ground_spawn import ( "sync" "testing" "time" ) // Mock implementations are in test_utils.go // Stress test GroundSpawn with concurrent operations func TestGroundSpawnConcurrency(t *testing.T) { config := GroundSpawnConfig{ GroundSpawnID: 1, CollectionSkill: SkillGathering, NumberHarvests: 10, AttemptsPerHarvest: 2, RandomizeHeading: true, Location: SpawnLocation{ X: 100.0, Y: 200.0, Z: 300.0, Heading: 45.0, GridID: 1, }, Name: "Test Node", Description: "A test harvestable node", } gs := NewGroundSpawn(config) const numGoroutines = 100 const operationsPerGoroutine = 100 var wg sync.WaitGroup t.Run("ConcurrentGetterSetterOperations", func(t *testing.T) { wg.Add(numGoroutines) for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() for j := range operationsPerGoroutine { switch j % 8 { case 0: gs.SetNumberHarvests(int8(goroutineID % 10)) case 1: _ = gs.GetNumberHarvests() case 2: gs.SetAttemptsPerHarvest(int8(goroutineID % 5)) case 3: _ = gs.GetAttemptsPerHarvest() case 4: gs.SetCollectionSkill(SkillMining) case 5: _ = gs.GetCollectionSkill() case 6: gs.SetRandomizeHeading(goroutineID%2 == 0) case 7: _ = gs.GetRandomizeHeading() } } }(i) } wg.Wait() }) t.Run("ConcurrentStateChecks", func(t *testing.T) { wg.Add(numGoroutines) for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() for j := range operationsPerGoroutine { switch j % 4 { case 0: _ = gs.IsDepleted() case 1: _ = gs.IsAvailable() case 2: _ = gs.GetHarvestMessageName(true, false) case 3: _ = gs.GetHarvestSpellType() } } }(i) } wg.Wait() }) t.Run("ConcurrentCopyOperations", func(t *testing.T) { wg.Add(numGoroutines) for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() for j := range operationsPerGoroutine { // Test concurrent copying while modifying if j%2 == 0 { copy := gs.Copy() if copy == nil { t.Errorf("Goroutine %d: Copy returned nil", goroutineID) } } else { gs.SetNumberHarvests(int8(goroutineID % 5)) } } }(i) } wg.Wait() }) t.Run("ConcurrentRespawnOperations", func(t *testing.T) { wg.Add(numGoroutines) for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() for j := range operationsPerGoroutine { if j%10 == 0 { gs.Respawn() } else { // Mix of reads and writes during respawn switch j % 4 { case 0: _ = gs.GetNumberHarvests() case 1: gs.SetNumberHarvests(int8(goroutineID % 3)) case 2: _ = gs.IsAvailable() case 3: _ = gs.IsDepleted() } } } }(i) } wg.Wait() }) } // Stress test Manager with concurrent operations func TestManagerConcurrency(t *testing.T) { manager := NewManager(nil, &mockLogger{}) // Pre-populate with some ground spawns for i := int32(1); i <= 10; i++ { config := GroundSpawnConfig{ GroundSpawnID: i, CollectionSkill: SkillGathering, NumberHarvests: 5, AttemptsPerHarvest: 1, Location: SpawnLocation{ X: float32(i * 10), Y: float32(i * 20), Z: float32(i * 30), Heading: float32(i * 45), GridID: 1, }, Name: "Test Node", Description: "Test node", } gs := manager.CreateGroundSpawn(config) if gs == nil { t.Fatalf("Failed to create ground spawn %d", i) } } const numGoroutines = 100 const operationsPerGoroutine = 100 var wg sync.WaitGroup t.Run("ConcurrentGroundSpawnAccess", func(t *testing.T) { wg.Add(numGoroutines) for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() for j := range operationsPerGoroutine { spawnID := int32((goroutineID % 10) + 1) switch j % 5 { case 0: _ = manager.GetGroundSpawn(spawnID) case 1: _ = manager.GetGroundSpawnsByZone(1) case 2: _ = manager.GetGroundSpawnCount() case 3: _ = manager.GetActiveGroundSpawns() case 4: _ = manager.GetDepletedGroundSpawns() } } }(i) } wg.Wait() }) t.Run("ConcurrentStatisticsOperations", func(t *testing.T) { wg.Add(numGoroutines) for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() for j := range operationsPerGoroutine { switch j % 3 { case 0: _ = manager.GetStatistics() case 1: manager.ResetStatistics() case 2: // Simulate harvest statistics updates manager.mutex.Lock() manager.totalHarvests++ manager.successfulHarvests++ skill := SkillGathering manager.harvestsBySkill[skill]++ manager.mutex.Unlock() } } }(i) } wg.Wait() // Verify statistics consistency stats := manager.GetStatistics() if stats.TotalHarvests < 0 || stats.SuccessfulHarvests < 0 { t.Errorf("Invalid statistics: total=%d, successful=%d", stats.TotalHarvests, stats.SuccessfulHarvests) } }) t.Run("ConcurrentGroundSpawnModification", func(t *testing.T) { wg.Add(numGoroutines) for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() for j := range operationsPerGoroutine { // Use a more unique ID generation strategy to avoid conflicts // Start at 10000 and use goroutine*1000 + iteration to ensure uniqueness newID := int32(10000 + goroutineID*1000 + j) config := GroundSpawnConfig{ GroundSpawnID: newID, CollectionSkill: SkillMining, NumberHarvests: 3, AttemptsPerHarvest: 1, Location: SpawnLocation{ X: float32(j), Y: float32(j * 2), Z: float32(j * 3), Heading: float32(j * 10), GridID: 1, }, Name: "Concurrent Node", Description: "Concurrent test node", } // Add ground spawn - note that CreateGroundSpawn overwrites the ID gs := manager.CreateGroundSpawn(config) if gs == nil { t.Errorf("Goroutine %d: Failed to create ground spawn", goroutineID) continue } // Since CreateGroundSpawn assigns its own ID, we need to get the actual ID actualID := gs.GetID() // Verify it was added with the manager-assigned ID retrieved := manager.GetGroundSpawn(actualID) if retrieved == nil { t.Errorf("Goroutine %d: Failed to retrieve ground spawn %d", goroutineID, actualID) } } }(i) } wg.Wait() }) t.Run("ConcurrentRespawnProcessing", func(t *testing.T) { wg.Add(numGoroutines) for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() for j := range operationsPerGoroutine { if j%50 == 0 { // Process respawns occasionally manager.ProcessRespawns() } else { // Schedule respawns spawnID := int32((goroutineID % 10) + 1) if gs := manager.GetGroundSpawn(spawnID); gs != nil { if gs.IsDepleted() { manager.scheduleRespawn(gs) } } } } }(i) } wg.Wait() }) } // Test for potential deadlocks func TestDeadlockPrevention(t *testing.T) { manager := NewManager(nil, &mockLogger{}) // Create test ground spawns for i := int32(1); i <= 10; i++ { config := GroundSpawnConfig{ GroundSpawnID: i, CollectionSkill: SkillGathering, NumberHarvests: 5, AttemptsPerHarvest: 1, Location: SpawnLocation{ X: float32(i * 10), Y: float32(i * 20), Z: float32(i * 30), Heading: 0, GridID: 1, }, Name: "Deadlock Test Node", Description: "Test node", } gs := manager.CreateGroundSpawn(config) if gs == nil { t.Fatalf("Failed to create ground spawn %d", i) } } const numGoroutines = 50 var wg sync.WaitGroup // Test potential deadlock scenarios t.Run("MixedOperations", func(t *testing.T) { done := make(chan bool, 1) // Set a timeout to detect deadlocks go func() { time.Sleep(10 * time.Second) select { case <-done: return default: t.Error("Potential deadlock detected - test timed out") } }() wg.Add(numGoroutines) for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() for j := range 100 { spawnID := int32((goroutineID % 10) + 1) // Mix operations that could potentially deadlock switch j % 8 { case 0: gs := manager.GetGroundSpawn(spawnID) if gs != nil { _ = gs.GetNumberHarvests() } case 1: _ = manager.GetStatistics() case 2: _ = manager.GetGroundSpawnsByZone(1) case 3: gs := manager.GetGroundSpawn(spawnID) if gs != nil { gs.SetNumberHarvests(int8(j % 5)) } case 4: manager.ProcessRespawns() case 5: _ = manager.GetActiveGroundSpawns() case 6: gs := manager.GetGroundSpawn(spawnID) if gs != nil { _ = gs.Copy() } case 7: gs := manager.GetGroundSpawn(spawnID) if gs != nil && gs.IsDepleted() { manager.scheduleRespawn(gs) } } } }(i) } wg.Wait() done <- true }) } // Race condition detection test - run with -race flag func TestRaceConditions(t *testing.T) { if testing.Short() { t.Skip("Skipping race condition test in short mode") } manager := NewManager(nil, &mockLogger{}) // Rapid concurrent operations to trigger race conditions const numGoroutines = 200 const operationsPerGoroutine = 50 var wg sync.WaitGroup wg.Add(numGoroutines) for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() for j := range operationsPerGoroutine { // Use unique IDs to avoid conflicts in rapid creation uniqueID := int32(20000 + goroutineID*1000 + j) // Rapid-fire operations config := GroundSpawnConfig{ GroundSpawnID: uniqueID, CollectionSkill: SkillGathering, NumberHarvests: 3, AttemptsPerHarvest: 1, Location: SpawnLocation{ X: float32(j), Y: float32(j * 2), Z: float32(j * 3), Heading: 0, GridID: 1, }, Name: "Race Test Node", Description: "Race test", } gs := manager.CreateGroundSpawn(config) if gs != nil { actualID := gs.GetID() // Get the manager-assigned ID gs.SetNumberHarvests(int8(j%5 + 1)) _ = gs.GetNumberHarvests() _ = gs.IsAvailable() copy := gs.Copy() if copy != nil { copy.SetCollectionSkill(SkillMining) } _ = manager.GetGroundSpawn(actualID) } _ = manager.GetStatistics() manager.ProcessRespawns() } }(i) } wg.Wait() } // Specific test for Copy() method mutex safety func TestCopyMutexSafety(t *testing.T) { config := GroundSpawnConfig{ GroundSpawnID: 1, CollectionSkill: SkillGathering, NumberHarvests: 5, AttemptsPerHarvest: 1, Location: SpawnLocation{ X: 100, Y: 200, Z: 300, Heading: 45, GridID: 1, }, Name: "Copy Test Node", Description: "Test node for copy safety", } original := NewGroundSpawn(config) const numGoroutines = 100 var wg sync.WaitGroup wg.Add(numGoroutines) // Test copying while modifying for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() for j := range 100 { if j%2 == 0 { // Copy operations copy := original.Copy() if copy == nil { t.Errorf("Goroutine %d: Copy returned nil", goroutineID) continue } // Verify copy is independent by setting a unique value expectedValue := int8(goroutineID%5 + 1) // Ensure non-zero value copy.SetNumberHarvests(expectedValue) // Verify the copy has the value we set if copy.GetNumberHarvests() != expectedValue { t.Errorf("Goroutine %d: Copy failed to set value correctly, expected %d got %d", goroutineID, expectedValue, copy.GetNumberHarvests()) } // Copy independence is verified by the fact that we can set different values // We don't check against original since other goroutines are modifying it concurrently } else { // Modify original original.SetNumberHarvests(int8(goroutineID % 10)) original.SetCollectionSkill(SkillMining) _ = original.GetRandomizeHeading() } } }(i) } wg.Wait() }