From e0af17a599e753551746f37f3e2a3d8cf998dc90 Mon Sep 17 00:00:00 2001 From: Ian Fijolek Date: Fri, 15 Nov 2024 11:30:34 -0800 Subject: [PATCH] Refactor test package and some field types Fairly big test refactor and changing some of the fields from pointers --- README.md | 2 +- alert.go | 11 -- alert_test.go | 60 +++---- config.go | 8 +- config_test.go | 19 +-- main.go | 12 +- main_test.go | 161 ++++++++----------- monitor.go | 24 ++- monitor_test.go | 211 ++++++++++--------------- test/invalid-config-missing-alerts.hcl | 7 + test/invalid-config-missing-alerts.yml | 8 - test/invalid-config-type.hcl | 1 + test/invalid-config-type.yml | 1 - test/invalid-config-unknown-alert.hcl | 12 ++ test/invalid-config-unknown-alert.yml | 13 -- test/valid-config-default-values.hcl | 11 ++ test/valid-config-default-values.yml | 12 -- test/valid-default-log-alert.yml | 8 - 18 files changed, 237 insertions(+), 344 deletions(-) create mode 100644 test/invalid-config-missing-alerts.hcl delete mode 100644 test/invalid-config-missing-alerts.yml create mode 100644 test/invalid-config-type.hcl delete mode 100644 test/invalid-config-type.yml create mode 100644 test/invalid-config-unknown-alert.hcl delete mode 100644 test/invalid-config-unknown-alert.yml create mode 100644 test/valid-config-default-values.hcl delete mode 100644 test/valid-config-default-values.yml delete mode 100644 test/valid-default-log-alert.yml diff --git a/README.md b/README.md index a95680e..a53e80c 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ Each monitor allows the following configuration: |`alert_down`|A list of Alerts to be triggered when the monitor is in a "down" state| |`alert_up`|A list of Alerts to be triggered when the monitor moves to an "up" state| |`check_interval`|The interval at which this monitor should be checked. This must be greater than the global `check_interval` value| -|`alert_after`|Allows specifying the number of failed checks before an alert should be triggered| +|`alert_after`|Allows specifying the number of failed checks before an alert should be triggered. A value of 1 will start sending alerts after the first failure.| |`alert_every`|Allows specifying how often an alert should be retriggered. There are a few magic numbers here. Defaults to `-1` for an exponential backoff. Setting to `0` disables re-alerting. Positive values will allow retriggering after the specified number of checks| ### Alerts diff --git a/alert.go b/alert.go index 1187aa0..b7d979c 100644 --- a/alert.go +++ b/alert.go @@ -162,14 +162,3 @@ func (alert Alert) Send(notice AlertNotice) (outputStr string, err error) { return outputStr, err } - -// NewLogAlert creates an alert that does basic logging using echo -func NewLogAlert() *Alert { - return &Alert{ - Name: "log", - Command: []string{ - "echo", - "{{.MonitorName}} {{if .IsUp}}has recovered{{else}}check has failed {{.FailureCount}} times{{end}}", - }, - } -} diff --git a/alert_test.go b/alert_test.go index d33c300..9e7e4a0 100644 --- a/alert_test.go +++ b/alert_test.go @@ -1,18 +1,20 @@ -package main +package main_test import ( "testing" + + m "git.iamthefij.com/iamthefij/minitor-go" ) func TestAlertIsValid(t *testing.T) { cases := []struct { - alert Alert + alert m.Alert expected bool name string }{ - {Alert{Command: []string{"echo", "test"}}, true, "Command only"}, - {Alert{ShellCommand: "echo test"}, true, "CommandShell only"}, - {Alert{}, false, "No commands"}, + {m.Alert{Command: []string{"echo", "test"}}, true, "Command only"}, + {m.Alert{ShellCommand: "echo test"}, true, "CommandShell only"}, + {m.Alert{}, false, "No commands"}, } for _, c := range cases { @@ -31,56 +33,40 @@ func TestAlertIsValid(t *testing.T) { func TestAlertSend(t *testing.T) { cases := []struct { - alert Alert - notice AlertNotice + alert m.Alert + notice m.AlertNotice expectedOutput string expectErr bool name string }{ { - Alert{Command: []string{"echo", "{{.MonitorName}}"}}, - AlertNotice{MonitorName: "test"}, + m.Alert{Command: []string{"echo", "{{.MonitorName}}"}}, + m.AlertNotice{MonitorName: "test"}, "test\n", false, "Command with template", }, { - Alert{ShellCommand: "echo {{.MonitorName}}"}, - AlertNotice{MonitorName: "test"}, + m.Alert{ShellCommand: "echo {{.MonitorName}}"}, + m.AlertNotice{MonitorName: "test"}, "test\n", false, "Command shell with template", }, { - Alert{Command: []string{"echo", "{{.Bad}}"}}, - AlertNotice{MonitorName: "test"}, + m.Alert{Command: []string{"echo", "{{.Bad}}"}}, + m.AlertNotice{MonitorName: "test"}, "", true, "Command with bad template", }, { - Alert{ShellCommand: "echo {{.Bad}}"}, - AlertNotice{MonitorName: "test"}, + m.Alert{ShellCommand: "echo {{.Bad}}"}, + m.AlertNotice{MonitorName: "test"}, "", true, "Command shell with bad template", }, - // Test default log alert down - { - *NewLogAlert(), - AlertNotice{MonitorName: "Test", FailureCount: 1, IsUp: false}, - "Test check has failed 1 times\n", - false, - "Default log alert down", - }, - // Test default log alert up - { - *NewLogAlert(), - AlertNotice{MonitorName: "Test", IsUp: true}, - "Test has recovered\n", - false, - "Default log alert up", - }, } for _, c := range cases { @@ -109,8 +95,8 @@ func TestAlertSend(t *testing.T) { } func TestAlertSendNoTemplates(t *testing.T) { - alert := Alert{} - notice := AlertNotice{} + alert := m.Alert{} + notice := m.AlertNotice{} output, err := alert.Send(notice) if err == nil { @@ -120,13 +106,13 @@ func TestAlertSendNoTemplates(t *testing.T) { func TestAlertBuildTemplate(t *testing.T) { cases := []struct { - alert Alert + alert m.Alert expectErr bool name string }{ - {Alert{Command: []string{"echo", "test"}}, false, "Command only"}, - {Alert{ShellCommand: "echo test"}, false, "CommandShell only"}, - {Alert{}, true, "No commands"}, + {m.Alert{Command: []string{"echo", "test"}}, false, "Command only"}, + {m.Alert{ShellCommand: "echo test"}, false, "CommandShell only"}, + {m.Alert{}, true, "No commands"}, } for _, c := range cases { diff --git a/config.go b/config.go index 7251aae..721a5e8 100644 --- a/config.go +++ b/config.go @@ -113,6 +113,8 @@ func (config *Config) Init() (err error) { } for _, monitor := range config.Monitors { + // TODO: Move this to a Monitor.Init() method + // Parse the check_interval string into a time.Duration if monitor.CheckIntervalStr != nil { monitor.CheckInterval, err = time.ParseDuration(*monitor.CheckIntervalStr) @@ -122,8 +124,10 @@ func (config *Config) Init() (err error) { } // Set default values for monitor alerts - if monitor.AlertAfter == nil { - monitor.AlertAfter = config.DefaultAlertAfter + if monitor.AlertAfter == 0 && config.DefaultAlertAfter != nil { + monitor.AlertAfter = *config.DefaultAlertAfter + } else if monitor.AlertAfter == 0 { + monitor.AlertAfter = 1 } if monitor.AlertEvery == nil { diff --git a/config_test.go b/config_test.go index 0e4acc5..66726c6 100644 --- a/config_test.go +++ b/config_test.go @@ -1,7 +1,9 @@ -package main +package main_test import ( "testing" + + m "git.iamthefij.com/iamthefij/minitor-go" ) func TestLoadConfig(t *testing.T) { @@ -11,12 +13,11 @@ func TestLoadConfig(t *testing.T) { name string }{ {"./test/does-not-exist", true, "Invalid config path"}, - // {"./test/invalid-config-missing-alerts.yml", true, "Invalid config missing alerts"}, - // {"./test/invalid-config-type.yml", true, "Invalid config type for key"}, - // {"./test/invalid-config-unknown-alert.yml", true, "Invalid config unknown alert"}, - // {"./test/valid-config-default-values.yml", false, "Valid config file with default values"}, + {"./test/invalid-config-missing-alerts.hcl", true, "Invalid config missing alerts"}, + {"./test/invalid-config-type.hcl", true, "Invalid config type for key"}, + {"./test/invalid-config-unknown-alert.hcl", true, "Invalid config unknown alert"}, + {"./test/valid-config-default-values.hcl", false, "Valid config file with default values"}, {"./test/valid-config.hcl", false, "Valid config file"}, - // {"./test/valid-default-log-alert.yml", true, "Invalid config file no log alert"}, } for _, c := range cases { c := c @@ -24,7 +25,7 @@ func TestLoadConfig(t *testing.T) { t.Run(c.name, func(t *testing.T) { t.Parallel() - _, err := LoadConfig(c.configPath) + _, err := m.LoadConfig(c.configPath) hasErr := (err != nil) if hasErr != c.expectErr { @@ -39,7 +40,7 @@ func TestLoadConfig(t *testing.T) { func TestMultiLineConfig(t *testing.T) { t.Parallel() - config, err := LoadConfig("./test/valid-verify-multi-line.hcl") + config, err := m.LoadConfig("./test/valid-verify-multi-line.hcl") if err != nil { t.Fatalf("TestMultiLineConfig(load), expected=no_error actual=%v", err) } @@ -87,7 +88,7 @@ func TestMultiLineConfig(t *testing.T) { t.Logf("bytes actual =%v", []byte(actual)) } - actual, err = alert.Send(AlertNotice{}) + actual, err = alert.Send(m.AlertNotice{}) if err != nil { t.Errorf("Execution of alert failed") } diff --git a/main.go b/main.go index 7f667bd..3f558b2 100644 --- a/main.go +++ b/main.go @@ -24,7 +24,7 @@ var ( errUnknownAlert = errors.New("unknown alert") ) -func sendAlerts(config *Config, monitor *Monitor, alertNotice *AlertNotice) error { +func SendAlerts(config *Config, monitor *Monitor, alertNotice *AlertNotice) error { slog.Debugf("Received an alert notice from %s", alertNotice.MonitorName) alertNames := monitor.GetAlertNames(alertNotice.IsUp) @@ -65,7 +65,7 @@ func sendAlerts(config *Config, monitor *Monitor, alertNotice *AlertNotice) erro return nil } -func checkMonitors(config *Config) error { +func CheckMonitors(config *Config) error { // TODO: Run this in goroutines and capture exceptions for _, monitor := range config.Monitors { if monitor.ShouldCheck() { @@ -77,7 +77,7 @@ func checkMonitors(config *Config) error { Metrics.CountCheck(monitor.Name, success, monitor.LastCheckMilliseconds(), hasAlert) if alertNotice != nil { - err := sendAlerts(config, monitor, alertNotice) + err := SendAlerts(config, monitor, alertNotice) // If there was an error in sending an alert, exit early and bubble it up if err != nil { return err @@ -89,7 +89,7 @@ func checkMonitors(config *Config) error { return nil } -func sendStartupAlerts(config *Config, alertNames []string) error { +func SendStartupAlerts(config *Config, alertNames []string) error { for _, alertName := range alertNames { var err error @@ -148,14 +148,14 @@ func main() { if *startupAlerts != "" { alertNames := strings.Split(*startupAlerts, ",") - err = sendStartupAlerts(&config, alertNames) + err = SendStartupAlerts(&config, alertNames) slog.OnErrPanicf(err, "Error running startup alerts") } // Start main loop for { - err = checkMonitors(&config) + err = CheckMonitors(&config) slog.OnErrPanicf(err, "Error checking monitors") time.Sleep(config.CheckInterval) diff --git a/main_test.go b/main_test.go index 48d0bd8..362c51f 100644 --- a/main_test.go +++ b/main_test.go @@ -1,125 +1,92 @@ -package main +package main_test -import "testing" +import ( + "testing" + + m "git.iamthefij.com/iamthefij/minitor-go" +) func Ptr[T any](v T) *T { return &v } +// TestCheckConfig tests the checkConfig function +// It also tests results for potentially invalid configuration. For example, no alerts func TestCheckMonitors(t *testing.T) { cases := []struct { - config Config - expectErr bool - name string + config m.Config + expectFailureError bool + expectRecoverError bool + name string }{ { - config: Config{ + config: m.Config{ CheckIntervalStr: "1s", - Monitors: []*Monitor{ + Monitors: []*m.Monitor{ { - Name: "Success", - Command: []string{"true"}, + Name: "Success", }, }, }, - expectErr: false, - name: "Monitor success, no alerts", + expectFailureError: false, + expectRecoverError: false, + name: "No alerts", }, { - config: Config{ + config: m.Config{ CheckIntervalStr: "1s", - Monitors: []*Monitor{ + Monitors: []*m.Monitor{ { Name: "Failure", - Command: []string{"false"}, - AlertAfter: Ptr(1), - }, - }, - }, - expectErr: true, - name: "Monitor failure, no alerts", - }, - { - config: Config{ - CheckIntervalStr: "1s", - Monitors: []*Monitor{ - { - Name: "Success", - Command: []string{"ls"}, - alertCount: 1, - }, - }, - }, - expectErr: false, - name: "Monitor recovery, no alerts", - }, - { - config: Config{ - CheckIntervalStr: "1s", - Monitors: []*Monitor{ - { - Name: "Failure", - Command: []string{"false"}, AlertDown: []string{"unknown"}, - AlertAfter: Ptr(1), - }, - }, - }, - expectErr: true, - name: "Monitor failure, unknown alerts", - }, - { - config: Config{ - CheckIntervalStr: "1s", - Monitors: []*Monitor{ - { - Name: "Success", - Command: []string{"true"}, AlertUp: []string{"unknown"}, - alertCount: 1, + AlertAfter: 1, }, }, }, - expectErr: true, - name: "Monitor recovery, unknown alerts", + expectFailureError: true, + expectRecoverError: true, + name: "Unknown alerts", }, { - config: Config{ + config: m.Config{ CheckIntervalStr: "1s", - Monitors: []*Monitor{ + Monitors: []*m.Monitor{ { Name: "Failure", - Command: []string{"false"}, AlertDown: []string{"good"}, - AlertAfter: Ptr(1), + AlertUp: []string{"good"}, + AlertAfter: 1, }, }, - Alerts: []*Alert{{ + Alerts: []*m.Alert{{ Name: "good", Command: []string{"true"}, }}, }, - expectErr: false, - name: "Monitor failure, successful alert", + expectFailureError: false, + expectRecoverError: false, + name: "Successful alert", }, { - config: Config{ + config: m.Config{ CheckIntervalStr: "1s", - Monitors: []*Monitor{ + Monitors: []*m.Monitor{ { Name: "Failure", - Command: []string{"false"}, AlertDown: []string{"bad"}, - AlertAfter: Ptr(1), + AlertUp: []string{"bad"}, + AlertAfter: 1, }, }, - Alerts: []*Alert{{ + Alerts: []*m.Alert{{ Name: "bad", Command: []string{"false"}, }}, }, - expectErr: true, - name: "Monitor failure, bad alert", + expectFailureError: true, + expectRecoverError: true, + name: "Failing alert", }, } @@ -134,11 +101,25 @@ func TestCheckMonitors(t *testing.T) { t.Errorf("checkMonitors(%s): unexpected error reading config: %v", c.name, err) } - err = checkMonitors(&c.config) - if err == nil && c.expectErr { - t.Errorf("checkMonitors(%s): Expected panic, the code did not panic", c.name) - } else if err != nil && !c.expectErr { - t.Errorf("checkMonitors(%s): Did not expect an error, but we got one anyway: %v", c.name, err) + for _, check := range []struct { + shellCmd string + name string + expectErr bool + }{ + {"false", "Failure", c.expectFailureError}, {"true", "Success", c.expectRecoverError}, + } { + // Set the shell command for this check + c.config.Monitors[0].ShellCommand = check.shellCmd + + // Run the check + err = m.CheckMonitors(&c.config) + + // Check the results + if err == nil && check.expectErr { + t.Errorf("checkMonitors(%s:%s): Expected error, the code did not error", c.name, check.name) + } else if err != nil && !check.expectErr { + t.Errorf("checkMonitors(%s:%s): Did not expect an error, but we got one anyway: %v", c.name, check.name, err) + } } }) } @@ -146,26 +127,23 @@ func TestCheckMonitors(t *testing.T) { func TestFirstRunAlerts(t *testing.T) { cases := []struct { - config Config + config m.Config expectErr bool startupAlerts []string name string }{ { - config: Config{}, - expectErr: false, - startupAlerts: []string{}, - name: "Empty", - }, - { - config: Config{}, + config: m.Config{ + CheckIntervalStr: "1s", + }, expectErr: true, startupAlerts: []string{"missing"}, name: "Unknown", }, { - config: Config{ - Alerts: []*Alert{ + config: m.Config{ + CheckIntervalStr: "1s", + Alerts: []*m.Alert{ { Name: "good", Command: []string{"true"}, @@ -177,8 +155,9 @@ func TestFirstRunAlerts(t *testing.T) { name: "Successful alert", }, { - config: Config{ - Alerts: []*Alert{ + config: m.Config{ + CheckIntervalStr: "1s", + Alerts: []*m.Alert{ { Name: "bad", Command: []string{"false"}, @@ -202,7 +181,7 @@ func TestFirstRunAlerts(t *testing.T) { t.Errorf("sendFirstRunAlerts(%s): unexpected error reading config: %v", c.name, err) } - err = sendStartupAlerts(&c.config, c.startupAlerts) + err = m.SendStartupAlerts(&c.config, c.startupAlerts) if err == nil && c.expectErr { t.Errorf("sendFirstRunAlerts(%s): Expected error, the code did not error", c.name) } else if err != nil && !c.expectErr { diff --git a/monitor.go b/monitor.go index 28dcc63..c7c83b6 100644 --- a/monitor.go +++ b/monitor.go @@ -15,7 +15,7 @@ type Monitor struct { //nolint:maligned CheckInterval time.Duration Name string `hcl:"name,label"` - AlertAfter *int `hcl:"alert_after,optional"` + AlertAfter int `hcl:"alert_after,optional"` AlertEvery *int `hcl:"alert_every,optional"` AlertDown []string `hcl:"alert_down,optional"` AlertUp []string `hcl:"alert_up,optional"` @@ -34,9 +34,10 @@ type Monitor struct { //nolint:maligned // IsValid returns a boolean indicating if the Monitor has been correctly // configured func (monitor Monitor) IsValid() bool { + // TODO: Refactor and return an error containing more information on what was invalid hasCommand := len(monitor.Command) > 0 hasShellCommand := monitor.ShellCommand != "" - hasValidAlertAfter := monitor.GetAlertAfter() > 0 + hasValidAlertAfter := monitor.AlertAfter > 0 hasAlertDown := len(monitor.AlertDown) > 0 hasAtLeastOneCommand := hasCommand || hasShellCommand @@ -48,6 +49,10 @@ func (monitor Monitor) IsValid() bool { hasAlertDown } +func (monitor Monitor) LastOutput() string { + return monitor.lastOutput +} + // ShouldCheck returns a boolean indicating if the Monitor is ready to be // be checked again func (monitor Monitor) ShouldCheck() bool { @@ -126,20 +131,20 @@ func (monitor *Monitor) success() (notice *AlertNotice) { func (monitor *Monitor) failure() (notice *AlertNotice) { monitor.failureCount++ // If we haven't hit the minimum failures, we can exit - if monitor.failureCount < monitor.GetAlertAfter() { + if monitor.failureCount < monitor.AlertAfter { slog.Debugf( "%s failed but did not hit minimum failures. "+ "Count: %v alert after: %v", monitor.Name, monitor.failureCount, - monitor.GetAlertAfter(), + monitor.AlertAfter, ) return } // Take number of failures after minimum - failureCount := (monitor.failureCount - monitor.GetAlertAfter()) + failureCount := (monitor.failureCount - monitor.AlertAfter) // Use alert cadence to determine if we should alert switch { @@ -168,15 +173,6 @@ func (monitor *Monitor) failure() (notice *AlertNotice) { return notice } -// GetAlertAfter will get or return the default alert after value -func (monitor Monitor) GetAlertAfter() int { - if monitor.AlertAfter == nil { - return 1 - } - - return *monitor.AlertAfter -} - // GetAlertNames gives a list of alert names for a given monitor status func (monitor Monitor) GetAlertNames(up bool) []string { if up { diff --git a/monitor_test.go b/monitor_test.go index a31f632..ec5e43e 100644 --- a/monitor_test.go +++ b/monitor_test.go @@ -1,22 +1,25 @@ -package main +package main_test import ( + "reflect" "testing" "time" + + m "git.iamthefij.com/iamthefij/minitor-go" ) // TestMonitorIsValid tests the Monitor.IsValid() func TestMonitorIsValid(t *testing.T) { cases := []struct { - monitor Monitor + monitor m.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"}, + {m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, true, "Command only"}, + {m.Monitor{AlertAfter: 1, ShellCommand: "echo test", AlertDown: []string{"log"}}, true, "CommandShell only"}, + {m.Monitor{AlertAfter: 1, Command: []string{"echo", "test"}}, false, "No AlertDown"}, + {m.Monitor{AlertAfter: 1, AlertDown: []string{"log"}}, false, "No commands"}, + {m.Monitor{AlertAfter: -1, Command: []string{"echo", "test"}, AlertDown: []string{"log"}}, false, "Invalid alert threshold, -1"}, } for _, c := range cases { @@ -35,75 +38,63 @@ func TestMonitorIsValid(t *testing.T) { // 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 + t.Parallel() - 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"}, + // Create a monitor that should check every second and then verify it checks with some sleeps + monitor := m.Monitor{ShellCommand: "true", CheckInterval: time.Second} + + if !monitor.ShouldCheck() { + t.Errorf("New monitor should be ready to check") } - for _, c := range cases { - c := c + monitor.Check() - t.Run(c.name, func(t *testing.T) { - t.Parallel() + if monitor.ShouldCheck() { + t.Errorf("Monitor should not be ready to check after a check") + } - actual := c.monitor.ShouldCheck() - if actual != c.expected { - t.Errorf("ShouldCheck(%v), expected=%t actual=%t", c.name, c.expected, actual) - } - }) + time.Sleep(time.Second) + + if !monitor.ShouldCheck() { + t.Errorf("Monitor should be ready to check after a second") } } // 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"}, + t.Parallel() + + // Creating a monitor that should alert after 2 failures. The monitor should be considered up until we reach two failed checks + monitor := m.Monitor{ShellCommand: "false", AlertAfter: 2} + if !monitor.IsUp() { + t.Errorf("New monitor should be considered up") } - for _, c := range cases { - c := c + monitor.Check() - t.Run(c.name, func(t *testing.T) { - t.Parallel() + if !monitor.IsUp() { + t.Errorf("Monitor should be considered up with one failure and no alerts") + } - actual := c.monitor.IsUp() - if actual != c.expected { - t.Errorf("IsUp(%v), expected=%t actual=%t", c.name, c.expected, actual) - } - }) + monitor.Check() + + if monitor.IsUp() { + t.Errorf("Monitor should be considered down with one alert") } } // TestMonitorGetAlertNames tests that proper alert names are returned func TestMonitorGetAlertNames(t *testing.T) { cases := []struct { - monitor Monitor + monitor m.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"}, + {m.Monitor{}, true, nil, "Empty up"}, + {m.Monitor{}, false, nil, "Empty down"}, + {m.Monitor{AlertUp: []string{"alert"}}, true, []string{"alert"}, "Return up"}, + {m.Monitor{AlertDown: []string{"alert"}}, false, []string{"alert"}, "Return down"}, } for _, c := range cases { @@ -113,57 +104,30 @@ func TestMonitorGetAlertNames(t *testing.T) { t.Parallel() actual := c.monitor.GetAlertNames(c.up) - if !EqualSliceString(actual, c.expected) { + if !reflect.DeepEqual(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 + var alertEveryOne int = 1 cases := []struct { - monitor Monitor + monitor m.Monitor + numChecks int 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"}, + {m.Monitor{ShellCommand: "false", AlertAfter: 1}, 1, true, "Empty After 1"}, // Defaults to true because and AlertEvery default to 0 + {m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: &alertEveryOne}, 1, true, "Alert after 1: first failure"}, + {m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: &alertEveryOne}, 2, true, "Alert after 1: second failure"}, + {m.Monitor{ShellCommand: "false", AlertAfter: 20, AlertEvery: &alertEveryOne}, 1, false, "Alert after 20: first failure"}, + {m.Monitor{ShellCommand: "false", AlertAfter: 20, AlertEvery: &alertEveryOne}, 20, true, "Alert after 20: 20th failure"}, + {m.Monitor{ShellCommand: "false", AlertAfter: 20, AlertEvery: &alertEveryOne}, 21, true, "Alert after 20: 21st failure"}, } for _, c := range cases { @@ -172,8 +136,12 @@ func TestMonitorFailureAlertAfter(t *testing.T) { t.Run(c.name, func(t *testing.T) { t.Parallel() - notice := c.monitor.failure() - hasNotice := (notice != nil) + hasNotice := false + + for i := 0; i < c.numChecks; i++ { + _, notice := c.monitor.Check() + hasNotice = (notice != nil) + } if hasNotice != c.expectNotice { t.Errorf("failure(%v), expected=%t actual=%t", c.name, c.expectNotice, hasNotice) @@ -185,39 +153,18 @@ func TestMonitorFailureAlertAfter(t *testing.T) { // 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 + monitor m.Monitor + expectedNotice []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 + {m.Monitor{ShellCommand: "false", AlertAfter: 1}, []bool{true}, "No AlertEvery set"}, // 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"}, + {m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: Ptr(0)}, []bool{true, false, false}, "Alert first time only after 1"}, // 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"}, + {m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: Ptr(1)}, []bool{true, true, true}, "Alert every time after 1"}, // 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"}, + {m.Monitor{ShellCommand: "false", AlertAfter: 1, AlertEvery: Ptr(2)}, []bool{true, false, true, false}, "Alert every other time after 1"}, } for _, c := range cases { @@ -226,11 +173,13 @@ func TestMonitorFailureAlertEvery(t *testing.T) { t.Run(c.name, func(t *testing.T) { t.Parallel() - notice := c.monitor.failure() - hasNotice := (notice != nil) + for i, expectNotice := range c.expectedNotice { + _, notice := c.monitor.Check() + hasNotice := (notice != nil) - if hasNotice != c.expectNotice { - t.Errorf("failure(%v), expected=%t actual=%t", c.name, c.expectNotice, hasNotice) + if hasNotice != expectNotice { + t.Errorf("failed %s check %d: expected=%t actual=%t", c.name, i, expectNotice, hasNotice) + } } }) } @@ -257,12 +206,12 @@ func TestMonitorFailureExponential(t *testing.T) { // 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} + monitor := m.Monitor{ShellCommand: "false", AlertAfter: 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() + _, notice := monitor.Check() hasNotice := (notice != nil) if hasNotice != c.expectNotice { @@ -281,27 +230,27 @@ func TestMonitorCheck(t *testing.T) { } cases := []struct { - monitor Monitor + monitor m.Monitor expect expected name string }{ { - Monitor{Command: []string{"echo", "success"}}, + m.Monitor{AlertAfter: 1, Command: []string{"echo", "success"}}, expected{isSuccess: true, hasNotice: false, lastOutput: "success\n"}, "Test successful command", }, { - Monitor{ShellCommand: "echo success"}, + m.Monitor{AlertAfter: 1, ShellCommand: "echo success"}, expected{isSuccess: true, hasNotice: false, lastOutput: "success\n"}, "Test successful command shell", }, { - Monitor{Command: []string{"total", "failure"}}, + m.Monitor{AlertAfter: 1, Command: []string{"total", "failure"}}, expected{isSuccess: false, hasNotice: true, lastOutput: ""}, "Test failed command", }, { - Monitor{ShellCommand: "false"}, + m.Monitor{AlertAfter: 1, ShellCommand: "false"}, expected{isSuccess: false, hasNotice: true, lastOutput: ""}, "Test failed command shell", }, @@ -323,7 +272,7 @@ func TestMonitorCheck(t *testing.T) { t.Errorf("Check(%v) (notice), expected=%t actual=%t", c.name, c.expect.hasNotice, hasNotice) } - lastOutput := c.monitor.lastOutput + lastOutput := c.monitor.LastOutput() if lastOutput != c.expect.lastOutput { t.Errorf("Check(%v) (output), expected=%v actual=%v", c.name, c.expect.lastOutput, lastOutput) } diff --git a/test/invalid-config-missing-alerts.hcl b/test/invalid-config-missing-alerts.hcl new file mode 100644 index 0000000..2992ece --- /dev/null +++ b/test/invalid-config-missing-alerts.hcl @@ -0,0 +1,7 @@ +check_interval = "1s" + +monitor "Command" { + command = ["echo", "$PATH"] + alert_down = [ "alert_down", "log_shell", "log_command" ] + alert_every = 0 +} diff --git a/test/invalid-config-missing-alerts.yml b/test/invalid-config-missing-alerts.yml deleted file mode 100644 index d92952a..0000000 --- a/test/invalid-config-missing-alerts.yml +++ /dev/null @@ -1,8 +0,0 @@ -check_interval: 1 - -monitors: - - name: Command - command: ['echo', '$PATH'] - alert_down: [ 'alert_down', 'log_shell', 'log_command' ] - # alert_every: -1 - alert_every: 0 diff --git a/test/invalid-config-type.hcl b/test/invalid-config-type.hcl new file mode 100644 index 0000000..84d26ae --- /dev/null +++ b/test/invalid-config-type.hcl @@ -0,0 +1 @@ +check_interval = "woops, I'm not an int!" diff --git a/test/invalid-config-type.yml b/test/invalid-config-type.yml deleted file mode 100644 index feed899..0000000 --- a/test/invalid-config-type.yml +++ /dev/null @@ -1 +0,0 @@ -check_interval: woops, I'm not an int! diff --git a/test/invalid-config-unknown-alert.hcl b/test/invalid-config-unknown-alert.hcl new file mode 100644 index 0000000..4d8f663 --- /dev/null +++ b/test/invalid-config-unknown-alert.hcl @@ -0,0 +1,12 @@ +check_interval = "1s" + +monitor "Command" { + command = ["echo", "$PATH"] + alert_down = ["not_log"] + alert_every = 0 +} + + +alert "log" { + command = ["true"] +} diff --git a/test/invalid-config-unknown-alert.yml b/test/invalid-config-unknown-alert.yml deleted file mode 100644 index 9991a6c..0000000 --- a/test/invalid-config-unknown-alert.yml +++ /dev/null @@ -1,13 +0,0 @@ -check_interval: 1 - -monitors: - - name: Command - command: ['echo', '$PATH'] - alert_down: [ 'not_log'] - # alert_every: -1 - alert_every: 0 - - -alerts: - log: - command: ['true'] diff --git a/test/valid-config-default-values.hcl b/test/valid-config-default-values.hcl new file mode 100644 index 0000000..f743af0 --- /dev/null +++ b/test/valid-config-default-values.hcl @@ -0,0 +1,11 @@ +check_interval = "1s" +default_alert_down = ["log_command"] +default_alert_after = 1 + +monitor "Command" { + command = ["echo", "$PATH"] +} + +alert "log_command" { + command = ["echo", "default", "'command!!!'", "{{.MonitorName}}"] +} diff --git a/test/valid-config-default-values.yml b/test/valid-config-default-values.yml deleted file mode 100644 index f25dd2d..0000000 --- a/test/valid-config-default-values.yml +++ /dev/null @@ -1,12 +0,0 @@ ---- -check_interval: 1 -default_alert_down: ["log_command"] -default_alert_after: 1 - -monitors: - - name: Command - command: ["echo", "$PATH"] - -alerts: - log_command: - command: ["echo", "regular", '"command!!!"', "{{.MonitorName}}"] diff --git a/test/valid-default-log-alert.yml b/test/valid-default-log-alert.yml deleted file mode 100644 index 074f7a3..0000000 --- a/test/valid-default-log-alert.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -check_interval: 1 - -monitors: - - name: Command - command: ['echo', '$PATH'] - alert_down: ['log'] - alert_every: 0