package ground_spawn import ( "sync" "testing" "time" ) // Mock implementations are in test_utils.go // Test core GroundSpawn concurrency patterns without dependencies func TestGroundSpawnCoreConcurrency(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("ConcurrentAccessors", func(t *testing.T) { wg.Add(numGoroutines) for i := 0; i < numGoroutines; i++ { go func(goroutineID int) { defer wg.Done() for j := 0; j < operationsPerGoroutine; j++ { 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 := 0; i < numGoroutines; i++ { go func(goroutineID int) { defer wg.Done() for j := 0; j < operationsPerGoroutine; j++ { switch j % 4 { case 0: _ = gs.IsDepleted() case 1: _ = gs.IsAvailable() case 2: _ = gs.GetHarvestMessageName(true, false) case 3: _ = gs.GetHarvestSpellType() } } }(i) } wg.Wait() }) } // Test Manager core concurrency patterns func TestManagerCoreConcurrency(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 = 50 const operationsPerGoroutine = 50 var wg sync.WaitGroup t.Run("ConcurrentAccess", func(t *testing.T) { wg.Add(numGoroutines) for i := 0; i < numGoroutines; i++ { go func(goroutineID int) { defer wg.Done() for j := 0; j < operationsPerGoroutine; j++ { 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("ConcurrentStatistics", func(t *testing.T) { wg.Add(numGoroutines) for i := 0; i < numGoroutines; i++ { go func(goroutineID int) { defer wg.Done() for j := 0; j < operationsPerGoroutine; j++ { switch j % 3 { case 0: _ = manager.GetStatistics() case 1: manager.ResetStatistics() case 2: // Simulate 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) } }) } // Test Copy() method thread safety func TestCopyThreadSafety(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 = 50 var wg sync.WaitGroup wg.Add(numGoroutines) // Test copying while modifying for i := 0; i < numGoroutines; i++ { go func(goroutineID int) { defer wg.Done() for j := 0; j < 100; j++ { 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 different values newValue := int8(goroutineID%5 + 1) // Ensure non-zero value copy.SetNumberHarvests(newValue) // Copy should have the new value we just set if copy.GetNumberHarvests() != newValue { t.Errorf("Goroutine %d: Copy failed to set value correctly, expected %d got %d", goroutineID, newValue, copy.GetNumberHarvests()) } // Note: We can't reliably test that original is unchanged due to concurrent modifications } else { // Modify original original.SetNumberHarvests(int8(goroutineID % 10)) original.SetCollectionSkill(SkillMining) _ = original.GetRandomizeHeading() } } }(i) } wg.Wait() } // Test core deadlock prevention func TestCoreDeadlockPrevention(t *testing.T) { manager := NewManager(nil, &mockLogger{}) // Create test ground spawns for i := int32(1); i <= 5; 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 = 25 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(5 * time.Second) select { case <-done: return default: t.Error("Potential deadlock detected - test timed out") } }() wg.Add(numGoroutines) for i := 0; i < numGoroutines; i++ { go func(goroutineID int) { defer wg.Done() for j := 0; j < 50; j++ { spawnID := int32((goroutineID % 5) + 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 }) }