blob: df1461bc5dae0e5038299a96759bc7b7a088196d [file] [log] [blame]
// 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"
"fmt"
"math/bits"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"runtime"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/maruel/panicparse/v2/internal/internaltest"
)
// goarchList is a list of GOARCH values.
type goarchList []string
var allPlatforms goarchList = nil
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
}
// For test case "negative int32".
negInt := uint64(4294967173)
negPtr := false
if bits.UintSize == 64 {
negInt = 828928688005
negPtr = true
}
type testCase struct {
name string
input string
// Due to inlining, 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
// archBlock lists the CPU architectures to skip this test case on.
//
// Many test are hard to parse in 32 bits. Eventually we should fix these
// but I don't have time for this.
//
// The list is based on https://github.com/maruel/panicparse/issues/80.
// I was able to locally reproduce amd64 and 386 but not the rest.
archBlock goarchList
want Stack
}
data := []testCase{
{
"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")
}`,
// The function became inlinable in go 1.17.
true,
goarchList{"arm", "arm64", "ppc64le", "riscv64"},
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{IsAggregate: true, Fields: 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,
goarchList{"arm"},
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,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{IsAggregate: true, Fields: Args{
Values: []Arg{{Value: pointer, IsPtr: true}, {Value: 1}, {Value: 1}},
}}},
Processed: []string{"<unknown>{0x2fffffff, 0x1, 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,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{IsAggregate: true, Fields: 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,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{IsAggregate: true, Fields: 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,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{IsAggregate: true, Fields: 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,
allPlatforms,
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,
allPlatforms,
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,
allPlatforms,
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,
allPlatforms,
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,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.S.f",
Args{Values: []Arg{{IsAggregate: true, Fields: 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,
allPlatforms,
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,
goarchList{"arm", "arm64", "ppc64le", "riscv64"},
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{IsAggregate: true, Fields: 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,
goarchList{"arm", "arm64", "ppc64le", "riscv64"},
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{
{IsAggregate: true, Fields: 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,
allPlatforms,
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,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{IsAggregate: true, Fields: Args{
Values: []Arg{{Value: pointer, IsPtr: true}, {Value: pointer, IsPtr: true}},
}}},
Processed: []string{"error{0x2fffffff, 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,
goarchList{"386", "arm", "arm64", "mipsle", "mips64le", "ppc64le", "riscv64", "s390x"},
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{IsAggregate: true, Fields: Args{
Values: []Arg{
{Value: pointer, IsPtr: true},
{Value: pointer, IsPtr: true},
},
}}},
Processed: []string{"error{0x2fffffff, 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,
allPlatforms,
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,
goarchList{"386", "arm", "mipsle"},
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),
},
},
},
{
"uint",
`func main() {
f(123)
}
func f(v uint) {
panic("ooh")
}`,
true,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{Value: 123}},
Processed: []string{"123"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"negative int32",
`func main() {
f(-123)
}
func f(v int32) {
panic("ooh")
}`,
true,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{{Value: negInt, IsPtr: negPtr}},
Processed: []string{"-123"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"array",
`func main() {
f([3]byte{2, 3, 4})
}
func f(v2 [3]byte) {
panic("ooh")
}`,
true,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
Args{
Values: []Arg{
{IsAggregate: true, Fields: Args{
Values: []Arg{{Value: 2}, {Value: 3}, {Value: 4}},
}},
},
Processed: []string{"[3]byte{0x2, 0x3, 0x4}"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"deeply nested aggregate type",
`func main() {
f(a{b{c{d{13}}}})
}
func f(v a) {
panic("ooh")
}
type a struct{ b }
type b struct{ c }
type c struct{ d }
type d struct{ i int }`,
true,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
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{{Value: 13}},
}}},
}}},
}}},
}}},
Processed: []string{"a{0xd}"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"deeply nested aggregate type with elision",
`func main() {
f(a{b{c{d{e{13}}}}})
}
func f(v a) {
panic("ooh")
}
type a struct{ b }
type b struct{ c }
type c struct{ d }
type d struct{ e }
type e struct{ i int }`,
true,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.f",
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,
}}},
}}},
}}},
}}},
}}},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"argument offsets partially too large",
`func main() {
f(a{b{c{d{e{}}}}}, 13, []int{14, 15})
}
func f(v a, i int, s []int) {
panic("ooh")
}
type a struct{ b }
type b struct{ c }
type c struct{ d }
type d struct{ e }
type e struct{ i [27]int }`,
true,
goarchList{"386", "arm", "mipsle"},
Stack{
Calls: []Call{
newCallSrc(
"main.f",
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,
}}},
}}},
}}},
}}},
}},
{Value: 13},
{IsAggregate: true, Fields: Args{
Values: []Arg{
{Value: pointer, IsPtr: true},
{Value: 2},
{IsOffsetTooLarge: true},
},
}},
},
Processed: []string{"a{}", "13", "[]int(0x2fffffff len=2 cap=_)"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"argument offsets entirely too large",
`func main() {
f(a{b{c{d{e{}}}}}, 13, []int{14, 15})
}
func f(v a, i int, s []int) {
panic("ooh")
}
type a struct{ b }
type b struct{ c }
type c struct{ d }
type d struct{ e }
type e struct{ i [30]int }`,
true,
goarchList{"386", "arm", "mipsle"},
Stack{
Calls: []Call{
newCallSrc(
"main.f",
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,
}}},
}}},
}}},
}}},
}},
{IsOffsetTooLarge: true},
{IsAggregate: true, Fields: Args{
Values: []Arg{
{IsOffsetTooLarge: true},
{IsOffsetTooLarge: true},
{IsOffsetTooLarge: true},
},
}},
},
Processed: []string{"a{}", "_", "[]int(_ len=_ cap=_)"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
// The following subtests are adapted from TestTracebackArgs in
// src/runtime/traceback_test.go in the Go runtime.
{
"testTracebackArgs1",
`func main() {
testTracebackArgs1(1, 2, 3, 4, 5)
}
func testTracebackArgs1(a, b, c, d, e int) int {
panic("ooh")
}`,
true,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.testTracebackArgs1",
Args{
Values: []Arg{
{Value: 1}, {Value: 2}, {Value: 3}, {Value: 4}, {Value: 5},
},
Processed: []string{"1", "2", "3", "4", "5"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"testTracebackArgs2",
`func main() {
testTracebackArgs2(false, struct {
a, b, c int
x [2]int
}{1, 2, 3, [2]int{4, 5}}, [0]int{}, [3]byte{6, 7, 8})
}
func testTracebackArgs2(a bool, b struct {
a, b, c int
x [2]int
}, _ [0]int, d [3]byte) int {
panic("ooh")
}`,
true,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.testTracebackArgs2",
Args{
Values: []Arg{
{Value: 0},
{IsAggregate: true, Fields: Args{
Values: []Arg{
{Value: 1},
{Value: 2},
{Value: 3},
{IsAggregate: true, Fields: Args{
Values: []Arg{
{Value: 4},
{Value: 5},
},
}},
},
}},
{IsAggregate: true},
{IsAggregate: true, Fields: Args{
Values: []Arg{
{Value: 6},
{Value: 7},
{Value: 8},
},
}},
},
Processed: []string{"false", "<unknown>{0x1, 0x2, 0x3, 0x4, 0x5}", "[0]int{}", "[3]byte{0x6, 0x7, 0x8}"},
},
"/root/main.go",
12),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"testTracebackArgs3",
`func main() {
testTracebackArgs3([3]byte{1, 2, 3}, 4, 5, 6, [3]byte{7, 8, 9})
}
func testTracebackArgs3(x [3]byte, a, b, c int, y [3]byte) int {
panic("ooh")
}`,
true,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.testTracebackArgs3",
Args{
Values: []Arg{
{IsAggregate: true, Fields: Args{
Values: []Arg{
{Value: 1},
{Value: 2},
{Value: 3},
},
}},
{Value: 4},
{Value: 5},
{Value: 6},
{IsAggregate: true, Fields: Args{
Values: []Arg{
{Value: 7},
{Value: 8},
{Value: 9},
},
}},
},
Processed: []string{"[3]byte{0x1, 0x2, 0x3}", "4", "5", "6", "[3]byte{0x7, 0x8, 0x9}"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"testTracebackArgs4",
`func main() {
testTracebackArgs4(true, [1][1][1][1][1][1][1][1][1][1]int{})
}
func testTracebackArgs4(a bool, x [1][1][1][1][1][1][1][1][1][1]int) int {
panic("ooh")
}`,
true,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.testTracebackArgs4",
Args{
Values: []Arg{
{Value: 1},
{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,
}}},
}}},
}}},
}}},
}},
},
Processed: []string{"true"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"testTracebackArgs5",
`func main() {
z := [0]int{}
testTracebackArgs5(false, struct {
x int
y [0]int
z [2][0]int
}{1, z, [2][0]int{}}, z, z, z, z, z, z, z, z, z, z, z, z)
}
func testTracebackArgs5(a bool, x struct {
x int
y [0]int
z [2][0]int
}, _, _, _, _, _, _, _, _, _, _, _, _ [0]int) {
panic("ooh")
}`,
true,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.testTracebackArgs5",
Args{
Values: []Arg{
{Value: 0},
{IsAggregate: true, Fields: Args{
Values: []Arg{
{Value: 1},
{IsAggregate: true},
{IsAggregate: true, Fields: Args{
Values: []Arg{
{IsAggregate: true}, {IsAggregate: true},
},
}},
},
}},
{IsAggregate: true}, {IsAggregate: true},
{IsAggregate: true}, {IsAggregate: true},
{IsAggregate: true},
},
Processed: []string{"false", "<unknown>{0x1}"},
Elided: true,
},
"/root/main.go",
15),
newCallSrc("main.main", Args{}, "/root/main.go", 4),
},
},
},
{
"testTracebackArgs6a",
`func main() {
testTracebackArgs6a(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
}
func testTracebackArgs6a(a, b, c, d, e, f, g, h, i, j int) int {
panic("ooh")
}`,
true,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.testTracebackArgs6a",
Args{
Values: []Arg{
{Value: 1}, {Value: 2}, {Value: 3}, {Value: 4}, {Value: 5},
{Value: 6}, {Value: 7}, {Value: 8}, {Value: 9}, {Value: 10},
},
Processed: []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"testTracebackArgs6b",
`func main() {
testTracebackArgs6b(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
}
func testTracebackArgs6b(a, b, c, d, e, f, g, h, i, j, k int) int {
panic("ooh")
}`,
true,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.testTracebackArgs6b",
Args{
Values: []Arg{
{Value: 1}, {Value: 2}, {Value: 3}, {Value: 4}, {Value: 5},
{Value: 6}, {Value: 7}, {Value: 8}, {Value: 9}, {Value: 10},
},
Processed: []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"},
Elided: true,
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"testTracebackArgs7a",
`func main() {
testTracebackArgs7a([10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
}
func testTracebackArgs7a(a [10]int) int {
panic("ooh")
}`,
true,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.testTracebackArgs7a",
Args{
Values: []Arg{{IsAggregate: true, Fields: Args{
Values: []Arg{
{Value: 1}, {Value: 2}, {Value: 3}, {Value: 4}, {Value: 5},
{Value: 6}, {Value: 7}, {Value: 8}, {Value: 9}, {Value: 10},
},
}}},
Processed: []string{"[10]int{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa}"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"testTracebackArgs7b",
`func main() {
testTracebackArgs7b([11]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11})
}
func testTracebackArgs7b(a [11]int) int {
panic("ooh")
}`,
true,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.testTracebackArgs7b",
Args{
Values: []Arg{{IsAggregate: true, Fields: 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,
}}},
Processed: []string{"[11]int{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, ...}"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"testTracebackArgs7c",
`func main() {
testTracebackArgs7c([10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 11)
}
func testTracebackArgs7c(a [10]int, b int) int {
panic("ooh")
}`,
true,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.testTracebackArgs7c",
Args{
Values: []Arg{{IsAggregate: true, Fields: Args{
Values: []Arg{
{Value: 1}, {Value: 2}, {Value: 3}, {Value: 4}, {Value: 5},
{Value: 6}, {Value: 7}, {Value: 8}, {Value: 9}, {Value: 10},
},
}}},
Processed: []string{"[10]int{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa}"},
Elided: true,
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"testTracebackArgs7d",
`func main() {
testTracebackArgs7d([11]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, 12)
}
func testTracebackArgs7d(a [11]int, b int) int {
panic("ooh")
}`,
true,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.testTracebackArgs7d",
Args{
Values: []Arg{{IsAggregate: true, Fields: 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,
}}},
Processed: []string{"[11]int{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, ...}"},
Elided: true,
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"testTracebackArgs8a",
`func main() {
testTracebackArgs8a(testArgsType8a{1, 2, 3, 4, 5, 6, 7, 8, [2]int{9, 10}})
}
func testTracebackArgs8a(a testArgsType8a) int {
panic("ooh")
}
type testArgsType8a struct {
a, b, c, d, e, f, g, h int
i [2]int
}`,
true,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.testTracebackArgs8a",
Args{
Values: []Arg{{IsAggregate: true, Fields: Args{
Values: []Arg{
{Value: 1}, {Value: 2}, {Value: 3}, {Value: 4},
{Value: 5}, {Value: 6}, {Value: 7}, {Value: 8},
{IsAggregate: true, Fields: Args{
Values: []Arg{{Value: 9}, {Value: 10}},
}},
},
}}},
Processed: []string{"testArgsType8a{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa}"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"testTracebackArgs8b",
`func main() {
testTracebackArgs8b(testArgsType8b{1, 2, 3, 4, 5, 6, 7, 8, [3]int{9, 10, 11}})
}
func testTracebackArgs8b(a testArgsType8b) int {
panic("ooh")
}
type testArgsType8b struct {
a, b, c, d, e, f, g, h int
i [3]int
}`,
true,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.testTracebackArgs8b",
Args{
Values: []Arg{{IsAggregate: true, Fields: Args{
Values: []Arg{
{Value: 1}, {Value: 2}, {Value: 3}, {Value: 4},
{Value: 5}, {Value: 6}, {Value: 7}, {Value: 8},
{IsAggregate: true, Fields: Args{
Values: []Arg{{Value: 9}, {Value: 10}},
Elided: true,
}},
},
}}},
Processed: []string{"testArgsType8b{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa}"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"testTracebackArgs8c",
`func main() {
testTracebackArgs8c(testArgsType8c{1, 2, 3, 4, 5, 6, 7, 8, [2]int{9, 10}, 11})
}
func testTracebackArgs8c(a testArgsType8c) int {
panic("ooh")
}
type testArgsType8c struct {
a, b, c, d, e, f, g, h int
i [2]int
j int
}`,
true,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.testTracebackArgs8c",
Args{
Values: []Arg{{IsAggregate: true, Fields: Args{
Values: []Arg{
{Value: 1}, {Value: 2}, {Value: 3}, {Value: 4},
{Value: 5}, {Value: 6}, {Value: 7}, {Value: 8},
{IsAggregate: true, Fields: Args{
Values: []Arg{{Value: 9}, {Value: 10}},
}},
},
Elided: true,
}}},
Processed: []string{"testArgsType8c{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, ...}"},
},
"/root/main.go",
6),
newCallSrc("main.main", Args{}, "/root/main.go", 3),
},
},
},
{
"testTracebackArgs8d",
`func main() {
testTracebackArgs8d(testArgsType8d{1, 2, 3, 4, 5, 6, 7, 8, [3]int{9, 10, 11}, 12})
}
func testTracebackArgs8d(a testArgsType8d) int {
panic("ooh")
}
type testArgsType8d struct {
a, b, c, d, e, f, g, h int
i [3]int
j int
}`,
true,
allPlatforms,
Stack{
Calls: []Call{
newCallSrc(
"main.testTracebackArgs8d",
Args{
Values: []Arg{{IsAggregate: true, Fields: Args{
Values: []Arg{
{Value: 1}, {Value: 2}, {Value: 3}, {Value: 4},
{Value: 5}, {Value: 6}, {Value: 7}, {Value: 8},
{IsAggregate: true, Fields: Args{
Values: []Arg{{Value: 9}, {Value: 10}},
Elided: true,
}},
},
Elided: true,
}}},
Processed: []string{"testArgsType8d{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, ...}"},
},
"/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()
for _, arch := range line.archBlock {
if runtime.GOARCH == arch {
t.Skipf("skipping test on platform %s", arch)
}
}
// 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 := os.MkdirTemp("", "stack")
if err != nil {
t.Fatalf("failed to create temporary directory: %v", err)
}
defer func() {
if err2 := os.RemoveAll(root); err2 != nil {
t.Fatalf("failed to remove temporary directory %q: %v", root, err2)
}
}()
main := filepath.Join(root, "main.go")
if err := os.WriteFile(main, []byte(input), 0500); err != nil {
t.Fatalf("failed to write %q: %v", main, err)
}
if runtime.GOOS == "windows" {
root = strings.Replace(root, pathSeparator, "/", -1)
}
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 := 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)
}
if got := prefix.String(); got != "panic: ooh\n\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)
// With a non-pointer method, elided argument can be shown. 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 := os.MkdirTemp("", "stack")
if err != nil {
t.Fatalf("failed to create temporary directory: %v", err)
}
defer func() {
if err2 := os.RemoveAll(root); err2 != nil {
t.Fatalf("failed to remove temporary directory %q: %v", root, err2)
}
}()
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
errRe string
}
// Note: these tests assumes an OS running in English-US locale. That should
// eventually be fixed, maybe by using regexes?
msgRe := ".+"
if runtime.GOOS != "windows" {
msgRe = "no such file or directory"
}
data := []dataLine{
{
name: "assembly is skipped",
src: "foo.asm",
args: Args{Values: []Arg{{}}},
errRe: regexp.QuoteMeta(fmt.Sprintf("cannot load non-go file %q", filepath.Join(root, "foo.asm"))),
},
{
name: "assembly is skipped (no arg)",
src: "foo.asm",
},
{
name: "invalid line number",
src: "good.go",
line: 2,
args: Args{Values: []Arg{{}}},
errRe: "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{{}}},
errRe: regexp.QuoteMeta(fmt.Sprintf("open %s: ", filepath.Join(root, "missing.go"))) + msgRe,
},
{
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{{}}},
errRe: regexp.QuoteMeta(fmt.Sprintf("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{{}}},
errRe: regexp.QuoteMeta(fmt.Sprintf("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},
}}}},
}}
if err := s.augment(); (line.errRe == "") != (err == nil) {
t.Fatalf("want: %q; got: %q", line.errRe, err)
} else if err != nil {
if m, err2 := regexp.MatchString(line.errRe, err.Error()); err2 != nil {
t.Fatal(err2)
} else if !m {
t.Fatalf("want: %q; got: %q", line.errRe, err)
}
}
})
}
}
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"
const is64Bit = uint64(^uintptr(0)) == ^uint64(0)
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 {
// Disable both optimization (-N) and inlining (-l).
args = append(args, "-gcflags", "-N -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.Helper()
t.Fatal("expected error since this is supposed to crash")
}
return out
}
// zapPointers zaps out pointers in got.
func zapPointers(t *testing.T, want, got *Stack) {
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
}
gotArgs := got.Calls[i].Args
ptrsToReplace := zapPointersInArgs(t, want.Calls[i].Args, gotArgs)
for _, ptr := range ptrsToReplace {
for k := range gotArgs.Processed {
gotArgs.Processed[k] = strings.Replace(gotArgs.Processed[k], ptr, pointerStr, -1)
}
}
}
}
func zapPointersInArgs(t *testing.T, want, got Args) (ptrs []string) {
for j := range got.Values {
if j >= len(want.Values) {
break
}
if want.Values[j].IsAggregate && got.Values[j].IsAggregate {
ptrs = append(ptrs, zapPointersInArgs(t, want.Values[j].Fields, got.Values[j].Fields)...)
} else if want.Values[j].IsPtr && got.Values[j].IsPtr {
// Record the existing pointer value and then replace.
ptrs = append(ptrs, fmt.Sprintf("0x%x", got.Values[j].Value))
got.Values[j].Value = want.Values[j].Value
}
}
return ptrs
}