Skip to content

Commit a1d04bd

Browse files
authored
Merge pull request #129 from maxatome/trace
feat(trace): when an error occurs, display trace if level > 1
2 parents bba25dd + 2964d9d commit a1d04bd

4 files changed

Lines changed: 515 additions & 3 deletions

File tree

internal/location/location.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ import (
1212
"strings"
1313
)
1414

15-
// Location record a place in a source file.
15+
// Location records a place in a source file.
1616
type Location struct {
1717
File string // File name
1818
Func string // Function name
1919
Line int // Line number inside file
20-
Inside string
21-
BehindCmp bool // BehindCmp is true when operator is behind a Cmp* function
20+
Inside string // Inside is used when Location is inside something else
21+
BehindCmp bool // BehindCmp is true when operator is behind a Cmp* function
2222
}
2323

2424
// GetLocationer is the interface that wraps the basic GetLocation method.

internal/trace/trace.go

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
// Copyright (c) 2021, Maxime Soulé
2+
// All rights reserved.
3+
//
4+
// This source code is licensed under the BSD-style license found in the
5+
// LICENSE file in the root directory of this source tree.
6+
7+
package trace
8+
9+
import (
10+
"fmt"
11+
"go/build"
12+
"os"
13+
"path/filepath"
14+
"runtime"
15+
"strings"
16+
)
17+
18+
var (
19+
ignorePkg = map[string]struct{}{}
20+
goPaths []string
21+
goModDir string
22+
)
23+
24+
// Level represents a level when retrieving a trace.
25+
type Level struct {
26+
Func string
27+
FileLine string
28+
}
29+
30+
func getPackage(skip ...int) string {
31+
sk := 2
32+
if len(skip) > 0 {
33+
sk += skip[0]
34+
}
35+
pc, _, _, ok := runtime.Caller(sk)
36+
if ok {
37+
fn := runtime.FuncForPC(pc)
38+
if fn != nil {
39+
pkg, _ := SplitPackageFunc(fn.Name())
40+
return pkg
41+
}
42+
}
43+
return ""
44+
}
45+
46+
// IgnorePackage records the calling package as ignored one in trace.
47+
func IgnorePackage(skip ...int) bool {
48+
if pkg := getPackage(skip...); pkg != "" {
49+
ignorePkg[pkg] = struct{}{}
50+
return true
51+
}
52+
return false
53+
}
54+
55+
// UnignorePackage cancels a previous use of IgnorePackage, so the
56+
// calling package is no longer ignored. Only intended to be used in
57+
// go-testdeep internal tests.
58+
func UnignorePackage(skip ...int) bool {
59+
if pkg := getPackage(skip...); pkg != "" {
60+
delete(ignorePkg, pkg)
61+
return true
62+
}
63+
return false
64+
}
65+
66+
// IsIgnoredPackage returns true if pkg is ignored, false
67+
// otherwise. Only intended to be used in go-testdeep internal tests.
68+
func IsIgnoredPackage(pkg string) (ok bool) {
69+
_, ok = ignorePkg[pkg]
70+
return
71+
}
72+
73+
// FindGoModDir finds the closest directory containing go.mod file
74+
// starting from directory in.
75+
func FindGoModDir(in string) string {
76+
for {
77+
_, err := os.Stat(filepath.Join(in, "go.mod"))
78+
if err == nil {
79+
// Do not accept /tmp/go.mod
80+
if in != os.TempDir() {
81+
return in + string(filepath.Separator)
82+
}
83+
return ""
84+
}
85+
86+
nd := filepath.Dir(in)
87+
if nd == in {
88+
return ""
89+
}
90+
in = nd
91+
}
92+
}
93+
94+
// FindGoModDirLinks finds the closest directory containing go.mod
95+
// file starting from directory in after cleaning it. If not found,
96+
// expands symlinks and re-searches.
97+
func FindGoModDirLinks(in string) string {
98+
in = filepath.Clean(in)
99+
100+
if gm := FindGoModDir(in); gm != "" {
101+
return gm
102+
}
103+
104+
lin, err := filepath.EvalSymlinks(in)
105+
if err == nil && lin != in {
106+
return FindGoModDir(lin)
107+
}
108+
return ""
109+
}
110+
111+
// Reset resets the ignored packages map plus cached mod and GOPATH
112+
// directories (Init() should be called again). Only intended to be
113+
// used in go-testdeep internal tests.
114+
func Reset() {
115+
ignorePkg = map[string]struct{}{}
116+
goPaths = nil
117+
goModDir = ""
118+
}
119+
120+
// Init initializes trace global variables.
121+
func Init() {
122+
// GOPATH directories
123+
goPaths = nil
124+
for _, dir := range filepath.SplitList(build.Default.GOPATH) {
125+
dir = filepath.Clean(dir)
126+
goPaths = append(goPaths,
127+
filepath.Join(dir, "pkg", "mod")+string(filepath.Separator),
128+
filepath.Join(dir, "src")+string(filepath.Separator),
129+
)
130+
}
131+
132+
if wd, err := os.Getwd(); err == nil {
133+
// go.mod directory
134+
goModDir = FindGoModDirLinks(wd)
135+
}
136+
}
137+
138+
// Frames is the interface corresponding to type returned by
139+
// runtime.CallersFrames. See CallersFrames variable.
140+
type Frames interface {
141+
Next() (frame runtime.Frame, more bool)
142+
}
143+
144+
// CallersFrames is only intended to be used in go-testdeep internal
145+
// tests to cover all cases.
146+
var CallersFrames = func(callers []uintptr) Frames {
147+
return runtime.CallersFrames(callers)
148+
}
149+
150+
// Retrieve retrieves a trace and returns it.
151+
func Retrieve(skip int, endFunction string) []Level {
152+
var trace []Level
153+
var pc [40]uintptr
154+
if num := runtime.Callers(skip+2, pc[:]); num > 0 {
155+
checkIgnore := true
156+
frames := CallersFrames(pc[:num])
157+
for {
158+
frame, more := frames.Next()
159+
160+
fn := frame.Function
161+
if fn == endFunction {
162+
break
163+
}
164+
165+
var pkg string
166+
if fn == "" {
167+
if frame.File == "" {
168+
if more {
169+
continue
170+
}
171+
break
172+
}
173+
fn = "<unknown function>"
174+
} else {
175+
pkg, fn = SplitPackageFunc(fn)
176+
if checkIgnore && IsIgnoredPackage(pkg) {
177+
if more {
178+
continue
179+
}
180+
break
181+
}
182+
checkIgnore = false
183+
}
184+
185+
file := strings.TrimPrefix(frame.File, goModDir)
186+
if file == frame.File {
187+
for _, dir := range goPaths {
188+
file = strings.TrimPrefix(frame.File, dir)
189+
if file != frame.File {
190+
break
191+
}
192+
}
193+
194+
if file == frame.File {
195+
file = strings.TrimPrefix(frame.File, build.Default.GOROOT)
196+
if file != frame.File {
197+
file = filepath.Join("$GOROOT", file)
198+
}
199+
}
200+
}
201+
202+
level := Level{Func: fn}
203+
if file != "" {
204+
level.FileLine = fmt.Sprintf("%s:%d", file, frame.Line)
205+
}
206+
207+
trace = append(trace, level)
208+
if !more {
209+
break
210+
}
211+
}
212+
}
213+
return trace
214+
}
215+
216+
// SplitPackageFunc splits a fully qualified function name into its
217+
// package and function parts:
218+
// "foo/bar/test.fn" → "foo/bar/test", "fn"
219+
// "foo/bar/test.X.fn" → "foo/bar/test", "X.fn"
220+
// "foo/bar/test.(*X).fn" → "foo/bar/test", "(*X).fn"
221+
// "foo/bar/test.(*X).fn.func1" → "foo/bar/test", "(*X).fn.func1"
222+
// "weird" → "", "weird"
223+
func SplitPackageFunc(fn string) (string, string) {
224+
sp := strings.LastIndexByte(fn, '/')
225+
if sp < 0 {
226+
sp = 0 // std package
227+
}
228+
229+
dp := strings.IndexByte(fn[sp:], '.')
230+
if dp < 0 {
231+
return "", fn
232+
}
233+
234+
return fn[:sp+dp], fn[sp+dp+1:]
235+
}

0 commit comments

Comments
 (0)