1package uierrors
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "testing"
8
9 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/require"
11)
12
13func TestDrop(t *testing.T) {
14 boom := errors.New("boom")
15 other := errors.New("other")
16 c := context.Canceled
17
18 assert.NoError(t, Drop(nil, c))
19 assert.NoError(t, Drop(c, c))
20 assert.NoError(t, Drop(errors.Join(c, c), c))
21 assert.Same(t, boom, Drop(errors.Join(c, boom), c), "a lone survivor is returned unwrapped")
22 assert.Same(t, boom, Drop(boom, c))
23
24 both := Drop(errors.Join(c, boom, other), c)
25 assert.ErrorIs(t, both, boom)
26 assert.ErrorIs(t, both, other)
27 assert.NotErrorIs(t, both, c)
28
29 nested := Drop(errors.Join(errors.Join(c, boom), c), c)
30 assert.Same(t, boom, nested)
31}
32
33func TestDropKeepsWrappedTargets(t *testing.T) {
34 wrapped := fmt.Errorf("surface close: %w", context.Canceled)
35 got := Drop(errors.Join(context.Canceled, wrapped), context.Canceled)
36 assert.Same(t, wrapped, got, "wrapping makes it a real failure")
37}
38
39func TestDropMultipleTargets(t *testing.T) {
40 assert.NoError(t, Drop(errors.Join(context.Canceled, context.DeadlineExceeded), context.Canceled, context.DeadlineExceeded))
41}
42
43var errClosed = errors.New("closed")
44
45// liveParent returns a parent context and its cancel, to classify with real
46// context state rather than hand-built errors.
47func liveParent() (context.Context, context.CancelFunc) {
48 return context.WithCancel(context.Background())
49}
50
51func TestEndedCleanAfterOwnDecision(t *testing.T) {
52 parent, cancel := liveParent()
53 defer cancel()
54 // We decided and canceled the child: Run reports a bare Canceled.
55 child, cancelChild := context.WithCancel(parent)
56 cancelChild()
57 assert.NoError(t, Ended(child.Err(), true, parent.Err(), errClosed))
58 assert.NoError(t, Ended(nil, true, parent.Err(), errClosed))
59}
60
61func TestEndedCleanAfterCallerCancel(t *testing.T) {
62 parent, cancel := liveParent()
63 cancel()
64 // The caller's context is done, which explains the end by itself.
65 perr := parent.Err()
66 require.Error(t, perr)
67 assert.NoError(t, Ended(context.Canceled, true, perr, errClosed))
68 assert.NoError(t, Ended(perr, true, perr, errClosed))
69}
70
71func TestEndedKeepsRealFailureJoinedWithCancel(t *testing.T) {
72 boom := errors.New("boom")
73 parent, cancel := liveParent()
74 cancel()
75 for _, explained := range []bool{true, false} {
76 err := Ended(errors.Join(context.Canceled, boom), explained, parent.Err(), errClosed)
77 assert.ErrorIs(t, err, boom, "explained=%v", explained)
78 }
79 wrapped := fmt.Errorf("surface close: %w", context.Canceled)
80 err := Ended(errors.Join(context.Canceled, wrapped), true, nil, errClosed)
81 assert.Same(t, wrapped, err, "a wrapped Canceled is a real failure")
82}
83
84func TestEndedUnexpectedCancelIsClosed(t *testing.T) {
85 boom := errors.New("boom")
86 // Nobody decided and the parent is live, yet Run ended.
87 assert.ErrorIs(t, Ended(context.Canceled, false, nil, errClosed), errClosed)
88 assert.ErrorIs(t, Ended(nil, false, nil, errClosed), errClosed)
89 assert.NotErrorIs(t, Ended(context.Canceled, false, nil, errClosed), context.Canceled)
90
91 // A real failure is reported as itself, never relabeled as a close.
92 real := Ended(errors.Join(context.Canceled, boom), false, nil, errClosed)
93 assert.Same(t, boom, real)
94 assert.NotErrorIs(t, real, errClosed)
95 assert.NotErrorIs(t, real, context.Canceled)
96 assert.Same(t, boom, Ended(boom, false, nil, errClosed))
97}
98
99func TestIsCancellation(t *testing.T) {
100 boom := errors.New("boom")
101 c := context.Canceled
102 assert.False(t, IsCancellation(nil))
103 assert.True(t, IsCancellation(c))
104 assert.True(t, IsCancellation(fmt.Errorf("capture first frame: %w", c)))
105 assert.True(t, IsCancellation(errors.Join(c, fmt.Errorf("x: %w", c))))
106 assert.False(t, IsCancellation(boom))
107 assert.False(t, IsCancellation(errors.Join(c, boom)))
108 assert.False(t, IsCancellation(fmt.Errorf("%w: %w", c, boom)))
109 assert.False(t, IsCancellation(context.DeadlineExceeded))
110 assert.False(t, IsCancellation(fmt.Errorf("finalize: %w", context.DeadlineExceeded)))
111}