xref: /aosp_15_r20/build/soong/android/util_test.go (revision 333d2b3687b3a337dbcca9d65000bca186795e39)
1// Copyright 2017 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package android
16
17import (
18	"cmp"
19	"fmt"
20	"reflect"
21	"strconv"
22	"strings"
23	"testing"
24	"unsafe"
25)
26
27var firstUniqueStringsTestCases = []struct {
28	in  []string
29	out []string
30}{
31	{
32		in:  []string{"a"},
33		out: []string{"a"},
34	},
35	{
36		in:  []string{"a", "b"},
37		out: []string{"a", "b"},
38	},
39	{
40		in:  []string{"a", "a"},
41		out: []string{"a"},
42	},
43	{
44		in:  []string{"a", "b", "a"},
45		out: []string{"a", "b"},
46	},
47	{
48		in:  []string{"b", "a", "a"},
49		out: []string{"b", "a"},
50	},
51	{
52		in:  []string{"a", "a", "b"},
53		out: []string{"a", "b"},
54	},
55	{
56		in:  []string{"a", "b", "a", "b"},
57		out: []string{"a", "b"},
58	},
59	{
60		in:  []string{"liblog", "libdl", "libc++", "libdl", "libc", "libm"},
61		out: []string{"liblog", "libdl", "libc++", "libc", "libm"},
62	},
63}
64
65func TestFirstUniqueStrings(t *testing.T) {
66	f := func(t *testing.T, imp func([]string) []string, in, want []string) {
67		t.Helper()
68		out := imp(in)
69		if !reflect.DeepEqual(out, want) {
70			t.Errorf("incorrect output:")
71			t.Errorf("     input: %#v", in)
72			t.Errorf("  expected: %#v", want)
73			t.Errorf("       got: %#v", out)
74		}
75	}
76
77	for _, testCase := range firstUniqueStringsTestCases {
78		t.Run("list", func(t *testing.T) {
79			f(t, firstUniqueList[string], testCase.in, testCase.out)
80		})
81		t.Run("map", func(t *testing.T) {
82			f(t, firstUniqueMap[string], testCase.in, testCase.out)
83		})
84	}
85}
86
87var lastUniqueStringsTestCases = []struct {
88	in  []string
89	out []string
90}{
91	{
92		in:  []string{"a"},
93		out: []string{"a"},
94	},
95	{
96		in:  []string{"a", "b"},
97		out: []string{"a", "b"},
98	},
99	{
100		in:  []string{"a", "a"},
101		out: []string{"a"},
102	},
103	{
104		in:  []string{"a", "b", "a"},
105		out: []string{"b", "a"},
106	},
107	{
108		in:  []string{"b", "a", "a"},
109		out: []string{"b", "a"},
110	},
111	{
112		in:  []string{"a", "a", "b"},
113		out: []string{"a", "b"},
114	},
115	{
116		in:  []string{"a", "b", "a", "b"},
117		out: []string{"a", "b"},
118	},
119	{
120		in:  []string{"liblog", "libdl", "libc++", "libdl", "libc", "libm"},
121		out: []string{"liblog", "libc++", "libdl", "libc", "libm"},
122	},
123}
124
125func TestLastUniqueStrings(t *testing.T) {
126	for _, testCase := range lastUniqueStringsTestCases {
127		out := LastUniqueStrings(testCase.in)
128		if !reflect.DeepEqual(out, testCase.out) {
129			t.Errorf("incorrect output:")
130			t.Errorf("     input: %#v", testCase.in)
131			t.Errorf("  expected: %#v", testCase.out)
132			t.Errorf("       got: %#v", out)
133		}
134	}
135}
136
137func TestJoinWithPrefix(t *testing.T) {
138	testcases := []struct {
139		name     string
140		input    []string
141		expected string
142	}{
143		{
144			name:     "zero_inputs",
145			input:    []string{},
146			expected: "",
147		},
148		{
149			name:     "one_input",
150			input:    []string{"a"},
151			expected: "prefix:a",
152		},
153		{
154			name:     "two_inputs",
155			input:    []string{"a", "b"},
156			expected: "prefix:a prefix:b",
157		},
158	}
159
160	prefix := "prefix:"
161
162	for _, testCase := range testcases {
163		t.Run(testCase.name, func(t *testing.T) {
164			out := JoinWithPrefix(testCase.input, prefix)
165			if out != testCase.expected {
166				t.Errorf("incorrect output:")
167				t.Errorf("     input: %#v", testCase.input)
168				t.Errorf("    prefix: %#v", prefix)
169				t.Errorf("  expected: %#v", testCase.expected)
170				t.Errorf("       got: %#v", out)
171			}
172		})
173	}
174}
175
176func TestIndexList(t *testing.T) {
177	input := []string{"a", "b", "c"}
178
179	testcases := []struct {
180		key      string
181		expected int
182	}{
183		{
184			key:      "a",
185			expected: 0,
186		},
187		{
188			key:      "b",
189			expected: 1,
190		},
191		{
192			key:      "c",
193			expected: 2,
194		},
195		{
196			key:      "X",
197			expected: -1,
198		},
199	}
200
201	for _, testCase := range testcases {
202		t.Run(testCase.key, func(t *testing.T) {
203			out := IndexList(testCase.key, input)
204			if out != testCase.expected {
205				t.Errorf("incorrect output:")
206				t.Errorf("       key: %#v", testCase.key)
207				t.Errorf("     input: %#v", input)
208				t.Errorf("  expected: %#v", testCase.expected)
209				t.Errorf("       got: %#v", out)
210			}
211		})
212	}
213}
214
215func TestInList(t *testing.T) {
216	input := []string{"a"}
217
218	testcases := []struct {
219		key      string
220		expected bool
221	}{
222		{
223			key:      "a",
224			expected: true,
225		},
226		{
227			key:      "X",
228			expected: false,
229		},
230	}
231
232	for _, testCase := range testcases {
233		t.Run(testCase.key, func(t *testing.T) {
234			out := InList(testCase.key, input)
235			if out != testCase.expected {
236				t.Errorf("incorrect output:")
237				t.Errorf("       key: %#v", testCase.key)
238				t.Errorf("     input: %#v", input)
239				t.Errorf("  expected: %#v", testCase.expected)
240				t.Errorf("       got: %#v", out)
241			}
242		})
243	}
244}
245
246func TestPrefixInList(t *testing.T) {
247	prefixes := []string{"a", "b"}
248
249	testcases := []struct {
250		str      string
251		expected bool
252	}{
253		{
254			str:      "a-example",
255			expected: true,
256		},
257		{
258			str:      "b-example",
259			expected: true,
260		},
261		{
262			str:      "X-example",
263			expected: false,
264		},
265	}
266
267	for _, testCase := range testcases {
268		t.Run(testCase.str, func(t *testing.T) {
269			out := HasAnyPrefix(testCase.str, prefixes)
270			if out != testCase.expected {
271				t.Errorf("incorrect output:")
272				t.Errorf("       str: %#v", testCase.str)
273				t.Errorf("  prefixes: %#v", prefixes)
274				t.Errorf("  expected: %#v", testCase.expected)
275				t.Errorf("       got: %#v", out)
276			}
277		})
278	}
279}
280
281func TestFilterList(t *testing.T) {
282	input := []string{"a", "b", "c", "c", "b", "d", "a"}
283	filter := []string{"a", "c"}
284	remainder, filtered := FilterList(input, filter)
285
286	expected := []string{"b", "b", "d"}
287	if !reflect.DeepEqual(remainder, expected) {
288		t.Errorf("incorrect remainder output:")
289		t.Errorf("     input: %#v", input)
290		t.Errorf("    filter: %#v", filter)
291		t.Errorf("  expected: %#v", expected)
292		t.Errorf("       got: %#v", remainder)
293	}
294
295	expected = []string{"a", "c", "c", "a"}
296	if !reflect.DeepEqual(filtered, expected) {
297		t.Errorf("incorrect filtered output:")
298		t.Errorf("     input: %#v", input)
299		t.Errorf("    filter: %#v", filter)
300		t.Errorf("  expected: %#v", expected)
301		t.Errorf("       got: %#v", filtered)
302	}
303}
304
305func TestFilterListPred(t *testing.T) {
306	pred := func(s string) bool { return strings.HasPrefix(s, "a/") }
307	AssertArrayString(t, "filter", FilterListPred([]string{"a/c", "b/a", "a/b"}, pred), []string{"a/c", "a/b"})
308	AssertArrayString(t, "filter", FilterListPred([]string{"b/c", "a/a", "b/b"}, pred), []string{"a/a"})
309	AssertArrayString(t, "filter", FilterListPred([]string{"c/c", "b/a", "c/b"}, pred), []string{})
310	AssertArrayString(t, "filter", FilterListPred([]string{"a/c", "a/a", "a/b"}, pred), []string{"a/c", "a/a", "a/b"})
311}
312
313func TestRemoveListFromList(t *testing.T) {
314	input := []string{"a", "b", "c", "d", "a", "c", "d"}
315	filter := []string{"a", "c"}
316	expected := []string{"b", "d", "d"}
317	out := RemoveListFromList(input, filter)
318	if !reflect.DeepEqual(out, expected) {
319		t.Errorf("incorrect output:")
320		t.Errorf("     input: %#v", input)
321		t.Errorf("    filter: %#v", filter)
322		t.Errorf("  expected: %#v", expected)
323		t.Errorf("       got: %#v", out)
324	}
325}
326
327func TestRemoveFromList(t *testing.T) {
328	testcases := []struct {
329		name          string
330		key           string
331		input         []string
332		expectedFound bool
333		expectedOut   []string
334	}{
335		{
336			name:          "remove_one_match",
337			key:           "a",
338			input:         []string{"a", "b", "c"},
339			expectedFound: true,
340			expectedOut:   []string{"b", "c"},
341		},
342		{
343			name:          "remove_three_matches",
344			key:           "a",
345			input:         []string{"a", "b", "a", "c", "a"},
346			expectedFound: true,
347			expectedOut:   []string{"b", "c"},
348		},
349		{
350			name:          "remove_zero_matches",
351			key:           "X",
352			input:         []string{"a", "b", "a", "c", "a"},
353			expectedFound: false,
354			expectedOut:   []string{"a", "b", "a", "c", "a"},
355		},
356		{
357			name:          "remove_all_matches",
358			key:           "a",
359			input:         []string{"a", "a", "a", "a"},
360			expectedFound: true,
361			expectedOut:   []string{},
362		},
363	}
364
365	for _, testCase := range testcases {
366		t.Run(testCase.name, func(t *testing.T) {
367			found, out := RemoveFromList(testCase.key, testCase.input)
368			if found != testCase.expectedFound {
369				t.Errorf("incorrect output:")
370				t.Errorf("       key: %#v", testCase.key)
371				t.Errorf("     input: %#v", testCase.input)
372				t.Errorf("  expected: %#v", testCase.expectedFound)
373				t.Errorf("       got: %#v", found)
374			}
375			if !reflect.DeepEqual(out, testCase.expectedOut) {
376				t.Errorf("incorrect output:")
377				t.Errorf("       key: %#v", testCase.key)
378				t.Errorf("     input: %#v", testCase.input)
379				t.Errorf("  expected: %#v", testCase.expectedOut)
380				t.Errorf("       got: %#v", out)
381			}
382		})
383	}
384}
385
386func TestCopyOfEmptyAndNil(t *testing.T) {
387	emptyList := []string{}
388	copyOfEmptyList := CopyOf(emptyList)
389	AssertBoolEquals(t, "Copy of an empty list should be an empty list and not nil", true, copyOfEmptyList != nil)
390	copyOfNilList := CopyOf([]string(nil))
391	AssertBoolEquals(t, "Copy of a nil list should be a nil list and not an empty list", true, copyOfNilList == nil)
392}
393
394func ExampleCopyOf() {
395	a := []string{"1", "2", "3"}
396	b := CopyOf(a)
397	a[0] = "-1"
398	fmt.Printf("a = %q\n", a)
399	fmt.Printf("b = %q\n", b)
400
401	// Output:
402	// a = ["-1" "2" "3"]
403	// b = ["1" "2" "3"]
404}
405
406func ExampleCopyOf_append() {
407	a := make([]string, 1, 2)
408	a[0] = "foo"
409
410	fmt.Println("Without CopyOf:")
411	b := append(a, "bar")
412	c := append(a, "baz")
413	fmt.Printf("a = %q\n", a)
414	fmt.Printf("b = %q\n", b)
415	fmt.Printf("c = %q\n", c)
416
417	a = make([]string, 1, 2)
418	a[0] = "foo"
419
420	fmt.Println("With CopyOf:")
421	b = append(CopyOf(a), "bar")
422	c = append(CopyOf(a), "baz")
423	fmt.Printf("a = %q\n", a)
424	fmt.Printf("b = %q\n", b)
425	fmt.Printf("c = %q\n", c)
426
427	// Output:
428	// Without CopyOf:
429	// a = ["foo"]
430	// b = ["foo" "baz"]
431	// c = ["foo" "baz"]
432	// With CopyOf:
433	// a = ["foo"]
434	// b = ["foo" "bar"]
435	// c = ["foo" "baz"]
436}
437
438func TestSplitFileExt(t *testing.T) {
439	t.Run("soname with version", func(t *testing.T) {
440		root, suffix, ext := SplitFileExt("libtest.so.1.0.30")
441		expected := "libtest"
442		if root != expected {
443			t.Errorf("root should be %q but got %q", expected, root)
444		}
445		expected = ".so.1.0.30"
446		if suffix != expected {
447			t.Errorf("suffix should be %q but got %q", expected, suffix)
448		}
449		expected = ".so"
450		if ext != expected {
451			t.Errorf("ext should be %q but got %q", expected, ext)
452		}
453	})
454
455	t.Run("soname with svn version", func(t *testing.T) {
456		root, suffix, ext := SplitFileExt("libtest.so.1svn")
457		expected := "libtest"
458		if root != expected {
459			t.Errorf("root should be %q but got %q", expected, root)
460		}
461		expected = ".so.1svn"
462		if suffix != expected {
463			t.Errorf("suffix should be %q but got %q", expected, suffix)
464		}
465		expected = ".so"
466		if ext != expected {
467			t.Errorf("ext should be %q but got %q", expected, ext)
468		}
469	})
470
471	t.Run("version numbers in the middle should be ignored", func(t *testing.T) {
472		root, suffix, ext := SplitFileExt("libtest.1.0.30.so")
473		expected := "libtest.1.0.30"
474		if root != expected {
475			t.Errorf("root should be %q but got %q", expected, root)
476		}
477		expected = ".so"
478		if suffix != expected {
479			t.Errorf("suffix should be %q but got %q", expected, suffix)
480		}
481		expected = ".so"
482		if ext != expected {
483			t.Errorf("ext should be %q but got %q", expected, ext)
484		}
485	})
486
487	t.Run("no known file extension", func(t *testing.T) {
488		root, suffix, ext := SplitFileExt("test.exe")
489		expected := "test"
490		if root != expected {
491			t.Errorf("root should be %q but got %q", expected, root)
492		}
493		expected = ".exe"
494		if suffix != expected {
495			t.Errorf("suffix should be %q but got %q", expected, suffix)
496		}
497		if ext != expected {
498			t.Errorf("ext should be %q but got %q", expected, ext)
499		}
500	})
501}
502
503func Test_Shard(t *testing.T) {
504	type args struct {
505		strings   []string
506		shardSize int
507	}
508	tests := []struct {
509		name string
510		args args
511		want [][]string
512	}{
513		{
514			name: "empty",
515			args: args{
516				strings:   nil,
517				shardSize: 1,
518			},
519			want: [][]string(nil),
520		},
521		{
522			name: "single shard",
523			args: args{
524				strings:   []string{"a", "b"},
525				shardSize: 2,
526			},
527			want: [][]string{{"a", "b"}},
528		},
529		{
530			name: "single short shard",
531			args: args{
532				strings:   []string{"a", "b"},
533				shardSize: 3,
534			},
535			want: [][]string{{"a", "b"}},
536		},
537		{
538			name: "shard per input",
539			args: args{
540				strings:   []string{"a", "b", "c"},
541				shardSize: 1,
542			},
543			want: [][]string{{"a"}, {"b"}, {"c"}},
544		},
545		{
546			name: "balanced shards",
547			args: args{
548				strings:   []string{"a", "b", "c", "d"},
549				shardSize: 2,
550			},
551			want: [][]string{{"a", "b"}, {"c", "d"}},
552		},
553		{
554			name: "unbalanced shards",
555			args: args{
556				strings:   []string{"a", "b", "c"},
557				shardSize: 2,
558			},
559			want: [][]string{{"a", "b"}, {"c"}},
560		},
561	}
562	for _, tt := range tests {
563		t.Run(tt.name, func(t *testing.T) {
564			t.Run("strings", func(t *testing.T) {
565				if got := ShardStrings(tt.args.strings, tt.args.shardSize); !reflect.DeepEqual(got, tt.want) {
566					t.Errorf("ShardStrings(%v, %v) = %v, want %v",
567						tt.args.strings, tt.args.shardSize, got, tt.want)
568				}
569			})
570
571			t.Run("paths", func(t *testing.T) {
572				stringsToPaths := func(strings []string) Paths {
573					if strings == nil {
574						return nil
575					}
576					paths := make(Paths, len(strings))
577					for i, s := range strings {
578						paths[i] = PathForTesting(s)
579					}
580					return paths
581				}
582
583				paths := stringsToPaths(tt.args.strings)
584
585				var want []Paths
586				if sWant := tt.want; sWant != nil {
587					want = make([]Paths, len(sWant))
588					for i, w := range sWant {
589						want[i] = stringsToPaths(w)
590					}
591				}
592
593				if got := ShardPaths(paths, tt.args.shardSize); !reflect.DeepEqual(got, want) {
594					t.Errorf("ShardPaths(%v, %v) = %v, want %v",
595						paths, tt.args.shardSize, got, want)
596				}
597			})
598		})
599	}
600}
601
602func BenchmarkFirstUniqueStrings(b *testing.B) {
603	implementations := []struct {
604		name string
605		f    func([]string) []string
606	}{
607		{
608			name: "list",
609			f:    firstUniqueList[string],
610		},
611		{
612			name: "map",
613			f:    firstUniqueMap[string],
614		},
615		{
616			name: "optimal",
617			f:    FirstUniqueStrings,
618		},
619	}
620	const maxSize = 1024
621	uniqueStrings := make([]string, maxSize)
622	for i := range uniqueStrings {
623		uniqueStrings[i] = strconv.Itoa(i)
624	}
625	sameString := make([]string, maxSize)
626	for i := range sameString {
627		sameString[i] = uniqueStrings[0]
628	}
629
630	f := func(b *testing.B, imp func([]string) []string, s []string) {
631		for i := 0; i < b.N; i++ {
632			b.ReportAllocs()
633			s = append([]string(nil), s...)
634			imp(s)
635		}
636	}
637
638	for n := 1; n <= maxSize; n <<= 1 {
639		b.Run(strconv.Itoa(n), func(b *testing.B) {
640			for _, implementation := range implementations {
641				b.Run(implementation.name, func(b *testing.B) {
642					b.Run("same", func(b *testing.B) {
643						f(b, implementation.f, sameString[:n])
644					})
645					b.Run("unique", func(b *testing.B) {
646						f(b, implementation.f, uniqueStrings[:n])
647					})
648				})
649			}
650		})
651	}
652}
653
654func testSortedKeysHelper[K cmp.Ordered, V any](t *testing.T, name string, input map[K]V, expected []K) {
655	t.Helper()
656	t.Run(name, func(t *testing.T) {
657		actual := SortedKeys(input)
658		if !reflect.DeepEqual(actual, expected) {
659			t.Errorf("expected %v, got %v", expected, actual)
660		}
661	})
662}
663
664func TestSortedKeys(t *testing.T) {
665	testSortedKeysHelper(t, "simple", map[string]string{
666		"b": "bar",
667		"a": "foo",
668	}, []string{
669		"a",
670		"b",
671	})
672	testSortedKeysHelper(t, "ints", map[int]interface{}{
673		10: nil,
674		5:  nil,
675	}, []int{
676		5,
677		10,
678	})
679
680	testSortedKeysHelper(t, "nil", map[string]string(nil), nil)
681	testSortedKeysHelper(t, "empty", map[string]string{}, nil)
682}
683
684func TestSortedStringValues(t *testing.T) {
685	testCases := []struct {
686		name     string
687		in       interface{}
688		expected []string
689	}{
690		{
691			name:     "nil",
692			in:       map[string]string(nil),
693			expected: nil,
694		},
695		{
696			name:     "empty",
697			in:       map[string]string{},
698			expected: nil,
699		},
700		{
701			name:     "simple",
702			in:       map[string]string{"foo": "a", "bar": "b"},
703			expected: []string{"a", "b"},
704		},
705		{
706			name:     "duplicates",
707			in:       map[string]string{"foo": "a", "bar": "b", "baz": "b"},
708			expected: []string{"a", "b", "b"},
709		},
710	}
711
712	for _, tt := range testCases {
713		t.Run(tt.name, func(t *testing.T) {
714			got := SortedStringValues(tt.in)
715			if g, w := got, tt.expected; !reflect.DeepEqual(g, w) {
716				t.Errorf("wanted %q, got %q", w, g)
717			}
718		})
719	}
720}
721
722func TestSortedUniqueStringValues(t *testing.T) {
723	testCases := []struct {
724		name     string
725		in       interface{}
726		expected []string
727	}{
728		{
729			name:     "nil",
730			in:       map[string]string(nil),
731			expected: nil,
732		},
733		{
734			name:     "empty",
735			in:       map[string]string{},
736			expected: nil,
737		},
738		{
739			name:     "simple",
740			in:       map[string]string{"foo": "a", "bar": "b"},
741			expected: []string{"a", "b"},
742		},
743		{
744			name:     "duplicates",
745			in:       map[string]string{"foo": "a", "bar": "b", "baz": "b"},
746			expected: []string{"a", "b"},
747		},
748	}
749
750	for _, tt := range testCases {
751		t.Run(tt.name, func(t *testing.T) {
752			got := SortedUniqueStringValues(tt.in)
753			if g, w := got, tt.expected; !reflect.DeepEqual(g, w) {
754				t.Errorf("wanted %q, got %q", w, g)
755			}
756		})
757	}
758}
759
760var reverseTestCases = []struct {
761	name     string
762	in       []string
763	expected []string
764}{
765	{
766		name:     "nil",
767		in:       nil,
768		expected: nil,
769	},
770	{
771		name:     "empty",
772		in:       []string{},
773		expected: []string{},
774	},
775	{
776		name:     "one",
777		in:       []string{"one"},
778		expected: []string{"one"},
779	},
780	{
781		name:     "even",
782		in:       []string{"one", "two"},
783		expected: []string{"two", "one"},
784	},
785	{
786		name:     "odd",
787		in:       []string{"one", "two", "three"},
788		expected: []string{"three", "two", "one"},
789	},
790}
791
792func TestReverseSliceInPlace(t *testing.T) {
793	for _, testCase := range reverseTestCases {
794		t.Run(testCase.name, func(t *testing.T) {
795			slice := CopyOf(testCase.in)
796			slice2 := slice
797			ReverseSliceInPlace(slice)
798			if !reflect.DeepEqual(slice, testCase.expected) {
799				t.Errorf("expected %#v, got %#v", testCase.expected, slice)
800			}
801			if unsafe.SliceData(slice) != unsafe.SliceData(slice2) {
802				t.Errorf("expected slices to share backing array")
803			}
804		})
805	}
806}
807
808func TestReverseSlice(t *testing.T) {
809	for _, testCase := range reverseTestCases {
810		t.Run(testCase.name, func(t *testing.T) {
811			slice := ReverseSlice(testCase.in)
812			if !reflect.DeepEqual(slice, testCase.expected) {
813				t.Errorf("expected %#v, got %#v", testCase.expected, slice)
814			}
815			if cap(slice) > 0 && unsafe.SliceData(testCase.in) == unsafe.SliceData(slice) {
816				t.Errorf("expected slices to have different backing arrays")
817			}
818		})
819	}
820}
821
822var hasIntersectionTestCases = []struct {
823	name     string
824	l1       []string
825	l2       []string
826	expected bool
827}{
828	{
829		name:     "empty",
830		l1:       []string{"a", "b", "c"},
831		l2:       []string{},
832		expected: false,
833	},
834	{
835		name:     "both empty",
836		l1:       []string{},
837		l2:       []string{},
838		expected: false,
839	},
840	{
841		name:     "identical",
842		l1:       []string{"a", "b", "c"},
843		l2:       []string{"a", "b", "c"},
844		expected: true,
845	},
846	{
847		name:     "duplicates",
848		l1:       []string{"a", "a", "a"},
849		l2:       []string{"a", "b", "c"},
850		expected: true,
851	},
852	{
853		name:     "duplicates with no intersection",
854		l1:       []string{"d", "d", "d", "d"},
855		l2:       []string{"a", "b", "c"},
856		expected: false,
857	},
858}
859
860func TestHasIntersection(t *testing.T) {
861	for _, testCase := range hasIntersectionTestCases {
862		t.Run(testCase.name, func(t *testing.T) {
863			hasIntersection := HasIntersection(testCase.l1, testCase.l2)
864			if !reflect.DeepEqual(hasIntersection, testCase.expected) {
865				t.Errorf("expected %#v, got %#v", testCase.expected, hasIntersection)
866			}
867		})
868	}
869}
870
871var prettyConcatTestCases = []struct {
872	name          string
873	list          []string
874	quote         bool
875	lastSeparator string
876	expected      string
877}{
878	{
879		name:          "empty",
880		list:          []string{},
881		quote:         false,
882		lastSeparator: "and",
883		expected:      ``,
884	},
885	{
886		name:          "single",
887		list:          []string{"a"},
888		quote:         true,
889		lastSeparator: "and",
890		expected:      `"a"`,
891	},
892	{
893		name:          "with separator",
894		list:          []string{"a", "b", "c"},
895		quote:         true,
896		lastSeparator: "or",
897		expected:      `"a", "b", or "c"`,
898	},
899	{
900		name:          "without separator",
901		list:          []string{"a", "b", "c"},
902		quote:         false,
903		lastSeparator: "",
904		expected:      `a, b, c`,
905	},
906}
907
908func TestPrettyConcat(t *testing.T) {
909	for _, testCase := range prettyConcatTestCases {
910		t.Run(testCase.name, func(t *testing.T) {
911			concatString := PrettyConcat(testCase.list, testCase.quote, testCase.lastSeparator)
912			if !reflect.DeepEqual(concatString, testCase.expected) {
913				t.Errorf("expected %#v, got %#v", testCase.expected, concatString)
914			}
915		})
916	}
917}
918