diff --git a/go.mod b/go.mod index cdb15b0f6..ad4b49e80 100644 --- a/go.mod +++ b/go.mod @@ -46,7 +46,7 @@ require ( go.opentelemetry.io/otel/sdk/log v0.20.0 go.opentelemetry.io/otel/sdk/metric v1.44.0 go.opentelemetry.io/otel/trace v1.44.0 - go.uber.org/goleak v1.3.0 + go.uber.org/goleak v1.3.1-0.20260602155401-5f791e3ef83b go.yaml.in/yaml/v3 v3.0.4 golang.org/x/crypto v0.54.0 golang.org/x/sync v0.22.0 diff --git a/go.sum b/go.sum index 9319a5eeb..7846b2e22 100644 --- a/go.sum +++ b/go.sum @@ -1805,8 +1805,9 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/goleak v1.3.1-0.20260602155401-5f791e3ef83b h1:FHYZT12VzXnb6P399kd1i9Ai7aLF+CFSUsOFylDt7Lk= +go.uber.org/goleak v1.3.1-0.20260602155401-5f791e3ef83b/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= diff --git a/spinifex/vm/watchdog_test.go b/spinifex/vm/watchdog_test.go index 0ae9e26e0..386c77cee 100644 --- a/spinifex/vm/watchdog_test.go +++ b/spinifex/vm/watchdog_test.go @@ -154,3 +154,44 @@ func assertStuckMarkedFailed(t *testing.T, m *Manager, rt *recordedTransitions, assert.Contains(t, targets, StateTerminated, "terminal transition must land in Terminated") } + +// deepRecurse recurses past the 100 frames runtime.Stack keeps before it +// collapses the rest into a literal "...N frames elided..." line. Kept +// non-inlinable so the recursion reaches the goroutine's stack trace. +// +//go:noinline +func deepRecurse(n int, ready chan<- struct{}, block <-chan struct{}) int { + if n <= 0 { + close(ready) + <-block + return 0 + } + return deepRecurse(n-1, ready, block) + n +} + +// TestGoleakParsesElidedFrameStacks guards against goleak's stack parser +// panicking on the "...N frames elided..." line runtime.Stack emits for +// goroutines parked past 100 frames deep. v1.3.0 reads it as a function name. +func TestGoleakParsesElidedFrameStacks(t *testing.T) { + ready := make(chan struct{}) + block := make(chan struct{}) + done := make(chan struct{}) + + // 200 frames guarantees the elided-frames line regardless of the + // runtime's exact truncation threshold, while the goroutine stays + // parked on <-block so its stack is captured mid-recursion. + go func() { + defer close(done) + deepRecurse(200, ready, block) + }() + <-ready + + assert.NotPanics(t, func() { + // Find legitimately reports the parked goroutine as a leak; + // only a panic escaping the call is a failure here. + _ = goleak.Find() + }) + + close(block) + <-done +}