package fn import ( "fmt" "math/rand" "slices" "testing" "testing/quick" "time" "github.com/stretchr/testify/require" ) func even(a int) bool { return a%2 == 0 } func odd(a int) bool { return a%2 != 0 } func TestAll(t *testing.T) { x := []int{0, 2, 4, 6, 8} require.True(t, All(x, even)) require.False(t, All(x, odd)) y := []int{1, 3, 5, 7, 9} require.False(t, All(y, even)) require.True(t, All(y, odd)) z := []int{0, 2, 4, 6, 9} require.False(t, All(z, even)) require.False(t, All(z, odd)) } func TestAny(t *testing.T) { x := []int{1, 3, 5, 7, 9} require.False(t, Any(x, even)) require.True(t, Any(x, odd)) y := []int{0, 3, 5, 7, 9} require.True(t, Any(y, even)) require.True(t, Any(y, odd)) z := []int{0, 2, 4, 6, 8} require.True(t, Any(z, even)) require.False(t, Any(z, odd)) } func TestMap(t *testing.T) { inc := func(i int) int { return i + 1 } x := []int{0, 2, 4, 6, 8} y := Map(x, inc) z := []int{1, 3, 5, 7, 9} require.True(t, slices.Equal(y, z)) } func TestFilter(t *testing.T) { x := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} y := Filter(x, even) require.True(t, All(y, even)) z := Filter(y, odd) require.Zero(t, len(z)) } func TestFoldl(t *testing.T) { seed := []int{} stupid := func(s []int, a int) []int { return append(s, a) } x := []int{0, 1, 2, 3, 4} r := Foldl(seed, x, stupid) require.True(t, slices.Equal(x, r)) } func TestFoldr(t *testing.T) { seed := []int{} stupid := func(a int, s []int) []int { return append(s, a) } x := []int{0, 1, 2, 3, 4} z := Foldr(seed, x, stupid) slices.Reverse[[]int](x) require.True(t, slices.Equal(x, z)) } func TestFind(t *testing.T) { x := []int{10, 11, 12, 13, 14, 15} div3 := func(a int) bool { return a%3 == 0 } div8 := func(a int) bool { return a%8 == 0 } require.Equal(t, Find(x, div3), Some(12)) require.Equal(t, Find(x, div8), None[int]()) } func TestFlatten(t *testing.T) { x := [][]int{{0}, {1}, {2}} y := Flatten(x) require.True(t, slices.Equal(y, []int{0, 1, 2})) } func TestReplicate(t *testing.T) { require.True(t, slices.Equal([]int{1, 1, 1, 1, 1}, Replicate(5, 1))) } func TestSpan(t *testing.T) { x := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} lt5 := func(a int) bool { return a < 5 } low, high := Span(x, lt5) require.True(t, slices.Equal(low, []int{0, 1, 2, 3, 4})) require.True(t, slices.Equal(high, []int{5, 6, 7, 8, 9})) } func TestSplitAt(t *testing.T) { x := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} fst, snd := SplitAt(5, x) require.True(t, slices.Equal(fst, []int{0, 1, 2, 3, 4})) require.True(t, slices.Equal(snd, []int{5, 6, 7, 8, 9})) } func TestZipWith(t *testing.T) { eq := func(a, b int) bool { return a == b } x := []int{0, 1, 2, 3, 4} y := Replicate(5, 1) z := ZipWith(x, y, eq) require.True(t, slices.Equal( z, []bool{false, true, false, false, false}, )) } // TestSum checks if the Sum function correctly calculates the sum of the // numbers in the slice. func TestSum(t *testing.T) { tests := []struct { name string items interface{} result interface{} }{ { name: "Sum of positive integers", items: []int{1, 2, 3}, result: 6, }, { name: "Sum of negative integers", items: []int{-1, -2, -3}, result: -6, }, { name: "Sum of float numbers", items: []float64{1.1, 2.2, 3.3}, result: 6.6, }, { name: "Sum of complex numbers", items: []complex128{ complex(1, 1), complex(2, 2), complex(3, 3), }, result: complex(6, 6), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { switch v := tt.items.(type) { case []int: require.Equal(t, tt.result, Sum(v)) case []float64: require.Equal(t, tt.result, Sum(v)) case []complex128: require.Equal(t, tt.result, Sum(v)) } }) } } // TestSliceToMap tests the SliceToMap function. func TestSliceToMap(t *testing.T) { tests := []struct { name string slice []int keyFunc func(int) int valueFunc func(int) string expected map[int]string }{ { name: "Integers to string map", slice: []int{1, 2, 3}, keyFunc: func(a int) int { return a }, valueFunc: func(a int) string { return fmt.Sprintf("Value%d", a) }, expected: map[int]string{ 1: "Value1", 2: "Value2", 3: "Value3", }, }, { name: "Duplicates in slice", slice: []int{1, 2, 2, 3}, keyFunc: func(a int) int { return a }, valueFunc: func(a int) string { return fmt.Sprintf("Value%d", a) }, expected: map[int]string{ 1: "Value1", 2: "Value2", 3: "Value3", }, }, { name: "Empty slice", slice: []int{}, keyFunc: func(a int) int { return a }, valueFunc: func(a int) string { return fmt.Sprintf("Value%d", a) }, expected: map[int]string{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { require.Equal( t, tt.expected, SliceToMap(tt.slice, tt.keyFunc, tt.valueFunc), ) }) } } // TestHasDuplicates tests the HasDuplicates function. func TestHasDuplicates(t *testing.T) { // Define test cases. testCases := []struct { name string items []int want bool }{ { name: "All unique", items: []int{1, 2, 3, 4, 5}, want: false, }, { name: "Some duplicates", items: []int{1, 2, 2, 3, 4}, want: true, }, { name: "No items", items: []int{}, want: false, }, { name: "All duplicates", items: []int{1, 1, 1, 1}, want: true, }, } // Execute each test case. for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { got := HasDuplicates(tc.items) require.Equal(t, tc.want, got) }) } } // TestPropForEachConcMapIsomorphism ensures the property that ForEachConc and // Map always yield the same results. func TestPropForEachConcMapIsomorphism(t *testing.T) { f := func(incSize int, s []int) bool { inc := func(i int) int { return i + incSize } mapped := Map(s, inc) conc := ForEachConc(s, inc) return slices.Equal(mapped, conc) } if err := quick.Check(f, nil); err != nil { t.Fatal(err) } } // TestPropForEachConcOutperformsMapWhenExpensive ensures the property that // ForEachConc will beat Map in a race in circumstances where the computation in // the argument closure is expensive. func TestPropForEachConcOutperformsMapWhenExpensive(t *testing.T) { f := func(incSize int, s []int) bool { if len(s) < 2 { // Intuitively we don't expect the extra overhead of // ForEachConc to justify itself for list sizes of 1 or // 0. return true } inc := func(i int) int { time.Sleep(time.Millisecond) return i + incSize } c := make(chan bool, 1) go func() { Map(s, inc) select { case c <- false: default: } }() go func() { ForEachConc(s, inc) select { case c <- true: default: } }() return <-c } if err := quick.Check(f, nil); err != nil { t.Fatal(err) } } func TestPropFindIdxFindIdentity(t *testing.T) { f := func(div, mod uint8, s []uint8) bool { if div == 0 || div == 1 { return true } pred := func(i uint8) bool { return i%div == mod } foundIdx := FindIdx(s, pred) // onlyVal :: Option[T2[A, B]] -> Option[B] onlyVal := MapOption(func(t2 T2[int, uint8]) uint8 { return t2.Second() }) valuesEqual := Find(s, pred) == onlyVal(foundIdx) idxGetsVal := ElimOption( foundIdx, func() bool { return true }, func(t2 T2[int, uint8]) bool { return s[t2.First()] == t2.Second() }) return valuesEqual && idxGetsVal } if err := quick.Check(f, nil); err != nil { t.Fatal(err) } } func TestPropLastTailIsLast(t *testing.T) { f := func(s []uint8) bool { // We exclude the singleton case because the Tail is empty. if len(s) <= 1 { return true } return Last(s) == FlatMapOption(Last[uint8])(Tail(s)) } require.NoError(t, quick.Check(f, nil)) } func TestPropHeadInitIsHead(t *testing.T) { f := func(s []uint8) bool { // We exclude the singleton case because the Init is empty. if len(s) <= 1 { return true } return Head(s) == FlatMapOption(Head[uint8])(Init(s)) } require.NoError(t, quick.Check(f, nil)) } func TestPropTailDecrementsLength(t *testing.T) { f := func(s []uint8) bool { if len(s) == 0 { return true } return Some(Len(s)-1) == MapOption(Len[uint8])(Tail(s)) } require.NoError(t, quick.Check(f, nil)) } func TestSingletonTailIsEmpty(t *testing.T) { require.Equal(t, Tail([]int{1}), Some([]int{})) } func TestSingletonInitIsEmpty(t *testing.T) { require.Equal(t, Init([]int{1}), Some([]int{})) } // TestPropAlwaysNoneEmptyFilterMap ensures the property that if we were to // always return none from our filter function then we would end up with an // empty slice. func TestPropAlwaysNoneEmptyFilterMap(t *testing.T) { f := func(s []int) bool { filtered := FilterMap(s, Const[int](None[int]())) return len(filtered) == 0 } require.NoError(t, quick.Check(f, nil)) } // TestPropFilterMapSomeIdentity ensures that if the filter function is a // trivial lift into Option space, then we will get back the original slice. func TestPropFilterMapSomeIdentity(t *testing.T) { f := func(s []int) bool { filtered := FilterMap(s, Some[int]) return slices.Equal(s, filtered) } require.NoError(t, quick.Check(f, nil)) } // TestPropFilterMapCantGrow ensures that regardless of the filter functions // return values, we will never end up with a slice larger than the original. func TestPropFilterMapCantGrow(t *testing.T) { f := func(s []int) bool { filterFunc := func(i int) Option[int] { if rand.Int()%2 == 0 { return None[int]() } return Some(i + rand.Int()) } return len(FilterMap(s, filterFunc)) <= len(s) } require.NoError(t, quick.Check(f, nil)) } // TestPropFilterMapBisectIdentity ensures that the concatenation of the // FilterMaps is the same as the FilterMap of the concatenation. func TestPropFilterMapBisectIdentity(t *testing.T) { f := func(s []int) bool { sz := len(s) first := s[0 : sz/2] second := s[sz/2 : sz] filterFunc := func(i int) Option[int] { if i%2 == 0 { return None[int]() } return Some(i) } firstFiltered := FilterMap(first, filterFunc) secondFiltered := FilterMap(second, filterFunc) allFiltered := FilterMap(s, filterFunc) reassembled := slices.Concat(firstFiltered, secondFiltered) return slices.Equal(allFiltered, reassembled) } require.NoError(t, quick.Check(f, nil)) } // TestTraverseOkIdentity ensures that trivially lifting the elements of a slice // via the Ok function during a Traverse is equivalent to just lifting the // entire slice via the Ok function. func TestPropTraverseOkIdentity(t *testing.T) { f := func(s []int) bool { traversed := TraverseResult(s, Ok[int]) traversedOk := traversed.UnwrapOrFail(t) return slices.Equal(s, traversedOk) } require.NoError(t, quick.Check(f, nil)) } // TestPropTraverseSingleErrEjection ensures that if the traverse function // returns even a single error, then the entire Traverse will error. func TestPropTraverseSingleErrEjection(t *testing.T) { f := func(s []int, errIdx uint8) bool { if len(s) == 0 { return true } errIdxMut := int(errIdx) % len(s) f := func(i int) Result[int] { if errIdxMut == 0 { return Errf[int]("err") } errIdxMut-- return Ok(i) } return TraverseResult(s, f).IsErr() } require.NoError(t, quick.Check(f, nil)) } func TestPropInitDecrementsLength(t *testing.T) { f := func(s []uint8) bool { if len(s) == 0 { return true } return Some(Len(s)-1) == MapOption(Len[uint8])(Init(s)) } require.NoError(t, quick.Check(f, nil)) } // TestPropTrimNonesEqualsFilterMapIden checks that if we use the Iden // function when calling FilterMap on a slice of Options that we get the same // result as we would if we called TrimNones on it. func TestPropTrimNonesEqualsFilterMapIden(t *testing.T) { f := func(s []uint8) bool { withNones := make([]Option[uint8], len(s)) for i, x := range s { if x%3 == 0 { withNones[i] = None[uint8]() } else { withNones[i] = Some(x) } } return slices.Equal( FilterMap(withNones, Iden[Option[uint8]]), TrimNones(withNones), ) } require.NoError(t, quick.Check(f, nil)) } // TestPropCollectResultsSingleErrEjection ensures that if there is even a // single error in the batch, then CollectResults will return an error. func TestPropCollectResultsSingleErrEjection(t *testing.T) { f := func(s []int, errIdx uint8) bool { if len(s) == 0 { return true } errIdxMut := int(errIdx) % len(s) f := func(i int) Result[int] { if errIdxMut == 0 { return Errf[int]("err") } errIdxMut-- return Ok(i) } return CollectResults(Map(s, f)).IsErr() } require.NoError(t, quick.Check(f, nil)) } // TestPropCollectResultsNoErrUnwrap ensures that if there are no errors in the // results then we end up with unwrapping all of the Results in the slice. func TestPropCollectResultsNoErrUnwrap(t *testing.T) { f := func(s []int) bool { res := CollectResults(Map(s, Ok[int])) return !res.isRight && slices.Equal(res.left, s) } require.NoError(t, quick.Check(f, nil)) } // TestPropTraverseSomeIdentity ensures that trivially lifting the elements of a // slice via the Some function during a Traverse is equivalent to just lifting // the entire slice via the Some function. func TestPropTraverseSomeIdentity(t *testing.T) { f := func(s []int) bool { traversed := TraverseOption(s, Some[int]) traversedSome := traversed.UnwrapOrFail(t) return slices.Equal(s, traversedSome) } require.NoError(t, quick.Check(f, nil)) } // TestTraverseSingleNoneEjection ensures that if the traverse function returns // even a single None, then the entire Traverse will return None. func TestTraverseSingleNoneEjection(t *testing.T) { f := func(s []int, errIdx uint8) bool { if len(s) == 0 { return true } errIdxMut := int(errIdx) % len(s) f := func(i int) Option[int] { if errIdxMut == 0 { return None[int]() } errIdxMut-- return Some(i) } return TraverseOption(s, f).IsNone() } require.NoError(t, quick.Check(f, nil)) } // TestPropCollectOptionsSingleNoneEjection ensures that if there is even a // single None in the batch, then CollectOptions will return a None. func TestPropCollectOptionsSingleNoneEjection(t *testing.T) { f := func(s []int, errIdx uint8) bool { if len(s) == 0 { return true } errIdxMut := int(errIdx) % len(s) f := func(i int) Option[int] { if errIdxMut == 0 { return None[int]() } errIdxMut-- return Some(i) } return CollectOptions(Map(s, f)).IsNone() } require.NoError(t, quick.Check(f, nil)) } // TestPropCollectOptionsNoNoneUnwrap ensures that if there are no nones in the // options then we end up with unwrapping all of the Options in the slice. func TestPropCollectOptionsNoNoneUnwrap(t *testing.T) { f := func(s []int) bool { res := CollectOptions(Map(s, Some[int])) return res.isSome && slices.Equal(res.some, s) } require.NoError(t, quick.Check(f, nil)) }