refactor: make internals easier to test and add unit tests ()

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: 
Co-authored-by: Emmanuel BENOÎT <tseeker@nocternity.net>
Co-committed-by: Emmanuel BENOÎT <tseeker@nocternity.net>
This commit is contained in:
Emmanuel BENOîT 2024-07-20 10:01:05 +02:00 committed by Emmanuel BENOîT
parent dcd732cc34
commit 78af496fe9
20 changed files with 939 additions and 369 deletions

25
pkg/perfdata/internals.go Normal file
View file

@ -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 + `|^~$`)
)

View file

@ -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)
}
}
}

View file

@ -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)
}

View file

@ -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())
}
}

62
pkg/perfdata/range.go Normal file
View file

@ -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
}

View file

@ -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)
}
}

20
pkg/perfdata/units.go Normal file
View file

@ -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]
}

View file

@ -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)
}
}

View file

@ -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

View file

@ -1,9 +0,0 @@
package program // import nocternity.net/gomonop/pkg/program
type Program interface {
CheckArguments() bool
RunCheck()
Done()
}
type Builder func() Program

107
pkg/results/results.go Normal file
View file

@ -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)
}

175
pkg/results/results_test.go Normal file
View file

@ -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())
}

19
pkg/results/status.go Normal file
View file

@ -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]
}

View file

@ -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))
}

View file

@ -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)
}
}