From 2def0526125e23316e39b1aabdd72eee76e6faf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20Soul=C3=A9?= Date: Sat, 16 Nov 2019 15:35:23 +0100 Subject: [PATCH 1/7] JSON accepts comment in its JSON parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 */. Signed-off-by: Maxime Soulé --- README.md | 5 +- example_cmp_test.go | 15 ++-- example_t_test.go | 15 ++-- example_test.go | 15 ++-- internal/util/json.go | 64 +++++++++++++--- internal/util/json_test.go | 65 ++++++++++++++-- td_json.go | 26 ++++++- td_json_test.go | 15 ++++ tools/docs_src/content/introduction/_index.md | 5 +- tools/docs_src/content/operators/JSON.md | 75 ++++++++++++++----- 10 files changed, 247 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 9fba61d7..f7f11de2 100644 --- a/README.md +++ b/README.md @@ -92,11 +92,12 @@ func TestMyApi(t *testing.T) { Status: http.StatusCreated, // Header can be tested too… See tdhttp doc. Body: td.JSON(` +// Note that comments are allowed { - "id": $id, + "id": $id, // set by the API/DB "name": "Bob", "age": 42, - "created_at": "$createdAt", + "created_at": "$createdAt", // set by the API/DB }`, td.Tag("id", td.Catch(&id, td.NotZero())), // catch $id and check ≠ 0 td.Tag("created_at", td.All( // ← All combines several operators like a AND diff --git a/example_cmp_test.go b/example_cmp_test.go index af27e263..037f327a 100644 --- a/example_cmp_test.go +++ b/example_cmp_test.go @@ -851,11 +851,16 @@ func ExampleCmpJSON_basic() { ok = CmpJSON(t, got, `{"fullname":"Bob","age":42}`, nil) fmt.Println("check got with fullname then age:", ok) - ok = CmpJSON(t, got, `{ - "fullname": "Bob", - "age": 42 + ok = 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 JSON:", ok) + fmt.Println("check got with nicely formatted and commented JSON:", ok) ok = CmpJSON(t, got, `{"fullname":"Bob","age":42,"gender":"male"}`, nil) fmt.Println("check got with gender field:", ok) @@ -876,7 +881,7 @@ func ExampleCmpJSON_basic() { // Output: // check got with age then fullname: true // check got with fullname then age: true - // check got with nicely formatted JSON: 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 diff --git a/example_t_test.go b/example_t_test.go index ee66e1b2..1d55d6e3 100644 --- a/example_t_test.go +++ b/example_t_test.go @@ -851,11 +851,16 @@ func ExampleT_JSON_basic() { ok = t.JSON(got, `{"fullname":"Bob","age":42}`, nil) fmt.Println("check got with fullname then age:", ok) - ok = t.JSON(got, `{ - "fullname": "Bob", - "age": 42 + 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 JSON:", ok) + 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) @@ -876,7 +881,7 @@ func ExampleT_JSON_basic() { // Output: // check got with age then fullname: true // check got with fullname then age: true - // check got with nicely formatted JSON: 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 diff --git a/example_test.go b/example_test.go index e1e21a52..542c0d6c 100644 --- a/example_test.go +++ b/example_test.go @@ -930,11 +930,16 @@ func ExampleJSON_basic() { ok = Cmp(t, got, JSON(`{"fullname":"Bob","age":42}`)) fmt.Println("check got with fullname then age:", ok) - ok = Cmp(t, got, JSON(`{ - "fullname": "Bob", - "age": 42 + ok = Cmp(t, got, 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 JSON:", ok) + fmt.Println("check got with nicely formatted and commented JSON:", ok) ok = Cmp(t, got, JSON(`{"fullname":"Bob","age":42,"gender":"male"}`)) fmt.Println("check got with gender field:", ok) @@ -955,7 +960,7 @@ func ExampleJSON_basic() { // Output: // check got with age then fullname: true // check got with fullname then age: true - // check got with nicely formatted JSON: 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 diff --git a/internal/util/json.go b/internal/util/json.go index 00214c50..7e1a0b41 100644 --- a/internal/util/json.go +++ b/internal/util/json.go @@ -7,15 +7,20 @@ package util import ( + "bytes" "encoding/json" "fmt" + "strings" "sync" "unicode" "unicode/utf8" ) -var jsonErrorMesg = "" // will be overwritten by UnmarshalJSON -var jsonErrorMesgOnce sync.Once +var ( + jsonErrPlaceholder = "" // will be overwritten by UnmarshalJSON + jsonErrCommentPrefix = "" // will be overwritten by UnmarshalJSON + jsonErrorMesgOnce sync.Once +) // `{"foo": $bar}`, 8 → `{"foo": "$bar"}` func stringifyPlaceholder(buf []byte, dollar int64) ([]byte, error) { @@ -74,6 +79,38 @@ func stringifyPlaceholder(buf []byte, dollar int64) ([]byte, error) { return buf, nil } +// `{"foo": 123 /* comment */}`, 13 → `{"foo": 123 }` +func clearComment(buf []byte, slash int64, origErr error) error { + r, _ := utf8.DecodeRune(buf[slash+1:]) // just after / + + var end int + + switch r { + case '/': // → // = comment until end of line or buffer + end = bytes.IndexAny(buf[slash+2:], "\r\n") + if end < 0 { + end = len(buf) + } else { + end += int(slash) + 2 + } + + case '*': // → /* = comment until */ + end = bytes.Index(buf[slash+2:], []byte(`*/`)) + if end < 0 { + return fmt.Errorf(`unterminated comment at offset %d`, slash+1) + } + end += int(slash) + 2 + 2 + + default: + return origErr + } + + for i := int(slash); i < end; i++ { + buf[i] = ' ' + } + return nil +} + // UnmarshalJSON is a custom json.Unmarshal function allowing to // handle placeholders not enclosed in strings. It relies on // json.SyntaxError errors detected before any memory allocation. So @@ -84,7 +121,8 @@ func UnmarshalJSON(buf []byte, target interface{}) error { var dummy interface{} err := json.Unmarshal([]byte(`$x`), &dummy) if jerr, ok := err.(*json.SyntaxError); ok { - jsonErrorMesg = jerr.Error() + jsonErrPlaceholder = jerr.Error() + jsonErrCommentPrefix = jerr.Error()[:strings.Index(jsonErrPlaceholder, "$")] + "/" } }) @@ -94,13 +132,21 @@ func UnmarshalJSON(buf []byte, target interface{}) error { return nil } jerr, ok := err.(*json.SyntaxError) - if !ok || jerr.Error() != jsonErrorMesg || jerr.Offset >= int64(len(buf)) { - return err - } + if ok && jerr.Offset < int64(len(buf)) { + switch { + case jerr.Error() == jsonErrPlaceholder: + buf, err = stringifyPlaceholder(buf, jerr.Offset-1) // "$" pos + if err == nil { + continue + } - buf, err = stringifyPlaceholder(buf, jerr.Offset-1) // $ pos - if err != nil { - return err + case strings.HasPrefix(jerr.Error(), jsonErrCommentPrefix): + err = clearComment(buf, jerr.Offset-1, err) // "/" pos + if err == nil { + continue + } + } } + return err } } diff --git a/internal/util/json_test.go b/internal/util/json_test.go index 254fc33e..fea4bcc9 100644 --- a/internal/util/json_test.go +++ b/internal/util/json_test.go @@ -11,6 +11,7 @@ import ( "strings" "testing" + "errors" "github.com/maxatome/go-testdeep/internal/test" ) @@ -103,22 +104,74 @@ func TestStringifyPlaceholder(t *testing.T) { func TestUnmarshalJSON(t *testing.T) { var target interface{} + t.Run("clearComment", func(t *testing.T) { + origErr := errors.New("orig error") + + from := []byte(`/* comment */`) + err := clearComment(from, 0, origErr) + if err != nil { + t.Errorf("clearComment failed: %s", err) + } + if !reflect.DeepEqual(from, []byte(` `)) { + t.Errorf(`clearComment failed, unexpected buffer: "%s"`, string(from)) + } + + from = []byte(`foo // bar`) + err = clearComment(from, 4, origErr) + if err != nil { + t.Errorf("clearComment failed: %s", err) + } + if !reflect.DeepEqual(from, []byte(`foo `)) { + t.Errorf(`clearComment failed, unexpected buffer: "%s"`, string(from)) + } + + from = []byte("foo // bar\nzip") + err = clearComment(from, 4, origErr) + if err != nil { + t.Errorf("clearComment failed: %s", err) + } + if !reflect.DeepEqual(from, []byte("foo \nzip")) { + t.Errorf(`clearComment failed, unexpected buffer: "%s"`, string(from)) + } + + from = []byte("foo //\nzip") + err = clearComment(from, 4, origErr) + if err != nil { + t.Errorf("clearComment failed: %s", err) + } + if !reflect.DeepEqual(from, []byte("foo \nzip")) { + t.Errorf(`clearComment failed, unexpected buffer: "%s"`, string(from)) + } + + from = []byte("foo /") + err = clearComment(from, 4, origErr) + if err != origErr { + t.Errorf("got: %s, expected: %s", err, origErr) + } + + from = []byte("foo /*") + err = clearComment(from, 4, origErr) + if err == nil || err.Error() != `unterminated comment at offset 5` { + t.Errorf("got: %s, expected: %s", err, origErr) + } + }) + // First call to initialize jsonErrorMesg variable err := UnmarshalJSON([]byte(`{}`), &target) if err != nil { t.Fatalf("First UnmarshalJSON failed: %s", err) } - if jsonErrorMesg == "" || jsonErrorMesg == "" { + if jsonErrPlaceholder == "" || jsonErrPlaceholder == "" { t.Fatal("json.SyntaxError error not found!") } - t.Logf("OK json.SyntaxError error found: %s", jsonErrorMesg) + t.Logf("OK json.SyntaxError error found: %s", jsonErrPlaceholder) // Normal case with several placeholders err = UnmarshalJSON([]byte(` -{ - "numeric_placeholders": [ $1, $2, $3 ], - "named_placeholders": [ $foo, $bar, $zip ] -}`), &target) +/* comment */ { /* comment + */ "numeric_placeholders" /* comment */: [ $1, $2, $3 ], // comment + "named_placeholders": [ $foo, $bar, /* commment */ $zip /* comment */ ] +} // comment`), &target) if err != nil { t.Fatalf("UnmarshalJSON failed: %s", err) } diff --git a/td_json.go b/td_json.go index df4ae1b1..448fa473 100644 --- a/td_json.go +++ b/td_json.go @@ -246,8 +246,30 @@ func scan(v *interface{}, params []interface{}, byTag map[string]*tdTag, path st // For the "details" key, the raw value "$info" is expected, no // placeholders are involved here. // -// Last but not least, Lax mode is automatically enabled by JSON -// operator to simplify numeric tests. +// Note that Lax mode is automatically enabled by JSON operator to +// simplify numeric tests. +// +// Last but not least, comments can be embedded in JSON data: +// +// Cmp(t, gotValue, +// 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 */ +// }`, +// Tag("name", HasPrefix("Foo")), // matches $1 and $name +// 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 */. // // TypeBehind method returns the reflect.Type of the "expectedJSON" // json.Unmarshal'ed. So it can be bool, string, float64, diff --git a/td_json_test.go b/td_json_test.go index af78a64b..865fb8a8 100644 --- a/td_json_test.go +++ b/td_json_test.go @@ -70,6 +70,21 @@ func TestJSON(t *testing.T) { testdeep.Tag("age", testdeep.Between(40, 45)), testdeep.Tag("name", testdeep.Re(`^Bob`)))) + // …with comments… + checkOK(t, MyStruct{Name: "Bob", Age: 42}, + testdeep.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 */ +}`, + testdeep.Tag("age", testdeep.Between(40, 45)), + testdeep.Tag("name", testdeep.Re(`^Bob`)))) + // // []byte checkOK(t, MyStruct{Name: "Bob", Age: 42}, diff --git a/tools/docs_src/content/introduction/_index.md b/tools/docs_src/content/introduction/_index.md index 517b7e07..c9add379 100644 --- a/tools/docs_src/content/introduction/_index.md +++ b/tools/docs_src/content/introduction/_index.md @@ -51,11 +51,12 @@ func TestMyApi(t *testing.T) { Status: http.StatusCreated, // Header can be tested too… See tdhttp doc. Body: td.JSON(` +// Note that comments are allowed { - "id": $id, + "id": $id, // set by the API/DB "name": "Bob", "age": 42, - "created_at": "$createdAt", + "created_at": "$createdAt", // set by the API/DB }`, td.Tag("id", td.Catch(&id, td.NotZero())), // catch $id and check ≠ 0 td.Tag("created_at", td.All( // ← All combines several operators like a AND diff --git a/tools/docs_src/content/operators/JSON.md b/tools/docs_src/content/operators/JSON.md index c65ea3bc..a67647f3 100644 --- a/tools/docs_src/content/operators/JSON.md +++ b/tools/docs_src/content/operators/JSON.md @@ -75,8 +75,34 @@ Cmp(t, gotValue, For the "details" key, the raw value "`$info`" is expected, no placeholders are involved here. -Last but not least, [`Lax`]({{< ref "Lax" >}}) mode is automatically enabled by [`JSON`]({{< ref "JSON" >}}) -operator to simplify numeric tests. +Note that [`Lax`]({{< ref "Lax" >}}) mode is automatically enabled by [`JSON`]({{< ref "JSON" >}}) operator to +simplify numeric tests. + +Last but not least, comments can be embedded in JSON data: + +```go +Cmp(t, gotValue, + 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 */ +}`, + Tag("name", HasPrefix("Foo")), // matches $1 and $name + 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 */. + [TypeBehind]({{< ref "operators#typebehind-method" >}}) method returns the [`reflect.Type`](https://golang.org/pkg/reflect/#Type) of the *expectedJSON* [`json.Unmarshal`](https://golang.org/pkg/json/#Unmarshal)'ed. So it can be `bool`, `string`, `float64`, @@ -105,11 +131,16 @@ operator to simplify numeric tests. ok = Cmp(t, got, JSON(`{"fullname":"Bob","age":42}`)) fmt.Println("check got with fullname then age:", ok) - ok = Cmp(t, got, JSON(`{ - "fullname": "Bob", - "age": 42 + ok = Cmp(t, got, 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 JSON:", ok) + fmt.Println("check got with nicely formatted and commented JSON:", ok) ok = Cmp(t, got, JSON(`{"fullname":"Bob","age":42,"gender":"male"}`)) fmt.Println("check got with gender field:", ok) @@ -130,7 +161,7 @@ operator to simplify numeric tests. // Output: // check got with age then fullname: true // check got with fullname then age: true - // check got with nicely formatted JSON: 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 @@ -276,11 +307,16 @@ reason of a potential failure. ok = CmpJSON(t, got, `{"fullname":"Bob","age":42}`, nil) fmt.Println("check got with fullname then age:", ok) - ok = CmpJSON(t, got, `{ - "fullname": "Bob", - "age": 42 + ok = 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 JSON:", ok) + fmt.Println("check got with nicely formatted and commented JSON:", ok) ok = CmpJSON(t, got, `{"fullname":"Bob","age":42,"gender":"male"}`, nil) fmt.Println("check got with gender field:", ok) @@ -301,7 +337,7 @@ reason of a potential failure. // Output: // check got with age then fullname: true // check got with fullname then age: true - // check got with nicely formatted JSON: 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 @@ -430,11 +466,16 @@ reason of a potential failure. ok = t.JSON(got, `{"fullname":"Bob","age":42}`, nil) fmt.Println("check got with fullname then age:", ok) - ok = t.JSON(got, `{ - "fullname": "Bob", - "age": 42 + 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 JSON:", ok) + 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) @@ -455,7 +496,7 @@ reason of a potential failure. // Output: // check got with age then fullname: true // check got with fullname then age: true - // check got with nicely formatted JSON: 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 From 5d67d21790289e426dba27d3e694f1f5eb17850a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20Soul=C3=A9?= Date: Sat, 16 Nov 2019 15:45:18 +0100 Subject: [PATCH 2/7] Typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime Soulé --- internal/location/location.go | 2 +- td_smuggle.go | 2 +- tools/docs_src/content/operators/Smuggle.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/location/location.go b/internal/location/location.go index b5ae590f..5cd6c95c 100644 --- a/internal/location/location.go +++ b/internal/location/location.go @@ -51,7 +51,7 @@ func New(callDepth int) (loc Location, ok bool) { } // IsInitialized returns true if the Location is initialized -// (eg. NewLocation() called without an error), false otherwise. +// (e.g. NewLocation() called without an error), false otherwise. func (l Location) IsInitialized() bool { return l.File != "" } diff --git a/td_smuggle.go b/td_smuggle.go index efa41f76..2c73ce4d 100644 --- a/td_smuggle.go +++ b/td_smuggle.go @@ -273,7 +273,7 @@ func buildStructFieldFn(path string) (func(interface{}) (smuggleValue, error), e // 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 tests writing (eg. the function can accept a float64 +// facilitate the tests writing (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 is not available with diff --git a/tools/docs_src/content/operators/Smuggle.md b/tools/docs_src/content/operators/Smuggle.md index d65116ab..3e82502b 100644 --- a/tools/docs_src/content/operators/Smuggle.md +++ b/tools/docs_src/content/operators/Smuggle.md @@ -182,7 +182,7 @@ The difference between [`Smuggle`]({{< ref "Smuggle" >}}) and [`Code`]({{< ref " used to do a final comparison while [`Smuggle`]({{< ref "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 tests writing (eg. the function can accept a `float64` +facilitate the tests writing (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 is not available with From 796a19de26b46f36cb4eb70fde7c4992739703f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20Soul=C3=A9?= Date: Sun, 17 Nov 2019 19:11:31 +0100 Subject: [PATCH 3/7] JSON accept some operator shortcuts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Empty → $^Empty - Ignore → $^Ignore - NaN → $^NaN - Nil → $^Nil - NotEmpty → $^NotEmpty - NotNaN → $^NotNaN - NotNil → $^NotNil - NotZero → $^NotZero - Zero → $^Zero Signed-off-by: Maxime Soulé --- internal/util/json.go | 9 ++- internal/util/json_test.go | 8 ++- td_json.go | 65 +++++++++++++++++++-- td_json_test.go | 73 ++++++++++++++---------- tools/docs_src/content/operators/JSON.md | 30 +++++++++- tools/gen_funcs.pl | 51 +++++++++-------- 6 files changed, 172 insertions(+), 64 deletions(-) diff --git a/internal/util/json.go b/internal/util/json.go index 7e1a0b41..74686d91 100644 --- a/internal/util/json.go +++ b/internal/util/json.go @@ -45,7 +45,8 @@ func stringifyPlaceholder(buf []byte, dollar int64) ([]byte, error) { } end = int64(len(buf)) endFound: - } else if unicode.IsLetter(r) || r == '_' { // Named placeholder: $pïpô12 + } else if shortcut := r == '^'; shortcut || // Operator shortcut, e.g. $^Zero + unicode.IsLetter(r) || r == '_' { // Named placeholder: $pïpô12 runes: for max := int64(len(buf)); cur < max; cur += int64(size) { r, size = utf8.DecodeRune(buf[cur:]) @@ -55,6 +56,10 @@ func stringifyPlaceholder(buf []byte, dollar int64) ([]byte, error) { break runes default: if !unicode.IsLetter(r) && !unicode.IsNumber(r) { + if shortcut { + return nil, + fmt.Errorf(`invalid operator shortcut at offset %d`, dollar+1) + } return nil, fmt.Errorf(`invalid named placeholder at offset %d`, dollar+1) } @@ -65,7 +70,7 @@ func stringifyPlaceholder(buf []byte, dollar int64) ([]byte, error) { return nil, fmt.Errorf(`invalid placeholder at offset %d`, dollar+1) } - // put "" around $éé123 or $12345 + // put "" around $éé123, $12345 or $^NotZero if cap(buf) == len(buf) { // allocate room for 20 extra placeholders buf = append(make([]byte, 0, len(buf)+40), buf...) diff --git a/internal/util/json_test.go b/internal/util/json_test.go index fea4bcc9..093c71c3 100644 --- a/internal/util/json_test.go +++ b/internal/util/json_test.go @@ -89,6 +89,8 @@ func TestStringifyPlaceholder(t *testing.T) { {jsonOrig: `[$456a]`, errExpected: `invalid numeric placeholder at offset 2`}, // Named placeholder {jsonOrig: `[$name%]`, errExpected: `invalid named placeholder at offset 2`}, + // Shortcut + {jsonOrig: `[$^Op%]`, errExpected: `invalid operator shortcut at offset 2`}, // Not a placeholder {jsonOrig: `[$%]`, errExpected: `invalid placeholder at offset 2`}, {jsonOrig: `$`, errExpected: `invalid placeholder at offset 1`}, @@ -166,18 +168,18 @@ func TestUnmarshalJSON(t *testing.T) { } t.Logf("OK json.SyntaxError error found: %s", jsonErrPlaceholder) - // Normal case with several placeholders + // Normal case with several placeholders and operator shorcuts err = UnmarshalJSON([]byte(` /* comment */ { /* comment */ "numeric_placeholders" /* comment */: [ $1, $2, $3 ], // comment - "named_placeholders": [ $foo, $bar, /* commment */ $zip /* comment */ ] + "named_placeholders": [ $foo, $^bar, /* ← op shortcut */ $zip /* comment */ ] } // comment`), &target) if err != nil { t.Fatalf("UnmarshalJSON failed: %s", err) } if !reflect.DeepEqual(target, map[string]interface{}{ "numeric_placeholders": []interface{}{"$1", "$2", "$3"}, - "named_placeholders": []interface{}{"$foo", "$bar", "$zip"}, + "named_placeholders": []interface{}{"$foo", "$^bar", "$zip"}, }) { t.Errorf("UnmarshalJSON mismatch: %#+v", target) } diff --git a/td_json.go b/td_json.go index 448fa473..790bf567 100644 --- a/td_json.go +++ b/td_json.go @@ -44,15 +44,21 @@ const ( type tdJSONPlaceholder struct { TestDeep - tag string - num uint64 + name string + num uint64 } func (p *tdJSONPlaceholder) MarshalJSON() ([]byte, error) { var b bytes.Buffer if p.num == 0 { - fmt.Fprintf(&b, `"$%s`, p.tag) + fmt.Fprintf(&b, `"$%s`, p.name) + + // Don't add a comment for operator shorcuts (aka. $^NotZero) + if p.name[0] == '^' { + b.WriteByte('"') + return b.Bytes(), nil + } } else { fmt.Fprintf(&b, `"$%d`, p.num) } @@ -108,6 +114,18 @@ func unmarshal(expectedJSON interface{}, target interface{}) { } } +var jsonOpShortcuts = map[string]func() TestDeep{ + "Empty": Empty, + "Ignore": Ignore, + "NaN": NaN, + "Nil": Nil, + "NotEmpty": NotEmpty, + "NotNaN": NotNaN, + "NotNil": NotNil, + "NotZero": NotZero, + "Zero": Zero, +} + // scan scans "*v" data structure to find strings containing // placeholders (like $123 or $name) corresponding to a value or // TestDeep operator contained in "params" and "byTag". @@ -165,6 +183,21 @@ func scan(v *interface{}, params []interface{}, byTag map[string]*tdTag, path st break } + // Test for operator shortcut + if firstRune == '^' { + fn := jsonOpShortcuts[tv[2:]] + if fn == nil { + panic(fmt.Sprintf(`JSON obj%s contains a bad operator shortcut "%s"`, + path, tv)) + } + + *v = &tdJSONPlaceholder{ + TestDeep: fn(), + name: tv[1:], + } + break + } + // Test for $tag err := util.CheckTag(tv[1:]) if err != nil { @@ -178,7 +211,7 @@ func scan(v *interface{}, params []interface{}, byTag map[string]*tdTag, path st } *v = &tdJSONPlaceholder{ TestDeep: op, - tag: tv[1:], + name: tv[1:], } } } @@ -249,7 +282,7 @@ func scan(v *interface{}, params []interface{}, byTag map[string]*tdTag, path st // Note that Lax mode is automatically enabled by JSON operator to // simplify numeric tests. // -// Last but not least, comments can be embedded in JSON data: +// Comments can be embedded in JSON data: // // Cmp(t, gotValue, // JSON(` @@ -271,6 +304,28 @@ func scan(v *interface{}, params []interface{}, byTag map[string]*tdTag, path st // - multi-lines comments start with the character sequence /* and stop // with the first subsequent character sequence */. // +// Last but not least, simple operators can be directly embedded in +// JSON data without requiring any placeholder but using directly +// $^OperatorName. They are operator shortcuts: +// +// Cmp(t, gotValue, JSON(`{"id": $1}`, NotZero())) +// +// can be written as: +// +// Cmp(t, gotValue, JSON(`{"id": $^NotZero}`)) +// +// Unfortunately, only simple operators (in fact those which take no +// parameters) have shortcuts. They follow: +// - Empty → $^Empty +// - Ignore → $^Ignore +// - NaN → $^NaN +// - Nil → $^Nil +// - NotEmpty → $^NotEmpty +// - NotNaN → $^NotNaN +// - NotNil → $^NotNil +// - NotZero → $^NotZero +// - Zero → $^Zero +// // TypeBehind method returns the reflect.Type of the "expectedJSON" // json.Unmarshal'ed. So it can be bool, string, float64, // []interface{}, map[string]interface{} or interface{} in case diff --git a/td_json_test.go b/td_json_test.go index 865fb8a8..150ca10c 100644 --- a/td_json_test.go +++ b/td_json_test.go @@ -26,8 +26,9 @@ func (r errReader) Read(p []byte) (int, error) { func TestJSON(t *testing.T) { type MyStruct struct { - Name string `json:"name"` - Age uint `json:"age"` + Name string `json:"name"` + Age uint `json:"age"` + Gender string `json:"gender"` } // @@ -45,50 +46,56 @@ func TestJSON(t *testing.T) { // // struct // + got := MyStruct{Name: "Bob", Age: 42, Gender: "male"} + // No placeholder - checkOK(t, MyStruct{Name: "Bob", Age: 42}, - testdeep.JSON(`{"name":"Bob","age":42}`)) + checkOK(t, got, + testdeep.JSON(`{"name":"Bob","age":42,"gender":"male"}`)) // Numeric placeholders - checkOK(t, MyStruct{Name: "Bob", Age: 42}, - testdeep.JSON(`{"name":"$1","age":$2}`, "Bob", 42)) // raw values + checkOK(t, got, + testdeep.JSON(`{"name":"$1","age":$2,"gender":$3}`, + "Bob", 42, "male")) // raw values - checkOK(t, MyStruct{Name: "Bob", Age: 42}, - testdeep.JSON(`{"name":"$1","age":$2}`, + checkOK(t, got, + testdeep.JSON(`{"name":"$1","age":$2,"gender":"$3"}`, testdeep.Re(`^Bob`), - testdeep.Between(40, 45))) + testdeep.Between(40, 45), + testdeep.NotEmpty())) // Tag placeholders - checkOK(t, MyStruct{Name: "Bob", Age: 42}, - testdeep.JSON(`{"name":"$name","age":$age}`, + checkOK(t, got, + testdeep.JSON(`{"name":"$name","age":$age,"gender":"$gender"}`, testdeep.Tag("name", testdeep.Re(`^Bob`)), - testdeep.Tag("age", testdeep.Between(40, 45)))) + testdeep.Tag("age", testdeep.Between(40, 45)), + testdeep.Tag("gender", testdeep.NotEmpty()))) - // Mixed placeholders - checkOK(t, MyStruct{Name: "Bob", Age: 42}, - testdeep.JSON(`{"name":"$name","age":$1}`, + // Mixed placeholders + operator shortcut + checkOK(t, got, + testdeep.JSON(`{"name":"$name","age":$1,"gender":$^NotEmpty}`, testdeep.Tag("age", testdeep.Between(40, 45)), testdeep.Tag("name", testdeep.Re(`^Bob`)))) // …with comments… - checkOK(t, MyStruct{Name: "Bob", Age: 42}, + checkOK(t, got, testdeep.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 */ + "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 }`, testdeep.Tag("age", testdeep.Between(40, 45)), testdeep.Tag("name", testdeep.Re(`^Bob`)))) // // []byte - checkOK(t, MyStruct{Name: "Bob", Age: 42}, - testdeep.JSON([]byte(`{"name":"$name","age":$1}`), + checkOK(t, got, + testdeep.JSON([]byte(`{"name":"$name","age":$1,"gender":"male"}`), testdeep.Tag("age", testdeep.Between(40, 45)), testdeep.Tag("name", testdeep.Re(`^Bob`)))) @@ -101,10 +108,12 @@ func TestJSON(t *testing.T) { defer os.RemoveAll(tmpDir) // clean up filename := tmpDir + "/test.json" - if err = ioutil.WriteFile(filename, []byte(`{"name":$name,"age":$1}`), 0644); err != nil { + err = ioutil.WriteFile( + filename, []byte(`{"name":$name,"age":$1,"gender":$^NotEmpty}`), 0644) + if err != nil { t.Fatal(err) } - checkOK(t, MyStruct{Name: "Bob", Age: 42}, + checkOK(t, got, testdeep.JSON(filename, testdeep.Tag("age", testdeep.Between(40, 45)), testdeep.Tag("name", testdeep.Re(`^Bob`)))) @@ -115,7 +124,7 @@ func TestJSON(t *testing.T) { if err != nil { t.Fatal(err) } - checkOK(t, MyStruct{Name: "Bob", Age: 42}, + checkOK(t, got, testdeep.JSON(tmpfile, testdeep.Tag("age", testdeep.Between(40, 45)), testdeep.Tag("name", testdeep.Re(`^Bob`)))) @@ -165,6 +174,9 @@ func TestJSON(t *testing.T) { test.CheckPanic(t, func() { testdeep.JSON(`[1, 2, $3]`, testdeep.Ignore()) }, `JSON obj[2] contains numeric placeholder "$3", but only 1 params given`) + // operator shortcut + test.CheckPanic(t, func() { testdeep.JSON(`[1, "$^bad%"]`) }, + `JSON obj[1] contains a bad operator shortcut "$^bad%"`) // named placeholders test.CheckPanic(t, func() { testdeep.JSON(`[1, "$bad%"]`) }, `JSON obj[1] contains a bad placeholder "$bad%"`) @@ -187,7 +199,7 @@ JSON([ test.EqualStr(t, testdeep.JSON(` null `).String(), `JSON(null)`) test.EqualStr(t, - testdeep.JSON(`[ $1, $name, $2 ]`, + testdeep.JSON(`[ $1, $name, $2, $^Nil ]`, testdeep.Between(12, 20), "test", testdeep.Tag("name", testdeep.Code( @@ -197,11 +209,12 @@ JSON([ JSON([ "$1" /* 12 ≤ got ≤ 20 */, "$name" /* Code(func(string) bool) */, - "test" + "test", + "$^Nil" ])`[1:]) test.EqualStr(t, - testdeep.JSON(`{"label": $value, "zip": 666}`, + testdeep.JSON(`{"label": $value, "zip": $^NotZero}`, testdeep.Tag("value", testdeep.Bag( testdeep.JSON(`{"name": $1,"age":$2}`, testdeep.HasPrefix("Bob"), @@ -219,7 +232,7 @@ JSON({ JSON({ "name": "$1" /* HasPrefix("Alice") */ })) */, - "zip": 666 + "zip": "$^NotZero" })`[1:]) // Improbable edge-case diff --git a/tools/docs_src/content/operators/JSON.md b/tools/docs_src/content/operators/JSON.md index a67647f3..208c1217 100644 --- a/tools/docs_src/content/operators/JSON.md +++ b/tools/docs_src/content/operators/JSON.md @@ -78,7 +78,7 @@ placeholders are involved here. Note that [`Lax`]({{< ref "Lax" >}}) mode is automatically enabled by [`JSON`]({{< ref "JSON" >}}) operator to simplify numeric tests. -Last but not least, comments can be embedded in JSON data: +Comments can be embedded in JSON data: ```go Cmp(t, gotValue, @@ -104,6 +104,34 @@ Comments, like in go, have 2 forms. To quote the Go language specification: with the first subsequent character sequence */. +Last but not least, simple operators can be directly embedded in +JSON data without requiring any placeholder but using directly +`$^OperatorName`. They are operator shortcuts: + +```go +Cmp(t, gotValue, JSON(`{"id": $1}`, NotZero())) +``` + +can be written as: + +```go +Cmp(t, gotValue, JSON(`{"id": $^NotZero}`)) +``` + +Unfortunately, only simple operators (in fact those which take no +parameters) have shortcuts. They follow: + +- [`Empty`]({{< ref "Empty" >}}) → `$^Empty` +- [`Ignore`]({{< ref "Ignore" >}}) → `$^Ignore` +- [`NaN`]({{< ref "NaN" >}}) → `$^NaN` +- [`Nil`]({{< ref "Nil" >}}) → `$^Nil` +- [`NotEmpty`]({{< ref "NotEmpty" >}}) → `$^NotEmpty` +- [`NotNaN`]({{< ref "NotNaN" >}}) → `$^NotNaN` +- [`NotNil`]({{< ref "NotNil" >}}) → `$^NotNil` +- [`NotZero`]({{< ref "NotZero" >}}) → `$^NotZero` +- [`Zero`]({{< ref "Zero" >}}) → `$^Zero` + + [TypeBehind]({{< ref "operators#typebehind-method" >}}) method returns the [`reflect.Type`](https://golang.org/pkg/reflect/#Type) of the *expectedJSON* [`json.Unmarshal`](https://golang.org/pkg/json/#Unmarshal)'ed. So it can be `bool`, `string`, `float64`, `[]interface{}`, `map[string]interface{}` or `interface{}` in case diff --git a/tools/gen_funcs.pl b/tools/gen_funcs.pl index 797ea1cc..3742da90 100755 --- a/tools/gen_funcs.pl +++ b/tools/gen_funcs.pl @@ -832,9 +832,10 @@ sub process_doc $doc =~ s/^(```go\n.*?^```\n)/push(@codes, $1); "CODE<$#codes>"/gems; $doc =~ s< - (\b(${\join('|', grep !/^JSON/, keys %operators)} + (\$\^[A-Za-z]+) # $1 + | (\b(${\join('|', grep !/^JSON/, keys %operators)} |JSON(?!\ (?:value|data|filename|representation|specification))) - (?:\([^)]*\)|\b)) # $1 $2 + (?:\([^)]*\)|\b)) # $2 $3 | ((?:(?:\[\])+|\*+|\b)(?:bool\b |u?int(?:\*|(?:8|16|32|64)?\b) |float(?:\*|(?:32|64)\b) @@ -847,7 +848,7 @@ sub process_doc |\bmap\[string\]interface\{\} |\b(?:len|cap)\(\) |\bnil\b - |\$(?:\d+|[a-zA-Z_]\w*)) # $3 + |\$(?:\d+|[a-zA-Z_]\w*)) # $4 | ((?:\b|\*)fmt\.Stringer |\breflect\.Type |\bregexp\.MustCompile @@ -855,52 +856,56 @@ sub process_doc |\btime\.[A-Z][a-zA-Z]+ |\bjson\.(?:Unm|M)arshal |\bio\.Reader - |\bioutil\.Read(?:All|File))\b # $4 - | (\berror\b) # $5 - | (\bTypeBehind(?:\(\)|\b)) # $6 - | \b(${\join('|', keys %consts)})\b # $7 - | \b(smuggler\s+operator)\b # $8 - | \b(TestDeep\s+operators?)\b # $9 - | (\*?SmuggledGot)\b # $10 + |\bioutil\.Read(?:All|File))\b # $5 + | (\berror\b) # $6 + | (\bTypeBehind(?:\(\)|\b)) # $7 + | \b(${\join('|', keys %consts)})\b # $8 + | \b(smuggler\s+operator)\b # $9 + | \b(TestDeep\s+operators?)\b # $10 + | (\*?SmuggledGot)\b # $11 >{ if ($1) { - qq![`$1`]({{< ref "$2" >}})! + "`$1`" } - elsif ($3) + elsif ($2) { - "`$3`" + qq![`$2`]({{< ref "$3" >}})! } elsif ($4) { - my $all = $4; - my($pkg, $fn) = split('\.', $all, 2); - $pkg =~ s/^\*//; - "[`$all`](https://golang.org/pkg/$pkg/#$fn)" + "`$4`" } elsif ($5) { - "[`$5`](https://golang.org/pkg/builtin/#error)" + my $all = $5; + my($pkg, $fn) = split('\.', $all, 2); + $pkg =~ s/^\*//; + "[`$all`](https://golang.org/pkg/$pkg/#$fn)" } elsif ($6) { - qq![$6]({{< ref "operators#typebehind-method" >}})! + "[`$6`](https://golang.org/pkg/builtin/#error)" } elsif ($7) { - "[`$7`](https://godoc.org/github.com/maxatome/go-testdeep#BoundsKind)" + qq![$7]({{< ref "operators#typebehind-method" >}})! } elsif ($8) { - qq![$8]({{< ref "operators#smuggler-operators" >}})! + "[`$8`](https://godoc.org/github.com/maxatome/go-testdeep#BoundsKind)" } elsif ($9) { - qq![$9]({{< ref "operators" >}})! + qq![$9]({{< ref "operators#smuggler-operators" >}})! } elsif ($10) { - qq![$10](https://godoc.org/github.com/maxatome/go-testdeep#SmuggledGot)! + qq![$10]({{< ref "operators" >}})! + } + elsif ($11) + { + qq![$11](https://godoc.org/github.com/maxatome/go-testdeep#SmuggledGot)! } }geox; From a3424be5284a430c40dd344b9d179f59b94a0f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20Soul=C3=A9?= Date: Sun, 17 Nov 2019 22:28:31 +0100 Subject: [PATCH 4/7] Fix ctxerr.CollectError() to check boolean context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime Soulé --- internal/ctxerr/context.go | 7 +++++++ internal/ctxerr/context_test.go | 7 ++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/internal/ctxerr/context.go b/internal/ctxerr/context.go index d8d4da00..665b58ab 100644 --- a/internal/ctxerr/context.go +++ b/internal/ctxerr/context.go @@ -55,11 +55,18 @@ func (c Context) ResetErrors() (new Context) { // CollectError collects an error in the context. It returns an error // if the collector is full, nil otherwise. +// +// In boolean context, ignore the passed error and return the BooleanError. func (c Context) CollectError(err *Error) *Error { if err == nil { return nil } + // The boolean error must not be altered! + if c.BooleanError { + return BooleanError + } + // Error context not initialized yet if err.Context.Depth == 0 { err.Context = c diff --git a/internal/ctxerr/context_test.go b/internal/ctxerr/context_test.go index 50ecc302..648b4dad 100644 --- a/internal/ctxerr/context_test.go +++ b/internal/ctxerr/context_test.go @@ -124,9 +124,14 @@ func TestContextCollectError(t *testing.T) { t.Error("ctx.CollectError(nil) returned non-nil *Error") } + err := ctxerr.Context{BooleanError: true}.CollectError(&ctxerr.Error{}) + if err != ctxerr.BooleanError { + t.Error("boolean-ctx.CollectError(X) did not return BooleanError") + } + // !err.Location.IsInitialized() + ctx.CurOperator == nil origErr := &ctxerr.Error{} - err := ctx.CollectError(origErr) + err = ctx.CollectError(origErr) if err != origErr { t.Error("ctx.CollectError(err) != err") } From ae597e98280986952b99930cf10f4ff84f695a16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20Soul=C3=A9?= Date: Mon, 18 Nov 2019 00:05:22 +0100 Subject: [PATCH 5/7] Add SubJSONOf and SuperJSONOf operators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime Soulé --- README.md | 6 + cmp_funcs.go | 38 + example_cmp_test.go | 262 +++++++ example_t_test.go | 262 +++++++ example_test.go | 309 +++++++++ t.go | 38 + td_json.go | 451 ++++++++++-- td_json_test.go | 263 +++++++ td_map.go | 4 + tools/docs_src/content/operators/JSON.md | 16 +- tools/docs_src/content/operators/SubJSONOf.md | 608 ++++++++++++++++ .../docs_src/content/operators/SuperJSONOf.md | 647 ++++++++++++++++++ tools/docs_src/content/operators/_index.md | 12 + tools/docs_src/content/operators/matrix.md | 16 +- tools/gen_funcs.pl | 2 +- 15 files changed, 2887 insertions(+), 47 deletions(-) create mode 100644 tools/docs_src/content/operators/SubJSONOf.md create mode 100644 tools/docs_src/content/operators/SuperJSONOf.md diff --git a/README.md b/README.md index f7f11de2..aa05c49f 100644 --- a/README.md +++ b/README.md @@ -259,9 +259,11 @@ See [FAQ](https://go-testdeep.zetta.rocks/faq/). [`String`]: https://go-testdeep.zetta.rocks/operators/string/ [`Struct`]: https://go-testdeep.zetta.rocks/operators/struct/ [`SubBagOf`]: https://go-testdeep.zetta.rocks/operators/subbagof/ +[`SubJSONOf`]: https://go-testdeep.zetta.rocks/operators/subjsonof/ [`SubMapOf`]: https://go-testdeep.zetta.rocks/operators/submapof/ [`SubSetOf`]: https://go-testdeep.zetta.rocks/operators/subsetof/ [`SuperBagOf`]: https://go-testdeep.zetta.rocks/operators/superbagof/ +[`SuperJSONOf`]: https://go-testdeep.zetta.rocks/operators/superjsonof/ [`SuperMapOf`]: https://go-testdeep.zetta.rocks/operators/supermapof/ [`SuperSetOf`]: https://go-testdeep.zetta.rocks/operators/supersetof/ [`Tag`]: https://go-testdeep.zetta.rocks/operators/tag/ @@ -314,9 +316,11 @@ See [FAQ](https://go-testdeep.zetta.rocks/faq/). [`CmpString`]:https://go-testdeep.zetta.rocks/operators/string/#cmpstring-shortcut [`CmpStruct`]:https://go-testdeep.zetta.rocks/operators/struct/#cmpstruct-shortcut [`CmpSubBagOf`]:https://go-testdeep.zetta.rocks/operators/subbagof/#cmpsubbagof-shortcut +[`CmpSubJSONOf`]:https://go-testdeep.zetta.rocks/operators/subjsonof/#cmpsubjsonof-shortcut [`CmpSubMapOf`]:https://go-testdeep.zetta.rocks/operators/submapof/#cmpsubmapof-shortcut [`CmpSubSetOf`]:https://go-testdeep.zetta.rocks/operators/subsetof/#cmpsubsetof-shortcut [`CmpSuperBagOf`]:https://go-testdeep.zetta.rocks/operators/superbagof/#cmpsuperbagof-shortcut +[`CmpSuperJSONOf`]:https://go-testdeep.zetta.rocks/operators/superjsonof/#cmpsuperjsonof-shortcut [`CmpSuperMapOf`]:https://go-testdeep.zetta.rocks/operators/supermapof/#cmpsupermapof-shortcut [`CmpSuperSetOf`]:https://go-testdeep.zetta.rocks/operators/supersetof/#cmpsupersetof-shortcut [`CmpTruncTime`]:https://go-testdeep.zetta.rocks/operators/trunctime/#cmptrunctime-shortcut @@ -368,9 +372,11 @@ See [FAQ](https://go-testdeep.zetta.rocks/faq/). [`T.String`]: https://go-testdeep.zetta.rocks/operators/string/#t-string-shortcut [`T.Struct`]: https://go-testdeep.zetta.rocks/operators/struct/#t-struct-shortcut [`T.SubBagOf`]: https://go-testdeep.zetta.rocks/operators/subbagof/#t-subbagof-shortcut +[`T.SubJSONOf`]: https://go-testdeep.zetta.rocks/operators/subjsonof/#t-subjsonof-shortcut [`T.SubMapOf`]: https://go-testdeep.zetta.rocks/operators/submapof/#t-submapof-shortcut [`T.SubSetOf`]: https://go-testdeep.zetta.rocks/operators/subsetof/#t-subsetof-shortcut [`T.SuperBagOf`]: https://go-testdeep.zetta.rocks/operators/superbagof/#t-superbagof-shortcut +[`T.SuperJSONOf`]: https://go-testdeep.zetta.rocks/operators/superjsonof/#t-superjsonof-shortcut [`T.SuperMapOf`]: https://go-testdeep.zetta.rocks/operators/supermapof/#t-supermapof-shortcut [`T.SuperSetOf`]: https://go-testdeep.zetta.rocks/operators/supersetof/#t-supersetof-shortcut [`T.TruncTime`]: https://go-testdeep.zetta.rocks/operators/trunctime/#t-trunctime-shortcut diff --git a/cmp_funcs.go b/cmp_funcs.go index 258aba07..c5c97bb1 100644 --- a/cmp_funcs.go +++ b/cmp_funcs.go @@ -879,6 +879,25 @@ func CmpSubBagOf(t TestingT, got interface{}, expectedItems []interface{}, args return Cmp(t, got, SubBagOf(expectedItems...), args...) } +// CmpSubJSONOf is a shortcut for: +// +// Cmp(t, got, SubJSONOf(expectedJSON, params...), args...) +// +// See https://godoc.org/github.com/maxatome/go-testdeep#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 CmpSubJSONOf(t TestingT, got interface{}, expectedJSON interface{}, params []interface{}, args ...interface{}) bool { + t.Helper() + return Cmp(t, got, SubJSONOf(expectedJSON, params...), args...) +} + // CmpSubMapOf is a shortcut for: // // Cmp(t, got, SubMapOf(model, expectedEntries), args...) @@ -936,6 +955,25 @@ func CmpSuperBagOf(t TestingT, got interface{}, expectedItems []interface{}, arg return Cmp(t, got, SuperBagOf(expectedItems...), args...) } +// CmpSuperJSONOf is a shortcut for: +// +// Cmp(t, got, SuperJSONOf(expectedJSON, params...), args...) +// +// See https://godoc.org/github.com/maxatome/go-testdeep#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 CmpSuperJSONOf(t TestingT, got interface{}, expectedJSON interface{}, params []interface{}, args ...interface{}) bool { + t.Helper() + return Cmp(t, got, SuperJSONOf(expectedJSON, params...), args...) +} + // CmpSuperMapOf is a shortcut for: // // Cmp(t, got, SuperMapOf(model, expectedEntries), args...) diff --git a/example_cmp_test.go b/example_cmp_test.go index 037f327a..7d3308b7 100644 --- a/example_cmp_test.go +++ b/example_cmp_test.go @@ -912,11 +912,15 @@ func ExampleCmpJSON_placeholders() { ok = CmpJSON(t, got, `{"age": $age, "fullname": $name}`, []interface{}{Tag("age", Between(40, 45)), Tag("name", HasSuffix("Foobar"))}) fmt.Println("check got with named placeholders:", ok) + ok = CmpJSON(t, got, `{"age": $^NotZero, "fullname": $^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 ExampleCmpJSON_file() { @@ -2314,6 +2318,129 @@ func ExampleCmpSubBagOf() { // true } +func ExampleCmpSubJSONOf_basic() { + t := &testing.T{} + + got := &struct { + Fullname string `json:"fullname"` + Age int `json:"age"` + }{ + Fullname: "Bob", + Age: 42, + } + + ok := CmpSubJSONOf(t, got, `{"age":42,"fullname":"Bob","gender":"male"}`, nil) + fmt.Println("check got with age then fullname:", ok) + + ok = CmpSubJSONOf(t, got, `{"fullname":"Bob","age":42,"gender":"male"}`, nil) + fmt.Println("check got with fullname then age:", ok) + + ok = 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 = 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 := CmpSubJSONOf(t, got, `{"age": $1, "fullname": $2, "gender": $3}`, []interface{}{42, "Bob Foobar", "male"}) + fmt.Println("check got with numeric placeholders without operators:", ok) + + ok = CmpSubJSONOf(t, got, `{"age": $1, "fullname": $2, "gender": $3}`, []interface{}{Between(40, 45), HasSuffix("Foobar"), NotEmpty()}) + fmt.Println("check got with numeric placeholders:", ok) + + ok = CmpSubJSONOf(t, got, `{"age": "$1", "fullname": "$2", "gender": "$3"}`, []interface{}{Between(40, 45), HasSuffix("Foobar"), NotEmpty()}) + fmt.Println("check got with double-quoted numeric placeholders:", ok) + + ok = CmpSubJSONOf(t, got, `{"age": $age, "fullname": $name, "gender": $gender}`, []interface{}{Tag("age", Between(40, 45)), Tag("name", HasSuffix("Foobar")), Tag("gender", NotEmpty())}) + fmt.Println("check got with named placeholders:", ok) + + ok = 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 := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) // clean up + + filename := tmpDir + "/test.json" + if err = ioutil.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 := CmpSubJSONOf(t, got, filename, []interface{}{Tag("name", HasPrefix("Bob")), Tag("age", Between(40, 45)), Tag("gender", 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 = CmpSubJSONOf(t, got, file, []interface{}{Tag("name", HasPrefix("Bob")), Tag("age", Between(40, 45)), Tag("gender", 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{} @@ -2386,6 +2513,141 @@ func ExampleCmpSuperBagOf() { // 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 := CmpSuperJSONOf(t, got, `{"age":42,"fullname":"Bob","gender":"male"}`, nil) + fmt.Println("check got with age then fullname:", ok) + + ok = CmpSuperJSONOf(t, got, `{"fullname":"Bob","age":42,"gender":"male"}`, nil) + fmt.Println("check got with fullname then age:", ok) + + ok = 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 = 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 := CmpSuperJSONOf(t, got, `{"age": $1, "fullname": $2, "gender": $3}`, []interface{}{42, "Bob Foobar", "male"}) + fmt.Println("check got with numeric placeholders without operators:", ok) + + ok = CmpSuperJSONOf(t, got, `{"age": $1, "fullname": $2, "gender": $3}`, []interface{}{Between(40, 45), HasSuffix("Foobar"), NotEmpty()}) + fmt.Println("check got with numeric placeholders:", ok) + + ok = CmpSuperJSONOf(t, got, `{"age": "$1", "fullname": "$2", "gender": "$3"}`, []interface{}{Between(40, 45), HasSuffix("Foobar"), NotEmpty()}) + fmt.Println("check got with double-quoted numeric placeholders:", ok) + + ok = CmpSuperJSONOf(t, got, `{"age": $age, "fullname": $name, "gender": $gender}`, []interface{}{Tag("age", Between(40, 45)), Tag("name", HasSuffix("Foobar")), Tag("gender", NotEmpty())}) + fmt.Println("check got with named placeholders:", ok) + + ok = 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 := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) // clean up + + filename := tmpDir + "/test.json" + if err = ioutil.WriteFile(filename, []byte(` +{ + "fullname": "$name", + "age": "$age", + "gender": "$gender" +}`), 0644); err != nil { + t.Fatal(err) + } + + // OK let's test with this file + ok := CmpSuperJSONOf(t, got, filename, []interface{}{Tag("name", HasPrefix("Bob")), Tag("age", Between(40, 45)), Tag("gender", 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 = CmpSuperJSONOf(t, got, file, []interface{}{Tag("name", HasPrefix("Bob")), Tag("age", Between(40, 45)), Tag("gender", 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{} diff --git a/example_t_test.go b/example_t_test.go index 1d55d6e3..574867ff 100644 --- a/example_t_test.go +++ b/example_t_test.go @@ -912,11 +912,15 @@ func ExampleT_JSON_placeholders() { ok = t.JSON(got, `{"age": $age, "fullname": $name}`, []interface{}{Tag("age", Between(40, 45)), Tag("name", HasSuffix("Foobar"))}) fmt.Println("check got with named placeholders:", ok) + ok = t.JSON(got, `{"age": $^NotZero, "fullname": $^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_JSON_file() { @@ -2314,6 +2318,129 @@ func ExampleT_SubBagOf() { // true } +func ExampleT_SubJSONOf_basic() { + t := 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 := 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}`, []interface{}{42, "Bob Foobar", "male"}) + fmt.Println("check got with numeric placeholders without operators:", ok) + + ok = t.SubJSONOf(got, `{"age": $1, "fullname": $2, "gender": $3}`, []interface{}{Between(40, 45), HasSuffix("Foobar"), NotEmpty()}) + fmt.Println("check got with numeric placeholders:", ok) + + ok = t.SubJSONOf(got, `{"age": "$1", "fullname": "$2", "gender": "$3"}`, []interface{}{Between(40, 45), HasSuffix("Foobar"), NotEmpty()}) + fmt.Println("check got with double-quoted numeric placeholders:", ok) + + ok = t.SubJSONOf(got, `{"age": $age, "fullname": $name, "gender": $gender}`, []interface{}{Tag("age", Between(40, 45)), Tag("name", HasSuffix("Foobar")), Tag("gender", 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 := 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 := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) // clean up + + filename := tmpDir + "/test.json" + if err = ioutil.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, []interface{}{Tag("name", HasPrefix("Bob")), Tag("age", Between(40, 45)), Tag("gender", 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, []interface{}{Tag("name", HasPrefix("Bob")), Tag("age", Between(40, 45)), Tag("gender", 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 := NewT(&testing.T{}) @@ -2386,6 +2513,141 @@ func ExampleT_SuperBagOf() { // true } +func ExampleT_SuperJSONOf_basic() { + t := 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 := 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}`, []interface{}{42, "Bob Foobar", "male"}) + fmt.Println("check got with numeric placeholders without operators:", ok) + + ok = t.SuperJSONOf(got, `{"age": $1, "fullname": $2, "gender": $3}`, []interface{}{Between(40, 45), HasSuffix("Foobar"), NotEmpty()}) + fmt.Println("check got with numeric placeholders:", ok) + + ok = t.SuperJSONOf(got, `{"age": "$1", "fullname": "$2", "gender": "$3"}`, []interface{}{Between(40, 45), HasSuffix("Foobar"), NotEmpty()}) + fmt.Println("check got with double-quoted numeric placeholders:", ok) + + ok = t.SuperJSONOf(got, `{"age": $age, "fullname": $name, "gender": $gender}`, []interface{}{Tag("age", Between(40, 45)), Tag("name", HasSuffix("Foobar")), Tag("gender", 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 := 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 := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) // clean up + + filename := tmpDir + "/test.json" + if err = ioutil.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, []interface{}{Tag("name", HasPrefix("Bob")), Tag("age", Between(40, 45)), Tag("gender", 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, []interface{}{Tag("name", HasPrefix("Bob")), Tag("age", Between(40, 45)), Tag("gender", 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 := NewT(&testing.T{}) diff --git a/example_test.go b/example_test.go index 542c0d6c..d102e2cd 100644 --- a/example_test.go +++ b/example_test.go @@ -1000,11 +1000,15 @@ func ExampleJSON_placeholders() { Tag("name", HasSuffix("Foobar")))) fmt.Println("check got with named placeholders:", ok) + ok = Cmp(t, got, JSON(`{"age": $^NotZero, "fullname": $^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 ExampleJSON_file() { @@ -2539,6 +2543,152 @@ func ExampleSubBagOf() { // true } +func ExampleSubJSONOf_basic() { + t := &testing.T{} + + got := &struct { + Fullname string `json:"fullname"` + Age int `json:"age"` + }{ + Fullname: "Bob", + Age: 42, + } + + ok := Cmp(t, got, SubJSONOf(`{"age":42,"fullname":"Bob","gender":"male"}`)) + fmt.Println("check got with age then fullname:", ok) + + ok = Cmp(t, got, SubJSONOf(`{"fullname":"Bob","age":42,"gender":"male"}`)) + fmt.Println("check got with fullname then age:", ok) + + ok = Cmp(t, got, 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 = Cmp(t, got, 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 := Cmp(t, got, + SubJSONOf(`{"age": $1, "fullname": $2, "gender": $3}`, + 42, "Bob Foobar", "male")) + fmt.Println("check got with numeric placeholders without operators:", ok) + + ok = Cmp(t, got, + SubJSONOf(`{"age": $1, "fullname": $2, "gender": $3}`, + Between(40, 45), + HasSuffix("Foobar"), + NotEmpty())) + fmt.Println("check got with numeric placeholders:", ok) + + ok = Cmp(t, got, + SubJSONOf(`{"age": "$1", "fullname": "$2", "gender": "$3"}`, + Between(40, 45), + HasSuffix("Foobar"), + NotEmpty())) + fmt.Println("check got with double-quoted numeric placeholders:", ok) + + ok = Cmp(t, got, + SubJSONOf(`{"age": $age, "fullname": $name, "gender": $gender}`, + Tag("age", Between(40, 45)), + Tag("name", HasSuffix("Foobar")), + Tag("gender", NotEmpty()))) + fmt.Println("check got with named placeholders:", ok) + + ok = Cmp(t, got, + 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 := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) // clean up + + filename := tmpDir + "/test.json" + if err = ioutil.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 := Cmp(t, got, + SubJSONOf(filename, + Tag("name", HasPrefix("Bob")), + Tag("age", Between(40, 45)), + Tag("gender", 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 = Cmp(t, got, + SubJSONOf(file, + Tag("name", HasPrefix("Bob")), + Tag("age", Between(40, 45)), + Tag("gender", 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{} @@ -2614,6 +2764,165 @@ func ExampleSuperBagOf() { // 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 := Cmp(t, got, SuperJSONOf(`{"age":42,"fullname":"Bob","gender":"male"}`)) + fmt.Println("check got with age then fullname:", ok) + + ok = Cmp(t, got, SuperJSONOf(`{"fullname":"Bob","age":42,"gender":"male"}`)) + fmt.Println("check got with fullname then age:", ok) + + ok = Cmp(t, got, 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 = Cmp(t, got, + 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 := Cmp(t, got, + SuperJSONOf(`{"age": $1, "fullname": $2, "gender": $3}`, + 42, "Bob Foobar", "male")) + fmt.Println("check got with numeric placeholders without operators:", ok) + + ok = Cmp(t, got, + SuperJSONOf(`{"age": $1, "fullname": $2, "gender": $3}`, + Between(40, 45), + HasSuffix("Foobar"), + NotEmpty())) + fmt.Println("check got with numeric placeholders:", ok) + + ok = Cmp(t, got, + SuperJSONOf(`{"age": "$1", "fullname": "$2", "gender": "$3"}`, + Between(40, 45), + HasSuffix("Foobar"), + NotEmpty())) + fmt.Println("check got with double-quoted numeric placeholders:", ok) + + ok = Cmp(t, got, + SuperJSONOf(`{"age": $age, "fullname": $name, "gender": $gender}`, + Tag("age", Between(40, 45)), + Tag("name", HasSuffix("Foobar")), + Tag("gender", NotEmpty()))) + fmt.Println("check got with named placeholders:", ok) + + ok = Cmp(t, got, + 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 := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) // clean up + + filename := tmpDir + "/test.json" + if err = ioutil.WriteFile(filename, []byte(` +{ + "fullname": "$name", + "age": "$age", + "gender": "$gender" +}`), 0644); err != nil { + t.Fatal(err) + } + + // OK let's test with this file + ok := Cmp(t, got, + SuperJSONOf(filename, + Tag("name", HasPrefix("Bob")), + Tag("age", Between(40, 45)), + Tag("gender", 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 = Cmp(t, got, + SuperJSONOf(file, + Tag("name", HasPrefix("Bob")), + Tag("age", Between(40, 45)), + Tag("gender", 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{} diff --git a/t.go b/t.go index 10a4c561..f0ae5c2b 100644 --- a/t.go +++ b/t.go @@ -879,6 +879,25 @@ func (t *T) SubBagOf(got interface{}, expectedItems []interface{}, args ...inter return t.Cmp(got, SubBagOf(expectedItems...), args...) } +// SubJSONOf is a shortcut for: +// +// t.Cmp(got, SubJSONOf(expectedJSON, params...), args...) +// +// See https://godoc.org/github.com/maxatome/go-testdeep#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 interface{}, expectedJSON interface{}, params []interface{}, args ...interface{}) bool { + t.Helper() + return t.Cmp(got, SubJSONOf(expectedJSON, params...), args...) +} + // SubMapOf is a shortcut for: // // t.Cmp(got, SubMapOf(model, expectedEntries), args...) @@ -936,6 +955,25 @@ func (t *T) SuperBagOf(got interface{}, expectedItems []interface{}, args ...int return t.Cmp(got, SuperBagOf(expectedItems...), args...) } +// SuperJSONOf is a shortcut for: +// +// t.Cmp(got, SuperJSONOf(expectedJSON, params...), args...) +// +// See https://godoc.org/github.com/maxatome/go-testdeep#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 interface{}, expectedJSON interface{}, params []interface{}, args ...interface{}) bool { + t.Helper() + return t.Cmp(got, SuperJSONOf(expectedJSON, params...), args...) +} + // SuperMapOf is a shortcut for: // // t.Cmp(got, SuperMapOf(model, expectedEntries), args...) diff --git a/td_json.go b/td_json.go index 790bf567..5d2f179b 100644 --- a/td_json.go +++ b/td_json.go @@ -19,6 +19,7 @@ import ( "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/dark" + "github.com/maxatome/go-testdeep/internal/types" "github.com/maxatome/go-testdeep/internal/util" ) @@ -35,7 +36,7 @@ var jsonUnescaper = strings.NewReplacer( ) const ( - commentStart = "" + commentStart = `` commentEnd = `"` // note final " commentStartRepl = `" /* ` @@ -75,6 +76,8 @@ type tdJSON struct { expected reflect.Value } +var _ TestDeep = &tdJSON{} + func unmarshal(expectedJSON interface{}, target interface{}) { var ( err error @@ -126,6 +129,21 @@ var jsonOpShortcuts = map[string]func() TestDeep{ "Zero": Zero, } +func getTags(params []interface{}) (byTag map[string]*tdTag) { + for _, op := range params { + if tag, ok := op.(*tdTag); ok { + if byTag[tag.tag] != nil { + panic(`2 params have the same tag "` + tag.tag + `"`) + } + if byTag == nil { + byTag = map[string]*tdTag{} + } + byTag[tag.tag] = tag + } + } + return +} + // scan scans "*v" data structure to find strings containing // placeholders (like $123 or $name) corresponding to a value or // TestDeep operator contained in "params" and "byTag". @@ -217,6 +235,31 @@ func scan(v *interface{}, params []interface{}, byTag map[string]*tdTag, path st } } +func gotViaJSON(ctx ctxerr.Context, pGot *reflect.Value) *ctxerr.Error { + gotIf, ok := dark.GetInterface(*pGot, true) + if !ok { + return ctx.CannotCompareError() + } + + b, err := json.Marshal(gotIf) + if err != nil { + if ctx.BooleanError { + return ctxerr.BooleanError + } + return &ctxerr.Error{ + Message: "json.Marshal failed", + Summary: ctxerr.NewSummary(err.Error()), + } + } + + // As Marshal succeeded, Unmarshal in an interface{} cannot fail + var vgot interface{} + json.Unmarshal(b, &vgot) //nolint: errcheck + + *pGot = reflect.ValueOf(vgot) + return nil +} + // summary(JSON): compares against JSON representation // input(JSON): nil,bool,str,int,float,array,slice,map,struct,ptr @@ -267,7 +310,7 @@ func scan(v *interface{}, params []interface{}, byTag map[string]*tdTag, path st // // A JSON filename ends with ".json". // -// To avoid a legit "$" string prefix cause a bad placeholder error, +// 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: // @@ -334,71 +377,45 @@ func JSON(expectedJSON interface{}, params ...interface{}) TestDeep { var v interface{} unmarshal(expectedJSON, &v) - // Load named placeholders - var byTag map[string]*tdTag - for _, op := range params { - if tag, ok := op.(*tdTag); ok { - if byTag[tag.tag] != nil { - panic(`2 params have the same tag "` + tag.tag + `"`) - } - if byTag == nil { - byTag = map[string]*tdTag{} - } - byTag[tag.tag] = tag - } - } + scan(&v, params, getTags(params), "") - j := tdJSON{ + return &tdJSON{ baseOKNil: newBaseOKNil(3), + expected: reflect.ValueOf(v), } - scan(&v, params, byTag, "") - - j.expected = reflect.ValueOf(v) - - return &j } func (j *tdJSON) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { - gotIf, ok := dark.GetInterface(got, true) - if !ok { - return ctx.CannotCompareError() - } - - b, err := json.Marshal(gotIf) + err := gotViaJSON(ctx, &got) if err != nil { - if ctx.BooleanError { - return ctxerr.BooleanError - } - return ctx.CollectError(&ctxerr.Error{ - Message: "json.Marshal failed", - Summary: ctxerr.NewSummary(err.Error()), - }) + return ctx.CollectError(err) } - // Unmarshal cannot fail - var vgot interface{} - json.Unmarshal(b, &vgot) //nolint: errcheck - ctx.BeLax = true - return deepValueEqual(ctx, reflect.ValueOf(vgot), j.expected) + return deepValueEqual(ctx, got, j.expected) } func (j *tdJSON) String() string { - if !j.expected.IsValid() { + 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("JSON(") + b.WriteString(opName) + b.WriteByte('(') enc := json.NewEncoder(&b) enc.SetEscapeHTML(false) - enc.SetIndent(" ", " ") + enc.SetIndent(strings.Repeat(" ", len(opName)+1), " ") // cannot return an error here - enc.Encode(j.expected.Interface()) //nolint: errcheck + enc.Encode(v.Interface()) //nolint: errcheck b.Truncate(b.Len() - 1) b.WriteByte(')') @@ -454,3 +471,351 @@ func (j *tdJSON) TypeBehind() reflect.Type { } return interfaceInterface } + +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, marshalled 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 ioutil.ReadFile before unmarshaling) +// - []byte containing JSON data +// - io.Reader stream containing JSON data (is ioutil.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, +// } +// Cmp(t, got, SubJSONOf(`{"name": "Bob", "age": 42, "city": "NY"}`)) // succeeds +// Cmp(t, got, 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: +// +// Cmp(t, gotValue, +// SubJSONOf(`{"fullname": $name, "age": $2, "gender": $3}`, +// Tag("name", HasPrefix("Foo")), // matches $1 and $name +// Between(41, 43), // matches only $2 +// "male")) // matches only $3 +// +// Note that placeholders can be double-quoted as in: +// +// Cmp(t, gotValue, +// SubJSONOf(`{"fullname": "$name", "age": "$2", "gender": "$3"}`, +// Tag("name", HasPrefix("Foo")), // matches $1 and $name +// 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. +// +// Note "expectedJSON" can be a []byte, JSON filename or io.Reader: +// +// Cmp(t, gotValue, SubJSONOf("file.json", Between(12, 34))) +// Cmp(t, gotValue, SubJSONOf([]byte(`[1, $1, 3]`), Between(12, 34))) +// Cmp(t, gotValue, SubJSONOf(osFile, 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: +// +// Cmp(t, gotValue, +// SubJSONOf(`{"fullname": "$name", "details": "$$info", "age": $2}`, +// Tag("name", HasPrefix("Foo")), // matches $1 and $name +// 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: +// +// 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 */ +// }`, +// Tag("name", HasPrefix("Foo")), // matches $1 and $name +// 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 */. +// +// Last but not least, simple operators can be directly embedded in +// JSON data without requiring any placeholder but using directly +// $^OperatorName. They are operator shortcuts: +// +// Cmp(t, gotValue, SubJSONOf(`{"id": $1}`, NotZero())) +// +// can be written as: +// +// Cmp(t, gotValue, SubJSONOf(`{"id": $^NotZero}`)) +// +// Unfortunately, only simple operators (in fact those which take no +// parameters) have shortcuts. They follow: +// - Empty → $^Empty +// - Ignore → $^Ignore +// - NaN → $^NaN +// - Nil → $^Nil +// - NotEmpty → $^NotEmpty +// - NotNaN → $^NotNaN +// - NotNil → $^NotNil +// - NotZero → $^NotZero +// - Zero → $^Zero +// +// TypeBehind method returns the map[string]interface{} type. +func SubJSONOf(expectedJSON interface{}, params ...interface{}) TestDeep { + var v interface{} + unmarshal(expectedJSON, &v) + + scan(&v, params, getTags(params), "") + + _, ok := v.(map[string]interface{}) + if !ok { + panic("SubJSONOf only accepts JSON objects {…}") + } + + m := tdMapJSON{ + tdMap: tdMap{ + tdExpectedType: tdExpectedType{ + base: newBase(3), + expectedType: reflect.TypeOf((map[string]interface{})(nil)), + }, + kind: subMap, + }, + 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, marshalled 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 ioutil.ReadFile before unmarshaling) +// - []byte containing JSON data +// - io.Reader stream containing JSON data (is ioutil.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", +// } +// Cmp(t, got, SuperJSONOf(`{"name": "Bob", "age": 42}`)) // succeeds +// Cmp(t, got, 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: +// +// Cmp(t, gotValue, +// SuperJSONOf(`{"fullname": $name, "age": $2, "gender": $3}`, +// Tag("name", HasPrefix("Foo")), // matches $1 and $name +// Between(41, 43), // matches only $2 +// "male")) // matches only $3 +// +// Note that placeholders can be double-quoted as in: +// +// Cmp(t, gotValue, +// SuperJSONOf(`{"fullname": "$name", "age": "$2", "gender": "$3"}`, +// Tag("name", HasPrefix("Foo")), // matches $1 and $name +// 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. +// +// Note "expectedJSON" can be a []byte, JSON filename or io.Reader: +// +// Cmp(t, gotValue, SuperJSONOf("file.json", Between(12, 34))) +// Cmp(t, gotValue, SuperJSONOf([]byte(`[1, $1, 3]`), Between(12, 34))) +// Cmp(t, gotValue, SuperJSONOf(osFile, 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: +// +// Cmp(t, gotValue, +// SuperJSONOf(`{"fullname": "$name", "details": "$$info", "age": $2}`, +// Tag("name", HasPrefix("Foo")), // matches $1 and $name +// 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: +// +// Cmp(t, gotValue, +// 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 */ +// }`, +// Tag("name", HasPrefix("Foo")), // matches $1 and $name +// 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 */. +// +// Last but not least, simple operators can be directly embedded in +// JSON data without requiring any placeholder but using directly +// $^OperatorName. They are operator shortcuts: +// +// Cmp(t, gotValue, SuperJSONOf(`{"id": $1}`, NotZero())) +// +// can be written as: +// +// Cmp(t, gotValue, SuperJSONOf(`{"id": $^NotZero}`)) +// +// Unfortunately, only simple operators (in fact those which take no +// parameters) have shortcuts. They follow: +// - Empty → $^Empty +// - Ignore → $^Ignore +// - NaN → $^NaN +// - Nil → $^Nil +// - NotEmpty → $^NotEmpty +// - NotNaN → $^NotNaN +// - NotNil → $^NotNil +// - NotZero → $^NotZero +// - Zero → $^Zero +// +// TypeBehind method returns the map[string]interface{} type. +func SuperJSONOf(expectedJSON interface{}, params ...interface{}) TestDeep { + var v interface{} + unmarshal(expectedJSON, &v) + + scan(&v, params, getTags(params), "") + + _, ok := v.(map[string]interface{}) + if !ok { + panic("SuperJSONOf only accepts JSON objects {…}") + } + + m := tdMapJSON{ + tdMap: tdMap{ + tdExpectedType: tdExpectedType{ + base: newBase(3), + expectedType: reflect.TypeOf((map[string]interface{})(nil)), + }, + kind: superMap, + }, + expected: reflect.ValueOf(v), + } + m.populateExpectedEntries(nil, m.expected) + + return &m +} + +func (m *tdMapJSON) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { + 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 { + return jsonStringify(m.GetLocation().Func, m.expected) +} + +func (m *tdMapJSON) HandleInvalid() bool { + return true +} diff --git a/td_json_test.go b/td_json_test.go index 150ca10c..df364a81 100644 --- a/td_json_test.go +++ b/td_json_test.go @@ -253,3 +253,266 @@ func TestJSONTypeBehind(t *testing.T) { t.Errorf("Failed test: got %s intead of interface {}", nullType) } } + +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, + testdeep.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, + testdeep.SubJSONOf(`{"name":"$1","age":$2,"gender":$3,"details":{}}`, + "Bob", 42, "male")) // raw values + + // Tag placeholders + checkOK(t, got, + testdeep.SubJSONOf( + `{"name":"$name","age":$age,"gender":"$gender","details":{}}`, + testdeep.Tag("name", testdeep.Re(`^Bob`)), + testdeep.Tag("age", testdeep.Between(40, 45)), + testdeep.Tag("gender", testdeep.NotEmpty()))) + + // Mixed placeholders + operator shortcut + checkOK(t, got, + testdeep.SubJSONOf( + `{"name":"$name","age":$1,"gender":$^NotEmpty,"details":{}}`, + testdeep.Tag("age", testdeep.Between(40, 45)), + testdeep.Tag("name", testdeep.Re(`^Bob`)))) + + // + // Errors + checkError(t, func() {}, testdeep.SubJSONOf(`{}`), + expectedError{ + Message: mustBe("json.Marshal failed"), + Summary: mustContain("json: unsupported type"), + }) + + for i, n := range []interface{}{ + nil, + (map[string]interface{})(nil), + (map[string]bool)(nil), + ([]int)(nil), + } { + checkError(t, n, testdeep.SubJSONOf(`{}`), + expectedError{ + Message: mustBe("values differ"), + Got: mustBe("null"), + Expected: mustBe("non-null"), + }, + "nil test #%d", i) + } + + // + // Panics + test.CheckPanic(t, func() { testdeep.SubJSONOf(`[1, "$123bad"]`) }, + `JSON obj[1] contains a bad numeric placeholder "$123bad"`) + test.CheckPanic(t, func() { testdeep.SubJSONOf(`[1, $000]`) }, + `JSON obj[1] contains invalid numeric placeholder "$000", it should start at "$1"`) + test.CheckPanic(t, func() { testdeep.SubJSONOf(`[1, $1]`) }, + `JSON obj[1] contains numeric placeholder "$1", but only 0 params given`) + test.CheckPanic(t, func() { testdeep.SubJSONOf(`[1, 2, $3]`, testdeep.Ignore()) }, + `JSON obj[2] contains numeric placeholder "$3", but only 1 params given`) + + // operator shortcut + test.CheckPanic(t, func() { testdeep.SubJSONOf(`[1, "$^bad%"]`) }, + `JSON obj[1] contains a bad operator shortcut "$^bad%"`) + // named placeholders + test.CheckPanic(t, func() { testdeep.SubJSONOf(`[1, "$bad%"]`) }, + `JSON obj[1] contains a bad placeholder "$bad%"`) + test.CheckPanic(t, func() { testdeep.SubJSONOf(`[1, $unknown]`) }, + `JSON obj[1] contains a unknown placeholder "$unknown"`) + + test.CheckPanic(t, func() { testdeep.SubJSONOf("null") }, + "SubJSONOf only accepts JSON objects {…}") + + // + // Stringification + test.EqualStr(t, testdeep.SubJSONOf(`{}`).String(), `SubJSONOf({})`) + + test.EqualStr(t, testdeep.SubJSONOf(`{"foo":1, "bar":2}`).String(), + ` +SubJSONOf({ + "bar": 2, + "foo": 1 + })`[1:]) + + test.EqualStr(t, + testdeep.SubJSONOf(`{"label": $value, "zip": $^NotZero}`, + testdeep.Tag("value", testdeep.Bag( + testdeep.SubJSONOf(`{"name": $1,"age":$2}`, + testdeep.HasPrefix("Bob"), + testdeep.Between(12, 24), + ), + testdeep.SubJSONOf(`{"name": $1}`, testdeep.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:]) +} + +func TestSubJSONOfTypeBehind(t *testing.T) { + equalTypes(t, testdeep.SubJSONOf(`{"a":12}`), (map[string]interface{})(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, testdeep.SuperJSONOf(`{"name": "Bob"}`)) + + // Numeric placeholders + checkOK(t, got, + testdeep.SuperJSONOf(`{"name":"$1","age":$2}`, + "Bob", 42)) // raw values + + // Tag placeholders + checkOK(t, got, + testdeep.SuperJSONOf(`{"name":"$name","gender":"$gender"}`, + testdeep.Tag("name", testdeep.Re(`^Bob`)), + testdeep.Tag("gender", testdeep.NotEmpty()))) + + // Mixed placeholders + operator shortcut + checkOK(t, got, + testdeep.SuperJSONOf( + `{"name":"$name","age":$1,"gender":$^NotEmpty}`, + testdeep.Tag("age", testdeep.Between(40, 45)), + testdeep.Tag("name", testdeep.Re(`^Bob`)))) + + // …with comments… + checkOK(t, got, + testdeep.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 +}`, + testdeep.Tag("age", testdeep.Between(40, 45)), + testdeep.Tag("name", testdeep.Re(`^Bob`)))) + + // + // Errors + checkError(t, func() {}, testdeep.SuperJSONOf(`{}`), + expectedError{ + Message: mustBe("json.Marshal failed"), + Summary: mustContain("json: unsupported type"), + }) + + for i, n := range []interface{}{ + nil, + (map[string]interface{})(nil), + (map[string]bool)(nil), + ([]int)(nil), + } { + checkError(t, n, testdeep.SuperJSONOf(`{}`), + expectedError{ + Message: mustBe("values differ"), + Got: mustBe("null"), + Expected: mustBe("non-null"), + }, + "nil test #%d", i) + } + + // + // Panics + test.CheckPanic(t, func() { testdeep.SuperJSONOf(`[1, "$123bad"]`) }, + `JSON obj[1] contains a bad numeric placeholder "$123bad"`) + test.CheckPanic(t, func() { testdeep.SuperJSONOf(`[1, $000]`) }, + `JSON obj[1] contains invalid numeric placeholder "$000", it should start at "$1"`) + test.CheckPanic(t, func() { testdeep.SuperJSONOf(`[1, $1]`) }, + `JSON obj[1] contains numeric placeholder "$1", but only 0 params given`) + test.CheckPanic(t, func() { testdeep.SuperJSONOf(`[1, 2, $3]`, testdeep.Ignore()) }, + `JSON obj[2] contains numeric placeholder "$3", but only 1 params given`) + + // operator shortcut + test.CheckPanic(t, func() { testdeep.SuperJSONOf(`[1, "$^bad%"]`) }, + `JSON obj[1] contains a bad operator shortcut "$^bad%"`) + // named placeholders + test.CheckPanic(t, func() { testdeep.SuperJSONOf(`[1, "$bad%"]`) }, + `JSON obj[1] contains a bad placeholder "$bad%"`) + test.CheckPanic(t, func() { testdeep.SuperJSONOf(`[1, $unknown]`) }, + `JSON obj[1] contains a unknown placeholder "$unknown"`) + + test.CheckPanic(t, func() { testdeep.SuperJSONOf("null") }, + "SuperJSONOf only accepts JSON objects {…}") + + // + // Stringification + test.EqualStr(t, testdeep.SuperJSONOf(`{}`).String(), `SuperJSONOf({})`) + + test.EqualStr(t, testdeep.SuperJSONOf(`{"foo":1, "bar":2}`).String(), + ` +SuperJSONOf({ + "bar": 2, + "foo": 1 + })`[1:]) + + test.EqualStr(t, + testdeep.SuperJSONOf(`{"label": $value, "zip": $^NotZero}`, + testdeep.Tag("value", testdeep.Bag( + testdeep.SuperJSONOf(`{"name": $1,"age":$2}`, + testdeep.HasPrefix("Bob"), + testdeep.Between(12, 24), + ), + testdeep.SuperJSONOf(`{"name": $1}`, testdeep.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:]) +} + +func TestSuperJSONOfTypeBehind(t *testing.T) { + equalTypes(t, testdeep.SuperJSONOf(`{"a":12}`), (map[string]interface{})(nil)) +} diff --git a/td_map.go b/td_map.go index 14e4b79b..74118570 100644 --- a/td_map.go +++ b/td_map.go @@ -231,6 +231,10 @@ func (m *tdMap) Match(ctx ctxerr.Context, got reflect.Value) (err *ctxerr.Error) 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) diff --git a/tools/docs_src/content/operators/JSON.md b/tools/docs_src/content/operators/JSON.md index 208c1217..a3770047 100644 --- a/tools/docs_src/content/operators/JSON.md +++ b/tools/docs_src/content/operators/JSON.md @@ -48,7 +48,7 @@ Cmp(t, gotValue, 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`]({{< ref "JSON" >}}) +can be preferred when the JSON data has to conform to the JSON specification, like when used in a ".json" file. Note *expectedJSON* can be a `[]byte`, JSON filename or [`io.Reader`](https://golang.org/pkg/io/#Reader): @@ -61,7 +61,7 @@ Cmp(t, gotValue, JSON(osFile, Between(12, 34))) A JSON filename ends with ".json". -To avoid a legit "$" `string` prefix cause a bad placeholder [`error`](https://golang.org/pkg/builtin/#error), +To avoid a legit "$" `string` prefix causes a bad placeholder [`error`](https://golang.org/pkg/builtin/#error), just double it to escape it. Note it is only needed when the "$" is the first character of a `string`: @@ -229,11 +229,15 @@ parameters) have shortcuts. They follow: Tag("name", HasSuffix("Foobar")))) fmt.Println("check got with named placeholders:", ok) + ok = Cmp(t, got, JSON(`{"age": $^NotZero, "fullname": $^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 ```{{% /expand%}} {{%expand "File example" %}}```go @@ -396,11 +400,15 @@ reason of a potential failure. ok = CmpJSON(t, got, `{"age": $age, "fullname": $name}`, []interface{}{Tag("age", Between(40, 45)), Tag("name", HasSuffix("Foobar"))}) fmt.Println("check got with named placeholders:", ok) + ok = CmpJSON(t, got, `{"age": $^NotZero, "fullname": $^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 ```{{% /expand%}} {{%expand "File example" %}}```go @@ -555,11 +563,15 @@ reason of a potential failure. ok = t.JSON(got, `{"age": $age, "fullname": $name}`, []interface{}{Tag("age", Between(40, 45)), Tag("name", HasSuffix("Foobar"))}) fmt.Println("check got with named placeholders:", ok) + ok = t.JSON(got, `{"age": $^NotZero, "fullname": $^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 ```{{% /expand%}} {{%expand "File example" %}}```go diff --git a/tools/docs_src/content/operators/SubJSONOf.md b/tools/docs_src/content/operators/SubJSONOf.md new file mode 100644 index 00000000..964af6c6 --- /dev/null +++ b/tools/docs_src/content/operators/SubJSONOf.md @@ -0,0 +1,608 @@ +--- +title: "SubJSONOf" +weight: 10 +--- + +```go +func SubJSONOf(expectedJSON interface{}, params ...interface{}) TestDeep +``` + +[`SubJSONOf`]({{< ref "SubJSONOf" >}}) operator allows to compare the JSON representation of +data against *expectedJSON*. Unlike [`JSON`]({{< ref "JSON" >}}) operator, marshalled 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 [`ioutil.ReadFile`](https://golang.org/pkg/ioutil/#ReadFile) before unmarshaling) +- `[]byte` containing JSON data +- [`io.Reader`](https://golang.org/pkg/io/#Reader) stream containing JSON data (is [`ioutil.ReadAll`](https://golang.org/pkg/ioutil/#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. + +```go +type MyStruct struct { + Name string `json:"name"` + Age int `json:"age"` +} +got := MyStruct + Name: "Bob", + Age: 42, +} +Cmp(t, got, SubJSONOf(`{"name": "Bob", "age": 42, "city": "NY"}`)) // succeeds +Cmp(t, got, 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]({{< ref "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`]({{< ref "Tag" >}}) operator as follows: + +```go +Cmp(t, gotValue, + SubJSONOf(`{"fullname": $name, "age": $2, "gender": $3}`, + Tag("name", HasPrefix("Foo")), // matches $1 and $name + Between(41, 43), // matches only $2 + "male")) // matches only $3 +``` + +Note that placeholders can be double-quoted as in: + +```go +Cmp(t, gotValue, + SubJSONOf(`{"fullname": "$name", "age": "$2", "gender": "$3"}`, + Tag("name", HasPrefix("Foo")), // matches $1 and $name + 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. + +Note *expectedJSON* can be a `[]byte`, JSON filename or [`io.Reader`](https://golang.org/pkg/io/#Reader): + +```go +Cmp(t, gotValue, SubJSONOf("file.json", Between(12, 34))) +Cmp(t, gotValue, SubJSONOf([]byte(`[1, $1, 3]`), Between(12, 34))) +Cmp(t, gotValue, SubJSONOf(osFile, Between(12, 34))) +``` + +A JSON filename ends with ".json". + +To avoid a legit "$" `string` prefix causes a bad placeholder [`error`](https://golang.org/pkg/builtin/#error), +just double it to escape it. Note it is only needed when the "$" is +the first character of a `string`: + +```go +Cmp(t, gotValue, + SubJSONOf(`{"fullname": "$name", "details": "$$info", "age": $2}`, + Tag("name", HasPrefix("Foo")), // matches $1 and $name + Between(41, 43))) // matches only $2 +``` + +For the "details" key, the raw value "`$info`" is expected, no +placeholders are involved here. + +Note that [`Lax`]({{< ref "Lax" >}}) mode is automatically enabled by [`SubJSONOf`]({{< ref "SubJSONOf" >}}) operator to +simplify numeric tests. + +Comments can be embedded in JSON data: + +```go +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 */ +}`, + Tag("name", HasPrefix("Foo")), // matches $1 and $name + 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 */. + + +Last but not least, simple operators can be directly embedded in +JSON data without requiring any placeholder but using directly +`$^OperatorName`. They are operator shortcuts: + +```go +Cmp(t, gotValue, SubJSONOf(`{"id": $1}`, NotZero())) +``` + +can be written as: + +```go +Cmp(t, gotValue, SubJSONOf(`{"id": $^NotZero}`)) +``` + +Unfortunately, only simple operators (in fact those which take no +parameters) have shortcuts. They follow: + +- [`Empty`]({{< ref "Empty" >}}) → `$^Empty` +- [`Ignore`]({{< ref "Ignore" >}}) → `$^Ignore` +- [`NaN`]({{< ref "NaN" >}}) → `$^NaN` +- [`Nil`]({{< ref "Nil" >}}) → `$^Nil` +- [`NotEmpty`]({{< ref "NotEmpty" >}}) → `$^NotEmpty` +- [`NotNaN`]({{< ref "NotNaN" >}}) → `$^NotNaN` +- [`NotNil`]({{< ref "NotNil" >}}) → `$^NotNil` +- [`NotZero`]({{< ref "NotZero" >}}) → `$^NotZero` +- [`Zero`]({{< ref "Zero" >}}) → `$^Zero` + + +[TypeBehind]({{< ref "operators#typebehind-method" >}}) method returns the `map[string]interface{}` type. + + +> See also [ SubJSONOf godoc](https://godoc.org/github.com/maxatome/go-testdeep#SubJSONOf). + +### Examples + +{{%expand "Basic example" %}}```go + t := &testing.T{} + + got := &struct { + Fullname string `json:"fullname"` + Age int `json:"age"` + }{ + Fullname: "Bob", + Age: 42, + } + + ok := Cmp(t, got, SubJSONOf(`{"age":42,"fullname":"Bob","gender":"male"}`)) + fmt.Println("check got with age then fullname:", ok) + + ok = Cmp(t, got, SubJSONOf(`{"fullname":"Bob","age":42,"gender":"male"}`)) + fmt.Println("check got with fullname then age:", ok) + + ok = Cmp(t, got, 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 = Cmp(t, got, 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 + +```{{% /expand%}} +{{%expand "Placeholders example" %}}```go + t := &testing.T{} + + got := &struct { + Fullname string `json:"fullname"` + Age int `json:"age"` + }{ + Fullname: "Bob Foobar", + Age: 42, + } + + ok := Cmp(t, got, + SubJSONOf(`{"age": $1, "fullname": $2, "gender": $3}`, + 42, "Bob Foobar", "male")) + fmt.Println("check got with numeric placeholders without operators:", ok) + + ok = Cmp(t, got, + SubJSONOf(`{"age": $1, "fullname": $2, "gender": $3}`, + Between(40, 45), + HasSuffix("Foobar"), + NotEmpty())) + fmt.Println("check got with numeric placeholders:", ok) + + ok = Cmp(t, got, + SubJSONOf(`{"age": "$1", "fullname": "$2", "gender": "$3"}`, + Between(40, 45), + HasSuffix("Foobar"), + NotEmpty())) + fmt.Println("check got with double-quoted numeric placeholders:", ok) + + ok = Cmp(t, got, + SubJSONOf(`{"age": $age, "fullname": $name, "gender": $gender}`, + Tag("age", Between(40, 45)), + Tag("name", HasSuffix("Foobar")), + Tag("gender", NotEmpty()))) + fmt.Println("check got with named placeholders:", ok) + + ok = Cmp(t, got, + 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 + +```{{% /expand%}} +{{%expand "File example" %}}```go + 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 := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) // clean up + + filename := tmpDir + "/test.json" + if err = ioutil.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 := Cmp(t, got, + SubJSONOf(filename, + Tag("name", HasPrefix("Bob")), + Tag("age", Between(40, 45)), + Tag("gender", 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 = Cmp(t, got, + SubJSONOf(file, + Tag("name", HasPrefix("Bob")), + Tag("age", Between(40, 45)), + Tag("gender", 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 + +```{{% /expand%}} +## CmpSubJSONOf shortcut + +```go +func CmpSubJSONOf(t TestingT, got interface{}, expectedJSON interface{}, params []interface{}, args ...interface{}) bool +``` + +CmpSubJSONOf is a shortcut for: + +```go +Cmp(t, got, SubJSONOf(expectedJSON, params...), args...) +``` + +See above 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`](https://golang.org/pkg/fmt/#Fprintf) is used to compose the name, else *args* are passed to +[`fmt.Fprint`](https://golang.org/pkg/fmt/#Fprint). Do not forget it is the name of the test, not the +reason of a potential failure. + + +> See also [ CmpSubJSONOf godoc](https://godoc.org/github.com/maxatome/go-testdeep#CmpSubJSONOf). + +### Examples + +{{%expand "Basic example" %}}```go + t := &testing.T{} + + got := &struct { + Fullname string `json:"fullname"` + Age int `json:"age"` + }{ + Fullname: "Bob", + Age: 42, + } + + ok := CmpSubJSONOf(t, got, `{"age":42,"fullname":"Bob","gender":"male"}`, nil) + fmt.Println("check got with age then fullname:", ok) + + ok = CmpSubJSONOf(t, got, `{"fullname":"Bob","age":42,"gender":"male"}`, nil) + fmt.Println("check got with fullname then age:", ok) + + ok = 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 = 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 + +```{{% /expand%}} +{{%expand "Placeholders example" %}}```go + t := &testing.T{} + + got := &struct { + Fullname string `json:"fullname"` + Age int `json:"age"` + }{ + Fullname: "Bob Foobar", + Age: 42, + } + + ok := CmpSubJSONOf(t, got, `{"age": $1, "fullname": $2, "gender": $3}`, []interface{}{42, "Bob Foobar", "male"}) + fmt.Println("check got with numeric placeholders without operators:", ok) + + ok = CmpSubJSONOf(t, got, `{"age": $1, "fullname": $2, "gender": $3}`, []interface{}{Between(40, 45), HasSuffix("Foobar"), NotEmpty()}) + fmt.Println("check got with numeric placeholders:", ok) + + ok = CmpSubJSONOf(t, got, `{"age": "$1", "fullname": "$2", "gender": "$3"}`, []interface{}{Between(40, 45), HasSuffix("Foobar"), NotEmpty()}) + fmt.Println("check got with double-quoted numeric placeholders:", ok) + + ok = CmpSubJSONOf(t, got, `{"age": $age, "fullname": $name, "gender": $gender}`, []interface{}{Tag("age", Between(40, 45)), Tag("name", HasSuffix("Foobar")), Tag("gender", NotEmpty())}) + fmt.Println("check got with named placeholders:", ok) + + ok = 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 + +```{{% /expand%}} +{{%expand "File example" %}}```go + 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 := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) // clean up + + filename := tmpDir + "/test.json" + if err = ioutil.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 := CmpSubJSONOf(t, got, filename, []interface{}{Tag("name", HasPrefix("Bob")), Tag("age", Between(40, 45)), Tag("gender", 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 = CmpSubJSONOf(t, got, file, []interface{}{Tag("name", HasPrefix("Bob")), Tag("age", Between(40, 45)), Tag("gender", 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 + +```{{% /expand%}} +## T.SubJSONOf shortcut + +```go +func (t *T) SubJSONOf(got interface{}, expectedJSON interface{}, params []interface{}, args ...interface{}) bool +``` + +[`SubJSONOf`]({{< ref "SubJSONOf" >}}) is a shortcut for: + +```go +t.Cmp(got, SubJSONOf(expectedJSON, params...), args...) +``` + +See above 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`](https://golang.org/pkg/fmt/#Fprintf) is used to compose the name, else *args* are passed to +[`fmt.Fprint`](https://golang.org/pkg/fmt/#Fprint). Do not forget it is the name of the test, not the +reason of a potential failure. + + +> See also [ T.SubJSONOf godoc](https://godoc.org/github.com/maxatome/go-testdeep#T.SubJSONOf). + +### Examples + +{{%expand "Basic example" %}}```go + t := 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 + +```{{% /expand%}} +{{%expand "Placeholders example" %}}```go + t := 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}`, []interface{}{42, "Bob Foobar", "male"}) + fmt.Println("check got with numeric placeholders without operators:", ok) + + ok = t.SubJSONOf(got, `{"age": $1, "fullname": $2, "gender": $3}`, []interface{}{Between(40, 45), HasSuffix("Foobar"), NotEmpty()}) + fmt.Println("check got with numeric placeholders:", ok) + + ok = t.SubJSONOf(got, `{"age": "$1", "fullname": "$2", "gender": "$3"}`, []interface{}{Between(40, 45), HasSuffix("Foobar"), NotEmpty()}) + fmt.Println("check got with double-quoted numeric placeholders:", ok) + + ok = t.SubJSONOf(got, `{"age": $age, "fullname": $name, "gender": $gender}`, []interface{}{Tag("age", Between(40, 45)), Tag("name", HasSuffix("Foobar")), Tag("gender", 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 + +```{{% /expand%}} +{{%expand "File example" %}}```go + t := 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 := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) // clean up + + filename := tmpDir + "/test.json" + if err = ioutil.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, []interface{}{Tag("name", HasPrefix("Bob")), Tag("age", Between(40, 45)), Tag("gender", 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, []interface{}{Tag("name", HasPrefix("Bob")), Tag("age", Between(40, 45)), Tag("gender", 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 + +```{{% /expand%}} diff --git a/tools/docs_src/content/operators/SuperJSONOf.md b/tools/docs_src/content/operators/SuperJSONOf.md new file mode 100644 index 00000000..eefdbb42 --- /dev/null +++ b/tools/docs_src/content/operators/SuperJSONOf.md @@ -0,0 +1,647 @@ +--- +title: "SuperJSONOf" +weight: 10 +--- + +```go +func SuperJSONOf(expectedJSON interface{}, params ...interface{}) TestDeep +``` + +[`SuperJSONOf`]({{< ref "SuperJSONOf" >}}) operator allows to compare the JSON representation of +data against *expectedJSON*. Unlike [`JSON`]({{< ref "JSON" >}}) operator, marshalled 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 [`ioutil.ReadFile`](https://golang.org/pkg/ioutil/#ReadFile) before unmarshaling) +- `[]byte` containing JSON data +- [`io.Reader`](https://golang.org/pkg/io/#Reader) stream containing JSON data (is [`ioutil.ReadAll`](https://golang.org/pkg/ioutil/#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. + +```go +type MyStruct struct { + Name string `json:"name"` + Age int `json:"age"` + City string `json:"city"` +} +got := MyStruct + Name: "Bob", + Age: 42, + City: "TestCity", +} +Cmp(t, got, SuperJSONOf(`{"name": "Bob", "age": 42}`)) // succeeds +Cmp(t, got, 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]({{< ref "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`]({{< ref "Tag" >}}) operator as follows: + +```go +Cmp(t, gotValue, + SuperJSONOf(`{"fullname": $name, "age": $2, "gender": $3}`, + Tag("name", HasPrefix("Foo")), // matches $1 and $name + Between(41, 43), // matches only $2 + "male")) // matches only $3 +``` + +Note that placeholders can be double-quoted as in: + +```go +Cmp(t, gotValue, + SuperJSONOf(`{"fullname": "$name", "age": "$2", "gender": "$3"}`, + Tag("name", HasPrefix("Foo")), // matches $1 and $name + 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. + +Note *expectedJSON* can be a `[]byte`, JSON filename or [`io.Reader`](https://golang.org/pkg/io/#Reader): + +```go +Cmp(t, gotValue, SuperJSONOf("file.json", Between(12, 34))) +Cmp(t, gotValue, SuperJSONOf([]byte(`[1, $1, 3]`), Between(12, 34))) +Cmp(t, gotValue, SuperJSONOf(osFile, Between(12, 34))) +``` + +A JSON filename ends with ".json". + +To avoid a legit "$" `string` prefix causes a bad placeholder [`error`](https://golang.org/pkg/builtin/#error), +just double it to escape it. Note it is only needed when the "$" is +the first character of a `string`: + +```go +Cmp(t, gotValue, + SuperJSONOf(`{"fullname": "$name", "details": "$$info", "age": $2}`, + Tag("name", HasPrefix("Foo")), // matches $1 and $name + Between(41, 43))) // matches only $2 +``` + +For the "details" key, the raw value "`$info`" is expected, no +placeholders are involved here. + +Note that [`Lax`]({{< ref "Lax" >}}) mode is automatically enabled by [`SuperJSONOf`]({{< ref "SuperJSONOf" >}}) operator to +simplify numeric tests. + +Comments can be embedded in JSON data: + +```go +Cmp(t, gotValue, + 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 */ +}`, + Tag("name", HasPrefix("Foo")), // matches $1 and $name + 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 */. + + +Last but not least, simple operators can be directly embedded in +JSON data without requiring any placeholder but using directly +`$^OperatorName`. They are operator shortcuts: + +```go +Cmp(t, gotValue, SuperJSONOf(`{"id": $1}`, NotZero())) +``` + +can be written as: + +```go +Cmp(t, gotValue, SuperJSONOf(`{"id": $^NotZero}`)) +``` + +Unfortunately, only simple operators (in fact those which take no +parameters) have shortcuts. They follow: + +- [`Empty`]({{< ref "Empty" >}}) → `$^Empty` +- [`Ignore`]({{< ref "Ignore" >}}) → `$^Ignore` +- [`NaN`]({{< ref "NaN" >}}) → `$^NaN` +- [`Nil`]({{< ref "Nil" >}}) → `$^Nil` +- [`NotEmpty`]({{< ref "NotEmpty" >}}) → `$^NotEmpty` +- [`NotNaN`]({{< ref "NotNaN" >}}) → `$^NotNaN` +- [`NotNil`]({{< ref "NotNil" >}}) → `$^NotNil` +- [`NotZero`]({{< ref "NotZero" >}}) → `$^NotZero` +- [`Zero`]({{< ref "Zero" >}}) → `$^Zero` + + +[TypeBehind]({{< ref "operators#typebehind-method" >}}) method returns the `map[string]interface{}` type. + + +> See also [ SuperJSONOf godoc](https://godoc.org/github.com/maxatome/go-testdeep#SuperJSONOf). + +### Examples + +{{%expand "Basic example" %}}```go + 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 := Cmp(t, got, SuperJSONOf(`{"age":42,"fullname":"Bob","gender":"male"}`)) + fmt.Println("check got with age then fullname:", ok) + + ok = Cmp(t, got, SuperJSONOf(`{"fullname":"Bob","age":42,"gender":"male"}`)) + fmt.Println("check got with fullname then age:", ok) + + ok = Cmp(t, got, 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 = Cmp(t, got, + 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 + +```{{% /expand%}} +{{%expand "Placeholders example" %}}```go + 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 := Cmp(t, got, + SuperJSONOf(`{"age": $1, "fullname": $2, "gender": $3}`, + 42, "Bob Foobar", "male")) + fmt.Println("check got with numeric placeholders without operators:", ok) + + ok = Cmp(t, got, + SuperJSONOf(`{"age": $1, "fullname": $2, "gender": $3}`, + Between(40, 45), + HasSuffix("Foobar"), + NotEmpty())) + fmt.Println("check got with numeric placeholders:", ok) + + ok = Cmp(t, got, + SuperJSONOf(`{"age": "$1", "fullname": "$2", "gender": "$3"}`, + Between(40, 45), + HasSuffix("Foobar"), + NotEmpty())) + fmt.Println("check got with double-quoted numeric placeholders:", ok) + + ok = Cmp(t, got, + SuperJSONOf(`{"age": $age, "fullname": $name, "gender": $gender}`, + Tag("age", Between(40, 45)), + Tag("name", HasSuffix("Foobar")), + Tag("gender", NotEmpty()))) + fmt.Println("check got with named placeholders:", ok) + + ok = Cmp(t, got, + 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 + +```{{% /expand%}} +{{%expand "File example" %}}```go + 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 := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) // clean up + + filename := tmpDir + "/test.json" + if err = ioutil.WriteFile(filename, []byte(` +{ + "fullname": "$name", + "age": "$age", + "gender": "$gender" +}`), 0644); err != nil { + t.Fatal(err) + } + + // OK let's test with this file + ok := Cmp(t, got, + SuperJSONOf(filename, + Tag("name", HasPrefix("Bob")), + Tag("age", Between(40, 45)), + Tag("gender", 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 = Cmp(t, got, + SuperJSONOf(file, + Tag("name", HasPrefix("Bob")), + Tag("age", Between(40, 45)), + Tag("gender", 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 + +```{{% /expand%}} +## CmpSuperJSONOf shortcut + +```go +func CmpSuperJSONOf(t TestingT, got interface{}, expectedJSON interface{}, params []interface{}, args ...interface{}) bool +``` + +CmpSuperJSONOf is a shortcut for: + +```go +Cmp(t, got, SuperJSONOf(expectedJSON, params...), args...) +``` + +See above 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`](https://golang.org/pkg/fmt/#Fprintf) is used to compose the name, else *args* are passed to +[`fmt.Fprint`](https://golang.org/pkg/fmt/#Fprint). Do not forget it is the name of the test, not the +reason of a potential failure. + + +> See also [ CmpSuperJSONOf godoc](https://godoc.org/github.com/maxatome/go-testdeep#CmpSuperJSONOf). + +### Examples + +{{%expand "Basic example" %}}```go + 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 := CmpSuperJSONOf(t, got, `{"age":42,"fullname":"Bob","gender":"male"}`, nil) + fmt.Println("check got with age then fullname:", ok) + + ok = CmpSuperJSONOf(t, got, `{"fullname":"Bob","age":42,"gender":"male"}`, nil) + fmt.Println("check got with fullname then age:", ok) + + ok = 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 = 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 + +```{{% /expand%}} +{{%expand "Placeholders example" %}}```go + 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 := CmpSuperJSONOf(t, got, `{"age": $1, "fullname": $2, "gender": $3}`, []interface{}{42, "Bob Foobar", "male"}) + fmt.Println("check got with numeric placeholders without operators:", ok) + + ok = CmpSuperJSONOf(t, got, `{"age": $1, "fullname": $2, "gender": $3}`, []interface{}{Between(40, 45), HasSuffix("Foobar"), NotEmpty()}) + fmt.Println("check got with numeric placeholders:", ok) + + ok = CmpSuperJSONOf(t, got, `{"age": "$1", "fullname": "$2", "gender": "$3"}`, []interface{}{Between(40, 45), HasSuffix("Foobar"), NotEmpty()}) + fmt.Println("check got with double-quoted numeric placeholders:", ok) + + ok = CmpSuperJSONOf(t, got, `{"age": $age, "fullname": $name, "gender": $gender}`, []interface{}{Tag("age", Between(40, 45)), Tag("name", HasSuffix("Foobar")), Tag("gender", NotEmpty())}) + fmt.Println("check got with named placeholders:", ok) + + ok = 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 + +```{{% /expand%}} +{{%expand "File example" %}}```go + 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 := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) // clean up + + filename := tmpDir + "/test.json" + if err = ioutil.WriteFile(filename, []byte(` +{ + "fullname": "$name", + "age": "$age", + "gender": "$gender" +}`), 0644); err != nil { + t.Fatal(err) + } + + // OK let's test with this file + ok := CmpSuperJSONOf(t, got, filename, []interface{}{Tag("name", HasPrefix("Bob")), Tag("age", Between(40, 45)), Tag("gender", 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 = CmpSuperJSONOf(t, got, file, []interface{}{Tag("name", HasPrefix("Bob")), Tag("age", Between(40, 45)), Tag("gender", 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 + +```{{% /expand%}} +## T.SuperJSONOf shortcut + +```go +func (t *T) SuperJSONOf(got interface{}, expectedJSON interface{}, params []interface{}, args ...interface{}) bool +``` + +[`SuperJSONOf`]({{< ref "SuperJSONOf" >}}) is a shortcut for: + +```go +t.Cmp(got, SuperJSONOf(expectedJSON, params...), args...) +``` + +See above 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`](https://golang.org/pkg/fmt/#Fprintf) is used to compose the name, else *args* are passed to +[`fmt.Fprint`](https://golang.org/pkg/fmt/#Fprint). Do not forget it is the name of the test, not the +reason of a potential failure. + + +> See also [ T.SuperJSONOf godoc](https://godoc.org/github.com/maxatome/go-testdeep#T.SuperJSONOf). + +### Examples + +{{%expand "Basic example" %}}```go + t := 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 + +```{{% /expand%}} +{{%expand "Placeholders example" %}}```go + t := 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}`, []interface{}{42, "Bob Foobar", "male"}) + fmt.Println("check got with numeric placeholders without operators:", ok) + + ok = t.SuperJSONOf(got, `{"age": $1, "fullname": $2, "gender": $3}`, []interface{}{Between(40, 45), HasSuffix("Foobar"), NotEmpty()}) + fmt.Println("check got with numeric placeholders:", ok) + + ok = t.SuperJSONOf(got, `{"age": "$1", "fullname": "$2", "gender": "$3"}`, []interface{}{Between(40, 45), HasSuffix("Foobar"), NotEmpty()}) + fmt.Println("check got with double-quoted numeric placeholders:", ok) + + ok = t.SuperJSONOf(got, `{"age": $age, "fullname": $name, "gender": $gender}`, []interface{}{Tag("age", Between(40, 45)), Tag("name", HasSuffix("Foobar")), Tag("gender", 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 + +```{{% /expand%}} +{{%expand "File example" %}}```go + t := 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 := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) // clean up + + filename := tmpDir + "/test.json" + if err = ioutil.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, []interface{}{Tag("name", HasPrefix("Bob")), Tag("age", Between(40, 45)), Tag("gender", 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, []interface{}{Tag("name", HasPrefix("Bob")), Tag("age", Between(40, 45)), Tag("gender", 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 + +```{{% /expand%}} diff --git a/tools/docs_src/content/operators/_index.md b/tools/docs_src/content/operators/_index.md index 368d1abd..4ca173cb 100644 --- a/tools/docs_src/content/operators/_index.md +++ b/tools/docs_src/content/operators/_index.md @@ -148,6 +148,9 @@ weight = 15 [`SubBagOf`]({{< ref "SubBagOf" >}}) : compares the contents of an array or a slice without taking care of the order of items but with potentially some exclusions +[`SubJSONOf`]({{< ref "SubJSONOf" >}}) +: compares struct or map against JSON representation but with potentially some exclusions + [`SubMapOf`]({{< ref "SubMapOf" >}}) : compares the contents of a map but with potentially some exclusions @@ -157,6 +160,9 @@ weight = 15 [`SuperBagOf`]({{< ref "SuperBagOf" >}}) : compares the contents of an array or a slice without taking care of the order of items but with potentially some extra items +[`SuperJSONOf`]({{< ref "SuperJSONOf" >}}) +: compares struct or map against JSON representation but with potentially extra entries + [`SuperMapOf`]({{< ref "SuperMapOf" >}}) : compares the contents of a map but with potentially some extra entries @@ -282,9 +288,11 @@ The following operators are smuggler ones: [`String`]: {{< ref "String" >}} [`Struct`]: {{< ref "Struct" >}} [`SubBagOf`]: {{< ref "SubBagOf" >}} +[`SubJSONOf`]: {{< ref "SubJSONOf" >}} [`SubMapOf`]: {{< ref "SubMapOf" >}} [`SubSetOf`]: {{< ref "SubSetOf" >}} [`SuperBagOf`]: {{< ref "SuperBagOf" >}} +[`SuperJSONOf`]: {{< ref "SuperJSONOf" >}} [`SuperMapOf`]: {{< ref "SuperMapOf" >}} [`SuperSetOf`]: {{< ref "SuperSetOf" >}} [`Tag`]: {{< ref "Tag" >}} @@ -337,9 +345,11 @@ The following operators are smuggler ones: [`CmpString`]: {{< ref "String#cmpstring-shortcut" >}} [`CmpStruct`]: {{< ref "Struct#cmpstruct-shortcut" >}} [`CmpSubBagOf`]: {{< ref "SubBagOf#cmpsubbagof-shortcut" >}} +[`CmpSubJSONOf`]: {{< ref "SubJSONOf#cmpsubjsonof-shortcut" >}} [`CmpSubMapOf`]: {{< ref "SubMapOf#cmpsubmapof-shortcut" >}} [`CmpSubSetOf`]: {{< ref "SubSetOf#cmpsubsetof-shortcut" >}} [`CmpSuperBagOf`]: {{< ref "SuperBagOf#cmpsuperbagof-shortcut" >}} +[`CmpSuperJSONOf`]: {{< ref "SuperJSONOf#cmpsuperjsonof-shortcut" >}} [`CmpSuperMapOf`]: {{< ref "SuperMapOf#cmpsupermapof-shortcut" >}} [`CmpSuperSetOf`]: {{< ref "SuperSetOf#cmpsupersetof-shortcut" >}} [`CmpTruncTime`]: {{< ref "TruncTime#cmptrunctime-shortcut" >}} @@ -391,9 +401,11 @@ The following operators are smuggler ones: [`T.String`]: {{< ref "String#t-string-shortcut" >}} [`T.Struct`]: {{< ref "Struct#t-struct-shortcut" >}} [`T.SubBagOf`]: {{< ref "SubBagOf#t-subbagof-shortcut" >}} +[`T.SubJSONOf`]: {{< ref "SubJSONOf#t-subjsonof-shortcut" >}} [`T.SubMapOf`]: {{< ref "SubMapOf#t-submapof-shortcut" >}} [`T.SubSetOf`]: {{< ref "SubSetOf#t-subsetof-shortcut" >}} [`T.SuperBagOf`]: {{< ref "SuperBagOf#t-superbagof-shortcut" >}} +[`T.SuperJSONOf`]: {{< ref "SuperJSONOf#t-superjsonof-shortcut" >}} [`T.SuperMapOf`]: {{< ref "SuperMapOf#t-supermapof-shortcut" >}} [`T.SuperSetOf`]: {{< ref "SuperSetOf#t-supersetof-shortcut" >}} [`T.TruncTime`]: {{< ref "TruncTime#t-trunctime-shortcut" >}} diff --git a/tools/docs_src/content/operators/matrix.md b/tools/docs_src/content/operators/matrix.md index 6ca73c7b..87bd159d 100644 --- a/tools/docs_src/content/operators/matrix.md +++ b/tools/docs_src/content/operators/matrix.md @@ -68,12 +68,14 @@ weight: 1 | [`String`] | ✗ | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ + [`fmt.Stringer`]/[`error`] | ✗ | ✗ | [`String`] | | [`Struct`] | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ptr on struct | ✓ | ✗ | ✗ | [`Struct`] | | [`SubBagOf`] | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ | ptr on array/slice | ✓ | ✗ | ✗ | [`SubBagOf`] | +| [`SubJSONOf`] | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ | ptr on map/struct | ✓ | ✗ | ✗ | [`SubJSONOf`] | | [`SubMapOf`] | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✗ | ptr on map | ✓ | ✗ | ✗ | [`SubMapOf`] | | [`SubSetOf`] | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ | ptr on array/slice | ✓ | ✗ | ✗ | [`SubSetOf`] | -| [`SuperBagOf`] | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ | ptr on array/slice | ✓ | ✗ | ✗ | [`SuperBagOf`] | | Operator vs go type | nil | bool | string | {u,}int* | float* | complex* | array | slice | map | struct | pointer | interface¹ | chan | func | operator | | ------------------- | --- | ---- | ------ | -------- | ------ | -------- | ----- | ----- | --- | ------ | ------- | ---------- | ---- | ---- | -------- | +| [`SuperBagOf`] | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ | ptr on array/slice | ✓ | ✗ | ✗ | [`SuperBagOf`] | +| [`SuperJSONOf`] | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ | ptr on map/struct | ✓ | ✗ | ✗ | [`SuperJSONOf`] | | [`SuperMapOf`] | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✗ | ptr on map | ✓ | ✗ | ✗ | [`SuperMapOf`] | | [`SuperSetOf`] | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ | ptr on array/slice | ✓ | ✗ | ✗ | [`SuperSetOf`] | | [`Tag`] | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | [`Tag`] | @@ -140,9 +142,11 @@ weight: 1 [`String`]: {{< ref "String" >}} [`Struct`]: {{< ref "Struct" >}} [`SubBagOf`]: {{< ref "SubBagOf" >}} +[`SubJSONOf`]: {{< ref "SubJSONOf" >}} [`SubMapOf`]: {{< ref "SubMapOf" >}} [`SubSetOf`]: {{< ref "SubSetOf" >}} [`SuperBagOf`]: {{< ref "SuperBagOf" >}} +[`SuperJSONOf`]: {{< ref "SuperJSONOf" >}} [`SuperMapOf`]: {{< ref "SuperMapOf" >}} [`SuperSetOf`]: {{< ref "SuperSetOf" >}} [`Tag`]: {{< ref "Tag" >}} @@ -195,9 +199,11 @@ weight: 1 [`CmpString`]: {{< ref "String#cmpstring-shortcut" >}} [`CmpStruct`]: {{< ref "Struct#cmpstruct-shortcut" >}} [`CmpSubBagOf`]: {{< ref "SubBagOf#cmpsubbagof-shortcut" >}} +[`CmpSubJSONOf`]: {{< ref "SubJSONOf#cmpsubjsonof-shortcut" >}} [`CmpSubMapOf`]: {{< ref "SubMapOf#cmpsubmapof-shortcut" >}} [`CmpSubSetOf`]: {{< ref "SubSetOf#cmpsubsetof-shortcut" >}} [`CmpSuperBagOf`]: {{< ref "SuperBagOf#cmpsuperbagof-shortcut" >}} +[`CmpSuperJSONOf`]: {{< ref "SuperJSONOf#cmpsuperjsonof-shortcut" >}} [`CmpSuperMapOf`]: {{< ref "SuperMapOf#cmpsupermapof-shortcut" >}} [`CmpSuperSetOf`]: {{< ref "SuperSetOf#cmpsupersetof-shortcut" >}} [`CmpTruncTime`]: {{< ref "TruncTime#cmptrunctime-shortcut" >}} @@ -249,9 +255,11 @@ weight: 1 [`T.String`]: {{< ref "String#t-string-shortcut" >}} [`T.Struct`]: {{< ref "Struct#t-struct-shortcut" >}} [`T.SubBagOf`]: {{< ref "SubBagOf#t-subbagof-shortcut" >}} +[`T.SubJSONOf`]: {{< ref "SubJSONOf#t-subjsonof-shortcut" >}} [`T.SubMapOf`]: {{< ref "SubMapOf#t-submapof-shortcut" >}} [`T.SubSetOf`]: {{< ref "SubSetOf#t-subsetof-shortcut" >}} [`T.SuperBagOf`]: {{< ref "SuperBagOf#t-superbagof-shortcut" >}} +[`T.SuperJSONOf`]: {{< ref "SuperJSONOf#t-superjsonof-shortcut" >}} [`T.SuperMapOf`]: {{< ref "SuperMapOf#t-supermapof-shortcut" >}} [`T.SuperSetOf`]: {{< ref "SuperSetOf#t-supersetof-shortcut" >}} [`T.TruncTime`]: {{< ref "TruncTime#t-trunctime-shortcut" >}} @@ -513,7 +521,9 @@ Operators likely to succeed for each go type: - [`NotZero`] - [`Shallow`] - [`Smuggle`] +- [`SubJSONOf`] - [`SubMapOf`] +- [`SuperJSONOf`] - [`SuperMapOf`] - [`Tag`] - [`Values`] @@ -541,6 +551,8 @@ Operators likely to succeed for each go type: - [`NotZero`] - [`Smuggle`] - [`Struct`] +- [`SubJSONOf`] +- [`SuperJSONOf`] - [`Tag`] - [`TruncTime`] only [`time.Time`] - [`Zero`] @@ -591,9 +603,11 @@ listed below: - [`Smuggle`] - [`Struct`] only ptr on struct - [`SubBagOf`] only ptr on array/slice +- [`SubJSONOf`] only ptr on map/struct - [`SubMapOf`] only ptr on map - [`SubSetOf`] only ptr on array/slice - [`SuperBagOf`] only ptr on array/slice +- [`SuperJSONOf`] only ptr on map/struct - [`SuperMapOf`] only ptr on map - [`SuperSetOf`] only ptr on array/slice - [`Tag`] diff --git a/tools/gen_funcs.pl b/tools/gen_funcs.pl index 3742da90..b7d6f414 100755 --- a/tools/gen_funcs.pl +++ b/tools/gen_funcs.pl @@ -834,7 +834,7 @@ sub process_doc $doc =~ s< (\$\^[A-Za-z]+) # $1 | (\b(${\join('|', grep !/^JSON/, keys %operators)} - |JSON(?!\ (?:value|data|filename|representation|specification))) + |JSON(?!\s+(?:value|data|filename|object|representation|specification))) (?:\([^)]*\)|\b)) # $2 $3 | ((?:(?:\[\])+|\*+|\b)(?:bool\b |u?int(?:\*|(?:8|16|32|64)?\b) From dfbdc1028e5156e0b30b41e6eef37764653f6603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20Soul=C3=A9?= Date: Mon, 18 Nov 2019 23:22:34 +0100 Subject: [PATCH 6/7] Correctly gofmt in-comment examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime Soulé --- td_catch.go | 2 +- td_code.go | 40 +++--- td_contains_key.go | 12 +- td_json.go | 6 +- td_map.go | 8 +- td_re.go | 6 +- td_shallow.go | 4 +- td_smuggle.go | 123 ++++++++---------- tools/docs_src/content/operators/Catch.md | 2 +- tools/docs_src/content/operators/Code.md | 40 +++--- .../docs_src/content/operators/ContainsKey.md | 12 +- tools/docs_src/content/operators/Re.md | 4 +- tools/docs_src/content/operators/ReAll.md | 2 +- tools/docs_src/content/operators/Shallow.md | 4 +- tools/docs_src/content/operators/Smuggle.md | 123 ++++++++---------- tools/docs_src/content/operators/SubJSONOf.md | 4 +- tools/docs_src/content/operators/SubMapOf.md | 4 +- .../docs_src/content/operators/SuperJSONOf.md | 2 +- .../docs_src/content/operators/SuperMapOf.md | 4 +- tools/gen_funcs.pl | 52 +++++++- 20 files changed, 241 insertions(+), 213 deletions(-) diff --git a/td_catch.go b/td_catch.go index 2c60fda4..76773055 100644 --- a/td_catch.go +++ b/td_catch.go @@ -46,7 +46,7 @@ var _ TestDeep = &tdCatch{} // api.Handler, // tdhttp.Response{ // Status: http.StatusCreated, -// Body: testdeep.JSON(`{"id": $id, "name": "foo"}`, +// Body: testdeep.JSON(`{"id": $id, "name": "foo"}`, // testdeep.Tag("id", testdeep.Catch(&id, testdeep.Gt(0)))), // }) { // t.Logf("Created record ID is %d", id) diff --git a/td_code.go b/td_code.go index d0fc4319..5ce7e71a 100644 --- a/td_code.go +++ b/td_code.go @@ -31,35 +31,35 @@ var _ TestDeep = &tdCode{} // "fn" can return a single bool kind value, telling that yes or no // the custom test is successful: // Cmp(t, gotTime, -// Code(func (date time.Time) bool { -// return date.Year() == 2018 -// })) +// 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: // Cmp(t, gotTime, -// Code(func (date time.Time) (bool, string) { -// if date.Year() == 2018 { -// return true, "" -// } -// return false, "year must be 2018" -// })) +// 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: // Cmp(t, gotJsonRawMesg, -// 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 -// })) +// 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. diff --git a/td_contains_key.go b/td_contains_key.go index a3e9f3a8..597f2f65 100644 --- a/td_contains_key.go +++ b/td_contains_key.go @@ -30,11 +30,11 @@ var _ TestDeep = &tdContainsKey{} // hash := map[string]int{"foo": 12, "bar": 34, "zip": 28} // Cmp(t, hash, ContainsKey("foo")) // succeeds // Cmp(t, hash, ContainsKey(HasPrefix("z"))) // succeeds -// Cmp(t, hash, ContainsKey(HasPrefix("x")) // fails +// Cmp(t, hash, ContainsKey(HasPrefix("x"))) // fails // // hnum := map[int]string{1: "foo", 42: "bar"} -// Cmp(t, hash, ContainsKey(42)) // succeeds -// Cmp(t, hash, ContainsKey(Between(40, 45)) // succeeds +// Cmp(t, hash, ContainsKey(42)) // succeeds +// Cmp(t, hash, ContainsKey(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 @@ -43,9 +43,9 @@ var _ TestDeep = &tdContainsKey{} // // num := 123 // hnum := map[*int]bool{&num: true, nil: true} -// Cmp(t, hnum, ContainsKey(nil)) // succeeds → (*int)(nil) -// Cmp(t, hnum, ContainsKey((*int)(nil))) // succeeds -// Cmp(t, hnum, ContainsKey(Nil())) // succeeds +// Cmp(t, hnum, ContainsKey(nil)) // succeeds → (*int)(nil) +// Cmp(t, hnum, ContainsKey((*int)(nil))) // succeeds +// Cmp(t, hnum, ContainsKey(Nil())) // succeeds // // But... // Cmp(t, hnum, ContainsKey((*byte)(nil))) // fails: (*byte)(nil) ≠ (*int)(nil) func ContainsKey(expectedValue interface{}) TestDeep { diff --git a/td_json.go b/td_json.go index 5d2f179b..cf65af9c 100644 --- a/td_json.go +++ b/td_json.go @@ -503,12 +503,12 @@ var _ TestDeep = &tdMapJSON{} // Name string `json:"name"` // Age int `json:"age"` // } -// got := MyStruct +// got := MyStruct{ // Name: "Bob", // Age: 42, // } // Cmp(t, got, SubJSONOf(`{"name": "Bob", "age": 42, "city": "NY"}`)) // succeeds -// Cmp(t, got, SubJSONOf(`{"name": "Bob", "zip": 666}`)) // fails, extra "age" +// Cmp(t, got, 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 @@ -658,7 +658,7 @@ func SubJSONOf(expectedJSON interface{}, params ...interface{}) TestDeep { // Age int `json:"age"` // City string `json:"city"` // } -// got := MyStruct +// got := MyStruct{ // Name: "Bob", // Age: 42, // City: "TestCity", diff --git a/td_map.go b/td_map.go index 74118570..e2e214fe 100644 --- a/td_map.go +++ b/td_map.go @@ -189,10 +189,10 @@ func Map(model interface{}, expectedEntries MapEntries) TestDeep { // compared map. // // Cmp(t, map[string]int{"a": 1}, -// SubMapOf(map[string]int{"a": 1, "b": 2}, nil) // succeeds +// SubMapOf(map[string]int{"a": 1, "b": 2}, nil)) // succeeds // // Cmp(t, map[string]int{"a": 1, "c": 3}, -// SubMapOf(map[string]int{"a": 1, "b": 2}, nil) // fails, extra {"c": 3} +// SubMapOf(map[string]int{"a": 1, "b": 2}, nil)) // fails, extra {"c": 3} // // TypeBehind method returns the reflect.Type of "model". func SubMapOf(model interface{}, expectedEntries MapEntries) TestDeep { @@ -215,10 +215,10 @@ func SubMapOf(model interface{}, expectedEntries MapEntries) TestDeep { // map. But some entries in the compared map may not be expected. // // Cmp(t, map[string]int{"a": 1, "b": 2}, -// SuperMapOf(map[string]int{"a": 1}, nil) // succeeds +// SuperMapOf(map[string]int{"a": 1}, nil)) // succeeds // // Cmp(t, map[string]int{"a": 1, "c": 3}, -// SuperMapOf(map[string]int{"a": 1, "b": 2}, nil) // fails, missing {"b": 2} +// SuperMapOf(map[string]int{"a": 1, "b": 2}, nil)) // fails, missing {"b": 2} // // TypeBehind method returns the reflect.Type of "model". func SuperMapOf(model interface{}, expectedEntries MapEntries) TestDeep { diff --git a/td_re.go b/td_re.go index 6d5d7062..d6f7f8c1 100644 --- a/td_re.go +++ b/td_re.go @@ -68,11 +68,11 @@ func newRe(regIf interface{}, capture ...interface{}) (r *tdRe) { // depending the original matched data. Note that an other operator // can be used here. // -// Cmp(t, "foobar zip!", Re(`^foobar`)) // succeeds +// Cmp(t, "foobar zip!", Re(`^foobar`)) // succeeds // Cmp(t, "John Doe", // Re(`^(\w+) (\w+)`, []string{"John", "Doe"})) // succeeds // Cmp(t, "John Doe", -// Re(`^(\w+) (\w+)`, Bag("Doe", "John")) // succeeds +// Re(`^(\w+) (\w+)`, Bag("Doe", "John"))) // succeeds func Re(reg interface{}, capture ...interface{}) TestDeep { r := newRe(reg, capture...) r.numMatches = 1 @@ -99,7 +99,7 @@ func Re(reg interface{}, capture ...interface{}) TestDeep { // Cmp(t, "John Doe", // ReAll(`(\w+)(?: |\z)`, []string{"John", "Doe"})) // succeeds // Cmp(t, "John Doe", -// ReAll(`(\w+)(?: |\z)`, Bag("Doe", "John")) // succeeds +// ReAll(`(\w+)(?: |\z)`, Bag("Doe", "John"))) // succeeds func ReAll(reg interface{}, capture interface{}) TestDeep { r := newRe(reg, capture) r.numMatches = -1 diff --git a/td_shallow.go b/td_shallow.go index be2dc9a8..c6845970 100644 --- a/td_shallow.go +++ b/td_shallow.go @@ -52,13 +52,13 @@ func stringPointer(s string) uintptr { // lengths. For example: // // a := "foobar yes!" -// b := a[:1] // aka. "f" +// b := a[:1] // aka. "f" // Cmp(t, &a, 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} +// b := a[:2] // aka. []int{1, 2} // Cmp(t, &a, Shallow(&b)) // succeeds as both slices point to the same area, even if len() differ func Shallow(expectedPtr interface{}) TestDeep { vptr := reflect.ValueOf(expectedPtr) diff --git a/td_smuggle.go b/td_smuggle.go index 2c73ce4d..88cd5391 100644 --- a/td_smuggle.go +++ b/td_smuggle.go @@ -133,50 +133,45 @@ func buildStructFieldFn(path string) (func(interface{}) (smuggleValue, error), e // "fn" must return at least one value. These value will be compared as is // to "expectedValue", here integer 28: // -// Smuggle(func (value string) int { -// num, _ := strconv.Atoi(value) -// return num -// }, -// 28) +// Smuggle(func(value string) int { +// num, _ := strconv.Atoi(value) +// return num +// }, 28) // // or using an other TestDeep operator, here Between(28, 30): // -// Smuggle(func (value string) int { -// num, _ := strconv.Atoi(value) -// return num -// }, -// Between(28, 30)) +// Smuggle(func(value string) int { +// num, _ := strconv.Atoi(value) +// return num +// }, Between(28, 30)) // // "fn" can return a second boolean value, used to tell that a problem // occurred and so stop the comparison: // -// Smuggle(func (value string) (int, bool) { -// num, err := strconv.Atoi(value) -// return num, err == nil -// }, -// Between(28, 30)) +// Smuggle(func(value string) (int, bool) { +// num, err := strconv.Atoi(value) +// return num, err == nil +// }, 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): // -// 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, "" -// }, -// Between(28, 30)) +// 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, "" +// }, 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: // -// Smuggle(func (value string) (int, error) { -// num, err := strconv.Atoi(value) -// return num, err -// }, -// Between(28, 30)) +// Smuggle(func(value string) (int, error) { +// num, err := strconv.Atoi(value) +// return num, err +// }, Between(28, 30)) // // Which can be simplified to: // @@ -185,22 +180,19 @@ func buildStructFieldFn(path string) (func(interface{}) (smuggleValue, error), e // Imagine you want to compare that the Year of a date is between 2010 // and 2020: // -// Smuggle(func (date time.Time) int { -// return date.Year() -// }, +// Smuggle(func(date time.Time) int { return date.Year() }, // 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): // -// Smuggle(func (date time.Time) SmuggledGot { -// return SmuggledGot{ -// Name: "Year", -// Got: date.Year(), -// } -// }, -// Between(2010, 2020)) +// Smuggle(func(date time.Time) SmuggledGot { +// return SmuggledGot{ +// Name: "Year", +// Got: date.Year(), +// } +// }, Between(2010, 2020)) // // then the data location forwarded to next test will be something like // "DATA.MyTimeField.Year". The "." between the current path (here @@ -215,48 +207,45 @@ func buildStructFieldFn(path string) (func(interface{}) (smuggleValue, error), e // // // 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. -// 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, "" -// }, -// Between(time.Now().Add(-2*time.Hour), time.Now())) +// 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, "" +// }, 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. -// 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 -// }, -// Between(time.Now().Add(-2*time.Hour), time.Now())) +// 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 +// }, 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 { A *A } -// type C struct { B B } +// type A struct{ Num int } +// type B struct{ A *A } +// type C struct{ B B } // got := C{B: B{A: &A{Num: 12}}} // // // Tests that got.B.A.Num is 12 // Cmp(t, got, -// Smuggle(func (c C) int { -// return c.B.A.Num -// }, -// 12)) +// Smuggle(func(c C) int { +// return c.B.A.Num +// }, 12)) // // As brought up above, a field-path can be passed as "fn" value // instead of a function pointer. Using this feature, the Cmp diff --git a/tools/docs_src/content/operators/Catch.md b/tools/docs_src/content/operators/Catch.md index dd4da7e8..54e8f979 100644 --- a/tools/docs_src/content/operators/Catch.md +++ b/tools/docs_src/content/operators/Catch.md @@ -32,7 +32,7 @@ if tdhttp.CmpJSONResponse(t, api.Handler, tdhttp.Response{ Status: http.StatusCreated, - Body: testdeep.JSON(`{"id": $id, "name": "foo"}`, + Body: testdeep.JSON(`{"id": $id, "name": "foo"}`, testdeep.Tag("id", testdeep.Catch(&id, testdeep.Gt(0)))), }) { t.Logf("Created record ID is %d", id) diff --git a/tools/docs_src/content/operators/Code.md b/tools/docs_src/content/operators/Code.md index 51ffdbb7..14812acf 100644 --- a/tools/docs_src/content/operators/Code.md +++ b/tools/docs_src/content/operators/Code.md @@ -15,9 +15,9 @@ the same as the type of the compared value. the custom test is successful: ```go Cmp(t, gotTime, - Code(func (date time.Time) bool { - return date.Year() == 2018 - })) + Code(func(date time.Time) bool { + return date.Year() == 2018 + })) ``` or two values (`bool`, `string`) kinds. The `bool` value has the same @@ -25,29 +25,29 @@ meaning as above, and the `string` value is used to describe the test when it fails: ```go Cmp(t, gotTime, - Code(func (date time.Time) (bool, string) { - if date.Year() == 2018 { - return true, "" - } - return false, "year must be 2018" - })) + Code(func(date time.Time) (bool, string) { + if date.Year() == 2018 { + return true, "" + } + return false, "year must be 2018" + })) ``` or a single [`error`](https://golang.org/pkg/builtin/#error) value. If the returned [`error`](https://golang.org/pkg/builtin/#error) is `nil`, the test succeeded, else the [`error`](https://golang.org/pkg/builtin/#error) contains the reason of failure: ```go Cmp(t, gotJsonRawMesg, - 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 - })) + 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 diff --git a/tools/docs_src/content/operators/ContainsKey.md b/tools/docs_src/content/operators/ContainsKey.md index 5a40d659..38441ff0 100644 --- a/tools/docs_src/content/operators/ContainsKey.md +++ b/tools/docs_src/content/operators/ContainsKey.md @@ -14,11 +14,11 @@ compares each key of map against *expectedValue*. hash := map[string]int{"foo": 12, "bar": 34, "zip": 28} Cmp(t, hash, ContainsKey("foo")) // succeeds Cmp(t, hash, ContainsKey(HasPrefix("z"))) // succeeds -Cmp(t, hash, ContainsKey(HasPrefix("x")) // fails +Cmp(t, hash, ContainsKey(HasPrefix("x"))) // fails hnum := map[int]string{1: "foo", 42: "bar"} -Cmp(t, hash, ContainsKey(42)) // succeeds -Cmp(t, hash, ContainsKey(Between(40, 45)) // succeeds +Cmp(t, hash, ContainsKey(42)) // succeeds +Cmp(t, hash, ContainsKey(Between(40, 45))) // succeeds ``` When [`ContainsKey(nil)`]({{< ref "ContainsKey" >}}) is used, `nil` is automatically converted to a @@ -29,9 +29,9 @@ it of course.) So all following Cmp calls are equivalent ```go num := 123 hnum := map[*int]bool{&num: true, nil: true} -Cmp(t, hnum, ContainsKey(nil)) // succeeds → (*int)(nil) -Cmp(t, hnum, ContainsKey((*int)(nil))) // succeeds -Cmp(t, hnum, ContainsKey(Nil())) // succeeds +Cmp(t, hnum, ContainsKey(nil)) // succeeds → (*int)(nil) +Cmp(t, hnum, ContainsKey((*int)(nil))) // succeeds +Cmp(t, hnum, ContainsKey(Nil())) // succeeds // But... Cmp(t, hnum, ContainsKey((*byte)(nil))) // fails: (*byte)(nil) ≠ (*int)(nil) ``` diff --git a/tools/docs_src/content/operators/Re.md b/tools/docs_src/content/operators/Re.md index 93062e6c..0e4dcbca 100644 --- a/tools/docs_src/content/operators/Re.md +++ b/tools/docs_src/content/operators/Re.md @@ -20,11 +20,11 @@ depending the original matched data. Note that an other operator can be used here. ```go -Cmp(t, "foobar zip!", Re(`^foobar`)) // succeeds +Cmp(t, "foobar zip!", Re(`^foobar`)) // succeeds Cmp(t, "John Doe", Re(`^(\w+) (\w+)`, []string{"John", "Doe"})) // succeeds Cmp(t, "John Doe", - Re(`^(\w+) (\w+)`, Bag("Doe", "John")) // succeeds + Re(`^(\w+) (\w+)`, Bag("Doe", "John"))) // succeeds ``` diff --git a/tools/docs_src/content/operators/ReAll.md b/tools/docs_src/content/operators/ReAll.md index e2a0dbf0..b085baf2 100644 --- a/tools/docs_src/content/operators/ReAll.md +++ b/tools/docs_src/content/operators/ReAll.md @@ -23,7 +23,7 @@ matched data. Note that an other operator can be used here. Cmp(t, "John Doe", ReAll(`(\w+)(?: |\z)`, []string{"John", "Doe"})) // succeeds Cmp(t, "John Doe", - ReAll(`(\w+)(?: |\z)`, Bag("Doe", "John")) // succeeds + ReAll(`(\w+)(?: |\z)`, Bag("Doe", "John"))) // succeeds ``` diff --git a/tools/docs_src/content/operators/Shallow.md b/tools/docs_src/content/operators/Shallow.md index 819b09ad..2f00ecf9 100644 --- a/tools/docs_src/content/operators/Shallow.md +++ b/tools/docs_src/content/operators/Shallow.md @@ -31,7 +31,7 @@ lengths. For example: ```go a := "foobar yes!" -b := a[:1] // aka. "f" +b := a[:1] // aka. "f" Cmp(t, &a, Shallow(&b)) // succeeds as both strings point to the same area, even if len() differ ``` @@ -39,7 +39,7 @@ The same behavior occurs for slices: ```go a := []int{1, 2, 3, 4, 5, 6} -b := a[:2] // aka. []int{1, 2} +b := a[:2] // aka. []int{1, 2} Cmp(t, &a, Shallow(&b)) // succeeds as both slices point to the same area, even if len() differ ``` diff --git a/tools/docs_src/content/operators/Smuggle.md b/tools/docs_src/content/operators/Smuggle.md index 3e82502b..3f4da871 100644 --- a/tools/docs_src/content/operators/Smuggle.md +++ b/tools/docs_src/content/operators/Smuggle.md @@ -19,46 +19,42 @@ details). to *expectedValue*, here integer 28: ```go -Smuggle(func (value string) int { - num, _ := strconv.Atoi(value) - return num - }, - 28) +Smuggle(func(value string) int { + num, _ := strconv.Atoi(value) + return num +}, 28) ``` or using an other [TestDeep operator]({{< ref "operators" >}}), here [`Between(28, 30)`]({{< ref "Between" >}}): ```go -Smuggle(func (value string) int { - num, _ := strconv.Atoi(value) - return num - }, - Between(28, 30)) +Smuggle(func(value string) int { + num, _ := strconv.Atoi(value) + return num +}, Between(28, 30)) ``` *fn* can return a second boolean value, used to tell that a problem occurred and so stop the comparison: ```go -Smuggle(func (value string) (int, bool) { - num, err := strconv.Atoi(value) - return num, err == nil - }, - Between(28, 30)) +Smuggle(func(value string) (int, bool) { + num, err := strconv.Atoi(value) + return num, err == nil +}, 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): ```go -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, "" - }, - Between(28, 30)) +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, "" +}, Between(28, 30)) ``` Instead of returning (X, `bool`) or (X, `bool`, `string`), *fn* can @@ -66,11 +62,10 @@ return (X, [`error`](https://golang.org/pkg/builtin/#error)). When a problem occ non-`nil`, as in: ```go -Smuggle(func (value string) (int, error) { - num, err := strconv.Atoi(value) - return num, err - }, - Between(28, 30)) +Smuggle(func(value string) (int, error) { + num, err := strconv.Atoi(value) + return num, err +}, Between(28, 30)) ``` Which can be simplified to: @@ -83,9 +78,7 @@ Imagine you want to compare that the Year of a date is between 2010 and 2020: ```go -Smuggle(func (date time.Time) int { - return date.Year() - }, +Smuggle(func(date time.Time) int { return date.Year() }, Between(2010, 2020)) ``` @@ -94,13 +87,12 @@ something like "DATA.MyTimeField", but you can act on it too by returning a [SmuggledGot](https://godoc.org/github.com/maxatome/go-testdeep#SmuggledGot) struct (by value or by address): ```go -Smuggle(func (date time.Time) SmuggledGot { - return SmuggledGot{ - Name: "Year", - Got: date.Year(), - } - }, - Between(2010, 2020)) +Smuggle(func(date time.Time) SmuggledGot { + return SmuggledGot{ + Name: "Year", + Got: date.Year(), + } +}, Between(2010, 2020)) ``` then the data location forwarded to next test will be something like @@ -117,17 +109,16 @@ Of course, all cases can go together: ```go // 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. -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, "" - }, - Between(time.Now().Add(-2*time.Hour), time.Now())) +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, "" +}, Between(time.Now().Add(-2*time.Hour), time.Now())) ``` or: @@ -135,34 +126,32 @@ or: ```go // 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. -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 - }, - Between(time.Now().Add(-2*time.Hour), time.Now())) +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 +}, Between(time.Now().Add(-2*time.Hour), time.Now())) ``` [`Smuggle`]({{< ref "Smuggle" >}}) can also be used to access a struct field embedded in several struct layers. ```go -type A struct { Num int } -type B struct { A *A } -type C struct { B B } +type A struct{ Num int } +type B struct{ A *A } +type C struct{ B B } got := C{B: B{A: &A{Num: 12}}} // Tests that got.B.A.Num is 12 Cmp(t, got, - Smuggle(func (c C) int { - return c.B.A.Num - }, - 12)) + Smuggle(func(c C) int { + return c.B.A.Num + }, 12)) ``` As brought up above, a field-path can be passed as *fn* value diff --git a/tools/docs_src/content/operators/SubJSONOf.md b/tools/docs_src/content/operators/SubJSONOf.md index 964af6c6..07818e36 100644 --- a/tools/docs_src/content/operators/SubJSONOf.md +++ b/tools/docs_src/content/operators/SubJSONOf.md @@ -29,12 +29,12 @@ type MyStruct struct { Name string `json:"name"` Age int `json:"age"` } -got := MyStruct +got := MyStruct{ Name: "Bob", Age: 42, } Cmp(t, got, SubJSONOf(`{"name": "Bob", "age": 42, "city": "NY"}`)) // succeeds -Cmp(t, got, SubJSONOf(`{"name": "Bob", "zip": 666}`)) // fails, extra "age" +Cmp(t, got, SubJSONOf(`{"name": "Bob", "zip": 666}`)) // fails, extra "age" ``` *expectedJSON* JSON value can contain placeholders. The *params* diff --git a/tools/docs_src/content/operators/SubMapOf.md b/tools/docs_src/content/operators/SubMapOf.md index b15021d8..16a92227 100644 --- a/tools/docs_src/content/operators/SubMapOf.md +++ b/tools/docs_src/content/operators/SubMapOf.md @@ -21,10 +21,10 @@ compared map. ```go Cmp(t, map[string]int{"a": 1}, - SubMapOf(map[string]int{"a": 1, "b": 2}, nil) // succeeds + SubMapOf(map[string]int{"a": 1, "b": 2}, nil)) // succeeds Cmp(t, map[string]int{"a": 1, "c": 3}, - SubMapOf(map[string]int{"a": 1, "b": 2}, nil) // fails, extra {"c": 3} + SubMapOf(map[string]int{"a": 1, "b": 2}, nil)) // fails, extra {"c": 3} ``` [TypeBehind]({{< ref "operators#typebehind-method" >}}) method returns the [`reflect.Type`](https://golang.org/pkg/reflect/#Type) of *model*. diff --git a/tools/docs_src/content/operators/SuperJSONOf.md b/tools/docs_src/content/operators/SuperJSONOf.md index eefdbb42..5291b62f 100644 --- a/tools/docs_src/content/operators/SuperJSONOf.md +++ b/tools/docs_src/content/operators/SuperJSONOf.md @@ -30,7 +30,7 @@ type MyStruct struct { Age int `json:"age"` City string `json:"city"` } -got := MyStruct +got := MyStruct{ Name: "Bob", Age: 42, City: "TestCity", diff --git a/tools/docs_src/content/operators/SuperMapOf.md b/tools/docs_src/content/operators/SuperMapOf.md index b9bbcdee..8f22534d 100644 --- a/tools/docs_src/content/operators/SuperMapOf.md +++ b/tools/docs_src/content/operators/SuperMapOf.md @@ -20,10 +20,10 @@ map. But some entries in the compared map may not be expected. ```go Cmp(t, map[string]int{"a": 1, "b": 2}, - SuperMapOf(map[string]int{"a": 1}, nil) // succeeds + SuperMapOf(map[string]int{"a": 1}, nil)) // succeeds Cmp(t, map[string]int{"a": 1, "c": 3}, - SuperMapOf(map[string]int{"a": 1, "b": 2}, nil) // fails, missing {"b": 2} + SuperMapOf(map[string]int{"a": 1, "b": 2}, nil)) // fails, missing {"b": 2} ``` [TypeBehind]({{< ref "operators#typebehind-method" >}}) method returns the [`reflect.Type`](https://golang.org/pkg/reflect/#Type) of *model*. diff --git a/tools/gen_funcs.pl b/tools/gen_funcs.pl index b7d6f414..133289b2 100755 --- a/tools/gen_funcs.pl +++ b/tools/gen_funcs.pl @@ -11,6 +11,8 @@ use autodie; use 5.010; +use IPC::Open2; + die "usage $0 [-h] DIR" if @ARGV == 0 or $ARGV[0] =~ /^--?h/; my $HEADER = <<'EOH'; @@ -145,6 +147,7 @@ die "TAB detected in $func operator documentation" if $doc =~ /\t/; $operators{$func} = { + name => $func, summary => delete $ops{$func}, input => delete $inputs{$func}, doc => $doc, @@ -915,5 +918,52 @@ sub process_doc @{$op->{args}})})"/*$1*/g; } - return $doc =~ s/^CODE<(\d+)>/$codes[$1]/gmr; + 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'); + + print $fmt_in <{name}.go:1 +func x() { +$code +} +EOM + close $fmt_in; + + (my $new_code = do { local $/; <$fmt_out> }) =~ s/[^\t]+//; + $new_code =~ s/\n\}\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"; } From 5eaad3bfc92cd5ce3cbca5e315348dd7af6ceacc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20Soul=C3=A9?= Date: Mon, 18 Nov 2019 23:26:13 +0100 Subject: [PATCH 7/7] README announce of SubJSONOf & SuperJSONOf new operators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime Soulé --- README.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index aa05c49f..05c6cd62 100644 --- a/README.md +++ b/README.md @@ -27,19 +27,16 @@ go-testdeep ## Latest news +- 2019/11/18: + - new [`SubJSONOf`] & [`SuperJSONOf`] operators (and their + friends [`CmpSubJSONOf`], [`CmpSuperJSONOf`], [`T.SubJSONOf`] & + [`T.SuperJSONOf`]), + - JSON data can now contain comments and some operator shortcuts; - 2019/11/01: new [`Catch`] operator; - 2019/10/31: new [`JSON`] operator (and its friends [`CmpJSON`] & [`T.JSON`] along with new fully dedicated [`Tag`] operator; - 2019/10/29: new web site [go-testdeep.zetta.rocks](https://go-testdeep.zetta.rocks/) -- 2019/09/22: new - [`BeLax` feature](https://godoc.org/github.com/maxatome/go-testdeep#T.BeLax) - with its - [`Lax`](https://godoc.org/github.com/maxatome/go-testdeep#Lax) - operator counterpart (and its friends - [`CmpLax`](https://godoc.org/github.com/maxatome/go-testdeep#CmpLax) - & - [`T.CmpLax`](https://godoc.org/github.com/maxatome/go-testdeep#T.CmpLax)); - see [commits history](https://github.com/maxatome/go-testdeep/commits/master) for other/older changes.