Files
summercms/modules/sieve/sieve_test.go
Jakub Zych a0bf8ece05 feat(sieve): add the remaining collection helpers and docs mentions
- Add TryMap, Filter, Reduce, KeyBy, GroupBy, Uniq, UniqBy, Partition
  and Find, plus the Set type built with SetOf (Has, Add, Slice).
- Add unit tests at 100% statement coverage and a runnable example for
  every function and Set method.
- Complete the module README: contracts, use cases, Pluck rationale,
  standard-library pointers and the full API reference.
- docs/architecture/introduction.md: add a Utilities row for sieve.
- docs/setup/coming-from-wintercms.md: map Laravel collection methods
  to sieve and narrow the not-provided row to the fluent collection
  object.
2026-10-11 14:27:13 +02:00

304 lines
7.7 KiB
Go

package sieve
import (
"errors"
"slices"
"strconv"
"testing"
)
type ids []int
type item struct {
ID int
Name string
}
func TestMap(t *testing.T) {
t.Run("nil input", func(t *testing.T) {
got := Map([]int(nil), func(v int) string { return strconv.Itoa(v) })
if got == nil || len(got) != 0 {
t.Fatalf("Map(nil) = %#v, want non-nil empty", got)
}
})
t.Run("order and call count", func(t *testing.T) {
in := []int{3, 1, 2}
snap := slices.Clone(in)
calls := 0
got := Map(in, func(v int) int { calls++; return v * 10 })
if !slices.Equal(got, []int{30, 10, 20}) {
t.Fatalf("Map = %v", got)
}
if calls != len(in) {
t.Fatalf("calls = %d, want %d", calls, len(in))
}
got[0] = 99
if !slices.Equal(in, snap) {
t.Fatalf("input changed: %v", in)
}
})
}
func TestTryMap(t *testing.T) {
t.Run("success", func(t *testing.T) {
got, err := TryMap([]string{"1", "2", "3"}, strconv.Atoi)
if err != nil || !slices.Equal(got, []int{1, 2, 3}) {
t.Fatalf("TryMap = %v, %v", got, err)
}
})
t.Run("nil input", func(t *testing.T) {
got, err := TryMap([]string(nil), strconv.Atoi)
if err != nil || got == nil || len(got) != 0 {
t.Fatalf("TryMap(nil) = %#v, %v", got, err)
}
})
sentinel := errors.New("boom")
for _, failAt := range []int{0, 2} {
t.Run("fails at "+strconv.Itoa(failAt), func(t *testing.T) {
calls := 0
got, err := TryMap([]int{0, 1, 2, 3}, func(v int) (int, error) {
calls++
if v == failAt {
return 0, sentinel
}
return v, nil
})
if got != nil {
t.Fatalf("result = %#v, want nil", got)
}
if err != sentinel || !errors.Is(err, sentinel) {
t.Fatalf("err = %v, want sentinel", err)
}
if calls != failAt+1 {
t.Fatalf("calls = %d, want %d", calls, failAt+1)
}
})
}
}
func TestFilter(t *testing.T) {
even := func(v int) bool { return v%2 == 0 }
tests := []struct {
name string
in []int
keep func(int) bool
want []int
}{
{"nil input", nil, even, []int{}},
{"no matches", []int{1, 3}, even, []int{}},
{"some matches", []int{1, 2, 3, 4}, even, []int{2, 4}},
{"all match", []int{2, 4}, even, []int{2, 4}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
snap := slices.Clone(tt.in)
got := Filter(tt.in, tt.keep)
if got == nil || !slices.Equal(got, tt.want) {
t.Fatalf("Filter = %#v, want %v", got, tt.want)
}
if len(got) > 0 {
got[0] = -1
}
if !slices.Equal(tt.in, snap) {
t.Fatalf("input changed: %v", tt.in)
}
})
}
t.Run("named type", func(t *testing.T) {
var got ids = Filter(ids{1, 2, 3}, even)
if !slices.Equal(got, ids{2}) {
t.Fatalf("Filter = %v", got)
}
})
}
func TestReduce(t *testing.T) {
sum := func(a, v int) int { return a + v }
if got := Reduce([]int{1, 2, 3}, 0, sum); got != 6 {
t.Fatalf("sum = %d", got)
}
if got := Reduce([]int(nil), 42, sum); got != 42 {
t.Fatalf("Reduce(nil) = %d, want init", got)
}
if got := Reduce([]string{"a", "b", "c"}, "", func(a, v string) string { return a + v }); got != "abc" {
t.Fatalf("concat = %q", got)
}
if got := Reduce([]int{1, 2}, "", func(a string, v int) string { return a + strconv.Itoa(v) }); got != "12" {
t.Fatalf("string accumulator = %q", got)
}
counts := Reduce([]int{1, 2, 1}, map[int]int{}, func(m map[int]int, v int) map[int]int { m[v]++; return m })
if counts[1] != 2 || counts[2] != 1 {
t.Fatalf("map accumulator = %v", counts)
}
}
func TestKeyBy(t *testing.T) {
in := []item{{1, "a"}, {2, "b"}, {1, "c"}}
calls := 0
got := KeyBy(in, func(it item) int { calls++; return it.ID })
if len(got) != 2 || got[1].Name != "c" || got[2].Name != "b" {
t.Fatalf("KeyBy = %v", got)
}
if calls != len(in) {
t.Fatalf("calls = %d", calls)
}
empty := KeyBy([]item(nil), func(it item) int { return it.ID })
if empty == nil || len(empty) != 0 {
t.Fatalf("KeyBy(nil) = %#v", empty)
}
}
func TestGroupBy(t *testing.T) {
in := ids{1, 2, 3, 4, 5}
snap := slices.Clone(in)
var got map[bool]ids = GroupBy(in, func(v int) bool { return v%2 == 0 })
if !slices.Equal(got[false], ids{1, 3, 5}) || !slices.Equal(got[true], ids{2, 4}) {
t.Fatalf("GroupBy = %v", got)
}
odd := got[false]
got[false] = append(got[false], 7)
got[false][0] = 100
odd[1] = 200
if !slices.Equal(in, snap) {
t.Fatalf("input changed: %v", in)
}
if !slices.Equal(got[true], ids{2, 4}) {
t.Fatalf("other group changed: %v", got[true])
}
empty := GroupBy([]int(nil), func(v int) int { return v })
if empty == nil || len(empty) != 0 {
t.Fatalf("GroupBy(nil) = %#v", empty)
}
}
func TestUniq(t *testing.T) {
in := []int{3, 1, 3, 2, 1}
snap := slices.Clone(in)
if got := Uniq(in); !slices.Equal(got, []int{3, 1, 2}) {
t.Fatalf("Uniq = %v", got)
}
if !slices.Equal(in, snap) {
t.Fatalf("input changed: %v", in)
}
distinct := []int{1, 2, 3}
got := Uniq(distinct)
got[0] = 9
if distinct[0] != 1 {
t.Fatalf("Uniq aliased its input")
}
empty := Uniq([]int(nil))
if empty == nil || len(empty) != 0 {
t.Fatalf("Uniq(nil) = %#v", empty)
}
var named ids = Uniq(ids{1, 1})
if !slices.Equal(named, ids{1}) {
t.Fatalf("Uniq(ids) = %v", named)
}
}
func TestUniqBy(t *testing.T) {
in := []item{{1, "a"}, {2, "b"}, {1, "c"}, {3, "d"}, {2, "e"}}
calls := 0
got := UniqBy(in, func(it item) int { calls++; return it.ID })
want := []item{{1, "a"}, {2, "b"}, {3, "d"}}
if !slices.Equal(got, want) {
t.Fatalf("UniqBy = %v", got)
}
if calls != len(in) {
t.Fatalf("calls = %d", calls)
}
empty := UniqBy([]item(nil), func(it item) int { return it.ID })
if empty == nil || len(empty) != 0 {
t.Fatalf("UniqBy(nil) = %#v", empty)
}
}
func TestPartition(t *testing.T) {
even := func(v int) bool { return v%2 == 0 }
matched, rest := Partition([]int{1, 2, 3, 4, 5}, even)
if !slices.Equal(matched, []int{2, 4}) || !slices.Equal(rest, []int{1, 3, 5}) {
t.Fatalf("Partition = %v, %v", matched, rest)
}
matched, rest = Partition([]int(nil), even)
if matched == nil || rest == nil || len(matched) != 0 || len(rest) != 0 {
t.Fatalf("Partition(nil) = %#v, %#v", matched, rest)
}
in := []int{2, 4}
matched, rest = Partition(in, even)
if rest == nil || len(rest) != 0 || !slices.Equal(matched, in) {
t.Fatalf("Partition(all) = %#v, %#v", matched, rest)
}
matched[0] = 9
if in[0] != 2 {
t.Fatalf("Partition aliased its input")
}
var m, r ids = Partition(ids{1, 2}, even)
if !slices.Equal(m, ids{2}) || !slices.Equal(r, ids{1}) {
t.Fatalf("Partition(ids) = %v, %v", m, r)
}
}
func TestFind(t *testing.T) {
calls := 0
got, ok := Find([]int{1, 4, 6, 8}, func(v int) bool { calls++; return v%2 == 0 })
if !ok || got != 4 {
t.Fatalf("Find = %d, %v", got, ok)
}
if calls != 2 {
t.Fatalf("calls = %d, want 2", calls)
}
got, ok = Find([]int{1, 3}, func(v int) bool { return v%2 == 0 })
if ok || got != 0 {
t.Fatalf("Find(no match) = %d, %v", got, ok)
}
s, ok := Find([]string(nil), func(string) bool { return true })
if ok || s != "" {
t.Fatalf("Find(nil) = %q, %v", s, ok)
}
}
func TestSet(t *testing.T) {
s := SetOf(1, 2, 2, 3)
if len(s) != 3 {
t.Fatalf("len = %d", len(s))
}
if !s.Has(2) || s.Has(4) {
t.Fatalf("Has wrong: %v", s)
}
s.Add(4, 5, 4)
if len(s) != 5 || !s.Has(5) {
t.Fatalf("Add wrong: %v", s)
}
delete(s, 1)
if s.Has(1) {
t.Fatalf("delete had no effect")
}
got := s.Slice()
slices.Sort(got)
if !slices.Equal(got, []int{2, 3, 4, 5}) {
t.Fatalf("Slice = %v", got)
}
empty := SetOf[string]()
if empty == nil {
t.Fatalf("SetOf() is nil")
}
empty.Add("a")
if !empty.Has("a") {
t.Fatalf("Add on SetOf() failed")
}
from := SetOf(ids{7, 7, 8}...)
if len(from) != 2 {
t.Fatalf("SetOf(ids...) = %v", from)
}
var zero Set[int]
if zero.Has(1) {
t.Fatalf("nil Set Has true")
}
if sl := zero.Slice(); sl == nil || len(sl) != 0 {
t.Fatalf("nil Set Slice = %#v", sl)
}
}