package groups import ( "fmt" "sync" "testing" "time" ) // Mock entity implementation for testing type mockEntity struct { id int32 name string level int8 class int8 race int8 hp int32 maxHP int32 power int32 maxPower int32 isPlayer bool isNPC bool isBot bool isDead bool zone *mockZone groupID int32 groupInfo *GroupMemberInfo } func (m *mockEntity) GetID() int32 { return m.id } func (m *mockEntity) GetName() string { return m.name } func (m *mockEntity) GetLevel() int8 { return m.level } func (m *mockEntity) GetClass() int8 { return m.class } func (m *mockEntity) GetRace() int8 { return m.race } func (m *mockEntity) GetHP() int32 { return m.hp } func (m *mockEntity) GetTotalHP() int32 { return m.maxHP } func (m *mockEntity) GetPower() int32 { return m.power } func (m *mockEntity) GetTotalPower() int32 { return m.maxPower } func (m *mockEntity) IsPlayer() bool { return m.isPlayer } func (m *mockEntity) IsNPC() bool { return m.isNPC } func (m *mockEntity) IsBot() bool { return m.isBot } func (m *mockEntity) IsDead() bool { return m.isDead } func (m *mockEntity) GetZone() Zone { return m.zone } func (m *mockEntity) GetDistance(other Entity) float32 { return 10.0 } // GroupAware implementation func (m *mockEntity) GetGroupMemberInfo() *GroupMemberInfo { return m.groupInfo } func (m *mockEntity) SetGroupMemberInfo(info *GroupMemberInfo) { m.groupInfo = info } func (m *mockEntity) GetGroupID() int32 { return m.groupID } func (m *mockEntity) SetGroupID(groupID int32) { m.groupID = groupID } func (m *mockEntity) IsInGroup() bool { return m.groupID > 0 } // Mock zone implementation type mockZone struct { zoneID int32 instanceID int32 zoneName string } func (m *mockZone) GetZoneID() int32 { return m.zoneID } func (m *mockZone) GetInstanceID() int32 { return m.instanceID } func (m *mockZone) GetZoneName() string { return m.zoneName } // Helper function to create mock entities func createMockEntity(id int32, name string, isPlayer bool) *mockEntity { return &mockEntity{ id: id, name: name, level: 50, class: 1, race: 0, hp: 1500, maxHP: 1500, power: 800, maxPower: 800, isPlayer: isPlayer, zone: &mockZone{ zoneID: 220, instanceID: 1, zoneName: "commonlands", }, } } // TestGroupCreation tests basic group creation func TestGroupCreation(t *testing.T) { tests := []struct { name string groupID int32 options *GroupOptions expectNil bool }{ { name: "Create group with default options", groupID: 1, options: nil, expectNil: false, }, { name: "Create group with custom options", groupID: 2, options: &GroupOptions{ LootMethod: LOOT_METHOD_NEED_BEFORE_GREED, LootItemsRarity: LOOT_RARITY_RARE, AutoSplit: AUTO_SPLIT_ENABLED, }, expectNil: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { group := NewGroup(tt.groupID, tt.options, nil) if (group == nil) != tt.expectNil { t.Errorf("NewGroup() returned nil = %v, want %v", group == nil, tt.expectNil) return } if group.GetID() != tt.groupID { t.Errorf("Group ID = %d, want %d", group.GetID(), tt.groupID) } if group.GetSize() != 0 { t.Errorf("Initial group size = %d, want 0", group.GetSize()) } // Cleanup group.Disband() }) } } // TestGroupMemberManagement tests adding and removing members func TestGroupMemberManagement(t *testing.T) { group := NewGroup(1, nil, nil) defer group.Disband() leader := createMockEntity(1, "Leader", true) member1 := createMockEntity(2, "Member1", true) member2 := createMockEntity(3, "Member2", true) // Test adding leader err := group.AddMember(leader, true) if err != nil { t.Fatalf("Failed to add leader: %v", err) } if group.GetSize() != 1 { t.Errorf("Group size after adding leader = %d, want 1", group.GetSize()) } // Test adding members err = group.AddMember(member1, false) if err != nil { t.Fatalf("Failed to add member1: %v", err) } err = group.AddMember(member2, false) if err != nil { t.Fatalf("Failed to add member2: %v", err) } if group.GetSize() != 3 { t.Errorf("Group size after adding members = %d, want 3", group.GetSize()) } // Test duplicate member err = group.AddMember(member1, false) if err == nil { t.Error("Expected error when adding duplicate member") } // Test member removal err = group.RemoveMember(member1) if err != nil { t.Fatalf("Failed to remove member1: %v", err) } if group.GetSize() != 2 { t.Errorf("Group size after removing member = %d, want 2", group.GetSize()) } // Test removing non-existent member err = group.RemoveMember(member1) if err == nil { t.Error("Expected error when removing non-existent member") } } // TestGroupLeadership tests leadership transfer func TestGroupLeadership(t *testing.T) { group := NewGroup(1, nil, nil) defer group.Disband() leader := createMockEntity(1, "Leader", true) member1 := createMockEntity(2, "Member1", true) member2 := createMockEntity(3, "Member2", true) // Add members group.AddMember(leader, true) group.AddMember(member1, false) group.AddMember(member2, false) // Test initial leader if group.GetLeaderName() != "Leader" { t.Errorf("Initial leader name = %s, want Leader", group.GetLeaderName()) } // Transfer leadership err := group.MakeLeader(member1) if err != nil { t.Errorf("Failed to transfer leadership: %v", err) } if group.GetLeaderName() != "Member1" { t.Errorf("New leader name = %s, want Member1", group.GetLeaderName()) } // Test invalid leadership transfer nonMember := createMockEntity(4, "NonMember", true) err = group.MakeLeader(nonMember) if err == nil { t.Error("Expected failure when making non-member leader") } } // TestGroupOptions tests group options management func TestGroupOptions(t *testing.T) { group := NewGroup(1, nil, nil) defer group.Disband() // Test default options options := group.GetGroupOptions() if options.LootMethod != LOOT_METHOD_ROUND_ROBIN { t.Errorf("Default loot method = %d, want %d", options.LootMethod, LOOT_METHOD_ROUND_ROBIN) } // Test setting options newOptions := GroupOptions{ LootMethod: LOOT_METHOD_NEED_BEFORE_GREED, LootItemsRarity: LOOT_RARITY_RARE, AutoSplit: AUTO_SPLIT_ENABLED, GroupLockMethod: LOCK_METHOD_INVITE_ONLY, } group.SetGroupOptions(&newOptions) options = group.GetGroupOptions() if options.LootMethod != LOOT_METHOD_NEED_BEFORE_GREED { t.Errorf("Updated loot method = %d, want %d", options.LootMethod, LOOT_METHOD_NEED_BEFORE_GREED) } if options.AutoSplit != AUTO_SPLIT_ENABLED { t.Errorf("Updated auto split = %d, want %d", options.AutoSplit, AUTO_SPLIT_ENABLED) } } // TestGroupRaidFunctionality tests raid-related functionality func TestGroupRaidFunctionality(t *testing.T) { group := NewGroup(1, nil, nil) defer group.Disband() // Initially not a raid if group.IsGroupRaid() { t.Error("New group should not be a raid") } // Add raid groups raidGroups := []int32{1, 2, 3, 4} group.ReplaceRaidGroups(raidGroups) if !group.IsGroupRaid() { t.Error("Group should be a raid after setting raid groups") } // Test raid group retrieval retrievedGroups := group.GetRaidGroups() if len(retrievedGroups) != len(raidGroups) { t.Errorf("Retrieved raid groups length = %d, want %d", len(retrievedGroups), len(raidGroups)) } // Clear raid group.ClearGroupRaid() if group.IsGroupRaid() { t.Error("Group should not be a raid after clearing") } } // TestGroupConcurrency tests concurrent access to group operations func TestGroupConcurrency(t *testing.T) { group := NewGroup(1, nil, nil) defer group.Disband() const numGoroutines = 100 const operationsPerGoroutine = 100 var wg sync.WaitGroup // Test concurrent member additions and removals t.Run("ConcurrentMemberOperations", func(t *testing.T) { // Add initial members for i := 0; i < MAX_GROUP_SIZE-1; i++ { member := createMockEntity(int32(i+1), fmt.Sprintf("Member%d", i+1), true) group.AddMember(member, i == 0) } wg.Add(numGoroutines) for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() member := createMockEntity(int32(100+goroutineID), fmt.Sprintf("Temp%d", goroutineID), true) for j := range operationsPerGoroutine { if j%2 == 0 { // Try to add member (will mostly fail due to full group) _ = group.AddMember(member, false) } else { // Remove and re-add existing member members := group.GetMembers() if len(members) > 0 { memberIdx := goroutineID % len(members) existingMember := members[memberIdx] _ = group.RemoveMember(existingMember.Member) _ = group.AddMember(existingMember.Member, existingMember.Leader) } } } }(i) } wg.Wait() }) // Test concurrent option updates t.Run("ConcurrentOptionUpdates", func(t *testing.T) { wg.Add(numGoroutines) for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() for j := range operationsPerGoroutine { if j%2 == 0 { // Read options _ = group.GetGroupOptions() } else { // Write options options := GroupOptions{ LootMethod: int8(goroutineID % 4), LootItemsRarity: int8(goroutineID % 5), AutoSplit: int8(goroutineID % 2), } group.SetGroupOptions(&options) } } }(i) } wg.Wait() }) // Test concurrent raid operations t.Run("ConcurrentRaidOperations", 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: _ = group.IsGroupRaid() case 1: _ = group.GetRaidGroups() case 2: raidGroups := []int32{int32(goroutineID%4 + 1)} group.ReplaceRaidGroups(raidGroups) case 3: group.ClearGroupRaid() } } }(i) } wg.Wait() }) // Test concurrent member info updates t.Run("ConcurrentMemberInfoUpdates", func(t *testing.T) { wg.Add(numGoroutines) for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() for range operationsPerGoroutine { members := group.GetMembers() if len(members) > 0 { // Update member stats memberIdx := goroutineID % len(members) members[memberIdx].UpdateStats() } } }(i) } wg.Wait() }) } // TestGroupManagerCreation tests group manager creation func TestGroupManagerCreation(t *testing.T) { config := GroupManagerConfig{ MaxGroups: 1000, MaxRaidGroups: 4, InviteTimeout: 30 * time.Second, UpdateInterval: 1 * time.Second, BuffUpdateInterval: 5 * time.Second, EnableCrossServer: true, EnableRaids: true, EnableQuestSharing: true, EnableStatistics: true, } manager := NewManager(config, nil) defer manager.Stop() if manager == nil { t.Fatal("NewGroupManager returned nil") } stats := manager.GetStats() if stats.ActiveGroups != 0 { t.Errorf("Initial active groups = %d, want 0", stats.ActiveGroups) } } // TestGroupManagerGroupOperations tests group operations through manager func TestGroupManagerGroupOperations(t *testing.T) { config := GroupManagerConfig{ MaxGroups: 1000, MaxRaidGroups: 4, InviteTimeout: 30 * time.Second, UpdateInterval: 0, // Disable background updates for testing BuffUpdateInterval: 0, // Disable background updates for testing EnableCrossServer: true, EnableRaids: true, EnableQuestSharing: true, EnableStatistics: false, // Disable statistics for testing } manager := NewManager(config, nil) defer manager.Stop() leader := createMockEntity(1, "Leader", true) member1 := createMockEntity(2, "Member1", true) member2 := createMockEntity(3, "Member2", true) // Create group groupID, err := manager.NewGroup(leader, nil, 0) if err != nil { t.Fatalf("Failed to create group: %v", err) } if groupID <= 0 { t.Errorf("Invalid group ID: %d", groupID) } // Add members err = manager.AddGroupMember(groupID, member1, false) if err != nil { t.Fatalf("Failed to add member1: %v", err) } err = manager.AddGroupMember(groupID, member2, false) if err != nil { t.Fatalf("Failed to add member2: %v", err) } // Check group size size := manager.GetGroupSize(groupID) if size != 3 { t.Errorf("Group size = %d, want 3", size) } // Test member checks if !manager.IsInGroup(groupID, leader) { t.Error("Leader should be in group") } if !manager.IsInGroup(groupID, member1) { t.Error("Member1 should be in group") } // Remove member err = manager.RemoveGroupMember(groupID, member1) if err != nil { t.Fatalf("Failed to remove member1: %v", err) } if manager.IsInGroup(groupID, member1) { t.Error("Member1 should not be in group after removal") } // Remove group err = manager.RemoveGroup(groupID) if err != nil { t.Fatalf("Failed to remove group: %v", err) } if manager.IsGroupIDValid(groupID) { t.Error("Group should not be valid after removal") } } // TestGroupManagerInvitations tests invitation system func TestGroupManagerInvitations(t *testing.T) { config := GroupManagerConfig{ MaxGroups: 1000, MaxRaidGroups: 4, InviteTimeout: 200 * time.Millisecond, // Very short timeout for testing UpdateInterval: 0, // Disable background updates for testing BuffUpdateInterval: 0, // Disable background updates for testing EnableCrossServer: true, EnableRaids: true, EnableQuestSharing: true, EnableStatistics: false, // Disable statistics for testing } manager := NewManager(config, nil) defer manager.Stop() leader := createMockEntity(1, "Leader", true) member := createMockEntity(2, "Member", true) // Create group groupID, _ := manager.NewGroup(leader, nil, 0) // Send invitation result := manager.Invite(leader, member) if result != GROUP_INVITE_SUCCESS { t.Errorf("Invite result = %d, want %d", result, GROUP_INVITE_SUCCESS) } // Check pending invite inviterName := manager.HasPendingInvite(member) if inviterName != "Leader" { t.Errorf("Pending invite from = %s, want Leader", inviterName) } // Accept invitation (will fail due to missing leader lookup, but that's expected in tests) acceptResult := manager.AcceptInvite(member, nil, true) if acceptResult != GROUP_INVITE_TARGET_NOT_FOUND { t.Logf("Accept invite result = %d (expected due to missing leader lookup in test)", acceptResult) } // Since invite acceptance failed due to missing world integration, // let's manually add the member to test the group functionality err := manager.AddGroupMember(groupID, member, false) if err != nil { t.Fatalf("Failed to manually add member: %v", err) } // Verify member is in group if !manager.IsInGroup(groupID, member) { t.Error("Member should be in group after adding") } // Test invitation timeout member2 := createMockEntity(3, "Member2", true) manager.Invite(leader, member2) // Wait for timeout (now timeout is 200ms so wait 250ms) time.Sleep(250 * time.Millisecond) // Try to accept after timeout (will fail due to missing leader lookup, // but we're mainly testing that the invite was cleaned up) acceptResult = manager.AcceptInvite(member2, nil, true) if acceptResult == GROUP_INVITE_SUCCESS { t.Error("Should not be able to accept expired invitation") } // Verify the invite was cleaned up by checking it no longer exists if manager.HasPendingInvite(member2) != "" { t.Error("Expired invitation should have been cleaned up") } } // TestGroupManagerConcurrency tests concurrent manager operations func TestGroupManagerConcurrency(t *testing.T) { config := GroupManagerConfig{ MaxGroups: 1000, MaxRaidGroups: 4, InviteTimeout: 30 * time.Second, UpdateInterval: 0, // Disable background updates for testing BuffUpdateInterval: 0, // Disable background updates for testing EnableCrossServer: true, EnableRaids: true, EnableQuestSharing: true, EnableStatistics: false, // Disable statistics for testing } manager := NewManager(config, nil) defer manager.Stop() const numGoroutines = 50 const groupsPerGoroutine = 10 var wg sync.WaitGroup // Test concurrent group creation and removal t.Run("ConcurrentGroupCreation", func(t *testing.T) { wg.Add(numGoroutines) for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() for j := range groupsPerGoroutine { leader := createMockEntity(int32(goroutineID*1000+j), fmt.Sprintf("Leader%d_%d", goroutineID, j), true) // Create group groupID, err := manager.NewGroup(leader, nil, 0) if err != nil { continue } // Add some members for k := range 3 { member := createMockEntity(int32(goroutineID*1000+j*10+k), fmt.Sprintf("Member%d_%d_%d", goroutineID, j, k), true) _ = manager.AddGroupMember(groupID, member, false) } // Sometimes remove the group if j%2 == 0 { _ = manager.RemoveGroup(groupID) } } }(i) } wg.Wait() }) // Test concurrent invitations t.Run("ConcurrentInvitations", func(t *testing.T) { // Create some groups groups := make([]int32, 10) leaders := make([]*mockEntity, 10) for i := range 10 { leader := createMockEntity(int32(10000+i), fmt.Sprintf("InviteLeader%d", i), true) leaders[i] = leader groupID, _ := manager.NewGroup(leader, nil, 0) groups[i] = groupID } wg.Add(numGoroutines) for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() for j := range 100 { leaderIdx := goroutineID % len(leaders) leader := leaders[leaderIdx] member := createMockEntity(int32(20000+goroutineID*100+j), fmt.Sprintf("InviteMember%d_%d", goroutineID, j), true) // Send invite _ = manager.Invite(leader, member) // Sometimes accept, sometimes decline if j%3 == 0 { _ = manager.AcceptInvite(member, nil, false) } else if j%3 == 1 { manager.DeclineInvite(member) } // Otherwise let it expire } }(i) } wg.Wait() // Cleanup groups for _, groupID := range groups { _ = manager.RemoveGroup(groupID) } }) // Test concurrent statistics updates t.Run("ConcurrentStatistics", func(t *testing.T) { wg.Add(numGoroutines) for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() for range 1000 { _ = manager.GetStats() _ = manager.GetGroupCount() _ = manager.GetAllGroups() } }(i) } wg.Wait() }) } // TestRaceConditions tests for race conditions with -race flag func TestRaceConditions(t *testing.T) { if testing.Short() { t.Skip("Skipping race condition test in short mode") } config := GroupManagerConfig{ MaxGroups: 1000, MaxRaidGroups: 4, InviteTimeout: 30 * time.Second, UpdateInterval: 0, // Disable background updates for testing BuffUpdateInterval: 0, // Disable background updates for testing EnableCrossServer: true, EnableRaids: true, EnableQuestSharing: true, EnableStatistics: false, // Disable statistics for testing } manager := NewManager(config, nil) defer manager.Stop() const numGoroutines = 100 var wg sync.WaitGroup // Create a shared group leader := createMockEntity(1, "RaceLeader", true) groupID, _ := manager.NewGroup(leader, nil, 0) // Add some initial members for i := range 5 { member := createMockEntity(int32(i+2), fmt.Sprintf("RaceMember%d", i+1), true) _ = manager.AddGroupMember(groupID, member, false) } wg.Add(numGoroutines) for i := range numGoroutines { go func(goroutineID int) { defer wg.Done() for j := range 50 { switch j % 10 { case 0: // Get group _ = manager.GetGroup(groupID) case 1: // Get size _ = manager.GetGroupSize(groupID) case 2: // Check membership _ = manager.IsInGroup(groupID, leader) case 3: // Get leader _ = manager.GetGroupLeader(groupID) case 4: // Send message manager.SimpleGroupMessage(groupID, fmt.Sprintf("Message %d", goroutineID)) case 5: // Update options options := DefaultGroupOptions() options.LootMethod = int8(goroutineID % 4) _ = manager.SetGroupOptions(groupID, &options) case 6: // Get options _, _ = manager.GetDefaultGroupOptions(groupID) case 7: // Send group update manager.SendGroupUpdate(groupID, nil, false) case 8: // Check raid status _ = manager.IsInRaidGroup(groupID, groupID+1, false) case 9: // Get stats _ = manager.GetStats() } } }(i) } wg.Wait() } // Benchmark tests func BenchmarkGroupOperations(b *testing.B) { b.Run("GroupCreation", func(b *testing.B) { for i := 0; i < b.N; i++ { group := NewGroup(int32(i), nil, nil) group.Disband() } }) b.Run("MemberAddition", func(b *testing.B) { group := NewGroup(1, nil, nil) defer group.Disband() b.ResetTimer() for i := 0; i < b.N; i++ { member := createMockEntity(int32(i), fmt.Sprintf("Member%d", i), true) _ = group.AddMember(member, false) _ = group.RemoveMember(member) } }) b.Run("ConcurrentMemberAccess", func(b *testing.B) { group := NewGroup(1, nil, nil) defer group.Disband() // Add some members for i := range MAX_GROUP_SIZE { member := createMockEntity(int32(i+1), fmt.Sprintf("Member%d", i+1), true) group.AddMember(member, i == 0) } b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { _ = group.GetMembers() } }) }) b.Run("ManagerGroupLookup", func(b *testing.B) { config := GroupManagerConfig{ MaxGroups: 1000, MaxRaidGroups: 4, InviteTimeout: 30 * time.Second, UpdateInterval: 0, // Disable background updates for testing BuffUpdateInterval: 0, // Disable background updates for testing EnableCrossServer: true, EnableRaids: true, EnableQuestSharing: true, EnableStatistics: false, // Disable statistics for testing } manager := NewManager(config, nil) defer manager.Stop() // Create some groups for i := range 100 { leader := createMockEntity(int32(i+1), fmt.Sprintf("Leader%d", i+1), true) manager.NewGroup(leader, nil, 0) } b.ResetTimer() b.RunParallel(func(pb *testing.PB) { i := 0 for pb.Next() { groupID := int32((i % 100) + 1) _ = manager.GetGroup(groupID) i++ } }) }) }