Skip to content

Commit 183e38c

Browse files
authored
Add leak detector (#811)
* feat: add goleak detection in test suites to prevent goroutine leaks * docs: add instructions for integrating goleak in Go package tests * feat: integrate goleak detection in end-to-end tests to prevent goroutine leaks * feat: refactor end-to-end tests to check for goroutine leaks using a dedicated function
1 parent 722ec52 commit 183e38c

41 files changed

Lines changed: 513 additions & 28 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,34 @@ If any step fails, fix the issue and re-run before committing.
2323
- **golangci-lint v2.7.2+**[Install instructions](https://golangci-lint.run/welcome/install/)
2424
- **ShellCheck**`brew install shellcheck` (macOS) or `apt-get install shellcheck` (Linux)
2525

26+
## Testing
27+
28+
When adding a new Go package that contains tests, include a
29+
`testmain_test.go` file in the package directory to enable automatic
30+
goroutine leak detection via `go.uber.org/goleak`. Match the `package`
31+
declaration to the existing test files in that directory (use the external
32+
`package foo_test` form if that is what the other test files use).
33+
34+
```go
35+
package <pkg> // or <pkg>_test — match the existing test files
36+
37+
import (
38+
"testing"
39+
40+
"go.uber.org/goleak"
41+
)
42+
43+
// TestMain runs goleak after the test suite to detect goroutine leaks.
44+
func TestMain(m *testing.M) {
45+
goleak.VerifyTestMain(m)
46+
}
47+
```
48+
49+
If the package already has a `TestMain` that does non-trivial setup (e.g.
50+
starting a server), integrate goleak manually using the
51+
setup → `m.Run()` → teardown → `goleak.Find``os.Exit` pattern instead
52+
of calling `goleak.VerifyTestMain`.
53+
2654
## Project documentation
2755

2856
- [README.md](./README.md) — project overview, building from source, API examples, and Makefile usage

cmd/cli/commands/testmain_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package commands
2+
3+
import (
4+
"testing"
5+
6+
"go.uber.org/goleak"
7+
)
8+
9+
// TestMain runs goleak after the test suite to detect goroutine leaks.
10+
func TestMain(m *testing.M) {
11+
goleak.VerifyTestMain(m)
12+
}

cmd/cli/desktop/testmain_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package desktop
2+
3+
import (
4+
"testing"
5+
6+
"go.uber.org/goleak"
7+
)
8+
9+
// TestMain runs goleak after the test suite to detect goroutine leaks.
10+
func TestMain(m *testing.M) {
11+
goleak.VerifyTestMain(m)
12+
}

cmd/cli/search/testmain_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package search
2+
3+
import (
4+
"testing"
5+
6+
"go.uber.org/goleak"
7+
)
8+
9+
// TestMain runs goleak after the test suite to detect goroutine leaks.
10+
func TestMain(m *testing.M) {
11+
goleak.VerifyTestMain(m)
12+
}

e2e/e2e_test.go

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import (
2626
"testing"
2727
"time"
2828

29+
"go.uber.org/goleak"
30+
2931
"github.com/docker/model-runner/cmd/cli/desktop"
3032
"github.com/docker/model-runner/pkg/ollama"
3133
)
@@ -93,7 +95,6 @@ func runNative(m *testing.M, root string) int {
9395
fmt.Fprintf(os.Stderr, "e2e: starting model-runner on port %d\n", port)
9496

9597
ctx, cancel := context.WithCancel(context.Background())
96-
defer cancel()
9798

9899
server := exec.CommandContext(ctx, serverBin)
99100
// TODO: os.Interrupt is not supported on Windows. When Windows e2e
@@ -111,14 +112,17 @@ func runNative(m *testing.M, root string) int {
111112

112113
if err := server.Start(); err != nil {
113114
fmt.Fprintf(os.Stderr, "e2e: failed to start server: %v\n", err)
115+
cancel()
114116
return 1
115117
}
116-
defer func() {
117-
cancel()
118-
_ = server.Wait()
119-
}()
120118

121-
return waitAndRunTests(m)
119+
code := waitAndRunTests(m)
120+
121+
// Shut down the server and drain its goroutines before the leak check.
122+
cancel()
123+
_ = server.Wait()
124+
125+
return checkLeaks(code)
122126
}
123127

124128
// runDocker builds the Docker image and CLI from source, then lets the CLI
@@ -153,7 +157,23 @@ func runDocker(m *testing.M, root string) int {
153157
}
154158

155159
serverURL = "http://localhost:12434"
156-
return waitAndRunTests(m)
160+
return checkLeaks(waitAndRunTests(m))
161+
}
162+
163+
// checkLeaks closes idle HTTP connections and checks for goroutine leaks in
164+
// the test harness. It returns code unchanged when code != 0 — a failing run
165+
// already reports errors and extra leak noise adds no value.
166+
func checkLeaks(code int) int {
167+
// Close idle keep-alive connections so the default HTTP transport does
168+
// not leave goroutines that would be false-positive leaks.
169+
http.DefaultClient.CloseIdleConnections()
170+
if code == 0 {
171+
if err := goleak.Find(); err != nil {
172+
fmt.Fprintf(os.Stderr, "e2e: goroutine leak detected: %v\n", err)
173+
return 1
174+
}
175+
}
176+
return code
157177
}
158178

159179
func waitAndRunTests(m *testing.M) int {

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ require (
3636
github.com/testcontainers/testcontainers-go v0.41.0
3737
github.com/testcontainers/testcontainers-go/modules/registry v0.41.0
3838
go.opentelemetry.io/otel v1.42.0
39+
go.uber.org/goleak v1.3.0
3940
go.uber.org/mock v0.6.0
4041
golang.org/x/sync v0.20.0
4142
golang.org/x/sys v0.42.0

pkg/anthropic/testmain_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package anthropic
2+
3+
import (
4+
"testing"
5+
6+
"go.uber.org/goleak"
7+
)
8+
9+
// TestMain runs goleak after the test suite to detect goroutine leaks.
10+
func TestMain(m *testing.M) {
11+
goleak.VerifyTestMain(m)
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package builder_test
2+
3+
import (
4+
"testing"
5+
6+
"go.uber.org/goleak"
7+
)
8+
9+
// TestMain runs goleak after the test suite to detect goroutine leaks.
10+
func TestMain(m *testing.M) {
11+
goleak.VerifyTestMain(m)
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package distribution
2+
3+
import (
4+
"testing"
5+
6+
"go.uber.org/goleak"
7+
)
8+
9+
// TestMain runs goleak after the test suite to detect goroutine leaks.
10+
func TestMain(m *testing.M) {
11+
goleak.VerifyTestMain(m)
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package files
2+
3+
import (
4+
"testing"
5+
6+
"go.uber.org/goleak"
7+
)
8+
9+
// TestMain runs goleak after the test suite to detect goroutine leaks.
10+
func TestMain(m *testing.M) {
11+
goleak.VerifyTestMain(m)
12+
}

0 commit comments

Comments
 (0)