aboutsummaryrefslogtreecommitdiff
path: root/pipeline/pipeline_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'pipeline/pipeline_test.go')
-rw-r--r--pipeline/pipeline_test.go255
1 files changed, 255 insertions, 0 deletions
diff --git a/pipeline/pipeline_test.go b/pipeline/pipeline_test.go
new file mode 100644
index 0000000..9593356
--- /dev/null
+++ b/pipeline/pipeline_test.go
@@ -0,0 +1,255 @@
1package pipeline
2
3import (
4 "context"
5 "fmt"
6 "sync/atomic"
7 "testing"
8 "time"
9)
10
11func TestRun_Passthrough(t *testing.T) {
12 t.Parallel()
13
14 input := make(chan int, 3)
15 input <- 1
16 input <- 2
17 input <- 3
18 close(input)
19
20 out := Run(t.Context(), input)
21
22 var got []int
23 for v := range out {
24 got = append(got, v)
25 }
26
27 if len(got) != 3 {
28 t.Fatalf("expected 3 items, got %d", len(got))
29 }
30}
31
32func TestRun_NilInput(t *testing.T) {
33 t.Parallel()
34
35 defer func() {
36 if r := recover(); r == nil {
37 t.Error("expected panic for nil input")
38 }
39 }()
40
41 Run[int](t.Context(), nil)
42}
43
44func TestRun_Composition(t *testing.T) {
45 t.Parallel()
46
47 input := make(chan int, 10)
48 for i := 1; i <= 10; i++ {
49 input <- i
50 }
51 close(input)
52
53 out := Run(t.Context(), input,
54 Filter(func(v int) bool { return v%2 == 0 }),
55 Map(1, func(_ context.Context, v int) (int, bool) {
56 return v * 2, true
57 }),
58 )
59
60 var got []int
61 for v := range out {
62 got = append(got, v)
63 }
64
65 if len(got) != 5 {
66 t.Fatalf("expected 5 items, got %d: %v", len(got), got)
67 }
68
69 sum := 0
70 for _, v := range got {
71 sum += v
72 }
73 if sum != 60 {
74 t.Fatalf("expected sum 60, got %d", sum)
75 }
76}
77
78func TestRun_Backpressure(t *testing.T) {
79 t.Parallel()
80
81 input := make(chan int)
82 consumed := make(chan int, 10)
83
84 out := Run(t.Context(), input,
85 Map(1, func(_ context.Context, v int) (int, bool) {
86 consumed <- v
87 return v, true
88 }),
89 )
90
91 input <- 42
92 v := <-consumed
93 if v != 42 {
94 t.Fatalf("expected 42, got %d", v)
95 }
96
97 select {
98 case input <- 99:
99 t.Fatal("producer was not blocked")
100 case <-time.After(50 * time.Millisecond):
101 }
102
103 go func() {
104 for range out {
105 }
106 }()
107 close(input)
108}
109
110func TestRun_GracefulDrain(t *testing.T) {
111 t.Parallel()
112
113 input := make(chan int, 5)
114 for i := 1; i <= 5; i++ {
115 input <- i
116 }
117 close(input)
118
119 var count atomic.Int32
120 out := Run(t.Context(), input,
121 Sink(2, func(_ context.Context, v int) {
122 count.Add(1)
123 }),
124 )
125
126 for range out {
127 }
128
129 if count.Load() != 5 {
130 t.Fatalf("expected all 5 items drained, got %d", count.Load())
131 }
132}
133
134func TestRun_Cancellation(t *testing.T) {
135 t.Parallel()
136 ctx, cancel := context.WithCancel(t.Context())
137
138 input := make(chan int)
139 out := Run(ctx, input,
140 Map(1, func(_ context.Context, v int) (int, bool) {
141 return v, true
142 }),
143 )
144
145 go func() {
146 select {
147 case input <- 1:
148 case <-ctx.Done():
149 }
150 }()
151
152 cancel()
153
154 done := make(chan struct{})
155 go func() {
156 for range out {
157 }
158 close(done)
159 }()
160
161 select {
162 case <-done:
163 case <-time.After(time.Second):
164 t.Fatal("pipeline did not shut down after cancellation")
165 }
166}
167
168func TestRun_ErrorPropagation(t *testing.T) {
169 t.Parallel()
170 ctx, cancel := context.WithCancelCause(t.Context())
171 defer cancel(nil)
172
173 input := make(chan int, 5)
174 for i := 1; i <= 5; i++ {
175 input <- i
176 }
177 close(input)
178
179 sentinel := fmt.Errorf("bad item")
180
181 out := Run(ctx, input,
182 Map(1, func(_ context.Context, v int) (int, bool) {
183 if v == 3 {
184 cancel(sentinel)
185 return v, false
186 }
187 return v * 2, true
188 }),
189 )
190
191 for range out {
192 }
193
194 if cause := context.Cause(ctx); cause != sentinel {
195 t.Fatalf("expected sentinel error, got: %v", cause)
196 }
197}
198
199func TestObserver(t *testing.T) {
200 t.Parallel()
201
202 var items, drops atomic.Int32
203 doneCh := make(chan struct{})
204
205 obs := &testObserver{
206 onItem: func() { items.Add(1) },
207 onDrop: func() { drops.Add(1) },
208 onDone: func() { close(doneCh) },
209 }
210
211 input := make(chan int, 6)
212 for i := 1; i <= 6; i++ {
213 input <- i
214 }
215 close(input)
216
217 out := Run(t.Context(), input,
218 Filter(func(v int) bool { return v%2 == 0 }, WithObserver(obs)),
219 )
220
221 for range out {
222 }
223 <-doneCh
224
225 if items.Load() != 3 {
226 t.Errorf("expected 3 items, got %d", items.Load())
227 }
228 if drops.Load() != 3 {
229 t.Errorf("expected 3 drops, got %d", drops.Load())
230 }
231}
232
233type testObserver struct {
234 onItem func()
235 onDrop func()
236 onDone func()
237}
238
239func (o *testObserver) OnItem() {
240 if o.onItem != nil {
241 o.onItem()
242 }
243}
244
245func (o *testObserver) OnDrop() {
246 if o.onDrop != nil {
247 o.onDrop()
248 }
249}
250
251func (o *testObserver) OnDone() {
252 if o.onDone != nil {
253 o.onDone()
254 }
255}