From 95ed457dab316cb8860806c60fa3ee032e2b109d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20Soul=C3=A9?= Date: Mon, 22 Aug 2022 17:31:31 +0200 Subject: [PATCH] feat: add Grep, First & Last operators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime Soulé --- README.md | 9 + td/cmp_funcs.go | 68 +++- td/example_cmp_test.go | 246 +++++++++++++ td/example_t_test.go | 246 +++++++++++++ td/example_test.go | 368 ++++++++++++++++++- td/t.go | 57 +++ td/td_grep.go | 395 ++++++++++++++++++++ td/td_grep_test.go | 791 +++++++++++++++++++++++++++++++++++++++++ td/td_json.go | 53 +-- 9 files changed, 2206 insertions(+), 27 deletions(-) create mode 100644 td/td_grep.go create mode 100644 td/td_grep_test.go diff --git a/README.md b/README.md index fd34148f..6206697d 100644 --- a/README.md +++ b/README.md @@ -305,6 +305,8 @@ See [FAQ](https://go-testdeep.zetta.rocks/faq/). [`ContainsKey`]: https://go-testdeep.zetta.rocks/operators/containskey/ [`Delay`]: https://go-testdeep.zetta.rocks/operators/delay/ [`Empty`]: https://go-testdeep.zetta.rocks/operators/empty/ +[`First`]: https://go-testdeep.zetta.rocks/operators/first/ +[`Grep`]: https://go-testdeep.zetta.rocks/operators/grep/ [`Gt`]: https://go-testdeep.zetta.rocks/operators/gt/ [`Gte`]: https://go-testdeep.zetta.rocks/operators/gte/ [`HasPrefix`]: https://go-testdeep.zetta.rocks/operators/hasprefix/ @@ -314,6 +316,7 @@ See [FAQ](https://go-testdeep.zetta.rocks/faq/). [`JSON`]: https://go-testdeep.zetta.rocks/operators/json/ [`JSONPointer`]: https://go-testdeep.zetta.rocks/operators/jsonpointer/ [`Keys`]: https://go-testdeep.zetta.rocks/operators/keys/ +[`Last`]: https://go-testdeep.zetta.rocks/operators/last/ [`Lax`]: https://go-testdeep.zetta.rocks/operators/lax/ [`Len`]: https://go-testdeep.zetta.rocks/operators/len/ [`Lt`]: https://go-testdeep.zetta.rocks/operators/lt/ @@ -367,6 +370,8 @@ See [FAQ](https://go-testdeep.zetta.rocks/faq/). [`CmpContains`]: https://go-testdeep.zetta.rocks/operators/contains/#cmpcontains-shortcut [`CmpContainsKey`]: https://go-testdeep.zetta.rocks/operators/containskey/#cmpcontainskey-shortcut [`CmpEmpty`]: https://go-testdeep.zetta.rocks/operators/empty/#cmpempty-shortcut +[`CmpFirst`]: https://go-testdeep.zetta.rocks/operators/first/#cmpfirst-shortcut +[`CmpGrep`]: https://go-testdeep.zetta.rocks/operators/grep/#cmpgrep-shortcut [`CmpGt`]: https://go-testdeep.zetta.rocks/operators/gt/#cmpgt-shortcut [`CmpGte`]: https://go-testdeep.zetta.rocks/operators/gte/#cmpgte-shortcut [`CmpHasPrefix`]: https://go-testdeep.zetta.rocks/operators/hasprefix/#cmphasprefix-shortcut @@ -375,6 +380,7 @@ See [FAQ](https://go-testdeep.zetta.rocks/faq/). [`CmpJSON`]: https://go-testdeep.zetta.rocks/operators/json/#cmpjson-shortcut [`CmpJSONPointer`]: https://go-testdeep.zetta.rocks/operators/jsonpointer/#cmpjsonpointer-shortcut [`CmpKeys`]: https://go-testdeep.zetta.rocks/operators/keys/#cmpkeys-shortcut +[`CmpLast`]: https://go-testdeep.zetta.rocks/operators/last/#cmplast-shortcut [`CmpLax`]: https://go-testdeep.zetta.rocks/operators/lax/#cmplax-shortcut [`CmpLen`]: https://go-testdeep.zetta.rocks/operators/len/#cmplen-shortcut [`CmpLt`]: https://go-testdeep.zetta.rocks/operators/lt/#cmplt-shortcut @@ -427,6 +433,8 @@ See [FAQ](https://go-testdeep.zetta.rocks/faq/). [`T.Contains`]: https://go-testdeep.zetta.rocks/operators/contains/#tcontains-shortcut [`T.ContainsKey`]: https://go-testdeep.zetta.rocks/operators/containskey/#tcontainskey-shortcut [`T.Empty`]: https://go-testdeep.zetta.rocks/operators/empty/#tempty-shortcut +[`T.First`]: https://go-testdeep.zetta.rocks/operators/first/#tfirst-shortcut +[`T.Grep`]: https://go-testdeep.zetta.rocks/operators/grep/#tgrep-shortcut [`T.Gt`]: https://go-testdeep.zetta.rocks/operators/gt/#tgt-shortcut [`T.Gte`]: https://go-testdeep.zetta.rocks/operators/gte/#tgte-shortcut [`T.HasPrefix`]: https://go-testdeep.zetta.rocks/operators/hasprefix/#thasprefix-shortcut @@ -435,6 +443,7 @@ See [FAQ](https://go-testdeep.zetta.rocks/faq/). [`T.JSON`]: https://go-testdeep.zetta.rocks/operators/json/#tjson-shortcut [`T.JSONPointer`]: https://go-testdeep.zetta.rocks/operators/jsonpointer/#tjsonpointer-shortcut [`T.Keys`]: https://go-testdeep.zetta.rocks/operators/keys/#tkeys-shortcut +[`T.Last`]: https://go-testdeep.zetta.rocks/operators/last/#tlast-shortcut [`T.CmpLax`]: https://go-testdeep.zetta.rocks/operators/lax/#tcmplax-shortcut [`T.Len`]: https://go-testdeep.zetta.rocks/operators/len/#tlen-shortcut [`T.Lt`]: https://go-testdeep.zetta.rocks/operators/lt/#tlt-shortcut diff --git a/td/cmp_funcs.go b/td/cmp_funcs.go index 25c7bfed..facc8119 100644 --- a/td/cmp_funcs.go +++ b/td/cmp_funcs.go @@ -12,7 +12,7 @@ import ( "time" ) -// allOperators lists the 63 operators. +// allOperators lists the 66 operators. // nil means not usable in JSON(). var allOperators = map[string]any{ "All": All, @@ -28,6 +28,8 @@ var allOperators = map[string]any{ "ContainsKey": ContainsKey, "Delay": nil, "Empty": Empty, + "First": First, + "Grep": Grep, "Gt": Gt, "Gte": Gte, "HasPrefix": HasPrefix, @@ -37,6 +39,7 @@ var allOperators = map[string]any{ "JSON": nil, "JSONPointer": JSONPointer, "Keys": Keys, + "Last": Last, "Lax": nil, "Len": Len, "Lt": Lt, @@ -315,6 +318,48 @@ func CmpEmpty(t TestingT, got any, args ...any) bool { return Cmp(t, got, Empty(), args...) } +// CmpFirst is a shortcut for: +// +// td.Cmp(t, got, td.First(filter, expectedValue), args...) +// +// See [First] 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 CmpFirst(t TestingT, got, filter, expectedValue any, args ...any) bool { + t.Helper() + return Cmp(t, got, First(filter, expectedValue), args...) +} + +// CmpGrep is a shortcut for: +// +// td.Cmp(t, got, td.Grep(filter, expectedValue), args...) +// +// See [Grep] 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 CmpGrep(t TestingT, got, filter, expectedValue any, args ...any) bool { + t.Helper() + return Cmp(t, got, Grep(filter, expectedValue), args...) +} + // CmpGt is a shortcut for: // // td.Cmp(t, got, td.Gt(minExpectedValue), args...) @@ -483,6 +528,27 @@ func CmpKeys(t TestingT, got, val any, args ...any) bool { return Cmp(t, got, Keys(val), args...) } +// CmpLast is a shortcut for: +// +// td.Cmp(t, got, td.Last(filter, expectedValue), args...) +// +// See [Last] 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 CmpLast(t TestingT, got, filter, expectedValue any, args ...any) bool { + t.Helper() + return Cmp(t, got, Last(filter, expectedValue), args...) +} + // CmpLax is a shortcut for: // // td.Cmp(t, got, td.Lax(expectedValue), args...) diff --git a/td/example_cmp_test.go b/td/example_cmp_test.go index 3a8007c5..0fe33029 100644 --- a/td/example_cmp_test.go +++ b/td/example_cmp_test.go @@ -727,6 +727,170 @@ func ExampleCmpEmpty_pointers() { // false } +func ExampleCmpFirst_classic() { + t := &testing.T{} + + got := []int{-3, -2, -1, 0, 1, 2, 3} + + ok := td.CmpFirst(t, got, td.Gt(0), 1) + fmt.Println("first positive number is 1:", ok) + + isEven := func(x int) bool { return x%2 == 0 } + + ok = td.CmpFirst(t, got, isEven, -2) + fmt.Println("first even number is -2:", ok) + + ok = td.CmpFirst(t, got, isEven, td.Lt(0)) + fmt.Println("first even number is < 0:", ok) + + ok = td.CmpFirst(t, got, isEven, td.Code(isEven)) + fmt.Println("first even number is well even:", ok) + + // Output: + // first positive number is 1: true + // first even number is -2: true + // first even number is < 0: true + // first even number is well even: true +} + +func ExampleCmpFirst_empty() { + t := &testing.T{} + + ok := td.CmpFirst(t, ([]int)(nil), td.Gt(0), td.Gt(0)) + fmt.Println("first in nil slice:", ok) + + ok = td.CmpFirst(t, []int{}, td.Gt(0), td.Gt(0)) + fmt.Println("first in empty slice:", ok) + + ok = td.CmpFirst(t, &[]int{}, td.Gt(0), td.Gt(0)) + fmt.Println("first in empty pointed slice:", ok) + + ok = td.CmpFirst(t, [0]int{}, td.Gt(0), td.Gt(0)) + fmt.Println("first in empty array:", ok) + + // Output: + // first in nil slice: false + // first in empty slice: false + // first in empty pointed slice: false + // first in empty array: false +} + +func ExampleCmpFirst_struct() { + t := &testing.T{} + + type Person struct { + Fullname string `json:"fullname"` + Age int `json:"age"` + } + + got := []*Person{ + { + Fullname: "Bob Foobar", + Age: 42, + }, + { + Fullname: "Alice Bingo", + Age: 37, + }, + } + + ok := td.CmpFirst(t, got, td.Smuggle("Age", td.Gt(30)), td.Smuggle("Fullname", "Bob Foobar")) + fmt.Println("first person.Age > 30 → Bob:", ok) + + ok = td.CmpFirst(t, got, td.JSONPointer("/age", td.Gt(30)), td.SuperJSONOf(`{"fullname":"Bob Foobar"}`)) + fmt.Println("first person.Age > 30 → Bob, using JSON:", ok) + + ok = td.CmpFirst(t, got, td.JSONPointer("/age", td.Gt(30)), td.JSONPointer("/fullname", td.HasPrefix("Bob"))) + fmt.Println("first person.Age > 30 → Bob, using JSONPointer:", ok) + + // Output: + // first person.Age > 30 → Bob: true + // first person.Age > 30 → Bob, using JSON: true + // first person.Age > 30 → Bob, using JSONPointer: true +} + +func ExampleCmpGrep_classic() { + t := &testing.T{} + + got := []int{-3, -2, -1, 0, 1, 2, 3} + + ok := td.CmpGrep(t, got, td.Gt(0), []int{1, 2, 3}) + fmt.Println("check positive numbers:", ok) + + isEven := func(x int) bool { return x%2 == 0 } + + ok = td.CmpGrep(t, got, isEven, []int{-2, 0, 2}) + fmt.Println("even numbers are -2, 0 and 2:", ok) + + ok = td.CmpGrep(t, got, isEven, td.Set(0, 2, -2)) + fmt.Println("even numbers are also 0, 2 and -2:", ok) + + ok = td.CmpGrep(t, got, isEven, td.ArrayEach(td.Code(isEven))) + fmt.Println("even numbers are each even:", ok) + + // Output: + // check positive numbers: true + // even numbers are -2, 0 and 2: true + // even numbers are also 0, 2 and -2: true + // even numbers are each even: true +} + +func ExampleCmpGrep_nil() { + t := &testing.T{} + + var got []int + ok := td.CmpGrep(t, got, td.Gt(0), ([]int)(nil)) + fmt.Println("typed []int nil:", ok) + + ok = td.CmpGrep(t, got, td.Gt(0), ([]string)(nil)) + fmt.Println("typed []string nil:", ok) + + ok = td.CmpGrep(t, got, td.Gt(0), td.Nil()) + fmt.Println("td.Nil:", ok) + + ok = td.CmpGrep(t, got, td.Gt(0), []int{}) + fmt.Println("empty non-nil slice:", ok) + + // Output: + // typed []int nil: true + // typed []string nil: false + // td.Nil: true + // empty non-nil slice: false +} + +func ExampleCmpGrep_struct() { + t := &testing.T{} + + type Person struct { + Fullname string `json:"fullname"` + Age int `json:"age"` + } + + got := []*Person{ + { + Fullname: "Bob Foobar", + Age: 42, + }, + { + Fullname: "Alice Bingo", + Age: 27, + }, + } + + ok := td.CmpGrep(t, got, td.Smuggle("Age", td.Gt(30)), td.All( + td.Len(1), + td.ArrayEach(td.Smuggle("Fullname", "Bob Foobar")), + )) + fmt.Println("person.Age > 30 → only Bob:", ok) + + ok = td.CmpGrep(t, got, td.JSONPointer("/age", td.Gt(30)), td.JSON(`[ SuperMapOf({"fullname":"Bob Foobar"}) ]`)) + fmt.Println("person.Age > 30 → only Bob, using JSON:", ok) + + // Output: + // person.Age > 30 → only Bob: true + // person.Age > 30 → only Bob, using JSON: true +} + func ExampleCmpGt_int() { t := &testing.T{} @@ -1356,6 +1520,88 @@ func ExampleCmpKeys() { // Each key is 3 bytes long: true } +func ExampleCmpLast_classic() { + t := &testing.T{} + + got := []int{-3, -2, -1, 0, 1, 2, 3} + + ok := td.CmpLast(t, got, td.Lt(0), -1) + fmt.Println("last negative number is -1:", ok) + + isEven := func(x int) bool { return x%2 == 0 } + + ok = td.CmpLast(t, got, isEven, 2) + fmt.Println("last even number is 2:", ok) + + ok = td.CmpLast(t, got, isEven, td.Gt(0)) + fmt.Println("last even number is > 0:", ok) + + ok = td.CmpLast(t, got, isEven, td.Code(isEven)) + fmt.Println("last even number is well even:", ok) + + // Output: + // last negative number is -1: true + // last even number is 2: true + // last even number is > 0: true + // last even number is well even: true +} + +func ExampleCmpLast_empty() { + t := &testing.T{} + + ok := td.CmpLast(t, ([]int)(nil), td.Gt(0), td.Gt(0)) + fmt.Println("last in nil slice:", ok) + + ok = td.CmpLast(t, []int{}, td.Gt(0), td.Gt(0)) + fmt.Println("last in empty slice:", ok) + + ok = td.CmpLast(t, &[]int{}, td.Gt(0), td.Gt(0)) + fmt.Println("last in empty pointed slice:", ok) + + ok = td.CmpLast(t, [0]int{}, td.Gt(0), td.Gt(0)) + fmt.Println("last in empty array:", ok) + + // Output: + // last in nil slice: false + // last in empty slice: false + // last in empty pointed slice: false + // last in empty array: false +} + +func ExampleCmpLast_struct() { + t := &testing.T{} + + type Person struct { + Fullname string `json:"fullname"` + Age int `json:"age"` + } + + got := []*Person{ + { + Fullname: "Bob Foobar", + Age: 42, + }, + { + Fullname: "Alice Bingo", + Age: 37, + }, + } + + ok := td.CmpLast(t, got, td.Smuggle("Age", td.Gt(30)), td.Smuggle("Fullname", "Alice Bingo")) + fmt.Println("last person.Age > 30 → Alice:", ok) + + ok = td.CmpLast(t, got, td.JSONPointer("/age", td.Gt(30)), td.SuperJSONOf(`{"fullname":"Alice Bingo"}`)) + fmt.Println("last person.Age > 30 → Alice, using JSON:", ok) + + ok = td.CmpLast(t, got, td.JSONPointer("/age", td.Gt(30)), td.JSONPointer("/fullname", td.HasPrefix("Alice"))) + fmt.Println("first person.Age > 30 → Alice, using JSONPointer:", ok) + + // Output: + // last person.Age > 30 → Alice: true + // last person.Age > 30 → Alice, using JSON: true + // first person.Age > 30 → Alice, using JSONPointer: true +} + func ExampleCmpLax() { t := &testing.T{} diff --git a/td/example_t_test.go b/td/example_t_test.go index b5eef765..8289490d 100644 --- a/td/example_t_test.go +++ b/td/example_t_test.go @@ -727,6 +727,170 @@ func ExampleT_Empty_pointers() { // false } +func ExampleT_First_classic() { + t := td.NewT(&testing.T{}) + + got := []int{-3, -2, -1, 0, 1, 2, 3} + + ok := t.First(got, td.Gt(0), 1) + fmt.Println("first positive number is 1:", ok) + + isEven := func(x int) bool { return x%2 == 0 } + + ok = t.First(got, isEven, -2) + fmt.Println("first even number is -2:", ok) + + ok = t.First(got, isEven, td.Lt(0)) + fmt.Println("first even number is < 0:", ok) + + ok = t.First(got, isEven, td.Code(isEven)) + fmt.Println("first even number is well even:", ok) + + // Output: + // first positive number is 1: true + // first even number is -2: true + // first even number is < 0: true + // first even number is well even: true +} + +func ExampleT_First_empty() { + t := td.NewT(&testing.T{}) + + ok := t.First(([]int)(nil), td.Gt(0), td.Gt(0)) + fmt.Println("first in nil slice:", ok) + + ok = t.First([]int{}, td.Gt(0), td.Gt(0)) + fmt.Println("first in empty slice:", ok) + + ok = t.First(&[]int{}, td.Gt(0), td.Gt(0)) + fmt.Println("first in empty pointed slice:", ok) + + ok = t.First([0]int{}, td.Gt(0), td.Gt(0)) + fmt.Println("first in empty array:", ok) + + // Output: + // first in nil slice: false + // first in empty slice: false + // first in empty pointed slice: false + // first in empty array: false +} + +func ExampleT_First_struct() { + t := td.NewT(&testing.T{}) + + type Person struct { + Fullname string `json:"fullname"` + Age int `json:"age"` + } + + got := []*Person{ + { + Fullname: "Bob Foobar", + Age: 42, + }, + { + Fullname: "Alice Bingo", + Age: 37, + }, + } + + ok := t.First(got, td.Smuggle("Age", td.Gt(30)), td.Smuggle("Fullname", "Bob Foobar")) + fmt.Println("first person.Age > 30 → Bob:", ok) + + ok = t.First(got, td.JSONPointer("/age", td.Gt(30)), td.SuperJSONOf(`{"fullname":"Bob Foobar"}`)) + fmt.Println("first person.Age > 30 → Bob, using JSON:", ok) + + ok = t.First(got, td.JSONPointer("/age", td.Gt(30)), td.JSONPointer("/fullname", td.HasPrefix("Bob"))) + fmt.Println("first person.Age > 30 → Bob, using JSONPointer:", ok) + + // Output: + // first person.Age > 30 → Bob: true + // first person.Age > 30 → Bob, using JSON: true + // first person.Age > 30 → Bob, using JSONPointer: true +} + +func ExampleT_Grep_classic() { + t := td.NewT(&testing.T{}) + + got := []int{-3, -2, -1, 0, 1, 2, 3} + + ok := t.Grep(got, td.Gt(0), []int{1, 2, 3}) + fmt.Println("check positive numbers:", ok) + + isEven := func(x int) bool { return x%2 == 0 } + + ok = t.Grep(got, isEven, []int{-2, 0, 2}) + fmt.Println("even numbers are -2, 0 and 2:", ok) + + ok = t.Grep(got, isEven, td.Set(0, 2, -2)) + fmt.Println("even numbers are also 0, 2 and -2:", ok) + + ok = t.Grep(got, isEven, td.ArrayEach(td.Code(isEven))) + fmt.Println("even numbers are each even:", ok) + + // Output: + // check positive numbers: true + // even numbers are -2, 0 and 2: true + // even numbers are also 0, 2 and -2: true + // even numbers are each even: true +} + +func ExampleT_Grep_nil() { + t := td.NewT(&testing.T{}) + + var got []int + ok := t.Grep(got, td.Gt(0), ([]int)(nil)) + fmt.Println("typed []int nil:", ok) + + ok = t.Grep(got, td.Gt(0), ([]string)(nil)) + fmt.Println("typed []string nil:", ok) + + ok = t.Grep(got, td.Gt(0), td.Nil()) + fmt.Println("td.Nil:", ok) + + ok = t.Grep(got, td.Gt(0), []int{}) + fmt.Println("empty non-nil slice:", ok) + + // Output: + // typed []int nil: true + // typed []string nil: false + // td.Nil: true + // empty non-nil slice: false +} + +func ExampleT_Grep_struct() { + t := td.NewT(&testing.T{}) + + type Person struct { + Fullname string `json:"fullname"` + Age int `json:"age"` + } + + got := []*Person{ + { + Fullname: "Bob Foobar", + Age: 42, + }, + { + Fullname: "Alice Bingo", + Age: 27, + }, + } + + ok := t.Grep(got, td.Smuggle("Age", td.Gt(30)), td.All( + td.Len(1), + td.ArrayEach(td.Smuggle("Fullname", "Bob Foobar")), + )) + fmt.Println("person.Age > 30 → only Bob:", ok) + + ok = t.Grep(got, td.JSONPointer("/age", td.Gt(30)), td.JSON(`[ SuperMapOf({"fullname":"Bob Foobar"}) ]`)) + fmt.Println("person.Age > 30 → only Bob, using JSON:", ok) + + // Output: + // person.Age > 30 → only Bob: true + // person.Age > 30 → only Bob, using JSON: true +} + func ExampleT_Gt_int() { t := td.NewT(&testing.T{}) @@ -1356,6 +1520,88 @@ func ExampleT_Keys() { // Each key is 3 bytes long: true } +func ExampleT_Last_classic() { + t := td.NewT(&testing.T{}) + + got := []int{-3, -2, -1, 0, 1, 2, 3} + + ok := t.Last(got, td.Lt(0), -1) + fmt.Println("last negative number is -1:", ok) + + isEven := func(x int) bool { return x%2 == 0 } + + ok = t.Last(got, isEven, 2) + fmt.Println("last even number is 2:", ok) + + ok = t.Last(got, isEven, td.Gt(0)) + fmt.Println("last even number is > 0:", ok) + + ok = t.Last(got, isEven, td.Code(isEven)) + fmt.Println("last even number is well even:", ok) + + // Output: + // last negative number is -1: true + // last even number is 2: true + // last even number is > 0: true + // last even number is well even: true +} + +func ExampleT_Last_empty() { + t := td.NewT(&testing.T{}) + + ok := t.Last(([]int)(nil), td.Gt(0), td.Gt(0)) + fmt.Println("last in nil slice:", ok) + + ok = t.Last([]int{}, td.Gt(0), td.Gt(0)) + fmt.Println("last in empty slice:", ok) + + ok = t.Last(&[]int{}, td.Gt(0), td.Gt(0)) + fmt.Println("last in empty pointed slice:", ok) + + ok = t.Last([0]int{}, td.Gt(0), td.Gt(0)) + fmt.Println("last in empty array:", ok) + + // Output: + // last in nil slice: false + // last in empty slice: false + // last in empty pointed slice: false + // last in empty array: false +} + +func ExampleT_Last_struct() { + t := td.NewT(&testing.T{}) + + type Person struct { + Fullname string `json:"fullname"` + Age int `json:"age"` + } + + got := []*Person{ + { + Fullname: "Bob Foobar", + Age: 42, + }, + { + Fullname: "Alice Bingo", + Age: 37, + }, + } + + ok := t.Last(got, td.Smuggle("Age", td.Gt(30)), td.Smuggle("Fullname", "Alice Bingo")) + fmt.Println("last person.Age > 30 → Alice:", ok) + + ok = t.Last(got, td.JSONPointer("/age", td.Gt(30)), td.SuperJSONOf(`{"fullname":"Alice Bingo"}`)) + fmt.Println("last person.Age > 30 → Alice, using JSON:", ok) + + ok = t.Last(got, td.JSONPointer("/age", td.Gt(30)), td.JSONPointer("/fullname", td.HasPrefix("Alice"))) + fmt.Println("first person.Age > 30 → Alice, using JSONPointer:", ok) + + // Output: + // last person.Age > 30 → Alice: true + // last person.Age > 30 → Alice, using JSON: true + // first person.Age > 30 → Alice, using JSONPointer: true +} + func ExampleT_CmpLax() { t := td.NewT(&testing.T{}) diff --git a/td/example_test.go b/td/example_test.go index 93498f7e..3bd07ba8 100644 --- a/td/example_test.go +++ b/td/example_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2021, Maxime Soulé +// Copyright (c) 2018-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the @@ -914,6 +914,248 @@ func ExampleEmpty_pointers() { // false } +func ExampleFirst_classic() { + t := &testing.T{} + + got := []int{-3, -2, -1, 0, 1, 2, 3} + + ok := td.Cmp(t, got, td.First(td.Gt(0), 1)) + fmt.Println("first positive number is 1:", ok) + + isEven := func(x int) bool { return x%2 == 0 } + + ok = td.Cmp(t, got, td.First(isEven, -2)) + fmt.Println("first even number is -2:", ok) + + ok = td.Cmp(t, got, td.First(isEven, td.Lt(0))) + fmt.Println("first even number is < 0:", ok) + + ok = td.Cmp(t, got, td.First(isEven, td.Code(isEven))) + fmt.Println("first even number is well even:", ok) + + // Output: + // first positive number is 1: true + // first even number is -2: true + // first even number is < 0: true + // first even number is well even: true +} + +func ExampleFirst_empty() { + t := &testing.T{} + + ok := td.Cmp(t, ([]int)(nil), td.First(td.Gt(0), td.Gt(0))) + fmt.Println("first in nil slice:", ok) + + ok = td.Cmp(t, []int{}, td.First(td.Gt(0), td.Gt(0))) + fmt.Println("first in empty slice:", ok) + + ok = td.Cmp(t, &[]int{}, td.First(td.Gt(0), td.Gt(0))) + fmt.Println("first in empty pointed slice:", ok) + + ok = td.Cmp(t, [0]int{}, td.First(td.Gt(0), td.Gt(0))) + fmt.Println("first in empty array:", ok) + + // Output: + // first in nil slice: false + // first in empty slice: false + // first in empty pointed slice: false + // first in empty array: false +} + +func ExampleFirst_struct() { + t := &testing.T{} + + type Person struct { + Fullname string `json:"fullname"` + Age int `json:"age"` + } + + got := []*Person{ + { + Fullname: "Bob Foobar", + Age: 42, + }, + { + Fullname: "Alice Bingo", + Age: 37, + }, + } + + ok := td.Cmp(t, got, td.First( + td.Smuggle("Age", td.Gt(30)), + td.Smuggle("Fullname", "Bob Foobar"))) + fmt.Println("first person.Age > 30 → Bob:", ok) + + ok = td.Cmp(t, got, td.First( + td.JSONPointer("/age", td.Gt(30)), + td.SuperJSONOf(`{"fullname":"Bob Foobar"}`))) + fmt.Println("first person.Age > 30 → Bob, using JSON:", ok) + + ok = td.Cmp(t, got, td.First( + td.JSONPointer("/age", td.Gt(30)), + td.JSONPointer("/fullname", td.HasPrefix("Bob")))) + fmt.Println("first person.Age > 30 → Bob, using JSONPointer:", ok) + + // Output: + // first person.Age > 30 → Bob: true + // first person.Age > 30 → Bob, using JSON: true + // first person.Age > 30 → Bob, using JSONPointer: true +} + +func ExampleFirst_json() { + t := &testing.T{} + + got := map[string]any{ + "values": []int{1, 2, 3, 4}, + } + ok := td.Cmp(t, got, td.JSON(`{"values": First(Gt(2), 3)}`)) + fmt.Println("first number > 2:", ok) + + got = map[string]any{ + "persons": []map[string]any{ + {"id": 1, "name": "Joe"}, + {"id": 2, "name": "Bob"}, + {"id": 3, "name": "Alice"}, + {"id": 4, "name": "Brian"}, + {"id": 5, "name": "Britt"}, + }, + } + ok = td.Cmp(t, got, td.JSON(` +{ + "persons": First(JSONPointer("/name", "Brian"), {"id": 4, "name": "Brian"}) +}`)) + fmt.Println(`is "Brian" content OK:`, ok) + + ok = td.Cmp(t, got, td.JSON(` +{ + "persons": First(JSONPointer("/name", "Brian"), JSONPointer("/id", 4)) +}`)) + fmt.Println(`ID of "Brian" is 4:`, ok) + + // Output: + // first number > 2: true + // is "Brian" content OK: true + // ID of "Brian" is 4: true +} + +func ExampleGrep_classic() { + t := &testing.T{} + + got := []int{-3, -2, -1, 0, 1, 2, 3} + + ok := td.Cmp(t, got, td.Grep(td.Gt(0), []int{1, 2, 3})) + fmt.Println("check positive numbers:", ok) + + isEven := func(x int) bool { return x%2 == 0 } + + ok = td.Cmp(t, got, td.Grep(isEven, []int{-2, 0, 2})) + fmt.Println("even numbers are -2, 0 and 2:", ok) + + ok = td.Cmp(t, got, td.Grep(isEven, td.Set(0, 2, -2))) + fmt.Println("even numbers are also 0, 2 and -2:", ok) + + ok = td.Cmp(t, got, td.Grep(isEven, td.ArrayEach(td.Code(isEven)))) + fmt.Println("even numbers are each even:", ok) + + // Output: + // check positive numbers: true + // even numbers are -2, 0 and 2: true + // even numbers are also 0, 2 and -2: true + // even numbers are each even: true +} + +func ExampleGrep_nil() { + t := &testing.T{} + + var got []int + ok := td.Cmp(t, got, td.Grep(td.Gt(0), ([]int)(nil))) + fmt.Println("typed []int nil:", ok) + + ok = td.Cmp(t, got, td.Grep(td.Gt(0), ([]string)(nil))) + fmt.Println("typed []string nil:", ok) + + ok = td.Cmp(t, got, td.Grep(td.Gt(0), td.Nil())) + fmt.Println("td.Nil:", ok) + + ok = td.Cmp(t, got, td.Grep(td.Gt(0), []int{})) + fmt.Println("empty non-nil slice:", ok) + + // Output: + // typed []int nil: true + // typed []string nil: false + // td.Nil: true + // empty non-nil slice: false +} + +func ExampleGrep_struct() { + t := &testing.T{} + + type Person struct { + Fullname string `json:"fullname"` + Age int `json:"age"` + } + + got := []*Person{ + { + Fullname: "Bob Foobar", + Age: 42, + }, + { + Fullname: "Alice Bingo", + Age: 27, + }, + } + + ok := td.Cmp(t, got, td.Grep( + td.Smuggle("Age", td.Gt(30)), + td.All( + td.Len(1), + td.ArrayEach(td.Smuggle("Fullname", "Bob Foobar")), + ))) + fmt.Println("person.Age > 30 → only Bob:", ok) + + ok = td.Cmp(t, got, td.Grep( + td.JSONPointer("/age", td.Gt(30)), + td.JSON(`[ SuperMapOf({"fullname":"Bob Foobar"}) ]`))) + fmt.Println("person.Age > 30 → only Bob, using JSON:", ok) + + // Output: + // person.Age > 30 → only Bob: true + // person.Age > 30 → only Bob, using JSON: true +} + +func ExampleGrep_json() { + t := &testing.T{} + + got := map[string]any{ + "values": []int{1, 2, 3, 4}, + } + ok := td.Cmp(t, got, td.JSON(`{"values": Grep(Gt(2), [3, 4])}`)) + fmt.Println("grep a number > 2:", ok) + + got = map[string]any{ + "persons": []map[string]any{ + {"id": 1, "name": "Joe"}, + {"id": 2, "name": "Bob"}, + {"id": 3, "name": "Alice"}, + {"id": 4, "name": "Brian"}, + {"id": 5, "name": "Britt"}, + }, + } + ok = td.Cmp(t, got, td.JSON(` +{ + "persons": Grep(JSONPointer("/name", HasPrefix("Br")), [ + {"id": 4, "name": "Brian"}, + {"id": 5, "name": "Britt"}, + ]) +}`)) + fmt.Println(`grep "Br" prefix:`, ok) + + // Output: + // grep a number > 2: true + // grep "Br" prefix: true +} + func ExampleGt_int() { t := &testing.T{} @@ -1489,6 +1731,130 @@ func ExampleKeys() { // Each key is 3 bytes long: true } +func ExampleLast_classic() { + t := &testing.T{} + + got := []int{-3, -2, -1, 0, 1, 2, 3} + + ok := td.Cmp(t, got, td.Last(td.Lt(0), -1)) + fmt.Println("last negative number is -1:", ok) + + isEven := func(x int) bool { return x%2 == 0 } + + ok = td.Cmp(t, got, td.Last(isEven, 2)) + fmt.Println("last even number is 2:", ok) + + ok = td.Cmp(t, got, td.Last(isEven, td.Gt(0))) + fmt.Println("last even number is > 0:", ok) + + ok = td.Cmp(t, got, td.Last(isEven, td.Code(isEven))) + fmt.Println("last even number is well even:", ok) + + // Output: + // last negative number is -1: true + // last even number is 2: true + // last even number is > 0: true + // last even number is well even: true +} + +func ExampleLast_empty() { + t := &testing.T{} + + ok := td.Cmp(t, ([]int)(nil), td.Last(td.Gt(0), td.Gt(0))) + fmt.Println("last in nil slice:", ok) + + ok = td.Cmp(t, []int{}, td.Last(td.Gt(0), td.Gt(0))) + fmt.Println("last in empty slice:", ok) + + ok = td.Cmp(t, &[]int{}, td.Last(td.Gt(0), td.Gt(0))) + fmt.Println("last in empty pointed slice:", ok) + + ok = td.Cmp(t, [0]int{}, td.Last(td.Gt(0), td.Gt(0))) + fmt.Println("last in empty array:", ok) + + // Output: + // last in nil slice: false + // last in empty slice: false + // last in empty pointed slice: false + // last in empty array: false +} + +func ExampleLast_struct() { + t := &testing.T{} + + type Person struct { + Fullname string `json:"fullname"` + Age int `json:"age"` + } + + got := []*Person{ + { + Fullname: "Bob Foobar", + Age: 42, + }, + { + Fullname: "Alice Bingo", + Age: 37, + }, + } + + ok := td.Cmp(t, got, td.Last( + td.Smuggle("Age", td.Gt(30)), + td.Smuggle("Fullname", "Alice Bingo"))) + fmt.Println("last person.Age > 30 → Alice:", ok) + + ok = td.Cmp(t, got, td.Last( + td.JSONPointer("/age", td.Gt(30)), + td.SuperJSONOf(`{"fullname":"Alice Bingo"}`))) + fmt.Println("last person.Age > 30 → Alice, using JSON:", ok) + + ok = td.Cmp(t, got, td.Last( + td.JSONPointer("/age", td.Gt(30)), + td.JSONPointer("/fullname", td.HasPrefix("Alice")))) + fmt.Println("first person.Age > 30 → Alice, using JSONPointer:", ok) + + // Output: + // last person.Age > 30 → Alice: true + // last person.Age > 30 → Alice, using JSON: true + // first person.Age > 30 → Alice, using JSONPointer: true +} + +func ExampleLast_json() { + t := &testing.T{} + + got := map[string]any{ + "values": []int{1, 2, 3, 4}, + } + ok := td.Cmp(t, got, td.JSON(`{"values": Last(Lt(3), 2)}`)) + fmt.Println("last number < 3:", ok) + + got = map[string]any{ + "persons": []map[string]any{ + {"id": 1, "name": "Joe"}, + {"id": 2, "name": "Bob"}, + {"id": 3, "name": "Alice"}, + {"id": 4, "name": "Brian"}, + {"id": 5, "name": "Britt"}, + }, + } + ok = td.Cmp(t, got, td.JSON(` +{ + "persons": Last(JSONPointer("/name", "Brian"), {"id": 4, "name": "Brian"}) +}`)) + fmt.Println(`is "Brian" content OK:`, ok) + + ok = td.Cmp(t, got, td.JSON(` +{ + "persons": Last(JSONPointer("/name", "Brian"), JSONPointer("/id", 4)) +}`)) + fmt.Println(`ID of "Brian" is 4:`, ok) + + // Output: + // last number < 3: true + // is "Brian" content OK: true + // ID of "Brian" is 4: true +} + func ExampleLax() { t := &testing.T{} diff --git a/td/t.go b/td/t.go index 450a066e..9e6e9597 100644 --- a/td/t.go +++ b/td/t.go @@ -225,6 +225,44 @@ func (t *T) Empty(got any, args ...any) bool { return t.Cmp(got, Empty(), args...) } +// First is a shortcut for: +// +// t.Cmp(got, td.First(filter, expectedValue), args...) +// +// See [First] 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) First(got, filter, expectedValue any, args ...any) bool { + t.Helper() + return t.Cmp(got, First(filter, expectedValue), args...) +} + +// Grep is a shortcut for: +// +// t.Cmp(got, td.Grep(filter, expectedValue), args...) +// +// See [Grep] 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) Grep(got, filter, expectedValue any, args ...any) bool { + t.Helper() + return t.Cmp(got, Grep(filter, expectedValue), args...) +} + // Gt is a shortcut for: // // t.Cmp(got, td.Gt(minExpectedValue), args...) @@ -377,6 +415,25 @@ func (t *T) Keys(got, val any, args ...any) bool { return t.Cmp(got, Keys(val), args...) } +// Last is a shortcut for: +// +// t.Cmp(got, td.Last(filter, expectedValue), args...) +// +// See [Last] 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) Last(got, filter, expectedValue any, args ...any) bool { + t.Helper() + return t.Cmp(got, Last(filter, expectedValue), args...) +} + // CmpLax is a shortcut for: // // t.Cmp(got, td.Lax(expectedValue), args...) diff --git a/td/td_grep.go b/td/td_grep.go new file mode 100644 index 00000000..3630b81a --- /dev/null +++ b/td/td_grep.go @@ -0,0 +1,395 @@ +// Copyright (c) 2022, 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 ( + "reflect" + + "github.com/maxatome/go-testdeep/internal/ctxerr" + "github.com/maxatome/go-testdeep/internal/types" +) + +const grepUsage = "(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE)" + +type tdGrepBase struct { + tdSmugglerBase + filter reflect.Value // func (argType ≠ nil) OR TestDeep operator + argType reflect.Type +} + +func (g *tdGrepBase) initGrepBase(filter, expectedValue any) { + g.tdSmugglerBase = newSmugglerBase(expectedValue, 1) + + if !g.isTestDeeper { + g.expectedValue = reflect.ValueOf(expectedValue) + } + + if op, ok := filter.(TestDeep); ok { + g.filter = reflect.ValueOf(op) + return + } + + vfilter := reflect.ValueOf(filter) + if vfilter.Kind() != reflect.Func { + g.err = ctxerr.OpBad(g.GetLocation().Func, + "usage: %s%s, FILTER_FUNC must be a function or FILTER_TESTDEEP_OPERATOR a TestDeep operator", + g.GetLocation().Func, grepUsage) + return + } + + filterType := vfilter.Type() + if filterType.IsVariadic() || filterType.NumIn() != 1 { + g.err = ctxerr.OpBad(g.GetLocation().Func, + "usage: %s%s, FILTER_FUNC must take only one non-variadic argument", + g.GetLocation().Func, grepUsage) + return + } + if filterType.NumOut() != 1 || filterType.Out(0) != types.Bool { + g.err = ctxerr.OpBad(g.GetLocation().Func, + "usage: %s%s, FILTER_FUNC must return bool", + g.GetLocation().Func, grepUsage) + return + } + + g.argType = filterType.In(0) + g.filter = vfilter +} + +func (g *tdGrepBase) matchItem(ctx ctxerr.Context, idx int, item reflect.Value) (bool, *ctxerr.Error) { + if g.argType == nil { + // g.filter is a TestDeep operator + return deepValueEqualFinalOK(ctx, item, g.filter), nil + } + + // item is an interface, but the filter function does not expect an + // interface, resolve it + if item.Kind() == reflect.Interface && g.argType.Kind() != reflect.Interface { + item = item.Elem() + } + + if !item.Type().AssignableTo(g.argType) { + if !types.IsConvertible(item, g.argType) { + if ctx.BooleanError { + return false, ctxerr.BooleanError + } + return false, ctx.AddArrayIndex(idx).CollectError(&ctxerr.Error{ + Message: "incompatible parameter type", + Got: types.RawString(item.Type().String()), + Expected: types.RawString(g.argType.String()), + }) + } + item = item.Convert(g.argType) + } + + return g.filter.Call([]reflect.Value{item})[0].Bool(), nil +} + +func (g *tdGrepBase) HandleInvalid() bool { + return true // Knows how to handle untyped nil values (aka invalid values) +} + +func (g *tdGrepBase) String() string { + if g.err != nil { + return g.stringError() + } + if g.argType == nil { + return S("%s(%s)", g.GetLocation().Func, g.filter.Interface().(TestDeep)) + } + return S("%s(%s)", g.GetLocation().Func, g.filter.Type()) +} + +func (g *tdGrepBase) TypeBehind() reflect.Type { + if g.err != nil { + return nil + } + return g.internalTypeBehind() +} + +// sliceTypeBehind is used by First & Last TypeBehind method. +func (g *tdGrepBase) sliceTypeBehind() reflect.Type { + typ := g.TypeBehind() + if typ == nil { + return nil + } + return reflect.SliceOf(typ) +} + +func (g *tdGrepBase) notFound(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { + if ctx.BooleanError { + return ctxerr.BooleanError + } + return ctx.CollectError(&ctxerr.Error{ + Message: "item not found", + Got: got, + Expected: types.RawString(g.String()), + }) +} + +func grepResolvePtr(ctx ctxerr.Context, got *reflect.Value) *ctxerr.Error { + if got.Kind() == reflect.Ptr { + gotElem := got.Elem() + if !gotElem.IsValid() { + if ctx.BooleanError { + return ctxerr.BooleanError + } + return ctx.CollectError(ctxerr.NilPointer(*got, "non-nil *slice OR *array")) + } + switch gotElem.Kind() { + case reflect.Slice, reflect.Array: + *got = gotElem + } + } + return nil +} + +func grepBadKind(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { + if ctx.BooleanError { + return ctxerr.BooleanError + } + return ctx.CollectError(ctxerr.BadKind(got, "slice OR array OR *slice OR *array")) +} + +type tdGrep struct { + tdGrepBase +} + +var _ TestDeep = &tdGrep{} + +// summary(Grep): reduces a slice or an array before comparing its content +// input(Grep): array,slice,ptr(ptr on array/slice) + +// Grep is a smuggler operator. It takes an array, a slice or a +// pointer on array/slice. For each item it applies filter, a +// [TestDeep] operator or a function returning a bool, and produces a +// slice consisting of those items for which the filter matched and +// compares it to expectedValue. The filter matches when it is a: +// - [TestDeep] operator and it matches for the item; +// - function receiving the item and it returns true. +// +// expectedValue can be a [TestDeep] operator or a slice (but never an +// array nor a pointer on a slice/array nor any other kind). +// +// got := []int{-3, -2, -1, 0, 1, 2, 3} +// td.Cmp(t, got, td.Grep(td.Gt(0), []int{1, 2, 3})) // succeeds +// td.Cmp(t, got, td.Grep( +// func(x int) bool { return x%2 == 0 }, +// []int{-2, 0, 2})) // succeeds +// td.Cmp(t, got, td.Grep( +// func(x int) bool { return x%2 == 0 }, +// td.Set(0, 2, -2))) // succeeds +// +// If Grep receives a nil slice or a pointer on a nil slice, it always +// returns a nil slice: +// +// var got []int +// td.Cmp(t, got, td.Grep(td.Gt(0), ([]int)(nil))) // succeeds +// td.Cmp(t, got, td.Grep(td.Gt(0), td.Nil())) // succeeds +// td.Cmp(t, got, td.Grep(td.Gt(0), []int{})) // fails +// +// See also [First] and [Last]. +func Grep(filter, expectedValue any) TestDeep { + g := tdGrep{} + g.initGrepBase(filter, expectedValue) + + if g.err == nil && !g.isTestDeeper && g.expectedValue.Kind() != reflect.Slice { + g.err = ctxerr.OpBad("Grep", + "usage: Grep%s, EXPECTED_VALUE must be a slice not a %s", + grepUsage, types.KindType(g.expectedValue)) + } + return &g +} + +func (g *tdGrep) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { + if g.err != nil { + return ctx.CollectError(g.err) + } + + if rErr := grepResolvePtr(ctx, &got); rErr != nil { + return rErr + } + + 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, + ) + } + + l := got.Len() + out := reflect.MakeSlice(reflect.SliceOf(got.Type().Elem()), 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) + } + + return grepBadKind(ctx, got) +} + +type tdFirst struct { + tdGrepBase +} + +var _ TestDeep = &tdFirst{} + +// summary(First): find the first matching item of a slice or an array +// then compare its content +// input(First): array,slice,ptr(ptr on array/slice) + +// First is a smuggler operator. It takes an array, a slice or a +// pointer on array/slice. For each item it applies filter, a +// [TestDeep] operator or a function returning a bool. It takes the +// first item for which the filter matched and compares it to +// expectedValue. The filter matches when it is a: +// - [TestDeep] operator and it matches for the item; +// - function receiving the item and it returns true. +// +// expectedValue can of course be a [TestDeep] operator. +// +// got := []int{-3, -2, -1, 0, 1, 2, 3} +// td.Cmp(t, got, td.First(td.Gt(0), 1)) // succeeds +// td.Cmp(t, got, td.First(func(x int) bool { return x%2 == 0 }, -2)) // succeeds +// td.Cmp(t, got, td.First(func(x int) bool { return x%2 == 0 }, td.Lt(0))) // succeeds +// +// If the input is empty (and/or nil for a slice), an "item not found" +// error is raised before comparing to expectedValue. +// +// var got []int +// td.Cmp(t, got, td.First(td.Gt(0), td.Gt(0))) // fails +// td.Cmp(t, []int{}, td.First(td.Gt(0), td.Gt(0))) // fails +// td.Cmp(t, [0]int{}, td.First(td.Gt(0), td.Gt(0))) // fails +// +// See also [Last] and [Grep]. +func First(filter, expectedValue any) TestDeep { + g := tdFirst{} + g.initGrepBase(filter, expectedValue) + return &g +} + +func (g *tdFirst) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { + if g.err != nil { + return ctx.CollectError(g.err) + } + + if rErr := grepResolvePtr(ctx, &got); rErr != nil { + return rErr + } + + switch got.Kind() { + case reflect.Slice, reflect.Array: + for idx, l := 0, got.Len(); idx < l; idx++ { + item := got.Index(idx) + ok, rErr := g.matchItem(ctx, idx, item) + if rErr != nil { + return rErr + } + if ok { + return deepValueEqual( + ctx.AddCustomLevel(S("", idx)), + item, + g.expectedValue, + ) + } + } + return g.notFound(ctx, got) + } + + return grepBadKind(ctx, got) +} + +func (g *tdFirst) TypeBehind() reflect.Type { + return g.sliceTypeBehind() +} + +type tdLast struct { + tdGrepBase +} + +var _ TestDeep = &tdLast{} + +// summary(Last): find the last matching item of a slice or an array +// then compare its content +// input(Last): array,slice,ptr(ptr on array/slice) + +// Last is a smuggler operator. It takes an array, a slice or a +// pointer on array/slice. For each item it applies filter, a +// [TestDeep] operator or a function returning a bool. It takes the +// last item for which the filter matched and compares it to +// expectedValue. The filter matches when it is a: +// - [TestDeep] operator and it matches for the item; +// - function receiving the item and it returns true. +// +// expectedValue can of course be a [TestDeep] operator. +// +// got := []int{-3, -2, -1, 0, 1, 2, 3} +// td.Cmp(t, got, td.Last(td.Lt(0), -1)) // succeeds +// td.Cmp(t, got, td.Last(func(x int) bool { return x%2 == 0 }, 2)) // succeeds +// td.Cmp(t, got, td.Last(func(x int) bool { return x%2 == 0 }, td.Gt(0))) // succeeds +// +// If the input is empty (and/or nil for a slice), an "item not found" +// error is raised before comparing to expectedValue. +// +// var got []int +// td.Cmp(t, got, td.Last(td.Gt(0), td.Gt(0))) // fails +// td.Cmp(t, []int{}, td.Last(td.Gt(0), td.Gt(0))) // fails +// td.Cmp(t, [0]int{}, td.Last(td.Gt(0), td.Gt(0))) // fails +// +// See also [First] and [Grep]. +func Last(filter, expectedValue any) TestDeep { + g := tdLast{} + g.initGrepBase(filter, expectedValue) + return &g +} + +func (g *tdLast) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { + if g.err != nil { + return ctx.CollectError(g.err) + } + + if rErr := grepResolvePtr(ctx, &got); rErr != nil { + return rErr + } + + switch got.Kind() { + case reflect.Slice, reflect.Array: + for idx := got.Len() - 1; idx >= 0; idx-- { + item := got.Index(idx) + ok, rErr := g.matchItem(ctx, idx, item) + if rErr != nil { + return rErr + } + if ok { + return deepValueEqual( + ctx.AddCustomLevel(S("", idx)), + item, + g.expectedValue, + ) + } + } + return g.notFound(ctx, got) + } + + return grepBadKind(ctx, got) +} + +func (g *tdLast) TypeBehind() reflect.Type { + return g.sliceTypeBehind() +} diff --git a/td/td_grep_test.go b/td/td_grep_test.go new file mode 100644 index 00000000..f5f9959e --- /dev/null +++ b/td/td_grep_test.go @@ -0,0 +1,791 @@ +// Copyright (c) 2022, 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 ( + "testing" + + "github.com/maxatome/go-testdeep/internal/test" + "github.com/maxatome/go-testdeep/td" +) + +func TestGrep(t *testing.T) { + t.Run("basic", func(t *testing.T) { + got := [...]int{-3, -2, -1, 0, 1, 2, 3} + sgot := got[:] + + testCases := []struct { + name string + got any + }{ + {"slice", sgot}, + {"array", got}, + {"*slice", &sgot}, + {"*array", &got}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + checkOK(t, tc.got, td.Grep(td.Gt(0), []int{1, 2, 3})) + checkOK(t, tc.got, td.Grep(td.Not(td.Between(-2, 2)), []int{-3, 3})) + + checkOK(t, tc.got, td.Grep( + func(x int) bool { return (x & 1) != 0 }, + []int{-3, -1, 1, 3})) + + checkOK(t, tc.got, td.Grep( + func(x int64) bool { return (x & 1) != 0 }, + []int{-3, -1, 1, 3}), + "int64 filter vs int items") + + checkOK(t, tc.got, td.Grep( + func(x any) bool { return (x.(int) & 1) != 0 }, + []int{-3, -1, 1, 3}), + "any filter vs int items") + }) + } + }) + + t.Run("struct", func(t *testing.T) { + type person struct { + ID int64 + Name string + } + got := [...]person{ + {ID: 1, Name: "Joe"}, + {ID: 2, Name: "Bob"}, + {ID: 3, Name: "Alice"}, + {ID: 4, Name: "Brian"}, + {ID: 5, Name: "Britt"}, + } + sgot := got[:] + + testCases := []struct { + name string + got any + }{ + {"slice", sgot}, + {"array", got}, + {"*slice", &sgot}, + {"*array", &got}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + checkOK(t, tc.got, td.Grep( + td.JSONPointer("/Name", td.HasPrefix("Br")), + []person{{ID: 4, Name: "Brian"}, {ID: 5, Name: "Britt"}})) + + checkOK(t, tc.got, td.Grep( + func(p person) bool { return p.ID < 3 }, + []person{{ID: 1, Name: "Joe"}, {ID: 2, Name: "Bob"}})) + }) + } + }) + + t.Run("interfaces", func(t *testing.T) { + got := [...]any{-3, -2, -1, 0, 1, 2, 3} + sgot := got[:] + + testCases := []struct { + name string + got any + }{ + {"slice", sgot}, + {"array", got}, + {"*slice", &sgot}, + {"*array", &got}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + checkOK(t, tc.got, td.Grep(td.Gt(0), []any{1, 2, 3})) + checkOK(t, tc.got, td.Grep(td.Not(td.Between(-2, 2)), []any{-3, 3})) + + checkOK(t, tc.got, td.Grep( + func(x int) bool { return (x & 1) != 0 }, + []any{-3, -1, 1, 3})) + + checkOK(t, tc.got, td.Grep( + func(x int64) bool { return (x & 1) != 0 }, + []any{-3, -1, 1, 3}), + "int64 filter vs any/int items") + + checkOK(t, tc.got, td.Grep( + func(x any) bool { return (x.(int) & 1) != 0 }, + []any{-3, -1, 1, 3}), + "any filter vs any/int items") + }) + } + }) + + t.Run("interfaces error", func(t *testing.T) { + got := [...]any{123, "foo"} + sgot := got[:] + + testCases := []struct { + name string + got any + }{ + {"slice", sgot}, + {"array", got}, + {"*slice", &sgot}, + {"*array", &got}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + checkError(t, tc.got, + td.Grep(func(x int) bool { return true }, []string{"never reached"}), + expectedError{ + Message: mustBe("incompatible parameter type"), + Path: mustBe("DATA[1]"), + Got: mustBe("string"), + Expected: mustBe("int"), + }) + }) + } + }) + + t.Run("nil slice", func(t *testing.T) { + var got []int + testCases := []struct { + name string + got any + }{ + {"slice", got}, + {"*slice", &got}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + checkOK(t, tc.got, td.Grep(td.Gt(666), ([]int)(nil))) + }) + } + }) + + t.Run("nil pointer", func(t *testing.T) { + checkError(t, (*[]int)(nil), td.Grep(td.Ignore(), []int{33}), + expectedError{ + Message: mustBe("nil pointer"), + Path: mustBe("DATA"), + Got: mustBe("nil *slice (*[]int type)"), + Expected: mustBe("non-nil *slice OR *array"), + }) + }) + + t.Run("JSON", func(t *testing.T) { + got := map[string]any{ + "values": []int{1, 2, 3, 4}, + } + checkOK(t, got, td.JSON(`{"values": Grep(Gt(2), [3, 4])}`)) + }) + + t.Run("errors", func(t *testing.T) { + for _, filter := range []any{nil, 33} { + checkError(t, "never tested", + td.Grep(filter, 42), + expectedError{ + Message: mustBe("bad usage of Grep operator"), + Path: mustBe("DATA"), + Summary: mustBe("usage: Grep(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must be a function or FILTER_TESTDEEP_OPERATOR a TestDeep operator"), + }, + "filter:", filter) + } + + for _, filter := range []any{ + func() bool { return true }, + func(a, b int) bool { return true }, + func(a ...int) bool { return true }, + } { + checkError(t, "never tested", + td.Grep(filter, 42), + expectedError{ + Message: mustBe("bad usage of Grep operator"), + Path: mustBe("DATA"), + Summary: mustBe("usage: Grep(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must take only one non-variadic argument"), + }, + "filter:", filter) + } + + for _, filter := range []any{ + func(a int) {}, + func(a int) int { return 0 }, + func(a int) (bool, bool) { return true, true }, + } { + checkError(t, "never tested", + td.Grep(filter, 42), + expectedError{ + Message: mustBe("bad usage of Grep operator"), + Path: mustBe("DATA"), + Summary: mustBe("usage: Grep(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must return bool"), + }, + "filter:", filter) + } + + checkError(t, "never tested", td.Grep(td.Ignore(), 42), + expectedError{ + Message: mustBe("bad usage of Grep operator"), + Path: mustBe("DATA"), + Summary: mustBe("usage: Grep(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), EXPECTED_VALUE must be a slice not a int"), + }) + + checkError(t, &struct{}{}, td.Grep(td.Ignore(), []int{33}), + expectedError{ + Message: mustBe("bad kind"), + Path: mustBe("DATA"), + Got: mustBe("*struct (*struct {} type)"), + Expected: mustBe("slice OR array OR *slice OR *array"), + }) + + checkError(t, nil, td.Grep(td.Ignore(), []int{33}), + expectedError{ + Message: mustBe("bad kind"), + Path: mustBe("DATA"), + Got: mustBe("nil"), + Expected: mustBe("slice OR array OR *slice OR *array"), + }) + }) +} + +func TestGrepTypeBehind(t *testing.T) { + equalTypes(t, td.Grep(func(n int) bool { return true }, []int{33}), []int{}) + equalTypes(t, td.Grep(td.Gt("0"), []string{"33"}), []string{}) + + // Erroneous op + equalTypes(t, td.Grep(42, 33), nil) +} + +func TestGrepString(t *testing.T) { + test.EqualStr(t, + td.Grep(func(n int) bool { return true }, []int{}).String(), + "Grep(func(int) bool)") + + test.EqualStr(t, td.Grep(td.Gt(0), []int{}).String(), "Grep(> 0)") + + // Erroneous op + test.EqualStr(t, td.Grep(42, []int{}).String(), "Grep()") +} + +func TestFirst(t *testing.T) { + t.Run("basic", func(t *testing.T) { + got := [...]int{-3, -2, -1, 0, 1, 2, 3} + sgot := got[:] + + testCases := []struct { + name string + got any + }{ + {"slice", sgot}, + {"array", got}, + {"*slice", &sgot}, + {"*array", &got}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + checkOK(t, tc.got, td.First(td.Gt(0), 1)) + checkOK(t, tc.got, td.First(td.Not(td.Between(-3, 2)), 3)) + + checkOK(t, tc.got, td.First( + func(x int) bool { return (x & 1) == 0 }, + -2)) + + checkOK(t, tc.got, td.First( + func(x int64) bool { return (x & 1) != 0 }, + -3), + "int64 filter vs int items") + + checkOK(t, tc.got, td.First( + func(x any) bool { return (x.(int) & 1) == 0 }, + -2), + "any filter vs int items") + + checkError(t, tc.got, + td.First(td.Gt(666), "never reached"), + expectedError{ + Message: mustBe("item not found"), + Path: mustBe("DATA"), + Got: mustContain(`]int) (len=7 `), + Expected: mustBe("First(> 666)"), + }) + }) + } + }) + + t.Run("struct", func(t *testing.T) { + type person struct { + ID int64 + Name string + } + got := [...]person{ + {ID: 1, Name: "Joe"}, + {ID: 2, Name: "Bob"}, + {ID: 3, Name: "Alice"}, + {ID: 4, Name: "Brian"}, + {ID: 5, Name: "Britt"}, + } + sgot := got[:] + + testCases := []struct { + name string + got any + }{ + {"slice", sgot}, + {"array", got}, + {"*slice", &sgot}, + {"*array", &got}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + checkOK(t, tc.got, td.First( + td.JSONPointer("/Name", td.HasPrefix("Br")), + person{ID: 4, Name: "Brian"})) + + checkOK(t, tc.got, td.First( + func(p person) bool { return p.ID < 3 }, + person{ID: 1, Name: "Joe"})) + }) + } + }) + + t.Run("interfaces", func(t *testing.T) { + got := [...]any{-3, -2, -1, 0, 1, 2, 3} + sgot := got[:] + + testCases := []struct { + name string + got any + }{ + {"slice", sgot}, + {"array", got}, + {"*slice", &sgot}, + {"*array", &got}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + checkOK(t, tc.got, td.First(td.Gt(0), 1)) + checkOK(t, tc.got, td.First(td.Not(td.Between(-3, 2)), 3)) + + checkOK(t, tc.got, td.First( + func(x int) bool { return (x & 1) == 0 }, + -2)) + + checkOK(t, tc.got, td.First( + func(x int64) bool { return (x & 1) != 0 }, + -3), + "int64 filter vs any/int items") + + checkOK(t, tc.got, td.First( + func(x any) bool { return (x.(int) & 1) == 0 }, + -2), + "any filter vs any/int items") + }) + } + }) + + t.Run("interfaces error", func(t *testing.T) { + got := [...]any{123, "foo"} + sgot := got[:] + + testCases := []struct { + name string + got any + }{ + {"slice", sgot}, + {"array", got}, + {"*slice", &sgot}, + {"*array", &got}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + checkError(t, tc.got, + td.First(func(x int) bool { return false }, "never reached"), + expectedError{ + Message: mustBe("incompatible parameter type"), + Path: mustBe("DATA[1]"), + Got: mustBe("string"), + Expected: mustBe("int"), + }) + }) + } + }) + + t.Run("nil slice", func(t *testing.T) { + var got []int + testCases := []struct { + name string + got any + }{ + {"slice", got}, + {"*slice", &got}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + checkError(t, tc.got, + td.First(td.Gt(666), "never reached"), + expectedError{ + Message: mustBe("item not found"), + Path: mustBe("DATA"), + Got: mustBe("([]int) "), + Expected: mustBe("First(> 666)"), + }) + }) + } + }) + + t.Run("nil pointer", func(t *testing.T) { + checkError(t, (*[]int)(nil), td.First(td.Ignore(), 33), + expectedError{ + Message: mustBe("nil pointer"), + Path: mustBe("DATA"), + Got: mustBe("nil *slice (*[]int type)"), + Expected: mustBe("non-nil *slice OR *array"), + }) + }) + + t.Run("JSON", func(t *testing.T) { + got := map[string]any{ + "values": []int{1, 2, 3, 4}, + } + checkOK(t, got, td.JSON(`{"values": First(Gt(2), 3)}`)) + }) + + t.Run("errors", func(t *testing.T) { + for _, filter := range []any{nil, 33} { + checkError(t, "never tested", + td.First(filter, 42), + expectedError{ + Message: mustBe("bad usage of First operator"), + Path: mustBe("DATA"), + Summary: mustBe("usage: First(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must be a function or FILTER_TESTDEEP_OPERATOR a TestDeep operator"), + }, + "filter:", filter) + } + + for _, filter := range []any{ + func() bool { return true }, + func(a, b int) bool { return true }, + func(a ...int) bool { return true }, + } { + checkError(t, "never tested", + td.First(filter, 42), + expectedError{ + Message: mustBe("bad usage of First operator"), + Path: mustBe("DATA"), + Summary: mustBe("usage: First(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must take only one non-variadic argument"), + }, + "filter:", filter) + } + + for _, filter := range []any{ + func(a int) {}, + func(a int) int { return 0 }, + func(a int) (bool, bool) { return true, true }, + } { + checkError(t, "never tested", + td.First(filter, 42), + expectedError{ + Message: mustBe("bad usage of First operator"), + Path: mustBe("DATA"), + Summary: mustBe("usage: First(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must return bool"), + }, + "filter:", filter) + } + + checkError(t, &struct{}{}, td.First(td.Ignore(), 33), + expectedError{ + Message: mustBe("bad kind"), + Path: mustBe("DATA"), + Got: mustBe("*struct (*struct {} type)"), + Expected: mustBe("slice OR array OR *slice OR *array"), + }) + + checkError(t, nil, td.First(td.Ignore(), 33), + expectedError{ + Message: mustBe("bad kind"), + Path: mustBe("DATA"), + Got: mustBe("nil"), + Expected: mustBe("slice OR array OR *slice OR *array"), + }) + }) +} + +func TestFirstString(t *testing.T) { + test.EqualStr(t, + td.First(func(n int) bool { return true }, 33).String(), + "First(func(int) bool)") + + test.EqualStr(t, td.First(td.Gt(0), 33).String(), "First(> 0)") + + // Erroneous op + test.EqualStr(t, td.First(42, 33).String(), "First()") +} + +func TestFirstTypeBehind(t *testing.T) { + equalTypes(t, td.First(func(n int) bool { return true }, 33), []int{}) + equalTypes(t, td.First(td.Gt("x"), "x"), []string{}) + + // Erroneous op + equalTypes(t, td.First(42, 33), nil) +} + +func TestLast(t *testing.T) { + t.Run("basic", func(t *testing.T) { + got := [...]int{-3, -2, -1, 0, 1, 2, 3} + sgot := got[:] + + testCases := []struct { + name string + got any + }{ + {"slice", sgot}, + {"array", got}, + {"*slice", &sgot}, + {"*array", &got}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + checkOK(t, tc.got, td.Last(td.Lt(0), -1)) + checkOK(t, tc.got, td.Last(td.Not(td.Between(1, 3)), 0)) + + checkOK(t, tc.got, td.Last( + func(x int) bool { return (x & 1) == 0 }, + 2)) + + checkOK(t, tc.got, td.Last( + func(x int64) bool { return (x & 1) != 0 }, + 3), + "int64 filter vs int items") + + checkOK(t, tc.got, td.Last( + func(x any) bool { return (x.(int) & 1) == 0 }, + 2), + "any filter vs int items") + + checkError(t, tc.got, + td.Last(td.Gt(666), "never reached"), + expectedError{ + Message: mustBe("item not found"), + Path: mustBe("DATA"), + Got: mustContain(`]int) (len=7 `), + Expected: mustBe("Last(> 666)"), + }) + }) + } + }) + + t.Run("struct", func(t *testing.T) { + type person struct { + ID int64 + Name string + } + got := [...]person{ + {ID: 1, Name: "Joe"}, + {ID: 2, Name: "Bob"}, + {ID: 3, Name: "Alice"}, + {ID: 4, Name: "Brian"}, + {ID: 5, Name: "Britt"}, + } + sgot := got[:] + + testCases := []struct { + name string + got any + }{ + {"slice", sgot}, + {"array", got}, + {"*slice", &sgot}, + {"*array", &got}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + checkOK(t, tc.got, td.Last( + td.JSONPointer("/Name", td.HasPrefix("Br")), + person{ID: 5, Name: "Britt"})) + + checkOK(t, tc.got, td.Last( + func(p person) bool { return p.ID < 3 }, + person{ID: 2, Name: "Bob"})) + }) + } + }) + + t.Run("interfaces", func(t *testing.T) { + got := [...]any{-3, -2, -1, 0, 1, 2, 3} + sgot := got[:] + + testCases := []struct { + name string + got any + }{ + {"slice", sgot}, + {"array", got}, + {"*slice", &sgot}, + {"*array", &got}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + checkOK(t, tc.got, td.Last(td.Lt(0), -1)) + checkOK(t, tc.got, td.Last(td.Not(td.Between(1, 3)), 0)) + + checkOK(t, tc.got, td.Last( + func(x int) bool { return (x & 1) == 0 }, + 2)) + + checkOK(t, tc.got, td.Last( + func(x int64) bool { return (x & 1) != 0 }, + 3), + "int64 filter vs any/int items") + + checkOK(t, tc.got, td.Last( + func(x any) bool { return (x.(int) & 1) == 0 }, + 2), + "any filter vs any/int items") + }) + } + }) + + t.Run("interfaces error", func(t *testing.T) { + got := [...]any{123, "foo", 456} + sgot := got[:] + + testCases := []struct { + name string + got any + }{ + {"slice", sgot}, + {"array", got}, + {"*slice", &sgot}, + {"*array", &got}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + checkError(t, tc.got, + td.Last(func(x int) bool { return false }, "never reached"), + expectedError{ + Message: mustBe("incompatible parameter type"), + Path: mustBe("DATA[1]"), + Got: mustBe("string"), + Expected: mustBe("int"), + }) + }) + } + }) + + t.Run("nil slice", func(t *testing.T) { + var got []int + testCases := []struct { + name string + got any + }{ + {"slice", got}, + {"*slice", &got}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + checkError(t, tc.got, + td.Last(td.Gt(666), "never reached"), + expectedError{ + Message: mustBe("item not found"), + Path: mustBe("DATA"), + Got: mustBe("([]int) "), + Expected: mustBe("Last(> 666)"), + }) + }) + } + }) + + t.Run("nil pointer", func(t *testing.T) { + checkError(t, (*[]int)(nil), td.Last(td.Ignore(), 33), + expectedError{ + Message: mustBe("nil pointer"), + Path: mustBe("DATA"), + Got: mustBe("nil *slice (*[]int type)"), + Expected: mustBe("non-nil *slice OR *array"), + }) + }) + + t.Run("JSON", func(t *testing.T) { + got := map[string]any{ + "values": []int{1, 2, 3, 4}, + } + checkOK(t, got, td.JSON(`{"values": Last(Lt(3), 2)}`)) + }) + + t.Run("errors", func(t *testing.T) { + for _, filter := range []any{nil, 33} { + checkError(t, "never tested", + td.Last(filter, 42), + expectedError{ + Message: mustBe("bad usage of Last operator"), + Path: mustBe("DATA"), + Summary: mustBe("usage: Last(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must be a function or FILTER_TESTDEEP_OPERATOR a TestDeep operator"), + }, + "filter:", filter) + } + + for _, filter := range []any{ + func() bool { return true }, + func(a, b int) bool { return true }, + func(a ...int) bool { return true }, + } { + checkError(t, "never tested", + td.Last(filter, 42), + expectedError{ + Message: mustBe("bad usage of Last operator"), + Path: mustBe("DATA"), + Summary: mustBe("usage: Last(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must take only one non-variadic argument"), + }, + "filter:", filter) + } + + for _, filter := range []any{ + func(a int) {}, + func(a int) int { return 0 }, + func(a int) (bool, bool) { return true, true }, + } { + checkError(t, "never tested", + td.Last(filter, 42), + expectedError{ + Message: mustBe("bad usage of Last operator"), + Path: mustBe("DATA"), + Summary: mustBe("usage: Last(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must return bool"), + }, + "filter:", filter) + } + + checkError(t, &struct{}{}, td.Last(td.Ignore(), 33), + expectedError{ + Message: mustBe("bad kind"), + Path: mustBe("DATA"), + Got: mustBe("*struct (*struct {} type)"), + Expected: mustBe("slice OR array OR *slice OR *array"), + }) + + checkError(t, nil, td.Last(td.Ignore(), 33), + expectedError{ + Message: mustBe("bad kind"), + Path: mustBe("DATA"), + Got: mustBe("nil"), + Expected: mustBe("slice OR array OR *slice OR *array"), + }) + }) +} + +func TestLastString(t *testing.T) { + test.EqualStr(t, + td.Last(func(n int) bool { return true }, 33).String(), + "Last(func(int) bool)") + + test.EqualStr(t, td.Last(td.Gt(0), 33).String(), "Last(> 0)") + + // Erroneous op + test.EqualStr(t, td.Last(42, 33).String(), "Last()") +} + +func TestLastTypeBehind(t *testing.T) { + equalTypes(t, td.Last(func(n int) bool { return true }, 33), []int{}) + equalTypes(t, td.Last(td.Gt("x"), "x"), []string{}) + + // Erroneous op + equalTypes(t, td.Last(42, 33), nil) +} diff --git a/td/td_json.go b/td/td_json.go index 6174d3d2..9c4eb8df 100644 --- a/td/td_json.go +++ b/td/td_json.go @@ -1,4 +1,4 @@ -// Copyright (c) 2019-2021, Maxime Soulé +// Copyright (c) 2019-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the @@ -611,14 +611,15 @@ func jsonify(ctx ctxerr.Context, got reflect.Value) (any, *ctxerr.Error) { // - the optional 3rd parameter of [Between] has to be specified as a string // and can be: "[]" or "BoundsInIn" (default), "[[" or "BoundsInOut", // "]]" or "BoundsOutIn", "][" or "BoundsOutOut"; -// - not all operators are embeddable only the following are: -// [All], [Any], [ArrayEach], [Bag], [Between], [Contains], -// [ContainsKey], [Empty], [Gt], [Gte], [HasPrefix], [HasSuffix], -// [Ignore], [JSONPointer], [Keys], [Len], [Lt], [Lte], [MapEach], -// [N], [NaN], [Nil], [None], [Not], [NotAny], [NotEmpty], [NotNaN], -// [NotNil], [NotZero], [Re], [ReAll], [Set], [SubBagOf], -// [SubMapOf], [SubSetOf], [SuperBagOf], [SuperMapOf], [SuperSetOf], -// [Values] and [Zero]. +// - not all operators are embeddable only the following are: [All], +// [Any], [ArrayEach], [Bag], [Between], [Contains], +// [ContainsKey], [Empty], [First], [Grep], [Gt], [Gte], +// [HasPrefix], [HasSuffix], [Ignore], [JSONPointer], [Keys], +// [Last], [Len], [Lt], [Lte], [MapEach], [N], [NaN], [Nil], +// [None], [Not], [NotAny], [NotEmpty], [NotNaN], [NotNil], +// [NotZero], [Re], [ReAll], [Set], [SubBagOf], [SubMapOf], +// [SubSetOf], [SuperBagOf], [SuperMapOf], [SuperSetOf], [Values] +// and [Zero]. // // Operators taking no parameters can also be directly embedded in // JSON data using $^OperatorName or "$^OperatorName" notation. They @@ -880,14 +881,15 @@ var _ TestDeep = &tdMapJSON{} // - the optional 3rd parameter of [Between] has to be specified as a string // and can be: "[]" or "BoundsInIn" (default), "[[" or "BoundsInOut", // "]]" or "BoundsOutIn", "][" or "BoundsOutOut"; -// - not all operators are embeddable only the following are: -// [All], [Any], [ArrayEach], [Bag], [Between], [Contains], -// [ContainsKey], [Empty], [Gt], [Gte], [HasPrefix], [HasSuffix], -// [Ignore], [JSONPointer], [Keys], [Len], [Lt], [Lte], [MapEach], -// [N], [NaN], [Nil], [None], [Not], [NotAny], [NotEmpty], [NotNaN], -// [NotNil], [NotZero], [Re], [ReAll], [Set], [SubBagOf], -// [SubMapOf], [SubSetOf], [SuperBagOf], [SuperMapOf], [SuperSetOf], -// [Values] and [Zero]. +// - not all operators are embeddable only the following are: [All], +// [Any], [ArrayEach], [Bag], [Between], [Contains], +// [ContainsKey], [Empty], [First], [Grep], [Gt], [Gte], +// [HasPrefix], [HasSuffix], [Ignore], [JSONPointer], [Keys], +// [Last], [Len], [Lt], [Lte], [MapEach], [N], [NaN], [Nil], +// [None], [Not], [NotAny], [NotEmpty], [NotNaN], [NotNil], +// [NotZero], [Re], [ReAll], [Set], [SubBagOf], [SubMapOf], +// [SubSetOf], [SuperBagOf], [SuperMapOf], [SuperSetOf], [Values] +// and [Zero]. // // Operators taking no parameters can also be directly embedded in // JSON data using $^OperatorName or "$^OperatorName" notation. They @@ -1102,14 +1104,15 @@ func SubJSONOf(expectedJSON any, params ...any) TestDeep { // - the optional 3rd parameter of [Between] has to be specified as a string // and can be: "[]" or "BoundsInIn" (default), "[[" or "BoundsInOut", // "]]" or "BoundsOutIn", "][" or "BoundsOutOut"; -// - not all operators are embeddable only the following are: -// [All], [Any], [ArrayEach], [Bag], [Between], [Contains], -// [ContainsKey], [Empty], [Gt], [Gte], [HasPrefix], [HasSuffix], -// [Ignore], [JSONPointer], [Keys], [Len], [Lt], [Lte], [MapEach], -// [N], [NaN], [Nil], [None], [Not], [NotAny], [NotEmpty], [NotNaN], -// [NotNil], [NotZero], [Re], [ReAll], [Set], [SubBagOf], -// [SubMapOf], [SubSetOf], [SuperBagOf], [SuperMapOf], [SuperSetOf], -// [Values] and [Zero]. +// - not all operators are embeddable only the following are: [All], +// [Any], [ArrayEach], [Bag], [Between], [Contains], +// [ContainsKey], [Empty], [First], [Grep], [Gt], [Gte], +// [HasPrefix], [HasSuffix], [Ignore], [JSONPointer], [Keys], +// [Last], [Len], [Lt], [Lte], [MapEach], [N], [NaN], [Nil], +// [None], [Not], [NotAny], [NotEmpty], [NotNaN], [NotNil], +// [NotZero], [Re], [ReAll], [Set], [SubBagOf], [SubMapOf], +// [SubSetOf], [SuperBagOf], [SuperMapOf], [SuperSetOf], [Values] +// and [Zero]. // // Operators taking no parameters can also be directly embedded in // JSON data using $^OperatorName or "$^OperatorName" notation. They