diff --git a/README.md b/README.md index d429ca9c..385e9a25 100644 --- a/README.md +++ b/README.md @@ -342,6 +342,8 @@ See [FAQ](https://go-testdeep.zetta.rocks/faq/). [`Shallow`]: https://go-testdeep.zetta.rocks/operators/shallow/ [`Slice`]: https://go-testdeep.zetta.rocks/operators/slice/ [`Smuggle`]: https://go-testdeep.zetta.rocks/operators/smuggle/ +[`Sort`]: https://go-testdeep.zetta.rocks/operators/sort/ +[`Sorted`]: https://go-testdeep.zetta.rocks/operators/sorted/ [`SStruct`]: https://go-testdeep.zetta.rocks/operators/sstruct/ [`String`]: https://go-testdeep.zetta.rocks/operators/string/ [`Struct`]: https://go-testdeep.zetta.rocks/operators/struct/ @@ -407,6 +409,8 @@ See [FAQ](https://go-testdeep.zetta.rocks/faq/). [`CmpShallow`]: https://go-testdeep.zetta.rocks/operators/shallow/#cmpshallow-shortcut [`CmpSlice`]: https://go-testdeep.zetta.rocks/operators/slice/#cmpslice-shortcut [`CmpSmuggle`]: https://go-testdeep.zetta.rocks/operators/smuggle/#cmpsmuggle-shortcut +[`CmpSort`]: https://go-testdeep.zetta.rocks/operators/sort/#cmpsort-shortcut +[`CmpSorted`]: https://go-testdeep.zetta.rocks/operators/sorted/#cmpsorted-shortcut [`CmpSStruct`]: https://go-testdeep.zetta.rocks/operators/sstruct/#cmpsstruct-shortcut [`CmpString`]: https://go-testdeep.zetta.rocks/operators/string/#cmpstring-shortcut [`CmpStruct`]: https://go-testdeep.zetta.rocks/operators/struct/#cmpstruct-shortcut @@ -471,6 +475,8 @@ See [FAQ](https://go-testdeep.zetta.rocks/faq/). [`T.Shallow`]: https://go-testdeep.zetta.rocks/operators/shallow/#tshallow-shortcut [`T.Slice`]: https://go-testdeep.zetta.rocks/operators/slice/#tslice-shortcut [`T.Smuggle`]: https://go-testdeep.zetta.rocks/operators/smuggle/#tsmuggle-shortcut +[`T.Sort`]: https://go-testdeep.zetta.rocks/operators/sort/#tsort-shortcut +[`T.Sorted`]: https://go-testdeep.zetta.rocks/operators/sorted/#tsorted-shortcut [`T.SStruct`]: https://go-testdeep.zetta.rocks/operators/sstruct/#tsstruct-shortcut [`T.String`]: https://go-testdeep.zetta.rocks/operators/string/#tstring-shortcut [`T.Struct`]: https://go-testdeep.zetta.rocks/operators/struct/#tstruct-shortcut diff --git a/helpers/tdutil/map.go b/helpers/tdutil/map.go index 4c7778f5..8245e363 100644 --- a/helpers/tdutil/map.go +++ b/helpers/tdutil/map.go @@ -10,6 +10,7 @@ import ( "reflect" "sort" + "github.com/maxatome/go-testdeep/internal/compare" "github.com/maxatome/go-testdeep/internal/visited" ) @@ -44,7 +45,7 @@ func newKvSlice(l int) *kvSlice { func (s *kvSlice) Len() int { return len(s.s) } func (s *kvSlice) Less(i, j int) bool { - return cmp(s.v, s.s[i].key, s.s[j].key) < 0 + return compare.Compare(s.v, s.s[i].key, s.s[j].key) < 0 } func (s *kvSlice) Swap(i, j int) { s.s[i], s.s[j] = s.s[j], s.s[i] } diff --git a/helpers/tdutil/sort_test.go b/helpers/tdutil/sort_test.go deleted file mode 100644 index 205bb59e..00000000 --- a/helpers/tdutil/sort_test.go +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright (c) 2019, Maxime Soulé -// All rights reserved. -// -// This source code is licensed under the BSD-style license found in the -// LICENSE file in the root directory of this source tree. - -package tdutil - -import ( - "math" - "reflect" - "testing" - - "github.com/maxatome/go-testdeep/internal/visited" -) - -func TestSortCmp(t *testing.T) { - checkCmp := func(a, b any, expected int) { - t.Helper() - got := cmp(visited.NewVisited(), reflect.ValueOf(a), reflect.ValueOf(b)) - if got != expected { - t.Errorf("cmp() failed: got=%d expected=%d\n", got, expected) - } - } - - // IsValid - checkCmp(nil, 12, -1) - checkCmp(nil, nil, 0) - checkCmp(12, nil, 1) - - // type mismatch: int is before string - checkCmp(42, "str", -1) - checkCmp("str", 42, 1) - - // bool - checkCmp(true, true, 0) - checkCmp(true, false, 1) - checkCmp(false, true, -1) - checkCmp(false, false, 0) - - // int - checkCmp(12, 42, -1) - checkCmp(42, 12, 1) - checkCmp(12, 12, 0) - - checkCmp(int8(12), int8(42), -1) - checkCmp(int8(42), int8(12), 1) - checkCmp(int8(12), int8(12), 0) - - checkCmp(int16(12), int16(42), -1) - checkCmp(int16(42), int16(12), 1) - checkCmp(int16(12), int16(12), 0) - - checkCmp(int32(12), int32(42), -1) - checkCmp(int32(42), int32(12), 1) - checkCmp(int32(12), int32(12), 0) - - checkCmp(int64(12), int64(42), -1) - checkCmp(int64(42), int64(12), 1) - checkCmp(int64(12), int64(12), 0) - - // uint - checkCmp(uint(12), uint(42), -1) - checkCmp(uint(42), uint(12), 1) - checkCmp(uint(12), uint(12), 0) - - checkCmp(uint8(12), uint8(42), -1) - checkCmp(uint8(42), uint8(12), 1) - checkCmp(uint8(12), uint8(12), 0) - - checkCmp(uint16(12), uint16(42), -1) - checkCmp(uint16(42), uint16(12), 1) - checkCmp(uint16(12), uint16(12), 0) - - checkCmp(uint32(12), uint32(42), -1) - checkCmp(uint32(42), uint32(12), 1) - checkCmp(uint32(12), uint32(12), 0) - - checkCmp(uint64(12), uint64(42), -1) - checkCmp(uint64(42), uint64(12), 1) - checkCmp(uint64(12), uint64(12), 0) - - checkCmp(uintptr(12), uintptr(42), -1) - checkCmp(uintptr(42), uintptr(12), 1) - checkCmp(uintptr(12), uintptr(12), 0) - - // float - checkCmp(float32(12), float32(42), -1) - checkCmp(float32(42), float32(12), 1) - checkCmp(float32(12), float32(12), 0) - - checkCmp(float64(12), float64(42), -1) - checkCmp(float64(42), float64(12), 1) - checkCmp(float64(12), float64(12), 0) - checkCmp(float64(12), float64(12), 0) - - checkCmp(math.NaN(), float64(12), -1) - checkCmp(math.NaN(), math.NaN(), -1) - checkCmp(float64(12), math.NaN(), 1) - - // complex - checkCmp(complex(12, 0), complex(42, 0), -1) - checkCmp(complex(42, 0), complex(12, 0), 1) - checkCmp(complex(0, 12), complex(0, 42), -1) - checkCmp(complex(0, 42), complex(0, 12), 1) - checkCmp(complex(12, 0), complex(12, 0), 0) - - checkCmp(complex(float32(12), 0), complex(float32(42), 0), -1) - checkCmp(complex(float32(42), 0), complex(float32(12), 0), 1) - checkCmp(complex(float32(0), 12), complex(float32(0), 42), -1) - checkCmp(complex(float32(0), 42), complex(float32(0), 12), 1) - checkCmp(complex(float32(12), 0), complex(float32(12), 0), 0) - - // string - checkCmp("aaa", "bbb", -1) - checkCmp("bbb", "aaa", 1) - checkCmp("aaa", "aaa", 0) - - // array - checkCmp([3]byte{1, 2, 3}, [3]byte{3, 1, 2}, -1) - checkCmp([3]byte{3, 1, 2}, [3]byte{1, 2, 3}, 1) - checkCmp([3]byte{1, 2, 3}, [3]byte{1, 2, 3}, 0) - - // slice - checkCmp([]byte{1, 2, 3}, []byte{3, 1, 2}, -1) - checkCmp([]byte{3, 1, 2}, []byte{1, 2, 3}, 1) - checkCmp([]byte{1, 2, 3}, []byte{1, 2, 3}, 0) - checkCmp([]byte{1, 2, 3}, []byte{1, 2, 3, 4}, -1) - checkCmp([]byte{1, 2, 3, 4}, []byte{1, 2, 3}, 1) - - // interface - checkCmp([]any{1}, []any{3}, -1) - checkCmp([]any{3}, []any{1}, 1) - checkCmp([]any{1}, []any{1}, 0) - checkCmp([]any{nil}, []any{nil}, 0) - checkCmp([]any{nil}, []any{1}, -1) - checkCmp([]any{1}, []any{nil}, 1) - - // struct - type myStruct struct { - n int - s string - p *myStruct - } - checkCmp(myStruct{n: 12, s: "a"}, myStruct{n: 12, s: "b"}, -1) - checkCmp(myStruct{n: 12, s: "b"}, myStruct{n: 12, s: "a"}, 1) - checkCmp(myStruct{n: 12, s: "a"}, myStruct{n: 12, s: "a"}, 0) - - // ptr - a, b := 12, 42 - checkCmp(&a, &a, 0) - checkCmp((*int)(nil), (*int)(nil), 0) - checkCmp(&a, &b, -1) - checkCmp((*int)(nil), &b, -1) - checkCmp(&b, &a, 1) - checkCmp(&b, (*int)(nil), 1) - - // map - ma, mb := map[int]bool{12: true}, map[int]bool{12: true, 13: false} - checkCmp(ma, mb, -1) - checkCmp(mb, ma, 1) - checkCmp(ma, ma, 0) - checkCmp((map[int]bool)(nil), (map[int]bool)(nil), 0) - checkCmp((map[int]bool)(nil), ma, -1) - checkCmp(ma, (map[int]bool)(nil), 1) - - // cyclic references protection - pa := &myStruct{n: 42, p: &myStruct{n: 18}} - pa.p.p = pa.p - pb := &myStruct{n: 42, p: &myStruct{n: 18}} - pb.p.p = pb.p - checkCmp(pa, pb, 0) -} diff --git a/helpers/tdutil/sort_values.go b/helpers/tdutil/sort_values.go index 5a8f77ca..373efbc4 100644 --- a/helpers/tdutil/sort_values.go +++ b/helpers/tdutil/sort_values.go @@ -10,6 +10,7 @@ import ( "reflect" "sort" + "github.com/maxatome/go-testdeep/internal/compare" "github.com/maxatome/go-testdeep/internal/visited" ) @@ -24,10 +25,12 @@ import ( // documentation. // // Sorting rules are as follows: +// - invalid value is always lower // - nil is always lower // - different types are sorted by their name +// - if method TYPE.Compare(TYPE) int exits, calls it // - false is lesser than true -// - float and int numbers are sorted by their value +// - float and int numbers are sorted by their value, NaN is always lower // - complex numbers are sorted by their real, then by their imaginary parts // - strings are sorted by their value // - map: shorter length is lesser, then sorted by address @@ -59,7 +62,7 @@ func (v *rValues) Len() int { } func (v *rValues) Less(i, j int) bool { - return cmp(v.Visited, v.Slice[i], v.Slice[j]) < 0 + return compare.Compare(v.Visited, v.Slice[i], v.Slice[j]) < 0 } func (v *rValues) Swap(i, j int) { diff --git a/internal/color/color.go b/internal/color/color.go index 50cacc67..1deb32aa 100644 --- a/internal/color/color.go +++ b/internal/color/color.go @@ -9,9 +9,10 @@ package color import ( "fmt" "os" - "reflect" "strings" "sync" + + "github.com/maxatome/go-testdeep/internal/util" ) const ( @@ -212,47 +213,17 @@ func Bad(s string, args ...any) string { } // BadUsage returns a string surrounded by BAD color to notice the -// user he passes a bad parameter to a function. Typically used in a +// user she/he passes a bad parameter to a function. Typically used in a // panic(). func BadUsage(usage string, param any, pos int, kind bool) string { - Init() - - var b strings.Builder - fmt.Fprintf(&b, "%susage: %s, but received ", BadOnBold, usage) - - if param == nil { - b.WriteString("nil") - } else { - t := reflect.TypeOf(param) - if kind && t.String() != t.Kind().String() { - fmt.Fprintf(&b, "%s (%s)", t, t.Kind()) - } else { - b.WriteString(t.String()) - } - } - - b.WriteString(" as ") - switch pos { - case 1: - b.WriteString("1st") - case 2: - b.WriteString("2nd") - case 3: - b.WriteString("3rd") - default: - fmt.Fprintf(&b, "%dth", pos) - } - b.WriteString(" parameter") - b.WriteString(BadOff) - return b.String() + return Bad("usage: %s, %s", usage, util.BadParam(param, pos, kind)) } // TooManyParams returns a string surrounded by BAD color to notice -// the user he called a variadic function with too many +// the user she/he called a variadic function with too many // parameters. Typically used in a panic(). func TooManyParams(usage string) string { - Init() - return BadOnBold + "usage: " + usage + ", too many parameters" + BadOff + return Bad("usage: " + usage + ", too many parameters") } // UnBad returns s with bad color prefix & suffix removed. diff --git a/internal/color/color_test.go b/internal/color/color_test.go index d32147f9..627a874a 100644 --- a/internal/color/color_test.go +++ b/internal/color/color_test.go @@ -139,30 +139,6 @@ func TestBadUsage(t *testing.T) { test.EqualStr(t, color.BadUsage("Zzz(STRING)", nil, 1, true), "usage: Zzz(STRING), but received nil as 1st parameter") - - test.EqualStr(t, - color.BadUsage("Zzz(STRING)", 42, 1, true), - "usage: Zzz(STRING), but received int as 1st parameter") - - test.EqualStr(t, - color.BadUsage("Zzz(STRING)", []int{}, 1, true), - "usage: Zzz(STRING), but received []int (slice) as 1st parameter") - test.EqualStr(t, - color.BadUsage("Zzz(STRING)", []int{}, 1, false), - "usage: Zzz(STRING), but received []int as 1st parameter") - - test.EqualStr(t, - color.BadUsage("Zzz(STRING)", nil, 1, true), - "usage: Zzz(STRING), but received nil as 1st parameter") - test.EqualStr(t, - color.BadUsage("Zzz(STRING)", nil, 2, true), - "usage: Zzz(STRING), but received nil as 2nd parameter") - test.EqualStr(t, - color.BadUsage("Zzz(STRING)", nil, 3, true), - "usage: Zzz(STRING), but received nil as 3rd parameter") - test.EqualStr(t, - color.BadUsage("Zzz(STRING)", nil, 4, true), - "usage: Zzz(STRING), but received nil as 4th parameter") } func TestTooManyParams(t *testing.T) { diff --git a/internal/compare/any.go b/internal/compare/any.go new file mode 100644 index 00000000..ea25a576 --- /dev/null +++ b/internal/compare/any.go @@ -0,0 +1,12 @@ +// Copyright (c) 2024, Maxime Soulé +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. + +//go:build !go1.18 +// +build !go1.18 + +package compare + +type any = interface{} diff --git a/internal/compare/any_test.go b/internal/compare/any_test.go new file mode 100644 index 00000000..d6cb0622 --- /dev/null +++ b/internal/compare/any_test.go @@ -0,0 +1,12 @@ +// Copyright (c) 2024, Maxime Soulé +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. + +//go:build !go1.18 +// +build !go1.18 + +package compare_test + +type any = interface{} diff --git a/helpers/tdutil/sort.go b/internal/compare/compare.go similarity index 68% rename from helpers/tdutil/sort.go rename to internal/compare/compare.go index 35c16943..785da62b 100644 --- a/helpers/tdutil/sort.go +++ b/internal/compare/compare.go @@ -1,18 +1,21 @@ -// Copyright (c) 2019, Maxime Soulé +// Copyright (c) 2019-2024, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. -package tdutil +package compare import ( "math" "reflect" + "sync" "github.com/maxatome/go-testdeep/internal/visited" ) +var intType = reflect.TypeOf(0) + func cmpRet(less, gt bool) int { if less { return -1 @@ -33,8 +36,27 @@ func cmpFloat(a, b float64) int { return cmpRet(a < b, a > b) } -// cmp returns -1 if a < b, 1 if a > b, 0 if a == b. -func cmp(v visited.Visited, a, b reflect.Value) int { +var methodCache sync.Map + +func methodCompare(typ reflect.Type) reflect.Value { + if m, ok := methodCache.Load(typ); ok { + return m.(reflect.Value) + } + m, ok := typ.MethodByName("Compare") + if !ok || + m.Type.IsVariadic() || + m.Type.NumIn() != 2 || + m.Type.In(1) != typ || + m.Type.NumOut() != 1 || + m.Type.Out(0) != intType { + m.Func = reflect.Value{} + } + methodCache.Store(typ, m.Func) + return m.Func +} + +// Compare returns -1 if a < b, 1 if a > b, 0 if a == b. +func Compare(v visited.Visited, a, b reflect.Value) int { if !a.IsValid() { if !b.IsValid() { return 0 @@ -45,7 +67,8 @@ func cmp(v visited.Visited, a, b reflect.Value) int { return 1 } - if at, bt := a.Type(), b.Type(); at != bt { + at, bt := a.Type(), b.Type() + if at != bt { sat, sbt := at.String(), bt.String() return cmpRet(sat < sbt, sat > sbt) } @@ -55,6 +78,21 @@ func cmp(v visited.Visited, a, b reflect.Value) int { return 0 } + if a.Kind() != reflect.Interface { + if cmpFn := methodCompare(at); cmpFn.IsValid() { + ok, cmp := false, 0 + func() { + defer recover() //nolint: errcheck + cmp = int(cmpFn.Call([]reflect.Value{a, b})[0].Int()) + ok = true + }() + if ok { + return cmp + } + // When a panic occurs, fallback on generic comparison + } + } + switch a.Kind() { case reflect.Bool: if a.Bool() { @@ -94,7 +132,7 @@ func cmp(v visited.Visited, a, b reflect.Value) int { case reflect.Array: for i := 0; i < a.Len(); i++ { - if r := cmp(v, a.Index(i), b.Index(i)); r != 0 { + if r := Compare(v, a.Index(i), b.Index(i)); r != 0 { return r } } @@ -107,7 +145,7 @@ func cmp(v visited.Visited, a, b reflect.Value) int { maxl = bl } for i := 0; i < maxl; i++ { - if r := cmp(v, a.Index(i), b.Index(i)); r != 0 { + if r := Compare(v, a.Index(i), b.Index(i)); r != 0 { return r } } @@ -123,11 +161,11 @@ func cmp(v visited.Visited, a, b reflect.Value) int { if b.IsNil() { return 1 } - return cmp(v, a.Elem(), b.Elem()) + return Compare(v, a.Elem(), b.Elem()) case reflect.Struct: for i, m := 0, a.NumField(); i < m; i++ { - if r := cmp(v, a.Field(i), b.Field(i)); r != 0 { + if r := Compare(v, a.Field(i), b.Field(i)); r != 0 { return r } } @@ -143,7 +181,7 @@ func cmp(v visited.Visited, a, b reflect.Value) int { if b.IsNil() { return 1 } - return cmp(v, a.Elem(), b.Elem()) + return Compare(v, a.Elem(), b.Elem()) case reflect.Map: // consider shorter maps are before longer ones diff --git a/internal/compare/compare_test.go b/internal/compare/compare_test.go new file mode 100644 index 00000000..cfb5481d --- /dev/null +++ b/internal/compare/compare_test.go @@ -0,0 +1,227 @@ +// Copyright (c) 2019-2024, Maxime Soulé +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. + +package compare_test + +import ( + "fmt" + "math" + "reflect" + "testing" + + "github.com/maxatome/go-testdeep/internal/compare" + "github.com/maxatome/go-testdeep/internal/visited" +) + +type cmp1 int + +func (cmp1) Compare(cmp1) bool { return false } + +type cmp2 int + +func (cmp2) Compare(int) int { return 0 } + +type cmp3 int + +func (cmp3) Compare(...int) int { return 0 } + +type cmpPanic int + +func (cmpPanic) Compare(cmpPanic) { panic("boom!") } + +type cmpOK int + +func (a cmpOK) Compare(b cmpOK) int { + if a == b { + return 0 + } + if a > b { + return -1 + } + return 1 +} + +type cmpOKp int + +func newCmpOKp(x cmpOKp) *cmpOKp { + return &x +} + +func (a *cmpOKp) Compare(b *cmpOKp) int { + if *a == *b { + return 0 + } + if *a > *b { + return -1 + } + return 1 +} + +func TestCompare(t *testing.T) { + type myStruct struct { + n int + s string + p *myStruct + } + a, b := 12, 42 + ma, mb := map[int]bool{12: true}, map[int]bool{12: true, 13: false} + + checkCompare := func(t *testing.T, a, b any, expected int) { + t.Helper() + got := compare.Compare(visited.NewVisited(), reflect.ValueOf(a), reflect.ValueOf(b)) + if got != expected { + t.Errorf("Compare() failed: got=%d expected=%d\n", got, expected) + } + } + + testCases := []struct { + a, b any + expected int + }{ + // nil + {nil, 12, -1}, + {nil, nil, 0}, + {12, nil, 1}, + // type mismatch: int is before string + {42, "str", -1}, + {"str", 42, 1}, + // Compare + {cmpOK(99), cmpOK(11), -1}, + {cmpOK(11), cmpOK(99), 1}, + {cmpOK(99), cmpOK(99), 0}, + {newCmpOKp(99), newCmpOKp(11), -1}, + {newCmpOKp(11), newCmpOKp(99), 1}, + {newCmpOKp(99), newCmpOKp(99), 0}, + // Compare does not match expected signature + {cmp1(99), cmp1(11), 1}, + {cmp1(11), cmp1(99), -1}, + {cmp1(99), cmp1(99), 0}, + {cmp2(99), cmp2(11), 1}, + {cmp2(11), cmp2(99), -1}, + {cmp2(99), cmp2(99), 0}, + {cmp3(99), cmp3(11), 1}, + {cmp3(11), cmp3(99), -1}, + {cmp3(99), cmp3(99), 0}, + // Compare panics fallback + {cmpPanic(99), cmpPanic(11), 1}, + {cmpPanic(11), cmpPanic(99), -1}, + {cmpPanic(99), cmpPanic(99), 0}, + // bool + {true, true, 0}, + {true, false, 1}, + {false, true, -1}, + {false, false, 0}, + // int + {12, 42, -1}, + {42, 12, 1}, + {12, 12, 0}, + {int8(12), int8(42), -1}, + {int8(42), int8(12), 1}, + {int8(12), int8(12), 0}, + {int16(12), int16(42), -1}, + {int16(42), int16(12), 1}, + {int16(12), int16(12), 0}, + {int32(12), int32(42), -1}, + {int32(42), int32(12), 1}, + {int32(12), int32(12), 0}, + {int64(12), int64(42), -1}, + {int64(42), int64(12), 1}, + {int64(12), int64(12), 0}, + // uint + {uint(12), uint(42), -1}, + {uint(42), uint(12), 1}, + {uint(12), uint(12), 0}, + {uint8(12), uint8(42), -1}, + {uint8(42), uint8(12), 1}, + {uint8(12), uint8(12), 0}, + {uint16(12), uint16(42), -1}, + {uint16(42), uint16(12), 1}, + {uint16(12), uint16(12), 0}, + {uint32(12), uint32(42), -1}, + {uint32(42), uint32(12), 1}, + {uint32(12), uint32(12), 0}, + {uint64(12), uint64(42), -1}, + {uint64(42), uint64(12), 1}, + {uint64(12), uint64(12), 0}, + {uintptr(12), uintptr(42), -1}, + {uintptr(42), uintptr(12), 1}, + {uintptr(12), uintptr(12), 0}, + // float + {float32(12), float32(42), -1}, + {float32(42), float32(12), 1}, + {float32(12), float32(12), 0}, + {float64(12), float64(42), -1}, + {float64(42), float64(12), 1}, + {float64(12), float64(12), 0}, + {float64(12), float64(12), 0}, + {math.NaN(), float64(12), -1}, + {math.NaN(), math.NaN(), -1}, + {float64(12), math.NaN(), 1}, + // complex + {complex(12, 0), complex(42, 0), -1}, + {complex(42, 0), complex(12, 0), 1}, + {complex(0, 12), complex(0, 42), -1}, + {complex(0, 42), complex(0, 12), 1}, + {complex(12, 0), complex(12, 0), 0}, + {complex(float32(12), 0), complex(float32(42), 0), -1}, + {complex(float32(42), 0), complex(float32(12), 0), 1}, + {complex(float32(0), 12), complex(float32(0), 42), -1}, + {complex(float32(0), 42), complex(float32(0), 12), 1}, + {complex(float32(12), 0), complex(float32(12), 0), 0}, + // string + {"aaa", "bbb", -1}, + {"bbb", "aaa", 1}, + {"aaa", "aaa", 0}, + // array + {[3]byte{1, 2, 3}, [3]byte{3, 1, 2}, -1}, + {[3]byte{3, 1, 2}, [3]byte{1, 2, 3}, 1}, + {[3]byte{1, 2, 3}, [3]byte{1, 2, 3}, 0}, + // slice + {[]byte{1, 2, 3}, []byte{3, 1, 2}, -1}, + {[]byte{3, 1, 2}, []byte{1, 2, 3}, 1}, + {[]byte{1, 2, 3}, []byte{1, 2, 3}, 0}, + {[]byte{1, 2, 3}, []byte{1, 2, 3, 4}, -1}, + {[]byte{1, 2, 3, 4}, []byte{1, 2, 3}, 1}, + // interface + {[]any{1}, []any{3}, -1}, + {[]any{3}, []any{1}, 1}, + {[]any{1}, []any{1}, 0}, + {[]any{nil}, []any{nil}, 0}, + {[]any{nil}, []any{1}, -1}, + {[]any{1}, []any{nil}, 1}, + // struct + {myStruct{n: 12, s: "a"}, myStruct{n: 12, s: "b"}, -1}, + {myStruct{n: 12, s: "b"}, myStruct{n: 12, s: "a"}, 1}, + {myStruct{n: 12, s: "a"}, myStruct{n: 12, s: "a"}, 0}, + // ptr + {&a, &a, 0}, + {(*int)(nil), (*int)(nil), 0}, + {&a, &b, -1}, + {(*int)(nil), &b, -1}, + {&b, &a, 1}, + {&b, (*int)(nil), 1}, + // map + {ma, mb, -1}, + {mb, ma, 1}, + {ma, ma, 0}, + {(map[int]bool)(nil), (map[int]bool)(nil), 0}, + {(map[int]bool)(nil), ma, -1}, + {ma, (map[int]bool)(nil), 1}, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("#%d %[1]T(%[1]v) %[1]T(%[1]v)", i, tc.a, tc.b), + func(t *testing.T) { + checkCompare(t, tc.a, tc.b, tc.expected) + }) + } + + // cyclic references protection + pa := &myStruct{n: 42, p: &myStruct{n: 18}} + pa.p.p = pa.p + pb := &myStruct{n: 42, p: &myStruct{n: 18}} + pb.p.p = pb.p + checkCompare(t, pa, pb, 0) +} diff --git a/internal/ctxerr/op_error.go b/internal/ctxerr/op_error.go index ea674d64..1201554d 100644 --- a/internal/ctxerr/op_error.go +++ b/internal/ctxerr/op_error.go @@ -9,54 +9,31 @@ package ctxerr import ( "fmt" "reflect" - "strings" "github.com/maxatome/go-testdeep/internal/types" + "github.com/maxatome/go-testdeep/internal/util" ) -// OpBadUsage returns a string to notice the user he passed a bad +// OpBadUsage returns an [*Error] to notice the user she/he passed a bad // parameter to an operator constructor. +// +// If kind and param's kind name ≠ param's type name: +// +// usage: {op}{usage}, but received {param type} ({param kind}) as {pos}th parameter +// +// else +// +// usage: {op}{usage}, but received {param type} as {pos}th parameter func OpBadUsage(op, usage string, param any, pos int, kind bool) *Error { - var b strings.Builder - fmt.Fprintf(&b, "usage: %s%s, but received ", op, usage) - - if param == nil { - b.WriteString("nil") - } else { - t := reflect.TypeOf(param) - if kind && t.String() != t.Kind().String() { - fmt.Fprintf(&b, "%s (%s)", t, t.Kind()) - } else { - b.WriteString(t.String()) - } - } - - b.WriteString(" as ") - switch pos { - case 1: - b.WriteString("1st") - case 2: - b.WriteString("2nd") - case 3: - b.WriteString("3rd") - default: - fmt.Fprintf(&b, "%dth", pos) - } - b.WriteString(" parameter") - - return &Error{ - Message: "bad usage of " + op + " operator", - Summary: NewSummary(b.String()), - } + return OpBad(op, "usage: %s%s, %s", op, usage, util.BadParam(param, pos, kind)) } -// OpTooManyParams returns an [*Error] to notice the user he called a +// OpTooManyParams returns an [*Error] to notice the user she/he called a // variadic operator constructor with too many parameters. +// +// usage: {op}{usage}, too many parameters func OpTooManyParams(op, usage string) *Error { - return &Error{ - Message: "bad usage of " + op + " operator", - Summary: NewSummary("usage: " + op + usage + ", too many parameters"), - } + return OpBad(op, "usage: %s%s, too many parameters", op, usage) } // OpBad returns an [*Error] to notice the user a bad operator diff --git a/internal/ctxerr/op_error_test.go b/internal/ctxerr/op_error_test.go index 772b80e8..fddcf215 100644 --- a/internal/ctxerr/op_error_test.go +++ b/internal/ctxerr/op_error_test.go @@ -23,30 +23,6 @@ func TestOpBadUsage(t *testing.T) { test.EqualStr(t, ctxerr.OpBadUsage("Zzz", "(STRING)", nil, 1, true).Error(), prefix+"usage: Zzz(STRING), but received nil as 1st parameter") - - test.EqualStr(t, - ctxerr.OpBadUsage("Zzz", "(STRING)", 42, 1, true).Error(), - prefix+"usage: Zzz(STRING), but received int as 1st parameter") - - test.EqualStr(t, - ctxerr.OpBadUsage("Zzz", "(STRING)", []int{}, 1, true).Error(), - prefix+"usage: Zzz(STRING), but received []int (slice) as 1st parameter") - test.EqualStr(t, - ctxerr.OpBadUsage("Zzz", "(STRING)", []int{}, 1, false).Error(), - prefix+"usage: Zzz(STRING), but received []int as 1st parameter") - - test.EqualStr(t, - ctxerr.OpBadUsage("Zzz", "(STRING)", nil, 1, true).Error(), - prefix+"usage: Zzz(STRING), but received nil as 1st parameter") - test.EqualStr(t, - ctxerr.OpBadUsage("Zzz", "(STRING)", nil, 2, true).Error(), - prefix+"usage: Zzz(STRING), but received nil as 2nd parameter") - test.EqualStr(t, - ctxerr.OpBadUsage("Zzz", "(STRING)", nil, 3, true).Error(), - prefix+"usage: Zzz(STRING), but received nil as 3rd parameter") - test.EqualStr(t, - ctxerr.OpBadUsage("Zzz", "(STRING)", nil, 4, true).Error(), - prefix+"usage: Zzz(STRING), but received nil as 4th parameter") } func TestOpTooManyParams(t *testing.T) { diff --git a/internal/util/utils.go b/internal/util/utils.go index d901acdb..a63f3c68 100644 --- a/internal/util/utils.go +++ b/internal/util/utils.go @@ -6,6 +6,51 @@ package util +import ( + "fmt" + "reflect" + "strings" +) + +// BadParam returns a string noticing a misuse of a function parameter. +// +// If kind and param's kind name ≠ param's type name: +// +// but received {param type} ({param kind}) as {pos}th parameter +// +// else +// +// but received {param type} as {pos}th parameter +func BadParam(param any, pos int, kind bool) string { + var b strings.Builder + b.WriteString("but received ") + + if param == nil { + b.WriteString("nil") + } else { + t := reflect.TypeOf(param) + if kind && t.String() != t.Kind().String() { + fmt.Fprintf(&b, "%s (%s)", t, t.Kind()) + } else { + b.WriteString(t.String()) + } + } + + b.WriteString(" as ") + switch pos { + case 1: + b.WriteString("1st") + case 2: + b.WriteString("2nd") + case 3: + b.WriteString("3rd") + default: + fmt.Fprintf(&b, "%dth", pos) + } + b.WriteString(" parameter") + return b.String() +} + // TernRune returns a if cond is true, b otherwise. func TernRune(cond bool, a, b rune) rune { if cond { diff --git a/internal/util/utils_test.go b/internal/util/utils_test.go index 19a4b650..44fba046 100644 --- a/internal/util/utils_test.go +++ b/internal/util/utils_test.go @@ -13,6 +13,36 @@ import ( "github.com/maxatome/go-testdeep/internal/util" ) +func TestBadParam(t *testing.T) { + test.EqualStr(t, + util.BadParam(nil, 1, true), + "but received nil as 1st parameter") + + test.EqualStr(t, + util.BadParam(42, 1, true), + "but received int as 1st parameter") + + test.EqualStr(t, + util.BadParam([]int{}, 1, true), + "but received []int (slice) as 1st parameter") + test.EqualStr(t, + util.BadParam([]int{}, 1, false), + "but received []int as 1st parameter") + + test.EqualStr(t, + util.BadParam(nil, 1, true), + "but received nil as 1st parameter") + test.EqualStr(t, + util.BadParam(nil, 2, true), + "but received nil as 2nd parameter") + test.EqualStr(t, + util.BadParam(nil, 3, true), + "but received nil as 3rd parameter") + test.EqualStr(t, + util.BadParam(nil, 4, true), + "but received nil as 4th parameter") +} + func TestTern(t *testing.T) { test.EqualInt(t, int(util.TernRune(true, 'A', 'B')), int('A')) test.EqualInt(t, int(util.TernRune(false, 'A', 'B')), int('B')) diff --git a/td/check_test.go b/td/check_test.go index d9641dcf..b2e1bd47 100644 --- a/td/check_test.go +++ b/td/check_test.go @@ -71,6 +71,12 @@ type expectedErrorMatch struct { Contain string } +func ptr(x any) any { + v := reflect.New(reflect.TypeOf(x)) + v.Elem().Set(reflect.ValueOf(x)) + return v.Interface() +} + func mustBe(str string) expectedErrorMatch { return expectedErrorMatch{Exact: str} } diff --git a/td/cmp_funcs.go b/td/cmp_funcs.go index a0eae4a2..be4ded17 100644 --- a/td/cmp_funcs.go +++ b/td/cmp_funcs.go @@ -12,7 +12,7 @@ import ( "time" ) -// allOperators lists the 67 operators. +// allOperators lists the 69 operators. // nil means not usable in JSON(). var allOperators = map[string]any{ "All": All, @@ -67,6 +67,8 @@ var allOperators = map[string]any{ "Shallow": nil, "Slice": nil, "Smuggle": nil, + "Sort": Sort, + "Sorted": Sorted, "String": nil, "Struct": nil, "SubBagOf": SubBagOf, @@ -1108,6 +1110,52 @@ func CmpSmuggle(t TestingT, got, fn, expectedValue any, args ...any) bool { return Cmp(t, got, Smuggle(fn, expectedValue), args...) } +// CmpSort is a shortcut for: +// +// td.Cmp(t, got, td.Sort(how, expectedValue), args...) +// +// See [Sort] for details. +// +// Returns true if the test is OK, false if it fails. +// +// If t is a [*T] then its Config field is inherited. +// +// args... are optional and allow to name the test. This name is +// used in case of failure to qualify the test. If len(args) > 1 and +// the first item of args is a string and contains a '%' rune then +// [fmt.Fprintf] is used to compose the name, else args are passed to +// [fmt.Fprint]. Do not forget it is the name of the test, not the +// reason of a potential failure. +func CmpSort(t TestingT, got, how, expectedValue any, args ...any) bool { + t.Helper() + return Cmp(t, got, Sort(how, expectedValue), args...) +} + +// CmpSorted is a shortcut for: +// +// td.Cmp(t, got, td.Sorted(how), args...) +// +// See [Sorted] for details. +// +// [Sorted] optional parameter how is here mandatory. +// nil value should be passed to mimic its absence in +// original [Sorted] call. +// +// Returns true if the test is OK, false if it fails. +// +// If t is a [*T] then its Config field is inherited. +// +// args... are optional and allow to name the test. This name is +// used in case of failure to qualify the test. If len(args) > 1 and +// the first item of args is a string and contains a '%' rune then +// [fmt.Fprintf] is used to compose the name, else args are passed to +// [fmt.Fprint]. Do not forget it is the name of the test, not the +// reason of a potential failure. +func CmpSorted(t TestingT, got, how any, args ...any) bool { + t.Helper() + return Cmp(t, got, Sorted(how), args...) +} + // CmpSStruct is a shortcut for: // // td.Cmp(t, got, td.SStruct(model, expectedFields), args...) diff --git a/td/example_cmp_test.go b/td/example_cmp_test.go index c400dfde..edc7887a 100644 --- a/td/example_cmp_test.go +++ b/td/example_cmp_test.go @@ -3103,6 +3103,42 @@ func ExampleCmpSmuggle_field_path() { // check fields-path including maps/slices: true } +func ExampleCmpSort_basic() { + t := &testing.T{} + + got := []int{-1, 1, 2, -3, 3, -2, 0} + + ok := td.CmpSort(t, got, 1, []int{-3, -2, -1, 0, 1, 2, 3}) + fmt.Println("asc order:", ok) + + ok = td.CmpSort(t, got, -1, []int{3, 2, 1, 0, -1, -2, -3}) + fmt.Println("desc order:", ok) + + // Output: + // asc order: true + // desc order: true +} + +func ExampleCmpSorted_basic() { + t := &testing.T{} + + got := []int{-3, -2, -1, 0, 1, 2, 3} + + ok := td.CmpSorted(t, got, nil) + fmt.Println("is asc order (default):", ok) + + ok = td.CmpSorted(t, got, 1) + fmt.Println("is asc order:", ok) + + ok = td.CmpSorted(t, got, -1) + fmt.Println("is desc order:", ok) + + // Output: + // is asc order (default): true + // is asc order: true + // is desc order: false +} + func ExampleCmpSStruct() { t := &testing.T{} diff --git a/td/example_t_test.go b/td/example_t_test.go index 7e373333..6cdc2123 100644 --- a/td/example_t_test.go +++ b/td/example_t_test.go @@ -3103,6 +3103,42 @@ func ExampleT_Smuggle_field_path() { // check fields-path including maps/slices: true } +func ExampleT_Sort_basic() { + t := td.NewT(&testing.T{}) + + got := []int{-1, 1, 2, -3, 3, -2, 0} + + ok := t.Sort(got, 1, []int{-3, -2, -1, 0, 1, 2, 3}) + fmt.Println("asc order:", ok) + + ok = t.Sort(got, -1, []int{3, 2, 1, 0, -1, -2, -3}) + fmt.Println("desc order:", ok) + + // Output: + // asc order: true + // desc order: true +} + +func ExampleT_Sorted_basic() { + t := td.NewT(&testing.T{}) + + got := []int{-3, -2, -1, 0, 1, 2, 3} + + ok := t.Sorted(got, nil) + fmt.Println("is asc order (default):", ok) + + ok = t.Sorted(got, 1) + fmt.Println("is asc order:", ok) + + ok = t.Sorted(got, -1) + fmt.Println("is desc order:", ok) + + // Output: + // is asc order (default): true + // is asc order: true + // is desc order: false +} + func ExampleT_SStruct() { t := td.NewT(&testing.T{}) diff --git a/td/example_test.go b/td/example_test.go index 552c65c6..c4a2e6c7 100644 --- a/td/example_test.go +++ b/td/example_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2022, Maxime Soulé +// Copyright (c) 2018-2025, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the @@ -3141,6 +3141,42 @@ func ExampleSlice_typedSlice() { // true } +func ExampleSort_basic() { + t := &testing.T{} + + got := []int{-1, 1, 2, -3, 3, -2, 0} + + ok := td.Cmp(t, got, td.Sort(1, []int{-3, -2, -1, 0, 1, 2, 3})) + fmt.Println("asc order:", ok) + + ok = td.Cmp(t, got, td.Sort(-1, []int{3, 2, 1, 0, -1, -2, -3})) + fmt.Println("desc order:", ok) + + // Output: + // asc order: true + // desc order: true +} + +func ExampleSorted_basic() { + t := &testing.T{} + + got := []int{-3, -2, -1, 0, 1, 2, 3} + + ok := td.Cmp(t, got, td.Sorted()) + fmt.Println("is asc order (default):", ok) + + ok = td.Cmp(t, got, td.Sorted(1)) + fmt.Println("is asc order:", ok) + + ok = td.Cmp(t, got, td.Sorted(-1)) + fmt.Println("is desc order:", ok) + + // Output: + // is asc order (default): true + // is asc order: true + // is desc order: false +} + func ExampleSuperSliceOf_array() { t := &testing.T{} diff --git a/td/t.go b/td/t.go index 84a3b975..04239a43 100644 --- a/td/t.go +++ b/td/t.go @@ -940,6 +940,48 @@ func (t *T) Smuggle(got, fn, expectedValue any, args ...any) bool { return t.Cmp(got, Smuggle(fn, expectedValue), args...) } +// Sort is a shortcut for: +// +// t.Cmp(got, td.Sort(how, expectedValue), args...) +// +// See [Sort] for details. +// +// Returns true if the test is OK, false if it fails. +// +// args... are optional and allow to name the test. This name is +// used in case of failure to qualify the test. If len(args) > 1 and +// the first item of args is a string and contains a '%' rune then +// [fmt.Fprintf] is used to compose the name, else args are passed to +// [fmt.Fprint]. Do not forget it is the name of the test, not the +// reason of a potential failure. +func (t *T) Sort(got, how, expectedValue any, args ...any) bool { + t.Helper() + return t.Cmp(got, Sort(how, expectedValue), args...) +} + +// Sorted is a shortcut for: +// +// t.Cmp(got, td.Sorted(how), args...) +// +// See [Sorted] for details. +// +// [Sorted] optional parameter how is here mandatory. +// nil value should be passed to mimic its absence in +// original [Sorted] call. +// +// Returns true if the test is OK, false if it fails. +// +// args... are optional and allow to name the test. This name is +// used in case of failure to qualify the test. If len(args) > 1 and +// the first item of args is a string and contains a '%' rune then +// [fmt.Fprintf] is used to compose the name, else args are passed to +// [fmt.Fprint]. Do not forget it is the name of the test, not the +// reason of a potential failure. +func (t *T) Sorted(got, how any, args ...any) bool { + t.Helper() + return t.Cmp(got, Sorted(how), args...) +} + // SStruct is a shortcut for: // // t.Cmp(got, td.SStruct(model, expectedFields), args...) diff --git a/td/td_grep.go b/td/td_grep.go index 4ce31e82..dd887822 100644 --- a/td/td_grep.go +++ b/td/td_grep.go @@ -214,34 +214,36 @@ func (g *tdGrep) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { switch got.Kind() { case reflect.Slice, reflect.Array: - const grepped = "" - - if got.Kind() == reflect.Slice && got.IsNil() { - return deepValueEqual( - ctx.AddCustomLevel(grepped), - reflect.New(got.Type()).Elem(), - g.expectedValue, - ) - } + default: + return grepBadKind(ctx, got) + } - l := got.Len() - out := reflect.MakeSlice(reflect.SliceOf(got.Type().Elem()), 0, l) + const grepped = "" - for idx := 0; idx < l; idx++ { - item := got.Index(idx) - ok, rErr := g.matchItem(ctx, idx, item) - if rErr != nil { - return rErr - } - if ok { - out = reflect.Append(out, item) - } - } + l := got.Len() + if l == 0 { + return deepValueEqual(ctx.AddCustomLevel(grepped), got, g.expectedValue) + } - return deepValueEqual(ctx.AddCustomLevel(grepped), out, g.expectedValue) + outType := got.Type() + if got.Kind() == reflect.Array { + outType = reflect.SliceOf(outType.Elem()) } - return grepBadKind(ctx, got) + out := reflect.MakeSlice(outType, 0, l) + + for idx := 0; idx < l; idx++ { + item := got.Index(idx) + ok, rErr := g.matchItem(ctx, idx, item) + if rErr != nil { + return rErr + } + if ok { + out = reflect.Append(out, item) + } + } + + return deepValueEqual(ctx.AddCustomLevel(grepped), out, g.expectedValue) } type tdFirst struct { diff --git a/td/td_smuggle.go b/td/td_smuggle.go index 93e459a5..fdc8baa3 100644 --- a/td/td_smuggle.go +++ b/td/td_smuggle.go @@ -95,7 +95,7 @@ func joinFieldsPath(path []smuggleField) string { func splitFieldsPath(origPath string) ([]smuggleField, error) { if origPath == "" { - return nil, fmt.Errorf("FIELD_PATH cannot be empty") + return nil, fmt.Errorf("FIELDS_PATH cannot be empty") } privateField := "" @@ -107,22 +107,22 @@ func splitFieldsPath(origPath string) ([]smuggleField, error) { path = path[1:] end := strings.IndexByte(path, ']') if end < 0 { - return nil, fmt.Errorf("cannot find final ']' in FIELD_PATH %q", origPath) + return nil, fmt.Errorf("cannot find final ']' in FIELDS_PATH %q", origPath) } res = append(res, smuggleField{Name: path[:end], Indexed: true}) path = path[end+1:] case '.': if len(res) == 0 { - return nil, fmt.Errorf("'.' cannot be the first rune in FIELD_PATH %q", origPath) + return nil, fmt.Errorf("'.' cannot be the first rune in FIELDS_PATH %q", origPath) } path = path[1:] if path == "" { - return nil, fmt.Errorf("final '.' in FIELD_PATH %q is not allowed", origPath) + return nil, fmt.Errorf("final '.' in FIELDS_PATH %q is not allowed", origPath) } r, _ = utf8.DecodeRuneInString(path) if r == '.' || r == '[' { - return nil, fmt.Errorf("unexpected %q after '.' in FIELD_PATH %q", r, origPath) + return nil, fmt.Errorf("unexpected %q after '.' in FIELDS_PATH %q", r, origPath) } fallthrough diff --git a/td/td_smuggle_private_test.go b/td/td_smuggle_private_test.go index 2175bb49..21de0b9b 100644 --- a/td/td_smuggle_private_test.go +++ b/td/td_smuggle_private_test.go @@ -65,19 +65,19 @@ func TestFieldsPath(t *testing.T) { } } - checkErr("", "FIELD_PATH cannot be empty") - checkErr(".test", `'.' cannot be the first rune in FIELD_PATH ".test"`) - checkErr("foo.bar.", `final '.' in FIELD_PATH "foo.bar." is not allowed`) - checkErr("foo..bar", `unexpected '.' after '.' in FIELD_PATH "foo..bar"`) - checkErr("foo.[bar]", `unexpected '[' after '.' in FIELD_PATH "foo.[bar]"`) - checkErr("foo[bar", `cannot find final ']' in FIELD_PATH "foo[bar"`) + checkErr("", "FIELDS_PATH cannot be empty") + checkErr(".test", `'.' cannot be the first rune in FIELDS_PATH ".test"`) + checkErr("foo.bar.", `final '.' in FIELDS_PATH "foo.bar." is not allowed`) + checkErr("foo..bar", `unexpected '.' after '.' in FIELDS_PATH "foo..bar"`) + checkErr("foo.[bar]", `unexpected '[' after '.' in FIELDS_PATH "foo.[bar]"`) + checkErr("foo[bar", `cannot find final ']' in FIELDS_PATH "foo[bar"`) checkErr("test.%foo", `unexpected '%' in field name "%foo" in FIELDS_PATH "test.%foo"`) checkErr("test.f%oo", `unexpected '%' in field name "f%oo" in FIELDS_PATH "test.f%oo"`) checkErr("Foo().()", `missing method name before () in FIELDS_PATH "Foo().()"`) checkErr("abc.foo()", `method name "foo()" is not public in FIELDS_PATH "abc.foo()"`) checkErr("Fo%o().abc", `unexpected '%' in method name "Fo%o()" in FIELDS_PATH "Fo%o().abc"`) checkErr("Pipo.bingo.zzz.Foo.Zip().abc", `cannot call method Zip() as it is based on an unexported field "bingo" in FIELDS_PATH "Pipo.bingo.zzz.Foo.Zip().abc"`) - checkErr("foo[bar", `cannot find final ']' in FIELD_PATH "foo[bar"`) + checkErr("foo[bar", `cannot find final ']' in FIELDS_PATH "foo[bar"`) } type SmuggleBuild struct { diff --git a/td/td_smuggle_test.go b/td/td_smuggle_test.go index c8f00ac8..b9427d65 100644 --- a/td/td_smuggle_test.go +++ b/td/td_smuggle_test.go @@ -467,7 +467,7 @@ func TestSmuggle(t *testing.T) { expectedError{ Message: mustBe("bad usage of Smuggle operator"), Path: mustBe("DATA"), - Summary: mustBe(usage + `cannot find final ']' in FIELD_PATH "bad[path"`), + Summary: mustBe(usage + `cannot find final ']' in FIELDS_PATH "bad[path"`), }) // Bad number of args diff --git a/td/td_sort.go b/td/td_sort.go new file mode 100644 index 00000000..505682ad --- /dev/null +++ b/td/td_sort.go @@ -0,0 +1,366 @@ +// Copyright (c) 2024, Maxime Soulé +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. + +package td + +import ( + "errors" + "fmt" + "reflect" + "sort" + "strings" + + "github.com/maxatome/go-testdeep/internal/compare" + "github.com/maxatome/go-testdeep/internal/ctxerr" + "github.com/maxatome/go-testdeep/internal/dark" + "github.com/maxatome/go-testdeep/internal/types" + "github.com/maxatome/go-testdeep/internal/util" + "github.com/maxatome/go-testdeep/internal/visited" +) + +type tdSortBase struct { + how any + mkSortFn func(reflect.Type) (reflect.Value, error) +} + +func (sb *tdSortBase) initSortBase(how ...any) error { + switch l := len(how); l { + case 0: + how = []any{1} + case 1: + default: // list of fields-paths used by Sorted only + fieldsPaths := make([]string, l) + for i, si := range how { + s, ok := si.(string) + if !ok { + return errors.New(util.BadParam(si, i+1, true)) + } + fieldsPaths[i] = s + } + how = []any{fieldsPaths} + } + + switch v := how[0].(type) { + case nil: + sb.mkSortFn = mkSortAsc + case int: + sb.mkSortFn = mkSortAscDesc(v >= 0) + case float64: // to be used in JSON, SubJSONOf & SuperJSONOf + sb.mkSortFn = mkSortAscDesc(v >= 0) + case string: // one fields-path + sb.mkSortFn = func(typ reflect.Type) (reflect.Value, error) { + return mkSortFieldsPaths(typ, []string{v}) + } + case []string: // fields-paths list + sb.mkSortFn = func(typ reflect.Type) (reflect.Value, error) { + return mkSortFieldsPaths(typ, v) + } + default: + vv := reflect.ValueOf(v) + if vv.Kind() != reflect.Func { + return errors.New(util.BadParam(v, 1, true)) + } + ft := vv.Type() + if ft.IsVariadic() || ft.NumIn() != 2 || ft.In(0) != ft.In(1) || + ft.NumOut() != 1 || ft.Out(0) != types.Bool { + return fmt.Errorf("SORT_FUNC must match func(T, T) bool signature, not %T", v) + } + sb.mkSortFn = func(typ reflect.Type) (reflect.Value, error) { + if !typ.AssignableTo(ft.In(0)) { + return reflect.Value{}, fmt.Errorf("%s is not assignable to %s", typ, ft.In(0)) + } + return vv, nil + } + } + return nil +} + +func mkSortAscDesc(asc bool) func(reflect.Type) (reflect.Value, error) { + if asc { + return mkSortAsc + } + return mkSortDesc +} + +func mkSortAsc(typ reflect.Type) (reflect.Value, error) { + v := visited.NewVisited() + return reflect.MakeFunc( + reflect.FuncOf([]reflect.Type{typ, typ}, []reflect.Type{types.Bool}, false), + func(args []reflect.Value) []reflect.Value { + less := compare.Compare(v, args[0], args[1]) < 0 + return []reflect.Value{reflect.ValueOf(less)} + }), nil +} + +func mkSortDesc(typ reflect.Type) (reflect.Value, error) { + v := visited.NewVisited() + return reflect.MakeFunc( + reflect.FuncOf([]reflect.Type{typ, typ}, []reflect.Type{types.Bool}, false), + func(args []reflect.Value) []reflect.Value { + less := compare.Compare(v, args[1], args[0]) < 0 + return []reflect.Value{reflect.ValueOf(less)} + }), nil +} + +func mkSortFieldsPaths(typ reflect.Type, fieldsPaths []string) (reflect.Value, error) { + type sortFP struct { + fn func(any) (smuggleValue, error) + asc bool + } + fns := make([]sortFP, len(fieldsPaths)) + for i, fp := range fieldsPaths { + var sfp sortFP + if strings.HasPrefix(fp, "-") { + fp = fp[1:] + } else { + sfp.asc = true + fp = strings.TrimPrefix(fp, "+") // optional + } + fn, err := getFieldsPathFn(fp) + if err != nil { + return reflect.Value{}, err + } + sfp.fn = fn.Interface().(func(any) (smuggleValue, error)) + fns[i] = sfp + } + + v := visited.NewVisited() + return reflect.MakeFunc( + reflect.FuncOf([]reflect.Type{typ, typ}, []reflect.Type{types.Bool}, false), + func(args []reflect.Value) []reflect.Value { + a, aOK := dark.GetInterface(args[0], true) + b, bOK := dark.GetInterface(args[1], true) + if aOK && bOK { + for _, fn := range fns { + va, aErr := fn.fn(a) + vb, bErr := fn.fn(b) + if aErr != nil || bErr != nil { + if aErr == nil || bErr == nil { + // nonexistent field is greater + return []reflect.Value{reflect.ValueOf(aErr == nil)} + } + break // both nonexistent fields, use Compare + } + cmp := compare.Compare(v, va.Value, vb.Value) + if cmp == 0 { + continue + } + return []reflect.Value{reflect.ValueOf(cmp < 0 == fn.asc)} + } + } + less := compare.Compare(v, args[0], args[1]) < 0 + return []reflect.Value{reflect.ValueOf(less)} + }), nil +} + +const sortUsage = "(SORT_FUNC|int|string|[]string, TESTDEEP_OPERATOR|EXPECTED_VALUE)" + +type tdSort struct { + tdSmugglerBase + tdSortBase +} + +var _ TestDeep = &tdSort{} + +// summary(Sort): sorts a slice or an array before comparing its content +// input(Sort): array,slice,ptr(ptr on array/slice) + +// Sort is a smuggler operator. It takes an array, a slice or a +// pointer on array/slice, it sorts it using how and compares the +// sorted result to expectedValue. +// +// how can be: +// - nil or a float64/int >= 0 for a generic ascendent order; +// - a float64/int < 0 for a generic descendent order; +// - a string specifying a fields-path (optionally prefixed by "+" +// or "-" for respectively an ascendent or a descendent order, +// defaulting to ascendent one); +// - a []string containing a list of fields-paths (as above), second +// and next fields-paths are checked when the previous ones are equal; +// - a function matching func(a, b T) bool signature and returning +// true if a is lesser than b. +// +// A fields-path, also used by [Smuggle] and [Sorted] operators, +// allows to access nested structs fields and maps & slices items. +// +// how can be a float64 to allow Sort to be used in expected JSON of +// [JSON], [SubJSONOf] & [SuperJSONOf] operators. +// +// TO BE COMPLETED XXXXXXXXXXXXXXXXX. +// +// See also [Sorted], [Smuggle] and [Bag]. +func Sort(how any, expectedValue any) TestDeep { + s := tdSort{} + s.tdSmugglerBase = newSmugglerBase(expectedValue, 0) + if !s.isTestDeeper { + s.expectedValue = reflect.ValueOf(expectedValue) + } + + err := s.initSortBase(how) + if err != nil { + s.err = ctxerr.OpBad("Sort", "usage: Sort%s, %s", sortUsage, err) + } else if !s.isTestDeeper { + switch s.expectedValue.Kind() { + case reflect.Slice, reflect.Array: + default: + s.err = ctxerr.OpBad("Sort", + "usage: Sort%s, EXPECTED_VALUE must be a slice or an array not a %s", + sortUsage, types.KindType(s.expectedValue)) + } + } + return &s +} + +func (s *tdSort) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { + if s.err != nil { + return ctx.CollectError(s.err) + } + + if rErr := grepResolvePtr(ctx, &got); rErr != nil { + return rErr + } + + switch got.Kind() { + case reflect.Slice, reflect.Array: + default: + return grepBadKind(ctx, got) + } + + const sorted = "" + + itemType := got.Type().Elem() + fn, err := s.mkSortFn(itemType) + if err != nil { + if ctx.BooleanError { + return ctxerr.BooleanError + } + return ctx.CollectError(&ctxerr.Error{ + Message: "incompatible parameter type", + Got: types.RawString(itemType.String()), + Expected: types.RawString(fn.Type().In(0).String()), + }) + } + + l := got.Len() + if l <= 1 { + return deepValueEqual(ctx.AddCustomLevel(sorted), got, s.expectedValue) + } + + var out reflect.Value + if got.Kind() == reflect.Slice { + out = reflect.MakeSlice(reflect.SliceOf(itemType), l, l) + } else { + out = reflect.New(got.Type()).Elem() + } + reflect.Copy(out, got) + + sort.SliceStable(out.Slice(0, out.Len()).Interface(), func(i, j int) bool { + return fn.Call([]reflect.Value{out.Index(i), out.Index(j)})[0].Bool() + }) + return deepValueEqual(ctx.AddCustomLevel(sorted), out, s.expectedValue) +} + +func (s *tdSort) String() string { + if s.err != nil { + return s.stringError() + } + how, typ := s.how, reflect.TypeOf(s.how) + if typ.Kind() == reflect.Func { + how = typ.String() + } + return fmt.Sprintf("Sort(%v)", how) +} + +type tdSorted struct { + baseOKNil + tdSortBase +} + +var _ TestDeep = &tdSorted{} + +const sortedUsage = "(SORT_FUNC|int|[]string|string...)" + +// summary(Sorted): checks a slice or an array is sorted +// input(Sorted): array,slice,ptr(ptr on array/slice) + +// Sorted operator checks that data is an array, a slice or a pointer +// on array/slice, and it is sorted as how tells it should be. +// +// A fields-path, also used by [Smuggle] and [Sort] operators, +// allows to access nested structs fields and maps & slices items. +// +// TO BE COMPLETED XXXXXXXXXXXXXXXXX. +// +// how can be a float64 to allow Sort to be used in expected JSON of +// [JSON], [SubJSONOf] & [SuperJSONOf] operators. +// +// See also [Sort]. +func Sorted(how ...any) TestDeep { + s := tdSorted{ + baseOKNil: newBaseOKNil(3), + } + + err := s.initSortBase(how...) + if err != nil { + s.err = ctxerr.OpBad("Sorted", "usage: Sorted%s, %s", sortedUsage, err) + } + return &s +} + +func (s *tdSorted) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { + if s.err != nil { + return ctx.CollectError(s.err) + } + + if rErr := grepResolvePtr(ctx, &got); rErr != nil { + return rErr + } + + switch got.Kind() { + case reflect.Slice, reflect.Array: + default: + return grepBadKind(ctx, got) + } + + itemType := got.Type().Elem() + fn, err := s.mkSortFn(itemType) + if err != nil { + if ctx.BooleanError { + return ctxerr.BooleanError + } + return ctx.CollectError(&ctxerr.Error{ + Message: "incompatible parameter type", + Got: types.RawString(itemType.String()), + Expected: types.RawString(fn.Type().In(0).String()), + }) + } + + for i, l := 1, got.Len(); i < l; i++ { + if fn.Call([]reflect.Value{got.Index(i), got.Index(i - 1)})[0].Bool() { + return ctx.CollectError(&ctxerr.Error{ + Message: "not sorted", + Summary: ctxerr.NewSummary(fmt.Sprintf( + "item #%d value is lesser than #%d one while it should not", i, i-1)), + }) + } + } + + return nil +} + +func (s *tdSorted) String() string { + if s.err != nil { + return s.stringError() + } + if s.how == nil { + return "Sorted()" + } + how, typ := s.how, reflect.TypeOf(s.how) + if typ.Kind() == reflect.Func { + how = typ.String() + } + return fmt.Sprintf("Sorted(%v)", how) +} diff --git a/td/td_sort_private_test.go b/td/td_sort_private_test.go new file mode 100644 index 00000000..5a5b459c --- /dev/null +++ b/td/td_sort_private_test.go @@ -0,0 +1,272 @@ +// Copyright (c) 2024, Maxime Soulé +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. + +package td + +import ( + "fmt" + "reflect" + "sort" + "strings" + "testing" + + "github.com/maxatome/go-testdeep/internal/test" + "github.com/maxatome/go-testdeep/internal/types" +) + +type sortA struct{ a, b, c int } + +type sortB struct { + name string + idx int +} + +func (b sortB) Compare(x sortB) int { + bn, xn := strings.ToLower(b.name), strings.ToLower(x.name) + if bn == xn { + return b.idx - x.idx + } + if bn < xn { + return -1 + } + return 1 +} + +func TestInitSortBase(t *testing.T) { + newSortA := func() []sortA { + return []sortA{ + {2, 3, 2}, + {1, 2, 3}, + {3, 1, 2}, + {2, 4, 2}, + {1, 2, 4}, + {2, 3, 1}, + } + } + + testCases := []struct { + name string + how any + slice any + got2str func(any) string + expected string + }{ + { + name: "mkSortAsc", + how: 0, + slice: []int{3, 5, 2, 1, 4}, + expected: "[1 2 3 4 5]", + }, + { + name: "mkSortDesc", + how: -1, + slice: []int{3, 5, 2, 1, 4}, + expected: "[5 4 3 2 1]", + }, + { + name: "mkSortAsc-float64", + how: float64(1), + slice: []int{3, 5, 2, 1, 4}, + expected: "[1 2 3 4 5]", + }, + { + name: "mkSortDesc-float64", + how: float64(-1), + slice: []int{3, 5, 2, 1, 4}, + expected: "[5 4 3 2 1]", + }, + { + name: "mkSortAsc-Compare", + how: 1, + slice: []sortB{{"Zb", 1}, {"za", 8}, {"a", 4}, {"A", 5}}, + expected: "[{a 4} {A 5} {za 8} {Zb 1}]", + }, + { + name: "mkSortFieldsPaths-asc1-1field", + how: "b", + slice: newSortA(), + expected: "[{3 1 2} {1 2 3} {1 2 4} {2 3 1} {2 3 2} {2 4 2}]", + }, + { + name: "mkSortFieldsPaths-asc1-1field-plus", + how: "+b", + slice: newSortA(), + expected: "[{3 1 2} {1 2 3} {1 2 4} {2 3 1} {2 3 2} {2 4 2}]", + }, + { + name: "mkSortFieldsPaths-desc1", + how: []string{"-b"}, + slice: newSortA(), + expected: "[{2 4 2} {2 3 1} {2 3 2} {1 2 3} {1 2 4} {3 1 2}]", + }, + { + name: "mkSortFieldsPaths-multi", + how: []string{"-a", "b", "-c"}, + slice: newSortA(), + expected: "[{3 1 2} {2 3 2} {2 3 1} {2 4 2} {1 2 4} {1 2 3}]", + }, + { + name: "mkSortFieldsPaths-deref", + how: []string{"-b"}, + slice: func() []any { + sl := newSortA() + var res []any + for _, v := range sl { + a := v + b := &a + res = append(res, &b) + } + var ps *sortA + n := 18 + var pn *int + return append(res, &ps, &pn, (**sortA)(nil), n, nil, &n) + }(), + got2str: func(got any) string { + sl := got.([]any) + sl2 := make([]any, len(sl)) + for i, e := range sl { + switch se := e.(type) { + case nil: + sl2[i] = "nil" + case int: + sl2[i] = e + case *int: + if se == nil { + sl2[i] = "(*int)(nil)" + } else { + sl2[i] = fmt.Sprintf("&%d", *se) + } + case **int: + switch { + case se == nil: + sl2[i] = "(**int)(nil)" + case *se == nil: + sl2[i] = "(**int)(&nil)" + default: + sl2[i] = fmt.Sprintf("&&%d", **se) + } + case **sortA: + switch { + case se == nil: + sl2[i] = "(**sortA:)(nil)" + case *se == nil: + sl2[i] = "(**sortA:)(&nil)" + default: + sl2[i] = **se + } + default: + sl2[i] = fmt.Sprintf("%[1]T(%[1]v)", se) + } + } + return fmt.Sprintf("%v", sl2) + }, + expected: "[{2 4 2} {2 3 1} {2 3 2} {1 2 3} {1 2 4} {3 1 2} nil (**int)(&nil) (**sortA:)(nil) (**sortA:)(&nil) &18 18]", + }, + { + name: "mkSortFieldsPaths-unknown", + how: []string{"a", "unknown"}, + slice: newSortA(), + expected: "[{1 2 3} {1 2 4} {2 3 1} {2 3 2} {2 4 2} {3 1 2}]", + }, + { + name: "custom func", + how: func(a, b int) bool { return a > b }, + slice: []int{3, 5, 2, 1, 4}, + expected: "[5 4 3 2 1]", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var sb tdSortBase + err := sb.initSortBase(tc.how) + test.NoError(t, err) + fn, err := sb.mkSortFn(reflect.TypeOf(tc.slice).Elem()) + test.NoError(t, err) + sl := reflect.ValueOf(tc.slice) + sort.SliceStable(tc.slice, func(i, j int) bool { + return fn.Call([]reflect.Value{sl.Index(i), sl.Index(j)})[0].Bool() + }) + var got string + if tc.got2str != nil { + got = tc.got2str(tc.slice) + } else { + got = fmt.Sprintf("%v", tc.slice) + } + test.EqualStr(t, got, tc.expected) + }) + } + + t.Run("Error", func(t *testing.T) { + testCases := []struct { + name string + how any + expectedErr string + }{ + { + name: "unknown how", + how: true, + expectedErr: "but received bool as 1st parameter", + }, + { + name: "bad func variadic", + how: func(...int) bool { return true }, + expectedErr: "SORT_FUNC must match func(T, T) bool signature, not func(...int) bool", + }, + { + name: "bad func num in1", + how: func(a int) bool { return true }, + expectedErr: "SORT_FUNC must match func(T, T) bool signature, not func(int) bool", + }, + { + name: "bad func num in3", + how: func(a, b, c int) bool { return true }, + expectedErr: "SORT_FUNC must match func(T, T) bool signature, not func(int, int, int) bool", + }, + { + name: "bad func in types", + how: func(a int, b bool) bool { return true }, + expectedErr: "SORT_FUNC must match func(T, T) bool signature, not func(int, bool) bool", + }, + { + name: "bad func num out0", + how: func(a, b int) {}, + expectedErr: "SORT_FUNC must match func(T, T) bool signature, not func(int, int)", + }, + { + name: "bad func num out2", + how: func(a, b int) (bool, bool) { return true, true }, + expectedErr: "SORT_FUNC must match func(T, T) bool signature, not func(int, int) (bool, bool)", + }, + { + name: "bad func out type", + how: func(a, b int) int { return 0 }, + expectedErr: "SORT_FUNC must match func(T, T) bool signature, not func(int, int) int", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var sb tdSortBase + err := sb.initSortBase(tc.how) + if sb.mkSortFn != nil { + t.Error("sortFunc should return nil function") + } + if test.Error(t, err) { + test.EqualStr(t, err.Error(), tc.expectedErr) + } + }) + } + + t.Run("custom func", func(t *testing.T) { + var sb tdSortBase + err := sb.initSortBase(func(a, b int) bool { return a > b }) + test.NoError(t, err) + _, err = sb.mkSortFn(types.Bool) + if test.Error(t, err) { + test.EqualStr(t, err.Error(), "bool is not assignable to int") + } + }) + }) +} diff --git a/td/td_sort_test.go b/td/td_sort_test.go new file mode 100644 index 00000000..722108ba --- /dev/null +++ b/td/td_sort_test.go @@ -0,0 +1,235 @@ +// Copyright (c) 2024, Maxime Soulé +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. + +package td_test + +import ( + "encoding/json" + "fmt" + "reflect" + "testing" + + "github.com/maxatome/go-testdeep/td" +) + +func TestSort(t *testing.T) { + type sortTest1 struct { + s string + a int + b int + } + type sortTest2 struct{ a, b, c int } + testCases := []struct { + name string + how any + got any + expected any + }{ + { + name: "slice", + how: 1, + got: []int{1, -2, -3, 0, -1, 3, 2}, + expected: []int{-3, -2, -1, 0, 1, 2, 3}, + }, + { + name: "*slice", + how: 1, + got: &[]int{1, -2, -3, 0, -1, 3, 2}, + expected: []int{-3, -2, -1, 0, 1, 2, 3}, + }, + { + name: "array", + how: 1, + got: [...]int{1, -2, -3, 0, -1, 3, 2}, + expected: [...]int{-3, -2, -1, 0, 1, 2, 3}, + }, + { + name: "*array", + how: 1, + got: &[...]int{1, -2, -3, 0, -1, 3, 2}, + expected: [...]int{-3, -2, -1, 0, 1, 2, 3}, + }, + { + name: "asc0", + how: 0, + got: []int{1, -2, -3, 0, -1, 3, 2}, + expected: []int{-3, -2, -1, 0, 1, 2, 3}, + }, + { + name: "desc", + how: -1, + got: []int{1, -2, -3, 0, -1, 3, 2}, + expected: []int{3, 2, 1, 0, -1, -2, -3}, + }, + { + name: "func", + how: func(a, b int) bool { + if a == 0 || b == 0 { + return b == 0 + } + return a > b + }, + got: []int{1, -2, -3, 0, -1, 3, 2}, + expected: []int{3, 2, 1, -1, -2, -3, 0}, + }, + { + name: "fields-path", + how: "s", + got: []sortTest1{{"c", 4, 2}, {"a", 8, 1}, {"b", 0, 3}}, + expected: []sortTest1{{"a", 8, 1}, {"b", 0, 3}, {"c", 4, 2}}, + }, + { + name: "multiple fields-paths", + how: []string{"a", "-b", "c"}, + got: []sortTest2{{1, 9, 5}, {2, 0, 0}, {1, 9, 4}, {1, 8, 0}}, + expected: []sortTest2{{1, 9, 4}, {1, 9, 5}, {1, 8, 0}, {2, 0, 0}}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + checkOK(t, tc.got, td.Sort(tc.how, tc.expected)) + }) + } + + t.Run("JSON", func(t *testing.T) { + checkOK(t, + json.RawMessage(`["c","a","b"]`), + td.JSON(`Sort(1, ["a","b","c"])`)) + + checkOK(t, + json.RawMessage(`{"x": ["c","a","b"]}`), + td.JSON(`{"x": Sort(-1, ["c","b","a"])}`)) + }) +} + +func TestSortTypeBehind(t *testing.T) { + equalTypes(t, td.Sort(1, []int{}), nil) + + // Erroneous op + equalTypes(t, td.Sort(func() {}, []int{}), nil) +} + +// nolint: unused +func TestSorted(t *testing.T) { + lastBecomesFirst := func(x any) (any, int) { + vx := reflect.ValueOf(x) + if vx.Kind() == reflect.Ptr { + vx = vx.Elem() + } + l := vx.Len() + if l <= 1 { + return nil, 0 + } + if vx.Kind() == reflect.Array { + vx2 := reflect.New(vx.Type()).Elem() + reflect.Copy(vx2, vx) + vx = vx2 + } + first := vx.Index(0).Interface() + reflect.Copy(vx, vx.Slice(1, l)) + vx.Index(l - 1).Set(reflect.ValueOf(first)) + return vx.Interface(), l + } + + type nested3 struct { + val int + dummy int + } + type nested2 struct { + val int + n3 *nested3 + } + type nested1 struct { + val int + n2 nested2 + } + testCases := []struct { + name string + got any + sorted []any + }{ + { + name: "slice", + got: []int{0, 1, 2, 2}, + }, + { + name: "*slice", + got: ptr([]int{0, 1, 2, 2}), + }, + { + name: "array", + got: [...]int{0, 1, 2, 2}, + }, + { + name: "*array", + got: ptr([...]int{0, 1, 2, 2}), + }, + { + name: "asc", + got: []int{0, 1, 2, 2}, + sorted: []any{1}, + }, + { + name: "asc", + got: []int{4, 3, 2, 2}, + sorted: []any{-1}, + }, + { + name: "flatten struct field", + got: []struct{ name string }{{"a"}, {"b"}, {"c"}, {"c"}}, + sorted: []any{"name"}, + }, + { + name: "struct field in slice", + got: []struct{ name string }{{"a"}, {"b"}, {"c"}, {"c"}}, + sorted: []any{[]string{"name"}}, + }, + { + name: "flatten struct field desc", + got: []struct{ name string }{{"d"}, {"c"}, {"b"}, {"b"}}, + sorted: []any{"-name"}, + }, + { + name: "flatten multiple struct fields", + got: []struct{ a, b, c int }{{1, 9, 4}, {1, 9, 5}, {1, 8, 0}, {2, 0, 0}}, + sorted: []any{"a", "-b", "c"}, + }, + { + name: "nested fields-path", + got: []*nested1{ + {1, nested2{1, &nested3{1, 8}}}, + {1, nested2{1, &nested3{2, 7}}}, + {1, nested2{2, &nested3{2, 6}}}, + {2, nested2{2, &nested3{2, 5}}}, + }, + sorted: []any{"n2.n3.val", "n2.val", "val"}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + checkOK(t, tc.got, td.Sorted(tc.sorted...)) + + if got, l := lastBecomesFirst(tc.got); got != nil { + checkError(t, got, td.Sorted(tc.sorted...), + expectedError{ + Message: mustBe("not sorted"), + Path: mustBe("DATA"), + Summary: mustBe(fmt.Sprintf( + "item #%d value is lesser than #%d one while it should not", + l-1, l-2)), + }) + } + }) + } +} + +func TestSortedTypeBehind(t *testing.T) { + equalTypes(t, td.Sorted(), nil) + equalTypes(t, td.Sorted(-1), nil) + + // Erroneous op + equalTypes(t, td.Sorted(func() {}), nil) +} diff --git a/tools/gen_funcs.pl b/tools/gen_funcs.pl index 8f244217..6db7d672 100755 --- a/tools/gen_funcs.pl +++ b/tools/gen_funcs.pl @@ -1,6 +1,6 @@ #!/usr/bin/env perl -# Copyright (c) 2018-2022, Maxime Soulé +# Copyright (c) 2018-2025, Maxime Soulé # All rights reserved. # # This source code is licensed under the BSD-style license found in the @@ -103,6 +103,7 @@ Re => 'nil', Recv => 0, TruncTime => 0, + Sorted => 'nil', # These operators accept several StructFields, # but we want only one here Struct => 'nil', @@ -245,7 +246,7 @@ } } - $funcs{$func}{args} = \@args unless $ONLY_OPERATORS{$func}; + $funcs{$func}{args} = \@args unless $ONLY_OPERATORS{$func}; # "//" is OK, otherwise TAB is not allowed die "TAB detected in $func operator documentation\n" if $doc =~ m,(?