From 0a0e3e70559f1a8e1e15bd0da49065b64d324e9e Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Wed, 11 Jun 2025 00:02:04 +0200 Subject: [PATCH] Add tests. They are quite basic ... --- agentx/agentx_test.go | 53 ++++++++++++ config/config_test.go | 30 +++++++ ifmib/ifmib_test.go | 180 +++++++++++++++++++++++++++++++++++++++++ logger/logger_test.go | 72 +++++++++++++++++ main_test.go | 20 +++++ vppstats/stats_test.go | 163 +++++++++++++++++++++++++++++++++++++ 6 files changed, 518 insertions(+) create mode 100644 agentx/agentx_test.go create mode 100644 config/config_test.go create mode 100644 ifmib/ifmib_test.go create mode 100644 logger/logger_test.go create mode 100644 main_test.go create mode 100644 vppstats/stats_test.go diff --git a/agentx/agentx_test.go b/agentx/agentx_test.go new file mode 100644 index 0000000..bc3b805 --- /dev/null +++ b/agentx/agentx_test.go @@ -0,0 +1,53 @@ +// Copyright 2025, IPng Networks GmbH, Pim van Pelt + +package agentx + +import ( + "flag" + "testing" +) + +func TestAgentXAddrFlag(t *testing.T) { + // Test that the flag is registered with correct default + if *AgentXAddr != "localhost:705" { + t.Errorf("Expected default AgentX address to be 'localhost:705', got '%s'", *AgentXAddr) + } +} + +func TestAgentXAddrFlagParsing(t *testing.T) { + // Save original flag value + originalAddr := *AgentXAddr + defer func() { *AgentXAddr = originalAddr }() + + // Test Unix socket path + testAddr := "/var/run/test.sock" + *AgentXAddr = testAddr + + if *AgentXAddr != testAddr { + t.Errorf("Expected AgentX address to be '%s', got '%s'", testAddr, *AgentXAddr) + } + + // Test TCP address + testAddr = "192.168.1.1:705" + *AgentXAddr = testAddr + + if *AgentXAddr != testAddr { + t.Errorf("Expected AgentX address to be '%s', got '%s'", testAddr, *AgentXAddr) + } +} + +func TestFlagRegistration(t *testing.T) { + // Test that our flag is properly registered + f := flag.Lookup("agentx.addr") + if f == nil { + t.Error("Expected agentx.addr flag to be registered") + } + + if f.DefValue != "localhost:705" { + t.Errorf("Expected flag default value to be 'localhost:705', got '%s'", f.DefValue) + } + + if f.Usage != "Address to connect to (hostname:port or Unix socket path)" { + t.Errorf("Unexpected flag usage string: %s", f.Usage) + } +} \ No newline at end of file diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..2d62ada --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,30 @@ +// Copyright 2025, IPng Networks GmbH, Pim van Pelt + +package config + +import "testing" + +func TestDebugFlagDefault(t *testing.T) { + // Test that Debug flag starts as false by default + if Debug != false { + t.Errorf("Expected Debug to be false by default, got %v", Debug) + } +} + +func TestDebugFlagSet(t *testing.T) { + // Save original value + original := Debug + defer func() { Debug = original }() + + // Test setting Debug to true + Debug = true + if Debug != true { + t.Errorf("Expected Debug to be true after setting, got %v", Debug) + } + + // Test setting Debug to false + Debug = false + if Debug != false { + t.Errorf("Expected Debug to be false after setting, got %v", Debug) + } +} \ No newline at end of file diff --git a/ifmib/ifmib_test.go b/ifmib/ifmib_test.go new file mode 100644 index 0000000..3ca53d7 --- /dev/null +++ b/ifmib/ifmib_test.go @@ -0,0 +1,180 @@ +// Copyright 2025, IPng Networks GmbH, Pim van Pelt + +package ifmib + +import ( + "os" + "testing" + + "go.fd.io/govpp/api" +) + +func TestNewInterfaceMIB(t *testing.T) { + mib := NewInterfaceMIB() + + if mib == nil { + t.Fatal("NewInterfaceMIB returned nil") + } + + if mib.handler == nil { + t.Error("Expected handler to be initialized") + } + + if mib.stats == nil { + t.Error("Expected stats map to be initialized") + } + + if mib.descriptions == nil { + t.Error("Expected descriptions map to be initialized") + } + + if len(mib.stats) != 0 { + t.Errorf("Expected stats map to be empty, got %d entries", len(mib.stats)) + } + + if len(mib.descriptions) != 0 { + t.Errorf("Expected descriptions map to be empty, got %d entries", len(mib.descriptions)) + } +} + +func TestGetHandler(t *testing.T) { + mib := NewInterfaceMIB() + handler := mib.GetHandler() + + if handler == nil { + t.Error("GetHandler returned nil") + } + + if handler != mib.handler { + t.Error("GetHandler returned different handler than expected") + } +} + +func TestLoadVPPConfigValidYAML(t *testing.T) { + mib := NewInterfaceMIB() + + // Create a temporary YAML file + yamlContent := `interfaces: + GigabitEthernet0/0/0: + description: 'Test: Interface' + sub-interfaces: + 100: + description: 'Test: Sub-interface' +loopbacks: + loop0: + description: 'Test: Loopback' +` + + tmpfile, err := os.CreateTemp("", "test_*.yaml") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpfile.Name()) + + if _, err := tmpfile.Write([]byte(yamlContent)); err != nil { + t.Fatal(err) + } + if err := tmpfile.Close(); err != nil { + t.Fatal(err) + } + + // Test loading the config + err = mib.LoadVPPConfig(tmpfile.Name()) + if err != nil { + t.Fatalf("LoadVPPConfig failed: %v", err) + } + + // Check that descriptions were loaded + if len(mib.descriptions) != 3 { + t.Errorf("Expected 3 descriptions, got %d", len(mib.descriptions)) + } + + if mib.descriptions["GigabitEthernet0/0/0"] != "Test: Interface" { + t.Errorf("Unexpected interface description: %s", mib.descriptions["GigabitEthernet0/0/0"]) + } + + if mib.descriptions["GigabitEthernet0/0/0.100"] != "Test: Sub-interface" { + t.Errorf("Unexpected sub-interface description: %s", mib.descriptions["GigabitEthernet0/0/0.100"]) + } + + if mib.descriptions["loop0"] != "Test: Loopback" { + t.Errorf("Unexpected loopback description: %s", mib.descriptions["loop0"]) + } +} + +func TestLoadVPPConfigNonExistentFile(t *testing.T) { + mib := NewInterfaceMIB() + + err := mib.LoadVPPConfig("/nonexistent/file.yaml") + if err == nil { + t.Error("Expected error for non-existent file") + } +} + +func TestLoadVPPConfigInvalidYAML(t *testing.T) { + mib := NewInterfaceMIB() + + // Create a temporary file with invalid YAML + invalidYAML := `interfaces: + test: [ +` + + tmpfile, err := os.CreateTemp("", "invalid_*.yaml") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpfile.Name()) + + if _, err := tmpfile.Write([]byte(invalidYAML)); err != nil { + t.Fatal(err) + } + if err := tmpfile.Close(); err != nil { + t.Fatal(err) + } + + err = mib.LoadVPPConfig(tmpfile.Name()) + if err == nil { + t.Error("Expected error for invalid YAML") + } +} + +func TestUpdateStatsBasic(t *testing.T) { + mib := NewInterfaceMIB() + + // Create mock interface stats + stats := &api.InterfaceStats{ + Interfaces: []api.InterfaceCounters{ + { + InterfaceIndex: 0, + InterfaceName: "test0", + Rx: api.InterfaceCounterCombined{ + Packets: 100, + Bytes: 1000, + }, + Tx: api.InterfaceCounterCombined{ + Packets: 200, + Bytes: 2000, + }, + }, + }, + } + + // Call UpdateStats (this will test the basic flow without AgentX sessions) + mib.UpdateStats(stats) + + // Check that stats were stored + if len(mib.stats) != 1 { + t.Errorf("Expected 1 interface in stats, got %d", len(mib.stats)) + } + + if storedStats, exists := mib.stats[0]; !exists { + t.Error("Expected interface 0 to be stored in stats") + } else { + if storedStats.InterfaceName != "test0" { + t.Errorf("Expected interface name 'test0', got '%s'", storedStats.InterfaceName) + } + if storedStats.Rx.Packets != 100 { + t.Errorf("Expected RX packets 100, got %d", storedStats.Rx.Packets) + } + } +} \ No newline at end of file diff --git a/logger/logger_test.go b/logger/logger_test.go new file mode 100644 index 0000000..01280dd --- /dev/null +++ b/logger/logger_test.go @@ -0,0 +1,72 @@ +// Copyright 2025, IPng Networks GmbH, Pim van Pelt + +package logger + +import ( + "bytes" + "log" + "os" + "strings" + "testing" + + "govpp-snmp-agentx/config" +) + +func TestPrintf(t *testing.T) { + // Capture log output + var buf bytes.Buffer + log.SetOutput(&buf) + defer log.SetOutput(os.Stderr) + + Printf("test message: %s", "hello") + + output := buf.String() + if !strings.Contains(output, "logger_test.go:logger.TestPrintf") { + t.Errorf("Expected log output to contain caller info, got: %s", output) + } + if !strings.Contains(output, "test message: hello") { + t.Errorf("Expected log output to contain message, got: %s", output) + } +} + +func TestDebugfWithDebugEnabled(t *testing.T) { + // Save original debug state + originalDebug := config.Debug + defer func() { config.Debug = originalDebug }() + + // Enable debug + config.Debug = true + + // Capture log output + var buf bytes.Buffer + log.SetOutput(&buf) + defer log.SetOutput(os.Stderr) + + Debugf("debug message: %s", "test") + + output := buf.String() + if !strings.Contains(output, "debug message: test") { + t.Errorf("Expected debug message to be logged when debug is enabled, got: %s", output) + } +} + +func TestDebugfWithDebugDisabled(t *testing.T) { + // Save original debug state + originalDebug := config.Debug + defer func() { config.Debug = originalDebug }() + + // Disable debug + config.Debug = false + + // Capture log output + var buf bytes.Buffer + log.SetOutput(&buf) + defer log.SetOutput(os.Stderr) + + Debugf("debug message: %s", "test") + + output := buf.String() + if strings.Contains(output, "debug message: test") { + t.Errorf("Expected debug message to NOT be logged when debug is disabled, got: %s", output) + } +} \ No newline at end of file diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..a49c56a --- /dev/null +++ b/main_test.go @@ -0,0 +1,20 @@ +// Copyright 2025, IPng Networks GmbH, Pim van Pelt + +package main + +import ( + "os" + "testing" +) + +func TestMainCompiles(t *testing.T) { + // This test simply ensures that main package compiles + // More comprehensive integration tests would require mocking VPP and SNMP + if os.Getenv("BE_MAIN") == "1" { + // This would run main(), but we skip it in tests + return + } + + // Just test that we can access main package + t.Log("Main package compiles successfully") +} \ No newline at end of file diff --git a/vppstats/stats_test.go b/vppstats/stats_test.go new file mode 100644 index 0000000..d72dee2 --- /dev/null +++ b/vppstats/stats_test.go @@ -0,0 +1,163 @@ +// Copyright 2025, IPng Networks GmbH, Pim van Pelt + +package vppstats + +import ( + "flag" + "fmt" + "testing" + "time" + + "go.fd.io/govpp/api" +) + +func TestVPPStatsFlags(t *testing.T) { + // Test default values + if *ApiAddr != "/var/run/vpp/api.sock" { + t.Errorf("Expected default API address to be '/var/run/vpp/api.sock', got '%s'", *ApiAddr) + } + + if *StatsAddr != "/var/run/vpp/stats.sock" { + t.Errorf("Expected default stats address to be '/var/run/vpp/stats.sock', got '%s'", *StatsAddr) + } + + if *IfIndexOffset != 1000 { + t.Errorf("Expected default interface index offset to be 1000, got %d", *IfIndexOffset) + } + + if *Period != 10 { + t.Errorf("Expected default period to be 10, got %d", *Period) + } +} + +func TestFlagRegistrations(t *testing.T) { + tests := []struct { + name string + flagName string + defValue string + }{ + {"API address", "vppstats.api.addr", "/var/run/vpp/api.sock"}, + {"Stats address", "vppstats.stats.addr", "/var/run/vpp/stats.sock"}, + {"Index offset", "vppstats.ifindex-offset", "1000"}, + {"Period", "vppstats.period", "10"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := flag.Lookup(tt.flagName) + if f == nil { + t.Errorf("Expected %s flag to be registered", tt.flagName) + return + } + + if f.DefValue != tt.defValue { + t.Errorf("Expected %s flag default value to be '%s', got '%s'", + tt.flagName, tt.defValue, f.DefValue) + } + }) + } +} + +func TestStatsCallbackType(t *testing.T) { + // Test that we can create a valid callback function + var called bool + var receivedStats *api.InterfaceStats + + callback := func(stats *api.InterfaceStats) { + called = true + receivedStats = stats + } + + // Create mock stats + mockStats := &api.InterfaceStats{ + Interfaces: []api.InterfaceCounters{ + { + InterfaceIndex: 1, + InterfaceName: "test", + }, + }, + } + + // Call the callback + callback(mockStats) + + if !called { + t.Error("Expected callback to be called") + } + + if receivedStats != mockStats { + t.Error("Expected callback to 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" { + t.Errorf("Expected interface name 'test', got '%s'", receivedStats.Interfaces[0].InterfaceName) + } +} + +func TestPeriodConversion(t *testing.T) { + // Test that period conversion works correctly + originalPeriod := *Period + defer func() { *Period = originalPeriod }() + + testPeriods := []struct { + input int + expected time.Duration + }{ + {1, time.Second}, + {5, 5 * time.Second}, + {10, 10 * time.Second}, + {60, time.Minute}, + } + + for _, tt := range testPeriods { + t.Run(fmt.Sprintf("period_%d", tt.input), func(t *testing.T) { + *Period = tt.input + result := time.Duration(*Period) * time.Second + + if result != tt.expected { + t.Errorf("Expected period %v, got %v", tt.expected, result) + } + }) + } +} + +func TestFlagValues(t *testing.T) { + // Save original flag values + originalApiAddr := *ApiAddr + originalStatsAddr := *StatsAddr + originalOffset := *IfIndexOffset + originalPeriod := *Period + + defer func() { + *ApiAddr = originalApiAddr + *StatsAddr = originalStatsAddr + *IfIndexOffset = originalOffset + *Period = originalPeriod + }() + + // Test setting flag values + *ApiAddr = "/custom/api.sock" + *StatsAddr = "/custom/stats.sock" + *IfIndexOffset = 2000 + *Period = 30 + + if *ApiAddr != "/custom/api.sock" { + t.Errorf("Expected API address to be '/custom/api.sock', got '%s'", *ApiAddr) + } + + if *StatsAddr != "/custom/stats.sock" { + t.Errorf("Expected stats address to be '/custom/stats.sock', got '%s'", *StatsAddr) + } + + if *IfIndexOffset != 2000 { + t.Errorf("Expected interface index offset to be 2000, got %d", *IfIndexOffset) + } + + if *Period != 30 { + t.Errorf("Expected period to be 30, got %d", *Period) + } +} \ No newline at end of file