package main import ( "testing" "time" ) // TestMonitorIsValid tests the Monitor.IsValid() func TestMonitorIsValid(t *testing.T) { cases := []struct { monitor Monitor expected bool name string }{ {Monitor{Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, true, "Command only"}, {Monitor{ShellCommand: "echo test", AlertDown: []string{"log"}}, true, "CommandShell only"}, {Monitor{Command: []string{"echo", "test"}}, false, "No AlertDown"}, {Monitor{AlertDown: []string{"log"}}, false, "No commands"}, {Monitor{Command: []string{"echo", "test"}, AlertDown: []string{"log"}, AlertAfter: Ptr(-1)}, false, "Invalid alert threshold, -1"}, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { t.Parallel() actual := c.monitor.IsValid() if actual != c.expected { t.Errorf("IsValid(%v), expected=%t actual=%t", c.name, c.expected, actual) } }) } } // TestMonitorShouldCheck tests the Monitor.ShouldCheck() func TestMonitorShouldCheck(t *testing.T) { timeNow := time.Now() timeTenSecAgo := time.Now().Add(time.Second * -10) timeTwentySecAgo := time.Now().Add(time.Second * -20) fifteenSeconds := time.Second * 15 cases := []struct { monitor Monitor expected bool name string }{ {Monitor{}, true, "Empty"}, {Monitor{lastCheck: timeNow, CheckInterval: fifteenSeconds}, false, "Just checked"}, {Monitor{lastCheck: timeTenSecAgo, CheckInterval: fifteenSeconds}, false, "-10s"}, {Monitor{lastCheck: timeTwentySecAgo, CheckInterval: fifteenSeconds}, true, "-20s"}, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { t.Parallel() actual := c.monitor.ShouldCheck() if actual != c.expected { t.Errorf("ShouldCheck(%v), expected=%t actual=%t", c.name, c.expected, actual) } }) } } // TestMonitorIsUp tests the Monitor.IsUp() func TestMonitorIsUp(t *testing.T) { cases := []struct { monitor Monitor expected bool name string }{ {Monitor{}, true, "Empty"}, {Monitor{alertCount: 1}, false, "Has alert"}, {Monitor{alertCount: -1}, false, "Negative alerts"}, {Monitor{alertCount: 0}, true, "No alerts"}, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { t.Parallel() actual := c.monitor.IsUp() if actual != c.expected { t.Errorf("IsUp(%v), expected=%t actual=%t", c.name, c.expected, actual) } }) } } // TestMonitorGetAlertNames tests that proper alert names are returned func TestMonitorGetAlertNames(t *testing.T) { cases := []struct { monitor Monitor up bool expected []string name string }{ {Monitor{}, true, nil, "Empty up"}, {Monitor{}, false, nil, "Empty down"}, {Monitor{AlertUp: []string{"alert"}}, true, []string{"alert"}, "Return up"}, {Monitor{AlertDown: []string{"alert"}}, false, []string{"alert"}, "Return down"}, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { t.Parallel() actual := c.monitor.GetAlertNames(c.up) if !EqualSliceString(actual, c.expected) { t.Errorf("GetAlertNames(%v), expected=%v actual=%v", c.name, c.expected, actual) } }) } } // TestMonitorSuccess tests the Monitor.success() func TestMonitorSuccess(t *testing.T) { cases := []struct { monitor Monitor expectNotice bool name string }{ {Monitor{}, false, "Empty"}, {Monitor{alertCount: 0}, false, "No alerts"}, {Monitor{alertCount: 1}, true, "Has alert"}, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { t.Parallel() notice := c.monitor.success() hasNotice := (notice != nil) if hasNotice != c.expectNotice { t.Errorf("success(%v), expected=%t actual=%t", c.name, c.expectNotice, hasNotice) } }) } } // TestMonitorFailureAlertAfter tests that alerts will not trigger until // hitting the threshold provided by AlertAfter func TestMonitorFailureAlertAfter(t *testing.T) { var alertEvery int = 1 cases := []struct { monitor Monitor expectNotice bool name string }{ {Monitor{AlertAfter: Ptr(1)}, true, "Empty"}, // Defaults to true because and AlertEvery default to 0 {Monitor{failureCount: 0, AlertAfter: Ptr(1), AlertEvery: &alertEvery}, true, "Alert after 1: first failure"}, {Monitor{failureCount: 1, AlertAfter: Ptr(1), AlertEvery: &alertEvery}, true, "Alert after 1: second failure"}, {Monitor{failureCount: 0, AlertAfter: Ptr(20), AlertEvery: &alertEvery}, false, "Alert after 20: first failure"}, {Monitor{failureCount: 19, AlertAfter: Ptr(20), AlertEvery: &alertEvery}, true, "Alert after 20: 20th failure"}, {Monitor{failureCount: 20, AlertAfter: Ptr(20), AlertEvery: &alertEvery}, true, "Alert after 20: 21st failure"}, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { t.Parallel() notice := c.monitor.failure() hasNotice := (notice != nil) if hasNotice != c.expectNotice { t.Errorf("failure(%v), expected=%t actual=%t", c.name, c.expectNotice, hasNotice) } }) } } // TestMonitorFailureAlertEvery tests that alerts will trigger // on the expected intervals func TestMonitorFailureAlertEvery(t *testing.T) { var alertEvery0, alertEvery1, alertEvery2 int alertEvery0 = 0 alertEvery1 = 1 alertEvery2 = 2 cases := []struct { monitor Monitor expectNotice bool name string }{ /* TODO: Actually found a bug in original implementation. There is an inconsistency in the way AlertAfter is treated. For "First alert only" (ie. AlertEvery=0), it is the number of failures to ignore before alerting, so AlertAfter=1 will ignore the first failure and alert on the second failure For other intervals (ie. AlertEvery=1), it is essentially indexed on one. Essentially making AlertAfter=1 trigger on the first failure. For usabilty, this should be consistent. Consistent with what though? minitor-py? Or itself? Dun dun duuuunnnnn! */ {Monitor{AlertAfter: Ptr(1)}, true, "Empty"}, // Defaults to true because AlertAfter and AlertEvery default to nil // Alert first time only, after 1 {Monitor{failureCount: 0, AlertAfter: Ptr(1), AlertEvery: &alertEvery0}, true, "Alert first time only after 1: first failure"}, {Monitor{failureCount: 1, AlertAfter: Ptr(1), AlertEvery: &alertEvery0}, false, "Alert first time only after 1: second failure"}, {Monitor{failureCount: 2, AlertAfter: Ptr(1), AlertEvery: &alertEvery0}, false, "Alert first time only after 1: third failure"}, // Alert every time, after 1 {Monitor{failureCount: 0, AlertAfter: Ptr(1), AlertEvery: &alertEvery1}, true, "Alert every time after 1: first failure"}, {Monitor{failureCount: 1, AlertAfter: Ptr(1), AlertEvery: &alertEvery1}, true, "Alert every time after 1: second failure"}, {Monitor{failureCount: 2, AlertAfter: Ptr(1), AlertEvery: &alertEvery1}, true, "Alert every time after 1: third failure"}, // Alert every other time, after 1 {Monitor{failureCount: 0, AlertAfter: Ptr(1), AlertEvery: &alertEvery2}, true, "Alert every other time after 1: first failure"}, {Monitor{failureCount: 1, AlertAfter: Ptr(1), AlertEvery: &alertEvery2}, false, "Alert every other time after 1: second failure"}, {Monitor{failureCount: 2, AlertAfter: Ptr(1), AlertEvery: &alertEvery2}, true, "Alert every other time after 1: third failure"}, {Monitor{failureCount: 3, AlertAfter: Ptr(1), AlertEvery: &alertEvery2}, false, "Alert every other time after 1: fourth failure"}, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { t.Parallel() notice := c.monitor.failure() hasNotice := (notice != nil) if hasNotice != c.expectNotice { t.Errorf("failure(%v), expected=%t actual=%t", c.name, c.expectNotice, hasNotice) } }) } } // TestMonitorFailureExponential tests that alerts will trigger // with an exponential backoff after repeated failures func TestMonitorFailureExponential(t *testing.T) { var alertEveryExp int = -1 cases := []struct { expectNotice bool name string }{ {true, "Alert exponential after 1: first failure"}, {true, "Alert exponential after 1: second failure"}, {false, "Alert exponential after 1: third failure"}, {true, "Alert exponential after 1: fourth failure"}, {false, "Alert exponential after 1: fifth failure"}, {false, "Alert exponential after 1: sixth failure"}, {false, "Alert exponential after 1: seventh failure"}, {true, "Alert exponential after 1: eighth failure"}, } // Unlike previous tests, this one requires a static Monitor with repeated // calls to the failure method monitor := Monitor{failureCount: 0, AlertAfter: Ptr(1), AlertEvery: &alertEveryExp} for _, c := range cases { t.Run(c.name, func(t *testing.T) { // NOTE: These tests are not parallel because they rely on the state of the Monitor notice := monitor.failure() hasNotice := (notice != nil) if hasNotice != c.expectNotice { t.Errorf("failure(%v), expected=%t actual=%t", c.name, c.expectNotice, hasNotice) } }) } } // TestMonitorCheck tests successful and failed commands and shell commands func TestMonitorCheck(t *testing.T) { type expected struct { isSuccess bool hasNotice bool lastOutput string } cases := []struct { monitor Monitor expect expected name string }{ { Monitor{Command: []string{"echo", "success"}}, expected{isSuccess: true, hasNotice: false, lastOutput: "success\n"}, "Test successful command", }, { Monitor{ShellCommand: "echo success"}, expected{isSuccess: true, hasNotice: false, lastOutput: "success\n"}, "Test successful command shell", }, { Monitor{Command: []string{"total", "failure"}}, expected{isSuccess: false, hasNotice: true, lastOutput: ""}, "Test failed command", }, { Monitor{ShellCommand: "false"}, expected{isSuccess: false, hasNotice: true, lastOutput: ""}, "Test failed command shell", }, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { t.Parallel() isSuccess, notice := c.monitor.Check() if isSuccess != c.expect.isSuccess { t.Errorf("Check(%v) (success), expected=%t actual=%t", c.name, c.expect.isSuccess, isSuccess) } hasNotice := (notice != nil) if hasNotice != c.expect.hasNotice { t.Errorf("Check(%v) (notice), expected=%t actual=%t", c.name, c.expect.hasNotice, hasNotice) } lastOutput := c.monitor.lastOutput if lastOutput != c.expect.lastOutput { t.Errorf("Check(%v) (output), expected=%v actual=%v", c.name, c.expect.lastOutput, lastOutput) } }) } }