1
0
Fork 0

Adding upstream version 0.0~git20250501.cd50c6a.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-18 22:31:57 +02:00
parent 947813c282
commit d8f2a7c92a
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
16 changed files with 3363 additions and 0 deletions

16
.build.yml Normal file
View file

@ -0,0 +1,16 @@
image: archlinux
packages:
- go
sources:
- https://github.com/go-ap/errors
environment:
GO111MODULE: 'on'
tasks:
- tests: |
cd errors
make test
- coverage: |
set -a +x
cd errors && make coverage
GIT_SHA=$(git rev-parse --verify HEAD)
GIT_BRANCH=$(git name-rev --name-only HEAD)

14
.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
# Gogland
.idea/
# Binaries for programs and plugins
*.so
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tools
*.out
*.coverprofile
*pkg

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Golang ActitvityPub
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

22
Makefile Normal file
View file

@ -0,0 +1,22 @@
GO ?= go
TEST := $(GO) test
TEST_FLAGS ?= -v
TEST_TARGET ?= ./...
GO111MODULE=on
PROJECT_NAME := $(shell basename $(PWD))
.PHONY: test coverage clean download
download:
$(GO) mod download all
test: download
$(TEST) $(TEST_FLAGS) $(TEST_TARGET)
coverage: TEST_TARGET := .
coverage: TEST_FLAGS += -covermode=count -coverprofile $(PROJECT_NAME).coverprofile
coverage: test
clean:
$(RM) -v *.coverprofile

6
README.md Normal file
View file

