123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582 |
- // Copyright (c) 2019 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 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
- }
|