diff --git a/.jules/bolt.md b/.jules/bolt.md index dfc62b8..643f039 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -45,3 +45,6 @@ ## 2026-07-28 - [Validating Context Cancellation for Blocking OS Executions] **Learning:** Functions that wrap long-running system operations (such as `exec.CommandContext` or `net.DialContext`) generally handle context cancellation automatically, but early aborts for network calls need explicit testing. Merely relying on lower-level Go standard library propagation does not automatically guarantee zero side-effects if the cancellation happens before execution logic completes in consumer boundaries. **Action:** Always include fine-grained tests that explicitly pass a pre-cancelled context to network discovery and connection routines (e.g. Ping, LookupAddr) to definitively prove zero leakage of OS processes or goroutines on early aborts. +## 2026-06-25 - Active context cancellation testing patterns +**Learning:** To statistically prove zero socket/goroutine leaks upon premature context cancellation in Go, active testing techniques using delayed cancellation combined with runtime metric evaluation (`runtime.NumGoroutine()`) before and after a settling period are required. +**Action:** Use a short sleep prior to `cancel()` in a background goroutine, followed by an execution step of the target code, ending with a loop that breaks if the final goroutine count drops back to within the allowed delta (e.g., `<= before+3`). This definitively proves the absence of leaks across async tasks. diff --git a/pkg/discovery/net_test.go b/pkg/discovery/net_test.go index 6fa4208..00262ee 100644 --- a/pkg/discovery/net_test.go +++ b/pkg/discovery/net_test.go @@ -126,3 +126,55 @@ func TestPingCancellationActive(t *testing.T) { t.Errorf("Goroutine leak detected in active Ping cancellation: before=%d, after=%d", before, after) } } + +func TestReverseDNSCancellationActive(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + before := runtime.NumGoroutine() + + go func() { + time.Sleep(5 * time.Millisecond) + cancel() + }() + + res := ReverseDNS(ctx, "192.0.2.1") + if res != "" { + t.Errorf("ReverseDNS() with actively cancelled context should return empty string, got: %q", res) + } + + for i := 0; i < 10; i++ { + if runtime.NumGoroutine() <= before+3 { + return + } + time.Sleep(10 * time.Millisecond) + } + after := runtime.NumGoroutine() + if after > before+3 { + t.Errorf("Goroutine leak detected in active ReverseDNS cancellation: before=%d, after=%d", before, after) + } +} + +func TestGetMACCancellationActive(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + before := runtime.NumGoroutine() + + go func() { + time.Sleep(5 * time.Millisecond) + cancel() + }() + + res := GetMAC(ctx, "192.0.2.1") + if res != "" { + t.Errorf("GetMAC() with actively cancelled context should return empty string, got: %q", res) + } + + for i := 0; i < 10; i++ { + if runtime.NumGoroutine() <= before+3 { + return + } + time.Sleep(10 * time.Millisecond) + } + after := runtime.NumGoroutine() + if after > before+3 { + t.Errorf("Goroutine leak detected in active GetMAC cancellation: before=%d, after=%d", before, after) + } +} diff --git a/pkg/ports/scanner_test.go b/pkg/ports/scanner_test.go index 2c6d770..c2e15c9 100644 --- a/pkg/ports/scanner_test.go +++ b/pkg/ports/scanner_test.go @@ -4,8 +4,10 @@ import ( "context" "net" "net/http/httptest" + "runtime" "strconv" "testing" + "time" ) func TestScanPorts(t *testing.T) { @@ -65,3 +67,37 @@ func TestScanPortsCancellation(t *testing.T) { t.Errorf("expected 0 ports returned after cancellation, got %d", count) } } + +func TestScanPortsCancellationActive(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + before := runtime.NumGoroutine() + + go func() { + time.Sleep(5 * time.Millisecond) + cancel() + }() + + ports := []int{80, 81, 8080, 8081, 8082, 8083, 8084, 8085, 8086, 8087} + openChan := ScanPorts(ctx, "192.0.2.1", ports, 1000) + + count := 0 + for range openChan { + count++ + } + + if count != 0 { + t.Errorf("expected 0 ports returned after active cancellation, got %d", count) + } + + // let background routines settle + for i := 0; i < 10; i++ { + if runtime.NumGoroutine() <= before+3 { + return + } + time.Sleep(10 * time.Millisecond) + } + after := runtime.NumGoroutine() + if after > before+3 { + t.Errorf("Goroutine leak detected in active ScanPorts cancellation: before=%d, after=%d", before, after) + } +}