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  }