1
0
Fork 0
telegraf/plugins/parsers/binary/parser_test.go
Daniel Baumann 4978089aab
Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-05-24 07:26:29 +02:00

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