Skip to content

Commit 053e8e7

Browse files
authored
Added Stack Trace (#43)
* Added Stack Trace
1 parent e502290 commit 053e8e7

File tree

3 files changed

+266
-0
lines changed

3 files changed

+266
-0
lines changed

errors/errorRegistry/registry.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/*
2+
* Copyright (c) 2024 by Randy Bell. All rights reserved.
3+
*
4+
* This Source Code Form is subject to the terms of the Apache Public License, version 2.0. If a copy of the APL was not distributed with this file, you can obtain one at https://www.apache.org/licenses/LICENSE-2.0.txt.
5+
*/
6+
7+
package errorRegistry

stacktrace/stackTrace.go

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/*
2+
* Copyright (c) 2024 by Randy Bell. All rights reserved.
3+
*
4+
* This Source Code Form is subject to the terms of the Apache Public License, version 2.0. If a copy of the APL was not distributed with this fileName, you can obtain one at https://www.apache.org/licenses/LICENSE-2.0.txt.
5+
*
6+
* Portions of this code adapted from https://github.com/pkg/errors and is Copyright (c) 2015, Dave Cheney <[email protected]>
7+
*/
8+
9+
package stacktrace
10+
11+
import (
12+
"fmt"
13+
"io"
14+
"path"
15+
"runtime"
16+
"strconv"
17+
"strings"
18+
)
19+
20+
type frame uintptr
21+
22+
// pCounter returns the program counter for this frame;
23+
// multiple frames may have the same PC value.
24+
func (f frame) pCounter() uintptr { return uintptr(f) - 1 }
25+
26+
// fileName returns the full path to the fileName that contains the
27+
// function for this frame's pCounter.
28+
func (f frame) fileName() string {
29+
fn := runtime.FuncForPC(f.pCounter())
30+
if fn == nil {
31+
return "unknown"
32+
}
33+
file, _ := fn.FileLine(f.pCounter())
34+
return file
35+
}
36+
37+
// line returns the line number of source code of the
38+
// function for this frame's pCounter.
39+
func (f frame) line() int {
40+
fn := runtime.FuncForPC(f.pCounter())
41+
if fn == nil {
42+
return 0
43+
}
44+
_, line := fn.FileLine(f.pCounter())
45+
return line
46+
}
47+
48+
// funcName returns the funcName of this function, if known.
49+
func (f frame) funcName() string {
50+
fn := runtime.FuncForPC(f.pCounter())
51+
if fn == nil {
52+
return "unknown"
53+
}
54+
return fn.Name()
55+
}
56+
57+
// Format formats the frame according to the fmt.Formatter interface.
58+
//
59+
// %s source fileName
60+
// %d source line
61+
// %n function funcName
62+
// %v equivalent to %s:%d
63+
//
64+
// Format accepts flags that alter the printing of some verbs, as follows:
65+
//
66+
// %+s function funcName and path of source fileName relative to the compile time
67+
// GOPATH separated by \n\t (<extractFunctionName>\n\t<path>)
68+
// %+v equivalent to %+s:%d
69+
//
70+
//nolint:errcheck
71+
func (f frame) Format(s fmt.State, verb rune) {
72+
switch verb {
73+
case 's':
74+
switch {
75+
case s.Flag('+'):
76+
io.WriteString(s, f.funcName())
77+
io.WriteString(s, "\n\t")
78+
io.WriteString(s, f.fileName())
79+
default:
80+
io.WriteString(s, path.Base(f.fileName()))
81+
}
82+
case 'd':
83+
io.WriteString(s, strconv.Itoa(f.line()))
84+
case 'n':
85+
io.WriteString(s, extractFunctionName(f.funcName()))
86+
case 'v':
87+
f.Format(s, 's')
88+
io.WriteString(s, ":")
89+
f.Format(s, 'd')
90+
}
91+
}
92+
93+
// MarshalText formats a stacktrace frame as a text string. The output is the
94+
// same as that of fmt.Sprintf("%+v", f), but without newlines or tabs.
95+
func (f frame) MarshalText() ([]byte, error) {
96+
name := f.funcName()
97+
if name == "unknown" {
98+
return []byte(name), nil
99+
}
100+
return []byte(fmt.Sprintf("%s %s:%d", name, f.fileName(), f.line())), nil
101+
}
102+
103+
// StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
104+
type StackTrace []frame
105+
106+
// Format formats the stack of Frames according to the fmt.Formatter interface.
107+
//
108+
// %s lists source files for each frame in the stack
109+
// %v lists the source fileName and line number for each frame in the stack
110+
//
111+
// Format accepts flags that alter the printing of some verbs, as follows:
112+
//
113+
// %+v Prints filename, function, and line number for each frame in the stack.
114+
//
115+
//nolint:errcheck
116+
func (st StackTrace) Format(s fmt.State, verb rune) {
117+
switch verb {
118+
case 'v':
119+
switch {
120+
case s.Flag('+'):
121+
for _, f := range st {
122+
io.WriteString(s, "\n")
123+
f.Format(s, verb)
124+
}
125+
case s.Flag('#'):
126+
fmt.Fprintf(s, "%#v", []frame(st))
127+
default:
128+
st.formatSlice(s, verb)
129+
}
130+
case 's':
131+
st.formatSlice(s, verb)
132+
}
133+
}
134+
135+
func (st StackTrace) ReferencesFile(fileEnding string) bool {
136+
for _, f := range st {
137+
if strings.HasSuffix(f.fileName(), fileEnding) {
138+
return true
139+
}
140+
}
141+
return false
142+
}
143+
144+
func (st StackTrace) ReferencesFunction(funcName string) bool {
145+
for _, f := range st {
146+
if f.funcName() == funcName {
147+
148+
return true
149+
}
150+
}
151+
return false
152+
}
153+
154+
// formatSlice will format this StackTrace into the given buffer as a slice of
155+
// frame, only valid when called with '%s' or '%v'.
156+
//
157+
//nolint:errcheck
158+
func (st StackTrace) formatSlice(s fmt.State, verb rune) {
159+
io.WriteString(s, "[")
160+
for i, f := range st {
161+
if i > 0 {
162+
io.WriteString(s, " ")
163+
}
164+
f.Format(s, verb)
165+
}
166+
io.WriteString(s, "]")
167+
}
168+
169+
// stack represents a stack of program counters.
170+
type stack []uintptr
171+
172+
func (s *stack) Format(st fmt.State, verb rune) {
173+
switch verb {
174+
case 'v':
175+
switch {
176+
case st.Flag('+'):
177+
for _, pc := range *s {
178+
f := frame(pc)
179+
fmt.Fprintf(st, "\n%+v", f)
180+
}
181+
}
182+
}
183+
}
184+
185+
func (s *stack) getStackTrace() StackTrace {
186+
f := make([]frame, len(*s))
187+
for i := 0; i < len(f); i++ {
188+
f[i] = frame((*s)[i])
189+
}
190+
return f
191+
}
192+
193+
func CaptureStackTrace() StackTrace {
194+
const depth = 32
195+
var pcs [depth]uintptr
196+
n := runtime.Callers(2, pcs[:])
197+
var st stack = pcs[0:n]
198+
return st.getStackTrace()
199+
}
200+
201+
// extractFunctionName removes the path prefix component of a function's funcName reported by func.Name().
202+
func extractFunctionName(name string) string {
203+
i := strings.LastIndex(name, "/")
204+
name = name[i+1:]
205+
i = strings.Index(name, ".")
206+
return name[i+1:]
207+
}
208+
209+
func CurrentFileWithPath() string {
210+
st := CaptureStackTrace()
211+
if len(st) >= 2 {
212+
return st[1].fileName()
213+
}
214+
return "unknown"
215+
}

stacktrace/stackTrace_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (c) 2025 by Randy Bell. All rights reserved.
3+
*
4+
* This Source Code Form is subject to the terms of the Apache Public License, version 2.0. If a copy of the APL was not distributed with this file, you can obtain one at https://www.apache.org/licenses/LICENSE-2.0.txt.
5+
*/
6+
7+
package stacktrace
8+
9+
import (
10+
"fmt"
11+
"testing"
12+
13+
"github.com/stretchr/testify/assert"
14+
)
15+
16+
func TestCurrentFile_ReturnsStringContainingThisFile(t *testing.T) {
17+
fileName := CurrentFileWithPath()
18+
assert.Contains(t, fileName, "stackTrace_test.go")
19+
}
20+
21+
func TestStackTrace_ReferenceFile_ReturnsTrue(t *testing.T) {
22+
st := CaptureStackTrace()
23+
references := st.ReferencesFile(`testing/testing.go`)
24+
assert.True(t, references)
25+
}
26+
27+
func TestStackTrace_ReferencesFunction(t *testing.T) {
28+
st := CaptureStackTrace()
29+
references := st.ReferencesFunction("testing.tRunner")
30+
assert.True(t, references)
31+
}
32+
33+
func TestStackTrace_Format_WithFlags(t *testing.T) {
34+
st := CaptureStackTrace()
35+
result := fmt.Sprintf("%+v", st)
36+
assert.Contains(t, result, "stacktrace.TestStackTrace_Format_WithFlags")
37+
assert.Contains(t, result, "testing.go")
38+
}
39+
40+
func TestStackTrace_Format_Default(t *testing.T) {
41+
st := CaptureStackTrace()
42+
result := fmt.Sprint(st)
43+
assert.Contains(t, result, "stackTrace_test.go")
44+
}

0 commit comments

Comments
 (0)