github.com/cilium/cilium@v1.16.2/pkg/slices/slices_test.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package slices 5 6 import ( 7 "fmt" 8 "math" 9 "math/rand/v2" 10 "slices" 11 "strconv" 12 "testing" 13 14 "github.com/stretchr/testify/assert" 15 ) 16 17 var testCases = [...]struct { 18 name string 19 input []int 20 expected []int 21 }{ 22 { 23 name: "nil slice", 24 input: nil, 25 expected: nil, 26 }, 27 { 28 name: "empty slice", 29 input: []int{}, 30 expected: []int{}, 31 }, 32 { 33 name: "single element", 34 input: []int{1}, 35 expected: []int{1}, 36 }, 37 { 38 name: "all uniques", 39 input: []int{1, 3, 4, 2, 9, 7, 6, 10, 5, 8}, 40 expected: []int{1, 3, 4, 2, 9, 7, 6, 10, 5, 8}, 41 }, 42 { 43 name: "all duplicates", 44 input: []int{1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, 45 expected: []int{1}, 46 }, 47 { 48 name: "uniques and duplicates", 49 input: []int{1, 2, 2, 1, 1, 3, 1, 3, 1, 4}, 50 expected: []int{1, 2, 3, 4}, 51 }, 52 } 53 54 func TestUnique(t *testing.T) { 55 for _, tc := range testCases { 56 t.Run(tc.name, func(t *testing.T) { 57 input := slices.Clone(tc.input) 58 got := Unique(input) 59 assert.ElementsMatch(t, tc.expected, got) 60 }) 61 } 62 } 63 64 func TestUniqueFunc(t *testing.T) { 65 for _, tc := range testCases { 66 t.Run(tc.name, func(t *testing.T) { 67 input := slices.Clone(tc.input) 68 got := UniqueFunc( 69 input, 70 func(i int) int { 71 return input[i] 72 }, 73 ) 74 assert.ElementsMatch(t, tc.expected, got) 75 }) 76 } 77 } 78 79 func TestSortedUnique(t *testing.T) { 80 for _, tc := range testCases { 81 t.Run(tc.name, func(t *testing.T) { 82 input := slices.Clone(tc.input) 83 got := SortedUnique(input) 84 assert.ElementsMatch(t, tc.expected, got) 85 }) 86 } 87 } 88 89 func TestSortedUniqueFunc(t *testing.T) { 90 for _, tc := range testCases { 91 t.Run(tc.name, func(t *testing.T) { 92 input := slices.Clone(tc.input) 93 got := SortedUniqueFunc( 94 input, 95 func(i, j int) bool { 96 return input[i] < input[j] 97 }, 98 func(a, b int) bool { 99 return a == b 100 }, 101 ) 102 assert.ElementsMatch(t, tc.expected, got) 103 }) 104 } 105 } 106 107 func TestUniqueKeepOrdering(t *testing.T) { 108 input := []string{"test-4", "test-1", "test-3", "test-4", "test-4", "test-3", "test-5"} 109 expected := []*string{&input[0], &input[1], &input[2], &input[3]} 110 111 got := Unique(input) 112 113 if len(got) != len(expected) { 114 t.Fatalf("expected slice of %d elements, got %d", len(expected), len(got)) 115 } 116 117 for i := 0; i < len(expected); i++ { 118 if got[i] != *expected[i] { 119 t.Fatalf("expected value %q at index %d, got %q", *expected[i], i, got[i]) 120 } 121 122 if &got[i] != expected[i] { 123 t.Fatalf("expected address of value at index %d to be %x, got %x", i, expected[i], &got[i]) 124 } 125 } 126 } 127 128 func TestDiff(t *testing.T) { 129 testCases := []struct { 130 name string 131 a []string 132 b []string 133 expected []string 134 }{ 135 { 136 name: "empty second slice", 137 a: []string{"foo"}, 138 b: []string{}, 139 expected: []string{"foo"}, 140 }, 141 { 142 name: "empty first slice", 143 a: []string{}, 144 b: []string{"foo"}, 145 expected: nil, 146 }, 147 { 148 name: "both empty", 149 a: []string{}, 150 b: []string{}, 151 expected: nil, 152 }, 153 { 154 name: "both nil", 155 a: nil, 156 b: nil, 157 expected: nil, 158 }, 159 { 160 name: "subset", 161 a: []string{"foo", "bar"}, 162 b: []string{"foo", "bar", "baz"}, 163 expected: nil, 164 }, 165 { 166 name: "equal", 167 a: []string{"foo", "bar"}, 168 b: []string{"foo", "bar"}, 169 expected: nil, 170 }, 171 { 172 name: "same size not equal", 173 a: []string{"foo", "bar"}, 174 b: []string{"foo", "baz"}, 175 expected: []string{"bar"}, 176 }, 177 { 178 name: "smaller size", 179 a: []string{"baz"}, 180 b: []string{"foo", "bar"}, 181 expected: []string{"baz"}, 182 }, 183 { 184 name: "larger size", 185 a: []string{"foo", "bar", "fizz"}, 186 b: []string{"fizz", "buzz"}, 187 expected: []string{"foo", "bar"}, 188 }, 189 { 190 name: "subset with duplicates", 191 a: []string{"foo", "foo", "bar"}, 192 b: []string{"foo", "bar"}, 193 expected: nil, 194 }, 195 { 196 name: "subset with more duplicates", 197 a: []string{"foo", "foo", "foo", "bar", "bar"}, 198 b: []string{"foo", "foo", "bar"}, 199 expected: nil, 200 }, 201 } 202 for _, tc := range testCases { 203 t.Run(tc.name, func(t *testing.T) { 204 diff := Diff(tc.a, tc.b) 205 assert.Equal(t, tc.expected, diff) 206 }) 207 } 208 } 209 210 func TestSubsetOf(t *testing.T) { 211 testCases := []struct { 212 name string 213 a []string 214 b []string 215 isSubset bool 216 diff []string 217 }{ 218 { 219 name: "empty second slice", 220 a: []string{"foo"}, 221 b: []string{}, 222 isSubset: false, 223 diff: []string{"foo"}, 224 }, 225 { 226 name: "empty first slice", 227 a: []string{}, 228 b: []string{"foo"}, 229 isSubset: true, 230 diff: nil, 231 }, 232 { 233 name: "both empty", 234 a: []string{}, 235 b: []string{}, 236 isSubset: true, 237 diff: nil, 238 }, 239 { 240 name: "both nil", 241 a: nil, 242 b: nil, 243 isSubset: true, 244 diff: nil, 245 }, 246 { 247 name: "subset", 248 a: []string{"foo", "bar"}, 249 b: []string{"foo", "bar", "baz"}, 250 isSubset: true, 251 diff: nil, 252 }, 253 { 254 name: "equal", 255 a: []string{"foo", "bar"}, 256 b: []string{"foo", "bar"}, 257 isSubset: true, 258 diff: nil, 259 }, 260 { 261 name: "same size not equal", 262 a: []string{"foo", "bar"}, 263 b: []string{"foo", "baz"}, 264 isSubset: false, 265 diff: []string{"bar"}, 266 }, 267 { 268 name: "smaller size", 269 a: []string{"baz"}, 270 b: []string{"foo", "bar"}, 271 isSubset: false, 272 diff: []string{"baz"}, 273 }, 274 { 275 name: "larger size", 276 a: []string{"foo", "bar", "fizz"}, 277 b: []string{"fizz", "buzz"}, 278 isSubset: false, 279 diff: []string{"foo", "bar"}, 280 }, 281 { 282 name: "subset with duplicates", 283 a: []string{"foo", "foo", "bar"}, 284 b: []string{"foo", "bar"}, 285 isSubset: true, 286 diff: nil, 287 }, 288 { 289 name: "subset with more duplicates", 290 a: []string{"foo", "foo", "foo", "bar", "bar"}, 291 b: []string{"foo", "foo", "bar"}, 292 isSubset: true, 293 diff: nil, 294 }, 295 } 296 for _, tc := range testCases { 297 t.Run(tc.name, func(t *testing.T) { 298 isSubset, diff := SubsetOf(tc.a, tc.b) 299 assert.Equal(t, tc.isSubset, isSubset) 300 assert.Equal(t, tc.diff, diff) 301 }) 302 } 303 } 304 305 func TestXorNil(t *testing.T) { 306 testCases := []struct { 307 name string 308 a []string 309 b []string 310 expected bool 311 }{ 312 { 313 name: "both nil", 314 a: nil, 315 b: nil, 316 expected: false, 317 }, 318 { 319 name: "first is nil", 320 a: nil, 321 b: []string{}, 322 expected: true, 323 }, 324 { 325 name: "second is nil", 326 a: []string{}, 327 b: nil, 328 expected: true, 329 }, 330 { 331 name: "both non-nil", 332 a: []string{}, 333 b: []string{}, 334 expected: false, 335 }, 336 } 337 for _, tc := range testCases { 338 t.Run(tc.name, func(t *testing.T) { 339 assert.Equal(t, tc.expected, XorNil(tc.a, tc.b)) 340 }) 341 } 342 } 343 344 // BenchmarkUnique runs the Unique function on a slice of size elements, where each element 345 // has a probability of 20% of being a duplicate. 346 // At each iteration the slice is restored to its original status and reshuffled, in order 347 // to benchmark the average time needed to deduplicate the elements whatever their specific order. 348 // 349 // This benchmark has been used to experimentally derive the size limit for Unique beyond which 350 // the algorithm changes from a O(N^2) search to a map based approach. 351 // Forcing Unique to rely only on a single algorithm at a time and running the benchmark with count=5, 352 // the compared results extracted with benchstat are the following: 353 // 354 // name old time/op new time/op delta 355 // Unique/96-8 3.17µs ± 9% 4.83µs ±15% +52.50% (p=0.008 n=5+5) 356 // Unique/128-8 4.97µs ± 5% 5.95µs ± 2% +19.83% (p=0.008 n=5+5) 357 // Unique/160-8 7.20µs ±12% 7.33µs ± 1% ~ (p=0.690 n=5+5) 358 // Unique/192-8 9.29µs ± 3% 9.07µs ± 2% ~ (p=0.151 n=5+5) 359 // Unique/256-8 15.4µs ± 4% 11.2µs ± 2% -27.56% (p=0.008 n=5+5) 360 361 // name old alloc/op new alloc/op delta 362 // Unique/96-8 0.00B 1474.00B ± 2% +Inf% (p=0.008 n=5+5) 363 // Unique/128-8 0.00B 3100.00B ± 0% +Inf% (p=0.008 n=5+5) 364 // Unique/160-8 0.00B 3113.20B ± 0% +Inf% (p=0.008 n=5+5) 365 // Unique/192-8 0.00B 3143.20B ± 0% +Inf% (p=0.008 n=5+5) 366 // Unique/256-8 0.00B 6178.00B ± 0% +Inf% (p=0.008 n=5+5) 367 368 // name old allocs/op new allocs/op delta 369 // Unique/96-8 0.00 3.20 ±38% +Inf% (p=0.008 n=5+5) 370 // Unique/128-8 0.00 2.00 ± 0% +Inf% (p=0.008 n=5+5) 371 // Unique/160-8 0.00 3.00 ± 0% +Inf% (p=0.016 n=5+4) 372 // Unique/192-8 0.00 4.00 ± 0% +Inf% (p=0.008 n=5+5) 373 // Unique/256-8 0.00 2.00 ± 0% +Inf% (p=0.008 n=5+5) 374 // 375 // After 192 elements, the map based approach becomes more efficient. 376 // Regarding the memory, the number of allocations for the double loop algorithm is always 0, 377 // that's why benchstat is reporting "+Inf%" in the delta column. 378 // The relevant differences between the two approaches in terms of memory are shown in the previous 379 // two columns. 380 func BenchmarkUnique(b *testing.B) { 381 benchmarkUnique(b, false) 382 } 383 384 func BenchmarkUniqueFunc(b *testing.B) { 385 benchmarkUnique(b, true) 386 } 387 388 func benchmarkUnique(b *testing.B, benchUniqueFunc bool) { 389 var benchCases = [...]int{96, 128, 160, 192, 256, 512, 1024} 390 391 for _, sz := range benchCases { 392 b.Run(strconv.Itoa(sz), func(b *testing.B) { 393 b.ReportAllocs() 394 395 orig := make([]int, 0, sz) 396 orig = append(orig, rand.IntN(math.MaxInt)) 397 for i := 1; i < sz; i++ { 398 var next int 399 if rand.IntN(100) < 20 { 400 next = orig[rand.IntN(len(orig))] 401 } else { 402 next = rand.IntN(math.MaxInt) 403 } 404 orig = append(orig, next) 405 } 406 values := make([]int, len(orig)) 407 408 key := func(i int) int { 409 return values[i] 410 } 411 412 b.ResetTimer() 413 414 for i := 0; i < b.N; i++ { 415 b.StopTimer() 416 values = values[:cap(values)] 417 copy(values, orig) 418 rand.Shuffle(len(orig), func(i, j int) { 419 orig[i], orig[j] = orig[j], orig[i] 420 }) 421 if benchUniqueFunc { 422 b.StartTimer() 423 UniqueFunc(values, key) 424 } else { 425 b.StartTimer() 426 Unique(values) 427 } 428 } 429 }) 430 } 431 } 432 433 func BenchmarkSubsetOf(b *testing.B) { 434 var benchCases = [...]struct { 435 subsetSz int 436 supersetSz int 437 }{ 438 {64, 512}, {128, 512}, 439 {256, 2048}, {512, 2048}, 440 {1024, 8192}, {2048, 8192}, 441 } 442 443 for _, bc := range benchCases { 444 b.Run( 445 fmt.Sprintf("%d-%d", bc.subsetSz, bc.supersetSz), 446 func(b *testing.B) { 447 b.ReportAllocs() 448 449 subset := make([]string, 0, bc.subsetSz) 450 for i := 0; i < bc.subsetSz; i++ { 451 subset = append(subset, strconv.Itoa(rand.IntN(bc.subsetSz))) 452 } 453 454 superset := make([]string, 0, bc.supersetSz) 455 for i := 0; i < bc.supersetSz; i++ { 456 superset = append(superset, strconv.Itoa(rand.IntN(bc.subsetSz))) 457 } 458 459 b.ResetTimer() 460 461 for i := 0; i < b.N; i++ { 462 _, _ = SubsetOf(subset, superset) 463 } 464 }, 465 ) 466 } 467 }