Upgrade Go tooling and strengthen static checks Update the Go and lint toolchain, expand static analysis, and keep exceptions scoped to intentional parser and stack fixtures. Lint the panic and panicweb entry points directly. Preserve their intentional stack signatures and non-ASCII identifiers with narrow annotations, and update the expected source lines for live snapshots. Validation: make fix, make verify, and make test.
diff --git a/.golangci.yml b/.golangci.yml index d0f7f56..664df9a 100644 --- a/.golangci.yml +++ b/.golangci.yml
@@ -10,22 +10,89 @@ linters: default: none enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - canonicalheader + - contextcheck + - copyloopvar + - dupl + - dupword + - durationcheck + - embeddedstructfieldcheck + - errcheck + - errchkjson + - errname + - errorlint + - exhaustive + - exptostd + - fatcontext + - forcetypeassert + - funcorder + - gocheckcompilerdirectives + - gochecksumtype + - gocritic + - godoclint + - godot + - gomoddirectives + - goprintffuncname - gosec + - gosmopolitan - govet + - grouper + - iface + - importas + - inamedparam - ineffassign - - misspell + - intrange + - iotamixing + - loggercheck + - makezero + - mirror + - modernize + - musttag + - nakedret + - nilerr + - nilnesserr + - nilnil + - noctx + - nolintlint + - nosprintfhostport + - paralleltest + - perfsprint + - prealloc + - predeclared + - reassign + - recvcheck + - revive + - sloglint + - sqlclosecheck - staticcheck + - tparallel + - unconvert + - unparam - unused + - usestdlibvars + - usetesting + - wastedassign + - whitespace exclusions: rules: + # This fixture deliberately exercises Unicode names. + - path: ^cmd/panic/internal/utf8/utf8\.go$ + linters: [asciicheck] + # These standalone examples intentionally omit error handling and context plumbing, + # and include fixed source line numbers in their expected stack output. + - path: ^stack/example_test\.go$ + linters: [errcheck, errorlint, gocritic, noctx, revive, whitespace] + # Repeated identifiers are valid source in the parser regression fixture. + - path: ^stack/source_test\.go$ + text: Duplicate words \(z,\) + linters: [dupword] # The standalone gosec CI command scanned production sources only. - path: _test\.go$ linters: [gosec] - # The restart resolves os.Executable and forwards argv without invoking a shell. - - path: ^cmd/panic/main\.go$ - source: 'exec\.Command\(exe, os\.Args\[1:\]\.\.\.\)' - text: G(204|702) - linters: [gosec] settings: staticcheck: checks: ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022", "-QF*"]
diff --git a/Makefile b/Makefile index 0cb0215..7a2d749 100644 --- a/Makefile +++ b/Makefile
@@ -31,7 +31,6 @@ @go tool addlicense -ignore '.venv/**' -ignore '.ruff_cache/**' -ignore 'third_party/**' -ignore 'vendor/**' -check . @uv run --locked python scripts/lint_binaries.py @uv run --locked python scripts/update_agents_file_index.py --check - @git ls-files -z -- . ':(exclude)go.mod' ':(exclude)go.sum' | xargs -0 go run github.com/client9/misspell/cmd/misspell@v0.3.4 -error @go mod tidy -diff test:
diff --git a/cmd/panic/main.go b/cmd/panic/main.go index 8eff9b7..2bfdf19 100644 --- a/cmd/panic/main.go +++ b/cmd/panic/main.go
@@ -28,6 +28,8 @@ // below. That's it! import ( + "context" + "errors" "fmt" "io" "os" @@ -40,7 +42,7 @@ "github.com/maruel/panicparse/v2/cmd/panic/internal" correct "github.com/maruel/panicparse/v2/cmd/panic/internal/incorrect" - ùtf8 "github.com/maruel/panicparse/v2/cmd/panic/internal/utf8" + ùtf8 "github.com/maruel/panicparse/v2/cmd/panic/internal/utf8" //nolint:asciicheck // The fixture tests non-ASCII identifiers. ) func main() { @@ -75,7 +77,7 @@ f.f() os.Exit(3) } - fmt.Fprintf(stdErr, "unknown panic style %q\n", n) + _, _ = fmt.Fprintf(stdErr, "unknown panic style %q\n", n) os.Exit(1) } } @@ -104,7 +106,7 @@ panic(a) } -func panicArgsElided(a, b, c, d, e, f, g, h, i, j, k int) { +func panicArgsElided(a, _, _, _, _, _, _, _, _, _, _ int) { panic(a) } @@ -120,24 +122,24 @@ help := "'panic %s' can only be used when built with the race detector.\n" + "To build, use:\n" + " go install -race github.com/maruel/panicparse/v2/cmd/panic\n" - fmt.Fprintf(stdErr, help, name) + _, _ = fmt.Fprintf(stdErr, help, name) } func rerunWithFastCrash() { if os.Getenv("GORACE") != "log_path=stderr halt_on_error=1" { exe, err := os.Executable() if err != nil { - fmt.Fprintln(stdErr, err) + _, _ = fmt.Fprintln(stdErr, err) os.Exit(1) } - c := exec.Command(exe, os.Args[1:]...) + c := exec.CommandContext(context.Background(), exe, os.Args[1:]...) // #nosec G204 G702 -- re-executes this binary without a shell. c.Env = append(os.Environ(), "GORACE=log_path=stderr halt_on_error=1") c.Stderr = os.Stderr if err := c.Run(); err != nil { - if e, ok := err.(*exec.ExitError); ok { + if e, ok := errors.AsType[*exec.ExitError](err); ok { os.Exit(e.ExitCode()) } - fmt.Fprintln(stdErr, err) + _, _ = fmt.Fprintln(stdErr, err) os.Exit(1) } os.Exit(0) @@ -177,7 +179,7 @@ } //go:noinline -func panicChanStruct(x chan struct{}) { +func panicChanStruct(_ chan struct{}) { panic("test") } @@ -285,7 +287,7 @@ "start 100 goroutines before panicking", func() { var wg sync.WaitGroup - for i := 0; i < 100; i++ { + for range 100 { wg.Add(1) go func() { wg.Done() @@ -301,9 +303,9 @@ "start 100 goroutines with different pointers before panicking", func() { var wg sync.WaitGroup - for i := 0; i < 100; i++ { + for range 100 { wg.Add(1) - go func(b *int) { + go func(_ *int) { wg.Done() time.Sleep(time.Minute) }(new(int)) @@ -466,7 +468,7 @@ } sort.Strings(names) for _, n := range names { - fmt.Fprintf(stdErr, "- %-*s %s\n", m, n, types[n].desc) + _, _ = fmt.Fprintf(stdErr, "- %-*s %s\n", m, n, types[n].desc) } }
diff --git a/cmd/panic/main_test.go b/cmd/panic/main_test.go index 475f773..b01a132 100644 --- a/cmd/panic/main_test.go +++ b/cmd/panic/main_test.go
@@ -19,9 +19,9 @@ t.Parallel() if !testing.Verbose() { stdErr = io.Discard - defer func() { + t.Cleanup(func() { stdErr = os.Stderr - }() + }) } for name, l := range types { if name == "simple" { @@ -44,7 +44,6 @@ continue } - l := l t.Run(name, func(t *testing.T) { t.Parallel() defer func() { @@ -59,6 +58,7 @@ // TestRerunWithFastCrash verifies restart uses the executable rather than argv[0]. func TestRerunWithFastCrash(t *testing.T) { + t.Parallel() if os.Getenv("PANICPARSE_REEXEC_TEST") == "1" { if os.Getenv("GORACE") == "log_path=stderr halt_on_error=1" { fmt.Fprintln(os.Stderr, "trusted restart") @@ -72,7 +72,7 @@ if err != nil { t.Fatal(err) } - cmd := exec.Command(exe, "-test.run=^TestRerunWithFastCrash$") + cmd := exec.CommandContext(t.Context(), exe, "-test.run=^TestRerunWithFastCrash$") cmd.Env = append(os.Environ(), "PANICPARSE_REEXEC_TEST=1", "GORACE=") out, err := cmd.CombinedOutput() if err != nil {
diff --git a/cmd/panicweb/internal/internal.go b/cmd/panicweb/internal/internal.go index 53d51a9..54df288 100644 --- a/cmd/panicweb/internal/internal.go +++ b/cmd/panicweb/internal/internal.go
@@ -19,7 +19,8 @@ // goroutine. func GetAsync(url string) { /* #nosec G107 */ - resp, err := http.Get(url) + // The response body is transferred to and closed by the goroutine below. + resp, err := http.Get(url) //nolint:bodyclose,noctx // Ownership moves to the goroutine; this fixture has no context. if err != nil { log.Fatalf("get %s: %v", url, err) } @@ -34,22 +35,22 @@ } // URL1Handler is a http.HandlerFunc that hangs. -func URL1Handler(w http.ResponseWriter, req *http.Request) { +func URL1Handler(w http.ResponseWriter, _ *http.Request) { // Respond the HTTP header to unblock the http.Get() function. w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Length", "100000") - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) b := [4096]byte{} _, _ = w.Write(b[:]) <-Unblock } // URL2Handler is a http.HandlerFunc that hangs. -func URL2Handler(w http.ResponseWriter, req *http.Request) { +func URL2Handler(w http.ResponseWriter, _ *http.Request) { // Respond the HTTP header to unblock the http.Get() function. w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Length", "100000") - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) b := [4096]byte{} _, _ = w.Write(b[:]) <-Unblock
diff --git a/cmd/panicweb/main.go b/cmd/panicweb/main.go index cda3bf6..2be0e69 100644 --- a/cmd/panicweb/main.go +++ b/cmd/panicweb/main.go
@@ -14,6 +14,7 @@ package main import ( + "context" "flag" "fmt" "log" @@ -25,6 +26,7 @@ _ "net/http/pprof" "os" "runtime" + "strconv" "strings" "sync" "time" @@ -57,7 +59,7 @@ if !*allowremote { addr = "localhost" + addr } - ln, err := net.Listen("tcp", addr) + ln, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", addr) if err != nil { log.Fatalf("Failed to listen on localhost: %v", err) } @@ -104,7 +106,7 @@ } else { http.HandleFunc("/panicparse", webstack.SnapshotHandler) } - http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { + http.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write(rootPage) }) @@ -112,7 +114,7 @@ Handler: http.DefaultServeMux, ReadHeaderTimeout: 2 * time.Second, } - go srv.Serve(ln) + go srv.Serve(ln) //nolint:errcheck // Preserve the server goroutine's stack signature. // Start many clients. a := ln.Addr() @@ -120,14 +122,14 @@ if *allowremote { if h, err := os.Hostname(); err == nil { if t, ok := a.(*net.TCPAddr); ok { - url = fmt.Sprintf("http://%s:%d/", h, t.Port) + url = "http://" + net.JoinHostPort(h, strconv.Itoa(t.Port)) + "/" } } } - for i := 0; i < 10; i++ { + for range 10 { internal.GetAsync(url + "url1") } - for i := 0; i < 3; i++ { + for range 3 { internal.GetAsync(url + "url2") } @@ -143,7 +145,7 @@ // that is both versioned and can be hacked to call back user code. w := writeHang{hung: make(chan struct{}), unblock: make(chan struct{})} v := colorable.NewNonColorable(&w) - go v.Write([]byte("foo bar")) + go v.Write([]byte("foo bar")) //nolint:errcheck // Preserve the writer goroutine's stack signature. <-w.hung if *sleep { @@ -159,7 +161,7 @@ unblock chan struct{} } -func (w *writeHang) Write(b []byte) (int, error) { +func (w *writeHang) Write(_ []byte) (int, error) { runtime.LockOSThread() w.hung <- struct{}{} <-w.unblock
diff --git a/go.mod b/go.mod index e2e6784..f9aa7c3 100644 --- a/go.mod +++ b/go.mod
@@ -1,10 +1,10 @@ module github.com/maruel/panicparse/v2 -go 1.27.0 +go 1.27.3 require ( github.com/google/go-cmp v0.7.0 - github.com/mattn/go-colorable v0.1.15 + github.com/mattn/go-colorable v0.1.16 github.com/mattn/go-isatty v0.0.22 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d golang.org/x/sys v0.49.0 @@ -47,7 +47,7 @@ github.com/bmatcuk/doublestar/v4 v4.0.2 // indirect github.com/bombsimon/wsl/v4 v4.7.0 // indirect github.com/bombsimon/wsl/v5 v5.9.0 // indirect - github.com/breml/bidichk v0.3.3 // indirect + github.com/breml/bidichk v0.3.4 // indirect github.com/breml/errchkjson v0.4.1 // indirect github.com/butuzov/ireturn v0.4.1 // indirect github.com/butuzov/mirror v1.3.3 // indirect @@ -66,7 +66,7 @@ github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/curioswitch/go-reassign v0.3.0 // indirect github.com/daixiang0/gci v0.13.7 // indirect - github.com/dave/dst v0.27.3 // indirect + github.com/dave/dst v0.28.0 // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect github.com/dlclark/regexp2/v2 v2.2.1 // indirect github.com/ettle/strcase v0.2.0 // indirect @@ -91,16 +91,16 @@ github.com/gofrs/flock v0.13.1 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golangci/asciicheck v0.5.0 // indirect - github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6 // indirect + github.com/golangci/canonicalheader v0.0.0-20260928145534-e18e5c5a078f // indirect github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 // indirect github.com/golangci/go-printf-func-name v0.1.1 // indirect github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792 // indirect - github.com/golangci/golangci-lint/v2 v2.14.0 // indirect - github.com/golangci/golines v0.15.0 // indirect + github.com/golangci/golangci-lint/v2 v2.15.0 // indirect + github.com/golangci/golines v0.16.0 // indirect github.com/golangci/misspell v0.8.0 // indirect github.com/golangci/plugin-module-register v0.1.2 // indirect github.com/golangci/revgrep v0.8.0 // indirect - github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba // indirect + github.com/golangci/rowserrcheck v0.0.0-20261005233414-e0740d52dee8 // indirect github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect github.com/google/addlicense v1.2.0 // indirect @@ -119,10 +119,10 @@ github.com/jjti/go-spancheck v0.6.5 // indirect github.com/julz/importas v0.2.0 // indirect github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect - github.com/kisielk/errcheck v1.20.0 // indirect + github.com/kisielk/errcheck v1.30.0 // indirect github.com/kkHAIKE/contextcheck v1.1.6 // indirect github.com/kulti/thelper v0.7.1 // indirect - github.com/kunwardeep/paralleltest v1.0.15 // indirect + github.com/kunwardeep/paralleltest v1.0.16 // indirect github.com/ldez/exptostd v0.4.5 // indirect github.com/ldez/gomoddirectives v0.10.0 // indirect github.com/ldez/grignotin v0.10.1 // indirect @@ -140,14 +140,14 @@ github.com/matoous/godox v1.1.0 // indirect github.com/mattn/go-runewidth v0.0.24 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect - github.com/mgechev/revive v1.17.0 // indirect + github.com/mgechev/revive v1.17.1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moricho/tparallel v0.3.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/nakabonne/nestif v0.3.1 // indirect - github.com/nishanths/exhaustive v0.13.0 // indirect - github.com/nishanths/predeclared v0.2.2 // indirect + github.com/nishanths/exhaustive v0.15.0 // indirect + github.com/nishanths/predeclared v0.3.0 // indirect github.com/nunnatsa/ginkgolinter v0.24.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.4.3 // indirect @@ -188,13 +188,13 @@ github.com/subosito/gotenv v1.4.1 // indirect github.com/tetafro/godot v1.5.6 // indirect github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c // indirect - github.com/timonwong/loggercheck v0.12.0 // indirect - github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect + github.com/timonwong/loggercheck v0.13.0 // indirect + github.com/tomarrell/wrapcheck/v2 v2.13.0 // indirect github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect github.com/ultraware/funlen v0.2.0 // indirect github.com/ultraware/whitespace v0.2.0 // indirect - github.com/uudashr/gocognit v1.2.1 // indirect - github.com/uudashr/iface v1.5.1 // indirect + github.com/uudashr/gocognit v1.3.0 // indirect + github.com/uudashr/iface v1.5.3 // indirect github.com/xen0n/gosmopolitan v1.3.0 // indirect github.com/xo/terminfo v1.0.0 // indirect github.com/yagipy/maintidx v1.0.0 // indirect @@ -217,7 +217,7 @@ gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - honnef.co/go/tools v0.8.1 // indirect + honnef.co/go/tools v0.8.2 // indirect mvdan.cc/gofumpt v0.12.0 // indirect mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8 // indirect )
diff --git a/go.sum b/go.sum index 8c9b9ee..a7e368a 100644 --- a/go.sum +++ b/go.sum
@@ -116,8 +116,8 @@ github.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg= github.com/bombsimon/wsl/v5 v5.9.0 h1:WCrgZ7RQnZO5oEwbVTlYgBdU3wL294kR1BSWV8vTfsU= github.com/bombsimon/wsl/v5 v5.9.0/go.mod h1:kjo4HiAV5FDkHC8/uzJq9mBffEEd6WT/nvN7DoMovDM= -github.com/breml/bidichk v0.3.3 h1:WSM67ztRusf1sMoqH6/c4OBCUlRVTKq+CbSeo0R17sE= -github.com/breml/bidichk v0.3.3/go.mod h1:ISbsut8OnjB367j5NseXEGGgO/th206dVa427kR8YTE= +github.com/breml/bidichk v0.3.4 h1:70G6w80G6GDZTUURHTC5gBKVjMLMw2736CzXquSf5Ek= +github.com/breml/bidichk v0.3.4/go.mod h1:GOHprnh4i0V7HrvWjtbvbpicTUcqoYt9zYESKcJkk4M= github.com/breml/errchkjson v0.4.1 h1:keFSS8D7A2T0haP9kzZTi7o26r7kE3vymjZNeNDRDwg= github.com/breml/errchkjson v0.4.1/go.mod h1:a23OvR6Qvcl7DG/Z4o0el6BRAjKnaReoPQFciAl9U3s= github.com/butuzov/ireturn v0.4.1 h1:vWb3NO4t77iku/sjCQ/2pHTQeOmxEhjIriJqRLg1Y+I= @@ -163,8 +163,8 @@ github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= github.com/daixiang0/gci v0.13.7 h1:+0bG5eK9vlI08J+J/NWGbWPTNiXPG4WhNLJOkSxWITQ= github.com/daixiang0/gci v0.13.7/go.mod h1:812WVN6JLFY9S6Tv76twqmNqevN0pa3SX3nih0brVzQ= -github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY= -github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= +github.com/dave/dst v0.28.0 h1:EIEuRC5XmVKuZF2OeyMahXX9ARVt6Xxr8vI0WtIY7K8= +github.com/dave/dst v0.28.0/go.mod h1:C4mfimBeSHfsgskhR/mUYSE05s2ih+PW6g9FP3c49tE= github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -275,26 +275,26 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0= github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ= -github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6 h1:fVLolA3dG6s0brGetsiDAtcnpMSwVa2LqXJEw/9RJG4= -github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6/go.mod h1:1xo+NFW5S+bEf2DKXhaxuzvcDN5AR6o/KMwvqC5Mkpo= +github.com/golangci/canonicalheader v0.0.0-20260928145534-e18e5c5a078f h1:pOnwRNeWLPkADfkgqKcCP7RPQAS9OIzffaBaktRsKnk= +github.com/golangci/canonicalheader v0.0.0-20260928145534-e18e5c5a078f/go.mod h1:aXo+AVSX0pC3TRo2wAfR92YwBKfG6C2eal+omjKXwYw= github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 h1:CbTB8KpqnViI6lIXxp03Oclc4VFHi3K4BWC1TacsZ+A= github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E= github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarogrvjO9AfiW3B4U= github.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss= github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792 h1:WL8YKrt3UbOBqSRU7GpP5BTtQTMWtVtj+mfPijgZeIg= github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792/go.mod h1:te5hX0dW4C5r6YbXs+6ysNr8Q5UTmdIqGbb+mlFiYmA= -github.com/golangci/golangci-lint/v2 v2.14.0 h1:ot8QffRa4LzAAEgtvNYrVs9esxQ7xoTwoAv6uhp4ngA= -github.com/golangci/golangci-lint/v2 v2.14.0/go.mod h1:amHimCzNDP1GfVno3hIiNssjvqXmuW+CPZAHOdR4cIc= -github.com/golangci/golines v0.15.0 h1:Qnph25g8Y1c5fdo1X7GaRDGgnMHgnxh4Gk4VfPTtRx0= -github.com/golangci/golines v0.15.0/go.mod h1:AZjXd23tbHMpowhtnGlj9KCNsysj72aeZVVHnVcZx10= +github.com/golangci/golangci-lint/v2 v2.15.0 h1:w94zf8HAYI7hm3164kwKsvZRuqXvY+YxeFoTbu1OLg8= +github.com/golangci/golangci-lint/v2 v2.15.0/go.mod h1:/FRI304IxqDVhX2o3ZZ0qp8Vvz8egJdzCY5A4GjvmTU= +github.com/golangci/golines v0.16.0 h1:xKdwzRvwzLd7V131B/dv90vUiEoTA0vQybdeqperPOg= +github.com/golangci/golines v0.16.0/go.mod h1:mCMgRQHiCvuALJOeaNtylu09zbzvS9Ka01dfvLLJios= github.com/golangci/misspell v0.8.0 h1:qvxQhiE2/5z+BVRo1kwYA8yGz+lOlu5Jfvtx2b04Jbg= github.com/golangci/misspell v0.8.0/go.mod h1:WZyyI2P3hxPY2UVHs3cS8YcllAeyfquQcKfdeE9AFVg= github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg= github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw= github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s= github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= -github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba h1:lqtcnSMDuuJdu/LrKWi5RJzpSNLOJXYe/nzQutTI5kg= -github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba/go.mod h1:sCBNcpRmhJCtbFGz49+IM3ETTFf7QdJ30AeYCd43NKk= +github.com/golangci/rowserrcheck v0.0.0-20261005233414-e0740d52dee8 h1:oN042y1yEcpldrqY81puyNA4ZjQZJzt0U/t7HyYrMic= +github.com/golangci/rowserrcheck v0.0.0-20261005233414-e0740d52dee8/go.mod h1:T1Bg58Sk3N352aBbPLJPpHykFWxEEwV3o4/Enn+PPU8= github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2bRA5htgAG9r7s3tHsfjIhN98WshBTJ9jM= github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s= github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM= @@ -380,8 +380,8 @@ github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY= github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0= github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY= -github.com/kisielk/errcheck v1.20.0 h1:9rwHBNKzd4wkDWcROy3DvFGNqEPlkxBg305rvk7HabI= -github.com/kisielk/errcheck v1.20.0/go.mod h1:O+f80MKNwX8Oor2jwgpeQ9An7uJm+hRSgT+h22knRJU= +github.com/kisielk/errcheck v1.30.0 h1:ZIxNDAfAd4larXHTceoZP/dYPAB90dHg1OYhA2sVh5k= +github.com/kisielk/errcheck v1.30.0/go.mod h1:uiCAInmFuyOje4VDdbQSP9mgqf5gORYSXrDf19XilsI= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE= github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= @@ -397,8 +397,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kulti/thelper v0.7.1 h1:fI8QITAoFVLx+y+vSyuLBP+rcVIB8jKooNSCT2EiI98= github.com/kulti/thelper v0.7.1/go.mod h1:NsMjfQEy6sd+9Kfw8kCP61W1I0nerGSYSFnGaxQkcbs= -github.com/kunwardeep/paralleltest v1.0.15 h1:ZMk4Qt306tHIgKISHWFJAO1IDQJLc6uDyJMLyncOb6w= -github.com/kunwardeep/paralleltest v1.0.15/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk= +github.com/kunwardeep/paralleltest v1.0.16 h1:I0gQkLNgfigjpxWAGhx7PYj3VpswzfGrSV/715t6g+o= +github.com/kunwardeep/paralleltest v1.0.16/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk= github.com/ldez/exptostd v0.4.5 h1:kv2ZGUVI6VwRfp/+bcQ6Nbx0ghFWcGIKInkG/oFn1aQ= github.com/ldez/exptostd v0.4.5/go.mod h1:QRjHRMXJrCTIm9WxVNH6VW7oN7KrGSht69bIRwvdFsM= github.com/ldez/gomoddirectives v0.10.0 h1:Zb23rfMMkG21B6pZ9OfESzemByP5Qr7YvWcXQZE/S3Q= @@ -431,16 +431,16 @@ github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= -github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= -github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-colorable v0.1.16 h1:+2YjeAe7xP1OQbZW1jERPWWCDLlNqFNCvjiqGiAt84A= +github.com/mattn/go-colorable v0.1.16/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mgechev/revive v1.17.0 h1:hwTTVC3svSv9X2FA2hiYT17Uws+lfxDWhPxzBZZCREg= -github.com/mgechev/revive v1.17.0/go.mod h1:uiVNUF8ruVh4WHm/aPs7kKEeuo+m6oO3tuOfniyzP/g= +github.com/mgechev/revive v1.17.1 h1:z8hV0L3uJ6yrwOMnJbkc5qBJpxXa2AxYKFKERGJyA4s= +github.com/mgechev/revive v1.17.1/go.mod h1:FRd64pJwWSEE6j8ESoaX0PixPzjh2rpNSfckbEu3rIE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -460,10 +460,10 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= -github.com/nishanths/exhaustive v0.13.0 h1:Rof0axOSNQaaPaPmLK5RxcRrJV2Vcv+vfUmuRy+2EAM= -github.com/nishanths/exhaustive v0.13.0/go.mod h1:3/adQjJgvD/UFHHSYBjpK9Wbz8DOvf0FBtzx+zI0Rlo= -github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= -github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= +github.com/nishanths/exhaustive v0.15.0 h1:uFjiO/WvrwQG+ojSZ+vtl/jAFbqA2oAtfpUWK/iKQ8M= +github.com/nishanths/exhaustive v0.15.0/go.mod h1:2by9rYmg9FnsnLQC4Ixig4fQaWxRsY7RluxJuAu9sa0= +github.com/nishanths/predeclared v0.3.0 h1:W3i3JK/DZS5h7QwsEIyU8ler4Z2vwEwQSxNrZzhnlfk= +github.com/nishanths/predeclared v0.3.0/go.mod h1:gN4uryPExDME/zmZdqkKIPSAqcfprj316ZDI6UTqz70= github.com/nunnatsa/ginkgolinter v0.24.0 h1:Mp0EagluLFP98JatP6nqp/gGEoljNG97uf9AcxcBVy8= github.com/nunnatsa/ginkgolinter v0.24.0/go.mod h1:2ZMRuzX6+3XXyY6UZOwb6n+MCocVGbkIsDBC4vuWz5c= github.com/onsi/ginkgo/v2 v2.32.1 h1:6tlvcDm/3sE8lGJbZ4+d4mO3RLy24/tQWOFzVSQNIfw= @@ -591,20 +591,20 @@ github.com/tetafro/godot v1.5.6/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU= github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c h1:d/FUP8GD4QiY/rdFzZmBu3KqL9eBNVzMu7W/5pGkaMM= github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c/go.mod h1:sDHLK7rb/59v/ZxZ7KtymgcoxuUMxjXq8gtu9VMOK8M= -github.com/timonwong/loggercheck v0.12.0 h1:7Cb9XGLJZSWYNjgL459DIhpSiEHupD+MLQ6vaN2Pl3g= -github.com/timonwong/loggercheck v0.12.0/go.mod h1:Wa6Y+SJHQR01mViZlLLY5GAZVOWOLNMbkXddMjFWQCs= -github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is= -github.com/tomarrell/wrapcheck/v2 v2.12.0/go.mod h1:AQhQuZd0p7b6rfW+vUwHm5OMCGgp63moQ9Qr/0BpIWo= +github.com/timonwong/loggercheck v0.13.0 h1:6amIOmLLPFYt24rXUbNWzcJeNGuJc+3c31tZtO85VHs= +github.com/timonwong/loggercheck v0.13.0/go.mod h1:thn3allpox0fI5JeUWTCwiwp45peQxCZZHkHuA1Oc6I= +github.com/tomarrell/wrapcheck/v2 v2.13.0 h1:jYbLNs8PDkzGCY6d5LUzJdY7Bq//CTlb2HxZGV5z0JY= +github.com/tomarrell/wrapcheck/v2 v2.13.0/go.mod h1:AQhQuZd0p7b6rfW+vUwHm5OMCGgp63moQ9Qr/0BpIWo= github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI= github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA= github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g= github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= -github.com/uudashr/gocognit v1.2.1 h1:CSJynt5txTnORn/DkhiB4mZjwPuifyASC8/6Q0I/QS4= -github.com/uudashr/gocognit v1.2.1/go.mod h1:acaubQc6xYlXFEMb9nWX2dYBzJ/bIjEkc1zzvyIZg5Q= -github.com/uudashr/iface v1.5.1 h1:BS3yrgaT55s3dAtJoxuTnsZNbsejq/mVuzp49YDyfBA= -github.com/uudashr/iface v1.5.1/go.mod h1:5UWoT6SvTdTww/KToRj6clO+n4Kg3/NrAQvgOB+5Ggw= +github.com/uudashr/gocognit v1.3.0 h1:Xdw5JwAoYmX3WAB+wsXY3nxEwdw7+MWAnM0wCAAv7A4= +github.com/uudashr/gocognit v1.3.0/go.mod h1:acaubQc6xYlXFEMb9nWX2dYBzJ/bIjEkc1zzvyIZg5Q= +github.com/uudashr/iface v1.5.3 h1:N61bzr11L7BHP6Rct+C6zQePHjSTMeyVBq+s03uB/Ds= +github.com/uudashr/iface v1.5.3/go.mod h1:5UWoT6SvTdTww/KToRj6clO+n4Kg3/NrAQvgOB+5Ggw= github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM= github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4= github.com/xo/terminfo v1.0.0 h1:2ZpYzqWzyyytjk3TP6aJVDhkMAkc99/1xKQdA3TDTBY= @@ -972,8 +972,8 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.8.1 h1:+JKf3xJ1ni4CwrhVg4/pqsfPGP6vNAXcKbMXJodYx3w= -honnef.co/go/tools v0.8.1/go.mod h1:XA+OnlRA9EDh/ukGvXMNSZNKGwFQJ+5dER0ioUkOxks= +honnef.co/go/tools v0.8.2 h1:+jPbCzuYxYrcoOvCecYUFDj+XJQRmzIhJmlbpps21Zk= +honnef.co/go/tools v0.8.2/go.mod h1:xG1Q4n+mg3bKZrO6RWFA9jFlkqplflmDRHGeuexiLDs= mvdan.cc/gofumpt v0.12.0 h1:1Lbudkz2kpM9Cjz2pL4M19u7q+GaEhCTNf7N9mfpcho= mvdan.cc/gofumpt v0.12.0/go.mod h1:SmBHHrljiZu/uoypeKup3rFzP6eoC9UwCp2iH5E3jZA= mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8 h1:Re1NRyLpiAt9kB+ImaaoapwWiQXrKwER4tY8fLOkDew=
diff --git a/internal/internaltest/internaltest.go b/internal/internaltest/internaltest.go index 4378263..f42e0a6 100644 --- a/internal/internaltest/internaltest.go +++ b/internal/internaltest/internaltest.go
@@ -4,6 +4,7 @@ // Compile and run panic fixtures and retrieve recorded goroutine snapshots. +// Package internaltest builds the panic fixtures used by integration tests. package internaltest import ( @@ -60,9 +61,7 @@ }() prace := build("panic", true) - if prace == "" { - // Race detector is not supported on this platform. - } else { + if prace != "" { defer func() { if err := os.Remove(prace); err != nil { panic(err) @@ -89,7 +88,7 @@ p = prace } if panicOutputs[cmd] = execRun(p, cmd); len(panicOutputs[cmd]) == 0 { - panic(fmt.Sprintf("no output for %s", cmd)) + panic("no output for " + cmd) } } }) @@ -170,7 +169,8 @@ args = append(args, "-race") } /* #nosec G204 */ - c := exec.Command("go", append(args, in)...) + // The helper API has no caller context and waits for compilation. + c := exec.Command("go", append(args, in)...) //nolint:noctx // Synchronous fixture helper. c.Dir = cwd if out, err := c.CombinedOutput(); err != nil { if race { @@ -192,7 +192,8 @@ // design. func execRun(cmd ...string) []byte { /* #nosec G204 */ - c := exec.Command(cmd[0], cmd[1:]...) + // The helper API has no caller context and waits for the fixture. + c := exec.Command(cmd[0], cmd[1:]...) //nolint:noctx // Synchronous fixture helper. c.Env = append(os.Environ(), "GOTRACEBACK=all") out, _ := c.CombinedOutput() return out
diff --git a/internal/main.go b/internal/main.go index cb90667..e20dc24 100644 --- a/internal/main.go +++ b/internal/main.go
@@ -106,7 +106,7 @@ } type toHTMLer interface { - ToHTML(io.Writer, template.HTML) error + ToHTML(w io.Writer, header template.HTML) error } func toHTML(h toHTMLer, p string, needsEnv bool) error { @@ -126,7 +126,7 @@ return err } -func processInner(out io.Writer, p *Palette, s stack.Similarity, pf pathFormat, html string, filter, match *regexp.Regexp, c *stack.Snapshot, first bool) error { +func processInner(out io.Writer, p *Palette, s stack.Similarity, pf pathFormat, html string, filter, match *regexp.Regexp, c *stack.Snapshot, _ bool) error { log.Printf("GOROOT=%s", c.RemoteGOROOT) log.Printf("GOPATH=%s", c.RemoteGOPATHs) needsEnv := len(c.Goroutines) == 1 && showBanner() @@ -175,7 +175,7 @@ err = err1 } } - if err == io.EOF { + if errors.Is(err, io.EOF) { return nil } // Parts of the input will be lost. @@ -216,27 +216,27 @@ } else { out = colorable.NewColorableStderr() } - fmt.Fprintf(out, "Usage of %s:\n", os.Args[0]) + _, _ = fmt.Fprintf(out, "Usage of %s:\n", os.Args[0]) flag.CommandLine.SetOutput(out) flag.CommandLine.PrintDefaults() - fmt.Fprintf(out, "\nLegend:\n") - fmt.Fprintf(out, " Type Exported Private\n") - fmt.Fprintf(out, " main %smain.Foo()%s %smain.foo()%s\n", + _, _ = fmt.Fprintf(out, "\nLegend:\n") + _, _ = fmt.Fprintf(out, " Type Exported Private\n") + _, _ = fmt.Fprintf(out, " main %smain.Foo()%s %smain.foo()%s\n", p.funcColor(stack.LocationUnknown, true, false), p.EOLReset, p.funcColor(stack.LocationUnknown, true, true), p.EOLReset) - fmt.Fprintf(out, " <unknown> %spkg.Foo()%s %spkg.foo()%s\n", + _, _ = fmt.Fprintf(out, " <unknown> %spkg.Foo()%s %spkg.foo()%s\n", p.funcColor(stack.LocationUnknown, false, false), p.EOLReset, p.funcColor(stack.LocationUnknown, false, true), p.EOLReset) - fmt.Fprintf(out, " go.mod %spkg.Foo()%s %spkg.foo()%s\n", + _, _ = fmt.Fprintf(out, " go.mod %spkg.Foo()%s %spkg.foo()%s\n", p.funcColor(stack.GoMod, false, false), p.EOLReset, p.funcColor(stack.GoMod, false, true), p.EOLReset) - fmt.Fprintf(out, " $GOPATH/src %spkg.Foo()%s %spkg.foo()%s\n", + _, _ = fmt.Fprintf(out, " $GOPATH/src %spkg.Foo()%s %spkg.foo()%s\n", p.funcColor(stack.GOPATH, false, false), p.EOLReset, p.funcColor(stack.GOPATH, false, true), p.EOLReset) - fmt.Fprintf(out, " $GOPATH/pkg/mod %spkg.Foo()%s %spkg.foo()%s\n", + _, _ = fmt.Fprintf(out, " $GOPATH/pkg/mod %spkg.Foo()%s %spkg.foo()%s\n", p.funcColor(stack.GoPkg, false, false), p.EOLReset, p.funcColor(stack.GoPkg, false, true), p.EOLReset) - fmt.Fprintf(out, " $GOROOT/src %spkg.Foo()%s %spkg.Foo()%s\n", + _, _ = fmt.Fprintf(out, " $GOROOT/src %spkg.Foo()%s %spkg.Foo()%s\n", p.funcColor(stack.Stdlib, false, false), p.EOLReset, p.funcColor(stack.Stdlib, false, true), p.EOLReset) } @@ -297,7 +297,11 @@ return fmt.Errorf("did you mean to specify a valid stack dump file name? %w", err) } /* #nosec G307 */ - defer in.Close() + defer func() { + if err := in.Close(); err != nil { + log.Printf("close input: %v", err) + } + }() default: return errors.New("pipe from stdin or specify a single file")
diff --git a/internal/main_test.go b/internal/main_test.go index 3c82202..766e32c 100644 --- a/internal/main_test.go +++ b/internal/main_test.go
@@ -43,7 +43,7 @@ palette: testPalette, simil: stack.AnyPointer, path: basePath, - want: "GOTRACEBACK=all\npanic: simple\n\nC1: runningA\n Emain Fmain.go:73 GmainR()A\n\n", + want: "GOTRACEBACK=all\npanic: simple\n\nC1: runningA\n Emain Fmain.go:75 GmainR()A\n\n", }, { name: "FullPath", @@ -51,14 +51,14 @@ simil: stack.AnyValue, path: fullPath, // "/" is used even on Windows. - want: fmt.Sprintf("GOTRACEBACK=all\npanic: simple\n\nC1: runningA\n Emain F%s:73 GmainR()A\n\n", strings.Replace(filepath.Join(filepath.Dir(d), "cmd", "panic", "main.go"), "\\", "/", -1)), + want: fmt.Sprintf("GOTRACEBACK=all\npanic: simple\n\nC1: runningA\n Emain F%s:75 GmainR()A\n\n", strings.ReplaceAll(filepath.Join(filepath.Dir(d), "cmd", "panic", "main.go"), "\\", "/")), }, { name: "NoColor", palette: &Palette{}, simil: stack.AnyValue, path: basePath, - want: "GOTRACEBACK=all\npanic: simple\n\n1: running\n main main.go:73 main()\n\n", + want: "GOTRACEBACK=all\npanic: simple\n\n1: running\n main main.go:75 main()\n\n", }, { name: "Match", @@ -74,11 +74,10 @@ simil: stack.AnyValue, path: basePath, filter: regexp.MustCompile(`notpresent`), - want: "GOTRACEBACK=all\npanic: simple\n\nC1: runningA\n Emain Fmain.go:73 GmainR()A\n\n", + want: "GOTRACEBACK=all\npanic: simple\n\nC1: runningA\n Emain Fmain.go:75 GmainR()A\n\n", }, } for i, line := range data { - line := line t.Run(fmt.Sprintf("%d-%s", i, line.name), func(t *testing.T) { t.Parallel() out := bytes.Buffer{} @@ -109,14 +108,14 @@ "GOTRACEBACK=all\n" + "panic: simple\n\n" + "1: running\n" + - " main main.go:73 main()\n\n" + + " main main.go:75 main()\n\n" + "Ye\n" + "GOTRACEBACK=all\n" + "panic: 42\n\n" + "1: running\n" + - " main main.go:92 panicint(0x2a)\n" + - " main main.go:319 init.func9()\n" + - " main main.go:75 main()\n\n" + + " main main.go:94 panicint(0x2a)\n" + + " main main.go:321 init.func9()\n" + + " main main.go:77 main()\n\n" + "Yo\n") compareString(t, want, out.String()) } @@ -144,6 +143,8 @@ log.SetOutput(io.Discard) } // Set the environment variable so the stack doesn't include the info header. - os.Setenv("GOTRACEBACK", "all") + if err := os.Setenv("GOTRACEBACK", "all"); err != nil { + log.Fatal(err) + } os.Exit(m.Run()) }
diff --git a/internal/ui.go b/internal/ui.go index 104b890..928b267 100644 --- a/internal/ui.go +++ b/internal/ui.go
@@ -63,6 +63,8 @@ return fmt.Sprintf("%s:%d", c.LocalSrcPath, c.Line) } return fmt.Sprintf("%s:%d", c.RemoteSrcPath, c.Line) + case basePath: + return fmt.Sprintf("%s:%d", c.SrcName, c.Line) default: return fmt.Sprintf("%s:%d", c.SrcName, c.Line) } @@ -111,6 +113,65 @@ return srcLen, pkgLen } +// StackLines prints one complete stack trace, without the header. +func (p *Palette) StackLines(signature *stack.Signature, srcLen, pkgLen int, pf pathFormat) string { + // Populate indexed entries before optionally appending the elision marker. + out := make([]string, len(signature.Stack.Calls)) + for i := range signature.Stack.Calls { + out[i] = p.callLine(&signature.Stack.Calls[i], srcLen, pkgLen, pf) + } + if signature.Stack.Elided { + //nolint:makezero // Existing entries were populated by index. + out = append(out, " (...)") + } + return strings.Join(out, "\n") + "\n" +} + +// GoroutineHeader prints the header of a goroutine. +func (p *Palette) GoroutineHeader(g *stack.Goroutine, pf pathFormat, multipleGoroutines bool) string { + extra := "" + if s := g.SleepString(); s != "" { + extra += " [" + s + "]" + } + if g.Locked { + extra += " [locked]" + } + if c := pf.createdByString(&g.Signature); c != "" { + extra += p.CreatedBy + " [Created by " + c + "]" + } + if g.RaceAddr != 0 { + r := "read" + if g.RaceWrite { + r = "write" + } + extra += fmt.Sprintf("%s%s Race %s @ 0x%08x", p.EOLReset, p.Race, r, g.RaceAddr) + } + return fmt.Sprintf( + "%s%d: %s%s%s\n", + p.routineColor(g.First, multipleGoroutines), g.ID, + g.State, extra, + p.EOLReset) +} + +// BucketHeader prints the header of a goroutine signature. +func (p *Palette) BucketHeader(b *stack.Bucket, pf pathFormat, multipleBuckets bool) string { + extra := "" + if s := b.SleepString(); s != "" { + extra += " [" + s + "]" + } + if b.Locked { + extra += " [locked]" + } + if c := pf.createdByString(&b.Signature); c != "" { + extra += p.CreatedBy + " [Created by " + c + "]" + } + return fmt.Sprintf( + "%s%d: %s%s%s\n", + p.routineColor(b.First, multipleBuckets), len(b.IDs), + b.State, extra, + p.EOLReset) +} + // functionColor returns the color to be used for the function name based on // the type of package the function is in. func (p *Palette) functionColor(c *stack.Call) string { @@ -160,51 +221,6 @@ return p.Routine } -// BucketHeader prints the header of a goroutine signature. -func (p *Palette) BucketHeader(b *stack.Bucket, pf pathFormat, multipleBuckets bool) string { - extra := "" - if s := b.SleepString(); s != "" { - extra += " [" + s + "]" - } - if b.Locked { - extra += " [locked]" - } - if c := pf.createdByString(&b.Signature); c != "" { - extra += p.CreatedBy + " [Created by " + c + "]" - } - return fmt.Sprintf( - "%s%d: %s%s%s\n", - p.routineColor(b.First, multipleBuckets), len(b.IDs), - b.State, extra, - p.EOLReset) -} - -// GoroutineHeader prints the header of a goroutine. -func (p *Palette) GoroutineHeader(g *stack.Goroutine, pf pathFormat, multipleGoroutines bool) string { - extra := "" - if s := g.SleepString(); s != "" { - extra += " [" + s + "]" - } - if g.Locked { - extra += " [locked]" - } - if c := pf.createdByString(&g.Signature); c != "" { - extra += p.CreatedBy + " [Created by " + c + "]" - } - if g.RaceAddr != 0 { - r := "read" - if g.RaceWrite { - r = "write" - } - extra += fmt.Sprintf("%s%s Race %s @ 0x%08x", p.EOLReset, p.Race, r, g.RaceAddr) - } - return fmt.Sprintf( - "%s%d: %s%s%s\n", - p.routineColor(g.First, multipleGoroutines), g.ID, - g.State, extra, - p.EOLReset) -} - // callLine prints one stack line. func (p *Palette) callLine(line *stack.Call, srcLen, pkgLen int, pf pathFormat) string { return fmt.Sprintf( @@ -215,15 +231,3 @@ p.Arguments, &line.Args, p.EOLReset) } - -// StackLines prints one complete stack trace, without the header. -func (p *Palette) StackLines(signature *stack.Signature, srcLen, pkgLen int, pf pathFormat) string { - out := make([]string, len(signature.Stack.Calls)) - for i := range signature.Stack.Calls { - out[i] = p.callLine(&signature.Stack.Calls[i], srcLen, pkgLen, pf) - } - if signature.Stack.Elided { - out = append(out, " (...)") - } - return strings.Join(out, "\n") + "\n" -}
diff --git a/internal/ui_test.go b/internal/ui_test.go index 018bf87..a63bd97 100644 --- a/internal/ui_test.go +++ b/internal/ui_test.go
@@ -41,11 +41,9 @@ a := stack.Aggregated{ Buckets: []*stack.Bucket{ { - Signature: stack.Signature{ - Stack: stack.Stack{ - Calls: []stack.Call{ - newCallLocal("main.func·001", stack.Args{}, "/home/user/go/src/foo/baz.go", 123), - }, + Stack: stack.Stack{ + Calls: []stack.Call{ + newCallLocal("main.func·001", stack.Args{}, "/home/user/go/src/foo/baz.go", 123), }, }, IDs: []int{}, @@ -70,18 +68,16 @@ func TestBucketHeader(t *testing.T) { t.Parallel() b := stack.Bucket{ - Signature: stack.Signature{ - State: "chan receive", - CreatedBy: stack.Stack{ - Calls: []stack.Call{ - newCallLocal("main.mainImpl", stack.Args{}, "/home/user/go/src/github.com/foo/bar/baz.go", 74), - }, + State: "chan receive", + CreatedBy: stack.Stack{ + Calls: []stack.Call{ + newCallLocal("main.mainImpl", stack.Args{}, "/home/user/go/src/github.com/foo/bar/baz.go", 74), }, - SleepMax: 6, - SleepMin: 2, }, - IDs: []int{1, 2}, - First: true, + SleepMax: 6, + SleepMin: 2, + IDs: []int{1, 2}, + First: true, } // When printing, it prints the remote path, not the transposed local path. compareString(t, "B2: chan receive [2~6 minutes]D [Created by main.mainImpl @ /home/user/go/src/github.com/foo/bar/baz.go:74]A\n", testPalette.BucketHeader(&b, fullPath, true)) @@ -92,14 +88,12 @@ compareString(t, "C2: chan receive [2~6 minutes]D [Created by main.mainImpl @ baz.go:74]A\n", testPalette.BucketHeader(&b, basePath, false)) b = stack.Bucket{ - Signature: stack.Signature{ - State: "b0rked", - SleepMax: 6, - SleepMin: 6, - Locked: true, - }, - IDs: []int{}, - First: true, + State: "b0rked", + SleepMax: 6, + SleepMin: 6, + Locked: true, + IDs: []int{}, + First: true, } compareString(t, "C0: b0rked [6 minutes] [locked]A\n", testPalette.BucketHeader(&b, basePath, false)) } @@ -191,15 +185,16 @@ const gopath = "/home/user/go/src/" const gopathmod = "/home/user/go/pkg/mod/" // Do the equivalent of Call.updateLocations(). - if strings.HasPrefix(s, goroot) { + switch { + case strings.HasPrefix(s, goroot): c.LocalSrcPath = s c.RelSrcPath = s[len(goroot):] c.Location = stack.Stdlib - } else if strings.HasPrefix(s, gopath) { + case strings.HasPrefix(s, gopath): c.LocalSrcPath = s c.RelSrcPath = s[len(gopath):] c.Location = stack.GOPATH - } else if strings.HasPrefix(s, gopathmod) { + case strings.HasPrefix(s, gopathmod): c.LocalSrcPath = s c.RelSrcPath = s[len(gopathmod):] c.Location = stack.GoPkg
diff --git a/stack/bucket.go b/stack/bucket.go index a3e5a81..309bc3b 100644 --- a/stack/bucket.go +++ b/stack/bucket.go
@@ -104,6 +104,7 @@ type Bucket struct { // Signature is the generalized signature for this bucket. Signature + // IDs is the ID of each Goroutine with this Signature. IDs []int // First is true if this Bucket contains the first goroutine, e.g. the one
diff --git a/stack/bucket_test.go b/stack/bucket_test.go index 82c7bab..1bc4761 100644 --- a/stack/bucket_test.go +++ b/stack/bucket_test.go
@@ -8,6 +8,7 @@ import ( "bytes" + "errors" "io" "strings" "testing" @@ -32,7 +33,7 @@ "", } s, suffix, err := ScanSnapshot(bytes.NewBufferString(strings.Join(data, "\n")), io.Discard, defaultOpts()) - if err != io.EOF { + if !errors.Is(err, io.EOF) { t.Fatal(err) } if s == nil { @@ -40,42 +41,38 @@ } want := []*Bucket{ { - Signature: Signature{ - State: "chan receive", - Stack: Stack{ - Calls: []Call{ - newCall( - "main.func·001", - Args{Values: []Arg{ - {IsAggregate: true, Fields: Args{ - Values: []Arg{{Value: 0x11000000, IsPtr: true}, {Value: 2}}, - }}, - {Value: 3}, + State: "chan receive", + Stack: Stack{ + Calls: []Call{ + newCall( + "main.func·001", + Args{Values: []Arg{ + {IsAggregate: true, Fields: Args{ + Values: []Arg{{Value: 0x11000000, IsPtr: true}, {Value: 2}}, }}, - "/gopath/src/github.com/maruel/panicparse/stack/stack.go", - 72), - }, + {Value: 3}, + }}, + "/gopath/src/github.com/maruel/panicparse/stack/stack.go", + 72), }, }, IDs: []int{6}, First: true, }, { - Signature: Signature{ - State: "chan receive", - Stack: Stack{ - Calls: []Call{ - newCall( - "main.func·001", - Args{Values: []Arg{ - {IsAggregate: true, Fields: Args{ - Values: []Arg{{Value: 0x21000000, Name: "#1", IsPtr: true}, {Value: 2}}, - }}, - {Value: 3}, + State: "chan receive", + Stack: Stack{ + Calls: []Call{ + newCall( + "main.func·001", + Args{Values: []Arg{ + {IsAggregate: true, Fields: Args{ + Values: []Arg{{Value: 0x21000000, Name: "#1", IsPtr: true}, {Value: 2}}, }}, - "/gopath/src/github.com/maruel/panicparse/stack/stack.go", - 72), - }, + {Value: 3}, + }}, + "/gopath/src/github.com/maruel/panicparse/stack/stack.go", + 72), }, }, IDs: []int{7}, @@ -109,7 +106,7 @@ "", } s, suffix, err := ScanSnapshot(bytes.NewBufferString(strings.Join(data, "\n")), io.Discard, defaultOpts()) - if err != io.EOF { + if !errors.Is(err, io.EOF) { t.Fatal(err) } if s == nil { @@ -117,30 +114,28 @@ } want := []*Bucket{ { - Signature: Signature{ - State: "chan receive", - CreatedBy: Stack{ - Calls: []Call{ - newCall( - "main.mainImpl", - Args{}, - "/gopath/src/github.com/maruel/panicparse/stack/stack.go", - 74), - }, + State: "chan receive", + CreatedBy: Stack{ + Calls: []Call{ + newCall( + "main.mainImpl", + Args{}, + "/gopath/src/github.com/maruel/panicparse/stack/stack.go", + 74), }, - Stack: Stack{ - Calls: []Call{ - newCall( - "main.func·001", - Args{Values: []Arg{ - {IsAggregate: true, Fields: Args{ - Values: []Arg{{Value: 0x11000000, Name: "#1", IsPtr: true}, {Value: 2}}, - }}, - {Value: 3}, + }, + Stack: Stack{ + Calls: []Call{ + newCall( + "main.func·001", + Args{Values: []Arg{ + {IsAggregate: true, Fields: Args{ + Values: []Arg{{Value: 0x11000000, Name: "#1", IsPtr: true}, {Value: 2}}, }}, - "/gopath/src/github.com/maruel/panicparse/stack/stack.go", - 72), - }, + {Value: 3}, + }}, + "/gopath/src/github.com/maruel/panicparse/stack/stack.go", + 72), }, }, IDs: []int{6, 7}, @@ -171,7 +166,7 @@ "", } s, suffix, err := ScanSnapshot(bytes.NewBufferString(strings.Join(data, "\n")), io.Discard, defaultOpts()) - if err != io.EOF { + if !errors.Is(err, io.EOF) { t.Fatal(err) } if s == nil { @@ -179,23 +174,21 @@ } want := []*Bucket{ { - Signature: Signature{ - State: "chan receive", - SleepMin: 10, - SleepMax: 100, - Stack: Stack{ - Calls: []Call{ - newCall( - "main.func·001", - Args{Values: []Arg{ - {IsAggregate: true, Fields: Args{ - Values: []Arg{{Value: 0x11000000, Name: "*", IsPtr: true}, {Value: 2}}, - }}, - {Value: 3}, + State: "chan receive", + SleepMin: 10, + SleepMax: 100, + Stack: Stack{ + Calls: []Call{ + newCall( + "main.func·001", + Args{Values: []Arg{ + {IsAggregate: true, Fields: Args{ + Values: []Arg{{Value: 0x11000000, Name: "*", IsPtr: true}, {Value: 2}}, }}, - "/gopath/src/github.com/maruel/panicparse/stack/stack.go", - 72), - }, + {Value: 3}, + }}, + "/gopath/src/github.com/maruel/panicparse/stack/stack.go", + 72), }, }, IDs: []int{6, 7, 8}, @@ -235,7 +228,7 @@ " foo/foo.go:631 +0x4b", } s, suffix, err := ScanSnapshot(bytes.NewBufferString(strings.Join(data, "\n")), io.Discard, defaultOpts()) - if err != io.EOF { + if !errors.Is(err, io.EOF) { t.Fatal(err) } if s == nil { @@ -243,41 +236,39 @@ } want := []*Bucket{ { - Signature: Signature{ - State: "select", - CreatedBy: Stack{Calls: []Call{ + State: "select", + CreatedBy: Stack{Calls: []Call{ + { + Func: Func{Complete: "main", Name: "main"}, + RemoteSrcPath: "foo/foo.go", + Line: 631, + SrcName: "foo.go", + }, + }}, + SleepMin: 55, + SleepMax: 55, + Stack: Stack{ + Calls: []Call{ { - Func: Func{Complete: "main", Name: "main"}, + Func: Func{ + Complete: "foo.Bar", + ImportPath: "foo", + DirName: "foo", + Name: "Bar", + IsExported: true, + }, RemoteSrcPath: "foo/foo.go", - Line: 631, + Line: 467, SrcName: "foo.go", + ImportPath: "foo", }, - }}, - SleepMin: 55, - SleepMax: 55, - Stack: Stack{ - Calls: []Call{ - { - Func: Func{ - Complete: "foo.Bar", - ImportPath: "foo", - DirName: "foo", - Name: "Bar", - IsExported: true, - }, - RemoteSrcPath: "foo/foo.go", - Line: 467, - SrcName: "foo.go", - ImportPath: "foo", - }, - { - Func: Func{Complete: "foo.baz", ImportPath: "foo", DirName: "foo", Name: "baz"}, - Args: Args{Values: []Arg{{Value: 3}}}, - RemoteSrcPath: "foo/foo.go", - Line: 643, - SrcName: "foo.go", - ImportPath: "foo", - }, + { + Func: Func{Complete: "foo.baz", ImportPath: "foo", DirName: "foo", Name: "baz"}, + Args: Args{Values: []Arg{{Value: 3}}}, + RemoteSrcPath: "foo/foo.go", + Line: 643, + SrcName: "foo.go", + ImportPath: "foo", }, }, }, @@ -285,76 +276,72 @@ First: true, }, { - Signature: Signature{ - State: "select", - CreatedBy: Stack{Calls: []Call{ + State: "select", + CreatedBy: Stack{Calls: []Call{ + { + Func: Func{Complete: "main", Name: "main"}, + RemoteSrcPath: "foo/foo.go", + Line: 631, + SrcName: "foo.go", + }, + }}, + SleepMin: 55, + SleepMax: 55, + Stack: Stack{ + Calls: []Call{ { - Func: Func{Complete: "main", Name: "main"}, + Func: Func{ + Complete: "foo.Bar", + ImportPath: "foo", + DirName: "foo", + Name: "Bar", + IsExported: true, + }, RemoteSrcPath: "foo/foo.go", - Line: 631, + Line: 467, SrcName: "foo.go", + ImportPath: "foo", }, - }}, - SleepMin: 55, - SleepMax: 55, - Stack: Stack{ - Calls: []Call{ - { - Func: Func{ - Complete: "foo.Bar", - ImportPath: "foo", - DirName: "foo", - Name: "Bar", - IsExported: true, - }, - RemoteSrcPath: "foo/foo.go", - Line: 467, - SrcName: "foo.go", - ImportPath: "foo", - }, - { - Func: Func{Complete: "foo.baz", ImportPath: "foo", DirName: "foo", Name: "baz"}, - Args: Args{Values: []Arg{{Value: 1}}}, - RemoteSrcPath: "foo/foo.go", - Line: 643, - SrcName: "foo.go", - ImportPath: "foo", - }, + { + Func: Func{Complete: "foo.baz", ImportPath: "foo", DirName: "foo", Name: "baz"}, + Args: Args{Values: []Arg{{Value: 1}}}, + RemoteSrcPath: "foo/foo.go", + Line: 643, + SrcName: "foo.go", + ImportPath: "foo", }, }, }, IDs: []int{55}, }, { - Signature: Signature{ - State: "select", - CreatedBy: Stack{ - Calls: []Call{ - { - Func: Func{Complete: "bozo", Name: "bozo"}, - RemoteSrcPath: "foo/foo.go", - Line: 420, - SrcName: "foo.go", - }, + State: "select", + CreatedBy: Stack{ + Calls: []Call{ + { + Func: Func{Complete: "bozo", Name: "bozo"}, + RemoteSrcPath: "foo/foo.go", + Line: 420, + SrcName: "foo.go", }, }, - SleepMin: 55, - SleepMax: 55, - Stack: Stack{ - Calls: []Call{ - { - Func: Func{ - Complete: "foo.Bar", - ImportPath: "foo", - DirName: "foo", - Name: "Bar", - IsExported: true, - }, - RemoteSrcPath: "foo/foo.go", - Line: 467, - SrcName: "foo.go", - ImportPath: "foo", + }, + SleepMin: 55, + SleepMax: 55, + Stack: Stack{ + Calls: []Call{ + { + Func: Func{ + Complete: "foo.Bar", + ImportPath: "foo", + DirName: "foo", + Name: "Bar", + IsExported: true, }, + RemoteSrcPath: "foo/foo.go", + Line: 467, + SrcName: "foo.go", + ImportPath: "foo", }, }, }, @@ -368,7 +355,7 @@ func BenchmarkAggregate(b *testing.B) { b.ReportAllocs() s, suffix, err := ScanSnapshot(bytes.NewReader(internaltest.StaticPanicwebOutput()), io.Discard, defaultOpts()) - if err != io.EOF { + if !errors.Is(err, io.EOF) { b.Fatal(err) } if s == nil { @@ -378,7 +365,7 @@ b.Fatalf("unexpected suffix: %q", string(suffix)) } b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { buckets := s.Aggregate(AnyPointer).Buckets if len(buckets) < 5 { b.Fatal("expected more buckets")
diff --git a/stack/context.go b/stack/context.go index 88a9df4..15adba7 100644 --- a/stack/context.go +++ b/stack/context.go
@@ -69,7 +69,7 @@ func DefaultOpts() *Opts { p := build.Default.GOROOT if runtime.GOOS == "windows" { - p = strings.Replace(p, pathSeparator, "/", -1) + p = strings.ReplaceAll(p, pathSeparator, "/") } return &Opts{ LocalGOROOT: p, @@ -181,7 +181,7 @@ var d []byte if d, err = r.readLine(); len(d) != 0 { l, err1 := s.scan(d) - if err1 != nil && (err == nil || err == io.EOF) { + if err1 != nil && (err == nil || errors.Is(err, io.EOF)) { err = err1 } if !l { @@ -190,7 +190,7 @@ suffix = append(suffix, r.buffered()...) break } - if _, err1 = prefix.Write(d); err1 != nil && (err == nil || err == io.EOF) { + if _, err1 = prefix.Write(d); err1 != nil && (err == nil || errors.Is(err, io.EOF)) { err = err1 break } @@ -257,9 +257,9 @@ var ( lockedToThread = []byte("locked to thread") - // gotRaceHeader1, done + // gotRaceHeader1, done. raceHeaderFooter = []byte("==================") - // gotRaceHeader2 + // gotRaceHeader2. raceHeader = []byte("WARNING: DATA RACE") crlf = []byte("\r\n") lf = []byte("\n") @@ -273,11 +273,11 @@ // These are effectively constants. var ( - // gotRoutineHeader + // gotRoutineHeader. reRoutineHeader = regexp.MustCompile("^([ \t]*)goroutine (\\d+)(?: gp=[^ ]+ m=[^ ]+(?: mp=[^ ]+)?)? \\[([^\\]]+)\\]\\:$") reMinutes = regexp.MustCompile(`^(\d+) minutes$`) - // gotUnavail + // gotUnavail. reUnavail = regexp.MustCompile("^(?:\t| +)goroutine running on other thread; stack unavailable") // gotFileFunc, gotRaceOperationFile, gotRaceGoroutineFile @@ -304,7 +304,7 @@ // parenthood. reCreated = regexp.MustCompile("^created by (.+)$") - // gotFunc, gotRaceOperationFunc, gotRaceGoroutineFunc + // gotFunc, gotRaceOperationFunc, gotRaceGoroutineFunc. reFunc = regexp.MustCompile(`^(.+)\((.*)\)$`) // Race: @@ -313,19 +313,19 @@ // #else // #if !SANITIZER_GO // is used. // TODO(maruel): " [failed to restore the stack]\n\n" - // TODO(maruel): "Global var %s of size %zu at %p declared at %s:%zu\n" + // TODO(maruel): "Global var %s of size %zu at %p declared at %s:%zu\n". - // gotRaceOperationHeader + // gotRaceOperationHeader. reRaceOperationHeader = regexp.MustCompile(`^(Read|Write) at (0x[0-9a-f]+) by goroutine (\d+):$`) - // gotRaceOperationHeader + // gotRaceOperationHeader. reRacePreviousOperationHeader = regexp.MustCompile(`^Previous (read|write) at (0x[0-9a-f]+) by goroutine (\d+):$`) - // gotRaceGoroutineHeader + // gotRaceGoroutineHeader. reRaceGoroutine = regexp.MustCompile(`^Goroutine (\d+) \((running|finished)\) created at:$`) // TODO(maruel): Use it. - //reRacePreviousOperationMainHeader = regexp.MustCompile("^Previous (read|write) at (0x[0-9a-f]+) by main goroutine:$") + // reRacePreviousOperationMainHeader = regexp.MustCompile("^Previous (read|write) at (0x[0-9a-f]+) by main goroutine:$"). ) // state is the state of the scan to detect and process a stack trace. @@ -340,7 +340,7 @@ // Done processing a stack trace. done - // Panic stack trace: + // Panic stack trace:. // Signature: "" // An empty line between goroutines. @@ -384,7 +384,7 @@ // to: betweenRoutine, gotCreated gotUnavail - // Race detector: + // Race detector:. // Constant: raceHeaderFooter // Signature: "==================" @@ -449,6 +449,7 @@ // and stores the traces found. type scanningState struct { *Snapshot + state state prefix []byte goroutineIndex int @@ -473,7 +474,7 @@ // TODO(maruel): Handle corrupted stack cases: // - missed stack barrier // - found next stack barrier at 0x123; expected -// - runtime: unexpected return pc for FUNC_NAME called from 0x123 +// - runtime: unexpected return pc for FUNC_NAME called from 0x123. func (s *scanningState) scan(line []byte) (bool, error) { /* This is very useful to debug issues in the state machine. defer func() { @@ -485,17 +486,14 @@ cur = s.Goroutines[len(s.Goroutines)-1] } trimmed := line - if bytes.HasSuffix(line, crlf) { + switch { + case bytes.HasSuffix(line, crlf): trimmed = line[:len(line)-2] - } else if bytes.HasSuffix(line, lf) { + case bytes.HasSuffix(line, lf): trimmed = line[:len(line)-1] - } else { - // It's the end of the stream and it's not terminating with EOL character. - if s.state == looking || s.state == done { - return false, nil - } - // Let it flow. It's possible the last line was trimmed and we still want - // to parse it. + case s.state == looking || s.state == done: + // A final unterminated line is ignored when no snapshot is active. + return false, nil } if len(trimmed) != 0 && len(s.prefix) != 0 { @@ -539,14 +537,12 @@ } } g := &Goroutine{ - Signature: Signature{ - State: string(items[0]), - SleepMin: sleep, - SleepMax: sleep, - Locked: locked, - }, - ID: id, - First: len(s.Goroutines) == 0, + State: string(items[0]), + SleepMin: sleep, + SleepMax: sleep, + Locked: locked, + ID: id, + First: len(s.Goroutines) == 0, } // Increase performance by always allocating 4 goroutines minimally. if s.Goroutines == nil { @@ -832,7 +828,7 @@ args, err := parseArgs(match[2]) if err != nil { - return true, fmt.Errorf("%s on line: %q", err, bytes.TrimSpace(line)) + return true, fmt.Errorf("%w on line: %q", err, bytes.TrimSpace(line)) } c.Args = args return true, nil @@ -847,15 +843,15 @@ var args Args frames := [maxDepth]*Args{&args} stack := frames[:1] - for _, s := range bytes.Split(line, commaSpace) { + for s := range bytes.SplitSeq(line, commaSpace) { opened, a, closed := trimCurlyBrackets(s) - for i := 0; i < opened; i++ { + for range opened { cur := stack[len(stack)-1] cur.Values = append(cur.Values, Arg{}) next := &cur.Values[len(cur.Values)-1] next.IsAggregate = true if len(stack) == maxDepth { - return Args{}, fmt.Errorf("nested aggregate-typed arguments exceeded depth limit") + return Args{}, errors.New("nested aggregate-typed arguments exceeded depth limit") } stack = stack[:len(stack)+1] stack[len(stack)-1] = &next.Fields @@ -884,7 +880,7 @@ cur.Values = append(cur.Values, arg) } } - for i := 0; i < closed; i++ { + for range closed { if len(stack) == 1 { return Args{}, errors.New("unmatched closing curly bracket") } @@ -1021,7 +1017,7 @@ (*g)[prefix] = struct{}{} p := pathJoin(prefix, "go.mod") if runtime.GOOS == "windows" { - p = strings.Replace(p, "/", pathSeparator, -1) + p = strings.ReplaceAll(p, "/", pathSeparator) } /* #nosec G304 */ b, err := os.ReadFile(p) @@ -1066,7 +1062,7 @@ for _, f := range getFiles(s.Goroutines) { // TODO(maruel): Could a stack dump have mixed cases? I think it's // possible, need to confirm and handle. - //log.Printf(" Analyzing %s", f) + // log.Printf(" Analyzing %s", f) // First checks skip file I/O. if s.RemoteGOROOT != "" && strings.HasPrefix(f, s.RemoteGOROOT+"/src/") { @@ -1087,14 +1083,14 @@ found := false for _, l := range s.LocalGOPATHs { if r := isRootedIn(l+src, parts); r != "" { - //log.Printf("Found RemoteGOPATHs[%s] = %s", r[:len(r)-len(src)], l) + // log.Printf("Found RemoteGOPATHs[%s] = %s", r[:len(r)-len(src)], l) s.RemoteGOPATHs[r[:len(r)-len(src)]] = l found = true break } const pkgmod = "/pkg/mod" if r := isRootedIn(l+pkgmod, parts); r != "" { - //log.Printf("Found RemoteGOPATHs[%s] = %s", r[:len(r)-len(pkgmod)], l) + // log.Printf("Found RemoteGOPATHs[%s] = %s", r[:len(r)-len(pkgmod)], l) s.RemoteGOPATHs[r[:len(r)-len(pkgmod)]] = l found = true break @@ -1118,7 +1114,7 @@ continue } // If the source is not found, just too bad. - //log.Printf("Failed to find locally: %s", f) + // log.Printf("Failed to find locally: %s", f) missing++ } return missing @@ -1132,7 +1128,7 @@ // Disallow non-absolute paths? if v != "" { if runtime.GOOS == "windows" { - v = strings.Replace(v, pathSeparator, "/", -1) + v = strings.ReplaceAll(v, pathSeparator, "/") } // Trim trailing "/". if l := len(v); v[l-1] == '/' { @@ -1143,7 +1139,7 @@ } } if len(out) == 0 { - homeDir := "" + var homeDir string u, err := user.Current() if err != nil { homeDir = os.Getenv("HOME") @@ -1155,7 +1151,7 @@ } p := homeDir + "/go" if runtime.GOOS == "windows" { - p = strings.Replace(p, pathSeparator, "/", -1) + p = strings.ReplaceAll(p, pathSeparator, "/") } out = []string{p} }
diff --git a/stack/context_test.go b/stack/context_test.go index f0ab908..b688a02 100644 --- a/stack/context_test.go +++ b/stack/context_test.go
@@ -91,29 +91,27 @@ err: io.EOF, want: []*Goroutine{ { - Signature: Signature{ - State: "running", - Stack: Stack{ - Calls: []Call{ - newCall( - "github.com/cockroachdb/cockroach/storage/engine._Cfunc_DBIterSeek", - Args{}, "??", 0), - newCall( - "gopkg.in/yaml%2ev2.handleErr", - Args{Values: []Arg{{Value: 0x433b20, IsPtr: true}}}, - "/gopath/src/gopkg.in/yaml.v2/yaml.go", - 153), - newCall( - "reflect.Value.assignTo", - Args{Values: []Arg{{Value: 0x570860, IsPtr: true}, {Value: 0x803f3e0, IsPtr: true}, {Value: 0x15}}}, - "/goroot/src/reflect/value.go", - 2125), - newCall( - "main.main", - Args{}, - "/gopath/src/github.com/maruel/panicparse/stack/stack.go", - 428), - }, + State: "running", + Stack: Stack{ + Calls: []Call{ + newCall( + "github.com/cockroachdb/cockroach/storage/engine._Cfunc_DBIterSeek", + Args{}, "??", 0), + newCall( + "gopkg.in/yaml%2ev2.handleErr", + Args{Values: []Arg{{Value: 0x433b20, IsPtr: true}}}, + "/gopath/src/gopkg.in/yaml.v2/yaml.go", + 153), + newCall( + "reflect.Value.assignTo", + Args{Values: []Arg{{Value: 0x570860, IsPtr: true}, {Value: 0x803f3e0, IsPtr: true}, {Value: 0x15}}}, + "/goroot/src/reflect/value.go", + 2125), + newCall( + "main.main", + Args{}, + "/gopath/src/github.com/maruel/panicparse/stack/stack.go", + 428), }, }, ID: 1, @@ -144,56 +142,50 @@ err: io.EOF, want: []*Goroutine{ { - Signature: Signature{ - State: "chan send", - SleepMin: 100, - SleepMax: 100, - Stack: Stack{ - Calls: []Call{ - newCall( - "gopkg.in/yaml%2ev2.handleErr", - Args{Values: []Arg{{Value: 0x433b20, IsPtr: true}}}, - "/gopath/src/gopkg.in/yaml.v2/yaml.go", - 153), - }, + State: "chan send", + SleepMin: 100, + SleepMax: 100, + Stack: Stack{ + Calls: []Call{ + newCall( + "gopkg.in/yaml%2ev2.handleErr", + Args{Values: []Arg{{Value: 0x433b20, IsPtr: true}}}, + "/gopath/src/gopkg.in/yaml.v2/yaml.go", + 153), }, }, ID: 1, First: true, }, { - Signature: Signature{ - State: "chan send", - Locked: true, - Stack: Stack{ - Calls: []Call{ - newCall( - "gopkg.in/yaml%2ev2.handleErr", - Args{Values: []Arg{{Value: 0x8033b21, Name: "#1", IsPtr: true}}}, - "/gopath/src/gopkg.in/yaml.v2/yaml.go", - 153), - }, + State: "chan send", + Locked: true, + Stack: Stack{ + Calls: []Call{ + newCall( + "gopkg.in/yaml%2ev2.handleErr", + Args{Values: []Arg{{Value: 0x8033b21, Name: "#1", IsPtr: true}}}, + "/gopath/src/gopkg.in/yaml.v2/yaml.go", + 153), }, }, ID: 2, }, { - Signature: Signature{ - State: "chan send", - SleepMin: 101, - SleepMax: 101, - Stack: Stack{ - Calls: []Call{ - newCall( - "gopkg.in/yaml%2ev2.handleErr", - Args{Values: []Arg{{Value: 0x8033b22, Name: "#2", IsPtr: true}}}, - "/gopath/src/gopkg.in/yaml.v2/yaml.go", - 153), - }, + State: "chan send", + SleepMin: 101, + SleepMax: 101, + Stack: Stack{ + Calls: []Call{ + newCall( + "gopkg.in/yaml%2ev2.handleErr", + Args{Values: []Arg{{Value: 0x8033b22, Name: "#2", IsPtr: true}}}, + "/gopath/src/gopkg.in/yaml.v2/yaml.go", + 153), }, - Locked: true, }, - ID: 3, + Locked: true, + ID: 3, }, }, }, @@ -212,16 +204,14 @@ err: io.EOF, want: []*Goroutine{ { - Signature: Signature{ - State: "garbage collection", - Stack: Stack{ - Calls: []Call{ - newCall( - "runtime.switchtoM", - Args{}, - "/goroot/src/runtime/asm_amd64.s", - 198), - }, + State: "garbage collection", + Stack: Stack{ + Calls: []Call{ + newCall( + "runtime.switchtoM", + Args{}, + "/goroot/src/runtime/asm_amd64.s", + 198), }, }, ID: 16, @@ -244,16 +234,14 @@ err: io.EOF, want: []*Goroutine{ { - Signature: Signature{ - State: "garbage collection", - Stack: Stack{ - Calls: []Call{ - newCall( - "runtime.switchtoM", - Args{}, - "/goroot/src/runtime/asm_amd64.s", - 198), - }, + State: "garbage collection", + Stack: Stack{ + Calls: []Call{ + newCall( + "runtime.switchtoM", + Args{}, + "/goroot/src/runtime/asm_amd64.s", + 198), }, }, ID: 16, @@ -277,12 +265,10 @@ err: errors.New("failed to parse int on line: \"/gopath/src/github.com/maruel/panicparse/stack/stack.go:12345678901234567890\""), want: []*Goroutine{ { - Signature: Signature{ - State: "running", - Stack: Stack{ - Calls: []Call{ - newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0), - }, + State: "running", + Stack: Stack{ + Calls: []Call{ + newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0), }, }, ID: 1, @@ -308,17 +294,15 @@ err: errors.New("failed to parse int on line: \"/goroot/src/testing/testing.go:123456789012345678901 +0xa8b\""), want: []*Goroutine{ { - Signature: Signature{ - State: "running", - CreatedBy: Stack{Calls: []Call{newCall("testing.RunTests", Args{}, "", 0)}}, - Stack: Stack{ - Calls: []Call{ - newCall( - "github.com/maruel/panicparse/stack/stack.recurseType", - Args{}, - "/gopath/src/github.com/maruel/panicparse/stack/stack.go", - 1), - }, + State: "running", + CreatedBy: Stack{Calls: []Call{newCall("testing.RunTests", Args{}, "", 0)}}, + Stack: Stack{ + Calls: []Call{ + newCall( + "github.com/maruel/panicparse/stack/stack.recurseType", + Args{}, + "/gopath/src/github.com/maruel/panicparse/stack/stack.go", + 1), }, }, ID: 1, @@ -343,12 +327,10 @@ err: errors.New("failed to parse int on line: \"github.com/maruel/panicparse/stack/stack.recurseType(123456789012345678901)\""), want: []*Goroutine{ { - Signature: Signature{ - State: "running", - Stack: Stack{ - Calls: []Call{ - newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0), - }, + State: "running", + Stack: Stack{ + Calls: []Call{ + newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0), }, }, ID: 1, @@ -371,28 +353,26 @@ err: io.EOF, want: []*Goroutine{ { - Signature: Signature{ - State: "running", - Stack: Stack{ - Calls: []Call{ - newCall( - "github.com/maruel/panicparse/stack/stack.recurseType", - Args{ + State: "running", + Stack: Stack{ + Calls: []Call{ + newCall( + "github.com/maruel/panicparse/stack/stack.recurseType", + Args{ + Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ Values: []Arg{{IsAggregate: true, Fields: Args{ - Values: []Arg{{IsAggregate: true, Fields: Args{ - Elided: true, - }}}, + Elided: true, }}}, }}}, }}}, }}}, - }, - "/gopath/src/github.com/maruel/panicparse/stack/stack.go", - 9), - }, + }}}, + }, + "/gopath/src/github.com/maruel/panicparse/stack/stack.go", + 9), }, }, ID: 1, @@ -417,12 +397,10 @@ err: errors.New("nested aggregate-typed arguments exceeded depth limit on line: \"github.com/maruel/panicparse/stack/stack.recurseType({{{{{{...}}}}}})\""), want: []*Goroutine{ { - Signature: Signature{ - State: "running", - Stack: Stack{ - Calls: []Call{ - newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0), - }, + State: "running", + Stack: Stack{ + Calls: []Call{ + newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0), }, }, ID: 1, @@ -447,12 +425,10 @@ err: errors.New("unmatched opening curly bracket on line: \"github.com/maruel/panicparse/stack/stack.recurseType({{{{{...}}}})\""), want: []*Goroutine{ { - Signature: Signature{ - State: "running", - Stack: Stack{ - Calls: []Call{ - newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0), - }, + State: "running", + Stack: Stack{ + Calls: []Call{ + newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0), }, }, ID: 1, @@ -477,12 +453,10 @@ err: errors.New("unmatched closing curly bracket on line: \"github.com/maruel/panicparse/stack/stack.recurseType({{{{...}}}}})\""), want: []*Goroutine{ { - Signature: Signature{ - State: "running", - Stack: Stack{ - Calls: []Call{ - newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0), - }, + State: "running", + Stack: Stack{ + Calls: []Call{ + newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0), }, }, ID: 1, @@ -503,12 +477,10 @@ err: errors.New(`inconsistent indentation: " \t/gopath/src/github.com/maruel/panicparse/stack/stack.go:1", expected " "`), want: []*Goroutine{ { - Signature: Signature{ - State: "running", - Stack: Stack{ - Calls: []Call{ - newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0), - }, + State: "running", + Stack: Stack{ + Calls: []Call{ + newCall("github.com/maruel/panicparse/stack/stack.recurseType", Args{}, "", 0), }, }, ID: 1, @@ -535,9 +507,9 @@ err: errors.New("expected a function after a goroutine header, got: \"/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6\""), want: []*Goroutine{ { - Signature: Signature{State: "garbage collection"}, - ID: 16, - First: true, + State: "garbage collection", + ID: 16, + First: true, }, }, }, @@ -559,35 +531,33 @@ err: io.EOF, want: []*Goroutine{ { - Signature: Signature{ - State: "garbage collection", - CreatedBy: Stack{ - Calls: []Call{ - newCall( - "testing.RunTests", - Args{}, - "/goroot/src/testing/testing.go", - 555), - }, + State: "garbage collection", + CreatedBy: Stack{ + Calls: []Call{ + newCall( + "testing.RunTests", + Args{}, + "/goroot/src/testing/testing.go", + 555), }, - Stack: Stack{ - Calls: []Call{ - newCall( - "github.com/maruel/panicparse/stack/stack.recurseType", - Args{ - Values: []Arg{ - {Value: 0x9a3ec70, IsPtr: true}, - {Value: 0x8062580, IsPtr: true}, - {Value: 0x9a3e818, IsPtr: true}, - {Value: 0x50a820, IsPtr: true}, - {Value: 0x803a8a0, IsPtr: true}, - }, + }, + Stack: Stack{ + Calls: []Call{ + newCall( + "github.com/maruel/panicparse/stack/stack.recurseType", + Args{ + Values: []Arg{ + {Value: 0x9a3ec70, IsPtr: true}, + {Value: 0x8062580, IsPtr: true}, + {Value: 0x9a3e818, IsPtr: true}, + {Value: 0x50a820, IsPtr: true}, + {Value: 0x803a8a0, IsPtr: true}, }, - "/gopath/src/github.com/maruel/panicparse/stack/stack.go", - 53), - }, - Elided: true, + }, + "/gopath/src/github.com/maruel/panicparse/stack/stack.go", + 53), }, + Elided: true, }, ID: 16, First: true, @@ -617,46 +587,44 @@ err: io.EOF, want: []*Goroutine{ { - Signature: Signature{ - State: "syscall", - CreatedBy: Stack{ - Calls: []Call{ - newCall( - "os/signal.init·1", - Args{}, - "/goroot/src/os/signal/signal_unix.go", - 27), - }, + State: "syscall", + CreatedBy: Stack{ + Calls: []Call{ + newCall( + "os/signal.init·1", + Args{}, + "/goroot/src/os/signal/signal_unix.go", + 27), }, - Stack: Stack{ - Calls: []Call{ - newCall( - "runtime.notetsleepg", - Args{ - Values: []Arg{ - {Value: 0x918100, IsPtr: true}, - {Value: 0xffffffffffffffff}, - {Value: 0x1}, - }, + }, + Stack: Stack{ + Calls: []Call{ + newCall( + "runtime.notetsleepg", + Args{ + Values: []Arg{ + {Value: 0x918100, IsPtr: true}, + {Value: 0xffffffffffffffff}, + {Value: 0x1}, }, - "/goroot/src/runtime/lock_futex.go", - 201), - newCall( - "runtime.signal_recv", - Args{Values: []Arg{{}}}, - "/goroot/src/runtime/sigqueue.go", - 109), - newCall( - "os/signal.loop", - Args{}, - "/goroot/src/os/signal/signal_unix.go", - 21), - newCall( - "runtime.goexit", - Args{}, - "/goroot/src/runtime/asm_amd64.s", - 2232), - }, + }, + "/goroot/src/runtime/lock_futex.go", + 201), + newCall( + "runtime.signal_recv", + Args{Values: []Arg{{}}}, + "/goroot/src/runtime/sigqueue.go", + 109), + newCall( + "os/signal.loop", + Args{}, + "/goroot/src/os/signal/signal_unix.go", + 21), + newCall( + "runtime.goexit", + Args{}, + "/goroot/src/runtime/asm_amd64.s", + 2232), }, }, ID: 5, @@ -680,20 +648,18 @@ err: io.EOF, want: []*Goroutine{ { - Signature: Signature{ - State: "running", - CreatedBy: Stack{ - Calls: []Call{ - newCall( - "github.com/maruel/panicparse/stack.New", - Args{}, - "/gopath/src/github.com/maruel/panicparse/stack/stack.go", - 131), - }, + State: "running", + CreatedBy: Stack{ + Calls: []Call{ + newCall( + "github.com/maruel/panicparse/stack.New", + Args{}, + "/gopath/src/github.com/maruel/panicparse/stack/stack.go", + 131), }, - Stack: Stack{ - Calls: []Call{newCall("", Args{}, "<unavailable>", 0)}, - }, + }, + Stack: Stack{ + Calls: []Call{newCall("", Args{}, "<unavailable>", 0)}, }, ID: 24, First: true, @@ -715,11 +681,9 @@ err: io.EOF, want: []*Goroutine{ { - Signature: Signature{ - State: "running", - Stack: Stack{ - Calls: []Call{newCall("", Args{}, "<unavailable>", 0)}, - }, + State: "running", + Stack: Stack{ + Calls: []Call{newCall("", Args{}, "<unavailable>", 0)}, }, ID: 24, First: true, @@ -741,11 +705,9 @@ err: errors.New("expected empty line after unavailable stack, got: \"junk\""), want: []*Goroutine{ { - Signature: Signature{ - State: "running", - Stack: Stack{ - Calls: []Call{newCall("", Args{}, "<unavailable>", 0)}, - }, + State: "running", + Stack: Stack{ + Calls: []Call{newCall("", Args{}, "<unavailable>", 0)}, }, ID: 24, First: true, @@ -769,25 +731,23 @@ err: io.EOF, want: []*Goroutine{ { - Signature: Signature{ - State: "runnable", - CreatedBy: Stack{ - Calls: []Call{ - newCall( - "github.com/maruel/panicparse/stack.New", - Args{}, - "/gopath/src/github.com/maruel/panicparse/stack/stack.go", - 113), - }, + State: "runnable", + CreatedBy: Stack{ + Calls: []Call{ + newCall( + "github.com/maruel/panicparse/stack.New", + Args{}, + "/gopath/src/github.com/maruel/panicparse/stack/stack.go", + 113), }, - Stack: Stack{ - Calls: []Call{ - newCall( - "github.com/maruel/panicparse/stack.func·002", - Args{}, - "/gopath/src/github.com/maruel/panicparse/stack/stack.go", - 110), - }, + }, + Stack: Stack{ + Calls: []Call{ + newCall( + "github.com/maruel/panicparse/stack.func·002", + Args{}, + "/gopath/src/github.com/maruel/panicparse/stack/stack.go", + 110), }, }, ID: 37, @@ -810,9 +770,9 @@ err: errors.New("expected a function after a goroutine header, got: \"junk\""), want: []*Goroutine{ { - Signature: Signature{State: "running"}, - ID: 1, - First: true, + State: "running", + ID: 1, + First: true, }, }, }, @@ -832,12 +792,10 @@ err: errors.New("expected a file after a function, got: \"junk\""), want: []*Goroutine{ { - Signature: Signature{ - State: "running", - Stack: Stack{ - Calls: []Call{ - newCall("github.com/maruel/panicparse/stack.func·002", Args{}, "", 0), - }, + State: "running", + Stack: Stack{ + Calls: []Call{ + newCall("github.com/maruel/panicparse/stack.func·002", Args{}, "", 0), }, }, ID: 1, @@ -864,25 +822,23 @@ err: io.EOF, want: []*Goroutine{ { - Signature: Signature{ - State: "running", - CreatedBy: Stack{ - Calls: []Call{ - newCall( - "github.com/maruel/panicparse/stack.New", - Args{}, - "/gopath/src/github.com/maruel/panicparse/stack/stack.go", - 131), - }, + State: "running", + CreatedBy: Stack{ + Calls: []Call{ + newCall( + "github.com/maruel/panicparse/stack.New", + Args{}, + "/gopath/src/github.com/maruel/panicparse/stack/stack.go", + 131), }, - Stack: Stack{ - Calls: []Call{ - newCall( - "github.com/maruel/panicparse/stack.func·002", - Args{}, - "/gopath/src/github.com/maruel/panicparse/stack/stack.go", - 110), - }, + }, + Stack: Stack{ + Calls: []Call{ + newCall( + "github.com/maruel/panicparse/stack.func·002", + Args{}, + "/gopath/src/github.com/maruel/panicparse/stack/stack.go", + 110), }, }, ID: 1, @@ -908,21 +864,19 @@ err: errors.New("expected a file after a created line, got: \"junk\""), want: []*Goroutine{ { - Signature: Signature{ - State: "running", - CreatedBy: Stack{ - Calls: []Call{ - newCall("github.com/maruel/panicparse/stack.New", Args{}, "", 0), - }, + State: "running", + CreatedBy: Stack{ + Calls: []Call{ + newCall("github.com/maruel/panicparse/stack.New", Args{}, "", 0), }, - Stack: Stack{ - Calls: []Call{ - newCall( - "github.com/maruel/panicparse/stack.func·002", - Args{}, - "/gopath/src/github.com/maruel/panicparse/stack/stack.go", - 110), - }, + }, + Stack: Stack{ + Calls: []Call{ + newCall( + "github.com/maruel/panicparse/stack.func·002", + Args{}, + "/gopath/src/github.com/maruel/panicparse/stack/stack.go", + 110), }, }, ID: 1, @@ -956,51 +910,49 @@ err: io.EOF, want: []*Goroutine{ { - Signature: Signature{ - State: "idle", - Stack: Stack{ - Calls: []Call{ - newCall( - "runtime.epollwait", - Args{ - Values: []Arg{ - {Value: 0x4}, - {Value: 0x71c7118, IsPtr: true}, - {Value: 0xffffffff00000080}, - {}, - {Value: 0xffffffff0028c1be}, - {}, - {}, - {}, - {}, - {}, - }, - Elided: true, + State: "idle", + Stack: Stack{ + Calls: []Call{ + newCall( + "runtime.epollwait", + Args{ + Values: []Arg{ + {Value: 0x4}, + {Value: 0x71c7118, IsPtr: true}, + {Value: 0xffffffff00000080}, + {}, + {Value: 0xffffffff0028c1be}, + {}, + {}, + {}, + {}, + {}, }, - "/goroot/src/runtime/sys_linux_amd64.s", - 400), - newCall( - "runtime.netpoll", - Args{Values: []Arg{{Value: 0x901b01, IsPtr: true}, {}}}, - "/goroot/src/runtime/netpoll_epoll.go", - 68), - newCall( - "findrunnable", - Args{Values: []Arg{{Value: 0x8012000, IsPtr: true}}}, - "/goroot/src/runtime/proc.c", - 1472), - newCall("schedule", Args{}, "/goroot/src/runtime/proc.c", 1575), - newCall( - "runtime.park_m", - Args{Values: []Arg{{Value: 0x80017a0, IsPtr: true}}}, - "/goroot/src/runtime/proc.c", - 1654), - newCall( - "runtime.mcall", - Args{Values: []Arg{{Value: 0x432684, IsPtr: true}}}, - "/goroot/src/runtime/asm_amd64.s", - 186), - }, + Elided: true, + }, + "/goroot/src/runtime/sys_linux_amd64.s", + 400), + newCall( + "runtime.netpoll", + Args{Values: []Arg{{Value: 0x901b01, IsPtr: true}, {}}}, + "/goroot/src/runtime/netpoll_epoll.go", + 68), + newCall( + "findrunnable", + Args{Values: []Arg{{Value: 0x8012000, IsPtr: true}}}, + "/goroot/src/runtime/proc.c", + 1472), + newCall("schedule", Args{}, "/goroot/src/runtime/proc.c", 1575), + newCall( + "runtime.park_m", + Args{Values: []Arg{{Value: 0x80017a0, IsPtr: true}}}, + "/goroot/src/runtime/proc.c", + 1654), + newCall( + "runtime.mcall", + Args{Values: []Arg{{Value: 0x432684, IsPtr: true}}}, + "/goroot/src/runtime/asm_amd64.s", + 186), }, }, ID: 0, @@ -1026,31 +978,29 @@ err: io.EOF, want: []*Goroutine{ { - Signature: Signature{ - State: "running", - Stack: Stack{ - Calls: []Call{ - newCall( - "github.com/cockroachdb/cockroach/storage/engine._Cfunc_DBIterSeek", - Args{}, - "??", - 0), - newCall( - "gopkg.in/yaml%2ev2.handleErr", - Args{Values: []Arg{{Value: 0x433b20, IsPtr: true}}}, - "/gopath/src/gopkg.in/yaml.v2/yaml.go", - 153), - newCall( - "reflect.Value.assignTo", - Args{Values: []Arg{{Value: 0x570860, IsPtr: true}, {Value: 0x803f3e0, IsPtr: true}, {Value: 0x15}}}, - "/goroot/src/reflect/value.go", - 2125), - newCall( - "main.main", - Args{}, - "/gopath/src/github.com/maruel/panicparse/stack/stack.go", - 428), - }, + State: "running", + Stack: Stack{ + Calls: []Call{ + newCall( + "github.com/cockroachdb/cockroach/storage/engine._Cfunc_DBIterSeek", + Args{}, + "??", + 0), + newCall( + "gopkg.in/yaml%2ev2.handleErr", + Args{Values: []Arg{{Value: 0x433b20, IsPtr: true}}}, + "/gopath/src/gopkg.in/yaml.v2/yaml.go", + 153), + newCall( + "reflect.Value.assignTo", + Args{Values: []Arg{{Value: 0x570860, IsPtr: true}, {Value: 0x803f3e0, IsPtr: true}, {Value: 0x15}}}, + "/goroot/src/reflect/value.go", + 2125), + newCall( + "main.main", + Args{}, + "/gopath/src/github.com/maruel/panicparse/stack/stack.go", + 428), }, }, ID: 1, @@ -1076,31 +1026,29 @@ err: io.EOF, want: []*Goroutine{ { - Signature: Signature{ - State: "running", - Stack: Stack{ - Calls: []Call{ - newCall( - "github.com/cockroachdb/cockroach/storage/engine._Cfunc_DBIterSeek", - Args{}, - "??", - 0), - newCall( - "gopkg.in/yaml%2ev2.handleErr", - Args{Values: []Arg{{Value: 0x433b20, IsPtr: true, IsInaccurate: true}}}, - "/gopath/src/gopkg.in/yaml.v2/yaml.go", - 153), - newCall( - "reflect.Value.assignTo", - Args{Values: []Arg{{Value: 0x570860, IsPtr: true}, {Value: 0x803f3e0, IsPtr: true}, {Value: 0x15}}}, - "/goroot/src/reflect/value.go", - 2125), - newCall( - "main.main", - Args{}, - "/gopath/src/github.com/maruel/panicparse/stack/stack.go", - 428), - }, + State: "running", + Stack: Stack{ + Calls: []Call{ + newCall( + "github.com/cockroachdb/cockroach/storage/engine._Cfunc_DBIterSeek", + Args{}, + "??", + 0), + newCall( + "gopkg.in/yaml%2ev2.handleErr", + Args{Values: []Arg{{Value: 0x433b20, IsPtr: true, IsInaccurate: true}}}, + "/gopath/src/gopkg.in/yaml.v2/yaml.go", + 153), + newCall( + "reflect.Value.assignTo", + Args{Values: []Arg{{Value: 0x570860, IsPtr: true}, {Value: 0x803f3e0, IsPtr: true}, {Value: 0x15}}}, + "/goroot/src/reflect/value.go", + 2125), + newCall( + "main.main", + Args{}, + "/gopath/src/github.com/maruel/panicparse/stack/stack.go", + 428), }, }, ID: 1, @@ -1145,35 +1093,33 @@ err: io.EOF, want: []*Goroutine{ { - Signature: Signature{ - State: "running", - CreatedBy: Stack{ - Calls: []Call{ - newCall( - "testing.(*T).Run", - Args{}, - "/home/maruel/golang/go/src/testing/testing.go", - 916), - }, + State: "running", + CreatedBy: Stack{ + Calls: []Call{ + newCall( + "testing.(*T).Run", + Args{}, + "/home/maruel/golang/go/src/testing/testing.go", + 916), }, - Stack: Stack{ - Calls: []Call{ - newCall( - "foo/bar.TestArchiveFail.func1.2", - Args{}, - "/home/maruel/go/foo/bar_test.go", - 209), - newCall( - "foo/bar.TestArchiveFail", - Args{Values: []Arg{{Value: 0x3382000, Name: "#1", IsPtr: true}}}, - "/home/maruel/go/src/foo/bar_test.go", - 155), - newCall( - "testing.tRunner", - Args{Values: []Arg{{Value: 0x3382000, Name: "#1", IsPtr: true}, {Value: 0x1615bf8, IsPtr: true}}}, - "/home/maruel/golang/go/src/testing/testing.go", - 865), - }, + }, + Stack: Stack{ + Calls: []Call{ + newCall( + "foo/bar.TestArchiveFail.func1.2", + Args{}, + "/home/maruel/go/foo/bar_test.go", + 209), + newCall( + "foo/bar.TestArchiveFail", + Args{Values: []Arg{{Value: 0x3382000, Name: "#1", IsPtr: true}}}, + "/home/maruel/go/src/foo/bar_test.go", + 155), + newCall( + "testing.tRunner", + Args{Values: []Arg{{Value: 0x3382000, Name: "#1", IsPtr: true}, {Value: 0x1615bf8, IsPtr: true}}}, + "/home/maruel/golang/go/src/testing/testing.go", + 865), }, }, ID: 8, @@ -1188,38 +1134,36 @@ prefix: "\nGOTRACEBACK=all\n", want: []*Goroutine{ { - Signature: Signature{ - State: "running", - CreatedBy: Stack{ - Calls: []Call{ - newCall( - "main.panicRace", - Args{}, - "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", - 153, - ), - newCall( - "main.main", - Args{}, - "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", - 54, - ), - }, + State: "running", + CreatedBy: Stack{ + Calls: []Call{ + newCall( + "main.panicRace", + Args{}, + "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", + 153, + ), + newCall( + "main.main", + Args{}, + "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", + 54, + ), }, - Stack: Stack{ - Calls: []Call{ - newCall( - "main.panicDoRaceRead", - Args{}, - "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", - 137, - ), - newCall( - "main.panicRace.func2", - Args{}, - "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", - 154), - }, + }, + Stack: Stack{ + Calls: []Call{ + newCall( + "main.panicDoRaceRead", + Args{}, + "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", + 137, + ), + newCall( + "main.panicRace.func2", + Args{}, + "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", + 154), }, }, ID: 8, @@ -1227,37 +1171,35 @@ RaceAddr: 0xc000014100, }, { - Signature: Signature{ - State: "running", - CreatedBy: Stack{ - Calls: []Call{ - newCall( - "main.panicRace", - Args{}, - "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", - 150, - ), - newCall( - "main.main", - Args{}, - "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", - 54, - ), - }, + State: "running", + CreatedBy: Stack{ + Calls: []Call{ + newCall( + "main.panicRace", + Args{}, + "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", + 150, + ), + newCall( + "main.main", + Args{}, + "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", + 54, + ), }, - Stack: Stack{ - Calls: []Call{ - newCall( - "main.panicDoRaceWrite", - Args{}, - "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", - 132), - newCall( - "main.panicRace.func1", - Args{}, - "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", - 151), - }, + }, + Stack: Stack{ + Calls: []Call{ + newCall( + "main.panicDoRaceWrite", + Args{}, + "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", + 132), + newCall( + "main.panicRace.func1", + Args{}, + "/go/src/github.com/maruel/panicparse/cmd/panic/main.go", + 151), }, }, ID: 7, @@ -1311,7 +1253,6 @@ }, } for i, line := range data { - line := line t.Run(fmt.Sprintf("%d-%s", i, line.name), func(t *testing.T) { t.Parallel() prefix := bytes.Buffer{} @@ -1356,17 +1297,15 @@ } want := []*Goroutine{ { - Signature: Signature{ - State: "running", - Stack: Stack{ - Calls: []Call{ - newCallLocal( - "main.main", - Args{}, - pathJoin(ppDir, "main.go"), - 73, - ), - }, + State: "running", + Stack: Stack{ + Calls: []Call{ + newCallLocal( + "main.main", + Args{}, + pathJoin(ppDir, "main.go"), + 75, + ), }, }, ID: 1, @@ -1386,29 +1325,27 @@ // This is a change detector on internal/main.go. want = []*Goroutine{ { - Signature: Signature{ - State: "running", - Stack: Stack{ - Calls: []Call{ - newCallLocal( - "main.panicint", - Args{Values: []Arg{{Value: 42}}}, - pathJoin(ppDir, "main.go"), - 92, - ), - newCallLocal( - "main.init.func9", - Args{}, - pathJoin(ppDir, "main.go"), - 319, - ), - newCallLocal( - "main.main", - Args{}, - pathJoin(ppDir, "main.go"), - 75, - ), - }, + State: "running", + Stack: Stack{ + Calls: []Call{ + newCallLocal( + "main.panicint", + Args{Values: []Arg{{Value: 42}}}, + pathJoin(ppDir, "main.go"), + 94, + ), + newCallLocal( + "main.init.func9", + Args{}, + pathJoin(ppDir, "main.go"), + 321, + ), + newCallLocal( + "main.main", + Args{}, + pathJoin(ppDir, "main.go"), + 77, + ), }, }, ID: 1, @@ -1429,24 +1366,14 @@ func TestGetGOPATHs(t *testing.T) { // This test cannot run in parallel. - old := os.Getenv("GOPATH") - defer os.Setenv("GOPATH", old) - os.Setenv("GOPATH", "") + t.Setenv("GOPATH", "") if p := getGOPATHs(); len(p) != 1 { // It's the home directory + /go. t.Fatalf("expected only one path: %v", p) } - root, err := os.MkdirTemp("", "stack") - if err != nil { - t.Fatal(err) - } - defer func() { - if err = os.RemoveAll(root); err != nil { - t.Error(err) - } - }() - os.Setenv("GOPATH", filepath.Join(root, "a")+string(filepath.ListSeparator)+filepath.Join(root, "b")+string(filepath.Separator)) + root := t.TempDir() + t.Setenv("GOPATH", filepath.Join(root, "a")+string(filepath.ListSeparator)+filepath.Join(root, "b")+string(filepath.Separator)) if p := getGOPATHs(); len(p) != 2 { t.Fatalf("expected two paths: %v", p) } @@ -1456,20 +1383,11 @@ // of go modules using the "replace" statement. func TestGomoduleComplex(t *testing.T) { // This test cannot run in parallel. - old := os.Getenv("GOPATH") - defer os.Setenv("GOPATH", old) - root, err := os.MkdirTemp("", "stack") - if err != nil { - t.Fatal(err) - } - defer func() { - if err = os.RemoveAll(root); err != nil { - t.Error(err) - } - }() + root := t.TempDir() + var err error - os.Setenv("GOPATH", filepath.Join(root, "go")) - os.Setenv("GO111MODULE", "on") + t.Setenv("GOPATH", filepath.Join(root, "go")) + t.Setenv("GO111MODULE", "on") tree := map[string]string{ "pkg1/go.mod": "module example.com/pkg1\n" + "require (\n" + @@ -1516,7 +1434,7 @@ t.Fatal(err) } - out, err := exec.Command(exe).CombinedOutput() + out, err := exec.CommandContext(t.Context(), exe).CombinedOutput() if err == nil { t.Error("expected failure") } @@ -1536,12 +1454,12 @@ compareString(t, "", string(suffix)) wantGOROOT := "" compareString(t, wantGOROOT, s.RemoteGOROOT) - compareString(t, build.Default.GOROOT, strings.Replace(s.LocalGOROOT, "/", pathSeparator, -1)) + compareString(t, build.Default.GOROOT, strings.ReplaceAll(s.LocalGOROOT, "/", pathSeparator)) rootRemote := root if runtime.GOOS == "windows" { // On Windows, we must make the path to be POSIX style. - rootRemote = strings.Replace(root, pathSeparator, "/", -1) + rootRemote = strings.ReplaceAll(root, pathSeparator, "/") } rootLocal := rootRemote wantGOPATHs := map[string]string{ @@ -1563,62 +1481,60 @@ want := []*Goroutine{ { - Signature: Signature{ - State: "running", - Stack: Stack{ - Calls: []Call{ - { - Func: newFunc("example.com/pkg3.Die"), - Args: Args{Elided: true}, - RemoteSrcPath: pathJoin(rootRemote, "go", "src", "example.com", "pkg3", "src3.go"), - Line: 2, - SrcName: "src3.go", - DirSrc: "pkg3/src3.go", - LocalSrcPath: pathJoin(rootLocal, "go", "src", "example.com", "pkg3", "src3.go"), - RelSrcPath: "example.com/pkg3/src3.go", - ImportPath: "example.com/pkg3", - Location: GOPATH, - }, - { - Func: newFunc("example.com/pkg2.CallDie"), - Args: Args{Elided: true}, - RemoteSrcPath: pathJoin(rootRemote, "pkg2", "src2.go"), - Line: 3, - SrcName: "src2.go", - DirSrc: "pkg2/src2.go", - // Since this was found locally as a go module using the remote - // path, this is correct, even if confusing. - LocalSrcPath: pathJoin(rootRemote, "pkg2", "src2.go"), - RelSrcPath: "src2.go", - ImportPath: "example.com/pkg2", - Location: GoMod, - }, - { - Func: newFunc("example.com/pkg1/internal.CallCallDie"), - RemoteSrcPath: pathJoin(rootRemote, "pkg1", "internal", "int.go"), - Line: 2, - SrcName: "int.go", - DirSrc: "internal/int.go", - // Since this was found locally as a go module using the remote - // path, this is correct, even if confusing. - LocalSrcPath: pathJoin(rootRemote, "pkg1", "internal", "int.go"), - RelSrcPath: "internal/int.go", - ImportPath: "example.com/pkg1/internal", - Location: GoMod, - }, - { - Func: newFunc("main.main"), - RemoteSrcPath: pathJoin(rootRemote, "pkg1", "cmd", "main.go"), - Line: 4, - SrcName: "main.go", - DirSrc: "cmd/main.go", - // Since this was found locally as a go module using the remote - // path, this is correct, even if confusing. - LocalSrcPath: pathJoin(rootRemote, "pkg1", "cmd", "main.go"), - RelSrcPath: "cmd/main.go", - ImportPath: "example.com/pkg1/cmd", - Location: GoMod, - }, + State: "running", + Stack: Stack{ + Calls: []Call{ + { + Func: newFunc("example.com/pkg3.Die"), + Args: Args{Elided: true}, + RemoteSrcPath: pathJoin(rootRemote, "go", "src", "example.com", "pkg3", "src3.go"), + Line: 2, + SrcName: "src3.go", + DirSrc: "pkg3/src3.go", + LocalSrcPath: pathJoin(rootLocal, "go", "src", "example.com", "pkg3", "src3.go"), + RelSrcPath: "example.com/pkg3/src3.go", + ImportPath: "example.com/pkg3", + Location: GOPATH, + }, + { + Func: newFunc("example.com/pkg2.CallDie"), + Args: Args{Elided: true}, + RemoteSrcPath: pathJoin(rootRemote, "pkg2", "src2.go"), + Line: 3, + SrcName: "src2.go", + DirSrc: "pkg2/src2.go", + // Since this was found locally as a go module using the remote + // path, this is correct, even if confusing. + LocalSrcPath: pathJoin(rootRemote, "pkg2", "src2.go"), + RelSrcPath: "src2.go", + ImportPath: "example.com/pkg2", + Location: GoMod, + }, + { + Func: newFunc("example.com/pkg1/internal.CallCallDie"), + RemoteSrcPath: pathJoin(rootRemote, "pkg1", "internal", "int.go"), + Line: 2, + SrcName: "int.go", + DirSrc: "internal/int.go", + // Since this was found locally as a go module using the remote + // path, this is correct, even if confusing. + LocalSrcPath: pathJoin(rootRemote, "pkg1", "internal", "int.go"), + RelSrcPath: "internal/int.go", + ImportPath: "example.com/pkg1/internal", + Location: GoMod, + }, + { + Func: newFunc("main.main"), + RemoteSrcPath: pathJoin(rootRemote, "pkg1", "cmd", "main.go"), + Line: 4, + SrcName: "main.go", + DirSrc: "cmd/main.go", + // Since this was found locally as a go module using the remote + // path, this is correct, even if confusing. + LocalSrcPath: pathJoin(rootRemote, "pkg1", "cmd", "main.go"), + RelSrcPath: "cmd/main.go", + ImportPath: "example.com/pkg1/cmd", + Location: GoMod, }, }, }, @@ -1631,22 +1547,15 @@ func TestGoRun(t *testing.T) { t.Parallel() - root, err := os.MkdirTemp("", "stack") - if err != nil { - t.Fatal(err) - } - defer func() { - if err = os.RemoveAll(root); err != nil { - t.Error(err) - } - }() + root := t.TempDir() + var err error p := filepath.Join(root, "main.go") content := "package main\nfunc main() { panic(42) }\n" if err = os.WriteFile(p, []byte(content), 0600); err != nil { t.Fatal(err) } - c := exec.Command("go", "run", p) + c := exec.CommandContext(t.Context(), "go", "run", p) out, err := c.CombinedOutput() if err == nil { t.Fatal("expected failure") @@ -1661,31 +1570,29 @@ } if runtime.GOOS == "windows" { // On Windows, we must make the path to be POSIX style. - p = strings.Replace(p, pathSeparator, "/", -1) + p = strings.ReplaceAll(p, pathSeparator, "/") } want := []*Goroutine{ { - Signature: Signature{ - State: "running", - Stack: Stack{ - Calls: []Call{ - { - Func: Func{ - Complete: "main.main", - ImportPath: "main", - DirName: "main", - Name: "main", - IsExported: true, - IsPkgMain: true, - }, - RemoteSrcPath: p, - Line: 2, - SrcName: "main.go", - DirSrc: path.Base(path.Dir(p)) + "/main.go", - ImportPath: "main", - Location: LocationUnknown, + State: "running", + Stack: Stack{ + Calls: []Call{ + { + Func: Func{ + Complete: "main.main", + ImportPath: "main", + DirName: "main", + Name: "main", + IsExported: true, + IsPkgMain: true, }, + RemoteSrcPath: p, + Line: 2, + SrcName: "main.go", + DirSrc: path.Base(path.Dir(p)) + "/main.go", + ImportPath: "main", + Location: LocationUnknown, }, }, }, @@ -1742,13 +1649,11 @@ } for cmd, data := range cmds { - cmd := cmd - data := data t.Run(cmd, func(t *testing.T) { t.Parallel() prefix := bytes.Buffer{} s, suffix, err := ScanSnapshot(bytes.NewReader(data), &prefix, defaultOpts()) - if err != nil && err != io.EOF { + if err != nil && !errors.Is(err, io.EOF) { t.Fatal(err) } if s == nil { @@ -1782,21 +1687,19 @@ } want := []*Goroutine{ { - Signature: Signature{ - State: "running", - Stack: Stack{ - Calls: []Call{ - newCallLocal( - "main.panicArgsElided", - Args{ - Values: []Arg{{Value: 1}, {Value: 2}, {Value: 3}, {Value: 4}, {Value: 5}, {Value: 6}, {Value: 7}, {Value: 8}, {Value: 9}, {Value: 10}}, - Elided: true, - }, - pathJoin(ppDir, "main.go"), - 58), - newCallLocal("main.init.func1", Args{}, pathJoin(ppDir, "main.go"), 134), - newCallLocal("main.main", Args{}, pathJoin(ppDir, "main.go"), 340), - }, + State: "running", + Stack: Stack{ + Calls: []Call{ + newCallLocal( + "main.panicArgsElided", + Args{ + Values: []Arg{{Value: 1}, {Value: 2}, {Value: 3}, {Value: 4}, {Value: 5}, {Value: 6}, {Value: 7}, {Value: 8}, {Value: 9}, {Value: 10}}, + Elided: true, + }, + pathJoin(ppDir, "main.go"), + 58), + newCallLocal("main.init.func1", Args{}, pathJoin(ppDir, "main.go"), 134), + newCallLocal("main.main", Args{}, pathJoin(ppDir, "main.go"), 340), }, }, ID: 1, @@ -1819,23 +1722,21 @@ } want := []*Goroutine{ { - Signature: Signature{ - State: "running", - Stack: Stack{ - Calls: []Call{ - newCallLocal( - // This is important to note here that the Go runtime prints out - // the package path, and not the package name. - // - // Here the package name is "correct". There is no way to deduce - // this from the stack trace. - "github.com/maruel/panicparse"+ver+"/cmd/panic/internal/incorrect.Panic", - Args{}, - pathJoin(ppDir, "internal", "incorrect", "correct.go"), - 7), - newCallLocal("main.init.func20", Args{}, pathJoin(ppDir, "main.go"), 314), - newCallLocal("main.main", Args{}, pathJoin(ppDir, "main.go"), 340), - }, + State: "running", + Stack: Stack{ + Calls: []Call{ + newCallLocal( + // This is important to note here that the Go runtime prints out + // the package path, and not the package name. + // + // Here the package name is "correct". There is no way to deduce + // this from the stack trace. + "github.com/maruel/panicparse"+ver+"/cmd/panic/internal/incorrect.Panic", + Args{}, + pathJoin(ppDir, "internal", "incorrect", "correct.go"), + 7), + newCallLocal("main.init.func20", Args{}, pathJoin(ppDir, "main.go"), 314), + newCallLocal("main.main", Args{}, pathJoin(ppDir, "main.go"), 340), }, }, ID: 1, @@ -1854,73 +1755,69 @@ } want := []*Goroutine{ { - Signature: Signature{ - State: "running", - CreatedBy: Stack{ - Calls: []Call{ - newCallLocal( - "main.panicRace", - Args{}, - pathJoin(ppDir, "main.go"), - 151, - ), - newCallLocal( - "main.main", - Args{}, - pathJoin(ppDir, "main.go"), - 75, - ), - }, + State: "running", + CreatedBy: Stack{ + Calls: []Call{ + newCallLocal( + "main.panicRace", + Args{}, + pathJoin(ppDir, "main.go"), + 151, + ), + newCallLocal( + "main.main", + Args{}, + pathJoin(ppDir, "main.go"), + 77, + ), }, - Stack: Stack{ - Calls: []Call{ - newCallLocal( - "main.panicDoRaceRead", - Args{}, - pathJoin(ppDir, "main.go"), - 150), - newCallLocal( - "main.panicRace.func2", - Args{}, - pathJoin(ppDir, "main.go"), - 135), - }, + }, + Stack: Stack{ + Calls: []Call{ + newCallLocal( + "main.panicDoRaceRead", + Args{}, + pathJoin(ppDir, "main.go"), + 150), + newCallLocal( + "main.panicRace.func2", + Args{}, + pathJoin(ppDir, "main.go"), + 135), }, }, RaceAddr: pointer, }, { - Signature: Signature{ - State: "running", - CreatedBy: Stack{ - Calls: []Call{ - newCallLocal( - "main.panicRace", - Args{}, - pathJoin(ppDir, "main.go"), - 151, - ), - newCallLocal( - "main.main", - Args{}, - pathJoin(ppDir, "main.go"), - 75, - ), - }, + State: "running", + CreatedBy: Stack{ + Calls: []Call{ + newCallLocal( + "main.panicRace", + Args{}, + pathJoin(ppDir, "main.go"), + 151, + ), + newCallLocal( + "main.main", + Args{}, + pathJoin(ppDir, "main.go"), + 77, + ), }, - Stack: Stack{ - Calls: []Call{ - newCallLocal( - "main.panicDoRaceWrite", - Args{}, - pathJoin(ppDir, "main.go"), - 145), - newCallLocal( - "main.panicRace.func1", - Args{}, - pathJoin(ppDir, "main.go"), - 132), - }, + }, + Stack: Stack{ + Calls: []Call{ + newCallLocal( + "main.panicDoRaceWrite", + Args{}, + pathJoin(ppDir, "main.go"), + 145), + newCallLocal( + "main.panicRace.func1", + Args{}, + pathJoin(ppDir, "main.go"), + 132), }, }, RaceWrite: true, @@ -1956,20 +1853,18 @@ } want := []*Goroutine{ { - Signature: Signature{ - State: "running", - Stack: Stack{ - Calls: []Call{ - newCallLocal( - "main.panicstr", - Args{Values: []Arg{{IsAggregate: true, Fields: Args{ - Values: []Arg{{Value: 0x123456, IsPtr: true}, {Value: 4}}, - }}}}, - pathJoin(ppDir, "main.go"), - 50), - newCallLocal("main.init.func19", Args{}, pathJoin(ppDir, "main.go"), 307), - newCallLocal("main.main", Args{}, pathJoin(ppDir, "main.go"), 340), - }, + State: "running", + Stack: Stack{ + Calls: []Call{ + newCallLocal( + "main.panicstr", + Args{Values: []Arg{{IsAggregate: true, Fields: Args{ + Values: []Arg{{Value: 0x123456, IsPtr: true}, {Value: 4}}, + }}}}, + pathJoin(ppDir, "main.go"), + 50), + newCallLocal("main.init.func19", Args{}, pathJoin(ppDir, "main.go"), 307), + newCallLocal("main.main", Args{}, pathJoin(ppDir, "main.go"), 340), }, }, ID: 1, @@ -1992,23 +1887,21 @@ } want := []*Goroutine{ { - Signature: Signature{ - State: "running", - Stack: Stack{ - Calls: []Call{ - newCallLocal( - // This is important to note here the inconsistency in the Go - // runtime stack generator. The path is escaped, but symbols are - // not. - "github.com/maruel/panicparse"+ver+"/cmd/panic/internal/utf8.(*Strùct).Pànic", - Args{Values: []Arg{{Value: 1, IsInaccurate: true}}}, - // See TestCallUTF8 in stack_test.go for exercising the methods on - // Call in this situation. - pathJoin(ppDir, "internal", "utf8", "utf8.go"), - 10), - newCallLocal("main.init.func21", Args{}, pathJoin(ppDir, "main.go"), 322), - newCallLocal("main.main", Args{}, pathJoin(ppDir, "main.go"), 340), - }, + State: "running", + Stack: Stack{ + Calls: []Call{ + newCallLocal( + // This is important to note here the inconsistency in the Go + // runtime stack generator. The path is escaped, but symbols are + // not. + "github.com/maruel/panicparse"+ver+"/cmd/panic/internal/utf8.(*Strùct).Pànic", + Args{Values: []Arg{{Value: 1, IsInaccurate: true}}}, + // See TestCallUTF8 in stack_test.go for exercising the methods on + // Call in this situation. + pathJoin(ppDir, "internal", "utf8", "utf8.go"), + 10), + newCallLocal("main.init.func21", Args{}, pathJoin(ppDir, "main.go"), 322), + newCallLocal("main.main", Args{}, pathJoin(ppDir, "main.go"), 340), }, }, ID: 1, @@ -2030,7 +1923,7 @@ } prefix := bytes.Buffer{} s, suffix, err := ScanSnapshot(bytes.NewReader(internaltest.PanicwebOutput()), &prefix, defaultOpts()) - if err != io.EOF { + if !errors.Is(err, io.EOF) { t.Fatal(err) } if s == nil { @@ -2044,7 +1937,7 @@ if !s.guessPaths() { t.Error("expected success") } - if s.RemoteGOROOT != strings.Replace(build.Default.GOROOT, "\\", "/", -1) { + if s.RemoteGOROOT != strings.ReplaceAll(build.Default.GOROOT, "\\", "/") { t.Fatalf("RemoteGOROOT mismatch; want:%q got:%q", build.Default.GOROOT, s.RemoteGOROOT) } if got := len(s.Goroutines); got < 30 { @@ -2054,8 +1947,9 @@ // OSes and Go versions, but to find some of the expected signatures. pwebDir := pathJoin(getPanicParseDir(t), "cmd", "panicweb") // Reduce the goroutines and categorize the signatures. - var types []panicwebSignatureType - for _, b := range s.Aggregate(AnyPointer).Buckets { + buckets := s.Aggregate(AnyPointer).Buckets + types := make([]panicwebSignatureType, 0, len(buckets)) + for _, b := range buckets { types = append(types, identifyPanicwebSignature(t, b, pwebDir)) } // Count the expected types. @@ -2092,7 +1986,7 @@ t.Fatal(err) } // Our internal functions work with '/' as path separator. - parts := splitPath(strings.Replace(pwd, "\\", "/", -1)) + parts := splitPath(strings.ReplaceAll(pwd, "\\", "/")) gmc := gomodCache{} root, importPath := gmc.isGoModule(parts) if want := strings.Join(parts[:len(parts)-1], "/"); want != root { @@ -2143,8 +2037,8 @@ {[]byte("{{}}a{{}}"), []byte("}}a{{"), 2, 2}, } for i, line := range data { - line := line t.Run(fmt.Sprintf("%d-%s", i, line.input), func(t *testing.T) { + t.Parallel() gotOpened, got, gotClosed := trimCurlyBrackets(line.input) if !bytes.Equal(line.want, got) { t.Errorf("want %s, got %s", line.want, got) @@ -2168,9 +2062,9 @@ data := internaltest.StaticPanicwebOutput() opts := defaultOpts() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { s, _, err := ScanSnapshot(bytes.NewReader(data), io.Discard, opts) - if err != io.EOF { + if !errors.Is(err, io.EOF) { b.Fatal(err) } if s == nil { @@ -2184,9 +2078,9 @@ data := internaltest.StaticPanicwebOutput() opts := defaultOpts() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { s, _, err := ScanSnapshot(bytes.NewReader(data), io.Discard, opts) - if err != io.EOF { + if !errors.Is(err, io.EOF) { b.Fatal(err) } if s == nil { @@ -2210,7 +2104,7 @@ opts := defaultOpts() b.ResetTimer() s, suffix, err := ScanSnapshot(r, &prefix, opts) - if err != io.EOF { + if !errors.Is(err, io.EOF) { b.Fatal(err) } if s != nil { @@ -2331,7 +2225,7 @@ t.Fatal("expected Locked") } // This is a change detector on internal/main.go. - want := Stack{Calls: []Call{newCallLocal("main.main in goroutine 1", Args{}, pathJoin(pwebDir, "main.go"), 146)}} + want := Stack{Calls: []Call{newCallLocal("main.main in goroutine 1", Args{}, pathJoin(pwebDir, "main.go"), 148)}} compareStacks(t, &b.Signature.CreatedBy, &want) for i := range b.Signature.Stack.Calls { if strings.HasPrefix(b.Signature.Stack.Calls[i].ImportPath, "github.com/mattn/go-colorable") { @@ -2398,7 +2292,7 @@ } if expectVersion { // Assert that it's using v0.1.0 format. - ver := strings.SplitN(b.Stack.Calls[callIdx].RelSrcPath[len(prefix):], "/", 2)[0] + ver, _, _ := strings.Cut(b.Stack.Calls[callIdx].RelSrcPath[len(prefix):], "/") re := regexp.MustCompile(`^v\d+\.\d+\.\d+$`) if !re.MatchString(ver) { t.Fatalf("unexpected version string %q", ver) @@ -2465,12 +2359,12 @@ } // "/" is used even on Windows in the stack trace, return in this format to // simply our life. - return strings.Replace(filepath.Dir(thisDir), "\\", "/", -1) + return strings.ReplaceAll(filepath.Dir(thisDir), "\\", "/") } func createTree(t *testing.T, root string, tree map[string]string) { for path, content := range tree { - p := filepath.Join(root, strings.Replace(path, "/", pathSeparator, -1)) + p := filepath.Join(root, strings.ReplaceAll(path, "/", pathSeparator)) b := filepath.Dir(p) if err := os.MkdirAll(b, 0700); err != nil { t.Fatal(err) @@ -2483,6 +2377,7 @@ // TestScanSnapshotToolchainRoots keeps standard-library files separate from cached modules. func TestScanSnapshotToolchainRoots(t *testing.T) { + t.Parallel() packages := []struct{ pkg, file, fn string }{ {"runtime", "proc.go", "gopark"}, {"net/http", "server.go", "(*Server).Serve"}, @@ -2502,6 +2397,7 @@ continue } t.Run(fmt.Sprintf("%s/%s/%s/stdlib=%t/module=%t", line.pkg, remoteRoot, prefix, stdlib, withModule), func(t *testing.T) { + t.Parallel() root := filepath.ToSlash(t.TempDir()) gopath := pathJoin(root, "gopath") goroot := pathJoin(root, "go") @@ -2529,7 +2425,7 @@ opts.LocalGOPATHs = []string{gopath} opts.AnalyzeSources = false s, _, err := ScanSnapshot(strings.NewReader(input), io.Discard, opts) - if err != io.EOF { + if !errors.Is(err, io.EOF) { t.Fatal(err) } wantRoot := ""
diff --git a/stack/html.go b/stack/html.go index 02b76f2..b7b936d 100644 --- a/stack/html.go +++ b/stack/html.go
@@ -24,7 +24,7 @@ // // Use footer to add custom HTML at the bottom of the page. func (a *Aggregated) ToHTML(w io.Writer, footer template.HTML) error { - data := map[string]interface{}{ + data := map[string]any{ "Aggregated": a, "Footer": footer, "Snapshot": a.Snapshot, @@ -36,7 +36,7 @@ // // Use footer to add custom HTML at the bottom of the page. func (s *Snapshot) ToHTML(w io.Writer, footer template.HTML) error { - data := map[string]interface{}{ + data := map[string]any{ "Footer": footer, "Snapshot": s, } @@ -45,7 +45,7 @@ // Private stuff. -func toHTML(w io.Writer, data map[string]interface{}) error { +func toHTML(w io.Writer, data map[string]any) error { m := template.FuncMap{ "funcClass": funcClass, "minus": minus, @@ -93,7 +93,7 @@ if ip == "" { return "" } - url := template.URL("") + var url template.URL if c.Location == Stdlib { // This always links to the latest release, past releases are not online. // That's somewhat unfortunate. @@ -193,11 +193,11 @@ if c.LocalSrcPath != "" { /* #nosec G203 */ - return template.URL("file:///" + escape(c.LocalSrcPath)), template.URL(tag) + return "file:///" + escape(c.LocalSrcPath), template.URL(tag) } if c.RemoteSrcPath != "" { /* #nosec G203 */ - return template.URL("file:///" + escape(c.RemoteSrcPath)), template.URL(tag) + return "file:///" + escape(c.RemoteSrcPath), template.URL(tag) } return "", "" } @@ -210,26 +210,26 @@ return parts[0], parts[1] } -// "v0.0.0-20200223170610-d5e6a3e2c0ae" +// "v0.0.0-20200223170610-d5e6a3e2c0ae". var reVersion = regexp.MustCompile(`v\d+\.\d+\.\d+\-\d+\-([a-f0-9]+)`) func splitTag(s string) (string, string, template.URL) { // Default to branch master for non-versioned dependencies. It's not // optimal but it's better than nothing? // TODO(maruel): Replace with HEAD. - i := strings.IndexByte(s, '@') - if i == -1 { + before, after, ok := strings.Cut(s, "@") + if !ok { // No tag was found. return s, "master", "master" } // We got a versioned go module. - tag := s[i+1:] + tag := after srcTag := tag if m := reVersion.FindStringSubmatch(tag); len(m) != 0 { srcTag = m[1] } /* #nosec G203 */ - return s[:i], url.QueryEscape(srcTag), template.URL(url.QueryEscape(tag)) + return before, url.QueryEscape(srcTag), template.URL(url.QueryEscape(tag)) } // symbol is the hashtag to use to refer to the symbol when looking at
diff --git a/stack/html_test.go b/stack/html_test.go index 85cda9a..4112532 100644 --- a/stack/html_test.go +++ b/stack/html_test.go
@@ -8,6 +8,7 @@ import ( "bytes" + "errors" "fmt" "go/build" "html/template" @@ -16,6 +17,7 @@ "os" "regexp" "runtime" + "strconv" "strings" "testing" @@ -185,7 +187,6 @@ }, } for i, line := range data { - line := line t.Run(fmt.Sprintf("%d-%s", i, line.name), func(t *testing.T) { t.Parallel() url, branch := getSrcBranchURL(&line.c) @@ -232,8 +233,8 @@ }, } for i, line := range data { - line := line - t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + t.Run(strconv.Itoa(i), func(t *testing.T) { + t.Parallel() if s := symbol(&line.in); s != line.want { t.Fatalf("%q != %q", s, line.want) } @@ -268,7 +269,7 @@ func BenchmarkAggregated_ToHTML(b *testing.B) { b.ReportAllocs() s, _, err := ScanSnapshot(bytes.NewReader(internaltest.StaticPanicwebOutput()), io.Discard, DefaultOpts()) - if err != io.EOF { + if !errors.Is(err, io.EOF) { b.Fatal(err) } if s == nil { @@ -276,7 +277,7 @@ } a := s.Aggregate(AnyPointer) b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { if err := a.ToHTML(io.Discard, ""); err != nil { b.Fatal(err) } @@ -309,54 +310,50 @@ }, Buckets: []*Bucket{ { - Signature: Signature{ - State: "chan receive", - Stack: Stack{ - Calls: []Call{ - newCall( - "main.func·001", - Args{Values: []Arg{{Value: 0x11000000}, {Value: 2}}}, - "/gopath/src/github.com/maruel/panicparse/stack/stack.go", - 72), - { - Func: newFunc("sliceInternal"), - Args: Args{Values: []Arg{{Value: 0x11000000}, {Value: 2}}}, - RemoteSrcPath: "/golang/src/sort/slices.go", - Line: 72, - Location: Stdlib, - }, - { - Func: newFunc("Slice"), - Args: Args{Values: []Arg{{Value: 0x11000000}, {Value: 2}}}, - RemoteSrcPath: "/golang/src/sort/slices.go", - Line: 72, - Location: Stdlib, - }, - newCall( - "DoStuff", - Args{Values: []Arg{{Value: 0x11000000}, {Value: 2}}}, - "/gopath/src/foo/bar.go", - 72), - newCall( - "doStuffInternal", - Args{ - Values: []Arg{{Value: 0x11000000}, {Value: 2}}, - Elided: true, - }, - "/gopath/src/foo/bar.go", - 72), + State: "chan receive", + Stack: Stack{ + Calls: []Call{ + newCall( + "main.func·001", + Args{Values: []Arg{{Value: 0x11000000}, {Value: 2}}}, + "/gopath/src/github.com/maruel/panicparse/stack/stack.go", + 72), + { + Func: newFunc("sliceInternal"), + Args: Args{Values: []Arg{{Value: 0x11000000}, {Value: 2}}}, + RemoteSrcPath: "/golang/src/sort/slices.go", + Line: 72, + Location: Stdlib, }, + { + Func: newFunc("Slice"), + Args: Args{Values: []Arg{{Value: 0x11000000}, {Value: 2}}}, + RemoteSrcPath: "/golang/src/sort/slices.go", + Line: 72, + Location: Stdlib, + }, + newCall( + "DoStuff", + Args{Values: []Arg{{Value: 0x11000000}, {Value: 2}}}, + "/gopath/src/foo/bar.go", + 72), + newCall( + "doStuffInternal", + Args{ + Values: []Arg{{Value: 0x11000000}, {Value: 2}}, + Elided: true, + }, + "/gopath/src/foo/bar.go", + 72), }, }, IDs: []int{1, 2}, First: true, }, { - IDs: []int{3}, - Signature: Signature{ - State: "running", - Stack: Stack{Elided: true}, - }, + IDs: []int{3}, + State: "running", + Stack: Stack{Elided: true}, }, }, }
diff --git a/stack/reader.go b/stack/reader.go index b7fe693..5409e8f 100644 --- a/stack/reader.go +++ b/stack/reader.go
@@ -91,7 +91,7 @@ var d []byte for { f, err := r.readSlice() - if err != errBufferFull { + if !errors.Is(err, errBufferFull) { if d == nil { return f, err }
diff --git a/stack/source.go b/stack/source.go index 83071c6..19f029d 100644 --- a/stack/source.go +++ b/stack/source.go
@@ -39,7 +39,7 @@ continue } if err1 := c.loadFile(g.Stack.Calls[i].LocalSrcPath); err1 != nil { - //log.Printf("%s", err) + // log.Printf("%s", err) err = err1 } if p := c.parsed[call.LocalSrcPath]; p != nil { @@ -113,7 +113,7 @@ // getFuncAST gets the callee site function AST representation for the code // inside the function f at line l. -func (p *parsedFile) getFuncAST(f string, l int) (d *ast.FuncDecl, err error) { +func (p *parsedFile) getFuncAST(_ string, l int) (d *ast.FuncDecl, err error) { if len(p.lineToByteOffset) <= l { // The line number in the stack trace line does not exist in the file. That // can only mean that the sources on disk do not match the sources used to @@ -146,14 +146,14 @@ // be edge cases, like: // a = A{Foo(), Bar()} d = lastFunc - //p.processNode(call, n) + // p.processNode(call, n) return false } else if f, ok := n.(*ast.FuncDecl); ok { lastFunc = f } return true }) - return + return d, err } func name(n ast.Node) string { @@ -201,7 +201,7 @@ case *ast.MapType: return fmt.Sprintf("map[%s]%s", name(arg.Key), name(arg.Value)), false case *ast.ChanType: - return fmt.Sprintf("chan %s", name(arg.Value)), false + return "chan " + name(arg.Value), false default: // TODO(maruel): Implement anything missing. return "<unknown>", false @@ -231,7 +231,7 @@ if mult == 0 { mult = 1 } - for i := 0; i < mult; i++ { + for range mult { types = append(types, t) } } @@ -340,9 +340,10 @@ }) str = fmt.Sprintf("%s(%s, len=%s)", t, name, lenStr) default: - if strings.HasPrefix(t, "*") { + switch { + case strings.HasPrefix(t, "*"): str = fmt.Sprintf("%s(%s)", t, popName()) - } else if strings.HasPrefix(t, "[]") { + case strings.HasPrefix(t, "[]"): name := popName() lenStr := popFmt(func(v uint64) string { return strconv.FormatUint(v, 10) @@ -351,13 +352,13 @@ return strconv.FormatUint(v, 10) }) str = fmt.Sprintf("%s(%s len=%s cap=%s)", t, name, lenStr, capStr) - } else { + default: if i < len(call.Args.Values) && call.Args.Values[i].IsAggregate { // If top-level argument is an aggregate-type, include each // of its sub-arguments. v := &call.Args.Values[i].Fields var fields []string - v.walk(func(arg *Arg) { + v.walk(func(_ *Arg) { fields = append(fields, popName()) }) if v.Elided {
diff --git a/stack/source_test.go b/stack/source_test.go index d107cf2..e927a86 100644 --- a/stack/source_test.go +++ b/stack/source_test.go
@@ -29,7 +29,7 @@ // goarchList is a list of GOARCH values. type goarchList []string -var allPlatforms goarchList = nil +var allPlatforms goarchList func TestAugment(t *testing.T) { t.Parallel() @@ -1350,8 +1350,6 @@ } for i, line := range data { - i := i - line := line t.Run(fmt.Sprintf("%d-%s", i, line.name), func(t *testing.T) { t.Parallel() for _, arch := range line.archBlock { @@ -1372,22 +1370,14 @@ input := strings.Join(lines, "\n") // Create one temporary directory by subtest. - root, err := os.MkdirTemp("", "stack") - if err != nil { - t.Fatalf("failed to create temporary directory: %v", err) - } - defer func() { - if err2 := os.RemoveAll(root); err2 != nil { - t.Fatalf("failed to remove temporary directory %q: %v", root, err2) - } - }() + root := t.TempDir() main := filepath.Join(root, "main.go") if err := os.WriteFile(main, []byte(input), 0500); err != nil { t.Fatalf("failed to write %q: %v", main, err) } if runtime.GOOS == "windows" { - root = strings.Replace(root, pathSeparator, "/", -1) + root = strings.ReplaceAll(root, pathSeparator, "/") } const prefix = "/root" for j, c := range line.want.Calls { @@ -1443,7 +1433,7 @@ t.Errorf("augment() returned %v", err) } got := s.Goroutines[0].Signature.Stack - zapPointers(t, &want, &got) + zapPointers(&want, &got) // With a non-pointer method, elided argument can be shown. It's only for // test case "non-pointer method". @@ -1467,15 +1457,7 @@ func TestAugmentErr(t *testing.T) { t.Parallel() - root, err := os.MkdirTemp("", "stack") - if err != nil { - t.Fatalf("failed to create temporary directory: %v", err) - } - defer func() { - if err2 := os.RemoveAll(root); err2 != nil { - t.Fatalf("failed to remove temporary directory %q: %v", root, err2) - } - }() + root := t.TempDir() tree := map[string]string{ "bad.go": "bad content", @@ -1558,17 +1540,17 @@ } for i, line := range data { - line := line t.Run(fmt.Sprintf("%d-%s", i, line.name), func(t *testing.T) { + t.Parallel() l := line.line if l == 0 { l = 1 } s := Snapshot{ Goroutines: []*Goroutine{ - {Signature: Signature{Stack: Stack{Calls: []Call{ + {Stack: Stack{Calls: []Call{ {LocalSrcPath: filepath.Join(root, line.src), Args: line.args, Line: l}, - }}}}, + }}}, }} if err := s.augment(); (line.errRe == "") != (err == nil) { t.Fatalf("want: %q; got: %q", line.errRe, err) @@ -1584,6 +1566,7 @@ } func TestLineToByteOffsets(t *testing.T) { + t.Parallel() src := "\n\n\n" want := []int{0, 0, 1, 2, 3} if diff := cmp.Diff(want, lineToByteOffsets([]byte(src))); diff != "" { @@ -1623,7 +1606,7 @@ // Disable both optimization (-N) and inlining (-l). args = append(args, "-gcflags", "-N -l") } - cmd := exec.Command("go", append(args, main)...) + cmd := exec.CommandContext(t.Context(), "go", append(args, main)...) // Use the Go 1.4 compatible format. cmd.Env = overrideEnv(os.Environ(), "GOTRACEBACK", "1") out, err := cmd.CombinedOutput() @@ -1635,7 +1618,7 @@ } // zapPointers zaps out pointers in got. -func zapPointers(t *testing.T, want, got *Stack) { +func zapPointers(want, got *Stack) { for i := range got.Calls { if i >= len(want.Calls) { // When using GOTRACEBACK=2, it'll include runtime.main() and @@ -1645,22 +1628,22 @@ break } gotArgs := got.Calls[i].Args - ptrsToReplace := zapPointersInArgs(t, want.Calls[i].Args, gotArgs) + ptrsToReplace := zapPointersInArgs(want.Calls[i].Args, gotArgs) for _, ptr := range ptrsToReplace { for k := range gotArgs.Processed { - gotArgs.Processed[k] = strings.Replace(gotArgs.Processed[k], ptr, pointerStr, -1) + gotArgs.Processed[k] = strings.ReplaceAll(gotArgs.Processed[k], ptr, pointerStr) } } } } -func zapPointersInArgs(t *testing.T, want, got Args) (ptrs []string) { +func zapPointersInArgs(want, got Args) (ptrs []string) { for j := range got.Values { if j >= len(want.Values) { break } if want.Values[j].IsAggregate && got.Values[j].IsAggregate { - ptrs = append(ptrs, zapPointersInArgs(t, want.Values[j].Fields, got.Values[j].Fields)...) + ptrs = append(ptrs, zapPointersInArgs(want.Values[j].Fields, got.Values[j].Fields)...) } else if want.Values[j].IsPtr && got.Values[j].IsPtr { // Record the existing pointer value and then replace. ptrs = append(ptrs, fmt.Sprintf("0x%x", got.Values[j].Value)) @@ -1672,6 +1655,7 @@ // TestAugmentCall covers signed stack words and unused high bits of narrow values. func TestAugmentCall(t *testing.T) { + t.Parallel() wantInt := "1311768467015204864" if strconv.IntSize == 32 { wantInt = "-2147483648" @@ -1697,12 +1681,17 @@ } for i, line := range data { t.Run(fmt.Sprintf("%s/%d", line.typ, i), func(t *testing.T) { + t.Parallel() f, err := parser.ParseFile(token.NewFileSet(), "args.go", "package main; func f(v "+line.typ+") {}", 0) if err != nil { t.Fatal(err) } call := Call{Args: Args{Values: []Arg{{Value: line.word}}}} - augmentCall(&call, f.Decls[0].(*ast.FuncDecl)) + decl, ok := f.Decls[0].(*ast.FuncDecl) + if !ok { + t.Fatal("expected function declaration") + } + augmentCall(&call, decl) if diff := cmp.Diff([]string{line.want}, call.Args.Processed); diff != "" { t.Fatalf("decoded argument (-want +got):\n%s", diff) }
diff --git a/stack/stack.go b/stack/stack.go index 4d936bd..5254327 100644 --- a/stack/stack.go +++ b/stack/stack.go
@@ -56,7 +56,7 @@ // - foo (C code) // // The function is optimized to reduce its memory usage. - endPkg := 0 + var endPkg int if lastSlash := strings.LastIndexByte(raw, '/'); lastSlash != -1 { // Cut the path elements. r := strings.IndexByte(raw[lastSlash+1:], '.') @@ -83,8 +83,8 @@ cut := f.Name[:idx] // TODO(go1.20): switch to strings.CutSuffix const inGoroutineSuffix = " in goroutine" - if strings.HasSuffix(cut, inGoroutineSuffix) { - f.Name = strings.TrimSuffix(cut, inGoroutineSuffix) + if before, ok := strings.CutSuffix(cut, inGoroutineSuffix); ok { + f.Name = before } } f.DirName = f.ImportPath @@ -175,6 +175,20 @@ pointerCeiling = uint64((^uint(0)) >> 1) ) +// SleepString returns a string "N-M minutes" if the goroutine(s) slept for a +// long time. +// +// Returns an empty string otherwise. +func (s *Signature) SleepString() string { + if s.SleepMax == 0 { + return "" + } + if s.SleepMin != s.SleepMax { + return fmt.Sprintf("%d~%d minutes", s.SleepMin, s.SleepMax) + } + return fmt.Sprintf("%d minutes", s.SleepMax) +} + // equal returns true only if both arguments are exactly equal. func (a *Arg) equal(r *Arg) bool { return a.similar(r, ExactFlags) @@ -281,14 +295,15 @@ } for i, l := range a.Values { rv := &r.Values[i] - if l.IsAggregate { + switch { + case l.IsAggregate: out.Values[i].IsAggregate = true out.Values[i].Fields = l.Fields.merge(&rv.Fields) - } else if !l.equal(rv) { + case !l.equal(rv): out.Values[i].Name = "*" out.Values[i].Value = l.Value out.Values[i].IsPtr = l.IsPtr - } else { + default: out.Values[i] = l } } @@ -711,19 +726,13 @@ // merge merges two similar Signature, zapping out differences. func (s *Signature) merge(r *Signature) *Signature { - min := s.SleepMin - if r.SleepMin < min { - min = r.SleepMin - } - max := s.SleepMax - if r.SleepMax > max { - max = r.SleepMax - } + minSleep := min(s.SleepMin, r.SleepMin) + maxSleep := max(s.SleepMax, r.SleepMax) return &Signature{ State: s.State, // Drop right side. CreatedBy: s.CreatedBy, // Drop right side. - SleepMin: min, - SleepMax: max, + SleepMin: minSleep, + SleepMax: maxSleep, Stack: *s.Stack.merge(&r.Stack), Locked: s.Locked || r.Locked, // TODO(maruel): This is weirdo. } @@ -755,20 +764,6 @@ return false } -// SleepString returns a string "N-M minutes" if the goroutine(s) slept for a -// long time. -// -// Returns an empty string otherwise. -func (s *Signature) SleepString() string { - if s.SleepMax == 0 { - return "" - } - if s.SleepMin != s.SleepMax { - return fmt.Sprintf("%d~%d minutes", s.SleepMin, s.SleepMax) - } - return fmt.Sprintf("%d minutes", s.SleepMax) -} - // updateLocations calls updateLocations on both CreatedBy and Stack and // returns true if they were both resolved. func (s *Signature) updateLocations(goroot, localgoroot string, localgomods, gopaths map[string]string) bool { @@ -782,6 +777,7 @@ // Signature is the stack trace, internal bits, state, which call site // created it, etc. Signature + // ID is the goroutine id. ID int // First is the goroutine first printed, normally the one that crashed.
diff --git a/stack/stack_test.go b/stack/stack_test.go index 16d030c..f326475 100644 --- a/stack/stack_test.go +++ b/stack/stack_test.go
@@ -208,7 +208,6 @@ }, } for i, line := range data { - line := line t.Run(fmt.Sprintf("%d-%s", i, line.name), func(t *testing.T) { t.Parallel() c := newCall(line.f, Args{}, line.s, 153) @@ -338,7 +337,7 @@ ) func init() { - goroot = strings.Replace(build.Default.GOROOT, "\\", "/", -1) + goroot = strings.ReplaceAll(build.Default.GOROOT, "\\", "/") gopaths = map[string]string{} for i, p := range getGOPATHs() { gopaths[p] = p @@ -353,7 +352,7 @@ panic(err) } // Our internal functions work with '/' as path separator. - pwd = strings.Replace(pwd, "\\", "/", -1) + pwd = strings.ReplaceAll(pwd, "\\", "/") gomods = map[string]string{} gmc := gomodCache{} if prefix, path := gmc.isGoModule(splitPath(pwd)); prefix != "" {
diff --git a/stack/webstack/example_test.go b/stack/webstack/example_test.go index a600bed..242cd48 100644 --- a/stack/webstack/example_test.go +++ b/stack/webstack/example_test.go
@@ -64,7 +64,10 @@ } // Must be called before touching req.Form. - req.ParseForm() + if err := req.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } // Disables source scanning by default since it's heavy. if req.FormValue("augment") == "" { req.Form.Set("augment", "0")
diff --git a/stack/webstack/webstack.go b/stack/webstack/webstack.go index 588060e..d6fc9cb 100644 --- a/stack/webstack/webstack.go +++ b/stack/webstack/webstack.go
@@ -10,6 +10,7 @@ import ( "bytes" + "errors" "io" "net/http" "runtime" @@ -44,7 +45,7 @@ // similarity: (default: "anypointer") Can be one of stack.Similarity value in // lowercase: "exactflags", "exactlines", "anypointer" or "anyvalue". func SnapshotHandler(w http.ResponseWriter, req *http.Request) { - if req.Method != "GET" { + if req.Method != http.MethodGet { http.Error(w, "invalid method", http.StatusMethodNotAllowed) return } @@ -112,15 +113,12 @@ if len(buf) >= maxmem { break } - l := len(buf) * 2 - if l > maxmem { - l = maxmem - } + l := min(len(buf)*2, maxmem) buf = make([]byte, l) } s, _, err := stack.ScanSnapshot(bytes.NewReader(buf), io.Discard, opts) // That's expected. - if err == io.EOF { + if errors.Is(err, io.EOF) { err = nil } return s, err
diff --git a/stack/webstack/webstack_test.go b/stack/webstack/webstack_test.go index 76db65f..901b2ba 100644 --- a/stack/webstack/webstack_test.go +++ b/stack/webstack/webstack_test.go
@@ -8,11 +8,13 @@ import ( "context" + "net/http" "net/http/httptest" "sync" "testing" ) +//nolint:paralleltest // Concurrent handlers change the goroutines being captured. func TestSnapshotHandler(t *testing.T) { data := []string{ "/debug", @@ -24,10 +26,10 @@ "/debug?similarity=anypointer", "/debug?similarity=anyvalue", } + //nolint:paralleltest // Snapshot contents depend on active goroutines. for _, url := range data { - url := url t.Run(url, func(t *testing.T) { - req := httptest.NewRequest("GET", url, nil) + req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, url, nil) w := httptest.NewRecorder() SnapshotHandler(w, req) if w.Code != 200 { @@ -45,10 +47,9 @@ "/debug?similarity=alike", } for _, url := range data { - url := url t.Run(url, func(t *testing.T) { t.Parallel() - req := httptest.NewRequest("GET", url, nil) + req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, url, nil) w := httptest.NewRecorder() SnapshotHandler(w, req) if w.Code != 400 { @@ -60,7 +61,7 @@ func TestSnapshotHandler_Method_POST(t *testing.T) { t.Parallel() - req := httptest.NewRequest("POST", "/debug", nil) + req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/debug", nil) w := httptest.NewRecorder() SnapshotHandler(w, req) if w.Code != 405 { @@ -68,6 +69,7 @@ } } +//nolint:paralleltest // This test intentionally creates thousands of goroutines. func TestSnapshotHandler_LargeMemory(t *testing.T) { // Try to create a stack frame over 1MiB in size when serialized to string. // This is tricky since this is dependent on many factors out of our control. @@ -81,20 +83,18 @@ // use more than 1MiB of call stack. We must not put it too high or it'll // crash on Travis. const parallel = 2500 - for i := 0; i < parallel; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range parallel { + wg.Go(func() { alive <- struct{}{} dummy(ctx, &a, &a, &a, &a, &a, &a, &a, &a, &a) - }() + }) } - for i := 0; i < parallel; i++ { + for range parallel { <-alive } // Normal. - req := httptest.NewRequest("GET", "/debug", nil) + req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/debug", nil) w := httptest.NewRecorder() SnapshotHandler(w, req) if w.Code != 200 { @@ -102,7 +102,7 @@ } // Cut off. That's 1<<20 + 1 - req = httptest.NewRequest("GET", "/debug?maxmem=1048577", nil) + req = httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/debug?maxmem=1048577", nil) w = httptest.NewRecorder() SnapshotHandler(w, req) // It can result in a 500 because the cut off is arbitrary, making parsing to @@ -119,9 +119,9 @@ // TODO(maruel): We should hook runtime.Stack() to make it a deterministic // output with internaltest.StaticPanicwebOutput(). b.ReportAllocs() - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequestWithContext(b.Context(), http.MethodGet, "/", nil) b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { w := httptest.NewRecorder() SnapshotHandler(w, req) if w.Code != 200 { @@ -130,6 +130,7 @@ } } +//nolint:revive,unparam // Arguments intentionally enlarge the captured stack frame. func dummy(ctx context.Context, a1, a2, a3, a4, a5, a6, a7, a8, a9 *int) { <-ctx.Done() }