| // 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. | |
| // Tests source augmentation, argument decoding, and native panic fixtures. | |
| package stack | |
| import ( | |
| "bytes" | |
| "fmt" | |
| "go/ast" | |
| "go/parser" | |
| "go/token" | |
| "math/bits" | |
| "os" | |
| "os/exec" | |
| "path" | |
| "path/filepath" | |
| "regexp" | |
| "runtime" | |
| "strconv" | |
| "strings" | |
| "testing" | |
| "github.com/google/go-cmp/cmp" | |
| ) | |
| // 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", | |
| }, | |
| } | |
| 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" | |
| 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 | |
| } | |
| // TestAugmentCall covers signed stack words and unused high bits of narrow values. | |
| func TestAugmentCall(t *testing.T) { | |
| wantInt := "1311768467015204864" | |
| if strconv.IntSize == 32 { | |
| wantInt = "-2147483648" | |
| } | |
| data := []struct { | |
| typ string | |
| word uint64 | |
| want string | |
| }{ | |
| {"int", 0x1234567880000000, wantInt}, | |
| {"int8", 0xffffffffffff007f, "127"}, | |
| {"int8", 0xffffffffffff0080, "-128"}, | |
| {"int8", 0xffffffffffffffff, "-1"}, | |
| {"int16", 0xffffffff00007fff, "32767"}, | |
| {"int16", 0xffffffff00008000, "-32768"}, | |
| {"int32", 0x123456787fffffff, "2147483647"}, | |
| {"int32", 0x1234567880000000, "-2147483648"}, | |
| {"int64", 0x7fffffffffffffff, "9223372036854775807"}, | |
| {"int64", 0x8000000000000000, "-9223372036854775808"}, | |
| {"int64", 0xffffffffffffffff, "-1"}, | |
| {"float32", 0x123456783fc00000, "1.5"}, | |
| {"float32", 0x1234567880000000, "-0"}, | |
| } | |
| for i, line := range data { | |
| t.Run(fmt.Sprintf("%s/%d", line.typ, i), func(t *testing.T) { | |
| f, err := parser.ParseFile(token.NewFileSet(), "args.go", "package main; func f(v "+line.typ+") {}", 0) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| call := Call{Args: Args{Values: []Arg{{Value: line.word}}}} | |
| augmentCall(&call, f.Decls[0].(*ast.FuncDecl)) | |
| if diff := cmp.Diff([]string{line.want}, call.Args.Processed); diff != "" { | |
| t.Fatalf("decoded argument (-want +got):\n%s", diff) | |
| } | |
| }) | |
| } | |
| } | |
| func TestAugmentGenericMethod(t *testing.T) { | |
| t.Parallel() | |
| main := filepath.Join(t.TempDir(), "main.go") | |
| const src = `package main | |
| type sample struct{} | |
| func (s *sample) Fail[T any]() { | |
| panic("ooh") | |
| } | |
| func main() { | |
| new(sample).Fail[int]() | |
| } | |
| ` | |
| if err := os.WriteFile(main, []byte(src), 0o600); err != nil { | |
| t.Fatal(err) | |
| } | |
| content := getCrash(t, main, true) | |
| s, _, err := ScanSnapshot(bytes.NewReader(content), &bytes.Buffer{}, defaultOpts()) | |
| if err != nil || s == nil { | |
| t.Fatalf("parse generic method panic: %v\n%s", err, content) | |
| } | |
| if !s.guessPaths() { | |
| t.Fatal("could not resolve generic method source") | |
| } | |
| if err := s.augment(); err != nil { | |
| t.Fatalf("analyze generic method source: %v", err) | |
| } | |
| call := s.Goroutines[0].Stack.Calls[0] | |
| if !strings.HasPrefix(call.Func.Name, "(*sample).Fail[") { | |
| t.Fatalf("generic method frame lost: %q", call.Func.Name) | |
| } | |
| if call.LocalSrcPath != filepath.ToSlash(main) || len(call.Args.Processed) == 0 { | |
| t.Fatalf("generic method source was not analyzed: %+v", call) | |
| } | |
| } |