blob: 7799f1216398c2f41061c539923f99d039aa5afa [file]
// 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 (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/maruel/panicparse/v2/internal/internaltest"
)
func TestAugment(t *testing.T) {
t.Parallel()
gm := map[string]string{"/root": "main"}
newCallSrc := func(f string, a Args, s string, l int) Call {
c := newCall(f, a, s, l)
// Simulate findRoots().
if !c.updateLocations(goroot, goroot, gm, gopaths) {
t.Fatalf("c.updateLocations(%v, %v, %v, %v) failed on %s", goroot, goroot, gm, gopaths, s)
}
return c
}
data := []struct {
name string
input string
// Starting with go1.11, inlining is enabled. The stack trace may (it
// depends on tool chain version) not contain much information about the
// arguments and shows as elided. Non-pointer call may show an elided
// argument, while there was no argument listed before.
mayBeInlined bool
want Stack
}{
{
"Local function doesn't interfere",
`func main() {
f("yo")
}
func f(s string) {
a := func(i int) int {
return 1 + i
}
_ = a(3)
panic("ooh")
}`,
false,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 2}},
Processed: []string{"string(0x2fffffff, len=2)"},
},
"/root/main.go",
10),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"func",
`func main() {
f(func() string { return "ooh" })
}
func f(a func() string) {
panic(a())
}`,
true,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{Value: pointer, IsPtr: true}},
Processed: []string{"func(0x2fffffff)"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"func ellipsis",
`func main() {
f(func() string { return "ooh" })
}
func f(a ...func() string) {
panic(a[0]())
}`,
true,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 1}, {Value: 1}},
// TODO(maruel): Handle.
Processed: []string{"<unknown>(0x2fffffff)", "<unknown>(0x1)"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"interface{}",
`func main() {
f(make([]interface{}, 5, 7))
}
func f(a []interface{}) {
panic("ooh")
}`,
true,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 5}, {Value: 7}},
Processed: []string{"[]interface{}(0x2fffffff len=5 cap=7)"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"[]int",
`func main() {
f(make([]int, 5, 7))
}
func f(a []int) {
panic("ooh")
}`,
true,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 5}, {Value: 7}},
Processed: []string{"[]int(0x2fffffff len=5 cap=7)"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"[]interface{}",
`func main() {
f([]interface{}{"ooh"})
}
func f(a []interface{}) {
panic(a[0].(string))
}`,
true,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 1}, {Value: 1}},
Processed: []string{"[]interface{}(0x2fffffff len=1 cap=1)"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"map[int]int",
`func main() {
f(map[int]int{1: 2})
}
func f(a map[int]int) {
panic("ooh")
}`,
true,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{Value: pointer, IsPtr: true}},
Processed: []string{"map[int]int(0x2fffffff)"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"map[interface{}]interface{}",
`func main() {
f(make(map[interface{}]interface{}))
}
func f(a map[interface{}]interface{}) {
panic("ooh")
}`,
true,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{Value: pointer, IsPtr: true}},
Processed: []string{"map[interface{}]interface{}(0x2fffffff)"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"chan int",
`func main() {
f(make(chan int))
}
func f(a chan int) {
panic("ooh")
}`,
true,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{Value: pointer, IsPtr: true}},
Processed: []string{"chan int(0x2fffffff)"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"chan interface{}",
`func main() {
f(make(chan interface{}))
}
func f(a chan interface{}) {
panic("ooh")
}`,
true,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{Value: pointer, IsPtr: true}},
Processed: []string{"chan interface{}(0x2fffffff)"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"non-pointer method",
`func main() {
var s S
s.f()
}
type S struct {}
func (s S) f() {
panic("ooh")
}`,
true,
Stack{
Calls: []Call{
newCallSrc(
"main.S.f",
Args{},
"/root/main.go",
8),
newCallSrc("main.main", Args{}, "/root/main.go", 4),
},
},
},
{
"pointer method",
`func main() {
var s S
s.f()
}
type S struct {}
func (s *S) f() {
panic("ooh")
}`,
true,
Stack{
Calls: []Call{
newCallSrc(
"main.(*S).f",
Args{
Values: []Arg{{Value: pointer, IsPtr: true}},
Processed: []string{"*S(0x2fffffff)"},
},
"/root/main.go",
8),
newCallSrc("main.main", Args{}, "/root/main.go", 4),
},
},
},
{
"string",
`func main() {
f("ooh")
}
func f(s string) {
panic(s)
}`,
true,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 3}},
Processed: []string{"string(0x2fffffff, len=3)"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"string and int",
`func main() {
f("ooh", 42)
}
func f(s string, i int) {
panic(s)
}`,
true,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 3}, {Value: 42}},
Processed: []string{"string(0x2fffffff, len=3)", "42"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"values are elided",
`func main() {
f(0, 0, 0, 0, 0, 0, 0, 0, 42, 43, 44, 45, nil)
}
func f(s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, s12 int, s13 interface{}) {
panic("ooh")
}`,
true,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{}, {}, {}, {}, {}, {}, {}, {}, {Value: 42}, {Value: 43}},
Processed: []string{"0", "0", "0", "0", "0", "0", "0", "0", "42", "43"},
Elided: true,
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"error",
`import "errors"
func main() {
f(errors.New("ooh"))
}
func f(err error) {
panic(err.Error())
}`,
true,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{Value: pointer, IsPtr: true}, {Value: pointer, IsPtr: true}},
Processed: []string{"error(0x2fffffff)"},
},
"/root/main.go",
7),
newCallSrc("main.main", Args{}, "/root/main.go", 4),
},
},
},
{
"error unnamed",
`import "errors"
func main() {
f(errors.New("ooh"))
}
func f(error) {
panic("ooh")
}`,
true,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{Value: pointer, IsPtr: true}, {Value: pointer, IsPtr: true}},
Processed: []string{"error(0x2fffffff)"},
},
"/root/main.go",
7),
newCallSrc("main.main", Args{}, "/root/main.go", 4),
},
},
},
{
"float32",
`func main() {
f(0.5)
}
func f(v float32) {
panic("ooh")
}`,
true,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
// The value is NOT a pointer but floating point encoding is not
// deterministic.
Args{
Values: []Arg{{Value: pointer, IsPtr: true}},
Processed: []string{"0.5"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"float64",
`func main() {
f(0.5)
}
func f(v float64) {
panic("ooh")
}`,
true,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
// The value is NOT a pointer but floating point encoding is not
// deterministic.
Args{
Values: []Arg{{Value: pointer, IsPtr: true}},
Processed: []string{"0.5"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
}
for i, line := range data {
i := i
line := line
t.Run(fmt.Sprintf("%d-%s", i, line.name), func(t *testing.T) {
t.Parallel()
// Marshal the code a bit to make it nicer. Inject 'package main'.
lines := append([]string{"package main"}, strings.Split(line.input, "\n")...)
for j := 2; j < len(lines); j++ {
// Strip the 3 first tab characters. It's very adhoc but good enough here
// and makes test failure much more readable.
if lines[j][:3] != "\t\t\t" {
t.Fatal("expected line to start with 3 tab characters")
}
lines[j] = lines[j][3:]
}
input := strings.Join(lines, "\n")
// Create one temporary directory by subtest.
root, err := ioutil.TempDir("", "stack")
if err != nil {
t.Fatalf("failed to create temporary directory: %v", err)
}
defer func() {
if err2 := os.RemoveAll(root); err2 != nil {
t.Fatalf("failed to remove temporary directory %q: %v", root, err2)
}
}()
main := filepath.Join(root, "main.go")
if err := ioutil.WriteFile(main, []byte(input), 0500); err != nil {
t.Fatalf("failed to write %q: %v", main, err)
}
if runtime.GOOS == "windows" {
root = strings.Replace(root, pathSeparator, "/", -1)
}
const prefix = "/root"
for j, c := range line.want.Calls {
if strings.HasPrefix(c.RemoteSrcPath, prefix) {
line.want.Calls[j].RemoteSrcPath = root + c.RemoteSrcPath[len(prefix):]
}
if strings.HasPrefix(c.LocalSrcPath, prefix) {
line.want.Calls[j].LocalSrcPath = root + c.LocalSrcPath[len(prefix):]
}
if strings.HasPrefix(c.DirSrc, "root") {
line.want.Calls[j].DirSrc = path.Base(root) + c.DirSrc[4:]
}
}
// Run the command up to twice.
// Only disable inlining if necessary.
disableInline := hasInlining && line.mayBeInlined
content := getCrash(t, main, disableInline)
t.Log("First")
// Warning: this function modifies want.
testAugmentCommon(t, content, false, line.want)
// If inlining was disabled, try a second time but zap things out.
if disableInline {
for j := range line.want.Calls {
line.want.Calls[j].Args.Processed = nil
}
content := getCrash(t, main, false)
t.Log("Second")
testAugmentCommon(t, content, line.mayBeInlined, line.want)
}
})
}
}
func testAugmentCommon(t *testing.T, content []byte, mayBeInlined bool, want Stack) {
// Analyze it.
prefix := bytes.Buffer{}
s, suffix, err := ScanSnapshot(bytes.NewBuffer(content), &prefix, defaultOpts())
if err != nil {
t.Fatalf("failed to parse input: %v", err)
}
// On go1.4, there's one less empty line.
if got := prefix.String(); got != "panic: ooh\n\n" && got != "panic: ooh\n" {
t.Fatalf("Unexpected panic output:\n%#v", got)
}
compareString(t, "exit status 2\n", string(suffix))
if !s.guessPaths() {
t.Error("expected success")
}
if err := s.augment(); err != nil {
t.Errorf("augment() returned %v", err)
}
got := s.Goroutines[0].Signature.Stack
zapPointers(t, &want, &got)
// On go1.11 with non-pointer method, it shows elided argument where
// there used to be none before. It's only for test case "non-pointer
// method".
if mayBeInlined {
for j := range got.Calls {
if !want.Calls[j].Args.Elided {
got.Calls[j].Args.Elided = false
}
if got.Calls[j].Args.Values == nil {
want.Calls[j].Args.Values = nil
}
}
}
if diff := cmp.Diff(want, got); diff != "" {
t.Logf("Different (-want +got):\n%s", diff)
t.Logf("Output:\n%s", content)
t.FailNow()
}
}
func TestAugmentErr(t *testing.T) {
t.Parallel()
root, err := ioutil.TempDir("", "stack")
if err != nil {
t.Fatalf("failed to create temporary directory: %v", err)
}
defer func() {
if err2 := os.RemoveAll(root); err2 != nil {
t.Fatalf("failed to remove temporary directory %q: %v", root, err2)
}
}()
tree := map[string]string{
"bad.go": "bad content",
"foo.asm": "; good but ignored",
"good.go": "package main",
"no_access.go": "package main",
}
createTree(t, root, tree)
type dataLine struct {
name string
src string
line int
args Args
err error
}
// Note: these tests assumes an OS running in English-US locale. That should
// eventually be fixed, maybe by using regexes?
msg := "The system cannot find the file specified."
if runtime.GOOS != "windows" {
msg = "no such file or directory"
}
data := []dataLine{
{
name: "assembly is skipped",
src: "foo.asm",
args: Args{Values: []Arg{{}}},
err: fmt.Errorf("cannot load non-go file %q", filepath.Join(root, "foo.asm")),
},
{
name: "assembly is skipped (no arg)",
src: "foo.asm",
err: nil,
},
{
name: "invalid line number",
src: "good.go",
line: 2,
args: Args{Values: []Arg{{}}},
err: errors.New("line 2 is over line count of 1"),
},
{
name: "invalid line number (no arg)",
src: "good.go",
line: 2,
},
{
name: "missing file",
src: "missing.go",
args: Args{Values: []Arg{{}}},
err: fmt.Errorf("open %s: %s", filepath.Join(root, "missing.go"), msg),
},
{
name: "missing file (no arg)",
src: "missing.go",
},
{
name: "invalid go code (no arg)",
src: "bad.go",
},
{
name: "no I/O access (no arg)",
src: "no_access.go",
},
}
if internaltest.GetGoMinorVersion() > 11 {
// The format changed between 1.9 and 1.12.
data = append(data, dataLine{
name: "invalid go code",
src: "bad.go",
args: Args{Values: []Arg{{}}},
err: fmt.Errorf("failed to parse %s:1:1: expected 'package', found bad", filepath.Join(root, "bad.go")),
})
}
if runtime.GOOS != "windows" {
// Chmod has no effect on Windows.
compareErr(t, nil, os.Chmod(filepath.Join(root, "no_access.go"), 0))
data = append(data, dataLine{
name: "no I/O access",
src: "no_access.go",
args: Args{Values: []Arg{{}}},
err: fmt.Errorf("open %s: permission denied", filepath.Join(root, "no_access.go")),
})
}
for i, line := range data {
line := line
t.Run(fmt.Sprintf("%d-%s", i, line.name), func(t *testing.T) {
l := line.line
if l == 0 {
l = 1
}
s := Snapshot{
Goroutines: []*Goroutine{
{Signature: Signature{Stack: Stack{Calls: []Call{
{LocalSrcPath: filepath.Join(root, line.src), Args: line.args, Line: l},
}}}},
}}
compareErr(t, line.err, s.augment())
})
}
}
func TestLineToByteOffsets(t *testing.T) {
src := "\n\n\n"
want := []int{0, 0, 1, 2, 3}
if diff := cmp.Diff(want, lineToByteOffsets([]byte(src))); diff != "" {
t.Error(diff)
}
src = "hello"
want = []int{0, 0}
if diff := cmp.Diff(want, lineToByteOffsets([]byte(src))); diff != "" {
t.Error(diff)
}
src = "this\nis\na\ntest"
want = []int{0, 0, 5, 8, 10}
if diff := cmp.Diff(want, lineToByteOffsets([]byte(src))); diff != "" {
t.Error(diff)
}
}
//
const pointer = uint64(0x2fffffff)
const pointerStr = "0x2fffffff"
func overrideEnv(env []string, key, value string) []string {
prefix := key + "="
for i, e := range env {
if strings.HasPrefix(e, prefix) {
env[i] = prefix + value
return env
}
}
return append(env, prefix+value)
}
func getCrash(t *testing.T, main string, disableInline bool) []byte {
args := []string{"run"}
if disableInline {
args = append(args, "-gcflags", "-l")
}
cmd := exec.Command("go", append(args, main)...)
// Use the Go 1.4 compatible format.
cmd.Env = overrideEnv(os.Environ(), "GOTRACEBACK", "1")
out, err := cmd.CombinedOutput()
if err == nil {
t.Fatal("expected error since this is supposed to crash")
}
return out
}
// zapPointers zaps out pointers.
func zapPointers(t *testing.T, want, got *Stack) {
helper(t)()
for i := range got.Calls {
if i >= len(want.Calls) {
// When using GOTRACEBACK=2, it'll include runtime.main() and
// runtime.goexit(). Ignore these since they could be changed in a future
// version.
got.Calls = got.Calls[:len(want.Calls)]
break
}
for j := range got.Calls[i].Args.Values {
if j >= len(want.Calls[i].Args.Values) {
break
}
if want.Calls[i].Args.Values[j].Value == pointer {
// Replace the pointer value.
if got.Calls[i].Args.Values[j].Value == 0 {
t.Fatalf("Call %d, value %d, expected pointer, got 0", i, j)
}
old := fmt.Sprintf("0x%x", got.Calls[i].Args.Values[j].Value)
got.Calls[i].Args.Values[j].Value = pointer
for k := range got.Calls[i].Args.Processed {
got.Calls[i].Args.Processed[k] = strings.Replace(got.Calls[i].Args.Processed[k], old, pointerStr, -1)
}
}
}
}
}