Skip to content

Commit 0b656fc

Browse files
author
Dima Kossovich
committed
add contrace tests in all variations
1 parent f9e146b commit 0b656fc

2 files changed

Lines changed: 524 additions & 0 deletions

File tree

Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
package internalpipe
2+
3+
import (
4+
"fmt"
5+
"math/rand"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
// This file targets the non-trivial parts of the new destination-pointer
12+
// generator contract: the parallel do() gather/compaction, chunk boundaries,
13+
// worker-scratch reuse and element order. The expectations are always computed
14+
// with a straight-line loop, never by re-running the code under test with a
15+
// different goroutine count, so a bug in the shared evaluation path cannot
16+
// mask itself.
17+
18+
// plainChain is an independent implementation of the pipeline built in
19+
// deepChain, used to derive the expected result.
20+
func plainChain(data []int) []int {
21+
out := make([]int, 0, len(data))
22+
for _, x := range data {
23+
x++ // Map(x + 1)
24+
if x%3 == 0 {
25+
continue // Filter(x%3 != 0)
26+
}
27+
x *= 2 // MapFilter(x*2, keep when != 0)
28+
if x == 0 {
29+
continue
30+
}
31+
if x%5 == 2 {
32+
continue // Filter(x%5 != 2)
33+
}
34+
x -= 1000 // Map(x - 1000)
35+
out = append(out, x)
36+
}
37+
return out
38+
}
39+
40+
func deepChain(data []int, goroutines int) []int {
41+
return Slice(data).
42+
Parallel(uint16(goroutines)).
43+
Map(func(x int) int { return x + 1 }).
44+
Filter(func(x *int) bool { return *x%3 != 0 }).
45+
MapFilter(func(x int) (int, bool) { return x * 2, x*2 != 0 }).
46+
Filter(func(x *int) bool { return *x%5 != 2 }).
47+
Map(func(x int) int { return x - 1000 }).
48+
Do()
49+
}
50+
51+
func Test_DeepChain_OrderAndSkips(t *testing.T) {
52+
t.Parallel()
53+
54+
data := make([]int, 2000)
55+
for i := range data {
56+
data[i] = i*7 - 3
57+
}
58+
want := plainChain(data)
59+
require.NotEmpty(t, want)
60+
61+
for _, g := range []int{1, 2, 3, 4, 5, 7, 16, 103} {
62+
g := g
63+
t.Run(fmt.Sprintf("goroutines=%d", g), func(t *testing.T) {
64+
t.Parallel()
65+
require.Equal(t, want, deepChain(data, g))
66+
})
67+
}
68+
}
69+
70+
// Test_ParallelDo_SkipBoundaries is built so that dropped positions land on
71+
// both ends, inside a chunk and (for the chosen goroutine counts) exactly on
72+
// chunk borders - the in-place compaction has to survive all of them.
73+
func Test_ParallelDo_SkipBoundaries(t *testing.T) {
74+
t.Parallel()
75+
76+
const n = 103 // deliberately not divisible by most of the goroutine counts
77+
data := make([]int, n)
78+
for i := range data {
79+
data[i] = i
80+
}
81+
keep := func(i int) bool { return i != 0 && i != n-1 && i%7 != 0 }
82+
83+
want := make([]int, 0, n)
84+
for i := 0; i < n; i++ {
85+
if keep(i) {
86+
want = append(want, i)
87+
}
88+
}
89+
require.NotEmpty(t, want)
90+
91+
for _, g := range []int{1, 2, 3, 4, 5, 7, 8, 13, 103} {
92+
g := g
93+
t.Run(fmt.Sprintf("goroutines=%d", g), func(t *testing.T) {
94+
t.Parallel()
95+
got := Slice(data).
96+
Parallel(uint16(g)).
97+
Filter(func(x *int) bool { return keep(*x) }).
98+
Do()
99+
require.Equal(t, want, got)
100+
})
101+
}
102+
}
103+
104+
func Test_ParallelDo_Edges(t *testing.T) {
105+
t.Parallel()
106+
107+
t.Run("empty pipe", func(t *testing.T) {
108+
t.Parallel()
109+
got := Slice([]int{}).Parallel(10).Map(func(x int) int { return x + 1 }).Do()
110+
require.Equal(t, []int{}, got)
111+
})
112+
113+
t.Run("more workers than elements", func(t *testing.T) {
114+
t.Parallel()
115+
got := Slice([]int{1, 2, 3}).Parallel(10).Map(func(x int) int { return x * x }).Do()
116+
require.Equal(t, []int{1, 4, 9}, got)
117+
})
118+
119+
t.Run("every element skipped", func(t *testing.T) {
120+
t.Parallel()
121+
data := make([]int, 1000)
122+
for i := range data {
123+
data[i] = i
124+
}
125+
got := Slice(data).
126+
Parallel(6).
127+
MapFilter(func(x int) (int, bool) { return x, false }).
128+
Do()
129+
require.Equal(t, []int{}, got)
130+
})
131+
132+
t.Run("only first and last survive", func(t *testing.T) {
133+
t.Parallel()
134+
data := make([]int, 1000)
135+
for i := range data {
136+
data[i] = i
137+
}
138+
got := Slice(data).
139+
Parallel(6).
140+
Filter(func(x *int) bool { return *x == 0 || *x == 999 }).
141+
Do()
142+
require.Equal(t, []int{0, 999}, got)
143+
})
144+
}
145+
146+
func Test_TakeWithSkips(t *testing.T) {
147+
t.Parallel()
148+
149+
for _, g := range []int{1, 3, 8} {
150+
g := g
151+
t.Run(fmt.Sprintf("goroutines=%d", g), func(t *testing.T) {
152+
t.Parallel()
153+
got := Func(func(i int) (int, bool) { return i, true }).
154+
Parallel(uint16(g)).
155+
MapFilter(func(x int) (int, bool) { return x, x%2 == 0 }).
156+
Take(5).
157+
Do()
158+
require.Equal(t, []int{0, 2, 4, 6, 8}, got)
159+
})
160+
}
161+
}
162+
163+
// Test_Reduce_OrderSensitive fails loudly if the parallel gather ever reorders
164+
// elements: the parallel Reduce folds over Do(), so a shuffled result would
165+
// change a non-commutative reduction.
166+
func Test_Reduce_OrderSensitive(t *testing.T) {
167+
t.Parallel()
168+
169+
data := []int{1, 2, 3, 4, 5, 6}
170+
sub := func(a, b *int) int { return *a - *b }
171+
172+
want := data[0]
173+
for _, v := range data[1:] {
174+
want -= v // -19
175+
}
176+
177+
t.Run("single thread", func(t *testing.T) {
178+
t.Parallel()
179+
got := Slice(data).Reduce(sub)
180+
require.NotNil(t, got)
181+
require.Equal(t, want, *got)
182+
})
183+
184+
t.Run("parallel preserves order", func(t *testing.T) {
185+
t.Parallel()
186+
got := Slice(data).Parallel(4).Reduce(sub)
187+
require.NotNil(t, got)
188+
require.Equal(t, want, *got)
189+
})
190+
191+
t.Run("parallel with skips", func(t *testing.T) {
192+
t.Parallel()
193+
got := Slice(data).
194+
Parallel(4).
195+
Filter(func(x *int) bool { return *x%2 == 0 }). // 2,4,6
196+
Reduce(sub)
197+
require.NotNil(t, got)
198+
require.Equal(t, 2-4-6, *got) // -8
199+
})
200+
}
201+
202+
func Test_Count_ParallelWithSkips(t *testing.T) {
203+
t.Parallel()
204+
205+
data := make([]int, 5000)
206+
for i := range data {
207+
data[i] = i
208+
}
209+
210+
for _, g := range []int{1, 4, 9} {
211+
g := g
212+
t.Run(fmt.Sprintf("goroutines=%d", g), func(t *testing.T) {
213+
t.Parallel()
214+
got := Slice(data).
215+
Parallel(uint16(g)).
216+
Filter(func(x *int) bool { return *x%3 == 0 }).
217+
Count()
218+
require.Equal(t, 1667, got) // multiples of 3 in [0, 5000)
219+
})
220+
}
221+
}
222+
223+
func Test_Sum_ParallelWithSkips(t *testing.T) {
224+
t.Parallel()
225+
226+
data := make([]int, 1000)
227+
for i := range data {
228+
data[i] = i
229+
}
230+
plus := func(a, b *int) int { return *a + *b }
231+
232+
want := 0
233+
for _, v := range data {
234+
if v%2 == 0 {
235+
want += v
236+
}
237+
}
238+
require.Equal(t, 249500, want)
239+
240+
for _, g := range []int{1, 4, 7} {
241+
g := g
242+
t.Run(fmt.Sprintf("goroutines=%d", g), func(t *testing.T) {
243+
t.Parallel()
244+
got := Slice(data).
245+
Parallel(uint16(g)).
246+
Filter(func(x *int) bool { return *x%2 == 0 }).
247+
Sum(plus)
248+
require.Equal(t, want, got)
249+
})
250+
}
251+
}
252+
253+
func Test_Any_ParallelEdges(t *testing.T) {
254+
t.Parallel()
255+
256+
data := make([]int, 1000)
257+
for i := range data {
258+
data[i] = i
259+
}
260+
last := len(data) - 1
261+
262+
t.Run("only the last element matches", func(t *testing.T) {
263+
t.Parallel()
264+
got := Slice(data).
265+
Parallel(7).
266+
Filter(func(x *int) bool { return *x == last }).
267+
Any()
268+
require.NotNil(t, got)
269+
require.Equal(t, last, *got)
270+
})
271+
272+
t.Run("nothing matches -> nil", func(t *testing.T) {
273+
t.Parallel()
274+
got := Slice(data).
275+
Parallel(7).
276+
Filter(func(*int) bool { return false }).
277+
Any()
278+
require.Nil(t, got)
279+
})
280+
281+
t.Run("single thread only the last element matches", func(t *testing.T) {
282+
t.Parallel()
283+
got := Slice(data).
284+
Filter(func(x *int) bool { return *x == last }).
285+
Any()
286+
require.NotNil(t, got)
287+
require.Equal(t, last, *got)
288+
})
289+
}
290+
291+
// Test_First_ParallelLowestIndex pins the ordering guarantee: with g=4 the
292+
// chunk size is 250, so the cases below probe a border, just-after-a-border,
293+
// the very last element and the first chunk.
294+
func Test_First_ParallelLowestIndex(t *testing.T) {
295+
t.Parallel()
296+
297+
data := make([]int, 1000)
298+
for i := range data {
299+
data[i] = i
300+
}
301+
302+
cases := []struct {
303+
name string
304+
pred func(*int) bool
305+
want int
306+
}{
307+
{"match on a chunk border", func(x *int) bool { return *x >= 500 }, 500},
308+
{"match just after a border", func(x *int) bool { return *x >= 501 }, 501},
309+
{"only the last element", func(x *int) bool { return *x == 999 }, 999},
310+
{"match in the first chunk", func(x *int) bool { return *x >= 7 }, 7},
311+
}
312+
313+
for _, c := range cases {
314+
c := c
315+
t.Run(c.name, func(t *testing.T) {
316+
t.Parallel()
317+
got := Slice(data).Parallel(4).Filter(c.pred).First()
318+
require.NotNil(t, got)
319+
require.Equal(t, c.want, *got)
320+
})
321+
}
322+
}
323+
324+
// Test_ParallelDo_Deterministic runs the same parallel pipeline many times and
325+
// requires byte-identical output every time - a cheap guard against
326+
// non-determinism and data races in the worker/gather machinery.
327+
func Test_ParallelDo_Deterministic(t *testing.T) {
328+
t.Parallel()
329+
330+
rnd := rand.New(rand.NewSource(1))
331+
data := make([]int, 6000)
332+
for i := range data {
333+
data[i] = rnd.Intn(1000)
334+
}
335+
pred := func(x *int) bool { return *x%3 != 1 }
336+
337+
ref := make([]int, 0, len(data))
338+
for _, v := range Slice(data).Filter(pred).Do() {
339+
ref = append(ref, v*2)
340+
}
341+
342+
for i := 0; i < 30; i++ {
343+
got := Slice(data).
344+
Parallel(5).
345+
Filter(pred).
346+
Map(func(x int) int { return x * 2 }).
347+
Do()
348+
require.Equal(t, ref, got)
349+
}
350+
}
351+
352+
// Test_Erase_ParallelRoundTrip checks the erased pipe keeps a distinct pointer
353+
// per surviving element, in order, even through the parallel path.
354+
func Test_Erase_ParallelRoundTrip(t *testing.T) {
355+
t.Parallel()
356+
357+
data := make([]int, 500)
358+
for i := range data {
359+
data[i] = i
360+
}
361+
362+
got := Slice(data).
363+
Parallel(4).
364+
Filter(func(x *int) bool { return *x%2 == 0 }).
365+
Erase().
366+
Do()
367+
368+
require.Equal(t, 250, len(got))
369+
for idx, v := range got {
370+
p, ok := v.(*int)
371+
require.True(t, ok, "element %d is not a *int", idx)
372+
require.Equal(t, idx*2, *p)
373+
}
374+
}

0 commit comments

Comments
 (0)