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
}
%%
golang-github-maxatome-go-testdeep-1.14.0/internal/json/parser_test.go 0000664 0000000 0000000 00000044666 14543133116 0026043 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/davecgh/go-spew/spew"
"github.com/maxatome/go-testdeep/internal/json"
"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,
strings.TrimRight(spew.Sdump(got), "\n"),
strings.TrimRight(spew.Sdump(expected), "\n"),
"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,
strings.TrimRight(spew.Sdump(got), "\n"),
strings.TrimRight(spew.Sdump(expected), "\n"),
"#%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,
strings.TrimRight(spew.Sdump(got), "\n"),
strings.TrimRight(spew.Sdump(tst.expected), "\n"),
"#%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,
strings.TrimRight(spew.Sdump(got), "\n"),
strings.TrimRight(spew.Sdump(`bar`), "\n"),
"#%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)`)
}
})
}
golang-github-maxatome-go-testdeep-1.14.0/internal/location/ 0000775 0000000 0000000 00000000000 14543133116 0024000 5 ustar 00root root 0000000 0000000 golang-github-maxatome-go-testdeep-1.14.0/internal/location/location.go 0000664 0000000 0000000 00000003175 14543133116 0026145 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, _, _, ok := runtime.Caller(callDepth)
if !ok {
return
}
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)
}
golang-github-maxatome-go-testdeep-1.14.0/internal/test/ 0000775 0000000 0000000 00000000000 14543133116 0023147 5 ustar 00root root 0000000 0000000 golang-github-maxatome-go-testdeep-1.14.0/internal/test/any.go 0000664 0000000 0000000 00000000420 14543133116 0024261 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{}
golang-github-maxatome-go-testdeep-1.14.0/internal/test/any_test.go 0000664 0000000 0000000 00000000425 14543133116 0025325 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{}
golang-github-maxatome-go-testdeep-1.14.0/internal/test/check.go 0000664 0000000 0000000 00000006121 14543133116 0024553 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 (
"strings"
"testing"
"unicode"
"github.com/davecgh/go-spew/spew"
"github.com/maxatome/go-testdeep/helpers/tdutil"
)
// 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 strings.TrimRight(spew.Sdump(s), "\n")
}
}
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
}
// 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
}
golang-github-maxatome-go-testdeep-1.14.0/internal/test/types.go 0000664 0000000 0000000 00000012025 14543133116 0024642 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
}
golang-github-maxatome-go-testdeep-1.14.0/internal/trace/ 0000775 0000000 0000000 00000000000 14543133116 0023266 5 ustar 00root root 0000000 0000000 golang-github-maxatome-go-testdeep-1.14.0/internal/trace/stack.go 0000664 0000000 0000000 00000003256 14543133116 0024730 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)
nl = "\n"
}
}
golang-github-maxatome-go-testdeep-1.14.0/internal/trace/stack_test.go 0000664 0000000 0000000 00000003214 14543133116 0025761 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`)
}
golang-github-maxatome-go-testdeep-1.14.0/internal/trace/trace.go 0000664 0000000 0000000 00000012126 14543133116 0024715 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
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:]
}
golang-github-maxatome-go-testdeep-1.14.0/internal/trace/trace_test.go 0000664 0000000 0000000 00000016331 14543133116 0025756 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)
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)
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)
}
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)
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)
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)
}
}
golang-github-maxatome-go-testdeep-1.14.0/internal/types/ 0000775 0000000 0000000 00000000000 14543133116 0023334 5 ustar 00root root 0000000 0000000 golang-github-maxatome-go-testdeep-1.14.0/internal/types/any.go 0000664 0000000 0000000 00000000421 14543133116 0024447 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{}
golang-github-maxatome-go-testdeep-1.14.0/internal/types/any_test.go 0000664 0000000 0000000 00000000426 14543133116 0025513 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{}
golang-github-maxatome-go-testdeep-1.14.0/internal/types/order.go 0000664 0000000 0000000 00000003171 14543133116 0025000 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
}
golang-github-maxatome-go-testdeep-1.14.0/internal/types/order_test.go 0000664 0000000 0000000 00000004671 14543133116 0026045 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")
}
}
golang-github-maxatome-go-testdeep-1.14.0/internal/types/reflect.go 0000664 0000000 0000000 00000005776 14543133116 0025326 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
}
golang-github-maxatome-go-testdeep-1.14.0/internal/types/reflect_go117_test.go 0000664 0000000 0000000 00000002303 14543133116 0027262 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)
}
golang-github-maxatome-go-testdeep-1.14.0/internal/types/reflect_go120_test.go 0000664 0000000 0000000 00000002637 14543133116 0027266 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)
}
golang-github-maxatome-go-testdeep-1.14.0/internal/types/reflect_test.go 0000664 0000000 0000000 00000003510 14543133116 0026345 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)
})
}
}
golang-github-maxatome-go-testdeep-1.14.0/internal/types/types.go 0000664 0000000 0000000 00000004376 14543133116 0025041 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}
golang-github-maxatome-go-testdeep-1.14.0/internal/types/types_private_test.go 0000664 0000000 0000000 00000000623 14543133116 0027621 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()
}
golang-github-maxatome-go-testdeep-1.14.0/internal/types/types_test.go 0000664 0000000 0000000 00000004275 14543133116 0026076 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)
}
}
golang-github-maxatome-go-testdeep-1.14.0/internal/util/ 0000775 0000000 0000000 00000000000 14543133116 0023145 5 ustar 00root root 0000000 0000000 golang-github-maxatome-go-testdeep-1.14.0/internal/util/any.go 0000664 0000000 0000000 00000000420 14543133116 0024257 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{}
golang-github-maxatome-go-testdeep-1.14.0/internal/util/any_test.go 0000664 0000000 0000000 00000000425 14543133116 0025323 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{}
golang-github-maxatome-go-testdeep-1.14.0/internal/util/json_pointer.go 0000664 0000000 0000000 00000004065 14543133116 0026212 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
}
golang-github-maxatome-go-testdeep-1.14.0/internal/util/json_pointer_test.go 0000664 0000000 0000000 00000003665 14543133116 0027256 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")
}
golang-github-maxatome-go-testdeep-1.14.0/internal/util/string.go 0000664 0000000 0000000 00000012013 14543133116 0024777 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.
func ToString(val any) string {
if val == nil {
return "nil"
}
switch tval := val.(type) {
case reflect.Value:
newVal, ok := dark.GetInterface(tval, true)
if ok {
return ToString(newVal)
}
case []reflect.Value:
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:
return TernStr(tval, "true", "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())
}
}
golang-github-maxatome-go-testdeep-1.14.0/internal/util/string_test.go 0000664 0000000 0000000 00000014210 14543133116 0026037 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.Value{reflect.ValueOf("foo"), reflect.ValueOf("bar")},
expected: `("foo",
"bar")`,
},
{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 {}")
}
golang-github-maxatome-go-testdeep-1.14.0/internal/util/tag.go 0000664 0000000 0000000 00000001565 14543133116 0024256 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
}
golang-github-maxatome-go-testdeep-1.14.0/internal/util/tag_test.go 0000664 0000000 0000000 00000001772 14543133116 0025315 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)
}
}
}
golang-github-maxatome-go-testdeep-1.14.0/internal/util/utils.go 0000664 0000000 0000000 00000000736 14543133116 0024642 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
// TernRune returns a if cond is true, b otherwise.
func TernRune(cond bool, a, b rune) rune {
if cond {
return a
}
return b
}
// TernStr returns a if cond is true, b otherwise.
func TernStr(cond bool, a, b string) string {
if cond {
return a
}
return b
}
golang-github-maxatome-go-testdeep-1.14.0/internal/util/utils_test.go 0000664 0000000 0000000 00000001137 14543133116 0025675 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 TestTern(t *testing.T) {
test.EqualStr(t, util.TernStr(true, "A", "B"), "A")
test.EqualStr(t, util.TernStr(false, "A", "B"), "B")
test.EqualInt(t, int(util.TernRune(true, 'A', 'B')), int('A'))
test.EqualInt(t, int(util.TernRune(false, 'A', 'B')), int('B'))
}
golang-github-maxatome-go-testdeep-1.14.0/internal/visited/ 0000775 0000000 0000000 00000000000 14543133116 0023637 5 ustar 00root root 0000000 0000000 golang-github-maxatome-go-testdeep-1.14.0/internal/visited/any.go 0000664 0000000 0000000 00000000423 14543133116 0024754 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{}
golang-github-maxatome-go-testdeep-1.14.0/internal/visited/any_test.go 0000664 0000000 0000000 00000000430 14543133116 0026011 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{}
golang-github-maxatome-go-testdeep-1.14.0/internal/visited/visited.go 0000664 0000000 0000000 00000003732 14543133116 0025642 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
// to not 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
}
golang-github-maxatome-go-testdeep-1.14.0/internal/visited/visited_test.go 0000664 0000000 0000000 00000006024 14543133116 0026676 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)))
})
}
golang-github-maxatome-go-testdeep-1.14.0/td/ 0000775 0000000 0000000 00000000000 14543133116 0020763 5 ustar 00root root 0000000 0000000 golang-github-maxatome-go-testdeep-1.14.0/td/any.go 0000664 0000000 0000000 00000000416 14543133116 0022102 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{}
golang-github-maxatome-go-testdeep-1.14.0/td/any_test.go 0000664 0000000 0000000 00000000423 14543133116 0023137 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{}
golang-github-maxatome-go-testdeep-1.14.0/td/check_test.go 0000664 0000000 0000000 00000022606 14543133116 0023434 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
}
type expectedErrorMatch struct {
Exact string
Match *regexp.Regexp
Contain string
}
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 err.Origin == nil {
t.Errorf(`%sError should originate from another Error`,
tdutil.BuildTestName(args...))
return false
}
return matchError(t, err.Origin, *expectedError.Origin,
expectedIsTestDeep, args...)
}
if err.Origin != nil {
t.Errorf(`%sError should NOT originate from another Error`,
tdutil.BuildTestName(args...))
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
}
golang-github-maxatome-go-testdeep-1.14.0/td/cmp_deeply.go 0000664 0000000 0000000 00000014004 14543133116 0023432 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 scene,
// [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...)
}
golang-github-maxatome-go-testdeep-1.14.0/td/cmp_deeply_test.go 0000664 0000000 0000000 00000023013 14543133116 0024471 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())
}
golang-github-maxatome-go-testdeep-1.14.0/td/cmp_funcs.go 0000664 0000000 0000000 00000142177 14543133116 0023303 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.
//
// DO NOT EDIT!!! AUTOMATICALLY GENERATED!!!
package td
import (
"time"
)
// allOperators lists the 67 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,
"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,
"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.
//
// 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...)
}
// 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.
//
// 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.
//
// 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...)
}
// 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.
//
// 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.
//
// 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.
//
// 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...)
}
golang-github-maxatome-go-testdeep-1.14.0/td/cmp_funcs_misc.go 0000664 0000000 0000000 00000020026 14543133116 0024302 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...)
}
golang-github-maxatome-go-testdeep-1.14.0/td/cmp_funcs_misc_test.go 0000664 0000000 0000000 00000007165 14543133116 0025352 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 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) }, nil, "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
}
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
}
golang-github-maxatome-go-testdeep-1.14.0/td/config.go 0000664 0000000 0000000 00000014170 14543133116 0022562 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,
}
}
golang-github-maxatome-go-testdeep-1.14.0/td/config_test.go 0000664 0000000 0000000 00000004272 14543133116 0023623 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)
} else {
os.Unsetenv(envMaxErrors)
}
}()
os.Setenv(envMaxErrors, "")
test.EqualInt(t, getMaxErrorsFromEnv(), 10)
os.Setenv(envMaxErrors, "aaa")
test.EqualInt(t, getMaxErrorsFromEnv(), 10)
os.Setenv(envMaxErrors, "-8")
test.EqualInt(t, getMaxErrorsFromEnv(), -8)
}
golang-github-maxatome-go-testdeep-1.14.0/td/doc.go 0000664 0000000 0000000 00000020053 14543133116 0022057 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 scene.
//
// 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 CreateAt 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 to not 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"
golang-github-maxatome-go-testdeep-1.14.0/td/equal.go 0000664 0000000 0000000 00000030515 14543133116 0022425 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 {
ctx = ctx.ResetErrors()
ctx.BooleanError = true
return deepValueEqualFinal(ctx, got, expected) == 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() || !expected.IsValid() {
if got.IsValid() == expected.IsValid() {
return
}
return nilHandler(ctx, got, expected)
}
// Check if a Smuggle hook matches got type
if handled, e := ctx.Hooks.Smuggle(&got); handled {
if 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,
})
}
}
// 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 {
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(),
})
}
return
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
}
golang-github-maxatome-go-testdeep-1.14.0/td/equal_examples_test.go 0000664 0000000 0000000 00000002433 14543133116 0025360 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]
}
golang-github-maxatome-go-testdeep-1.14.0/td/equal_test.go 0000664 0000000 0000000 00000051335 14543133116 0023467 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})
// First error
ok := t.Run("First error",
func(t *testing.T) {
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"),
}, false) {
return
}
})
if !ok {
return
}
// Second error
eErr := err.(*ctxerr.Error).Next
t.Run("Second error",
func(t *testing.T) {
if eErr == nil {
t.Errorf("A second Error should have occurred")
return
}
if !matchError(t, eErr,
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[2]"),
Got: mustBe("3"),
Expected: mustBe("43"),
}, false) {
return
}
if eErr.Next != ctxerr.ErrTooManyErrors {
if eErr.Next == nil {
t.Error("ErrTooManyErrors should follow the 2 errors")
} else {
t.Errorf("Only 2 Errors should have occurred. Found 3rd: %s",
eErr.Next)
}
return
}
})
})
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})
// First error
ok := t.Run("First error",
func(t *testing.T) {
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"),
}, false) {
return
}
})
if !ok {
return
}
// Second error
eErr := err.(*ctxerr.Error).Next
ok = t.Run("Second error",
func(t *testing.T) {
if eErr == nil {
t.Errorf("A second Error should have occurred")
return
}
if !matchError(t, eErr,
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[2]"),
Got: mustBe("3"),
Expected: mustBe("43"),
}, false) {
return
}
})
if !ok {
return
}
// Third error
eErr = eErr.Next
t.Run("Third error",
func(t *testing.T) {
if eErr == nil {
t.Errorf("A third Error should have occurred")
return
}
if !matchError(t, eErr,
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA[3]"),
Got: mustBe("4"),
Expected: mustBe("44"),
}, false) {
return
}
if eErr.Next != nil {
t.Errorf("Only 3 Errors should have occurred")
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,
})
}
golang-github-maxatome-go-testdeep-1.14.0/td/equal_unsafe_test.go 0000664 0000000 0000000 00000001543 14543133116 0025024 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"
})`),
})
}
golang-github-maxatome-go-testdeep-1.14.0/td/example_cmp_test.go 0000664 0000000 0000000 00000307472 14543133116 0024660 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.
//
// 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) // 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 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)
// 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-a-number", got)
fmt.Println("math.NaN() is not-a-number:", ok)
got = 12
ok = td.CmpNotNaN(t, got,
"checks %v is not-a-number", got)
fmt.Println("float64(12) is not-a-number:", ok)
// 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_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 to not 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(t *Transaction) (int, error) {
if t.Request.Body == nil ||
t.Request.Body.Value == nil {
return 0, errors.New("Request.Body or Request.Body.Value is nil")
}
if v, ok := t.Request.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.Request.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 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) // 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 to not 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) // 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
}
golang-github-maxatome-go-testdeep-1.14.0/td/example_t_test.go 0000664 0000000 0000000 00000303522 14543133116 0024334 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.
//
// 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) // 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_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)
// 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-a-number", got)
fmt.Println("math.NaN() is not-a-number:", ok)
got = 12
ok = t.NotNaN(got,
"checks %v is not-a-number", got)
fmt.Println("float64(12) is not-a-number:", ok)
// 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_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 to not 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(t *Transaction) (int, error) {
if t.Request.Body == nil ||
t.Request.Body.Value == nil {
return 0, errors.New("Request.Body or Request.Body.Value is nil")
}
if v, ok := t.Request.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.Request.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_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) // 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 to not 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) // 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
}
golang-github-maxatome-go-testdeep-1.14.0/td/example_test.go 0000664 0000000 0000000 00000332105 14543133116 0024010 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 (
"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) // 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 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)
// 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-a-number", got)
fmt.Println("math.NaN() is not-a-number:", ok)
got = 12
ok = td.Cmp(t, got, td.NotNaN(),
"checks %v is not-a-number", got)
fmt.Println("float64(12) is not-a-number:", ok)
// 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 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 to not 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 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(t *Transaction) (int, error) {
if t.Request.Body == nil ||
t.Request.Body.Value == nil {
return 0, errors.New("Request.Body or Request.Body.Value is nil")
}
if v, ok := t.Request.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.Request.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) // 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 to not 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) // 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
}
golang-github-maxatome-go-testdeep-1.14.0/td/flatten.go 0000664 0000000 0000000 00000023107 14543133116 0022752 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"
)
// Flatten allows to flatten any slice, array or map 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{
// 11,
// td.Flatten([]int{22, 33}),
// td.Flatten([]int{44, 55, 66}),
// }),
// 77,
// ))
//
// is exactly the same as:
//
// td.Cmp(t, got, td.Set(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.
//
// 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)
//
// T can be the same as V, but it is not mandatory. The (V, bool)
// returned case allows to exclude some items when returning false.
//
// If the function signature does not match these cases, Flatten panics.
//
// If the type of an item of sliceOrMap is not convertible to T, the
// item is dropped silently, as if fn returned 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 [Set] or [Bag] operators families. 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(sliceOrMap any, fn ...any) flat.Slice {
const (
smugglePrefix = "Smuggle:"
jsonPointerPrefix = "JSONPointer:"
usage = "Flatten(SLICE|ARRAY|MAP[, FUNC])"
usageFunc = usage + `, FUNC should be non-nil func(T) V or func(T) (V, bool) or a string "` + smugglePrefix + `…" or "` + jsonPointerPrefix + `…"`
)
switch reflect.ValueOf(sliceOrMap).Kind() {
case reflect.Slice, reflect.Array, reflect.Map:
default:
panic(color.BadUsage(usage, sliceOrMap, 1, true))
}
switch len(fn) {
case 1:
if fn[0] != nil {
break
}
fallthrough
case 0:
return flat.Slice{Slice: sliceOrMap}
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 fnType.Kind() != reflect.Func ||
fnType.NumIn() != 1 || fnType.IsVariadic() ||
(fnType.NumOut() != 1 && (fnType.NumOut() != 2 || fnType.Out(1) != types.Bool)) {
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: sliceOrMap}}) {
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}
}
// Flatten allows to flatten any slice, array or map in
// parameters of operators expecting ...any after applying a function
// on each item to exclude or transform it.
//
// fn must be a non-nil function with a signature like:
//
// func(T) V
// func(T) (V, bool)
//
// T can be the same as V but it is not mandatory. The (V, bool)
// returned case allows to exclude some items when returning false.
//
// If fn signature does not match these cases, Flatten panics.
//
// If the type of an item of sliceOrMap is not convertible to T, the
// item is dropped silently, as if fn returned false.
//
// fn 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 can be useful when testing some fields of structs in
// a slice with [Set] or [Bag] operators families. 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(
// func(name string) any { return td.Smuggle("Name", name) },
// []string{"alice", "britt", "brian", "bob"})))
// // 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(
// "Smuggle:Name", []string{"alice", "britt", "brian", "bob"})))
//
// // Same here, but using JSONPointer operator
// td.Cmp(t, got,
// td.Bag(td.Flatten(
// func(name string) any { return td.JSONPointer("/name", name) },
// []string{"alice", "britt", "brian", "bob"})))
//
// // Same here using JSONPointer string shortcut
// td.Cmp(t, got,
// td.Bag(td.Flatten(
// "JSONPointer:/name", []string{"alice", "britt", "brian", "bob"})))
//
// // Same here, but using SuperJSONOf operator
// td.Cmp(t, got,
// td.Bag(td.Flatten(
// func(name string) any { return td.SuperJSONOf(`{"name":$1}`, name) },
// []string{"alice", "britt", "brian", "bob"})))
//
// // Same here, but using Struct operator
// td.Cmp(t, got,
// td.Bag(td.Flatten(
// func(name string) any { return td.Struct(person{Name: name}) },
// []string{"alice", "britt", "brian", "bob"})))
//
// See also [Flatten] and [Grep].
golang-github-maxatome-go-testdeep-1.14.0/td/flatten_test.go 0000664 0000000 0000000 00000015336 14543133116 0024016 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_test
import (
"reflect"
"strconv"
"testing"
"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
sliceOrMap any
fn []any
expectedType reflect.Type
expectedLen int
}{
{
name: "slice",
sliceOrMap: []int{1, 2, 3},
expectedType: reflect.TypeOf([]int{}),
expectedLen: 3,
},
{
name: "array",
sliceOrMap: [3]int{1, 2, 3},
expectedType: reflect.TypeOf([3]int{}),
expectedLen: 3,
},
{
name: "map",
sliceOrMap: map[int]int{1: 2, 3: 4},
expectedType: reflect.TypeOf(map[int]int{}),
expectedLen: 2,
},
{
name: "slice+untyped nil fn",
sliceOrMap: []int{1, 2, 3},
fn: []any{nil},
expectedType: reflect.TypeOf([]int{}),
expectedLen: 3,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := td.Flatten(tc.sliceOrMap, tc.fn...)
if reflect.TypeOf(s.Slice) != tc.expectedType {
t.Errorf("types differ: got=%s, expected=%s",
reflect.TypeOf(s.Slice), tc.expectedType)
return
}
test.EqualInt(t, reflect.ValueOf(s.Slice).Len(), tc.expectedLen)
})
}
})
t.Run("ok+func", func(t *testing.T) {
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])
}
}
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),
},
},
}
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("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[, 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
sliceOrMap any
expected string
}{
{
name: "too many params",
sliceOrMap: []int{},
fn: []any{1, 2},
expected: usage + ", too many parameters",
},
{
name: "nil sliceOrMap",
expected: usage + ", but received nil as 1st parameter",
},
{
name: "bad sliceOrMap type",
sliceOrMap: 42,
expected: usage + ", but received int as 1st parameter",
},
{
name: "not func",
sliceOrMap: []int{},
fn: []any{42},
expected: usageFunc + ", but received int as 2nd parameter",
},
{
name: "func w/0 inputs",
sliceOrMap: []int{},
fn: []any{func() int { return 0 }},
expected: usageFunc + ", but received func() int as 2nd parameter",
},
{
name: "func w/2 inputs",
sliceOrMap: []int{},
fn: []any{func(a, b int) int { return 0 }},
expected: usageFunc + ", but received func(int, int) int as 2nd parameter",
},
{
name: "variadic func",
sliceOrMap: []int{},
fn: []any{func(a ...int) int { return 0 }},
expected: usageFunc + ", but received func(...int) int as 2nd parameter",
},
{
name: "func w/0 output",
sliceOrMap: []int{},
fn: []any{func(a int) {}},
expected: usageFunc + ", but received func(int) as 2nd parameter",
},
{
name: "func w/2 out without bool",
sliceOrMap: []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",
sliceOrMap: []int{},
fn: []any{"Pipo"},
expected: usageFunc + `, but received "Pipo" as 2nd parameter`,
},
{
name: "typed nil func",
sliceOrMap: []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.sliceOrMap, tc.fn...) }, tc.expected)
})
}
})
}
golang-github-maxatome-go-testdeep-1.14.0/td/private_test.go 0000664 0000000 0000000 00000002657 14543133116 0024035 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, "")
}
golang-github-maxatome-go-testdeep-1.14.0/td/t.go 0000664 0000000 0000000 00000125760 14543133116 0021570 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.
//
// 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.
//
// 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...)
}
// 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.
//
// 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.
//
// 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...)
}
// 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.
//
// 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.
//
// 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.
//
// 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...)
}
golang-github-maxatome-go-testdeep-1.14.0/td/t_anchor.go 0000664 0000000 0000000 00000021326 14543133116 0023113 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)
})
}
}
}
golang-github-maxatome-go-testdeep-1.14.0/td/t_anchor_118.go 0000664 0000000 0000000 00000001012 14543133116 0023472 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/t_anchor_118_test.go 0000664 0000000 0000000 00000002474 14543133116 0024546 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)),
}))
}
golang-github-maxatome-go-testdeep-1.14.0/td/t_anchor_test.go 0000664 0000000 0000000 00000012042 14543133116 0024145 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())
})
}
golang-github-maxatome-go-testdeep-1.14.0/td/t_hooks.go 0000664 0000000 0000000 00000011737 14543133116 0022771 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 A, expected A) bool
// func (got A, 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
}
golang-github-maxatome-go-testdeep-1.14.0/td/t_hooks_test.go 0000664 0000000 0000000 00000011467 14543133116 0024030 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,
},
} {
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("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)
}
})
}
}
golang-github-maxatome-go-testdeep-1.14.0/td/t_struct.go 0000664 0000000 0000000 00000063350 14543133116 0023170 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.
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 [Require], [AssertRequire] and [T.Assert].
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 [Assert], [AssertRequire] and [T.Require].
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 [Assert] and [Require].
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.
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 [T.Assert] and [T.Require].
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 [T.FailureIsFatal] and [T.Require].
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 [T.FailureIsFatal] and [T.Assert].
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.
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).
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.
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 to not 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).
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!`")
//
// t.CmpPanic(t, func() { panic(nil) }, nil, "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].
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].
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(NewT(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...))
}
golang-github-maxatome-go-testdeep-1.14.0/td/t_struct_examples_test.go 0000664 0000000 0000000 00000006455 14543133116 0026130 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_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)
ok = t.CmpPanic(func() { panic(nil) }, nil, "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
}
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
}
golang-github-maxatome-go-testdeep-1.14.0/td/t_struct_test.go 0000664 0000000 0000000 00000045210 14543133116 0024222 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`)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_all.go 0000664 0000000 0000000 00000005127 14543133116 0022556 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"
"github.com/maxatome/go-testdeep/internal/ctxerr"
)
type tdAll struct {
tdList
}
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{
tdList: newList(expectedValues...),
}
}
func (a *tdAll) Match(ctx ctxerr.Context, got reflect.Value) (err *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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_all_test.go 0000664 0000000 0000000 00000004753 14543133116 0023621 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_any.go 0000664 0000000 0000000 00000003477 14543133116 0022603 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 tdAny struct {
tdList
}
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{
tdList: newList(expectedValues...),
}
}
func (a *tdAny) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
for _, item := range a.items {
if deepValueEqualFinalOK(ctx, got, item) {
return nil
}
}
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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_any_test.go 0000664 0000000 0000000 00000004007 14543133116 0023630 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)"),
})
// 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_array.go 0000664 0000000 0000000 00000030072 14543133116 0023121 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 (
"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
const (
arrayArray uint = iota
arraySlice
arraySuper
)
func newArray(kind uint, model any, expectedEntries ArrayEntries) *tdArray {
vmodel := reflect.ValueOf(model)
a := tdArray{
tdExpectedType: tdExpectedType{
base: newBase(4),
},
}
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 nil, if no zero entries are expected and
// no [TestDeep] operators are involved.
//
// 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 [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 nil, if no zero entries are expected and
// no [TestDeep] operators are involved.
//
// 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] 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 nil, if no zero entries are expected and
// no [TestDeep] operators are involved.
//
// Works with slices:
//
// got := []int{12, 14, 17}
// td.Cmp(t, got, td.SuperSliceOf([]int{12}, nil)) // 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}, nil)) // 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:
a.err = ctxerr.OpBad(
a.GetLocation().Func,
"expected value of #%d cannot be nil as items type is %s",
index,
elemType)
return
}
} else {
vexpectedValue = reflect.ValueOf(expectedValue)
if _, ok := expectedValue.(TestDeep); !ok {
if !vexpectedValue.Type().AssignableTo(elemType) {
a.err = ctxerr.OpBad(
a.GetLocation().Func,
"type %s of #%d expected value differs from %s contents (%s)",
vexpectedValue.Type(),
index,
util.TernStr(array, "array", "slice"),
elemType)
return
}
}
}
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()
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_array_each.go 0000664 0000000 0000000 00000005351 14543133116 0024103 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 + ")"
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_array_each_test.go 0000664 0000000 0000000 00000006016 14543133116 0025141 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_array_test.go 0000664 0000000 0000000 00000053221 14543133116 0024161 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{}, nil))
checkOK(t, [5]int{0, 0, 0, 4}, td.Array([5]int{0, 0, 0, 4}, nil))
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}))
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{}, nil),
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}, nil),
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}, nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustContain("Array("),
})
//
// Array type
checkOK(t, MyArray{}, td.Array(MyArray{}, nil))
checkOK(t, MyArray{0, 0, 0, 4}, td.Array(MyArray{0, 0, 0, 4}, nil))
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{}, nil))
checkOK(t, &MyArray{0, 0, 0, 4}, td.Array(&MyArray{0, 0, 0, 4}, nil))
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{}, nil),
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}, nil),
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{}, nil),
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}, nil),
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{}, nil),
expectedError{
Message: mustBe("type mismatch"),
})
checkError(t, [5]int{}, td.Array(MyArray{}, nil),
expectedError{
Message: mustBe("type mismatch"),
})
checkOK(t, MyArray{}, td.Lax(td.Array([5]int{}, nil)))
checkOK(t, [5]int{}, td.Lax(td.Array(MyArray{}, nil)))
//
// Bad usage
checkError(t, "never tested",
td.Array("test", nil),
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{}, nil),
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{}, nil),
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, "never tested",
td.Array([3]int{}, td.ArrayEntries{1: nil}),
expectedError{
Message: mustBe("bad usage of Array operator"),
Path: mustBe("DATA"),
Summary: mustBe("expected value of #1 cannot be nil as items type is int"),
})
checkError(t, "never tested",
td.Array([3]int{}, td.ArrayEntries{1: "bad"}),
expectedError{
Message: mustBe("bad usage of Array operator"),
Path: mustBe("DATA"),
Summary: mustBe("type string of #1 expected value differs from array contents (int)"),
})
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([3]int{}, td.ArrayEntries{1: "bad"}).String(),
"Array()")
}
func TestArrayTypeBehind(t *testing.T) {
type MyArray [12]int
equalTypes(t, td.Array([12]int{}, nil), [12]int{})
equalTypes(t, td.Array(MyArray{}, nil), MyArray{})
equalTypes(t, td.Array(&MyArray{}, nil), &MyArray{})
// Erroneous op
equalTypes(t, td.Array([3]int{}, td.ArrayEntries{1: "bad"}), nil)
}
func TestSlice(t *testing.T) {
type MySlice []int
//
// Simple slice
checkOK(t, []int{}, td.Slice([]int{}, nil))
checkOK(t, []int{0, 3}, td.Slice([]int{0, 3}, nil))
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{}, nil),
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}, nil),
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}, nil),
expectedError{
Message: mustBe("values differ"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustContain("Slice("),
})
//
// Slice type
checkOK(t, MySlice{}, td.Slice(MySlice{}, nil))
checkOK(t, MySlice{0, 3}, td.Slice(MySlice{0, 3}, nil))
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{}, nil))
checkOK(t, &MySlice{0, 3}, td.Slice(&MySlice{0, 3}, nil))
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{}, nil),
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}, nil),
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}, nil),
expectedError{
Message: mustBe("got value out of range"),
Path: mustBe("DATA[2]"),
Got: mustBe("4"),
Expected: mustBe(""),
})
checkError(t, &gotTypedSlice, td.Slice([]int{}, nil),
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}, nil),
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}, nil),
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{}, nil))
checkOK(t, gotNilTypedSlice, td.Slice(MySlice{}, nil))
checkOK(t, &gotNilTypedSlice, td.Slice(&MySlice{}, nil))
// Be lax...
// Without Lax → error
checkError(t, MySlice{}, td.Slice([]int{}, nil),
expectedError{
Message: mustBe("type mismatch"),
})
checkError(t, []int{}, td.Slice(MySlice{}, nil),
expectedError{
Message: mustBe("type mismatch"),
})
checkOK(t, MySlice{}, td.Lax(td.Slice([]int{}, nil)))
checkOK(t, []int{}, td.Lax(td.Slice(MySlice{}, nil)))
//
// 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{}, nil),
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{}, nil),
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, "never tested",
td.Slice([]int{}, td.ArrayEntries{1: "bad"}),
expectedError{
Message: mustBe("bad usage of Slice operator"),
Path: mustBe("DATA"),
Summary: mustBe("type string of #1 expected value differs from slice contents (int)"),
})
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([]int{}, td.ArrayEntries{1: "bad"}).String(),
"Slice()")
}
func TestSliceTypeBehind(t *testing.T) {
type MySlice []int
equalTypes(t, td.Slice([]int{}, nil), []int{})
equalTypes(t, td.Slice(MySlice{}, nil), MySlice{})
equalTypes(t, td.Slice(&MySlice{}, nil), &MySlice{})
// Erroneous op
equalTypes(t, td.Slice([]int{}, td.ArrayEntries{1: "bad"}), 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)}))
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{}, nil))
got := MyArray{3: 4}
checkOK(t, got, td.SuperSliceOf(MyArray{}, nil))
checkOK(t, got, td.SuperSliceOf(MyArray{3: 4}, nil))
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{}, nil))
checkOK(t, MySlice(nil), td.SuperSliceOf(MySlice{}, nil))
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}, nil),
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{}, nil),
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, "never tested",
td.SuperSliceOf([]int{}, td.ArrayEntries{1: "bad"}),
expectedError{
Message: mustBe("bad usage of SuperSliceOf operator"),
Path: mustBe("DATA"),
Summary: mustBe("type string of #1 expected value differs from slice contents (int)"),
})
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([]int{}, td.ArrayEntries{1: "bad"}).String(),
"SuperSliceOf()")
}
func TestSuperSliceOfTypeBehind(t *testing.T) {
type MySlice []int
equalTypes(t, td.SuperSliceOf([]int{}, nil), []int{})
equalTypes(t, td.SuperSliceOf(MySlice{}, nil), MySlice{})
equalTypes(t, td.SuperSliceOf(&MySlice{}, nil), &MySlice{})
type MyArray [12]int
equalTypes(t, td.SuperSliceOf([12]int{}, nil), [12]int{})
equalTypes(t, td.SuperSliceOf(MyArray{}, nil), MyArray{})
equalTypes(t, td.SuperSliceOf(&MyArray{}, nil), &MyArray{})
// Erroneous op
equalTypes(t, td.SuperSliceOf([]int{}, td.ArrayEntries{1: "bad"}), nil)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_bag.go 0000664 0000000 0000000 00000012360 14543133116 0022534 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
// 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] and [Set].
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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_bag_test.go 0000664 0000000 0000000 00000012653 14543133116 0023600 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)
// 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)
// 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)
// 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_between.go 0000664 0000000 0000000 00000046241 14543133116 0023441 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
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_between_test.go 0000664 0000000 0000000 00000060014 14543133116 0024472 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_catch.go 0000664 0000000 0000000 00000007171 14543133116 0023071 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
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_catch_test.go 0000664 0000000 0000000 00000004356 14543133116 0024132 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_code.go 0000664 0000000 0000000 00000021353 14543133116 0022717 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
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_code_test.go 0000664 0000000 0000000 00000034506 14543133116 0023762 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_contains.go 0000664 0000000 0000000 00000023551 14543133116 0023625 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)
}
if expectedLen == gotLen {
if deepValueEqualOK(got, c.expectedValue) {
return nil
}
return c.doesNotContainErr(ctx, got)
}
for i := 0; i <= gotLen-expectedLen; i++ {
if deepValueEqualOK(got.Slice(i, i+expectedLen), c.expectedValue) {
return nil
}
}
}
}
fallthrough
case reflect.Array:
expectedValue := c.getExpectedValue(got)
for index := got.Len() - 1; index >= 0; index-- {
if deepValueEqualFinalOK(ctx, got.Index(index), expectedValue) {
return nil
}
}
return c.doesNotContainErr(ctx, got)
case reflect.Map:
expectedValue := c.getExpectedValue(got)
if !tdutil.MapEachValue(got, func(v reflect.Value) bool {
return !deepValueEqualFinalOK(ctx, v, expectedValue)
}) {
return nil
}
return c.doesNotContainErr(ctx, got)
}
str, err := getString(ctx, got)
if err != nil {
return 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 {
if deepValueEqualFinalOK(ctx, reflect.ValueOf(chr), c.expectedValue) {
return nil
}
}
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) + ")"
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_contains_key.go 0000664 0000000 0000000 00000010047 14543133116 0024471 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() {
if deepValueEqualFinalOK(ctx, k, expectedValue) {
return nil
}
}
} 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) + ")"
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_contains_key_test.go 0000664 0000000 0000000 00000005527 14543133116 0025537 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)
}
}
// 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_contains_test.go 0000664 0000000 0000000 00000017766 14543133116 0024677 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)
}
}
// 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"),
})
}
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 `),
})
}
func TestContainsTypeBehind(t *testing.T) {
equalTypes(t, td.Contains("x"), nil)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_delay.go 0000664 0000000 0000000 00000003104 14543133116 0023075 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
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_delay_test.go 0000664 0000000 0000000 00000002653 14543133116 0024144 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_empty.go 0000664 0000000 0000000 00000007445 14543133116 0023151 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()"
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_empty_test.go 0000664 0000000 0000000 00000012365 14543133116 0024205 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_error_is.go 0000664 0000000 0000000 00000011507 14543133116 0023631 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
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_error_is_test.go 0000664 0000000 0000000 00000012730 14543133116 0024667 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_expected_type.go 0000664 0000000 0000000 00000003531 14543133116 0024645 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()
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_grep.go 0000664 0000000 0000000 00000025571 14543133116 0022750 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"
)
const grepUsage = "(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE)"
type tdGrepBase struct {
tdSmugglerBase
filter reflect.Value // func (argType ≠ nil) OR TestDeep operator
argType reflect.Type
}
func (g *tdGrepBase) initGrepBase(filter, expectedValue any) {
g.tdSmugglerBase = newSmugglerBase(expectedValue, 1)
if !g.isTestDeeper {
g.expectedValue = reflect.ValueOf(expectedValue)
}
if op, ok := filter.(TestDeep); ok {
g.filter = reflect.ValueOf(op)
return
}
vfilter := reflect.ValueOf(filter)
if vfilter.Kind() != reflect.Func {
g.err = ctxerr.OpBad(g.GetLocation().Func,
"usage: %s%s, FILTER_FUNC must be a function or FILTER_TESTDEEP_OPERATOR a TestDeep operator",
g.GetLocation().Func, grepUsage)
return
}
filterType := vfilter.Type()
if filterType.IsVariadic() || filterType.NumIn() != 1 {
g.err = ctxerr.OpBad(g.GetLocation().Func,
"usage: %s%s, FILTER_FUNC must take only one non-variadic argument",
g.GetLocation().Func, grepUsage)
return
}
if filterType.NumOut() != 1 || filterType.Out(0) != types.Bool {
g.err = ctxerr.OpBad(g.GetLocation().Func,
"usage: %s%s, FILTER_FUNC must return bool",
g.GetLocation().Func, grepUsage)
return
}
g.argType = filterType.In(0)
g.filter = vfilter
}
func (g *tdGrepBase) matchItem(ctx ctxerr.Context, idx int, item reflect.Value) (bool, *ctxerr.Error) {
if g.argType == nil {
// g.filter is a TestDeep operator
return deepValueEqualFinalOK(ctx, item, g.filter), nil
}
// item is an interface, but the filter function does not expect an
// interface, resolve it
if item.Kind() == reflect.Interface && g.argType.Kind() != reflect.Interface {
item = item.Elem()
}
if !item.Type().AssignableTo(g.argType) {
if !types.IsConvertible(item, g.argType) {
if ctx.BooleanError {
return false, ctxerr.BooleanError
}
return false, ctx.AddArrayIndex(idx).CollectError(&ctxerr.Error{
Message: "incompatible parameter type",
Got: types.RawString(item.Type().String()),
Expected: types.RawString(g.argType.String()),
})
}
item = item.Convert(g.argType)
}
return g.filter.Call([]reflect.Value{item})[0].Bool(), nil
}
func (g *tdGrepBase) HandleInvalid() bool {
return true // Knows how to handle untyped nil values (aka invalid values)
}
func (g *tdGrepBase) String() string {
if g.err != nil {
return g.stringError()
}
if g.argType == nil {
return S("%s(%s)", g.GetLocation().Func, g.filter.Interface().(TestDeep))
}
return S("%s(%s)", g.GetLocation().Func, g.filter.Type())
}
func (g *tdGrepBase) TypeBehind() reflect.Type {
if g.err != nil {
return nil
}
return g.internalTypeBehind()
}
// sliceTypeBehind is used by First & Last TypeBehind method.
func (g *tdGrepBase) sliceTypeBehind() reflect.Type {
typ := g.TypeBehind()
if typ == nil {
return nil
}
return reflect.SliceOf(typ)
}
func (g *tdGrepBase) notFound(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(&ctxerr.Error{
Message: "item not found",
Got: got,
Expected: types.RawString(g.String()),
})
}
func grepResolvePtr(ctx ctxerr.Context, got *reflect.Value) *ctxerr.Error {
if got.Kind() == reflect.Ptr {
gotElem := got.Elem()
if !gotElem.IsValid() {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(ctxerr.NilPointer(*got, "non-nil *slice OR *array"))
}
switch gotElem.Kind() {
case reflect.Slice, reflect.Array:
*got = gotElem
}
}
return nil
}
func grepBadKind(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if ctx.BooleanError {
return ctxerr.BooleanError
}
return ctx.CollectError(ctxerr.BadKind(got, "slice OR array OR *slice OR *array"))
}
type tdGrep struct {
tdGrepBase
}
var _ TestDeep = &tdGrep{}
// summary(Grep): reduces a slice or an array before comparing its content
// input(Grep): array,slice,ptr(ptr on array/slice)
// Grep is a smuggler operator. It takes an array, a slice or a
// pointer on array/slice. For each item it applies filter, a
// [TestDeep] operator or a function returning a bool, and produces a
// slice consisting of those items for which the filter matched and
// compares it to expectedValue. The filter matches when it is a:
// - [TestDeep] operator and it matches for the item;
// - function receiving the item and it returns true.
//
// expectedValue can be a [TestDeep] operator or a slice (but never an
// array nor a pointer on a slice/array nor any other kind).
//
// got := []int{-3, -2, -1, 0, 1, 2, 3}
// td.Cmp(t, got, td.Grep(td.Gt(0), []int{1, 2, 3})) // succeeds
// td.Cmp(t, got, td.Grep(
// func(x int) bool { return x%2 == 0 },
// []int{-2, 0, 2})) // succeeds
// td.Cmp(t, got, td.Grep(
// func(x int) bool { return x%2 == 0 },
// td.Set(0, 2, -2))) // succeeds
//
// If Grep receives a nil slice or a pointer on a nil slice, it always
// returns a nil slice:
//
// var got []int
// td.Cmp(t, got, td.Grep(td.Gt(0), ([]int)(nil))) // succeeds
// td.Cmp(t, got, td.Grep(td.Gt(0), td.Nil())) // succeeds
// td.Cmp(t, got, td.Grep(td.Gt(0), []int{})) // fails
//
// See also [First], [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 rErr
}
switch got.Kind() {
case reflect.Slice, reflect.Array:
const grepped = ""
if got.Kind() == reflect.Slice && got.IsNil() {
return deepValueEqual(
ctx.AddCustomLevel(grepped),
reflect.New(got.Type()).Elem(),
g.expectedValue,
)
}
l := got.Len()
out := reflect.MakeSlice(reflect.SliceOf(got.Type().Elem()), 0, l)
for idx := 0; idx < l; idx++ {
item := got.Index(idx)
ok, rErr := g.matchItem(ctx, idx, item)
if rErr != nil {
return rErr
}
if ok {
out = reflect.Append(out, item)
}
}
return deepValueEqual(ctx.AddCustomLevel(grepped), out, g.expectedValue)
}
return grepBadKind(ctx, got)
}
type tdFirst struct {
tdGrepBase
}
var _ TestDeep = &tdFirst{}
// summary(First): find the first matching item of a slice or an array
// then compare its content
// input(First): array,slice,ptr(ptr on array/slice)
// First is a smuggler operator. It takes an array, a slice or a
// pointer on array/slice. For each item it applies filter, a
// [TestDeep] operator or a function returning a bool. It takes the
// first item for which the filter matched and compares it to
// expectedValue. The filter matches when it is a:
// - [TestDeep] operator and it matches for the item;
// - function receiving the item and it returns true.
//
// expectedValue can of course be a [TestDeep] operator.
//
// got := []int{-3, -2, -1, 0, 1, 2, 3}
// td.Cmp(t, got, td.First(td.Gt(0), 1)) // succeeds
// td.Cmp(t, got, td.First(func(x int) bool { return x%2 == 0 }, -2)) // succeeds
// td.Cmp(t, got, td.First(func(x int) bool { return x%2 == 0 }, td.Lt(0))) // succeeds
//
// If the input is empty (and/or nil for a slice), an "item not found"
// error is raised before comparing to expectedValue.
//
// var got []int
// td.Cmp(t, got, td.First(td.Gt(0), td.Gt(0))) // fails
// td.Cmp(t, []int{}, td.First(td.Gt(0), td.Gt(0))) // fails
// td.Cmp(t, [0]int{}, td.First(td.Gt(0), td.Gt(0))) // fails
//
// See also [Last] and [Grep].
func First(filter, expectedValue any) TestDeep {
g := tdFirst{}
g.initGrepBase(filter, expectedValue)
return &g
}
func (g *tdFirst) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if g.err != nil {
return ctx.CollectError(g.err)
}
if rErr := grepResolvePtr(ctx, &got); rErr != nil {
return rErr
}
switch got.Kind() {
case reflect.Slice, reflect.Array:
for idx, l := 0, got.Len(); idx < l; idx++ {
item := got.Index(idx)
ok, rErr := g.matchItem(ctx, idx, item)
if rErr != nil {
return rErr
}
if ok {
return deepValueEqual(
ctx.AddCustomLevel(S("", idx)),
item,
g.expectedValue,
)
}
}
return g.notFound(ctx, got)
}
return grepBadKind(ctx, got)
}
func (g *tdFirst) TypeBehind() reflect.Type {
return g.sliceTypeBehind()
}
type tdLast struct {
tdGrepBase
}
var _ TestDeep = &tdLast{}
// summary(Last): find the last matching item of a slice or an array
// then compare its content
// input(Last): array,slice,ptr(ptr on array/slice)
// Last is a smuggler operator. It takes an array, a slice or a
// pointer on array/slice. For each item it applies filter, a
// [TestDeep] operator or a function returning a bool. It takes the
// last item for which the filter matched and compares it to
// expectedValue. The filter matches when it is a:
// - [TestDeep] operator and it matches for the item;
// - function receiving the item and it returns true.
//
// expectedValue can of course be a [TestDeep] operator.
//
// got := []int{-3, -2, -1, 0, 1, 2, 3}
// td.Cmp(t, got, td.Last(td.Lt(0), -1)) // succeeds
// td.Cmp(t, got, td.Last(func(x int) bool { return x%2 == 0 }, 2)) // succeeds
// td.Cmp(t, got, td.Last(func(x int) bool { return x%2 == 0 }, td.Gt(0))) // succeeds
//
// If the input is empty (and/or nil for a slice), an "item not found"
// error is raised before comparing to expectedValue.
//
// var got []int
// td.Cmp(t, got, td.Last(td.Gt(0), td.Gt(0))) // fails
// td.Cmp(t, []int{}, td.Last(td.Gt(0), td.Gt(0))) // fails
// td.Cmp(t, [0]int{}, td.Last(td.Gt(0), td.Gt(0))) // fails
//
// See also [First] and [Grep].
func Last(filter, expectedValue any) TestDeep {
g := tdLast{}
g.initGrepBase(filter, expectedValue)
return &g
}
func (g *tdLast) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
if g.err != nil {
return ctx.CollectError(g.err)
}
if rErr := grepResolvePtr(ctx, &got); rErr != nil {
return rErr
}
switch got.Kind() {
case reflect.Slice, reflect.Array:
for idx := got.Len() - 1; idx >= 0; idx-- {
item := got.Index(idx)
ok, rErr := g.matchItem(ctx, idx, item)
if rErr != nil {
return rErr
}
if ok {
return deepValueEqual(
ctx.AddCustomLevel(S("", idx)),
item,
g.expectedValue,
)
}
}
return g.notFound(ctx, got)
}
return grepBadKind(ctx, got)
}
func (g *tdLast) TypeBehind() reflect.Type {
return g.sliceTypeBehind()
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_grep_test.go 0000664 0000000 0000000 00000047415 14543133116 0024010 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"),
})
})
}
func TestGrepTypeBehind(t *testing.T) {
equalTypes(t, td.Grep(func(n int) bool { return true }, []int{33}), []int{})
equalTypes(t, td.Grep(td.Gt("0"), []string{"33"}), []string{})
// Erroneous op
equalTypes(t, td.Grep(42, 33), nil)
}
func TestGrepString(t *testing.T) {
test.EqualStr(t,
td.Grep(func(n int) bool { return true }, []int{}).String(),
"Grep(func(int) bool)")
test.EqualStr(t, td.Grep(td.Gt(0), []int{}).String(), "Grep(> 0)")
// Erroneous op
test.EqualStr(t, td.Grep(42, []int{}).String(), "Grep()")
}
func TestFirst(t *testing.T) {
t.Run("basic", func(t *testing.T) {
got := [...]int{-3, -2, -1, 0, 1, 2, 3}
sgot := got[:]
testCases := []struct {
name string
got any
}{
{"slice", sgot},
{"array", got},
{"*slice", &sgot},
{"*array", &got},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
checkOK(t, tc.got, td.First(td.Gt(0), 1))
checkOK(t, tc.got, td.First(td.Not(td.Between(-3, 2)), 3))
checkOK(t, tc.got, td.First(
func(x int) bool { return (x & 1) == 0 },
-2))
checkOK(t, tc.got, td.First(
func(x int64) bool { return (x & 1) != 0 },
-3),
"int64 filter vs int items")
checkOK(t, tc.got, td.First(
func(x any) bool { return (x.(int) & 1) == 0 },
-2),
"any filter vs int items")
checkError(t, tc.got,
td.First(td.Gt(666), "never reached"),
expectedError{
Message: mustBe("item not found"),
Path: mustBe("DATA"),
Got: mustContain(`]int) (len=7 `),
Expected: mustBe("First(> 666)"),
})
})
}
})
t.Run("struct", func(t *testing.T) {
type person struct {
ID int64
Name string
}
got := [...]person{
{ID: 1, Name: "Joe"},
{ID: 2, Name: "Bob"},
{ID: 3, Name: "Alice"},
{ID: 4, Name: "Brian"},
{ID: 5, Name: "Britt"},
}
sgot := got[:]
testCases := []struct {
name string
got any
}{
{"slice", sgot},
{"array", got},
{"*slice", &sgot},
{"*array", &got},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
checkOK(t, tc.got, td.First(
td.JSONPointer("/Name", td.HasPrefix("Br")),
person{ID: 4, Name: "Brian"}))
checkOK(t, tc.got, td.First(
func(p person) bool { return p.ID < 3 },
person{ID: 1, Name: "Joe"}))
})
}
})
t.Run("interfaces", func(t *testing.T) {
got := [...]any{-3, -2, -1, 0, 1, 2, 3}
sgot := got[:]
testCases := []struct {
name string
got any
}{
{"slice", sgot},
{"array", got},
{"*slice", &sgot},
{"*array", &got},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
checkOK(t, tc.got, td.First(td.Gt(0), 1))
checkOK(t, tc.got, td.First(td.Not(td.Between(-3, 2)), 3))
checkOK(t, tc.got, td.First(
func(x int) bool { return (x & 1) == 0 },
-2))
checkOK(t, tc.got, td.First(
func(x int64) bool { return (x & 1) != 0 },
-3),
"int64 filter vs any/int items")
checkOK(t, tc.got, td.First(
func(x any) bool { return (x.(int) & 1) == 0 },
-2),
"any filter vs any/int items")
})
}
})
t.Run("interfaces error", func(t *testing.T) {
got := [...]any{123, "foo"}
sgot := got[:]
testCases := []struct {
name string
got any
}{
{"slice", sgot},
{"array", got},
{"*slice", &sgot},
{"*array", &got},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
checkError(t, tc.got,
td.First(func(x int) bool { return false }, "never reached"),
expectedError{
Message: mustBe("incompatible parameter type"),
Path: mustBe("DATA[1]"),
Got: mustBe("string"),
Expected: mustBe("int"),
})
})
}
})
t.Run("nil slice", func(t *testing.T) {
var got []int
testCases := []struct {
name string
got any
}{
{"slice", got},
{"*slice", &got},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
checkError(t, tc.got,
td.First(td.Gt(666), "never reached"),
expectedError{
Message: mustBe("item not found"),
Path: mustBe("DATA"),
Got: mustBe("([]int) "),
Expected: mustBe("First(> 666)"),
})
})
}
})
t.Run("nil pointer", func(t *testing.T) {
checkError(t, (*[]int)(nil), td.First(td.Ignore(), 33),
expectedError{
Message: mustBe("nil pointer"),
Path: mustBe("DATA"),
Got: mustBe("nil *slice (*[]int type)"),
Expected: mustBe("non-nil *slice OR *array"),
})
})
t.Run("JSON", func(t *testing.T) {
got := map[string]any{
"values": []int{1, 2, 3, 4},
}
checkOK(t, got, td.JSON(`{"values": First(Gt(2), 3)}`))
})
t.Run("errors", func(t *testing.T) {
for _, filter := range []any{nil, 33} {
checkError(t, "never tested",
td.First(filter, 42),
expectedError{
Message: mustBe("bad usage of First operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: First(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must be a function or FILTER_TESTDEEP_OPERATOR a TestDeep operator"),
},
"filter:", filter)
}
for _, filter := range []any{
func() bool { return true },
func(a, b int) bool { return true },
func(a ...int) bool { return true },
} {
checkError(t, "never tested",
td.First(filter, 42),
expectedError{
Message: mustBe("bad usage of First operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: First(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must take only one non-variadic argument"),
},
"filter:", filter)
}
for _, filter := range []any{
func(a int) {},
func(a int) int { return 0 },
func(a int) (bool, bool) { return true, true },
} {
checkError(t, "never tested",
td.First(filter, 42),
expectedError{
Message: mustBe("bad usage of First operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: First(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must return bool"),
},
"filter:", filter)
}
checkError(t, &struct{}{}, td.First(td.Ignore(), 33),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("*struct (*struct {} type)"),
Expected: mustBe("slice OR array OR *slice OR *array"),
})
checkError(t, nil, td.First(td.Ignore(), 33),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("slice OR array OR *slice OR *array"),
})
})
}
func TestFirstString(t *testing.T) {
test.EqualStr(t,
td.First(func(n int) bool { return true }, 33).String(),
"First(func(int) bool)")
test.EqualStr(t, td.First(td.Gt(0), 33).String(), "First(> 0)")
// Erroneous op
test.EqualStr(t, td.First(42, 33).String(), "First()")
}
func TestFirstTypeBehind(t *testing.T) {
equalTypes(t, td.First(func(n int) bool { return true }, 33), []int{})
equalTypes(t, td.First(td.Gt("x"), "x"), []string{})
// Erroneous op
equalTypes(t, td.First(42, 33), nil)
}
func TestLast(t *testing.T) {
t.Run("basic", func(t *testing.T) {
got := [...]int{-3, -2, -1, 0, 1, 2, 3}
sgot := got[:]
testCases := []struct {
name string
got any
}{
{"slice", sgot},
{"array", got},
{"*slice", &sgot},
{"*array", &got},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
checkOK(t, tc.got, td.Last(td.Lt(0), -1))
checkOK(t, tc.got, td.Last(td.Not(td.Between(1, 3)), 0))
checkOK(t, tc.got, td.Last(
func(x int) bool { return (x & 1) == 0 },
2))
checkOK(t, tc.got, td.Last(
func(x int64) bool { return (x & 1) != 0 },
3),
"int64 filter vs int items")
checkOK(t, tc.got, td.Last(
func(x any) bool { return (x.(int) & 1) == 0 },
2),
"any filter vs int items")
checkError(t, tc.got,
td.Last(td.Gt(666), "never reached"),
expectedError{
Message: mustBe("item not found"),
Path: mustBe("DATA"),
Got: mustContain(`]int) (len=7 `),
Expected: mustBe("Last(> 666)"),
})
})
}
})
t.Run("struct", func(t *testing.T) {
type person struct {
ID int64
Name string
}
got := [...]person{
{ID: 1, Name: "Joe"},
{ID: 2, Name: "Bob"},
{ID: 3, Name: "Alice"},
{ID: 4, Name: "Brian"},
{ID: 5, Name: "Britt"},
}
sgot := got[:]
testCases := []struct {
name string
got any
}{
{"slice", sgot},
{"array", got},
{"*slice", &sgot},
{"*array", &got},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
checkOK(t, tc.got, td.Last(
td.JSONPointer("/Name", td.HasPrefix("Br")),
person{ID: 5, Name: "Britt"}))
checkOK(t, tc.got, td.Last(
func(p person) bool { return p.ID < 3 },
person{ID: 2, Name: "Bob"}))
})
}
})
t.Run("interfaces", func(t *testing.T) {
got := [...]any{-3, -2, -1, 0, 1, 2, 3}
sgot := got[:]
testCases := []struct {
name string
got any
}{
{"slice", sgot},
{"array", got},
{"*slice", &sgot},
{"*array", &got},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
checkOK(t, tc.got, td.Last(td.Lt(0), -1))
checkOK(t, tc.got, td.Last(td.Not(td.Between(1, 3)), 0))
checkOK(t, tc.got, td.Last(
func(x int) bool { return (x & 1) == 0 },
2))
checkOK(t, tc.got, td.Last(
func(x int64) bool { return (x & 1) != 0 },
3),
"int64 filter vs any/int items")
checkOK(t, tc.got, td.Last(
func(x any) bool { return (x.(int) & 1) == 0 },
2),
"any filter vs any/int items")
})
}
})
t.Run("interfaces error", func(t *testing.T) {
got := [...]any{123, "foo", 456}
sgot := got[:]
testCases := []struct {
name string
got any
}{
{"slice", sgot},
{"array", got},
{"*slice", &sgot},
{"*array", &got},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
checkError(t, tc.got,
td.Last(func(x int) bool { return false }, "never reached"),
expectedError{
Message: mustBe("incompatible parameter type"),
Path: mustBe("DATA[1]"),
Got: mustBe("string"),
Expected: mustBe("int"),
})
})
}
})
t.Run("nil slice", func(t *testing.T) {
var got []int
testCases := []struct {
name string
got any
}{
{"slice", got},
{"*slice", &got},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
checkError(t, tc.got,
td.Last(td.Gt(666), "never reached"),
expectedError{
Message: mustBe("item not found"),
Path: mustBe("DATA"),
Got: mustBe("([]int) "),
Expected: mustBe("Last(> 666)"),
})
})
}
})
t.Run("nil pointer", func(t *testing.T) {
checkError(t, (*[]int)(nil), td.Last(td.Ignore(), 33),
expectedError{
Message: mustBe("nil pointer"),
Path: mustBe("DATA"),
Got: mustBe("nil *slice (*[]int type)"),
Expected: mustBe("non-nil *slice OR *array"),
})
})
t.Run("JSON", func(t *testing.T) {
got := map[string]any{
"values": []int{1, 2, 3, 4},
}
checkOK(t, got, td.JSON(`{"values": Last(Lt(3), 2)}`))
})
t.Run("errors", func(t *testing.T) {
for _, filter := range []any{nil, 33} {
checkError(t, "never tested",
td.Last(filter, 42),
expectedError{
Message: mustBe("bad usage of Last operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Last(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must be a function or FILTER_TESTDEEP_OPERATOR a TestDeep operator"),
},
"filter:", filter)
}
for _, filter := range []any{
func() bool { return true },
func(a, b int) bool { return true },
func(a ...int) bool { return true },
} {
checkError(t, "never tested",
td.Last(filter, 42),
expectedError{
Message: mustBe("bad usage of Last operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Last(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must take only one non-variadic argument"),
},
"filter:", filter)
}
for _, filter := range []any{
func(a int) {},
func(a int) int { return 0 },
func(a int) (bool, bool) { return true, true },
} {
checkError(t, "never tested",
td.Last(filter, 42),
expectedError{
Message: mustBe("bad usage of Last operator"),
Path: mustBe("DATA"),
Summary: mustBe("usage: Last(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must return bool"),
},
"filter:", filter)
}
checkError(t, &struct{}{}, td.Last(td.Ignore(), 33),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("*struct (*struct {} type)"),
Expected: mustBe("slice OR array OR *slice OR *array"),
})
checkError(t, nil, td.Last(td.Ignore(), 33),
expectedError{
Message: mustBe("bad kind"),
Path: mustBe("DATA"),
Got: mustBe("nil"),
Expected: mustBe("slice OR array OR *slice OR *array"),
})
})
}
func TestLastString(t *testing.T) {
test.EqualStr(t,
td.Last(func(n int) bool { return true }, 33).String(),
"Last(func(int) bool)")
test.EqualStr(t, td.Last(td.Gt(0), 33).String(), "Last(> 0)")
// Erroneous op
test.EqualStr(t, td.Last(42, 33).String(), "Last()")
}
func TestLastTypeBehind(t *testing.T) {
equalTypes(t, td.Last(func(n int) bool { return true }, 33), []int{})
equalTypes(t, td.Last(td.Gt("x"), "x"), []string{})
// Erroneous op
equalTypes(t, td.Last(42, 33), nil)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_ignore.go 0000664 0000000 0000000 00000002073 14543133116 0023266 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()"
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_ignore_test.go 0000664 0000000 0000000 00000001145 14543133116 0024324 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_isa.go 0000664 0000000 0000000 00000004600 14543133116 0022555 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()
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_isa_test.go 0000664 0000000 0000000 00000006424 14543133116 0023622 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_json.go 0000664 0000000 0000000 00000130702 14543133116 0022755 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": "",
"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
addNilParam := false
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 "SubMapOf", "SuperMapOf":
min, max, addNilParam = 1, 1, true
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 addNilParam {
in = append(in, reflect.ValueOf(MapEntries(nil)))
}
// 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], [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], [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], [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
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_json_pointer.go 0000664 0000000 0000000 00000011556 14543133116 0024522 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 + ">")
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_json_pointer_test.go 0000664 0000000 0000000 00000020170 14543133116 0025551 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_json_test.go 0000664 0000000 0000000 00000116474 14543133116 0024026 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()
//
// 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_keys_values.go 0000664 0000000 0000000 00000010116 14543133116 0024332 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())
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_keys_values_test.go 0000664 0000000 0000000 00000010607 14543133116 0025376 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_lax.go 0000664 0000000 0000000 00000005554 14543133116 0022576 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()
}
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_lax_test.go 0000664 0000000 0000000 00000004017 14543133116 0023626 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_len_cap.go 0000664 0000000 0000000 00000012221 14543133116 0023400 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"))
}
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_len_cap_test.go 0000664 0000000 0000000 00000016536 14543133116 0024454 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_list.go 0000664 0000000 0000000 00000001236 14543133116 0022756 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/flat"
"github.com/maxatome/go-testdeep/internal/util"
)
type tdList struct {
baseOKNil
items []reflect.Value
}
func newList(items ...any) tdList {
return tdList{
baseOKNil: newBaseOKNil(4),
items: flat.Values(items),
}
}
func (l *tdList) String() string {
var b strings.Builder
b.WriteString(l.GetLocation().Func)
return util.SliceToString(&b, l.items).
String()
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_map.go 0000664 0000000 0000000 00000023704 14543133116 0022564 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 newMap(model any, entries MapEntries, kind mapKind) *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(entries, reflect.Value{})
return &m
}
vmodel = vmodel.Elem()
fallthrough
case reflect.Map:
m.expectedType = vmodel.Type()
m.populateExpectedEntries(entries, 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:
m.err = ctxerr.OpBad(
m.GetLocation().Func,
"expected key %s value cannot be nil as entries value type is %s",
util.ToString(key),
valueType)
return
}
} else {
entryInfo.expected = reflect.ValueOf(expectedValue)
if _, ok := expectedValue.(TestDeep); !ok {
if !entryInfo.expected.Type().AssignableTo(valueType) {
m.err = ctxerr.OpBad(
m.GetLocation().Func,
"expected key %s value type mismatch: %s != model key type (%s)",
util.ToString(key),
entryInfo.expected.Type(),
valueType)
return
}
}
}
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 nil, if no zero entries are expected and
// no [TestDeep] operators are involved.
//
// 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, expectedEntries, allMap)
}
// 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 nil, if no zero entries are expected and
// no [TestDeep] operators are involved.
//
// 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, expectedEntries, subMap)
}
// 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 nil, if no zero entries are expected and
// no [TestDeep] operators are involved.
//
// 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, expectedEntries, superMap)
}
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()
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_map_each.go 0000664 0000000 0000000 00000004421 14543133116 0023537 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 + ")"
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_map_each_test.go 0000664 0000000 0000000 00000005557 14543133116 0024611 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_map_test.go 0000664 0000000 0000000 00000033430 14543133116 0023620 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}, 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}))
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}, 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}))
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}, nil),
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}, 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}))
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}, nil),
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, "never tested",
td.Map(&MyMap{}, td.MapEntries{"foo": nil}),
expectedError{
Message: mustBe("bad usage of Map operator"),
Path: mustBe("DATA"),
Summary: mustBe(`expected key "foo" value cannot be nil as entries value type is int`),
})
checkError(t, "never tested",
td.Map(&MyMap{}, td.MapEntries{"foo": uint16(2)}),
expectedError{
Message: mustBe("bad usage of Map operator"),
Path: mustBe("DATA"),
Summary: mustBe(`expected key "foo" value type mismatch: uint16 != model key type (int)`),
})
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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_nan.go 0000664 0000000 0000000 00000004357 14543133116 0022566 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"
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_nan_test.go 0000664 0000000 0000000 00000003353 14543133116 0023620 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_nil.go 0000664 0000000 0000000 00000005444 14543133116 0022572 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"
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_nil_test.go 0000664 0000000 0000000 00000005571 14543133116 0023632 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_none.go 0000664 0000000 0000000 00000004141 14543133116 0022740 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"
"github.com/maxatome/go-testdeep/internal/ctxerr"
)
type tdNone struct {
tdList
}
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{
tdList: newList(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{
tdList: newList(notExpected),
}
}
func (n *tdNone) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error {
for idx, item := range n.items {
if deepValueEqualFinalOK(ctx, got, item) {
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
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_none_test.go 0000664 0000000 0000000 00000003520 14543133116 0023777 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)"),
})
//
// 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)"),
})
//
// 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_ptr.go 0000664 0000000 0000000 00000011764 14543133116 0022617 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()
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_ptr_test.go 0000664 0000000 0000000 00000013446 14543133116 0023655 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_re.go 0000664 0000000 0000000 00000016202 14543133116 0022410 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"
"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()),
}
}
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
//
// 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()
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_re_test.go 0000664 0000000 0000000 00000011172 14543133116 0023450 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_recv.go 0000664 0000000 0000000 00000015131 14543133116 0022741 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()
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_recv_test.go 0000664 0000000 0000000 00000014215 14543133116 0024002 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_set.go 0000664 0000000 0000000 00000015321 14543133116 0022576 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
// 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] and [Bag].
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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_set_base.go 0000664 0000000 0000000 00000007432 14543133116 0023574 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
}
if deepValueEqualFinalOK(ctx, got.Index(idx), expected) {
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 {
if deepValueEqualFinalOK(ctx, got.Index(idxGot), expected) {
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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_set_result.go 0000664 0000000 0000000 00000003440 14543133116 0024173 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
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_set_test.go 0000664 0000000 0000000 00000014605 14543133116 0023641 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)
// 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)
// 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)
// 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)
// 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_shallow.go 0000664 0000000 0000000 00000007220 14543133116 0023453 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_shallow_test.go 0000664 0000000 0000000 00000007741 14543133116 0024522 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_smuggle.go 0000664 0000000 0000000 00000056307 14543133116 0023457 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-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"
"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
}
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)
}
}
return buf.String()
}
func splitFieldsPath(origPath string) ([]smuggleField, error) {
if origPath == "" {
return nil, fmt.Errorf("FIELD_PATH cannot be empty")
}
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 FIELD_PATH %q", origPath)
}
res = append(res, smuggleField{Name: path[:end], Indexed: true})
path = path[end+1:]
case '.':
if len(res) == 0 {
return nil, fmt.Errorf("'.' cannot be the first rune in FIELD_PATH %q", origPath)
}
path = path[1:]
if path == "" {
return nil, fmt.Errorf("final '.' in FIELD_PATH %q is not allowed", origPath)
}
r, _ = utf8.DecodeRuneInString(path)
if r == '.' || r == '[' {
return nil, fmt.Errorf("unexpected %q after '.' in FIELD_PATH %q", r, origPath)
}
fallthrough
default:
var field string
end := strings.IndexAny(path, ".[")
if end < 0 {
field, path = path, ""
} else {
field, path = path[:end], path[end:]
}
for j, r := range 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 {
// Resolve all interface and pointer dereferences
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
}
if idxPart == 0 {
return smuggleValue{},
fmt.Errorf("it is a %s and should be a struct", vgot.Kind())
}
return smuggleValue{}, fmt.Errorf(
"field %q is a %s and should be a struct",
joinFieldsPath(parts[:idxPart]), vgot.Kind())
}
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 }
// 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))
//
// Contrary to [JSONPointer] operator, private fields can be
// followed. 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 add some checks against nil values and
// auto-dereference 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
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_smuggle_private_test.go 0000664 0000000 0000000 00000011520 14543133116 0026234 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"
"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 {
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)
}
//
// Errors
checkErr := func(in, expectedErr string) {
t.Helper()
_, err := splitFieldsPath(in)
if test.Error(t, err) {
test.EqualStr(t, err.Error(), expectedErr)
}
}
checkErr("", "FIELD_PATH cannot be empty")
checkErr(".test", `'.' cannot be the first rune in FIELD_PATH ".test"`)
checkErr("foo.bar.", `final '.' in FIELD_PATH "foo.bar." is not allowed`)
checkErr("foo..bar", `unexpected '.' after '.' in FIELD_PATH "foo..bar"`)
checkErr("foo.[bar]", `unexpected '[' after '.' in FIELD_PATH "foo.[bar]"`)
checkErr("foo[bar", `cannot find final ']' in FIELD_PATH "foo[bar"`)
checkErr("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[bar", `cannot find final ']' in FIELD_PATH "foo[bar"`)
}
func TestBuildFieldsPathFn(t *testing.T) {
_, err := buildFieldsPathFn("bad[path")
test.Error(t, err)
//
// Struct
type Build struct {
Field struct {
Path string
}
Iface any
}
fn, err := buildFieldsPathFn("Field.Path.Bad")
if test.NoError(t, err) {
_, err = fn(Build{})
if test.Error(t, err) {
test.EqualStr(t, err.Error(),
`field "Field.Path" is a string and should be a struct`)
}
_, err = fn(123)
if test.Error(t, err) {
test.EqualStr(t, err.Error(), "it is a int and should be a struct")
}
}
fn, err = buildFieldsPathFn("Field.Unknown")
if test.NoError(t, err) {
_, err = fn(Build{})
if test.Error(t, err) {
test.EqualStr(t, err.Error(), `field "Field.Unknown" not found`)
}
}
//
// Map
fn, err = buildFieldsPathFn("Iface[str].Field")
if test.NoError(t, err) {
_, err = fn(Build{Iface: map[int]Build{}})
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(Build{Iface: map[uint]Build{}})
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(Build{Iface: map[float32]Build{}})
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(Build{Iface: map[complex128]Build{}})
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(Build{Iface: map[struct{ A int }]Build{}})
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(Build{Iface: map[string]Build{}})
if test.Error(t, err) {
test.EqualStr(t, err.Error(), `field "Iface[str]", "str" map key not found`)
}
}
//
// Array / Slice
fn, err = buildFieldsPathFn("Iface[str].Field")
if test.NoError(t, err) {
_, err = fn(Build{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(Build{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(Build{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`)
}
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_smuggle_test.go 0000664 0000000 0000000 00000050214 14543133116 0024505 0 ustar 00root root 0000000 0000000 // Copyright (c) 2018-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 (
"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 FIELD_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
// 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"),
})
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("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()))
//
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 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_smuggler_base.go 0000664 0000000 0000000 00000005304 14543133116 0024622 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_string.go 0000664 0000000 0000000 00000012431 14543133116 0023310 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 "", ctx.CollectError(&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 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 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 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) + ")"
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_string_test.go 0000664 0000000 0000000 00000007575 14543133116 0024364 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_struct.go 0000664 0000000 0000000 00000054664 14543133116 0023344 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, "")
if st.err != nil {
return st
}
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),
)
if st.err != nil {
return st
}
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),
)
if st.err != nil {
return st
}
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:
s.err = ctxerr.OpBad(s.location.Func,
"expected value of field %s%s cannot be nil as it is a %s",
field.Name, ctxInfo, field.Type)
return
}
} else {
vexpectedValue = reflect.ValueOf(expectedValue)
if _, ok := expectedValue.(TestDeep); !ok {
if !vexpectedValue.Type().AssignableTo(field.Type) {
s.err = ctxerr.OpBad(s.location.Func,
"type %s of field expected value %s%s differs from struct one (%s)",
vexpectedValue.Type(),
field.Name,
ctxInfo,
field.Type)
return
}
}
}
s.expectedFields = append(s.expectedFields, fieldInfo{
name: field.Name,
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()
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_struct_lazy.go 0000664 0000000 0000000 00000003711 14543133116 0024366 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()
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_struct_lazy_test.go 0000664 0000000 0000000 00000010612 14543133116 0025423 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: badUsage,
Path: mustBe("DATA"),
Summary: mustBe("type string of field expected value ValInt differs from struct one (int)"),
})
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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_struct_private_test.go 0000664 0000000 0000000 00000006265 14543133116 0026127 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(*`: "))
}
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_struct_test.go 0000664 0000000 0000000 00000067067 14543133116 0024404 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, "never tested",
td.Struct(&MyStruct{}, td.StructFields{"ValBool": 123}),
expectedError{
Message: mustBe("bad usage of Struct operator"),
Path: mustBe("DATA"),
Summary: mustBe("type int of field expected value ValBool differs from struct one (bool)"),
})
checkError(t, "never tested",
td.Struct(&MyStruct{}, td.StructFields{">ValBool": 123}),
expectedError{
Message: mustBe("bad usage of Struct operator"),
Path: mustBe("DATA"),
Summary: mustBe(`type int of field expected value ValBool (from ">ValBool") differs from struct one (bool)`),
})
checkError(t, "never tested",
td.Struct(&MyStruct{}, td.StructFields{"ValBool": nil}),
expectedError{
Message: mustBe("bad usage of Struct operator"),
Path: mustBe("DATA"),
Summary: mustBe("expected value of field ValBool cannot be nil as it is a bool"),
})
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: NotZero()
ValStr: 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"),
})
}
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, "never tested",
td.Struct(paTest{Num: 666}, td.StructFields{"= alpha*": nil}), expectedError{
Message: mustBe("bad usage of Struct operator"),
Path: mustBe("DATA"),
Summary: mustBe("expected value of field alphaNum (from pattern `= alpha*`) cannot be nil as it is a int"),
})
})
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, "never tested",
td.Struct(paTest{Num: 666}, td.StructFields{"=~ alpha": nil}),
expectedError{
Message: mustBe("bad usage of Struct operator"),
Path: mustBe("DATA"),
Summary: mustBe("expected value of field alphaNum (from pattern `=~ alpha`) cannot be nil as it is a int"),
})
})
}
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, "never tested",
td.SStruct(&MyStruct{}, td.StructFields{"ValBool": 123}),
expectedError{
Message: mustBe("bad usage of SStruct operator"),
Path: mustBe("DATA"),
Summary: mustBe("type int of field expected value ValBool differs from struct one (bool)"),
})
checkError(t, "never tested",
td.SStruct(&MyStruct{}, td.StructFields{">ValBool": 123}),
expectedError{
Message: mustBe("bad usage of SStruct operator"),
Path: mustBe("DATA"),
Summary: mustBe(`type int of field expected value ValBool (from ">ValBool") differs from struct one (bool)`),
})
checkError(t, "never tested",
td.SStruct(&MyStruct{}, td.StructFields{"ValBool": nil}),
expectedError{
Message: mustBe("bad usage of SStruct operator"),
Path: mustBe("DATA"),
Summary: mustBe("expected value of field ValBool cannot be nil as it is a bool"),
})
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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_tag.go 0000664 0000000 0000000 00000004551 14543133116 0022561 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
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_tag_test.go 0000664 0000000 0000000 00000003255 14543133116 0023620 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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_trunc_time.go 0000664 0000000 0000000 00000007232 14543133116 0024156 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()
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_trunc_time_test.go 0000664 0000000 0000000 00000011653 14543133116 0025217 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"),
})
}
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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_zero.go 0000664 0000000 0000000 00000005073 14543133116 0022765 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()"
}
golang-github-maxatome-go-testdeep-1.14.0/td/td_zero_test.go 0000664 0000000 0000000 00000012215 14543133116 0024020 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: mustContain(`ValInt: (int) 0`),
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: mustContain(`ValInt: (int) 0`),
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: mustContain(`ValInt: (int) 0`),
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)
}
golang-github-maxatome-go-testdeep-1.14.0/td/tuple.go 0000664 0000000 0000000 00000003164 14543133116 0022447 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]
}
golang-github-maxatome-go-testdeep-1.14.0/td/tuple_test.go 0000664 0000000 0000000 00000002325 14543133116 0023504 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),
)
}
golang-github-maxatome-go-testdeep-1.14.0/td/types.go 0000664 0000000 0000000 00000012346 14543133116 0022464 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
}
golang-github-maxatome-go-testdeep-1.14.0/td/types_test.go 0000664 0000000 0000000 00000006210 14543133116 0023514 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.T{}
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.T{}
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.T{}
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.T{}
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.T{}
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.T{}
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"))
}
}
golang-github-maxatome-go-testdeep-1.14.0/td/uniq_type_behind.go 0000664 0000000 0000000 00000003107 14543133116 0024641 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
}
golang-github-maxatome-go-testdeep-1.14.0/td/utils.go 0000664 0000000 0000000 00000001526 14543133116 0022456 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
}
golang-github-maxatome-go-testdeep-1.14.0/td/utils_test.go 0000664 0000000 0000000 00000003505 14543133116 0023514 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")
}
}
}
golang-github-maxatome-go-testdeep-1.14.0/td_compat.go 0000664 0000000 0000000 00000027102 14543133116 0022657 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
golang-github-maxatome-go-testdeep-1.14.0/td_compat_test.go 0000664 0000000 0000000 00000022667 14543133116 0023731 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)
})
}
golang-github-maxatome-go-testdeep-1.14.0/testdeep.go 0000664 0000000 0000000 00000003323 14543133116 0022521 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"
golang-github-maxatome-go-testdeep-1.14.0/tools/ 0000775 0000000 0000000 00000000000 14543133116 0021514 5 ustar 00root root 0000000 0000000 golang-github-maxatome-go-testdeep-1.14.0/tools/gen_funcs.pl 0000775 0000000 0000000 00000105056 14543133116 0024032 0 ustar 00root root 0000000 0000000 #!/usr/bin/env perl
# 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.
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-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.
//
// 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,
# These operators accept several StructFields,
# but we want only one here
Struct => 'nil',
SStruct => '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 ioutil 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 = <{type})
{
if ($arg->{type} ne 'any' or $arg->{variadic})
{
$cmp_args .= ' any';
}
last
}
}
}
else
{
$cmp_args .= ' any';
}
my $call_args = '';
my @cmpt_args;
foreach my $arg (@{$funcs{$func}{args}})
{
$call_args .= ', ' unless $call_args eq '';
$call_args .= $arg->{name};
push(@cmpt_args, { name => $arg->{name} });
$cmp_args .= ", $arg->{name} ";
if ($arg->{variadic})
{
$call_args .= '...';
$cmp_args .= '[]';
}
$cmp_args .= $arg->{type} if defined $arg->{type};
}
my $cmp_doc = <{default})
{
my $default = $last_arg->{default};
$default = "[$1]" if $default =~ /^td\.(.+)/ and exists $consts{$1};
$func_comment .= <{name} is here mandatory.
$default value should be passed to mimic its absence in
original [$func] call.
EOF
}
$func_comment .= < $name // '', code => $code });
}
}
{
open(my $fh, "| gofmt -s > '$DIR/cmp_funcs.go'");
print $fh $funcs_contents;
close $fh;
say "$DIR/cmp_funcs.go generated";
}
{
open(my $fh, "| gofmt -s > '$DIR/t.go'");
print $fh $t_contents;
close $fh;
say "$DIR/t.go generated";
}
my $funcs_test_contents = <{name};
foreach my $info ([ "td.Cmp$func(t, ", "Cmp$func", \$funcs_test_contents ],
[ "t.$method(", "T_$method",\$t_test_contents ])
{
(my $code = $example->{code}) =~
s%td\.Cmp\(t,\s+($rparam),\s+td\.$func($rep)%
my @params = extract_params("$func$name", $2);
my $repl = $info->[0] . $1;
for (my $i = 0; $i < @$args; $i++)
{
$repl .= ', ';
if ($args->[$i]{variadic})
{
if (defined $params[$i])
{
$repl .= '[]' . $args->[$i]{type} . '{'
. join(', ', @params[$i .. $#params])
. '}';
}
else
{
$repl .= 'nil';
}
last
}
$repl .= $params[$i]
// $args->[$i]{default}
// die("Internal error, no param: "
. "$func$name -> #$i/$args->[$i]{name}!\n");
}
$repl
%egs;
${$info->[2]} .= <[1]$name() {
$code}
EOF
}
}
}
{
# Cmp* examples
open(my $fh, "| gofmt -s > '$DIR/example_cmp_test.go'");
print $fh $funcs_test_contents;
close $fh;
say "$DIR/example_cmp_test.go generated";
}
{
# T.* examples
$t_test_contents =~ s/t := &testing\.T\{\}/t := td.NewT(&testing\.T\{\})/g;
$t_test_contents =~ s/td\.Cmp\(t,/t.Cmp(/g;
open(my $fh, "| gofmt -s > '$DIR/example_t_test.go'");
print $fh $t_test_contents;
close $fh;
say "$DIR/example_t_test.go generated";
}
#
# Check "args..." comment is the same everywhere it needs to be
my @args_errors;
$ARGS_COMMENT_GD = go_comment($ARGS_COMMENT_GD);
foreach my $go_file (do { opendir(my $dh, $DIR);
grep /(?[0]`]: https://pkg.go.dev/$_->[1]",
[ 'fmt.Stringer' => 'fmt/#Stringer' ],
[ 'time.Time' => 'time/#Time' ],
[ 'math.NaN' => 'math/#NaN' ])
. "\n";
};
my @sorted_operators = sort { lc($a) cmp lc($b) } keys %operators;
my $md_links = do
{
$common_links
. join("\n", map qq([`$_`]: {{< ref "$_" >}}), @sorted_operators)
. "\n\n"
# Cmp* functions
. join("\n", map qq([`Cmp$_`]: {{< ref "$_#cmp\L$_\E-shortcut" >}}),
@sorted_funcs)
. "\n\n"
# T.Cmp* methods
. join("\n", map
{
my $m = $RENAME_METHOD{$_} // $_;
qq([`T.$m`]: {{< ref "$_#t\L$m\E-shortcut" >}})
}
@sorted_funcs);
};
my $gh_links = do
{
my $td_url = "$URL_ZETTA/operators/";
$common_links
. join("\n", map qq([`$_`]: $td_url\L$_/), @sorted_operators)
. "\n\n"
# Cmp* functions
. join("\n", map qq([`Cmp$_`]: $td_url\L$_/#cmp$_-shortcut), @sorted_funcs)
. "\n\n"
# T.Cmp* methods
. join("\n", map
{
my $m = $RENAME_METHOD{$_} // $_;
qq([`T.$m`]: $td_url\L$_/#t$m-shortcut)
}
@sorted_funcs);
};
# README.md
{
my $readme = slurp("$REPO_DIR/README.md");
# Links
$readme =~ s{().*()}
{$1\n$gh_links\n$2}s;
open(my $fh, '>', "$REPO_DIR/README.md.new");
print $fh $readme;
close $fh;
rename "$REPO_DIR/README.md.new", "$REPO_DIR/README.md";
say "$REPO_DIR/README.md modified";
}
# Hugo
if (defined $SITE_REPO_DIR)
{
my $op_examples = slurp("$DIR/example_test.go");
# Reload generated examples so they are properly gofmt'ed
my $cmp_examples = slurp("$DIR/example_cmp_test.go");
my $t_examples = slurp("$DIR/example_t_test.go");
foreach my $operator (@sorted_operators)
{
# Rework each operator doc
my $doc = process_doc($operators{$operator});
open(my $fh, '>', "$SITE_REPO_DIR/docs_src/content/operators/$operator.md");
print $fh < See also [ $operator godoc](https://pkg.go.dev/github.com/maxatome/go-testdeep/td#$operator).
EOM
my @examples;
my $re = qr/^func Example${operator}(?:_(\w+))?\(\) \{\n(.+?)^\}$/ms;
while ($op_examples =~ /$re/g)
{
my $name = ucfirst($1 // 'Base');
push(@examples, < 1 ? 's' : '';
print $fh @examples;
}
if (my $cmp = $operators{$operator}{cmp})
{
$cmp->{imports} = $operators{$operator}{imports};
unshift(@{$cmp->{args}}, { name => 't' });
my $doc = process_doc($cmp);
shift @{$cmp->{args}};
print $fh <{name} shortcut
```go
$cmp->{signature}
```
$doc
> See also [ $cmp->{name} godoc](https://pkg.go.dev/github.com/maxatome/go-testdeep/td#$cmp->{name}).
EOM
@examples = ();
my $re = qr/func ExampleCmp${operator}(?:_(\w+))?\(\) \{\n(.+?)^\}$/ms;
while ($cmp_examples =~ /$re/g)
{
my $name = ucfirst($1 // 'Base');
push(@examples, < 1 ? 's' : '';
print $fh @examples;
}
}
if (my $t = $operators{$operator}{t})
{
$t->{imports} = $operators{$operator}{imports};
unshift(@{$t->{args}}, { name => 't' });
my $doc = process_doc($t);
shift @{$t->{args}};
print $fh <{name} shortcut
```go
$t->{signature}
```
$doc
> See also [ T.$t->{name} godoc](https://pkg.go.dev/github.com/maxatome/go-testdeep/td#T.$t->{name}).
EOM
@examples = ();
my $re = qr/func ExampleT_$t->{name}(?:_(\w+))?\(\) \{\n(.+?)^\}$/ms;
while ($t_examples =~ /$re/g)
{
my $name = ucfirst($1 // 'Base');
push(@examples, < 1 ? 's' : '';
print $fh @examples;
}
}
close $fh;
}
# Dump operators
{
my $op_list_file = "$SITE_REPO_DIR/docs_src/content/operators/_index.md";
my $op_list = slurp($op_list_file);
$op_list =~ s{().*()}
{
"$1\n"
. join('',
map qq\n: $operators{$_}{summary}\n\n!,
@sorted_operators)
. $2
}se or die "operators tags not found in $op_list_file\n";
$op_list =~ s{().*()}
{
"$1\n"
. join('',
map qq\n: $operators{$_}{summary}\n\n!,
sort { lc($a) cmp lc($b) }
keys %SMUGGLER_OPERATORS)
. "$md_links\n$2"
}se or die "smugglers tags not found in $op_list_file\n";
open(my $fh, '>', "$op_list_file.new");
print $fh $op_list;
close $fh;
rename "$op_list_file.new", $op_list_file;
}
# Dump matrices
{
my $matrix_file = "$SITE_REPO_DIR/docs_src/content/operators/matrix.md";
my $matrix = slurp($matrix_file);
my $header = <<'EOH';
| Operator vs go type | nil | bool | string | {u,}int* | float* | complex* | array | slice | map | struct | pointer | interface¹ | chan | func | operator |
| ------------------- | --- | ---- | ------ | -------- | ------ | -------- | ----- | ----- | --- | ------ | ------- | ---------- | ---- | ---- | -------- |
EOH
$matrix =~ s{().*()}
{
my $repl = "$1\n";
my $num = 0;
foreach my $op (@sorted_operators)
{
$repl .= $header if $num++ % 10 == 0;
$repl .= "| [`$op`]";
for my $label (@INPUT_LABELS)
{
$repl .= " | "
. ($operators{$op}{input}{$label} // '✗');
}
$repl .= " | [`$op`] |\n";
}
"$repl\n$md_links\n$2"
}se or die "op-go-matrix tags not found in $matrix_file\n";
my %by_input;
while (my($op, $info) = each %operators)
{
while (my($label, $comment) = each %{$operators{$op}{input}})
{
$by_input{$label}{$op} = $comment;
}
}
$matrix =~ s{().*()}
{
my $repl = "$1\n";
foreach my $op (sort keys %{$by_input{$2}})
{
my $comment = $by_input{$2}{$op};
next if $comment eq 'todo';
if ($comment eq '✓')
{
next if $2 eq 'if';
$comment = '';
}
elsif ($2 eq 'if')
{
$comment =~ s/^✓ \+/ →/;
}
else
{
substr($comment, 0, 0, ' only ');
}
$repl .= "- [`$op`]$comment\n";
}
$repl . $3
}gse or die "go-op-matrix tags not found in $matrix_file\n";
open(my $fh, '>', "$matrix_file.new");
print $fh $matrix;
close $fh;
rename "$matrix_file.new", $matrix_file;
}
# tdhttp example
{
my $example = slurp("$REPO_DIR/helpers/tdhttp/example_test.go");
my($import) = $example =~ /^(import \(.*?^\))$/ms;
$import or die "tdhttp example, import not found!\n";
$example =~ s/.*^func Example\(\) \{\n\tt := &testing.T\{\}\n\n//ms
or die "tdhttp example, func Example() not found!\n";
$example =~ s/fmt\.Printf/t.Logf/g
or die "tdhttp example, fmt.Printf not found\n";
$example =~ s/fmt\.Println/t.Log/g
or die "tdhttp example, fmt.Println not found\n";
$example =~ s,\n\t// Output:\n.*,},s
or die "tdhttp example, Output: not found\n";
my $md_file = "$SITE_REPO_DIR/docs_src/content/helpers/_index.md";
my $final = slurp($md_file) =~
s{().*()}
<$1
{{%expand "Main example" %}}```go
package myapi
$import
func TestMyAPI(t *testing.T) {
$example
```{{% /expand%}}
$2>rs or die "tdhttp example not found in $md_file!";
open(my $out, '>', "$md_file.new");
print $out $final;
close $out;
rename "$md_file.new", $md_file;
}
# Final publish
if ($ENV{PROD_SITE})
{
# Delegate to go-testdeep-site repository
chdir $SITE_REPO_DIR;
exec './publish.sh';
}
}
# "" → "//"
# "\txxx" → "//\txxx"
# "xxx" → "// xxx"
sub go_comment
{
shift =~ s{^(.?)}
{
$1 eq ''
? '//'
: substr($1, 0, 1) eq "\t" ? "//$1" : "// $1"
}egmr
}
sub doc2godoc
{
my $doc = shift;
state $repl = { arg => sub { $_[0] },
code => sub { $_[0] },
godoc => sub { $_[0] } };
$doc =~ s/%([a-z]+)\{([^}]+)\}/($repl->{$1} or die $1)->($2)/egr;
}
sub doc2md
{
my $doc = shift;
state $repl = { arg => sub { "*$_[0]*" },
code => sub { "`$_[0]`" },
godoc => sub
{
my($pkg, $fn) = split('\.', $_[0], 2);
"[`$_[0]`](https://pkg.go.dev/$pkg/#$fn)"
} };
$doc =~ s/%([a-z]+)\{([^}]+)\}/($repl->{$1} or die $1)->($2)/egr;
}
sub process_summary
{
my $text = shift;
$text =~ s/(time\.Time|fmt.Stringer|error)/[`$1`]/g;
$text =~ s/BeLax config flag/[`BeLax` config flag]/;
$text =~ s/(\[\]byte|\bnil\b)/`$1`/g;
return $text;
}
sub process_doc
{
my $op = shift;
my $doc = $op->{doc};
$doc =~ s,^// ?,,mg if $doc =~ m,^//,;
$doc =~ s/\n{3,}/\n\n/g;
my($inEx, $inBul);
$doc =~ s{^(?:(\n?\S)
|(\n?)(\s+)(\S+))}
<
if (defined $1)
{
if ($inEx) { $inEx = ''; "```\n$1" }
elsif ($inBul) { $inBul = ''; "\n$1" }
else { $1 }
}
else
{
my($nl, $indent, $beg) = ($2, $3, $4);
if ($inEx) { $nl . substr($indent, length($inEx)) . $beg }
elsif ($inBul) { $nl . substr($indent, length($inBul)) . $beg }
elsif ($beg =~ /^---/) { $inEx = $indent; "$nl```\n$beg" }
elsif ($beg =~ /^-/) { $inBul = $indent; "\n-" }
else { $inEx = $indent; "$nl```go\n$beg" }
}
>gemx;
$doc .= "```\n" if $inEx;
# Get & remove links at the end of comment
my %links;
while ($doc =~ s/^\[([^]\n]+)\]: (.+)\n\z//m)
{
$links{$1} = $2;
}
my @codes;
$doc =~ s/^(```go\n.*?^```\n)/push(@codes, $1); "CODE<$#codes>"/gems;
$doc =~ s{
(? \$\^[A-Za-z]+) # placeholder
| \[TestDeep\](?\s+operators?) # operator
| (? [^]\n]+) \] (?!\w) # link
| (? (?:(?:\[\])+|\*+|\b)
(?:bool\b
|u?int(?:\*|(?:8|16|32|64)?\b)
|float(?:\*|(?:32|64)\b)
|complex(?:\*|(?:64|128)\b)
|string\b
|rune\b
|byte\b
|any(?!\s+(?:numeric|of|placeholder)))
|\(\*byte\)\(nil\)
|\bmap\[string\]any
|\b(?:len|cap)\(\)
|\bnil\b
|\$(?:\d+|[a-zA-Z_]\w*)) # native_type
| (? \berror\b) # error
| (? \bTypeBehind(?:\(\)|\b)) # type_behind
| \b(? smuggler\s+operator)\b # smuggler
| (? BeLax\s+config\s+flag) # belax
}{
if ($+{placeholder})
{
"`$+{placeholder}`"
}
elsif ($+{operator})
{
qq!;
}
elsif (my $inner = $+{link})
{
if ($links{$inner})
{
qq!;
}
elsif ($operators{$inner})
{
qq!;
}
# local exported identifier
elsif ($inner =~ /^\*?([A-Z]\w*(?:\.[A-Z]\w*)?)\z/)
{
qq!;
}
# imported package
elsif ($inner =~ m,^\*?([a-z][\w/]*)(?:\.([A-Z]\w*(?:\.[A-Z]\w*)?))?\z,
and my $full = $op->{imports}{$1})
{
qq . ')';
}
else
{
qq![$inner]!;
}
}
elsif ($+{native_type})
{
"`$+{native_type}`"
}
elsif ($+{error})
{
"[`error`]($URL_GODEV/builtin#error)"
}
elsif ($+{type_behind})
{
qq!
}
elsif ($+{smuggler})
{
qq!
}
elsif ($+{belax})
{
qq!;
}
}geox;
$doc =~ s/^See also /> See also /m;
if ($op->{args} and @{$op->{args}})
{
$doc =~ s/(?{name}), @{$op->{args}})})
(?!\w)/*$1*/gx;
}
return $doc =~ s/^CODE<(\d+)>/go_format($op, $codes[$1])/egmr;
}
sub go_format
{
my($operator, $code) = @_;
$code =~ s/^```go\n// or return $code;
$code =~ s/\n```\n\z//;
my $pid = open2(my $fmt_out, my $fmt_in, 'gofmt', '-s');
my $root;
if ($code =~ /^func/)
{
$root = 1;
print $fmt_in <{name}.go:1
$code
EOM
}
else
{
print $fmt_in <{name}.go:1
func x() {
$code
}
EOM
}
close $fmt_in;
my $new_code = do { <$fmt_out>; <$fmt_out>; <$fmt_out>; # skip 1st 3 lines
local $/; <$fmt_out> };
chomp($new_code);
unless ($root)
{
$new_code =~ s/[^\t]+//;
$new_code =~ s/\n\}\z//;
$new_code =~ s/^\t//gm;
}
waitpid $pid, 0;
if ($? != 0)
{
die <{name} failed:
$code
EOD
}
$new_code =~ s/^(\t+)/" " x length $1/gme;
if ($new_code ne $code)
{
die <{name} is not correctly indented:
$code
------------------ should be ------------------
$new_code
EOD
}
return "```go\n$new_code\n```\n";
}
sub slurp
{
local $/;
open(my $fh, '<', shift);
<$fh>
}
golang-github-maxatome-go-testdeep-1.14.0/tools/import_spew_bypass.pl 0000775 0000000 0000000 00000002052 14543133116 0026004 0 ustar 00root root 0000000 0000000 #!/usr/bin/env perl
# 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.
use strict;
use warnings;
use autodie;
use 5.014;
use HTTP::Tiny;
my $SPEW_BASE_URL =
'https://raw.githubusercontent.com/davecgh/go-spew/master/spew/';
foreach my $file (qw(bypass.go bypasssafe.go))
{
my $resp = HTTP::Tiny::->new->get("$SPEW_BASE_URL$file");
$resp->{success} or die "Failed to retrieve $file!\n";
unless ($resp->{content} =~ s/^package \Kspew$/dark/m)
{
die "'package spew' line not found in $file!\n";
}
open(my $fh, '>', "internal/dark/$file");
say $fh < ' && ', ' ' => ' || ');
$resp->{content} =~
s{^(?=// \+build (.*))}
{"//go:build " . ($1 =~ s!([ ,])!$ops{$1}!gr) . "\n"}em;
print $fh $resp->{content};
close $fh;
}