From 217f85fbddcedd3fcd6f4ef571801193e04a97bc Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sat, 6 Jun 2026 13:50:54 -0500 Subject: [PATCH] speed up tests and update dependencies - Override wsHandshakeTimeout in 3 websocket tests that waited 30s each - Make initialRetryDelay a package variable for faster retry tests - Reduce time.Sleep calls across agent_test.go (1.5s->150ms, 3s->1s, etc.) - Update Makefile with test-fast and test-short targets, add -count=1 - Update golang.org/x/crypto, x/net, x/sys to latest minors - Fix TestWSDialDefaultPorts to use mocked dials instead of real TCP --- .forgejo/workflows/test.yml | 2 +- Makefile | 10 ++++-- agent.go | 7 ++-- agent_test.go | 72 +++++++++++++++++++++---------------- go.mod | 6 ++-- go.sum | 16 ++++----- ping_test.go | 40 ++++++++++++++++----- websocket_test.go | 39 ++++++++++++++++++-- 8 files changed, 135 insertions(+), 57 deletions(-) diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml index d5a3db7..57a7e41 100644 --- a/.forgejo/workflows/test.yml +++ b/.forgejo/workflows/test.yml @@ -19,7 +19,7 @@ jobs: go-version-file: go.mod - name: Run tests - run: go test -race -v ./... + run: go test -race -v -count=1 -timeout 60s ./... - name: Vet run: go vet ./... diff --git a/Makefile b/Makefile index 58d4ba2..2718338 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: proto test build vet lint +.PHONY: proto test test-fast test-race build vet lint proto: protoc --go_out=. --go_opt=paths=source_relative proto/agent.proto @@ -6,7 +6,13 @@ proto: mv proto/agent.pb.go pb/agent.pb.go test: - go test -race -v ./... + go test -race -v -count=1 -timeout 60s ./... + +test-fast: + go test -count=1 -timeout 60s ./... + +test-short: + go test -count=1 -timeout 30s -short ./... build: go build -o towerops-agent . diff --git a/agent.go b/agent.go index c6c8084..d0349fe 100644 --- a/agent.go +++ b/agent.go @@ -26,6 +26,7 @@ var errChannelReloaded = fmt.Errorf("channel reloaded") var joinTimeout = 10 * time.Second var heartbeatInterval = 60 * time.Second var channelHeartbeatInterval = 25 * time.Second +var initialRetryDelay = time.Second const maxJobPayloadBytes = 4 << 20 // 4 MB — well above any legitimate job list @@ -40,7 +41,7 @@ type channelMsg struct { // runAgent connects to the server and runs the event loop with reconnect. func runAgent(ctx context.Context, wsURL, token string) { baseURL := strings.TrimRight(wsURL, "/") - retryDelay := time.Second + retryDelay := initialRetryDelay maxRetry := 10 * time.Second const successfulConnectionThreshold = 30 * time.Second @@ -60,7 +61,7 @@ func runAgent(ctx context.Context, wsURL, token string) { slog.Debug("resetting reconnect backoff after successful session", "duration", sessionDuration, "previous_delay", retryDelay) - retryDelay = time.Second + retryDelay = initialRetryDelay } if ctx.Err() != nil { @@ -68,7 +69,7 @@ func runAgent(ctx context.Context, wsURL, token string) { } if errors.Is(err, errRestartRequested) { slog.Info("restart requested, reconnecting immediately") - retryDelay = time.Second + retryDelay = initialRetryDelay continue } if err != nil { diff --git a/agent_test.go b/agent_test.go index e1be18d..005199a 100644 --- a/agent_test.go +++ b/agent_test.go @@ -141,7 +141,7 @@ func TestHandleMessage(t *testing.T) { // Wait for goroutine to finish select { case <-snmpCh: - case <-time.After(2 * time.Second): + case <-time.After(500 * time.Millisecond): t.Error("timed out waiting for snmp result") } }) @@ -337,7 +337,7 @@ func TestHandleMessage(t *testing.T) { _, _ = handleMessage(context.Background(), channelMsg{Event: "check_jobs", Payload: payload}, testPools(t), snmpCh, mtCh, credCh, monCh, checkCh, make(chan *pb.LldpTopologyResult, 1)) select { case <-checkCh: - case <-time.After(5 * time.Second): + case <-time.After(time.Second): t.Error("timed out waiting for check result") } }) @@ -400,7 +400,7 @@ func TestHandleMessage(t *testing.T) { _, _ = handleMessage(context.Background(), channelMsg{Event: "discovery_job", Payload: payload}, testPools(t), snmpCh, mtCh, credCh, monCh, checkCh, make(chan *pb.LldpTopologyResult, 1)) select { case <-snmpCh: - case <-time.After(2 * time.Second): + case <-time.After(500 * time.Millisecond): t.Error("timed out waiting for discovery result") } }) @@ -431,7 +431,7 @@ func TestHandleMessage(t *testing.T) { if result.Error != "" { t.Errorf("unexpected error: %s", result.Error) } - case <-time.After(2 * time.Second): + case <-time.After(500 * time.Millisecond): t.Error("timed out waiting for backup result") } }) @@ -560,7 +560,7 @@ func TestDispatchJob(t *testing.T) { if result.Error == "" { t.Error("expected error from unreachable device") } - case <-time.After(2 * time.Second): + case <-time.After(500 * time.Millisecond): t.Error("timed out") } }) @@ -589,7 +589,7 @@ func TestDispatchJob(t *testing.T) { if result.Success { t.Error("expected failure") } - case <-time.After(2 * time.Second): + case <-time.After(500 * time.Millisecond): t.Error("timed out") } }) @@ -618,7 +618,7 @@ func TestDispatchJob(t *testing.T) { if result.Status != "success" { t.Errorf("expected success, got %q", result.Status) } - case <-time.After(2 * time.Second): + case <-time.After(500 * time.Millisecond): t.Error("timed out") } }) @@ -648,7 +648,7 @@ func TestDispatchJob(t *testing.T) { select { case <-snmpCh: - case <-time.After(2 * time.Second): + case <-time.After(500 * time.Millisecond): t.Error("timed out") } }) @@ -701,7 +701,7 @@ func TestRunSessionRejectsFailedJoin(t *testing.T) { _, _ = conn.Write(frame) // Keep connection open for a bit - time.Sleep(time.Second) + time.Sleep(200 * time.Millisecond) }() addr := ln.Addr().String() @@ -745,7 +745,7 @@ func TestRunSessionJoinTimeout(t *testing.T) { frameBuf := make([]byte, 4096) _, _ = conn.Read(frameBuf) - time.Sleep(5 * time.Second) + time.Sleep(2 * time.Second) }() addr := ln.Addr().String() @@ -932,8 +932,8 @@ func TestRunSessionCtxCancel(t *testing.T) { if err != nil { t.Errorf("expected nil error on ctx cancel, got: %v", err) } - case <-time.After(5 * time.Second): - t.Error("runSession did not exit after ctx cancel") + case <-time.After(2 * time.Second): + t.Error("runSession did not exit after ctx cancel") } srv.close() } @@ -1088,6 +1088,10 @@ func TestRunSessionReadErrorDuringJoin(t *testing.T) { } func TestRunAgentReconnectOnError(t *testing.T) { + origRetry := initialRetryDelay + defer func() { initialRetryDelay = origRetry }() + initialRetryDelay = 50 * time.Millisecond + // Server that fails first connection then succeeds, then sends restart. // Agent should reconnect (not exit) after both error and restart. ln, err := net.Listen("tcp", "127.0.0.1:0") @@ -1130,14 +1134,14 @@ func TestRunAgentReconnectOnError(t *testing.T) { Ref: strPtr("1"), }) _, _ = conn.Write(makeTextFrame(reply)) - time.Sleep(50 * time.Millisecond) + time.Sleep(20 * time.Millisecond) restart, _ := json.Marshal(channelMsg{ Topic: "agent:agent-0", Event: "restart", Payload: json.RawMessage(`{}`), }) _, _ = conn.Write(makeTextFrame(restart)) - time.Sleep(time.Second) + time.Sleep(50 * time.Millisecond) _ = conn.Close() default: // Third connection: agent successfully reconnected after restart @@ -1146,7 +1150,7 @@ func TestRunAgentReconnectOnError(t *testing.T) { } }() - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() done := make(chan struct{}) @@ -1156,7 +1160,7 @@ func TestRunAgentReconnectOnError(t *testing.T) { }() // Wait for 3rd connection attempt (proves agent reconnected after both error and restart) - deadline := time.After(10 * time.Second) + deadline := time.After(5 * time.Second) for { if connCount.Load() >= 3 { cancel() @@ -1166,7 +1170,7 @@ func TestRunAgentReconnectOnError(t *testing.T) { case <-deadline: t.Fatalf("expected at least 3 connections, got %d", connCount.Load()) default: - time.Sleep(50 * time.Millisecond) + time.Sleep(10 * time.Millisecond) } } @@ -1192,6 +1196,10 @@ func TestRunAgentContextCancellation(t *testing.T) { } func TestRunAgentRestart(t *testing.T) { + origRetry := initialRetryDelay + defer func() { initialRetryDelay = origRetry }() + initialRetryDelay = 50 * time.Millisecond + // After receiving a restart event, the agent should reconnect (not exit). ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { @@ -1228,20 +1236,20 @@ func TestRunAgentRestart(t *testing.T) { if count == 1 { // First connection: send restart event - time.Sleep(50 * time.Millisecond) + time.Sleep(20 * time.Millisecond) restart, _ := json.Marshal(channelMsg{ Topic: "agent:agent-0", Event: "restart", Payload: json.RawMessage(`{}`), }) _, _ = conn.Write(makeTextFrame(restart)) - time.Sleep(time.Second) + time.Sleep(200 * time.Millisecond) } _ = conn.Close() } }() - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() done := make(chan struct{}) @@ -1261,7 +1269,7 @@ func TestRunAgentRestart(t *testing.T) { case <-deadline: t.Fatalf("expected at least 2 connections, got %d", connCount.Load()) default: - time.Sleep(50 * time.Millisecond) + time.Sleep(10 * time.Millisecond) } } @@ -1401,7 +1409,7 @@ func TestRunSessionProcessesJobResults(t *testing.T) { srv.sendEvent("check_jobs", checkPayload) // Wait for results to flow through all channels - time.Sleep(2 * time.Second) + time.Sleep(500 * time.Millisecond) close(stopDrain) srv.close() }() @@ -1447,7 +1455,7 @@ func TestRunSessionSnmpBatchThreshold(t *testing.T) { } srv.sendEvent("jobs", makeJobPayload(jobs...)) - time.Sleep(3 * time.Second) + time.Sleep(time.Second) close(stopDrain) srv.close() }() @@ -1509,10 +1517,10 @@ func TestExecuteCheckCtxDoneInClosure(t *testing.T) { executeCheck(ctx, check, p, checkCh) // Wait for the check to complete (TCP to port 1 fails fast) - time.Sleep(500 * time.Millisecond) + time.Sleep(20 * time.Millisecond) // Cancel ctx so the closure's select picks ctx.Done instead of blocked channel send cancel() - time.Sleep(100 * time.Millisecond) + time.Sleep(10 * time.Millisecond) } func TestRunSessionRestartInMainLoop(t *testing.T) { @@ -1523,10 +1531,10 @@ func TestRunSessionRestartInMainLoop(t *testing.T) { stopDrain := make(chan struct{}) go drainFrames(srv.conn, stopDrain) - time.Sleep(100 * time.Millisecond) + time.Sleep(50 * time.Millisecond) // Send restart event — exercised in the main loop select srv.sendEvent("restart", json.RawMessage(`{}`)) - time.Sleep(time.Second) + time.Sleep(200 * time.Millisecond) close(stopDrain) srv.close() }() @@ -1554,7 +1562,7 @@ func TestRunSessionHeartbeats(t *testing.T) { stopDrain := make(chan struct{}) go drainFrames(srv.conn, stopDrain) // Let heartbeats fire a few times - time.Sleep(500 * time.Millisecond) + time.Sleep(200 * time.Millisecond) close(stopDrain) srv.close() }() @@ -1566,6 +1574,10 @@ func TestRunSessionHeartbeats(t *testing.T) { } func TestRunAgentCancelDuringRetry(t *testing.T) { + origRetry := initialRetryDelay + defer func() { initialRetryDelay = origRetry }() + initialRetryDelay = 100 * time.Millisecond + // Connects to a port nothing listens on, then cancel during retry delay ctx, cancel := context.WithCancel(context.Background()) @@ -1576,13 +1588,13 @@ func TestRunAgentCancelDuringRetry(t *testing.T) { }() // Wait for first connection attempt to fail and retry delay to start - time.Sleep(1500 * time.Millisecond) + time.Sleep(150 * time.Millisecond) cancel() select { case <-done: // runAgent returned after cancel during retry - case <-time.After(5 * time.Second): + case <-time.After(time.Second): t.Error("runAgent did not return after cancel during retry") } } diff --git a/go.mod b/go.mod index 6dc4af5..7c81f57 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,9 @@ go 1.25.6 require ( github.com/gosnmp/gosnmp v1.43.2 - golang.org/x/crypto v0.49.0 - golang.org/x/net v0.52.0 + golang.org/x/crypto v0.52.0 + golang.org/x/net v0.55.0 google.golang.org/protobuf v1.36.11 ) -require golang.org/x/sys v0.42.0 // indirect +require golang.org/x/sys v0.45.0 // indirect diff --git a/go.sum b/go.sum index bb1b74a..69e3340 100644 --- a/go.sum +++ b/go.sum @@ -8,14 +8,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/ping_test.go b/ping_test.go index 3498151..61a13a4 100644 --- a/ping_test.go +++ b/ping_test.go @@ -14,7 +14,10 @@ func TestPingDeviceLocalhost(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("skipping ping test on windows") } - ms, err := pingDevice("127.0.0.1", 5000) + if testing.Short() { + t.Skip("skipping real ICMP ping in short mode") + } + ms, err := pingDevice("127.0.0.1", 2000) if err != nil { t.Skipf("ping not available: %v", err) } @@ -34,7 +37,10 @@ func TestPingDeviceIPv6(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("skipping ping test on windows") } - ms, err := pingDevice("::1", 5000) + if testing.Short() { + t.Skip("skipping real ICMP ping in short mode") + } + ms, err := pingDevice("::1", 2000) if err != nil { t.Skipf("IPv6 not available: %v", err) } @@ -44,7 +50,10 @@ func TestPingDeviceIPv6(t *testing.T) { } func TestIcmpPingLocalhost(t *testing.T) { - ms, err := icmpPing("127.0.0.1", 5000) + if testing.Short() { + t.Skip("skipping real ICMP ping in short mode") + } + ms, err := icmpPing("127.0.0.1", 2000) if err != nil { t.Skipf("ICMP not available: %v", err) } @@ -54,7 +63,10 @@ func TestIcmpPingLocalhost(t *testing.T) { } func TestIcmpPingIPv6(t *testing.T) { - ms, err := icmpPing("::1", 5000) + if testing.Short() { + t.Skip("skipping real ICMP ping in short mode") + } + ms, err := icmpPing("::1", 2000) if err != nil { t.Skipf("IPv6 ICMP not available: %v", err) } @@ -139,7 +151,10 @@ func TestExecPingLocalhost(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("skipping on windows") } - ms, err := execPing("127.0.0.1", 5000) + if testing.Short() { + t.Skip("skipping real exec ping in short mode") + } + ms, err := execPing("127.0.0.1", 2000) if err != nil { t.Skipf("ping command not available: %v", err) } @@ -159,7 +174,10 @@ func TestExecPingIPv6Localhost(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("skipping on windows") } - ms, err := execPing("::1", 5000) + if testing.Short() { + t.Skip("skipping real exec ping in short mode") + } + ms, err := execPing("::1", 2000) if err != nil { t.Skipf("ping6 not available: %v", err) } @@ -185,6 +203,9 @@ func TestPingDeviceFallbackToExec(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("skipping on windows") } + if testing.Short() { + t.Skip("skipping real ping in short mode") + } // Mock icmpListenPacket to always fail → forces fallback to execPing origListen := icmpListenPacket defer func() { icmpListenPacket = origListen }() @@ -193,7 +214,7 @@ func TestPingDeviceFallbackToExec(t *testing.T) { return nil, fmt.Errorf("permission denied") } - ms, err := pingDevice("127.0.0.1", 5000) + ms, err := pingDevice("127.0.0.1", 2000) if err != nil { t.Skipf("exec ping fallback not available: %v", err) } @@ -288,6 +309,9 @@ func TestIcmpPingNonICMPUnavailableError(t *testing.T) { } func TestIcmpPingUDPFallback(t *testing.T) { + if testing.Short() { + t.Skip("skipping real ICMP in short mode") + } // Mock raw ICMP to fail, forcing UDP fallback path in icmpPing origListen := icmpListenPacket defer func() { icmpListenPacket = origListen }() @@ -303,7 +327,7 @@ func TestIcmpPingUDPFallback(t *testing.T) { return icmp.ListenPacket(network, address) } - ms, err := icmpPing("127.0.0.1", 5000) + ms, err := icmpPing("127.0.0.1", 2000) if err != nil { t.Skipf("UDP ICMP not available: %v", err) } diff --git a/websocket_test.go b/websocket_test.go index c2850b5..d6e4ef4 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -712,6 +712,10 @@ func TestWSDialMissingAcceptHeader(t *testing.T) { } func TestWSDialMissingUpgradeHeader(t *testing.T) { + origTimeout := wsHandshakeTimeout + defer func() { wsHandshakeTimeout = origTimeout }() + wsHandshakeTimeout = time.Second + ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) @@ -743,6 +747,10 @@ func TestWSDialMissingUpgradeHeader(t *testing.T) { } func TestWSDialRejectsNon101Status(t *testing.T) { + origTimeout := wsHandshakeTimeout + defer func() { wsHandshakeTimeout = origTimeout }() + wsHandshakeTimeout = time.Second + ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) @@ -774,17 +782,44 @@ func TestWSDialRejectsNon101Status(t *testing.T) { } func TestWSDialDefaultPorts(t *testing.T) { - // Test that ws:// defaults to port 80 — will fail to connect but verifies URL parsing + origTimeout := wsHandshakeTimeout + origDial := netDial + origTLS := tlsDial + defer func() { + wsHandshakeTimeout = origTimeout + netDial = origDial + tlsDial = origTLS + }() + wsHandshakeTimeout = time.Second + + var wsCalls, wssCalls int + netDial = func(network, addr string) (net.Conn, error) { + wsCalls++ + if addr == "127.0.0.1:80" { + return nil, fmt.Errorf("connection refused (mock)") + } + return nil, fmt.Errorf("mock dial: %s", addr) + } + tlsDial = func(network, addr string) (net.Conn, error) { + wssCalls++ + return nil, fmt.Errorf("tls dial refused (mock)") + } + _, err := WSDial("ws://127.0.0.1/path") if err == nil { t.Error("expected connection error (nothing on port 80)") } + if wsCalls == 0 { + t.Error("expected netDial to be called for ws://") + } - // Test that wss:// defaults to port 443 _, err = WSDial("wss://127.0.0.1/path") if err == nil { t.Error("expected connection error (nothing on port 443)") } + if wssCalls == 0 { + t.Error("expected tlsDial to be called for wss://") + } } func TestWSDialRealHTTPServer(t *testing.T) {