eq2go/internal/ground_spawn/concurrency_test.go

524 lines
12 KiB
Go

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