From 37e2e39ff8355a8079b0253ba35980b2404fa235 Mon Sep 17 00:00:00 2001 From: Pavel Shevaev Date: Wed, 26 Oct 2022 10:52:57 +0300 Subject: [PATCH] First commit --- .gitignore | 24 +++ LICENSE | 26 +++ Makefile | 21 ++ README.md | 102 +++++++++ appveyor.yml | 33 +++ bench_test.go | 110 ++++++++++ errors.go | 368 +++++++++++++++++++++++++++++++ errors_test.go | 273 +++++++++++++++++++++++ example_test.go | 205 ++++++++++++++++++ format_test.go | 560 ++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 3 + json_test.go | 51 +++++ stack.go | 198 +++++++++++++++++ stack_test.go | 250 +++++++++++++++++++++ 14 files changed, 2224 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 appveyor.yml create mode 100644 bench_test.go create mode 100644 errors.go create mode 100644 errors_test.go create mode 100644 example_test.go create mode 100644 format_test.go create mode 100644 go.mod create mode 100644 json_test.go create mode 100644 stack.go create mode 100644 stack_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..daf913b --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..45fd3d1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2015, Dave Cheney +All rights reserved. + +Modifications of the code made by by Friends of Go +Copyright (c) 2019, Friends of Go + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..226c4f9 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +# general +WORKDIR = $(PWD) + +# coverage +COVERAGE_REPORT = coverage.txt +COVERAGE_PROFILE = profile.out +COVERAGE_MODE = atomic + +coverage: + @cd $(WORKDIR); \ + echo "" > $(COVERAGE_REPORT); \ + for dir in `find . -name "*.go" | grep -o '.*/' | sort | uniq`; do \ + go test -v -race $$dir -coverprofile=$(COVERAGE_PROFILE) -covermode=$(COVERAGE_MODE); \ + if [ $$? != 0 ]; then \ + exit 2; \ + fi; \ + if [ -f $(COVERAGE_PROFILE) ]; then \ + cat $(COVERAGE_PROFILE) >> $(COVERAGE_REPORT); \ + rm $(COVERAGE_PROFILE); \ + fi; \ + done; \ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c0de9cd --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +[![FriendsOfGo](https://img.shields.io/badge/powered%20by-Friends%20of%20Go-73D7E2.svg)](https://friendsofgo.tech) +[![CircleCI](https://circleci.com/gh/friendsofgo/errors.svg?style=svg)](https://circleci.com/gh/friendsofgo/errors) +[![Build status](https://ci.appveyor.com/api/projects/status/phjkr6de4mnb19kq?svg=true)](https://ci.appveyor.com/project/aperezg/errors) +[![Version](https://img.shields.io/github/release/friendsofgo/errors.svg?style=flat-square)](https://github.com/friendsofgo/errors/releases/latest) +[![Go Report Card](https://goreportcard.com/badge/github.com/friendsofgo/errors)](https://goreportcard.com/report/github.com/friendsofgo/errors) +[![GoDoc](https://godoc.org/github.com/friendsofgo/errors?status.svg)](https://godoc.org/github.com/friendsofgo/errors) + +# errors + +This package is a fork from [github.com/pkg/errors](https://github.com/pkg/errors) package created by +[Dave Cheney](https://github.com/davecheney). The original package has no longer accepting proposals for new functionality. + +With the new errors on [go 1.13](https://godoc.org/errors), the way to using the errors on Go has some +changes that can be applied into Dave Cheney library. We want to offer one way to migrate your code to new +errors, but with the minimum refactor, for that we've created this package. + +This package provide the same interface that the original library have, but using new [go 1.13](https://godoc.org/errors) +errors, or in previous version [golang.org/x/xerrors](https://golang.org/x/xerrors) package. + +## How to start using friendsofgo/errors + +If you previously was using the package [github.com/pkg/errors](https://github.com/pkg/errors), you only need +change your imports for **github.com/friendsofgo/errors**, with this simple change now you're capable to use +[go 1.13](https://godoc.org/errors) in your code, and use the new methods `As` and `Is` if you want. + +Furthermore the method `Wrap` `Wrapf become compatible with `Unwrap` interface of new [go 1.13](https://godoc.org/errors) errors. + +## Adding context to an error + +With the original package [go 1.13](https://godoc.org/errors) if you want add context, ergo wrap your error you need to create +a new error and using the new verb `"%w" like that: + +```go +_, err := ioutil.ReadAll(r) +if err != nil { + return fmt.Errorf("read failed: %w", err) +} +``` + +Using our library you can do that forgetting to the new verb: + +```go +_, err := ioutil.ReadAll(r) +if err != nil { + return errors.Wrap(err, "read failed") +} +``` + +## Retrieving the cause of an error + +We want to keep the compatibility with the [github.com/pkg/errors](https://github.com/pkg/errors) package, for that +our package provides a `Cause` method, but this method is not longer needed, because we can use the new methods `Is` or `As` +that provides the official package. + +So previously if you needed to check an error cause, your error must be implemented the `causer` inteface: + +```go +type causer interface { + Cause() error +} +``` + +`errors.Cause` will recursively retrieve the topmost error which does not implement causer, which is assumed to be the original cause. For example: + +```go +switch err := errors.Cause(err).(type) { +case *MyError: + // handle specifically +default: + // unknown error +} +``` + +But now you can do: + +```go +var target *MyError +if errors.As(err, &target) { + // handle specifically +} else { + // unknown error +} +``` + +Or if you uses a sentinel error: + +```go +var ErrMyError = errors.New("my sentinel error") +if errors.Is(err, ErrMyError) { + // handle specifically +} else { + // unknown error +} +``` + +## Disclaimer +This package was created to using with go 1.13 version however if you uses this package with a previous version, the methods +`As`, `Is`, `Wrap` and `Wrapf` will be using [golang.org/x/xerrors](https://golang.org/x/xerrors) package. + +## Contributing + +[Contributions](https://github.com/friendsofgo/errors/issues?q=is%3Aissue+is%3Aopen) are more than welcome, if you are interested please fork this repo and send your Pull Request. diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..251f3f7 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,33 @@ +version: build-{build}.{branch} + +clone_folder: C:\gopath\src\github.com\friendsofgo\errors +shallow_clone: true # for startup speed + +environment: + GOPATH: C:\gopath + GO111MODULE: on + +stack: go 1.13 + +platform: + - x64 + +# http://www.appveyor.com/docs/installed-software +install: + - go version + - go env + - set PATH=C:\msys64\mingw64\bin;%GOPATH%\bin;c:\go\bin;%PATH% + - go mod tidy + - gcc --version + - g++ --version + +build_script: + - go install -v ./... + +test_script: + - set PATH=C:\gopath\bin;%PATH% + - go test -v ./... + +#artifacts: +# - path: '%GOPATH%\bin\*.exe' +deploy: off diff --git a/bench_test.go b/bench_test.go new file mode 100644 index 0000000..6a2c8cd --- /dev/null +++ b/bench_test.go @@ -0,0 +1,110 @@ +// +build go1.7 + +package errors + +import ( + "fmt" + "testing" + + stderrors "errors" +) + +func noErrors(at, depth int) error { + if at >= depth { + return stderrors.New("no error") + } + return noErrors(at+1, depth) +} + +func yesErrors(at, depth int) error { + if at >= depth { + return New("ye error") + } + return yesErrors(at+1, depth) +} + +// GlobalE is an exported global to store the result of benchmark results, +// preventing the compiler from optimising the benchmark functions away. +var GlobalE interface{} + +func BenchmarkErrors(b *testing.B) { + type run struct { + stack int + std bool + } + runs := []run{ + {10, false}, + {10, true}, + {100, false}, + {100, true}, + {1000, false}, + {1000, true}, + } + for _, r := range runs { + part := "friendsofgo/errors" + if r.std { + part = "errors" + } + name := fmt.Sprintf("%s-stack-%d", part, r.stack) + b.Run(name, func(b *testing.B) { + var err error + f := yesErrors + if r.std { + f = noErrors + } + b.ReportAllocs() + for i := 0; i < b.N; i++ { + err = f(0, r.stack) + } + b.StopTimer() + GlobalE = err + }) + } +} + +func BenchmarkStackFormatting(b *testing.B) { + type run struct { + stack int + format string + } + runs := []run{ + {10, "%s"}, + {10, "%v"}, + {10, "%+v"}, + {30, "%s"}, + {30, "%v"}, + {30, "%+v"}, + {60, "%s"}, + {60, "%v"}, + {60, "%+v"}, + } + + var stackStr string + for _, r := range runs { + name := fmt.Sprintf("%s-stack-%d", r.format, r.stack) + b.Run(name, func(b *testing.B) { + err := yesErrors(0, r.stack) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + stackStr = fmt.Sprintf(r.format, err) + } + b.StopTimer() + }) + } + + for _, r := range runs { + name := fmt.Sprintf("%s-stacktrace-%d", r.format, r.stack) + b.Run(name, func(b *testing.B) { + err := yesErrors(0, r.stack) + st := err.(*fundamental).stack.StackTrace() + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + stackStr = fmt.Sprintf(r.format, st) + } + b.StopTimer() + }) + } + GlobalE = stackStr +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..08be5b5 --- /dev/null +++ b/errors.go @@ -0,0 +1,368 @@ +package errors + +// Original package created by Dave Cheney +// Copyright (c) 2015, Dave Cheney +// +// Modifications of the original package by Friends of Go +// Copyright (c) 2019, Friends of Go +// +// Package errors provides simple error handling primitives. +// +// The traditional error handling idiom in Go is roughly akin to +// +// if err != nil { +// return err +// } +// +// which when applied recursively up the call stack results in error reports +// without context or debugging information. The errors package allows +// programmers to add context to the failure path in their code in a way +// that does not destroy the original value of the error. +// +// Adding context to an error +// +// The errors.Wrap function returns a new error that adds context to the +// original error by recording a stack trace at the point Wrap is called, +// together with the supplied message. For example +// +// _, err := ioutil.ReadAll(r) +// if err != nil { +// return errors.Wrap(err, "read failed") +// } +// +// If additional control is required, the errors.WithStack and +// errors.WithMessage functions destructure errors.Wrap into its component +// operations: annotating an error with a stack trace and with a message, +// respectively. +// +// Retrieving the cause of an error +// +// Using errors.Wrap constructs a stack of errors, adding context to the +// preceding error. Depending on the nature of the error it may be necessary +// to reverse the operation of errors.Wrap to retrieve the original error +// for inspection. Any error value which implements this interface +// +// type causer interface { +// Cause() error +// } +// +// can be inspected by errors.Cause. errors.Cause will recursively retrieve +// the topmost error that does not implement causer, which is assumed to be +// the original cause. For example: +// +// switch err := errors.Cause(err).(type) { +// case *MyError: +// // handle specifically +// default: +// // unknown error +// } +// +// Although the causer interface is not exported by this package, it is +// considered a part of its stable public interface. +// +// With the new standard package error we have two new ways to figure what is the cause of +// our error: +// +// var target *MyError +// if errors.As(err, &target) { +// // handle specifically +// } else { +// // unknown error +// } +// +// or even with sentinel errors: +// +// var ErrMyError = errors.New("my sentinel error") +// if errors.Is(err, ErrMyError) { +// // handle specifically +// } else { +// // unknown error +// } +// +// Formatted printing of errors +// +// All error values returned from this package implement fmt.Formatter and can +// be formatted by the fmt package. The following verbs are supported: +// +// %s print the error. If the error has a Cause it will be +// printed recursively. +// %v see %s +// %+v extended format. Each Frame of the error's StackTrace will +// be printed in detail. +// +// Retrieving the stack trace of an error or wrapper +// +// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are +// invoked. This information can be retrieved with the following interface: +// +// type stackTracer interface { +// StackTrace() errors.StackTrace +// } +// +// The returned errors.StackTrace type is defined as +// +// type StackTrace []Frame +// +// The Frame type represents a call site in the stack trace. Frame supports +// the fmt.Formatter interface that can be used for printing information about +// the stack trace of this error. For example: +// +// if err, ok := err.(stackTracer); ok { +// for _, f := range err.StackTrace() { +// fmt.Printf("%+s:%d\n", f, f) +// } +// } +// +// Although the stackTracer interface is not exported by this package, it is +// considered a part of its stable public interface. +// +// See the documentation for Frame.Format for more details. + +import ( + "errors" + "fmt" + "io" + "strings" +) + +// New returns an error with the supplied message. +// New also records the stack trace at the point it was called. +func New(message string) error { + return &fundamental{ + msg: message, + stack: callers(), + } +} + +// Errorf formats according to a format specifier and returns the string +// as a value that satisfies error. +// Errorf also records the stack trace at the point it was called. +func Errorf(format string, args ...interface{}) error { + return &fundamental{ + msg: fmt.Sprintf(format, args...), + stack: callers(), + } +} + +// fundamental is an error that has a message and a stack, but no caller. +type fundamental struct { + msg string + *stack +} + +func (f *fundamental) Error() string { return f.msg } + +func (f *fundamental) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + io.WriteString(s, f.msg) + f.stack.Format(s, verb) + return + } + if s.Flag('-') { + io.WriteString(s, f.msg) + f.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, f.msg) + case 'q': + fmt.Fprintf(s, "%q", f.msg) + } +} + +// WithStack annotates err with a stack trace at the point WithStack was called. +// If err is nil, WithStack returns nil. +func WithStack(err error) error { + if err == nil { + return nil + } + return &withStack{ + err, + callers(), + } +} + +type withStack struct { + error + *stack +} + +func (w *withStack) Cause() error { return w.error } +func (w *withStack) Unwrap() error { return w.error } + +func (w *withStack) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v", w.Cause()) + w.stack.Format(s, verb) + return + } + if s.Flag('-') { + fmt.Fprintf(s, "%-v", w.Cause()) + w.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, w.Error()) + case 'q': + fmt.Fprintf(s, "%q", w.Error()) + } +} + +// Wrap returns an error annotating err with a stack trace +// at the point Wrap is called, and the supplied message. +// If err is nil, Wrap returns nil. +func Wrap(err error, message string) error { + if err == nil { + return nil + } + err = &withMessage{ + cause: err, + msg: message, + } + return &withStack{ + err, + callers(), + } +} + +// Wrapf returns an error annotating err with a stack trace +// at the point Wrapf is called, and the format specifier. +// If err is nil, Wrapf returns nil. +func Wrapf(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + err = &withMessage{ + cause: err, + msg: fmt.Sprintf(format, args...), + } + return &withStack{ + err, + callers(), + } +} + +// Is reports whether any error in err's chain matches target. +// +// The chain consists of err itself followed by the sequence of errors obtained by +// repeatedly calling Unwrap. +// +// An error is considered to match a target if it is equal to that target or if +// it implements a method Is(error) bool such that Is(target) returns true. +func Is(err error, target error) bool { + return errors.Is(err, target) +} + +// As finds the first error in err's chain that matches target, and if so, sets +// target to that error value and returns true. +// +// The chain consists of err itself followed by the sequence of errors obtained by +// repeatedly calling Unwrap. +// +// An error matches target if the error's concrete value is assignable to the value +// pointed to by target, or if the error has a method As(interface{}) bool such that +// As(target) returns true. In the latter case, the As method is responsible for +// setting target. +// +// As will panic if target is not a non-nil pointer to either a type that implements +// error, or to any interface type. As returns false if err is nil. +func As(err error, target interface{}) bool { + return errors.As(err, target) +} + +// WithMessage annotates err with a new message. +// If err is nil, WithMessage returns nil. +func WithMessage(err error, message string) error { + if err == nil { + return nil + } + return &withMessage{ + cause: err, + msg: message, + } +} + +// WithMessagef annotates err with the format specifier. +// If err is nil, WithMessagef returns nil. +func WithMessagef(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + return &withMessage{ + cause: err, + msg: fmt.Sprintf(format, args...), + } +} + +type withMessage struct { + cause error + msg string +} + +func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() } +func (w *withMessage) Cause() error { return w.cause } +func (w *withMessage) Unwrap() error { return w.cause } + +func (w *withMessage) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v\n", w.Cause()) + io.WriteString(s, w.msg) + return + } + if s.Flag('-') { + fmt.Fprintf(s, "%-v ; ", w.Cause()) + io.WriteString(s, w.msg) + return + } + fallthrough + case 's', 'q': + io.WriteString(s, w.Error()) + } +} + +// Cause returns the underlying cause of the error, if possible. +// An error value has a cause if it implements the following +// interface: +// +// type causer interface { +// Cause() error +// } +// +// If the error does not implement Cause, the original error will +// be returned. If the error is nil, nil will be returned without further +// investigation. +func Cause(err error) error { + type causer interface { + Cause() error + } + + for err != nil { + var c causer + if !As(err, &c) { + break + } + err = c.Cause() + } + return err +} + +func FormatPanicDebugStack(msg string) string { + msg = strings.ReplaceAll(msg, "\n", " < ") + msg = strings.ReplaceAll(msg, ") <", ") at") + idx := strings.Index(msg, "panic.go:") + if idx != -1 { + msg = msg[idx:] + } + if len(msg) > 2000 { + msg = msg[:2000] + "..." + } + return msg +} diff --git a/errors_test.go b/errors_test.go new file mode 100644 index 0000000..6cd419e --- /dev/null +++ b/errors_test.go @@ -0,0 +1,273 @@ +package errors + +import ( + "errors" + stdlib_errors "errors" + "fmt" + "io" + "reflect" + "testing" +) + +func TestNew(t *testing.T) { + tests := []struct { + err string + want error + }{ + {"", fmt.Errorf("")}, + {"foo", fmt.Errorf("foo")}, + {"foo", New("foo")}, + {"string with format specifiers: %v", errors.New("string with format specifiers: %v")}, + } + + for _, tt := range tests { + got := New(tt.err) + if got.Error() != tt.want.Error() { + t.Errorf("New.Error(): got: %q, want %q", got, tt.want) + } + } +} + +func TestWrapNil(t *testing.T) { + got := Wrap(nil, "no error") + if got != nil { + t.Errorf("Wrap(nil, \"no error\"): got %#v, expected nil", got) + } +} + +func TestWrap(t *testing.T) { + tests := []struct { + err error + message string + want string + }{ + {io.EOF, "read error", "read error: EOF"}, + {Wrap(io.EOF, "read error"), "client error", "client error: read error: EOF"}, + } + + for _, tt := range tests { + got := Wrap(tt.err, tt.message).Error() + if got != tt.want { + t.Errorf("Wrap(%v, %q): got: %v, want %v", tt.err, tt.message, got, tt.want) + } + } +} + +type nilError struct{} + +func (nilError) Error() string { return "nil error" } + +func TestCause(t *testing.T) { + x := New("error") + tests := []struct { + err error + want error + }{{ + // nil error is nil + err: nil, + want: nil, + }, { + // explicit nil error is nil + err: (error)(nil), + want: nil, + }, { + // typed nil is nil + err: (*nilError)(nil), + want: (*nilError)(nil), + }, { + // uncaused error is unaffected + err: io.EOF, + want: io.EOF, + }, { + // caused error returns cause + err: Wrap(io.EOF, "ignored"), + want: io.EOF, + }, { + err: x, // return from errors.New + want: x, + }, { + WithMessage(nil, "whoops"), + nil, + }, { + WithMessage(io.EOF, "whoops"), + io.EOF, + }, { + WithStack(nil), + nil, + }, { + WithStack(io.EOF), + io.EOF, + }} + + for i, tt := range tests { + got := Cause(tt.err) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("test %d: got %#v, want %#v", i+1, got, tt.want) + } + } +} + +func TestWrapfNil(t *testing.T) { + got := Wrapf(nil, "no error") + if got != nil { + t.Errorf("Wrapf(nil, \"no error\"): got %#v, expected nil", got) + } +} + +func TestWrapf(t *testing.T) { + tests := []struct { + err error + message string + want string + }{ + {io.EOF, "read error", "read error: EOF"}, + {Wrapf(io.EOF, "read error without format specifiers"), "client error", "client error: read error without format specifiers: EOF"}, + {Wrapf(io.EOF, "read error with %d format specifier", 1), "client error", "client error: read error with 1 format specifier: EOF"}, + } + + for _, tt := range tests { + got := Wrapf(tt.err, tt.message).Error() + if got != tt.want { + t.Errorf("Wrapf(%v, %q): got: %v, want %v", tt.err, tt.message, got, tt.want) + } + } +} + +func TestErrorf(t *testing.T) { + tests := []struct { + err error + want string + }{ + {Errorf("read error without format specifiers"), "read error without format specifiers"}, + {Errorf("read error with %d format specifier", 1), "read error with 1 format specifier"}, + } + + for _, tt := range tests { + got := tt.err.Error() + if got != tt.want { + t.Errorf("Errorf(%v): got: %q, want %q", tt.err, got, tt.want) + } + } +} + +func TestWithStackNil(t *testing.T) { + got := WithStack(nil) + if got != nil { + t.Errorf("WithStack(nil): got %#v, expected nil", got) + } +} + +func TestWithStack(t *testing.T) { + tests := []struct { + err error + want string + }{ + {io.EOF, "EOF"}, + {WithStack(io.EOF), "EOF"}, + } + + for _, tt := range tests { + got := WithStack(tt.err).Error() + if got != tt.want { + t.Errorf("WithStack(%v): got: %v, want %v", tt.err, got, tt.want) + } + } +} + +func TestWithMessageNil(t *testing.T) { + got := WithMessage(nil, "no error") + if got != nil { + t.Errorf("WithMessage(nil, \"no error\"): got %#v, expected nil", got) + } +} + +func TestWithMessage(t *testing.T) { + tests := []struct { + err error + message string + want string + }{ + {io.EOF, "read error", "read error: EOF"}, + {WithMessage(io.EOF, "read error"), "client error", "client error: read error: EOF"}, + } + + for _, tt := range tests { + got := WithMessage(tt.err, tt.message).Error() + if got != tt.want { + t.Errorf("WithMessage(%v, %q): got: %q, want %q", tt.err, tt.message, got, tt.want) + } + } +} + +func TestWithMessagefNil(t *testing.T) { + got := WithMessagef(nil, "no error") + if got != nil { + t.Errorf("WithMessage(nil, \"no error\"): got %#v, expected nil", got) + } +} + +func TestWithMessagef(t *testing.T) { + tests := []struct { + err error + message string + want string + }{ + {io.EOF, "read error", "read error: EOF"}, + {WithMessagef(io.EOF, "read error without format specifier"), "client error", "client error: read error without format specifier: EOF"}, + {WithMessagef(io.EOF, "read error with %d format specifier", 1), "client error", "client error: read error with 1 format specifier: EOF"}, + } + + for _, tt := range tests { + got := WithMessagef(tt.err, tt.message).Error() + if got != tt.want { + t.Errorf("WithMessage(%v, %q): got: %q, want %q", tt.err, tt.message, got, tt.want) + } + } +} + +// errors.New, etc values are not expected to be compared by value +// but the change in errors#27 made them incomparable. Assert that +// various kinds of errors have a functional equality operator, even +// if the result of that equality is always false. +func TestErrorEquality(t *testing.T) { + vals := []error{ + nil, + io.EOF, + errors.New("EOF"), + New("EOF"), + Errorf("EOF"), + Wrap(io.EOF, "EOF"), + Wrapf(io.EOF, "EOF%d", 2), + WithMessage(nil, "whoops"), + WithMessage(io.EOF, "whoops"), + WithStack(io.EOF), + WithStack(nil), + } + + for i := range vals { + for j := range vals { + _ = vals[i] == vals[j] // mustn't panic + } + } +} + +func TestIs(t *testing.T) { + sentinelError := stdlib_errors.New("sentinel error") + wrap := Wrap(sentinelError, "wrap error") + if !Is(wrap, sentinelError) { + t.Errorf("Expected that '%v' error and the '%v' error should be of the same type", sentinelError, wrap) + } +} + +func TestAs(t *testing.T) { + type myError struct { + error + } + err := &myError{stdlib_errors.New("error")} + wrap := Wrap(err, "wrap error") + + var tt *myError + if !As(wrap, &tt) { + t.Errorf("Expected that '%v' error and the '%v' error should be of the same type", err, wrap) + } +} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..f2133b8 --- /dev/null +++ b/example_test.go @@ -0,0 +1,205 @@ +package errors_test + +import ( + "fmt" + + "git.bit5.ru/backend/errors" +) + +func ExampleNew() { + err := errors.New("whoops") + fmt.Println(err) + + // Output: whoops +} + +func ExampleNew_printf() { + err := errors.New("whoops") + fmt.Printf("%+v", err) + + // Example output: + // whoops + // github.com/friendsofgo/errors_test.ExampleNew_printf + // /home/dfc/src/github.com/friendsofgo/errors/example_test.go:17 + // testing.runExample + // /home/dfc/go/src/testing/example.go:114 + // testing.RunExamples + // /home/dfc/go/src/testing/example.go:38 + // testing.(*M).Run + // /home/dfc/go/src/testing/testing.go:744 + // main.main + // /github.com/friendsofgo/errors/_test/_testmain.go:106 + // runtime.main + // /home/dfc/go/src/runtime/proc.go:183 + // runtime.goexit + // /home/dfc/go/src/runtime/asm_amd64.s:2059 +} + +func ExampleWithMessage() { + cause := errors.New("whoops") + err := errors.WithMessage(cause, "oh noes") + fmt.Println(err) + + // Output: oh noes: whoops +} + +func ExampleWithStack() { + cause := errors.New("whoops") + err := errors.WithStack(cause) + fmt.Println(err) + + // Output: whoops +} + +func ExampleWithStack_printf() { + cause := errors.New("whoops") + err := errors.WithStack(cause) + fmt.Printf("%+v", err) + + // Example Output: + // whoops + // github.com/friendsofgo/errors_test.ExampleWithStack_printf + // /home/fabstu/go/src/github.com/friendsofgo/errors/example_test.go:55 + // testing.runExample + // /usr/lib/go/src/testing/example.go:114 + // testing.RunExamples + // /usr/lib/go/src/testing/example.go:38 + // testing.(*M).Run + // /usr/lib/go/src/testing/testing.go:744 + // main.main + // github.com/friendsofgo/errors/_test/_testmain.go:106 + // runtime.main + // /usr/lib/go/src/runtime/proc.go:183 + // runtime.goexit + // /usr/lib/go/src/runtime/asm_amd64.s:2086 + // github.com/friendsofgo/errors_test.ExampleWithStack_printf + // /home/fabstu/go/src/github.com/friendsofgo/errors/example_test.go:56 + // testing.runExample + // /usr/lib/go/src/testing/example.go:114 + // testing.RunExamples + // /usr/lib/go/src/testing/example.go:38 + // testing.(*M).Run + // /usr/lib/go/src/testing/testing.go:744 + // main.main + // github.com/friendsofgo/errors/_test/_testmain.go:106 + // runtime.main + // /usr/lib/go/src/runtime/proc.go:183 + // runtime.goexit + // /usr/lib/go/src/runtime/asm_amd64.s:2086 +} + +func ExampleWrap() { + cause := errors.New("whoops") + err := errors.Wrap(cause, "oh noes") + fmt.Println(err) + + // Output: oh noes: whoops +} + +func fn() error { + e1 := errors.New("error") + e2 := errors.Wrap(e1, "inner") + e3 := errors.Wrap(e2, "middle") + return errors.Wrap(e3, "outer") +} + +func ExampleCause() { + err := fn() + fmt.Println(err) + fmt.Println(errors.Cause(err)) + + // Output: outer: middle: inner: error + // error +} + +func ExampleWrap_extended() { + err := fn() + fmt.Printf("%+v\n", err) + + // Example output: + // error + // github.com/friendsofgo/errors_test.fn + // /home/dfc/src/github.com/friendsofgo/errors/example_test.go:47 + // github.com/friendsofgo/errors_test.ExampleCause_printf + // /home/dfc/src/github.com/friendsofgo/errors/example_test.go:63 + // testing.runExample + // /home/dfc/go/src/testing/example.go:114 + // testing.RunExamples + // /home/dfc/go/src/testing/example.go:38 + // testing.(*M).Run + // /home/dfc/go/src/testing/testing.go:744 + // main.main + // /github.com/friendsofgo/errors/_test/_testmain.go:104 + // runtime.main + // /home/dfc/go/src/runtime/proc.go:183 + // runtime.goexit + // /home/dfc/go/src/runtime/asm_amd64.s:2059 + // github.com/friendsofgo/errors_test.fn + // /home/dfc/src/github.com/friendsofgo/errors/example_test.go:48: inner + // github.com/friendsofgo/errors_test.fn + // /home/dfc/src/github.com/friendsofgo/errors/example_test.go:49: middle + // github.com/friendsofgo/errors_test.fn + // /home/dfc/src/github.com/friendsofgo/errors/example_test.go:50: outer +} + +func ExampleWrapf() { + cause := errors.New("whoops") + err := errors.Wrapf(cause, "oh noes #%d", 2) + fmt.Println(err) + + // Output: oh noes #2: whoops +} + +func ExampleErrorf_extended() { + err := errors.Errorf("whoops: %s", "foo") + fmt.Printf("%+v", err) + + // Example output: + // whoops: foo + // github.com/friendsofgo/errors_test.ExampleErrorf + // /home/dfc/src/github.com/friendsofgo/errors/example_test.go:101 + // testing.runExample + // /home/dfc/go/src/testing/example.go:114 + // testing.RunExamples + // /home/dfc/go/src/testing/example.go:38 + // testing.(*M).Run + // /home/dfc/go/src/testing/testing.go:744 + // main.main + // /github.com/friendsofgo/errors/_test/_testmain.go:102 + // runtime.main + // /home/dfc/go/src/runtime/proc.go:183 + // runtime.goexit + // /home/dfc/go/src/runtime/asm_amd64.s:2059 +} + +func Example_stackTrace() { + type stackTracer interface { + StackTrace() errors.StackTrace + } + + err, ok := errors.Cause(fn()).(stackTracer) + if !ok { + panic("oops, err does not implement stackTracer") + } + + st := err.StackTrace() + fmt.Printf("%+v", st[0:2]) // top two frames + + // Example output: + // github.com/friendsofgo/errors_test.fn + // /home/dfc/src/github.com/friendsofgo/errors/example_test.go:47 + // github.com/friendsofgo/errors_test.Example_stackTrace + // /home/dfc/src/github.com/friendsofgo/errors/example_test.go:127 +} + +func ExampleCause_printf() { + err := errors.Wrap(func() error { + return func() error { + return errors.Errorf("hello %s", fmt.Sprintf("world")) + }() + }(), "failed") + + fmt.Printf("%v", err) + + // Output: failed: hello world +} diff --git a/format_test.go b/format_test.go new file mode 100644 index 0000000..2f3725d --- /dev/null +++ b/format_test.go @@ -0,0 +1,560 @@ +package errors + +//import ( +// "errors" +// "fmt" +// "io" +// "regexp" +// "strings" +// "testing" +//) +// +//func TestFormatNew(t *testing.T) { +// tests := []struct { +// error +// format string +// want string +// }{{ +// New("error"), +// "%s", +// "error", +// }, { +// New("error"), +// "%v", +// "error", +// }, { +// New("error"), +// "%+v", +// "error\n" + +// "game/errors.TestFormatNew\n" + +// "\t.+game/errors/errors/format_test.go:26", +// }, { +// New("error"), +// "%q", +// `"error"`, +// }} +// +// for i, tt := range tests { +// testFormatRegexp(t, i, tt.error, tt.format, tt.want) +// } +//} +// +//func TestFormatErrorf(t *testing.T) { +// tests := []struct { +// error +// format string +// want string +// }{{ +// Errorf("%s", "error"), +// "%s", +// "error", +// }, { +// Errorf("%s", "error"), +// "%v", +// "error", +// }, { +// Errorf("%s", "error"), +// "%+v", +// "error\n" + +// "game/errors.TestFormatErrorf\n" + +// "\t.+game/errors/errors/format_test.go:56", +// }} +// +// for i, tt := range tests { +// testFormatRegexp(t, i, tt.error, tt.format, tt.want) +// } +//} +// +//func TestFormatWrap(t *testing.T) { +// tests := []struct { +// error +// format string +// want string +// }{{ +// Wrap(New("error"), "error2"), +// "%s", +// "error2: error", +// }, { +// Wrap(New("error"), "error2"), +// "%v", +// "error2: error", +// }, { +// Wrap(New("error"), "error2"), +// "%+v", +// "error\n" + +// "game/errors.TestFormatWrap\n" + +// "\t.+game/errors/errors/format_test.go:82", +// }, { +// Wrap(io.EOF, "error"), +// "%s", +// "error: EOF", +// }, { +// Wrap(io.EOF, "error"), +// "%v", +// "error: EOF", +// }, { +// Wrap(io.EOF, "error"), +// "%+v", +// "EOF\n" + +// "error\n" + +// "game/errors.TestFormatWrap\n" + +// "\t.+game/errors/errors/format_test.go:96", +// }, { +// Wrap(Wrap(io.EOF, "error1"), "error2"), +// "%+v", +// "EOF\n" + +// "error1\n" + +// "game/errors.TestFormatWrap\n" + +// "\t.+game/errors/errors/format_test.go:103\n", +// }, { +// Wrap(New("error with space"), "context"), +// "%q", +// `"context: error with space"`, +// }} +// +// for i, tt := range tests { +// testFormatRegexp(t, i, tt.error, tt.format, tt.want) +// } +//} +// +//func TestFormatWrapf(t *testing.T) { +// tests := []struct { +// error +// format string +// want string +// }{{ +// Wrapf(io.EOF, "error%d", 2), +// "%s", +// "error2: EOF", +// }, { +// Wrapf(io.EOF, "error%d", 2), +// "%v", +// "error2: EOF", +// }, { +// Wrapf(io.EOF, "error%d", 2), +// "%+v", +// "EOF\n" + +// "error2\n" + +// "game/errors.TestFormatWrapf\n" + +// "\t.+game/errors/errors/format_test.go:134", +// }, { +// Wrapf(New("error"), "error%d", 2), +// "%s", +// "error2: error", +// }, { +// Wrapf(New("error"), "error%d", 2), +// "%v", +// "error2: error", +// }, { +// Wrapf(New("error"), "error%d", 2), +// "%+v", +// "error\n" + +// "game/errors.TestFormatWrapf\n" + +// "\t.+game/errors/errors/format_test.go:149", +// }} +// +// for i, tt := range tests { +// testFormatRegexp(t, i, tt.error, tt.format, tt.want) +// } +//} +// +//func TestFormatWithStack(t *testing.T) { +// tests := []struct { +// error +// format string +// want []string +// }{{ +// WithStack(io.EOF), +// "%s", +// []string{"EOF"}, +// }, { +// WithStack(io.EOF), +// "%v", +// []string{"EOF"}, +// }, { +// WithStack(io.EOF), +// "%+v", +// []string{"EOF", +// "game/errors.TestFormatWithStack\n" + +// "\t.+game/errors/errors/format_test.go:175"}, +// }, { +// WithStack(New("error")), +// "%s", +// []string{"error"}, +// }, { +// WithStack(New("error")), +// "%v", +// []string{"error"}, +// }, { +// WithStack(New("error")), +// "%+v", +// []string{"error", +// "game/errors.TestFormatWithStack\n" + +// "\t.+game/errors/errors/format_test.go:189", +// "game/errors.TestFormatWithStack\n" + +// "\t.+game/errors/errors/format_test.go:189"}, +// }, { +// WithStack(WithStack(io.EOF)), +// "%+v", +// []string{"EOF", +// "game/errors.TestFormatWithStack\n" + +// "\t.+game/errors/errors/format_test.go:197", +// "game/errors.TestFormatWithStack\n" + +// "\t.+game/errors/errors/format_test.go:197"}, +// }, { +// WithStack(WithStack(Wrapf(io.EOF, "message"))), +// "%+v", +// []string{"EOF", +// "message", +// "game/errors.TestFormatWithStack\n" + +// "\t.+game/errors/errors/format_test.go:205", +// "game/errors.TestFormatWithStack\n" + +// "\t.+game/errors/errors/format_test.go:205", +// "game/errors.TestFormatWithStack\n" + +// "\t.+game/errors/errors/format_test.go:205"}, +// }, { +// WithStack(Errorf("error%d", 1)), +// "%+v", +// []string{"error1", +// "game/errors.TestFormatWithStack\n" + +// "\t.+game/errors/errors/format_test.go:216", +// "game/errors.TestFormatWithStack\n" + +// "\t.+game/errors/errors/format_test.go:216"}, +// }} +// +// for i, tt := range tests { +// testFormatCompleteCompare(t, i, tt.error, tt.format, tt.want, true) +// } +//} +// +//func TestFormatWithMessage(t *testing.T) { +// tests := []struct { +// error +// format string +// want []string +// }{{ +// WithMessage(New("error"), "error2"), +// "%s", +// []string{"error2: error"}, +// }, { +// WithMessage(New("error"), "error2"), +// "%v", +// []string{"error2: error"}, +// }, { +// WithMessage(New("error"), "error2"), +// "%+v", +// []string{ +// "error", +// "game/errors.TestFormatWithMessage\n" + +// "\t.+game/errors/errors/format_test.go:244", +// "error2"}, +// }, { +// WithMessage(io.EOF, "addition1"), +// "%s", +// []string{"addition1: EOF"}, +// }, { +// WithMessage(io.EOF, "addition1"), +// "%v", +// []string{"addition1: EOF"}, +// }, { +// WithMessage(io.EOF, "addition1"), +// "%+v", +// []string{"EOF", "addition1"}, +// }, { +// WithMessage(WithMessage(io.EOF, "addition1"), "addition2"), +// "%v", +// []string{"addition2: addition1: EOF"}, +// }, { +// WithMessage(WithMessage(io.EOF, "addition1"), "addition2"), +// "%+v", +// []string{"EOF", "addition1", "addition2"}, +// }, { +// Wrap(WithMessage(io.EOF, "error1"), "error2"), +// "%+v", +// []string{"EOF", "error1", "error2", +// "game/errors.TestFormatWithMessage\n" + +// "\t.+game/errors/errors/format_test.go:272"}, +// }, { +// WithMessage(Errorf("error%d", 1), "error2"), +// "%+v", +// []string{"error1", +// "game/errors.TestFormatWithMessage\n" + +// "\t.+game/errors/errors/format_test.go:278", +// "error2"}, +// }, { +// WithMessage(WithStack(io.EOF), "error"), +// "%+v", +// []string{ +// "EOF", +// "game/errors.TestFormatWithMessage\n" + +// "\t.+game/errors/errors/format_test.go:285", +// "error"}, +// }, { +// WithMessage(Wrap(WithStack(io.EOF), "inside-error"), "outside-error"), +// "%+v", +// []string{ +// "EOF", +// "game/errors.TestFormatWithMessage\n" + +// "\t.+game/errors/errors/format_test.go:293", +// "inside-error", +// "game/errors.TestFormatWithMessage\n" + +// "\t.+game/errors/errors/format_test.go:293", +// "outside-error"}, +// }} +// +// for i, tt := range tests { +// testFormatCompleteCompare(t, i, tt.error, tt.format, tt.want, true) +// } +//} +// +//func TestFormatGeneric(t *testing.T) { +// starts := []struct { +// err error +// want []string +// }{ +// {New("new-error"), []string{ +// "new-error", +// "game/errors.TestFormatGeneric\n" + +// "\t.+game/errors/errors/format_test.go:315"}, +// }, {Errorf("errorf-error"), []string{ +// "errorf-error", +// "game/errors.TestFormatGeneric\n" + +// "\t.+game/errors/errors/format_test.go:319"}, +// }, {errors.New("errors-new-error"), []string{ +// "errors-new-error"}, +// }, +// } +// +// wrappers := []wrapper{ +// { +// func(err error) error { return WithMessage(err, "with-message") }, +// []string{"with-message"}, +// }, { +// func(err error) error { return WithStack(err) }, +// []string{ +// "game/errors.(func·002|TestFormatGeneric.func2)\n\t" + +// ".+game/errors/errors/format_test.go:333", +// }, +// }, { +// func(err error) error { return Wrap(err, "wrap-error") }, +// []string{ +// "wrap-error", +// "game/errors.(func·003|TestFormatGeneric.func3)\n\t" + +// ".+game/errors/errors/format_test.go:339", +// }, +// }, { +// func(err error) error { return Wrapf(err, "wrapf-error%d", 1) }, +// []string{ +// "wrapf-error1", +// "game/errors.(func·004|TestFormatGeneric.func4)\n\t" + +// ".+game/errors/errors/format_test.go:346", +// }, +// }, +// } +// +// for s := range starts { +// err := starts[s].err +// want := starts[s].want +// testFormatCompleteCompare(t, s, err, "%+v", want, false) +// testGenericRecursive(t, err, want, wrappers, 3) +// } +//} +// +//func wrappedNew(message string) error { // This function will be mid-stack inlined in go 1.12+ +// return New(message) +//} +// +//func TestFormatWrappedNew(t *testing.T) { +// tests := []struct { +// error +// format string +// want string +// }{{ +// wrappedNew("error"), +// "%+v", +// "error\n" + +// "game/errors.wrappedNew\n" + +// "\t.+game/errors/errors/format_test.go:364\n" + +// "game/errors.TestFormatWrappedNew\n" + +// "\t.+game/errors/errors/format_test.go:373", +// }} +// +// for i, tt := range tests { +// testFormatRegexp(t, i, tt.error, tt.format, tt.want) +// } +//} +// +//func testFormatRegexp(t *testing.T, n int, arg interface{}, format, want string) { +// t.Helper() +// got := fmt.Sprintf(format, arg) +// gotLines := strings.SplitN(got, "\n", -1) +// wantLines := strings.SplitN(want, "\n", -1) +// +// if len(wantLines) > len(gotLines) { +// t.Errorf("test %d: wantLines(%d) > gotLines(%d):\n got: %q\nwant: %q", n+1, len(wantLines), len(gotLines), got, want) +// return +// } +// +// for i, w := range wantLines { +// match, err := regexp.MatchString(w, gotLines[i]) +// if err != nil { +// t.Fatal(err) +// } +// if !match { +// t.Errorf("test %d: line %d: fmt.Sprintf(%q, err):\n got: %q\nwant: %q", n+1, i+1, format, got, want) +// } +// } +//} +// +//var stackLineR = regexp.MustCompile(`\.`) +// +//// parseBlocks parses input into a slice, where: +//// - incase entry contains a newline, its a stacktrace +//// - incase entry contains no newline, its a solo line. +//// +//// Detecting stack boundaries only works incase the WithStack-calls are +//// to be found on the same line, thats why it is optionally here. +//// +//// Example use: +//// +//// for _, e := range blocks { +//// if strings.ContainsAny(e, "\n") { +//// // Match as stack +//// } else { +//// // Match as line +//// } +//// } +//// +//func parseBlocks(input string, detectStackboundaries bool) ([]string, error) { +// var blocks []string +// +// stack := "" +// wasStack := false +// lines := map[string]bool{} // already found lines +// +// for _, l := range strings.Split(input, "\n") { +// isStackLine := stackLineR.MatchString(l) +// +// switch { +// case !isStackLine && wasStack: +// blocks = append(blocks, stack, l) +// stack = "" +// lines = map[string]bool{} +// case isStackLine: +// if wasStack { +// // Detecting two stacks after another, possible cause lines match in +// // our tests due to WithStack(WithStack(io.EOF)) on same line. +// if detectStackboundaries { +// if lines[l] { +// if len(stack) == 0 { +// return nil, errors.New("len of block must not be zero here") +// } +// +// blocks = append(blocks, stack) +// stack = l +// lines = map[string]bool{l: true} +// continue +// } +// } +// +// stack = stack + "\n" + l +// } else { +// stack = l +// } +// lines[l] = true +// case !isStackLine && !wasStack: +// blocks = append(blocks, l) +// default: +// return nil, errors.New("must not happen") +// } +// +// wasStack = isStackLine +// } +// +// // Use up stack +// if stack != "" { +// blocks = append(blocks, stack) +// } +// return blocks, nil +//} +// +//func testFormatCompleteCompare(t *testing.T, n int, arg interface{}, format string, want []string, detectStackBoundaries bool) { +// gotStr := fmt.Sprintf(format, arg) +// +// got, err := parseBlocks(gotStr, detectStackBoundaries) +// if err != nil { +// t.Fatal(err) +// } +// +// if len(got) != len(want) { +// t.Fatalf("test %d: fmt.Sprintf(%s, err) -> wrong number of blocks: got(%d) want(%d)\n got: %s\nwant: %s\ngotStr: %q", +// n+1, format, len(got), len(want), prettyBlocks(got), prettyBlocks(want), gotStr) +// } +// +// for i := range got { +// if strings.ContainsAny(want[i], "\n") { +// // Match as stack +// match, err := regexp.MatchString(want[i], got[i]) +// if err != nil { +// t.Fatal(err) +// } +// if !match { +// t.Fatalf("test %d: block %d: fmt.Sprintf(%q, err):\ngot:\n%q\nwant:\n%q\nall-got:\n%s\nall-want:\n%s\n", +// n+1, i+1, format, got[i], want[i], prettyBlocks(got), prettyBlocks(want)) +// } +// } else { +// // Match as message +// if got[i] != want[i] { +// t.Fatalf("test %d: fmt.Sprintf(%s, err) at block %d got != want:\n got: %q\nwant: %q", n+1, format, i+1, got[i], want[i]) +// } +// } +// } +//} +// +//type wrapper struct { +// wrap func(err error) error +// want []string +//} +// +//func prettyBlocks(blocks []string) string { +// var out []string +// +// for _, b := range blocks { +// out = append(out, fmt.Sprintf("%v", b)) +// } +// +// return " " + strings.Join(out, "\n ") +//} +// +//func testGenericRecursive(t *testing.T, beforeErr error, beforeWant []string, list []wrapper, maxDepth int) { +// if len(beforeWant) == 0 { +// panic("beforeWant must not be empty") +// } +// for _, w := range list { +// if len(w.want) == 0 { +// panic("want must not be empty") +// } +// +// err := w.wrap(beforeErr) +// +// // Copy required cause append(beforeWant, ..) modified beforeWant subtly. +// beforeCopy := make([]string, len(beforeWant)) +// copy(beforeCopy, beforeWant) +// +// beforeWant := beforeCopy +// last := len(beforeWant) - 1 +// var want []string +// +// // Merge two stacks behind each other. +// if strings.ContainsAny(beforeWant[last], "\n") && strings.ContainsAny(w.want[0], "\n") { +// want = append(beforeWant[:last], append([]string{beforeWant[last] + "((?s).*)" + w.want[0]}, w.want[1:]...)...) +// } else { +// want = append(beforeWant, w.want...) +// } +// +// testFormatCompleteCompare(t, maxDepth, err, "%+v", want, false) +// if maxDepth > 0 { +// testGenericRecursive(t, err, want, list, maxDepth-1) +// } +// } +//} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..34607e2 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.bit5.ru/backend/errors + +go 1.13 diff --git a/json_test.go b/json_test.go new file mode 100644 index 0000000..cbd9d8b --- /dev/null +++ b/json_test.go @@ -0,0 +1,51 @@ +package errors + +import ( + "encoding/json" + "regexp" + "testing" +) + +func TestFrameMarshalText(t *testing.T) { + var tests = []struct { + Frame + want string + }{{ + initpc, + `^game/errors\.init(\.ializers)? .+/game/errors/stack_test.go:\d+$`, + }, { + 0, + `^unknown$`, + }} + for i, tt := range tests { + got, err := tt.Frame.MarshalText() + if err != nil { + t.Fatal(err) + } + if !regexp.MustCompile(tt.want).Match(got) { + t.Errorf("test %d: MarshalJSON:\n got %q\n want %q", i+1, string(got), tt.want) + } + } +} + +func TestFrameMarshalJSON(t *testing.T) { + var tests = []struct { + Frame + want string + }{{ + initpc, + `^"game/errors\.init(\.ializers)? .+/game/errors/stack_test.go:\d+"$`, + }, { + 0, + `^"unknown"$`, + }} + for i, tt := range tests { + got, err := json.Marshal(tt.Frame) + if err != nil { + t.Fatal(err) + } + if !regexp.MustCompile(tt.want).Match(got) { + t.Errorf("test %d: MarshalJSON:\n got %q\n want %q", i+1, string(got), tt.want) + } + } +} diff --git a/stack.go b/stack.go new file mode 100644 index 0000000..f1098eb --- /dev/null +++ b/stack.go @@ -0,0 +1,198 @@ +package errors + +import ( + "fmt" + "io" + "path" + "runtime" + "strconv" + "strings" +) + +// Frame represents a program counter inside a stack frame. +// For historical reasons if Frame is interpreted as a uintptr +// its value represents the program counter + 1. +type Frame uintptr + +// pc returns the program counter for this frame; +// multiple frames may have the same PC value. +func (f Frame) pc() uintptr { return uintptr(f) - 1 } + +// file returns the full path to the file that contains the +// function for this Frame's pc. +func (f Frame) file() string { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return "unknown" + } + file, _ := fn.FileLine(f.pc()) + return file +} + +// line returns the line number of source code of the +// function for this Frame's pc. +func (f Frame) line() int { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return 0 + } + _, line := fn.FileLine(f.pc()) + return line +} + +// name returns the name of this function, if known. +func (f Frame) name() string { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return "unknown" + } + return fn.Name() +} + +// Format formats the frame according to the fmt.Formatter interface. +// +// %s source file +// %d source line +// %n function name +// %v equivalent to %s:%d +// +// Format accepts flags that alter the printing of some verbs, as follows: +// +// %+s function name and path of source file relative to the compile time +// GOPATH separated by \n\t (\n\t) +// %+v equivalent to %+s:%d +func (f Frame) Format(s fmt.State, verb rune) { + switch verb { + case 's': + switch { + case s.Flag('-'): + io.WriteString(s, path.Ext(f.name())) + io.WriteString(s, " ") + dir, file := path.Split(f.file()) + io.WriteString(s, path.Join(path.Base(path.Dir(dir)), path.Base(dir), file)) + case s.Flag('+'): + io.WriteString(s, f.name()) + io.WriteString(s, "\n\t") + io.WriteString(s, f.file()) + default: + io.WriteString(s, path.Base(f.file())) + } + case 'd': + io.WriteString(s, strconv.Itoa(f.line())) + case 'n': + io.WriteString(s, funcname(f.name())) + case 'v': + if s.Flag('-') { + io.WriteString(s, "[") + } + f.Format(s, 's') + io.WriteString(s, ":") + f.Format(s, 'd') + if s.Flag('-') { + io.WriteString(s, "]") + } + } +} + +// MarshalText formats a stacktrace Frame as a text string. The output is the +// same as that of fmt.Sprintf("%+v", f), but without newlines or tabs. +func (f Frame) MarshalText() ([]byte, error) { + name := f.name() + if name == "unknown" { + return []byte(name), nil + } + return []byte(fmt.Sprintf("%s %s:%d", name, f.file(), f.line())), nil +} + +// StackTrace is stack of Frames from innermost (newest) to outermost (oldest). +type StackTrace []Frame + +// Format formats the stack of Frames according to the fmt.Formatter interface. +// +// %s lists source files for each Frame in the stack +// %v lists the source file and line number for each Frame in the stack +// +// Format accepts flags that alter the printing of some verbs, as follows: +// +// %+v Prints filename, function, and line number for each Frame in the stack. +func (st StackTrace) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case s.Flag('+'): + for _, f := range st { + io.WriteString(s, "\n") + f.Format(s, verb) + } + case s.Flag('-'): + for _, f := range st { + io.WriteString(s, " ") + f.Format(s, verb) + } + case s.Flag('#'): + fmt.Fprintf(s, "%#v", []Frame(st)) + default: + st.formatSlice(s, verb) + } + case 's': + st.formatSlice(s, verb) + } +} + +// formatSlice will format this StackTrace into the given buffer as a slice of +// Frame, only valid when called with '%s' or '%v'. +func (st StackTrace) formatSlice(s fmt.State, verb rune) { + io.WriteString(s, "[") + for i, f := range st { + if i > 0 { + io.WriteString(s, " ") + } + f.Format(s, verb) + } + io.WriteString(s, "]") +} + +// stack represents a stack of program counters. +type stack []uintptr + +func (s *stack) Format(st fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case st.Flag('+'): + for _, pc := range *s { + f := Frame(pc) + fmt.Fprintf(st, "\n%+v", f) + } + case st.Flag('-'): + for _, pc := range *s { + f := Frame(pc) + fmt.Fprintf(st, " < %-v", f) + } + } + } +} + +func (s *stack) StackTrace() StackTrace { + f := make([]Frame, len(*s)) + for i := 0; i < len(f); i++ { + f[i] = Frame((*s)[i]) + } + return f +} + +func callers() *stack { + const depth = 7 + var pcs [depth]uintptr + n := runtime.Callers(3, pcs[:]) + var st stack = pcs[0:n] + return &st +} + +// funcname removes the path prefix component of a function's name reported by func.Name(). +func funcname(name string) string { + i := strings.LastIndex(name, "/") + name = name[i+1:] + i = strings.Index(name, ".") + return name[i+1:] +} diff --git a/stack_test.go b/stack_test.go new file mode 100644 index 0000000..0bb135a --- /dev/null +++ b/stack_test.go @@ -0,0 +1,250 @@ +package errors + +import ( + // "fmt" + "runtime" + // "testing" +) + +var initpc = caller() + +//type X struct{} +// +//// val returns a Frame pointing to itself. +//func (x X) val() Frame { +// return caller() +//} +// +//// ptr returns a Frame pointing to itself. +//func (x *X) ptr() Frame { +// return caller() +//} +// +//func TestFrameFormat(t *testing.T) { +// var tests = []struct { +// Frame +// format string +// want string +// }{{ +// initpc, +// "%s", +// "stack_test.go", +// }, { +// initpc, +// "%+s", +// "game/errors/errors.init\n" + +// "\t.+game/errors/errors/stack_test.go", +// }, { +// 0, +// "%s", +// "unknown", +// }, { +// 0, +// "%+s", +// "unknown", +// }, { +// initpc, +// "%d", +// "9", +// }, { +// 0, +// "%d", +// "0", +// }, { +// initpc, +// "%n", +// "init", +// }, { +// func() Frame { +// var x X +// return x.ptr() +// }(), +// "%n", +// `\(\*X\).ptr`, +// }, { +// func() Frame { +// var x X +// return x.val() +// }(), +// "%n", +// "X.val", +// }, { +// 0, +// "%n", +// "", +// }, { +// initpc, +// "%v", +// "stack_test.go:9", +// }, { +// initpc, +// "%+v", +// "game/errors/errors.init\n" + +// "\t.+game/errors/errors/stack_test.go:9", +// }, { +// 0, +// "%v", +// "unknown:0", +// }} +// +// for i, tt := range tests { +// testFormatRegexp(t, i, tt.Frame, tt.format, tt.want) +// } +//} +// +//func TestFuncname(t *testing.T) { +// tests := []struct { +// name, want string +// }{ +// {"", ""}, +// {"runtime.main", "main"}, +// {"game/errors/errors.funcname", "funcname"}, +// {"funcname", "funcname"}, +// {"io.copyBuffer", "copyBuffer"}, +// {"main.(*R).Write", "(*R).Write"}, +// } +// +// for _, tt := range tests { +// got := funcname(tt.name) +// want := tt.want +// if got != want { +// t.Errorf("funcname(%q): want: %q, got %q", tt.name, want, got) +// } +// } +//} +// +//func TestStackTrace(t *testing.T) { +// tests := []struct { +// err error +// want []string +// }{{ +// New("ooh"), []string{ +// "game/errors/errors.TestStackTrace\n" + +// "\t.+game/errors/errors/stack_test.go:121", +// }, +// }, { +// Wrap(New("ooh"), "ahh"), []string{ +// "game/errors/errors.TestStackTrace\n" + +// "\t.+game/errors/errors/stack_test.go:126", // this is the stack of Wrap, not New +// }, +// }, { +// Cause(Wrap(New("ooh"), "ahh")), []string{ +// "game/errors/errors.TestStackTrace\n" + +// "\t.+game/errors/errors/stack_test.go:131", // this is the stack of New +// }, +// }, { +// func() error { return New("ooh") }(), []string{ +// `game/errors/errors.TestStackTrace.func1` + +// "\n\t.+game/errors/errors/stack_test.go:136", // this is the stack of New +// "game/errors/errors.TestStackTrace\n" + +// "\t.+game/errors/errors/stack_test.go:136", // this is the stack of New's caller +// }, +// }, { +// Cause(func() error { +// return func() error { +// return Errorf("hello %s", fmt.Sprintf("world")) +// }() +// }()), []string{ +// `game/errors/errors.TestStackTrace.func2.1` + +// "\n\t.+game/errors/errors/stack_test.go:145", // this is the stack of Errorf +// `game/errors/errors.TestStackTrace.func2` + +// "\n\t.+game/errors/errors/stack_test.go:146", // this is the stack of Errorf's caller +// "game/errors/errors.TestStackTrace\n" + +// "\t.+game/errors/errors/stack_test.go:147", // this is the stack of Errorf's caller's caller +// }, +// }} +// for i, tt := range tests { +// x, ok := tt.err.(interface { +// StackTrace() StackTrace +// }) +// if !ok { +// t.Errorf("expected %#v to implement StackTrace() StackTrace", tt.err) +// continue +// } +// st := x.StackTrace() +// for j, want := range tt.want { +// testFormatRegexp(t, i, st[j], "%+v", want) +// } +// } +//} +// +//func stackTrace() StackTrace { +// const depth = 8 +// var pcs [depth]uintptr +// n := runtime.Callers(1, pcs[:]) +// var st stack = pcs[0:n] +// return st.StackTrace() +//} +// +//func TestStackTraceFormat(t *testing.T) { +// tests := []struct { +// StackTrace +// format string +// want string +// }{{ +// nil, +// "%s", +// `\[\]`, +// }, { +// nil, +// "%v", +// `\[\]`, +// }, { +// nil, +// "%+v", +// "", +// }, { +// nil, +// "%#v", +// `\[\]errors.Frame\(nil\)`, +// }, { +// make(StackTrace, 0), +// "%s", +// `\[\]`, +// }, { +// make(StackTrace, 0), +// "%v", +// `\[\]`, +// }, { +// make(StackTrace, 0), +// "%+v", +// "", +// }, { +// make(StackTrace, 0), +// "%#v", +// `\[\]errors.Frame{}`, +// }, { +// stackTrace()[:2], +// "%s", +// `\[stack_test.go stack_test.go\]`, +// }, { +// stackTrace()[:2], +// "%v", +// `\[stack_test.go:174 stack_test.go:221\]`, +// }, { +// stackTrace()[:2], +// "%+v", +// "\n" + +// "game/errors/errors.stackTrace\n" + +// "\t.+game/errors/errors/stack_test.go:174\n" + +// "game/errors/errors.TestStackTraceFormat\n" + +// "\t.+game/errors/errors/stack_test.go:225", +// }, { +// stackTrace()[:2], +// "%#v", +// `\[\]errors.Frame{stack_test.go:174, stack_test.go:233}`, +// }} +// +// for i, tt := range tests { +// testFormatRegexp(t, i, tt.StackTrace, tt.format, tt.want) +// } +//} +// +// a version of runtime.Caller that returns a Frame, not a uintptr. +func caller() Frame { + var pcs [3]uintptr + n := runtime.Callers(2, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + frame, _ := frames.Next() + return Frame(frame.PC) +}