From 78af496fe93ab58d6c715483cbe73994a1f653a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20BENO=C3=8ET?= Date: Sat, 20 Jul 2024 10:01:05 +0200 Subject: [PATCH] refactor: make internals easier to test and add unit tests (#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR refactors most of the internals to make them easier to test (and also because the names didn't make sense). It adds unit tests for all internal components. Reviewed-on: https://git.nocternity.net/projects/gomonop/pulls/2 Co-authored-by: Emmanuel BENOÎT Co-committed-by: Emmanuel BENOÎT --- cmd/sslcert/main.go | 46 ++--- cmd/zoneserial/main.go | 36 ++-- go.mod | 4 + go.sum | 9 + main.go | 39 +++-- pkg/perfdata/internals.go | 25 +++ pkg/perfdata/internals_test.go | 50 ++++++ pkg/perfdata/perfdata.go | 124 ++------------ pkg/perfdata/perfdata_test.go | 303 +++++++++++++++++++++++++-------- pkg/perfdata/range.go | 62 +++++++ pkg/perfdata/range_test.go | 67 ++++++++ pkg/perfdata/units.go | 20 +++ pkg/perfdata/units_test.go | 26 +++ pkg/plugin/plugin.go | 130 ++------------ pkg/program/program.go | 9 - pkg/results/results.go | 107 ++++++++++++ pkg/results/results_test.go | 175 +++++++++++++++++++ pkg/results/status.go | 19 +++ pkg/results/status_test.go | 26 +++ pkg/version/version_test.go | 31 ++++ 20 files changed, 939 insertions(+), 369 deletions(-) create mode 100644 pkg/perfdata/internals.go create mode 100644 pkg/perfdata/internals_test.go create mode 100644 pkg/perfdata/range.go create mode 100644 pkg/perfdata/range_test.go create mode 100644 pkg/perfdata/units.go create mode 100644 pkg/perfdata/units_test.go delete mode 100644 pkg/program/program.go create mode 100644 pkg/results/results.go create mode 100644 pkg/results/results_test.go create mode 100644 pkg/results/status.go create mode 100644 pkg/results/status_test.go create mode 100644 pkg/version/version_test.go diff --git a/cmd/sslcert/main.go b/cmd/sslcert/main.go index d942cc3..eb7b2f1 100644 --- a/cmd/sslcert/main.go +++ b/cmd/sslcert/main.go @@ -16,7 +16,7 @@ import ( "nocternity.net/gomonop/pkg/perfdata" "nocternity.net/gomonop/pkg/plugin" - "nocternity.net/gomonop/pkg/program" + "nocternity.net/gomonop/pkg/results" ) //-------------------------------------------------------------------------------------------------------- @@ -167,7 +167,7 @@ type programFlags struct { // Program data including configuration and runtime data. type checkProgram struct { programFlags // Flags from the command line - plugin *plugin.Plugin // Plugin output state + plugin *results.Results // Plugin output state certificate *x509.Certificate // X.509 certificate from the server } @@ -207,37 +207,37 @@ func (flags *programFlags) parseArguments() { } // Initialise the monitoring check program. -func NewProgram() program.Program { +func NewProgram() plugin.Plugin { program := &checkProgram{ - plugin: plugin.New("Certificate check"), + plugin: results.New("Certificate check"), } program.parseArguments() return program } -// Terminate the monitoring check program. -func (program *checkProgram) Done() { - program.plugin.Done() +// Return the program's output value. +func (program *checkProgram) Results() *results.Results { + return program.plugin } // Check the values that were specified from the command line. Returns true // if the arguments made sense. func (program *checkProgram) CheckArguments() bool { if program.hostname == "" { - program.plugin.SetState(plugin.UNKNOWN, "no hostname specified") + program.plugin.SetState(results.StatusUnknown, "no hostname specified") return false } if program.port < 1 || program.port > 65535 { - program.plugin.SetState(plugin.UNKNOWN, "invalid or missing port number") + program.plugin.SetState(results.StatusUnknown, "invalid or missing port number") return false } if program.warn != -1 && program.crit != -1 && program.warn <= program.crit { - program.plugin.SetState(plugin.UNKNOWN, "nonsensical thresholds") + program.plugin.SetState(results.StatusUnknown, "nonsensical thresholds") return false } if _, ok := certGetters[program.startTLS]; !ok { errstr := "unsupported StartTLS protocol " + program.startTLS - program.plugin.SetState(plugin.UNKNOWN, errstr) + program.plugin.SetState(results.StatusUnknown, errstr) return false } program.hostname = strings.ToLower(program.hostname) @@ -262,13 +262,13 @@ func (program *checkProgram) getCertificate() error { // matches the requested host name. func (program *checkProgram) checkSANlessCertificate() bool { if !program.ignoreCnOnly || len(program.extraNames) != 0 { - program.plugin.SetState(plugin.WARNING, + program.plugin.SetState(results.StatusWarning, "certificate doesn't have SAN domain names") return false } dn := strings.ToLower(program.certificate.Subject.String()) if !strings.HasPrefix(dn, fmt.Sprintf("cn=%s,", program.hostname)) { - program.plugin.SetState(plugin.CRITICAL, "incorrect certificate CN") + program.plugin.SetState(results.StatusCritical, "incorrect certificate CN") return false } return true @@ -298,7 +298,7 @@ func (program *checkProgram) checkNames() bool { certificateIsOk = program.checkHostName(name) && certificateIsOk } if !certificateIsOk { - program.plugin.SetState(plugin.CRITICAL, "names missing from SAN domain names") + program.plugin.SetState(results.StatusCritical, "names missing from SAN domain names") } return certificateIsOk } @@ -306,26 +306,26 @@ func (program *checkProgram) checkNames() bool { // Check a certificate's time to expiry against the warning and critical // thresholds, returning a status code and description based on these // values. -func (program *checkProgram) checkCertificateExpiry(tlDays int) (plugin.Status, string) { +func (program *checkProgram) checkCertificateExpiry(tlDays int) (results.Status, string) { if tlDays <= 0 { - return plugin.CRITICAL, "certificate expired" + return results.StatusCritical, "certificate expired" } var limitStr string - var state plugin.Status + var state results.Status switch { case program.crit > 0 && tlDays <= program.crit: limitStr = fmt.Sprintf(" (<= %d)", program.crit) - state = plugin.CRITICAL + state = results.StatusCritical case program.warn > 0 && tlDays <= program.warn: limitStr = fmt.Sprintf(" (<= %d)", program.warn) - state = plugin.WARNING + state = results.StatusWarning default: limitStr = "" - state = plugin.OK + state = results.StatusOK } statusString := fmt.Sprintf("certificate will expire in %d days%s", @@ -338,10 +338,10 @@ func (program *checkProgram) checkCertificateExpiry(tlDays int) (plugin.Status, func (program *checkProgram) setPerfData(tlDays int) { pdat := perfdata.New("validity", perfdata.UomNone, strconv.Itoa(tlDays)) if program.crit > 0 { - pdat.SetCrit(perfdata.PDRMax(strconv.Itoa(program.crit))) + pdat.SetCrit(perfdata.RangeMax(strconv.Itoa(program.crit))) } if program.warn > 0 { - pdat.SetWarn(perfdata.PDRMax(strconv.Itoa(program.warn))) + pdat.SetWarn(perfdata.RangeMax(strconv.Itoa(program.warn))) } program.plugin.AddPerfData(pdat) } @@ -351,7 +351,7 @@ func (program *checkProgram) setPerfData(tlDays int) { func (program *checkProgram) RunCheck() { err := program.getCertificate() if err != nil { - program.plugin.SetState(plugin.UNKNOWN, err.Error()) + program.plugin.SetState(results.StatusUnknown, err.Error()) } else if program.checkNames() { timeLeft := time.Until(program.certificate.NotAfter) tlDays := int((timeLeft + 86399*time.Second) / (24 * time.Hour)) diff --git a/cmd/zoneserial/main.go b/cmd/zoneserial/main.go index f00547a..f5a1f25 100644 --- a/cmd/zoneserial/main.go +++ b/cmd/zoneserial/main.go @@ -14,7 +14,7 @@ import ( "nocternity.net/gomonop/pkg/perfdata" "nocternity.net/gomonop/pkg/plugin" - "nocternity.net/gomonop/pkg/program" + "nocternity.net/gomonop/pkg/results" ) //------------------------------------------------------------------------------------------------------- @@ -55,8 +55,8 @@ type programFlags struct { // Program data including configuration and runtime data. type checkProgram struct { - programFlags // Flags from the command line - plugin *plugin.Plugin // Plugin output state + programFlags // Flags from the command line + plugin *results.Results // Plugin output state } // Parse command line arguments and store their values. If the -h flag is present, @@ -77,43 +77,39 @@ func (flags *programFlags) parseArguments() { } // Initialise the monitoring check program. -func NewProgram() program.Program { +func NewProgram() plugin.Plugin { program := &checkProgram{ - plugin: plugin.New("DNS zone serial match check"), + plugin: results.New("DNS zone serial match check"), } program.parseArguments() return program } -// Terminate the monitoring check program. -func (program *checkProgram) Done() { - if r := recover(); r != nil { - program.plugin.SetState(plugin.UNKNOWN, "Internal error") - program.plugin.AddLinef("Error info: %v", r) - } - program.plugin.Done() +// Return the program's output value. +func (program *checkProgram) Results() *results.Results { + return program.plugin } // Check the values that were specified from the command line. Returns true if the arguments made sense. func (program *checkProgram) CheckArguments() bool { if program.hostname == "" { - program.plugin.SetState(plugin.UNKNOWN, "no DNS hostname specified") + program.plugin.SetState(results.StatusUnknown, "no DNS hostname specified") return false } if program.port < 1 || program.port > 65535 { - program.plugin.SetState(plugin.UNKNOWN, "invalid DNS port number") + program.plugin.SetState(results.StatusUnknown, "invalid DNS port number") return false } if program.zone == "" { - program.plugin.SetState(plugin.UNKNOWN, "no DNS zone specified") + program.plugin.SetState(results.StatusUnknown, "no DNS zone specified") return false } if program.rsHostname == "" { - program.plugin.SetState(plugin.UNKNOWN, "no reference DNS hostname specified") + program.plugin.SetState(results.StatusUnknown, "no reference DNS hostname specified") return false } if program.rsPort < 1 || program.rsPort > 65535 { - program.plugin.SetState(plugin.UNKNOWN, "invalid reference DNS port number") + program.plugin.SetState(results.StatusUnknown, "invalid reference DNS port number") return false } program.hostname = strings.ToLower(program.hostname) @@ -180,12 +176,12 @@ func (program *checkProgram) RunCheck() { cOk, cSerial := program.getSerial("checked", checkResponse) rOk, rSerial := program.getSerial("reference", refResponse) if !(cOk && rOk) { - program.plugin.SetState(plugin.UNKNOWN, "could not read serials") + program.plugin.SetState(results.StatusUnknown, "could not read serials") return } if cSerial == rSerial { - program.plugin.SetState(plugin.OK, "serials match") + program.plugin.SetState(results.StatusOK, "serials match") } else { - program.plugin.SetState(plugin.CRITICAL, "serials mismatch") + program.plugin.SetState(results.StatusCritical, "serials mismatch") } } diff --git a/go.mod b/go.mod index fbbf082..111d2ca 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,11 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect golang.org/x/net v0.0.0-20190923162816-aa69164e4478 // indirect golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 17c7bb2..37c20f7 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,13 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/karrick/golf v1.4.0 h1:9i9HnUh7uCyUFJhIqg311HBibw4f2pbGldi0ZM2FhaQ= github.com/karrick/golf v1.4.0/go.mod h1:qGN0IhcEL+IEgCXp00RvH32UP59vtwc8w5YcIdArNRk= github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA= github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -19,3 +25,6 @@ golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index e8ce79a..fd15700 100644 --- a/main.go +++ b/main.go @@ -7,33 +7,34 @@ import ( "nocternity.net/gomonop/cmd/sslcert" "nocternity.net/gomonop/cmd/zoneserial" - "nocternity.net/gomonop/pkg/program" + "nocternity.net/gomonop/pkg/plugin" + "nocternity.net/gomonop/pkg/results" "nocternity.net/gomonop/pkg/version" ) var ( - programs map[string]program.Builder = map[string]program.Builder{ + plugins map[string]plugin.Builder = map[string]plugin.Builder{ "check_ssl_certificate": sslcert.NewProgram, "check_zone_serial": zoneserial.NewProgram, } ) -func getProgram() program.Program { +func getPlugin() plugin.Plugin { ownName := filepath.Base(os.Args[0]) - if builder, ok := programs[ownName]; ok { + if builder, ok := plugins[ownName]; ok { return builder() } if len(os.Args) < 2 { - fmt.Printf("Syntax: %s [arguments]\n", ownName) - fmt.Printf(" %s --programs|-p\n", ownName) + fmt.Printf("Syntax: %s [arguments]\n", ownName) + fmt.Printf(" %s --plugin|-p\n", ownName) fmt.Printf(" %s --version|-v", ownName) } switch os.Args[1] { - case "--programs", "-p": - for name := range programs { + case "--plugins", "-p": + for name := range plugins { fmt.Println(name) } os.Exit(0) @@ -43,20 +44,30 @@ func getProgram() program.Program { os.Exit(0) } - if builder, ok := programs[os.Args[1]]; ok { + if builder, ok := plugins[os.Args[1]]; ok { os.Args = os.Args[1:] return builder() } - fmt.Printf("Unknown program: %s\n", os.Args[1]) + fmt.Printf("Unknown plugin: %s\n", os.Args[1]) os.Exit(1) return nil } func main() { - program := getProgram() - defer program.Done() - if program.CheckArguments() { - program.RunCheck() + runPlugin := getPlugin() + + output := runPlugin.Results() + defer func() { + if r := recover(); r != nil { + output.SetState(results.StatusUnknown, "Internal error") + output.AddLinef("Error info: %v", r) + } + fmt.Println(output.String()) + os.Exit(output.ExitCode()) + }() + + if runPlugin.CheckArguments() { + runPlugin.RunCheck() } } diff --git a/pkg/perfdata/internals.go b/pkg/perfdata/internals.go new file mode 100644 index 0000000..0f5f0da --- /dev/null +++ b/pkg/perfdata/internals.go @@ -0,0 +1,25 @@ +package perfdata // import nocternity.net/gomonop/pkg/perfdata + +import ( + "regexp" +) + +// Flags indicating which elements of performance data have been set. +type perfDataBits int + +const ( + PDatWarn perfDataBits = 1 << iota + PDatCrit + PDatMin + PDatMax +) + +// Regexps used to check values and ranges in performance data records. +var ( + // Common value check regexp. + vcRegexp = `^-?(0(\.\d*)?|[1-9]\d*(\.\d*)?|\.\d+)$` + // Compiled value check regexp. + valueCheck = regexp.MustCompile(vcRegexp) + // Compiled range min value check. + rangeMinCheck = regexp.MustCompile(vcRegexp + `|^~$`) +) diff --git a/pkg/perfdata/internals_test.go b/pkg/perfdata/internals_test.go new file mode 100644 index 0000000..8f77c52 --- /dev/null +++ b/pkg/perfdata/internals_test.go @@ -0,0 +1,50 @@ +package perfdata // import nocternity.net/gomonop/pkg/perfdata + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValueCheckValid(t *testing.T) { + validValues := []string{ + "0", "0.", "0.952", "1", "123", "123.", "123.45", ".1", + "-0", "-0.", "-0.952", "-1", "-123", "-123.", "-123.45", "-.1", + } + + for _, value := range validValues { + assert.True(t, valueCheck.MatchString(value), "'%s' is a valid value string", value) + } +} + +func TestValueCheckInvalid(t *testing.T) { + invalidValues := []string{".", "-.", "a", " ", "", "~"} + + for _, value := range invalidValues { + assert.False(t, valueCheck.MatchString(value), "'%s' is an invalid value string", value) + } +} + +func TestMinCheckValid(t *testing.T) { + validValues := []string{ + "0", "0.", "0.952", "1", "123", "123.", "123.45", ".1", + "-0", "-0.", "-0.952", "-1", "-123", "-123.", "-123.45", "-.1", + "~", + } + + for _, value := range validValues { + if !rangeMinCheck.MatchString(value) { + t.Errorf("'%s' is a valid value string", value) + } + } +} + +func TestMinCheckInvalid(t *testing.T) { + invalidValues := []string{".", "-.", "a", " ", ""} + + for _, value := range invalidValues { + if rangeMinCheck.MatchString(value) { + t.Errorf("'%s' is an invalid value string", value) + } + } +} diff --git a/pkg/perfdata/perfdata.go b/pkg/perfdata/perfdata.go index bc7195b..fc1914e 100644 --- a/pkg/perfdata/perfdata.go +++ b/pkg/perfdata/perfdata.go @@ -3,111 +3,9 @@ package perfdata // import nocternity.net/gomonop/pkg/perfdata import ( - "fmt" - "regexp" "strings" ) -// Units of measurement, which may be used to qualify the performance data. -type UnitOfMeasurement int - -const ( - UomNone UnitOfMeasurement = iota - UomSeconds - UomPercent - UomBytes - UomKilobytes - UomMegabytes - UomGigabytes - UomTerabytes - UomCounter -) - -func (u UnitOfMeasurement) String() string { - return [...]string{"", "s", "%", "B", "KB", "MB", "GB", "TB", "c"}[u] -} - -// Flags indicating which elements of performance data have been set. -type perfDataBits int - -const ( - PDatWarn perfDataBits = 1 << iota - PDatCrit - PDatMin - PDatMax -) - -// Regexps used to check values and ranges in performance data records. -var ( - // Common value check regexp. - vcRegexp = `^-?(0(\.\d*)?|[1-9]\d*(\.\d*)?|\.\d+)$` - // Compiled value check regexp. - valueCheck = regexp.MustCompile(vcRegexp) - // Compiled range min value check. - rangeMinCheck = regexp.MustCompile(vcRegexp + `|^~$`) -) - -// Performance data range. -type PDRange struct { - start string - end string - inside bool -} - -// Creates a performance data range from -inf to 0 and from the specified -// value to +inf. -func PDRMax(max string) *PDRange { - if !valueCheck.MatchString(max) { - panic("invalid performance data range maximum value") - } - pdRange := &PDRange{} - pdRange.start = "0" - pdRange.end = max - return pdRange -} - -// Creates a performance data range from -inf to the specified minimal value -// and from the specified maximal value to +inf. -func PDRMinMax(min, max string) *PDRange { - if !valueCheck.MatchString(max) { - panic("invalid performance data range maximum value") - } - if !rangeMinCheck.MatchString(min) { - panic("invalid performance data range minimum value") - } - pdRange := &PDRange{} - pdRange.start = min - pdRange.end = max - return pdRange -} - -// Inverts the range. -func (r *PDRange) Inside() *PDRange { - r.inside = true - return r -} - -// Generates the range's string representation so it can be sent to the -// monitoring system. -func (r *PDRange) String() string { - var start, inside string - - switch r.start { - case "": - start = "~" - case "0": - start = "" - default: - start = r.start - } - - if r.inside { - inside = "@" - } - - return inside + start + ":" + r.end -} - // Performance data, including a label, units, a value, warning/critical // ranges and min/max boundaries. type PerfData struct { @@ -115,7 +13,7 @@ type PerfData struct { units UnitOfMeasurement bits perfDataBits value string - warn, crit PDRange + warn, crit Range min, max string } @@ -136,13 +34,13 @@ func New(label string, units UnitOfMeasurement, value string) *PerfData { } // Set the warning range for the performance data record. -func (d *PerfData) SetWarn(r *PDRange) { +func (d *PerfData) SetWarn(r *Range) { d.warn = *r d.bits |= PDatWarn } // Set the critical range for the performance data record. -func (d *PerfData) SetCrit(r *PDRange) { +func (d *PerfData) SetCrit(r *Range) { d.crit = *r d.bits |= PDatCrit } @@ -171,26 +69,28 @@ func (d *PerfData) String() string { var strBuilder strings.Builder needsQuotes := strings.ContainsAny(d.Label, " '=\"") if needsQuotes { - strBuilder.WriteString("'") + strBuilder.WriteRune('\'') } strBuilder.WriteString(strings.ReplaceAll(d.Label, "'", "''")) if needsQuotes { - strBuilder.WriteString("'") + strBuilder.WriteRune('\'') } - strBuilder.WriteString("=") - strBuilder.WriteString(fmt.Sprintf("%s%s;", d.value, d.units.String())) + strBuilder.WriteRune('=') + strBuilder.WriteString(d.value) + strBuilder.WriteString(d.units.String()) + strBuilder.WriteRune(';') if d.bits&PDatWarn != 0 { strBuilder.WriteString(d.warn.String()) } - strBuilder.WriteString(";") + strBuilder.WriteRune(';') if d.bits&PDatCrit != 0 { strBuilder.WriteString(d.crit.String()) } - strBuilder.WriteString(";") + strBuilder.WriteRune(';') if d.bits&PDatMin != 0 { strBuilder.WriteString(d.min) } - strBuilder.WriteString(";") + strBuilder.WriteRune(';') if d.bits&PDatMax != 0 { strBuilder.WriteString(d.max) } diff --git a/pkg/perfdata/perfdata_test.go b/pkg/perfdata/perfdata_test.go index 0c0fe2f..894077c 100644 --- a/pkg/perfdata/perfdata_test.go +++ b/pkg/perfdata/perfdata_test.go @@ -1,108 +1,261 @@ -package perfdata // import nocternity.net/gomonop/pkg/perfdata +package perfdata import ( "fmt" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func assert(t *testing.T, check bool, msg string) { - if !check { - t.Errorf(msg) - } +func TestNewNoValue(t *testing.T) { + const label = "label" + const units = UomNone + + out := New(label, units, "") + + assert.Equal(t, label, out.Label) + assert.Equal(t, units, out.units) + assert.Equal(t, perfDataBits(0), out.bits) + assert.Equal(t, "U", out.value) + // min, max, warn and crit are meaningless here } -func assertPanic(t *testing.T, f func(), msg string) { - defer func() { - if r := recover(); r == nil { - t.Errorf(msg) - } - }() - f() +func TestNewValidValue(t *testing.T) { + const label = "label" + const units = UomNone + const value = "1234" + + out := New(label, units, value) + + assert.Equal(t, label, out.Label) + assert.Equal(t, units, out.units) + assert.Equal(t, perfDataBits(0), out.bits) + assert.Equal(t, value, out.value) + // min, max, warn and crit are meaningless here } -func TestValueCheckValid(t *testing.T) { - validValues := []string{ - "0", "0.", "0.952", "1", "123", "123.", "123.45", ".1", - "-0", "-0.", "-0.952", "-1", "-123", "-123.", "-123.45", "-.1", - } - - for _, value := range validValues { - if !valueCheck.MatchString(value) { - t.Errorf("'%s' is a valid value string", value) - } - } +func TestNewInvalidValue(t *testing.T) { + assert.Panics(t, func() { New("label", UomNone, "nope") }) } -func TestValueCheckInvalid(t *testing.T) { - invalidValues := []string{".", "-.", "a", " ", ""} +func TestSetWarn(t *testing.T) { + rangeValue := Range{start: "A", end: "B"} + rangeStr := rangeValue.String() - for _, value := range invalidValues { - if valueCheck.MatchString(value) { - t.Errorf("'%s' is an invalid value string", value) - } - } + data := PerfData{} + data.SetWarn(&rangeValue) + + assert.True(t, data.bits&PDatWarn != 0) + assert.Equal(t, rangeStr, data.warn.String()) } -func TestPdrMaxInvalid(t *testing.T) { - assertPanic( - t, func() { PDRMax("") }, - "Created PerfDataRange with invalid max value", - ) +func TestSetWarnTwice(t *testing.T) { + range1Value := Range{start: "A", end: "B"} + range2Value := Range{start: "C", end: "D"} + range2Str := range2Value.String() + require.NotEqual(t, range2Str, range1Value.String()) + + data := PerfData{} + data.SetWarn(&range1Value) + data.SetWarn(&range2Value) + + assert.True(t, data.bits&PDatWarn != 0) + assert.Equal(t, range2Str, data.warn.String()) } -func TestPdrMax(t *testing.T) { - value := "123" - pdr := PDRMax(value) - assert(t, pdr.start == "0", "Min value should be '0'") - assert(t, pdr.end == value, "Max value not copied to PerfDataRange") - assert(t, !pdr.inside, "Inside flag should not be set") +func TestSetCrit(t *testing.T) { + rangeValue := Range{start: "A", end: "B"} + rangeStr := rangeValue.String() + + data := PerfData{} + data.SetCrit(&rangeValue) + + assert.True(t, data.bits&PDatCrit != 0) + assert.Equal(t, rangeStr, data.crit.String()) } -func TestPdrMinMaxInvalid(t *testing.T) { - assertPanic( - t, func() { PDRMinMax("", "123") }, - "Created PerfDataRange with invalid min value", - ) - assertPanic( - t, func() { PDRMinMax("123", "") }, - "Created PerfDataRange with invalid max value", - ) +func TestSetCritTwice(t *testing.T) { + range1Value := Range{start: "A", end: "B"} + range2Value := Range{start: "C", end: "D"} + range2Str := range2Value.String() + require.NotEqual(t, range2Str, range1Value.String()) + + data := PerfData{} + data.SetCrit(&range1Value) + data.SetCrit(&range2Value) + + assert.True(t, data.bits&PDatCrit != 0) + assert.Equal(t, range2Str, data.crit.String()) } -func TestPdrMinMax(t *testing.T) { - min, max := "123", "456" - pdr := PDRMinMax(min, max) - assert(t, pdr.start == min, "Min value not copied to PerfDataRange") - assert(t, pdr.end == max, "Max value not copied to PerfDataRange") - assert(t, !pdr.inside, "Inside flag should not be set") +func TestSetMin(t *testing.T) { + const min = "100" + + data := PerfData{} + data.SetMin(min) + + assert.True(t, data.bits&PDatMin != 0) + assert.Equal(t, min, data.min) } -func TestPdrInside(t *testing.T) { - pdr := &PDRange{} - pdr = pdr.Inside() - assert(t, pdr.inside, "Inside flag should be set") - pdr = pdr.Inside() - assert(t, pdr.inside, "Inside flag should still be set") +func TestSetMinInvalid(t *testing.T) { + data := PerfData{} + assert.Panics(t, func() { data.SetMin("nope") }) } -func TestPdrString(t *testing.T) { +func TestSetMinTwice(t *testing.T) { + data := PerfData{} + data.SetMin("100") + data.SetMin("200") + assert.Equal(t, "200", data.min) + assert.True(t, data.bits&PDatMin != 0) +} + +func TestSetMax(t *testing.T) { + const max = "100" + + data := PerfData{} + data.SetMax(max) + + assert.True(t, data.bits&PDatMax != 0) + assert.Equal(t, max, data.max) +} + +func TestSetMaxInvalid(t *testing.T) { + data := PerfData{} + assert.Panics(t, func() { data.SetMax("nope") }) +} + +func TestSetMaxTwice(t *testing.T) { + data := PerfData{} + data.SetMax("100") + data.SetMax("200") + assert.Equal(t, "200", data.max) + assert.True(t, data.bits&PDatMax != 0) +} + +func TestString(t *testing.T) { type Test struct { - pdr PDRange - out string + PerfData + expected string } + + range1 := Range{start: "A", end: "B"} + range2 := Range{start: "C", end: "D"} + tests := []Test{ - {pdr: PDRange{start: "Y", end: "X"}, out: "Y:X"}, - {pdr: PDRange{end: "X"}, out: "~:X"}, - {pdr: PDRange{start: "0", end: "X"}, out: ":X"}, - {pdr: PDRange{inside: true, start: "Y", end: "X"}, out: "@Y:X"}, + { + PerfData{ + Label: "label", + units: UomNone, + bits: perfDataBits(0), + value: "1234", + }, + "label=1234;;;;", + }, + { + PerfData{ + Label: "la=bel", + units: UomNone, + bits: perfDataBits(0), + value: "1234", + }, + "'la=bel'=1234;;;;", + }, + { + PerfData{ + Label: "la bel", + units: UomNone, + bits: perfDataBits(0), + value: "1234", + }, + "'la bel'=1234;;;;", + }, + { + PerfData{ + Label: "la\"bel", + units: UomNone, + bits: perfDataBits(0), + value: "1234", + }, + "'la\"bel'=1234;;;;", + }, + { + PerfData{ + Label: "la'bel", + units: UomNone, + bits: perfDataBits(0), + value: "1234", + }, + "'la''bel'=1234;;;;", + }, + { + PerfData{ + Label: "label", + units: UomNone, + bits: PDatWarn, + value: "1234", + warn: range1, + }, + "label=1234;" + range1.String() + ";;;", + }, + { + PerfData{ + Label: "label", + units: UomNone, + bits: PDatCrit, + value: "1234", + crit: range1, + }, + "label=1234;;" + range1.String() + ";;", + }, + { + PerfData{ + Label: "label", + units: UomNone, + bits: PDatWarn | PDatCrit, + value: "1234", + warn: range1, + crit: range2, + }, + "label=1234;" + range1.String() + ";" + range2.String() + ";;", + }, + { + PerfData{ + Label: "label", + units: UomNone, + bits: PDatMin, + value: "1234", + min: "X", + }, + "label=1234;;;X;", + }, + { + PerfData{ + Label: "label", + units: UomNone, + bits: PDatMax, + value: "1234", + max: "Y", + }, + "label=1234;;;;Y", + }, + } + + for _, units := range []UnitOfMeasurement{UomSeconds, UomPercent, UomBytes, UomKilobytes, UomMegabytes, UomGigabytes, UomTerabytes, UomCounter} { + tests = append(tests, Test{ + PerfData{ + Label: "label", + units: units, + bits: perfDataBits(0), + value: "1234", + }, + fmt.Sprintf("label=1234%s;;;;", units), + }) } for _, test := range tests { - result := test.pdr.String() - assert( - t, - result == test.out, - fmt.Sprintf("Expected '%s', got '%s'", test.out, result), - ) + assert.Equal(t, test.expected, test.PerfData.String()) } } diff --git a/pkg/perfdata/range.go b/pkg/perfdata/range.go new file mode 100644 index 0000000..e4ad2c7 --- /dev/null +++ b/pkg/perfdata/range.go @@ -0,0 +1,62 @@ +package perfdata // import nocternity.net/gomonop/pkg/perfdata + +// Performance data range. +type Range struct { + start string + end string + inside bool +} + +// Creates a performance data range from -inf to 0 and from the specified +// value to +inf. +func RangeMax(max string) *Range { + if !valueCheck.MatchString(max) { + panic("invalid performance data range maximum value") + } + pdRange := &Range{} + pdRange.start = "0" + pdRange.end = max + return pdRange +} + +// Creates a performance data range from -inf to the specified minimal value +// and from the specified maximal value to +inf. +func RangeMinMax(min, max string) *Range { + if !valueCheck.MatchString(max) { + panic("invalid performance data range maximum value") + } + if !rangeMinCheck.MatchString(min) { + panic("invalid performance data range minimum value") + } + pdRange := &Range{} + pdRange.start = min + pdRange.end = max + return pdRange +} + +// Inverts the range. +func (r *Range) Inside() *Range { + r.inside = true + return r +} + +// Generates the range's string representation so it can be sent to the +// monitoring system. +func (r *Range) String() string { + var start, inside string + + switch r.start { + case "": + start = "~" + case "0": + start = "" + default: + start = r.start + } + + if r.inside { + inside = "@" + } + + return inside + start + ":" + r.end +} diff --git a/pkg/perfdata/range_test.go b/pkg/perfdata/range_test.go new file mode 100644 index 0000000..26d1f21 --- /dev/null +++ b/pkg/perfdata/range_test.go @@ -0,0 +1,67 @@ +package perfdata // import nocternity.net/gomonop/pkg/perfdata + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRangeMaxInvalid(t *testing.T) { + assert.Panics( + t, func() { RangeMax("") }, + "Created PerfDataRange with invalid max value", + ) +} + +func TestRangeMax(t *testing.T) { + value := "123" + pdr := RangeMax(value) + assert.Equal(t, "0", pdr.start, "Min value should be '0'") + assert.Equal(t, value, pdr.end, "Max value not copied to PerfDataRange") + assert.False(t, pdr.inside, "Inside flag should not be set") +} + +func TestRangeMinMaxInvalid(t *testing.T) { + assert.Panics( + t, func() { RangeMinMax("", "123") }, + "Created PerfDataRange with invalid min value", + ) + assert.Panics( + t, func() { RangeMinMax("123", "") }, + "Created PerfDataRange with invalid max value", + ) +} + +func TestRangeMinMax(t *testing.T) { + min, max := "123", "456" + pdr := RangeMinMax(min, max) + assert.Equal(t, min, pdr.start, "Min value not copied to PerfDataRange") + assert.Equal(t, max, pdr.end, "Max value not copied to PerfDataRange") + assert.False(t, pdr.inside, "Inside flag should not be set") +} + +func TestRangeInside(t *testing.T) { + pdr := &Range{} + pdr = pdr.Inside() + assert.True(t, pdr.inside, "Inside flag should be set") + pdr = pdr.Inside() + assert.True(t, pdr.inside, "Inside flag should still be set") +} + +func TestRangeString(t *testing.T) { + type Test struct { + pdr Range + out string + } + tests := []Test{ + {pdr: Range{start: "Y", end: "X"}, out: "Y:X"}, + {pdr: Range{end: "X"}, out: "~:X"}, + {pdr: Range{start: "0", end: "X"}, out: ":X"}, + {pdr: Range{inside: true, start: "Y", end: "X"}, out: "@Y:X"}, + } + + for _, test := range tests { + result := test.pdr.String() + assert.Equal(t, test.out, result, "Expected '%s', got '%s'", test.out, result) + } +} diff --git a/pkg/perfdata/units.go b/pkg/perfdata/units.go new file mode 100644 index 0000000..604625c --- /dev/null +++ b/pkg/perfdata/units.go @@ -0,0 +1,20 @@ +package perfdata // import nocternity.net/gomonop/pkg/perfdata + +// Units of measurement, which may be used to qualify the performance data. +type UnitOfMeasurement int + +const ( + UomNone UnitOfMeasurement = iota + UomSeconds + UomPercent + UomBytes + UomKilobytes + UomMegabytes + UomGigabytes + UomTerabytes + UomCounter +) + +func (u UnitOfMeasurement) String() string { + return [...]string{"", "s", "%", "B", "KB", "MB", "GB", "TB", "c"}[u] +} diff --git a/pkg/perfdata/units_test.go b/pkg/perfdata/units_test.go new file mode 100644 index 0000000..b5b56a5 --- /dev/null +++ b/pkg/perfdata/units_test.go @@ -0,0 +1,26 @@ +package perfdata // import nocternity.net/gomonop/pkg/perfdata + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestUnitsToString(t *testing.T) { + checks := map[UnitOfMeasurement]string{ + UomNone: "", + UomSeconds: "s", + UomPercent: "%", + UomBytes: "B", + UomKilobytes: "KB", + UomMegabytes: "MB", + UomGigabytes: "GB", + UomTerabytes: "TB", + UomCounter: "c", + } + + for u, s := range checks { + result := u.String() + assert.Equal(t, s, result, "Expected '%s', got '%s'", s, result) + } +} diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index f32e360..a60366f 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -1,122 +1,20 @@ -// Package plugin implements a helper that can be used to implement a Nagios, -// Centreon, Icinga... service monitoring plugin. -package plugin // import nocternity.net/gomonop/pkg/perfdata +package plugin // import nocternity.net/gomonop/pkg/plugin -import ( - "container/list" - "fmt" - "os" - "strings" +import "nocternity.net/gomonop/pkg/results" - "nocternity.net/gomonop/pkg/perfdata" -) +// Plugin represents the interface to a monitoring plugin. +type Plugin interface { + // Results accesses the results of the monitoring plugin. + Results() *results.Results -// Status represents the return status of the monitoring plugin. The -// corresponding integer value will be used as the program's exit code, -// to be interpreted by the monitoring system. -type Status int + // CheckArguments ensures that the arguments that were passed to the plugin + // actually make sense. Errors should be stored in the plugin's results. + CheckArguments() bool -// Plugin exit statuses. -const ( - OK Status = iota - WARNING - CRITICAL - UNKNOWN -) - -// String representations of the plugin statuses. -func (s Status) String() string { - return [...]string{"OK", "WARNING", "ERROR", "UNKNOWN"}[s] + // RunCheck actually runs whatever checks are implemented by the plugin and + // updates the results accordingly. + RunCheck() } -// Plugin represents the monitoring plugin's state, including its name, -// return status and message, additional lines of text, and performance -// data to be encoded in the output. -type Plugin struct { - name string - status Status - message string - extraText *list.List - perfData map[string]*perfdata.PerfData -} - -// New creates the plugin with `name` as its name and an unknown status. -func New(name string) *Plugin { - p := new(Plugin) - p.name = name - p.status = UNKNOWN - p.message = "no status set" - p.perfData = make(map[string]*perfdata.PerfData) - return p -} - -// SetState sets the plugin's output code to `status` and its message to -// the specified `message`. -func (p *Plugin) SetState(status Status, message string) { - p.status = status - p.message = message -} - -// AddLine adds the specified string to the extra output text buffer. -func (p *Plugin) AddLine(line string) { - if p.extraText == nil { - p.extraText = list.New() - } - p.extraText.PushBack(line) -} - -// AddLinef formats the input and adds it to the text buffer. -func (p *Plugin) AddLinef(format string, data ...interface{}) { - p.AddLine(fmt.Sprintf(format, data...)) -} - -// AddLines add the specified `lines` to the output text. -func (p *Plugin) AddLines(lines []string) { - for _, line := range lines { - p.AddLine(line) - } -} - -// AddPerfData adds performance data described by the "pd" argument to the -// output's performance data. If two performance data records are added for -// the same label, the program panics. -func (p *Plugin) AddPerfData(pd *perfdata.PerfData) { - _, exists := p.perfData[pd.Label] - if exists { - panic("duplicate performance data " + pd.Label) - } - p.perfData[pd.Label] = pd -} - -// Done generates the plugin's text output from its name, status, text data -// and performance data, before exiting with the code corresponding to the -// status. -func (p *Plugin) Done() { - var strBuilder strings.Builder - strBuilder.WriteString(p.name) - strBuilder.WriteString(" ") - strBuilder.WriteString(p.status.String()) - strBuilder.WriteString(": ") - strBuilder.WriteString(p.message) - if len(p.perfData) > 0 { - strBuilder.WriteString(" | ") - needSep := false - for _, data := range p.perfData { - if needSep { - strBuilder.WriteString(", ") - } else { - needSep = true - } - strBuilder.WriteString(data.String()) - } - } - if p.extraText != nil { - for em := p.extraText.Front(); em != nil; em = em.Next() { - strBuilder.WriteString("\n") - //nolint:forcetypeassert // we want to panic if this isn't a string - strBuilder.WriteString(em.Value.(string)) - } - } - fmt.Println(strBuilder.String()) - os.Exit(int(p.status)) -} +// Builder is a function that can be called in order to instantiate a plugin. +type Builder func() Plugin diff --git a/pkg/program/program.go b/pkg/program/program.go deleted file mode 100644 index d8b3510..0000000 --- a/pkg/program/program.go +++ /dev/null @@ -1,9 +0,0 @@ -package program // import nocternity.net/gomonop/pkg/program - -type Program interface { - CheckArguments() bool - RunCheck() - Done() -} - -type Builder func() Program diff --git a/pkg/results/results.go b/pkg/results/results.go new file mode 100644 index 0000000..3118071 --- /dev/null +++ b/pkg/results/results.go @@ -0,0 +1,107 @@ +// Package results implements a helper that can be used to store the results of +// a Nagios, Centreon, Icinga... service monitoring plugin, and convert them to +// text which can be sent to the monitoring server. +package results // import nocternity.net/gomonop/pkg/results + +import ( + "container/list" + "fmt" + "strings" + + "nocternity.net/gomonop/pkg/perfdata" +) + +// Results represents the monitoring plugin's results, including its name, +// return status and message, additional lines of text, and performance +// data to be encoded in the output. +type Results struct { + name string + status Status + message string + extraText *list.List + perfData map[string]*perfdata.PerfData +} + +// New creates the plugin with `name` as its name and an unknown status. +func New(name string) *Results { + p := new(Results) + p.name = name + p.status = StatusUnknown + p.message = "no status set" + p.perfData = make(map[string]*perfdata.PerfData) + return p +} + +// SetState sets the plugin's output code to `status` and its message to +// the specified `message`. +func (p *Results) SetState(status Status, message string) { + p.status = status + p.message = message +} + +// AddLine adds the specified string to the extra output text buffer. +func (p *Results) AddLine(line string) { + if p.extraText == nil { + p.extraText = list.New() + } + p.extraText.PushBack(line) +} + +// AddLinef formats the input and adds it to the text buffer. +func (p *Results) AddLinef(format string, data ...interface{}) { + p.AddLine(fmt.Sprintf(format, data...)) +} + +// AddLines add the specified `lines` to the output text. +func (p *Results) AddLines(lines []string) { + for _, line := range lines { + p.AddLine(line) + } +} + +// AddPerfData adds performance data described by the "pd" argument to the +// output's performance data. If two performance data records are added for +// the same label, the program panics. +func (p *Results) AddPerfData(pd *perfdata.PerfData) { + _, exists := p.perfData[pd.Label] + if exists { + panic("duplicate performance data " + pd.Label) + } + p.perfData[pd.Label] = pd +} + +// String generates the plugin's text output from its name, status, text data +// and performance data. +func (p *Results) String() string { + var strBuilder strings.Builder + strBuilder.WriteString(p.name) + strBuilder.WriteString(" ") + strBuilder.WriteString(p.status.String()) + strBuilder.WriteString(": ") + strBuilder.WriteString(p.message) + if len(p.perfData) > 0 { + strBuilder.WriteString(" | ") + needSep := false + for _, data := range p.perfData { + if needSep { + strBuilder.WriteString(", ") + } else { + needSep = true + } + strBuilder.WriteString(data.String()) + } + } + if p.extraText != nil { + for em := p.extraText.Front(); em != nil; em = em.Next() { + strBuilder.WriteString("\n") + //nolint:forcetypeassert // we want to panic if this isn't a string + strBuilder.WriteString(em.Value.(string)) + } + } + return strBuilder.String() +} + +// ExitCode returns the plugin's exit code. +func (p *Results) ExitCode() int { + return int(p.status) +} diff --git a/pkg/results/results_test.go b/pkg/results/results_test.go new file mode 100644 index 0000000..32ddacf --- /dev/null +++ b/pkg/results/results_test.go @@ -0,0 +1,175 @@ +package results // import nocternity.net/gomonop/pkg/results + +import ( + "container/list" + "testing" + + "github.com/stretchr/testify/assert" + "nocternity.net/gomonop/pkg/perfdata" +) + +func TestNew(t *testing.T) { + p := New("test") + + assert.Equal(t, p.name, "test") + assert.Equal(t, p.status, StatusUnknown) + assert.Equal(t, p.message, "no status set") + assert.Nil(t, p.extraText) + assert.NotNil(t, p.perfData) +} + +func TestSetState(t *testing.T) { + p := Results{} + + p.SetState(StatusWarning, "test") + + assert.Equal(t, p.status, StatusWarning) + assert.Equal(t, p.message, "test") +} + +func TestAddLineFirst(t *testing.T) { + p := Results{} + + p.AddLine("test") + + assert.NotNil(t, p.extraText) + assert.Equal(t, p.extraText.Len(), 1) + assert.Equal(t, p.extraText.Front().Value, "test") +} + +func TestAddLineNext(t *testing.T) { + p := Results{} + p.extraText = list.New() + + p.AddLine("test") + + assert.Equal(t, p.extraText.Len(), 1) + assert.Equal(t, p.extraText.Front().Value, "test") +} + +func TestAddLinef(t *testing.T) { + p := Results{} + + p.AddLinef("test %d", 123) + + assert.Equal(t, p.extraText.Len(), 1) + assert.Equal(t, p.extraText.Front().Value, "test 123") +} + +func TestAddLines(t *testing.T) { + p := Results{} + + p.AddLines([]string{"test", "test2"}) + + assert.Equal(t, p.extraText.Len(), 2) + assert.Equal(t, p.extraText.Front().Value, "test") + assert.Equal(t, p.extraText.Front().Next().Value, "test2") +} + +func TestAddPerfData(t *testing.T) { + p := Results{} + p.perfData = make(map[string]*perfdata.PerfData) + + p.AddPerfData(&perfdata.PerfData{Label: "test"}) + + value, ok := p.perfData["test"] + assert.True(t, ok) + assert.Equal(t, value.Label, "test") +} + +func TestAddPerfDataDuplicate(t *testing.T) { + p := Results{} + p.perfData = make(map[string]*perfdata.PerfData) + p.perfData["test"] = &perfdata.PerfData{Label: "test"} + + assert.Panics(t, func() { p.AddPerfData(&perfdata.PerfData{Label: "test"}) }) +} + +func TestString(t *testing.T) { + type Test struct { + Results + expected string + } + + pdat := perfdata.PerfData{Label: "test"} + tests := []Test{ + { + Results{ + name: "test", + status: StatusWarning, + message: "test", + perfData: make(map[string]*perfdata.PerfData), + }, + "test WARNING: test", + }, + { + func() Results { + p := Results{ + name: "test", + status: StatusWarning, + message: "test", + perfData: make(map[string]*perfdata.PerfData), + extraText: list.New(), + } + p.extraText.PushBack("test 1") + p.extraText.PushBack("test 2") + return p + }(), + "test WARNING: test\ntest 1\ntest 2", + }, + { + func() Results { + p := Results{ + name: "test", + status: StatusWarning, + message: "test", + perfData: make(map[string]*perfdata.PerfData), + } + p.perfData["test 1"] = &pdat + p.perfData["test 2"] = &pdat + return p + }(), + "test WARNING: test | " + pdat.String() + ", " + + pdat.String(), + }, + { + func() Results { + p := Results{ + name: "test", + status: StatusWarning, + message: "test", + perfData: make(map[string]*perfdata.PerfData), + extraText: list.New(), + } + p.perfData["test 1"] = &pdat + p.perfData["test 2"] = &pdat + p.extraText.PushBack("test 1") + p.extraText.PushBack("test 2") + return p + }(), + "test WARNING: test | " + pdat.String() + ", " + + pdat.String() + "\ntest 1\ntest 2", + }, + } + + for _, test := range tests { + result := test.Results.String() + assert.Equal(t, test.expected, result, "Expected '%s', got '%s'", test.expected, result) + } +} + +func TestExitCode(t *testing.T) { + p := Results{} + + p.status = StatusOK + assert.Equal(t, int(StatusOK), p.ExitCode()) + + p.status = StatusWarning + assert.Equal(t, int(StatusWarning), p.ExitCode()) + + p.status = StatusCritical + assert.Equal(t, int(StatusCritical), p.ExitCode()) + + p.status = StatusUnknown + assert.Equal(t, int(StatusUnknown), p.ExitCode()) +} diff --git a/pkg/results/status.go b/pkg/results/status.go new file mode 100644 index 0000000..6238a20 --- /dev/null +++ b/pkg/results/status.go @@ -0,0 +1,19 @@ +package results // import nocternity.net/gomonop/pkg/results + +// Status represents the return status of the monitoring plugin. The +// corresponding integer value will be used as the program's exit code, +// to be interpreted by the monitoring system. +type Status int + +// Plugin exit statuses. +const ( + StatusOK Status = iota + StatusWarning + StatusCritical + StatusUnknown +) + +// String representations of the plugin statuses. +func (s Status) String() string { + return [...]string{"OK", "WARNING", "ERROR", "UNKNOWN"}[s] +} diff --git a/pkg/results/status_test.go b/pkg/results/status_test.go new file mode 100644 index 0000000..8f81f43 --- /dev/null +++ b/pkg/results/status_test.go @@ -0,0 +1,26 @@ +package results // import nocternity.net/gomonop/pkg/results + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStatusDefaultOK(t *testing.T) { + var s Status + assert.Equal(t, StatusOK, s) +} + +func TestStatusToString(t *testing.T) { + assert.Equal(t, "OK", StatusOK.String()) + assert.Equal(t, "WARNING", StatusWarning.String()) + assert.Equal(t, "ERROR", StatusCritical.String()) + assert.Equal(t, "UNKNOWN", StatusUnknown.String()) +} + +func TestStatusToInt(t *testing.T) { + assert.Equal(t, 0, int(StatusOK)) + assert.Equal(t, 1, int(StatusWarning)) + assert.Equal(t, 2, int(StatusCritical)) + assert.Equal(t, 3, int(StatusUnknown)) +} diff --git a/pkg/version/version_test.go b/pkg/version/version_test.go new file mode 100644 index 0000000..17f96e4 --- /dev/null +++ b/pkg/version/version_test.go @@ -0,0 +1,31 @@ +package version // import nocternity.net/gomonop/pkg/version + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestVersion(t *testing.T) { + type Test struct { + version, commit, status, target, expected string + } + + tests := []Test{ + {"", "COMMIT", "clean", "TARGET", "development version (COMMIT) TARGET"}, + {"VERSION", "COMMIT", "clean", "TARGET", "VERSION (COMMIT) TARGET"}, + {"", "COMMIT", "dirty", "TARGET", "development version (COMMIT*) TARGET"}, + {"VERSION", "COMMIT", "dirty", "TARGET", "VERSION (COMMIT*) TARGET"}, + } + + for _, test := range tests { + version = test.version + commit = test.commit + status = test.status + target = test.target + + result := Version() + + assert.Equal(t, test.expected, result, "Expected '%s', got '%s'", test.expected, result) + } +}