diff --git a/checker/response_body/response_body.go b/checker/response_body/response_body.go index d150e95..0cef94d 100644 --- a/checker/response_body/response_body.go +++ b/checker/response_body/response_body.go @@ -24,7 +24,7 @@ func createWrongStatusError(statusCode int, known map[int]string) error { for code := range known { knownCodes = append(knownCodes, fmt.Sprintf("%d", code)) } - return colorize.NewNotEqualError("server responded with unexpected ", "status", ":", strings.Join(knownCodes, " / "), statusCode, nil) + return colorize.NewNotEqualError("server responded with unexpected %s:", "status", strings.Join(knownCodes, " / "), statusCode) } func (c *responseBodyChecker) Check(t models.TestInterface, result *models.Result) ([]error, error) { diff --git a/checker/response_db/response_db.go b/checker/response_db/response_db.go index 7ff50c2..6ca0b73 100644 --- a/checker/response_db/response_db.go +++ b/checker/response_db/response_db.go @@ -122,7 +122,7 @@ func compareDbResponseLength(expected, actual []string, query interface{}) error diffCfg.Diffable = true chunks := diff.DiffChunks(strings.Split(diffCfg.Sprint(expected), "\n"), strings.Split(diffCfg.Sprint(actual), "\n")) - tail := []*colorize.Part{ + tail := []colorize.Part{ colorize.None("\n\n query: "), colorize.Cyan(query), colorize.None("\n diff (--- expected vs +++ actual):\n"), @@ -130,22 +130,17 @@ func compareDbResponseLength(expected, actual []string, query interface{}) error tail = append(tail, colorize.MakeColorDiff(chunks)...) return colorize.NewNotEqualError( - "quantity of ", + "quantity of %s do not match:", "items in database", - " do not match:", len(expected), len(actual), - tail, - ) + ).AddParts(tail...) return colorize.NewError( - colorize.None("quantity of items in database do not match (-expected: "), + "quantity of items in database do not match (-expected: %s +actual: %s)\n test query:\n%s\n result diff:\n%s", colorize.Cyan(len(expected)), - colorize.None(" +actual: "), colorize.Cyan(len(actual)), - colorize.None(")\n test query:\n"), colorize.Cyan(query), - colorize.None("\n result diff:\n"), colorize.Cyan(pretty.Compare(expected, actual)), ) } diff --git a/checker/response_header/response_header.go b/checker/response_header/response_header.go index 26b97d5..ece84fe 100644 --- a/checker/response_header/response_header.go +++ b/checker/response_header/response_header.go @@ -27,7 +27,7 @@ func (c *responseHeaderChecker) Check(t models.TestInterface, result *models.Res actualValues, ok := result.ResponseHeaders[k] if !ok { errs = append(errs, colorize.NewError( - colorize.None("response does not include expected header "), + "response does not include expected header %s", colorize.Cyan(k), )) continue @@ -44,16 +44,15 @@ func (c *responseHeaderChecker) Check(t models.TestInterface, result *models.Res } if len(actualValues) == 1 { errs = append(errs, colorize.NewNotEqualError( - "response header ", k, " value does not match:", + "response header %s value does not match:", + k, v, actualValues[0], - nil, )) } else { errs = append(errs, colorize.NewError( - colorize.None("response header "), + "response header %s value does not match expected %s", colorize.Cyan(k), - colorize.None(" value does not match expected "), colorize.Green(v), )) } diff --git a/checker/response_header/response_header_test.go b/checker/response_header/response_header_test.go index c8d37de..f189286 100644 --- a/checker/response_header/response_header_test.go +++ b/checker/response_header/response_header_test.go @@ -73,8 +73,8 @@ func TestCheckWhenNotMatchedShouldReturnError(t *testing.T) { t, errText, []string{ - "response does not include expected header Content-Type", - "response header Accept value does not match:\n expected: text/html\n actual: application/json", + "response does not include expected header 'Content-Type'", + "response header 'Accept' value does not match:\n expected: text/html\n actual: application/json", }, ) } diff --git a/colorize/diff.go b/colorize/diff.go index fc6a3b4..073f8d1 100644 --- a/colorize/diff.go +++ b/colorize/diff.go @@ -2,13 +2,12 @@ package colorize import ( "fmt" - "strings" "github.com/kylelemons/godebug/diff" ) -func MakeColorDiff(chunks []diff.Chunk) []*Part { - parts := []*Part{} +func MakeColorDiff(chunks []diff.Chunk) []Part { + parts := []Part{} for _, c := range chunks { for _, line := range c.Added { parts = append(parts, Red(fmt.Sprintf("+%s\n", line))) @@ -20,8 +19,5 @@ func MakeColorDiff(chunks []diff.Chunk) []*Part { parts = append(parts, None(fmt.Sprintf(" %s\n", line))) } } - if len(parts) != 0 { - parts[len(parts)-1].value = strings.TrimRight(parts[len(parts)-1].value, "\n") - } return parts } diff --git a/colorize/error.go b/colorize/error.go index fd92ea6..d867bd6 100644 --- a/colorize/error.go +++ b/colorize/error.go @@ -1,50 +1,51 @@ package colorize import ( + "errors" "fmt" "strings" "github.com/fatih/color" ) -type Color int +func Red(v interface{}) Part { + return &partImpl{color.HiRedString, fmt.Sprintf("%v", v), false} +} -const ( - ColorRed Color = iota - ColorCyan - ColorGreen - ColorNone -) +func Cyan(v interface{}) Part { + return &partImpl{color.HiCyanString, fmt.Sprintf("%v", v), true} +} -type Part struct { - attr Color - value string +func Green(v interface{}) Part { + return &partImpl{color.HiGreenString, fmt.Sprintf("%v", v), false} } -func Red(v interface{}) *Part { - return &Part{ColorRed, fmt.Sprintf("%v", v)} +func None(v interface{}) Part { + return &partImpl{asIsString, fmt.Sprintf("%v", v), false} } -func Cyan(v interface{}) *Part { - return &Part{ColorCyan, fmt.Sprintf("%v", v)} +func SubError(err error) Part { + return &subErrorImpl{err} } -func Green(v interface{}) *Part { - return &Part{ColorGreen, fmt.Sprintf("%v", v)} +type Error struct { + parts []Part } -func None(v interface{}) *Part { - return &Part{ColorNone, fmt.Sprintf("%v", v)} +func (e *Error) AddParts(values ...Part) *Error { + e.parts = append(e.parts, values...) + return e } -type Error struct { - parts []*Part +func (e *Error) SetSubError(err error) *Error { + e.AddParts(SubError(err)) + return e } func (e *Error) Error() string { buf := strings.Builder{} for _, p := range e.parts { - buf.WriteString(p.value) + buf.WriteString(p.Text()) } return buf.String() } @@ -56,32 +57,56 @@ func asIsString(format string, a ...interface{}) string { func (e *Error) ColorError() string { buf := strings.Builder{} for _, p := range e.parts { - if p.value == "" { - continue + buf.WriteString(p.ColorText()) + } + return buf.String() +} + +func alternateJoin(list1, list2 []Part) []Part { + result := []Part{} + i, j := 0, 0 + for len(result) != len(list1)+len(list2) { + if i < len(list1) { + result = append(result, list1[i]) + i++ } - f := asIsString - switch p.attr { - case ColorRed: - f = color.RedString - case ColorCyan: - f = color.CyanString - case ColorGreen: - f = color.GreenString + if j < len(list2) { + result = append(result, list2[j]) + j++ } + } + + return result +} - buf.WriteString(f(p.value)) +func GetColoredValue(err error) string { + var pErr *Error + if errors.As(err, &pErr) { + return pErr.ColorError() } - return buf.String() + return err.Error() +} + +func NewError(format string, values ...Part) *Error { + plain := []Part{} + for _, s := range strings.Split(format, "%s") { + plain = append(plain, None(s)) + } + return &Error{alternateJoin(plain, values)} +} + +func NewEntityError(pattern, entity string) *Error { + return NewError(pattern, Cyan(entity)) } -func NewError(parts ...*Part) error { - return &Error{parts} +func NewNotEqualError(pattern, entity string, expected, actual interface{}) *Error { + pattern += "\n expected: %s\n actual: %s" + return NewError(pattern, Cyan(entity), Green(expected), Red(actual)) } -func NewNotEqualError(before, entity, after string, expected, actual interface{}, tail []*Part) error { - parts := []*Part{ +func NewNotEqualError2(before, entity, after string, expected, actual interface{}) *Error { + parts := []Part{ None(before), Cyan(entity), None(after + "\n expected: "), Green(expected), None("\n actual: "), Red(actual), } - parts = append(parts, tail...) - return NewError(parts...) + return NewError("", parts...) } diff --git a/colorize/part.go b/colorize/part.go new file mode 100644 index 0000000..e1a7199 --- /dev/null +++ b/colorize/part.go @@ -0,0 +1,35 @@ +package colorize + +type Part interface { + Text() string + ColorText() string +} + +type partImpl struct { + colorer func(format string, a ...interface{}) string + value string + entity bool +} + +func (p *partImpl) Text() string { + if p.entity { + return "'" + p.value + "'" + } + return p.value +} + +func (p *partImpl) ColorText() string { + return p.colorer(p.value) +} + +type subErrorImpl struct { + err error +} + +func (p *subErrorImpl) Text() string { + return ": " + p.err.Error() +} + +func (p *subErrorImpl) ColorText() string { + return ": " + GetColoredValue(p.err) +} diff --git a/compare/compare.go b/compare/compare.go index ca1e2f9..0e4a7d4 100644 --- a/compare/compare.go +++ b/compare/compare.go @@ -219,7 +219,7 @@ func leafMatchType(expected interface{}) leafsMatchType { return pure } -func diffStrings(a, b string) []*colorize.Part { +func diffStrings(a, b string) []colorize.Part { chunks := diff.DiffChunks(strings.Split(a, "\n"), strings.Split(b, "\n")) return colorize.MakeColorDiff(chunks) } @@ -232,18 +232,16 @@ func makeValueCompareError(path, msg string, expected, actual interface{}) error } // special case for multi-line strings - parts := []*colorize.Part{ - colorize.None("at path "), + parts := []colorize.Part{ colorize.Cyan(path), - colorize.None(" " + msg + ":\n diff (--- expected vs +++ actual):\n"), } parts = append(parts, diffStrings(actualStr, expectedStr)...) - return colorize.NewError(parts...) + return colorize.NewError("at path %s "+msg+":\n diff (--- expected vs +++ actual):\n", parts...) } func makeError(path, msg string, expected, actual interface{}) error { - return colorize.NewNotEqualError("at path ", path, " "+msg+":", expected, actual, nil) + return colorize.NewNotEqualError("at path %s "+msg+":", path, expected, actual) } func convertToArray(array interface{}) []interface{} { diff --git a/compare/compare_test.go b/compare/compare_test.go index f0109fd..9754382 100644 --- a/compare/compare_test.go +++ b/compare/compare_test.go @@ -10,7 +10,7 @@ import ( func makeErrorString(path, msg string, expected, actual interface{}) string { return fmt.Sprintf( - "at path %s %s:\n expected: %v\n actual: %v", + "at path '%s' %s:\n expected: %v\n actual: %v", path, msg, expected, diff --git a/go.mod b/go.mod index e82beaf..30e9ace 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,6 @@ require ( github.com/tidwall/gjson v1.17.0 golang.org/x/sync v0.4.0 gopkg.in/yaml.v2 v2.4.0 - gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -22,4 +21,5 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/mocks/constraint_body_json_field_matches_json.go b/mocks/constraint_body_json_field_matches_json.go index 5e409c0..ceedcaa 100644 --- a/mocks/constraint_body_json_field_matches_json.go +++ b/mocks/constraint_body_json_field_matches_json.go @@ -2,10 +2,11 @@ package mocks import ( "encoding/json" - "fmt" "net/http" + "github.com/lansfy/gonkex/colorize" "github.com/lansfy/gonkex/compare" + "github.com/tidwall/gjson" ) @@ -58,10 +59,10 @@ func (c *bodyJSONFieldMatchesJSONConstraint) Verify(r *http.Request) []error { value := gjson.Get(string(body), c.path) if !value.Exists() { - return []error{fmt.Errorf("json field %s does not exist", c.path)} + return []error{colorize.NewEntityError("json field %s does not exist", c.path)} } if value.String() == "" { - return []error{fmt.Errorf("json field %s is empty", c.path)} + return []error{colorize.NewEntityError("json field %s is empty", c.path)} } var actual interface{} diff --git a/mocks/constraint_body_matches_json.go b/mocks/constraint_body_matches_json.go index baba1c3..88ef4bf 100644 --- a/mocks/constraint_body_matches_json.go +++ b/mocks/constraint_body_matches_json.go @@ -2,7 +2,7 @@ package mocks import ( "encoding/json" - "fmt" + "errors" "net/http" "github.com/lansfy/gonkex/compare" @@ -50,7 +50,7 @@ func (c *bodyMatchesJSONConstraint) Verify(r *http.Request) []error { } if len(body) == 0 { - return []error{fmt.Errorf("request is empty")} + return []error{errors.New("request is empty")} } var actual interface{} err = json.Unmarshal(body, &actual) diff --git a/mocks/constraint_body_matches_text.go b/mocks/constraint_body_matches_text.go index 88f1c43..404d76a 100644 --- a/mocks/constraint_body_matches_text.go +++ b/mocks/constraint_body_matches_text.go @@ -52,10 +52,10 @@ func (c *bodyMatchesTextConstraint) Verify(r *http.Request) []error { textBody := string(body) if c.body != "" && c.body != textBody { - return []error{fmt.Errorf("body value\n%s\ndoesn't match expected\n%s", textBody, c.body)} + return []error{fmt.Errorf("body value\n%s\ndoes not match expected\n%s", textBody, c.body)} } if c.regexp != nil && !c.regexp.MatchString(textBody) { - return []error{fmt.Errorf("body value\n%s\ndoesn't match regexp %s", textBody, c.regexp)} + return []error{fmt.Errorf("body value\n%s\ndoes not match regexp %s", textBody, c.regexp)} } return nil } diff --git a/mocks/constraint_body_matches_xml.go b/mocks/constraint_body_matches_xml.go index a4f1ea5..6a94403 100644 --- a/mocks/constraint_body_matches_xml.go +++ b/mocks/constraint_body_matches_xml.go @@ -1,7 +1,7 @@ package mocks import ( - "fmt" + "errors" "net/http" "github.com/lansfy/gonkex/compare" @@ -50,7 +50,7 @@ func (c *bodyMatchesXMLConstraint) Verify(r *http.Request) []error { } if len(body) == 0 { - return []error{fmt.Errorf("request is empty")} + return []error{errors.New("request is empty")} } actual, err := xmlparsing.Parse(string(body)) diff --git a/mocks/constraint_header.go b/mocks/constraint_header.go index 741056a..94e0cec 100644 --- a/mocks/constraint_header.go +++ b/mocks/constraint_header.go @@ -1,10 +1,10 @@ package mocks import ( - "fmt" "net/http" "regexp" + "github.com/lansfy/gonkex/colorize" "github.com/lansfy/gonkex/compare" ) @@ -62,13 +62,13 @@ func (c *headerConstraint) GetName() string { func (c *headerConstraint) Verify(r *http.Request) []error { value := r.Header.Get(c.header) if value == "" { - return []error{fmt.Errorf("request doesn't have header %s", c.header)} + return []error{colorize.NewEntityError("request does not have header %s", c.header)} } if c.value != "" && c.value != value { - return []error{fmt.Errorf("%s header value %s doesn't match expected %s", c.header, value, c.value)} + return []error{colorize.NewNotEqualError("%s header value does not match:", c.header, c.value, value)} } if c.regexp != nil && !c.regexp.MatchString(value) { - return []error{fmt.Errorf("%s header value %s doesn't match regexp %s", c.header, value, c.regexp)} + return []error{colorize.NewNotEqualError("%s header value does not match regexp:", c.header, c.regexp, value)} } return nil } diff --git a/mocks/constraint_header_test.go b/mocks/constraint_header_test.go index b29a9aa..9007996 100644 --- a/mocks/constraint_header_test.go +++ b/mocks/constraint_header_test.go @@ -100,7 +100,7 @@ func Test_headerConstraint_Verify(t *testing.T) { request: &http.Request{ Header: http.Header{}, }, - wantErr: "request doesn't have header X-Test", + wantErr: "request does not have header 'X-Test'", }, { description: "header value does not match", @@ -109,7 +109,7 @@ func Test_headerConstraint_Verify(t *testing.T) { request: &http.Request{ Header: http.Header{"X-Test": []string{"actual-value"}}, }, - wantErr: "X-Test header value actual-value doesn't match expected expected-value", + wantErr: "'X-Test' header value does not match:\n expected: expected-value\n actual: actual-value", }, { description: "header value matches regexp", @@ -127,7 +127,7 @@ func Test_headerConstraint_Verify(t *testing.T) { request: &http.Request{ Header: http.Header{"X-Test": []string{"wrong-value"}}, }, - wantErr: "X-Test header value wrong-value doesn't match regexp ^test-.*$", + wantErr: "'X-Test' header value does not match regexp:\n expected: ^test-.*$\n actual: wrong-value", }, { description: "header value matches expected value", diff --git a/mocks/constraint_method.go b/mocks/constraint_method.go index 4b26b8b..546c2a6 100644 --- a/mocks/constraint_method.go +++ b/mocks/constraint_method.go @@ -1,9 +1,10 @@ package mocks import ( - "fmt" "net/http" "strings" + + "github.com/lansfy/gonkex/colorize" ) func loadMethodConstraint(def map[interface{}]interface{}) (verifier, error) { @@ -28,7 +29,7 @@ func (c *methodConstraint) GetName() string { func (c *methodConstraint) Verify(r *http.Request) []error { if !strings.EqualFold(r.Method, c.method) { - return []error{fmt.Errorf("method does not match: expected %s, actual %s", r.Method, c.method)} + return []error{colorize.NewNotEqualError("%s does not match:", "method", c.method, r.Method)} } return nil } diff --git a/mocks/constraint_method_test.go b/mocks/constraint_method_test.go index 78f7424..54b0d5c 100644 --- a/mocks/constraint_method_test.go +++ b/mocks/constraint_method_test.go @@ -73,7 +73,7 @@ func Test_methodConstraint_Verify(t *testing.T) { description: "method does not match", method: "POST", request: &http.Request{Method: "GET"}, - wantErr: "method does not match: expected GET, actual POST", + wantErr: "'method' does not match:\n expected: POST\n actual: GET", }, } diff --git a/mocks/constraint_path.go b/mocks/constraint_path.go index ec36de9..864e98b 100644 --- a/mocks/constraint_path.go +++ b/mocks/constraint_path.go @@ -1,10 +1,10 @@ package mocks import ( - "fmt" "net/http" "regexp" + "github.com/lansfy/gonkex/colorize" "github.com/lansfy/gonkex/compare" ) @@ -54,10 +54,10 @@ func (c *pathConstraint) GetName() string { func (c *pathConstraint) Verify(r *http.Request) []error { path := r.URL.Path if c.path != "" && c.path != path { - return []error{fmt.Errorf("url path %s doesn't match expected %s", path, c.path)} + return []error{colorize.NewNotEqualError("url %s does not match expected:", "path", c.path, path)} } if c.regexp != nil && !c.regexp.MatchString(path) { - return []error{fmt.Errorf("url path %s doesn't match regexp %s", path, c.regexp)} + return []error{colorize.NewNotEqualError("url %s does not match expected regexp:", "path", c.regexp, path)} } return nil } diff --git a/mocks/constraint_path_test.go b/mocks/constraint_path_test.go index 79090bb..43975c2 100644 --- a/mocks/constraint_path_test.go +++ b/mocks/constraint_path_test.go @@ -113,7 +113,7 @@ func Test_pathConstraint_Verify(t *testing.T) { description: "path does not match", path: "/test", reqPath: "/mismatch", - wantErr: "url path /mismatch doesn't match expected /test", + wantErr: "url 'path' does not match expected:\n expected: /test\n actual: /mismatch", }, { description: "regexp matches", @@ -125,7 +125,7 @@ func Test_pathConstraint_Verify(t *testing.T) { description: "regexp does not match", re: "^/test[0-9]*$", reqPath: "/mismatch", - wantErr: "url path /mismatch doesn't match regexp ^/test[0-9]*$", + wantErr: "url 'path' does not match expected regexp:\n expected: ^/test[0-9]*$\n actual: /mismatch", }, } diff --git a/mocks/definition.go b/mocks/definition.go index 2fb784f..1e7f735 100644 --- a/mocks/definition.go +++ b/mocks/definition.go @@ -1,10 +1,13 @@ package mocks import ( + "errors" "fmt" "net/http" "net/http/httputil" "sync" + + "github.com/lansfy/gonkex/colorize" ) var _ contextAwareStrategy = (*Definition)(nil) @@ -58,8 +61,10 @@ func (d *Definition) EndRunningContext() []error { errs = s.EndRunningContext() } if d.callsConstraint != CallsNoConstraint && d.calls != d.callsConstraint { - err := fmt.Errorf("at path %s: number of calls does not match: expected %d, actual %d", - d.path, d.callsConstraint, d.calls) + err := colorize.NewEntityError("at path %s", d.path) + err.SetSubError( + colorize.NewNotEqualError("number of %s does not match:", "calls", d.callsConstraint, d.calls), + ) errs = append(errs, err) } return errs @@ -79,11 +84,9 @@ func verifyRequestConstraints(requestConstraints []verifier, r *http.Request) [] for _, c := range requestConstraints { errs := c.Verify(r) for _, e := range errs { - errors = append(errors, &RequestConstraintError{ - error: e, - Constraint: c, - RequestDump: requestDump, - }) + err := colorize.NewEntityError("request constraint %s", c.GetName()).SetSubError(e) + err.AddParts(colorize.None(", request was:\n\n"), colorize.None(string(requestDump))) + errors = append(errors, err) } } @@ -97,6 +100,6 @@ func (d *Definition) ExecuteWithoutVerifying(w http.ResponseWriter, r *http.Requ return d.replyStrategy.HandleRequest(w, r) } return []error{ - fmt.Errorf("reply strategy undefined"), + errors.New("reply strategy undefined"), } } diff --git a/mocks/error.go b/mocks/error.go index 0c65589..8c4eb6b 100644 --- a/mocks/error.go +++ b/mocks/error.go @@ -6,15 +6,6 @@ import ( "net/http/httputil" ) -type Error struct { - error - ServiceName string -} - -func (e *Error) Error() string { - return fmt.Sprintf("mock %s: %s", e.ServiceName, e.error.Error()) -} - type RequestConstraintError struct { error Constraint verifier diff --git a/mocks/loader.go b/mocks/loader.go index 3fd27ea..ac5c45c 100644 --- a/mocks/loader.go +++ b/mocks/loader.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" "text/template" + + "github.com/lansfy/gonkex/colorize" ) type Loader interface { @@ -37,9 +39,13 @@ func (l *loaderImpl) LoadDefinition(rawDef interface{}) (*Definition, error) { } func (l *loaderImpl) loadDefinition(path string, rawDef interface{}) (*Definition, error) { + wrap := func(err error) error { + return colorize.NewEntityError("at path %s", path).SetSubError(err) + } + def, ok := rawDef.(map[interface{}]interface{}) if !ok { - return nil, fmt.Errorf("at path %s: Definition must be key-values", path) + return nil, wrap(errors.New("definition must be key-values")) } // load request constraints @@ -47,13 +53,13 @@ func (l *loaderImpl) loadDefinition(path string, rawDef interface{}) (*Definitio if constraints, ok := def["requestConstraints"]; ok { constraints, ok := constraints.([]interface{}) if !ok || len(constraints) == 0 { - return nil, fmt.Errorf("at path %s: `requestConstraints` requires array", path) + return nil, wrap(colorize.NewEntityError("%s requires array", "requestConstraints")) } requestConstraints = make([]verifier, len(constraints)) for i, c := range constraints { constraint, err := loadConstraint(c) if err != nil { - return nil, fmt.Errorf("at path %s: unable to load constraint %d: %w", path, i+1, err) + return nil, wrap(fmt.Errorf("unable to load constraint %d: %w", i+1, err)) } requestConstraints[i] = constraint } @@ -68,11 +74,11 @@ func (l *loaderImpl) loadDefinition(path string, rawDef interface{}) (*Definitio // load reply strategy strategyName, err := getRequiredStringKey(def, "strategy", false) if err != nil { - return nil, fmt.Errorf("at path %s: %w", path, err) + return nil, wrap(err) } - wrap := func(err error) error { - return fmt.Errorf("strategy '%s': %w", strategyName, err) + wrap = func(err error) error { + return colorize.NewEntityError("strategy %s", strategyName).SetSubError(err) } replyStrategy, err := l.loadStrategy(path, strategyName, def, &ak) @@ -136,7 +142,7 @@ func loadConstraint(definition interface{}) (verifier, error) { ak := []string{"kind"} wrap := func(err error) error { - return fmt.Errorf("constraint '%s': %w", kind, err) + return colorize.NewEntityError("constraint %s", kind).SetSubError(err) } c, err := loadConstraintOfKind(kind, def, &ak) @@ -193,7 +199,10 @@ func validateMapKeys(m map[interface{}]interface{}, allowedKeys []string) error for key := range m { skey, ok := key.(string) if !ok { - return fmt.Errorf("key '%v' has non-string type", key) + return colorize.NewEntityError( + "key %s has non-string type", + fmt.Sprintf("%v", key), + ) } found := false diff --git a/mocks/mocks.go b/mocks/mocks.go index 7937793..964fa2a 100644 --- a/mocks/mocks.go +++ b/mocks/mocks.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "strings" + + "github.com/lansfy/gonkex/colorize" ) type Mocks struct { @@ -107,7 +109,9 @@ func (m *Mocks) LoadDefinitions(loader Loader, definitions map[string]interface{ def, err := loader.LoadDefinition(definition) if err != nil { - return fmt.Errorf("load definition for '%s': %w", serviceName, err) + perr := colorize.NewEntityError("load definition for %s", serviceName) + perr.SetSubError(err) + return perr } service.SetDefinition(def) } diff --git a/mocks/service_mock.go b/mocks/service_mock.go index 73dd913..ee8faac 100644 --- a/mocks/service_mock.go +++ b/mocks/service_mock.go @@ -5,6 +5,8 @@ import ( "net" "net/http" "sync" + + "github.com/lansfy/gonkex/colorize" ) type ServiceMock struct { @@ -93,10 +95,9 @@ func (m *ServiceMock) EndRunningContext() []error { errs := append(m.errors, m.mock.EndRunningContext()...) for i, e := range errs { - errs[i] = &Error{ - error: e, - ServiceName: m.ServiceName, - } + err := colorize.NewEntityError("mock %s", m.ServiceName) + err.SetSubError(e) + errs[i] = err } return errs } diff --git a/mocks/strategy_uri_vary_reply.go b/mocks/strategy_uri_vary_reply.go index db34974..9c124e6 100644 --- a/mocks/strategy_uri_vary_reply.go +++ b/mocks/strategy_uri_vary_reply.go @@ -1,9 +1,10 @@ package mocks import ( - "errors" "net/http" "strings" + + "github.com/lansfy/gonkex/colorize" ) func (l *loaderImpl) loadUriVaryReplyStrategy(path string, def map[interface{}]interface{}) (ReplyStrategy, error) { @@ -15,7 +16,7 @@ func (l *loaderImpl) loadUriVaryReplyStrategy(path string, def map[interface{}]i if u, ok := def["uris"]; ok { urisMap, ok := u.(map[interface{}]interface{}) if !ok { - return nil, errors.New("map under `uris` key required") + return nil, colorize.NewEntityError("map under %s key required", "uris") } uris = make(map[string]*Definition, len(urisMap)) for uri, v := range urisMap { diff --git a/output/terminal/terminal.go b/output/terminal/terminal.go index 9d07abc..50ffeaf 100644 --- a/output/terminal/terminal.go +++ b/output/terminal/terminal.go @@ -3,7 +3,6 @@ package terminal import ( "bytes" _ "embed" - "errors" "fmt" "io" "text/template" @@ -108,7 +107,7 @@ func getTemplateFuncMap(policy ColorPolicy, showHeaders bool) template.FuncMap { "yellow": color.YellowString, "danger": color.New(color.FgHiWhite, color.BgRed).Sprint, "success": color.New(color.FgHiWhite, color.BgGreen).Sprint, - "printError": printWithColor, + "printError": colorize.GetColoredValue, } } else { funcMap = template.FuncMap{ @@ -128,11 +127,3 @@ func getTemplateFuncMap(policy ColorPolicy, showHeaders bool) template.FuncMap { func suppressColor(err error) string { return err.Error() } - -func printWithColor(err error) string { - var pErr *colorize.Error - if errors.As(err, &pErr) { - return pErr.ColorError() - } - return err.Error() -} diff --git a/runner/errors_output_test.go b/runner/errors_output_test.go index a2b4e70..c942339 100644 --- a/runner/errors_output_test.go +++ b/runner/errors_output_test.go @@ -13,6 +13,7 @@ import ( "github.com/lansfy/gonkex/checker/response_body" "github.com/lansfy/gonkex/checker/response_db" "github.com/lansfy/gonkex/checker/response_header" + "github.com/lansfy/gonkex/mocks" "github.com/lansfy/gonkex/output/terminal" "github.com/lansfy/gonkex/testloader/yaml_file" "github.com/lansfy/gonkex/variables" @@ -61,17 +62,24 @@ func Test_Error_Examples(t *testing.T) { initErrorServer() server := httptest.NewServer(nil) - for caseID := 1; caseID <= 4; caseID++ { + for caseID := 1; caseID <= 5; caseID++ { t.Run(fmt.Sprintf("case%d", caseID), func(t *testing.T) { expected, err := os.ReadFile(fmt.Sprintf("testdata/errors-example/case%d_output.txt", caseID)) require.NoError(t, err) + m := mocks.NewNop("subservice") + err = m.Start() + require.NoError(t, err) + defer m.Shutdown() + testHandler := NewConsoleHandler() yamlLoader := yaml_file.NewLoader(fmt.Sprintf("testdata/errors-example/case%d.yaml", caseID)) r := New( &Config{ - Host: server.URL, - Variables: variables.New(), + Host: server.URL, + Variables: variables.New(), + Mocks: m, + MocksLoader: mocks.NewYamlLoader(nil), }, yamlLoader, testHandler.HandleTest, @@ -94,7 +102,7 @@ func Test_Error_Examples(t *testing.T) { require.NoError(t, err) if !showOnScreen { - require.Equal(t, normalize(string(expected)), normalize(buf.String())) + require.Equal(t, normalize(buf.String()), normalize(string(expected))) } }) } diff --git a/runner/runner.go b/runner/runner.go index 8f75fe0..8da25d9 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -11,6 +11,7 @@ import ( "github.com/lansfy/gonkex/checker" "github.com/lansfy/gonkex/cmd_runner" + "github.com/lansfy/gonkex/colorize" "github.com/lansfy/gonkex/mocks" "github.com/lansfy/gonkex/models" "github.com/lansfy/gonkex/output" @@ -96,7 +97,7 @@ func (r *Runner) Run() error { } err := r.handler(test, testExecutor) if err != nil { - return fmt.Errorf("test '%s' error: %w", test.GetName(), err) + return colorize.NewEntityError("test %s error", test.GetName()).SetSubError(err) } } diff --git a/runner/testdata/errors-example/case1_output.txt b/runner/testdata/errors-example/case1_output.txt index 6a5dae4..bdb9de4 100644 --- a/runner/testdata/errors-example/case1_output.txt +++ b/runner/testdata/errors-example/case1_output.txt @@ -20,7 +20,7 @@ Response: Errors: -1) at path $ values do not match: +1) at path '$' values do not match: expected: 123 actual: 1234 diff --git a/runner/testdata/errors-example/case2_output.txt b/runner/testdata/errors-example/case2_output.txt index de9fe48..a4e03c9 100644 --- a/runner/testdata/errors-example/case2_output.txt +++ b/runner/testdata/errors-example/case2_output.txt @@ -24,11 +24,11 @@ Response: Errors: -1) at path $.somefield values do not match: +1) at path '$.somefield' values do not match: expected: 1234 actual: 123 -2) response header Content-Type value does not match: +2) response header 'Content-Type' value does not match: expected: text/plain actual: application/json diff --git a/runner/testdata/errors-example/case3_output.txt b/runner/testdata/errors-example/case3_output.txt index d85201c..7a10425 100644 --- a/runner/testdata/errors-example/case3_output.txt +++ b/runner/testdata/errors-example/case3_output.txt @@ -25,15 +25,15 @@ SELECT * Errors: -1) server responded with unexpected status: +1) server responded with unexpected 'status': expected: 400 actual: 200 -2) at path $[0].field1 values do not match: +2) at path '$[0].field1' values do not match: expected: value2 actual: value1 -3) at path $[1].field2 values do not match: +3) at path '$[1].field2' values do not match: expected: 124 actual: 123 diff --git a/runner/testdata/errors-example/case4_output.txt b/runner/testdata/errors-example/case4_output.txt index fc712af..cdc3201 100644 --- a/runner/testdata/errors-example/case4_output.txt +++ b/runner/testdata/errors-example/case4_output.txt @@ -25,11 +25,11 @@ SELECT * Errors: -1) quantity of items in database do not match: +1) quantity of 'items in database' do not match: expected: 1 actual: 2 - query: SELECT * + query: 'SELECT *' diff (--- expected vs +++ actual): [ + "{\"field1\":\"value1\"}", @@ -37,3 +37,4 @@ Errors: ] + diff --git a/runner/testdata/errors-example/case5.yaml b/runner/testdata/errors-example/case5.yaml new file mode 100644 index 0000000..4db69dd --- /dev/null +++ b/runner/testdata/errors-example/case5.yaml @@ -0,0 +1,13 @@ +- name: Test case 5 + description: Test mock call count error + method: GET + path: /json + response: + 200: > + {"somefield": 123} + mocks: + subservice: + calls: 1 + strategy: constant + body: "{}" + statusCode: 200 diff --git a/runner/testdata/errors-example/case5_output.txt b/runner/testdata/errors-example/case5_output.txt new file mode 100644 index 0000000..5645a91 --- /dev/null +++ b/runner/testdata/errors-example/case5_output.txt @@ -0,0 +1,27 @@ + + Name: Test case 5 +Description: Test mock call count error + File: testdata/errors-example/case5.yaml + +Request: + Method: GET + Path: /json + Query: + Body: + + +Response: + Status: 200 OK + Body: +{"somefield":123} + + + Result: ERRORS! + +Errors: + +1) mock 'subservice': at path '$': number of 'calls' does not match: + expected: 1 + actual: 0 + +