Fixed few memory leaks. Implement testing of the functionality.

This commit is contained in:
Kalzu Rekku
2026-01-08 18:55:32 +02:00
parent c663ec0431
commit 1130b7fb8c
10 changed files with 1334 additions and 13 deletions

View File

@@ -0,0 +1,313 @@
package main
import (
"encoding/json"
"net/netip"
"os"
"path/filepath"
"testing"
"time"
)
// TestIPParsingDoesNotPanic verifies that invalid IPs don't cause panics
func TestIPParsingDoesNotPanic(t *testing.T) {
testCases := []string{
"not-an-ip",
"999.999.999.999",
"192.168.1",
"",
"192.168.1.1.1",
"hello world",
"2001:db8::1", // IPv6 (should be filtered)
}
// This test passes if it doesn't panic
for _, testIP := range testCases {
func() {
defer func() {
if r := recover(); r != nil {
t.Errorf("Parsing %q caused panic: %v", testIP, r)
}
}()
// Test the safe parsing logic
addr, err := netip.ParseAddr(testIP)
if err == nil && addr.IsValid() {
// Valid IP, this is fine
}
}()
}
}
// TestStateSerializationPreservesActiveGens verifies activeGens are saved/restored
func TestStateSerializationPreservesActiveGens(t *testing.T) {
// Create a temporary server for testing
s := &Server{
allCIDRs: []string{"192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"},
globalSeen: make(map[string]bool),
}
// Create a generator with activeGens
gen, err := newIPGenerator(s, "test-consumer")
if err != nil {
t.Fatalf("Failed to create generator: %v", err)
}
// Generate some IPs to populate activeGens
for i := 0; i < 15; i++ {
_, err := gen.Next()
if err != nil {
break
}
}
// Verify we have activeGens
if len(gen.activeGens) == 0 {
t.Log("Warning: No activeGens created, test may not be comprehensive")
}
originalActiveGensCount := len(gen.activeGens)
// Build state
gen.mu.Lock()
state := gen.buildState()
gen.mu.Unlock()
// Verify activeGens were serialized
if len(state.ActiveGens) == 0 && originalActiveGensCount > 0 {
t.Errorf("ActiveGens not serialized: had %d active gens but state has 0", originalActiveGensCount)
}
// Create new generator and restore state
gen2, err := newIPGenerator(s, "test-consumer-2")
if err != nil {
t.Fatalf("Failed to create second generator: %v", err)
}
// Manually restore state (simulating loadState)
gen2.mu.Lock()
gen2.remainingCIDRs = state.RemainingCIDRs
gen2.totalCIDRsCount = state.TotalCIDRs
// Restore activeGens
if len(state.ActiveGens) > 0 {
gen2.activeGens = make([]*hostGenerator, 0, len(state.ActiveGens))
for _, genState := range state.ActiveGens {
hg, err := newHostGenerator(genState.CIDR)
if err != nil {
continue
}
hg.current, err = netip.ParseAddr(genState.Current)
if err != nil {
continue
}
hg.done = genState.Done
gen2.activeGens = append(gen2.activeGens, hg)
}
}
gen2.mu.Unlock()
// Verify activeGens were restored
if len(gen2.activeGens) != len(state.ActiveGens) {
t.Errorf("ActiveGens restoration failed: expected %d, got %d", len(state.ActiveGens), len(gen2.activeGens))
}
}
// TestGeneratorStateJSONSerialization verifies state can be marshaled/unmarshaled
func TestGeneratorStateJSONSerialization(t *testing.T) {
state := GeneratorState{
RemainingCIDRs: []string{"192.0.2.0/24", "198.51.100.0/24"},
CurrentGen: &HostGenState{
CIDR: "203.0.113.0/24",
Current: "203.0.113.10",
Done: false,
},
ActiveGens: []HostGenState{
{CIDR: "192.0.2.0/24", Current: "192.0.2.5", Done: false},
{CIDR: "198.51.100.0/24", Current: "198.51.100.20", Done: false},
},
TotalCIDRs: 10,
}
// Marshal
data, err := json.Marshal(state)
if err != nil {
t.Fatalf("Failed to marshal state: %v", err)
}
// Unmarshal
var restored GeneratorState
if err := json.Unmarshal(data, &restored); err != nil {
t.Fatalf("Failed to unmarshal state: %v", err)
}
// Verify
if len(restored.RemainingCIDRs) != len(state.RemainingCIDRs) {
t.Error("RemainingCIDRs count mismatch")
}
if len(restored.ActiveGens) != len(state.ActiveGens) {
t.Errorf("ActiveGens count mismatch: expected %d, got %d", len(state.ActiveGens), len(restored.ActiveGens))
}
if restored.TotalCIDRs != state.TotalCIDRs {
t.Error("TotalCIDRs mismatch")
}
if restored.CurrentGen == nil {
t.Error("CurrentGen was not restored")
} else if restored.CurrentGen.CIDR != state.CurrentGen.CIDR {
t.Error("CurrentGen CIDR mismatch")
}
}
// TestHostGeneratorBasic verifies basic IP generation
func TestHostGeneratorBasic(t *testing.T) {
gen, err := newHostGenerator("192.0.2.0/30")
if err != nil {
t.Fatalf("Failed to create host generator: %v", err)
}
// /30 network has 4 addresses: .0 (network), .1 and .2 (hosts), .3 (broadcast)
// We should get .1 and .2
ips := make([]string, 0)
for {
ip, ok := gen.next()
if !ok {
break
}
ips = append(ips, ip)
}
expectedCount := 2
if len(ips) != expectedCount {
t.Errorf("Expected %d IPs from /30 network, got %d: %v", expectedCount, len(ips), ips)
}
// Verify we got valid IPs
for _, ip := range ips {
addr, err := netip.ParseAddr(ip)
if err != nil || !addr.IsValid() {
t.Errorf("Generated invalid IP: %s", ip)
}
}
}
// TestGlobalDeduplication verifies that globalSeen prevents duplicates
func TestGlobalDeduplication(t *testing.T) {
s := &Server{
allCIDRs: []string{"192.0.2.0/29"},
globalSeen: make(map[string]bool),
}
// Mark some IPs as seen
s.globalSeen["192.0.2.1"] = true
s.globalSeen["192.0.2.2"] = true
if !s.globalSeen["192.0.2.1"] {
t.Error("IP should be marked as seen")
}
if s.globalSeen["192.0.2.100"] {
t.Error("Unseen IP should not be in globalSeen")
}
}
// TestIPGeneratorConcurrency verifies thread-safe generator access
func TestIPGeneratorConcurrency(t *testing.T) {
s := &Server{
allCIDRs: []string{"192.0.2.0/24", "198.51.100.0/24"},
globalSeen: make(map[string]bool),
}
gen, err := newIPGenerator(s, "test-consumer")
if err != nil {
t.Fatalf("Failed to create generator: %v", err)
}
done := make(chan bool)
errors := make(chan error, 10)
// Spawn multiple goroutines calling Next() concurrently
for i := 0; i < 10; i++ {
go func() {
for j := 0; j < 50; j++ {
_, err := gen.Next()
if err != nil {
errors <- err
break
}
}
done <- true
}()
}
// Wait for all goroutines
for i := 0; i < 10; i++ {
<-done
}
close(errors)
if len(errors) > 0 {
for err := range errors {
t.Errorf("Concurrent access error: %v", err)
}
}
}
// TestStatePersistence verifies state can be saved and loaded from disk
func TestStatePersistence(t *testing.T) {
// Use default stateDir (progress_state) for this test
// Ensure it exists
if err := os.MkdirAll(stateDir, 0755); err != nil {
t.Fatalf("Failed to create state dir: %v", err)
}
s := &Server{
allCIDRs: []string{"192.0.2.0/24"},
globalSeen: make(map[string]bool),
}
gen, err := newIPGenerator(s, "test-persistence-"+time.Now().Format("20060102150405"))
if err != nil {
t.Fatalf("Failed to create generator: %v", err)
}
// Generate some IPs
for i := 0; i < 10; i++ {
_, err := gen.Next()
if err != nil {
break
}
}
// Save state
if err := gen.saveState(); err != nil {
t.Fatalf("Failed to save state: %v", err)
}
// Verify state file was created
files, err := filepath.Glob(filepath.Join(stateDir, "*.json"))
if err != nil {
t.Fatalf("Failed to list state files: %v", err)
}
if len(files) == 0 {
t.Error("No state file was created")
}
// Create new generator and load state
gen2, err := newIPGenerator(s, gen.consumer)
if err != nil {
t.Fatalf("Failed to create second generator: %v", err)
}
if err := gen2.loadState(); err != nil {
t.Fatalf("Failed to load state: %v", err)
}
// Verify state was loaded (should have remaining CIDRs and progress)
if len(gen2.remainingCIDRs) == 0 && len(gen.remainingCIDRs) > 0 {
t.Error("State was not properly restored")
}
}