test: Add comprehensive tests for HandleHostRemoved, ReevaluateGuestAlert
HandleHostRemoved (0%→100%): - Empty host ID is no-op - Clears host offline alert and confirmations - Clears host metric alerts (CPU, memory) - Clears host disk alerts - Clears all alert types together ReevaluateGuestAlert (0%→100%): - No active alerts is no-op - Clears alert when threshold disabled (nil) - Clears alert when trigger is zero - Clears alert when value below clear threshold - Clears alert when value below trigger threshold - Keeps alert when value above both thresholds - Processes all metric types (7 types) - Clears pending alert when threshold disabled - Uses clear equals trigger when clear is zero - Ignores alerts for different guests NormalizeMetricTimeThresholds (0%→100%): - Updated existing test to call public wrapper instead of internal function Coverage: 54.6% → 55.6%
This commit is contained in:
parent
65e5eb84ae
commit
7bac96d60b
2 changed files with 404 additions and 1 deletions
|
|
@ -5075,3 +5075,406 @@ func TestReevaluateActiveAlertsLocked(t *testing.T) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandleHostRemoved(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("empty host ID is no-op", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := NewManager()
|
||||||
|
m.mu.Lock()
|
||||||
|
m.activeAlerts["host-offline-host1"] = &Alert{ID: "host-offline-host1"}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
// Empty ID host
|
||||||
|
m.HandleHostRemoved(models.Host{ID: ""})
|
||||||
|
|
||||||
|
// Alert should still exist
|
||||||
|
m.mu.RLock()
|
||||||
|
_, exists := m.activeAlerts["host-offline-host1"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
if !exists {
|
||||||
|
t.Error("expected alert to remain when empty host ID passed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("clears host offline alert", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := NewManager()
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.Enabled = true
|
||||||
|
m.activeAlerts["host-offline-host1"] = &Alert{
|
||||||
|
ID: "host-offline-host1",
|
||||||
|
ResourceID: "host:host1",
|
||||||
|
}
|
||||||
|
m.offlineConfirmations["host:host1"] = 5
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
m.HandleHostRemoved(models.Host{ID: "host1", Hostname: "testhost"})
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, alertExists := m.activeAlerts["host-offline-host1"]
|
||||||
|
_, confirmExists := m.offlineConfirmations["host:host1"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if alertExists {
|
||||||
|
t.Error("expected host offline alert to be cleared")
|
||||||
|
}
|
||||||
|
if confirmExists {
|
||||||
|
t.Error("expected offline confirmations to be cleared")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("clears host metric alerts", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := NewManager()
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.Enabled = true
|
||||||
|
// Add CPU and memory alerts for host
|
||||||
|
m.activeAlerts["host:host1-cpu"] = &Alert{
|
||||||
|
ID: "host:host1-cpu",
|
||||||
|
ResourceID: "host:host1",
|
||||||
|
}
|
||||||
|
m.activeAlerts["host:host1-memory"] = &Alert{
|
||||||
|
ID: "host:host1-memory",
|
||||||
|
ResourceID: "host:host1",
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
m.HandleHostRemoved(models.Host{ID: "host1", Hostname: "testhost"})
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, cpuExists := m.activeAlerts["host:host1-cpu"]
|
||||||
|
_, memExists := m.activeAlerts["host:host1-memory"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if cpuExists {
|
||||||
|
t.Error("expected host CPU alert to be cleared")
|
||||||
|
}
|
||||||
|
if memExists {
|
||||||
|
t.Error("expected host memory alert to be cleared")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("clears host disk alerts", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := NewManager()
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.Enabled = true
|
||||||
|
// Add disk alerts for host
|
||||||
|
m.activeAlerts["host:host1/disk:sda-usage"] = &Alert{
|
||||||
|
ID: "host:host1/disk:sda-usage",
|
||||||
|
ResourceID: "host:host1/disk:sda",
|
||||||
|
}
|
||||||
|
m.activeAlerts["host:host1/disk:sdb-usage"] = &Alert{
|
||||||
|
ID: "host:host1/disk:sdb-usage",
|
||||||
|
ResourceID: "host:host1/disk:sdb",
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
m.HandleHostRemoved(models.Host{ID: "host1", Hostname: "testhost"})
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, sda := m.activeAlerts["host:host1/disk:sda-usage"]
|
||||||
|
_, sdb := m.activeAlerts["host:host1/disk:sdb-usage"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if sda {
|
||||||
|
t.Error("expected host disk sda alert to be cleared")
|
||||||
|
}
|
||||||
|
if sdb {
|
||||||
|
t.Error("expected host disk sdb alert to be cleared")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("clears all alert types together", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := NewManager()
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.Enabled = true
|
||||||
|
// Add multiple alert types
|
||||||
|
m.activeAlerts["host-offline-host1"] = &Alert{ID: "host-offline-host1", ResourceID: "host:host1"}
|
||||||
|
m.activeAlerts["host:host1-cpu"] = &Alert{ID: "host:host1-cpu", ResourceID: "host:host1"}
|
||||||
|
m.activeAlerts["host:host1-memory"] = &Alert{ID: "host:host1-memory", ResourceID: "host:host1"}
|
||||||
|
m.activeAlerts["host:host1/disk:sda-usage"] = &Alert{ID: "host:host1/disk:sda-usage", ResourceID: "host:host1/disk:sda"}
|
||||||
|
m.offlineConfirmations["host:host1"] = 3
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
m.HandleHostRemoved(models.Host{ID: "host1", Hostname: "testhost"})
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
alertCount := 0
|
||||||
|
for id := range m.activeAlerts {
|
||||||
|
if strings.Contains(id, "host1") {
|
||||||
|
alertCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, confirmExists := m.offlineConfirmations["host:host1"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if alertCount > 0 {
|
||||||
|
t.Errorf("expected all host1 alerts to be cleared, got %d remaining", alertCount)
|
||||||
|
}
|
||||||
|
if confirmExists {
|
||||||
|
t.Error("expected offline confirmations to be cleared")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReevaluateGuestAlert(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("no active alerts is no-op", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := NewManager()
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.Enabled = true
|
||||||
|
m.config.GuestDefaults.CPU = &HysteresisThreshold{Trigger: 80, Clear: 70}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
// No alerts exist - should not panic
|
||||||
|
m.ReevaluateGuestAlert(nil, "guest1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
count := len(m.activeAlerts)
|
||||||
|
m.mu.RUnlock()
|
||||||
|
if count != 0 {
|
||||||
|
t.Errorf("expected 0 alerts, got %d", count)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("clears alert when threshold disabled (nil)", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := NewManager()
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.Enabled = true
|
||||||
|
m.activeAlerts["guest1-cpu"] = &Alert{
|
||||||
|
ID: "guest1-cpu",
|
||||||
|
Type: "cpu",
|
||||||
|
Value: 90,
|
||||||
|
}
|
||||||
|
m.config.GuestDefaults.CPU = nil // Disabled
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
m.ReevaluateGuestAlert(nil, "guest1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, exists := m.activeAlerts["guest1-cpu"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
if exists {
|
||||||
|
t.Error("expected alert to be cleared when threshold is nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("clears alert when trigger is zero", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := NewManager()
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.Enabled = true
|
||||||
|
m.activeAlerts["guest1-memory"] = &Alert{
|
||||||
|
ID: "guest1-memory",
|
||||||
|
Type: "memory",
|
||||||
|
Value: 85,
|
||||||
|
}
|
||||||
|
m.config.GuestDefaults.Memory = &HysteresisThreshold{Trigger: 0, Clear: 0}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
m.ReevaluateGuestAlert(nil, "guest1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, exists := m.activeAlerts["guest1-memory"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
if exists {
|
||||||
|
t.Error("expected alert to be cleared when trigger is 0")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("clears alert when value below clear threshold", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := NewManager()
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.Enabled = true
|
||||||
|
m.activeAlerts["guest1-cpu"] = &Alert{
|
||||||
|
ID: "guest1-cpu",
|
||||||
|
Type: "cpu",
|
||||||
|
Value: 65, // Below clear threshold of 70
|
||||||
|
}
|
||||||
|
m.config.GuestDefaults.CPU = &HysteresisThreshold{Trigger: 80, Clear: 70}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
m.ReevaluateGuestAlert(nil, "guest1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, exists := m.activeAlerts["guest1-cpu"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
if exists {
|
||||||
|
t.Error("expected alert to be cleared when value below clear threshold")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("clears alert when value below trigger threshold", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := NewManager()
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.Enabled = true
|
||||||
|
m.activeAlerts["guest1-disk"] = &Alert{
|
||||||
|
ID: "guest1-disk",
|
||||||
|
Type: "disk",
|
||||||
|
Value: 75, // Below trigger of 80
|
||||||
|
}
|
||||||
|
m.config.GuestDefaults.Disk = &HysteresisThreshold{Trigger: 80, Clear: 70}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
m.ReevaluateGuestAlert(nil, "guest1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, exists := m.activeAlerts["guest1-disk"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
if exists {
|
||||||
|
t.Error("expected alert to be cleared when value below trigger")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("keeps alert when value above both thresholds", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := NewManager()
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.Enabled = true
|
||||||
|
m.activeAlerts["guest1-cpu"] = &Alert{
|
||||||
|
ID: "guest1-cpu",
|
||||||
|
Type: "cpu",
|
||||||
|
Value: 90, // Above both trigger (80) and clear (70)
|
||||||
|
}
|
||||||
|
m.config.GuestDefaults.CPU = &HysteresisThreshold{Trigger: 80, Clear: 70}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
m.ReevaluateGuestAlert(nil, "guest1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, exists := m.activeAlerts["guest1-cpu"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
if !exists {
|
||||||
|
t.Error("expected alert to remain when value above thresholds")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("processes all metric types", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := NewManager()
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.Enabled = true
|
||||||
|
// Add alerts for all metric types with values below threshold
|
||||||
|
metrics := []string{"cpu", "memory", "disk", "diskRead", "diskWrite", "networkIn", "networkOut"}
|
||||||
|
for _, metric := range metrics {
|
||||||
|
m.activeAlerts[fmt.Sprintf("guest1-%s", metric)] = &Alert{
|
||||||
|
ID: fmt.Sprintf("guest1-%s", metric),
|
||||||
|
Type: metric,
|
||||||
|
Value: 50, // Below threshold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
threshold := &HysteresisThreshold{Trigger: 80, Clear: 70}
|
||||||
|
m.config.GuestDefaults.CPU = threshold
|
||||||
|
m.config.GuestDefaults.Memory = threshold
|
||||||
|
m.config.GuestDefaults.Disk = threshold
|
||||||
|
m.config.GuestDefaults.DiskRead = threshold
|
||||||
|
m.config.GuestDefaults.DiskWrite = threshold
|
||||||
|
m.config.GuestDefaults.NetworkIn = threshold
|
||||||
|
m.config.GuestDefaults.NetworkOut = threshold
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
m.ReevaluateGuestAlert(nil, "guest1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
remaining := len(m.activeAlerts)
|
||||||
|
m.mu.RUnlock()
|
||||||
|
if remaining != 0 {
|
||||||
|
t.Errorf("expected all alerts to be cleared, got %d remaining", remaining)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("clears pending alert when threshold disabled", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := NewManager()
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.Enabled = true
|
||||||
|
m.activeAlerts["guest1-cpu"] = &Alert{
|
||||||
|
ID: "guest1-cpu",
|
||||||
|
Type: "cpu",
|
||||||
|
Value: 90,
|
||||||
|
}
|
||||||
|
m.pendingAlerts["guest1-cpu"] = time.Now() // pendingAlerts is map[string]time.Time
|
||||||
|
m.config.GuestDefaults.CPU = nil // Disabled
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
m.ReevaluateGuestAlert(nil, "guest1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, alertExists := m.activeAlerts["guest1-cpu"]
|
||||||
|
_, pendingExists := m.pendingAlerts["guest1-cpu"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
if alertExists {
|
||||||
|
t.Error("expected active alert to be cleared")
|
||||||
|
}
|
||||||
|
if pendingExists {
|
||||||
|
t.Error("expected pending alert to be cleared")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("uses clear equals trigger when clear is zero", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := NewManager()
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.Enabled = true
|
||||||
|
m.activeAlerts["guest1-cpu"] = &Alert{
|
||||||
|
ID: "guest1-cpu",
|
||||||
|
Type: "cpu",
|
||||||
|
Value: 75, // Below trigger of 80
|
||||||
|
}
|
||||||
|
// Clear is 0, so it should use trigger (80) as clear threshold
|
||||||
|
m.config.GuestDefaults.CPU = &HysteresisThreshold{Trigger: 80, Clear: 0}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
m.ReevaluateGuestAlert(nil, "guest1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, exists := m.activeAlerts["guest1-cpu"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
if exists {
|
||||||
|
t.Error("expected alert to be cleared when value below trigger (used as clear)")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ignores alerts for different guests", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
m := NewManager()
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.Enabled = true
|
||||||
|
m.activeAlerts["guest1-cpu"] = &Alert{
|
||||||
|
ID: "guest1-cpu",
|
||||||
|
Type: "cpu",
|
||||||
|
Value: 50, // Below threshold
|
||||||
|
}
|
||||||
|
m.activeAlerts["guest2-cpu"] = &Alert{
|
||||||
|
ID: "guest2-cpu",
|
||||||
|
Type: "cpu",
|
||||||
|
Value: 50, // Below threshold
|
||||||
|
}
|
||||||
|
m.config.GuestDefaults.CPU = &HysteresisThreshold{Trigger: 80, Clear: 70}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
// Only reevaluate guest1
|
||||||
|
m.ReevaluateGuestAlert(nil, "guest1")
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, guest1Exists := m.activeAlerts["guest1-cpu"]
|
||||||
|
_, guest2Exists := m.activeAlerts["guest2-cpu"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if guest1Exists {
|
||||||
|
t.Error("expected guest1 alert to be cleared")
|
||||||
|
}
|
||||||
|
if !guest2Exists {
|
||||||
|
t.Error("expected guest2 alert to remain (not reevaluated)")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2282,7 +2282,7 @@ func TestNormalizeMetricTimeThresholds(t *testing.T) {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
result := normalizeMetricTimeThresholds(tc.input)
|
result := NormalizeMetricTimeThresholds(tc.input)
|
||||||
|
|
||||||
if tc.want == nil {
|
if tc.want == nil {
|
||||||
if result != nil {
|
if result != nil {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue