//go:build linux // +build linux package dpdk import ( "encoding/json" "fmt" "net" "strings" "testing" "time" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/influxdata/telegraf" "github.com/influxdata/telegraf/filter" "github.com/influxdata/telegraf/plugins/inputs/dpdk/mocks" "github.com/influxdata/telegraf/testutil" ) func Test_Init(t *testing.T) { t.Run("when SocketPath field isn't set then it should be set to default value", func(t *testing.T) { _, dpdk, _ := prepareEnvironment() dpdk.SocketPath = "" require.Equal(t, "", dpdk.SocketPath) _ = dpdk.Init() require.Equal(t, defaultPathToSocket, dpdk.SocketPath) }) t.Run("when commands are in invalid format (doesn't start with '/') then error should be returned", func(t *testing.T) { pathToSocket, socket := createSocketForTest(t) defer socket.Close() dpdk := dpdk{ SocketPath: pathToSocket, AdditionalCommands: []string{"invalid"}, } err := dpdk.Init() require.Error(t, err) require.Contains(t, err.Error(), "command should start with '/'") }) t.Run("when all values are valid, then no error should be returned", func(t *testing.T) { pathToSocket, socket := createSocketForTest(t) defer socket.Close() dpdk := dpdk{ SocketPath: pathToSocket, DeviceTypes: []string{"ethdev"}, Log: testutil.Logger{}, } go simulateSocketResponse(socket, t) err := dpdk.Init() require.NoError(t, err) }) t.Run("when device_types and additional_commands are empty, then error should be returned", func(t *testing.T) { pathToSocket, socket := createSocketForTest(t) defer socket.Close() dpdk := dpdk{ SocketPath: pathToSocket, DeviceTypes: []string{}, AdditionalCommands: []string{}, Log: testutil.Logger{}, } err := dpdk.Init() require.Error(t, err) require.Contains(t, err.Error(), "plugin was configured with nothing to read") }) } func Test_validateCommands(t *testing.T) { t.Run("when validating commands in correct format then no error should be returned", func(t *testing.T) { dpdk := dpdk{ AdditionalCommands: []string{"/test", "/help"}, } err := dpdk.validateCommands() require.NoError(t, err) }) t.Run("when validating command that doesn't begin with slash then error should be returned", func(t *testing.T) { dpdk := dpdk{ AdditionalCommands: []string{ "/test", "commandWithoutSlash", }, } err := dpdk.validateCommands() require.Error(t, err) require.Contains(t, err.Error(), "command should start with '/'") }) t.Run("when validating long command (without parameters) then error should be returned", func(t *testing.T) { dpdk := dpdk{ AdditionalCommands: []string{ "/test", "/" + strings.Repeat("a", maxCommandLength), }, } err := dpdk.validateCommands() require.Error(t, err) require.Contains(t, err.Error(), "command is too long") }) t.Run("when validating long command (with params) then error should be returned", func(t *testing.T) { dpdk := dpdk{ AdditionalCommands: []string{ "/test", "/," + strings.Repeat("a", maxCommandLengthWithParams), }, } err := dpdk.validateCommands() require.Error(t, err) require.Contains(t, err.Error(), "shall be less than 1024 characters") }) t.Run("when validating empty command then error should be returned", func(t *testing.T) { dpdk := dpdk{ AdditionalCommands: []string{ "/test", "", }, } err := dpdk.validateCommands() require.Error(t, err) require.Contains(t, err.Error(), "got empty command") }) t.Run("when validating commands with duplicates then duplicates should be removed and no error should be returned", func(t *testing.T) { dpdk := dpdk{ AdditionalCommands: []string{ "/test", "/test", }, } require.Equal(t, 2, len(dpdk.AdditionalCommands)) err := dpdk.validateCommands() require.Equal(t, 1, len(dpdk.AdditionalCommands)) require.NoError(t, err) }) } func Test_dpdkPluginDescriber(t *testing.T) { dpdk := dpdk{} t.Run("sampleConfig function should return value from constant", func(t *testing.T) { require.Equal(t, sampleConfig, dpdk.SampleConfig()) }) t.Run("description function should return value from constant", func(t *testing.T) { require.Equal(t, description, dpdk.Description()) }) } func prepareEnvironment() (*mocks.Conn, dpdk, *testutil.Accumulator) { mockConnection := &mocks.Conn{} dpdk := dpdk{ connector: &dpdkConnector{ connection: mockConnection, maxOutputLen: 1024, accessTimeout: 2 * time.Second, }, Log: testutil.Logger{}, } mockAcc := &testutil.Accumulator{} return mockConnection, dpdk, mockAcc } func Test_processCommand(t *testing.T) { t.Run("should pass if received valid response", func(t *testing.T) { mockConn, dpdk, mockAcc := prepareEnvironment() defer mockConn.AssertExpectations(t) response := `{"/": ["/", "/eal/app_params", "/eal/params", "/ethdev/link_status"]}` simulateResponse(mockConn, response, nil) dpdk.processCommand(mockAcc, "/") require.Equal(t, 0, len(mockAcc.Errors)) }) t.Run("if received a non-JSON object then should return error", func(t *testing.T) { mockConn, dpdk, mockAcc := prepareEnvironment() defer mockConn.AssertExpectations(t) response := `notAJson` simulateResponse(mockConn, response, nil) dpdk.processCommand(mockAcc, "/") require.Equal(t, 1, len(mockAcc.Errors)) require.Contains(t, mockAcc.Errors[0].Error(), "invalid character") }) t.Run("if failed to get command response then accumulator should contain error", func(t *testing.T) { mockConn, dpdk, mockAcc := prepareEnvironment() defer mockConn.AssertExpectations(t) mockConn.On("Write", mock.Anything).Return(0, fmt.Errorf("deadline exceeded")) mockConn.On("SetDeadline", mock.Anything).Return(nil) mockConn.On("Close").Return(nil) dpdk.processCommand(mockAcc, "/") require.Equal(t, 1, len(mockAcc.Errors)) require.Contains(t, mockAcc.Errors[0].Error(), "deadline exceeded") }) t.Run("if response contains nil or empty value then error should be returned in accumulator", func(t *testing.T) { mockConn, dpdk, mockAcc := prepareEnvironment() defer mockConn.AssertExpectations(t) response := `{"/test": null}` simulateResponse(mockConn, response, nil) dpdk.processCommand(mockAcc, "/test,param") require.Equal(t, 1, len(mockAcc.Errors)) require.Contains(t, mockAcc.Errors[0].Error(), "got empty json on") }) } func Test_appendCommandsWithParams(t *testing.T) { t.Run("when got valid data, then valid commands with params should be created", func(t *testing.T) { mockConn, dpdk, _ := prepareEnvironment() defer mockConn.AssertExpectations(t) response := `{"/testendpoint": [1,123]}` simulateResponse(mockConn, response, nil) expectedCommands := []string{"/action1,1", "/action1,123", "/action2,1", "/action2,123"} result, err := dpdk.appendCommandsWithParamsFromList("/testendpoint", []string{"/action1", "/action2"}) require.NoError(t, err) require.Equal(t, 4, len(result)) require.ElementsMatch(t, result, expectedCommands) }) } func Test_getCommandsAndParamsCombinations(t *testing.T) { t.Run("when 2 ethdev commands are enabled, then 2*numberOfIds new commands should be appended", func(t *testing.T) { mockConn, dpdk, mockAcc := prepareEnvironment() defer mockConn.AssertExpectations(t) response := fmt.Sprintf(`{"%s": [1, 123]}`, ethdevListCommand) simulateResponse(mockConn, response, nil) expectedCommands := []string{"/ethdev/stats,1", "/ethdev/stats,123", "/ethdev/xstats,1", "/ethdev/xstats,123"} dpdk.DeviceTypes = []string{"ethdev"} dpdk.ethdevCommands = []string{"/ethdev/stats", "/ethdev/xstats"} dpdk.ethdevExcludedCommandsFilter, _ = filter.Compile([]string{}) dpdk.AdditionalCommands = []string{} commands := dpdk.gatherCommands(mockAcc) require.ElementsMatch(t, commands, expectedCommands) require.Equal(t, 0, len(mockAcc.Errors)) }) t.Run("when 1 rawdev command is enabled, then 2*numberOfIds new commands should be appended", func(t *testing.T) { mockConn, dpdk, mockAcc := prepareEnvironment() defer mockConn.AssertExpectations(t) response := fmt.Sprintf(`{"%s": [1, 123]}`, rawdevListCommand) simulateResponse(mockConn, response, nil) expectedCommands := []string{"/rawdev/xstats,1", "/rawdev/xstats,123"} dpdk.DeviceTypes = []string{"rawdev"} dpdk.rawdevCommands = []string{"/rawdev/xstats"} dpdk.AdditionalCommands = []string{} commands := dpdk.gatherCommands(mockAcc) require.ElementsMatch(t, commands, expectedCommands) require.Equal(t, 0, len(mockAcc.Errors)) }) t.Run("when 2 ethdev commands are enabled but one command is disabled, then numberOfIds new commands should be appended", func(t *testing.T) { mockConn, dpdk, mockAcc := prepareEnvironment() defer mockConn.AssertExpectations(t) response := fmt.Sprintf(`{"%s": [1, 123]}`, ethdevListCommand) simulateResponse(mockConn, response, nil) expectedCommands := []string{"/ethdev/stats,1", "/ethdev/stats,123"} dpdk.DeviceTypes = []string{"ethdev"} dpdk.ethdevCommands = []string{"/ethdev/stats", "/ethdev/xstats"} dpdk.ethdevExcludedCommandsFilter, _ = filter.Compile([]string{"/ethdev/xstats"}) dpdk.AdditionalCommands = []string{} commands := dpdk.gatherCommands(mockAcc) require.ElementsMatch(t, commands, expectedCommands) require.Equal(t, 0, len(mockAcc.Errors)) }) t.Run("when ethdev commands are enabled but params fetching command returns error then error should be logged in accumulator", func(t *testing.T) { mockConn, dpdk, mockAcc := prepareEnvironment() defer mockConn.AssertExpectations(t) simulateResponse(mockConn, `{notAJson}`, fmt.Errorf("some error")) dpdk.DeviceTypes = []string{"ethdev"} dpdk.ethdevCommands = []string{"/ethdev/stats", "/ethdev/xstats"} dpdk.ethdevExcludedCommandsFilter, _ = filter.Compile([]string{}) dpdk.AdditionalCommands = []string{} commands := dpdk.gatherCommands(mockAcc) require.Equal(t, 0, len(commands)) require.Equal(t, 1, len(mockAcc.Errors)) }) } func Test_Gather(t *testing.T) { t.Run("When parsing a plain json without nested object, then its key should be equal to \"\"", func(t *testing.T) { mockConn, dpdk, mockAcc := prepareEnvironment() defer mockConn.AssertExpectations(t) dpdk.AdditionalCommands = []string{"/endpoint1"} simulateResponse(mockConn, `{"/endpoint1":"myvalue"}`, nil) err := dpdk.Gather(mockAcc) require.NoError(t, err) require.Equal(t, 0, len(mockAcc.Errors)) expected := []telegraf.Metric{ testutil.MustMetric( "dpdk", map[string]string{ "command": "/endpoint1", "params": "", }, map[string]interface{}{ "": "myvalue", }, time.Unix(0, 0), ), } actual := mockAcc.GetTelegrafMetrics() testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime()) }) t.Run("When parsing a list of value in nested object then list should be flattened", func(t *testing.T) { mockConn, dpdk, mockAcc := prepareEnvironment() defer mockConn.AssertExpectations(t) dpdk.AdditionalCommands = []string{"/endpoint1"} simulateResponse(mockConn, `{"/endpoint1":{"myvalue":[0,1,123]}}`, nil) err := dpdk.Gather(mockAcc) require.NoError(t, err) require.Equal(t, 0, len(mockAcc.Errors)) expected := []telegraf.Metric{ testutil.MustMetric( "dpdk", map[string]string{ "command": "/endpoint1", "params": "", }, map[string]interface{}{ "myvalue_0": float64(0), "myvalue_1": float64(1), "myvalue_2": float64(123), }, time.Unix(0, 0), ), } actual := mockAcc.GetTelegrafMetrics() testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime()) }) } func simulateResponse(mockConn *mocks.Conn, response string, readErr error) { mockConn.On("Write", mock.Anything).Return(0, nil) mockConn.On("Read", mock.Anything).Run(func(arg mock.Arguments) { elem := arg.Get(0).([]byte) copy(elem, response) }).Return(len(response), readErr) mockConn.On("SetDeadline", mock.Anything).Return(nil) if readErr != nil { mockConn.On("Close").Return(nil) } } func simulateSocketResponse(socket net.Listener, t *testing.T) { conn, err := socket.Accept() require.NoError(t, err) initMessage, err := json.Marshal(initMessage{MaxOutputLen: 1}) require.NoError(t, err) _, err = conn.Write(initMessage) require.NoError(t, err) }