Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:

env:
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
GOLANGCI_LINT_VERSION: v1.57.2
GOLANGCI_LINT_VERSION: v2.6.2

jobs:
lint:
Expand All @@ -32,7 +32,7 @@ jobs:
run: go mod tidy && git diff --exit-code

- name: golangci-lint
uses: golangci/golangci-lint-action@v4
uses: golangci/golangci-lint-action@v8
with:
version: ${{ env.GOLANGCI_LINT_VERSION }}
skip-cache: true # cache/restore is done by actions/setup-go@v3 step
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@ jobs:
strategy:
matrix:
go-version:
- 1.21.x
- 1.24.x
- 1.25.x
os: [ ubuntu-latest ]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@master

- name: Set up Go
uses: actions/setup-go@v3
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache: true # caching and restoring go modules and build outputs
Expand Down
188 changes: 74 additions & 114 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,125 +1,85 @@
version: "2"
run:
tests: true
linters:
# Please, do not use `enable-all`: it's deprecated and will be removed soon.
# Inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint.
# Full list of linters - https://golangci-lint.run/usage/linters
disable-all: true
default: none
enable:
- bodyclose # https://github.com/timakin/bodyclose
- gomodguard
- errcheck # Mandatory. Do not disable.
- bodyclose
- errcheck
- errorlint # Wrapping
- gocritic
- goimports
- gomodguard
- gosec
- gosimple
- govet
- ineffassign
- makezero
- misspell
- nestif # Deep nesting avoidance
- noctx
- nolintlint
- ineffassign # Mandatory. Do not disable.
- staticcheck # Mandatory. Do not disable.
- stylecheck
- typecheck
- prealloc
- staticcheck
- testifylint # Helps catch expected/got reversals
- unused

# Other linters:
# - dogsled
# - dupl
# - exportloopref
# - exhaustive # e.g. missing cases in switch of type
# - funlen
# - gochecknoinits
# - gocognit
# - goconst
# - gocyclo
# - goerr113
# - gofmt
# - goprintffuncname
# - lll
# - misspell
# - nakedret
# - nlreturn
# - prealloc
# - revive
# - rowserrcheck
# - stylecheck
# - unconvert
# - unparam

linters-settings:
gocritic:
enabled-tags:
- diagnostic
- experimental
- opinionated
- performance
- style
disabled-checks:
- dupImport # https://github.com/go-critic/go-critic/issues/845
- whyNoLint # checked by nolintlint linter
- hugeParam # TODO(vtopc): configure(80 bytes is probably not so much) and enable.
- rangeValCopy # TODO(vtopc): configure(disable for tests) and enable.
- appendAssign
- commentedOutCode

errcheck:
# List of functions to exclude from checking, where each entry is a single function to exclude.
# See https://github.com/kisielk/errcheck#excluding-functions for details.
exclude-functions:
- (io.Closer).Close
- (io.ReadCloser).Close

govet:
enable-all: true
disable:
- shadow
- fieldalignment

gomodguard:
blocked:
# List of blocked modules.
# Default: []
modules:
- github.com/golang/protobuf:
recommendations:
- google.golang.org/protobuf
reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules"
- github.com/pkg/errors:
recommendations:
- errors
- github.com/mailgun/errors
reason: "Deprecated"

stylecheck:
# https://staticcheck.io/docs/options#checks
checks: ["all"]

settings:
errcheck:
exclude-functions:
- (io.Closer).Close
- (io.ReadCloser).Close
gocritic:
enabled-tags:
- diagnostic
- experimental
- opinionated
- performance
- style
testifylint:
disable:
- error-nil # prefer explicit checks instead for error contract adherence
- error-is-as # prefer explicit checks instead for error contract adherence
gomodguard:
blocked:
modules:
- github.com/golang/protobuf:
recommendations:
- google.golang.org/protobuf
reason: see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules
- github.com/pkg/errors:
recommendations:
- errors
- github.com/mailgun/errors
reason: Deprecated
govet:
disable:
- fieldalignment
enable-all: true
staticcheck:
checks:
- all
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- errcheck
path: '(.+)_test\.go'
paths:
- third_party$
- builtin$
- examples$
issues:
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-issues-per-linter: 0

# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same-issues: 50

exclude:
# Some packages have deprecated fields which continue to be useful
- SA1019

exclude-rules:
# Exclude some rules from tests.
- path: '_test\.go$'
linters:
- gosec
- noctx
- path: '_test\.go$'
text: "unnamedResult:"
- path: '.*mxresolv.*'
linters:
- gosec


run:
# include test files or not, default is true
tests: true

# Timeout for analysis, e.g. 30s, 5m.
# Default: 1m
timeout: 5m
formatters:
enable:
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
GOLANGCI_LINT = $(GOPATH)/bin/golangci-lint
GOLANGCI_LINT_VERSION = v1.57.2
GOLANGCI_LINT_VERSION = v2.6.2

.PHONY: lint
lint: $(GOLANGCI_LINT)
Expand All @@ -10,4 +10,4 @@ $(GOLANGCI_LINT):

.PHONY: test
test:
go test -p 1 ./... -short -race -timeout 1m -count=1
go test -short -race -timeout 1m ./...
115 changes: 98 additions & 17 deletions errors_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,57 @@
package errors_test

