diff --git a/src/vpp/vpp_iface_test.go b/src/vpp/vpp_iface_test.go new file mode 100644 index 0000000..35b37ab --- /dev/null +++ b/src/vpp/vpp_iface_test.go @@ -0,0 +1,190 @@ +// Copyright 2025, IPng Networks GmbH, Pim van Pelt + +package vpp + +import ( + "go.fd.io/govpp/binapi/interface_types" + "testing" +) + +func TestNewInterfaceManager(t *testing.T) { + client := NewVPPClient() + manager := NewInterfaceManager(client) + + if manager == nil { + t.Fatal("NewInterfaceManager() returned nil") + } + + if manager.client != client { + t.Error("InterfaceManager should store the provided client") + } + + if manager.eventCallback != nil { + t.Error("InterfaceManager should have nil callback initially") + } +} + +func TestInterfaceManagerSetEventCallback(t *testing.T) { + client := NewVPPClient() + manager := NewInterfaceManager(client) + + var callbackCalled bool + var receivedDetails []InterfaceDetails + + callback := func(details []InterfaceDetails) { + callbackCalled = true + receivedDetails = details + } + + manager.SetEventCallback(callback) + + if manager.eventCallback == nil { + t.Error("SetEventCallback() should store the callback") + } + + // Test callback execution + testDetails := []InterfaceDetails{ + { + SwIfIndex: 1, + InterfaceName: "test-interface", + MacAddress: []byte{0xde, 0xad, 0xbe, 0xef, 0x00, 0x01}, + Speed: 1000000000, + AdminStatus: true, + OperStatus: true, + MTU: 1500, + }, + } + + manager.eventCallback(testDetails) + + if !callbackCalled { + t.Error("Callback should have been called") + } + + if len(receivedDetails) != 1 { + t.Errorf("Expected 1 interface detail, got %d", len(receivedDetails)) + } + + if receivedDetails[0].InterfaceName != "test-interface" { + t.Errorf("Expected interface name 'test-interface', got %q", receivedDetails[0].InterfaceName) + } +} + +func TestInterfaceManagerGetAllInterfaceDetailsWithoutConnection(t *testing.T) { + client := NewVPPClient() + manager := NewInterfaceManager(client) + + _, err := manager.GetAllInterfaceDetails() + if err == nil { + t.Error("GetAllInterfaceDetails() should return error when not connected") + } + + vppErr, ok := err.(*VPPError) + if !ok { + t.Errorf("Expected VPPError, got %T", err) + } + + if vppErr.Message != "VPP client not connected" { + t.Errorf("Expected specific error message, got: %s", vppErr.Message) + } +} + +func TestInterfaceManagerStartEventWatcherWithoutConnection(t *testing.T) { + client := NewVPPClient() + manager := NewInterfaceManager(client) + + err := manager.StartEventWatcher() + if err == nil { + t.Error("StartEventWatcher() should return error when not connected") + } + + vppErr, ok := err.(*VPPError) + if !ok { + t.Errorf("Expected VPPError, got %T", err) + } + + if vppErr.Message != "VPP client not connected" { + t.Errorf("Expected specific error message, got: %s", vppErr.Message) + } +} + +func TestInterfaceManagerHandleInterfaceEventWithoutCallback(t *testing.T) { + client := NewVPPClient() + manager := NewInterfaceManager(client) + + // Should not panic when callback is nil + manager.handleInterfaceEvent() +} + +func TestInterfaceDetails(t *testing.T) { + details := InterfaceDetails{ + SwIfIndex: interface_types.InterfaceIndex(42), + InterfaceName: "GigabitEthernet0/8/0", + MacAddress: []byte{0x02, 0xfe, 0x3c, 0x4d, 0x5e, 0x6f}, + Speed: 10000000000, // 10 Gbps + AdminStatus: true, + OperStatus: false, + MTU: 9000, + } + + if details.SwIfIndex != 42 { + t.Errorf("Expected SwIfIndex 42, got %d", details.SwIfIndex) + } + + if details.InterfaceName != "GigabitEthernet0/8/0" { + t.Errorf("Expected interface name 'GigabitEthernet0/8/0', got %q", details.InterfaceName) + } + + if len(details.MacAddress) != 6 { + t.Errorf("Expected MAC address length 6, got %d", len(details.MacAddress)) + } + + if details.Speed != 10000000000 { + t.Errorf("Expected speed 10000000000, got %d", details.Speed) + } + + if !details.AdminStatus { + t.Error("Expected AdminStatus true") + } + + if details.OperStatus { + t.Error("Expected OperStatus false") + } + + if details.MTU != 9000 { + t.Errorf("Expected MTU 9000, got %d", details.MTU) + } +} + +func TestInterfaceEventCallback(t *testing.T) { + var callbackInvoked bool + var callbackDetails []InterfaceDetails + + callback := InterfaceEventCallback(func(details []InterfaceDetails) { + callbackInvoked = true + callbackDetails = details + }) + + testDetails := []InterfaceDetails{ + {SwIfIndex: 1, InterfaceName: "test1"}, + {SwIfIndex: 2, InterfaceName: "test2"}, + } + + callback(testDetails) + + if !callbackInvoked { + t.Error("Callback should have been invoked") + } + + if len(callbackDetails) != 2 { + t.Errorf("Expected 2 interface details, got %d", len(callbackDetails)) + } + + if callbackDetails[0].InterfaceName != "test1" { + t.Errorf("Expected first interface 'test1', got %q", callbackDetails[0].InterfaceName) + } + + if callbackDetails[1].InterfaceName != "test2" { + t.Errorf("Expected second interface 'test2', got %q", callbackDetails[1].InterfaceName) + } +} diff --git a/src/vpp/vpp_stats_test.go b/src/vpp/vpp_stats_test.go new file mode 100644 index 0000000..654c15f --- /dev/null +++ b/src/vpp/vpp_stats_test.go @@ -0,0 +1,250 @@ +// Copyright 2025, IPng Networks GmbH, Pim van Pelt + +package vpp + +import ( + "testing" + "time" + + "go.fd.io/govpp/api" +) + +func TestNewStatsManager(t *testing.T) { + client := NewVPPClient() + interfaceManager := NewInterfaceManager(client) + manager := NewStatsManager(client, interfaceManager) + + if manager == nil { + t.Fatal("NewStatsManager() returned nil") + } + + if manager.client != client { + t.Error("StatsManager should store the provided client") + } + + if manager.interfaceManager != interfaceManager { + t.Error("StatsManager should store the provided interface manager") + } + + if manager.period != time.Duration(*Period)*time.Second { + t.Errorf("Expected period %v, got %v", time.Duration(*Period)*time.Second, manager.period) + } + + if manager.running { + t.Error("StatsManager should not be running initially") + } + + if manager.statsCallback != nil { + t.Error("StatsManager should have nil callback initially") + } +} + +func TestStatsManagerSetStatsCallback(t *testing.T) { + client := NewVPPClient() + interfaceManager := NewInterfaceManager(client) + manager := NewStatsManager(client, interfaceManager) + + var callbackCalled bool + var receivedStats *api.InterfaceStats + + callback := func(stats *api.InterfaceStats) { + callbackCalled = true + receivedStats = stats + } + + manager.SetStatsCallback(callback) + + if manager.statsCallback == nil { + t.Error("SetStatsCallback() should store the callback") + } + + // Test callback execution + testStats := &api.InterfaceStats{ + Interfaces: []api.InterfaceCounters{ + { + InterfaceIndex: 1, + InterfaceName: "test-interface", + Rx: api.InterfaceCounterCombined{Packets: 100, Bytes: 1500}, + Tx: api.InterfaceCounterCombined{Packets: 50, Bytes: 750}, + }, + }, + } + + manager.statsCallback(testStats) + + if !callbackCalled { + t.Error("Callback should have been called") + } + + if receivedStats != testStats { + t.Error("Callback should receive the same stats object") + } + + if len(receivedStats.Interfaces) != 1 { + t.Errorf("Expected 1 interface, got %d", len(receivedStats.Interfaces)) + } + + if receivedStats.Interfaces[0].InterfaceName != "test-interface" { + t.Errorf("Expected interface name 'test-interface', got %q", receivedStats.Interfaces[0].InterfaceName) + } +} + +func TestStatsManagerSetPeriod(t *testing.T) { + client := NewVPPClient() + interfaceManager := NewInterfaceManager(client) + manager := NewStatsManager(client, interfaceManager) + + newPeriod := 5 * time.Second + manager.SetPeriod(newPeriod) + + if manager.period != newPeriod { + t.Errorf("Expected period %v, got %v", newPeriod, manager.period) + } +} + +func TestStatsManagerStartStopStatsRoutine(t *testing.T) { + client := NewVPPClient() + interfaceManager := NewInterfaceManager(client) + manager := NewStatsManager(client, interfaceManager) + + if manager.running { + t.Error("StatsManager should not be running initially") + } + + manager.StartStatsRoutine() + + if !manager.running { + t.Error("StatsManager should be running after StartStatsRoutine()") + } + + // Test starting again (should be safe) + manager.StartStatsRoutine() + + if !manager.running { + t.Error("StatsManager should still be running after second StartStatsRoutine()") + } + + manager.StopStatsRoutine() + + if manager.running { + t.Error("StatsManager should not be running after StopStatsRoutine()") + } +} + +func TestStatsManagerGetInterfaceStatsWithoutConnection(t *testing.T) { + client := NewVPPClient() + interfaceManager := NewInterfaceManager(client) + manager := NewStatsManager(client, interfaceManager) + + _, err := manager.GetInterfaceStats() + if err == nil { + t.Error("GetInterfaceStats() should return error when not connected") + } + + vppErr, ok := err.(*VPPError) + if !ok { + t.Errorf("Expected VPPError, got %T", err) + } + + if vppErr.Message != "VPP client not connected" { + t.Errorf("Expected specific error message, got: %s", vppErr.Message) + } +} + +func TestStatsCallback(t *testing.T) { + var callbackInvoked bool + var callbackStats *api.InterfaceStats + + callback := StatsCallback(func(stats *api.InterfaceStats) { + callbackInvoked = true + callbackStats = stats + }) + + testStats := &api.InterfaceStats{ + Interfaces: []api.InterfaceCounters{ + { + InterfaceIndex: 42, + InterfaceName: "test-callback-interface", + Rx: api.InterfaceCounterCombined{Packets: 200, Bytes: 3000}, + Tx: api.InterfaceCounterCombined{Packets: 100, Bytes: 1500}, + RxUnicast: api.InterfaceCounterCombined{Packets: 180, Bytes: 2700}, + TxUnicast: api.InterfaceCounterCombined{Packets: 90, Bytes: 1350}, + }, + }, + } + + callback(testStats) + + if !callbackInvoked { + t.Error("Callback should have been invoked") + } + + if callbackStats != testStats { + t.Error("Callback should receive the same stats object") + } + + if len(callbackStats.Interfaces) != 1 { + t.Errorf("Expected 1 interface, got %d", len(callbackStats.Interfaces)) + } + + iface := callbackStats.Interfaces[0] + if iface.InterfaceIndex != 42 { + t.Errorf("Expected interface index 42, got %d", iface.InterfaceIndex) + } + + if iface.InterfaceName != "test-callback-interface" { + t.Errorf("Expected interface name 'test-callback-interface', got %q", iface.InterfaceName) + } + + if iface.Rx.Packets != 200 { + t.Errorf("Expected RX packets 200, got %d", iface.Rx.Packets) + } + + if iface.Tx.Bytes != 1500 { + t.Errorf("Expected TX bytes 1500, got %d", iface.Tx.Bytes) + } + + if iface.RxUnicast.Packets != 180 { + t.Errorf("Expected RX unicast packets 180, got %d", iface.RxUnicast.Packets) + } + + if iface.TxUnicast.Bytes != 1350 { + t.Errorf("Expected TX unicast bytes 1350, got %d", iface.TxUnicast.Bytes) + } +} + +func TestStatsManagerQueryAndReportStatsWithoutConnection(t *testing.T) { + client := NewVPPClient() + interfaceManager := NewInterfaceManager(client) + manager := NewStatsManager(client, interfaceManager) + + // Should return false when not connected + if manager.queryAndReportStats() { + t.Error("queryAndReportStats() should return false when not connected") + } +} + +func TestStatsManagerWithShortPeriod(t *testing.T) { + client := NewVPPClient() + interfaceManager := NewInterfaceManager(client) + manager := NewStatsManager(client, interfaceManager) + + // Set a very short period for testing + manager.SetPeriod(10 * time.Millisecond) + + if manager.period != 10*time.Millisecond { + t.Errorf("Expected period 10ms, got %v", manager.period) + } + + manager.StartStatsRoutine() + + // Let it run briefly + time.Sleep(50 * time.Millisecond) + + manager.StopStatsRoutine() + + // Should stop gracefully + if manager.running { + t.Error("StatsManager should have stopped") + } +} diff --git a/src/vpp/vpp_test.go b/src/vpp/vpp_test.go new file mode 100644 index 0000000..96ff40e --- /dev/null +++ b/src/vpp/vpp_test.go @@ -0,0 +1,100 @@ +// Copyright 2025, IPng Networks GmbH, Pim van Pelt + +package vpp + +import ( + "testing" +) + +func TestNewVPPClient(t *testing.T) { + client := NewVPPClient() + + if client == nil { + t.Fatal("NewVPPClient() returned nil") + } + + if client.IsConnected() { + t.Error("NewVPPClient() should return disconnected client") + } + + if client.GetAPIConnection() != nil { + t.Error("NewVPPClient() should have nil API connection initially") + } + + if client.GetStatsConnection() != nil { + t.Error("NewVPPClient() should have nil stats connection initially") + } +} + +func TestVPPClientDisconnect(t *testing.T) { + client := NewVPPClient() + + // Should be safe to call disconnect on unconnected client + client.Disconnect() + + if client.IsConnected() { + t.Error("Client should not be connected after Disconnect()") + } +} + +func TestVPPClientNewAPIChannelWithoutConnection(t *testing.T) { + client := NewVPPClient() + + _, err := client.NewAPIChannel() + if err == nil { + t.Error("NewAPIChannel() should return error when not connected") + } + + vppErr, ok := err.(*VPPError) + if !ok { + t.Errorf("Expected VPPError, got %T", err) + } + + if vppErr.Message != "API connection not established" { + t.Errorf("Expected specific error message, got: %s", vppErr.Message) + } +} + +func TestVPPClientCheckLivenessWithoutConnection(t *testing.T) { + client := NewVPPClient() + + if client.CheckLiveness() { + t.Error("CheckLiveness() should return false when not connected") + } +} + +func TestVPPError(t *testing.T) { + err := &VPPError{Message: "test error"} + + if err.Error() != "test error" { + t.Errorf("VPPError.Error() returned %q, expected %q", err.Error(), "test error") + } +} + +func TestVPPClientConnectWithInvalidPaths(t *testing.T) { + // Save original values + origApiAddr := *ApiAddr + origStatsAddr := *StatsAddr + + // Set invalid paths + *ApiAddr = "/tmp/nonexistent_api.sock" + *StatsAddr = "/tmp/nonexistent_stats.sock" + + // Restore original values after test + defer func() { + *ApiAddr = origApiAddr + *StatsAddr = origStatsAddr + }() + + client := NewVPPClient() + err := client.Connect() + + if err == nil { + t.Error("Connect() should fail with invalid socket paths") + client.Disconnect() // Clean up if somehow it connected + } + + if client.IsConnected() { + t.Error("Client should not be connected after failed Connect()") + } +}