| // stackdump_test checks that the heuristics the stackdump package applies to |
| // prune frames work as expected in production Go compilers. |
| |
| package stackdump_test |
| |
| import ( |
| "bytes" |
| "fmt" |
| "regexp" |
| "runtime" |
| "testing" |
| |
| "github.com/golang/glog/internal/stackdump" |
| ) |
| |
| var file string |
| |
| func init() { |
| _, file, _, _ = runtime.Caller(0) |
| } |
| |
| func TestCallerText(t *testing.T) { |
| stack := stackdump.CallerText(0) |
| _, _, line, _ := runtime.Caller(0) |
| line-- |
| |
| wantRE := regexp.MustCompile(fmt.Sprintf( |
| `^goroutine \d+ \[running\]: |
| github.com/golang/glog/internal/stackdump_test\.TestCallerText(\([^)]*\))? |
| %v:%v.* |
| `, file, line)) |
| if !wantRE.Match(stack) { |
| t.Errorf("Stack dump:\n%s\nwant matching regexp:\n%s", stack, wantRE.String()) |
| |
| buf := make([]byte, len(stack)*2) |
| origStack := buf[:runtime.Stack(buf, false)] |
| t.Logf("Unpruned stack:\n%s", origStack) |
| } |
| } |
| |
| func callerAt(calls int, depth int) (stack []byte) { |
| if calls == 1 { |
| return stackdump.CallerText(depth) |
| } |
| return callerAt(calls-1, depth) |
| } |
| |
| func TestCallerTextSkip(t *testing.T) { |
| const calls = 3 |
| cases := []struct { |
| depth int |
| callerAtFrames int |
| wantEndOfStack bool |
| }{ |
| {depth: 0, callerAtFrames: calls}, |
| {depth: calls - 1, callerAtFrames: 1}, |
| {depth: calls, callerAtFrames: 0}, |
| {depth: calls + 1, callerAtFrames: 0}, |
| {depth: calls + 100, wantEndOfStack: true}, |
| } |
| |
| for _, tc := range cases { |
| stack := callerAt(calls, tc.depth) |
| |
| wantREBuf := bytes.NewBuffer(nil) |
| fmt.Fprintf(wantREBuf, `^goroutine \d+ \[running\]: |
| `) |
| if tc.wantEndOfStack { |
| fmt.Fprintf(wantREBuf, "\n|$") |
| } else { |
| for n := tc.callerAtFrames; n > 0; n-- { |
| fmt.Fprintf(wantREBuf, `github.com/golang/glog/internal/stackdump_test\.callerAt(\([^)]*\))? |
| %v:\d+.* |
| `, file) |
| } |
| |
| if tc.depth <= calls { |
| fmt.Fprintf(wantREBuf, `github.com/golang/glog/internal/stackdump_test\.TestCallerTextSkip(\([^)]*\))? |
| %v:\d+.* |
| `, file) |
| } |
| } |
| |
| wantRE := regexp.MustCompile(wantREBuf.String()) |
| |
| if !wantRE.Match(stack) { |
| t.Errorf("for %v calls, stackdump.CallerText(%v) =\n%s\n\nwant matching regexp:\n%s", calls, tc.depth, stack, wantRE.String()) |
| } |
| } |
| } |
| |
| func pcAt(calls int, depth int) (stack []uintptr) { |
| if calls == 1 { |
| return stackdump.CallerPC(depth) |
| } |
| stack = pcAt(calls-1, depth) |
| runtime.Gosched() // Thwart tail-call optimization. |
| return stack |
| } |
| |
| func TestCallerPC(t *testing.T) { |
| const calls = 3 |
| cases := []struct { |
| depth int |
| pcAtFrames int |
| wantEndOfStack bool |
| }{ |
| {depth: 0, pcAtFrames: calls}, |
| {depth: calls - 1, pcAtFrames: 1}, |
| {depth: calls, pcAtFrames: 0}, |
| {depth: calls + 1, pcAtFrames: 0}, |
| {depth: calls + 100, wantEndOfStack: true}, |
| } |
| |
| for _, tc := range cases { |
| stack := pcAt(calls, tc.depth) |
| if tc.wantEndOfStack { |
| if len(stack) != 0 { |
| t.Errorf("for %v calls, stackdump.CallerPC(%v) =\n%q\nwant []", calls, tc.depth, stack) |
| } |
| continue |
| } |
| |
| wantFuncs := []string{} |
| for n := tc.pcAtFrames; n > 0; n-- { |
| wantFuncs = append(wantFuncs, `github.com/golang/glog/internal/stackdump_test\.pcAt$`) |
| } |
| if tc.depth <= calls { |
| wantFuncs = append(wantFuncs, `^github.com/golang/glog/internal/stackdump_test\.TestCallerPC$`) |
| } |
| |
| gotFuncs := []string{} |
| for _, pc := range stack { |
| gotFuncs = append(gotFuncs, runtime.FuncForPC(pc).Name()) |
| } |
| if len(gotFuncs) > len(wantFuncs) { |
| gotFuncs = gotFuncs[:len(wantFuncs)] |
| } |
| |
| ok := true |
| for i, want := range wantFuncs { |
| re := regexp.MustCompile(want) |
| if i >= len(gotFuncs) || !re.MatchString(gotFuncs[i]) { |
| ok = false |
| break |
| } |
| } |
| if !ok { |
| t.Errorf("for %v calls, stackdump.CallerPC(%v) =\n%q\nwant %q", calls, tc.depth, gotFuncs, wantFuncs) |
| } |
| } |
| } |