├── .github └── workflows │ └── test.yml ├── LICENSE.md ├── README.md ├── format_test.go ├── go.mod ├── stack-go19_test.go ├── stack.go └── stack_test.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | types: [synchronize] 5 | schedule: 6 | - cron: "0 0 1,11,21 * *" 7 | name: Test 8 | jobs: 9 | test: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | go-version: 14 | - 1.7.x 15 | - 1.8.x 16 | - 1.9.x 17 | - 1.10.x 18 | - 1.11.x 19 | - 1.12.x 20 | - 1.13.x 21 | - 1.14.x 22 | - 1.15.x 23 | - 1.16.x 24 | - 1.17.x 25 | go-flags: [""] 26 | os: [ubuntu-latest, macos-latest, windows-latest] 27 | include: 28 | - go-version: 1.13.x 29 | os: ubuntu-latest 30 | go-flags: "-trimpath" 31 | - go-version: 1.13.x 32 | os: macos-latest 33 | go-flags: "-trimpath" 34 | - go-version: 1.13.x 35 | os: windows-latest 36 | go-flags: "-trimpath" 37 | - go-version: 1.14.x 38 | os: ubuntu-latest 39 | go-flags: "-trimpath" 40 | - go-version: 1.14.x 41 | os: macos-latest 42 | go-flags: "-trimpath" 43 | - go-version: 1.14.x 44 | os: windows-latest 45 | go-flags: "-trimpath" 46 | - go-version: 1.15.x 47 | os: ubuntu-latest 48 | go-flags: "-trimpath" 49 | - go-version: 1.15.x 50 | os: macos-latest 51 | go-flags: "-trimpath" 52 | - go-version: 1.15.x 53 | os: windows-latest 54 | go-flags: "-trimpath" 55 | - go-version: 1.16.x 56 | os: ubuntu-latest 57 | go-flags: "-trimpath" 58 | - go-version: 1.16.x 59 | os: macos-latest 60 | go-flags: "-trimpath" 61 | - go-version: 1.16.x 62 | os: windows-latest 63 | go-flags: "-trimpath" 64 | - go-version: 1.17.x 65 | os: ubuntu-latest 66 | go-flags: "-trimpath" 67 | - go-version: 1.17.x 68 | os: macos-latest 69 | go-flags: "-trimpath" 70 | - go-version: 1.17.x 71 | os: windows-latest 72 | go-flags: "-trimpath" 73 | runs-on: ${{ matrix.os }} 74 | steps: 75 | - name: Install Go 76 | uses: actions/setup-go@v2 77 | with: 78 | go-version: ${{ matrix.go-version }} 79 | - name: Checkout code 80 | uses: actions/checkout@v2 81 | with: 82 | path: "${{github.workspace}}/src/github.com/${{github.repository}}" 83 | - name: Test 84 | env: 85 | GOPATH: "${{github.workspace}}" 86 | GOFLAGS: ${{ matrix.go-flags }} 87 | working-directory: "${{github.workspace}}/src/github.com/${{github.repository}}" 88 | run: go test ./... 89 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Chris Hines 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://godoc.org/github.com/go-stack/stack?status.svg)](https://godoc.org/github.com/go-stack/stack) 2 | [![Go Report Card](https://goreportcard.com/badge/go-stack/stack)](https://goreportcard.com/report/go-stack/stack) 3 | [![TravisCI](https://travis-ci.org/go-stack/stack.svg?branch=master)](https://travis-ci.org/go-stack/stack) 4 | [![Coverage Status](https://coveralls.io/repos/github/go-stack/stack/badge.svg?branch=master)](https://coveralls.io/github/go-stack/stack?branch=master) 5 | 6 | # stack 7 | 8 | Package stack implements utilities to capture, manipulate, and format call 9 | stacks. It provides a simpler API than package runtime. 10 | 11 | The implementation takes care of the minutia and special cases of interpreting 12 | the program counter (pc) values returned by runtime.Callers. 13 | 14 | ## Versioning 15 | 16 | Package stack publishes releases via [semver](http://semver.org/) compatible Git 17 | tags prefixed with a single 'v'. The master branch always contains the latest 18 | release. The develop branch contains unreleased commits. 19 | 20 | ## Formatting 21 | 22 | Package stack's types implement fmt.Formatter, which provides a simple and 23 | flexible way to declaratively configure formatting when used with logging or 24 | error tracking packages. 25 | 26 | ```go 27 | func DoTheThing() { 28 | c := stack.Caller(0) 29 | log.Print(c) // "source.go:10" 30 | log.Printf("%+v", c) // "pkg/path/source.go:10" 31 | log.Printf("%n", c) // "DoTheThing" 32 | 33 | s := stack.Trace().TrimRuntime() 34 | log.Print(s) // "[source.go:15 caller.go:42 main.go:14]" 35 | } 36 | ``` 37 | 38 | See the docs for all of the supported formatting options. 39 | -------------------------------------------------------------------------------- /format_test.go: -------------------------------------------------------------------------------- 1 | // +build go1.2 2 | 3 | package stack_test 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/go-stack/stack" 9 | ) 10 | 11 | func Example_callFormat() { 12 | logCaller("%+s") 13 | logCaller("%v %[1]n()") 14 | // Output: 15 | // github.com/go-stack/stack/format_test.go 16 | // format_test.go:13 Example_callFormat() 17 | } 18 | 19 | func logCaller(format string) { 20 | fmt.Printf(format+"\n", stack.Caller(1)) 21 | } 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-stack/stack 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /stack-go19_test.go: -------------------------------------------------------------------------------- 1 | // +build go1.9 2 | 3 | package stack_test 4 | 5 | import ( 6 | "runtime" 7 | "testing" 8 | 9 | "github.com/go-stack/stack" 10 | ) 11 | 12 | func TestCallerInlinedPanic(t *testing.T) { 13 | t.Parallel() 14 | 15 | var line int 16 | 17 | defer func() { 18 | if recover() != nil { 19 | var pcs [32]uintptr 20 | n := runtime.Callers(1, pcs[:]) 21 | frames := runtime.CallersFrames(pcs[:n]) 22 | // count frames to runtime.sigpanic 23 | panicIdx := 0 24 | for { 25 | f, more := frames.Next() 26 | if f.Function == "runtime.sigpanic" { 27 | break 28 | } 29 | panicIdx++ 30 | if !more { 31 | t.Fatal("no runtime.sigpanic entry on the stack") 32 | } 33 | } 34 | 35 | c := stack.Caller(panicIdx) 36 | if got, want := c.Frame().Function, "runtime.sigpanic"; got != want { 37 | t.Errorf("sigpanic frame: got name == %v, want name == %v", got, want) 38 | } 39 | 40 | c1 := stack.Caller(panicIdx + 1) 41 | if got, want := c1.Frame().Function, "github.com/go-stack/stack_test.inlinablePanic"; got != want { 42 | t.Errorf("TestCallerInlinedPanic frame: got name == %v, want name == %v", got, want) 43 | } 44 | if got, want := c1.Frame().Line, line; got != want { 45 | t.Errorf("TestCallerInlinedPanic frame: got line == %v, want line == %v", got, want) 46 | } 47 | } 48 | }() 49 | 50 | doPanic(t, &line) 51 | t.Fatal("failed to panic") 52 | } 53 | 54 | func doPanic(t *testing.T, panicLine *int) { 55 | _, _, line, ok := runtime.Caller(0) 56 | *panicLine = line + 11 // adjust to match line of panic below 57 | if !ok { 58 | t.Fatal("runtime.Caller(0) failed") 59 | } 60 | inlinablePanic() 61 | } 62 | 63 | func inlinablePanic() { 64 | // Initiate a sigpanic. 65 | var x *uintptr 66 | _ = *x 67 | } 68 | -------------------------------------------------------------------------------- /stack.go: -------------------------------------------------------------------------------- 1 | // +build go1.7 2 | 3 | // Package stack implements utilities to capture, manipulate, and format call 4 | // stacks. It provides a simpler API than package runtime. 5 | // 6 | // The implementation takes care of the minutia and special cases of 7 | // interpreting the program counter (pc) values returned by runtime.Callers. 8 | // 9 | // Package stack's types implement fmt.Formatter, which provides a simple and 10 | // flexible way to declaratively configure formatting when used with logging 11 | // or error tracking packages. 12 | package stack 13 | 14 | import ( 15 | "bytes" 16 | "errors" 17 | "fmt" 18 | "io" 19 | "runtime" 20 | "strconv" 21 | "strings" 22 | ) 23 | 24 | // Call records a single function invocation from a goroutine stack. 25 | type Call struct { 26 | frame runtime.Frame 27 | } 28 | 29 | // Caller returns a Call from the stack of the current goroutine. The argument 30 | // skip is the number of stack frames to ascend, with 0 identifying the 31 | // calling function. 32 | func Caller(skip int) Call { 33 | // As of Go 1.9 we need room for up to three PC entries. 34 | // 35 | // 0. An entry for the stack frame prior to the target to check for 36 | // special handling needed if that prior entry is runtime.sigpanic. 37 | // 1. A possible second entry to hold metadata about skipped inlined 38 | // functions. If inline functions were not skipped the target frame 39 | // PC will be here. 40 | // 2. A third entry for the target frame PC when the second entry 41 | // is used for skipped inline functions. 42 | var pcs [3]uintptr 43 | n := runtime.Callers(skip+1, pcs[:]) 44 | frames := runtime.CallersFrames(pcs[:n]) 45 | frame, _ := frames.Next() 46 | frame, _ = frames.Next() 47 | 48 | return Call{ 49 | frame: frame, 50 | } 51 | } 52 | 53 | // String implements fmt.Stinger. It is equivalent to fmt.Sprintf("%v", c). 54 | func (c Call) String() string { 55 | return fmt.Sprint(c) 56 | } 57 | 58 | // MarshalText implements encoding.TextMarshaler. It formats the Call the same 59 | // as fmt.Sprintf("%v", c). 60 | func (c Call) MarshalText() ([]byte, error) { 61 | if c.frame == (runtime.Frame{}) { 62 | return nil, ErrNoFunc 63 | } 64 | 65 | buf := bytes.Buffer{} 66 | fmt.Fprint(&buf, c) 67 | return buf.Bytes(), nil 68 | } 69 | 70 | // ErrNoFunc means that the Call has a nil *runtime.Func. The most likely 71 | // cause is a Call with the zero value. 72 | var ErrNoFunc = errors.New("no call stack information") 73 | 74 | // Format implements fmt.Formatter with support for the following verbs. 75 | // 76 | // %s source file 77 | // %d line number 78 | // %n function name 79 | // %k last segment of the package path 80 | // %v equivalent to %s:%d 81 | // 82 | // It accepts the '+' and '#' flags for most of the verbs as follows. 83 | // 84 | // %+s path of source file relative to the compile time GOPATH, 85 | // or the module path joined to the path of source file relative 86 | // to module root 87 | // %#s full path of source file 88 | // %+n import path qualified function name 89 | // %+k full package path 90 | // %+v equivalent to %+s:%d 91 | // %#v equivalent to %#s:%d 92 | func (c Call) Format(s fmt.State, verb rune) { 93 | if c.frame == (runtime.Frame{}) { 94 | fmt.Fprintf(s, "%%!%c(NOFUNC)", verb) 95 | return 96 | } 97 | 98 | switch verb { 99 | case 's', 'v': 100 | file := c.frame.File 101 | switch { 102 | case s.Flag('#'): 103 | // done 104 | case s.Flag('+'): 105 | file = pkgFilePath(&c.frame) 106 | default: 107 | const sep = "/" 108 | if i := strings.LastIndex(file, sep); i != -1 { 109 | file = file[i+len(sep):] 110 | } 111 | } 112 | io.WriteString(s, file) 113 | if verb == 'v' { 114 | buf := [7]byte{':'} 115 | s.Write(strconv.AppendInt(buf[:1], int64(c.frame.Line), 10)) 116 | } 117 | 118 | case 'd': 119 | buf := [6]byte{} 120 | s.Write(strconv.AppendInt(buf[:0], int64(c.frame.Line), 10)) 121 | 122 | case 'k': 123 | name := c.frame.Function 124 | const pathSep = "/" 125 | start, end := 0, len(name) 126 | if i := strings.LastIndex(name, pathSep); i != -1 { 127 | start = i + len(pathSep) 128 | } 129 | const pkgSep = "." 130 | if i := strings.Index(name[start:], pkgSep); i != -1 { 131 | end = start + i 132 | } 133 | if s.Flag('+') { 134 | start = 0 135 | } 136 | io.WriteString(s, name[start:end]) 137 | 138 | case 'n': 139 | name := c.frame.Function 140 | if !s.Flag('+') { 141 | const pathSep = "/" 142 | if i := strings.LastIndex(name, pathSep); i != -1 { 143 | name = name[i+len(pathSep):] 144 | } 145 | const pkgSep = "." 146 | if i := strings.Index(name, pkgSep); i != -1 { 147 | name = name[i+len(pkgSep):] 148 | } 149 | } 150 | io.WriteString(s, name) 151 | } 152 | } 153 | 154 | // Frame returns the call frame infomation for the Call. 155 | func (c Call) Frame() runtime.Frame { 156 | return c.frame 157 | } 158 | 159 | // PC returns the program counter for this call frame; multiple frames may 160 | // have the same PC value. 161 | // 162 | // Deprecated: Use Call.Frame instead. 163 | func (c Call) PC() uintptr { 164 | return c.frame.PC 165 | } 166 | 167 | // CallStack records a sequence of function invocations from a goroutine 168 | // stack. 169 | type CallStack []Call 170 | 171 | // String implements fmt.Stinger. It is equivalent to fmt.Sprintf("%v", cs). 172 | func (cs CallStack) String() string { 173 | return fmt.Sprint(cs) 174 | } 175 | 176 | var ( 177 | openBracketBytes = []byte("[") 178 | closeBracketBytes = []byte("]") 179 | spaceBytes = []byte(" ") 180 | ) 181 | 182 | // MarshalText implements encoding.TextMarshaler. It formats the CallStack the 183 | // same as fmt.Sprintf("%v", cs). 184 | func (cs CallStack) MarshalText() ([]byte, error) { 185 | buf := bytes.Buffer{} 186 | buf.Write(openBracketBytes) 187 | for i, pc := range cs { 188 | if i > 0 { 189 | buf.Write(spaceBytes) 190 | } 191 | fmt.Fprint(&buf, pc) 192 | } 193 | buf.Write(closeBracketBytes) 194 | return buf.Bytes(), nil 195 | } 196 | 197 | // Format implements fmt.Formatter by printing the CallStack as square brackets 198 | // ([, ]) surrounding a space separated list of Calls each formatted with the 199 | // supplied verb and options. 200 | func (cs CallStack) Format(s fmt.State, verb rune) { 201 | s.Write(openBracketBytes) 202 | for i, pc := range cs { 203 | if i > 0 { 204 | s.Write(spaceBytes) 205 | } 206 | pc.Format(s, verb) 207 | } 208 | s.Write(closeBracketBytes) 209 | } 210 | 211 | // Trace returns a CallStack for the current goroutine with element 0 212 | // identifying the calling function. 213 | func Trace() CallStack { 214 | var pcs [512]uintptr 215 | n := runtime.Callers(1, pcs[:]) 216 | 217 | frames := runtime.CallersFrames(pcs[:n]) 218 | cs := make(CallStack, 0, n) 219 | 220 | // Skip extra frame retrieved just to make sure the runtime.sigpanic 221 | // special case is handled. 222 | frame, more := frames.Next() 223 | 224 | for more { 225 | frame, more = frames.Next() 226 | cs = append(cs, Call{frame: frame}) 227 | } 228 | 229 | return cs 230 | } 231 | 232 | // TrimBelow returns a slice of the CallStack with all entries below c 233 | // removed. 234 | func (cs CallStack) TrimBelow(c Call) CallStack { 235 | for len(cs) > 0 && cs[0] != c { 236 | cs = cs[1:] 237 | } 238 | return cs 239 | } 240 | 241 | // TrimAbove returns a slice of the CallStack with all entries above c 242 | // removed. 243 | func (cs CallStack) TrimAbove(c Call) CallStack { 244 | for len(cs) > 0 && cs[len(cs)-1] != c { 245 | cs = cs[:len(cs)-1] 246 | } 247 | return cs 248 | } 249 | 250 | // pkgIndex returns the index that results in file[index:] being the path of 251 | // file relative to the compile time GOPATH, and file[:index] being the 252 | // $GOPATH/src/ portion of file. funcName must be the name of a function in 253 | // file as returned by runtime.Func.Name. 254 | func pkgIndex(file, funcName string) int { 255 | // As of Go 1.6.2 there is no direct way to know the compile time GOPATH 256 | // at runtime, but we can infer the number of path segments in the GOPATH. 257 | // We note that runtime.Func.Name() returns the function name qualified by 258 | // the import path, which does not include the GOPATH. Thus we can trim 259 | // segments from the beginning of the file path until the number of path 260 | // separators remaining is one more than the number of path separators in 261 | // the function name. For example, given: 262 | // 263 | // GOPATH /home/user 264 | // file /home/user/src/pkg/sub/file.go 265 | // fn.Name() pkg/sub.Type.Method 266 | // 267 | // We want to produce: 268 | // 269 | // file[:idx] == /home/user/src/ 270 | // file[idx:] == pkg/sub/file.go 271 | // 272 | // From this we can easily see that fn.Name() has one less path separator 273 | // than our desired result for file[idx:]. We count separators from the 274 | // end of the file path until it finds two more than in the function name 275 | // and then move one character forward to preserve the initial path 276 | // segment without a leading separator. 277 | const sep = "/" 278 | i := len(file) 279 | for n := strings.Count(funcName, sep) + 2; n > 0; n-- { 280 | i = strings.LastIndex(file[:i], sep) 281 | if i == -1 { 282 | i = -len(sep) 283 | break 284 | } 285 | } 286 | // get back to 0 or trim the leading separator 287 | return i + len(sep) 288 | } 289 | 290 | // pkgFilePath returns the frame's filepath relative to the compile-time GOPATH, 291 | // or its module path joined to its path relative to the module root. 292 | // 293 | // As of Go 1.11 there is no direct way to know the compile time GOPATH or 294 | // module paths at runtime, but we can piece together the desired information 295 | // from available information. We note that runtime.Frame.Function contains the 296 | // function name qualified by the package path, which includes the module path 297 | // but not the GOPATH. We can extract the package path from that and append the 298 | // last segments of the file path to arrive at the desired package qualified 299 | // file path. For example, given: 300 | // 301 | // GOPATH /home/user 302 | // import path pkg/sub 303 | // frame.File /home/user/src/pkg/sub/file.go 304 | // frame.Function pkg/sub.Type.Method 305 | // Desired return pkg/sub/file.go 306 | // 307 | // It appears that we simply need to trim ".Type.Method" from frame.Function and 308 | // append "/" + path.Base(file). 309 | // 310 | // But there are other wrinkles. Although it is idiomatic to do so, the internal 311 | // name of a package is not required to match the last segment of its import 312 | // path. In addition, the introduction of modules in Go 1.11 allows working 313 | // without a GOPATH. So we also must make these work right: 314 | // 315 | // GOPATH /home/user 316 | // import path pkg/go-sub 317 | // package name sub 318 | // frame.File /home/user/src/pkg/go-sub/file.go 319 | // frame.Function pkg/sub.Type.Method 320 | // Desired return pkg/go-sub/file.go 321 | // 322 | // Module path pkg/v2 323 | // import path pkg/v2/go-sub 324 | // package name sub 325 | // frame.File /home/user/cloned-pkg/go-sub/file.go 326 | // frame.Function pkg/v2/sub.Type.Method 327 | // Desired return pkg/v2/go-sub/file.go 328 | // 329 | // We can handle all of these situations by using the package path extracted 330 | // from frame.Function up to, but not including, the last segment as the prefix 331 | // and the last two segments of frame.File as the suffix of the returned path. 332 | // This preserves the existing behavior when working in a GOPATH without modules 333 | // and a semantically equivalent behavior when used in module aware project. 334 | func pkgFilePath(frame *runtime.Frame) string { 335 | pre := pkgPrefix(frame.Function) 336 | post := pathSuffix(frame.File) 337 | if pre == "" { 338 | return post 339 | } 340 | return pre + "/" + post 341 | } 342 | 343 | // pkgPrefix returns the import path of the function's package with the final 344 | // segment removed. 345 | func pkgPrefix(funcName string) string { 346 | const pathSep = "/" 347 | end := strings.LastIndex(funcName, pathSep) 348 | if end == -1 { 349 | return "" 350 | } 351 | return funcName[:end] 352 | } 353 | 354 | // pathSuffix returns the last two segments of path. 355 | func pathSuffix(path string) string { 356 | const pathSep = "/" 357 | lastSep := strings.LastIndex(path, pathSep) 358 | if lastSep == -1 { 359 | return path 360 | } 361 | return path[strings.LastIndex(path[:lastSep], pathSep)+1:] 362 | } 363 | 364 | var runtimePath string 365 | 366 | func init() { 367 | var pcs [3]uintptr 368 | runtime.Callers(0, pcs[:]) 369 | frames := runtime.CallersFrames(pcs[:]) 370 | frame, _ := frames.Next() 371 | file := frame.File 372 | 373 | idx := pkgIndex(frame.File, frame.Function) 374 | 375 | runtimePath = file[:idx] 376 | if runtime.GOOS == "windows" { 377 | runtimePath = strings.ToLower(runtimePath) 378 | } 379 | } 380 | 381 | func inGoroot(c Call) bool { 382 | file := c.frame.File 383 | if len(file) == 0 || file[0] == '?' { 384 | return true 385 | } 386 | if runtime.GOOS == "windows" { 387 | file = strings.ToLower(file) 388 | } 389 | return strings.HasPrefix(file, runtimePath) || strings.HasSuffix(file, "/_testmain.go") 390 | } 391 | 392 | // TrimRuntime returns a slice of the CallStack with the topmost entries from 393 | // the go runtime removed. It considers any calls originating from unknown 394 | // files, files under GOROOT, or _testmain.go as part of the runtime. 395 | func (cs CallStack) TrimRuntime() CallStack { 396 | for len(cs) > 0 && inGoroot(cs[len(cs)-1]) { 397 | cs = cs[:len(cs)-1] 398 | } 399 | return cs 400 | } 401 | -------------------------------------------------------------------------------- /stack_test.go: -------------------------------------------------------------------------------- 1 | package stack_test 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "path" 7 | "path/filepath" 8 | "reflect" 9 | "runtime" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/go-stack/stack" 14 | ) 15 | 16 | func TestCaller(t *testing.T) { 17 | t.Parallel() 18 | 19 | c := stack.Caller(0) 20 | _, file, line, ok := runtime.Caller(0) 21 | line-- 22 | if !ok { 23 | t.Fatal("runtime.Caller(0) failed") 24 | } 25 | 26 | if got, want := c.Frame().File, file; got != want { 27 | t.Errorf("got file == %v, want file == %v", got, want) 28 | } 29 | 30 | if got, want := c.Frame().Line, line; got != want { 31 | t.Errorf("got line == %v, want line == %v", got, want) 32 | } 33 | } 34 | 35 | func f3(f1 func() stack.Call) stack.Call { 36 | return f2(f1) 37 | } 38 | 39 | func f2(f1 func() stack.Call) stack.Call { 40 | return f1() 41 | } 42 | 43 | func TestCallerMidstackInlined(t *testing.T) { 44 | t.Parallel() 45 | 46 | _, _, line, ok := runtime.Caller(0) 47 | line -= 10 // adjust to return f1() line inside f2() 48 | if !ok { 49 | t.Fatal("runtime.Caller(0) failed") 50 | } 51 | 52 | c := f3(func() stack.Call { 53 | return stack.Caller(2) 54 | }) 55 | 56 | if got, want := c.Frame().Line, line; got != want { 57 | t.Errorf("got line == %v, want line == %v", got, want) 58 | } 59 | if got, want := c.Frame().Function, "github.com/go-stack/stack_test.f3"; got != want { 60 | t.Errorf("got func name == %v, want func name == %v", got, want) 61 | } 62 | } 63 | 64 | func TestCallerPanic(t *testing.T) { 65 | t.Parallel() 66 | 67 | var ( 68 | line int 69 | ok bool 70 | ) 71 | 72 | defer func() { 73 | if recover() != nil { 74 | var pcs [32]uintptr 75 | n := runtime.Callers(1, pcs[:]) 76 | frames := runtime.CallersFrames(pcs[:n]) 77 | // count frames to runtime.sigpanic 78 | panicIdx := 0 79 | for { 80 | f, more := frames.Next() 81 | if f.Function == "runtime.sigpanic" { 82 | break 83 | } 84 | panicIdx++ 85 | if !more { 86 | t.Fatal("no runtime.sigpanic entry on the stack") 87 | } 88 | } 89 | c := stack.Caller(panicIdx) 90 | if got, want := c.Frame().Function, "runtime.sigpanic"; got != want { 91 | t.Errorf("sigpanic frame: got name == %v, want name == %v", got, want) 92 | } 93 | c1 := stack.Caller(panicIdx + 1) 94 | if got, want := c1.Frame().Function, "github.com/go-stack/stack_test.TestCallerPanic"; got != want { 95 | t.Errorf("TestCallerPanic frame: got name == %v, want name == %v", got, want) 96 | } 97 | if got, want := c1.Frame().Line, line; got != want { 98 | t.Errorf("TestCallerPanic frame: got line == %v, want line == %v", got, want) 99 | } 100 | } 101 | }() 102 | 103 | _, _, line, ok = runtime.Caller(0) 104 | line += 7 // adjust to match line of panic below 105 | if !ok { 106 | t.Fatal("runtime.Caller(0) failed") 107 | } 108 | // Initiate a sigpanic. 109 | var x *uintptr 110 | _ = *x 111 | } 112 | 113 | type tholder struct { 114 | trace func() stack.CallStack 115 | } 116 | 117 | func (th *tholder) traceLabyrinth() stack.CallStack { 118 | for { 119 | return th.trace() 120 | } 121 | } 122 | 123 | func TestTrace(t *testing.T) { 124 | t.Parallel() 125 | 126 | _, _, line, ok := runtime.Caller(0) 127 | if !ok { 128 | t.Fatal("runtime.Caller(0) failed") 129 | } 130 | 131 | fh := tholder{ 132 | trace: func() stack.CallStack { 133 | cs := stack.Trace() 134 | return cs 135 | }, 136 | } 137 | 138 | cs := fh.traceLabyrinth() 139 | 140 | lines := []int{line + 7, line - 7, line + 12} 141 | 142 | for i, line := range lines { 143 | if got, want := cs[i].Frame().Line, line; got != want { 144 | t.Errorf("got line[%d] == %v, want line[%d] == %v", i, got, i, want) 145 | } 146 | } 147 | } 148 | 149 | // Test stack handling originating from a sigpanic. 150 | func TestTracePanic(t *testing.T) { 151 | t.Parallel() 152 | 153 | var ( 154 | line int 155 | ok bool 156 | ) 157 | 158 | defer func() { 159 | if recover() != nil { 160 | trace := stack.Trace() 161 | 162 | // find runtime.sigpanic 163 | panicIdx := -1 164 | for i, c := range trace { 165 | if c.Frame().Function == "runtime.sigpanic" { 166 | panicIdx = i 167 | break 168 | } 169 | } 170 | if panicIdx == -1 { 171 | t.Fatal("no runtime.sigpanic entry on the stack") 172 | } 173 | if got, want := trace[panicIdx].Frame().Function, "runtime.sigpanic"; got != want { 174 | t.Errorf("sigpanic frame: got name == %v, want name == %v", got, want) 175 | } 176 | if got, want := trace[panicIdx+1].Frame().Function, "github.com/go-stack/stack_test.TestTracePanic"; got != want { 177 | t.Errorf("TestTracePanic frame: got name == %v, want name == %v", got, want) 178 | } 179 | if got, want := trace[panicIdx+1].Frame().Line, line; got != want { 180 | t.Errorf("TestTracePanic frame: got line == %v, want line == %v", got, want) 181 | } 182 | } 183 | }() 184 | 185 | _, _, line, ok = runtime.Caller(0) 186 | line += 7 // adjust to match line of panic below 187 | if !ok { 188 | t.Fatal("runtime.Caller(0) failed") 189 | } 190 | // Initiate a sigpanic. 191 | var x *uintptr 192 | _ = *x 193 | } 194 | 195 | const importPath = "github.com/go-stack/stack" 196 | 197 | type testType struct{} 198 | 199 | func (tt testType) testMethod() (c stack.Call, file string, line int, ok bool) { 200 | c = stack.Caller(0) 201 | _, file, line, ok = runtime.Caller(0) 202 | line-- 203 | return 204 | } 205 | 206 | func TestCallFormat(t *testing.T) { 207 | t.Parallel() 208 | 209 | c := stack.Caller(0) 210 | _, file, line, ok := runtime.Caller(0) 211 | line-- 212 | if !ok { 213 | t.Fatal("runtime.Caller(0) failed") 214 | } 215 | relFile := path.Join(importPath, filepath.Base(file)) 216 | 217 | c2, file2, line2, ok2 := testType{}.testMethod() 218 | if !ok2 { 219 | t.Fatal("runtime.Caller(0) failed") 220 | } 221 | relFile2 := path.Join(importPath, filepath.Base(file2)) 222 | 223 | data := []struct { 224 | c stack.Call 225 | desc string 226 | fmt string 227 | out string 228 | }{ 229 | {stack.Call{}, "error", "%s", "%!s(NOFUNC)"}, 230 | 231 | {c, "func", "%s", path.Base(file)}, 232 | {c, "func", "%+s", relFile}, 233 | {c, "func", "%#s", file}, 234 | {c, "func", "%d", fmt.Sprint(line)}, 235 | {c, "func", "%n", "TestCallFormat"}, 236 | {c, "func", "%+n", "github.com/go-stack/stack_test.TestCallFormat"}, 237 | {c, "func", "%k", "stack_test"}, 238 | {c, "func", "%+k", "github.com/go-stack/stack_test"}, 239 | {c, "func", "%v", fmt.Sprint(path.Base(file), ":", line)}, 240 | {c, "func", "%+v", fmt.Sprint(relFile, ":", line)}, 241 | {c, "func", "%#v", fmt.Sprint(file, ":", line)}, 242 | 243 | {c2, "meth", "%s", path.Base(file2)}, 244 | {c2, "meth", "%+s", relFile2}, 245 | {c2, "meth", "%#s", file2}, 246 | {c2, "meth", "%d", fmt.Sprint(line2)}, 247 | {c2, "meth", "%n", "testType.testMethod"}, 248 | {c2, "meth", "%+n", "github.com/go-stack/stack_test.testType.testMethod"}, 249 | {c2, "meth", "%k", "stack_test"}, 250 | {c2, "meth", "%+k", "github.com/go-stack/stack_test"}, 251 | {c2, "meth", "%v", fmt.Sprint(path.Base(file2), ":", line2)}, 252 | {c2, "meth", "%+v", fmt.Sprint(relFile2, ":", line2)}, 253 | {c2, "meth", "%#v", fmt.Sprint(file2, ":", line2)}, 254 | } 255 | 256 | for _, d := range data { 257 | got := fmt.Sprintf(d.fmt, d.c) 258 | if got != d.out { 259 | t.Errorf("fmt.Sprintf(%q, Call(%s)) = %s, want %s", d.fmt, d.desc, got, d.out) 260 | } 261 | } 262 | } 263 | 264 | func TestCallString(t *testing.T) { 265 | t.Parallel() 266 | 267 | c := stack.Caller(0) 268 | _, file, line, ok := runtime.Caller(0) 269 | line-- 270 | if !ok { 271 | t.Fatal("runtime.Caller(0) failed") 272 | } 273 | 274 | c2, file2, line2, ok2 := testType{}.testMethod() 275 | if !ok2 { 276 | t.Fatal("runtime.Caller(0) failed") 277 | } 278 | 279 | data := []struct { 280 | c stack.Call 281 | desc string 282 | out string 283 | }{ 284 | {stack.Call{}, "error", "%!v(NOFUNC)"}, 285 | {c, "func", fmt.Sprint(path.Base(file), ":", line)}, 286 | {c2, "meth", fmt.Sprint(path.Base(file2), ":", line2)}, 287 | } 288 | 289 | for _, d := range data { 290 | got := d.c.String() 291 | if got != d.out { 292 | t.Errorf("got %s, want %s", got, d.out) 293 | } 294 | } 295 | } 296 | 297 | func TestCallMarshalText(t *testing.T) { 298 | t.Parallel() 299 | 300 | c := stack.Caller(0) 301 | _, file, line, ok := runtime.Caller(0) 302 | line-- 303 | if !ok { 304 | t.Fatal("runtime.Caller(0) failed") 305 | } 306 | 307 | c2, file2, line2, ok2 := testType{}.testMethod() 308 | if !ok2 { 309 | t.Fatal("runtime.Caller(0) failed") 310 | } 311 | 312 | data := []struct { 313 | c stack.Call 314 | desc string 315 | out []byte 316 | err error 317 | }{ 318 | {stack.Call{}, "error", nil, stack.ErrNoFunc}, 319 | {c, "func", []byte(fmt.Sprint(path.Base(file), ":", line)), nil}, 320 | {c2, "meth", []byte(fmt.Sprint(path.Base(file2), ":", line2)), nil}, 321 | } 322 | 323 | for _, d := range data { 324 | text, err := d.c.MarshalText() 325 | if got, want := err, d.err; got != want { 326 | t.Errorf("%s: got err %v, want err %v", d.desc, got, want) 327 | } 328 | if got, want := text, d.out; !reflect.DeepEqual(got, want) { 329 | t.Errorf("%s: got %s, want %s", d.desc, got, want) 330 | } 331 | } 332 | } 333 | 334 | func TestCallStackString(t *testing.T) { 335 | cs, line0 := getTrace(t) 336 | _, file, line1, ok := runtime.Caller(0) 337 | line1-- 338 | if !ok { 339 | t.Fatal("runtime.Caller(0) failed") 340 | } 341 | file = path.Base(file) 342 | if got, want := cs.String(), fmt.Sprintf("[%s:%d %s:%d]", file, line0, file, line1); got != want { 343 | t.Errorf("\n got %v\nwant %v", got, want) 344 | } 345 | } 346 | 347 | func TestCallStackMarshalText(t *testing.T) { 348 | cs, line0 := getTrace(t) 349 | _, file, line1, ok := runtime.Caller(0) 350 | line1-- 351 | if !ok { 352 | t.Fatal("runtime.Caller(0) failed") 353 | } 354 | file = path.Base(file) 355 | text, _ := cs.MarshalText() 356 | if got, want := text, []byte(fmt.Sprintf("[%s:%d %s:%d]", file, line0, file, line1)); !reflect.DeepEqual(got, want) { 357 | t.Errorf("\n got %v\nwant %v", got, want) 358 | } 359 | } 360 | 361 | func getTrace(t *testing.T) (stack.CallStack, int) { 362 | cs := stack.Trace().TrimRuntime() 363 | _, _, line, ok := runtime.Caller(0) 364 | line-- 365 | if !ok { 366 | t.Fatal("runtime.Caller(0) failed") 367 | } 368 | return cs, line 369 | } 370 | 371 | func TestTrimAbove(t *testing.T) { 372 | trace := trimAbove() 373 | if got, want := len(trace), 2; got != want { 374 | t.Fatalf("got len(trace) == %v, want %v, trace: %n", got, want, trace) 375 | } 376 | if got, want := fmt.Sprintf("%n", trace[1]), "TestTrimAbove"; got != want { 377 | t.Errorf("got %q, want %q", got, want) 378 | } 379 | } 380 | 381 | func trimAbove() stack.CallStack { 382 | call := stack.Caller(1) 383 | trace := stack.Trace() 384 | return trace.TrimAbove(call) 385 | } 386 | 387 | func TestTrimBelow(t *testing.T) { 388 | trace := trimBelow() 389 | if got, want := fmt.Sprintf("%n", trace[0]), "TestTrimBelow"; got != want { 390 | t.Errorf("got %q, want %q", got, want) 391 | } 392 | } 393 | 394 | func trimBelow() stack.CallStack { 395 | call := stack.Caller(1) 396 | trace := stack.Trace() 397 | return trace.TrimBelow(call) 398 | } 399 | 400 | func TestTrimRuntime(t *testing.T) { 401 | trace := stack.Trace().TrimRuntime() 402 | if got, want := len(trace), 1; got != want { 403 | t.Errorf("got len(trace) == %v, want %v, goroot: %q, trace: %#v", got, want, runtime.GOROOT(), trace) 404 | } 405 | } 406 | 407 | func BenchmarkCallVFmt(b *testing.B) { 408 | c := stack.Caller(0) 409 | b.ResetTimer() 410 | for i := 0; i < b.N; i++ { 411 | fmt.Fprint(ioutil.Discard, c) 412 | } 413 | } 414 | 415 | func BenchmarkCallPlusVFmt(b *testing.B) { 416 | c := stack.Caller(0) 417 | b.ResetTimer() 418 | for i := 0; i < b.N; i++ { 419 | fmt.Fprintf(ioutil.Discard, "%+v", c) 420 | } 421 | } 422 | 423 | func BenchmarkCallSharpVFmt(b *testing.B) { 424 | c := stack.Caller(0) 425 | b.ResetTimer() 426 | for i := 0; i < b.N; i++ { 427 | fmt.Fprintf(ioutil.Discard, "%#v", c) 428 | } 429 | } 430 | 431 | func BenchmarkCallSFmt(b *testing.B) { 432 | c := stack.Caller(0) 433 | b.ResetTimer() 434 | for i := 0; i < b.N; i++ { 435 | fmt.Fprintf(ioutil.Discard, "%s", c) 436 | } 437 | } 438 | 439 | func BenchmarkCallPlusSFmt(b *testing.B) { 440 | c := stack.Caller(0) 441 | b.ResetTimer() 442 | for i := 0; i < b.N; i++ { 443 | fmt.Fprintf(ioutil.Discard, "%+s", c) 444 | } 445 | } 446 | 447 | func BenchmarkCallSharpSFmt(b *testing.B) { 448 | c := stack.Caller(0) 449 | b.ResetTimer() 450 | for i := 0; i < b.N; i++ { 451 | fmt.Fprintf(ioutil.Discard, "%#s", c) 452 | } 453 | } 454 | 455 | func BenchmarkCallDFmt(b *testing.B) { 456 | c := stack.Caller(0) 457 | b.ResetTimer() 458 | for i := 0; i < b.N; i++ { 459 | fmt.Fprintf(ioutil.Discard, "%d", c) 460 | } 461 | } 462 | 463 | func BenchmarkCallNFmt(b *testing.B) { 464 | c := stack.Caller(0) 465 | b.ResetTimer() 466 | for i := 0; i < b.N; i++ { 467 | fmt.Fprintf(ioutil.Discard, "%n", c) 468 | } 469 | } 470 | 471 | func BenchmarkCallPlusNFmt(b *testing.B) { 472 | c := stack.Caller(0) 473 | b.ResetTimer() 474 | for i := 0; i < b.N; i++ { 475 | fmt.Fprintf(ioutil.Discard, "%+n", c) 476 | } 477 | } 478 | 479 | func BenchmarkCaller(b *testing.B) { 480 | for i := 0; i < b.N; i++ { 481 | stack.Caller(0) 482 | } 483 | } 484 | 485 | func BenchmarkTrace(b *testing.B) { 486 | for i := 0; i < b.N; i++ { 487 | stack.Trace() 488 | } 489 | } 490 | 491 | func deepStack(depth int, b *testing.B) stack.CallStack { 492 | if depth > 0 { 493 | return deepStack(depth-1, b) 494 | } 495 | b.StartTimer() 496 | s := stack.Trace() 497 | return s 498 | } 499 | 500 | func BenchmarkTrace10(b *testing.B) { 501 | for i := 0; i < b.N; i++ { 502 | b.StopTimer() 503 | deepStack(10, b) 504 | } 505 | } 506 | 507 | func BenchmarkTrace50(b *testing.B) { 508 | b.StopTimer() 509 | for i := 0; i < b.N; i++ { 510 | deepStack(50, b) 511 | } 512 | } 513 | 514 | func BenchmarkTrace100(b *testing.B) { 515 | b.StopTimer() 516 | for i := 0; i < b.N; i++ { 517 | deepStack(100, b) 518 | } 519 | } 520 | 521 | //////////////// 522 | // Benchmark functions followed by formatting 523 | //////////////// 524 | 525 | func BenchmarkCallerAndVFmt(b *testing.B) { 526 | for i := 0; i < b.N; i++ { 527 | fmt.Fprint(ioutil.Discard, stack.Caller(0)) 528 | } 529 | } 530 | 531 | func BenchmarkTraceAndVFmt(b *testing.B) { 532 | for i := 0; i < b.N; i++ { 533 | fmt.Fprint(ioutil.Discard, stack.Trace()) 534 | } 535 | } 536 | 537 | func BenchmarkTrace10AndVFmt(b *testing.B) { 538 | for i := 0; i < b.N; i++ { 539 | b.StopTimer() 540 | fmt.Fprint(ioutil.Discard, deepStack(10, b)) 541 | } 542 | } 543 | 544 | //////////////// 545 | // Baseline against package runtime. 546 | //////////////// 547 | 548 | func BenchmarkRuntimeCaller(b *testing.B) { 549 | for i := 0; i < b.N; i++ { 550 | runtime.Caller(0) 551 | } 552 | } 553 | 554 | func BenchmarkRuntimeCallerAndFmt(b *testing.B) { 555 | for i := 0; i < b.N; i++ { 556 | _, file, line, _ := runtime.Caller(0) 557 | const sep = "/" 558 | if i := strings.LastIndex(file, sep); i != -1 { 559 | file = file[i+len(sep):] 560 | } 561 | fmt.Fprint(ioutil.Discard, file, ":", line) 562 | } 563 | } 564 | 565 | func BenchmarkFuncForPC(b *testing.B) { 566 | pc, _, _, _ := runtime.Caller(0) 567 | pc-- 568 | b.ResetTimer() 569 | for i := 0; i < b.N; i++ { 570 | runtime.FuncForPC(pc) 571 | } 572 | } 573 | 574 | func BenchmarkFuncFileLine(b *testing.B) { 575 | pc, _, _, _ := runtime.Caller(0) 576 | pc-- 577 | fn := runtime.FuncForPC(pc) 578 | b.ResetTimer() 579 | for i := 0; i < b.N; i++ { 580 | fn.FileLine(pc) 581 | } 582 | } 583 | --------------------------------------------------------------------------------