diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml
index 0e6bddc..d750b9d 100644
--- a/.github/workflows/bench.yml
+++ b/.github/workflows/bench.yml
@@ -21,7 +21,7 @@ env:
GO111MODULE: "on"
CACHE_BENCHMARK: "off" # Enables benchmark result reuse between runs, may skew latency results.
RUN_BASE_BENCHMARK: "on" # Runs benchmark for PR base in case benchmark result is missing.
- GO_VERSION: 1.18.x
+ GO_VERSION: 1.19.x
jobs:
bench:
runs-on: ubuntu-latest
diff --git a/.github/workflows/cloc.yml b/.github/workflows/cloc.yml
index d6ff20d..619ca74 100644
--- a/.github/workflows/cloc.yml
+++ b/.github/workflows/cloc.yml
@@ -24,7 +24,9 @@ jobs:
- name: Count Lines Of Code
id: loc
run: |
- curl -sLO https://github.com/vearutop/sccdiff/releases/download/v1.0.1/linux_amd64.tar.gz && tar xf linux_amd64.tar.gz
+ curl -sLO https://github.com/vearutop/sccdiff/releases/download/v1.0.3/linux_amd64.tar.gz && tar xf linux_amd64.tar.gz
+ sccdiff_hash=$(git hash-object ./sccdiff)
+ [ "$sccdiff_hash" == "ae8a07b687bd3dba60861584efe724351aa7ff63" ] || (echo "::error::unexpected hash for sccdiff, possible tampering: $sccdiff_hash" && exit 1)
OUTPUT=$(cd pr && ../sccdiff -basedir ../base)
echo "${OUTPUT}"
OUTPUT="${OUTPUT//$'\n'/%0A}"
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
index b4f7600..bf0bcdb 100644
--- a/.github/workflows/golangci-lint.yml
+++ b/.github/workflows/golangci-lint.yml
@@ -21,13 +21,13 @@ jobs:
steps:
- uses: actions/setup-go@v3
with:
- go-version: 1.18.x
+ go-version: 1.19.x
- uses: actions/checkout@v2
- name: golangci-lint
- uses: golangci/golangci-lint-action@v3.1.0
+ uses: golangci/golangci-lint-action@v3.2.0
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
- version: v1.45.2
+ version: v1.50.0
# Optional: working directory, useful for monorepos
# working-directory: somedir
diff --git a/.github/workflows/gorelease.yml b/.github/workflows/gorelease.yml
index 429d5ed..6267500 100644
--- a/.github/workflows/gorelease.yml
+++ b/.github/workflows/gorelease.yml
@@ -9,7 +9,7 @@ concurrency:
cancel-in-progress: true
env:
- GO_VERSION: 1.18.x
+ GO_VERSION: 1.19.x
jobs:
gorelease:
runs-on: ubuntu-latest
@@ -35,12 +35,12 @@ jobs:
with:
path: |
~/go/bin/gorelease
- key: ${{ runner.os }}-gorelease
+ key: ${{ runner.os }}-gorelease-generic
- name: Gorelease
id: gorelease
run: |
test -e ~/go/bin/gorelease || go install golang.org/x/exp/cmd/gorelease@latest
- OUTPUT=$(gorelease || exit 0)
+ OUTPUT=$(gorelease 2>&1 || exit 0)
echo "${OUTPUT}"
OUTPUT="${OUTPUT//$'\n'/%0A}"
echo "::set-output name=report::$OUTPUT"
diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml
index a1d30c9..94441bd 100644
--- a/.github/workflows/test-unit.yml
+++ b/.github/workflows/test-unit.yml
@@ -16,11 +16,12 @@ env:
GO111MODULE: "on"
RUN_BASE_COVERAGE: "on" # Runs test for PR base in case base test coverage is missing.
COV_GO_VERSION: 1.18.x # Version of Go to collect coverage
+ TARGET_DELTA_COV: 90 # Target coverage of changed lines, in percents
jobs:
test:
strategy:
matrix:
- go-version: [ 1.16.x, 1.17.x, 1.18.x, tip ]
+ go-version: [ 1.16.x, 1.17.x, 1.18.x, 1.19.x ]
runs-on: ubuntu-latest
steps:
- name: Install Go stable
@@ -28,6 +29,7 @@ jobs:
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
+
- name: Install Go tip
if: matrix.go-version == 'tip'
run: |
@@ -37,8 +39,10 @@ jobs:
tar -C ~/sdk/gotip -xzf gotip.tar.gz
~/sdk/gotip/bin/go version
echo "PATH=$HOME/go/bin:$HOME/sdk/gotip/bin/:$PATH" >> $GITHUB_ENV
+
- name: Checkout code
uses: actions/checkout@v2
+
- name: Go cache
uses: actions/cache@v2
with:
@@ -51,44 +55,57 @@ jobs:
key: ${{ runner.os }}-go-cache-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-cache
+
- name: Restore base test coverage
id: base-coverage
- if: matrix.go-version == env.COV_GO_VERSION
+ if: matrix.go-version == env.COV_GO_VERSION && github.event.pull_request.base.sha != ''
uses: actions/cache@v2
with:
path: |
unit-base.txt
# Use base sha for PR or new commit hash for master/main push in test result key.
key: ${{ runner.os }}-unit-test-coverage-${{ (github.event.pull_request.base.sha != github.event.after) && github.event.pull_request.base.sha || github.event.after }}
- - name: Checkout base code
- if: matrix.go-version == env.COV_GO_VERSION && env.RUN_BASE_COVERAGE == 'on' && steps.base-coverage.outputs.cache-hit != 'true' && github.event.pull_request.base.sha != ''
- uses: actions/checkout@v2
- with:
- ref: ${{ github.event.pull_request.base.sha }}
- path: __base
+
- name: Run test for base code
if: matrix.go-version == env.COV_GO_VERSION && env.RUN_BASE_COVERAGE == 'on' && steps.base-coverage.outputs.cache-hit != 'true' && github.event.pull_request.base.sha != ''
run: |
- cd __base
- make | grep test-unit && (make test-unit && go tool cover -func=./unit.coverprofile | sed -e 's/.go:[0-9]*:\t/.go\t/g' | sed -e 's/\t\t*/\t/g' > ../unit-base.txt) || echo "No test-unit in base"
+ git fetch origin master ${{ github.event.pull_request.base.sha }}
+ HEAD=$(git rev-parse HEAD)
+ git reset --hard ${{ github.event.pull_request.base.sha }}
+ (make test-unit && go tool cover -func=./unit.coverprofile > unit-base.txt) || echo "No test-unit in base"
+ git reset --hard $HEAD
+
- name: Test
id: test
run: |
make test-unit
- go tool cover -func=./unit.coverprofile | sed -e 's/.go:[0-9]*:\t/.go\t/g' | sed -e 's/\t\t*/\t/g' > unit.txt
- OUTPUT=$(test -e unit-base.txt && (diff unit-base.txt unit.txt || exit 0) || cat unit.txt)
- echo "${OUTPUT}"
- OUTPUT="${OUTPUT//$'\n'/%0A}"
+ go tool cover -func=./unit.coverprofile > unit.txt
TOTAL=$(grep 'total:' unit.txt)
echo "${TOTAL}"
- echo "::set-output name=diff::$OUTPUT"
echo "::set-output name=total::$TOTAL"
- - name: Store base coverage
- if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' }}
- run: cp unit.txt unit-base.txt
+
+ - name: Annotate missing test coverage
+ id: annotate
+ if: matrix.go-version == env.COV_GO_VERSION && github.event.pull_request.base.sha != ''
+ run: |
+ curl -sLO https://github.com/vearutop/gocovdiff/releases/download/v1.3.6/linux_amd64.tar.gz && tar xf linux_amd64.tar.gz
+ gocovdiff_hash=$(git hash-object ./gocovdiff)
+ [ "$gocovdiff_hash" == "8e507e0d671d4d6dfb3612309b72b163492f28eb" ] || (echo "::error::unexpected hash for gocovdiff, possible tampering: $gocovdiff_hash" && exit 1)
+ git fetch origin master ${{ github.event.pull_request.base.sha }}
+ REP=$(./gocovdiff -cov unit.coverprofile -gha-annotations gha-unit.txt -delta-cov-file delta-cov-unit.txt -target-delta-cov ${TARGET_DELTA_COV})
+ echo "${REP}"
+ REP="${REP//$'\n'/%0A}"
+ cat gha-unit.txt
+ DIFF=$(test -e unit-base.txt && ./gocovdiff -func-cov unit.txt -func-base-cov unit-base.txt || echo "Missing base coverage file")
+ DIFF="${DIFF//$'\n'/%0A}"
+ TOTAL=$(cat delta-cov-unit.txt)
+ echo "::set-output name=rep::$REP"
+ echo "::set-output name=diff::$DIFF"
+ echo "::set-output name=total::$TOTAL"
+
- name: Comment Test Coverage
continue-on-error: true
- if: matrix.go-version == env.COV_GO_VERSION
+ if: matrix.go-version == env.COV_GO_VERSION && github.event.pull_request.base.sha != ''
uses: marocchino/sticky-pull-request-comment@v2
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -96,13 +113,23 @@ jobs:
message: |
### Unit Test Coverage
${{ steps.test.outputs.total }}
+ ${{ steps.annotate.outputs.total }}
+ Coverage of changed lines
+
+ ${{ steps.annotate.outputs.rep }}
+
+
+
Coverage diff with base branch
- ```diff
- ${{ steps.test.outputs.diff }}
- ```
+ ${{ steps.annotate.outputs.diff }}
+
+ - name: Store base coverage
+ if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' }}
+ run: cp unit.txt unit-base.txt
+
- name: Upload code coverage
if: matrix.go-version == env.COV_GO_VERSION
uses: codecov/codecov-action@v1
diff --git a/.golangci.yml b/.golangci.yml
index 97710e1..219b387 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -37,6 +37,14 @@ linters:
- tagliatelle
- errname
- ireturn
+ - exhaustruct
+ - nonamedreturns
+ - nosnakecase
+ - structcheck
+ - varcheck
+ - deadcode
+ - testableexamples
+ - dupword
issues:
exclude-use-default: false
@@ -48,5 +56,13 @@ issues:
- noctx
- funlen
- dupl
+ - structcheck
+ - unused
+ - unparam
+ - nosnakecase
path: "_test.go"
+ - linters:
+ - errcheck # Error checking omitted for brevity.
+ - gosec
+ path: "example_"
diff --git a/Makefile b/Makefile
index 77a5ec3..80dc683 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-#GOLANGCI_LINT_VERSION := "v1.45.2" # Optional configuration to pinpoint golangci-lint version.
+#GOLANGCI_LINT_VERSION := "v1.50.0" # Optional configuration to pinpoint golangci-lint version.
# The head of Makefile determines location of dev-go to include standard targets.
GO ?= go
diff --git a/error.go b/error.go
index 86111fe..5a9b3e3 100644
--- a/error.go
+++ b/error.go
@@ -87,7 +87,7 @@ func WrapError(ctx context.Context, err error, message string, keysAndValues ...
//
// LogError fields from context are also added to error structured data.
func NewError(ctx context.Context, message string, keysAndValues ...interface{}) error {
- // nolint:goerr113 // Static errors can be used with WrapError.
+ //nolint:goerr113 // Static errors can be used with WrapError.
err := errors.New(message)
se, ok := newError(ctx, err, keysAndValues...)
@@ -236,6 +236,8 @@ func (se structuredError) MarshalJSON() ([]byte, error) {
}
// SentinelError is a constant error.
+//
+// See https://dave.cheney.net/2016/04/07/constant-errors for more details.
type SentinelError string
// Error returns error message.
@@ -300,3 +302,64 @@ func (le labeledError) As(v interface{}) bool {
func (le labeledError) Unwrap() error {
return le.err
}
+
+// MultiError creates an error with multiple unwrappables.
+//
+// Secondary errors could be checked with errors.Is, errors.As.
+// Error message remains the same with primary error.
+//
+// Multi errors can be used to augment error with multiple
+// checkable perks, without a limitation of single wrapping inheritance.
+func MultiError(primary error, secondary ...error) error {
+ return multi{
+ primary: primary,
+ secondary: secondary,
+ }
+}
+
+type multi struct {
+ primary error
+ secondary []error
+}
+
+// Error returns message.
+func (le multi) Error() string {
+ return le.primary.Error()
+}
+
+// Is returns true if err matches primary error or any of secondary.
+func (le multi) Is(err error) bool {
+ if errors.Is(le.primary, err) {
+ return true
+ }
+
+ for _, l := range le.secondary {
+ if errors.Is(err, l) {
+ return true
+ }
+ }
+
+ return false
+}
+
+// As returns true if primary error or any of secondary can be assigned to v.
+//
+// If multiple assignations are possible, only first one is performed.
+func (le multi) As(v interface{}) bool {
+ if errors.As(le.primary, v) {
+ return true
+ }
+
+ for _, l := range le.secondary {
+ if errors.As(l, v) {
+ return true
+ }
+ }
+
+ return false
+}
+
+// Unwrap returns primary error.
+func (le multi) Unwrap() error {
+ return le.primary
+}
diff --git a/error_test.go b/error_test.go
index 5c7bb11..86eed42 100644
--- a/error_test.go
+++ b/error_test.go
@@ -259,3 +259,26 @@ func TestTuples_Fields(t *testing.T) {
assert.Equal(t, map[string]interface{}{"a": 123, "b": 456},
ctxd.Tuples{"a", 123, "b", 456}.Fields()) // All good.
}
+
+func TestNewMulti(t *testing.T) {
+ errPrimary := errors.New("failed")
+ errSecondary1 := ctxd.SentinelError("miserably")
+ errSecondary2 := ctxd.SentinelError("hopelessly")
+ errSecondary3 := ctxd.SentinelError("deadly")
+
+ err := ctxd.MultiError(fmt.Errorf("oops: %w", errPrimary), errSecondary1, errSecondary2)
+
+ assert.True(t, errors.Is(err, errPrimary))
+ assert.True(t, errors.Is(err, errSecondary1))
+ assert.True(t, errors.Is(err, errSecondary2))
+ assert.False(t, errors.Is(err, errSecondary3))
+
+ // Labels do not implicitly contribute to error message.
+ assert.Equal(t, "oops: failed", err.Error())
+
+ // If there are two matches, only first is returned.
+ var errSentinel ctxd.SentinelError
+
+ assert.True(t, errors.As(err, &errSentinel))
+ assert.Equal(t, "miserably", string(errSentinel))
+}
diff --git a/go.mod b/go.mod
index 1d8e1a7..85c4f21 100644
--- a/go.mod
+++ b/go.mod
@@ -3,13 +3,13 @@ module github.com/bool64/ctxd
go 1.17
require (
- github.com/bool64/dev v0.2.5
- github.com/stretchr/testify v1.7.1
- github.com/swaggest/usecase v1.1.2
+ github.com/bool64/dev v0.2.22
+ github.com/stretchr/testify v1.8.0
+ github.com/swaggest/usecase v1.2.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
- gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index 7a4a08c..f391c0d 100644
--- a/go.sum
+++ b/go.sum
@@ -1,20 +1,20 @@
-github.com/bool64/dev v0.2.5 h1:H0bylghwcjDBBhEwSFTjArEO9Dr8cCaB54QSOF7esOA=
-github.com/bool64/dev v0.2.5/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
+github.com/bool64/dev v0.2.20/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
+github.com/bool64/dev v0.2.22 h1:YJFKBRKplkt+0Emq/5Xk1Z5QRmMNzc1UOJkR3rxJksA=
+github.com/bool64/dev v0.2.22/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/swaggest/usecase v1.1.2 h1:2LfuSyjYtPtpHnxqPwV87/eunbhGBC5HKdRp8/fINBk=
-github.com/swaggest/usecase v1.1.2/go.mod h1:abZWuMFYujaeLDODqRySJZpWD/ugsnE3Wj9K6jUeCjo=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/swaggest/usecase v1.2.0 h1:cHVFqxIbHfyTXp02JmWXk+ZADaSa87UZP+b3qL5Nz90=
+github.com/swaggest/usecase v1.2.0/go.mod h1:oc5+QoAxG3Et5Gl9lRXgEOm00l4VN9gdVQSMIa5EeLY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=