1
0
Fork 0
telegraf/plugins/common/opcua/input/input_client_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

890 lines
22 KiB
Go

package input
import (
"errors"
"testing"
"time"
"github.com/gopcua/opcua/ua"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/metric"
"github.com/influxdata/telegraf/plugins/common/opcua"
"github.com/influxdata/telegraf/testutil"
)
func TestTagsSliceToMap(t *testing.T) {
m, err := tagsSliceToMap([][]string{{"foo", "bar"}, {"baz", "bat"}})
require.NoError(t, err)
require.Len(t, m, 2)
require.Equal(t, "bar", m["foo"])
require.Equal(t, "bat", m["baz"])
}
func TestTagsSliceToMap_twoStrings(t *testing.T) {
var err error
_, err = tagsSliceToMap([][]string{{"foo", "bar", "baz"}})
require.Error(t, err)
_, err = tagsSliceToMap([][]string{{"foo"}})
require.Error(t, err)
}
func TestTagsSliceToMap_dupeKey(t *testing.T) {
_, err := tagsSliceToMap([][]string{{"foo", "bar"}, {"foo", "bat"}})
require.Error(t, err)
}
func TestTagsSliceToMap_empty(t *testing.T) {
_, err := tagsSliceToMap([][]string{{"foo", ""}})
require.Equal(t, errors.New("tag 1 has empty value"), err)
_, err = tagsSliceToMap([][]string{{"", "bar"}})
require.Equal(t, errors.New("tag 1 has empty name"), err)
}
func TestValidateOPCTags(t *testing.T) {
tests := []struct {
name string
config InputClientConfig
err error
}{
{
"duplicates",
InputClientConfig{
MetricName: "mn",
RootNodes: []NodeSettings{
{
FieldName: "fn",
Namespace: "2",
IdentifierType: "s",
Identifier: "i1",
TagsSlice: [][]string{{"t1", "v1"}, {"t2", "v2"}},
},
},
Groups: []NodeGroupSettings{
{
Nodes: []NodeSettings{
{
FieldName: "fn",
Namespace: "2",
IdentifierType: "s",
Identifier: "i1",
},
},
TagsSlice: [][]string{{"t1", "v1"}, {"t2", "v2"}},
},
},
},
errors.New(`name "fn" is duplicated (metric name "mn", tags "t1=v1, t2=v2")`),
},
{
"empty tag value not allowed",
InputClientConfig{
MetricName: "mn",
RootNodes: []NodeSettings{
{
FieldName: "fn",
IdentifierType: "s",
TagsSlice: [][]string{{"t1", ""}},
},
},
},
errors.New("tag 1 has empty value"),
},
{
"empty tag name not allowed",
InputClientConfig{
MetricName: "mn",
RootNodes: []NodeSettings{
{
FieldName: "fn",
IdentifierType: "s",
TagsSlice: [][]string{{"", "1"}},
},
},
},
errors.New("tag 1 has empty name"),
},
{
"different metric tag names",
InputClientConfig{
MetricName: "mn",
RootNodes: []NodeSettings{
{
FieldName: "fn",
Namespace: "2",
IdentifierType: "s",
Identifier: "i1",
TagsSlice: [][]string{{"t1", "v1"}, {"t2", "v2"}},
},
{
FieldName: "fn",
Namespace: "2",
IdentifierType: "s",
Identifier: "i1",
TagsSlice: [][]string{{"t1", "v1"}, {"t3", "v2"}},
},
},
},
nil,
},
{
"different metric tag values",
InputClientConfig{
MetricName: "mn",
RootNodes: []NodeSettings{
{
FieldName: "fn",
Namespace: "2",
IdentifierType: "s",
Identifier: "i1",
TagsSlice: [][]string{{"t1", "foo"}, {"t2", "v2"}},
},
{
FieldName: "fn",
Namespace: "2",
IdentifierType: "s",
Identifier: "i1",
TagsSlice: [][]string{{"t1", "bar"}, {"t2", "v2"}},
},
},
},
nil,
},
{
"different metric names",
InputClientConfig{
MetricName: "mn",
Groups: []NodeGroupSettings{
{
MetricName: "mn",
Namespace: "2",
Nodes: []NodeSettings{
{
FieldName: "fn",
IdentifierType: "s",
Identifier: "i1",
TagsSlice: [][]string{{"t1", "v1"}, {"t2", "v2"}},
},
},
},
{
MetricName: "mn2",
Namespace: "2",
Nodes: []NodeSettings{
{
FieldName: "fn",
IdentifierType: "s",
Identifier: "i1",
TagsSlice: [][]string{{"t1", "v1"}, {"t2", "v2"}},
},
},
},
},
},
nil,
},
{
"different field names",
InputClientConfig{
MetricName: "mn",
RootNodes: []NodeSettings{
{
FieldName: "fn",
Namespace: "2",
IdentifierType: "s",
Identifier: "i1",
TagsSlice: [][]string{{"t1", "v1"}, {"t2", "v2"}},
},
{
FieldName: "fn2",
Namespace: "2",
IdentifierType: "s",
Identifier: "i1",
TagsSlice: [][]string{{"t1", "v1"}, {"t2", "v2"}},
},
},
},
nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := OpcUAInputClient{
Config: tt.config,
Log: testutil.Logger{},
}
require.Equal(t, tt.err, o.InitNodeMetricMapping())
})
}
}
func TestNewNodeMetricMappingTags(t *testing.T) {
tests := []struct {
name string
settings NodeSettings
groupTags map[string]string
expectedTags map[string]string
err error
}{
{
name: "empty tags",
settings: NodeSettings{
FieldName: "f",
Namespace: "2",
IdentifierType: "s",
Identifier: "h",
},
groupTags: map[string]string{},
expectedTags: map[string]string{},
err: nil,
},
{
name: "node tags only",
settings: NodeSettings{
FieldName: "f",
Namespace: "2",
IdentifierType: "s",
Identifier: "h",
TagsSlice: [][]string{{"t1", "v1"}},
},
groupTags: map[string]string{},
expectedTags: map[string]string{"t1": "v1"},
err: nil,
},
{
name: "group tags only",
settings: NodeSettings{
FieldName: "f",
Namespace: "2",
IdentifierType: "s",
Identifier: "h",
},
groupTags: map[string]string{"t1": "v1"},
expectedTags: map[string]string{"t1": "v1"},
err: nil,
},
{
name: "node tag overrides group tags",
settings: NodeSettings{
FieldName: "f",
Namespace: "2",
IdentifierType: "s",
Identifier: "h",
TagsSlice: [][]string{{"t1", "v2"}},
},
groupTags: map[string]string{"t1": "v1"},
expectedTags: map[string]string{"t1": "v2"},
err: nil,
},
{
name: "node tag merged with group tags",
settings: NodeSettings{
FieldName: "f",
Namespace: "2",
IdentifierType: "s",
Identifier: "h",
TagsSlice: [][]string{{"t2", "v2"}},
},
groupTags: map[string]string{"t1": "v1"},
expectedTags: map[string]string{"t1": "v1", "t2": "v2"},
err: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
nmm, err := NewNodeMetricMapping("testmetric", tt.settings, tt.groupTags)
require.Equal(t, tt.err, err)
require.Equal(t, tt.expectedTags, nmm.MetricTags)
})
}
}
func TestNewNodeMetricMappingIdStrInstantiated(t *testing.T) {
nmm, err := NewNodeMetricMapping("testmetric", NodeSettings{
FieldName: "f",
Namespace: "2",
IdentifierType: "s",
Identifier: "h",
}, map[string]string{})
require.NoError(t, err)
require.Equal(t, "ns=2;s=h", nmm.idStr)
}
func TestValidateNodeToAdd(t *testing.T) {
tests := []struct {
name string
existing map[metricParts]struct{}
nmm *NodeMetricMapping
err error
}{
{
name: "valid",
existing: map[metricParts]struct{}{},
nmm: func() *NodeMetricMapping {
nmm, err := NewNodeMetricMapping("testmetric", NodeSettings{
FieldName: "f",
Namespace: "2",
IdentifierType: "s",
Identifier: "hf",
}, map[string]string{})
require.NoError(t, err)
return nmm
}(),
err: nil,
},
{
name: "empty field name not allowed",
existing: map[metricParts]struct{}{},
nmm: func() *NodeMetricMapping {
nmm, err := NewNodeMetricMapping("testmetric", NodeSettings{
FieldName: "",
Namespace: "2",
IdentifierType: "s",
Identifier: "hf",
}, map[string]string{})
require.NoError(t, err)
return nmm
}(),
err: errors.New(`empty name in ""`),
},
{
name: "empty namespace not allowed",
existing: map[metricParts]struct{}{},
nmm: func() *NodeMetricMapping {
nmm, err := NewNodeMetricMapping("testmetric", NodeSettings{
FieldName: "f",
Namespace: "",
IdentifierType: "s",
Identifier: "hf",
}, map[string]string{})
require.NoError(t, err)
return nmm
}(),
err: errors.New("empty node namespace not allowed"),
},
{
name: "empty identifier type not allowed",
existing: map[metricParts]struct{}{},
nmm: func() *NodeMetricMapping {
nmm, err := NewNodeMetricMapping("testmetric", NodeSettings{
FieldName: "f",
Namespace: "2",
IdentifierType: "",
Identifier: "hf",
}, map[string]string{})
require.NoError(t, err)
return nmm
}(),
err: errors.New(`invalid identifier type "" in "f"`),
},
{
name: "invalid identifier type not allowed",
existing: map[metricParts]struct{}{},
nmm: func() *NodeMetricMapping {
nmm, err := NewNodeMetricMapping("testmetric", NodeSettings{
FieldName: "f",
Namespace: "2",
IdentifierType: "j",
Identifier: "hf",
}, map[string]string{})
require.NoError(t, err)
return nmm
}(),
err: errors.New(`invalid identifier type "j" in "f"`),
},
{
name: "duplicate metric not allowed",
existing: map[metricParts]struct{}{
{metricName: "testmetric", fieldName: "f", tags: "t1=v1, t2=v2"}: {},
},
nmm: func() *NodeMetricMapping {
nmm, err := NewNodeMetricMapping("testmetric", NodeSettings{
FieldName: "f",
Namespace: "2",
IdentifierType: "s",
Identifier: "hf",
TagsSlice: [][]string{{"t1", "v1"}, {"t2", "v2"}},
}, map[string]string{})
require.NoError(t, err)
return nmm
}(),
err: errors.New(`name "f" is duplicated (metric name "testmetric", tags "t1=v1, t2=v2")`),
},
{
name: "identifier type mismatch",
existing: map[metricParts]struct{}{},
nmm: func() *NodeMetricMapping {
nmm, err := NewNodeMetricMapping("testmetric", NodeSettings{
FieldName: "f",
Namespace: "2",
IdentifierType: "i",
Identifier: "hf",
}, map[string]string{})
require.NoError(t, err)
return nmm
}(),
err: errors.New(`identifier type "i" does not match the type of identifier "hf"`),
},
}
for idT, idV := range map[string]string{
"s": "hf",
"i": "1",
"g": "849683f0-ce92-4fa2-836f-a02cde61d75d",
"b": "aGVsbG8gSSBhbSBhIHRlc3QgaWRlbnRpZmllcg=="} {
tests = append(tests, struct {
name string
existing map[metricParts]struct{}
nmm *NodeMetricMapping
err error
}{
name: "identifier type " + idT + " allowed",
existing: map[metricParts]struct{}{},
nmm: func() *NodeMetricMapping {
nmm, err := NewNodeMetricMapping("testmetric", NodeSettings{
FieldName: "f",
Namespace: "2",
IdentifierType: idT,
Identifier: idV,
}, map[string]string{})
require.NoError(t, err)
return nmm
}(),
err: nil,
})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateNodeToAdd(tt.existing, tt.nmm)
require.Equal(t, tt.err, err)
})
}
}
func TestInitNodeMetricMapping(t *testing.T) {
tests := []struct {
testname string
config InputClientConfig
expected []NodeMetricMapping
err error
}{
{
testname: "only root node",
config: InputClientConfig{
MetricName: "testmetric",
Timestamp: TimestampSourceTelegraf,
RootNodes: []NodeSettings{
{
FieldName: "f",
Namespace: "2",
IdentifierType: "s",
Identifier: "id1",
TagsSlice: [][]string{{"t1", "v1"}},
},
},
},
expected: []NodeMetricMapping{
{
Tag: NodeSettings{
FieldName: "f",
Namespace: "2",
IdentifierType: "s",
Identifier: "id1",
TagsSlice: [][]string{{"t1", "v1"}},
},
idStr: "ns=2;s=id1",
metricName: "testmetric",
MetricTags: map[string]string{"t1": "v1"},
},
},
err: nil,
},
{
testname: "root node and group node",
config: InputClientConfig{
MetricName: "testmetric",
Timestamp: TimestampSourceTelegraf,
RootNodes: []NodeSettings{
{
FieldName: "f",
Namespace: "2",
IdentifierType: "s",
Identifier: "id1",
TagsSlice: [][]string{{"t1", "v1"}},
},
},
Groups: []NodeGroupSettings{
{
MetricName: "groupmetric",
Namespace: "3",
IdentifierType: "s",
Nodes: []NodeSettings{
{
FieldName: "f",
Identifier: "id2",
TagsSlice: [][]string{{"t2", "v2"}},
},
},
},
},
},
expected: []NodeMetricMapping{
{
Tag: NodeSettings{
FieldName: "f",
Namespace: "2",
IdentifierType: "s",
Identifier: "id1",
TagsSlice: [][]string{{"t1", "v1"}},
},
idStr: "ns=2;s=id1",
metricName: "testmetric",
MetricTags: map[string]string{"t1": "v1"},
},
{
Tag: NodeSettings{
FieldName: "f",
Namespace: "3",
IdentifierType: "s",
Identifier: "id2",
TagsSlice: [][]string{{"t2", "v2"}},
},
idStr: "ns=3;s=id2",
metricName: "groupmetric",
MetricTags: map[string]string{"t2": "v2"},
},
},
err: nil,
},
{
testname: "only group node",
config: InputClientConfig{
MetricName: "testmetric",
Timestamp: TimestampSourceTelegraf,
Groups: []NodeGroupSettings{
{
MetricName: "groupmetric",
Namespace: "3",
IdentifierType: "s",
Nodes: []NodeSettings{
{
FieldName: "f",
Identifier: "id2",
TagsSlice: [][]string{{"t2", "v2"}},
},
},
},
},
},
expected: []NodeMetricMapping{
{
Tag: NodeSettings{
FieldName: "f",
Namespace: "3",
IdentifierType: "s",
Identifier: "id2",
TagsSlice: [][]string{{"t2", "v2"}},
},
idStr: "ns=3;s=id2",
metricName: "groupmetric",
MetricTags: map[string]string{"t2": "v2"},
},
},
err: nil,
},
{
testname: "tags and default only default tags used",
config: InputClientConfig{
MetricName: "testmetric",
Timestamp: TimestampSourceTelegraf,
Groups: []NodeGroupSettings{
{
MetricName: "groupmetric",
Namespace: "3",
IdentifierType: "s",
Nodes: []NodeSettings{
{
FieldName: "f",
Identifier: "id2",
TagsSlice: [][]string{{"t2", "v2"}},
DefaultTags: map[string]string{"t3": "v3"},
},
},
},
},
},
expected: []NodeMetricMapping{
{
Tag: NodeSettings{
FieldName: "f",
Namespace: "3",
IdentifierType: "s",
Identifier: "id2",
TagsSlice: [][]string{{"t2", "v2"}},
DefaultTags: map[string]string{"t3": "v3"},
},
idStr: "ns=3;s=id2",
metricName: "groupmetric",
MetricTags: map[string]string{"t3": "v3"},
},
},
err: nil,
},
{
testname: "only root node default overrides slice",
config: InputClientConfig{
MetricName: "testmetric",
Timestamp: TimestampSourceTelegraf,
RootNodes: []NodeSettings{
{
FieldName: "f",
Namespace: "2",
IdentifierType: "s",
Identifier: "id1",
TagsSlice: [][]string{{"t1", "v1"}},
DefaultTags: map[string]string{"t3": "v3"},
},
},
},
expected: []NodeMetricMapping{
{
Tag: NodeSettings{
FieldName: "f",
Namespace: "2",
IdentifierType: "s",
Identifier: "id1",
TagsSlice: [][]string{{"t1", "v1"}},
DefaultTags: map[string]string{"t3": "v3"},
},
idStr: "ns=2;s=id1",
metricName: "testmetric",
MetricTags: map[string]string{"t3": "v3"},
},
},
err: nil,
},
}
for _, tt := range tests {
t.Run(tt.testname, func(t *testing.T) {
o := OpcUAInputClient{Config: tt.config}
err := o.InitNodeMetricMapping()
require.NoError(t, err)
require.Equal(t, tt.expected, o.NodeMetricMapping)
})
}
}
func TestUpdateNodeValue(t *testing.T) {
type testStep struct {
nodeIdx int
value interface{}
status ua.StatusCode
expected interface{}
}
tests := []struct {
testname string
steps []testStep
}{
{
"value should update when code ok",
[]testStep{
{
0,
"Harmony",
ua.StatusOK,
"Harmony",
},
},
},
{
"value should not update when code bad",
[]testStep{
{
0,
"Harmony",
ua.StatusOK,
"Harmony",
},
{
0,
"Odium",
ua.StatusBad,
"Harmony",
},
{
0,
"Ati",
ua.StatusOK,
"Ati",
},
},
},
}
conf := &opcua.OpcUAClientConfig{
Endpoint: "opc.tcp://localhost:4930",
SecurityPolicy: "None",
SecurityMode: "None",
AuthMethod: "",
ConnectTimeout: config.Duration(2 * time.Second),
RequestTimeout: config.Duration(2 * time.Second),
Workarounds: opcua.OpcUAWorkarounds{},
}
c, err := conf.CreateClient(testutil.Logger{})
require.NoError(t, err)
o := OpcUAInputClient{
OpcUAClient: c,
Log: testutil.Logger{},
NodeMetricMapping: []NodeMetricMapping{
{
Tag: NodeSettings{
FieldName: "f",
},
},
{
Tag: NodeSettings{
FieldName: "f2",
},
},
},
LastReceivedData: make([]NodeValue, 2),
}
for _, tt := range tests {
t.Run(tt.testname, func(t *testing.T) {
o.LastReceivedData = make([]NodeValue, 2)
for i, step := range tt.steps {
v, err := ua.NewVariant(step.value)
require.NoError(t, err)
o.UpdateNodeValue(0, &ua.DataValue{
Value: v,
Status: step.status,
SourceTimestamp: time.Date(2022, 03, 17, 8, 33, 00, 00, &time.Location{}).Add(time.Duration(i) * time.Second),
SourcePicoseconds: 0,
ServerTimestamp: time.Date(2022, 03, 17, 8, 33, 00, 500, &time.Location{}).Add(time.Duration(i) * time.Second),
ServerPicoseconds: 0,
})
require.Equal(t, step.expected, o.LastReceivedData[0].Value)
}
})
}
}
func TestMetricForNode(t *testing.T) {
conf := &opcua.OpcUAClientConfig{
Endpoint: "opc.tcp://localhost:4930",
SecurityPolicy: "None",
SecurityMode: "None",
AuthMethod: "",
ConnectTimeout: config.Duration(2 * time.Second),
RequestTimeout: config.Duration(2 * time.Second),
Workarounds: opcua.OpcUAWorkarounds{},
}
c, err := conf.CreateClient(testutil.Logger{})
require.NoError(t, err)
o := OpcUAInputClient{
Config: InputClientConfig{
Timestamp: TimestampSourceSource,
},
OpcUAClient: c,
Log: testutil.Logger{},
LastReceivedData: make([]NodeValue, 2),
}
tests := []struct {
testname string
nmm []NodeMetricMapping
v interface{}
isArray bool
dataType ua.TypeID
time time.Time
status ua.StatusCode
expected telegraf.Metric
}{
{
testname: "metric build correctly",
nmm: []NodeMetricMapping{
{
Tag: NodeSettings{
FieldName: "fn",
},
idStr: "ns=3;s=hi",
metricName: "testingmetric",
MetricTags: map[string]string{"t1": "v1"},
},
},
v: 16,
isArray: false,
dataType: ua.TypeIDInt32,
time: time.Date(2022, 03, 17, 8, 55, 00, 00, &time.Location{}),
status: ua.StatusOK,
expected: metric.New("testingmetric",
map[string]string{"t1": "v1", "id": "ns=3;s=hi"},
map[string]interface{}{"Quality": "The operation succeeded. StatusGood (0x0)", "fn": 16},
time.Date(2022, 03, 17, 8, 55, 00, 00, &time.Location{})),
},
{
testname: "array-like metric build correctly",
nmm: []NodeMetricMapping{
{
Tag: NodeSettings{
FieldName: "fn",
},
idStr: "ns=3;s=hi",
metricName: "testingmetric",
MetricTags: map[string]string{"t1": "v1"},
},
},
v: []int32{16, 17},
isArray: true,
dataType: ua.TypeIDInt32,
time: time.Date(2022, 03, 17, 8, 55, 00, 00, &time.Location{}),
status: ua.StatusOK,
expected: metric.New("testingmetric",
map[string]string{"t1": "v1", "id": "ns=3;s=hi"},
map[string]interface{}{"Quality": "The operation succeeded. StatusGood (0x0)", "fn[0]": 16, "fn[1]": 17},
time.Date(2022, 03, 17, 8, 55, 00, 00, &time.Location{})),
},
{
testname: "nil does not panic",
nmm: []NodeMetricMapping{
{
Tag: NodeSettings{
FieldName: "fn",
},
idStr: "ns=3;s=hi",
metricName: "testingmetric",
MetricTags: map[string]string{"t1": "v1"},
},
},
v: nil,
isArray: false,
dataType: ua.TypeIDNull,
time: time.Date(2022, 03, 17, 8, 55, 00, 00, &time.Location{}),
status: ua.StatusOK,
expected: metric.New("testingmetric",
map[string]string{"t1": "v1", "id": "ns=3;s=hi"},
map[string]interface{}{"Quality": "The operation succeeded. StatusGood (0x0)"},
time.Date(2022, 03, 17, 8, 55, 00, 00, &time.Location{})),
},
}
for _, tt := range tests {
t.Run(tt.testname, func(t *testing.T) {
o.NodeMetricMapping = tt.nmm
o.LastReceivedData[0].SourceTime = tt.time
o.LastReceivedData[0].Quality = tt.status
o.LastReceivedData[0].Value = tt.v
o.LastReceivedData[0].DataType = tt.dataType
o.LastReceivedData[0].IsArray = tt.isArray
actual := o.MetricForNode(0)
require.Equal(t, tt.expected.Tags(), actual.Tags())
require.Equal(t, tt.expected.Fields(), actual.Fields())
require.Equal(t, tt.expected.Time(), actual.Time())
})
}
}