import (
"fmt"
"testing"

stderr "errors"

"github.com/mailgun/errors"
"github.com/mailgun/errors/callstack"
"github.com/stretchr/testify/assert"
)

// Ensure errors.Is remains stdlib compliant
func TestIs(t *testing.T) {
err := errors.New("bottom")
top := fmt.Errorf("top: %w", err)

assert.True(t, stderr.Is(top, err))
assert.True(t, errors.Is(top, err))
}

// Ensure errors.As remains stdlib compliant
func TestAs(t *testing.T) {
err := &ErrTest{Msg: "bottom"}
top := fmt.Errorf("top: %w", err)

var exp *ErrTest
assert.True(t, stderr.As(top, &exp))
assert.True(t, errors.As(top, &exp))
}

func TestLast(t *testing.T) {
err := errors.New("bottom")
err = errors.Wrap(err, "last")
err = errors.Wrap(err, "second")
err = errors.Wrap(err, "first")
err = errors.Errorf("wrapped: %w", err)

// errors.As() returns the "first" error in the chain with a stack trace
var first callstack.HasStackTrace
assert.True(t, errors.As(err, &first))
assert.Equal(t, "first: second: last: bottom", first.(error).Error())

// errors.Last() returns the last error in the chain with a stack trace
var last callstack.HasStackTrace
assert.True(t, errors.Last(err, &last))
assert.Equal(t, "last: bottom", last.(error).Error())

// If no stack trace is found, then should not set target and should return false
assert.False(t, errors.Last(errors.New("no stack"), &last))
assert.Equal(t, "last: bottom", last.(error).Error())
}

type ErrTest struct {
Msg string
}
Expand Down Expand Up @@ -39,24 +83,61 @@ func (e *ErrHasFields) HasFields() map[string]any {
return e.F
}

func TestLast(t *testing.T) {
err := errors.New("bottom")
err = errors.Wrap(err, "last")
err = errors.Wrap(err, "second")
err = errors.Wrap(err, "first")
err = errors.Errorf("wrapped: %w", err)
// Benchmarks for comparison purposes

// errors.As() returns the "first" error in the chain with a stack trace
var first callstack.HasStackTrace
assert.True(t, errors.As(err, &first))
assert.Equal(t, "first: second: last: bottom", first.(error).Error())
/*
go test -bench=. -benchmem -count=5 -run="^$" ./...
goos: darwin
goarch: arm64
pkg: github.com/mailgun/errors
cpu: Apple M3 Pro
BenchmarkErrors-12 4825278 221.8 ns/op 328 B/op 3 allocs/op
BenchmarkErrors-12 5548051 216.4 ns/op 328 B/op 3 allocs/op
BenchmarkErrors-12 5548090 215.5 ns/op 328 B/op 3 allocs/op
BenchmarkErrors-12 5548306 215.7 ns/op 328 B/op 3 allocs/op
BenchmarkErrors-12 5557860 215.8 ns/op 328 B/op 3 allocs/op
*/
func BenchmarkErrorsWrap(b *testing.B) {
base := errors.New("init")
for b.Loop() {
errors.Wrap(base, "loop")
}
}

// errors.Last() returns the last error in the chain with a stack trace
var last callstack.HasStackTrace
assert.True(t, errors.Last(err, &last))
assert.Equal(t, "last: bottom", last.(error).Error())
/*
go test -bench=. -benchmem -count=5 -run="^$" ./...
goos: darwin
goarch: arm64
pkg: github.com/mailgun/errors
cpu: Apple M3 Pro
BenchmarkErrorsWrapf-12 4733302 252.8 ns/op 336 B/op 4 allocs/op
BenchmarkErrorsWrapf-12 4750388 252.1 ns/op 336 B/op 4 allocs/op
BenchmarkErrorsWrapf-12 4720851 251.7 ns/op 336 B/op 4 allocs/op
BenchmarkErrorsWrapf-12 4740823 252.3 ns/op 336 B/op 4 allocs/op
BenchmarkErrorsWrapf-12 4753670 254.1 ns/op 336 B/op 4 allocs/op
*/
func BenchmarkErrorsWrapf(b *testing.B) {
base := errors.New("init")
for b.Loop() {
errors.Wrapf(base, "loop %s", "two")
}
}

// If no stack trace is found, then should not set target and should return false
assert.False(t, errors.Last(errors.New("no stack"), &last))
assert.Equal(t, "last: bottom", last.(error).Error())
/*
go test -bench=. -benchmem -count=5 -run="^$" ./...
goos: darwin
goarch: arm64
pkg: github.com/mailgun/errors
cpu: Apple M3 Pro
BenchmarkErrorsStack-12 5713897 210.4 ns/op 304 B/op 3 allocs/op
BenchmarkErrorsStack-12 5677599 210.1 ns/op 304 B/op 3 allocs/op
BenchmarkErrorsStack-12 5701461 210.1 ns/op 304 B/op 3 allocs/op
BenchmarkErrorsStack-12 5655940 210.1 ns/op 304 B/op 3 allocs/op
BenchmarkErrorsStack-12 5574022 210.8 ns/op 304 B/op 3 allocs/op
*/
func BenchmarkErrorsStack(b *testing.B) {
base := errors.New("init")
for b.Loop() {
_ = errors.Stack(base)
}
}
Loading