Help us shape the future of Go! Take the 2026 Go Developer Survey

parleak

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 3, 2026 License: Apache-2.0, MIT Imports: 7 Imported by: 0

README

parleak

parleak tracks the goroutines a test starts through g.Go, and after the test cancels the context it reports any that are still running. It works under t.Parallel().

Install

go get github.com/slepp/parleak

Example

func TestWorker(t *testing.T) {
	t.Parallel()

	g := parleak.New(t)
	g.Go("poller", func(ctx context.Context) {
		poll(ctx) // must return when ctx is done
	})

	// exercise the system under test
}

New registers a t.Cleanup, so the check runs when the test ends. It cancels the group's context, waits up to a timeout (one second by default, set with WithTimeout), and fails the test for each goroutine still running. The first line of a report reads:

parleak: goroutine "poller" leaked: still running 1s after cleanup cancelled the context

parleak indents the rest: the launch site, which is the file string returned by runtime.Caller, and the likely cause; t.Errorf adds only its own file:line prefix. WithStackDump adds the process-wide runtime.Stack output to the leak reports, stripped of parleak's reporting frame and indented. Every goroutine parleak started ends its block with created by github.com/slepp/parleak.(*Group).Go, so search the dump for that string; the top frame is where the goroutine is blocked, and the function passed to Go sits directly above the parleak.(*Group).Go.func1 wrapper frame.

Pass g.Context() to the system under test so cancellation reaches shared state.

A Group reports only goroutines started through g.Go. To track one a library spawns with a bare go, expose its body as a func(context.Context) and start it from the test; production code never imports parleak. A goroutine a dependency leaks on its own is invisible to parleak. goleak inspects a process-wide snapshot and can catch those, though under t.Parallel() it cannot attribute a goroutine to one test.

See the package documentation for the full API.

License

Dual-licensed under Apache-2.0 or MIT at your option. Contributions are accepted under the same dual license.

Documentation

Overview

Package parleak reports the goroutines a single test leaks, including tests that run under t.Parallel.

A test starts its goroutines through a Group instead of the go keyword. New binds a Group to a test and registers a t.Cleanup that cancels the group's context, waits up to a timeout for the tracked goroutines to return, and reports any still running. A Group reports only the goroutines started through it, so the check runs per test even under t.Parallel.

Example (Leak)

Example_leak shows what parleak reports when a tracked goroutine ignores cancellation and is still running when the test's cleanup checks it.

package main

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/slepp/parleak"
)

// fakeTB stands in for *testing.T. An example function does not receive a
// *testing.T, so this records failures instead of failing, which lets the
// example print what parleak reported. Real tests pass their own *testing.T to
// parleak.New; see the New documentation for that form.
type fakeTB struct {
	errors   []string
	cleanups []func()
}

func (f *fakeTB) Errorf(format string, args ...any) {
	f.errors = append(f.errors, fmt.Sprintf(format, args...))
}
func (f *fakeTB) Cleanup(fn func()) { f.cleanups = append(f.cleanups, fn) }
func (f *fakeTB) Helper()           {}

// runCleanups runs the registered cleanups in LIFO order, as testing does when
// a test ends.
func (f *fakeTB) runCleanups() {
	for i := len(f.cleanups) - 1; i >= 0; i-- {
		f.cleanups[i]()
	}
}

// Example_leak shows what parleak reports when a tracked goroutine ignores
// cancellation and is still running when the test's cleanup checks it.
func main() {
	release := make(chan struct{})
	defer close(release)

	t := &fakeTB{}
	g := parleak.New(t, parleak.WithTimeout(10*time.Millisecond))
	g.Go("poller", func(ctx context.Context) {
		<-release // never checks ctx.Done, so it outlives the test
	})

	t.runCleanups() // testing runs the registered cleanup when a test ends

	// The report also names the launch site (file:line from runtime.Caller) and
	// the likely cause; only the first line is stable enough to print here.
	fmt.Println(strings.SplitN(t.errors[0], "\n", 2)[0])

}
Output:
parleak: goroutine "poller" leaked: still running 10ms after cleanup cancelled the context

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Group

type Group struct {
	// contains filtered or unexported fields
}

Group tracks the goroutines a single test starts through Go and, when the test's cleanup runs, reports any still running after the group's context is cancelled and a timeout elapses. Create a Group with New.

A Group is safe for concurrent use, and Go may be called from any goroutine while the test runs. A goroutine started after the cleanup check has begun is not included in leak detection.

func New

func New(t TB, opts ...Option) *Group

New returns a Group bound to t and registers a t.Cleanup that runs the leak check when the test ends. With a real *testing.T the cleanup runs automatically, so the check is not a separate call to remember. Options such as WithTimeout configure the check.

func TestWorker(t *testing.T) {
	t.Parallel()

	g := parleak.New(t)
	g.Go("poller", func(ctx context.Context) {
		poll(ctx) // must return when ctx is done
	})

	// exercise the system under test; the registered cleanup then cancels
	// the context and reports "poller" if it is still running.
}

func (*Group) Context

func (g *Group) Context() context.Context

Context returns the Group's context. It's cancelled when the test's cleanup runs. Pass it to the system under test so cancellation reaches code that a tracked goroutine depends on.

func (*Group) Go

func (g *Group) Go(label string, fn func(ctx context.Context))

Go starts fn in a new goroutine tracked by the Group. The label names the goroutine in a failure message, and Go records its own call site as the launch site.

fn receives the Group's context and is expected to return when that context is done. If it does not, the cleanup registered by New reports it as a leak.

A panic in fn is recovered rather than crashing the process. If it happens before the group's leak check records its verdict, it fails the owning test. A later panic can no longer fail the test and is written to standard error.

type Option

type Option func(*config)

Option configures a Group at construction time.

func WithStackDump

func WithStackDump() Option

WithStackDump appends a process-wide goroutine dump to the leak reports, once per check. The dump is the output of runtime.Stack, stripped of parleak's own reporting frame and indented; it contains goroutines from other tests running in parallel and is not trimmed to the leaked ones. Every goroutine parleak started ends its block with "created by github.com/slepp/parleak.(*Group).Go", so search for that string to find them all; within a block the top frame is where the goroutine is blocked, and the function passed to Go sits directly above the parleak.(*Group).Go.func1 wrapper frame. The label does not appear in the dump.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets how long a Group waits, after cancelling its context, for a tracked goroutine to return before reporting it as leaked. The default is one second. A non-positive duration means don't wait at all: a goroutine that hasn't already returned is reported immediately.

type TB

type TB interface {
	Errorf(format string, args ...any)
	Cleanup(func())
	Helper()
}

TB is the subset of *testing.T (and *testing.B) that a Group needs. Depending on this interface rather than the concrete type lets a Group be driven by a test double, so the failure path can be asserted on without failing a real test. *testing.T and *testing.B satisfy it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL