feat(pkg): add parser for perf data ranges

This commit is contained in:
Emmanuel BENOîT 2024-07-20 16:22:22 +02:00
parent c87f31d89d
commit 14c7a0e5e8
Signed by: Emmanuel BENOîT
SSH key fingerprint: SHA256:l7PFUUF5TCDsvYeQC9OnTNz08dFY7Fvf4Hv3neIqYpg
2 changed files with 177 additions and 0 deletions

View file

@ -1,5 +1,10 @@
package perfdata // import nocternity.net/gomonop/pkg/perfdata package perfdata // import nocternity.net/gomonop/pkg/perfdata
import (
"fmt"
"strings"
)
// Performance data range. // Performance data range.
type Range struct { type Range struct {
start string start string
@ -60,3 +65,116 @@ func (r *Range) String() string {
return inside + start + ":" + r.end return inside + start + ":" + r.end
} }
// A state of the range parser.
type rangeParserState int
const (
rpsInit = 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 += 1
}
state = rpsExpectStart
case rpsExpectStart:
switch curRune {
case ':':
parsed.start = "0"
state = rpsExpectEnd
case '~':
state = rpsExpectColon
default:
strBuilder.WriteRune(curRune)
startOfStart = index
state = rpsInStart
}
index += 1
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 += 1
case rpsExpectColon:
switch curRune {
case ':':
state = rpsExpectEnd
index += 1
default:
return nil, parseError(input, index, "expected ':'")
}
case rpsExpectEnd:
startOfEnd = index
state = rpsInEnd
case rpsInEnd:
strBuilder.WriteRune(curRune)
index += 1
}
}
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
}

View file

@ -1,9 +1,12 @@
package perfdata // import nocternity.net/gomonop/pkg/perfdata package perfdata // import nocternity.net/gomonop/pkg/perfdata
import ( import (
"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) {
@ -65,3 +68,59 @@ func TestRangeString(t *testing.T) {
assert.Equal(t, test.out, result, "Expected '%s', got '%s'", test.out, result) assert.Equal(t, test.out, result, "Expected '%s', got '%s'", test.out, result)
} }
} }
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: "@: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}},
}
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: "1", errPos: 1},
{in: "@", errPos: 1},
{in: "@:", errPos: 2},
{in: "@x:1", errPos: 1},
{in: "@:~", errPos: 2},
{in: "@1", 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,
)
}
}