686 lines
14 KiB
Go
686 lines
14 KiB
Go
// Copyright (c) 2017-2021 Uber Technologies, Inc.
|
|
//
|
|
// 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.
|
|
|
|
package multierr
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// richFormatError is an error that prints a different output depending on
|
|
// whether %v or %+v was used.
|
|
type richFormatError struct{}
|
|
|
|
func (r richFormatError) Error() string {
|
|
return fmt.Sprint(r)
|
|
}
|
|
|
|
func (richFormatError) Format(f fmt.State, c rune) {
|
|
if c == 'v' && f.Flag('+') {
|
|
io.WriteString(f, "multiline\nmessage\nwith plus")
|
|
} else {
|
|
io.WriteString(f, "without plus")
|
|
}
|
|
}
|
|
|
|
func appendN(initial, err error, n int) error {
|
|
errs := initial
|
|
for i := 0; i < n; i++ {
|
|
errs = Append(errs, err)
|
|
}
|
|
return errs
|
|
}
|
|
|
|
func newMultiErr(errors ...error) error {
|
|
return &multiError{errors: errors}
|
|
}
|
|
|
|
func TestCombine(t *testing.T) {
|
|
tests := []struct {
|
|
// Input
|
|
giveErrors []error
|
|
|
|
// Resulting error
|
|
wantError error
|
|
|
|
// %+v and %v string representations
|
|
wantMultiline string
|
|
wantSingleline string
|
|
}{
|
|
{
|
|
giveErrors: nil,
|
|
wantError: nil,
|
|
},
|
|
{
|
|
giveErrors: []error{},
|
|
wantError: nil,
|
|
},
|
|
{
|
|
giveErrors: []error{
|
|
errors.New("foo"),
|
|
nil,
|
|
newMultiErr(
|
|
errors.New("bar"),
|
|
),
|
|
nil,
|
|
},
|
|
wantError: newMultiErr(
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
),
|
|
wantMultiline: "the following errors occurred:\n" +
|
|
" - foo\n" +
|
|
" - bar",
|
|
wantSingleline: "foo; bar",
|
|
},
|
|
{
|
|
giveErrors: []error{
|
|
errors.New("foo"),
|
|
newMultiErr(
|
|
errors.New("bar"),
|
|
),
|
|
},
|
|
wantError: newMultiErr(
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
),
|
|
wantMultiline: "the following errors occurred:\n" +
|
|
" - foo\n" +
|
|
" - bar",
|
|
wantSingleline: "foo; bar",
|
|
},
|
|
{
|
|
giveErrors: []error{errors.New("great sadness")},
|
|
wantError: errors.New("great sadness"),
|
|
wantMultiline: "great sadness",
|
|
wantSingleline: "great sadness",
|
|
},
|
|
{
|
|
giveErrors: []error{
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
},
|
|
wantError: newMultiErr(
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
),
|
|
wantMultiline: "the following errors occurred:\n" +
|
|
" - foo\n" +
|
|
" - bar",
|
|
wantSingleline: "foo; bar",
|
|
},
|
|
{
|
|
giveErrors: []error{
|
|
errors.New("great sadness"),
|
|
errors.New("multi\n line\nerror message"),
|
|
errors.New("single line error message"),
|
|
},
|
|
wantError: newMultiErr(
|
|
errors.New("great sadness"),
|
|
errors.New("multi\n line\nerror message"),
|
|
errors.New("single line error message"),
|
|
),
|
|
wantMultiline: "the following errors occurred:\n" +
|
|
" - great sadness\n" +
|
|
" - multi\n" +
|
|
" line\n" +
|
|
" error message\n" +
|
|
" - single line error message",
|
|
wantSingleline: "great sadness; " +
|
|
"multi\n line\nerror message; " +
|
|
"single line error message",
|
|
},
|
|
{
|
|
giveErrors: []error{
|
|
errors.New("foo"),
|
|
newMultiErr(
|
|
errors.New("bar"),
|
|
errors.New("baz"),
|
|
),
|
|
errors.New("qux"),
|
|
},
|
|
wantError: newMultiErr(
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
errors.New("baz"),
|
|
errors.New("qux"),
|
|
),
|
|
wantMultiline: "the following errors occurred:\n" +
|
|
" - foo\n" +
|
|
" - bar\n" +
|
|
" - baz\n" +
|
|
" - qux",
|
|
wantSingleline: "foo; bar; baz; qux",
|
|
},
|
|
{
|
|
giveErrors: []error{
|
|
errors.New("foo"),
|
|
nil,
|
|
newMultiErr(
|
|
errors.New("bar"),
|
|
),
|
|
nil,
|
|
},
|
|
wantError: newMultiErr(
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
),
|
|
wantMultiline: "the following errors occurred:\n" +
|
|
" - foo\n" +
|
|
" - bar",
|
|
wantSingleline: "foo; bar",
|
|
},
|
|
{
|
|
giveErrors: []error{
|
|
errors.New("foo"),
|
|
newMultiErr(
|
|
errors.New("bar"),
|
|
),
|
|
},
|
|
wantError: newMultiErr(
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
),
|
|
wantMultiline: "the following errors occurred:\n" +
|
|
" - foo\n" +
|
|
" - bar",
|
|
wantSingleline: "foo; bar",
|
|
},
|
|
{
|
|
giveErrors: []error{
|
|
errors.New("foo"),
|
|
richFormatError{},
|
|
errors.New("bar"),
|
|
},
|
|
wantError: newMultiErr(
|
|
errors.New("foo"),
|
|
richFormatError{},
|
|
errors.New("bar"),
|
|
),
|
|
wantMultiline: "the following errors occurred:\n" +
|
|
" - foo\n" +
|
|
" - multiline\n" +
|
|
" message\n" +
|
|
" with plus\n" +
|
|
" - bar",
|
|
wantSingleline: "foo; without plus; bar",
|
|
},
|
|
}
|
|
|
|
for i, tt := range tests {
|
|
t.Run(fmt.Sprint(i), func(t *testing.T) {
|
|
err := Combine(tt.giveErrors...)
|
|
require.Equal(t, tt.wantError, err)
|
|
|
|
if tt.wantMultiline != "" {
|
|
t.Run("Sprintf/multiline", func(t *testing.T) {
|
|
assert.Equal(t, tt.wantMultiline, fmt.Sprintf("%+v", err))
|
|
})
|
|
}
|
|
|
|
if tt.wantSingleline != "" {
|
|
t.Run("Sprintf/singleline", func(t *testing.T) {
|
|
assert.Equal(t, tt.wantSingleline, fmt.Sprintf("%v", err))
|
|
})
|
|
|
|
t.Run("Error()", func(t *testing.T) {
|
|
assert.Equal(t, tt.wantSingleline, err.Error())
|
|
})
|
|
|
|
if s, ok := err.(fmt.Stringer); ok {
|
|
t.Run("String()", func(t *testing.T) {
|
|
assert.Equal(t, tt.wantSingleline, s.String())
|
|
})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCombineDoesNotModifySlice(t *testing.T) {
|
|
errors := []error{
|
|
errors.New("foo"),
|
|
nil,
|
|
errors.New("bar"),
|
|
}
|
|
|
|
assert.NotNil(t, Combine(errors...))
|
|
assert.Len(t, errors, 3)
|
|
assert.Nil(t, errors[1], 3)
|
|
}
|
|
|
|
func TestAppend(t *testing.T) {
|
|
tests := []struct {
|
|
left error
|
|
right error
|
|
want error
|
|
}{
|
|
{
|
|
left: nil,
|
|
right: nil,
|
|
want: nil,
|
|
},
|
|
{
|
|
left: nil,
|
|
right: errors.New("great sadness"),
|
|
want: errors.New("great sadness"),
|
|
},
|
|
{
|
|
left: errors.New("great sadness"),
|
|
right: nil,
|
|
want: errors.New("great sadness"),
|
|
},
|
|
{
|
|
left: errors.New("foo"),
|
|
right: errors.New("bar"),
|
|
want: newMultiErr(
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
),
|
|
},
|
|
{
|
|
left: newMultiErr(
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
),
|
|
right: errors.New("baz"),
|
|
want: newMultiErr(
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
errors.New("baz"),
|
|
),
|
|
},
|
|
{
|
|
left: errors.New("baz"),
|
|
right: newMultiErr(
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
),
|
|
want: newMultiErr(
|
|
errors.New("baz"),
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
),
|
|
},
|
|
{
|
|
left: newMultiErr(
|
|
errors.New("foo"),
|
|
),
|
|
right: newMultiErr(
|
|
errors.New("bar"),
|
|
),
|
|
want: newMultiErr(
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
assert.Equal(t, tt.want, Append(tt.left, tt.right))
|
|
}
|
|
}
|
|
|
|
type notMultiErr struct{}
|
|
|
|
var _ errorGroup = notMultiErr{}
|
|
|
|
func (notMultiErr) Error() string {
|
|
return "great sadness"
|
|
}
|
|
|
|
func (notMultiErr) Errors() []error {
|
|
return []error{errors.New("great sadness")}
|
|
}
|
|
|
|
func TestErrors(t *testing.T) {
|
|
tests := []struct {
|
|
give error
|
|
want []error
|
|
|
|
// Don't attempt to cast to errorGroup or *multiError
|
|
dontCast bool
|
|
}{
|
|
{dontCast: true}, // nil
|
|
{
|
|
give: errors.New("hi"),
|
|
want: []error{errors.New("hi")},
|
|
dontCast: true,
|
|
},
|
|
{
|
|
// We don't yet support non-multierr errors.
|
|
give: notMultiErr{},
|
|
want: []error{notMultiErr{}},
|
|
dontCast: true,
|
|
},
|
|
{
|
|
give: Combine(
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
),
|
|
want: []error{
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
},
|
|
},
|
|
{
|
|
give: Append(
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
),
|
|
want: []error{
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
},
|
|
},
|
|
{
|
|
give: Append(
|
|
errors.New("foo"),
|
|
Combine(
|
|
errors.New("bar"),
|
|
),
|
|
),
|
|
want: []error{
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
},
|
|
},
|
|
{
|
|
give: Combine(
|
|
errors.New("foo"),
|
|
Append(
|
|
errors.New("bar"),
|
|
errors.New("baz"),
|
|
),
|
|
errors.New("qux"),
|
|
),
|
|
want: []error{
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
errors.New("baz"),
|
|
errors.New("qux"),
|
|
},
|
|
},
|
|
}
|
|
|
|
for i, tt := range tests {
|
|
t.Run(fmt.Sprint(i), func(t *testing.T) {
|
|
t.Run("Errors()", func(t *testing.T) {
|
|
require.Equal(t, tt.want, Errors(tt.give))
|
|
})
|
|
|
|
if tt.dontCast {
|
|
return
|
|
}
|
|
|
|
t.Run("multiError", func(t *testing.T) {
|
|
require.Equal(t, tt.want, tt.give.(*multiError).Errors())
|
|
})
|
|
|
|
t.Run("errorGroup", func(t *testing.T) {
|
|
require.Equal(t, tt.want, tt.give.(errorGroup).Errors())
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func createMultiErrWithCapacity() error {
|
|
// Create a multiError that has capacity for more errors so Append will
|
|
// modify the underlying array that may be shared.
|
|
return appendN(nil, errors.New("append"), 50)
|
|
}
|
|
|
|
func TestAppendDoesNotModify(t *testing.T) {
|
|
initial := createMultiErrWithCapacity()
|
|
err1 := Append(initial, errors.New("err1"))
|
|
err2 := Append(initial, errors.New("err2"))
|
|
|
|
// Make sure the error messages match, since we do modify the copyNeeded
|
|
// atomic, the values cannot be compared.
|
|
assert.EqualError(t, initial, createMultiErrWithCapacity().Error(), "Initial should not be modified")
|
|
|
|
assert.EqualError(t, err1, Append(createMultiErrWithCapacity(), errors.New("err1")).Error())
|
|
assert.EqualError(t, err2, Append(createMultiErrWithCapacity(), errors.New("err2")).Error())
|
|
}
|
|
|
|
func TestAppendRace(t *testing.T) {
|
|
initial := createMultiErrWithCapacity()
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 10; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
err := initial
|
|
for j := 0; j < 10; j++ {
|
|
err = Append(err, errors.New("err"))
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestErrorsSliceIsImmutable(t *testing.T) {
|
|
err1 := errors.New("err1")
|
|
err2 := errors.New("err2")
|
|
|
|
err := Append(err1, err2)
|
|
gotErrors := Errors(err)
|
|
require.Equal(t, []error{err1, err2}, gotErrors, "errors must match")
|
|
|
|
gotErrors[0] = nil
|
|
gotErrors[1] = errors.New("err3")
|
|
|
|
require.Equal(t, []error{err1, err2}, Errors(err),
|
|
"errors must match after modification")
|
|
}
|
|
|
|
func TestNilMultierror(t *testing.T) {
|
|
// For safety, all operations on multiError should be safe even if it is
|
|
// nil.
|
|
var err *multiError
|
|
|
|
require.Empty(t, err.Error())
|
|
require.Empty(t, err.Errors())
|
|
}
|
|
|
|
func TestAppendInto(t *testing.T) {
|
|
tests := []struct {
|
|
desc string
|
|
into *error
|
|
give error
|
|
want error
|
|
}{
|
|
{
|
|
desc: "append into empty",
|
|
into: new(error),
|
|
give: errors.New("foo"),
|
|
want: errors.New("foo"),
|
|
},
|
|
{
|
|
desc: "append into non-empty, non-multierr",
|
|
into: errorPtr(errors.New("foo")),
|
|
give: errors.New("bar"),
|
|
want: Combine(
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
),
|
|
},
|
|
{
|
|
desc: "append into non-empty multierr",
|
|
into: errorPtr(Combine(
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
)),
|
|
give: errors.New("baz"),
|
|
want: Combine(
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
errors.New("baz"),
|
|
),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
assert.True(t, AppendInto(tt.into, tt.give))
|
|
assert.Equal(t, tt.want, *tt.into)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAppendInvoke(t *testing.T) {
|
|
tests := []struct {
|
|
desc string
|
|
into *error
|
|
give Invoker
|
|
want error
|
|
}{
|
|
{
|
|
desc: "append into empty",
|
|
into: new(error),
|
|
give: Invoke(func() error {
|
|
return errors.New("foo")
|
|
}),
|
|
want: errors.New("foo"),
|
|
},
|
|
{
|
|
desc: "append into non-empty, non-multierr",
|
|
into: errorPtr(errors.New("foo")),
|
|
give: Invoke(func() error {
|
|
return errors.New("bar")
|
|
}),
|
|
want: Combine(
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
),
|
|
},
|
|
{
|
|
desc: "append into non-empty multierr",
|
|
into: errorPtr(Combine(
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
)),
|
|
give: Invoke(func() error {
|
|
return errors.New("baz")
|
|
}),
|
|
want: Combine(
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
errors.New("baz"),
|
|
),
|
|
},
|
|
{
|
|
desc: "close/empty",
|
|
into: new(error),
|
|
give: Close(newCloserMock(t, errors.New("foo"))),
|
|
want: errors.New("foo"),
|
|
},
|
|
{
|
|
desc: "close/no fail",
|
|
into: new(error),
|
|
give: Close(newCloserMock(t, nil)),
|
|
want: nil,
|
|
},
|
|
{
|
|
desc: "close/non-empty",
|
|
into: errorPtr(errors.New("foo")),
|
|
give: Close(newCloserMock(t, errors.New("bar"))),
|
|
want: Combine(
|
|
errors.New("foo"),
|
|
errors.New("bar"),
|
|
),
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
AppendInvoke(tt.into, tt.give)
|
|
assert.Equal(t, tt.want, *tt.into)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClose(t *testing.T) {
|
|
t.Run("fail", func(t *testing.T) {
|
|
give := errors.New("great sadness")
|
|
got := Close(newCloserMock(t, give)).Invoke()
|
|
assert.Same(t, give, got)
|
|
})
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
got := Close(newCloserMock(t, nil)).Invoke()
|
|
assert.Nil(t, got)
|
|
})
|
|
}
|
|
|
|
func TestAppendIntoNil(t *testing.T) {
|
|
t.Run("nil pointer panics", func(t *testing.T) {
|
|
assert.Panics(t, func() {
|
|
AppendInto(nil, errors.New("foo"))
|
|
})
|
|
})
|
|
|
|
t.Run("nil error is no-op", func(t *testing.T) {
|
|
t.Run("empty left", func(t *testing.T) {
|
|
var err error
|
|
assert.False(t, AppendInto(&err, nil))
|
|
assert.Nil(t, err)
|
|
})
|
|
|
|
t.Run("non-empty left", func(t *testing.T) {
|
|
err := errors.New("foo")
|
|
assert.False(t, AppendInto(&err, nil))
|
|
assert.Equal(t, errors.New("foo"), err)
|
|
})
|
|
})
|
|
}
|
|
|
|
func errorPtr(err error) *error {
|
|
return &err
|
|
}
|
|
|
|
type closerMock func() error
|
|
|
|
func (c closerMock) Close() error {
|
|
return c()
|
|
}
|
|
|
|
func newCloserMock(tb testing.TB, err error) io.Closer {
|
|
var closed bool
|
|
tb.Cleanup(func() {
|
|
if !closed {
|
|
tb.Error("closerMock wasn't closed before test end")
|
|
}
|
|
})
|
|
return closerMock(func() error {
|
|
closed = true
|
|
return err
|
|
})
|
|
}
|