1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package multierr
22
23 import (
24 "errors"
25 "fmt"
26 "io"
27 "sync"
28 "testing"
29
30 "github.com/stretchr/testify/assert"
31 "github.com/stretchr/testify/require"
32 )
33
34
35
36 type richFormatError struct{}
37
38 func (r richFormatError) Error() string {
39 return fmt.Sprint(r)
40 }
41
42 func (richFormatError) Format(f fmt.State, c rune) {
43 if c == 'v' && f.Flag('+') {
44 io.WriteString(f, "multiline\nmessage\nwith plus")
45 } else {
46 io.WriteString(f, "without plus")
47 }
48 }
49
50 func appendN(initial, err error, n int) error {
51 errs := initial
52 for i := 0; i < n; i++ {
53 errs = Append(errs, err)
54 }
55 return errs
56 }
57
58 func newMultiErr(errors ...error) error {
59 return &multiError{errors: errors}
60 }
61
62 func TestEvery(t *testing.T) {
63 myError1 := errors.New("woeful misfortune")
64 myError2 := errors.New("worrisome travesty")
65
66 for _, tt := range []struct {
67 desc string
68 giveErr error
69 giveTarget error
70 wantIs bool
71 wantEvery bool
72 }{
73 {
74 desc: "all match",
75 giveErr: newMultiErr(myError1, myError1, myError1),
76 giveTarget: myError1,
77 wantIs: true,
78 wantEvery: true,
79 },
80 {
81 desc: "one matches",
82 giveErr: newMultiErr(myError1, myError2),
83 giveTarget: myError1,
84 wantIs: true,
85 wantEvery: false,
86 },
87 {
88 desc: "not multiErrs and non equal",
89 giveErr: myError1,
90 giveTarget: myError2,
91 wantIs: false,
92 wantEvery: false,
93 },
94 {
95 desc: "not multiErrs but equal",
96 giveErr: myError1,
97 giveTarget: myError1,
98 wantIs: true,
99 wantEvery: true,
100 },
101 {
102 desc: "not multiErr w multiErr target",
103 giveErr: myError1,
104 giveTarget: newMultiErr(myError1, myError1),
105 wantIs: false,
106 wantEvery: false,
107 },
108 {
109 desc: "multiErr w multiErr target",
110 giveErr: newMultiErr(myError1, myError1),
111 giveTarget: newMultiErr(myError1, myError1),
112 wantIs: false,
113 wantEvery: false,
114 },
115 } {
116 t.Run(tt.desc, func(t *testing.T) {
117 assert.Equal(t, tt.wantIs, errors.Is(tt.giveErr, tt.giveTarget))
118 assert.Equal(t, tt.wantEvery, Every(tt.giveErr, tt.giveTarget))
119 })
120 }
121 }
122
123 func TestCombine(t *testing.T) {
124 tests := []struct {
125
126 giveErrors []error
127
128
129 wantError error
130
131
132 wantMultiline string
133 wantSingleline string
134 }{
135 {
136 giveErrors: nil,
137 wantError: nil,
138 },
139 {
140 giveErrors: []error{},
141 wantError: nil,
142 },
143 {
144 giveErrors: []error{
145 errors.New("foo"),
146 nil,
147 newMultiErr(
148 errors.New("bar"),
149 ),
150 nil,
151 },
152 wantError: newMultiErr(
153 errors.New("foo"),
154 errors.New("bar"),
155 ),
156 wantMultiline: "the following errors occurred:\n" +
157 " - foo\n" +
158 " - bar",
159 wantSingleline: "foo; bar",
160 },
161 {
162 giveErrors: []error{nil, nil, errors.New("great sadness"), nil},
163 wantError: errors.New("great sadness"),
164 wantMultiline: "great sadness",
165 wantSingleline: "great sadness",
166 },
167 {
168 giveErrors: []error{
169 errors.New("foo"),
170 newMultiErr(
171 errors.New("bar"),
172 ),
173 },
174 wantError: newMultiErr(
175 errors.New("foo"),
176 errors.New("bar"),
177 ),
178 wantMultiline: "the following errors occurred:\n" +
179 " - foo\n" +
180 " - bar",
181 wantSingleline: "foo; bar",
182 },
183 {
184 giveErrors: []error{errors.New("great sadness")},
185 wantError: errors.New("great sadness"),
186 wantMultiline: "great sadness",
187 wantSingleline: "great sadness",
188 },
189 {
190 giveErrors: []error{
191 errors.New("foo"),
192 errors.New("bar"),
193 },
194 wantError: newMultiErr(
195 errors.New("foo"),
196 errors.New("bar"),
197 ),
198 wantMultiline: "the following errors occurred:\n" +
199 " - foo\n" +
200 " - bar",
201 wantSingleline: "foo; bar",
202 },
203 {
204 giveErrors: []error{
205 errors.New("great sadness"),
206 errors.New("multi\n line\nerror message"),
207 errors.New("single line error message"),
208 },
209 wantError: newMultiErr(
210 errors.New("great sadness"),
211 errors.New("multi\n line\nerror message"),
212 errors.New("single line error message"),
213 ),
214 wantMultiline: "the following errors occurred:\n" +
215 " - great sadness\n" +
216 " - multi\n" +
217 " line\n" +
218 " error message\n" +
219 " - single line error message",
220 wantSingleline: "great sadness; " +
221 "multi\n line\nerror message; " +
222 "single line error message",
223 },
224 {
225 giveErrors: []error{
226 errors.New("foo"),
227 newMultiErr(
228 errors.New("bar"),
229 errors.New("baz"),
230 ),
231 errors.New("qux"),
232 },
233 wantError: newMultiErr(
234 errors.New("foo"),
235 errors.New("bar"),
236 errors.New("baz"),
237 errors.New("qux"),
238 ),
239 wantMultiline: "the following errors occurred:\n" +
240 " - foo\n" +
241 " - bar\n" +
242 " - baz\n" +
243 " - qux",
244 wantSingleline: "foo; bar; baz; qux",
245 },
246 {
247 giveErrors: []error{
248 errors.New("foo"),
249 nil,
250 newMultiErr(
251 errors.New("bar"),
252 ),
253 nil,
254 },
255 wantError: newMultiErr(
256 errors.New("foo"),
257 errors.New("bar"),
258 ),
259 wantMultiline: "the following errors occurred:\n" +
260 " - foo\n" +
261 " - bar",
262 wantSingleline: "foo; bar",
263 },
264 {
265 giveErrors: []error{
266 errors.New("foo"),
267 newMultiErr(
268 errors.New("bar"),
269 ),
270 },
271 wantError: newMultiErr(
272 errors.New("foo"),
273 errors.New("bar"),
274 ),
275 wantMultiline: "the following errors occurred:\n" +
276 " - foo\n" +
277 " - bar",
278 wantSingleline: "foo; bar",
279 },
280 {
281 giveErrors: []error{
282 errors.New("foo"),
283 richFormatError{},
284 errors.New("bar"),
285 },
286 wantError: newMultiErr(
287 errors.New("foo"),
288 richFormatError{},
289 errors.New("bar"),
290 ),
291 wantMultiline: "the following errors occurred:\n" +
292 " - foo\n" +
293 " - multiline\n" +
294 " message\n" +
295 " with plus\n" +
296 " - bar",
297 wantSingleline: "foo; without plus; bar",
298 },
299 }
300
301 for i, tt := range tests {
302 t.Run(fmt.Sprint(i), func(t *testing.T) {
303 err := Combine(tt.giveErrors...)
304 require.Equal(t, tt.wantError, err)
305
306 if tt.wantMultiline != "" {
307 t.Run("Sprintf/multiline", func(t *testing.T) {
308 assert.Equal(t, tt.wantMultiline, fmt.Sprintf("%+v", err))
309 })
310 }
311
312 if tt.wantSingleline != "" {
313 t.Run("Sprintf/singleline", func(t *testing.T) {
314 assert.Equal(t, tt.wantSingleline, fmt.Sprintf("%v", err))
315 })
316
317 t.Run("Error()", func(t *testing.T) {
318 assert.Equal(t, tt.wantSingleline, err.Error())
319 })
320
321 if s, ok := err.(fmt.Stringer); ok {
322 t.Run("String()", func(t *testing.T) {
323 assert.Equal(t, tt.wantSingleline, s.String())
324 })
325 }
326 }
327 })
328 }
329 }
330
331 func TestCombineDoesNotModifySlice(t *testing.T) {
332 errors := []error{
333 errors.New("foo"),
334 nil,
335 errors.New("bar"),
336 }
337
338 assert.NotNil(t, Combine(errors...))
339 assert.Len(t, errors, 3)
340 assert.Nil(t, errors[1], 3)
341 }
342
343 func TestCombineGoodCaseNoAlloc(t *testing.T) {
344 errs := make([]error, 10)
345 allocs := testing.AllocsPerRun(100, func() {
346 Combine(errs...)
347 })
348 assert.Equal(t, 0.0, allocs)
349 }
350
351 func TestAppend(t *testing.T) {
352 tests := []struct {
353 left error
354 right error
355 want error
356 }{
357 {
358 left: nil,
359 right: nil,
360 want: nil,
361 },
362 {
363 left: nil,
364 right: errors.New("great sadness"),
365 want: errors.New("great sadness"),
366 },
367 {
368 left: errors.New("great sadness"),
369 right: nil,
370 want: errors.New("great sadness"),
371 },
372 {
373 left: errors.New("foo"),
374 right: errors.New("bar"),
375 want: newMultiErr(
376 errors.New("foo"),
377 errors.New("bar"),
378 ),
379 },
380 {
381 left: newMultiErr(
382 errors.New("foo"),
383 errors.New("bar"),
384 ),
385 right: errors.New("baz"),
386 want: newMultiErr(
387 errors.New("foo"),
388 errors.New("bar"),
389 errors.New("baz"),
390 ),
391 },
392 {
393 left: errors.New("baz"),
394 right: newMultiErr(
395 errors.New("foo"),
396 errors.New("bar"),
397 ),
398 want: newMultiErr(
399 errors.New("baz"),
400 errors.New("foo"),
401 errors.New("bar"),
402 ),
403 },
404 {
405 left: newMultiErr(
406 errors.New("foo"),
407 ),
408 right: newMultiErr(
409 errors.New("bar"),
410 ),
411 want: newMultiErr(
412 errors.New("foo"),
413 errors.New("bar"),
414 ),
415 },
416 }
417
418 for _, tt := range tests {
419 assert.Equal(t, tt.want, Append(tt.left, tt.right))
420 }
421 }
422
423 type notMultiErr struct{}
424
425 var _ errorGroup = notMultiErr{}
426
427 func (notMultiErr) Error() string {
428 return "great sadness"
429 }
430
431 func (notMultiErr) Errors() []error {
432 return []error{errors.New("great sadness")}
433 }
434
435 func TestErrors(t *testing.T) {
436 tests := []struct {
437 give error
438 want []error
439
440
441 dontCast bool
442 }{
443 {dontCast: true},
444 {
445 give: errors.New("hi"),
446 want: []error{errors.New("hi")},
447 dontCast: true,
448 },
449 {
450
451
452 give: notMultiErr{},
453 want: []error{notMultiErr{}},
454 dontCast: true,
455 },
456 {
457 give: Combine(
458 errors.New("foo"),
459 errors.New("bar"),
460 ),
461 want: []error{
462 errors.New("foo"),
463 errors.New("bar"),
464 },
465 },
466 {
467 give: Append(
468 errors.New("foo"),
469 errors.New("bar"),
470 ),
471 want: []error{
472 errors.New("foo"),
473 errors.New("bar"),
474 },
475 },
476 {
477 give: Append(
478 errors.New("foo"),
479 Combine(
480 errors.New("bar"),
481 ),
482 ),
483 want: []error{
484 errors.New("foo"),
485 errors.New("bar"),
486 },
487 },
488 {
489 give: Combine(
490 errors.New("foo"),
491 Append(
492 errors.New("bar"),
493 errors.New("baz"),
494 ),
495 errors.New("qux"),
496 ),
497 want: []error{
498 errors.New("foo"),
499 errors.New("bar"),
500 errors.New("baz"),
501 errors.New("qux"),
502 },
503 },
504 }
505
506 for i, tt := range tests {
507 t.Run(fmt.Sprint(i), func(t *testing.T) {
508 t.Run("Errors()", func(t *testing.T) {
509 require.Equal(t, tt.want, Errors(tt.give))
510 })
511
512 if tt.dontCast {
513 return
514 }
515
516 t.Run("multiError", func(t *testing.T) {
517 require.Equal(t, tt.want, tt.give.(*multiError).Errors())
518 })
519
520 t.Run("errorGroup", func(t *testing.T) {
521 require.Equal(t, tt.want, tt.give.(errorGroup).Errors())
522 })
523 })
524 }
525 }
526
527 func createMultiErrWithCapacity() error {
528
529
530 return appendN(nil, errors.New("append"), 50)
531 }
532
533 func TestAppendDoesNotModify(t *testing.T) {
534 initial := createMultiErrWithCapacity()
535 err1 := Append(initial, errors.New("err1"))
536 err2 := Append(initial, errors.New("err2"))
537
538
539
540 assert.EqualError(t, initial, createMultiErrWithCapacity().Error(), "Initial should not be modified")
541
542 assert.EqualError(t, err1, Append(createMultiErrWithCapacity(), errors.New("err1")).Error())
543 assert.EqualError(t, err2, Append(createMultiErrWithCapacity(), errors.New("err2")).Error())
544 }
545
546 func TestAppendRace(t *testing.T) {
547 initial := createMultiErrWithCapacity()
548
549 var wg sync.WaitGroup
550 for i := 0; i < 10; i++ {
551 wg.Add(1)
552 go func() {
553 defer wg.Done()
554
555 err := initial
556 for j := 0; j < 10; j++ {
557 err = Append(err, errors.New("err"))
558 }
559 }()
560 }
561
562 wg.Wait()
563 }
564
565 func TestErrorsSliceIsImmutable(t *testing.T) {
566 err1 := errors.New("err1")
567 err2 := errors.New("err2")
568
569 err := Append(err1, err2)
570 gotErrors := Errors(err)
571 require.Equal(t, []error{err1, err2}, gotErrors, "errors must match")
572
573 gotErrors[0] = nil
574 gotErrors[1] = errors.New("err3")
575
576 require.Equal(t, []error{err1, err2}, Errors(err),
577 "errors must match after modification")
578 }
579
580 func TestNilMultierror(t *testing.T) {
581
582
583 var err *multiError
584
585 require.Empty(t, err.Error())
586 require.Empty(t, err.Errors())
587 }
588
589 func TestAppendInto(t *testing.T) {
590 tests := []struct {
591 desc string
592 into *error
593 give error
594 want error
595 }{
596 {
597 desc: "append into empty",
598 into: new(error),
599 give: errors.New("foo"),
600 want: errors.New("foo"),
601 },
602 {
603 desc: "append into non-empty, non-multierr",
604 into: errorPtr(errors.New("foo")),
605 give: errors.New("bar"),
606 want: Combine(
607 errors.New("foo"),
608 errors.New("bar"),
609 ),
610 },
611 {
612 desc: "append into non-empty multierr",
613 into: errorPtr(Combine(
614 errors.New("foo"),
615 errors.New("bar"),
616 )),
617 give: errors.New("baz"),
618 want: Combine(
619 errors.New("foo"),
620 errors.New("bar"),
621 errors.New("baz"),
622 ),
623 },
624 }
625
626 for _, tt := range tests {
627 t.Run(tt.desc, func(t *testing.T) {
628 assert.True(t, AppendInto(tt.into, tt.give))
629 assert.Equal(t, tt.want, *tt.into)
630 })
631 }
632 }
633
634 func TestAppendInvoke(t *testing.T) {
635 tests := []struct {
636 desc string
637 into *error
638 give Invoker
639 want error
640 }{
641 {
642 desc: "append into empty",
643 into: new(error),
644 give: Invoke(func() error {
645 return errors.New("foo")
646 }),
647 want: errors.New("foo"),
648 },
649 {
650 desc: "append into non-empty, non-multierr",
651 into: errorPtr(errors.New("foo")),
652 give: Invoke(func() error {
653 return errors.New("bar")
654 }),
655 want: Combine(
656 errors.New("foo"),
657 errors.New("bar"),
658 ),
659 },
660 {
661 desc: "append into non-empty multierr",
662 into: errorPtr(Combine(
663 errors.New("foo"),
664 errors.New("bar"),
665 )),
666 give: Invoke(func() error {
667 return errors.New("baz")
668 }),
669 want: Combine(
670 errors.New("foo"),
671 errors.New("bar"),
672 errors.New("baz"),
673 ),
674 },
675 {
676 desc: "close/empty",
677 into: new(error),
678 give: Close(newCloserMock(t, errors.New("foo"))),
679 want: errors.New("foo"),
680 },
681 {
682 desc: "close/no fail",
683 into: new(error),
684 give: Close(newCloserMock(t, nil)),
685 want: nil,
686 },
687 {
688 desc: "close/non-empty",
689 into: errorPtr(errors.New("foo")),
690 give: Close(newCloserMock(t, errors.New("bar"))),
691 want: Combine(
692 errors.New("foo"),
693 errors.New("bar"),
694 ),
695 },
696 }
697 for _, tt := range tests {
698 t.Run(tt.desc, func(t *testing.T) {
699 AppendInvoke(tt.into, tt.give)
700 assert.Equal(t, tt.want, *tt.into)
701 })
702 }
703 }
704
705 func TestClose(t *testing.T) {
706 t.Run("fail", func(t *testing.T) {
707 give := errors.New("great sadness")
708 got := Close(newCloserMock(t, give)).Invoke()
709 assert.Same(t, give, got)
710 })
711
712 t.Run("success", func(t *testing.T) {
713 got := Close(newCloserMock(t, nil)).Invoke()
714 assert.Nil(t, got)
715 })
716 }
717
718 func TestAppendIntoNil(t *testing.T) {
719 t.Run("nil pointer panics", func(t *testing.T) {
720 assert.Panics(t, func() {
721 AppendInto(nil, errors.New("foo"))
722 })
723 })
724
725 t.Run("nil error is no-op", func(t *testing.T) {
726 t.Run("empty left", func(t *testing.T) {
727 var err error
728 assert.False(t, AppendInto(&err, nil))
729 assert.Nil(t, err)
730 })
731
732 t.Run("non-empty left", func(t *testing.T) {
733 err := errors.New("foo")
734 assert.False(t, AppendInto(&err, nil))
735 assert.Equal(t, errors.New("foo"), err)
736 })
737 })
738 }
739
740 func TestAppendFunc(t *testing.T) {
741 var (
742 errDeferred = errors.New("deferred func called")
743 errOriginal = errors.New("original error")
744 )
745
746 stopFunc := func() error {
747 return errDeferred
748 }
749
750 err := func() (err error) {
751 defer AppendFunc(&err, stopFunc)
752
753 return errOriginal
754 }()
755 assert.Equal(t, []error{errOriginal, errDeferred}, Errors(err), "both deferred and original error must be returned")
756 }
757
758 func errorPtr(err error) *error {
759 return &err
760 }
761
762 type closerMock func() error
763
764 func (c closerMock) Close() error {
765 return c()
766 }
767
768 func newCloserMock(tb testing.TB, err error) io.Closer {
769 var closed bool
770 tb.Cleanup(func() {
771 if !closed {
772 tb.Error("closerMock wasn't closed before test end")
773 }
774 })
775 return closerMock(func() error {
776 closed = true
777 return err
778 })
779 }
780
View as plain text