pax_global_header00006660000000000000000000000064145261346040014517gustar00rootroot0000000000000052 comment=4020168e860fcd0f3ca425372166b198269ebf70 golang-uber-goleak-1.3.0/000077500000000000000000000000001452613460400151625ustar00rootroot00000000000000golang-uber-goleak-1.3.0/.github/000077500000000000000000000000001452613460400165225ustar00rootroot00000000000000golang-uber-goleak-1.3.0/.github/workflows/000077500000000000000000000000001452613460400205575ustar00rootroot00000000000000golang-uber-goleak-1.3.0/.github/workflows/ci.yml000066400000000000000000000017601452613460400217010ustar00rootroot00000000000000name: CI on: push: branches: [master] tags: ['v*'] pull_request: branches: ['*'] permissions: contents: read jobs: test: runs-on: ubuntu-latest strategy: matrix: go: ["1.20.x", "1.21.x"] steps: - name: Checkout code uses: actions/checkout@v3 - name: Setup Go uses: actions/setup-go@v4 with: go-version: ${{ matrix.go }} - name: Test run: make cover - name: Upload coverage to codecov.io uses: codecov/codecov-action@v3 lint: name: Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 name: Check out repository - uses: actions/setup-go@v4 name: Set up Go with: go-version: 1.21.x cache: false # managed by golangci-lint - uses: golangci/golangci-lint-action@v3 name: Install golangci-lint with: version: latest args: --version # make lint will run the linter - run: make lint name: Lint golang-uber-goleak-1.3.0/.github/workflows/fossa.yaml000066400000000000000000000005411452613460400225560ustar00rootroot00000000000000name: FOSSA Analysis on: push permissions: contents: read jobs: build: runs-on: ubuntu-latest if: github.repository_owner == 'uber-go' steps: - name: Checkout code uses: actions/checkout@v2 - name: FOSSA analysis uses: fossas/fossa-action@v1 with: api-key: ${{ secrets.FOSSA_API_KEY }} golang-uber-goleak-1.3.0/.gitignore000066400000000000000000000000561452613460400171530ustar00rootroot00000000000000vendor/ /bin /lint.log /cover.out /cover.html golang-uber-goleak-1.3.0/.golangci.yml000066400000000000000000000011731452613460400175500ustar00rootroot00000000000000output: # Make output more digestible with quickfix in vim/emacs/etc. sort-results: true print-issued-lines: false linters: enable: - gofumpt - nolintlint - revive linters-settings: govet: # These govet checks are disabled by default, but they're useful. enable: - niliness - reflectvaluecompare - sortslice - unusedwrite issues: # Print all issues reported by all linters. max-issues-per-linter: 0 max-same-issues: 0 # Don't ignore some of the issues that golangci-lint considers okay. # This includes documenting all exported entities. exclude-use-default: false golang-uber-goleak-1.3.0/CHANGELOG.md000066400000000000000000000044701452613460400170000ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [1.3.0] ### Fixed - Built-in ignores now match function names more accurately. They will no longer ignore stacks because of file names that look similar to function names. (#112) ### Added - Add an `IgnoreAnyFunction` option to ignore stack traces that have the provided function anywhere in the stack. (#113) - Ignore `testing.runFuzzing` and `testing.runFuzzTests` alongside other already-ignored test functions (`testing.RunTests`, etc). (#105) ### Changed - Miscellaneous CI-related fixes. (#103, #108, #114) [1.3.0]: https://github.com/uber-go/goleak/compare/v1.2.1...v1.3.0 ## [1.2.1] ### Changed - Drop golang/x/lint dependency. [1.2.1]: https://github.com/uber-go/goleak/compare/v1.2.0...v1.2.1 ## [1.2.0] ### Added - Add Cleanup option that can be used for registering cleanup callbacks. (#78) ### Changed - Mark VerifyNone as a test helper. (#75) Thanks to @tallclair for their contribution to this release. [1.2.0]: https://github.com/uber-go/goleak/compare/v1.1.12...v1.2.0 ## [1.1.12] ### Fixed - Fixed logic for ignoring trace related goroutines on Go versions 1.16 and above. [1.1.12]: https://github.com/uber-go/goleak/compare/v1.1.11...v1.1.12 ## [1.1.11] ### Fixed - Documentation fix on how to test. - Update dependency on stretchr/testify to v1.7.0. (#59) - Update dependency on golang.org/x/tools to address CVE-2020-14040. (#62) [1.1.11]: https://github.com/uber-go/goleak/compare/v1.1.10...v1.1.11 ## [1.1.10] ### Added - [#49]: Add option to ignore current goroutines, which checks for any additional leaks and allows for incremental adoption of goleak in larger projects. Thanks to @denis-tingajkin for their contributions to this release. [#49]: https://github.com/uber-go/goleak/pull/49 [1.1.10]: https://github.com/uber-go/goleak/compare/v1.0.0...v1.1.10 ## [1.0.0] ### Changed - Migrate to Go modules. ### Fixed - Ignore trace related goroutines that cause false positives with -trace. [1.0.0]: https://github.com/uber-go/goleak/compare/v0.10.0...v1.0.0 ## [0.10.0] - Initial release. [0.10.0]: https://github.com/uber-go/goleak/compare/v0.10.0...HEAD golang-uber-goleak-1.3.0/LICENSE000066400000000000000000000021021452613460400161620ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2018 Uber Technologies, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-uber-goleak-1.3.0/Makefile000066400000000000000000000014401452613460400166210ustar00rootroot00000000000000# Directory containing the Makefile. PROJECT_ROOT = $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) export GOBIN = $(PROJECT_ROOT)/bin export PATH := $(GOBIN):$(PATH) GO_FILES = $(shell find . \ -path '*/.*' -prune -o \ '(' -type f -a -name '*.go' ')' -print) # Additional test flags. TEST_FLAGS ?= .PHONY: all all: lint build test .PHONY: lint lint: golangci-lint tidy-lint .PHONY: build build: go build ./... .PHONY: test test: go test -v -race ./... go test -v -trace=/dev/null . .PHONY: cover cover: go test -race -coverprofile=cover.out -coverpkg=./... ./... go tool cover -html=cover.out -o cover.html .PHONY: golangci-lint golangci-lint: golangci-lint run .PHONY: tidy tidy: go mod tidy .PHONY: tidy-lint tidy-lint: go mod tidy git diff --exit-code -- go.mod go.sum golang-uber-goleak-1.3.0/README.md000066400000000000000000000043021452613460400164400ustar00rootroot00000000000000# goleak [![GoDoc][doc-img]][doc] [![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov] Goroutine leak detector to help avoid Goroutine leaks. ## Installation You can use `go get` to get the latest version: `go get -u go.uber.org/goleak` `goleak` also supports semver releases. Note that go-leak only [supports][release] the two most recent minor versions of Go. ## Quick Start To verify that there are no unexpected goroutines running at the end of a test: ```go func TestA(t *testing.T) { defer goleak.VerifyNone(t) // test logic here. } ``` Instead of checking for leaks at the end of every test, `goleak` can also be run at the end of every test package by creating a `TestMain` function for your package: ```go func TestMain(m *testing.M) { goleak.VerifyTestMain(m) } ``` ## Determine Source of Package Leaks When verifying leaks using `TestMain`, the leak test is only run once after all tests have been run. This is typically enough to ensure there's no goroutines leaked from tests, but when there are leaks, it's hard to determine which test is causing them. You can use the following bash script to determine the source of the failing test: ```sh # Create a test binary which will be used to run each test individually $ go test -c -o tests # Run each test individually, printing "." for successful tests, or the test name # for failing tests. $ for test in $(go test -list . | grep -E "^(Test|Example)"); do ./tests -test.run "^$test\$" &>/dev/null && echo -n "." || echo -e "\n$test failed"; done ``` This will only print names of failing tests which can be investigated individually. E.g., ``` ..... TestLeakyTest failed ....... ``` ## Stability goleak is v1 and follows [SemVer](http://semver.org/) strictly. No breaking changes will be made to exported APIs before 2.0. [doc-img]: https://godoc.org/go.uber.org/goleak?status.svg [doc]: https://godoc.org/go.uber.org/goleak [ci-img]: https://github.com/uber-go/goleak/actions/workflows/ci.yml/badge.svg [ci]: https://github.com/uber-go/goleak/actions/workflows/ci.yml [cov-img]: https://codecov.io/gh/uber-go/goleak/branch/master/graph/badge.svg [cov]: https://codecov.io/gh/uber-go/goleak [release]: https://go.dev/doc/devel/release#policy golang-uber-goleak-1.3.0/doc.go000066400000000000000000000022761452613460400162650ustar00rootroot00000000000000// Copyright (c) 2018 Uber Technologies, Inc. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // Package goleak is a Goroutine leak detector. package goleak // import "go.uber.org/goleak" golang-uber-goleak-1.3.0/go.mod000066400000000000000000000005151452613460400162710ustar00rootroot00000000000000module go.uber.org/goleak go 1.20 require github.com/stretchr/testify v1.8.0 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/pretty v0.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) golang-uber-goleak-1.3.0/go.sum000066400000000000000000000034631452613460400163230ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= golang-uber-goleak-1.3.0/internal/000077500000000000000000000000001452613460400167765ustar00rootroot00000000000000golang-uber-goleak-1.3.0/internal/stack/000077500000000000000000000000001452613460400201035ustar00rootroot00000000000000golang-uber-goleak-1.3.0/internal/stack/doc.go000066400000000000000000000022671452613460400212060ustar00rootroot00000000000000// Copyright (c) 2017-2023 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // Package stack is used for parsing stacks from `runtime.Stack`. package stack golang-uber-goleak-1.3.0/internal/stack/scan.go000066400000000000000000000033671452613460400213670ustar00rootroot00000000000000// Copyright (c) 2023 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package stack import ( "bufio" "io" ) // scanner provides a bufio.Scanner the ability to Unscan, // which allows the current token to be read again // after the next Scan. type scanner struct { *bufio.Scanner unscanned bool } func newScanner(r io.Reader) *scanner { return &scanner{Scanner: bufio.NewScanner(r)} } func (s *scanner) Scan() bool { if s.unscanned { s.unscanned = false return true } return s.Scanner.Scan() } // Unscan stops the scanner from advancing its position // for the next Scan. // // Bytes and Text will return the same token after next Scan // that they do right now. func (s *scanner) Unscan() { s.unscanned = true } golang-uber-goleak-1.3.0/internal/stack/scan_test.go000066400000000000000000000032441452613460400224200ustar00rootroot00000000000000// Copyright (c) 2023 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package stack import ( "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestScanner(t *testing.T) { scanner := newScanner(strings.NewReader("foo\nbar\nbaz\n")) require.True(t, scanner.Scan()) assert.Equal(t, "foo", scanner.Text()) require.True(t, scanner.Scan()) assert.Equal(t, "bar", scanner.Text()) scanner.Unscan() assert.Equal(t, "bar", scanner.Text()) require.True(t, scanner.Scan()) assert.Equal(t, "bar", scanner.Text()) require.True(t, scanner.Scan()) assert.Equal(t, "baz", scanner.Text()) } golang-uber-goleak-1.3.0/internal/stack/stacks.go000066400000000000000000000200061452613460400217200ustar00rootroot00000000000000// Copyright (c) 2017-2023 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package stack import ( "bytes" "errors" "fmt" "io" "runtime" "strconv" "strings" ) const _defaultBufferSize = 64 * 1024 // 64 KiB // Stack represents a single Goroutine's stack. type Stack struct { id int state string // e.g. 'running', 'chan receive' // The first function on the stack. firstFunction string // A set of all functions in the stack, allFunctions map[string]struct{} // Full, raw stack trace. fullStack string } // ID returns the goroutine ID. func (s Stack) ID() int { return s.id } // State returns the Goroutine's state. func (s Stack) State() string { return s.state } // Full returns the full stack trace for this goroutine. func (s Stack) Full() string { return s.fullStack } // FirstFunction returns the name of the first function on the stack. func (s Stack) FirstFunction() string { return s.firstFunction } // HasFunction reports whether the stack has the given function // anywhere in it. func (s Stack) HasFunction(name string) bool { _, ok := s.allFunctions[name] return ok } func (s Stack) String() string { return fmt.Sprintf( "Goroutine %v in state %v, with %v on top of the stack:\n%s", s.id, s.state, s.firstFunction, s.Full()) } func getStacks(all bool) []Stack { trace := getStackBuffer(all) stacks, err := newStackParser(bytes.NewReader(trace)).Parse() if err != nil { // Well-formed stack traces should never fail to parse. // If they do, it's a bug in this package. // Panic so we can fix it. panic(fmt.Sprintf("Failed to parse stack trace: %v\n%s", err, trace)) } return stacks } type stackParser struct { scan *scanner stacks []Stack errors []error } func newStackParser(r io.Reader) *stackParser { return &stackParser{ scan: newScanner(r), } } func (p *stackParser) Parse() ([]Stack, error) { for p.scan.Scan() { line := p.scan.Text() // If we see the goroutine header, start a new stack. if strings.HasPrefix(line, "goroutine ") { stack, err := p.parseStack(line) if err != nil { p.errors = append(p.errors, err) continue } p.stacks = append(p.stacks, stack) } } p.errors = append(p.errors, p.scan.Err()) return p.stacks, errors.Join(p.errors...) } // parseStack parses a single stack trace from the given scanner. // line is the first line of the stack trace, which should look like: // // goroutine 123 [runnable]: func (p *stackParser) parseStack(line string) (Stack, error) { id, state, err := parseGoStackHeader(line) if err != nil { return Stack{}, fmt.Errorf("parse header: %w", err) } // Read the rest of the stack trace. var ( firstFunction string fullStack bytes.Buffer ) funcs := make(map[string]struct{}) for p.scan.Scan() { line := p.scan.Text() if strings.HasPrefix(line, "goroutine ") { // If we see the goroutine header, // it's the end of this stack. // Unscan so the next Scan sees the same line. p.scan.Unscan() break } fullStack.WriteString(line) fullStack.WriteByte('\n') // scanner trims the newline if len(line) == 0 { // Empty line usually marks the end of the stack // but we don't want to have to rely on that. // Just skip it. continue } funcName, creator, err := parseFuncName(line) if err != nil { return Stack{}, fmt.Errorf("parse function: %w", err) } if !creator { // A function is part of a goroutine's stack // only if it's not a "created by" function. // // The creator function is part of a different stack. // We don't care about it right now. funcs[funcName] = struct{}{} if firstFunction == "" { firstFunction = funcName } } // The function name followed by a line in the form: // // example.com/path/to/package/file.go:123 +0x123 // // We don't care about the position so we can skip this line. if p.scan.Scan() { // Be defensive: // Skip the line only if it starts with a tab. bs := p.scan.Bytes() if len(bs) > 0 && bs[0] == '\t' { fullStack.Write(bs) fullStack.WriteByte('\n') } else { // Put it back and let the next iteration handle it // if it doesn't start with a tab. p.scan.Unscan() } } if creator { // The "created by" line is the last line of the stack. // We can stop parsing now. // // Note that if tracebackancestors=N is set, // there may be more a traceback of the creator function // following the "created by" line, // but it should not be considered part of this stack. // e.g., // // created by testing.(*T).Run in goroutine 1 // /usr/lib/go/src/testing/testing.go:1648 +0x3ad // [originating from goroutine 1]: // testing.(*T).Run(...) // /usr/lib/go/src/testing/testing.go:1649 +0x3ad // break } } return Stack{ id: id, state: state, firstFunction: firstFunction, allFunctions: funcs, fullStack: fullStack.String(), }, nil } // All returns the stacks for all running goroutines. func All() []Stack { return getStacks(true) } // Current returns the stack for the current goroutine. func Current() Stack { return getStacks(false)[0] } func getStackBuffer(all bool) []byte { for i := _defaultBufferSize; ; i *= 2 { buf := make([]byte, i) if n := runtime.Stack(buf, all); n < i { return buf[:n] } } } // Parses a single function from the given line. // The line is in one of these formats: // // example.com/path/to/package.funcName(args...) // example.com/path/to/package.(*typeName).funcName(args...) // created by example.com/path/to/package.funcName // created by example.com/path/to/package.funcName in goroutine [...] // // Also reports whether the line was a "created by" line. func parseFuncName(line string) (name string, creator bool, err error) { if after, ok := strings.CutPrefix(line, "created by "); ok { // The function name is the part after "created by " // and before " in goroutine [...]". idx := strings.Index(after, " in goroutine") if idx >= 0 { after = after[:idx] } name = after creator = true } else if idx := strings.LastIndexByte(line, '('); idx >= 0 { // The function name is the part before the last '('. name = line[:idx] } if name == "" { return "", false, fmt.Errorf("no function found: %q", line) } return name, creator, nil } // parseGoStackHeader parses a stack header that looks like: // goroutine 643 [runnable]:\n // And returns the goroutine ID, and the state. func parseGoStackHeader(line string) (goroutineID int, state string, err error) { // The scanner will have already trimmed the "\n", // but we'll guard against it just in case. // // Trimming them separately makes them both optional. line = strings.TrimSuffix(strings.TrimSuffix(line, ":"), "\n") parts := strings.SplitN(line, " ", 3) if len(parts) != 3 { return 0, "", fmt.Errorf("unexpected format: %q", line) } id, err := strconv.Atoi(parts[1]) if err != nil { return 0, "", fmt.Errorf("bad goroutine ID %q in line %q", parts[1], line) } state = strings.TrimSuffix(strings.TrimPrefix(parts[2], "["), "]") return id, state, nil } golang-uber-goleak-1.3.0/internal/stack/stacks_test.go000066400000000000000000000336351452613460400227730ustar00rootroot00000000000000// Copyright (c) 2017-2023 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package stack import ( "os" "path/filepath" "runtime" "sort" "strings" "sync" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var _allDone chan struct{} func waitForDone() { <-_allDone } func TestAll(t *testing.T) { // We use a global channel so that the function below does not // receive any arguments, so we can test that parseFirstFunc works // regardless of arguments on the stack. _allDone = make(chan struct{}) defer close(_allDone) for i := 0; i < 5; i++ { go waitForDone() } cur := Current() got := All() // Retry until the background stacks are not runnable/running. for { if !isBackgroundRunning(cur, got) { break } runtime.Gosched() got = All() } // We have exactly 7 gorotuines: // "main" goroutine // test goroutine // 5 goroutines started above. require.Len(t, got, 7) sort.Sort(byGoroutineID(got)) assert.Contains(t, got[0].Full(), "testing.(*T).Run") assert.Contains(t, got[0].allFunctions, "testing.(*T).Run") assert.Contains(t, got[1].Full(), "TestAll") assert.Contains(t, got[1].allFunctions, "go.uber.org/goleak/internal/stack.TestAll") for i := 0; i < 5; i++ { assert.Contains(t, got[2+i].Full(), "stack.waitForDone") } } func TestCurrent(t *testing.T) { const pkgPrefix = "go.uber.org/goleak/internal/stack" got := Current() assert.NotZero(t, got.ID(), "Should get non-zero goroutine id") assert.Equal(t, "running", got.State()) assert.Equal(t, "go.uber.org/goleak/internal/stack.getStackBuffer", got.FirstFunction()) wantFrames := []string{ "getStackBuffer", "getStacks", "Current", "Current", "TestCurrent", } all := got.Full() for _, frame := range wantFrames { name := pkgPrefix + "." + frame assert.Contains(t, all, name) assert.True(t, got.HasFunction(name), "missing in stack: %v\n%s", name, all) } assert.Contains(t, got.String(), "in state") assert.Contains(t, got.String(), "on top of the stack") assert.Contains(t, all, "stack/stacks_test.go", "file name missing in stack:\n%s", all) // Ensure that we are not returning the buffer without slicing it // from getStackBuffer. if len(got.Full()) > 1024 { t.Fatalf("Returned stack is too large") } } func TestCurrentCreatedBy(t *testing.T) { var stack Stack done := make(chan struct{}) go func() { defer close(done) stack = Current() }() <-done // The test function created the goroutine // so it won't be part of the stack. assert.False(t, stack.HasFunction("go.uber.org/goleak/internal/stack.TestCurrentCreatedBy"), "TestCurrentCreatedBy should not be in stack:\n%s", stack.Full()) // However, the nested function should be. assert.True(t, stack.HasFunction("go.uber.org/goleak/internal/stack.TestCurrentCreatedBy.func1"), "TestCurrentCreatedBy.func1 is not in stack:\n%s", stack.Full()) } func TestAllLargeStack(t *testing.T) { const ( stackDepth = 100 numGoroutines = 100 ) var started sync.WaitGroup done := make(chan struct{}) for i := 0; i < numGoroutines; i++ { var f func(int) f = func(count int) { if count == 0 { started.Done() <-done return } f(count - 1) } started.Add(1) go f(stackDepth) } started.Wait() buf := getStackBuffer(true /* all */) if len(buf) <= _defaultBufferSize { t.Fatalf("Expected larger stack buffer") } // Start enough goroutines so we exceed the default buffer size. close(done) } func TestParseFuncName(t *testing.T) { tests := []struct { name string give string want string creator bool }{ { name: "function", give: "example.com/foo/bar.baz()", want: "example.com/foo/bar.baz", }, { name: "method", give: "example.com/foo/bar.(*baz).qux()", want: "example.com/foo/bar.(*baz).qux", }, { name: "created by", // Go 1.20 give: "created by example.com/foo/bar.baz", want: "example.com/foo/bar.baz", creator: true, }, { name: "created by/in goroutine", // Go 1.21 give: "created by example.com/foo/bar.baz in goroutine 123", want: "example.com/foo/bar.baz", creator: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, creator, err := parseFuncName(tt.give) require.NoError(t, err) assert.Equal(t, tt.want, got) assert.Equal(t, tt.creator, creator) }) } } func TestParseStack(t *testing.T) { tests := []struct { name string give string id int state string firstFunc string funcs []string }{ { name: "running", give: joinLines( "goroutine 1 [running]:", "example.com/foo/bar.baz()", " example.com/foo/bar.go:123", ), id: 1, state: "running", firstFunc: "example.com/foo/bar.baz", funcs: []string{"example.com/foo/bar.baz"}, }, { name: "without position", give: joinLines( "goroutine 1 [running]:", "example.com/foo/bar.baz()", // Oops, no "file:line" entry for this function. "example.com/foo/bar.qux()", " example.com/foo/bar.go:456", ), id: 1, state: "running", firstFunc: "example.com/foo/bar.baz", funcs: []string{ "example.com/foo/bar.baz", "example.com/foo/bar.qux", }, }, { name: "created by", give: joinLines( "goroutine 1 [running]:", "example.com/foo/bar.baz()", " example.com/foo/bar.go:123", "created by example.com/foo/bar.qux", " example.com/foo/bar.go:456", ), id: 1, state: "running", firstFunc: "example.com/foo/bar.baz", funcs: []string{ "example.com/foo/bar.baz", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { stacks, err := newStackParser(strings.NewReader(tt.give)).Parse() require.NoError(t, err) require.Len(t, stacks, 1) stack := stacks[0] assert.Equal(t, tt.id, stack.ID()) assert.Equal(t, tt.state, stack.State()) assert.Equal(t, tt.firstFunc, stack.FirstFunction()) for _, fn := range tt.funcs { assert.True(t, stack.HasFunction(fn), "missing in stack: %v\n%s", fn, stack.Full()) } }) } } func TestParseStackErrors(t *testing.T) { tests := []struct { name string give string wantErr string }{ { name: "bad goroutine ID", give: "goroutine no-number [running]:", wantErr: `bad goroutine ID "no-number"`, }, { name: "not enough parts", give: "goroutine [running]:", wantErr: `unexpected format`, }, { name: "bad function name", give: joinLines( "goroutine 1 [running]:", "example.com/foo/bar.baz", // no arguments " example.com/foo/bar.go:123", ), wantErr: `no function found`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := newStackParser(strings.NewReader(tt.give)).Parse() require.Error(t, err) assert.ErrorContains(t, err, tt.wantErr) }) } } func TestParseStackFixtures(t *testing.T) { type goroutine struct { // ID must match the goroutine ID in the fixture. // We use this to ensure that we are matching the right goroutine. ID int State string FirstFunction string HasFunctions []string // non-exhaustive, in any order NotHasFunctions []string } tests := []struct { name string // file name inside testdata stacks []goroutine // in any order }{ { name: "http.txt", stacks: []goroutine{ { ID: 1, State: "running", FirstFunction: "main.getStackBuffer", HasFunctions: []string{ "main.getStackBuffer", "main.main", }, }, { ID: 4, State: "IO wait", FirstFunction: "internal/poll.runtime_pollWait", HasFunctions: []string{ "internal/poll.runtime_pollWait", "net/http.Serve", }, NotHasFunctions: []string{"main.start"}, }, { ID: 20, State: "select", FirstFunction: "net/http.(*persistConn).readLoop", }, { ID: 21, State: "select", FirstFunction: "net/http.(*persistConn).writeLoop", }, { ID: 8, State: "IO wait", FirstFunction: "internal/poll.runtime_pollWait", HasFunctions: []string{ "internal/poll.runtime_pollWait", "net/http.(*conn).serve", }, NotHasFunctions: []string{"net/http.(*Server).Serve"}, }, }, }, { name: "http.go1.20.txt", stacks: []goroutine{ { ID: 1, State: "running", FirstFunction: "main.getStackBuffer", HasFunctions: []string{ "main.getStackBuffer", "main.main", }, }, { ID: 20, State: "IO wait", FirstFunction: "internal/poll.runtime_pollWait", HasFunctions: []string{ "internal/poll.runtime_pollWait", "net/http.(*Server).Serve", }, NotHasFunctions: []string{"main.start"}, }, { ID: 24, State: "select", FirstFunction: "net/http.(*persistConn).readLoop", }, { ID: 25, State: "select", FirstFunction: "net/http.(*persistConn).writeLoop", }, { ID: 4, State: "IO wait", FirstFunction: "internal/poll.runtime_pollWait", HasFunctions: []string{ "internal/poll.runtime_pollWait", "net/http.(*conn).serve", }, NotHasFunctions: []string{"net/http.(*Server).Serve"}, }, }, }, { name: "http.tracebackancestors.txt", stacks: []goroutine{ { ID: 1, State: "running", FirstFunction: "main.getStackBuffer", HasFunctions: []string{ "main.getStackBuffer", "main.main", }, }, { ID: 20, State: "IO wait", FirstFunction: "internal/poll.runtime_pollWait", HasFunctions: []string{ "internal/poll.runtime_pollWait", "net/http.Serve", }, NotHasFunctions: []string{ "main.start", // created by "main.main", // tracebackancestors }, }, { ID: 24, State: "select", FirstFunction: "net/http.(*persistConn).readLoop", NotHasFunctions: []string{ "net/http.(*Transport).dialConn", // created by // tracebackancestors: "net/http.(*Transport).dialConnFor", "net/http.(*Transport).queueForDial", "net/http.(*Client).Get", "main.start", "main.main", }, }, { ID: 4, State: "IO wait", FirstFunction: "internal/poll.runtime_pollWait", HasFunctions: []string{ "internal/poll.runtime_pollWait", "net/http.(*conn).serve", }, NotHasFunctions: []string{ "net/http.(*Server).Serve", // created by // tracebackancestors: "net/http.Serve", "main.start", "main.main", }, }, { ID: 25, State: "select", FirstFunction: "net/http.(*persistConn).writeLoop", NotHasFunctions: []string{ "net/http.(*Transport).dialConn", // created by // tracebackancestors: "net/http.(*Transport).dialConnFor", "net/http.(*Transport).queueForDial", "net/http.(*Client).Get", "main.start", "main.main", }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fixture, err := os.Open(filepath.Join("testdata", tt.name)) require.NoError(t, err) defer func() { assert.NoError(t, fixture.Close()) }() stacks, err := newStackParser(fixture).Parse() require.NoError(t, err) stacksByID := make(map[int]Stack, len(stacks)) for _, s := range stacks { stacksByID[s.ID()] = s } for _, wantStack := range tt.stacks { gotStack, ok := stacksByID[wantStack.ID] if !assert.True(t, ok, "missing stack %v", wantStack.ID) { continue } delete(stacksByID, wantStack.ID) assert.Equal(t, wantStack.State, gotStack.State()) assert.Equal(t, wantStack.FirstFunction, gotStack.FirstFunction()) for _, fn := range wantStack.HasFunctions { assert.True(t, gotStack.HasFunction(fn), "missing in stack: %v\n%s", fn, gotStack.Full()) } for _, fn := range wantStack.NotHasFunctions { assert.False(t, gotStack.HasFunction(fn), "unexpected in stack: %v\n%s", fn, gotStack.Full()) } } for _, s := range stacksByID { t.Errorf("unexpected stack:\n%s", s.Full()) } }) } } func joinLines(lines ...string) string { return strings.Join(lines, "\n") + "\n" } type byGoroutineID []Stack func (ss byGoroutineID) Len() int { return len(ss) } func (ss byGoroutineID) Less(i, j int) bool { return ss[i].ID() < ss[j].ID() } func (ss byGoroutineID) Swap(i, j int) { ss[i], ss[j] = ss[j], ss[i] } // Note: This is the same logic as in ../../utils_test.go // Copy+pasted to avoid dependency loops and exporting this test-helper. func isBackgroundRunning(cur Stack, stacks []Stack) bool { for _, s := range stacks { if cur.ID() == s.ID() { continue } if strings.Contains(s.State(), "run") { return true } } return false } golang-uber-goleak-1.3.0/internal/stack/testdata/000077500000000000000000000000001452613460400217145ustar00rootroot00000000000000golang-uber-goleak-1.3.0/internal/stack/testdata/Makefile000066400000000000000000000011041452613460400233500ustar00rootroot00000000000000.DEFAULT_GOAL := all GO_VERSION = $(shell go version | cut -d' ' -f3) # Append to this list to add new stacks. STACKS = # In Go 1.21, the output format was changed slightly. # # Generate a 1.20 version of the output # only if we're running on Go 1.20. ifneq (,$(findstring go1.20,$(GO_VERSION))) STACKS += http.go1.20.txt http.go1.20.txt: http.go go run $< > $@ else STACKS += http.txt http.txt: http.go go run $< > $@ endif STACKS += http.tracebackancestors.txt http.tracebackancestors.txt: http.go GODEBUG=tracebackancestors=10 go run $< > $@ .PHONY: all all: $(STACKS) golang-uber-goleak-1.3.0/internal/stack/testdata/http.go000066400000000000000000000013361452613460400232250ustar00rootroot00000000000000//go:build ignore package main import ( "fmt" "net" "net/http" "runtime" "time" ) func main() { if err := start(); err != nil { panic(err) } fmt.Println(string(getStackBuffer())) } func start() error { ln, err := net.Listen("tcp", ":0") if err != nil { return err } go http.Serve(ln, nil) // Wait until HTTP server is ready. url := "http://" + ln.Addr().String() for i := 0; i < 10; i++ { if _, err := http.Get(url); err == nil { return nil } time.Sleep(100 * time.Millisecond) } return fmt.Errorf("failed to start HTTP server") } func getStackBuffer() []byte { for i := 4096; ; i *= 2 { buf := make([]byte, i) if n := runtime.Stack(buf, true /* all */); n < i { return buf[:n] } } } golang-uber-goleak-1.3.0/internal/stack/testdata/http.go1.20.txt000066400000000000000000000070301452613460400243410ustar00rootroot00000000000000goroutine 1 [running]: main.getStackBuffer() /home/abg/src/goleak/internal/stack/testdata/http.go:44 +0x4f main.main() /home/abg/src/goleak/internal/stack/testdata/http.go:18 +0x2a goroutine 20 [IO wait]: internal/poll.runtime_pollWait(0x7866b1e34f08, 0x72) /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/runtime/netpoll.go:306 +0x89 internal/poll.(*pollDesc).wait(0xc0000dc000?, 0x16?, 0x0) /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/internal/poll/fd_poll_runtime.go:84 +0x32 internal/poll.(*pollDesc).waitRead(...) /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/internal/poll/fd_poll_runtime.go:89 internal/poll.(*FD).Accept(0xc0000dc000) /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/internal/poll/fd_unix.go:614 +0x2bd net.(*netFD).accept(0xc0000dc000) /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/fd_unix.go:172 +0x35 net.(*TCPListener).accept(0xc0000a00f0) /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/tcpsock_posix.go:148 +0x25 net.(*TCPListener).Accept(0xc0000a00f0) /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/tcpsock.go:297 +0x3d net/http.(*Server).Serve(0xc000076000, {0x73dbe0, 0xc0000a00f0}) /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/http/server.go:3059 +0x385 net/http.Serve({0x73dbe0, 0xc0000a00f0}, {0x0?, 0x0}) /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/http/server.go:2581 +0x74 created by main.start /home/abg/src/goleak/internal/stack/testdata/http.go:27 +0x8e goroutine 24 [select]: net/http.(*persistConn).readLoop(0xc0000b4480) /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/http/transport.go:2227 +0xd85 created by net/http.(*Transport).dialConn /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/http/transport.go:1765 +0x16ea goroutine 25 [select]: net/http.(*persistConn).writeLoop(0xc0000b4480) /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/http/transport.go:2410 +0xf2 created by net/http.(*Transport).dialConn /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/http/transport.go:1766 +0x173d goroutine 4 [IO wait]: internal/poll.runtime_pollWait(0x7866b1e34d28, 0x72) /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/runtime/netpoll.go:306 +0x89 internal/poll.(*pollDesc).wait(0xc00007e000?, 0xc000106000?, 0x0) /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/internal/poll/fd_poll_runtime.go:84 +0x32 internal/poll.(*pollDesc).waitRead(...) /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/internal/poll/fd_poll_runtime.go:89 internal/poll.(*FD).Read(0xc00007e000, {0xc000106000, 0x1000, 0x1000}) /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/internal/poll/fd_unix.go:167 +0x299 net.(*netFD).Read(0xc00007e000, {0xc000106000?, 0x4a92e6?, 0x0?}) /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/fd_posix.go:55 +0x29 net.(*conn).Read(0xc000014028, {0xc000106000?, 0x0?, 0xc0000781e8?}) /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/net.go:183 +0x45 net/http.(*connReader).Read(0xc0000781e0, {0xc000106000, 0x1000, 0x1000}) /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/http/server.go:782 +0x171 bufio.(*Reader).fill(0xc000104000) /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/bufio/bufio.go:106 +0xff bufio.(*Reader).Peek(0xc000104000, 0x4) /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/bufio/bufio.go:144 +0x5d net/http.(*conn).serve(0xc000100000, {0x73df98, 0xc0000780f0}) /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/http/server.go:2030 +0x77c created by net/http.(*Server).Serve /home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/http/server.go:3089 +0x5ed golang-uber-goleak-1.3.0/internal/stack/testdata/http.tracebackancestors.txt000066400000000000000000000136011452613460400272750ustar00rootroot00000000000000goroutine 1 [running]: main.getStackBuffer() /home/abg/src/goleak/internal/stack/testdata/http.go:44 +0x49 main.main() /home/abg/src/goleak/internal/stack/testdata/http.go:18 +0x1d goroutine 20 [IO wait]: internal/poll.runtime_pollWait(0x7c3a3d619e48, 0x72) /usr/lib/go/src/runtime/netpoll.go:343 +0x85 internal/poll.(*pollDesc).wait(0xc0000da000?, 0x16?, 0x0) /usr/lib/go/src/internal/poll/fd_poll_runtime.go:84 +0x27 internal/poll.(*pollDesc).waitRead(...) /usr/lib/go/src/internal/poll/fd_poll_runtime.go:89 internal/poll.(*FD).Accept(0xc0000da000) /usr/lib/go/src/internal/poll/fd_unix.go:611 +0x2ac net.(*netFD).accept(0xc0000da000) /usr/lib/go/src/net/fd_unix.go:172 +0x29 net.(*TCPListener).accept(0xc0000ba0c0) /usr/lib/go/src/net/tcpsock_posix.go:152 +0x1e net.(*TCPListener).Accept(0xc0000ba0c0) /usr/lib/go/src/net/tcpsock.go:315 +0x30 net/http.(*Server).Serve(0xc000078000, {0x738a20, 0xc0000ba0c0}) /usr/lib/go/src/net/http/server.go:3056 +0x364 net/http.Serve({0x738a20, 0xc0000ba0c0}, {0x0?, 0x0}) /usr/lib/go/src/net/http/server.go:2595 +0x6c created by main.start in goroutine 1 /home/abg/src/goleak/internal/stack/testdata/http.go:27 +0x87 [originating from goroutine 1]: main.start(...) /home/abg/src/goleak/internal/stack/testdata/http.go:30 +0x87 main.main(...) /home/abg/src/goleak/internal/stack/testdata/http.go:14 +0x13 goroutine 24 [select]: net/http.(*persistConn).readLoop(0xc0000be480) /usr/lib/go/src/net/http/transport.go:2238 +0xd25 created by net/http.(*Transport).dialConn in goroutine 21 /usr/lib/go/src/net/http/transport.go:1776 +0x169f [originating from goroutine 21]: net/http.(*Transport).dialConn(...) /usr/lib/go/src/net/http/transport.go:1777 +0x169f net/http.(*Transport).dialConnFor(...) /usr/lib/go/src/net/http/transport.go:1469 +0x9f created by net/http.(*Transport).queueForDial /usr/lib/go/src/net/http/transport.go:1436 +0x3cb [originating from goroutine 1]: net/http.(*Transport).queueForDial(...) /usr/lib/go/src/net/http/transport.go:1437 +0x3cb net/http.(*Request).Context(...) /usr/lib/go/src/net/http/request.go:346 +0x4c9 net/http.(*Transport).roundTrip(...) /usr/lib/go/src/net/http/transport.go:591 +0x73a net/http.(*Transport).RoundTrip(...) /usr/lib/go/src/net/http/roundtrip.go:17 +0x13 net/http.send(...) /usr/lib/go/src/net/http/client.go:260 +0x606 net/http.(*Client).send(...) /usr/lib/go/src/net/http/client.go:182 +0x98 net/http.(*Client).do(...) /usr/lib/go/src/net/http/client.go:724 +0x912 net/http.(*Client).Get(...) /usr/lib/go/src/net/http/client.go:488 +0x5f net/http.(*Client).Get(...) /usr/lib/go/src/net/http/client.go:488 +0x60 main.start(...) /home/abg/src/goleak/internal/stack/testdata/http.go:32 +0x111 main.start(...) /home/abg/src/goleak/internal/stack/testdata/http.go:32 +0x112 main.main(...) /home/abg/src/goleak/internal/stack/testdata/http.go:14 +0x13 goroutine 4 [IO wait]: internal/poll.runtime_pollWait(0x7c3a3d619d50, 0x72) /usr/lib/go/src/runtime/netpoll.go:343 +0x85 internal/poll.(*pollDesc).wait(0xc00007e000?, 0xc000106000?, 0x0) /usr/lib/go/src/internal/poll/fd_poll_runtime.go:84 +0x27 internal/poll.(*pollDesc).waitRead(...) /usr/lib/go/src/internal/poll/fd_poll_runtime.go:89 internal/poll.(*FD).Read(0xc00007e000, {0xc000106000, 0x1000, 0x1000}) /usr/lib/go/src/internal/poll/fd_unix.go:164 +0x27a net.(*netFD).Read(0xc00007e000, {0xc000106000?, 0x4a8965?, 0x0?}) /usr/lib/go/src/net/fd_posix.go:55 +0x25 net.(*conn).Read(0xc000046018, {0xc000106000?, 0x0?, 0xc000064248?}) /usr/lib/go/src/net/net.go:179 +0x45 net/http.(*connReader).Read(0xc000064240, {0xc000106000, 0x1000, 0x1000}) /usr/lib/go/src/net/http/server.go:791 +0x14b bufio.(*Reader).fill(0xc000104000) /usr/lib/go/src/bufio/bufio.go:113 +0x103 bufio.(*Reader).Peek(0xc000104000, 0x4) /usr/lib/go/src/bufio/bufio.go:151 +0x53 net/http.(*conn).serve(0xc000100000, {0x739108, 0xc000064150}) /usr/lib/go/src/net/http/server.go:2044 +0x75c created by net/http.(*Server).Serve in goroutine 20 /usr/lib/go/src/net/http/server.go:3086 +0x5cb [originating from goroutine 20]: net/http.(*Server).Serve(...) /usr/lib/go/src/net/http/server.go:3086 +0x5cb net/http.Serve(...) /usr/lib/go/src/net/http/server.go:2595 +0x6c created by main.start /home/abg/src/goleak/internal/stack/testdata/http.go:27 +0x87 [originating from goroutine 1]: main.start(...) /home/abg/src/goleak/internal/stack/testdata/http.go:30 +0x87 main.main(...) /home/abg/src/goleak/internal/stack/testdata/http.go:14 +0x13 goroutine 25 [select]: net/http.(*persistConn).writeLoop(0xc0000be480) /usr/lib/go/src/net/http/transport.go:2421 +0xe5 created by net/http.(*Transport).dialConn in goroutine 21 /usr/lib/go/src/net/http/transport.go:1777 +0x16f1 [originating from goroutine 21]: net/http.(*Transport).dialConn(...) /usr/lib/go/src/net/http/transport.go:1778 +0x16f1 net/http.(*Transport).dialConnFor(...) /usr/lib/go/src/net/http/transport.go:1469 +0x9f created by net/http.(*Transport).queueForDial /usr/lib/go/src/net/http/transport.go:1436 +0x3cb [originating from goroutine 1]: net/http.(*Transport).queueForDial(...) /usr/lib/go/src/net/http/transport.go:1437 +0x3cb net/http.(*Request).Context(...) /usr/lib/go/src/net/http/request.go:346 +0x4c9 net/http.(*Transport).roundTrip(...) /usr/lib/go/src/net/http/transport.go:591 +0x73a net/http.(*Transport).RoundTrip(...) /usr/lib/go/src/net/http/roundtrip.go:17 +0x13 net/http.send(...) /usr/lib/go/src/net/http/client.go:260 +0x606 net/http.(*Client).send(...) /usr/lib/go/src/net/http/client.go:182 +0x98 net/http.(*Client).do(...) /usr/lib/go/src/net/http/client.go:724 +0x912 net/http.(*Client).Get(...) /usr/lib/go/src/net/http/client.go:488 +0x5f net/http.(*Client).Get(...) /usr/lib/go/src/net/http/client.go:488 +0x60 main.start(...) /home/abg/src/goleak/internal/stack/testdata/http.go:32 +0x111 main.start(...) /home/abg/src/goleak/internal/stack/testdata/http.go:32 +0x112 main.main(...) /home/abg/src/goleak/internal/stack/testdata/http.go:14 +0x13 golang-uber-goleak-1.3.0/internal/stack/testdata/http.txt000066400000000000000000000053631452613460400234430ustar00rootroot00000000000000goroutine 1 [running]: main.getStackBuffer() /home/abg/src/goleak/internal/stack/testdata/http.go:44 +0x49 main.main() /home/abg/src/goleak/internal/stack/testdata/http.go:18 +0x1d goroutine 4 [IO wait]: internal/poll.runtime_pollWait(0x7bf130ae7ea0, 0x72) /usr/lib/go/src/runtime/netpoll.go:343 +0x85 internal/poll.(*pollDesc).wait(0xc000132000?, 0x4?, 0x0) /usr/lib/go/src/internal/poll/fd_poll_runtime.go:84 +0x27 internal/poll.(*pollDesc).waitRead(...) /usr/lib/go/src/internal/poll/fd_poll_runtime.go:89 internal/poll.(*FD).Accept(0xc000132000) /usr/lib/go/src/internal/poll/fd_unix.go:611 +0x2ac net.(*netFD).accept(0xc000132000) /usr/lib/go/src/net/fd_unix.go:172 +0x29 net.(*TCPListener).accept(0xc0000600e0) /usr/lib/go/src/net/tcpsock_posix.go:152 +0x1e net.(*TCPListener).Accept(0xc0000600e0) /usr/lib/go/src/net/tcpsock.go:315 +0x30 net/http.(*Server).Serve(0xc00008c000, {0x738a20, 0xc0000600e0}) /usr/lib/go/src/net/http/server.go:3056 +0x364 net/http.Serve({0x738a20, 0xc0000600e0}, {0x0?, 0x0}) /usr/lib/go/src/net/http/server.go:2595 +0x6c created by main.start in goroutine 1 /home/abg/src/goleak/internal/stack/testdata/http.go:27 +0x87 goroutine 20 [select]: net/http.(*persistConn).readLoop(0xc000112480) /usr/lib/go/src/net/http/transport.go:2238 +0xd25 created by net/http.(*Transport).dialConn in goroutine 5 /usr/lib/go/src/net/http/transport.go:1776 +0x169f goroutine 21 [select]: net/http.(*persistConn).writeLoop(0xc000112480) /usr/lib/go/src/net/http/transport.go:2421 +0xe5 created by net/http.(*Transport).dialConn in goroutine 5 /usr/lib/go/src/net/http/transport.go:1777 +0x16f1 goroutine 8 [IO wait]: internal/poll.runtime_pollWait(0x7bf130ae7cb0, 0x72) /usr/lib/go/src/runtime/netpoll.go:343 +0x85 internal/poll.(*pollDesc).wait(0xc000132200?, 0xc000142000?, 0x0) /usr/lib/go/src/internal/poll/fd_poll_runtime.go:84 +0x27 internal/poll.(*pollDesc).waitRead(...) /usr/lib/go/src/internal/poll/fd_poll_runtime.go:89 internal/poll.(*FD).Read(0xc000132200, {0xc000142000, 0x1000, 0x1000}) /usr/lib/go/src/internal/poll/fd_unix.go:164 +0x27a net.(*netFD).Read(0xc000132200, {0xc000142000?, 0x4a8965?, 0x0?}) /usr/lib/go/src/net/fd_posix.go:55 +0x25 net.(*conn).Read(0xc000044070, {0xc000142000?, 0x0?, 0xc00007ad28?}) /usr/lib/go/src/net/net.go:179 +0x45 net/http.(*connReader).Read(0xc00007ad20, {0xc000142000, 0x1000, 0x1000}) /usr/lib/go/src/net/http/server.go:791 +0x14b bufio.(*Reader).fill(0xc000102540) /usr/lib/go/src/bufio/bufio.go:113 +0x103 bufio.(*Reader).Peek(0xc000102540, 0x4) /usr/lib/go/src/bufio/bufio.go:151 +0x53 net/http.(*conn).serve(0xc000134240, {0x739108, 0xc00008e0f0}) /usr/lib/go/src/net/http/server.go:2044 +0x75c created by net/http.(*Server).Serve in goroutine 4 /usr/lib/go/src/net/http/server.go:3086 +0x5cb golang-uber-goleak-1.3.0/leaks.go000066400000000000000000000063631452613460400166200ustar00rootroot00000000000000// Copyright (c) 2017 Uber Technologies, Inc. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package goleak import ( "errors" "fmt" "go.uber.org/goleak/internal/stack" ) // TestingT is the minimal subset of testing.TB that we use. type TestingT interface { Error(...interface{}) } // filterStacks will filter any stacks excluded by the given opts. // filterStacks modifies the passed in stacks slice. func filterStacks(stacks []stack.Stack, skipID int, opts *opts) []stack.Stack { filtered := stacks[:0] for _, stack := range stacks { // Always skip the running goroutine. if stack.ID() == skipID { continue } // Run any default or user-specified filters. if opts.filter(stack) { continue } filtered = append(filtered, stack) } return filtered } // Find looks for extra goroutines, and returns a descriptive error if // any are found. func Find(options ...Option) error { cur := stack.Current().ID() opts := buildOpts(options...) if opts.cleanup != nil { return errors.New("Cleanup can only be passed to VerifyNone or VerifyTestMain") } var stacks []stack.Stack retry := true for i := 0; retry; i++ { stacks = filterStacks(stack.All(), cur, opts) if len(stacks) == 0 { return nil } retry = opts.retry(i) } return fmt.Errorf("found unexpected goroutines:\n%s", stacks) } type testHelper interface { Helper() } // VerifyNone marks the given TestingT as failed if any extra goroutines are // found by Find. This is a helper method to make it easier to integrate in // tests by doing: // // defer VerifyNone(t) // // VerifyNone is currently incompatible with t.Parallel because it cannot // associate specific goroutines with specific tests. Thus, non-leaking // goroutines from other tests running in parallel could fail this check. // If you need to run tests in parallel, use [VerifyTestMain] instead, // which will verify that no leaking goroutines exist after ALL tests finish. func VerifyNone(t TestingT, options ...Option) { opts := buildOpts(options...) var cleanup func(int) cleanup, opts.cleanup = opts.cleanup, nil if h, ok := t.(testHelper); ok { // Mark this function as a test helper, if available. h.Helper() } if err := Find(opts); err != nil { t.Error(err) } if cleanup != nil { cleanup(0) } } golang-uber-goleak-1.3.0/leaks_test.go000066400000000000000000000122031452613460400176450ustar00rootroot00000000000000// Copyright (c) 2017 Uber Technologies, Inc. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package goleak import ( "fmt" "sync" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // Ensure that testingT is a subset of testing.TB. var _ = TestingT(testing.TB(nil)) // testOptions passes a shorter max sleep time, used so tests don't wait // ~1 second in cases where we expect Find to error out. func testOptions() Option { return maxSleep(time.Millisecond) } func TestFind(t *testing.T) { t.Run("Should find no leaks by default", func(t *testing.T) { require.NoError(t, Find()) }) t.Run("Find leaks with leaked goroutine", func(t *testing.T) { bg := startBlockedG() err := Find(testOptions()) require.Error(t, err, "Should find leaks with leaked goroutine") assert.ErrorContains(t, err, "blockedG") assert.ErrorContains(t, err, "created by go.uber.org/goleak.startBlockedG") // Once we unblock the goroutine, we shouldn't have leaks. bg.unblock() require.NoError(t, Find(), "Should find no leaks by default") }) t.Run("Find can't take in Cleanup option", func(t *testing.T) { err := Find(Cleanup(func(int) { assert.Fail(t, "this should not be called") })) require.Error(t, err, "Should exit with invalid option") }) } func TestFindRetry(t *testing.T) { // for i := 0; i < 10; i++ { bg := startBlockedG() require.Error(t, Find(testOptions()), "Should find leaks with leaked goroutine") go func() { time.Sleep(time.Millisecond) bg.unblock() }() require.NoError(t, Find(), "Find should retry while background goroutine ends") } type fakeT struct { errors []string } func (ft *fakeT) Error(args ...interface{}) { ft.errors = append(ft.errors, fmt.Sprint(args...)) } func TestVerifyNone(t *testing.T) { t.Run("VerifyNone finds leaks", func(t *testing.T) { ft := &fakeT{} VerifyNone(ft) require.Empty(t, ft.errors, "Expect no errors from VerifyNone") bg := startBlockedG() VerifyNone(ft, testOptions()) require.NotEmpty(t, ft.errors, "Expect errors from VerifyNone on leaked goroutine") bg.unblock() }) t.Run("cleanup registered callback should be called", func(t *testing.T) { ft := &fakeT{} cleanupCalled := false VerifyNone(ft, Cleanup(func(c int) { assert.Equal(t, 0, c) cleanupCalled = true })) require.True(t, cleanupCalled, "expect cleanup registered callback to be called") }) } func TestIgnoreCurrent(t *testing.T) { t.Run("Should ignore current", func(t *testing.T) { defer VerifyNone(t) done := make(chan struct{}) go func() { <-done }() // We expect the above goroutine to be ignored. VerifyNone(t, IgnoreCurrent()) close(done) }) t.Run("Should detect new leaks", func(t *testing.T) { defer VerifyNone(t) // There are no leaks currently. VerifyNone(t) done1 := make(chan struct{}) done2 := make(chan struct{}) go func() { <-done1 }() err := Find() require.Error(t, err, "Expected to find background goroutine as leak") opt := IgnoreCurrent() VerifyNone(t, opt) // A second goroutine started after IgnoreCurrent is a leak go func() { <-done2 }() err = Find(opt) require.Error(t, err, "Expect second goroutine to be flagged as a leak") close(done1) close(done2) }) t.Run("Should not ignore false positive", func(t *testing.T) { defer VerifyNone(t) const goroutinesCount = 5 var wg sync.WaitGroup done := make(chan struct{}) // Spawn few goroutines before checking leaks for i := 0; i < goroutinesCount; i++ { wg.Add(1) go func() { <-done wg.Done() }() } // Store all goroutines option := IgnoreCurrent() // Free goroutines close(done) wg.Wait() // We expect the below goroutines to be founded. for i := 0; i < goroutinesCount; i++ { ch := make(chan struct{}) go func() { <-ch }() require.Error(t, Find(option), "Expect spawned goroutine to be flagged as a leak") // Free spawned goroutine close(ch) // Make sure that there are no leaks VerifyNone(t) } }) } func TestVerifyParallel(t *testing.T) { t.Run("parallel", func(t *testing.T) { t.Parallel() }) t.Run("serial", func(t *testing.T) { VerifyNone(t) }) } golang-uber-goleak-1.3.0/options.go000066400000000000000000000137721452613460400172160ustar00rootroot00000000000000// Copyright (c) 2017-2023 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package goleak import ( "strings" "time" "go.uber.org/goleak/internal/stack" ) // Option lets users specify custom verifications. type Option interface { apply(*opts) } // We retry up to 20 times if we can't find the goroutine that // we are looking for. In between each attempt, we will sleep for // a short while to let any running goroutines complete. const _defaultRetries = 20 type opts struct { filters []func(stack.Stack) bool maxRetries int maxSleep time.Duration cleanup func(int) } // implement apply so that opts struct itself can be used as // an Option. func (o *opts) apply(opts *opts) { opts.filters = o.filters opts.maxRetries = o.maxRetries opts.maxSleep = o.maxSleep opts.cleanup = o.cleanup } // optionFunc lets us easily write options without a custom type. type optionFunc func(*opts) func (f optionFunc) apply(opts *opts) { f(opts) } // IgnoreTopFunction ignores any goroutines where the specified function // is at the top of the stack. The function name should be fully qualified, // e.g., go.uber.org/goleak.IgnoreTopFunction func IgnoreTopFunction(f string) Option { return addFilter(func(s stack.Stack) bool { return s.FirstFunction() == f }) } // IgnoreAnyFunction ignores goroutines where the specified function // is present anywhere in the stack. // // The function name must be fully qualified, e.g., // // go.uber.org/goleak.IgnoreAnyFunction // // For methods, the fully qualified form looks like: // // go.uber.org/goleak.(*MyType).MyMethod func IgnoreAnyFunction(f string) Option { return addFilter(func(s stack.Stack) bool { return s.HasFunction(f) }) } // Cleanup sets up a cleanup function that will be executed at the // end of the leak check. // When passed to [VerifyTestMain], the exit code passed to cleanupFunc // will be set to the exit code of TestMain. // When passed to [VerifyNone], the exit code will be set to 0. // This cannot be passed to [Find]. func Cleanup(cleanupFunc func(exitCode int)) Option { return optionFunc(func(opts *opts) { opts.cleanup = cleanupFunc }) } // IgnoreCurrent records all current goroutines when the option is created, and ignores // them in any future Find/Verify calls. func IgnoreCurrent() Option { excludeIDSet := map[int]bool{} for _, s := range stack.All() { excludeIDSet[s.ID()] = true } return addFilter(func(s stack.Stack) bool { return excludeIDSet[s.ID()] }) } func maxSleep(d time.Duration) Option { return optionFunc(func(opts *opts) { opts.maxSleep = d }) } func addFilter(f func(stack.Stack) bool) Option { return optionFunc(func(opts *opts) { opts.filters = append(opts.filters, f) }) } func buildOpts(options ...Option) *opts { opts := &opts{ maxRetries: _defaultRetries, maxSleep: 100 * time.Millisecond, } opts.filters = append(opts.filters, isTestStack, isSyscallStack, isStdLibStack, isTraceStack, ) for _, option := range options { option.apply(opts) } return opts } func (o *opts) filter(s stack.Stack) bool { for _, filter := range o.filters { if filter(s) { return true } } return false } func (o *opts) retry(i int) bool { if i >= o.maxRetries { return false } d := time.Duration(int(time.Microsecond) << uint(i)) if d > o.maxSleep { d = o.maxSleep } time.Sleep(d) return true } // isTestStack is a default filter installed to automatically skip goroutines // that the testing package runs while the user's tests are running. func isTestStack(s stack.Stack) bool { // Until go1.7, the main goroutine ran RunTests, which started // the test in a separate goroutine and waited for that test goroutine // to end by waiting on a channel. // Since go1.7, a separate goroutine is started to wait for signals. // T.Parallel is for parallel tests, which are blocked until all serial // tests have run with T.Parallel at the top of the stack. // testing.runFuzzTests is for fuzz testing, it's blocked until the test // function with all seed corpus have run. // testing.runFuzzing is for fuzz testing, it's blocked until a failing // input is found. switch s.FirstFunction() { case "testing.RunTests", "testing.(*T).Run", "testing.(*T).Parallel", "testing.runFuzzing", "testing.runFuzzTests": // In pre1.7 and post-1.7, background goroutines started by the testing // package are blocked waiting on a channel. return strings.HasPrefix(s.State(), "chan receive") } return false } func isSyscallStack(s stack.Stack) bool { // Typically runs in the background when code uses CGo: // https://github.com/golang/go/issues/16714 return s.HasFunction("runtime.goexit") && strings.HasPrefix(s.State(), "syscall") } func isStdLibStack(s stack.Stack) bool { // Importing os/signal starts a background goroutine. // The name of the function at the top has changed between versions. if f := s.FirstFunction(); f == "os/signal.signal_recv" || f == "os/signal.loop" { return true } // Using signal.Notify will start a runtime goroutine. return s.HasFunction("runtime.ensureSigM") } golang-uber-goleak-1.3.0/options_test.go000066400000000000000000000065751452613460400202600ustar00rootroot00000000000000// Copyright (c) 2017 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package goleak import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/goleak/internal/stack" ) func TestOptionsFilters(t *testing.T) { opts := buildOpts() cur := stack.Current() all := getStableAll(t, cur) // At least one of these should be the same as current, the others should be filtered out. for _, s := range all { if s.ID() == cur.ID() { require.False(t, opts.filter(s), "Current test running function should not be filtered") } else { require.True(t, opts.filter(s), "Default goroutines should be filtered: %v", s) } } defer startBlockedG().unblock() // Now the filters should find something that doesn't match a filter. countUnfiltered := func() int { var unmatched int for _, s := range stack.All() { if s.ID() == cur.ID() { continue } if !opts.filter(s) { unmatched++ } } return unmatched } require.Equal(t, 1, countUnfiltered(), "Expected blockedG goroutine to not match any filter") // If we add an extra filter to ignore blockTill, it shouldn't match. opts = buildOpts(IgnoreTopFunction("go.uber.org/goleak.(*blockedG).block")) require.Zero(t, countUnfiltered(), "blockedG should be filtered out. running: %v", stack.All()) // If we ignore startBlockedG, that should not ignore the blockedG goroutine // because startBlockedG should be the "created by" function in the stack. opts = buildOpts(IgnoreAnyFunction("go.uber.org/goleak.startBlockedG")) require.Equal(t, 1, countUnfiltered(), "startBlockedG should not be filtered out. running: %v", stack.All()) } func TestOptionsIgnoreAnyFunction(t *testing.T) { cur := stack.Current() opts := buildOpts(IgnoreAnyFunction("go.uber.org/goleak.(*blockedG).run")) for _, s := range stack.All() { if s.ID() == cur.ID() { continue } if opts.filter(s) { continue } t.Errorf("Unexpected goroutine: %v", s) } } func TestOptionsRetry(t *testing.T) { opts := buildOpts() opts.maxRetries = 50 // initial attempt + 50 retries = 11 opts.maxSleep = time.Millisecond for i := 0; i < 50; i++ { assert.True(t, opts.retry(i), "Attempt %v/51 should allow retrying", i) } assert.False(t, opts.retry(51), "Attempt 51/51 should not allow retrying") assert.False(t, opts.retry(52), "Attempt 52/51 should not allow retrying") } golang-uber-goleak-1.3.0/signal_test.go000066400000000000000000000034241452613460400200300ustar00rootroot00000000000000// Copyright (c) 2017 Uber Technologies, Inc. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package goleak_test // Importing the os/signal package causes a goroutine to be started. import ( "os" "os/signal" "testing" "github.com/stretchr/testify/require" "go.uber.org/goleak" ) func TestNoLeaks(t *testing.T) { // Just importing the package can cause leaks. require.NoError(t, goleak.Find(), "Found leaks caused by signal import") // Register some signal handlers and ensure there's no leaks. c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) require.NoError(t, goleak.Find(), "Found leaks caused by signal.Notify") // Restore all registered signals. signal.Reset(os.Interrupt) require.NoError(t, goleak.Find(), "Found leaks caused after signal.Reset") } golang-uber-goleak-1.3.0/testmain.go000066400000000000000000000043271452613460400173430ustar00rootroot00000000000000// Copyright (c) 2017 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package goleak import ( "fmt" "io" "os" ) // Variables for stubbing in unit tests. var ( _osExit = os.Exit _osStderr io.Writer = os.Stderr ) // TestingM is the minimal subset of testing.M that we use. type TestingM interface { Run() int } // VerifyTestMain can be used in a TestMain function for package tests to // verify that there were no goroutine leaks. // To use it, your TestMain function should look like: // // func TestMain(m *testing.M) { // goleak.VerifyTestMain(m) // } // // See https://golang.org/pkg/testing/#hdr-Main for more details. // // This will run all tests as per normal, and if they were successful, look // for any goroutine leaks and fail the tests if any leaks were found. func VerifyTestMain(m TestingM, options ...Option) { exitCode := m.Run() opts := buildOpts(options...) var cleanup func(int) cleanup, opts.cleanup = opts.cleanup, nil if cleanup == nil { cleanup = _osExit } defer func() { cleanup(exitCode) }() if exitCode == 0 { if err := Find(opts); err != nil { fmt.Fprintf(_osStderr, "goleak: Errors on successful test run: %v\n", err) exitCode = 1 } } } golang-uber-goleak-1.3.0/testmain_test.go000066400000000000000000000052351452613460400204010ustar00rootroot00000000000000// Copyright (c) 2017 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package goleak import ( "bytes" "testing" "github.com/stretchr/testify/assert" ) func init() { clearOSStubs() } func clearOSStubs() { // We don't want to use the real os.Exit or os.Stderr so nil them out. // Tests MUST set them explicitly if they rely on them. _osExit = nil _osStderr = nil } type dummyTestMain int func (d dummyTestMain) Run() int { return int(d) } func osStubs() (chan int, chan string) { exitCode := make(chan int, 1) stderr := make(chan string, 1) buf := &bytes.Buffer{} _osStderr = buf _osExit = func(code int) { exitCode <- code stderr <- buf.String() buf.Reset() } return exitCode, stderr } func TestVerifyTestMain(t *testing.T) { defer clearOSStubs() exitCode, stderr := osStubs() blocked := startBlockedG() VerifyTestMain(dummyTestMain(7)) assert.Equal(t, 7, <-exitCode, "Exit code should not be modified") assert.NotContains(t, <-stderr, "goleak: Errors", "Ignore leaks on unsuccessful runs") VerifyTestMain(dummyTestMain(0)) assert.Equal(t, 1, <-exitCode, "Expect error due to leaks on successful runs") assert.Contains(t, <-stderr, "goleak: Errors", "Find leaks on successful runs") blocked.unblock() VerifyTestMain(dummyTestMain(0)) assert.Equal(t, 0, <-exitCode, "Expect no errors without leaks") assert.NotContains(t, <-stderr, "goleak: Errors", "No errors on successful run without leaks") cleanupCalled := false cleanupExitcode := 0 VerifyTestMain(dummyTestMain(3), Cleanup(func(ec int) { cleanupCalled = true cleanupExitcode = ec })) assert.True(t, cleanupCalled) assert.Equal(t, 3, cleanupExitcode) } golang-uber-goleak-1.3.0/tracestack_new.go000066400000000000000000000024341452613460400205110ustar00rootroot00000000000000// Copyright (c) 2021-2023 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. //go:build go1.16 // +build go1.16 package goleak import "go.uber.org/goleak/internal/stack" func isTraceStack(s stack.Stack) bool { return s.HasFunction("runtime.ReadTrace") } golang-uber-goleak-1.3.0/utils_test.go000066400000000000000000000046051452613460400177150ustar00rootroot00000000000000// Copyright (c) 2017 Uber Technologies, Inc. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package goleak import ( "runtime" "strings" "testing" "go.uber.org/goleak/internal/stack" ) type blockedG struct { started chan struct{} wait chan struct{} } func startBlockedG() *blockedG { bg := &blockedG{ started: make(chan struct{}), wait: make(chan struct{}), } go bg.run() <-bg.started return bg } func (bg *blockedG) run() { close(bg.started) bg.block() } func (bg *blockedG) block() { <-bg.wait } func (bg *blockedG) unblock() { close(bg.wait) } func getStableAll(t *testing.T, cur stack.Stack) []stack.Stack { all := stack.All() // There may be running goroutines that were just scheduled or finishing up // from previous tests, so reduce flakiness by waiting till no other goroutines // are runnable or running except the current goroutine. for retry := 0; true; retry++ { if !isBackgroundRunning(cur, all) { break } if retry >= 100 { t.Fatalf("background goroutines are possibly running, %v", all) } runtime.Gosched() all = stack.All() } return all } // Note: This is the same logic as in internal/stacks/stacks_test.go func isBackgroundRunning(cur stack.Stack, stacks []stack.Stack) bool { for _, s := range stacks { if cur.ID() == s.ID() { continue } if strings.Contains(s.State(), "run") { return true } } return false }