feat: add the check_output_matches
plugin #5
4 changed files with 262 additions and 177 deletions
|
@ -3,7 +3,6 @@ package perfdata // import nocternity.net/gomonop/pkg/perfdata
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Performance data range.
|
// Performance data range.
|
||||||
|
@ -92,121 +91,3 @@ func (r *Range) Contains(value float64) bool {
|
||||||
|
|
||||||
return (inStart || inEnd) != r.inside
|
return (inStart || inEnd) != r.inside
|
||||||
}
|
}
|
||||||
|
|
||||||
// A state of the range parser.
|
|
||||||
type rangeParserState int
|
|
||||||
|
|
||||||
const (
|
|
||||||
rpsInit rangeParserState = iota // Initial state
|
|
||||||
rpsExpectStart // Expect the start of the range
|
|
||||||
rpsInStart // Reading the start of the range
|
|
||||||
rpsExpectColon // Expect the colon that separates the start and end
|
|
||||||
rpsExpectEnd // Expect the end of the range
|
|
||||||
rpsInEnd // Reading the end of the range
|
|
||||||
)
|
|
||||||
|
|
||||||
// An error emitted by the range parser.
|
|
||||||
type rangeParserError struct {
|
|
||||||
input string
|
|
||||||
position int
|
|
||||||
message string
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseError creates a new range parser error.
|
|
||||||
func parseError(input string, position int, message string) *rangeParserError {
|
|
||||||
return &rangeParserError{
|
|
||||||
input: input,
|
|
||||||
position: position,
|
|
||||||
message: message,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error implements the error interface for the range parser error type.
|
|
||||||
func (rpe *rangeParserError) Error() string {
|
|
||||||
return fmt.Sprintf("in `%s' at position %d: %s", rpe.input, rpe.position, rpe.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to parse a string into a performance data range.
|
|
||||||
func ParseRange(input string) (*Range, error) {
|
|
||||||
runes := []rune(input)
|
|
||||||
index := 0
|
|
||||||
state := rpsInit
|
|
||||||
parsed := Range{}
|
|
||||||
startOfStart := 0
|
|
||||||
startOfEnd := 0
|
|
||||||
strBuilder := strings.Builder{}
|
|
||||||
|
|
||||||
for index < len(runes) {
|
|
||||||
curRune := runes[index]
|
|
||||||
|
|
||||||
switch state {
|
|
||||||
case rpsInit:
|
|
||||||
if curRune == '@' {
|
|
||||||
parsed.inside = true
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
state = rpsExpectStart
|
|
||||||
|
|
||||||
case rpsExpectStart:
|
|
||||||
switch curRune {
|
|
||||||
case ':':
|
|
||||||
parsed.start = "0"
|
|
||||||
state = rpsExpectEnd
|
|
||||||
case '~':
|
|
||||||
state = rpsExpectColon
|
|
||||||
default:
|
|
||||||
strBuilder.WriteRune(curRune)
|
|
||||||
startOfStart = index
|
|
||||||
state = rpsInStart
|
|
||||||
}
|
|
||||||
index++
|
|
||||||
|
|
||||||
case rpsInStart:
|
|
||||||
switch curRune {
|
|
||||||
case ':':
|
|
||||||
parsed.start = strBuilder.String()
|
|
||||||
if !valueCheck.MatchString(parsed.start) {
|
|
||||||
return nil, parseError(input, startOfStart, "invalid start value")
|
|
||||||
}
|
|
||||||
strBuilder.Reset()
|
|
||||||
state = rpsExpectEnd
|
|
||||||
default:
|
|
||||||
strBuilder.WriteRune(curRune)
|
|
||||||
}
|
|
||||||
index++
|
|
||||||
|
|
||||||
case rpsExpectColon:
|
|
||||||
switch curRune {
|
|
||||||
case ':':
|
|
||||||
state = rpsExpectEnd
|
|
||||||
index++
|
|
||||||
default:
|
|
||||||
return nil, parseError(input, index, "expected ':'")
|
|
||||||
}
|
|
||||||
|
|
||||||
case rpsExpectEnd:
|
|
||||||
startOfEnd = index
|
|
||||||
state = rpsInEnd
|
|
||||||
|
|
||||||
case rpsInEnd:
|
|
||||||
strBuilder.WriteRune(curRune)
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if state == rpsInStart {
|
|
||||||
// The range was a single value, so that's the upper bound.
|
|
||||||
parsed.start = "0"
|
|
||||||
state = rpsInEnd
|
|
||||||
}
|
|
||||||
if state != rpsInEnd {
|
|
||||||
return nil, parseError(input, index, "unexpected end of input")
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed.end = strBuilder.String()
|
|
||||||
if !valueCheck.MatchString(parsed.end) {
|
|
||||||
return nil, parseError(input, startOfEnd, "invalid end value")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &parsed, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,11 +2,9 @@ package perfdata // import nocternity.net/gomonop/pkg/perfdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRangeMaxInvalid(t *testing.T) {
|
func TestRangeMaxInvalid(t *testing.T) {
|
||||||
|
@ -126,59 +124,3 @@ func TestRangeContains(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRangeParserOk(t *testing.T) {
|
|
||||||
type Test struct {
|
|
||||||
in string
|
|
||||||
out Range
|
|
||||||
}
|
|
||||||
tests := []Test{
|
|
||||||
{in: ":0", out: Range{start: "0", end: "0"}},
|
|
||||||
{in: "0:0", out: Range{start: "0", end: "0"}},
|
|
||||||
{in: "~:0", out: Range{start: "", end: "0"}},
|
|
||||||
{in: ":123", out: Range{start: "0", end: "123"}},
|
|
||||||
{in: "0:123", out: Range{start: "0", end: "123"}},
|
|
||||||
{in: "~:123", out: Range{start: "", end: "123"}},
|
|
||||||
{in: "1", out: Range{start: "0", end: "1"}},
|
|
||||||
{in: "@:0", out: Range{start: "0", end: "0", inside: true}},
|
|
||||||
{in: "@0:0", out: Range{start: "0", end: "0", inside: true}},
|
|
||||||
{in: "@~:0", out: Range{start: "", end: "0", inside: true}},
|
|
||||||
{in: "@:123", out: Range{start: "0", end: "123", inside: true}},
|
|
||||||
{in: "@0:123", out: Range{start: "0", end: "123", inside: true}},
|
|
||||||
{in: "@~:123", out: Range{start: "", end: "123", inside: true}},
|
|
||||||
{in: "@1", out: Range{start: "0", end: "1", inside: true}},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
result, err := ParseRange(test.in)
|
|
||||||
require.NoError(t, err, "Expected no error, got '%v'", err)
|
|
||||||
assert.Equal(t, test.out, *result, "Expected '%v', got '%v'", test.out, *result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRangeParserError(t *testing.T) {
|
|
||||||
type Test struct {
|
|
||||||
in string
|
|
||||||
errPos int
|
|
||||||
}
|
|
||||||
tests := []Test{
|
|
||||||
{in: "", errPos: 0},
|
|
||||||
{in: ":", errPos: 1},
|
|
||||||
{in: "x:1", errPos: 0},
|
|
||||||
{in: ":~", errPos: 1},
|
|
||||||
{in: "@", errPos: 1},
|
|
||||||
{in: "@:", errPos: 2},
|
|
||||||
{in: "@x:1", errPos: 1},
|
|
||||||
{in: "@:~", errPos: 2},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
result, err := ParseRange(test.in)
|
|
||||||
require.Error(t, err, "Expected error, got '%v'", err)
|
|
||||||
assert.Nil(t, result, "Expected nil result, got '%v'", result)
|
|
||||||
assert.True(
|
|
||||||
t, strings.Contains(err.Error(), fmt.Sprintf("at position %d", test.errPos)),
|
|
||||||
"Expected error to contain '%s', got '%s'", fmt.Sprintf("at position %d", test.errPos), err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
196
pkg/perfdata/rangeparser.go
Normal file
196
pkg/perfdata/rangeparser.go
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
package perfdata // import nocternity.net/gomonop/pkg/perfdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A state of the range parser.
|
||||||
|
type rangeParserState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
rpsInit rangeParserState = iota // Initial state
|
||||||
|
rpsExpectStart // Expect the start of the range
|
||||||
|
rpsInStart // Reading the start of the range
|
||||||
|
rpsExpectColon // Expect the colon that separates the start and end
|
||||||
|
rpsExpectEnd // Expect the end of the range
|
||||||
|
rpsInEnd // Reading the end of the range
|
||||||
|
)
|
||||||
|
|
||||||
|
// An error emitted by the range parser.
|
||||||
|
type rangeParserError struct {
|
||||||
|
input string
|
||||||
|
position int
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseError creates a new range parser error.
|
||||||
|
func parseError(input string, position int, message string) *rangeParserError {
|
||||||
|
return &rangeParserError{
|
||||||
|
input: input,
|
||||||
|
position: position,
|
||||||
|
message: message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error implements the error interface for the range parser error type.
|
||||||
|
func (rpe *rangeParserError) Error() string {
|
||||||
|
return fmt.Sprintf("in `%s' at position %d: %s", rpe.input, rpe.position, rpe.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The full state of the range parser.
|
||||||
|
type rangeParser struct {
|
||||||
|
runes []rune // The runes being parsed
|
||||||
|
index int // The read position
|
||||||
|
state rangeParserState // The FSM state
|
||||||
|
startOfValue int // The position of the start of the value being read
|
||||||
|
strBuilder strings.Builder // An accumulator for values
|
||||||
|
output Range // The output being generated
|
||||||
|
}
|
||||||
|
|
||||||
|
// initRangeParser initializes the range parser from the specified input.
|
||||||
|
func initRangeParser(input string) *rangeParser {
|
||||||
|
return &rangeParser{
|
||||||
|
runes: []rune(input),
|
||||||
|
state: rpsInit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleInit handles the parser's rpsInit state. It can accept a '@' or skip
|
||||||
|
// to rpsExpectStart.
|
||||||
|
func (rp *rangeParser) handleInit(current rune) {
|
||||||
|
if current == '@' {
|
||||||
|
rp.output.inside = true
|
||||||
|
rp.index++
|
||||||
|
}
|
||||||
|
rp.state = rpsExpectStart
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleExpectStart handles the parser's rpsExpectStart state. It may accept
|
||||||
|
// a colon, which will move the parser to the rpsExpectEnd state, a '~' which
|
||||||
|
// will move it to rpsExpectColon, or any other rune which will cause the
|
||||||
|
// accumulation of the "start" value to begin, and the state to be moved to
|
||||||
|
// handleInStart.
|
||||||
|
func (rp *rangeParser) handleExpectStart(current rune) {
|
||||||
|
switch current {
|
||||||
|
case ':':
|
||||||
|
rp.output.start = "0"
|
||||||
|
rp.state = rpsExpectEnd
|
||||||
|
rp.index++
|
||||||
|
case '~':
|
||||||
|
rp.state = rpsExpectColon
|
||||||
|
rp.index++
|
||||||
|
default:
|
||||||
|
rp.startOfValue = rp.index
|
||||||
|
rp.state = rpsInStart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleInStart handles the parser's rpsInStart state, which corresponds to the
|
||||||
|
// reading of the "start" value. If a colon is found, the value will be written
|
||||||
|
// to the range structure and validated, before switching to the rpsExpectEnd
|
||||||
|
// state. Otherwise it will simply accumulate runes.
|
||||||
|
func (rp *rangeParser) handleInStart(current rune) error {
|
||||||
|
switch current {
|
||||||
|
case ':':
|
||||||
|
rp.output.start = rp.strBuilder.String()
|
||||||
|
if !valueCheck.MatchString(rp.output.start) {
|
||||||
|
return parseError(string(rp.runes), rp.startOfValue, "invalid start value")
|
||||||
|
}
|
||||||
|
rp.strBuilder.Reset()
|
||||||
|
rp.state = rpsExpectEnd
|
||||||
|
default:
|
||||||
|
rp.strBuilder.WriteRune(current)
|
||||||
|
}
|
||||||
|
|
||||||
|
rp.index++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleExpectColon handles the parser's rpsExpectColon state. A colon MUST be
|
||||||
|
// read, otherwise an error will be returned. The parser will then switch to the
|
||||||
|
// rpsExpectEnd state.
|
||||||
|
func (rp *rangeParser) handleExpectColon(current rune) error {
|
||||||
|
switch current {
|
||||||
|
case ':':
|
||||||
|
rp.state = rpsExpectEnd
|
||||||
|
rp.index++
|
||||||
|
default:
|
||||||
|
return parseError(string(rp.runes), rp.index, "expected ':'")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleExpectEnd handles the parser's rpsExpectEnd state, which sets the
|
||||||
|
// position of the beginning of the "end" value, and jumps to the rpsInEnd
|
||||||
|
// state.
|
||||||
|
func (rp *rangeParser) handleExpectEnd() {
|
||||||
|
rp.startOfValue = rp.index
|
||||||
|
rp.state = rpsInEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleInEnd handles the parser's rpsInEnd state, which accumulates runes for
|
||||||
|
// the "end" value.
|
||||||
|
func (rp *rangeParser) handleInEnd(current rune) {
|
||||||
|
rp.strBuilder.WriteRune(current)
|
||||||
|
rp.index++
|
||||||
|
}
|
||||||
|
|
||||||
|
// consumeInput is the parser's state machine.
|
||||||
|
func (rp *rangeParser) consumeInput() error {
|
||||||
|
for rp.index < len(rp.runes) {
|
||||||
|
var err error
|
||||||
|
curRune := rp.runes[rp.index]
|
||||||
|
|
||||||
|
switch rp.state {
|
||||||
|
case rpsInit:
|
||||||
|
rp.handleInit(curRune)
|
||||||
|
|
||||||
|
case rpsExpectStart:
|
||||||
|
rp.handleExpectStart(curRune)
|
||||||
|
|
||||||
|
case rpsInStart:
|
||||||
|
err = rp.handleInStart(curRune)
|
||||||
|
|
||||||
|
case rpsExpectColon:
|
||||||
|
err = rp.handleExpectColon(curRune)
|
||||||
|
|
||||||
|
case rpsExpectEnd:
|
||||||
|
rp.handleExpectEnd()
|
||||||
|
|
||||||
|
case rpsInEnd:
|
||||||
|
rp.handleInEnd(curRune)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse a string into a performance data range.
|
||||||
|
func ParseRange(input string) (*Range, error) {
|
||||||
|
parser := initRangeParser(input)
|
||||||
|
if err := parser.consumeInput(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if parser.state == rpsInStart {
|
||||||
|
// The range was a single value, so that's the upper bound.
|
||||||
|
parser.output.start = "0"
|
||||||
|
parser.state = rpsInEnd
|
||||||
|
}
|
||||||
|
if parser.state != rpsInEnd {
|
||||||
|
return nil, parseError(input, parser.index, "unexpected end of input")
|
||||||
|
}
|
||||||
|
|
||||||
|
parser.output.end = parser.strBuilder.String()
|
||||||
|
if !valueCheck.MatchString(parser.output.end) {
|
||||||
|
return nil, parseError(input, parser.startOfValue, "invalid end value")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &parser.output, nil
|
||||||
|
}
|
66
pkg/perfdata/rangeparser_test.go
Normal file
66
pkg/perfdata/rangeparser_test.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
package perfdata // import nocternity.net/gomonop/pkg/perfdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRangeParserOk(t *testing.T) {
|
||||||
|
type Test struct {
|
||||||
|
in string
|
||||||
|
out Range
|
||||||
|
}
|
||||||
|
tests := []Test{
|
||||||
|
{in: ":0", out: Range{start: "0", end: "0"}},
|
||||||
|
{in: "0:0", out: Range{start: "0", end: "0"}},
|
||||||
|
{in: "~:0", out: Range{start: "", end: "0"}},
|
||||||
|
{in: ":123", out: Range{start: "0", end: "123"}},
|
||||||
|
{in: "0:123", out: Range{start: "0", end: "123"}},
|
||||||
|
{in: "~:123", out: Range{start: "", end: "123"}},
|
||||||
|
{in: "1", out: Range{start: "0", end: "1"}},
|
||||||
|
{in: "@:0", out: Range{start: "0", end: "0", inside: true}},
|
||||||
|
{in: "@0:0", out: Range{start: "0", end: "0", inside: true}},
|
||||||
|
{in: "@~:0", out: Range{start: "", end: "0", inside: true}},
|
||||||
|
{in: "@:123", out: Range{start: "0", end: "123", inside: true}},
|
||||||
|
{in: "@0:123", out: Range{start: "0", end: "123", inside: true}},
|
||||||
|
{in: "@~:123", out: Range{start: "", end: "123", inside: true}},
|
||||||
|
{in: "@1", out: Range{start: "0", end: "1", inside: true}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
result, err := ParseRange(test.in)
|
||||||
|
require.NoError(t, err, "Expected no error, got '%v'", err)
|
||||||
|
assert.Equal(t, test.out, *result, "Expected '%v', got '%v'", test.out, *result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRangeParserError(t *testing.T) {
|
||||||
|
type Test struct {
|
||||||
|
in string
|
||||||
|
errPos int
|
||||||
|
}
|
||||||
|
tests := []Test{
|
||||||
|
{in: "", errPos: 0},
|
||||||
|
{in: ":", errPos: 1},
|
||||||
|
{in: "x:1", errPos: 0},
|
||||||
|
{in: ":~", errPos: 1},
|
||||||
|
{in: "@", errPos: 1},
|
||||||
|
{in: "@:", errPos: 2},
|
||||||
|
{in: "@x:1", errPos: 1},
|
||||||
|
{in: "@:~", errPos: 2},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
result, err := ParseRange(test.in)
|
||||||
|
require.Error(t, err, "Expected error, got '%v'", err)
|
||||||
|
assert.Nil(t, result, "Expected nil result, got '%v'", result)
|
||||||
|
assert.True(
|
||||||
|
t, strings.Contains(err.Error(), fmt.Sprintf("at position %d", test.errPos)),
|
||||||
|
"Expected error to contain '%s', got '%s'", fmt.Sprintf("at position %d", test.errPos), err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue