├── .github └── workflows │ └── go.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── colors.go ├── error.go ├── error_bench_test.go ├── error_helper_test.go ├── error_test.go ├── examples ├── existing_error.go ├── new_error.go ├── nil_error.go ├── save_log.go └── stack_trace.go ├── go.mod ├── output.png ├── print.go └── print_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version: '1.22' 23 | 24 | - name: Test 25 | run: go test -cover -v 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # IDE 25 | /.idea 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.4.0] - 2023-05-21 4 | 5 | ### Changed 6 | 7 | - `github.com/logrusorgru/aurora` dependency removed. 8 | - Deprecated `ioutil.ReadFile` replaced with `os.ReadFile`. 9 | 10 | ## [0.3.0] - 2019-03-15 11 | 12 | ### Added 13 | 14 | - `tracerr.CustomError()` that allows to create error with custom stack trace. 15 | 16 | ### Changed 17 | 18 | - `*tracerr.Error` struct replaced with `tracerr.Error` interface. 19 | 20 | ## [0.2.1] - 2019-02-16 21 | 22 | ### Added 23 | 24 | - Benchmarks. 25 | - `DefaultCap` variable for performance tuning purposes. 26 | 27 | ### Changed 28 | 29 | - Stack trace performance optimisation. 30 | 31 | ## [0.2.0] - 2019-02-15 32 | 33 | ### Added 34 | 35 | - `tracerr.Unwrap()` and `Error.Unwrap()` that returns the original error. 36 | 37 | ## [0.1.2] - 2019-02-12 38 | 39 | ### Changed 40 | 41 | - RWMutex added for files caching, which fixing concurrent cache writing or writing-reading if any. 42 | 43 | ## [0.1.1] - 2019-02-09 44 | 45 | ### Added 46 | 47 | - Changelog. 48 | - `go.mod` file. 49 | - License. 50 | - Tests with 100% coverage. 51 | - Travis CI. 52 | 53 | ### Changed 54 | 55 | - `Error.Err` and `Error.Frames` properties are now exported. 56 | - `Error.Error()` called on `nil` now returns empty string instead of panics. 57 | - All print and sprint functions called with `nil` error now returns or prints empty string instead of panics. 58 | 59 | ## [0.1.0] - 2019-02-06 60 | 61 | ### Added 62 | 63 | - Initial version. 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 ztrue 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: test 2 | 3 | .PHONY: lint 4 | lint: 5 | go fmt . && \ 6 | go fmt ./examples && \ 7 | go vet && \ 8 | golint $$(go list ./...) 9 | 10 | .PHONY: doc 11 | doc: 12 | @echo GoDoc link: http://localhost:6060/pkg/github.com/ztrue/tracerr 13 | godoc -http=:6060 14 | 15 | .PHONY: test 16 | test: 17 | go test -cover -v 18 | 19 | .PHONY: coverage 20 | coverage: 21 | go test -coverprofile=coverage.out && \ 22 | go tool cover -func=coverage.out && \ 23 | go tool cover -html=coverage.out 24 | 25 | .PHONY: bench 26 | bench: 27 | GOMAXPROCS=1 go test -bench=. -benchmem 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang Errors with Stack Trace and Source Fragments 2 | 3 | [![GoDoc](https://godoc.org/github.com/ztrue/tracerr?status.svg)](https://godoc.org/github.com/ztrue/tracerr) 4 | [![Report](https://goreportcard.com/badge/github.com/ztrue/tracerr)](https://goreportcard.com/report/github.com/ztrue/tracerr) 5 | [![Coverage Status](https://coveralls.io/repos/github/ztrue/tracerr/badge.svg?branch=master)](https://coveralls.io/github/ztrue/tracerr?branch=master) 6 | 7 | Tired of uninformative error output? Probably this will be more convenient: 8 | 9 | ![Output](output.png) 10 | 11 | ## Example 12 | 13 | ```go 14 | package main 15 | 16 | import ( 17 | "io/ioutil" 18 | 19 | "github.com/ztrue/tracerr" 20 | ) 21 | 22 | func main() { 23 | if err := read(); err != nil { 24 | tracerr.PrintSourceColor(err) 25 | } 26 | } 27 | 28 | func read() error { 29 | return readNonExistent() 30 | } 31 | 32 | func readNonExistent() error { 33 | _, err := ioutil.ReadFile("/tmp/non_existent_file") 34 | // Add stack trace to existing error, no matter if it's nil. 35 | return tracerr.Wrap(err) 36 | } 37 | ``` 38 | 39 | Find more executable examples in [examples](examples) dir. 40 | 41 | ## How to Use 42 | 43 | ### Import 44 | 45 | ```go 46 | import "github.com/ztrue/tracerr" 47 | ``` 48 | 49 | ### Create New Error 50 | 51 | ```go 52 | err := tracerr.New("some error") 53 | ``` 54 | 55 | Or: 56 | 57 | ```go 58 | err := tracerr.Errorf("some error %d", num) 59 | ``` 60 | 61 | ### Add Stack Trace to Existing Error 62 | 63 | > If `err` is `nil` then it still be `nil` with no stack trace added. 64 | 65 | ```go 66 | err = tracerr.Wrap(err) 67 | ``` 68 | 69 | ### Print Error and Stack Trace 70 | 71 | > Stack trace will be printed only if `err` is of type `tracerr.Error`, otherwise just error text will be shown. 72 | 73 | This will print error message and stack trace if any: 74 | 75 | ```go 76 | tracerr.Print(err) 77 | ``` 78 | 79 | This will add source code: 80 | 81 | ```go 82 | tracerr.PrintSource(err) 83 | ``` 84 | 85 | It's able to set up number of lines of code to display for each frame, which is `6` by default: 86 | 87 | ```go 88 | tracerr.PrintSource(err, 9) 89 | ``` 90 | 91 | Or to set up number of lines before and after traced line: 92 | 93 | ```go 94 | tracerr.PrintSource(err, 5, 2) 95 | ``` 96 | 97 | The same, but with color, which is much more useful: 98 | 99 | ```go 100 | tracerr.PrintSourceColor(err) 101 | ``` 102 | 103 | ```go 104 | tracerr.PrintSourceColor(err, 9) 105 | ``` 106 | 107 | ```go 108 | tracerr.PrintSourceColor(err, 5, 2) 109 | ``` 110 | 111 | ### Save Output to Variable 112 | 113 | It's also able to save output to variable instead of printing it, which works the same way: 114 | 115 | ```go 116 | text := tracerr.Sprint(err) 117 | ``` 118 | 119 | ```go 120 | text := tracerr.SprintSource(err) 121 | ``` 122 | 123 | ```go 124 | text := tracerr.SprintSource(err, 9) 125 | ``` 126 | 127 | ```go 128 | text := tracerr.SprintSource(err, 5, 2) 129 | ``` 130 | 131 | ### Get Stack Trace 132 | 133 | > Stack trace will be empty if `err` is not an instance of `tracerr.Error`. 134 | 135 | ```go 136 | frames := tracerr.StackTrace(err) 137 | ``` 138 | 139 | Or if `err` is of type `tracerr.Error`: 140 | 141 | ```go 142 | frames := err.StackTrace() 143 | ``` 144 | 145 | ### Get Original Error 146 | 147 | > Unwrapped error will be `nil` if `err` is `nil` and will be the same error if `err` is not an instance of `tracerr.Error`. 148 | 149 | ```go 150 | err = tracerr.Unwrap(err) 151 | ``` 152 | 153 | Or if `err` is of type `tracerr.Error`: 154 | 155 | ```go 156 | err = err.Unwrap() 157 | ``` 158 | 159 | ## Performance 160 | 161 | Stack trace causes a performance overhead, depending on a stack trace depth. This can be insignificant in a number of situations (such as HTTP request handling), however, avoid of adding a stack trace for really hot spots where a high number of errors created frequently, this can be inefficient. 162 | 163 | > Benchmarks done on a MacBook Pro 2015 with go 1.11. 164 | 165 | Benchmarks for creating a new error with a stack trace of different depth: 166 | 167 | ``` 168 | BenchmarkNew/5 200000 5646 ns/op 976 B/op 4 allocs/op 169 | BenchmarkNew/10 200000 11565 ns/op 976 B/op 4 allocs/op 170 | BenchmarkNew/20 50000 25629 ns/op 976 B/op 4 allocs/op 171 | BenchmarkNew/40 20000 65833 ns/op 2768 B/op 5 allocs/op 172 | ``` 173 | -------------------------------------------------------------------------------- /colors.go: -------------------------------------------------------------------------------- 1 | package tracerr 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Colorize outputs using [ANSI Escape Codes](https://en.wikipedia.org/wiki/ANSI_escape_code) 8 | 9 | func color(code int, in string) string { 10 | return fmt.Sprintf("\x1b[%dm%s\x1b[0m", code, in) 11 | } 12 | 13 | func bold(in string) string { 14 | return color(1, in) 15 | } 16 | 17 | func black(in string) string { 18 | return color(30, in) 19 | } 20 | 21 | func red(in string) string { 22 | return color(31, in) 23 | } 24 | 25 | func yellow(in string) string { 26 | return color(33, in) 27 | } 28 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | // Package tracerr makes error output more informative. 2 | // It adds stack trace to error and can display error with source fragments. 3 | // 4 | // Check example of output here https://github.com/ztrue/tracerr 5 | package tracerr 6 | 7 | import ( 8 | "fmt" 9 | "runtime" 10 | ) 11 | 12 | // DefaultCap is a default cap for frames array. 13 | // It can be changed to number of expected frames 14 | // for purpose of performance optimisation. 15 | var DefaultCap = 20 16 | 17 | // Error is an error with stack trace. 18 | type Error interface { 19 | Error() string 20 | StackTrace() []Frame 21 | Unwrap() error 22 | } 23 | 24 | type errorData struct { 25 | // err contains original error. 26 | err error 27 | // frames contains stack trace of an error. 28 | frames []Frame 29 | } 30 | 31 | // CustomError creates an error with provided frames. 32 | func CustomError(err error, frames []Frame) Error { 33 | return &errorData{ 34 | err: err, 35 | frames: frames, 36 | } 37 | } 38 | 39 | // Errorf creates new error with stacktrace and formatted message. 40 | // Formatting works the same way as in fmt.Errorf. 41 | func Errorf(message string, args ...interface{}) Error { 42 | return trace(fmt.Errorf(message, args...), 2) 43 | } 44 | 45 | // New creates new error with stacktrace. 46 | func New(message string) Error { 47 | return trace(fmt.Errorf(message), 2) 48 | } 49 | 50 | // Wrap adds stacktrace to existing error. 51 | func Wrap(err error) Error { 52 | if err == nil { 53 | return nil 54 | } 55 | e, ok := err.(Error) 56 | if ok { 57 | return e 58 | } 59 | return trace(err, 2) 60 | } 61 | 62 | // Unwrap returns the original error. 63 | func Unwrap(err error) error { 64 | if err == nil { 65 | return nil 66 | } 67 | e, ok := err.(Error) 68 | if !ok { 69 | return err 70 | } 71 | return e.Unwrap() 72 | } 73 | 74 | // Error returns error message. 75 | func (e *errorData) Error() string { 76 | return e.err.Error() 77 | } 78 | 79 | // StackTrace returns stack trace of an error. 80 | func (e *errorData) StackTrace() []Frame { 81 | return e.frames 82 | } 83 | 84 | // Unwrap returns the original error. 85 | func (e *errorData) Unwrap() error { 86 | return e.err 87 | } 88 | 89 | // Frame is a single step in stack trace. 90 | type Frame struct { 91 | // Func contains a function name. 92 | Func string 93 | // Line contains a line number. 94 | Line int 95 | // Path contains a file path. 96 | Path string 97 | } 98 | 99 | // StackTrace returns stack trace of an error. 100 | // It will be empty if err is not of type Error. 101 | func StackTrace(err error) []Frame { 102 | e, ok := err.(Error) 103 | if !ok { 104 | return nil 105 | } 106 | return e.StackTrace() 107 | } 108 | 109 | // String formats Frame to string. 110 | func (f Frame) String() string { 111 | return fmt.Sprintf("%s:%d %s()", f.Path, f.Line, f.Func) 112 | } 113 | 114 | func trace(err error, skip int) Error { 115 | frames := make([]Frame, 0, DefaultCap) 116 | for { 117 | pc, path, line, ok := runtime.Caller(skip) 118 | if !ok { 119 | break 120 | } 121 | fn := runtime.FuncForPC(pc) 122 | frame := Frame{ 123 | Func: fn.Name(), 124 | Line: line, 125 | Path: path, 126 | } 127 | frames = append(frames, frame) 128 | skip++ 129 | } 130 | return &errorData{ 131 | err: err, 132 | frames: frames, 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /error_bench_test.go: -------------------------------------------------------------------------------- 1 | package tracerr_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/ztrue/tracerr" 8 | ) 9 | 10 | func BenchmarkNew(b *testing.B) { 11 | for _, frames := range []int{5, 10, 20, 40} { 12 | suffix := fmt.Sprintf("%d", frames) 13 | b.Run(suffix, func(b *testing.B) { 14 | err := tracerr.New("") 15 | // Reduce by number of parent frames in order to have a correct depth. 16 | depth := frames - len(err.StackTrace()) 17 | if depth < 1 { 18 | panic("number of frames is negative") 19 | } 20 | b.ResetTimer() 21 | 22 | for i := 0; i < b.N; i++ { 23 | addFrames(depth, "test error") 24 | } 25 | }) 26 | } 27 | } 28 | 29 | func addFrames(depth int, message string) error { 30 | if depth <= 1 { 31 | return tracerr.New(message) 32 | } 33 | return addFrames(depth-1, message) 34 | } 35 | -------------------------------------------------------------------------------- /error_helper_test.go: -------------------------------------------------------------------------------- 1 | // This file is added for purpose of having an example of different path in tests. 2 | package tracerr_test 3 | 4 | import ( 5 | "github.com/ztrue/tracerr" 6 | ) 7 | 8 | func addFrameA(message string) error { 9 | return addFrameB(message) 10 | } 11 | 12 | func addFrameB(message string) error { 13 | return addFrameC(message) 14 | } 15 | 16 | func addFrameC(message string) error { 17 | return tracerr.New(message) 18 | } 19 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package tracerr_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/ztrue/tracerr" 10 | ) 11 | 12 | type ErrorTestCase struct { 13 | Error tracerr.Error 14 | ExpectedMessage string 15 | ExpectedStackTrace []tracerr.Frame 16 | } 17 | 18 | func TestError(t *testing.T) { 19 | cases := []ErrorTestCase{ 20 | { 21 | Error: nil, 22 | ExpectedMessage: "", 23 | ExpectedStackTrace: nil, 24 | }, 25 | { 26 | Error: tracerr.Wrap(nil), 27 | ExpectedMessage: "", 28 | ExpectedStackTrace: nil, 29 | }, 30 | { 31 | Error: tracerr.New("error message text"), 32 | ExpectedMessage: "error message text", 33 | ExpectedStackTrace: []tracerr.Frame{ 34 | { 35 | Func: "github.com/ztrue/tracerr_test.TestError", 36 | Line: 31, 37 | Path: "/tracerr/error_test.go", 38 | }, 39 | }, 40 | }, 41 | { 42 | Error: tracerr.Errorf("invalid argument %d: %#v", 5, "foo"), 43 | ExpectedMessage: "invalid argument 5: \"foo\"", 44 | ExpectedStackTrace: []tracerr.Frame{ 45 | { 46 | Func: "github.com/ztrue/tracerr_test.TestError", 47 | Line: 42, 48 | Path: "/tracerr/error_test.go", 49 | }, 50 | }, 51 | }, 52 | { 53 | Error: tracerr.Wrap(errors.New("wrapped error")), 54 | ExpectedMessage: "wrapped error", 55 | ExpectedStackTrace: []tracerr.Frame{ 56 | { 57 | Func: "github.com/ztrue/tracerr_test.TestError", 58 | Line: 53, 59 | Path: "/tracerr/error_test.go", 60 | }, 61 | }, 62 | }, 63 | { 64 | Error: addFrameA("error with stack trace").(tracerr.Error), 65 | ExpectedMessage: "error with stack trace", 66 | ExpectedStackTrace: []tracerr.Frame{ 67 | { 68 | Func: "github.com/ztrue/tracerr_test.addFrameC", 69 | Line: 17, 70 | Path: "/tracerr/error_helper_test.go", 71 | }, 72 | { 73 | Func: "github.com/ztrue/tracerr_test.addFrameB", 74 | Line: 13, 75 | Path: "/tracerr/error_helper_test.go", 76 | }, 77 | { 78 | Func: "github.com/ztrue/tracerr_test.addFrameA", 79 | Line: 9, 80 | Path: "/tracerr/error_helper_test.go", 81 | }, 82 | { 83 | Func: "github.com/ztrue/tracerr_test.TestError", 84 | Line: 64, 85 | Path: "/tracerr/error_test.go", 86 | }, 87 | }, 88 | }, 89 | { 90 | Error: tracerr.Wrap(addFrameA("error wrapped twice")), 91 | ExpectedMessage: "error wrapped twice", 92 | ExpectedStackTrace: []tracerr.Frame{ 93 | { 94 | Func: "github.com/ztrue/tracerr_test.addFrameC", 95 | Line: 17, 96 | Path: "/tracerr/error_helper_test.go", 97 | }, 98 | { 99 | Func: "github.com/ztrue/tracerr_test.addFrameB", 100 | Line: 13, 101 | Path: "/tracerr/error_helper_test.go", 102 | }, 103 | { 104 | Func: "github.com/ztrue/tracerr_test.addFrameA", 105 | Line: 9, 106 | Path: "/tracerr/error_helper_test.go", 107 | }, 108 | { 109 | Func: "github.com/ztrue/tracerr_test.TestError", 110 | Line: 90, 111 | Path: "/tracerr/error_test.go", 112 | }, 113 | }, 114 | }, 115 | } 116 | 117 | for i, c := range cases { 118 | if c.Error == nil { 119 | if c.ExpectedMessage != "" { 120 | t.Errorf( 121 | "cases[%#v].Error = nil; want %#v", 122 | i, c.ExpectedMessage, 123 | ) 124 | } 125 | } else if c.Error.Error() != c.ExpectedMessage { 126 | t.Errorf( 127 | "cases[%#v].Error.Error() = %#v; want %#v", 128 | i, c.Error.Error(), c.ExpectedMessage, 129 | ) 130 | } 131 | 132 | if c.ExpectedStackTrace == nil { 133 | if c.Error != nil && c.Error.StackTrace() != nil { 134 | t.Errorf( 135 | "cases[%#v].Error.StackTrace() = %#v; want %#v", 136 | i, c.Error.StackTrace(), nil, 137 | ) 138 | } 139 | if tracerr.StackTrace(c.Error) != nil { 140 | t.Errorf( 141 | "tracerr.StackTrace(cases[%#v].Error) = %#v; want %#v", 142 | i, tracerr.StackTrace(c.Error), nil, 143 | ) 144 | } 145 | continue 146 | } 147 | 148 | frames1 := c.Error.StackTrace() 149 | frames2 := tracerr.StackTrace(c.Error) 150 | for k, frames := range [][]tracerr.Frame{frames1, frames2} { 151 | // Different failing message, depend on stack trace method. 152 | var pattern string 153 | if k == 0 { 154 | pattern = "cases[%#v].Error.StackTrace()" 155 | } else { 156 | pattern = "tracerr.StackTrace(cases[%#v].Error)" 157 | } 158 | prefix := fmt.Sprintf(pattern, i) 159 | // There must be at least two frames of test runner. 160 | expectedMinLen := len(c.ExpectedStackTrace) + 2 161 | if len(frames) < expectedMinLen { 162 | t.Errorf( 163 | "len(%s) = %#v; want >= %#v", 164 | prefix, len(frames), expectedMinLen, 165 | ) 166 | } 167 | for j, expectedFrame := range c.ExpectedStackTrace { 168 | if frames[j].Func != expectedFrame.Func { 169 | t.Errorf( 170 | "%s[%#v].Func = %#v; want %#v", 171 | prefix, j, frames[j].Func, expectedFrame.Func, 172 | ) 173 | } 174 | if frames[j].Line != expectedFrame.Line { 175 | t.Errorf( 176 | "%s[%#v].Line = %#v; want %#v", 177 | prefix, j, frames[j].Line, expectedFrame.Line, 178 | ) 179 | } 180 | if !strings.HasSuffix(frames[j].Path, expectedFrame.Path) { 181 | t.Errorf( 182 | "%s[%#v].Path = %#v; want to has suffix %#v", 183 | prefix, j, frames[j].Path, expectedFrame.Path, 184 | ) 185 | } 186 | } 187 | } 188 | 189 | } 190 | } 191 | 192 | func TestCustomError(t *testing.T) { 193 | err := errors.New("some error") 194 | frames := []tracerr.Frame{ 195 | { 196 | Func: "main.foo", 197 | Line: 42, 198 | Path: "/src/github.com/john/doe/foobar.go", 199 | }, 200 | { 201 | Func: "main.bar", 202 | Line: 43, 203 | Path: "/src/github.com/john/doe/bazqux.go", 204 | }, 205 | } 206 | customErr := tracerr.CustomError(err, frames) 207 | message := customErr.Error() 208 | if message != err.Error() { 209 | t.Errorf( 210 | "customErr.Error() = %#v; want %#v", 211 | message, err.Error(), 212 | ) 213 | } 214 | unwrapped := customErr.Unwrap() 215 | if unwrapped != err { 216 | t.Errorf( 217 | "customErr.Unwrap() = %#v; want %#v", 218 | unwrapped, err, 219 | ) 220 | } 221 | stackTrace := customErr.StackTrace() 222 | if len(stackTrace) != len(frames) { 223 | t.Errorf( 224 | "len(customErr.StackTrace()) = %#v; want %#v", 225 | len(stackTrace), len(frames), 226 | ) 227 | } 228 | for i, frame := range frames { 229 | if stackTrace[i] != frame { 230 | t.Errorf( 231 | "customErr.StackTrace()[%#v] = %#v; want %#v", 232 | i, stackTrace[i], frame, 233 | ) 234 | } 235 | } 236 | } 237 | 238 | func TestErrorNil(t *testing.T) { 239 | wrapped := wrapError(nil) 240 | if wrapped != nil { 241 | t.Errorf( 242 | "wrapped = %#v; want nil", 243 | wrapped, 244 | ) 245 | } 246 | } 247 | 248 | func TestFrameString(t *testing.T) { 249 | frame := tracerr.Frame{ 250 | Func: "main.read", 251 | Line: 1337, 252 | Path: "/src/github.com/john/doe/foobar.go", 253 | } 254 | expected := "/src/github.com/john/doe/foobar.go:1337 main.read()" 255 | if frame.String() != expected { 256 | t.Errorf( 257 | "frame.String() = %#v; want %#v", 258 | frame.String(), expected, 259 | ) 260 | } 261 | } 262 | 263 | func TestStackTraceNotInstance(t *testing.T) { 264 | err := errors.New("regular error") 265 | if tracerr.StackTrace(err) != nil { 266 | t.Errorf( 267 | "tracerr.StackTrace(%#v) = %#v; want %#v", 268 | err, tracerr.StackTrace(err), nil, 269 | ) 270 | } 271 | } 272 | 273 | type UnwrapTestCase struct { 274 | Error error 275 | Wrap bool 276 | } 277 | 278 | func TestUnwrap(t *testing.T) { 279 | cases := []UnwrapTestCase{ 280 | { 281 | Error: nil, 282 | }, 283 | { 284 | Error: fmt.Errorf("some error #%d", 9), 285 | Wrap: false, 286 | }, 287 | { 288 | Error: fmt.Errorf("some error #%d", 9), 289 | Wrap: true, 290 | }, 291 | } 292 | 293 | for i, c := range cases { 294 | err := c.Error 295 | if c.Wrap { 296 | err = tracerr.Wrap(err) 297 | } 298 | unwrappedError := tracerr.Unwrap(err) 299 | if unwrappedError != c.Error { 300 | t.Errorf( 301 | "tracerr.Unwrap(cases[%#v].Error) = %#v; want %#v", 302 | i, unwrappedError, c.Error, 303 | ) 304 | } 305 | } 306 | } 307 | 308 | func wrapError(err error) error { 309 | return tracerr.Wrap(err) 310 | } 311 | -------------------------------------------------------------------------------- /examples/existing_error.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/ztrue/tracerr" 7 | ) 8 | 9 | func main() { 10 | if err := read(); err != nil { 11 | tracerr.PrintSourceColor(err) 12 | } 13 | } 14 | 15 | func read() error { 16 | return readNonExistent() 17 | } 18 | 19 | func readNonExistent() error { 20 | _, err := os.ReadFile("/tmp/non_existent_file") 21 | // Add stack trace to existing error, no matter if it's nil. 22 | return tracerr.Wrap(err) 23 | } 24 | -------------------------------------------------------------------------------- /examples/new_error.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/ztrue/tracerr" 5 | ) 6 | 7 | func main() { 8 | if err := foo(); err != nil { 9 | tracerr.PrintSourceColor(err) 10 | } 11 | } 12 | 13 | func foo() error { 14 | return bar(0) 15 | } 16 | 17 | func bar(i int) error { 18 | if i >= 2 { 19 | // Create new error with stack trace. 20 | return tracerr.Errorf("i = %d", i) 21 | } 22 | return bar(i + 1) 23 | } 24 | -------------------------------------------------------------------------------- /examples/nil_error.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ztrue/tracerr" 7 | ) 8 | 9 | func main() { 10 | if err := nilError(); err != nil { 11 | tracerr.PrintSourceColor(err) 12 | } else { 13 | fmt.Println("no error") 14 | } 15 | } 16 | 17 | func nilError() error { 18 | return tracerr.Wrap(nil) 19 | } 20 | -------------------------------------------------------------------------------- /examples/save_log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/ztrue/tracerr" 7 | ) 8 | 9 | func main() { 10 | if err := read(); err != nil { 11 | // Save output to variable. 12 | text := tracerr.SprintSource(err) 13 | os.WriteFile("/tmp/tracerr.log", []byte(text), 0644) 14 | } 15 | } 16 | 17 | func read() error { 18 | return readNonExistent() 19 | } 20 | 21 | func readNonExistent() error { 22 | _, err := os.ReadFile("/tmp/non_existent_file") 23 | return tracerr.Wrap(err) 24 | } 25 | -------------------------------------------------------------------------------- /examples/stack_trace.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/ztrue/tracerr" 8 | ) 9 | 10 | func main() { 11 | if err := read(); err != nil { 12 | // Dump raw stack trace. 13 | frames := tracerr.StackTrace(err) 14 | fmt.Printf("%#v\n", frames) 15 | } 16 | } 17 | 18 | func read() error { 19 | return readNonExistent() 20 | } 21 | 22 | func readNonExistent() error { 23 | _, err := os.ReadFile("/tmp/non_existent_file") 24 | return tracerr.Wrap(err) 25 | } 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ztrue/tracerr 2 | -------------------------------------------------------------------------------- /output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ztrue/tracerr/9b614df93bcb8a1246a1eb0913a0567412ad0425/output.png -------------------------------------------------------------------------------- /print.go: -------------------------------------------------------------------------------- 1 | package tracerr 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | // DefaultLinesAfter is number of source lines after traced line to display. 12 | var DefaultLinesAfter = 2 13 | 14 | // DefaultLinesBefore is number of source lines before traced line to display. 15 | var DefaultLinesBefore = 3 16 | 17 | var cache = map[string][]string{} 18 | 19 | var mutex sync.RWMutex 20 | 21 | // Print prints error message with stack trace. 22 | func Print(err error) { 23 | fmt.Println(Sprint(err)) 24 | } 25 | 26 | // PrintSource prints error message with stack trace and source fragments. 27 | // 28 | // By default, 6 lines of source code will be printed, 29 | // see DefaultLinesAfter and DefaultLinesBefore. 30 | // 31 | // Pass a single number to specify a total number of source lines. 32 | // 33 | // Pass two numbers to specify exactly how many lines should be shown 34 | // before and after traced line. 35 | func PrintSource(err error, nums ...int) { 36 | fmt.Println(SprintSource(err, nums...)) 37 | } 38 | 39 | // PrintSourceColor prints error message with stack trace and source fragments, 40 | // which are in color. 41 | // Output rules are the same as in PrintSource. 42 | func PrintSourceColor(err error, nums ...int) { 43 | fmt.Println(SprintSourceColor(err, nums...)) 44 | } 45 | 46 | // Sprint returns error output by the same rules as Print. 47 | func Sprint(err error) string { 48 | return sprint(err, []int{0}, false) 49 | } 50 | 51 | // SprintSource returns error output by the same rules as PrintSource. 52 | func SprintSource(err error, nums ...int) string { 53 | return sprint(err, nums, false) 54 | } 55 | 56 | // SprintSourceColor returns error output by the same rules as PrintSourceColor. 57 | func SprintSourceColor(err error, nums ...int) string { 58 | return sprint(err, nums, true) 59 | } 60 | 61 | func calcRows(nums []int) (before, after int, withSource bool) { 62 | before = DefaultLinesBefore 63 | after = DefaultLinesAfter 64 | withSource = true 65 | if len(nums) > 1 { 66 | before = nums[0] 67 | after = nums[1] 68 | withSource = true 69 | } else if len(nums) == 1 { 70 | if nums[0] > 0 { 71 | // Extra line goes to "before" rather than "after". 72 | after = (nums[0] - 1) / 2 73 | before = nums[0] - after - 1 74 | } else { 75 | after = 0 76 | before = 0 77 | withSource = false 78 | } 79 | } 80 | if before < 0 { 81 | before = 0 82 | } 83 | if after < 0 { 84 | after = 0 85 | } 86 | return before, after, withSource 87 | } 88 | 89 | func readLines(path string) ([]string, error) { 90 | mutex.RLock() 91 | lines, ok := cache[path] 92 | mutex.RUnlock() 93 | if ok { 94 | return lines, nil 95 | } 96 | 97 | b, err := os.ReadFile(path) 98 | if err != nil { 99 | return nil, fmt.Errorf("tracerr: file %s not found", path) 100 | } 101 | lines = strings.Split(string(b), "\n") 102 | mutex.Lock() 103 | defer mutex.Unlock() 104 | cache[path] = lines 105 | return lines, nil 106 | } 107 | 108 | func sourceRows(rows []string, frame Frame, before, after int, colorized bool) []string { 109 | lines, err := readLines(frame.Path) 110 | if err != nil { 111 | message := err.Error() 112 | if colorized { 113 | message = yellow(message) 114 | } 115 | return append(rows, message, "") 116 | } 117 | if len(lines) < frame.Line { 118 | message := fmt.Sprintf( 119 | "tracerr: too few lines, got %d, want %d", 120 | len(lines), frame.Line, 121 | ) 122 | if colorized { 123 | message = yellow(message) 124 | } 125 | return append(rows, message, "") 126 | } 127 | current := frame.Line - 1 128 | start := current - before 129 | end := current + after 130 | for i := start; i <= end; i++ { 131 | if i < 0 || i >= len(lines) { 132 | continue 133 | } 134 | line := lines[i] 135 | var message string 136 | // TODO Pad to the same length. 137 | if i == frame.Line-1 { 138 | message = fmt.Sprintf("%d\t%s", i+1, line) 139 | if colorized { 140 | message = red(message) 141 | } 142 | } else if colorized { 143 | message = fmt.Sprintf("%s\t%s", black(strconv.Itoa(i+1)), line) 144 | } else { 145 | message = fmt.Sprintf("%d\t%s", i+1, line) 146 | } 147 | rows = append(rows, message) 148 | } 149 | return append(rows, "") 150 | } 151 | 152 | func sprint(err error, nums []int, colorized bool) string { 153 | if err == nil { 154 | return "" 155 | } 156 | e, ok := err.(Error) 157 | if !ok { 158 | return err.Error() 159 | } 160 | before, after, withSource := calcRows(nums) 161 | frames := e.StackTrace() 162 | expectedRows := len(frames) + 1 163 | if withSource { 164 | expectedRows = (before+after+3)*len(frames) + 2 165 | } 166 | rows := make([]string, 0, expectedRows) 167 | rows = append(rows, e.Error()) 168 | if withSource { 169 | rows = append(rows, "") 170 | } 171 | for _, frame := range frames { 172 | message := frame.String() 173 | if colorized { 174 | message = bold(message) 175 | } 176 | rows = append(rows, message) 177 | if withSource { 178 | rows = sourceRows(rows, frame, before, after, colorized) 179 | } 180 | } 181 | return strings.Join(rows, "\n") 182 | } 183 | -------------------------------------------------------------------------------- /print_test.go: -------------------------------------------------------------------------------- 1 | package tracerr_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | "regexp" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/ztrue/tracerr" 14 | ) 15 | 16 | // PrintTestCase 17 | type PrintTestCase struct { 18 | Output string 19 | Printer func() 20 | ExpectedRows []string 21 | ExpectedMinExtraRows int 22 | } 23 | 24 | func TestPrint(t *testing.T) { 25 | message := "runtime error: index out of range" 26 | err := addFrameA(message) 27 | 28 | cases := []PrintTestCase{ 29 | { 30 | Output: tracerr.Sprint(nil), 31 | Printer: func() { 32 | tracerr.Print(nil) 33 | }, 34 | ExpectedRows: []string{ 35 | "", 36 | }, 37 | ExpectedMinExtraRows: 0, 38 | }, 39 | { 40 | Output: tracerr.Sprint(errors.New("regular error")), 41 | Printer: func() { 42 | tracerr.Print(errors.New("regular error")) 43 | }, 44 | ExpectedRows: []string{ 45 | "regular error", 46 | }, 47 | ExpectedMinExtraRows: 0, 48 | }, 49 | { 50 | Output: tracerr.Sprint(err), 51 | Printer: func() { 52 | tracerr.Print(err) 53 | }, 54 | ExpectedRows: []string{ 55 | message, 56 | "/tracerr/error_helper_test.go:17 github.com/ztrue/tracerr_test.addFrameC()", 57 | "/tracerr/error_helper_test.go:13 github.com/ztrue/tracerr_test.addFrameB()", 58 | "/tracerr/error_helper_test.go:9 github.com/ztrue/tracerr_test.addFrameA()", 59 | "/tracerr/print_test.go:26 github.com/ztrue/tracerr_test.TestPrint()", 60 | }, 61 | ExpectedMinExtraRows: 2, 62 | }, 63 | { 64 | Output: tracerr.SprintSource(err), 65 | Printer: func() { 66 | tracerr.PrintSource(err) 67 | }, 68 | ExpectedRows: []string{ 69 | message, 70 | "", 71 | "/tracerr/error_helper_test.go:17 github.com/ztrue/tracerr_test.addFrameC()", 72 | "14\t}", 73 | "15\t", 74 | "16\tfunc addFrameC(message string) error {", 75 | "17\t\treturn tracerr.New(message)", 76 | "18\t}", 77 | "19\t", 78 | "", 79 | "/tracerr/error_helper_test.go:13 github.com/ztrue/tracerr_test.addFrameB()", 80 | "10\t}", 81 | "11\t", 82 | "12\tfunc addFrameB(message string) error {", 83 | "13\t\treturn addFrameC(message)", 84 | "14\t}", 85 | "15\t", 86 | "", 87 | "/tracerr/error_helper_test.go:9 github.com/ztrue/tracerr_test.addFrameA()", 88 | "6\t)", 89 | "7\t", 90 | "8\tfunc addFrameA(message string) error {", 91 | "9\t\treturn addFrameB(message)", 92 | "10\t}", 93 | "11\t", 94 | "", 95 | "/tracerr/print_test.go:26 github.com/ztrue/tracerr_test.TestPrint()", 96 | "23\t", 97 | "24\tfunc TestPrint(t *testing.T) {", 98 | "25\t\tmessage := \"runtime error: index out of range\"", 99 | "26\t\terr := addFrameA(message)", 100 | "27\t", 101 | "28\t\tcases := []PrintTestCase{", 102 | "", 103 | }, 104 | ExpectedMinExtraRows: 2, 105 | }, 106 | { 107 | Output: tracerr.SprintSource(err, 2, 1), 108 | Printer: func() { 109 | tracerr.PrintSource(err, 2, 1) 110 | }, 111 | ExpectedRows: []string{ 112 | message, 113 | "", 114 | "/tracerr/error_helper_test.go:17 github.com/ztrue/tracerr_test.addFrameC()", 115 | "15\t", 116 | "16\tfunc addFrameC(message string) error {", 117 | "17\t\treturn tracerr.New(message)", 118 | "18\t}", 119 | "", 120 | "/tracerr/error_helper_test.go:13 github.com/ztrue/tracerr_test.addFrameB()", 121 | "11\t", 122 | "12\tfunc addFrameB(message string) error {", 123 | "13\t\treturn addFrameC(message)", 124 | "14\t}", 125 | "", 126 | "/tracerr/error_helper_test.go:9 github.com/ztrue/tracerr_test.addFrameA()", 127 | "7\t", 128 | "8\tfunc addFrameA(message string) error {", 129 | "9\t\treturn addFrameB(message)", 130 | "10\t}", 131 | "", 132 | "/tracerr/print_test.go:26 github.com/ztrue/tracerr_test.TestPrint()", 133 | "24\tfunc TestPrint(t *testing.T) {", 134 | "25\t\tmessage := \"runtime error: index out of range\"", 135 | "26\t\terr := addFrameA(message)", 136 | "27\t", 137 | "", 138 | }, 139 | ExpectedMinExtraRows: 2, 140 | }, 141 | { 142 | Output: tracerr.SprintSource(err, 4), 143 | Printer: func() { 144 | tracerr.PrintSource(err, 4) 145 | }, 146 | ExpectedRows: []string{ 147 | message, 148 | "", 149 | "/tracerr/error_helper_test.go:17 github.com/ztrue/tracerr_test.addFrameC()", 150 | "15\t", 151 | "16\tfunc addFrameC(message string) error {", 152 | "17\t\treturn tracerr.New(message)", 153 | "18\t}", 154 | "", 155 | "/tracerr/error_helper_test.go:13 github.com/ztrue/tracerr_test.addFrameB()", 156 | "11\t", 157 | "12\tfunc addFrameB(message string) error {", 158 | "13\t\treturn addFrameC(message)", 159 | "14\t}", 160 | "", 161 | "/tracerr/error_helper_test.go:9 github.com/ztrue/tracerr_test.addFrameA()", 162 | "7\t", 163 | "8\tfunc addFrameA(message string) error {", 164 | "9\t\treturn addFrameB(message)", 165 | "10\t}", 166 | "", 167 | "/tracerr/print_test.go:26 github.com/ztrue/tracerr_test.TestPrint()", 168 | "24\tfunc TestPrint(t *testing.T) {", 169 | "25\t\tmessage := \"runtime error: index out of range\"", 170 | "26\t\terr := addFrameA(message)", 171 | "27\t", 172 | "", 173 | }, 174 | ExpectedMinExtraRows: 2, 175 | }, 176 | { 177 | Output: tracerr.SprintSource(err, -1, -1), 178 | Printer: func() { 179 | tracerr.PrintSource(err, -1, -1) 180 | }, 181 | ExpectedRows: []string{ 182 | message, 183 | "", 184 | "/tracerr/error_helper_test.go:17 github.com/ztrue/tracerr_test.addFrameC()", 185 | "17\t\treturn tracerr.New(message)", 186 | "", 187 | "/tracerr/error_helper_test.go:13 github.com/ztrue/tracerr_test.addFrameB()", 188 | "13\t\treturn addFrameC(message)", 189 | "", 190 | "/tracerr/error_helper_test.go:9 github.com/ztrue/tracerr_test.addFrameA()", 191 | "9\t\treturn addFrameB(message)", 192 | "", 193 | "/tracerr/print_test.go:26 github.com/ztrue/tracerr_test.TestPrint()", 194 | "26\t\terr := addFrameA(message)", 195 | "", 196 | }, 197 | ExpectedMinExtraRows: 2, 198 | }, 199 | { 200 | Output: tracerr.SprintSource(err, 0, 4), 201 | Printer: func() { 202 | tracerr.PrintSource(err, 0, 4) 203 | }, 204 | ExpectedRows: []string{ 205 | message, 206 | "", 207 | "/tracerr/error_helper_test.go:17 github.com/ztrue/tracerr_test.addFrameC()", 208 | "17\t\treturn tracerr.New(message)", 209 | "18\t}", 210 | "19\t", 211 | "", 212 | "/tracerr/error_helper_test.go:13 github.com/ztrue/tracerr_test.addFrameB()", 213 | "13\t\treturn addFrameC(message)", 214 | "14\t}", 215 | "15\t", 216 | "16\tfunc addFrameC(message string) error {", 217 | "17\t\treturn tracerr.New(message)", 218 | "", 219 | "/tracerr/error_helper_test.go:9 github.com/ztrue/tracerr_test.addFrameA()", 220 | "9\t\treturn addFrameB(message)", 221 | "10\t}", 222 | "11\t", 223 | "12\tfunc addFrameB(message string) error {", 224 | "13\t\treturn addFrameC(message)", 225 | "", 226 | "/tracerr/print_test.go:26 github.com/ztrue/tracerr_test.TestPrint()", 227 | "26\t\terr := addFrameA(message)", 228 | "27\t", 229 | "28\t\tcases := []PrintTestCase{", 230 | "29\t\t\t{", 231 | "30\t\t\t\tOutput: tracerr.Sprint(nil),", 232 | }, 233 | ExpectedMinExtraRows: 2, 234 | }, 235 | { 236 | Output: tracerr.SprintSourceColor(err, 1, 1), 237 | Printer: func() { 238 | tracerr.PrintSourceColor(err, 1, 1) 239 | }, 240 | ExpectedRows: []string{ 241 | message, 242 | "", 243 | bold("/tracerr/error_helper_test.go:17 github.com/ztrue/tracerr_test.addFrameC()"), 244 | black("16") + "\tfunc addFrameC(message string) error {", 245 | red("17\t\treturn tracerr.New(message)"), 246 | black("18") + "\t}", 247 | "", 248 | bold("/tracerr/error_helper_test.go:13 github.com/ztrue/tracerr_test.addFrameB()"), 249 | black("12") + "\tfunc addFrameB(message string) error {", 250 | red("13\t\treturn addFrameC(message)"), 251 | black("14") + "\t}", 252 | "", 253 | bold("/tracerr/error_helper_test.go:9 github.com/ztrue/tracerr_test.addFrameA()"), 254 | black("8") + "\tfunc addFrameA(message string) error {", 255 | red("9\t\treturn addFrameB(message)"), 256 | black("10") + "\t}", 257 | "", 258 | bold("/tracerr/print_test.go:26 github.com/ztrue/tracerr_test.TestPrint()"), 259 | black("25") + "\t\tmessage := \"runtime error: index out of range\"", 260 | red("26\t\terr := addFrameA(message)"), 261 | black("27") + "\t", 262 | "", 263 | }, 264 | ExpectedMinExtraRows: 2, 265 | }, 266 | } 267 | 268 | for i, c := range cases { 269 | assertRows(t, i, c.Output, c.ExpectedRows, c.ExpectedMinExtraRows) 270 | output := captureOutput(c.Printer) 271 | assertRows(t, i, output, c.ExpectedRows, c.ExpectedMinExtraRows) 272 | } 273 | } 274 | 275 | func TestNoLine(t *testing.T) { 276 | err := tracerr.CustomError( 277 | errors.New("some error"), 278 | []tracerr.Frame{ 279 | { 280 | Func: "main.Foo", 281 | Line: 1337, 282 | Path: "error_helper_test.go", 283 | }, 284 | { 285 | Func: "main.Bar", 286 | Line: 1338, 287 | Path: "error_helper_test.go", 288 | }, 289 | }, 290 | ) 291 | output := tracerr.SprintSource(err) 292 | expectedRows := []string{ 293 | "some error", 294 | "", 295 | "error_helper_test.go:1337 main.Foo()", 296 | "tracerr: too few lines, got 19, want 1337", 297 | "", 298 | "error_helper_test.go:1338 main.Bar()", 299 | "tracerr: too few lines, got 19, want 1338", 300 | "", 301 | } 302 | expected := strings.Join(expectedRows, "\n") 303 | if output != expected { 304 | t.Errorf( 305 | "tracerr.SprintSource(err) = %#v; want %#v", 306 | output, expected, 307 | ) 308 | } 309 | } 310 | 311 | func TestNoLineColor(t *testing.T) { 312 | err := tracerr.CustomError( 313 | errors.New("some error"), 314 | []tracerr.Frame{ 315 | { 316 | Func: "main.Foo", 317 | Line: 1337, 318 | Path: "error_helper_test.go", 319 | }, 320 | { 321 | Func: "main.Bar", 322 | Line: 1338, 323 | Path: "error_helper_test.go", 324 | }, 325 | }, 326 | ) 327 | output := tracerr.SprintSourceColor(err) 328 | expectedRows := []string{ 329 | "some error", 330 | "", 331 | bold("error_helper_test.go:1337 main.Foo()"), 332 | yellow("tracerr: too few lines, got 19, want 1337"), 333 | "", 334 | bold("error_helper_test.go:1338 main.Bar()"), 335 | yellow("tracerr: too few lines, got 19, want 1338"), 336 | "", 337 | } 338 | expected := strings.Join(expectedRows, "\n") 339 | if output != expected { 340 | t.Errorf( 341 | "tracerr.SprintSource(err) = %#v; want %#v", 342 | output, expected, 343 | ) 344 | } 345 | } 346 | 347 | func TestNoSourceFile(t *testing.T) { 348 | err := tracerr.CustomError( 349 | errors.New("some error"), 350 | []tracerr.Frame{ 351 | { 352 | Func: "main.Foo", 353 | Line: 42, 354 | Path: "/tmp/not_exists.go", 355 | }, 356 | { 357 | Func: "main.Bar", 358 | Line: 43, 359 | Path: "/tmp/not_exists_2.go", 360 | }, 361 | }, 362 | ) 363 | output := tracerr.SprintSource(err) 364 | expectedRows := []string{ 365 | "some error", 366 | "", 367 | "/tmp/not_exists.go:42 main.Foo()", 368 | "tracerr: file /tmp/not_exists.go not found", 369 | "", 370 | "/tmp/not_exists_2.go:43 main.Bar()", 371 | "tracerr: file /tmp/not_exists_2.go not found", 372 | "", 373 | } 374 | expected := strings.Join(expectedRows, "\n") 375 | if output != expected { 376 | t.Errorf( 377 | "tracerr.SprintSource(err) = %#v; want %#v", 378 | output, expected, 379 | ) 380 | } 381 | } 382 | 383 | func TestNoSourceFileColor(t *testing.T) { 384 | err := tracerr.CustomError( 385 | errors.New("some error"), 386 | []tracerr.Frame{ 387 | { 388 | Func: "main.Foo", 389 | Line: 42, 390 | Path: "/tmp/not_exists.go", 391 | }, 392 | { 393 | Func: "main.Bar", 394 | Line: 43, 395 | Path: "/tmp/not_exists_2.go", 396 | }, 397 | }, 398 | ) 399 | output := tracerr.SprintSourceColor(err) 400 | expectedRows := []string{ 401 | "some error", 402 | "", 403 | bold("/tmp/not_exists.go:42 main.Foo()"), 404 | yellow("tracerr: file /tmp/not_exists.go not found"), 405 | "", 406 | bold("/tmp/not_exists_2.go:43 main.Bar()"), 407 | yellow("tracerr: file /tmp/not_exists_2.go not found"), 408 | "", 409 | } 410 | expected := strings.Join(expectedRows, "\n") 411 | if output != expected { 412 | t.Errorf( 413 | "tracerr.SprintSource(err) = %#v; want %#v", 414 | output, expected, 415 | ) 416 | } 417 | } 418 | 419 | func assertRows(t *testing.T, i int, output string, expectedRows []string, extra int) { 420 | rows := strings.Split(output, "\n") 421 | // There must be at least "extra" frames of test runner. 422 | expectedMinLen := len(expectedRows) + extra 423 | if len(rows) < expectedMinLen { 424 | t.Fatalf( 425 | "case #%d: len(rows) = %#v; want >= %#v", 426 | i, len(rows), expectedMinLen, 427 | ) 428 | } 429 | // Remove root path, cause it could be different on different environments. 430 | re := regexp.MustCompile("([^/]*)/.*(/tracerr/.*)") 431 | for j, expectedRow := range expectedRows { 432 | row := re.ReplaceAllString(rows[j], "$1$2") 433 | if row != expectedRow { 434 | t.Errorf( 435 | "case #%d: rows[%#v] = %#v; want %#v", 436 | i, j, row, expectedRow, 437 | ) 438 | } 439 | } 440 | } 441 | 442 | func captureOutput(fn func()) string { 443 | r, w, err := os.Pipe() 444 | if err != nil { 445 | panic(err.Error()) 446 | } 447 | stdout := os.Stdout 448 | os.Stdout = w 449 | fn() 450 | w.Close() 451 | os.Stdout = stdout 452 | var buf bytes.Buffer 453 | io.Copy(&buf, r) 454 | return buf.String() 455 | } 456 | 457 | func bold(in string) string { 458 | return fmt.Sprintf("\x1b[1m%s\x1b[0m", in) 459 | } 460 | 461 | func black(in string) string { 462 | return fmt.Sprintf("\x1b[30m%s\x1b[0m", in) 463 | } 464 | 465 | func red(in string) string { 466 | return fmt.Sprintf("\x1b[31m%s\x1b[0m", in) 467 | } 468 | 469 | func yellow(in string) string { 470 | return fmt.Sprintf("\x1b[33m%s\x1b[0m", in) 471 | } 472 | --------------------------------------------------------------------------------