@ -0,0 +1,6 @@
[![MIT Licensed](https://img.shields.io/github/license/go-ap/errors.svg)](https://raw.githubusercontent.com/go-ap/errors/master/LICENSE)
[![Build Status](https://builds.sr.ht/~mariusor/errors.svg)](https://builds.sr.ht/~mariusor/errors)
[![Test Coverage](https://img.shields.io/codecov/c/github/go-ap/errors.svg)](https://codecov.io/gh/go-ap/errors)
[![Go Report Card](https://goreportcard.com/badge/github.com/go-ap/errors)](https://goreportcard.com/report/github.com/go-ap/errors)
<!--[![Codacy Badge](https://api.codacy.com/project/badge/Grade/29664f7ae6c643bca76700143e912cd3)](https://www.codacy.com/app/go-ap/errors/dashboard)-->

85
decoding.go Normal file
View file

@ -0,0 +1,85 @@
package errors
import (
"github.com/valyala/fastjson"
)
func UnmarshalJSON(data []byte) ([]error, error) {
if len(data) == 0 {
return nil, nil
}
p := fastjson.Parser{}
val, err := p.ParseBytes(data)
if err != nil {
return nil, err
}
v := val.Get("errors")
if v == nil {
return nil, wrap(nil, "invalid errors array")
}
items := make([]error, 0)
switch v.Type() {
case fastjson.TypeArray:
for _, v := range v.GetArray() {
status := v.GetInt("status")
localErr := errorFromStatus(status)
if err := localErr.UnmarshalJSON([]byte(v.String())); err == nil {
items = append(items, localErr)
}
}
return items, err
case fastjson.TypeObject:
status := v.GetInt("status")
localErr := errorFromStatus(status)
if err := localErr.UnmarshalJSON([]byte(v.String())); err == nil {
items = append(items, localErr)
}
case fastjson.TypeString:
it := new(Err)
it.m = string(data)
items = append(items, it)
}
return items, nil
}
func (e *Err) UnmarshalJSON(data []byte) error {
if m := fastjson.GetString(data, "message"); len(m) > 0 {
e.m = m
}
return nil
}
func (n *notFound) UnmarshalJSON(data []byte) error {
return n.Err.UnmarshalJSON(data)
}
func (m *methodNotAllowed) UnmarshalJSON(data []byte) error {
return m.Err.UnmarshalJSON(data)
}
func (n *notValid) UnmarshalJSON(data []byte) error {
return n.Err.UnmarshalJSON(data)
}
func (f *forbidden) UnmarshalJSON(data []byte) error {
return f.Err.UnmarshalJSON(data)
}
func (n *notImplemented) UnmarshalJSON(data []byte) error {
return n.Err.UnmarshalJSON(data)
}
func (b *badRequest) UnmarshalJSON(data []byte) error {
return b.Err.UnmarshalJSON(data)
}
func (u *unauthorized) UnmarshalJSON(data []byte) error {
return u.Err.UnmarshalJSON(data)
}
func (n *notSupported) UnmarshalJSON(data []byte) error {
return n.Err.UnmarshalJSON(data)
}
func (t *timeout) UnmarshalJSON(data []byte) error {
return t.Err.UnmarshalJSON(data)
}
func (b *badGateway) UnmarshalJSON(data []byte) error {
return b.Err.UnmarshalJSON(data)
}
func (s *serviceUnavailable) UnmarshalJSON(data []byte) error {
return s.Err.UnmarshalJSON(data)
}

99
decoding_test.go Normal file

File diff suppressed because one or more lines are too long

166
errors.go Normal file
View file

@ -0,0 +1,166 @@
package errors
import (
"errors"
"fmt"
"io"
"strings"
)
// Export a number of functions or variables from package errors.
var (
As = errors.As
Is = errors.Is
Unwrap = errors.Unwrap
//Join = errors.Join
)
// IncludeBacktrace is a static variable that decides if when creating an error we store the backtrace with it.
var IncludeBacktrace = true
// Err is our custom error type that can store backtrace, file and line number
type Err struct {
m string
c error
t stack
}
func (e Err) Format(s fmt.State, verb rune) {
switch verb {
case 's':
io.WriteString(s, e.m)
switch {
case s.Flag('+'):
if e.c != nil {
io.WriteString(s, ": ")
io.WriteString(s, fmt.Sprintf("%+s", e.c))
}
}
case 'v':
e.Format(s, 's')
switch {
case s.Flag('+'):
if e.t != nil {
io.WriteString(s, "\n\t")
e.t.Format(s, 'v')
}
}
}
}
// Error implements the error interface
func (e Err) Error() string {
if IncludeBacktrace {
return e.m
}
s := strings.Builder{}
s.WriteString(e.m)
if ch := errors.Unwrap(e); ch != nil {
s.WriteString(": ")
s.WriteString(ch.Error())
}
return s.String()
}
// Unwrap implements the errors.Wrapper interface
func (e Err) Unwrap() error {
return e.c
}
// StackTrace returns the stack trace as returned by the debug.Stack function
func (e Err) StackTrace() StackTrace {
return e.t.StackTrace()
}
// Annotatef wraps an error with new message
func Annotatef(e error, s string, args ...interface{}) *Err {
err := wrap(e, s, args...)
return &err
}
// Newf creaates a new error
func Newf(s string, args ...interface{}) *Err {
err := wrap(nil, s, args...)
return &err
}
// Errorf is an alias for Newf
func Errorf(s string, args ...interface{}) error {
err := wrap(nil, s, args...)
return &err
}
// As implements support for errors.As
func (e *Err) As(err interface{}) bool {
switch x := err.(type) {
case **Err:
*x = e
case *Err:
*x = *e
default:
return false
}
return true
}
type StackTracer interface {
StackTrace() StackTrace
}
// ancestorOfCause returns true if the caller looks to be an ancestor of the given stack
// trace. We check this by seeing whether our stack prefix-matches the cause stack, which
// should imply the error was generated directly from our goroutine.
func ancestorOfCause(ourStack stack, causeStack StackTrace) bool {
// Stack traces are ordered such that the deepest frame is first. We'll want to check
// for prefix matching in reverse.
//
// As an example, imagine we have a prefix-matching stack for ourselves:
// [
// "github.com/go-ap/processing/processing.Validate",
// "testing.tRunner",
// "runtime.goexit"
// ]
//
// We'll want to compare this against an error cause that will have happened further
// down the stack. An example stack trace from such an error might be:
// [
// "github.com/go-ap/errors/errors.New",
// "testing.tRunner",
// "runtime.goexit"
// ]
//
// Their prefix matches, but we'll have to handle the match carefully as we need to match
// from back to forward.
// We can't possibly prefix match if our stack is larger than the cause stack.
if len(ourStack) > len(causeStack) {
return false
}
// We know the sizes are compatible, so compare program counters from back to front.
for idx := 0; idx < len(ourStack); idx++ {
if ourStack[len(ourStack)-1] != (uintptr)(causeStack[len(causeStack)-1]) {
return false
}
}
return true
}
func wrap(e error, s string, args ...interface{}) Err {
err := Err{
c: e,
m: fmt.Sprintf(s, args...),
}
if IncludeBacktrace {
causeStackTracer := new(StackTracer)
// If our cause has set a stack trace, and that trace is a child of our own function
// as inferred by prefix matching our current program counter stack, then we only want
// to decorate the error message rather than add a redundant stack trace.
stack := callers(2)
if !(As(e, causeStackTracer) && ancestorOfCause(*stack, (*causeStackTracer).StackTrace())) {
err.t = *stack
}
}
return err
}

238
errors_test.go Normal file
View file

@ -0,0 +1,238 @@
package errors
import (
"encoding/json"
"fmt"
"strings"
"testing"
)
var err = fmt.Errorf("test error")
func TestFormat(t *testing.T) {
IncludeBacktrace = false
// %+s check for unwrapped error
e1 := Newf("test")
str := fmt.Sprintf("%+s", e1)
if str != e1.m {
t.Errorf("Error message invalid %s, expected %s", str, e1.m)
}
// %+s check for wrapped error
e2 := Annotatef(e1, "another")
str = fmt.Sprintf("%+s", e2)
val := e2.m + ": " + e2.c.Error()
if str != val {
t.Errorf("Error message invalid %s, expected %s", str, val)
}
// %v check for unwrapped error with trace
IncludeBacktrace = true
e3 := Newf("test1")
str = fmt.Sprintf("%+v", e3)
if !strings.Contains(str, e3.m) {
t.Errorf("Error message %s\n should contain %s", str, e3.m)
}
if !strings.Contains(str, fmt.Sprintf("%+v", e3.t.StackTrace())) {
t.Errorf("Error message %s\n should contain %+v", str, e3.t.StackTrace())
}
}
func TestMarshalJSON(t *testing.T) {
IncludeBacktrace = true
e := Newf("test")
b, err := e.t.StackTrace().MarshalJSON()
if err != nil {
t.Errorf("MarshalJSON failed with error: %+s", err)
}
stack := make([]json.RawMessage, 0)
err = json.Unmarshal(b, &stack)
if err != nil {
t.Errorf("JSON message could not be unmarshaled: %+s", err)
}
if len(stack) != len(e.t) {
t.Errorf("Count of stack frames different after marshaling, expected %d got %d", len(e.t), len(stack))
}
}
func TestAnnotatef(t *testing.T) {
testStr := "Annotatef string"
te := Annotatef(err, testStr)
if te.c != err {
t.Errorf("Invalid parent error %T:%s, expected %T:%s", te.c, te.c, err, err)
}
if te.m != testStr {
t.Errorf("Invalid error message %s, expected %s", te.m, testStr)
}
}
var homeVal = "$HOME"
func TestNewf(t *testing.T) {
testStr := "Newf string"
te := Newf(testStr)
if te.c != nil {
t.Errorf("Invalid parent error %T:%s, expected nil", te.c, te.c)
}
if te.m != testStr {
t.Errorf("Invalid error message %s, expected %s", te.m, testStr)
}
}
func TestErrorf(t *testing.T) {
testStr := "Errorf string"
err := Errorf(testStr)
if te, ok := err.(*Err); ok {
if te.c != nil {
t.Errorf("Invalid parent error %T:%s, expected nil", te.c, te.c)
}
if te.m != testStr {
t.Errorf("Invalid error message %s, expected %s", te.m, testStr)
}
} else {
t.Errorf("Invalid error type returned %T, expected type %T", err, &Err{})
}
}
/*
func TestErr_As(t *testing.T) {
e := Err{m: "test", l: 11, f: "random", t: []uintptr{0x6, 0x6, 0x6}, c: fmt.Errorf("ttt")}
if e.As(&err) {
t.Errorf("%T should not be assertable as %T", err, e)
}
type clone = Err
e1 := clone{}
if !e.As(&e1) {
t.Errorf("%T should be assertable as %T", e, e1)
}
if e1.m != e.m {
t.Errorf("%T message should equal %T's, received %s, expected %s", e1, e, e1.m, e.m)
}
if e1.l != e.l {
t.Errorf("%T line should equal %T's, received %d, expected %d", e1, e, e1.l, e.l)
}
if e1.f != e.f {
t.Errorf("%T file should equal %T's, received %s, expected %s", e1, e, e1.f, e.f)
}
if !bytes.Equal(e1.t, e.t) {
t.Errorf("%T trace should equal %T's, received %2x, expected %2x", e1, e, e1.t, e.t)
}
if e1.c != e.c {
t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e1, e, e1.c, e1.c, e.c, e.c)
}
e2 := &e
if !e.As(&e2) {
t.Errorf("%T should be assertable as %T", e, e2)
}
if e2.m != e.m {
t.Errorf("%T message should equal %T's, received %s, expected %s", e2, e, e2.m, e.m)
}
if e2.l != e.l {
t.Errorf("%T line should equal %T's, received %d, expected %d", e2, e, e2.l, e.l)
}
if e2.f != e.f {
t.Errorf("%T file should equal %T's, received %s, expected %s", e2, e, e2.f, e.f)
}
if !bytes.Equal(e2.t, e.t) {
t.Errorf("%T trace should equal %T's, received %2x, expected %2x", e2, e, e2.t, e.t)
}
if e2.c != e.c {
t.Errorf("%T parent error should equal %T's, received %T[%s], expected %T[%s]", e2, e, e2.c, e2.c, e.c, e.c)
}
}
func TestErr_Error(t *testing.T) {
e := Err{m: "test"}
if e.Error() != e.m {
t.Errorf("Error() returned %s, expected %s", e.Error(), e.m)
}
}
func TestErr_Location(t *testing.T) {
e := Err{l: 11, f: "random"}
if f, l := e.Location(); l != e.l || f != e.f {
t.Errorf("Location() returned: %s:%d, expected: %s:%d", f, l, e.f, e.l)
}
}
func TestErr_StackTrace(t *testing.T) {
e := Err{t: []byte{0x6, 0x6, 0x6}}
if !bytes.Equal(e.StackTrace(), e.t) {
t.Errorf("StackTrace() returned: %2x, expected: %2x", e.StackTrace(), e.t)
}
}
func TestErr_Unwrap(t *testing.T) {
e := Err{c: fmt.Errorf("ttt")}
w := e.Unwrap()
if w != e.c {
t.Errorf("Unwrap() returned: %T[%s], expected: %T[%s]", w, w, e.c, e.c)
}
}
*/
func TestErr_Error(t *testing.T) {
type fields struct {
m string
c error
t stack
}
tests := []struct {
quiet bool
name string
fields fields
want string
}{
{
name: "empty",
fields: fields{},
want: "",
},
{
name: "just text",
fields: fields{m: "test"},
want: "test",
},
{
name: "text with single wrapped error",
fields: fields{m: "test", c: fmt.Errorf("error")},
want: "test: error",
},
{
name: "text with two wrapped errors",
fields: fields{m: "test", c: fmt.Errorf("error: %w", Newf("some error"))},
want: "test: error: some error",
},
{
name: "text with two wrapped errors, but no unwrapping",
quiet: true,
fields: fields{m: "test", c: fmt.Errorf("error: %w", Newf("some error"))},
want: "test",
},
{
name: "text with two wrapped Err errors",
fields: fields{m: "test", c: &Err{m: "error", c: &Err{m: "another error"}}},
want: "test: error: another error",
},
{
name: "text with two wrapped Err errors, without unwrapping",
quiet: true,
fields: fields{m: "test", c: &Err{m: "error", c: &Err{m: "another error"}}},
want: "test",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
IncludeBacktrace = tt.quiet
e := Err{
m: tt.fields.m,
c: tt.fields.c,
t: tt.fields.t,
}
if got := e.Error(); got != tt.want {
t.Errorf("Error() = %v, want %v", got, tt.want)
}
})
}
}

8
go.mod Normal file
View file

@ -0,0 +1,8 @@
module github.com/go-ap/errors
go 1.20
require (
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
github.com/valyala/fastjson v1.6.4
)

1001
http.go Normal file

File diff suppressed because it is too large Load diff

1063
http_test.go Normal file

File diff suppressed because it is too large Load diff

44
join.go Normal file
View file

@ -0,0 +1,44 @@
package errors
import "unsafe"
func Join(errs ...error) error {
n := 0
for _, err := range errs {
if err != nil {
n++
}
}
if n == 0 {
return nil
}
e := &joinError{
errs: make([]error, 0, n),
}
for _, err := range errs {
if err != nil {
e.errs = append(e.errs, err)
}
}
return e
}
type joinError struct {
errs []error
}
func (e *joinError) Error() string {
// Since Join returns nil if every value in errs is nil,
// e.errs cannot be empty.
if len(e.errs) == 1 {
return e.errs[0].Error()
}
b := []byte(e.errs[0].Error())
for _, err := range e.errs[1:] {
b = append(b, ':', ' ')
b = append(b, err.Error()...)
}
// At this point, b has at least one byte '\n'.
return unsafe.String(&b[0], len(b))
}

98
redirects.go Normal file
View file

@ -0,0 +1,98 @@
package errors
import (
"fmt"
"net/http"
)
func SeeOther(u string) *redirect {
return &redirect{s: http.StatusSeeOther, u: u}
}
func NewSeeOther(e error, u string) *redirect {
return &redirect{c: e, s: http.StatusSeeOther, u: u}
}
func Found(u string) *redirect {
return &redirect{s: http.StatusFound, u: u}
}
func NewFound(e error, u string) *redirect {
return &redirect{c: e, s: http.StatusFound, u: u}
}
func MovedPermanently(u string) *redirect {
return &redirect{s: http.StatusMovedPermanently, u: u}
}
func NewMovedPermanently(e error, u string) *redirect {
return &redirect{c: e, s: http.StatusMovedPermanently, u: u}
}
func NotModified(u string) *redirect {
return &redirect{s: http.StatusNotModified, u: u}
}
func NewNotModified(e error, u string) *redirect {
return &redirect{c: e, s: http.StatusNotModified, u: u}
}
func TemporaryRedirect(u string) *redirect {
return &redirect{s: http.StatusTemporaryRedirect, u: u}
}
func NewTemporaryRedirect(e error, u string) *redirect {
return &redirect{c: e, s: http.StatusTemporaryRedirect, u: u}
}
func PermanentRedirect(u string) *redirect {
return &redirect{s: http.StatusPermanentRedirect, u: u}
}
func NewPermanentRedirect(e error, u string) *redirect {
return &redirect{c: e, s: http.StatusPermanentRedirect, u: u}
}
type redirect struct {
c error
u string
s int
}
func (r redirect) Error() string {
if r.c == nil {
return fmt.Sprintf("Redirect %d to %s", r.s, r.u)
}
return fmt.Sprintf("Redirect %d to %s: %s", r.s, r.u, r.c)
}
// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver
//
// if the underlying logic of the receiver's type can understand it.
//
// In this case we're converting a forbidden to its underlying type Err.
func (r *redirect) As(err interface{}) bool {
switch x := err.(type) {
case **redirect:
*x = r
case *redirect:
*x = *r
default:
return false
}
return true
}
func (r redirect) Is(e error) bool {
rr := redirect{}
return As(e, &rr) && r.s == rr.s
}
func IsRedirect(e error) bool {
_, okp := e.(*redirect)
_, oks := e.(redirect)
return okp || oks || As(e, &redirect{})
}
func IsNotModified(e error) bool {
ep, okp := e.(*redirect)
es, oks := e.(redirect)
ae := redirect{}
return (okp && ep.s == http.StatusNotModified) ||
(oks && es.s == http.StatusNotModified) ||
(As(e, &ae) && ae.s == http.StatusNotModified)
}
func (r redirect) Unwrap() error {
return r.Unwrap()
}

219
stack.go Normal file
View file

@ -0,0 +1,219 @@
package errors
import (
"bytes"
"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 (<funcname>\n\t<path>)
// %+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, 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':
f.Format(s, 's')
io.WriteString(s, ":")
f.Format(s, 'd')
}
}
// 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
}
func (f Frame) MarshalJSON() ([]byte, error) {
name := f.name()
w := bytes.NewBuffer(nil)
if name == "unknown" {
w.WriteByte('"')
w.WriteString(name)
w.WriteByte('"')
return w.Bytes(), nil
}
w.WriteByte('{')
w.WriteString("\"function\": ")
w.WriteByte('"')
w.WriteString(funcname(name))
w.WriteByte('"')
w.WriteByte(',')
w.WriteString("\"file\": ")
w.WriteByte('"')
w.WriteString(f.file())
w.WriteByte('"')
w.WriteByte(',')
w.WriteString("\"line\": ")
w.WriteString(fmt.Sprintf("%d", f.line()))
w.WriteByte('}')
return w.Bytes(), 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('#'):
fmt.Fprintf(s, "%#v", []Frame(st))
default:
st.formatSlice(s, verb)
}
case 's':
st.formatSlice(s, verb)
}
}
func (st StackTrace) MarshalJSON() ([]byte, error) {
w := bytes.NewBuffer(nil)
w.WriteByte('[')
for i, f := range st {
b, _ := f.MarshalJSON()
w.Write(b)
if i == len(st)-1 {
break
}
w.WriteByte(',')
}
w.WriteByte(']')
return w.Bytes(), nil
}
// 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)
}
}
}
}
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(skip int) *stack {
const depth = 32
var pcs [depth]uintptr
n := runtime.Callers(skip+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:]
}

263
stack_test.go Normal file
View file

@ -0,0 +1,263 @@
package errors
import (
"fmt"
"regexp"
"runtime"
"strings"
"testing"
)
var initpc = caller()
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)
}
}
}
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",
"github.com/go-ap/errors.init\n" +
"\t.+/errors/stack_test.go",
}, {
0,
"%s",
"unknown",
}, {
0,
"%+s",
"unknown",
}, {
initpc,
"%d",
"11",
}, {
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:11",
}, {
initpc,
"%+v",
"github.com/go-ap/errors.init\n" +
"\t.+/errors/stack_test.go:11",
}, {
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"},
{"github.com/go-ap/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
}{{
Newf("ooh"), []string{
"github.com/go-ap/errors.TestStackTrace\n" +
"\t.+/errors/stack_test.go:145",
},
}, {
wrap(Newf("ooh"), "ahh"), []string{
"github.com/go-ap/errors.TestStackTrace\n" +
"\t.+/errors/stack_test.go:150", // this is the stack of Wrap, not New
},
}, {
func() error { return Newf("ooh") }(), []string{
`github.com/go-ap/errors.TestStackTrace.func1` +
"\n\t.+/errors/stack_test.go:155", // this is the stack of New
"github.com/go-ap/errors.TestStackTrace\n" +
"\t.+/errors/stack_test.go:155", // this is the stack of New's caller
},
}}
t.Skipf(`TODO(marius): This needs some more work
As going one level up the stack in stack.callers() removes meaningful information from the tests`)
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()
if len(st) == 0 {
continue
}
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:183 stack_test.go:230\]`,
}, {
stackTrace()[:2],
"%+v",
"\n" +
"github.com/go-ap/errors.stackTrace\n" +
"\t.+/errors/stack_test.go:183\n" +
"github.com/go-ap/errors.TestStackTraceFormat\n" +
"\t.+/errors/stack_test.go:234",
}, {
stackTrace()[:2],
"%#v",
`\[\]errors.Frame{stack_test.go:183, stack_test.go:242}`,
}}
t.Skipf(`TODO(marius): This needs some more work
As going one level up the stack in stack.callers() removes meaningful information from the tests`)
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)
}