1614 lines
34 KiB
Go
1614 lines
34 KiB
Go
package binary
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/influxdata/telegraf"
|
|
"github.com/influxdata/telegraf/config"
|
|
"github.com/influxdata/telegraf/internal"
|
|
"github.com/influxdata/telegraf/metric"
|
|
"github.com/influxdata/telegraf/testutil"
|
|
test "github.com/influxdata/telegraf/testutil/plugin_input"
|
|
)
|
|
|
|
var dummyEntry = Entry{
|
|
Name: "dummy",
|
|
Type: "uint8",
|
|
Bits: 8,
|
|
Assignment: "field",
|
|
}
|
|
|
|
func generateBinary(data []interface{}, order binary.ByteOrder) ([]byte, error) {
|
|
var buf bytes.Buffer
|
|
|
|
for _, x := range data {
|
|
var err error
|
|
switch v := x.(type) {
|
|
case []byte:
|
|
buf.Write(v)
|
|
case string:
|
|
buf.WriteString(v)
|
|
default:
|
|
err = binary.Write(&buf, order, x)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func determineEndianness(endianness string) binary.ByteOrder {
|
|
switch endianness {
|
|
case "le":
|
|
return binary.LittleEndian
|
|
case "be":
|
|
return binary.BigEndian
|
|
case "host":
|
|
return internal.HostEndianness
|
|
}
|
|
panic(fmt.Errorf("unknown endianness %q", endianness))
|
|
}
|
|
|
|
func TestInitInvalid(t *testing.T) {
|
|
var tests = []struct {
|
|
name string
|
|
metric string
|
|
config []Config
|
|
endianness string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "wrong endianness",
|
|
metric: "binary",
|
|
endianness: "garbage",
|
|
expected: `unknown endianness "garbage"`,
|
|
},
|
|
{
|
|
name: "empty configuration",
|
|
metric: "binary",
|
|
endianness: "host",
|
|
expected: `no configuration given`,
|
|
},
|
|
{
|
|
name: "no metric name",
|
|
config: []Config{{}},
|
|
endianness: "host",
|
|
expected: `config 0 invalid: no metric name given`,
|
|
},
|
|
{
|
|
name: "no field",
|
|
config: []Config{{}},
|
|
metric: "binary",
|
|
expected: `config 0 invalid: no field defined`,
|
|
},
|
|
{
|
|
name: "invalid entry",
|
|
config: []Config{{
|
|
Entries: []Entry{
|
|
{
|
|
Bits: 8,
|
|
},
|
|
},
|
|
}},
|
|
metric: "binary",
|
|
expected: `config 0 invalid: entry "" (0): missing name`,
|
|
},
|
|
{
|
|
name: "multiple measurements",
|
|
config: []Config{{
|
|
Entries: []Entry{
|
|
{
|
|
Bits: 8,
|
|
Assignment: "measurement",
|
|
},
|
|
{
|
|
Bits: 8,
|
|
Assignment: "measurement",
|
|
},
|
|
},
|
|
}},
|
|
metric: "binary",
|
|
expected: `config 0 invalid: multiple definitions of "measurement"`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
parser := &Parser{
|
|
Endianness: tt.endianness,
|
|
Log: testutil.Logger{Name: "parsers.binary"},
|
|
metricName: tt.metric,
|
|
}
|
|
|
|
parser.Configs = tt.config
|
|
require.EqualError(t, parser.Init(), tt.expected)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFilterInvalid(t *testing.T) {
|
|
var tests = []struct {
|
|
name string
|
|
filter *Filter
|
|
expected string
|
|
}{
|
|
{
|
|
name: "both length and length-min",
|
|
filter: &Filter{Length: 35, LengthMin: 33},
|
|
expected: `config 0 invalid: length and length_min cannot be used together`,
|
|
},
|
|
{
|
|
name: "filter too long length",
|
|
filter: &Filter{Length: 3, Selection: []BinaryPart{{Offset: 16, Bits: 16}}},
|
|
expected: `config 0 invalid: filter length (4) larger than constraint (3)`,
|
|
},
|
|
{
|
|
name: "filter invalid match",
|
|
filter: &Filter{Selection: []BinaryPart{{Offset: 16, Bits: 16, Match: "XYZ"}}},
|
|
expected: `config 0 invalid: decoding match 0 failed: encoding/hex: invalid byte: U+0058 'X'`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
parser := &Parser{
|
|
Configs: []Config{{Filter: tt.filter}},
|
|
Log: testutil.Logger{Name: "parsers.binary"},
|
|
metricName: "binary",
|
|
}
|
|
require.EqualError(t, parser.Init(), tt.expected)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFilterMatchInvalid(t *testing.T) {
|
|
testdata := []byte{0x01, 0x02}
|
|
|
|
var tests = []struct {
|
|
name string
|
|
filter *Filter
|
|
expected string
|
|
}{
|
|
{
|
|
name: "filter length mismatch",
|
|
filter: &Filter{Selection: []BinaryPart{{Offset: 0, Bits: 8, Match: "0x0102"}}},
|
|
expected: `no matching configuration`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
parser := &Parser{
|
|
Configs: []Config{{Filter: tt.filter, Entries: []Entry{{Name: "test", Type: "uint8"}}}},
|
|
Log: testutil.Logger{Name: "parsers.binary"},
|
|
metricName: "binary",
|
|
}
|
|
require.NoError(t, parser.Init())
|
|
_, err := parser.Parse(testdata)
|
|
require.EqualError(t, err, tt.expected)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFilterNoMatch(t *testing.T) {
|
|
testdata := []interface{}{uint16(0x0102)}
|
|
|
|
t.Run("no match error", func(t *testing.T) {
|
|
parser := &Parser{
|
|
Configs: []Config{
|
|
{
|
|
Filter: &Filter{Length: 32},
|
|
Entries: []Entry{dummyEntry},
|
|
},
|
|
},
|
|
Log: testutil.Logger{Name: "parsers.binary"},
|
|
metricName: "binary",
|
|
}
|
|
require.NoError(t, parser.Init())
|
|
|
|
data, err := generateBinary(testdata, internal.HostEndianness)
|
|
require.NoError(t, err)
|
|
|
|
_, err = parser.Parse(data)
|
|
require.EqualError(t, err, "no matching configuration")
|
|
})
|
|
|
|
t.Run("no match allow", func(t *testing.T) {
|
|
parser := &Parser{
|
|
AllowNoMatch: true,
|
|
Configs: []Config{
|
|
{
|
|
Filter: &Filter{Length: 32},
|
|
Entries: []Entry{dummyEntry},
|
|
},
|
|
},
|
|
Log: testutil.Logger{Name: "parsers.binary"},
|
|
metricName: "binary",
|
|
}
|
|
require.NoError(t, parser.Init())
|
|
|
|
data, err := generateBinary(testdata, internal.HostEndianness)
|
|
require.NoError(t, err)
|
|
|
|
metrics, err := parser.Parse(data)
|
|
require.NoError(t, err)
|
|
require.Empty(t, metrics)
|
|
})
|
|
}
|
|
|
|
func TestFilterNone(t *testing.T) {
|
|
testdata := []interface{}{
|
|
uint64(0x01020304050607),
|
|
uint64(0x08090A0B0C0D0E),
|
|
uint64(0x0F101213141516),
|
|
uint64(0x1718191A1B1C1D),
|
|
uint64(0x1E1F2021222324),
|
|
}
|
|
|
|
var tests = []struct {
|
|
name string
|
|
data []interface{}
|
|
filter *Filter
|
|
endianness string
|
|
}{
|
|
{
|
|
name: "no filter (BE)",
|
|
data: testdata,
|
|
filter: nil,
|
|
endianness: "be",
|
|
},
|
|
{
|
|
name: "no filter (LE)",
|
|
data: testdata,
|
|
filter: nil,
|
|
endianness: "le",
|
|
},
|
|
{
|
|
name: "no filter (host)",
|
|
data: testdata,
|
|
filter: nil,
|
|
endianness: "host",
|
|
},
|
|
{
|
|
name: "empty filter (BE)",
|
|
data: testdata,
|
|
filter: &Filter{},
|
|
endianness: "be",
|
|
},
|
|
{
|
|
name: "empty filter (LE)",
|
|
data: testdata,
|
|
filter: &Filter{},
|
|
endianness: "le",
|
|
},
|
|
{
|
|
name: "empty filter (host)",
|
|
data: testdata,
|
|
filter: &Filter{},
|
|
endianness: "host",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
parser := &Parser{
|
|
Endianness: tt.endianness,
|
|
Configs: []Config{
|
|
{
|
|
Filter: tt.filter,
|
|
Entries: []Entry{dummyEntry},
|
|
},
|
|
},
|
|
Log: testutil.Logger{Name: "parsers.binary"},
|
|
metricName: "binary",
|
|
}
|
|
require.NoError(t, parser.Init())
|
|
|
|
order := determineEndianness(tt.endianness)
|
|
data, err := generateBinary(tt.data, order)
|
|
require.NoError(t, err)
|
|
|
|
metrics, err := parser.Parse(data)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, metrics)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFilterLength(t *testing.T) {
|
|
testdata := []interface{}{
|
|
uint64(0x01020304050607),
|
|
uint64(0x08090A0B0C0D0E),
|
|
uint64(0x0F101213141516),
|
|
uint64(0x1718191A1B1C1D),
|
|
uint64(0x1E1F2021222324),
|
|
}
|
|
|
|
var tests = []struct {
|
|
name string
|
|
data []interface{}
|
|
filter *Filter
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "length match",
|
|
data: testdata,
|
|
filter: &Filter{Length: 40},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "length no match too short",
|
|
data: testdata,
|
|
filter: &Filter{Length: 41},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "length no match too long",
|
|
data: testdata,
|
|
filter: &Filter{Length: 39},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "length min match",
|
|
data: testdata,
|
|
filter: &Filter{LengthMin: 40},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "length min no match too short",
|
|
data: testdata,
|
|
filter: &Filter{LengthMin: 41},
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
parser := &Parser{
|
|
AllowNoMatch: true,
|
|
Configs: []Config{
|
|
{
|
|
Filter: tt.filter,
|
|
Entries: []Entry{dummyEntry},
|
|
},
|
|
},
|
|
Log: testutil.Logger{Name: "parsers.binary"},
|
|
metricName: "binary",
|
|
}
|
|
require.NoError(t, parser.Init())
|
|
|
|
data, err := generateBinary(tt.data, internal.HostEndianness)
|
|
require.NoError(t, err)
|
|
|
|
metrics, err := parser.Parse(data)
|
|
require.NoError(t, err)
|
|
if tt.expected {
|
|
require.NotEmpty(t, metrics)
|
|
} else {
|
|
require.Empty(t, metrics)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFilterContent(t *testing.T) {
|
|
testdata := [][]byte{
|
|
{0x01, 0x02, 0x03, 0xA4, 0x05, 0x06, 0x07, 0x08},
|
|
{0x01, 0xA2, 0x03, 0x04, 0x15, 0x01, 0x07, 0x08},
|
|
{0xF1, 0xB1, 0x03, 0xA4, 0x25, 0x06, 0x07, 0x08},
|
|
{0xF1, 0xC2, 0x03, 0x04, 0x35, 0x01, 0x07, 0x08},
|
|
{0x42, 0xD1, 0x03, 0xA4, 0x25, 0x06, 0x42, 0x08},
|
|
{0x42, 0xE2, 0x03, 0x04, 0x35, 0x01, 0x42, 0x08},
|
|
{0x01, 0x00, 0x00, 0xA4},
|
|
}
|
|
var tests = []struct {
|
|
name string
|
|
filter *Filter
|
|
expected int
|
|
}{
|
|
{
|
|
name: "first byte",
|
|
filter: &Filter{
|
|
Selection: []BinaryPart{
|
|
{
|
|
Offset: 0,
|
|
Bits: 8,
|
|
Match: "0xF1",
|
|
},
|
|
},
|
|
},
|
|
expected: 2,
|
|
},
|
|
{
|
|
name: "last byte",
|
|
filter: &Filter{
|
|
Selection: []BinaryPart{
|
|
{
|
|
Offset: 7 * 8,
|
|
Bits: 8,
|
|
Match: "0x08",
|
|
},
|
|
},
|
|
},
|
|
expected: 6,
|
|
},
|
|
{
|
|
name: "none-byte boundary begin",
|
|
filter: &Filter{
|
|
Selection: []BinaryPart{
|
|
{
|
|
Offset: 12,
|
|
Bits: 12,
|
|
Match: "0x0203",
|
|
},
|
|
},
|
|
},
|
|
expected: 4,
|
|
},
|
|
{
|
|
name: "none-byte boundary end",
|
|
filter: &Filter{
|
|
Selection: []BinaryPart{
|
|
{
|
|
Offset: 16,
|
|
Bits: 12,
|
|
Match: "0x003A",
|
|
},
|
|
},
|
|
},
|
|
expected: 3,
|
|
},
|
|
{
|
|
name: "none-byte boundary end",
|
|
filter: &Filter{
|
|
Selection: []BinaryPart{
|
|
{
|
|
Offset: 36,
|
|
Bits: 8,
|
|
Match: "0x50",
|
|
},
|
|
},
|
|
},
|
|
expected: 6,
|
|
},
|
|
{
|
|
name: "multiple elements",
|
|
filter: &Filter{
|
|
Selection: []BinaryPart{
|
|
{
|
|
Offset: 4,
|
|
Bits: 4,
|
|
Match: "0x01",
|
|
},
|
|
{
|
|
Offset: 24,
|
|
Bits: 8,
|
|
Match: "0xA4",
|
|
},
|
|
},
|
|
},
|
|
expected: 3,
|
|
},
|
|
{
|
|
name: "multiple elements and length",
|
|
filter: &Filter{
|
|
Selection: []BinaryPart{
|
|
{
|
|
Offset: 4,
|
|
Bits: 4,
|
|
Match: "0x01",
|
|
},
|
|
{
|
|
Offset: 24,
|
|
Bits: 8,
|
|
Match: "0xA4",
|
|
},
|
|
},
|
|
Length: 4,
|
|
},
|
|
expected: 1,
|
|
},
|
|
{
|
|
name: "multiple elements and length-min",
|
|
filter: &Filter{
|
|
Selection: []BinaryPart{
|
|
{
|
|
Offset: 4,
|
|
Bits: 4,
|
|
Match: "0x01",
|
|
},
|
|
{
|
|
Offset: 24,
|
|
Bits: 8,
|
|
Match: "0xA4",
|
|
},
|
|
},
|
|
LengthMin: 5,
|
|
},
|
|
expected: 2,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
parser := &Parser{
|
|
AllowNoMatch: true,
|
|
Configs: []Config{
|
|
{
|
|
Filter: tt.filter,
|
|
Entries: []Entry{dummyEntry},
|
|
},
|
|
},
|
|
Log: testutil.Logger{Name: "parsers.binary"},
|
|
metricName: "binary",
|
|
}
|
|
require.NoError(t, parser.Init())
|
|
|
|
var metrics []telegraf.Metric
|
|
for _, data := range testdata {
|
|
m, err := parser.Parse(data)
|
|
require.NoError(t, err)
|
|
metrics = append(metrics, m...)
|
|
}
|
|
require.Len(t, metrics, tt.expected)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseLineInvalid(t *testing.T) {
|
|
var tests = []struct {
|
|
name string
|
|
data []interface{}
|
|
configs []Config
|
|
expected string
|
|
}{
|
|
{
|
|
name: "out-of-bounds",
|
|
data: []interface{}{
|
|
"2022-07-25T20:41:29+02:00", // time
|
|
uint16(0x0102), // address
|
|
float64(42.123), // value
|
|
},
|
|
configs: []Config{
|
|
{
|
|
Entries: []Entry{
|
|
{
|
|
Type: "2006-01-02T15:04:05Z07:00",
|
|
Assignment: "time",
|
|
Timezone: "UTC",
|
|
},
|
|
{
|
|
Type: "uint32",
|
|
Omit: true,
|
|
},
|
|
{
|
|
Name: "value",
|
|
Type: "float64",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: `out-of-bounds @232 with 64 bits`,
|
|
},
|
|
{
|
|
name: "multiple matches",
|
|
data: []interface{}{
|
|
"2022-07-25T20:41:29+02:00", // time
|
|
uint16(0x0102), // address
|
|
float64(42.123), // value
|
|
},
|
|
configs: []Config{
|
|
{
|
|
Entries: []Entry{
|
|
{
|
|
Type: "2006-01-02T15:04:05Z07:00",
|
|
Assignment: "time",
|
|
Timezone: "UTC",
|
|
},
|
|
{
|
|
Type: "uint16",
|
|
Omit: true,
|
|
},
|
|
{
|
|
Name: "value",
|
|
Type: "float64",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Entries: []Entry{
|
|
{
|
|
Type: "2006-01-02T15:04:05Z07:00",
|
|
Assignment: "time",
|
|
Timezone: "UTC",
|
|
},
|
|
{
|
|
Name: "address",
|
|
Type: "uint16",
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Name: "value",
|
|
Type: "float64",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: `cannot parse line with multiple (2) metrics`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
for _, endianness := range []string{"be", "le", "host"} {
|
|
name := fmt.Sprintf("%s (%s)", tt.name, endianness)
|
|
t.Run(name, func(t *testing.T) {
|
|
parser := &Parser{
|
|
Endianness: endianness,
|
|
Configs: tt.configs,
|
|
Log: testutil.Logger{Name: "parsers.binary"},
|
|
metricName: "binary",
|
|
}
|
|
require.NoError(t, parser.Init())
|
|
|
|
order := determineEndianness(endianness)
|
|
data, err := generateBinary(tt.data, order)
|
|
require.NoError(t, err)
|
|
|
|
_, err = parser.ParseLine(string(data))
|
|
require.EqualError(t, err, tt.expected)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseLine(t *testing.T) {
|
|
var tests = []struct {
|
|
name string
|
|
data []interface{}
|
|
filter *Filter
|
|
entries []Entry
|
|
expected telegraf.Metric
|
|
}{
|
|
{
|
|
name: "no match",
|
|
data: []interface{}{
|
|
"2022-07-25T20:41:29+02:00", // time
|
|
uint16(0x0102), // address
|
|
float64(42.123), // value
|
|
},
|
|
filter: &Filter{Length: 4},
|
|
entries: []Entry{
|
|
{
|
|
Type: "2006-01-02T15:04:05Z07:00",
|
|
Assignment: "time",
|
|
Timezone: "UTC",
|
|
},
|
|
{
|
|
Type: "uint16",
|
|
Omit: true,
|
|
},
|
|
{
|
|
Name: "value",
|
|
Type: "float64",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "single match",
|
|
data: []interface{}{
|
|
"2022-07-25T20:41:29+02:00", // time
|
|
uint16(0x0102), // address
|
|
float64(42.123), // value
|
|
},
|
|
entries: []Entry{
|
|
{
|
|
Type: "2006-01-02T15:04:05Z07:00",
|
|
Assignment: "time",
|
|
Timezone: "UTC",
|
|
},
|
|
{
|
|
Type: "uint16",
|
|
Omit: true,
|
|
},
|
|
{
|
|
Name: "value",
|
|
Type: "float64",
|
|
},
|
|
},
|
|
expected: metric.New(
|
|
"binary",
|
|
map[string]string{},
|
|
map[string]interface{}{"value": float64(42.123)},
|
|
time.Unix(1658774489, 0),
|
|
),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
for _, endianness := range []string{"be", "le", "host"} {
|
|
name := fmt.Sprintf("%s (%s)", tt.name, endianness)
|
|
t.Run(name, func(t *testing.T) {
|
|
parser := &Parser{
|
|
AllowNoMatch: true,
|
|
Endianness: endianness,
|
|
Configs: []Config{{
|
|
Filter: tt.filter,
|
|
Entries: tt.entries,
|
|
}},
|
|
Log: testutil.Logger{Name: "parsers.binary"},
|
|
metricName: "binary",
|
|
}
|
|
require.NoError(t, parser.Init())
|
|
|
|
order := determineEndianness(endianness)
|
|
data, err := generateBinary(tt.data, order)
|
|
require.NoError(t, err)
|
|
|
|
m, err := parser.ParseLine(string(data))
|
|
require.NoError(t, err)
|
|
|
|
testutil.RequireMetricEqual(t, tt.expected, m)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseInvalid(t *testing.T) {
|
|
var tests = []struct {
|
|
name string
|
|
data []interface{}
|
|
entries []Entry
|
|
expected string
|
|
}{
|
|
{
|
|
name: "message too short",
|
|
data: []interface{}{uint64(0x0102030405060708)},
|
|
entries: []Entry{
|
|
{
|
|
Name: "command",
|
|
Type: "uint32",
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Name: "version",
|
|
Type: "uint32",
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Name: "address",
|
|
Type: "uint32",
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Name: "value",
|
|
Type: "float64",
|
|
},
|
|
},
|
|
expected: `out-of-bounds @64 with 32 bits`,
|
|
},
|
|
{
|
|
name: "non-terminated string",
|
|
data: []interface{}{
|
|
uint16(0xAB42), // address
|
|
"testmetric", // metric
|
|
float64(42.23432243), // value
|
|
},
|
|
entries: []Entry{
|
|
{
|
|
Name: "address",
|
|
Type: "uint16",
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Type: "string",
|
|
Terminator: "null",
|
|
Assignment: "measurement",
|
|
},
|
|
{
|
|
Name: "value",
|
|
Type: "float64",
|
|
},
|
|
},
|
|
expected: `terminator not found for "measurement"`,
|
|
},
|
|
{
|
|
name: "invalid time",
|
|
data: []interface{}{
|
|
"2022-07-25T18:41:XYZ", // time
|
|
uint16(0x0102), // address
|
|
float64(42.123), // value
|
|
},
|
|
entries: []Entry{
|
|
{
|
|
Type: "2006-01-02T15:04:05Z",
|
|
Assignment: "time",
|
|
},
|
|
{
|
|
Name: "address",
|
|
Type: "uint16",
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Name: "value",
|
|
Type: "float64",
|
|
},
|
|
},
|
|
expected: `time failed: parsing time "2022-07-25T18:41:XYZ" as "2006-01-02T15:04:05Z": cannot parse "XYZ" as "05"`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
for _, endianness := range []string{"be", "le", "host"} {
|
|
name := fmt.Sprintf("%s (%s)", tt.name, endianness)
|
|
t.Run(name, func(t *testing.T) {
|
|
parser := &Parser{
|
|
Endianness: endianness,
|
|
Configs: []Config{{Entries: tt.entries}},
|
|
Log: testutil.Logger{Name: "parsers.binary"},
|
|
metricName: "binary",
|
|
}
|
|
require.NoError(t, parser.Init())
|
|
|
|
order := determineEndianness(endianness)
|
|
data, err := generateBinary(tt.data, order)
|
|
require.NoError(t, err)
|
|
|
|
_, err = parser.Parse(data)
|
|
require.EqualError(t, err, tt.expected)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParse(t *testing.T) {
|
|
timeBerlin, err := time.Parse(time.RFC3339, "2022-07-25T20:41:29+02:00")
|
|
require.NoError(t, err)
|
|
timeBerlinMilli, err := time.Parse(time.RFC3339Nano, "2022-07-25T20:41:29.123+02:00")
|
|
require.NoError(t, err)
|
|
|
|
var tests = []struct {
|
|
name string
|
|
data []interface{}
|
|
entries []Entry
|
|
ignoreTime bool
|
|
expected []telegraf.Metric
|
|
}{
|
|
{
|
|
name: "fixed numbers",
|
|
data: []interface{}{
|
|
uint16(0xAB42), // command
|
|
uint8(0x02), // version
|
|
uint32(0x010000FF), // address
|
|
uint64(0x0102030405060708), // serial-number
|
|
int8(-25), // countdown as int32
|
|
int16(-42), // overdue
|
|
int32(-65535), // batchleft
|
|
int64(12345678), // counter
|
|
float32(3.1415), // x
|
|
float32(99.471), // y
|
|
float64(0.23432243), // z
|
|
uint8(0xFF), // status
|
|
uint8(0x0F), // on/off bit-field
|
|
},
|
|
entries: []Entry{
|
|
{
|
|
Name: "command",
|
|
Type: "uint16",
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Name: "version",
|
|
Type: "uint8",
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Name: "address",
|
|
Type: "uint32",
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Name: "serialnumber",
|
|
Type: "uint64",
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Name: "countdown",
|
|
Type: "int8",
|
|
},
|
|
{
|
|
Name: "overdue",
|
|
Type: "int16",
|
|
},
|
|
{
|
|
Name: "batchleft",
|
|
Type: "int32",
|
|
},
|
|
{
|
|
Name: "counter",
|
|
Type: "int64",
|
|
},
|
|
{
|
|
Name: "x",
|
|
Type: "float32",
|
|
},
|
|
{
|
|
Name: "y",
|
|
Type: "float32",
|
|
},
|
|
{
|
|
Name: "z",
|
|
Type: "float64",
|
|
},
|
|
{
|
|
Name: "status",
|
|
Type: "bool",
|
|
Bits: 8,
|
|
},
|
|
{
|
|
Name: "error_part",
|
|
Type: "bool",
|
|
Bits: 4,
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Name: "ok_part1",
|
|
Type: "bool",
|
|
Bits: 1,
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Name: "ok_part2",
|
|
Type: "bool",
|
|
Bits: 1,
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Name: "ok_part3",
|
|
Type: "bool",
|
|
Bits: 1,
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Name: "ok_part4",
|
|
Type: "bool",
|
|
Bits: 1,
|
|
Assignment: "tag",
|
|
},
|
|
},
|
|
ignoreTime: true,
|
|
expected: []telegraf.Metric{
|
|
metric.New(
|
|
"binary",
|
|
map[string]string{
|
|
"command": "43842",
|
|
"version": "2",
|
|
"address": "16777471",
|
|
"serialnumber": "72623859790382856",
|
|
"error_part": "false",
|
|
"ok_part1": "true",
|
|
"ok_part2": "true",
|
|
"ok_part3": "true",
|
|
"ok_part4": "true",
|
|
},
|
|
map[string]interface{}{
|
|
"x": float32(3.1415),
|
|
"y": float32(99.471),
|
|
"z": float64(0.23432243),
|
|
"countdown": int8(-25),
|
|
"overdue": int16(-42),
|
|
"batchleft": int32(-65535),
|
|
"counter": int64(12345678),
|
|
|
|
"status": true,
|
|
},
|
|
time.Unix(0, 0),
|
|
),
|
|
},
|
|
},
|
|
{
|
|
name: "fixed length string",
|
|
data: []interface{}{
|
|
uint16(0xAB42), // address
|
|
"test", // metric
|
|
float64(0.23432243), // value
|
|
},
|
|
entries: []Entry{
|
|
{
|
|
Name: "address",
|
|
Type: "uint16",
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Name: "app",
|
|
Type: "string",
|
|
Bits: 4 * 8,
|
|
Assignment: "field",
|
|
},
|
|
{
|
|
Name: "value",
|
|
Type: "float64",
|
|
},
|
|
},
|
|
ignoreTime: true,
|
|
expected: []telegraf.Metric{
|
|
metric.New(
|
|
"binary",
|
|
map[string]string{"address": "43842"},
|
|
map[string]interface{}{
|
|
"app": "test",
|
|
"value": float64(0.23432243),
|
|
},
|
|
time.Unix(0, 0),
|
|
),
|
|
},
|
|
},
|
|
{
|
|
name: "null-terminated string",
|
|
data: []interface{}{
|
|
uint16(0xAB42), // address
|
|
append([]byte("testmetric"), 0x00), // metric
|
|
float64(42.23432243), // value
|
|
},
|
|
entries: []Entry{
|
|
{
|
|
Name: "address",
|
|
Type: "uint16",
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Type: "string",
|
|
Terminator: "null",
|
|
Assignment: "measurement",
|
|
},
|
|
{
|
|
Name: "value",
|
|
Type: "float64",
|
|
},
|
|
},
|
|
ignoreTime: true,
|
|
expected: []telegraf.Metric{
|
|
metric.New(
|
|
"testmetric",
|
|
map[string]string{"address": "43842"},
|
|
map[string]interface{}{"value": float64(42.23432243)},
|
|
time.Unix(0, 0),
|
|
),
|
|
},
|
|
},
|
|
{
|
|
name: "char-terminated string",
|
|
data: []interface{}{
|
|
uint16(0xAB42), // address
|
|
append([]byte("testmetric"), 0x0A, 0x0B), // metric
|
|
float64(42.23432243), // value
|
|
},
|
|
entries: []Entry{
|
|
{
|
|
Name: "address",
|
|
Type: "uint16",
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Type: "string",
|
|
Terminator: "0x0A0B",
|
|
Assignment: "measurement",
|
|
},
|
|
{
|
|
Name: "value",
|
|
Type: "float64",
|
|
},
|
|
},
|
|
ignoreTime: true,
|
|
expected: []telegraf.Metric{
|
|
metric.New(
|
|
"testmetric",
|
|
map[string]string{"address": "43842"},
|
|
map[string]interface{}{"value": float64(42.23432243)},
|
|
time.Unix(0, 0),
|
|
),
|
|
},
|
|
},
|
|
{
|
|
name: "time (unix/UTC)",
|
|
data: []interface{}{
|
|
uint64(1658774489), // time
|
|
uint16(0x0102), // address
|
|
float64(42.123), // value
|
|
},
|
|
entries: []Entry{
|
|
{
|
|
Type: "unix",
|
|
Assignment: "time",
|
|
},
|
|
{
|
|
Name: "address",
|
|
Type: "uint16",
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Name: "value",
|
|
Type: "float64",
|
|
},
|
|
},
|
|
expected: []telegraf.Metric{
|
|
metric.New(
|
|
"binary",
|
|
map[string]string{"address": "258"},
|
|
map[string]interface{}{"value": float64(42.123)},
|
|
time.Unix(1658774489, 0),
|
|
),
|
|
},
|
|
},
|
|
{
|
|
name: "time (unix/Berlin)",
|
|
data: []interface{}{
|
|
uint64(1658774489), // time
|
|
uint16(0x0102), // address
|
|
float64(42.123), // value
|
|
},
|
|
entries: []Entry{
|
|
{
|
|
Type: "unix",
|
|
Assignment: "time",
|
|
Timezone: "Europe/Berlin",
|
|
},
|
|
{
|
|
Name: "address",
|
|
Type: "uint16",
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Name: "value",
|
|
Type: "float64",
|
|
},
|
|
},
|
|
expected: []telegraf.Metric{
|
|
metric.New(
|
|
"binary",
|
|
map[string]string{"address": "258"},
|
|
map[string]interface{}{"value": float64(42.123)},
|
|
timeBerlin,
|
|
),
|
|
},
|
|
},
|
|
{
|
|
name: "time (unix_ms/UTC)",
|
|
data: []interface{}{
|
|
uint64(1658774489123), // time
|
|
uint16(0x0102), // address
|
|
float64(42.123), // value
|
|
},
|
|
entries: []Entry{
|
|
{
|
|
Type: "unix_ms",
|
|
Assignment: "time",
|
|
},
|
|
{
|
|
Name: "address",
|
|
Type: "uint16",
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Name: "value",
|
|
Type: "float64",
|
|
},
|
|
},
|
|
expected: []telegraf.Metric{
|
|
metric.New(
|
|
"binary",
|
|
map[string]string{"address": "258"},
|
|
map[string]interface{}{"value": float64(42.123)},
|
|
time.Unix(0, 1658774489123*1_000_000),
|
|
),
|
|
},
|
|
},
|
|
{
|
|
name: "time (unix_ms/Berlin)",
|
|
data: []interface{}{
|
|
uint64(1658774489123), // time
|
|
uint16(0x0102), // address
|
|
float64(42.123), // value
|
|
},
|
|
entries: []Entry{
|
|
{
|
|
Type: "unix_ms",
|
|
Assignment: "time",
|
|
Timezone: "Europe/Berlin",
|
|
},
|
|
{
|
|
Name: "address",
|
|
Type: "uint16",
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Name: "value",
|
|
Type: "float64",
|
|
},
|
|
},
|
|
expected: []telegraf.Metric{
|
|
metric.New(
|
|
"binary",
|
|
map[string]string{"address": "258"},
|
|
map[string]interface{}{"value": float64(42.123)},
|
|
timeBerlinMilli,
|
|
),
|
|
},
|
|
},
|
|
{
|
|
name: "time (RFC3339/UTC)",
|
|
data: []interface{}{
|
|
"2022-07-25T18:41:29Z", // time
|
|
uint16(0x0102), // address
|
|
float64(42.123), // value
|
|
},
|
|
entries: []Entry{
|
|
{
|
|
Type: "2006-01-02T15:04:05Z",
|
|
Assignment: "time",
|
|
},
|
|
{
|
|
Name: "address",
|
|
Type: "uint16",
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Name: "value",
|
|
Type: "float64",
|
|
},
|
|
},
|
|
expected: []telegraf.Metric{
|
|
metric.New(
|
|
"binary",
|
|
map[string]string{"address": "258"},
|
|
map[string]interface{}{"value": float64(42.123)},
|
|
time.Unix(1658774489, 0),
|
|
),
|
|
},
|
|
},
|
|
{
|
|
name: "time (RFC3339/Berlin)",
|
|
data: []interface{}{
|
|
"2022-07-25T20:41:29+02:00", // time
|
|
uint16(0x0102), // address
|
|
float64(42.123), // value
|
|
},
|
|
entries: []Entry{
|
|
{
|
|
Type: "2006-01-02T15:04:05Z07:00",
|
|
Assignment: "time",
|
|
Timezone: "Europe/Berlin",
|
|
},
|
|
{
|
|
Name: "address",
|
|
Type: "uint16",
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Name: "value",
|
|
Type: "float64",
|
|
},
|
|
},
|
|
expected: []telegraf.Metric{
|
|
metric.New(
|
|
"binary",
|
|
map[string]string{"address": "258"},
|
|
map[string]interface{}{"value": float64(42.123)},
|
|
timeBerlin,
|
|
),
|
|
},
|
|
},
|
|
{
|
|
name: "time (RFC3339/Berlin->UTC)",
|
|
data: []interface{}{
|
|
"2022-07-25T20:41:29+02:00", // time
|
|
uint16(0x0102), // address
|
|
float64(42.123), // value
|
|
},
|
|
entries: []Entry{
|
|
{
|
|
Type: "2006-01-02T15:04:05Z07:00",
|
|
Assignment: "time",
|
|
Timezone: "UTC",
|
|
},
|
|
{
|
|
Name: "address",
|
|
Type: "uint16",
|
|
Assignment: "tag",
|
|
},
|
|
{
|
|
Name: "value",
|
|
Type: "float64",
|
|
},
|
|
},
|
|
expected: []telegraf.Metric{
|
|
metric.New(
|
|
"binary",
|
|
map[string]string{"address": "258"},
|
|
map[string]interface{}{"value": float64(42.123)},
|
|
time.Unix(1658774489, 0),
|
|
),
|
|
},
|
|
},
|
|
{
|
|
name: "omit",
|
|
data: []interface{}{
|
|
"2022-07-25T20:41:29+02:00", // time
|
|
uint16(0x0102), // address
|
|
float64(42.123), // value
|
|
},
|
|
entries: []Entry{
|
|
{
|
|
Type: "2006-01-02T15:04:05Z07:00",
|
|
Assignment: "time",
|
|
Timezone: "UTC",
|
|
},
|
|
{
|
|
Type: "uint16",
|
|
Omit: true,
|
|
},
|
|
{
|
|
Name: "value",
|
|
Type: "float64",
|
|
},
|
|
},
|
|
expected: []telegraf.Metric{
|
|
metric.New(
|
|
"binary",
|
|
map[string]string{},
|
|
map[string]interface{}{"value": float64(42.123)},
|
|
time.Unix(1658774489, 0),
|
|
),
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
for _, endianness := range []string{"be", "le", "host"} {
|
|
name := fmt.Sprintf("%s (%s)", tt.name, endianness)
|
|
t.Run(name, func(t *testing.T) {
|
|
parser := &Parser{
|
|
Endianness: endianness,
|
|
Configs: []Config{{Entries: tt.entries}},
|
|
Log: testutil.Logger{Name: "parsers.binary"},
|
|
metricName: "binary",
|
|
}
|
|
require.NoError(t, parser.Init())
|
|
|
|
order := determineEndianness(endianness)
|
|
data, err := generateBinary(tt.data, order)
|
|
require.NoError(t, err)
|
|
|
|
metrics, err := parser.Parse(data)
|
|
require.NoError(t, err)
|
|
|
|
var options []cmp.Option
|
|
if tt.ignoreTime {
|
|
options = append(options, testutil.IgnoreTime())
|
|
}
|
|
testutil.RequireMetricsEqual(t, tt.expected, metrics, options...)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCases(t *testing.T) {
|
|
// Get all directories in testdata
|
|
folders, err := os.ReadDir("testcases")
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, folders)
|
|
|
|
for _, f := range folders {
|
|
testcasePath := filepath.Join("testcases", f.Name())
|
|
configFilename := filepath.Join(testcasePath, "telegraf.conf")
|
|
|
|
t.Run(f.Name(), func(t *testing.T) {
|
|
// Configure the plugin
|
|
cfg := config.NewConfig()
|
|
require.NoError(t, cfg.LoadConfig(configFilename))
|
|
require.NoError(t, err)
|
|
require.Len(t, cfg.Inputs, 1)
|
|
|
|
// Tune the test-plugin
|
|
plugin := cfg.Inputs[0].Input.(*test.Plugin)
|
|
plugin.Path = testcasePath
|
|
require.NoError(t, plugin.Init())
|
|
|
|
// Gather the metrics and check for potential errors
|
|
var acc testutil.Accumulator
|
|
err := plugin.Gather(&acc)
|
|
switch len(plugin.ExpectedErrors) {
|
|
case 0:
|
|
require.NoError(t, err)
|
|
case 1:
|
|
require.ErrorContains(t, err, plugin.ExpectedErrors[0])
|
|
default:
|
|
require.Contains(t, plugin.ExpectedErrors, err.Error())
|
|
}
|
|
|
|
// Determine checking options
|
|
options := []cmp.Option{
|
|
cmpopts.EquateApprox(0, 1e-6),
|
|
}
|
|
if plugin.ShouldIgnoreTimestamp {
|
|
options = append(options, testutil.IgnoreTime())
|
|
}
|
|
|
|
// Process expected metrics and compare with resulting metrics
|
|
actual := acc.GetTelegrafMetrics()
|
|
testutil.RequireMetricsEqual(t, plugin.Expected, actual, options...)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHexEncoding(t *testing.T) {
|
|
testdata := []interface{}{
|
|
uint64(0x01020304050607),
|
|
uint64(0x08090A0B0C0D0E),
|
|
uint64(0x0F101213141516),
|
|
uint64(0x1718191A1B1C1D),
|
|
uint64(0x1E1F2021222324),
|
|
}
|
|
|
|
parser := &Parser{
|
|
Endianness: "be",
|
|
Encoding: "hex",
|
|
Configs: []Config{
|
|
{
|
|
Entries: []Entry{dummyEntry},
|
|
},
|
|
},
|
|
Log: testutil.Logger{Name: "parsers.binary"},
|
|
metricName: "binary",
|
|
}
|
|
require.NoError(t, parser.Init())
|
|
|
|
// Generate the binary data and encode it to HEX
|
|
data, err := generateBinary(testdata, binary.BigEndian)
|
|
require.NoError(t, err)
|
|
encoded := hex.EncodeToString(data)
|
|
|
|
metrics, err := parser.Parse([]byte(encoded))
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, metrics)
|
|
}
|
|
|
|
var benchmarkData = [][]byte{
|
|
{
|
|
0x6d, 0x79, 0x68, 0x6f, 0x73, 0x74, 0x00, 0x33,
|
|
0x2e, 0x31, 0x31, 0x2e, 0x35, 0x00, 0x70, 0x79,
|
|
0x74, 0x68, 0x6f, 0x6e, 0x00, 0x40, 0x14, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00,
|
|
},
|
|
{
|
|
0x6d, 0x79, 0x68, 0x6f, 0x73, 0x74, 0x00, 0x33,
|
|
0x2e, 0x31, 0x31, 0x2e, 0x34, 0x00, 0x70, 0x79,
|
|
0x74, 0x68, 0x6f, 0x6e, 0x00, 0x40, 0x10, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00,
|
|
},
|
|
}
|
|
|
|
func TestBenchmarkData(t *testing.T) {
|
|
plugin := &Parser{
|
|
Endianness: "be",
|
|
Configs: []Config{
|
|
{
|
|
MetricName: "benchmark",
|
|
Entries: []Entry{
|
|
{
|
|
Name: "source",
|
|
Type: "string",
|
|
Assignment: "tag",
|
|
Terminator: "null",
|
|
},
|
|
{
|
|
Name: "tags_sdkver",
|
|
Type: "string",
|
|
Assignment: "tag",
|
|
Terminator: "null",
|
|
},
|
|
{
|
|
Name: "tags_platform",
|
|
Type: "string",
|
|
Assignment: "tag",
|
|
Terminator: "null",
|
|
},
|
|
{
|
|
Name: "value",
|
|
Type: "float64",
|
|
Assignment: "field",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, plugin.Init())
|
|
|
|
expected := []telegraf.Metric{
|
|
metric.New(
|
|
"benchmark",
|
|
map[string]string{
|
|
"source": "myhost",
|
|
"tags_platform": "python",
|
|
"tags_sdkver": "3.11.5",
|
|
},
|
|
map[string]interface{}{
|
|
"value": 5.0,
|
|
},
|
|
time.Unix(0, 0),
|
|
),
|
|
metric.New(
|
|
"benchmark",
|
|
map[string]string{
|
|
"source": "myhost",
|
|
"tags_platform": "python",
|
|
"tags_sdkver": "3.11.4",
|
|
},
|
|
map[string]interface{}{
|
|
"value": 4.0,
|
|
},
|
|
time.Unix(0, 0),
|
|
),
|
|
}
|
|
|
|
actual := make([]telegraf.Metric, 0, 2)
|
|
for _, buf := range benchmarkData {
|
|
m, err := plugin.Parse(buf)
|
|
require.NoError(t, err)
|
|
actual = append(actual, m...)
|
|
}
|
|
testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime(), testutil.SortMetrics())
|
|
}
|
|
|
|
func BenchmarkParsing(b *testing.B) {
|
|
plugin := &Parser{
|
|
Endianness: "be",
|
|
Configs: []Config{
|
|
{
|
|
MetricName: "benchmark",
|
|
Entries: []Entry{
|
|
{
|
|
Name: "source",
|
|
Type: "string",
|
|
Assignment: "tag",
|
|
Terminator: "null",
|
|
},
|
|
{
|
|
Name: "tags_sdkver",
|
|
Type: "string",
|
|
Assignment: "tag",
|
|
Terminator: "null",
|
|
},
|
|
{
|
|
Name: "tags_platform",
|
|
Type: "string",
|
|
Assignment: "tag",
|
|
Terminator: "null",
|
|
},
|
|
{
|
|
Name: "value",
|
|
Type: "float64",
|
|
Assignment: "field",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
require.NoError(b, plugin.Init())
|
|
|
|
for n := 0; n < b.N; n++ {
|
|
//nolint:errcheck // Benchmarking so skip the error check to avoid the unnecessary operations
|
|
plugin.Parse(benchmarkData[n%2])
|
|
}
|
|
}
|