Skip to content

Commit fe0699a

Browse files
committed
chore(profiler): test the goroutine leak profile support
Test that a program built with the goroutine leak experiment produces a goroutine leak profiler when instrumented with our library. And test that a program build without that experiment doesn't produce the profile. The first draft of this was generated using Sonnet 4.6 and I tidied it up.
1 parent 6b793d7 commit fe0699a

1 file changed

Lines changed: 126 additions & 0 deletions

File tree

profiler/goroutineleak_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2026 Datadog, Inc.
5+
6+
package profiler
7+
8+
import (
9+
"fmt"
10+
"net/http/httptest"
11+
"os"
12+
"os/exec"
13+
"path/filepath"
14+
"runtime"
15+
"strings"
16+
"testing"
17+
)
18+
19+
func TestGoroutineLeakProfile(t *testing.T) {
20+
if !strings.HasPrefix(runtime.Version(), "go1.26.") {
21+
// This is experimental in Go 1.26. We'll need to revisit this
22+
// code when Go 1.27 is released.
23+
t.Skipf("goroutineleakprofile requires Go 1.26, got %s", runtime.Version())
24+
}
25+
26+
t.Run("with_experiment", func(t *testing.T) {
27+
meta := runGoroutineLeakProgram(t, true)
28+
if _, ok := meta.attachments["goroutineleak.pprof"]; !ok {
29+
t.Errorf("expected goroutineleak.pprof attachment, got: %v", meta.event.Attachments)
30+
}
31+
})
32+
33+
t.Run("without_experiment", func(t *testing.T) {
34+
meta := runGoroutineLeakProgram(t, false)
35+
if _, ok := meta.attachments["goroutineleak.pprof"]; ok {
36+
t.Errorf("unexpected goroutineleak.pprof attachment without GOEXPERIMENT")
37+
}
38+
})
39+
}
40+
41+
func runGoroutineLeakProgram(t *testing.T, withExperiment bool) profileMeta {
42+
t.Helper()
43+
dir := t.TempDir()
44+
45+
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(goroutineLeakSource), 0644); err != nil {
46+
t.Fatalf("writing test source: %s", err)
47+
}
48+
49+
repoRoot, err := filepath.Abs("..")
50+
if err != nil {
51+
t.Fatalf("getting repo root: %s", err)
52+
}
53+
54+
for _, cmd := range []*exec.Cmd{
55+
exec.Command("go", "mod", "init", "goroutineleak_test_app"),
56+
exec.Command("go", "mod", "edit",
57+
"-require=github.com/DataDog/dd-trace-go/[email protected]",
58+
"-replace=github.com/DataDog/dd-trace-go/[email protected]="+repoRoot,
59+
),
60+
exec.Command("go", "mod", "tidy"),
61+
} {
62+
cmd.Dir = dir
63+
cmd.Env = append(os.Environ(), "GOWORK=off")
64+
if out, err := cmd.CombinedOutput(); err != nil {
65+
t.Fatalf("%s: %s", cmd.String(), out)
66+
}
67+
}
68+
69+
binPath := filepath.Join(dir, "app")
70+
if runtime.GOOS == "windows" {
71+
binPath += ".exe"
72+
}
73+
build := exec.Command("go", "build", "-o", binPath, ".")
74+
build.Dir = dir
75+
build.Env = append(os.Environ(), "GOWORK=off")
76+
if withExperiment {
77+
build.Env = append(build.Env, "GOEXPERIMENT=goroutineleakprofile")
78+
}
79+
if out, err := build.CombinedOutput(); err != nil {
80+
t.Fatalf("%s: %s", build.String(), out)
81+
}
82+
83+
backend := &fakeBackend{profiles: make(chan profileMeta, 1)}
84+
srv := httptest.NewServer(backend)
85+
t.Cleanup(srv.Close)
86+
87+
cmd := exec.Command(binPath)
88+
cmd.Env = []string{fmt.Sprintf("DD_TRACE_AGENT_URL=%s", srv.URL)}
89+
if err := cmd.Start(); err != nil {
90+
t.Fatalf("starting test program: %s", err)
91+
}
92+
t.Cleanup(func() {
93+
cmd.Process.Kill()
94+
cmd.Wait()
95+
})
96+
97+
p := <-backend.profiles
98+
if p.err != nil {
99+
t.Fatalf("profile upload error: %s", p.err)
100+
}
101+
return p
102+
}
103+
104+
const goroutineLeakSource = `package main
105+
106+
import (
107+
"time"
108+
109+
"github.com/DataDog/dd-trace-go/v2/profiler"
110+
)
111+
112+
func main() {
113+
err := profiler.Start(
114+
profiler.WithProfileTypes(), // only leak profile matters; auto-enabled if available
115+
profiler.WithPeriod(10*time.Millisecond),
116+
)
117+
if err != nil {
118+
panic(err)
119+
}
120+
defer profiler.Stop()
121+
122+
// Run until killed. This has the side effect of leaking a goroutine in
123+
// case we care about checking for a non-empty profile.
124+
select {}
125+
}
126+
`

0 commit comments

Comments
 (0)