json value operator
%%
json: value
{
$$ = $1
finalize(yylex, $$)
}
value:
object { $$ = $1 }
| array { $$ = $1 }
| operator { $$ = $1 }
| SUB_PARSER { $$ = $1 }
| STRING { $$ = $1 }
| NUMBER
| TRUE
| FALSE
| NULL
| PLACEHOLDER
;
object: '{' '}'
{
$$ = map[string]any{}
}
| '{' members '}'
{
$$ = $2
}
| '{' members ',' '}' // not JSON spec but useful
{
$$ = $2
}
members: member
{
$$ = map[string]any{
$1.key: $1.value,
}
}
| members ',' member
{
$1[$3.key] = $3.value
$$ = $1
}
member: STRING ':' value
{
$$ = member{
key: $1,
value: $3,
}
}
array: '[' ']'
{
$$ = []any{}
}
| '[' elements ']'
{
$$ = $2
}
| '[' elements ',' ']' // not JSON spec but useful
{
$$ = $2
}
elements: value
{
$$ = []any{$1}
}
| elements ',' value
{
$$ = append($1, $3)
}
op_params: '(' ')'
{
$$ = []any{}
}
| '(' elements ')'
{
$$ = $2
}
| '(' elements ',' ')'
{
$$ = $2
}
operator:
OPERATOR op_params
{
op := yylex.(*json).newOperator($1, $2)
if op == nil {
return 1
}
$$ = op
}
| OPERATOR
{
op := yylex.(*json).newOperator($1, nil)
if op == nil {
return 1
}
$$ = op
}
%%
go-testdeep-1.15.0/internal/json/parser_test.go 0000664 0000000 0000000 00000044373 15144170453 0021504 0 ustar 00root root 0000000 0000000 // Copyright (c) 2020-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 json_test
import (
ejson "encoding/json"
"fmt"
"reflect"
"strings"
"testing"
"github.com/maxatome/go-testdeep/internal/json"
"github.com/maxatome/go-testdeep/internal/spew"
"github.com/maxatome/go-testdeep/internal/test"
)
func checkJSON(t *testing.T, gotJSON, expectedJSON string) {
t.Helper()
var expected any
err := ejson.Unmarshal([]byte(expectedJSON), &expected)
if err != nil {
t.Fatalf("bad JSON: %s", err)
}
got, err := json.Parse([]byte(gotJSON))
if !test.NoError(t, err, "json.Parse succeeds") {
return
}
if !reflect.DeepEqual(got, expected) {
test.EqualErrorMessage(t,
spew.Sdump(got),
spew.Sdump(expected),
"got matches expected",
)
}
}
func TestJSON(t *testing.T) {
t.Run("Basics", func(t *testing.T) {
for i, js := range []string{
`true`,
` true `,
"\t\nfalse \n ",
` null `,
`{}`,
`[]`,
` 123.456 `,
` 123.456e4 `,
` 123.456E-4 `,
` -123e-4 `,
`0`,
`""`,
`"123.456$"`,
` "foo bar \" \\ \/ \b \f \n\r \t \u20ac \u10e6 \u10E6 héhô" `,
`"\""`,
`"\\"`,
`"\/"`,
`"\b"`,
`"\f"`,
`"\n"`,
`"\r"`,
`"\t"`,
`"\u20ac"`,
`"zz\""`,
`"zz\\"`,
`"zz\/"`,
`"zz\b"`,
`"zz\f"`,
`"zz\n"`,
`"zz\r"`,
`"zz\t"`,
`"zz\u20ac"`,
`["74.99 \u20ac"]`,
`{"text": "74.99 \u20ac"}`,
`[ 1, 2,3, 4 ]`,
`{"foo":{"bar":true},"zip":1234}`,
} {
js := []byte(js)
var expected any
err := ejson.Unmarshal(js, &expected)
if err != nil {
t.Fatalf("#%d, bad JSON: %s", i, err)
}
got, err := json.Parse(js)
if !test.NoError(t, err, "#%d, json.Parse succeeds", i) {
continue
}
if !reflect.DeepEqual(got, expected) {
test.EqualErrorMessage(t,
spew.Sdump(got),
spew.Sdump(expected),
"#%d is OK", i,
)
}
}
})
t.Run("JSON spec infringements", func(t *testing.T) {
for _, tc := range []struct{ got, expected string }{
// "," is accepted just before non-empty "}" or "]"
{`{"foo": "bar", }`, `{"foo":"bar"}`},
{`{"foo":"bar",}`, `{"foo":"bar"}`},
{`[ 1, 2, 3, ]`, `[1,2,3]`},
{`[ 1,2,3,]`, `[1,2,3]`},
// No need to escape \n, \r & \t
{"\"\n\r\t\"", `"\n\r\t"`},
// Extend to golang accepted numbers
// as int64
{`+42`, `42`},
{`0600`, `384`},
{`-0600`, `-384`},
{`+0600`, `384`},
{`0xBadFace`, `195951310`},
{`-0xBadFace`, `-195951310`},
{`+0xBadFace`, `195951310`},
// as float64
{`0600.123`, `600.123`}, // float64 can not be an octal number
{`0600.`, `600`}, // float64 can not be an octal number
{`.25`, `0.25`},
{`+123.`, `123`},
// Extend to golang 1.13 accepted numbers
// as int64
{`4_2`, `42`},
{`+4_2`, `42`},
{`-4_2`, `-42`},
{`0b101010`, `42`},
{`-0b101010`, `-42`},
{`+0b101010`, `42`},
{`0b10_1010`, `42`},
{`-0b_10_1010`, `-42`},
{`+0b10_10_10`, `42`},
{`0B101010`, `42`},
{`-0B101010`, `-42`},
{`+0B101010`, `42`},
{`0B10_1010`, `42`},
{`-0B_10_1010`, `-42`},
{`+0B10_10_10`, `42`},
{`0_600`, `384`},
{`-0_600`, `-384`},
{`+0_600`, `384`},
{`0o600`, `384`},
{`0o_600`, `384`},
{`-0o600`, `-384`},
{`-0o6_00`, `-384`},
{`+0o600`, `384`},
{`+0o60_0`, `384`},
{`0O600`, `384`},
{`0O_600`, `384`},
{`-0O600`, `-384`},
{`-0O6_00`, `-384`},
{`+0O600`, `384`},
{`+0O60_0`, `384`},
{`0xBad_Face`, `195951310`},
{`-0x_Bad_Face`, `-195951310`},
{`+0xBad_Face`, `195951310`},
{`0XBad_Face`, `195951310`},
{`-0X_Bad_Face`, `-195951310`},
{`+0XBad_Face`, `195951310`},
// as float64
{`0_600.123`, `600.123`}, // float64 can not be an octal number
{`1_5.`, `15`},
{`0.15e+0_2`, `15`},
{`0x1p-2`, `0.25`},
{`0x2.p10`, `2048`},
{`0x1.Fp+0`, `1.9375`},
{`0X.8p-0`, `0.5`},
{`0X_1FFFP-16`, `0.1249847412109375`},
// Raw strings
{`r"pipo"`, `"pipo"`},
{`r "pipo"`, `"pipo"`},
{"r\n'pipo'", `"pipo"`},
{`r%pipo%`, `"pipo"`},
{`r·pipo·`, `"pipo"`},
{"r`pipo`", `"pipo"`},
{`r/pipo/`, `"pipo"`},
{"r //comment\n`pipo`", `"pipo"`}, // comments accepted bw r and string
{"r//comment\n`pipo`", `"pipo"`},
{"r/*comment\n*/|pipo|", `"pipo"`},
{"r(p\ni\rp\to)", `"p\ni\rp\to"`}, // accepted raw whitespaces
{`r@pi\po\@`, `"pi\\po\\"`}, // backslash has no meaning
// balanced delimiters
{`r(p(i(hey)p)o)`, `"p(i(hey)p)o"`},
{`r{p{i{hey}p}o}`, `"p{i{hey}p}o"`},
{`r[p[i[hey]p]o]`, `"p[i[hey]p]o"`},
{`rp>o>`, `"pp>o"`},
{`r(pipo)`, `"pipo"`},
{"r \t\n(pipo)", `"pipo"`},
{`r{pipo}`, `"pipo"`},
{`r[pipo]`, `"pipo"`},
{`r`, `"pipo"`},
// Not balanced
{`r)pipo)`, `"pipo"`},
{`r}pipo}`, `"pipo"`},
{`r]pipo]`, `"pipo"`},
{`r>pipo>`, `"pipo"`},
} {
t.Run(tc.got, func(t *testing.T) {
checkJSON(t, tc.got, tc.expected)
})
}
})
t.Run("Special string cases", func(t *testing.T) {
for i, tst := range []struct{ in, expected string }{
{
in: `"$"`,
expected: `$`,
},
{
in: `"$$"`,
expected: `$`,
},
{
in: `"$$toto"`,
expected: `$toto`,
},
} {
got, err := json.Parse([]byte(tst.in))
if !test.NoError(t, err, "#%d, json.Parse succeeds", i) {
continue
}
if !reflect.DeepEqual(got, tst.expected) {
test.EqualErrorMessage(t,
spew.Sdump(got),
spew.Sdump(tst.expected),
"#%d is OK", i,
)
}
}
})
t.Run("Placeholder cases", func(t *testing.T) {
for i, js := range []string{
` $2 `,
` "$2" `,
` $ph `,
` "$ph" `,
` $héhé `,
` "$héhé" `,
} {
got, err := json.Parse([]byte(js), json.ParseOpts{
Placeholders: []any{"foo", "bar"},
PlaceholdersByName: map[string]any{
"ph": "bar",
"héhé": "bar",
},
})
if !test.NoError(t, err, "#%d, json.Parse succeeds", i) {
continue
}
if !reflect.DeepEqual(got, `bar`) {
test.EqualErrorMessage(t,
spew.Sdump(got),
spew.Sdump(`bar`),
"#%d is OK", i,
)
}
}
})
t.Run("Comments", func(t *testing.T) {
for i, js := range []string{
" // comment\ntrue",
" true // comment\n ",
" true // comment\n",
" true // comment",
" /* comment\nmulti\nline */true",
" true /* comment\nmulti\nline */",
" true /* comment\nmulti\nline */ \t",
" true /* comment\nmulti\nline */ // comment",
"/**///\ntrue/**/",
} {
for j, s := range []string{
js,
strings.ReplaceAll(js, "\n", "\r"),
strings.ReplaceAll(js, "\n", "\r\n"),
} {
got, err := json.Parse([]byte(s))
if !test.NoError(t, err, "#%d/%d, json.Parse succeeds", i, j) {
continue
}
if !reflect.DeepEqual(got, true) {
test.EqualErrorMessage(t,
got, true,
"#%d/%d is OK", i, j,
)
}
}
}
})
t.Run("OK", func(t *testing.T) {
opts := json.ParseOpts{
OpFn: func(op json.Operator, pos json.Position) (any, error) {
if op.Name == "KnownOp" {
return "OK", nil
}
return nil, fmt.Errorf("hmm weird operator %q", op.Name)
},
}
for _, js := range []string{
`[ KnownOp ]`,
`[ KnownOp() ]`,
`[ $^KnownOp() ]`,
`[ $^KnownOp ]`,
`[ KnownOp($^KnownOp) ]`,
`[ KnownOp( $^KnownOp() ) ]`,
`[ $^KnownOp(KnownOp) ]`,
} {
_, err := json.Parse([]byte(js), opts)
test.NoError(t, err, "json.Parse OK", js)
}
})
t.Run("Reentrant parser", func(t *testing.T) {
opts := json.ParseOpts{
OpFn: func(op json.Operator, pos json.Position) (any, error) {
if op.Name == "KnownOp" {
return "OK", nil
}
return nil, fmt.Errorf("hmm weird operator %q", op.Name)
},
}
for _, js := range []string{
`[ "$^KnownOp(1, 2, 3)" ]`,
`[ "$^KnownOp(1, 2, 3) " ]`,
`[ "$^KnownOp(r<$^KnownOp(11, 12)>, 2, KnownOp(31, 32))" ]`,
} {
_, err := json.Parse([]byte(js), opts)
test.NoError(t, err, "json.Parse OK", js)
}
})
t.Run("Errors", func(t *testing.T) {
for i, tst := range []struct{ nam, js, err string }{
// comment
{
nam: "unterminated comment",
js: " \n /* unterminated",
err: "multi-lines comment not terminated at line 2:3 (pos 5)",
},
{
nam: "/ at EOF",
js: " \n /",
err: "syntax error: unexpected '/' at line 2:1 (pos 3)",
},
{
nam: "/toto",
js: " \n /toto",
err: "syntax error: unexpected '/' at line 2:1 (pos 3)",
},
// string
{
nam: "unterminated string+multi lines",
js: "/* multi\nline\ncomment */ \"...",
err: "unterminated string at line 3:11 (pos 25)",
},
{
nam: "unterminated string",
js: ` "unterminated\`,
err: "unterminated string at line 1:2 (pos 2)",
},
{
nam: "bad escape",
js: `"bad escape \a"`,
err: "invalid escape sequence at line 1:13 (pos 13)",
},
{
nam: `bad escape \u`,
js: `"bad échappe \u123t"`,
err: "invalid escape sequence at line 1:14 (pos 14)",
},
{
nam: "bad rune",
js: "\"bad rune \007\"",
err: "invalid character in string at line 1:10 (pos 10)",
},
// number
{
nam: "bad number",
js: " \n 123.345.45",
err: "invalid number at line 2:1 (pos 4)",
},
// dollar token
{
nam: "dollar at EOF",
js: " $",
err: "syntax error: unexpected '$' at line 1:2 (pos 2)",
},
{
nam: "dollar alone",
js: " $ ",
err: "syntax error: unexpected '$' at line 1:2 (pos 2)",
},
{
nam: "multi lines+dollar at EOF",
js: " \n 123.345$",
err: "syntax error: unexpected '$' at line 2:8 (pos 11)",
},
{
nam: "bad num placeholder",
js: ` $123a `,
err: "invalid numeric placeholder at line 1:2 (pos 2)",
},
{
nam: "bad num placeholder in string",
js: ` "$123a" `,
err: "invalid numeric placeholder at line 1:3 (pos 3)",
},
{
nam: "bad 0 placeholder",
js: ` $00 `,
err: `invalid numeric placeholder "$00", it should start at "$1" at line 1:2 (pos 2)`,
},
{
nam: "bad 0 placeholder in string",
js: ` "$00" `,
err: `invalid numeric placeholder "$00", it should start at "$1" at line 1:3 (pos 3)`,
},
{
nam: "placeholder/params mismatch",
js: ` $1 `,
err: `numeric placeholder "$1", but no params given at line 1:2 (pos 2)`,
},
{
nam: "placeholder in string/params mismatch",
js: `[ "$1", 1, 2 ] `,
err: `numeric placeholder "$1", but no params given at line 1:3 (pos 3)`,
},
{
nam: "invalid operator in string",
js: ` "$^UnknownAndBad>" `,
err: `invalid operator name "UnknownAndBad>" at line 1:4 (pos 4)`,
},
{
nam: "unknown operator close paren",
js: ` UnknownAndBad)`,
err: `unknown operator "UnknownAndBad" at line 1:1 (pos 1)`,
},
{
nam: "unknown operator close paren in string",
js: ` "$^UnknownAndBad)" `,
err: `unknown operator "UnknownAndBad" at line 1:4 (pos 4)`,
},
{
nam: "op and syntax error",
js: ` KnownOp)`,
err: `syntax error: unexpected ')' at line 1:8 (pos 8)`,
},
{
nam: "op in string and syntax error",
js: ` "$^KnownOp)" `,
err: `syntax error: unexpected ')' at line 1:11 (pos 11)`,
},
{
nam: "op paren in string and syntax error",
js: ` "$^KnownOp())" `,
err: `syntax error: unexpected ')' at line 1:13 (pos 13)`,
},
{
nam: "invalid $^",
js: ` $^. `,
err: `$^ must be followed by an operator name at line 1:2 (pos 2)`,
},
{
nam: "invalid $^ in string",
js: ` "$^."`,
err: `$^ must be followed by an operator name at line 1:2 (pos 2)`,
},
{
nam: "invalid $^ at EOF",
js: ` $^`,
err: `$^ must be followed by an operator name at line 1:2 (pos 2)`,
},
{
nam: "invalid $^ in string at EOF",
js: ` "$^"`,
err: `$^ must be followed by an operator name at line 1:2 (pos 2)`,
},
{
nam: "bad placeholder",
js: ` $tag%`,
err: `bad placeholder "$tag%" at line 1:2 (pos 2)`,
},
{
nam: "bad placeholder in string",
js: ` "$tag%"`,
err: `bad placeholder "$tag%" at line 1:3 (pos 3)`,
},
{
nam: "unknown placeholder",
js: ` $tag`,
err: `unknown placeholder "$tag" at line 1:2 (pos 2)`,
},
{
nam: "unknown placeholder in string",
js: ` "$tag"`,
err: `unknown placeholder "$tag" at line 1:3 (pos 3)`,
},
// operator
{
nam: "invalid operator",
js: " AnyOpé",
err: `invalid operator name "AnyOp\xc3" at line 1:2 (pos 2)`,
},
{
nam: "invalid $^operator",
js: " $^AnyOpé",
err: `invalid operator name "AnyOp\xc3" at line 1:4 (pos 4)`,
},
{
nam: "invalid $^operator in string",
js: ` "$^AnyOpé"`,
err: `invalid operator name "AnyOp\xc3" at line 1:5 (pos 5)`,
},
{
nam: "unknown operator",
js: " AnyOp",
err: `unknown operator "AnyOp" at line 1:2 (pos 2)`,
},
{
nam: "unknown operator paren",
js: " AnyOp()",
err: `unknown operator "AnyOp" at line 1:2 (pos 2)`,
},
{
nam: "unknown $^operator",
js: "$^AnyOp",
err: `unknown operator "AnyOp" at line 1:2 (pos 2)`,
},
{
nam: "unknown $^operator paren",
js: "$^AnyOp()",
err: `unknown operator "AnyOp" at line 1:2 (pos 2)`,
},
{
nam: "unknown $^operator in string",
js: `"$^AnyOp"`,
err: `unknown operator "AnyOp" at line 1:3 (pos 3)`,
},
{
nam: "unknown $^operator paren in string",
js: `"$^AnyOp()"`,
err: `unknown operator "AnyOp" at line 1:3 (pos 3)`,
},
{
nam: "unknown $^operator in rawstring",
js: `r<$^AnyOp>`,
err: `unknown operator "AnyOp" at line 1:4 (pos 4)`,
},
{
nam: "unknown $^operator paren in rawstring",
js: `r<$^AnyOp()>`,
err: `unknown operator "AnyOp" at line 1:4 (pos 4)`,
},
// syntax error
{
nam: "syntax error num+bool",
js: " \n 123.345true",
err: "syntax error: unexpected TRUE at line 2:8 (pos 11)",
},
{
nam: "syntax error num+%",
js: " \n 123.345%",
err: "syntax error: unexpected '%' at line 2:8 (pos 11)",
},
{
nam: "syntax error num+ESC",
js: " \n 123.345\x1f",
err: `syntax error: unexpected '\u001f' at line 2:8 (pos 11)`,
},
{
nam: "syntax error num+unicode",
js: " \n 123.345\U0002f500",
err: `syntax error: unexpected '\U0002f500' at line 2:8 (pos 11)`,
},
// multiple errors
{
nam: "multi errors placeholders",
js: "[$1,$2,",
err: `numeric placeholder "$1", but no params given at line 1:1 (pos 1)
numeric placeholder "$2", but no params given at line 1:4 (pos 4)
syntax error: unexpected EOF at line 1:6 (pos 6)`,
},
{
nam: "multi errors placeholder+operator",
js: `[$1,"$^Unknown1()","$^Unknown2()"]`,
err: `numeric placeholder "$1", but no params given at line 1:1 (pos 1)
invalid operator name "Unknown1" at line 1:7 (pos 7)
invalid operator name "Unknown2" at line 1:22 (pos 22)`,
},
// raw strings
{
nam: "rawstring start delimiter",
js: " \n r ",
err: `cannot find r start delimiter at line 2:7 (pos 10)`,
},
{
nam: "rawstring start delimiter EOF",
js: " \n r",
err: `cannot find r start delimiter at line 2:4 (pos 7)`,
},
{
nam: "rawstring bad delimiter",
js: ` rxpipox`,
err: `invalid r delimiter 'x', should be either a punctuation or a symbol rune, excluding '_' at line 1:3 (pos 3)`,
},
{
nam: "rawstring bad underscore delimiter",
js: ` r_pipo_`,
err: `invalid r delimiter '_', should be either a punctuation or a symbol rune, excluding '_' at line 1:3 (pos 3)`,
},
{
nam: "rawstring bad rune",
js: " r:bad rune \007:",
err: `invalid character in raw string at line 1:13 (pos 13)`,
},
{
nam: "unterminated rawstring",
js: ` r!pipo...`,
err: `unterminated raw string at line 1:3 (pos 3)`,
},
} {
t.Run(tst.nam, func(t *testing.T) {
opts := json.ParseOpts{
OpFn: func(op json.Operator, pos json.Position) (any, error) {
if op.Name == "KnownOp" {
return "OK", nil
}
return nil, fmt.Errorf("unknown operator %q", op.Name)
},
}
_, err := json.Parse([]byte(tst.js), opts)
if test.Error(t, err, `#%d \n, json.Parse fails`, i) {
test.EqualStr(t, err.Error(), tst.err, `#%d \n, err OK`, i)
}
_, err = json.Parse([]byte(strings.ReplaceAll(tst.js, "\n", "\r")), opts)
if test.Error(t, err, `#%d \r, json.Parse fails`, i) {
test.EqualStr(t, err.Error(), tst.err, `#%d \r, err OK`, i)
}
_, err = json.Parse([]byte(strings.ReplaceAll(tst.js, "\n", "\r\n")), opts)
if test.Error(t, err, `#%d \r\n, json.Parse fails`, i) {
test.EqualStr(t, err.Error(), tst.err, `#%d \r\n, err OK`, i)
}
})
}
_, err := json.Parse(
[]byte(`[$2]`),
json.ParseOpts{Placeholders: []any{1}},
)
if test.Error(t, err) {
test.EqualStr(t, err.Error(),
`numeric placeholder "$2", but only one param given at line 1:1 (pos 1)`)
}
_, err = json.Parse(
[]byte(`[$3]`),
json.ParseOpts{Placeholders: []any{1, 2}},
)
if test.Error(t, err) {
test.EqualStr(t, err.Error(),
`numeric placeholder "$3", but only 2 params given at line 1:1 (pos 1)`)
}
for _, js := range []string{
` KnownOp( AnyOp() )`,
` KnownOp( AnyOp )`,
` KnownOp("$^AnyOp()" )`,
` KnownOp("$^AnyOp" )`,
` KnownOp( $^AnyOp() )`,
` $^KnownOp( AnyOp )`,
` "$^KnownOp( AnyOp )"`,
` "$^KnownOp( AnyOp() )"`,
` "$^KnownOp( $^AnyOp() )"`,
`"$^KnownOp(r'$^AnyOp()')"`,
} {
t.Run(js, func(t *testing.T) {
var anyOpPos json.Position
_, err = json.Parse([]byte(js), json.ParseOpts{
OpFn: func(op json.Operator, pos json.Position) (any, error) {
if op.Name == "KnownOp" {
return "OK", nil
}
anyOpPos = pos
return nil, fmt.Errorf("hmm weird operator %q", op.Name)
},
})
if test.Error(t, err, "json.Parse fails") {
test.EqualInt(t, anyOpPos.Pos, 15)
test.EqualInt(t, anyOpPos.Line, 1)
test.EqualInt(t, anyOpPos.Col, 15)
test.EqualStr(t, err.Error(),
`hmm weird operator "AnyOp" at line 1:15 (pos 15)`)
}
})
}
})
t.Run("no operators", func(t *testing.T) {
_, err := json.Parse([]byte(" Operator"))
if test.Error(t, err, "json.Parse fails") {
test.EqualStr(t, err.Error(),
`unknown operator "Operator" at line 1:2 (pos 2)`)
}
})
}
go-testdeep-1.15.0/internal/location/ 0000775 0000000 0000000 00000000000 15144170453 0017446 5 ustar 00root root 0000000 0000000 go-testdeep-1.15.0/internal/location/location.go 0000664 0000000 0000000 00000003145 15144170453 0021610 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2021, 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 location
import (
"fmt"
"runtime"
"strings"
)
// Location records a place in a source file.
type Location struct {
File string // File name
Func string // Function name
Line int // Line number inside file
Inside string // Inside is used when Location is inside something else
BehindCmp bool // BehindCmp is true when operator is behind a Cmp* function
}
// GetLocationer is the interface that wraps the basic GetLocation method.
type GetLocationer interface {
GetLocation() Location
}
// New returns a new [Location]. callDepth is the number of
// stack frames to ascend to get the calling function (Func field),
// added to 1 to get the File & Line fields.
//
// If the location can not be determined, ok is false and location is
// not valid.
func New(callDepth int) (loc Location, ok bool) {
_, loc.File, loc.Line, ok = runtime.Caller(callDepth + 1)
if !ok {
return
}
if index := strings.LastIndexAny(loc.File, `/\`); index >= 0 {
loc.File = loc.File[index+1:]
}
pc, _, _, _ := runtime.Caller(callDepth)
loc.Func = runtime.FuncForPC(pc).Name()
return
}
// IsInitialized returns true if l is initialized
// (e.g. [NewLocation] called without an error), false otherwise.
func (l Location) IsInitialized() bool {
return l.File != ""
}
// Implements [fmt.Stringer].
func (l Location) String() string {
return fmt.Sprintf("%s %sat %s:%d", l.Func, l.Inside, l.File, l.Line)
}
go-testdeep-1.15.0/internal/location/location_test.go 0000664 0000000 0000000 00000003365 15144170453 0022653 0 ustar 00root root 0000000 0000000 package location
import (
"testing"
"github.com/maxatome/go-testdeep/internal/test"
)
func TestNew(t *testing.T) {
for _, curTest := range []struct {
callDepth int
expectOK bool
}{
{callDepth: 0, expectOK: true},
{callDepth: 1, expectOK: true},
{callDepth: 100, expectOK: false},
} {
loc, ok := New(curTest.callDepth)
test.EqualBool(t, ok, curTest.expectOK, "callDepth=%d", curTest.callDepth)
if ok {
test.IsTrue(t, loc.File != "", "File should not be empty for callDepth=%d", curTest.callDepth)
test.IsTrue(t, loc.Func != "", "Func should not be empty for callDepth=%d", curTest.callDepth)
test.IsTrue(t, loc.Line > 0, "Line should be >0 for callDepth=%d", curTest.callDepth)
}
}
}
func TestIsInitialized(t *testing.T) {
for _, curTest := range []struct {
loc Location
expected bool
}{
{loc: Location{}, expected: false},
{loc: Location{File: ""}, expected: false},
{loc: Location{File: "test.go"}, expected: true},
{loc: Location{File: "test.go", Func: "Test", Line: 10}, expected: true},
} {
test.EqualBool(t, curTest.loc.IsInitialized(), curTest.expected, "loc=%+v", curTest.loc)
}
}
func TestString(t *testing.T) {
for _, curTest := range []struct {
loc Location
expected string
}{
{
loc: Location{File: "test.go", Func: "TestFunc", Line: 10},
expected: "TestFunc at test.go:10",
},
{
loc: Location{File: "main.go", Func: "main", Line: 5, Inside: "inside something "},
expected: "main inside something at main.go:5",
},
{
loc: Location{File: "pkg/utils.go", Func: "utils.Helper", Line: 42, Inside: "in map "},
expected: "utils.Helper in map at pkg/utils.go:42",
},
} {
test.EqualStr(t, curTest.loc.String(), curTest.expected, "loc=%+v", curTest.loc)
}
}
go-testdeep-1.15.0/internal/or/ 0000775 0000000 0000000 00000000000 15144170453 0016256 5 ustar 00root root 0000000 0000000 go-testdeep-1.15.0/internal/or/any.go 0000664 0000000 0000000 00000000416 15144170453 0017375 0 ustar 00root root 0000000 0000000 // Copyright (c) 2025, 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 or
type any = interface{}
go-testdeep-1.15.0/internal/or/any_test.go 0000664 0000000 0000000 00000000423 15144170453 0020432 0 ustar 00root root 0000000 0000000 // Copyright (c) 2025, 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 or_test
type any = interface{}
go-testdeep-1.15.0/internal/or/panic.go 0000664 0000000 0000000 00000000303 15144170453 0017673 0 ustar 00root root 0000000 0000000 package or
import "fmt"
// Panic panics with panicArgs if test is false. It does nothing otherwise.
func Panic(test bool, panicArgs ...any) {
if !test {
panic(fmt.Sprint(panicArgs...))
}
}
go-testdeep-1.15.0/internal/or/panic_test.go 0000664 0000000 0000000 00000000503 15144170453 0020734 0 ustar 00root root 0000000 0000000 package or_test
import (
"reflect"
"testing"
"github.com/maxatome/go-testdeep/internal/or"
"github.com/maxatome/go-testdeep/internal/test"
)
func TestOrPanic(t *testing.T) {
test.CheckPanic(t,
func() { or.Panic(false, "big ", reflect.Uint16, " bang") },
"big uint16 bang")
or.Panic(true, "no panic at all")
}
go-testdeep-1.15.0/internal/sort/ 0000775 0000000 0000000 00000000000 15144170453 0016625 5 ustar 00root root 0000000 0000000 go-testdeep-1.15.0/internal/sort/map.go 0000664 0000000 0000000 00000002460 15144170453 0017733 0 ustar 00root root 0000000 0000000 // Copyright (c) 2025, 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 sort
import (
"reflect"
"sort"
"github.com/maxatome/go-testdeep/internal/compare"
"github.com/maxatome/go-testdeep/internal/visited"
)
type kv struct {
key reflect.Value
value reflect.Value
}
type kvSlice struct {
v visited.Visited
s []kv
}
func newKvSlice(l int) *kvSlice {
s := kvSlice{}
if l > 0 {
s.s = make([]kv, 0, l)
if l > 1 {
s.v = visited.NewVisited()
}
}
return &s
}
func (s *kvSlice) Len() int { return len(s.s) }
func (s *kvSlice) Less(i, j int) bool {
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] }
// MapEach calls fn for each key/value pair of map m using the order
// of keys. If fn returns false, it will not be called again.
// MapEach returns false if fn returned false.
func MapEach(m reflect.Value, fn func(k, v reflect.Value) bool) bool {
kvs := newKvSlice(m.Len())
iter := m.MapRange()
for iter.Next() {
kvs.s = append(kvs.s, kv{key: iter.Key(), value: iter.Value()})
}
sort.Sort(kvs)
for _, kv := range kvs.s {
if !fn(kv.key, kv.value) {
return false
}
}
return true
}
go-testdeep-1.15.0/internal/sort/map_private_test.go 0000664 0000000 0000000 00000002473 15144170453 0022530 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2025, 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 sort
import (
"reflect"
"sort"
"testing"
)
func TestKvSlice(t *testing.T) {
t.Run("len=0", func(t *testing.T) {
kvs := newKvSlice(0)
if kvs.s != nil || kvs.v != nil {
t.Errorf("newKvSlice failed: %v", *kvs)
}
sort.Sort(kvs)
})
t.Run("len=1", func(t *testing.T) {
kvs := newKvSlice(1)
if kvs.s == nil || kvs.v != nil {
t.Errorf("newKvSlice failed: %v", *kvs)
}
kvs.s = append(kvs.s, kv{
key: reflect.ValueOf("a"),
value: reflect.ValueOf(1),
})
sort.Sort(kvs)
})
t.Run("len>1", func(t *testing.T) {
kvs := newKvSlice(3)
if kvs.s == nil || kvs.v == nil {
t.Errorf("newKvSlice failed: %v", *kvs)
}
kvs.s = append(kvs.s,
kv{
key: reflect.ValueOf("b"),
value: reflect.ValueOf(2),
},
kv{
key: reflect.ValueOf("c"),
value: reflect.ValueOf(3),
},
kv{
key: reflect.ValueOf("a"),
value: reflect.ValueOf(1),
},
)
sort.Sort(kvs)
if kvs.s[0].key.String() != "a" ||
kvs.s[1].key.String() != "b" ||
kvs.s[2].key.String() != "c" {
t.Errorf("Sort failed: [%v, %v, %v]",
kvs.s[0].key, kvs.s[1].key, kvs.s[2].key)
}
})
}
go-testdeep-1.15.0/internal/sort/map_test.go 0000664 0000000 0000000 00000002434 15144170453 0020773 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2025, 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 sort_test
import (
"reflect"
"sort"
"testing"
tdsort "github.com/maxatome/go-testdeep/internal/sort"
)
func TestMapEach(t *testing.T) {
m := map[string]int{"a": 1, "b": 2, "c": 3}
t.Run("full", func(t *testing.T) {
type kv struct {
key string
value int
}
var s []kv
ok := tdsort.MapEach(reflect.ValueOf(m), func(k, v reflect.Value) bool {
s = append(s, kv{
key: k.Interface().(string),
value: v.Interface().(int),
})
return true
})
if !ok {
t.Error("MapEach returned false")
}
sort.Slice(s, func(i, j int) bool { return s[i].key < s[j].key })
if len(s) != 3 ||
s[0] != (kv{key: "a", value: 1}) ||
s[1] != (kv{key: "b", value: 2}) ||
s[2] != (kv{key: "c", value: 3}) {
t.Errorf("MapEach failed: %v", s)
}
})
t.Run("short circuit", func(t *testing.T) {
called := 0
ok := tdsort.MapEach(reflect.ValueOf(m), func(k, v reflect.Value) bool {
called++
return false
})
if ok {
t.Error("MapEach returned true")
}
if called != 1 {
t.Errorf("MapEach callback called %d times instead of 1", called)
}
})
}
go-testdeep-1.15.0/internal/sort/sort.go 0000664 0000000 0000000 00000004502 15144170453 0020144 0 ustar 00root root 0000000 0000000 // Copyright (c) 2025, 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 sort
import (
"reflect"
"sort"
"github.com/maxatome/go-testdeep/internal/compare"
"github.com/maxatome/go-testdeep/internal/visited"
)
// Values is used to allow the sorting of a [][reflect.Value]
// slice. It is used with the standard sort package:
//
// vals := []reflect.Value{a, b, c, d}
// sort.Sort(Values(vals))
// // vals contents now sorted
//
// Replace [sort.Sort] by [sort.Stable] for a stable sort. See [sort]
// 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, 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
// - functions, channels and unsafe pointer are sorted by their address
// - struct: comparison is spread to each field
// - pointer: comparison is spread to the pointed value
// - arrays: comparison is spread to each item
// - slice: comparison is spread to each item, then shorter length is lesser
// - interface: comparison is spread to the value
//
// Cyclic references are correctly handled.
//
// See also [Func].
func Values(s []reflect.Value) sort.Interface {
r := &rValues{
Slice: s,
}
if len(s) > 1 {
r.Visited = visited.NewVisited()
}
return r
}
type rValues struct {
Visited visited.Visited
Slice []reflect.Value
}
func (v *rValues) Len() int {
return len(v.Slice)
}
func (v *rValues) Less(i, j int) bool {
return compare.Compare(v.Visited, v.Slice[i], v.Slice[j]) < 0
}
func (v *rValues) Swap(i, j int) {
v.Slice[i], v.Slice[j] = v.Slice[j], v.Slice[i]
}
// Func returns a function able to compare 2 [reflect.Value] values.
//
// The sorting rules are listed in [Values] documentation.
//
// Cyclic references are correctly handled.
func Func() func(a, b reflect.Value) int {
var v visited.Visited
return func(a, b reflect.Value) int {
return compare.Compare(v, a, b)
}
}
go-testdeep-1.15.0/internal/sort/sort_121_test.go 0000664 0000000 0000000 00000001203 15144170453 0021561 0 ustar 00root root 0000000 0000000 // Copyright (c) 2025, 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.21
// +build go1.21
package sort_test
import (
"reflect"
"slices"
"testing"
"github.com/maxatome/go-testdeep/internal/sort"
)
func TestFunc(t *testing.T) {
s := []reflect.Value{
reflect.ValueOf(4),
reflect.ValueOf(3),
reflect.ValueOf(1),
}
slices.SortFunc(s, sort.Func())
if s[0].Int() != 1 || s[1].Int() != 3 || s[2].Int() != 4 {
t.Errorf("sort error: [ %v, %v, %v ]", s[0].Int(), s[1].Int(), s[2].Int())
}
}
go-testdeep-1.15.0/internal/sort/sort_test.go 0000664 0000000 0000000 00000001427 15144170453 0021206 0 ustar 00root root 0000000 0000000 // Copyright (c) 2025, 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 sort_test
import (
"reflect"
"sort"
"testing"
tdsort "github.com/maxatome/go-testdeep/internal/sort"
)
func TestValues(t *testing.T) {
s := []reflect.Value{
reflect.ValueOf(4),
reflect.ValueOf(3),
reflect.ValueOf(1),
}
sort.Sort(tdsort.Values(s))
if s[0].Int() != 1 || s[1].Int() != 3 || s[2].Int() != 4 {
t.Errorf("sort error: [ %v, %v, %v ]", s[0].Int(), s[1].Int(), s[2].Int())
}
s = []reflect.Value{
reflect.ValueOf(42),
}
sort.Sort(tdsort.Values(s))
if s[0].Int() != 42 {
t.Errorf("sort error: [ %v ]", s[0].Int())
}
sort.Sort(tdsort.Values(nil))
}
go-testdeep-1.15.0/internal/spew/ 0000775 0000000 0000000 00000000000 15144170453 0016614 5 ustar 00root root 0000000 0000000 go-testdeep-1.15.0/internal/spew/LICENSE 0000664 0000000 0000000 00000001376 15144170453 0017630 0 ustar 00root root 0000000 0000000 ISC License
Copyright (c) 2012-2016 Dave Collins
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
go-testdeep-1.15.0/internal/spew/README.md 0000664 0000000 0000000 00000007466 15144170453 0020110 0 ustar 00root root 0000000 0000000 # spew
Originally copied from
[github.com/davecgh/go-spew](https://github.com/davecgh/go-spew), it
has been patched to fit go-testdeep needs as the original repository
seems to have no activity anymore.
spew implements a deep pretty printer for Go data structures to aid
in debugging. A comprehensive suite of tests with 100% test coverage
is provided to ensure proper functionality. This directory and its
sub-directories are licensed under the [copyfree](http://copyfree.org)
[ISC License](internal/spew/LICENSE), so it may be used in open
source or commercial projects.
If you're interested in reading about how the original go-spew package
came to life and some of the challenges involved in providing a deep
pretty printer, there is a blog post about it
[here](https://web.archive.org/web/20160304013555/https://blog.cyphertite.com/go-spew-a-journey-into-dumping-go-data-structures/).
## Documentation
https://pkg.go.dev/github.com/maxatome/go-testdeep/internal/spew
## Quick Start
Add this import line to the file you're working in:
```go
import "github.com/maxatome/go-testdeep/internal/spew"
```
To dump a variable with full newlines, indentation, type and pointer
information use Sdump:
```go
str := spew.Sdump(myVar1)
```
## Sample Sdump Output
```
(main.Foo) {
unexportedField: (*main.Bar)(0xf84002e210)({
flag: (main.Flag) flagTwo,
data: (uintptr)
}),
ExportedField: (map[interface {}]interface {}) {
(string) "one": (bool) true
}
}
([]uint8) {
00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... |
00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0|
00000020 31 32 |12|
}
```
## Configuration Options
Configuration of spew is handled by fields in the ConfigState type. For
convenience, all of the top-level functions use a global state available via the
spew.Config global.
It is also possible to create a ConfigState instance that provides methods
equivalent to the top-level functions. This allows concurrent configuration
options. See the ConfigState documentation for more details.
```
* Indent
String to use for each indentation level for Sdump function.
It is a single space by default. A popular alternative is "\t".
* MaxDepth
Maximum number of levels to descend into nested data structures.
There is no limit by default.
* DisableMethods
Disables invocation of error and fmt.Stringer interface methods.
Method invocation is enabled by default.
* DisablePointerMethods
Disables invocation of error and fmt.Stringer interface methods on types
which only accept pointer receivers from non-pointer variables. This option
relies on access to the unsafe package, so it will not have any effect when
running in environments without access to the unsafe package such as Google
App Engine or with the "safe" build tag specified.
Pointer method invocation is enabled by default.
* DisablePointerAddresses
DisablePointerAddresses specifies whether to disable the printing of
pointer addresses. This is useful when diffing data structures in tests.
* EnableCapacities
EnableCapacities specifies whether to enable the printing of capacities
for arrays, slices and channels.
```
## Unsafe Package Dependency
This package relies on the unsafe package to perform some of the more advanced
features, however it also supports a "limited" mode which allows it to work in
environments where the unsafe package is not available. By default, it will
operate in this mode on Google App Engine and when compiled with GopherJS. The
"safe" build tag may also be specified to force the package to build without
using the unsafe package.
## License
This directory and its sub-directories are licensed under the
[copyfree](http://copyfree.org) [ISC License](internal/spew/LICENSE),
so it may be used in open source or commercial projects.
go-testdeep-1.15.0/internal/spew/any.go 0000664 0000000 0000000 00000000420 15144170453 0017726 0 ustar 00root root 0000000 0000000 // Copyright (c) 2025, 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 spew
type any = interface{}
go-testdeep-1.15.0/internal/spew/any_test.go 0000664 0000000 0000000 00000000425 15144170453 0020772 0 ustar 00root root 0000000 0000000 // Copyright (c) 2025, 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 spew_test
type any = interface{}
go-testdeep-1.15.0/internal/spew/bypass.go 0000664 0000000 0000000 00000010447 15144170453 0020452 0 ustar 00root root 0000000 0000000 // Copyright (c) 2015-2016 Dave Collins
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// NOTE: Due to the following build constraints, this file will only be compiled
// when the code is not running on Google App Engine, compiled by GopherJS, and
// "-tags safe" is not added to the go build command line. The "disableunsafe"
// tag is deprecated and thus should not be used.
//go:build !js && !appengine && !safe && !disableunsafe
// +build !js,!appengine,!safe,!disableunsafe
package spew
import (
"reflect"
"unsafe"
"github.com/maxatome/go-testdeep/internal/or"
)
// UnsafeDisabled is a build-time constant which specifies whether or
// not access to the unsafe package is available.
const UnsafeDisabled = false
type flag uintptr
var (
// flagRO indicates whether the value field of a reflect.Value
// is read-only.
flagRO flag
// flagAddr indicates whether the address of the reflect.Value's
// value may be taken.
flagAddr flag
)
// flagKindMask holds the bits that make up the kind
// part of the flags field. In all the supported versions,
// it is in the lower 5 bits.
const flagKindMask = flag(0x1f)
// Different versions of Go have used different bit layouts for the
// flags type. This layout is OK since go 1.6.
var okFlags = struct {
ro, addr flag
}{
ro: 1<<5 | 1<<6,
addr: 1 << 8,
}
var flagValOffset = func() uintptr {
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
or.Panic(ok, "reflect.Value has no flag field")
return field.Offset
}()
// flagField returns a pointer to the flag field of a reflect.Value.
func flagField(v *reflect.Value) *flag {
return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset))
}
// UnsafeReflectValue converts the passed reflect.Value into a one that bypasses
// the typical safety restrictions preventing access to unaddressable and
// unexported data. It works by digging the raw pointer to the underlying
// value out of the protected value and generating a new unprotected (unsafe)
// reflect.Value to it.
//
// This allows us to check for implementations of the fmt.Stringer and error
// interfaces to be used for pretty printing ordinarily unaddressable and
// inaccessible values such as unexported struct fields.
func UnsafeReflectValue(v reflect.Value) reflect.Value {
if v.IsValid() && (!v.CanInterface() || !v.CanAddr()) {
flagFieldPtr := flagField(&v)
*flagFieldPtr &^= flagRO
*flagFieldPtr |= flagAddr
}
return v
}
// Sanity checks against future reflect package changes
// to the type or semantics of the Value.flag field.
func init() {
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
or.Panic(ok, "reflect.Value has no flag field")
or.Panic(field.Type.Kind() == reflect.TypeOf(flag(0)).Kind(),
"reflect.Value flag field has changed kind")
type t0 int
var t struct {
A t0
// t0 will have flagEmbedRO set.
t0
// a will have flagStickyRO set
a t0
}
vA := reflect.ValueOf(t).FieldByName("A")
va := reflect.ValueOf(t).FieldByName("a")
vt0 := reflect.ValueOf(t).FieldByName("t0")
// Infer flagRO from the difference between the flags
// for the (otherwise identical) fields in t.
flagPublic := *flagField(&vA)
flagWithRO := *flagField(&va) | *flagField(&vt0)
flagRO = flagPublic ^ flagWithRO
// Infer flagAddr from the difference between a value
// taken from a pointer and not.
vPtrA := reflect.ValueOf(&t).Elem().FieldByName("A")
flagNoPtr := *flagField(&vA)
flagPtr := *flagField(&vPtrA)
flagAddr = flagNoPtr ^ flagPtr
// Check that the inferred flags tally with the known version
or.Panic(flagRO == okFlags.ro && flagAddr == okFlags.addr,
"reflect.Value read-only flag has changed semantics")
}
go-testdeep-1.15.0/internal/spew/bypasssafe.go 0000664 0000000 0000000 00000003373 15144170453 0021311 0 ustar 00root root 0000000 0000000 // Copyright (c) 2015-2016 Dave Collins
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// NOTE: Due to the following build constraints, this file will only be compiled
// when the code is running on Google App Engine, compiled by GopherJS, or
// "-tags safe" is added to the go build command line. The "disableunsafe"
// tag is deprecated and thus should not be used.
//go:build js || appengine || safe || disableunsafe
// +build js appengine safe disableunsafe
package spew
import "reflect"
const (
// UnsafeDisabled is a build-time constant which specifies whether or
// not access to the unsafe package is available.
UnsafeDisabled = true
)
// UnsafeReflectValue typically converts the passed reflect.Value into a one
// that bypasses the typical safety restrictions preventing access to
// unaddressable and unexported data. However, doing this relies on access to
// the unsafe package. This is a stub version which simply returns the passed
// reflect.Value when the unsafe package is not available.
func UnsafeReflectValue(v reflect.Value) reflect.Value {
return v
}
go-testdeep-1.15.0/internal/spew/common.go 0000664 0000000 0000000 00000013507 15144170453 0020441 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2013-2016 Dave Collins
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew
import (
"fmt"
"io"
"reflect"
"strconv"
)
// Some constants in the form of bytes to avoid string overhead. This mirrors
// the technique used in the fmt package.
var (
panicBytes = []byte("(PANIC=")
plusBytes = []byte("+")
iBytes = []byte("i")
trueBytes = []byte("true")
falseBytes = []byte("false")
interfaceBytes = []byte("(interface {})")
commaNewlineBytes = []byte(",\n")
newlineBytes = []byte("\n")
openBraceNewlineBytes = []byte("{\n")
closeBraceBytes = []byte("}")
asteriskBytes = []byte("*")
colonSpaceBytes = []byte(": ")
openParenBytes = []byte("(")
closeParenBytes = []byte(")")
spaceBytes = []byte(" ")
pointerChainBytes = []byte("->")
nilAngleBytes = []byte("")
maxNewlineBytes = []byte("\n")
circularBytes = []byte("")
invalidAngleBytes = []byte("")
lenEqualsBytes = []byte("len=")
capEqualsBytes = []byte("cap=")
)
// hexDigits is used to map a decimal value to a hex digit.
var hexDigits = "0123456789abcdef"
// catchPanic handles any panics that might occur during the handleMethods
// calls.
func catchPanic(w io.Writer) {
if err := recover(); err != nil {
w.Write(panicBytes) //nolint: errcheck
fmt.Fprintf(w, "%v", err) //nolint: errcheck
w.Write(closeParenBytes) //nolint: errcheck
}
}
// handleMethods attempts to call the Error and String methods on the underlying
// type the passed reflect.Value represents and outputes the result to Writer w.
//
// It handles panics in any called methods by catching and displaying the error
// as the formatted value.
func handleMethods(cs *ConfigState, w io.Writer, v reflect.Value) (handled bool) {
// We need an interface to check if the type implements the error or
// fmt.Stringer interface. However, the reflect package won't give us an
// interface on certain things like unexported struct fields in order
// to enforce visibility rules. We use unsafe, when it's available,
// to bypass these restrictions since this package does not mutate the
// values.
if !v.CanInterface() {
if UnsafeDisabled {
return false
}
v = UnsafeReflectValue(v)
}
// Choose whether or not to do error and fmt.Stringer interface
// lookups against the base type or a pointer to the base type
// depending on settings. Technically calling one of these methods
// with a pointer receiver can mutate the value, however, types
// which choose to satisify an error or fmt.Stringer interface with
// a pointer receiver should not be mutating their state inside
// these interface methods.
if !cs.DisablePointerMethods && !UnsafeDisabled && !v.CanAddr() {
v = UnsafeReflectValue(v)
}
if v.CanAddr() {
v = v.Addr()
}
// Is it an error or fmt.Stringer?
switch iface := v.Interface().(type) {
case error:
defer catchPanic(w)
w.Write([]byte(iface.Error())) //nolint: errcheck
return true
case fmt.Stringer:
defer catchPanic(w)
w.Write([]byte(iface.String())) //nolint: errcheck
return true
}
return false
}
// printBool outputs a boolean value as true or false to Writer w.
// nolint: errcheck
func printBool(w io.Writer, val bool) {
if val {
w.Write(trueBytes)
} else {
w.Write(falseBytes)
}
}
// printInt outputs a signed integer value to Writer w.
// nolint: errcheck
func printInt(w io.Writer, val int64, base int) {
w.Write([]byte(strconv.FormatInt(val, base)))
}
// printUint outputs an unsigned integer value to Writer w.
// nolint: errcheck
func printUint(w io.Writer, val uint64, base int) {
w.Write([]byte(strconv.FormatUint(val, base)))
}
// printFloat outputs a floating point value using the specified precision,
// which is expected to be 32 or 64bit, to Writer w.
// nolint: errcheck
func printFloat(w io.Writer, val float64, precision int) {
w.Write([]byte(strconv.FormatFloat(val, 'g', -1, precision)))
}
// printComplex outputs a complex value using the specified float precision
// for the real and imaginary parts to Writer w.
// nolint: errcheck
func printComplex(w io.Writer, c complex128, floatPrecision int) {
r := real(c)
w.Write(openParenBytes)
w.Write([]byte(strconv.FormatFloat(r, 'g', -1, floatPrecision)))
i := imag(c)
if i >= 0 {
w.Write(plusBytes) //nolint: errcheck
}
w.Write([]byte(strconv.FormatFloat(i, 'g', -1, floatPrecision)))
w.Write(iBytes)
w.Write(closeParenBytes)
}
// printHexPtr outputs a uintptr formatted as hexadecimal with a leading '0x'
// prefix to Writer w.
// nolint: errcheck
func printHexPtr(w io.Writer, p uintptr) {
// Null pointer.
num := uint64(p)
if num == 0 {
w.Write(nilAngleBytes)
return
}
// Max uint64 is 16 bytes in hex + 2 bytes for '0x' prefix
buf := make([]byte, 18)
// It's simpler to construct the hex string right to left.
base := uint64(16)
i := len(buf) - 1
for num >= base {
buf[i] = hexDigits[num%base]
num /= base
i--
}
buf[i] = hexDigits[num]
// Add '0x' prefix.
i--
buf[i] = 'x'
i--
buf[i] = '0'
// Strip unused leading bytes.
buf = buf[i:]
w.Write(buf)
}
go-testdeep-1.15.0/internal/spew/common_test.go 0000664 0000000 0000000 00000005542 15144170453 0021500 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2013-2016 Dave Collins
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew_test
import (
"fmt"
)
// custom type to test Stinger interface on non-pointer receiver.
type stringer string
// String implements the fmt.Stringer interface for testing invocation of custom
// stringers on types with non-pointer receivers.
func (s stringer) String() string {
return "stringer " + string(s)
}
// custom type to test Stinger interface on pointer receiver.
type pstringer string
// String implements the fmt.Stringer interface for testing invocation of custom
// stringers on types with only pointer receivers.
func (s *pstringer) String() string {
return "stringer " + string(*s)
}
// xref1 and xref2 are cross referencing structs for testing circular reference
// detection.
type xref1 struct {
ps2 *xref2
}
type xref2 struct {
ps1 *xref1
}
// indirCir1, indirCir2, and indirCir3 are used to generate an indirect circular
// reference for testing detection.
type indirCir1 struct {
ps2 *indirCir2
}
type indirCir2 struct {
ps3 *indirCir3
}
type indirCir3 struct {
ps1 *indirCir1
}
// embed is used to test embedded structures.
type embed struct {
a string
}
// embedwrap is used to test embedded structures.
type embedwrap struct {
*embed
e *embed
}
// panicer is used to intentionally cause a panic for testing spew properly
// handles them.
type panicer int
func (p panicer) String() string {
panic("test panic")
}
// customError is used to test custom error interface invocation.
type customError int
func (e customError) Error() string {
return fmt.Sprintf("error: %d", int(e))
}
// stringizeWants converts a slice of wanted test output into a format suitable
// for a test error message.
func stringizeWants(wants []string) string {
s := ""
for i, want := range wants {
if i > 0 {
s += fmt.Sprintf("\nwant%d: %s", i+1, want)
} else {
s += "want: " + want
}
}
return s
}
// testFailed returns whether or not a test failed by checking if the result
// of the test is in the slice of wanted strings.
func testFailed(result string, wants []string) bool {
for _, want := range wants {
if result == want {
return false
}
}
return true
}
go-testdeep-1.15.0/internal/spew/config.go 0000664 0000000 0000000 00000006245 15144170453 0020417 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2013-2016 Dave Collins
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew
// ConfigState houses the configuration options used by spew to format and
// display values. There is a global instance, Config, that is used to control
// top-level Sdump functionality.
//
// The zero value for ConfigState provides no indentation. You would typically
// want to set it to a space or a tab.
type ConfigState struct {
// Indent specifies the string to use for each indentation level. The
// global config instance that all top-level functions use set this to a
// single space by default. If you would like more indentation, you might
// set this to a tab with "\t" or perhaps two spaces with " ".
Indent string
// MaxDepth controls the maximum number of levels to descend into nested
// data structures. The default, 0, means there is no limit.
//
// NOTE: Circular data structures are properly detected, so it is not
// necessary to set this value unless you specifically want to limit deeply
// nested data structures.
MaxDepth int
// DisableMethods specifies whether or not error and fmt.Stringer
// interfaces are invoked for types that implement them.
DisableMethods bool
// DisablePointerMethods specifies whether or not to check for and invoke
// error and fmt.Stringer interfaces on types which only accept a pointer
// receiver when the current type is not a pointer.
//
// NOTE: This might be an unsafe action since calling one of these methods
// with a pointer receiver could technically mutate the value, however,
// in practice, types which choose to satisfy an error or fmt.Stringer
// interface with a pointer receiver should not be mutating their state
// inside these interface methods. As a result, this option relies on
// access to the unsafe package, so it will not have any effect when
// running in environments without access to the unsafe package such as
// Google App Engine or with the "safe" build tag specified.
DisablePointerMethods bool
// DisablePointerAddresses specifies whether to disable the printing of
// pointer addresses. This is useful when diffing data structures in tests.
DisablePointerAddresses bool
// EnableCapacities specifies whether to enable the printing of capacities
// for arrays, slices and channels.
EnableCapacities bool
}
// Config is the active configuration of the top-level functions.
// The configuration can be changed by modifying the contents of spew.Config.
var Config = ConfigState{Indent: " "}
go-testdeep-1.15.0/internal/spew/doc.go 0000664 0000000 0000000 00000011026 15144170453 0017710 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2013-2016 Dave Collins
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
Package spew implements a deep pretty printer for Go data structures to aid in
debugging.
Originally copied from https://github.com/davecgh/go-spew it has been
patched to fit go-testdeep needs as the original repository seems to
have no activity anymore.
A quick overview of the additional features spew provides over the built-in
printing facilities for Go data types are as follows:
- Pointers are dereferenced and followed
- Circular data structures are detected and handled properly
- Custom fmt.Stringer/error interfaces are optionally invoked, including
on unexported types
- Custom types which only implement the fmt.Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
- Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output
spew dumps Go data structures using a style which prints with newlines,
customizable indentation, and additional debug information such as
types and all pointer addresses used to indirect to the final value.
# Quick Start
This section demonstrates how to quickly get started with spew. See the
sections below for further details on formatting and configuration options.
To dump a variable with full newlines, indentation, type and pointer
information use Sdump:
str := spew.Sdump(myVar1)
# Configuration Options
Configuration of spew is handled by fields in the ConfigState type. For
convenience, all of the top-level functions use a global state available
via the spew.Config global.
It is also possible to create a ConfigState instance that provides methods
equivalent to the top-level functions. This allows concurrent configuration
options. See the ConfigState documentation for more details.
The following configuration options are available:
- Indent
String to use for each indentation level for Sdump function.
It is a single space by default. A popular alternative is "\t".
- MaxDepth
Maximum number of levels to descend into nested data structures.
There is no limit by default.
- DisableMethods
Disables invocation of error and fmt.Stringer interface methods.
Method invocation is enabled by default.
- DisablePointerMethods
Disables invocation of error and fmt.Stringer interface methods on types
which only accept pointer receivers from non-pointer variables.
Pointer method invocation is enabled by default.
- DisablePointerAddresses
DisablePointerAddresses specifies whether to disable the printing of
pointer addresses. This is useful when diffing data structures in tests.
- EnableCapacities
EnableCapacities specifies whether to enaable the printing of
capacities for arrays, slices and channels.
# Sdump Usage
Simply call spew.Sdump with a variable you want to dump as a string:
str := spew.Sdump(myVar1)
# Sample Sdump Output
(main.Foo) {
unexportedField: (*main.Bar)(0xf84002e210)({
flag: (main.Flag) flagTwo,
data: (uintptr)
}),
ExportedField: (map[interface {}]interface {}) (len=1) {
(string) (len=3) "one": (bool) true
}
}
Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C
command as shown.
([]uint8) (len=32) {
00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... |
00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0|
00000020 31 32 |12|
}
# Errors
Since it is possible for custom fmt.Stringer/error interfaces to panic,
spew detects them and handles them internally by printing the panic
information inline with the output. Since spew is intended to
provide deep pretty printing capabilities on structures, it
intentionally does not return any errors.
*/
package spew
go-testdeep-1.15.0/internal/spew/dump.go 0000664 0000000 0000000 00000030202 15144170453 0020105 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2013-2016 Dave Collins
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew
import (
"bytes"
"encoding/hex"
"fmt"
"io"
"reflect"
"strconv"
"strings"
"github.com/maxatome/go-testdeep/internal/sort"
)
var (
// uint8Type is a reflect.Type representing a uint8. It is used to
// convert cgo types to uint8 slices for hexdumping.
uint8Type = reflect.TypeOf(uint8(0))
// cCharSuffix is the suffix that matches a cgo char.
// It is used to detect character arrays to hexdump them.
cCharSuffix = `._Ctype_char`
// cUnsignedCharSuffix is the suffix that matches a cgo unsigned
// char. It is used to detect unsigned character arrays to hexdump
// them.
cUnsignedCharSuffix = `._Ctype_unsignedchar`
// cUint8tCharSuffix is the suffix that matches a cgo uint8_t.
// It is used to detect uint8_t arrays to hexdump them.
cUint8tCharSuffix = `._Ctype_uint8_t`
)
// dumpState contains information about the state of a dump operation.
type dumpState struct {
w io.Writer
depth int
pointers map[uintptr]int
ignoreNextType bool
ignoreNextIndent bool
cs *ConfigState
}
// indent performs indentation according to the depth level and cs.Indent
// option.
func (d *dumpState) indent() {
if d.ignoreNextIndent {
d.ignoreNextIndent = false
return
}
d.w.Write(bytes.Repeat([]byte(d.cs.Indent), d.depth)) //nolint: errcheck
}
// unpackValue returns values inside of non-nil interfaces when possible.
// This is useful for data types like structs, arrays, slices, and maps which
// can contain varying types packed inside an interface.
func (d *dumpState) unpackValue(v reflect.Value) reflect.Value {
if v.Kind() == reflect.Interface && !v.IsNil() {
v = v.Elem()
}
return v
}
// dumpPtr handles formatting of pointers by indirecting them as necessary.
// nolint: errcheck
func (d *dumpState) dumpPtr(v reflect.Value) {
// Remove pointers at or below the current depth from map used to detect
// circular refs.
for k, depth := range d.pointers {
if depth >= d.depth {
delete(d.pointers, k)
}
}
// Keep list of all dereferenced pointers to show later.
pointerChain := make([]uintptr, 0)
// Figure out how many levels of indirection there are by dereferencing
// pointers and unpacking interfaces down the chain while detecting circular
// references.
nilFound := false
cycleFound := false
indirects := 0
ve := v
for ve.Kind() == reflect.Ptr {
if ve.IsNil() {
nilFound = true
break
}
indirects++
addr := ve.Pointer()
pointerChain = append(pointerChain, addr)
if pd, ok := d.pointers[addr]; ok && pd < d.depth {
cycleFound = true
indirects--
break
}
d.pointers[addr] = d.depth
ve = ve.Elem()
if ve.Kind() == reflect.Interface {
if ve.IsNil() {
nilFound = true
break
}
ve = ve.Elem()
}
}
// Display type information.
d.w.Write(openParenBytes)
d.w.Write(bytes.Repeat(asteriskBytes, indirects))
d.w.Write([]byte(ve.Type().String()))
d.w.Write(closeParenBytes)
// Display pointer information.
if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 {
d.w.Write(openParenBytes)
for i, addr := range pointerChain {
if i > 0 {
d.w.Write(pointerChainBytes)
}
printHexPtr(d.w, addr)
}
d.w.Write(closeParenBytes)
}
// Display dereferenced value.
d.w.Write(openParenBytes)
switch {
case nilFound:
d.w.Write(nilAngleBytes)
case cycleFound:
d.w.Write(circularBytes)
default:
d.ignoreNextType = true
d.dump(ve)
}
d.w.Write(closeParenBytes)
}
// dumpSlice handles formatting of arrays and slices. Byte (uint8 under
// reflection) arrays and slices are dumped in hexdump -C fashion.
// nolint: errcheck
func (d *dumpState) dumpSlice(v reflect.Value) {
// Determine whether this type should be hex dumped or not. Also,
// for types which should be hexdumped, try to use the underlying data
// first, then fall back to trying to convert them to a uint8 slice.
var buf []uint8
doHexDump := false
numEntries := v.Len()
if numEntries > 0 {
vt := v.Index(0).Type()
vts := vt.String()
// C types that need to be converted.
doConvert := strings.HasSuffix(vts, cCharSuffix) ||
strings.HasSuffix(vts, cUnsignedCharSuffix) ||
strings.HasSuffix(vts, cUint8tCharSuffix)
// Try to use existing uint8 slices and fall back to converting
// and copying if that fails.
if !doConvert && vt.Kind() == reflect.Uint8 {
// The underlying data needs to be converted if it can't
// be type asserted to a uint8 slice.
doConvert = true
// We need an addressable interface to convert the type
// to a byte slice. However, the reflect package won't
// give us an interface on certain things like
// unexported struct fields in order to enforce
// visibility rules. We use unsafe, when available, to
// bypass these restrictions since this package does not
// mutate the values.
vs := v
if !vs.CanInterface() || !vs.CanAddr() {
vs = UnsafeReflectValue(vs)
}
if !UnsafeDisabled {
vs = vs.Slice(0, numEntries)
// Use the existing uint8 slice if it can be type asserted.
if slice, ok := vs.Interface().([]uint8); ok {
buf = slice
doHexDump = true
doConvert = false
}
}
}
// Copy and convert the underlying type if needed.
if doConvert && vt.ConvertibleTo(uint8Type) {
// Convert and copy each element into a uint8 byte
// slice.
buf = make([]uint8, numEntries)
for i := 0; i < numEntries; i++ {
vv := v.Index(i)
buf[i] = uint8(vv.Convert(uint8Type).Uint())
}
doHexDump = true
}
}
// Hexdump the entire slice as needed.
if doHexDump {
indent := strings.Repeat(d.cs.Indent, d.depth)
str := indent + hex.Dump(buf)
str = strings.ReplaceAll(str, "\n", "\n"+indent)
str = strings.TrimRight(str, d.cs.Indent)
d.w.Write([]byte(str))
return
}
// Recursively call dump for each item.
for i := 0; i < numEntries; i++ {
d.dump(d.unpackValue(v.Index(i)))
if i < (numEntries - 1) {
d.w.Write(commaNewlineBytes)
} else {
d.w.Write(newlineBytes)
}
}
}
// dump is the main workhorse for dumping a value. It uses the passed reflect
// value to figure out what kind of object we are dealing with and formats it
// appropriately. It is a recursive function, however circular data structures
// are detected and handled properly.
// nolint: errcheck
func (d *dumpState) dump(v reflect.Value) {
// Handle invalid reflect values immediately.
kind := v.Kind()
if kind == reflect.Invalid {
d.w.Write(invalidAngleBytes)
return
}
// Handle pointers specially.
if kind == reflect.Ptr {
d.indent()
d.dumpPtr(v)
return
}
// Print type information unless already handled elsewhere.
if !d.ignoreNextType {
d.indent()
d.w.Write(openParenBytes)
d.w.Write([]byte(v.Type().String()))
d.w.Write(closeParenBytes)
d.w.Write(spaceBytes)
}
d.ignoreNextType = false
// Display length and capacity if the built-in len and cap functions
// work with the value's kind and the len/cap itself is non-zero.
valueLen, valueCap := 0, 0
switch v.Kind() {
case reflect.Array, reflect.Slice, reflect.Chan:
valueLen, valueCap = v.Len(), v.Cap()
case reflect.Map, reflect.String:
valueLen = v.Len()
}
if valueLen != 0 || d.cs.EnableCapacities && valueCap != 0 {
d.w.Write(openParenBytes)
if valueLen != 0 {
d.w.Write(lenEqualsBytes)
printInt(d.w, int64(valueLen), 10)
}
if d.cs.EnableCapacities && valueCap != 0 {
if valueLen != 0 {
d.w.Write(spaceBytes)
}
d.w.Write(capEqualsBytes)
printInt(d.w, int64(valueCap), 10)
}
d.w.Write(closeParenBytes)
d.w.Write(spaceBytes)
}
// Call fmt.Stringer/error interfaces if they exist and the handle
// methods flag is enabled
if !d.cs.DisableMethods &&
kind != reflect.Invalid && kind != reflect.Interface {
if handled := handleMethods(d.cs, d.w, v); handled {
return
}
}
switch kind {
case reflect.Invalid, reflect.Ptr:
// Do nothing. We should never get here since invalid & pointers
// have already been handled above.
case reflect.Bool:
printBool(d.w, v.Bool())
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
printInt(d.w, v.Int(), 10)
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
printUint(d.w, v.Uint(), 10)
case reflect.Float32:
printFloat(d.w, v.Float(), 32)
case reflect.Float64:
printFloat(d.w, v.Float(), 64)
case reflect.Complex64:
printComplex(d.w, v.Complex(), 32)
case reflect.Complex128:
printComplex(d.w, v.Complex(), 64)
case reflect.Slice:
if v.IsNil() {
d.w.Write(nilAngleBytes)
break
}
fallthrough
case reflect.Array:
d.w.Write(openBraceNewlineBytes)
d.depth++
if d.cs.MaxDepth != 0 && d.depth > d.cs.MaxDepth {
d.indent()
d.w.Write(maxNewlineBytes)
} else {
d.dumpSlice(v)
}
d.depth--
d.indent()
d.w.Write(closeBraceBytes)
case reflect.String:
d.w.Write([]byte(strconv.Quote(v.String())))
case reflect.Interface:
// The only time we should get here is for nil interfaces due to
// unpackValue calls.
if v.IsNil() {
d.w.Write(nilAngleBytes)
}
case reflect.Map:
// nil maps should be indicated as different than empty maps
if v.IsNil() {
d.w.Write(nilAngleBytes)
break
}
d.w.Write(openBraceNewlineBytes)
d.depth++
if d.cs.MaxDepth != 0 && d.depth > d.cs.MaxDepth {
d.indent()
d.w.Write(maxNewlineBytes)
} else {
i := v.Len()
sort.MapEach(v, func(key reflect.Value, val reflect.Value) bool {
d.dump(d.unpackValue(key))
d.w.Write(colonSpaceBytes)
d.ignoreNextIndent = true
d.dump(d.unpackValue(val))
i--
if i > 0 {
d.w.Write(commaNewlineBytes)
} else {
d.w.Write(newlineBytes)
}
return true
})
}
d.depth--
d.indent()
d.w.Write(closeBraceBytes)
case reflect.Struct:
d.w.Write(openBraceNewlineBytes)
d.depth++
if d.cs.MaxDepth != 0 && d.depth > d.cs.MaxDepth {
d.indent()
d.w.Write(maxNewlineBytes)
} else {
vt := v.Type()
numFields := v.NumField()
for i := 0; i < numFields; i++ {
vf := v.Field(i)
if vf.IsZero() {
continue
}
d.indent()
vtf := vt.Field(i)
d.w.Write([]byte(vtf.Name))
d.w.Write(colonSpaceBytes)
d.ignoreNextIndent = true
d.dump(d.unpackValue(vf))
if i < (numFields - 1) {
d.w.Write(commaNewlineBytes)
} else {
d.w.Write(newlineBytes)
}
}
}
d.depth--
d.indent()
d.w.Write(closeBraceBytes)
case reflect.Uintptr:
printHexPtr(d.w, uintptr(v.Uint()))
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
printHexPtr(d.w, v.Pointer())
// There were not any other types at the time this code was written, but
// fall back to letting the default fmt package handle it in case any new
// types are added.
default:
if v.CanInterface() {
fmt.Fprintf(d.w, "%v", v.Interface())
} else {
fmt.Fprintf(d.w, "%v", v.String())
}
}
}
// fdump is a helper function to consolidate the logic from the various public
// methods which take varying writers and config states.
// nolint: errcheck
func fdump(cs *ConfigState, w io.Writer, arg any) {
if arg == nil {
w.Write(interfaceBytes)
w.Write(spaceBytes)
w.Write(nilAngleBytes)
w.Write(newlineBytes)
return
}
d := dumpState{w: w, cs: cs}
d.pointers = make(map[uintptr]int)
d.dump(reflect.ValueOf(arg))
d.w.Write(newlineBytes)
}
// Sdump returns the string representation of a using config[0] if passed.
func Sdump(a any, config ...ConfigState) string {
var buf strings.Builder
cs := Config
if len(config) > 0 {
cs = config[0]
}
fdump(&cs, &buf, a)
return strings.TrimRight(buf.String(), "\n")
}
go-testdeep-1.15.0/internal/spew/dump_test.go 0000664 0000000 0000000 00000104244 15144170453 0021154 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2013-2016 Dave Collins
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
Test Summary:
NOTE: For each test, a nil pointer, a single pointer and double pointer to the
base test element are also tested to ensure proper indirection across all types.
- Max int8, int16, int32, int64, int
- Max uint8, uint16, uint32, uint64, uint
- Boolean true and false
- Standard complex64 and complex128
- Array containing standard ints
- Array containing type with custom formatter on pointer receiver only
- Array containing interfaces
- Array containing bytes
- Slice containing standard float32 values
- Slice containing type with custom formatter on pointer receiver only
- Slice containing interfaces
- Slice containing bytes
- Nil slice
- Standard string
- Nil interface
- Sub-interface
- Map with string keys and int vals
- Map with custom formatter type on pointer receiver only keys and vals
- Map with interface keys and values
- Map with nil interface value
- Struct with primitives
- Struct that contains another struct
- Struct that contains custom type with fmt.Stringer pointer interface via both
exported and unexported fields
- Struct that contains embedded struct and field to same struct
- Uintptr to 0 (null pointer)
- Uintptr address of real variable
- Unsafe.Pointer to 0 (null pointer)
- Unsafe.Pointer to address of real variable
- Nil channel
- Standard int channel
- Function with no params and no returns
- Function with param and no returns
- Function with multiple params and multiple returns
- Struct that is circular through self referencing
- Structs that are circular through cross referencing
- Structs that are indirectly circular
- Type that panics in its fmt.Stringer interface
*/
package spew_test
import (
"fmt"
"testing"
"unsafe"
"github.com/maxatome/go-testdeep/internal/spew"
)
// dumpTest is used to describe a test to be performed against Sdump function.
type dumpTest struct {
name string
in any
wants []string
}
// dumpTests houses all of the tests to be performed against the Dump method.
var dumpTests = make([]dumpTest, 0)
// addSdumpTest is a helper method to append the passed input and desired result
// to dumpTests.
func addSdumpTest(name string, in any, wants ...string) {
test := dumpTest{name, in, wants}
dumpTests = append(dumpTests, test)
}
func addIntSdumpTests() {
// Max int8.
v := int8(127)
nv := (*int8)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "int8"
vs := "127"
addSdumpTest("int8", v, "("+vt+") "+vs)
addSdumpTest("int8 ptr", pv, "(*"+vt+")("+vAddr+")("+vs+")")
addSdumpTest("int8 2ptr", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")")
addSdumpTest("int8 nil ptr", nv, "(*"+vt+")()")
// Max int16.
v2 := int16(32767)
nv2 := (*int16)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "int16"
v2s := "32767"
addSdumpTest("int16", v2, "("+v2t+") "+v2s)
addSdumpTest("int16 ptr", pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")")
addSdumpTest("int16 2ptr", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")")
addSdumpTest("int16 nil ptr", nv2, "(*"+v2t+")()")
// Max int32.
v3 := int32(2147483647)
nv3 := (*int32)(nil)
pv3 := &v3
v3Addr := fmt.Sprintf("%p", pv3)
pv3Addr := fmt.Sprintf("%p", &pv3)
v3t := "int32"
v3s := "2147483647"
addSdumpTest("int32", v3, "("+v3t+") "+v3s)
addSdumpTest("int32 ptr", pv3, "(*"+v3t+")("+v3Addr+")("+v3s+")")
addSdumpTest("int32 2ptr", &pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")("+v3s+")")
addSdumpTest("int32 nil ptr", nv3, "(*"+v3t+")()")
// Max int64.
v4 := int64(9223372036854775807)
nv4 := (*int64)(nil)
pv4 := &v4
v4Addr := fmt.Sprintf("%p", pv4)
pv4Addr := fmt.Sprintf("%p", &pv4)
v4t := "int64"
v4s := "9223372036854775807"
addSdumpTest("int64", v4, "("+v4t+") "+v4s)
addSdumpTest("int64 ptr", pv4, "(*"+v4t+")("+v4Addr+")("+v4s+")")
addSdumpTest("int64 2ptr", &pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")("+v4s+")")
addSdumpTest("int64 nil ptr", nv4, "(*"+v4t+")()")
// Max int.
v5 := int(2147483647)
nv5 := (*int)(nil)
pv5 := &v5
v5Addr := fmt.Sprintf("%p", pv5)
pv5Addr := fmt.Sprintf("%p", &pv5)
v5t := "int"
v5s := "2147483647"
addSdumpTest("int", v5, "("+v5t+") "+v5s)
addSdumpTest("int ptr", pv5, "(*"+v5t+")("+v5Addr+")("+v5s+")")
addSdumpTest("int 2ptr", &pv5, "(**"+v5t+")("+pv5Addr+"->"+v5Addr+")("+v5s+")")
addSdumpTest("int nil ptr", nv5, "(*"+v5t+")()")
}
func addUintSdumpTests() {
// Max uint8.
v := uint8(255)
nv := (*uint8)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "uint8"
vs := "255"
addSdumpTest("uint8", v, "("+vt+") "+vs)
addSdumpTest("uint8 ptr", pv, "(*"+vt+")("+vAddr+")("+vs+")")
addSdumpTest("uint8 2ptr", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")")
addSdumpTest("uint8 nil ptr", nv, "(*"+vt+")()")
// Max uint16.
v2 := uint16(65535)
nv2 := (*uint16)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "uint16"
v2s := "65535"
addSdumpTest("uint16", v2, "("+v2t+") "+v2s)
addSdumpTest("uint16 ptr", pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")")
addSdumpTest("uint16 2ptr", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")")
addSdumpTest("uint16 nil ptr", nv2, "(*"+v2t+")()")
// Max uint32.
v3 := uint32(4294967295)
nv3 := (*uint32)(nil)
pv3 := &v3
v3Addr := fmt.Sprintf("%p", pv3)
pv3Addr := fmt.Sprintf("%p", &pv3)
v3t := "uint32"
v3s := "4294967295"
addSdumpTest("uint32", v3, "("+v3t+") "+v3s)
addSdumpTest("uint32 ptr", pv3, "(*"+v3t+")("+v3Addr+")("+v3s+")")
addSdumpTest("uint32 2ptr", &pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")("+v3s+")")
addSdumpTest("uint32 nil ptr", nv3, "(*"+v3t+")()")
// Max uint64.
v4 := uint64(18446744073709551615)
nv4 := (*uint64)(nil)
pv4 := &v4
v4Addr := fmt.Sprintf("%p", pv4)
pv4Addr := fmt.Sprintf("%p", &pv4)
v4t := "uint64"
v4s := "18446744073709551615"
addSdumpTest("uint64", v4, "("+v4t+") "+v4s)
addSdumpTest("uint64 ptr", pv4, "(*"+v4t+")("+v4Addr+")("+v4s+")")
addSdumpTest("uint64 2ptr", &pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")("+v4s+")")
addSdumpTest("uint64 nil ptr", nv4, "(*"+v4t+")()")
// Max uint.
v5 := uint(4294967295)
nv5 := (*uint)(nil)
pv5 := &v5
v5Addr := fmt.Sprintf("%p", pv5)
pv5Addr := fmt.Sprintf("%p", &pv5)
v5t := "uint"
v5s := "4294967295"
addSdumpTest("uint", v5, "("+v5t+") "+v5s)
addSdumpTest("uint ptr", pv5, "(*"+v5t+")("+v5Addr+")("+v5s+")")
addSdumpTest("uint 2ptr", &pv5, "(**"+v5t+")("+pv5Addr+"->"+v5Addr+")("+v5s+")")
addSdumpTest("uint nil ptr", nv5, "(*"+v5t+")()")
}
func addBoolSdumpTests() {
// Boolean true.
v := bool(true)
nv := (*bool)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "bool"
vs := "true"
addSdumpTest("true", v, "("+vt+") "+vs)
addSdumpTest("true ptr", pv, "(*"+vt+")("+vAddr+")("+vs+")")
addSdumpTest("true 2ptr", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")")
addSdumpTest("true nil ptr", nv, "(*"+vt+")()")
// Boolean false.
v2 := bool(false)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "bool"
v2s := "false"
addSdumpTest("false", v2, "("+v2t+") "+v2s)
addSdumpTest("false ptr", pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")")
addSdumpTest("false 2ptr", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")")
}
func addFloatSdumpTests() {
// Standard float32.
v := float32(3.1415)
nv := (*float32)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "float32"
vs := "3.1415"
addSdumpTest("float32", v, "("+vt+") "+vs)
addSdumpTest("float32 ptr", pv, "(*"+vt+")("+vAddr+")("+vs+")")
addSdumpTest("float32 2ptr", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")")
addSdumpTest("float32 nil ptr", nv, "(*"+vt+")()")
// Standard float64.
v2 := float64(3.1415926)
nv2 := (*float64)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "float64"
v2s := "3.1415926"
addSdumpTest("float64", v2, "("+v2t+") "+v2s)
addSdumpTest("float64 ptr", pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")")
addSdumpTest("float64 2ptr", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")")
addSdumpTest("float64 nil ptr", nv2, "(*"+v2t+")()")
}
func addComplexSdumpTests() {
// Standard complex64.
v := complex(float32(6), -2)
nv := (*complex64)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "complex64"
vs := "(6-2i)"
addSdumpTest("complex64", v, "("+vt+") "+vs)
addSdumpTest("complex64 ptr", pv, "(*"+vt+")("+vAddr+")("+vs+")")
addSdumpTest("complex64 2ptr", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")")
addSdumpTest("complex64 nil ptr", nv, "(*"+vt+")()")
// Standard complex128.
v2 := complex(float64(-6), 2)
nv2 := (*complex128)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "complex128"
v2s := "(-6+2i)"
addSdumpTest("complex128", v2, "("+v2t+") "+v2s)
addSdumpTest("complex128 ptr", pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")")
addSdumpTest("complex128 2ptr", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")")
addSdumpTest("complex128 nil ptr", nv2, "(*"+v2t+")()")
}
func addArraySdumpTests() {
// Array containing standard ints.
v := [3]int{1, 2, 3}
vLen := fmt.Sprintf("%d", len(v))
nv := (*[3]int)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "int"
vs := "(len=" + vLen + ") {\n (" + vt + ") 1,\n (" +
vt + ") 2,\n (" + vt + ") 3\n}"
addSdumpTest("array of int", v, "([3]"+vt+") "+vs)
addSdumpTest("array of int ptr", pv, "(*[3]"+vt+")("+vAddr+")("+vs+")")
addSdumpTest("array of int 2ptr", &pv, "(**[3]"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")")
addSdumpTest("array of int nil ptr", nv, "(*[3]"+vt+")()")
// Array containing type with fmt.Stringer on pointer receiver only.
v2i0 := pstringer("1")
v2i1 := pstringer("2")
v2i2 := pstringer("3")
v2 := [3]pstringer{v2i0, v2i1, v2i2}
v2i0Len := fmt.Sprintf("%d", len(v2i0))
v2i1Len := fmt.Sprintf("%d", len(v2i1))
v2i2Len := fmt.Sprintf("%d", len(v2i2))
v2Len := fmt.Sprintf("%d", len(v2))
nv2 := (*[3]pstringer)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "spew_test.pstringer"
v2sp := "(len=" + v2Len + ") {\n (" + v2t +
") (len=" + v2i0Len + ") stringer 1,\n (" + v2t +
") (len=" + v2i1Len + ") stringer 2,\n (" + v2t +
") (len=" + v2i2Len + ") stringer 3\n}"
v2s := v2sp
if spew.UnsafeDisabled {
v2s = "(len=" + v2Len + ") {\n (" + v2t +
") (len=" + v2i0Len + ") \"1\",\n (" + v2t +
") (len=" + v2i1Len + ") \"2\",\n (" + v2t +
") (len=" + v2i2Len + ") " + "\"3\"\n}"
}
addSdumpTest("array of stringer", v2, "([3]"+v2t+") "+v2s)
addSdumpTest("array of stringer ptr", pv2, "(*[3]"+v2t+")("+v2Addr+")("+v2sp+")")
addSdumpTest("array of stringer 2ptr", &pv2, "(**[3]"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2sp+")")
addSdumpTest("array of stringer nil ptr", nv2, "(*[3]"+v2t+")()")
// Array containing interfaces.
v3i0 := "one"
v3 := [3]any{v3i0, int(2), uint(3)}
v3i0Len := fmt.Sprintf("%d", len(v3i0))
v3Len := fmt.Sprintf("%d", len(v3))
nv3 := (*[3]any)(nil)
pv3 := &v3
v3Addr := fmt.Sprintf("%p", pv3)
pv3Addr := fmt.Sprintf("%p", &pv3)
v3t := "[3]interface {}"
v3t2 := "string"
v3t3 := "int"
v3t4 := "uint"
v3s := "(len=" + v3Len + ") {\n (" + v3t2 + ") " +
"(len=" + v3i0Len + ") \"one\",\n (" + v3t3 + ") 2,\n (" +
v3t4 + ") 3\n}"
addSdumpTest("array of iface", v3, "("+v3t+") "+v3s)
addSdumpTest("array of iface ptr", pv3, "(*"+v3t+")("+v3Addr+")("+v3s+")")
addSdumpTest("array of iface 2ptr", &pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")("+v3s+")")
addSdumpTest("array of iface nil ptr", nv3, "(*"+v3t+")()")
// Array containing bytes.
v4 := [34]byte{
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30,
0x31, 0x32,
}
v4Len := fmt.Sprintf("%d", len(v4))
nv4 := (*[34]byte)(nil)
pv4 := &v4
v4Addr := fmt.Sprintf("%p", pv4)
pv4Addr := fmt.Sprintf("%p", &pv4)
v4t := "[34]uint8"
v4s := "(len=" + v4Len + ") " +
"{\n 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20" +
" |............... |\n" +
" 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30" +
" |!\"#$%&'()*+,-./0|\n" +
" 00000020 31 32 " +
" |12|\n}"
addSdumpTest("array of byte", v4, "("+v4t+") "+v4s)
addSdumpTest("array of byte ptr", pv4, "(*"+v4t+")("+v4Addr+")("+v4s+")")
addSdumpTest("array of byte 2ptr", &pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")("+v4s+")")
addSdumpTest("array of byte nil ptr", nv4, "(*"+v4t+")()")
}
func addSliceSdumpTests() {
// Slice containing standard float32 values.
v := []float32{3.14, 6.28, 12.56}
vLen := fmt.Sprintf("%d", len(v))
nv := (*[]float32)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "float32"
vs := "(len=" + vLen + ") {\n (" + vt + ") 3.14,\n (" +
vt + ") 6.28,\n (" + vt + ") 12.56\n}"
addSdumpTest("slice of float32", v, "([]"+vt+") "+vs)
addSdumpTest("slice of float32 ptr", pv, "(*[]"+vt+")("+vAddr+")("+vs+")")
addSdumpTest("slice of float32 2ptr", &pv, "(**[]"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")")
addSdumpTest("slice of float32 nil ptr", nv, "(*[]"+vt+")()")
// Slice containing type with custom formatter on pointer receiver only.
v2i0 := pstringer("1")
v2i1 := pstringer("2")
v2i2 := pstringer("3")
v2 := []pstringer{v2i0, v2i1, v2i2}
v2i0Len := fmt.Sprintf("%d", len(v2i0))
v2i1Len := fmt.Sprintf("%d", len(v2i1))
v2i2Len := fmt.Sprintf("%d", len(v2i2))
v2Len := fmt.Sprintf("%d", len(v2))
nv2 := (*[]pstringer)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "spew_test.pstringer"
v2s := "(len=" + v2Len + ") {\n (" + v2t + ") (len=" +
v2i0Len + ") stringer 1,\n (" + v2t + ") (len=" + v2i1Len +
") stringer 2,\n (" + v2t + ") (len=" + v2i2Len + ") " +
"stringer 3\n}"
addSdumpTest("slice of stringer", v2, "([]"+v2t+") "+v2s)
addSdumpTest("slice of stringer ptr", pv2, "(*[]"+v2t+")("+v2Addr+")("+v2s+")")
addSdumpTest("slice of stringer 2ptr", &pv2, "(**[]"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")")
addSdumpTest("slice of stringer nil ptr", nv2, "(*[]"+v2t+")()")
// Slice containing interfaces.
v3i0 := "one"
v3 := []any{v3i0, int(2), uint(3), nil}
v3i0Len := fmt.Sprintf("%d", len(v3i0))
v3Len := fmt.Sprintf("%d", len(v3))
nv3 := (*[]any)(nil)
pv3 := &v3
v3Addr := fmt.Sprintf("%p", pv3)
pv3Addr := fmt.Sprintf("%p", &pv3)
v3t := "[]interface {}"
v3t2 := "string"
v3t3 := "int"
v3t4 := "uint"
v3t5 := "interface {}"
v3s := "(len=" + v3Len + ") {\n (" + v3t2 + ") " +
"(len=" + v3i0Len + ") \"one\",\n (" + v3t3 + ") 2,\n (" +
v3t4 + ") 3,\n (" + v3t5 + ") \n}"
addSdumpTest("slice of iface", v3, "("+v3t+") "+v3s)
addSdumpTest("slice of iface ptr", pv3, "(*"+v3t+")("+v3Addr+")("+v3s+")")
addSdumpTest("slice of iface 2ptr", &pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")("+v3s+")")
addSdumpTest("slice of iface nil ptr", nv3, "(*"+v3t+")()")
// Slice containing bytes.
v4 := []byte{
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30,
0x31, 0x32,
}
v4Len := fmt.Sprintf("%d", len(v4))
nv4 := (*[]byte)(nil)
pv4 := &v4
v4Addr := fmt.Sprintf("%p", pv4)
pv4Addr := fmt.Sprintf("%p", &pv4)
v4t := "[]uint8"
v4s := "(len=" + v4Len + ") " +
"{\n 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20" +
" |............... |\n" +
" 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30" +
" |!\"#$%&'()*+,-./0|\n" +
" 00000020 31 32 " +
" |12|\n}"
addSdumpTest("slice of byte", v4, "("+v4t+") "+v4s)
addSdumpTest("slice of byte ptr", pv4, "(*"+v4t+")("+v4Addr+")("+v4s+")")
addSdumpTest("slice of byte 2ptr", &pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")("+v4s+")")
addSdumpTest("slice of byte nil ptr", nv4, "(*"+v4t+")()")
type myUint8 uint8
v4b := []myUint8{
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30,
0x31, 0x32,
}
addSdumpTest("slice of myUint8", v4b, "([]spew_test.myUint8) "+v4s)
// Nil slice.
v5 := []int(nil)
nv5 := (*[]int)(nil)
pv5 := &v5
v5Addr := fmt.Sprintf("%p", pv5)
pv5Addr := fmt.Sprintf("%p", &pv5)
v5t := "[]int"
v5s := ""
addSdumpTest("nil slice", v5, "("+v5t+") "+v5s)
addSdumpTest("nil slice ptr", pv5, "(*"+v5t+")("+v5Addr+")("+v5s+")")
addSdumpTest("nil slice 2ptr", &pv5, "(**"+v5t+")("+pv5Addr+"->"+v5Addr+")("+v5s+")")
addSdumpTest("nil slice nil ptr", nv5, "(*"+v5t+")()")
}
func addStringSdumpTests() {
// Standard string.
v := "test"
vLen := fmt.Sprintf("%d", len(v))
nv := (*string)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "string"
vs := "(len=" + vLen + ") \"test\""
addSdumpTest("string", v, "("+vt+") "+vs)
addSdumpTest("string ptr", pv, "(*"+vt+")("+vAddr+")("+vs+")")
addSdumpTest("string 2ptr", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")")
addSdumpTest("string nil ptr", nv, "(*"+vt+")()")
}
func addInterfaceSdumpTests() {
// Nil interface.
var v any
nv := (*any)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "interface {}"
vs := ""
addSdumpTest("nil iface", v, "("+vt+") "+vs)
addSdumpTest("nil iface ptr", pv, "(*"+vt+")("+vAddr+")("+vs+")")
addSdumpTest("nil iface 2ptr", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")")
addSdumpTest("nil iface nil ptr", nv, "(*"+vt+")()")
// Sub-interface.
v2 := any(uint16(65535))
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "uint16"
v2s := "65535"
addSdumpTest("iface", v2, "("+v2t+") "+v2s)
addSdumpTest("iface ptr", pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")")
addSdumpTest("iface 2ptr", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")")
}
func addMapSdumpTests() {
// Map with string keys and int vals.
k := "one"
kk := "two"
m := map[string]int{k: 1, kk: 2}
klen := fmt.Sprintf("%d", len(k)) // not kLen to shut golint up
kkLen := fmt.Sprintf("%d", len(kk))
mLen := fmt.Sprintf("%d", len(m))
nilMap := map[string]int(nil)
nm := (*map[string]int)(nil)
pm := &m
mAddr := fmt.Sprintf("%p", pm)
pmAddr := fmt.Sprintf("%p", &pm)
mt := "map[string]int"
mt1 := "string"
mt2 := "int"
ms := "(len=" + mLen + ") {\n (" + mt1 + ") (len=" + klen + ") " +
"\"one\": (" + mt2 + ") 1,\n (" + mt1 + ") (len=" + kkLen +
") \"two\": (" + mt2 + ") 2\n}"
ms2 := "(len=" + mLen + ") {\n (" + mt1 + ") (len=" + kkLen + ") " +
"\"two\": (" + mt2 + ") 2,\n (" + mt1 + ") (len=" + klen +
") \"one\": (" + mt2 + ") 1\n}"
addSdumpTest("map string-int", m,
"("+mt+") "+ms,
"("+mt+") "+ms2)
addSdumpTest("map string-int ptr", pm,
"(*"+mt+")("+mAddr+")("+ms+")",
"(*"+mt+")("+mAddr+")("+ms2+")")
addSdumpTest("map string-int 2ptr", &pm,
"(**"+mt+")("+pmAddr+"->"+mAddr+")("+ms+")",
"(**"+mt+")("+pmAddr+"->"+mAddr+")("+ms2+")")
addSdumpTest("map string-int nil ptr", nm, "(*"+mt+")()")
addSdumpTest("nil map string-int", nilMap, "("+mt+") ")
// Map with fmt.Stringer on pointer receiver only keys and vals.
k2 := pstringer("one")
v2 := pstringer("1")
m2 := map[pstringer]pstringer{k2: v2}
k2Len := fmt.Sprintf("%d", len(k2))
v2Len := fmt.Sprintf("%d", len(v2))
m2Len := fmt.Sprintf("%d", len(m2))
nilMap2 := map[pstringer]pstringer(nil)
nm2 := (*map[pstringer]pstringer)(nil)
pm2 := &m2
m2Addr := fmt.Sprintf("%p", pm2)
pm2Addr := fmt.Sprintf("%p", &pm2)
m2t := "map[spew_test.pstringer]spew_test.pstringer"
m2t1 := "spew_test.pstringer"
m2t2 := "spew_test.pstringer"
m2s := "(len=" + m2Len + ") {\n (" + m2t1 + ") (len=" + k2Len + ") " +
"stringer one: (" + m2t2 + ") (len=" + v2Len + ") stringer 1\n}"
if spew.UnsafeDisabled {
m2s = "(len=" + m2Len + ") {\n (" + m2t1 + ") (len=" + k2Len + ") " +
"\"one\": (" + m2t2 + ") (len=" + v2Len + ") \"1\"\n}"
}
addSdumpTest("map pstringer-pstringer", m2, "("+m2t+") "+m2s)
addSdumpTest("map pstringer-pstringer ptr", pm2, "(*"+m2t+")("+m2Addr+")("+m2s+")")
addSdumpTest("map pstringer-pstringer 2ptr", &pm2, "(**"+m2t+")("+pm2Addr+"->"+m2Addr+")("+m2s+")")
addSdumpTest("map pstringer-pstringer nil ptr", nm2, "(*"+m2t+")()")
addSdumpTest("nil map pstringer-pstringer", nilMap2, "("+m2t+") ")
// Map with interface keys and values.
k3 := "one"
k3Len := fmt.Sprintf("%d", len(k3))
m3 := map[any]any{k3: 1}
m3Len := fmt.Sprintf("%d", len(m3))
nilMap3 := map[any]any(nil)
nm3 := (*map[any]any)(nil)
pm3 := &m3
m3Addr := fmt.Sprintf("%p", pm3)
pm3Addr := fmt.Sprintf("%p", &pm3)
m3t := "map[interface {}]interface {}"
m3t1 := "string"
m3t2 := "int"
m3s := "(len=" + m3Len + ") {\n (" + m3t1 + ") (len=" + k3Len + ") " +
"\"one\": (" + m3t2 + ") 1\n}"
addSdumpTest("map any-any", m3, "("+m3t+") "+m3s)
addSdumpTest("map any-any ptr", pm3, "(*"+m3t+")("+m3Addr+")("+m3s+")")
addSdumpTest("map any-any 2ptr", &pm3, "(**"+m3t+")("+pm3Addr+"->"+m3Addr+")("+m3s+")")
addSdumpTest("map any-any nil ptr", nm3, "(*"+m3t+")()")
addSdumpTest("nil map any-any", nilMap3, "("+m3t+") ")
// Map with nil interface value.
k4 := "nil"
k4Len := fmt.Sprintf("%d", len(k4))
m4 := map[string]any{k4: nil}
m4Len := fmt.Sprintf("%d", len(m4))
nilMap4 := map[string]any(nil)
nm4 := (*map[string]any)(nil)
pm4 := &m4
m4Addr := fmt.Sprintf("%p", pm4)
pm4Addr := fmt.Sprintf("%p", &pm4)
m4t := "map[string]interface {}"
m4t1 := "string"
m4t2 := "interface {}"
m4s := "(len=" + m4Len + ") {\n (" + m4t1 + ") (len=" + k4Len + ")" +
" \"nil\": (" + m4t2 + ") \n}"
addSdumpTest("map string-any", m4, "("+m4t+") "+m4s)
addSdumpTest("map string-any ptr", pm4, "(*"+m4t+")("+m4Addr+")("+m4s+")")
addSdumpTest("map string-any 2ptr", &pm4, "(**"+m4t+")("+pm4Addr+"->"+m4Addr+")("+m4s+")")
addSdumpTest("map string-any nil ptr", nm4, "(*"+m4t+")()")
addSdumpTest("nil map string-any", nilMap4, "("+m4t+") ")
}
func addStructSdumpTests() {
// Struct with primitives.
type s1 struct {
a int8
b uint8
}
v := s1{127, 255}
nv := (*s1)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "spew_test.s1"
vt2 := "int8"
vt3 := "uint8"
vs := "{\n a: (" + vt2 + ") 127,\n b: (" + vt3 + ") 255\n}"
addSdumpTest("struct", v, "("+vt+") "+vs)
addSdumpTest("struct ptr", pv, "(*"+vt+")("+vAddr+")("+vs+")")
addSdumpTest("struct 2ptr", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")")
addSdumpTest("struct nil ptr", nv, "(*"+vt+")()")
// Struct that contains another struct.
type s2 struct {
s1 s1
b bool
}
v2 := s2{s1{127, 255}, true}
nv2 := (*s2)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "spew_test.s2"
v2t2 := "spew_test.s1"
v2t3 := "int8"
v2t4 := "uint8"
v2t5 := "bool"
v2s := "{\n s1: (" + v2t2 + ") {\n a: (" + v2t3 + ") 127,\n b: (" +
v2t4 + ") 255\n },\n b: (" + v2t5 + ") true\n}"
addSdumpTest("struct of struct", v2, "("+v2t+") "+v2s)
addSdumpTest("struct of struct ptr", pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")")
addSdumpTest("struct of struct 2ptr", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")")
addSdumpTest("struct of struct nil ptr", nv2, "(*"+v2t+")()")
// Struct that contains custom type with fmt.Stringer pointer
// interface via both exported and unexported fields.
type s3 struct {
s pstringer
S pstringer
}
v3 := s3{"test", "test2"}
nv3 := (*s3)(nil)
pv3 := &v3
v3Addr := fmt.Sprintf("%p", pv3)
pv3Addr := fmt.Sprintf("%p", &pv3)
v3t := "spew_test.s3"
v3t2 := "spew_test.pstringer"
v3s := "{\n s: (" + v3t2 + ") (len=4) stringer test,\n S: (" + v3t2 +
") (len=5) stringer test2\n}"
v3sp := v3s
if spew.UnsafeDisabled {
v3s = "{\n s: (" + v3t2 + ") (len=4) \"test\",\n S: (" + v3t2 +
") (len=5) \"test2\"\n}"
v3sp = "{\n s: (" + v3t2 + ") (len=4) \"test\",\n S: (" + v3t2 +
") (len=5) stringer test2\n}"
}
addSdumpTest("struct of pstringer", v3, "("+v3t+") "+v3s)
addSdumpTest("struct of pstringer ptr", pv3, "(*"+v3t+")("+v3Addr+")("+v3sp+")")
addSdumpTest("struct of pstringer 2ptr", &pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")("+v3sp+")")
addSdumpTest("struct of pstringer nil ptr", nv3, "(*"+v3t+")()")
// Struct that contains embedded struct and field to same struct.
e := embed{"embedstr"}
eLen := fmt.Sprintf("%d", len("embedstr"))
v4 := embedwrap{embed: &e, e: &e}
nv4 := (*embedwrap)(nil)
pv4 := &v4
eAddr := fmt.Sprintf("%p", &e)
v4Addr := fmt.Sprintf("%p", pv4)
pv4Addr := fmt.Sprintf("%p", &pv4)
v4t := "spew_test.embedwrap"
v4t2 := "spew_test.embed"
v4t3 := "string"
v4s := "{\n embed: (*" + v4t2 + ")(" + eAddr + ")({\n a: (" + v4t3 +
") (len=" + eLen + ") \"embedstr\"\n }),\n e: (*" + v4t2 +
")(" + eAddr + ")({\n a: (" + v4t3 + ") (len=" + eLen + ")" +
" \"embedstr\"\n })\n}"
addSdumpTest("embedded struct", v4, "("+v4t+") "+v4s)
addSdumpTest("embedded struct ptr", pv4, "(*"+v4t+")("+v4Addr+")("+v4s+")")
addSdumpTest("embedded struct 2ptr", &pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")("+v4s+")")
addSdumpTest("embedded struct nil ptr", nv4, "(*"+v4t+")()")
}
func addUintptrSdumpTests() {
// Null pointer.
v := uintptr(0)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "uintptr"
vs := ""
addSdumpTest("uintptr", v, "("+vt+") "+vs)
addSdumpTest("uintptr ptr", pv, "(*"+vt+")("+vAddr+")("+vs+")")
addSdumpTest("uintptr 2ptr", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")")
// Address of real variable.
i := 1
v2 := uintptr(unsafe.Pointer(&i))
nv2 := (*uintptr)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "uintptr"
v2s := fmt.Sprintf("%p", &i)
addSdumpTest("real uintptr", v2, "("+v2t+") "+v2s)
addSdumpTest("real uintptr ptr", pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")")
addSdumpTest("real uintptr 2ptr", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")")
addSdumpTest("real uintptr nil ptr", nv2, "(*"+v2t+")()")
}
func addUnsafePointerSdumpTests() {
// Null pointer.
v := unsafe.Pointer(nil)
nv := (*unsafe.Pointer)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "unsafe.Pointer"
vs := ""
addSdumpTest("unsafe.Pointer", v, "("+vt+") "+vs)
addSdumpTest("unsafe.Pointer ptr", pv, "(*"+vt+")("+vAddr+")("+vs+")")
addSdumpTest("unsafe.Pointer 2ptr", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")")
addSdumpTest("unsafe.Pointer nil ptr", nv, "(*"+vt+")()")
// Address of real variable.
i := 1
v2 := unsafe.Pointer(&i)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "unsafe.Pointer"
v2s := fmt.Sprintf("%p", &i)
addSdumpTest("real unsafe.Pointer", v2, "("+v2t+") "+v2s)
addSdumpTest("real unsafe.Pointer ptr", pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")")
addSdumpTest("real unsafe.Pointer 2ptr", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")")
addSdumpTest("real unsafe.Pointer nil ptr", nv, "(*"+vt+")()")
}
func addChanSdumpTests() {
// Nil channel.
var v chan int
pv := &v
nv := (*chan int)(nil)
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "chan int"
vs := ""
addSdumpTest("nil chan", v, "("+vt+") "+vs)
addSdumpTest("nil chan ptr", pv, "(*"+vt+")("+vAddr+")("+vs+")")
addSdumpTest("nil chan 2ptr", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")")
addSdumpTest("nil chan nil ptr", nv, "(*"+vt+")()")
// Real channel.
v2 := make(chan int)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "chan int"
v2s := fmt.Sprintf("%p", v2)
addSdumpTest("chan", v2, "("+v2t+") "+v2s)
addSdumpTest("chan ptr", pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")")
addSdumpTest("chan 2ptr", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")")
}
func addFuncSdumpTests() {
// Function with no params and no returns.
v := addIntSdumpTests
nv := (*func())(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "func()"
vs := fmt.Sprintf("%p", v)
addSdumpTest("func()", v, "("+vt+") "+vs)
addSdumpTest("func() ptr", pv, "(*"+vt+")("+vAddr+")("+vs+")")
addSdumpTest("func() 2ptr", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")")
addSdumpTest("func() nil ptr", nv, "(*"+vt+")()")
// Function with param and no returns.
v2 := TestSdump
nv2 := (*func(*testing.T))(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "func(*testing.T)"
v2s := fmt.Sprintf("%p", v2)
addSdumpTest("func(P)", v2, "("+v2t+") "+v2s)
addSdumpTest("func(P) ptr", pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")")
addSdumpTest("func(P) 2ptr", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")")
addSdumpTest("func(P) nil ptr", nv2, "(*"+v2t+")()")
// Function with multiple params and multiple returns.
v3 := func(i int, s string) (b bool, err error) {
return true, nil
}
nv3 := (*func(int, string) (bool, error))(nil)
pv3 := &v3
v3Addr := fmt.Sprintf("%p", pv3)
pv3Addr := fmt.Sprintf("%p", &pv3)
v3t := "func(int, string) (bool, error)"
v3s := fmt.Sprintf("%p", v3)
addSdumpTest("func(P)R", v3, "("+v3t+") "+v3s)
addSdumpTest("func(P)R ptr", pv3, "(*"+v3t+")("+v3Addr+")("+v3s+")")
addSdumpTest("func(P)R 2ptr", &pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")("+v3s+")")
addSdumpTest("func(P)R nil ptr", nv3, "(*"+v3t+")()")
}
func addCircularSdumpTests() {
// Struct that is circular through self referencing.
type circular struct {
c *circular
}
v := circular{nil}
v.c = &v
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "spew_test.circular"
vs := "{\n c: (*" + vt + ")(" + vAddr + ")({\n c: (*" + vt + ")(" +
vAddr + ")()\n })\n}"
vs2 := "{\n c: (*" + vt + ")(" + vAddr + ")()\n}"
addSdumpTest("circ struct", v, "("+vt+") "+vs)
addSdumpTest("circ struct ptr", pv, "(*"+vt+")("+vAddr+")("+vs2+")")
addSdumpTest("circ struct 2ptr", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs2+")")
// Structs that are circular through cross referencing.
v2 := xref1{nil}
ts2 := xref2{&v2}
v2.ps2 = &ts2
pv2 := &v2
ts2Addr := fmt.Sprintf("%p", &ts2)
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "spew_test.xref1"
v2t2 := "spew_test.xref2"
v2s := "{\n ps2: (*" + v2t2 + ")(" + ts2Addr + ")({\n ps1: (*" + v2t +
")(" + v2Addr + ")({\n ps2: (*" + v2t2 + ")(" + ts2Addr +
")()\n })\n })\n}"
v2s2 := "{\n ps2: (*" + v2t2 + ")(" + ts2Addr + ")({\n ps1: (*" + v2t +
")(" + v2Addr + ")()\n })\n}"
addSdumpTest("circ X struct", v2, "("+v2t+") "+v2s)
addSdumpTest("circ X struct ptr", pv2, "(*"+v2t+")("+v2Addr+")("+v2s2+")")
addSdumpTest("circ X struct 2ptr", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s2+")")
// Structs that are indirectly circular.
v3 := indirCir1{nil}
tic2 := indirCir2{nil}
tic3 := indirCir3{&v3}
tic2.ps3 = &tic3
v3.ps2 = &tic2
pv3 := &v3
tic2Addr := fmt.Sprintf("%p", &tic2)
tic3Addr := fmt.Sprintf("%p", &tic3)
v3Addr := fmt.Sprintf("%p", pv3)
pv3Addr := fmt.Sprintf("%p", &pv3)
v3t := "spew_test.indirCir1"
v3t2 := "spew_test.indirCir2"
v3t3 := "spew_test.indirCir3"
v3s := "{\n ps2: (*" + v3t2 + ")(" + tic2Addr + ")({\n ps3: (*" + v3t3 +
")(" + tic3Addr + ")({\n ps1: (*" + v3t + ")(" + v3Addr +
")({\n ps2: (*" + v3t2 + ")(" + tic2Addr +
")()\n })\n })\n })\n}"
v3s2 := "{\n ps2: (*" + v3t2 + ")(" + tic2Addr + ")({\n ps3: (*" + v3t3 +
")(" + tic3Addr + ")({\n ps1: (*" + v3t + ")(" + v3Addr +
")()\n })\n })\n}"
addSdumpTest("circ indir struct", v3, "("+v3t+") "+v3s)
addSdumpTest("circ indir struct ptr", pv3, "(*"+v3t+")("+v3Addr+")("+v3s2+")")
addSdumpTest("circ indir struct 2ptr", &pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")("+v3s2+")")
}
func addPanicSdumpTests() {
// Type that panics in its fmt.Stringer interface.
v := panicer(127)
nv := (*panicer)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "spew_test.panicer"
vs := "(PANIC=test panic)127"
addSdumpTest("panicer", v, "("+vt+") "+vs)
addSdumpTest("panicer ptr", pv, "(*"+vt+")("+vAddr+")("+vs+")")
addSdumpTest("panicer 2ptr", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")")
addSdumpTest("panicer nil ptr", nv, "(*"+vt+")()")
}
func addErrorSdumpTests() {
// Type that has a custom Error interface.
v := customError(127)
nv := (*customError)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "spew_test.customError"
vs := "error: 127"
addSdumpTest("customError", v, "("+vt+") "+vs)
addSdumpTest("customError ptr", pv, "(*"+vt+")("+vAddr+")("+vs+")")
addSdumpTest("customError 2ptr", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")")
addSdumpTest("customError nil ptr", nv, "(*"+vt+")()")
}
// TestSdump executes all of the tests described by dumpTests.
func TestSdump(t *testing.T) {
// Setup tests.
addIntSdumpTests()
addUintSdumpTests()
addBoolSdumpTests()
addFloatSdumpTests()
addComplexSdumpTests()
addArraySdumpTests()
addSliceSdumpTests()
addStringSdumpTests()
addInterfaceSdumpTests()
addMapSdumpTests()
addStructSdumpTests()
addUintptrSdumpTests()
addUnsafePointerSdumpTests()
addChanSdumpTests()
addFuncSdumpTests()
addCircularSdumpTests()
addPanicSdumpTests()
addErrorSdumpTests()
addCgoSdumpTests()
t.Logf("Running %d tests", len(dumpTests))
for _, test := range dumpTests {
t.Run(test.name, func(t *testing.T) {
s := spew.Sdump(test.in)
if testFailed(s, test.wants) {
t.Errorf("%s\n got: %s\n%s", test.name, s, stringizeWants(test.wants))
}
})
}
}
go-testdeep-1.15.0/internal/spew/dumpcgo_test.go 0000664 0000000 0000000 00000007451 15144170453 0021647 0 ustar 00root root 0000000 0000000 // Copyright (c) 2013-2016 Dave Collins
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// NOTE: Due to the following build constraints, this file will only be compiled
// when both cgo is supported and "-tags testcgo" is added to the go test
// command line. This means the cgo tests are only added (and hence run) when
// specifically requested. This configuration is used because spew itself
// does not require cgo to run even though it does handle certain cgo types
// specially. Rather than forcing all clients to require cgo and an external
// C compiler just to run the tests, this scheme makes them optional.
//go:build cgo && testcgo
// +build cgo,testcgo
package spew_test
import (
"fmt"
"github.com/maxatome/go-testdeep/internal/spew/testdata"
)
func addCgoSdumpTests() {
// C char pointer.
v := testdata.GetCgoCharPointer()
nv := testdata.GetCgoNullCharPointer()
pv := &v
vcAddr := fmt.Sprintf("%p", v)
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "*testdata._Ctype_char"
vs := "116"
addSdumpTest("C char*", v, "("+vt+")("+vcAddr+")("+vs+")")
addSdumpTest("C char* ptr", pv, "(*"+vt+")("+vAddr+"->"+vcAddr+")("+vs+")")
addSdumpTest("C char* 2ptr", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+"->"+vcAddr+")("+vs+")")
addSdumpTest("C char* nil ptr", nv, "("+vt+")()")
// C char array.
v2, v2l := testdata.GetCgoCharArray()
v2Len := fmt.Sprintf("%d", v2l)
v2t := "[6]testdata._Ctype_char"
v2s := "(len=" + v2Len + ") " +
"{\n 00000000 74 65 73 74 32 00 " +
" |test2.|\n}"
addSdumpTest("C char[]", v2, "("+v2t+") "+v2s)
// C unsigned char array.
v3, v3l := testdata.GetCgoUnsignedCharArray()
v3Len := fmt.Sprintf("%d", v3l)
v3t := "[6]testdata._Ctype_unsignedchar"
v3t2 := "[6]testdata._Ctype_uchar"
v3s := "(len=" + v3Len + ") " +
"{\n 00000000 74 65 73 74 33 00 " +
" |test3.|\n}"
addSdumpTest("C unsigned char[]", v3, "("+v3t+") "+v3s+"\n", "("+v3t2+") "+v3s)
// C signed char array.
v4, v4l := testdata.GetCgoSignedCharArray()
v4Len := fmt.Sprintf("%d", v4l)
v4t := "[6]testdata._Ctype_schar"
v4t2 := "testdata._Ctype_schar"
v4s := "(len=" + v4Len + ") " +
"{\n (" + v4t2 + ") 116,\n (" + v4t2 + ") 101,\n (" + v4t2 +
") 115,\n (" + v4t2 + ") 116,\n (" + v4t2 + ") 52,\n (" + v4t2 +
") 0\n}"
addSdumpTest("C signed char[]", v4, "("+v4t+") "+v4s)
// C uint8_t array.
v5, v5l := testdata.GetCgoUint8tArray()
v5Len := fmt.Sprintf("%d", v5l)
v5t := "[6]testdata._Ctype_uint8_t"
v5t2 := "[6]testdata._Ctype_uchar"
v5s := "(len=" + v5Len + ") " +
"{\n 00000000 74 65 73 74 35 00 " +
" |test5.|\n}"
addSdumpTest("C uint8_t[]", v5, "("+v5t+") "+v5s+"\n", "("+v5t2+") "+v5s)
// C typedefed unsigned char array.
v6, v6l := testdata.GetCgoTypdefedUnsignedCharArray()
v6Len := fmt.Sprintf("%d", v6l)
v6t := "[6]testdata._Ctype_custom_uchar_t"
v6t2 := "[6]testdata._Ctype_uchar"
v6s := "(len=" + v6Len + ") " +
"{\n 00000000 74 65 73 74 36 00 " +
" |test6.|\n}"
addSdumpTest("C custom_uchar_t[]", v6, "("+v6t+") "+v6s+"\n", "("+v6t2+") "+v6s)
}
go-testdeep-1.15.0/internal/spew/dumpnocgo_test.go 0000664 0000000 0000000 00000002341 15144170453 0022175 0 ustar 00root root 0000000 0000000 // Copyright (c) 2013 Dave Collins
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// NOTE: Due to the following build constraints, this file will only be compiled
// when either cgo is not supported or "-tags testcgo" is not added to the go
// test command line. This file intentionally does not setup any cgo tests in
// this scenario.
//go:build !cgo || !testcgo
// +build !cgo !testcgo
package spew_test
func addCgoSdumpTests() {
// Don't add any tests for cgo since this file is only compiled when
// there should not be any cgo tests.
}
go-testdeep-1.15.0/internal/spew/example_test.go 0000664 0000000 0000000 00000005460 15144170453 0021642 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2013-2016 Dave Collins
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew_test
import (
"fmt"
"github.com/maxatome/go-testdeep/internal/spew"
)
type Flag int
const (
flagOne Flag = iota
flagTwo
)
var flagStrings = map[Flag]string{
flagOne: "flagOne",
flagTwo: "flagTwo",
}
func (f Flag) String() string {
if s, ok := flagStrings[f]; ok {
return s
}
return fmt.Sprintf("Unknown flag (%d)", int(f))
}
type Bar struct {
data uintptr
}
type Foo struct {
unexportedField Bar
ExportedField map[any]any
}
// This example demonstrates how to use Sdump to dump variables to stdout.
func ExampleSdump() {
// The following package level declarations are assumed for this example:
/*
type Flag int
const (
flagOne Flag = iota
flagTwo
)
var flagStrings = map[Flag]string{
flagOne: "flagOne",
flagTwo: "flagTwo",
}
func (f Flag) String() string {
if s, ok := flagStrings[f]; ok {
return s
}
return fmt.Sprintf("Unknown flag (%d)", int(f))
}
type Bar struct {
data uintptr
}
type Foo struct {
unexportedField Bar
ExportedField map[any]any
}
*/
// Setup some sample data structures for the example.
bar := Bar{uintptr(0x1234)}
s1 := Foo{bar, map[any]any{"one": true}}
f := Flag(5)
b := []byte{
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30,
0x31, 0x32,
}
// Dump!
fmt.Println(spew.Sdump(s1))
fmt.Println(spew.Sdump(f))
fmt.Println(spew.Sdump(b))
// Output:
// (spew_test.Foo) {
// unexportedField: (spew_test.Bar) {
// data: (uintptr) 0x1234
// },
// ExportedField: (map[interface {}]interface {}) (len=1) {
// (string) (len=3) "one": (bool) true
// }
// }
// (spew_test.Flag) Unknown flag (5)
// ([]uint8) (len=34) {
// 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... |
// 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0|
// 00000020 31 32 |12|
// }
//
}
go-testdeep-1.15.0/internal/spew/internal_test.go 0000664 0000000 0000000 00000003143 15144170453 0022017 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2013-2016 Dave Collins
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
This test file is part of the spew package rather than than the spew_test
package because it needs access to internals to properly test certain cases
which are not possible via the public interface since they should never happen.
*/
package spew
import (
"reflect"
"strings"
"testing"
)
// TestInvalidReflectValue ensures the dump and formatter code handles an
// invalid reflect value properly. This needs access to internal state since it
// should never happen in real code and therefore can't be tested via the public
// API.
func TestInvalidReflectValue(t *testing.T) {
// Dump invalid reflect value.
v := new(reflect.Value)
var buf strings.Builder
d := dumpState{w: &buf, cs: &Config}
d.dump(*v)
s := buf.String()
want := ""
if s != want {
t.Errorf("InvalidReflectValue\n got: %s want: %s", s, want)
}
}
go-testdeep-1.15.0/internal/spew/internalunsafe_test.go 0000664 0000000 0000000 00000005151 15144170453 0023222 0 ustar 00root root 0000000 0000000 // Copyright (c) 2013-2016 Dave Collins
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// NOTE: Due to the following build constraints, this file will only be compiled
// when the code is not running on Google App Engine, compiled by GopherJS, and
// "-tags safe" is not added to the go build command line. The "disableunsafe"
// tag is deprecated and thus should not be used.
//go:build !js && !appengine && !safe && !disableunsafe
// +build !js,!appengine,!safe,!disableunsafe
/*
This test file is part of the spew package rather than than the spew_test
package because it needs access to internals to properly test certain cases
which are not possible via the public interface since they should never happen.
*/
package spew
import (
"reflect"
"strings"
"testing"
)
// changeKind uses unsafe to intentionally change the kind of a reflect.Value to
// the maximum kind value which does not exist. This is needed to test the
// fallback code which punts to the standard fmt library for new types that
// might get added to the language.
func changeKind(v *reflect.Value, readOnly bool) {
flags := flagField(v)
if readOnly {
*flags |= flagRO
} else {
*flags &^= flagRO
}
*flags |= flagKindMask
}
// TestAddedReflectValue tests functionally of the dump and formatter code which
// falls back to the standard fmt library for new types that might get added to
// the language.
func TestAddedReflectValue(t *testing.T) {
// Dump using a reflect.Value that is exported.
v := reflect.ValueOf(int8(5))
changeKind(&v, false)
var buf strings.Builder
d := dumpState{w: &buf, cs: &Config}
d.dump(v)
s := buf.String()
want := "(int8) 5"
if s != want {
t.Errorf("TestAddedReflectValue\n got: %s want: %s", s, want)
}
// Dump using a reflect.Value that is not exported.
changeKind(&v, true)
buf.Reset()
d.dump(v)
s = buf.String()
want = "(int8) "
if s != want {
t.Errorf("TestAddedReflectValue\n got: %s want: %s", s, want)
}
}
go-testdeep-1.15.0/internal/spew/spew_test.go 0000664 0000000 0000000 00000011014 15144170453 0021155 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2013-2016 Dave Collins
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew_test
import (
"testing"
"github.com/maxatome/go-testdeep/internal/spew"
"github.com/maxatome/go-testdeep/internal/test"
)
// spewTest is used to describe a test to be performed against the public
// functions of the spew package or ConfigState.
type spewTest struct {
cs *spew.ConfigState
in any
want string
}
// spewTests houses the tests to be performed against the public functions of
// the spew package and ConfigState.
//
// These tests are only intended to ensure the public functions are exercised
// and are intentionally not exhaustive of types. The exhaustive type
// tests are handled in the dump and format tests.
var spewTests []spewTest
func initSpewTests() {
scsNoMethods := &spew.ConfigState{
Indent: " ",
DisableMethods: true,
DisablePointerAddresses: true,
}
scsNoPmethods := &spew.ConfigState{
Indent: " ",
DisablePointerMethods: true,
DisablePointerAddresses: true,
}
scsMaxDepth := &spew.ConfigState{
Indent: " ",
MaxDepth: 1,
DisablePointerAddresses: true,
}
scsNoPtrAddr := &spew.ConfigState{DisablePointerAddresses: true}
scsCap := &spew.ConfigState{EnableCapacities: true}
// Variables for tests on types which implement fmt.Stringer
// interface with and without a pointer receiver.
ts := stringer("test")
tps := pstringer("test")
type ptrTester struct {
s *struct{}
}
tptr := &ptrTester{s: &struct{}{}}
// depthTester is used to test max depth handling for structs, array, slices
// and maps.
type depthTester struct {
ic indirCir1
arr [1]string
slice []string
m map[string]int
}
dt := depthTester{
indirCir1{&indirCir2{}},
[1]string{"arr"},
[]string{"slice"},
map[string]int{"one": 1},
}
dz := depthTester{}
spewTests = []spewTest{
{nil, int8(127), "(int8) 127"},
{nil, int16(32767), "(int16) 32767"},
{nil, 2147483647, "(int) 2147483647"},
{nil, int64(9223372036854775807), "(int64) 9223372036854775807"},
{nil, uint8(64), "(uint8) 64"},
{nil, complex(1, 2), "(complex128) (1+2i)"},
{nil, complex(float64(5), 6), "(complex128) (5+6i)"},
{nil, float32(3.14), "(float32) 3.14"},
{nil, float64(6.28), "(float64) 6.28"},
{nil, true, "(bool) true"},
{nil, false, "(bool) false"},
{nil, complex(-10, -20), "(complex128) (-10-20i)"},
{nil, dz, "(spew_test.depthTester) {\n}"},
{scsMaxDepth, dt, `(spew_test.depthTester) {
ic: (spew_test.indirCir1) {
},
arr: ([1]string) (len=1) {
},
slice: ([]string) (len=1) {
},
m: (map[string]int) (len=1) {
}
}`},
// scsNoMethods
{scsNoMethods, ts, `(spew_test.stringer) (len=4) "test"`},
{scsNoMethods, &ts, `(*spew_test.stringer)((len=4) "test")`},
{scsNoMethods, tps, `(spew_test.pstringer) (len=4) "test"`},
{scsNoMethods, &tps, `(*spew_test.pstringer)((len=4) "test")`},
// scsNoPmethods
{scsNoPmethods, ts, "(spew_test.stringer) (len=4) stringer test"},
{scsNoPmethods, &ts, "(*spew_test.stringer)((len=4) stringer test)"},
{scsNoPmethods, tps, `(spew_test.pstringer) (len=4) "test"`},
{scsNoPmethods, &tps, "(*spew_test.pstringer)((len=4) stringer test)"},
{scsNoPtrAddr, tptr, "(*spew_test.ptrTester)({\ns: (*struct {})({\n})\n})"},
{scsCap, make([]string, 0, 10), "([]string) (cap=10) {\n}"},
{scsCap, make([]string, 1, 10), "([]string) (len=1 cap=10) {\n(string) \"\"\n}"},
}
}
// TestSpew executes all of the tests described by spewTests.
func TestSpew(t *testing.T) {
initSpewTests()
t.Logf("Running %d tests", len(spewTests))
for i, tc := range spewTests {
var cs []spew.ConfigState
if tc.cs != nil {
cs = append(cs, *tc.cs)
}
str := spew.Sdump(tc.in, cs...)
test.EqualStr(t, str, tc.want, "#%d", i)
}
}
go-testdeep-1.15.0/internal/spew/testdata/ 0000775 0000000 0000000 00000000000 15144170453 0020425 5 ustar 00root root 0000000 0000000 go-testdeep-1.15.0/internal/spew/testdata/any.go 0000664 0000000 0000000 00000000424 15144170453 0021543 0 ustar 00root root 0000000 0000000 // Copyright (c) 2025, 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 testdata
type any = interface{}
go-testdeep-1.15.0/internal/spew/testdata/dumpcgo.go 0000664 0000000 0000000 00000006140 15144170453 0022413 0 ustar 00root root 0000000 0000000 // Copyright (c) 2013 Dave Collins
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// NOTE: Due to the following build constraints, this file will only be compiled
// when both cgo is supported and "-tags testcgo" is added to the go test
// command line. This code should really only be in the dumpcgo_test.go file,
// but unfortunately Go will not allow cgo in test files, so this is a
// workaround to allow cgo types to be tested. This configuration is used
// because spew itself does not require cgo to run even though it does handle
// certain cgo types specially. Rather than forcing all clients to require cgo
// and an external C compiler just to run the tests, this scheme makes them
// optional.
//go:build cgo && testcgo
// +build cgo,testcgo
package testdata
/*
#include
typedef unsigned char custom_uchar_t;
char *ncp = 0;
char *cp = "test";
char ca[6] = {'t', 'e', 's', 't', '2', '\0'};
unsigned char uca[6] = {'t', 'e', 's', 't', '3', '\0'};
signed char sca[6] = {'t', 'e', 's', 't', '4', '\0'};
uint8_t ui8ta[6] = {'t', 'e', 's', 't', '5', '\0'};
custom_uchar_t tuca[6] = {'t', 'e', 's', 't', '6', '\0'};
*/
import "C"
// GetCgoNullCharPointer returns a null char pointer via cgo. This is only
// used for tests.
func GetCgoNullCharPointer() any {
return C.ncp
}
// GetCgoCharPointer returns a char pointer via cgo. This is only used for
// tests.
func GetCgoCharPointer() any {
return C.cp
}
// GetCgoCharArray returns a char array via cgo and the array's len and cap.
// This is only used for tests.
func GetCgoCharArray() (any, int) {
return C.ca, len(C.ca)
}
// GetCgoUnsignedCharArray returns an unsigned char array via cgo and the
// array's len and cap. This is only used for tests.
func GetCgoUnsignedCharArray() (any, int) {
return C.uca, len(C.uca)
}
// GetCgoSignedCharArray returns a signed char array via cgo and the array's len
// and cap. This is only used for tests.
func GetCgoSignedCharArray() (any, int) {
return C.sca, len(C.sca)
}
// GetCgoUint8tArray returns a uint8_t array via cgo and the array's len and
// cap. This is only used for tests.
func GetCgoUint8tArray() (any, int) {
return C.ui8ta, len(C.ui8ta)
}
// GetCgoTypdefedUnsignedCharArray returns a typedefed unsigned char array via
// cgo and the array's len and cap. This is only used for tests.
func GetCgoTypdefedUnsignedCharArray() (any, int) {
return C.tuca, len(C.tuca)
}
go-testdeep-1.15.0/internal/test/ 0000775 0000000 0000000 00000000000 15144170453 0016615 5 ustar 00root root 0000000 0000000 go-testdeep-1.15.0/internal/test/any.go 0000664 0000000 0000000 00000000420 15144170453 0017727 0 ustar 00root root 0000000 0000000 // 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.
//go:build !go1.18
// +build !go1.18
package test
type any = interface{}
go-testdeep-1.15.0/internal/test/any_test.go 0000664 0000000 0000000 00000000425 15144170453 0020773 0 ustar 00root root 0000000 0000000 // 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.
//go:build !go1.18
// +build !go1.18
package test_test
type any = interface{}
go-testdeep-1.15.0/internal/test/check.go 0000664 0000000 0000000 00000006611 15144170453 0020225 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 test
import (
"regexp"
"strings"
"testing"
"unicode"
"github.com/maxatome/go-testdeep/helpers/tdutil"
"github.com/maxatome/go-testdeep/internal/spew"
)
// EqualErrorMessage prints a test error message of the form:
//
// Message
// Failed test
// got: got_value
// expected: expected_value
func EqualErrorMessage(t *testing.T, got, expected any,
args ...any,
) {
t.Helper()
testName := tdutil.BuildTestName(args...)
if testName != "" {
testName = " '" + testName + "'"
}
t.Errorf(`Failed test%s
got: %v
expected: %v`,
testName, got, expected)
}
func spewIfNeeded(s string) string {
for _, chr := range s {
if !unicode.IsPrint(chr) {
return spew.Sdump(s)
}
}
return s
}
// EqualStr checks that got equals expected.
func EqualStr(t *testing.T, got, expected string, args ...any) bool {
if got == expected {
return true
}
t.Helper()
EqualErrorMessage(t, spewIfNeeded(got), spewIfNeeded(expected), args...)
return false
}
// MatchStr checks that got matches expected regexp.
func MatchStr(t *testing.T, got, expectedRe string, args ...any) bool {
re := regexp.MustCompile(expectedRe)
if re.MatchString(got) {
return true
}
t.Helper()
EqualErrorMessage(t, spewIfNeeded(got), spewIfNeeded(expectedRe), args...)
return false
}
// EqualInt checks that got equals expected.
func EqualInt(t *testing.T, got, expected int, args ...any) bool {
if got == expected {
return true
}
t.Helper()
EqualErrorMessage(t, got, expected, args...)
return false
}
// EqualBool checks that got equals expected.
func EqualBool(t *testing.T, got, expected bool, args ...any) bool {
if got == expected {
return true
}
t.Helper()
EqualErrorMessage(t, got, expected, args...)
return false
}
// IsTrue checks that got is true.
func IsTrue(t *testing.T, got bool, args ...any) bool {
if got {
return true
}
t.Helper()
EqualErrorMessage(t, false, true, args...)
return false
}
// IsFalse checks that got is false.
func IsFalse(t *testing.T, got bool, args ...any) bool {
if !got {
return true
}
t.Helper()
EqualErrorMessage(t, true, false, args...)
return false
}
// CheckPanic checks that fn() panics and that the panic() arg is a
// string that contains contains.
func CheckPanic(t *testing.T, fn func(), contains string) bool {
t.Helper()
var (
panicked bool
panicParam any
)
func() {
defer func() { panicParam = recover() }()
panicked = true
fn()
panicked = false
}()
if !panicked {
t.Error("panic() did not occur")
return false
}
panicStr, ok := panicParam.(string)
if !ok {
t.Errorf("panic() occurred but recover()d %T type (%v) instead of string",
panicParam, panicParam)
return false
}
if !strings.Contains(panicStr, contains) {
t.Errorf("panic() string `%s'\ndoes not contain `%s'", panicStr, contains)
return false
}
return true
}
// NoError checks that err is nil.
func NoError(t *testing.T, err error, args ...any) bool {
if err == nil {
return true
}
t.Helper()
EqualErrorMessage(t, err, nil, args...)
return false
}
// Error checks that err is non-nil.
func Error(t *testing.T, err error, args ...any) bool {
if err != nil {
return true
}
t.Helper()
EqualErrorMessage(t, err, "", args...)
return false
}
go-testdeep-1.15.0/internal/test/types.go 0000664 0000000 0000000 00000012025 15144170453 0020310 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 test
import (
"fmt"
"runtime"
"strings"
"testing"
"github.com/maxatome/go-testdeep/internal/trace"
)
// TestingT is a type implementing td.TestingT intended to be used in
// tests.
type TestingT struct {
Messages []string
IsFatal bool
HasFailed bool
}
type testingFatal string
// NewTestingT returns a new instance of [*TestingT].
func NewTestingT() *TestingT {
return &TestingT{}
}
// Error mocks [testing.T] Error method.
func (t *TestingT) Error(args ...any) {
t.Messages = append(t.Messages, fmt.Sprint(args...))
t.IsFatal = false
t.HasFailed = true
}
// Fatal mocks [testing.T.Fatal] method.
func (t *TestingT) Fatal(args ...any) {
t.Messages = append(t.Messages, fmt.Sprint(args...))
t.IsFatal = true
t.HasFailed = true
panic(testingFatal(t.Messages[len(t.Messages)-1]))
}
func (t *TestingT) CatchFatal(fn func()) (fatalStr string) {
panicked := true
trace.IgnorePackage()
defer func() {
trace.UnignorePackage()
if panicked {
if x := recover(); x != nil {
if str, ok := x.(testingFatal); ok {
fatalStr = string(str)
} else {
panic(x) // rethrow
}
}
}
}()
fn()
panicked = false
return
}
// ContainsMessages checks expectedMsgs are all present in Messages, in
// this order. It stops when a message is not found and returns the
// remaining messages.
func (t *TestingT) ContainsMessages(expectedMsgs ...string) []string {
curExp := 0
for _, msg := range t.Messages {
for {
if curExp == len(expectedMsgs) {
return nil
}
pos := strings.Index(msg, expectedMsgs[curExp])
if pos < 0 {
break
}
msg = msg[pos+len(expectedMsgs[curExp]):]
curExp++
}
}
return expectedMsgs[curExp:]
}
// Helper mocks [testing.T.Helper] method.
func (t *TestingT) Helper() {
// Do nothing
}
// LastMessage returns the last message.
func (t *TestingT) LastMessage() string {
if len(t.Messages) == 0 {
return ""
}
return t.Messages[len(t.Messages)-1]
}
// ResetMessages resets the messages.
func (t *TestingT) ResetMessages() {
t.Messages = t.Messages[:0]
}
// TestingTB is a type implementing [testing.TB] intended to be used in
// tests.
type TestingTB struct {
TestingT
name string
testing.TB
cleanup func()
}
// NewTestingTB returns a new instance of [*TestingTB].
func NewTestingTB(name string) *TestingTB {
return &TestingTB{name: name}
}
// Cleanup mocks [testing.T.Cleanup] method. Not thread-safe but we
// don't care in tests.
func (t *TestingTB) Cleanup(fn func()) {
old := t.cleanup
t.cleanup = func() {
if old != nil {
defer old()
}
fn()
}
runtime.SetFinalizer(t, func(t *TestingTB) { t.cleanup() })
}
// Fatal mocks [testing.T.Error] method.
func (t *TestingTB) Error(args ...any) {
t.TestingT.Error(args...)
}
// Errorf mocks [testing.T.Errorf] method.
func (t *TestingTB) Errorf(format string, args ...any) {
t.TestingT.Error(fmt.Sprintf(format, args...))
}
// Fail mocks [testing.T.Fail] method.
func (t *TestingTB) Fail() {
t.HasFailed = true
}
// FailNow mocks [testing.T.FailNow] method.
func (t *TestingTB) FailNow() {
t.HasFailed = true
t.IsFatal = true
}
// Failed mocks [testing.T.Failed] method.
func (t *TestingTB) Failed() bool {
return t.HasFailed
}
// Fatal mocks [testing.T.Fatal] method.
func (t *TestingTB) Fatal(args ...any) {
t.TestingT.Fatal(args...)
}
// Fatalf mocks [testing.T.Fatalf] method.
func (t *TestingTB) Fatalf(format string, args ...any) {
t.TestingT.Fatal(fmt.Sprintf(format, args...))
}
// Helper mocks [testing.T.Helper] method.
func (t *TestingTB) Helper() {
// Do nothing
}
// Log mocks [testing.T.Log] method.
func (t *TestingTB) Log(args ...any) {
t.Messages = append(t.Messages, fmt.Sprint(args...))
}
// Logf mocks [testing.T.Logf] method.
func (t *TestingTB) Logf(format string, args ...any) {
t.Log(fmt.Sprintf(format, args...))
}
// Name mocks [testing.T.Name] method.
func (t *TestingTB) Name() string {
return t.name
}
// Skip mocks [testing.T.Skip] method.
func (t *TestingTB) Skip(args ...any) {}
// SkipNow mocks [testing.T.SkipNow] method.
func (t *TestingTB) SkipNow() {}
// Skipf mocks [testing.T.Skipf] method.
func (t *TestingTB) Skipf(format string, args ...any) {}
// Skipped mocks [testing.T.Skipped] method.
func (t *TestingTB) Skipped() bool {
return false
}
// ParallelTestingTB is a type implementing [testing.TB] and a
// Parallel() method intended to be used in tests.
type ParallelTestingTB struct {
IsParallel bool
*TestingTB
}
// NewParallelTestingTB returns a new instance of [*ParallelTestingTB].
func NewParallelTestingTB(name string) *ParallelTestingTB {
return &ParallelTestingTB{TestingTB: NewTestingTB(name)}
}
// Parallel mocks the [testing.T.Parallel] method. Not thread-safe.
func (t *ParallelTestingTB) Parallel() {
if t.IsParallel {
// testing.T.Parallel() panics if called multiple times for the
// same test.
panic("testing: t.Parallel called multiple times")
}
t.IsParallel = true
}
go-testdeep-1.15.0/internal/trace/ 0000775 0000000 0000000 00000000000 15144170453 0016734 5 ustar 00root root 0000000 0000000 go-testdeep-1.15.0/internal/trace/stack.go 0000664 0000000 0000000 00000003301 15144170453 0020365 0 ustar 00root root 0000000 0000000 // Copyright (c) 2021, 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 trace
import (
"fmt"
"io"
"strings"
)
// Level is a level when retrieving a stack trace.
type Level struct {
Package string
Func string
FileLine string
}
// Stack is a simple stack trace.
type Stack []Level
// Match returns true if the ith level of s matches pkg (if not empty)
// and any function in anyFunc.
//
// If anyFunc is empty, only the package is tested.
//
// If a function in anyFunc ends with "*", only the prefix is checked.
func (s Stack) Match(i int, pkg string, anyFunc ...string) bool {
if i < 0 {
i = len(s) + i
}
if i < 0 || i >= len(s) {
return false
}
level := s[i]
if pkg != "" && level.Package != pkg {
return false
}
if len(anyFunc) == 0 {
return true
}
for _, fn := range anyFunc {
if strings.HasSuffix(fn, "*") {
if strings.HasPrefix(level.Func, fn[:len(fn)-1]) {
return true
}
} else if level.Func == fn {
return true
}
}
return false
}
// IsRelevant returns true if the stack contains more than one level,
// or if the single level has a path with at least one directory.
func (s Stack) IsRelevant() bool {
return len(s) > 1 || (len(s) > 0 && strings.ContainsAny(s[0].FileLine, `/\`))
}
// Dump writes the stack to w.
func (s Stack) Dump(w io.Writer) {
fnMaxLen := 0
for _, level := range s {
if len(level.Func) > fnMaxLen {
fnMaxLen = len(level.Func)
}
}
fnMaxLen += 2
nl := ""
for _, level := range s {
fmt.Fprintf(w, "%s\t%-*s %s", nl, fnMaxLen, level.Func+"()", level.FileLine) //nolint: errcheck
nl = "\n"
}
}
go-testdeep-1.15.0/internal/trace/stack_test.go 0000664 0000000 0000000 00000003214 15144170453 0021427 0 ustar 00root root 0000000 0000000 // Copyright (c) 2021, 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 trace_test
import (
"bytes"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/internal/trace"
)
func TestStackMatch(t *testing.T) {
s := trace.Stack{
{Package: "A", Func: "Aaa.func1"},
{Package: "A", Func: "Aaa.func2"},
{Package: "B", Func: "Bbb"},
{Package: "C", Func: "Ccc"},
}
test.IsFalse(t, s.Match(100, "A"))
test.IsFalse(t, s.Match(-100, "A"))
test.IsFalse(t, s.Match(3, "B"))
test.IsFalse(t, s.Match(-1, "B"))
test.IsTrue(t, s.Match(3, "C"))
test.IsTrue(t, s.Match(-1, "C"))
test.IsFalse(t, s.Match(1, "A", "Aaa.func3", "Aaa.func1"))
test.IsTrue(t, s.Match(1, "A", "Aaa.func3", "Aaa.func2"))
test.IsTrue(t, s.Match(1, "A", "Aaa.func3", "Aaa.func*"))
}
func TestStackIsRelevant(t *testing.T) {
s := trace.Stack{}
test.IsFalse(t, s.IsRelevant())
s = trace.Stack{
{FileLine: "xxx.go:456"},
}
test.IsFalse(t, s.IsRelevant())
s = trace.Stack{
{FileLine: "xxx.go:456"},
{FileLine: "yyy.go:789"},
}
test.IsTrue(t, s.IsRelevant())
s = trace.Stack{
{FileLine: "xxx/yyy.go:456"},
}
test.IsTrue(t, s.IsRelevant())
s = trace.Stack{
{FileLine: `xxx\yyy.go:456`},
}
test.IsTrue(t, s.IsRelevant())
}
func TestStackDump(t *testing.T) {
s := trace.Stack{
{Func: "Pipo", FileLine: "xxx.go:456"},
{Func: "Bingo", FileLine: "yyy.go:789"},
}
b := bytes.NewBufferString("Stack:\n")
s.Dump(b)
test.EqualStr(t, b.String(), `Stack:
Pipo() xxx.go:456
Bingo() yyy.go:789`)
}
go-testdeep-1.15.0/internal/trace/trace.go 0000664 0000000 0000000 00000012151 15144170453 0020361 0 ustar 00root root 0000000 0000000 // Copyright (c) 2021, 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 trace
import (
"fmt"
"go/build"
"os"
"path/filepath"
"runtime"
"strings"
)
var (
ignorePkg = map[string]struct{}{}
goPaths []string
goModDir string
)
func getPackage(skip ...int) string {
sk := 2
if len(skip) > 0 {
sk += skip[0]
}
pc, _, _, ok := runtime.Caller(sk)
if ok {
fn := runtime.FuncForPC(pc)
if fn != nil {
pkg, _ := SplitPackageFunc(fn.Name())
return pkg
}
}
return ""
}
// IgnorePackage records the calling package as ignored one in trace.
func IgnorePackage(skip ...int) bool {
if pkg := getPackage(skip...); pkg != "" {
ignorePkg[pkg] = struct{}{}
return true
}
return false
}
// UnignorePackage cancels a previous use of [IgnorePackage], so the
// calling package is no longer ignored. Only intended to be used in
// go-testdeep internal tests.
func UnignorePackage(skip ...int) bool {
if pkg := getPackage(skip...); pkg != "" {
delete(ignorePkg, pkg)
return true
}
return false
}
// IsIgnoredPackage returns true if pkg is ignored, false
// otherwise. Only intended to be used in go-testdeep internal tests.
func IsIgnoredPackage(pkg string) (ok bool) {
_, ok = ignorePkg[pkg]
return
}
// FindGoModDir finds the closest directory containing go.mod file
// starting from directory in.
func FindGoModDir(in string) string {
for {
_, err := os.Stat(filepath.Join(in, "go.mod"))
if err == nil {
// Do not accept /tmp/go.mod
if in != os.TempDir() {
return in + string(filepath.Separator)
}
return ""
}
nd := filepath.Dir(in)
if nd == in {
return ""
}
in = nd
}
}
// FindGoModDirLinks finds the closest directory containing go.mod
// file starting from directory in after cleaning it. If not found,
// expands symlinks and re-searches.
func FindGoModDirLinks(in string) string {
in = filepath.Clean(in)
if gm := FindGoModDir(in); gm != "" {
return gm
}
lin, err := filepath.EvalSymlinks(in)
if err == nil && lin != in {
return FindGoModDir(lin)
}
return ""
}
// Reset resets the ignored packages map plus cached mod and GOPATH
// directories ([Init] should be called again). Only intended to be
// used in go-testdeep internal tests.
func Reset() {
ignorePkg = map[string]struct{}{}
goPaths = nil
goModDir = ""
}
// Init initializes trace global variables.
func Init() {
// GOPATH directories
goPaths = nil //nolint: prealloc
for _, dir := range filepath.SplitList(build.Default.GOPATH) {
dir = filepath.Clean(dir)
goPaths = append(goPaths,
filepath.Join(dir, "pkg", "mod")+string(filepath.Separator),
filepath.Join(dir, "src")+string(filepath.Separator),
)
}
if wd, err := os.Getwd(); err == nil {
// go.mod directory
goModDir = FindGoModDirLinks(wd)
}
}
// Frames is the interface corresponding to type returned by
// [runtime.CallersFrames]. See [CallersFrames] variable.
type Frames interface {
Next() (frame runtime.Frame, more bool)
}
// CallersFrames is only intended to be used in go-testdeep internal
// tests to cover all cases.
var CallersFrames = func(callers []uintptr) Frames {
return runtime.CallersFrames(callers)
}
// Retrieve retrieves a trace and returns it.
func Retrieve(skip int, endFunction string) Stack {
var trace Stack
var pc [40]uintptr
if num := runtime.Callers(skip+2, pc[:]); num > 0 {
checkIgnore := true
frames := CallersFrames(pc[:num])
for {
frame, more := frames.Next()
fn := frame.Function
if fn == endFunction {
break
}
var pkg string
if fn == "" {
if frame.File == "" {
if more {
continue
}
break
}
fn = ""
} else {
pkg, fn = SplitPackageFunc(fn)
if checkIgnore && IsIgnoredPackage(pkg) {
if more {
continue
}
break
}
checkIgnore = false
}
file := strings.TrimPrefix(frame.File, goModDir)
if file == frame.File {
for _, dir := range goPaths {
file = strings.TrimPrefix(frame.File, dir)
if file != frame.File {
break
}
}
if file == frame.File {
file = strings.TrimPrefix(frame.File, build.Default.GOROOT)
if file != frame.File {
file = filepath.Join("$GOROOT", file)
}
}
}
level := Level{
Package: pkg,
Func: fn,
}
if file != "" {
level.FileLine = fmt.Sprintf("%s:%d", file, frame.Line)
}
trace = append(trace, level)
if !more {
break
}
}
}
return trace
}
// SplitPackageFunc splits a fully qualified function name into its
// package and function parts:
//
// "foo/bar/test.fn" → "foo/bar/test", "fn"
// "foo/bar/test.X.fn" → "foo/bar/test", "X.fn"
// "foo/bar/test.(*X).fn" → "foo/bar/test", "(*X).fn"
// "foo/bar/test.(*X).fn.func1" → "foo/bar/test", "(*X).fn.func1"
// "weird" → "", "weird"
func SplitPackageFunc(fn string) (string, string) {
sp := strings.LastIndexByte(fn, '/')
if sp < 0 {
sp = 0 // std package
}
dp := strings.IndexByte(fn[sp:], '.')
if dp < 0 {
return "", fn
}
return fn[:sp+dp], fn[sp+dp+1:]
}
go-testdeep-1.15.0/internal/trace/trace_test.go 0000664 0000000 0000000 00000016470 15144170453 0021430 0 ustar 00root root 0000000 0000000 // Copyright (c) 2021, 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 trace_test
import (
"go/build"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/internal/trace"
)
func TestIgnorePackage(t *testing.T) {
const ourPkg = "github.com/maxatome/go-testdeep/internal/trace_test"
trace.Reset()
test.IsFalse(t, trace.IsIgnoredPackage(ourPkg))
test.IsTrue(t, trace.IgnorePackage())
test.IsTrue(t, trace.IsIgnoredPackage(ourPkg))
test.IsTrue(t, trace.UnignorePackage())
test.IsFalse(t, trace.IsIgnoredPackage(ourPkg))
test.IsTrue(t, trace.IgnorePackage())
test.IsTrue(t, trace.IsIgnoredPackage(ourPkg))
test.IsFalse(t, trace.IgnorePackage(300))
test.IsFalse(t, trace.UnignorePackage(300))
}
func TestFindGoModDir(t *testing.T) {
tmp, err := os.MkdirTemp("", "go-testdeep")
if err != nil {
t.Fatalf("TempDir() failed: %s", err)
}
final := filepath.Join(tmp, "a", "b", "c", "d", "e")
err = os.MkdirAll(final, 0755)
if err != nil {
t.Fatalf("MkdirAll(%s) failed: %s", final, err)
}
defer os.RemoveAll(tmp) //nolint: errcheck
test.EqualStr(t, trace.FindGoModDir(final), "")
t.Run("/tmp/.../a/b/c/go.mod", func(t *testing.T) {
goMod := filepath.Join(tmp, "a", "b", "c", "go.mod")
err := os.WriteFile(goMod, nil, 0644)
if err != nil {
t.Fatalf("WriteFile(%s) failed: %s", goMod, err)
}
defer os.Remove(goMod) //nolint: errcheck
test.EqualStr(t,
trace.FindGoModDir(final),
filepath.Join(tmp, "a", "b", "c")+string(filepath.Separator),
)
})
t.Run("/tmp/go.mod", func(t *testing.T) {
goMod := filepath.Join(os.TempDir(), "go.mod")
if _, err := os.Stat(goMod); err != nil {
if !os.IsNotExist(err) {
t.Fatalf("Stat(%s) failed: %s", goMod, err)
}
err := os.WriteFile(goMod, nil, 0644)
if err != nil {
t.Fatalf("WriteFile(%s) failed: %s", goMod, err)
}
defer os.Remove(goMod) //nolint: errcheck
}
test.EqualStr(t, trace.FindGoModDir(final), "")
})
}
func TestFindGoModDirLinks(t *testing.T) {
tmp, err := os.MkdirTemp("", "go-testdeep")
if err != nil {
t.Fatalf("TempDir() failed: %s", err)
}
goModDir := filepath.Join(tmp, "a", "b", "c")
truePath := filepath.Join(goModDir, "d", "e")
linkPath := filepath.Join(tmp, "a", "b", "e")
err = os.MkdirAll(truePath, 0755)
if err != nil {
t.Fatalf("MkdirAll(%s) failed: %s", truePath, err)
}
defer os.RemoveAll(tmp) //nolint: errcheck
err = os.Symlink(truePath, linkPath)
if err != nil {
t.Fatalf("Symlink(%s, %s) failed: %s", truePath, linkPath, err)
}
goMod := filepath.Join(goModDir, "go.mod")
err = os.WriteFile(goMod, nil, 0644)
if err != nil {
t.Fatalf("WriteFile(%s) failed: %s", goMod, err)
}
defer os.Remove(goMod) //nolint: errcheck
goModDir += string(filepath.Separator)
// Simple FindGoModDir
test.EqualStr(t, trace.FindGoModDir(truePath), goModDir)
test.EqualStr(t, trace.FindGoModDir(linkPath), "") // not found
// FindGoModDirLinks
test.EqualStr(t, trace.FindGoModDirLinks(truePath), goModDir)
test.EqualStr(t, trace.FindGoModDirLinks(linkPath), goModDir)
test.EqualStr(t, trace.FindGoModDirLinks(tmp), "")
}
func TestSplitPackageFunc(t *testing.T) {
pkg, fn := trace.SplitPackageFunc("testing.Fatal")
test.EqualStr(t, pkg, "testing")
test.EqualStr(t, fn, "Fatal")
pkg, fn = trace.SplitPackageFunc("github.com/maxatome/go-testdeep/td.Cmp")
test.EqualStr(t, pkg, "github.com/maxatome/go-testdeep/td")
test.EqualStr(t, fn, "Cmp")
pkg, fn = trace.SplitPackageFunc("foo/bar/test.(*T).Cmp")
test.EqualStr(t, pkg, "foo/bar/test")
test.EqualStr(t, fn, "(*T).Cmp")
pkg, fn = trace.SplitPackageFunc("foo/bar/test.(*X).c.func1")
test.EqualStr(t, pkg, "foo/bar/test")
test.EqualStr(t, fn, "(*X).c.func1")
pkg, fn = trace.SplitPackageFunc("foo/bar/test.(*X).c.func1")
test.EqualStr(t, pkg, "foo/bar/test")
test.EqualStr(t, fn, "(*X).c.func1")
pkg, fn = trace.SplitPackageFunc("foobar")
test.EqualStr(t, pkg, "")
test.EqualStr(t, fn, "foobar")
pkg, fn = trace.SplitPackageFunc("")
test.EqualStr(t, pkg, "")
test.EqualStr(t, fn, "")
}
func d(end string) []trace.Level { return trace.Retrieve(0, end) }
func c(end string) []trace.Level { return d(end) }
func b(end string) []trace.Level { return c(end) }
func a(end string) []trace.Level { return b(end) }
func TestZRetrieve(t *testing.T) {
trace.Reset()
levels := a("testing.tRunner")
if !test.EqualInt(t, len(levels), 5) ||
!test.EqualStr(t, levels[0].Func, "d") ||
!test.EqualStr(t, levels[0].Package, "github.com/maxatome/go-testdeep/internal/trace_test") ||
!test.EqualStr(t, levels[1].Func, "c") ||
!test.EqualStr(t, levels[1].Package, "github.com/maxatome/go-testdeep/internal/trace_test") ||
!test.EqualStr(t, levels[2].Func, "b") ||
!test.EqualStr(t, levels[2].Package, "github.com/maxatome/go-testdeep/internal/trace_test") ||
!test.EqualStr(t, levels[3].Func, "a") ||
!test.EqualStr(t, levels[3].Package, "github.com/maxatome/go-testdeep/internal/trace_test") ||
!test.EqualStr(t, levels[4].Func, "TestZRetrieve") ||
!test.EqualStr(t, levels[4].Package, "github.com/maxatome/go-testdeep/internal/trace_test") {
t.Errorf("%#v", levels)
}
levels = trace.Retrieve(0, "unknown.unknown")
maxLevels := len(levels)
test.IsTrue(t, maxLevels > 2)
test.EqualStr(t, levels[len(levels)-1].Func, "goexit") // runtime.goexit
for i := range levels {
test.IsTrue(t, trace.IgnorePackage(i))
}
levels = trace.Retrieve(0, "unknown.unknown")
test.EqualInt(t, len(levels), 0)
// Init GOPATH filter
trace.Reset()
trace.Init()
test.IsTrue(t, trace.IgnorePackage())
levels = trace.Retrieve(0, "unknown.unknown")
test.EqualInt(t, len(levels), maxLevels-1)
}
type FakeFrames struct {
frames []runtime.Frame
cur int
}
func (f *FakeFrames) Next() (runtime.Frame, bool) {
if f.cur >= len(f.frames) {
return runtime.Frame{}, false
}
f.cur++
return f.frames[f.cur-1], f.cur < len(f.frames)
}
func TestZRetrieveFake(t *testing.T) {
saveCallersFrames, saveGOPATH := trace.CallersFrames, build.Default.GOPATH
defer func() {
trace.CallersFrames, build.Default.GOPATH = saveCallersFrames, saveGOPATH
}()
var fakeFrames FakeFrames
trace.CallersFrames = func(_ []uintptr) trace.Frames { return &fakeFrames }
build.Default.GOPATH = "/foo/bar"
trace.Reset()
trace.Init()
fakeFrames = FakeFrames{
frames: []runtime.Frame{
{},
{Function: "", File: "/foo/bar/src/zip/zip.go", Line: 23},
{Function: "", File: "/foo/bar/pkg/mod/zzz/zzz.go", Line: 42},
{Function: "", File: "/bar/foo.go", Line: 34},
{Function: "pkg.MyFunc"},
{},
},
}
levels := trace.Retrieve(0, "pipo")
if test.EqualInt(t, len(levels), 4) {
test.EqualStr(t, levels[0].Func, "")
test.EqualStr(t, levels[0].Package, "")
test.EqualStr(t, levels[0].FileLine, "zip/zip.go:23")
test.EqualStr(t, levels[1].Func, "")
test.EqualStr(t, levels[1].Package, "")
test.EqualStr(t, levels[1].FileLine, "zzz/zzz.go:42")
test.EqualStr(t, levels[2].Func, "")
test.EqualStr(t, levels[2].Package, "")
test.EqualStr(t, levels[2].FileLine, "/bar/foo.go:34")
test.EqualStr(t, levels[3].Func, "MyFunc")
test.EqualStr(t, levels[3].Package, "pkg")
test.EqualStr(t, levels[3].FileLine, "")
} else {
t.Errorf("%#v", levels)
}
}
go-testdeep-1.15.0/internal/types/ 0000775 0000000 0000000 00000000000 15144170453 0017002 5 ustar 00root root 0000000 0000000 go-testdeep-1.15.0/internal/types/any.go 0000664 0000000 0000000 00000000421 15144170453 0020115 0 ustar 00root root 0000000 0000000 // 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.
//go:build !go1.18
// +build !go1.18
package types
type any = interface{}
go-testdeep-1.15.0/internal/types/any_test.go 0000664 0000000 0000000 00000000426 15144170453 0021161 0 ustar 00root root 0000000 0000000 // 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.
//go:build !go1.18
// +build !go1.18
package types_test
type any = interface{}
go-testdeep-1.15.0/internal/types/order.go 0000664 0000000 0000000 00000003171 15144170453 0020446 0 ustar 00root root 0000000 0000000 // Copyright (c) 2021-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 types
import (
"reflect"
"github.com/maxatome/go-testdeep/internal/dark"
)
// NewOrder returns a function able to compare 2 non-nil values of type t.
// It returns nil if the type t is not comparable.
func NewOrder(t reflect.Type) func(a, b reflect.Value) int {
// Compare(T) int
if m, ok := cmpMethod("Compare", t, Int); ok {
return func(va, vb reflect.Value) int {
// use dark.MustGetInterface() to bypass possible private fields
ret := m.Call([]reflect.Value{
reflect.ValueOf(dark.MustGetInterface(va)),
reflect.ValueOf(dark.MustGetInterface(vb)),
})
return int(ret[0].Int())
}
}
// Less(T) bool
if m, ok := cmpMethod("Less", t, Bool); ok {
return func(va, vb reflect.Value) int {
// use dark.MustGetInterface() to bypass possible private fields
va = reflect.ValueOf(dark.MustGetInterface(va))
vb = reflect.ValueOf(dark.MustGetInterface(vb))
ret := m.Call([]reflect.Value{va, vb})
if ret[0].Bool() { // a < b
return -1
}
ret = m.Call([]reflect.Value{vb, va})
if ret[0].Bool() { // b < a
return 1
}
return 0
}
}
return nil
}
func cmpMethod(name string, in, out reflect.Type) (reflect.Value, bool) {
if equal, ok := in.MethodByName(name); ok {
ft := equal.Type
if !ft.IsVariadic() &&
ft.NumIn() == 2 &&
ft.NumOut() == 1 &&
ft.In(0) == in &&
ft.In(1) == in &&
ft.Out(0) == out {
return equal.Func, true
}
}
return reflect.Value{}, false
}
go-testdeep-1.15.0/internal/types/order_test.go 0000664 0000000 0000000 00000004671 15144170453 0021513 0 ustar 00root root 0000000 0000000 // 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 types_test
import (
"reflect"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/internal/types"
)
type compareType int
func (i compareType) Compare(j compareType) int {
if i < j {
return -1
}
if i > j {
return 1
}
return 0
}
type lessType int
func (i lessType) Less(j lessType) bool {
return i < j
}
type badType1 int
func (i badType1) Compare(j ...badType1) int { return 0 } // IsVariadic()
func (i badType1) Less(j, k badType1) bool { return false } // NumIn() == 3
type badType2 int
func (i badType2) Compare() int { return 0 } // NumIn() == 1
func (i badType2) Less(j badType2) {} // NumOut() == 0
type badType3 int
func (i badType3) Compare(j badType3) (int, int) { return 0, 0 } // NumOut() == 2
func (i badType3) Less(j int) bool { return false } // In(1) ≠ in
type badType4 int
func (i badType4) Compare(j badType4) bool { return false } // Out(0) ≠ out
func (i badType4) Less(j badType4) int { return 0 } // Out(0) ≠ out
func TestOrder(t *testing.T) {
if types.NewOrder(reflect.TypeOf(0)) != nil {
t.Error("types.NewOrder(int) returned non-nil func")
}
fn := types.NewOrder(reflect.TypeOf(compareType(0)))
if fn == nil {
t.Error("types.NewOrder(compareType) returned nil func")
} else {
a, b := reflect.ValueOf(compareType(1)), reflect.ValueOf(compareType(2))
test.EqualInt(t, fn(a, b), -1)
test.EqualInt(t, fn(b, a), 1)
test.EqualInt(t, fn(a, a), 0)
}
fn = types.NewOrder(reflect.TypeOf(lessType(0)))
if fn == nil {
t.Error("types.NewOrder(lessType) returned nil func")
} else {
a, b := reflect.ValueOf(lessType(1)), reflect.ValueOf(lessType(2))
test.EqualInt(t, fn(a, b), -1)
test.EqualInt(t, fn(b, a), 1)
test.EqualInt(t, fn(a, a), 0)
}
if types.NewOrder(reflect.TypeOf(badType1(0))) != nil {
t.Error("types.NewOrder(badType1) returned non-nil func")
}
if types.NewOrder(reflect.TypeOf(badType2(0))) != nil {
t.Error("types.NewOrder(badType2) returned non-nil func")
}
if types.NewOrder(reflect.TypeOf(badType3(0))) != nil {
t.Error("types.NewOrder(badType3) returned non-nil func")
}
if types.NewOrder(reflect.TypeOf(badType4(0))) != nil {
t.Error("types.NewOrder(badType4) returned non-nil func")
}
}
go-testdeep-1.15.0/internal/types/reflect.go 0000664 0000000 0000000 00000005776 15144170453 0020774 0 ustar 00root root 0000000 0000000 // Copyright (c) 2020-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 types
import (
"encoding/json"
"fmt"
"reflect"
"strings"
"time"
)
var (
Bool = reflect.TypeOf(false)
Interface = reflect.TypeOf((*any)(nil)).Elem()
SliceInterface = reflect.TypeOf(([]any)(nil))
FmtStringer = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
Error = reflect.TypeOf((*error)(nil)).Elem()
JsonUnmarshaler = reflect.TypeOf((*json.Unmarshaler)(nil)).Elem() //nolint: revive
Time = reflect.TypeOf(time.Time{})
Int = reflect.TypeOf(int(0))
Uint8 = reflect.TypeOf(uint8(0))
Rune = reflect.TypeOf(rune(0))
String = reflect.TypeOf("")
)
// IsStruct returns true if t is a struct or a pointer on a struct
// (whatever the number of chained pointers), false otherwise.
func IsStruct(t reflect.Type) bool {
for {
switch t.Kind() {
case reflect.Struct:
return true
case reflect.Ptr:
t = t.Elem()
default:
return false
}
}
}
// IsTypeOrConvertible returns (true, false) if v type == target,
// (true, true) if v if convertible to target type, (false, false)
// otherwise.
//
// It handles go 1.17 slice to array pointer convertibility.
func IsTypeOrConvertible(v reflect.Value, target reflect.Type) (bool, bool) {
if v.Type() == target {
return true, false
}
if IsConvertible(v, target) {
return true, true
}
return false, false
}
// IsConvertible returns true if v is convertible to target type,
// false otherwise.
//
// It handles go 1.17 slice to array pointer convertibility.
// It handles go 1.20 slice to array convertibility.
func IsConvertible(v reflect.Value, target reflect.Type) bool {
if v.Type().ConvertibleTo(target) {
tk := target.Kind()
if v.Kind() != reflect.Slice ||
(tk != reflect.Ptr && tk != reflect.Array) ||
// Since go 1.17, a slice can be convertible to a pointer to an
// array, but Convert() may still panic if the slice length is lesser
// than array pointed one
(tk == reflect.Ptr && (target.Elem().Kind() != reflect.Array ||
v.Len() >= target.Elem().Len())) ||
// Since go 1.20, a slice can also be convertible to an array, but
// Convert() may still panic if the slice length is lesser than
// array one
(tk == reflect.Array && v.Len() >= target.Len()) {
return true
}
}
return false
}
// KindType returns the kind of val as a string. If the kind is
// [reflect.Ptr], a "*" is used as prefix of kind of
// val.Type().Elem(), and so on. If the final kind differs from
// val.Type(), the type is appended inside parenthesis.
func KindType(val reflect.Value) string {
if !val.IsValid() {
return "nil"
}
nptr := 0
typ := val.Type()
for typ.Kind() == reflect.Ptr {
nptr++
typ = typ.Elem()
}
kind := strings.Repeat("*", nptr) + typ.Kind().String()
if typ := val.Type().String(); kind != typ {
kind += " (" + typ + " type)"
}
return kind
}
go-testdeep-1.15.0/internal/types/reflect_go117_test.go 0000664 0000000 0000000 00000002303 15144170453 0022730 0 ustar 00root root 0000000 0000000 // Copyright (c) 2021, 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.
//go:build go1.17 && !go1.20
// +build go1.17,!go1.20
package types_test
import (
"reflect"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/internal/types"
)
// go1.17 allows to convert []T to *[n]T.
func TestIsTypeOrConvertible_go117(t *testing.T) {
type ArrP *[5]int
ok, convertible := types.IsTypeOrConvertible(
reflect.ValueOf([]int{1, 2, 3, 4, 5}),
reflect.TypeOf((ArrP)(nil)))
test.IsTrue(t, ok)
test.IsTrue(t, convertible)
ok, convertible = types.IsTypeOrConvertible(
reflect.ValueOf([]int{1, 2, 3, 4}), // not enough items
reflect.TypeOf((ArrP)(nil)))
test.IsFalse(t, ok)
test.IsFalse(t, convertible)
ok, convertible = types.IsTypeOrConvertible(
reflect.ValueOf([]int{1, 2, 3, 4, 5}),
reflect.TypeOf(&struct{}{}))
test.IsFalse(t, ok)
test.IsFalse(t, convertible)
ok, convertible = types.IsTypeOrConvertible(
reflect.ValueOf([]int{1, 2, 3, 4, 5}),
reflect.TypeOf([5]int{}))
test.IsFalse(t, ok)
test.IsFalse(t, convertible)
}
go-testdeep-1.15.0/internal/types/reflect_go120_test.go 0000664 0000000 0000000 00000002637 15144170453 0022734 0 ustar 00root root 0000000 0000000 // 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.
//go:build go1.20
// +build go1.20
package types_test
import (
"reflect"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/internal/types"
)
// go1.17 allows to convert []T to *[n]T.
// go1.20 allows to convert []T to [n]T.
func TestIsTypeOrConvertible_go117(t *testing.T) {
type ArrP *[5]int
type Arr [5]int
// 1.17
ok, convertible := types.IsTypeOrConvertible(
reflect.ValueOf([]int{1, 2, 3, 4, 5}),
reflect.TypeOf((ArrP)(nil)))
test.IsTrue(t, ok)
test.IsTrue(t, convertible)
// 1.20
ok, convertible = types.IsTypeOrConvertible(
reflect.ValueOf([]int{1, 2, 3, 4, 5}),
reflect.TypeOf([5]int{}))
test.IsTrue(t, ok)
test.IsTrue(t, convertible)
// 1.20
ok, convertible = types.IsTypeOrConvertible(
reflect.ValueOf([]int{1, 2, 3, 4, 5}),
reflect.TypeOf(Arr{}))
test.IsTrue(t, ok)
test.IsTrue(t, convertible)
ok, convertible = types.IsTypeOrConvertible(
reflect.ValueOf([]int{1, 2, 3, 4}), // not enough items
reflect.TypeOf((ArrP)(nil)))
test.IsFalse(t, ok)
test.IsFalse(t, convertible)
ok, convertible = types.IsTypeOrConvertible(
reflect.ValueOf([]int{1, 2, 3, 4, 5}),
reflect.TypeOf(&struct{}{}))
test.IsFalse(t, ok)
test.IsFalse(t, convertible)
}
go-testdeep-1.15.0/internal/types/reflect_test.go 0000664 0000000 0000000 00000003510 15144170453 0022013 0 ustar 00root root 0000000 0000000 // Copyright (c) 2020-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 types_test
import (
"reflect"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/internal/types"
)
func TestIsStruct(t *testing.T) {
s := struct{}{}
ps := &s
pps := &ps
m := map[string]struct{}{}
for i, test := range []struct {
val any
ok bool
}{
{val: s, ok: true},
{val: ps, ok: true},
{val: pps, ok: true},
{val: &pps, ok: true},
{val: m, ok: false},
{val: &m, ok: false},
} {
if types.IsStruct(reflect.TypeOf(test.val)) != test.ok {
t.Errorf("#%d IsStruct() mismatch as ≠ %t", i, test.ok)
}
}
}
func TestIsTypeOrConvertible(t *testing.T) {
type MyInt int
ok, convertible := types.IsTypeOrConvertible(reflect.ValueOf(123), reflect.TypeOf(123))
test.IsTrue(t, ok)
test.IsFalse(t, convertible)
ok, convertible = types.IsTypeOrConvertible(reflect.ValueOf(123), reflect.TypeOf(123.45))
test.IsTrue(t, ok)
test.IsTrue(t, convertible)
ok, convertible = types.IsTypeOrConvertible(reflect.ValueOf(123), reflect.TypeOf(MyInt(123)))
test.IsTrue(t, ok)
test.IsTrue(t, convertible)
ok, convertible = types.IsTypeOrConvertible(reflect.ValueOf("xx"), reflect.TypeOf(123))
test.IsFalse(t, ok)
test.IsFalse(t, convertible)
}
func TestKindType(t *testing.T) {
for _, tc := range []struct {
val any
expected string
}{
{nil, "nil"},
{42, "int"},
{(*int)(nil), "*int"},
{(*[]int)(nil), "*slice (*[]int type)"},
{(***int)(nil), "***int"},
} {
vval := reflect.ValueOf(tc.val)
name := "nil"
if tc.val != nil {
name = vval.Type().String()
}
t.Run(name, func(t *testing.T) {
test.EqualStr(t, types.KindType(vval), tc.expected)
})
}
}
go-testdeep-1.15.0/internal/types/types.go 0000664 0000000 0000000 00000004376 15144170453 0020507 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 types
import (
"encoding/json"
"strconv"
)
// TestDeepStringer is a TestDeep specific interface for objects which
// know how to stringify themselves.
type TestDeepStringer interface {
_TestDeep()
String() string
}
// TestDeepStamp is a useful type providing the _TestDeep() method
// needed to implement [TestDeepStringer] interface.
type TestDeepStamp struct{}
func (t TestDeepStamp) _TestDeep() {}
// RawString implements [TestDeepStringer] interface.
type RawString string
func (s RawString) _TestDeep() {}
func (s RawString) String() string {
return string(s)
}
// RawInt implements [TestDeepStringer] interface.
type RawInt int
func (i RawInt) _TestDeep() {}
func (i RawInt) String() string {
return strconv.Itoa(int(i))
}
var _ = []TestDeepStringer{RawString(""), RawInt(0)}
// OperatorNotJSONMarshallableError implements error interface. It
// is returned by (*td.TestDeep).MarshalJSON() to notice the user an
// operator cannot be JSON Marshal'ed.
type OperatorNotJSONMarshallableError string
// Error implements error interface.
func (e OperatorNotJSONMarshallableError) Error() string {
return string(e) + " TestDeep operator cannot be json.Marshal'led"
}
// Operator returns the operator behind this error.
func (e OperatorNotJSONMarshallableError) Operator() string {
return string(e)
}
// AsOperatorNotJSONMarshallableError checks that err is or contains
// an [OperatorNotJSONMarshallableError] and if yes, returns it and
// true.
func AsOperatorNotJSONMarshallableError(err error) (OperatorNotJSONMarshallableError, bool) {
switch err := err.(type) {
case OperatorNotJSONMarshallableError:
return err, true
case *json.MarshalerError:
if err, ok := err.Err.(OperatorNotJSONMarshallableError); ok {
return err, true
}
}
return "", false
}
type RecvKind bool
const (
_ RecvKind = (iota & 1) == 0
RecvNothing
RecvClosed
)
func (r RecvKind) _TestDeep() {}
func (r RecvKind) String() string {
if r == RecvNothing {
return "nothing received on channel"
}
return "channel is closed"
}
var _ = []TestDeepStringer{RecvNothing, RecvClosed}
go-testdeep-1.15.0/internal/types/types_private_test.go 0000664 0000000 0000000 00000000623 15144170453 0023267 0 ustar 00root root 0000000 0000000 // Copyright (c) 2020-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 types
import (
"testing"
)
// Only for coverage...
func TestTypes(t *testing.T) {
(TestDeepStamp{})._TestDeep()
RawString("")._TestDeep()
RawInt(0)._TestDeep()
RecvNothing._TestDeep()
}
go-testdeep-1.15.0/internal/types/types_test.go 0000664 0000000 0000000 00000004275 15144170453 0021544 0 ustar 00root root 0000000 0000000 // Copyright (c) 2021-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 types_test
import (
"encoding/json"
"errors"
"testing"
"github.com/maxatome/go-testdeep/internal/types"
)
var _ error = types.OperatorNotJSONMarshallableError("")
func TestOperatorNotJSONMarshallableError(t *testing.T) {
e := types.OperatorNotJSONMarshallableError("Pipo")
if e.Error() != "Pipo TestDeep operator cannot be json.Marshal'led" {
t.Errorf("unexpected %q", e.Error())
}
if e.Operator() != "Pipo" {
t.Errorf("unexpected %q", e.Operator())
}
t.Run("AsOperatorNotJSONMarshallableError", func(t *testing.T) {
ne, ok := types.AsOperatorNotJSONMarshallableError(e)
if !ok {
t.Error("AsOperatorNotJSONMarshallableError() returned false")
return
}
if ne != e {
t.Errorf("AsOperatorNotJSONMarshallableError(): %q ≠ %q",
ne.Error(), e.Error())
}
other := errors.New("Other error")
_, ok = types.AsOperatorNotJSONMarshallableError(other)
if ok {
t.Error("AsOperatorNotJSONMarshallableError() returned true")
return
}
je := &json.MarshalerError{Err: e}
ne, ok = types.AsOperatorNotJSONMarshallableError(je)
if !ok {
t.Error("AsOperatorNotJSONMarshallableError() returned false")
return
}
if ne != e {
t.Errorf("AsOperatorNotJSONMarshallableError(): %q ≠ %q",
ne.Error(), e.Error())
}
je.Err = other
_, ok = types.AsOperatorNotJSONMarshallableError(je)
if ok {
t.Error("AsOperatorNotJSONMarshallableError() returned true")
return
}
})
}
func TestRawString(t *testing.T) {
s := types.RawString("foo")
if str := s.String(); str != "foo" {
t.Errorf("Very weird, got %s", str)
}
}
func TestRawInt(t *testing.T) {
i := types.RawInt(42)
if str := i.String(); str != "42" {
t.Errorf("Very weird, got %s", str)
}
}
func TestRecvKind(t *testing.T) {
s := types.RecvNothing.String()
if s != "nothing received on channel" {
t.Errorf(`got: %q / expected: "nothing received on channel"`, s)
}
s = types.RecvClosed.String()
if s != "channel is closed" {
t.Errorf(`got: %q / expected: "channel is closed"`, s)
}
}
go-testdeep-1.15.0/internal/util/ 0000775 0000000 0000000 00000000000 15144170453 0016613 5 ustar 00root root 0000000 0000000 go-testdeep-1.15.0/internal/util/any.go 0000664 0000000 0000000 00000000420 15144170453 0017725 0 ustar 00root root 0000000 0000000 // 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.
//go:build !go1.18
// +build !go1.18
package util
type any = interface{}
go-testdeep-1.15.0/internal/util/any_test.go 0000664 0000000 0000000 00000000425 15144170453 0020771 0 ustar 00root root 0000000 0000000 // 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.
//go:build !go1.18
// +build !go1.18
package util_test
type any = interface{}
go-testdeep-1.15.0/internal/util/json_pointer.go 0000664 0000000 0000000 00000004065 15144170453 0021660 0 ustar 00root root 0000000 0000000 // Copyright (c) 2020, 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 util
import (
"strconv"
"strings"
)
var jsonPointerEsc = strings.NewReplacer("~0", "~", "~1", "/")
const (
ErrJSONPointerInvalid = "invalid JSON pointer"
ErrJSONPointerKeyNotFound = "key not found"
ErrJSONPointerArrayNoIndex = "array but not an index in JSON pointer"
ErrJSONPointerArrayOutOfRange = "out of array range"
ErrJSONPointerArrayBadType = "not a map nor an array"
)
type JSONPointerError struct {
Type string
Pointer string
}
func (e *JSONPointerError) Error() string {
if e.Pointer == "" {
return e.Type
}
return e.Type + " @" + e.Pointer
}
// JSONPointer returns the value corresponding to JSON pointer
// pointer in v as [RFC 6901] specifies it. To be searched, v has
// to contains map[string]any or []any values. All
// other types fail to be searched.
//
// [RFC 6901]: https://tools.ietf.org/html/rfc6901
func JSONPointer(v any, pointer string) (any, error) {
if !strings.HasPrefix(pointer, "/") {
if pointer == "" {
return v, nil
}
return nil, &JSONPointerError{Type: ErrJSONPointerInvalid}
}
pos := 0
for _, part := range strings.Split(pointer[1:], "/") {
pos += 1 + len(part)
part = jsonPointerEsc.Replace(part)
switch tv := v.(type) {
case map[string]any:
var ok bool
v, ok = tv[part]
if !ok {
return nil, &JSONPointerError{
Type: ErrJSONPointerKeyNotFound,
Pointer: pointer[:pos],
}
}
case []any:
i, err := strconv.Atoi(part)
if err != nil || i < 0 {
return nil, &JSONPointerError{
Type: ErrJSONPointerArrayNoIndex,
Pointer: pointer[:pos],
}
}
if i >= len(tv) {
return nil, &JSONPointerError{
Type: ErrJSONPointerArrayOutOfRange,
Pointer: pointer[:pos],
}
}
v = tv[i]
default:
return nil, &JSONPointerError{
Type: ErrJSONPointerArrayBadType,
Pointer: pointer[:pos],
}
}
}
return v, nil
}
go-testdeep-1.15.0/internal/util/json_pointer_test.go 0000664 0000000 0000000 00000003665 15144170453 0022724 0 ustar 00root root 0000000 0000000 // Copyright (c) 2020, 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 util_test
import (
"encoding/json"
"reflect"
"testing"
"github.com/maxatome/go-testdeep/internal/util"
)
func TestJSONPointer(t *testing.T) {
var ref any
err := json.Unmarshal([]byte(`
{
"foo": ["bar", "baz"],
"": 0,
"a/b": 1,
"c%d": 2,
"e^f": 3,
"g|h": 4,
"i\\j": 5,
"k\"l": 6,
" ": 7,
"m~n": 8
}`),
&ref)
if err != nil {
t.Fatalf("json.Unmarshal failed: %s", err)
}
checkOK := func(pointer string, expected any) {
t.Helper()
got, err := util.JSONPointer(ref, pointer)
if !reflect.DeepEqual(got, expected) {
t.Errorf("got: %v expected: %v", got, expected)
}
if err != nil {
t.Errorf("error <%s> received instead of nil", err)
}
}
checkErr := func(pointer, errExpected string) {
t.Helper()
got, err := util.JSONPointer(ref, pointer)
if got != nil {
t.Errorf("got: %v expected: nil", got)
}
if err == nil {
t.Errorf("error nil received instead of <%s>", errExpected)
} else if err.Error() != errExpected {
t.Errorf("error <%s> received instead of <%s>", err, errExpected)
}
}
checkOK(``, ref)
checkOK(`/foo`, []any{"bar", "baz"})
checkOK(`/foo/0`, "bar")
checkOK(`/`, float64(0))
checkOK(`/a~1b`, float64(1))
checkOK(`/c%d`, float64(2))
checkOK(`/e^f`, float64(3))
checkOK(`/g|h`, float64(4))
checkOK(`/i\j`, float64(5))
checkOK(`/k"l`, float64(6))
checkOK(`/ `, float64(7))
checkOK(`/m~0n`, float64(8))
checkErr("x", "invalid JSON pointer")
checkErr("/8", "key not found @/8")
checkErr("/foo/-1/pipo", "array but not an index in JSON pointer @/foo/-1")
checkErr("/foo/bingo/pipo", "array but not an index in JSON pointer @/foo/bingo")
checkErr("/foo/2/pipo", "out of array range @/foo/2")
checkErr("/foo/1/pipo", "not a map nor an array @/foo/1/pipo")
}
go-testdeep-1.15.0/internal/util/string.go 0000664 0000000 0000000 00000012415 15144170453 0020453 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-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 util
import (
"bytes"
"fmt"
"io"
"reflect"
"strconv"
"strings"
"github.com/maxatome/go-testdeep/helpers/tdutil"
"github.com/maxatome/go-testdeep/internal/dark"
"github.com/maxatome/go-testdeep/internal/types"
)
// ToString does its best to stringify val. inReflectValue is used
// internally to avoid treating specifically reflect.Value type.
func ToString(val any, inReflectValue ...bool) string {
if val == nil {
return "nil"
}
switch tval := val.(type) {
case reflect.Value:
if len(inReflectValue) > 0 && inReflectValue[0] {
break
}
newVal, ok := dark.GetInterface(tval, true)
if ok {
return ToString(newVal, true)
}
case []reflect.Value:
if len(inReflectValue) > 0 && inReflectValue[0] {
break
}
var buf strings.Builder
SliceToString(&buf, tval)
return buf.String()
// no "(string) " prefix for printable strings
case string:
return tdutil.FormatString(tval)
// no "(int) " prefix for ints
case int:
return strconv.Itoa(tval)
// no "(float64) " prefix for float64s
case float64:
s := strconv.FormatFloat(tval, 'g', -1, 64)
if strings.ContainsAny(s, "e.IN") { // I for Inf, N for NaN
return s
}
return s + ".0" // to distinguish from ints
// no "(bool) " prefix for booleans
case bool:
if tval {
return "true"
}
return "false"
case types.TestDeepStringer:
return tval.String()
}
return tdutil.SpewString(val)
}
// IndentString indents str lines (from 2nd one = 1st line is not
// indented) by indent.
func IndentString(str, indent string) string {
return strings.ReplaceAll(str, "\n", "\n"+indent)
}
// IndentStringIn indents str lines (from 2nd one = 1st line is not
// indented) by indent and write it to w.
func IndentStringIn(w io.Writer, str, indent string) {
repl := strings.NewReplacer("\n", "\n"+indent)
repl.WriteString(w, str) //nolint: errcheck
}
// IndentColorizeStringIn indents str lines (from 2nd one = 1st line
// is not indented) by indent and write it to w. Before each end of
// line, colOff is inserted, and after each indent on new line, colOn
// is inserted.
func IndentColorizeStringIn(w io.Writer, str, indent, colOn, colOff string) {
if str != "" {
if colOn == "" && colOff == "" {
IndentStringIn(w, str, indent)
return
}
repl := strings.NewReplacer("\n", colOff+"\n"+indent+colOn)
io.WriteString(w, colOn) //nolint: errcheck
repl.WriteString(w, str) //nolint: errcheck
io.WriteString(w, colOff) //nolint: errcheck
}
}
// SliceToString stringifies items slice into buf then returns buf.
func SliceToString(buf *strings.Builder, items []reflect.Value) *strings.Builder {
buf.WriteByte('(')
begLine := strings.LastIndexByte(buf.String(), '\n') + 1
prefix := strings.Repeat(" ", buf.Len()-begLine)
if len(items) < 2 {
if len(items) > 0 {
buf.WriteString(IndentString(ToString(items[0]), prefix))
}
} else {
buf.WriteString(IndentString(ToString(items[0]), prefix))
for _, item := range items[1:] {
buf.WriteString(",\n")
buf.WriteString(prefix)
buf.WriteString(IndentString(ToString(item), prefix))
}
}
buf.WriteByte(')')
return buf
}
// TypeFullName returns the t type name with packages fully visible
// instead of the last package part in t.String().
func TypeFullName(t reflect.Type) string {
var b bytes.Buffer
typeFullName(&b, t)
return b.String()
}
func typeFullName(b *bytes.Buffer, t reflect.Type) {
if t.Name() != "" {
if pkg := t.PkgPath(); pkg != "" {
fmt.Fprintf(b, "%s.", pkg)
}
b.WriteString(t.Name())
return
}
switch t.Kind() {
case reflect.Ptr:
b.WriteByte('*')
typeFullName(b, t.Elem())
case reflect.Slice:
b.WriteString("[]")
typeFullName(b, t.Elem())
case reflect.Array:
fmt.Fprintf(b, "[%d]", t.Len())
typeFullName(b, t.Elem())
case reflect.Map:
b.WriteString("map[")
typeFullName(b, t.Key())
b.WriteByte(']')
typeFullName(b, t.Elem())
case reflect.Struct:
b.WriteString("struct {")
if num := t.NumField(); num > 0 {
for i := 0; i < num; i++ {
sf := t.Field(i)
if !sf.Anonymous {
b.WriteByte(' ')
b.WriteString(sf.Name)
}
b.WriteByte(' ')
typeFullName(b, sf.Type)
b.WriteByte(';')
}
b.Truncate(b.Len() - 1)
b.WriteByte(' ')
}
b.WriteByte('}')
case reflect.Func:
b.WriteString("func(")
if num := t.NumIn(); num > 0 {
for i := 0; i < num; i++ {
if i == num-1 && t.IsVariadic() {
b.WriteString("...")
typeFullName(b, t.In(i).Elem())
} else {
typeFullName(b, t.In(i))
}
b.WriteString(", ")
}
b.Truncate(b.Len() - 2)
}
b.WriteByte(')')
if num := t.NumOut(); num > 0 {
if num == 1 {
b.WriteByte(' ')
} else {
b.WriteString(" (")
}
for i := 0; i < num; i++ {
typeFullName(b, t.Out(i))
b.WriteString(", ")
}
b.Truncate(b.Len() - 2)
if num > 1 {
b.WriteByte(')')
}
}
case reflect.Chan:
switch t.ChanDir() {
case reflect.RecvDir:
b.WriteString("<-chan ")
case reflect.SendDir:
b.WriteString("chan<- ")
case reflect.BothDir:
b.WriteString("chan ")
}
typeFullName(b, t.Elem())
default:
// Fallback to default implementation
b.WriteString(t.String())
}
}
go-testdeep-1.15.0/internal/util/string_test.go 0000664 0000000 0000000 00000014521 15144170453 0021512 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-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 util_test
import (
"bytes"
"math"
"reflect"
"runtime"
"strings"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/internal/types"
"github.com/maxatome/go-testdeep/internal/util"
)
type myTestDeepStringer struct {
types.TestDeepStamp
}
func (m myTestDeepStringer) String() string {
return "TesT!"
}
func TestToString(t *testing.T) {
for _, curTest := range []struct {
paramGot any
expected string
}{
{paramGot: nil, expected: "nil"},
{paramGot: "foobar", expected: `"foobar"`},
{paramGot: "foo\rbar", expected: `(string) (len=7) "foo\rbar"`},
{paramGot: "foo\u2028bar", expected: `(string) (len=9) "foo\u2028bar"`},
{paramGot: `foo"bar`, expected: "`foo\"bar`"},
{paramGot: "foo\n\"bar", expected: "`foo\n\"bar`"},
{paramGot: "foo`\"\nbar", expected: "(string) (len=9) \"foo`\\\"\\nbar\""},
{paramGot: "foo`\n\"bar", expected: "(string) (len=9) \"foo`\\n\\\"bar\""},
{paramGot: "foo\n`\"bar", expected: "(string) (len=9) \"foo\\n`\\\"bar\""},
{paramGot: "foo\n\"`bar", expected: "(string) (len=9) \"foo\\n\\\"`bar\""},
{paramGot: reflect.ValueOf("foobar"), expected: `"foobar"`},
{
paramGot: reflect.ValueOf(reflect.ValueOf(42)),
expected: `(reflect.Value) `,
},
{
paramGot: []reflect.Value{reflect.ValueOf("foo"), reflect.ValueOf("bar")},
expected: `("foo",
"bar")`,
},
{
paramGot: reflect.ValueOf([]reflect.Value{}),
expected: "([]reflect.Value) {\n}",
},
{paramGot: types.RawString("test"), expected: "test"},
{paramGot: types.RawInt(42), expected: "42"},
{paramGot: myTestDeepStringer{}, expected: "TesT!"},
{paramGot: 42, expected: "42"},
{paramGot: true, expected: "true"},
{paramGot: false, expected: "false"},
{paramGot: int64(42), expected: "(int64) 42"},
{paramGot: float64(42), expected: "42.0"},
{paramGot: float64(42.56), expected: "42.56"},
{paramGot: float64(4e56), expected: "4e+56"},
{paramGot: math.Inf(1), expected: "+Inf"},
{paramGot: math.Inf(-1), expected: "-Inf"},
{paramGot: math.NaN(), expected: "NaN"},
} {
test.EqualStr(t, util.ToString(curTest.paramGot), curTest.expected)
}
}
func TestIndentString(t *testing.T) {
for _, curTest := range []struct {
ParamGot string
Expected string
}{
{ParamGot: "", Expected: ""},
{ParamGot: "pipo", Expected: "pipo"},
{ParamGot: "pipo\nbingo\nzip", Expected: "pipo\n-bingo\n-zip"},
} {
test.EqualStr(t, util.IndentString(curTest.ParamGot, "-"), curTest.Expected)
var buf bytes.Buffer
util.IndentStringIn(&buf, curTest.ParamGot, "-")
test.EqualStr(t, buf.String(), curTest.Expected)
buf.Reset()
util.IndentColorizeStringIn(&buf, curTest.ParamGot, "-", "", "")
test.EqualStr(t, buf.String(), curTest.Expected)
}
for _, curTest := range []struct {
ParamGot string
Expected string
}{
{ParamGot: "", Expected: ""},
{ParamGot: "pipo", Expected: "<>"},
{ParamGot: "pipo\nbingo\nzip", Expected: "<>\n-<>\n-<>"},
} {
var buf bytes.Buffer
util.IndentColorizeStringIn(&buf, curTest.ParamGot, "-", "<<", ">>")
test.EqualStr(t, buf.String(), curTest.Expected)
}
}
func TestSliceToBuffer(t *testing.T) {
for _, curTest := range []struct {
BufInit string
Items []any
Expected string
}{
{BufInit: ">", Items: nil, Expected: ">()"},
{BufInit: ">", Items: []any{"pipo"}, Expected: `>("pipo")`},
{
BufInit: ">",
Items: []any{"pipo", "bingo", "zip"},
Expected: `>("pipo",
"bingo",
"zip")`,
},
{
BufInit: "List\n of\nitems:\n>",
Items: []any{"pipo", "bingo", "zip"},
Expected: `List
of
items:
>("pipo",
"bingo",
"zip")`,
},
} {
var items []reflect.Value
if curTest.Items != nil {
items = make([]reflect.Value, len(curTest.Items))
for i, val := range curTest.Items {
items[i] = reflect.ValueOf(val)
}
}
var buf strings.Builder
buf.WriteString(curTest.BufInit)
test.EqualStr(t, util.SliceToString(&buf, items).String(),
curTest.Expected)
}
}
func TestTypeFullName(t *testing.T) {
// our full package name
pc, _, _, _ := runtime.Caller(0)
pkg := strings.TrimSuffix(runtime.FuncForPC(pc).Name(), ".TestTypeFullName")
test.EqualStr(t, util.TypeFullName(reflect.TypeOf(123)), "int")
test.EqualStr(t, util.TypeFullName(reflect.TypeOf([]int{})), "[]int")
test.EqualStr(t, util.TypeFullName(reflect.TypeOf([3]int{})), "[3]int")
test.EqualStr(t, util.TypeFullName(reflect.TypeOf((**float64)(nil))), "**float64")
test.EqualStr(t, util.TypeFullName(reflect.TypeOf(map[int]float64{})), "map[int]float64")
test.EqualStr(t, util.TypeFullName(reflect.TypeOf(struct{}{})), "struct {}")
test.EqualStr(t, util.TypeFullName(reflect.TypeOf(struct {
a int
b bool
}{})), "struct { a int; b bool }")
test.EqualStr(t, util.TypeFullName(reflect.TypeOf(struct {
s struct{ a []int }
b bool
}{})), "struct { s struct { a []int }; b bool }")
type anon struct{ a []int } //nolint: unused
test.EqualStr(t, util.TypeFullName(reflect.TypeOf(struct {
anon
b bool
}{})), "struct { "+pkg+".anon; b bool }")
test.EqualStr(t, util.TypeFullName(reflect.TypeOf(func() {})), "func()")
test.EqualStr(t,
util.TypeFullName(reflect.TypeOf(func(a int) {})),
"func(int)")
test.EqualStr(t,
util.TypeFullName(reflect.TypeOf(func(a int, b ...bool) rune { return 0 })),
"func(int, ...bool) int32")
test.EqualStr(t,
util.TypeFullName(reflect.TypeOf(func() (int, bool, int) { return 0, true, 0 })),
"func() (int, bool, int)")
test.EqualStr(t, util.TypeFullName(reflect.TypeOf(func() {})), "func()")
test.EqualStr(t,
util.TypeFullName(reflect.TypeOf(func(a int) {})),
"func(int)")
test.EqualStr(t,
util.TypeFullName(reflect.TypeOf(func(a int, b ...bool) rune { return 0 })),
"func(int, ...bool) int32")
test.EqualStr(t,
util.TypeFullName(reflect.TypeOf(func() (int, bool, int) { return 0, true, 0 })),
"func() (int, bool, int)")
test.EqualStr(t,
util.TypeFullName(reflect.TypeOf((<-chan []int)(nil))),
"<-chan []int")
test.EqualStr(t,
util.TypeFullName(reflect.TypeOf((chan<- []int)(nil))),
"chan<- []int")
test.EqualStr(t,
util.TypeFullName(reflect.TypeOf((chan []int)(nil))),
"chan []int")
test.EqualStr(t,
util.TypeFullName(reflect.TypeOf((*any)(nil))),
"*interface {}")
}
go-testdeep-1.15.0/internal/util/tag.go 0000664 0000000 0000000 00000001565 15144170453 0017724 0 ustar 00root root 0000000 0000000 // 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 util
import (
"errors"
"unicode"
)
// ErrTagEmpty is the error returned by [CheckTag] for an empty tag.
var ErrTagEmpty = errors.New("a tag cannot be empty")
// ErrTagInvalid is the error returned by [CheckTag] for an invalid tag.
var ErrTagInvalid = errors.New("invalid tag, should match (Letter|_)(Letter|_|Number)*")
// CheckTag checks that tag is a valid tag (see operator [Tag]) or not.
//
// [Tag]: https://go-testdeep.zetta.rocks/operators/tag/
func CheckTag(tag string) error {
if tag == "" {
return ErrTagEmpty
}
for i, r := range tag {
if !unicode.IsLetter(r) && r != '_' && (i == 0 || !unicode.IsNumber(r)) {
return ErrTagInvalid
}
}
return nil
}
go-testdeep-1.15.0/internal/util/tag_test.go 0000664 0000000 0000000 00000001772 15144170453 0020763 0 ustar 00root root 0000000 0000000 // 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 util_test
import (
"testing"
"github.com/maxatome/go-testdeep/internal/util"
)
func TestCheckTag(t *testing.T) {
tags := []string{
"tag12",
"_1é",
"a9",
"a",
"é൫",
"é",
"_",
}
for _, tag := range tags {
if err := util.CheckTag(tag); err != nil {
t.Errorf("check(%s) failed: %s", tag, err)
}
}
tagsInfo := []struct {
tag string
err error
}{
{tag: "", err: util.ErrTagEmpty},
{tag: "൫a", err: util.ErrTagInvalid},
{tag: "9a", err: util.ErrTagInvalid},
{tag: "é ", err: util.ErrTagInvalid},
}
for _, info := range tagsInfo {
err := util.CheckTag(info.tag)
if err == nil {
t.Errorf("check(%s) should not succeed", info.tag)
} else if err != info.err {
t.Errorf(`check(%s) returned "%s" intead of expected "%s"`,
info.tag, err, info.err)
}
}
}
go-testdeep-1.15.0/internal/util/utils.go 0000664 0000000 0000000 00000002264 15144170453 0020306 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 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 {
return a
}
return b
}
go-testdeep-1.15.0/internal/util/utils_test.go 0000664 0000000 0000000 00000002357 15144170453 0021350 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 util_test
import (
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"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'))
}
go-testdeep-1.15.0/internal/visited/ 0000775 0000000 0000000 00000000000 15144170453 0017305 5 ustar 00root root 0000000 0000000 go-testdeep-1.15.0/internal/visited/any.go 0000664 0000000 0000000 00000000423 15144170453 0020422 0 ustar 00root root 0000000 0000000 // 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.
//go:build !go1.18
// +build !go1.18
package visited
type any = interface{}
go-testdeep-1.15.0/internal/visited/any_test.go 0000664 0000000 0000000 00000000430 15144170453 0021457 0 ustar 00root root 0000000 0000000 // 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.
//go:build !go1.18
// +build !go1.18
package visited_test
type any = interface{}
go-testdeep-1.15.0/internal/visited/visited.go 0000664 0000000 0000000 00000003732 15144170453 0021310 0 ustar 00root root 0000000 0000000 // 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 visited
import (
"reflect"
)
// visitKey is used by ctxerr.Context and its Visited map to handle
// cyclic references.
type visitedKey struct {
a1 uintptr
a2 uintptr
typ reflect.Type
}
// Visited allows to remember couples of same type pointers, typically
// not to do the same action twice if the couple has already been seen.
type Visited map[visitedKey]bool
// NewVisited returns a new [Visited] instance.
func NewVisited() Visited {
return Visited{}
}
// Record checks and, if needed, records a new entry for (got,
// expected) couple. It returns true if got & expected are pointers
// and have already been seen together. It returns false otherwise.
// It is the caller responsibility to check that got and expected
// types are the same.
func (v Visited) Record(got, expected reflect.Value) bool {
var addr1, addr2 uintptr
switch got.Kind() {
// Pointer() can not be used for interfaces and for slices the
// returned address is the array behind the slice, use UnsafeAddr()
// instead
case reflect.Slice, reflect.Interface:
if got.IsNil() || expected.IsNil() ||
!got.CanAddr() || !expected.CanAddr() {
return false
}
addr1 = got.UnsafeAddr()
addr2 = expected.UnsafeAddr()
// For maps and pointers use Pointer() to automatically handle
// indirect pointers
case reflect.Map, reflect.Ptr:
if got.IsNil() || expected.IsNil() {
return false
}
addr1 = got.Pointer()
addr2 = expected.Pointer()
default:
return false
}
if addr1 > addr2 {
// Canonicalize order to reduce number of entries in v.
// Assumes non-moving garbage collector.
addr1, addr2 = addr2, addr1
}
k := visitedKey{
a1: addr1,
a2: addr2,
typ: got.Type(),
}
if v[k] {
return true // references already seen
}
// Remember for later.
v[k] = true
return false
}
go-testdeep-1.15.0/internal/visited/visited_test.go 0000664 0000000 0000000 00000006024 15144170453 0022344 0 ustar 00root root 0000000 0000000 // 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 visited_test
import (
"reflect"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/internal/visited"
)
func TestVisited(t *testing.T) {
t.Run("not a pointer", func(t *testing.T) {
v := visited.NewVisited()
a, b := 1, 2
test.IsFalse(t, v.Record(reflect.ValueOf(a), reflect.ValueOf(b)))
test.IsFalse(t, v.Record(reflect.ValueOf(a), reflect.ValueOf(b)))
})
t.Run("map", func(t *testing.T) {
v := visited.NewVisited()
a, b := map[string]bool{}, map[string]bool{}
f := func(m map[string]bool) reflect.Value {
return reflect.ValueOf(m)
}
test.IsFalse(t, v.Record(f(a), f(b)))
test.IsTrue(t, v.Record(f(a), f(b)))
test.IsTrue(t, v.Record(f(b), f(a)))
// nil maps are not recorded
b = nil
test.IsFalse(t, v.Record(f(a), f(b)))
test.IsFalse(t, v.Record(f(a), f(b)))
test.IsFalse(t, v.Record(f(b), f(a)))
test.IsFalse(t, v.Record(f(b), f(a)))
})
t.Run("pointer", func(t *testing.T) {
v := visited.NewVisited()
type S struct {
p *S
ok bool
}
a, b := &S{}, &S{}
a.p = &S{ok: true}
b.p = &S{ok: false}
f := func(m *S) reflect.Value {
return reflect.ValueOf(m)
}
test.IsFalse(t, v.Record(f(a), f(b)))
test.IsTrue(t, v.Record(f(a), f(b)))
test.IsTrue(t, v.Record(f(b), f(a)))
test.IsFalse(t, v.Record(f(a.p), f(b.p)))
test.IsTrue(t, v.Record(f(a.p), f(b.p)))
test.IsTrue(t, v.Record(f(b.p), f(a.p)))
// nil pointers are not recorded
b = nil
test.IsFalse(t, v.Record(f(a), f(b)))
test.IsFalse(t, v.Record(f(a), f(b)))
test.IsFalse(t, v.Record(f(b), f(a)))
test.IsFalse(t, v.Record(f(b), f(a)))
})
// Visited.Record() needs its slice or interface param be
// addressable, that's why we use a struct pointer below
t.Run("slice", func(t *testing.T) {
v := visited.NewVisited()
type vSlice struct{ s []string }
a, b := &vSlice{s: []string{}}, &vSlice{[]string{}}
f := func(vm *vSlice) reflect.Value {
return reflect.ValueOf(vm).Elem().Field(0)
}
test.IsFalse(t, v.Record(f(a), f(b)))
test.IsTrue(t, v.Record(f(a), f(b)))
test.IsTrue(t, v.Record(f(b), f(a)))
// nil slices are not recorded
b = &vSlice{}
test.IsFalse(t, v.Record(f(a), f(b)))
test.IsFalse(t, v.Record(f(a), f(b)))
test.IsFalse(t, v.Record(f(b), f(a)))
test.IsFalse(t, v.Record(f(b), f(a)))
})
t.Run("interface", func(t *testing.T) {
v := visited.NewVisited()
type vIf struct{ i any }
a, b := &vIf{i: 42}, &vIf{i: 24}
f := func(vm *vIf) reflect.Value {
return reflect.ValueOf(vm).Elem().Field(0)
}
test.IsFalse(t, v.Record(f(a), f(b)))
test.IsTrue(t, v.Record(f(a), f(b)))
test.IsTrue(t, v.Record(f(b), f(a)))
// nil interfaces are not recorded
b = &vIf{}
test.IsFalse(t, v.Record(f(a), f(b)))
test.IsFalse(t, v.Record(f(a), f(b)))
test.IsFalse(t, v.Record(f(b), f(a)))
test.IsFalse(t, v.Record(f(b), f(a)))
})
}
go-testdeep-1.15.0/td/ 0000775 0000000 0000000 00000000000 15144170453 0014431 5 ustar 00root root 0000000 0000000 go-testdeep-1.15.0/td/any.go 0000664 0000000 0000000 00000000416 15144170453 0015550 0 ustar 00root root 0000000 0000000 // 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.
//go:build !go1.18
// +build !go1.18
package td
type any = interface{}
go-testdeep-1.15.0/td/any_test.go 0000664 0000000 0000000 00000000423 15144170453 0016605 0 ustar 00root root 0000000 0000000 // 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.
//go:build !go1.18
// +build !go1.18
package td_test
type any = interface{}
go-testdeep-1.15.0/td/check_test.go 0000664 0000000 0000000 00000024211 15144170453 0017074 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (
"fmt"
"os"
"reflect"
"regexp"
"strings"
"testing"
"github.com/maxatome/go-testdeep/helpers/tdutil"
"github.com/maxatome/go-testdeep/internal/color"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/dark"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestMain(m *testing.M) {
color.SaveState()
os.Exit(m.Run())
}
type MyStructBase struct {
ValBool bool
}
type MyStructMid struct {
MyStructBase
ValStr string
}
type MyStruct struct {
MyStructMid
ValInt int
Ptr *int
}
func (s *MyStruct) MyString() string {
return "!"
}
type MyInterface interface {
MyString() string
}
type MyStringer struct{}
func (s MyStringer) String() string { return "pipo bingo" }
type expectedError struct {
Path expectedErrorMatch
Message expectedErrorMatch
Got expectedErrorMatch
Expected expectedErrorMatch
Summary expectedErrorMatch
Located bool
Under expectedErrorMatch
Origin *expectedError
Next *expectedError
}
var ignoreExpectedError = &expectedError{}
type expectedErrorMatch struct {
Exact string
Match *regexp.Regexp
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}
}
func mustMatch(str string) expectedErrorMatch {
return expectedErrorMatch{Match: regexp.MustCompile(str)}
}
func mustContain(str string) expectedErrorMatch {
return expectedErrorMatch{Contain: str}
}
func indent(str string, numSpc int) string {
return strings.ReplaceAll(str, "\n", "\n\t"+strings.Repeat(" ", numSpc))
}
func fullError(err *ctxerr.Error) string {
return strings.ReplaceAll(err.Error(), "\n", "\n\t> ")
}
func cmpErrorStr(t *testing.T, err *ctxerr.Error,
got string, expected expectedErrorMatch, fieldName string,
args ...any,
) bool {
t.Helper()
if expected.Exact != "" && got != expected.Exact {
t.Errorf(`%sError.%s mismatch
got: %s
expected: %s
Full error:
> %s`,
tdutil.BuildTestName(args...),
fieldName, indent(got, 10), indent(expected.Exact, 10),
fullError(err))
return false
}
if expected.Contain != "" && !strings.Contains(got, expected.Contain) {
t.Errorf(`%sError.%s mismatch
got: %s
should contain: %s
Full error:
> %s`,
tdutil.BuildTestName(args...),
fieldName,
indent(got, 16), indent(expected.Contain, 16),
fullError(err))
return false
}
if expected.Match != nil && !expected.Match.MatchString(got) {
t.Errorf(`%sError.%s mismatch
got: %s
should match: %s
Full error:
> %s`,
tdutil.BuildTestName(args...),
fieldName,
indent(got, 14), indent(expected.Match.String(), 14),
fullError(err))
return false
}
return true
}
func matchError(t *testing.T, err *ctxerr.Error, expectedError expectedError,
expectedIsTestDeep bool, args ...any,
) bool {
t.Helper()
if !cmpErrorStr(t, err, err.Message, expectedError.Message,
"Message", args...) {
return false
}
if !cmpErrorStr(t, err, err.Context.Path.String(), expectedError.Path,
"Context.Path", args...) {
return false
}
if !cmpErrorStr(t, err, err.GotString(), expectedError.Got, "Got", args...) {
return false
}
if !cmpErrorStr(t, err,
err.ExpectedString(), expectedError.Expected, "Expected", args...) {
return false
}
if !cmpErrorStr(t, err,
err.SummaryString(), expectedError.Summary, "Summary", args...) {
return false
}
// under
serr, under := err.Error(), ""
if pos := strings.Index(serr, "\n[under operator "); pos > 0 {
under = serr[pos+2:]
under = under[:strings.IndexByte(under, ']')]
}
if !cmpErrorStr(t, err, under, expectedError.Under, "[under operator …]", args...) {
return false
}
// If expected is a TestDeep, the Location should be set
if expectedIsTestDeep {
expectedError.Located = true
}
if expectedError.Located != err.Location.IsInitialized() {
t.Errorf(`%sLocation of the origin of the error
got: %v
expected: %v`,
tdutil.BuildTestName(args...), err.Location.IsInitialized(), expectedError.Located)
return false
}
if expectedError.Located &&
!strings.HasSuffix(err.Location.File, "_test.go") {
t.Errorf(`%sFile of the origin of the error
got: line %d of %s
expected: *_test.go`,
tdutil.BuildTestName(args...), err.Location.Line, err.Location.File)
return false
}
if expectedError.Origin != nil {
if expectedError.Origin != ignoreExpectedError {
if err.Origin == nil {
t.Errorf(`%sError should originate from another Error`,
tdutil.BuildTestName(args...))
return false
}
if !matchError(t, err.Origin, *expectedError.Origin,
expectedIsTestDeep, args...) {
return false
}
}
} else if err.Origin != nil {
t.Errorf(`%sError should NOT originate from another Error`,
tdutil.BuildTestName(args...))
return false
}
if expectedError.Next != nil {
if expectedError.Next != ignoreExpectedError {
if err.Next == nil {
t.Errorf(`%sError should have a next Error`,
tdutil.BuildTestName(args...))
return false
}
if !matchError(t, err.Next, *expectedError.Next,
expectedIsTestDeep, args...) {
return false
}
}
} else if err.Next != nil {
t.Errorf(`%sError should NOT have a next Error
Full error:
> %s`,
tdutil.BuildTestName(args...),
fullError(err))
return false
}
return true
}
func _checkError(t *testing.T, got, expected any,
expectedError expectedError, args ...any,
) bool {
t.Helper()
err := td.EqDeeplyError(got, expected)
if err == nil {
t.Errorf("%sAn Error should have occurred", tdutil.BuildTestName(args...))
return false
}
_, expectedIsTestDeep := expected.(td.TestDeep)
if !matchError(t, err.(*ctxerr.Error), expectedError, expectedIsTestDeep, args...) {
return false
}
if td.EqDeeply(got, expected) {
t.Errorf(`%sBoolean context failed
got: true
expected: false`, tdutil.BuildTestName(args...))
return false
}
return true
}
func ifaceExpectedError(t *testing.T, expectedError expectedError) expectedError {
t.Helper()
if !strings.Contains(expectedError.Path.Exact, "DATA") {
return expectedError
}
newExpectedError := expectedError
newExpectedError.Path.Exact = strings.Replace(expectedError.Path.Exact,
"DATA", "DATA.Iface", 1)
if newExpectedError.Origin != nil {
newOrigin := ifaceExpectedError(t, *newExpectedError.Origin)
newExpectedError.Origin = &newOrigin
}
return newExpectedError
}
// checkError calls _checkError twice. The first time with the same
// parameters, the second time in an any context.
func checkError(t *testing.T, got, expected any,
expectedError expectedError, args ...any,
) bool {
t.Helper()
if ok := _checkError(t, got, expected, expectedError, args...); !ok {
return false
}
type tmpStruct struct {
Iface any
}
return _checkError(t, tmpStruct{Iface: got},
td.Struct(
tmpStruct{},
td.StructFields{
"Iface": expected,
}),
ifaceExpectedError(t, expectedError),
args...)
}
func checkErrorForEach(t *testing.T,
gotList []any, expected any,
expectedError expectedError, args ...any,
) (ret bool) {
t.Helper()
globalTestName := tdutil.BuildTestName(args...)
ret = true
for idx, got := range gotList {
testName := fmt.Sprintf("Got #%d", idx)
if globalTestName != "" {
testName += ", " + globalTestName
}
ret = checkError(t, got, expected, expectedError, testName) && ret
}
return
}
// customCheckOK calls chk twice. The first time with the same
// parameters, the second time in an any context.
func customCheckOK(t *testing.T,
chk func(t *testing.T, got, expected any, args ...any) bool,
got, expected any,
args ...any,
) bool {
t.Helper()
if ok := chk(t, got, expected, args...); !ok {
return false
}
type tmpStruct struct {
Iface any
}
// Dirty hack to force got be passed as an interface kind
return chk(t, tmpStruct{Iface: got},
td.Struct(
tmpStruct{},
td.StructFields{
"Iface": expected,
}),
args...)
}
func _checkOK(t *testing.T, got, expected any,
args ...any,
) bool {
t.Helper()
if !td.Cmp(t, got, expected, args...) {
return false
}
if !td.EqDeeply(got, expected) {
t.Errorf(`%sBoolean context failed
got: false
expected: true`, tdutil.BuildTestName(args...))
return false
}
if err := td.EqDeeplyError(got, expected); err != nil {
t.Errorf(`%sEqDeeplyError returned an error: %s`,
tdutil.BuildTestName(args...), err)
return false
}
return true
}
// checkOK calls _checkOK twice. The first time with the same
// parameters, the second time in an any context.
func checkOK(t *testing.T, got, expected any,
args ...any,
) bool {
t.Helper()
return customCheckOK(t, _checkOK, got, expected, args...)
}
func checkOKOrPanicIfUnsafeDisabled(t *testing.T, got, expected any,
args ...any,
) (ret bool) {
t.Helper()
cmp := func() {
t.Helper()
ret = _checkOK(t, got, expected, args...)
}
// Should panic if unsafe package is not available
if dark.UnsafeDisabled {
return test.CheckPanic(t, cmp,
"dark.GetInterface() does not handle private ")
}
cmp()
return
}
func checkOKForEach(t *testing.T, gotList []any, expected any,
args ...any,
) (ret bool) {
t.Helper()
globalTestName := tdutil.BuildTestName(args...)
ret = true
for idx, got := range gotList {
testName := fmt.Sprintf("Got #%d", idx)
if globalTestName != "" {
testName += ", " + globalTestName
}
ret = checkOK(t, got, expected, testName) && ret
}
return
}
func equalTypes(t *testing.T, got td.TestDeep, expected any, args ...any) bool {
gotType := got.TypeBehind()
expectedType, ok := expected.(reflect.Type)
if !ok {
expectedType = reflect.TypeOf(expected)
}
if gotType == expectedType {
return true
}
var gotStr, expectedStr string
if gotType == nil {
gotStr = "nil"
} else {
gotStr = gotType.String()
}
if expected == nil {
expectedStr = "nil"
} else {
expectedStr = expectedType.String()
}
t.Helper()
t.Errorf(`%sFailed test
got: %s
expected: %s`,
tdutil.BuildTestName(args...), gotStr, expectedStr)
return false
}
go-testdeep-1.15.0/td/cmp_deeply.go 0000664 0000000 0000000 00000014005 15144170453 0017101 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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"
"strings"
"github.com/maxatome/go-testdeep/helpers/tdutil"
"github.com/maxatome/go-testdeep/internal/color"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/flat"
"github.com/maxatome/go-testdeep/internal/trace"
)
func init() {
trace.Init()
trace.IgnorePackage()
}
// stripTrace removes go-testdeep useless calls in a trace returned by
// trace.Retrieve() to make it clearer for the reader.
func stripTrace(s trace.Stack) trace.Stack {
if len(s) == 0 {
return s
}
const (
tdPkg = "github.com/maxatome/go-testdeep/td"
tdhttpPkg = "github.com/maxatome/go-testdeep/helpers/tdhttp"
tdsuitePkg = "github.com/maxatome/go-testdeep/helpers/tdsuite"
)
// Remove useless possible (*T).Run() or (*T).RunAssertRequire() first call
if s.Match(-1, tdPkg, "(*T).Run.func1", "(*T).RunAssertRequire.func1") {
// Remove useless tdhttp (*TestAPI).Run() call
//
// ✓ xxx Subtest.func1()
// ✗ …/tdhttp (*TestAPI).Run.func1
// ✗ …/td (*T).Run.func1()
if s.Match(-2, tdhttpPkg, "(*TestAPI).Run.func1") {
return s[:len(s)-2]
}
// Remove useless tdsuite calls
//
// ✓ xxx Suite.TestSuite
// ✗ reflect Value.call
// ✗ reflect Value.Call
// ✗ …/tdsuite run.func2
// ✗ …/td (*T).Run.func1() or (*T).RunAssertRequire.func1()
//
// or for PostTest
// ✓ xxx Suite.PostTest
// ✗ …/tdsuite run.func2.1
// ✗ …/tdsuite run.func2
// ✗ …/td (*T).Run.func1() or (*T).RunAssertRequire.func1()
if s.Match(-2, tdsuitePkg, "run.func*") {
// PostTest
if s.Match(-3, tdsuitePkg, "run.func*") &&
len(s) > 4 &&
strings.HasSuffix(s[len(s)-4].Func, ".PostTest") {
return s[:len(s)-3]
}
for i := len(s) - 3; i >= 1; i-- {
if !s.Match(i, "reflect") {
return s[:i+1]
}
}
return nil
}
return s[:len(s)-1]
}
// Remove testing.Cleanup() stack
//
// ✓ xxx TestCleanup.func2
// ✗ testing (*common).Cleanup.func1
// ✗ testing (*common).runCleanup
// ✗ testing tRunner.func2
if s.Match(-1, "testing", "tRunner.func*") &&
s.Match(-2, "testing", "(*common).runCleanup") &&
s.Match(-3, "testing", "(*common).Cleanup.func1") {
return s[:len(s)-3]
}
// Remove tdsuite pre-Setup/BetweenTests/Destroy stack
//
// ✓ xxx Suite.Destroy
// ✗ …/tdsuite run.func1
// ✗ …/tdsuite run
// ✗ …/tdsuite Run
// ✓ xxx TestSuiteDestroy
if !s.Match(-1, tdsuitePkg) &&
s.Match(-2, tdsuitePkg, "Run") {
for i := len(s) - 3; i >= 0; i-- {
if !s.Match(i, tdsuitePkg) {
s[i+1] = s[len(s)-1]
return s[:i+2]
}
}
return s[:1]
}
return s
}
func formatError(t TestingT, isFatal bool, err *ctxerr.Error, args ...any) {
t.Helper()
const failedTest = "Failed test"
args = flat.Interfaces(args...)
var buf strings.Builder
color.AppendTestNameOn(&buf)
if len(args) == 0 {
buf.WriteString(failedTest)
} else {
buf.WriteString(failedTest + " '")
tdutil.FbuildTestName(&buf, args...)
buf.WriteString("'")
}
color.AppendTestNameOff(&buf)
buf.WriteString("\n")
err.Append(&buf, "", true)
// Stask trace
if s := stripTrace(trace.Retrieve(0, "testing.tRunner")); s.IsRelevant() {
buf.WriteString("\nThis is how we got here:\n")
s.Dump(&buf)
}
if isFatal {
t.Fatal(buf.String())
} else {
t.Error(buf.String())
}
}
func cmpDeeply(ctx ctxerr.Context, t TestingT, got, expected any,
args ...any,
) bool {
err := deepValueEqualFinal(ctx,
reflect.ValueOf(got), reflect.ValueOf(expected))
if err == nil {
return true
}
t.Helper()
formatError(t, ctx.FailureIsFatal, err, args...)
return false
}
// S returns a string based on args as Cmp* functions do with their
// own args parameter to name their test. So behind the scenes,
// [tdutil.BuildTestName] is used.
//
// 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
// returned string, else args are passed to [fmt.Fprint].
//
// It can be used as a shorter [fmt.Sprintf]:
//
// t.Run(fmt.Sprintf("Foo #%d", i), func(t *td.T) {})
// t.Run(td.S("Foo #%d", i), func(t *td.T) {})
//
// or to print any values as [fmt.Sprint] handles them:
//
// a, ok := []int{1, 2, 3}, true
// t.Run(fmt.Sprint(a, ok), func(t *td.T) {})
// t.Run(td.S(a, ok), func(t *td.T) {})
//
// The only gain is less characters to type.
func S(args ...any) string {
return tdutil.BuildTestName(args...)
}
// Cmp returns true if got matches expected. expected can
// be the same type as got is, or contains some [TestDeep]
// operators. If got does not match expected, it returns false and
// the reason of failure is logged with the help of t Error()
// method.
//
// got := "foobar"
// td.Cmp(t, got, "foobar") // succeeds
// td.Cmp(t, got, td.HasPrefix("foo")) // succeeds
//
// If t is a [*T] then its Config is inherited, so:
//
// td.Cmp(td.Require(t), got, 42)
//
// is the same as:
//
// td.Require(t).Cmp(got, 42)
//
// 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 Cmp(t TestingT, got, expected any, args ...any) bool {
t.Helper()
return cmpDeeply(newContext(t), t, got, expected, args...)
}
// CmpDeeply works the same as [Cmp] and is still available for
// compatibility purpose. Use shorter [Cmp] in new code.
//
// got := "foobar"
// td.CmpDeeply(t, got, "foobar") // succeeds
// td.CmpDeeply(t, got, td.HasPrefix("foo")) // succeeds
func CmpDeeply(t TestingT, got, expected any, args ...any) bool {
t.Helper()
return cmpDeeply(newContext(t), t, got, expected, args...)
}
go-testdeep-1.15.0/td/cmp_deeply_test.go 0000664 0000000 0000000 00000023013 15144170453 0020137 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (
"bytes"
"reflect"
"testing"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/internal/trace"
)
func TestStripTrace(t *testing.T) {
check := func(got, expected trace.Stack) {
got = stripTrace(got)
if !reflect.DeepEqual(got, expected) {
t.Helper()
t.Errorf("\n got: %#v\nexpected: %#v", got, expected)
}
}
check(nil, nil)
s := trace.Stack{
{Package: "test", Func: "A"},
}
check(s, s)
s = trace.Stack{
{Package: "test", Func: "A"},
{Package: "test", Func: "TestSimple"},
}
check(s, s)
// inside testing.Cleanup() call
s = trace.Stack{
{Package: "test", Func: "TestCleanup.func2"},
{Package: "testing", Func: "(*common).Cleanup.func1"},
{Package: "testing", Func: "(*common).runCleanup"},
{Package: "testing", Func: "tRunner.func2"},
}
check(s, s[:1])
//
// td
//
// td.(*T).Run() call
s = trace.Stack{
{Package: "test", Func: "A"},
{Package: "test", Func: "TestSubtestTd.func1"},
{Package: "github.com/maxatome/go-testdeep/td", Func: "(*T).Run.func1"},
}
check(s, s[:2])
// td.(*T).RunAssertRequire() call
s = trace.Stack{
{Package: "test", Func: "A"},
{Package: "test", Func: "TestSubtestTd.func1"},
{Package: "github.com/maxatome/go-testdeep/td", Func: "(*T).RunAssertRequire.func1"},
}
check(s, s[:2])
//
// tdhttp
//
// tdhttp.(*TestAPI).Run() call
s = trace.Stack{
{Package: "test", Func: "A"},
{Package: "test", Func: "TestSubtestTd.func1"},
{Package: "github.com/maxatome/go-testdeep/helpers/tdhttp", Func: "(*TestAPI).Run.func1"},
{Package: "github.com/maxatome/go-testdeep/td", Func: "(*T).Run.func1"},
}
check(s, s[:2])
//
// tdsuite
//
// tdsuite.Run() call → TestSuite(*td.T)
s = trace.Stack{
{Package: "test", Func: "A"},
{Package: "test", Func: "Suite.TestSuite"},
{Package: "reflect", Func: "Value.call"},
{Package: "reflect", Func: "Value.Call"},
{Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "run.func2"},
{Package: "github.com/maxatome/go-testdeep/td", Func: "(*T).Run.func1"},
}
check(s, s[:2])
// tdsuite.Run() call → TestSuite(assert, require *td.T)
s = trace.Stack{
{Package: "test", Func: "A"},
{Package: "test", Func: "Suite.TestSuite"},
{Package: "reflect", Func: "Value.call"},
{Package: "reflect", Func: "Value.Call"},
{Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "run.func1"},
{Package: "github.com/maxatome/go-testdeep/td", Func: "(*T).RunAssertRequire.func1"},
}
check(s, s[:2])
// tdsuite.Run() call → Suite.Setup()
s = trace.Stack{
{Package: "test", Func: "A"},
{Package: "test", Func: "Suite.Setup"},
{Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "run"},
{Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "Run"},
{Package: "test", Func: "TestSuiteSetup"},
}
check(s, append(s[:2:2], s[4]))
// tdsuite.Run() call → Suite.PreTest()
s = trace.Stack{
{Package: "test", Func: "A"},
{Package: "test", Func: "Suite.PreTest"},
{Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "run.func2"},
{Package: "github.com/maxatome/go-testdeep/td", Func: "(*T).Run.func1"},
}
check(s, s[:2])
// tdsuite.Run() call → Suite.PostTest()
s = trace.Stack{
{Package: "test", Func: "A"},
{Package: "test", Func: "Suite.PostTest"},
{Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "run.func2.1"},
{Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "run.func2"},
{Package: "github.com/maxatome/go-testdeep/td", Func: "(*T).Run.func1"},
}
check(s, s[:2])
// tdsuite.Run() call → Suite.BetweenTests()
s = trace.Stack{
{Package: "test", Func: "A"},
{Package: "test", Func: "Suite.BetweenTests"},
{Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "run"},
{Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "Run"},
{Package: "test", Func: "TestSuiteBetweenTests"},
}
check(s, append(s[:2:2], s[4]))
// tdsuite.Run() call → Suite.Destroy()
s = trace.Stack{
{Package: "test", Func: "A"},
{Package: "test", Func: "Suite.Destroy"},
{Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "run.func1"},
{Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "run"},
{Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "Run"},
{Package: "test", Func: "TestSuiteDestroy"},
}
check(s, append(s[:2:2], s[5]))
// Improbable cases
s = trace.Stack{
{Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "Run"},
{Package: "test", Func: "TestSuiteDestroy"},
}
check(s, s[:1])
s = trace.Stack{
{Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "y"},
{Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "x"},
{Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "Run"},
{Package: "test", Func: "TestSuiteDestroy"},
}
check(s, s[:1])
s = trace.Stack{
{Package: "test", Func: "Suite.TestXxx"},
{Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "Run"},
{Package: "test", Func: "TestSuiteDestroy"},
}
check(s, append(s[:1:1], s[2]))
s = trace.Stack{
{Package: "reflect", Func: "Value.call"},
{Package: "reflect", Func: "Value.Call"},
{Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "run.func1"},
{Package: "github.com/maxatome/go-testdeep/td", Func: "(*T).RunAssertRequire.func1"},
}
check(s, nil)
s = trace.Stack{
{Package: "test", Func: "A"},
{Package: "test", Func: "Suite.TestSuite"},
{Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "run.func1"},
{Package: "github.com/maxatome/go-testdeep/td", Func: "(*T).RunAssertRequire.func1"},
}
check(s, s[:2])
}
func TestFormatError(t *testing.T) {
err := &ctxerr.Error{
Context: newContext(nil),
Message: "test error message",
Summary: ctxerr.NewSummary("test error summary"),
}
nonStringName := bytes.NewBufferString("zip!")
for _, fatal := range []bool{false, true} {
//
// Without args
ttt := test.NewTestingT()
ttt.CatchFatal(func() { formatError(ttt, fatal, err) })
test.EqualStr(t, ttt.LastMessage(), `Failed test
DATA: test error message
test error summary`)
test.EqualBool(t, ttt.IsFatal, fatal)
//
// With one arg
ttt = test.NewTestingT()
ttt.CatchFatal(func() { formatError(ttt, fatal, err, "foo bar!") })
test.EqualStr(t, ttt.LastMessage(), `Failed test 'foo bar!'
DATA: test error message
test error summary`)
test.EqualBool(t, ttt.IsFatal, fatal)
ttt = test.NewTestingT()
ttt.CatchFatal(func() { formatError(ttt, fatal, err, nonStringName) })
test.EqualStr(t, ttt.LastMessage(), `Failed test 'zip!'
DATA: test error message
test error summary`)
test.EqualBool(t, ttt.IsFatal, fatal)
//
// With several args & Printf format
ttt = test.NewTestingT()
ttt.CatchFatal(func() { formatError(ttt, fatal, err, "hello %d!", 123) })
test.EqualStr(t, ttt.LastMessage(), `Failed test 'hello 123!'
DATA: test error message
test error summary`)
test.EqualBool(t, ttt.IsFatal, fatal)
//
// With several args & Printf format + Flatten
ttt = test.NewTestingT()
ttt.CatchFatal(func() {
formatError(ttt, fatal, err, "hello %s → %d/%d!", "bob", Flatten([]int{123, 125}))
})
test.EqualStr(t, ttt.LastMessage(), `Failed test 'hello bob → 123/125!'
DATA: test error message
test error summary`)
test.EqualBool(t, ttt.IsFatal, fatal)
//
// With several args without Printf format
ttt = test.NewTestingT()
ttt.CatchFatal(func() { formatError(ttt, fatal, err, "hello ", "world! ", 123) })
test.EqualStr(t, ttt.LastMessage(), `Failed test 'hello world! 123'
DATA: test error message
test error summary`)
test.EqualBool(t, ttt.IsFatal, fatal)
//
// With several args without Printf format + Flatten
ttt = test.NewTestingT()
ttt.CatchFatal(func() { formatError(ttt, fatal, err, "hello ", "world! ", Flatten([]int{123, 125})) })
test.EqualStr(t, ttt.LastMessage(), `Failed test 'hello world! 123 125'
DATA: test error message
test error summary`)
test.EqualBool(t, ttt.IsFatal, fatal)
ttt = test.NewTestingT()
ttt.CatchFatal(func() { formatError(ttt, fatal, err, nonStringName, "hello ", "world! ", 123) })
test.EqualStr(t, ttt.LastMessage(), `Failed test 'zip!hello world! 123'
DATA: test error message
test error summary`)
test.EqualBool(t, ttt.IsFatal, fatal)
}
}
func TestS(t *testing.T) {
for i, curTest := range []struct {
params []any
expected string
}{
{
params: []any{},
expected: "",
},
{
params: []any{"pipo", "bingo"},
expected: "pipobingo",
},
{
params: []any{"pipo %d %s", 42, "bingo"},
expected: "pipo 42 bingo",
},
{
params: []any{"pipo %d"},
expected: "pipo %d",
},
{
params: []any{"pipo %", 42},
expected: "pipo %42",
},
{
params: []any{42, 666},
expected: "42 666",
},
} {
test.EqualStr(t, S(curTest.params...), curTest.expected, "#%d", i)
}
}
func TestCmp(t *testing.T) {
tt := test.NewTestingTB(t.Name())
test.IsTrue(t, Cmp(tt, 1, 1))
test.IsFalse(t, tt.Failed())
tt = test.NewTestingTB(t.Name())
test.IsFalse(t, Cmp(tt, 1, 2))
test.IsTrue(t, tt.Failed())
}
func TestCmpDeeply(t *testing.T) {
tt := test.NewTestingTB(t.Name())
test.IsTrue(t, CmpDeeply(tt, 1, 1))
test.IsFalse(t, tt.Failed())
tt = test.NewTestingTB(t.Name())
test.IsFalse(t, CmpDeeply(tt, 1, 2))
test.IsTrue(t, tt.Failed())
}
go-testdeep-1.15.0/td/cmp_funcs.go 0000664 0000000 0000000 00000150624 15144170453 0016745 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2025, 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.
//
// DO NOT EDIT!!! AUTOMATICALLY GENERATED!!!
package td
import (
"time"
)
// allOperators lists the 70 operators.
// nil means not usable in JSON().
var allOperators = map[string]any{
"All": All,
"Any": Any,
"Array": nil,
"ArrayEach": ArrayEach,
"Bag": Bag,
"Between": Between,
"Cap": nil,
"Catch": nil,
"Code": nil,
"Contains": Contains,
"ContainsKey": ContainsKey,
"Delay": nil,
"Empty": Empty,
"ErrorIs": nil,
"First": First,
"Grep": Grep,
"Gt": Gt,
"Gte": Gte,
"HasPrefix": HasPrefix,
"HasSuffix": HasSuffix,
"Ignore": Ignore,
"Isa": nil,
"JSON": nil,
"JSONPointer": JSONPointer,
"Keys": Keys,
"Last": Last,
"Lax": nil,
"Len": Len,
"List": nil,
"Lt": Lt,
"Lte": Lte,
"Map": nil,
"MapEach": MapEach,
"N": N,
"NaN": NaN,
"Nil": Nil,
"None": None,
"Not": Not,
"NotAny": NotAny,
"NotEmpty": NotEmpty,
"NotNaN": NotNaN,
"NotNil": NotNil,
"NotZero": NotZero,
"PPtr": nil,
"Ptr": nil,
"Re": Re,
"ReAll": ReAll,
"Recv": nil,
"SStruct": nil,
"Set": Set,
"Shallow": nil,
"Slice": nil,
"Smuggle": nil,
"Sort": Sort,
"Sorted": Sorted,
"String": nil,
"Struct": nil,
"SubBagOf": SubBagOf,
"SubJSONOf": nil,
"SubMapOf": SubMapOf,
"SubSetOf": SubSetOf,
"SuperBagOf": SuperBagOf,
"SuperJSONOf": nil,
"SuperMapOf": SuperMapOf,
"SuperSetOf": SuperSetOf,
"SuperSliceOf": nil,
"Tag": nil,
"TruncTime": nil,
"Values": Values,
"Zero": Zero,
}
// CmpAll is a shortcut for:
//
// td.Cmp(t, got, td.All(expectedValues...), args...)
//
// See [All] 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 CmpAll(t TestingT, got any, expectedValues []any, args ...any) bool {
t.Helper()
return Cmp(t, got, All(expectedValues...), args...)
}
// CmpAny is a shortcut for:
//
// td.Cmp(t, got, td.Any(expectedValues...), args...)
//
// See [Any] 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 CmpAny(t TestingT, got any, expectedValues []any, args ...any) bool {
t.Helper()
return Cmp(t, got, Any(expectedValues...), args...)
}
// CmpArray is a shortcut for:
//
// td.Cmp(t, got, td.Array(model, expectedEntries), args...)
//
// See [Array] for details.
//
// [Array] optional parameter expectedEntries is here mandatory.
// nil value should be passed to mimic its absence in
// original [Array] 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 CmpArray(t TestingT, got, model any, expectedEntries ArrayEntries, args ...any) bool {
t.Helper()
return Cmp(t, got, Array(model, expectedEntries), args...)
}
// CmpArrayEach is a shortcut for:
//
// td.Cmp(t, got, td.ArrayEach(expectedValue), args...)
//
// See [ArrayEach] 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 CmpArrayEach(t TestingT, got, expectedValue any, args ...any) bool {
t.Helper()
return Cmp(t, got, ArrayEach(expectedValue), args...)
}
// CmpBag is a shortcut for:
//
// td.Cmp(t, got, td.Bag(expectedItems...), args...)
//
// See [Bag] 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 CmpBag(t TestingT, got any, expectedItems []any, args ...any) bool {
t.Helper()
return Cmp(t, got, Bag(expectedItems...), args...)
}
// CmpBetween is a shortcut for:
//
// td.Cmp(t, got, td.Between(from, to, bounds), args...)
//
// See [Between] for details.
//
// [Between] optional parameter bounds is here mandatory.
// [BoundsInIn] value should be passed to mimic its absence in
// original [Between] 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 CmpBetween(t TestingT, got, from, to any, bounds BoundsKind, args ...any) bool {
t.Helper()
return Cmp(t, got, Between(from, to, bounds), args...)
}
// CmpCap is a shortcut for:
//
// td.Cmp(t, got, td.Cap(expectedCap), args...)
//
// See [Cap] 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 CmpCap(t TestingT, got, expectedCap any, args ...any) bool {
t.Helper()
return Cmp(t, got, Cap(expectedCap), args...)
}
// CmpCode is a shortcut for:
//
// td.Cmp(t, got, td.Code(fn), args...)
//
// See [Code] 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 CmpCode(t TestingT, got, fn any, args ...any) bool {
t.Helper()
return Cmp(t, got, Code(fn), args...)
}
// CmpContains is a shortcut for:
//
// td.Cmp(t, got, td.Contains(expectedValue), args...)
//
// See [Contains] 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 CmpContains(t TestingT, got, expectedValue any, args ...any) bool {
t.Helper()
return Cmp(t, got, Contains(expectedValue), args...)
}
// CmpContainsKey is a shortcut for:
//
// td.Cmp(t, got, td.ContainsKey(expectedValue), args...)
//
// See [ContainsKey] 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 CmpContainsKey(t TestingT, got, expectedValue any, args ...any) bool {
t.Helper()
return Cmp(t, got, ContainsKey(expectedValue), args...)
}
// CmpEmpty is a shortcut for:
//
// td.Cmp(t, got, td.Empty(), args...)
//
// See [Empty] 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 CmpEmpty(t TestingT, got any, args ...any) bool {
t.Helper()
return Cmp(t, got, Empty(), args...)
}
// CmpErrorIs is a shortcut for:
//
// td.Cmp(t, got, td.ErrorIs(expectedError), args...)
//
// See [ErrorIs] 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 CmpErrorIs(t TestingT, got, expectedError any, args ...any) bool {
t.Helper()
return Cmp(t, got, ErrorIs(expectedError), 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...)
//
// See [Gt] 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 CmpGt(t TestingT, got, minExpectedValue any, args ...any) bool {
t.Helper()
return Cmp(t, got, Gt(minExpectedValue), args...)
}
// CmpGte is a shortcut for:
//
// td.Cmp(t, got, td.Gte(minExpectedValue), args...)
//
// See [Gte] 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 CmpGte(t TestingT, got, minExpectedValue any, args ...any) bool {
t.Helper()
return Cmp(t, got, Gte(minExpectedValue), args...)
}
// CmpHasPrefix is a shortcut for:
//
// td.Cmp(t, got, td.HasPrefix(expected), args...)
//
// See [HasPrefix] 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 CmpHasPrefix(t TestingT, got any, expected string, args ...any) bool {
t.Helper()
return Cmp(t, got, HasPrefix(expected), args...)
}
// CmpHasSuffix is a shortcut for:
//
// td.Cmp(t, got, td.HasSuffix(expected), args...)
//
// See [HasSuffix] 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 CmpHasSuffix(t TestingT, got any, expected string, args ...any) bool {
t.Helper()
return Cmp(t, got, HasSuffix(expected), args...)
}
// CmpIsa is a shortcut for:
//
// td.Cmp(t, got, td.Isa(model), args...)
//
// See [Isa] 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 CmpIsa(t TestingT, got, model any, args ...any) bool {
t.Helper()
return Cmp(t, got, Isa(model), args...)
}
// CmpJSON is a shortcut for:
//
// td.Cmp(t, got, td.JSON(expectedJSON, params...), args...)
//
// See [JSON] 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 CmpJSON(t TestingT, got, expectedJSON any, params []any, args ...any) bool {
t.Helper()
return Cmp(t, got, JSON(expectedJSON, params...), args...)
}
// CmpJSONPointer is a shortcut for:
//
// td.Cmp(t, got, td.JSONPointer(ptr, expectedValue), args...)
//
// See [JSONPointer] 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 CmpJSONPointer(t TestingT, got any, ptr string, expectedValue any, args ...any) bool {
t.Helper()
return Cmp(t, got, JSONPointer(ptr, expectedValue), args...)
}
// CmpKeys is a shortcut for:
//
// td.Cmp(t, got, td.Keys(val), args...)
//
// See [Keys] 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 CmpKeys(t TestingT, got, val any, args ...any) bool {
t.Helper()
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...)
//
// See [Lax] 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 CmpLax(t TestingT, got, expectedValue any, args ...any) bool {
t.Helper()
return Cmp(t, got, Lax(expectedValue), args...)
}
// CmpLen is a shortcut for:
//
// td.Cmp(t, got, td.Len(expectedLen), args...)
//
// See [Len] 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 CmpLen(t TestingT, got, expectedLen any, args ...any) bool {
t.Helper()
return Cmp(t, got, Len(expectedLen), args...)
}
// CmpList is a shortcut for:
//
// td.Cmp(t, got, td.List(expectedValues...), args...)
//
// See [List] 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 CmpList(t TestingT, got any, expectedValues []any, args ...any) bool {
t.Helper()
return Cmp(t, got, List(expectedValues...), args...)
}
// CmpLt is a shortcut for:
//
// td.Cmp(t, got, td.Lt(maxExpectedValue), args...)
//
// See [Lt] 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 CmpLt(t TestingT, got, maxExpectedValue any, args ...any) bool {
t.Helper()
return Cmp(t, got, Lt(maxExpectedValue), args...)
}
// CmpLte is a shortcut for:
//
// td.Cmp(t, got, td.Lte(maxExpectedValue), args...)
//
// See [Lte] 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 CmpLte(t TestingT, got, maxExpectedValue any, args ...any) bool {
t.Helper()
return Cmp(t, got, Lte(maxExpectedValue), args...)
}
// CmpMap is a shortcut for:
//
// td.Cmp(t, got, td.Map(model, expectedEntries), args...)
//
// See [Map] for details.
//
// [Map] optional parameter expectedEntries is here mandatory.
// nil value should be passed to mimic its absence in
// original [Map] 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 CmpMap(t TestingT, got, model any, expectedEntries MapEntries, args ...any) bool {
t.Helper()
return Cmp(t, got, Map(model, expectedEntries), args...)
}
// CmpMapEach is a shortcut for:
//
// td.Cmp(t, got, td.MapEach(expectedValue), args...)
//
// See [MapEach] 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 CmpMapEach(t TestingT, got, expectedValue any, args ...any) bool {
t.Helper()
return Cmp(t, got, MapEach(expectedValue), args...)
}
// CmpN is a shortcut for:
//
// td.Cmp(t, got, td.N(num, tolerance), args...)
//
// See [N] for details.
//
// [N] optional parameter tolerance is here mandatory.
// 0 value should be passed to mimic its absence in
// original [N] 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 CmpN(t TestingT, got, num, tolerance any, args ...any) bool {
t.Helper()
return Cmp(t, got, N(num, tolerance), args...)
}
// CmpNaN is a shortcut for:
//
// td.Cmp(t, got, td.NaN(), args...)
//
// See [NaN] 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 CmpNaN(t TestingT, got any, args ...any) bool {
t.Helper()
return Cmp(t, got, NaN(), args...)
}
// CmpNil is a shortcut for:
//
// td.Cmp(t, got, td.Nil(), args...)
//
// See [Nil] 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 CmpNil(t TestingT, got any, args ...any) bool {
t.Helper()
return Cmp(t, got, Nil(), args...)
}
// CmpNone is a shortcut for:
//
// td.Cmp(t, got, td.None(notExpectedValues...), args...)
//
// See [None] 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 CmpNone(t TestingT, got any, notExpectedValues []any, args ...any) bool {
t.Helper()
return Cmp(t, got, None(notExpectedValues...), args...)
}
// CmpNot is a shortcut for:
//
// td.Cmp(t, got, td.Not(notExpected), args...)
//
// See [Not] 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 CmpNot(t TestingT, got, notExpected any, args ...any) bool {
t.Helper()
return Cmp(t, got, Not(notExpected), args...)
}
// CmpNotAny is a shortcut for:
//
// td.Cmp(t, got, td.NotAny(notExpectedItems...), args...)
//
// See [NotAny] 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 CmpNotAny(t TestingT, got any, notExpectedItems []any, args ...any) bool {
t.Helper()
return Cmp(t, got, NotAny(notExpectedItems...), args...)
}
// CmpNotEmpty is a shortcut for:
//
// td.Cmp(t, got, td.NotEmpty(), args...)
//
// See [NotEmpty] 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 CmpNotEmpty(t TestingT, got any, args ...any) bool {
t.Helper()
return Cmp(t, got, NotEmpty(), args...)
}
// CmpNotNaN is a shortcut for:
//
// td.Cmp(t, got, td.NotNaN(), args...)
//
// See [NotNaN] 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 CmpNotNaN(t TestingT, got any, args ...any) bool {
t.Helper()
return Cmp(t, got, NotNaN(), args...)
}
// CmpNotNil is a shortcut for:
//
// td.Cmp(t, got, td.NotNil(), args...)
//
// See [NotNil] 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 CmpNotNil(t TestingT, got any, args ...any) bool {
t.Helper()
return Cmp(t, got, NotNil(), args...)
}
// CmpNotZero is a shortcut for:
//
// td.Cmp(t, got, td.NotZero(), args...)
//
// See [NotZero] 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 CmpNotZero(t TestingT, got any, args ...any) bool {
t.Helper()
return Cmp(t, got, NotZero(), args...)
}
// CmpPPtr is a shortcut for:
//
// td.Cmp(t, got, td.PPtr(val), args...)
//
// See [PPtr] 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 CmpPPtr(t TestingT, got, val any, args ...any) bool {
t.Helper()
return Cmp(t, got, PPtr(val), args...)
}
// CmpPtr is a shortcut for:
//
// td.Cmp(t, got, td.Ptr(val), args...)
//
// See [Ptr] 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 CmpPtr(t TestingT, got, val any, args ...any) bool {
t.Helper()
return Cmp(t, got, Ptr(val), args...)
}
// CmpRe is a shortcut for:
//
// td.Cmp(t, got, td.Re(reg, capture), args...)
//
// See [Re] for details.
//
// [Re] optional parameter capture is here mandatory.
// nil value should be passed to mimic its absence in
// original [Re] 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 CmpRe(t TestingT, got, reg, capture any, args ...any) bool {
t.Helper()
return Cmp(t, got, Re(reg, capture), args...)
}
// CmpReAll is a shortcut for:
//
// td.Cmp(t, got, td.ReAll(reg, capture), args...)
//
// See [ReAll] 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 CmpReAll(t TestingT, got, reg, capture any, args ...any) bool {
t.Helper()
return Cmp(t, got, ReAll(reg, capture), args...)
}
// CmpRecv is a shortcut for:
//
// td.Cmp(t, got, td.Recv(expectedValue, timeout), args...)
//
// See [Recv] for details.
//
// [Recv] optional parameter timeout is here mandatory.
// 0 value should be passed to mimic its absence in
// original [Recv] 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 CmpRecv(t TestingT, got, expectedValue any, timeout time.Duration, args ...any) bool {
t.Helper()
return Cmp(t, got, Recv(expectedValue, timeout), args...)
}
// CmpSet is a shortcut for:
//
// td.Cmp(t, got, td.Set(expectedItems...), args...)
//
// See [Set] 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 CmpSet(t TestingT, got any, expectedItems []any, args ...any) bool {
t.Helper()
return Cmp(t, got, Set(expectedItems...), args...)
}
// CmpShallow is a shortcut for:
//
// td.Cmp(t, got, td.Shallow(expectedPtr), args...)
//
// See [Shallow] 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 CmpShallow(t TestingT, got, expectedPtr any, args ...any) bool {
t.Helper()
return Cmp(t, got, Shallow(expectedPtr), args...)
}
// CmpSlice is a shortcut for:
//
// td.Cmp(t, got, td.Slice(model, expectedEntries), args...)
//
// See [Slice] for details.
//
// [Slice] optional parameter expectedEntries is here mandatory.
// nil value should be passed to mimic its absence in
// original [Slice] 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 CmpSlice(t TestingT, got, model any, expectedEntries ArrayEntries, args ...any) bool {
t.Helper()
return Cmp(t, got, Slice(model, expectedEntries), args...)
}
// CmpSmuggle is a shortcut for:
//
// td.Cmp(t, got, td.Smuggle(fn, expectedValue), args...)
//
// See [Smuggle] 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 CmpSmuggle(t TestingT, got, fn, expectedValue any, args ...any) bool {
t.Helper()
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...)
//
// See [SStruct] for details.
//
// [SStruct] optional parameter expectedFields is here mandatory.
// nil value should be passed to mimic its absence in
// original [SStruct] 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 CmpSStruct(t TestingT, got, model any, expectedFields StructFields, args ...any) bool {
t.Helper()
return Cmp(t, got, SStruct(model, expectedFields), args...)
}
// CmpString is a shortcut for:
//
// td.Cmp(t, got, td.String(expected), args...)
//
// See [String] 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 CmpString(t TestingT, got any, expected string, args ...any) bool {
t.Helper()
return Cmp(t, got, String(expected), args...)
}
// CmpStruct is a shortcut for:
//
// td.Cmp(t, got, td.Struct(model, expectedFields), args...)
//
// See [Struct] for details.
//
// [Struct] optional parameter expectedFields is here mandatory.
// nil value should be passed to mimic its absence in
// original [Struct] 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 CmpStruct(t TestingT, got, model any, expectedFields StructFields, args ...any) bool {
t.Helper()
return Cmp(t, got, Struct(model, expectedFields), args...)
}
// CmpSubBagOf is a shortcut for:
//
// td.Cmp(t, got, td.SubBagOf(expectedItems...), args...)
//
// See [SubBagOf] 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 CmpSubBagOf(t TestingT, got any, expectedItems []any, args ...any) bool {
t.Helper()
return Cmp(t, got, SubBagOf(expectedItems...), args...)
}
// CmpSubJSONOf is a shortcut for:
//
// td.Cmp(t, got, td.SubJSONOf(expectedJSON, params...), args...)
//
// See [SubJSONOf] 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 CmpSubJSONOf(t TestingT, got, expectedJSON any, params []any, args ...any) bool {
t.Helper()
return Cmp(t, got, SubJSONOf(expectedJSON, params...), args...)
}
// CmpSubMapOf is a shortcut for:
//
// td.Cmp(t, got, td.SubMapOf(model, expectedEntries), args...)
//
// See [SubMapOf] for details.
//
// [SubMapOf] optional parameter expectedEntries is here mandatory.
// nil value should be passed to mimic its absence in
// original [SubMapOf] 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 CmpSubMapOf(t TestingT, got, model any, expectedEntries MapEntries, args ...any) bool {
t.Helper()
return Cmp(t, got, SubMapOf(model, expectedEntries), args...)
}
// CmpSubSetOf is a shortcut for:
//
// td.Cmp(t, got, td.SubSetOf(expectedItems...), args...)
//
// See [SubSetOf] 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 CmpSubSetOf(t TestingT, got any, expectedItems []any, args ...any) bool {
t.Helper()
return Cmp(t, got, SubSetOf(expectedItems...), args...)
}
// CmpSuperBagOf is a shortcut for:
//
// td.Cmp(t, got, td.SuperBagOf(expectedItems...), args...)
//
// See [SuperBagOf] 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 CmpSuperBagOf(t TestingT, got any, expectedItems []any, args ...any) bool {
t.Helper()
return Cmp(t, got, SuperBagOf(expectedItems...), args...)
}
// CmpSuperJSONOf is a shortcut for:
//
// td.Cmp(t, got, td.SuperJSONOf(expectedJSON, params...), args...)
//
// See [SuperJSONOf] 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 CmpSuperJSONOf(t TestingT, got, expectedJSON any, params []any, args ...any) bool {
t.Helper()
return Cmp(t, got, SuperJSONOf(expectedJSON, params...), args...)
}
// CmpSuperMapOf is a shortcut for:
//
// td.Cmp(t, got, td.SuperMapOf(model, expectedEntries), args...)
//
// See [SuperMapOf] for details.
//
// [SuperMapOf] optional parameter expectedEntries is here mandatory.
// nil value should be passed to mimic its absence in
// original [SuperMapOf] 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 CmpSuperMapOf(t TestingT, got, model any, expectedEntries MapEntries, args ...any) bool {
t.Helper()
return Cmp(t, got, SuperMapOf(model, expectedEntries), args...)
}
// CmpSuperSetOf is a shortcut for:
//
// td.Cmp(t, got, td.SuperSetOf(expectedItems...), args...)
//
// See [SuperSetOf] 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 CmpSuperSetOf(t TestingT, got any, expectedItems []any, args ...any) bool {
t.Helper()
return Cmp(t, got, SuperSetOf(expectedItems...), args...)
}
// CmpSuperSliceOf is a shortcut for:
//
// td.Cmp(t, got, td.SuperSliceOf(model, expectedEntries), args...)
//
// See [SuperSliceOf] for details.
//
// [SuperSliceOf] optional parameter expectedEntries is here mandatory.
// nil value should be passed to mimic its absence in
// original [SuperSliceOf] 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 CmpSuperSliceOf(t TestingT, got, model any, expectedEntries ArrayEntries, args ...any) bool {
t.Helper()
return Cmp(t, got, SuperSliceOf(model, expectedEntries), args...)
}
// CmpTruncTime is a shortcut for:
//
// td.Cmp(t, got, td.TruncTime(expectedTime, trunc), args...)
//
// See [TruncTime] for details.
//
// [TruncTime] optional parameter trunc is here mandatory.
// 0 value should be passed to mimic its absence in
// original [TruncTime] 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 CmpTruncTime(t TestingT, got, expectedTime any, trunc time.Duration, args ...any) bool {
t.Helper()
return Cmp(t, got, TruncTime(expectedTime, trunc), args...)
}
// CmpValues is a shortcut for:
//
// td.Cmp(t, got, td.Values(val), args...)
//
// See [Values] 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 CmpValues(t TestingT, got, val any, args ...any) bool {
t.Helper()
return Cmp(t, got, Values(val), args...)
}
// CmpZero is a shortcut for:
//
// td.Cmp(t, got, td.Zero(), args...)
//
// See [Zero] 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 CmpZero(t TestingT, got any, args ...any) bool {
t.Helper()
return Cmp(t, got, Zero(), args...)
}
go-testdeep-1.15.0/td/cmp_funcs_misc.go 0000664 0000000 0000000 00000020026 15144170453 0017750 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (
"runtime"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/types"
"github.com/maxatome/go-testdeep/internal/util"
)
// CmpTrue is a shortcut for:
//
// td.Cmp(t, got, true, args...)
//
// Returns true if the test is OK, false if it fails.
//
// td.CmpTrue(t, IsAvailable(x), "x should be available")
//
// 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.
//
// See also [CmpFalse].
func CmpTrue(t TestingT, got bool, args ...any) bool {
t.Helper()
return Cmp(t, got, true, args...)
}
// CmpFalse is a shortcut for:
//
// td.Cmp(t, got, false, args...)
//
// Returns true if the test is OK, false if it fails.
//
// td.CmpFalse(t, IsAvailable(x), "x should not be available")
//
// 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.
//
// See also [CmpTrue].
func CmpFalse(t TestingT, got bool, args ...any) bool {
t.Helper()
return Cmp(t, got, false, args...)
}
func cmpError(ctx ctxerr.Context, t TestingT, got error, args ...any) bool {
if got != nil {
return true
}
t.Helper()
formatError(t,
ctx.FailureIsFatal,
&ctxerr.Error{
Context: ctx,
Message: "should be an error",
Got: types.RawString("nil"),
Expected: types.RawString("non-nil error"),
},
args...)
return false
}
func cmpNoError(ctx ctxerr.Context, t TestingT, got error, args ...any) bool {
if got == nil {
return true
}
t.Helper()
formatError(t,
ctx.FailureIsFatal,
&ctxerr.Error{
Context: ctx,
Message: "should NOT be an error",
Got: got,
Expected: types.RawString("nil"),
},
args...)
return false
}
// CmpError checks that got is non-nil error.
//
// _, err := MyFunction(1, 2, 3)
// td.CmpError(t, err, "MyFunction(1, 2, 3) should return an error")
//
// 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.
//
// See also [CmpNoError].
func CmpError(t TestingT, got error, args ...any) bool {
t.Helper()
return cmpError(newContext(t), t, got, args...)
}
// CmpNoError checks that got is nil error.
//
// value, err := MyFunction(1, 2, 3)
// if td.CmpNoError(t, err) {
// // one can now check value...
// }
//
// 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.
//
// See also [CmpError].
func CmpNoError(t TestingT, got error, args ...any) bool {
t.Helper()
return cmpNoError(newContext(t), t, got, args...)
}
func cmpPanic(ctx ctxerr.Context, t TestingT, fn func(), expected any, args ...any) bool {
t.Helper()
if ctx.Path.Len() == 1 && ctx.Path.String() == contextDefaultRootName {
ctx.Path = ctxerr.NewPath(contextPanicRootName)
}
var (
panicked bool
panicParam any
)
func() {
defer func() { panicParam = recover() }()
panicked = true
fn()
panicked = false
}()
if !panicked {
formatError(t,
ctx.FailureIsFatal,
&ctxerr.Error{
Context: ctx,
Message: "should have panicked",
Summary: ctxerr.NewSummary("did not panic"),
},
args...)
return false
}
return cmpDeeply(ctx.AddCustomLevel("→panic()"), t, panicParam, expected, args...)
}
func cmpNotPanic(ctx ctxerr.Context, t TestingT, fn func(), args ...any) bool {
var (
panicked bool
stackTrace types.RawString
)
func() {
defer func() {
panicParam := recover()
if panicked {
buf := make([]byte, 8192)
n := runtime.Stack(buf, false)
for ; n > 0; n-- {
if buf[n-1] != '\n' {
break
}
}
stackTrace = types.RawString("panic: " + util.ToString(panicParam) + "\n\n" +
string(buf[:n]))
}
}()
panicked = true
fn()
panicked = false
}()
if !panicked {
return true
}
t.Helper()
if ctx.Path.Len() == 1 && ctx.Path.String() == contextDefaultRootName {
ctx.Path = ctxerr.NewPath(contextPanicRootName)
}
formatError(t,
ctx.FailureIsFatal,
&ctxerr.Error{
Context: ctx,
Message: "should NOT have panicked",
Got: stackTrace,
Expected: types.RawString("not panicking at all"),
},
args...)
return false
}
// CmpPanic calls fn and checks a panic() occurred with the
// expectedPanic parameter. It returns true only if both conditions
// are fulfilled.
//
// Note that calling panic(nil) in fn body is always detected as a
// panic. [runtime] package says: before Go 1.21, programs that called
// panic(nil) observed recover returning nil. Starting in Go 1.21,
// programs that call panic(nil) observe recover returning a
// [*runtime.PanicNilError]. Programs can change back to the old
// behavior by setting GODEBUG=panicnil=1.
//
// td.CmpPanic(t,
// func() { panic("I am panicking!") },
// "I am panicking!",
// "The function should panic with the right string") // succeeds
//
// td.CmpPanic(t,
// func() { panic("I am panicking!") },
// Contains("panicking!"),
// "The function should panic with a string containing `panicking!`") // succeeds
//
// td.CmpPanic(t, func() { panic(nil) }, nil, "Checks for panic(nil)") // succeeds
//
// 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.
//
// See also [CmpNotPanic].
func CmpPanic(t TestingT, fn func(), expectedPanic any, args ...any) bool {
t.Helper()
return cmpPanic(newContext(t), t, fn, expectedPanic, args...)
}
// CmpNotPanic calls fn and checks no panic() occurred. If a panic()
// occurred false is returned then the panic() parameter and the stack
// trace appear in the test report.
//
// Note that calling panic(nil) in fn body is always detected as a
// panic. [runtime] package says: before Go 1.21, programs that called
// panic(nil) observed recover returning nil. Starting in Go 1.21,
// programs that call panic(nil) observe recover returning a
// [*runtime.PanicNilError]. Programs can change back to the old
// behavior by setting GODEBUG=panicnil=1.
//
// td.CmpNotPanic(t, func() {}) // succeeds as function does not panic
//
// td.CmpNotPanic(t, func() { panic("I am panicking!") }) // fails
// td.CmpNotPanic(t, func() { panic(nil) }) // fails too
//
// 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.
//
// See also [CmpPanic].
func CmpNotPanic(t TestingT, fn func(), args ...any) bool {
t.Helper()
return cmpNotPanic(newContext(t), t, fn, args...)
}
go-testdeep-1.15.0/td/cmp_funcs_misc_121_test.go 0000664 0000000 0000000 00000004342 15144170453 0021375 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2025, 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.21
// +build go1.21
// Until go 1.21 in go.mod
//go:debug panicnil=0
package td_test
import (
"fmt"
"runtime"
"testing"
"github.com/maxatome/go-testdeep/td"
)
func ExampleCmpPanic() {
t := &testing.T{}
ok := td.CmpPanic(t,
func() { panic("I am panicking!") }, "I am panicking!",
"Checks for panic")
fmt.Println("checks exact panic() string:", ok)
// Can use TestDeep operator too
ok = td.CmpPanic(t,
func() { panic("I am panicking!") }, td.Contains("panicking!"),
"Checks for panic")
fmt.Println("checks panic() sub-string:", ok)
// Can detect panic(nil)
// Before Go 1.21, programs that called panic(nil) observed recover
// returning nil. Starting in Go 1.21, programs that call panic(nil)
// observe recover returning a *PanicNilError. Programs can change
// back to the old behavior by setting GODEBUG=panicnil=1.
// See https://pkg.go.dev/runtime#PanicNilError
ok = td.CmpPanic(t, func() { panic(nil) }, &runtime.PanicNilError{},
"Checks for panic(nil)")
fmt.Println("checks for panic(nil):", ok)
// As well as structured data panic
type PanicStruct struct {
Error string
Code int
}
ok = td.CmpPanic(t,
func() {
panic(PanicStruct{Error: "Memory violation", Code: 11})
},
PanicStruct{
Error: "Memory violation",
Code: 11,
})
fmt.Println("checks exact panic() struct:", ok)
// or combined with TestDeep operators too
ok = td.CmpPanic(t,
func() {
panic(PanicStruct{Error: "Memory violation", Code: 11})
},
td.Struct(PanicStruct{}, td.StructFields{
"Code": td.Between(10, 20),
}))
fmt.Println("checks panic() struct against TestDeep operators:", ok)
// Of course, do not panic = test failure, even for expected nil
// panic parameter
ok = td.CmpPanic(t, func() {}, nil)
fmt.Println("checks a panic occurred:", ok)
// Output:
// checks exact panic() string: true
// checks panic() sub-string: true
// checks for panic(nil): true
// checks exact panic() struct: true
// checks panic() struct against TestDeep operators: true
// checks a panic occurred: false
}
go-testdeep-1.15.0/td/cmp_funcs_misc_test.go 0000664 0000000 0000000 00000003452 15144170453 0021013 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (
"fmt"
"testing"
"github.com/maxatome/go-testdeep/td"
)
func ExampleCmpTrue() {
t := &testing.T{}
got := true
ok := td.CmpTrue(t, got, "check that got is true!")
fmt.Println(ok)
got = false
ok = td.CmpTrue(t, got, "check that got is true!")
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleCmpFalse() {
t := &testing.T{}
got := false
ok := td.CmpFalse(t, got, "check that got is false!")
fmt.Println(ok)
got = true
ok = td.CmpFalse(t, got, "check that got is false!")
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleCmpError() {
t := &testing.T{}
got := fmt.Errorf("Error #%d", 42)
ok := td.CmpError(t, got, "An error occurred")
fmt.Println(ok)
got = nil
ok = td.CmpError(t, got, "An error occurred") // fails
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleCmpNoError() {
t := &testing.T{}
got := fmt.Errorf("Error #%d", 42)
ok := td.CmpNoError(t, got, "An error occurred") // fails
fmt.Println(ok)
got = nil
ok = td.CmpNoError(t, got, "An error occurred")
fmt.Println(ok)
// Output:
// false
// true
}
func ExampleCmpNotPanic() {
t := &testing.T{}
ok := td.CmpNotPanic(t, func() {})
fmt.Println("checks a panic DID NOT occur:", ok)
// Classic panic
ok = td.CmpNotPanic(t, func() { panic("I am panicking!") },
"Hope it does not panic!")
fmt.Println("still no panic?", ok)
// Can detect panic(nil)
ok = td.CmpNotPanic(t, func() { panic(nil) }, "Checks for panic(nil)")
fmt.Println("last no panic?", ok)
// Output:
// checks a panic DID NOT occur: true
// still no panic? false
// last no panic? false
}
go-testdeep-1.15.0/td/config.go 0000664 0000000 0000000 00000014170 15144170453 0016230 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (
"os"
"strconv"
"testing"
"github.com/maxatome/go-testdeep/internal/anchors"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/hooks"
"github.com/maxatome/go-testdeep/internal/visited"
)
// ContextConfig allows to configure finely how tests failures are rendered.
//
// See [NewT] function to use it.
type ContextConfig struct {
// RootName is the string used to represent the root of got data. It
// defaults to "DATA". For an HTTP response body, it could be "BODY"
// for example.
RootName string
forkedFromCtx *ctxerr.Context
// MaxErrors is the maximal number of errors to dump in case of Cmp*
// failure.
//
// It defaults to 10 except if the environment variable
// TESTDEEP_MAX_ERRORS is set. In this latter case, the
// TESTDEEP_MAX_ERRORS value is converted to an int and used as is.
//
// Setting it to 0 has the same effect as 1: only the first error
// will be dumped without the "Too many errors" error.
//
// Setting it to a negative number means no limit: all errors
// will be dumped.
MaxErrors int
anchors *anchors.Info
hooks *hooks.Info
// FailureIsFatal allows to Fatal() (instead of Error()) when a test
// fails. Using *testing.T or *testing.B instance as t.TB value, FailNow()
// is called behind the scenes when Fatal() is called. See testing
// documentation for details.
FailureIsFatal bool
// UseEqual allows to use the Equal method on got (if it exists) or
// on any of its component to compare got and expected values.
//
// The signature of the Equal method should be:
// (A) Equal(B) bool
// with B assignable to A.
//
// See time.Time as an example of accepted Equal() method.
//
// See (*T).UseEqual method to only apply this property to some
// specific types.
UseEqual bool
// BeLax allows to compare different but convertible types. If set
// to false (default), got and expected types must be the same. If
// set to true and expected type is convertible to got one, expected
// is first converted to go type before its comparison. See CmpLax
// function/method and Lax operator to set this flag without
// providing a specific configuration.
BeLax bool
// IgnoreUnexported allows to ignore unexported struct fields. Be
// careful about structs entirely composed of unexported fields
// (like time.Time for example). With this flag set to true, they
// are all equal. In such case it is advised to set UseEqual flag,
// to use (*T).UseEqual method or to add a Cmp hook using
// (*T).WithCmpHooks method.
//
// See (*T).IgnoreUnexported method to only apply this property to some
// specific types.
IgnoreUnexported bool
// TestDeepInGotOK allows to accept TestDeep operator in got Cmp*
// parameter. By default it is forbidden and a panic occurs, because
// most of the time it is a mistake to compare (expected, got)
// instead of official (got, expected).
TestDeepInGotOK bool
}
// Equal returns true if both c and o are equal. Only public fields
// are taken into account to check equality.
func (c ContextConfig) Equal(o ContextConfig) bool {
return c.RootName == o.RootName &&
c.MaxErrors == o.MaxErrors &&
c.FailureIsFatal == o.FailureIsFatal &&
c.UseEqual == o.UseEqual &&
c.BeLax == o.BeLax &&
c.IgnoreUnexported == o.IgnoreUnexported &&
c.TestDeepInGotOK == o.TestDeepInGotOK
}
// OriginalPath returns the current path when the [ContextConfig] has
// been built. It always returns ContextConfig.RootName except if c
// has been built by [Code] operator. See [Code] documentation for an
// example of use.
func (c ContextConfig) OriginalPath() string {
if c.forkedFromCtx == nil {
return c.RootName
}
return c.forkedFromCtx.Path.String()
}
const (
contextDefaultRootName = "DATA"
contextPanicRootName = "FUNCTION"
envMaxErrors = "TESTDEEP_MAX_ERRORS"
)
func getMaxErrorsFromEnv() int {
env := os.Getenv(envMaxErrors)
if env != "" {
n, err := strconv.Atoi(env)
if err == nil {
return n
}
}
return 10
}
// DefaultContextConfig is the default configuration used to render
// tests failures. If overridden, new settings will impact all Cmp*
// functions and [*T] methods (if not specifically configured.)
var DefaultContextConfig = ContextConfig{
RootName: contextDefaultRootName,
MaxErrors: getMaxErrorsFromEnv(),
FailureIsFatal: false,
UseEqual: false,
BeLax: false,
IgnoreUnexported: false,
TestDeepInGotOK: false,
}
func (c *ContextConfig) sanitize() {
if c.RootName == "" {
c.RootName = DefaultContextConfig.RootName
}
if c.MaxErrors == 0 {
c.MaxErrors = DefaultContextConfig.MaxErrors
}
}
// newContext creates a new ctxerr.Context using DefaultContextConfig
// configuration.
func newContext(t TestingT) ctxerr.Context {
if tt, ok := t.(*T); ok {
return newContextWithConfig(tt, tt.Config)
}
tb, _ := t.(testing.TB)
return newContextWithConfig(tb, DefaultContextConfig)
}
// newContextWithConfig creates a new ctxerr.Context using a specific
// configuration.
func newContextWithConfig(tb testing.TB, config ContextConfig) (ctx ctxerr.Context) {
config.sanitize()
ctx = ctxerr.Context{
Path: ctxerr.NewPath(config.RootName),
Visited: visited.NewVisited(),
MaxErrors: config.MaxErrors,
Anchors: config.anchors,
Hooks: config.hooks,
OriginalTB: tb,
FailureIsFatal: config.FailureIsFatal,
UseEqual: config.UseEqual,
BeLax: config.BeLax,
IgnoreUnexported: config.IgnoreUnexported,
TestDeepInGotOK: config.TestDeepInGotOK,
}
ctx.InitErrors()
return
}
// newBooleanContext creates a new boolean ctxerr.Context.
func newBooleanContext() ctxerr.Context {
return ctxerr.Context{
Visited: visited.NewVisited(),
BooleanError: true,
UseEqual: DefaultContextConfig.UseEqual,
BeLax: DefaultContextConfig.BeLax,
IgnoreUnexported: DefaultContextConfig.IgnoreUnexported,
TestDeepInGotOK: DefaultContextConfig.TestDeepInGotOK,
}
}
go-testdeep-1.15.0/td/config_test.go 0000664 0000000 0000000 00000004431 15144170453 0017266 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (
"os"
"testing"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/test"
)
func TestContext(t *testing.T) {
nctx := newContext(nil)
test.EqualStr(t, nctx.Path.String(), "DATA")
if nctx.OriginalTB != nil {
t.Error("OriginalTB should be nil")
}
nctx = newContext(t)
test.EqualStr(t, nctx.Path.String(), "DATA")
if nctxt, ok := nctx.OriginalTB.(*testing.T); test.IsTrue(t, ok, "%T", nctx.OriginalTB) {
if nctxt != t {
t.Errorf("OriginalTB, got=%p expected=%p", nctxt, t)
}
}
nctx = newContext(Require(t).UseEqual().TestDeepInGotOK())
_, ok := nctx.OriginalTB.(*T)
test.IsTrue(t, ok)
test.IsTrue(t, nctx.FailureIsFatal)
test.IsTrue(t, nctx.UseEqual)
test.IsTrue(t, nctx.TestDeepInGotOK)
test.EqualStr(t, nctx.Path.String(), "DATA")
nctx = newBooleanContext()
test.EqualStr(t, nctx.Path.String(), "")
if nctx.OriginalTB != nil {
t.Error("OriginalTB should be nil")
}
if newContextWithConfig(nil, ContextConfig{MaxErrors: -1}).CollectError(nil) != nil {
t.Errorf("ctx.CollectError(nil) should return nil")
}
ctx := ContextConfig{}
if ctx.Equal(DefaultContextConfig) {
t.Errorf("Empty ContextConfig should be ≠ from DefaultContextConfig")
}
ctx.sanitize()
if !ctx.Equal(DefaultContextConfig) {
t.Errorf("Sanitized empty ContextConfig should be = to DefaultContextConfig")
}
ctx.RootName = "PIPO"
test.EqualStr(t, ctx.OriginalPath(), "PIPO")
nctx = newContext(t)
nctx.Path = ctxerr.NewPath("BINGO[0].Zip")
ctx.forkedFromCtx = &nctx
test.EqualStr(t, ctx.OriginalPath(), "BINGO[0].Zip")
}
func TestGetMaxErrorsFromEnv(t *testing.T) {
oldEnv, set := os.LookupEnv(envMaxErrors)
defer func() {
if set {
os.Setenv(envMaxErrors, oldEnv) //nolint: errcheck
} else {
os.Unsetenv(envMaxErrors) //nolint: errcheck
}
}()
os.Setenv(envMaxErrors, "") //nolint: errcheck
test.EqualInt(t, getMaxErrorsFromEnv(), 10)
os.Setenv(envMaxErrors, "aaa") //nolint: errcheck
test.EqualInt(t, getMaxErrorsFromEnv(), 10)
os.Setenv(envMaxErrors, "-8") //nolint: errcheck
test.EqualInt(t, getMaxErrorsFromEnv(), -8)
}
go-testdeep-1.15.0/td/doc.go 0000664 0000000 0000000 00000020055 15144170453 0015527 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (from [go-testdeep]) allows extremely flexible deep
// comparison, it is built for testing.
//
// It is a go rewrite and adaptation of wonderful [Test::Deep] perl
// module.
//
// In golang, comparing data structure is usually done using
// [reflect.DeepEqual] or using a package that uses this function
// behind the scenes.
//
// This function works very well, but it is not flexible. Both
// compared structures must match exactly.
//
// The purpose of td package is to do its best to introduce this
// missing flexibility using ["operators"] when the expected value (or
// one of its component) cannot be matched exactly.
//
// See [go-testdeep] for details.
//
// For easy HTTP API testing, see the [tdhttp] helper.
//
// For tests suites also just as easy, see [tdsuite] helper.
//
// # Example of use
//
// Imagine a function returning a struct containing a newly created
// database record. The Id and the CreatedAt fields are set by the
// database layer:
//
// type Record struct {
// Id uint64
// Name string
// Age int
// CreatedAt time.Time
// }
//
// func CreateRecord(name string, age int) (*Record, error) {
// // Do INSERT INTO … and return newly created record or error if it failed
// }
//
// # Using standard testing package
//
// To check the freshly created record contents using standard testing
// package, we have to do something like that:
//
// import (
// "testing"
// "time"
// )
//
// func TestCreateRecord(t *testing.T) {
// before := time.Now().Truncate(time.Second)
// record, err := CreateRecord()
//
// if err != nil {
// t.Errorf("An error occurred: %s", err)
// } else {
// expected := Record{Name: "Bob", Age: 23}
//
// if record.Id == 0 {
// t.Error("Id probably not initialized")
// }
// if before.After(record.CreatedAt) ||
// time.Now().Before(record.CreatedAt) {
// t.Errorf("CreatedAt field not expected: %s", record.CreatedAt)
// }
// if record.Name != expected.Name {
// t.Errorf("Name field differs, got=%s, expected=%s",
// record.Name, expected.Name)
// }
// if record.Age != expected.Age {
// t.Errorf("Age field differs, got=%s, expected=%s",
// record.Age, expected.Age)
// }
// }
// }
//
// # Using basic go-testdeep approach
//
// td package, via its Cmp* functions, handles the tests and all the
// error message boiler plate. Let's do it:
//
// import (
// "testing"
// "time"
//
// "github.com/maxatome/go-testdeep/td"
// )
//
// func TestCreateRecord(t *testing.T) {
// before := time.Now().Truncate(time.Second)
// record, err := CreateRecord()
//
// if td.CmpNoError(t, err) {
// td.Cmp(t, record.Id, td.NotZero(), "Id initialized")
// td.Cmp(t, record.Name, "Bob")
// td.Cmp(t, record.Age, 23)
// td.Cmp(t, record.CreatedAt, td.Between(before, time.Now()))
// }
// }
//
// As we cannot guess the Id field value before its creation, we use
// the [NotZero] operator to check it is set by CreateRecord()
// call. The same it true for the creation date field
// CreatedAt. Thanks to the [Between] operator we can check it is set
// with a value included between the date before CreateRecord() call
// and the date just after.
//
// Note that if Id and CreatedAt could be known in advance, we could
// simply do:
//
// import (
// "testing"
// "time"
//
// "github.com/maxatome/go-testdeep/td"
// )
//
// func TestCreateRecord(t *testing.T) {
// before := time.Now().Truncate(time.Second)
// record, err := CreateRecord()
//
// if td.CmpNoError(t, err) {
// td.Cmp(t, record, &Record{
// Id: 1234,
// Name: "Bob",
// Age: 23,
// CreatedAt: time.Date(2019, time.May, 1, 12, 13, 14, 0, time.UTC),
// })
// }
// }
//
// But unfortunately, it is common not to know exactly the value of some
// fields…
//
// # Using advanced go-testdeep technique
//
// Of course we can test struct fields one by one, but with go-testdeep,
// the whole struct can be compared with one [Cmp] call.
//
// import (
// "testing"
// "time"
//
// "github.com/maxatome/go-testdeep/td"
// )
//
// func TestCreateRecord(t *testing.T) {
// before := time.Now().Truncate(time.Second)
// record, err := CreateRecord()
//
// if td.CmpNoError(t, err) {
// td.Cmp(t, record,
// td.Struct(
// &Record{
// Name: "Bob",
// Age: 23,
// },
// td.StructFields{
// "Id": td.NotZero(),
// "CreatedAt": td.Between(before, time.Now()),
// }),
// "Newly created record")
// }
// }
//
// See the use of the [Struct] operator. It is needed here to overcome
// the go static typing system and so use other go-testdeep operators
// for some fields, here [NotZero] and [Between].
//
// Not only structs can be compared. A lot of ["operators"] can be found
// below to cover most (all?) needed tests. See [TestDeep].
//
// # Using go-testdeep Cmp shortcuts
//
// The [Cmp] function is the keystone of this package, but to make
// the writing of tests even easier, the family of Cmp* functions are
// provided and act as shortcuts. Using [CmpStruct] function, the
// previous example can be written as:
//
// import (
// "testing"
// "time"
//
// "github.com/maxatome/go-testdeep/td"
// )
//
// func TestCreateRecord(t *testing.T) {
// before := time.Now().Truncate(time.Second)
// record, err := CreateRecord()
//
// if td.CmpNoError(t, err) {
// td.CmpStruct(t, record,
// &Record{
// Name: "Bob",
// Age: 23,
// },
// td.StructFields{
// "Id": td.NotZero(),
// "CreatedAt": td.Between(before, time.Now()),
// },
// "Newly created record")
// }
// }
//
// # Using T type
//
// [testing.T] can be encapsulated in [T] type, simplifying again the
// test:
//
// import (
// "testing"
// "time"
//
// "github.com/maxatome/go-testdeep/td"
// )
//
// func TestCreateRecord(tt *testing.T) {
// t := td.NewT(tt)
//
// before := time.Now().Truncate(time.Second)
// record, err := CreateRecord()
//
// if t.CmpNoError(err) {
// t.RootName("RECORD").Struct(record,
// &Record{
// Name: "Bob",
// Age: 23,
// },
// td.StructFields{
// "Id": td.NotZero(),
// "CreatedAt": td.Between(before, time.Now()),
// },
// "Newly created record")
// }
// }
//
// Note the use of [T.RootName] method, it allows to name what we are
// going to test, instead of the default "DATA".
//
// # A step further with operator anchoring
//
// Overcome the go static typing system using the [Struct] operator is
// sometimes heavy. Especially when structs are nested, as the [Struct]
// operator needs to be used for each level surrounding the level in
// which an operator is involved. Operator anchoring feature has been
// designed to avoid this heaviness:
//
// import (
// "testing"
// "time"
//
// "github.com/maxatome/go-testdeep/td"
// )
//
// func TestCreateRecord(tt *testing.T) {
// before := time.Now().Truncate(time.Second)
// record, err := CreateRecord()
//
// t := td.NewT(tt) // operator anchoring needs a *td.T instance
//
// if t.CmpNoError(err) {
// t.Cmp(record,
// &Record{
// Name: "Bob",
// Age: 23,
// ID: t.A(td.NotZero(), uint64(0)).(uint64),
// CreatedAt: t.A(td.Between(before, time.Now())).(time.Time),
// },
// "Newly created record")
// }
// }
//
// See the [T.A] method (or its full name alias [T.Anchor])
// documentation for details.
//
// [go-testdeep]: https://go-testdeep.zetta.rocks/
// [Test::Deep]: https://metacpan.org/pod/Test::Deep
// ["operators"]: https://go-testdeep.zetta.rocks/operators/
// [tdhttp]: https://pkg.go.dev/github.com/maxatome/go-testdeep/helpers/tdhttp
// [tdsuite]: https://pkg.go.dev/github.com/maxatome/go-testdeep/helpers/tdsuite
package td // import "github.com/maxatome/go-testdeep/td"
go-testdeep-1.15.0/td/equal.go 0000664 0000000 0000000 00000030752 15144170453 0016076 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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.
//
// deepValueEqual function is heavily based on reflect.deepValueEqual function
// licensed under the BSD-style license found in the LICENSE file in the
// golang repository: https://github.com/golang/go/blob/master/LICENSE
package td
import (
"fmt"
"reflect"
"github.com/maxatome/go-testdeep/helpers/tdutil"
"github.com/maxatome/go-testdeep/internal/color"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/dark"
"github.com/maxatome/go-testdeep/internal/types"
)
func isNilStr(isNil bool) types.RawString {
if isNil {
return "nil"
}
return "not nil"
}
func deepValueEqualFinal(ctx ctxerr.Context, got, expected reflect.Value) (err *ctxerr.Error) {
err = deepValueEqual(ctx, got, expected)
if err == nil {
// Try to merge pending errors
errMerge := ctx.MergeErrors()
if errMerge != nil {
return errMerge
}
}
return
}
func deepValueEqualFinalOK(ctx ctxerr.Context, got, expected reflect.Value) (bool, *ctxerr.Error) {
ctx = ctx.ResetErrors()
ctx.BooleanError = true
if err := deepValueEqualFinal(ctx, got, expected); err != nil {
if err == ctxerr.BooleanError {
return false, nil
}
// The error is a user error
return false, err
}
return true, nil
}
// nilHandler is called when one of got or expected is nil (but never
// both, it is caller responsibility).
func nilHandler(ctx ctxerr.Context, got, expected reflect.Value) *ctxerr.Error {
err := ctxerr.Error{}
if expected.IsValid() { // here: !got.IsValid()
if expected.Type().Implements(testDeeper) {
curOperator := dark.MustGetInterface(expected).(TestDeep)
if curOperator.GetLocation().IsInitialized() {
ctx.CurOperator = curOperator
}
if curOperator.HandleInvalid() {
return curOperator.Match(ctx, got)
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
// Special case if expected is a TestDeep operator which does
// not handle invalid values: the operator is not called, but
// for the user the error comes from it
} else if ctx.BooleanError {
return ctxerr.BooleanError
}
err.Expected = expected
} else { // here: !expected.IsValid() && got.IsValid()
switch got.Kind() {
// Special case: got is a nil interface, so consider as equal
// to expected nil.
case reflect.Interface:
if got.IsNil() {
return nil
}
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Slice:
// If BeLax, it is OK: we consider typed nil is equal to (untyped) nil
if ctx.BeLax && got.IsNil() {
return nil
}
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
err.Got = got
}
err.Message = "values differ"
return ctx.CollectError(&err)
}
func isCustomEqual(a, b reflect.Value) (bool, bool) {
aType, bType := a.Type(), b.Type()
equal, ok := aType.MethodByName("Equal")
if ok {
ft := equal.Type
if !ft.IsVariadic() &&
ft.NumIn() == 2 &&
ft.NumOut() == 1 &&
ft.In(0).AssignableTo(ft.In(1)) &&
ft.Out(0) == types.Bool &&
bType.AssignableTo(ft.In(1)) {
return true, equal.Func.Call([]reflect.Value{a, b})[0].Bool()
}
}
return false, false
}
// resolveAnchor does the same as ctx.Anchors.ResolveAnchor but checks
// whether v is valid and not already a TestDeep operator first.
func resolveAnchor(ctx ctxerr.Context, v reflect.Value) (reflect.Value, bool) {
if !v.IsValid() || v.Type().Implements(testDeeper) {
return v, false
}
return ctx.Anchors.ResolveAnchor(v)
}
func deepValueEqual(ctx ctxerr.Context, got, expected reflect.Value) (err *ctxerr.Error) {
if !ctx.TestDeepInGotOK {
// got must not implement testDeeper
if got.IsValid() && got.Type().Implements(testDeeper) {
panic(color.Bad("Found a TestDeep operator in got param, " +
"can only use it in expected one!"))
}
}
// Try to see if a TestDeep operator is anchored in expected
if op, ok := resolveAnchor(ctx, expected); ok {
expected = op
}
if got.IsValid() {
// Check if a Smuggle hook matches got type
if handled, e := ctx.Hooks.Smuggle(&got); handled && e != nil {
// ctx.BooleanError is always false here as hooks cannot be set globally
return ctx.CollectError(&ctxerr.Error{
Message: e.Error(),
Got: got,
Expected: expected,
})
}
}
if !got.IsValid() || !expected.IsValid() {
if got.IsValid() == expected.IsValid() {
return
}
return nilHandler(ctx, got, expected)
}
// Check if a Cmp hook matches got & expected types
if handled, e := ctx.Hooks.Cmp(got, expected); handled {
if e == nil {
return
}
// ctx.BooleanError is always false here as hooks cannot be set globally
return ctx.CollectError(&ctxerr.Error{
Message: e.Error(),
Got: got,
Expected: expected,
})
}
// Look for an Equal() method
if ctx.UseEqual || ctx.Hooks.UseEqual(got.Type()) {
hasEqual, isEqual := isCustomEqual(got, expected)
if hasEqual {
if isEqual {
return
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "got.Equal(expected) failed",
Got: got,
Expected: expected,
})
}
}
if got.Type() != expected.Type() {
if expected.Type().Implements(testDeeper) {
curOperator := dark.MustGetInterface(expected).(TestDeep)
// Resolve interface
if got.Kind() == reflect.Interface {
got = got.Elem()
if !got.IsValid() {
return nilHandler(ctx, got, expected)
}
}
if curOperator.GetLocation().IsInitialized() {
ctx.CurOperator = curOperator
}
return curOperator.Match(ctx, got)
}
// expected is not a TestDeep operator
if got.Type() == recvKindType || expected.Type() == recvKindType {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "values differ",
Got: got,
Expected: expected,
})
}
if ctx.BeLax && types.IsConvertible(expected, got.Type()) {
return deepValueEqual(ctx, got, expected.Convert(got.Type()))
}
// If got is an interface, try to see what is behind before failing
// Used by Set/Bag Match method in such cases:
// []any{123, "foo"} → Bag("foo", 123)
// Interface kind -^-----^ but String-^ and ^- Int kinds
if got.Kind() == reflect.Interface {
return deepValueEqual(ctx, got.Elem(), expected)
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(ctxerr.TypeMismatch(got.Type(), expected.Type()))
}
// if ctx.Depth > 10 { panic("deepValueEqual") } // for debugging
// Avoid looping forever on cyclic references
if ctx.Visited.Record(got, expected) {
return
}
switch got.Kind() {
case reflect.Array:
for i, l := 0, got.Len(); i < l; i++ {
err = deepValueEqual(ctx.AddArrayIndex(i),
got.Index(i), expected.Index(i))
if err != nil {
return
}
}
return
case reflect.Slice:
if got.IsNil() != expected.IsNil() {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "nil slice",
Got: isNilStr(got.IsNil()),
Expected: isNilStr(expected.IsNil()),
})
}
var (
gotLen = got.Len()
expectedLen = expected.Len()
)
if gotLen != expectedLen {
// Shortcut in boolean context
if ctx.BooleanError {
return ctxerr.BooleanError
}
} else {
if got.Pointer() == expected.Pointer() {
return
}
}
var maxLen int
if gotLen >= expectedLen {
maxLen = expectedLen
} else {
maxLen = gotLen
}
// Special case for internal tuple type: it is clearer to read
// TUPLE instead of DATA when an error occurs when using this type
if got.Type() == tupleType &&
ctx.Path.Len() == 1 && ctx.Path.String() == contextDefaultRootName {
ctx = ctx.ResetPath("TUPLE")
}
for i := 0; i < maxLen; i++ {
err = deepValueEqual(ctx.AddArrayIndex(i),
got.Index(i), expected.Index(i))
if err != nil {
return
}
}
if gotLen == expectedLen {
return
}
res := tdSetResult{
Kind: itemsSetResult,
// do not sort Extra/Mising here
}
if gotLen > expectedLen {
res.Extra = make([]reflect.Value, gotLen-expectedLen)
for i := expectedLen; i < gotLen; i++ {
res.Extra[i-expectedLen] = got.Index(i)
}
} else {
res.Missing = make([]reflect.Value, expectedLen-gotLen)
for i := gotLen; i < expectedLen; i++ {
res.Missing[i-gotLen] = expected.Index(i)
}
}
return ctx.CollectError(&ctxerr.Error{
Message: fmt.Sprintf("comparing slices, from index #%d", maxLen),
Summary: res.Summary(),
})
case reflect.Interface:
return deepValueEqual(ctx, got.Elem(), expected.Elem())
case reflect.Ptr:
if got.Pointer() == expected.Pointer() {
return
}
return deepValueEqual(ctx.AddPtr(1), got.Elem(), expected.Elem())
case reflect.Struct:
sType := got.Type()
ignoreUnexported := ctx.IgnoreUnexported || ctx.Hooks.IgnoreUnexported(sType)
for i, n := 0, got.NumField(); i < n; i++ {
field := sType.Field(i)
if ignoreUnexported && field.PkgPath != "" {
continue
}
err = deepValueEqual(ctx.AddField(field.Name),
got.Field(i), expected.Field(i))
if err != nil {
return
}
}
return
case reflect.Map:
if got.IsNil() != expected.IsNil() {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "nil map",
Got: isNilStr(got.IsNil()),
Expected: isNilStr(expected.IsNil()),
})
}
// Shortcut in boolean context
if ctx.BooleanError && got.Len() != expected.Len() {
return ctxerr.BooleanError
}
if got.Pointer() == expected.Pointer() {
return
}
var notFoundKeys []reflect.Value
foundKeys := map[any]bool{}
for _, vkey := range tdutil.MapSortedKeys(expected) {
gotValue := got.MapIndex(vkey)
if !gotValue.IsValid() {
notFoundKeys = append(notFoundKeys, vkey)
continue
}
err = deepValueEqual(ctx.AddMapKey(vkey),
gotValue, expected.MapIndex(vkey))
if err != nil {
return
}
foundKeys[dark.MustGetInterface(vkey)] = true
}
if got.Len() == len(foundKeys) {
if len(notFoundKeys) == 0 {
return
}
return ctx.CollectError(&ctxerr.Error{
Message: "comparing map",
Summary: (tdSetResult{
Kind: keysSetResult,
Missing: notFoundKeys,
Sort: true,
}).Summary(),
})
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
// Retrieve extra keys
res := tdSetResult{
Kind: keysSetResult,
Missing: notFoundKeys,
Extra: make([]reflect.Value, 0, got.Len()-len(foundKeys)),
Sort: true,
}
for _, vkey := range tdutil.MapSortedKeys(got) {
if !foundKeys[dark.MustGetInterface(vkey)] {
res.Extra = append(res.Extra, vkey)
}
}
return ctx.CollectError(&ctxerr.Error{
Message: "comparing map",
Summary: res.Summary(),
})
case reflect.Func:
if got.IsNil() && expected.IsNil() {
return
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
// Can't do better than this:
return ctx.CollectError(&ctxerr.Error{
Message: "functions mismatch",
Summary: ctxerr.NewSummary(""),
})
default:
// Normal equality suffices
if dark.MustGetInterface(got) == dark.MustGetInterface(expected) {
return
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "values differ",
Got: got,
Expected: expected,
})
}
}
func deepValueEqualOK(got, expected reflect.Value) bool {
return deepValueEqualFinal(newBooleanContext(), got, expected) == nil
}
// EqDeeply returns true if got matches expected. expected can
// be the same type as got is, or contains some [TestDeep] operators.
//
// got := "foobar"
// td.EqDeeply(got, "foobar") // returns true
// td.EqDeeply(got, td.HasPrefix("foo")) // returns true
func EqDeeply(got, expected any) bool {
return deepValueEqualOK(reflect.ValueOf(got), reflect.ValueOf(expected))
}
// EqDeeplyError returns nil if got matches expected. expected can be
// the same type as got is, or contains some [TestDeep] operators. If
// got does not match expected, the returned [*ctxerr.Error] contains
// the reason of the first mismatch detected.
//
// got := "foobar"
// if err := td.EqDeeplyError(got, "foobar"); err != nil {
// // …
// }
// if err := td.EqDeeplyError(got, td.HasPrefix("foo")); err != nil {
// // …
// }
func EqDeeplyError(got, expected any) error {
err := deepValueEqualFinal(newContext(nil),
reflect.ValueOf(got), reflect.ValueOf(expected))
if err == nil {
return nil
}
return err
}
go-testdeep-1.15.0/td/equal_examples_test.go 0000664 0000000 0000000 00000002433 15144170453 0021026 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2021, 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 (
"fmt"
"github.com/maxatome/go-testdeep/td"
)
func ExampleEqDeeply() {
type MyStruct struct {
Name string
Num int
Items []int
}
got := &MyStruct{
Name: "Foobar",
Num: 12,
Items: []int{4, 5, 9, 3, 8},
}
if td.EqDeeply(got,
td.Struct(&MyStruct{},
td.StructFields{
"Name": td.Re("^Foo"),
"Num": td.Between(10, 20),
"Items": td.ArrayEach(td.Between(3, 9)),
})) {
fmt.Println("Match!")
} else {
fmt.Println("NO!")
}
// Output:
// Match!
}
func ExampleEqDeeplyError() {
//line /testdeep/example.go:1
type MyStruct struct {
Name string
Num int
Items []int
}
got := &MyStruct{
Name: "Foobar",
Num: 12,
Items: []int{4, 5, 9, 3, 8},
}
err := td.EqDeeplyError(got,
td.Struct(&MyStruct{},
td.StructFields{
"Name": td.Re("^Foo"),
"Num": td.Between(10, 20),
"Items": td.ArrayEach(td.Between(3, 8)),
}))
if err != nil {
fmt.Println(err)
}
// Output:
// DATA.Items[2]: values differ
// got: 9
// expected: 3 ≤ got ≤ 8
// [under operator Between at example.go:18]
}
go-testdeep-1.15.0/td/equal_test.go 0000664 0000000 0000000 00000046601 15144170453 0017135 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-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"
"time"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
type ItemPropertyKind uint8
type ItemProperty struct {
name string
kind ItemPropertyKind
value any
}
// Array.
func TestEqualArray(t *testing.T) {
checkOK(t, [8]int{1, 2}, [8]int{1, 2})
checkError(t, [8]int{1, 2}, [8]int{1, 3},
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[1]"),
Got: mustBe("2"),
Expected: mustBe("3"),
})
oldMaxErrors := td.DefaultContextConfig.MaxErrors
defer func() { td.DefaultContextConfig.MaxErrors = oldMaxErrors }()
t.Run("DefaultContextConfig.MaxErrors = 2",
func(t *testing.T) {
td.DefaultContextConfig.MaxErrors = 2
err := td.EqDeeplyError([8]int{1, 2, 3, 4}, [8]int{1, 42, 43, 44})
if err == nil {
t.Errorf("An Error should have occurred")
return
}
matchError(t, err.(*ctxerr.Error),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[1]"),
Got: mustBe("2"),
Expected: mustBe("42"),
Next: &expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[2]"),
Got: mustBe("3"),
Expected: mustBe("43"),
Next: &expectedError{
Message: mustBe(ctxerr.ErrTooManyErrors.Message),
},
},
}, false)
})
t.Run("DefaultContextConfig.MaxErrors = -1 (aka all errors)",
func(t *testing.T) {
td.DefaultContextConfig.MaxErrors = -1
err := td.EqDeeplyError([8]int{1, 2, 3, 4}, [8]int{1, 42, 43, 44})
if err == nil {
t.Errorf("An Error should have occurred")
return
}
if !matchError(t, err.(*ctxerr.Error),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[1]"),
Got: mustBe("2"),
Expected: mustBe("42"),
Next: &expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[2]"),
Got: mustBe("3"),
Expected: mustBe("43"),
Next: &expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[3]"),
Got: mustBe("4"),
Expected: mustBe("44"),
},
},
}, false) {
return
}
})
}
// Slice.
func TestEqualSlice(t *testing.T) {
checkOK(t, []int{1, 2}, []int{1, 2})
// Same pointer
array := [...]int{2, 1, 4, 3}
checkOK(t, array[:], array[:])
checkOK(t, ([]int)(nil), ([]int)(nil))
// Same pointer, but not same len
checkError(t, array[:2], array[:],
expectedError{
Message: mustBe("comparing slices, from index #2"),
Path: mustBe("DATA"),
// Missing items are not sorted
Summary: mustBe(`Missing 2 items: (4,
3)`),
})
checkError(t, []int{1, 2}, []int{1, 2, 3},
expectedError{
Message: mustBe("comparing slices, from index #2"),
Path: mustBe("DATA"),
Summary: mustBe(`Missing item: (3)`),
})
checkError(t, []int{1, 2, 3}, []int{1, 2},
expectedError{
Message: mustBe("comparing slices, from index #2"),
Path: mustBe("DATA"),
Summary: mustBe(`Extra item: (3)`),
})
checkError(t, []int{1, 2}, ([]int)(nil),
expectedError{
Message: mustBe("nil slice"),
Path: mustBe("DATA"),
Got: mustBe("not nil"),
Expected: mustBe("nil"),
})
checkError(t, ([]int)(nil), []int{1, 2},
expectedError{
Message: mustBe("nil slice"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("not nil"),
})
checkError(t, []int{1, 2}, []int{1, 3},
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[1]"),
Got: mustBe("2"),
Expected: mustBe("3"),
})
}
// Interface.
func TestEqualInterface(t *testing.T) {
checkOK(t, []any{1, "foo"}, []any{1, "foo"})
checkOK(t, []any{1, nil}, []any{1, nil})
checkError(t, []any{1, nil}, []any{1, "foo"},
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[1]"),
Got: mustBe("nil"),
Expected: mustBe(`"foo"`),
})
checkError(t, []any{1, "foo"}, []any{1, nil},
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[1]"),
Got: mustBe(`"foo"`),
Expected: mustBe("nil"),
})
checkError(t, []any{1, "foo"}, []any{1, 12},
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA[1]"),
Got: mustBe("string"),
Expected: mustBe("int"),
})
}
// Ptr.
func TestEqualPtr(t *testing.T) {
expected := 12
gotOK := expected
gotBad := 13
checkOK(t, &gotOK, &expected)
checkOK(t, &expected, &expected) // Same pointer
checkError(t, &gotBad, &expected,
expectedError{
Message: mustBe("values differ"),
Path: mustBe("*DATA"),
Got: mustBe("13"),
Expected: mustBe("12"),
})
}
// Struct.
func TestEqualStruct(t *testing.T) {
checkOK(t,
ItemProperty{ // got
name: "foo",
kind: 12,
value: "bar",
},
ItemProperty{ // expected
name: "foo",
kind: 12,
value: "bar",
})
checkError(t,
ItemProperty{ // got
name: "foo",
kind: 12,
value: 12,
},
ItemProperty{ // expected
name: "foo",
kind: 12,
value: "bar",
},
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA.value"),
Got: mustBe("int"),
Expected: mustBe("string"),
})
type SType struct {
Public int
private string
}
checkOK(t,
SType{Public: 42, private: "test"},
SType{Public: 42, private: "test"})
checkError(t,
SType{Public: 42, private: "test"},
SType{Public: 42},
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.private"),
Got: mustBe(`"test"`),
Expected: mustBe(`""`),
})
defer func() { td.DefaultContextConfig.IgnoreUnexported = false }()
td.DefaultContextConfig.IgnoreUnexported = true
checkOK(t,
SType{Public: 42, private: "test"},
SType{Public: 42})
// Be careful with structs containing only private fields
checkOK(t,
ItemProperty{
name: "foo",
kind: 12,
value: "bar",
},
ItemProperty{})
}
// Map.
func TestEqualMap(t *testing.T) {
checkOK(t, map[string]int{}, map[string]int{})
checkOK(t, (map[string]int)(nil), (map[string]int)(nil))
expected := map[string]int{"foo": 1, "bar": 4}
checkOK(t, map[string]int{"foo": 1, "bar": 4}, expected)
checkOK(t, expected, expected) // Same pointer
checkError(t, map[string]int{"foo": 1, "bar": 4}, (map[string]int)(nil),
expectedError{
Message: mustBe("nil map"),
Path: mustBe("DATA"),
Got: mustBe("not nil"),
Expected: mustBe("nil"),
})
checkError(t, (map[string]int)(nil), map[string]int{"foo": 1, "bar": 4},
expectedError{
Message: mustBe("nil map"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("not nil"),
})
checkError(t, map[string]int{"foo": 1, "bar": 4},
map[string]int{"foo": 1, "bar": 5},
expectedError{
Message: mustBe("values differ"),
Path: mustBe(`DATA["bar"]`),
Got: mustBe("4"),
Expected: mustBe("5"),
})
checkError(t, map[string]int{"foo": 1, "bar": 4, "test": 12},
map[string]int{"foo": 1, "bar": 4},
expectedError{
Message: mustBe("comparing map"),
Path: mustBe("DATA"),
Summary: mustMatch(`Extra key:[^"]+"test"`),
})
checkError(t, map[string]int{"foo": 1, "bar": 4},
map[string]int{"foo": 1, "bar": 4, "test": 12},
expectedError{
Message: mustBe("comparing map"),
Path: mustBe("DATA"),
Summary: mustMatch(`Missing key:[^"]+"test"`),
})
// Extra and missing keys are sorted
checkError(t, map[string]int{"foo": 1, "bar": 4, "test1+": 12, "test2+": 13},
map[string]int{"foo": 1, "bar": 4, "test1-": 12, "test2-": 13},
expectedError{
Message: mustBe("comparing map"),
Path: mustBe("DATA"),
Summary: mustBe(`Missing 2 keys: ("test1-",
"test2-")
Extra 2 keys: ("test1+",
"test2+")`),
})
}
// Func.
func TestEqualFunc(t *testing.T) {
checkOK(t, (func())(nil), (func())(nil))
checkError(t, func() {}, func() {},
expectedError{
Message: mustBe("functions mismatch"),
Path: mustBe("DATA"),
Summary: mustBe(""),
})
}
// Channel.
func TestEqualChannel(t *testing.T) {
var gotCh, expectedCh chan int
checkOK(t, gotCh, expectedCh) // nil channels
gotCh = make(chan int, 1)
checkOK(t, gotCh, gotCh) // exactly the same
checkError(t, gotCh, make(chan int, 1),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustContain("0x"), // hexadecimal pointer
Expected: mustContain("0x"), // hexadecimal pointer
})
}
// Others.
func TestEqualOthers(t *testing.T) {
type Private struct { //nolint: maligned
num int
num8 int8
num16 int16
num32 int32
num64 int64
numu uint
numu8 uint8
numu16 uint16
numu32 uint32
numu64 uint64
numf32 float32
numf64 float64
numc64 complex64
numc128 complex128
boolean bool
}
checkOK(t,
Private{ // got
num: 1,
num8: 8,
num16: 16,
num32: 32,
num64: 64,
numu: 1,
numu8: 8,
numu16: 16,
numu32: 32,
numu64: 64,
numf32: 32,
numf64: 64,
numc64: complex(64, 1),
numc128: complex(128, -1),
boolean: true,
},
Private{
num: 1,
num8: 8,
num16: 16,
num32: 32,
num64: 64,
numu: 1,
numu8: 8,
numu16: 16,
numu32: 32,
numu64: 64,
numf32: 32,
numf64: 64,
numc64: complex(64, 1),
numc128: complex(128, -1),
boolean: true,
})
checkError(t, Private{num: 1}, Private{num: 2},
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.num"),
Got: mustBe("1"),
Expected: mustBe("2"),
})
checkError(t, Private{num8: 1}, Private{num8: 2},
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.num8"),
Got: mustBe("(int8) 1"),
Expected: mustBe("(int8) 2"),
})
checkError(t, Private{num16: 1}, Private{num16: 2},
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.num16"),
Got: mustBe("(int16) 1"),
Expected: mustBe("(int16) 2"),
})
checkError(t, Private{num32: 1}, Private{num32: 2},
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.num32"),
Got: mustBe("(int32) 1"),
Expected: mustBe("(int32) 2"),
})
checkError(t, Private{num64: 1}, Private{num64: 2},
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.num64"),
Got: mustBe("(int64) 1"),
Expected: mustBe("(int64) 2"),
})
checkError(t, Private{numu: 1}, Private{numu: 2},
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.numu"),
Got: mustBe("(uint) 1"),
Expected: mustBe("(uint) 2"),
})
checkError(t, Private{numu8: 1}, Private{numu8: 2},
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.numu8"),
Got: mustBe("(uint8) 1"),
Expected: mustBe("(uint8) 2"),
})
checkError(t, Private{numu16: 1}, Private{numu16: 2},
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.numu16"),
Got: mustBe("(uint16) 1"),
Expected: mustBe("(uint16) 2"),
})
checkError(t, Private{numu32: 1}, Private{numu32: 2},
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.numu32"),
Got: mustBe("(uint32) 1"),
Expected: mustBe("(uint32) 2"),
})
checkError(t, Private{numu64: 1}, Private{numu64: 2},
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.numu64"),
Got: mustBe("(uint64) 1"),
Expected: mustBe("(uint64) 2"),
})
checkError(t, Private{numf32: 1}, Private{numf32: 2},
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.numf32"),
Got: mustBe("(float32) 1"),
Expected: mustBe("(float32) 2"),
})
checkError(t, Private{numf64: 1}, Private{numf64: 2},
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.numf64"),
Got: mustBe("1.0"),
Expected: mustBe("2.0"),
})
checkError(t, Private{numc64: complex(1, 2)}, Private{numc64: complex(2, 1)},
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.numc64"),
Got: mustBe("(complex64) (1+2i)"),
Expected: mustBe("(complex64) (2+1i)"),
})
checkError(t, Private{numc128: complex(1, 2)},
Private{numc128: complex(2, 1)},
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.numc128"),
Got: mustBe("(complex128) (1+2i)"),
Expected: mustBe("(complex128) (2+1i)"),
})
checkError(t, Private{boolean: true}, Private{boolean: false},
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.boolean"),
Got: mustBe("true"),
Expected: mustBe("false"),
})
}
// Private non-copyable fields.
func TestEqualReallyPrivate(t *testing.T) {
type Private struct {
channel chan int
}
ch := make(chan int, 3)
checkOKOrPanicIfUnsafeDisabled(t, Private{channel: ch}, Private{channel: ch})
}
func TestEqualRecursPtr(t *testing.T) {
type S struct {
Next *S
OK bool
}
expected1 := &S{}
expected1.Next = expected1
got := &S{}
got.Next = got
expected2 := &S{}
expected2.Next = expected2
checkOK(t, got, expected1)
checkOK(t, got, expected2)
got.Next = &S{OK: true}
expected1.Next = &S{OK: false}
checkError(t, got, expected1,
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.Next.OK"),
Got: mustBe("true"),
Expected: mustBe("false"),
})
}
func TestEqualRecursMap(t *testing.T) { // issue #101
gen := func() any {
type S struct {
Map map[int]S
}
m := make(map[int]S)
m[1] = S{
Map: m,
}
return m
}
checkOK(t, gen(), gen())
}
func TestEqualPanic(t *testing.T) {
test.CheckPanic(t,
func() {
td.EqDeeply(td.Ignore(), td.Ignore())
},
"Found a TestDeep operator in got param, can only use it in expected one!")
type tdInside struct {
Operator td.TestDeep
}
test.CheckPanic(t,
func() {
td.EqDeeply(&tdInside{}, &tdInside{})
},
"Found a TestDeep operator in got param, can only use it in expected one!")
t.Cleanup(func() { td.DefaultContextConfig.TestDeepInGotOK = false })
td.DefaultContextConfig.TestDeepInGotOK = true
test.IsTrue(t, td.EqDeeply(td.Ignore(), td.Ignore()))
test.IsTrue(t, td.EqDeeply(&tdInside{}, &tdInside{}))
}
type AssignableType1 struct{ x, Ignore int }
func (a AssignableType1) Equal(b AssignableType1) bool {
return a.x == b.x
}
type AssignableType2 struct{ x, Ignore int }
func (a AssignableType2) Equal(b struct{ x, Ignore int }) bool {
return a.x == b.x
}
type AssignablePtrType3 struct{ x, Ignore int }
func (a *AssignablePtrType3) Equal(b *AssignablePtrType3) bool {
if a == nil {
return b == nil
}
return b != nil && a.x == b.x
}
type BadEqual1 int
func (b BadEqual1) Equal(o ...BadEqual1) bool { return true } // IsVariadic
type BadEqual2 int
func (b BadEqual2) Equal() bool { return true } // NumIn() ≠ 2
type BadEqual3 int
func (b BadEqual3) Equal(o BadEqual3) (int, int) { return 1, 2 } // NumOut() ≠ 1
type BadEqual4 int
func (b BadEqual4) Equal(o string) int { return 1 } // !AssignableTo
type BadEqual5 int
func (b BadEqual5) Equal(o BadEqual5) int { return 1 } // Out=bool
func TestUseEqualGlobal(t *testing.T) {
defer func() { td.DefaultContextConfig.UseEqual = false }()
td.DefaultContextConfig.UseEqual = true
// Real case with time.Time
time1 := time.Now()
time2 := time1.Truncate(0)
if !time1.Equal(time2) || !time2.Equal(time1) {
t.Fatal("time.Equal() does not work as expected")
}
checkOK(t, time1, time2)
checkOK(t, time2, time1)
// AssignableType1
a1 := AssignableType1{x: 13, Ignore: 666}
b1 := AssignableType1{x: 13, Ignore: 789}
checkOK(t, a1, b1)
checkOK(t, b1, a1)
checkError(t, a1, AssignableType1{x: 14, Ignore: 666},
expectedError{
Message: mustBe("got.Equal(expected) failed"),
Path: mustBe("DATA"),
Got: mustContain("x: (int) 13,"),
Expected: mustContain("x: (int) 14,"),
})
bs := struct{ x, Ignore int }{x: 13, Ignore: 789}
checkOK(t, a1, bs) // bs type is assignable to AssignableType1
checkError(t, bs, a1,
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("struct { x int; Ignore int }"),
Expected: mustBe("td_test.AssignableType1"),
})
// AssignableType2
a2 := AssignableType2{x: 13, Ignore: 666}
b2 := AssignableType2{x: 13, Ignore: 789}
checkOK(t, a2, b2)
checkOK(t, b2, a2)
checkOK(t, a2, bs) // bs type is assignable to AssignableType2
checkError(t, bs, a2,
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("struct { x int; Ignore int }"),
Expected: mustBe("td_test.AssignableType2"),
})
// AssignablePtrType3
a3 := &AssignablePtrType3{x: 13, Ignore: 666}
b3 := &AssignablePtrType3{x: 13, Ignore: 789}
checkOK(t, a3, b3)
checkOK(t, b3, a3)
checkError(t, a3, &bs, // &bs type not assignable to AssignablePtrType3
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("*td_test.AssignablePtrType3"),
Expected: mustBe("*struct { x int; Ignore int }"),
})
checkOK(t, (*AssignablePtrType3)(nil), (*AssignablePtrType3)(nil))
checkError(t, (*AssignablePtrType3)(nil), b3,
expectedError{
Message: mustBe("got.Equal(expected) failed"),
Path: mustBe("DATA"),
Got: mustBe("(*td_test.AssignablePtrType3)()"),
Expected: mustContain("x: (int) 13,"),
})
checkError(t, b3, (*AssignablePtrType3)(nil),
expectedError{
Message: mustBe("got.Equal(expected) failed"),
Path: mustBe("DATA"),
Got: mustContain("x: (int) 13,"),
Expected: mustBe("(*td_test.AssignablePtrType3)()"),
})
// (A) Equal(A) method not found
checkError(t, BadEqual1(1), BadEqual1(2),
expectedError{
Message: mustBe("values differ"),
})
checkError(t, BadEqual2(1), BadEqual2(2),
expectedError{
Message: mustBe("values differ"),
})
checkError(t, BadEqual3(1), BadEqual3(2),
expectedError{
Message: mustBe("values differ"),
})
checkError(t, BadEqual4(1), BadEqual4(2),
expectedError{
Message: mustBe("values differ"),
})
checkError(t, BadEqual5(1), BadEqual5(2),
expectedError{
Message: mustBe("values differ"),
})
}
func TestUseEqualGlobalVsAnchor(t *testing.T) {
defer func() { td.DefaultContextConfig.UseEqual = false }()
td.DefaultContextConfig.UseEqual = true
tt := test.NewTestingTB(t.Name())
assert := td.Assert(tt)
type timeAnchored struct {
Time time.Time
}
td.CmpTrue(t,
assert.Cmp(
timeAnchored{Time: timeParse(t, "2022-05-31T06:00:00Z")},
timeAnchored{
Time: assert.A(td.Between(
timeParse(t, "2022-05-31T00:00:00Z"),
timeParse(t, "2022-05-31T12:00:00Z"),
)).(time.Time),
}))
}
func TestBeLaxGlobalt(t *testing.T) {
defer func() { td.DefaultContextConfig.BeLax = false }()
td.DefaultContextConfig.BeLax = true
// expected float64 value first converted to int64 before comparison
checkOK(t, int64(123), float64(123.56))
type MyInt int32
checkOK(t, int64(123), MyInt(123))
checkOK(t, MyInt(123), int64(123))
type gotStruct struct {
name string
age int
}
type expectedStruct struct {
name string
age int
}
checkOK(t,
gotStruct{
name: "bob",
age: 42,
},
expectedStruct{
name: "bob",
age: 42,
})
checkOK(t,
&gotStruct{
name: "bob",
age: 42,
},
&expectedStruct{
name: "bob",
age: 42,
})
}
go-testdeep-1.15.0/td/equal_unsafe_test.go 0000664 0000000 0000000 00000001543 15144170453 0020472 0 ustar 00root root 0000000 0000000 // 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.
//go:build !js && !appengine && !safe && !disableunsafe
// +build !js,!appengine,!safe,!disableunsafe
package td_test
import (
"testing"
)
// Map, unsafe access is mandatory here.
func TestEqualMapUnsafe(t *testing.T) {
type key struct{ k string }
type A struct{ x map[key]struct{} }
checkError(t, A{x: map[key]struct{}{{k: "z"}: {}}},
A{x: map[key]struct{}{{k: "x"}: {}}},
expectedError{
Message: mustBe("comparing map"),
Path: mustBe("DATA.x"),
Summary: mustBe(`Missing key: ((td_test.key) {
k: (string) (len=1) "x"
})
Extra key: ((td_test.key) {
k: (string) (len=1) "z"
})`),
})
}
go-testdeep-1.15.0/td/example_cmp_test.go 0000664 0000000 0000000 00000324335 15144170453 0020323 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2025, 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.
//
// DO NOT EDIT!!! AUTOMATICALLY GENERATED!!!
package td_test
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"math"
"os"
"regexp"
"strconv"
"strings"
"testing"
"time"
"github.com/maxatome/go-testdeep/td"
)
func ExampleCmpAll() {
t := &testing.T{}
got := "foo/bar"
// Checks got string against:
// "o/b" regexp *AND* "bar" suffix *AND* exact "foo/bar" string
ok := td.CmpAll(t, got, []any{td.Re("o/b"), td.HasSuffix("bar"), "foo/bar"},
"checks value %s", got)
fmt.Println(ok)
// Checks got string against:
// "o/b" regexp *AND* "bar" suffix *AND* exact "fooX/Ybar" string
ok = td.CmpAll(t, got, []any{td.Re("o/b"), td.HasSuffix("bar"), "fooX/Ybar"},
"checks value %s", got)
fmt.Println(ok)
// When some operators or values have to be reused and mixed between
// several calls, Flatten can be used to avoid boring and
// inefficient []any copies:
regOps := td.Flatten([]td.TestDeep{td.Re("o/b"), td.Re(`^fo`), td.Re(`ar$`)})
ok = td.CmpAll(t, got, []any{td.HasPrefix("foo"), regOps, td.HasSuffix("bar")},
"checks all operators against value %s", got)
fmt.Println(ok)
// Output:
// true
// false
// true
}
func ExampleCmpAny() {
t := &testing.T{}
got := "foo/bar"
// Checks got string against:
// "zip" regexp *OR* "bar" suffix
ok := td.CmpAny(t, got, []any{td.Re("zip"), td.HasSuffix("bar")},
"checks value %s", got)
fmt.Println(ok)
// Checks got string against:
// "zip" regexp *OR* "foo" suffix
ok = td.CmpAny(t, got, []any{td.Re("zip"), td.HasSuffix("foo")},
"checks value %s", got)
fmt.Println(ok)
// When some operators or values have to be reused and mixed between
// several calls, Flatten can be used to avoid boring and
// inefficient []any copies:
regOps := td.Flatten([]td.TestDeep{td.Re("a/c"), td.Re(`^xx`), td.Re(`ar$`)})
ok = td.CmpAny(t, got, []any{td.HasPrefix("xxx"), regOps, td.HasSuffix("zip")},
"check at least one operator matches value %s", got)
fmt.Println(ok)
// Output:
// true
// false
// true
}
func ExampleCmpArray_array() {
t := &testing.T{}
got := [3]int{42, 58, 26}
ok := td.CmpArray(t, got, [3]int{42}, td.ArrayEntries{1: 58, 2: td.Ignore()},
"checks array %v", got)
fmt.Println("Simple array:", ok)
ok = td.CmpArray(t, &got, &[3]int{42}, td.ArrayEntries{1: 58, 2: td.Ignore()},
"checks array %v", got)
fmt.Println("Array pointer:", ok)
ok = td.CmpArray(t, &got, (*[3]int)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()},
"checks array %v", got)
fmt.Println("Array pointer, nil model:", ok)
// Output:
// Simple array: true
// Array pointer: true
// Array pointer, nil model: true
}
func ExampleCmpArray_typedArray() {
t := &testing.T{}
type MyArray [3]int
got := MyArray{42, 58, 26}
ok := td.CmpArray(t, got, MyArray{42}, td.ArrayEntries{1: 58, 2: td.Ignore()},
"checks typed array %v", got)
fmt.Println("Typed array:", ok)
ok = td.CmpArray(t, &got, &MyArray{42}, td.ArrayEntries{1: 58, 2: td.Ignore()},
"checks pointer on typed array %v", got)
fmt.Println("Pointer on a typed array:", ok)
ok = td.CmpArray(t, &got, &MyArray{}, td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()},
"checks pointer on typed array %v", got)
fmt.Println("Pointer on a typed array, empty model:", ok)
ok = td.CmpArray(t, &got, (*MyArray)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()},
"checks pointer on typed array %v", got)
fmt.Println("Pointer on a typed array, nil model:", ok)
// Output:
// Typed array: true
// Pointer on a typed array: true
// Pointer on a typed array, empty model: true
// Pointer on a typed array, nil model: true
}
func ExampleCmpArrayEach_array() {
t := &testing.T{}
got := [3]int{42, 58, 26}
ok := td.CmpArrayEach(t, got, td.Between(25, 60),
"checks each item of array %v is in [25 .. 60]", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleCmpArrayEach_typedArray() {
t := &testing.T{}
type MyArray [3]int
got := MyArray{42, 58, 26}
ok := td.CmpArrayEach(t, got, td.Between(25, 60),
"checks each item of typed array %v is in [25 .. 60]", got)
fmt.Println(ok)
ok = td.CmpArrayEach(t, &got, td.Between(25, 60),
"checks each item of typed array pointer %v is in [25 .. 60]", got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleCmpArrayEach_slice() {
t := &testing.T{}
got := []int{42, 58, 26}
ok := td.CmpArrayEach(t, got, td.Between(25, 60),
"checks each item of slice %v is in [25 .. 60]", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleCmpArrayEach_typedSlice() {
t := &testing.T{}
type MySlice []int
got := MySlice{42, 58, 26}
ok := td.CmpArrayEach(t, got, td.Between(25, 60),
"checks each item of typed slice %v is in [25 .. 60]", got)
fmt.Println(ok)
ok = td.CmpArrayEach(t, &got, td.Between(25, 60),
"checks each item of typed slice pointer %v is in [25 .. 60]", got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleCmpBag() {
t := &testing.T{}
got := []int{1, 3, 5, 8, 8, 1, 2}
// Matches as all items are present
ok := td.CmpBag(t, got, []any{1, 1, 2, 3, 5, 8, 8},
"checks all items are present, in any order")
fmt.Println(ok)
// Does not match as got contains 2 times 1 and 8, and these
// duplicates are not expected
ok = td.CmpBag(t, got, []any{1, 2, 3, 5, 8},
"checks all items are present, in any order")
fmt.Println(ok)
got = []int{1, 3, 5, 8, 2}
// Duplicates of 1 and 8 are expected but not present in got
ok = td.CmpBag(t, got, []any{1, 1, 2, 3, 5, 8, 8},
"checks all items are present, in any order")
fmt.Println(ok)
// Matches as all items are present
ok = td.CmpBag(t, got, []any{1, 2, 3, 5, td.Gt(7)},
"checks all items are present, in any order")
fmt.Println(ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using expected... without copying it to a new
// []any slice, then use td.Flatten!
expected := []int{1, 2, 3, 5}
ok = td.CmpBag(t, got, []any{td.Flatten(expected), td.Gt(7)},
"checks all expected items are present, in any order")
fmt.Println(ok)
// Output:
// true
// false
// false
// true
// true
}
func ExampleCmpBetween_int() {
t := &testing.T{}
got := 156
ok := td.CmpBetween(t, got, 154, 156, td.BoundsInIn,
"checks %v is in [154 .. 156]", got)
fmt.Println(ok)
// BoundsInIn is implicit
ok = td.CmpBetween(t, got, 154, 156, td.BoundsInIn,
"checks %v is in [154 .. 156]", got)
fmt.Println(ok)
ok = td.CmpBetween(t, got, 154, 156, td.BoundsInOut,
"checks %v is in [154 .. 156[", got)
fmt.Println(ok)
ok = td.CmpBetween(t, got, 154, 156, td.BoundsOutIn,
"checks %v is in ]154 .. 156]", got)
fmt.Println(ok)
ok = td.CmpBetween(t, got, 154, 156, td.BoundsOutOut,
"checks %v is in ]154 .. 156[", got)
fmt.Println(ok)
// Output:
// true
// true
// false
// true
// false
}
func ExampleCmpBetween_string() {
t := &testing.T{}
got := "abc"
ok := td.CmpBetween(t, got, "aaa", "abc", td.BoundsInIn,
`checks "%v" is in ["aaa" .. "abc"]`, got)
fmt.Println(ok)
// BoundsInIn is implicit
ok = td.CmpBetween(t, got, "aaa", "abc", td.BoundsInIn,
`checks "%v" is in ["aaa" .. "abc"]`, got)
fmt.Println(ok)
ok = td.CmpBetween(t, got, "aaa", "abc", td.BoundsInOut,
`checks "%v" is in ["aaa" .. "abc"[`, got)
fmt.Println(ok)
ok = td.CmpBetween(t, got, "aaa", "abc", td.BoundsOutIn,
`checks "%v" is in ]"aaa" .. "abc"]`, got)
fmt.Println(ok)
ok = td.CmpBetween(t, got, "aaa", "abc", td.BoundsOutOut,
`checks "%v" is in ]"aaa" .. "abc"[`, got)
fmt.Println(ok)
// Output:
// true
// true
// false
// true
// false
}
func ExampleCmpBetween_time() {
t := &testing.T{}
before := time.Now()
occurredAt := time.Now()
after := time.Now()
ok := td.CmpBetween(t, occurredAt, before, after, td.BoundsInIn)
fmt.Println("It occurred between before and after:", ok)
type MyTime time.Time
ok = td.CmpBetween(t, MyTime(occurredAt), MyTime(before), MyTime(after), td.BoundsInIn)
fmt.Println("Same for convertible MyTime type:", ok)
ok = td.CmpBetween(t, MyTime(occurredAt), before, after, td.BoundsInIn)
fmt.Println("MyTime vs time.Time:", ok)
ok = td.CmpBetween(t, occurredAt, before, 10*time.Second, td.BoundsInIn)
fmt.Println("Using a time.Duration as TO:", ok)
ok = td.CmpBetween(t, MyTime(occurredAt), MyTime(before), 10*time.Second, td.BoundsInIn)
fmt.Println("Using MyTime as FROM and time.Duration as TO:", ok)
// Output:
// It occurred between before and after: true
// Same for convertible MyTime type: true
// MyTime vs time.Time: false
// Using a time.Duration as TO: true
// Using MyTime as FROM and time.Duration as TO: true
}
func ExampleCmpCap() {
t := &testing.T{}
got := make([]int, 0, 12)
ok := td.CmpCap(t, got, 12, "checks %v capacity is 12", got)
fmt.Println(ok)
ok = td.CmpCap(t, got, 0, "checks %v capacity is 0", got)
fmt.Println(ok)
got = nil
ok = td.CmpCap(t, got, 0, "checks %v capacity is 0", got)
fmt.Println(ok)
// Output:
// true
// false
// true
}
func ExampleCmpCap_operator() {
t := &testing.T{}
got := make([]int, 0, 12)
ok := td.CmpCap(t, got, td.Between(10, 12),
"checks %v capacity is in [10 .. 12]", got)
fmt.Println(ok)
ok = td.CmpCap(t, got, td.Gt(10),
"checks %v capacity is in [10 .. 12]", got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleCmpCode() {
t := &testing.T{}
got := "12"
ok := td.CmpCode(t, got, func(num string) bool {
n, err := strconv.Atoi(num)
return err == nil && n > 10 && n < 100
},
"checks string `%s` contains a number and this number is in ]10 .. 100[",
got)
fmt.Println(ok)
// Same with failure reason
ok = td.CmpCode(t, got, func(num string) (bool, string) {
n, err := strconv.Atoi(num)
if err != nil {
return false, "not a number"
}
if n > 10 && n < 100 {
return true, ""
}
return false, "not in ]10 .. 100["
},
"checks string `%s` contains a number and this number is in ]10 .. 100[",
got)
fmt.Println(ok)
// Same with failure reason thanks to error
ok = td.CmpCode(t, got, func(num string) error {
n, err := strconv.Atoi(num)
if err != nil {
return err
}
if n > 10 && n < 100 {
return nil
}
return fmt.Errorf("%d not in ]10 .. 100[", n)
},
"checks string `%s` contains a number and this number is in ]10 .. 100[",
got)
fmt.Println(ok)
// Output:
// true
// true
// true
}
func ExampleCmpCode_custom() {
t := &testing.T{}
got := 123
ok := td.CmpCode(t, got, func(t *td.T, num int) {
t.Cmp(num, 123)
})
fmt.Println("with one *td.T:", ok)
ok = td.CmpCode(t, got, func(assert, require *td.T, num int) {
assert.Cmp(num, 123)
require.Cmp(num, 123)
})
fmt.Println("with assert & require *td.T:", ok)
// Output:
// with one *td.T: true
// with assert & require *td.T: true
}
func ExampleCmpContains_arraySlice() {
t := &testing.T{}
ok := td.CmpContains(t, [...]int{11, 22, 33, 44}, 22)
fmt.Println("array contains 22:", ok)
ok = td.CmpContains(t, [...]int{11, 22, 33, 44}, td.Between(20, 25))
fmt.Println("array contains at least one item in [20 .. 25]:", ok)
ok = td.CmpContains(t, []int{11, 22, 33, 44}, 22)
fmt.Println("slice contains 22:", ok)
ok = td.CmpContains(t, []int{11, 22, 33, 44}, td.Between(20, 25))
fmt.Println("slice contains at least one item in [20 .. 25]:", ok)
ok = td.CmpContains(t, []int{11, 22, 33, 44}, []int{22, 33})
fmt.Println("slice contains the sub-slice [22, 33]:", ok)
// Output:
// array contains 22: true
// array contains at least one item in [20 .. 25]: true
// slice contains 22: true
// slice contains at least one item in [20 .. 25]: true
// slice contains the sub-slice [22, 33]: true
}
func ExampleCmpContains_nil() {
t := &testing.T{}
num := 123
got := [...]*int{&num, nil}
ok := td.CmpContains(t, got, nil)
fmt.Println("array contains untyped nil:", ok)
ok = td.CmpContains(t, got, (*int)(nil))
fmt.Println("array contains *int nil:", ok)
ok = td.CmpContains(t, got, td.Nil())
fmt.Println("array contains Nil():", ok)
ok = td.CmpContains(t, got, (*byte)(nil))
fmt.Println("array contains *byte nil:", ok) // types differ: *byte ≠ *int
// Output:
// array contains untyped nil: true
// array contains *int nil: true
// array contains Nil(): true
// array contains *byte nil: false
}
func ExampleCmpContains_map() {
t := &testing.T{}
ok := td.CmpContains(t, map[string]int{"foo": 11, "bar": 22, "zip": 33}, 22)
fmt.Println("map contains value 22:", ok)
ok = td.CmpContains(t, map[string]int{"foo": 11, "bar": 22, "zip": 33}, td.Between(20, 25))
fmt.Println("map contains at least one value in [20 .. 25]:", ok)
// Output:
// map contains value 22: true
// map contains at least one value in [20 .. 25]: true
}
func ExampleCmpContains_string() {
t := &testing.T{}
got := "foobar"
ok := td.CmpContains(t, got, "oob", "checks %s", got)
fmt.Println("contains `oob` string:", ok)
ok = td.CmpContains(t, got, []byte("oob"), "checks %s", got)
fmt.Println("contains `oob` []byte:", ok)
ok = td.CmpContains(t, got, 'b', "checks %s", got)
fmt.Println("contains 'b' rune:", ok)
ok = td.CmpContains(t, got, byte('a'), "checks %s", got)
fmt.Println("contains 'a' byte:", ok)
ok = td.CmpContains(t, got, td.Between('n', 'p'), "checks %s", got)
fmt.Println("contains at least one character ['n' .. 'p']:", ok)
// Output:
// contains `oob` string: true
// contains `oob` []byte: true
// contains 'b' rune: true
// contains 'a' byte: true
// contains at least one character ['n' .. 'p']: true
}
func ExampleCmpContains_stringer() {
t := &testing.T{}
// bytes.Buffer implements fmt.Stringer
got := bytes.NewBufferString("foobar")
ok := td.CmpContains(t, got, "oob", "checks %s", got)
fmt.Println("contains `oob` string:", ok)
ok = td.CmpContains(t, got, 'b', "checks %s", got)
fmt.Println("contains 'b' rune:", ok)
ok = td.CmpContains(t, got, byte('a'), "checks %s", got)
fmt.Println("contains 'a' byte:", ok)
ok = td.CmpContains(t, got, td.Between('n', 'p'), "checks %s", got)
fmt.Println("contains at least one character ['n' .. 'p']:", ok)
// Output:
// contains `oob` string: true
// contains 'b' rune: true
// contains 'a' byte: true
// contains at least one character ['n' .. 'p']: true
}
func ExampleCmpContains_error() {
t := &testing.T{}
got := errors.New("foobar")
ok := td.CmpContains(t, got, "oob", "checks %s", got)
fmt.Println("contains `oob` string:", ok)
ok = td.CmpContains(t, got, 'b', "checks %s", got)
fmt.Println("contains 'b' rune:", ok)
ok = td.CmpContains(t, got, byte('a'), "checks %s", got)
fmt.Println("contains 'a' byte:", ok)
ok = td.CmpContains(t, got, td.Between('n', 'p'), "checks %s", got)
fmt.Println("contains at least one character ['n' .. 'p']:", ok)
// Output:
// contains `oob` string: true
// contains 'b' rune: true
// contains 'a' byte: true
// contains at least one character ['n' .. 'p']: true
}
func ExampleCmpContainsKey() {
t := &testing.T{}
ok := td.CmpContainsKey(t, map[string]int{"foo": 11, "bar": 22, "zip": 33}, "foo")
fmt.Println(`map contains key "foo":`, ok)
ok = td.CmpContainsKey(t, map[int]bool{12: true, 24: false, 42: true, 51: false}, td.Between(40, 50))
fmt.Println("map contains at least a key in [40 .. 50]:", ok)
ok = td.CmpContainsKey(t, map[string]int{"FOO": 11, "bar": 22, "zip": 33}, td.Smuggle(strings.ToLower, "foo"))
fmt.Println(`map contains key "foo" without taking case into account:`, ok)
// Output:
// map contains key "foo": true
// map contains at least a key in [40 .. 50]: true
// map contains key "foo" without taking case into account: true
}
func ExampleCmpContainsKey_nil() {
t := &testing.T{}
num := 1234
got := map[*int]bool{&num: false, nil: true}
ok := td.CmpContainsKey(t, got, nil)
fmt.Println("map contains untyped nil key:", ok)
ok = td.CmpContainsKey(t, got, (*int)(nil))
fmt.Println("map contains *int nil key:", ok)
ok = td.CmpContainsKey(t, got, td.Nil())
fmt.Println("map contains Nil() key:", ok)
ok = td.CmpContainsKey(t, got, (*byte)(nil))
fmt.Println("map contains *byte nil key:", ok) // types differ: *byte ≠ *int
// Output:
// map contains untyped nil key: true
// map contains *int nil key: true
// map contains Nil() key: true
// map contains *byte nil key: false
}
func ExampleCmpEmpty() {
t := &testing.T{}
ok := td.CmpEmpty(t, nil) // special case: nil is considered empty
fmt.Println(ok)
// fails, typed nil is not empty (expect for channel, map, slice or
// pointers on array, channel, map slice and strings)
ok = td.CmpEmpty(t, (*int)(nil))
fmt.Println(ok)
ok = td.CmpEmpty(t, "")
fmt.Println(ok)
// Fails as 0 is a number, so not empty. Use Zero() instead
ok = td.CmpEmpty(t, 0)
fmt.Println(ok)
ok = td.CmpEmpty(t, (map[string]int)(nil))
fmt.Println(ok)
ok = td.CmpEmpty(t, map[string]int{})
fmt.Println(ok)
ok = td.CmpEmpty(t, ([]int)(nil))
fmt.Println(ok)
ok = td.CmpEmpty(t, []int{})
fmt.Println(ok)
ok = td.CmpEmpty(t, []int{3}) // fails, as not empty
fmt.Println(ok)
ok = td.CmpEmpty(t, [3]int{}) // fails, Empty() is not Zero()!
fmt.Println(ok)
// Output:
// true
// false
// true
// false
// true
// true
// true
// true
// false
// false
}
func ExampleCmpEmpty_pointers() {
t := &testing.T{}
type MySlice []int
ok := td.CmpEmpty(t, MySlice{}) // Ptr() not needed
fmt.Println(ok)
ok = td.CmpEmpty(t, &MySlice{})
fmt.Println(ok)
l1 := &MySlice{}
l2 := &l1
l3 := &l2
ok = td.CmpEmpty(t, &l3)
fmt.Println(ok)
// Works the same for array, map, channel and string
// But not for others types as:
type MyStruct struct {
Value int
}
ok = td.CmpEmpty(t, &MyStruct{}) // fails, use Zero() instead
fmt.Println(ok)
// Output:
// true
// true
// true
// false
}
func ExampleCmpErrorIs() {
t := &testing.T{}
err1 := fmt.Errorf("failure1")
err2 := fmt.Errorf("failure2: %w", err1)
err3 := fmt.Errorf("failure3: %w", err2)
err := fmt.Errorf("failure4: %w", err3)
ok := td.CmpErrorIs(t, err, err)
fmt.Println("error is itself:", ok)
ok = td.CmpErrorIs(t, err, err1)
fmt.Println("error is also err1:", ok)
ok = td.CmpErrorIs(t, err1, err)
fmt.Println("err1 is err:", ok)
// Output:
// error is itself: true
// error is also err1: true
// err1 is err: 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{}
got := 156
ok := td.CmpGt(t, got, 155, "checks %v is > 155", got)
fmt.Println(ok)
ok = td.CmpGt(t, got, 156, "checks %v is > 156", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleCmpGt_string() {
t := &testing.T{}
got := "abc"
ok := td.CmpGt(t, got, "abb", `checks "%v" is > "abb"`, got)
fmt.Println(ok)
ok = td.CmpGt(t, got, "abc", `checks "%v" is > "abc"`, got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleCmpGte_int() {
t := &testing.T{}
got := 156
ok := td.CmpGte(t, got, 156, "checks %v is ≥ 156", got)
fmt.Println(ok)
ok = td.CmpGte(t, got, 155, "checks %v is ≥ 155", got)
fmt.Println(ok)
ok = td.CmpGte(t, got, 157, "checks %v is ≥ 157", got)
fmt.Println(ok)
// Output:
// true
// true
// false
}
func ExampleCmpGte_string() {
t := &testing.T{}
got := "abc"
ok := td.CmpGte(t, got, "abc", `checks "%v" is ≥ "abc"`, got)
fmt.Println(ok)
ok = td.CmpGte(t, got, "abb", `checks "%v" is ≥ "abb"`, got)
fmt.Println(ok)
ok = td.CmpGte(t, got, "abd", `checks "%v" is ≥ "abd"`, got)
fmt.Println(ok)
// Output:
// true
// true
// false
}
func ExampleCmpHasPrefix() {
t := &testing.T{}
got := "foobar"
ok := td.CmpHasPrefix(t, got, "foo", "checks %s", got)
fmt.Println("using string:", ok)
ok = td.Cmp(t, []byte(got), td.HasPrefix("foo"), "checks %s", got)
fmt.Println("using []byte:", ok)
// Output:
// using string: true
// using []byte: true
}
func ExampleCmpHasPrefix_stringer() {
t := &testing.T{}
// bytes.Buffer implements fmt.Stringer
got := bytes.NewBufferString("foobar")
ok := td.CmpHasPrefix(t, got, "foo", "checks %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleCmpHasPrefix_error() {
t := &testing.T{}
got := errors.New("foobar")
ok := td.CmpHasPrefix(t, got, "foo", "checks %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleCmpHasSuffix() {
t := &testing.T{}
got := "foobar"
ok := td.CmpHasSuffix(t, got, "bar", "checks %s", got)
fmt.Println("using string:", ok)
ok = td.Cmp(t, []byte(got), td.HasSuffix("bar"), "checks %s", got)
fmt.Println("using []byte:", ok)
// Output:
// using string: true
// using []byte: true
}
func ExampleCmpHasSuffix_stringer() {
t := &testing.T{}
// bytes.Buffer implements fmt.Stringer
got := bytes.NewBufferString("foobar")
ok := td.CmpHasSuffix(t, got, "bar", "checks %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleCmpHasSuffix_error() {
t := &testing.T{}
got := errors.New("foobar")
ok := td.CmpHasSuffix(t, got, "bar", "checks %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleCmpIsa() {
t := &testing.T{}
type TstStruct struct {
Field int
}
got := TstStruct{Field: 1}
ok := td.CmpIsa(t, got, TstStruct{}, "checks got is a TstStruct")
fmt.Println(ok)
ok = td.CmpIsa(t, got, &TstStruct{},
"checks got is a pointer on a TstStruct")
fmt.Println(ok)
ok = td.CmpIsa(t, &got, &TstStruct{},
"checks &got is a pointer on a TstStruct")
fmt.Println(ok)
// Output:
// true
// false
// true
}
func ExampleCmpIsa_interface() {
t := &testing.T{}
got := bytes.NewBufferString("foobar")
ok := td.CmpIsa(t, got, (*fmt.Stringer)(nil),
"checks got implements fmt.Stringer interface")
fmt.Println(ok)
errGot := fmt.Errorf("An error #%d occurred", 123)
ok = td.CmpIsa(t, errGot, (*error)(nil),
"checks errGot is a *error or implements error interface")
fmt.Println(ok)
// As nil, is passed below, it is not an interface but nil… So it
// does not match
errGot = nil
ok = td.CmpIsa(t, errGot, (*error)(nil),
"checks errGot is a *error or implements error interface")
fmt.Println(ok)
// BUT if its address is passed, now it is OK as the types match
ok = td.CmpIsa(t, &errGot, (*error)(nil),
"checks &errGot is a *error or implements error interface")
fmt.Println(ok)
// Output:
// true
// true
// false
// true
}
func ExampleCmpJSON_basic() {
t := &testing.T{}
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
}{
Fullname: "Bob",
Age: 42,
}
ok := td.CmpJSON(t, got, `{"age":42,"fullname":"Bob"}`, nil)
fmt.Println("check got with age then fullname:", ok)
ok = td.CmpJSON(t, got, `{"fullname":"Bob","age":42}`, nil)
fmt.Println("check got with fullname then age:", ok)
ok = td.CmpJSON(t, got, `
// This should be the JSON representation of a struct
{
// A person:
"fullname": "Bob", // The name of this person
"age": 42 /* The age of this person:
- 42 of course
- to demonstrate a multi-lines comment */
}`, nil)
fmt.Println("check got with nicely formatted and commented JSON:", ok)
ok = td.CmpJSON(t, got, `{"fullname":"Bob","age":42,"gender":"male"}`, nil)
fmt.Println("check got with gender field:", ok)
ok = td.CmpJSON(t, got, `{"fullname":"Bob"}`, nil)
fmt.Println("check got with fullname only:", ok)
ok = td.CmpJSON(t, true, `true`, nil)
fmt.Println("check boolean got is true:", ok)
ok = td.CmpJSON(t, 42, `42`, nil)
fmt.Println("check numeric got is 42:", ok)
got = nil
ok = td.CmpJSON(t, got, `null`, nil)
fmt.Println("check nil got is null:", ok)
// Output:
// check got with age then fullname: true
// check got with fullname then age: true
// check got with nicely formatted and commented JSON: true
// check got with gender field: false
// check got with fullname only: false
// check boolean got is true: true
// check numeric got is 42: true
// check nil got is null: true
}
func ExampleCmpJSON_placeholders() {
t := &testing.T{}
type Person struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
Children []*Person `json:"children,omitempty"`
}
got := &Person{
Fullname: "Bob Foobar",
Age: 42,
}
ok := td.CmpJSON(t, got, `{"age": $1, "fullname": $2}`, []any{42, "Bob Foobar"})
fmt.Println("check got with numeric placeholders without operators:", ok)
ok = td.CmpJSON(t, got, `{"age": $1, "fullname": $2}`, []any{td.Between(40, 45), td.HasSuffix("Foobar")})
fmt.Println("check got with numeric placeholders:", ok)
ok = td.CmpJSON(t, got, `{"age": "$1", "fullname": "$2"}`, []any{td.Between(40, 45), td.HasSuffix("Foobar")})
fmt.Println("check got with double-quoted numeric placeholders:", ok)
ok = td.CmpJSON(t, got, `{"age": $age, "fullname": $name}`, []any{td.Tag("age", td.Between(40, 45)), td.Tag("name", td.HasSuffix("Foobar"))})
fmt.Println("check got with named placeholders:", ok)
got.Children = []*Person{
{Fullname: "Alice", Age: 28},
{Fullname: "Brian", Age: 22},
}
ok = td.CmpJSON(t, got, `{"age": $age, "fullname": $name, "children": $children}`, []any{td.Tag("age", td.Between(40, 45)), td.Tag("name", td.HasSuffix("Foobar")), td.Tag("children", td.Bag(
&Person{Fullname: "Brian", Age: 22},
&Person{Fullname: "Alice", Age: 28},
))})
fmt.Println("check got w/named placeholders, and children w/go structs:", ok)
ok = td.CmpJSON(t, got, `{"age": Between($1, $2), "fullname": HasSuffix($suffix), "children": Len(2)}`, []any{40, 45, td.Tag("suffix", "Foobar")})
fmt.Println("check got w/num & named placeholders:", ok)
// Output:
// check got with numeric placeholders without operators: true
// check got with numeric placeholders: true
// check got with double-quoted numeric placeholders: true
// check got with named placeholders: true
// check got w/named placeholders, and children w/go structs: true
// check got w/num & named placeholders: true
}
func ExampleCmpJSON_embedding() {
t := &testing.T{}
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
}{
Fullname: "Bob Foobar",
Age: 42,
}
ok := td.CmpJSON(t, got, `{"age": NotZero(), "fullname": NotEmpty()}`, nil)
fmt.Println("check got with simple operators:", ok)
ok = td.CmpJSON(t, got, `{"age": $^NotZero, "fullname": $^NotEmpty}`, nil)
fmt.Println("check got with operator shortcuts:", ok)
ok = td.CmpJSON(t, got, `
{
"age": Between(40, 42, "]]"), // in ]40; 42]
"fullname": All(
HasPrefix("Bob"),
HasSuffix("bar") // ← comma is optional here
)
}`, nil)
fmt.Println("check got with complex operators:", ok)
ok = td.CmpJSON(t, got, `
{
"age": Between(40, 42, "]["), // in ]40; 42[ → 42 excluded
"fullname": All(
HasPrefix("Bob"),
HasSuffix("bar"),
)
}`, nil)
fmt.Println("check got with complex operators:", ok)
ok = td.CmpJSON(t, got, `
{
"age": Between($1, $2, $3), // in ]40; 42]
"fullname": All(
HasPrefix($4),
HasSuffix("bar") // ← comma is optional here
)
}`, []any{40, 42, td.BoundsOutIn, "Bob"})
fmt.Println("check got with complex operators, w/placeholder args:", ok)
// Output:
// check got with simple operators: true
// check got with operator shortcuts: true
// check got with complex operators: true
// check got with complex operators: false
// check got with complex operators, w/placeholder args: true
}
func ExampleCmpJSON_rawStrings() {
t := &testing.T{}
type details struct {
Address string `json:"address"`
Car string `json:"car"`
}
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
Details details `json:"details"`
}{
Fullname: "Foo Bar",
Age: 42,
Details: details{
Address: "something",
Car: "Peugeot",
},
}
ok := td.CmpJSON(t, got, `
{
"fullname": HasPrefix("Foo"),
"age": Between(41, 43),
"details": SuperMapOf({
"address": NotEmpty, // () are optional when no parameters
"car": Any("Peugeot", "Tesla", "Jeep") // any of these
})
}`, nil)
fmt.Println("Original:", ok)
ok = td.CmpJSON(t, got, `
{
"fullname": "$^HasPrefix(\"Foo\")",
"age": "$^Between(41, 43)",
"details": "$^SuperMapOf({\n\"address\": NotEmpty,\n\"car\": Any(\"Peugeot\", \"Tesla\", \"Jeep\")\n})"
}`, nil)
fmt.Println("JSON compliant:", ok)
ok = td.CmpJSON(t, got, `
{
"fullname": "$^HasPrefix(\"Foo\")",
"age": "$^Between(41, 43)",
"details": "$^SuperMapOf({
\"address\": NotEmpty, // () are optional when no parameters
\"car\": Any(\"Peugeot\", \"Tesla\", \"Jeep\") // any of these
})"
}`, nil)
fmt.Println("JSON multilines strings:", ok)
ok = td.CmpJSON(t, got, `
{
"fullname": "$^HasPrefix(r)",
"age": "$^Between(41, 43)",
"details": "$^SuperMapOf({
r: NotEmpty, // () are optional when no parameters
r: Any(r, r, r) // any of these
})"
}`, nil)
fmt.Println("Raw strings:", ok)
// Output:
// Original: true
// JSON compliant: true
// JSON multilines strings: true
// Raw strings: true
}
func ExampleCmpJSON_file() {
t := &testing.T{}
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
Gender string `json:"gender"`
}{
Fullname: "Bob Foobar",
Age: 42,
Gender: "male",
}
tmpDir, err := os.MkdirTemp("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir) //nolint: errcheck // clean up
filename := tmpDir + "/test.json"
if err = os.WriteFile(filename, []byte(`
{
"fullname": "$name",
"age": "$age",
"gender": "$gender"
}`), 0644); err != nil {
t.Fatal(err)
}
// OK let's test with this file
ok := td.CmpJSON(t, got, filename, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))})
fmt.Println("Full match from file name:", ok)
// When the file is already open
file, err := os.Open(filename)
if err != nil {
t.Fatal(err)
}
ok = td.CmpJSON(t, got, file, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))})
fmt.Println("Full match from io.Reader:", ok)
// Output:
// Full match from file name: true
// Full match from io.Reader: true
}
func ExampleCmpJSONPointer_rfc6901() {
t := &testing.T{}
got := json.RawMessage(`
{
"foo": ["bar", "baz"],
"": 0,
"a/b": 1,
"c%d": 2,
"e^f": 3,
"g|h": 4,
"i\\j": 5,
"k\"l": 6,
" ": 7,
"m~n": 8
}`)
expected := map[string]any{
"foo": []any{"bar", "baz"},
"": 0,
"a/b": 1,
"c%d": 2,
"e^f": 3,
"g|h": 4,
`i\j`: 5,
`k"l`: 6,
" ": 7,
"m~n": 8,
}
ok := td.CmpJSONPointer(t, got, "", expected)
fmt.Println("Empty JSON pointer means all:", ok)
ok = td.CmpJSONPointer(t, got, `/foo`, []any{"bar", "baz"})
fmt.Println("Extract `foo` key:", ok)
ok = td.CmpJSONPointer(t, got, `/foo/0`, "bar")
fmt.Println("First item of `foo` key slice:", ok)
ok = td.CmpJSONPointer(t, got, `/`, 0)
fmt.Println("Empty key:", ok)
ok = td.CmpJSONPointer(t, got, `/a~1b`, 1)
fmt.Println("Slash has to be escaped using `~1`:", ok)
ok = td.CmpJSONPointer(t, got, `/c%d`, 2)
fmt.Println("% in key:", ok)
ok = td.CmpJSONPointer(t, got, `/e^f`, 3)
fmt.Println("^ in key:", ok)
ok = td.CmpJSONPointer(t, got, `/g|h`, 4)
fmt.Println("| in key:", ok)
ok = td.CmpJSONPointer(t, got, `/i\j`, 5)
fmt.Println("Backslash in key:", ok)
ok = td.CmpJSONPointer(t, got, `/k"l`, 6)
fmt.Println("Double-quote in key:", ok)
ok = td.CmpJSONPointer(t, got, `/ `, 7)
fmt.Println("Space key:", ok)
ok = td.CmpJSONPointer(t, got, `/m~0n`, 8)
fmt.Println("Tilde has to be escaped using `~0`:", ok)
// Output:
// Empty JSON pointer means all: true
// Extract `foo` key: true
// First item of `foo` key slice: true
// Empty key: true
// Slash has to be escaped using `~1`: true
// % in key: true
// ^ in key: true
// | in key: true
// Backslash in key: true
// Double-quote in key: true
// Space key: true
// Tilde has to be escaped using `~0`: true
}
func ExampleCmpJSONPointer_struct() {
t := &testing.T{}
// Without json tags, encoding/json uses public fields name
type Item struct {
Name string
Value int64
Next *Item
}
got := Item{
Name: "first",
Value: 1,
Next: &Item{
Name: "second",
Value: 2,
Next: &Item{
Name: "third",
Value: 3,
},
},
}
ok := td.CmpJSONPointer(t, got, "/Next/Next/Name", "third")
fmt.Println("3rd item name is `third`:", ok)
ok = td.CmpJSONPointer(t, got, "/Next/Next/Value", td.Gte(int64(3)))
fmt.Println("3rd item value is greater or equal than 3:", ok)
ok = td.CmpJSONPointer(t, got, "/Next", td.JSONPointer("/Next",
td.JSONPointer("/Value", td.Gte(int64(3)))))
fmt.Println("3rd item value is still greater or equal than 3:", ok)
ok = td.CmpJSONPointer(t, got, "/Next/Next/Next/Name", td.Ignore())
fmt.Println("4th item exists and has a name:", ok)
// Struct comparison work with or without pointer: &Item{…} works too
ok = td.CmpJSONPointer(t, got, "/Next/Next", Item{
Name: "third",
Value: 3,
})
fmt.Println("3rd item full comparison:", ok)
// Output:
// 3rd item name is `third`: true
// 3rd item value is greater or equal than 3: true
// 3rd item value is still greater or equal than 3: true
// 4th item exists and has a name: false
// 3rd item full comparison: true
}
func ExampleCmpJSONPointer_has_hasnt() {
t := &testing.T{}
got := json.RawMessage(`
{
"name": "Bob",
"age": 42,
"children": [
{
"name": "Alice",
"age": 16
},
{
"name": "Britt",
"age": 21,
"children": [
{
"name": "John",
"age": 1
}
]
}
]
}`)
// Has Bob some children?
ok := td.CmpJSONPointer(t, got, "/children", td.Len(td.Gt(0)))
fmt.Println("Bob has at least one child:", ok)
// But checking "children" exists is enough here
ok = td.CmpJSONPointer(t, got, "/children/0/children", td.Ignore())
fmt.Println("Alice has children:", ok)
ok = td.CmpJSONPointer(t, got, "/children/1/children", td.Ignore())
fmt.Println("Britt has children:", ok)
// The reverse can be checked too
ok = td.Cmp(t, got, td.Not(td.JSONPointer("/children/0/children", td.Ignore())))
fmt.Println("Alice hasn't children:", ok)
ok = td.Cmp(t, got, td.Not(td.JSONPointer("/children/1/children", td.Ignore())))
fmt.Println("Britt hasn't children:", ok)
// Output:
// Bob has at least one child: true
// Alice has children: false
// Britt has children: true
// Alice hasn't children: true
// Britt hasn't children: false
}
func ExampleCmpKeys() {
t := &testing.T{}
got := map[string]int{"foo": 1, "bar": 2, "zip": 3}
// Keys tests keys in an ordered manner
ok := td.CmpKeys(t, got, []string{"bar", "foo", "zip"})
fmt.Println("All sorted keys are found:", ok)
// If the expected keys are not ordered, it fails
ok = td.CmpKeys(t, got, []string{"zip", "bar", "foo"})
fmt.Println("All unsorted keys are found:", ok)
// To circumvent that, one can use Bag operator
ok = td.CmpKeys(t, got, td.Bag("zip", "bar", "foo"))
fmt.Println("All unsorted keys are found, with the help of Bag operator:", ok)
// Check that each key is 3 bytes long
ok = td.CmpKeys(t, got, td.ArrayEach(td.Len(3)))
fmt.Println("Each key is 3 bytes long:", ok)
// Output:
// All sorted keys are found: true
// All unsorted keys are found: false
// All unsorted keys are found, with the help of Bag operator: true
// 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{}
gotInt64 := int64(1234)
gotInt32 := int32(1235)
type myInt uint16
gotMyInt := myInt(1236)
expected := td.Between(1230, 1240) // int type here
ok := td.CmpLax(t, gotInt64, expected)
fmt.Println("int64 got between ints [1230 .. 1240]:", ok)
ok = td.CmpLax(t, gotInt32, expected)
fmt.Println("int32 got between ints [1230 .. 1240]:", ok)
ok = td.CmpLax(t, gotMyInt, expected)
fmt.Println("myInt got between ints [1230 .. 1240]:", ok)
// Output:
// int64 got between ints [1230 .. 1240]: true
// int32 got between ints [1230 .. 1240]: true
// myInt got between ints [1230 .. 1240]: true
}
func ExampleCmpLen_slice() {
t := &testing.T{}
got := []int{11, 22, 33}
ok := td.CmpLen(t, got, 3, "checks %v len is 3", got)
fmt.Println(ok)
ok = td.CmpLen(t, got, 0, "checks %v len is 0", got)
fmt.Println(ok)
got = nil
ok = td.CmpLen(t, got, 0, "checks %v len is 0", got)
fmt.Println(ok)
// Output:
// true
// false
// true
}
func ExampleCmpLen_map() {
t := &testing.T{}
got := map[int]bool{11: true, 22: false, 33: false}
ok := td.CmpLen(t, got, 3, "checks %v len is 3", got)
fmt.Println(ok)
ok = td.CmpLen(t, got, 0, "checks %v len is 0", got)
fmt.Println(ok)
got = nil
ok = td.CmpLen(t, got, 0, "checks %v len is 0", got)
fmt.Println(ok)
// Output:
// true
// false
// true
}
func ExampleCmpLen_operatorSlice() {
t := &testing.T{}
got := []int{11, 22, 33}
ok := td.CmpLen(t, got, td.Between(3, 8),
"checks %v len is in [3 .. 8]", got)
fmt.Println(ok)
ok = td.CmpLen(t, got, td.Lt(5), "checks %v len is < 5", got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleCmpLen_operatorMap() {
t := &testing.T{}
got := map[int]bool{11: true, 22: false, 33: false}
ok := td.CmpLen(t, got, td.Between(3, 8),
"checks %v len is in [3 .. 8]", got)
fmt.Println(ok)
ok = td.CmpLen(t, got, td.Gte(3), "checks %v len is ≥ 3", got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleCmpList() {
t := &testing.T{}
got := []int{1, 33, 8, 2}
// Matches as all items are present
ok := td.CmpList(t, got, []any{1, td.Between(32, 34), td.Gt(7), 2})
fmt.Println("checks all items match, in this order:", ok)
// Does not match as got does not use the same order as expected
ok = td.CmpList(t, got, []any{1, td.Gt(7), 2, td.Between(32, 34)})
fmt.Println("checks all items match, in wrong order:", ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using expected... without copying it to a new
// []any slice, then use td.Flatten!
expected := []int{1, 33, 8}
ok = td.CmpList(t, got, []any{td.Flatten(expected), td.Lte(2)})
fmt.Println("checks all expected items are present + last one ≤ 2:", ok)
// Output:
// checks all items match, in this order: true
// checks all items match, in wrong order: false
// checks all expected items are present + last one ≤ 2: true
}
func ExampleCmpLt_int() {
t := &testing.T{}
got := 156
ok := td.CmpLt(t, got, 157, "checks %v is < 157", got)
fmt.Println(ok)
ok = td.CmpLt(t, got, 156, "checks %v is < 156", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleCmpLt_string() {
t := &testing.T{}
got := "abc"
ok := td.CmpLt(t, got, "abd", `checks "%v" is < "abd"`, got)
fmt.Println(ok)
ok = td.CmpLt(t, got, "abc", `checks "%v" is < "abc"`, got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleCmpLte_int() {
t := &testing.T{}
got := 156
ok := td.CmpLte(t, got, 156, "checks %v is ≤ 156", got)
fmt.Println(ok)
ok = td.CmpLte(t, got, 157, "checks %v is ≤ 157", got)
fmt.Println(ok)
ok = td.CmpLte(t, got, 155, "checks %v is ≤ 155", got)
fmt.Println(ok)
// Output:
// true
// true
// false
}
func ExampleCmpLte_string() {
t := &testing.T{}
got := "abc"
ok := td.CmpLte(t, got, "abc", `checks "%v" is ≤ "abc"`, got)
fmt.Println(ok)
ok = td.CmpLte(t, got, "abd", `checks "%v" is ≤ "abd"`, got)
fmt.Println(ok)
ok = td.CmpLte(t, got, "abb", `checks "%v" is ≤ "abb"`, got)
fmt.Println(ok)
// Output:
// true
// true
// false
}
func ExampleCmpMap_map() {
t := &testing.T{}
got := map[string]int{"foo": 12, "bar": 42, "zip": 89}
ok := td.CmpMap(t, got, map[string]int{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": td.Ignore()},
"checks map %v", got)
fmt.Println(ok)
ok = td.CmpMap(t, got, map[string]int{}, td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()},
"checks map %v", got)
fmt.Println(ok)
ok = td.CmpMap(t, got, (map[string]int)(nil), td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()},
"checks map %v", got)
fmt.Println(ok)
// Output:
// true
// true
// true
}
func ExampleCmpMap_typedMap() {
t := &testing.T{}
type MyMap map[string]int
got := MyMap{"foo": 12, "bar": 42, "zip": 89}
ok := td.CmpMap(t, got, MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": td.Ignore()},
"checks typed map %v", got)
fmt.Println(ok)
ok = td.CmpMap(t, &got, &MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": td.Ignore()},
"checks pointer on typed map %v", got)
fmt.Println(ok)
ok = td.CmpMap(t, &got, &MyMap{}, td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()},
"checks pointer on typed map %v", got)
fmt.Println(ok)
ok = td.CmpMap(t, &got, (*MyMap)(nil), td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()},
"checks pointer on typed map %v", got)
fmt.Println(ok)
// Output:
// true
// true
// true
// true
}
func ExampleCmpMapEach_map() {
t := &testing.T{}
got := map[string]int{"foo": 12, "bar": 42, "zip": 89}
ok := td.CmpMapEach(t, got, td.Between(10, 90),
"checks each value of map %v is in [10 .. 90]", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleCmpMapEach_typedMap() {
t := &testing.T{}
type MyMap map[string]int
got := MyMap{"foo": 12, "bar": 42, "zip": 89}
ok := td.CmpMapEach(t, got, td.Between(10, 90),
"checks each value of typed map %v is in [10 .. 90]", got)
fmt.Println(ok)
ok = td.CmpMapEach(t, &got, td.Between(10, 90),
"checks each value of typed map pointer %v is in [10 .. 90]", got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleCmpN() {
t := &testing.T{}
got := 1.12345
ok := td.CmpN(t, got, 1.1234, 0.00006,
"checks %v = 1.1234 ± 0.00006", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleCmpNaN_float32() {
t := &testing.T{}
got := float32(math.NaN())
ok := td.CmpNaN(t, got,
"checks %v is not-a-number", got)
fmt.Println("float32(math.NaN()) is float32 not-a-number:", ok)
got = 12
ok = td.CmpNaN(t, got,
"checks %v is not-a-number", got)
fmt.Println("float32(12) is float32 not-a-number:", ok)
// Output:
// float32(math.NaN()) is float32 not-a-number: true
// float32(12) is float32 not-a-number: false
}
func ExampleCmpNaN_float64() {
t := &testing.T{}
got := math.NaN()
ok := td.CmpNaN(t, got,
"checks %v is not-a-number", got)
fmt.Println("math.NaN() is not-a-number:", ok)
got = 12
ok = td.CmpNaN(t, got,
"checks %v is not-a-number", got)
fmt.Println("float64(12) is not-a-number:", ok)
// Output:
// math.NaN() is not-a-number: true
// float64(12) is not-a-number: false
}
func ExampleCmpNil() {
t := &testing.T{}
var got fmt.Stringer // interface
// nil value can be compared directly with nil, no need of Nil() here
ok := td.Cmp(t, got, nil)
fmt.Println(ok)
// But it works with Nil() anyway
ok = td.CmpNil(t, got)
fmt.Println(ok)
got = (*bytes.Buffer)(nil)
// In the case of an interface containing a nil pointer, comparing
// with nil fails, as the interface is not nil
ok = td.Cmp(t, got, nil)
fmt.Println(ok)
// In this case Nil() succeed
ok = td.CmpNil(t, got)
fmt.Println(ok)
// Output:
// true
// true
// false
// true
}
func ExampleCmpNone() {
t := &testing.T{}
got := 18
ok := td.CmpNone(t, got, []any{0, 10, 20, 30, td.Between(100, 199)},
"checks %v is non-null, and ≠ 10, 20 & 30, and not in [100-199]", got)
fmt.Println(ok)
got = 20
ok = td.CmpNone(t, got, []any{0, 10, 20, 30, td.Between(100, 199)},
"checks %v is non-null, and ≠ 10, 20 & 30, and not in [100-199]", got)
fmt.Println(ok)
got = 142
ok = td.CmpNone(t, got, []any{0, 10, 20, 30, td.Between(100, 199)},
"checks %v is non-null, and ≠ 10, 20 & 30, and not in [100-199]", got)
fmt.Println(ok)
prime := td.Flatten([]int{1, 2, 3, 5, 7, 11, 13})
even := td.Flatten([]int{2, 4, 6, 8, 10, 12, 14})
for _, got := range [...]int{9, 3, 8, 15} {
ok = td.CmpNone(t, got, []any{prime, even, td.Gt(14)},
"checks %v is not prime number, nor an even number and not > 14")
fmt.Printf("%d → %t\n", got, ok)
}
// Output:
// true
// false
// false
// 9 → true
// 3 → false
// 8 → false
// 15 → false
}
func ExampleCmpNot() {
t := &testing.T{}
got := 42
ok := td.CmpNot(t, got, 0, "checks %v is non-null", got)
fmt.Println(ok)
ok = td.CmpNot(t, got, td.Between(10, 30),
"checks %v is not in [10 .. 30]", got)
fmt.Println(ok)
got = 0
ok = td.CmpNot(t, got, 0, "checks %v is non-null", got)
fmt.Println(ok)
// Output:
// true
// true
// false
}
func ExampleCmpNotAny() {
t := &testing.T{}
got := []int{4, 5, 9, 42}
ok := td.CmpNotAny(t, got, []any{3, 6, 8, 41, 43},
"checks %v contains no item listed in NotAny()", got)
fmt.Println(ok)
ok = td.CmpNotAny(t, got, []any{3, 6, 8, 42, 43},
"checks %v contains no item listed in NotAny()", got)
fmt.Println(ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using notExpected... without copying it to a new
// []any slice, then use td.Flatten!
notExpected := []int{3, 6, 8, 41, 43}
ok = td.CmpNotAny(t, got, []any{td.Flatten(notExpected)},
"checks %v contains no item listed in notExpected", got)
fmt.Println(ok)
// Output:
// true
// false
// true
}
func ExampleCmpNotEmpty() {
t := &testing.T{}
ok := td.CmpNotEmpty(t, nil) // fails, as nil is considered empty
fmt.Println(ok)
ok = td.CmpNotEmpty(t, "foobar")
fmt.Println(ok)
// Fails as 0 is a number, so not empty. Use NotZero() instead
ok = td.CmpNotEmpty(t, 0)
fmt.Println(ok)
ok = td.CmpNotEmpty(t, map[string]int{"foobar": 42})
fmt.Println(ok)
ok = td.CmpNotEmpty(t, []int{1})
fmt.Println(ok)
ok = td.CmpNotEmpty(t, [3]int{}) // succeeds, NotEmpty() is not NotZero()!
fmt.Println(ok)
// Output:
// false
// true
// false
// true
// true
// true
}
func ExampleCmpNotEmpty_pointers() {
t := &testing.T{}
type MySlice []int
ok := td.CmpNotEmpty(t, MySlice{12})
fmt.Println(ok)
ok = td.CmpNotEmpty(t, &MySlice{12}) // Ptr() not needed
fmt.Println(ok)
l1 := &MySlice{12}
l2 := &l1
l3 := &l2
ok = td.CmpNotEmpty(t, &l3)
fmt.Println(ok)
// Works the same for array, map, channel and string
// But not for others types as:
type MyStruct struct {
Value int
}
ok = td.CmpNotEmpty(t, &MyStruct{}) // fails, use NotZero() instead
fmt.Println(ok)
// Output:
// true
// true
// true
// false
}
func ExampleCmpNotNaN_float32() {
t := &testing.T{}
got := float32(math.NaN())
ok := td.CmpNotNaN(t, got,
"checks %v is not-a-number", got)
fmt.Println("float32(math.NaN()) is NOT float32 not-a-number:", ok)
got = 12
ok = td.CmpNotNaN(t, got,
"checks %v is not-a-number", got)
fmt.Println("float32(12) is NOT float32 not-a-number:", ok)
// Output:
// float32(math.NaN()) is NOT float32 not-a-number: false
// float32(12) is NOT float32 not-a-number: true
}
func ExampleCmpNotNaN_float64() {
t := &testing.T{}
got := math.NaN()
ok := td.CmpNotNaN(t, got,
"checks %v is NOT not-a-number", got)
fmt.Println("math.NaN() is NOT not-a-number:", ok)
got = 12
ok = td.CmpNotNaN(t, got,
"checks %v is NOT not-a-number", got)
fmt.Println("float64(12) is NOT not-a-number:", ok)
// Output:
// math.NaN() is NOT not-a-number: false
// float64(12) is NOT not-a-number: true
}
func ExampleCmpNotNil() {
t := &testing.T{}
var got fmt.Stringer = &bytes.Buffer{}
// nil value can be compared directly with Not(nil), no need of NotNil() here
ok := td.Cmp(t, got, td.Not(nil))
fmt.Println(ok)
// But it works with NotNil() anyway
ok = td.CmpNotNil(t, got)
fmt.Println(ok)
got = (*bytes.Buffer)(nil)
// In the case of an interface containing a nil pointer, comparing
// with Not(nil) succeeds, as the interface is not nil
ok = td.Cmp(t, got, td.Not(nil))
fmt.Println(ok)
// In this case NotNil() fails
ok = td.CmpNotNil(t, got)
fmt.Println(ok)
// Output:
// true
// true
// true
// false
}
func ExampleCmpNotZero() {
t := &testing.T{}
ok := td.CmpNotZero(t, 0) // fails
fmt.Println(ok)
ok = td.CmpNotZero(t, float64(0)) // fails
fmt.Println(ok)
ok = td.CmpNotZero(t, 12)
fmt.Println(ok)
ok = td.CmpNotZero(t, (map[string]int)(nil)) // fails, as nil
fmt.Println(ok)
ok = td.CmpNotZero(t, map[string]int{}) // succeeds, as not nil
fmt.Println(ok)
ok = td.CmpNotZero(t, ([]int)(nil)) // fails, as nil
fmt.Println(ok)
ok = td.CmpNotZero(t, []int{}) // succeeds, as not nil
fmt.Println(ok)
ok = td.CmpNotZero(t, [3]int{}) // fails
fmt.Println(ok)
ok = td.CmpNotZero(t, [3]int{0, 1}) // succeeds, DATA[1] is not 0
fmt.Println(ok)
ok = td.CmpNotZero(t, bytes.Buffer{}) // fails
fmt.Println(ok)
ok = td.CmpNotZero(t, &bytes.Buffer{}) // succeeds, as pointer not nil
fmt.Println(ok)
ok = td.Cmp(t, &bytes.Buffer{}, td.Ptr(td.NotZero())) // fails as deref by Ptr()
fmt.Println(ok)
// Output:
// false
// false
// true
// false
// true
// false
// true
// false
// true
// false
// true
// false
}
func ExampleCmpPPtr() {
t := &testing.T{}
num := 12
got := &num
ok := td.CmpPPtr(t, &got, 12)
fmt.Println(ok)
ok = td.CmpPPtr(t, &got, td.Between(4, 15))
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleCmpPtr() {
t := &testing.T{}
got := 12
ok := td.CmpPtr(t, &got, 12)
fmt.Println(ok)
ok = td.CmpPtr(t, &got, td.Between(4, 15))
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleCmpRe() {
t := &testing.T{}
got := "foo bar"
ok := td.CmpRe(t, got, "(zip|bar)$", nil, "checks value %s", got)
fmt.Println(ok)
got = "bar foo"
ok = td.CmpRe(t, got, "(zip|bar)$", nil, "checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleCmpRe_stringer() {
t := &testing.T{}
// bytes.Buffer implements fmt.Stringer
got := bytes.NewBufferString("foo bar")
ok := td.CmpRe(t, got, "(zip|bar)$", nil, "checks value %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleCmpRe_error() {
t := &testing.T{}
got := errors.New("foo bar")
ok := td.CmpRe(t, got, "(zip|bar)$", nil, "checks value %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleCmpRe_capture() {
t := &testing.T{}
got := "foo bar biz"
ok := td.CmpRe(t, got, `^(\w+) (\w+) (\w+)$`, td.Set("biz", "foo", "bar"),
"checks value %s", got)
fmt.Println(ok)
got = "foo bar! biz"
ok = td.CmpRe(t, got, `^(\w+) (\w+) (\w+)$`, td.Set("biz", "foo", "bar"),
"checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleCmpRe_multilines() {
t := &testing.T{}
got := `multi
lines
probably
more
than 4
`
expectedRe := `^multi
lines?
(probably|possibly)
more
than \d+
\z`
ok := td.CmpRe(t, got, expectedRe, nil)
fmt.Println("Raw multi-lines string matches:", ok)
// But for strings with many, many, many lines, when the regexp
// doesn't match, it is sometimes difficult to see where the regexp
// failed in the string. Here td.List & td.Flatten can help to apply
// regexp line per line (note expectedRe is not anchored anymore):
expectedRe = `multi
lines?
(probably|possibly)
more
than \d+
`
ok = td.Cmp(t,
strings.Split(got, "\n"),
td.List(td.Flatten(strings.Split(expectedRe, "\n"),
func(line string) any {
return td.Re(`^` + line + `\z`)
})))
fmt.Println("All string lines match:", ok)
// Output:
// Raw multi-lines string matches: true
// All string lines match: true
}
func ExampleCmpRe_compiled() {
t := &testing.T{}
expected := regexp.MustCompile("(zip|bar)$")
got := "foo bar"
ok := td.CmpRe(t, got, expected, nil, "checks value %s", got)
fmt.Println(ok)
got = "bar foo"
ok = td.CmpRe(t, got, expected, nil, "checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleCmpRe_compiledStringer() {
t := &testing.T{}
expected := regexp.MustCompile("(zip|bar)$")
// bytes.Buffer implements fmt.Stringer
got := bytes.NewBufferString("foo bar")
ok := td.CmpRe(t, got, expected, nil, "checks value %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleCmpRe_compiledError() {
t := &testing.T{}
expected := regexp.MustCompile("(zip|bar)$")
got := errors.New("foo bar")
ok := td.CmpRe(t, got, expected, nil, "checks value %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleCmpRe_compiledCapture() {
t := &testing.T{}
expected := regexp.MustCompile(`^(\w+) (\w+) (\w+)$`)
got := "foo bar biz"
ok := td.CmpRe(t, got, expected, td.Set("biz", "foo", "bar"),
"checks value %s", got)
fmt.Println(ok)
got = "foo bar! biz"
ok = td.CmpRe(t, got, expected, td.Set("biz", "foo", "bar"),
"checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleCmpReAll_capture() {
t := &testing.T{}
got := "foo bar biz"
ok := td.CmpReAll(t, got, `(\w+)`, td.Set("biz", "foo", "bar"),
"checks value %s", got)
fmt.Println(ok)
// Matches, but all catured groups do not match Set
got = "foo BAR biz"
ok = td.CmpReAll(t, got, `(\w+)`, td.Set("biz", "foo", "bar"),
"checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleCmpReAll_captureComplex() {
t := &testing.T{}
got := "11 45 23 56 85 96"
ok := td.CmpReAll(t, got, `(\d+)`, td.ArrayEach(td.Code(func(num string) bool {
n, err := strconv.Atoi(num)
return err == nil && n > 10 && n < 100
})),
"checks value %s", got)
fmt.Println(ok)
// Matches, but 11 is not greater than 20
ok = td.CmpReAll(t, got, `(\d+)`, td.ArrayEach(td.Code(func(num string) bool {
n, err := strconv.Atoi(num)
return err == nil && n > 20 && n < 100
})),
"checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleCmpReAll_compiledCapture() {
t := &testing.T{}
expected := regexp.MustCompile(`(\w+)`)
got := "foo bar biz"
ok := td.CmpReAll(t, got, expected, td.Set("biz", "foo", "bar"),
"checks value %s", got)
fmt.Println(ok)
// Matches, but all catured groups do not match Set
got = "foo BAR biz"
ok = td.CmpReAll(t, got, expected, td.Set("biz", "foo", "bar"),
"checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleCmpReAll_compiledCaptureComplex() {
t := &testing.T{}
expected := regexp.MustCompile(`(\d+)`)
got := "11 45 23 56 85 96"
ok := td.CmpReAll(t, got, expected, td.ArrayEach(td.Code(func(num string) bool {
n, err := strconv.Atoi(num)
return err == nil && n > 10 && n < 100
})),
"checks value %s", got)
fmt.Println(ok)
// Matches, but 11 is not greater than 20
ok = td.CmpReAll(t, got, expected, td.ArrayEach(td.Code(func(num string) bool {
n, err := strconv.Atoi(num)
return err == nil && n > 20 && n < 100
})),
"checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleCmpRecv_basic() {
t := &testing.T{}
got := make(chan int, 3)
ok := td.CmpRecv(t, got, td.RecvNothing, 0)
fmt.Println("nothing to receive:", ok)
got <- 1
got <- 2
got <- 3
close(got)
ok = td.CmpRecv(t, got, 1, 0)
fmt.Println("1st receive is 1:", ok)
ok = td.Cmp(t, got, td.All(
td.Recv(2),
td.Recv(td.Between(3, 4)),
td.Recv(td.RecvClosed),
))
fmt.Println("next receives are 2, 3 then closed:", ok)
ok = td.CmpRecv(t, got, td.RecvNothing, 0)
fmt.Println("nothing to receive:", ok)
// Output:
// nothing to receive: true
// 1st receive is 1: true
// next receives are 2, 3 then closed: true
// nothing to receive: false
}
func ExampleCmpRecv_channelPointer() {
t := &testing.T{}
got := make(chan int, 3)
ok := td.CmpRecv(t, got, td.RecvNothing, 0)
fmt.Println("nothing to receive:", ok)
got <- 1
got <- 2
got <- 3
close(got)
ok = td.CmpRecv(t, &got, 1, 0)
fmt.Println("1st receive is 1:", ok)
ok = td.Cmp(t, &got, td.All(
td.Recv(2),
td.Recv(td.Between(3, 4)),
td.Recv(td.RecvClosed),
))
fmt.Println("next receives are 2, 3 then closed:", ok)
ok = td.CmpRecv(t, got, td.RecvNothing, 0)
fmt.Println("nothing to receive:", ok)
// Output:
// nothing to receive: true
// 1st receive is 1: true
// next receives are 2, 3 then closed: true
// nothing to receive: false
}
func ExampleCmpRecv_withTimeout() {
t := &testing.T{}
got := make(chan int, 1)
tick := make(chan struct{})
go func() {
// ①
<-tick
time.Sleep(100 * time.Millisecond)
got <- 0
// ②
<-tick
time.Sleep(100 * time.Millisecond)
got <- 1
// ③
<-tick
time.Sleep(100 * time.Millisecond)
close(got)
}()
td.CmpRecv(t, got, td.RecvNothing, 0)
// ①
tick <- struct{}{}
ok := td.CmpRecv(t, got, td.RecvNothing, 0)
fmt.Println("① RecvNothing:", ok)
ok = td.CmpRecv(t, got, 0, 150*time.Millisecond)
fmt.Println("① receive 0 w/150ms timeout:", ok)
ok = td.CmpRecv(t, got, td.RecvNothing, 0)
fmt.Println("① RecvNothing:", ok)
// ②
tick <- struct{}{}
ok = td.CmpRecv(t, got, td.RecvNothing, 0)
fmt.Println("② RecvNothing:", ok)
ok = td.CmpRecv(t, got, 1, 150*time.Millisecond)
fmt.Println("② receive 1 w/150ms timeout:", ok)
ok = td.CmpRecv(t, got, td.RecvNothing, 0)
fmt.Println("② RecvNothing:", ok)
// ③
tick <- struct{}{}
ok = td.CmpRecv(t, got, td.RecvNothing, 0)
fmt.Println("③ RecvNothing:", ok)
ok = td.CmpRecv(t, got, td.RecvClosed, 150*time.Millisecond)
fmt.Println("③ check closed w/150ms timeout:", ok)
// Output:
// ① RecvNothing: true
// ① receive 0 w/150ms timeout: true
// ① RecvNothing: true
// ② RecvNothing: true
// ② receive 1 w/150ms timeout: true
// ② RecvNothing: true
// ③ RecvNothing: true
// ③ check closed w/150ms timeout: true
}
func ExampleCmpRecv_nilChannel() {
t := &testing.T{}
var ch chan int
ok := td.CmpRecv(t, ch, td.RecvNothing, 0)
fmt.Println("nothing to receive from nil channel:", ok)
ok = td.CmpRecv(t, ch, 42, 0)
fmt.Println("something to receive from nil channel:", ok)
ok = td.CmpRecv(t, ch, td.RecvClosed, 0)
fmt.Println("is a nil channel closed:", ok)
// Output:
// nothing to receive from nil channel: true
// something to receive from nil channel: false
// is a nil channel closed: false
}
func ExampleCmpSet() {
t := &testing.T{}
got := []int{1, 3, 5, 8, 8, 1, 2}
// Matches as all items are present, ignoring duplicates
ok := td.CmpSet(t, got, []any{1, 2, 3, 5, 8},
"checks all items are present, in any order")
fmt.Println(ok)
// Duplicates are ignored in a Set
ok = td.CmpSet(t, got, []any{1, 2, 2, 2, 2, 2, 3, 5, 8},
"checks all items are present, in any order")
fmt.Println(ok)
// Tries its best not to raise an error when a value can be matched
// by several Set entries
ok = td.CmpSet(t, got, []any{td.Between(1, 4), 3, td.Between(2, 10)},
"checks all items are present, in any order")
fmt.Println(ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using expected... without copying it to a new
// []any slice, then use td.Flatten!
expected := []int{1, 2, 3, 5, 8}
ok = td.CmpSet(t, got, []any{td.Flatten(expected)},
"checks all expected items are present, in any order")
fmt.Println(ok)
// Output:
// true
// true
// true
// true
}
func ExampleCmpShallow() {
t := &testing.T{}
type MyStruct struct {
Value int
}
data := MyStruct{Value: 12}
got := &data
ok := td.CmpShallow(t, got, &data,
"checks pointers only, not contents")
fmt.Println(ok)
// Same contents, but not same pointer
ok = td.CmpShallow(t, got, &MyStruct{Value: 12},
"checks pointers only, not contents")
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleCmpShallow_slice() {
t := &testing.T{}
back := []int{1, 2, 3, 1, 2, 3}
a := back[:3]
b := back[3:]
ok := td.CmpShallow(t, a, back)
fmt.Println("are ≠ but share the same area:", ok)
ok = td.CmpShallow(t, b, back)
fmt.Println("are = but do not point to same area:", ok)
// Output:
// are ≠ but share the same area: true
// are = but do not point to same area: false
}
func ExampleCmpShallow_string() {
t := &testing.T{}
back := "foobarfoobar"
a := back[:6]
b := back[6:]
ok := td.CmpShallow(t, a, back)
fmt.Println("are ≠ but share the same area:", ok)
ok = td.CmpShallow(t, b, a)
fmt.Println("are = but do not point to same area:", ok)
// Output:
// are ≠ but share the same area: true
// are = but do not point to same area: false
}
func ExampleCmpSlice_slice() {
t := &testing.T{}
got := []int{42, 58, 26}
ok := td.CmpSlice(t, got, []int{42}, td.ArrayEntries{1: 58, 2: td.Ignore()},
"checks slice %v", got)
fmt.Println(ok)
ok = td.CmpSlice(t, got, []int{}, td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()},
"checks slice %v", got)
fmt.Println(ok)
ok = td.CmpSlice(t, got, ([]int)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()},
"checks slice %v", got)
fmt.Println(ok)
// Output:
// true
// true
// true
}
func ExampleCmpSlice_typedSlice() {
t := &testing.T{}
type MySlice []int
got := MySlice{42, 58, 26}
ok := td.CmpSlice(t, got, MySlice{42}, td.ArrayEntries{1: 58, 2: td.Ignore()},
"checks typed slice %v", got)
fmt.Println(ok)
ok = td.CmpSlice(t, &got, &MySlice{42}, td.ArrayEntries{1: 58, 2: td.Ignore()},
"checks pointer on typed slice %v", got)
fmt.Println(ok)
ok = td.CmpSlice(t, &got, &MySlice{}, td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()},
"checks pointer on typed slice %v", got)
fmt.Println(ok)
ok = td.CmpSlice(t, &got, (*MySlice)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()},
"checks pointer on typed slice %v", got)
fmt.Println(ok)
// Output:
// true
// true
// true
// true
}
func ExampleCmpSmuggle_convert() {
t := &testing.T{}
got := int64(123)
ok := td.CmpSmuggle(t, got, func(n int64) int { return int(n) }, 123,
"checks int64 got against an int value")
fmt.Println(ok)
ok = td.CmpSmuggle(t, "123", func(numStr string) (int, bool) {
n, err := strconv.Atoi(numStr)
return n, err == nil
}, td.Between(120, 130),
"checks that number in %#v is in [120 .. 130]")
fmt.Println(ok)
ok = td.CmpSmuggle(t, "123", func(numStr string) (int, bool, string) {
n, err := strconv.Atoi(numStr)
if err != nil {
return 0, false, "string must contain a number"
}
return n, true, ""
}, td.Between(120, 130),
"checks that number in %#v is in [120 .. 130]")
fmt.Println(ok)
ok = td.CmpSmuggle(t, "123", func(numStr string) (int, error) { //nolint: gocritic
return strconv.Atoi(numStr)
}, td.Between(120, 130),
"checks that number in %#v is in [120 .. 130]")
fmt.Println(ok)
// Short version :)
ok = td.CmpSmuggle(t, "123", strconv.Atoi, td.Between(120, 130),
"checks that number in %#v is in [120 .. 130]")
fmt.Println(ok)
// Output:
// true
// true
// true
// true
// true
}
func ExampleCmpSmuggle_lax() {
t := &testing.T{}
// got is an int16 and Smuggle func input is an int64: it is OK
got := int(123)
ok := td.CmpSmuggle(t, got, func(n int64) uint32 { return uint32(n) }, uint32(123))
fmt.Println("got int16(123) → smuggle via int64 → uint32(123):", ok)
// Output:
// got int16(123) → smuggle via int64 → uint32(123): true
}
func ExampleCmpSmuggle_auto_unmarshal() {
t := &testing.T{}
// Automatically json.Unmarshal to compare
got := []byte(`{"a":1,"b":2}`)
ok := td.CmpSmuggle(t, got, func(b json.RawMessage) (r map[string]int, err error) {
err = json.Unmarshal(b, &r)
return
}, map[string]int{
"a": 1,
"b": 2,
})
fmt.Println("JSON contents is OK:", ok)
// Output:
// JSON contents is OK: true
}
func ExampleCmpSmuggle_cast() {
t := &testing.T{}
// A string containing JSON
got := `{ "foo": 123 }`
// Automatically cast a string to a json.RawMessage so td.JSON can operate
ok := td.CmpSmuggle(t, got, json.RawMessage{}, td.JSON(`{"foo":123}`))
fmt.Println("JSON contents in string is OK:", ok)
// Automatically read from io.Reader to a json.RawMessage
ok = td.CmpSmuggle(t, bytes.NewReader([]byte(got)), json.RawMessage{}, td.JSON(`{"foo":123}`))
fmt.Println("JSON contents just read is OK:", ok)
// Output:
// JSON contents in string is OK: true
// JSON contents just read is OK: true
}
func ExampleCmpSmuggle_complex() {
t := &testing.T{}
// No end date but a start date and a duration
type StartDuration struct {
StartDate time.Time
Duration time.Duration
}
// Checks that end date is between 17th and 19th February both at 0h
// for each of these durations in hours
for _, duration := range []time.Duration{48 * time.Hour, 72 * time.Hour, 96 * time.Hour} {
got := StartDuration{
StartDate: time.Date(2018, time.February, 14, 12, 13, 14, 0, time.UTC),
Duration: duration,
}
// Simplest way, but in case of Between() failure, error will be bound
// to DATA, not very clear...
ok := td.CmpSmuggle(t, got, func(sd StartDuration) time.Time {
return sd.StartDate.Add(sd.Duration)
}, td.Between(
time.Date(2018, time.February, 17, 0, 0, 0, 0, time.UTC),
time.Date(2018, time.February, 19, 0, 0, 0, 0, time.UTC)))
fmt.Println(ok)
// Name the computed value "ComputedEndDate" to render a Between() failure
// more understandable, so error will be bound to DATA.ComputedEndDate
ok = td.CmpSmuggle(t, got, func(sd StartDuration) td.SmuggledGot {
return td.SmuggledGot{
Name: "ComputedEndDate",
Got: sd.StartDate.Add(sd.Duration),
}
}, td.Between(
time.Date(2018, time.February, 17, 0, 0, 0, 0, time.UTC),
time.Date(2018, time.February, 19, 0, 0, 0, 0, time.UTC)))
fmt.Println(ok)
}
// Output:
// false
// false
// true
// true
// true
// true
}
func ExampleCmpSmuggle_interface() {
t := &testing.T{}
gotTime, err := time.Parse(time.RFC3339, "2018-05-23T12:13:14Z")
if err != nil {
t.Fatal(err)
}
// Do not check the struct itself, but its stringified form
ok := td.CmpSmuggle(t, gotTime, func(s fmt.Stringer) string {
return s.String()
}, "2018-05-23 12:13:14 +0000 UTC")
fmt.Println("stringified time.Time OK:", ok)
// If got does not implement the fmt.Stringer interface, it fails
// without calling the Smuggle func
type MyTime time.Time
ok = td.CmpSmuggle(t, MyTime(gotTime), func(s fmt.Stringer) string {
fmt.Println("Smuggle func called!")
return s.String()
}, "2018-05-23 12:13:14 +0000 UTC")
fmt.Println("stringified MyTime OK:", ok)
// Output:
// stringified time.Time OK: true
// stringified MyTime OK: false
}
func ExampleCmpSmuggle_field_path() {
t := &testing.T{}
type Body struct {
Name string
Value any
}
type Request struct {
Body *Body
}
type Transaction struct {
Request
}
type ValueNum struct {
Num int
}
got := &Transaction{
Request: Request{
Body: &Body{
Name: "test",
Value: &ValueNum{Num: 123},
},
},
}
// Want to check whether Num is between 100 and 200?
ok := td.CmpSmuggle(t, got, func(tr *Transaction) (int, error) {
if tr.Body == nil ||
tr.Body.Value == nil {
return 0, errors.New("Request.Body or Request.Body.Value is nil")
}
if v, ok := tr.Body.Value.(*ValueNum); ok && v != nil {
return v.Num, nil
}
return 0, errors.New("Request.Body.Value isn't *ValueNum or nil")
}, td.Between(100, 200))
fmt.Println("check Num by hand:", ok)
// Same, but automagically generated...
ok = td.CmpSmuggle(t, got, "Request.Body.Value.Num", td.Between(100, 200))
fmt.Println("check Num using a fields-path:", ok)
// And as Request is an anonymous field, can be simplified further
// as it can be omitted
ok = td.CmpSmuggle(t, got, "Body.Value.Num", td.Between(100, 200))
fmt.Println("check Num using an other fields-path:", ok)
// Note that maps and array/slices are supported
got.Body.Value = map[string]any{
"foo": []any{
3: map[int]string{666: "bar"},
},
}
ok = td.CmpSmuggle(t, got, "Body.Value[foo][3][666]", "bar")
fmt.Println("check fields-path including maps/slices:", ok)
// Output:
// check Num by hand: true
// check Num using a fields-path: true
// check Num using an other fields-path: true
// check fields-path including maps/slices: true
}
func ExampleCmpSort_basic() {
t := &testing.T{}
got := []int{-1, 1, 2, -3, 3, -2, 0}
// Generic ascending order (≥0 or nil)
ok := td.CmpSort(t, got, 1, []int{-3, -2, -1, 0, 1, 2, 3})
fmt.Println("asc order:", ok)
ok = td.CmpSort(t, got, 0, []int{-3, -2, -1, 0, 1, 2, 3})
fmt.Println("asc order:", ok)
ok = td.CmpSort(t, got, nil, []int{-3, -2, -1, 0, 1, 2, 3})
fmt.Println("asc order:", ok)
// Generic descending order (< 0)
ok = td.CmpSort(t, got, -1, []int{3, 2, 1, 0, -1, -2, -3})
fmt.Println("desc order:", ok)
evenHigher := func(a, b int) bool {
if (a%2 == 0) != (b%2 == 0) {
return a%2 != 0
}
return a < b
}
ok = td.CmpSort(t, got, evenHigher, []int{-3, -1, 1, 3, -2, 0, 2})
fmt.Println("even higher order:", ok)
// Output:
// asc order: true
// asc order: true
// asc order: true
// desc order: true
// even higher order: true
}
func ExampleCmpSort_fields_path() {
t := &testing.T{}
type Person struct {
Name string
Age int
}
brian := Person{Name: "Brian", Age: 22}
bob := Person{Name: "Bob", Age: 19}
stephen := Person{Name: "Stephen", Age: 19}
alice := Person{Name: "Alice", Age: 20}
marcel := Person{Name: "Marcel", Age: 25}
got := []Person{brian, bob, stephen, alice, marcel}
ok := td.CmpSort(t, got, "Name", []Person{alice, bob, brian, marcel, stephen})
fmt.Println("by name asc:", ok)
ok = td.CmpSort(t, got, "-Name", []Person{stephen, marcel, brian, bob, alice})
fmt.Println("by name desc:", ok)
ok = td.CmpSort(t, got, []string{"-Age", "Name"}, []Person{marcel, brian, alice, bob, stephen})
fmt.Println("by age desc, then by name asc:", ok)
type A struct{ props map[string]int }
p12 := A{props: map[string]int{"priority": 12}}
p23 := A{props: map[string]int{"priority": 23}}
p34 := A{props: map[string]int{"priority": 34}}
got2 := []A{p23, p12, p34}
ok = td.CmpSort(t, got2, `-props[priority]`, []A{p34, p23, p12})
fmt.Println("by priority desc:", ok)
ok = td.CmpSort(t, got2, `props.priority`, []A{p12, p23, p34})
fmt.Println("by priority asc:", ok)
// Output:
// by name asc: true
// by name desc: true
// by age desc, then by name asc: true
// by priority desc: true
// by priority asc: 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 (1):", ok)
ok = td.CmpSorted(t, got, 0)
fmt.Println("is asc order (0):", ok)
ok = td.CmpSorted(t, got, nil)
fmt.Println("is asc order (nil):", ok)
ok = td.CmpSorted(t, got, -1)
fmt.Println("is desc order:", ok)
got = []int{-3, -1, 1, 3, -2, 0, 2}
evenHigher := func(a, b int) bool {
if (a%2 == 0) != (b%2 == 0) {
return a%2 != 0
}
return a < b
}
ok = td.CmpSorted(t, got, evenHigher)
fmt.Println("is even higher order:", ok)
// Output:
// is asc order (default): true
// is asc order (1): true
// is asc order (0): true
// is asc order (nil): true
// is desc order: false
// is even higher order: true
}
func ExampleCmpSorted_fields_path() {
t := &testing.T{}
type Person struct {
Name string
Age int
}
alice := Person{Name: "Alice", Age: 20}
bob := Person{Name: "Bob", Age: 19}
brian := Person{Name: "Brian", Age: 22}
marcel := Person{Name: "Marcel", Age: 25}
stephen := Person{Name: "Stephen", Age: 19}
got := []Person{alice, bob, brian, marcel, stephen}
ok := td.CmpSorted(t, got, "Name")
fmt.Println("is sorted by name asc:", ok)
got = []Person{stephen, marcel, brian, bob, alice}
ok = td.CmpSorted(t, got, "-Name")
fmt.Println("is sorted by name desc:", ok)
got = []Person{marcel, brian, alice, bob, stephen}
ok = td.CmpSorted(t, got, []string{"-Age", "Name"})
fmt.Println("is sorted by age desc, then by name asc 1:", ok)
got = []Person{marcel, brian, alice, stephen, bob}
ok = td.CmpSorted(t, got, []string{"-Age", "Name"})
fmt.Println("is sorted by age desc, then by name asc 2:", ok)
type A struct{ props map[string]int }
p12 := A{props: map[string]int{"priority": 12}}
p23 := A{props: map[string]int{"priority": 23}}
p34 := A{props: map[string]int{"priority": 34}}
got2 := []A{p34, p23, p12}
ok = td.CmpSorted(t, got2, `-props[priority]`)
fmt.Println("is sorted by priority desc:", ok)
ok = td.CmpSorted(t, got2, `props.priority`)
fmt.Println("is sorted by priority asc:", ok)
// Output:
// is sorted by name asc: true
// is sorted by name desc: true
// is sorted by age desc, then by name asc 1: true
// is sorted by age desc, then by name asc 2: false
// is sorted by priority desc: true
// is sorted by priority asc: false
}
func ExampleCmpSStruct() {
t := &testing.T{}
type Person struct {
Name string
Age int
NumChildren int
}
got := Person{
Name: "Foobar",
Age: 42,
NumChildren: 0,
}
// NumChildren is not listed in expected fields so it must be zero
ok := td.CmpSStruct(t, got, Person{Name: "Foobar"}, td.StructFields{
"Age": td.Between(40, 50),
},
"checks %v is the right Person")
fmt.Println("Foobar is between 40 & 50:", ok)
// Model can be empty
got.NumChildren = 3
ok = td.CmpSStruct(t, got, Person{}, td.StructFields{
"Name": "Foobar",
"Age": td.Between(40, 50),
"NumChildren": td.Not(0),
},
"checks %v is the right Person")
fmt.Println("Foobar has some children:", ok)
// Works with pointers too
ok = td.CmpSStruct(t, &got, &Person{}, td.StructFields{
"Name": "Foobar",
"Age": td.Between(40, 50),
"NumChildren": td.Not(0),
},
"checks %v is the right Person")
fmt.Println("Foobar has some children (using pointer):", ok)
// Model does not need to be instanciated
ok = td.CmpSStruct(t, &got, (*Person)(nil), td.StructFields{
"Name": "Foobar",
"Age": td.Between(40, 50),
"NumChildren": td.Not(0),
},
"checks %v is the right Person")
fmt.Println("Foobar has some children (using nil model):", ok)
// Output:
// Foobar is between 40 & 50: true
// Foobar has some children: true
// Foobar has some children (using pointer): true
// Foobar has some children (using nil model): true
}
func ExampleCmpSStruct_overwrite_model() {
t := &testing.T{}
type Person struct {
Name string
Age int
NumChildren int
}
got := Person{
Name: "Foobar",
Age: 42,
NumChildren: 3,
}
ok := td.CmpSStruct(t, got, Person{
Name: "Foobar",
Age: 53,
}, td.StructFields{
">Age": td.Between(40, 50), // ">" to overwrite Age:53 in model
"NumChildren": td.Gt(2),
},
"checks %v is the right Person")
fmt.Println("Foobar is between 40 & 50:", ok)
ok = td.CmpSStruct(t, got, Person{
Name: "Foobar",
Age: 53,
}, td.StructFields{
"> Age": td.Between(40, 50), // same, ">" can be followed by spaces
"NumChildren": td.Gt(2),
},
"checks %v is the right Person")
fmt.Println("Foobar is between 40 & 50:", ok)
// Output:
// Foobar is between 40 & 50: true
// Foobar is between 40 & 50: true
}
func ExampleCmpSStruct_patterns() {
t := &testing.T{}
type Person struct {
Firstname string
Lastname string
Surname string
Nickname string
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
id int64
secret string
}
now := time.Now()
got := Person{
Firstname: "Maxime",
Lastname: "Foo",
Surname: "Max",
Nickname: "max",
CreatedAt: now,
UpdatedAt: now,
DeletedAt: nil, // not deleted yet
id: 2345,
secret: "5ecr3T",
}
ok := td.CmpSStruct(t, got, Person{Lastname: "Foo"}, td.StructFields{
`DeletedAt`: nil,
`= *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model
`=~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt
`! [A-Z]*`: td.Ignore(), // private fields
},
"mix shell & regexp patterns")
fmt.Println("Patterns match only remaining fields:", ok)
ok = td.CmpSStruct(t, got, Person{Lastname: "Foo"}, td.StructFields{
`DeletedAt`: nil,
`1 = *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model
`2 =~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt
`3 !~ ^[A-Z]`: td.Ignore(), // private fields
},
"ordered patterns")
fmt.Println("Ordered patterns match only remaining fields:", ok)
// Output:
// Patterns match only remaining fields: true
// Ordered patterns match only remaining fields: true
}
func ExampleCmpSStruct_lazy_model() {
t := &testing.T{}
got := struct {
name string
age int
}{
name: "Foobar",
age: 42,
}
ok := td.CmpSStruct(t, got, nil, td.StructFields{
"name": "Foobar",
"age": td.Between(40, 45),
})
fmt.Println("Lazy model:", ok)
ok = td.CmpSStruct(t, got, nil, td.StructFields{
"name": "Foobar",
"zip": 666,
})
fmt.Println("Lazy model with unknown field:", ok)
// Output:
// Lazy model: true
// Lazy model with unknown field: false
}
func ExampleCmpString() {
t := &testing.T{}
got := "foobar"
ok := td.CmpString(t, got, "foobar", "checks %s", got)
fmt.Println("using string:", ok)
ok = td.Cmp(t, []byte(got), td.String("foobar"), "checks %s", got)
fmt.Println("using []byte:", ok)
// Output:
// using string: true
// using []byte: true
}
func ExampleCmpString_stringer() {
t := &testing.T{}
// bytes.Buffer implements fmt.Stringer
got := bytes.NewBufferString("foobar")
ok := td.CmpString(t, got, "foobar", "checks %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleCmpString_error() {
t := &testing.T{}
got := errors.New("foobar")
ok := td.CmpString(t, got, "foobar", "checks %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleCmpStruct() {
t := &testing.T{}
type Person struct {
Name string
Age int
NumChildren int
}
got := Person{
Name: "Foobar",
Age: 42,
NumChildren: 3,
}
// As NumChildren is zero in Struct() call, it is not checked
ok := td.CmpStruct(t, got, Person{Name: "Foobar"}, td.StructFields{
"Age": td.Between(40, 50),
},
"checks %v is the right Person")
fmt.Println("Foobar is between 40 & 50:", ok)
// Model can be empty
ok = td.CmpStruct(t, got, Person{}, td.StructFields{
"Name": "Foobar",
"Age": td.Between(40, 50),
"NumChildren": td.Not(0),
},
"checks %v is the right Person")
fmt.Println("Foobar has some children:", ok)
// Works with pointers too
ok = td.CmpStruct(t, &got, &Person{}, td.StructFields{
"Name": "Foobar",
"Age": td.Between(40, 50),
"NumChildren": td.Not(0),
},
"checks %v is the right Person")
fmt.Println("Foobar has some children (using pointer):", ok)
// Model does not need to be instanciated
ok = td.CmpStruct(t, &got, (*Person)(nil), td.StructFields{
"Name": "Foobar",
"Age": td.Between(40, 50),
"NumChildren": td.Not(0),
},
"checks %v is the right Person")
fmt.Println("Foobar has some children (using nil model):", ok)
// Output:
// Foobar is between 40 & 50: true
// Foobar has some children: true
// Foobar has some children (using pointer): true
// Foobar has some children (using nil model): true
}
func ExampleCmpStruct_overwrite_model() {
t := &testing.T{}
type Person struct {
Name string
Age int
NumChildren int
}
got := Person{
Name: "Foobar",
Age: 42,
NumChildren: 3,
}
ok := td.CmpStruct(t, got, Person{
Name: "Foobar",
Age: 53,
}, td.StructFields{
">Age": td.Between(40, 50), // ">" to overwrite Age:53 in model
"NumChildren": td.Gt(2),
},
"checks %v is the right Person")
fmt.Println("Foobar is between 40 & 50:", ok)
ok = td.CmpStruct(t, got, Person{
Name: "Foobar",
Age: 53,
}, td.StructFields{
"> Age": td.Between(40, 50), // same, ">" can be followed by spaces
"NumChildren": td.Gt(2),
},
"checks %v is the right Person")
fmt.Println("Foobar is between 40 & 50:", ok)
// Output:
// Foobar is between 40 & 50: true
// Foobar is between 40 & 50: true
}
func ExampleCmpStruct_patterns() {
t := &testing.T{}
type Person struct {
Firstname string
Lastname string
Surname string
Nickname string
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
}
now := time.Now()
got := Person{
Firstname: "Maxime",
Lastname: "Foo",
Surname: "Max",
Nickname: "max",
CreatedAt: now,
UpdatedAt: now,
DeletedAt: nil, // not deleted yet
}
ok := td.CmpStruct(t, got, Person{Lastname: "Foo"}, td.StructFields{
`DeletedAt`: nil,
`= *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model
`=~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt
},
"mix shell & regexp patterns")
fmt.Println("Patterns match only remaining fields:", ok)
ok = td.CmpStruct(t, got, Person{Lastname: "Foo"}, td.StructFields{
`DeletedAt`: nil,
`1 = *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model
`2 =~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt
},
"ordered patterns")
fmt.Println("Ordered patterns match only remaining fields:", ok)
// Output:
// Patterns match only remaining fields: true
// Ordered patterns match only remaining fields: true
}
func ExampleCmpStruct_lazy_model() {
t := &testing.T{}
got := struct {
name string
age int
}{
name: "Foobar",
age: 42,
}
ok := td.CmpStruct(t, got, nil, td.StructFields{
"name": "Foobar",
"age": td.Between(40, 45),
})
fmt.Println("Lazy model:", ok)
ok = td.CmpStruct(t, got, nil, td.StructFields{
"name": "Foobar",
"zip": 666,
})
fmt.Println("Lazy model with unknown field:", ok)
// Output:
// Lazy model: true
// Lazy model with unknown field: false
}
func ExampleCmpSubBagOf() {
t := &testing.T{}
got := []int{1, 3, 5, 8, 8, 1, 2}
ok := td.CmpSubBagOf(t, got, []any{0, 0, 1, 1, 2, 2, 3, 3, 5, 5, 8, 8, 9, 9},
"checks at least all items are present, in any order")
fmt.Println(ok)
// got contains one 8 too many
ok = td.CmpSubBagOf(t, got, []any{0, 0, 1, 1, 2, 2, 3, 3, 5, 5, 8, 9, 9},
"checks at least all items are present, in any order")
fmt.Println(ok)
got = []int{1, 3, 5, 2}
ok = td.CmpSubBagOf(t, got, []any{td.Between(0, 3), td.Between(0, 3), td.Between(0, 3), td.Between(0, 3), td.Gt(4), td.Gt(4)},
"checks at least all items match, in any order with TestDeep operators")
fmt.Println(ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using expected... without copying it to a new
// []any slice, then use td.Flatten!
expected := []int{1, 2, 3, 5, 9, 8}
ok = td.CmpSubBagOf(t, got, []any{td.Flatten(expected)},
"checks at least all expected items are present, in any order")
fmt.Println(ok)
// Output:
// true
// false
// true
// true
}
func ExampleCmpSubJSONOf_basic() {
t := &testing.T{}
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
}{
Fullname: "Bob",
Age: 42,
}
ok := td.CmpSubJSONOf(t, got, `{"age":42,"fullname":"Bob","gender":"male"}`, nil)
fmt.Println("check got with age then fullname:", ok)
ok = td.CmpSubJSONOf(t, got, `{"fullname":"Bob","age":42,"gender":"male"}`, nil)
fmt.Println("check got with fullname then age:", ok)
ok = td.CmpSubJSONOf(t, got, `
// This should be the JSON representation of a struct
{
// A person:
"fullname": "Bob", // The name of this person
"age": 42, /* The age of this person:
- 42 of course
- to demonstrate a multi-lines comment */
"gender": "male" // This field is ignored as SubJSONOf
}`, nil)
fmt.Println("check got with nicely formatted and commented JSON:", ok)
ok = td.CmpSubJSONOf(t, got, `{"fullname":"Bob","gender":"male"}`, nil)
fmt.Println("check got without age field:", ok)
// Output:
// check got with age then fullname: true
// check got with fullname then age: true
// check got with nicely formatted and commented JSON: true
// check got without age field: false
}
func ExampleCmpSubJSONOf_placeholders() {
t := &testing.T{}
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
}{
Fullname: "Bob Foobar",
Age: 42,
}
ok := td.CmpSubJSONOf(t, got, `{"age": $1, "fullname": $2, "gender": $3}`, []any{42, "Bob Foobar", "male"})
fmt.Println("check got with numeric placeholders without operators:", ok)
ok = td.CmpSubJSONOf(t, got, `{"age": $1, "fullname": $2, "gender": $3}`, []any{td.Between(40, 45), td.HasSuffix("Foobar"), td.NotEmpty()})
fmt.Println("check got with numeric placeholders:", ok)
ok = td.CmpSubJSONOf(t, got, `{"age": "$1", "fullname": "$2", "gender": "$3"}`, []any{td.Between(40, 45), td.HasSuffix("Foobar"), td.NotEmpty()})
fmt.Println("check got with double-quoted numeric placeholders:", ok)
ok = td.CmpSubJSONOf(t, got, `{"age": $age, "fullname": $name, "gender": $gender}`, []any{td.Tag("age", td.Between(40, 45)), td.Tag("name", td.HasSuffix("Foobar")), td.Tag("gender", td.NotEmpty())})
fmt.Println("check got with named placeholders:", ok)
ok = td.CmpSubJSONOf(t, got, `{"age": $^NotZero, "fullname": $^NotEmpty, "gender": $^NotEmpty}`, nil)
fmt.Println("check got with operator shortcuts:", ok)
// Output:
// check got with numeric placeholders without operators: true
// check got with numeric placeholders: true
// check got with double-quoted numeric placeholders: true
// check got with named placeholders: true
// check got with operator shortcuts: true
}
func ExampleCmpSubJSONOf_file() {
t := &testing.T{}
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
Gender string `json:"gender"`
}{
Fullname: "Bob Foobar",
Age: 42,
Gender: "male",
}
tmpDir, err := os.MkdirTemp("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir) //nolint: errcheck // clean up
filename := tmpDir + "/test.json"
if err = os.WriteFile(filename, []byte(`
{
"fullname": "$name",
"age": "$age",
"gender": "$gender",
"details": {
"city": "TestCity",
"zip": 666
}
}`), 0644); err != nil {
t.Fatal(err)
}
// OK let's test with this file
ok := td.CmpSubJSONOf(t, got, filename, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))})
fmt.Println("Full match from file name:", ok)
// When the file is already open
file, err := os.Open(filename)
if err != nil {
t.Fatal(err)
}
ok = td.CmpSubJSONOf(t, got, file, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))})
fmt.Println("Full match from io.Reader:", ok)
// Output:
// Full match from file name: true
// Full match from io.Reader: true
}
func ExampleCmpSubMapOf_map() {
t := &testing.T{}
got := map[string]int{"foo": 12, "bar": 42}
ok := td.CmpSubMapOf(t, got, map[string]int{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": 666},
"checks map %v is included in expected keys/values", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleCmpSubMapOf_typedMap() {
t := &testing.T{}
type MyMap map[string]int
got := MyMap{"foo": 12, "bar": 42}
ok := td.CmpSubMapOf(t, got, MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": 666},
"checks typed map %v is included in expected keys/values", got)
fmt.Println(ok)
ok = td.CmpSubMapOf(t, &got, &MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": 666},
"checks pointed typed map %v is included in expected keys/values", got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleCmpSubSetOf() {
t := &testing.T{}
got := []int{1, 3, 5, 8, 8, 1, 2}
// Matches as all items are expected, ignoring duplicates
ok := td.CmpSubSetOf(t, got, []any{1, 2, 3, 4, 5, 6, 7, 8},
"checks at least all items are present, in any order, ignoring duplicates")
fmt.Println(ok)
// Tries its best not to raise an error when a value can be matched
// by several SubSetOf entries
ok = td.CmpSubSetOf(t, got, []any{td.Between(1, 4), 3, td.Between(2, 10), td.Gt(100)},
"checks at least all items are present, in any order, ignoring duplicates")
fmt.Println(ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using expected... without copying it to a new
// []any slice, then use td.Flatten!
expected := []int{1, 2, 3, 4, 5, 6, 7, 8}
ok = td.CmpSubSetOf(t, got, []any{td.Flatten(expected)},
"checks at least all expected items are present, in any order, ignoring duplicates")
fmt.Println(ok)
// Output:
// true
// true
// true
}
func ExampleCmpSuperBagOf() {
t := &testing.T{}
got := []int{1, 3, 5, 8, 8, 1, 2}
ok := td.CmpSuperBagOf(t, got, []any{8, 5, 8},
"checks the items are present, in any order")
fmt.Println(ok)
ok = td.CmpSuperBagOf(t, got, []any{td.Gt(5), td.Lte(2)},
"checks at least 2 items of %v match", got)
fmt.Println(ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using expected... without copying it to a new
// []any slice, then use td.Flatten!
expected := []int{8, 5, 8}
ok = td.CmpSuperBagOf(t, got, []any{td.Flatten(expected)},
"checks the expected items are present, in any order")
fmt.Println(ok)
// Output:
// true
// true
// true
}
func ExampleCmpSuperJSONOf_basic() {
t := &testing.T{}
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
Gender string `json:"gender"`
City string `json:"city"`
Zip int `json:"zip"`
}{
Fullname: "Bob",
Age: 42,
Gender: "male",
City: "TestCity",
Zip: 666,
}
ok := td.CmpSuperJSONOf(t, got, `{"age":42,"fullname":"Bob","gender":"male"}`, nil)
fmt.Println("check got with age then fullname:", ok)
ok = td.CmpSuperJSONOf(t, got, `{"fullname":"Bob","age":42,"gender":"male"}`, nil)
fmt.Println("check got with fullname then age:", ok)
ok = td.CmpSuperJSONOf(t, got, `
// This should be the JSON representation of a struct
{
// A person:
"fullname": "Bob", // The name of this person
"age": 42, /* The age of this person:
- 42 of course
- to demonstrate a multi-lines comment */
"gender": "male" // The gender!
}`, nil)
fmt.Println("check got with nicely formatted and commented JSON:", ok)
ok = td.CmpSuperJSONOf(t, got, `{"fullname":"Bob","gender":"male","details":{}}`, nil)
fmt.Println("check got with details field:", ok)
// Output:
// check got with age then fullname: true
// check got with fullname then age: true
// check got with nicely formatted and commented JSON: true
// check got with details field: false
}
func ExampleCmpSuperJSONOf_placeholders() {
t := &testing.T{}
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
Gender string `json:"gender"`
City string `json:"city"`
Zip int `json:"zip"`
}{
Fullname: "Bob Foobar",
Age: 42,
Gender: "male",
City: "TestCity",
Zip: 666,
}
ok := td.CmpSuperJSONOf(t, got, `{"age": $1, "fullname": $2, "gender": $3}`, []any{42, "Bob Foobar", "male"})
fmt.Println("check got with numeric placeholders without operators:", ok)
ok = td.CmpSuperJSONOf(t, got, `{"age": $1, "fullname": $2, "gender": $3}`, []any{td.Between(40, 45), td.HasSuffix("Foobar"), td.NotEmpty()})
fmt.Println("check got with numeric placeholders:", ok)
ok = td.CmpSuperJSONOf(t, got, `{"age": "$1", "fullname": "$2", "gender": "$3"}`, []any{td.Between(40, 45), td.HasSuffix("Foobar"), td.NotEmpty()})
fmt.Println("check got with double-quoted numeric placeholders:", ok)
ok = td.CmpSuperJSONOf(t, got, `{"age": $age, "fullname": $name, "gender": $gender}`, []any{td.Tag("age", td.Between(40, 45)), td.Tag("name", td.HasSuffix("Foobar")), td.Tag("gender", td.NotEmpty())})
fmt.Println("check got with named placeholders:", ok)
ok = td.CmpSuperJSONOf(t, got, `{"age": $^NotZero, "fullname": $^NotEmpty, "gender": $^NotEmpty}`, nil)
fmt.Println("check got with operator shortcuts:", ok)
// Output:
// check got with numeric placeholders without operators: true
// check got with numeric placeholders: true
// check got with double-quoted numeric placeholders: true
// check got with named placeholders: true
// check got with operator shortcuts: true
}
func ExampleCmpSuperJSONOf_file() {
t := &testing.T{}
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
Gender string `json:"gender"`
City string `json:"city"`
Zip int `json:"zip"`
}{
Fullname: "Bob Foobar",
Age: 42,
Gender: "male",
City: "TestCity",
Zip: 666,
}
tmpDir, err := os.MkdirTemp("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir) //nolint: errcheck // clean up
filename := tmpDir + "/test.json"
if err = os.WriteFile(filename, []byte(`
{
"fullname": "$name",
"age": "$age",
"gender": "$gender"
}`), 0644); err != nil {
t.Fatal(err)
}
// OK let's test with this file
ok := td.CmpSuperJSONOf(t, got, filename, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))})
fmt.Println("Full match from file name:", ok)
// When the file is already open
file, err := os.Open(filename)
if err != nil {
t.Fatal(err)
}
ok = td.CmpSuperJSONOf(t, got, file, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))})
fmt.Println("Full match from io.Reader:", ok)
// Output:
// Full match from file name: true
// Full match from io.Reader: true
}
func ExampleCmpSuperMapOf_map() {
t := &testing.T{}
got := map[string]int{"foo": 12, "bar": 42, "zip": 89}
ok := td.CmpSuperMapOf(t, got, map[string]int{"bar": 42}, td.MapEntries{"foo": td.Lt(15)},
"checks map %v contains at least all expected keys/values", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleCmpSuperMapOf_typedMap() {
t := &testing.T{}
type MyMap map[string]int
got := MyMap{"foo": 12, "bar": 42, "zip": 89}
ok := td.CmpSuperMapOf(t, got, MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15)},
"checks typed map %v contains at least all expected keys/values", got)
fmt.Println(ok)
ok = td.CmpSuperMapOf(t, &got, &MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15)},
"checks pointed typed map %v contains at least all expected keys/values",
got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleCmpSuperSetOf() {
t := &testing.T{}
got := []int{1, 3, 5, 8, 8, 1, 2}
ok := td.CmpSuperSetOf(t, got, []any{1, 2, 3},
"checks the items are present, in any order and ignoring duplicates")
fmt.Println(ok)
ok = td.CmpSuperSetOf(t, got, []any{td.Gt(5), td.Lte(2)},
"checks at least 2 items of %v match ignoring duplicates", got)
fmt.Println(ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using expected... without copying it to a new
// []any slice, then use td.Flatten!
expected := []int{1, 2, 3}
ok = td.CmpSuperSetOf(t, got, []any{td.Flatten(expected)},
"checks the expected items are present, in any order and ignoring duplicates")
fmt.Println(ok)
// Output:
// true
// true
// true
}
func ExampleCmpSuperSliceOf_array() {
t := &testing.T{}
got := [4]int{42, 58, 26, 666}
ok := td.CmpSuperSliceOf(t, got, [4]int{1: 58}, td.ArrayEntries{3: td.Gt(660)},
"checks array %v", got)
fmt.Println("Only check items #1 & #3:", ok)
ok = td.CmpSuperSliceOf(t, got, [4]int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3:", ok)
ok = td.CmpSuperSliceOf(t, &got, &[4]int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of an array pointer:", ok)
ok = td.CmpSuperSliceOf(t, &got, (*[4]int)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of an array pointer, using nil model:", ok)
// Output:
// Only check items #1 & #3: true
// Only check items #0 & #3: true
// Only check items #0 & #3 of an array pointer: true
// Only check items #0 & #3 of an array pointer, using nil model: true
}
func ExampleCmpSuperSliceOf_typedArray() {
t := &testing.T{}
type MyArray [4]int
got := MyArray{42, 58, 26, 666}
ok := td.CmpSuperSliceOf(t, got, MyArray{1: 58}, td.ArrayEntries{3: td.Gt(660)},
"checks typed array %v", got)
fmt.Println("Only check items #1 & #3:", ok)
ok = td.CmpSuperSliceOf(t, got, MyArray{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3:", ok)
ok = td.CmpSuperSliceOf(t, &got, &MyArray{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of an array pointer:", ok)
ok = td.CmpSuperSliceOf(t, &got, (*MyArray)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of an array pointer, using nil model:", ok)
// Output:
// Only check items #1 & #3: true
// Only check items #0 & #3: true
// Only check items #0 & #3 of an array pointer: true
// Only check items #0 & #3 of an array pointer, using nil model: true
}
func ExampleCmpSuperSliceOf_slice() {
t := &testing.T{}
got := []int{42, 58, 26, 666}
ok := td.CmpSuperSliceOf(t, got, []int{1: 58}, td.ArrayEntries{3: td.Gt(660)},
"checks array %v", got)
fmt.Println("Only check items #1 & #3:", ok)
ok = td.CmpSuperSliceOf(t, got, []int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3:", ok)
ok = td.CmpSuperSliceOf(t, &got, &[]int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of a slice pointer:", ok)
ok = td.CmpSuperSliceOf(t, &got, (*[]int)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of a slice pointer, using nil model:", ok)
// Output:
// Only check items #1 & #3: true
// Only check items #0 & #3: true
// Only check items #0 & #3 of a slice pointer: true
// Only check items #0 & #3 of a slice pointer, using nil model: true
}
func ExampleCmpSuperSliceOf_typedSlice() {
t := &testing.T{}
type MySlice []int
got := MySlice{42, 58, 26, 666}
ok := td.CmpSuperSliceOf(t, got, MySlice{1: 58}, td.ArrayEntries{3: td.Gt(660)},
"checks typed array %v", got)
fmt.Println("Only check items #1 & #3:", ok)
ok = td.CmpSuperSliceOf(t, got, MySlice{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3:", ok)
ok = td.CmpSuperSliceOf(t, &got, &MySlice{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of a slice pointer:", ok)
ok = td.CmpSuperSliceOf(t, &got, (*MySlice)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of a slice pointer, using nil model:", ok)
// Output:
// Only check items #1 & #3: true
// Only check items #0 & #3: true
// Only check items #0 & #3 of a slice pointer: true
// Only check items #0 & #3 of a slice pointer, using nil model: true
}
func ExampleCmpTruncTime() {
t := &testing.T{}
dateToTime := func(str string) time.Time {
t, err := time.Parse(time.RFC3339Nano, str)
if err != nil {
panic(err)
}
return t
}
got := dateToTime("2018-05-01T12:45:53.123456789Z")
// Compare dates ignoring nanoseconds and monotonic parts
expected := dateToTime("2018-05-01T12:45:53Z")
ok := td.CmpTruncTime(t, got, expected, time.Second,
"checks date %v, truncated to the second", got)
fmt.Println(ok)
// Compare dates ignoring time and so monotonic parts
expected = dateToTime("2018-05-01T11:22:33.444444444Z")
ok = td.CmpTruncTime(t, got, expected, 24*time.Hour,
"checks date %v, truncated to the day", got)
fmt.Println(ok)
// Compare dates exactly but ignoring monotonic part
expected = dateToTime("2018-05-01T12:45:53.123456789Z")
ok = td.CmpTruncTime(t, got, expected, 0,
"checks date %v ignoring monotonic part", got)
fmt.Println(ok)
// Output:
// true
// true
// true
}
func ExampleCmpValues() {
t := &testing.T{}
got := map[string]int{"foo": 1, "bar": 2, "zip": 3}
// Values tests values in an ordered manner
ok := td.CmpValues(t, got, []int{1, 2, 3})
fmt.Println("All sorted values are found:", ok)
// If the expected values are not ordered, it fails
ok = td.CmpValues(t, got, []int{3, 1, 2})
fmt.Println("All unsorted values are found:", ok)
// To circumvent that, one can use Bag operator
ok = td.CmpValues(t, got, td.Bag(3, 1, 2))
fmt.Println("All unsorted values are found, with the help of Bag operator:", ok)
// Check that each value is between 1 and 3
ok = td.CmpValues(t, got, td.ArrayEach(td.Between(1, 3)))
fmt.Println("Each value is between 1 and 3:", ok)
// Output:
// All sorted values are found: true
// All unsorted values are found: false
// All unsorted values are found, with the help of Bag operator: true
// Each value is between 1 and 3: true
}
func ExampleCmpZero() {
t := &testing.T{}
ok := td.CmpZero(t, 0)
fmt.Println(ok)
ok = td.CmpZero(t, float64(0))
fmt.Println(ok)
ok = td.CmpZero(t, 12) // fails, as 12 is not 0 :)
fmt.Println(ok)
ok = td.CmpZero(t, (map[string]int)(nil))
fmt.Println(ok)
ok = td.CmpZero(t, map[string]int{}) // fails, as not nil
fmt.Println(ok)
ok = td.CmpZero(t, ([]int)(nil))
fmt.Println(ok)
ok = td.CmpZero(t, []int{}) // fails, as not nil
fmt.Println(ok)
ok = td.CmpZero(t, [3]int{})
fmt.Println(ok)
ok = td.CmpZero(t, [3]int{0, 1}) // fails, DATA[1] is not 0
fmt.Println(ok)
ok = td.CmpZero(t, bytes.Buffer{})
fmt.Println(ok)
ok = td.CmpZero(t, &bytes.Buffer{}) // fails, as pointer not nil
fmt.Println(ok)
ok = td.Cmp(t, &bytes.Buffer{}, td.Ptr(td.Zero())) // OK with the help of Ptr()
fmt.Println(ok)
// Output:
// true
// true
// false
// true
// false
// true
// false
// true
// false
// true
// false
// true
}
go-testdeep-1.15.0/td/example_t_test.go 0000664 0000000 0000000 00000320154 15144170453 0020002 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2025, 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.
//
// DO NOT EDIT!!! AUTOMATICALLY GENERATED!!!
package td_test
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"math"
"os"
"regexp"
"strconv"
"strings"
"testing"
"time"
"github.com/maxatome/go-testdeep/td"
)
func ExampleT_All() {
t := td.NewT(&testing.T{})
got := "foo/bar"
// Checks got string against:
// "o/b" regexp *AND* "bar" suffix *AND* exact "foo/bar" string
ok := t.All(got, []any{td.Re("o/b"), td.HasSuffix("bar"), "foo/bar"},
"checks value %s", got)
fmt.Println(ok)
// Checks got string against:
// "o/b" regexp *AND* "bar" suffix *AND* exact "fooX/Ybar" string
ok = t.All(got, []any{td.Re("o/b"), td.HasSuffix("bar"), "fooX/Ybar"},
"checks value %s", got)
fmt.Println(ok)
// When some operators or values have to be reused and mixed between
// several calls, Flatten can be used to avoid boring and
// inefficient []any copies:
regOps := td.Flatten([]td.TestDeep{td.Re("o/b"), td.Re(`^fo`), td.Re(`ar$`)})
ok = t.All(got, []any{td.HasPrefix("foo"), regOps, td.HasSuffix("bar")},
"checks all operators against value %s", got)
fmt.Println(ok)
// Output:
// true
// false
// true
}
func ExampleT_Any() {
t := td.NewT(&testing.T{})
got := "foo/bar"
// Checks got string against:
// "zip" regexp *OR* "bar" suffix
ok := t.Any(got, []any{td.Re("zip"), td.HasSuffix("bar")},
"checks value %s", got)
fmt.Println(ok)
// Checks got string against:
// "zip" regexp *OR* "foo" suffix
ok = t.Any(got, []any{td.Re("zip"), td.HasSuffix("foo")},
"checks value %s", got)
fmt.Println(ok)
// When some operators or values have to be reused and mixed between
// several calls, Flatten can be used to avoid boring and
// inefficient []any copies:
regOps := td.Flatten([]td.TestDeep{td.Re("a/c"), td.Re(`^xx`), td.Re(`ar$`)})
ok = t.Any(got, []any{td.HasPrefix("xxx"), regOps, td.HasSuffix("zip")},
"check at least one operator matches value %s", got)
fmt.Println(ok)
// Output:
// true
// false
// true
}
func ExampleT_Array_array() {
t := td.NewT(&testing.T{})
got := [3]int{42, 58, 26}
ok := t.Array(got, [3]int{42}, td.ArrayEntries{1: 58, 2: td.Ignore()},
"checks array %v", got)
fmt.Println("Simple array:", ok)
ok = t.Array(&got, &[3]int{42}, td.ArrayEntries{1: 58, 2: td.Ignore()},
"checks array %v", got)
fmt.Println("Array pointer:", ok)
ok = t.Array(&got, (*[3]int)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()},
"checks array %v", got)
fmt.Println("Array pointer, nil model:", ok)
// Output:
// Simple array: true
// Array pointer: true
// Array pointer, nil model: true
}
func ExampleT_Array_typedArray() {
t := td.NewT(&testing.T{})
type MyArray [3]int
got := MyArray{42, 58, 26}
ok := t.Array(got, MyArray{42}, td.ArrayEntries{1: 58, 2: td.Ignore()},
"checks typed array %v", got)
fmt.Println("Typed array:", ok)
ok = t.Array(&got, &MyArray{42}, td.ArrayEntries{1: 58, 2: td.Ignore()},
"checks pointer on typed array %v", got)
fmt.Println("Pointer on a typed array:", ok)
ok = t.Array(&got, &MyArray{}, td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()},
"checks pointer on typed array %v", got)
fmt.Println("Pointer on a typed array, empty model:", ok)
ok = t.Array(&got, (*MyArray)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()},
"checks pointer on typed array %v", got)
fmt.Println("Pointer on a typed array, nil model:", ok)
// Output:
// Typed array: true
// Pointer on a typed array: true
// Pointer on a typed array, empty model: true
// Pointer on a typed array, nil model: true
}
func ExampleT_ArrayEach_array() {
t := td.NewT(&testing.T{})
got := [3]int{42, 58, 26}
ok := t.ArrayEach(got, td.Between(25, 60),
"checks each item of array %v is in [25 .. 60]", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleT_ArrayEach_typedArray() {
t := td.NewT(&testing.T{})
type MyArray [3]int
got := MyArray{42, 58, 26}
ok := t.ArrayEach(got, td.Between(25, 60),
"checks each item of typed array %v is in [25 .. 60]", got)
fmt.Println(ok)
ok = t.ArrayEach(&got, td.Between(25, 60),
"checks each item of typed array pointer %v is in [25 .. 60]", got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleT_ArrayEach_slice() {
t := td.NewT(&testing.T{})
got := []int{42, 58, 26}
ok := t.ArrayEach(got, td.Between(25, 60),
"checks each item of slice %v is in [25 .. 60]", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleT_ArrayEach_typedSlice() {
t := td.NewT(&testing.T{})
type MySlice []int
got := MySlice{42, 58, 26}
ok := t.ArrayEach(got, td.Between(25, 60),
"checks each item of typed slice %v is in [25 .. 60]", got)
fmt.Println(ok)
ok = t.ArrayEach(&got, td.Between(25, 60),
"checks each item of typed slice pointer %v is in [25 .. 60]", got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleT_Bag() {
t := td.NewT(&testing.T{})
got := []int{1, 3, 5, 8, 8, 1, 2}
// Matches as all items are present
ok := t.Bag(got, []any{1, 1, 2, 3, 5, 8, 8},
"checks all items are present, in any order")
fmt.Println(ok)
// Does not match as got contains 2 times 1 and 8, and these
// duplicates are not expected
ok = t.Bag(got, []any{1, 2, 3, 5, 8},
"checks all items are present, in any order")
fmt.Println(ok)
got = []int{1, 3, 5, 8, 2}
// Duplicates of 1 and 8 are expected but not present in got
ok = t.Bag(got, []any{1, 1, 2, 3, 5, 8, 8},
"checks all items are present, in any order")
fmt.Println(ok)
// Matches as all items are present
ok = t.Bag(got, []any{1, 2, 3, 5, td.Gt(7)},
"checks all items are present, in any order")
fmt.Println(ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using expected... without copying it to a new
// []any slice, then use td.Flatten!
expected := []int{1, 2, 3, 5}
ok = t.Bag(got, []any{td.Flatten(expected), td.Gt(7)},
"checks all expected items are present, in any order")
fmt.Println(ok)
// Output:
// true
// false
// false
// true
// true
}
func ExampleT_Between_int() {
t := td.NewT(&testing.T{})
got := 156
ok := t.Between(got, 154, 156, td.BoundsInIn,
"checks %v is in [154 .. 156]", got)
fmt.Println(ok)
// BoundsInIn is implicit
ok = t.Between(got, 154, 156, td.BoundsInIn,
"checks %v is in [154 .. 156]", got)
fmt.Println(ok)
ok = t.Between(got, 154, 156, td.BoundsInOut,
"checks %v is in [154 .. 156[", got)
fmt.Println(ok)
ok = t.Between(got, 154, 156, td.BoundsOutIn,
"checks %v is in ]154 .. 156]", got)
fmt.Println(ok)
ok = t.Between(got, 154, 156, td.BoundsOutOut,
"checks %v is in ]154 .. 156[", got)
fmt.Println(ok)
// Output:
// true
// true
// false
// true
// false
}
func ExampleT_Between_string() {
t := td.NewT(&testing.T{})
got := "abc"
ok := t.Between(got, "aaa", "abc", td.BoundsInIn,
`checks "%v" is in ["aaa" .. "abc"]`, got)
fmt.Println(ok)
// BoundsInIn is implicit
ok = t.Between(got, "aaa", "abc", td.BoundsInIn,
`checks "%v" is in ["aaa" .. "abc"]`, got)
fmt.Println(ok)
ok = t.Between(got, "aaa", "abc", td.BoundsInOut,
`checks "%v" is in ["aaa" .. "abc"[`, got)
fmt.Println(ok)
ok = t.Between(got, "aaa", "abc", td.BoundsOutIn,
`checks "%v" is in ]"aaa" .. "abc"]`, got)
fmt.Println(ok)
ok = t.Between(got, "aaa", "abc", td.BoundsOutOut,
`checks "%v" is in ]"aaa" .. "abc"[`, got)
fmt.Println(ok)
// Output:
// true
// true
// false
// true
// false
}
func ExampleT_Between_time() {
t := td.NewT(&testing.T{})
before := time.Now()
occurredAt := time.Now()
after := time.Now()
ok := t.Between(occurredAt, before, after, td.BoundsInIn)
fmt.Println("It occurred between before and after:", ok)
type MyTime time.Time
ok = t.Between(MyTime(occurredAt), MyTime(before), MyTime(after), td.BoundsInIn)
fmt.Println("Same for convertible MyTime type:", ok)
ok = t.Between(MyTime(occurredAt), before, after, td.BoundsInIn)
fmt.Println("MyTime vs time.Time:", ok)
ok = t.Between(occurredAt, before, 10*time.Second, td.BoundsInIn)
fmt.Println("Using a time.Duration as TO:", ok)
ok = t.Between(MyTime(occurredAt), MyTime(before), 10*time.Second, td.BoundsInIn)
fmt.Println("Using MyTime as FROM and time.Duration as TO:", ok)
// Output:
// It occurred between before and after: true
// Same for convertible MyTime type: true
// MyTime vs time.Time: false
// Using a time.Duration as TO: true
// Using MyTime as FROM and time.Duration as TO: true
}
func ExampleT_Cap() {
t := td.NewT(&testing.T{})
got := make([]int, 0, 12)
ok := t.Cap(got, 12, "checks %v capacity is 12", got)
fmt.Println(ok)
ok = t.Cap(got, 0, "checks %v capacity is 0", got)
fmt.Println(ok)
got = nil
ok = t.Cap(got, 0, "checks %v capacity is 0", got)
fmt.Println(ok)
// Output:
// true
// false
// true
}
func ExampleT_Cap_operator() {
t := td.NewT(&testing.T{})
got := make([]int, 0, 12)
ok := t.Cap(got, td.Between(10, 12),
"checks %v capacity is in [10 .. 12]", got)
fmt.Println(ok)
ok = t.Cap(got, td.Gt(10),
"checks %v capacity is in [10 .. 12]", got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleT_Code() {
t := td.NewT(&testing.T{})
got := "12"
ok := t.Code(got, func(num string) bool {
n, err := strconv.Atoi(num)
return err == nil && n > 10 && n < 100
},
"checks string `%s` contains a number and this number is in ]10 .. 100[",
got)
fmt.Println(ok)
// Same with failure reason
ok = t.Code(got, func(num string) (bool, string) {
n, err := strconv.Atoi(num)
if err != nil {
return false, "not a number"
}
if n > 10 && n < 100 {
return true, ""
}
return false, "not in ]10 .. 100["
},
"checks string `%s` contains a number and this number is in ]10 .. 100[",
got)
fmt.Println(ok)
// Same with failure reason thanks to error
ok = t.Code(got, func(num string) error {
n, err := strconv.Atoi(num)
if err != nil {
return err
}
if n > 10 && n < 100 {
return nil
}
return fmt.Errorf("%d not in ]10 .. 100[", n)
},
"checks string `%s` contains a number and this number is in ]10 .. 100[",
got)
fmt.Println(ok)
// Output:
// true
// true
// true
}
func ExampleT_Code_custom() {
t := td.NewT(&testing.T{})
got := 123
ok := t.Code(got, func(t *td.T, num int) {
t.Cmp(num, 123)
})
fmt.Println("with one *td.T:", ok)
ok = t.Code(got, func(assert, require *td.T, num int) {
assert.Cmp(num, 123)
require.Cmp(num, 123)
})
fmt.Println("with assert & require *td.T:", ok)
// Output:
// with one *td.T: true
// with assert & require *td.T: true
}
func ExampleT_Contains_arraySlice() {
t := td.NewT(&testing.T{})
ok := t.Contains([...]int{11, 22, 33, 44}, 22)
fmt.Println("array contains 22:", ok)
ok = t.Contains([...]int{11, 22, 33, 44}, td.Between(20, 25))
fmt.Println("array contains at least one item in [20 .. 25]:", ok)
ok = t.Contains([]int{11, 22, 33, 44}, 22)
fmt.Println("slice contains 22:", ok)
ok = t.Contains([]int{11, 22, 33, 44}, td.Between(20, 25))
fmt.Println("slice contains at least one item in [20 .. 25]:", ok)
ok = t.Contains([]int{11, 22, 33, 44}, []int{22, 33})
fmt.Println("slice contains the sub-slice [22, 33]:", ok)
// Output:
// array contains 22: true
// array contains at least one item in [20 .. 25]: true
// slice contains 22: true
// slice contains at least one item in [20 .. 25]: true
// slice contains the sub-slice [22, 33]: true
}
func ExampleT_Contains_nil() {
t := td.NewT(&testing.T{})
num := 123
got := [...]*int{&num, nil}
ok := t.Contains(got, nil)
fmt.Println("array contains untyped nil:", ok)
ok = t.Contains(got, (*int)(nil))
fmt.Println("array contains *int nil:", ok)
ok = t.Contains(got, td.Nil())
fmt.Println("array contains Nil():", ok)
ok = t.Contains(got, (*byte)(nil))
fmt.Println("array contains *byte nil:", ok) // types differ: *byte ≠ *int
// Output:
// array contains untyped nil: true
// array contains *int nil: true
// array contains Nil(): true
// array contains *byte nil: false
}
func ExampleT_Contains_map() {
t := td.NewT(&testing.T{})
ok := t.Contains(map[string]int{"foo": 11, "bar": 22, "zip": 33}, 22)
fmt.Println("map contains value 22:", ok)
ok = t.Contains(map[string]int{"foo": 11, "bar": 22, "zip": 33}, td.Between(20, 25))
fmt.Println("map contains at least one value in [20 .. 25]:", ok)
// Output:
// map contains value 22: true
// map contains at least one value in [20 .. 25]: true
}
func ExampleT_Contains_string() {
t := td.NewT(&testing.T{})
got := "foobar"
ok := t.Contains(got, "oob", "checks %s", got)
fmt.Println("contains `oob` string:", ok)
ok = t.Contains(got, []byte("oob"), "checks %s", got)
fmt.Println("contains `oob` []byte:", ok)
ok = t.Contains(got, 'b', "checks %s", got)
fmt.Println("contains 'b' rune:", ok)
ok = t.Contains(got, byte('a'), "checks %s", got)
fmt.Println("contains 'a' byte:", ok)
ok = t.Contains(got, td.Between('n', 'p'), "checks %s", got)
fmt.Println("contains at least one character ['n' .. 'p']:", ok)
// Output:
// contains `oob` string: true
// contains `oob` []byte: true
// contains 'b' rune: true
// contains 'a' byte: true
// contains at least one character ['n' .. 'p']: true
}
func ExampleT_Contains_stringer() {
t := td.NewT(&testing.T{})
// bytes.Buffer implements fmt.Stringer
got := bytes.NewBufferString("foobar")
ok := t.Contains(got, "oob", "checks %s", got)
fmt.Println("contains `oob` string:", ok)
ok = t.Contains(got, 'b', "checks %s", got)
fmt.Println("contains 'b' rune:", ok)
ok = t.Contains(got, byte('a'), "checks %s", got)
fmt.Println("contains 'a' byte:", ok)
ok = t.Contains(got, td.Between('n', 'p'), "checks %s", got)
fmt.Println("contains at least one character ['n' .. 'p']:", ok)
// Output:
// contains `oob` string: true
// contains 'b' rune: true
// contains 'a' byte: true
// contains at least one character ['n' .. 'p']: true
}
func ExampleT_Contains_error() {
t := td.NewT(&testing.T{})
got := errors.New("foobar")
ok := t.Contains(got, "oob", "checks %s", got)
fmt.Println("contains `oob` string:", ok)
ok = t.Contains(got, 'b', "checks %s", got)
fmt.Println("contains 'b' rune:", ok)
ok = t.Contains(got, byte('a'), "checks %s", got)
fmt.Println("contains 'a' byte:", ok)
ok = t.Contains(got, td.Between('n', 'p'), "checks %s", got)
fmt.Println("contains at least one character ['n' .. 'p']:", ok)
// Output:
// contains `oob` string: true
// contains 'b' rune: true
// contains 'a' byte: true
// contains at least one character ['n' .. 'p']: true
}
func ExampleT_ContainsKey() {
t := td.NewT(&testing.T{})
ok := t.ContainsKey(map[string]int{"foo": 11, "bar": 22, "zip": 33}, "foo")
fmt.Println(`map contains key "foo":`, ok)
ok = t.ContainsKey(map[int]bool{12: true, 24: false, 42: true, 51: false}, td.Between(40, 50))
fmt.Println("map contains at least a key in [40 .. 50]:", ok)
ok = t.ContainsKey(map[string]int{"FOO": 11, "bar": 22, "zip": 33}, td.Smuggle(strings.ToLower, "foo"))
fmt.Println(`map contains key "foo" without taking case into account:`, ok)
// Output:
// map contains key "foo": true
// map contains at least a key in [40 .. 50]: true
// map contains key "foo" without taking case into account: true
}
func ExampleT_ContainsKey_nil() {
t := td.NewT(&testing.T{})
num := 1234
got := map[*int]bool{&num: false, nil: true}
ok := t.ContainsKey(got, nil)
fmt.Println("map contains untyped nil key:", ok)
ok = t.ContainsKey(got, (*int)(nil))
fmt.Println("map contains *int nil key:", ok)
ok = t.ContainsKey(got, td.Nil())
fmt.Println("map contains Nil() key:", ok)
ok = t.ContainsKey(got, (*byte)(nil))
fmt.Println("map contains *byte nil key:", ok) // types differ: *byte ≠ *int
// Output:
// map contains untyped nil key: true
// map contains *int nil key: true
// map contains Nil() key: true
// map contains *byte nil key: false
}
func ExampleT_Empty() {
t := td.NewT(&testing.T{})
ok := t.Empty(nil) // special case: nil is considered empty
fmt.Println(ok)
// fails, typed nil is not empty (expect for channel, map, slice or
// pointers on array, channel, map slice and strings)
ok = t.Empty((*int)(nil))
fmt.Println(ok)
ok = t.Empty("")
fmt.Println(ok)
// Fails as 0 is a number, so not empty. Use Zero() instead
ok = t.Empty(0)
fmt.Println(ok)
ok = t.Empty((map[string]int)(nil))
fmt.Println(ok)
ok = t.Empty(map[string]int{})
fmt.Println(ok)
ok = t.Empty(([]int)(nil))
fmt.Println(ok)
ok = t.Empty([]int{})
fmt.Println(ok)
ok = t.Empty([]int{3}) // fails, as not empty
fmt.Println(ok)
ok = t.Empty([3]int{}) // fails, Empty() is not Zero()!
fmt.Println(ok)
// Output:
// true
// false
// true
// false
// true
// true
// true
// true
// false
// false
}
func ExampleT_Empty_pointers() {
t := td.NewT(&testing.T{})
type MySlice []int
ok := t.Empty(MySlice{}) // Ptr() not needed
fmt.Println(ok)
ok = t.Empty(&MySlice{})
fmt.Println(ok)
l1 := &MySlice{}
l2 := &l1
l3 := &l2
ok = t.Empty(&l3)
fmt.Println(ok)
// Works the same for array, map, channel and string
// But not for others types as:
type MyStruct struct {
Value int
}
ok = t.Empty(&MyStruct{}) // fails, use Zero() instead
fmt.Println(ok)
// Output:
// true
// true
// true
// false
}
func ExampleT_CmpErrorIs() {
t := td.NewT(&testing.T{})
err1 := fmt.Errorf("failure1")
err2 := fmt.Errorf("failure2: %w", err1)
err3 := fmt.Errorf("failure3: %w", err2)
err := fmt.Errorf("failure4: %w", err3)
ok := t.CmpErrorIs(err, err)
fmt.Println("error is itself:", ok)
ok = t.CmpErrorIs(err, err1)
fmt.Println("error is also err1:", ok)
ok = t.CmpErrorIs(err1, err)
fmt.Println("err1 is err:", ok)
// Output:
// error is itself: true
// error is also err1: true
// err1 is err: 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{})
got := 156
ok := t.Gt(got, 155, "checks %v is > 155", got)
fmt.Println(ok)
ok = t.Gt(got, 156, "checks %v is > 156", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleT_Gt_string() {
t := td.NewT(&testing.T{})
got := "abc"
ok := t.Gt(got, "abb", `checks "%v" is > "abb"`, got)
fmt.Println(ok)
ok = t.Gt(got, "abc", `checks "%v" is > "abc"`, got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleT_Gte_int() {
t := td.NewT(&testing.T{})
got := 156
ok := t.Gte(got, 156, "checks %v is ≥ 156", got)
fmt.Println(ok)
ok = t.Gte(got, 155, "checks %v is ≥ 155", got)
fmt.Println(ok)
ok = t.Gte(got, 157, "checks %v is ≥ 157", got)
fmt.Println(ok)
// Output:
// true
// true
// false
}
func ExampleT_Gte_string() {
t := td.NewT(&testing.T{})
got := "abc"
ok := t.Gte(got, "abc", `checks "%v" is ≥ "abc"`, got)
fmt.Println(ok)
ok = t.Gte(got, "abb", `checks "%v" is ≥ "abb"`, got)
fmt.Println(ok)
ok = t.Gte(got, "abd", `checks "%v" is ≥ "abd"`, got)
fmt.Println(ok)
// Output:
// true
// true
// false
}
func ExampleT_HasPrefix() {
t := td.NewT(&testing.T{})
got := "foobar"
ok := t.HasPrefix(got, "foo", "checks %s", got)
fmt.Println("using string:", ok)
ok = t.Cmp([]byte(got), td.HasPrefix("foo"), "checks %s", got)
fmt.Println("using []byte:", ok)
// Output:
// using string: true
// using []byte: true
}
func ExampleT_HasPrefix_stringer() {
t := td.NewT(&testing.T{})
// bytes.Buffer implements fmt.Stringer
got := bytes.NewBufferString("foobar")
ok := t.HasPrefix(got, "foo", "checks %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleT_HasPrefix_error() {
t := td.NewT(&testing.T{})
got := errors.New("foobar")
ok := t.HasPrefix(got, "foo", "checks %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleT_HasSuffix() {
t := td.NewT(&testing.T{})
got := "foobar"
ok := t.HasSuffix(got, "bar", "checks %s", got)
fmt.Println("using string:", ok)
ok = t.Cmp([]byte(got), td.HasSuffix("bar"), "checks %s", got)
fmt.Println("using []byte:", ok)
// Output:
// using string: true
// using []byte: true
}
func ExampleT_HasSuffix_stringer() {
t := td.NewT(&testing.T{})
// bytes.Buffer implements fmt.Stringer
got := bytes.NewBufferString("foobar")
ok := t.HasSuffix(got, "bar", "checks %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleT_HasSuffix_error() {
t := td.NewT(&testing.T{})
got := errors.New("foobar")
ok := t.HasSuffix(got, "bar", "checks %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleT_Isa() {
t := td.NewT(&testing.T{})
type TstStruct struct {
Field int
}
got := TstStruct{Field: 1}
ok := t.Isa(got, TstStruct{}, "checks got is a TstStruct")
fmt.Println(ok)
ok = t.Isa(got, &TstStruct{},
"checks got is a pointer on a TstStruct")
fmt.Println(ok)
ok = t.Isa(&got, &TstStruct{},
"checks &got is a pointer on a TstStruct")
fmt.Println(ok)
// Output:
// true
// false
// true
}
func ExampleT_Isa_interface() {
t := td.NewT(&testing.T{})
got := bytes.NewBufferString("foobar")
ok := t.Isa(got, (*fmt.Stringer)(nil),
"checks got implements fmt.Stringer interface")
fmt.Println(ok)
errGot := fmt.Errorf("An error #%d occurred", 123)
ok = t.Isa(errGot, (*error)(nil),
"checks errGot is a *error or implements error interface")
fmt.Println(ok)
// As nil, is passed below, it is not an interface but nil… So it
// does not match
errGot = nil
ok = t.Isa(errGot, (*error)(nil),
"checks errGot is a *error or implements error interface")
fmt.Println(ok)
// BUT if its address is passed, now it is OK as the types match
ok = t.Isa(&errGot, (*error)(nil),
"checks &errGot is a *error or implements error interface")
fmt.Println(ok)
// Output:
// true
// true
// false
// true
}
func ExampleT_JSON_basic() {
t := td.NewT(&testing.T{})
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
}{
Fullname: "Bob",
Age: 42,
}
ok := t.JSON(got, `{"age":42,"fullname":"Bob"}`, nil)
fmt.Println("check got with age then fullname:", ok)
ok = t.JSON(got, `{"fullname":"Bob","age":42}`, nil)
fmt.Println("check got with fullname then age:", ok)
ok = t.JSON(got, `
// This should be the JSON representation of a struct
{
// A person:
"fullname": "Bob", // The name of this person
"age": 42 /* The age of this person:
- 42 of course
- to demonstrate a multi-lines comment */
}`, nil)
fmt.Println("check got with nicely formatted and commented JSON:", ok)
ok = t.JSON(got, `{"fullname":"Bob","age":42,"gender":"male"}`, nil)
fmt.Println("check got with gender field:", ok)
ok = t.JSON(got, `{"fullname":"Bob"}`, nil)
fmt.Println("check got with fullname only:", ok)
ok = t.JSON(true, `true`, nil)
fmt.Println("check boolean got is true:", ok)
ok = t.JSON(42, `42`, nil)
fmt.Println("check numeric got is 42:", ok)
got = nil
ok = t.JSON(got, `null`, nil)
fmt.Println("check nil got is null:", ok)
// Output:
// check got with age then fullname: true
// check got with fullname then age: true
// check got with nicely formatted and commented JSON: true
// check got with gender field: false
// check got with fullname only: false
// check boolean got is true: true
// check numeric got is 42: true
// check nil got is null: true
}
func ExampleT_JSON_placeholders() {
t := td.NewT(&testing.T{})
type Person struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
Children []*Person `json:"children,omitempty"`
}
got := &Person{
Fullname: "Bob Foobar",
Age: 42,
}
ok := t.JSON(got, `{"age": $1, "fullname": $2}`, []any{42, "Bob Foobar"})
fmt.Println("check got with numeric placeholders without operators:", ok)
ok = t.JSON(got, `{"age": $1, "fullname": $2}`, []any{td.Between(40, 45), td.HasSuffix("Foobar")})
fmt.Println("check got with numeric placeholders:", ok)
ok = t.JSON(got, `{"age": "$1", "fullname": "$2"}`, []any{td.Between(40, 45), td.HasSuffix("Foobar")})
fmt.Println("check got with double-quoted numeric placeholders:", ok)
ok = t.JSON(got, `{"age": $age, "fullname": $name}`, []any{td.Tag("age", td.Between(40, 45)), td.Tag("name", td.HasSuffix("Foobar"))})
fmt.Println("check got with named placeholders:", ok)
got.Children = []*Person{
{Fullname: "Alice", Age: 28},
{Fullname: "Brian", Age: 22},
}
ok = t.JSON(got, `{"age": $age, "fullname": $name, "children": $children}`, []any{td.Tag("age", td.Between(40, 45)), td.Tag("name", td.HasSuffix("Foobar")), td.Tag("children", td.Bag(
&Person{Fullname: "Brian", Age: 22},
&Person{Fullname: "Alice", Age: 28},
))})
fmt.Println("check got w/named placeholders, and children w/go structs:", ok)
ok = t.JSON(got, `{"age": Between($1, $2), "fullname": HasSuffix($suffix), "children": Len(2)}`, []any{40, 45, td.Tag("suffix", "Foobar")})
fmt.Println("check got w/num & named placeholders:", ok)
// Output:
// check got with numeric placeholders without operators: true
// check got with numeric placeholders: true
// check got with double-quoted numeric placeholders: true
// check got with named placeholders: true
// check got w/named placeholders, and children w/go structs: true
// check got w/num & named placeholders: true
}
func ExampleT_JSON_embedding() {
t := td.NewT(&testing.T{})
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
}{
Fullname: "Bob Foobar",
Age: 42,
}
ok := t.JSON(got, `{"age": NotZero(), "fullname": NotEmpty()}`, nil)
fmt.Println("check got with simple operators:", ok)
ok = t.JSON(got, `{"age": $^NotZero, "fullname": $^NotEmpty}`, nil)
fmt.Println("check got with operator shortcuts:", ok)
ok = t.JSON(got, `
{
"age": Between(40, 42, "]]"), // in ]40; 42]
"fullname": All(
HasPrefix("Bob"),
HasSuffix("bar") // ← comma is optional here
)
}`, nil)
fmt.Println("check got with complex operators:", ok)
ok = t.JSON(got, `
{
"age": Between(40, 42, "]["), // in ]40; 42[ → 42 excluded
"fullname": All(
HasPrefix("Bob"),
HasSuffix("bar"),
)
}`, nil)
fmt.Println("check got with complex operators:", ok)
ok = t.JSON(got, `
{
"age": Between($1, $2, $3), // in ]40; 42]
"fullname": All(
HasPrefix($4),
HasSuffix("bar") // ← comma is optional here
)
}`, []any{40, 42, td.BoundsOutIn, "Bob"})
fmt.Println("check got with complex operators, w/placeholder args:", ok)
// Output:
// check got with simple operators: true
// check got with operator shortcuts: true
// check got with complex operators: true
// check got with complex operators: false
// check got with complex operators, w/placeholder args: true
}
func ExampleT_JSON_rawStrings() {
t := td.NewT(&testing.T{})
type details struct {
Address string `json:"address"`
Car string `json:"car"`
}
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
Details details `json:"details"`
}{
Fullname: "Foo Bar",
Age: 42,
Details: details{
Address: "something",
Car: "Peugeot",
},
}
ok := t.JSON(got, `
{
"fullname": HasPrefix("Foo"),
"age": Between(41, 43),
"details": SuperMapOf({
"address": NotEmpty, // () are optional when no parameters
"car": Any("Peugeot", "Tesla", "Jeep") // any of these
})
}`, nil)
fmt.Println("Original:", ok)
ok = t.JSON(got, `
{
"fullname": "$^HasPrefix(\"Foo\")",
"age": "$^Between(41, 43)",
"details": "$^SuperMapOf({\n\"address\": NotEmpty,\n\"car\": Any(\"Peugeot\", \"Tesla\", \"Jeep\")\n})"
}`, nil)
fmt.Println("JSON compliant:", ok)
ok = t.JSON(got, `
{
"fullname": "$^HasPrefix(\"Foo\")",
"age": "$^Between(41, 43)",
"details": "$^SuperMapOf({
\"address\": NotEmpty, // () are optional when no parameters
\"car\": Any(\"Peugeot\", \"Tesla\", \"Jeep\") // any of these
})"
}`, nil)
fmt.Println("JSON multilines strings:", ok)
ok = t.JSON(got, `
{
"fullname": "$^HasPrefix(r)",
"age": "$^Between(41, 43)",
"details": "$^SuperMapOf({
r: NotEmpty, // () are optional when no parameters
r: Any(r, r, r) // any of these
})"
}`, nil)
fmt.Println("Raw strings:", ok)
// Output:
// Original: true
// JSON compliant: true
// JSON multilines strings: true
// Raw strings: true
}
func ExampleT_JSON_file() {
t := td.NewT(&testing.T{})
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
Gender string `json:"gender"`
}{
Fullname: "Bob Foobar",
Age: 42,
Gender: "male",
}
tmpDir, err := os.MkdirTemp("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir) //nolint: errcheck // clean up
filename := tmpDir + "/test.json"
if err = os.WriteFile(filename, []byte(`
{
"fullname": "$name",
"age": "$age",
"gender": "$gender"
}`), 0644); err != nil {
t.Fatal(err)
}
// OK let's test with this file
ok := t.JSON(got, filename, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))})
fmt.Println("Full match from file name:", ok)
// When the file is already open
file, err := os.Open(filename)
if err != nil {
t.Fatal(err)
}
ok = t.JSON(got, file, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))})
fmt.Println("Full match from io.Reader:", ok)
// Output:
// Full match from file name: true
// Full match from io.Reader: true
}
func ExampleT_JSONPointer_rfc6901() {
t := td.NewT(&testing.T{})
got := json.RawMessage(`
{
"foo": ["bar", "baz"],
"": 0,
"a/b": 1,
"c%d": 2,
"e^f": 3,
"g|h": 4,
"i\\j": 5,
"k\"l": 6,
" ": 7,
"m~n": 8
}`)
expected := map[string]any{
"foo": []any{"bar", "baz"},
"": 0,
"a/b": 1,
"c%d": 2,
"e^f": 3,
"g|h": 4,
`i\j`: 5,
`k"l`: 6,
" ": 7,
"m~n": 8,
}
ok := t.JSONPointer(got, "", expected)
fmt.Println("Empty JSON pointer means all:", ok)
ok = t.JSONPointer(got, `/foo`, []any{"bar", "baz"})
fmt.Println("Extract `foo` key:", ok)
ok = t.JSONPointer(got, `/foo/0`, "bar")
fmt.Println("First item of `foo` key slice:", ok)
ok = t.JSONPointer(got, `/`, 0)
fmt.Println("Empty key:", ok)
ok = t.JSONPointer(got, `/a~1b`, 1)
fmt.Println("Slash has to be escaped using `~1`:", ok)
ok = t.JSONPointer(got, `/c%d`, 2)
fmt.Println("% in key:", ok)
ok = t.JSONPointer(got, `/e^f`, 3)
fmt.Println("^ in key:", ok)
ok = t.JSONPointer(got, `/g|h`, 4)
fmt.Println("| in key:", ok)
ok = t.JSONPointer(got, `/i\j`, 5)
fmt.Println("Backslash in key:", ok)
ok = t.JSONPointer(got, `/k"l`, 6)
fmt.Println("Double-quote in key:", ok)
ok = t.JSONPointer(got, `/ `, 7)
fmt.Println("Space key:", ok)
ok = t.JSONPointer(got, `/m~0n`, 8)
fmt.Println("Tilde has to be escaped using `~0`:", ok)
// Output:
// Empty JSON pointer means all: true
// Extract `foo` key: true
// First item of `foo` key slice: true
// Empty key: true
// Slash has to be escaped using `~1`: true
// % in key: true
// ^ in key: true
// | in key: true
// Backslash in key: true
// Double-quote in key: true
// Space key: true
// Tilde has to be escaped using `~0`: true
}
func ExampleT_JSONPointer_struct() {
t := td.NewT(&testing.T{})
// Without json tags, encoding/json uses public fields name
type Item struct {
Name string
Value int64
Next *Item
}
got := Item{
Name: "first",
Value: 1,
Next: &Item{
Name: "second",
Value: 2,
Next: &Item{
Name: "third",
Value: 3,
},
},
}
ok := t.JSONPointer(got, "/Next/Next/Name", "third")
fmt.Println("3rd item name is `third`:", ok)
ok = t.JSONPointer(got, "/Next/Next/Value", td.Gte(int64(3)))
fmt.Println("3rd item value is greater or equal than 3:", ok)
ok = t.JSONPointer(got, "/Next", td.JSONPointer("/Next",
td.JSONPointer("/Value", td.Gte(int64(3)))))
fmt.Println("3rd item value is still greater or equal than 3:", ok)
ok = t.JSONPointer(got, "/Next/Next/Next/Name", td.Ignore())
fmt.Println("4th item exists and has a name:", ok)
// Struct comparison work with or without pointer: &Item{…} works too
ok = t.JSONPointer(got, "/Next/Next", Item{
Name: "third",
Value: 3,
})
fmt.Println("3rd item full comparison:", ok)
// Output:
// 3rd item name is `third`: true
// 3rd item value is greater or equal than 3: true
// 3rd item value is still greater or equal than 3: true
// 4th item exists and has a name: false
// 3rd item full comparison: true
}
func ExampleT_JSONPointer_has_hasnt() {
t := td.NewT(&testing.T{})
got := json.RawMessage(`
{
"name": "Bob",
"age": 42,
"children": [
{
"name": "Alice",
"age": 16
},
{
"name": "Britt",
"age": 21,
"children": [
{
"name": "John",
"age": 1
}
]
}
]
}`)
// Has Bob some children?
ok := t.JSONPointer(got, "/children", td.Len(td.Gt(0)))
fmt.Println("Bob has at least one child:", ok)
// But checking "children" exists is enough here
ok = t.JSONPointer(got, "/children/0/children", td.Ignore())
fmt.Println("Alice has children:", ok)
ok = t.JSONPointer(got, "/children/1/children", td.Ignore())
fmt.Println("Britt has children:", ok)
// The reverse can be checked too
ok = t.Cmp(got, td.Not(td.JSONPointer("/children/0/children", td.Ignore())))
fmt.Println("Alice hasn't children:", ok)
ok = t.Cmp(got, td.Not(td.JSONPointer("/children/1/children", td.Ignore())))
fmt.Println("Britt hasn't children:", ok)
// Output:
// Bob has at least one child: true
// Alice has children: false
// Britt has children: true
// Alice hasn't children: true
// Britt hasn't children: false
}
func ExampleT_Keys() {
t := td.NewT(&testing.T{})
got := map[string]int{"foo": 1, "bar": 2, "zip": 3}
// Keys tests keys in an ordered manner
ok := t.Keys(got, []string{"bar", "foo", "zip"})
fmt.Println("All sorted keys are found:", ok)
// If the expected keys are not ordered, it fails
ok = t.Keys(got, []string{"zip", "bar", "foo"})
fmt.Println("All unsorted keys are found:", ok)
// To circumvent that, one can use Bag operator
ok = t.Keys(got, td.Bag("zip", "bar", "foo"))
fmt.Println("All unsorted keys are found, with the help of Bag operator:", ok)
// Check that each key is 3 bytes long
ok = t.Keys(got, td.ArrayEach(td.Len(3)))
fmt.Println("Each key is 3 bytes long:", ok)
// Output:
// All sorted keys are found: true
// All unsorted keys are found: false
// All unsorted keys are found, with the help of Bag operator: true
// 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{})
gotInt64 := int64(1234)
gotInt32 := int32(1235)
type myInt uint16
gotMyInt := myInt(1236)
expected := td.Between(1230, 1240) // int type here
ok := t.CmpLax(gotInt64, expected)
fmt.Println("int64 got between ints [1230 .. 1240]:", ok)
ok = t.CmpLax(gotInt32, expected)
fmt.Println("int32 got between ints [1230 .. 1240]:", ok)
ok = t.CmpLax(gotMyInt, expected)
fmt.Println("myInt got between ints [1230 .. 1240]:", ok)
// Output:
// int64 got between ints [1230 .. 1240]: true
// int32 got between ints [1230 .. 1240]: true
// myInt got between ints [1230 .. 1240]: true
}
func ExampleT_Len_slice() {
t := td.NewT(&testing.T{})
got := []int{11, 22, 33}
ok := t.Len(got, 3, "checks %v len is 3", got)
fmt.Println(ok)
ok = t.Len(got, 0, "checks %v len is 0", got)
fmt.Println(ok)
got = nil
ok = t.Len(got, 0, "checks %v len is 0", got)
fmt.Println(ok)
// Output:
// true
// false
// true
}
func ExampleT_Len_map() {
t := td.NewT(&testing.T{})
got := map[int]bool{11: true, 22: false, 33: false}
ok := t.Len(got, 3, "checks %v len is 3", got)
fmt.Println(ok)
ok = t.Len(got, 0, "checks %v len is 0", got)
fmt.Println(ok)
got = nil
ok = t.Len(got, 0, "checks %v len is 0", got)
fmt.Println(ok)
// Output:
// true
// false
// true
}
func ExampleT_Len_operatorSlice() {
t := td.NewT(&testing.T{})
got := []int{11, 22, 33}
ok := t.Len(got, td.Between(3, 8),
"checks %v len is in [3 .. 8]", got)
fmt.Println(ok)
ok = t.Len(got, td.Lt(5), "checks %v len is < 5", got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleT_Len_operatorMap() {
t := td.NewT(&testing.T{})
got := map[int]bool{11: true, 22: false, 33: false}
ok := t.Len(got, td.Between(3, 8),
"checks %v len is in [3 .. 8]", got)
fmt.Println(ok)
ok = t.Len(got, td.Gte(3), "checks %v len is ≥ 3", got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleT_List() {
t := td.NewT(&testing.T{})
got := []int{1, 33, 8, 2}
// Matches as all items are present
ok := t.List(got, []any{1, td.Between(32, 34), td.Gt(7), 2})
fmt.Println("checks all items match, in this order:", ok)
// Does not match as got does not use the same order as expected
ok = t.List(got, []any{1, td.Gt(7), 2, td.Between(32, 34)})
fmt.Println("checks all items match, in wrong order:", ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using expected... without copying it to a new
// []any slice, then use td.Flatten!
expected := []int{1, 33, 8}
ok = t.List(got, []any{td.Flatten(expected), td.Lte(2)})
fmt.Println("checks all expected items are present + last one ≤ 2:", ok)
// Output:
// checks all items match, in this order: true
// checks all items match, in wrong order: false
// checks all expected items are present + last one ≤ 2: true
}
func ExampleT_Lt_int() {
t := td.NewT(&testing.T{})
got := 156
ok := t.Lt(got, 157, "checks %v is < 157", got)
fmt.Println(ok)
ok = t.Lt(got, 156, "checks %v is < 156", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleT_Lt_string() {
t := td.NewT(&testing.T{})
got := "abc"
ok := t.Lt(got, "abd", `checks "%v" is < "abd"`, got)
fmt.Println(ok)
ok = t.Lt(got, "abc", `checks "%v" is < "abc"`, got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleT_Lte_int() {
t := td.NewT(&testing.T{})
got := 156
ok := t.Lte(got, 156, "checks %v is ≤ 156", got)
fmt.Println(ok)
ok = t.Lte(got, 157, "checks %v is ≤ 157", got)
fmt.Println(ok)
ok = t.Lte(got, 155, "checks %v is ≤ 155", got)
fmt.Println(ok)
// Output:
// true
// true
// false
}
func ExampleT_Lte_string() {
t := td.NewT(&testing.T{})
got := "abc"
ok := t.Lte(got, "abc", `checks "%v" is ≤ "abc"`, got)
fmt.Println(ok)
ok = t.Lte(got, "abd", `checks "%v" is ≤ "abd"`, got)
fmt.Println(ok)
ok = t.Lte(got, "abb", `checks "%v" is ≤ "abb"`, got)
fmt.Println(ok)
// Output:
// true
// true
// false
}
func ExampleT_Map_map() {
t := td.NewT(&testing.T{})
got := map[string]int{"foo": 12, "bar": 42, "zip": 89}
ok := t.Map(got, map[string]int{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": td.Ignore()},
"checks map %v", got)
fmt.Println(ok)
ok = t.Map(got, map[string]int{}, td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()},
"checks map %v", got)
fmt.Println(ok)
ok = t.Map(got, (map[string]int)(nil), td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()},
"checks map %v", got)
fmt.Println(ok)
// Output:
// true
// true
// true
}
func ExampleT_Map_typedMap() {
t := td.NewT(&testing.T{})
type MyMap map[string]int
got := MyMap{"foo": 12, "bar": 42, "zip": 89}
ok := t.Map(got, MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": td.Ignore()},
"checks typed map %v", got)
fmt.Println(ok)
ok = t.Map(&got, &MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": td.Ignore()},
"checks pointer on typed map %v", got)
fmt.Println(ok)
ok = t.Map(&got, &MyMap{}, td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()},
"checks pointer on typed map %v", got)
fmt.Println(ok)
ok = t.Map(&got, (*MyMap)(nil), td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()},
"checks pointer on typed map %v", got)
fmt.Println(ok)
// Output:
// true
// true
// true
// true
}
func ExampleT_MapEach_map() {
t := td.NewT(&testing.T{})
got := map[string]int{"foo": 12, "bar": 42, "zip": 89}
ok := t.MapEach(got, td.Between(10, 90),
"checks each value of map %v is in [10 .. 90]", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleT_MapEach_typedMap() {
t := td.NewT(&testing.T{})
type MyMap map[string]int
got := MyMap{"foo": 12, "bar": 42, "zip": 89}
ok := t.MapEach(got, td.Between(10, 90),
"checks each value of typed map %v is in [10 .. 90]", got)
fmt.Println(ok)
ok = t.MapEach(&got, td.Between(10, 90),
"checks each value of typed map pointer %v is in [10 .. 90]", got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleT_N() {
t := td.NewT(&testing.T{})
got := 1.12345
ok := t.N(got, 1.1234, 0.00006,
"checks %v = 1.1234 ± 0.00006", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleT_NaN_float32() {
t := td.NewT(&testing.T{})
got := float32(math.NaN())
ok := t.NaN(got,
"checks %v is not-a-number", got)
fmt.Println("float32(math.NaN()) is float32 not-a-number:", ok)
got = 12
ok = t.NaN(got,
"checks %v is not-a-number", got)
fmt.Println("float32(12) is float32 not-a-number:", ok)
// Output:
// float32(math.NaN()) is float32 not-a-number: true
// float32(12) is float32 not-a-number: false
}
func ExampleT_NaN_float64() {
t := td.NewT(&testing.T{})
got := math.NaN()
ok := t.NaN(got,
"checks %v is not-a-number", got)
fmt.Println("math.NaN() is not-a-number:", ok)
got = 12
ok = t.NaN(got,
"checks %v is not-a-number", got)
fmt.Println("float64(12) is not-a-number:", ok)
// Output:
// math.NaN() is not-a-number: true
// float64(12) is not-a-number: false
}
func ExampleT_Nil() {
t := td.NewT(&testing.T{})
var got fmt.Stringer // interface
// nil value can be compared directly with nil, no need of Nil() here
ok := t.Cmp(got, nil)
fmt.Println(ok)
// But it works with Nil() anyway
ok = t.Nil(got)
fmt.Println(ok)
got = (*bytes.Buffer)(nil)
// In the case of an interface containing a nil pointer, comparing
// with nil fails, as the interface is not nil
ok = t.Cmp(got, nil)
fmt.Println(ok)
// In this case Nil() succeed
ok = t.Nil(got)
fmt.Println(ok)
// Output:
// true
// true
// false
// true
}
func ExampleT_None() {
t := td.NewT(&testing.T{})
got := 18
ok := t.None(got, []any{0, 10, 20, 30, td.Between(100, 199)},
"checks %v is non-null, and ≠ 10, 20 & 30, and not in [100-199]", got)
fmt.Println(ok)
got = 20
ok = t.None(got, []any{0, 10, 20, 30, td.Between(100, 199)},
"checks %v is non-null, and ≠ 10, 20 & 30, and not in [100-199]", got)
fmt.Println(ok)
got = 142
ok = t.None(got, []any{0, 10, 20, 30, td.Between(100, 199)},
"checks %v is non-null, and ≠ 10, 20 & 30, and not in [100-199]", got)
fmt.Println(ok)
prime := td.Flatten([]int{1, 2, 3, 5, 7, 11, 13})
even := td.Flatten([]int{2, 4, 6, 8, 10, 12, 14})
for _, got := range [...]int{9, 3, 8, 15} {
ok = t.None(got, []any{prime, even, td.Gt(14)},
"checks %v is not prime number, nor an even number and not > 14")
fmt.Printf("%d → %t\n", got, ok)
}
// Output:
// true
// false
// false
// 9 → true
// 3 → false
// 8 → false
// 15 → false
}
func ExampleT_Not() {
t := td.NewT(&testing.T{})
got := 42
ok := t.Not(got, 0, "checks %v is non-null", got)
fmt.Println(ok)
ok = t.Not(got, td.Between(10, 30),
"checks %v is not in [10 .. 30]", got)
fmt.Println(ok)
got = 0
ok = t.Not(got, 0, "checks %v is non-null", got)
fmt.Println(ok)
// Output:
// true
// true
// false
}
func ExampleT_NotAny() {
t := td.NewT(&testing.T{})
got := []int{4, 5, 9, 42}
ok := t.NotAny(got, []any{3, 6, 8, 41, 43},
"checks %v contains no item listed in NotAny()", got)
fmt.Println(ok)
ok = t.NotAny(got, []any{3, 6, 8, 42, 43},
"checks %v contains no item listed in NotAny()", got)
fmt.Println(ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using notExpected... without copying it to a new
// []any slice, then use td.Flatten!
notExpected := []int{3, 6, 8, 41, 43}
ok = t.NotAny(got, []any{td.Flatten(notExpected)},
"checks %v contains no item listed in notExpected", got)
fmt.Println(ok)
// Output:
// true
// false
// true
}
func ExampleT_NotEmpty() {
t := td.NewT(&testing.T{})
ok := t.NotEmpty(nil) // fails, as nil is considered empty
fmt.Println(ok)
ok = t.NotEmpty("foobar")
fmt.Println(ok)
// Fails as 0 is a number, so not empty. Use NotZero() instead
ok = t.NotEmpty(0)
fmt.Println(ok)
ok = t.NotEmpty(map[string]int{"foobar": 42})
fmt.Println(ok)
ok = t.NotEmpty([]int{1})
fmt.Println(ok)
ok = t.NotEmpty([3]int{}) // succeeds, NotEmpty() is not NotZero()!
fmt.Println(ok)
// Output:
// false
// true
// false
// true
// true
// true
}
func ExampleT_NotEmpty_pointers() {
t := td.NewT(&testing.T{})
type MySlice []int
ok := t.NotEmpty(MySlice{12})
fmt.Println(ok)
ok = t.NotEmpty(&MySlice{12}) // Ptr() not needed
fmt.Println(ok)
l1 := &MySlice{12}
l2 := &l1
l3 := &l2
ok = t.NotEmpty(&l3)
fmt.Println(ok)
// Works the same for array, map, channel and string
// But not for others types as:
type MyStruct struct {
Value int
}
ok = t.NotEmpty(&MyStruct{}) // fails, use NotZero() instead
fmt.Println(ok)
// Output:
// true
// true
// true
// false
}
func ExampleT_NotNaN_float32() {
t := td.NewT(&testing.T{})
got := float32(math.NaN())
ok := t.NotNaN(got,
"checks %v is not-a-number", got)
fmt.Println("float32(math.NaN()) is NOT float32 not-a-number:", ok)
got = 12
ok = t.NotNaN(got,
"checks %v is not-a-number", got)
fmt.Println("float32(12) is NOT float32 not-a-number:", ok)
// Output:
// float32(math.NaN()) is NOT float32 not-a-number: false
// float32(12) is NOT float32 not-a-number: true
}
func ExampleT_NotNaN_float64() {
t := td.NewT(&testing.T{})
got := math.NaN()
ok := t.NotNaN(got,
"checks %v is NOT not-a-number", got)
fmt.Println("math.NaN() is NOT not-a-number:", ok)
got = 12
ok = t.NotNaN(got,
"checks %v is NOT not-a-number", got)
fmt.Println("float64(12) is NOT not-a-number:", ok)
// Output:
// math.NaN() is NOT not-a-number: false
// float64(12) is NOT not-a-number: true
}
func ExampleT_NotNil() {
t := td.NewT(&testing.T{})
var got fmt.Stringer = &bytes.Buffer{}
// nil value can be compared directly with Not(nil), no need of NotNil() here
ok := t.Cmp(got, td.Not(nil))
fmt.Println(ok)
// But it works with NotNil() anyway
ok = t.NotNil(got)
fmt.Println(ok)
got = (*bytes.Buffer)(nil)
// In the case of an interface containing a nil pointer, comparing
// with Not(nil) succeeds, as the interface is not nil
ok = t.Cmp(got, td.Not(nil))
fmt.Println(ok)
// In this case NotNil() fails
ok = t.NotNil(got)
fmt.Println(ok)
// Output:
// true
// true
// true
// false
}
func ExampleT_NotZero() {
t := td.NewT(&testing.T{})
ok := t.NotZero(0) // fails
fmt.Println(ok)
ok = t.NotZero(float64(0)) // fails
fmt.Println(ok)
ok = t.NotZero(12)
fmt.Println(ok)
ok = t.NotZero((map[string]int)(nil)) // fails, as nil
fmt.Println(ok)
ok = t.NotZero(map[string]int{}) // succeeds, as not nil
fmt.Println(ok)
ok = t.NotZero(([]int)(nil)) // fails, as nil
fmt.Println(ok)
ok = t.NotZero([]int{}) // succeeds, as not nil
fmt.Println(ok)
ok = t.NotZero([3]int{}) // fails
fmt.Println(ok)
ok = t.NotZero([3]int{0, 1}) // succeeds, DATA[1] is not 0
fmt.Println(ok)
ok = t.NotZero(bytes.Buffer{}) // fails
fmt.Println(ok)
ok = t.NotZero(&bytes.Buffer{}) // succeeds, as pointer not nil
fmt.Println(ok)
ok = t.Cmp(&bytes.Buffer{}, td.Ptr(td.NotZero())) // fails as deref by Ptr()
fmt.Println(ok)
// Output:
// false
// false
// true
// false
// true
// false
// true
// false
// true
// false
// true
// false
}
func ExampleT_PPtr() {
t := td.NewT(&testing.T{})
num := 12
got := &num
ok := t.PPtr(&got, 12)
fmt.Println(ok)
ok = t.PPtr(&got, td.Between(4, 15))
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleT_Ptr() {
t := td.NewT(&testing.T{})
got := 12
ok := t.Ptr(&got, 12)
fmt.Println(ok)
ok = t.Ptr(&got, td.Between(4, 15))
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleT_Re() {
t := td.NewT(&testing.T{})
got := "foo bar"
ok := t.Re(got, "(zip|bar)$", nil, "checks value %s", got)
fmt.Println(ok)
got = "bar foo"
ok = t.Re(got, "(zip|bar)$", nil, "checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleT_Re_stringer() {
t := td.NewT(&testing.T{})
// bytes.Buffer implements fmt.Stringer
got := bytes.NewBufferString("foo bar")
ok := t.Re(got, "(zip|bar)$", nil, "checks value %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleT_Re_error() {
t := td.NewT(&testing.T{})
got := errors.New("foo bar")
ok := t.Re(got, "(zip|bar)$", nil, "checks value %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleT_Re_capture() {
t := td.NewT(&testing.T{})
got := "foo bar biz"
ok := t.Re(got, `^(\w+) (\w+) (\w+)$`, td.Set("biz", "foo", "bar"),
"checks value %s", got)
fmt.Println(ok)
got = "foo bar! biz"
ok = t.Re(got, `^(\w+) (\w+) (\w+)$`, td.Set("biz", "foo", "bar"),
"checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleT_Re_multilines() {
t := td.NewT(&testing.T{})
got := `multi
lines
probably
more
than 4
`
expectedRe := `^multi
lines?
(probably|possibly)
more
than \d+
\z`
ok := t.Re(got, expectedRe, nil)
fmt.Println("Raw multi-lines string matches:", ok)
// But for strings with many, many, many lines, when the regexp
// doesn't match, it is sometimes difficult to see where the regexp
// failed in the string. Here td.List & td.Flatten can help to apply
// regexp line per line (note expectedRe is not anchored anymore):
expectedRe = `multi
lines?
(probably|possibly)
more
than \d+
`
ok = t.Cmp(
strings.Split(got, "\n"),
td.List(td.Flatten(strings.Split(expectedRe, "\n"),
func(line string) any {
return td.Re(`^` + line + `\z`)
})))
fmt.Println("All string lines match:", ok)
// Output:
// Raw multi-lines string matches: true
// All string lines match: true
}
func ExampleT_Re_compiled() {
t := td.NewT(&testing.T{})
expected := regexp.MustCompile("(zip|bar)$")
got := "foo bar"
ok := t.Re(got, expected, nil, "checks value %s", got)
fmt.Println(ok)
got = "bar foo"
ok = t.Re(got, expected, nil, "checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleT_Re_compiledStringer() {
t := td.NewT(&testing.T{})
expected := regexp.MustCompile("(zip|bar)$")
// bytes.Buffer implements fmt.Stringer
got := bytes.NewBufferString("foo bar")
ok := t.Re(got, expected, nil, "checks value %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleT_Re_compiledError() {
t := td.NewT(&testing.T{})
expected := regexp.MustCompile("(zip|bar)$")
got := errors.New("foo bar")
ok := t.Re(got, expected, nil, "checks value %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleT_Re_compiledCapture() {
t := td.NewT(&testing.T{})
expected := regexp.MustCompile(`^(\w+) (\w+) (\w+)$`)
got := "foo bar biz"
ok := t.Re(got, expected, td.Set("biz", "foo", "bar"),
"checks value %s", got)
fmt.Println(ok)
got = "foo bar! biz"
ok = t.Re(got, expected, td.Set("biz", "foo", "bar"),
"checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleT_ReAll_capture() {
t := td.NewT(&testing.T{})
got := "foo bar biz"
ok := t.ReAll(got, `(\w+)`, td.Set("biz", "foo", "bar"),
"checks value %s", got)
fmt.Println(ok)
// Matches, but all catured groups do not match Set
got = "foo BAR biz"
ok = t.ReAll(got, `(\w+)`, td.Set("biz", "foo", "bar"),
"checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleT_ReAll_captureComplex() {
t := td.NewT(&testing.T{})
got := "11 45 23 56 85 96"
ok := t.ReAll(got, `(\d+)`, td.ArrayEach(td.Code(func(num string) bool {
n, err := strconv.Atoi(num)
return err == nil && n > 10 && n < 100
})),
"checks value %s", got)
fmt.Println(ok)
// Matches, but 11 is not greater than 20
ok = t.ReAll(got, `(\d+)`, td.ArrayEach(td.Code(func(num string) bool {
n, err := strconv.Atoi(num)
return err == nil && n > 20 && n < 100
})),
"checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleT_ReAll_compiledCapture() {
t := td.NewT(&testing.T{})
expected := regexp.MustCompile(`(\w+)`)
got := "foo bar biz"
ok := t.ReAll(got, expected, td.Set("biz", "foo", "bar"),
"checks value %s", got)
fmt.Println(ok)
// Matches, but all catured groups do not match Set
got = "foo BAR biz"
ok = t.ReAll(got, expected, td.Set("biz", "foo", "bar"),
"checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleT_ReAll_compiledCaptureComplex() {
t := td.NewT(&testing.T{})
expected := regexp.MustCompile(`(\d+)`)
got := "11 45 23 56 85 96"
ok := t.ReAll(got, expected, td.ArrayEach(td.Code(func(num string) bool {
n, err := strconv.Atoi(num)
return err == nil && n > 10 && n < 100
})),
"checks value %s", got)
fmt.Println(ok)
// Matches, but 11 is not greater than 20
ok = t.ReAll(got, expected, td.ArrayEach(td.Code(func(num string) bool {
n, err := strconv.Atoi(num)
return err == nil && n > 20 && n < 100
})),
"checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleT_Recv_basic() {
t := td.NewT(&testing.T{})
got := make(chan int, 3)
ok := t.Recv(got, td.RecvNothing, 0)
fmt.Println("nothing to receive:", ok)
got <- 1
got <- 2
got <- 3
close(got)
ok = t.Recv(got, 1, 0)
fmt.Println("1st receive is 1:", ok)
ok = t.Cmp(got, td.All(
td.Recv(2),
td.Recv(td.Between(3, 4)),
td.Recv(td.RecvClosed),
))
fmt.Println("next receives are 2, 3 then closed:", ok)
ok = t.Recv(got, td.RecvNothing, 0)
fmt.Println("nothing to receive:", ok)
// Output:
// nothing to receive: true
// 1st receive is 1: true
// next receives are 2, 3 then closed: true
// nothing to receive: false
}
func ExampleT_Recv_channelPointer() {
t := td.NewT(&testing.T{})
got := make(chan int, 3)
ok := t.Recv(got, td.RecvNothing, 0)
fmt.Println("nothing to receive:", ok)
got <- 1
got <- 2
got <- 3
close(got)
ok = t.Recv(&got, 1, 0)
fmt.Println("1st receive is 1:", ok)
ok = t.Cmp(&got, td.All(
td.Recv(2),
td.Recv(td.Between(3, 4)),
td.Recv(td.RecvClosed),
))
fmt.Println("next receives are 2, 3 then closed:", ok)
ok = t.Recv(got, td.RecvNothing, 0)
fmt.Println("nothing to receive:", ok)
// Output:
// nothing to receive: true
// 1st receive is 1: true
// next receives are 2, 3 then closed: true
// nothing to receive: false
}
func ExampleT_Recv_withTimeout() {
t := td.NewT(&testing.T{})
got := make(chan int, 1)
tick := make(chan struct{})
go func() {
// ①
<-tick
time.Sleep(100 * time.Millisecond)
got <- 0
// ②
<-tick
time.Sleep(100 * time.Millisecond)
got <- 1
// ③
<-tick
time.Sleep(100 * time.Millisecond)
close(got)
}()
t.Recv(got, td.RecvNothing, 0)
// ①
tick <- struct{}{}
ok := t.Recv(got, td.RecvNothing, 0)
fmt.Println("① RecvNothing:", ok)
ok = t.Recv(got, 0, 150*time.Millisecond)
fmt.Println("① receive 0 w/150ms timeout:", ok)
ok = t.Recv(got, td.RecvNothing, 0)
fmt.Println("① RecvNothing:", ok)
// ②
tick <- struct{}{}
ok = t.Recv(got, td.RecvNothing, 0)
fmt.Println("② RecvNothing:", ok)
ok = t.Recv(got, 1, 150*time.Millisecond)
fmt.Println("② receive 1 w/150ms timeout:", ok)
ok = t.Recv(got, td.RecvNothing, 0)
fmt.Println("② RecvNothing:", ok)
// ③
tick <- struct{}{}
ok = t.Recv(got, td.RecvNothing, 0)
fmt.Println("③ RecvNothing:", ok)
ok = t.Recv(got, td.RecvClosed, 150*time.Millisecond)
fmt.Println("③ check closed w/150ms timeout:", ok)
// Output:
// ① RecvNothing: true
// ① receive 0 w/150ms timeout: true
// ① RecvNothing: true
// ② RecvNothing: true
// ② receive 1 w/150ms timeout: true
// ② RecvNothing: true
// ③ RecvNothing: true
// ③ check closed w/150ms timeout: true
}
func ExampleT_Recv_nilChannel() {
t := td.NewT(&testing.T{})
var ch chan int
ok := t.Recv(ch, td.RecvNothing, 0)
fmt.Println("nothing to receive from nil channel:", ok)
ok = t.Recv(ch, 42, 0)
fmt.Println("something to receive from nil channel:", ok)
ok = t.Recv(ch, td.RecvClosed, 0)
fmt.Println("is a nil channel closed:", ok)
// Output:
// nothing to receive from nil channel: true
// something to receive from nil channel: false
// is a nil channel closed: false
}
func ExampleT_Set() {
t := td.NewT(&testing.T{})
got := []int{1, 3, 5, 8, 8, 1, 2}
// Matches as all items are present, ignoring duplicates
ok := t.Set(got, []any{1, 2, 3, 5, 8},
"checks all items are present, in any order")
fmt.Println(ok)
// Duplicates are ignored in a Set
ok = t.Set(got, []any{1, 2, 2, 2, 2, 2, 3, 5, 8},
"checks all items are present, in any order")
fmt.Println(ok)
// Tries its best not to raise an error when a value can be matched
// by several Set entries
ok = t.Set(got, []any{td.Between(1, 4), 3, td.Between(2, 10)},
"checks all items are present, in any order")
fmt.Println(ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using expected... without copying it to a new
// []any slice, then use td.Flatten!
expected := []int{1, 2, 3, 5, 8}
ok = t.Set(got, []any{td.Flatten(expected)},
"checks all expected items are present, in any order")
fmt.Println(ok)
// Output:
// true
// true
// true
// true
}
func ExampleT_Shallow() {
t := td.NewT(&testing.T{})
type MyStruct struct {
Value int
}
data := MyStruct{Value: 12}
got := &data
ok := t.Shallow(got, &data,
"checks pointers only, not contents")
fmt.Println(ok)
// Same contents, but not same pointer
ok = t.Shallow(got, &MyStruct{Value: 12},
"checks pointers only, not contents")
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleT_Shallow_slice() {
t := td.NewT(&testing.T{})
back := []int{1, 2, 3, 1, 2, 3}
a := back[:3]
b := back[3:]
ok := t.Shallow(a, back)
fmt.Println("are ≠ but share the same area:", ok)
ok = t.Shallow(b, back)
fmt.Println("are = but do not point to same area:", ok)
// Output:
// are ≠ but share the same area: true
// are = but do not point to same area: false
}
func ExampleT_Shallow_string() {
t := td.NewT(&testing.T{})
back := "foobarfoobar"
a := back[:6]
b := back[6:]
ok := t.Shallow(a, back)
fmt.Println("are ≠ but share the same area:", ok)
ok = t.Shallow(b, a)
fmt.Println("are = but do not point to same area:", ok)
// Output:
// are ≠ but share the same area: true
// are = but do not point to same area: false
}
func ExampleT_Slice_slice() {
t := td.NewT(&testing.T{})
got := []int{42, 58, 26}
ok := t.Slice(got, []int{42}, td.ArrayEntries{1: 58, 2: td.Ignore()},
"checks slice %v", got)
fmt.Println(ok)
ok = t.Slice(got, []int{}, td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()},
"checks slice %v", got)
fmt.Println(ok)
ok = t.Slice(got, ([]int)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()},
"checks slice %v", got)
fmt.Println(ok)
// Output:
// true
// true
// true
}
func ExampleT_Slice_typedSlice() {
t := td.NewT(&testing.T{})
type MySlice []int
got := MySlice{42, 58, 26}
ok := t.Slice(got, MySlice{42}, td.ArrayEntries{1: 58, 2: td.Ignore()},
"checks typed slice %v", got)
fmt.Println(ok)
ok = t.Slice(&got, &MySlice{42}, td.ArrayEntries{1: 58, 2: td.Ignore()},
"checks pointer on typed slice %v", got)
fmt.Println(ok)
ok = t.Slice(&got, &MySlice{}, td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()},
"checks pointer on typed slice %v", got)
fmt.Println(ok)
ok = t.Slice(&got, (*MySlice)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()},
"checks pointer on typed slice %v", got)
fmt.Println(ok)
// Output:
// true
// true
// true
// true
}
func ExampleT_Smuggle_convert() {
t := td.NewT(&testing.T{})
got := int64(123)
ok := t.Smuggle(got, func(n int64) int { return int(n) }, 123,
"checks int64 got against an int value")
fmt.Println(ok)
ok = t.Smuggle("123", func(numStr string) (int, bool) {
n, err := strconv.Atoi(numStr)
return n, err == nil
}, td.Between(120, 130),
"checks that number in %#v is in [120 .. 130]")
fmt.Println(ok)
ok = t.Smuggle("123", func(numStr string) (int, bool, string) {
n, err := strconv.Atoi(numStr)
if err != nil {
return 0, false, "string must contain a number"
}
return n, true, ""
}, td.Between(120, 130),
"checks that number in %#v is in [120 .. 130]")
fmt.Println(ok)
ok = t.Smuggle("123", func(numStr string) (int, error) { //nolint: gocritic
return strconv.Atoi(numStr)
}, td.Between(120, 130),
"checks that number in %#v is in [120 .. 130]")
fmt.Println(ok)
// Short version :)
ok = t.Smuggle("123", strconv.Atoi, td.Between(120, 130),
"checks that number in %#v is in [120 .. 130]")
fmt.Println(ok)
// Output:
// true
// true
// true
// true
// true
}
func ExampleT_Smuggle_lax() {
t := td.NewT(&testing.T{})
// got is an int16 and Smuggle func input is an int64: it is OK
got := int(123)
ok := t.Smuggle(got, func(n int64) uint32 { return uint32(n) }, uint32(123))
fmt.Println("got int16(123) → smuggle via int64 → uint32(123):", ok)
// Output:
// got int16(123) → smuggle via int64 → uint32(123): true
}
func ExampleT_Smuggle_auto_unmarshal() {
t := td.NewT(&testing.T{})
// Automatically json.Unmarshal to compare
got := []byte(`{"a":1,"b":2}`)
ok := t.Smuggle(got, func(b json.RawMessage) (r map[string]int, err error) {
err = json.Unmarshal(b, &r)
return
}, map[string]int{
"a": 1,
"b": 2,
})
fmt.Println("JSON contents is OK:", ok)
// Output:
// JSON contents is OK: true
}
func ExampleT_Smuggle_cast() {
t := td.NewT(&testing.T{})
// A string containing JSON
got := `{ "foo": 123 }`
// Automatically cast a string to a json.RawMessage so td.JSON can operate
ok := t.Smuggle(got, json.RawMessage{}, td.JSON(`{"foo":123}`))
fmt.Println("JSON contents in string is OK:", ok)
// Automatically read from io.Reader to a json.RawMessage
ok = t.Smuggle(bytes.NewReader([]byte(got)), json.RawMessage{}, td.JSON(`{"foo":123}`))
fmt.Println("JSON contents just read is OK:", ok)
// Output:
// JSON contents in string is OK: true
// JSON contents just read is OK: true
}
func ExampleT_Smuggle_complex() {
t := td.NewT(&testing.T{})
// No end date but a start date and a duration
type StartDuration struct {
StartDate time.Time
Duration time.Duration
}
// Checks that end date is between 17th and 19th February both at 0h
// for each of these durations in hours
for _, duration := range []time.Duration{48 * time.Hour, 72 * time.Hour, 96 * time.Hour} {
got := StartDuration{
StartDate: time.Date(2018, time.February, 14, 12, 13, 14, 0, time.UTC),
Duration: duration,
}
// Simplest way, but in case of Between() failure, error will be bound
// to DATA, not very clear...
ok := t.Smuggle(got, func(sd StartDuration) time.Time {
return sd.StartDate.Add(sd.Duration)
}, td.Between(
time.Date(2018, time.February, 17, 0, 0, 0, 0, time.UTC),
time.Date(2018, time.February, 19, 0, 0, 0, 0, time.UTC)))
fmt.Println(ok)
// Name the computed value "ComputedEndDate" to render a Between() failure
// more understandable, so error will be bound to DATA.ComputedEndDate
ok = t.Smuggle(got, func(sd StartDuration) td.SmuggledGot {
return td.SmuggledGot{
Name: "ComputedEndDate",
Got: sd.StartDate.Add(sd.Duration),
}
}, td.Between(
time.Date(2018, time.February, 17, 0, 0, 0, 0, time.UTC),
time.Date(2018, time.February, 19, 0, 0, 0, 0, time.UTC)))
fmt.Println(ok)
}
// Output:
// false
// false
// true
// true
// true
// true
}
func ExampleT_Smuggle_interface() {
t := td.NewT(&testing.T{})
gotTime, err := time.Parse(time.RFC3339, "2018-05-23T12:13:14Z")
if err != nil {
t.Fatal(err)
}
// Do not check the struct itself, but its stringified form
ok := t.Smuggle(gotTime, func(s fmt.Stringer) string {
return s.String()
}, "2018-05-23 12:13:14 +0000 UTC")
fmt.Println("stringified time.Time OK:", ok)
// If got does not implement the fmt.Stringer interface, it fails
// without calling the Smuggle func
type MyTime time.Time
ok = t.Smuggle(MyTime(gotTime), func(s fmt.Stringer) string {
fmt.Println("Smuggle func called!")
return s.String()
}, "2018-05-23 12:13:14 +0000 UTC")
fmt.Println("stringified MyTime OK:", ok)
// Output:
// stringified time.Time OK: true
// stringified MyTime OK: false
}
func ExampleT_Smuggle_field_path() {
t := td.NewT(&testing.T{})
type Body struct {
Name string
Value any
}
type Request struct {
Body *Body
}
type Transaction struct {
Request
}
type ValueNum struct {
Num int
}
got := &Transaction{
Request: Request{
Body: &Body{
Name: "test",
Value: &ValueNum{Num: 123},
},
},
}
// Want to check whether Num is between 100 and 200?
ok := t.Smuggle(got, func(tr *Transaction) (int, error) {
if tr.Body == nil ||
tr.Body.Value == nil {
return 0, errors.New("Request.Body or Request.Body.Value is nil")
}
if v, ok := tr.Body.Value.(*ValueNum); ok && v != nil {
return v.Num, nil
}
return 0, errors.New("Request.Body.Value isn't *ValueNum or nil")
}, td.Between(100, 200))
fmt.Println("check Num by hand:", ok)
// Same, but automagically generated...
ok = t.Smuggle(got, "Request.Body.Value.Num", td.Between(100, 200))
fmt.Println("check Num using a fields-path:", ok)
// And as Request is an anonymous field, can be simplified further
// as it can be omitted
ok = t.Smuggle(got, "Body.Value.Num", td.Between(100, 200))
fmt.Println("check Num using an other fields-path:", ok)
// Note that maps and array/slices are supported
got.Body.Value = map[string]any{
"foo": []any{
3: map[int]string{666: "bar"},
},
}
ok = t.Smuggle(got, "Body.Value[foo][3][666]", "bar")
fmt.Println("check fields-path including maps/slices:", ok)
// Output:
// check Num by hand: true
// check Num using a fields-path: true
// check Num using an other fields-path: true
// 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}
// Generic ascending order (≥0 or nil)
ok := t.Sort(got, 1, []int{-3, -2, -1, 0, 1, 2, 3})
fmt.Println("asc order:", ok)
ok = t.Sort(got, 0, []int{-3, -2, -1, 0, 1, 2, 3})
fmt.Println("asc order:", ok)
ok = t.Sort(got, nil, []int{-3, -2, -1, 0, 1, 2, 3})
fmt.Println("asc order:", ok)
// Generic descending order (< 0)
ok = t.Sort(got, -1, []int{3, 2, 1, 0, -1, -2, -3})
fmt.Println("desc order:", ok)
evenHigher := func(a, b int) bool {
if (a%2 == 0) != (b%2 == 0) {
return a%2 != 0
}
return a < b
}
ok = t.Sort(got, evenHigher, []int{-3, -1, 1, 3, -2, 0, 2})
fmt.Println("even higher order:", ok)
// Output:
// asc order: true
// asc order: true
// asc order: true
// desc order: true
// even higher order: true
}
func ExampleT_Sort_fields_path() {
t := td.NewT(&testing.T{})
type Person struct {
Name string
Age int
}
brian := Person{Name: "Brian", Age: 22}
bob := Person{Name: "Bob", Age: 19}
stephen := Person{Name: "Stephen", Age: 19}
alice := Person{Name: "Alice", Age: 20}
marcel := Person{Name: "Marcel", Age: 25}
got := []Person{brian, bob, stephen, alice, marcel}
ok := t.Sort(got, "Name", []Person{alice, bob, brian, marcel, stephen})
fmt.Println("by name asc:", ok)
ok = t.Sort(got, "-Name", []Person{stephen, marcel, brian, bob, alice})
fmt.Println("by name desc:", ok)
ok = t.Sort(got, []string{"-Age", "Name"}, []Person{marcel, brian, alice, bob, stephen})
fmt.Println("by age desc, then by name asc:", ok)
type A struct{ props map[string]int }
p12 := A{props: map[string]int{"priority": 12}}
p23 := A{props: map[string]int{"priority": 23}}
p34 := A{props: map[string]int{"priority": 34}}
got2 := []A{p23, p12, p34}
ok = t.Sort(got2, `-props[priority]`, []A{p34, p23, p12})
fmt.Println("by priority desc:", ok)
ok = t.Sort(got2, `props.priority`, []A{p12, p23, p34})
fmt.Println("by priority asc:", ok)
// Output:
// by name asc: true
// by name desc: true
// by age desc, then by name asc: true
// by priority desc: true
// by priority asc: 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 (1):", ok)
ok = t.Sorted(got, 0)
fmt.Println("is asc order (0):", ok)
ok = t.Sorted(got, nil)
fmt.Println("is asc order (nil):", ok)
ok = t.Sorted(got, -1)
fmt.Println("is desc order:", ok)
got = []int{-3, -1, 1, 3, -2, 0, 2}
evenHigher := func(a, b int) bool {
if (a%2 == 0) != (b%2 == 0) {
return a%2 != 0
}
return a < b
}
ok = t.Sorted(got, evenHigher)
fmt.Println("is even higher order:", ok)
// Output:
// is asc order (default): true
// is asc order (1): true
// is asc order (0): true
// is asc order (nil): true
// is desc order: false
// is even higher order: true
}
func ExampleT_Sorted_fields_path() {
t := td.NewT(&testing.T{})
type Person struct {
Name string
Age int
}
alice := Person{Name: "Alice", Age: 20}
bob := Person{Name: "Bob", Age: 19}
brian := Person{Name: "Brian", Age: 22}
marcel := Person{Name: "Marcel", Age: 25}
stephen := Person{Name: "Stephen", Age: 19}
got := []Person{alice, bob, brian, marcel, stephen}
ok := t.Sorted(got, "Name")
fmt.Println("is sorted by name asc:", ok)
got = []Person{stephen, marcel, brian, bob, alice}
ok = t.Sorted(got, "-Name")
fmt.Println("is sorted by name desc:", ok)
got = []Person{marcel, brian, alice, bob, stephen}
ok = t.Sorted(got, []string{"-Age", "Name"})
fmt.Println("is sorted by age desc, then by name asc 1:", ok)
got = []Person{marcel, brian, alice, stephen, bob}
ok = t.Sorted(got, []string{"-Age", "Name"})
fmt.Println("is sorted by age desc, then by name asc 2:", ok)
type A struct{ props map[string]int }
p12 := A{props: map[string]int{"priority": 12}}
p23 := A{props: map[string]int{"priority": 23}}
p34 := A{props: map[string]int{"priority": 34}}
got2 := []A{p34, p23, p12}
ok = t.Sorted(got2, `-props[priority]`)
fmt.Println("is sorted by priority desc:", ok)
ok = t.Sorted(got2, `props.priority`)
fmt.Println("is sorted by priority asc:", ok)
// Output:
// is sorted by name asc: true
// is sorted by name desc: true
// is sorted by age desc, then by name asc 1: true
// is sorted by age desc, then by name asc 2: false
// is sorted by priority desc: true
// is sorted by priority asc: false
}
func ExampleT_SStruct() {
t := td.NewT(&testing.T{})
type Person struct {
Name string
Age int
NumChildren int
}
got := Person{
Name: "Foobar",
Age: 42,
NumChildren: 0,
}
// NumChildren is not listed in expected fields so it must be zero
ok := t.SStruct(got, Person{Name: "Foobar"}, td.StructFields{
"Age": td.Between(40, 50),
},
"checks %v is the right Person")
fmt.Println("Foobar is between 40 & 50:", ok)
// Model can be empty
got.NumChildren = 3
ok = t.SStruct(got, Person{}, td.StructFields{
"Name": "Foobar",
"Age": td.Between(40, 50),
"NumChildren": td.Not(0),
},
"checks %v is the right Person")
fmt.Println("Foobar has some children:", ok)
// Works with pointers too
ok = t.SStruct(&got, &Person{}, td.StructFields{
"Name": "Foobar",
"Age": td.Between(40, 50),
"NumChildren": td.Not(0),
},
"checks %v is the right Person")
fmt.Println("Foobar has some children (using pointer):", ok)
// Model does not need to be instanciated
ok = t.SStruct(&got, (*Person)(nil), td.StructFields{
"Name": "Foobar",
"Age": td.Between(40, 50),
"NumChildren": td.Not(0),
},
"checks %v is the right Person")
fmt.Println("Foobar has some children (using nil model):", ok)
// Output:
// Foobar is between 40 & 50: true
// Foobar has some children: true
// Foobar has some children (using pointer): true
// Foobar has some children (using nil model): true
}
func ExampleT_SStruct_overwrite_model() {
t := td.NewT(&testing.T{})
type Person struct {
Name string
Age int
NumChildren int
}
got := Person{
Name: "Foobar",
Age: 42,
NumChildren: 3,
}
ok := t.SStruct(got, Person{
Name: "Foobar",
Age: 53,
}, td.StructFields{
">Age": td.Between(40, 50), // ">" to overwrite Age:53 in model
"NumChildren": td.Gt(2),
},
"checks %v is the right Person")
fmt.Println("Foobar is between 40 & 50:", ok)
ok = t.SStruct(got, Person{
Name: "Foobar",
Age: 53,
}, td.StructFields{
"> Age": td.Between(40, 50), // same, ">" can be followed by spaces
"NumChildren": td.Gt(2),
},
"checks %v is the right Person")
fmt.Println("Foobar is between 40 & 50:", ok)
// Output:
// Foobar is between 40 & 50: true
// Foobar is between 40 & 50: true
}
func ExampleT_SStruct_patterns() {
t := td.NewT(&testing.T{})
type Person struct {
Firstname string
Lastname string
Surname string
Nickname string
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
id int64
secret string
}
now := time.Now()
got := Person{
Firstname: "Maxime",
Lastname: "Foo",
Surname: "Max",
Nickname: "max",
CreatedAt: now,
UpdatedAt: now,
DeletedAt: nil, // not deleted yet
id: 2345,
secret: "5ecr3T",
}
ok := t.SStruct(got, Person{Lastname: "Foo"}, td.StructFields{
`DeletedAt`: nil,
`= *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model
`=~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt
`! [A-Z]*`: td.Ignore(), // private fields
},
"mix shell & regexp patterns")
fmt.Println("Patterns match only remaining fields:", ok)
ok = t.SStruct(got, Person{Lastname: "Foo"}, td.StructFields{
`DeletedAt`: nil,
`1 = *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model
`2 =~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt
`3 !~ ^[A-Z]`: td.Ignore(), // private fields
},
"ordered patterns")
fmt.Println("Ordered patterns match only remaining fields:", ok)
// Output:
// Patterns match only remaining fields: true
// Ordered patterns match only remaining fields: true
}
func ExampleT_SStruct_lazy_model() {
t := td.NewT(&testing.T{})
got := struct {
name string
age int
}{
name: "Foobar",
age: 42,
}
ok := t.SStruct(got, nil, td.StructFields{
"name": "Foobar",
"age": td.Between(40, 45),
})
fmt.Println("Lazy model:", ok)
ok = t.SStruct(got, nil, td.StructFields{
"name": "Foobar",
"zip": 666,
})
fmt.Println("Lazy model with unknown field:", ok)
// Output:
// Lazy model: true
// Lazy model with unknown field: false
}
func ExampleT_String() {
t := td.NewT(&testing.T{})
got := "foobar"
ok := t.String(got, "foobar", "checks %s", got)
fmt.Println("using string:", ok)
ok = t.Cmp([]byte(got), td.String("foobar"), "checks %s", got)
fmt.Println("using []byte:", ok)
// Output:
// using string: true
// using []byte: true
}
func ExampleT_String_stringer() {
t := td.NewT(&testing.T{})
// bytes.Buffer implements fmt.Stringer
got := bytes.NewBufferString("foobar")
ok := t.String(got, "foobar", "checks %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleT_String_error() {
t := td.NewT(&testing.T{})
got := errors.New("foobar")
ok := t.String(got, "foobar", "checks %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleT_Struct() {
t := td.NewT(&testing.T{})
type Person struct {
Name string
Age int
NumChildren int
}
got := Person{
Name: "Foobar",
Age: 42,
NumChildren: 3,
}
// As NumChildren is zero in Struct() call, it is not checked
ok := t.Struct(got, Person{Name: "Foobar"}, td.StructFields{
"Age": td.Between(40, 50),
},
"checks %v is the right Person")
fmt.Println("Foobar is between 40 & 50:", ok)
// Model can be empty
ok = t.Struct(got, Person{}, td.StructFields{
"Name": "Foobar",
"Age": td.Between(40, 50),
"NumChildren": td.Not(0),
},
"checks %v is the right Person")
fmt.Println("Foobar has some children:", ok)
// Works with pointers too
ok = t.Struct(&got, &Person{}, td.StructFields{
"Name": "Foobar",
"Age": td.Between(40, 50),
"NumChildren": td.Not(0),
},
"checks %v is the right Person")
fmt.Println("Foobar has some children (using pointer):", ok)
// Model does not need to be instanciated
ok = t.Struct(&got, (*Person)(nil), td.StructFields{
"Name": "Foobar",
"Age": td.Between(40, 50),
"NumChildren": td.Not(0),
},
"checks %v is the right Person")
fmt.Println("Foobar has some children (using nil model):", ok)
// Output:
// Foobar is between 40 & 50: true
// Foobar has some children: true
// Foobar has some children (using pointer): true
// Foobar has some children (using nil model): true
}
func ExampleT_Struct_overwrite_model() {
t := td.NewT(&testing.T{})
type Person struct {
Name string
Age int
NumChildren int
}
got := Person{
Name: "Foobar",
Age: 42,
NumChildren: 3,
}
ok := t.Struct(got, Person{
Name: "Foobar",
Age: 53,
}, td.StructFields{
">Age": td.Between(40, 50), // ">" to overwrite Age:53 in model
"NumChildren": td.Gt(2),
},
"checks %v is the right Person")
fmt.Println("Foobar is between 40 & 50:", ok)
ok = t.Struct(got, Person{
Name: "Foobar",
Age: 53,
}, td.StructFields{
"> Age": td.Between(40, 50), // same, ">" can be followed by spaces
"NumChildren": td.Gt(2),
},
"checks %v is the right Person")
fmt.Println("Foobar is between 40 & 50:", ok)
// Output:
// Foobar is between 40 & 50: true
// Foobar is between 40 & 50: true
}
func ExampleT_Struct_patterns() {
t := td.NewT(&testing.T{})
type Person struct {
Firstname string
Lastname string
Surname string
Nickname string
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
}
now := time.Now()
got := Person{
Firstname: "Maxime",
Lastname: "Foo",
Surname: "Max",
Nickname: "max",
CreatedAt: now,
UpdatedAt: now,
DeletedAt: nil, // not deleted yet
}
ok := t.Struct(got, Person{Lastname: "Foo"}, td.StructFields{
`DeletedAt`: nil,
`= *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model
`=~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt
},
"mix shell & regexp patterns")
fmt.Println("Patterns match only remaining fields:", ok)
ok = t.Struct(got, Person{Lastname: "Foo"}, td.StructFields{
`DeletedAt`: nil,
`1 = *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model
`2 =~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt
},
"ordered patterns")
fmt.Println("Ordered patterns match only remaining fields:", ok)
// Output:
// Patterns match only remaining fields: true
// Ordered patterns match only remaining fields: true
}
func ExampleT_Struct_lazy_model() {
t := td.NewT(&testing.T{})
got := struct {
name string
age int
}{
name: "Foobar",
age: 42,
}
ok := t.Struct(got, nil, td.StructFields{
"name": "Foobar",
"age": td.Between(40, 45),
})
fmt.Println("Lazy model:", ok)
ok = t.Struct(got, nil, td.StructFields{
"name": "Foobar",
"zip": 666,
})
fmt.Println("Lazy model with unknown field:", ok)
// Output:
// Lazy model: true
// Lazy model with unknown field: false
}
func ExampleT_SubBagOf() {
t := td.NewT(&testing.T{})
got := []int{1, 3, 5, 8, 8, 1, 2}
ok := t.SubBagOf(got, []any{0, 0, 1, 1, 2, 2, 3, 3, 5, 5, 8, 8, 9, 9},
"checks at least all items are present, in any order")
fmt.Println(ok)
// got contains one 8 too many
ok = t.SubBagOf(got, []any{0, 0, 1, 1, 2, 2, 3, 3, 5, 5, 8, 9, 9},
"checks at least all items are present, in any order")
fmt.Println(ok)
got = []int{1, 3, 5, 2}
ok = t.SubBagOf(got, []any{td.Between(0, 3), td.Between(0, 3), td.Between(0, 3), td.Between(0, 3), td.Gt(4), td.Gt(4)},
"checks at least all items match, in any order with TestDeep operators")
fmt.Println(ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using expected... without copying it to a new
// []any slice, then use td.Flatten!
expected := []int{1, 2, 3, 5, 9, 8}
ok = t.SubBagOf(got, []any{td.Flatten(expected)},
"checks at least all expected items are present, in any order")
fmt.Println(ok)
// Output:
// true
// false
// true
// true
}
func ExampleT_SubJSONOf_basic() {
t := td.NewT(&testing.T{})
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
}{
Fullname: "Bob",
Age: 42,
}
ok := t.SubJSONOf(got, `{"age":42,"fullname":"Bob","gender":"male"}`, nil)
fmt.Println("check got with age then fullname:", ok)
ok = t.SubJSONOf(got, `{"fullname":"Bob","age":42,"gender":"male"}`, nil)
fmt.Println("check got with fullname then age:", ok)
ok = t.SubJSONOf(got, `
// This should be the JSON representation of a struct
{
// A person:
"fullname": "Bob", // The name of this person
"age": 42, /* The age of this person:
- 42 of course
- to demonstrate a multi-lines comment */
"gender": "male" // This field is ignored as SubJSONOf
}`, nil)
fmt.Println("check got with nicely formatted and commented JSON:", ok)
ok = t.SubJSONOf(got, `{"fullname":"Bob","gender":"male"}`, nil)
fmt.Println("check got without age field:", ok)
// Output:
// check got with age then fullname: true
// check got with fullname then age: true
// check got with nicely formatted and commented JSON: true
// check got without age field: false
}
func ExampleT_SubJSONOf_placeholders() {
t := td.NewT(&testing.T{})
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
}{
Fullname: "Bob Foobar",
Age: 42,
}
ok := t.SubJSONOf(got, `{"age": $1, "fullname": $2, "gender": $3}`, []any{42, "Bob Foobar", "male"})
fmt.Println("check got with numeric placeholders without operators:", ok)
ok = t.SubJSONOf(got, `{"age": $1, "fullname": $2, "gender": $3}`, []any{td.Between(40, 45), td.HasSuffix("Foobar"), td.NotEmpty()})
fmt.Println("check got with numeric placeholders:", ok)
ok = t.SubJSONOf(got, `{"age": "$1", "fullname": "$2", "gender": "$3"}`, []any{td.Between(40, 45), td.HasSuffix("Foobar"), td.NotEmpty()})
fmt.Println("check got with double-quoted numeric placeholders:", ok)
ok = t.SubJSONOf(got, `{"age": $age, "fullname": $name, "gender": $gender}`, []any{td.Tag("age", td.Between(40, 45)), td.Tag("name", td.HasSuffix("Foobar")), td.Tag("gender", td.NotEmpty())})
fmt.Println("check got with named placeholders:", ok)
ok = t.SubJSONOf(got, `{"age": $^NotZero, "fullname": $^NotEmpty, "gender": $^NotEmpty}`, nil)
fmt.Println("check got with operator shortcuts:", ok)
// Output:
// check got with numeric placeholders without operators: true
// check got with numeric placeholders: true
// check got with double-quoted numeric placeholders: true
// check got with named placeholders: true
// check got with operator shortcuts: true
}
func ExampleT_SubJSONOf_file() {
t := td.NewT(&testing.T{})
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
Gender string `json:"gender"`
}{
Fullname: "Bob Foobar",
Age: 42,
Gender: "male",
}
tmpDir, err := os.MkdirTemp("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir) //nolint: errcheck // clean up
filename := tmpDir + "/test.json"
if err = os.WriteFile(filename, []byte(`
{
"fullname": "$name",
"age": "$age",
"gender": "$gender",
"details": {
"city": "TestCity",
"zip": 666
}
}`), 0644); err != nil {
t.Fatal(err)
}
// OK let's test with this file
ok := t.SubJSONOf(got, filename, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))})
fmt.Println("Full match from file name:", ok)
// When the file is already open
file, err := os.Open(filename)
if err != nil {
t.Fatal(err)
}
ok = t.SubJSONOf(got, file, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))})
fmt.Println("Full match from io.Reader:", ok)
// Output:
// Full match from file name: true
// Full match from io.Reader: true
}
func ExampleT_SubMapOf_map() {
t := td.NewT(&testing.T{})
got := map[string]int{"foo": 12, "bar": 42}
ok := t.SubMapOf(got, map[string]int{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": 666},
"checks map %v is included in expected keys/values", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleT_SubMapOf_typedMap() {
t := td.NewT(&testing.T{})
type MyMap map[string]int
got := MyMap{"foo": 12, "bar": 42}
ok := t.SubMapOf(got, MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": 666},
"checks typed map %v is included in expected keys/values", got)
fmt.Println(ok)
ok = t.SubMapOf(&got, &MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": 666},
"checks pointed typed map %v is included in expected keys/values", got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleT_SubSetOf() {
t := td.NewT(&testing.T{})
got := []int{1, 3, 5, 8, 8, 1, 2}
// Matches as all items are expected, ignoring duplicates
ok := t.SubSetOf(got, []any{1, 2, 3, 4, 5, 6, 7, 8},
"checks at least all items are present, in any order, ignoring duplicates")
fmt.Println(ok)
// Tries its best not to raise an error when a value can be matched
// by several SubSetOf entries
ok = t.SubSetOf(got, []any{td.Between(1, 4), 3, td.Between(2, 10), td.Gt(100)},
"checks at least all items are present, in any order, ignoring duplicates")
fmt.Println(ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using expected... without copying it to a new
// []any slice, then use td.Flatten!
expected := []int{1, 2, 3, 4, 5, 6, 7, 8}
ok = t.SubSetOf(got, []any{td.Flatten(expected)},
"checks at least all expected items are present, in any order, ignoring duplicates")
fmt.Println(ok)
// Output:
// true
// true
// true
}
func ExampleT_SuperBagOf() {
t := td.NewT(&testing.T{})
got := []int{1, 3, 5, 8, 8, 1, 2}
ok := t.SuperBagOf(got, []any{8, 5, 8},
"checks the items are present, in any order")
fmt.Println(ok)
ok = t.SuperBagOf(got, []any{td.Gt(5), td.Lte(2)},
"checks at least 2 items of %v match", got)
fmt.Println(ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using expected... without copying it to a new
// []any slice, then use td.Flatten!
expected := []int{8, 5, 8}
ok = t.SuperBagOf(got, []any{td.Flatten(expected)},
"checks the expected items are present, in any order")
fmt.Println(ok)
// Output:
// true
// true
// true
}
func ExampleT_SuperJSONOf_basic() {
t := td.NewT(&testing.T{})
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
Gender string `json:"gender"`
City string `json:"city"`
Zip int `json:"zip"`
}{
Fullname: "Bob",
Age: 42,
Gender: "male",
City: "TestCity",
Zip: 666,
}
ok := t.SuperJSONOf(got, `{"age":42,"fullname":"Bob","gender":"male"}`, nil)
fmt.Println("check got with age then fullname:", ok)
ok = t.SuperJSONOf(got, `{"fullname":"Bob","age":42,"gender":"male"}`, nil)
fmt.Println("check got with fullname then age:", ok)
ok = t.SuperJSONOf(got, `
// This should be the JSON representation of a struct
{
// A person:
"fullname": "Bob", // The name of this person
"age": 42, /* The age of this person:
- 42 of course
- to demonstrate a multi-lines comment */
"gender": "male" // The gender!
}`, nil)
fmt.Println("check got with nicely formatted and commented JSON:", ok)
ok = t.SuperJSONOf(got, `{"fullname":"Bob","gender":"male","details":{}}`, nil)
fmt.Println("check got with details field:", ok)
// Output:
// check got with age then fullname: true
// check got with fullname then age: true
// check got with nicely formatted and commented JSON: true
// check got with details field: false
}
func ExampleT_SuperJSONOf_placeholders() {
t := td.NewT(&testing.T{})
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
Gender string `json:"gender"`
City string `json:"city"`
Zip int `json:"zip"`
}{
Fullname: "Bob Foobar",
Age: 42,
Gender: "male",
City: "TestCity",
Zip: 666,
}
ok := t.SuperJSONOf(got, `{"age": $1, "fullname": $2, "gender": $3}`, []any{42, "Bob Foobar", "male"})
fmt.Println("check got with numeric placeholders without operators:", ok)
ok = t.SuperJSONOf(got, `{"age": $1, "fullname": $2, "gender": $3}`, []any{td.Between(40, 45), td.HasSuffix("Foobar"), td.NotEmpty()})
fmt.Println("check got with numeric placeholders:", ok)
ok = t.SuperJSONOf(got, `{"age": "$1", "fullname": "$2", "gender": "$3"}`, []any{td.Between(40, 45), td.HasSuffix("Foobar"), td.NotEmpty()})
fmt.Println("check got with double-quoted numeric placeholders:", ok)
ok = t.SuperJSONOf(got, `{"age": $age, "fullname": $name, "gender": $gender}`, []any{td.Tag("age", td.Between(40, 45)), td.Tag("name", td.HasSuffix("Foobar")), td.Tag("gender", td.NotEmpty())})
fmt.Println("check got with named placeholders:", ok)
ok = t.SuperJSONOf(got, `{"age": $^NotZero, "fullname": $^NotEmpty, "gender": $^NotEmpty}`, nil)
fmt.Println("check got with operator shortcuts:", ok)
// Output:
// check got with numeric placeholders without operators: true
// check got with numeric placeholders: true
// check got with double-quoted numeric placeholders: true
// check got with named placeholders: true
// check got with operator shortcuts: true
}
func ExampleT_SuperJSONOf_file() {
t := td.NewT(&testing.T{})
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
Gender string `json:"gender"`
City string `json:"city"`
Zip int `json:"zip"`
}{
Fullname: "Bob Foobar",
Age: 42,
Gender: "male",
City: "TestCity",
Zip: 666,
}
tmpDir, err := os.MkdirTemp("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir) //nolint: errcheck // clean up
filename := tmpDir + "/test.json"
if err = os.WriteFile(filename, []byte(`
{
"fullname": "$name",
"age": "$age",
"gender": "$gender"
}`), 0644); err != nil {
t.Fatal(err)
}
// OK let's test with this file
ok := t.SuperJSONOf(got, filename, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))})
fmt.Println("Full match from file name:", ok)
// When the file is already open
file, err := os.Open(filename)
if err != nil {
t.Fatal(err)
}
ok = t.SuperJSONOf(got, file, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))})
fmt.Println("Full match from io.Reader:", ok)
// Output:
// Full match from file name: true
// Full match from io.Reader: true
}
func ExampleT_SuperMapOf_map() {
t := td.NewT(&testing.T{})
got := map[string]int{"foo": 12, "bar": 42, "zip": 89}
ok := t.SuperMapOf(got, map[string]int{"bar": 42}, td.MapEntries{"foo": td.Lt(15)},
"checks map %v contains at least all expected keys/values", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleT_SuperMapOf_typedMap() {
t := td.NewT(&testing.T{})
type MyMap map[string]int
got := MyMap{"foo": 12, "bar": 42, "zip": 89}
ok := t.SuperMapOf(got, MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15)},
"checks typed map %v contains at least all expected keys/values", got)
fmt.Println(ok)
ok = t.SuperMapOf(&got, &MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15)},
"checks pointed typed map %v contains at least all expected keys/values",
got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleT_SuperSetOf() {
t := td.NewT(&testing.T{})
got := []int{1, 3, 5, 8, 8, 1, 2}
ok := t.SuperSetOf(got, []any{1, 2, 3},
"checks the items are present, in any order and ignoring duplicates")
fmt.Println(ok)
ok = t.SuperSetOf(got, []any{td.Gt(5), td.Lte(2)},
"checks at least 2 items of %v match ignoring duplicates", got)
fmt.Println(ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using expected... without copying it to a new
// []any slice, then use td.Flatten!
expected := []int{1, 2, 3}
ok = t.SuperSetOf(got, []any{td.Flatten(expected)},
"checks the expected items are present, in any order and ignoring duplicates")
fmt.Println(ok)
// Output:
// true
// true
// true
}
func ExampleT_SuperSliceOf_array() {
t := td.NewT(&testing.T{})
got := [4]int{42, 58, 26, 666}
ok := t.SuperSliceOf(got, [4]int{1: 58}, td.ArrayEntries{3: td.Gt(660)},
"checks array %v", got)
fmt.Println("Only check items #1 & #3:", ok)
ok = t.SuperSliceOf(got, [4]int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3:", ok)
ok = t.SuperSliceOf(&got, &[4]int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of an array pointer:", ok)
ok = t.SuperSliceOf(&got, (*[4]int)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of an array pointer, using nil model:", ok)
// Output:
// Only check items #1 & #3: true
// Only check items #0 & #3: true
// Only check items #0 & #3 of an array pointer: true
// Only check items #0 & #3 of an array pointer, using nil model: true
}
func ExampleT_SuperSliceOf_typedArray() {
t := td.NewT(&testing.T{})
type MyArray [4]int
got := MyArray{42, 58, 26, 666}
ok := t.SuperSliceOf(got, MyArray{1: 58}, td.ArrayEntries{3: td.Gt(660)},
"checks typed array %v", got)
fmt.Println("Only check items #1 & #3:", ok)
ok = t.SuperSliceOf(got, MyArray{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3:", ok)
ok = t.SuperSliceOf(&got, &MyArray{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of an array pointer:", ok)
ok = t.SuperSliceOf(&got, (*MyArray)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of an array pointer, using nil model:", ok)
// Output:
// Only check items #1 & #3: true
// Only check items #0 & #3: true
// Only check items #0 & #3 of an array pointer: true
// Only check items #0 & #3 of an array pointer, using nil model: true
}
func ExampleT_SuperSliceOf_slice() {
t := td.NewT(&testing.T{})
got := []int{42, 58, 26, 666}
ok := t.SuperSliceOf(got, []int{1: 58}, td.ArrayEntries{3: td.Gt(660)},
"checks array %v", got)
fmt.Println("Only check items #1 & #3:", ok)
ok = t.SuperSliceOf(got, []int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3:", ok)
ok = t.SuperSliceOf(&got, &[]int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of a slice pointer:", ok)
ok = t.SuperSliceOf(&got, (*[]int)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of a slice pointer, using nil model:", ok)
// Output:
// Only check items #1 & #3: true
// Only check items #0 & #3: true
// Only check items #0 & #3 of a slice pointer: true
// Only check items #0 & #3 of a slice pointer, using nil model: true
}
func ExampleT_SuperSliceOf_typedSlice() {
t := td.NewT(&testing.T{})
type MySlice []int
got := MySlice{42, 58, 26, 666}
ok := t.SuperSliceOf(got, MySlice{1: 58}, td.ArrayEntries{3: td.Gt(660)},
"checks typed array %v", got)
fmt.Println("Only check items #1 & #3:", ok)
ok = t.SuperSliceOf(got, MySlice{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3:", ok)
ok = t.SuperSliceOf(&got, &MySlice{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of a slice pointer:", ok)
ok = t.SuperSliceOf(&got, (*MySlice)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)},
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of a slice pointer, using nil model:", ok)
// Output:
// Only check items #1 & #3: true
// Only check items #0 & #3: true
// Only check items #0 & #3 of a slice pointer: true
// Only check items #0 & #3 of a slice pointer, using nil model: true
}
func ExampleT_TruncTime() {
t := td.NewT(&testing.T{})
dateToTime := func(str string) time.Time {
t, err := time.Parse(time.RFC3339Nano, str)
if err != nil {
panic(err)
}
return t
}
got := dateToTime("2018-05-01T12:45:53.123456789Z")
// Compare dates ignoring nanoseconds and monotonic parts
expected := dateToTime("2018-05-01T12:45:53Z")
ok := t.TruncTime(got, expected, time.Second,
"checks date %v, truncated to the second", got)
fmt.Println(ok)
// Compare dates ignoring time and so monotonic parts
expected = dateToTime("2018-05-01T11:22:33.444444444Z")
ok = t.TruncTime(got, expected, 24*time.Hour,
"checks date %v, truncated to the day", got)
fmt.Println(ok)
// Compare dates exactly but ignoring monotonic part
expected = dateToTime("2018-05-01T12:45:53.123456789Z")
ok = t.TruncTime(got, expected, 0,
"checks date %v ignoring monotonic part", got)
fmt.Println(ok)
// Output:
// true
// true
// true
}
func ExampleT_Values() {
t := td.NewT(&testing.T{})
got := map[string]int{"foo": 1, "bar": 2, "zip": 3}
// Values tests values in an ordered manner
ok := t.Values(got, []int{1, 2, 3})
fmt.Println("All sorted values are found:", ok)
// If the expected values are not ordered, it fails
ok = t.Values(got, []int{3, 1, 2})
fmt.Println("All unsorted values are found:", ok)
// To circumvent that, one can use Bag operator
ok = t.Values(got, td.Bag(3, 1, 2))
fmt.Println("All unsorted values are found, with the help of Bag operator:", ok)
// Check that each value is between 1 and 3
ok = t.Values(got, td.ArrayEach(td.Between(1, 3)))
fmt.Println("Each value is between 1 and 3:", ok)
// Output:
// All sorted values are found: true
// All unsorted values are found: false
// All unsorted values are found, with the help of Bag operator: true
// Each value is between 1 and 3: true
}
func ExampleT_Zero() {
t := td.NewT(&testing.T{})
ok := t.Zero(0)
fmt.Println(ok)
ok = t.Zero(float64(0))
fmt.Println(ok)
ok = t.Zero(12) // fails, as 12 is not 0 :)
fmt.Println(ok)
ok = t.Zero((map[string]int)(nil))
fmt.Println(ok)
ok = t.Zero(map[string]int{}) // fails, as not nil
fmt.Println(ok)
ok = t.Zero(([]int)(nil))
fmt.Println(ok)
ok = t.Zero([]int{}) // fails, as not nil
fmt.Println(ok)
ok = t.Zero([3]int{})
fmt.Println(ok)
ok = t.Zero([3]int{0, 1}) // fails, DATA[1] is not 0
fmt.Println(ok)
ok = t.Zero(bytes.Buffer{})
fmt.Println(ok)
ok = t.Zero(&bytes.Buffer{}) // fails, as pointer not nil
fmt.Println(ok)
ok = t.Cmp(&bytes.Buffer{}, td.Ptr(td.Zero())) // OK with the help of Ptr()
fmt.Println(ok)
// Output:
// true
// true
// false
// true
// false
// true
// false
// true
// false
// true
// false
// true
}
go-testdeep-1.15.0/td/example_test.go 0000664 0000000 0000000 00000347055 15144170453 0017470 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2025, 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 (
"bytes"
"encoding/json"
"errors"
"fmt"
"math"
"os"
"regexp"
"strconv"
"strings"
"testing"
"time"
"github.com/maxatome/go-testdeep/td"
)
func Example() {
t := &testing.T{}
dateToTime := func(str string) time.Time {
t, err := time.Parse(time.RFC3339, str)
if err != nil {
panic(err)
}
return t
}
type PetFamily uint8
const (
Canidae PetFamily = 1
Felidae PetFamily = 2
)
type Pet struct {
Name string
Birthday time.Time
Family PetFamily
}
type Master struct {
Name string
AnnualIncome int
Pets []*Pet
}
// Imagine a function returning a Master slice...
masters := []Master{
{
Name: "Bob Smith",
AnnualIncome: 25000,
Pets: []*Pet{
{
Name: "Quizz",
Birthday: dateToTime("2010-11-05T10:00:00Z"),
Family: Canidae,
},
{
Name: "Charlie",
Birthday: dateToTime("2013-05-11T08:00:00Z"),
Family: Canidae,
},
},
},
{
Name: "John Doe",
AnnualIncome: 38000,
Pets: []*Pet{
{
Name: "Coco",
Birthday: dateToTime("2015-08-05T18:00:00Z"),
Family: Felidae,
},
{
Name: "Lucky",
Birthday: dateToTime("2014-04-17T07:00:00Z"),
Family: Canidae,
},
},
},
}
// Let's check masters slice contents
ok := td.Cmp(t, masters, td.All(
td.Len(td.Gt(0)), // len(masters) should be > 0
td.ArrayEach(
// For each Master
td.Struct(Master{}, td.StructFields{
// Master Name should be composed of 2 words, with 1st letter uppercased
"Name": td.Re(`^[A-Z][a-z]+ [A-Z][a-z]+\z`),
// Annual income should be greater than $10000
"AnnualIncome": td.Gt(10000),
"Pets": td.ArrayEach(
// For each Pet
td.Struct(&Pet{}, td.StructFields{
// Pet Name should be composed of 1 word, with 1st letter uppercased
"Name": td.Re(`^[A-Z][a-z]+\z`),
"Birthday": td.All(
// Pet should be born after 2010, January 1st, but before now!
td.Between(dateToTime("2010-01-01T00:00:00Z"), time.Now()),
// AND minutes, seconds and nanoseconds should be 0
td.Code(func(t time.Time) bool {
return t.Minute() == 0 && t.Second() == 0 && t.Nanosecond() == 0
}),
),
// Only dogs and cats allowed
"Family": td.Any(Canidae, Felidae),
}),
),
}),
),
))
fmt.Println(ok)
// Output:
// true
}
func ExampleIgnore() {
t := &testing.T{}
ok := td.Cmp(t, []int{1, 2, 3},
td.Slice([]int{}, td.ArrayEntries{
0: 1,
1: td.Ignore(), // do not care about this entry
2: 3,
}))
fmt.Println(ok)
// Output:
// true
}
func ExampleAll() {
t := &testing.T{}
got := "foo/bar"
// Checks got string against:
// "o/b" regexp *AND* "bar" suffix *AND* exact "foo/bar" string
ok := td.Cmp(t,
got,
td.All(td.Re("o/b"), td.HasSuffix("bar"), "foo/bar"),
"checks value %s", got)
fmt.Println(ok)
// Checks got string against:
// "o/b" regexp *AND* "bar" suffix *AND* exact "fooX/Ybar" string
ok = td.Cmp(t,
got,
td.All(td.Re("o/b"), td.HasSuffix("bar"), "fooX/Ybar"),
"checks value %s", got)
fmt.Println(ok)
// When some operators or values have to be reused and mixed between
// several calls, Flatten can be used to avoid boring and
// inefficient []any copies:
regOps := td.Flatten([]td.TestDeep{td.Re("o/b"), td.Re(`^fo`), td.Re(`ar$`)})
ok = td.Cmp(t,
got,
td.All(td.HasPrefix("foo"), regOps, td.HasSuffix("bar")),
"checks all operators against value %s", got)
fmt.Println(ok)
// Output:
// true
// false
// true
}
func ExampleAny() {
t := &testing.T{}
got := "foo/bar"
// Checks got string against:
// "zip" regexp *OR* "bar" suffix
ok := td.Cmp(t, got, td.Any(td.Re("zip"), td.HasSuffix("bar")),
"checks value %s", got)
fmt.Println(ok)
// Checks got string against:
// "zip" regexp *OR* "foo" suffix
ok = td.Cmp(t, got, td.Any(td.Re("zip"), td.HasSuffix("foo")),
"checks value %s", got)
fmt.Println(ok)
// When some operators or values have to be reused and mixed between
// several calls, Flatten can be used to avoid boring and
// inefficient []any copies:
regOps := td.Flatten([]td.TestDeep{td.Re("a/c"), td.Re(`^xx`), td.Re(`ar$`)})
ok = td.Cmp(t,
got,
td.Any(td.HasPrefix("xxx"), regOps, td.HasSuffix("zip")),
"check at least one operator matches value %s", got)
fmt.Println(ok)
// Output:
// true
// false
// true
}
func ExampleArray_array() {
t := &testing.T{}
got := [3]int{42, 58, 26}
ok := td.Cmp(t, got,
td.Array([3]int{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}),
"checks array %v", got)
fmt.Println("Simple array:", ok)
ok = td.Cmp(t, &got,
td.Array(&[3]int{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}),
"checks array %v", got)
fmt.Println("Array pointer:", ok)
ok = td.Cmp(t, &got,
td.Array((*[3]int)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}),
"checks array %v", got)
fmt.Println("Array pointer, nil model:", ok)
// Output:
// Simple array: true
// Array pointer: true
// Array pointer, nil model: true
}
func ExampleArray_typedArray() {
t := &testing.T{}
type MyArray [3]int
got := MyArray{42, 58, 26}
ok := td.Cmp(t, got,
td.Array(MyArray{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}),
"checks typed array %v", got)
fmt.Println("Typed array:", ok)
ok = td.Cmp(t, &got,
td.Array(&MyArray{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}),
"checks pointer on typed array %v", got)
fmt.Println("Pointer on a typed array:", ok)
ok = td.Cmp(t, &got,
td.Array(&MyArray{}, td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}),
"checks pointer on typed array %v", got)
fmt.Println("Pointer on a typed array, empty model:", ok)
ok = td.Cmp(t, &got,
td.Array((*MyArray)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}),
"checks pointer on typed array %v", got)
fmt.Println("Pointer on a typed array, nil model:", ok)
// Output:
// Typed array: true
// Pointer on a typed array: true
// Pointer on a typed array, empty model: true
// Pointer on a typed array, nil model: true
}
func ExampleArrayEach_array() {
t := &testing.T{}
got := [3]int{42, 58, 26}
ok := td.Cmp(t, got, td.ArrayEach(td.Between(25, 60)),
"checks each item of array %v is in [25 .. 60]", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleArrayEach_typedArray() {
t := &testing.T{}
type MyArray [3]int
got := MyArray{42, 58, 26}
ok := td.Cmp(t, got, td.ArrayEach(td.Between(25, 60)),
"checks each item of typed array %v is in [25 .. 60]", got)
fmt.Println(ok)
ok = td.Cmp(t, &got, td.ArrayEach(td.Between(25, 60)),
"checks each item of typed array pointer %v is in [25 .. 60]", got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleArrayEach_slice() {
t := &testing.T{}
got := []int{42, 58, 26}
ok := td.Cmp(t, got, td.ArrayEach(td.Between(25, 60)),
"checks each item of slice %v is in [25 .. 60]", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleArrayEach_typedSlice() {
t := &testing.T{}
type MySlice []int
got := MySlice{42, 58, 26}
ok := td.Cmp(t, got, td.ArrayEach(td.Between(25, 60)),
"checks each item of typed slice %v is in [25 .. 60]", got)
fmt.Println(ok)
ok = td.Cmp(t, &got, td.ArrayEach(td.Between(25, 60)),
"checks each item of typed slice pointer %v is in [25 .. 60]", got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleBag() {
t := &testing.T{}
got := []int{1, 3, 5, 8, 8, 1, 2}
// Matches as all items are present
ok := td.Cmp(t, got, td.Bag(1, 1, 2, 3, 5, 8, 8),
"checks all items are present, in any order")
fmt.Println(ok)
// Does not match as got contains 2 times 1 and 8, and these
// duplicates are not expected
ok = td.Cmp(t, got, td.Bag(1, 2, 3, 5, 8),
"checks all items are present, in any order")
fmt.Println(ok)
got = []int{1, 3, 5, 8, 2}
// Duplicates of 1 and 8 are expected but not present in got
ok = td.Cmp(t, got, td.Bag(1, 1, 2, 3, 5, 8, 8),
"checks all items are present, in any order")
fmt.Println(ok)
// Matches as all items are present
ok = td.Cmp(t, got, td.Bag(1, 2, 3, 5, td.Gt(7)),
"checks all items are present, in any order")
fmt.Println(ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using expected... without copying it to a new
// []any slice, then use td.Flatten!
expected := []int{1, 2, 3, 5}
ok = td.Cmp(t, got, td.Bag(td.Flatten(expected), td.Gt(7)),
"checks all expected items are present, in any order")
fmt.Println(ok)
// Output:
// true
// false
// false
// true
// true
}
func ExampleBetween_int() {
t := &testing.T{}
got := 156
ok := td.Cmp(t, got, td.Between(154, 156),
"checks %v is in [154 .. 156]", got)
fmt.Println(ok)
// BoundsInIn is implicit
ok = td.Cmp(t, got, td.Between(154, 156, td.BoundsInIn),
"checks %v is in [154 .. 156]", got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Between(154, 156, td.BoundsInOut),
"checks %v is in [154 .. 156[", got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Between(154, 156, td.BoundsOutIn),
"checks %v is in ]154 .. 156]", got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Between(154, 156, td.BoundsOutOut),
"checks %v is in ]154 .. 156[", got)
fmt.Println(ok)
// Output:
// true
// true
// false
// true
// false
}
func ExampleBetween_string() {
t := &testing.T{}
got := "abc"
ok := td.Cmp(t, got, td.Between("aaa", "abc"),
`checks "%v" is in ["aaa" .. "abc"]`, got)
fmt.Println(ok)
// BoundsInIn is implicit
ok = td.Cmp(t, got, td.Between("aaa", "abc", td.BoundsInIn),
`checks "%v" is in ["aaa" .. "abc"]`, got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Between("aaa", "abc", td.BoundsInOut),
`checks "%v" is in ["aaa" .. "abc"[`, got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Between("aaa", "abc", td.BoundsOutIn),
`checks "%v" is in ]"aaa" .. "abc"]`, got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Between("aaa", "abc", td.BoundsOutOut),
`checks "%v" is in ]"aaa" .. "abc"[`, got)
fmt.Println(ok)
// Output:
// true
// true
// false
// true
// false
}
func ExampleBetween_time() {
t := &testing.T{}
before := time.Now()
occurredAt := time.Now()
after := time.Now()
ok := td.Cmp(t, occurredAt, td.Between(before, after))
fmt.Println("It occurred between before and after:", ok)
type MyTime time.Time
ok = td.Cmp(t, MyTime(occurredAt), td.Between(MyTime(before), MyTime(after)))
fmt.Println("Same for convertible MyTime type:", ok)
ok = td.Cmp(t, MyTime(occurredAt), td.Between(before, after))
fmt.Println("MyTime vs time.Time:", ok)
ok = td.Cmp(t, occurredAt, td.Between(before, 10*time.Second))
fmt.Println("Using a time.Duration as TO:", ok)
ok = td.Cmp(t, MyTime(occurredAt), td.Between(MyTime(before), 10*time.Second))
fmt.Println("Using MyTime as FROM and time.Duration as TO:", ok)
// Output:
// It occurred between before and after: true
// Same for convertible MyTime type: true
// MyTime vs time.Time: false
// Using a time.Duration as TO: true
// Using MyTime as FROM and time.Duration as TO: true
}
func ExampleCap() {
t := &testing.T{}
got := make([]int, 0, 12)
ok := td.Cmp(t, got, td.Cap(12), "checks %v capacity is 12", got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Cap(0), "checks %v capacity is 0", got)
fmt.Println(ok)
got = nil
ok = td.Cmp(t, got, td.Cap(0), "checks %v capacity is 0", got)
fmt.Println(ok)
// Output:
// true
// false
// true
}
func ExampleCap_operator() {
t := &testing.T{}
got := make([]int, 0, 12)
ok := td.Cmp(t, got, td.Cap(td.Between(10, 12)),
"checks %v capacity is in [10 .. 12]", got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Cap(td.Gt(10)),
"checks %v capacity is in [10 .. 12]", got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleCatch() {
t := &testing.T{}
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
}{
Fullname: "Bob",
Age: 42,
}
var age int
ok := td.Cmp(t, got,
td.JSON(`{"age":$1,"fullname":"Bob"}`,
td.Catch(&age, td.Between(40, 45))))
fmt.Println("check got age+fullname:", ok)
fmt.Println("caught age:", age)
// Output:
// check got age+fullname: true
// caught age: 42
}
func ExampleCode() {
t := &testing.T{}
got := "12"
ok := td.Cmp(t, got,
td.Code(func(num string) bool {
n, err := strconv.Atoi(num)
return err == nil && n > 10 && n < 100
}),
"checks string `%s` contains a number and this number is in ]10 .. 100[",
got)
fmt.Println(ok)
// Same with failure reason
ok = td.Cmp(t, got,
td.Code(func(num string) (bool, string) {
n, err := strconv.Atoi(num)
if err != nil {
return false, "not a number"
}
if n > 10 && n < 100 {
return true, ""
}
return false, "not in ]10 .. 100["
}),
"checks string `%s` contains a number and this number is in ]10 .. 100[",
got)
fmt.Println(ok)
// Same with failure reason thanks to error
ok = td.Cmp(t, got,
td.Code(func(num string) error {
n, err := strconv.Atoi(num)
if err != nil {
return err
}
if n > 10 && n < 100 {
return nil
}
return fmt.Errorf("%d not in ]10 .. 100[", n)
}),
"checks string `%s` contains a number and this number is in ]10 .. 100[",
got)
fmt.Println(ok)
// Output:
// true
// true
// true
}
func ExampleCode_custom() {
t := &testing.T{}
got := 123
ok := td.Cmp(t, got, td.Code(func(t *td.T, num int) {
t.Cmp(num, 123)
}))
fmt.Println("with one *td.T:", ok)
ok = td.Cmp(t, got, td.Code(func(assert, require *td.T, num int) {
assert.Cmp(num, 123)
require.Cmp(num, 123)
}))
fmt.Println("with assert & require *td.T:", ok)
// Output:
// with one *td.T: true
// with assert & require *td.T: true
}
func ExampleContains_arraySlice() {
t := &testing.T{}
ok := td.Cmp(t, [...]int{11, 22, 33, 44}, td.Contains(22))
fmt.Println("array contains 22:", ok)
ok = td.Cmp(t, [...]int{11, 22, 33, 44}, td.Contains(td.Between(20, 25)))
fmt.Println("array contains at least one item in [20 .. 25]:", ok)
ok = td.Cmp(t, []int{11, 22, 33, 44}, td.Contains(22))
fmt.Println("slice contains 22:", ok)
ok = td.Cmp(t, []int{11, 22, 33, 44}, td.Contains(td.Between(20, 25)))
fmt.Println("slice contains at least one item in [20 .. 25]:", ok)
ok = td.Cmp(t, []int{11, 22, 33, 44}, td.Contains([]int{22, 33}))
fmt.Println("slice contains the sub-slice [22, 33]:", ok)
// Output:
// array contains 22: true
// array contains at least one item in [20 .. 25]: true
// slice contains 22: true
// slice contains at least one item in [20 .. 25]: true
// slice contains the sub-slice [22, 33]: true
}
func ExampleContains_nil() {
t := &testing.T{}
num := 123
got := [...]*int{&num, nil}
ok := td.Cmp(t, got, td.Contains(nil))
fmt.Println("array contains untyped nil:", ok)
ok = td.Cmp(t, got, td.Contains((*int)(nil)))
fmt.Println("array contains *int nil:", ok)
ok = td.Cmp(t, got, td.Contains(td.Nil()))
fmt.Println("array contains Nil():", ok)
ok = td.Cmp(t, got, td.Contains((*byte)(nil)))
fmt.Println("array contains *byte nil:", ok) // types differ: *byte ≠ *int
// Output:
// array contains untyped nil: true
// array contains *int nil: true
// array contains Nil(): true
// array contains *byte nil: false
}
func ExampleContains_map() {
t := &testing.T{}
ok := td.Cmp(t,
map[string]int{"foo": 11, "bar": 22, "zip": 33}, td.Contains(22))
fmt.Println("map contains value 22:", ok)
ok = td.Cmp(t,
map[string]int{"foo": 11, "bar": 22, "zip": 33},
td.Contains(td.Between(20, 25)))
fmt.Println("map contains at least one value in [20 .. 25]:", ok)
// Output:
// map contains value 22: true
// map contains at least one value in [20 .. 25]: true
}
func ExampleContains_string() {
t := &testing.T{}
got := "foobar"
ok := td.Cmp(t, got, td.Contains("oob"), "checks %s", got)
fmt.Println("contains `oob` string:", ok)
ok = td.Cmp(t, got, td.Contains([]byte("oob")), "checks %s", got)
fmt.Println("contains `oob` []byte:", ok)
ok = td.Cmp(t, got, td.Contains('b'), "checks %s", got)
fmt.Println("contains 'b' rune:", ok)
ok = td.Cmp(t, got, td.Contains(byte('a')), "checks %s", got)
fmt.Println("contains 'a' byte:", ok)
ok = td.Cmp(t, got, td.Contains(td.Between('n', 'p')), "checks %s", got)
fmt.Println("contains at least one character ['n' .. 'p']:", ok)
// Output:
// contains `oob` string: true
// contains `oob` []byte: true
// contains 'b' rune: true
// contains 'a' byte: true
// contains at least one character ['n' .. 'p']: true
}
func ExampleContains_stringer() {
t := &testing.T{}
// bytes.Buffer implements fmt.Stringer
got := bytes.NewBufferString("foobar")
ok := td.Cmp(t, got, td.Contains("oob"), "checks %s", got)
fmt.Println("contains `oob` string:", ok)
ok = td.Cmp(t, got, td.Contains('b'), "checks %s", got)
fmt.Println("contains 'b' rune:", ok)
ok = td.Cmp(t, got, td.Contains(byte('a')), "checks %s", got)
fmt.Println("contains 'a' byte:", ok)
ok = td.Cmp(t, got, td.Contains(td.Between('n', 'p')), "checks %s", got)
fmt.Println("contains at least one character ['n' .. 'p']:", ok)
// Output:
// contains `oob` string: true
// contains 'b' rune: true
// contains 'a' byte: true
// contains at least one character ['n' .. 'p']: true
}
func ExampleContains_error() {
t := &testing.T{}
got := errors.New("foobar")
ok := td.Cmp(t, got, td.Contains("oob"), "checks %s", got)
fmt.Println("contains `oob` string:", ok)
ok = td.Cmp(t, got, td.Contains('b'), "checks %s", got)
fmt.Println("contains 'b' rune:", ok)
ok = td.Cmp(t, got, td.Contains(byte('a')), "checks %s", got)
fmt.Println("contains 'a' byte:", ok)
ok = td.Cmp(t, got, td.Contains(td.Between('n', 'p')), "checks %s", got)
fmt.Println("contains at least one character ['n' .. 'p']:", ok)
// Output:
// contains `oob` string: true
// contains 'b' rune: true
// contains 'a' byte: true
// contains at least one character ['n' .. 'p']: true
}
func ExampleContainsKey() {
t := &testing.T{}
ok := td.Cmp(t,
map[string]int{"foo": 11, "bar": 22, "zip": 33}, td.ContainsKey("foo"))
fmt.Println(`map contains key "foo":`, ok)
ok = td.Cmp(t,
map[int]bool{12: true, 24: false, 42: true, 51: false},
td.ContainsKey(td.Between(40, 50)))
fmt.Println("map contains at least a key in [40 .. 50]:", ok)
ok = td.Cmp(t,
map[string]int{"FOO": 11, "bar": 22, "zip": 33},
td.ContainsKey(td.Smuggle(strings.ToLower, "foo")))
fmt.Println(`map contains key "foo" without taking case into account:`, ok)
// Output:
// map contains key "foo": true
// map contains at least a key in [40 .. 50]: true
// map contains key "foo" without taking case into account: true
}
func ExampleContainsKey_nil() {
t := &testing.T{}
num := 1234
got := map[*int]bool{&num: false, nil: true}
ok := td.Cmp(t, got, td.ContainsKey(nil))
fmt.Println("map contains untyped nil key:", ok)
ok = td.Cmp(t, got, td.ContainsKey((*int)(nil)))
fmt.Println("map contains *int nil key:", ok)
ok = td.Cmp(t, got, td.ContainsKey(td.Nil()))
fmt.Println("map contains Nil() key:", ok)
ok = td.Cmp(t, got, td.ContainsKey((*byte)(nil)))
fmt.Println("map contains *byte nil key:", ok) // types differ: *byte ≠ *int
// Output:
// map contains untyped nil key: true
// map contains *int nil key: true
// map contains Nil() key: true
// map contains *byte nil key: false
}
func ExampleDelay() {
t := &testing.T{}
cmpNow := func(expected td.TestDeep) bool {
time.Sleep(time.Microsecond) // imagine a DB insert returning a CreatedAt
return td.Cmp(t, time.Now(), expected)
}
before := time.Now()
ok := cmpNow(td.Between(before, time.Now()))
fmt.Println("Between called before compare:", ok)
ok = cmpNow(td.Delay(func() td.TestDeep {
return td.Between(before, time.Now())
}))
fmt.Println("Between delayed until compare:", ok)
// Output:
// Between called before compare: false
// Between delayed until compare: true
}
func ExampleEmpty() {
t := &testing.T{}
ok := td.Cmp(t, nil, td.Empty()) // special case: nil is considered empty
fmt.Println(ok)
// fails, typed nil is not empty (expect for channel, map, slice or
// pointers on array, channel, map slice and strings)
ok = td.Cmp(t, (*int)(nil), td.Empty())
fmt.Println(ok)
ok = td.Cmp(t, "", td.Empty())
fmt.Println(ok)
// Fails as 0 is a number, so not empty. Use Zero() instead
ok = td.Cmp(t, 0, td.Empty())
fmt.Println(ok)
ok = td.Cmp(t, (map[string]int)(nil), td.Empty())
fmt.Println(ok)
ok = td.Cmp(t, map[string]int{}, td.Empty())
fmt.Println(ok)
ok = td.Cmp(t, ([]int)(nil), td.Empty())
fmt.Println(ok)
ok = td.Cmp(t, []int{}, td.Empty())
fmt.Println(ok)
ok = td.Cmp(t, []int{3}, td.Empty()) // fails, as not empty
fmt.Println(ok)
ok = td.Cmp(t, [3]int{}, td.Empty()) // fails, Empty() is not Zero()!
fmt.Println(ok)
// Output:
// true
// false
// true
// false
// true
// true
// true
// true
// false
// false
}
func ExampleEmpty_pointers() {
t := &testing.T{}
type MySlice []int
ok := td.Cmp(t, MySlice{}, td.Empty()) // Ptr() not needed
fmt.Println(ok)
ok = td.Cmp(t, &MySlice{}, td.Empty())
fmt.Println(ok)
l1 := &MySlice{}
l2 := &l1
l3 := &l2
ok = td.Cmp(t, &l3, td.Empty())
fmt.Println(ok)
// Works the same for array, map, channel and string
// But not for others types as:
type MyStruct struct {
Value int
}
ok = td.Cmp(t, &MyStruct{}, td.Empty()) // fails, use Zero() instead
fmt.Println(ok)
// Output:
// true
// true
// true
// false
}
func ExampleErrorIs() {
t := &testing.T{}
err1 := fmt.Errorf("failure1")
err2 := fmt.Errorf("failure2: %w", err1)
err3 := fmt.Errorf("failure3: %w", err2)
err := fmt.Errorf("failure4: %w", err3)
ok := td.Cmp(t, err, td.ErrorIs(err))
fmt.Println("error is itself:", ok)
ok = td.Cmp(t, err, td.ErrorIs(err1))
fmt.Println("error is also err1:", ok)
ok = td.Cmp(t, err1, td.ErrorIs(err))
fmt.Println("err1 is err:", ok)
// Output:
// error is itself: true
// error is also err1: true
// err1 is err: 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{}
got := 156
ok := td.Cmp(t, got, td.Gt(155), "checks %v is > 155", got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Gt(156), "checks %v is > 156", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleGt_string() {
t := &testing.T{}
got := "abc"
ok := td.Cmp(t, got, td.Gt("abb"), `checks "%v" is > "abb"`, got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Gt("abc"), `checks "%v" is > "abc"`, got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleGte_int() {
t := &testing.T{}
got := 156
ok := td.Cmp(t, got, td.Gte(156), "checks %v is ≥ 156", got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Gte(155), "checks %v is ≥ 155", got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Gte(157), "checks %v is ≥ 157", got)
fmt.Println(ok)
// Output:
// true
// true
// false
}
func ExampleGte_string() {
t := &testing.T{}
got := "abc"
ok := td.Cmp(t, got, td.Gte("abc"), `checks "%v" is ≥ "abc"`, got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Gte("abb"), `checks "%v" is ≥ "abb"`, got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Gte("abd"), `checks "%v" is ≥ "abd"`, got)
fmt.Println(ok)
// Output:
// true
// true
// false
}
func ExampleIsa() {
t := &testing.T{}
type TstStruct struct {
Field int
}
got := TstStruct{Field: 1}
ok := td.Cmp(t, got, td.Isa(TstStruct{}), "checks got is a TstStruct")
fmt.Println(ok)
ok = td.Cmp(t, got, td.Isa(&TstStruct{}),
"checks got is a pointer on a TstStruct")
fmt.Println(ok)
ok = td.Cmp(t, &got, td.Isa(&TstStruct{}),
"checks &got is a pointer on a TstStruct")
fmt.Println(ok)
// Output:
// true
// false
// true
}
func ExampleIsa_interface() {
t := &testing.T{}
got := bytes.NewBufferString("foobar")
ok := td.Cmp(t, got, td.Isa((*fmt.Stringer)(nil)),
"checks got implements fmt.Stringer interface")
fmt.Println(ok)
errGot := fmt.Errorf("An error #%d occurred", 123)
ok = td.Cmp(t, errGot, td.Isa((*error)(nil)),
"checks errGot is a *error or implements error interface")
fmt.Println(ok)
// As nil, is passed below, it is not an interface but nil… So it
// does not match
errGot = nil
ok = td.Cmp(t, errGot, td.Isa((*error)(nil)),
"checks errGot is a *error or implements error interface")
fmt.Println(ok)
// BUT if its address is passed, now it is OK as the types match
ok = td.Cmp(t, &errGot, td.Isa((*error)(nil)),
"checks &errGot is a *error or implements error interface")
fmt.Println(ok)
// Output:
// true
// true
// false
// true
}
func ExampleJSON_basic() {
t := &testing.T{}
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
}{
Fullname: "Bob",
Age: 42,
}
ok := td.Cmp(t, got, td.JSON(`{"age":42,"fullname":"Bob"}`))
fmt.Println("check got with age then fullname:", ok)
ok = td.Cmp(t, got, td.JSON(`{"fullname":"Bob","age":42}`))
fmt.Println("check got with fullname then age:", ok)
ok = td.Cmp(t, got, td.JSON(`
// This should be the JSON representation of a struct
{
// A person:
"fullname": "Bob", // The name of this person
"age": 42 /* The age of this person:
- 42 of course
- to demonstrate a multi-lines comment */
}`))
fmt.Println("check got with nicely formatted and commented JSON:", ok)
ok = td.Cmp(t, got, td.JSON(`{"fullname":"Bob","age":42,"gender":"male"}`))
fmt.Println("check got with gender field:", ok)
ok = td.Cmp(t, got, td.JSON(`{"fullname":"Bob"}`))
fmt.Println("check got with fullname only:", ok)
ok = td.Cmp(t, true, td.JSON(`true`))
fmt.Println("check boolean got is true:", ok)
ok = td.Cmp(t, 42, td.JSON(`42`))
fmt.Println("check numeric got is 42:", ok)
got = nil
ok = td.Cmp(t, got, td.JSON(`null`))
fmt.Println("check nil got is null:", ok)
// Output:
// check got with age then fullname: true
// check got with fullname then age: true
// check got with nicely formatted and commented JSON: true
// check got with gender field: false
// check got with fullname only: false
// check boolean got is true: true
// check numeric got is 42: true
// check nil got is null: true
}
func ExampleJSON_placeholders() {
t := &testing.T{}
type Person struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
Children []*Person `json:"children,omitempty"`
}
got := &Person{
Fullname: "Bob Foobar",
Age: 42,
}
ok := td.Cmp(t, got, td.JSON(`{"age": $1, "fullname": $2}`, 42, "Bob Foobar"))
fmt.Println("check got with numeric placeholders without operators:", ok)
ok = td.Cmp(t, got,
td.JSON(`{"age": $1, "fullname": $2}`,
td.Between(40, 45),
td.HasSuffix("Foobar")))
fmt.Println("check got with numeric placeholders:", ok)
ok = td.Cmp(t, got,
td.JSON(`{"age": "$1", "fullname": "$2"}`,
td.Between(40, 45),
td.HasSuffix("Foobar")))
fmt.Println("check got with double-quoted numeric placeholders:", ok)
ok = td.Cmp(t, got,
td.JSON(`{"age": $age, "fullname": $name}`,
td.Tag("age", td.Between(40, 45)),
td.Tag("name", td.HasSuffix("Foobar"))))
fmt.Println("check got with named placeholders:", ok)
got.Children = []*Person{
{Fullname: "Alice", Age: 28},
{Fullname: "Brian", Age: 22},
}
ok = td.Cmp(t, got,
td.JSON(`{"age": $age, "fullname": $name, "children": $children}`,
td.Tag("age", td.Between(40, 45)),
td.Tag("name", td.HasSuffix("Foobar")),
td.Tag("children", td.Bag(
&Person{Fullname: "Brian", Age: 22},
&Person{Fullname: "Alice", Age: 28},
))))
fmt.Println("check got w/named placeholders, and children w/go structs:", ok)
ok = td.Cmp(t, got,
td.JSON(`{"age": Between($1, $2), "fullname": HasSuffix($suffix), "children": Len(2)}`,
40, 45,
td.Tag("suffix", "Foobar")))
fmt.Println("check got w/num & named placeholders:", ok)
// Output:
// check got with numeric placeholders without operators: true
// check got with numeric placeholders: true
// check got with double-quoted numeric placeholders: true
// check got with named placeholders: true
// check got w/named placeholders, and children w/go structs: true
// check got w/num & named placeholders: true
}
func ExampleJSON_embedding() {
t := &testing.T{}
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
}{
Fullname: "Bob Foobar",
Age: 42,
}
ok := td.Cmp(t, got, td.JSON(`{"age": NotZero(), "fullname": NotEmpty()}`))
fmt.Println("check got with simple operators:", ok)
ok = td.Cmp(t, got, td.JSON(`{"age": $^NotZero, "fullname": $^NotEmpty}`))
fmt.Println("check got with operator shortcuts:", ok)
ok = td.Cmp(t, got, td.JSON(`
{
"age": Between(40, 42, "]]"), // in ]40; 42]
"fullname": All(
HasPrefix("Bob"),
HasSuffix("bar") // ← comma is optional here
)
}`))
fmt.Println("check got with complex operators:", ok)
ok = td.Cmp(t, got, td.JSON(`
{
"age": Between(40, 42, "]["), // in ]40; 42[ → 42 excluded
"fullname": All(
HasPrefix("Bob"),
HasSuffix("bar"),
)
}`))
fmt.Println("check got with complex operators:", ok)
ok = td.Cmp(t, got, td.JSON(`
{
"age": Between($1, $2, $3), // in ]40; 42]
"fullname": All(
HasPrefix($4),
HasSuffix("bar") // ← comma is optional here
)
}`,
40, 42, td.BoundsOutIn,
"Bob"))
fmt.Println("check got with complex operators, w/placeholder args:", ok)
// Output:
// check got with simple operators: true
// check got with operator shortcuts: true
// check got with complex operators: true
// check got with complex operators: false
// check got with complex operators, w/placeholder args: true
}
func ExampleJSON_rawStrings() {
t := &testing.T{}
type details struct {
Address string `json:"address"`
Car string `json:"car"`
}
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
Details details `json:"details"`
}{
Fullname: "Foo Bar",
Age: 42,
Details: details{
Address: "something",
Car: "Peugeot",
},
}
ok := td.Cmp(t, got,
td.JSON(`
{
"fullname": HasPrefix("Foo"),
"age": Between(41, 43),
"details": SuperMapOf({
"address": NotEmpty, // () are optional when no parameters
"car": Any("Peugeot", "Tesla", "Jeep") // any of these
})
}`))
fmt.Println("Original:", ok)
ok = td.Cmp(t, got,
td.JSON(`
{
"fullname": "$^HasPrefix(\"Foo\")",
"age": "$^Between(41, 43)",
"details": "$^SuperMapOf({\n\"address\": NotEmpty,\n\"car\": Any(\"Peugeot\", \"Tesla\", \"Jeep\")\n})"
}`))
fmt.Println("JSON compliant:", ok)
ok = td.Cmp(t, got,
td.JSON(`
{
"fullname": "$^HasPrefix(\"Foo\")",
"age": "$^Between(41, 43)",
"details": "$^SuperMapOf({
\"address\": NotEmpty, // () are optional when no parameters
\"car\": Any(\"Peugeot\", \"Tesla\", \"Jeep\") // any of these
})"
}`))
fmt.Println("JSON multilines strings:", ok)
ok = td.Cmp(t, got,
td.JSON(`
{
"fullname": "$^HasPrefix(r)",
"age": "$^Between(41, 43)",
"details": "$^SuperMapOf({
r: NotEmpty, // () are optional when no parameters
r: Any(r, r, r) // any of these
})"
}`))
fmt.Println("Raw strings:", ok)
// Output:
// Original: true
// JSON compliant: true
// JSON multilines strings: true
// Raw strings: true
}
func ExampleJSON_file() {
t := &testing.T{}
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
Gender string `json:"gender"`
}{
Fullname: "Bob Foobar",
Age: 42,
Gender: "male",
}
tmpDir, err := os.MkdirTemp("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir) //nolint: errcheck // clean up
filename := tmpDir + "/test.json"
if err = os.WriteFile(filename, []byte(`
{
"fullname": "$name",
"age": "$age",
"gender": "$gender"
}`), 0644); err != nil {
t.Fatal(err)
}
// OK let's test with this file
ok := td.Cmp(t, got,
td.JSON(filename,
td.Tag("name", td.HasPrefix("Bob")),
td.Tag("age", td.Between(40, 45)),
td.Tag("gender", td.Re(`^(male|female)\z`))))
fmt.Println("Full match from file name:", ok)
// When the file is already open
file, err := os.Open(filename)
if err != nil {
t.Fatal(err)
}
ok = td.Cmp(t, got,
td.JSON(file,
td.Tag("name", td.HasPrefix("Bob")),
td.Tag("age", td.Between(40, 45)),
td.Tag("gender", td.Re(`^(male|female)\z`))))
fmt.Println("Full match from io.Reader:", ok)
// Output:
// Full match from file name: true
// Full match from io.Reader: true
}
func ExampleJSONPointer_rfc6901() {
t := &testing.T{}
got := json.RawMessage(`
{
"foo": ["bar", "baz"],
"": 0,
"a/b": 1,
"c%d": 2,
"e^f": 3,
"g|h": 4,
"i\\j": 5,
"k\"l": 6,
" ": 7,
"m~n": 8
}`)
expected := map[string]any{
"foo": []any{"bar", "baz"},
"": 0,
"a/b": 1,
"c%d": 2,
"e^f": 3,
"g|h": 4,
`i\j`: 5,
`k"l`: 6,
" ": 7,
"m~n": 8,
}
ok := td.Cmp(t, got, td.JSONPointer("", expected))
fmt.Println("Empty JSON pointer means all:", ok)
ok = td.Cmp(t, got, td.JSONPointer(`/foo`, []any{"bar", "baz"}))
fmt.Println("Extract `foo` key:", ok)
ok = td.Cmp(t, got, td.JSONPointer(`/foo/0`, "bar"))
fmt.Println("First item of `foo` key slice:", ok)
ok = td.Cmp(t, got, td.JSONPointer(`/`, 0))
fmt.Println("Empty key:", ok)
ok = td.Cmp(t, got, td.JSONPointer(`/a~1b`, 1))
fmt.Println("Slash has to be escaped using `~1`:", ok)
ok = td.Cmp(t, got, td.JSONPointer(`/c%d`, 2))
fmt.Println("% in key:", ok)
ok = td.Cmp(t, got, td.JSONPointer(`/e^f`, 3))
fmt.Println("^ in key:", ok)
ok = td.Cmp(t, got, td.JSONPointer(`/g|h`, 4))
fmt.Println("| in key:", ok)
ok = td.Cmp(t, got, td.JSONPointer(`/i\j`, 5))
fmt.Println("Backslash in key:", ok)
ok = td.Cmp(t, got, td.JSONPointer(`/k"l`, 6))
fmt.Println("Double-quote in key:", ok)
ok = td.Cmp(t, got, td.JSONPointer(`/ `, 7))
fmt.Println("Space key:", ok)
ok = td.Cmp(t, got, td.JSONPointer(`/m~0n`, 8))
fmt.Println("Tilde has to be escaped using `~0`:", ok)
// Output:
// Empty JSON pointer means all: true
// Extract `foo` key: true
// First item of `foo` key slice: true
// Empty key: true
// Slash has to be escaped using `~1`: true
// % in key: true
// ^ in key: true
// | in key: true
// Backslash in key: true
// Double-quote in key: true
// Space key: true
// Tilde has to be escaped using `~0`: true
}
func ExampleJSONPointer_struct() {
t := &testing.T{}
// Without json tags, encoding/json uses public fields name
type Item struct {
Name string
Value int64
Next *Item
}
got := Item{
Name: "first",
Value: 1,
Next: &Item{
Name: "second",
Value: 2,
Next: &Item{
Name: "third",
Value: 3,
},
},
}
ok := td.Cmp(t, got, td.JSONPointer("/Next/Next/Name", "third"))
fmt.Println("3rd item name is `third`:", ok)
ok = td.Cmp(t, got, td.JSONPointer("/Next/Next/Value", td.Gte(int64(3))))
fmt.Println("3rd item value is greater or equal than 3:", ok)
ok = td.Cmp(t, got,
td.JSONPointer("/Next",
td.JSONPointer("/Next",
td.JSONPointer("/Value", td.Gte(int64(3))))))
fmt.Println("3rd item value is still greater or equal than 3:", ok)
ok = td.Cmp(t, got, td.JSONPointer("/Next/Next/Next/Name", td.Ignore()))
fmt.Println("4th item exists and has a name:", ok)
// Struct comparison work with or without pointer: &Item{…} works too
ok = td.Cmp(t, got, td.JSONPointer("/Next/Next", Item{
Name: "third",
Value: 3,
}))
fmt.Println("3rd item full comparison:", ok)
// Output:
// 3rd item name is `third`: true
// 3rd item value is greater or equal than 3: true
// 3rd item value is still greater or equal than 3: true
// 4th item exists and has a name: false
// 3rd item full comparison: true
}
func ExampleJSONPointer_has_hasnt() {
t := &testing.T{}
got := json.RawMessage(`
{
"name": "Bob",
"age": 42,
"children": [
{
"name": "Alice",
"age": 16
},
{
"name": "Britt",
"age": 21,
"children": [
{
"name": "John",
"age": 1
}
]
}
]
}`)
// Has Bob some children?
ok := td.Cmp(t, got, td.JSONPointer("/children", td.Len(td.Gt(0))))
fmt.Println("Bob has at least one child:", ok)
// But checking "children" exists is enough here
ok = td.Cmp(t, got, td.JSONPointer("/children/0/children", td.Ignore()))
fmt.Println("Alice has children:", ok)
ok = td.Cmp(t, got, td.JSONPointer("/children/1/children", td.Ignore()))
fmt.Println("Britt has children:", ok)
// The reverse can be checked too
ok = td.Cmp(t, got, td.Not(td.JSONPointer("/children/0/children", td.Ignore())))
fmt.Println("Alice hasn't children:", ok)
ok = td.Cmp(t, got, td.Not(td.JSONPointer("/children/1/children", td.Ignore())))
fmt.Println("Britt hasn't children:", ok)
// Output:
// Bob has at least one child: true
// Alice has children: false
// Britt has children: true
// Alice hasn't children: true
// Britt hasn't children: false
}
func ExampleList() {
t := &testing.T{}
got := []int{1, 33, 8, 2}
// Matches as all items are present
ok := td.Cmp(t, got, td.List(1, td.Between(32, 34), td.Gt(7), 2))
fmt.Println("checks all items match, in this order:", ok)
// Does not match as got does not use the same order as expected
ok = td.Cmp(t, got, td.List(1, td.Gt(7), 2, td.Between(32, 34)))
fmt.Println("checks all items match, in wrong order:", ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using expected... without copying it to a new
// []any slice, then use td.Flatten!
expected := []int{1, 33, 8}
ok = td.Cmp(t, got, td.List(td.Flatten(expected), td.Lte(2)))
fmt.Println("checks all expected items are present + last one ≤ 2:", ok)
// Output:
// checks all items match, in this order: true
// checks all items match, in wrong order: false
// checks all expected items are present + last one ≤ 2: true
}
func ExampleKeys() {
t := &testing.T{}
got := map[string]int{"foo": 1, "bar": 2, "zip": 3}
// Keys tests keys in an ordered manner
ok := td.Cmp(t, got, td.Keys([]string{"bar", "foo", "zip"}))
fmt.Println("All sorted keys are found:", ok)
// If the expected keys are not ordered, it fails
ok = td.Cmp(t, got, td.Keys([]string{"zip", "bar", "foo"}))
fmt.Println("All unsorted keys are found:", ok)
// To circumvent that, one can use Bag operator
ok = td.Cmp(t, got, td.Keys(td.Bag("zip", "bar", "foo")))
fmt.Println("All unsorted keys are found, with the help of Bag operator:", ok)
// Check that each key is 3 bytes long
ok = td.Cmp(t, got, td.Keys(td.ArrayEach(td.Len(3))))
fmt.Println("Each key is 3 bytes long:", ok)
// Output:
// All sorted keys are found: true
// All unsorted keys are found: false
// All unsorted keys are found, with the help of Bag operator: true
// 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{}
gotInt64 := int64(1234)
gotInt32 := int32(1235)
type myInt uint16
gotMyInt := myInt(1236)
expected := td.Between(1230, 1240) // int type here
ok := td.Cmp(t, gotInt64, td.Lax(expected))
fmt.Println("int64 got between ints [1230 .. 1240]:", ok)
ok = td.Cmp(t, gotInt32, td.Lax(expected))
fmt.Println("int32 got between ints [1230 .. 1240]:", ok)
ok = td.Cmp(t, gotMyInt, td.Lax(expected))
fmt.Println("myInt got between ints [1230 .. 1240]:", ok)
// Output:
// int64 got between ints [1230 .. 1240]: true
// int32 got between ints [1230 .. 1240]: true
// myInt got between ints [1230 .. 1240]: true
}
func ExampleLen_slice() {
t := &testing.T{}
got := []int{11, 22, 33}
ok := td.Cmp(t, got, td.Len(3), "checks %v len is 3", got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Len(0), "checks %v len is 0", got)
fmt.Println(ok)
got = nil
ok = td.Cmp(t, got, td.Len(0), "checks %v len is 0", got)
fmt.Println(ok)
// Output:
// true
// false
// true
}
func ExampleLen_map() {
t := &testing.T{}
got := map[int]bool{11: true, 22: false, 33: false}
ok := td.Cmp(t, got, td.Len(3), "checks %v len is 3", got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Len(0), "checks %v len is 0", got)
fmt.Println(ok)
got = nil
ok = td.Cmp(t, got, td.Len(0), "checks %v len is 0", got)
fmt.Println(ok)
// Output:
// true
// false
// true
}
func ExampleLen_operatorSlice() {
t := &testing.T{}
got := []int{11, 22, 33}
ok := td.Cmp(t, got, td.Len(td.Between(3, 8)),
"checks %v len is in [3 .. 8]", got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Len(td.Lt(5)), "checks %v len is < 5", got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleLen_operatorMap() {
t := &testing.T{}
got := map[int]bool{11: true, 22: false, 33: false}
ok := td.Cmp(t, got, td.Len(td.Between(3, 8)),
"checks %v len is in [3 .. 8]", got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Len(td.Gte(3)), "checks %v len is ≥ 3", got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleLt_int() {
t := &testing.T{}
got := 156
ok := td.Cmp(t, got, td.Lt(157), "checks %v is < 157", got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Lt(156), "checks %v is < 156", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleLt_string() {
t := &testing.T{}
got := "abc"
ok := td.Cmp(t, got, td.Lt("abd"), `checks "%v" is < "abd"`, got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Lt("abc"), `checks "%v" is < "abc"`, got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleLte_int() {
t := &testing.T{}
got := 156
ok := td.Cmp(t, got, td.Lte(156), "checks %v is ≤ 156", got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Lte(157), "checks %v is ≤ 157", got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Lte(155), "checks %v is ≤ 155", got)
fmt.Println(ok)
// Output:
// true
// true
// false
}
func ExampleLte_string() {
t := &testing.T{}
got := "abc"
ok := td.Cmp(t, got, td.Lte("abc"), `checks "%v" is ≤ "abc"`, got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Lte("abd"), `checks "%v" is ≤ "abd"`, got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Lte("abb"), `checks "%v" is ≤ "abb"`, got)
fmt.Println(ok)
// Output:
// true
// true
// false
}
func ExampleMap_map() {
t := &testing.T{}
got := map[string]int{"foo": 12, "bar": 42, "zip": 89}
ok := td.Cmp(t, got,
td.Map(map[string]int{"bar": 42},
td.MapEntries{"foo": td.Lt(15), "zip": td.Ignore()}),
"checks map %v", got)
fmt.Println(ok)
ok = td.Cmp(t, got,
td.Map(map[string]int{},
td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()}),
"checks map %v", got)
fmt.Println(ok)
ok = td.Cmp(t, got,
td.Map((map[string]int)(nil),
td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()}),
"checks map %v", got)
fmt.Println(ok)
// Output:
// true
// true
// true
}
func ExampleMap_typedMap() {
t := &testing.T{}
type MyMap map[string]int
got := MyMap{"foo": 12, "bar": 42, "zip": 89}
ok := td.Cmp(t, got,
td.Map(MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": td.Ignore()}),
"checks typed map %v", got)
fmt.Println(ok)
ok = td.Cmp(t, &got,
td.Map(&MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": td.Ignore()}),
"checks pointer on typed map %v", got)
fmt.Println(ok)
ok = td.Cmp(t, &got,
td.Map(&MyMap{}, td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()}),
"checks pointer on typed map %v", got)
fmt.Println(ok)
ok = td.Cmp(t, &got,
td.Map((*MyMap)(nil), td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()}),
"checks pointer on typed map %v", got)
fmt.Println(ok)
// Output:
// true
// true
// true
// true
}
func ExampleMapEach_map() {
t := &testing.T{}
got := map[string]int{"foo": 12, "bar": 42, "zip": 89}
ok := td.Cmp(t, got, td.MapEach(td.Between(10, 90)),
"checks each value of map %v is in [10 .. 90]", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleMapEach_typedMap() {
t := &testing.T{}
type MyMap map[string]int
got := MyMap{"foo": 12, "bar": 42, "zip": 89}
ok := td.Cmp(t, got, td.MapEach(td.Between(10, 90)),
"checks each value of typed map %v is in [10 .. 90]", got)
fmt.Println(ok)
ok = td.Cmp(t, &got, td.MapEach(td.Between(10, 90)),
"checks each value of typed map pointer %v is in [10 .. 90]", got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleN() {
t := &testing.T{}
got := 1.12345
ok := td.Cmp(t, got, td.N(1.1234, 0.00006),
"checks %v = 1.1234 ± 0.00006", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleNaN_float32() {
t := &testing.T{}
got := float32(math.NaN())
ok := td.Cmp(t, got, td.NaN(),
"checks %v is not-a-number", got)
fmt.Println("float32(math.NaN()) is float32 not-a-number:", ok)
got = 12
ok = td.Cmp(t, got, td.NaN(),
"checks %v is not-a-number", got)
fmt.Println("float32(12) is float32 not-a-number:", ok)
// Output:
// float32(math.NaN()) is float32 not-a-number: true
// float32(12) is float32 not-a-number: false
}
func ExampleNaN_float64() {
t := &testing.T{}
got := math.NaN()
ok := td.Cmp(t, got, td.NaN(),
"checks %v is not-a-number", got)
fmt.Println("math.NaN() is not-a-number:", ok)
got = 12
ok = td.Cmp(t, got, td.NaN(),
"checks %v is not-a-number", got)
fmt.Println("float64(12) is not-a-number:", ok)
// Output:
// math.NaN() is not-a-number: true
// float64(12) is not-a-number: false
}
func ExampleNil() {
t := &testing.T{}
var got fmt.Stringer // interface
// nil value can be compared directly with nil, no need of Nil() here
ok := td.Cmp(t, got, nil)
fmt.Println(ok)
// But it works with Nil() anyway
ok = td.Cmp(t, got, td.Nil())
fmt.Println(ok)
got = (*bytes.Buffer)(nil)
// In the case of an interface containing a nil pointer, comparing
// with nil fails, as the interface is not nil
ok = td.Cmp(t, got, nil)
fmt.Println(ok)
// In this case Nil() succeed
ok = td.Cmp(t, got, td.Nil())
fmt.Println(ok)
// Output:
// true
// true
// false
// true
}
func ExampleNone() {
t := &testing.T{}
got := 18
ok := td.Cmp(t, got, td.None(0, 10, 20, 30, td.Between(100, 199)),
"checks %v is non-null, and ≠ 10, 20 & 30, and not in [100-199]", got)
fmt.Println(ok)
got = 20
ok = td.Cmp(t, got, td.None(0, 10, 20, 30, td.Between(100, 199)),
"checks %v is non-null, and ≠ 10, 20 & 30, and not in [100-199]", got)
fmt.Println(ok)
got = 142
ok = td.Cmp(t, got, td.None(0, 10, 20, 30, td.Between(100, 199)),
"checks %v is non-null, and ≠ 10, 20 & 30, and not in [100-199]", got)
fmt.Println(ok)
prime := td.Flatten([]int{1, 2, 3, 5, 7, 11, 13})
even := td.Flatten([]int{2, 4, 6, 8, 10, 12, 14})
for _, got := range [...]int{9, 3, 8, 15} {
ok = td.Cmp(t, got, td.None(prime, even, td.Gt(14)),
"checks %v is not prime number, nor an even number and not > 14")
fmt.Printf("%d → %t\n", got, ok)
}
// Output:
// true
// false
// false
// 9 → true
// 3 → false
// 8 → false
// 15 → false
}
func ExampleNotAny() {
t := &testing.T{}
got := []int{4, 5, 9, 42}
ok := td.Cmp(t, got, td.NotAny(3, 6, 8, 41, 43),
"checks %v contains no item listed in NotAny()", got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.NotAny(3, 6, 8, 42, 43),
"checks %v contains no item listed in NotAny()", got)
fmt.Println(ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using notExpected... without copying it to a new
// []any slice, then use td.Flatten!
notExpected := []int{3, 6, 8, 41, 43}
ok = td.Cmp(t, got, td.NotAny(td.Flatten(notExpected)),
"checks %v contains no item listed in notExpected", got)
fmt.Println(ok)
// Output:
// true
// false
// true
}
func ExampleNot() {
t := &testing.T{}
got := 42
ok := td.Cmp(t, got, td.Not(0), "checks %v is non-null", got)
fmt.Println(ok)
ok = td.Cmp(t, got, td.Not(td.Between(10, 30)),
"checks %v is not in [10 .. 30]", got)
fmt.Println(ok)
got = 0
ok = td.Cmp(t, got, td.Not(0), "checks %v is non-null", got)
fmt.Println(ok)
// Output:
// true
// true
// false
}
func ExampleNotEmpty() {
t := &testing.T{}
ok := td.Cmp(t, nil, td.NotEmpty()) // fails, as nil is considered empty
fmt.Println(ok)
ok = td.Cmp(t, "foobar", td.NotEmpty())
fmt.Println(ok)
// Fails as 0 is a number, so not empty. Use NotZero() instead
ok = td.Cmp(t, 0, td.NotEmpty())
fmt.Println(ok)
ok = td.Cmp(t, map[string]int{"foobar": 42}, td.NotEmpty())
fmt.Println(ok)
ok = td.Cmp(t, []int{1}, td.NotEmpty())
fmt.Println(ok)
ok = td.Cmp(t, [3]int{}, td.NotEmpty()) // succeeds, NotEmpty() is not NotZero()!
fmt.Println(ok)
// Output:
// false
// true
// false
// true
// true
// true
}
func ExampleNotEmpty_pointers() {
t := &testing.T{}
type MySlice []int
ok := td.Cmp(t, MySlice{12}, td.NotEmpty())
fmt.Println(ok)
ok = td.Cmp(t, &MySlice{12}, td.NotEmpty()) // Ptr() not needed
fmt.Println(ok)
l1 := &MySlice{12}
l2 := &l1
l3 := &l2
ok = td.Cmp(t, &l3, td.NotEmpty())
fmt.Println(ok)
// Works the same for array, map, channel and string
// But not for others types as:
type MyStruct struct {
Value int
}
ok = td.Cmp(t, &MyStruct{}, td.NotEmpty()) // fails, use NotZero() instead
fmt.Println(ok)
// Output:
// true
// true
// true
// false
}
func ExampleNotNaN_float32() {
t := &testing.T{}
got := float32(math.NaN())
ok := td.Cmp(t, got, td.NotNaN(),
"checks %v is not-a-number", got)
fmt.Println("float32(math.NaN()) is NOT float32 not-a-number:", ok)
got = 12
ok = td.Cmp(t, got, td.NotNaN(),
"checks %v is not-a-number", got)
fmt.Println("float32(12) is NOT float32 not-a-number:", ok)
// Output:
// float32(math.NaN()) is NOT float32 not-a-number: false
// float32(12) is NOT float32 not-a-number: true
}
func ExampleNotNaN_float64() {
t := &testing.T{}
got := math.NaN()
ok := td.Cmp(t, got, td.NotNaN(),
"checks %v is NOT not-a-number", got)
fmt.Println("math.NaN() is NOT not-a-number:", ok)
got = 12
ok = td.Cmp(t, got, td.NotNaN(),
"checks %v is NOT not-a-number", got)
fmt.Println("float64(12) is NOT not-a-number:", ok)
// Output:
// math.NaN() is NOT not-a-number: false
// float64(12) is NOT not-a-number: true
}
func ExampleNotNil() {
t := &testing.T{}
var got fmt.Stringer = &bytes.Buffer{}
// nil value can be compared directly with Not(nil), no need of NotNil() here
ok := td.Cmp(t, got, td.Not(nil))
fmt.Println(ok)
// But it works with NotNil() anyway
ok = td.Cmp(t, got, td.NotNil())
fmt.Println(ok)
got = (*bytes.Buffer)(nil)
// In the case of an interface containing a nil pointer, comparing
// with Not(nil) succeeds, as the interface is not nil
ok = td.Cmp(t, got, td.Not(nil))
fmt.Println(ok)
// In this case NotNil() fails
ok = td.Cmp(t, got, td.NotNil())
fmt.Println(ok)
// Output:
// true
// true
// true
// false
}
func ExampleNotZero() {
t := &testing.T{}
ok := td.Cmp(t, 0, td.NotZero()) // fails
fmt.Println(ok)
ok = td.Cmp(t, float64(0), td.NotZero()) // fails
fmt.Println(ok)
ok = td.Cmp(t, 12, td.NotZero())
fmt.Println(ok)
ok = td.Cmp(t, (map[string]int)(nil), td.NotZero()) // fails, as nil
fmt.Println(ok)
ok = td.Cmp(t, map[string]int{}, td.NotZero()) // succeeds, as not nil
fmt.Println(ok)
ok = td.Cmp(t, ([]int)(nil), td.NotZero()) // fails, as nil
fmt.Println(ok)
ok = td.Cmp(t, []int{}, td.NotZero()) // succeeds, as not nil
fmt.Println(ok)
ok = td.Cmp(t, [3]int{}, td.NotZero()) // fails
fmt.Println(ok)
ok = td.Cmp(t, [3]int{0, 1}, td.NotZero()) // succeeds, DATA[1] is not 0
fmt.Println(ok)
ok = td.Cmp(t, bytes.Buffer{}, td.NotZero()) // fails
fmt.Println(ok)
ok = td.Cmp(t, &bytes.Buffer{}, td.NotZero()) // succeeds, as pointer not nil
fmt.Println(ok)
ok = td.Cmp(t, &bytes.Buffer{}, td.Ptr(td.NotZero())) // fails as deref by Ptr()
fmt.Println(ok)
// Output:
// false
// false
// true
// false
// true
// false
// true
// false
// true
// false
// true
// false
}
func ExamplePPtr() {
t := &testing.T{}
num := 12
got := &num
ok := td.Cmp(t, &got, td.PPtr(12))
fmt.Println(ok)
ok = td.Cmp(t, &got, td.PPtr(td.Between(4, 15)))
fmt.Println(ok)
// Output:
// true
// true
}
func ExamplePtr() {
t := &testing.T{}
got := 12
ok := td.Cmp(t, &got, td.Ptr(12))
fmt.Println(ok)
ok = td.Cmp(t, &got, td.Ptr(td.Between(4, 15)))
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleRe() {
t := &testing.T{}
got := "foo bar"
ok := td.Cmp(t, got, td.Re("(zip|bar)$"), "checks value %s", got)
fmt.Println(ok)
got = "bar foo"
ok = td.Cmp(t, got, td.Re("(zip|bar)$"), "checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleRe_stringer() {
t := &testing.T{}
// bytes.Buffer implements fmt.Stringer
got := bytes.NewBufferString("foo bar")
ok := td.Cmp(t, got, td.Re("(zip|bar)$"), "checks value %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleRe_error() {
t := &testing.T{}
got := errors.New("foo bar")
ok := td.Cmp(t, got, td.Re("(zip|bar)$"), "checks value %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleRe_capture() {
t := &testing.T{}
got := "foo bar biz"
ok := td.Cmp(t, got, td.Re(`^(\w+) (\w+) (\w+)$`, td.Set("biz", "foo", "bar")),
"checks value %s", got)
fmt.Println(ok)
got = "foo bar! biz"
ok = td.Cmp(t, got, td.Re(`^(\w+) (\w+) (\w+)$`, td.Set("biz", "foo", "bar")),
"checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleRe_multilines() {
t := &testing.T{}
got := `multi
lines
probably
more
than 4
`
expectedRe := `^multi
lines?
(probably|possibly)
more
than \d+
\z`
ok := td.Cmp(t, got, td.Re(expectedRe))
fmt.Println("Raw multi-lines string matches:", ok)
// But for strings with many, many, many lines, when the regexp
// doesn't match, it is sometimes difficult to see where the regexp
// failed in the string. Here td.List & td.Flatten can help to apply
// regexp line per line (note expectedRe is not anchored anymore):
expectedRe = `multi
lines?
(probably|possibly)
more
than \d+
`
ok = td.Cmp(t,
strings.Split(got, "\n"),
td.List(td.Flatten(strings.Split(expectedRe, "\n"),
func(line string) any {
return td.Re(`^` + line + `\z`)
})))
fmt.Println("All string lines match:", ok)
// Output:
// Raw multi-lines string matches: true
// All string lines match: true
}
func ExampleReAll_capture() {
t := &testing.T{}
got := "foo bar biz"
ok := td.Cmp(t, got, td.ReAll(`(\w+)`, td.Set("biz", "foo", "bar")),
"checks value %s", got)
fmt.Println(ok)
// Matches, but all catured groups do not match Set
got = "foo BAR biz"
ok = td.Cmp(t, got, td.ReAll(`(\w+)`, td.Set("biz", "foo", "bar")),
"checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleReAll_captureComplex() {
t := &testing.T{}
got := "11 45 23 56 85 96"
ok := td.Cmp(t, got,
td.ReAll(`(\d+)`, td.ArrayEach(td.Code(func(num string) bool {
n, err := strconv.Atoi(num)
return err == nil && n > 10 && n < 100
}))),
"checks value %s", got)
fmt.Println(ok)
// Matches, but 11 is not greater than 20
ok = td.Cmp(t, got,
td.ReAll(`(\d+)`, td.ArrayEach(td.Code(func(num string) bool {
n, err := strconv.Atoi(num)
return err == nil && n > 20 && n < 100
}))),
"checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleRe_compiled() {
t := &testing.T{}
expected := regexp.MustCompile("(zip|bar)$")
got := "foo bar"
ok := td.Cmp(t, got, td.Re(expected), "checks value %s", got)
fmt.Println(ok)
got = "bar foo"
ok = td.Cmp(t, got, td.Re(expected), "checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleRe_compiledStringer() {
t := &testing.T{}
expected := regexp.MustCompile("(zip|bar)$")
// bytes.Buffer implements fmt.Stringer
got := bytes.NewBufferString("foo bar")
ok := td.Cmp(t, got, td.Re(expected), "checks value %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleRe_compiledError() {
t := &testing.T{}
expected := regexp.MustCompile("(zip|bar)$")
got := errors.New("foo bar")
ok := td.Cmp(t, got, td.Re(expected), "checks value %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleRe_compiledCapture() {
t := &testing.T{}
expected := regexp.MustCompile(`^(\w+) (\w+) (\w+)$`)
got := "foo bar biz"
ok := td.Cmp(t, got, td.Re(expected, td.Set("biz", "foo", "bar")),
"checks value %s", got)
fmt.Println(ok)
got = "foo bar! biz"
ok = td.Cmp(t, got, td.Re(expected, td.Set("biz", "foo", "bar")),
"checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleReAll_compiledCapture() {
t := &testing.T{}
expected := regexp.MustCompile(`(\w+)`)
got := "foo bar biz"
ok := td.Cmp(t, got, td.ReAll(expected, td.Set("biz", "foo", "bar")),
"checks value %s", got)
fmt.Println(ok)
// Matches, but all catured groups do not match Set
got = "foo BAR biz"
ok = td.Cmp(t, got, td.ReAll(expected, td.Set("biz", "foo", "bar")),
"checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleReAll_compiledCaptureComplex() {
t := &testing.T{}
expected := regexp.MustCompile(`(\d+)`)
got := "11 45 23 56 85 96"
ok := td.Cmp(t, got,
td.ReAll(expected, td.ArrayEach(td.Code(func(num string) bool {
n, err := strconv.Atoi(num)
return err == nil && n > 10 && n < 100
}))),
"checks value %s", got)
fmt.Println(ok)
// Matches, but 11 is not greater than 20
ok = td.Cmp(t, got,
td.ReAll(expected, td.ArrayEach(td.Code(func(num string) bool {
n, err := strconv.Atoi(num)
return err == nil && n > 20 && n < 100
}))),
"checks value %s", got)
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleRecv_basic() {
t := &testing.T{}
got := make(chan int, 3)
ok := td.Cmp(t, got, td.Recv(td.RecvNothing))
fmt.Println("nothing to receive:", ok)
got <- 1
got <- 2
got <- 3
close(got)
ok = td.Cmp(t, got, td.Recv(1))
fmt.Println("1st receive is 1:", ok)
ok = td.Cmp(t, got, td.All(
td.Recv(2),
td.Recv(td.Between(3, 4)),
td.Recv(td.RecvClosed),
))
fmt.Println("next receives are 2, 3 then closed:", ok)
ok = td.Cmp(t, got, td.Recv(td.RecvNothing))
fmt.Println("nothing to receive:", ok)
// Output:
// nothing to receive: true
// 1st receive is 1: true
// next receives are 2, 3 then closed: true
// nothing to receive: false
}
func ExampleRecv_channelPointer() {
t := &testing.T{}
got := make(chan int, 3)
ok := td.Cmp(t, got, td.Recv(td.RecvNothing))
fmt.Println("nothing to receive:", ok)
got <- 1
got <- 2
got <- 3
close(got)
ok = td.Cmp(t, &got, td.Recv(1))
fmt.Println("1st receive is 1:", ok)
ok = td.Cmp(t, &got, td.All(
td.Recv(2),
td.Recv(td.Between(3, 4)),
td.Recv(td.RecvClosed),
))
fmt.Println("next receives are 2, 3 then closed:", ok)
ok = td.Cmp(t, got, td.Recv(td.RecvNothing))
fmt.Println("nothing to receive:", ok)
// Output:
// nothing to receive: true
// 1st receive is 1: true
// next receives are 2, 3 then closed: true
// nothing to receive: false
}
func ExampleRecv_withTimeout() {
t := &testing.T{}
got := make(chan int, 1)
tick := make(chan struct{})
go func() {
// ①
<-tick
time.Sleep(100 * time.Millisecond)
got <- 0
// ②
<-tick
time.Sleep(100 * time.Millisecond)
got <- 1
// ③
<-tick
time.Sleep(100 * time.Millisecond)
close(got)
}()
td.Cmp(t, got, td.Recv(td.RecvNothing))
// ①
tick <- struct{}{}
ok := td.Cmp(t, got, td.Recv(td.RecvNothing))
fmt.Println("① RecvNothing:", ok)
ok = td.Cmp(t, got, td.Recv(0, 150*time.Millisecond))
fmt.Println("① receive 0 w/150ms timeout:", ok)
ok = td.Cmp(t, got, td.Recv(td.RecvNothing))
fmt.Println("① RecvNothing:", ok)
// ②
tick <- struct{}{}
ok = td.Cmp(t, got, td.Recv(td.RecvNothing))
fmt.Println("② RecvNothing:", ok)
ok = td.Cmp(t, got, td.Recv(1, 150*time.Millisecond))
fmt.Println("② receive 1 w/150ms timeout:", ok)
ok = td.Cmp(t, got, td.Recv(td.RecvNothing))
fmt.Println("② RecvNothing:", ok)
// ③
tick <- struct{}{}
ok = td.Cmp(t, got, td.Recv(td.RecvNothing))
fmt.Println("③ RecvNothing:", ok)
ok = td.Cmp(t, got, td.Recv(td.RecvClosed, 150*time.Millisecond))
fmt.Println("③ check closed w/150ms timeout:", ok)
// Output:
// ① RecvNothing: true
// ① receive 0 w/150ms timeout: true
// ① RecvNothing: true
// ② RecvNothing: true
// ② receive 1 w/150ms timeout: true
// ② RecvNothing: true
// ③ RecvNothing: true
// ③ check closed w/150ms timeout: true
}
func ExampleRecv_nilChannel() {
t := &testing.T{}
var ch chan int
ok := td.Cmp(t, ch, td.Recv(td.RecvNothing))
fmt.Println("nothing to receive from nil channel:", ok)
ok = td.Cmp(t, ch, td.Recv(42))
fmt.Println("something to receive from nil channel:", ok)
ok = td.Cmp(t, ch, td.Recv(td.RecvClosed))
fmt.Println("is a nil channel closed:", ok)
// Output:
// nothing to receive from nil channel: true
// something to receive from nil channel: false
// is a nil channel closed: false
}
func ExampleSet() {
t := &testing.T{}
got := []int{1, 3, 5, 8, 8, 1, 2}
// Matches as all items are present, ignoring duplicates
ok := td.Cmp(t, got, td.Set(1, 2, 3, 5, 8),
"checks all items are present, in any order")
fmt.Println(ok)
// Duplicates are ignored in a Set
ok = td.Cmp(t, got, td.Set(1, 2, 2, 2, 2, 2, 3, 5, 8),
"checks all items are present, in any order")
fmt.Println(ok)
// Tries its best not to raise an error when a value can be matched
// by several Set entries
ok = td.Cmp(t, got, td.Set(td.Between(1, 4), 3, td.Between(2, 10)),
"checks all items are present, in any order")
fmt.Println(ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using expected... without copying it to a new
// []any slice, then use td.Flatten!
expected := []int{1, 2, 3, 5, 8}
ok = td.Cmp(t, got, td.Set(td.Flatten(expected)),
"checks all expected items are present, in any order")
fmt.Println(ok)
// Output:
// true
// true
// true
// true
}
func ExampleShallow() {
t := &testing.T{}
type MyStruct struct {
Value int
}
data := MyStruct{Value: 12}
got := &data
ok := td.Cmp(t, got, td.Shallow(&data),
"checks pointers only, not contents")
fmt.Println(ok)
// Same contents, but not same pointer
ok = td.Cmp(t, got, td.Shallow(&MyStruct{Value: 12}),
"checks pointers only, not contents")
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleShallow_slice() {
t := &testing.T{}
back := []int{1, 2, 3, 1, 2, 3}
a := back[:3]
b := back[3:]
ok := td.Cmp(t, a, td.Shallow(back))
fmt.Println("are ≠ but share the same area:", ok)
ok = td.Cmp(t, b, td.Shallow(back))
fmt.Println("are = but do not point to same area:", ok)
// Output:
// are ≠ but share the same area: true
// are = but do not point to same area: false
}
func ExampleShallow_string() {
t := &testing.T{}
back := "foobarfoobar"
a := back[:6]
b := back[6:]
ok := td.Cmp(t, a, td.Shallow(back))
fmt.Println("are ≠ but share the same area:", ok)
ok = td.Cmp(t, b, td.Shallow(a))
fmt.Println("are = but do not point to same area:", ok)
// Output:
// are ≠ but share the same area: true
// are = but do not point to same area: false
}
func ExampleSlice_slice() {
t := &testing.T{}
got := []int{42, 58, 26}
ok := td.Cmp(t, got, td.Slice([]int{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}),
"checks slice %v", got)
fmt.Println(ok)
ok = td.Cmp(t, got,
td.Slice([]int{}, td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}),
"checks slice %v", got)
fmt.Println(ok)
ok = td.Cmp(t, got,
td.Slice(([]int)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}),
"checks slice %v", got)
fmt.Println(ok)
// Output:
// true
// true
// true
}
func ExampleSlice_typedSlice() {
t := &testing.T{}
type MySlice []int
got := MySlice{42, 58, 26}
ok := td.Cmp(t, got, td.Slice(MySlice{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}),
"checks typed slice %v", got)
fmt.Println(ok)
ok = td.Cmp(t, &got, td.Slice(&MySlice{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}),
"checks pointer on typed slice %v", got)
fmt.Println(ok)
ok = td.Cmp(t, &got,
td.Slice(&MySlice{}, td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}),
"checks pointer on typed slice %v", got)
fmt.Println(ok)
ok = td.Cmp(t, &got,
td.Slice((*MySlice)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}),
"checks pointer on typed slice %v", got)
fmt.Println(ok)
// Output:
// true
// true
// true
// true
}
func ExampleSort_basic() {
t := &testing.T{}
got := []int{-1, 1, 2, -3, 3, -2, 0}
// Generic ascending order (≥0 or nil)
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(0, []int{-3, -2, -1, 0, 1, 2, 3}))
fmt.Println("asc order:", ok)
ok = td.Cmp(t, got, td.Sort(nil, []int{-3, -2, -1, 0, 1, 2, 3}))
fmt.Println("asc order:", ok)
// Generic descending order (< 0)
ok = td.Cmp(t, got, td.Sort(-1, []int{3, 2, 1, 0, -1, -2, -3}))
fmt.Println("desc order:", ok)
evenHigher := func(a, b int) bool {
if (a%2 == 0) != (b%2 == 0) {
return a%2 != 0
}
return a < b
}
ok = td.Cmp(t, got, td.Sort(evenHigher, []int{-3, -1, 1, 3, -2, 0, 2}))
fmt.Println("even higher order:", ok)
// Output:
// asc order: true
// asc order: true
// asc order: true
// desc order: true
// even higher order: true
}
func ExampleSort_fields_path() {
t := &testing.T{}
type Person struct {
Name string
Age int
}
brian := Person{Name: "Brian", Age: 22}
bob := Person{Name: "Bob", Age: 19}
stephen := Person{Name: "Stephen", Age: 19}
alice := Person{Name: "Alice", Age: 20}
marcel := Person{Name: "Marcel", Age: 25}
got := []Person{brian, bob, stephen, alice, marcel}
ok := td.Cmp(t, got,
td.Sort("Name", []Person{alice, bob, brian, marcel, stephen}))
fmt.Println("by name asc:", ok)
ok = td.Cmp(t, got,
td.Sort("-Name", []Person{stephen, marcel, brian, bob, alice}))
fmt.Println("by name desc:", ok)
ok = td.Cmp(t, got,
td.Sort([]string{"-Age", "Name"}, []Person{marcel, brian, alice, bob, stephen}))
fmt.Println("by age desc, then by name asc:", ok)
type A struct{ props map[string]int }
p12 := A{props: map[string]int{"priority": 12}}
p23 := A{props: map[string]int{"priority": 23}}
p34 := A{props: map[string]int{"priority": 34}}
got2 := []A{p23, p12, p34}
ok = td.Cmp(t, got2, td.Sort(`-props[priority]`, []A{p34, p23, p12}))
fmt.Println("by priority desc:", ok)
ok = td.Cmp(t, got2, td.Sort(`props.priority`, []A{p12, p23, p34}))
fmt.Println("by priority asc:", ok)
// Output:
// by name asc: true
// by name desc: true
// by age desc, then by name asc: true
// by priority desc: true
// by priority asc: 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 (1):", ok)
ok = td.Cmp(t, got, td.Sorted(0))
fmt.Println("is asc order (0):", ok)
ok = td.Cmp(t, got, td.Sorted(nil))
fmt.Println("is asc order (nil):", ok)
ok = td.Cmp(t, got, td.Sorted(-1))
fmt.Println("is desc order:", ok)
got = []int{-3, -1, 1, 3, -2, 0, 2}
evenHigher := func(a, b int) bool {
if (a%2 == 0) != (b%2 == 0) {
return a%2 != 0
}
return a < b
}
ok = td.Cmp(t, got, td.Sorted(evenHigher))
fmt.Println("is even higher order:", ok)
// Output:
// is asc order (default): true
// is asc order (1): true
// is asc order (0): true
// is asc order (nil): true
// is desc order: false
// is even higher order: true
}
func ExampleSorted_fields_path() {
t := &testing.T{}
type Person struct {
Name string
Age int
}
alice := Person{Name: "Alice", Age: 20}
bob := Person{Name: "Bob", Age: 19}
brian := Person{Name: "Brian", Age: 22}
marcel := Person{Name: "Marcel", Age: 25}
stephen := Person{Name: "Stephen", Age: 19}
got := []Person{alice, bob, brian, marcel, stephen}
ok := td.Cmp(t, got, td.Sorted("Name"))
fmt.Println("is sorted by name asc:", ok)
got = []Person{stephen, marcel, brian, bob, alice}
ok = td.Cmp(t, got, td.Sorted("-Name"))
fmt.Println("is sorted by name desc:", ok)
got = []Person{marcel, brian, alice, bob, stephen}
ok = td.Cmp(t, got, td.Sorted("-Age", "Name"))
fmt.Println("is sorted by age desc, then by name asc 1:", ok)
got = []Person{marcel, brian, alice, stephen, bob}
ok = td.Cmp(t, got, td.Sorted("-Age", "Name"))
fmt.Println("is sorted by age desc, then by name asc 2:", ok)
type A struct{ props map[string]int }
p12 := A{props: map[string]int{"priority": 12}}
p23 := A{props: map[string]int{"priority": 23}}
p34 := A{props: map[string]int{"priority": 34}}
got2 := []A{p34, p23, p12}
ok = td.Cmp(t, got2, td.Sorted(`-props[priority]`))
fmt.Println("is sorted by priority desc:", ok)
ok = td.Cmp(t, got2, td.Sorted(`props.priority`))
fmt.Println("is sorted by priority asc:", ok)
// Output:
// is sorted by name asc: true
// is sorted by name desc: true
// is sorted by age desc, then by name asc 1: true
// is sorted by age desc, then by name asc 2: false
// is sorted by priority desc: true
// is sorted by priority asc: false
}
func ExampleSuperSliceOf_array() {
t := &testing.T{}
got := [4]int{42, 58, 26, 666}
ok := td.Cmp(t, got,
td.SuperSliceOf([4]int{1: 58}, td.ArrayEntries{3: td.Gt(660)}),
"checks array %v", got)
fmt.Println("Only check items #1 & #3:", ok)
ok = td.Cmp(t, got,
td.SuperSliceOf([4]int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}),
"checks array %v", got)
fmt.Println("Only check items #0 & #3:", ok)
ok = td.Cmp(t, &got,
td.SuperSliceOf(&[4]int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}),
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of an array pointer:", ok)
ok = td.Cmp(t, &got,
td.SuperSliceOf((*[4]int)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)}),
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of an array pointer, using nil model:", ok)
// Output:
// Only check items #1 & #3: true
// Only check items #0 & #3: true
// Only check items #0 & #3 of an array pointer: true
// Only check items #0 & #3 of an array pointer, using nil model: true
}
func ExampleSuperSliceOf_typedArray() {
t := &testing.T{}
type MyArray [4]int
got := MyArray{42, 58, 26, 666}
ok := td.Cmp(t, got,
td.SuperSliceOf(MyArray{1: 58}, td.ArrayEntries{3: td.Gt(660)}),
"checks typed array %v", got)
fmt.Println("Only check items #1 & #3:", ok)
ok = td.Cmp(t, got,
td.SuperSliceOf(MyArray{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}),
"checks array %v", got)
fmt.Println("Only check items #0 & #3:", ok)
ok = td.Cmp(t, &got,
td.SuperSliceOf(&MyArray{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}),
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of an array pointer:", ok)
ok = td.Cmp(t, &got,
td.SuperSliceOf((*MyArray)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)}),
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of an array pointer, using nil model:", ok)
// Output:
// Only check items #1 & #3: true
// Only check items #0 & #3: true
// Only check items #0 & #3 of an array pointer: true
// Only check items #0 & #3 of an array pointer, using nil model: true
}
func ExampleSuperSliceOf_slice() {
t := &testing.T{}
got := []int{42, 58, 26, 666}
ok := td.Cmp(t, got,
td.SuperSliceOf([]int{1: 58}, td.ArrayEntries{3: td.Gt(660)}),
"checks array %v", got)
fmt.Println("Only check items #1 & #3:", ok)
ok = td.Cmp(t, got,
td.SuperSliceOf([]int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}),
"checks array %v", got)
fmt.Println("Only check items #0 & #3:", ok)
ok = td.Cmp(t, &got,
td.SuperSliceOf(&[]int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}),
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of a slice pointer:", ok)
ok = td.Cmp(t, &got,
td.SuperSliceOf((*[]int)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)}),
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of a slice pointer, using nil model:", ok)
// Output:
// Only check items #1 & #3: true
// Only check items #0 & #3: true
// Only check items #0 & #3 of a slice pointer: true
// Only check items #0 & #3 of a slice pointer, using nil model: true
}
func ExampleSuperSliceOf_typedSlice() {
t := &testing.T{}
type MySlice []int
got := MySlice{42, 58, 26, 666}
ok := td.Cmp(t, got,
td.SuperSliceOf(MySlice{1: 58}, td.ArrayEntries{3: td.Gt(660)}),
"checks typed array %v", got)
fmt.Println("Only check items #1 & #3:", ok)
ok = td.Cmp(t, got,
td.SuperSliceOf(MySlice{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}),
"checks array %v", got)
fmt.Println("Only check items #0 & #3:", ok)
ok = td.Cmp(t, &got,
td.SuperSliceOf(&MySlice{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}),
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of a slice pointer:", ok)
ok = td.Cmp(t, &got,
td.SuperSliceOf((*MySlice)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)}),
"checks array %v", got)
fmt.Println("Only check items #0 & #3 of a slice pointer, using nil model:", ok)
// Output:
// Only check items #1 & #3: true
// Only check items #0 & #3: true
// Only check items #0 & #3 of a slice pointer: true
// Only check items #0 & #3 of a slice pointer, using nil model: true
}
func ExampleSmuggle_convert() {
t := &testing.T{}
got := int64(123)
ok := td.Cmp(t, got,
td.Smuggle(func(n int64) int { return int(n) }, 123),
"checks int64 got against an int value")
fmt.Println(ok)
ok = td.Cmp(t, "123",
td.Smuggle(
func(numStr string) (int, bool) {
n, err := strconv.Atoi(numStr)
return n, err == nil
},
td.Between(120, 130)),
"checks that number in %#v is in [120 .. 130]")
fmt.Println(ok)
ok = td.Cmp(t, "123",
td.Smuggle(
func(numStr string) (int, bool, string) {
n, err := strconv.Atoi(numStr)
if err != nil {
return 0, false, "string must contain a number"
}
return n, true, ""
},
td.Between(120, 130)),
"checks that number in %#v is in [120 .. 130]")
fmt.Println(ok)
ok = td.Cmp(t, "123",
td.Smuggle(
func(numStr string) (int, error) { //nolint: gocritic
return strconv.Atoi(numStr)
},
td.Between(120, 130)),
"checks that number in %#v is in [120 .. 130]")
fmt.Println(ok)
// Short version :)
ok = td.Cmp(t, "123",
td.Smuggle(strconv.Atoi, td.Between(120, 130)),
"checks that number in %#v is in [120 .. 130]")
fmt.Println(ok)
// Output:
// true
// true
// true
// true
// true
}
func ExampleSmuggle_lax() {
t := &testing.T{}
// got is an int16 and Smuggle func input is an int64: it is OK
got := int(123)
ok := td.Cmp(t, got,
td.Smuggle(func(n int64) uint32 { return uint32(n) }, uint32(123)))
fmt.Println("got int16(123) → smuggle via int64 → uint32(123):", ok)
// Output:
// got int16(123) → smuggle via int64 → uint32(123): true
}
func ExampleSmuggle_auto_unmarshal() {
t := &testing.T{}
// Automatically json.Unmarshal to compare
got := []byte(`{"a":1,"b":2}`)
ok := td.Cmp(t, got,
td.Smuggle(
func(b json.RawMessage) (r map[string]int, err error) {
err = json.Unmarshal(b, &r)
return
},
map[string]int{
"a": 1,
"b": 2,
}))
fmt.Println("JSON contents is OK:", ok)
// Output:
// JSON contents is OK: true
}
func ExampleSmuggle_cast() {
t := &testing.T{}
// A string containing JSON
got := `{ "foo": 123 }`
// Automatically cast a string to a json.RawMessage so td.JSON can operate
ok := td.Cmp(t, got,
td.Smuggle(json.RawMessage{}, td.JSON(`{"foo":123}`)))
fmt.Println("JSON contents in string is OK:", ok)
// Automatically read from io.Reader to a json.RawMessage
ok = td.Cmp(t, bytes.NewReader([]byte(got)),
td.Smuggle(json.RawMessage{}, td.JSON(`{"foo":123}`)))
fmt.Println("JSON contents just read is OK:", ok)
// Output:
// JSON contents in string is OK: true
// JSON contents just read is OK: true
}
func ExampleSmuggle_complex() {
t := &testing.T{}
// No end date but a start date and a duration
type StartDuration struct {
StartDate time.Time
Duration time.Duration
}
// Checks that end date is between 17th and 19th February both at 0h
// for each of these durations in hours
for _, duration := range []time.Duration{48 * time.Hour, 72 * time.Hour, 96 * time.Hour} {
got := StartDuration{
StartDate: time.Date(2018, time.February, 14, 12, 13, 14, 0, time.UTC),
Duration: duration,
}
// Simplest way, but in case of Between() failure, error will be bound
// to DATA, not very clear...
ok := td.Cmp(t, got,
td.Smuggle(
func(sd StartDuration) time.Time {
return sd.StartDate.Add(sd.Duration)
},
td.Between(
time.Date(2018, time.February, 17, 0, 0, 0, 0, time.UTC),
time.Date(2018, time.February, 19, 0, 0, 0, 0, time.UTC))))
fmt.Println(ok)
// Name the computed value "ComputedEndDate" to render a Between() failure
// more understandable, so error will be bound to DATA.ComputedEndDate
ok = td.Cmp(t, got,
td.Smuggle(
func(sd StartDuration) td.SmuggledGot {
return td.SmuggledGot{
Name: "ComputedEndDate",
Got: sd.StartDate.Add(sd.Duration),
}
},
td.Between(
time.Date(2018, time.February, 17, 0, 0, 0, 0, time.UTC),
time.Date(2018, time.February, 19, 0, 0, 0, 0, time.UTC))))
fmt.Println(ok)
}
// Output:
// false
// false
// true
// true
// true
// true
}
func ExampleSmuggle_interface() {
t := &testing.T{}
gotTime, err := time.Parse(time.RFC3339, "2018-05-23T12:13:14Z")
if err != nil {
t.Fatal(err)
}
// Do not check the struct itself, but its stringified form
ok := td.Cmp(t, gotTime,
td.Smuggle(func(s fmt.Stringer) string {
return s.String()
},
"2018-05-23 12:13:14 +0000 UTC"))
fmt.Println("stringified time.Time OK:", ok)
// If got does not implement the fmt.Stringer interface, it fails
// without calling the Smuggle func
type MyTime time.Time
ok = td.Cmp(t, MyTime(gotTime),
td.Smuggle(func(s fmt.Stringer) string {
fmt.Println("Smuggle func called!")
return s.String()
},
"2018-05-23 12:13:14 +0000 UTC"))
fmt.Println("stringified MyTime OK:", ok)
// Output:
// stringified time.Time OK: true
// stringified MyTime OK: false
}
func ExampleSmuggle_field_path() {
t := &testing.T{}
type Body struct {
Name string
Value any
}
type Request struct {
Body *Body
}
type Transaction struct {
Request
}
type ValueNum struct {
Num int
}
got := &Transaction{
Request: Request{
Body: &Body{
Name: "test",
Value: &ValueNum{Num: 123},
},
},
}
// Want to check whether Num is between 100 and 200?
ok := td.Cmp(t, got,
td.Smuggle(
func(tr *Transaction) (int, error) {
if tr.Body == nil ||
tr.Body.Value == nil {
return 0, errors.New("Request.Body or Request.Body.Value is nil")
}
if v, ok := tr.Body.Value.(*ValueNum); ok && v != nil {
return v.Num, nil
}
return 0, errors.New("Request.Body.Value isn't *ValueNum or nil")
},
td.Between(100, 200)))
fmt.Println("check Num by hand:", ok)
// Same, but automagically generated...
ok = td.Cmp(t, got, td.Smuggle("Request.Body.Value.Num", td.Between(100, 200)))
fmt.Println("check Num using a fields-path:", ok)
// And as Request is an anonymous field, can be simplified further
// as it can be omitted
ok = td.Cmp(t, got, td.Smuggle("Body.Value.Num", td.Between(100, 200)))
fmt.Println("check Num using an other fields-path:", ok)
// Note that maps and array/slices are supported
got.Body.Value = map[string]any{
"foo": []any{
3: map[int]string{666: "bar"},
},
}
ok = td.Cmp(t, got, td.Smuggle("Body.Value[foo][3][666]", "bar"))
fmt.Println("check fields-path including maps/slices:", ok)
// Output:
// check Num by hand: true
// check Num using a fields-path: true
// check Num using an other fields-path: true
// check fields-path including maps/slices: true
}
func ExampleString() {
t := &testing.T{}
got := "foobar"
ok := td.Cmp(t, got, td.String("foobar"), "checks %s", got)
fmt.Println("using string:", ok)
ok = td.Cmp(t, []byte(got), td.String("foobar"), "checks %s", got)
fmt.Println("using []byte:", ok)
// Output:
// using string: true
// using []byte: true
}
func ExampleString_stringer() {
t := &testing.T{}
// bytes.Buffer implements fmt.Stringer
got := bytes.NewBufferString("foobar")
ok := td.Cmp(t, got, td.String("foobar"), "checks %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleString_error() {
t := &testing.T{}
got := errors.New("foobar")
ok := td.Cmp(t, got, td.String("foobar"), "checks %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleHasPrefix() {
t := &testing.T{}
got := "foobar"
ok := td.Cmp(t, got, td.HasPrefix("foo"), "checks %s", got)
fmt.Println("using string:", ok)
ok = td.Cmp(t, []byte(got), td.HasPrefix("foo"), "checks %s", got)
fmt.Println("using []byte:", ok)
// Output:
// using string: true
// using []byte: true
}
func ExampleHasPrefix_stringer() {
t := &testing.T{}
// bytes.Buffer implements fmt.Stringer
got := bytes.NewBufferString("foobar")
ok := td.Cmp(t, got, td.HasPrefix("foo"), "checks %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleHasPrefix_error() {
t := &testing.T{}
got := errors.New("foobar")
ok := td.Cmp(t, got, td.HasPrefix("foo"), "checks %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleHasSuffix() {
t := &testing.T{}
got := "foobar"
ok := td.Cmp(t, got, td.HasSuffix("bar"), "checks %s", got)
fmt.Println("using string:", ok)
ok = td.Cmp(t, []byte(got), td.HasSuffix("bar"), "checks %s", got)
fmt.Println("using []byte:", ok)
// Output:
// using string: true
// using []byte: true
}
func ExampleHasSuffix_stringer() {
t := &testing.T{}
// bytes.Buffer implements fmt.Stringer
got := bytes.NewBufferString("foobar")
ok := td.Cmp(t, got, td.HasSuffix("bar"), "checks %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleHasSuffix_error() {
t := &testing.T{}
got := errors.New("foobar")
ok := td.Cmp(t, got, td.HasSuffix("bar"), "checks %s", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleStruct() {
t := &testing.T{}
type Person struct {
Name string
Age int
NumChildren int
}
got := Person{
Name: "Foobar",
Age: 42,
NumChildren: 3,
}
// As NumChildren is zero in Struct() call, it is not checked
ok := td.Cmp(t, got,
td.Struct(Person{Name: "Foobar"}, td.StructFields{
"Age": td.Between(40, 50),
}),
"checks %v is the right Person")
fmt.Println("Foobar is between 40 & 50:", ok)
// Model can be empty
ok = td.Cmp(t, got,
td.Struct(Person{}, td.StructFields{
"Name": "Foobar",
"Age": td.Between(40, 50),
"NumChildren": td.Not(0),
}),
"checks %v is the right Person")
fmt.Println("Foobar has some children:", ok)
// Works with pointers too
ok = td.Cmp(t, &got,
td.Struct(&Person{}, td.StructFields{
"Name": "Foobar",
"Age": td.Between(40, 50),
"NumChildren": td.Not(0),
}),
"checks %v is the right Person")
fmt.Println("Foobar has some children (using pointer):", ok)
// Model does not need to be instanciated
ok = td.Cmp(t, &got,
td.Struct((*Person)(nil), td.StructFields{
"Name": "Foobar",
"Age": td.Between(40, 50),
"NumChildren": td.Not(0),
}),
"checks %v is the right Person")
fmt.Println("Foobar has some children (using nil model):", ok)
// Output:
// Foobar is between 40 & 50: true
// Foobar has some children: true
// Foobar has some children (using pointer): true
// Foobar has some children (using nil model): true
}
func ExampleStruct_overwrite_model() {
t := &testing.T{}
type Person struct {
Name string
Age int
NumChildren int
}
got := Person{
Name: "Foobar",
Age: 42,
NumChildren: 3,
}
ok := td.Cmp(t, got,
td.Struct(
Person{
Name: "Foobar",
Age: 53,
},
td.StructFields{
">Age": td.Between(40, 50), // ">" to overwrite Age:53 in model
"NumChildren": td.Gt(2),
}),
"checks %v is the right Person")
fmt.Println("Foobar is between 40 & 50:", ok)
ok = td.Cmp(t, got,
td.Struct(
Person{
Name: "Foobar",
Age: 53,
},
td.StructFields{
"> Age": td.Between(40, 50), // same, ">" can be followed by spaces
"NumChildren": td.Gt(2),
}),
"checks %v is the right Person")
fmt.Println("Foobar is between 40 & 50:", ok)
// Output:
// Foobar is between 40 & 50: true
// Foobar is between 40 & 50: true
}
func ExampleStruct_patterns() {
t := &testing.T{}
type Person struct {
Firstname string
Lastname string
Surname string
Nickname string
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
}
now := time.Now()
got := Person{
Firstname: "Maxime",
Lastname: "Foo",
Surname: "Max",
Nickname: "max",
CreatedAt: now,
UpdatedAt: now,
DeletedAt: nil, // not deleted yet
}
ok := td.Cmp(t, got,
td.Struct(Person{Lastname: "Foo"}, td.StructFields{
`DeletedAt`: nil,
`= *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model
`=~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt
}),
"mix shell & regexp patterns")
fmt.Println("Patterns match only remaining fields:", ok)
ok = td.Cmp(t, got,
td.Struct(Person{Lastname: "Foo"}, td.StructFields{
`DeletedAt`: nil,
`1 = *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model
`2 =~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt
}),
"ordered patterns")
fmt.Println("Ordered patterns match only remaining fields:", ok)
// Output:
// Patterns match only remaining fields: true
// Ordered patterns match only remaining fields: true
}
func ExampleStruct_struct_fields() { // only operator
t := &testing.T{}
type Person struct {
Name string
Age int
NumChildren int
}
got := Person{
Name: "Foobar",
Age: 42,
NumChildren: 3,
}
ok := td.Cmp(t, got, td.Struct(Person{Name: "Foobar"}), "no StructFields")
fmt.Println("Without any StructFields:", ok)
ok = td.Cmp(t, got,
td.Struct(Person{Name: "Bingo"},
td.StructFields{
"> Name": "pipo",
"Age": 42,
},
td.StructFields{
"> Name": "bingo",
"NumChildren": 10,
},
td.StructFields{
">Name": "Foobar",
"NumChildren": 3,
}),
"merge several StructFields")
fmt.Println("Merge several StructFields:", ok)
// Output:
// Without any StructFields: true
// Merge several StructFields: true
}
func ExampleStruct_lazy_model() {
t := &testing.T{}
got := struct {
name string
age int
}{
name: "Foobar",
age: 42,
}
ok := td.Cmp(t, got, td.Struct(nil, td.StructFields{
"name": "Foobar",
"age": td.Between(40, 45),
}))
fmt.Println("Lazy model:", ok)
ok = td.Cmp(t, got, td.Struct(nil, td.StructFields{
"name": "Foobar",
"zip": 666,
}))
fmt.Println("Lazy model with unknown field:", ok)
// Output:
// Lazy model: true
// Lazy model with unknown field: false
}
func ExampleSStruct() {
t := &testing.T{}
type Person struct {
Name string
Age int
NumChildren int
}
got := Person{
Name: "Foobar",
Age: 42,
NumChildren: 0,
}
// NumChildren is not listed in expected fields so it must be zero
ok := td.Cmp(t, got,
td.SStruct(Person{Name: "Foobar"}, td.StructFields{
"Age": td.Between(40, 50),
}),
"checks %v is the right Person")
fmt.Println("Foobar is between 40 & 50:", ok)
// Model can be empty
got.NumChildren = 3
ok = td.Cmp(t, got,
td.SStruct(Person{}, td.StructFields{
"Name": "Foobar",
"Age": td.Between(40, 50),
"NumChildren": td.Not(0),
}),
"checks %v is the right Person")
fmt.Println("Foobar has some children:", ok)
// Works with pointers too
ok = td.Cmp(t, &got,
td.SStruct(&Person{}, td.StructFields{
"Name": "Foobar",
"Age": td.Between(40, 50),
"NumChildren": td.Not(0),
}),
"checks %v is the right Person")
fmt.Println("Foobar has some children (using pointer):", ok)
// Model does not need to be instanciated
ok = td.Cmp(t, &got,
td.SStruct((*Person)(nil), td.StructFields{
"Name": "Foobar",
"Age": td.Between(40, 50),
"NumChildren": td.Not(0),
}),
"checks %v is the right Person")
fmt.Println("Foobar has some children (using nil model):", ok)
// Output:
// Foobar is between 40 & 50: true
// Foobar has some children: true
// Foobar has some children (using pointer): true
// Foobar has some children (using nil model): true
}
func ExampleSStruct_overwrite_model() {
t := &testing.T{}
type Person struct {
Name string
Age int
NumChildren int
}
got := Person{
Name: "Foobar",
Age: 42,
NumChildren: 3,
}
ok := td.Cmp(t, got,
td.SStruct(
Person{
Name: "Foobar",
Age: 53,
},
td.StructFields{
">Age": td.Between(40, 50), // ">" to overwrite Age:53 in model
"NumChildren": td.Gt(2),
}),
"checks %v is the right Person")
fmt.Println("Foobar is between 40 & 50:", ok)
ok = td.Cmp(t, got,
td.SStruct(
Person{
Name: "Foobar",
Age: 53,
},
td.StructFields{
"> Age": td.Between(40, 50), // same, ">" can be followed by spaces
"NumChildren": td.Gt(2),
}),
"checks %v is the right Person")
fmt.Println("Foobar is between 40 & 50:", ok)
// Output:
// Foobar is between 40 & 50: true
// Foobar is between 40 & 50: true
}
func ExampleSStruct_patterns() {
t := &testing.T{}
type Person struct {
Firstname string
Lastname string
Surname string
Nickname string
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
id int64
secret string
}
now := time.Now()
got := Person{
Firstname: "Maxime",
Lastname: "Foo",
Surname: "Max",
Nickname: "max",
CreatedAt: now,
UpdatedAt: now,
DeletedAt: nil, // not deleted yet
id: 2345,
secret: "5ecr3T",
}
ok := td.Cmp(t, got,
td.SStruct(Person{Lastname: "Foo"}, td.StructFields{
`DeletedAt`: nil,
`= *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model
`=~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt
`! [A-Z]*`: td.Ignore(), // private fields
}),
"mix shell & regexp patterns")
fmt.Println("Patterns match only remaining fields:", ok)
ok = td.Cmp(t, got,
td.SStruct(Person{Lastname: "Foo"}, td.StructFields{
`DeletedAt`: nil,
`1 = *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model
`2 =~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt
`3 !~ ^[A-Z]`: td.Ignore(), // private fields
}),
"ordered patterns")
fmt.Println("Ordered patterns match only remaining fields:", ok)
// Output:
// Patterns match only remaining fields: true
// Ordered patterns match only remaining fields: true
}
func ExampleSStruct_struct_fields() { // only operator
t := &testing.T{}
type Person struct {
Name string
Age int
NumChildren int
}
got := Person{
Name: "Foobar",
Age: 42,
NumChildren: 3,
}
// No added value here, but it works
ok := td.Cmp(t, got,
td.SStruct(Person{
Name: "Foobar",
Age: 42,
NumChildren: 3,
}),
"no StructFields")
fmt.Println("Without any StructFields:", ok)
ok = td.Cmp(t, got,
td.SStruct(Person{Name: "Bingo"},
td.StructFields{
"> Name": "pipo",
"Age": 42,
},
td.StructFields{
"> Name": "bingo",
"NumChildren": 10,
},
td.StructFields{
">Name": "Foobar",
"NumChildren": 3,
}),
"merge several StructFields")
fmt.Println("Merge several StructFields:", ok)
// Output:
// Without any StructFields: true
// Merge several StructFields: true
}
func ExampleSStruct_lazy_model() {
t := &testing.T{}
got := struct {
name string
age int
}{
name: "Foobar",
age: 42,
}
ok := td.Cmp(t, got, td.SStruct(nil, td.StructFields{
"name": "Foobar",
"age": td.Between(40, 45),
}))
fmt.Println("Lazy model:", ok)
ok = td.Cmp(t, got, td.SStruct(nil, td.StructFields{
"name": "Foobar",
"zip": 666,
}))
fmt.Println("Lazy model with unknown field:", ok)
// Output:
// Lazy model: true
// Lazy model with unknown field: false
}
func ExampleSubBagOf() {
t := &testing.T{}
got := []int{1, 3, 5, 8, 8, 1, 2}
ok := td.Cmp(t, got, td.SubBagOf(0, 0, 1, 1, 2, 2, 3, 3, 5, 5, 8, 8, 9, 9),
"checks at least all items are present, in any order")
fmt.Println(ok)
// got contains one 8 too many
ok = td.Cmp(t, got, td.SubBagOf(0, 0, 1, 1, 2, 2, 3, 3, 5, 5, 8, 9, 9),
"checks at least all items are present, in any order")
fmt.Println(ok)
got = []int{1, 3, 5, 2}
ok = td.Cmp(t, got, td.SubBagOf(
td.Between(0, 3),
td.Between(0, 3),
td.Between(0, 3),
td.Between(0, 3),
td.Gt(4),
td.Gt(4)),
"checks at least all items match, in any order with TestDeep operators")
fmt.Println(ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using expected... without copying it to a new
// []any slice, then use td.Flatten!
expected := []int{1, 2, 3, 5, 9, 8}
ok = td.Cmp(t, got, td.SubBagOf(td.Flatten(expected)),
"checks at least all expected items are present, in any order")
fmt.Println(ok)
// Output:
// true
// false
// true
// true
}
func ExampleSubJSONOf_basic() {
t := &testing.T{}
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
}{
Fullname: "Bob",
Age: 42,
}
ok := td.Cmp(t, got, td.SubJSONOf(`{"age":42,"fullname":"Bob","gender":"male"}`))
fmt.Println("check got with age then fullname:", ok)
ok = td.Cmp(t, got, td.SubJSONOf(`{"fullname":"Bob","age":42,"gender":"male"}`))
fmt.Println("check got with fullname then age:", ok)
ok = td.Cmp(t, got, td.SubJSONOf(`
// This should be the JSON representation of a struct
{
// A person:
"fullname": "Bob", // The name of this person
"age": 42, /* The age of this person:
- 42 of course
- to demonstrate a multi-lines comment */
"gender": "male" // This field is ignored as SubJSONOf
}`))
fmt.Println("check got with nicely formatted and commented JSON:", ok)
ok = td.Cmp(t, got, td.SubJSONOf(`{"fullname":"Bob","gender":"male"}`))
fmt.Println("check got without age field:", ok)
// Output:
// check got with age then fullname: true
// check got with fullname then age: true
// check got with nicely formatted and commented JSON: true
// check got without age field: false
}
func ExampleSubJSONOf_placeholders() {
t := &testing.T{}
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
}{
Fullname: "Bob Foobar",
Age: 42,
}
ok := td.Cmp(t, got,
td.SubJSONOf(`{"age": $1, "fullname": $2, "gender": $3}`,
42, "Bob Foobar", "male"))
fmt.Println("check got with numeric placeholders without operators:", ok)
ok = td.Cmp(t, got,
td.SubJSONOf(`{"age": $1, "fullname": $2, "gender": $3}`,
td.Between(40, 45),
td.HasSuffix("Foobar"),
td.NotEmpty()))
fmt.Println("check got with numeric placeholders:", ok)
ok = td.Cmp(t, got,
td.SubJSONOf(`{"age": "$1", "fullname": "$2", "gender": "$3"}`,
td.Between(40, 45),
td.HasSuffix("Foobar"),
td.NotEmpty()))
fmt.Println("check got with double-quoted numeric placeholders:", ok)
ok = td.Cmp(t, got,
td.SubJSONOf(`{"age": $age, "fullname": $name, "gender": $gender}`,
td.Tag("age", td.Between(40, 45)),
td.Tag("name", td.HasSuffix("Foobar")),
td.Tag("gender", td.NotEmpty())))
fmt.Println("check got with named placeholders:", ok)
ok = td.Cmp(t, got,
td.SubJSONOf(`{"age": $^NotZero, "fullname": $^NotEmpty, "gender": $^NotEmpty}`))
fmt.Println("check got with operator shortcuts:", ok)
// Output:
// check got with numeric placeholders without operators: true
// check got with numeric placeholders: true
// check got with double-quoted numeric placeholders: true
// check got with named placeholders: true
// check got with operator shortcuts: true
}
func ExampleSubJSONOf_file() {
t := &testing.T{}
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
Gender string `json:"gender"`
}{
Fullname: "Bob Foobar",
Age: 42,
Gender: "male",
}
tmpDir, err := os.MkdirTemp("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir) //nolint: errcheck // clean up
filename := tmpDir + "/test.json"
if err = os.WriteFile(filename, []byte(`
{
"fullname": "$name",
"age": "$age",
"gender": "$gender",
"details": {
"city": "TestCity",
"zip": 666
}
}`), 0644); err != nil {
t.Fatal(err)
}
// OK let's test with this file
ok := td.Cmp(t, got,
td.SubJSONOf(filename,
td.Tag("name", td.HasPrefix("Bob")),
td.Tag("age", td.Between(40, 45)),
td.Tag("gender", td.Re(`^(male|female)\z`))))
fmt.Println("Full match from file name:", ok)
// When the file is already open
file, err := os.Open(filename)
if err != nil {
t.Fatal(err)
}
ok = td.Cmp(t, got,
td.SubJSONOf(file,
td.Tag("name", td.HasPrefix("Bob")),
td.Tag("age", td.Between(40, 45)),
td.Tag("gender", td.Re(`^(male|female)\z`))))
fmt.Println("Full match from io.Reader:", ok)
// Output:
// Full match from file name: true
// Full match from io.Reader: true
}
func ExampleSubMapOf_map() {
t := &testing.T{}
got := map[string]int{"foo": 12, "bar": 42}
ok := td.Cmp(t, got,
td.SubMapOf(map[string]int{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": 666}),
"checks map %v is included in expected keys/values", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleSubMapOf_typedMap() {
t := &testing.T{}
type MyMap map[string]int
got := MyMap{"foo": 12, "bar": 42}
ok := td.Cmp(t, got,
td.SubMapOf(MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": 666}),
"checks typed map %v is included in expected keys/values", got)
fmt.Println(ok)
ok = td.Cmp(t, &got,
td.SubMapOf(&MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": 666}),
"checks pointed typed map %v is included in expected keys/values", got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleSubSetOf() {
t := &testing.T{}
got := []int{1, 3, 5, 8, 8, 1, 2}
// Matches as all items are expected, ignoring duplicates
ok := td.Cmp(t, got, td.SubSetOf(1, 2, 3, 4, 5, 6, 7, 8),
"checks at least all items are present, in any order, ignoring duplicates")
fmt.Println(ok)
// Tries its best not to raise an error when a value can be matched
// by several SubSetOf entries
ok = td.Cmp(t, got, td.SubSetOf(td.Between(1, 4), 3, td.Between(2, 10), td.Gt(100)),
"checks at least all items are present, in any order, ignoring duplicates")
fmt.Println(ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using expected... without copying it to a new
// []any slice, then use td.Flatten!
expected := []int{1, 2, 3, 4, 5, 6, 7, 8}
ok = td.Cmp(t, got, td.SubSetOf(td.Flatten(expected)),
"checks at least all expected items are present, in any order, ignoring duplicates")
fmt.Println(ok)
// Output:
// true
// true
// true
}
func ExampleSuperBagOf() {
t := &testing.T{}
got := []int{1, 3, 5, 8, 8, 1, 2}
ok := td.Cmp(t, got, td.SuperBagOf(8, 5, 8),
"checks the items are present, in any order")
fmt.Println(ok)
ok = td.Cmp(t, got, td.SuperBagOf(td.Gt(5), td.Lte(2)),
"checks at least 2 items of %v match", got)
fmt.Println(ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using expected... without copying it to a new
// []any slice, then use td.Flatten!
expected := []int{8, 5, 8}
ok = td.Cmp(t, got, td.SuperBagOf(td.Flatten(expected)),
"checks the expected items are present, in any order")
fmt.Println(ok)
// Output:
// true
// true
// true
}
func ExampleSuperJSONOf_basic() {
t := &testing.T{}
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
Gender string `json:"gender"`
City string `json:"city"`
Zip int `json:"zip"`
}{
Fullname: "Bob",
Age: 42,
Gender: "male",
City: "TestCity",
Zip: 666,
}
ok := td.Cmp(t, got, td.SuperJSONOf(`{"age":42,"fullname":"Bob","gender":"male"}`))
fmt.Println("check got with age then fullname:", ok)
ok = td.Cmp(t, got, td.SuperJSONOf(`{"fullname":"Bob","age":42,"gender":"male"}`))
fmt.Println("check got with fullname then age:", ok)
ok = td.Cmp(t, got, td.SuperJSONOf(`
// This should be the JSON representation of a struct
{
// A person:
"fullname": "Bob", // The name of this person
"age": 42, /* The age of this person:
- 42 of course
- to demonstrate a multi-lines comment */
"gender": "male" // The gender!
}`))
fmt.Println("check got with nicely formatted and commented JSON:", ok)
ok = td.Cmp(t, got,
td.SuperJSONOf(`{"fullname":"Bob","gender":"male","details":{}}`))
fmt.Println("check got with details field:", ok)
// Output:
// check got with age then fullname: true
// check got with fullname then age: true
// check got with nicely formatted and commented JSON: true
// check got with details field: false
}
func ExampleSuperJSONOf_placeholders() {
t := &testing.T{}
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
Gender string `json:"gender"`
City string `json:"city"`
Zip int `json:"zip"`
}{
Fullname: "Bob Foobar",
Age: 42,
Gender: "male",
City: "TestCity",
Zip: 666,
}
ok := td.Cmp(t, got,
td.SuperJSONOf(`{"age": $1, "fullname": $2, "gender": $3}`,
42, "Bob Foobar", "male"))
fmt.Println("check got with numeric placeholders without operators:", ok)
ok = td.Cmp(t, got,
td.SuperJSONOf(`{"age": $1, "fullname": $2, "gender": $3}`,
td.Between(40, 45),
td.HasSuffix("Foobar"),
td.NotEmpty()))
fmt.Println("check got with numeric placeholders:", ok)
ok = td.Cmp(t, got,
td.SuperJSONOf(`{"age": "$1", "fullname": "$2", "gender": "$3"}`,
td.Between(40, 45),
td.HasSuffix("Foobar"),
td.NotEmpty()))
fmt.Println("check got with double-quoted numeric placeholders:", ok)
ok = td.Cmp(t, got,
td.SuperJSONOf(`{"age": $age, "fullname": $name, "gender": $gender}`,
td.Tag("age", td.Between(40, 45)),
td.Tag("name", td.HasSuffix("Foobar")),
td.Tag("gender", td.NotEmpty())))
fmt.Println("check got with named placeholders:", ok)
ok = td.Cmp(t, got,
td.SuperJSONOf(`{"age": $^NotZero, "fullname": $^NotEmpty, "gender": $^NotEmpty}`))
fmt.Println("check got with operator shortcuts:", ok)
// Output:
// check got with numeric placeholders without operators: true
// check got with numeric placeholders: true
// check got with double-quoted numeric placeholders: true
// check got with named placeholders: true
// check got with operator shortcuts: true
}
func ExampleSuperJSONOf_file() {
t := &testing.T{}
got := &struct {
Fullname string `json:"fullname"`
Age int `json:"age"`
Gender string `json:"gender"`
City string `json:"city"`
Zip int `json:"zip"`
}{
Fullname: "Bob Foobar",
Age: 42,
Gender: "male",
City: "TestCity",
Zip: 666,
}
tmpDir, err := os.MkdirTemp("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir) //nolint: errcheck // clean up
filename := tmpDir + "/test.json"
if err = os.WriteFile(filename, []byte(`
{
"fullname": "$name",
"age": "$age",
"gender": "$gender"
}`), 0644); err != nil {
t.Fatal(err)
}
// OK let's test with this file
ok := td.Cmp(t, got,
td.SuperJSONOf(filename,
td.Tag("name", td.HasPrefix("Bob")),
td.Tag("age", td.Between(40, 45)),
td.Tag("gender", td.Re(`^(male|female)\z`))))
fmt.Println("Full match from file name:", ok)
// When the file is already open
file, err := os.Open(filename)
if err != nil {
t.Fatal(err)
}
ok = td.Cmp(t, got,
td.SuperJSONOf(file,
td.Tag("name", td.HasPrefix("Bob")),
td.Tag("age", td.Between(40, 45)),
td.Tag("gender", td.Re(`^(male|female)\z`))))
fmt.Println("Full match from io.Reader:", ok)
// Output:
// Full match from file name: true
// Full match from io.Reader: true
}
func ExampleSuperMapOf_map() {
t := &testing.T{}
got := map[string]int{"foo": 12, "bar": 42, "zip": 89}
ok := td.Cmp(t, got,
td.SuperMapOf(map[string]int{"bar": 42}, td.MapEntries{"foo": td.Lt(15)}),
"checks map %v contains at least all expected keys/values", got)
fmt.Println(ok)
// Output:
// true
}
func ExampleSuperMapOf_typedMap() {
t := &testing.T{}
type MyMap map[string]int
got := MyMap{"foo": 12, "bar": 42, "zip": 89}
ok := td.Cmp(t, got,
td.SuperMapOf(MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15)}),
"checks typed map %v contains at least all expected keys/values", got)
fmt.Println(ok)
ok = td.Cmp(t, &got,
td.SuperMapOf(&MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15)}),
"checks pointed typed map %v contains at least all expected keys/values",
got)
fmt.Println(ok)
// Output:
// true
// true
}
func ExampleSuperSetOf() {
t := &testing.T{}
got := []int{1, 3, 5, 8, 8, 1, 2}
ok := td.Cmp(t, got, td.SuperSetOf(1, 2, 3),
"checks the items are present, in any order and ignoring duplicates")
fmt.Println(ok)
ok = td.Cmp(t, got, td.SuperSetOf(td.Gt(5), td.Lte(2)),
"checks at least 2 items of %v match ignoring duplicates", got)
fmt.Println(ok)
// When expected is already a non-[]any slice, it cannot be
// flattened directly using expected... without copying it to a new
// []any slice, then use td.Flatten!
expected := []int{1, 2, 3}
ok = td.Cmp(t, got, td.SuperSetOf(td.Flatten(expected)),
"checks the expected items are present, in any order and ignoring duplicates")
fmt.Println(ok)
// Output:
// true
// true
// true
}
func ExampleTruncTime() {
t := &testing.T{}
dateToTime := func(str string) time.Time {
t, err := time.Parse(time.RFC3339Nano, str)
if err != nil {
panic(err)
}
return t
}
got := dateToTime("2018-05-01T12:45:53.123456789Z")
// Compare dates ignoring nanoseconds and monotonic parts
expected := dateToTime("2018-05-01T12:45:53Z")
ok := td.Cmp(t, got, td.TruncTime(expected, time.Second),
"checks date %v, truncated to the second", got)
fmt.Println(ok)
// Compare dates ignoring time and so monotonic parts
expected = dateToTime("2018-05-01T11:22:33.444444444Z")
ok = td.Cmp(t, got, td.TruncTime(expected, 24*time.Hour),
"checks date %v, truncated to the day", got)
fmt.Println(ok)
// Compare dates exactly but ignoring monotonic part
expected = dateToTime("2018-05-01T12:45:53.123456789Z")
ok = td.Cmp(t, got, td.TruncTime(expected),
"checks date %v ignoring monotonic part", got)
fmt.Println(ok)
// Output:
// true
// true
// true
}
func ExampleValues() {
t := &testing.T{}
got := map[string]int{"foo": 1, "bar": 2, "zip": 3}
// Values tests values in an ordered manner
ok := td.Cmp(t, got, td.Values([]int{1, 2, 3}))
fmt.Println("All sorted values are found:", ok)
// If the expected values are not ordered, it fails
ok = td.Cmp(t, got, td.Values([]int{3, 1, 2}))
fmt.Println("All unsorted values are found:", ok)
// To circumvent that, one can use Bag operator
ok = td.Cmp(t, got, td.Values(td.Bag(3, 1, 2)))
fmt.Println("All unsorted values are found, with the help of Bag operator:", ok)
// Check that each value is between 1 and 3
ok = td.Cmp(t, got, td.Values(td.ArrayEach(td.Between(1, 3))))
fmt.Println("Each value is between 1 and 3:", ok)
// Output:
// All sorted values are found: true
// All unsorted values are found: false
// All unsorted values are found, with the help of Bag operator: true
// Each value is between 1 and 3: true
}
func ExampleZero() {
t := &testing.T{}
ok := td.Cmp(t, 0, td.Zero())
fmt.Println(ok)
ok = td.Cmp(t, float64(0), td.Zero())
fmt.Println(ok)
ok = td.Cmp(t, 12, td.Zero()) // fails, as 12 is not 0 :)
fmt.Println(ok)
ok = td.Cmp(t, (map[string]int)(nil), td.Zero())
fmt.Println(ok)
ok = td.Cmp(t, map[string]int{}, td.Zero()) // fails, as not nil
fmt.Println(ok)
ok = td.Cmp(t, ([]int)(nil), td.Zero())
fmt.Println(ok)
ok = td.Cmp(t, []int{}, td.Zero()) // fails, as not nil
fmt.Println(ok)
ok = td.Cmp(t, [3]int{}, td.Zero())
fmt.Println(ok)
ok = td.Cmp(t, [3]int{0, 1}, td.Zero()) // fails, DATA[1] is not 0
fmt.Println(ok)
ok = td.Cmp(t, bytes.Buffer{}, td.Zero())
fmt.Println(ok)
ok = td.Cmp(t, &bytes.Buffer{}, td.Zero()) // fails, as pointer not nil
fmt.Println(ok)
ok = td.Cmp(t, &bytes.Buffer{}, td.Ptr(td.Zero())) // OK with the help of Ptr()
fmt.Println(ok)
// Output:
// true
// true
// false
// true
// false
// true
// false
// true
// false
// true
// false
// true
}
go-testdeep-1.15.0/td/flatten.go 0000664 0000000 0000000 00000016702 15144170453 0016423 0 ustar 00root root 0000000 0000000 // Copyright (c) 2020-2023, 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"
"strings"
"github.com/maxatome/go-testdeep/internal/color"
"github.com/maxatome/go-testdeep/internal/flat"
"github.com/maxatome/go-testdeep/internal/types"
)
func flattenFuncIsValid(typ reflect.Type) bool {
return typ.Kind() == reflect.Func &&
(typ.NumIn() == 1 && !typ.IsVariadic() ||
typ.NumIn() == 2 && typ.IsVariadic()) &&
(typ.NumOut() == 1 || typ.NumOut() == 2 && typ.Out(1) == types.Bool)
}
// Flatten allows to flatten any slice, array, map or int range in
// parameters of operators expecting ...any. fn parameter allows to
// filter and/or transform items before flattening and is described
// below.
//
// For example the [Set] operator is defined as:
//
// func Set(expectedItems ...any) TestDeep
//
// so when comparing to a []int slice, we usually do:
//
// got := []int{42, 66, 22}
// td.Cmp(t, got, td.Set(22, 42, 66))
//
// it works but if the expected items are already in a []int, we have
// to copy them in a []any as it can not be flattened directly
// in [Set] parameters:
//
// expected := []int{22, 42, 66}
// expectedIf := make([]any, len(expected))
// for i, item := range expected {
// expectedIf[i] = item
// }
// td.Cmp(t, got, td.Set(expectedIf...))
//
// but it is a bit boring and less efficient, as [Set] does not keep
// the []any behind the scene.
//
// The same with Flatten follows:
//
// expected := []int{22, 42, 66}
// td.Cmp(t, got, td.Set(td.Flatten(expected)))
//
// Several Flatten calls can be passed, and even combined with normal
// parameters:
//
// expectedPart1 := []int{11, 22, 33}
// expectedPart2 := []int{55, 66, 77}
// expectedPart3 := []int{99}
// td.Cmp(t, got,
// td.Set(
// td.Flatten(expectedPart1),
// 44,
// td.Flatten(expectedPart2),
// 88,
// td.Flatten(expectedPart3),
// ))
//
// is exactly the same as:
//
// td.Cmp(t, got, td.Set(11, 22, 33, 44, 55, 66, 77, 88, 99))
//
// Note that Flatten calls can even be nested:
//
// td.Cmp(t, got,
// td.Set(
// td.Flatten([]any{
// td.Flatten(5),
// 11,
// td.Flatten([]int{22, 33}),
// td.Flatten([]int{44, 55, 66}),
// }),
// 77,
// ))
//
// is exactly the same as:
//
// td.Cmp(t, got, td.Set(0, 1, 2, 3, 4, 11, 22, 33, 44, 55, 66, 77))
//
// Maps can be flattened too, keeping in mind there is no particular order:
//
// td.Flatten(map[int]int{1: 2, 3: 4})
//
// is flattened as 1, 2, 3, 4 or 3, 4, 1, 2.
//
// As seen in the example above, int range:
//
// td.Flatten(5)
//
// is flattened as 0, 1, 2, 3, 4.
//
// Optional fn parameter can be used to filter and/or transform items
// before flattening. If passed, it has to be one element length and
// this single element can be:
//
// - untyped nil: it is a no-op, as if it was not passed
// - a function
// - a string shortcut
//
// If it is a function, it must be a non-nil function with a signature like:
//
// func(T) V
// func(T) (V, bool)
// func(T, X...) V
// func(T, X...) (V, bool)
//
// T can be the same as V, but it is not mandatory. The (V, bool)
// returned cases allow to exclude some items when returning
// false. For the variadic cases, X does not matter as the function is
// always called without any variadic argument.
//
// If the function signature does not match these cases, Flatten panics.
//
// If the type of an item of sliceOrMapOrInt is not convertible to T,
// the item is dropped silently, as if fn returns false.
//
// This single element can also be a string among:
//
// "Smuggle:FIELD"
// "JSONPointer:/PATH"
//
// that are shortcuts for respectively:
//
// func(in any) any { return td.Smuggle("FIELD", in) }
// func(in any) any { return td.JSONPointer("/PATH", in) }
//
// See [Smuggle] and [JSONPointer] for a description of what "FIELD"
// and "/PATH" can really be.
//
// Flatten with an fn can be useful when testing some fields of
// structs in a slice with [Bag] or [Set] operators families as well
// as [List]. As an example, here we test only "Name" field for each
// item of a person slice:
//
// type person struct {
// Name string `json:"name"`
// Age int `json:"age"`
// }
// got := []person{{"alice", 22}, {"bob", 18}, {"brian", 34}, {"britt", 32}}
//
// td.Cmp(t, got,
// td.Bag(td.Flatten(
// []string{"alice", "britt", "brian", "bob"},
// func(name string) any { return td.Smuggle("Name", name) })))
// // distributes td.Smuggle for each Name, so is equivalent of:
// td.Cmp(t, got, td.Bag(
// td.Smuggle("Name", "alice"),
// td.Smuggle("Name", "britt"),
// td.Smuggle("Name", "brian"),
// td.Smuggle("Name", "bob"),
// ))
//
// // Same here using Smuggle string shortcut
// td.Cmp(t, got,
// td.Bag(td.Flatten(
// []string{"alice", "britt", "brian", "bob"}, "Smuggle:Name")))
//
// // Same here, but using JSONPointer operator
// td.Cmp(t, got,
// td.Bag(td.Flatten(
// []string{"alice", "britt", "brian", "bob"},
// func(name string) any { return td.JSONPointer("/name", name) })))
//
// // Same here using JSONPointer string shortcut
// td.Cmp(t, got,
// td.Bag(td.Flatten(
// []string{"alice", "britt", "brian", "bob"}, "JSONPointer:/name")))
//
// // Same here, but using SuperJSONOf operator
// td.Cmp(t, got,
// td.Bag(td.Flatten(
// []string{"alice", "britt", "brian", "bob"},
// func(name string) any { return td.SuperJSONOf(`{"name":$1}`, name) })))
//
// // Same here, but using Struct operator
// td.Cmp(t, got,
// td.Bag(td.Flatten(
// []string{"alice", "britt", "brian", "bob"},
// func(name string) any { return td.Struct(person{Name: name}) })))
//
// See also [Grep].
func Flatten(sliceOrMapOrInt any, fn ...any) flat.Slice {
const (
smugglePrefix = "Smuggle:"
jsonPointerPrefix = "JSONPointer:"
usage = "Flatten(SLICE|ARRAY|MAP|int[, FUNC])"
usageFunc = usage + `, FUNC should be non-nil func(T) V or func(T) (V, bool) or a string "` + smugglePrefix + `…" or "` + jsonPointerPrefix + `…"`
)
if _, isInt := sliceOrMapOrInt.(int); !isInt {
switch reflect.ValueOf(sliceOrMapOrInt).Kind() {
case reflect.Slice, reflect.Array, reflect.Map:
default:
panic(color.BadUsage(usage, sliceOrMapOrInt, 1, true))
}
}
switch len(fn) {
case 1:
if fn[0] != nil {
break
}
fallthrough
case 0:
return flat.Slice{Slice: sliceOrMapOrInt}
default:
panic(color.TooManyParams(usage))
}
f := fn[0]
// Smuggle & JSONPointer specific shortcuts
if s, ok := f.(string); ok {
switch {
case strings.HasPrefix(s, smugglePrefix):
f = func(in any) any {
return Smuggle(s[len(smugglePrefix):], in)
}
case strings.HasPrefix(s, jsonPointerPrefix):
f = func(in any) any {
return JSONPointer(s[len(jsonPointerPrefix):], in)
}
default:
panic(color.Bad("usage: "+usageFunc+", but received %q as 2nd parameter", s))
}
}
fnType := reflect.TypeOf(f)
vfn := reflect.ValueOf(f)
if !flattenFuncIsValid(fnType) {
panic(color.BadUsage(usageFunc, f, 2, false))
}
if vfn.IsNil() {
panic(color.Bad("usage: " + usageFunc))
}
inType := fnType.In(0)
var final []any
for _, v := range flat.Values([]any{flat.Slice{Slice: sliceOrMapOrInt}}) {
if v.Type() != inType {
if !v.Type().ConvertibleTo(inType) {
continue
}
v = v.Convert(inType)
}
ret := vfn.Call([]reflect.Value{v})
if len(ret) == 1 || ret[1].Bool() {
final = append(final, ret[0].Interface())
}
}
return flat.Slice{Slice: final}
}
go-testdeep-1.15.0/td/flatten_test.go 0000664 0000000 0000000 00000020306 15144170453 0017455 0 ustar 00root root 0000000 0000000 // Copyright (c) 2020-2025, 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 (
"reflect"
"strconv"
"testing"
"github.com/maxatome/go-testdeep/internal/flat"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestFlatten(t *testing.T) {
t.Run("ok", func(t *testing.T) {
testCases := []struct {
name string
sliceOrMapOrInt any
fn []any
expectedType reflect.Type
expectedLen int
}{
{
name: "slice",
sliceOrMapOrInt: []int{1, 2, 3},
expectedType: reflect.TypeOf([]int{}),
expectedLen: 3,
},
{
name: "array",
sliceOrMapOrInt: [3]int{1, 2, 3},
expectedType: reflect.TypeOf([3]int{}),
expectedLen: 3,
},
{
name: "map",
sliceOrMapOrInt: map[int]int{1: 2, 3: 4},
expectedType: reflect.TypeOf(map[int]int{}),
expectedLen: 4,
},
{
name: "slice+untyped nil fn",
sliceOrMapOrInt: []int{1, 2, 3},
fn: []any{nil},
expectedType: reflect.TypeOf([]int{}),
expectedLen: 3,
},
{
name: "int",
sliceOrMapOrInt: 5,
expectedType: reflect.TypeOf(42),
expectedLen: 5,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := td.Flatten(tc.sliceOrMapOrInt, tc.fn...)
if reflect.TypeOf(s.Slice) != tc.expectedType {
t.Errorf("types differ: got=%s, expected=%s",
reflect.TypeOf(s.Slice), tc.expectedType)
return
}
l, _ := flat.Len([]any{s})
test.EqualInt(t, l, tc.expectedLen)
})
}
})
cmp := func(t *testing.T, got, expected []any) {
t.Helper()
if (got == nil) != (expected == nil) {
t.Errorf("nil mismatch: got=%#v, expected=%#v", got, expected)
return
}
lg, le := len(got), len(expected)
l := lg
if l > le {
l = le
}
i := 0
for ; i < l; i++ {
if got[i] != expected[i] {
t.Errorf("#%d item differ, got=%v, expected=%v", i, got[i], expected[i])
}
}
for ; i < lg; i++ {
t.Errorf("#%d item is extra, got=%v", i, got[i])
}
for ; i < le; i++ {
t.Errorf("#%d item is missing, expected=%v", i, expected[i])
}
}
t.Run("ok+func", func(t *testing.T) {
testCases := []struct {
name string
fn any
expected []any
}{
{
name: "func never called",
fn: func(s bool) bool { return true },
expected: nil,
},
{
name: "double",
fn: func(a int) int { return a * 2 },
expected: []any{0, 2, 4, 6, 8, 10, 12, 14, 16, 18},
},
{
name: "even",
fn: func(a int) (int, bool) { return a, a%2 == 0 },
expected: []any{0, 2, 4, 6, 8},
},
{
name: "transform",
fn: func(a int) (string, bool) { return strconv.Itoa(a), a%2 == 0 },
expected: []any{"0", "2", "4", "6", "8"},
},
{
name: "nil",
fn: func(a int) any { return nil },
expected: []any{nil, nil, nil, nil, nil, nil, nil, nil, nil, nil},
},
{
name: "convertible",
fn: func(a int8) int8 { return a * 3 },
expected: []any{
int8(0), int8(3), int8(6), int8(9), int8(12),
int8(15), int8(18), int8(21), int8(24), int8(27),
},
},
{
name: "any+variadic",
fn: func(a any, opts ...bool) int {
x, _ := a.(int)
return x
},
expected: []any{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := td.Flatten([]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, tc.fn)
if sa, ok := s.Slice.([]any); test.IsTrue(t, ok) {
cmp(t, sa, tc.expected)
}
})
}
})
t.Run("int", func(t *testing.T) {
testCases := []struct {
name string
input int
fn any
expected []any
}{
{
name: "classic",
input: 5,
fn: func(a int) int { return -a },
expected: []any{0, -1, -2, -3, -4},
},
{
name: "convertible to int64",
input: 5,
fn: func(a int64) int64 { return a * 1000 },
expected: []any{int64(0), int64(1000), int64(2000), int64(3000), int64(4000)},
},
{
name: "empty",
input: 0,
fn: func(a int) int { return a },
},
{
name: "empty coz neg",
input: -42,
fn: func(a int) int { return a },
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := td.Flatten(tc.input, tc.fn)
if sa, ok := s.Slice.([]any); test.IsTrue(t, ok) {
cmp(t, sa, tc.expected)
}
})
}
})
t.Run("complex", func(t *testing.T) {
type person struct {
Name string `json:"name"`
Age int `json:"age"`
}
got := []person{{"alice", 22}, {"bob", 18}, {"brian", 34}, {"britt", 32}}
td.Cmp(t, got,
td.Bag(td.Flatten(
[]string{"alice", "britt", "brian", "bob"},
func(name string) any { return td.Smuggle("Name", name) })))
td.Cmp(t, got,
td.Bag(td.Flatten(
[]string{"alice", "britt", "brian", "bob"}, "Smuggle:Name")))
td.Cmp(t, got,
td.Bag(td.Flatten(
[]string{"alice", "britt", "brian", "bob"},
func(name string) any { return td.JSONPointer("/name", name) })))
td.Cmp(t, got,
td.Bag(td.Flatten(
[]string{"alice", "britt", "brian", "bob"}, "JSONPointer:/name")))
td.Cmp(t, got,
td.Bag(td.Flatten(
[]string{"alice", "britt", "brian", "bob"},
func(name string) any { return td.SuperJSONOf(`{"name":$1}`, name) })))
td.Cmp(t, got,
td.Bag(td.Flatten(
[]string{"alice", "britt", "brian", "bob"},
func(name string) any { return td.Struct(person{Name: name}) })))
})
t.Run("errors", func(t *testing.T) {
const (
usage = `usage: Flatten(SLICE|ARRAY|MAP|int[, FUNC])`
usageFunc = usage + `, FUNC should be non-nil func(T) V or func(T) (V, bool) or a string "Smuggle:…" or "JSONPointer:…"`
)
testCases := []struct {
name string
fn []any
sliceOrMapOrInt any
expected string
}{
{
name: "too many params",
sliceOrMapOrInt: []int{},
fn: []any{1, 2},
expected: usage + ", too many parameters",
},
{
name: "nil sliceOrMapOrInt",
expected: usage + ", but received nil as 1st parameter",
},
{
name: "bad sliceOrMapOrInt type",
sliceOrMapOrInt: "42",
expected: usage + ", but received string as 1st parameter",
},
{
name: "not func",
sliceOrMapOrInt: []int{},
fn: []any{42},
expected: usageFunc + ", but received int as 2nd parameter",
},
{
name: "func w/0 inputs",
sliceOrMapOrInt: []int{},
fn: []any{func() int { return 0 }},
expected: usageFunc + ", but received func() int as 2nd parameter",
},
{
name: "func w/2 inputs",
sliceOrMapOrInt: []int{},
fn: []any{func(a, b int) int { return 0 }},
expected: usageFunc + ", but received func(int, int) int as 2nd parameter",
},
{
name: "variadic func",
sliceOrMapOrInt: []int{},
fn: []any{func(a ...int) int { return 0 }},
expected: usageFunc + ", but received func(...int) int as 2nd parameter",
},
{
name: "func w/0 output",
sliceOrMapOrInt: []int{},
fn: []any{func(a int) {}},
expected: usageFunc + ", but received func(int) as 2nd parameter",
},
{
name: "func w/2 out without bool",
sliceOrMapOrInt: []int{},
fn: []any{func(a int) (int, int) { return 0, 0 }},
expected: usageFunc + ", but received func(int) (int, int) as 2nd parameter",
},
{
name: "bad shortcut",
sliceOrMapOrInt: []int{},
fn: []any{"Pipo"},
expected: usageFunc + `, but received "Pipo" as 2nd parameter`,
},
{
name: "typed nil func",
sliceOrMapOrInt: []int{},
fn: []any{(func(a int) int)(nil)},
expected: usageFunc,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
test.CheckPanic(t, func() { td.Flatten(tc.sliceOrMapOrInt, tc.fn...) }, tc.expected)
})
}
})
}
go-testdeep-1.15.0/td/must_118.go 0000664 0000000 0000000 00000003665 15144170453 0016353 0 ustar 00root root 0000000 0000000 // Copyright (c) 2026, 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 td
// Must panics if ret is non-nil. Otherwise it returns ret.
//
// fn := func() (int, error) { … }
// value := td.Must(fn())
//
// It typically avoids to use 2 lines for the mostly same effect than:
//
// value, err := fn()
// td.Require(t).CmpNoError(err)
//
// except that in case of error a panic occurs instead of a clean
// [testing.T.Fatal] error.
//
// See also [Must2], [Must3], [CmpNoError] and [T.CmpNoError].
func Must[X any](ret X, err error) X {
if err != nil {
panic("Must: " + err.Error())
}
return ret
}
// Must2 panics if ret is non-nil. Otherwise it returns ret.
//
// fn := func() (int, string, error) { … }
// value1, value2 := td.Must2(fn())
//
// It typically avoids to use 2 lines for the mostly same effect than:
//
// value1, value2, err := fn()
// td.Require(t).CmpNoError(err)
//
// except that in case of error a panic occurs instead of a clean
// [testing.T.Fatal] error.
//
// See also [Must], [Must3], [CmpNoError] and [T.CmpNoError].
func Must2[X, Y any](ret1 X, ret2 Y, err error) (X, Y) {
if err != nil {
panic("Must2: " + err.Error())
}
return ret1, ret2
}
// Must3 panics if ret is non-nil. Otherwise it returns ret.
//
// fn := func() (int, string, bool, error) { … }
// value1, value2, value3 := td.Must3(fn())
//
// It typically avoids to use 2 lines for the mostly same effect than:
//
// value1, value2, value3, err := fn()
// td.Require(t).CmpNoError(err)
//
// except that in case of error a panic occurs instead of a clean
// [testing.T.Fatal] error.
//
// See also [Must], [Must2], [CmpNoError] and [T.CmpNoError].
func Must3[X, Y, Z any](ret1 X, ret2 Y, ret3 Z, err error) (X, Y, Z) {
if err != nil {
panic("Must3: " + err.Error())
}
return ret1, ret2, ret3
}
go-testdeep-1.15.0/td/must_118_test.go 0000664 0000000 0000000 00000002415 15144170453 0017402 0 ustar 00root root 0000000 0000000 // Copyright (c) 2026, 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 td_test
import (
"errors"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestMust(t *testing.T) {
fn := func(ok bool) (int, error) {
if ok {
return 42, nil
}
return 0, errors.New("error")
}
test.EqualInt(t, td.Must(fn(true)), 42)
test.CheckPanic(t, func() { td.Must(fn(false)) }, "error")
}
func TestMust2(t *testing.T) {
fn := func(ok bool) (int, string, error) {
if ok {
return 42, "pipo", nil
}
return 0, "", errors.New("error")
}
val1, val2 := td.Must2(fn(true))
test.EqualInt(t, val1, 42)
test.EqualStr(t, val2, "pipo")
test.CheckPanic(t, func() { td.Must2(fn(false)) }, "error")
}
func TestMust3(t *testing.T) {
fn := func(ok bool) (int, string, bool, error) {
if ok {
return 42, "pipo", true, nil
}
return 0, "", false, errors.New("error")
}
val1, val2, val3 := td.Must3(fn(true))
test.EqualInt(t, val1, 42)
test.EqualStr(t, val2, "pipo")
test.EqualBool(t, val3, true)
test.CheckPanic(t, func() { td.Must3(fn(false)) }, "error")
}
go-testdeep-1.15.0/td/private_test.go 0000664 0000000 0000000 00000002657 15144170453 0017503 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2021, 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 (
"testing"
"github.com/maxatome/go-testdeep/internal/test"
)
// Edge cases not tested elsewhere...
func TestBase(t *testing.T) {
td := base{}
td.setLocation(200)
if td.location.File != "???" && td.location.Line != 0 {
t.Errorf("Location found! => %s", td.location)
}
}
func TestTdSetResult(t *testing.T) {
if tdSetResultKind(199).String() != "?" {
t.Errorf("tdSetResultKind stringification failed => %s",
tdSetResultKind(199))
}
}
func TestPkgFunc(t *testing.T) {
pkg, fn := pkgFunc("package.Foo")
test.EqualStr(t, pkg, "package")
test.EqualStr(t, fn, "Foo")
pkg, fn = pkgFunc("the/package.Foo")
test.EqualStr(t, pkg, "the/package")
test.EqualStr(t, fn, "Foo")
pkg, fn = pkgFunc("the/package.(*T).Foo")
test.EqualStr(t, pkg, "the/package")
test.EqualStr(t, fn, "(*T).Foo")
pkg, fn = pkgFunc("the/package.glob..func1")
test.EqualStr(t, pkg, "the/package")
test.EqualStr(t, fn, "glob..func1")
// Theorically not possible, but...
pkg, fn = pkgFunc(".Foo")
test.EqualStr(t, pkg, "")
test.EqualStr(t, fn, "Foo")
pkg, fn = pkgFunc("no/func")
test.EqualStr(t, pkg, "no/func")
test.EqualStr(t, fn, "")
pkg, fn = pkgFunc("no/func.")
test.EqualStr(t, pkg, "no/func")
test.EqualStr(t, fn, "")
}
go-testdeep-1.15.0/td/t.go 0000664 0000000 0000000 00000133744 15144170453 0015237 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2025, 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.
//
// DO NOT EDIT!!! AUTOMATICALLY GENERATED!!!
package td
import (
"time"
)
// All is a shortcut for:
//
// t.Cmp(got, td.All(expectedValues...), args...)
//
// See [All] 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) All(got any, expectedValues []any, args ...any) bool {
t.Helper()
return t.Cmp(got, All(expectedValues...), args...)
}
// Any is a shortcut for:
//
// t.Cmp(got, td.Any(expectedValues...), args...)
//
// See [Any] 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) Any(got any, expectedValues []any, args ...any) bool {
t.Helper()
return t.Cmp(got, Any(expectedValues...), args...)
}
// Array is a shortcut for:
//
// t.Cmp(got, td.Array(model, expectedEntries), args...)
//
// See [Array] for details.
//
// [Array] optional parameter expectedEntries is here mandatory.
// nil value should be passed to mimic its absence in
// original [Array] 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) Array(got, model any, expectedEntries ArrayEntries, args ...any) bool {
t.Helper()
return t.Cmp(got, Array(model, expectedEntries), args...)
}
// ArrayEach is a shortcut for:
//
// t.Cmp(got, td.ArrayEach(expectedValue), args...)
//
// See [ArrayEach] 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) ArrayEach(got, expectedValue any, args ...any) bool {
t.Helper()
return t.Cmp(got, ArrayEach(expectedValue), args...)
}
// Bag is a shortcut for:
//
// t.Cmp(got, td.Bag(expectedItems...), args...)
//
// See [Bag] 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) Bag(got any, expectedItems []any, args ...any) bool {
t.Helper()
return t.Cmp(got, Bag(expectedItems...), args...)
}
// Between is a shortcut for:
//
// t.Cmp(got, td.Between(from, to, bounds), args...)
//
// See [Between] for details.
//
// [Between] optional parameter bounds is here mandatory.
// [BoundsInIn] value should be passed to mimic its absence in
// original [Between] 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) Between(got, from, to any, bounds BoundsKind, args ...any) bool {
t.Helper()
return t.Cmp(got, Between(from, to, bounds), args...)
}
// Cap is a shortcut for:
//
// t.Cmp(got, td.Cap(expectedCap), args...)
//
// See [Cap] 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) Cap(got, expectedCap any, args ...any) bool {
t.Helper()
return t.Cmp(got, Cap(expectedCap), args...)
}
// Code is a shortcut for:
//
// t.Cmp(got, td.Code(fn), args...)
//
// See [Code] 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) Code(got, fn any, args ...any) bool {
t.Helper()
return t.Cmp(got, Code(fn), args...)
}
// Contains is a shortcut for:
//
// t.Cmp(got, td.Contains(expectedValue), args...)
//
// See [Contains] 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) Contains(got, expectedValue any, args ...any) bool {
t.Helper()
return t.Cmp(got, Contains(expectedValue), args...)
}
// ContainsKey is a shortcut for:
//
// t.Cmp(got, td.ContainsKey(expectedValue), args...)
//
// See [ContainsKey] 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) ContainsKey(got, expectedValue any, args ...any) bool {
t.Helper()
return t.Cmp(got, ContainsKey(expectedValue), args...)
}
// Empty is a shortcut for:
//
// t.Cmp(got, td.Empty(), args...)
//
// See [Empty] 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) Empty(got any, args ...any) bool {
t.Helper()
return t.Cmp(got, Empty(), args...)
}
// CmpErrorIs is a shortcut for:
//
// t.Cmp(got, td.ErrorIs(expectedError), args...)
//
// See [ErrorIs] 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) CmpErrorIs(got, expectedError any, args ...any) bool {
t.Helper()
return t.Cmp(got, ErrorIs(expectedError), 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...)
//
// See [Gt] 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) Gt(got, minExpectedValue any, args ...any) bool {
t.Helper()
return t.Cmp(got, Gt(minExpectedValue), args...)
}
// Gte is a shortcut for:
//
// t.Cmp(got, td.Gte(minExpectedValue), args...)
//
// See [Gte] 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) Gte(got, minExpectedValue any, args ...any) bool {
t.Helper()
return t.Cmp(got, Gte(minExpectedValue), args...)
}
// HasPrefix is a shortcut for:
//
// t.Cmp(got, td.HasPrefix(expected), args...)
//
// See [HasPrefix] 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) HasPrefix(got any, expected string, args ...any) bool {
t.Helper()
return t.Cmp(got, HasPrefix(expected), args...)
}
// HasSuffix is a shortcut for:
//
// t.Cmp(got, td.HasSuffix(expected), args...)
//
// See [HasSuffix] 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) HasSuffix(got any, expected string, args ...any) bool {
t.Helper()
return t.Cmp(got, HasSuffix(expected), args...)
}
// Isa is a shortcut for:
//
// t.Cmp(got, td.Isa(model), args...)
//
// See [Isa] 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) Isa(got, model any, args ...any) bool {
t.Helper()
return t.Cmp(got, Isa(model), args...)
}
// JSON is a shortcut for:
//
// t.Cmp(got, td.JSON(expectedJSON, params...), args...)
//
// See [JSON] 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) JSON(got, expectedJSON any, params []any, args ...any) bool {
t.Helper()
return t.Cmp(got, JSON(expectedJSON, params...), args...)
}
// JSONPointer is a shortcut for:
//
// t.Cmp(got, td.JSONPointer(ptr, expectedValue), args...)
//
// See [JSONPointer] 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) JSONPointer(got any, ptr string, expectedValue any, args ...any) bool {
t.Helper()
return t.Cmp(got, JSONPointer(ptr, expectedValue), args...)
}
// Keys is a shortcut for:
//
// t.Cmp(got, td.Keys(val), args...)
//
// See [Keys] 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) Keys(got, val any, args ...any) bool {
t.Helper()
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...)
//
// See [Lax] 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) CmpLax(got, expectedValue any, args ...any) bool {
t.Helper()
return t.Cmp(got, Lax(expectedValue), args...)
}
// Len is a shortcut for:
//
// t.Cmp(got, td.Len(expectedLen), args...)
//
// See [Len] 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) Len(got, expectedLen any, args ...any) bool {
t.Helper()
return t.Cmp(got, Len(expectedLen), args...)
}
// List is a shortcut for:
//
// t.Cmp(got, td.List(expectedValues...), args...)
//
// See [List] 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) List(got any, expectedValues []any, args ...any) bool {
t.Helper()
return t.Cmp(got, List(expectedValues...), args...)
}
// Lt is a shortcut for:
//
// t.Cmp(got, td.Lt(maxExpectedValue), args...)
//
// See [Lt] 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) Lt(got, maxExpectedValue any, args ...any) bool {
t.Helper()
return t.Cmp(got, Lt(maxExpectedValue), args...)
}
// Lte is a shortcut for:
//
// t.Cmp(got, td.Lte(maxExpectedValue), args...)
//
// See [Lte] 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) Lte(got, maxExpectedValue any, args ...any) bool {
t.Helper()
return t.Cmp(got, Lte(maxExpectedValue), args...)
}
// Map is a shortcut for:
//
// t.Cmp(got, td.Map(model, expectedEntries), args...)
//
// See [Map] for details.
//
// [Map] optional parameter expectedEntries is here mandatory.
// nil value should be passed to mimic its absence in
// original [Map] 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) Map(got, model any, expectedEntries MapEntries, args ...any) bool {
t.Helper()
return t.Cmp(got, Map(model, expectedEntries), args...)
}
// MapEach is a shortcut for:
//
// t.Cmp(got, td.MapEach(expectedValue), args...)
//
// See [MapEach] 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) MapEach(got, expectedValue any, args ...any) bool {
t.Helper()
return t.Cmp(got, MapEach(expectedValue), args...)
}
// N is a shortcut for:
//
// t.Cmp(got, td.N(num, tolerance), args...)
//
// See [N] for details.
//
// [N] optional parameter tolerance is here mandatory.
// 0 value should be passed to mimic its absence in
// original [N] 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) N(got, num, tolerance any, args ...any) bool {
t.Helper()
return t.Cmp(got, N(num, tolerance), args...)
}
// NaN is a shortcut for:
//
// t.Cmp(got, td.NaN(), args...)
//
// See [NaN] 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) NaN(got any, args ...any) bool {
t.Helper()
return t.Cmp(got, NaN(), args...)
}
// Nil is a shortcut for:
//
// t.Cmp(got, td.Nil(), args...)
//
// See [Nil] 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) Nil(got any, args ...any) bool {
t.Helper()
return t.Cmp(got, Nil(), args...)
}
// None is a shortcut for:
//
// t.Cmp(got, td.None(notExpectedValues...), args...)
//
// See [None] 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) None(got any, notExpectedValues []any, args ...any) bool {
t.Helper()
return t.Cmp(got, None(notExpectedValues...), args...)
}
// Not is a shortcut for:
//
// t.Cmp(got, td.Not(notExpected), args...)
//
// See [Not] 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) Not(got, notExpected any, args ...any) bool {
t.Helper()
return t.Cmp(got, Not(notExpected), args...)
}
// NotAny is a shortcut for:
//
// t.Cmp(got, td.NotAny(notExpectedItems...), args...)
//
// See [NotAny] 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) NotAny(got any, notExpectedItems []any, args ...any) bool {
t.Helper()
return t.Cmp(got, NotAny(notExpectedItems...), args...)
}
// NotEmpty is a shortcut for:
//
// t.Cmp(got, td.NotEmpty(), args...)
//
// See [NotEmpty] 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) NotEmpty(got any, args ...any) bool {
t.Helper()
return t.Cmp(got, NotEmpty(), args...)
}
// NotNaN is a shortcut for:
//
// t.Cmp(got, td.NotNaN(), args...)
//
// See [NotNaN] 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) NotNaN(got any, args ...any) bool {
t.Helper()
return t.Cmp(got, NotNaN(), args...)
}
// NotNil is a shortcut for:
//
// t.Cmp(got, td.NotNil(), args...)
//
// See [NotNil] 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) NotNil(got any, args ...any) bool {
t.Helper()
return t.Cmp(got, NotNil(), args...)
}
// NotZero is a shortcut for:
//
// t.Cmp(got, td.NotZero(), args...)
//
// See [NotZero] 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) NotZero(got any, args ...any) bool {
t.Helper()
return t.Cmp(got, NotZero(), args...)
}
// PPtr is a shortcut for:
//
// t.Cmp(got, td.PPtr(val), args...)
//
// See [PPtr] 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) PPtr(got, val any, args ...any) bool {
t.Helper()
return t.Cmp(got, PPtr(val), args...)
}
// Ptr is a shortcut for:
//
// t.Cmp(got, td.Ptr(val), args...)
//
// See [Ptr] 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) Ptr(got, val any, args ...any) bool {
t.Helper()
return t.Cmp(got, Ptr(val), args...)
}
// Re is a shortcut for:
//
// t.Cmp(got, td.Re(reg, capture), args...)
//
// See [Re] for details.
//
// [Re] optional parameter capture is here mandatory.
// nil value should be passed to mimic its absence in
// original [Re] 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) Re(got, reg, capture any, args ...any) bool {
t.Helper()
return t.Cmp(got, Re(reg, capture), args...)
}
// ReAll is a shortcut for:
//
// t.Cmp(got, td.ReAll(reg, capture), args...)
//
// See [ReAll] 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) ReAll(got, reg, capture any, args ...any) bool {
t.Helper()
return t.Cmp(got, ReAll(reg, capture), args...)
}
// Recv is a shortcut for:
//
// t.Cmp(got, td.Recv(expectedValue, timeout), args...)
//
// See [Recv] for details.
//
// [Recv] optional parameter timeout is here mandatory.
// 0 value should be passed to mimic its absence in
// original [Recv] 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) Recv(got, expectedValue any, timeout time.Duration, args ...any) bool {
t.Helper()
return t.Cmp(got, Recv(expectedValue, timeout), args...)
}
// Set is a shortcut for:
//
// t.Cmp(got, td.Set(expectedItems...), args...)
//
// See [Set] 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) Set(got any, expectedItems []any, args ...any) bool {
t.Helper()
return t.Cmp(got, Set(expectedItems...), args...)
}
// Shallow is a shortcut for:
//
// t.Cmp(got, td.Shallow(expectedPtr), args...)
//
// See [Shallow] 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) Shallow(got, expectedPtr any, args ...any) bool {
t.Helper()
return t.Cmp(got, Shallow(expectedPtr), args...)
}
// Slice is a shortcut for:
//
// t.Cmp(got, td.Slice(model, expectedEntries), args...)
//
// See [Slice] for details.
//
// [Slice] optional parameter expectedEntries is here mandatory.
// nil value should be passed to mimic its absence in
// original [Slice] 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) Slice(got, model any, expectedEntries ArrayEntries, args ...any) bool {
t.Helper()
return t.Cmp(got, Slice(model, expectedEntries), args...)
}
// Smuggle is a shortcut for:
//
// t.Cmp(got, td.Smuggle(fn, expectedValue), args...)
//
// See [Smuggle] 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) Smuggle(got, fn, expectedValue any, args ...any) bool {
t.Helper()
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...)
//
// See [SStruct] for details.
//
// [SStruct] optional parameter expectedFields is here mandatory.
// nil value should be passed to mimic its absence in
// original [SStruct] 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) SStruct(got, model any, expectedFields StructFields, args ...any) bool {
t.Helper()
return t.Cmp(got, SStruct(model, expectedFields), args...)
}
// String is a shortcut for:
//
// t.Cmp(got, td.String(expected), args...)
//
// See [String] 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) String(got any, expected string, args ...any) bool {
t.Helper()
return t.Cmp(got, String(expected), args...)
}
// Struct is a shortcut for:
//
// t.Cmp(got, td.Struct(model, expectedFields), args...)
//
// See [Struct] for details.
//
// [Struct] optional parameter expectedFields is here mandatory.
// nil value should be passed to mimic its absence in
// original [Struct] 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) Struct(got, model any, expectedFields StructFields, args ...any) bool {
t.Helper()
return t.Cmp(got, Struct(model, expectedFields), args...)
}
// SubBagOf is a shortcut for:
//
// t.Cmp(got, td.SubBagOf(expectedItems...), args...)
//
// See [SubBagOf] 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) SubBagOf(got any, expectedItems []any, args ...any) bool {
t.Helper()
return t.Cmp(got, SubBagOf(expectedItems...), args...)
}
// SubJSONOf is a shortcut for:
//
// t.Cmp(got, td.SubJSONOf(expectedJSON, params...), args...)
//
// See [SubJSONOf] 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) SubJSONOf(got, expectedJSON any, params []any, args ...any) bool {
t.Helper()
return t.Cmp(got, SubJSONOf(expectedJSON, params...), args...)
}
// SubMapOf is a shortcut for:
//
// t.Cmp(got, td.SubMapOf(model, expectedEntries), args...)
//
// See [SubMapOf] for details.
//
// [SubMapOf] optional parameter expectedEntries is here mandatory.
// nil value should be passed to mimic its absence in
// original [SubMapOf] 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) SubMapOf(got, model any, expectedEntries MapEntries, args ...any) bool {
t.Helper()
return t.Cmp(got, SubMapOf(model, expectedEntries), args...)
}
// SubSetOf is a shortcut for:
//
// t.Cmp(got, td.SubSetOf(expectedItems...), args...)
//
// See [SubSetOf] 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) SubSetOf(got any, expectedItems []any, args ...any) bool {
t.Helper()
return t.Cmp(got, SubSetOf(expectedItems...), args...)
}
// SuperBagOf is a shortcut for:
//
// t.Cmp(got, td.SuperBagOf(expectedItems...), args...)
//
// See [SuperBagOf] 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) SuperBagOf(got any, expectedItems []any, args ...any) bool {
t.Helper()
return t.Cmp(got, SuperBagOf(expectedItems...), args...)
}
// SuperJSONOf is a shortcut for:
//
// t.Cmp(got, td.SuperJSONOf(expectedJSON, params...), args...)
//
// See [SuperJSONOf] 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) SuperJSONOf(got, expectedJSON any, params []any, args ...any) bool {
t.Helper()
return t.Cmp(got, SuperJSONOf(expectedJSON, params...), args...)
}
// SuperMapOf is a shortcut for:
//
// t.Cmp(got, td.SuperMapOf(model, expectedEntries), args...)
//
// See [SuperMapOf] for details.
//
// [SuperMapOf] optional parameter expectedEntries is here mandatory.
// nil value should be passed to mimic its absence in
// original [SuperMapOf] 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) SuperMapOf(got, model any, expectedEntries MapEntries, args ...any) bool {
t.Helper()
return t.Cmp(got, SuperMapOf(model, expectedEntries), args...)
}
// SuperSetOf is a shortcut for:
//
// t.Cmp(got, td.SuperSetOf(expectedItems...), args...)
//
// See [SuperSetOf] 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) SuperSetOf(got any, expectedItems []any, args ...any) bool {
t.Helper()
return t.Cmp(got, SuperSetOf(expectedItems...), args...)
}
// SuperSliceOf is a shortcut for:
//
// t.Cmp(got, td.SuperSliceOf(model, expectedEntries), args...)
//
// See [SuperSliceOf] for details.
//
// [SuperSliceOf] optional parameter expectedEntries is here mandatory.
// nil value should be passed to mimic its absence in
// original [SuperSliceOf] 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) SuperSliceOf(got, model any, expectedEntries ArrayEntries, args ...any) bool {
t.Helper()
return t.Cmp(got, SuperSliceOf(model, expectedEntries), args...)
}
// TruncTime is a shortcut for:
//
// t.Cmp(got, td.TruncTime(expectedTime, trunc), args...)
//
// See [TruncTime] for details.
//
// [TruncTime] optional parameter trunc is here mandatory.
// 0 value should be passed to mimic its absence in
// original [TruncTime] 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) TruncTime(got, expectedTime any, trunc time.Duration, args ...any) bool {
t.Helper()
return t.Cmp(got, TruncTime(expectedTime, trunc), args...)
}
// Values is a shortcut for:
//
// t.Cmp(got, td.Values(val), args...)
//
// See [Values] 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) Values(got, val any, args ...any) bool {
t.Helper()
return t.Cmp(got, Values(val), args...)
}
// Zero is a shortcut for:
//
// t.Cmp(got, td.Zero(), args...)
//
// See [Zero] 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) Zero(got any, args ...any) bool {
t.Helper()
return t.Cmp(got, Zero(), args...)
}
go-testdeep-1.15.0/td/t_anchor.go 0000664 0000000 0000000 00000021326 15144170453 0016561 0 ustar 00root root 0000000 0000000 // 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 td
import (
"reflect"
"sync"
"github.com/maxatome/go-testdeep/internal/anchors"
"github.com/maxatome/go-testdeep/internal/color"
)
// Anchors are stored globally by testing.TB.Name().
var allAnchors = map[string]*anchors.Info{}
var allAnchorsMu sync.Mutex
// AddAnchorableStructType declares a struct type as anchorable. fn
// is a function allowing to return a unique and identifiable instance
// of the struct type.
//
// fn has to have the following signature:
//
// func (nextAnchor int) TYPE
//
// TYPE is the struct type to make anchorable and nextAnchor is an
// index to allow to differentiate several instances of the same type.
//
// For example, the [time.Time] type which is anchorable by default,
// could be declared as:
//
// AddAnchorableStructType(func (nextAnchor int) time.Time {
// return time.Unix(int64(math.MaxInt64-1000424443-nextAnchor), 42)
// })
//
// Just as a note, the 1000424443 constant allows to avoid to flirt
// with the math.MaxInt64 extreme limit and so avoid possible
// collision with real world values.
//
// It panics if the provided fn is not a function or if it has not the
// expected signature (see above).
//
// See also [T.Anchor], [T.AnchorsPersistTemporarily],
// [T.DoAnchorsPersist], [T.ResetAnchors] and [T.SetAnchorsPersist].
func AddAnchorableStructType(fn any) {
err := anchors.AddAnchorableStructType(fn)
if err != nil {
panic(color.Bad(err.Error()))
}
}
// Anchor returns a typed value allowing to anchor the TestDeep
// operator operator in a go classic literal like a struct, slice,
// array or map value.
//
// If the TypeBehind method of operator returns non-nil, model can be
// omitted (like with [Between] operator in the example
// below). Otherwise, model should contain only one value
// corresponding to the returning type. It can be:
// - a go value: returning type is the type of the value,
// whatever the value is;
// - a [reflect.Type].
//
// It returns a typed value ready to be embed in a go data structure to
// be compared using [T.Cmp] or [T.CmpLax]:
//
// import (
// "testing"
//
// "github.com/maxatome/go-testdeep/td"
// )
//
// func TestFunc(tt *testing.T) {
// got := Func()
//
// t := td.NewT(tt)
// t.Cmp(got, &MyStruct{
// Name: "Bob",
// Details: &MyDetails{
// Nick: t.Anchor(td.HasPrefix("Bobby"), "").(string),
// Age: t.Anchor(td.Between(40, 50)).(int),
// },
// })
// }
//
// In this example:
//
// - [HasPrefix] operates on several input types (string,
// [fmt.Stringer], error, …), so its TypeBehind method returns always
// nil as it can not guess in advance on which type it operates. In
// this case, we must pass "" as model parameter in order to tell it
// to return the string type. Note that the .(string) type assertion
// is then mandatory to conform to the strict type checking.
// - [Between], on its side, knows the type on which it operates, as
// it is the same as the one of its parameters. So its TypeBehind
// method returns the right type, and so no need to pass it as model
// parameter. Note that the .(int) type assertion is still mandatory
// to conform to the strict type checking.
//
// Without operator anchoring feature, the previous example would have
// been:
//
// import (
// "testing"
//
// "github.com/maxatome/go-testdeep/td"
// )
//
// func TestFunc(tt *testing.T) {
// got := Func()
//
// t := td.NewT(tt)
// t.Cmp(got, td.Struct(&MyStruct{Name: "Bob"},
// td.StructFields{
// "Details": td.Struct(&MyDetails{},
// td.StructFields{
// "Nick": td.HasPrefix("Bobby"),
// "Age": td.Between(40, 50),
// }),
// }))
// }
//
// using two times the [Struct] operator to work around the strict type
// checking of golang.
//
// By default, the value returned by Anchor can only be used in the
// next [T.Cmp] or [T.CmpLax] call. To make it persistent across calls,
// see [T.SetAnchorsPersist] and [T.AnchorsPersistTemporarily] methods.
//
// See [T.A] method for a shorter synonym of Anchor.
//
// See also [T.AnchorsPersistTemporarily], [T.DoAnchorsPersist],
// [T.ResetAnchors], [T.SetAnchorsPersist] and [AddAnchorableStructType].
func (t *T) Anchor(operator TestDeep, model ...any) any {
if operator == nil {
t.Helper()
t.Fatal(color.Bad("Cannot anchor a nil TestDeep operator"))
}
var typ reflect.Type
if len(model) > 0 {
if len(model) != 1 {
t.Helper()
t.Fatal(color.TooManyParams("Anchor(OPERATOR[, MODEL])"))
}
var ok bool
typ, ok = model[0].(reflect.Type)
if !ok {
typ = reflect.TypeOf(model[0])
if typ == nil {
t.Helper()
t.Fatal(color.Bad("Untyped nil value is not valid as model for an anchor"))
}
}
typeBehind := operator.TypeBehind()
if typeBehind != nil && typeBehind != typ {
t.Helper()
t.Fatal(color.Bad("Operator %s TypeBehind() returned %s which differs from model type %s. Omit model or ensure its type is %[2]s",
operator.GetLocation().Func, typeBehind, typ))
}
} else {
typ = operator.TypeBehind()
if typ == nil {
t.Helper()
t.Fatal(color.Bad("Cannot anchor operator %s as TypeBehind() returned nil. Use model parameter to specify the type to return",
operator.GetLocation().Func))
}
}
nvm, err := t.Config.anchors.AddAnchor(typ, reflect.ValueOf(operator))
if err != nil {
t.Helper()
t.Fatal(color.Bad(err.Error()))
}
return nvm.Interface()
}
// A is a synonym for [T.Anchor].
//
// import (
// "testing"
//
// "github.com/maxatome/go-testdeep/td"
// )
//
// func TestFunc(tt *testing.T) {
// got := Func()
//
// t := td.NewT(tt)
// t.Cmp(got, &MyStruct{
// Name: "Bob",
// Details: &MyDetails{
// Nick: t.A(td.HasPrefix("Bobby"), "").(string),
// Age: t.A(td.Between(40, 50)).(int),
// },
// })
// }
//
// See also [T.AnchorsPersistTemporarily], [T.DoAnchorsPersist],
// [T.ResetAnchors], [T.SetAnchorsPersist] and [AddAnchorableStructType].
func (t *T) A(operator TestDeep, model ...any) any {
t.Helper()
return t.Anchor(operator, model...)
}
func (t *T) resetNonPersistentAnchors() {
t.Config.anchors.ResetAnchors(false)
}
// ResetAnchors frees all operators anchored with [T.Anchor]
// method. Unless operators anchoring persistence has been enabled
// with [T.SetAnchorsPersist], there is no need to call this
// method. Anchored operators are automatically freed after each [Cmp],
// [CmpDeeply] and [CmpPanic] call (or others methods calling them behind
// the scene).
//
// See also [T.Anchor], [T.AnchorsPersistTemporarily],
// [T.DoAnchorsPersist], [T.SetAnchorsPersist] and [AddAnchorableStructType].
func (t *T) ResetAnchors() {
t.Config.anchors.ResetAnchors(true)
}
// AnchorsPersistTemporarily is used by helpers to temporarily enable
// anchors persistence. See [tdhttp] package for an example of use. It
// returns a function to be deferred, to restore the normal behavior
// (clear anchored operators if persistence was false, do nothing
// otherwise).
//
// Typically used as:
//
// defer t.AnchorsPersistTemporarily()()
// // or
// t.Cleanup(t.AnchorsPersistTemporarily())
//
// See also [T.Anchor], [T.DoAnchorsPersist], [T.ResetAnchors],
// [T.SetAnchorsPersist] and [AddAnchorableStructType].
//
// [tdhttp]: https://pkg.go.dev/github.com/maxatome/go-testdeep/helpers/tdhttp
func (t *T) AnchorsPersistTemporarily() func() {
// If already persistent, do nothing on defer
if t.DoAnchorsPersist() {
return func() {}
}
t.SetAnchorsPersist(true)
return func() {
t.SetAnchorsPersist(false)
t.Config.anchors.ResetAnchors(true)
}
}
// DoAnchorsPersist returns true if anchors persistence is enabled,
// false otherwise.
//
// See also [T.Anchor], [T.AnchorsPersistTemporarily],
// [T.ResetAnchors], [T.SetAnchorsPersist] and [AddAnchorableStructType].
func (t *T) DoAnchorsPersist() bool {
return t.Config.anchors.DoAnchorsPersist()
}
// SetAnchorsPersist allows to enable or disable anchors persistence.
//
// See also [T.Anchor], [T.AnchorsPersistTemporarily],
// [T.DoAnchorsPersist], [T.ResetAnchors] and [AddAnchorableStructType].
func (t *T) SetAnchorsPersist(persist bool) {
t.Config.anchors.SetAnchorsPersist(persist)
}
func (t *T) initAnchors() {
if t.Config.anchors != nil {
return
}
name := t.Name()
allAnchorsMu.Lock()
defer allAnchorsMu.Unlock()
t.Config.anchors = allAnchors[name]
if t.Config.anchors == nil {
t.Config.anchors = anchors.NewInfo()
allAnchors[name] = t.Config.anchors
// Do not record a finalizer if no name (should not happen
// except perhaps in tests)
if name != "" {
t.Cleanup(func() {
allAnchorsMu.Lock()
defer allAnchorsMu.Unlock()
delete(allAnchors, name)
})
}
}
}
go-testdeep-1.15.0/td/t_anchor_118.go 0000664 0000000 0000000 00000001012 15144170453 0017140 0 ustar 00root root 0000000 0000000 // Copyright (c) 2023, 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 td
// Anchor is a generic shortcut to [T.Anchor].
func Anchor[X any](t *T, operator TestDeep) X {
var model X
return t.Anchor(operator, model).(X)
}
// A is a generic shortcut to [T.A].
func A[X any](t *T, operator TestDeep) X {
var model X
return t.A(operator, model).(X)
}
go-testdeep-1.15.0/td/t_anchor_118_test.go 0000664 0000000 0000000 00000002474 15144170453 0020214 0 ustar 00root root 0000000 0000000 // Copyright (c) 2023, 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 td_test
import (
"testing"
"time"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestAnchor(tt *testing.T) {
ttt := test.NewTestingTB(tt.Name())
t := td.NewT(ttt)
type MyStruct struct {
PNum *int
Num int64
Str string
Slice []int
Map map[string]bool
Time time.Time
}
n := 42
got := MyStruct{
PNum: &n,
Num: 136,
Str: "Pipo bingo",
Time: timeParse(tt, "2019-01-02T11:22:33.123456Z"),
}
td.CmpTrue(tt,
t.Cmp(got, MyStruct{
PNum: td.Anchor[*int](t, td.Ptr(td.Between(40, 45))),
Num: td.Anchor[int64](t, td.Between(int64(135), int64(137))),
Str: td.Anchor[string](t, td.HasPrefix("Pipo")),
Time: td.Anchor[time.Time](t, td.TruncTime(timeParse(tt, "2019-01-02T11:22:00Z"), time.Minute)),
}))
td.CmpTrue(tt,
t.Cmp(got, MyStruct{
PNum: td.A[*int](t, td.Ptr(td.Between(40, 45))),
Num: td.A[int64](t, td.Between(int64(135), int64(137))),
Str: td.A[string](t, td.HasPrefix("Pipo")),
Time: td.A[time.Time](t, td.TruncTime(timeParse(tt, "2019-01-02T11:22:00Z"), time.Minute)),
}))
}
go-testdeep-1.15.0/td/t_anchor_test.go 0000664 0000000 0000000 00000012042 15144170453 0017613 0 ustar 00root root 0000000 0000000 // 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 td_test
import (
"testing"
"time"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func timeParse(t *testing.T, s string) time.Time {
dt, err := time.Parse(time.RFC3339Nano, s)
if err != nil {
t.Helper()
t.Fatalf("Cannot parse `%s`: %s", s, err)
}
return dt
}
func TestT_Anchor(tt *testing.T) {
ttt := test.NewTestingTB(tt.Name())
t := td.NewT(ttt)
type MyStruct struct {
PNum *int
Num int64
Str string
Slice []int
Map map[string]bool
Time time.Time
}
n := 42
got := MyStruct{
PNum: &n,
Num: 136,
Str: "Pipo bingo",
Time: timeParse(tt, "2019-01-02T11:22:33.123456Z"),
}
// Using T.Anchor()
td.CmpTrue(tt,
t.Cmp(got, MyStruct{
PNum: t.Anchor(td.Ptr(td.Between(40, 45))).(*int),
Num: t.Anchor(td.Between(int64(135), int64(137))).(int64),
Str: t.Anchor(td.HasPrefix("Pipo"), "").(string),
Time: t.Anchor(td.TruncTime(timeParse(tt, "2019-01-02T11:22:00Z"), time.Minute)).(time.Time),
}))
// Using T.A()
td.CmpTrue(tt,
t.Cmp(got, MyStruct{
PNum: t.A(td.Ptr(td.Between(40, 45))).(*int),
Num: t.A(td.Between(int64(135), int64(137))).(int64),
Str: t.A(td.HasPrefix("Pipo"), "").(string),
Time: t.A(td.TruncTime(timeParse(tt, "2019-01-02T11:22:00Z"), time.Minute)).(time.Time),
}))
// Testing persistence
got = MyStruct{Num: 136}
tt.Run("without persistence", func(tt *testing.T) {
numOp := t.Anchor(td.Between(int64(135), int64(137))).(int64)
td.CmpTrue(tt, t.Cmp(got, MyStruct{Num: numOp}))
td.CmpFalse(tt, t.Cmp(got, MyStruct{Num: numOp}))
})
tt.Run("with persistence", func(tt *testing.T) {
numOp := t.Anchor(td.Between(int64(135), int64(137))).(int64)
defer t.AnchorsPersistTemporarily()()
td.CmpTrue(tt, t.Cmp(got, MyStruct{Num: numOp}))
td.CmpTrue(tt, t.Cmp(got, MyStruct{Num: numOp}))
t.ResetAnchors() // force reset anchored operators
td.CmpFalse(tt, t.Cmp(got, MyStruct{Num: numOp}))
})
// Errors
tt.Run("errors", func(tt *testing.T) {
td.Cmp(tt, ttt.CatchFatal(func() { t.Anchor(nil) }),
"Cannot anchor a nil TestDeep operator")
td.Cmp(tt, ttt.CatchFatal(func() { t.Anchor(td.Ignore(), 1, 2) }),
"usage: Anchor(OPERATOR[, MODEL]), too many parameters")
td.Cmp(tt, ttt.CatchFatal(func() { t.Anchor(td.Ignore(), nil) }),
"Untyped nil value is not valid as model for an anchor")
td.Cmp(tt, ttt.CatchFatal(func() { t.Anchor(td.Between(1, 2), 12.3) }),
"Operator Between TypeBehind() returned int which differs from model type float64. Omit model or ensure its type is int")
td.Cmp(tt, ttt.CatchFatal(func() { t.Anchor(td.Ignore()) }),
"Cannot anchor operator Ignore as TypeBehind() returned nil. Use model parameter to specify the type to return")
})
}
type privStruct struct {
num int64
}
func (p privStruct) Num() int64 {
return p.num
}
func TestAddAnchorableStructType(tt *testing.T) {
type MyStruct struct {
Priv privStruct
}
ttt := test.NewTestingTB(tt.Name())
t := td.NewT(ttt)
// We want to anchor this operator
op := td.Smuggle((privStruct).Num, int64(42))
// Without making privStruct anchorable, it does not work
td.Cmp(tt, ttt.CatchFatal(func() { t.A(op, privStruct{}) }),
"td_test.privStruct struct type is not supported as an anchor. Try AddAnchorableStructType")
// Make privStruct anchorable
td.AddAnchorableStructType(func(nextAnchor int) privStruct {
return privStruct{num: int64(2e9 - nextAnchor)}
})
td.CmpTrue(tt,
t.Cmp(MyStruct{Priv: privStruct{num: 42}},
MyStruct{
Priv: t.A(op, privStruct{}).(privStruct), // ← now it works
}))
// Error
test.CheckPanic(tt,
func() { td.AddAnchorableStructType(123) },
"usage: AddAnchorableStructType(func (nextAnchor int) STRUCT_TYPE)")
}
func TestT_AnchorsPersist(tt *testing.T) {
ttt := test.NewTestingTB(tt.Name())
t1 := td.NewT(ttt)
t2 := td.NewT(ttt)
t3 := td.NewT(t1)
tt.Run("without anchors persistence", func(tt *testing.T) {
// Anchors persistence is shared for a same testing.TB
td.CmpFalse(tt, t1.DoAnchorsPersist())
td.CmpFalse(tt, t2.DoAnchorsPersist())
td.CmpFalse(tt, t3.DoAnchorsPersist())
func() {
defer t1.AnchorsPersistTemporarily()()
td.CmpTrue(tt, t1.DoAnchorsPersist())
td.CmpTrue(tt, t2.DoAnchorsPersist())
td.CmpTrue(tt, t3.DoAnchorsPersist())
}()
td.CmpFalse(tt, t1.DoAnchorsPersist())
td.CmpFalse(tt, t2.DoAnchorsPersist())
td.CmpFalse(tt, t3.DoAnchorsPersist())
})
tt.Run("with anchors persistence", func(tt *testing.T) {
t3.SetAnchorsPersist(true)
td.CmpTrue(tt, t1.DoAnchorsPersist())
td.CmpTrue(tt, t2.DoAnchorsPersist())
td.CmpTrue(tt, t3.DoAnchorsPersist())
func() {
defer t1.AnchorsPersistTemporarily()()
td.CmpTrue(tt, t1.DoAnchorsPersist())
td.CmpTrue(tt, t2.DoAnchorsPersist())
td.CmpTrue(tt, t3.DoAnchorsPersist())
}()
td.CmpTrue(tt, t1.DoAnchorsPersist())
td.CmpTrue(tt, t2.DoAnchorsPersist())
td.CmpTrue(tt, t3.DoAnchorsPersist())
})
}
go-testdeep-1.15.0/td/t_hooks.go 0000664 0000000 0000000 00000011733 15144170453 0016433 0 ustar 00root root 0000000 0000000 // Copyright (c) 2020, 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 (
"github.com/maxatome/go-testdeep/internal/color"
)
// WithCmpHooks returns a new [*T] instance with new Cmp hooks recorded
// using functions passed in fns.
//
// Each function in fns has to be a function with the following
// possible signatures:
//
// func (got, expected A) bool
// func (got, expected A) error
//
// First arg is always got, and second is always expected.
//
// A cannot be an interface. This restriction can be removed in the
// future, if really needed.
//
// This function is called as soon as possible each time the type A is
// encountered for got while expected type is assignable to A.
//
// When it returns a bool, false means A is not equal to B.
//
// When it returns a non-nil error (meaning got ≠ expected), its
// content is used to tell the reason of the failure.
//
// Cmp hooks are checked before [UseEqual] feature.
//
// Cmp hooks are run just after [Smuggle] hooks.
//
// func TestCmpHook(tt *testing.T) {
// t := td.NewT(tt)
//
// // Test reflect.Value contents instead of default field/field
// t = t.WithCmpHooks(func (got, expected reflect.Value) bool {
// return td.EqDeeply(got.Interface(), expected.Interface())
// })
// a, b := 1, 1
// t.Cmp(reflect.ValueOf(&a), reflect.ValueOf(&b)) // succeeds
//
// // Test reflect.Type correctly instead of default field/field
// t = t.WithCmpHooks(func (got, expected reflect.Type) bool {
// return got == expected
// })
//
// // Test time.Time via its Equal() method instead of default
// // field/field (note it bypasses the UseEqual flag)
// t = t.WithCmpHooks((time.Time).Equal)
// date, _ := time.Parse(time.RFC3339, "2020-09-08T22:13:54+02:00")
// t.Cmp(date, date.UTC()) // succeeds
//
// // Several hooks can be declared at once
// t = t.WithCmpHooks(
// func (got, expected reflect.Value) bool {
// return td.EqDeeply(got.Interface(), expected.Interface())
// },
// func (got, expected reflect.Type) bool {
// return got == expected
// },
// (time.Time).Equal,
// )
// }
//
// There is no way to add or remove hooks of an existing [*T]
// instance, only to create a new [*T] instance with this method or
// [T.WithSmuggleHooks] to add some.
//
// WithCmpHooks calls t.Fatal if an item of fns is not a function or
// if its signature does not match the expected ones.
//
// See also [T.WithSmuggleHooks].
//
// [UseEqual]: https://pkg.go.dev/github.com/maxatome/go-testdeep/td#ContextConfig.UseEqual
func (t *T) WithCmpHooks(fns ...any) *T {
t = t.copyWithHooks()
err := t.Config.hooks.AddCmpHooks(fns)
if err != nil {
t.Helper()
t.Fatal(color.Bad("WithCmpHooks " + err.Error()))
}
return t
}
// WithSmuggleHooks returns a new [*T] instance with new Smuggle hooks
// recorded using functions passed in fns.
//
// Each function in fns has to be a function with the following
// possible signatures:
//
// func (got A) B
// func (got A) (B, error)
//
// A cannot be an interface. This restriction can be removed in the
// future, if really needed.
//
// B cannot be an interface. If you have a use case, we can talk about it.
//
// This function is called as soon as possible each time the type A is
// encountered for got.
//
// The B value returned replaces the got value for subsequent tests.
// Smuggle hooks are NOT run again for this returned value to avoid
// easy infinite loop recursion.
//
// When it returns non-nil error (meaning something wrong happened
// during the conversion of A to B), it raises a global error and its
// content is used to tell the reason of the failure.
//
// Smuggle hooks are run just before Cmp hooks.
//
// func TestSmuggleHook(tt *testing.T) {
// t := td.NewT(tt)
//
// // Each encountered int is changed to a bool
// t = t.WithSmuggleHooks(func (got int) bool {
// return got != 0
// })
// t.Cmp(map[string]int{"ok": 1, "no": 0},
// map[string]bool{"ok", true, "no", false}) // succeeds
//
// // Each encountered string is converted to int
// t = t.WithSmuggleHooks(strconv.Atoi)
// t.Cmp("123", 123) // succeeds
//
// // Several hooks can be declared at once
// t = t.WithSmuggleHooks(
// func (got int) bool { return got != 0 },
// strconv.Atoi,
// )
// }
//
// There is no way to add or remove hooks of an existing [*T]
// instance, only create a new [*T] instance with this method or
// [T.WithCmpHooks] to add some.
//
// WithSmuggleHooks calls t.Fatal if an item of fns is not a
// function or if its signature does not match the expected ones.
//
// See also [T.WithCmpHooks].
func (t *T) WithSmuggleHooks(fns ...any) *T {
t = t.copyWithHooks()
err := t.Config.hooks.AddSmuggleHooks(fns)
if err != nil {
t.Helper()
t.Fatal(color.Bad("WithSmuggleHooks " + err.Error()))
}
return t
}
func (t *T) copyWithHooks() *T {
nt := NewT(t)
nt.Config.hooks = t.Config.hooks.Copy()
return nt
}
go-testdeep-1.15.0/td/t_hooks_test.go 0000664 0000000 0000000 00000017335 15144170453 0017476 0 ustar 00root root 0000000 0000000 // Copyright (c) 2020, 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 (
"errors"
"fmt"
"reflect"
"strconv"
"strings"
"testing"
"time"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestWithCmpHooks(tt *testing.T) {
na, nb := 1234, 1234
date, _ := time.Parse(time.RFC3339, "2020-09-08T22:13:54+02:00")
for _, tst := range []struct {
name string
cmp any
got, expected any
}{
{
name: "reflect.Value",
cmp: func(got, expected reflect.Value) bool {
return td.EqDeeply(got.Interface(), expected.Interface())
},
got: reflect.ValueOf(&na),
expected: reflect.ValueOf(&nb),
},
{
name: "time.Time",
cmp: (time.Time).Equal,
got: date,
expected: date.UTC(),
},
{
name: "numify",
cmp: func(got, expected string) error {
ngot, err := strconv.Atoi(got)
if err != nil {
return fmt.Errorf("strconv.Atoi(got) failed: %s", err)
}
nexpected, err := strconv.Atoi(expected)
if err != nil {
return fmt.Errorf("strconv.Atoi(expected) failed: %s", err)
}
if ngot != nexpected {
return errors.New("values differ")
}
return nil
},
got: "0000001234",
expected: "1234",
},
{
name: "false test :)",
cmp: func(got, expected int) bool {
return got == -expected
},
got: 1,
expected: -1,
},
} {
tt.Run(tst.name, func(tt *testing.T) {
ttt := test.NewTestingTB(tt.Name())
t := td.NewT(ttt)
td.CmpFalse(tt, func() bool {
// A panic can occur when -tags safe:
// dark.GetInterface() does not handle private unsafe.Pointer kind
defer func() { recover() }() //nolint: errcheck
return t.Cmp(tst.got, tst.expected)
}())
t = t.WithCmpHooks(tst.cmp)
td.CmpTrue(tt, t.Cmp(tst.got, tst.expected))
})
}
tt.Run("Error", func(tt *testing.T) {
ttt := test.NewTestingTB(tt.Name())
t := td.NewT(ttt).
WithCmpHooks(func(got, expected int) error {
return errors.New("never equal")
})
td.CmpFalse(tt, t.Cmp(1, 1))
if !strings.Contains(ttt.LastMessage(), "DATA: never equal\n") {
tt.Errorf(`<%s> does not contain "DATA: never equal\n"`, ttt.LastMessage())
}
})
for _, tst := range []struct {
name string
cmp any
fatal string
}{
{
name: "not a function",
cmp: "Booh",
fatal: "WithCmpHooks expects a function, not a string",
},
{
name: "wrong signature",
cmp: func(a []int, b ...int) bool { return false },
fatal: "WithCmpHooks expects: func (T, T) bool|error not ",
},
} {
tt.Run("panic: "+tst.name, func(tt *testing.T) {
ttt := test.NewTestingTB(tt.Name())
t := td.NewT(ttt)
fatalMesg := ttt.CatchFatal(func() { t.WithCmpHooks(tst.cmp) })
test.IsTrue(tt, ttt.IsFatal)
if !strings.Contains(fatalMesg, tst.fatal) {
tt.Errorf(`<%s> does not contain %q`, fatalMesg, tst.fatal)
}
})
}
}
func TestWithSmuggleHooks(tt *testing.T) {
for _, tst := range []struct {
name string
cmp any
got, expected any
}{
{
name: "abs",
cmp: func(got int) int {
if got < 0 {
return -got
}
return got
},
got: -1234,
expected: 1234,
},
{
name: "int2bool",
cmp: func(got int) bool { return got != 0 },
got: 1,
expected: true,
},
{
name: "Atoi",
cmp: strconv.Atoi,
got: "1234",
expected: 1234,
},
{
name: "reflect2any",
cmp: func(v reflect.Value) any {
return v.Interface()
},
got: reflect.ValueOf(123),
expected: 123,
},
} {
tt.Run(tst.name, func(tt *testing.T) {
ttt := test.NewTestingTB(tt.Name())
t := td.NewT(ttt)
td.CmpFalse(tt, t.Cmp(tst.got, tst.expected))
t = t.WithSmuggleHooks(tst.cmp)
td.CmpTrue(tt, t.Cmp(tst.got, tst.expected))
})
}
tt.Run("Array", func(tt *testing.T) {
ttt := test.NewTestingTB(tt.Name())
t := td.NewT(ttt).WithSmuggleHooks(func(got reflect.Value) any {
if got.IsValid() {
return got.Interface()
}
return nil
})
got := [3]reflect.Value{1: reflect.ValueOf(42)}
td.CmpTrue(tt, td.Cmp(t, got, td.Array([3]reflect.Value{}, td.ArrayEntries{
0: nil, // if omitted, td.Array sets it to reflect.Value{}
1: 42,
2: nil, // if omitted, td.Array sets it to reflect.Value{}
})))
test.EqualStr(tt, ttt.LastMessage(), "")
ttt.ResetMessages()
td.CmpTrue(tt, td.Cmp(t, got[:], td.Slice([]reflect.Value{}, td.ArrayEntries{
0: nil, // if omitted, td.Array sets it to reflect.Value{}
1: 42,
2: nil, // if omitted, td.Array sets it to reflect.Value{}
})))
test.EqualStr(tt, ttt.LastMessage(), "")
ttt.ResetMessages()
td.CmpTrue(tt, td.Cmp(t, got[:], td.SuperSliceOf([]reflect.Value{}, td.ArrayEntries{
1: 42,
})))
test.EqualStr(tt, ttt.LastMessage(), "")
ttt.ResetMessages()
})
tt.Run("Map", func(tt *testing.T) {
ttt := test.NewTestingTB(tt.Name())
t := td.NewT(ttt).WithSmuggleHooks(func(got reflect.Value) any {
if got.IsValid() {
return got.Interface()
}
return nil
})
got := map[string]reflect.Value{
"pipo": reflect.ValueOf(42),
"bingo": reflect.ValueOf(666),
"zip": {},
}
td.CmpTrue(tt, td.Cmp(t, got, td.Map(map[string]reflect.Value{}, td.MapEntries{
"pipo": 42,
"bingo": 666,
"zip": nil,
})))
test.EqualStr(tt, ttt.LastMessage(), "")
ttt.ResetMessages()
td.CmpTrue(tt, td.Cmp(t, got, td.SubMapOf(map[string]reflect.Value{}, td.MapEntries{
"pipo": 42,
"bingo": 666,
"zip": nil,
"test": "more",
})))
test.EqualStr(tt, ttt.LastMessage(), "")
ttt.ResetMessages()
td.CmpTrue(tt, td.Cmp(t, got, td.SuperMapOf(map[string]reflect.Value{}, td.MapEntries{
"pipo": 42,
"zip": nil,
})))
test.EqualStr(tt, ttt.LastMessage(), "")
ttt.ResetMessages()
})
tt.Run("Struct", func(tt *testing.T) {
ttt := test.NewTestingTB(tt.Name())
t := td.NewT(ttt).WithSmuggleHooks(func(got reflect.Value) any {
if got.IsValid() {
return got.Interface()
}
return nil
})
type Rstruct struct {
V1 reflect.Value
V2 reflect.Value
}
got := Rstruct{V1: reflect.ValueOf(42)}
td.CmpTrue(tt, td.Cmp(t, got, td.Struct(Rstruct{}, td.StructFields{
"V1": 42,
"V2": nil,
})))
test.EqualStr(tt, ttt.LastMessage(), "")
ttt.ResetMessages()
td.CmpTrue(tt, td.Cmp(t, got, td.SStruct(Rstruct{}, td.StructFields{
"V1": 42,
"V2": nil,
})))
test.EqualStr(tt, ttt.LastMessage(), "")
ttt.ResetMessages()
td.CmpTrue(tt, td.Cmp(t, got, td.Struct(nil, td.StructFields{
"V1": 42,
"V2": nil,
})))
test.EqualStr(tt, ttt.LastMessage(), "")
ttt.ResetMessages()
})
tt.Run("Error", func(tt *testing.T) {
ttt := test.NewTestingTB(tt.Name())
t := td.NewT(ttt).WithSmuggleHooks(func(got int) (int, error) {
return 0, errors.New("never equal")
})
td.CmpFalse(tt, t.Cmp(1, 1))
if !strings.Contains(ttt.LastMessage(), "DATA: never equal\n") {
tt.Errorf(`<%s> does not contain "DATA: never equal\n"`, ttt.LastMessage())
}
})
for _, tst := range []struct {
name string
cmp any
fatal string
}{
{
name: "not a function",
cmp: "Booh",
fatal: "WithSmuggleHooks expects a function, not a string",
},
{
name: "wrong signature",
cmp: func(a []int, b ...int) bool { return false },
fatal: "WithSmuggleHooks expects: func (A) (B[, error]) not ",
},
} {
tt.Run("panic: "+tst.name, func(tt *testing.T) {
ttt := test.NewTestingTB(tt.Name())
t := td.NewT(ttt)
fatalMesg := ttt.CatchFatal(func() { t.WithSmuggleHooks(tst.cmp) })
test.IsTrue(tt, ttt.IsFatal)
if !strings.Contains(fatalMesg, tst.fatal) {
tt.Errorf(`<%s> does not contain %q`, fatalMesg, tst.fatal)
}
})
}
}
go-testdeep-1.15.0/td/t_struct.go 0000664 0000000 0000000 00000067622 15144170453 0016644 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 td
import (
"reflect"
"strings"
"sync"
"testing"
"github.com/maxatome/go-testdeep/helpers/tdutil"
"github.com/maxatome/go-testdeep/internal/color"
"github.com/maxatome/go-testdeep/internal/trace"
"github.com/maxatome/go-testdeep/internal/types"
)
// T is a type that encapsulates [testing.TB] interface (which is
// implemented by [*testing.T] and [*testing.B]) allowing to easily use
// [*testing.T] methods as well as T ones.
type T struct {
testing.TB
Config ContextConfig // defaults to DefaultContextConfig
}
var _ testing.TB = T{}
// NewT returns a new [*T] instance. Typically used as:
//
// import (
// "testing"
//
// "github.com/maxatome/go-testdeep/td"
// )
//
// type Record struct {
// Id uint64
// Name string
// Age int
// CreatedAt time.Time
// }
//
// func TestCreateRecord(tt *testing.T) {
// t := NewT(tt, ContextConfig{
// MaxErrors: 3, // in case of failure, will dump up to 3 errors
// })
//
// before := time.Now()
// record, err := CreateRecord()
//
// if t.CmpNoError(err) {
// t.Log("No error, can now check struct contents")
//
// ok := t.Struct(record,
// &Record{
// Name: "Bob",
// Age: 23,
// },
// td.StructFields{
// "Id": td.NotZero(),
// "CreatedAt": td.Between(before, time.Now()),
// },
// "Newly created record")
// if ok {
// t.Log(Record created successfully!")
// }
// }
// }
//
// config is an optional parameter and, if passed, must be unique. It
// allows to configure how failures will be rendered during the
// lifetime of the returned instance.
//
// t := NewT(tt)
// t.Cmp(
// Record{Age: 12, Name: "Bob", Id: 12}, // got
// Record{Age: 21, Name: "John", Id: 28}) // expected
//
// will produce:
//
// === RUN TestFoobar
// --- FAIL: TestFoobar (0.00s)
// foobar_test.go:88: Failed test
// DATA.Id: values differ
// got: (uint64) 12
// expected: (uint64) 28
// DATA.Name: values differ
// got: "Bob"
// expected: "John"
// DATA.Age: values differ
// got: 12
// expected: 28
// FAIL
//
// Now with a special configuration:
//
// t := NewT(tt, ContextConfig{
// RootName: "RECORD", // got data named "RECORD" instead of "DATA"
// MaxErrors: 2, // stops after 2 errors instead of default 10
// })
// t.Cmp(
// Record{Age: 12, Name: "Bob", Id: 12}, // got
// Record{Age: 21, Name: "John", Id: 28}, // expected
// )
//
// will produce:
//
// === RUN TestFoobar
// --- FAIL: TestFoobar (0.00s)
// foobar_test.go:96: Failed test
// RECORD.Id: values differ
// got: (uint64) 12
// expected: (uint64) 28
// RECORD.Name: values differ
// got: "Bob"
// expected: "John"
// Too many errors (use TESTDEEP_MAX_ERRORS=-1 to see all)
// FAIL
//
// See [T.RootName] method to configure RootName in a more specific fashion.
//
// Note that setting MaxErrors to a negative value produces a dump
// with all errors.
//
// If MaxErrors is not set (or set to 0), it is set to
// DefaultContextConfig.MaxErrors which is potentially dependent from
// the TESTDEEP_MAX_ERRORS environment variable (else defaults to 10.)
// See [ContextConfig] documentation for details.
//
// Of course t can already be a [*T], in this special case if config
// is omitted, the Config of the new instance is a copy of the t
// Config, including hooks.
//
// See also other constructors [Assert], [Require] and [AssertRequire].
//
// See also configurators [T.Assert], [T.Require], [T.RootName],
// [T.FailureIsFatal], [T.UseEqual], [T.BeLax], [T.IgnoreUnexported]
// and [T.TestDeepInGotOK].
func NewT(t testing.TB, config ...ContextConfig) *T {
var newT T
const usage = "NewT(testing.TB[, ContextConfig])"
if t == nil {
panic(color.BadUsage(usage, nil, 1, false))
}
if len(config) > 1 {
t.Helper()
t.Fatal(color.TooManyParams(usage))
}
// Already a *T, so steal its testing.TB and its Config if needed
if tdT, ok := t.(*T); ok {
newT.TB = tdT.TB
if len(config) == 0 {
newT.Config = tdT.Config
} else {
newT.Config = config[0]
}
} else {
newT.TB = t
if len(config) == 0 {
newT.Config = DefaultContextConfig
} else {
newT.Config = config[0]
}
}
newT.Config.sanitize()
newT.initAnchors()
return &newT
}
// Assert returns a new [*T] instance with FailureIsFatal flag set to
// false.
//
// assert := Assert(t)
//
// is roughly equivalent to:
//
// assert := NewT(t).FailureIsFatal(false)
//
// See [NewT] documentation for usefulness of config optional parameter.
//
// See also other constructors [Require] and [AssertRequire].
//
// See also configurators [T.Assert], [T.Require], [T.RootName],
// [T.FailureIsFatal], [T.UseEqual], [T.BeLax], [T.IgnoreUnexported]
// and [T.TestDeepInGotOK].
func Assert(t testing.TB, config ...ContextConfig) *T {
return NewT(t, config...).FailureIsFatal(false)
}
// Require returns a new [*T] instance with FailureIsFatal flag set to
// true.
//
// require := Require(t)
//
// is roughly equivalent to:
//
// require := NewT(t).FailureIsFatal(true)
//
// See [NewT] documentation for usefulness of config optional parameter.
//
// See also other constructors [Assert] and [AssertRequire].
//
// See also configurators [T.Assert], [T.Require], [T.RootName],
// [T.FailureIsFatal], [T.UseEqual], [T.BeLax], [T.IgnoreUnexported]
// and [T.TestDeepInGotOK].
func Require(t testing.TB, config ...ContextConfig) *T {
return NewT(t, config...).FailureIsFatal()
}
// AssertRequire returns 2 instances of [*T]. assert with
// FailureIsFatal flag set to false, and require with FailureIsFatal
// flag set to true.
//
// assert, require := AssertRequire(t)
//
// is roughly equivalent to:
//
// assert, require := Assert(t), Require(t)
//
// See [NewT] documentation for usefulness of config optional parameter.
//
// See also other constructors [Assert] and [Require].
//
// See also configurators [T.Assert], [T.Require], [T.RootName],
// [T.FailureIsFatal], [T.UseEqual], [T.BeLax], [T.IgnoreUnexported]
// and [T.TestDeepInGotOK].
func AssertRequire(t testing.TB, config ...ContextConfig) (assert, require *T) {
assert = Assert(t, config...)
require = assert.FailureIsFatal()
return
}
// RootName changes the name of the got data. By default it is
// "DATA". For an HTTP response body, it could be "BODY" for example.
//
// It returns a new instance of [*T] so does not alter the original t
// and is used as follows:
//
// t.RootName("RECORD").
// Struct(record,
// &Record{
// Name: "Bob",
// Age: 23,
// },
// td.StructFields{
// "Id": td.NotZero(),
// "CreatedAt": td.Between(before, time.Now()),
// },
// "Newly created record")
//
// In case of error for the field Age, the failure message will contain:
//
// RECORD.Age: values differ
//
// Which is more readable than the generic:
//
// DATA.Age: values differ
//
// If "" is passed the name is set to "DATA", the default value.
//
// See also other configurators [T.Assert], [T.Require],
// [T.FailureIsFatal], [T.UseEqual], [T.BeLax], [T.IgnoreUnexported]
// and [T.TestDeepInGotOK].
func (t *T) RootName(rootName string) *T {
nt := *t
if rootName == "" {
rootName = contextDefaultRootName
}
nt.Config.RootName = rootName
return &nt
}
// FailureIsFatal allows to choose whether t.TB.Fatal() or
// t.TB.Error() will be used to print the next failure reports. When
// enable is true (or missing) testing.Fatal() will be called, else
// testing.Error(). Using [*testing.T] or [*testing.B] instance as
// t.TB value, FailNow() method is called behind the scenes when
// Fatal() is called. See [testing] documentation for details.
//
// It returns a new instance of [*T] so does not alter the original t
// and used as follows:
//
// // Following t.Cmp() will call Fatal() if failure
// t = t.FailureIsFatal()
// t.Cmp(...)
// t.Cmp(...)
// // Following t.Cmp() won't call Fatal() if failure
// t = t.FailureIsFatal(false)
// t.Cmp(...)
//
// or, if only one call is critic:
//
// // This Cmp() call will call Fatal() if failure
// t.FailureIsFatal().Cmp(...)
// // Following t.Cmp() won't call Fatal() if failure
// t.Cmp(...)
// t.Cmp(...)
//
// Note that t.FailureIsFatal() acts as t.FailureIsFatal(true).
//
// See also other configurators [T.Assert], [T.Require], [T.RootName],
// [T.UseEqual], [T.BeLax], [T.IgnoreUnexported] and
// [T.TestDeepInGotOK].
func (t *T) FailureIsFatal(enable ...bool) *T {
nt := *t
nt.Config.FailureIsFatal = len(enable) == 0 || enable[0]
return &nt
}
// Assert returns a new [*T] instance inheriting the t config but with
// FailureIsFatal flag set to false.
//
// It returns a new instance of [*T] so does not alter the original t
//
// It is a shortcut for:
//
// t.FailureIsFatal(false)
//
// See also other configurators [T.Require], [T.RootName],
// [T.FailureIsFatal], [T.UseEqual], [T.BeLax], [T.IgnoreUnexported]
// and [T.TestDeepInGotOK].
func (t *T) Assert() *T {
return t.FailureIsFatal(false)
}
// Require returns a new [*T] instance inheriting the t config but
// with FailureIsFatal flag set to true.
//
// It returns a new instance of [*T] so does not alter the original t
//
// It is a shortcut for:
//
// t.FailureIsFatal(true)
//
// See also other configurators [T.Assert], [T.RootName],
// [T.FailureIsFatal], [T.UseEqual], [T.BeLax], [T.IgnoreUnexported]
// and [T.TestDeepInGotOK].
func (t *T) Require() *T {
return t.FailureIsFatal(true)
}
// UseEqual tells go-testdeep to delegate the comparison of items
// whose type is one of types to their Equal() method.
//
// The signature this method should be:
//
// (A) Equal(B) bool
//
// with B assignable to A.
//
// See [time.Time.Equal] as an example of accepted Equal() method.
//
// It always returns a new instance of [*T] so does not alter the
// original t.
//
// t = t.UseEqual(time.Time{}, net.IP{})
//
// types items can also be [reflect.Type] items. In this case, the
// target type is the one reflected by the [reflect.Type].
//
// t = t.UseEqual(reflect.TypeOf(time.Time{}), reflect.typeOf(net.IP{}))
//
// As a special case, calling t.UseEqual() or t.UseEqual(true) returns
// an instance using the Equal() method globally, for all types owning
// an Equal() method. Other types fall back to the default comparison
// mechanism. t.UseEqual(false) returns an instance not using Equal()
// method anymore, except for types already recorded using a previous
// UseEqual call.
//
// See also other configurators [T.Assert], [T.Require], [T.RootName],
// [T.FailureIsFatal], [T.BeLax], [T.IgnoreUnexported] and
// [T.TestDeepInGotOK].
func (t *T) UseEqual(types ...any) *T {
// special case: UseEqual()
if len(types) == 0 {
nt := *t
nt.Config.UseEqual = true
return &nt
}
// special cases: UseEqual(true) or UseEqual(false)
if len(types) == 1 {
if ignore, ok := types[0].(bool); ok {
nt := *t
nt.Config.UseEqual = ignore
return &nt
}
}
// Enable UseEqual only for types types
t = t.copyWithHooks()
err := t.Config.hooks.AddUseEqual(types)
if err != nil {
t.Helper()
t.Fatal(color.Bad("UseEqual " + err.Error()))
}
return t
}
// BeLax allows to compare different but convertible types. If set to
// false, got and expected types must be the same. If set to true and
// expected type is convertible to got one, expected is first
// converted to go type before its comparison. See [CmpLax] or
// [T.CmpLax] and [Lax] operator to set this flag without providing a
// specific configuration.
//
// It returns a new instance of [*T] so does not alter the original t.
//
// Note that t.BeLax() acts as t.BeLax(true).
//
// See also other configurators [T.Assert], [T.Require], [T.RootName],
// [T.FailureIsFatal], [T.UseEqual], [T.IgnoreUnexported] and
// [T.TestDeepInGotOK].
func (t *T) BeLax(enable ...bool) *T {
nt := *t
nt.Config.BeLax = len(enable) == 0 || enable[0]
return &nt
}
// IgnoreUnexported tells go-testdeep to ignore unexported fields of
// structs whose type is one of types.
//
// It always returns a new instance of [*T] so does not alter the original t.
//
// t = t.IgnoreUnexported(MyStruct1{}, MyStruct2{})
//
// types items can also be [reflect.Type] items. In this case, the
// target type is the one reflected by the [reflect.Type].
//
// t = t.IgnoreUnexported(reflect.TypeOf(MyStruct1{}))
//
// As a special case, calling t.IgnoreUnexported() or
// t.IgnoreUnexported(true) returns an instance ignoring unexported
// fields globally, for all struct types. t.IgnoreUnexported(false)
// returns an instance not ignoring unexported fields anymore, except
// for types already recorded using a previous IgnoreUnexported call.
//
// See also other configurators [T.Assert], [T.Require], [T.RootName],
// [T.FailureIsFatal], [T.UseEqual], [T.BeLax] and
// [T.TestDeepInGotOK].
func (t *T) IgnoreUnexported(types ...any) *T {
// special case: IgnoreUnexported()
if len(types) == 0 {
nt := *t
nt.Config.IgnoreUnexported = true
return &nt
}
// special cases: IgnoreUnexported(true) or IgnoreUnexported(false)
if len(types) == 1 {
if ignore, ok := types[0].(bool); ok {
nt := *t
nt.Config.IgnoreUnexported = ignore
return &nt
}
}
// Enable IgnoreUnexported only for types types
t = t.copyWithHooks()
err := t.Config.hooks.AddIgnoreUnexported(types)
if err != nil {
t.Helper()
t.Fatal(color.Bad("IgnoreUnexported " + err.Error()))
}
return t
}
// TestDeepInGotOK tells go-testdeep not to panic when a [TestDeep]
// operator is found on got side. By default it is forbidden because
// most of the time it is a mistake to compare (expected, got) instead
// of official (got, expected).
//
// It returns a new instance of [*T] so does not alter the original t.
//
// Note that t.TestDeepInGotOK() acts as t.TestDeepInGotOK(true).
//
// See also other configurators [T.Assert], [T.Require], [T.RootName],
// [T.FailureIsFatal], [T.UseEqual], [T.BeLax] and
// [T.IgnoreUnexported].
func (t *T) TestDeepInGotOK(enable ...bool) *T {
nt := *t
nt.Config.TestDeepInGotOK = len(enable) == 0 || enable[0]
return &nt
}
// Cmp is mostly a shortcut for:
//
// Cmp(t.TB, got, expected, args...)
//
// with the exception that t.Config is used to configure the test
// [ContextConfig].
//
// 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) Cmp(got, expected any, args ...any) bool {
t.Helper()
defer t.resetNonPersistentAnchors()
return cmpDeeply(newContext(t), t.TB, got, expected, args...)
}
// CmpDeeply works the same as [Cmp] and is still available for
// compatibility purpose. Use shorter [Cmp] in new code.
func (t *T) CmpDeeply(got, expected any, args ...any) bool {
t.Helper()
defer t.resetNonPersistentAnchors()
return cmpDeeply(newContext(t), t.TB, got, expected, args...)
}
// True is shortcut for:
//
// t.Cmp(got, true, args...)
//
// Returns true if the test is OK, false if it fails.
//
// t.True(IsAvailable(x), "x should be available")
//
// 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.
//
// See also [T.False].
func (t *T) True(got any, args ...any) bool {
t.Helper()
return t.Cmp(got, true, args...)
}
// False is shortcut for:
//
// t.Cmp(got, false, args...)
//
// Returns true if the test is OK, false if it fails.
//
// t.False(IsAvailable(x), "x should not be available")
//
// 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.
//
// See also [T.True].
func (t *T) False(got any, args ...any) bool {
t.Helper()
return t.Cmp(got, false, args...)
}
// CmpError checks that got is non-nil error.
//
// _, err := MyFunction(1, 2, 3)
// t.CmpError(err, "MyFunction(1, 2, 3) should return an error")
//
// CmpError and not Error to avoid collision with t.TB.Error method.
//
// 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.
//
// See also [T.CmpNoError].
func (t *T) CmpError(got error, args ...any) bool {
t.Helper()
return cmpError(newContext(t), t.TB, got, args...)
}
// CmpNoError checks that got is nil error.
//
// value, err := MyFunction(1, 2, 3)
// if t.CmpNoError(err) {
// // one can now check value...
// }
//
// CmpNoError and not NoError to be consistent with [T.CmpError] method.
//
// 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.
//
// See also [T.CmpError].
func (t *T) CmpNoError(got error, args ...any) bool {
t.Helper()
return cmpNoError(newContext(t), t.TB, got, args...)
}
// CmpPanic calls fn and checks a panic() occurred with the
// expectedPanic parameter. It returns true only if both conditions
// are fulfilled.
//
// Note that calling panic(nil) in fn body is always detected as a
// panic. [runtime] package says: before Go 1.21, programs that called
// panic(nil) observed recover returning nil. Starting in Go 1.21,
// programs that call panic(nil) observe recover returning a
// [*runtime.PanicNilError]. Programs can change back to the old
// behavior by setting GODEBUG=panicnil=1.
//
// t.CmpPanic(func() { panic("I am panicking!") },
// "I am panicking!",
// "The function should panic with the right string")
//
// t.CmpPanic(func() { panic("I am panicking!") },
// Contains("panicking!"),
// "The function should panic with a string containing `panicking!`")
//
// expected := &runtime.PanicNilError{} // should be nil if GODEBUG=panicnil=1
// t.CmpPanic(t, func() { panic(nil) }, expected, "Checks for panic(nil)")
//
// 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.
//
// See also [T.CmpNotPanic].
func (t *T) CmpPanic(fn func(), expected any, args ...any) bool {
t.Helper()
defer t.resetNonPersistentAnchors()
return cmpPanic(newContext(t), t, fn, expected, args...)
}
// CmpNotPanic calls fn and checks no panic() occurred. If a panic()
// occurred false is returned then the panic() parameter and the stack
// trace appear in the test report.
//
// Note that calling panic(nil) in fn body is always detected as a
// panic. [runtime] package says: before Go 1.21, programs that called
// panic(nil) observed recover returning nil. Starting in Go 1.21,
// programs that call panic(nil) observe recover returning a
// [*runtime.PanicNilError]. Programs can change back to the old
// behavior by setting GODEBUG=panicnil=1.
//
// t.CmpNotPanic(func() {}) // succeeds as function does not panic
//
// t.CmpNotPanic(func() { panic("I am panicking!") }) // fails
// t.CmpNotPanic(func() { panic(nil) }) // fails too
//
// 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.
//
// See also [T.CmpPanic].
func (t *T) CmpNotPanic(fn func(), args ...any) bool {
t.Helper()
return cmpNotPanic(newContext(t), t, fn, args...)
}
// Parallel marks this test as runnable in parallel with other
// parallel tests. If t.TB implements Parallel(), as [*testing.T]
// does, it is usually used to mark top-level tests and/or subtests as
// safe for parallel execution:
//
// func TestCreateRecord(tt *testing.T) {
// t := td.NewT(tt)
// t.Parallel()
//
// t.Run("no error", func(t *td.T) {
// t.Parallel()
//
// // ...
// })
//
// If t.TB does not implement Parallel(), this method is a no-op.
func (t *T) Parallel() {
p, ok := t.TB.(interface{ Parallel() })
if ok {
p.Parallel()
}
}
type runtFuncs struct {
run reflect.Value
fnt reflect.Type
}
var (
runtMu sync.Mutex
runt = map[reflect.Type]runtFuncs{}
)
func (t *T) getRunFunc() (runtFuncs, bool) {
ttb := reflect.TypeOf(t.TB)
runtMu.Lock()
defer runtMu.Unlock()
vfuncs, ok := runt[ttb]
if !ok {
run, ok := ttb.MethodByName("Run")
if ok {
mt := run.Type
if mt.NumIn() == 3 && mt.NumOut() == 1 && !mt.IsVariadic() &&
mt.In(1) == types.String && mt.Out(0) == types.Bool {
fnt := mt.In(2)
if fnt.Kind() == reflect.Func &&
fnt.NumIn() == 1 && fnt.NumOut() == 0 &&
fnt.In(0) == mt.In(0) {
vfuncs = runtFuncs{
run: run.Func,
fnt: fnt,
}
runt[ttb] = vfuncs
ok = true
}
}
}
if !ok {
runt[ttb] = vfuncs
}
}
return vfuncs, vfuncs != (runtFuncs{})
}
// Run runs f as a subtest of t called name.
//
// If t.TB implement a method with the following signature:
//
// (X) Run(string, func(X)) bool
//
// it calls it with a function of its own in which it creates a new
// instance of [*T] on the fly before calling f with it.
//
// So if t.TB is a [*testing.T] or a [*testing.B] (which is in normal
// cases), let's quote the [testing.T.Run] & [testing.B.Run]
// documentation: f is called in a separate goroutine and blocks
// until f returns or calls t.Parallel to become a parallel
// test. Run reports whether f succeeded (or at least did not fail
// before calling t.Parallel). Run may be called simultaneously from
// multiple goroutines, but all such calls must return before the
// outer test function for t returns.
//
// If this Run() method is not found, it simply logs name then
// executes f using a new [*T] instance in the current goroutine. Note
// that it is only done for convenience.
//
// The t param of f inherits the configuration of the self-reference.
//
// See also [T.RunAssertRequire],
// [github.com/maxatome/go-testdeep/helpers/tdsynctest.Run] and
// [github.com/maxatome/go-testdeep/helpers/tdsynctest.RunAssertRequire].
func (t *T) Run(name string, f func(t *T)) bool {
t.Helper()
vfuncs, ok := t.getRunFunc()
if !ok {
t = NewT(t)
t.Logf("++++ %s", name)
f(t)
return !t.Failed()
}
conf := t.Config
ret := vfuncs.run.Call([]reflect.Value{
reflect.ValueOf(t.TB),
reflect.ValueOf(name),
reflect.MakeFunc(vfuncs.fnt,
func(args []reflect.Value) (results []reflect.Value) {
f(NewT(args[0].Interface().(testing.TB), conf))
return nil
}),
})
return ret[0].Bool()
}
// RunAssertRequire runs f as a subtest of t called name.
//
// If t.TB implement a method with the following signature:
//
// (X) Run(string, func(X)) bool
//
// it calls it with a function of its own in which it creates two new
// instances of [*T] using [AssertRequire] on the fly before calling f
// with them.
//
// So if t.TB is a [*testing.T] or a [*testing.B] (which is in normal
// cases), let's quote the [testing.T.Run] & [testing.B.Run]
// documentation: f is called in a separate goroutine and blocks
// until f returns or calls t.Parallel to become a parallel
// test. Run reports whether f succeeded (or at least did not fail
// before calling t.Parallel). Run may be called simultaneously from
// multiple goroutines, but all such calls must return before the
// outer test function for t returns.
//
// If this Run() method is not found, it simply logs name then
// executes f using two new instances of [*T] (built with
// [AssertRequire]) in the current goroutine. Note that it is only
// done for convenience.
//
// The assert and require params of f inherit the configuration
// of the self-reference, except that a failure is never fatal using
// assert and always fatal using require.
//
// See also [T.Run],
// [github.com/maxatome/go-testdeep/helpers/tdsynctest.RunAssertRequire]
// and [github.com/maxatome/go-testdeep/helpers/tdsynctest.Run].
func (t *T) RunAssertRequire(name string, f func(assert, require *T)) bool {
t.Helper()
vfuncs, ok := t.getRunFunc()
if !ok {
assert, require := AssertRequire(t)
t.Logf("++++ %s", name)
f(assert, require)
return !t.Failed()
}
conf := t.Config
ret := vfuncs.run.Call([]reflect.Value{
reflect.ValueOf(t.TB),
reflect.ValueOf(name),
reflect.MakeFunc(vfuncs.fnt,
func(args []reflect.Value) (results []reflect.Value) {
f(AssertRequire(args[0].Interface().(testing.TB), conf))
return nil
}),
})
return ret[0].Bool()
}
// RunT runs f as a subtest of t called name.
//
// Deprecated: RunT has been superseded by [T.Run] method. It is kept
// for compatibility.
func (t *T) RunT(name string, f func(t *T)) bool {
t.Helper()
return t.Run(name, f)
}
func getTrace(args ...any) string {
var b strings.Builder
tdutil.FbuildTestName(&b, args...)
if b.Len() == 0 {
b.WriteString("Stack trace:\n")
} else if !strings.HasSuffix(b.String(), "\n") {
b.WriteByte('\n')
}
s := stripTrace(trace.Retrieve(1, "testing.tRunner"))
if len(s) == 0 {
b.WriteString("\tEmpty stack trace")
return b.String()
}
s.Dump(&b)
return b.String()
}
// LogTrace uses t.TB.Log() to log a stack trace.
//
// args... are optional and allow to prefix the trace by a
// message. If empty, this message defaults to "Stack trace:\n". If
// this message does not end with a "\n", one is automatically
// added. 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].
//
// See also [T.ErrorTrace] and [T.FatalTrace].
func (t *T) LogTrace(args ...any) {
t.Helper()
t.Log(getTrace(args...))
}
// ErrorTrace uses t.TB.Error() to log a stack trace.
//
// args... are optional and allow to prefix the trace by a
// message. If empty, this message defaults to "Stack trace:\n". If
// this message does not end with a "\n", one is automatically
// added. 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].
//
// See also [T.LogTrace] and [T.FatalTrace].
func (t *T) ErrorTrace(args ...any) {
t.Helper()
t.Error(getTrace(args...))
}
// FatalTrace uses t.TB.Fatal() to log a stack trace.
//
// args... are optional and allow to prefix the trace by a
// message. If empty, this message defaults to "Stack trace:\n". If
// this message does not end with a "\n", one is automatically
// added. 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].
//
// See also [T.LogTrace] and [T.ErrorTrace].
func (t *T) FatalTrace(args ...any) {
t.Helper()
t.Fatal(getTrace(args...))
}
go-testdeep-1.15.0/td/t_struct_examples_121_test.go 0000664 0000000 0000000 00000004246 15144170453 0022155 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2025, 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.21
// +build go1.21
package td_test
import (
"fmt"
"runtime"
"testing"
"github.com/maxatome/go-testdeep/td"
)
func ExampleT_CmpPanic() {
t := td.NewT(&testing.T{})
ok := t.CmpPanic(func() { panic("I am panicking!") }, "I am panicking!",
"Checks for panic")
fmt.Println("checks exact panic() string:", ok)
// Can use TestDeep operator too
ok = t.CmpPanic(
func() { panic("I am panicking!") },
td.Contains("panicking!"),
"Checks for panic")
fmt.Println("checks panic() sub-string:", ok)
// Can detect panic(nil)
// Before Go 1.21, programs that called panic(nil) observed recover
// returning nil. Starting in Go 1.21, programs that call panic(nil)
// observe recover returning a *PanicNilError. Programs can change
// back to the old behavior by setting GODEBUG=panicnil=1.
// See https://pkg.go.dev/runtime#PanicNilError
ok = t.CmpPanic(func() { panic(nil) }, &runtime.PanicNilError{},
"Checks for panic(nil)")
fmt.Println("checks for panic(nil):", ok)
// As well as structured data panic
type PanicStruct struct {
Error string
Code int
}
ok = t.CmpPanic(
func() {
panic(PanicStruct{Error: "Memory violation", Code: 11})
},
PanicStruct{
Error: "Memory violation",
Code: 11,
})
fmt.Println("checks exact panic() struct:", ok)
// or combined with TestDeep operators too
ok = t.CmpPanic(
func() {
panic(PanicStruct{Error: "Memory violation", Code: 11})
},
td.Struct(PanicStruct{}, td.StructFields{
"Code": td.Between(10, 20),
}))
fmt.Println("checks panic() struct against TestDeep operators:", ok)
// Of course, do not panic = test failure, even for expected nil
// panic parameter
ok = t.CmpPanic(func() {}, nil)
fmt.Println("checks a panic occurred:", ok)
// Output:
// checks exact panic() string: true
// checks panic() sub-string: true
// checks for panic(nil): true
// checks exact panic() struct: true
// checks panic() struct against TestDeep operators: true
// checks a panic occurred: false
}
go-testdeep-1.15.0/td/t_struct_examples_test.go 0000664 0000000 0000000 00000003450 15144170453 0021566 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (
"fmt"
"testing"
"github.com/maxatome/go-testdeep/td"
)
func ExampleT_True() {
t := td.NewT(&testing.T{})
got := true
ok := t.True(got, "check that got is true!")
fmt.Println(ok)
got = false
ok = t.True(got, "check that got is true!")
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleT_False() {
t := td.NewT(&testing.T{})
got := false
ok := t.False(got, "check that got is false!")
fmt.Println(ok)
got = true
ok = t.False(got, "check that got is false!")
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleT_CmpError() {
t := td.NewT(&testing.T{})
got := fmt.Errorf("Error #%d", 42)
ok := t.CmpError(got, "An error occurred")
fmt.Println(ok)
got = nil
ok = t.CmpError(got, "An error occurred") // fails
fmt.Println(ok)
// Output:
// true
// false
}
func ExampleT_CmpNoError() {
t := td.NewT(&testing.T{})
got := fmt.Errorf("Error #%d", 42)
ok := t.CmpNoError(got, "An error occurred") // fails
fmt.Println(ok)
got = nil
ok = t.CmpNoError(got, "An error occurred")
fmt.Println(ok)
// Output:
// false
// true
}
func ExampleT_CmpNotPanic() {
t := td.NewT(&testing.T{})
ok := t.CmpNotPanic(func() {}, nil)
fmt.Println("checks a panic DID NOT occur:", ok)
// Classic panic
ok = t.CmpNotPanic(func() { panic("I am panicking!") },
"Hope it does not panic!")
fmt.Println("still no panic?", ok)
// Can detect panic(nil)
ok = t.CmpNotPanic(func() { panic(nil) }, "Checks for panic(nil)")
fmt.Println("last no panic?", ok)
// Output:
// checks a panic DID NOT occur: true
// still no panic? false
// last no panic? false
}
go-testdeep-1.15.0/td/t_struct_test.go 0000664 0000000 0000000 00000045210 15144170453 0017670 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2021, 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 (
"regexp"
"strings"
"sync"
"testing"
"time"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/internal/trace"
"github.com/maxatome/go-testdeep/td"
)
func TestT(tt *testing.T) {
// We don't want to include "anchors" field in comparison
cmp := func(tt *testing.T, got, expected td.ContextConfig) {
tt.Helper()
td.Cmp(tt, got,
td.SStruct(expected, td.StructFields{
"anchors": td.Ignore(),
"hooks": td.Ignore(),
}),
)
}
tt.Run("without config", func(tt *testing.T) {
t := td.NewT(tt)
cmp(tt, t.Config, td.DefaultContextConfig)
tDup := td.NewT(t)
cmp(tt, tDup.Config, td.DefaultContextConfig)
})
tt.Run("explicit default config", func(tt *testing.T) {
t := td.NewT(tt, td.ContextConfig{})
cmp(tt, t.Config, td.DefaultContextConfig)
tDup := td.NewT(t)
cmp(tt, tDup.Config, td.DefaultContextConfig)
})
tt.Run("specific config", func(tt *testing.T) {
conf := td.ContextConfig{
RootName: "TEST",
MaxErrors: 33,
}
t := td.NewT(tt, conf)
cmp(tt, t.Config, conf)
tDup := td.NewT(t)
cmp(tt, tDup.Config, conf)
newConf := conf
newConf.MaxErrors = 34
tDup = td.NewT(t, newConf)
cmp(tt, tDup.Config, newConf)
t2 := t.RootName("T2")
cmp(tt, t.Config, conf)
cmp(tt, t2.Config, td.ContextConfig{
RootName: "T2",
MaxErrors: 33,
})
t3 := t.RootName("")
cmp(tt, t3.Config, td.ContextConfig{
RootName: "DATA",
MaxErrors: 33,
})
})
//
// Bad usages
ttb := test.NewTestingTB("usage params")
ttb.CatchFatal(func() {
td.NewT(ttb, td.ContextConfig{}, td.ContextConfig{})
})
test.IsTrue(tt, ttb.IsFatal)
test.IsTrue(tt, strings.Contains(ttb.Messages[0], "usage: NewT("))
test.CheckPanic(tt, func() { td.NewT(nil) }, "usage: NewT")
}
func TestTCmp(tt *testing.T) {
ttt := test.NewTestingTB(tt.Name())
t := td.NewT(ttt)
test.IsTrue(tt, t.Cmp(1, 1))
test.IsFalse(tt, ttt.Failed())
ttt = test.NewTestingTB(tt.Name())
t = td.NewT(ttt)
test.IsFalse(tt, t.Cmp(1, 2))
test.IsTrue(tt, ttt.Failed())
}
func TestTCmpDeeply(tt *testing.T) {
ttt := test.NewTestingTB(tt.Name())
t := td.NewT(ttt)
test.IsTrue(tt, t.CmpDeeply(1, 1))
test.IsFalse(tt, ttt.Failed())
ttt = test.NewTestingTB(tt.Name())
t = td.NewT(ttt)
test.IsFalse(tt, t.CmpDeeply(1, 2))
test.IsTrue(tt, ttt.Failed())
}
func TestParallel(t *testing.T) {
t.Run("without Parallel", func(tt *testing.T) {
ttt := test.NewTestingTB(tt.Name())
t := td.NewT(ttt)
t.Parallel()
// has no effect
})
t.Run("with Parallel", func(tt *testing.T) {
ttt := test.NewParallelTestingTB(tt.Name())
t := td.NewT(ttt)
t.Parallel()
test.IsTrue(tt, ttt.IsParallel)
})
t.Run("Run with Parallel", func(tt *testing.T) {
// This test verifies that subtests with t.Parallel() are run
// in parallel. We use a WaitGroup to make both subtests block
// until they're both ready. This test will block forever if
// the tests are not run together.
var ready sync.WaitGroup
ready.Add(2)
t := td.NewT(tt)
t.Run("level 1", func(t *td.T) {
t.Parallel()
ready.Done() // I'm ready.
ready.Wait() // Are you?
})
t.Run("level 2", func(t *td.T) {
t.Parallel()
ready.Done() // I'm ready.
ready.Wait() // Are you?
})
})
}
func TestRun(t *testing.T) {
t.Run("test.TB with Run", func(tt *testing.T) {
t := td.NewT(tt)
runPassed := false
nestedFailureIsFatal := false
ok := t.Run("Test level1",
func(t *td.T) {
ok := t.FailureIsFatal().Run("Test level2",
func(t *td.T) {
runPassed = t.True(true) // test succeeds!
// Check we inherit config from caller
nestedFailureIsFatal = t.Config.FailureIsFatal
})
t.True(ok)
})
test.IsTrue(tt, ok)
test.IsTrue(tt, runPassed)
test.IsTrue(tt, nestedFailureIsFatal)
})
t.Run("test.TB without Run", func(tt *testing.T) {
t := td.NewT(test.NewTestingTB("gg"))
runPassed := false
ok := t.Run("Test level1",
func(t *td.T) {
ok := t.Run("Test level2",
func(t *td.T) {
runPassed = t.True(true) // test succeeds!
})
t.True(ok)
})
t.True(ok)
t.True(runPassed)
})
}
func TestRunAssertRequire(t *testing.T) {
t.Run("test.TB with Run", func(tt *testing.T) {
t := td.NewT(tt)
runPassed := false
assertIsFatal := true
requireIsFatal := false
ok := t.RunAssertRequire("Test level1",
func(assert, require *td.T) {
assertIsFatal = assert.Config.FailureIsFatal
requireIsFatal = require.Config.FailureIsFatal
ok := assert.RunAssertRequire("Test level2",
func(assert, require *td.T) {
runPassed = assert.True(true) // test succeeds!
runPassed = runPassed && require.True(true) // test succeeds!
assertIsFatal = assertIsFatal || assert.Config.FailureIsFatal
requireIsFatal = requireIsFatal && require.Config.FailureIsFatal
})
assert.True(ok)
require.True(ok)
ok = require.RunAssertRequire("Test level2",
func(assert, require *td.T) {
runPassed = runPassed && assert.True(true) // test succeeds!
runPassed = runPassed && require.True(true) // test succeeds!
assertIsFatal = assertIsFatal || assert.Config.FailureIsFatal
requireIsFatal = requireIsFatal && require.Config.FailureIsFatal
})
assert.True(ok)
require.True(ok)
})
test.IsTrue(tt, ok)
test.IsTrue(tt, runPassed)
test.IsFalse(tt, assertIsFatal)
test.IsTrue(tt, requireIsFatal)
})
t.Run("test.TB without Run", func(tt *testing.T) {
t := td.NewT(test.NewTestingTB("gg"))
runPassed := false
assertIsFatal := true
requireIsFatal := false
ok := t.RunAssertRequire("Test level1",
func(assert, require *td.T) {
assertIsFatal = assert.Config.FailureIsFatal
requireIsFatal = require.Config.FailureIsFatal
ok := assert.RunAssertRequire("Test level2",
func(assert, require *td.T) {
runPassed = assert.True(true) // test succeeds!
runPassed = runPassed && require.True(true) // test succeeds!
assertIsFatal = assertIsFatal || assert.Config.FailureIsFatal
requireIsFatal = requireIsFatal && require.Config.FailureIsFatal
})
assert.True(ok)
require.True(ok)
ok = require.RunAssertRequire("Test level2",
func(assert, require *td.T) {
runPassed = runPassed && assert.True(true) // test succeeds!
runPassed = runPassed && require.True(true) // test succeeds!
assertIsFatal = assertIsFatal || assert.Config.FailureIsFatal
requireIsFatal = requireIsFatal && require.Config.FailureIsFatal
})
assert.True(ok)
require.True(ok)
})
test.IsTrue(tt, ok)
test.IsTrue(tt, runPassed)
test.IsFalse(tt, assertIsFatal)
test.IsTrue(tt, requireIsFatal)
})
}
// Deprecated RunT.
func TestRunT(t *testing.T) {
t.Run("test.TB with Run", func(tt *testing.T) {
t := td.NewT(tt)
runPassed := false
ok := t.RunT("Test level1", //nolint: staticcheck
func(t *td.T) {
ok := t.RunT("Test level2", //nolint: staticcheck
func(t *td.T) {
runPassed = t.True(true) // test succeeds!
})
t.True(ok)
})
test.IsTrue(tt, ok)
test.IsTrue(tt, runPassed)
})
t.Run("test.TB without Run", func(tt *testing.T) {
t := td.NewT(test.NewTestingTB("gg"))
runPassed := false
ok := t.RunT("Test level1", //nolint: staticcheck
func(t *td.T) {
ok := t.RunT("Test level2", //nolint: staticcheck
func(t *td.T) {
runPassed = t.True(true) // test succeeds!
})
t.True(ok)
})
test.IsTrue(tt, ok)
test.IsTrue(tt, runPassed)
})
}
func TestFailureIsFatal(tt *testing.T) {
// All t.True(false) tests of course fail
// Using default config
ttt := test.NewTestingTB(tt.Name())
t := td.NewT(ttt)
t.True(false) // failure
test.IsTrue(tt, ttt.LastMessage() != "")
test.IsFalse(tt, ttt.IsFatal, "by default it is not fatal")
// Using specific config
ttt = test.NewTestingTB(tt.Name())
t = td.NewT(ttt, td.ContextConfig{FailureIsFatal: true})
ttt.CatchFatal(func() { t.True(false) }) // failure
test.IsTrue(tt, ttt.LastMessage() != "")
test.IsTrue(tt, ttt.IsFatal, "it must be fatal")
// Using FailureIsFatal()
ttt = test.NewTestingTB(tt.Name())
t = td.NewT(ttt).FailureIsFatal()
ttt.CatchFatal(func() { t.True(false) }) // failure
test.IsTrue(tt, ttt.LastMessage() != "")
test.IsTrue(tt, ttt.IsFatal, "it must be fatal")
// Using FailureIsFatal(true)
ttt = test.NewTestingTB(tt.Name())
t = td.NewT(ttt).FailureIsFatal(true)
ttt.CatchFatal(func() { t.True(false) }) // failure
test.IsTrue(tt, ttt.LastMessage() != "")
test.IsTrue(tt, ttt.IsFatal, "it must be fatal")
// Using T.Assert()
ttt = test.NewTestingTB(tt.Name())
t = td.NewT(ttt, td.ContextConfig{FailureIsFatal: true}).Assert()
t.True(false) // failure
test.IsTrue(tt, ttt.LastMessage() != "")
test.IsFalse(tt, ttt.IsFatal, "by default it is not fatal")
// Using T.Require()
ttt = test.NewTestingTB(tt.Name())
t = td.NewT(ttt).Require()
ttt.CatchFatal(func() { t.True(false) }) // failure
test.IsTrue(tt, ttt.LastMessage() != "")
test.IsTrue(tt, ttt.IsFatal, "it must be fatal")
// Using Require()
ttt = test.NewTestingTB(tt.Name())
t = td.Require(ttt)
ttt.CatchFatal(func() { t.True(false) }) // failure
test.IsTrue(tt, ttt.LastMessage() != "")
test.IsTrue(tt, ttt.IsFatal, "it must be fatal")
// Using Require() with specific config (cannot override FailureIsFatal)
ttt = test.NewTestingTB(tt.Name())
t = td.Require(ttt, td.ContextConfig{FailureIsFatal: false})
ttt.CatchFatal(func() { t.True(false) }) // failure
test.IsTrue(tt, ttt.LastMessage() != "")
test.IsTrue(tt, ttt.IsFatal, "it must be fatal")
// Canceling specific config
ttt = test.NewTestingTB(tt.Name())
t = td.NewT(ttt, td.ContextConfig{FailureIsFatal: false}).
FailureIsFatal(false)
t.True(false) // failure
test.IsTrue(tt, ttt.LastMessage() != "")
test.IsFalse(tt, ttt.IsFatal, "it must be not fatal")
// Using Assert()
ttt = test.NewTestingTB(tt.Name())
t = td.Assert(ttt)
t.True(false) // failure
test.IsTrue(tt, ttt.LastMessage() != "")
test.IsFalse(tt, ttt.IsFatal, "it must be not fatal")
// Using Assert() with specific config (cannot override FailureIsFatal)
ttt = test.NewTestingTB(tt.Name())
t = td.Assert(ttt, td.ContextConfig{FailureIsFatal: true})
t.True(false) // failure
test.IsTrue(tt, ttt.LastMessage() != "")
test.IsFalse(tt, ttt.IsFatal, "it must be not fatal")
// AssertRequire() / assert
ttt = test.NewTestingTB(tt.Name())
t, _ = td.AssertRequire(ttt)
t.True(false) // failure
test.IsTrue(tt, ttt.LastMessage() != "")
test.IsFalse(tt, ttt.IsFatal, "it must be not fatal")
// Using AssertRequire() / assert with specific config (cannot
// override FailureIsFatal)
ttt = test.NewTestingTB(tt.Name())
t, _ = td.AssertRequire(ttt, td.ContextConfig{FailureIsFatal: true})
t.True(false) // failure
test.IsTrue(tt, ttt.LastMessage() != "")
test.IsFalse(tt, ttt.IsFatal, "it must be not fatal")
// AssertRequire() / require
ttt = test.NewTestingTB(tt.Name())
_, t = td.AssertRequire(ttt)
ttt.CatchFatal(func() { t.True(false) }) // failure
test.IsTrue(tt, ttt.LastMessage() != "")
test.IsTrue(tt, ttt.IsFatal, "it must be fatal")
// Using AssertRequire() / require with specific config (cannot
// override FailureIsFatal)
ttt = test.NewTestingTB(tt.Name())
_, t = td.AssertRequire(ttt, td.ContextConfig{FailureIsFatal: true})
ttt.CatchFatal(func() { t.True(false) }) // failure
test.IsTrue(tt, ttt.LastMessage() != "")
test.IsTrue(tt, ttt.IsFatal, "it must be fatal")
}
func TestUseEqual(tt *testing.T) {
ttt := test.NewTestingTB(tt.Name())
var time1, time2 time.Time
for {
time1 = time.Now()
time2 = time1.Truncate(0)
if !time1.Equal(time2) {
tt.Fatal("time.Equal() does not work as expected")
}
if time1 != time2 { // to avoid the bad luck case where time1.wall=0
break
}
}
// Using default config
t := td.NewT(ttt)
test.IsFalse(tt, t.Cmp(time1, time2))
// UseEqual
t = td.NewT(ttt).UseEqual() // enable globally
test.IsTrue(tt, t.Cmp(time1, time2))
t = td.NewT(ttt).UseEqual(true) // enable globally
test.IsTrue(tt, t.Cmp(time1, time2))
t = td.NewT(ttt).UseEqual(false) // disable globally
test.IsFalse(tt, t.Cmp(time1, time2))
t = td.NewT(ttt).UseEqual(time.Time{}) // enable only for time.Time
test.IsTrue(tt, t.Cmp(time1, time2))
t = t.UseEqual().UseEqual(false) // enable then disable globally
test.IsTrue(tt, t.Cmp(time1, time2)) // Equal() still used
test.EqualStr(tt,
ttt.CatchFatal(func() { td.NewT(ttt).UseEqual(42) }),
"UseEqual expects type int owns an Equal method (@0)")
}
func TestBeLax(tt *testing.T) {
ttt := test.NewTestingTB(tt.Name())
// Using default config
t := td.NewT(ttt)
test.IsFalse(tt, t.Cmp(int64(123), 123))
// BeLax
t = td.NewT(ttt).BeLax()
test.IsTrue(tt, t.Cmp(int64(123), 123))
t = td.NewT(ttt).BeLax(true)
test.IsTrue(tt, t.Cmp(int64(123), 123))
t = td.NewT(ttt).BeLax(false)
test.IsFalse(tt, t.Cmp(int64(123), 123))
}
func TestIgnoreUnexported(tt *testing.T) {
ttt := test.NewTestingTB(tt.Name())
type SType1 struct {
Public int
private string
}
a1, b1 := SType1{Public: 42, private: "test"}, SType1{Public: 42}
type SType2 struct {
Public int
private string
}
a2, b2 := SType2{Public: 42, private: "test"}, SType2{Public: 42}
// Using default config
t := td.NewT(ttt)
test.IsFalse(tt, t.Cmp(a1, b1))
// IgnoreUnexported
t = td.NewT(ttt).IgnoreUnexported() // ignore unexported globally
test.IsTrue(tt, t.Cmp(a1, b1))
test.IsTrue(tt, t.Cmp(a2, b2))
t = td.NewT(ttt).IgnoreUnexported(true) // ignore unexported globally
test.IsTrue(tt, t.Cmp(a1, b1))
test.IsTrue(tt, t.Cmp(a2, b2))
t = td.NewT(ttt).IgnoreUnexported(false) // handle unexported globally
test.IsFalse(tt, t.Cmp(a1, b1))
test.IsFalse(tt, t.Cmp(a2, b2))
t = td.NewT(ttt).IgnoreUnexported(SType1{}) // ignore only for SType1
test.IsTrue(tt, t.Cmp(a1, b1))
test.IsFalse(tt, t.Cmp(a2, b2))
t = t.UseEqual().UseEqual(false) // enable then disable globally
test.IsTrue(tt, t.Cmp(a1, b1))
test.IsFalse(tt, t.Cmp(a2, b2))
t = td.NewT(ttt).IgnoreUnexported(SType1{}, SType2{}) // enable for both
test.IsTrue(tt, t.Cmp(a1, b1))
test.IsTrue(tt, t.Cmp(a2, b2))
test.EqualStr(tt,
ttt.CatchFatal(func() { td.NewT(ttt).IgnoreUnexported(42) }),
"IgnoreUnexported expects type int be a struct, not a int (@0)")
}
func TestTestDeepInGotOK(tt *testing.T) {
ttt := test.NewTestingTB(tt.Name())
var t *td.T
cmp := func() bool { return t.Cmp(td.Ignore(), td.Ignore()) }
// Using default config
t = td.NewT(ttt)
test.CheckPanic(tt, func() { cmp() },
"Found a TestDeep operator in got param, can only use it in expected one!")
t = td.NewT(ttt).TestDeepInGotOK()
test.IsTrue(tt, cmp())
t = t.TestDeepInGotOK(false)
test.CheckPanic(tt, func() { cmp() },
"Found a TestDeep operator in got param, can only use it in expected one!")
t = t.TestDeepInGotOK(true)
test.IsTrue(tt, cmp())
}
func TestLogTrace(tt *testing.T) {
ttt := test.NewTestingTB(tt.Name())
t := td.NewT(ttt)
//line /t_struct_test.go:100
t.LogTrace()
test.EqualStr(tt, ttt.LastMessage(), `Stack trace:
TestLogTrace() /t_struct_test.go:100`)
test.IsFalse(tt, ttt.HasFailed)
test.IsFalse(tt, ttt.IsFatal)
ttt.ResetMessages()
//line /t_struct_test.go:110
t.LogTrace("This is the %s:", "stack")
test.EqualStr(tt, ttt.LastMessage(), `This is the stack:
TestLogTrace() /t_struct_test.go:110`)
ttt.ResetMessages()
//line /t_struct_test.go:120
t.LogTrace("This is the %s:\n", "stack")
test.EqualStr(tt, ttt.LastMessage(), `This is the stack:
TestLogTrace() /t_struct_test.go:120`)
ttt.ResetMessages()
//line /t_struct_test.go:130
t.LogTrace("This is the ", "stack")
test.EqualStr(tt, ttt.LastMessage(), `This is the stack
TestLogTrace() /t_struct_test.go:130`)
ttt.ResetMessages()
trace.IgnorePackage()
defer trace.UnignorePackage()
//line /t_struct_test.go:140
t.LogTrace("Stack:\n")
test.EqualStr(tt, ttt.LastMessage(), `Stack:
Empty stack trace`)
}
func TestErrorTrace(tt *testing.T) {
ttt := test.NewTestingTB(tt.Name())
t := td.NewT(ttt)
//line /t_struct_test.go:200
t.ErrorTrace()
test.EqualStr(tt, ttt.LastMessage(), `Stack trace:
TestErrorTrace() /t_struct_test.go:200`)
test.IsTrue(tt, ttt.HasFailed)
test.IsFalse(tt, ttt.IsFatal)
ttt.ResetMessages()
//line /t_struct_test.go:210
t.ErrorTrace("This is the %s:", "stack")
test.EqualStr(tt, ttt.LastMessage(), `This is the stack:
TestErrorTrace() /t_struct_test.go:210`)
ttt.ResetMessages()
//line /t_struct_test.go:220
t.ErrorTrace("This is the %s:\n", "stack")
test.EqualStr(tt, ttt.LastMessage(), `This is the stack:
TestErrorTrace() /t_struct_test.go:220`)
ttt.ResetMessages()
//line /t_struct_test.go:230
t.ErrorTrace("This is the ", "stack")
test.EqualStr(tt, ttt.LastMessage(), `This is the stack
TestErrorTrace() /t_struct_test.go:230`)
ttt.ResetMessages()
trace.IgnorePackage()
defer trace.UnignorePackage()
//line /t_struct_test.go:240
t.ErrorTrace("Stack:\n")
test.EqualStr(tt, ttt.LastMessage(), `Stack:
Empty stack trace`)
}
func TestFatalTrace(tt *testing.T) {
ttt := test.NewTestingTB(tt.Name())
t := td.NewT(ttt)
match := func(got, expectedRe string) {
tt.Helper()
re := regexp.MustCompile(expectedRe)
if !re.MatchString(got) {
test.EqualErrorMessage(tt, got, expectedRe)
}
}
//line /t_struct_test.go:300
match(ttt.CatchFatal(func() { t.FatalTrace() }), `Stack trace:
TestFatalTrace\.func\d\(\) /t_struct_test\.go:300
\(\*TestingT\)\.CatchFatal\(\) internal/test/types\.go:\d+
TestFatalTrace\(\) /t_struct_test\.go:300`)
test.IsTrue(tt, ttt.HasFailed)
test.IsTrue(tt, ttt.IsFatal)
ttt.ResetMessages()
//line /t_struct_test.go:310
match(ttt.CatchFatal(func() { t.FatalTrace("This is the %s:", "stack") }),
`This is the stack:
TestFatalTrace\.func\d\(\) /t_struct_test\.go:310
\(\*TestingT\)\.CatchFatal\(\) internal/test/types\.go:\d+
TestFatalTrace\(\) /t_struct_test\.go:310`)
ttt.ResetMessages()
//line /t_struct_test.go:320
match(ttt.CatchFatal(func() { t.FatalTrace("This is the %s:\n", "stack") }),
`This is the stack:
TestFatalTrace\.func\d\(\) /t_struct_test\.go:320
\(\*TestingT\)\.CatchFatal\(\) internal/test/types\.go:\d+
TestFatalTrace\(\) /t_struct_test\.go:320`)
ttt.ResetMessages()
//line /t_struct_test.go:330
match(ttt.CatchFatal(func() { t.FatalTrace("This is the ", "stack") }),
`This is the stack
TestFatalTrace\.func\d\(\) /t_struct_test\.go:330
\(\*TestingT\)\.CatchFatal\(\) internal/test/types\.go:\d+
TestFatalTrace\(\) /t_struct_test\.go:330`)
ttt.ResetMessages()
trace.IgnorePackage()
defer trace.UnignorePackage()
//line /t_struct_test.go:340
test.EqualStr(tt, ttt.CatchFatal(func() { t.FatalTrace("Stack:\n") }),
`Stack:
Empty stack trace`)
}
go-testdeep-1.15.0/td/td_all.go 0000664 0000000 0000000 00000005142 15144170453 0016221 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2025, 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"
"github.com/maxatome/go-testdeep/internal/ctxerr"
)
type tdAll struct {
tdListBase
}
var _ TestDeep = &tdAll{}
// summary(All): all expected values have to match
// input(All): all
// All operator compares data against several expected values. During
// a match, all of them have to match to succeed. Consider it
// as a "AND" logical operator.
//
// td.Cmp(t, "foobar", td.All(
// td.Len(6),
// td.HasPrefix("fo"),
// td.HasSuffix("ar"),
// )) // succeeds
//
// Note [Flatten] function can be used to group or reuse some values or
// operators and so avoid boring and inefficient copies:
//
// stringOps := td.Flatten([]td.TestDeep{td.HasPrefix("fo"), td.HasSuffix("ar")})
// td.Cmp(t, "foobar", td.All(
// td.Len(6),
// stringOps,
// )) // succeeds
//
// One can do the same with All operator itself:
//
// stringOps := td.All(td.HasPrefix("fo"), td.HasSuffix("ar"))
// td.Cmp(t, "foobar", td.All(
// td.Len(6),
// stringOps,
// )) // succeeds
//
// but if an error occurs in the nested All, the report is a bit more
// complex to read due to the nested level. [Flatten] does not create
// a new level, its slice is just flattened in the All parameters.
//
// TypeBehind method can return a non-nil [reflect.Type] if all items
// known non-interface types are equal, or if only interface types
// are found (mostly issued from [Isa]) and they are equal.
//
// See also [Any] and [None].
func All(expectedValues ...any) TestDeep {
return &tdAll{
tdListBase: newListBase(expectedValues...),
}
}
func (a *tdAll) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
var origErr *ctxerr.Error
for idx, item := range a.items {
// Use deepValueEqualFinal here instead of deepValueEqual as we
// want to know whether an error occurred or not, we do not want
// to accumulate it silently
origErr = deepValueEqualFinal(
ctx.ResetErrors().
AddCustomLevel(fmt.Sprintf("", idx+1, len(a.items))),
got, item)
if origErr != nil {
if ctx.BooleanError {
return ctxerr.BooleanError
}
err := &ctxerr.Error{
Message: fmt.Sprintf("compared (part %d of %d)", idx+1, len(a.items)),
Got: got,
Expected: item,
}
if item.IsValid() && item.Type().Implements(testDeeper) {
err.Origin = origErr
}
return ctx.CollectError(err)
}
}
return nil
}
func (a *tdAll) TypeBehind() reflect.Type {
return uniqTypeBehindSlice(a.items)
}
go-testdeep-1.15.0/td/td_all_test.go 0000664 0000000 0000000 00000004753 15144170453 0017267 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (
"fmt"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestAll(t *testing.T) {
checkOK(t, 6, td.All(6, 6, 6))
checkOK(t, nil, td.All(nil, nil, nil))
checkError(t, 6, td.All(6, 5, 6),
expectedError{
Message: mustBe("compared (part 2 of 3)"),
Path: mustBe("DATA"),
Got: mustBe("6"),
Expected: mustBe("5"),
})
checkError(t, 6, td.All(6, nil, 6),
expectedError{
Message: mustBe("compared (part 2 of 3)"),
Path: mustBe("DATA"),
Got: mustBe("6"),
Expected: mustBe("nil"),
})
checkError(t, nil, td.All(nil, 5, nil),
expectedError{
Message: mustBe("compared (part 2 of 3)"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("5"),
})
checkError(t,
6,
td.All(
6,
td.All(td.Between(3, 8), td.Between(4, 5)),
6),
expectedError{
Message: mustBe("compared (part 2 of 3)"),
Path: mustBe("DATA"),
Got: mustBe("6"),
Expected: mustBe("All(3 ≤ got ≤ 8,\n 4 ≤ got ≤ 5)"),
Origin: &expectedError{
Message: mustBe("compared (part 2 of 2)"),
Path: mustBe("DATA"),
Got: mustBe("6"),
Expected: mustBe("4 ≤ got ≤ 5"),
Origin: &expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("6"),
Expected: mustBe("4 ≤ got ≤ 5"),
},
},
})
//
// String
test.EqualStr(t, td.All(6).String(), "All(6)")
test.EqualStr(t, td.All(6, 7).String(), "All(6,\n 7)")
}
func TestAllTypeBehind(t *testing.T) {
equalTypes(t, td.All(6, nil), nil)
equalTypes(t, td.All(6, "toto"), nil)
equalTypes(t, td.All(6, td.Zero(), 7, 8), 26)
// Always the same non-interface type (even if we encounter several
// interface types)
equalTypes(t,
td.All(
td.Empty(),
5,
td.Isa((*error)(nil)), // interface type (in fact pointer to ...)
td.All(6, 7),
td.Isa((*fmt.Stringer)(nil)), // interface type
8),
42)
// Only one interface type
equalTypes(t,
td.All(
td.Isa((*error)(nil)),
td.Isa((*error)(nil)),
td.Isa((*error)(nil)),
),
(*error)(nil))
// Several interface types, cannot be sure
equalTypes(t,
td.All(
td.Isa((*error)(nil)),
td.Isa((*fmt.Stringer)(nil)),
),
nil)
}
go-testdeep-1.15.0/td/td_any.go 0000664 0000000 0000000 00000003556 15144170453 0016247 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2025, 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"
)
type tdAny struct {
tdListBase
}
var _ TestDeep = &tdAny{}
// summary(Any): at least one expected value have to match
// input(Any): all
// Any operator compares data against several expected values. During
// a match, at least one of them has to match to succeed. Consider it
// as a "OR" logical operator.
//
// td.Cmp(t, "foo", td.Any("bar", "foo", "zip")) // succeeds
// td.Cmp(t, "foo", td.Any(
// td.Len(4),
// td.HasPrefix("f"),
// td.HasSuffix("z"),
// )) // succeeds coz "f" prefix
//
// Note [Flatten] function can be used to group or reuse some values or
// operators and so avoid boring and inefficient copies:
//
// stringOps := td.Flatten([]td.TestDeep{td.HasPrefix("f"), td.HasSuffix("z")})
// td.Cmp(t, "foobar", td.All(
// td.Len(4),
// stringOps,
// )) // succeeds coz "f" prefix
//
// TypeBehind method can return a non-nil [reflect.Type] if all items
// known non-interface types are equal, or if only interface types
// are found (mostly issued from Isa()) and they are equal.
//
// See also [All] and [None].
func Any(expectedValues ...any) TestDeep {
return &tdAny{
tdListBase: newListBase(expectedValues...),
}
}
func (a *tdAny) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
for _, item := range a.items {
ok, err := deepValueEqualFinalOK(ctx, got, item)
if err != nil || ok {
return err
}
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "comparing with Any",
Got: got,
Expected: a,
})
}
func (a *tdAny) TypeBehind() reflect.Type {
return uniqTypeBehindSlice(a.items)
}
go-testdeep-1.15.0/td/td_any_test.go 0000664 0000000 0000000 00000004434 15144170453 0017302 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (
"fmt"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestAny(t *testing.T) {
checkOK(t, 6, td.Any(nil, 5, 6, 7))
checkOK(t, nil, td.Any(5, 6, 7, nil))
checkError(t, 6, td.Any(5),
expectedError{
Message: mustBe("comparing with Any"),
Path: mustBe("DATA"),
Got: mustBe("6"),
Expected: mustBe("Any(5)"),
})
checkError(t, 6, td.Any(nil),
expectedError{
Message: mustBe("comparing with Any"),
Path: mustBe("DATA"),
Got: mustBe("6"),
Expected: mustBe("Any(nil)"),
})
checkError(t, nil, td.Any(6),
expectedError{
Message: mustBe("comparing with Any"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("Any(6)"),
})
checkError(t, 123, td.Any(td.JSON("{")),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustContain("JSON unmarshal error"),
Under: mustContain("under operator JSON at "),
},
"erroneous operator expected")
// Lax
checkOK(t, float64(123), td.Lax(td.Any(122, 123, 124)))
//
// String
test.EqualStr(t, td.Any(6).String(), "Any(6)")
test.EqualStr(t, td.Any(6, 7).String(), "Any(6,\n 7)")
}
func TestAnyTypeBehind(t *testing.T) {
equalTypes(t, td.Any(6, nil), nil)
equalTypes(t, td.Any(6, "toto"), nil)
equalTypes(t, td.Any(6, td.Zero(), 7, 8), 26)
// Always the same non-interface type (even if we encounter several
// interface types)
equalTypes(t,
td.Any(
td.Empty(),
5,
td.Isa((*error)(nil)), // interface type (in fact pointer to ...)
td.Any(6, 7),
td.Isa((*fmt.Stringer)(nil)), // interface type
8),
42)
// Only one interface type
equalTypes(t,
td.Any(
td.Isa((*error)(nil)),
td.Isa((*error)(nil)),
td.Isa((*error)(nil)),
),
(*error)(nil))
// Several interface types, cannot be sure
equalTypes(t,
td.Any(
td.Isa((*error)(nil)),
td.Isa((*fmt.Stringer)(nil)),
),
nil)
equalTypes(t,
td.Any(
td.Code(func(x any) bool { return true }),
td.Code(func(y int) bool { return true }),
),
12)
}
go-testdeep-1.15.0/td/td_array.go 0000664 0000000 0000000 00000031006 15144170453 0016565 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2025, 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 (
"bytes"
"fmt"
"reflect"
"sort"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/types"
"github.com/maxatome/go-testdeep/internal/util"
)
type tdArray struct {
tdExpectedType
expectedEntries []reflect.Value
onlyIndexes []int // only used by SuperSliceOf, nil otherwise
}
var _ TestDeep = &tdArray{}
// ArrayEntries allows to pass array or slice entries to check in
// functions [Array], [Slice] and [SuperSliceOf]. It is a map whose
// each key is the item index and the corresponding value the expected
// item value (which can be a [TestDeep] operator as well as a zero
// value).
type ArrayEntries map[int]any
func mergeArrayEntries(aes ...ArrayEntries) ArrayEntries {
switch len(aes) {
case 0:
return nil
case 1:
return aes[0]
}
ret := make(ArrayEntries, len(aes[0]))
for _, me := range aes {
for k, v := range me {
ret[k] = v
}
}
return ret
}
const (
arrayArray uint = iota
arraySlice
arraySuper
)
func newArray(kind uint, model any, aes ...ArrayEntries) *tdArray {
vmodel := reflect.ValueOf(model)
a := tdArray{
tdExpectedType: tdExpectedType{
base: newBase(4),
},
}
expectedEntries := mergeArrayEntries(aes...)
if kind == arraySuper {
a.onlyIndexes = make([]int, 0, len(expectedEntries))
}
kindIsOK := func(k reflect.Kind) bool {
switch kind {
case arrayArray:
return k == reflect.Array
case arraySlice:
return k == reflect.Slice
default: // arraySuper
return k == reflect.Slice || k == reflect.Array
}
}
switch vk := vmodel.Kind(); {
case vk == reflect.Ptr:
if !kindIsOK(vmodel.Type().Elem().Kind()) {
break
}
a.isPtr = true
if vmodel.IsNil() {
a.expectedType = vmodel.Type().Elem()
a.populateExpectedEntries(expectedEntries, reflect.Value{})
return &a
}
vmodel = vmodel.Elem()
fallthrough
case kindIsOK(vk):
a.expectedType = vmodel.Type()
a.populateExpectedEntries(expectedEntries, vmodel)
return &a
}
switch kind {
case arrayArray:
a.err = ctxerr.OpBadUsage("Array",
"(ARRAY|&ARRAY, EXPECTED_ENTRIES)", model, 1, true)
case arraySlice:
a.err = ctxerr.OpBadUsage("Slice",
"(SLICE|&SLICE, EXPECTED_ENTRIES)", model, 1, true)
default: // arraySuper
a.err = ctxerr.OpBadUsage("SuperSliceOf",
"(ARRAY|&ARRAY|SLICE|&SLICE, EXPECTED_ENTRIES)", model, 1, true)
}
return &a
}
// summary(Array): compares the contents of an array or a pointer on an array
// input(Array): array,ptr(ptr on array)
// Array operator compares the contents of an array or a pointer on an
// array against the values of model and the values of
// expectedEntries. Entries with zero values of model are ignored
// if the same entry is present in expectedEntries, otherwise they
// are taken into account. An entry cannot be present in both model
// and expectedEntries, except if it is a zero-value in model. At
// the end, all entries are checked. To check only some entries of an
// array, see [SuperSliceOf] operator.
//
// model must be the same type as compared data.
//
// expectedEntries can be omitted, if no zero entries are expected and
// no [TestDeep] operators are involved. If expectedEntries contains
// more than one item, all items are merged before their use, from
// left to right.
//
// got := [3]int{12, 14, 17}
// td.Cmp(t, got, td.Array([3]int{0, 14}, td.ArrayEntries{0: 12, 2: 17})) // succeeds
// td.Cmp(t, &got,
// td.Array(&[3]int{0, 14}, td.ArrayEntries{0: td.Gt(10), 2: td.Gt(15)})) // succeeds
//
// TypeBehind method returns the [reflect.Type] of model.
//
// See also [List], [Slice] and [SuperSliceOf].
func Array(model any, expectedEntries ...ArrayEntries) TestDeep {
return newArray(arrayArray, model, expectedEntries...)
}
// summary(Slice): compares the contents of a slice or a pointer on a slice
// input(Slice): slice,ptr(ptr on slice)
// Slice operator compares the contents of a slice or a pointer on a
// slice against the values of model and the values of
// expectedEntries. Entries with zero values of model are ignored
// if the same entry is present in expectedEntries, otherwise they
// are taken into account. An entry cannot be present in both model
// and expectedEntries, except if it is a zero-value in model. At
// the end, all entries are checked. To check only some entries of a
// slice, see [SuperSliceOf] operator.
//
// model must be the same type as compared data.
//
// expectedEntries can be omitted, if no zero entries are expected and
// no [TestDeep] operators are involved. If expectedEntries contains
// more than one item, all items are merged before their use, from
// left to right.
//
// got := []int{12, 14, 17}
// td.Cmp(t, got, td.Slice([]int{0, 14}, td.ArrayEntries{0: 12, 2: 17})) // succeeds
// td.Cmp(t, &got,
// td.Slice(&[]int{0, 14}, td.ArrayEntries{0: td.Gt(10), 2: td.Gt(15)})) // succeeds
//
// TypeBehind method returns the [reflect.Type] of model.
//
// See also [Array], [List] and [SuperSliceOf].
func Slice(model any, expectedEntries ...ArrayEntries) TestDeep {
return newArray(arraySlice, model, expectedEntries...)
}
// summary(SuperSliceOf): compares the contents of a slice, a pointer
// on a slice, an array or a pointer on an array but with potentially
// some extra entries
// input(SuperSliceOf): array,slice,ptr(ptr on array/slice)
// SuperSliceOf operator compares the contents of an array, a pointer
// on an array, a slice or a pointer on a slice against the non-zero
// values of model (if any) and the values of expectedEntries. So
// entries with zero value of model are always ignored. If a zero
// value check is needed, this zero value has to be set in
// expectedEntries. An entry cannot be present in both model and
// expectedEntries, except if it is a zero-value in model. At the
// end, only entries present in expectedEntries and non-zero ones
// present in model are checked. To check all entries of an array
// see [Array] operator. To check all entries of a slice see [Slice]
// operator.
//
// model must be the same type as compared data.
//
// expectedEntries can be omitted, if no zero entries are expected and
// no [TestDeep] operators are involved. If expectedEntries contains
// more than one item, all items are merged before their use, from
// left to right.
//
// Works with slices:
//
// got := []int{12, 14, 17}
// td.Cmp(t, got, td.SuperSliceOf([]int{12})) // succeeds
// td.Cmp(t, got, td.SuperSliceOf([]int{12}, td.ArrayEntries{2: 17})) // succeeds
// td.Cmp(t, &got, td.SuperSliceOf(&[]int{0, 14}, td.ArrayEntries{2: td.Gt(16)})) // succeeds
//
// and arrays:
//
// got := [5]int{12, 14, 17, 26, 56}
// td.Cmp(t, got, td.SuperSliceOf([5]int{12})) // succeeds
// td.Cmp(t, got, td.SuperSliceOf([5]int{12}, td.ArrayEntries{2: 17})) // succeeds
// td.Cmp(t, &got, td.SuperSliceOf(&[5]int{0, 14}, td.ArrayEntries{2: td.Gt(16)})) // succeeds
//
// See also [Array] and [Slice].
func SuperSliceOf(model any, expectedEntries ...ArrayEntries) TestDeep {
return newArray(arraySuper, model, expectedEntries...)
}
func (a *tdArray) populateExpectedEntries(expectedEntries ArrayEntries, expectedModel reflect.Value) {
// Compute highest expected index
maxExpectedIdx := -1
for index := range expectedEntries {
if index > maxExpectedIdx {
maxExpectedIdx = index
}
}
var numEntries int
array := a.expectedType.Kind() == reflect.Array
if array {
numEntries = a.expectedType.Len()
if numEntries <= maxExpectedIdx {
a.err = ctxerr.OpBad(
a.GetLocation().Func,
"array length is %d, so cannot have #%d expected index",
numEntries,
maxExpectedIdx)
return
}
} else {
numEntries = maxExpectedIdx + 1
// If slice is non-nil
if expectedModel.IsValid() {
if numEntries < expectedModel.Len() {
numEntries = expectedModel.Len()
}
}
}
a.expectedEntries = make([]reflect.Value, numEntries)
elemType := a.expectedType.Elem()
var vexpectedValue reflect.Value
for index, expectedValue := range expectedEntries {
if expectedValue == nil {
switch elemType.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map,
reflect.Ptr, reflect.Slice:
vexpectedValue = reflect.Zero(elemType) // change to a typed nil
default:
// Don't raise an error if item cannot be nil as a smuggle hook can
// change it at fly during the comparison
vexpectedValue = reflect.Value{}
}
} else {
vexpectedValue = reflect.ValueOf(expectedValue)
// Don't check vexpectedValue type against slice/array item one as a
// smuggle hook can change it at fly during the comparison
}
a.expectedEntries[index] = vexpectedValue
// SuperSliceOf
if a.onlyIndexes != nil {
a.onlyIndexes = append(a.onlyIndexes, index)
}
}
vzero := reflect.Zero(elemType)
// Check initialized entries in model
if expectedModel.IsValid() {
zero := vzero.Interface()
for index := expectedModel.Len() - 1; index >= 0; index-- {
ventry := expectedModel.Index(index)
modelIsZero := reflect.DeepEqual(zero, ventry.Interface())
// Entry already expected
if _, ok := expectedEntries[index]; ok {
// If non-zero entry, consider it as an error (= 2 expected
// values for the same item)
if !modelIsZero {
a.err = ctxerr.OpBad(
a.GetLocation().Func,
"non zero #%d entry in model already exists in expectedEntries",
index)
return
}
continue
}
// Expect this entry except if not SuperSliceOf || not zero entry
if a.onlyIndexes == nil || !modelIsZero {
a.expectedEntries[index] = ventry
// SuperSliceOf
if a.onlyIndexes != nil {
a.onlyIndexes = append(a.onlyIndexes, index)
}
}
}
} else if a.expectedType.Kind() == reflect.Slice {
sort.Ints(a.onlyIndexes)
// nil slice
return
}
// For SuperSliceOf, we don't want to initialize missing entries
if a.onlyIndexes != nil {
sort.Ints(a.onlyIndexes)
return
}
var index int
// Array case, all is OK
if array {
// Non-nil array => a.expectedEntries already fully initialized
if expectedModel.IsValid() {
return
}
// nil array => a.expectedEntries must be initialized from index=0
// to numEntries - 1 below
} else {
// Non-nil slice => a.expectedEntries must be initialized from
// index=len(slice) to last entry index of expectedEntries
index = expectedModel.Len()
}
// Slice case, initialize missing expected items to zero
for ; index < numEntries; index++ {
if _, ok := expectedEntries[index]; !ok {
a.expectedEntries[index] = vzero
}
}
}
func (a *tdArray) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if a.err != nil {
return ctx.CollectError(a.err)
}
err := a.checkPtr(ctx, &got, true)
if err != nil {
return ctx.CollectError(err)
}
err = a.checkType(ctx, got)
if err != nil {
return ctx.CollectError(err)
}
gotLen := got.Len()
check := func(index int, expectedValue reflect.Value) *ctxerr.Error {
curCtx := ctx.AddArrayIndex(index)
if index >= gotLen {
if curCtx.BooleanError {
return ctxerr.BooleanError
}
return curCtx.CollectError(&ctxerr.Error{
Message: "expected value out of range",
Got: types.RawString(""),
Expected: expectedValue,
})
}
return deepValueEqual(curCtx, got.Index(index), expectedValue)
}
// SuperSliceOf, only check some indexes
if a.onlyIndexes != nil {
for _, index := range a.onlyIndexes {
err = check(index, a.expectedEntries[index])
if err != nil {
return err
}
}
return nil
}
// Array or Slice
for index, expectedValue := range a.expectedEntries {
err = check(index, expectedValue)
if err != nil {
return err
}
}
if gotLen > len(a.expectedEntries) {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.AddArrayIndex(len(a.expectedEntries)).
CollectError(&ctxerr.Error{
Message: "got value out of range",
Got: got.Index(len(a.expectedEntries)),
Expected: types.RawString(""),
})
}
return nil
}
func (a *tdArray) String() string {
if a.err != nil {
return a.stringError()
}
buf := bytes.NewBufferString(a.GetLocation().Func)
buf.WriteByte('(')
buf.WriteString(a.expectedTypeStr())
if len(a.expectedEntries) == 0 {
buf.WriteString("{})")
} else {
buf.WriteString("{\n")
for index, expectedValue := range a.expectedEntries {
fmt.Fprintf(buf, " %d: %s\n", //nolint: errcheck
index, util.ToString(expectedValue))
}
buf.WriteString("})")
}
return buf.String()
}
go-testdeep-1.15.0/td/td_array_each.go 0000664 0000000 0000000 00000005351 15144170453 0017551 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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"
"strings"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/types"
"github.com/maxatome/go-testdeep/internal/util"
)
type tdArrayEach struct {
baseOKNil
expected reflect.Value
}
var _ TestDeep = &tdArrayEach{}
// summary(ArrayEach): compares each array or slice item
// input(ArrayEach): array,slice,ptr(ptr on array/slice)
// ArrayEach operator has to be applied on arrays or slices or on
// pointers on array/slice. It compares each item of data array/slice
// against expectedValue. During a match, all items have to match to
// succeed.
//
// got := [3]string{"foo", "bar", "biz"}
// td.Cmp(t, got, td.ArrayEach(td.Len(3))) // succeeds
// td.Cmp(t, got, td.ArrayEach(td.HasPrefix("b"))) // fails coz "foo"
//
// Works on slices as well:
//
// got := []Person{
// {Name: "Bob", Age: 42},
// {Name: "Alice", Age: 24},
// }
// td.Cmp(t, got, td.ArrayEach(
// td.Struct(Person{}, td.StructFields{
// Age: td.Between(20, 45),
// })),
// ) // succeeds, each Person has Age field between 20 and 45
func ArrayEach(expectedValue any) TestDeep {
return &tdArrayEach{
baseOKNil: newBaseOKNil(3),
expected: reflect.ValueOf(expectedValue),
}
}
func (a *tdArrayEach) Match(ctx ctxerr.Context, got reflect.Value) (err *ctxerr.Error) {
if !got.IsValid() {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "nil value",
Got: types.RawString("nil"),
Expected: types.RawString("slice OR array OR *slice OR *array"),
})
}
switch got.Kind() {
case 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"))
}
if gotElem.Kind() != reflect.Array && gotElem.Kind() != reflect.Slice {
break
}
got = gotElem
fallthrough
case reflect.Array, reflect.Slice:
gotLen := got.Len()
var err *ctxerr.Error
for idx := 0; idx < gotLen; idx++ {
err = deepValueEqual(ctx.AddArrayIndex(idx), got.Index(idx), a.expected)
if err != nil {
return err
}
}
return nil
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(ctxerr.BadKind(got, "slice OR array OR *slice OR *array"))
}
func (a *tdArrayEach) String() string {
const prefix = "ArrayEach("
content := util.ToString(a.expected)
if strings.Contains(content, "\n") {
return prefix + util.IndentString(content, " ") + ")"
}
return prefix + content + ")"
}
go-testdeep-1.15.0/td/td_array_each_test.go 0000664 0000000 0000000 00000006016 15144170453 0020607 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 TestArrayEach(t *testing.T) {
type MyArray [3]int
type MyEmptyArray [0]int
type MySlice []int
checkOKForEach(t,
[]any{
[...]int{4, 4, 4},
[]int{4, 4, 4},
&[...]int{4, 4, 4},
&[]int{4, 4, 4},
MyArray{4, 4, 4},
MySlice{4, 4, 4},
&MyArray{4, 4, 4},
&MySlice{4, 4, 4},
},
td.ArrayEach(4))
// Empty slice/array
checkOKForEach(t,
[]any{
[0]int{},
[]int{},
&[0]int{},
&[]int{},
MyEmptyArray{},
MySlice{},
&MyEmptyArray{},
&MySlice{},
// nil cases
([]int)(nil),
MySlice(nil),
},
td.ArrayEach(4))
checkError(t, (*MyArray)(nil), td.ArrayEach(4),
expectedError{
Message: mustBe("nil pointer"),
Path: mustBe("DATA"),
Got: mustBe("nil *array (*td_test.MyArray type)"),
Expected: mustBe("non-nil *slice OR *array"),
})
checkError(t, (*MySlice)(nil), td.ArrayEach(4),
expectedError{
Message: mustBe("nil pointer"),
Path: mustBe("DATA"),
Got: mustBe("nil *slice (*td_test.MySlice type)"),
Expected: mustBe("non-nil *slice OR *array"),
})
checkOKForEach(t,
[]any{
[...]int{20, 22, 29},
[]int{20, 22, 29},
MyArray{20, 22, 29},
MySlice{20, 22, 29},
&MyArray{20, 22, 29},
&MySlice{20, 22, 29},
},
td.ArrayEach(td.Between(20, 30)))
checkError(t, nil, td.ArrayEach(4),
expectedError{
Message: mustBe("nil value"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("slice OR array OR *slice OR *array"),
})
checkErrorForEach(t,
[]any{
[...]int{4, 5, 4},
[]int{4, 5, 4},
MyArray{4, 5, 4},
MySlice{4, 5, 4},
&MyArray{4, 5, 4},
&MySlice{4, 5, 4},
},
td.ArrayEach(4),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[1]"),
Got: mustBe("5"),
Expected: mustBe("4"),
})
checkError(t, 666, td.ArrayEach(4),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("slice OR array OR *slice OR *array"),
})
num := 666
checkError(t, &num, td.ArrayEach(4),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("*int"),
Expected: mustBe("slice OR array OR *slice OR *array"),
})
checkOK(t, []any{nil, nil, nil}, td.ArrayEach(nil))
checkError(t, []any{nil, nil, nil, 66}, td.ArrayEach(nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[3]"),
Got: mustBe("66"),
Expected: mustBe("nil"),
})
//
// String
test.EqualStr(t, td.ArrayEach(4).String(), "ArrayEach(4)")
test.EqualStr(t, td.ArrayEach(td.All(1, 2)).String(),
`ArrayEach(All(1,
2))`)
}
func TestArrayEachTypeBehind(t *testing.T) {
equalTypes(t, td.ArrayEach(6), nil)
}
go-testdeep-1.15.0/td/td_array_test.go 0000664 0000000 0000000 00000052742 15144170453 0017636 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 TestArray(t *testing.T) {
type MyArray [5]int
//
// Simple array
checkOK(t, [5]int{}, td.Array([5]int{}))
checkOK(t, [5]int{}, td.Array([5]int{}, nil))
checkOK(t, [5]int{0, 0, 0, 4}, td.Array([5]int{0, 0, 0, 4}))
checkOK(t, [5]int{1, 0, 3},
td.Array([5]int{}, td.ArrayEntries{2: 3, 0: 1}))
checkOK(t, [5]int{1, 2, 3},
td.Array([5]int{0, 2}, td.ArrayEntries{2: 3, 0: 1}))
checkOK(t, [5]any{1, 2, nil, 4, nil},
td.Array([5]any{nil, 2, nil, 4}, td.ArrayEntries{0: 1, 2: nil}))
checkOK(t, [5]any{1, 2, nil, 4, nil},
td.Array([5]any{nil, 2, nil, 4},
td.ArrayEntries{0: 3, 2: 28},
td.ArrayEntries{0: 2, 2: "pipo"},
td.ArrayEntries{0: 1, 2: nil},
))
zero, one, two := 0, 1, 2
checkOK(t, [5]*int{nil, &zero, &one, &two},
td.Array(
[5]*int{}, td.ArrayEntries{1: &zero, 2: &one, 3: &two, 4: nil}))
gotArray := [...]int{1, 2, 3, 4, 5}
checkError(t, gotArray, td.Array(MyArray{}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("[5]int"),
Expected: mustBe("td_test.MyArray"),
})
checkError(t, gotArray, td.Array([5]int{1, 2, 3, 4, 6}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[4]"),
Got: mustBe("5"),
Expected: mustBe("6"),
})
checkError(t, gotArray,
td.Array([5]int{1, 2, 3, 4}, td.ArrayEntries{4: 6}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[4]"),
Got: mustBe("5"),
Expected: mustBe("6"),
})
checkError(t, nil,
td.Array([1]int{42}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustContain("Array("),
})
//
// Array type
checkOK(t, MyArray{}, td.Array(MyArray{}))
checkOK(t, MyArray{}, td.Array(MyArray{}, nil))
checkOK(t, MyArray{0, 0, 0, 4}, td.Array(MyArray{0, 0, 0, 4}))
checkOK(t, MyArray{1, 0, 3},
td.Array(MyArray{}, td.ArrayEntries{2: 3, 0: 1}))
checkOK(t, MyArray{1, 2, 3},
td.Array(MyArray{0, 2}, td.ArrayEntries{2: 3, 0: 1}))
checkOK(t, &MyArray{}, td.Array(&MyArray{}))
checkOK(t, &MyArray{0, 0, 0, 4}, td.Array(&MyArray{0, 0, 0, 4}))
checkOK(t, &MyArray{1, 0, 3},
td.Array(&MyArray{}, td.ArrayEntries{2: 3, 0: 1}))
checkOK(t, &MyArray{1, 0, 3},
td.Array((*MyArray)(nil), td.ArrayEntries{2: 3, 0: 1}))
checkOK(t, &MyArray{1, 2, 3},
td.Array(&MyArray{0, 2}, td.ArrayEntries{2: 3, 0: 1}))
gotTypedArray := MyArray{1, 2, 3, 4, 5}
checkError(t, 123, td.Array(&MyArray{}, td.ArrayEntries{}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("*td_test.MyArray"),
})
checkError(t, &MyStruct{},
td.Array(&MyArray{}, td.ArrayEntries{}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("*td_test.MyStruct"),
Expected: mustBe("*td_test.MyArray"),
})
checkError(t, gotTypedArray, td.Array([5]int{}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("td_test.MyArray"),
Expected: mustBe("[5]int"),
})
checkError(t, gotTypedArray, td.Array(MyArray{1, 2, 3, 4, 6}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[4]"),
Got: mustBe("5"),
Expected: mustBe("6"),
})
checkError(t, gotTypedArray,
td.Array(MyArray{1, 2, 3, 4}, td.ArrayEntries{4: 6}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[4]"),
Got: mustBe("5"),
Expected: mustBe("6"),
})
checkError(t, &gotTypedArray, td.Array([5]int{}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("*td_test.MyArray"),
Expected: mustBe("[5]int"),
})
checkError(t, &gotTypedArray, td.Array(&MyArray{1, 2, 3, 4, 6}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[4]"),
Got: mustBe("5"),
Expected: mustBe("6"),
})
checkError(t, &gotTypedArray,
td.Array(&MyArray{1, 2, 3, 4}, td.ArrayEntries{4: 6}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[4]"),
Got: mustBe("5"),
Expected: mustBe("6"),
})
// Be lax...
// Without Lax → error
checkError(t, MyArray{}, td.Array([5]int{}),
expectedError{
Message: mustBe("type mismatch"),
})
checkError(t, [5]int{}, td.Array(MyArray{}),
expectedError{
Message: mustBe("type mismatch"),
})
checkOK(t, MyArray{}, td.Lax(td.Array([5]int{})))
checkOK(t, [5]int{}, td.Lax(td.Array(MyArray{})))
//
// Bad usage
checkError(t, "never tested",
td.Array("test"),
expectedError{
Message: mustBe("bad usage of Array operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Array(ARRAY|&ARRAY, EXPECTED_ENTRIES), but received string as 1st parameter"),
})
checkError(t, "never tested",
td.Array(&MyStruct{}),
expectedError{
Message: mustBe("bad usage of Array operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Array(ARRAY|&ARRAY, EXPECTED_ENTRIES), but received *td_test.MyStruct (ptr) as 1st parameter"),
})
checkError(t, "never tested",
td.Array([]int{}),
expectedError{
Message: mustBe("bad usage of Array operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Array(ARRAY|&ARRAY, EXPECTED_ENTRIES), but received []int (slice) as 1st parameter"),
})
checkError(t, "never tested",
td.Array([1]int{}, td.ArrayEntries{1: 34}),
expectedError{
Message: mustBe("bad usage of Array operator"),
Path: mustBe("DATA"),
Summary: mustBe("array length is 1, so cannot have #1 expected index"),
})
checkError(t, [3]int{},
td.Array([3]int{}, td.ArrayEntries{1: nil}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[1]"),
Got: mustBe("0"),
Expected: mustBe("nil"),
})
checkError(t, [3]int{},
td.Array([3]int{}, td.ArrayEntries{1: "bad"}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA[1]"),
Got: mustBe("int"),
Expected: mustBe("string"),
})
checkError(t, "never tested",
td.Array([1]int{12}, td.ArrayEntries{0: 21}),
expectedError{
Message: mustBe("bad usage of Array operator"),
Path: mustBe("DATA"),
Summary: mustBe("non zero #0 entry in model already exists in expectedEntries"),
})
//
// String
test.EqualStr(t,
td.Array(MyArray{0, 0, 4}, td.ArrayEntries{1: 3, 0: 2}).String(),
`Array(td_test.MyArray{
0: 2
1: 3
2: 4
3: 0
4: 0
})`)
test.EqualStr(t,
td.Array(&MyArray{0, 0, 4}, td.ArrayEntries{1: 3, 0: 2}).String(),
`Array(*td_test.MyArray{
0: 2
1: 3
2: 4
3: 0
4: 0
})`)
test.EqualStr(t, td.Array([0]int{}, td.ArrayEntries{}).String(),
`Array([0]int{})`)
// Erroneous op
test.EqualStr(t, td.Array(12).String(), "Array()")
}
func TestArrayTypeBehind(t *testing.T) {
type MyArray [12]int
equalTypes(t, td.Array([12]int{}), [12]int{})
equalTypes(t, td.Array(MyArray{}), MyArray{})
equalTypes(t, td.Array(&MyArray{}), &MyArray{})
// Erroneous op
equalTypes(t, td.Array(12), nil)
}
func TestSlice(t *testing.T) {
type MySlice []int
//
// Simple slice
checkOK(t, []int{}, td.Slice([]int{}))
checkOK(t, []int{}, td.Slice([]int{}, nil))
checkOK(t, []int{0, 3}, td.Slice([]int{0, 3}))
checkOK(t, []int{2, 3},
td.Slice([]int{}, td.ArrayEntries{1: 3, 0: 2}))
checkOK(t, []int{2, 3},
td.Slice(([]int)(nil), td.ArrayEntries{1: 3, 0: 2}))
checkOK(t, []int{2, 3, 4},
td.Slice([]int{0, 0, 4}, td.ArrayEntries{1: 3, 0: 2}))
checkOK(t, []int{2, 3, 4},
td.Slice([]int{2, 3}, td.ArrayEntries{2: 4}))
checkOK(t, []int{2, 3, 4, 0, 6},
td.Slice([]int{2, 3}, td.ArrayEntries{2: 4, 4: 6}))
gotSlice := []int{2, 3, 4}
checkError(t, gotSlice, td.Slice(MySlice{}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("[]int"),
Expected: mustBe("td_test.MySlice"),
})
checkError(t, gotSlice, td.Slice([]int{2, 3, 5}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[2]"),
Got: mustBe("4"),
Expected: mustBe("5"),
})
checkError(t, gotSlice,
td.Slice([]int{2, 3}, td.ArrayEntries{2: 5}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[2]"),
Got: mustBe("4"),
Expected: mustBe("5"),
})
checkError(t, nil,
td.Slice([]int{2, 3}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustContain("Slice("),
})
//
// Slice type
checkOK(t, MySlice{}, td.Slice(MySlice{}))
checkOK(t, MySlice{}, td.Slice(MySlice{}, nil))
checkOK(t, MySlice{0, 3}, td.Slice(MySlice{0, 3}))
checkOK(t, MySlice{2, 3},
td.Slice(MySlice{}, td.ArrayEntries{1: 3, 0: 2}))
checkOK(t, MySlice{2, 3},
td.Slice((MySlice)(nil), td.ArrayEntries{1: 3, 0: 2}))
checkOK(t, MySlice{2, 3, 4},
td.Slice(MySlice{0, 0, 4}, td.ArrayEntries{1: 3, 0: 2}))
checkOK(t, MySlice{2, 3, 4, 0, 6},
td.Slice(MySlice{2, 3}, td.ArrayEntries{2: 4, 4: 6}))
checkOK(t, &MySlice{}, td.Slice(&MySlice{}))
checkOK(t, &MySlice{0, 3}, td.Slice(&MySlice{0, 3}))
checkOK(t, &MySlice{2, 3},
td.Slice(&MySlice{}, td.ArrayEntries{1: 3, 0: 2}))
checkOK(t, &MySlice{2, 3},
td.Slice((*MySlice)(nil), td.ArrayEntries{1: 3, 0: 2}))
checkOK(t, &MySlice{2, 3, 4},
td.Slice(&MySlice{0, 0, 4}, td.ArrayEntries{1: 3, 0: 2}))
checkOK(t, &MySlice{2, 3, 4, 0, 6},
td.Slice(&MySlice{2, 3}, td.ArrayEntries{2: 4, 4: 6}))
gotTypedSlice := MySlice{2, 3, 4}
checkError(t, 123, td.Slice(&MySlice{}, td.ArrayEntries{}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("*td_test.MySlice"),
})
checkError(t, &MyStruct{},
td.Slice(&MySlice{}, td.ArrayEntries{}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("*td_test.MyStruct"),
Expected: mustBe("*td_test.MySlice"),
})
checkError(t, gotTypedSlice, td.Slice([]int{}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("td_test.MySlice"),
Expected: mustBe("[]int"),
})
checkError(t, gotTypedSlice, td.Slice(MySlice{2, 3, 5}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[2]"),
Got: mustBe("4"),
Expected: mustBe("5"),
})
checkError(t, gotTypedSlice,
td.Slice(MySlice{2, 3}, td.ArrayEntries{2: 5}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[2]"),
Got: mustBe("4"),
Expected: mustBe("5"),
})
checkError(t, gotTypedSlice,
td.Slice(MySlice{2, 3, 4}, td.ArrayEntries{3: 5}),
expectedError{
Message: mustBe("expected value out of range"),
Path: mustBe("DATA[3]"),
Got: mustBe(""),
Expected: mustBe("5"),
})
checkError(t, gotTypedSlice, td.Slice(MySlice{2, 3}),
expectedError{
Message: mustBe("got value out of range"),
Path: mustBe("DATA[2]"),
Got: mustBe("4"),
Expected: mustBe(""),
})
checkError(t, &gotTypedSlice, td.Slice([]int{}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("*td_test.MySlice"),
Expected: mustBe("[]int"),
})
checkError(t, &gotTypedSlice, td.Slice(&MySlice{2, 3, 5}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[2]"),
Got: mustBe("4"),
Expected: mustBe("5"),
})
checkError(t, &gotTypedSlice,
td.Slice(&MySlice{2, 3}, td.ArrayEntries{2: 5}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[2]"),
Got: mustBe("4"),
Expected: mustBe("5"),
})
checkError(t, &gotTypedSlice, td.Slice(&MySlice{2, 3}),
expectedError{
Message: mustBe("got value out of range"),
Path: mustBe("DATA[2]"),
Got: mustBe("4"),
Expected: mustBe(""),
})
//
// nil cases
var (
gotNilSlice []int
gotNilTypedSlice MySlice
)
checkOK(t, gotNilSlice, td.Slice([]int{}))
checkOK(t, gotNilTypedSlice, td.Slice(MySlice{}))
checkOK(t, &gotNilTypedSlice, td.Slice(&MySlice{}))
// Be lax...
// Without Lax → error
checkError(t, MySlice{}, td.Slice([]int{}),
expectedError{
Message: mustBe("type mismatch"),
})
checkError(t, []int{}, td.Slice(MySlice{}),
expectedError{
Message: mustBe("type mismatch"),
})
checkOK(t, MySlice{}, td.Lax(td.Slice([]int{})))
checkOK(t, []int{}, td.Lax(td.Slice(MySlice{})))
//
// Bad usage
checkError(t, "never tested",
td.Slice("test", nil),
expectedError{
Message: mustBe("bad usage of Slice operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Slice(SLICE|&SLICE, EXPECTED_ENTRIES), but received string as 1st parameter"),
})
checkError(t, "never tested",
td.Slice(&MyStruct{}),
expectedError{
Message: mustBe("bad usage of Slice operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Slice(SLICE|&SLICE, EXPECTED_ENTRIES), but received *td_test.MyStruct (ptr) as 1st parameter"),
})
checkError(t, "never tested",
td.Slice([0]int{}),
expectedError{
Message: mustBe("bad usage of Slice operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Slice(SLICE|&SLICE, EXPECTED_ENTRIES), but received [0]int (array) as 1st parameter"),
})
checkError(t, []int{0, 5},
td.Slice([]int{}, td.ArrayEntries{1: "bad"}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA[1]"),
Got: mustBe("int"),
Expected: mustBe("string"),
})
checkError(t, "never tested",
td.Slice([]int{12}, td.ArrayEntries{0: 21}),
expectedError{
Message: mustBe("bad usage of Slice operator"),
Path: mustBe("DATA"),
Summary: mustBe("non zero #0 entry in model already exists in expectedEntries"),
})
//
// String
test.EqualStr(t,
td.Slice(MySlice{0, 0, 4}, td.ArrayEntries{1: 3, 0: 2}).String(),
`Slice(td_test.MySlice{
0: 2
1: 3
2: 4
})`)
test.EqualStr(t,
td.Slice(&MySlice{0, 0, 4}, td.ArrayEntries{1: 3, 0: 2}).String(),
`Slice(*td_test.MySlice{
0: 2
1: 3
2: 4
})`)
test.EqualStr(t, td.Slice(&MySlice{}, td.ArrayEntries{}).String(),
`Slice(*td_test.MySlice{})`)
// Erroneous op
test.EqualStr(t, td.Slice(12).String(), "Slice()")
}
func TestSliceTypeBehind(t *testing.T) {
type MySlice []int
equalTypes(t, td.Slice([]int{}), []int{})
equalTypes(t, td.Slice(MySlice{}), MySlice{})
equalTypes(t, td.Slice(&MySlice{}), &MySlice{})
// Erroneous op
equalTypes(t, td.Slice(12), nil)
}
func TestSuperSliceOf(t *testing.T) {
t.Run("interface array", func(t *testing.T) {
got := [5]any{"foo", "bar", nil, 666, 777}
checkOK(t, got,
td.SuperSliceOf([5]any{1: "bar"}, td.ArrayEntries{2: td.Nil()}))
checkOK(t, got,
td.SuperSliceOf([5]any{1: "bar"}, td.ArrayEntries{2: nil}))
checkOK(t, got,
td.SuperSliceOf([5]any{1: "bar"}, td.ArrayEntries{3: 666}))
checkOK(t, got,
td.SuperSliceOf([5]any{1: "bar"}, td.ArrayEntries{3: td.Between(665, 667)}))
checkOK(t, &got,
td.SuperSliceOf(&[5]any{1: "bar"}, td.ArrayEntries{3: td.Between(665, 667)}))
checkOK(t, got,
td.SuperSliceOf([5]any{1: "bar"},
td.ArrayEntries{3: 42},
td.ArrayEntries{3: "pipo"},
td.ArrayEntries{3: 666},
))
checkError(t, got,
td.SuperSliceOf([5]any{1: "foo"}, td.ArrayEntries{2: td.Nil()}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[1]"),
Got: mustBe(`"bar"`),
Expected: mustBe(`"foo"`),
})
checkError(t, got,
td.SuperSliceOf([5]any{1: 666}, td.ArrayEntries{2: td.Nil()}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA[1]"),
Got: mustBe("string"),
Expected: mustBe("int"),
})
checkError(t, &got,
td.SuperSliceOf([5]any{1: 666}, td.ArrayEntries{2: td.Nil()}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("*[5]interface {}"),
Expected: mustBe("[5]interface {}"),
})
checkError(t, got,
td.SuperSliceOf(&[5]any{1: 666}, td.ArrayEntries{2: td.Nil()}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("[5]interface {}"),
Expected: mustBe("*[5]interface {}"),
})
})
t.Run("ints array", func(t *testing.T) {
type MyArray [5]int
checkOK(t, MyArray{}, td.SuperSliceOf(MyArray{}))
got := MyArray{3: 4}
checkOK(t, got, td.SuperSliceOf(MyArray{}))
checkOK(t, got, td.SuperSliceOf(MyArray{3: 4}))
checkOK(t, got, td.SuperSliceOf(MyArray{}, td.ArrayEntries{3: 4}))
checkError(t, got,
td.SuperSliceOf(MyArray{}, td.ArrayEntries{1: 666}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[1]"),
Got: mustBe(`0`),
Expected: mustBe(`666`),
})
// Be lax...
// Without Lax → error
checkError(t, got,
td.SuperSliceOf([5]int{}, td.ArrayEntries{3: 4}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe(`td_test.MyArray`),
Expected: mustBe(`[5]int`),
})
checkOK(t, got, td.Lax(td.SuperSliceOf([5]int{}, td.ArrayEntries{3: 4})))
checkError(t, [5]int{3: 4},
td.SuperSliceOf(MyArray{}, td.ArrayEntries{3: 4}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe(`[5]int`),
Expected: mustBe(`td_test.MyArray`),
})
checkOK(t, [5]int{3: 4},
td.Lax(td.SuperSliceOf(MyArray{}, td.ArrayEntries{3: 4})))
checkError(t, "never tested",
td.SuperSliceOf(MyArray{}, td.ArrayEntries{8: 34}),
expectedError{
Message: mustBe("bad usage of SuperSliceOf operator"),
Path: mustBe("DATA"),
Summary: mustBe("array length is 5, so cannot have #8 expected index"),
})
})
t.Run("ints slice", func(t *testing.T) {
type MySlice []int
checkOK(t, MySlice{}, td.SuperSliceOf(MySlice{}))
checkOK(t, MySlice(nil), td.SuperSliceOf(MySlice{}))
got := MySlice{3: 4}
checkOK(t, got, td.SuperSliceOf(MySlice{}, td.ArrayEntries{3: td.N(5, 1)}))
checkOK(t, got, td.SuperSliceOf(MySlice{3: 4}, td.ArrayEntries{2: 0}))
checkError(t, got,
td.SuperSliceOf(MySlice{}, td.ArrayEntries{1: 666}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[1]"),
Got: mustBe(`0`),
Expected: mustBe(`666`),
})
checkError(t, got,
td.SuperSliceOf(MySlice{}, td.ArrayEntries{3: 0}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[3]"),
Got: mustBe(`4`),
Expected: mustBe(`0`),
})
checkError(t, got,
td.SuperSliceOf(MySlice{}, td.ArrayEntries{28: 666}),
expectedError{
Message: mustBe("expected value out of range"),
Path: mustBe("DATA[28]"),
Got: mustBe(``),
Expected: mustBe(`666`),
})
checkError(t, got,
td.SuperSliceOf(MySlice{28: 666}),
expectedError{
Message: mustBe("expected value out of range"),
Path: mustBe("DATA[28]"),
Got: mustBe(``),
Expected: mustBe(`666`),
})
// Be lax...
// Without Lax → error
checkError(t, got,
td.SuperSliceOf([]int{}, td.ArrayEntries{3: 4}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe(`td_test.MySlice`),
Expected: mustBe(`[]int`),
})
checkOK(t, got, td.Lax(td.SuperSliceOf([]int{}, td.ArrayEntries{3: 4})))
checkError(t, []int{3: 4},
td.SuperSliceOf(MySlice{}, td.ArrayEntries{3: 4}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe(`[]int`),
Expected: mustBe(`td_test.MySlice`),
})
checkOK(t, []int{3: 4},
td.Lax(td.SuperSliceOf(MySlice{}, td.ArrayEntries{3: 4})))
})
//
// Bad usage
checkError(t, "never tested",
td.SuperSliceOf("test", nil),
expectedError{
Message: mustBe("bad usage of SuperSliceOf operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: SuperSliceOf(ARRAY|&ARRAY|SLICE|&SLICE, EXPECTED_ENTRIES), but received string as 1st parameter"),
})
checkError(t, "never tested",
td.SuperSliceOf(&MyStruct{}),
expectedError{
Message: mustBe("bad usage of SuperSliceOf operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: SuperSliceOf(ARRAY|&ARRAY|SLICE|&SLICE, EXPECTED_ENTRIES), but received *td_test.MyStruct (ptr) as 1st parameter"),
})
checkError(t, []int{0, 1},
td.SuperSliceOf([]int{}, td.ArrayEntries{1: "bad"}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA[1]"),
Got: mustBe("int"),
Expected: mustBe("string"),
})
checkError(t, "never tested",
td.SuperSliceOf([]int{12}, td.ArrayEntries{0: 21}),
expectedError{
Message: mustBe("bad usage of SuperSliceOf operator"),
Path: mustBe("DATA"),
Summary: mustBe("non zero #0 entry in model already exists in expectedEntries"),
})
// Erroneous op
test.EqualStr(t, td.SuperSliceOf(12).String(), "SuperSliceOf()")
}
func TestSuperSliceOfTypeBehind(t *testing.T) {
type MySlice []int
equalTypes(t, td.SuperSliceOf([]int{}), []int{})
equalTypes(t, td.SuperSliceOf(MySlice{}), MySlice{})
equalTypes(t, td.SuperSliceOf(&MySlice{}), &MySlice{})
type MyArray [12]int
equalTypes(t, td.SuperSliceOf([12]int{}), [12]int{})
equalTypes(t, td.SuperSliceOf(MyArray{}), MyArray{})
equalTypes(t, td.SuperSliceOf(&MyArray{}), &MyArray{})
// Erroneous op
equalTypes(t, td.SuperSliceOf(12), nil)
}
go-testdeep-1.15.0/td/td_bag.go 0000664 0000000 0000000 00000012405 15144170453 0016202 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2025, 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
// summary(Bag): compares the contents of an array or a slice without taking
// care of the order of items
// input(Bag): array,slice,ptr(ptr on array/slice)
// Bag operator compares the contents of an array or a slice (or a
// pointer on array/slice) without taking care of the order of items.
//
// During a match, each expected item should match in the compared
// array/slice, and each array/slice item should be matched by an
// expected item to succeed.
//
// td.Cmp(t, []int{1, 1, 2}, td.Bag(1, 1, 2)) // succeeds
// td.Cmp(t, []int{1, 1, 2}, td.Bag(1, 2, 1)) // succeeds
// td.Cmp(t, []int{1, 1, 2}, td.Bag(2, 1, 1)) // succeeds
// td.Cmp(t, []int{1, 1, 2}, td.Bag(1, 2)) // fails, one 1 is missing
// td.Cmp(t, []int{1, 1, 2}, td.Bag(1, 2, 1, 3)) // fails, 3 is missing
//
// // works with slices/arrays of any type
// td.Cmp(t, personSlice, td.Bag(
// Person{Name: "Bob", Age: 32},
// Person{Name: "Alice", Age: 26},
// ))
//
// To flatten a non-[]any slice/array, use [Flatten] function
// and so avoid boring and inefficient copies:
//
// expected := []int{1, 2, 1}
// td.Cmp(t, []int{1, 1, 2}, td.Bag(td.Flatten(expected))) // succeeds
// // = td.Cmp(t, []int{1, 1, 2}, td.Bag(1, 2, 1))
//
// exp1 := []int{5, 1, 1}
// exp2 := []int{8, 42, 3}
// td.Cmp(t, []int{1, 5, 1, 8, 42, 3, 3},
// td.Bag(td.Flatten(exp1), 3, td.Flatten(exp2))) // succeeds
// // = td.Cmp(t, []int{1, 5, 1, 8, 42, 3, 3}, td.Bag(5, 1, 1, 3, 8, 42, 3))
//
// TypeBehind method can return a non-nil [reflect.Type] if all items
// known non-interface types are equal, or if only interface types
// are found (mostly issued from Isa()) and they are equal.
//
// See also [SubBagOf], [SuperBagOf], [Set], [List] and [Sort].
func Bag(expectedItems ...any) TestDeep {
return newSetBase(allSet, false, expectedItems)
}
// summary(SubBagOf): compares the contents of an array or a slice
// without taking care of the order of items but with potentially some
// exclusions
// input(SubBagOf): array,slice,ptr(ptr on array/slice)
// SubBagOf operator compares the contents of an array or a slice (or a
// pointer on array/slice) without taking care of the order of items.
//
// During a match, each array/slice item should be matched by an
// expected item to succeed. But some expected items can be missing
// from the compared array/slice.
//
// td.Cmp(t, []int{1}, td.SubBagOf(1, 1, 2)) // succeeds
// td.Cmp(t, []int{1, 1, 1}, td.SubBagOf(1, 1, 2)) // fails, one 1 is an extra item
//
// // works with slices/arrays of any type
// td.Cmp(t, personSlice, td.SubBagOf(
// Person{Name: "Bob", Age: 32},
// Person{Name: "Alice", Age: 26},
// ))
//
// To flatten a non-[]any slice/array, use [Flatten] function
// and so avoid boring and inefficient copies:
//
// expected := []int{1, 2, 1}
// td.Cmp(t, []int{1}, td.SubBagOf(td.Flatten(expected))) // succeeds
// // = td.Cmp(t, []int{1}, td.SubBagOf(1, 2, 1))
//
// exp1 := []int{5, 1, 1}
// exp2 := []int{8, 42, 3}
// td.Cmp(t, []int{1, 42, 3},
// td.SubBagOf(td.Flatten(exp1), 3, td.Flatten(exp2))) // succeeds
// // = td.Cmp(t, []int{1, 42, 3}, td.SubBagOf(5, 1, 1, 3, 8, 42, 3))
//
// TypeBehind method can return a non-nil [reflect.Type] if all items
// known non-interface types are equal, or if only interface types
// are found (mostly issued from Isa()) and they are equal.
//
// See also [Bag] and [SuperBagOf].
func SubBagOf(expectedItems ...any) TestDeep {
return newSetBase(subSet, false, expectedItems)
}
// summary(SuperBagOf): compares the contents of an array or a slice
// without taking care of the order of items but with potentially some
// extra items
// input(SuperBagOf): array,slice,ptr(ptr on array/slice)
// SuperBagOf operator compares the contents of an array or a slice (or a
// pointer on array/slice) without taking care of the order of items.
//
// During a match, each expected item should match in the compared
// array/slice. But some items in the compared array/slice may not be
// expected.
//
// td.Cmp(t, []int{1, 1, 2}, td.SuperBagOf(1)) // succeeds
// td.Cmp(t, []int{1, 1, 2}, td.SuperBagOf(1, 1, 1)) // fails, one 1 is missing
//
// // works with slices/arrays of any type
// td.Cmp(t, personSlice, td.SuperBagOf(
// Person{Name: "Bob", Age: 32},
// Person{Name: "Alice", Age: 26},
// ))
//
// To flatten a non-[]any slice/array, use [Flatten] function
// and so avoid boring and inefficient copies:
//
// expected := []int{1, 2, 1}
// td.Cmp(t, []int{1}, td.SuperBagOf(td.Flatten(expected))) // succeeds
// // = td.Cmp(t, []int{1}, td.SuperBagOf(1, 2, 1))
//
// exp1 := []int{5, 1, 1}
// exp2 := []int{8, 42}
// td.Cmp(t, []int{1, 5, 1, 8, 42, 3, 3, 6},
// td.SuperBagOf(td.Flatten(exp1), 3, td.Flatten(exp2))) // succeeds
// // = td.Cmp(t, []int{1, 5, 1, 8, 42, 3, 3, 6}, td.SuperBagOf(5, 1, 1, 3, 8, 42))
//
// TypeBehind method can return a non-nil [reflect.Type] if all items
// known non-interface types are equal, or if only interface types
// are found (mostly issued from Isa()) and they are equal.
//
// See also [Bag] and [SubBagOf].
func SuperBagOf(expectedItems ...any) TestDeep {
return newSetBase(superSet, false, expectedItems)
}
go-testdeep-1.15.0/td/td_bag_test.go 0000664 0000000 0000000 00000014470 15144170453 0017245 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (
"fmt"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestBag(t *testing.T) {
type MyArray [5]int
type MySlice []int
for idx, got := range []any{
[]int{1, 3, 4, 4, 5},
[...]int{1, 3, 4, 4, 5},
MySlice{1, 3, 4, 4, 5},
MyArray{1, 3, 4, 4, 5},
&MySlice{1, 3, 4, 4, 5},
&MyArray{1, 3, 4, 4, 5},
} {
testName := fmt.Sprintf("Test #%d → %v", idx, got)
//
// Bag
checkOK(t, got, td.Bag(5, 4, 1, 4, 3), testName)
checkError(t, got, td.Bag(5, 4, 1, 3),
expectedError{
Message: mustBe("comparing %% as a Bag"),
Path: mustBe("DATA"),
Summary: mustBe("Extra item: (4)"),
},
testName)
checkError(t, got, td.Bag(5, 4, 1, 4, 3, 66, 42),
expectedError{
Message: mustBe("comparing %% as a Bag"),
Path: mustBe("DATA"),
// items are sorted
Summary: mustBe(`Missing 2 items: (42,
66)`),
},
testName)
checkError(t, got, td.Bag(5, 66, 4, 1, 4, 3),
expectedError{
Message: mustBe("comparing %% as a Bag"),
Path: mustBe("DATA"),
Summary: mustBe("Missing item: (66)"),
},
testName)
checkError(t, got, td.Bag(5, 66, 4, 1, 4, 3, 66),
expectedError{
Message: mustBe("comparing %% as a Bag"),
Path: mustBe("DATA"),
Summary: mustBe("Missing 2 items: (66,\n 66)"),
},
testName)
checkError(t, got, td.Bag(5, 66, 4, 1, 3),
expectedError{
Message: mustBe("comparing %% as a Bag"),
Path: mustBe("DATA"),
Summary: mustBe("Missing item: (66)\n Extra item: (4)"),
},
testName)
checkError(t, got, td.Bag(1, td.JSON("{")),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe(`DATA`),
Summary: mustContain("JSON unmarshal error"),
Under: mustContain("under operator JSON at "),
},
testName+": erroneous operator expected")
// Lax
checkOK(t, got, td.Lax(td.Bag(float64(5), 4, 1, 4, 3)), testName)
//
// SubBagOf
checkOK(t, got, td.SubBagOf(5, 4, 1, 4, 3), testName)
checkOK(t, got, td.SubBagOf(5, 66, 4, 1, 4, 3), testName)
checkError(t, got, td.SubBagOf(5, 66, 4, 1, 3),
expectedError{
Message: mustBe("comparing %% as a SubBagOf"),
Path: mustBe("DATA"),
Summary: mustBe("Extra item: (4)"),
},
testName)
checkError(t, got, td.SubBagOf(1, td.JSON("{")),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe(`DATA`),
Summary: mustContain("JSON unmarshal error"),
Under: mustContain("under operator JSON at "),
},
testName+": erroneous operator expected")
// Lax
checkOK(t, got, td.Lax(td.SubBagOf(float64(5), 4, 1, 4, 3)), testName)
//
// SuperBagOf
checkOK(t, got, td.SuperBagOf(5, 4, 1, 4, 3), testName)
checkOK(t, got, td.SuperBagOf(5, 4, 3), testName)
checkError(t, got, td.SuperBagOf(5, 66, 4, 1, 3),
expectedError{
Message: mustBe("comparing %% as a SuperBagOf"),
Path: mustBe("DATA"),
Summary: mustBe("Missing item: (66)"),
},
testName)
checkError(t, got, td.SuperBagOf(1, td.JSON("{")),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe(`DATA`),
Summary: mustContain("JSON unmarshal error"),
Under: mustContain("under operator JSON at "),
},
testName+": erroneous operator expected")
// Lax
checkOK(t, got, td.Lax(td.SuperBagOf(float64(5), 4, 1, 4, 3)), testName)
}
checkOK(t, []any{123, "foo", nil, "bar", nil},
td.Bag("foo", "bar", 123, nil, nil))
var nilSlice MySlice
for idx, got := range []any{([]int)(nil), &nilSlice} {
testName := fmt.Sprintf("Test #%d", idx)
checkOK(t, got, td.Bag(), testName)
checkOK(t, got, td.SubBagOf(), testName)
checkOK(t, got, td.SubBagOf(1, 2), testName)
checkOK(t, got, td.SuperBagOf(), testName)
}
for idx, bag := range []td.TestDeep{
td.Bag(123),
td.SubBagOf(123),
td.SuperBagOf(123),
} {
testName := fmt.Sprintf("Test #%d → %s", idx, bag)
checkError(t, 123, bag,
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("slice OR array OR *slice OR *array"),
},
testName)
num := 123
checkError(t, &num, bag,
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("*int"),
Expected: mustBe("slice OR array OR *slice OR *array"),
},
testName)
var list *MySlice
checkError(t, list, bag,
expectedError{
Message: mustBe("nil pointer"),
Path: mustBe("DATA"),
Got: mustBe("nil *slice (*td_test.MySlice type)"),
Expected: mustBe("non-nil *slice OR *array"),
},
testName)
checkError(t, nil, bag,
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("slice OR array OR *slice OR *array"),
},
testName)
}
//
// String
test.EqualStr(t, td.Bag(1).String(), "Bag(1)")
test.EqualStr(t, td.Bag(1, 2).String(), "Bag(1,\n 2)")
test.EqualStr(t, td.SubBagOf(1).String(), "SubBagOf(1)")
test.EqualStr(t, td.SubBagOf(1, 2).String(), "SubBagOf(1,\n 2)")
test.EqualStr(t, td.SuperBagOf(1).String(), "SuperBagOf(1)")
test.EqualStr(t, td.SuperBagOf(1, 2).String(),
"SuperBagOf(1,\n 2)")
}
func TestBagTypeBehind(t *testing.T) {
equalTypes(t, td.Bag(6, 5), ([]int)(nil))
equalTypes(t, td.Bag(6, "foo"), nil)
equalTypes(t, td.SubBagOf(6, 5), ([]int)(nil))
equalTypes(t, td.SubBagOf(6, "foo"), nil)
equalTypes(t, td.SuperBagOf(6, 5), ([]int)(nil))
equalTypes(t, td.SuperBagOf(6, "foo"), nil)
// Always the same non-interface type (even if we encounter several
// interface types)
equalTypes(t,
td.Bag(
td.Empty(),
5,
td.Isa((*error)(nil)), // interface type (in fact pointer to ...)
td.All(6, 7),
td.Isa((*fmt.Stringer)(nil)), // interface type
8),
([]int)(nil))
// Only one interface type
equalTypes(t,
td.Bag(
td.Isa((*error)(nil)),
td.Isa((*error)(nil)),
td.Isa((*error)(nil)),
),
([]*error)(nil))
// Several interface types, cannot be sure
equalTypes(t,
td.Bag(
td.Isa((*error)(nil)),
td.Isa((*fmt.Stringer)(nil)),
),
nil)
}
go-testdeep-1.15.0/td/td_between.go 0000664 0000000 0000000 00000046241 15144170453 0017107 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-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 (
"fmt"
"math"
"reflect"
"time"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/types"
"github.com/maxatome/go-testdeep/internal/util"
)
type boundCmp uint8
const (
boundNone boundCmp = iota
boundIn
boundOut
)
type tdBetween struct {
base
expectedMin reflect.Value
expectedMax reflect.Value
minBound boundCmp
maxBound boundCmp
}
var _ TestDeep = &tdBetween{}
// BoundsKind type qualifies the [Between] bounds.
type BoundsKind uint8
const (
_ BoundsKind = (iota - 1) & 3
BoundsInIn // allows to match between "from" and "to" both included.
BoundsInOut // allows to match between "from" included and "to" excluded.
BoundsOutIn // allows to match between "from" excluded and "to" included.
BoundsOutOut // allows to match between "from" and "to" both excluded.
)
type tdBetweenTime struct {
tdBetween
expectedType reflect.Type
mustConvert bool
}
var _ TestDeep = &tdBetweenTime{}
type tdBetweenCmp struct {
tdBetween
expectedType reflect.Type
cmp func(a, b reflect.Value) int
}
// summary(Between): checks that a number, string or time.Time is
// between two bounds
// input(Between): str,int,float,cplx(todo),struct(time.Time)
// Between operator checks that data is between from and
// to. from and to can be any numeric, string, [time.Time] (or
// assignable) value or implement at least one of the two following
// methods:
//
// func (a T) Less(b T) bool // returns true if a < b
// func (a T) Compare(b T) int // returns -1 if a < b, 1 if a > b, 0 if a == b
//
// from and to must be the same type as the compared value, except
// if BeLax config flag is true. [time.Duration] type is accepted as
// to when from is [time.Time] or convertible. bounds allows to
// specify whether bounds are included or not:
// - [BoundsInIn] (default): between from and to both included
// - [BoundsInOut]: between from included and to excluded
// - [BoundsOutIn]: between from excluded and to included
// - [BoundsOutOut]: between from and to both excluded
//
// If bounds is missing, it defaults to [BoundsInIn].
//
// tc.Cmp(t, 17, td.Between(17, 20)) // succeeds, BoundsInIn by default
// tc.Cmp(t, 17, td.Between(10, 17, BoundsInOut)) // fails
// tc.Cmp(t, 17, td.Between(10, 17, BoundsOutIn)) // succeeds
// tc.Cmp(t, 17, td.Between(17, 20, BoundsOutOut)) // fails
// tc.Cmp(t, // succeeds
// netip.MustParse("127.0.0.1"),
// td.Between(netip.MustParse("127.0.0.0"), netip.MustParse("127.255.255.255")))
//
// TypeBehind method returns the [reflect.Type] of from.
func Between(from, to any, bounds ...BoundsKind) TestDeep {
b := tdBetween{
base: newBase(3),
expectedMin: reflect.ValueOf(from),
expectedMax: reflect.ValueOf(to),
}
const usage = "(NUM|STRING|TIME, NUM|STRING|TIME/DURATION[, BOUNDS_KIND])"
if len(bounds) > 0 {
if len(bounds) > 1 {
b.err = ctxerr.OpTooManyParams("Between", usage)
return &b
}
if bounds[0] == BoundsInIn || bounds[0] == BoundsInOut {
b.minBound = boundIn
} else {
b.minBound = boundOut
}
if bounds[0] == BoundsInIn || bounds[0] == BoundsOutIn {
b.maxBound = boundIn
} else {
b.maxBound = boundOut
}
} else {
b.minBound = boundIn
b.maxBound = boundIn
}
if b.expectedMax.Type() == b.expectedMin.Type() {
return b.initBetween(usage)
}
// Special case for (TIME, DURATION)
ok, convertible := types.IsTypeOrConvertible(b.expectedMin, types.Time)
if ok {
if d, ok := to.(time.Duration); ok {
if convertible {
b.expectedMax = reflect.ValueOf(
b.expectedMin.
Convert(types.Time).
Interface().(time.Time).
Add(d)).
Convert(b.expectedMin.Type())
} else {
b.expectedMax = reflect.ValueOf(from.(time.Time).Add(d))
}
return b.initBetween(usage)
}
b.err = ctxerr.OpBad("Between",
"Between(FROM, TO): when FROM type is %[1]s, TO must have the same type or time.Duration: %[2]s ≠ %[1]s|time.Duration",
b.expectedMin.Type(),
b.expectedMax.Type(),
)
return &b
}
b.err = ctxerr.OpBad("Between",
"Between(FROM, TO): FROM and TO must have the same type: %s ≠ %s",
b.expectedMin.Type(),
b.expectedMax.Type(),
)
return &b
}
func (b *tdBetween) initBetween(usage string) TestDeep {
if !b.expectedMax.IsValid() {
b.expectedMax = b.expectedMin
}
// Is any of:
// (T) Compare(T) int
// or
// (T) Less(T) bool
// available?
if cmp := types.NewOrder(b.expectedMin.Type()); cmp != nil {
if order := cmp(b.expectedMin, b.expectedMax); order > 0 {
b.expectedMin, b.expectedMax = b.expectedMax, b.expectedMin
}
return &tdBetweenCmp{
tdBetween: *b,
expectedType: b.expectedMin.Type(),
cmp: cmp,
}
}
switch b.expectedMin.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if b.expectedMin.Int() > b.expectedMax.Int() {
b.expectedMin, b.expectedMax = b.expectedMax, b.expectedMin
}
return b
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if b.expectedMin.Uint() > b.expectedMax.Uint() {
b.expectedMin, b.expectedMax = b.expectedMax, b.expectedMin
}
return b
case reflect.Float32, reflect.Float64:
if b.expectedMin.Float() > b.expectedMax.Float() {
b.expectedMin, b.expectedMax = b.expectedMax, b.expectedMin
}
return b
case reflect.String:
if b.expectedMin.String() > b.expectedMax.String() {
b.expectedMin, b.expectedMax = b.expectedMax, b.expectedMin
}
return b
case reflect.Struct:
ok, convertible := types.IsTypeOrConvertible(b.expectedMin, types.Time)
if !ok {
break
}
bt := tdBetweenTime{
tdBetween: *b,
expectedType: b.expectedMin.Type(),
mustConvert: convertible,
}
if convertible {
bt.expectedMin = b.expectedMin.Convert(types.Time)
bt.expectedMax = b.expectedMax.Convert(types.Time)
}
if bt.expectedMin.Interface().(time.Time).
After(bt.expectedMax.Interface().(time.Time)) {
bt.expectedMin, bt.expectedMax = bt.expectedMax, bt.expectedMin
}
return &bt
}
b.err = ctxerr.OpBadUsage(b.GetLocation().Func,
usage, b.expectedMin.Interface(), 1, true)
return b
}
func (b *tdBetween) nInt(tolerance reflect.Value) {
if diff := tolerance.Int(); diff != 0 {
expectedBase := b.expectedMin.Int()
max := expectedBase + diff
if max < expectedBase {
max = math.MaxInt64
}
min := expectedBase - diff
if min > expectedBase {
min = math.MinInt64
}
b.expectedMin = reflect.New(tolerance.Type()).Elem()
b.expectedMin.SetInt(min)
b.expectedMax = reflect.New(tolerance.Type()).Elem()
b.expectedMax.SetInt(max)
}
}
func (b *tdBetween) nUint(tolerance reflect.Value) {
if diff := tolerance.Uint(); diff != 0 {
base := b.expectedMin.Uint()
max := base + diff
if max < base {
max = math.MaxUint64
}
min := base - diff
if min > base {
min = 0
}
b.expectedMin = reflect.New(tolerance.Type()).Elem()
b.expectedMin.SetUint(min)
b.expectedMax = reflect.New(tolerance.Type()).Elem()
b.expectedMax.SetUint(max)
}
}
func (b *tdBetween) nFloat(tolerance reflect.Value) {
if diff := tolerance.Float(); diff != 0 {
base := b.expectedMin.Float()
b.expectedMin = reflect.New(tolerance.Type()).Elem()
b.expectedMin.SetFloat(base - diff)
b.expectedMax = reflect.New(tolerance.Type()).Elem()
b.expectedMax.SetFloat(base + diff)
}
}
// summary(N): compares a number with a tolerance value
// input(N): int,float,cplx(todo)
// N operator compares a numeric data against num ± tolerance. If
// tolerance is missing, it defaults to 0. num and tolerance
// must be the same type as the compared value, except if BeLax config
// flag is true.
//
// td.Cmp(t, 12.2, td.N(12., 0.3)) // succeeds
// td.Cmp(t, 12.2, td.N(12., 0.1)) // fails
//
// TypeBehind method returns the [reflect.Type] of num.
func N(num any, tolerance ...any) TestDeep {
n := tdBetween{
base: newBase(3),
expectedMin: reflect.ValueOf(num),
minBound: boundIn,
maxBound: boundIn,
}
const usage = "({,U}INT{,8,16,32,64}|FLOAT{32,64}[, TOLERANCE])"
switch n.expectedMin.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64:
default:
n.err = ctxerr.OpBadUsage("N", usage, num, 1, true)
return &n
}
n.expectedMax = n.expectedMin
if len(tolerance) > 0 {
if len(tolerance) > 1 {
n.err = ctxerr.OpTooManyParams("N", usage)
return &n
}
tol := reflect.ValueOf(tolerance[0])
if tol.Type() != n.expectedMin.Type() {
n.err = ctxerr.OpBad("N",
"N(NUM, TOLERANCE): NUM and TOLERANCE must have the same type: %s ≠ %s",
n.expectedMin.Type(), tol.Type())
return &n
}
switch tol.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
n.nInt(tol)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
reflect.Uint64:
n.nUint(tol)
default: // case reflect.Float32, reflect.Float64:
n.nFloat(tol)
}
}
return &n
}
// summary(Gt): checks that a number, string or time.Time is
// greater than a value
// input(Gt): str,int,float,cplx(todo),struct(time.Time)
// Gt operator checks that data is greater than
// minExpectedValue. minExpectedValue can be any numeric, string,
// [time.Time] (or assignable) value or implements at least one of the
// two following methods:
//
// func (a T) Less(b T) bool // returns true if a < b
// func (a T) Compare(b T) int // returns -1 if a < b, 1 if a > b, 0 if a == b
//
// minExpectedValue must be the same type as the compared value,
// except if BeLax config flag is true.
//
// td.Cmp(t, 17, td.Gt(15))
// before := time.Now()
// td.Cmp(t, time.Now(), td.Gt(before))
//
// TypeBehind method returns the [reflect.Type] of minExpectedValue.
func Gt(minExpectedValue any) TestDeep {
b := &tdBetween{
base: newBase(3),
expectedMin: reflect.ValueOf(minExpectedValue),
minBound: boundOut,
}
return b.initBetween("(NUM|STRING|TIME)")
}
// summary(Gte): checks that a number, string or time.Time is
// greater or equal than a value
// input(Gte): str,int,float,cplx(todo),struct(time.Time)
// Gte operator checks that data is greater or equal than
// minExpectedValue. minExpectedValue can be any numeric, string,
// [time.Time] (or assignable) value or implements at least one of the
// two following methods:
//
// func (a T) Less(b T) bool // returns true if a < b
// func (a T) Compare(b T) int // returns -1 if a < b, 1 if a > b, 0 if a == b
//
// minExpectedValue must be the same type as the compared value,
// except if BeLax config flag is true.
//
// td.Cmp(t, 17, td.Gte(17))
// before := time.Now()
// td.Cmp(t, time.Now(), td.Gte(before))
//
// TypeBehind method returns the [reflect.Type] of minExpectedValue.
func Gte(minExpectedValue any) TestDeep {
b := &tdBetween{
base: newBase(3),
expectedMin: reflect.ValueOf(minExpectedValue),
minBound: boundIn,
}
return b.initBetween("(NUM|STRING|TIME)")
}
// summary(Lt): checks that a number, string or time.Time is
// lesser than a value
// input(Lt): str,int,float,cplx(todo),struct(time.Time)
// Lt operator checks that data is lesser than
// maxExpectedValue. maxExpectedValue can be any numeric, string,
// [time.Time] (or assignable) value or implements at least one of the
// two following methods:
//
// func (a T) Less(b T) bool // returns true if a < b
// func (a T) Compare(b T) int // returns -1 if a < b, 1 if a > b, 0 if a == b
//
// maxExpectedValue must be the same type as the compared value,
// except if BeLax config flag is true.
//
// td.Cmp(t, 17, td.Lt(19))
// before := time.Now()
// td.Cmp(t, before, td.Lt(time.Now()))
//
// TypeBehind method returns the [reflect.Type] of maxExpectedValue.
func Lt(maxExpectedValue any) TestDeep {
b := &tdBetween{
base: newBase(3),
expectedMin: reflect.ValueOf(maxExpectedValue),
maxBound: boundOut,
}
return b.initBetween("(NUM|STRING|TIME)")
}
// summary(Lte): checks that a number, string or time.Time is
// lesser or equal than a value
// input(Lte): str,int,float,cplx(todo),struct(time.Time)
// Lte operator checks that data is lesser or equal than
// maxExpectedValue. maxExpectedValue can be any numeric, string,
// [time.Time] (or assignable) value or implements at least one of the
// two following methods:
//
// func (a T) Less(b T) bool // returns true if a < b
// func (a T) Compare(b T) int // returns -1 if a < b, 1 if a > b, 0 if a == b
//
// maxExpectedValue must be the same type as the compared value,
// except if BeLax config flag is true.
//
// td.Cmp(t, 17, td.Lte(17))
// before := time.Now()
// td.Cmp(t, before, td.Lt(time.Now()))
//
// TypeBehind method returns the [reflect.Type] of maxExpectedValue.
func Lte(maxExpectedValue any) TestDeep {
b := &tdBetween{
base: newBase(3),
expectedMin: reflect.ValueOf(maxExpectedValue),
maxBound: boundIn,
}
return b.initBetween("(NUM|STRING|TIME)")
}
func (b *tdBetween) matchInt(got reflect.Value) (ok bool) {
switch b.minBound {
case boundIn:
ok = got.Int() >= b.expectedMin.Int()
case boundOut:
ok = got.Int() > b.expectedMin.Int()
default:
ok = true
}
if ok {
switch b.maxBound {
case boundIn:
ok = got.Int() <= b.expectedMax.Int()
case boundOut:
ok = got.Int() < b.expectedMax.Int()
default:
ok = true
}
}
return
}
func (b *tdBetween) matchUint(got reflect.Value) (ok bool) {
switch b.minBound {
case boundIn:
ok = got.Uint() >= b.expectedMin.Uint()
case boundOut:
ok = got.Uint() > b.expectedMin.Uint()
default:
ok = true
}
if ok {
switch b.maxBound {
case boundIn:
ok = got.Uint() <= b.expectedMax.Uint()
case boundOut:
ok = got.Uint() < b.expectedMax.Uint()
default:
ok = true
}
}
return
}
func (b *tdBetween) matchFloat(got reflect.Value) (ok bool) {
switch b.minBound {
case boundIn:
ok = got.Float() >= b.expectedMin.Float()
case boundOut:
ok = got.Float() > b.expectedMin.Float()
default:
ok = true
}
if ok {
switch b.maxBound {
case boundIn:
ok = got.Float() <= b.expectedMax.Float()
case boundOut:
ok = got.Float() < b.expectedMax.Float()
default:
ok = true
}
}
return
}
func (b *tdBetween) matchString(got reflect.Value) (ok bool) {
switch b.minBound {
case boundIn:
ok = got.String() >= b.expectedMin.String()
case boundOut:
ok = got.String() > b.expectedMin.String()
default:
ok = true
}
if ok {
switch b.maxBound {
case boundIn:
ok = got.String() <= b.expectedMax.String()
case boundOut:
ok = got.String() < b.expectedMax.String()
default:
ok = true
}
}
return
}
func (b *tdBetween) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if b.err != nil {
return ctx.CollectError(b.err)
}
if got.Type() != b.expectedMin.Type() {
if !ctx.BeLax || !types.IsConvertible(b.expectedMin, got.Type()) {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(ctxerr.TypeMismatch(got.Type(), b.expectedMin.Type()))
}
nb := *b
nb.expectedMin = b.expectedMin.Convert(got.Type())
nb.expectedMax = b.expectedMax.Convert(got.Type())
b = &nb
}
var ok bool
switch got.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
ok = b.matchInt(got)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
ok = b.matchUint(got)
case reflect.Float32, reflect.Float64:
ok = b.matchFloat(got)
case reflect.String:
ok = b.matchString(got)
}
if ok {
return nil
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "values differ",
Got: got,
Expected: types.RawString(b.String()),
})
}
func (b *tdBetween) String() string {
if b.err != nil {
return b.stringError()
}
var (
min, max any
minStr, maxStr string
)
if b.minBound != boundNone {
min = b.expectedMin.Interface()
minStr = util.ToString(min)
}
if b.maxBound != boundNone {
max = b.expectedMax.Interface()
maxStr = util.ToString(max)
}
if min != nil {
if max != nil {
return fmt.Sprintf("%s %c got %c %s",
minStr,
util.TernRune(b.minBound == boundIn, '≤', '<'),
util.TernRune(b.maxBound == boundIn, '≤', '<'),
maxStr)
}
return fmt.Sprintf("%c %s",
util.TernRune(b.minBound == boundIn, '≥', '>'), minStr)
}
return fmt.Sprintf("%c %s",
util.TernRune(b.maxBound == boundIn, '≤', '<'), maxStr)
}
func (b *tdBetween) TypeBehind() reflect.Type {
if b.err != nil {
return nil
}
return b.expectedMin.Type()
}
var _ TestDeep = &tdBetweenTime{}
func (b *tdBetweenTime) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
// b.err != nil is not possible here, as when a *tdBetweenTime is
// built, there is never an error
if got.Type() != b.expectedType {
if !ctx.BeLax || !types.IsConvertible(got, b.expectedType) {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(ctxerr.TypeMismatch(got.Type(), b.expectedType))
}
got = got.Convert(b.expectedType)
}
cmpGot, err := getTime(ctx, got, b.mustConvert)
if err != nil {
return ctx.CollectError(err)
}
var ok bool
if b.minBound != boundNone {
min := b.expectedMin.Interface().(time.Time)
if b.minBound == boundIn {
ok = !min.After(cmpGot)
} else {
ok = cmpGot.After(min)
}
} else {
ok = true
}
if ok && b.maxBound != boundNone {
max := b.expectedMax.Interface().(time.Time)
if b.maxBound == boundIn {
ok = !max.Before(cmpGot)
} else {
ok = cmpGot.Before(max)
}
}
if ok {
return nil
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "values differ",
Got: got,
Expected: types.RawString(b.String()),
})
}
func (b *tdBetweenTime) TypeBehind() reflect.Type {
// b.err != nil is not possible here, as when a *tdBetweenTime is
// built, there is never an error
return b.expectedType
}
var _ TestDeep = &tdBetweenCmp{}
func (b *tdBetweenCmp) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
// b.err != nil is not possible here, as when a *tdBetweenCmp is
// built, there is never an error
if got.Type() != b.expectedType {
if ctx.BeLax && types.IsConvertible(got, b.expectedType) {
got = got.Convert(b.expectedType)
} else {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(ctxerr.TypeMismatch(got.Type(), b.expectedType))
}
}
var ok bool
if b.minBound != boundNone {
order := b.cmp(got, b.expectedMin)
if b.minBound == boundIn {
ok = order >= 0
} else {
ok = order > 0
}
} else {
ok = true
}
if ok && b.maxBound != boundNone {
order := b.cmp(got, b.expectedMax)
if b.maxBound == boundIn {
ok = order <= 0
} else {
ok = order < 0
}
}
if ok {
return nil
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "values differ",
Got: got,
Expected: types.RawString(b.String()),
})
}
func (b *tdBetweenCmp) TypeBehind() reflect.Type {
// b.err != nil is not possible here, as when a *tdBetweenCmp is
// built, there is never an error
return b.expectedType
}
go-testdeep-1.15.0/td/td_between_test.go 0000664 0000000 0000000 00000060014 15144170453 0020140 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-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 (
"fmt"
"math"
"testing"
"time"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestBetween(t *testing.T) {
checkOK(t, 12, td.Between(9, 13))
checkOK(t, 12, td.Between(13, 9))
checkOK(t, 12, td.Between(9, 12, td.BoundsOutIn))
checkOK(t, 12, td.Between(12, 13, td.BoundsInOut))
checkError(t, 10, td.Between(10, 15, td.BoundsOutIn),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("10"),
Expected: mustBe("10 < got ≤ 15"),
})
checkError(t, 10, td.Between(10, 15, td.BoundsOutOut),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("10"),
Expected: mustBe("10 < got < 15"),
})
checkError(t, 15, td.Between(10, 15, td.BoundsInOut),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("15"),
Expected: mustBe("10 ≤ got < 15"),
})
checkError(t, 15, td.Between(10, 15, td.BoundsOutOut),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("15"),
Expected: mustBe("10 < got < 15"),
})
checkError(t, 15, td.Between(uint(10), uint(15), td.BoundsOutOut),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("uint"),
})
checkOK(t, uint16(12), td.Between(uint16(9), uint16(13)))
checkOK(t, uint16(12), td.Between(uint16(13), uint16(9)))
checkOK(t, uint16(12),
td.Between(uint16(9), uint16(12), td.BoundsOutIn))
checkOK(t, uint16(12),
td.Between(uint16(12), uint16(13), td.BoundsInOut))
checkOK(t, 12.1, td.Between(9.5, 13.1))
checkOK(t, 12.1, td.Between(13.1, 9.5))
checkOK(t, 12.1, td.Between(9.5, 12.1, td.BoundsOutIn))
checkOK(t, 12.1, td.Between(12.1, 13.1, td.BoundsInOut))
checkOK(t, "abc", td.Between("aaa", "bbb"))
checkOK(t, "abc", td.Between("bbb", "aaa"))
checkOK(t, "abc", td.Between("aaa", "abc", td.BoundsOutIn))
checkOK(t, "abc", td.Between("abc", "bbb", td.BoundsInOut))
checkOK(t, 12*time.Hour, td.Between(60*time.Second, 24*time.Hour))
//
// Bad usage
checkError(t, "never tested",
td.Between([]byte("test"), []byte("test")),
expectedError{
Message: mustBe("bad usage of Between operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Between(NUM|STRING|TIME, NUM|STRING|TIME/DURATION[, BOUNDS_KIND]), but received []uint8 (slice) as 1st parameter"),
})
checkError(t, "never tested",
td.Between(12, "test"),
expectedError{
Message: mustBe("bad usage of Between operator"),
Path: mustBe("DATA"),
Summary: mustBe("Between(FROM, TO): FROM and TO must have the same type: int ≠ string"),
})
checkError(t, "never tested",
td.Between("test", 12),
expectedError{
Message: mustBe("bad usage of Between operator"),
Path: mustBe("DATA"),
Summary: mustBe("Between(FROM, TO): FROM and TO must have the same type: string ≠ int"),
})
checkError(t, "never tested",
td.Between(1, 2, td.BoundsInIn, td.BoundsInOut),
expectedError{
Message: mustBe("bad usage of Between operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Between(NUM|STRING|TIME, NUM|STRING|TIME/DURATION[, BOUNDS_KIND]), too many parameters"),
})
type notTime struct{}
checkError(t, "never tested",
td.Between(notTime{}, notTime{}),
expectedError{
Message: mustBe("bad usage of Between operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Between(NUM|STRING|TIME, NUM|STRING|TIME/DURATION[, BOUNDS_KIND]), but received td_test.notTime (struct) as 1st parameter"),
})
// Erroneous op
test.EqualStr(t, td.Between("test", 12).String(), "Between()")
}
func TestN(t *testing.T) {
//
// Unsigned
checkOK(t, uint(12), td.N(uint(12)))
checkOK(t, uint(11), td.N(uint(12), uint(1)))
checkOK(t, uint(13), td.N(uint(12), uint(1)))
checkError(t, 10, td.N(uint(12), uint(1)),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("uint"),
})
checkOK(t, uint8(12), td.N(uint8(12)))
checkOK(t, uint8(11), td.N(uint8(12), uint8(1)))
checkOK(t, uint8(13), td.N(uint8(12), uint8(1)))
checkError(t, 10, td.N(uint8(12), uint8(1)),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("uint8"),
})
checkOK(t, uint16(12), td.N(uint16(12)))
checkOK(t, uint16(11), td.N(uint16(12), uint16(1)))
checkOK(t, uint16(13), td.N(uint16(12), uint16(1)))
checkError(t, 10, td.N(uint16(12), uint16(1)),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("uint16"),
})
checkOK(t, uint32(12), td.N(uint32(12)))
checkOK(t, uint32(11), td.N(uint32(12), uint32(1)))
checkOK(t, uint32(13), td.N(uint32(12), uint32(1)))
checkError(t, 10, td.N(uint32(12), uint32(1)),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("uint32"),
})
checkOK(t, uint64(12), td.N(uint64(12)))
checkOK(t, uint64(11), td.N(uint64(12), uint64(1)))
checkOK(t, uint64(13), td.N(uint64(12), uint64(1)))
checkError(t, 10, td.N(uint64(12), uint64(1)),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("uint64"),
})
checkOK(t, uint64(math.MaxUint64),
td.N(uint64(math.MaxUint64), uint64(2)))
checkError(t, uint64(0), td.N(uint64(math.MaxUint64), uint64(2)),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("(uint64) 0"),
Expected: mustBe(fmt.Sprintf("(uint64) %v ≤ got ≤ (uint64) %v",
uint64(math.MaxUint64)-2, uint64(math.MaxUint64))),
})
checkOK(t, uint64(0), td.N(uint64(0), uint64(2)))
checkError(t, uint64(math.MaxUint64), td.N(uint64(0), uint64(2)),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe(fmt.Sprintf("(uint64) %v", uint64(math.MaxUint64))),
Expected: mustBe("(uint64) 0 ≤ got ≤ (uint64) 2"),
})
//
// Signed
checkOK(t, 12, td.N(12))
checkOK(t, 11, td.N(12, 1))
checkOK(t, 13, td.N(12, 1))
checkError(t, 10, td.N(12, 1),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("10"),
Expected: mustBe("11 ≤ got ≤ 13"),
})
checkError(t, 10, td.N(12, 0),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("10"),
Expected: mustBe("12 ≤ got ≤ 12"),
})
checkOK(t, int8(12), td.N(int8(12)))
checkOK(t, int8(11), td.N(int8(12), int8(1)))
checkOK(t, int8(13), td.N(int8(12), int8(1)))
checkError(t, 10, td.N(int8(12), int8(1)),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("int8"),
})
checkOK(t, int16(12), td.N(int16(12)))
checkOK(t, int16(11), td.N(int16(12), int16(1)))
checkOK(t, int16(13), td.N(int16(12), int16(1)))
checkError(t, 10, td.N(int16(12), int16(1)),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("int16"),
})
checkOK(t, int32(12), td.N(int32(12)))
checkOK(t, int32(11), td.N(int32(12), int32(1)))
checkOK(t, int32(13), td.N(int32(12), int32(1)))
checkError(t, 10, td.N(int32(12), int32(1)),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("int32"),
})
checkOK(t, int64(12), td.N(int64(12)))
checkOK(t, int64(11), td.N(int64(12), int64(1)))
checkOK(t, int64(13), td.N(int64(12), int64(1)))
checkError(t, 10, td.N(int64(12), int64(1)),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("int64"),
})
checkOK(t, int64(math.MaxInt64), td.N(int64(math.MaxInt64), int64(2)))
checkError(t, int64(0), td.N(int64(math.MaxInt64), int64(2)),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("(int64) 0"),
Expected: mustBe(fmt.Sprintf("(int64) %v ≤ got ≤ (int64) %v",
int64(math.MaxInt64)-2, int64(math.MaxInt64))),
})
checkOK(t, int64(math.MinInt64), td.N(int64(math.MinInt64), int64(2)))
checkError(t, int64(0), td.N(int64(math.MinInt64), int64(2)),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("(int64) 0"),
Expected: mustBe(fmt.Sprintf("(int64) %v ≤ got ≤ (int64) %v",
int64(math.MinInt64), int64(math.MinInt64)+2)),
})
//
// Float
checkOK(t, 12.1, td.N(12.1))
checkOK(t, 11.9, td.N(12.0, 0.1))
checkOK(t, 12.1, td.N(12.0, 0.1))
checkError(t, 11.8, td.N(12.0, 0.1),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("11.8"),
Expected: mustBe("11.9 ≤ got ≤ 12.1"),
})
checkOK(t, float32(12.1), td.N(float32(12.1)))
checkOK(t, float32(11.9), td.N(float32(12), float32(0.1)))
checkOK(t, float32(12.1), td.N(float32(12), float32(0.1)))
checkError(t, 11.8, td.N(float32(12), float32(0.1)),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("float64"),
Expected: mustBe("float32"),
})
floatTol := 10e304
checkOK(t, float64(math.MaxFloat64),
td.N(float64(math.MaxFloat64), floatTol))
checkError(t, float64(0), td.N(float64(math.MaxFloat64), floatTol),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("0.0"),
Expected: mustBe(fmt.Sprintf("%v ≤ got ≤ +Inf",
float64(math.MaxFloat64)-floatTol)),
})
checkOK(t, -float64(math.MaxFloat64),
td.N(-float64(math.MaxFloat64), float64(2)))
checkError(t, float64(0), td.N(-float64(math.MaxFloat64), floatTol),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("0.0"),
Expected: mustBe(fmt.Sprintf("-Inf ≤ got ≤ %v",
-float64(math.MaxFloat64)+floatTol)),
})
//
// Bad usage
checkError(t, "never tested",
td.N("test"),
expectedError{
Message: mustBe("bad usage of N operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: N({,U}INT{,8,16,32,64}|FLOAT{32,64}[, TOLERANCE]), but received string as 1st parameter"),
})
checkError(t, "never tested",
td.N(10, 1, 2),
expectedError{
Message: mustBe("bad usage of N operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: N({,U}INT{,8,16,32,64}|FLOAT{32,64}[, TOLERANCE]), too many parameters"),
})
checkError(t, "never tested",
td.N(10, "test"),
expectedError{
Message: mustBe("bad usage of N operator"),
Path: mustBe("DATA"),
Summary: mustBe("N(NUM, TOLERANCE): NUM and TOLERANCE must have the same type: int ≠ string"),
})
// Erroneous op
test.EqualStr(t, td.N(10, 1, 2).String(), "N()")
}
func TestLGt(t *testing.T) {
type MyTime time.Time
checkOK(t, 12, td.Gt(11))
checkOK(t, 12, td.Gte(12))
checkOK(t, 12, td.Lt(13))
checkOK(t, 12, td.Lte(12))
checkOK(t, uint16(12), td.Gt(uint16(11)))
checkOK(t, uint16(12), td.Gte(uint16(12)))
checkOK(t, uint16(12), td.Lt(uint16(13)))
checkOK(t, uint16(12), td.Lte(uint16(12)))
checkOK(t, 12.3, td.Gt(12.2))
checkOK(t, 12.3, td.Gte(12.3))
checkOK(t, 12.3, td.Lt(12.4))
checkOK(t, 12.3, td.Lte(12.3))
checkOK(t, "abc", td.Gt("abb"))
checkOK(t, "abc", td.Gte("abc"))
checkOK(t, "abc", td.Lt("abd"))
checkOK(t, "abc", td.Lte("abc"))
checkError(t, 12, td.Gt(12),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("12"),
Expected: mustBe("> 12"),
})
checkError(t, 12, td.Lt(12),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("12"),
Expected: mustBe("< 12"),
})
checkError(t, 12, td.Gte(13),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("12"),
Expected: mustBe("≥ 13"),
})
checkError(t, 12, td.Lte(11),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("12"),
Expected: mustBe("≤ 11"),
})
checkError(t, "abc", td.Gt("abc"),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe(`"abc"`),
Expected: mustBe(`> "abc"`),
})
checkError(t, "abc", td.Lt("abc"),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe(`"abc"`),
Expected: mustBe(`< "abc"`),
})
checkError(t, "abc", td.Gte("abd"),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe(`"abc"`),
Expected: mustBe(`≥ "abd"`),
})
checkError(t, "abc", td.Lte("abb"),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe(`"abc"`),
Expected: mustBe(`≤ "abb"`),
})
gotDate := time.Date(2018, time.March, 4, 1, 2, 3, 0, time.UTC)
expectedDate := gotDate
checkOK(t, gotDate, td.Gte(expectedDate))
checkOK(t, gotDate, td.Lte(expectedDate))
checkOK(t, gotDate, td.Lax(td.Gte(MyTime(expectedDate))))
checkOK(t, gotDate, td.Lax(td.Lte(MyTime(expectedDate))))
checkError(t, gotDate, td.Gt(expectedDate),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("(time.Time) 2018-03-04 01:02:03 +0000 UTC"),
Expected: mustBe("> (time.Time) 2018-03-04 01:02:03 +0000 UTC"),
})
checkError(t, gotDate, td.Lt(expectedDate),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("(time.Time) 2018-03-04 01:02:03 +0000 UTC"),
Expected: mustBe("< (time.Time) 2018-03-04 01:02:03 +0000 UTC"),
})
//
// Bad usage
checkError(t, "never tested",
td.Gt([]byte("test")),
expectedError{
Message: mustBe("bad usage of Gt operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Gt(NUM|STRING|TIME), but received []uint8 (slice) as 1st parameter"),
})
checkError(t, "never tested",
td.Gte([]byte("test")),
expectedError{
Message: mustBe("bad usage of Gte operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Gte(NUM|STRING|TIME), but received []uint8 (slice) as 1st parameter"),
})
checkError(t, "never tested",
td.Lt([]byte("test")),
expectedError{
Message: mustBe("bad usage of Lt operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Lt(NUM|STRING|TIME), but received []uint8 (slice) as 1st parameter"),
})
checkError(t, "never tested",
td.Lte([]byte("test")),
expectedError{
Message: mustBe("bad usage of Lte operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Lte(NUM|STRING|TIME), but received []uint8 (slice) as 1st parameter"),
})
// Erroneous op
test.EqualStr(t, td.Gt([]byte("test")).String(), "Gt()")
test.EqualStr(t, td.Gte([]byte("test")).String(), "Gte()")
test.EqualStr(t, td.Lt([]byte("test")).String(), "Lt()")
test.EqualStr(t, td.Lte([]byte("test")).String(), "Lte()")
}
func TestBetweenTime(t *testing.T) {
type MyTime time.Time
now := time.Now()
checkOK(t, now, td.Between(now, now))
checkOK(t, now, td.Between(now.Add(-time.Second), now.Add(time.Second)))
checkOK(t, now, td.Between(now.Add(time.Second), now.Add(-time.Second)))
// (TIME, DURATION)
checkOK(t, now, td.Between(now.Add(-time.Second), 2*time.Second))
checkOK(t, now, td.Between(now.Add(time.Second), -2*time.Second))
checkOK(t, MyTime(now),
td.Between(
MyTime(now.Add(-time.Second)),
MyTime(now.Add(time.Second))))
// (TIME, DURATION)
checkOK(t, MyTime(now),
td.Between(
MyTime(now.Add(-time.Second)),
2*time.Second))
checkOK(t, MyTime(now),
td.Between(
MyTime(now.Add(time.Second)),
-2*time.Second))
// Lax mode
checkOK(t, MyTime(now),
td.Lax(td.Between(
now.Add(time.Second),
now.Add(-time.Second))))
checkOK(t, now,
td.Lax(td.Between(
MyTime(now.Add(time.Second)),
MyTime(now.Add(-time.Second)))))
checkOK(t, MyTime(now),
td.Lax(td.Between(
now.Add(-time.Second),
2*time.Second)))
checkOK(t, now,
td.Lax(td.Between(
MyTime(now.Add(-time.Second)),
2*time.Second)))
checkOK(t, now,
td.Lax(td.Between(
MyTime(now.Add(-time.Second)),
2*time.Second,
td.BoundsOutOut)))
date := time.Date(2018, time.March, 4, 0, 0, 0, 0, time.UTC)
checkError(t, date,
td.Between(date.Add(-2*time.Second), date.Add(-time.Second)),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("(time.Time) 2018-03-04 00:00:00 +0000 UTC"),
Expected: mustBe("(time.Time) 2018-03-03 23:59:58 +0000 UTC" +
" ≤ got ≤ " +
"(time.Time) 2018-03-03 23:59:59 +0000 UTC"),
})
checkError(t, MyTime(date),
td.Between(MyTime(date.Add(-2*time.Second)), MyTime(date.Add(-time.Second))),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustContain("(td_test.MyTime) "),
Expected: mustBe(
"(time.Time) 2018-03-03 23:59:58 +0000 UTC" +
" ≤ got ≤ " +
"(time.Time) 2018-03-03 23:59:59 +0000 UTC"),
})
checkError(t, date,
td.Between(date.Add(-2*time.Second), date, td.BoundsInOut),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("(time.Time) 2018-03-04 00:00:00 +0000 UTC"),
Expected: mustBe("(time.Time) 2018-03-03 23:59:58 +0000 UTC" +
" ≤ got < " +
"(time.Time) 2018-03-04 00:00:00 +0000 UTC"),
})
checkError(t, date,
td.Between(date, date.Add(2*time.Second), td.BoundsOutIn),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("(time.Time) 2018-03-04 00:00:00 +0000 UTC"),
Expected: mustBe("(time.Time) 2018-03-04 00:00:00 +0000 UTC" +
" < got ≤ " +
"(time.Time) 2018-03-04 00:00:02 +0000 UTC"),
})
checkError(t, "string",
td.Between(date, date.Add(2*time.Second), td.BoundsOutIn),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("string"),
Expected: mustBe("time.Time"),
})
checkError(t, "string",
td.Between(MyTime(date), MyTime(date.Add(2*time.Second)), td.BoundsOutIn),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("string"),
Expected: mustBe("td_test.MyTime"),
})
checkError(t, "never tested",
td.Between(date, 12), // (Time, Time) or (Time, Duration)
expectedError{
Message: mustBe("bad usage of Between operator"),
Path: mustBe("DATA"),
Summary: mustBe("Between(FROM, TO): when FROM type is time.Time, TO must have the same type or time.Duration: int ≠ time.Time|time.Duration"),
})
checkError(t, "never tested",
td.Between(MyTime(date), 12), // (MyTime, MyTime) or (MyTime, Duration)
expectedError{
Message: mustBe("bad usage of Between operator"),
Path: mustBe("DATA"),
Summary: mustBe("Between(FROM, TO): when FROM type is td_test.MyTime, TO must have the same type or time.Duration: int ≠ td_test.MyTime|time.Duration"),
})
checkOK(t, now, td.Gt(now.Add(-time.Second)))
checkOK(t, now, td.Lt(now.Add(time.Second)))
}
type compareType int
func (i compareType) Compare(j compareType) int {
if i < j {
return -1
}
if i > j {
return 1
}
return 0
}
type lessType int
func (i lessType) Less(j lessType) bool {
return i < j
}
func TestBetweenCmp(t *testing.T) {
t.Run("compareType", func(t *testing.T) {
checkOK(t, compareType(5), td.Between(compareType(4), compareType(6)))
checkOK(t, compareType(5), td.Between(compareType(6), compareType(4)))
checkOK(t, compareType(5), td.Between(compareType(5), compareType(6)))
checkOK(t, compareType(5), td.Between(compareType(4), compareType(5)))
checkOK(t, compareType(5),
td.Between(compareType(4), compareType(6), td.BoundsOutOut))
checkError(t, compareType(5),
td.Between(compareType(5), compareType(6), td.BoundsOutIn),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("(td_test.compareType) 5"),
Expected: mustBe("(td_test.compareType) 5 < got ≤ (td_test.compareType) 6"),
})
checkError(t, compareType(5),
td.Between(compareType(4), compareType(5), td.BoundsInOut),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("(td_test.compareType) 5"),
Expected: mustBe("(td_test.compareType) 4 ≤ got < (td_test.compareType) 5"),
})
// Other between forms
checkOK(t, compareType(5), td.Gt(compareType(4)))
checkOK(t, compareType(5), td.Gte(compareType(5)))
checkOK(t, compareType(5), td.Lt(compareType(6)))
checkOK(t, compareType(5), td.Lte(compareType(5)))
// BeLax or not BeLax
for i, op := range []td.TestDeep{
td.Between(compareType(4), compareType(6)),
td.Gt(compareType(4)),
td.Gte(compareType(5)),
td.Lt(compareType(6)),
td.Lte(compareType(5)),
} {
// Type mismatch if BeLax not enabled
checkError(t, 5, op,
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("td_test.compareType"),
},
"Op #%d", i)
// BeLax enabled is OK
checkOK(t, 5, td.Lax(op), "Op #%d", i)
}
// In a private field
type private struct {
num compareType
}
checkOK(t, private{num: 5},
td.Struct(private{},
td.StructFields{
"num": td.Between(compareType(4), compareType(6)),
}))
})
t.Run("lessType", func(t *testing.T) {
checkOK(t, lessType(5), td.Between(lessType(4), lessType(6)))
checkOK(t, lessType(5), td.Between(lessType(6), lessType(4)))
checkOK(t, lessType(5), td.Between(lessType(5), lessType(6)))
checkOK(t, lessType(5), td.Between(lessType(4), lessType(5)))
checkOK(t, lessType(5),
td.Between(lessType(4), lessType(6), td.BoundsOutOut))
checkError(t, lessType(5),
td.Between(lessType(5), lessType(6), td.BoundsOutIn),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("(td_test.lessType) 5"),
Expected: mustBe("(td_test.lessType) 5 < got ≤ (td_test.lessType) 6"),
})
checkError(t, lessType(5),
td.Between(lessType(4), lessType(5), td.BoundsInOut),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("(td_test.lessType) 5"),
Expected: mustBe("(td_test.lessType) 4 ≤ got < (td_test.lessType) 5"),
})
// Other between forms
checkOK(t, lessType(5), td.Gt(lessType(4)))
checkOK(t, lessType(5), td.Gte(lessType(5)))
checkOK(t, lessType(5), td.Lt(lessType(6)))
checkOK(t, lessType(5), td.Lte(lessType(5)))
// BeLax or not BeLax
for i, op := range []td.TestDeep{
td.Between(lessType(4), lessType(6)),
td.Gt(lessType(4)),
td.Gte(lessType(5)),
td.Lt(lessType(6)),
td.Lte(lessType(5)),
} {
// Type mismatch if BeLax not enabled
checkError(t, 5, op,
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("td_test.lessType"),
},
"Op #%d", i)
// BeLax enabled is OK
checkOK(t, 5, td.Lax(op), "Op #%d", i)
}
// In a private field
type private struct {
num lessType
}
checkOK(t, private{num: 5},
td.Struct(private{},
td.StructFields{
"num": td.Between(lessType(4), lessType(6)),
}))
})
}
func TestBetweenTypeBehind(t *testing.T) {
type MyTime time.Time
for _, typ := range []any{
10,
int64(23),
int32(23),
time.Time{},
MyTime{},
compareType(0),
lessType(0),
} {
equalTypes(t, td.Between(typ, typ), typ)
equalTypes(t, td.Gt(typ), typ)
equalTypes(t, td.Gte(typ), typ)
equalTypes(t, td.Lt(typ), typ)
equalTypes(t, td.Lte(typ), typ)
}
equalTypes(t, td.N(int64(23), int64(5)), int64(0))
// Erroneous op
equalTypes(t, td.Between("test", 12), nil)
equalTypes(t, td.N(10, 1, 2), nil)
equalTypes(t, td.Gt([]byte("test")), nil)
equalTypes(t, td.Gte([]byte("test")), nil)
equalTypes(t, td.Lt([]byte("test")), nil)
equalTypes(t, td.Lte([]byte("test")), nil)
}
go-testdeep-1.15.0/td/td_catch.go 0000664 0000000 0000000 00000007171 15144170453 0016537 0 ustar 00root root 0000000 0000000 // 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 td
import (
"reflect"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/types"
"github.com/maxatome/go-testdeep/internal/util"
)
type tdCatch struct {
tdSmugglerBase
target reflect.Value
}
var _ TestDeep = &tdCatch{}
// summary(Catch): catches data on the fly before comparing it
// input(Catch): all
// Catch is a smuggler operator. It allows to copy data in target on
// the fly before comparing it as usual against expectedValue.
//
// target must be a non-nil pointer and data should be assignable to
// its pointed type. If BeLax config flag is true or called under [Lax]
// (and so [JSON]) operator, data should be convertible to its pointer
// type.
//
// var id int64
// if td.Cmp(t, CreateRecord("test"),
// td.JSON(`{"id": $1, "name": "test"}`, td.Catch(&id, td.NotZero()))) {
// t.Logf("Created record ID is %d", id)
// }
//
// It is really useful when used with [JSON] operator and/or [tdhttp] helper.
//
// var id int64
// ta := tdhttp.NewTestAPI(t, api.Handler).
// PostJSON("/item", `{"name":"foo"}`).
// CmpStatus(http.StatusCreated).
// CmpJSONBody(td.JSON(`{"id": $1, "name": "foo"}`, td.Catch(&id, td.Gt(0))))
// if !ta.Failed() {
// t.Logf("Created record ID is %d", id)
// }
//
// If you need to only catch data without comparing it, use [Ignore]
// operator as expectedValue as in:
//
// var id int64
// if td.Cmp(t, CreateRecord("test"),
// td.JSON(`{"id": $1, "name": "test"}`, td.Catch(&id, td.Ignore()))) {
// t.Logf("Created record ID is %d", id)
// }
//
// TypeBehind method returns the [reflect.Type] of expectedValue,
// except if expectedValue is a [TestDeep] operator. In this case, it
// delegates TypeBehind() to the operator, but if nil is returned by
// this call, the dereferenced [reflect.Type] of target is returned.
//
// [tdhttp]: https://pkg.go.dev/github.com/maxatome/go-testdeep/helpers/tdhttp
func Catch(target, expectedValue any) TestDeep {
vt := reflect.ValueOf(target)
c := tdCatch{
tdSmugglerBase: newSmugglerBase(expectedValue),
target: vt,
}
if vt.Kind() != reflect.Ptr || vt.IsNil() || !vt.Elem().CanSet() {
c.err = ctxerr.OpBadUsage("Catch", "(NON_NIL_PTR, EXPECTED_VALUE)", target, 1, true)
return &c
}
if !c.isTestDeeper {
c.expectedValue = reflect.ValueOf(expectedValue)
}
return &c
}
func (c *tdCatch) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if c.err != nil {
return ctx.CollectError(c.err)
}
if targetType := c.target.Elem().Type(); !got.Type().AssignableTo(targetType) {
if !ctx.BeLax || !types.IsConvertible(got, targetType) {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(ctxerr.TypeMismatch(got.Type(), c.target.Elem().Type()))
}
c.target.Elem().Set(got.Convert(targetType))
} else {
c.target.Elem().Set(got)
}
return deepValueEqual(ctx, got, c.expectedValue)
}
func (c *tdCatch) String() string {
if c.err != nil {
return c.stringError()
}
if c.isTestDeeper {
return c.expectedValue.Interface().(TestDeep).String()
}
return util.ToString(c.expectedValue)
}
func (c *tdCatch) TypeBehind() reflect.Type {
if c.err != nil {
return nil
}
if c.isTestDeeper {
if typ := c.expectedValue.Interface().(TestDeep).TypeBehind(); typ != nil {
return typ
}
// Operator unknown type behind, fallback on target dereferenced type
return c.target.Type().Elem()
}
if c.expectedValue.IsValid() {
return c.expectedValue.Type()
}
return nil
}
go-testdeep-1.15.0/td/td_catch_test.go 0000664 0000000 0000000 00000004356 15144170453 0017600 0 ustar 00root root 0000000 0000000 // 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 td_test
import (
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestCatch(t *testing.T) {
var num int
checkOK(t, 12, td.Catch(&num, 12))
test.EqualInt(t, num, 12)
var num64 int64
checkError(t, 12, td.Catch(&num64, 12),
expectedError{
Message: mustBe("type mismatch"),
Got: mustBe("int"),
Expected: mustBe("int64"),
})
checkOK(t, 12, td.Lax(td.Catch(&num64, 12)))
test.EqualInt(t, int(num64), 12)
// Lax not needed for interfaces
var val any
if checkOK(t, 12, td.Catch(&val, 12)) {
if n, ok := val.(int); ok {
test.EqualInt(t, n, 12)
} else {
t.Errorf("val is not an int but a %T", val)
}
}
//
// Bad usages
checkError(t, "never tested",
td.Catch(12, 28),
expectedError{
Message: mustBe("bad usage of Catch operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Catch(NON_NIL_PTR, EXPECTED_VALUE), but received int as 1st parameter"),
})
checkError(t, "never tested",
td.Catch(nil, 28),
expectedError{
Message: mustBe("bad usage of Catch operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Catch(NON_NIL_PTR, EXPECTED_VALUE), but received nil as 1st parameter"),
})
checkError(t, "never tested",
td.Catch((*int)(nil), 28),
expectedError{
Message: mustBe("bad usage of Catch operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Catch(NON_NIL_PTR, EXPECTED_VALUE), but received *int (ptr) as 1st parameter"),
})
//
// String
test.EqualStr(t, td.Catch(&num, 12).String(), "12")
test.EqualStr(t,
td.Catch(&num, td.Gt(4)).String(),
td.Gt(4).String())
test.EqualStr(t, td.Catch(&num, nil).String(), "nil")
// Erroneous op
test.EqualStr(t, td.Catch(nil, 28).String(), "Catch()")
}
func TestCatchTypeBehind(t *testing.T) {
var num int
equalTypes(t, td.Catch(&num, 8), 0)
equalTypes(t, td.Catch(&num, td.Gt(4)), 0)
equalTypes(t, td.Catch(&num, td.Ignore()), 0) // fallback on *target
equalTypes(t, td.Catch(&num, nil), nil)
// Erroneous op
equalTypes(t, td.Catch(nil, 28), nil)
}
go-testdeep-1.15.0/td/td_code.go 0000664 0000000 0000000 00000021353 15144170453 0016365 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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"
)
type tdCode struct {
base
function reflect.Value
argType reflect.Type
tParams int
}
var _ TestDeep = &tdCode{}
// summary(Code): checks using a custom function
// input(Code): all
// Code operator allows to check data using a custom function. So
// fn is a function that must take one parameter whose type must be
// the same as the type of the compared value.
//
// fn can return a single bool kind value, telling that yes or no
// the custom test is successful:
//
// td.Cmp(t, gotTime,
// td.Code(func(date time.Time) bool {
// return date.Year() == 2018
// }))
//
// or two values (bool, string) kinds. The bool value has the same
// meaning as above, and the string value is used to describe the
// test when it fails:
//
// td.Cmp(t, gotTime,
// td.Code(func(date time.Time) (bool, string) {
// if date.Year() == 2018 {
// return true, ""
// }
// return false, "year must be 2018"
// }))
//
// or a single error value. If the returned error is nil, the test
// succeeded, else the error contains the reason of failure:
//
// td.Cmp(t, gotJsonRawMesg,
// td.Code(func(b json.RawMessage) error {
// var c map[string]int
// err := json.Unmarshal(b, &c)
// if err != nil {
// return err
// }
// if c["test"] != 42 {
// return fmt.Errorf(`key "test" does not match 42`)
// }
// return nil
// }))
//
// This operator allows to handle any specific comparison not handled
// by standard operators.
//
// It is not recommended to call [Cmp] (or any other Cmp*
// functions or [*T] methods) inside the body of fn, because of
// confusion produced by output in case of failure. When the data
// needs to be transformed before being compared again, [Smuggle]
// operator should be used instead.
//
// But in some cases it can be better to handle yourself the
// comparison than to chain [TestDeep] operators. In this case, fn can
// be a function receiving one or two [*T] as first parameters and
// returning no values.
//
// When fn expects one [*T] parameter, it is directly derived from the
// [testing.TB] instance passed originally to [Cmp] (or its derivatives)
// using [NewT]:
//
// td.Cmp(t, httpRequest, td.Code(func(t *td.T, r *http.Request) {
// token, err := DecodeToken(r.Header.Get("X-Token-1"))
// if t.CmpNoError(err) {
// t.True(token.OK())
// }
// }))
//
// When fn expects two [*T] parameters, they are directly derived from
// the [testing.TB] instance passed originally to [Cmp] (or its derivatives)
// using [AssertRequire]:
//
// td.Cmp(t, httpRequest, td.Code(func(assert, require *td.T, r *http.Request) {
// token, err := DecodeToken(r.Header.Get("X-Token-1"))
// require.CmpNoError(err)
// assert.True(token.OK())
// }))
//
// Note that these forms do not work when there is no initial
// [testing.TB] instance, like when using [EqDeeplyError] or
// [EqDeeply] functions, or when the Code operator is called behind
// the following operators, as they just check if a match occurs
// without raising an error: [Any], [Bag], [Contains], [ContainsKey],
// [None], [Not], [NotAny], [Set], [SubBagOf], [SubSetOf],
// [SuperBagOf] and [SuperSetOf].
//
// RootName is inherited but not the current path, but it can be
// recovered if needed:
//
// got := map[string]int{"foo": 123}
// td.NewT(t).
// RootName("PIPO").
// Cmp(got, td.Map(map[string]int{}, td.MapEntries{
// "foo": td.Code(func(t *td.T, n int) {
// t.Cmp(n, 124) // inherit only RootName
// t.RootName(t.Config.OriginalPath()).Cmp(n, 125) // recover current path
// t.RootName("").Cmp(n, 126) // undo RootName inheritance
// }),
// }))
//
// produces the following errors:
//
// --- FAIL: TestCodeCustom (0.00s)
// td_code_test.go:339: Failed test
// PIPO: values differ ← inherit only RootName
// got: 123
// expected: 124
// td_code_test.go:338: Failed test
// PIPO["foo"]: values differ ← recover current path
// got: 123
// expected: 125
// td_code_test.go:342: Failed test
// DATA: values differ ← undo RootName inheritance
// got: 123
// expected: 126
//
// TypeBehind method returns the [reflect.Type] of last parameter of fn.
func Code(fn any) TestDeep {
vfn := reflect.ValueOf(fn)
c := tdCode{
base: newBase(3),
function: vfn,
}
if vfn.Kind() != reflect.Func {
c.err = ctxerr.OpBadUsage("Code", "(FUNC)", fn, 1, true)
return &c
}
if vfn.IsNil() {
c.err = ctxerr.OpBad("Code", "Code(FUNC): FUNC cannot be a nil function")
return &c
}
fnType := vfn.Type()
in := fnType.NumIn()
// We accept only:
// func (arg) bool
// func (arg) error
// func (arg) (bool, error)
// func (*td.T, arg) // with arg ≠ *td.T, as it is certainly an error
// func (assert, require *td.T, arg)
if fnType.IsVariadic() || in == 0 || in > 3 ||
(in > 1 && (fnType.In(0) != tType)) ||
(in >= 2 && (in == 2) == (fnType.In(1) == tType)) {
c.err = ctxerr.OpBad("Code",
"Code(FUNC): FUNC must take only one non-variadic argument or (*td.T, arg) or (*td.T, *td.T, arg)")
return &c
}
// func (arg) bool
// func (arg) error
// func (arg) (bool, error)
if in == 1 {
switch fnType.NumOut() {
case 2: // (bool, *string*)
if fnType.Out(1).Kind() != reflect.String {
break
}
fallthrough
case 1:
// (*bool*) or (*bool*, string)
if fnType.Out(0).Kind() == reflect.Bool ||
// (*error*)
(fnType.NumOut() == 1 && fnType.Out(0) == types.Error) {
c.argType = fnType.In(0)
return &c
}
}
c.err = ctxerr.OpBad("Code",
"Code(FUNC): FUNC must return bool or (bool, string) or error")
return &c
}
// in == 2 || in == 3
// func (*td.T, arg) (with arg ≠ *td.T)
// func (assert, require *td.T, arg)
if fnType.NumOut() != 0 {
c.err = ctxerr.OpBad("Code", "Code(FUNC): FUNC must return nothing")
return &c
}
c.tParams = in - 1
c.argType = fnType.In(c.tParams)
return &c
}
func (c *tdCode) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if c.err != nil {
return ctx.CollectError(c.err)
}
if !got.Type().AssignableTo(c.argType) {
if !ctx.BeLax || !types.IsConvertible(got, c.argType) {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "incompatible parameter type",
Got: types.RawString(got.Type().String()),
Expected: types.RawString(c.argType.String()),
})
}
got = got.Convert(c.argType)
}
// Refuse to override unexported fields access in this case. It is a
// choice, as we think it is better to use Code() on surrounding
// struct instead.
if !got.CanInterface() {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "cannot compare unexported field",
Summary: ctxerr.NewSummary("use Code() on surrounding struct instead"),
})
}
if c.tParams == 0 {
ret := c.function.Call([]reflect.Value{got})
if ret[0].Kind() == reflect.Bool {
if ret[0].Bool() {
return nil
}
} else if ret[0].IsNil() { // reflect.Interface
return nil
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
var reason string
if len(ret) > 1 { // (bool, string)
reason = ret[1].String()
} else if ret[0].Kind() == reflect.Interface { // (error)
// For internal use only
if cErr, ok := ret[0].Interface().(*ctxerr.Error); ok {
return ctx.CollectError(cErr)
}
reason = ret[0].Interface().(error).Error()
}
// else (bool) so no reason to report
return ctx.CollectError(&ctxerr.Error{
Message: "ran code with %% as argument",
Summary: ctxerr.NewSummaryReason(got, reason),
})
}
if ctx.OriginalTB == nil {
return ctx.CollectError(&ctxerr.Error{
Message: "cannot build *td.T instance",
Summary: ctxerr.NewSummary("original testing.TB instance is missing"),
})
}
t := NewT(ctx.OriginalTB)
t.Config.forkedFromCtx = &ctx
// func(*td.T, arg)
if c.tParams == 1 {
c.function.Call([]reflect.Value{
reflect.ValueOf(t),
got,
})
return nil
}
// func(assert, require *td.T, arg)
assert, require := AssertRequire(t)
c.function.Call([]reflect.Value{
reflect.ValueOf(assert),
reflect.ValueOf(require),
got,
})
return nil
}
func (c *tdCode) String() string {
if c.err != nil {
return c.stringError()
}
return "Code(" + c.function.Type().String() + ")"
}
func (c *tdCode) TypeBehind() reflect.Type {
if c.err != nil {
return nil
}
return c.argType
}
go-testdeep-1.15.0/td/td_code_test.go 0000664 0000000 0000000 00000034506 15144170453 0017430 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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"
"errors"
"fmt"
"strings"
"testing"
"time"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestCode(t *testing.T) {
checkOK(t, 12, td.Code(func(n int) bool { return n >= 10 && n < 20 }))
checkOK(t, 12, td.Code(func(val any) bool {
num, ok := val.(int)
return ok && num == 12
}))
checkOK(t, errors.New("foobar"), td.Code(func(val error) bool {
return val.Error() == "foobar"
}))
checkOK(t, json.RawMessage(`[42]`),
td.Code(func(b json.RawMessage) error {
var l []int
err := json.Unmarshal(b, &l)
if err != nil {
return err
}
if len(l) != 1 || l[0] != 42 {
return errors.New("42 not found")
}
return nil
}))
// Lax
checkOK(t, 123, td.Lax(td.Code(func(n float64) bool { return n == 123 })))
checkError(t, 123, td.Code(func(n float64) bool { return true }),
expectedError{
Message: mustBe("incompatible parameter type"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("float64"),
})
type xInt int
checkError(t, xInt(12),
td.Code(func(n int) bool { return n >= 10 && n < 20 }),
expectedError{
Message: mustBe("incompatible parameter type"),
Path: mustBe("DATA"),
Got: mustBe("td_test.xInt"),
Expected: mustBe("int"),
})
checkError(t, 12,
td.Code(func(n int) (bool, string) { return false, "custom error" }),
expectedError{
Message: mustBe("ran code with %% as argument"),
Path: mustBe("DATA"),
Summary: mustBe(" value: 12\nit failed coz: custom error"),
})
checkError(t, 12,
td.Code(func(n int) bool { return false }),
expectedError{
Message: mustBe("ran code with %% as argument"),
Path: mustBe("DATA"),
Summary: mustBe(" value: 12\nit failed but didn't say why"),
})
type MyBool bool
type MyString string
checkError(t, 12,
td.Code(func(n int) (MyBool, MyString) { return false, "very custom error" }),
expectedError{
Message: mustBe("ran code with %% as argument"),
Path: mustBe("DATA"),
Summary: mustBe(" value: 12\nit failed coz: very custom error"),
})
checkError(t, 12,
td.Code(func(i int) error {
return errors.New("very custom error")
}),
expectedError{
Message: mustBe("ran code with %% as argument"),
Path: mustBe("DATA"),
Summary: mustBe(" value: 12\nit failed coz: very custom error"),
})
// Internal use
checkError(t, 12,
td.Code(func(i int) error {
return &ctxerr.Error{
Message: "my message",
Summary: ctxerr.NewSummary("my summary"),
}
}),
expectedError{
Message: mustBe("my message"),
Path: mustBe("DATA"),
Summary: mustBe("my summary"),
})
//
// Bad usage
checkError(t, "never tested",
td.Code(nil),
expectedError{
Message: mustBe("bad usage of Code operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Code(FUNC), but received nil as 1st parameter"),
})
checkError(t, "never tested",
td.Code((func(string) bool)(nil)),
expectedError{
Message: mustBe("bad usage of Code operator"),
Path: mustBe("DATA"),
Summary: mustBe("Code(FUNC): FUNC cannot be a nil function"),
})
checkError(t, "never tested",
td.Code("test"),
expectedError{
Message: mustBe("bad usage of Code operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Code(FUNC), but received string as 1st parameter"),
})
checkError(t, "never tested",
td.Code(func(x ...int) bool { return true }),
expectedError{
Message: mustBe("bad usage of Code operator"),
Path: mustBe("DATA"),
Summary: mustBe("Code(FUNC): FUNC must take only one non-variadic argument or (*td.T, arg) or (*td.T, *td.T, arg)"),
})
checkError(t, "never tested",
td.Code(func() bool { return true }),
expectedError{
Message: mustBe("bad usage of Code operator"),
Path: mustBe("DATA"),
Summary: mustBe("Code(FUNC): FUNC must take only one non-variadic argument or (*td.T, arg) or (*td.T, *td.T, arg)"),
})
checkError(t, "never tested",
td.Code(func(a, b, c, d string) bool { return true }),
expectedError{
Message: mustBe("bad usage of Code operator"),
Path: mustBe("DATA"),
Summary: mustBe("Code(FUNC): FUNC must take only one non-variadic argument or (*td.T, arg) or (*td.T, *td.T, arg)"),
})
checkError(t, "never tested",
td.Code(func(a int, b string) bool { return true }),
expectedError{
Message: mustBe("bad usage of Code operator"),
Path: mustBe("DATA"),
Summary: mustBe("Code(FUNC): FUNC must take only one non-variadic argument or (*td.T, arg) or (*td.T, *td.T, arg)"),
})
checkError(t, "never tested",
td.Code(func(t *td.T, a int, b string) bool { return true }),
expectedError{
Message: mustBe("bad usage of Code operator"),
Path: mustBe("DATA"),
Summary: mustBe("Code(FUNC): FUNC must take only one non-variadic argument or (*td.T, arg) or (*td.T, *td.T, arg)"),
})
checkError(t, "never tested", // because it is certainly an error
td.Code(func(assert, require *td.T) bool { return true }),
expectedError{
Message: mustBe("bad usage of Code operator"),
Path: mustBe("DATA"),
Summary: mustBe("Code(FUNC): FUNC must take only one non-variadic argument or (*td.T, arg) or (*td.T, *td.T, arg)"),
})
checkError(t, "never tested",
td.Code(func(n int) (bool, int) { return true, 0 }),
expectedError{
Message: mustBe("bad usage of Code operator"),
Path: mustBe("DATA"),
Summary: mustBe("Code(FUNC): FUNC must return bool or (bool, string) or error"),
})
checkError(t, "never tested",
td.Code(func(n int) (error, string) { return nil, "" }), //nolint: staticcheck
expectedError{
Message: mustBe("bad usage of Code operator"),
Path: mustBe("DATA"),
Summary: mustBe("Code(FUNC): FUNC must return bool or (bool, string) or error"),
})
checkError(t, "never tested",
td.Code(func(n int) (int, string) { return 0, "" }),
expectedError{
Message: mustBe("bad usage of Code operator"),
Path: mustBe("DATA"),
Summary: mustBe("Code(FUNC): FUNC must return bool or (bool, string) or error"),
})
checkError(t, "never tested",
td.Code(func(n int) (string, bool) { return "", true }),
expectedError{
Message: mustBe("bad usage of Code operator"),
Path: mustBe("DATA"),
Summary: mustBe("Code(FUNC): FUNC must return bool or (bool, string) or error"),
})
checkError(t, "never tested",
td.Code(func(n int) (bool, string, int) { return true, "", 0 }),
expectedError{
Message: mustBe("bad usage of Code operator"),
Path: mustBe("DATA"),
Summary: mustBe("Code(FUNC): FUNC must return bool or (bool, string) or error"),
})
checkError(t, "never tested",
td.Code(func(n int) {}),
expectedError{
Message: mustBe("bad usage of Code operator"),
Path: mustBe("DATA"),
Summary: mustBe("Code(FUNC): FUNC must return bool or (bool, string) or error"),
})
checkError(t, "never tested",
td.Code(func(n int) int { return 0 }),
expectedError{
Message: mustBe("bad usage of Code operator"),
Path: mustBe("DATA"),
Summary: mustBe("Code(FUNC): FUNC must return bool or (bool, string) or error"),
})
checkError(t, "never tested",
td.Code(func(t *td.T, a int) bool { return true }),
expectedError{
Message: mustBe("bad usage of Code operator"),
Path: mustBe("DATA"),
Summary: mustBe("Code(FUNC): FUNC must return nothing"),
})
checkError(t, "never tested",
td.Code(func(assert, require *td.T, a int) bool { return true }),
expectedError{
Message: mustBe("bad usage of Code operator"),
Path: mustBe("DATA"),
Summary: mustBe("Code(FUNC): FUNC must return nothing"),
})
//
// String
test.EqualStr(t,
td.Code(func(n int) bool { return false }).String(),
"Code(func(int) bool)")
test.EqualStr(t,
td.Code(func(n int) (bool, string) { return false, "" }).String(),
"Code(func(int) (bool, string))")
test.EqualStr(t,
td.Code(func(n int) error { return nil }).String(),
"Code(func(int) error)")
test.EqualStr(t,
td.Code(func(n int) (MyBool, MyString) { return false, "" }).String(),
"Code(func(int) (td_test.MyBool, td_test.MyString))")
// Erroneous op
test.EqualStr(t, td.Code(nil).String(), "Code()")
}
func TestCodeCustom(t *testing.T) {
// Specific _checkOK func as td.Code(FUNC) with FUNC(t,arg) or
// FUNC(assert,require,arg) works in non-boolean context but cannot
// work in boolean context as there is no initial testing.TB instance
_customCheckOK := func(t *testing.T, got, expected any, args ...any) bool {
t.Helper()
if !td.Cmp(t, got, expected, args...) {
return false
}
// Should always fail in boolean context as no original testing.TB available
err := td.EqDeeplyError(got, expected)
if err == nil {
t.Error(`Boolean context succeeded and it shouldn't`)
return false
}
expErr := expectedError{
Message: mustBe("cannot build *td.T instance"),
Path: mustBe("DATA"),
Summary: mustBe("original testing.TB instance is missing"),
}
if !strings.HasPrefix(expected.(fmt.Stringer).String(), "Code") {
expErr = ifaceExpectedError(t, expErr)
}
if !matchError(t, err.(*ctxerr.Error), expErr, true, args...) {
return false
}
if td.EqDeeply(got, expected) {
t.Error(`Boolean context succeeded and it shouldn't`)
return false
}
return true
}
customCheckOK(t, _customCheckOK, 123, td.Code(func(t *td.T, n int) {
t.Cmp(t.Config.FailureIsFatal, false)
t.Cmp(n, 123)
}))
customCheckOK(t, _customCheckOK, 123, td.Code(func(assert, require *td.T, n int) {
assert.Cmp(assert.Config.FailureIsFatal, false)
assert.Cmp(require.Config.FailureIsFatal, true)
assert.Cmp(n, 123)
require.Cmp(n, 123)
}))
got := map[string]int{"foo": 123}
t.Run("Simple success", func(t *testing.T) {
mockT := test.NewTestingTB("TestCodeCustom")
td.Cmp(mockT, got, td.Map(map[string]int{}, td.MapEntries{
"foo": td.Code(func(t *td.T, n int) {
t.Cmp(n, 123)
}),
}))
test.EqualInt(t, len(mockT.Messages), 0)
})
t.Run("Simple failure", func(t *testing.T) {
mockT := test.NewTestingTB("TestCodeCustom")
td.NewT(mockT).
RootName("PIPO").
Cmp(got, td.Map(map[string]int{}, td.MapEntries{
"foo": td.Code(func(t *td.T, n int) {
t.Cmp(n, 124) // inherit only RootName
t.RootName(t.Config.OriginalPath()).Cmp(n, 125) // recover current path
t.RootName("").Cmp(n, 126) // undo RootName inheritance
}),
}))
test.IsTrue(t, mockT.HasFailed)
test.IsFalse(t, mockT.IsFatal)
missing := mockT.ContainsMessages(
`PIPO: values differ`,
` got: 123`,
`expected: 124`,
`PIPO["foo"]: values differ`,
` got: 123`,
`expected: 125`,
`DATA: values differ`,
` got: 123`,
`expected: 126`,
)
if len(missing) != 0 {
t.Error("Following expected messages are not found:\n-", strings.Join(missing, "\n- "))
t.Error("================================ in:")
t.Error(strings.Join(mockT.Messages, "\n"))
t.Error("====================================")
}
})
t.Run("AssertRequire success", func(t *testing.T) {
mockT := test.NewTestingTB("TestCodeCustom")
td.Cmp(mockT, got, td.Map(map[string]int{}, td.MapEntries{
"foo": td.Code(func(assert, require *td.T, n int) {
assert.Cmp(n, 123)
require.Cmp(n, 123)
}),
}))
test.EqualInt(t, len(mockT.Messages), 0)
})
t.Run("AssertRequire failure", func(t *testing.T) {
mockT := test.NewTestingTB("TestCodeCustom")
td.NewT(mockT).
RootName("PIPO").
Cmp(got, td.Map(map[string]int{}, td.MapEntries{
"foo": td.Code(func(assert, require *td.T, n int) {
assert.Cmp(n, 124) // inherit only RootName
assert.RootName(assert.Config.OriginalPath()).Cmp(n, 125) // recover current path
assert.RootName(require.Config.OriginalPath()).Cmp(n, 126) // recover current path
assert.RootName("").Cmp(n, 127) // undo RootName inheritance
}),
}))
test.IsTrue(t, mockT.HasFailed)
test.IsFalse(t, mockT.IsFatal)
missing := mockT.ContainsMessages(
`PIPO: values differ`,
` got: 123`,
`expected: 124`,
`PIPO["foo"]: values differ`,
` got: 123`,
`expected: 125`,
`PIPO["foo"]: values differ`,
` got: 123`,
`expected: 126`,
`DATA: values differ`,
` got: 123`,
`expected: 127`,
)
if len(missing) != 0 {
t.Error("Following expected messages are not found:\n-", strings.Join(missing, "\n- "))
t.Error("================================ in:")
t.Error(strings.Join(mockT.Messages, "\n"))
t.Error("====================================")
}
})
t.Run("AssertRequire fatalfailure", func(t *testing.T) {
mockT := test.NewTestingTB("TestCodeCustom")
td.NewT(mockT).
RootName("PIPO").
Cmp(got, td.Map(map[string]int{}, td.MapEntries{
"foo": td.Code(func(assert, require *td.T, n int) {
mockT.CatchFatal(func() {
assert.RootName("FIRST").Cmp(n, 124)
require.RootName("SECOND").Cmp(n, 125)
assert.RootName("THIRD").Cmp(n, 126)
})
}),
}))
test.IsTrue(t, mockT.HasFailed)
test.IsTrue(t, mockT.IsFatal)
missing := mockT.ContainsMessages(
`FIRST: values differ`,
` got: 123`,
`expected: 124`,
`SECOND: values differ`,
` got: 123`,
`expected: 125`,
)
mesgs := strings.Join(mockT.Messages, "\n")
if len(missing) != 0 {
t.Error("Following expected messages are not found:\n-", strings.Join(missing, "\n- "))
t.Error("================================ in:")
t.Error(mesgs)
t.Error("====================================")
}
if strings.Contains(mesgs, "THIRD") {
t.Error("THIRD test found, but shouldn't, in:")
t.Error(mesgs)
t.Error("====================================")
}
})
}
func TestCodeTypeBehind(t *testing.T) {
// Type behind is the code function parameter one
equalTypes(t, td.Code(func(n int) bool { return n != 0 }), 23)
equalTypes(t, td.Code(func(_ *td.T, n int) {}), 23)
equalTypes(t, td.Code(func(_, _ *td.T, n int) {}), 23)
type MyTime time.Time
equalTypes(t,
td.Code(func(t MyTime) bool { return time.Time(t).IsZero() }),
MyTime{})
equalTypes(t, td.Code(func(_ *td.T, t MyTime) {}), MyTime{})
equalTypes(t, td.Code(func(_, _ *td.T, t MyTime) {}), MyTime{})
// Erroneous op
equalTypes(t, td.Code(nil), nil)
}
go-testdeep-1.15.0/td/td_contains.go 0000664 0000000 0000000 00000023670 15144170453 0017275 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (
"bytes"
"reflect"
"strings"
"github.com/maxatome/go-testdeep/helpers/tdutil"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/types"
"github.com/maxatome/go-testdeep/internal/util"
)
type tdContains struct {
tdSmugglerBase
}
var _ TestDeep = &tdContains{}
// summary(Contains): checks that a string, []byte, error or
// fmt.Stringer interfaces contain a rune, byte or a sub-string; or a
// slice contains a single value or a sub-slice; or an array or map
// contain a single value
// input(Contains): str,array,slice,map,if(✓ + fmt.Stringer/error)
// Contains is a smuggler operator to check if something is contained
// in another thing. Contains has to be applied on arrays, slices, maps or
// strings. It tries to be as smarter as possible.
//
// If expectedValue is a [TestDeep] operator, each item of data
// array/slice/map/string (rune for strings) is compared to it. The
// use of a [TestDeep] operator as expectedValue works only in this
// way: item per item.
//
// If data is a slice, and expectedValue has the same type, then
// expectedValue is searched as a sub-slice, otherwise
// expectedValue is compared to each slice value.
//
// list := []int{12, 34, 28}
// td.Cmp(t, list, td.Contains(34)) // succeeds
// td.Cmp(t, list, td.Contains(td.Between(30, 35))) // succeeds too
// td.Cmp(t, list, td.Contains(35)) // fails
// td.Cmp(t, list, td.Contains([]int{34, 28})) // succeeds
//
// If data is an array or a map, each value is compared to
// expectedValue. Map keys are not checked: see [ContainsKey] to check
// map keys existence.
//
// hash := map[string]int{"foo": 12, "bar": 34, "zip": 28}
// td.Cmp(t, hash, td.Contains(34)) // succeeds
// td.Cmp(t, hash, td.Contains(td.Between(30, 35))) // succeeds too
// td.Cmp(t, hash, td.Contains(35)) // fails
//
// array := [...]int{12, 34, 28}
// td.Cmp(t, array, td.Contains(34)) // succeeds
// td.Cmp(t, array, td.Contains(td.Between(30, 35))) // succeeds too
// td.Cmp(t, array, td.Contains(35)) // fails
//
// If data is a string (or convertible), []byte (or convertible),
// error or [fmt.Stringer] interface (error interface is tested before
// [fmt.Stringer]), expectedValue can be a string, a []byte, a rune or
// a byte. In this case, it tests if the got string contains this
// expected string, []byte, rune or byte.
//
// got := "foo bar"
// td.Cmp(t, got, td.Contains('o')) // succeeds
// td.Cmp(t, got, td.Contains(rune('o'))) // succeeds
// td.Cmp(t, got, td.Contains(td.Between('n', 'p'))) // succeeds
// td.Cmp(t, got, td.Contains("bar")) // succeeds
// td.Cmp(t, got, td.Contains([]byte("bar"))) // succeeds
//
// td.Cmp(t, []byte("foobar"), td.Contains("ooba")) // succeeds
//
// type Foobar string
// td.Cmp(t, Foobar("foobar"), td.Contains("ooba")) // succeeds
//
// err := errors.New("error!")
// td.Cmp(t, err, td.Contains("ror")) // succeeds
//
// bstr := bytes.NewBufferString("fmt.Stringer!")
// td.Cmp(t, bstr, td.Contains("String")) // succeeds
//
// Pitfall: if you want to check if 2 words are contained in got, don't do:
//
// td.Cmp(t, "foobar", td.Contains(td.All("foo", "bar"))) // Bad!
//
// as [TestDeep] operator [All] in Contains operates on each rune, so it
// does not work as expected, but do::
//
// td.Cmp(t, "foobar", td.All(td.Contains("foo"), td.Contains("bar")))
//
// When Contains(nil) is used, nil is automatically converted to a
// typed nil on the fly to avoid confusion (if the array/slice/map
// item type allows it of course.) So all following [Cmp] calls
// are equivalent (except the (*byte)(nil) one):
//
// num := 123
// list := []*int{&num, nil}
// td.Cmp(t, list, td.Contains(nil)) // succeeds → (*int)(nil)
// td.Cmp(t, list, td.Contains((*int)(nil))) // succeeds
// td.Cmp(t, list, td.Contains(td.Nil())) // succeeds
// // But...
// td.Cmp(t, list, td.Contains((*byte)(nil))) // fails: (*byte)(nil) ≠ (*int)(nil)
//
// As well as these ones:
//
// hash := map[string]*int{"foo": nil, "bar": &num}
// td.Cmp(t, hash, td.Contains(nil)) // succeeds → (*int)(nil)
// td.Cmp(t, hash, td.Contains((*int)(nil))) // succeeds
// td.Cmp(t, hash, td.Contains(td.Nil())) // succeeds
//
// See also [ContainsKey].
func Contains(expectedValue any) TestDeep {
c := tdContains{
tdSmugglerBase: newSmugglerBase(expectedValue),
}
if !c.isTestDeeper {
c.expectedValue = reflect.ValueOf(expectedValue)
}
return &c
}
func (c *tdContains) doesNotContainErr(ctx ctxerr.Context, got any) *ctxerr.Error {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "does not contain",
Got: got,
Expected: c,
})
}
// getExpectedValue returns the expected value handling the
// Contains(nil) case: in this case it returns a typed nil (same type
// as the items of got).
// got is an array, a slice or a map (it's the caller responsibility to check).
func (c *tdContains) getExpectedValue(got reflect.Value) reflect.Value {
// If the expectValue is non-typed nil
if !c.expectedValue.IsValid() {
// AND the kind of items in got is...
switch got.Type().Elem().Kind() {
case reflect.Chan, reflect.Func, reflect.Interface,
reflect.Map, reflect.Ptr, reflect.Slice:
// returns a typed nil
return reflect.Zero(got.Type().Elem())
}
}
return c.expectedValue
}
func (c *tdContains) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
switch got.Kind() {
case reflect.Slice:
if !c.isTestDeeper && c.expectedValue.IsValid() {
// Special case for []byte & expected []byte or string
if got.Type().Elem() == types.Uint8 {
switch c.expectedValue.Kind() {
case reflect.String:
if bytes.Contains(got.Bytes(), []byte(c.expectedValue.String())) {
return nil
}
return c.doesNotContainErr(ctx, got)
case reflect.Slice:
if c.expectedValue.Type().Elem() == types.Uint8 {
if bytes.Contains(got.Bytes(), c.expectedValue.Bytes()) {
return nil
}
return c.doesNotContainErr(ctx, got)
}
case reflect.Int32: // rune
if bytes.ContainsRune(got.Bytes(), rune(c.expectedValue.Int())) {
return nil
}
return c.doesNotContainErr(ctx, got)
case reflect.Uint8: // byte
if bytes.ContainsRune(got.Bytes(), rune(c.expectedValue.Uint())) {
return nil
}
return c.doesNotContainErr(ctx, got)
}
// fall back on string conversion
break
}
// Search slice in slice
if got.Type() == c.expectedValue.Type() {
gotLen, expectedLen := got.Len(), c.expectedValue.Len()
if expectedLen == 0 {
return nil
}
if expectedLen > gotLen {
return c.doesNotContainErr(ctx, got)
}
for i := 0; i <= gotLen-expectedLen; i++ {
ok, err := deepValueEqualFinalOK(ctx, got.Slice(i, i+expectedLen), c.expectedValue)
if err != nil || ok {
return err
}
}
return c.doesNotContainErr(ctx, got)
}
}
fallthrough
case reflect.Array:
expectedValue := c.getExpectedValue(got)
for index := got.Len() - 1; index >= 0; index-- {
ok, err := deepValueEqualFinalOK(ctx, got.Index(index), expectedValue)
if err != nil || ok {
return err
}
}
return c.doesNotContainErr(ctx, got)
case reflect.Map:
expectedValue := c.getExpectedValue(got)
var err *ctxerr.Error
var ok bool
if !tdutil.MapEachValue(got, func(v reflect.Value) bool {
ok, err = deepValueEqualFinalOK(ctx, v, expectedValue)
return err == nil && !ok
}) {
return err
}
return c.doesNotContainErr(ctx, got)
}
str, err := getString(ctx, got)
if err != nil {
return ctx.CollectError(err)
}
// If a TestDeep operator is expected, applies this operator on
// each character of the string
if c.isTestDeeper {
// If the type behind the operator is known *and* is not rune,
// then no need to go further, but return an explicit error to
// help our user to fix his probably bogus code
op := c.expectedValue.Interface().(TestDeep)
if typeBehind := op.TypeBehind(); typeBehind != nil && typeBehind != types.Rune && !ctx.BeLax {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: op.GetLocation().Func + " operator has to match rune in string, but it does not",
Got: types.RawString(typeBehind.String()),
Expected: types.RawString("rune"),
})
}
for _, chr := range str {
ok, err := deepValueEqualFinalOK(ctx, reflect.ValueOf(chr), c.expectedValue)
if err != nil || ok {
return err
}
}
return c.doesNotContainErr(ctx, got)
}
// If expectedValue is a []byte, a string, a rune or a byte, we
// check whether it is contained in the string or not
var contains bool
switch expectedKind := c.expectedValue.Kind(); expectedKind {
case reflect.String:
contains = strings.Contains(str, c.expectedValue.String())
case reflect.Int32: // rune
contains = strings.ContainsRune(str, rune(c.expectedValue.Int()))
case reflect.Uint8: // byte
contains = strings.ContainsRune(str, rune(c.expectedValue.Uint()))
case reflect.Slice:
// Only []byte
if c.expectedValue.Type().Elem() == types.Uint8 {
contains = strings.Contains(str, string(c.expectedValue.Bytes()))
break
}
fallthrough
default:
if ctx.BooleanError {
return ctxerr.BooleanError
}
var expectedType any
if c.expectedValue.IsValid() {
expectedType = types.RawString(c.expectedValue.Type().String())
} else {
expectedType = c
}
return ctx.CollectError(&ctxerr.Error{
Message: "cannot check contains",
Got: types.RawString(got.Type().String()),
Expected: expectedType,
})
}
if contains {
return nil
}
return c.doesNotContainErr(ctx, str)
}
func (c *tdContains) String() string {
return "Contains(" + util.ToString(c.expectedValue) + ")"
}
go-testdeep-1.15.0/td/td_contains_key.go 0000664 0000000 0000000 00000010107 15144170453 0020134 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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/helpers/tdutil"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/types"
"github.com/maxatome/go-testdeep/internal/util"
)
type tdContainsKey struct {
tdSmugglerBase
}
var _ TestDeep = &tdContainsKey{}
// summary(ContainsKey): checks that a map contains a key
// input(ContainsKey): map
// ContainsKey is a smuggler operator and works on maps only. It
// compares each key of map against expectedValue.
//
// hash := map[string]int{"foo": 12, "bar": 34, "zip": 28}
// td.Cmp(t, hash, td.ContainsKey("foo")) // succeeds
// td.Cmp(t, hash, td.ContainsKey(td.HasPrefix("z"))) // succeeds
// td.Cmp(t, hash, td.ContainsKey(td.HasPrefix("x"))) // fails
//
// hnum := map[int]string{1: "foo", 42: "bar"}
// td.Cmp(t, hash, td.ContainsKey(42)) // succeeds
// td.Cmp(t, hash, td.ContainsKey(td.Between(40, 45))) // succeeds
//
// When ContainsKey(nil) is used, nil is automatically converted to a
// typed nil on the fly to avoid confusion (if the map key type allows
// it of course.) So all following [Cmp] calls are equivalent
// (except the (*byte)(nil) one):
//
// num := 123
// hnum := map[*int]bool{&num: true, nil: true}
// td.Cmp(t, hnum, td.ContainsKey(nil)) // succeeds → (*int)(nil)
// td.Cmp(t, hnum, td.ContainsKey((*int)(nil))) // succeeds
// td.Cmp(t, hnum, td.ContainsKey(td.Nil())) // succeeds
// // But...
// td.Cmp(t, hnum, td.ContainsKey((*byte)(nil))) // fails: (*byte)(nil) ≠ (*int)(nil)
//
// See also [Contains].
func ContainsKey(expectedValue any) TestDeep {
c := tdContainsKey{
tdSmugglerBase: newSmugglerBase(expectedValue),
}
if !c.isTestDeeper {
c.expectedValue = reflect.ValueOf(expectedValue)
}
return &c
}
func (c *tdContainsKey) doesNotContainKey(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "does not contain key",
Summary: ctxerr.ErrorSummaryItems{
{
Label: "expected key",
Value: util.ToString(c.expectedValue),
},
{
Label: "not in keys",
Value: util.ToString(tdutil.MapSortedKeys(got)),
},
},
})
}
// getExpectedValue returns the expected value handling the
// Contains(nil) case: in this case it returns a typed nil (same type
// as the keys of got).
// got is a map (it's the caller responsibility to check).
func (c *tdContainsKey) getExpectedValue(got reflect.Value) reflect.Value {
// If the expectValue is non-typed nil
if !c.expectedValue.IsValid() {
// AND the kind of items in got is...
switch got.Type().Key().Kind() {
case reflect.Chan, reflect.Func, reflect.Interface,
reflect.Map, reflect.Ptr, reflect.Slice:
// returns a typed nil
return reflect.Zero(got.Type().Key())
}
}
return c.expectedValue
}
func (c *tdContainsKey) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if got.Kind() == reflect.Map {
expectedValue := c.getExpectedValue(got)
// If expected value is a TestDeep operator OR BeLax, check each key
if c.isTestDeeper || ctx.BeLax {
for _, k := range got.MapKeys() {
ok, err := deepValueEqualFinalOK(ctx, k, expectedValue)
if err != nil || ok {
return err
}
}
} else if expectedValue.IsValid() &&
got.Type().Key() == expectedValue.Type() &&
got.MapIndex(expectedValue).IsValid() {
return nil
}
return c.doesNotContainKey(ctx, got)
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
var expectedType any
if c.expectedValue.IsValid() {
expectedType = types.RawString(c.expectedValue.Type().String())
} else {
expectedType = c
}
return ctx.CollectError(&ctxerr.Error{
Message: "cannot check contains key",
Got: types.RawString(got.Type().String()),
Expected: expectedType,
})
}
func (c *tdContainsKey) String() string {
return "ContainsKey(" + util.ToString(c.expectedValue) + ")"
}
go-testdeep-1.15.0/td/td_contains_key_test.go 0000664 0000000 0000000 00000006210 15144170453 0021173 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (
"fmt"
"testing"
"github.com/maxatome/go-testdeep/td"
)
func TestContainsKey(t *testing.T) {
type MyMap map[int]string
for idx, got := range []any{
map[int]string{12: "foo", 34: "bar", 28: "zip"},
MyMap{12: "foo", 34: "bar", 28: "zip"},
} {
testName := fmt.Sprintf("#%d: got=%v", idx, got)
checkOK(t, got, td.ContainsKey(34), testName)
checkOK(t, got, td.ContainsKey(td.Between(30, 35)),
testName)
checkError(t, got, td.ContainsKey(35),
expectedError{
Message: mustBe("does not contain key"),
Path: mustBe("DATA"),
Summary: mustMatch(`expected key: 35
not in keys: \((12|28|34),
(12|28|34),
(12|28|34)\)`),
}, testName)
// Lax
checkOK(t, got, td.Lax(td.ContainsKey(float64(34))), testName)
}
checkError(t, map[int]bool{12: false}, td.ContainsKey(td.JSON("{")),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe(`DATA`),
Summary: mustContain("JSON unmarshal error"),
Under: mustContain("under operator JSON at "),
},
"erroneous operator expected")
}
// nil case.
func TestContainsKeyNil(t *testing.T) {
type MyPtrMap map[*int]int
num := 12345642
for idx, got := range []any{
map[*int]int{&num: 42, nil: 666},
MyPtrMap{&num: 42, nil: 666},
} {
testName := fmt.Sprintf("#%d: got=%v", idx, got)
checkOK(t, got, td.ContainsKey(nil), testName)
checkOK(t, got, td.ContainsKey((*int)(nil)), testName)
checkOK(t, got, td.ContainsKey(td.Nil()), testName)
checkOK(t, got, td.ContainsKey(td.NotNil()), testName)
checkError(t, got, td.ContainsKey((*uint8)(nil)),
expectedError{
Message: mustBe("does not contain key"),
Path: mustBe("DATA"),
Summary: mustMatch(`expected key: \(\*uint8\)\(\)
not in keys: \(\(\*int\)\((|.*12345642.*)\),
\(\*int\)\((|.*12345642.*)\)\)`),
}, testName)
}
checkError(t,
map[string]int{"foo": 12, "bar": 34, "zip": 28}, // got
td.ContainsKey(nil),
expectedError{
Message: mustBe("does not contain key"),
Path: mustBe("DATA"),
Summary: mustMatch(`expected key: nil
not in keys: \("(foo|bar|zip)",
"(foo|bar|zip)",
"(foo|bar|zip)"\)`),
})
checkError(t, "foobar", td.ContainsKey(nil),
expectedError{
Message: mustBe("cannot check contains key"),
Path: mustBe("DATA"),
Got: mustBe("string"),
Expected: mustBe("ContainsKey(nil)"),
})
checkError(t, "foobar", td.ContainsKey(123),
expectedError{
Message: mustBe("cannot check contains key"),
Path: mustBe("DATA"),
Got: mustBe("string"),
Expected: mustBe("int"),
})
// Caught by deepValueEqual, before Match() call
checkError(t, nil, td.ContainsKey(nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("ContainsKey(nil)"),
})
}
func TestContainsKeyTypeBehind(t *testing.T) {
equalTypes(t, td.ContainsKey("x"), nil)
}
go-testdeep-1.15.0/td/td_contains_test.go 0000664 0000000 0000000 00000022274 15144170453 0020333 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-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 (
"errors"
"fmt"
"reflect"
"testing"
"github.com/maxatome/go-testdeep/td"
)
func TestContains(t *testing.T) {
type (
MySlice []int
MyArray [3]int
MyMap map[string]int
MyString string
)
for idx, got := range []any{
[]int{12, 34, 28},
MySlice{12, 34, 28},
[...]int{12, 34, 28},
MyArray{12, 34, 28},
map[string]int{"foo": 12, "bar": 34, "zip": 28},
MyMap{"foo": 12, "bar": 34, "zip": 28},
} {
testName := fmt.Sprintf("#%d: got=%v", idx, got)
checkOK(t, got, td.Contains(34), testName)
checkOK(t, got, td.Contains(td.Between(30, 35)), testName)
checkError(t, got, td.Contains(35),
expectedError{
Message: mustBe("does not contain"),
Path: mustBe("DATA"),
Got: mustContain("34"), // as well as other items in fact...
Expected: mustBe("Contains(35)"),
}, testName)
// Lax
checkOK(t, got, td.Lax(td.Contains(float64(34))), testName)
}
for idx, got := range []any{
"foobar",
MyString("foobar"),
} {
testName := fmt.Sprintf("#%d: got=%v", idx, got)
checkOK(t, got, td.Contains(td.Between('n', 'p')), testName)
checkError(t, got, td.Contains(td.Between('y', 'z')),
expectedError{
Message: mustBe("does not contain"),
Path: mustBe("DATA"),
Got: mustContain(`"foobar"`), // as well as other items in fact...
Expected: mustBe(fmt.Sprintf("Contains((int32) %d ≤ got ≤ (int32) %d)", 'y', 'z')),
}, testName)
}
checkError(t, [...]any{1, 2, 3}, td.Contains(td.JSON("{")),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe(`DATA`),
Summary: mustContain("JSON unmarshal error"),
Under: mustContain("under operator JSON at "),
},
"erroneous operator expected")
checkError(t, map[string]any{"foo": 123}, td.Contains(td.JSON("{")),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe(`DATA`),
Summary: mustContain("JSON unmarshal error"),
Under: mustContain("under operator JSON at "),
},
"erroneous operator expected")
}
// nil case.
func TestContainsNil(t *testing.T) {
type (
MyPtrSlice []*int
MyPtrArray [3]*int
MyPtrMap map[string]*int
)
num := 12345642
for idx, got := range []any{
[]*int{&num, nil},
MyPtrSlice{&num, nil},
[...]*int{&num, nil},
MyPtrArray{&num},
map[string]*int{"foo": &num, "bar": nil},
MyPtrMap{"foo": &num, "bar": nil},
} {
testName := fmt.Sprintf("#%d: got=%v", idx, got)
checkOK(t, got, td.Contains(nil), testName)
checkOK(t, got, td.Contains((*int)(nil)), testName)
checkOK(t, got, td.Contains(td.Nil()), testName)
checkOK(t, got, td.Contains(td.NotNil()), testName)
checkError(t, got, td.Contains((*uint8)(nil)),
expectedError{
Message: mustBe("does not contain"),
Path: mustBe("DATA"),
Got: mustContain("12345642"),
Expected: mustBe("Contains((*uint8)())"),
}, testName)
}
for idx, got := range []any{
[]any{nil, 12345642},
[]func(){nil, func() {}},
[][]int{{}, nil},
[...]any{nil, 12345642},
[...]func(){nil, func() {}},
[...][]int{{}, nil},
map[bool]any{true: nil, false: 12345642},
map[bool]func(){true: nil, false: func() {}},
map[bool][]int{true: {}, false: nil},
} {
testName := fmt.Sprintf("#%d: got=%v", idx, got)
checkOK(t, got, td.Contains(nil), testName)
checkOK(t, got, td.Contains(td.Nil()), testName)
checkOK(t, got, td.Contains(td.NotNil()), testName)
}
for idx, got := range []any{
[]int{1, 2, 3},
[...]int{1, 2, 3},
map[string]int{"foo": 12, "bar": 34, "zip": 28},
} {
testName := fmt.Sprintf("#%d: got=%v", idx, got)
checkError(t, got, td.Contains(nil),
expectedError{
Message: mustBe("does not contain"),
Path: mustBe("DATA"),
// Got
Expected: mustBe("Contains(nil)"),
}, testName)
}
checkError(t, "foobar", td.Contains(nil),
expectedError{
Message: mustBe("cannot check contains"),
Path: mustBe("DATA"),
Got: mustBe("string"),
Expected: mustBe("Contains(nil)"),
})
// Caught by deepValueEqual, before Match() call
checkError(t, nil, td.Contains(nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("Contains(nil)"),
})
}
func TestContainsString(t *testing.T) {
type MyString string
for idx, got := range []any{
"pipo bingo",
MyString("pipo bingo"),
[]byte("pipo bingo"),
errors.New("pipo bingo"), // error interface
MyStringer{}, // fmt.Stringer interface
} {
testName := fmt.Sprintf("#%d: got=%v", idx, got)
checkOK(t, got, td.Contains("pipo"), testName)
checkOK(t, got, td.Contains("po bi"), testName)
checkOK(t, got, td.Contains("bingo"), testName)
checkOK(t, got, td.Contains([]byte("pipo")), testName)
checkOK(t, got, td.Contains([]byte("po bi")), testName)
checkOK(t, got, td.Contains([]byte("bingo")), testName)
checkOK(t, got, td.Contains('o'), testName)
checkOK(t, got, td.Contains(byte('o')), testName)
checkOK(t, got, td.Contains(""), testName)
checkOK(t, got, td.Contains([]byte{}), testName)
if _, ok := got.([]byte); ok {
checkOK(t, got,
td.Contains(td.Code(func(b byte) bool { return b == 'o' })),
testName)
} else {
checkOK(t, got,
td.Contains(td.Code(func(r rune) bool { return r == 'o' })),
testName)
}
checkError(t, got, td.Contains("zip"),
expectedError{
Message: mustBe("does not contain"),
Path: mustBe("DATA"),
Got: mustContain(`pipo bingo`),
Expected: mustMatch(`^Contains\(.*"zip"`),
})
checkError(t, got, td.Contains([]byte("zip")),
expectedError{
Message: mustBe("does not contain"),
Path: mustBe("DATA"),
Got: mustContain(`pipo bingo`),
Expected: mustMatch(`^(?s)Contains\(.*zip`),
})
checkError(t, got, td.Contains('z'),
expectedError{
Message: mustBe("does not contain"),
Path: mustBe("DATA"),
Got: mustContain(`pipo bingo`),
Expected: mustBe(`Contains((int32) 122)`),
})
checkError(t, got, td.Contains(byte('z')),
expectedError{
Message: mustBe("does not contain"),
Path: mustBe("DATA"),
Got: mustContain(`pipo bingo`),
Expected: mustBe(`Contains((uint8) 122)`),
})
checkError(t, got, td.Contains(12),
expectedError{
Message: mustBe("cannot check contains"),
Path: mustBe("DATA"),
Got: mustBe(reflect.TypeOf(got).String()),
Expected: mustBe("int"),
})
checkError(t, got, td.Contains([]int{1, 2, 3}),
expectedError{
Message: mustBe("cannot check contains"),
Path: mustBe("DATA"),
Got: mustBe(reflect.TypeOf(got).String()),
Expected: mustBe("[]int"),
})
// Lax
checkOK(t, got,
td.Lax(td.Contains(td.Code(func(b int) bool { return b == 'o' }))),
testName)
}
checkError(t, 12, td.Contains("bar"),
expectedError{
Message: mustBe("bad type"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("string (convertible) OR []byte (convertible) OR fmt.Stringer OR error"),
})
checkError(t, "pipo", td.Contains(td.Code(func(x int) bool { return true })),
expectedError{
Message: mustBe("Code operator has to match rune in string, but it does not"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("rune"),
})
checkError(t, "pipo", td.Contains(td.JSON("{")),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe(`DATA`),
Summary: mustContain("JSON unmarshal error"),
Under: mustContain("under operator JSON at "),
},
"erroneous operator expected")
}
func TestContainsSlice(t *testing.T) {
got := []int{1, 2, 3, 4, 5, 6}
// Empty slice is always OK
checkOK(t, got, td.Contains([]int{}))
// Expected length > got length
checkError(t, got, td.Contains([]int{1, 2, 3, 4, 5, 6, 7}),
expectedError{
Message: mustBe("does not contain"),
Path: mustBe("DATA"),
Got: mustContain(`([]int) (len=6)`),
Expected: mustContain(`Contains(([]int) (len=7)`),
})
// Same length
checkOK(t, got, td.Contains([]int{1, 2, 3, 4, 5, 6}))
checkError(t, got, td.Contains([]int{8, 8, 8, 8, 8, 8}),
expectedError{
Message: mustBe("does not contain"),
Path: mustBe("DATA"),
Got: mustContain(`([]int) (len=6)`),
Expected: mustContain(`Contains(([]int) (len=6)`),
})
checkOK(t, got, td.Contains([]int{1, 2, 3}))
checkOK(t, got, td.Contains([]int{3, 4, 5}))
checkOK(t, got, td.Contains([]int{4, 5, 6}))
checkError(t, got, td.Contains([]int{8, 8, 8}),
expectedError{
Message: mustBe("does not contain"),
Path: mustBe("DATA"),
Got: mustContain(`([]int) (len=6)`),
Expected: mustContain(`Contains(([]int) (len=3)`),
})
checkError(t, []any{1, 2, 3}, td.Contains([]any{1, td.JSON("{")}),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustMatch(`DATA\[1\]`), // always without .Iface
Summary: mustContain("JSON unmarshal error"),
Under: mustContain("under operator JSON at "),
},
"erroneous operator expected")
}
func TestContainsTypeBehind(t *testing.T) {
equalTypes(t, td.Contains("x"), nil)
}
go-testdeep-1.15.0/td/td_delay.go 0000664 0000000 0000000 00000003104 15144170453 0016543 0 ustar 00root root 0000000 0000000 // Copyright (c) 2020, 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"
"sync"
"github.com/maxatome/go-testdeep/internal/ctxerr"
)
type tdDelay struct {
base
operator TestDeep
once sync.Once
delayed func() TestDeep
}
var _ TestDeep = &tdDelay{}
// summary(Delay): delays the operator construction till first use
// input(Delay): all
// Delay operator allows to delay the construction of an operator to
// the time it is used for the first time. Most of the time, it is
// used with helpers. See the example for a very simple use case.
func Delay(delayed func() TestDeep) TestDeep {
d := tdDelay{
base: newBase(3),
delayed: delayed,
}
if delayed == nil {
d.err = ctxerr.OpBad("Delay", "Delay(DELAYED): DELAYED must be non-nil")
}
return &d
}
func (d *tdDelay) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if d.err != nil {
return ctx.CollectError(d.err)
}
op := d.getOperator()
ctx.CurOperator = op // to have correct location
return op.Match(ctx, got)
}
func (d *tdDelay) String() string {
if d.err != nil {
return d.stringError()
}
return d.getOperator().String()
}
func (d *tdDelay) TypeBehind() reflect.Type {
if d.err != nil {
return nil
}
return d.getOperator().TypeBehind()
}
func (d *tdDelay) HandleInvalid() bool {
return d.getOperator().HandleInvalid()
}
func (d *tdDelay) getOperator() TestDeep {
d.once.Do(func() { d.operator = d.delayed() })
return d.operator
}
go-testdeep-1.15.0/td/td_delay_test.go 0000664 0000000 0000000 00000002653 15144170453 0017612 0 ustar 00root root 0000000 0000000 // Copyright (c) 2020, 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 TestDelay(t *testing.T) {
called := 0
op := td.Delay(func() td.TestDeep {
called++
return td.Lt(13)
})
test.EqualInt(t, called, 0)
checkOK(t, 12, op)
test.EqualInt(t, called, 1)
checkOK(t, 12, op)
test.EqualInt(t, called, 1)
delayNil := td.Delay(td.Nil)
checkOK(t, nil, delayNil)
test.EqualStr(t, delayNil.String(), "nil")
checkError(t, 8,
td.Delay(
func() td.TestDeep {
return td.Gt(13)
},
),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("8"),
Expected: mustBe("> 13"),
})
// Bad usage
checkError(t, "never tested",
td.Delay(nil),
expectedError{
Message: mustBe("bad usage of Delay operator"),
Path: mustBe("DATA"),
Summary: mustBe("Delay(DELAYED): DELAYED must be non-nil"),
})
// Erroneous op
test.EqualStr(t, td.Delay(nil).String(), "Delay()")
}
func TestDelayTypeBehind(t *testing.T) {
equalTypes(t, td.Delay(func() td.TestDeep { return td.String("x") }), nil)
equalTypes(t, td.Delay(func() td.TestDeep { return td.Gt(16) }), 42)
// Erroneous op
equalTypes(t, td.Delay(nil), nil)
}
go-testdeep-1.15.0/td/td_empty.go 0000664 0000000 0000000 00000007445 15144170453 0016617 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 emptyBadKind = "array OR chan OR map OR slice OR string OR pointer(s) on them"
type tdEmpty struct {
baseOKNil
}
var _ TestDeep = &tdEmpty{}
// summary(Empty): checks that an array, a channel, a map, a slice or
// a string is empty
// input(Empty): str,array,slice,map,ptr(ptr on array/slice/map/string),chan
// Empty operator checks that an array, a channel, a map, a slice or a
// string is empty. As a special case (non-typed) nil, as well as nil
// channel, map or slice are considered empty.
//
// Note that the compared data can be a pointer (of pointer of pointer
// etc.) on an array, a channel, a map, a slice or a string.
//
// td.Cmp(t, "", td.Empty()) // succeeds
// td.Cmp(t, map[string]bool{}, td.Empty()) // succeeds
// td.Cmp(t, []string{"foo"}, td.Empty()) // fails
func Empty() TestDeep {
return &tdEmpty{
baseOKNil: newBaseOKNil(3),
}
}
// isEmpty returns (isEmpty, kindError) boolean values with only 3
// possible cases:
// - true, false → "got" is empty
// - false, false → "got" is not empty
// - false, true → "got" kind is not compatible with emptiness
func isEmpty(got reflect.Value) (bool, bool) {
switch got.Kind() {
case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String:
return got.Len() == 0, false
case reflect.Ptr:
switch got.Type().Elem().Kind() {
case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice,
reflect.String:
if got.IsNil() {
return true, false
}
fallthrough
case reflect.Ptr:
return isEmpty(got.Elem())
default:
return false, true // bad kind
}
default:
// nil case
if !got.IsValid() {
return true, false
}
return false, true // bad kind
}
}
func (e *tdEmpty) Match(ctx ctxerr.Context, got reflect.Value) (err *ctxerr.Error) {
ok, badKind := isEmpty(got)
if ok {
return nil
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
if badKind {
return ctx.CollectError(ctxerr.BadKind(got, emptyBadKind))
}
return ctx.CollectError(&ctxerr.Error{
Message: "not empty",
Got: got,
Expected: types.RawString("empty"),
})
}
func (e *tdEmpty) String() string {
return "Empty()"
}
type tdNotEmpty struct {
baseOKNil
}
var _ TestDeep = &tdNotEmpty{}
// summary(NotEmpty): checks that an array, a channel, a map, a slice
// or a string is not empty
// input(NotEmpty): str,array,slice,map,ptr(ptr on array/slice/map/string),chan
// NotEmpty operator checks that an array, a channel, a map, a slice
// or a string is not empty. As a special case (non-typed) nil, as
// well as nil channel, map or slice are considered empty.
//
// Note that the compared data can be a pointer (of pointer of pointer
// etc.) on an array, a channel, a map, a slice or a string.
//
// td.Cmp(t, "", td.NotEmpty()) // fails
// td.Cmp(t, map[string]bool{}, td.NotEmpty()) // fails
// td.Cmp(t, []string{"foo"}, td.NotEmpty()) // succeeds
func NotEmpty() TestDeep {
return &tdNotEmpty{
baseOKNil: newBaseOKNil(3),
}
}
func (e *tdNotEmpty) Match(ctx ctxerr.Context, got reflect.Value) (err *ctxerr.Error) {
ok, badKind := isEmpty(got)
if ok {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "empty",
Got: got,
Expected: types.RawString("not empty"),
})
}
if badKind {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(ctxerr.BadKind(got, emptyBadKind))
}
return nil
}
func (e *tdNotEmpty) String() string {
return "NotEmpty()"
}
go-testdeep-1.15.0/td/td_empty_test.go 0000664 0000000 0000000 00000012365 15144170453 0017653 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 TestEmpty(t *testing.T) {
checkOK(t, nil, td.Empty())
checkOK(t, "", td.Empty())
checkOK(t, ([]int)(nil), td.Empty())
checkOK(t, []int{}, td.Empty())
checkOK(t, (map[string]bool)(nil), td.Empty())
checkOK(t, map[string]bool{}, td.Empty())
checkOK(t, (chan int)(nil), td.Empty())
checkOK(t, make(chan int), td.Empty())
checkOK(t, [0]int{}, td.Empty())
type MySlice []int
checkOK(t, MySlice{}, td.Empty())
checkOK(t, &MySlice{}, td.Empty())
l1 := &MySlice{}
l2 := &l1
l3 := &l2
checkOK(t, &l3, td.Empty())
l1 = nil
checkOK(t, &l3, td.Empty())
checkError(t, 12, td.Empty(),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("array OR chan OR map OR slice OR string OR pointer(s) on them"),
})
num := 12
n1 := &num
n2 := &n1
n3 := &n2
checkError(t, &n3, td.Empty(),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("****int"),
Expected: mustBe("array OR chan OR map OR slice OR string OR pointer(s) on them"),
})
n1 = nil
checkError(t, &n3, td.Empty(),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("****int"),
Expected: mustBe("array OR chan OR map OR slice OR string OR pointer(s) on them"),
})
checkError(t, "foobar", td.Empty(),
expectedError{
Message: mustBe("not empty"),
Path: mustBe("DATA"),
Got: mustContain(`"foobar"`),
Expected: mustBe("empty"),
})
checkError(t, []int{1}, td.Empty(),
expectedError{
Message: mustBe("not empty"),
Path: mustBe("DATA"),
Got: mustContain("1"),
Expected: mustBe("empty"),
})
checkError(t, map[string]bool{"foo": true}, td.Empty(),
expectedError{
Message: mustBe("not empty"),
Path: mustBe("DATA"),
Got: mustContain(`"foo": (bool) true`),
Expected: mustBe("empty"),
})
ch := make(chan int, 1)
ch <- 42
checkError(t, ch, td.Empty(),
expectedError{
Message: mustBe("not empty"),
Path: mustBe("DATA"),
Got: mustContain("(chan int)"),
Expected: mustBe("empty"),
})
checkError(t, [3]int{}, td.Empty(),
expectedError{
Message: mustBe("not empty"),
Path: mustBe("DATA"),
Got: mustContain("0"),
Expected: mustBe("empty"),
})
//
// String
test.EqualStr(t, td.Empty().String(), "Empty()")
}
func TestNotEmpty(t *testing.T) {
checkOK(t, "foobar", td.NotEmpty())
checkOK(t, []int{1}, td.NotEmpty())
checkOK(t, map[string]bool{"foo": true}, td.NotEmpty())
checkOK(t, [3]int{}, td.NotEmpty())
ch := make(chan int, 1)
ch <- 42
checkOK(t, ch, td.NotEmpty())
type MySlice []int
checkOK(t, MySlice{1}, td.NotEmpty())
checkOK(t, &MySlice{1}, td.NotEmpty())
l1 := &MySlice{1}
l2 := &l1
l3 := &l2
checkOK(t, &l3, td.NotEmpty())
checkError(t, 12, td.NotEmpty(),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("array OR chan OR map OR slice OR string OR pointer(s) on them"),
})
checkError(t, nil, td.NotEmpty(),
expectedError{
Message: mustBe("empty"),
Path: mustBe("DATA"),
Got: mustContain("nil"),
Expected: mustBe("not empty"),
})
checkError(t, "", td.NotEmpty(),
expectedError{
Message: mustBe("empty"),
Path: mustBe("DATA"),
Got: mustContain(`""`),
Expected: mustBe("not empty"),
})
checkError(t, ([]int)(nil), td.NotEmpty(),
expectedError{
Message: mustBe("empty"),
Path: mustBe("DATA"),
Got: mustBe("([]int) "),
Expected: mustBe("not empty"),
})
checkError(t, []int{}, td.NotEmpty(),
expectedError{
Message: mustBe("empty"),
Path: mustBe("DATA"),
Got: mustContain("([]int)"),
Expected: mustBe("not empty"),
})
checkError(t, (map[string]bool)(nil), td.NotEmpty(),
expectedError{
Message: mustBe("empty"),
Path: mustBe("DATA"),
Got: mustContain("(map[string]bool) "),
Expected: mustBe("not empty"),
})
checkError(t, map[string]bool{}, td.NotEmpty(),
expectedError{
Message: mustBe("empty"),
Path: mustBe("DATA"),
Got: mustContain("(map[string]bool)"),
Expected: mustBe("not empty"),
})
checkError(t, (chan int)(nil), td.NotEmpty(),
expectedError{
Message: mustBe("empty"),
Path: mustBe("DATA"),
Got: mustContain("(chan int) "),
Expected: mustBe("not empty"),
})
checkError(t, make(chan int), td.NotEmpty(),
expectedError{
Message: mustBe("empty"),
Path: mustBe("DATA"),
Got: mustContain("(chan int)"),
Expected: mustBe("not empty"),
})
checkError(t, [0]int{}, td.NotEmpty(),
expectedError{
Message: mustBe("empty"),
Path: mustBe("DATA"),
Got: mustContain("([0]int)"),
Expected: mustBe("not empty"),
})
//
// String
test.EqualStr(t, td.NotEmpty().String(), "NotEmpty()")
}
func TestEmptyTypeBehind(t *testing.T) {
equalTypes(t, td.Empty(), nil)
equalTypes(t, td.NotEmpty(), nil)
}
go-testdeep-1.15.0/td/td_error_is.go 0000664 0000000 0000000 00000011505 15144170453 0017275 0 ustar 00root root 0000000 0000000 // 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 (
"errors"
"fmt"
"reflect"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/dark"
"github.com/maxatome/go-testdeep/internal/types"
)
type tdErrorIs struct {
tdSmugglerBase
typeBehind reflect.Type
}
var _ TestDeep = &tdErrorIs{}
func errorToRawString(err error) types.RawString {
if err == nil {
return "nil"
}
return types.RawString(fmt.Sprintf("(%[1]T) %[1]q", err))
}
// summary(ErrorIs): checks the data is an error and matches a wrapped error
// input(ErrorIs): if(error)
// ErrorIs is a smuggler operator. It reports whether any error in an
// error's chain matches expectedError.
//
// _, err := os.Open("/unknown/file")
// td.Cmp(t, err, os.ErrNotExist) // fails
// td.Cmp(t, err, td.ErrorIs(os.ErrNotExist)) // succeeds
//
// err1 := fmt.Errorf("failure1")
// err2 := fmt.Errorf("failure2: %w", err1)
// err3 := fmt.Errorf("failure3: %w", err2)
// err := fmt.Errorf("failure4: %w", err3)
// td.Cmp(t, err, td.ErrorIs(err)) // succeeds
// td.Cmp(t, err, td.ErrorIs(err1)) // succeeds
// td.Cmp(t, err1, td.ErrorIs(err)) // fails
//
// var cerr myError
// td.Cmp(t, err, td.ErrorIs(td.Catch(&cerr, td.String("my error..."))))
//
// td.Cmp(t, err, td.ErrorIs(td.All(
// td.Isa(myError{}),
// td.String("my error..."),
// )))
//
// Behind the scene it uses [errors.Is] function if expectedError is
// an error and [errors.As] function if expectedError is a [TestDeep]
// operator.
//
// Note that like [errors.Is], expectedError can be nil: in this case
// the comparison succeeds only when got is nil too.
//
// See also [CmpError] and [CmpNoError].
func ErrorIs(expectedError any) TestDeep {
e := tdErrorIs{
tdSmugglerBase: newSmugglerBase(expectedError),
}
switch expErr := expectedError.(type) {
case nil:
case error:
e.expectedValue = reflect.ValueOf(expectedError)
case TestDeep:
e.typeBehind = expErr.TypeBehind()
if e.typeBehind == nil {
e.typeBehind = types.Interface
break
}
if !e.typeBehind.Implements(types.Error) &&
e.typeBehind.Kind() != reflect.Interface {
e.err = ctxerr.OpBad("ErrorIs",
"ErrorIs(%[1]s): type %[2]s behind %[1]s operator is not an interface or does not implement error",
expErr.GetLocation().Func, e.typeBehind)
}
default:
e.err = ctxerr.OpBadUsage("ErrorIs",
"(error|TESTDEEP_OPERATOR)", expectedError, 1, false)
}
return &e
}
func (e *tdErrorIs) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if e.err != nil {
return ctx.CollectError(e.err)
}
// nil case
if !got.IsValid() {
// Special case
if !e.expectedValue.IsValid() {
return nil
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "nil value",
Got: types.RawString("nil"),
Expected: types.RawString("anything implementing error interface"),
})
}
gotIf, ok := dark.GetInterface(got, true)
if !ok {
return ctx.CollectError(ctx.CannotCompareError())
}
gotErr, ok := gotIf.(error)
if !ok {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: got.Type().String() + " does not implement error interface",
Got: gotIf,
Expected: types.RawString("anything implementing error interface"),
})
}
if e.isTestDeeper {
target := reflect.New(e.typeBehind)
if !errors.As(gotErr, target.Interface()) {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "type is not found in err's tree",
Got: gotIf,
Expected: types.RawString(e.typeBehind.String()),
})
}
return deepValueEqual(ctx.AddCustomLevel(S(".ErrorIs(%s)", e.typeBehind)),
target.Elem(), e.expectedValue)
}
var expErr error
if e.expectedValue.IsValid() {
expErr = e.expectedValue.Interface().(error)
if errors.Is(gotErr, expErr) {
return nil
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "is not found in err's tree",
Got: errorToRawString(gotErr),
Expected: errorToRawString(expErr),
})
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "is not nil",
Got: errorToRawString(gotErr),
Expected: errorToRawString(expErr),
})
}
func (e *tdErrorIs) String() string {
if e.err != nil {
return e.stringError()
}
if e.isTestDeeper {
return "ErrorIs(" + e.expectedValue.Interface().(TestDeep).String() + ")"
}
if !e.expectedValue.IsValid() {
return "ErrorIs(nil)"
}
return "ErrorIs(" + e.expectedValue.Interface().(error).Error() + ")"
}
func (e *tdErrorIs) HandleInvalid() bool {
return true
}
go-testdeep-1.15.0/td/td_error_is_test.go 0000664 0000000 0000000 00000012730 15144170453 0020335 0 ustar 00root root 0000000 0000000 // 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 (
"fmt"
"io"
"testing"
"github.com/maxatome/go-testdeep/internal/dark"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
type errorIsSimpleErr string
func (e errorIsSimpleErr) Error() string {
return string(e)
}
type errorIsWrappedErr struct {
s string
err error
}
func (e errorIsWrappedErr) Error() string {
if e.err != nil {
return e.s + ": " + e.err.Error()
}
return e.s + ": nil"
}
func (e errorIsWrappedErr) Unwrap() error {
return e.err
}
var _ = []error{errorIsSimpleErr(""), errorIsWrappedErr{}}
func TestErrorIs(t *testing.T) {
insideErr1 := errorIsSimpleErr("failure1")
insideErr2 := errorIsWrappedErr{"failure2", insideErr1}
insideErr3 := errorIsWrappedErr{"failure3", insideErr2}
err := errorIsWrappedErr{"failure4", insideErr3}
checkOK(t, err, td.ErrorIs(err))
checkOK(t, err, td.ErrorIs(insideErr3))
checkOK(t, err, td.ErrorIs(insideErr2))
checkOK(t, err, td.ErrorIs(insideErr1))
checkOK(t, nil, td.ErrorIs(nil))
checkOK(t, err, td.ErrorIs(td.All(
td.Isa(errorIsSimpleErr("")),
td.String("failure1"),
)))
// many errorIsWrappedErr in the err's tree, so only the first
// encountered matches
checkOK(t, err, td.ErrorIs(td.All(
td.Isa(errorIsWrappedErr{}),
td.HasPrefix("failure4"),
)))
// HasPrefix().TypeBehind() always returns nil
// so errors.As() is called with &any, so the toplevel error matches
checkOK(t, err, td.ErrorIs(td.HasPrefix("failure4")))
var errNil error
checkOK(t, &errNil, td.Ptr(td.ErrorIs(nil)))
var inside errorIsSimpleErr
checkOK(t, err, td.ErrorIs(td.Catch(&inside, td.String("failure1"))))
test.EqualStr(t, string(inside), "failure1")
checkError(t, nil, td.ErrorIs(insideErr1),
expectedError{
Path: mustBe("DATA"),
Message: mustBe("nil value"),
Got: mustBe("nil"),
Expected: mustBe("anything implementing error interface"),
})
checkError(t, 45, td.ErrorIs(insideErr1),
expectedError{
Path: mustBe("DATA"),
Message: mustBe("int does not implement error interface"),
Got: mustBe("45"),
Expected: mustBe("anything implementing error interface"),
})
checkError(t, 45, td.ErrorIs(fmt.Errorf("another")),
expectedError{
Path: mustBe("DATA"),
Message: mustBe("int does not implement error interface"),
Got: mustBe("45"),
Expected: mustBe("anything implementing error interface"),
})
checkError(t, err, td.ErrorIs(fmt.Errorf("another")),
expectedError{
Path: mustBe("DATA"),
Message: mustBe("is not found in err's tree"),
Got: mustBe(`(td_test.errorIsWrappedErr) "failure4: failure3: failure2: failure1"`),
Expected: mustBe(`(*errors.errorString) "another"`),
})
checkError(t, err, td.ErrorIs(td.String("nonono")),
expectedError{
Path: mustBe("DATA.ErrorIs(interface {})"),
Message: mustBe("does not match"),
Got: mustBe(`"failure4: failure3: failure2: failure1"`),
Expected: mustBe(`"nonono"`),
})
checkError(t, err, td.ErrorIs(td.Isa(fmt.Errorf("another"))),
expectedError{
Path: mustBe("DATA"),
Message: mustBe("type is not found in err's tree"),
Got: mustBe(`(td_test.errorIsWrappedErr) failure4: failure3: failure2: failure1`),
Expected: mustBe(`*errors.errorString`),
})
checkError(t, err, td.ErrorIs(td.Smuggle(io.ReadAll, td.String("xx"))),
expectedError{
Path: mustBe("DATA"),
Message: mustBe("type is not found in err's tree"),
Got: mustBe(`(td_test.errorIsWrappedErr) failure4: failure3: failure2: failure1`),
Expected: mustBe(`io.Reader`),
})
checkError(t, err, td.ErrorIs(nil),
expectedError{
Path: mustBe("DATA"),
Message: mustBe("is not nil"),
Got: mustBe(`(td_test.errorIsWrappedErr) "failure4: failure3: failure2: failure1"`),
Expected: mustBe(`nil`),
})
// As errors.Is, it does not match
checkError(t, errorIsWrappedErr{"failure", nil}, td.ErrorIs(nil),
expectedError{
Path: mustBe("DATA"),
Message: mustBe("is not nil"),
Got: mustBe(`(td_test.errorIsWrappedErr) "failure: nil"`),
Expected: mustBe(`nil`),
})
checkError(t, err, td.ErrorIs(td.Gt(0)),
expectedError{
Path: mustBe("DATA"),
Message: mustBe("bad usage of ErrorIs operator"),
Summary: mustBe(`ErrorIs(Gt): type int behind Gt operator is not an interface or does not implement error`),
})
type private struct{ err error }
got := private{err: err}
for _, expErr := range []error{err, insideErr3} {
expected := td.Struct(private{}, td.StructFields{"err": td.ErrorIs(expErr)})
if dark.UnsafeDisabled {
checkError(t, got, expected,
expectedError{
Message: mustBe("cannot compare"),
Path: mustBe("DATA.err"),
Summary: mustBe("unexported field that cannot be overridden"),
})
} else {
checkOK(t, got, expected)
}
}
if !dark.UnsafeDisabled {
got = private{}
checkOK(t, got, td.Struct(private{}, td.StructFields{"err": td.ErrorIs(nil)}))
}
//
// String
test.EqualStr(t, td.ErrorIs(insideErr1).String(), "ErrorIs(failure1)")
test.EqualStr(t, td.ErrorIs(nil).String(), "ErrorIs(nil)")
test.EqualStr(t, td.ErrorIs(td.HasPrefix("pipo")).String(),
`ErrorIs(HasPrefix("pipo"))`)
test.EqualStr(t, td.ErrorIs(12).String(), "ErrorIs()")
}
func TestErrorIsTypeBehind(t *testing.T) {
equalTypes(t, td.ErrorIs(fmt.Errorf("another")), nil)
}
go-testdeep-1.15.0/td/td_expected_type.go 0000664 0000000 0000000 00000003531 15144170453 0020313 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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"
)
type tdExpectedType struct {
base
expectedType reflect.Type
isPtr bool
}
func (t *tdExpectedType) errorTypeMismatch(gotType reflect.Type) *ctxerr.Error {
expectedType := t.expectedType
if t.isPtr {
expectedType = reflect.PtrTo(expectedType)
}
return ctxerr.TypeMismatch(gotType, expectedType)
}
func (t *tdExpectedType) checkPtr(ctx ctxerr.Context, pGot *reflect.Value, nilAllowed bool) *ctxerr.Error {
if t.isPtr {
got := *pGot
if got.Kind() != reflect.Ptr {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return t.errorTypeMismatch(got.Type())
}
if !nilAllowed && got.IsNil() {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return &ctxerr.Error{
Message: "values differ",
Got: got,
Expected: types.RawString("non-nil"),
}
}
*pGot = got.Elem()
}
return nil
}
func (t *tdExpectedType) checkType(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if got.Type() != t.expectedType {
if ctx.BeLax && t.expectedType.ConvertibleTo(got.Type()) {
return nil
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
gt := got.Type()
if t.isPtr {
gt = reflect.PtrTo(gt)
}
return t.errorTypeMismatch(gt)
}
return nil
}
func (t *tdExpectedType) TypeBehind() reflect.Type {
if t.err != nil {
return nil
}
if t.isPtr {
return reflect.New(t.expectedType).Type()
}
return t.expectedType
}
func (t *tdExpectedType) expectedTypeStr() string {
if t.isPtr {
return "*" + t.expectedType.String()
}
return t.expectedType.String()
}
go-testdeep-1.15.0/td/td_grep.go 0000664 0000000 0000000 00000026301 15144170453 0016406 0 ustar 00root root 0000000 0000000 // Copyright (c) 2022-2023, 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"
"github.com/maxatome/go-testdeep/internal/util"
)
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, item reflect.Value) (bool, *ctxerr.Error) {
if g.argType == nil {
// g.filter is a TestDeep operator
return deepValueEqualFinalOK(ctx, item, g.filter)
}
// 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, &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, %s)",
g.GetLocation().Func,
g.filter.Interface().(TestDeep),
util.ToString(g.expectedValue))
}
return S("%s(%s, %s)",
g.GetLocation().Func,
g.filter.Type(),
util.ToString(g.expectedValue))
}
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 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], [Last] and [Flatten].
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 ctx.CollectError(rErr)
}
switch got.Kind() {
case reflect.Slice, reflect.Array:
default:
return grepBadKind(ctx, got)
}
const grepped = ""
l := got.Len()
if l == 0 {
return deepValueEqual(ctx.AddCustomLevel(grepped), got, g.expectedValue)
}
outType := got.Type()
if got.Kind() == reflect.Array {
outType = reflect.SliceOf(outType.Elem())
}
out := reflect.MakeSlice(outType, 0, l)
for idx := 0; idx < l; idx++ {
item := got.Index(idx)
ok, rErr := g.matchItem(ctx, item)
if rErr != nil {
if !rErr.User {
ctx = ctx.AddArrayIndex(idx)
}
return ctx.CollectError(rErr)
}
if ok {
out = reflect.Append(out, item)
}
}
return deepValueEqual(ctx.AddCustomLevel(grepped), out, g.expectedValue)
}
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 ctx.CollectError(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, item)
if rErr != nil {
if !rErr.User {
ctx = ctx.AddArrayIndex(idx)
}
return ctx.CollectError(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 ctx.CollectError(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, item)
if rErr != nil {
if !rErr.User {
ctx = ctx.AddArrayIndex(idx)
}
return ctx.CollectError(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()
}
go-testdeep-1.15.0/td/td_grep_test.go 0000664 0000000 0000000 00000051016 15144170453 0017446 0 ustar 00root root 0000000 0000000 // 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"),
})
checkError(t, []any{1, 2, 3}, td.Grep(td.JSON("{"), td.Ignore()),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe(`DATA`),
Summary: mustContain("JSON unmarshal error"),
Under: mustContain("under operator JSON at "),
},
"erroneous operator expected")
checkError(t, []any{1, 2, 3}, td.Grep(td.Ignore(), td.JSON("{")),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustMatch(`DATA`), // always without .Iface
Summary: mustContain("JSON unmarshal error"),
Under: mustContain("under operator JSON at "),
},
"erroneous operator expected")
})
}
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, ([]int) {\n})")
test.EqualStr(t, td.Grep(td.Gt(0), []int{}).String(), "Grep(> 0, ([]int) {\n})")
// 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, "never reached")`),
})
})
}
})
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, "never reached")`),
})
})
}
})
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, 33)")
test.EqualStr(t, td.First(td.Gt(0), 33).String(), "First(> 0, 33)")
// 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, "never reached")`),
})
})
}
})
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, "never reached")`),
})
})
}
})
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, 33)")
test.EqualStr(t, td.Last(td.Gt(0), 33).String(), "Last(> 0, 33)")
// 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)
}
go-testdeep-1.15.0/td/td_ignore.go 0000664 0000000 0000000 00000002073 15144170453 0016734 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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"
)
type tdIgnore struct {
baseOKNil
}
// summary(Ignore): allows to ignore a comparison
// input(Ignore): all
// Ignore operator is always true, whatever data is. It is useful when
// comparing a slice with [Slice] and wanting to ignore some indexes,
// for example (if you don't want to use [SuperSliceOf]). Or comparing
// a struct with [SStruct] and wanting to ignore some fields:
//
// td.Cmp(t, got, td.SStruct(
// Person{
// Name: "John Doe",
// },
// td.StructFields{
// Age: td.Between(40, 45),
// Children: td.Ignore(),
// }),
// )
func Ignore() TestDeep {
return &tdIgnore{
baseOKNil: newBaseOKNil(3),
}
}
func (i *tdIgnore) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
return nil
}
func (i *tdIgnore) String() string {
return "Ignore()"
}
go-testdeep-1.15.0/td/td_ignore_test.go 0000664 0000000 0000000 00000001145 15144170453 0017772 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 TestIgnore(t *testing.T) {
checkOK(t, "any value!", td.Ignore())
checkOK(t, nil, td.Ignore())
checkOK(t, (*int)(nil), td.Ignore())
//
// String
test.EqualStr(t, td.Ignore().String(), "Ignore()")
}
func TestIgnoreTypeBehind(t *testing.T) {
equalTypes(t, td.Ignore(), nil)
}
go-testdeep-1.15.0/td/td_isa.go 0000664 0000000 0000000 00000004600 15144170453 0016223 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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"
)
type tdIsa struct {
tdExpectedType
checkImplement bool
}
var _ TestDeep = &tdIsa{}
// summary(Isa): checks the data type or whether data implements an
// interface or not
// input(Isa): bool,str,int,float,cplx,array,slice,map,struct,ptr,chan,func
// Isa operator checks the data type or whether data implements an
// interface or not.
//
// Typical type checks:
//
// td.Cmp(t, time.Now(), td.Isa(time.Time{})) // succeeds
// td.Cmp(t, time.Now(), td.Isa(&time.Time{})) // fails, as not a *time.Time
// td.Cmp(t, got, td.Isa(map[string]time.Time{}))
//
// For interfaces, it is a bit more complicated, as:
//
// fmt.Stringer(nil)
//
// is not an interface, but just nil… To bypass this golang
// limitation, Isa accepts pointers on interfaces. So checking that
// data implements [fmt.Stringer] interface should be written as:
//
// td.Cmp(t, bytes.Buffer{}, td.Isa((*fmt.Stringer)(nil))) // succeeds
//
// Of course, in the latter case, if checked data type is
// [*fmt.Stringer], Isa will match too (in fact before checking whether
// it implements [fmt.Stringer] or not).
//
// TypeBehind method returns the [reflect.Type] of model.
func Isa(model any) TestDeep {
modelType := reflect.TypeOf(model)
i := tdIsa{
tdExpectedType: tdExpectedType{
base: newBase(3),
expectedType: modelType,
},
}
if modelType == nil {
i.err = ctxerr.OpBad("Isa", "Isa(nil) is not allowed. To check an interface, try Isa((*fmt.Stringer)(nil)), for fmt.Stringer for example")
return &i
}
i.checkImplement = modelType.Kind() == reflect.Ptr &&
modelType.Elem().Kind() == reflect.Interface
return &i
}
func (i *tdIsa) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if i.err != nil {
return ctx.CollectError(i.err)
}
gotType := got.Type()
if gotType == i.expectedType {
return nil
}
if i.checkImplement {
if gotType.Implements(i.expectedType.Elem()) {
return nil
}
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(i.errorTypeMismatch(gotType))
}
func (i *tdIsa) String() string {
if i.err != nil {
return i.stringError()
}
return i.expectedType.String()
}
go-testdeep-1.15.0/td/td_isa_test.go 0000664 0000000 0000000 00000006424 15144170453 0017270 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (
"bytes"
"fmt"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestIsa(t *testing.T) {
gotStruct := MyStruct{
MyStructMid: MyStructMid{
MyStructBase: MyStructBase{
ValBool: true,
},
ValStr: "foobar",
},
ValInt: 123,
}
checkOK(t, &gotStruct, td.Isa(&MyStruct{}))
checkOK(t, (*MyStruct)(nil), td.Isa(&MyStruct{}))
checkOK(t, (*MyStruct)(nil), td.Isa((*MyStruct)(nil)))
checkOK(t, gotStruct, td.Isa(MyStruct{}))
checkOK(t, bytes.NewBufferString("foobar"),
td.Isa((*fmt.Stringer)(nil)),
"checks bytes.NewBufferString() implements fmt.Stringer")
// does bytes.NewBufferString("foobar") implements fmt.Stringer?
checkOK(t, bytes.NewBufferString("foobar"), td.Isa((*fmt.Stringer)(nil)))
checkError(t, &gotStruct, td.Isa(&MyStructBase{}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustContain("*td_test.MyStruct"),
Expected: mustContain("*td_test.MyStructBase"),
})
checkError(t, (*MyStruct)(nil), td.Isa(&MyStructBase{}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustContain("*td_test.MyStruct"),
Expected: mustContain("*td_test.MyStructBase"),
})
checkError(t, gotStruct, td.Isa(&MyStruct{}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustContain("td_test.MyStruct"),
Expected: mustContain("*td_test.MyStruct"),
})
checkError(t, &gotStruct, td.Isa(MyStruct{}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustContain("*td_test.MyStruct"),
Expected: mustContain("td_test.MyStruct"),
})
gotSlice := []int{1, 2, 3}
checkOK(t, gotSlice, td.Isa([]int{}))
checkOK(t, &gotSlice, td.Isa(((*[]int)(nil))))
checkError(t, &gotSlice, td.Isa([]int{}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustContain("*[]int"),
Expected: mustContain("[]int"),
})
checkError(t, gotSlice, td.Isa((*[]int)(nil)),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustContain("[]int"),
Expected: mustContain("*[]int"),
})
checkError(t, gotSlice, td.Isa([1]int{2}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustContain("[]int"),
Expected: mustContain("[1]int"),
})
//
// Bad usage
checkError(t, "never tested",
td.Isa(nil),
expectedError{
Message: mustBe("bad usage of Isa operator"),
Path: mustBe("DATA"),
Summary: mustBe("Isa(nil) is not allowed. To check an interface, try Isa((*fmt.Stringer)(nil)), for fmt.Stringer for example"),
})
//
// String
test.EqualStr(t, td.Isa((*MyStruct)(nil)).String(),
"*td_test.MyStruct")
// Erroneous op
test.EqualStr(t, td.Isa(nil).String(), "Isa()")
}
func TestIsaTypeBehind(t *testing.T) {
equalTypes(t, td.Isa(([]int)(nil)), []int{})
equalTypes(t, td.Isa((*fmt.Stringer)(nil)), (*fmt.Stringer)(nil))
// Erroneous op
equalTypes(t, td.Isa(nil), nil)
}
go-testdeep-1.15.0/td/td_json.go 0000664 0000000 0000000 00000130703 15144170453 0016424 0 ustar 00root root 0000000 0000000 // Copyright (c) 2019-2023, 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 (
"bytes"
ejson "encoding/json"
"errors"
"fmt"
"io"
"os"
"reflect"
"strings"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/dark"
"github.com/maxatome/go-testdeep/internal/flat"
"github.com/maxatome/go-testdeep/internal/json"
"github.com/maxatome/go-testdeep/internal/location"
"github.com/maxatome/go-testdeep/internal/types"
"github.com/maxatome/go-testdeep/internal/util"
)
// forbiddenOpsInJSON contains operators forbidden inside JSON,
// SubJSONOf or SuperJSONOf, optionally with an alternative to help
// the user.
var forbiddenOpsInJSON = map[string]string{
"Array": "literal []",
"Cap": "",
"Catch": "",
"Code": "",
"Delay": "",
"ErrorIs": "",
"Isa": "",
"JSON": "literal JSON",
"Lax": "",
"List": "literal []",
"Map": "literal {}",
"PPtr": "",
"Ptr": "",
"Recv": "",
"SStruct": "",
"Shallow": "",
"Slice": "literal []",
"Smuggle": "",
"String": `literal ""`,
"SubJSONOf": "SubMapOf operator",
"SuperJSONOf": "SuperMapOf operator",
"SuperSliceOf": "All and JSONPointer operators",
"Struct": "",
"Tag": "",
"TruncTime": "",
}
// tdJSONUnmarshaler handles the JSON unmarshaling of JSON, SubJSONOf
// and SuperJSONOf first parameter.
type tdJSONUnmarshaler struct {
location.Location // position of the operator
}
// newJSONUnmarshaler returns a new instance of tdJSONUnmarshaler.
func newJSONUnmarshaler(pos location.Location) tdJSONUnmarshaler {
return tdJSONUnmarshaler{
Location: pos,
}
}
// replaceLocation replaces the location of tdOp by the
// JSON/SubJSONOf/SuperJSONOf one then add the position of the
// operator inside the JSON string.
func (u tdJSONUnmarshaler) replaceLocation(tdOp TestDeep, posInJSON json.Position) {
// The goal, instead of:
// [under operator Len at value.go:476]
// having:
// [under operator Len at line 12:7 (pos 123) inside operator JSON at file.go:23]
// so add ^------------------------------------------^
newPos := u.Location
newPos.Inside = fmt.Sprintf("%s inside operator %s ", posInJSON, u.Func)
newPos.Func = tdOp.GetLocation().Func
tdOp.replaceLocation(newPos)
}
// unmarshal unmarshals expectedJSON using placeholder parameters params.
func (u tdJSONUnmarshaler) unmarshal(expectedJSON any, params []any) (any, *ctxerr.Error) {
var (
err error
b []byte
)
switch data := expectedJSON.(type) {
case string:
// Try to load this file (if it seems it can be a filename and not
// a JSON content)
if strings.HasSuffix(data, ".json") {
// It could be a file name, try to read from it
b, err = os.ReadFile(data)
if err != nil {
return nil, ctxerr.OpBad(u.Func, "JSON file %s cannot be read: %s", data, err)
}
break
}
b = []byte(data)
case []byte:
b = data
case ejson.RawMessage:
b = data
case io.Reader:
b, err = io.ReadAll(data)
if err != nil {
return nil, ctxerr.OpBad(u.Func, "JSON read error: %s", err)
}
default:
return nil, ctxerr.OpBadUsage(
u.Func, "(STRING_JSON|STRING_FILENAME|[]byte|json.RawMessage|io.Reader, ...)",
expectedJSON, 1, false)
}
params = flat.Interfaces(params...)
var byTag map[string]any
for i, p := range params {
if op, ok := p.(*tdTag); ok && op.err == nil {
if byTag[op.tag] != nil {
return nil, ctxerr.OpBad(u.Func, `2 params have the same tag "%s"`, op.tag)
}
if byTag == nil {
byTag = map[string]any{}
}
// Don't keep the tag layer
p = nil
if op.expectedValue.IsValid() {
p = op.expectedValue.Interface()
}
byTag[op.tag] = newJSONNamedPlaceholder(op.tag, p)
}
params[i] = newJSONNumPlaceholder(uint64(i+1), p)
}
final, err := json.Parse(b, json.ParseOpts{
Placeholders: params,
PlaceholdersByName: byTag,
OpFn: u.resolveOp(),
})
if err != nil {
return nil, ctxerr.OpBad(u.Func, "JSON unmarshal error: %s", err)
}
return final, nil
}
// resolveOp returns a closure usable as json.ParseOpts.OpFn.
func (u tdJSONUnmarshaler) resolveOp() func(json.Operator, json.Position) (any, error) {
return func(jop json.Operator, posInJSON json.Position) (any, error) {
op, exists := allOperators[jop.Name]
if !exists {
return nil, fmt.Errorf("unknown operator %s()", jop.Name)
}
if hint, exists := forbiddenOpsInJSON[jop.Name]; exists {
if hint == "" {
return nil, fmt.Errorf("%s() is not usable in JSON()", jop.Name)
}
return nil, fmt.Errorf("%s() is not usable in JSON(), use %s instead",
jop.Name, hint)
}
vfn := reflect.ValueOf(op)
tfn := vfn.Type()
// If some parameters contain a placeholder, dereference it
for i, p := range jop.Params {
if ph, ok := p.(*tdJSONPlaceholder); ok {
jop.Params[i] = ph.expectedValue.Interface()
}
}
// Special cases
var min, max int
switch jop.Name {
case "Between":
min, max = 2, 3
if len(jop.Params) == 3 {
bad := false
switch tp := jop.Params[2].(type) {
case BoundsKind:
// Special case, accept numeric values of Bounds*
// constants, for the case:
// td.JSON(`Between(40, 42, $1)`, td.BoundsInOut)
case string:
switch tp {
case "[]", "BoundsInIn":
jop.Params[2] = BoundsInIn
case "[[", "BoundsInOut":
jop.Params[2] = BoundsInOut
case "]]", "BoundsOutIn":
jop.Params[2] = BoundsOutIn
case "][", "BoundsOutOut":
jop.Params[2] = BoundsOutOut
default:
bad = true
}
default:
bad = true
}
if bad {
return nil, errors.New(`Between() bad 3rd parameter, use "[]", "[[", "]]" or "]["`)
}
}
case "N", "Re":
min, max = 1, 2
case "Sorted":
min, max = 0, -1
case "SubMapOf", "SuperMapOf":
min, max = 1, 1
default:
min = tfn.NumIn()
if tfn.IsVariadic() {
// for All(expected ...any) → min == 1, as All() is a non-sense
max = -1
} else {
max = min
}
}
if len(jop.Params) < min || (max >= 0 && len(jop.Params) > max) {
switch {
case max < 0:
return nil, fmt.Errorf("%s() requires at least one parameter", jop.Name)
case max == 0:
return nil, fmt.Errorf("%s() requires no parameters", jop.Name)
case min == max:
if min == 1 {
return nil, fmt.Errorf("%s() requires only one parameter", jop.Name)
}
return nil, fmt.Errorf("%s() requires %d parameters", jop.Name, min)
default:
return nil, fmt.Errorf("%s() requires %d or %d parameters", jop.Name, min, max)
}
}
var in []reflect.Value
if len(jop.Params) > 0 {
in = make([]reflect.Value, len(jop.Params))
for i, p := range jop.Params {
in[i] = reflect.ValueOf(p)
}
// If the function is variadic, no need to check each param as all
// variadic operators have always a ...any
numCheck := len(in)
if tfn.IsVariadic() {
numCheck = tfn.NumIn() - 1
}
for i, p := range in[:numCheck] {
fpt := tfn.In(i)
if fpt.Kind() != reflect.Interface && p.Type() != fpt {
return nil, fmt.Errorf(
"%s() bad #%d parameter type: %s required but %s received",
jop.Name, i+1,
fpt, p.Type(),
)
}
}
}
tdOp := vfn.Call(in)[0].Interface().(TestDeep)
// let erroneous operators (tdOp.err != nil) pass
// replace the location by the JSON/SubJSONOf/SuperJSONOf one
u.replaceLocation(tdOp, posInJSON)
return newJSONEmbedded(tdOp), nil
}
}
// tdJSONSmuggler is the base type for tdJSONPlaceholder & tdJSONEmbedded.
type tdJSONSmuggler struct {
tdSmugglerBase // ignored by tools/gen_funcs.pl
}
func (s *tdJSONSmuggler) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
vgot, _ := jsonify(ctx, got) // Cannot fail
// Here, vgot type is either a bool, float64, string,
// []any, a map[string]any or simply nil
return s.jsonValueEqual(ctx, vgot)
}
func (s *tdJSONSmuggler) String() string {
return util.ToString(s.expectedValue.Interface())
}
func (s *tdJSONSmuggler) HandleInvalid() bool {
return true
}
func (s *tdJSONSmuggler) TypeBehind() reflect.Type {
return s.internalTypeBehind()
}
// tdJSONPlaceholder is an internal smuggler operator. It represents a
// JSON placeholder in an unmarshaled JSON expected data structure. As $1 in:
//
// td.JSON(`{"foo": $1}`, td.Between(12, 34))
//
// It takes the JSON representation of data and compares it to
// expectedValue.
//
// It does its best to convert back the JSON pointed data to the type
// of expectedValue or to the type behind the expectedValue.
type tdJSONPlaceholder struct {
tdJSONSmuggler
name string
num uint64
}
func newJSONNamedPlaceholder(name string, expectedValue any) TestDeep {
p := tdJSONPlaceholder{
tdJSONSmuggler: tdJSONSmuggler{
tdSmugglerBase: newSmugglerBase(expectedValue, -100), // without location
},
name: name,
}
if !p.isTestDeeper {
p.expectedValue = reflect.ValueOf(expectedValue)
}
return &p
}
func newJSONNumPlaceholder(num uint64, expectedValue any) TestDeep {
p := tdJSONPlaceholder{
tdJSONSmuggler: tdJSONSmuggler{
tdSmugglerBase: newSmugglerBase(expectedValue, -100), // without location
},
num: num,
}
if !p.isTestDeeper {
p.expectedValue = reflect.ValueOf(expectedValue)
}
return &p
}
func (p *tdJSONPlaceholder) MarshalJSON() ([]byte, error) {
if !p.isTestDeeper {
var expected any
if p.expectedValue.IsValid() {
expected = p.expectedValue.Interface()
}
return ejson.Marshal(expected)
}
var b bytes.Buffer
if p.num == 0 {
fmt.Fprintf(&b, `"$%s"`, p.name)
} else {
fmt.Fprintf(&b, `"$%d"`, p.num)
}
b.WriteString(` /* `)
indent := "\n" + strings.Repeat(" ", b.Len())
b.WriteString(strings.ReplaceAll(p.String(), "\n", indent))
b.WriteString(` */`)
return b.Bytes(), nil
}
// tdJSONEmbedded represents a MarshalJSON'able operator. As Between() in:
//
// td.JSON(`{"foo": Between(12, 34)}`)
//
// tdSmugglerBase always contains a TestDeep operator, newJSONEmbedded()
// ensures that.
//
// It does its best to convert back the JSON pointed data to the type
// of the type behind the expectedValue (which is always a TestDeep
// operator).
type tdJSONEmbedded struct {
tdJSONSmuggler
}
func newJSONEmbedded(tdOp TestDeep) TestDeep {
e := tdJSONEmbedded{
tdJSONSmuggler: tdJSONSmuggler{
tdSmugglerBase: newSmugglerBase(tdOp, -100), // without location
},
}
return &e
}
func (e *tdJSONEmbedded) MarshalJSON() ([]byte, error) {
return []byte(e.String()), nil
}
// tdJSON is the JSON operator.
type tdJSON struct {
baseOKNil
expected reflect.Value
}
var _ TestDeep = &tdJSON{}
func gotViaJSON(ctx ctxerr.Context, pGot *reflect.Value) *ctxerr.Error {
got, err := jsonify(ctx, *pGot)
if err != nil {
return err
}
*pGot = reflect.ValueOf(got)
return nil
}
func jsonify(ctx ctxerr.Context, got reflect.Value) (any, *ctxerr.Error) {
gotIf, ok := dark.GetInterface(got, true)
if !ok {
return nil, ctx.CannotCompareError()
}
b, err := ejson.Marshal(gotIf)
if err != nil {
if ctx.BooleanError {
return nil, ctxerr.BooleanError
}
return nil, &ctxerr.Error{
Message: "json.Marshal failed",
Summary: ctxerr.NewSummary(err.Error()),
}
}
// As Marshal succeeded, Unmarshal in an any cannot fail
var vgot any
ejson.Unmarshal(b, &vgot) //nolint: errcheck
return vgot, nil
}
// summary(JSON): compares against JSON representation
// input(JSON): nil,bool,str,int,float,array,slice,map,struct,ptr
// JSON operator allows to compare the JSON representation of data
// against expectedJSON. expectedJSON can be a:
//
// - string containing JSON data like `{"fullname":"Bob","age":42}`
// - string containing a JSON filename, ending with ".json" (its
// content is [os.ReadFile] before unmarshaling)
// - []byte containing JSON data
// - [encoding/json.RawMessage] containing JSON data
// - [io.Reader] stream containing JSON data (is [io.ReadAll]
// before unmarshaling)
//
// expectedJSON JSON value can contain placeholders. The params
// are for any placeholder parameters in expectedJSON. params can
// contain [TestDeep] operators as well as raw values. A placeholder can
// be numeric like $2 or named like $name and always references an
// item in params.
//
// Numeric placeholders reference the n'th "operators" item (starting
// at 1). Named placeholders are used with [Tag] operator as follows:
//
// td.Cmp(t, gotValue,
// td.JSON(`{"fullname": $name, "age": $2, "gender": $3}`,
// td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name
// td.Between(41, 43), // matches only $2
// "male")) // matches only $3
//
// Note that placeholders can be double-quoted as in:
//
// td.Cmp(t, gotValue,
// td.JSON(`{"fullname": "$name", "age": "$2", "gender": "$3"}`,
// td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name
// td.Between(41, 43), // matches only $2
// "male")) // matches only $3
//
// It makes no difference whatever the underlying type of the replaced
// item is (= double quoting a placeholder matching a number is not a
// problem). It is just a matter of taste, double-quoting placeholders
// can be preferred when the JSON data has to conform to the JSON
// specification, like when used in a ".json" file.
//
// JSON does its best to convert back the JSON corresponding to a
// placeholder to the type of the placeholder or, if the placeholder
// is an operator, to the type behind the operator. Allowing to do
// things like:
//
// td.Cmp(t, gotValue, td.JSON(`{"foo":$1}`, []int{1, 2, 3, 4}))
// td.Cmp(t, gotValue,
// td.JSON(`{"foo":$1}`, []any{1, 2, td.Between(2, 4), 4}))
// td.Cmp(t, gotValue, td.JSON(`{"foo":$1}`, td.Between(27, 32)))
//
// Of course, it does this conversion only if the expected type can be
// guessed. In the case the conversion cannot occur, data is compared
// as is, in its freshly unmarshaled JSON form (so as bool, float64,
// string, []any, map[string]any or simply nil).
//
// Note expectedJSON can be a []byte, an [encoding/json.RawMessage], a
// JSON filename or a [io.Reader]:
//
// td.Cmp(t, gotValue, td.JSON("file.json", td.Between(12, 34)))
// td.Cmp(t, gotValue, td.JSON([]byte(`[1, $1, 3]`), td.Between(12, 34)))
// td.Cmp(t, gotValue, td.JSON(osFile, td.Between(12, 34)))
//
// A JSON filename ends with ".json".
//
// To avoid a legit "$" string prefix causes a bad placeholder error,
// just double it to escape it. Note it is only needed when the "$" is
// the first character of a string:
//
// td.Cmp(t, gotValue,
// td.JSON(`{"fullname": "$name", "details": "$$info", "age": $2}`,
// td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name
// td.Between(41, 43))) // matches only $2
//
// For the "details" key, the raw value "$info" is expected, no
// placeholders are involved here.
//
// Note that [Lax] mode is automatically enabled by JSON operator to
// simplify numeric tests.
//
// Comments can be embedded in JSON data:
//
// td.Cmp(t, gotValue,
// td.JSON(`
// {
// // A guy properties:
// "fullname": "$name", // The full name of the guy
// "details": "$$info", // Literally "$info", thanks to "$" escape
// "age": $2 /* The age of the guy:
// - placeholder unquoted, but could be without
// any change
// - to demonstrate a multi-lines comment */
// }`,
// td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name
// td.Between(41, 43))) // matches only $2
//
// Comments, like in go, have 2 forms. To quote the Go language specification:
// - line comments start with the character sequence // and stop at the
// end of the line.
// - multi-lines comments start with the character sequence /* and stop
// with the first subsequent character sequence */.
//
// Other JSON divergences:
// - ',' can precede a '}' or a ']' (as in go);
// - strings can contain non-escaped \n, \r and \t;
// - raw strings are accepted (r{raw}, r!raw!, …), see below;
// - int_lit & float_lit numbers as defined in go spec are accepted;
// - numbers can be prefixed by '+'.
//
// Most operators can be directly embedded in JSON without requiring
// any placeholder. If an operators does not take any parameter, the
// parenthesis can be omitted.
//
// td.Cmp(t, gotValue,
// td.JSON(`
// {
// "fullname": HasPrefix("Foo"),
// "age": Between(41, 43),
// "details": SuperMapOf({
// "address": NotEmpty, // () are optional when no parameters
// "car": Any("Peugeot", "Tesla", "Jeep") // any of these
// })
// }`))
//
// Placeholders can be used anywhere, even in operators parameters as in:
//
// td.Cmp(t, gotValue, td.JSON(`{"fullname": HasPrefix($1)}`, "Zip"))
//
// A few notes about operators embedding:
// - [SubMapOf] and [SuperMapOf] take only one parameter, a JSON object;
// - 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], [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], [Sort], [Sorted], [SubBagOf],
// [SubMapOf], [SubSetOf], [SuperBagOf], [SuperMapOf],
// [SuperSetOf], [Values] and [Zero].
//
// It is also possible to embed operators in JSON strings. This way,
// the JSON specification can be fulfilled. To avoid collision with
// possible strings, just prefix the first operator name with
// "$^". The previous example becomes:
//
// td.Cmp(t, gotValue,
// td.JSON(`
// {
// "fullname": "$^HasPrefix(\"Foo\")",
// "age": "$^Between(41, 43)",
// "details": "$^SuperMapOf({
// \"address\": NotEmpty, // () are optional when no parameters
// \"car\": Any(\"Peugeot\", \"Tesla\", \"Jeep\") // any of these
// })"
// }`))
//
// As you can see, in this case, strings in strings have to be
// escaped. Fortunately, newlines are accepted, but unfortunately they
// are forbidden by JSON specification. To avoid too much escaping,
// raw strings are accepted. A raw string is a "r" followed by a
// delimiter, the corresponding delimiter closes the string. The
// following raw strings are all the same as "foo\\bar(\"zip\")!":
// - r'foo\bar"zip"!'
// - r,foo\bar"zip"!,
// - r%foo\bar"zip"!%
// - r(foo\bar("zip")!)
// - r{foo\bar("zip")!}
// - r[foo\bar("zip")!]
// - r
//
// So non-bracketing delimiters use the same character before and
// after, but the 4 sorts of ASCII brackets (round, angle, square,
// curly) all nest: r[x[y]z] equals "x[y]z". The end delimiter cannot
// be escaped.
//
// With raw strings, the previous example becomes:
//
// td.Cmp(t, gotValue,
// td.JSON(`
// {
// "fullname": "$^HasPrefix(r)",
// "age": "$^Between(41, 43)",
// "details": "$^SuperMapOf({
// r: NotEmpty, // () are optional when no parameters
// r: Any(r, r, r) // any of these
// })"
// }`))
//
// Note that raw strings are accepted anywhere, not only in original
// JSON strings.
//
// To be complete, $^ can prefix an operator even outside a
// string. This is accepted for compatibility purpose as the first
// operator embedding feature used this way to embed some operators.
//
// So the following calls are all equivalent:
//
// td.Cmp(t, gotValue, td.JSON(`{"id": $1}`, td.NotZero()))
// td.Cmp(t, gotValue, td.JSON(`{"id": NotZero}`))
// td.Cmp(t, gotValue, td.JSON(`{"id": NotZero()}`))
// td.Cmp(t, gotValue, td.JSON(`{"id": $^NotZero}`))
// td.Cmp(t, gotValue, td.JSON(`{"id": $^NotZero()}`))
// td.Cmp(t, gotValue, td.JSON(`{"id": "$^NotZero"}`))
// td.Cmp(t, gotValue, td.JSON(`{"id": "$^NotZero()"}`))
//
// As for placeholders, there is no differences between $^NotZero and
// "$^NotZero".
//
// Tip: when an [io.Reader] is expected to contain JSON data, it
// cannot be tested directly, but using the [Smuggle] operator simply
// solves the problem:
//
// var body io.Reader
// // …
// td.Cmp(t, body, td.Smuggle(json.RawMessage{}, td.JSON(`{"foo":1}`)))
// // or equally
// td.Cmp(t, body, td.Smuggle(json.RawMessage(nil), td.JSON(`{"foo":1}`)))
//
// [Smuggle] reads from body into an [encoding/json.RawMessage] then
// this buffer is unmarshaled by JSON operator before the comparison.
//
// TypeBehind method returns the [reflect.Type] of the expectedJSON
// once JSON unmarshaled. So it can be bool, string, float64, []any,
// map[string]any or any in case expectedJSON is "null".
//
// See also [JSONPointer], [SubJSONOf] and [SuperJSONOf].
func JSON(expectedJSON any, params ...any) TestDeep {
j := &tdJSON{
baseOKNil: newBaseOKNil(3),
}
v, err := newJSONUnmarshaler(j.GetLocation()).unmarshal(expectedJSON, params)
if err != nil {
j.err = err
} else {
j.expected = reflect.ValueOf(v)
}
return j
}
func (j *tdJSON) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if j.err != nil {
return ctx.CollectError(j.err)
}
err := gotViaJSON(ctx, &got)
if err != nil {
return ctx.CollectError(err)
}
ctx.BeLax = true
return deepValueEqual(ctx, got, j.expected)
}
func (j *tdJSON) String() string {
if j.err != nil {
return j.stringError()
}
return jsonStringify("JSON", j.expected)
}
func jsonStringify(opName string, v reflect.Value) string {
if !v.IsValid() {
return "JSON(null)"
}
var b bytes.Buffer
b.WriteString(opName)
b.WriteByte('(')
json.AppendMarshal(&b, v.Interface(), len(opName)+1) //nolint: errcheck
b.WriteByte(')')
return b.String()
}
func (j *tdJSON) TypeBehind() reflect.Type {
if j.err != nil {
return nil
}
if j.expected.IsValid() {
// In case we have an operator at the root, delegate it the call
if tdOp, ok := j.expected.Interface().(TestDeep); ok {
return tdOp.TypeBehind()
}
return j.expected.Type()
}
return types.Interface
}
type tdMapJSON struct {
tdMap
expected reflect.Value
}
var _ TestDeep = &tdMapJSON{}
// summary(SubJSONOf): compares struct or map against JSON
// representation but with potentially some exclusions
// input(SubJSONOf): map,struct,ptr(ptr on map/struct)
// SubJSONOf operator allows to compare the JSON representation of
// data against expectedJSON. Unlike [JSON] operator, marshaled data
// must be a JSON object/map (aka {…}). expectedJSON can be a:
//
// - string containing JSON data like `{"fullname":"Bob","age":42}`
// - string containing a JSON filename, ending with ".json" (its
// content is [os.ReadFile] before unmarshaling)
// - []byte containing JSON data
// - [encoding/json.RawMessage] containing JSON data
// - [io.Reader] stream containing JSON data (is [io.ReadAll] before
// unmarshaling)
//
// JSON data contained in expectedJSON must be a JSON object/map
// (aka {…}) too. During a match, each expected entry should match in
// the compared map. But some expected entries can be missing from the
// compared map.
//
// type MyStruct struct {
// Name string `json:"name"`
// Age int `json:"age"`
// }
// got := MyStruct{
// Name: "Bob",
// Age: 42,
// }
// td.Cmp(t, got, td.SubJSONOf(`{"name": "Bob", "age": 42, "city": "NY"}`)) // succeeds
// td.Cmp(t, got, td.SubJSONOf(`{"name": "Bob", "zip": 666}`)) // fails, extra "age"
//
// expectedJSON JSON value can contain placeholders. The params
// are for any placeholder parameters in expectedJSON. params can
// contain [TestDeep] operators as well as raw values. A placeholder can
// be numeric like $2 or named like $name and always references an
// item in params.
//
// Numeric placeholders reference the n'th "operators" item (starting
// at 1). Named placeholders are used with [Tag] operator as follows:
//
// td.Cmp(t, gotValue,
// td.SubJSONOf(`{"fullname": $name, "age": $2, "gender": $3}`,
// td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name
// td.Between(41, 43), // matches only $2
// "male")) // matches only $3
//
// Note that placeholders can be double-quoted as in:
//
// td.Cmp(t, gotValue,
// td.SubJSONOf(`{"fullname": "$name", "age": "$2", "gender": "$3"}`,
// td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name
// td.Between(41, 43), // matches only $2
// "male")) // matches only $3
//
// It makes no difference whatever the underlying type of the replaced
// item is (= double quoting a placeholder matching a number is not a
// problem). It is just a matter of taste, double-quoting placeholders
// can be preferred when the JSON data has to conform to the JSON
// specification, like when used in a ".json" file.
//
// SubJSONOf does its best to convert back the JSON corresponding to a
// placeholder to the type of the placeholder or, if the placeholder
// is an operator, to the type behind the operator. Allowing to do
// things like:
//
// td.Cmp(t, gotValue,
// td.SubJSONOf(`{"foo":$1, "bar": 12}`, []int{1, 2, 3, 4}))
// td.Cmp(t, gotValue,
// td.SubJSONOf(`{"foo":$1, "bar": 12}`, []any{1, 2, td.Between(2, 4), 4}))
// td.Cmp(t, gotValue,
// td.SubJSONOf(`{"foo":$1, "bar": 12}`, td.Between(27, 32)))
//
// Of course, it does this conversion only if the expected type can be
// guessed. In the case the conversion cannot occur, data is compared
// as is, in its freshly unmarshaled JSON form (so as bool, float64,
// string, []any, map[string]any or simply nil).
//
// Note expectedJSON can be a []byte, an [encoding/json.RawMessage], a
// JSON filename or a [io.Reader]:
//
// td.Cmp(t, gotValue, td.SubJSONOf("file.json", td.Between(12, 34)))
// td.Cmp(t, gotValue, td.SubJSONOf([]byte(`[1, $1, 3]`), td.Between(12, 34)))
// td.Cmp(t, gotValue, td.SubJSONOf(osFile, td.Between(12, 34)))
//
// A JSON filename ends with ".json".
//
// To avoid a legit "$" string prefix causes a bad placeholder error,
// just double it to escape it. Note it is only needed when the "$" is
// the first character of a string:
//
// td.Cmp(t, gotValue,
// td.SubJSONOf(`{"fullname": "$name", "details": "$$info", "age": $2}`,
// td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name
// td.Between(41, 43))) // matches only $2
//
// For the "details" key, the raw value "$info" is expected, no
// placeholders are involved here.
//
// Note that [Lax] mode is automatically enabled by SubJSONOf operator to
// simplify numeric tests.
//
// Comments can be embedded in JSON data:
//
// td.Cmp(t, gotValue,
// SubJSONOf(`
// {
// // A guy properties:
// "fullname": "$name", // The full name of the guy
// "details": "$$info", // Literally "$info", thanks to "$" escape
// "age": $2 /* The age of the guy:
// - placeholder unquoted, but could be without
// any change
// - to demonstrate a multi-lines comment */
// }`,
// td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name
// td.Between(41, 43))) // matches only $2
//
// Comments, like in go, have 2 forms. To quote the Go language specification:
// - line comments start with the character sequence // and stop at the
// end of the line.
// - multi-lines comments start with the character sequence /* and stop
// with the first subsequent character sequence */.
//
// Other JSON divergences:
// - ',' can precede a '}' or a ']' (as in go);
// - strings can contain non-escaped \n, \r and \t;
// - raw strings are accepted (r{raw}, r!raw!, …), see below;
// - int_lit & float_lit numbers as defined in go spec are accepted;
// - numbers can be prefixed by '+'.
//
// Most operators can be directly embedded in SubJSONOf without requiring
// any placeholder. If an operators does not take any parameter, the
// parenthesis can be omitted.
//
// td.Cmp(t, gotValue,
// td.SubJSONOf(`
// {
// "fullname": HasPrefix("Foo"),
// "age": Between(41, 43),
// "details": SuperMapOf({
// "address": NotEmpty, // () are optional when no parameters
// "car": Any("Peugeot", "Tesla", "Jeep") // any of these
// })
// }`))
//
// Placeholders can be used anywhere, even in operators parameters as in:
//
// td.Cmp(t, gotValue,
// td.SubJSONOf(`{"fullname": HasPrefix($1), "bar": 42}`, "Zip"))
//
// A few notes about operators embedding:
// - [SubMapOf] and [SuperMapOf] take only one parameter, a JSON object;
// - 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], [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], [Sort], [Sorted], [SubBagOf],
// [SubMapOf], [SubSetOf], [SuperBagOf], [SuperMapOf],
// [SuperSetOf], [Values] and [Zero].
//
// It is also possible to embed operators in JSON strings. This way,
// the JSON specification can be fulfilled. To avoid collision with
// possible strings, just prefix the first operator name with
// "$^". The previous example becomes:
//
// td.Cmp(t, gotValue,
// td.SubJSONOf(`
// {
// "fullname": "$^HasPrefix(\"Foo\")",
// "age": "$^Between(41, 43)",
// "details": "$^SuperMapOf({
// \"address\": NotEmpty, // () are optional when no parameters
// \"car\": Any(\"Peugeot\", \"Tesla\", \"Jeep\") // any of these
// })"
// }`))
//
// As you can see, in this case, strings in strings have to be
// escaped. Fortunately, newlines are accepted, but unfortunately they
// are forbidden by JSON specification. To avoid too much escaping,
// raw strings are accepted. A raw string is a "r" followed by a
// delimiter, the corresponding delimiter closes the string. The
// following raw strings are all the same as "foo\\bar(\"zip\")!":
// - r'foo\bar"zip"!'
// - r,foo\bar"zip"!,
// - r%foo\bar"zip"!%
// - r(foo\bar("zip")!)
// - r{foo\bar("zip")!}
// - r[foo\bar("zip")!]
// - r
//
// So non-bracketing delimiters use the same character before and
// after, but the 4 sorts of ASCII brackets (round, angle, square,
// curly) all nest: r[x[y]z] equals "x[y]z". The end delimiter cannot
// be escaped.
//
// With raw strings, the previous example becomes:
//
// td.Cmp(t, gotValue,
// td.SubJSONOf(`
// {
// "fullname": "$^HasPrefix(r)",
// "age": "$^Between(41, 43)",
// "details": "$^SuperMapOf({
// r: NotEmpty, // () are optional when no parameters
// r: Any(r, r, r) // any of these
// })"
// }`))
//
// Note that raw strings are accepted anywhere, not only in original
// JSON strings.
//
// To be complete, $^ can prefix an operator even outside a
// string. This is accepted for compatibility purpose as the first
// operator embedding feature used this way to embed some operators.
//
// So the following calls are all equivalent:
//
// td.Cmp(t, gotValue, td.SubJSONOf(`{"id": $1}`, td.NotZero()))
// td.Cmp(t, gotValue, td.SubJSONOf(`{"id": NotZero}`))
// td.Cmp(t, gotValue, td.SubJSONOf(`{"id": NotZero()}`))
// td.Cmp(t, gotValue, td.SubJSONOf(`{"id": $^NotZero}`))
// td.Cmp(t, gotValue, td.SubJSONOf(`{"id": $^NotZero()}`))
// td.Cmp(t, gotValue, td.SubJSONOf(`{"id": "$^NotZero"}`))
// td.Cmp(t, gotValue, td.SubJSONOf(`{"id": "$^NotZero()"}`))
//
// As for placeholders, there is no differences between $^NotZero and
// "$^NotZero".
//
// Tip: when an [io.Reader] is expected to contain JSON data, it
// cannot be tested directly, but using the [Smuggle] operator simply
// solves the problem:
//
// var body io.Reader
// // …
// td.Cmp(t, body, td.Smuggle(json.RawMessage{}, td.SubJSONOf(`{"foo":1,"bar":2}`)))
// // or equally
// td.Cmp(t, body, td.Smuggle(json.RawMessage(nil), td.SubJSONOf(`{"foo":1,"bar":2}`)))
//
// [Smuggle] reads from body into an [encoding/json.RawMessage] then
// this buffer is unmarshaled by SubJSONOf operator before the comparison.
//
// TypeBehind method returns the map[string]any type.
//
// See also [JSON], [JSONPointer] and [SuperJSONOf].
func SubJSONOf(expectedJSON any, params ...any) TestDeep {
m := &tdMapJSON{
tdMap: tdMap{
tdExpectedType: tdExpectedType{
base: newBase(3),
expectedType: reflect.TypeOf((map[string]any)(nil)),
},
kind: subMap,
},
}
v, err := newJSONUnmarshaler(m.GetLocation()).unmarshal(expectedJSON, params)
if err != nil {
m.err = err
return m
}
_, ok := v.(map[string]any)
if !ok {
m.err = ctxerr.OpBad("SubJSONOf", "SubJSONOf() only accepts JSON objects {…}")
return m
}
m.expected = reflect.ValueOf(v)
m.populateExpectedEntries(nil, m.expected)
return m
}
// summary(SuperJSONOf): compares struct or map against JSON
// representation but with potentially extra entries
// input(SuperJSONOf): map,struct,ptr(ptr on map/struct)
// SuperJSONOf operator allows to compare the JSON representation of
// data against expectedJSON. Unlike JSON operator, marshaled data
// must be a JSON object/map (aka {…}). expectedJSON can be a:
//
// - string containing JSON data like `{"fullname":"Bob","age":42}`
// - string containing a JSON filename, ending with ".json" (its
// content is [os.ReadFile] before unmarshaling)
// - []byte containing JSON data
// - [encoding/json.RawMessage] containing JSON data
// - [io.Reader] stream containing JSON data (is [io.ReadAll] before
// unmarshaling)
//
// JSON data contained in expectedJSON must be a JSON object/map
// (aka {…}) too. During a match, each expected entry should match in
// the compared map. But some entries in the compared map may not be
// expected.
//
// type MyStruct struct {
// Name string `json:"name"`
// Age int `json:"age"`
// City string `json:"city"`
// }
// got := MyStruct{
// Name: "Bob",
// Age: 42,
// City: "TestCity",
// }
// td.Cmp(t, got, td.SuperJSONOf(`{"name": "Bob", "age": 42}`)) // succeeds
// td.Cmp(t, got, td.SuperJSONOf(`{"name": "Bob", "zip": 666}`)) // fails, miss "zip"
//
// expectedJSON JSON value can contain placeholders. The params are
// for any placeholder parameters in expectedJSON. params can contain
// [TestDeep] operators as well as raw values. A placeholder can be
// numeric like $2 or named like $name and always references an item
// in params.
//
// Numeric placeholders reference the n'th "operators" item (starting
// at 1). Named placeholders are used with [Tag] operator as follows:
//
// td.Cmp(t, gotValue,
// SuperJSONOf(`{"fullname": $name, "age": $2, "gender": $3}`,
// td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name
// td.Between(41, 43), // matches only $2
// "male")) // matches only $3
//
// Note that placeholders can be double-quoted as in:
//
// td.Cmp(t, gotValue,
// td.SuperJSONOf(`{"fullname": "$name", "age": "$2", "gender": "$3"}`,
// td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name
// td.Between(41, 43), // matches only $2
// "male")) // matches only $3
//
// It makes no difference whatever the underlying type of the replaced
// item is (= double quoting a placeholder matching a number is not a
// problem). It is just a matter of taste, double-quoting placeholders
// can be preferred when the JSON data has to conform to the JSON
// specification, like when used in a ".json" file.
//
// SuperJSONOf does its best to convert back the JSON corresponding to a
// placeholder to the type of the placeholder or, if the placeholder
// is an operator, to the type behind the operator. Allowing to do
// things like:
//
// td.Cmp(t, gotValue,
// td.SuperJSONOf(`{"foo":$1}`, []int{1, 2, 3, 4}))
// td.Cmp(t, gotValue,
// td.SuperJSONOf(`{"foo":$1}`, []any{1, 2, td.Between(2, 4), 4}))
// td.Cmp(t, gotValue,
// td.SuperJSONOf(`{"foo":$1}`, td.Between(27, 32)))
//
// Of course, it does this conversion only if the expected type can be
// guessed. In the case the conversion cannot occur, data is compared
// as is, in its freshly unmarshaled JSON form (so as bool, float64,
// string, []any, map[string]any or simply nil).
//
// Note expectedJSON can be a []byte, an [encoding/json.RawMessage], a
// JSON filename or a [io.Reader]:
//
// td.Cmp(t, gotValue, td.SuperJSONOf("file.json", td.Between(12, 34)))
// td.Cmp(t, gotValue, td.SuperJSONOf([]byte(`[1, $1, 3]`), td.Between(12, 34)))
// td.Cmp(t, gotValue, td.SuperJSONOf(osFile, td.Between(12, 34)))
//
// A JSON filename ends with ".json".
//
// To avoid a legit "$" string prefix causes a bad placeholder error,
// just double it to escape it. Note it is only needed when the "$" is
// the first character of a string:
//
// td.Cmp(t, gotValue,
// td.SuperJSONOf(`{"fullname": "$name", "details": "$$info", "age": $2}`,
// td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name
// td.Between(41, 43))) // matches only $2
//
// For the "details" key, the raw value "$info" is expected, no
// placeholders are involved here.
//
// Note that [Lax] mode is automatically enabled by SuperJSONOf operator to
// simplify numeric tests.
//
// Comments can be embedded in JSON data:
//
// td.Cmp(t, gotValue,
// td.SuperJSONOf(`
// {
// // A guy properties:
// "fullname": "$name", // The full name of the guy
// "details": "$$info", // Literally "$info", thanks to "$" escape
// "age": $2 /* The age of the guy:
// - placeholder unquoted, but could be without
// any change
// - to demonstrate a multi-lines comment */
// }`,
// td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name
// td.Between(41, 43))) // matches only $2
//
// Comments, like in go, have 2 forms. To quote the Go language specification:
// - line comments start with the character sequence // and stop at the
// end of the line.
// - multi-lines comments start with the character sequence /* and stop
// with the first subsequent character sequence */.
//
// Other JSON divergences:
// - ',' can precede a '}' or a ']' (as in go);
// - strings can contain non-escaped \n, \r and \t;
// - raw strings are accepted (r{raw}, r!raw!, …), see below;
// - int_lit & float_lit numbers as defined in go spec are accepted;
// - numbers can be prefixed by '+'.
//
// Most operators can be directly embedded in SuperJSONOf without requiring
// any placeholder. If an operators does not take any parameter, the
// parenthesis can be omitted.
//
// td.Cmp(t, gotValue,
// td.SuperJSONOf(`
// {
// "fullname": HasPrefix("Foo"),
// "age": Between(41, 43),
// "details": SuperMapOf({
// "address": NotEmpty, // () are optional when no parameters
// "car": Any("Peugeot", "Tesla", "Jeep") // any of these
// })
// }`))
//
// Placeholders can be used anywhere, even in operators parameters as in:
//
// td.Cmp(t, gotValue, td.SuperJSONOf(`{"fullname": HasPrefix($1)}`, "Zip"))
//
// A few notes about operators embedding:
// - [SubMapOf] and [SuperMapOf] take only one parameter, a JSON object;
// - 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], [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], [Sort], [Sorted], [SubBagOf],
// [SubMapOf], [SubSetOf], [SuperBagOf], [SuperMapOf],
// [SuperSetOf], [Values] and [Zero].
//
// It is also possible to embed operators in JSON strings. This way,
// the JSON specification can be fulfilled. To avoid collision with
// possible strings, just prefix the first operator name with
// "$^". The previous example becomes:
//
// td.Cmp(t, gotValue,
// td.SuperJSONOf(`
// {
// "fullname": "$^HasPrefix(\"Foo\")",
// "age": "$^Between(41, 43)",
// "details": "$^SuperMapOf({
// \"address\": NotEmpty, // () are optional when no parameters
// \"car\": Any(\"Peugeot\", \"Tesla\", \"Jeep\") // any of these
// })"
// }`))
//
// As you can see, in this case, strings in strings have to be
// escaped. Fortunately, newlines are accepted, but unfortunately they
// are forbidden by JSON specification. To avoid too much escaping,
// raw strings are accepted. A raw string is a "r" followed by a
// delimiter, the corresponding delimiter closes the string. The
// following raw strings are all the same as "foo\\bar(\"zip\")!":
// - r'foo\bar"zip"!'
// - r,foo\bar"zip"!,
// - r%foo\bar"zip"!%
// - r(foo\bar("zip")!)
// - r{foo\bar("zip")!}
// - r[foo\bar("zip")!]
// - r
//
// So non-bracketing delimiters use the same character before and
// after, but the 4 sorts of ASCII brackets (round, angle, square,
// curly) all nest: r[x[y]z] equals "x[y]z". The end delimiter cannot
// be escaped.
//
// With raw strings, the previous example becomes:
//
// td.Cmp(t, gotValue,
// td.SuperJSONOf(`
// {
// "fullname": "$^HasPrefix(r)",
// "age": "$^Between(41, 43)",
// "details": "$^SuperMapOf({
// r: NotEmpty, // () are optional when no parameters
// r: Any(r, r, r) // any of these
// })"
// }`))
//
// Note that raw strings are accepted anywhere, not only in original
// JSON strings.
//
// To be complete, $^ can prefix an operator even outside a
// string. This is accepted for compatibility purpose as the first
// operator embedding feature used this way to embed some operators.
//
// So the following calls are all equivalent:
//
// td.Cmp(t, gotValue, td.SuperJSONOf(`{"id": $1}`, td.NotZero()))
// td.Cmp(t, gotValue, td.SuperJSONOf(`{"id": NotZero}`))
// td.Cmp(t, gotValue, td.SuperJSONOf(`{"id": NotZero()}`))
// td.Cmp(t, gotValue, td.SuperJSONOf(`{"id": $^NotZero}`))
// td.Cmp(t, gotValue, td.SuperJSONOf(`{"id": $^NotZero()}`))
// td.Cmp(t, gotValue, td.SuperJSONOf(`{"id": "$^NotZero"}`))
// td.Cmp(t, gotValue, td.SuperJSONOf(`{"id": "$^NotZero()"}`))
//
// As for placeholders, there is no differences between $^NotZero and
// "$^NotZero".
//
// Tip: when an [io.Reader] is expected to contain JSON data, it
// cannot be tested directly, but using the [Smuggle] operator simply
// solves the problem:
//
// var body io.Reader
// // …
// td.Cmp(t, body, td.Smuggle(json.RawMessage{}, td.SuperJSONOf(`{"foo":1}`)))
// // or equally
// td.Cmp(t, body, td.Smuggle(json.RawMessage(nil), td.SuperJSONOf(`{"foo":1}`)))
//
// [Smuggle] reads from body into an [encoding/json.RawMessage] then
// this buffer is unmarshaled by SuperJSONOf operator before the comparison.
//
// TypeBehind method returns the map[string]any type.
//
// See also [JSON], [JSONPointer] and [SubJSONOf].
func SuperJSONOf(expectedJSON any, params ...any) TestDeep {
m := &tdMapJSON{
tdMap: tdMap{
tdExpectedType: tdExpectedType{
base: newBase(3),
expectedType: reflect.TypeOf((map[string]any)(nil)),
},
kind: superMap,
},
}
v, err := newJSONUnmarshaler(m.GetLocation()).unmarshal(expectedJSON, params)
if err != nil {
m.err = err
return m
}
_, ok := v.(map[string]any)
if !ok {
m.err = ctxerr.OpBad("SuperJSONOf", "SuperJSONOf() only accepts JSON objects {…}")
return m
}
m.expected = reflect.ValueOf(v)
m.populateExpectedEntries(nil, m.expected)
return m
}
func (m *tdMapJSON) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if m.err != nil {
return ctx.CollectError(m.err)
}
err := gotViaJSON(ctx, &got)
if err != nil {
return ctx.CollectError(err)
}
// nil case
if !got.IsValid() {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "values differ",
Got: types.RawString("null"),
Expected: types.RawString("non-null"),
})
}
ctx.BeLax = true
return m.match(ctx, got)
}
func (m *tdMapJSON) String() string {
if m.err != nil {
return m.stringError()
}
return jsonStringify(m.GetLocation().Func, m.expected)
}
func (m *tdMapJSON) HandleInvalid() bool {
return true
}
go-testdeep-1.15.0/td/td_json_pointer.go 0000664 0000000 0000000 00000011556 15144170453 0020170 0 ustar 00root root 0000000 0000000 // Copyright (c) 2020-2023, 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"
"strings"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/util"
)
type tdJSONPointer struct {
tdSmugglerBase
pointer string
}
var _ TestDeep = &tdJSONPointer{}
// summary(JSONPointer): compares against JSON representation using a
// JSON pointer
// input(JSONPointer): nil,bool,str,int,float,array,slice,map,struct,ptr
// JSONPointer is a smuggler operator. It takes the JSON
// representation of data, gets the value corresponding to the JSON
// pointer ptr (as [RFC 6901] specifies it) and compares it to
// expectedValue.
//
// [Lax] mode is automatically enabled to simplify numeric tests.
//
// JSONPointer does its best to convert back the JSON pointed data to
// the type of expectedValue or to the type behind the
// expectedValue operator, if it is an operator. Allowing to do
// things like:
//
// type Item struct {
// Val int `json:"val"`
// Next *Item `json:"next"`
// }
// got := Item{Val: 1, Next: &Item{Val: 2, Next: &Item{Val: 3}}}
//
// td.Cmp(t, got, td.JSONPointer("/next/next", Item{Val: 3}))
// td.Cmp(t, got, td.JSONPointer("/next/next", &Item{Val: 3}))
// td.Cmp(t,
// got,
// td.JSONPointer("/next/next",
// td.Struct(Item{}, td.StructFields{"Val": td.Gte(3)})),
// )
//
// got := map[string]int64{"zzz": 42} // 42 is int64 here
// td.Cmp(t, got, td.JSONPointer("/zzz", 42))
// td.Cmp(t, got, td.JSONPointer("/zzz", td.Between(40, 45)))
//
// Of course, it does this conversion only if the expected type can be
// guessed. In the case the conversion cannot occur, data is compared
// as is, in its freshly unmarshaled JSON form (so as bool, float64,
// string, []any, map[string]any or simply nil).
//
// Note that as any [TestDeep] operator can be used as expectedValue,
// [JSON] operator works out of the box:
//
// got := json.RawMessage(`{"foo":{"bar": {"zip": true}}}`)
// td.Cmp(t, got, td.JSONPointer("/foo/bar", td.JSON(`{"zip": true}`)))
//
// It can be used with structs lacking json tags. In this case, fields
// names have to be used in JSON pointer:
//
// type Item struct {
// Val int
// Next *Item
// }
// got := Item{Val: 1, Next: &Item{Val: 2, Next: &Item{Val: 3}}}
//
// td.Cmp(t, got, td.JSONPointer("/Next/Next", Item{Val: 3}))
//
// Contrary to [Smuggle] operator and its fields-path feature, only
// public fields can be followed, as private ones are never (un)marshaled.
//
// There is no JSONHas nor JSONHasnt operators to only check a JSON
// pointer exists or not, but they can easily be emulated:
//
// JSONHas := func(pointer string) td.TestDeep {
// return td.JSONPointer(pointer, td.Ignore())
// }
//
// JSONHasnt := func(pointer string) td.TestDeep {
// return td.Not(td.JSONPointer(pointer, td.Ignore()))
// }
//
// TypeBehind method always returns nil as the expected type cannot be
// guessed from a JSON pointer.
//
// See also [JSON], [SubJSONOf], [SuperJSONOf], [Smuggle] and [Flatten].
//
// [RFC 6901]: https://tools.ietf.org/html/rfc6901
func JSONPointer(ptr string, expectedValue any) TestDeep {
p := tdJSONPointer{
tdSmugglerBase: newSmugglerBase(expectedValue),
pointer: ptr,
}
if !strings.HasPrefix(ptr, "/") && ptr != "" {
p.err = ctxerr.OpBad("JSONPointer", "bad JSON pointer %q", ptr)
return &p
}
if !p.isTestDeeper {
p.expectedValue = reflect.ValueOf(expectedValue)
}
return &p
}
func (p *tdJSONPointer) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if p.err != nil {
return ctx.CollectError(p.err)
}
vgot, eErr := jsonify(ctx, got)
if eErr != nil {
return ctx.CollectError(eErr)
}
vgot, err := util.JSONPointer(vgot, p.pointer)
if err != nil {
if ctx.BooleanError {
return ctxerr.BooleanError
}
pErr := err.(*util.JSONPointerError)
ctx = jsonPointerContext(ctx, pErr.Pointer)
return ctx.CollectError(&ctxerr.Error{
Message: "cannot retrieve value via JSON pointer",
Summary: ctxerr.NewSummary(pErr.Type),
})
}
// Here, vgot type is either a bool, float64, string,
// []any, a map[string]any or simply nil
ctx = jsonPointerContext(ctx, p.pointer)
ctx.BeLax = true
return p.jsonValueEqual(ctx, vgot)
}
func (p *tdJSONPointer) String() string {
if p.err != nil {
return p.stringError()
}
var expected string
switch {
case p.isTestDeeper:
expected = p.expectedValue.Interface().(TestDeep).String()
case p.expectedValue.IsValid():
expected = util.ToString(p.expectedValue.Interface())
default:
expected = "nil"
}
return fmt.Sprintf("JSONPointer(%s, %s)", p.pointer, expected)
}
func (p *tdJSONPointer) HandleInvalid() bool {
return true
}
func jsonPointerContext(ctx ctxerr.Context, pointer string) ctxerr.Context {
return ctx.AddCustomLevel(".JSONPointer<" + pointer + ">")
}
go-testdeep-1.15.0/td/td_json_pointer_test.go 0000664 0000000 0000000 00000020170 15144170453 0021217 0 ustar 00root root 0000000 0000000 // Copyright (c) 2020-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 (
"encoding/json"
"errors"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
type jsonPtrTest int
func (j jsonPtrTest) UnmarshalJSON(b []byte) error {
return errors.New("jsonPtrTest unmarshal custom error")
}
type jsonPtrMap map[string]any
func (j *jsonPtrMap) UnmarshalJSON(b []byte) error {
return json.Unmarshal(b, (*map[string]any)(j))
}
var _ = []json.Unmarshaler{jsonPtrTest(0), &jsonPtrMap{}}
func TestJSONPointer(t *testing.T) {
//
// nil
t.Run("nil", func(t *testing.T) {
checkOK(t, nil, td.JSONPointer("", nil))
checkOK(t, (*int)(nil), td.JSONPointer("", nil))
// Yes encoding/json succeeds to unmarshal nil into an int
checkOK(t, nil, td.JSONPointer("", 0))
checkOK(t, (*int)(nil), td.JSONPointer("", 0))
checkError(t, map[string]int{"foo": 42}, td.JSONPointer("/foo", nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.JSONPointer"),
Got: mustBe(`42.0`),
Expected: mustBe(`nil`),
})
// As encoding/json succeeds to unmarshal nil into an int
checkError(t, map[string]any{"foo": nil}, td.JSONPointer("/foo", 1),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.JSONPointer"),
Got: mustBe(`0`), // as an int is expected, nil becomes 0
Expected: mustBe(`1`),
})
})
//
// Basic types
t.Run("basic types", func(t *testing.T) {
checkOK(t, 123, td.JSONPointer("", 123))
checkOK(t, 123, td.JSONPointer("", td.Between(120, 130)))
checkOK(t, true, td.JSONPointer("", true))
})
//
// More complex type with encoding/json tags
t.Run("complex type with json tags", func(t *testing.T) {
type jpStruct struct {
Slice []string `json:"slice,omitempty"`
Map map[string]*jpStruct `json:"map,omitempty"`
Num int `json:"num"`
Bool bool `json:"bool"`
Str string `json:"str,omitempty"`
}
got := jpStruct{
Slice: []string{"bar", "baz"},
Map: map[string]*jpStruct{
"test": {
Num: 2,
Str: "level2",
},
},
Num: 1,
Bool: true,
Str: "level1",
}
// No filter, should match got or its map representation
checkOK(t, got, td.JSONPointer("",
map[string]any{
"slice": []any{"bar", "baz"},
"map": map[string]any{
"test": map[string]any{
"num": 2,
"str": "level2",
"bool": false,
},
},
"num": int64(1), // should be OK as Lax is enabled
"bool": true,
"str": "level1",
}))
checkOK(t, got, td.JSONPointer("", got))
checkOK(t, got, td.JSONPointer("", &got))
// A specific field
checkOK(t, got, td.JSONPointer("/num", int64(1))) // Lax enabled
checkOK(t, got, td.JSONPointer("/slice/1", "baz"))
checkOK(t, got, td.JSONPointer("/map/test/num", 2))
checkOK(t, got, td.JSONPointer("/map/test/str", td.Contains("vel2")))
checkOK(t, got,
td.JSONPointer("/map", td.JSONPointer("/test", td.JSONPointer("/num", 2))))
checkError(t, got, td.JSONPointer("/zzz/pipo", 666),
expectedError{
Message: mustBe("cannot retrieve value via JSON pointer"),
Path: mustBe("DATA.JSONPointer"),
Summary: mustBe("key not found"),
})
checkError(t, got, td.JSONPointer("/num/pipo", 666),
expectedError{
Message: mustBe("cannot retrieve value via JSON pointer"),
Path: mustBe("DATA.JSONPointer"),
Summary: mustBe("not a map nor an array"),
})
checkError(t, got, td.JSONPointer("/slice/2", "zip"),
expectedError{
Message: mustBe("cannot retrieve value via JSON pointer"),
Path: mustBe("DATA.JSONPointer"),
Summary: mustBe("out of array range"),
})
checkError(t, got, td.JSONPointer("/slice/xxx", "zip"),
expectedError{
Message: mustBe("cannot retrieve value via JSON pointer"),
Path: mustBe("DATA.JSONPointer"),
Summary: mustBe("array but not an index in JSON pointer"),
})
checkError(t, got, td.JSONPointer("/slice/1", "zip"),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.JSONPointer"),
Got: mustBe(`"baz"`),
Expected: mustBe(`"zip"`),
})
// A struct behind a specific field
checkOK(t, got, td.JSONPointer("/map/test", map[string]any{
"num": 2,
"str": "level2",
"bool": false,
}))
checkOK(t, got, td.JSONPointer("/map/test", jpStruct{
Num: 2,
Str: "level2",
}))
checkOK(t, got, td.JSONPointer("/map/test", &jpStruct{
Num: 2,
Str: "level2",
}))
checkOK(t, got, td.JSONPointer("/map/test", td.Struct(&jpStruct{
Num: 2,
Str: "level2",
}, nil)))
})
//
// Complex type without encoding/json tags
t.Run("complex type without json tags", func(t *testing.T) {
type jpStruct struct {
Slice []string
Map map[string]*jpStruct
Num int
Bool bool
Str string
}
got := jpStruct{
Slice: []string{"bar", "baz"},
Map: map[string]*jpStruct{
"test": {
Num: 2,
Str: "level2",
},
},
Num: 1,
Bool: true,
Str: "level1",
}
checkOK(t, got, td.JSONPointer("/Num", 1))
checkOK(t, got, td.JSONPointer("/Slice/1", "baz"))
checkOK(t, got, td.JSONPointer("/Map/test/Num", 2))
checkOK(t, got, td.JSONPointer("/Map/test/Str", td.Contains("vel2")))
})
//
// Chained list
t.Run("Chained list", func(t *testing.T) {
type Item struct {
Val int `json:"val"`
Next *Item `json:"next"`
}
got := Item{Val: 1, Next: &Item{Val: 2, Next: &Item{Val: 3}}}
checkOK(t, got, td.JSONPointer("/next/next", Item{Val: 3}))
checkOK(t, got, td.JSONPointer("/next/next", &Item{Val: 3}))
checkOK(t, got,
td.JSONPointer("/next/next",
td.Struct(Item{}, td.StructFields{"Val": td.Gte(3)})))
checkOK(t, json.RawMessage(`{"foo":{"bar": {"zip": true}}}`),
td.JSONPointer("/foo/bar", td.JSON(`{"zip": true}`)))
})
//
// Lax cases
t.Run("Lax", func(t *testing.T) {
t.Run("json.Unmarshaler", func(t *testing.T) {
got := jsonPtrMap{"x": 123}
checkOK(t, got, td.JSONPointer("", jsonPtrMap{"x": float64(123)}))
checkOK(t, got, td.JSONPointer("", &jsonPtrMap{"x": float64(123)}))
checkOK(t, got, td.JSONPointer("", got))
checkOK(t, got, td.JSONPointer("", &got))
})
t.Run("struct", func(t *testing.T) {
type jpStruct struct {
Num any
}
got := jpStruct{Num: 123}
checkOK(t, got, td.JSONPointer("", jpStruct{Num: float64(123)}))
checkOK(t, jpStruct{Num: got}, td.JSONPointer("/Num", jpStruct{Num: float64(123)}))
checkOK(t, got, td.JSONPointer("", got))
checkOK(t, got, td.JSONPointer("", &got))
expected := int8(123)
checkOK(t, got, td.JSONPointer("/Num", expected))
checkOK(t, got, td.JSONPointer("/Num", &expected))
})
})
//
// Errors
t.Run("errors", func(t *testing.T) {
checkError(t, func() {}, td.JSONPointer("", td.NotNil()),
expectedError{
Message: mustBe("json.Marshal failed"),
Path: mustBe("DATA"),
Summary: mustContain("json: unsupported type"),
})
checkError(t,
map[string]int{"zzz": 42},
td.JSONPointer("/zzz", jsonPtrTest(56)),
expectedError{
Message: mustBe("an error occurred while unmarshalling JSON into td_test.jsonPtrTest"),
Path: mustBe("DATA.JSONPointer"),
Summary: mustBe("jsonPtrTest unmarshal custom error"),
})
})
//
// Bad usage
checkError(t, "never tested",
td.JSONPointer("x", 1234),
expectedError{
Message: mustBe("bad usage of JSONPointer operator"),
Path: mustBe("DATA"),
Summary: mustBe(`bad JSON pointer "x"`),
})
//
// String
test.EqualStr(t, td.JSONPointer("/x", td.Gt(2)).String(),
"JSONPointer(/x, > 2)")
test.EqualStr(t, td.JSONPointer("/x", 2).String(),
"JSONPointer(/x, 2)")
test.EqualStr(t, td.JSONPointer("/x", nil).String(),
"JSONPointer(/x, nil)")
// Erroneous op
test.EqualStr(t, td.JSONPointer("x", 1234).String(), "JSONPointer()")
}
func TestJSONPointerTypeBehind(t *testing.T) {
equalTypes(t, td.JSONPointer("", 42), nil)
// Erroneous op
equalTypes(t, td.JSONPointer("x", 1234), nil)
}
go-testdeep-1.15.0/td/td_json_test.go 0000664 0000000 0000000 00000116517 15144170453 0017472 0 ustar 00root root 0000000 0000000 // Copyright (c) 2019-2023, 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"
"errors"
"os"
"reflect"
"testing"
"time"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
type errReader struct{}
// Read implements io.Reader.
func (r errReader) Read(p []byte) (int, error) {
return 0, errors.New("an error occurred")
}
const (
insideOpJSON = " inside operator JSON at td_json_test.go:"
underOpJSON = "under operator JSON at td_json_test.go:"
)
func TestJSON(t *testing.T) {
type MyStruct struct {
Name string `json:"name"`
Age uint `json:"age"`
Gender string `json:"gender"`
}
//
// nil
checkOK(t, nil, td.JSON(`null`))
checkOK(t, (*int)(nil), td.JSON(`null`))
//
// Basic types
checkOK(t, 123, td.JSON(` 123 `))
checkOK(t, true, td.JSON(` true `))
checkOK(t, false, td.JSON(` false `))
checkOK(t, "foobar", td.JSON(` "foobar" `))
//
// struct
//
got := MyStruct{Name: "Bob", Age: 42, Gender: "male"}
// No placeholder
checkOK(t, got,
td.JSON(`{"name":"Bob","age":42,"gender":"male"}`))
checkOK(t, got, td.JSON(`$1`, got)) // json.Marshal() got for $1
// Numeric placeholders
checkOK(t, got,
td.JSON(`{"name":"$1","age":$2,"gender":$3}`,
"Bob", 42, "male")) // raw values
checkOK(t, got,
td.JSON(`{"name":"$1","age":$2,"gender":"$3"}`,
td.Re(`^Bob`),
td.Between(40, 45),
td.NotEmpty()))
// Same using Flatten
checkOK(t, got,
td.JSON(`{"name":"$1","age":$2,"gender":"$3"}`,
td.Re(`^Bob`),
td.Flatten([]td.TestDeep{td.Between(40, 45), td.NotEmpty()}),
))
// Operators are not JSON marshallable
checkOK(t, got,
td.JSON(`$1`, map[string]any{
"name": td.Re(`^Bob`),
"age": 42,
"gender": td.NotEmpty(),
}))
// Placeholder + unmarshal before comparison
checkOK(t, json.RawMessage(`[1,2,3]`), td.JSON(`$1`, []int{1, 2, 3}))
checkOK(t, json.RawMessage(`{"foo":[1,2,3]}`),
td.JSON(`{"foo":$1}`, []int{1, 2, 3}))
checkOK(t, json.RawMessage(`[1,2,3]`),
td.JSON(`$1`, []any{1, td.Between(1, 3), 3}))
// Tag placeholders
checkOK(t, got,
td.JSON(`{"name":"$name","age":$age,"gender":$gender}`,
// raw values
td.Tag("name", "Bob"), td.Tag("age", 42), td.Tag("gender", "male")))
checkOK(t, got,
td.JSON(`{"name":"$name","age":$age,"gender":"$gender"}`,
td.Tag("name", td.Re(`^Bo`)),
td.Tag("age", td.Between(40, 45)),
td.Tag("gender", td.NotEmpty())))
// Tag placeholders + numeric placeholders
checkOK(t, []MyStruct{got, got},
td.JSON(`[
{"name":"$1","age":$age,"gender":"$3"},
{"name":"$1","age":$2,"gender":"$3"}
]`,
td.Re(`^Bo`), // $1
td.Tag("age", td.Between(40, 45)), // $2
"male")) // $3
// Tag placeholders + operators are not JSON marshallable
checkOK(t, got,
td.JSON(`$all`, td.Tag("all", map[string]any{
"name": td.Re(`^Bob`),
"age": 42,
"gender": td.NotEmpty(),
})))
checkError(t, got,
td.JSON(`{"name":$1, "age":$1, "gender":$1}`,
td.Tag("!!", td.Ignore())),
expectedError{
Message: mustBe("bad usage of Tag operator"),
Summary: mustBe("invalid tag, should match (Letter|_)(Letter|_|Number)*"),
Under: mustContain("under operator Tag"),
})
// Tag placeholders + nil
checkOK(t, nil, td.JSON(`$all`, td.Tag("all", nil)))
// Mixed placeholders + operator
for _, op := range []string{
"NotEmpty",
"NotEmpty()",
"$^NotEmpty",
"$^NotEmpty()",
`"$^NotEmpty"`,
`"$^NotEmpty()"`,
`r<$^NotEmpty>`,
`r<$^NotEmpty()>`,
} {
checkOK(t, got,
td.JSON(`{"name":"$name","age":$1,"gender":`+op+`}`,
td.Tag("age", td.Between(40, 45)),
td.Tag("name", td.Re(`^Bob`))),
"using operator %s", op)
}
checkOK(t, got,
td.JSON(`{"name":Re("^Bo\\w"),"age":Between(40,45),"gender":NotEmpty()}`))
checkOK(t, got,
td.JSON(`
{
"name": All(Re("^Bo\\w"), HasPrefix("Bo"), HasSuffix("ob")),
"age": Between(40,45),
"gender": NotEmpty()
}`))
checkOK(t, got,
td.JSON(`
{
"name": All(Re("^Bo\\w"), HasPrefix("Bo"), HasSuffix("ob")),
"age": Between(40,45),
"gender": NotEmpty
}`))
// Same but operators in strings using "$^"
checkOK(t, got,
td.JSON(`{"name":Re("^Bo\\w"),"age":"$^Between(40,45)","gender":"$^NotEmpty()"}`))
checkOK(t, got, // using classic "" string, so each \ has to be escaped
td.JSON(`
{
"name": "$^All(Re(\"^Bo\\\\w\"), HasPrefix(\"Bo\"), HasSuffix(\"ob\"))",
"age": "$^Between(40,45)",
"gender": "$^NotEmpty()",
}`))
checkOK(t, got, // using raw strings, no escape needed
td.JSON(`
{
"name": "$^All(Re(r(^Bo\\w)), HasPrefix(r{Bo}), HasSuffix(r'ob'))",
"age": "$^Between(40,45)",
"gender": "$^NotEmpty()",
}`))
// …with comments…
checkOK(t, got,
td.JSON(`
// This should be the JSON representation of MyStruct struct
{
// A person:
"name": "$name", // The name of this person
"age": $1, /* The age of this person:
- placeholder unquoted, but could be without
any change
- to demonstrate a multi-lines comment */
"gender": $^NotEmpty // Operator NotEmpty
}`,
td.Tag("age", td.Between(40, 45)),
td.Tag("name", td.Re(`^Bob`))))
before := time.Now()
timeGot := map[string]time.Time{"created_at": time.Now()}
checkOK(t, timeGot,
td.JSON(`{"created_at": Between($1, $2)}`, before, time.Now()))
checkOK(t, timeGot,
td.JSON(`{"created_at": $1}`, td.Between(before, time.Now())))
// Len
checkOK(t, []int{1, 2, 3}, td.JSON(`Len(3)`))
//
// []byte
checkOK(t, got,
td.JSON([]byte(`{"name":"$name","age":$1,"gender":"male"}`),
td.Tag("age", td.Between(40, 45)),
td.Tag("name", td.Re(`^Bob`))))
//
// json.RawMessage
checkOK(t, got,
td.JSON(json.RawMessage(`{"name":"$name","age":$1,"gender":"male"}`),
td.Tag("age", td.Between(40, 45)),
td.Tag("name", td.Re(`^Bob`))))
//
// nil++
checkOK(t, nil, td.JSON(`$1`, nil))
checkOK(t, (*int)(nil), td.JSON(`$1`, td.Nil()))
checkOK(t, nil, td.JSON(`$x`, td.Tag("x", nil)))
checkOK(t, (*int)(nil), td.JSON(`$x`, td.Tag("x", nil)))
checkOK(t, json.RawMessage(`{"foo": null}`), td.JSON(`{"foo": null}`))
checkOK(t,
json.RawMessage(`{"foo": null}`),
td.JSON(`{"foo": $1}`, nil))
checkOK(t,
json.RawMessage(`{"foo": null}`),
td.JSON(`{"foo": $1}`, td.Nil()))
checkOK(t,
json.RawMessage(`{"foo": null}`),
td.JSON(`{"foo": $x}`, td.Tag("x", nil)))
checkOK(t,
json.RawMessage(`{"foo": null}`),
td.JSON(`{"foo": $x}`, td.Tag("x", td.Nil())))
//
// Loading a file
tmpDir := t.TempDir()
filename := tmpDir + "/test.json"
err := os.WriteFile(
filename, []byte(`{"name":$name,"age":$1,"gender":$^NotEmpty}`), 0644)
if err != nil {
t.Fatal(err)
}
checkOK(t, got,
td.JSON(filename,
td.Tag("age", td.Between(40, 45)),
td.Tag("name", td.Re(`^Bob`))))
//
// Reading (a file)
tmpfile, err := os.Open(filename)
if err != nil {
t.Fatal(err)
}
checkOK(t, got,
td.JSON(tmpfile,
td.Tag("age", td.Between(40, 45)),
td.Tag("name", td.Re(`^Bob`))))
tmpfile.Close() //nolint: errcheck
//
// Escaping $ in strings
checkOK(t, "$test", td.JSON(`"$$test"`))
//
// Errors
checkError(t, func() {}, td.JSON(`null`),
expectedError{
Message: mustBe("json.Marshal failed"),
Summary: mustContain("json: unsupported type"),
Under: mustContain(underOpJSON),
})
checkError(t, map[string]string{"zip": "pipo"},
td.All(td.JSON(`SuperMapOf({"zip":$1})`, "bingo")),
expectedError{
Path: mustBe(`DATA`),
Message: mustBe("compared (part 1 of 1)"),
Got: mustBe(`(map[string]string) (len=1) {
(string) (len=3) "zip": (string) (len=4) "pipo"
}`),
Expected: mustBe(`JSON(SuperMapOf(map[string]interface {}{
"zip": "bingo",
}))`,
),
Under: mustContain("under operator All at "),
Origin: &expectedError{
Path: mustBe(`DATA["zip"]`),
Message: mustBe(`values differ`),
Got: mustBe(`"pipo"`),
Expected: mustBe(`"bingo"`),
Under: mustContain("under operator SuperMapOf at line 1:0 (pos 0)" + insideOpJSON),
},
})
checkError(t, map[string]string{"zip": "pipo"},
td.JSON(`SuperMapOf({"zip":$1})`, "bingo"),
expectedError{
Path: mustBe(`DATA["zip"]`),
Message: mustBe("values differ"),
Got: mustBe(`"pipo"`),
Expected: mustBe(`"bingo"`),
Under: mustContain("under operator SuperMapOf at line 1:0 (pos 0)" + insideOpJSON),
})
checkError(t, json.RawMessage(`"pipo:bingo"`),
td.JSON(`Re(r;^pipo:(\w+);, ["bad"])`),
expectedError{
Path: mustBe(`(DATA =~ ^pipo:(\w+))[0]`),
Got: mustBe(`"bingo"`),
Expected: mustBe(`"bad"`),
Under: mustContain("under operator Re at line 1:0 (pos 0)" + insideOpJSON),
})
checkError(t, json.RawMessage(`"pipo:bingo"`),
td.JSON(`Re(r;^pipo:(\w+);, [$1])`, "bad"),
expectedError{
Path: mustBe(`(DATA =~ ^pipo:(\w+))[0]`),
Got: mustBe(`"bingo"`),
Expected: mustBe(`"bad"`),
Under: mustContain("under operator Re at line 1:0 (pos 0)" + insideOpJSON),
})
checkError(t, json.RawMessage(`"pipo:bingo"`),
td.JSON(`Re(r;^pipo:(\w+);, [$param])`, td.Tag("param", "bad")),
expectedError{
Path: mustBe(`(DATA =~ ^pipo:(\w+))[0]`),
Got: mustBe(`"bingo"`),
Expected: mustBe(`"bad"`),
Under: mustContain("under operator Re at line 1:0 (pos 0)" + insideOpJSON),
})
checkError(t, json.RawMessage(`"pipo:bingo"`),
td.JSON(`Re(r;^pipo:(\w+);, Bag($1))`, "bad"),
expectedError{
Path: mustBe(`(DATA =~ ^pipo:(\w+))`),
Summary: mustBe(`Missing item: ("bad")` + "\n" + ` Extra item: ("bingo")`),
Under: mustContain("under operator Bag at line 1:19 (pos 19)" + insideOpJSON),
})
checkError(t, json.RawMessage(`"pipo:bingo"`),
td.JSON(`Re(r;^pipo:(\w+);, Bag($param))`, td.Tag("param", "bad")),
expectedError{
Path: mustBe(`(DATA =~ ^pipo:(\w+))`),
Summary: mustBe(`Missing item: ("bad")` + "\n" + ` Extra item: ("bingo")`),
Under: mustContain("under operator Bag at line 1:19 (pos 19)" + insideOpJSON),
})
//
// Fatal errors
checkError(t, "never tested",
td.JSON("uNkNoWnFiLe.json"),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustContain("JSON file uNkNoWnFiLe.json cannot be read: "),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(42),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: JSON(STRING_JSON|STRING_FILENAME|[]byte|json.RawMessage|io.Reader, ...), but received int as 1st parameter"),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(errReader{}),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe("JSON read error: an error occurred"),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(`pipo`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustContain("JSON unmarshal error: "),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(`[$foo]`,
td.Tag("foo", td.Ignore()),
td.Tag("foo", td.Ignore())),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`2 params have the same tag "foo"`),
Under: mustContain(underOpJSON),
})
checkError(t, []int{42},
td.JSON(`[$1]`, func() {}),
expectedError{
Message: mustBe("an error occurred while unmarshalling JSON into func()"),
Path: mustBe("DATA[0]"),
Summary: mustBe("json: cannot unmarshal number into Go value of type func()"),
Under: mustContain(underOpJSON),
})
checkError(t, []int{42},
td.JSON(`[$foo]`, td.Tag("foo", func() {})),
expectedError{
Message: mustBe("an error occurred while unmarshalling JSON into func()"),
Path: mustBe("DATA[0]"),
Summary: mustBe("json: cannot unmarshal number into Go value of type func()"),
Under: mustContain(underOpJSON),
})
// numeric placeholders
checkError(t, "never tested",
td.JSON(`[1, "$123bad"]`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: invalid numeric placeholder at line 1:5 (pos 5)`),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(`[1, $000]`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: invalid numeric placeholder "$000", it should start at "$1" at line 1:4 (pos 4)`),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(`[1, $1]`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: numeric placeholder "$1", but no params given at line 1:4 (pos 4)`),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(`[1, 2, $3]`, td.Ignore()),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: numeric placeholder "$3", but only one param given at line 1:7 (pos 7)`),
Under: mustContain(underOpJSON),
})
// $^Operator
checkError(t, "never tested",
td.JSON(`[1, $^bad%]`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: $^ must be followed by an operator name at line 1:4 (pos 4)`),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(`[1, "$^bad%"]`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: $^ must be followed by an operator name at line 1:5 (pos 5)`),
Under: mustContain(underOpJSON),
})
// named placeholders
checkError(t, "never tested",
td.JSON(`[
1,
"$bad%"
]`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: bad placeholder "$bad%" at line 3:3 (pos 10)`),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(`[1, $unknown]`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: unknown placeholder "$unknown" at line 1:4 (pos 4)`),
Under: mustContain(underOpJSON),
})
//
// Stringification
test.EqualStr(t, td.JSON(`1`).String(),
`JSON(1)`)
test.EqualStr(t, td.JSON(`[ 1, 2, 3 ]`).String(),
`
JSON([
1,
2,
3
])`[1:])
test.EqualStr(t, td.JSON(` null `).String(), `JSON(null)`)
test.EqualStr(t,
td.JSON(`[ $1, $name, $2, Nil(), $nil, 26, Between(5, 6), Len(34), Len(Between(5, 6)), 28 ]`,
td.Between(12, 20),
"test",
td.Tag("name", td.Code(func(s string) bool { return len(s) > 0 })),
td.Tag("nil", nil),
14,
).String(),
`
JSON([
"$1" /* 12 ≤ got ≤ 20 */,
"$name" /* Code(func(string) bool) */,
"test",
nil,
null,
26,
5.0 ≤ got ≤ 6.0,
len=34,
len: 5.0 ≤ got ≤ 6.0,
28
])`[1:])
test.EqualStr(t,
td.JSON(`[ $1, $name, $2, $^Nil, $nil ]`,
td.Between(12, 20),
"test",
td.Tag("name", td.Code(func(s string) bool { return len(s) > 0 })),
td.Tag("nil", nil),
).String(),
`
JSON([
"$1" /* 12 ≤ got ≤ 20 */,
"$name" /* Code(func(string) bool) */,
"test",
nil,
null
])`[1:])
test.EqualStr(t,
td.JSON(`{"label": $value, "zip": $^NotZero}`,
td.Tag("value", td.Bag(
td.JSON(`{"name": $1,"age":$2,"surname":$3}`,
td.HasPrefix("Bob"),
td.Between(12, 24),
"captain",
),
td.JSON(`{"name": $1}`, td.HasPrefix("Alice")),
)),
).String(),
`
JSON({
"label": "$value" /* Bag(JSON({
"age": "$2" /* 12 ≤ got ≤ 24 */,
"name": "$1" /* HasPrefix("Bob") */,
"surname": "captain"
}),
JSON({
"name": "$1" /* HasPrefix("Alice") */
})) */,
"zip": NotZero()
})`[1:])
test.EqualStr(t,
td.JSON(`
{
"label": {"name": HasPrefix("Bob"), "age": Between(12,24)},
"zip": NotZero()
}`).String(),
`
JSON({
"label": {
"age": 12.0 ≤ got ≤ 24.0,
"name": HasPrefix("Bob")
},
"zip": NotZero()
})`[1:])
// Erroneous op
test.EqualStr(t, td.JSON(`[`).String(), "JSON()")
}
func TestJSONInside(t *testing.T) {
// Between
t.Run("Between", func(t *testing.T) {
got := map[string]int{"val1": 1, "val2": 2}
checkOK(t, got,
td.JSON(`{"val1": Between(0, 2), "val2": Between(2, 3, "[[")}`))
checkOK(t, got,
td.JSON(`{"val1": Between(0, 2), "val2": Between(2, 3, "BoundsInOut")}`))
for _, bounds := range []string{"[[", "BoundsInOut"} {
checkError(t, got,
td.JSON(`{"val1": Between(0, 2), "val2": Between(1, 2, $1)}`, bounds),
expectedError{
Message: mustBe("values differ"),
Path: mustBe(`DATA["val2"]`),
Got: mustBe("2.0"),
Expected: mustBe("1.0 ≤ got < 2.0"),
Under: mustContain("under operator Between at line 1:32 (pos 32)" + insideOpJSON),
})
}
checkError(t, json.RawMessage(`123`),
td.JSON(`Between(0, 2)`),
expectedError{
Message: mustBe("values differ"),
Path: mustBe(`DATA`),
Got: mustBe("123.0"),
Expected: mustBe("0.0 ≤ got ≤ 2.0"),
Under: mustContain("under operator Between at line 1:0 (pos 0)" + insideOpJSON),
})
checkOK(t, got,
td.JSON(`{"val1": Between(1, 1), "val2": Between(2, 2, "[]")}`))
checkOK(t, got,
td.JSON(`{"val1": Between(1, 1), "val2": Between(2, 2, "BoundsInIn")}`))
checkOK(t, got,
td.JSON(`{"val1": Between(0, 1, "]]"), "val2": Between(1, 3, "][")}`))
checkOK(t, got,
td.JSON(`{"val1": Between(0, 1, "BoundsOutIn"), "val2": Between(1, 3, "BoundsOutOut")}`))
for _, bounds := range []string{"]]", "BoundsOutIn"} {
checkError(t, got,
td.JSON(`{"val1": 1, "val2": Between(2, 3, $1)}`, bounds),
expectedError{
Message: mustBe("values differ"),
Path: mustBe(`DATA["val2"]`),
Got: mustBe("2.0"),
Expected: mustBe("2.0 < got ≤ 3.0"),
Under: mustContain("under operator Between at line 1:20 (pos 20)" + insideOpJSON),
})
}
for _, bounds := range []string{"][", "BoundsOutOut"} {
checkError(t, got,
td.JSON(`{"val1": 1, "val2": Between(2, 3, $1)}`, bounds),
expectedError{
Message: mustBe("values differ"),
Path: mustBe(`DATA["val2"]`),
Got: mustBe("2.0"),
Expected: mustBe("2.0 < got < 3.0"),
Under: mustContain("under operator Between at line 1:20 (pos 20)" + insideOpJSON),
},
"using bounds %q", bounds)
checkError(t, got,
td.JSON(`{"val1": 1, "val2": Between(1, 2, $1)}`, bounds),
expectedError{
Message: mustBe("values differ"),
Path: mustBe(`DATA["val2"]`),
Got: mustBe("2.0"),
Expected: mustBe("1.0 < got < 2.0"),
Under: mustContain("under operator Between at line 1:20 (pos 20)" + insideOpJSON),
},
"using bounds %q", bounds)
}
// Bad 3rd parameter
checkError(t, "never tested",
td.JSON(`{
"val2": Between(1, 2, "<>")
}`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: Between() bad 3rd parameter, use "[]", "[[", "]]" or "][" at line 2:10 (pos 12)`),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(`{
"val2": Between(1, 2, 125)
}`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: Between() bad 3rd parameter, use "[]", "[[", "]]" or "][" at line 2:10 (pos 12)`),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(`{"val2": Between(1)}`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: Between() requires 2 or 3 parameters at line 1:9 (pos 9)`),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(`{"val2": Between(1,2,3,4)}`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: Between() requires 2 or 3 parameters at line 1:9 (pos 9)`),
Under: mustContain(underOpJSON),
})
})
// N
t.Run("N", func(t *testing.T) {
got := map[string]float32{"val": 2.1}
checkOK(t, got, td.JSON(`{"val": N(2.1)}`))
checkOK(t, got, td.JSON(`{"val": N(2, 0.1)}`))
checkError(t, "never tested",
td.JSON(`{"val2": N()}`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: N() requires 1 or 2 parameters at line 1:9 (pos 9)`),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(`{"val2": N(1,2,3)}`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: N() requires 1 or 2 parameters at line 1:9 (pos 9)`),
Under: mustContain(underOpJSON),
})
})
// Re
t.Run("Re", func(t *testing.T) {
got := map[string]string{"val": "Foo bar"}
checkOK(t, got, td.JSON(`{"val": Re("^Foo")}`))
checkOK(t, got, td.JSON(`{"val": Re("^(\\w+)", ["Foo"])}`))
checkOK(t, got, td.JSON(`{"val": Re("^(\\w+)", Bag("Foo"))}`))
checkError(t, "never tested",
td.JSON(`{"val2": Re()}`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: Re() requires 1 or 2 parameters at line 1:9 (pos 9)`),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(`{"val2": Re(1,2,3)}`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: Re() requires 1 or 2 parameters at line 1:9 (pos 9)`),
Under: mustContain(underOpJSON),
})
})
// SubMapOf
t.Run("SubMapOf", func(t *testing.T) {
got := []map[string]int{{"val1": 1, "val2": 2}}
checkOK(t, got, td.JSON(`[ SubMapOf({"val1":1, "val2":2, "xxx": "yyy"}) ]`))
checkError(t, "never tested",
td.JSON(`[ SubMapOf() ]`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: SubMapOf() requires only one parameter at line 1:2 (pos 2)`),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(`[ SubMapOf(1, 2) ]`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: SubMapOf() requires only one parameter at line 1:2 (pos 2)`),
Under: mustContain(underOpJSON),
})
})
// SuperMapOf
t.Run("SuperMapOf", func(t *testing.T) {
got := []map[string]int{{"val1": 1, "val2": 2}}
checkOK(t, got, td.JSON(`[ SuperMapOf({"val1":1}) ]`))
checkError(t, "never tested",
td.JSON(`[ SuperMapOf() ]`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: SuperMapOf() requires only one parameter at line 1:2 (pos 2)`),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(`[ SuperMapOf(1, 2) ]`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: SuperMapOf() requires only one parameter at line 1:2 (pos 2)`),
Under: mustContain(underOpJSON),
})
})
// errors
t.Run("Errors", func(t *testing.T) {
checkError(t, "never tested",
td.JSON(`[ UnknownOp() ]`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: unknown operator UnknownOp() at line 1:2 (pos 2)`),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(`[ Catch() ]`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: Catch() is not usable in JSON() at line 1:2 (pos 2)`),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(`[ JSON() ]`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: JSON() is not usable in JSON(), use literal JSON instead at line 1:2 (pos 2)`),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(`[ All() ]`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: All() requires at least one parameter at line 1:2 (pos 2)`),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(`[ Empty(12) ]`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: Empty() requires no parameters at line 1:2 (pos 2)`),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(`[ HasPrefix() ]`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: HasPrefix() requires only one parameter at line 1:2 (pos 2)`),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(`[ JSONPointer(1, 2, 3) ]`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: JSONPointer() requires 2 parameters at line 1:2 (pos 2)`),
Under: mustContain(underOpJSON),
})
checkError(t, "never tested",
td.JSON(`[ JSONPointer(1, 2) ]`),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: JSONPointer() bad #1 parameter type: string required but float64 received at line 1:2 (pos 2)`),
Under: mustContain(underOpJSON),
})
// This one is not caught by JSON, but by Re itself, as the number
// of parameters is correct
checkError(t, json.RawMessage(`"never tested"`),
td.JSON(`Re(1)`),
expectedError{
Message: mustBe("bad usage of Re operator"),
Path: mustBe("DATA"),
Summary: mustBe(`usage: Re(STRING|*regexp.Regexp[, NON_NIL_CAPTURE]), but received float64 as 1st parameter`),
Under: mustContain("under operator Re at line 1:0 (pos 0)" + insideOpJSON),
})
})
}
func TestJSONTypeBehind(t *testing.T) {
equalTypes(t, td.JSON(`false`), true)
equalTypes(t, td.JSON(`"foo"`), "")
equalTypes(t, td.JSON(`42`), float64(0))
equalTypes(t, td.JSON(`[1,2,3]`), ([]any)(nil))
equalTypes(t, td.JSON(`{"a":12}`), (map[string]any)(nil))
// operator at the root → delegate it TypeBehind() call
equalTypes(t, td.JSON(`$1`, td.SuperMapOf(map[string]any{"x": 1}, nil)), (map[string]any)(nil))
equalTypes(t, td.JSON(`SuperMapOf({"x":1})`), (map[string]any)(nil))
equalTypes(t, td.JSON(`$1`, 123), 42)
nullType := td.JSON(`null`).TypeBehind()
if nullType != reflect.TypeOf((*any)(nil)).Elem() {
t.Errorf("Failed test: got %s intead of interface {}", nullType)
}
// Erroneous op
equalTypes(t, td.JSON(`[`), nil)
}
func TestSubJSONOf(t *testing.T) {
type MyStruct struct {
Name string `json:"name"`
Age uint `json:"age"`
Gender string `json:"gender"`
}
//
// struct
//
got := MyStruct{Name: "Bob", Age: 42, Gender: "male"}
// No placeholder
checkOK(t, got,
td.SubJSONOf(`
{
"name": "Bob",
"age": 42,
"gender": "male",
"details": { // ← we don't want to test this field
"city": "Test City",
"zip": 666
}
}`))
// Numeric placeholders
checkOK(t, got,
td.SubJSONOf(`{"name":"$1","age":$2,"gender":$3,"details":{}}`,
"Bob", 42, "male")) // raw values
checkOK(t, got,
td.SubJSONOf(`{"name":"$1","age":$2,"gender":$3,"details":{}}`,
td.Re(`^Bob`),
td.Between(40, 45),
td.NotEmpty()))
// Same using Flatten
checkOK(t, got,
td.SubJSONOf(`{"name":"$1","age":$2,"gender":$3,"details":{}}`,
td.Re(`^Bob`),
td.Flatten([]td.TestDeep{td.Between(40, 45), td.NotEmpty()}),
))
// Tag placeholders
checkOK(t, got,
td.SubJSONOf(
`{"name":"$name","age":$age,"gender":"$gender","details":{}}`,
td.Tag("name", td.Re(`^Bob`)),
td.Tag("age", td.Between(40, 45)),
td.Tag("gender", td.NotEmpty())))
// Mixed placeholders + operator
for _, op := range []string{
"NotEmpty",
"NotEmpty()",
"$^NotEmpty",
"$^NotEmpty()",
`"$^NotEmpty"`,
`"$^NotEmpty()"`,
`r<$^NotEmpty>`,
`r<$^NotEmpty()>`,
} {
checkOK(t, got,
td.SubJSONOf(
`{"name":"$name","age":$1,"gender":`+op+`,"details":{}}`,
td.Tag("age", td.Between(40, 45)),
td.Tag("name", td.Re(`^Bob`))),
"using operator %s", op)
}
//
// Errors
checkError(t, func() {}, td.SubJSONOf(`{}`),
expectedError{
Message: mustBe("json.Marshal failed"),
Summary: mustContain("json: unsupported type"),
})
for i, n := range []any{
nil,
(map[string]any)(nil),
(map[string]bool)(nil),
([]int)(nil),
} {
checkError(t, n, td.SubJSONOf(`{}`),
expectedError{
Message: mustBe("values differ"),
Got: mustBe("null"),
Expected: mustBe("non-null"),
},
"nil test #%d", i)
}
//
// Fatal errors
checkError(t, "never tested",
td.SubJSONOf(`[1, "$123bad"]`),
expectedError{
Message: mustBe("bad usage of SubJSONOf operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: invalid numeric placeholder at line 1:5 (pos 5)`),
})
checkError(t, "never tested",
td.SubJSONOf(`[1, $000]`),
expectedError{
Message: mustBe("bad usage of SubJSONOf operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: invalid numeric placeholder "$000", it should start at "$1" at line 1:4 (pos 4)`),
})
checkError(t, "never tested",
td.SubJSONOf(`[1, $1]`),
expectedError{
Message: mustBe("bad usage of SubJSONOf operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: numeric placeholder "$1", but no params given at line 1:4 (pos 4)`),
})
checkError(t, "never tested",
td.SubJSONOf(`[1, 2, $3]`, td.Ignore()),
expectedError{
Message: mustBe("bad usage of SubJSONOf operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: numeric placeholder "$3", but only one param given at line 1:7 (pos 7)`),
})
// $^Operator
checkError(t, "never tested",
td.SubJSONOf(`[1, $^bad%]`),
expectedError{
Message: mustBe("bad usage of SubJSONOf operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: $^ must be followed by an operator name at line 1:4 (pos 4)`),
})
checkError(t, "never tested",
td.SubJSONOf(`[1, "$^bad%"]`),
expectedError{
Message: mustBe("bad usage of SubJSONOf operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: $^ must be followed by an operator name at line 1:5 (pos 5)`),
})
// named placeholders
checkError(t, "never tested",
td.SubJSONOf(`[1, "$bad%"]`),
expectedError{
Message: mustBe("bad usage of SubJSONOf operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: bad placeholder "$bad%" at line 1:5 (pos 5)`),
})
checkError(t, "never tested",
td.SubJSONOf(`[1, $unknown]`),
expectedError{
Message: mustBe("bad usage of SubJSONOf operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: unknown placeholder "$unknown" at line 1:4 (pos 4)`),
})
checkError(t, "never tested",
td.SubJSONOf("null"),
expectedError{
Message: mustBe("bad usage of SubJSONOf operator"),
Path: mustBe("DATA"),
Summary: mustBe("SubJSONOf() only accepts JSON objects {…}"),
})
//
// Stringification
test.EqualStr(t, td.SubJSONOf(`{}`).String(), `SubJSONOf({})`)
test.EqualStr(t, td.SubJSONOf(`{"foo":1, "bar":2}`).String(),
`
SubJSONOf({
"bar": 2,
"foo": 1
})`[1:])
test.EqualStr(t,
td.SubJSONOf(`{"label": $value, "zip": $^NotZero}`,
td.Tag("value", td.Bag(
td.SubJSONOf(`{"name": $1,"age":$2}`,
td.HasPrefix("Bob"),
td.Between(12, 24),
),
td.SubJSONOf(`{"name": $1}`, td.HasPrefix("Alice")),
)),
).String(),
`
SubJSONOf({
"label": "$value" /* Bag(SubJSONOf({
"age": "$2" /* 12 ≤ got ≤ 24 */,
"name": "$1" /* HasPrefix("Bob") */
}),
SubJSONOf({
"name": "$1" /* HasPrefix("Alice") */
})) */,
"zip": NotZero()
})`[1:])
// Erroneous op
test.EqualStr(t, td.SubJSONOf(`123`).String(), "SubJSONOf()")
}
func TestSubJSONOfTypeBehind(t *testing.T) {
equalTypes(t, td.SubJSONOf(`{"a":12}`), (map[string]any)(nil))
// Erroneous op
equalTypes(t, td.SubJSONOf(`123`), nil)
}
func TestSuperJSONOf(t *testing.T) {
type MyStruct struct {
Name string `json:"name"`
Age uint `json:"age"`
Gender string `json:"gender"`
Details string `json:"details"`
}
//
// struct
//
got := MyStruct{Name: "Bob", Age: 42, Gender: "male", Details: "Nice"}
// No placeholder
checkOK(t, got, td.SuperJSONOf(`{"name": "Bob"}`))
// Numeric placeholders
checkOK(t, got,
td.SuperJSONOf(`{"name":"$1","age":$2}`,
"Bob", 42)) // raw values
checkOK(t, got,
td.SuperJSONOf(`{"name":"$1","age":$2}`,
td.Re(`^Bob`),
td.Between(40, 45)))
// Same using Flatten
checkOK(t, got,
td.SuperJSONOf(`{"name":"$1","age":$2}`,
td.Flatten([]td.TestDeep{td.Re(`^Bob`), td.Between(40, 45)}),
))
// Tag placeholders
checkOK(t, got,
td.SuperJSONOf(`{"name":"$name","gender":"$gender"}`,
td.Tag("name", td.Re(`^Bob`)),
td.Tag("gender", td.NotEmpty())))
// Mixed placeholders + operator
for _, op := range []string{
"NotEmpty",
"NotEmpty()",
"$^NotEmpty",
"$^NotEmpty()",
`"$^NotEmpty"`,
`"$^NotEmpty()"`,
`r<$^NotEmpty>`,
`r<$^NotEmpty()>`,
} {
checkOK(t, got,
td.SuperJSONOf(
`{"name":"$name","age":$1,"gender":`+op+`}`,
td.Tag("age", td.Between(40, 45)),
td.Tag("name", td.Re(`^Bob`))),
"using operator %s", op)
}
// …with comments…
checkOK(t, got,
td.SuperJSONOf(`
// This should be the JSON representation of MyStruct struct
{
// A person:
"name": "$name", // The name of this person
"age": $1, /* The age of this person:
- placeholder unquoted, but could be without
any change
- to demonstrate a multi-lines comment */
"gender": $^NotEmpty // Shortcut to operator NotEmpty
}`,
td.Tag("age", td.Between(40, 45)),
td.Tag("name", td.Re(`^Bob`))))
//
// Errors
checkError(t, func() {}, td.SuperJSONOf(`{}`),
expectedError{
Message: mustBe("json.Marshal failed"),
Summary: mustContain("json: unsupported type"),
})
for i, n := range []any{
nil,
(map[string]any)(nil),
(map[string]bool)(nil),
([]int)(nil),
} {
checkError(t, n, td.SuperJSONOf(`{}`),
expectedError{
Message: mustBe("values differ"),
Got: mustBe("null"),
Expected: mustBe("non-null"),
},
"nil test #%d", i)
}
//
// Fatal errors
checkError(t, "never tested",
td.SuperJSONOf(`[1, "$123bad"]`),
expectedError{
Message: mustBe("bad usage of SuperJSONOf operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: invalid numeric placeholder at line 1:5 (pos 5)`),
})
checkError(t, "never tested",
td.SuperJSONOf(`[1, $000]`),
expectedError{
Message: mustBe("bad usage of SuperJSONOf operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: invalid numeric placeholder "$000", it should start at "$1" at line 1:4 (pos 4)`),
})
checkError(t, "never tested",
td.SuperJSONOf(`[1, $1]`),
expectedError{
Message: mustBe("bad usage of SuperJSONOf operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: numeric placeholder "$1", but no params given at line 1:4 (pos 4)`),
})
checkError(t, "never tested",
td.SuperJSONOf(`[1, 2, $3]`, td.Ignore()),
expectedError{
Message: mustBe("bad usage of SuperJSONOf operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: numeric placeholder "$3", but only one param given at line 1:7 (pos 7)`),
})
// $^Operator
checkError(t, "never tested",
td.SuperJSONOf(`[1, $^bad%]`),
expectedError{
Message: mustBe("bad usage of SuperJSONOf operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: $^ must be followed by an operator name at line 1:4 (pos 4)`),
})
checkError(t, "never tested",
td.SuperJSONOf(`[1, "$^bad%"]`),
expectedError{
Message: mustBe("bad usage of SuperJSONOf operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: $^ must be followed by an operator name at line 1:5 (pos 5)`),
})
// named placeholders
checkError(t, "never tested",
td.SuperJSONOf(`[1, "$bad%"]`),
expectedError{
Message: mustBe("bad usage of SuperJSONOf operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: bad placeholder "$bad%" at line 1:5 (pos 5)`),
})
checkError(t, "never tested",
td.SuperJSONOf(`[1, $unknown]`),
expectedError{
Message: mustBe("bad usage of SuperJSONOf operator"),
Path: mustBe("DATA"),
Summary: mustBe(`JSON unmarshal error: unknown placeholder "$unknown" at line 1:4 (pos 4)`),
})
checkError(t, "never tested",
td.SuperJSONOf("null"),
expectedError{
Message: mustBe("bad usage of SuperJSONOf operator"),
Path: mustBe("DATA"),
Summary: mustBe("SuperJSONOf() only accepts JSON objects {…}"),
})
//
// Stringification
test.EqualStr(t, td.SuperJSONOf(`{}`).String(), `SuperJSONOf({})`)
test.EqualStr(t, td.SuperJSONOf(`{"foo":1, "bar":2}`).String(),
`
SuperJSONOf({
"bar": 2,
"foo": 1
})`[1:])
test.EqualStr(t,
td.SuperJSONOf(`{"label": $value, "zip": $^NotZero}`,
td.Tag("value", td.Bag(
td.SuperJSONOf(`{"name": $1,"age":$2}`,
td.HasPrefix("Bob"),
td.Between(12, 24),
),
td.SuperJSONOf(`{"name": $1}`, td.HasPrefix("Alice")),
)),
).String(),
`
SuperJSONOf({
"label": "$value" /* Bag(SuperJSONOf({
"age": "$2" /* 12 ≤ got ≤ 24 */,
"name": "$1" /* HasPrefix("Bob") */
}),
SuperJSONOf({
"name": "$1" /* HasPrefix("Alice") */
})) */,
"zip": NotZero()
})`[1:])
// Erroneous op
test.EqualStr(t, td.SuperJSONOf(`123`).String(), "SuperJSONOf()")
}
func TestSuperJSONOfTypeBehind(t *testing.T) {
equalTypes(t, td.SuperJSONOf(`{"a":12}`), (map[string]any)(nil))
// Erroneous op
equalTypes(t, td.SuperJSONOf(`123`), nil)
}
go-testdeep-1.15.0/td/td_keys_values.go 0000664 0000000 0000000 00000010116 15144170453 0020000 0 ustar 00root root 0000000 0000000 // 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 td
import (
"reflect"
"github.com/maxatome/go-testdeep/helpers/tdutil"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/util"
)
type tdKVBase struct {
tdSmugglerBase
}
func (b *tdKVBase) initKVBase(val any) bool {
b.tdSmugglerBase = newSmugglerBase(val, 1)
if vval := reflect.ValueOf(val); vval.IsValid() {
if b.isTestDeeper {
return true
}
if vval.Kind() == reflect.Slice {
b.expectedValue = vval
return true
}
}
return false
}
type tdKeys struct {
tdKVBase
}
var _ TestDeep = &tdKeys{}
// summary(Keys): checks keys of a map
// input(Keys): map
// Keys is a smuggler operator. It takes a map and compares its
// ordered keys to val.
//
// val can be a slice of items of the same type as the map keys:
//
// got := map[string]bool{"c": true, "a": false, "b": true}
// td.Cmp(t, got, td.Keys([]string{"a", "b", "c"})) // succeeds, keys sorted
// td.Cmp(t, got, td.Keys([]string{"c", "a", "b"})) // fails as not sorted
//
// as well as an other operator as [Bag], for example, to test keys in
// an unsorted manner:
//
// got := map[string]bool{"c": true, "a": false, "b": true}
// td.Cmp(t, got, td.Keys(td.Bag("c", "a", "b"))) // succeeds
//
// See also [Values] and [ContainsKey].
func Keys(val any) TestDeep {
k := tdKeys{}
if !k.initKVBase(val) {
k.err = ctxerr.OpBadUsage("Keys", "(TESTDEEP_OPERATOR|SLICE)", val, 1, true)
}
return &k
}
func (k *tdKeys) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if k.err != nil {
return ctx.CollectError(k.err)
}
if got.Kind() != reflect.Map {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(ctxerr.BadKind(got, reflect.Map.String()))
}
// Build a sorted slice of keys
l := got.Len()
keys := reflect.MakeSlice(reflect.SliceOf(got.Type().Key()), l, l)
for i, k := range tdutil.MapSortedKeys(got) {
keys.Index(i).Set(k)
}
return deepValueEqual(ctx.AddFunctionCall("keys"), keys, k.expectedValue)
}
func (k *tdKeys) String() string {
if k.err != nil {
return k.stringError()
}
if k.isTestDeeper {
return "keys: " + k.expectedValue.Interface().(TestDeep).String()
}
return "keys=" + util.ToString(k.expectedValue.Interface())
}
type tdValues struct {
tdKVBase
}
var _ TestDeep = &tdValues{}
// summary(Values): checks values of a map
// input(Values): map
// Values is a smuggler operator. It takes a map and compares its
// ordered values to val.
//
// val can be a slice of items of the same type as the map values:
//
// got := map[int]string{3: "c", 1: "a", 2: "b"}
// td.Cmp(t, got, td.Values([]string{"a", "b", "c"})) // succeeds, values sorted
// td.Cmp(t, got, td.Values([]string{"c", "a", "b"})) // fails as not sorted
//
// as well as an other operator as [Bag], for example, to test values in
// an unsorted manner:
//
// got := map[int]string{3: "c", 1: "a", 2: "b"}
// td.Cmp(t, got, td.Values(td.Bag("c", "a", "b"))) // succeeds
//
// See also [Keys].
func Values(val any) TestDeep {
v := tdValues{}
if !v.initKVBase(val) {
v.err = ctxerr.OpBadUsage("Values", "(TESTDEEP_OPERATOR|SLICE)", val, 1, true)
}
return &v
}
func (v *tdValues) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if v.err != nil {
return ctx.CollectError(v.err)
}
if got.Kind() != reflect.Map {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(ctxerr.BadKind(got, reflect.Map.String()))
}
// Build a sorted slice of values
l := got.Len()
values := reflect.MakeSlice(reflect.SliceOf(got.Type().Elem()), l, l)
for i, v := range tdutil.MapSortedValues(got) {
values.Index(i).Set(v)
}
return deepValueEqual(ctx.AddFunctionCall("values"), values, v.expectedValue)
}
func (v *tdValues) String() string {
if v.err != nil {
return v.stringError()
}
if v.isTestDeeper {
return "values: " + v.expectedValue.Interface().(TestDeep).String()
}
return "values=" + util.ToString(v.expectedValue.Interface())
}
go-testdeep-1.15.0/td/td_keys_values_test.go 0000664 0000000 0000000 00000010607 15144170453 0021044 0 ustar 00root root 0000000 0000000 // 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 td_test
import (
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestKeysValues(t *testing.T) {
var m map[string]int
//
t.Run("nil map", func(t *testing.T) {
checkOK(t, m, td.Keys([]string{}))
checkOK(t, m, td.Values([]int{}))
checkOK(t, m, td.Keys(td.Empty()))
checkOK(t, m, td.Values(td.Empty()))
checkError(t, m, td.Keys(td.NotEmpty()),
expectedError{
Message: mustBe("empty"),
Path: mustBe("keys(DATA)"),
Expected: mustBe("not empty"),
})
checkError(t, m, td.Values(td.NotEmpty()),
expectedError{
Message: mustBe("empty"),
Path: mustBe("values(DATA)"),
Expected: mustBe("not empty"),
})
})
//
t.Run("non-nil but empty map", func(t *testing.T) {
m = map[string]int{}
checkOK(t, m, td.Keys([]string{}))
checkOK(t, m, td.Values([]int{}))
checkOK(t, m, td.Keys(td.Empty()))
checkOK(t, m, td.Values(td.Empty()))
checkError(t, m, td.Keys(td.NotEmpty()),
expectedError{
Message: mustBe("empty"),
Path: mustBe("keys(DATA)"),
Expected: mustBe("not empty"),
})
checkError(t, m, td.Values(td.NotEmpty()),
expectedError{
Message: mustBe("empty"),
Path: mustBe("values(DATA)"),
Expected: mustBe("not empty"),
})
})
//
t.Run("Filled map", func(t *testing.T) {
m = map[string]int{"a": 1, "b": 2, "c": 3, "d": 4, "e": 5, "f": 6}
checkOK(t, m, td.Keys([]string{"a", "b", "c", "d", "e", "f"}))
checkOK(t, m, td.Values([]int{1, 2, 3, 4, 5, 6}))
checkOK(t, m, td.Keys(td.Bag("a", "b", "c", "d", "e", "f")))
checkOK(t, m, td.Values(td.Bag(1, 2, 3, 4, 5, 6)))
checkOK(t, m, td.Keys(td.ArrayEach(td.Between("a", "f"))))
checkOK(t, m, td.Values(td.ArrayEach(td.Between(1, 6))))
checkError(t, m, td.Keys(td.Empty()),
expectedError{
Message: mustBe("not empty"),
Path: mustBe("keys(DATA)"),
Expected: mustBe("empty"),
})
checkError(t, m, td.Values(td.Empty()),
expectedError{
Message: mustBe("not empty"),
Path: mustBe("values(DATA)"),
Expected: mustBe("empty"),
})
})
//
t.Run("Errors", func(t *testing.T) {
checkError(t, nil, td.Keys([]int{1, 2, 3}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustContain("keys=([]int)"),
})
checkError(t, nil, td.Values([]int{1, 2, 3}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustContain("values=([]int)"),
})
checkError(t, nil, td.Keys(td.Empty()),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("keys: Empty()"),
})
checkError(t, nil, td.Values(td.Empty()),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("values: Empty()"),
})
checkError(t, 123, td.Keys(td.Empty()),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("map"),
})
checkError(t, 123, td.Values(td.Empty()),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("map"),
})
})
//
t.Run("Bad usage", func(t *testing.T) {
checkError(t, "never tested",
td.Keys(12),
expectedError{
Message: mustBe("bad usage of Keys operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Keys(TESTDEEP_OPERATOR|SLICE), but received int as 1st parameter"),
})
checkError(t, "never tested",
td.Values(12),
expectedError{
Message: mustBe("bad usage of Values operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Values(TESTDEEP_OPERATOR|SLICE), but received int as 1st parameter"),
})
})
// Erroneous op
test.EqualStr(t, td.Keys(12).String(), "Keys()")
test.EqualStr(t, td.Values(12).String(), "Values()")
}
func TestKeysValuesTypeBehind(t *testing.T) {
equalTypes(t, td.Keys([]string{}), nil)
equalTypes(t, td.Values([]string{}), nil)
// Erroneous op
equalTypes(t, td.Keys(12), nil)
equalTypes(t, td.Values(12), nil)
}
go-testdeep-1.15.0/td/td_lax.go 0000664 0000000 0000000 00000005554 15144170453 0016244 0 ustar 00root root 0000000 0000000 // 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 td
import (
"reflect"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/util"
)
type tdLax struct {
tdSmugglerBase
}
var _ TestDeep = &tdLax{}
// summary(Lax): temporarily enables [`BeLax` config flag]
// input(Lax): all
// Lax is a smuggler operator, it temporarily enables the BeLax config
// flag before letting the comparison process continue its course.
//
// It is more commonly used as [CmpLax] function than as an operator. It
// could be used when, for example, an operator is constructed once
// but applied to different, but compatible types as in:
//
// bw := td.Between(20, 30)
// intValue := 21
// floatValue := 21.89
// td.Cmp(t, intValue, bw) // no need to be lax here: same int types
// td.Cmp(t, floatValue, td.Lax(bw)) // be lax please, as float64 ≠ int
//
// Note that in the latter case, [CmpLax] could be used as well:
//
// td.CmpLax(t, floatValue, bw)
//
// TypeBehind method returns the greatest convertible or more common
// [reflect.Type] of expectedValue if it is a base type (bool, int*,
// uint*, float*, complex*, string), the [reflect.Type] of
// expectedValue otherwise, except if expectedValue is a [TestDeep]
// operator. In this case, it delegates TypeBehind() to the operator.
func Lax(expectedValue any) TestDeep {
c := tdLax{
tdSmugglerBase: newSmugglerBase(expectedValue),
}
if !c.isTestDeeper {
c.expectedValue = reflect.ValueOf(expectedValue)
}
return &c
}
func (l *tdLax) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
ctx.BeLax = true
return deepValueEqual(ctx, got, l.expectedValue)
}
func (l *tdLax) HandleInvalid() bool {
return true // Knows how to handle untyped nil values (aka invalid values)
}
func (l *tdLax) String() string {
return "Lax(" + util.ToString(l.expectedValue) + ")"
}
func (l *tdLax) TypeBehind() reflect.Type {
// If the expected value is a TestDeep operator, delegate TypeBehind to it
if l.isTestDeeper {
return l.expectedValue.Interface().(TestDeep).TypeBehind()
}
// For base types, returns the greatest convertible or more common one
switch l.expectedValue.Kind() {
case reflect.Invalid:
return nil
case reflect.Bool:
return reflect.TypeOf(false)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return reflect.TypeOf(int64(0))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return reflect.TypeOf(uint64(0))
case reflect.Float32, reflect.Float64:
return reflect.TypeOf(float64(0))
case reflect.Complex64, reflect.Complex128:
return reflect.TypeOf(complex(128, -1))
case reflect.String:
return reflect.TypeOf("")
default:
return l.expectedValue.Type()
}
}
go-testdeep-1.15.0/td/td_lax_test.go 0000664 0000000 0000000 00000004017 15144170453 0017274 0 ustar 00root root 0000000 0000000 // 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 td_test
import (
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestLax(t *testing.T) {
checkOK(t, int64(1234), td.Lax(1234))
type MyInt int32
checkOK(t, int64(123), td.Lax(MyInt(123)))
checkOK(t, MyInt(123), td.Lax(int64(123)))
type gotStruct struct {
name string
age int
}
type expectedStruct struct {
name string
age int
}
checkOK(t,
gotStruct{
name: "bob",
age: 42,
},
td.Lax(expectedStruct{
name: "bob",
age: 42,
}))
checkOK(t,
&gotStruct{
name: "bob",
age: 42,
},
td.Lax(&expectedStruct{
name: "bob",
age: 42,
}))
checkError(t, int64(123), td.Between(120, 125),
expectedError{
Message: mustBe("type mismatch"),
})
checkOK(t, int64(123), td.Lax(td.Between(120, 125)))
// nil cases
checkOK(t, nil, td.Lax(nil))
checkOK(t, (*gotStruct)(nil), td.Lax((*expectedStruct)(nil)))
checkOK(t, (*gotStruct)(nil), td.Lax(nil))
checkOK(t, (chan int)(nil), td.Lax(nil))
checkOK(t, (func())(nil), td.Lax(nil))
checkOK(t, (map[int]int)(nil), td.Lax(nil))
checkOK(t, ([]int)(nil), td.Lax(nil))
//
// String
test.EqualStr(t, td.Lax(6).String(), "Lax(6)")
}
func TestLaxTypeBehind(t *testing.T) {
equalTypes(t, td.Lax(nil), nil)
type MyBool bool
equalTypes(t, td.Lax(MyBool(false)), false)
equalTypes(t, td.Lax(0), int64(0))
equalTypes(t, td.Lax(uint8(0)), uint64(0))
equalTypes(t, td.Lax(float32(0)), float64(0))
equalTypes(t, td.Lax(complex64(complex(1, 1))), complex128(complex(1, 1)))
type MyString string
equalTypes(t, td.Lax(MyString("")), "")
type MyBytes []byte
equalTypes(t, td.Lax([]byte{}), []byte{})
equalTypes(t, td.Lax(MyBytes{}), MyBytes{})
// Another TestDeep operator delegation
equalTypes(t, td.Lax(td.Struct(MyStruct{}, nil)), MyStruct{})
equalTypes(t, td.Lax(td.Any(1, 1.2)), nil)
}
go-testdeep-1.15.0/td/td_len_cap.go 0000664 0000000 0000000 00000012221 15144170453 0017046 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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"
"math"
"reflect"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/types"
)
type tdLenCapBase struct {
tdSmugglerBase
}
func (b *tdLenCapBase) initLenCapBase(val any) {
b.tdSmugglerBase = newSmugglerBase(val, 1)
// math.MaxInt appeared in go1.17
const (
maxUint = ^uint(0)
maxInt = int(maxUint >> 1)
minInt = -maxInt - 1
usage = "(TESTDEEP_OPERATOR|INT)"
)
if val == nil {
b.err = ctxerr.OpBadUsage(b.GetLocation().Func, usage, val, 1, true)
return
}
if b.isTestDeeper {
return
}
vval := reflect.ValueOf(val)
// A len or capacity is always an int, but accept any MinInt ≤ num ≤ MaxInt,
// so it can be used in JSON, SubJSONOf and SuperJSONOf as float64
switch vval.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
num := vval.Int()
if num >= int64(minInt) && num <= int64(maxInt) {
b.expectedValue = reflect.ValueOf(int(num))
return
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
num := vval.Uint()
if num <= uint64(maxInt) {
b.expectedValue = reflect.ValueOf(int(num))
return
}
case reflect.Float32, reflect.Float64:
num := vval.Float()
if num == math.Trunc(num) && num >= float64(minInt) && num <= float64(maxInt) {
b.expectedValue = reflect.ValueOf(int(num))
return
}
default:
b.err = ctxerr.OpBadUsage(b.GetLocation().Func, usage, val, 1, true)
return
}
op := b.GetLocation().Func
b.err = ctxerr.OpBad(op, "usage: "+op+usage+
", but received an out of bounds or not integer 1st parameter (%v), should be in int range", val)
}
func (b *tdLenCapBase) isEqual(ctx ctxerr.Context, got int) (bool, *ctxerr.Error) {
if b.isTestDeeper {
return true, deepValueEqual(ctx, reflect.ValueOf(got), b.expectedValue)
}
if int64(got) == b.expectedValue.Int() {
return true, nil
}
return false, nil
}
type tdLen struct {
tdLenCapBase
}
var _ TestDeep = &tdLen{}
// summary(Len): checks an array, slice, map, string or channel length
// input(Len): array,slice,map,chan
// Len is a smuggler operator. It takes data, applies len() function
// on it and compares its result to expectedLen. Of course, the
// compared value must be an array, a channel, a map, a slice or a
// string.
//
// expectedLen can be an int value:
//
// td.Cmp(t, gotSlice, td.Len(12))
//
// as well as an other operator:
//
// td.Cmp(t, gotSlice, td.Len(td.Between(3, 4)))
//
// See also [Cap].
func Len(expectedLen any) TestDeep {
l := tdLen{}
l.initLenCapBase(expectedLen)
return &l
}
func (l *tdLen) String() string {
if l.err != nil {
return l.stringError()
}
if l.isTestDeeper {
return "len: " + l.expectedValue.Interface().(TestDeep).String()
}
return fmt.Sprintf("len=%d", l.expectedValue.Int())
}
func (l *tdLen) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if l.err != nil {
return ctx.CollectError(l.err)
}
switch got.Kind() {
case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String:
ret, err := l.isEqual(ctx.AddFunctionCall("len"), got.Len())
if ret {
return err
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "bad length",
Got: types.RawInt(got.Len()),
Expected: types.RawInt(l.expectedValue.Int()),
})
default:
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(ctxerr.BadKind(got, "array OR chan OR map OR slice OR string"))
}
}
type tdCap struct {
tdLenCapBase
}
var _ TestDeep = &tdCap{}
// summary(Cap): checks an array, slice or channel capacity
// input(Cap): array,slice,chan
// Cap is a smuggler operator. It takes data, applies cap() function
// on it and compares its result to expectedCap. Of course, the
// compared value must be an array, a channel or a slice.
//
// expectedCap can be an int value:
//
// td.Cmp(t, gotSlice, td.Cap(12))
//
// as well as an other operator:
//
// td.Cmp(t, gotSlice, td.Cap(td.Between(3, 4)))
//
// See also [Len].
func Cap(expectedCap any) TestDeep {
c := tdCap{}
c.initLenCapBase(expectedCap)
return &c
}
func (c *tdCap) String() string {
if c.err != nil {
return c.stringError()
}
if c.isTestDeeper {
return "cap: " + c.expectedValue.Interface().(TestDeep).String()
}
return fmt.Sprintf("cap=%d", c.expectedValue.Int())
}
func (c *tdCap) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if c.err != nil {
return ctx.CollectError(c.err)
}
switch got.Kind() {
case reflect.Array, reflect.Chan, reflect.Slice:
ret, err := c.isEqual(ctx.AddFunctionCall("cap"), got.Cap())
if ret {
return err
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "bad capacity",
Got: types.RawInt(got.Cap()),
Expected: types.RawInt(c.expectedValue.Int()),
})
default:
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(ctxerr.BadKind(got, "array OR chan OR slice"))
}
}
go-testdeep-1.15.0/td/td_len_cap_test.go 0000664 0000000 0000000 00000016536 15144170453 0020122 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (
"math"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestLen(t *testing.T) {
checkOK(t, "abcd", td.Len(4))
checkOK(t, "abcd", td.Len(td.Between(4, 6)))
checkOK(t, "abcd", td.Len(td.Between(6, 4)))
checkOK(t, []byte("abcd"), td.Len(4))
checkOK(t, []byte("abcd"), td.Len(td.Between(4, 6)))
checkOK(t, [5]int{}, td.Len(5))
checkOK(t, [5]int{}, td.Len(int8(5)))
checkOK(t, [5]int{}, td.Len(int16(5)))
checkOK(t, [5]int{}, td.Len(int32(5)))
checkOK(t, [5]int{}, td.Len(int64(5)))
checkOK(t, [5]int{}, td.Len(uint(5)))
checkOK(t, [5]int{}, td.Len(uint8(5)))
checkOK(t, [5]int{}, td.Len(uint16(5)))
checkOK(t, [5]int{}, td.Len(uint32(5)))
checkOK(t, [5]int{}, td.Len(uint64(5)))
checkOK(t, [5]int{}, td.Len(float32(5)))
checkOK(t, [5]int{}, td.Len(float64(5)))
checkOK(t, [5]int{}, td.Len(td.Between(4, 6)))
checkOK(t, map[int]bool{1: true, 2: false}, td.Len(2))
checkOK(t, map[int]bool{1: true, 2: false},
td.Len(td.Between(1, 6)))
checkOK(t, make(chan int, 3), td.Len(0))
checkError(t, [5]int{}, td.Len(4),
expectedError{
Message: mustBe("bad length"),
Path: mustBe("DATA"),
Got: mustBe("5"),
Expected: mustBe("4"),
})
checkError(t, [5]int{}, td.Len(td.Lt(4)),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("len(DATA)"),
Got: mustBe("5"),
Expected: mustBe("< 4"),
})
checkError(t, 123, td.Len(4),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("array OR chan OR map OR slice OR string"),
})
//
// Bad usage
checkError(t, "never tested",
td.Len(nil),
expectedError{
Message: mustBe("bad usage of Len operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Len(TESTDEEP_OPERATOR|INT), but received nil as 1st parameter"),
})
checkError(t, "never tested",
td.Len("12"),
expectedError{
Message: mustBe("bad usage of Len operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Len(TESTDEEP_OPERATOR|INT), but received string as 1st parameter"),
})
// out of bounds
checkError(t, "never tested",
td.Len(uint64(math.MaxUint64)),
expectedError{
Message: mustBe("bad usage of Len operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Len(TESTDEEP_OPERATOR|INT), but received an out of bounds or not integer 1st parameter (18446744073709551615), should be in int range"),
})
checkError(t, "never tested",
td.Len(float64(math.MaxUint64)),
expectedError{
Message: mustBe("bad usage of Len operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Len(TESTDEEP_OPERATOR|INT), but received an out of bounds or not integer 1st parameter (1.8446744073709552e+19), should be in int range"),
})
checkError(t, "never tested",
td.Len(float64(-math.MaxUint64)),
expectedError{
Message: mustBe("bad usage of Len operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Len(TESTDEEP_OPERATOR|INT), but received an out of bounds or not integer 1st parameter (-1.8446744073709552e+19), should be in int range"),
})
checkError(t, "never tested",
td.Len(3.1),
expectedError{
Message: mustBe("bad usage of Len operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Len(TESTDEEP_OPERATOR|INT), but received an out of bounds or not integer 1st parameter (3.1), should be in int range"),
})
//
// String
test.EqualStr(t, td.Len(3).String(), "len=3")
test.EqualStr(t,
td.Len(td.Between(3, 8)).String(), "len: 3 ≤ got ≤ 8")
test.EqualStr(t, td.Len(td.Gt(8)).String(), "len: > 8")
// Erroneous
test.EqualStr(t, td.Len("12").String(), "Len()")
}
func TestCap(t *testing.T) {
checkOK(t, make([]byte, 0, 4), td.Cap(4))
checkOK(t, make([]byte, 0, 4), td.Cap(td.Between(4, 6)))
checkOK(t, [5]int{}, td.Cap(5))
checkOK(t, [5]int{}, td.Cap(int8(5)))
checkOK(t, [5]int{}, td.Cap(int16(5)))
checkOK(t, [5]int{}, td.Cap(int32(5)))
checkOK(t, [5]int{}, td.Cap(int64(5)))
checkOK(t, [5]int{}, td.Cap(uint(5)))
checkOK(t, [5]int{}, td.Cap(uint8(5)))
checkOK(t, [5]int{}, td.Cap(uint16(5)))
checkOK(t, [5]int{}, td.Cap(uint32(5)))
checkOK(t, [5]int{}, td.Cap(uint64(5)))
checkOK(t, [5]int{}, td.Cap(float32(5)))
checkOK(t, [5]int{}, td.Cap(float64(5)))
checkOK(t, [5]int{}, td.Cap(td.Between(4, 6)))
checkOK(t, make(chan int, 3), td.Cap(3))
checkError(t, [5]int{}, td.Cap(4),
expectedError{
Message: mustBe("bad capacity"),
Path: mustBe("DATA"),
Got: mustBe("5"),
Expected: mustBe("4"),
})
checkError(t, [5]int{}, td.Cap(td.Between(2, 4)),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("cap(DATA)"),
Got: mustBe("5"),
Expected: mustBe("2 ≤ got ≤ 4"),
})
checkError(t, map[int]int{1: 2}, td.Cap(1),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("map (map[int]int type)"),
Expected: mustBe("array OR chan OR slice"),
})
//
// Bad usage
checkError(t, "never tested",
td.Cap(nil),
expectedError{
Message: mustBe("bad usage of Cap operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Cap(TESTDEEP_OPERATOR|INT), but received nil as 1st parameter"),
})
checkError(t, "never tested",
td.Cap("12"),
expectedError{
Message: mustBe("bad usage of Cap operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Cap(TESTDEEP_OPERATOR|INT), but received string as 1st parameter"),
})
// out of bounds
checkError(t, "never tested",
td.Cap(uint64(math.MaxUint64)),
expectedError{
Message: mustBe("bad usage of Cap operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Cap(TESTDEEP_OPERATOR|INT), but received an out of bounds or not integer 1st parameter (18446744073709551615), should be in int range"),
})
checkError(t, "never tested",
td.Cap(float64(math.MaxUint64)),
expectedError{
Message: mustBe("bad usage of Cap operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Cap(TESTDEEP_OPERATOR|INT), but received an out of bounds or not integer 1st parameter (1.8446744073709552e+19), should be in int range"),
})
checkError(t, "never tested",
td.Cap(float64(-math.MaxUint64)),
expectedError{
Message: mustBe("bad usage of Cap operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Cap(TESTDEEP_OPERATOR|INT), but received an out of bounds or not integer 1st parameter (-1.8446744073709552e+19), should be in int range"),
})
checkError(t, "never tested",
td.Cap(3.1),
expectedError{
Message: mustBe("bad usage of Cap operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Cap(TESTDEEP_OPERATOR|INT), but received an out of bounds or not integer 1st parameter (3.1), should be in int range"),
})
//
// String
test.EqualStr(t, td.Cap(3).String(), "cap=3")
test.EqualStr(t,
td.Cap(td.Between(3, 8)).String(), "cap: 3 ≤ got ≤ 8")
test.EqualStr(t, td.Cap(td.Gt(8)).String(), "cap: > 8")
// Erroneous op
test.EqualStr(t, td.Cap("12").String(), "Cap()")
}
func TestLenCapTypeBehind(t *testing.T) {
equalTypes(t, td.Cap(3), nil)
equalTypes(t, td.Len(3), nil)
// Erroneous op
equalTypes(t, td.Cap("12"), nil)
equalTypes(t, td.Len("12"), nil)
}
go-testdeep-1.15.0/td/td_list.go 0000664 0000000 0000000 00000010425 15144170453 0016424 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2025, 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"
"strings"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/flat"
"github.com/maxatome/go-testdeep/internal/util"
)
type tdListBase struct {
baseOKNil
items []reflect.Value
}
func newListBase(items ...any) tdListBase {
return tdListBase{
baseOKNil: newBaseOKNil(4),
items: flat.Values(items),
}
}
func (l *tdListBase) String() string {
var b strings.Builder
b.WriteString(l.GetLocation().Func)
return util.SliceToString(&b, l.items).String()
}
type tdList struct {
tdListBase
}
var _ TestDeep = &tdList{}
// summary(List): compares the contents of an array or a slice with taking
// care of the order of items
// input(List): array,slice,ptr(ptr on array/slice)
// List operator compares the contents of an array or a slice (or a
// pointer on array/slice) with taking care of the order of items.
//
// [Array] and [Slice] need to specify the type of array/slice being
// compared then to index all expected items. List does not. It acts
// as comparing a literal array/slice, but without having to specify
// the type and allowing to easily use TestDeep operators:
//
// td.Cmp(t, []int{1, 9, 5}, td.List(1, 9, 5)) // succeeds
// td.Cmp(t, []int{1, 9, 5}, td.List(td.Gt(0), td.Between(8, 9), td.Lt(5))) // succeeds
// td.Cmp(t, []int{1, 9, 5}, td.List(1, 9)) // fails, 5 is extra
// td.Cmp(t, []int{1, 9, 5}, td.List(1, 9, 5, 4)) // fails, 4 is missing
//
// // works with slices/arrays of any type
// td.Cmp(t, personSlice, td.List(
// Person{Name: "Bob", Age: 32},
// Person{Name: "Alice", Age: 26},
// ))
//
// To flatten a non-[]any slice/array, use [Flatten] function
// and so avoid boring and inefficient copies:
//
// expected := []int{1, 2, 1}
// td.Cmp(t, []int{1, 1, 2}, td.List(td.Flatten(expected))) // succeeds
// // = td.Cmp(t, []int{1, 1, 2}, td.List(1, 2, 1))
//
// // Compare only Name field of a slice of Person structs
// td.Cmp(t, personSlice, td.List(td.Flatten([]string{"Bob", "Alice"}, "Smuggle:Name")))
//
// TypeBehind method can return a non-nil [reflect.Type] if all items
// known non-interface types are equal, or if only interface types
// are found (mostly issued from Isa()) and they are equal.
//
// See also [Bag], [Set] and [Sort].
func List(expectedValues ...any) TestDeep {
return &tdList{
tdListBase: newListBase(expectedValues...),
}
}
func (l *tdList) Match(ctx ctxerr.Context, got reflect.Value) (err *ctxerr.Error) {
switch got.Kind() {
case 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"))
}
if gotElem.Kind() != reflect.Array && gotElem.Kind() != reflect.Slice {
break
}
got = gotElem
fallthrough
case reflect.Array, reflect.Slice:
gotLen, expectedLen := got.Len(), len(l.items)
if ctx.BooleanError && gotLen != expectedLen {
return ctxerr.BooleanError // shortcut in boolean context
}
var maxLen int
if gotLen >= expectedLen {
maxLen = expectedLen
} else {
maxLen = gotLen
}
for i := 0; i < maxLen; i++ {
err = deepValueEqual(ctx.AddArrayIndex(i), got.Index(i), l.items[i])
if err != nil {
return err
}
}
if gotLen == expectedLen {
return
}
res := tdSetResult{
Kind: itemsSetResult,
// do not sort Extra/Mising here
}
if gotLen > expectedLen {
res.Extra = make([]reflect.Value, gotLen-expectedLen)
for i := expectedLen; i < gotLen; i++ {
res.Extra[i-expectedLen] = got.Index(i)
}
} else {
res.Missing = l.items[gotLen:]
}
return ctx.CollectError(&ctxerr.Error{
Message: fmt.Sprintf("comparing %s, from index #%d", got.Kind(), maxLen),
Summary: res.Summary(),
})
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(ctxerr.BadKind(got, "slice OR array OR *slice OR *array"))
}
func (l *tdList) TypeBehind() reflect.Type {
typ := uniqTypeBehindSlice(l.items)
if typ == nil {
return nil
}
return reflect.SliceOf(typ)
}
go-testdeep-1.15.0/td/td_list_test.go 0000664 0000000 0000000 00000006572 15144170453 0017473 0 ustar 00root root 0000000 0000000 // Copyright (c) 2025, 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 (
"fmt"
"reflect"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestList(t *testing.T) {
type MyArray [5]int
type MySlice []int
for idx, got := range []any{
[]int{1, 3, 4, 4, 5},
[...]int{1, 3, 4, 4, 5},
MySlice{1, 3, 4, 4, 5},
MyArray{1, 3, 4, 4, 5},
&MySlice{1, 3, 4, 4, 5},
&MyArray{1, 3, 4, 4, 5},
} {
testName := fmt.Sprintf("Test #%d → %v", idx, got)
checkOK(t, got, td.List(1, 3, 4, 4, 5), testName)
typ := reflect.TypeOf(got)
if typ.Kind() == reflect.Ptr {
typ = typ.Elem()
}
checkError(t, got, td.List(1, 3, 4),
expectedError{
Message: mustBe(fmt.Sprintf("comparing %s, from index #3", typ.Kind())),
Path: mustBe("DATA"),
Summary: mustBe("Extra 2 items: (4,\n 5)"),
},
testName)
checkError(t, got, td.List(1, 3, 4, 4, 5, 666),
expectedError{
Message: mustBe(fmt.Sprintf("comparing %s, from index #5", typ.Kind())),
Path: mustBe("DATA"),
Summary: mustBe("Missing item: (666)"),
},
testName)
checkError(t, got, td.List(1, 3, 666, 4, 5),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[2]"),
Got: mustBe("4"),
Expected: mustBe("666"),
},
testName)
// Lax
checkOK(t, got, td.Lax(td.Bag(float64(1), 3, 4, 4, uint8(5))), testName)
}
var nilSlice MySlice
for idx, got := range []any{([]int)(nil), &nilSlice} {
checkOK(t, got, td.List(), "Test #%d", idx)
}
checkError(t, 123, td.List(123),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("slice OR array OR *slice OR *array"),
})
num := 123
checkError(t, &num, td.List(123),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("*int"),
Expected: mustBe("slice OR array OR *slice OR *array"),
})
var list *MySlice
checkError(t, list, td.List(123),
expectedError{
Message: mustBe("nil pointer"),
Path: mustBe("DATA"),
Got: mustBe("nil *slice (*td_test.MySlice type)"),
Expected: mustBe("non-nil *slice OR *array"),
})
checkError(t, nil, td.List(123),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("slice OR array OR *slice OR *array"),
})
//
// String
test.EqualStr(t, td.List(1).String(), "List(1)")
test.EqualStr(t, td.List(1, 2).String(), "List(1,\n 2)")
}
func TestListTypeBehind(t *testing.T) {
equalTypes(t, td.List(6, 5), ([]int)(nil))
equalTypes(t, td.List(6, "foo"), nil)
// Always the same non-interface type (even if we encounter several
// interface types)
equalTypes(t,
td.List(
td.Empty(),
5,
td.Isa((*error)(nil)), // interface type (in fact pointer to ...)
td.All(6, 7),
td.Isa((*fmt.Stringer)(nil)), // interface type
8),
([]int)(nil))
// Only one interface type
equalTypes(t,
td.List(
td.Isa((*error)(nil)),
td.Isa((*error)(nil)),
td.Isa((*error)(nil)),
),
([]*error)(nil))
// Several interface types, cannot be sure
equalTypes(t,
td.List(
td.Isa((*error)(nil)),
td.Isa((*fmt.Stringer)(nil)),
),
nil)
}
go-testdeep-1.15.0/td/td_map.go 0000664 0000000 0000000 00000024404 15144170453 0016230 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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"
"strings"
"github.com/maxatome/go-testdeep/helpers/tdutil"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/dark"
"github.com/maxatome/go-testdeep/internal/util"
)
type mapKind uint8
const (
allMap mapKind = iota
subMap
superMap
)
type tdMap struct {
tdExpectedType
expectedEntries []mapEntryInfo
kind mapKind
}
var _ TestDeep = &tdMap{}
type mapEntryInfo struct {
key reflect.Value
expected reflect.Value
}
// MapEntries allows to pass map entries to check in functions [Map],
// [SubMapOf] and [SuperMapOf]. It is a map whose each key is the
// expected entry key and the corresponding value the expected entry
// value (which can be a [TestDeep] operator as well as a zero value.)
type MapEntries map[any]any
func mergeMapEntries(mes ...MapEntries) MapEntries {
switch len(mes) {
case 0:
return nil
case 1:
return mes[0]
}
ret := make(MapEntries, len(mes[0]))
for _, me := range mes {
for k, v := range me {
ret[k] = v
}
}
return ret
}
func newMap(model any, kind mapKind, mes ...MapEntries) *tdMap {
vmodel := reflect.ValueOf(model)
m := tdMap{
tdExpectedType: tdExpectedType{
base: newBase(4),
},
kind: kind,
}
switch vmodel.Kind() {
case reflect.Ptr:
if vmodel.Type().Elem().Kind() != reflect.Map {
break
}
m.isPtr = true
if vmodel.IsNil() {
m.expectedType = vmodel.Type().Elem()
m.populateExpectedEntries(mergeMapEntries(mes...), reflect.Value{})
return &m
}
vmodel = vmodel.Elem()
fallthrough
case reflect.Map:
m.expectedType = vmodel.Type()
m.populateExpectedEntries(mergeMapEntries(mes...), vmodel)
return &m
}
m.err = ctxerr.OpBadUsage(
m.GetLocation().Func, "(MAP|&MAP, EXPECTED_ENTRIES)",
model, 1, true)
return &m
}
func (m *tdMap) populateExpectedEntries(entries MapEntries, expectedModel reflect.Value) {
var keysInModel int
if expectedModel.IsValid() {
keysInModel = expectedModel.Len()
}
m.expectedEntries = make([]mapEntryInfo, 0, keysInModel+len(entries))
checkedEntries := make(map[any]bool, len(entries))
keyType := m.expectedType.Key()
valueType := m.expectedType.Elem()
var entryInfo mapEntryInfo
for key, expectedValue := range entries {
vkey := reflect.ValueOf(key)
if !vkey.Type().AssignableTo(keyType) {
m.err = ctxerr.OpBad(
m.GetLocation().Func,
"expected key %s type mismatch: %s != model key type (%s)",
util.ToString(key),
vkey.Type(),
keyType)
return
}
if expectedValue == nil {
switch valueType.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map,
reflect.Ptr, reflect.Slice:
entryInfo.expected = reflect.Zero(valueType) // change to a typed nil
default:
entryInfo.expected = reflect.Value{}
// Don't raise an error if map value cannot be nil as a
// smuggle hook can change it at fly during the comparison
}
} else {
entryInfo.expected = reflect.ValueOf(expectedValue)
// Don't check vexpectedValue type against map value one as a
// smuggle hook can change it at fly during the comparison
}
entryInfo.key = vkey
m.expectedEntries = append(m.expectedEntries, entryInfo)
checkedEntries[dark.MustGetInterface(vkey)] = true
}
// Check entries in model
if keysInModel == 0 {
return
}
tdutil.MapEach(expectedModel, func(k, v reflect.Value) bool {
entryInfo.expected = v
if checkedEntries[dark.MustGetInterface(k)] {
m.err = ctxerr.OpBad(
m.GetLocation().Func,
"%s entry exists in both model & expectedEntries",
util.ToString(k))
return false
}
entryInfo.key = k
m.expectedEntries = append(m.expectedEntries, entryInfo)
return true
})
}
// summary(Map): compares the contents of a map
// input(Map): map,ptr(ptr on map)
// Map operator compares the contents of a map against the non-zero
// values of model (if any) and the values of expectedEntries.
//
// model must be the same type as compared data.
//
// expectedEntries can be omitted, if no [TestDeep] operators are
// involved. If expectedEntries contains more than one item, all items
// are merged before their use, from left to right.
//
// During a match, all expected entries must be found and all data
// entries must be expected to succeed.
//
// got := map[string]string{
// "foo": "test",
// "bar": "wizz",
// "zip": "buzz",
// }
// td.Cmp(t, got, td.Map(
// map[string]string{
// "foo": "test",
// "bar": "wizz",
// },
// td.MapEntries{
// "zip": td.HasSuffix("zz"),
// }),
// ) // succeeds
//
// TypeBehind method returns the [reflect.Type] of model.
//
// See also [SubMapOf] and [SuperMapOf].
func Map(model any, expectedEntries ...MapEntries) TestDeep {
return newMap(model, allMap, expectedEntries...)
}
// summary(SubMapOf): compares the contents of a map but with
// potentially some exclusions
// input(SubMapOf): map,ptr(ptr on map)
// SubMapOf operator compares the contents of a map against the non-zero
// values of model (if any) and the values of expectedEntries.
//
// model must be the same type as compared data.
//
// expectedEntries can be omitted, if no [TestDeep] operators are
// involved. If expectedEntries contains more than one item, all items
// are merged before their use, from left to right.
//
// During a match, each map entry should be matched by an expected
// entry to succeed. But some expected entries can be missing from the
// compared map.
//
// got := map[string]string{
// "foo": "test",
// "zip": "buzz",
// }
// td.Cmp(t, got, td.SubMapOf(
// map[string]string{
// "foo": "test",
// "bar": "wizz",
// },
// td.MapEntries{
// "zip": td.HasSuffix("zz"),
// }),
// ) // succeeds
//
// td.Cmp(t, got, td.SubMapOf(
// map[string]string{
// "bar": "wizz",
// },
// td.MapEntries{
// "zip": td.HasSuffix("zz"),
// }),
// ) // fails, extra {"foo": "test"} in got
//
// TypeBehind method returns the [reflect.Type] of model.
//
// See also [Map] and [SuperMapOf].
func SubMapOf(model any, expectedEntries ...MapEntries) TestDeep {
return newMap(model, subMap, expectedEntries...)
}
// summary(SuperMapOf): compares the contents of a map but with
// potentially some extra entries
// input(SuperMapOf): map,ptr(ptr on map)
// SuperMapOf operator compares the contents of a map against the non-zero
// values of model (if any) and the values of expectedEntries.
//
// model must be the same type as compared data.
//
// expectedEntries can be omitted, if no [TestDeep] operators are
// involved. If expectedEntries contains more than one item, all items
// are merged before their use, from left to right.
//
// During a match, each expected entry should match in the compared
// map. But some entries in the compared map may not be expected.
//
// got := map[string]string{
// "foo": "test",
// "bar": "wizz",
// "zip": "buzz",
// }
// td.Cmp(t, got, td.SuperMapOf(
// map[string]string{
// "foo": "test",
// },
// td.MapEntries{
// "zip": td.HasSuffix("zz"),
// }),
// ) // succeeds
//
// td.Cmp(t, got, td.SuperMapOf(
// map[string]string{
// "foo": "test",
// },
// td.MapEntries{
// "biz": td.HasSuffix("zz"),
// }),
// ) // fails, missing {"biz": …} in got
//
// TypeBehind method returns the [reflect.Type] of model.
//
// See also [SuperMapOf] and [SubMapOf].
func SuperMapOf(model any, expectedEntries ...MapEntries) TestDeep {
return newMap(model, superMap, expectedEntries...)
}
func (m *tdMap) Match(ctx ctxerr.Context, got reflect.Value) (err *ctxerr.Error) {
if m.err != nil {
return ctx.CollectError(m.err)
}
err = m.checkPtr(ctx, &got, true)
if err != nil {
return ctx.CollectError(err)
}
return m.match(ctx, got)
}
func (m *tdMap) match(ctx ctxerr.Context, got reflect.Value) (err *ctxerr.Error) {
err = m.checkType(ctx, got)
if err != nil {
return ctx.CollectError(err)
}
var notFoundKeys []reflect.Value
foundKeys := map[any]bool{}
for _, entryInfo := range m.expectedEntries {
gotValue := got.MapIndex(entryInfo.key)
if !gotValue.IsValid() {
notFoundKeys = append(notFoundKeys, entryInfo.key)
continue
}
err = deepValueEqual(ctx.AddMapKey(entryInfo.key),
gotValue, entryInfo.expected)
if err != nil {
return err
}
foundKeys[dark.MustGetInterface(entryInfo.key)] = true
}
const errorMessage = "comparing hash keys of %%"
// For SuperMapOf we don't care about extra keys
if m.kind == superMap {
if len(notFoundKeys) == 0 {
return nil
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: errorMessage,
Summary: (tdSetResult{
Kind: keysSetResult,
Missing: notFoundKeys,
Sort: true,
}).Summary(),
})
}
// No extra key to search, all got keys have been found
if got.Len() == len(foundKeys) {
if m.kind == subMap {
return nil
}
// allMap
if len(notFoundKeys) == 0 {
return nil
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: errorMessage,
Summary: (tdSetResult{
Kind: keysSetResult,
Missing: notFoundKeys,
Sort: true,
}).Summary(),
})
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
// Retrieve extra keys
res := tdSetResult{
Kind: keysSetResult,
Missing: notFoundKeys,
Extra: make([]reflect.Value, 0, got.Len()-len(foundKeys)),
Sort: true,
}
for _, k := range tdutil.MapSortedKeys(got) {
if !foundKeys[dark.MustGetInterface(k)] {
res.Extra = append(res.Extra, k)
}
}
return ctx.CollectError(&ctxerr.Error{
Message: errorMessage,
Summary: res.Summary(),
})
}
func (m *tdMap) String() string {
if m.err != nil {
return m.stringError()
}
var buf strings.Builder
if m.kind != allMap {
buf.WriteString(m.GetLocation().Func)
buf.WriteByte('(')
}
buf.WriteString(m.expectedTypeStr())
if len(m.expectedEntries) == 0 {
buf.WriteString("{}")
} else {
buf.WriteString("{\n")
for _, entryInfo := range m.expectedEntries {
fmt.Fprintf(&buf, " %s: %s,\n", //nolint: errcheck
util.ToString(entryInfo.key),
util.ToString(entryInfo.expected))
}
buf.WriteByte('}')
}
if m.kind != allMap {
buf.WriteByte(')')
}
return buf.String()
}
go-testdeep-1.15.0/td/td_map_each.go 0000664 0000000 0000000 00000004421 15144170453 0017205 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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"
"strings"
"github.com/maxatome/go-testdeep/helpers/tdutil"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/types"
"github.com/maxatome/go-testdeep/internal/util"
)
type tdMapEach struct {
baseOKNil
expected reflect.Value
}
var _ TestDeep = &tdMapEach{}
// summary(MapEach): compares each map entry
// input(MapEach): map,ptr(ptr on map)
// MapEach operator has to be applied on maps. It compares each value
// of data map against expectedValue. During a match, all values have
// to match to succeed.
//
// got := map[string]string{"test": "foo", "buzz": "bar"}
// td.Cmp(t, got, td.MapEach("bar")) // fails, coz "foo" ≠ "bar"
// td.Cmp(t, got, td.MapEach(td.Len(3))) // succeeds as values are 3 chars long
func MapEach(expectedValue any) TestDeep {
return &tdMapEach{
baseOKNil: newBaseOKNil(3),
expected: reflect.ValueOf(expectedValue),
}
}
func (m *tdMapEach) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if !got.IsValid() {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "nil value",
Got: types.RawString("nil"),
Expected: types.RawString("map OR *map"),
})
}
switch got.Kind() {
case reflect.Ptr:
gotElem := got.Elem()
if !gotElem.IsValid() {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(ctxerr.NilPointer(got, "non-nil *map"))
}
if gotElem.Kind() != reflect.Map {
break
}
got = gotElem
fallthrough
case reflect.Map:
var err *ctxerr.Error
tdutil.MapEach(got, func(k, v reflect.Value) bool {
err = deepValueEqual(ctx.AddMapKey(k), v, m.expected)
return err == nil
})
return err
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(ctxerr.BadKind(got, "map OR *map"))
}
func (m *tdMapEach) String() string {
const prefix = "MapEach("
content := util.ToString(m.expected)
if strings.Contains(content, "\n") {
return prefix + util.IndentString(content, " ") + ")"
}
return prefix + content + ")"
}
go-testdeep-1.15.0/td/td_map_each_test.go 0000664 0000000 0000000 00000005557 15144170453 0020257 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 TestMapEach(t *testing.T) {
type MyMap map[string]int
checkOKForEach(t,
[]any{
map[string]int{"foo": 1, "bar": 1},
&map[string]int{"foo": 1, "bar": 1},
MyMap{"foo": 1, "bar": 1},
&MyMap{"foo": 1, "bar": 1},
},
td.MapEach(1))
checkOKForEach(t,
[]any{
map[int]string{1: "foo", 2: "bar"},
&map[int]string{1: "foo", 2: "bar"},
},
td.MapEach(td.Len(3)))
checkOKForEach(t,
[]any{
map[string]int{},
&map[string]int{},
MyMap{},
&MyMap{},
},
td.MapEach(1))
checkOK(t, (map[string]int)(nil), td.MapEach(1))
checkOK(t, (MyMap)(nil), td.MapEach(1))
checkError(t, (*MyMap)(nil), td.MapEach(4),
expectedError{
Message: mustBe("nil pointer"),
Path: mustBe("DATA"),
Got: mustBe("nil *map (*td_test.MyMap type)"),
Expected: mustBe("non-nil *map"),
})
checkOKForEach(t,
[]any{
map[string]int{"foo": 20, "bar": 22, "test": 29},
&map[string]int{"foo": 20, "bar": 22, "test": 29},
MyMap{"foo": 20, "bar": 22, "test": 29},
&MyMap{"foo": 20, "bar": 22, "test": 29},
},
td.MapEach(td.Between(20, 30)))
checkError(t, nil, td.MapEach(4),
expectedError{
Message: mustBe("nil value"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("map OR *map"),
})
checkErrorForEach(t,
[]any{
map[string]int{"foo": 4, "bar": 5, "test": 4},
&map[string]int{"foo": 4, "bar": 5, "test": 4},
MyMap{"foo": 4, "bar": 5, "test": 4},
&MyMap{"foo": 4, "bar": 5, "test": 4},
},
td.MapEach(4),
expectedError{
Message: mustBe("values differ"),
Path: mustBe(`DATA["bar"]`),
Got: mustBe("5"),
Expected: mustBe("4"),
})
checkError(t, 666, td.MapEach(4),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("map OR *map"),
})
num := 666
checkError(t, &num, td.MapEach(4),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("*int"),
Expected: mustBe("map OR *map"),
})
checkOK(t, map[string]any{"a": nil, "b": nil, "c": nil},
td.MapEach(nil))
checkError(t,
map[string]any{"a": nil, "b": nil, "c": nil, "d": 66},
td.MapEach(nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe(`DATA["d"]`),
Got: mustBe("66"),
Expected: mustBe("nil"),
})
//
// String
test.EqualStr(t, td.MapEach(4).String(), "MapEach(4)")
test.EqualStr(t, td.MapEach(td.All(1, 2)).String(),
`MapEach(All(1,
2))`)
}
func TestMapEachTypeBehind(t *testing.T) {
equalTypes(t, td.MapEach(4), nil)
}
go-testdeep-1.15.0/td/td_map_test.go 0000664 0000000 0000000 00000035473 15144170453 0017277 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 TestMap(t *testing.T) {
type MyMap map[string]int
//
// Map
checkOK(t, (map[string]int)(nil), td.Map(map[string]int{}, nil))
checkError(t, nil, td.Map(map[string]int{}, nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe(`DATA`),
Got: mustBe("nil"),
Expected: mustBe("map[string]int{}"),
})
gotMap := map[string]int{"foo": 1, "bar": 2}
checkOK(t, gotMap, td.Map(map[string]int{"foo": 1, "bar": 2}))
checkOK(t, gotMap, td.Map(map[string]int{"foo": 1, "bar": 2}, nil))
checkOK(t, gotMap,
td.Map(map[string]int{"foo": 1}, td.MapEntries{"bar": 2}))
checkOK(t, gotMap,
td.Map(map[string]int{}, td.MapEntries{"foo": 1, "bar": 2}))
checkOK(t, gotMap,
td.Map((map[string]int)(nil), td.MapEntries{"foo": 1, "bar": 2}))
checkOK(t, gotMap,
td.Map((map[string]int)(nil),
td.MapEntries{"foo": 66, "bar": 0},
td.MapEntries{"foo": 42, "bar": 1},
td.MapEntries{"foo": 1, "bar": 2},
))
one := 1
checkOK(t, map[string]*int{"foo": nil, "bar": &one},
td.Map(map[string]*int{}, td.MapEntries{"foo": nil, "bar": &one}))
checkError(t, gotMap, td.Map(map[string]int{"foo": 1, "bar": 3}, nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe(`DATA["bar"]`),
Got: mustBe("2"),
Expected: mustBe("3"),
})
checkError(t, gotMap, td.Map(map[string]int{}, nil),
expectedError{
Message: mustBe("comparing hash keys of %%"),
Path: mustBe("DATA"),
Summary: mustMatch(`^Extra 2 keys: \("bar",\s+"foo"\)\z`),
})
checkError(t, gotMap, td.Map(map[string]int{"test": 2}, nil),
expectedError{
Message: mustBe("comparing hash keys of %%"),
Path: mustBe("DATA"),
Summary: mustMatch(
`^ Missing key: \("test"\)\nExtra 2 keys: \("bar",\s+"foo"\)\z`),
})
checkError(t, gotMap,
td.Map(map[string]int{}, td.MapEntries{"test": 2}),
expectedError{
Message: mustBe("comparing hash keys of %%"),
Path: mustBe("DATA"),
Summary: mustMatch(
`^ Missing key: \("test"\)\nExtra 2 keys: \("bar",\s+"foo"\)\z`),
})
checkError(t, gotMap,
td.Map(map[string]int{}, td.MapEntries{"foo": 1, "bar": 2, "test": 2}),
expectedError{
Message: mustBe("comparing hash keys of %%"),
Path: mustBe("DATA"),
Summary: mustBe(`Missing key: ("test")`),
})
checkError(t, gotMap,
td.Map(MyMap{}, td.MapEntries{"foo": 1, "bar": 2}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("map[string]int"),
Expected: mustBe("td_test.MyMap"),
})
//
// Map type
gotTypedMap := MyMap{"foo": 1, "bar": 2}
checkOK(t, gotTypedMap, td.Map(MyMap{"foo": 1, "bar": 2}, nil))
checkOK(t, gotTypedMap,
td.Map(MyMap{"foo": 1}, td.MapEntries{"bar": 2}))
checkOK(t, gotTypedMap,
td.Map(MyMap{}, td.MapEntries{"foo": 1, "bar": 2}))
checkOK(t, gotTypedMap,
td.Map((MyMap)(nil), td.MapEntries{"foo": 1, "bar": 2}))
checkOK(t, &gotTypedMap, td.Map(&MyMap{"foo": 1, "bar": 2}, nil))
checkOK(t, &gotTypedMap,
td.Map(&MyMap{"foo": 1}, td.MapEntries{"bar": 2}))
checkOK(t, &gotTypedMap,
td.Map(&MyMap{}, td.MapEntries{"foo": 1, "bar": 2}))
checkOK(t, &gotTypedMap,
td.Map((*MyMap)(nil), td.MapEntries{"foo": 1, "bar": 2}))
checkError(t, gotTypedMap, td.Map(MyMap{"foo": 1, "bar": 3}, nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe(`DATA["bar"]`),
Got: mustBe("2"),
Expected: mustBe("3"),
})
checkError(t, gotTypedMap, td.Map(MyMap{}, nil),
expectedError{
Message: mustBe("comparing hash keys of %%"),
Path: mustBe("DATA"),
Summary: mustMatch(`^Extra 2 keys: \("bar",\s+"foo"\)\z`),
})
checkError(t, gotTypedMap, td.Map(MyMap{"test": 2}, nil),
expectedError{
Message: mustBe("comparing hash keys of %%"),
Path: mustBe("DATA"),
Summary: mustMatch(
`^ Missing key: \("test"\)\nExtra 2 keys: \("bar",\s+"foo"\)\z`),
})
checkError(t, gotTypedMap, td.Map(MyMap{}, td.MapEntries{"test": 2}),
expectedError{
Message: mustBe("comparing hash keys of %%"),
Path: mustBe("DATA"),
Summary: mustMatch(
`^ Missing key: \("test"\)\nExtra 2 keys: \("bar",\s+"foo"\)\z`),
})
checkError(t, gotTypedMap,
td.Map(MyMap{}, td.MapEntries{"foo": 1, "bar": 2, "test": 2}),
expectedError{
Message: mustBe("comparing hash keys of %%"),
Path: mustBe("DATA"),
Summary: mustBe(`Missing key: ("test")`),
})
checkError(t, &gotTypedMap, td.Map(&MyMap{"foo": 1, "bar": 3}, nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe(`DATA["bar"]`),
Got: mustBe("2"),
Expected: mustBe("3"),
})
checkError(t, &gotTypedMap, td.Map(&MyMap{}, nil),
expectedError{
Message: mustBe("comparing hash keys of %%"),
Path: mustBe("DATA"),
Summary: mustMatch(`^Extra 2 keys: \("bar",\s+"foo"\)\z`),
})
checkError(t, &gotTypedMap, td.Map(&MyMap{"test": 2}, nil),
expectedError{
Message: mustBe("comparing hash keys of %%"),
Path: mustBe("DATA"),
Summary: mustMatch(
`^ Missing key: \("test"\)\nExtra 2 keys: \("bar",\s+"foo"\)\z`),
})
checkError(t, &gotTypedMap, td.Map(&MyMap{}, td.MapEntries{"test": 2}),
expectedError{
Message: mustBe("comparing hash keys of %%"),
Path: mustBe("DATA"),
Summary: mustMatch(
`^ Missing key: \("test"\)\nExtra 2 keys: \("bar",\s+"foo"\)\z`),
})
checkError(t, &gotTypedMap,
td.Map(&MyMap{}, td.MapEntries{"foo": 1, "bar": 2, "test": 2}),
expectedError{
Message: mustBe("comparing hash keys of %%"),
Path: mustBe("DATA"),
Summary: mustBe(`Missing key: ("test")`),
})
checkError(t, &gotMap, td.Map(&MyMap{}, nil),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("*map[string]int"),
Expected: mustBe("*td_test.MyMap"),
})
checkError(t, gotMap, td.Map(&MyMap{}, nil),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("map[string]int"),
Expected: mustBe("*td_test.MyMap"),
})
checkError(t, nil, td.Map(&MyMap{}, nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("*td_test.MyMap{}"),
})
checkError(t, nil, td.Map(MyMap{}, nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("td_test.MyMap{}"),
})
//
// nil cases
var (
gotNilMap map[string]int
gotNilTypedMap MyMap
)
checkOK(t, gotNilMap, td.Map(map[string]int{}, nil))
checkOK(t, gotNilTypedMap, td.Map(MyMap{}, nil))
checkOK(t, &gotNilTypedMap, td.Map(&MyMap{}, nil))
// Be lax...
// Without Lax → error
checkError(t, MyMap{}, td.Map(map[string]int{}, nil),
expectedError{
Message: mustBe("type mismatch"),
})
checkError(t, map[string]int{}, td.Map(MyMap{}, nil),
expectedError{
Message: mustBe("type mismatch"),
})
// With Lax → OK
checkOK(t, MyMap{}, td.Lax(td.Map(map[string]int{}, nil)))
checkOK(t, map[string]int{}, td.Lax(td.Map(MyMap{}, nil)))
//
// SuperMapOf
checkOK(t, gotMap, td.SuperMapOf(map[string]int{"foo": 1}))
checkOK(t, gotMap, td.SuperMapOf(map[string]int{"foo": 1}, nil))
checkOK(t, gotMap,
td.SuperMapOf(map[string]int{"foo": 1}, td.MapEntries{"bar": 2}))
checkOK(t, gotMap,
td.SuperMapOf(map[string]int{}, td.MapEntries{"foo": 1, "bar": 2}))
checkOK(t, gotMap,
td.SuperMapOf(map[string]int{"foo": 1},
td.MapEntries{"bar": 0},
td.MapEntries{"bar": 1},
td.MapEntries{"bar": 2},
))
checkError(t, gotMap,
td.SuperMapOf(map[string]int{"foo": 1, "bar": 3}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe(`DATA["bar"]`),
Got: mustBe("2"),
Expected: mustBe("3"),
})
checkError(t, gotMap,
td.SuperMapOf(map[string]int{"foo": 1, "bar": 3}, nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe(`DATA["bar"]`),
Got: mustBe("2"),
Expected: mustBe("3"),
})
checkError(t, gotMap, td.SuperMapOf(map[string]int{"test": 2}),
expectedError{
Message: mustBe("comparing hash keys of %%"),
Path: mustBe("DATA"),
Summary: mustBe(`Missing key: ("test")`),
})
checkError(t, gotMap,
td.SuperMapOf(map[string]int{}, td.MapEntries{"test": 2}),
expectedError{
Message: mustBe("comparing hash keys of %%"),
Path: mustBe("DATA"),
Summary: mustBe(`Missing key: ("test")`),
})
checkOK(t, gotNilMap, td.SuperMapOf(map[string]int{}, nil))
checkOK(t, gotNilTypedMap, td.SuperMapOf(MyMap{}, nil))
checkOK(t, &gotNilTypedMap, td.SuperMapOf(&MyMap{}, nil))
//
// SubMapOf
checkOK(t, gotMap,
td.SubMapOf(map[string]int{"foo": 1, "bar": 2, "tst": 3}))
checkOK(t, gotMap,
td.SubMapOf(map[string]int{"foo": 1, "bar": 2, "tst": 3}, nil))
checkOK(t, gotMap,
td.SubMapOf(map[string]int{"foo": 1, "tst": 3}, td.MapEntries{"bar": 2}))
checkOK(t, gotMap,
td.SubMapOf(map[string]int{}, td.MapEntries{"foo": 1, "bar": 2, "tst": 3}))
checkOK(t, gotMap,
td.SubMapOf(map[string]int{"foo": 1, "tst": 3},
td.MapEntries{"bar": 0},
td.MapEntries{"bar": 1},
td.MapEntries{"bar": 2},
))
checkError(t, gotMap,
td.SubMapOf(map[string]int{"foo": 1, "bar": 3}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe(`DATA["bar"]`),
Got: mustBe("2"),
Expected: mustBe("3"),
})
checkError(t, gotMap,
td.SubMapOf(map[string]int{"foo": 1, "bar": 3}, nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe(`DATA["bar"]`),
Got: mustBe("2"),
Expected: mustBe("3"),
})
checkError(t, gotMap, td.SubMapOf(map[string]int{"foo": 1}),
expectedError{
Message: mustBe("comparing hash keys of %%"),
Path: mustBe("DATA"),
Summary: mustBe(`Extra key: ("bar")`),
})
checkError(t, gotMap,
td.SubMapOf(map[string]int{}, td.MapEntries{"foo": 1, "test": 2}),
expectedError{
Message: mustBe("comparing hash keys of %%"),
Path: mustBe("DATA"),
Summary: mustBe(`Missing key: ("test")
Extra key: ("bar")`),
})
checkOK(t, gotNilMap, td.SubMapOf(map[string]int{"foo": 1}, nil))
checkOK(t, gotNilTypedMap, td.SubMapOf(MyMap{"foo": 1}, nil))
checkOK(t, &gotNilTypedMap, td.SubMapOf(&MyMap{"foo": 1}, nil))
//
// Bad usage
checkError(t, "never tested",
td.Map("test", nil),
expectedError{
Message: mustBe("bad usage of Map operator"),
Path: mustBe("DATA"),
Summary: mustContain("usage: Map("),
})
checkError(t, "never tested",
td.SuperMapOf("test", nil),
expectedError{
Message: mustBe("bad usage of SuperMapOf operator"),
Path: mustBe("DATA"),
Summary: mustContain("usage: SuperMapOf("),
})
checkError(t, "never tested",
td.SubMapOf("test", nil),
expectedError{
Message: mustBe("bad usage of SubMapOf operator"),
Path: mustBe("DATA"),
Summary: mustContain("usage: SubMapOf("),
})
num := 12
checkError(t, "never tested",
td.Map(&num, nil),
expectedError{
Message: mustBe("bad usage of Map operator"),
Path: mustBe("DATA"),
Summary: mustContain("usage: Map("),
})
checkError(t, "never tested",
td.SuperMapOf(&num, nil),
expectedError{
Message: mustBe("bad usage of SuperMapOf operator"),
Path: mustBe("DATA"),
Summary: mustContain("usage: SuperMapOf("),
})
checkError(t, "never tested",
td.SubMapOf(&num, nil),
expectedError{
Message: mustBe("bad usage of SubMapOf operator"),
Path: mustBe("DATA"),
Summary: mustContain("usage: SubMapOf("),
})
checkError(t, "never tested",
td.Map(&MyMap{}, td.MapEntries{1: 2}),
expectedError{
Message: mustBe("bad usage of Map operator"),
Path: mustBe("DATA"),
Summary: mustBe("expected key 1 type mismatch: int != model key type (string)"),
})
checkError(t, "never tested",
td.SuperMapOf(&MyMap{}, td.MapEntries{1: 2}),
expectedError{
Message: mustBe("bad usage of SuperMapOf operator"),
Path: mustBe("DATA"),
Summary: mustBe("expected key 1 type mismatch: int != model key type (string)"),
})
checkError(t, "never tested",
td.SubMapOf(&MyMap{}, td.MapEntries{1: 2}),
expectedError{
Message: mustBe("bad usage of SubMapOf operator"),
Path: mustBe("DATA"),
Summary: mustBe("expected key 1 type mismatch: int != model key type (string)"),
})
checkError(t, &MyMap{"foo": 42},
td.Map(&MyMap{}, td.MapEntries{"foo": nil}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe(`DATA["foo"]`),
Got: mustBe("42"),
Expected: mustBe("nil"),
})
checkError(t, &MyMap{"foo": 12},
td.Map(&MyMap{}, td.MapEntries{"foo": uint16(2)}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe(`DATA["foo"]`),
Got: mustBe("int"),
Expected: mustBe("uint16"),
})
checkError(t, "never tested",
td.Map(&MyMap{"foo": 1}, td.MapEntries{"foo": 1}),
expectedError{
Message: mustBe("bad usage of Map operator"),
Path: mustBe("DATA"),
Summary: mustBe(`"foo" entry exists in both model & expectedEntries`),
})
//
// String
test.EqualStr(t, td.Map(MyMap{}, nil).String(),
"td_test.MyMap{}")
test.EqualStr(t, td.Map(&MyMap{}, nil).String(),
"*td_test.MyMap{}")
test.EqualStr(t, td.Map(&MyMap{"foo": 2}, nil).String(),
`*td_test.MyMap{
"foo": 2,
}`)
test.EqualStr(t, td.SubMapOf(MyMap{}, nil).String(),
"SubMapOf(td_test.MyMap{})")
test.EqualStr(t, td.SubMapOf(&MyMap{}, nil).String(),
"SubMapOf(*td_test.MyMap{})")
test.EqualStr(t, td.SubMapOf(&MyMap{"foo": 2}, nil).String(),
`SubMapOf(*td_test.MyMap{
"foo": 2,
})`)
test.EqualStr(t, td.SuperMapOf(MyMap{}, nil).String(),
"SuperMapOf(td_test.MyMap{})")
test.EqualStr(t, td.SuperMapOf(&MyMap{}, nil).String(),
"SuperMapOf(*td_test.MyMap{})")
test.EqualStr(t, td.SuperMapOf(&MyMap{"foo": 2}, nil).String(),
`SuperMapOf(*td_test.MyMap{
"foo": 2,
})`)
// Erroneous op
test.EqualStr(t, td.Map(12, nil).String(), "Map()")
test.EqualStr(t, td.SubMapOf(12, nil).String(), "SubMapOf()")
test.EqualStr(t, td.SuperMapOf(12, nil).String(), "SuperMapOf()")
}
func TestMapTypeBehind(t *testing.T) {
type MyMap map[string]int
// Map
equalTypes(t, td.Map(map[string]int{}, nil), map[string]int{})
equalTypes(t, td.Map(MyMap{}, nil), MyMap{})
equalTypes(t, td.Map(&MyMap{}, nil), &MyMap{})
// SubMap
equalTypes(t, td.SubMapOf(map[string]int{}, nil), map[string]int{})
equalTypes(t, td.SubMapOf(MyMap{}, nil), MyMap{})
equalTypes(t, td.SubMapOf(&MyMap{}, nil), &MyMap{})
// SuperMap
equalTypes(t, td.SuperMapOf(map[string]int{}, nil), map[string]int{})
equalTypes(t, td.SuperMapOf(MyMap{}, nil), MyMap{})
equalTypes(t, td.SuperMapOf(&MyMap{}, nil), &MyMap{})
// Erroneous op
equalTypes(t, td.Map(12, nil), nil)
equalTypes(t, td.SubMapOf(12, nil), nil)
equalTypes(t, td.SuperMapOf(12, nil), nil)
}
go-testdeep-1.15.0/td/td_nan.go 0000664 0000000 0000000 00000004357 15144170453 0016234 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (
"math"
"reflect"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/types"
)
type tdNaN struct {
base
}
var _ TestDeep = &tdNaN{}
// summary(NaN): checks a floating number is [`math.NaN`]
// input(NaN): float
// NaN operator checks that data is a float and is not-a-number.
//
// got := math.NaN()
// td.Cmp(t, got, td.NaN()) // succeeds
// td.Cmp(t, 4.2, td.NaN()) // fails
//
// See also [NotNaN].
func NaN() TestDeep {
return &tdNaN{
base: newBase(3),
}
}
func (n *tdNaN) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
switch got.Kind() {
case reflect.Float32, reflect.Float64:
if math.IsNaN(got.Float()) {
return nil
}
return ctx.CollectError(&ctxerr.Error{
Message: "values differ",
Got: got,
Expected: n,
})
}
return ctx.CollectError(&ctxerr.Error{
Message: "type mismatch",
Got: types.RawString(got.Type().String()),
Expected: types.RawString("float32 OR float64"),
})
}
func (n *tdNaN) String() string {
return "NaN"
}
type tdNotNaN struct {
base
}
var _ TestDeep = &tdNotNaN{}
// summary(NotNaN): checks a floating number is not [`math.NaN`]
// input(NotNaN): float
// NotNaN operator checks that data is a float and is not not-a-number.
//
// got := math.NaN()
// td.Cmp(t, got, td.NotNaN()) // fails
// td.Cmp(t, 4.2, td.NotNaN()) // succeeds
// td.Cmp(t, 4, td.NotNaN()) // fails, as 4 is not a float
//
// See also [NaN].
func NotNaN() TestDeep {
return &tdNotNaN{
base: newBase(3),
}
}
func (n *tdNotNaN) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
switch got.Kind() {
case reflect.Float32, reflect.Float64:
if !math.IsNaN(got.Float()) {
return nil
}
return ctx.CollectError(&ctxerr.Error{
Message: "values differ",
Got: got,
Expected: n,
})
}
return ctx.CollectError(&ctxerr.Error{
Message: "type mismatch",
Got: types.RawString(got.Type().String()),
Expected: types.RawString("float32 OR float64"),
})
}
func (n *tdNotNaN) String() string {
return "not NaN"
}
go-testdeep-1.15.0/td/td_nan_test.go 0000664 0000000 0000000 00000003353 15144170453 0017266 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-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 (
"math"
"testing"
"github.com/maxatome/go-testdeep/td"
)
func TestNaN(t *testing.T) {
checkOK(t, math.NaN(), td.NaN())
checkOK(t, float32(math.NaN()), td.NaN())
checkError(t, float32(12), td.NaN(),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("(float32) 12"),
Expected: mustBe("NaN"),
})
checkError(t, float64(12), td.NaN(),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("12.0"),
Expected: mustBe("NaN"),
})
checkError(t, 12, td.NaN(),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("float32 OR float64"),
})
}
func TestNotNaN(t *testing.T) {
checkOK(t, float64(12), td.NotNaN())
checkOK(t, float32(12), td.NotNaN())
checkError(t, float32(math.NaN()), td.NotNaN(),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("(float32) NaN"),
Expected: mustBe("not NaN"),
})
checkError(t, math.NaN(), td.NotNaN(),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("NaN"),
Expected: mustBe("not NaN"),
})
checkError(t, 12, td.NotNaN(),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("float32 OR float64"),
})
}
func TestNaNTypeBehind(t *testing.T) {
equalTypes(t, td.NaN(), nil)
equalTypes(t, td.NotNaN(), nil)
}
go-testdeep-1.15.0/td/td_nil.go 0000664 0000000 0000000 00000005444 15144170453 0016240 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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"
)
type tdNil struct {
baseOKNil
}
var _ TestDeep = &tdNil{}
// summary(Nil): compares to nil
// input(Nil): nil,slice,map,ptr,chan,func
// Nil operator checks that data is nil (or is a non-nil interface,
// but containing a nil pointer.)
//
// var got *int
// td.Cmp(t, got, td.Nil()) // succeeds
// td.Cmp(t, got, nil) // fails as (*int)(nil) ≠ untyped nil
// td.Cmp(t, got, (*int)(nil)) // succeeds
//
// but:
//
// var got fmt.Stringer = (*bytes.Buffer)(nil)
// td.Cmp(t, got, td.Nil()) // succeeds
// td.Cmp(t, got, nil) // fails, as the interface is not nil
// got = nil
// td.Cmp(t, got, nil) // succeeds
//
// See also [Empty], [NotNil] and [Zero].
func Nil() TestDeep {
return &tdNil{
baseOKNil: newBaseOKNil(3),
}
}
func (n *tdNil) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if !got.IsValid() {
return nil
}
switch got.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface,
reflect.Map, reflect.Ptr, reflect.Slice:
if got.IsNil() {
return nil
}
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "non-nil",
Got: got,
Expected: n,
})
}
func (n *tdNil) String() string {
return "nil"
}
type tdNotNil struct {
baseOKNil
}
var _ TestDeep = &tdNotNil{}
// summary(NotNil): checks that data is not nil
// input(NotNil): nil,slice,map,ptr,chan,func
// NotNil operator checks that data is not nil (or is a non-nil
// interface, containing a non-nil pointer.)
//
// got := &Person{}
// td.Cmp(t, got, td.NotNil()) // succeeds
// td.Cmp(t, got, td.Not(nil)) // succeeds too, but be careful it is first
// // because of got type *Person ≠ untyped nil so prefer NotNil()
//
// but:
//
// var got fmt.Stringer = (*bytes.Buffer)(nil)
// td.Cmp(t, got, td.NotNil()) // fails
// td.Cmp(t, got, td.Not(nil)) // succeeds, as the interface is not nil
//
// See also [Nil], [NotEmpty] and [NotZero].
func NotNil() TestDeep {
return &tdNotNil{
baseOKNil: newBaseOKNil(3),
}
}
func (n *tdNotNil) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if got.IsValid() {
switch got.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface,
reflect.Map, reflect.Ptr, reflect.Slice:
if !got.IsNil() {
return nil
}
// All other kinds are non-nil by nature
default:
return nil
}
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "nil value",
Got: got,
Expected: n,
})
}
func (n *tdNotNil) String() string {
return "not nil"
}
go-testdeep-1.15.0/td/td_nil_test.go 0000664 0000000 0000000 00000005571 15144170453 0017300 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (
"bytes"
"fmt"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestNil(t *testing.T) {
checkOK(t, (func())(nil), td.Nil())
checkOK(t, ([]int)(nil), td.Nil())
checkOK(t, (map[bool]bool)(nil), td.Nil())
checkOK(t, (*int)(nil), td.Nil())
checkOK(t, (chan int)(nil), td.Nil())
checkOK(t, nil, td.Nil())
checkOK(t,
map[string]any{"foo": nil},
map[string]any{"foo": td.Nil()},
)
var got fmt.Stringer = (*bytes.Buffer)(nil)
checkOK(t, got, td.Nil())
checkError(t, 42, td.Nil(),
expectedError{
Message: mustBe("non-nil"),
Path: mustBe("DATA"),
Got: mustBe("42"),
Expected: mustBe("nil"),
})
num := 42
checkError(t, &num, td.Nil(),
expectedError{
Message: mustBe("non-nil"),
Path: mustBe("DATA"),
Got: mustMatch(`\(\*int\).*42`),
Expected: mustBe("nil"),
})
//
// String
test.EqualStr(t, td.Nil().String(), "nil")
}
func TestNotNil(t *testing.T) {
num := 42
checkOK(t, func() {}, td.NotNil())
checkOK(t, []int{}, td.NotNil())
checkOK(t, map[bool]bool{}, td.NotNil())
checkOK(t, &num, td.NotNil())
checkOK(t, 42, td.NotNil())
checkError(t, (func())(nil), td.NotNil(),
expectedError{
Message: mustBe("nil value"),
Path: mustBe("DATA"),
Got: mustContain("nil"),
Expected: mustBe("not nil"),
})
checkError(t, ([]int)(nil), td.NotNil(),
expectedError{
Message: mustBe("nil value"),
Path: mustBe("DATA"),
Got: mustContain("nil"),
Expected: mustBe("not nil"),
})
checkError(t, (map[bool]bool)(nil), td.NotNil(),
expectedError{
Message: mustBe("nil value"),
Path: mustBe("DATA"),
Got: mustContain("nil"),
Expected: mustBe("not nil"),
})
checkError(t, (*int)(nil), td.NotNil(),
expectedError{
Message: mustBe("nil value"),
Path: mustBe("DATA"),
Got: mustContain("nil"),
Expected: mustBe("not nil"),
})
checkError(t, (chan int)(nil), td.NotNil(),
expectedError{
Message: mustBe("nil value"),
Path: mustBe("DATA"),
Got: mustContain("nil"),
Expected: mustBe("not nil"),
})
checkError(t, nil, td.NotNil(),
expectedError{
Message: mustBe("nil value"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("not nil"),
})
var got fmt.Stringer = (*bytes.Buffer)(nil)
checkError(t, got, td.NotNil(),
expectedError{
Message: mustBe("nil value"),
Path: mustBe("DATA"),
Got: mustContain(""),
Expected: mustBe("not nil"),
})
//
// String
test.EqualStr(t, td.NotNil().String(), "not nil")
}
func TestNilTypeBehind(t *testing.T) {
equalTypes(t, td.Nil(), nil)
equalTypes(t, td.NotNil(), nil)
}
go-testdeep-1.15.0/td/td_none.go 0000664 0000000 0000000 00000004256 15144170453 0016415 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2025, 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"
"github.com/maxatome/go-testdeep/internal/ctxerr"
)
type tdNone struct {
tdListBase
}
var _ TestDeep = &tdNone{}
// summary(None): no values have to match
// input(None): all
// None operator compares data against several not expected
// values. During a match, none of them have to match to succeed.
//
// td.Cmp(t, 12, td.None(8, 10, 14)) // succeeds
// td.Cmp(t, 12, td.None(8, 10, 12, 14)) // fails
//
// Note [Flatten] function can be used to group or reuse some values or
// operators and so avoid boring and inefficient copies:
//
// prime := td.Flatten([]int{1, 2, 3, 5, 7, 11, 13})
// even := td.Flatten([]int{2, 4, 6, 8, 10, 12, 14})
// td.Cmp(t, 9, td.None(prime, even)) // succeeds
//
// See also [All], [Any] and [Not].
func None(notExpectedValues ...any) TestDeep {
return &tdNone{
tdListBase: newListBase(notExpectedValues...),
}
}
// summary(Not): value must not match
// input(Not): all
// Not operator compares data against the not expected value. During a
// match, it must not match to succeed.
//
// Not is the same operator as [None] with only one argument. It is
// provided as a more readable function when only one argument is
// needed.
//
// td.Cmp(t, 12, td.Not(10)) // succeeds
// td.Cmp(t, 12, td.Not(12)) // fails
//
// See also [None].
func Not(notExpected any) TestDeep {
return &tdNone{
tdListBase: newListBase(notExpected),
}
}
func (n *tdNone) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
for idx, item := range n.items {
ok, err := deepValueEqualFinalOK(ctx, got, item)
if err != nil {
return err
}
if ok {
if ctx.BooleanError {
return ctxerr.BooleanError
}
var mesg string
if n.GetLocation().Func == "Not" {
mesg = "comparing with Not"
} else {
mesg = fmt.Sprintf("comparing with None (part %d of %d is OK)",
idx+1, len(n.items))
}
return ctx.CollectError(&ctxerr.Error{
Message: mesg,
Got: got,
Expected: n,
})
}
}
return nil
}
go-testdeep-1.15.0/td/td_none_test.go 0000664 0000000 0000000 00000004576 15144170453 0017461 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-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 TestNone(t *testing.T) {
checkOK(t, 6, td.None(7, 8, 9, nil))
checkOK(t, nil, td.None(7, 8, 9))
checkError(t, 6, td.None(6, 7),
expectedError{
Message: mustBe("comparing with None (part 1 of 2 is OK)"),
Path: mustBe("DATA"),
Got: mustBe("6"),
Expected: mustBe("None(6,\n 7)"),
})
checkError(t, nil, td.None(7, nil),
expectedError{
Message: mustBe("comparing with None (part 2 of 2 is OK)"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("None(7,\n nil)"),
})
// Lax
checkError(t, float64(6), td.Lax(td.None(6, 7)),
expectedError{
Message: mustBe("comparing with None (part 1 of 2 is OK)"),
Path: mustBe("DATA"),
Got: mustBe("6.0"),
Expected: mustBe("None(6,\n 7)"),
})
checkError(t, 123, td.None(1, td.JSON("{")),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe(`DATA`),
Summary: mustContain("JSON unmarshal error"),
Under: mustContain("under operator JSON at "),
},
"erroneous operator expected")
//
// String
test.EqualStr(t, td.None(6).String(), "None(6)")
test.EqualStr(t, td.None(6, 7).String(), "None(6,\n 7)")
}
func TestNot(t *testing.T) {
checkOK(t, 6, td.Not(7))
checkOK(t, nil, td.Not(7))
checkError(t, 6, td.Not(6),
expectedError{
Message: mustBe("comparing with Not"),
Path: mustBe("DATA"),
Got: mustBe("6"),
Expected: mustBe("Not(6)"),
})
checkError(t, nil, td.Not(nil),
expectedError{
Message: mustBe("comparing with Not"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("Not(nil)"),
})
checkError(t, 123, td.Not(td.JSON("{")),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe(`DATA`),
Summary: mustContain("JSON unmarshal error"),
Under: mustContain("under operator JSON at "),
},
"erroneous operator expected")
//
// String
test.EqualStr(t, td.Not(6).String(), "Not(6)")
}
func TestNoneTypeBehind(t *testing.T) {
equalTypes(t, td.None(6), nil)
equalTypes(t, td.Not(6), nil)
}
go-testdeep-1.15.0/td/td_ptr.go 0000664 0000000 0000000 00000011764 15144170453 0016265 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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"
)
type tdPtr struct {
tdSmugglerBase
}
var _ TestDeep = &tdPtr{}
// summary(Ptr): allows to easily test a pointer value
// input(Ptr): ptr
// Ptr is a smuggler operator. It takes the address of data and
// compares it to val.
//
// val depends on data type. For example, if the compared data is an
// *int, one can have:
//
// num := 12
// td.Cmp(t, &num, td.Ptr(12)) // succeeds
//
// as well as an other operator:
//
// num := 3
// td.Cmp(t, &num, td.Ptr(td.Between(3, 4)))
//
// TypeBehind method returns the [reflect.Type] of a pointer on val,
// except if val is a [TestDeep] operator. In this case, it delegates
// TypeBehind() to the operator and returns the [reflect.Type] of a
// pointer on the returned value (if non-nil of course).
//
// See also [PPtr] and [Shallow].
func Ptr(val any) TestDeep {
p := tdPtr{
tdSmugglerBase: newSmugglerBase(val),
}
vval := reflect.ValueOf(val)
if !vval.IsValid() {
p.err = ctxerr.OpBadUsage("Ptr", "(NON_NIL_VALUE)", val, 1, true)
return &p
}
if !p.isTestDeeper {
p.expectedValue = reflect.New(vval.Type())
p.expectedValue.Elem().Set(vval)
}
return &p
}
func (p *tdPtr) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if p.err != nil {
return ctx.CollectError(p.err)
}
if got.Kind() != reflect.Ptr {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "pointer type mismatch",
Got: types.RawString(got.Type().String()),
Expected: types.RawString(p.String()),
})
}
if p.isTestDeeper {
return deepValueEqual(ctx.AddPtr(1), got.Elem(), p.expectedValue)
}
return deepValueEqual(ctx, got, p.expectedValue)
}
func (p *tdPtr) String() string {
if p.err != nil {
return p.stringError()
}
if p.isTestDeeper {
return "*"
}
return p.expectedValue.Type().String()
}
func (p *tdPtr) TypeBehind() reflect.Type {
if p.err != nil {
return nil
}
// If the expected value is a TestDeep operator, delegate TypeBehind to it
if p.isTestDeeper {
typ := p.expectedValue.Interface().(TestDeep).TypeBehind()
if typ == nil {
return nil
}
// Add a level of pointer
return reflect.New(typ).Type()
}
return p.expectedValue.Type()
}
type tdPPtr struct {
tdSmugglerBase
}
var _ TestDeep = &tdPPtr{}
// summary(PPtr): allows to easily test a pointer of pointer value
// input(PPtr): ptr
// PPtr is a smuggler operator. It takes the address of the address of
// data and compares it to val.
//
// val depends on data type. For example, if the compared data is an
// **int, one can have:
//
// num := 12
// pnum = &num
// td.Cmp(t, &pnum, td.PPtr(12)) // succeeds
//
// as well as an other operator:
//
// num := 3
// pnum = &num
// td.Cmp(t, &pnum, td.PPtr(td.Between(3, 4))) // succeeds
//
// It is more efficient and shorter to write than:
//
// td.Cmp(t, &pnum, td.Ptr(td.Ptr(val))) // succeeds too
//
// TypeBehind method returns the [reflect.Type] of a pointer on a
// pointer on val, except if val is a [TestDeep] operator. In this
// case, it delegates TypeBehind() to the operator and returns the
// [reflect.Type] of a pointer on a pointer on the returned value (if
// non-nil of course).
//
// See also [Ptr].
func PPtr(val any) TestDeep {
p := tdPPtr{
tdSmugglerBase: newSmugglerBase(val),
}
vval := reflect.ValueOf(val)
if !vval.IsValid() {
p.err = ctxerr.OpBadUsage("PPtr", "(NON_NIL_VALUE)", val, 1, true)
return &p
}
if !p.isTestDeeper {
pVval := reflect.New(vval.Type())
pVval.Elem().Set(vval)
p.expectedValue = reflect.New(pVval.Type())
p.expectedValue.Elem().Set(pVval)
}
return &p
}
func (p *tdPPtr) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if p.err != nil {
return ctx.CollectError(p.err)
}
if got.Kind() != reflect.Ptr || got.Elem().Kind() != reflect.Ptr {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "pointer type mismatch",
Got: types.RawString(got.Type().String()),
Expected: types.RawString(p.String()),
})
}
if p.isTestDeeper {
return deepValueEqual(ctx.AddPtr(2), got.Elem().Elem(), p.expectedValue)
}
return deepValueEqual(ctx, got, p.expectedValue)
}
func (p *tdPPtr) String() string {
if p.err != nil {
return p.stringError()
}
if p.isTestDeeper {
return "**"
}
return p.expectedValue.Type().String()
}
func (p *tdPPtr) TypeBehind() reflect.Type {
if p.err != nil {
return nil
}
// If the expected value is a TestDeep operator, delegate TypeBehind to it
if p.isTestDeeper {
typ := p.expectedValue.Interface().(TestDeep).TypeBehind()
if typ == nil {
return nil
}
// Add 2 levels of pointer
return reflect.New(reflect.New(typ).Type()).Type()
}
return p.expectedValue.Type()
}
go-testdeep-1.15.0/td/td_ptr_test.go 0000664 0000000 0000000 00000013446 15144170453 0017323 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 TestPtr(t *testing.T) {
//
// Ptr
num := 12
str := "test"
pNum := &num
pStr := &str
pStruct := &struct{}{}
checkOK(t, &num, td.Ptr(12))
checkOK(t, &str, td.Ptr("test"))
checkOK(t, &struct{}{}, td.Ptr(struct{}{}))
checkError(t, &num, td.Ptr(13),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("*DATA"),
Got: mustBe("12"),
Expected: mustBe("13"),
})
checkError(t, nil, td.Ptr(13),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("*int"),
})
checkError(t, (*int)(nil), td.Ptr(13),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("*DATA"), // should be DATA, but seems hard to be done
Got: mustBe("nil"),
Expected: mustBe("13"),
})
checkError(t, (*int)(nil), td.Ptr((*int)(nil)),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("*int"),
Expected: mustBe("**int"),
})
checkError(t, &num, td.Ptr("test"),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("*int"),
Expected: mustBe("*string"),
})
checkError(t, &num, td.Ptr(td.Any(11)),
expectedError{
Message: mustBe("comparing with Any"),
Path: mustBe("*DATA"),
Got: mustBe("12"),
Expected: mustBe("Any(11)"),
})
checkError(t, &str, td.Ptr("foobar"),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("*DATA"),
Got: mustContain(`"test"`),
Expected: mustContain(`"foobar"`),
})
checkError(t, 13, td.Ptr(13),
expectedError{
Message: mustBe("pointer type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("*int"),
})
checkError(t, &str, td.Ptr(12),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("*string"),
Expected: mustBe("*int"),
})
checkError(t, &pNum, td.Ptr(12),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("**int"),
Expected: mustBe("*int"),
})
checkError(t, &pStr, td.Ptr("test"),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("**string"),
Expected: mustBe("*string"),
})
//
// PPtr
checkOK(t, &pNum, td.PPtr(12))
checkOK(t, &pStr, td.PPtr("test"))
checkOK(t, &pStruct, td.PPtr(struct{}{}))
checkError(t, &pNum, td.PPtr(13),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("**DATA"),
Got: mustBe("12"),
Expected: mustBe("13"),
})
checkError(t, nil, td.PPtr(13),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("**int"),
})
checkError(t, &num, td.PPtr(13),
expectedError{
Message: mustBe("pointer type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("*int"),
Expected: mustBe("**int"),
})
checkError(t, &pStr, td.PPtr("foobar"),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("**DATA"),
})
checkError(t, &str, td.PPtr("foobar"),
expectedError{
Message: mustBe("pointer type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("*string"),
Expected: mustBe("**string"),
})
checkError(t, &pNum, td.PPtr(td.Any(11)),
expectedError{
Message: mustBe("comparing with Any"),
Path: mustBe("**DATA"),
Got: mustBe("12"),
Expected: mustBe("Any(11)"),
})
pStruct = nil
checkError(t, &pStruct, td.PPtr(struct{}{}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("**DATA"), // should be *DATA, but seems hard to be done
Got: mustBe("nil"),
Expected: mustContain("struct"),
})
//
// Bad usage
checkError(t, "never tested",
td.Ptr(nil),
expectedError{
Message: mustBe("bad usage of Ptr operator"),
Path: mustBe("DATA"),
Summary: mustContain("usage: Ptr("),
})
checkError(t, "never tested",
td.Ptr(MyInterface(nil)),
expectedError{
Message: mustBe("bad usage of Ptr operator"),
Path: mustBe("DATA"),
Summary: mustContain("usage: Ptr("),
})
checkError(t, "never tested",
td.PPtr(nil),
expectedError{
Message: mustBe("bad usage of PPtr operator"),
Path: mustBe("DATA"),
Summary: mustContain("usage: PPtr("),
})
checkError(t, "never tested",
td.PPtr(MyInterface(nil)),
expectedError{
Message: mustBe("bad usage of PPtr operator"),
Path: mustBe("DATA"),
Summary: mustContain("usage: PPtr("),
})
//
// String
test.EqualStr(t, td.Ptr(13).String(), "*int")
test.EqualStr(t, td.PPtr(13).String(), "**int")
test.EqualStr(t, td.Ptr(td.Ptr(13)).String(), "*")
test.EqualStr(t, td.PPtr(td.Ptr(13)).String(), "**")
// Erroneous op
test.EqualStr(t, td.Ptr(nil).String(), "Ptr()")
test.EqualStr(t, td.PPtr(nil).String(), "PPtr()")
}
func TestPtrTypeBehind(t *testing.T) {
var num int
equalTypes(t, td.Ptr(6), &num)
// Another TestDeep operator delegation
var num64 int64
equalTypes(t, td.Ptr(td.Between(int64(1), int64(2))), &num64)
equalTypes(t, td.Ptr(td.Any(1, 1.2)), nil)
// Erroneous op
equalTypes(t, td.Ptr(nil), nil)
}
func TestPPtrTypeBehind(t *testing.T) {
var pnum *int
equalTypes(t, td.PPtr(6), &pnum)
// Another TestDeep operator delegation
var pnum64 *int64
equalTypes(t, td.PPtr(td.Between(int64(1), int64(2))), &pnum64)
equalTypes(t, td.PPtr(td.Any(1, 1.2)), nil)
// Erroneous op
equalTypes(t, td.PPtr(nil), nil)
}
go-testdeep-1.15.0/td/td_re.go 0000664 0000000 0000000 00000017220 15144170453 0016057 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2025, 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"
"regexp"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/dark"
"github.com/maxatome/go-testdeep/internal/types"
)
type tdRe struct {
base
re *regexp.Regexp
captures reflect.Value
numMatches int
}
var _ TestDeep = &tdRe{}
func newRe(regIf any, capture ...any) *tdRe {
r := &tdRe{
base: newBase(4),
}
const (
usageRe = "(STRING|*regexp.Regexp[, NON_NIL_CAPTURE])"
usageReAll = "(STRING|*regexp.Regexp, NON_NIL_CAPTURE)"
)
usage := usageRe
if len(r.location.Func) != 2 {
usage = usageReAll
}
switch len(capture) {
case 0:
case 1:
if capture[0] != nil {
r.captures = reflect.ValueOf(capture[0])
}
default:
r.err = ctxerr.OpTooManyParams(r.location.Func, usage)
return r
}
switch reg := regIf.(type) {
case *regexp.Regexp:
r.re = reg
case string:
var err error
r.re, err = regexp.Compile(reg)
if err != nil {
r.err = &ctxerr.Error{
Message: "invalid regexp given to " + r.location.Func + " operator",
Summary: ctxerr.NewSummary(err.Error()),
User: true,
}
}
default:
r.err = ctxerr.OpBadUsage(r.location.Func, usage, regIf, 1, false)
}
return r
}
// summary(Re): allows to apply a regexp on a string (or convertible),
// []byte, error or fmt.Stringer interfaces, and even test the
// captured groups
// input(Re): str,slice([]byte),if(✓ + fmt.Stringer/error)
// Re operator allows to apply a regexp on a string (or convertible),
// []byte, error or [fmt.Stringer] interface (error interface is tested
// before [fmt.Stringer].)
//
// reg is the regexp. It can be a string that is automatically
// compiled using [regexp.Compile], or a [*regexp.Regexp].
//
// Optional capture parameter can be used to match the contents of
// regexp groups. Groups are presented as a []string or [][]byte
// depending the original matched data. Note that an other operator
// can be used here.
//
// td.Cmp(t, "foobar zip!", td.Re(`^foobar`)) // succeeds
// td.Cmp(t, "John Doe",
// td.Re(`^(\w+) (\w+)`, []string{"John", "Doe"})) // succeeds
// td.Cmp(t, "John Doe",
// td.Re(`^(\w+) (\w+)`, td.Bag("Doe", "John"))) // succeeds
//
// When matching many, many lines with a single regexp, it is
// sometimes difficult to see where the regexp failed in the input
// string. To avoid that, the regexp can be split by lines and so the
// failure is easier to locate, thanks to [List] operator and [Flatten]:
//
// td.Cmp(t,
// strings.Split(got, "\n"),
// td.List(td.Flatten(strings.Split(expectedRe, "\n"),
// func(line string) any {
// return `^` + td.Re(line) + `\z`
// })))
//
// See also the multilines example.
//
// See also [ReAll].
func Re(reg any, capture ...any) TestDeep {
r := newRe(reg, capture...)
r.numMatches = 1
return r
}
// summary(ReAll): allows to successively apply a regexp on a string
// (or convertible), []byte, error or fmt.Stringer interfaces, and
// even test the captured groups
// input(ReAll): str,slice([]byte),if(✓ + fmt.Stringer/error)
// ReAll operator allows to successively apply a regexp on a string
// (or convertible), []byte, error or [fmt.Stringer] interface (error
// interface is tested before [fmt.Stringer]) and to match its groups
// contents.
//
// reg is the regexp. It can be a string that is automatically
// compiled using [regexp.Compile], or a [*regexp.Regexp].
//
// capture is used to match the contents of regexp groups. Groups
// are presented as a []string or [][]byte depending the original
// matched data. Note that an other operator can be used here.
//
// td.Cmp(t, "John Doe",
// td.ReAll(`(\w+)(?: |\z)`, []string{"John", "Doe"})) // succeeds
// td.Cmp(t, "John Doe",
// td.ReAll(`(\w+)(?: |\z)`, td.Bag("Doe", "John"))) // succeeds
//
// See also [Re].
func ReAll(reg, capture any) TestDeep {
r := newRe(reg, capture)
r.numMatches = -1
return r
}
func (r *tdRe) needCaptures() bool {
return r.captures.IsValid()
}
func (r *tdRe) matchByteCaptures(ctx ctxerr.Context, got []byte, result [][][]byte) *ctxerr.Error {
if len(result) == 0 {
return r.doesNotMatch(ctx, got)
}
num := 0
for _, set := range result {
num += len(set) - 1
}
// Not perfect but cast captured groups to string
// Special case to accepted expected []any type
if r.captures.Type() == types.SliceInterface {
captures := make([]any, 0, num)
for _, set := range result {
for _, match := range set[1:] {
captures = append(captures, string(match))
}
}
return r.matchCaptures(ctx, captures)
}
captures := make([]string, 0, num)
for _, set := range result {
for _, match := range set[1:] {
captures = append(captures, string(match))
}
}
return r.matchCaptures(ctx, captures)
}
func (r *tdRe) matchStringCaptures(ctx ctxerr.Context, got string, result [][]string) *ctxerr.Error {
if len(result) == 0 {
return r.doesNotMatch(ctx, got)
}
num := 0
for _, set := range result {
num += len(set) - 1
}
// Special case to accepted expected []any type
if r.captures.Type() == types.SliceInterface {
captures := make([]any, 0, num)
for _, set := range result {
for _, match := range set[1:] {
captures = append(captures, match)
}
}
return r.matchCaptures(ctx, captures)
}
captures := make([]string, 0, num)
for _, set := range result {
captures = append(captures, set[1:]...)
}
return r.matchCaptures(ctx, captures)
}
func (r *tdRe) matchCaptures(ctx ctxerr.Context, captures any) (err *ctxerr.Error) {
return deepValueEqual(
ctx.ResetPath("("+ctx.Path.String()+" =~ "+r.String()+")"),
reflect.ValueOf(captures), r.captures)
}
func (r *tdRe) matchBool(ctx ctxerr.Context, got any, result bool) *ctxerr.Error {
if result {
return nil
}
return r.doesNotMatch(ctx, got)
}
func (r *tdRe) doesNotMatch(ctx ctxerr.Context, got any) *ctxerr.Error {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "does not match Regexp",
Got: got,
Expected: types.RawString(r.re.String()),
})
}
func (r *tdRe) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if r.err != nil {
return ctx.CollectError(r.err)
}
var str string
switch got.Kind() {
case reflect.String:
str = got.String()
case reflect.Slice:
if got.Type().Elem().Kind() == reflect.Uint8 {
gotBytes := got.Bytes()
if r.needCaptures() {
return r.matchByteCaptures(ctx,
gotBytes, r.re.FindAllSubmatch(gotBytes, r.numMatches))
}
return r.matchBool(ctx, gotBytes, r.re.Match(gotBytes))
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "bad slice type",
Got: types.RawString("[]" + got.Type().Elem().Kind().String()),
Expected: types.RawString("[]uint8"),
})
default:
var strOK bool
iface := dark.MustGetInterface(got)
switch gotVal := iface.(type) {
case error:
str = gotVal.Error()
strOK = true
case fmt.Stringer:
str = gotVal.String()
strOK = true
default:
}
if !strOK {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "bad type",
Got: types.RawString(got.Type().String()),
Expected: types.RawString(
"string (convertible) OR fmt.Stringer OR error OR []uint8"),
})
}
}
if r.needCaptures() {
return r.matchStringCaptures(ctx,
str, r.re.FindAllStringSubmatch(str, r.numMatches))
}
return r.matchBool(ctx, str, r.re.MatchString(str))
}
func (r *tdRe) String() string {
if r.err != nil {
return r.stringError()
}
return r.re.String()
}
go-testdeep-1.15.0/td/td_re_test.go 0000664 0000000 0000000 00000011172 15144170453 0017116 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (
"errors"
"regexp"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestRe(t *testing.T) {
//
// string
checkOK(t, "foo bar test", td.Re("bar"))
checkOK(t, "foo bar test", td.Re(regexp.MustCompile("test$")))
checkOK(t, "foo bar test",
td.ReAll(`(\w+)`, td.Bag("bar", "test", "foo")))
type MyString string
checkOK(t, MyString("Ho zz hoho"),
td.ReAll("(?i)(ho)", []string{"Ho", "ho", "ho"}))
checkOK(t, MyString("Ho zz hoho"),
td.ReAll("(?i)(ho)", []any{"Ho", "ho", "ho"}))
// error interface
checkOK(t, errors.New("pipo bingo"), td.Re("bin"))
// fmt.Stringer interface
checkOK(t, MyStringer{}, td.Re("bin"))
checkError(t, 12, td.Re("bar"),
expectedError{
Message: mustBe("bad type"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe(
"string (convertible) OR fmt.Stringer OR error OR []uint8"),
})
checkError(t, "foo bar test", td.Re("pipo"),
expectedError{
Message: mustBe("does not match Regexp"),
Path: mustBe("DATA"),
Got: mustContain(`"foo bar test"`),
Expected: mustBe("pipo"),
})
checkError(t, "foo bar test", td.Re("(pi)(po)", []string{"pi", "po"}),
expectedError{
Message: mustBe("does not match Regexp"),
Path: mustBe("DATA"),
Got: mustContain(`"foo bar test"`),
Expected: mustBe("(pi)(po)"),
})
checkError(t, "foo bar test", td.Re("(pi)(po)", []any{"pi", "po"}),
expectedError{
Message: mustBe("does not match Regexp"),
Path: mustBe("DATA"),
Got: mustContain(`"foo bar test"`),
Expected: mustBe("(pi)(po)"),
})
//
// bytes
checkOK(t, []byte("foo bar test"), td.Re("bar"))
checkOK(t, []byte("foo bar test"),
td.ReAll(`(\w+)`, td.Bag("bar", "test", "foo")))
type MySlice []byte
checkOK(t, MySlice("Ho zz hoho"),
td.ReAll("(?i)(ho)", []string{"Ho", "ho", "ho"}))
checkOK(t, MySlice("Ho zz hoho"),
td.ReAll("(?i)(ho)", []any{"Ho", "ho", "ho"}))
checkError(t, []int{12}, td.Re("bar"),
expectedError{
Message: mustBe("bad slice type"),
Path: mustBe("DATA"),
Got: mustBe("[]int"),
Expected: mustBe("[]uint8"),
})
checkError(t, []byte("foo bar test"), td.Re("pipo"),
expectedError{
Message: mustBe("does not match Regexp"),
Path: mustBe("DATA"),
Got: mustContain(`foo bar test`),
Expected: mustBe("pipo"),
})
checkError(t, []byte("foo bar test"),
td.Re("(pi)(po)", []string{"pi", "po"}),
expectedError{
Message: mustBe("does not match Regexp"),
Path: mustBe("DATA"),
Got: mustContain(`foo bar test`),
Expected: mustBe("(pi)(po)"),
})
checkError(t, []byte("foo bar test"),
td.Re("(pi)(po)", []any{"pi", "po"}),
expectedError{
Message: mustBe("does not match Regexp"),
Path: mustBe("DATA"),
Got: mustContain(`foo bar test`),
Expected: mustBe("(pi)(po)"),
})
//
// Bad usage
const (
ur = "(STRING|*regexp.Regexp[, NON_NIL_CAPTURE])"
ua = "(STRING|*regexp.Regexp, NON_NIL_CAPTURE)"
)
checkError(t, "never tested",
td.Re(123),
expectedError{
Message: mustBe("bad usage of Re operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Re" + ur + ", but received int as 1st parameter"),
})
checkError(t, "never tested",
td.ReAll(123, nil),
expectedError{
Message: mustBe("bad usage of ReAll operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: ReAll" + ua + ", but received int as 1st parameter"),
})
checkError(t, "never tested",
td.Re("bar", []string{}, 1),
expectedError{
Message: mustBe("bad usage of Re operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Re" + ur + ", too many parameters"),
})
checkError(t, "never tested",
td.ReAll(123, 456),
expectedError{
Message: mustBe("bad usage of ReAll operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: ReAll" + ua + ", but received int as 1st parameter"),
})
checkError(t, "never tested",
td.ReAll(`12[3,4`, nil),
expectedError{
Message: mustBe("invalid regexp given to ReAll operator"),
Path: mustBe("DATA"),
Summary: mustContain("error parsing regexp: "),
})
// Erroneous op
test.EqualStr(t, td.Re(123).String(), "Re()")
test.EqualStr(t, td.ReAll(123, nil).String(), "ReAll()")
}
func TestReTypeBehind(t *testing.T) {
equalTypes(t, td.Re("x"), nil)
equalTypes(t, td.ReAll("x", nil), nil)
// Erroneous op
equalTypes(t, td.Re(123), nil)
equalTypes(t, td.ReAll(123, nil), nil)
}
go-testdeep-1.15.0/td/td_recv.go 0000664 0000000 0000000 00000015131 15144170453 0016407 0 ustar 00root root 0000000 0000000 // 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 (
"fmt"
"reflect"
"time"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/types"
)
// A RecvKind allows to match that nothing has been received on a
// channel or that a channel has been closed when using [Recv]
// operator.
type RecvKind = types.RecvKind
const (
_ RecvKind = (iota & 1) == 0
RecvNothing // nothing read on channel
RecvClosed // channel closed
)
type tdRecv struct {
tdSmugglerBase
timeout time.Duration
}
var _ TestDeep = &tdRecv{}
// summary(Recv): checks the value read from a channel
// input(Recv): chan,ptr(ptr on chan)
// Recv is a smuggler operator. It reads from a channel or a pointer
// to a channel and compares the read value to expectedValue.
//
// expectedValue can be any value including a [TestDeep] operator. It
// can also be [RecvNothing] to test nothing can be read from the
// channel or [RecvClosed] to check the channel is closed.
//
// If timeout is passed it should be only one item. It means: try to
// read the channel during this duration to get a value before giving
// up. If timeout is missing or ≤ 0, it defaults to 0 meaning Recv
// does not wait for a value but gives up instantly if no value is
// available on the channel.
//
// ch := make(chan int, 6)
// td.Cmp(t, ch, td.Recv(td.RecvNothing)) // succeeds
// td.Cmp(t, ch, td.Recv(42)) // fails, nothing to receive
// // recv(DATA): values differ
// // got: nothing received on channel
// // expected: 42
//
// ch <- 42
// td.Cmp(t, ch, td.Recv(td.RecvNothing)) // fails, 42 received instead
// // recv(DATA): values differ
// // got: 42
// // expected: nothing received on channel
//
// td.Cmp(t, ch, td.Recv(42)) // fails, nothing to receive anymore
// // recv(DATA): values differ
// // got: nothing received on channel
// // expected: 42
//
// ch <- 666
// td.Cmp(t, ch, td.Recv(td.Between(600, 700))) // succeeds
//
// close(ch)
// td.Cmp(t, ch, td.Recv(td.RecvNothing)) // fails as channel is closed
// // recv(DATA): values differ
// // got: channel is closed
// // expected: nothing received on channel
//
// td.Cmp(t, ch, td.Recv(td.RecvClosed)) // succeeds
//
// Note that for convenience Recv accepts pointer on channel:
//
// ch := make(chan int, 6)
// ch <- 42
// td.Cmp(t, &ch, td.Recv(42)) // succeeds
//
// Each time Recv is called, it tries to consume one item from the
// channel, immediately or, if given, before timeout duration. To
// consume several items in a same [Cmp] call, one can use [All]
// operator as in:
//
// ch := make(chan int, 6)
// ch <- 1
// ch <- 2
// ch <- 3
// close(ch)
// td.Cmp(t, ch, td.All( // succeeds
// td.Recv(1),
// td.Recv(2),
// td.Recv(3),
// td.Recv(td.RecvClosed),
// ))
//
// To check nothing can be received during 100ms on channel ch (if
// something is received before, including a close, it fails):
//
// td.Cmp(t, ch, td.Recv(td.RecvNothing, 100*time.Millisecond))
//
// note that in case of success, the above [Cmp] call always lasts 100ms.
//
// To check 42 can be received from channel ch during the next 100ms
// (if nothing is received during these 100ms or something different
// from 42, including a close, it fails):
//
// td.Cmp(t, ch, td.Recv(42, 100*time.Millisecond))
//
// note that in case of success, the above [Cmp] call lasts less than 100ms.
//
// A nil channel is not handled specifically, so it “is never ready
// for communication” as specification says:
//
// var ch chan int
// td.Cmp(t, ch, td.Recv(td.RecvNothing)) // always succeeds
// td.Cmp(t, ch, td.Recv(42)) // or any other value, always fails
// td.Cmp(t, ch, td.Recv(td.RecvClosed)) // always fails
//
// so to check if a channel is not nil before reading from it, one can
// either do:
//
// td.Cmp(t, ch, td.All(
// td.NotNil(),
// td.Recv(42),
// ))
// // or
// if td.Cmp(t, ch, td.NotNil()) {
// td.Cmp(t, ch, td.Recv(42))
// }
//
// TypeBehind method returns the [reflect.Type] of expectedValue,
// except if expectedValue is a [TestDeep] operator. In this case, it
// delegates TypeBehind() to the operator.
//
// See also [Cap] and [Len].
func Recv(expectedValue any, timeout ...time.Duration) TestDeep {
r := tdRecv{}
r.tdSmugglerBase = newSmugglerBase(expectedValue, 0)
if !r.isTestDeeper {
r.expectedValue = reflect.ValueOf(expectedValue)
}
switch len(timeout) {
case 0:
case 1:
r.timeout = timeout[0]
default:
r.err = ctxerr.OpTooManyParams(r.location.Func, "(EXPECTED[, TIMEOUT])")
}
return &r
}
func (r *tdRecv) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if r.err != nil {
return ctx.CollectError(r.err)
}
switch got.Kind() {
case reflect.Ptr:
gotElem := got.Elem()
if !gotElem.IsValid() {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(ctxerr.NilPointer(got, "non-nil *chan"))
}
if gotElem.Kind() != reflect.Chan {
break
}
got = gotElem
fallthrough
case reflect.Chan:
cases := [2]reflect.SelectCase{
{
Dir: reflect.SelectRecv,
Chan: got,
},
}
var timer *time.Timer
if r.timeout > 0 {
timer = time.NewTimer(r.timeout)
cases[1] = reflect.SelectCase{
Dir: reflect.SelectRecv,
Chan: reflect.ValueOf(timer.C),
}
} else {
cases[1] = reflect.SelectCase{
Dir: reflect.SelectDefault,
}
}
chosen, recv, recvOK := reflect.Select(cases[:])
if chosen == 1 && timer != nil {
// check quickly both timeout & expected case didn't occur
// concurrently and timeout masked the expected case
cases[1] = reflect.SelectCase{
Dir: reflect.SelectDefault,
}
chosen, recv, recvOK = reflect.Select(cases[:])
}
if chosen == 0 {
if !recvOK {
recv = reflect.ValueOf(RecvClosed)
}
if timer != nil {
timer.Stop()
}
} else {
recv = reflect.ValueOf(RecvNothing)
}
return deepValueEqual(ctx.AddFunctionCall("recv"), recv, r.expectedValue)
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(ctxerr.BadKind(got, "chan OR *chan"))
}
func (r *tdRecv) HandleInvalid() bool {
return true // Knows how to handle untyped nil values (aka invalid values)
}
func (r *tdRecv) String() string {
if r.err != nil {
return r.stringError()
}
if r.isTestDeeper {
return "recv: " + r.expectedValue.Interface().(TestDeep).String()
}
return fmt.Sprintf("recv=%d", r.expectedValue.Int())
}
func (r *tdRecv) TypeBehind() reflect.Type {
if r.err != nil {
return nil
}
return r.internalTypeBehind()
}
go-testdeep-1.15.0/td/td_recv_test.go 0000664 0000000 0000000 00000014215 15144170453 0017450 0 ustar 00root root 0000000 0000000 // 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"
"time"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/internal/types"
"github.com/maxatome/go-testdeep/td"
)
func TestRecv(t *testing.T) {
fillCh := func(ch chan int, val int) {
ch <- val // td.Cmp
ch <- val // EqDeeply aka boolean context
ch <- val // EqDeeplyError
ch <- val // interface + td.Cmp
ch <- val // interface + EqDeeply aka boolean context
ch <- val // interface + EqDeeplyError
}
mkCh := func(val int) chan int {
ch := make(chan int, 6)
fillCh(ch, val)
close(ch)
return ch
}
t.Run("all good", func(t *testing.T) {
ch := mkCh(1)
checkOK(t, ch, td.Recv(1))
checkOK(t, ch, td.Recv(td.RecvClosed, 10*time.Microsecond))
ch = mkCh(42)
checkOK(t, ch, td.Recv(td.Between(40, 45)))
checkOK(t, ch, td.Recv(td.RecvClosed))
})
t.Run("complete cycle", func(t *testing.T) {
ch := make(chan int, 6)
t.Run("empty", func(t *testing.T) {
checkOK(t, ch, td.Recv(td.RecvNothing))
checkOK(t, ch, td.Recv(td.RecvNothing, 10*time.Microsecond))
checkOK(t, &ch, td.Recv(td.RecvNothing))
checkOK(t, &ch, td.Recv(td.RecvNothing, 10*time.Microsecond))
})
t.Run("just filled", func(t *testing.T) {
fillCh(ch, 33)
checkOK(t, ch, td.Recv(33))
fillCh(ch, 34)
checkOK(t, &ch, td.Recv(34))
})
t.Run("nothing to recv on channel", func(t *testing.T) {
checkError(t, ch, td.Recv(td.RecvClosed),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("recv(DATA)"),
Got: mustBe("nothing received on channel"),
Expected: mustBe("channel is closed"),
})
checkError(t, &ch, td.Recv(td.RecvClosed),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("recv(DATA)"),
Got: mustBe("nothing received on channel"),
Expected: mustBe("channel is closed"),
})
checkError(t, ch, td.Recv(42),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("recv(DATA)"),
Got: mustBe("nothing received on channel"),
Expected: mustBe("42"),
})
checkError(t, &ch, td.Recv(42),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("recv(DATA)"),
Got: mustBe("nothing received on channel"),
Expected: mustBe("42"),
})
})
close(ch)
t.Run("closed channel", func(t *testing.T) {
checkError(t, ch, td.Recv(td.RecvNothing),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("recv(DATA)"),
Got: mustBe("channel is closed"),
Expected: mustBe("nothing received on channel"),
})
checkError(t, &ch, td.Recv(td.RecvNothing),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("recv(DATA)"),
Got: mustBe("channel is closed"),
Expected: mustBe("nothing received on channel"),
})
checkError(t, ch, td.Recv(42),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("recv(DATA)"),
Got: mustBe("channel is closed"),
Expected: mustBe("42"),
})
checkError(t, &ch, td.Recv(42),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("recv(DATA)"),
Got: mustBe("channel is closed"),
Expected: mustBe("42"),
})
})
})
t.Run("nil channel", func(t *testing.T) {
var ch chan int
checkError(t, ch, td.Recv(42),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("recv(DATA)"),
Got: mustBe("nothing received on channel"),
Expected: mustBe("42"),
})
checkError(t, &ch, td.Recv(42),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("recv(DATA)"),
Got: mustBe("nothing received on channel"),
Expected: mustBe("42"),
})
})
t.Run("nil pointer", func(t *testing.T) {
checkError(t, (*chan int)(nil), td.Recv(42),
expectedError{
Message: mustBe("nil pointer"),
Path: mustBe("DATA"),
Got: mustBe("nil *chan (*chan int type)"),
Expected: mustBe("non-nil *chan"),
})
})
t.Run("chan any", func(t *testing.T) {
ch := make(chan any, 6)
fillCh := func(val any) {
ch <- val // td.Cmp
ch <- val // EqDeeply aka boolean context
ch <- val // EqDeeplyError
ch <- val // interface + td.Cmp
ch <- val // interface + EqDeeply aka boolean context
ch <- val // interface + EqDeeplyError
}
fillCh(1)
checkOK(t, ch, td.Recv(1))
fillCh(nil)
checkOK(t, ch, td.Recv(nil))
close(ch)
checkOK(t, ch, td.Recv(td.RecvClosed))
})
t.Run("errors", func(t *testing.T) {
checkError(t, "never tested",
td.Recv(23, time.Second, time.Second),
expectedError{
Message: mustBe("bad usage of Recv operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Recv(EXPECTED[, TIMEOUT]), too many parameters"),
})
checkError(t, 42, td.Recv(33),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("chan OR *chan"),
})
checkError(t, &struct{}{}, td.Recv(33),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("*struct (*struct {} type)"),
Expected: mustBe("chan OR *chan"),
})
checkError(t, nil, td.Recv(33),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("chan OR *chan"),
})
})
}
func TestRecvString(t *testing.T) {
test.EqualStr(t, td.Recv(3).String(), "recv=3")
test.EqualStr(t, td.Recv(td.Between(3, 8)).String(), "recv: 3 ≤ got ≤ 8")
test.EqualStr(t, td.Recv(td.Gt(8)).String(), "recv: > 8")
// Erroneous op
test.EqualStr(t, td.Recv(3, 0, 0).String(), "Recv()")
}
func TestRecvTypeBehind(t *testing.T) {
equalTypes(t, td.Recv(3), 0)
equalTypes(t, td.Recv(td.Between(3, 4)), 0)
// Erroneous op
equalTypes(t, td.Recv(3, 0, 0), nil)
}
func TestRecvKind(t *testing.T) {
test.IsTrue(t, td.RecvNothing == types.RecvNothing)
test.IsTrue(t, td.RecvClosed == types.RecvClosed)
}
go-testdeep-1.15.0/td/td_set.go 0000664 0000000 0000000 00000015336 15144170453 0016252 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2025, 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
// summary(Set): compares the contents of an array or a slice ignoring
// duplicates and without taking care of the order of items
// input(Set): array,slice,ptr(ptr on array/slice)
// Set operator compares the contents of an array or a slice (or a
// pointer on array/slice) ignoring duplicates and without taking care
// of the order of items.
//
// During a match, each expected item should match in the compared
// array/slice, and each array/slice item should be matched by an
// expected item to succeed.
//
// td.Cmp(t, []int{1, 1, 2}, td.Set(1, 2)) // succeeds
// td.Cmp(t, []int{1, 1, 2}, td.Set(2, 1)) // succeeds
// td.Cmp(t, []int{1, 1, 2}, td.Set(1, 2, 3)) // fails, 3 is missing
//
// // works with slices/arrays of any type
// td.Cmp(t, personSlice, td.Set(
// Person{Name: "Bob", Age: 32},
// Person{Name: "Alice", Age: 26},
// ))
//
// To flatten a non-[]any slice/array, use [Flatten] function
// and so avoid boring and inefficient copies:
//
// expected := []int{2, 1}
// td.Cmp(t, []int{1, 1, 2}, td.Set(td.Flatten(expected))) // succeeds
// // = td.Cmp(t, []int{1, 1, 2}, td.Set(2, 1))
//
// exp1 := []int{2, 1}
// exp2 := []int{5, 8}
// td.Cmp(t, []int{1, 5, 1, 2, 8, 3, 3},
// td.Set(td.Flatten(exp1), 3, td.Flatten(exp2))) // succeeds
// // = td.Cmp(t, []int{1, 5, 1, 2, 8, 3, 3}, td.Set(2, 1, 3, 5, 8))
//
// TypeBehind method can return a non-nil [reflect.Type] if all items
// known non-interface types are equal, or if only interface types
// are found (mostly issued from [Isa]) and they are equal.
//
// See also [NotAny], [SubSetOf], [SuperSetOf], [Bag] and [List].
func Set(expectedItems ...any) TestDeep {
return newSetBase(allSet, true, expectedItems)
}
// summary(SubSetOf): compares the contents of an array or a slice
// ignoring duplicates and without taking care of the order of items
// but with potentially some exclusions
// input(SubSetOf): array,slice,ptr(ptr on array/slice)
// SubSetOf operator compares the contents of an array or a slice (or a
// pointer on array/slice) ignoring duplicates and without taking care
// of the order of items.
//
// During a match, each array/slice item should be matched by an
// expected item to succeed. But some expected items can be missing
// from the compared array/slice.
//
// td.Cmp(t, []int{1, 1}, td.SubSetOf(1, 2)) // succeeds
// td.Cmp(t, []int{1, 1, 2}, td.SubSetOf(1, 3)) // fails, 2 is an extra item
//
// // works with slices/arrays of any type
// td.Cmp(t, personSlice, td.SubSetOf(
// Person{Name: "Bob", Age: 32},
// Person{Name: "Alice", Age: 26},
// ))
//
// To flatten a non-[]any slice/array, use [Flatten] function
// and so avoid boring and inefficient copies:
//
// expected := []int{2, 1}
// td.Cmp(t, []int{1, 1}, td.SubSetOf(td.Flatten(expected))) // succeeds
// // = td.Cmp(t, []int{1, 1}, td.SubSetOf(2, 1))
//
// exp1 := []int{2, 1}
// exp2 := []int{5, 8}
// td.Cmp(t, []int{1, 5, 1, 3, 3},
// td.SubSetOf(td.Flatten(exp1), 3, td.Flatten(exp2))) // succeeds
// // = td.Cmp(t, []int{1, 5, 1, 3, 3}, td.SubSetOf(2, 1, 3, 5, 8))
//
// TypeBehind method can return a non-nil [reflect.Type] if all items
// known non-interface types are equal, or if only interface types
// are found (mostly issued from [Isa]) and they are equal.
//
// See also [NotAny], [Set] and [SuperSetOf].
func SubSetOf(expectedItems ...any) TestDeep {
return newSetBase(subSet, true, expectedItems)
}
// summary(SuperSetOf): compares the contents of an array or a slice
// ignoring duplicates and without taking care of the order of items
// but with potentially some extra items
// input(SuperSetOf): array,slice,ptr(ptr on array/slice)
// SuperSetOf operator compares the contents of an array or a slice (or
// a pointer on array/slice) ignoring duplicates and without taking
// care of the order of items.
//
// During a match, each expected item should match in the compared
// array/slice. But some items in the compared array/slice may not be
// expected.
//
// td.Cmp(t, []int{1, 1, 2}, td.SuperSetOf(1)) // succeeds
// td.Cmp(t, []int{1, 1, 2}, td.SuperSetOf(1, 3)) // fails, 3 is missing
//
// // works with slices/arrays of any type
// td.Cmp(t, personSlice, td.SuperSetOf(
// Person{Name: "Bob", Age: 32},
// Person{Name: "Alice", Age: 26},
// ))
//
// To flatten a non-[]any slice/array, use [Flatten] function
// and so avoid boring and inefficient copies:
//
// expected := []int{2, 1}
// td.Cmp(t, []int{1, 1, 2, 8}, td.SuperSetOf(td.Flatten(expected))) // succeeds
// // = td.Cmp(t, []int{1, 1, 2, 8}, td.SubSetOf(2, 1))
//
// exp1 := []int{2, 1}
// exp2 := []int{5, 8}
// td.Cmp(t, []int{1, 5, 1, 8, 42, 3, 3},
// td.SuperSetOf(td.Flatten(exp1), 3, td.Flatten(exp2))) // succeeds
// // = td.Cmp(t, []int{1, 5, 1, 8, 42, 3, 3}, td.SuperSetOf(2, 1, 3, 5, 8))
//
// TypeBehind method can return a non-nil [reflect.Type] if all items
// known non-interface types are equal, or if only interface types
// are found (mostly issued from [Isa]) and they are equal.
//
// See also [NotAny], [Set] and [SubSetOf].
func SuperSetOf(expectedItems ...any) TestDeep {
return newSetBase(superSet, true, expectedItems)
}
// summary(NotAny): compares the contents of an array or a slice, no
// values have to match
// input(NotAny): array,slice,ptr(ptr on array/slice)
// NotAny operator checks that the contents of an array or a slice (or
// a pointer on array/slice) does not contain any of "notExpectedItems".
//
// td.Cmp(t, []int{1}, td.NotAny(1, 2, 3)) // fails
// td.Cmp(t, []int{5}, td.NotAny(1, 2, 3)) // succeeds
//
// // works with slices/arrays of any type
// td.Cmp(t, personSlice, td.NotAny(
// Person{Name: "Bob", Age: 32},
// Person{Name: "Alice", Age: 26},
// ))
//
// To flatten a non-[]any slice/array, use [Flatten] function
// and so avoid boring and inefficient copies:
//
// notExpected := []int{2, 1}
// td.Cmp(t, []int{4, 4, 3, 8}, td.NotAny(td.Flatten(notExpected))) // succeeds
// // = td.Cmp(t, []int{4, 4, 3, 8}, td.NotAny(2, 1))
//
// notExp1 := []int{2, 1}
// notExp2 := []int{5, 8}
// td.Cmp(t, []int{4, 4, 42, 8},
// td.NotAny(td.Flatten(notExp1), 3, td.Flatten(notExp2))) // succeeds
// // = td.Cmp(t, []int{4, 4, 42, 8}, td.NotAny(2, 1, 3, 5, 8))
//
// Beware that NotAny(…) is not equivalent to Not(Any(…)) but is like
// Not(SuperSet(…)).
//
// TypeBehind method can return a non-nil [reflect.Type] if all items
// known non-interface types are equal, or if only interface types
// are found (mostly issued from [Isa]) and they are equal.
//
// See also [Set], [SubSetOf] and [SuperSetOf].
func NotAny(notExpectedItems ...any) TestDeep {
return newSetBase(noneSet, true, notExpectedItems)
}
go-testdeep-1.15.0/td/td_set_base.go 0000664 0000000 0000000 00000007602 15144170453 0017241 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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"
"strings"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/flat"
"github.com/maxatome/go-testdeep/internal/util"
)
type setKind uint8
const (
allSet setKind = iota
subSet
superSet
noneSet
)
type tdSetBase struct {
baseOKNil
kind setKind
ignoreDups bool
expectedItems []reflect.Value
}
func newSetBase(kind setKind, ignoreDups bool, expectedItems []any) *tdSetBase {
return &tdSetBase{
baseOKNil: newBaseOKNil(4),
kind: kind,
ignoreDups: ignoreDups,
expectedItems: flat.Values(expectedItems),
}
}
func (s *tdSetBase) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
switch got.Kind() {
case 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"))
}
if gotElem.Kind() != reflect.Array && gotElem.Kind() != reflect.Slice {
break
}
got = gotElem
fallthrough
case reflect.Array, reflect.Slice:
var (
gotLen = got.Len()
foundItems []reflect.Value
missingItems []reflect.Value
foundGotIdxes = map[int]bool{}
)
for _, expected := range s.expectedItems {
found := false
for idx := 0; len(foundGotIdxes) < gotLen && idx < gotLen; idx++ {
if foundGotIdxes[idx] {
continue
}
ok, err := deepValueEqualFinalOK(ctx, got.Index(idx), expected)
if err != nil { // user error, stop asap
return err
}
if ok {
foundItems = append(foundItems, expected)
foundGotIdxes[idx] = true
found = true
if !s.ignoreDups {
break
}
}
}
if !found {
missingItems = append(missingItems, expected)
}
}
res := tdSetResult{
Kind: itemsSetResult,
Sort: true,
}
if s.kind != noneSet {
if s.kind != subSet {
// In Set* cases with missing items, try a second pass. Perhaps
// an already matching got item, matches another expected item?
if s.ignoreDups && len(missingItems) > 0 {
var newMissingItems []reflect.Value
nextExpected:
for _, expected := range missingItems {
for idxGot := range foundGotIdxes {
ok, _ := deepValueEqualFinalOK(ctx, got.Index(idxGot), expected)
if ok {
continue nextExpected
}
}
newMissingItems = append(newMissingItems, expected)
}
missingItems = newMissingItems
}
if len(missingItems) > 0 {
if ctx.BooleanError {
return ctxerr.BooleanError
}
res.Missing = missingItems
}
}
if len(foundGotIdxes) < gotLen && s.kind != superSet {
if ctx.BooleanError {
return ctxerr.BooleanError
}
notFoundRemain := gotLen - len(foundGotIdxes)
res.Extra = make([]reflect.Value, 0, notFoundRemain)
for idx := 0; notFoundRemain > 0; idx++ {
if !foundGotIdxes[idx] {
res.Extra = append(res.Extra, got.Index(idx))
notFoundRemain--
}
}
}
} else if len(foundItems) > 0 {
if ctx.BooleanError {
return ctxerr.BooleanError
}
res.Extra = foundItems
}
if res.IsEmpty() {
return nil
}
return ctx.CollectError(&ctxerr.Error{
Message: "comparing %% as a " + s.GetLocation().Func,
Summary: res.Summary(),
})
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(ctxerr.BadKind(got, "slice OR array OR *slice OR *array"))
}
func (s *tdSetBase) String() string {
var b strings.Builder
b.WriteString(s.GetLocation().Func)
return util.SliceToString(&b, s.expectedItems).String()
}
func (s *tdSetBase) TypeBehind() reflect.Type {
typ := uniqTypeBehindSlice(s.expectedItems)
if typ == nil {
return nil
}
return reflect.SliceOf(typ)
}
go-testdeep-1.15.0/td/td_set_result.go 0000664 0000000 0000000 00000003440 15144170453 0017641 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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"
"github.com/maxatome/go-testdeep/helpers/tdutil"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/types"
"github.com/maxatome/go-testdeep/internal/util"
)
type tdSetResultKind uint8
const (
itemsSetResult tdSetResultKind = iota
keysSetResult
)
// Implements fmt.Stringer.
func (k tdSetResultKind) String() string {
switch k {
case itemsSetResult:
return "item"
case keysSetResult:
return "key"
default:
return "?"
}
}
type tdSetResult struct {
types.TestDeepStamp
Missing []reflect.Value
Extra []reflect.Value
Kind tdSetResultKind
Sort bool
}
func (r tdSetResult) IsEmpty() bool {
return len(r.Missing) == 0 && len(r.Extra) == 0
}
func (r tdSetResult) Summary() ctxerr.ErrorSummary {
var summary ctxerr.ErrorSummaryItems
if len(r.Missing) > 0 {
var missing string
if len(r.Missing) > 1 {
if r.Sort {
sort.Stable(tdutil.SortableValues(r.Missing))
}
missing = fmt.Sprintf("Missing %d %ss", len(r.Missing), r.Kind)
} else {
missing = fmt.Sprintf("Missing %s", r.Kind)
}
summary = append(summary, ctxerr.ErrorSummaryItem{
Label: missing,
Value: util.ToString(r.Missing),
})
}
if len(r.Extra) > 0 {
var extra string
if len(r.Extra) > 1 {
if r.Sort {
sort.Stable(tdutil.SortableValues(r.Extra))
}
extra = fmt.Sprintf("Extra %d %ss", len(r.Extra), r.Kind)
} else {
extra = fmt.Sprintf("Extra %s", r.Kind)
}
summary = append(summary, ctxerr.ErrorSummaryItem{
Label: extra,
Value: util.ToString(r.Extra),
})
}
return summary
}
go-testdeep-1.15.0/td/td_set_test.go 0000664 0000000 0000000 00000017101 15144170453 0017301 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-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 (
"fmt"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestSet(t *testing.T) {
type MyArray [5]int
type MySlice []int
for idx, got := range []any{
[]int{1, 3, 4, 4, 5},
[...]int{1, 3, 4, 4, 5},
MySlice{1, 3, 4, 4, 5},
MyArray{1, 3, 4, 4, 5},
&MySlice{1, 3, 4, 4, 5},
&MyArray{1, 3, 4, 4, 5},
} {
testName := fmt.Sprintf("Test #%d → %v", idx, got)
//
// Set
checkOK(t, got, td.Set(5, 4, 1, 3), testName)
checkOK(t, got,
td.Set(5, 4, 1, 3, 3, 3, 3), testName) // duplicated fields
checkOK(t, got,
td.Set(
td.Between(0, 5),
td.Between(0, 5),
td.Between(0, 5))) // dup too
checkError(t, got, td.Set(5, 4),
expectedError{
Message: mustBe("comparing %% as a Set"),
Path: mustBe("DATA"),
// items are sorted
Summary: mustBe(`Extra 2 items: (1,
3)`),
},
testName)
checkError(t, got, td.Set(5, 4, 1, 3, 66),
expectedError{
Message: mustBe("comparing %% as a Set"),
Path: mustBe("DATA"),
Summary: mustBe("Missing item: (66)"),
},
testName)
checkError(t, got, td.Set(5, 66, 4, 1, 3),
expectedError{
Message: mustBe("comparing %% as a Set"),
Path: mustBe("DATA"),
Summary: mustBe("Missing item: (66)"),
},
testName)
checkError(t, got, td.Set(5, 67, 4, 1, 3, 66),
expectedError{
Message: mustBe("comparing %% as a Set"),
Path: mustBe("DATA"),
Summary: mustBe("Missing 2 items: (66,\n 67)"),
},
testName)
checkError(t, got, td.Set(5, 66, 4, 3),
expectedError{
Message: mustBe("comparing %% as a Set"),
Path: mustBe("DATA"),
Summary: mustBe("Missing item: (66)\n Extra item: (1)"),
},
testName)
checkError(t, got, td.Set(1, td.JSON("{")),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe(`DATA`),
Summary: mustContain("JSON unmarshal error"),
Under: mustContain("under operator JSON at "),
},
testName+": erroneous operator expected")
// Lax
checkOK(t, got, td.Lax(td.Set(5, float64(4), 1, 3)), testName)
//
// SubSetOf
checkOK(t, got, td.SubSetOf(5, 4, 1, 3), testName)
checkOK(t, got, td.SubSetOf(5, 4, 1, 3, 66), testName)
checkError(t, got, td.SubSetOf(5, 66, 4, 3),
expectedError{
Message: mustBe("comparing %% as a SubSetOf"),
Path: mustBe("DATA"),
Summary: mustBe("Extra item: (1)"),
},
testName)
checkError(t, got, td.SubSetOf(1, td.JSON("{")),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe(`DATA`),
Summary: mustContain("JSON unmarshal error"),
Under: mustContain("under operator JSON at "),
},
testName+": erroneous operator expected")
// Lax
checkOK(t, got, td.Lax(td.SubSetOf(5, float64(4), 1, 3)), testName)
//
// SuperSetOf
checkOK(t, got, td.SuperSetOf(5, 4, 1, 3), testName)
checkOK(t, got, td.SuperSetOf(5, 4), testName)
checkError(t, got, td.SuperSetOf(5, 66, 4, 1, 3),
expectedError{
Message: mustBe("comparing %% as a SuperSetOf"),
Path: mustBe("DATA"),
Summary: mustBe("Missing item: (66)"),
},
testName)
checkError(t, got, td.SuperSetOf(1, td.JSON("{")),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe(`DATA`),
Summary: mustContain("JSON unmarshal error"),
Under: mustContain("under operator JSON at "),
},
testName+": erroneous operator expected")
// Lax
checkOK(t, got, td.Lax(td.SuperSetOf(5, float64(4), 1, 3)), testName)
//
// NotAny
checkOK(t, got, td.NotAny(10, 20, 30), testName)
checkError(t, got, td.NotAny(3, 66),
expectedError{
Message: mustBe("comparing %% as a NotAny"),
Path: mustBe("DATA"),
Summary: mustBe("Extra item: (3)"),
},
testName)
checkError(t, got, td.NotAny(10, td.JSON("{")),
expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustBe(`DATA`),
Summary: mustContain("JSON unmarshal error"),
Under: mustContain("under operator JSON at "),
},
testName+": erroneous operator expected")
// Lax
checkOK(t, got, td.NotAny(float64(3)), testName)
checkError(t, got, td.Lax(td.NotAny(float64(3))),
expectedError{
Message: mustBe("comparing %% as a NotAny"),
Path: mustBe("DATA"),
Summary: mustBe("Extra item: (3.0)"),
},
testName)
}
checkOK(t, []any{123, "foo", nil, "bar", nil},
td.Set("foo", "bar", 123, nil))
var nilSlice MySlice
for idx, got := range []any{([]int)(nil), &nilSlice} {
testName := fmt.Sprintf("Test #%d", idx)
checkOK(t, got, td.Set(), testName)
checkOK(t, got, td.SubSetOf(), testName)
checkOK(t, got, td.SubSetOf(1, 2), testName)
checkOK(t, got, td.SuperSetOf(), testName)
checkOK(t, got, td.NotAny(), testName)
checkOK(t, got, td.NotAny(1, 2), testName)
}
for idx, set := range []td.TestDeep{
td.Set(123),
td.SubSetOf(123),
td.SuperSetOf(123),
td.NotAny(123),
} {
testName := fmt.Sprintf("Test #%d → %s", idx, set)
checkError(t, 123, set,
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("slice OR array OR *slice OR *array"),
},
testName)
num := 123
checkError(t, &num, set,
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("*int"),
Expected: mustBe("slice OR array OR *slice OR *array"),
},
testName)
var list *MySlice
checkError(t, list, set,
expectedError{
Message: mustBe("nil pointer"),
Path: mustBe("DATA"),
Got: mustBe("nil *slice (*td_test.MySlice type)"),
Expected: mustBe("non-nil *slice OR *array"),
},
testName)
checkError(t, nil, set,
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("slice OR array OR *slice OR *array"),
},
testName)
}
//
// String
test.EqualStr(t, td.Set(1).String(), "Set(1)")
test.EqualStr(t, td.Set(1, 2).String(), "Set(1,\n 2)")
test.EqualStr(t, td.SubSetOf(1).String(), "SubSetOf(1)")
test.EqualStr(t, td.SubSetOf(1, 2).String(), "SubSetOf(1,\n 2)")
test.EqualStr(t, td.SuperSetOf(1).String(), "SuperSetOf(1)")
test.EqualStr(t, td.SuperSetOf(1, 2).String(),
"SuperSetOf(1,\n 2)")
test.EqualStr(t, td.NotAny(1).String(), "NotAny(1)")
test.EqualStr(t, td.NotAny(1, 2).String(), "NotAny(1,\n 2)")
}
func TestSetTypeBehind(t *testing.T) {
equalTypes(t, td.Set(6, 5), ([]int)(nil))
equalTypes(t, td.Set(6, "foo"), nil)
equalTypes(t, td.SubSetOf(6, 5), ([]int)(nil))
equalTypes(t, td.SubSetOf(6, "foo"), nil)
equalTypes(t, td.SuperSetOf(6, 5), ([]int)(nil))
equalTypes(t, td.SuperSetOf(6, "foo"), nil)
equalTypes(t, td.NotAny(6, 5), ([]int)(nil))
equalTypes(t, td.NotAny(6, "foo"), nil)
// Always the same non-interface type (even if we encounter several
// interface types)
equalTypes(t,
td.Set(
td.Empty(),
5,
td.Isa((*error)(nil)), // interface type (in fact pointer to ...)
td.All(6, 7),
td.Isa((*fmt.Stringer)(nil)), // interface type
8),
([]int)(nil))
// Only one interface type
equalTypes(t,
td.Set(
td.Isa((*error)(nil)),
td.Isa((*error)(nil)),
td.Isa((*error)(nil)),
),
([]*error)(nil))
// Several interface types, cannot be sure
equalTypes(t,
td.Set(
td.Isa((*error)(nil)),
td.Isa((*fmt.Stringer)(nil)),
),
nil)
}
go-testdeep-1.15.0/td/td_shallow.go 0000664 0000000 0000000 00000007220 15144170453 0017121 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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"
"unsafe"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/types"
)
type tdShallow struct {
base
expectedKind reflect.Kind
expectedPointer uintptr
expectedStr string // in reflect.String case, to avoid contents GC
}
var _ TestDeep = &tdShallow{}
func stringPointer(s string) uintptr {
return (*reflect.StringHeader)(unsafe.Pointer(&s)).Data
}
// summary(Shallow): compares pointers only, not their contents
// input(Shallow): nil,str,slice,map,ptr,chan,func
// Shallow operator compares pointers only, not their contents. It
// applies on channels, functions (with some restrictions), maps,
// pointers, slices and strings.
//
// During a match, the compared data must be the same as expectedPtr
// to succeed.
//
// a, b := 123, 123
// td.Cmp(t, &a, td.Shallow(&a)) // succeeds
// td.Cmp(t, &a, td.Shallow(&b)) // fails even if a == b as &a != &b
//
// back := "foobarfoobar"
// a, b := back[:6], back[6:]
// // a == b but...
// td.Cmp(t, &a, td.Shallow(&b)) // fails
//
// Be careful for slices and strings! Shallow can succeed but the
// slices/strings not be identical because of their different
// lengths. For example:
//
// a := "foobar yes!"
// b := a[:1] // aka "f"
// td.Cmp(t, &a, td.Shallow(&b)) // succeeds as both strings point to the same area, even if len() differ
//
// The same behavior occurs for slices:
//
// a := []int{1, 2, 3, 4, 5, 6}
// b := a[:2] // aka []int{1, 2}
// td.Cmp(t, &a, td.Shallow(&b)) // succeeds as both slices point to the same area, even if len() differ
//
// See also [Ptr].
func Shallow(expectedPtr any) TestDeep {
vptr := reflect.ValueOf(expectedPtr)
shallow := tdShallow{
base: newBase(3),
expectedKind: vptr.Kind(),
}
// Note from reflect documentation:
// If v's Kind is Func, the returned pointer is an underlying code
// pointer, but not necessarily enough to identify a single function
// uniquely. The only guarantee is that the result is zero if and
// only if v is a nil func Value.
switch shallow.expectedKind {
case reflect.Chan,
reflect.Func,
reflect.Map,
reflect.Ptr,
reflect.Slice,
reflect.UnsafePointer:
shallow.expectedPointer = vptr.Pointer()
case reflect.String:
shallow.expectedStr = vptr.String()
shallow.expectedPointer = stringPointer(shallow.expectedStr)
default:
shallow.err = ctxerr.OpBadUsage(
"Shallow", "(CHANNEL|FUNC|MAP|PTR|SLICE|UNSAFE_PTR|STRING)",
expectedPtr, 1, true)
}
return &shallow
}
func (s *tdShallow) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if s.err != nil {
return ctx.CollectError(s.err)
}
if got.Kind() != s.expectedKind {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(ctxerr.BadKind(got, s.expectedKind.String()))
}
var ptr uintptr
// Special case for strings
if s.expectedKind == reflect.String {
ptr = stringPointer(got.String())
} else {
ptr = got.Pointer()
}
if ptr != s.expectedPointer {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: fmt.Sprintf("%s pointer mismatch", s.expectedKind),
Got: types.RawString(fmt.Sprintf("0x%x", ptr)),
Expected: types.RawString(fmt.Sprintf("0x%x", s.expectedPointer)),
})
}
return nil
}
func (s *tdShallow) String() string {
if s.err != nil {
return s.stringError()
}
return fmt.Sprintf("(%s) 0x%x", s.expectedKind, s.expectedPointer)
}
go-testdeep-1.15.0/td/td_shallow_test.go 0000664 0000000 0000000 00000007741 15144170453 0020170 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (
"regexp"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestShallow(t *testing.T) {
checkOK(t, nil, nil)
//
// Slice
back := [...]int{1, 2, 3, 1, 2, 3}
as := back[:3]
bs := back[3:]
checkError(t, bs, td.Shallow(back[:]),
expectedError{
Message: mustBe("slice pointer mismatch"),
Path: mustBe("DATA"),
Got: mustContain("0x"),
Expected: mustContain("0x"),
})
checkOK(t, as, td.Shallow(back[:]))
checkOK(t, ([]byte)(nil), ([]byte)(nil))
//
// Map
gotMap := map[string]bool{"a": true, "b": false}
expectedMap := map[string]bool{"a": true, "b": false}
checkError(t, gotMap, td.Shallow(expectedMap),
expectedError{
Message: mustBe("map pointer mismatch"),
Path: mustBe("DATA"),
Got: mustContain("0x"),
Expected: mustContain("0x"),
})
expectedMap = gotMap
checkOK(t, gotMap, td.Shallow(expectedMap))
checkOK(t, (map[string]bool)(nil), (map[string]bool)(nil))
//
// Ptr
type MyStruct struct {
val int
}
gotPtr := &MyStruct{val: 12}
expectedPtr := &MyStruct{val: 12}
checkError(t, gotPtr, td.Shallow(expectedPtr),
expectedError{
Message: mustBe("ptr pointer mismatch"),
Path: mustBe("DATA"),
Got: mustContain("0x"),
Expected: mustContain("0x"),
})
expectedPtr = gotPtr
checkOK(t, gotPtr, td.Shallow(expectedPtr))
checkOK(t, (*MyStruct)(nil), (*MyStruct)(nil))
//
// Func
gotFunc := func(a int) int { return a * 2 }
expectedFunc := func(a int) int { return a * 2 }
checkError(t, gotFunc, td.Shallow(expectedFunc),
expectedError{
Message: mustBe("func pointer mismatch"),
Path: mustBe("DATA"),
Got: mustContain("0x"),
Expected: mustContain("0x"),
})
expectedFunc = gotFunc
checkOK(t, gotFunc, td.Shallow(expectedFunc))
checkOK(t, (func(a int) int)(nil), (func(a int) int)(nil))
//
// Chan
gotChan := make(chan int)
expectedChan := make(chan int)
checkError(t, gotChan, td.Shallow(expectedChan),
expectedError{
Message: mustBe("chan pointer mismatch"),
Path: mustBe("DATA"),
Got: mustContain("0x"),
Expected: mustContain("0x"),
})
expectedChan = gotChan
checkOK(t, gotChan, td.Shallow(expectedChan))
checkOK(t, (chan int)(nil), (chan int)(nil))
//
// String
backStr := "foobarfoobar!"
a := backStr[:6]
b := backStr[6:12]
checkOK(t, a, td.Shallow(backStr))
checkOK(t, backStr, td.Shallow(a))
checkOK(t, b, td.Shallow(backStr[6:7]))
checkError(t, backStr, td.Shallow(b),
expectedError{
Message: mustBe("string pointer mismatch"),
Path: mustBe("DATA"),
Got: mustContain("0x"),
Expected: mustContain("0x"),
})
checkError(t, b, td.Shallow(backStr),
expectedError{
Message: mustBe("string pointer mismatch"),
Path: mustBe("DATA"),
Got: mustContain("0x"),
Expected: mustContain("0x"),
})
//
// Erroneous mix
checkError(t, gotMap, td.Shallow(expectedChan),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustContain("map"),
Expected: mustContain("chan"),
})
//
// Bad usage
checkError(t, "never tested",
td.Shallow(42),
expectedError{
Message: mustBe("bad usage of Shallow operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Shallow(CHANNEL|FUNC|MAP|PTR|SLICE|UNSAFE_PTR|STRING), but received int as 1st parameter"),
})
//
//
reg := regexp.MustCompile(`^\(map\) 0x[a-f0-9]+\z`)
if !reg.MatchString(td.Shallow(expectedMap).String()) {
t.Errorf("Shallow().String() failed\n got: %s\nexpected: %s",
td.Shallow(expectedMap).String(), reg)
}
// Erroneous op
test.EqualStr(t, td.Shallow(42).String(), "Shallow()")
}
func TestShallowTypeBehind(t *testing.T) {
equalTypes(t, td.Shallow(t), nil)
// Erroneous op
equalTypes(t, td.Shallow(42), nil)
}
go-testdeep-1.15.0/td/td_smuggle.go 0000664 0000000 0000000 00000066336 15144170453 0017130 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-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 (
"bytes"
"fmt"
"io"
"reflect"
"strconv"
"strings"
"sync"
"unicode"
"unicode/utf8"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/types"
"github.com/maxatome/go-testdeep/internal/util"
)
// SmuggledGot can be returned by a [Smuggle] function to name the
// transformed / returned value.
type SmuggledGot struct {
Name string
Got any
}
const smuggled = ""
var (
smuggleFnsMu sync.Mutex
smuggleFns = map[any]reflect.Value{}
nilError = reflect.New(types.Error).Elem()
)
func (s SmuggledGot) contextAndGot(ctx ctxerr.Context) (ctxerr.Context, reflect.Value) {
// If the Name starts with a Letter, prefix it by a "."
var name string
if s.Name != "" {
first, _ := utf8.DecodeRuneInString(s.Name)
if unicode.IsLetter(first) {
name = "."
}
name += s.Name
} else {
name = smuggled
}
return ctx.AddCustomLevel(name), reflect.ValueOf(s.Got)
}
type tdSmuggle struct {
tdSmugglerBase
function reflect.Value
argType reflect.Type
str string
}
var _ TestDeep = &tdSmuggle{}
type smuggleValue struct {
Path string
Value reflect.Value
}
var smuggleValueType = reflect.TypeOf(smuggleValue{})
type smuggleField struct {
Name string
Indexed bool
Method bool
}
func joinFieldsPath(path []smuggleField) string {
var buf strings.Builder
for i, part := range path {
if part.Indexed {
fmt.Fprintf(&buf, "[%s]", part.Name)
} else {
if i > 0 {
buf.WriteByte('.')
}
buf.WriteString(part.Name)
if part.Method {
buf.WriteString("()")
}
}
}
return buf.String()
}
func splitFieldsPath(origPath string) ([]smuggleField, error) {
if origPath == "" {
return nil, fmt.Errorf("FIELDS_PATH cannot be empty")
}
privateField := ""
var res []smuggleField
for path := origPath; len(path) > 0; {
r, _ := utf8.DecodeRuneInString(path)
switch r {
case '[':
path = path[1:]
end := strings.IndexByte(path, ']')
if end < 0 {
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 FIELDS_PATH %q", origPath)
}
path = path[1:]
if path == "" {
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 FIELDS_PATH %q", r, origPath)
}
fallthrough
default:
var field string
end := strings.IndexAny(path, ".[")
if end < 0 {
field, path = path, ""
} else {
field, path = path[:end], path[end:]
}
if strings.HasSuffix(field, "()") {
if len(field) == 2 {
return nil, fmt.Errorf("missing method name before () in FIELDS_PATH %q", origPath)
}
for j, r := range field[:len(field)-2] {
if j == 0 && !unicode.IsUpper(r) {
return nil, fmt.Errorf("method name %q is not public in FIELDS_PATH %q", field, origPath)
}
if !unicode.IsLetter(r) && !unicode.IsNumber(r) {
return nil, fmt.Errorf("unexpected %q in method name %q in FIELDS_PATH %q", r, field, origPath)
}
}
if privateField != "" {
return nil, fmt.Errorf("cannot call method %s as it is based on an unexported field %q in FIELDS_PATH %q", field, privateField, origPath)
}
res = append(res, smuggleField{Name: field[:len(field)-2], Method: true})
} else {
for j, r := range field {
if privateField == "" && j == 0 && !unicode.IsUpper(r) {
privateField = field
}
if !unicode.IsLetter(r) && (j == 0 || !unicode.IsNumber(r)) {
return nil, fmt.Errorf("unexpected %q in field name %q in FIELDS_PATH %q", r, field, origPath)
}
}
res = append(res, smuggleField{Name: field})
}
}
}
return res, nil
}
func nilFieldErr(path []smuggleField) error {
return fmt.Errorf("field %q is nil", joinFieldsPath(path))
}
func buildFieldsPathFn(path string) (func(any) (smuggleValue, error), error) {
parts, err := splitFieldsPath(path)
if err != nil {
return nil, err
}
return func(got any) (smuggleValue, error) {
vgot := reflect.ValueOf(got)
for idxPart, field := range parts {
if field.Method {
var method reflect.Value
for {
method = vgot.MethodByName(field.Name)
if !method.IsValid() {
switch vgot.Kind() {
case reflect.Interface, reflect.Ptr:
if !vgot.IsNil() {
vgot = vgot.Elem()
continue
}
return smuggleValue{}, nilFieldErr(parts[:idxPart])
}
if idxPart > 0 {
return smuggleValue{}, fmt.Errorf(
"field %s (type %s) does not implement %s() method",
joinFieldsPath(parts[:idxPart]),
vgot.Type(),
field.Name)
}
return smuggleValue{}, fmt.Errorf(
"type %s has no method %s()", vgot.Type(), field.Name)
}
break
}
mt := method.Type()
if mt.NumIn() != 0 ||
(mt.NumOut() != 1 && (mt.NumOut() != 2 || mt.Out(1) != types.Error)) {
return smuggleValue{}, fmt.Errorf(
"cannot call %s, signature %s not handled, only func() A or func() (A, error) allowed",
joinFieldsPath(parts[:idxPart+1]),
method.Type())
}
var ret []reflect.Value
var panicked any
func() {
defer func() { panicked = recover() }()
ret = method.Call(nil)
}()
if panicked != nil {
return smuggleValue{}, fmt.Errorf(
"method %s panicked: %v",
joinFieldsPath(parts[:idxPart+1]),
panicked)
}
if len(ret) == 2 && !ret[1].IsNil() {
return smuggleValue{}, fmt.Errorf(
"method %s returned an error: %w",
joinFieldsPath(parts[:idxPart+1]),
ret[1].Interface().(error))
}
vgot = ret[0]
continue
}
// Resolve all interface and pointer dereferences
origKind := vgot.Kind()
for {
switch vgot.Kind() {
case reflect.Interface, reflect.Ptr:
if vgot.IsNil() {
return smuggleValue{}, nilFieldErr(parts[:idxPart])
}
vgot = vgot.Elem()
continue
}
break
}
if !field.Indexed {
if vgot.Kind() == reflect.Struct {
vgot = vgot.FieldByName(field.Name)
if !vgot.IsValid() {
return smuggleValue{}, fmt.Errorf(
"field %q not found",
joinFieldsPath(parts[:idxPart+1]))
}
continue
}
// Accept map but only map[string]…
if vgot.Kind() != reflect.Map ||
vgot.Type().Key().Kind() != reflect.String {
deref := ""
if origKind != vgot.Kind() {
deref = " (after dereferencing)"
}
if idxPart == 0 {
return smuggleValue{}, fmt.Errorf(
"it is a %s%s and should be a struct or a map[string]…",
vgot.Kind(), deref)
}
if parts[idxPart-1].Method {
return smuggleValue{}, fmt.Errorf(
"method %s returned a %s%s and should be a struct or a map[string]…",
joinFieldsPath(parts[:idxPart]), vgot.Kind(), deref)
}
return smuggleValue{}, fmt.Errorf(
"field %q is a %s%s and should be a struct or a map[string]…",
joinFieldsPath(parts[:idxPart]), vgot.Kind(), deref)
}
}
switch vgot.Kind() {
case reflect.Map:
tkey := vgot.Type().Key()
var vkey reflect.Value
switch tkey.Kind() {
case reflect.String:
vkey = reflect.ValueOf(field.Name)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
i, err := strconv.ParseInt(field.Name, 10, 64)
if err != nil {
return smuggleValue{}, fmt.Errorf(
"field %q, %q is not an integer and so cannot match %s map key type",
joinFieldsPath(parts[:idxPart+1]), field.Name, tkey)
}
vkey = reflect.ValueOf(i).Convert(tkey)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
i, err := strconv.ParseUint(field.Name, 10, 64)
if err != nil {
return smuggleValue{}, fmt.Errorf(
"field %q, %q is not an unsigned integer and so cannot match %s map key type",
joinFieldsPath(parts[:idxPart+1]), field.Name, tkey)
}
vkey = reflect.ValueOf(i).Convert(tkey)
case reflect.Float32, reflect.Float64:
f, err := strconv.ParseFloat(field.Name, 64)
if err != nil {
return smuggleValue{}, fmt.Errorf(
"field %q, %q is not a float and so cannot match %s map key type",
joinFieldsPath(parts[:idxPart+1]), field.Name, tkey)
}
vkey = reflect.ValueOf(f).Convert(tkey)
case reflect.Complex64, reflect.Complex128:
c, err := strconv.ParseComplex(field.Name, 128)
if err != nil {
return smuggleValue{}, fmt.Errorf(
"field %q, %q is not a complex number and so cannot match %s map key type",
joinFieldsPath(parts[:idxPart+1]), field.Name, tkey)
}
vkey = reflect.ValueOf(c).Convert(tkey)
default:
return smuggleValue{}, fmt.Errorf(
"field %q, %q cannot match unsupported %s map key type",
joinFieldsPath(parts[:idxPart+1]), field.Name, tkey)
}
vgot = vgot.MapIndex(vkey)
if !vgot.IsValid() {
return smuggleValue{}, fmt.Errorf("field %q, %q map key not found",
joinFieldsPath(parts[:idxPart+1]), field.Name)
}
case reflect.Slice, reflect.Array:
i, err := strconv.ParseInt(field.Name, 10, 64)
if err != nil {
return smuggleValue{}, fmt.Errorf(
"field %q, %q is not a slice/array index",
joinFieldsPath(parts[:idxPart+1]), field.Name)
}
if i < 0 {
i = int64(vgot.Len()) + i
}
if i < 0 || i >= int64(vgot.Len()) {
return smuggleValue{}, fmt.Errorf(
"field %q, %d is out of slice/array range (len %d)",
joinFieldsPath(parts[:idxPart+1]), i, vgot.Len())
}
vgot = vgot.Index(int(i))
default:
if idxPart == 0 {
return smuggleValue{},
fmt.Errorf("it is a %s, but a map, array or slice is expected",
vgot.Kind())
}
return smuggleValue{}, fmt.Errorf(
"field %q is a %s, but a map, array or slice is expected",
joinFieldsPath(parts[:idxPart]), vgot.Kind())
}
}
return smuggleValue{
Path: path,
Value: vgot,
}, nil
}, nil
}
func getFieldsPathFn(fieldPath string) (reflect.Value, error) {
smuggleFnsMu.Lock()
defer smuggleFnsMu.Unlock()
if vfn, ok := smuggleFns[fieldPath]; ok {
return vfn, nil
}
fn, err := buildFieldsPathFn(fieldPath)
if err != nil {
return reflect.Value{}, err
}
vfn := reflect.ValueOf(fn)
smuggleFns[fieldPath] = vfn
return vfn, err
}
func getCaster(outType reflect.Type) reflect.Value {
smuggleFnsMu.Lock()
defer smuggleFnsMu.Unlock()
if vfn, ok := smuggleFns[outType]; ok {
return vfn
}
var fn reflect.Value
switch outType.Kind() {
case reflect.String:
fn = buildCaster(outType, true)
case reflect.Slice:
if outType.Elem().Kind() == reflect.Uint8 {
// Special case for slices of bytes: falls back on io.Reader if not []byte
fn = buildCaster(outType, false)
break
}
fallthrough
default:
// For all other types, take the received param and return
// it. Smuggle already converted got to the type of param, so the
// work is done.
inOut := []reflect.Type{outType}
fn = reflect.MakeFunc(
reflect.FuncOf(inOut, inOut, false),
func(args []reflect.Value) []reflect.Value { return args },
)
}
smuggleFns[outType] = fn
return fn
}
// buildCaster returns a function:
//
// func(in any) (out outType, err error)
//
// dynamically checks…
// - if useString is false, as outType is a slice of bytes:
// 1. in is a []byte or convertible to []byte
// 2. in implements io.Reader
// - if useString is true, as outType is a string:
// 1. in is a []byte or convertible to string
// 2. in implements io.Reader
func buildCaster(outType reflect.Type, useString bool) reflect.Value {
zeroRet := reflect.New(outType).Elem()
return reflect.MakeFunc(
reflect.FuncOf(
[]reflect.Type{types.Interface},
[]reflect.Type{outType, types.Error},
false,
),
func(args []reflect.Value) []reflect.Value {
if args[0].IsNil() {
return []reflect.Value{
zeroRet,
reflect.ValueOf(&ctxerr.Error{
Message: "incompatible parameter type",
Got: types.RawString("nil"),
Expected: types.RawString(outType.String() + " or convertible or io.Reader"),
}),
}
}
// 1st & only arg is always an interface
args[0] = args[0].Elem()
if ok, convertible := types.IsTypeOrConvertible(args[0], outType); ok {
if convertible {
return []reflect.Value{args[0].Convert(outType), nilError}
}
return []reflect.Value{args[0], nilError}
}
// Our caller encures Interface() can be called safely
switch ta := args[0].Interface().(type) {
case io.Reader:
var b bytes.Buffer
if _, err := b.ReadFrom(ta); err != nil {
return []reflect.Value{
zeroRet,
reflect.ValueOf(&ctxerr.Error{
Message: "an error occurred while reading from io.Reader",
Summary: ctxerr.NewSummary(err.Error()),
}),
}
}
var buf any
if useString {
buf = b.String()
} else {
buf = b.Bytes()
}
return []reflect.Value{
reflect.ValueOf(buf).Convert(outType),
nilError,
}
default:
return []reflect.Value{
zeroRet,
reflect.ValueOf(&ctxerr.Error{
Message: "incompatible parameter type",
Got: types.RawString(args[0].Type().String()),
Expected: types.RawString(outType.String() + " or convertible or io.Reader"),
}),
}
}
})
}
// summary(Smuggle): changes data contents or mutates it into another
// type via a custom function or a struct fields-path before stepping
// down in favor of generic comparison process
// input(Smuggle): all
// Smuggle operator allows to change data contents or mutate it into
// another type before stepping down in favor of generic comparison
// process. Of course it is a smuggler operator. So fn is a function
// that must take one parameter whose type must be convertible to the
// type of the compared value.
//
// As convenient shortcuts, fn can be a string specifying a
// fields-path through structs, maps & slices, or any other type, in
// this case a simple cast is done (see below for details).
//
// fn must return at least one value. These value will be compared as is
// to expectedValue, here integer 28:
//
// td.Cmp(t, "0028",
// td.Smuggle(func(value string) int {
// num, _ := strconv.Atoi(value)
// return num
// }, 28),
// )
//
// or using an other [TestDeep] operator, here [Between](28, 30):
//
// td.Cmp(t, "0029",
// td.Smuggle(func(value string) int {
// num, _ := strconv.Atoi(value)
// return num
// }, td.Between(28, 30)),
// )
//
// fn can return a second boolean value, used to tell that a problem
// occurred and so stop the comparison:
//
// td.Cmp(t, "0029",
// td.Smuggle(func(value string) (int, bool) {
// num, err := strconv.Atoi(value)
// return num, err == nil
// }, td.Between(28, 30)),
// )
//
// fn can return a third string value which is used to describe the
// test when a problem occurred (false second boolean value):
//
// td.Cmp(t, "0029",
// td.Smuggle(func(value string) (int, bool, string) {
// num, err := strconv.Atoi(value)
// if err != nil {
// return 0, false, "string must contain a number"
// }
// return num, true, ""
// }, td.Between(28, 30)),
// )
//
// Instead of returning (X, bool) or (X, bool, string), fn can
// return (X, error). When a problem occurs, the returned error is
// non-nil, as in:
//
// td.Cmp(t, "0029",
// td.Smuggle(func(value string) (int, error) {
// num, err := strconv.Atoi(value)
// return num, err
// }, td.Between(28, 30)),
// )
//
// Which can be simplified to:
//
// td.Cmp(t, "0029", td.Smuggle(strconv.Atoi, td.Between(28, 30)))
//
// Imagine you want to compare that the Year of a date is between 2010
// and 2020:
//
// td.Cmp(t, time.Date(2015, time.May, 1, 1, 2, 3, 0, time.UTC),
// td.Smuggle(func(date time.Time) int { return date.Year() },
// td.Between(2010, 2020)),
// )
//
// In this case the data location forwarded to next test will be
// something like "DATA.MyTimeField", but you can act on it
// too by returning a [SmuggledGot] struct (by value or by address):
//
// td.Cmp(t, time.Date(2015, time.May, 1, 1, 2, 3, 0, time.UTC),
// td.Smuggle(func(date time.Time) SmuggledGot {
// return SmuggledGot{
// Name: "Year",
// Got: date.Year(),
// }
// }, td.Between(2010, 2020)),
// )
//
// then the data location forwarded to next test will be something like
// "DATA.MyTimeField.Year". The "." between the current path (here
// "DATA.MyTimeField") and the returned Name "Year" is automatically
// added when Name starts with a Letter.
//
// Note that [SmuggledGot] and [*SmuggledGot] returns are treated
// equally, and they are only used when fn has only one returned value
// or when the second boolean returned value is true.
//
// Of course, all cases can go together:
//
// // Accepts a "YYYY/mm/DD HH:MM:SS" string to produce a time.Time and tests
// // whether this date is contained between 2 hours before now and now.
// td.Cmp(t, "2020-01-25 12:13:14",
// td.Smuggle(func(date string) (*SmuggledGot, bool, string) {
// date, err := time.Parse("2006/01/02 15:04:05", date)
// if err != nil {
// return nil, false, `date must conform to "YYYY/mm/DD HH:MM:SS" format`
// }
// return &SmuggledGot{
// Name: "Date",
// Got: date,
// }, true, ""
// }, td.Between(time.Now().Add(-2*time.Hour), time.Now())),
// )
//
// or:
//
// // Accepts a "YYYY/mm/DD HH:MM:SS" string to produce a time.Time and tests
// // whether this date is contained between 2 hours before now and now.
// td.Cmp(t, "2020-01-25 12:13:14",
// td.Smuggle(func(date string) (*SmuggledGot, error) {
// date, err := time.Parse("2006/01/02 15:04:05", date)
// if err != nil {
// return nil, err
// }
// return &SmuggledGot{
// Name: "Date",
// Got: date,
// }, nil
// }, td.Between(time.Now().Add(-2*time.Hour), time.Now())),
// )
//
// Smuggle can also be used to access a struct field embedded in
// several struct layers.
//
// type A struct{ Num int }
// // func (a *A) String() string { return fmt.Sprintf("Num is %d", a.Num) }
// type B struct{ As map[string]*A }
// type C struct{ B B }
// got := C{B: B{As: map[string]*A{"foo": {Num: 12}}}}
//
// // Tests that got.B.A.Num is 12
// td.Cmp(t, got,
// td.Smuggle(func(c C) int {
// return c.B.As["foo"].Num
// }, 12))
//
// As brought up above, a fields-path can be passed as fn value
// instead of a function pointer. Using this feature, the [Cmp]
// call in the above example can be rewritten as follows:
//
// // Tests that got.B.As["foo"].Num is 12
// td.Cmp(t, got, td.Smuggle("B.As[foo].Num", 12))
//
// For convenience, if a map[string]… is addressed, it can be done
// like a struct field as "foo" key in:
//
// td.Cmp(t, got, td.Smuggle("B.As.foo.Num", 12))
//
// In addition, simple public methods can also be called like in:
//
// td.Cmp(t, got, td.Smuggle("B.As[foo].String()", "Num is 12"))
//
// Allowed methods must not take any parameter and must return one
// value or a value and an error. For the latter case, if the method
// returns a non-nil error, the comparison fails. The comparison also
// fails if a panic occurs or if a method cannot be called. No private
// fields should be traversed before calling the method. For fun,
// consider a more complex example involving [reflect] and chaining
// method calls:
//
// got := reflect.Valueof(&C{B: B{As: map[string]*A{"foo": {Num: 12}}}})
// td.Cmp(t, got, td.Smuggle("Elem().Interface().B.As[foo].String()", "Num is 12"))
//
// Contrary to [JSONPointer] operator, private fields can be followed
// and public methods on public fields can be called. Arrays, slices
// and maps work using the index/key inside square brackets (e.g. [12]
// or [foo]). Maps work only for simple key types (string or numbers),
// without "" when using strings (e.g. [foo]).
//
// Behind the scenes, a temporary function is automatically created to
// achieve the same goal, but adds some checks against nil values and
// auto-dereferences interfaces and pointers, even on several levels,
// like in:
//
// type A struct{ N any }
// num := 12
// pnum := &num
// td.Cmp(t, A{N: &pnum}, td.Smuggle("N", 12))
//
// Last but not least, a simple type can be passed as fn to operate
// a cast, handling specifically strings and slices of bytes:
//
// td.Cmp(t, `{"foo":1}`, td.Smuggle(json.RawMessage{}, td.JSON(`{"foo":1}`)))
// // or equally
// td.Cmp(t, `{"foo":1}`, td.Smuggle(json.RawMessage(nil), td.JSON(`{"foo":1}`)))
//
// converts on the fly a string to a [json.RawMessage] so [JSON] operator
// can parse it as JSON. This is mostly a shortcut for:
//
// td.Cmp(t, `{"foo":1}`, td.Smuggle(
// func(r json.RawMessage) json.RawMessage { return r },
// td.JSON(`{"foo":1}`)))
//
// except that for strings and slices of bytes (like here), it accepts
// [io.Reader] interface too:
//
// var body io.Reader
// // …
// td.Cmp(t, body, td.Smuggle(json.RawMessage{}, td.JSON(`{"foo":1}`)))
// // or equally
// td.Cmp(t, body, td.Smuggle(json.RawMessage(nil), td.JSON(`{"foo":1}`)))
//
// This last example allows to easily inject body content into JSON
// operator.
//
// The difference between Smuggle and [Code] operators is that [Code]
// is used to do a final comparison while Smuggle transforms the data
// and then steps down in favor of generic comparison
// process. Moreover, the type accepted as input for the function is
// more lax to facilitate the writing of tests (e.g. the function can
// accept a float64 and the got value be an int). See examples. On the
// other hand, the output type is strict and must match exactly the
// expected value type. The fields-path string fn shortcut and the
// cast feature are not available with [Code] operator.
//
// TypeBehind method returns the [reflect.Type] of only parameter of
// fn. For the case where fn is a fields-path, it is always
// any, as the type can not be known in advance.
//
// See also [Code], [JSONPointer] and [Flatten].
//
// [json.RawMessage]: https://pkg.go.dev/encoding/json#RawMessage
func Smuggle(fn, expectedValue any) TestDeep {
s := tdSmuggle{
tdSmugglerBase: newSmugglerBase(expectedValue),
}
const usage = "(FUNC|FIELDS_PATH|ANY_TYPE, TESTDEEP_OPERATOR|EXPECTED_VALUE)"
const fullUsage = "Smuggle" + usage
var vfn reflect.Value
switch rfn := fn.(type) {
case reflect.Type:
switch rfn.Kind() {
case reflect.Func, reflect.Invalid, reflect.Interface:
s.err = ctxerr.OpBad("Smuggle",
"usage: Smuggle%s, ANY_TYPE reflect.Type cannot be Func nor Interface", usage)
return &s
default:
vfn = getCaster(rfn)
s.str = "type:" + rfn.String()
}
case string:
if rfn == "" {
vfn = getCaster(reflect.TypeOf(fn))
s.str = "type:string"
break
}
var err error
vfn, err = getFieldsPathFn(rfn)
if err != nil {
s.err = ctxerr.OpBad("Smuggle", "Smuggle%s: %s", usage, err)
return &s
}
s.str = strconv.Quote(rfn)
default:
vfn = reflect.ValueOf(fn)
switch vfn.Kind() {
case reflect.Func:
s.str = vfn.Type().String()
// nothing to check
case reflect.Invalid, reflect.Interface:
s.err = ctxerr.OpBad("Smuggle",
"usage: Smuggle%s, ANY_TYPE cannot be nil nor Interface", usage)
return &s
default:
typ := vfn.Type()
vfn = getCaster(typ)
s.str = "type:" + typ.String()
}
}
fnType := vfn.Type()
if fnType.IsVariadic() || fnType.NumIn() != 1 {
s.err = ctxerr.OpBad("Smuggle", fullUsage+": FUNC must take only one non-variadic argument")
return &s
}
switch fnType.NumOut() {
case 3: // (value, bool, string)
if fnType.Out(2).Kind() != reflect.String {
break
}
fallthrough
case 2:
// (value, *bool*) or (value, *bool*, string)
if fnType.Out(1).Kind() != reflect.Bool &&
// (value, *error*)
(fnType.NumOut() > 2 ||
fnType.Out(1) != types.Error) {
break
}
fallthrough
case 1: // (value)
if vfn.IsNil() {
s.err = ctxerr.OpBad("Smuggle", "Smuggle(FUNC): FUNC cannot be a nil function")
return &s
}
s.argType = fnType.In(0)
s.function = vfn
if !s.isTestDeeper {
s.expectedValue = reflect.ValueOf(expectedValue)
}
return &s
}
s.err = ctxerr.OpBad("Smuggle",
fullUsage+": FUNC must return value or (value, bool) or (value, bool, string) or (value, error)")
return &s
}
func (s *tdSmuggle) laxConvert(got reflect.Value) (reflect.Value, bool) {
if got.IsValid() {
if types.IsConvertible(got, s.argType) {
return got.Convert(s.argType), true
}
} else if s.argType == types.Interface {
// nil only accepted if any expected
return reflect.New(types.Interface).Elem(), true
}
return got, false
}
func (s *tdSmuggle) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if s.err != nil {
return ctx.CollectError(s.err)
}
got, ok := s.laxConvert(got)
if !ok {
if ctx.BooleanError {
return ctxerr.BooleanError
}
err := ctxerr.Error{
Message: "incompatible parameter type",
Expected: types.RawString(s.argType.String()),
}
if got.IsValid() {
err.Got = types.RawString(got.Type().String())
} else {
err.Got = types.RawString("nil")
}
return ctx.CollectError(&err)
}
// Refuse to override unexported fields access in this case. It is a
// choice, as we think it is better to work on surrounding struct
// instead.
if !got.CanInterface() {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "cannot smuggle unexported field",
Summary: ctxerr.NewSummary("work on surrounding struct instead"),
})
}
ret := s.function.Call([]reflect.Value{got})
if len(ret) == 1 ||
(ret[1].Kind() == reflect.Bool && ret[1].Bool()) ||
(ret[1].Kind() == reflect.Interface && ret[1].IsNil()) {
newGot := ret[0]
var newCtx ctxerr.Context
if newGot.IsValid() {
switch newGot.Type() {
case smuggledGotType:
newCtx, newGot = newGot.Interface().(SmuggledGot).contextAndGot(ctx)
case smuggledGotPtrType:
if smGot := newGot.Interface().(*SmuggledGot); smGot == nil {
newCtx, newGot = ctx, reflect.ValueOf(nil)
} else {
newCtx, newGot = smGot.contextAndGot(ctx)
}
case smuggleValueType:
smv := newGot.Interface().(smuggleValue)
newCtx, newGot = ctx.AddCustomLevel("."+smv.Path), smv.Value
default:
newCtx = ctx.AddCustomLevel(smuggled)
}
}
return deepValueEqual(newCtx, newGot, s.expectedValue)
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
var reason string
switch len(ret) {
case 3: // (value, false, string)
reason = ret[2].String()
case 2:
// (value, error)
if ret[1].Kind() == reflect.Interface {
// For internal use only
if cErr, ok := ret[1].Interface().(*ctxerr.Error); ok {
return ctx.CollectError(cErr)
}
reason = ret[1].Interface().(error).Error()
}
// (value, false)
}
return ctx.CollectError(&ctxerr.Error{
Message: "ran smuggle code with %% as argument",
Summary: ctxerr.NewSummaryReason(got, reason),
})
}
func (s *tdSmuggle) HandleInvalid() bool {
return true // Knows how to handle untyped nil values (aka invalid values)
}
func (s *tdSmuggle) String() string {
if s.err != nil {
return s.stringError()
}
return "Smuggle(" + s.str + ", " + util.ToString(s.expectedValue) + ")"
}
func (s *tdSmuggle) TypeBehind() reflect.Type {
if s.err != nil {
return nil
}
return s.argType
}
go-testdeep-1.15.0/td/td_smuggle_private_test.go 0000664 0000000 0000000 00000022234 15144170453 0021706 0 ustar 00root root 0000000 0000000 // Copyright (c) 2021-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"
"reflect"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
)
func TestFieldsPath(t *testing.T) {
check := func(in string, expected ...string) []smuggleField {
t.Helper()
got, err := splitFieldsPath(in)
test.NoError(t, err)
var gotStr []string
for _, s := range got {
if s.Method {
gotStr = append(gotStr, s.Name+"()")
} else {
gotStr = append(gotStr, s.Name)
}
}
if !reflect.DeepEqual(gotStr, expected) {
t.Errorf("Failed:\n got: %v\n expected: %v", got, expected)
}
test.EqualStr(t, in, joinFieldsPath(got))
return got
}
check("test", "test")
check("Test.Foo().bar", "Test", "Foo()", "bar")
check("test.foo.bar", "test", "foo", "bar")
check("test[foo.bar]", "test", "foo.bar")
check("test[foo][bar]", "test", "foo", "bar")
fp := check("test[foo][bar].zip", "test", "foo", "bar", "zip")
// "." can be omitted just after "]"
got, err := splitFieldsPath("test[foo][bar]zip")
test.NoError(t, err)
if !reflect.DeepEqual(got, fp) {
t.Errorf("Failed:\n got: %v\n expected: %v", got, fp)
}
check("[foo][bar]", "foo", "bar")
check("[0][foo][bar]", "0", "foo", "bar")
//
// Errors
checkErr := func(in, expectedErr string) {
t.Helper()
_, err := splitFieldsPath(in)
if test.Error(t, err) {
test.EqualStr(t, err.Error(), expectedErr)
}
}
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 FIELDS_PATH "foo[bar"`)
}
type SmuggleBuild struct {
Field struct {
Path string
}
Iface any
Next *SmuggleBuild
}
func (s SmuggleBuild) FollowIface() any {
return s.Iface
}
func (s *SmuggleBuild) PtrFollowIface() any {
return s.Iface
}
func (s SmuggleBuild) MayFollowIface() (any, error) {
if s.Iface == nil {
return nil, errors.New("Iface is nil")
}
return s.Iface, nil
}
func (s SmuggleBuild) FollowNext() *SmuggleBuild {
return s.Next
}
func (s *SmuggleBuild) PtrFollowNext() *SmuggleBuild {
return s.Next
}
func (s SmuggleBuild) MayFollowNext() (*SmuggleBuild, error) {
if s.Next == nil {
return nil, errors.New("Next is nil")
}
return s.Next, nil
}
func (s SmuggleBuild) Error() (bool, error) {
return false, errors.New("an error occurred")
}
func (s SmuggleBuild) SetPath(path string) {
s.Field.Path = path
}
func (s SmuggleBuild) Panic() string {
panic("oops!")
}
func (s SmuggleBuild) Num() int {
return 42
}
func (s *SmuggleBuild) PNum() int {
return 42
}
func TestBuildFieldsPathFn(t *testing.T) {
_, err := buildFieldsPathFn("bad[path")
test.Error(t, err)
t.Run("Struct", func(t *testing.T) {
fn, err := buildFieldsPathFn("Field.Path.Bad")
if test.NoError(t, err) {
_, err = fn(SmuggleBuild{})
if test.Error(t, err) {
test.EqualStr(t, err.Error(),
`field "Field.Path" is a string and should be a struct or a map[string]…`)
}
_, err = fn(123)
if test.Error(t, err) {
test.EqualStr(t, err.Error(),
"it is a int and should be a struct or a map[string]…")
}
}
fn, err = buildFieldsPathFn("Iface.Bad")
if test.NoError(t, err) {
_, err = fn(SmuggleBuild{Iface: 42})
if test.Error(t, err) {
test.EqualStr(t, err.Error(),
`field "Iface" is a int (after dereferencing) and should be a struct or a map[string]…`)
}
num := 42
_, err = fn(&num)
if test.Error(t, err) {
test.EqualStr(t, err.Error(),
`it is a int (after dereferencing) and should be a struct or a map[string]…`)
}
}
fn, err = buildFieldsPathFn("Field.Unknown")
if test.NoError(t, err) {
_, err = fn(SmuggleBuild{})
if test.Error(t, err) {
test.EqualStr(t, err.Error(), `field "Field.Unknown" not found`)
}
}
})
t.Run("Map", func(t *testing.T) {
fn, err := buildFieldsPathFn("Iface[str].Field")
if test.NoError(t, err) {
_, err = fn(SmuggleBuild{Iface: map[int]SmuggleBuild{}})
if test.Error(t, err) {
test.EqualStr(t, err.Error(),
`field "Iface[str]", "str" is not an integer and so cannot match int map key type`)
}
_, err = fn(SmuggleBuild{Iface: map[uint]SmuggleBuild{}})
if test.Error(t, err) {
test.EqualStr(t, err.Error(),
`field "Iface[str]", "str" is not an unsigned integer and so cannot match uint map key type`)
}
_, err = fn(SmuggleBuild{Iface: map[float32]SmuggleBuild{}})
if test.Error(t, err) {
test.EqualStr(t, err.Error(),
`field "Iface[str]", "str" is not a float and so cannot match float32 map key type`)
}
_, err = fn(SmuggleBuild{Iface: map[complex128]SmuggleBuild{}})
if test.Error(t, err) {
test.EqualStr(t, err.Error(),
`field "Iface[str]", "str" is not a complex number and so cannot match complex128 map key type`)
}
_, err = fn(SmuggleBuild{Iface: map[struct{ A int }]SmuggleBuild{}})
if test.Error(t, err) {
test.EqualStr(t, err.Error(),
`field "Iface[str]", "str" cannot match unsupported struct { A int } map key type`)
}
_, err = fn(SmuggleBuild{Iface: map[string]SmuggleBuild{}})
if test.Error(t, err) {
test.EqualStr(t, err.Error(), `field "Iface[str]", "str" map key not found`)
}
}
})
t.Run("Array-Slice", func(t *testing.T) {
fn, err := buildFieldsPathFn("Iface[str].Field")
if test.NoError(t, err) {
_, err = fn(SmuggleBuild{Iface: []int{}})
if test.Error(t, err) {
test.EqualStr(t, err.Error(),
`field "Iface[str]", "str" is not a slice/array index`)
}
}
fn, err = buildFieldsPathFn("Iface[18].Field")
if test.NoError(t, err) {
_, err = fn(SmuggleBuild{Iface: []int{1, 2, 3}})
if test.Error(t, err) {
test.EqualStr(t, err.Error(),
`field "Iface[18]", 18 is out of slice/array range (len 3)`)
}
_, err = fn(SmuggleBuild{Iface: 42})
if test.Error(t, err) {
test.EqualStr(t, err.Error(),
`field "Iface" is a int, but a map, array or slice is expected`)
}
}
fn, err = buildFieldsPathFn("[18].Field")
if test.NoError(t, err) {
_, err = fn(42)
test.EqualStr(t, err.Error(),
`it is a int, but a map, array or slice is expected`)
}
})
t.Run("Function", func(t *testing.T) {
fn, err := buildFieldsPathFn("Iface.Unknown()")
if test.NoError(t, err) {
_, err = fn(SmuggleBuild{Iface: SmuggleBuild{}})
if test.Error(t, err) {
test.EqualStr(t, err.Error(),
`field Iface (type td.SmuggleBuild) does not implement Unknown() method`)
}
}
fn, err = buildFieldsPathFn("Iface.NilUnknown()")
if test.NoError(t, err) {
_, err = fn(SmuggleBuild{})
if test.Error(t, err) {
test.EqualStr(t, err.Error(), `field "Iface" is nil`)
}
}
fn, err = buildFieldsPathFn("Unknown()")
if test.NoError(t, err) {
_, err = fn(SmuggleBuild{})
if test.Error(t, err) {
test.EqualStr(t, err.Error(),
`type td.SmuggleBuild has no method Unknown()`)
}
}
fn, err = buildFieldsPathFn("Iface.SetPath()")
if test.NoError(t, err) {
_, err = fn(SmuggleBuild{Iface: &SmuggleBuild{}})
if test.Error(t, err) {
test.EqualStr(t, err.Error(),
`cannot call Iface.SetPath(), signature func(string) not handled, only func() A or func() (A, error) allowed`)
}
}
fn, err = buildFieldsPathFn("Iface.Panic()")
if test.NoError(t, err) {
_, err = fn(SmuggleBuild{Iface: &SmuggleBuild{}})
if test.Error(t, err) {
test.EqualStr(t, err.Error(),
`method Iface.Panic() panicked: oops!`)
}
}
fn, err = buildFieldsPathFn("Iface.Error()")
if test.NoError(t, err) {
_, err = fn(SmuggleBuild{Iface: &SmuggleBuild{}})
if test.Error(t, err) {
test.EqualStr(t, err.Error(),
`method Iface.Error() returned an error: an error occurred`)
}
}
fn, err = buildFieldsPathFn("Num().Bad")
if test.NoError(t, err) {
_, err = fn(SmuggleBuild{})
if test.Error(t, err) {
test.EqualStr(t, err.Error(),
`method Num() returned a int and should be a struct or a map[string]…`)
}
}
fn, err = buildFieldsPathFn("FollowIface().Bad")
if test.NoError(t, err) {
_, err = fn(SmuggleBuild{Iface: 42})
if test.Error(t, err) {
test.EqualStr(t, err.Error(),
`method FollowIface() returned a int (after dereferencing) and should be a struct or a map[string]…`)
}
}
})
}
go-testdeep-1.15.0/td/td_smuggle_test.go 0000664 0000000 0000000 00000056654 15144170453 0020171 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-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 (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"reflect"
"testing"
"time"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
// reArmReader is a bytes.Reader that re-arms when an error occurs,
// typically on EOF.
type reArmReader bytes.Reader
var _ io.Reader = (*reArmReader)(nil)
func newReArmReader(b []byte) *reArmReader {
return (*reArmReader)(bytes.NewReader(b))
}
func (r *reArmReader) Read(b []byte) (n int, err error) {
n, err = (*bytes.Reader)(r).Read(b)
if err != nil {
(*bytes.Reader)(r).Seek(0, io.SeekStart) //nolint: errcheck
}
return
}
func (r *reArmReader) String() string { return "" }
func TestSmuggle(t *testing.T) {
num := 42
gotStruct := MyStruct{
MyStructMid: MyStructMid{
MyStructBase: MyStructBase{
ValBool: true,
},
ValStr: "foobar",
},
ValInt: 123,
Ptr: &num,
}
gotTime, err := time.Parse(time.RFC3339, "2018-05-23T12:13:14Z")
if err != nil {
t.Fatal(err)
}
//
// One returned value
checkOK(t,
gotTime,
td.Smuggle(
func(date time.Time) int {
return date.Year()
},
td.Between(2010, 2020)))
checkOK(t,
gotStruct,
td.Smuggle(
func(s MyStruct) td.SmuggledGot {
return td.SmuggledGot{
Name: "ValStr",
Got: s.ValStr,
}
},
td.Contains("oob")))
checkOK(t,
gotStruct,
td.Smuggle(
func(s MyStruct) *td.SmuggledGot {
return &td.SmuggledGot{
Name: "ValStr",
Got: s.ValStr,
}
},
td.Contains("oob")))
//
// 2 returned values
checkOK(t,
gotStruct,
td.Smuggle(
func(s MyStruct) (string, bool) {
if s.ValStr == "" {
return "", false
}
return s.ValStr, true
},
td.Contains("oob")))
checkOK(t,
gotStruct,
td.Smuggle(
func(s MyStruct) (td.SmuggledGot, bool) {
if s.ValStr == "" {
return td.SmuggledGot{}, false
}
return td.SmuggledGot{
Name: "ValStr",
Got: s.ValStr,
}, true
},
td.Contains("oob")))
checkOK(t,
gotStruct,
td.Smuggle(
func(s MyStruct) (*td.SmuggledGot, bool) {
if s.ValStr == "" {
return nil, false
}
return &td.SmuggledGot{
Name: "ValStr",
Got: s.ValStr,
}, true
},
td.Contains("oob")))
//
// 3 returned values
checkOK(t,
gotStruct,
td.Smuggle(
func(s MyStruct) (string, bool, string) {
if s.ValStr == "" {
return "", false, "ValStr must not be empty"
}
return s.ValStr, true, ""
},
td.Contains("oob")))
checkOK(t,
gotStruct,
td.Smuggle(
func(s MyStruct) (td.SmuggledGot, bool, string) {
if s.ValStr == "" {
return td.SmuggledGot{}, false, "ValStr must not be empty"
}
return td.SmuggledGot{
Name: "ValStr",
Got: s.ValStr,
}, true, ""
},
td.Contains("oob")))
checkOK(t,
gotStruct,
td.Smuggle(
func(s MyStruct) (*td.SmuggledGot, bool, string) {
if s.ValStr == "" {
return nil, false, "ValStr must not be empty"
}
return &td.SmuggledGot{
Name: "ValStr",
Got: s.ValStr,
}, true, ""
},
td.Contains("oob")))
//
// Convertible types
checkOK(t, 123,
td.Smuggle(func(n float64) int { return int(n) }, 123))
type xInt int
checkOK(t, xInt(123),
td.Smuggle(func(n int) int64 { return int64(n) }, int64(123)))
checkOK(t, xInt(123),
td.Smuggle(func(n uint32) int64 { return int64(n) }, int64(123)))
checkOK(t, int32(123),
td.Smuggle(func(n int64) int { return int(n) }, 123))
checkOK(t, gotTime,
td.Smuggle(func(t fmt.Stringer) string { return t.String() },
"2018-05-23 12:13:14 +0000 UTC"))
checkOK(t, []byte("{}"),
td.Smuggle(
func(x json.RawMessage) json.RawMessage { return x },
td.JSON(`{}`)))
//
// bytes slice caster variations
checkOK(t, []byte(`{"foo":1}`),
td.Smuggle(json.RawMessage{}, td.JSON(`{"foo":1}`)))
checkOK(t, []byte(`{"foo":1}`),
td.Smuggle(json.RawMessage(nil), td.JSON(`{"foo":1}`)))
checkOK(t, []byte(`{"foo":1}`),
td.Smuggle(reflect.TypeOf(json.RawMessage(nil)), td.JSON(`{"foo":1}`)))
checkOK(t, `{"foo":1}`,
td.Smuggle(json.RawMessage{}, td.JSON(`{"foo":1}`)))
checkOK(t, newReArmReader([]byte(`{"foo":1}`)), // io.Reader first
td.Smuggle(json.RawMessage{}, td.JSON(`{"foo":1}`)))
checkError(t, nil,
td.Smuggle(json.RawMessage{}, td.JSON(`{}`)),
expectedError{
Message: mustBe("incompatible parameter type"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("json.RawMessage or convertible or io.Reader"),
})
checkError(t, MyStruct{},
td.Smuggle(json.RawMessage{}, td.JSON(`{}`)),
expectedError{
Message: mustBe("incompatible parameter type"),
Path: mustBe("DATA"),
Got: mustBe("td_test.MyStruct"),
Expected: mustBe("json.RawMessage or convertible or io.Reader"),
})
checkError(t, errReader{}, // erroneous io.Reader
td.Smuggle(json.RawMessage{}, td.JSON(`{}`)),
expectedError{
Message: mustBe("an error occurred while reading from io.Reader"),
Path: mustBe("DATA"),
Summary: mustBe("an error occurred"),
})
//
// strings caster variations
type myString string
checkOK(t, `pipo bingo`,
td.Smuggle("", td.HasSuffix("bingo")))
checkOK(t, []byte(`pipo bingo`),
td.Smuggle(myString(""), td.HasSuffix("bingo")))
checkOK(t, []byte(`pipo bingo`),
td.Smuggle(reflect.TypeOf(myString("")), td.HasSuffix("bingo")))
checkOK(t, newReArmReader([]byte(`pipo bingo`)), // io.Reader first
td.Smuggle(myString(""), td.HasSuffix("bingo")))
checkError(t, nil,
td.Smuggle("", "bingo"),
expectedError{
Message: mustBe("incompatible parameter type"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("string or convertible or io.Reader"),
})
checkError(t, MyStruct{},
td.Smuggle(myString(""), "bingo"),
expectedError{
Message: mustBe("incompatible parameter type"),
Path: mustBe("DATA"),
Got: mustBe("td_test.MyStruct"),
Expected: mustBe("td_test.myString or convertible or io.Reader"),
})
checkError(t, errReader{}, // erroneous io.Reader
td.Smuggle("", "bingo"),
expectedError{
Message: mustBe("an error occurred while reading from io.Reader"),
Path: mustBe("DATA"),
Summary: mustBe("an error occurred"),
})
//
// Any other caster variations
checkOK(t, `pipo bingo`,
td.Smuggle([]rune{}, td.Contains([]rune(`bing`))))
checkOK(t, `pipo bingo`,
td.Smuggle(([]rune)(nil), td.Contains([]rune(`bing`))))
checkOK(t, `pipo bingo`,
td.Smuggle(reflect.TypeOf([]rune{}), td.Contains([]rune(`bing`))))
checkOK(t, 123.456, td.Smuggle(int64(0), int64(123)))
checkOK(t, 123.456, td.Smuggle(reflect.TypeOf(int64(0)), int64(123)))
//
// Errors
checkError(t, "123",
td.Smuggle(func(n float64) int { return int(n) }, 123),
expectedError{
Message: mustBe("incompatible parameter type"),
Path: mustBe("DATA"),
Got: mustBe("string"),
Expected: mustBe("float64"),
})
checkError(t, nil,
td.Smuggle(func(n int64) int { return int(n) }, 123),
expectedError{
Message: mustBe("incompatible parameter type"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("int64"),
})
checkError(t, 12,
td.Smuggle(func(n int) (int, bool) { return n, false }, 12),
expectedError{
Message: mustBe("ran smuggle code with %% as argument"),
Path: mustBe("DATA"),
Summary: mustBe(" value: 12\nit failed but didn't say why"),
})
type MyBool bool
type MyString string
checkError(t, 12,
td.Smuggle(func(n int) (int, MyBool, MyString) {
return n, false, "very custom error"
}, 12),
expectedError{
Message: mustBe("ran smuggle code with %% as argument"),
Path: mustBe("DATA"),
Summary: mustBe(" value: 12\nit failed coz: very custom error"),
})
checkError(t, 12,
td.Smuggle(func(n int) (int, error) {
return n, errors.New("very custom error")
}, 12),
expectedError{
Message: mustBe("ran smuggle code with %% as argument"),
Path: mustBe("DATA"),
Summary: mustBe(" value: 12\nit failed coz: very custom error"),
})
checkError(t, 12,
td.Smuggle(func(n int) *td.SmuggledGot { return nil }, int64(13)),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("(int64) 13"),
})
// Internal use
checkError(t, 12,
td.Smuggle(func(n int) (int, error) {
return n, &ctxerr.Error{
Message: "my message",
Summary: ctxerr.NewSummary("my summary"),
}
}, 13),
expectedError{
Message: mustBe("my message"),
Path: mustBe("DATA"),
Summary: mustBe("my summary"),
})
//
// Errors behind Smuggle()
checkError(t, 12,
td.Smuggle(func(n int) int64 { return int64(n) }, int64(13)),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("(int64) 12"),
Expected: mustBe("(int64) 13"),
})
checkError(t, gotStruct,
td.Smuggle("MyStructMid.MyStructBase.ValBool", false),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.MyStructMid.MyStructBase.ValBool"),
Got: mustBe("true"),
Expected: mustBe("false"),
})
checkError(t, 12,
td.Smuggle(func(n int) td.SmuggledGot {
return td.SmuggledGot{
// With Name = ""
Got: int64(n),
}
}, int64(13)),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("(int64) 12"),
Expected: mustBe("(int64) 13"),
})
checkError(t, 12,
td.Smuggle(func(n int) *td.SmuggledGot {
return &td.SmuggledGot{
Name: "",
Got: int64(n),
}
}, int64(13)),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"), // no dot added between DATA and
Got: mustBe("(int64) 12"),
Expected: mustBe("(int64) 13"),
})
checkError(t, 12,
td.Smuggle(func(n int) *td.SmuggledGot {
return &td.SmuggledGot{
Name: "Int64",
Got: int64(n),
}
}, int64(13)),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.Int64"), // dot added between DATA and Int64
Got: mustBe("(int64) 12"),
Expected: mustBe("(int64) 13"),
})
//
// Bad usage
const usage = "Smuggle(FUNC|FIELDS_PATH|ANY_TYPE, TESTDEEP_OPERATOR|EXPECTED_VALUE): "
checkError(t, "never tested",
td.Smuggle(nil, 12),
expectedError{
Message: mustBe("bad usage of Smuggle operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: " + usage[:len(usage)-2] + ", ANY_TYPE cannot be nil nor Interface"),
})
checkError(t, nil,
td.Smuggle(reflect.TypeOf((*fmt.Stringer)(nil)).Elem(), 1234),
expectedError{
Message: mustBe("bad usage of Smuggle operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: " + usage[:len(usage)-2] + ", ANY_TYPE reflect.Type cannot be Func nor Interface"),
})
checkError(t, nil,
td.Smuggle(reflect.TypeOf(func() {}), 1234),
expectedError{
Message: mustBe("bad usage of Smuggle operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: " + usage[:len(usage)-2] + ", ANY_TYPE reflect.Type cannot be Func nor Interface"),
})
checkError(t, "never tested",
td.Smuggle((func(string) int)(nil), 12),
expectedError{
Message: mustBe("bad usage of Smuggle operator"),
Path: mustBe("DATA"),
Summary: mustBe("Smuggle(FUNC): FUNC cannot be a nil function"),
})
checkError(t, "never tested",
td.Smuggle("bad[path", 12),
expectedError{
Message: mustBe("bad usage of Smuggle operator"),
Path: mustBe("DATA"),
Summary: mustBe(usage + `cannot find final ']' in FIELDS_PATH "bad[path"`),
})
// Bad number of args
checkError(t, "never tested",
td.Smuggle(func() int { return 0 }, 12),
expectedError{
Message: mustBe("bad usage of Smuggle operator"),
Path: mustBe("DATA"),
Summary: mustBe(usage + "FUNC must take only one non-variadic argument"),
})
checkError(t, "never tested",
td.Smuggle(func(x ...int) int { return 0 }, 12),
expectedError{
Message: mustBe("bad usage of Smuggle operator"),
Path: mustBe("DATA"),
Summary: mustBe(usage + "FUNC must take only one non-variadic argument"),
})
checkError(t, "never tested",
td.Smuggle(func(a int, b string) int { return 0 }, 12),
expectedError{
Message: mustBe("bad usage of Smuggle operator"),
Path: mustBe("DATA"),
Summary: mustBe(usage + "FUNC must take only one non-variadic argument"),
})
// Bad number of returned values
const errMesg = usage + "FUNC must return value or (value, bool) or (value, bool, string) or (value, error)"
checkError(t, "never tested",
td.Smuggle(func(a int) {}, 12),
expectedError{
Message: mustBe("bad usage of Smuggle operator"),
Path: mustBe("DATA"),
Summary: mustBe(errMesg),
})
checkError(t, "never tested",
td.Smuggle(
func(a int) (int, bool, string, int) { return 0, false, "", 23 },
12),
expectedError{
Message: mustBe("bad usage of Smuggle operator"),
Path: mustBe("DATA"),
Summary: mustBe(errMesg),
})
// Bad returned types
checkError(t, "never tested",
td.Smuggle(func(a int) (int, int) { return 0, 0 }, 12),
expectedError{
Message: mustBe("bad usage of Smuggle operator"),
Path: mustBe("DATA"),
Summary: mustBe(errMesg),
})
checkError(t, "never tested",
td.Smuggle(func(a int) (int, bool, int) { return 0, false, 23 }, 12),
expectedError{
Message: mustBe("bad usage of Smuggle operator"),
Path: mustBe("DATA"),
Summary: mustBe(errMesg),
})
checkError(t, "never tested",
td.Smuggle(func(a int) (int, error, string) { return 0, nil, "" }, 12), //nolint: staticcheck
expectedError{
Message: mustBe("bad usage of Smuggle operator"),
Path: mustBe("DATA"),
Summary: mustBe(errMesg),
})
//
// String
test.EqualStr(t,
td.Smuggle(func(n int) int { return 0 }, 12).String(),
"Smuggle(func(int) int, 12)")
test.EqualStr(t,
td.Smuggle(func(n int) (int, bool) { return 23, false }, 12).String(),
"Smuggle(func(int) (int, bool), 12)")
test.EqualStr(t,
td.Smuggle(func(n int) (int, error) { return 23, nil }, 12).String(),
"Smuggle(func(int) (int, error), 12)")
test.EqualStr(t,
td.Smuggle(func(n int) (int, MyBool, MyString) { return 23, false, "" }, 12).
String(),
"Smuggle(func(int) (int, td_test.MyBool, td_test.MyString), 12)")
test.EqualStr(t,
td.Smuggle(reflect.TypeOf(42), 23).String(),
"Smuggle(type:int, 23)")
test.EqualStr(t,
td.Smuggle(666, 23).String(),
"Smuggle(type:int, 23)")
test.EqualStr(t,
td.Smuggle("", 23).String(),
"Smuggle(type:string, 23)")
test.EqualStr(t,
td.Smuggle("name", "bob").String(),
`Smuggle("name", "bob")`)
// Erroneous op
test.EqualStr(t,
td.Smuggle((func(int) int)(nil), 12).String(),
"Smuggle()")
}
func TestSmuggleFieldsPath(t *testing.T) {
num := 42
gotStruct := MyStruct{
MyStructMid: MyStructMid{
MyStructBase: MyStructBase{
ValBool: true,
},
ValStr: "foobar",
},
ValInt: 123,
Ptr: &num,
}
type A struct {
Num int
Str string
}
type C struct {
A A
PA1 *A
PA2 *A
Iface1 any
Iface2 any
Iface3 any
Iface4 any
}
type B struct {
A A
PA *A
PppA ***A
Iface any
Iface2 any
Iface3 any
C *C
}
pa := &A{Num: 3, Str: "three"}
ppa := &pa
b := B{
A: A{Num: 1, Str: "one"},
PA: &A{Num: 2, Str: "two"},
PppA: &ppa,
Iface: A{Num: 4, Str: "four"},
Iface2: &ppa,
Iface3: nil,
C: &C{
A: A{Num: 5, Str: "five"},
PA1: &A{Num: 6, Str: "six"},
PA2: nil, // explicit to be clear
Iface1: A{Num: 7, Str: "seven"},
Iface2: &A{Num: 8, Str: "eight"},
Iface3: nil, // explicit to be clear
Iface4: (*A)(nil),
},
}
//
// OK
checkOK(t, gotStruct, td.Smuggle("ValInt", 123))
checkOK(t, gotStruct,
td.Smuggle("MyStructMid.ValStr", td.Contains("oob")))
checkOK(t, gotStruct,
td.Smuggle("MyStructMid.MyStructBase.ValBool", true))
checkOK(t, gotStruct, td.Smuggle("ValBool", true)) // thanks to composition
checkOK(t, gotStruct, td.Smuggle("Ptr", td.Ptr(42)))
// OK across pointers
checkOK(t, b, td.Smuggle("PA.Num", 2))
checkOK(t, b, td.Smuggle("PppA.Num", 3))
// OK with any
checkOK(t, b, td.Smuggle("Iface.Num", 4))
checkOK(t, b, td.Smuggle("Iface2.Num", 3))
checkOK(t, b, td.Smuggle("C.Iface1.Num", 7))
checkOK(t, b, td.Smuggle("C.Iface2.Num", 8))
// Errors
checkError(t, 12, td.Smuggle("foo.bar", 23),
expectedError{
Message: mustBe("ran smuggle code with %% as argument"),
Path: mustBe("DATA"),
Summary: mustBe(" value: 12\nit failed coz: it is a int and should be a struct or a map[string]…"),
})
checkError(t, gotStruct, td.Smuggle("ValInt.bar", 23),
expectedError{
Message: mustBe("ran smuggle code with %% as argument"),
Path: mustBe("DATA"),
Summary: mustContain("\nit failed coz: field \"ValInt\" is a int and should be a struct"),
})
checkError(t, gotStruct, td.Smuggle("MyStructMid.ValStr.foobar", 23),
expectedError{
Message: mustBe("ran smuggle code with %% as argument"),
Path: mustBe("DATA"),
Summary: mustContain("\nit failed coz: field \"MyStructMid.ValStr\" is a string and should be a struct"),
})
checkError(t, gotStruct, td.Smuggle("foo.bar", 23),
expectedError{
Message: mustBe("ran smuggle code with %% as argument"),
Path: mustBe("DATA"),
Summary: mustContain("\nit failed coz: field \"foo\" not found"),
})
checkError(t, b, td.Smuggle("C.PA2.Num", 456),
expectedError{
Message: mustBe("ran smuggle code with %% as argument"),
Path: mustBe("DATA"),
Summary: mustContain("\nit failed coz: field \"C.PA2\" is nil"),
})
checkError(t, b, td.Smuggle("C.Iface3.Num", 456),
expectedError{
Message: mustBe("ran smuggle code with %% as argument"),
Path: mustBe("DATA"),
Summary: mustContain("\nit failed coz: field \"C.Iface3\" is nil"),
})
checkError(t, b, td.Smuggle("C.Iface4.Num", 456),
expectedError{
Message: mustBe("ran smuggle code with %% as argument"),
Path: mustBe("DATA"),
Summary: mustContain("\nit failed coz: field \"C.Iface4\" is nil"),
})
checkError(t, b, td.Smuggle("Iface3.Num", 456),
expectedError{
Message: mustBe("ran smuggle code with %% as argument"),
Path: mustBe("DATA"),
Summary: mustContain("\nit failed coz: field \"Iface3\" is nil"),
})
// Referencing maps and array/slices
x := B{
Iface: map[string]any{
"test": []int{2, 3, 4},
},
C: &C{
Iface1: []any{
map[int]any{42: []string{"pipo"}, 66: [2]string{"foo", "bar"}},
map[int8]any{42: []string{"pipo"}},
map[int16]any{42: []string{"pipo"}},
map[int32]any{42: []string{"pipo"}},
map[int64]any{42: []string{"pipo"}},
map[uint]any{42: []string{"pipo"}},
map[uint8]any{42: []string{"pipo"}},
map[uint16]any{42: []string{"pipo"}},
map[uint32]any{42: []string{"pipo"}},
map[uint64]any{42: []string{"pipo"}},
map[uintptr]any{42: []string{"pipo"}},
map[float32]any{42: []string{"pipo"}},
map[float64]any{42: []string{"pipo"}},
},
},
}
checkOK(t, x, td.Smuggle("Iface[test][1]", 3))
checkOK(t, x, td.Smuggle("Iface.test[1]", 3)) // shortcut for map[string]…
checkOK(t, x, td.Smuggle("C.Iface1[0][66][1]", "bar"))
for i := 0; i < 12; i++ {
checkOK(t, x,
td.Smuggle(fmt.Sprintf("C.Iface1[%d][42][0]", i), "pipo"))
checkOK(t, x,
td.Smuggle(fmt.Sprintf("C.Iface1[%d][42][-1]", i-12), "pipo"))
}
checkOK(t, x, td.Lax(td.Smuggle("PppA", nil)))
checkOK(t, x, td.Smuggle("PppA", td.Nil()))
checkOK(t, x.Iface, td.Smuggle("[test][1]", 3))
checkError(t, x.Iface, td.Smuggle("[unknown][1]", 42),
expectedError{
Message: mustBe("ran smuggle code with %% as argument"),
Path: mustBe("DATA"),
Summary: mustContain(`
it failed coz: field "[unknown]", "unknown" map key not found`),
})
checkOK(t, x.C.Iface1, td.Smuggle("[0][66][1]", "bar"))
checkError(t, x.C.Iface1, td.Smuggle("[42][66][1]", "bar"),
expectedError{
Message: mustBe("ran smuggle code with %% as argument"),
Path: mustBe("DATA"),
Summary: mustContain(`
it failed coz: field "[42]", 42 is out of slice/array range (len 13)`),
})
//
type D struct {
Iface any
}
got := D{
Iface: []any{
map[complex64]any{complex(42, 0): []string{"pipo"}},
map[complex128]any{complex(42, 0): []string{"pipo"}},
},
}
for i := 0; i < 2; i++ {
checkOK(t, got, td.Smuggle(fmt.Sprintf("Iface[%d][42][0]", i), "pipo"))
checkOK(t, got, td.Smuggle(fmt.Sprintf("Iface[%d][42][0]", i-2), "pipo"))
}
}
func TestSmuggleFieldsPathMethod(t *testing.T) {
pipo := td.SmuggleBuild{Field: struct{ Path string }{"pipo"}}
checkOK(t, td.SmuggleBuild{Next: &pipo},
td.Smuggle(`FollowNext().Field.Path`, "pipo"))
checkOK(t, &td.SmuggleBuild{Next: &pipo},
td.Smuggle(`FollowNext().Field.Path`, "pipo"))
checkOK(t, &td.SmuggleBuild{Next: &pipo},
td.Smuggle(`PtrFollowNext().Field.Path`, "pipo"))
checkOK(t, td.SmuggleBuild{Iface: pipo},
td.Smuggle(`FollowIface().Field.Path`, "pipo"))
checkOK(t, &td.SmuggleBuild{Iface: pipo},
td.Smuggle(`FollowIface().Field.Path`, "pipo"))
checkOK(t, &td.SmuggleBuild{Iface: &pipo},
td.Smuggle(`PtrFollowIface().Field.Path`, "pipo"))
checkOK(t, td.SmuggleBuild{Iface: pipo},
td.Smuggle(`MayFollowIface().Field.Path`, "pipo"))
// Method call on typed nil is OK as PNum() is a method on *td.SmuggleBuild
checkOK(t, td.SmuggleBuild{}, td.Smuggle(`Next.PNum()`, 42))
checkOK(t, td.SmuggleBuild{Iface: (*td.SmuggleBuild)(nil)},
td.Smuggle(`Iface.PNum()`, 42))
// Method call on typed nil is KO as Num() is a method on td.SmuggleBuild
checkError(t, td.SmuggleBuild{}, td.Smuggle(`Next.Num()`, 42),
expectedError{
Message: mustBe("ran smuggle code with %% as argument"),
Path: mustBe("DATA"),
Summary: mustMatch(`
it failed coz: method Next.Num\(\) panicked: value method .+/td.SmuggleBuild.Num called using nil \*SmuggleBuild pointer`),
})
checkError(t, td.SmuggleBuild{Next: &pipo},
td.Smuggle(`Next.Panic()`, "dummy"),
expectedError{
Message: mustBe("ran smuggle code with %% as argument"),
Path: mustBe("DATA"),
Summary: mustContain(`
it failed coz: method Next.Panic() panicked: oops!`),
})
checkError(t, td.SmuggleBuild{Next: &td.SmuggleBuild{}},
td.Smuggle(`Next.MayFollowNext().Field.Path`, "pipo"),
expectedError{
Message: mustBe("ran smuggle code with %% as argument"),
Path: mustBe("DATA"),
Summary: mustContain(`
it failed coz: method Next.MayFollowNext() returned an error: Next is nil`),
})
checkError(t, td.SmuggleBuild{Next: &td.SmuggleBuild{}},
td.Smuggle(`Next.MayFollowIface().Field.Path`, "pipo"),
expectedError{
Message: mustBe("ran smuggle code with %% as argument"),
Path: mustBe("DATA"),
Summary: mustContain(`
it failed coz: method Next.MayFollowIface() returned an error: Iface is nil`),
})
// Test with reflect
checkOK(t, reflect.ValueOf(pipo),
td.Smuggle(`Interface().Field.Path`, "pipo"))
checkOK(t, reflect.ValueOf(reflect.ValueOf(&pipo)),
td.Smuggle(`Interface().Elem().Interface().Field.Path`, "pipo"))
}
func TestSmuggleTypeBehind(t *testing.T) {
// Type behind is the smuggle function parameter one
equalTypes(t, td.Smuggle(func(n int) bool { return n != 0 }, true), 23)
type MyTime time.Time
equalTypes(t,
td.Smuggle(
func(t MyTime) time.Time { return time.Time(t) },
time.Now()),
MyTime{})
equalTypes(t,
td.Smuggle(func(from any) any { return from }, nil),
reflect.TypeOf((*any)(nil)).Elem())
equalTypes(t,
td.Smuggle("foo.bar", nil),
reflect.TypeOf((*any)(nil)).Elem())
// Erroneous op
equalTypes(t, td.Smuggle((func(int) int)(nil), 12), nil)
}
go-testdeep-1.15.0/td/td_smuggler_base.go 0000664 0000000 0000000 00000005304 15144170453 0020270 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (
"encoding/json"
"fmt"
"reflect"
"github.com/maxatome/go-testdeep/internal/ctxerr"
)
// tdSmugglerBase is the base class of all smuggler TestDeep operators.
type tdSmugglerBase struct {
base
expectedValue reflect.Value
isTestDeeper bool
}
func newSmugglerBase(val any, depth ...int) (ret tdSmugglerBase) {
callDepth := 4
if len(depth) > 0 {
callDepth += depth[0]
}
ret.base = newBase(callDepth)
// Initializes only if TestDeep operator. Other cases are specific.
if _, ok := val.(TestDeep); ok {
ret.expectedValue = reflect.ValueOf(val)
ret.isTestDeeper = true
}
return
}
// internalTypeBehind returns the type behind expectedValue or nil if
// it cannot be determined.
func (s *tdSmugglerBase) internalTypeBehind() reflect.Type {
if s.isTestDeeper {
return s.expectedValue.Interface().(TestDeep).TypeBehind()
}
if s.expectedValue.IsValid() {
return s.expectedValue.Type()
}
return nil
}
// jsonValueEqual compares "got" to expectedValue, trying to do it
// using a JSON point of view. It is the caller responsibility to
// ensure that "got" value is either a bool, float64, string,
// []any, a map[string]any or simply nil.
//
// If the type behind expectedValue can be determined and is different
// from "got" type, "got" value is JSON marshaled, then unmarshaled
// in a new value of this type. This new value is then compared to
// expectedValue.
//
// Otherwise, "got" value is compared as-is to expectedValue.
func (s *tdSmugglerBase) jsonValueEqual(ctx ctxerr.Context, got any) *ctxerr.Error {
expectedType := s.internalTypeBehind()
// Unknown expected type (operator with nil TypeBehind() result or
// untyped nil), lets deepValueEqual() handles the comparison using
// BeLax flag
if expectedType == nil {
return deepValueEqual(ctx, reflect.ValueOf(got), s.expectedValue)
}
// Same type for got & expected type, no need to Marshal/Unmarshal
if got != nil && expectedType == reflect.TypeOf(got) {
return deepValueEqual(ctx, reflect.ValueOf(got), s.expectedValue)
}
// Unmarshal got into the expectedType
b, _ := json.Marshal(got) // No error can occur here
finalGot := reflect.New(expectedType)
if err := json.Unmarshal(b, finalGot.Interface()); err != nil {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: fmt.Sprintf(
"an error occurred while unmarshalling JSON into %s", expectedType),
Summary: ctxerr.NewSummary(err.Error()),
})
}
return deepValueEqual(ctx, finalGot.Elem(), s.expectedValue)
}
go-testdeep-1.15.0/td/td_sort.go 0000664 0000000 0000000 00000032262 15144170453 0016443 0 ustar 00root root 0000000 0000000 // Copyright (c) 2024-2025, 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 {
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("string... expected " + 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 []any: // fields-paths list in JSON context
ss := make([]string, len(v))
var ok bool
for i, s := range v {
ss[i], ok = s.(string)
if !ok {
return fmt.Errorf(
"slice of strings expected as how, %T encountered at pos %d", s, i)
}
}
sb.mkSortFn = func(typ reflect.Type) (reflect.Value, error) {
return mkSortFieldsPaths(typ, ss)
}
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
how any
}
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. It can be seen as an alternative to
// [Bag].
//
// how can be:
// - nil or a float64/int >= 0 for a generic ascending order;
// - a float64/int < 0 for a generic descending order;
// - a string specifying a fields-path (optionally prefixed by "+"
// or "-" for respectively an ascending or a descending order,
// defaulting to ascending 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 before b.
//
// A fields-path, also used by [Smuggle] and [Sorted] operators,
// allows to access nested structs fields and maps & slices items. See
// [Smuggle] for details on fields-path possibilities.
//
// type A struct{ props map[string]int }
// p12 := A{props: map[string]int{"priority": 12}}
// p23 := A{props: map[string]int{"priority": 23}}
// p34 := A{props: map[string]int{"priority": 34}}
// got := []A{p23, p12, p34}
// td.Cmp(t, got, td.Sort("-props[priority]", []A{p34, p23, p12})) // succeeds
//
// how can be a float64 to allow Sort to be used in expected JSON of
// [JSON], [SubJSONOf] & [SuperJSONOf] operators:
//
// got := map[string][]string{"labels": {"c", "a", "b"}}
// td.Cmp(t, got, td.JSON(`{ "labels": Sort(1, ["a", "b", "c"]) }`)) // succeeds
//
// or using fields-path feature:
//
// type Person struct {
// Name string `json:"name"`
// Age int `json:"age"`
// }
// got := struct {
// People []Person `json:"people"`
// }{
// People: []Person{
// {"Brian", 22},
// {"Bob", 19},
// {"Stephen", 19},
// {"Alice", 20},
// {"Marcel", 25},
// },
// }
// td.Cmp(t, got, td.JSON(`{
// "people": Sort("name", [ // sort by name ascending
// {"name": "Alice", "age": 20},
// {"name": "Bob", "age": 19},
// {"name": "Brian", "age": 22},
// {"name": "Marcel", "age": 25},
// {"name": "Stephen", "age": 19},
// ])
// }`)) // succeeds
// td.Cmp(t, got, td.JSON(`{
// "people": Sort([ "-age", "name" ], [ // sort by age desc, then by name asc
// {"name": "Marcel", "age": 25},
// {"name": "Brian", "age": 22},
// {"name": "Alice", "age": 20},
// {"name": "Bob", "age": 19},
// {"name": "Stephen", "age": 19},
// ])
// }`)) // succeeds
//
// See also [Sorted], [Smuggle] and [Bag].
func Sort(how any, expectedValue any) TestDeep {
s := tdSort{how: how}
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 ctx.CollectError(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: "cannot sort items",
Summary: ctxerr.NewSummary(err.Error()),
})
}
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 != nil && typ.Kind() == reflect.Func {
how = typ.String()
}
return S("Sort(%v, %s)", how, util.ToString(s.expectedValue))
}
type tdSorted struct {
baseOKNil
tdSortBase
how []any
}
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 well sorted as how tells it should be.
//
// how... can be:
// - empty to check a generic ascending order;
// - nil or a float64/int >= 0 to check a generic ascending order;
// - a float64/int < 0 to check a generic descending order;
// - strings specifying fields-paths (each optionally prefixed by "+"
// or "-" for respectively checking an ascending or a descending order,
// defaulting to ascending one);
// - a function matching func(a, b T) bool signature and returning
// true if a is before b.
//
// A fields-path, also used by [Smuggle] and [Sort] operators,
// allows to access nested structs fields and maps & slices items. See
// [Smuggle] for details on fields-path possibilities.
//
// type A struct{ props map[string]int }
// p12 := A{props: map[string]int{"priority": 12}}
// p23 := A{props: map[string]int{"priority": 23}}
// p34 := A{props: map[string]int{"priority": 34}}
// got := []A{p34, p23, p12}
// td.Cmp(t, got, td.Sorted("-props[priority]")) // succeeds
//
// how can be a float64 to allow Sort to be used in expected JSON of
// [JSON], [SubJSONOf] & [SuperJSONOf] operators:
//
// got := map[string][]string{"labels": {"a", "b", "c"}}
// td.Cmp(t, got, td.JSON(`{ "labels": Sorted }`)) // succeeds
//
// or using fields-path feature:
//
// type Person struct {
// Name string `json:"name"`
// Age int `json:"age"`
// }
// got := struct {
// People []Person `json:"people"`
// }{
// People: []Person{
// {"Marcel", 25},
// {"Brian", 22},
// {"Alice", 20},
// {"Bob", 19},
// {"Stephen", 19},
// },
// }
// // sorted by age desc, then by name asc
// td.Cmp(t, got, td.JSON(`{ "people": Sorted("-age", "name") }`)) // succeeds
//
// See also [Sort].
func Sorted(how ...any) TestDeep {
s := tdSorted{
baseOKNil: newBaseOKNil(3),
how: how,
}
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 ctx.CollectError(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: "cannot sort items",
Summary: ctxerr.NewSummary(err.Error()),
})
}
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: fmt.Sprintf("not sorted, item #%d value is before #%d one while it should not", i, i-1),
Summary: ctxerr.ErrorSummaryItems{
{
Label: fmt.Sprintf("item #%d", i-1),
Value: util.ToString(got.Index(i - 1)),
},
{
Label: fmt.Sprintf("item #%d", i),
Value: util.ToString(got.Index(i)),
},
},
})
}
}
return nil
}
func (s *tdSorted) String() string {
if s.err != nil {
return s.stringError()
}
var b strings.Builder
b.WriteString("Sorted(")
for i, cur := range s.how {
how, typ := cur, reflect.TypeOf(cur)
if typ != nil && typ.Kind() == reflect.Func {
how = typ.String()
}
if i > 0 {
b.WriteString(", ")
}
fmt.Fprintf(&b, "%v", how)
}
b.WriteByte(')')
return b.String()
}
go-testdeep-1.15.0/td/td_sort_private_test.go 0000664 0000000 0000000 00000015746 15144170453 0021244 0 ustar 00root root 0000000 0000000 // Copyright (c) 2024-2025, 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 //nolint: prealloc
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: "not a string",
how: []any{"ok", false},
expectedErr: "string... expected but received bool as 2nd parameter",
},
{
name: "any slice as used in JSON",
how: []any{[]any{"zzz", true}},
expectedErr: "slice of strings expected as how, bool encountered at pos 1",
},
{
name: "unknown how",
how: []any{true},
expectedErr: "but received bool as 1st parameter",
},
{
name: "bad func variadic",
how: []any{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: []any{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: []any{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: []any{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: []any{func(a, b int) {}},
expectedErr: "SORT_FUNC must match func(T, T) bool signature, not func(int, int)",
},
{
name: "bad func num out2",
how: []any{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: []any{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")
}
})
})
}
go-testdeep-1.15.0/td/td_sort_test.go 0000664 0000000 0000000 00000027430 15144170453 0017503 0 ustar 00root root 0000000 0000000 // Copyright (c) 2024-2025, 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/internal/test"
"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
expectedErr expectedError
}{
{
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: "asc float",
how: 42.3,
got: []int{1, -2, -3, 0, -1, 3, 2},
expected: []int{-3, -2, -1, 0, 1, 2, 3},
},
{
name: "no items",
how: 1,
got: []int{},
expected: []int{},
},
{
name: "one item",
how: 1,
got: []int{23},
expected: []int{23},
},
{
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: "evenHigher",
how: func(a, b int) bool {
if (a%2 == 0) != (b%2 == 0) {
return a%2 != 0
}
return a < b
},
got: []int{-1, 1, 2, -3, 3, -2, 0},
expected: []int{-3, -1, 1, 3, -2, 0, 2},
},
{
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}},
},
{
name: "invalid fields-path",
how: "",
got: []int{1, 2},
expected: []int{42, 42},
expectedErr: expectedError{
Message: mustBe("cannot sort items"),
Path: mustBe("DATA"),
Summary: mustBe("FIELDS_PATH cannot be empty"),
Under: mustContain("under operator Sort at "),
},
},
{
name: "bad usage",
how: 1,
got: []int{1, 2},
expected: 42,
expectedErr: expectedError{
Message: mustBe("bad usage of Sort operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Sort(SORT_FUNC|int|string|[]string, TESTDEEP_OPERATOR|EXPECTED_VALUE), EXPECTED_VALUE must be a slice or an array not a int"),
Under: mustContain("under operator Sort at "),
},
},
{
name: "grepResolvePtr error",
got: (*int)(nil),
expected: td.Ignore(),
expectedErr: expectedError{
Message: mustBe("nil pointer"),
Got: mustBe("nil *int"),
Expected: mustBe("non-nil *slice OR *array"),
Under: mustContain("under operator Sort at "),
},
},
{
name: "grepBadKind",
got: 123,
expected: td.Ignore(),
expectedErr: expectedError{
Message: mustBe("bad kind"),
Got: mustBe("int"),
Expected: mustBe("slice OR array OR *slice OR *array"),
Under: mustContain("under operator Sort at "),
},
},
{
name: "erroneous operator expected",
how: 1,
got: []int{1, 2},
expected: td.JSON("{"),
expectedErr: expectedError{
Message: mustBe("bad usage of JSON operator"),
Path: mustMatch(`DATA`), // always without .Iface
Summary: mustContain("JSON unmarshal error"),
Under: mustContain("under operator JSON at "),
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.expectedErr == (expectedError{}) {
checkOK(t, tc.got, td.Sort(tc.how, tc.expected))
} else {
checkError(t, tc.got, td.Sort(tc.how, tc.expected), tc.expectedErr)
}
})
}
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"])}`))
checkOK(t,
map[string][]string{"labels": {"c", "a", "b"}},
td.JSON(`{"labels": Sort(1, ["a", "b", "c"])}`))
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
got := struct {
People []Person `json:"people"`
}{
People: []Person{
{"Brian", 22},
{"Bob", 19},
{"Stephen", 19},
{"Alice", 20},
{"Marcel", 25},
},
}
checkOK(t, got, td.JSON(`{
"people": Sort("name", [ // sort by name ascending
{"name": "Alice", "age": 20},
{"name": "Bob", "age": 19},
{"name": "Brian", "age": 22},
{"name": "Marcel", "age": 25},
{"name": "Stephen", "age": 19},
])
}`))
checkOK(t, got, td.JSON(`{
"people": Sort([ "-age", "name" ], [ // sort by age desc, then by name asc
{"name": "Marcel", "age": 25},
{"name": "Brian", "age": 22},
{"name": "Alice", "age": 20},
{"name": "Bob", "age": 19},
{"name": "Stephen", "age": 19},
])
}`))
})
}
func TestSortTypeBehind(t *testing.T) {
equalTypes(t, td.Sort(1, []int{}), nil)
// Erroneous op
equalTypes(t, td.Sort(func() {}, []int{}), nil)
}
func TestSortString(t *testing.T) {
test.EqualStr(t, td.Sort(nil, []int{}).String(), "Sort(, ([]int) {\n})")
test.EqualStr(t, td.Sort(1, []int{}).String(), "Sort(1, ([]int) {\n})")
test.EqualStr(t,
td.Sort(func(a, b int) bool { return true }, []int{}).String(),
"Sort(func(int, int) bool, ([]int) {\n})")
// Erroneous op
test.EqualStr(t, td.Sort(func() {}, []int{}).String(), "Sort()")
}
// nolint: unused
func TestSorted(t *testing.T) {
firstBecomesLast := 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
expectedErr expectedError
}{
{
name: "slice",
got: []int{0, 1, 2, 2},
},
{
name: "*slice",
got: &[]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"},
},
{
name: "invalid fields-path",
got: []int{1, 2},
sorted: []any{""},
expectedErr: expectedError{
Message: mustBe("cannot sort items"),
Path: mustBe("DATA"),
Summary: mustBe("FIELDS_PATH cannot be empty"),
Under: mustContain("under operator Sorted at "),
},
},
{
name: "bad usage",
got: []int{1, 2},
sorted: []any{42, 23},
expectedErr: expectedError{
Message: mustBe("bad usage of Sorted operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Sorted(SORT_FUNC|int|[]string|string...), string... expected but received int as 1st parameter"),
Under: mustContain("under operator Sorted at "),
},
},
{
name: "grepResolvePtr error",
got: (*int)(nil),
sorted: []any{1},
expectedErr: expectedError{
Message: mustBe("nil pointer"),
Got: mustBe("nil *int"),
Expected: mustBe("non-nil *slice OR *array"),
Under: mustContain("under operator Sorted at "),
},
},
{
name: "grepBadKind",
got: 123,
sorted: []any{1},
expectedErr: expectedError{
Message: mustBe("bad kind"),
Got: mustBe("int"),
Expected: mustBe("slice OR array OR *slice OR *array"),
Under: mustContain("under operator Sorted at "),
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.expectedErr == (expectedError{}) {
checkOK(t, tc.got, td.Sorted(tc.sorted...))
// expect an error after moving first item at last position
if got, l := firstBecomesLast(tc.got); got != nil {
checkError(t, got, td.Sorted(tc.sorted...),
expectedError{
Message: mustBe(
fmt.Sprintf("not sorted, item #%d value is before #%d one while it should not",
l-1, l-2)),
Path: mustBe("DATA"),
Summary: mustMatch(
fmt.Sprintf(`(?s)^item #%d: .+\nitem #%d: `, l-2, l-1)),
})
}
return
}
checkError(t, tc.got, td.Sorted(tc.sorted...), tc.expectedErr)
})
}
t.Run("JSON", func(t *testing.T) {
checkOK(t,
json.RawMessage(`["a","b","c"]`),
td.JSON(`Sorted`))
checkOK(t,
json.RawMessage(`{"x": ["c","b","a"]}`),
td.JSON(`{ "x": Sorted(-1) }`))
checkOK(t,
map[string][]string{"labels": {"a", "b", "c"}},
td.JSON(`{ "labels": Sorted }`))
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
got := struct {
People []Person `json:"people"`
}{
People: []Person{
{"Alice", 20},
{"Bob", 19},
{"Brian", 22},
{"Marcel", 25},
{"Stephen", 19},
},
}
// is sorted by name ascending
checkOK(t, got, td.JSON(`{ "people": Sorted("name") }`))
// is sorted by age desc, then by name asc
checkError(t, got, td.JSON(`{ "people": Sorted("-age", "name") }`),
expectedError{
Message: mustBe("not sorted, item #2 value is before #1 one while it should not"),
Path: mustBe(`DATA["people"]`),
Summary: mustMatch(`(?s)^item #1: .+"Bob".*\nitem #2: .+"Brian"`),
Under: mustContain("under operator Sorted at "),
})
})
}
func TestSortedTypeBehind(t *testing.T) {
equalTypes(t, td.Sorted(), nil)
equalTypes(t, td.Sorted(-1), nil)
// Erroneous op
equalTypes(t, td.Sorted(func() {}), nil)
}
func TestSortedString(t *testing.T) {
test.EqualStr(t, td.Sorted(nil).String(), "Sorted()")
test.EqualStr(t, td.Sorted(1).String(), "Sorted(1)")
test.EqualStr(t, td.Sorted("a.b", "-d.e").String(), `Sorted(a.b, -d.e)`)
test.EqualStr(t,
td.Sorted(func(a, b int) bool { return true }).String(),
"Sorted(func(int, int) bool)")
// Erroneous op
test.EqualStr(t, td.Sorted(func() {}).String(), "Sorted()")
}
go-testdeep-1.15.0/td/td_string.go 0000664 0000000 0000000 00000012475 15144170453 0016766 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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"
"strings"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/types"
"github.com/maxatome/go-testdeep/internal/util"
)
type tdStringBase struct {
base
expected string
}
func newStringBase(expected string) tdStringBase {
return tdStringBase{
base: newBase(4),
expected: expected,
}
}
func getString(ctx ctxerr.Context, got reflect.Value) (string, *ctxerr.Error) {
switch got.Kind() {
case reflect.String:
return got.String(), nil
case reflect.Slice:
if got.Type().Elem() == types.Uint8 {
return string(got.Bytes()), nil
}
fallthrough
default:
if got.CanInterface() {
switch iface := got.Interface().(type) {
case error:
return iface.Error(), nil
case fmt.Stringer:
return iface.String(), nil
}
}
}
if ctx.BooleanError {
return "", ctxerr.BooleanError
}
return "", &ctxerr.Error{
Message: "bad type",
Got: types.RawString(got.Type().String()),
Expected: types.RawString(
"string (convertible) OR []byte (convertible) OR fmt.Stringer OR error"),
}
}
type tdString struct {
tdStringBase
}
var _ TestDeep = &tdString{}
// summary(String): checks a string, []byte, error or fmt.Stringer
// interfaces string contents
// input(String): str,slice([]byte),if(✓ + fmt.Stringer/error)
// String operator allows to compare a string (or convertible), []byte
// (or convertible), error or [fmt.Stringer] interface (error interface
// is tested before [fmt.Stringer]).
//
// err := errors.New("error!")
// td.Cmp(t, err, td.String("error!")) // succeeds
//
// bstr := bytes.NewBufferString("fmt.Stringer!")
// td.Cmp(t, bstr, td.String("fmt.Stringer!")) // succeeds
//
// See also [Contains], [HasPrefix], [HasSuffix], [Re] and [ReAll].
func String(expected string) TestDeep {
return &tdString{
tdStringBase: newStringBase(expected),
}
}
func (s *tdString) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
str, err := getString(ctx, got)
if err != nil {
return ctx.CollectError(err)
}
if str == s.expected {
return nil
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "does not match",
Got: str,
Expected: s,
})
}
func (s *tdString) String() string {
return util.ToString(s.expected)
}
type tdHasPrefix struct {
tdStringBase
}
var _ TestDeep = &tdHasPrefix{}
// summary(HasPrefix): checks the prefix of a string, []byte, error or
// fmt.Stringer interfaces
// input(HasPrefix): str,slice([]byte),if(✓ + fmt.Stringer/error)
// HasPrefix operator allows to compare the prefix of a string (or
// convertible), []byte (or convertible), error or [fmt.Stringer]
// interface (error interface is tested before [fmt.Stringer]).
//
// td.Cmp(t, []byte("foobar"), td.HasPrefix("foo")) // succeeds
//
// type Foobar string
// td.Cmp(t, Foobar("foobar"), td.HasPrefix("foo")) // succeeds
//
// err := errors.New("error!")
// td.Cmp(t, err, td.HasPrefix("err")) // succeeds
//
// bstr := bytes.NewBufferString("fmt.Stringer!")
// td.Cmp(t, bstr, td.HasPrefix("fmt")) // succeeds
//
// See also [Contains], [HasSuffix], [Re], [ReAll] and [String].
func HasPrefix(expected string) TestDeep {
return &tdHasPrefix{
tdStringBase: newStringBase(expected),
}
}
func (s *tdHasPrefix) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
str, err := getString(ctx, got)
if err != nil {
return ctx.CollectError(err)
}
if strings.HasPrefix(str, s.expected) {
return nil
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "has not prefix",
Got: str,
Expected: s,
})
}
func (s *tdHasPrefix) String() string {
return "HasPrefix(" + util.ToString(s.expected) + ")"
}
type tdHasSuffix struct {
tdStringBase
}
var _ TestDeep = &tdHasSuffix{}
// summary(HasSuffix): checks the suffix of a string, []byte, error or
// fmt.Stringer interfaces
// input(HasSuffix): str,slice([]byte),if(✓ + fmt.Stringer/error)
// HasSuffix operator allows to compare the suffix of a string (or
// convertible), []byte (or convertible), error or [fmt.Stringer]
// interface (error interface is tested before [fmt.Stringer]).
//
// td.Cmp(t, []byte("foobar"), td.HasSuffix("bar")) // succeeds
//
// type Foobar string
// td.Cmp(t, Foobar("foobar"), td.HasSuffix("bar")) // succeeds
//
// err := errors.New("error!")
// td.Cmp(t, err, td.HasSuffix("!")) // succeeds
//
// bstr := bytes.NewBufferString("fmt.Stringer!")
// td.Cmp(t, bstr, td.HasSuffix("!")) // succeeds
//
// See also [Contains], [HasPrefix], [Re], [ReAll] and [String].
func HasSuffix(expected string) TestDeep {
return &tdHasSuffix{
tdStringBase: newStringBase(expected),
}
}
func (s *tdHasSuffix) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
str, err := getString(ctx, got)
if err != nil {
return ctx.CollectError(err)
}
if strings.HasSuffix(str, s.expected) {
return nil
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "has not suffix",
Got: str,
Expected: s,
})
}
func (s *tdHasSuffix) String() string {
return "HasSuffix(" + util.ToString(s.expected) + ")"
}
go-testdeep-1.15.0/td/td_string_test.go 0000664 0000000 0000000 00000007575 15144170453 0020032 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (
"errors"
"testing"
"github.com/maxatome/go-testdeep/td"
)
func TestString(t *testing.T) {
checkOK(t, "foobar", td.String("foobar"))
checkOK(t, []byte("foobar"), td.String("foobar"))
type MyBytes []byte
checkOK(t, MyBytes("foobar"), td.String("foobar"))
type MyString string
checkOK(t, MyString("foobar"), td.String("foobar"))
// error interface
checkOK(t, errors.New("pipo bingo"), td.String("pipo bingo"))
// fmt.Stringer interface
checkOK(t, MyStringer{}, td.String("pipo bingo"))
checkError(t, "foo bar test", td.String("pipo"),
expectedError{
Message: mustBe("does not match"),
Path: mustBe("DATA"),
Got: mustContain(`"foo bar test"`),
Expected: mustContain(`"pipo"`),
})
checkError(t, []int{1, 2}, td.String("bar"),
expectedError{
Message: mustBe("bad type"),
Path: mustBe("DATA"),
Got: mustBe("[]int"),
Expected: mustBe("string (convertible) OR []byte (convertible) OR fmt.Stringer OR error"),
})
checkError(t, 12, td.String("bar"),
expectedError{
Message: mustBe("bad type"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("string (convertible) OR []byte (convertible) OR fmt.Stringer OR error"),
})
}
func TestHasPrefix(t *testing.T) {
checkOK(t, "foobar", td.HasPrefix("foo"))
checkOK(t, []byte("foobar"), td.HasPrefix("foo"))
type MyBytes []byte
checkOK(t, MyBytes("foobar"), td.HasPrefix("foo"))
type MyString string
checkOK(t, MyString("foobar"), td.HasPrefix("foo"))
// error interface
checkOK(t, errors.New("pipo bingo"), td.HasPrefix("pipo"))
// fmt.Stringer interface
checkOK(t, MyStringer{}, td.HasPrefix("pipo"))
checkError(t, "foo bar test", td.HasPrefix("pipo"),
expectedError{
Message: mustBe("has not prefix"),
Path: mustBe("DATA"),
Got: mustContain(`"foo bar test"`),
Expected: mustMatch(`^HasPrefix\(.*"pipo"`),
})
checkError(t, []int{1, 2}, td.HasPrefix("bar"),
expectedError{
Message: mustBe("bad type"),
Path: mustBe("DATA"),
Got: mustBe("[]int"),
Expected: mustBe("string (convertible) OR []byte (convertible) OR fmt.Stringer OR error"),
})
checkError(t, 12, td.HasPrefix("bar"),
expectedError{
Message: mustBe("bad type"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("string (convertible) OR []byte (convertible) OR fmt.Stringer OR error"),
})
}
func TestHasSuffix(t *testing.T) {
checkOK(t, "foobar", td.HasSuffix("bar"))
checkOK(t, []byte("foobar"), td.HasSuffix("bar"))
type MyBytes []byte
checkOK(t, MyBytes("foobar"), td.HasSuffix("bar"))
type MyString string
checkOK(t, MyString("foobar"), td.HasSuffix("bar"))
// error interface
checkOK(t, errors.New("pipo bingo"), td.HasSuffix("bingo"))
// fmt.Stringer interface
checkOK(t, MyStringer{}, td.HasSuffix("bingo"))
checkError(t, "foo bar test", td.HasSuffix("pipo"),
expectedError{
Message: mustBe("has not suffix"),
Path: mustBe("DATA"),
Got: mustContain(`"foo bar test"`),
Expected: mustMatch(`^HasSuffix\(.*"pipo"`),
})
checkError(t, []int{1, 2}, td.HasSuffix("bar"),
expectedError{
Message: mustBe("bad type"),
Path: mustBe("DATA"),
Got: mustBe("[]int"),
Expected: mustBe("string (convertible) OR []byte (convertible) OR fmt.Stringer OR error"),
})
checkError(t, 12, td.HasSuffix("bar"),
expectedError{
Message: mustBe("bad type"),
Path: mustBe("DATA"),
Got: mustBe("int"),
Expected: mustBe("string (convertible) OR []byte (convertible) OR fmt.Stringer OR error"),
})
}
func TestStringTypeBehind(t *testing.T) {
equalTypes(t, td.String("x"), nil)
equalTypes(t, td.HasPrefix("x"), nil)
equalTypes(t, td.HasSuffix("x"), nil)
}
go-testdeep-1.15.0/td/td_struct.go 0000664 0000000 0000000 00000054125 15144170453 0017002 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (
"bytes"
"errors"
"fmt"
"os"
"path"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"unicode"
"unicode/utf8"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/dark"
"github.com/maxatome/go-testdeep/internal/util"
)
type tdStruct struct {
tdExpectedType
expectedFields fieldInfoSlice
}
var _ TestDeep = &tdStruct{}
type fieldInfo struct {
name string
expected reflect.Value
index []int
unexported bool
}
type fieldInfoSlice []fieldInfo
func (e fieldInfoSlice) Len() int { return len(e) }
func (e fieldInfoSlice) Less(i, j int) bool { return e[i].name < e[j].name }
func (e fieldInfoSlice) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
type fieldMatcher struct {
name string
match func(string) (bool, error)
expected any
order int
ok bool
}
var (
reMatcherOnce sync.Once
reMatcher *regexp.Regexp
errNotAMatcher = errors.New("Not a matcher")
)
// parseMatcher parses " [NUM] OP PATTERN " and returns 3 strings
// corresponding to each part or nil if "s" is not a matcher.
func parseMatcher(s string) []string {
reMatcherOnce.Do(func() {
reMatcher = regexp.MustCompile(`^(?:(\d+)\s*)?([=!]~?)\s*(.+)`)
})
subs := reMatcher.FindStringSubmatch(strings.TrimSpace(s))
if subs != nil {
subs = subs[1:]
}
return subs
}
// newFieldMatcher checks name matches "[NUM] OP PATTERN" where NUM
// is an optional number used to sort patterns, OP is "=~", "!~", "="
// or "!" and PATTERN is a regexp (when OP is either "=~" or "!~") or
// a shell pattern (when OP is either "=" or "!").
//
// NUM, OP and PATTERN can be separated by spaces (or not).
func newFieldMatcher(name string, expected any) (fieldMatcher, error) {
subs := parseMatcher(name)
if subs == nil {
return fieldMatcher{}, errNotAMatcher
}
fm := fieldMatcher{
name: name,
expected: expected,
ok: subs[1][0] == '=',
}
if subs[0] != "" {
fm.order, _ = strconv.Atoi(subs[0]) //nolint: errcheck
}
// Shell pattern
if subs[1] == "=" || subs[1] == "!" {
pattern := subs[2]
fm.match = func(s string) (bool, error) {
return path.Match(pattern, s)
}
return fm, nil
}
// Regexp
r, err := regexp.Compile(subs[2])
if err != nil {
return fieldMatcher{}, fmt.Errorf("bad regexp field %#q: %s", name, err)
}
fm.match = func(s string) (bool, error) {
return r.MatchString(s), nil
}
return fm, nil
}
type fieldMatcherSlice []fieldMatcher
func (m fieldMatcherSlice) Len() int { return len(m) }
func (m fieldMatcherSlice) Less(i, j int) bool {
if m[i].order != m[j].order {
return m[i].order < m[j].order
}
return m[i].name < m[j].name
}
func (m fieldMatcherSlice) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
// StructFields allows to pass struct fields to check in functions
// [Struct] and [SStruct]. It is a map whose each key is the expected
// field name (or a regexp or a shell pattern matching a field name,
// see [Struct] & [SStruct] docs for details) and the corresponding
// value the expected field value (which can be a [TestDeep] operator
// as well as a zero value.)
type StructFields map[string]any
// canonStructField canonicalizes name, a key in a StructFields map,
// so it can be compared with other keys during a mergeStructFields().
// - "name" → "name"
// - "> name " → ">name"
// - " 22 =~ [A-Z].*At$ " → "22=~[A-Z].*At$"
func canonStructField(name string) string {
r, _ := utf8.DecodeRuneInString(name)
if r == utf8.RuneError || unicode.IsLetter(r) {
return name // shortcut
}
// Overwrite a field
if strings.HasPrefix(name, ">") {
nn := strings.TrimSpace(name[1:])
if 1+len(nn) == len(name) {
return name // already canonicalized
}
return ">" + nn
}
// Matcher
if subs := parseMatcher(name); subs != nil {
if len(subs[0])+len(subs[1])+len(subs[2]) == len(name) {
return name // already canonicalized
}
return subs[0] + subs[1] + subs[2]
}
// Will probably raise an error later as it cannot be a field, not
// an overwritter and not a matcher
return name
}
// mergeStructFields merges all sfs items into one StructFields and
// returns it.
func mergeStructFields(sfs ...StructFields) StructFields {
switch len(sfs) {
case 0:
return nil
case 1:
return sfs[0]
default:
// Do a smart merge so "> pipo" replaces ">pipo " for example.
canon2field := map[string]string{}
ret := make(StructFields, len(sfs[0]))
for _, sf := range sfs {
for field, value := range sf {
canon := canonStructField(field)
if prevField, ok := canon2field[canon]; ok {
delete(ret, prevField)
delete(canon2field, canon)
} else {
delete(ret, canon)
}
if canon != field {
canon2field[canon] = field
}
ret[field] = value
}
}
return ret
}
}
func newStruct(base base, vmodel reflect.Value) (*tdStruct, reflect.Value) {
st := tdStruct{
tdExpectedType: tdExpectedType{
base: base,
},
}
switch vmodel.Kind() {
case reflect.Ptr:
if vmodel.Type().Elem().Kind() != reflect.Struct {
break
}
st.isPtr = true
if vmodel.IsNil() {
st.expectedType = vmodel.Type().Elem()
return &st, reflect.Value{}
}
vmodel = vmodel.Elem()
fallthrough
case reflect.Struct:
st.expectedType = vmodel.Type()
return &st, vmodel
}
st.err = ctxerr.OpBadUsage(st.location.Func,
"(STRUCT|&STRUCT|nil, EXPECTED_FIELDS)",
vmodel.Interface(), 1, true)
return &st, reflect.Value{}
}
// structTypeString returns stringified t. It is the caller
// responsibility to check t is a struct type.
// - struct{} → "struct {}"
// - pkg.MyType → "struct pkg.MyType"
func structTypeString(t reflect.Type) string {
if t.Name() == "" {
return t.String()
}
return "struct " + t.String()
}
func anyStruct(base base, model reflect.Value, expectedFields StructFields, strict bool) *tdStruct {
st, vmodel := newStruct(base, model)
if st.err != nil {
return st
}
st.expectedFields = make([]fieldInfo, 0, len(expectedFields))
checkedFields := make(map[string]bool, len(expectedFields))
var matchers fieldMatcherSlice //nolint: prealloc
// Check that all given fields are available in model
stType := st.expectedType
for fieldName, expectedValue := range expectedFields {
field, found := stType.FieldByName(fieldName)
if found {
st.addExpectedValue(field, expectedValue, "")
checkedFields[fieldName] = false
continue
}
// overwrite model field: ">fieldName", "> fieldName"
if strings.HasPrefix(fieldName, ">") {
name := strings.TrimSpace(fieldName[1:])
field, found = stType.FieldByName(name)
if !found {
st.err = ctxerr.OpBad(st.location.Func,
"%s has no field %q (from %q)", structTypeString(stType), name, fieldName)
return st
}
st.addExpectedValue(
field, expectedValue,
fmt.Sprintf(" (from %q)", fieldName))
checkedFields[name] = true
continue
}
// matcher: "=~At$", "!~At$", "=*At", "!*At"
matcher, err := newFieldMatcher(fieldName, expectedValue)
if err != nil {
if err == errNotAMatcher {
st.err = ctxerr.OpBad(st.location.Func,
"%s has no field %q", structTypeString(stType), fieldName)
} else {
st.err = ctxerr.OpBad(st.location.Func, err.Error())
}
return st
}
matchers = append(matchers, matcher)
}
// Get all field names
allFields := map[string]struct{}{}
stType.FieldByNameFunc(func(fieldName string) bool {
allFields[fieldName] = struct{}{}
return false
})
// Check initialized fields in model
if vmodel.IsValid() {
for fieldName := range allFields {
overwrite, alreadySet := checkedFields[fieldName]
if overwrite {
continue
}
field, _ := stType.FieldByName(fieldName)
if field.Anonymous {
continue
}
vfield := vmodel.FieldByIndex(field.Index)
// Try to force access to unexported fields
fieldIf, ok := dark.GetInterface(vfield, true)
if !ok {
// Probably in an environment where "unsafe" package is forbidden… :(
fmt.Fprintf(os.Stderr, //nolint: errcheck
"%s(): field %s is unexported and cannot be overridden, skip it from model.\n",
st.location.Func,
fieldName)
continue
}
// If non-zero field
if !reflect.DeepEqual(reflect.Zero(field.Type).Interface(), fieldIf) {
if alreadySet {
st.err = ctxerr.OpBad(st.location.Func,
"non zero field %s in model already exists in expectedFields",
fieldName)
return st
}
st.expectedFields = append(st.expectedFields, fieldInfo{
name: fieldName,
expected: vfield,
index: field.Index,
unexported: field.PkgPath != "",
})
checkedFields[fieldName] = true
}
}
}
// At least one matcher (regexp/shell pattern)
if matchers != nil {
sort.Sort(matchers) // always process matchers in the same order
for _, m := range matchers {
for fieldName := range allFields {
if _, ok := checkedFields[fieldName]; ok {
continue
}
field, _ := stType.FieldByName(fieldName)
if field.Anonymous {
continue
}
ok, err := m.match(fieldName)
if err != nil {
st.err = ctxerr.OpBad(st.location.Func,
"bad shell pattern field %#q: %s", m.name, err)
return st
}
if ok == m.ok {
st.addExpectedValue(
field, m.expected,
fmt.Sprintf(" (from pattern %#q)", m.name))
checkedFields[fieldName] = true
}
}
}
}
// If strict, fill non explicitly expected fields to zero
if strict {
for fieldName := range allFields {
if _, ok := checkedFields[fieldName]; ok {
continue
}
field, _ := stType.FieldByName(fieldName)
if field.Anonymous {
continue
}
st.expectedFields = append(st.expectedFields, fieldInfo{
name: fieldName,
expected: reflect.New(field.Type).Elem(), // zero
index: field.Index,
unexported: field.PkgPath != "",
})
}
}
sort.Sort(st.expectedFields)
return st
}
func (s *tdStruct) addExpectedValue(field reflect.StructField, expectedValue any, ctxInfo string) {
var vexpectedValue reflect.Value
if expectedValue == nil {
switch field.Type.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map,
reflect.Ptr, reflect.Slice:
vexpectedValue = reflect.Zero(field.Type) // change to a typed nil
// default:
// Don't raise an error if field cannot be nil as a smuggle hook can
// change it at fly during the comparison
}
} else {
vexpectedValue = reflect.ValueOf(expectedValue)
// Don't check vexpectedValue type against field one as a
// smuggle hook can change it at fly during the comparison
}
s.expectedFields = append(s.expectedFields, fieldInfo{
name: field.Name + ctxInfo,
expected: vexpectedValue,
index: field.Index,
unexported: field.PkgPath != "",
})
}
// summary(Struct): compares the contents of a struct or a pointer on
// a struct
// input(Struct): struct,ptr(ptr on struct)
// Struct operator compares the contents of a struct or a pointer on a
// struct against the non-zero values of model (if any) and the
// values of expectedFields. See [SStruct] to compares against zero
// fields without specifying them in expectedFields.
//
// model must be the same type as compared data. If the expected type
// is anonymous or private, model can be nil. In this case it is
// considered lazy and determined each time the operator is involved
// in a match, see below.
//
// expectedFields can be omitted, if no zero entries are expected
// and no [TestDeep] operators are involved. If expectedFields
// contains more than one item, all items are merged before their use,
// from left to right.
//
// td.Cmp(t, got, td.Struct(
// Person{
// Name: "John Doe",
// },
// td.StructFields{
// "Children": 4,
// },
// td.StructFields{
// "Age": td.Between(40, 45),
// "Children": 0, // overwrite 4
// }),
// )
//
// It is an error to set a non-zero field in model AND to set the
// same field in expectedFields, as in such cases the Struct
// operator does not know if the user wants to override the non-zero
// model field value or if it is an error. To explicitly override a
// non-zero model in expectedFields, just prefix its name with a
// ">" (followed by some optional spaces), as in:
//
// td.Cmp(t, got, td.Struct(
// Person{
// Name: "John Doe",
// Age: 23,
// Children: 4,
// },
// td.StructFields{
// "> Age": td.Between(40, 45),
// ">Children": 0, // spaces after ">" are optional
// }),
// )
//
// expectedFields can also contain regexps or shell patterns to
// match multiple fields not explicitly listed in model and in
// expectedFields. Regexps are prefixed by "=~" or "!~" to
// respectively match or don't-match. Shell patterns are prefixed by "="
// or "!" to respectively match or don't-match.
//
// td.Cmp(t, got, td.Struct(
// Person{
// Name: "John Doe",
// },
// td.StructFields{
// "=*At": td.Lte(time.Now()), // matches CreatedAt & UpdatedAt fields using shell pattern
// "=~^[a-z]": td.Ignore(), // explicitly ignore private fields using a regexp
// }),
// )
//
// When several patterns can match a same field, it is advised to tell
// go-testdeep in which order patterns should be tested, as once a
// pattern matches a field, the other patterns are ignored for this
// field. To do so, each pattern can be prefixed by a number, as in:
//
// td.Cmp(t, got, td.Struct(
// Person{
// Name: "John Doe",
// },
// td.StructFields{
// "1=*At": td.Lte(time.Now()),
// "2=~^[a-z]": td.NotNil(),
// }),
// )
//
// This way, "*At" shell pattern is always used before "^[a-z]"
// regexp, so if a field "createdAt" exists it is tested against
// time.Now() and never against [NotNil]. A pattern without a
// prefix number is the same as specifying "0" as prefix.
//
// To make it clearer, some spaces can be added, as well as bigger
// numbers used:
//
// td.Cmp(t, got, td.Struct(
// Person{
// Name: "John Doe",
// },
// td.StructFields{
// " 900 = *At": td.Lte(time.Now()),
// "2000 =~ ^[a-z]": td.NotNil(),
// }),
// )
//
// The following example combines all possibilities:
//
// td.Cmp(t, got, td.Struct(
// Person{
// NickName: "Joe",
// },
// td.StructFields{
// "Firstname": td.Any("John", "Johnny"),
// "1 = *[nN]ame": td.NotEmpty(), // matches LastName, lastname, …
// "2 ! [A-Z]*": td.NotZero(), // matches all private fields
// "3 =~ ^(Crea|Upda)tedAt$": td.Gte(time.Now()),
// "4 !~ ^(Dogs|Children)$": td.Zero(), // matches all remaining fields except Dogs and Children
// "5 =~ .": td.NotNil(), // matches all remaining fields (same as "5 = *")
// }),
// )
//
// If the expected type is private to the current package, it cannot
// be passed as model. To overcome this limitation, model can be nil,
// it is then considered as lazy. This way, the model is automatically
// set during each match to the same type (still requiring struct or
// struct pointer) of the compared data. Similarly, testing an
// anonymous struct can be boring as all fields have to be re-declared
// to define model. A nil model avoids that:
//
// got := struct {
// name string
// age int
// }{"Bob", 42}
// td.Cmp(t, got, td.Struct(nil, td.StructFields{"age": td.Between(40, 42)}))
//
// During a match, all expected fields must be found to
// succeed. Non-expected fields (and so zero model fields) are
// ignored.
//
// TypeBehind method returns the [reflect.Type] of model.
//
// See also [SStruct].
func Struct(model any, expectedFields ...StructFields) TestDeep {
ef := mergeStructFields(expectedFields...)
if model == nil {
return newStructLazy(ef, false)
}
return anyStruct(newBase(3), reflect.ValueOf(model), ef, false)
}
// summary(SStruct): strictly compares the contents of a struct or a
// pointer on a struct
// input(SStruct): struct,ptr(ptr on struct)
// SStruct operator (aka strict-[Struct]) compares the contents of a
// struct or a pointer on a struct against values of model (if any)
// and the values of expectedFields. The zero values are compared
// too even if they are omitted from expectedFields: that is the
// difference with [Struct] operator.
//
// model must be the same type as compared data. If the expected type
// is private or anonymous, model can be nil. In this case it is
// considered lazy and determined each time the operator is involved
// in a match, see below.
//
// expectedFields can be omitted, if no [TestDeep] operators are
// involved. If expectedFields contains more than one item, all
// items are merged before their use, from left to right.
//
// To ignore a field, one has to specify it in expectedFields and
// use the [Ignore] operator.
//
// td.Cmp(t, got, td.SStruct(
// Person{
// Name: "John Doe",
// },
// td.StructFields{
// "Children": 4,
// },
// td.StructFields{
// "Age": td.Between(40, 45),
// "Children": td.Ignore(), // overwrite 4
// }),
// )
//
// It is an error to set a non-zero field in model AND to set the
// same field in expectedFields, as in such cases the SStruct
// operator does not know if the user wants to override the non-zero
// model field value or if it is an error. To explicitly override a
// non-zero model in expectedFields, just prefix its name with a
// ">" (followed by some optional spaces), as in:
//
// td.Cmp(t, got, td.SStruct(
// Person{
// Name: "John Doe",
// Age: 23,
// Children: 4,
// },
// td.StructFields{
// "> Age": td.Between(40, 45),
// ">Children": 0, // spaces after ">" are optional
// }),
// )
//
// expectedFields can also contain regexps or shell patterns to
// match multiple fields not explicitly listed in model and in
// expectedFields. Regexps are prefixed by "=~" or "!~" to
// respectively match or don't-match. Shell patterns are prefixed by "="
// or "!" to respectively match or don't-match.
//
// td.Cmp(t, got, td.SStruct(
// Person{
// Name: "John Doe",
// },
// td.StructFields{
// "=*At": td.Lte(time.Now()), // matches CreatedAt & UpdatedAt fields using shell pattern
// "=~^[a-z]": td.Ignore(), // explicitly ignore private fields using a regexp
// }),
// )
//
// When several patterns can match a same field, it is advised to tell
// go-testdeep in which order patterns should be tested, as once a
// pattern matches a field, the other patterns are ignored for this
// field. To do so, each pattern can be prefixed by a number, as in:
//
// td.Cmp(t, got, td.SStruct(
// Person{
// Name: "John Doe",
// },
// td.StructFields{
// "1=*At": td.Lte(time.Now()),
// "2=~^[a-z]": td.NotNil(),
// }),
// )
//
// This way, "*At" shell pattern is always used before "^[a-z]"
// regexp, so if a field "createdAt" exists it is tested against
// time.Now() and never against [NotNil]. A pattern without a
// prefix number is the same as specifying "0" as prefix.
//
// To make it clearer, some spaces can be added, as well as bigger
// numbers used:
//
// td.Cmp(t, got, td.SStruct(
// Person{
// Name: "John Doe",
// },
// td.StructFields{
// " 900 = *At": td.Lte(time.Now()),
// "2000 =~ ^[a-z]": td.NotNil(),
// }),
// )
//
// The following example combines all possibilities:
//
// td.Cmp(t, got, td.SStruct(
// Person{
// NickName: "Joe",
// },
// td.StructFields{
// "Firstname": td.Any("John", "Johnny"),
// "1 = *[nN]ame": td.NotEmpty(), // matches LastName, lastname, …
// "2 ! [A-Z]*": td.NotZero(), // matches all private fields
// "3 =~ ^(Crea|Upda)tedAt$": td.Gte(time.Now()),
// "4 !~ ^(Dogs|Children)$": td.Zero(), // matches all remaining fields except Dogs and Children
// "5 =~ .": td.NotNil(), // matches all remaining fields (same as "5 = *")
// }),
// )
//
// If the expected type is private to the current package, it cannot
// be passed as model. To overcome this limitation, model can be nil,
// it is then considered as lazy. This way, the model is automatically
// set during each match to the same type (still requiring struct or
// struct pointer) of the compared data. Similarly, testing an
// anonymous struct can be boring as all fields have to be re-declared
// to define model. A nil model avoids that:
//
// got := struct {
// name string
// age int
// }{"Bob", 42}
// td.Cmp(t, got, td.SStruct(nil, td.StructFields{
// "name": "Bob",
// "age": td.Between(40, 42),
// }))
//
// During a match, all expected and zero fields must be found to
// succeed.
//
// TypeBehind method returns the [reflect.Type] of model.
//
// See also [SStruct].
func SStruct(model any, expectedFields ...StructFields) TestDeep {
ef := mergeStructFields(expectedFields...)
if model == nil {
return newStructLazy(ef, false)
}
return anyStruct(newBase(3), reflect.ValueOf(model), ef, true)
}
func (s *tdStruct) Match(ctx ctxerr.Context, got reflect.Value) (err *ctxerr.Error) {
if s.err != nil {
return ctx.CollectError(s.err)
}
err = s.checkPtr(ctx, &got, false)
if err != nil {
return ctx.CollectError(err)
}
err = s.checkType(ctx, got)
if err != nil {
return ctx.CollectError(err)
}
ignoreUnexported := ctx.IgnoreUnexported || ctx.Hooks.IgnoreUnexported(got.Type())
for _, fieldInfo := range s.expectedFields {
if ignoreUnexported && fieldInfo.unexported {
continue
}
err = deepValueEqual(ctx.AddField(fieldInfo.name),
got.FieldByIndex(fieldInfo.index), fieldInfo.expected)
if err != nil {
return
}
}
return nil
}
func (s *tdStruct) String() string {
if s.err != nil {
return s.stringError()
}
buf := bytes.NewBufferString(s.location.Func)
buf.WriteByte('(')
if s.isPtr {
buf.WriteByte('*')
}
buf.WriteString(s.expectedType.String())
if len(s.expectedFields) == 0 {
buf.WriteString("{})")
} else {
buf.WriteString("{\n")
maxLen := 0
for _, fieldInfo := range s.expectedFields {
if len(fieldInfo.name) > maxLen {
maxLen = len(fieldInfo.name)
}
}
maxLen++
for _, fieldInfo := range s.expectedFields {
fmt.Fprintf(buf, " %-*s %s\n", //nolint: errcheck
maxLen, fieldInfo.name+":", util.ToString(fieldInfo.expected))
}
buf.WriteString("})")
}
return buf.String()
}
go-testdeep-1.15.0/td/td_struct_lazy.go 0000664 0000000 0000000 00000003711 15144170453 0020034 0 ustar 00root root 0000000 0000000 // Copyright (c) 2023, 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 (
"bytes"
"fmt"
"reflect"
"sort"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/util"
)
type tdStructLazy struct {
base
cache map[reflect.Type]*tdStruct
expectedFields StructFields
strict bool
}
var _ TestDeep = &tdStructLazy{}
func newStructLazy(expectedFields StructFields, strict bool) TestDeep {
return &tdStructLazy{
base: newBase(4),
cache: map[reflect.Type]*tdStruct{},
expectedFields: expectedFields,
strict: strict,
}
}
func (s *tdStructLazy) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
gotType := got.Type()
tds := s.cache[gotType]
if tds == nil {
switch gotType.Kind() {
case reflect.Struct:
case reflect.Ptr:
if gotType.Elem().Kind() == reflect.Struct {
break
}
fallthrough
default:
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(ctxerr.BadKind(got, "struct OR *struct"))
}
tds = anyStruct(s.base, reflect.New(got.Type()).Elem(), s.expectedFields, s.strict)
tds.location = s.location
s.cache[gotType] = tds
}
return tds.Match(ctx, got)
}
func (s *tdStructLazy) String() string {
buf := bytes.NewBufferString(s.location.Func)
buf.WriteString("({")
if len(s.expectedFields) > 0 {
buf.WriteByte('\n')
fields := make([]string, 0, len(s.expectedFields))
maxLen := 0
for name := range s.expectedFields {
fields = append(fields, name)
if len(name) > maxLen {
maxLen = len(name)
}
}
sort.Strings(fields)
maxLen++
for _, name := range fields {
fmt.Fprintf(buf, " %-*s %s\n", //nolint: errcheck
maxLen, name+":", util.ToString(s.expectedFields[name]))
}
}
buf.WriteString("})")
return buf.String()
}
go-testdeep-1.15.0/td/td_struct_lazy_test.go 0000664 0000000 0000000 00000010577 15144170453 0021103 0 ustar 00root root 0000000 0000000 // Copyright (c) 2023, 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 TestStructLazy(t *testing.T) {
got := struct {
ValInt int
ValStr string
}{0, "foobar"}
t.Run("Struct OK", func(t *testing.T) {
got.ValInt = 123
checkOK(t, got, td.Struct(nil, td.StructFields{
"ValStr": "foobar",
}))
checkOK(t, &got, td.Struct(nil, td.StructFields{
"ValInt": 123,
"ValStr": "foobar",
}))
checkOK(t, &got, td.Struct(nil, td.StructFields{"=Val*": td.NotZero()}))
})
t.Run("SStruct OK", func(t *testing.T) {
got.ValInt = 0
checkOK(t, got, td.SStruct(nil, td.StructFields{
"ValStr": "foobar",
}))
checkOK(t, &got, td.SStruct(nil, td.StructFields{
"ValInt": 0,
"ValStr": "foobar",
}))
got.ValInt = 123
checkOK(t, &got, td.SStruct(nil, td.StructFields{"=Val*": td.NotZero()}))
})
got.ValInt = 666
ops := []struct {
name string
new func(any, ...td.StructFields) td.TestDeep
}{
{"Struct", td.Struct},
{"SStruct", td.SStruct},
}
for _, op := range ops {
t.Run(op.name+" errors", func(t *testing.T) {
under := mustContain("under operator " + op.name + " at td_struct_lazy_test.go:")
badUsage := mustBe("bad usage of " + op.name + " operator")
checkError(t, got, op.new(nil, td.StructFields{"Zip": 345}),
expectedError{
Message: badUsage,
Path: mustBe("DATA"),
Summary: mustBe(`struct { ValInt int; ValStr string } has no field "Zip"`),
Under: under,
})
checkError(t, got, op.new(nil, td.StructFields{">\tZip": 345}),
expectedError{
Message: badUsage,
Path: mustBe("DATA"),
Summary: mustBe(`struct { ValInt int; ValStr string } has no field "Zip" (from ">\tZip")`),
Under: under,
})
checkError(t, got, op.new(nil, td.StructFields{"ValInt": "zip"}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA.ValInt"),
Got: mustBe("int"),
Expected: mustBe("string"),
})
checkError(t, 123,
op.new(nil, td.StructFields{}),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustContain("int"),
Expected: mustContain("struct OR *struct"),
Under: under,
})
n := 123
checkError(t, &n,
op.new(nil, td.StructFields{}),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustContain("*int"),
Expected: mustContain("struct OR *struct"),
Under: under,
})
type myInt int
checkError(t, myInt(123),
op.new(nil, td.StructFields{}),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustContain("int (td_test.myInt type)"),
Expected: mustContain("struct OR *struct"),
Under: under,
})
mi := myInt(123)
checkError(t, &mi,
op.new(nil, td.StructFields{}),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustContain("*int (*td_test.myInt type)"),
Expected: mustContain("struct OR *struct"),
Under: under,
})
checkError(t, nil, op.new(nil, td.StructFields{}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustContain("Struct({})"),
Under: under,
})
checkError(t, (*struct{ x int })(nil), op.new(nil, td.StructFields{}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("(*struct { x int })()"),
Expected: mustContain("non-nil"),
Under: under,
})
})
t.Run(op.name+" String", func(t *testing.T) {
test.EqualStr(t, op.new(nil).String(), op.name+`({})`)
test.EqualStr(t,
op.new(nil,
td.StructFields{
"ValBool": false,
"= Val*": td.NotZero(),
"> Foo": 12,
"=~Bar[6-9]$": "zip",
}).String(),
op.name+`({
= Val*: NotZero()
=~Bar[6-9]$: "zip"
> Foo: 12
ValBool: false
})`)
})
}
}
func TestStructLazyTypeBehind(t *testing.T) {
equalTypes(t, td.Struct(nil, nil), nil)
equalTypes(t, td.SStruct(nil, nil), nil)
}
go-testdeep-1.15.0/td/td_struct_private_test.go 0000664 0000000 0000000 00000006265 15144170453 0021575 0 ustar 00root root 0000000 0000000 // Copyright (c) 2021, 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"
"strings"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
)
func TestCanonStructField(t *testing.T) {
for _, tst := range []struct{ got, expected string }{
{"", ""},
{"pipo", "pipo"},
{">pipo", ">pipo"},
{"> pipo ", ">pipo"},
{"123=~.*", "123=~.*"},
{" 123 =~ .* ", "123=~.*"},
{"&badField", "&badField"},
} {
test.EqualStr(t, canonStructField(tst.got), tst.expected)
}
}
func TestMergeStructFields(t *testing.T) {
sfs := mergeStructFields()
if sfs != nil {
t.Errorf("not nil")
}
x := StructFields{}
sfs = mergeStructFields(x)
if reflect.ValueOf(sfs).Pointer() != reflect.ValueOf(x).Pointer() {
t.Errorf("not x")
}
a := StructFields{"pipo": 1}
b := StructFields{"pipo": 2}
c := StructFields{"pipo": 3}
sfs = mergeStructFields(a, b, c)
if reflect.ValueOf(sfs).Pointer() == reflect.ValueOf(c).Pointer() {
t.Errorf("is c")
}
test.EqualInt(t, len(sfs), 1)
test.EqualInt(t, sfs["pipo"].(int), 3)
a = StructFields{">pipo": 1}
b = StructFields{"> pipo": 2}
c = StructFields{">pipo ": 3}
sfs = mergeStructFields(a, b, c)
if reflect.ValueOf(sfs).Pointer() == reflect.ValueOf(c).Pointer() {
t.Errorf("is c")
}
test.EqualInt(t, len(sfs), 1)
test.EqualInt(t, sfs[">pipo "].(int), 3)
a = StructFields{"1=~pipo": 1}
b = StructFields{" 1 =~ pipo ": 2}
c = StructFields{"1\t=~\tpipo": 3}
sfs = mergeStructFields(a, b, c)
if reflect.ValueOf(sfs).Pointer() == reflect.ValueOf(c).Pointer() {
t.Errorf("is c")
}
test.EqualInt(t, len(sfs), 1)
test.EqualInt(t, sfs["1\t=~\tpipo"].(int), 3)
}
func TestFieldMatcher(t *testing.T) {
_, err := newFieldMatcher("pipo", 123)
if test.Error(t, err) {
if err != errNotAMatcher {
t.Errorf("got %q, but %q was expected", err, errNotAMatcher)
}
}
for _, tst := range []struct {
name string
order int
match bool
}{
// Regexp
{name: "=~.*", match: true},
{name: "=~bc", match: true},
{name: "=~3$", match: true},
{name: "!~^b", match: false},
{name: "134=~bc", match: true, order: 134},
{name: "134 =~ bc", match: true, order: 134},
{name: " 134 =~ bc", match: true, order: 134},
// Shell pattern
{name: "=*", match: true},
{name: "=*bc*", match: true},
{name: "=*3", match: true},
{name: "!b*", match: false},
{name: "134=*", match: true, order: 134},
{name: "134 = *", match: true, order: 134},
{name: " 134 = *", match: true, order: 134},
} {
fm, err := newFieldMatcher(tst.name, 123)
test.NoError(t, err, tst.name)
test.EqualStr(t, fm.name, tst.name, tst.name)
test.EqualInt(t, fm.expected.(int), 123, tst.name)
test.EqualInt(t, fm.order, tst.order, tst.name)
test.EqualBool(t, fm.ok, strings.ContainsRune(tst.name, '='), tst.name)
if test.IsTrue(t, fm.match != nil, tst.name) {
ok, err := fm.match("abc123")
test.NoError(t, err, tst.name)
test.EqualBool(t, ok, tst.match)
}
}
_, err = newFieldMatcher("=~bad(*", 123)
if test.Error(t, err) {
test.IsTrue(t, strings.HasPrefix(err.Error(), "bad regexp field `=~bad(*`: "))
}
}
go-testdeep-1.15.0/td/td_struct_test.go 0000664 0000000 0000000 00000067014 15144170453 0020042 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 (
"bytes"
"errors"
"testing"
"time"
"github.com/maxatome/go-testdeep/internal/dark"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestStruct(t *testing.T) {
gotStruct := MyStruct{
MyStructMid: MyStructMid{
MyStructBase: MyStructBase{
ValBool: true,
},
ValStr: "foobar",
},
ValInt: 123,
}
//
// Using pointer
checkOK(t, &gotStruct,
td.Struct(&MyStruct{}, td.StructFields{
"ValBool": true,
"ValStr": "foobar",
"ValInt": 123,
"Ptr": nil,
}))
checkOK(t, &gotStruct,
td.Struct(
&MyStruct{
MyStructMid: MyStructMid{
ValStr: "zip",
},
ValInt: 666,
},
td.StructFields{
"ValBool": true,
"> ValStr": "foobar",
">ValInt": 123,
}))
checkOK(t, &gotStruct,
td.Struct((*MyStruct)(nil), td.StructFields{
"ValBool": true,
"ValStr": "foobar",
"ValInt": 123,
"Ptr": nil,
}))
checkError(t, 123,
td.Struct(&MyStruct{}, td.StructFields{}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustContain("int"),
Expected: mustContain("*td_test.MyStruct"),
})
checkError(t, &MyStructBase{},
td.Struct(&MyStruct{}, td.StructFields{}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustContain("*td_test.MyStructBase"),
Expected: mustContain("*td_test.MyStruct"),
})
checkError(t, &gotStruct,
td.Struct(&MyStruct{}, td.StructFields{
"ValBool": false, // ← does not match
"ValStr": "foobar",
"ValInt": 123,
}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.ValBool"),
Got: mustContain("true"),
Expected: mustContain("false"),
})
checkOK(t, &gotStruct,
td.Struct(&MyStruct{
MyStructMid: MyStructMid{
MyStructBase: MyStructBase{
ValBool: true,
},
ValStr: "foobar",
},
ValInt: 123,
}, nil))
checkError(t, &gotStruct,
td.Struct(&MyStruct{
MyStructMid: MyStructMid{
MyStructBase: MyStructBase{
ValBool: true,
},
ValStr: "foobax", // ← does not match
},
ValInt: 123,
}, nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.ValStr"),
Got: mustContain("foobar"),
Expected: mustContain("foobax"),
})
// Zero values
checkOK(t, &MyStruct{},
td.Struct(&MyStruct{}, td.StructFields{
"ValBool": false,
"ValStr": "",
"ValInt": 0,
}))
// nil cases
checkError(t, nil, td.Struct(&MyStruct{}, nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustContain("nil"),
Expected: mustContain("*td_test.MyStruct"),
})
checkError(t, (*MyStruct)(nil), td.Struct(&MyStruct{}, nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustContain("nil"),
Expected: mustBe("non-nil"),
})
//
// Without pointer
checkOK(t, gotStruct,
td.Struct(MyStruct{}, td.StructFields{
"ValBool": true,
"ValStr": "foobar",
"ValInt": 123,
}))
checkOK(t, gotStruct,
td.Struct(
MyStruct{
MyStructMid: MyStructMid{
ValStr: "zip",
},
ValInt: 666,
},
td.StructFields{
"ValBool": true,
"> ValStr": "foobar",
">ValInt": 123,
}))
checkError(t, 123, td.Struct(MyStruct{}, td.StructFields{}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustContain("int"),
Expected: mustContain("td_test.MyStruct"),
})
checkError(t, gotStruct,
td.Struct(MyStruct{}, td.StructFields{
"ValBool": false, // ← does not match
"ValStr": "foobar",
"ValInt": 123,
}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.ValBool"),
Got: mustContain("true"),
Expected: mustContain("false"),
})
checkOK(t, gotStruct,
td.Struct(MyStruct{
MyStructMid: MyStructMid{
MyStructBase: MyStructBase{
ValBool: true,
},
ValStr: "foobar",
},
ValInt: 123,
}, nil))
checkError(t, gotStruct,
td.Struct(MyStruct{
MyStructMid: MyStructMid{
MyStructBase: MyStructBase{
ValBool: true,
},
ValStr: "foobax", // ← does not match
},
ValInt: 123,
}, nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.ValStr"),
Got: mustContain("foobar"),
Expected: mustContain("foobax"),
})
// Zero values
checkOK(t, MyStruct{},
td.Struct(MyStruct{}, td.StructFields{
"ValBool": false,
"ValStr": "",
"ValInt": 0,
}))
// nil cases
checkError(t, nil, td.Struct(MyStruct{}, nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustContain("nil"),
Expected: mustContain("td_test.MyStruct"),
})
checkError(t, (*MyStruct)(nil), td.Struct(MyStruct{}, nil),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("*td_test.MyStruct"),
Expected: mustBe("td_test.MyStruct"),
})
//
// Be lax...
type Struct1 struct {
name string
age int
}
type Struct2 struct {
name string
age int
}
// Without Lax → error
checkError(t,
Struct1{name: "Bob", age: 42},
td.Struct(Struct2{name: "Bob", age: 42}, nil),
expectedError{
Message: mustBe("type mismatch"),
})
// With Lax → OK
checkOK(t,
Struct1{name: "Bob", age: 42},
td.Lax(td.Struct(Struct2{name: "Bob", age: 42}, nil)))
//
// IgnoreUnexported
t.Run("IgnoreUnexported", func(tt *testing.T) {
type SType struct {
Public int
private string
}
got := SType{Public: 42, private: "test"}
expected := td.Struct(SType{Public: 42, private: "zip"}, nil)
checkError(tt, got, expected,
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.private"),
Got: mustBe(`"test"`),
Expected: mustBe(`"zip"`),
})
// Ignore unexported globally
defer func() { td.DefaultContextConfig.IgnoreUnexported = false }()
td.DefaultContextConfig.IgnoreUnexported = true
checkOK(tt, got, expected)
td.DefaultContextConfig.IgnoreUnexported = false
ttt := test.NewTestingTB(t.Name())
t := td.NewT(ttt).IgnoreUnexported(SType{}) // ignore only for SType
test.IsTrue(tt, t.Cmp(got, expected))
})
//
// Bad usage
checkError(t, "never tested",
td.Struct("test", nil),
expectedError{
Message: mustBe("bad usage of Struct operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Struct(STRUCT|&STRUCT|nil, EXPECTED_FIELDS), but received string as 1st parameter"),
})
i := 12
checkError(t, "never tested",
td.Struct(&i, nil),
expectedError{
Message: mustBe("bad usage of Struct operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Struct(STRUCT|&STRUCT|nil, EXPECTED_FIELDS), but received *int (ptr) as 1st parameter"),
})
checkError(t, "never tested",
td.Struct(&MyStruct{}, td.StructFields{"UnknownField": 123}),
expectedError{
Message: mustBe("bad usage of Struct operator"),
Path: mustBe("DATA"),
Summary: mustBe(`struct td_test.MyStruct has no field "UnknownField"`),
})
checkError(t, "never tested",
td.Struct(&MyStruct{}, td.StructFields{">\tUnknownField": 123}),
expectedError{
Message: mustBe("bad usage of Struct operator"),
Path: mustBe("DATA"),
Summary: mustBe(`struct td_test.MyStruct has no field "UnknownField" (from ">\tUnknownField")`),
})
checkError(t, &MyStruct{},
td.Struct(&MyStruct{}, td.StructFields{"ValBool": 123}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA.ValBool"),
Got: mustBe("bool"),
Expected: mustBe("int"),
})
checkError(t, &MyStruct{},
td.Struct(&MyStruct{}, td.StructFields{">ValBool": 123}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe(`DATA.ValBool (from ">ValBool")`),
Got: mustBe("bool"),
Expected: mustBe("int"),
})
checkError(t, &MyStruct{},
td.Struct(&MyStruct{}, td.StructFields{"ValBool": nil}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.ValBool"),
Got: mustBe("false"),
Expected: mustBe("nil"),
})
checkError(t, "never tested",
td.Struct(&MyStruct{
MyStructMid: MyStructMid{
MyStructBase: MyStructBase{
ValBool: true,
},
},
},
td.StructFields{"ValBool": false}),
expectedError{
Message: mustBe("bad usage of Struct operator"),
Path: mustBe("DATA"),
Summary: mustBe("non zero field ValBool in model already exists in expectedFields"),
})
//
// String
test.EqualStr(t,
td.Struct(MyStruct{
MyStructMid: MyStructMid{
ValStr: "foobar",
},
ValInt: 123,
},
td.StructFields{
"ValBool": false,
}).String(),
`Struct(td_test.MyStruct{
ValBool: false
ValInt: 123
ValStr: "foobar"
})`)
test.EqualStr(t,
td.Struct(&MyStruct{
MyStructMid: MyStructMid{
ValStr: "foobar",
},
ValInt: 123,
},
td.StructFields{
"ValBool": false,
}).String(),
`Struct(*td_test.MyStruct{
ValBool: false
ValInt: 123
ValStr: "foobar"
})`)
test.EqualStr(t,
td.Struct(&MyStruct{},
td.StructFields{
"ValBool": false,
"= Val*": td.NotZero(),
}).String(),
`Struct(*td_test.MyStruct{
ValBool: false
ValInt (from pattern `+"`= Val*`"+`): NotZero()
ValStr (from pattern `+"`= Val*`"+`): NotZero()
})`)
test.EqualStr(t,
td.Struct(&MyStruct{}, td.StructFields{}).String(),
`Struct(*td_test.MyStruct{})`)
// Erroneous op
test.EqualStr(t, td.Struct("test", nil).String(), "Struct()")
}
func TestStructPrivateFields(t *testing.T) {
type privateKey struct {
num int
name string
}
type privateValue struct {
value string
weight int
}
type MyTime time.Time
type structPrivateFields struct {
byKey map[privateKey]*privateValue
name string
nameb []byte
err error
iface any
properties []int
birth time.Time
birth2 MyTime
next *structPrivateFields
}
d := func(rfc3339Date string) (ret time.Time) {
var err error
ret, err = time.Parse(time.RFC3339Nano, rfc3339Date)
if err != nil {
panic(err)
}
return
}
got := structPrivateFields{
byKey: map[privateKey]*privateValue{
{num: 1, name: "foo"}: {value: "test", weight: 12},
{num: 2, name: "bar"}: {value: "tset", weight: 23},
{num: 3, name: "zip"}: {value: "ttse", weight: 34},
},
name: "foobar",
nameb: []byte("foobar"),
err: errors.New("the error"),
iface: 1234,
properties: []int{20, 22, 23, 21},
birth: d("2018-04-01T10:11:12.123456789Z"),
birth2: MyTime(d("2018-03-01T09:08:07.987654321Z")),
next: &structPrivateFields{
byKey: map[privateKey]*privateValue{},
name: "sub",
iface: bytes.NewBufferString("buffer!"),
birth: d("2018-04-02T10:11:12.123456789Z"),
birth2: MyTime(d("2018-03-02T09:08:07.987654321Z")),
},
}
checkOK(t, got,
td.Struct(structPrivateFields{}, td.StructFields{
"name": "foobar",
}))
checkOK(t, got,
td.Struct(structPrivateFields{}, td.StructFields{
"name": td.Re("^foo"),
}))
checkOK(t, got,
td.Struct(structPrivateFields{}, td.StructFields{
"nameb": td.Re("^foo"),
}))
checkOKOrPanicIfUnsafeDisabled(t, got,
td.Struct(structPrivateFields{}, td.StructFields{
"err": td.Re("error"),
}))
checkError(t, got,
td.Struct(structPrivateFields{}, td.StructFields{
"iface": td.Re("buffer"),
}),
expectedError{
Message: mustBe("bad type"),
Path: mustBe("DATA.iface"),
Got: mustBe("int"),
Expected: mustBe("string (convertible) OR fmt.Stringer OR error OR []uint8"),
})
checkOKOrPanicIfUnsafeDisabled(t, got,
td.Struct(structPrivateFields{}, td.StructFields{
"next": td.Struct(&structPrivateFields{}, td.StructFields{
"iface": td.Re("buffer"),
}),
}))
checkOK(t, got,
td.Struct(structPrivateFields{}, td.StructFields{
"properties": []int{20, 22, 23, 21},
}))
checkOK(t, got,
td.Struct(structPrivateFields{}, td.StructFields{
"properties": td.ArrayEach(td.Between(20, 23)),
}))
checkOK(t, got,
td.Struct(structPrivateFields{}, td.StructFields{
"byKey": td.MapEach(td.Struct(&privateValue{}, td.StructFields{
"weight": td.Between(12, 34),
"value": td.Any(td.HasPrefix("t"), td.HasSuffix("e")),
})),
}))
checkOK(t, got,
td.Struct(structPrivateFields{}, td.StructFields{
"byKey": td.SuperMapOf(
map[privateKey]*privateValue{
{num: 3, name: "zip"}: {value: "ttse", weight: 34},
},
td.MapEntries{
privateKey{num: 2, name: "bar"}: &privateValue{value: "tset", weight: 23},
}),
}))
expected := td.Struct(structPrivateFields{}, td.StructFields{
"birth": td.TruncTime(d("2018-04-01T10:11:12Z"), time.Second),
"birth2": td.TruncTime(MyTime(d("2018-03-01T09:08:07Z")), time.Second),
})
if !dark.UnsafeDisabled {
checkOK(t, got, expected)
} else {
checkError(t, got, expected,
expectedError{
Message: mustBe("cannot compare"),
Path: mustBe("DATA.birth"),
Summary: mustBe("unexported field that cannot be overridden"),
Next: &expectedError{
Message: mustBe("cannot compare"),
Path: mustMatch(`DATA\.(?:Iface\.)?birth2`),
Summary: mustBe("unexported field that cannot be overridden"),
},
})
}
checkError(t, got,
td.Struct(structPrivateFields{}, td.StructFields{
"next": td.Struct(&structPrivateFields{}, td.StructFields{
"name": "sub",
"birth": td.Code(func(t time.Time) bool { return true }),
}),
}),
expectedError{
Message: mustBe("cannot compare unexported field"),
Path: mustBe("DATA.next.birth"),
Summary: mustBe("use Code() on surrounding struct instead"),
})
checkError(t, got,
td.Struct(structPrivateFields{}, td.StructFields{
"next": td.Struct(&structPrivateFields{}, td.StructFields{
"name": "sub",
"birth": td.Smuggle(
func(t time.Time) string { return t.String() },
"2018-04-01T10:11:12.123456789Z"),
}),
}),
expectedError{
Message: mustBe("cannot smuggle unexported field"),
Path: mustBe("DATA.next.birth"),
Summary: mustBe("work on surrounding struct instead"),
})
}
func TestStructPatterns(t *testing.T) {
type paAnon struct {
alphaNum int
betaNum int
}
type paTest struct {
paAnon
Num int
}
got := paTest{
paAnon: paAnon{
alphaNum: 1000,
betaNum: 2000,
},
Num: 666,
}
t.Run("Shell pattern", func(t *testing.T) {
checkOK(t, got,
td.Struct(paTest{Num: 666},
td.StructFields{
"=*Num": td.Gte(1000), // matches alphaNum & betaNum
}))
checkOK(t, got,
td.Struct(paTest{Num: 666},
td.StructFields{
"=a*Num": td.Lt(0), // no remaining fields to match
"=*": td.Gte(1000), // first, matches alphaNum & betaNum
"=b*Num": td.Lt(0), // no remaining fields to match
}),
"Default sorting uses patterns")
checkOK(t, got,
td.Struct(paTest{Num: 666},
td.StructFields{
"1 = a*Num": td.Between(999, 1001), // matches alphaNum
"2 = *": td.Gte(2000), // matches betaNum
"3 = b*Num": td.Gt(3000), // no remaining fields to match
}),
"Explicitly sorted")
checkOK(t, got,
td.Struct(paTest{Num: 666},
td.StructFields{
"1 ! beta*": 1000, // matches alphaNum
"2 = *": 2000, // matches betaNum
}),
"negative shell pattern")
checkError(t, "never tested",
td.Struct(paTest{Num: 666}, td.StructFields{"= al[pha": 123}),
expectedError{
Message: mustBe("bad usage of Struct operator"),
Path: mustBe("DATA"),
Summary: mustContain("bad shell pattern field `= al[pha`: "),
})
checkError(t, paTest{Num: 666},
td.Struct(paTest{Num: 666}, td.StructFields{"= alpha*": nil}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.alphaNum (from pattern `= alpha*`)"),
Got: mustBe("0"),
Expected: mustBe("nil"),
})
})
t.Run("Regexp", func(t *testing.T) {
checkOK(t, got,
td.Struct(paTest{Num: 666},
td.StructFields{
"=~Num$": td.Gte(1000), // matches alphaNum & betaNum
}))
checkOK(t, got,
td.Struct(paTest{Num: 666},
td.StructFields{
"=~^a.*Num$": td.Lt(0), // no remaining fields to match
"=~.": td.Gte(1000), // first, matches alphaNum & betaNum
"=~^b.*Num$": td.Lt(0), // no remaining fields to match
}),
"Default sorting uses patterns")
checkOK(t, got,
td.Struct(paTest{Num: 666},
td.StructFields{
"1 =~ ^a.*Num$": td.Between(999, 1001), // matches alphaNum
"2 =~ .": td.Gte(2000), // matches betaNum
"3 =~ ^b.*Num$": td.Gt(3000), // no remaining fields to match
}),
"Explicitly sorted")
checkOK(t, got,
td.Struct(paTest{Num: 666},
td.StructFields{
"1 !~ ^beta": 1000, // matches alphaNum
"2 =~ .": 2000, // matches betaNum
}),
"negative regexp")
checkError(t, "never tested",
td.Struct(paTest{Num: 666}, td.StructFields{"=~ al(*": 123}),
expectedError{
Message: mustBe("bad usage of Struct operator"),
Path: mustBe("DATA"),
Summary: mustContain("bad regexp field `=~ al(*`: "),
})
checkError(t, paTest{Num: 666},
td.Struct(paTest{Num: 666}, td.StructFields{"=~ alpha": nil}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.alphaNum (from pattern `=~ alpha`)"),
Got: mustBe("0"),
Expected: mustBe("nil"),
})
})
}
func TestStructTypeBehind(t *testing.T) {
equalTypes(t, td.Struct(MyStruct{}, nil), MyStruct{})
equalTypes(t, td.Struct(&MyStruct{}, nil), &MyStruct{})
// Erroneous op
equalTypes(t, td.Struct("test", nil), nil)
}
func TestSStruct(t *testing.T) {
gotStruct := MyStruct{
MyStructMid: MyStructMid{
MyStructBase: MyStructBase{
ValBool: true,
},
ValStr: "foobar",
},
ValInt: 123,
}
//
// Using pointer
checkOK(t, &gotStruct,
td.SStruct(&MyStruct{}, td.StructFields{
"ValBool": true,
"ValStr": "foobar",
"ValInt": 123,
// nil Ptr
}))
checkOK(t, &gotStruct,
td.SStruct(
&MyStruct{
MyStructMid: MyStructMid{
ValStr: "zip",
},
ValInt: 666,
},
td.StructFields{
"ValBool": true,
"> ValStr": "foobar",
">ValInt": 123,
}))
checkOK(t, &gotStruct,
td.SStruct((*MyStruct)(nil), td.StructFields{
"ValBool": true,
"ValStr": "foobar",
"ValInt": 123,
// nil Ptr
}))
checkError(t, 123,
td.SStruct(&MyStruct{}, td.StructFields{}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustContain("int"),
Expected: mustContain("*td_test.MyStruct"),
})
checkError(t, &MyStructBase{},
td.SStruct(&MyStruct{}, td.StructFields{}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustContain("*td_test.MyStructBase"),
Expected: mustContain("*td_test.MyStruct"),
})
checkError(t, &gotStruct,
td.SStruct(&MyStruct{}, td.StructFields{
// ValBool false ← does not match
"ValStr": "foobar",
"ValInt": 123,
}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.ValBool"),
Got: mustContain("true"),
Expected: mustContain("false"),
})
checkOK(t, &gotStruct,
td.SStruct(&MyStruct{
MyStructMid: MyStructMid{
MyStructBase: MyStructBase{
ValBool: true,
},
ValStr: "foobar",
},
ValInt: 123,
}, nil))
checkError(t, &gotStruct,
td.SStruct(&MyStruct{
MyStructMid: MyStructMid{
MyStructBase: MyStructBase{
ValBool: true,
},
ValStr: "foobax", // ← does not match
},
ValInt: 123,
}, nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.ValStr"),
Got: mustContain("foobar"),
Expected: mustContain("foobax"),
})
// Zero values
checkOK(t, &MyStruct{}, td.SStruct(&MyStruct{}, nil))
checkOK(t, &MyStruct{}, td.SStruct(&MyStruct{}, td.StructFields{}))
// nil cases
checkError(t, nil, td.SStruct(&MyStruct{}, nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustContain("nil"),
Expected: mustContain("*td_test.MyStruct"),
})
checkError(t, (*MyStruct)(nil), td.SStruct(&MyStruct{}, nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustContain("nil"),
Expected: mustBe("non-nil"),
})
//
// Without pointer
checkOK(t, gotStruct,
td.SStruct(MyStruct{}, td.StructFields{
"ValBool": true,
"ValStr": "foobar",
"ValInt": 123,
}))
checkOK(t, gotStruct,
td.SStruct(
MyStruct{
MyStructMid: MyStructMid{
ValStr: "zip",
},
ValInt: 666,
},
td.StructFields{
"ValBool": true,
"> ValStr": "foobar",
">ValInt": 123,
}))
checkError(t, 123, td.SStruct(MyStruct{}, td.StructFields{}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustContain("int"),
Expected: mustContain("td_test.MyStruct"),
})
checkError(t, gotStruct,
td.SStruct(MyStruct{}, td.StructFields{
// "ValBool" false ← does not match
"ValStr": "foobar",
"ValInt": 123,
}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.ValBool"),
Got: mustContain("true"),
Expected: mustContain("false"),
})
checkOK(t, gotStruct,
td.SStruct(MyStruct{
MyStructMid: MyStructMid{
MyStructBase: MyStructBase{
ValBool: true,
},
ValStr: "foobar",
},
ValInt: 123,
}, nil))
checkError(t, gotStruct,
td.SStruct(MyStruct{
MyStructMid: MyStructMid{
MyStructBase: MyStructBase{
ValBool: true,
},
ValStr: "foobax", // ← does not match
},
ValInt: 123,
}, nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.ValStr"),
Got: mustContain("foobar"),
Expected: mustContain("foobax"),
})
// Zero values
checkOK(t, MyStruct{}, td.Struct(MyStruct{}, td.StructFields{}))
checkOK(t, MyStruct{}, td.Struct(MyStruct{}, nil))
// nil cases
checkError(t, nil, td.SStruct(MyStruct{}, nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustContain("nil"),
Expected: mustContain("td_test.MyStruct"),
})
checkError(t, (*MyStruct)(nil), td.SStruct(MyStruct{}, nil),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("*td_test.MyStruct"),
Expected: mustBe("td_test.MyStruct"),
})
//
// Be lax...
type Struct1 struct {
name string
age int
}
type Struct2 struct {
name string
age int
}
// Without Lax → error
checkError(t,
Struct1{name: "Bob", age: 42},
td.SStruct(Struct2{name: "Bob", age: 42}, nil),
expectedError{
Message: mustBe("type mismatch"),
})
// With Lax → OK
checkOK(t,
Struct1{name: "Bob", age: 42},
td.Lax(td.SStruct(Struct2{name: "Bob", age: 42}, nil)))
//
// IgnoreUnexported
t.Run("IgnoreUnexported", func(tt *testing.T) {
type SType struct {
Public int
private string
}
got := SType{Public: 42, private: "test"}
expected := td.SStruct(SType{Public: 42}, nil)
checkError(tt, got, expected,
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.private"),
Got: mustBe(`"test"`),
Expected: mustBe(`""`),
})
// Ignore unexported globally
defer func() { td.DefaultContextConfig.IgnoreUnexported = false }()
td.DefaultContextConfig.IgnoreUnexported = true
checkOK(tt, got, expected)
td.DefaultContextConfig.IgnoreUnexported = false
ttt := test.NewTestingTB(t.Name())
t := td.NewT(ttt).IgnoreUnexported(SType{}) // ignore only for SType
test.IsTrue(tt, t.Cmp(got, expected))
})
//
// Bad usage
checkError(t, "never tested",
td.SStruct("test", nil),
expectedError{
Message: mustBe("bad usage of SStruct operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: SStruct(STRUCT|&STRUCT|nil, EXPECTED_FIELDS), but received string as 1st parameter"),
})
i := 12
checkError(t, "never tested",
td.SStruct(&i, nil),
expectedError{
Message: mustBe("bad usage of SStruct operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: SStruct(STRUCT|&STRUCT|nil, EXPECTED_FIELDS), but received *int (ptr) as 1st parameter"),
})
checkError(t, "never tested",
td.SStruct(&MyStruct{}, td.StructFields{"UnknownField": 123}),
expectedError{
Message: mustBe("bad usage of SStruct operator"),
Path: mustBe("DATA"),
Summary: mustBe(`struct td_test.MyStruct has no field "UnknownField"`),
})
checkError(t, "never tested",
td.SStruct(&MyStruct{}, td.StructFields{">\tUnknownField": 123}),
expectedError{
Message: mustBe("bad usage of SStruct operator"),
Path: mustBe("DATA"),
Summary: mustBe(`struct td_test.MyStruct has no field "UnknownField" (from ">\tUnknownField")`),
})
checkError(t, &MyStruct{},
td.SStruct(&MyStruct{}, td.StructFields{"ValBool": 123}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA.ValBool"),
Got: mustBe("bool"),
Expected: mustBe("int"),
})
checkError(t, &MyStruct{},
td.SStruct(&MyStruct{}, td.StructFields{">ValBool": 123}),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe(`DATA.ValBool (from ">ValBool")`),
Got: mustBe("bool"),
Expected: mustBe("int"),
})
checkError(t, &MyStruct{},
td.SStruct(&MyStruct{}, td.StructFields{"ValBool": nil}),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.ValBool"),
Got: mustBe("false"),
Expected: mustBe("nil"),
})
checkError(t, "never tested",
td.SStruct(&MyStruct{
MyStructMid: MyStructMid{
MyStructBase: MyStructBase{
ValBool: true,
},
},
},
td.StructFields{"ValBool": false}),
expectedError{
Message: mustBe("bad usage of SStruct operator"),
Path: mustBe("DATA"),
Summary: mustBe("non zero field ValBool in model already exists in expectedFields"),
})
//
// String
test.EqualStr(t,
td.SStruct(MyStruct{
MyStructMid: MyStructMid{
ValStr: "foobar",
},
ValInt: 123,
},
td.StructFields{
"ValBool": false,
}).String(),
`SStruct(td_test.MyStruct{
Ptr: (*int)()
ValBool: false
ValInt: 123
ValStr: "foobar"
})`)
test.EqualStr(t,
td.SStruct(&MyStruct{
MyStructMid: MyStructMid{
ValStr: "foobar",
},
ValInt: 123,
},
td.StructFields{
"ValBool": false,
}).String(),
`SStruct(*td_test.MyStruct{
Ptr: (*int)()
ValBool: false
ValInt: 123
ValStr: "foobar"
})`)
test.EqualStr(t,
td.SStruct(&MyStruct{}, td.StructFields{}).String(),
`SStruct(*td_test.MyStruct{
Ptr: (*int)()
ValBool: false
ValInt: 0
ValStr: ""
})`)
// Erroneous op
test.EqualStr(t, td.SStruct("test", nil).String(), "SStruct()")
}
func TestSStructPattern(t *testing.T) {
// Patterns are already fully tested in TestStructPatterns
type paAnon struct {
alphaNum int
betaNum int
}
type paTest struct {
paAnon
Num int
}
got := paTest{
paAnon: paAnon{
alphaNum: 1000,
betaNum: 2000,
},
Num: 666,
}
checkOK(t, got,
td.SStruct(paTest{},
td.StructFields{
"=*Num": td.Gte(666), // matches Num, alphaNum & betaNum
}))
checkOK(t, got,
td.SStruct(paTest{},
td.StructFields{
"=~Num$": td.Gte(666), // matches Num, alphaNum & betaNum
}))
checkOK(t, paTest{Num: 666},
td.SStruct(paTest{},
td.StructFields{
"=~^Num": 666, // only matches Num
// remaining fields are tested as 0
}))
}
func TestSStructTypeBehind(t *testing.T) {
equalTypes(t, td.SStruct(MyStruct{}, nil), MyStruct{})
equalTypes(t, td.SStruct(&MyStruct{}, nil), &MyStruct{})
// Erroneous op
equalTypes(t, td.SStruct("test", nil), nil)
}
go-testdeep-1.15.0/td/td_tag.go 0000664 0000000 0000000 00000004551 15144170453 0016227 0 ustar 00root root 0000000 0000000 // 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 td
import (
"reflect"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/util"
)
type tdTag struct {
tdSmugglerBase
tag string
}
var _ TestDeep = &tdTag{}
// summary(Tag): names an operator or a value. Only useful as a
// parameter of JSON operator, to name placeholders
// input(Tag): all
// Tag is a smuggler operator. It only allows to name expectedValue,
// which can be an operator or a value. The data is then compared
// against expectedValue as if Tag was never called. It is only useful
// as [JSON] operator parameter, to name placeholders. See [JSON]
// operator for more details.
//
// td.Cmp(t, gotValue,
// td.JSON(`{"fullname": $name, "age": $age, "gender": $gender}`,
// td.Tag("name", td.HasPrefix("Foo")), // matches $name
// td.Tag("age", td.Between(41, 43)), // matches $age
// td.Tag("gender", "male"))) // matches $gender
//
// TypeBehind method is delegated to expectedValue one if
// expectedValue is a [TestDeep] operator, otherwise it returns the
// type of expectedValue (or nil if it is originally untyped nil).
func Tag(tag string, expectedValue any) TestDeep {
t := tdTag{
tdSmugglerBase: newSmugglerBase(expectedValue),
tag: tag,
}
if err := util.CheckTag(tag); err != nil {
t.err = ctxerr.OpBad("Tag", err.Error())
return &t
}
if !t.isTestDeeper {
t.expectedValue = reflect.ValueOf(expectedValue)
}
return &t
}
func (t *tdTag) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if t.err != nil {
return ctx.CollectError(t.err)
}
return deepValueEqual(ctx, got, t.expectedValue)
}
func (t *tdTag) HandleInvalid() bool {
return true // Knows how to handle untyped nil values (aka invalid values)
}
func (t *tdTag) String() string {
if t.err != nil {
return t.stringError()
}
if t.isTestDeeper {
return t.expectedValue.Interface().(TestDeep).String()
}
return util.ToString(t.expectedValue)
}
func (t *tdTag) TypeBehind() reflect.Type {
if t.err != nil {
return nil
}
if t.isTestDeeper {
return t.expectedValue.Interface().(TestDeep).TypeBehind()
}
if t.expectedValue.IsValid() {
return t.expectedValue.Type()
}
return nil
}
go-testdeep-1.15.0/td/td_tag_test.go 0000664 0000000 0000000 00000003255 15144170453 0017266 0 ustar 00root root 0000000 0000000 // 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 td_test
import (
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/internal/util"
"github.com/maxatome/go-testdeep/td"
)
func TestTag(t *testing.T) {
// expected value
checkOK(t, 12, td.Tag("number", 12))
checkOK(t, nil, td.Tag("number", nil))
checkError(t, 8, td.Tag("number", 9),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("8"),
Expected: mustBe("9"),
})
// expected operator
checkOK(t, 12, td.Tag("number", td.Between(9, 13)))
checkError(t, 8, td.Tag("number", td.Between(9, 13)),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("8"),
Expected: mustBe("9 ≤ got ≤ 13"),
})
//
// Bad usage
checkError(t, "never tested",
td.Tag("1badTag", td.Between(9, 13)),
expectedError{
Message: mustBe("bad usage of Tag operator"),
Path: mustBe("DATA"),
Summary: mustBe(util.ErrTagInvalid.Error()),
})
//
// String
test.EqualStr(t,
td.Tag("foo", td.Gt(4)).String(),
td.Gt(4).String())
test.EqualStr(t, td.Tag("foo", 8).String(), "8")
test.EqualStr(t, td.Tag("foo", nil).String(), "nil")
// Erroneous op
test.EqualStr(t, td.Tag("1badTag", 12).String(), "Tag()")
}
func TestTagTypeBehind(t *testing.T) {
equalTypes(t, td.Tag("foo", 8), 0)
equalTypes(t, td.Tag("foo", td.Gt(4)), 0)
equalTypes(t, td.Tag("foo", nil), nil)
// Erroneous op
equalTypes(t, td.Tag("1badTag", 12), nil)
}
go-testdeep-1.15.0/td/td_trunc_time.go 0000664 0000000 0000000 00000007232 15144170453 0017624 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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"
"time"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/types"
)
type tdTruncTime struct {
tdExpectedType
expectedTime time.Time
trunc time.Duration
}
var _ TestDeep = &tdTruncTime{}
// summary(TruncTime): compares time.Time (or assignable) values after
// truncating them
// input(TruncTime): struct(time.Time),ptr(todo)
// TruncTime operator compares [time.Time] (or assignable) values
// after truncating them to the optional trunc duration. See
// [time.Time.Truncate] for details about the truncation.
//
// If trunc is missing, it defaults to 0.
//
// During comparison, location does not matter as [time.Time.Equal]
// method is used behind the scenes: a time instant in two different
// locations is the same time instant.
//
// Whatever the trunc value is, the monotonic clock is stripped
// before the comparison against expectedTime.
//
// gotDate := time.Date(2018, time.March, 9, 1, 2, 3, 999999999, time.UTC).
// In(time.FixedZone("UTC+2", 2))
//
// expected := time.Date(2018, time.March, 9, 1, 2, 3, 0, time.UTC)
//
// td.Cmp(t, gotDate, td.TruncTime(expected)) // fails, ns differ
// td.Cmp(t, gotDate, td.TruncTime(expected, time.Second)) // succeeds
//
// TypeBehind method returns the [reflect.Type] of expectedTime.
func TruncTime(expectedTime any, trunc ...time.Duration) TestDeep {
const usage = "(time.Time[, time.Duration])"
t := tdTruncTime{
tdExpectedType: tdExpectedType{
base: newBase(3),
},
}
if len(trunc) > 1 {
t.err = ctxerr.OpTooManyParams("TruncTime", usage)
return &t
}
if len(trunc) == 1 {
t.trunc = trunc[0]
}
vval := reflect.ValueOf(expectedTime)
t.expectedType = vval.Type()
if t.expectedType == types.Time {
t.expectedTime = expectedTime.(time.Time).Truncate(t.trunc)
return &t
}
if !t.expectedType.ConvertibleTo(types.Time) { // 1.17 ok as time.Time is a struct
t.err = ctxerr.OpBad("TruncTime", "usage: TruncTime%s, 1st parameter must be time.Time or convertible to time.Time, but not %T",
usage, expectedTime)
return &t
}
t.expectedTime = vval.Convert(types.Time).
Interface().(time.Time).Truncate(t.trunc)
return &t
}
func (t *tdTruncTime) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if t.err != nil {
return ctx.CollectError(t.err)
}
err := t.checkType(ctx, got)
if err != nil {
return ctx.CollectError(err)
}
gotTime, err := getTime(ctx, got, got.Type() != types.Time)
if err != nil {
return ctx.CollectError(err)
}
gotTimeTrunc := gotTime.Truncate(t.trunc)
if gotTimeTrunc.Equal(t.expectedTime) {
return nil
}
// Fail
if ctx.BooleanError {
return ctxerr.BooleanError
}
var gotRawStr, gotTruncStr string
if t.expectedType != types.Time &&
t.expectedType.Implements(types.FmtStringer) {
gotRawStr = got.Interface().(fmt.Stringer).String()
gotTruncStr = reflect.ValueOf(gotTimeTrunc).Convert(t.expectedType).
Interface().(fmt.Stringer).String()
} else {
gotRawStr = gotTime.String()
gotTruncStr = gotTimeTrunc.String()
}
return ctx.CollectError(&ctxerr.Error{
Message: "values differ",
Got: types.RawString(gotRawStr + "\ntruncated to:\n" + gotTruncStr),
Expected: t,
})
}
func (t *tdTruncTime) String() string {
if t.err != nil {
return t.stringError()
}
if t.expectedType.Implements(types.FmtStringer) {
return reflect.ValueOf(t.expectedTime).Convert(t.expectedType).
Interface().(fmt.Stringer).String()
}
return t.expectedTime.String()
}
go-testdeep-1.15.0/td/td_trunc_time_test.go 0000664 0000000 0000000 00000011715 15144170453 0020664 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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"
"time"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
type (
MyTime time.Time
MyTimeStr time.Time
)
func (t MyTimeStr) String() string {
return "<<" + time.Time(t).Format(time.RFC3339Nano) + ">>"
}
func TestTruncTime(t *testing.T) {
//
// Monotonic
now := time.Now()
nowWithoutMono := now.Truncate(0)
// If monotonic clock available, check without TruncTime()
if now != nowWithoutMono {
// OK now contains a monotonic part != 0, so fail coz "==" used inside
checkError(t, now, nowWithoutMono,
expectedError{
Message: mustBe("values differ"),
Path: mustContain("DATA"),
Next: ignoreExpectedError,
})
}
checkOK(t, now, td.TruncTime(nowWithoutMono))
//
// time.Time
gotDate := time.Date(2018, time.March, 9, 1, 2, 3, 4, time.UTC)
// Time zone / location does not matter
UTCp2 := time.FixedZone("UTC+2", 2)
UTCm2 := time.FixedZone("UTC-2", 2)
checkOK(t, gotDate, td.TruncTime(gotDate.In(UTCp2)))
checkOK(t, gotDate, td.TruncTime(gotDate.In(UTCm2)))
checkOK(t, gotDate.In(UTCm2), td.TruncTime(gotDate.In(UTCp2)))
checkOK(t, gotDate.In(UTCp2), td.TruncTime(gotDate.In(UTCm2)))
expDate := gotDate
checkOK(t, gotDate, td.TruncTime(expDate))
checkOK(t, gotDate, td.TruncTime(expDate, time.Second))
checkOK(t, gotDate, td.TruncTime(expDate, time.Minute))
expDate = expDate.Add(time.Second)
checkError(t, gotDate, td.TruncTime(expDate, time.Second),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("2018-03-09 01:02:03.000000004 +0000 UTC\n" +
"truncated to:\n" +
"2018-03-09 01:02:03 +0000 UTC"),
Expected: mustBe("2018-03-09 01:02:04 +0000 UTC"),
})
checkOK(t, gotDate, td.TruncTime(expDate, time.Minute))
checkError(t, gotDate, td.TruncTime(MyTime(gotDate)),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("time.Time"),
Expected: mustBe("td_test.MyTime"),
})
//
// Type convertible to time.Time NOT implementing fmt.Stringer
gotMyDate := MyTime(gotDate)
expMyDate := MyTime(gotDate)
checkOK(t, gotMyDate, td.TruncTime(expMyDate))
checkOK(t, gotMyDate, td.TruncTime(expMyDate, time.Second))
checkOK(t, gotMyDate, td.TruncTime(expMyDate, time.Minute))
expMyDate = MyTime(gotDate.Add(time.Second))
checkError(t, gotMyDate, td.TruncTime(expMyDate, time.Second),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("2018-03-09 01:02:03.000000004 +0000 UTC\n" +
"truncated to:\n" +
"2018-03-09 01:02:03 +0000 UTC"),
Expected: mustBe("2018-03-09 01:02:04 +0000 UTC"),
})
checkOK(t, gotMyDate, td.TruncTime(expMyDate, time.Minute))
checkError(t, MyTime(gotDate), td.TruncTime(gotDate),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("td_test.MyTime"),
Expected: mustBe("time.Time"),
})
//
// Type convertible to time.Time implementing fmt.Stringer
gotMyStrDate := MyTimeStr(gotDate)
expMyStrDate := MyTimeStr(gotDate)
checkOK(t, gotMyStrDate, td.TruncTime(expMyStrDate))
checkOK(t, gotMyStrDate, td.TruncTime(expMyStrDate, time.Second))
checkOK(t, gotMyStrDate, td.TruncTime(expMyStrDate, time.Minute))
expMyStrDate = MyTimeStr(gotDate.Add(time.Second))
checkError(t, gotMyStrDate, td.TruncTime(expMyStrDate, time.Second),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("<<2018-03-09T01:02:03.000000004Z>>\n" +
"truncated to:\n" +
"<<2018-03-09T01:02:03Z>>"),
Expected: mustBe("<<2018-03-09T01:02:04Z>>"),
})
checkOK(t, gotMyStrDate, td.TruncTime(expMyStrDate, time.Minute))
checkError(t, MyTimeStr(gotDate), td.TruncTime(gotDate),
expectedError{
Message: mustBe("type mismatch"),
Path: mustBe("DATA"),
Got: mustBe("td_test.MyTimeStr"),
Expected: mustBe("time.Time"),
})
//
// Bad usage
checkError(t, "never tested",
td.TruncTime("test"),
expectedError{
Message: mustBe("bad usage of TruncTime operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: TruncTime(time.Time[, time.Duration]), 1st parameter must be time.Time or convertible to time.Time, but not string"),
})
checkError(t, "never tested",
td.TruncTime(1, 2, 3),
expectedError{
Message: mustBe("bad usage of TruncTime operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: TruncTime(time.Time[, time.Duration]), too many parameters"),
})
// Erroneous op
test.EqualStr(t, td.TruncTime("test").String(), "TruncTime()")
}
func TestTruncTimeTypeBehind(t *testing.T) {
type MyTime time.Time
equalTypes(t, td.TruncTime(time.Time{}), time.Time{})
equalTypes(t, td.TruncTime(MyTime{}), MyTime{})
// Erroneous op
equalTypes(t, td.TruncTime("test"), nil)
}
go-testdeep-1.15.0/td/td_zero.go 0000664 0000000 0000000 00000005073 15144170453 0016433 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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"
)
type tdZero struct {
baseOKNil
}
var _ TestDeep = &tdZero{}
// summary(Zero): checks data against its zero'ed conterpart
// input(Zero): all
// Zero operator checks that data is zero regarding its type.
//
// - nil is the zero value of pointers, maps, slices, channels and functions;
// - 0 is the zero value of numbers;
// - "" is the 0 value of strings;
// - false is the zero value of booleans;
// - zero value of structs is the struct with no fields initialized.
//
// Beware that:
//
// td.Cmp(t, AnyStruct{}, td.Zero()) // is true
// td.Cmp(t, &AnyStruct{}, td.Zero()) // is false, coz pointer ≠ nil
// td.Cmp(t, &AnyStruct{}, td.Ptr(td.Zero())) // is true
//
// See also [Empty], [Nil] and [NotZero].
func Zero() TestDeep {
return &tdZero{
baseOKNil: newBaseOKNil(3),
}
}
func (z *tdZero) Match(ctx ctxerr.Context, got reflect.Value) (err *ctxerr.Error) {
// nil case
if !got.IsValid() {
return nil
}
return deepValueEqual(ctx, got, reflect.New(got.Type()).Elem())
}
func (z *tdZero) String() string {
return "Zero()"
}
type tdNotZero struct {
baseOKNil
}
var _ TestDeep = &tdNotZero{}
// summary(NotZero): checks that data is not zero regarding its type
// input(NotZero): all
// NotZero operator checks that data is not zero regarding its type.
//
// - nil is the zero value of pointers, maps, slices, channels and functions;
// - 0 is the zero value of numbers;
// - "" is the 0 value of strings;
// - false is the zero value of booleans;
// - zero value of structs is the struct with no fields initialized.
//
// Beware that:
//
// td.Cmp(t, AnyStruct{}, td.NotZero()) // is false
// td.Cmp(t, &AnyStruct{}, td.NotZero()) // is true, coz pointer ≠ nil
// td.Cmp(t, &AnyStruct{}, td.Ptr(td.NotZero())) // is false
//
// See also [NotEmpty], [NotNil] and [Zero].
func NotZero() TestDeep {
return &tdNotZero{
baseOKNil: newBaseOKNil(3),
}
}
func (z *tdNotZero) Match(ctx ctxerr.Context, got reflect.Value) (err *ctxerr.Error) {
if got.IsValid() && !deepValueEqualOK(got, reflect.New(got.Type()).Elem()) {
return nil
}
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "zero value",
Got: got,
Expected: z,
})
}
func (z *tdNotZero) String() string {
return "NotZero()"
}
go-testdeep-1.15.0/td/td_zero_test.go 0000664 0000000 0000000 00000012226 15144170453 0017470 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-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 TestZero(t *testing.T) {
checkOK(t, 0, td.Zero())
checkOK(t, int64(0), td.Zero())
checkOK(t, float64(0), td.Zero())
checkOK(t, nil, td.Zero())
checkOK(t, (map[string]int)(nil), td.Zero())
checkOK(t, ([]int)(nil), td.Zero())
checkOK(t, [3]int{}, td.Zero())
checkOK(t, MyStruct{}, td.Zero())
checkOK(t, (*MyStruct)(nil), td.Zero())
checkOK(t, &MyStruct{}, td.Ptr(td.Zero()))
checkOK(t, (chan int)(nil), td.Zero())
checkOK(t, (func())(nil), td.Zero())
checkOK(t, false, td.Zero())
checkError(t, 12, td.Zero(),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("12"),
Expected: mustBe("0"),
})
checkError(t, int64(12), td.Zero(),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("(int64) 12"),
Expected: mustBe("(int64) 0"),
})
checkError(t, float64(12), td.Zero(),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("12.0"),
Expected: mustBe("0.0"),
})
checkError(t, map[string]int{}, td.Zero(),
expectedError{
Message: mustBe("nil map"),
Path: mustBe("DATA"),
Got: mustBe("not nil"),
Expected: mustBe("nil"),
})
checkError(t, []int{}, td.Zero(),
expectedError{
Message: mustBe("nil slice"),
Path: mustBe("DATA"),
Got: mustBe("not nil"),
Expected: mustBe("nil"),
})
checkError(t, [3]int{0, 12}, td.Zero(),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[1]"),
Got: mustBe("12"),
Expected: mustBe("0"),
})
checkError(t, MyStruct{ValInt: 12}, td.Zero(),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA.ValInt"),
Got: mustBe("12"),
Expected: mustBe("0"),
})
checkError(t, &MyStruct{}, td.Zero(),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("*DATA"),
// in fact, pointer on 0'ed struct contents
Got: mustBe("(td_test.MyStruct) {\n}"),
Expected: mustBe("nil"),
})
checkError(t, true, td.Zero(),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("true"),
Expected: mustBe("false"),
})
//
// String
test.EqualStr(t, td.Zero().String(), "Zero()")
}
func TestNotZero(t *testing.T) {
checkOK(t, 12, td.NotZero())
checkOK(t, int64(12), td.NotZero())
checkOK(t, float64(12), td.NotZero())
checkOK(t, map[string]int{}, td.NotZero())
checkOK(t, []int{}, td.NotZero())
checkOK(t, [3]int{1}, td.NotZero())
checkOK(t, MyStruct{ValInt: 1}, td.NotZero())
checkOK(t, &MyStruct{}, td.NotZero())
checkOK(t, make(chan int), td.NotZero())
checkOK(t, func() {}, td.NotZero())
checkOK(t, true, td.NotZero())
checkError(t, nil, td.NotZero(),
expectedError{
Message: mustBe("zero value"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("NotZero()"),
})
checkError(t, 0, td.NotZero(),
expectedError{
Message: mustBe("zero value"),
Path: mustBe("DATA"),
Got: mustBe("0"),
Expected: mustBe("NotZero()"),
})
checkError(t, int64(0), td.NotZero(),
expectedError{
Message: mustBe("zero value"),
Path: mustBe("DATA"),
Got: mustBe("(int64) 0"),
Expected: mustBe("NotZero()"),
})
checkError(t, float64(0), td.NotZero(),
expectedError{
Message: mustBe("zero value"),
Path: mustBe("DATA"),
Got: mustBe("0.0"),
Expected: mustBe("NotZero()"),
})
checkError(t, (map[string]int)(nil), td.NotZero(),
expectedError{
Message: mustBe("zero value"),
Path: mustBe("DATA"),
Got: mustBe("(map[string]int) "),
Expected: mustBe("NotZero()"),
})
checkError(t, ([]int)(nil), td.NotZero(),
expectedError{
Message: mustBe("zero value"),
Path: mustBe("DATA"),
Got: mustBe("([]int) "),
Expected: mustBe("NotZero()"),
})
checkError(t, [3]int{}, td.NotZero(),
expectedError{
Message: mustBe("zero value"),
Path: mustBe("DATA"),
Got: mustContain("0"),
Expected: mustBe("NotZero()"),
})
checkError(t, MyStruct{}, td.NotZero(),
expectedError{
Message: mustBe("zero value"),
Path: mustBe("DATA"),
Got: mustBe("(td_test.MyStruct) {\n}"),
Expected: mustBe("NotZero()"),
})
checkError(t, &MyStruct{}, td.Ptr(td.NotZero()),
expectedError{
Message: mustBe("zero value"),
Path: mustBe("*DATA"),
// in fact, pointer on 0'ed struct contents
Got: mustBe("(td_test.MyStruct) {\n}"),
Expected: mustBe("NotZero()"),
})
checkError(t, false, td.NotZero(),
expectedError{
Message: mustBe("zero value"),
Path: mustBe("DATA"),
Got: mustBe("false"),
Expected: mustBe("NotZero()"),
})
//
// String
test.EqualStr(t, td.NotZero().String(), "NotZero()")
}
func TestZeroTypeBehind(t *testing.T) {
equalTypes(t, td.Zero(), nil)
equalTypes(t, td.NotZero(), nil)
}
go-testdeep-1.15.0/td/tuple.go 0000664 0000000 0000000 00000003164 15144170453 0016115 0 ustar 00root root 0000000 0000000 // Copyright (c) 2020, 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/flat"
)
var tupleType = reflect.TypeOf(tuple{})
// A Tuple is an immutable container. It is used to easily compare
// several values at once, typically when a function returns several
// values:
//
// price := func(p float64) (float64, string, error) {
// if p < 0 {
// return 0, "", errors.New("negative price not supported")
// }
// return p * 1.2, "€", nil
// }
//
// td.Cmp(t,
// td.TupleFrom(price(10)),
// td.TupleFrom(float64(12), "€", nil),
// )
//
// td.Cmp(t,
// td.TupleFrom(price(-10)),
// td.TupleFrom(float64(0), "", td.Not(nil)),
// )
//
// Once initialized with [TupleFrom], a Tuple is immutable.
type Tuple interface {
// Len returns t length, aka the number of items the tuple contains.
Len() int
// Index returns t's i'th element. It panics if i is out of range.
Index(int) any
}
// TupleFrom returns a new [Tuple] initialized to the values of vals.
//
// td.TupleFrom(float64(0), "", td.Not(nil))
//
// [Flatten] can be used to flatten non-[]any slice/array into a
// new [Tuple]:
//
// ints := []int64{1, 2, 3}
// td.TupleFrom(td.Flatten(ints), "OK", nil)
//
// is the same as:
//
// td.TupleFrom(int64(1), int64(2), int64(3), "OK", nil)
func TupleFrom(vals ...any) Tuple {
return tuple(flat.Interfaces(vals...))
}
type tuple []any
func (t tuple) Len() int {
return len(t)
}
func (t tuple) Index(i int) any {
return t[i]
}
go-testdeep-1.15.0/td/tuple_test.go 0000664 0000000 0000000 00000002325 15144170453 0017152 0 ustar 00root root 0000000 0000000 // Copyright (c) 2020, 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 (
"errors"
"testing"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestTuple(t *testing.T) {
multi := func() (a int, b string, err error) {
return 12, "test", errors.New("err")
}
tuple := td.TupleFrom(multi())
test.EqualInt(t, tuple.Len(), 3)
test.EqualInt(t, tuple.Index(0).(int), 12)
test.EqualStr(t, tuple.Index(1).(string), "test")
test.EqualStr(t, tuple.Index(2).(error).Error(), "err")
td.Cmp(t,
td.TupleFrom(multi()),
td.TupleFrom(12, "test", td.Not(nil)),
)
price := func(p float64) (float64, string, error) {
if p < 0 {
return 0, "", errors.New("negative price not supported")
}
return p * 1.2, "€", nil
}
td.Cmp(t,
td.TupleFrom(price(10)),
td.TupleFrom(float64(12), "€", nil),
)
td.Cmp(t,
td.TupleFrom(price(-10)),
td.TupleFrom(float64(0), "", td.Not(nil)),
)
// With Flatten
td.Cmp(t,
td.TupleFrom(td.Flatten([]int64{1, 2, 3}), "OK", nil),
td.TupleFrom(int64(1), int64(2), int64(3), "OK", nil),
)
}
go-testdeep-1.15.0/td/types.go 0000664 0000000 0000000 00000012346 15144170453 0016132 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2021, 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"
"strings"
"testing"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/location"
"github.com/maxatome/go-testdeep/internal/types"
)
var (
tType = reflect.TypeOf((*T)(nil))
testDeeper = reflect.TypeOf((*TestDeep)(nil)).Elem()
smuggledGotType = reflect.TypeOf(SmuggledGot{})
smuggledGotPtrType = reflect.TypeOf((*SmuggledGot)(nil))
recvKindType = reflect.TypeOf(RecvNothing)
)
// TestingT is the minimal interface used by [Cmp] to report errors. It
// is commonly implemented by [*testing.T] and [*testing.B].
type TestingT interface {
Error(args ...any)
Fatal(args ...any)
Helper()
}
// TestingFT is a deprecated alias of [testing.TB]. Use [testing.TB]
// directly in new code.
type TestingFT = testing.TB
// TestDeep is the representation of a [go-testdeep operator]. It is not
// intended to be used directly, but through Cmp* functions.
//
// [go-testdeep operator]: https://go-testdeep.zetta.rocks/operators/
type TestDeep interface {
types.TestDeepStringer
location.GetLocationer
// Match checks got against the operator. It returns nil if it matches.
Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error
setLocation(int)
replaceLocation(location.Location)
// HandleInvalid returns true if the operator is able to handle
// untyped nil value. Otherwise the untyped nil value is handled
// generically.
HandleInvalid() bool
// TypeBehind returns the type handled by the operator or nil if it
// is not known. tdhttp helper uses it to know how to unmarshal HTTP
// responses bodies before comparing them using the operator.
TypeBehind() reflect.Type
// Error returns nil if the operator is operational, the
// corresponding error otherwise.
Error() error
}
// base is a base type providing some methods needed by the TestDeep
// interface.
type base struct {
types.TestDeepStamp
location location.Location
err *ctxerr.Error
}
func pkgFunc(full string) (string, string) {
// the/package.Foo → "the/package", "Foo"
// the/package.(*T).Foo → "the/package", "(*T).Foo"
// the/package.glob..func1 → "the/package", "glob..func1"
sp := strings.LastIndexByte(full, '/')
if sp < 0 {
sp = 0 // std package without any '/' in name
}
dp := strings.IndexByte(full[sp:], '.')
if dp < 0 {
return full, ""
}
dp += sp
return full[:dp], full[dp+1:]
}
// setLocation sets location using the stack trace going callDepth levels up.
func (t *base) setLocation(callDepth int) {
if callDepth < 0 {
return
}
var ok bool
t.location, ok = location.New(callDepth)
if !ok {
t.location.File = "???"
t.location.Line = 0
return
}
// Here package is github.com/maxatome/go-testdeep, or its vendored
// counterpart
var pkg string
pkg, t.location.Func = pkgFunc(t.location.Func)
// Try to go one level upper, if we are still in go-testdeep package
cmpLoc, ok := location.New(callDepth + 1)
if ok {
cmpPkg, _ := pkgFunc(cmpLoc.Func)
if cmpPkg == pkg {
t.location.File = cmpLoc.File
t.location.Line = cmpLoc.Line
t.location.BehindCmp = true
}
}
}
// replaceLocation replaces the location by loc.
func (t *base) replaceLocation(loc location.Location) {
t.location = loc
}
// GetLocation returns a copy of the location.Location where the TestDeep
// operator has been created.
func (t *base) GetLocation() location.Location {
return t.location
}
// HandleInvalid tells go-testdeep internals that this operator does
// not handle nil values directly.
func (t base) HandleInvalid() bool {
return false
}
// TypeBehind returns the type handled by the operator. Only few operators
// knows the type they are handling. If they do not know, nil is
// returned.
func (t base) TypeBehind() reflect.Type {
return nil
}
// Error returns nil if the operator is operational, the corresponding
// error otherwise.
func (t base) Error() error {
if t.err == nil {
return nil
}
return t.err
}
// stringError is a convenience method to call in String()
// implementations when the operator is in error.
func (t base) stringError() string {
return t.GetLocation().Func + "()"
}
// MarshalJSON implements encoding/json.Marshaler only to returns an
// error, as a TestDeep operator should never be JSON marshaled. So
// it is better to tell the user he/she does a mistake.
func (t base) MarshalJSON() ([]byte, error) {
return nil, types.OperatorNotJSONMarshallableError(t.location.Func)
}
// newBase returns a new base struct with location.Location set to the
// callDepth depth.
func newBase(callDepth int) (b base) {
b.setLocation(callDepth)
return
}
// baseOKNil is a base type providing some methods needed by the TestDeep
// interface, for operators handling nil values.
type baseOKNil struct {
base
}
// HandleInvalid tells go-testdeep internals that this operator
// handles nil values directly.
func (t baseOKNil) HandleInvalid() bool {
return true
}
// newBaseOKNil returns a new baseOKNil struct with location.Location set to
// the callDepth depth.
func newBaseOKNil(callDepth int) (b baseOKNil) {
b.setLocation(callDepth)
return
}
go-testdeep-1.15.0/td/types_test.go 0000664 0000000 0000000 00000006270 15144170453 0017170 0 ustar 00root root 0000000 0000000 // Copyright (c) 2019-2021, 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"
"strings"
"testing"
"github.com/maxatome/go-testdeep/helpers/tdutil"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)
func TestSetlocation(t *testing.T) {
//nolint: gocritic
//line types_test.go:10
tt := tdutil.NewT("test")
ok := td.Cmp(tt, 12, 13)
if !ok {
test.EqualStr(t, tt.LogBuf(), ` types_test.go:11: Failed test
DATA: values differ
got: 12
expected: 13
`)
} else {
t.Error("Cmp returned true!")
}
//nolint: gocritic
//line types_test.go:20
tt = tdutil.NewT("test")
ok = td.Cmp(tt,
12,
td.Any(13, 14, 15))
if !ok {
test.EqualStr(t, tt.LogBuf(), ` types_test.go:21: Failed test
DATA: comparing with Any
got: 12
expected: Any(13,
14,
15)
[under operator Any at types_test.go:23]
`)
} else {
t.Error("Cmp returned true!")
}
//nolint: gocritic
//line types_test.go:30
tt = tdutil.NewT("test")
ok = td.CmpAny(tt,
12,
[]any{13, 14, 15})
if !ok {
test.EqualStr(t, tt.LogBuf(), ` types_test.go:31: Failed test
DATA: comparing with Any
got: 12
expected: Any(13,
14,
15)
`)
} else {
t.Error("CmpAny returned true!")
}
//nolint: gocritic
//line types_test.go:40
tt = tdutil.NewT("test")
ttt := td.NewT(tt)
ok = ttt.Cmp(
12,
td.Any(13, 14, 15))
if !ok {
test.EqualStr(t, tt.LogBuf(), ` types_test.go:42: Failed test
DATA: comparing with Any
got: 12
expected: Any(13,
14,
15)
[under operator Any at types_test.go:44]
`)
} else {
t.Error("Cmp returned true!")
}
//nolint: gocritic
//line types_test.go:50
tt = tdutil.NewT("test")
ttt = td.NewT(tt)
ok = ttt.Any(
12,
[]any{13, 14, 15})
if !ok {
test.EqualStr(t, tt.LogBuf(), ` types_test.go:52: Failed test
DATA: comparing with Any
got: 12
expected: Any(13,
14,
15)
`)
} else {
t.Error("Cmp returned true!")
}
//line /a/full/path/types_test.go:50
tt = tdutil.NewT("test")
ttt = td.NewT(tt)
ok = ttt.Any(
12,
[]any{13, 14, 15})
if !ok {
test.EqualStr(t, tt.LogBuf(), ` types_test.go:52: Failed test
DATA: comparing with Any
got: 12
expected: Any(13,
14,
15)
This is how we got here:
TestSetlocation() /a/full/path/types_test.go:52
`) // at least one '/' in file name → "This is how we got here"
} else {
t.Error("Cmp returned true!")
}
}
func TestError(t *testing.T) {
test.NoError(t, td.Re(`x`).Error())
test.Error(t, td.Re(123).Error())
}
func TestMarshalJSON(t *testing.T) {
op := td.String("foo")
_, err := json.Marshal(op)
if test.Error(t, err) {
test.IsTrue(t, strings.HasSuffix(err.Error(), "String TestDeep operator cannot be json.Marshal'led"))
}
}
go-testdeep-1.15.0/td/uniq_type_behind.go 0000664 0000000 0000000 00000003107 15144170453 0020307 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-2021, 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"
)
// uniqTypeBehindSlice can return a non-nil reflect.Type if all items
// known non-interface types are equal, or if only interface types
// are found (mostly issued from Isa()) and they are equal.
func uniqTypeBehindSlice(items []reflect.Value) reflect.Type {
var (
lastIfType, lastType, curType reflect.Type
severalIfTypes bool
)
for _, item := range items {
if !item.IsValid() {
return nil // no need to go further
}
if item.Type().Implements(testDeeper) {
curType = item.Interface().(TestDeep).TypeBehind()
// Ignore unknown TypeBehind
if curType == nil {
continue
}
// Ignore interfaces & interface pointers too (see Isa), but
// keep them in mind in case we encounter always the same
// interface pointer
if curType.Kind() == reflect.Interface ||
(curType.Kind() == reflect.Ptr &&
curType.Elem().Kind() == reflect.Interface) {
if lastIfType == nil {
lastIfType = curType
} else if lastIfType != curType {
severalIfTypes = true
}
continue
}
} else {
curType = item.Type()
}
if lastType != curType {
if lastType != nil {
return nil
}
lastType = curType
}
}
// Only one type found
if lastType != nil {
return lastType
}
// Only one interface type found
if lastIfType != nil && !severalIfTypes {
return lastIfType
}
return nil
}
go-testdeep-1.15.0/td/utils.go 0000664 0000000 0000000 00000001526 15144170453 0016124 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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"
"time"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/dark"
"github.com/maxatome/go-testdeep/internal/types"
)
// getTime returns the time.Time that is inside got or that can be
// converted from got contents.
func getTime(ctx ctxerr.Context, got reflect.Value, mustConvert bool) (time.Time, *ctxerr.Error) {
var (
gotIf any
ok bool
)
if mustConvert {
gotIf, ok = dark.GetInterface(got.Convert(types.Time), true)
} else {
gotIf, ok = dark.GetInterface(got, true)
}
if !ok {
return time.Time{}, ctx.CannotCompareError()
}
return gotIf.(time.Time), nil
}
go-testdeep-1.15.0/td/utils_test.go 0000664 0000000 0000000 00000003505 15144170453 0017162 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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"
"testing"
"time"
"github.com/maxatome/go-testdeep/internal/ctxerr"
"github.com/maxatome/go-testdeep/internal/dark"
"github.com/maxatome/go-testdeep/internal/test"
)
func TestGetTime(t *testing.T) {
type MyTime time.Time
oneTime := time.Date(2018, 7, 14, 12, 11, 10, 0, time.UTC)
// OK cases
for idx, curTest := range []struct {
ParamGot any
ParamMustConvert bool
ExpectedTime time.Time
}{
{
ParamGot: oneTime,
ExpectedTime: oneTime,
},
{
ParamGot: MyTime(oneTime),
ParamMustConvert: true,
ExpectedTime: oneTime,
},
} {
testName := fmt.Sprintf("Test #%d: ", idx)
tm, err := getTime(newContext(nil),
reflect.ValueOf(curTest.ParamGot), curTest.ParamMustConvert)
if !tm.Equal(curTest.ExpectedTime) {
test.EqualErrorMessage(t, tm, curTest.ExpectedTime,
testName+"time")
}
if err != nil {
test.EqualErrorMessage(t, err, "no error",
testName+"should NOT return an error")
}
}
// Simulate error return from dark.GetInterface
oldGetInterface := dark.GetInterface
defer func() { dark.GetInterface = oldGetInterface }()
dark.GetInterface = func(val reflect.Value, force bool) (any, bool) {
return nil, false
}
// Error cases
for idx, ctx := range []ctxerr.Context{newContext(nil), newBooleanContext()} {
testName := fmt.Sprintf("Test #%d: ", idx)
tm, err := getTime(ctx, reflect.ValueOf(oneTime), false)
if !tm.Equal(time.Time{}) {
test.EqualErrorMessage(t, tm, time.Time{}, testName+"time")
}
if err == nil {
test.EqualErrorMessage(t, "no error", err,
testName+"should return an error")
}
}
}
go-testdeep-1.15.0/td_compat.go 0000664 0000000 0000000 00000027102 15144170453 0016325 0 ustar 00root root 0000000 0000000 // Copyright (c) 2020, 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 testdeep
import (
"github.com/maxatome/go-testdeep/td"
)
// TestingT is a deprecated alias of [td.TestingT].
type TestingT = td.TestingT
// TestingFT is a deprecated alias of [td.TestingFT], which is itself
// a deprecated alias of [testing.TB].
type TestingFT = td.TestingFT
// TestDeep is a deprecated alias of [td.TestDeep].
type TestDeep = td.TestDeep
// ContextConfig is a deprecated alias of [td.ContextConfig].
type ContextConfig = td.ContextConfig
// T is a deprecated alias of [td.T].
type T = td.T
// ArrayEntries is a deprecated alias of [td.ArrayEntries].
type ArrayEntries = td.ArrayEntries
// BoundsKind is a deprecated alias of [td.BoundsKind].
type BoundsKind = td.BoundsKind
// MapEntries is a deprecated alias of [td.MapEntries].
type MapEntries = td.MapEntries
// SmuggledGot is a deprecated alias of [td.SmuggledGot].
type SmuggledGot = td.SmuggledGot
// StructFields is a deprecated alias of [td.StructFields].
type StructFields = td.StructFields
// Incompatible change: testdeep.DefaultContextConfig must be replaced
// by td.DefaultContextConfig. Commented here to raise an error if used.
// var DefaultContextConfig = td.DefaultContextConfig
// Cmp is a deprecated alias of [td.Cmp].
var Cmp = td.Cmp
// CmpDeeply is a deprecated alias of [td.CmpDeeply].
var CmpDeeply = td.CmpDeeply
// CmpTrue is a deprecated alias of [td.CmpTrue].
var CmpTrue = td.CmpTrue
// CmpFalse is a deprecated alias of [td.CmpFalse].
var CmpFalse = td.CmpFalse
// CmpError is a deprecated alias of [td.CmpError].
var CmpError = td.CmpError
// CmpNoError is a deprecated alias of [td.CmpNoError].
var CmpNoError = td.CmpNoError
// CmpPanic is a deprecated alias of [td.CmpPanic].
var CmpPanic = td.CmpPanic
// CmpNotPanic is a deprecated alias of [td.CmpNotPanic].
var CmpNotPanic = td.CmpNotPanic
// EqDeeply is a deprecated alias of [td.EqDeeply].
var EqDeeply = td.EqDeeply
// EqDeeplyError is a deprecated alias of [td.EqDeeplyError].
var EqDeeplyError = td.EqDeeplyError
// AddAnchorableStructType is a deprecated alias of [td.AddAnchorableStructType].
var AddAnchorableStructType = td.AddAnchorableStructType
// NewT is a deprecated alias of [td.NewT].
var NewT = td.NewT
// Assert is a deprecated alias of [td.Assert].
var Assert = td.Assert
// Require is a deprecated alias of [td.Require].
var Require = td.Require
// AssertRequire is a deprecated alias of [td.AssertRequire].
var AssertRequire = td.AssertRequire
// CmpAll is a deprecated alias of [td.CmpAll].
var CmpAll = td.CmpAll
// CmpAny is a deprecated alias of [td.CmpAny].
var CmpAny = td.CmpAny
// CmpArray is a deprecated alias of [td.CmpArray].
var CmpArray = td.CmpArray
// CmpArrayEach is a deprecated alias of [td.CmpArrayEach].
var CmpArrayEach = td.CmpArrayEach
// CmpBag is a deprecated alias of [td.CmpBag].
var CmpBag = td.CmpBag
// CmpBetween is a deprecated alias of [td.CmpBetween].
var CmpBetween = td.CmpBetween
// CmpCap is a deprecated alias of [td.CmpCap].
var CmpCap = td.CmpCap
// CmpCode is a deprecated alias of [td.CmpCode].
var CmpCode = td.CmpCode
// CmpContains is a deprecated alias of [td.CmpContains].
var CmpContains = td.CmpContains
// CmpContainsKey is a deprecated alias of [td.CmpContainsKey].
var CmpContainsKey = td.CmpContainsKey
// CmpEmpty is a deprecated alias of [td.CmpEmpty].
var CmpEmpty = td.CmpEmpty
// CmpGt is a deprecated alias of [td.CmpGt].
var CmpGt = td.CmpGt
// CmpGte is a deprecated alias of [td.CmpGte].
var CmpGte = td.CmpGte
// CmpHasPrefix is a deprecated alias of [td.CmpHasPrefix].
var CmpHasPrefix = td.CmpHasPrefix
// CmpHasSuffix is a deprecated alias of [td.CmpHasSuffix].
var CmpHasSuffix = td.CmpHasSuffix
// CmpIsa is a deprecated alias of [td.CmpIsa].
var CmpIsa = td.CmpIsa
// CmpJSON is a deprecated alias of [td.CmpJSON].
var CmpJSON = td.CmpJSON
// CmpKeys is a deprecated alias of [td.CmpKeys].
var CmpKeys = td.CmpKeys
// CmpLax is a deprecated alias of [td.CmpLax].
var CmpLax = td.CmpLax
// CmpLen is a deprecated alias of [td.CmpLen].
var CmpLen = td.CmpLen
// CmpLt is a deprecated alias of [td.CmpLt].
var CmpLt = td.CmpLt
// CmpLte is a deprecated alias of [td.CmpLte].
var CmpLte = td.CmpLte
// CmpMap is a deprecated alias of [td.CmpMap].
var CmpMap = td.CmpMap
// CmpMapEach is a deprecated alias of [td.CmpMapEach].
var CmpMapEach = td.CmpMapEach
// CmpN is a deprecated alias of [td.CmpN].
var CmpN = td.CmpN
// CmpNaN is a deprecated alias of [td.CmpNaN].
var CmpNaN = td.CmpNaN
// CmpNil is a deprecated alias of [td.CmpNil].
var CmpNil = td.CmpNil
// CmpNone is a deprecated alias of [td.CmpNone].
var CmpNone = td.CmpNone
// CmpNot is a deprecated alias of [td.CmpNot].
var CmpNot = td.CmpNot
// CmpNotAny is a deprecated alias of [td.CmpNotAny].
var CmpNotAny = td.CmpNotAny
// CmpNotEmpty is a deprecated alias of [td.CmpNotEmpty].
var CmpNotEmpty = td.CmpNotEmpty
// CmpNotNaN is a deprecated alias of [td.CmpNotNaN].
var CmpNotNaN = td.CmpNotNaN
// CmpNotNil is a deprecated alias of [td.CmpNotNil].
var CmpNotNil = td.CmpNotNil
// CmpNotZero is a deprecated alias of [td.CmpNotZero].
var CmpNotZero = td.CmpNotZero
// CmpPPtr is a deprecated alias of [td.CmpPPtr].
var CmpPPtr = td.CmpPPtr
// CmpPtr is a deprecated alias of [td.CmpPtr].
var CmpPtr = td.CmpPtr
// CmpRe is a deprecated alias of [td.CmpRe].
var CmpRe = td.CmpRe
// CmpReAll is a deprecated alias of [td.CmpReAll].
var CmpReAll = td.CmpReAll
// CmpSet is a deprecated alias of [td.CmpSet].
var CmpSet = td.CmpSet
// CmpShallow is a deprecated alias of [td.CmpShallow].
var CmpShallow = td.CmpShallow
// CmpSlice is a deprecated alias of [td.CmpSlice].
var CmpSlice = td.CmpSlice
// CmpSmuggle is a deprecated alias of [td.CmpSmuggle].
var CmpSmuggle = td.CmpSmuggle
// CmpSStruct is a deprecated alias of [td.CmpSStruct].
var CmpSStruct = td.CmpSStruct
// CmpString is a deprecated alias of [td.CmpString].
var CmpString = td.CmpString
// CmpStruct is a deprecated alias of [td.CmpStruct].
var CmpStruct = td.CmpStruct
// CmpSubBagOf is a deprecated alias of [td.CmpSubBagOf].
var CmpSubBagOf = td.CmpSubBagOf
// CmpSubJSONOf is a deprecated alias of [td.CmpSubJSONOf].
var CmpSubJSONOf = td.CmpSubJSONOf
// CmpSubMapOf is a deprecated alias of [td.CmpSubMapOf].
var CmpSubMapOf = td.CmpSubMapOf
// CmpSubSetOf is a deprecated alias of [td.CmpSubSetOf].
var CmpSubSetOf = td.CmpSubSetOf
// CmpSuperBagOf is a deprecated alias of [td.CmpSuperBagOf].
var CmpSuperBagOf = td.CmpSuperBagOf
// CmpSuperJSONOf is a deprecated alias of [td.CmpSuperJSONOf].
var CmpSuperJSONOf = td.CmpSuperJSONOf
// CmpSuperMapOf is a deprecated alias of [td.CmpSuperMapOf].
var CmpSuperMapOf = td.CmpSuperMapOf
// CmpSuperSetOf is a deprecated alias of [td.CmpSuperSetOf].
var CmpSuperSetOf = td.CmpSuperSetOf
// CmpTruncTime is a deprecated alias of [td.CmpTruncTime].
var CmpTruncTime = td.CmpTruncTime
// CmpValues is a deprecated alias of [td.CmpValues].
var CmpValues = td.CmpValues
// CmpZero is a deprecated alias of [td.CmpZero].
var CmpZero = td.CmpZero
// All is a deprecated alias of [td.All].
var All = td.All
// Any is a deprecated alias of [td.Any].
var Any = td.Any
// Array is a deprecated alias of [td.Array].
var Array = td.Array
// ArrayEach is a deprecated alias of [td.ArrayEach].
var ArrayEach = td.ArrayEach
// Bag is a deprecated alias of [td.Bag].
var Bag = td.Bag
// Between is a deprecated alias of [td.Between].
var Between = td.Between
// Cap is a deprecated alias of [td.Cap].
var Cap = td.Cap
// Catch is a deprecated alias of [td.Catch].
var Catch = td.Catch
// Code is a deprecated alias of [td.Code].
var Code = td.Code
// Contains is a deprecated alias of [td.Contains].
var Contains = td.Contains
// ContainsKey is a deprecated alias of [td.ContainsKey].
var ContainsKey = td.ContainsKey
// Delay is a deprecated alias of [td.ContainsKey].
var Delay = td.Delay
// Empty is a deprecated alias of [td.Empty].
var Empty = td.Empty
// Gt is a deprecated alias of [td.Gt].
var Gt = td.Gt
// Gte is a deprecated alias of [td.Gte].
var Gte = td.Gte
// HasPrefix is a deprecated alias of [td.HasPrefix].
var HasPrefix = td.HasPrefix
// HasSuffix is a deprecated alias of [td.HasSuffix].
var HasSuffix = td.HasSuffix
// Ignore is a deprecated alias of [td.Ignore].
var Ignore = td.Ignore
// Isa is a deprecated alias of [td.Isa].
var Isa = td.Isa
// JSON is a deprecated alias of [td.JSON].
var JSON = td.JSON
// Keys is a deprecated alias of [td.Keys].
var Keys = td.Keys
// Lax is a deprecated alias of [td.Lax].
var Lax = td.Lax
// Len is a deprecated alias of [td.Len].
var Len = td.Len
// Lt is a deprecated alias of [td.Lt].
var Lt = td.Lt
// Lte is a deprecated alias of [td.Lte].
var Lte = td.Lte
// Map is a deprecated alias of [td.Map].
var Map = td.Map
// MapEach is a deprecated alias of [td.MapEach].
var MapEach = td.MapEach
// N is a deprecated alias of [td.N].
var N = td.N
// NaN is a deprecated alias of [td.NaN].
var NaN = td.NaN
// Nil is a deprecated alias of [td.Nil].
var Nil = td.Nil
// None is a deprecated alias of [td.None].
var None = td.None
// Not is a deprecated alias of [td.Not].
var Not = td.Not
// NotAny is a deprecated alias of [td.NotAny].
var NotAny = td.NotAny
// NotEmpty is a deprecated alias of [td.NotEmpty].
var NotEmpty = td.NotEmpty
// NotNaN is a deprecated alias of [td.NotNaN].
var NotNaN = td.NotNaN
// NotNil is a deprecated alias of [td.NotNil].
var NotNil = td.NotNil
// NotZero is a deprecated alias of [td.NotZero].
var NotZero = td.NotZero
// Ptr is a deprecated alias of [td.Ptr].
var Ptr = td.Ptr
// PPtr is a deprecated alias of [td.PPtr].
var PPtr = td.PPtr
// Re is a deprecated alias of [td.Re].
var Re = td.Re
// ReAll is a deprecated alias of [td.ReAll].
var ReAll = td.ReAll
// Set is a deprecated alias of [td.Set].
var Set = td.Set
// Shallow is a deprecated alias of [td.Shallow].
var Shallow = td.Shallow
// Slice is a deprecated alias of [td.Slice].
var Slice = td.Slice
// Smuggle is a deprecated alias of [td.Smuggle].
var Smuggle = td.Smuggle
// String is a deprecated alias of [td.String].
var String = td.String
// SStruct is a deprecated alias of [td.SStruct].
var SStruct = td.SStruct
// Struct is a deprecated alias of [td.Struct].
var Struct = td.Struct
// SubBagOf is a deprecated alias of [td.SubBagOf].
var SubBagOf = td.SubBagOf
// SubJSONOf is a deprecated alias of [td.SubJSONOf].
var SubJSONOf = td.SubJSONOf
// SubMapOf is a deprecated alias of [td.SubMapOf].
var SubMapOf = td.SubMapOf
// SubSetOf is a deprecated alias of [td.SubSetOf].
var SubSetOf = td.SubSetOf
// SuperBagOf is a deprecated alias of [td.SuperBagOf].
var SuperBagOf = td.SuperBagOf
// SuperJSONOf is a deprecated alias of [td.SuperJSONOf].
var SuperJSONOf = td.SuperJSONOf
// SuperMapOf is a deprecated alias of [td.SuperMapOf].
var SuperMapOf = td.SuperMapOf
// SuperSetOf is a deprecated alias of [td.SuperSetOf].
var SuperSetOf = td.SuperSetOf
// Tag is a deprecated alias of [td.Tag].
var Tag = td.Tag
// TruncTime is a deprecated alias of [td.TruncTime].
var TruncTime = td.TruncTime
// Values is a deprecated alias of [td.Values].
var Values = td.Values
// Zero is a deprecated alias of [td.Zero].
var Zero = td.Zero
// BoundsInIn is a deprecated alias of [td.BoundsInIn].
const BoundsInIn = td.BoundsInIn
// BoundsInOut is a deprecated alias of [td.BoundsInOut].
const BoundsInOut = td.BoundsInOut
// BoundsOutIn is a deprecated alias of [td.BoundsOutIn].
const BoundsOutIn = td.BoundsOutIn
// BoundsOutOut is a deprecated alias of [td.BoundsOutOut].
const BoundsOutOut = td.BoundsOutOut
go-testdeep-1.15.0/td_compat_test.go 0000664 0000000 0000000 00000022667 15144170453 0017377 0 ustar 00root root 0000000 0000000 // Copyright (c) 2020, 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 testdeep_test
import (
"errors"
"math"
"testing"
"time"
td "github.com/maxatome/go-testdeep"
)
// These tests are only here to ensure that all obsolete but aliased
// functions are still callable from outside.
//
// See https://pkg.go.dev/github.com/maxatome/go-testdeep/td for real
// tests and examples.
func TestCompat(tt *testing.T) {
type MyStruct struct {
Num int64 `json:"num"`
Str string `json:"str"`
}
tt.Run("Cmp", func(t *testing.T) {
td.Cmp(t, 1, 1)
td.CmpDeeply(t, 1, 1)
td.CmpTrue(t, true)
td.CmpFalse(t, false)
td.CmpError(t, errors.New("Error"))
td.CmpNoError(t, nil)
td.CmpPanic(t, func() { panic("boom!") }, "boom!")
td.CmpNotPanic(t, func() {})
td.CmpTrue(t, td.EqDeeply(1, 1))
td.CmpNoError(t, td.EqDeeplyError(1, 1))
})
td.AddAnchorableStructType(func(nextAnchor int) MyStruct {
return MyStruct{Num: 999999999 - int64(nextAnchor)}
})
tt.Run("td.T", func(tt *testing.T) {
t := td.NewT(tt)
t.Cmp(1, 1)
assert := td.Assert(tt)
assert.Cmp(1, 1)
require := td.Require(tt)
require.Cmp(1, 1)
assert, require = td.AssertRequire(tt)
assert.Cmp(1, 1)
require.Cmp(1, 1)
})
tt.Run("All", func(t *testing.T) {
td.Cmp(t, 1, td.All(1))
td.CmpAll(t, 1, []any{1})
})
tt.Run("Any", func(t *testing.T) {
td.Cmp(t, 1, td.Any(3, 2, 1))
td.CmpAny(t, 1, []any{3, 2, 1})
})
tt.Run("Array", func(t *testing.T) {
td.Cmp(t, [2]int{1, 2}, td.Array([2]int{}, td.ArrayEntries{0: 1, 1: 2}))
td.CmpArray(t, [2]int{1, 2}, [2]int{}, td.ArrayEntries{0: 1, 1: 2})
})
tt.Run("ArrayEach", func(t *testing.T) {
got := []int{1, 1}
td.Cmp(t, got, td.ArrayEach(1))
td.CmpArrayEach(t, got, 1)
})
tt.Run("Bag", func(t *testing.T) {
got := []int{1, 2}
td.Cmp(t, got, td.Bag(1, 2))
td.CmpBag(t, got, []any{1, 2})
})
tt.Run("Between", func(t *testing.T) {
for _, bounds := range []td.BoundsKind{
td.BoundsInIn, td.BoundsInOut, td.BoundsOutIn, td.BoundsOutOut,
} {
td.Cmp(t, 5, td.Between(0, 10, bounds))
td.CmpBetween(t, 5, 0, 10, bounds)
}
})
tt.Run("Cap", func(t *testing.T) {
got := make([]int, 2, 3)
td.Cmp(t, got, td.Cap(3))
td.CmpCap(t, got, 3)
})
tt.Run("Catch", func(t *testing.T) {
var num int
td.Cmp(t, 12, td.Catch(&num, 12))
td.Cmp(t, num, 12)
})
tt.Run("Code", func(t *testing.T) {
fn := func(n int) bool { return n == 5 }
td.Cmp(t, 5, td.Code(fn))
td.CmpCode(t, 5, fn)
})
tt.Run("Contains", func(t *testing.T) {
td.Cmp(t, "foobar", td.Contains("ob"))
td.CmpContains(t, "foobar", "ob")
})
tt.Run("ContainsKey", func(t *testing.T) {
got := map[string]bool{"a": false}
td.Cmp(t, got, td.ContainsKey("a"))
td.CmpContainsKey(t, got, "a")
})
tt.Run("Empty", func(t *testing.T) {
td.Cmp(t, "", td.Empty())
td.CmpEmpty(t, "")
})
tt.Run("Gt", func(t *testing.T) {
td.Cmp(t, 5, td.Gt(3))
td.CmpGt(t, 5, 3)
})
tt.Run("Gte", func(t *testing.T) {
td.Cmp(t, 5, td.Gte(3))
td.CmpGte(t, 5, 3)
})
tt.Run("HasPrefix", func(t *testing.T) {
td.Cmp(t, "foobar", td.HasPrefix("foo"))
td.CmpHasPrefix(t, "foobar", "foo")
})
tt.Run("HasSuffix", func(t *testing.T) {
td.Cmp(t, "foobar", td.HasSuffix("bar"))
td.CmpHasSuffix(t, "foobar", "bar")
})
td.Cmp(tt, 42, td.Ignore())
tt.Run("Isa", func(t *testing.T) {
td.Cmp(t, 2, td.Isa(0))
td.CmpIsa(t, 2, 0)
})
tt.Run("JSON", func(t *testing.T) {
td.Cmp(t, []int{1, 2}, td.JSON(`[1,$val]`, td.Tag("val", 2)))
td.CmpJSON(t, []int{1, 2}, `[1,$val]`, []any{td.Tag("val", 2)})
})
tt.Run("Keys", func(t *testing.T) {
got := map[string]bool{"a": false}
td.Cmp(t, got, td.Keys([]string{"a"}))
td.CmpKeys(t, got, []string{"a"})
})
tt.Run("Lax", func(t *testing.T) {
td.Cmp(t, int64(42), td.Lax(42))
td.CmpLax(t, int64(42), 42)
})
tt.Run("Len", func(t *testing.T) {
got := make([]int, 2, 3)
td.Cmp(t, got, td.Len(2))
td.CmpLen(t, got, 2)
})
tt.Run("Lt", func(t *testing.T) {
td.Cmp(t, 5, td.Lt(10))
td.CmpLt(t, 5, 10)
})
tt.Run("Lte", func(t *testing.T) {
td.Cmp(t, 5, td.Lte(10))
td.CmpLte(t, 5, 10)
})
tt.Run("Map", func(t *testing.T) {
got := map[string]bool{"a": false, "b": true}
td.Cmp(t, got, td.Map(map[string]bool{"a": false}, td.MapEntries{"b": true}))
td.CmpMap(t, got, map[string]bool{"a": false}, td.MapEntries{"b": true})
})
tt.Run("MapEach", func(t *testing.T) {
got := map[string]int{"a": 1}
td.Cmp(t, got, td.MapEach(1))
td.CmpMapEach(t, got, 1)
})
tt.Run("N", func(t *testing.T) {
td.Cmp(t, 12, td.N(10, 2))
td.CmpN(t, 12, 10, 2)
})
tt.Run("NaN", func(t *testing.T) {
td.Cmp(t, math.NaN(), td.NaN())
td.CmpNaN(t, math.NaN())
})
tt.Run("Nil", func(t *testing.T) {
td.Cmp(t, nil, td.Nil())
td.CmpNil(t, nil)
})
tt.Run("None", func(t *testing.T) {
td.Cmp(t, 28, td.None(3, 4, 5))
td.CmpNone(t, 28, []any{3, 4, 5})
})
tt.Run("Not", func(t *testing.T) {
td.Cmp(t, 28, td.Not(3))
td.CmpNot(t, 28, 3)
})
tt.Run("NotAny", func(t *testing.T) {
got := []int{5}
td.Cmp(t, got, td.NotAny(1, 2, 3))
td.CmpNotAny(t, got, []any{1, 2, 3})
})
tt.Run("NotEmpty", func(t *testing.T) {
td.Cmp(t, "OOO", td.NotEmpty())
td.CmpNotEmpty(t, "OOO")
})
tt.Run("NotNaN", func(t *testing.T) {
td.Cmp(t, 12., td.NotNaN())
td.CmpNotNaN(t, 12.)
})
tt.Run("NotNil", func(t *testing.T) {
td.Cmp(t, 4, td.NotNil())
td.CmpNotNil(t, 4)
})
tt.Run("NotZero", func(t *testing.T) {
td.Cmp(t, 3, td.NotZero())
td.CmpNotZero(t, 3)
})
tt.Run("Ptr", func(t *testing.T) {
num := 12
td.Cmp(t, &num, td.Ptr(12))
td.CmpPtr(t, &num, 12)
})
tt.Run("PPtr", func(t *testing.T) {
num := 12
pnum := &num
td.Cmp(t, &pnum, td.PPtr(12))
td.CmpPPtr(t, &pnum, 12)
})
tt.Run("Re", func(t *testing.T) {
td.Cmp(t, "foobar", td.Re(`o+`))
td.CmpRe(t, "foobar", `o+`, nil)
})
tt.Run("ReAll", func(t *testing.T) {
td.Cmp(t, "foo bar", td.ReAll(`([a-z]+)(?: |\z)`, td.Bag("bar", "foo")))
td.CmpReAll(t, "foo bar", `([a-z]+)(?: |\z)`, td.Bag("bar", "foo"))
})
tt.Run("Set", func(t *testing.T) {
got := []int{1, 1, 2}
td.Cmp(t, got, td.Set(2, 1))
td.CmpSet(t, got, []any{2, 1})
})
tt.Run("Shallow", func(t *testing.T) {
got := []int{1, 2, 3}
expected := got[:1]
td.Cmp(t, got, td.Shallow(expected))
td.CmpShallow(t, got, expected)
})
tt.Run("Slice", func(t *testing.T) {
got := []int{1, 2}
td.Cmp(t, got, td.Slice([]int{}, td.ArrayEntries{0: 1, 1: 2}))
td.CmpSlice(t, got, []int{}, td.ArrayEntries{0: 1, 1: 2})
})
tt.Run("Smuggle", func(t *testing.T) {
fn := func(v int) int { return v * 2 }
td.Cmp(t, 5, td.Smuggle(fn, 10))
td.CmpSmuggle(t, 5, fn, 10)
})
tt.Run("String", func(t *testing.T) {
td.Cmp(t, "foo", td.String("foo"))
td.CmpString(t, "foo", "foo")
})
tt.Run("SStruct", func(t *testing.T) {
got := MyStruct{
Num: 42,
Str: "foo",
}
td.Cmp(t, got, td.SStruct(MyStruct{Num: 42}, td.StructFields{"Str": "foo"}))
td.CmpSStruct(t, got, MyStruct{Num: 42}, td.StructFields{"Str": "foo"})
})
tt.Run("Struct", func(t *testing.T) {
got := MyStruct{
Num: 42,
Str: "foo",
}
td.Cmp(t, got, td.Struct(MyStruct{Num: 42}, td.StructFields{"Str": "foo"}))
td.CmpStruct(t, got, MyStruct{Num: 42}, td.StructFields{"Str": "foo"})
})
tt.Run("SubBagOf", func(t *testing.T) {
got := []int{1}
td.Cmp(t, got, td.SubBagOf(1, 1, 2))
td.CmpSubBagOf(t, got, []any{1, 1, 2})
})
tt.Run("SubJSONOf", func(t *testing.T) {
got := MyStruct{
Num: 42,
Str: "foo",
}
td.Cmp(t, got,
td.SubJSONOf(`{"num":42,"str":$str,"zip":45600}`, td.Tag("str", "foo")))
td.CmpSubJSONOf(t, got,
`{"num":42,"str":$str,"zip":45600}`, []any{td.Tag("str", "foo")})
})
tt.Run("SubMapOf", func(t *testing.T) {
got := map[string]int{"a": 1, "b": 2}
td.Cmp(t, got,
td.SubMapOf(map[string]int{"a": 1, "c": 3}, td.MapEntries{"b": 2}))
td.CmpSubMapOf(t, got, map[string]int{"a": 1, "c": 3}, td.MapEntries{"b": 2})
})
tt.Run("SubSetOf", func(t *testing.T) {
got := []int{1, 1}
td.Cmp(t, got, td.SubSetOf(1, 2))
td.CmpSubSetOf(t, got, []any{1, 2})
})
tt.Run("SuperBagOf", func(t *testing.T) {
got := []int{1, 1, 2}
td.Cmp(t, got, td.SuperBagOf(1))
td.CmpSuperBagOf(t, got, []any{1})
})
tt.Run("SuperJSONOf", func(t *testing.T) {
got := MyStruct{
Num: 42,
Str: "foo",
}
td.Cmp(t, got, td.SuperJSONOf(`{"str":$str}`, td.Tag("str", "foo")))
td.CmpSuperJSONOf(t, got, `{"str":$str}`, []any{td.Tag("str", "foo")})
})
tt.Run("SuperMapOf", func(t *testing.T) {
got := map[string]int{"a": 1, "b": 2, "c": 3}
td.Cmp(t, got, td.SuperMapOf(map[string]int{"a": 1}, td.MapEntries{"b": 2}))
td.CmpSuperMapOf(t, got, map[string]int{"a": 1}, td.MapEntries{"b": 2})
})
tt.Run("SuperSetOf", func(t *testing.T) {
got := []int{1, 1, 2}
td.Cmp(t, got, td.SuperSetOf(1))
td.CmpSuperSetOf(t, got, []any{1})
})
tt.Run("TruncTime", func(t *testing.T) {
got, err := time.Parse(time.RFC3339Nano, "2020-02-22T12:34:56.789Z")
if err != nil {
t.Fatal(err)
}
expected, err := time.Parse(time.RFC3339, "2020-02-22T12:34:56Z")
if err != nil {
t.Fatal(err)
}
td.Cmp(t, got, td.TruncTime(expected, time.Second))
td.CmpTruncTime(t, got, expected, time.Second)
})
tt.Run("Values", func(t *testing.T) {
got := map[string]string{"a": "b"}
td.Cmp(t, got, td.Values([]string{"b"}))
td.CmpValues(t, got, []string{"b"})
})
tt.Run("Zero", func(t *testing.T) {
td.Cmp(t, 0, td.Zero())
td.CmpZero(t, 0)
})
}
go-testdeep-1.15.0/testdeep.go 0000664 0000000 0000000 00000003323 15144170453 0016167 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018, 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 testdeep allows extremely flexible deep comparison. It is
// built for testing.
//
// It is a go rewrite and adaptation of wonderful [Test::Deep] perl
// module.
//
// In golang, comparing data structure is usually done using
// [reflect.DeepEqual] or using a package that uses this function
// behind the scene.
//
// This function works very well, but it is not flexible. Both
// compared structures must match exactly.
//
// The purpose of go-testdeep is to do its best to introduce this
// missing flexibility using ["operators"] when the expected value (or
// one of its component) cannot be matched exactly.
//
// testdeep package should not be used in new code, even if it can for
// backward compatibility reasons, but [td] package.
//
// All variables and types of testdeep package are aliases to
// respectively functions and types of [td] package. They are only
// here for compatibility purpose as
//
// import "github.com/maxatome/go-testdeep/td"
//
// should now be used, in preference of older, but still supported:
//
// import td "github.com/maxatome/go-testdeep"
//
// For easy HTTP API testing, see [tdhttp] package.
//
// For tests suites also just as easy, see [tdsuite] package.
//
// [Test::Deep]: https://metacpan.org/pod/Test::Deep
// ["operators"]: https://go-testdeep.zetta.rocks/operators/
// [tdhttp]: https://pkg.go.dev/github.com/maxatome/go-testdeep/helpers/tdhttp
// [tdsuite]: https://pkg.go.dev/github.com/maxatome/go-testdeep/helpers/tdsuite
package testdeep // import "github.com/maxatome/go-testdeep"
go-testdeep-1.15.0/tools/ 0000775 0000000 0000000 00000000000 15144170453 0015162 5 ustar 00root root 0000000 0000000 go-testdeep-1.15.0/tools/gen_funcs.pl 0000775 0000000 0000000 00000111246 15144170453 0017476 0 ustar 00root root 0000000 0000000 #!/usr/bin/env perl
# Copyright (c) 2018-2025, 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.
use strict;
use warnings;
use autodie;
use 5.010;
use IPC::Open2;
die "usage $0 [-h]\n" if @ARGV != 0;
(my $REPO_DIR = $0) =~ s,/[^/]+\z,/..,;
-d $REPO_DIR or die "Cannot find repository directory ($REPO_DIR)\n";
# Check .golangci.yml vs .github/workflows/ci.yml
if (open(my $fh, '<', "$REPO_DIR/.github/workflows/ci.yml"))
{
my($ci_min, $linter_min);
while (defined(my $line = <$fh>))
{
if ($line =~ /^\s+go-version: \[(\d+\.\d+)/)
{
$ci_min = $1;
last;
}
}
close $fh;
$ci_min // die "*** Cannot extract min go version from .github/workflows/ci.yml\n";
undef $fh;
open($fh, '<', "$REPO_DIR/.golangci.yml");
while (defined(my $line = <$fh>))
{
if ($line =~ /^\s+go: '([\d.]+)'/)
{
$linter_min = $1;
last;
}
}
close $fh;
$linter_min // die "*** Cannot extract min go version from .golangci.yml\n";
if ($ci_min ne $linter_min)
{
die "*** min go versions mismatch: ci=$ci_min linter=$linter_min\n";
}
}
my $SITE_REPO_DIR = "$REPO_DIR/../go-testdeep-site";
unless (-d $SITE_REPO_DIR)
{
if ($ENV{PROD_SITE})
{
die "*** Cannot PROD_SITE as $SITE_REPO_DIR not found!\n";
}
warn "*** WARNING: cannot find $SITE_REPO_DIR. Disabling site upgrade.\n";
undef $SITE_REPO_DIR;
}
my $DIR = "$REPO_DIR/td";
-d $DIR or die "Cannot find td/ directory ($DIR)\n";
my $URL_ZETTA = 'https://go-testdeep.zetta.rocks';
my $URL_GODEV = 'https://pkg.go.dev';
my $URL_GODOC = "$URL_GODEV/github.com/maxatome/go-testdeep";
my $HEADER = <<'EOH';
// Copyright (c) 2018-2025, 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.
//
// DO NOT EDIT!!! AUTOMATICALLY GENERATED!!!
EOH
my $args_comment_src = <<'EOC';
%arg{args...} are optional and allow to name the test. This name is
used in case of failure to qualify the test. If %code{len(args) > 1} and
the first item of %arg{args} is a string and contains a '%' rune then
[fmt.Fprintf] is used to compose the name, else %arg{args} are passed to
[fmt.Fprint]. Do not forget it is the name of the test, not the
reason of a potential failure.
EOC
my $ARGS_COMMENT_GD = doc2godoc($args_comment_src);
my $ARGS_COMMENT_MD = doc2md($args_comment_src);
# These functions are variadics, but with only one possible param. In
# this case, discard the variadic property and use a default value for
# this optional parameter.
my %IGNORE_VARIADIC = (Between => 'td.BoundsInIn',
N => 0,
Re => 'nil',
Recv => 0,
TruncTime => 0,
Sorted => 'nil',
# These operators accept several StructFields,
# but we want only one here
Struct => 'nil',
SStruct => 'nil',
# These operators accept several MapEntries,
# but we want only one here
Map => 'nil',
SubMapOf => 'nil',
SuperMapOf => 'nil',
# These operators accept several ArrayEntries,
# but we want only one here
Array => 'nil',
Slice => 'nil',
SuperSliceOf => 'nil');
# Smuggler operators (automatically filled)
my %SMUGGLER_OPERATORS;
# These operators should be renamed when used as *T method
my %RENAME_METHOD = (Lax => 'CmpLax',
ErrorIs => 'CmpErrorIs');
# These operators do not have *T method nor Cmp shortcut
my %ONLY_OPERATORS = map { $_ => 1 } qw(Catch Delay Ignore Tag);
my @INPUT_LABELS = qw(nil bool str int float cplx
array slice map struct ptr if chan func);
my %INPUTS;
@INPUTS{@INPUT_LABELS} = ();
opendir(my $dh, $DIR);
my(%funcs, %operators, %consts, %forbiddenOpsInJSON);
while (readdir $dh)
{
if (/^td_.*\.go\z/ and not /_test.go\z/)
{
my $contents = slurp("$DIR/$_");
# Load the operators forbidden inside JSON()
if ($_ eq 'td_json.go')
{
$contents =~ /^var forbiddenOpsInJSON = map\[string\]string\{(.*?)^\}/ms
or die "$_: forbiddenOpsInJSON map not found\n";
@forbiddenOpsInJSON{$1 =~ /"([^"]+)":/g} = ();
}
while ($contents =~ /^const \(\n(.+)^\)\n/gms)
{
@consts{$1 =~ /^\t([A-Z]\w+)/mg} = ();
}
my %imports = map { ($_ => $_) } qw(fmt io os reflect testing);
if ($contents =~ /^import \(\n(.+?)\s*\)\n/ms)
{
foreach my $pkg (split(/\n+/, $1))
{
if ($pkg =~ /^\s*(\w+)\s+\"([^"]+)/)
{
$imports{$1} = $2;
$imports{$2} = $2;
}
elsif ($pkg =~ m,^\s*"((?:.+/)?([^/"]+)),)
{
$imports{$2} = $1;
$imports{$1} = $1;
}
else
{
die "$_: cannot parse import line <$pkg>\n";
}
}
}
my %ops;
while ($contents =~ m,^// summary\((\w+)\): (.*\n(?://.*\n)*),gm)
{
my($op, $summary) = ($1, $2);
$summary =~ s,^// input\(.*,,sm;
$ops{$op} = process_summary($summary =~ s,\n(?://|\z),,gr);
}
my %inputs;
while ($contents =~ m,^// input\((\w+)\): (.*\n(?://.*\n)*),gm)
{
my $op = $1;
foreach my $in (split(/\s*,\s*/, $2 =~ s,\n(?://|\z),,gr))
{
if ($in eq 'all')
{
@{$inputs{$op}}{keys %INPUTS} = ('✓') x keys %INPUTS;
next;
}
if ($in =~ /^(\w+)\((.*)\)\z/)
{
$inputs{$op}{$1} = process_summary($2);
$in = $1;
}
else
{
$inputs{$op}{$in} = '✓';
}
exists $INPUTS{$in} or die "$_: input($op) unknown input '$in'\n";
$inputs{$op}{if} //= '✓'; # interface
}
}
my $num_smugglers = keys %SMUGGLER_OPERATORS;
while ($contents =~ m,^(// ([A-Z]\w*) .*\n(?://.*\n)*)func \2\((.*?)\) TestDeep \{\n,gm)
{
exists $ops{$2} or die "$_: no summary($2) found\n";
exists $inputs{$2} or die "$_: no input($2) found\n";
my($doc, $func, $params) = ($1, $2, $3);
if ($doc =~ /is a smuggler operator/)
{
$SMUGGLER_OPERATORS{$func} = 1;
}
my @args;
foreach my $arg (split(/, /, $params))
{
my %arg;
@arg{qw(name type)} = split(/ /, $arg, 2);
if (defined $arg{type}
and $arg{variadic} = $arg{type} =~ s/^\.{3}//)
{
if (exists $IGNORE_VARIADIC{$func})
{
$arg{default} = $IGNORE_VARIADIC{$func};
delete $arg{variadic};
}
}
push(@args, \%arg);
}
my $last_type;
foreach my $arg (reverse @args)
{
if (defined(my $arg_type = $arg->{type}) and not $arg->{variadic})
{
if (defined $last_type and $arg_type eq $last_type)
{
delete $arg->{type};
}
$last_type = $arg_type;
}
}
$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,(? $func,
summary => delete $ops{$func},
input => delete $inputs{$func},
doc => $doc,
signature => "func $func($params) TestDeep",
args => \@args,
imports => \%imports,
};
}
if (%ops)
{
die "$_: summary found without operator definition: "
. join(', ', keys %ops) . "\n";
}
if (%inputs)
{
die "$_: input found without operator definition: "
. join(', ', keys %inputs) . "\n";
}
if ($contents =~ m,^\ttdSmugglerBase(?! // ignored),m
and $num_smugglers == keys %SMUGGLER_OPERATORS)
{
die "$_: this file should contain at least one smuggler operator\n";
}
}
}
closedir($dh);
%funcs or die "No TestDeep golang source file found!\n";
my $funcs_contents = my $t_contents = <