blob: 173a29863f1fa89e1d7b22902351cd30992b9ef9 [file] [log] [blame] [edit]
// 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())
}