| // Copyright 2015 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 ( |
| "flag" |
| "fmt" |
| "io" |
| "log" |
| "os" |
| "runtime" |
| "strings" |
| "testing" |
| |
| "github.com/google/go-cmp/cmp" |
| ) |
| |
| func TestFuncInit(t *testing.T) { |
| t.Parallel() |
| data := []struct { |
| raw string |
| want Func |
| }{ |
| { |
| "github.com/maruel/panicparse/cmd/panic/internal/%c3%b9tf8.(*Strùct).Pànic", |
| Func{ |
| Complete: "github.com/maruel/panicparse/cmd/panic/internal/ùtf8.(*Strùct).Pànic", |
| ImportPath: "github.com/maruel/panicparse/cmd/panic/internal/ùtf8", |
| DirName: "ùtf8", |
| Name: "(*Strùct).Pànic", |
| IsExported: true, |
| }, |
| }, |
| { |
| "gopkg.in/yaml%2ev2.handleErr", |
| Func{ |
| Complete: "gopkg.in/yaml.v2.handleErr", |
| ImportPath: "gopkg.in/yaml.v2", |
| DirName: "yaml.v2", |
| Name: "handleErr", |
| }, |
| }, |
| { |
| "github.com/maruel/panicparse/vendor/golang.org/x/sys/unix.Nanosleep", |
| Func{ |
| Complete: "github.com/maruel/panicparse/vendor/golang.org/x/sys/unix.Nanosleep", |
| ImportPath: "github.com/maruel/panicparse/vendor/golang.org/x/sys/unix", |
| DirName: "unix", |
| Name: "Nanosleep", |
| IsExported: true, |
| }, |
| }, |
| { |
| "main.func·001", |
| Func{ |
| Complete: "main.func·001", |
| ImportPath: "main", |
| DirName: "main", |
| Name: "func·001", |
| IsPkgMain: true, |
| }, |
| }, |
| { |
| "gc", |
| Func{ |
| Complete: "gc", |
| Name: "gc", |
| }, |
| }, |
| } |
| for _, line := range data { |
| got := newFunc(line.raw) |
| if diff := cmp.Diff(line.want, got); diff != "" { |
| t.Fatalf("Call mismatch (-want +got):\n%s", diff) |
| } |
| } |
| } |
| |
| func TestCallPkg(t *testing.T) { |
| t.Parallel() |
| data := []struct { |
| name string |
| f string |
| s string |
| // Expectations |
| DirSrc string |
| SrcName string |
| LocalSrcPath string |
| RelSrcPath string |
| ImportPath string |
| Location Location |
| }{ |
| { |
| name: "Pkg", |
| f: "gopkg.in/yaml%2ev2.handleErr", |
| s: "/gpremote/src/gopkg.in/yaml.v2/yaml.go", |
| DirSrc: pathJoin("yaml.v2", "yaml.go"), |
| SrcName: "yaml.go", |
| LocalSrcPath: "/gplocal/src/gopkg.in/yaml.v2/yaml.go", |
| RelSrcPath: "gopkg.in/yaml.v2/yaml.go", |
| ImportPath: "gopkg.in/yaml.v2", |
| Location: GOPATH, |
| }, |
| { |
| name: "PkgMod", |
| f: "gopkg.in/yaml%2ev2.handleErr", |
| s: "/gpremote/pkg/mod/gopkg.in/yaml.v2@v2.3.0/yaml.go", |
| DirSrc: pathJoin("yaml.v2@v2.3.0", "yaml.go"), |
| SrcName: "yaml.go", |
| LocalSrcPath: "/gplocal/pkg/mod/gopkg.in/yaml.v2@v2.3.0/yaml.go", |
| RelSrcPath: "gopkg.in/yaml.v2@v2.3.0/yaml.go", |
| ImportPath: "gopkg.in/yaml.v2@v2.3.0", |
| Location: GoPkg, |
| }, |
| { |
| name: "PkgMethod", |
| f: "gopkg.in/yaml%2ev2.(*decoder).unmarshal", |
| s: "/gpremote/src/gopkg.in/yaml.v2/yaml.go", |
| DirSrc: pathJoin("yaml.v2", "yaml.go"), |
| SrcName: "yaml.go", |
| LocalSrcPath: "/gplocal/src/gopkg.in/yaml.v2/yaml.go", |
| RelSrcPath: "gopkg.in/yaml.v2/yaml.go", |
| ImportPath: "gopkg.in/yaml.v2", |
| Location: GOPATH, |
| }, |
| { |
| name: "Stdlib", |
| f: "reflect.Value.assignTo", |
| s: "/grremote/src/reflect/value.go", |
| DirSrc: pathJoin("reflect", "value.go"), |
| SrcName: "value.go", |
| LocalSrcPath: "/grlocal/src/reflect/value.go", |
| RelSrcPath: "reflect/value.go", |
| ImportPath: "reflect", |
| Location: Stdlib, |
| }, |
| { |
| name: "Main", |
| f: "main.main", |
| s: "/gpremote/src/github.com/maruel/panicparse/cmd/pp/main.go", |
| DirSrc: pathJoin("pp", "main.go"), |
| SrcName: "main.go", |
| LocalSrcPath: "/gplocal/src/github.com/maruel/panicparse/cmd/pp/main.go", |
| RelSrcPath: "github.com/maruel/panicparse/cmd/pp/main.go", |
| ImportPath: "github.com/maruel/panicparse/cmd/pp", |
| Location: GOPATH, |
| }, |
| { |
| // See testPanicMismatched in context_test.go. |
| name: "Mismatched", |
| f: "github.com/maruel/panicparse/cmd/panic/internal/incorrect.Panic", |
| s: "/gpremote/src/github.com/maruel/panicparse/cmd/panic/internal/incorrect/correct.go", |
| DirSrc: pathJoin("incorrect", "correct.go"), |
| SrcName: "correct.go", |
| LocalSrcPath: "/gplocal/src/github.com/maruel/panicparse/cmd/panic/internal/incorrect/correct.go", |
| RelSrcPath: "github.com/maruel/panicparse/cmd/panic/internal/incorrect/correct.go", |
| ImportPath: "github.com/maruel/panicparse/cmd/panic/internal/incorrect", |
| Location: GOPATH, |
| }, |
| { |
| // See testPanicUTF8 in context_test.go. |
| name: "UTF8", |
| f: "github.com/maruel/panicparse/cmd/panic/internal/%c3%b9tf8.(*Strùct).Pànic", |
| s: "/gpremote/src/github.com/maruel/panicparse/cmd/panic/internal/ùtf8/utf8.go", |
| DirSrc: pathJoin("ùtf8", "utf8.go"), |
| SrcName: "utf8.go", |
| LocalSrcPath: "/gplocal/src/github.com/maruel/panicparse/cmd/panic/internal/ùtf8/utf8.go", |
| RelSrcPath: "github.com/maruel/panicparse/cmd/panic/internal/ùtf8/utf8.go", |
| ImportPath: "github.com/maruel/panicparse/cmd/panic/internal/ùtf8", |
| Location: GOPATH, |
| }, |
| { |
| name: "C", |
| f: "findrunnable", |
| s: "/grremote/src/runtime/proc.c", |
| DirSrc: pathJoin("runtime", "proc.c"), |
| SrcName: "proc.c", |
| LocalSrcPath: "/grlocal/src/runtime/proc.c", |
| RelSrcPath: "runtime/proc.c", |
| ImportPath: "runtime", |
| Location: Stdlib, |
| }, |
| { |
| name: "Gomod", |
| f: "example.com/foo/bar.Func", |
| s: "/gomod/bar/baz.go", |
| DirSrc: "bar/baz.go", |
| SrcName: "baz.go", |
| LocalSrcPath: "/gomod/bar/baz.go", |
| RelSrcPath: "bar/baz.go", |
| ImportPath: "example.com/foo/bar", |
| Location: GoMod, |
| }, |
| { |
| name: "GomodMain", |
| f: "main.main", |
| s: "/gomod/cmd/panic/main.go", |
| DirSrc: "panic/main.go", |
| SrcName: "main.go", |
| LocalSrcPath: "/gomod/cmd/panic/main.go", |
| RelSrcPath: "cmd/panic/main.go", |
| ImportPath: "example.com/foo/cmd/panic", |
| Location: GoMod, |
| }, |
| } |
| 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) |
| compareString(t, line.DirSrc, c.DirSrc) |
| compareString(t, line.SrcName, c.SrcName) |
| // Equivalent of calling GuessPaths(). |
| gp := map[string]string{"/gpremote": "/gplocal"} |
| gm := map[string]string{"/gomod": "example.com/foo"} |
| if !c.updateLocations("/grremote", "/grlocal", gm, gp) { |
| t.Error("Unexpected") |
| } |
| compareString(t, line.ImportPath, c.ImportPath) |
| if line.Location != c.Location { |
| t.Errorf("want %s, got %s", line.Location, c.Location) |
| } |
| compareString(t, line.LocalSrcPath, c.LocalSrcPath) |
| compareString(t, line.RelSrcPath, c.RelSrcPath) |
| }) |
| } |
| } |
| |
| func TestArgs(t *testing.T) { |
| t.Parallel() |
| a := Args{ |
| Values: []Arg{ |
| {Value: 0x4}, |
| {Value: 0x7fff671c7118}, |
| {Value: 0xffffffff00000080}, |
| {}, |
| {Value: 0xffffffff0028c1be}, |
| {Name: "foo"}, |
| {}, |
| {}, |
| {IsOffsetTooLarge: true}, |
| {IsAggregate: true}, |
| {IsAggregate: true, Fields: Args{ |
| Values: []Arg{{IsAggregate: true, Fields: Args{ |
| Values: []Arg{{IsAggregate: true}}, |
| }}}, |
| }}, |
| {IsAggregate: true, Fields: Args{ |
| Values: []Arg{{Value: 0x1}, {Value: 0x7fff671c7118}}, |
| }}, |
| {IsAggregate: true, Fields: Args{ |
| Values: []Arg{{IsAggregate: true, Fields: Args{ |
| Values: []Arg{{Value: 0x5}}, Elided: true, |
| }}}, |
| }}, |
| {IsAggregate: true, Fields: Args{Elided: true}}, |
| {IsAggregate: true, Fields: Args{ |
| Values: []Arg{{IsAggregate: true, Fields: Args{ |
| Values: []Arg{{IsOffsetTooLarge: true}, {IsOffsetTooLarge: true}}, |
| }}}, |
| }}, |
| }, |
| Elided: true, |
| } |
| compareString(t, "4, 0x7fff671c7118, 0xffffffff00000080, 0, 0xffffffff0028c1be, foo, "+ |
| "0, 0, _, {}, {{{}}}, {1, 0x7fff671c7118}, {{5, ...}}, {...}, {{_, _}}, ...", a.String()) |
| |
| a = Args{Processed: []string{"yo"}} |
| compareString(t, "yo", a.String()) |
| } |
| |
| func TestSignature(t *testing.T) { |
| t.Parallel() |
| s := getSignature() |
| compareString(t, "", s.SleepString()) |
| s.SleepMax = 10 |
| compareString(t, "0~10 minutes", s.SleepString()) |
| s.SleepMin = 10 |
| compareString(t, "10 minutes", s.SleepString()) |
| } |
| |
| func TestSignature_Equal(t *testing.T) { |
| t.Parallel() |
| s1 := getSignature() |
| s2 := getSignature() |
| if !s1.equal(s2) { |
| t.Fatal("equal") |
| } |
| s2.State = "foo" |
| if s1.equal(s2) { |
| t.Fatal("inequal") |
| } |
| } |
| |
| func TestSignature_Similar(t *testing.T) { |
| t.Parallel() |
| s1 := getSignature() |
| s2 := getSignature() |
| if !s1.similar(s2, ExactFlags) { |
| t.Fatal("equal") |
| } |
| s2.State = "foo" |
| if s1.similar(s2, ExactFlags) { |
| t.Fatal("inequal") |
| } |
| } |
| |
| func TestSignature_Less(t *testing.T) { |
| t.Parallel() |
| s1 := getSignature() |
| s2 := getSignature() |
| if s1.less(s2) || s2.less(s1) { |
| t.Fatal("less") |
| } |
| s2.State = "foo" |
| if !s1.less(s2) || s2.less(s1) { |
| t.Fatal("not less") |
| } |
| s2 = getSignature() |
| s2.Stack.Calls = s2.Stack.Calls[:1] |
| if !s1.less(s2) || s2.less(s1) { |
| t.Fatal("not less") |
| } |
| } |
| |
| // |
| |
| var ( |
| goroot string |
| gopaths map[string]string |
| gopath string |
| gomods map[string]string |
| isInGOPATH bool |
| ) |
| |
| func init() { |
| goroot = strings.Replace(runtime.GOROOT(), "\\", "/", -1) |
| gopaths = map[string]string{} |
| for i, p := range getGOPATHs() { |
| gopaths[p] = p |
| if i == 0 { |
| gopath = p |
| } |
| } |
| |
| // Assumes pwd == this directory. |
| pwd, err := os.Getwd() |
| if err != nil { |
| panic(err) |
| } |
| // Our internal functions work with '/' as path separator. |
| pwd = strings.Replace(pwd, "\\", "/", -1) |
| gomods = map[string]string{} |
| gmc := gomodCache{} |
| if prefix, path := gmc.isGoModule(splitPath(pwd)); prefix != "" { |
| gomods[prefix] = path |
| } |
| |
| // When inside GOPATH, no version is added. When outside, the version path is |
| // added from the reading of module statement in go.mod. |
| for _, p := range getGOPATHs() { |
| if strings.HasPrefix(pwd, p) { |
| isInGOPATH = true |
| break |
| } |
| } |
| } |
| |
| func newFunc(s string) Func { |
| f := Func{} |
| if s != "" { |
| if err := f.Init(s); err != nil { |
| panic(err) |
| } |
| } |
| return f |
| } |
| |
| func newCall(f string, a Args, s string, l int) Call { |
| c := Call{Func: newFunc(f), Args: a} |
| c.init(s, l) |
| return c |
| } |
| |
| func newCallLocal(f string, a Args, s string, l int) Call { |
| c := newCall(f, a, s, l) |
| r := c.updateLocations(goroot, goroot, gomods, gopaths) |
| if !r { |
| panic("Unexpected") |
| } |
| if c.LocalSrcPath == "" || c.RelSrcPath == "" { |
| panic(fmt.Sprintf("newCallLocal(%q, %q): invariant failed; gomods=%v, GOPATHs=%v", f, s, gomods, gopaths)) |
| } |
| return c |
| } |
| |
| func compareErr(t *testing.T, want, got error) { |
| if want == nil && got == nil { |
| return |
| } |
| if want == nil || got == nil { |
| t.Helper() |
| t.Errorf("want: %v, got: %v", want, got) |
| } else if want.Error() != got.Error() { |
| t.Helper() |
| t.Errorf("want: %q, got: %q", want.Error(), got.Error()) |
| } |
| } |
| |
| func compareString(t *testing.T, want, got string) { |
| if want != got { |
| t.Helper() |
| t.Fatalf("%q != %q", want, got) |
| } |
| } |
| |
| // similarGoroutines compares slice of Goroutine to be similar enough. |
| // |
| // Warning: it mutates inputs. |
| func similarGoroutines(t *testing.T, want, got []*Goroutine) { |
| zapGoroutines(t, want, got) |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Helper() |
| t.Fatalf("Goroutine mismatch (-want +got):\n%s", diff) |
| } |
| } |
| |
| func zapGoroutines(t *testing.T, want, got []*Goroutine) { |
| if len(want) != len(got) { |
| t.Helper() |
| t.Error("different []*Goroutine length") |
| return |
| } |
| for i := range want { |
| // &(*Goroutine).Signature |
| zapSignatures(t, &want[i].Signature, &got[i].Signature) |
| } |
| } |
| |
| // similarSignatures compares Signature to be similar enough. |
| // |
| // Warning: it mutates inputs. |
| func similarSignatures(t *testing.T, want, got *Signature) { |
| zapSignatures(t, want, got) |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Helper() |
| t.Fatalf("Signature mismatch (-want +got):\n%s", diff) |
| } |
| } |
| |
| func zapSignatures(t *testing.T, want, got *Signature) { |
| // Signature.Stack.([]Call) |
| t.Helper() |
| if len(want.Stack.Calls) != len(got.Stack.Calls) { |
| t.Error("different call length") |
| return |
| } |
| if len(want.CreatedBy.Calls) != 0 && len(got.CreatedBy.Calls) != 0 { |
| if want.CreatedBy.Calls[0].Line != 0 && got.CreatedBy.Calls[0].Line != 0 { |
| want.CreatedBy.Calls[0].Line = 42 |
| got.CreatedBy.Calls[0].Line = 42 |
| } |
| } |
| zapStacks(t, &want.Stack, &got.Stack) |
| } |
| |
| func zapStacks(t *testing.T, want, got *Stack) { |
| if len(want.Calls) != len(got.Calls) { |
| t.Helper() |
| t.Error("different Stack.[]Call length") |
| return |
| } |
| if len(want.Calls) != 0 { |
| t.Helper() |
| } |
| for i := range want.Calls { |
| zapCalls(t, &want.Calls[i], &got.Calls[i]) |
| } |
| } |
| |
| func zapCalls(t *testing.T, want, got *Call) { |
| t.Helper() |
| if want.Line != 0 && got.Line != 0 { |
| want.Line = 42 |
| got.Line = 42 |
| } |
| zapArgs(t, &want.Args, &got.Args) |
| } |
| |
| func zapArgs(t *testing.T, want, got *Args) { |
| if len(want.Values) != len(got.Values) { |
| t.Helper() |
| t.Error("different Args.Values length") |
| return |
| } |
| if len(want.Values) != 0 { |
| t.Helper() |
| } |
| for i := range want.Values { |
| if want.Values[i].Value != 0 && got.Values[i].Value != 0 { |
| want.Values[i].Value = 42 |
| got.Values[i].Value = 42 |
| } |
| if want.Values[i].IsAggregate && got.Values[i].IsAggregate { |
| zapArgs(t, &want.Values[i].Fields, &got.Values[i].Fields) |
| } |
| if want.Values[i].Name != "" && got.Values[i].Name != "" { |
| want.Values[i].Name = "foo" |
| got.Values[i].Name = "foo" |
| } |
| if want.Values[i].IsInaccurate && !got.Values[i].IsInaccurate { |
| want.Values[i].IsInaccurate = false |
| } |
| } |
| } |
| |
| func compareGoroutines(t *testing.T, want, got []*Goroutine) { |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Helper() |
| t.Fatalf("Goroutine mismatch (-want +got):\n%s", diff) |
| } |
| } |
| |
| func compareStacks(t *testing.T, want, got *Stack) { |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Helper() |
| t.Fatalf("Stack mismatch (-want +got):\n%s", diff) |
| } |
| } |
| |
| func getSignature() *Signature { |
| return &Signature{ |
| State: "chan receive", |
| Stack: Stack{ |
| Calls: []Call{ |
| { |
| Func: newFunc("main.func·001"), |
| Args: Args{Values: []Arg{{Value: 0x11000000}, {Value: 2}}}, |
| RemoteSrcPath: "/gopath/src/github.com/maruel/panicparse/stack/stack.go", |
| Line: 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, |
| }, |
| { |
| Func: newFunc("DoStuff"), |
| Args: Args{Values: []Arg{{Value: 0x11000000}, {Value: 2}}}, |
| RemoteSrcPath: "/gopath/src/foo/bar.go", |
| Line: 72, |
| }, |
| { |
| Func: newFunc("doStuffInternal"), |
| Args: Args{ |
| Values: []Arg{{Value: 0x11000000}, {Value: 2}}, |
| Elided: true, |
| }, |
| RemoteSrcPath: "/gopath/src/foo/bar.go", |
| Line: 72, |
| }, |
| }, |
| }, |
| } |
| } |
| |
| // TestMain manages a temporary directory to build on first use ../cmd/panic |
| // and clean up at the end. |
| func TestMain(m *testing.M) { |
| flag.Parse() |
| if !testing.Verbose() { |
| log.SetOutput(io.Discard) |
| } |
| os.Exit(m.Run()) |
| } |