524 lines
12 KiB
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()
|
|
}
|