| // Copyright 2018 Marc-Antoine Ruel. All rights reserved. |
| // Use of this source code is governed under the Apache License, Version 2.0 |
| // that can be found in the LICENSE file. |
| |
| package stack |
| |
| import ( |
| "bufio" |
| "bytes" |
| "errors" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "os" |
| "os/exec" |
| "path" |
| "path/filepath" |
| "regexp" |
| "runtime" |
| "strings" |
| "testing" |
| |
| "github.com/google/go-cmp/cmp" |
| "github.com/maruel/panicparse/v2/internal/internaltest" |
| ) |
| |
| func TestScanSnapshotErr(t *testing.T) { |
| t.Parallel() |
| data := []*Opts{ |
| nil, |
| {LocalGOROOT: "\\"}, |
| {LocalGOPATHs: []string{"\\"}}, |
| } |
| for _, opts := range data { |
| if _, _, err := ScanSnapshot(&bytes.Buffer{}, ioutil.Discard, opts); err == nil { |
| t.Fatal("expected error") |
| } |
| } |
| } |
| |
| func TestScanSnapshotSynthetic(t *testing.T) { |
| t.Parallel() |
| data := []struct { |
| name string |
| in []string |
| prefix string |
| suffix string |
| err error |
| want []*Goroutine |
| }{ |
| { |
| name: "Nothing", |
| err: io.EOF, |
| }, |
| { |
| name: "NothingEmpty", |
| in: make([]string, 111), |
| prefix: strings.Repeat("\n", 110), |
| err: io.EOF, |
| }, |
| { |
| name: "NothingLong", |
| in: []string{strings.Repeat("a", bufio.MaxScanTokenSize+10)}, |
| prefix: strings.Repeat("a", bufio.MaxScanTokenSize+10), |
| err: io.EOF, |
| }, |
| |
| // One call from main, one from stdlib, one from third party. |
| // Create a long first line that will be ignored. It is to guard against |
| // https://github.com/maruel/panicparse/issues/17. |
| { |
| name: "long,main,stdlib,third", |
| in: []string{ |
| strings.Repeat("a", bufio.MaxScanTokenSize+1), |
| "panic: reflect.Set: value of type", |
| "", |
| "goroutine 1 [running]:", |
| "github.com/cockroachdb/cockroach/storage/engine._Cfunc_DBIterSeek()", |
| " ??:0 +0x6d", |
| "gopkg.in/yaml%2ev2.handleErr(0x433b20)", |
| "\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6", |
| "reflect.Value.assignTo(0x570860, 0x803f3e0, 0x15)", |
| "\t/goroot/src/reflect/value.go:2125 +0x368", |
| "main.main()", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:428 +0x27", |
| "", |
| }, |
| prefix: strings.Repeat("a", bufio.MaxScanTokenSize+1) + "\npanic: reflect.Set: value of type\n\n", |
| 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), |
| }, |
| }, |
| }, |
| ID: 1, |
| First: true, |
| }, |
| }, |
| }, |
| |
| { |
| name: "LongWait", |
| in: []string{ |
| "panic: bleh", |
| "", |
| "goroutine 1 [chan send, 100 minutes]:", |
| "gopkg.in/yaml%2ev2.handleErr(0x433b20)", |
| "\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6", |
| "", |
| "goroutine 2 [chan send, locked to thread]:", |
| "gopkg.in/yaml%2ev2.handleErr(0x8033b21)", |
| "\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6", |
| "", |
| "goroutine 3 [chan send, 101 minutes, locked to thread]:", |
| "gopkg.in/yaml%2ev2.handleErr(0x8033b22)", |
| "\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6", |
| "", |
| }, |
| prefix: "panic: bleh\n\n", |
| 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), |
| }, |
| }, |
| }, |
| 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), |
| }, |
| }, |
| }, |
| 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), |
| }, |
| }, |
| Locked: true, |
| }, |
| ID: 3, |
| }, |
| }, |
| }, |
| |
| { |
| name: "Assembly", |
| in: []string{ |
| "panic: reflect.Set: value of type", |
| "", |
| "goroutine 16 [garbage collection]:", |
| "runtime.switchtoM()", |
| "\t/goroot/src/runtime/asm_amd64.s:198 fp=0xc20cfb80d8 sp=0xc20cfb80d0", |
| "", |
| }, |
| prefix: "panic: reflect.Set: value of type\n\n", |
| 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), |
| }, |
| }, |
| }, |
| ID: 16, |
| First: true, |
| }, |
| }, |
| }, |
| |
| { |
| name: "Assembly1.3", |
| in: []string{ |
| "panic: reflect.Set: value of type", |
| "", |
| "goroutine 16 [garbage collection]:", |
| "runtime.switchtoM()", |
| "\t/goroot/src/runtime/asm_amd64.s:198 fp=0xc20cfb80d8 sp=0xc20cfb80d0 pc=0x5007be", |
| "", |
| }, |
| prefix: "panic: reflect.Set: value of type\n\n", |
| 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), |
| }, |
| }, |
| }, |
| ID: 16, |
| First: true, |
| }, |
| }, |
| }, |
| |
| { |
| name: "LineErr", |
| in: []string{ |
| "panic: reflect.Set: value of type", |
| "", |
| "goroutine 1 [running]:", |
| "github.com/maruel/panicparse/stack/stack.recurseType()", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:12345678901234567890", |
| "", |
| }, |
| prefix: "panic: reflect.Set: value of type\n\n", |
| suffix: "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:12345678901234567890\n", |
| 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), |
| }, |
| }, |
| }, |
| ID: 1, |
| First: true, |
| }, |
| }, |
| }, |
| |
| { |
| name: "CreatedErr", |
| in: []string{ |
| "panic: reflect.Set: value of type", |
| "", |
| "goroutine 1 [running]:", |
| "github.com/maruel/panicparse/stack/stack.recurseType()", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:1", |
| "created by testing.RunTests", |
| "\t/goroot/src/testing/testing.go:123456789012345678901 +0xa8b", |
| "", |
| }, |
| prefix: "panic: reflect.Set: value of type\n\n", |
| suffix: "\t/goroot/src/testing/testing.go:123456789012345678901 +0xa8b\n", |
| 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), |
| }, |
| }, |
| }, |
| ID: 1, |
| First: true, |
| }, |
| }, |
| }, |
| |
| { |
| name: "ValueErr", |
| in: []string{ |
| "panic: reflect.Set: value of type", |
| "", |
| "goroutine 1 [running]:", |
| "github.com/maruel/panicparse/stack/stack.recurseType(123456789012345678901)", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9", |
| "", |
| }, |
| prefix: "panic: reflect.Set: value of type\n\n", |
| suffix: "github.com/maruel/panicparse/stack/stack.recurseType(123456789012345678901)\n" + |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9\n", |
| 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), |
| }, |
| }, |
| }, |
| ID: 1, |
| First: true, |
| }, |
| }, |
| }, |
| |
| { |
| name: "MaxNestingDepth", |
| in: []string{ |
| "panic: reflect.Set: value of type", |
| "", |
| "goroutine 1 [running]:", |
| "github.com/maruel/panicparse/stack/stack.recurseType({{{{{...}}}}})", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9", |
| "", |
| }, |
| prefix: "panic: reflect.Set: value of type\n\n", |
| err: io.EOF, |
| want: []*Goroutine{ |
| { |
| Signature: Signature{ |
| 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{ |
| Elided: true, |
| }}}, |
| }}}, |
| }}}, |
| }}}, |
| }}}, |
| }, |
| "/gopath/src/github.com/maruel/panicparse/stack/stack.go", |
| 9), |
| }, |
| }, |
| }, |
| ID: 1, |
| First: true, |
| }, |
| }, |
| }, |
| |
| { |
| name: "MaxNestingDepthExceededErr", |
| in: []string{ |
| "panic: reflect.Set: value of type", |
| "", |
| "goroutine 1 [running]:", |
| "github.com/maruel/panicparse/stack/stack.recurseType({{{{{{...}}}}}})", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9", |
| "", |
| }, |
| prefix: "panic: reflect.Set: value of type\n\n", |
| suffix: "github.com/maruel/panicparse/stack/stack.recurseType({{{{{{...}}}}}})\n" + |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9\n", |
| 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), |
| }, |
| }, |
| }, |
| ID: 1, |
| First: true, |
| }, |
| }, |
| }, |
| |
| { |
| name: "UnmatchedOpeningCurlyBracketErr", |
| in: []string{ |
| "panic: reflect.Set: value of type", |
| "", |
| "goroutine 1 [running]:", |
| "github.com/maruel/panicparse/stack/stack.recurseType({{{{{...}}}})", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9", |
| "", |
| }, |
| prefix: "panic: reflect.Set: value of type\n\n", |
| suffix: "github.com/maruel/panicparse/stack/stack.recurseType({{{{{...}}}})\n" + |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9\n", |
| 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), |
| }, |
| }, |
| }, |
| ID: 1, |
| First: true, |
| }, |
| }, |
| }, |
| |
| { |
| name: "UnmatchedClosingCurlyBracketErr", |
| in: []string{ |
| "panic: reflect.Set: value of type", |
| "", |
| "goroutine 1 [running]:", |
| "github.com/maruel/panicparse/stack/stack.recurseType({{{{...}}}}})", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9", |
| "", |
| }, |
| prefix: "panic: reflect.Set: value of type\n\n", |
| suffix: "github.com/maruel/panicparse/stack/stack.recurseType({{{{...}}}}})\n" + |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:9\n", |
| 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), |
| }, |
| }, |
| }, |
| ID: 1, |
| First: true, |
| }, |
| }, |
| }, |
| |
| { |
| name: "InconsistentIndent", |
| in: []string{ |
| " goroutine 1 [running]:", |
| " github.com/maruel/panicparse/stack/stack.recurseType()", |
| " \t/gopath/src/github.com/maruel/panicparse/stack/stack.go:1", |
| "", |
| }, |
| suffix: " \t/gopath/src/github.com/maruel/panicparse/stack/stack.go:1\n", |
| 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), |
| }, |
| }, |
| }, |
| ID: 1, |
| First: true, |
| }, |
| }, |
| }, |
| |
| { |
| name: "OrderErr", |
| in: []string{ |
| "panic: reflect.Set: value of type", |
| "", |
| "goroutine 16 [garbage collection]:", |
| "\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6", |
| "runtime.switchtoM()", |
| "\t/goroot/src/runtime/asm_amd64.s:198 fp=0xc20cfb80d8 sp=0xc20cfb80d0", |
| "", |
| }, |
| prefix: "panic: reflect.Set: value of type\n\n", |
| suffix: "\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6\n" + |
| "runtime.switchtoM()\n" + |
| "\t/goroot/src/runtime/asm_amd64.s:198 fp=0xc20cfb80d8 sp=0xc20cfb80d0\n", |
| 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, |
| }, |
| }, |
| }, |
| |
| { |
| name: "Elided", |
| in: []string{ |
| "panic: reflect.Set: value of type", |
| "", |
| "goroutine 16 [garbage collection]:", |
| "github.com/maruel/panicparse/stack/stack.recurseType(0x9a3ec70, 0x8062580, 0x9a3e818, 0x50a820, 0x803a8a0)", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:53 +0x845 fp=0xc20cfc66d8 sp=0xc20cfc6470", |
| "...additional frames elided...", |
| "created by testing.RunTests", |
| "\t/goroot/src/testing/testing.go:555 +0xa8b", |
| "", |
| }, |
| prefix: "panic: reflect.Set: value of type\n\n", |
| err: io.EOF, |
| want: []*Goroutine{ |
| { |
| Signature: Signature{ |
| 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}, |
| }, |
| }, |
| "/gopath/src/github.com/maruel/panicparse/stack/stack.go", |
| 53), |
| }, |
| Elided: true, |
| }, |
| }, |
| ID: 16, |
| First: true, |
| }, |
| }, |
| }, |
| |
| { |
| name: "Syscall", |
| in: []string{ |
| "panic: reflect.Set: value of type", |
| "", |
| "goroutine 5 [syscall]:", |
| "runtime.notetsleepg(0x918100, 0xffffffffffffffff, 0x1)", |
| "\t/goroot/src/runtime/lock_futex.go:201 +0x52 fp=0xc208018f68 sp=0xc208018f40", |
| "runtime.signal_recv(0x0)", |
| "\t/goroot/src/runtime/sigqueue.go:109 +0x135 fp=0xc208018fa0 sp=0xc208018f68", |
| "os/signal.loop()", |
| "\t/goroot/src/os/signal/signal_unix.go:21 +0x1f fp=0xc208018fe0 sp=0xc208018fa0", |
| "runtime.goexit()", |
| "\t/goroot/src/runtime/asm_amd64.s:2232 +0x1 fp=0xc208018fe8 sp=0xc208018fe0", |
| "created by os/signal.init·1", |
| "\t/goroot/src/os/signal/signal_unix.go:27 +0x35", |
| "", |
| }, |
| prefix: "panic: reflect.Set: value of type\n\n", |
| 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), |
| }, |
| }, |
| 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), |
| }, |
| }, |
| }, |
| ID: 5, |
| First: true, |
| }, |
| }, |
| }, |
| |
| { |
| name: "UnavailCreated", |
| in: []string{ |
| "panic: reflect.Set: value of type", |
| "", |
| "goroutine 24 [running]:", |
| "\tgoroutine running on other thread; stack unavailable", |
| "created by github.com/maruel/panicparse/stack.New", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:131 +0x381", |
| "", |
| }, |
| prefix: "panic: reflect.Set: value of type\n\n", |
| 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), |
| }, |
| }, |
| Stack: Stack{ |
| Calls: []Call{newCall("", Args{}, "<unavailable>", 0)}, |
| }, |
| }, |
| ID: 24, |
| First: true, |
| }, |
| }, |
| }, |
| |
| { |
| name: "Unavail", |
| in: []string{ |
| "panic: reflect.Set: value of type", |
| "", |
| "goroutine 24 [running]:", |
| "\tgoroutine running on other thread; stack unavailable", |
| "", |
| "", |
| }, |
| prefix: "panic: reflect.Set: value of type\n\n", |
| err: io.EOF, |
| want: []*Goroutine{ |
| { |
| Signature: Signature{ |
| State: "running", |
| Stack: Stack{ |
| Calls: []Call{newCall("", Args{}, "<unavailable>", 0)}, |
| }, |
| }, |
| ID: 24, |
| First: true, |
| }, |
| }, |
| }, |
| |
| { |
| name: "UnavailError", |
| in: []string{ |
| "panic: reflect.Set: value of type", |
| "", |
| "goroutine 24 [running]:", |
| "\tgoroutine running on other thread; stack unavailable", |
| "junk", |
| }, |
| prefix: "panic: reflect.Set: value of type\n\n", |
| suffix: "junk", |
| 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)}, |
| }, |
| }, |
| ID: 24, |
| First: true, |
| }, |
| }, |
| }, |
| |
| { |
| name: "NoOffset", |
| in: []string{ |
| "panic: runtime error: index out of range", |
| "", |
| "goroutine 37 [runnable]:", |
| "github.com/maruel/panicparse/stack.func·002()", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:110", |
| "created by github.com/maruel/panicparse/stack.New", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:113 +0x43b", |
| "", |
| }, |
| prefix: "panic: runtime error: index out of range\n\n", |
| 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), |
| }, |
| }, |
| 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, |
| First: true, |
| }, |
| }, |
| }, |
| |
| // For coverage of scanLines. |
| { |
| name: "HeaderError", |
| in: []string{ |
| "panic: reflect.Set: value of type", |
| "", |
| "goroutine 1 [running]:", |
| "junk", |
| }, |
| prefix: "panic: reflect.Set: value of type\n\n", |
| suffix: "junk", |
| err: errors.New("expected a function after a goroutine header, got: \"junk\""), |
| want: []*Goroutine{ |
| { |
| Signature: Signature{State: "running"}, |
| ID: 1, |
| First: true, |
| }, |
| }, |
| }, |
| |
| // For coverage of scanLines. |
| { |
| name: "FileError", |
| in: []string{ |
| "panic: reflect.Set: value of type", |
| "", |
| "goroutine 1 [running]:", |
| "github.com/maruel/panicparse/stack.func·002()", |
| "junk", |
| }, |
| prefix: "panic: reflect.Set: value of type\n\n", |
| suffix: "junk", |
| 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), |
| }, |
| }, |
| }, |
| ID: 1, |
| First: true, |
| }, |
| }, |
| }, |
| |
| // For coverage of scanLines. |
| { |
| name: "Created", |
| in: []string{ |
| "panic: reflect.Set: value of type", |
| "", |
| "goroutine 1 [running]:", |
| "github.com/maruel/panicparse/stack.func·002()", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:110", |
| "created by github.com/maruel/panicparse/stack.New", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:131 +0x381", |
| "exit status 2", |
| }, |
| prefix: "panic: reflect.Set: value of type\n\n", |
| suffix: "exit status 2", |
| 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), |
| }, |
| }, |
| 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, |
| First: true, |
| }, |
| }, |
| }, |
| |
| // For coverage of scanLines. |
| { |
| name: "CreatedError", |
| in: []string{ |
| "panic: reflect.Set: value of type", |
| "", |
| "goroutine 1 [running]:", |
| "github.com/maruel/panicparse/stack.func·002()", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:110", |
| "created by github.com/maruel/panicparse/stack.New", |
| "junk", |
| }, |
| prefix: "panic: reflect.Set: value of type\n\n", |
| suffix: "junk", |
| 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), |
| }, |
| }, |
| 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, |
| First: true, |
| }, |
| }, |
| }, |
| |
| { |
| name: "CCode", |
| in: []string{ |
| "SIGQUIT: quit", |
| "PC=0x43f349", |
| "", |
| "goroutine 0 [idle]:", |
| "runtime.epollwait(0x4, 0x71c7118, 0xffffffff00000080, 0x0, 0xffffffff0028c1be, 0x0, 0x0, 0x0, 0x0, 0x0, ...)", |
| " /goroot/src/runtime/sys_linux_amd64.s:400 +0x19", |
| "runtime.netpoll(0x901b01, 0x0)", |
| " /goroot/src/runtime/netpoll_epoll.go:68 +0xa3", |
| "findrunnable(0x8012000)", |
| " /goroot/src/runtime/proc.c:1472 +0x485", |
| "schedule()", |
| " /goroot/src/runtime/proc.c:1575 +0x151", |
| "runtime.park_m(0x80017a0)", |
| " /goroot/src/runtime/proc.c:1654 +0x113", |
| "runtime.mcall(0x432684)", |
| " /goroot/src/runtime/asm_amd64.s:186 +0x5a", |
| "", |
| }, |
| prefix: "SIGQUIT: quit\nPC=0x43f349\n\n", |
| 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, |
| }, |
| "/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, |
| First: true, |
| }, |
| }, |
| }, |
| |
| { |
| name: "WithCarriageReturn", |
| in: []string{ |
| "goroutine 1 [running]:", |
| "github.com/cockroachdb/cockroach/storage/engine._Cfunc_DBIterSeek()", |
| " ??:0 +0x6d", |
| "gopkg.in/yaml%2ev2.handleErr(0x433b20)", |
| "\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6", |
| "reflect.Value.assignTo(0x570860, 0x803f3e0, 0x15)", |
| "\t/goroot/src/reflect/value.go:2125 +0x368", |
| "main.main()", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:428 +0x27", |
| "", |
| }, |
| 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), |
| }, |
| }, |
| }, |
| ID: 1, |
| First: true, |
| }, |
| }, |
| }, |
| |
| { |
| name: "WithCarriageReturn1.18Inaccurate", |
| in: []string{ |
| "goroutine 1 [running]:", |
| "github.com/cockroachdb/cockroach/storage/engine._Cfunc_DBIterSeek()", |
| " ??:0 +0x6d", |
| "gopkg.in/yaml%2ev2.handleErr(0x433b20?)", |
| "\t/gopath/src/gopkg.in/yaml.v2/yaml.go:153 +0xc6", |
| "reflect.Value.assignTo(0x570860, 0x803f3e0, 0x15)", |
| "\t/goroot/src/reflect/value.go:2125 +0x368", |
| "main.main()", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:428 +0x27", |
| "", |
| }, |
| 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), |
| }, |
| }, |
| }, |
| ID: 1, |
| First: true, |
| }, |
| }, |
| }, |
| |
| // goconvey is culprit of this. |
| { |
| name: "Indented", |
| in: []string{ |
| "Failures:", |
| "", |
| " * /home/maruel/go/src/foo/bar_test.go", |
| " Line 209:", |
| " Expected: '(*errors.errorString){s:\"context canceled\"}'", |
| " Actual: 'nil'", |
| " (Should resemble)!", |
| " goroutine 8 [running]:", |
| " foo/bar.TestArchiveFail.func1.2()", |
| " /home/maruel/go/foo/bar_test.go:209 +0x469", |
| " foo/bar.TestArchiveFail(0x3382000)", |
| " /home/maruel/go/src/foo/bar_test.go:155 +0xf1", |
| " testing.tRunner(0x3382000, 0x1615bf8)", |
| " /home/maruel/golang/go/src/testing/testing.go:865 +0xc0", |
| " created by testing.(*T).Run", |
| " /home/maruel/golang/go/src/testing/testing.go:916 +0x35a", |
| "", |
| "", |
| }, |
| prefix: strings.Join([]string{ |
| "Failures:", |
| "", |
| " * /home/maruel/go/src/foo/bar_test.go", |
| " Line 209:", |
| " Expected: '(*errors.errorString){s:\"context canceled\"}'", |
| " Actual: 'nil'", |
| " (Should resemble)!", |
| "", |
| }, "\n"), |
| 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), |
| }, |
| }, |
| 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, |
| First: true, |
| }, |
| }, |
| }, |
| |
| { |
| name: "Race", |
| in: []string{string(internaltest.StaticPanicRaceOutput())}, |
| 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, |
| ), |
| }, |
| }, |
| 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, |
| First: true, |
| 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, |
| ), |
| }, |
| }, |
| 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, |
| RaceWrite: true, |
| RaceAddr: 0xc000014100, |
| }, |
| }, |
| }, |
| |
| { |
| name: "RaceHdr1Err", |
| in: []string{ |
| string(raceHeaderFooter), |
| }, |
| prefix: string(raceHeaderFooter), |
| err: io.EOF, |
| }, |
| |
| { |
| name: "RaceHdr2Err", |
| in: []string{ |
| string(raceHeaderFooter), |
| "", |
| }, |
| // TODO(maruel): This is incorrect. |
| prefix: "", |
| err: io.EOF, |
| }, |
| |
| { |
| name: "RaceHdr3Err", |
| in: []string{ |
| string(raceHeaderFooter), |
| string(raceHeader), |
| }, |
| // TODO(maruel): This is incorrect. |
| prefix: "", |
| err: io.EOF, |
| }, |
| |
| { |
| name: "RaceHdr4Err", |
| in: []string{ |
| string(raceHeaderFooter), |
| string(raceHeader), |
| "", |
| }, |
| // TODO(maruel): This is incorrect. |
| prefix: "", |
| err: io.EOF, |
| }, |
| } |
| 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{} |
| r := bytes.NewBufferString(strings.Join(line.in, "\n")) |
| s, suffix, err := ScanSnapshot(r, &prefix, defaultOpts()) |
| compareErr(t, line.err, err) |
| if line.want == nil { |
| if s != nil { |
| t.Fatalf("unexpected %v", s) |
| } |
| } else { |
| if s == nil { |
| t.Fatalf("expected snapshot") |
| } |
| compareGoroutines(t, line.want, s.Goroutines) |
| } |
| compareString(t, line.prefix, prefix.String()) |
| rest, err := ioutil.ReadAll(r) |
| compareErr(t, nil, err) |
| compareString(t, line.suffix, string(suffix)+string(rest)) |
| }) |
| } |
| } |
| |
| func TestScanSnapshotSyntheticTwoSnapshots(t *testing.T) { |
| t.Parallel() |
| in := bytes.Buffer{} |
| in.WriteString("Ya\n") |
| in.Write(internaltest.PanicOutputs()["simple"]) |
| in.WriteString("Ye\n") |
| in.Write(internaltest.PanicOutputs()["int"]) |
| in.WriteString("Yo\n") |
| panicParseDir := getPanicParseDir(t) |
| ppDir := pathJoin(panicParseDir, "cmd", "panic") |
| |
| // First stack: |
| prefix := bytes.Buffer{} |
| s, suffix, err := ScanSnapshot(&in, &prefix, defaultOpts()) |
| compareErr(t, nil, err) |
| if !s.guessPaths() { |
| t.Error("expected success") |
| } |
| want := []*Goroutine{ |
| { |
| Signature: Signature{ |
| State: "running", |
| Stack: Stack{ |
| Calls: []Call{ |
| newCallLocal( |
| "main.main", |
| Args{}, |
| pathJoin(ppDir, "main.go"), |
| 71, |
| ), |
| }, |
| }, |
| }, |
| ID: 1, |
| First: true, |
| }, |
| } |
| compareGoroutines(t, want, s.Goroutines) |
| compareString(t, "Ya\nGOTRACEBACK=all\npanic: simple\n\n", prefix.String()) |
| |
| prefix.Reset() |
| r := io.MultiReader(bytes.NewReader(suffix), &in) |
| s, suffix, err = ScanSnapshot(r, &prefix, defaultOpts()) |
| compareErr(t, nil, err) |
| if !s.guessPaths() { |
| t.Error("expected success") |
| } |
| want = []*Goroutine{ |
| { |
| Signature: Signature{ |
| State: "running", |
| Stack: Stack{ |
| Calls: []Call{ |
| newCallLocal( |
| "main.panicint", |
| Args{Values: []Arg{{Value: 42}}}, |
| pathJoin(ppDir, "main.go"), |
| 90, |
| ), |
| newCallLocal( |
| "main.glob..func9", |
| Args{}, |
| pathJoin(ppDir, "main.go"), |
| 311, |
| ), |
| newCallLocal( |
| "main.main", |
| Args{}, |
| pathJoin(ppDir, "main.go"), |
| 73, |
| ), |
| }, |
| }, |
| }, |
| ID: 1, |
| First: true, |
| }, |
| } |
| compareGoroutines(t, want, s.Goroutines) |
| compareString(t, "Ye\nGOTRACEBACK=all\npanic: 42\n\n", prefix.String()) |
| compareString(t, "Yo\n", string(suffix)) |
| } |
| |
| func TestSplitPath(t *testing.T) { |
| t.Parallel() |
| if p := splitPath(""); p != nil { |
| t.Fatalf("expected nil, got: %v", p) |
| } |
| } |
| |
| func TestGetGOPATHs(t *testing.T) { |
| // This test cannot run in parallel. |
| old := os.Getenv("GOPATH") |
| defer os.Setenv("GOPATH", old) |
| os.Setenv("GOPATH", "") |
| if p := getGOPATHs(); len(p) != 1 { |
| // It's the home directory + /go. |
| t.Fatalf("expected only one path: %v", p) |
| } |
| |
| root, err := ioutil.TempDir("", "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)) |
| if p := getGOPATHs(); len(p) != 2 { |
| t.Fatalf("expected two paths: %v", p) |
| } |
| } |
| |
| // TestGomoduleComplex is an integration test that creates a non-trivial tree |
| // of go modules using the "replace" statement. |
| func TestGomoduleComplex(t *testing.T) { |
| // This test cannot run in parallel. |
| if internaltest.GetGoMinorVersion() < 11 { |
| t.Skip("requires go module support") |
| } |
| old := os.Getenv("GOPATH") |
| defer os.Setenv("GOPATH", old) |
| root, err := ioutil.TempDir("", "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, "go")) |
| tree := map[string]string{ |
| "pkg1/go.mod": "module example.com/pkg1\n" + |
| "require (\n" + |
| "\texample.com/pkg2 v0.0.1\n" + |
| "\texample.com/pkg3 v0.0.1\n" + |
| ")\n" + |
| "replace example.com/pkg2 => ../pkg2\n" + |
| // This is kind of a hack to force testing with a package inside GOPATH, |
| // since this won't normally work by default. |
| "replace example.com/pkg3 => ../go/src/example.com/pkg3\n", |
| "pkg1/cmd/main.go": "package main\n" + |
| "import \"example.com/pkg1/internal\"\n" + |
| "func main() {\n" + |
| "\tinternal.CallCallDie()\n" + |
| "}\n", |
| "pkg1/internal/int.go": "package internal\n" + |
| "import \"example.com/pkg2\"\n" + |
| "func CallCallDie() {\n" + |
| "\tpkg2.CallDie()\n" + |
| "}\n", |
| |
| "pkg2/go.mod": "module example.com/pkg2\n" + |
| "require (\n" + |
| "\texample.com/pkg3 v0.0.1\n" + |
| ")\n" + |
| // This is kind of a hack to force testing with a package inside GOPATH, |
| // since this won't normally work by default. |
| "replace example.com/pkg3 => ../go/src/example.com/pkg3\n", |
| "pkg2/src2.go": "package pkg2\n" + |
| "import \"example.com/pkg3\"\n" + |
| "func CallDie() { pkg3.Die() }\n", |
| |
| "go/src/example.com/pkg3/go.mod": "module example.com/pkg3\n", |
| "go/src/example.com/pkg3/src3.go": "package pkg3\n" + |
| "func Die() { panic(42) }\n", |
| } |
| createTree(t, root, tree) |
| |
| exe := filepath.Join(root, "yo") |
| if runtime.GOOS == "windows" { |
| exe += ".exe" |
| } |
| if err = internaltest.Compile("./cmd", exe, filepath.Join(root, "pkg1"), true, false); err != nil { |
| t.Fatal(err) |
| } |
| |
| out, err := exec.Command(exe).CombinedOutput() |
| if err == nil { |
| t.Error("expected failure") |
| } |
| prefix := bytes.Buffer{} |
| s, suffix, err := ScanSnapshot(bytes.NewReader(out), &prefix, defaultOpts()) |
| compareErr(t, io.EOF, err) |
| if !s.guessPaths() { |
| t.Error("expected success") |
| } |
| if s == nil { |
| t.Fatal("expected snapshot") |
| } |
| if s.IsRace() { |
| t.Fatal("unexpected race") |
| } |
| compareString(t, "panic: 42\n\n", prefix.String()) |
| compareString(t, "", string(suffix)) |
| wantGOROOT := "" |
| compareString(t, wantGOROOT, s.RemoteGOROOT) |
| compareString(t, runtime.GOROOT(), strings.Replace(s.LocalGOROOT, "/", pathSeparator, -1)) |
| |
| rootRemote := root |
| if runtime.GOOS == "windows" { |
| // On Windows, we must make the path to be POSIX style. |
| rootRemote = strings.Replace(root, pathSeparator, "/", -1) |
| } |
| rootLocal := rootRemote |
| if runtime.GOOS == "darwin" { |
| // On MacOS, the path is a symlink and it will be somehow evaluated when we |
| // get the traces back. This must NOT be run on Windows otherwise the path |
| // will be converted to 8.3 format. |
| if rootRemote, err = filepath.EvalSymlinks(rootLocal); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| // This part is a bit tricky. The symlink is evaluated on the left since, |
| // since it's what is the "remote" path, but it is not on the right since, |
| // which is the "local" path. This difference only exists on MacOS. |
| wantGOPATHs := map[string]string{ |
| pathJoin(rootRemote, "go"): pathJoin(rootLocal, "go"), |
| } |
| if diff := cmp.Diff(s.RemoteGOPATHs, wantGOPATHs); diff != "" { |
| t.Fatalf("+want/-got: %s", diff) |
| } |
| |
| // Local go module search is on the path with symlink evaluated on MacOS. |
| // This is kind of confusing because it is the "remote" path. |
| wantGomods := map[string]string{ |
| pathJoin(rootRemote, "pkg1"): "example.com/pkg1", |
| pathJoin(rootRemote, "pkg2"): "example.com/pkg2", |
| } |
| if diff := cmp.Diff(s.LocalGomods, wantGomods); diff != "" { |
| t.Fatalf("+want/-got: %s", diff) |
| } |
| |
| 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, |
| }, |
| }, |
| }, |
| }, |
| ID: 1, |
| First: true, |
| }, |
| } |
| similarGoroutines(t, want, s.Goroutines) |
| } |
| |
| func TestGoRun(t *testing.T) { |
| t.Parallel() |
| root, err := ioutil.TempDir("", "stack") |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer func() { |
| if err = os.RemoveAll(root); err != nil { |
| t.Error(err) |
| } |
| }() |
| |
| p := filepath.Join(root, "main.go") |
| content := "package main\nfunc main() { panic(42) }\n" |
| if err = ioutil.WriteFile(p, []byte(content), 0600); err != nil { |
| t.Fatal(err) |
| } |
| c := exec.Command("go", "run", p) |
| out, err := c.CombinedOutput() |
| if err == nil { |
| t.Fatal("expected failure") |
| } |
| prefix := bytes.Buffer{} |
| s, suffix, err := ScanSnapshot(bytes.NewReader(out), &prefix, defaultOpts()) |
| compareErr(t, nil, err) |
| compareString(t, "panic: 42\n\n", prefix.String()) |
| compareString(t, "exit status 2\n", string(suffix)) |
| if s == nil { |
| t.Fatal("expected snapshot") |
| } |
| if runtime.GOOS == "windows" { |
| // On Windows, we must make the path to be POSIX style. |
| p = strings.Replace(p, pathSeparator, "/", -1) |
| } |
| |
| 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, |
| }, |
| }, |
| }, |
| }, |
| ID: 1, |
| First: true, |
| }, |
| } |
| similarGoroutines(t, want, s.Goroutines) |
| |
| if !s.guessPaths() { |
| t.Error("expected success") |
| } |
| want[0].Stack.Calls[0].LocalSrcPath = p |
| want[0].Stack.Calls[0].RelSrcPath = "main.go" |
| // This is not technically true, when using go run there's no need for a |
| // go.mod file, but I don't think it's worth handling specifically. |
| want[0].Stack.Calls[0].Location = GoMod |
| similarGoroutines(t, want, s.Goroutines) |
| } |
| |
| // TestPanic runs github.com/maruel/panicparse/v2/cmd/panic with every |
| // supported panic modes. |
| func TestPanic(t *testing.T) { |
| t.Parallel() |
| cmds := internaltest.PanicOutputs() |
| want := map[string]int{ |
| "chan_receive": 2, |
| "chan_send": 2, |
| "goroutine_1": 2, |
| "goroutine_dedupe_pointers": 101, |
| "goroutine_100": 101, |
| } |
| |
| panicParseDir := getPanicParseDir(t) |
| ppDir := pathJoin(panicParseDir, "cmd", "panic") |
| |
| // Test runtime code. For those not in "custom", just assert that they |
| // succeed. |
| custom := map[string]func(*testing.T, *Snapshot, *bytes.Buffer, string){ |
| "args_elided": testPanicArgsElided, |
| "mismatched": testPanicMismatched, |
| "race": testPanicRace, |
| "str": testPanicStr, |
| "utf8": testPanicUTF8, |
| } |
| // Make sure all custom handlers are showing up in cmds. |
| for n := range custom { |
| if _, ok := cmds[n]; !ok { |
| if n == "race" { |
| t.Skip("race is unsupported") |
| } |
| t.Fatalf("untested mode %q:\n%v", n, cmds[n]) |
| } |
| } |
| |
| 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 { |
| t.Fatal(err) |
| } |
| if s == nil { |
| t.Fatal("context is nil") |
| } |
| if !s.guessPaths() { |
| t.Fatal("expected GuessPaths to work") |
| } |
| if f := custom[cmd]; f != nil { |
| f(t, s, &prefix, ppDir) |
| return |
| } |
| e := want[cmd] |
| if e == 0 { |
| e = 1 |
| } |
| if got := len(s.Goroutines); got != e { |
| t.Fatalf("unexpected Goroutines; want %d, got %d", e, got) |
| } |
| compareString(t, "", string(suffix)) |
| }) |
| } |
| } |
| |
| func testPanicArgsElided(t *testing.T, s *Snapshot, b *bytes.Buffer, ppDir string) { |
| if s.RemoteGOROOT != "" { |
| t.Fatalf("RemoteGOROOT is %q", s.RemoteGOROOT) |
| } |
| if b.String() != "GOTRACEBACK=all\npanic: 1\n\n" { |
| t.Fatalf("output: %q", b.String()) |
| } |
| 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.glob..func1", Args{}, pathJoin(ppDir, "main.go"), 134), |
| newCallLocal("main.main", Args{}, pathJoin(ppDir, "main.go"), 340), |
| }, |
| }, |
| }, |
| ID: 1, |
| First: true, |
| }, |
| } |
| similarGoroutines(t, want, s.Goroutines) |
| } |
| |
| func testPanicMismatched(t *testing.T, s *Snapshot, b *bytes.Buffer, ppDir string) { |
| if s.RemoteGOROOT != "" { |
| t.Fatalf("RemoteGOROOT is %q", s.RemoteGOROOT) |
| } |
| if b.String() != "GOTRACEBACK=all\npanic: 42\n\n" { |
| t.Fatalf("output: %q", b.String()) |
| } |
| ver := "/v2" |
| if !internaltest.IsUsingModules() { |
| ver = "" |
| } |
| 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.glob..func20", Args{}, pathJoin(ppDir, "main.go"), 314), |
| newCallLocal("main.main", Args{}, pathJoin(ppDir, "main.go"), 340), |
| }, |
| }, |
| }, |
| ID: 1, |
| First: true, |
| }, |
| } |
| similarGoroutines(t, want, s.Goroutines) |
| } |
| |
| func testPanicRace(t *testing.T, s *Snapshot, b *bytes.Buffer, ppDir string) { |
| if s.RemoteGOROOT != "" { |
| t.Fatalf("RemoteGOROOT is %q", s.RemoteGOROOT) |
| } |
| if b.String() != "GOTRACEBACK=all\n" { |
| t.Fatalf("output: %q", b.String()) |
| } |
| 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"), |
| 73, |
| ), |
| }, |
| }, |
| 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"), |
| 73, |
| ), |
| }, |
| }, |
| 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, |
| RaceAddr: pointer, |
| }, |
| } |
| // IDs are not deterministic, so zap them too but take them for the race |
| // detector first. |
| for i, g := range s.Goroutines { |
| g.ID = i + 1 |
| if g.RaceAddr > 4*1024*1024 { |
| g.RaceAddr = pointer |
| } |
| } |
| // Sometimes the read is detected first. |
| if s.Goroutines[0].RaceWrite { |
| want[0], want[1] = want[1], want[0] |
| } |
| // These fields are order-dependent, so set them last. |
| want[0].ID = 1 |
| want[1].ID = 2 |
| want[0].First = true |
| want[1].First = false |
| similarGoroutines(t, want, s.Goroutines) |
| } |
| |
| func testPanicStr(t *testing.T, s *Snapshot, b *bytes.Buffer, ppDir string) { |
| if s.RemoteGOROOT != "" { |
| t.Fatalf("RemoteGOROOT is %q", s.RemoteGOROOT) |
| } |
| if b.String() != "GOTRACEBACK=all\npanic: allo\n\n" { |
| t.Fatalf("output: %q", b.String()) |
| } |
| want := []*Goroutine{ |
| { |
| Signature: Signature{ |
| State: "running", |
| Stack: Stack{ |
| Calls: []Call{ |
| newCallLocal( |
| "main.panicstr", |
| ifCombinedAggregateArgs( |
| Args{Values: []Arg{{IsAggregate: true, Fields: Args{ |
| Values: []Arg{{Value: 0x123456, IsPtr: true}, {Value: 4}}, |
| }}}}, |
| // else |
| Args{Values: []Arg{{Value: 0x123456, IsPtr: true}, {Value: 4}}}, |
| ), |
| pathJoin(ppDir, "main.go"), |
| 50), |
| newCallLocal("main.glob..func19", Args{}, pathJoin(ppDir, "main.go"), 307), |
| newCallLocal("main.main", Args{}, pathJoin(ppDir, "main.go"), 340), |
| }, |
| }, |
| }, |
| ID: 1, |
| First: true, |
| }, |
| } |
| similarGoroutines(t, want, s.Goroutines) |
| } |
| |
| func testPanicUTF8(t *testing.T, s *Snapshot, b *bytes.Buffer, ppDir string) { |
| if s.RemoteGOROOT != "" { |
| t.Fatalf("RemoteGOROOT is %q", s.RemoteGOROOT) |
| } |
| if b.String() != "GOTRACEBACK=all\npanic: 42\n\n" { |
| t.Fatalf("output: %q", b.String()) |
| } |
| ver := "/v2" |
| if !internaltest.IsUsingModules() { |
| ver = "" |
| } |
| 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", |
| ifCombinedAggregateArgs( |
| Args{Values: []Arg{{Value: 1, IsInaccurate: true}}}, |
| // else |
| Args{Values: []Arg{{Value: 0xc0000b2e48, IsPtr: true, IsInaccurate: true}}}, |
| ), |
| // See TestCallUTF8 in stack_test.go for exercising the methods on |
| // Call in this situation. |
| pathJoin(ppDir, "internal", "utf8", "ùtf8.go"), |
| 10), |
| newCallLocal("main.glob..func21", Args{}, pathJoin(ppDir, "main.go"), 322), |
| newCallLocal("main.main", Args{}, pathJoin(ppDir, "main.go"), 340), |
| }, |
| }, |
| }, |
| ID: 1, |
| First: true, |
| }, |
| } |
| similarGoroutines(t, want, s.Goroutines) |
| } |
| |
| // TestPanicweb implements the parsing of panicweb output. |
| // |
| // panicweb is a separate binary from the rest of panic because importing the |
| // "net" package causes a background thread to be started, which breaks "panic |
| // asleep". |
| func TestPanicweb(t *testing.T) { |
| t.Parallel() |
| if runtime.GOARCH == "ppc64" || runtime.GOARCH == "ppc64le" { |
| t.Skip("https://github.com/maruel/panicparse/issues/66") |
| } |
| prefix := bytes.Buffer{} |
| s, suffix, err := ScanSnapshot(bytes.NewReader(internaltest.PanicwebOutput()), &prefix, defaultOpts()) |
| if err != io.EOF { |
| t.Fatal(err) |
| } |
| if s == nil { |
| t.Fatal("snapshot is nil") |
| } |
| compareString(t, "panic: Here's a snapshot of a normal web server.\n\n", prefix.String()) |
| compareString(t, "", string(suffix)) |
| if s.RemoteGOROOT != "" { |
| t.Fatalf("unexpected RemoteGOROOT: %q", s.RemoteGOROOT) |
| } |
| if !s.guessPaths() { |
| t.Error("expected success") |
| } |
| if s.RemoteGOROOT != strings.Replace(runtime.GOROOT(), "\\", "/", -1) { |
| t.Fatalf("RemoteGOROOT mismatch; want:%q got:%q", runtime.GOROOT(), s.RemoteGOROOT) |
| } |
| if got := len(s.Goroutines); got < 30 { |
| t.Fatalf("unexpected Goroutines; want at least 30, got %d", got) |
| } |
| // The goal here is not to find the exact match since it'll change across |
| // 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 { |
| types = append(types, identifyPanicwebSignature(t, b, pwebDir)) |
| } |
| // Count the expected types. |
| if v := pstCount(types, pstUnknown); v != 0 { |
| t.Fatalf("found %d unknown signatures", v) |
| } |
| if v := pstCount(types, pstMain); v != 1 { |
| t.Fatalf("found %d pstMain signatures", v) |
| } |
| if v := pstCount(types, pstURL1handler); v != 1 && v != 2 { |
| t.Fatalf("found %d URL1Handler signatures", v) |
| } |
| if v := pstCount(types, pstURL2handler); v != 1 && v != 2 { |
| t.Fatalf("found %d URL2Handler signatures", v) |
| } |
| if v := pstCount(types, pstClient); v == 0 { |
| t.Fatalf("found %d client signatures", v) |
| } |
| if v := pstCount(types, pstServe); v != 1 { |
| t.Fatalf("found %d serve signatures", v) |
| } |
| if v := pstCount(types, pstColorable); v != 1 { |
| t.Fatalf("found %d colorable signatures", v) |
| } |
| if v := pstCount(types, pstStdlib); v < 3 { |
| t.Fatalf("found %d stdlib signatures", v) |
| } |
| } |
| |
| func TestIsGomodule(t *testing.T) { |
| t.Parallel() |
| pwd, err := os.Getwd() |
| if err != nil { |
| t.Fatal(err) |
| } |
| // Our internal functions work with '/' as path separator. |
| parts := splitPath(strings.Replace(pwd, "\\", "/", -1)) |
| gmc := gomodCache{} |
| root, importPath := gmc.isGoModule(parts) |
| if want := strings.Join(parts[:len(parts)-1], "/"); want != root { |
| t.Errorf("want: %q, got: %q", want, root) |
| } |
| if want := "github.com/maruel/panicparse/v2"; want != importPath { |
| t.Errorf("want: %q, got: %q", want, importPath) |
| } |
| got := reModule.FindStringSubmatch("foo\r\nmodule bar\r\nbaz") |
| if diff := cmp.Diff([]string{"module bar\r", "bar"}, got); diff != "" { |
| t.Fatalf("-want, +got:\n%s", diff) |
| } |
| } |
| |
| func TestAtou(t *testing.T) { |
| t.Parallel() |
| if i, b := atou([]byte("a")); i != 0 || b { |
| t.Error("oops") |
| } |
| } |
| |
| func TestTrimLeftSpace(t *testing.T) { |
| t.Parallel() |
| if trimLeftSpace(nil) != nil { |
| t.Error("oops") |
| } |
| } |
| |
| func TestTrimCurlyBrackets(t *testing.T) { |
| t.Parallel() |
| data := []struct { |
| input []byte |
| want []byte |
| wantOpened, wantClosed int |
| }{ |
| {nil, nil, 0, 0}, |
| {[]byte(""), []byte(""), 0, 0}, |
| {[]byte("a"), []byte("a"), 0, 0}, |
| {[]byte("{a"), []byte("a"), 1, 0}, |
| {[]byte("{{a"), []byte("a"), 2, 0}, |
| {[]byte("{{a}}"), []byte("a"), 2, 2}, |
| {[]byte("{a}}"), []byte("a"), 1, 2}, |
| {[]byte("a}}"), []byte("a"), 0, 2}, |
| {[]byte("{}"), []byte(""), 1, 1}, |
| {[]byte("{{}}"), []byte(""), 2, 2}, |
| // Not expected in practice. |
| {[]byte("}{"), []byte("}{"), 0, 0}, |
| {[]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) { |
| gotOpened, got, gotClosed := trimCurlyBrackets(line.input) |
| if !bytes.Equal(line.want, got) { |
| t.Errorf("want %s, got %s", line.want, got) |
| } |
| equiv := bytes.TrimRight(bytes.TrimLeft(line.input, "{"), "}") |
| if !bytes.Equal(line.want, equiv) { |
| t.Errorf("want %s, got %s", line.want, got) |
| } |
| if line.wantOpened != gotOpened { |
| t.Errorf("want %d opening curly brackets, got %d", line.wantOpened, gotOpened) |
| } |
| if line.wantClosed != gotClosed { |
| t.Errorf("want %d closing curly brackets, got %d", line.wantClosed, gotClosed) |
| } |
| }) |
| } |
| } |
| |
| func BenchmarkScanSnapshot_Guess(b *testing.B) { |
| b.ReportAllocs() |
| data := internaltest.StaticPanicwebOutput() |
| opts := defaultOpts() |
| b.ResetTimer() |
| for i := 0; i < b.N; i++ { |
| s, _, err := ScanSnapshot(bytes.NewReader(data), ioutil.Discard, opts) |
| if err != io.EOF { |
| b.Fatal(err) |
| } |
| if s == nil { |
| b.Fatal("missing context") |
| } |
| } |
| } |
| |
| func BenchmarkScanSnapshot_NoGuess(b *testing.B) { |
| b.ReportAllocs() |
| data := internaltest.StaticPanicwebOutput() |
| opts := defaultOpts() |
| b.ResetTimer() |
| for i := 0; i < b.N; i++ { |
| s, _, err := ScanSnapshot(bytes.NewReader(data), ioutil.Discard, opts) |
| if err != io.EOF { |
| b.Fatal(err) |
| } |
| if s == nil { |
| b.Fatal("missing context") |
| } |
| } |
| } |
| |
| func BenchmarkScanSnapshot_Passthru(b *testing.B) { |
| b.ReportAllocs() |
| buf := make([]byte, b.N) |
| for i := range buf { |
| buf[i] = 'i' |
| if i%16 == 0 { |
| buf[i] = '\n' |
| } |
| } |
| prefix := bytes.Buffer{} |
| prefix.Grow(len(buf)) |
| r := bytes.NewReader(buf) |
| opts := defaultOpts() |
| b.ResetTimer() |
| s, suffix, err := ScanSnapshot(r, &prefix, opts) |
| if err != io.EOF { |
| b.Fatal(err) |
| } |
| if s != nil { |
| b.Fatalf("unexpected %v", s) |
| } |
| b.StopTimer() |
| if !bytes.Equal(prefix.Bytes(), buf) { |
| b.Fatal("unexpected prefix") |
| } |
| if len(suffix) != 0 { |
| b.Fatal("unexpected suffix") |
| } |
| } |
| |
| // |
| |
| type panicwebSignatureType int |
| |
| const ( |
| pstUnknown panicwebSignatureType = iota |
| pstMain |
| pstURL1handler |
| pstURL2handler |
| pstClient |
| pstServe |
| pstColorable |
| pstStdlib |
| ) |
| |
| func pstCount(s []panicwebSignatureType, t panicwebSignatureType) int { |
| i := 0 |
| for _, v := range s { |
| if v == t { |
| i++ |
| } |
| } |
| return i |
| } |
| |
| // identifyPanicwebSignature tries to assign one of the predefined signature to |
| // the bucket provided. |
| // |
| // One challenge is that the path will be different depending if this test is |
| // run within GOPATH or outside. |
| func identifyPanicwebSignature(t *testing.T, b *Bucket, pwebDir string) panicwebSignatureType { |
| ver := "" |
| if !isInGOPATH { |
| ver = "/v2" |
| } |
| |
| // The first bucket (the one calling panic()) is deterministic. |
| if b.First { |
| if len(b.IDs) != 1 { |
| t.Fatal("first bucket is not correct") |
| return pstUnknown |
| } |
| want := Signature{ |
| State: "running", |
| Stack: Stack{ |
| Calls: []Call{ |
| newCallLocal("main.main", Args{}, pathJoin(pwebDir, "main.go"), 80), |
| }, |
| }, |
| } |
| similarSignatures(t, &want, &b.Signature) |
| return pstMain |
| } |
| |
| // We should find exactly 10 sleeping routines in the URL1Handler handler |
| // signature and 3 in URL2Handler. |
| if s := b.Stack.Calls[0].Func.Name; s == "URL1Handler" || s == "URL2Handler" { |
| if b.State != "chan receive" { |
| t.Fatalf("suspicious: %#v", b) |
| return pstUnknown |
| } |
| if b.Stack.Calls[0].ImportPath != "github.com/maruel/panicparse"+ver+"/cmd/panicweb/internal" { |
| t.Fatalf("suspicious: %q\n%#v", b.Stack.Calls[0].ImportPath, b) |
| return pstUnknown |
| } |
| if b.Stack.Calls[0].SrcName != "internal.go" { |
| t.Fatalf("suspicious: %#v", b) |
| return pstUnknown |
| } |
| if b.CreatedBy.Calls[0].SrcName != "server.go" { |
| t.Fatalf("suspicious: %#v", b) |
| return pstUnknown |
| } |
| if b.CreatedBy.Calls[0].ImportPath != "net/http" { |
| t.Fatalf("suspicious: %#v", b) |
| return pstUnknown |
| } |
| if b.CreatedBy.Calls[0].Func.Name != "(*Server).Serve" { |
| t.Fatalf("suspicious: %#v", b) |
| return pstUnknown |
| } |
| if s == "URL1Handler" { |
| return pstURL1handler |
| } |
| return pstURL2handler |
| } |
| |
| // Find the client goroutine signatures. For the client, it is likely that |
| // they haven't all bucketed perfectly. |
| if b.CreatedBy.Calls[0].ImportPath == "github.com/maruel/panicparse"+ver+"/cmd/panicweb/internal" && b.CreatedBy.Calls[0].Func.Name == "GetAsync" { |
| // TODO(maruel): More checks. |
| return pstClient |
| } |
| |
| // Now find the two goroutine started by main. |
| if b.CreatedBy.Calls[0].ImportPath == "github.com/maruel/panicparse"+ver+"/cmd/panicweb" && b.CreatedBy.Calls[0].Func.ImportPath == "main" && b.CreatedBy.Calls[0].Func.Name == "main" { |
| if b.State == "IO wait" { |
| return pstServe |
| } |
| if b.State == "chan receive" { |
| // Warning: This is brittle and will fail whenever go-colorable is |
| // updated so only check the string minimum here. |
| if !b.Signature.Locked { |
| t.Fatal("expected Locked") |
| } |
| want := Stack{Calls: []Call{newCallLocal("main.main", Args{}, pathJoin(pwebDir, "main.go"), 139)}} |
| 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") { |
| return pstColorable |
| } |
| } |
| t.Fatalf("failed to find go-colorable\n%# v", b.Signature.Stack.Calls) |
| } |
| // That's the unix.Nanosleep() or windows.SleepEx() call. |
| if b.State == "syscall" { |
| { |
| want := Stack{ |
| Calls: []Call{ |
| newCallLocal("main.main", Args{}, pathJoin(pwebDir, "main.go"), 63), |
| }, |
| } |
| zapStacks(t, &want, &b.CreatedBy) |
| compareStacks(t, &want, &b.CreatedBy) |
| } |
| if l := len(b.IDs); l != 1 { |
| t.Fatalf("expected 1 goroutine for the signature, got %d", l) |
| } |
| if l := len(b.Stack.Calls); l != 4 { |
| t.Fatalf("expected %d calls, got %d", 4, l) |
| } |
| if runtime.GOOS == "windows" { |
| if s := b.Stack.Calls[0].RelSrcPath; s != "runtime/syscall_windows.go" { |
| t.Fatalf("expected %q file, got %q", "runtime/syscall_windows.go", s) |
| } |
| } else { |
| // The first item shall be an assembly file independent of the OS. |
| if s := b.Stack.Calls[0].RelSrcPath; !strings.HasSuffix(s, ".s") { |
| t.Fatalf("expected assembly file, got %q", s) |
| } |
| } |
| // Process the golang.org/x/sys call specifically. |
| path := "golang.org/x/sys/unix" |
| fn := "Nanosleep" |
| mainOS := "main_unix.go" |
| if runtime.GOOS == "windows" { |
| path = "golang.org/x/sys/windows" |
| fn = "SleepEx" |
| mainOS = "main_windows.go" |
| } |
| usingModules := internaltest.IsUsingModules() |
| if b.Stack.Calls[1].Func.ImportPath != path || b.Stack.Calls[1].Func.Name != fn { |
| t.Fatalf("expected %q & %q, got %#v", path, fn, b.Stack.Calls[1].Func) |
| } |
| prefix := "golang.org/x/sys@v0.0.0-" |
| if !usingModules { |
| // Assert that there's no version by including the trailing /. |
| prefix = "golang.org/x/sys/" |
| } |
| if !strings.HasPrefix(b.Stack.Calls[1].RelSrcPath, prefix) { |
| t.Fatalf("expected %q, got %q", prefix, b.Stack.Calls[1].RelSrcPath) |
| } |
| if usingModules { |
| // Assert that it's using @v0-0-0.<date>-<commit> format. |
| ver := strings.SplitN(b.Stack.Calls[1].RelSrcPath[len(prefix):], "/", 2)[0] |
| re := regexp.MustCompile(`^\d{14}-[a-f0-9]{12}$`) |
| if !re.MatchString(ver) { |
| t.Fatalf("unexpected version string %q", ver) |
| } |
| } |
| { |
| want := []Call{ |
| newCallLocal("main.sysHang", Args{}, pathJoin(pwebDir, mainOS), 12), |
| newCallLocal( |
| "main.main.func3", |
| ifCombinedAggregateArgs( |
| Args{}, |
| // else |
| Args{Values: []Arg{{Value: 0xc000140720, Name: "#135", IsPtr: true}}}, |
| ), |
| pathJoin(pwebDir, "main.go"), |
| 65), |
| } |
| got := b.Stack.Calls[2:] |
| for i := range want { |
| zapCalls(t, &want[i], &got[i]) |
| } |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Fatalf("rest of stack mismatch (-want +got):\n%s", diff) |
| } |
| } |
| return pstStdlib |
| } |
| t.Fatalf("suspicious: %# v", b) |
| return pstUnknown |
| } |
| |
| // The rest should all be created with internal threads. |
| if b.CreatedBy.Calls[0].Location == Stdlib { |
| return pstStdlib |
| } |
| |
| // On older Go version, there's often an assembly stack in asm_amd64.s. |
| if b.CreatedBy.Calls[0].Func.Complete == "" { |
| if len(b.Stack.Calls) == 1 && b.Stack.Calls[0].Func.Complete == "runtime.goexit" { |
| return pstStdlib |
| } |
| } |
| t.Logf("CreatedBy import: %s", b.CreatedBy.Calls[0].ImportPath) |
| t.Logf("CreatedBy:\n%#v", b.CreatedBy) |
| t.Fatalf("unexpected thread started by non-stdlib:\n%#v", b.Stack.Calls) |
| return pstUnknown |
| } |
| |
| // |
| |
| func defaultOpts() *Opts { |
| o := DefaultOpts() |
| o.GuessPaths = false |
| o.AnalyzeSources = false |
| return o |
| } |
| |
| // getPanicParseDir returns the path to the root directory of panicparse |
| // package, using "/" as path separator. |
| func getPanicParseDir(t *testing.T) string { |
| // We assume that the working directory is the directory containing this |
| // source. In Go test framework, this normally holds true. If this ever |
| // becomes false, let's fix this. |
| thisDir, err := os.Getwd() |
| if err != nil { |
| t.Fatal(err) |
| } |
| // "/" is used even on Windows in the stack trace, return in this format to |
| // simply our life. |
| return strings.Replace(filepath.Dir(thisDir), "\\", "/", -1) |
| } |
| |
| 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)) |
| b := filepath.Dir(p) |
| if err := os.MkdirAll(b, 0700); err != nil { |
| t.Fatal(err) |
| } |
| if err := ioutil.WriteFile(p, []byte(content), 0600); err != nil { |
| t.Fatal(err) |
| } |
| } |
| } |
| |
| // ifCombinedAggregateArgs returns one of the two provided Args structs, based |
| // on the go compiler version. For 1.17 and above, combined is returned. For pre |
| // 1.17, separate is returned. |
| func ifCombinedAggregateArgs(combined, separate Args) Args { |
| if combinedAggregateArgs { |
| return combined |
| } |
| return separate |
| } |