From 2f9536a14934c25c548e23d88e7bba9f539cd9ae Mon Sep 17 00:00:00 2001 From: Nayeem Hasan Date: Wed, 11 Oct 2023 21:22:40 +0600 Subject: [PATCH 1/2] Handle FloatFormat and DigitSeparator when formatting slices/maps --- e2e/report_test.go | 32 ++++++++++ formatter.go | 114 ++++++++++++++++++++++++++++++++- formatter_test.go | 154 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 298 insertions(+), 2 deletions(-) diff --git a/e2e/report_test.go b/e2e/report_test.go index 334662dc7..933e235c2 100644 --- a/e2e/report_test.go +++ b/e2e/report_test.go @@ -69,6 +69,10 @@ func TestE2EReport_Values(t *testing.T) { w.Header().Add("Content-Type", "application/json") _, _ = w.Write([]byte("[111, 222, 444, 333]")) }) + mux.HandleFunc("/slice/float", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + _, _ = w.Write([]byte("[12.34, 0.0056, 78]")) + }) server := httptest.NewServer(mux) defer server.Close() @@ -219,6 +223,34 @@ func TestE2EReport_Values(t *testing.T) { t, trimmed, "333333 444444", "missing Expected", ) }) + + t.Run("slice of floats", func(t *testing.T) { + reporter := &recordingReporter{} + + e := httpexpect.WithConfig(httpexpect.Config{ + BaseURL: server.URL, + Reporter: reporter, + Formatter: &httpexpect.DefaultFormatter{ + FloatFormat: httpexpect.FloatFormatScientific, + ColorMode: httpexpect.ColorModeAlways, + }, + }) + + e.GET("/slice/float"). + Expect(). + JSON(). + Array(). + IsEmpty() + t.Logf("%s", reporter.recorded) + + assert.Contains( + t, reporter.recorded, "expected: empty array", + "missing Errors", + ) + assert.Contains( + t, reporter.recorded, "[\n 1.234e+01,\n 5.6e-03,\n 7.8e+01\n ]", "missing Actual", + ) + }) } func TestE2EReport_Path(t *testing.T) { diff --git a/formatter.go b/formatter.go index 02985f31c..45b7077c8 100644 --- a/formatter.go +++ b/formatter.go @@ -5,10 +5,13 @@ import ( "encoding/json" "flag" "fmt" + "io" "math" + "math/big" "net/http/httputil" "os" "path/filepath" + "reflect" "regexp" "strconv" "strings" @@ -622,6 +625,7 @@ func (f *DefaultFormatter) formatValue(value interface{}) string { return ss } } + value, _ = f.convertNumber(value) if b, err := json.MarshalIndent(value, "", defaultIndent); err == nil { return string(b) } @@ -633,6 +637,94 @@ func (f *DefaultFormatter) formatValue(value interface{}) string { return sq.Sdump(value) } +func (f *DefaultFormatter) convertNumber(value interface{}) (interface{}, bool) { + if fm := newFormatNumber(value, f); fm != nil { + return fm, true + } + + rv := reflect.ValueOf(value) + + switch rv.Kind() { + case reflect.Slice: + sl := reflect.MakeSlice(reflect.TypeOf([]interface{}{}), rv.Len(), rv.Cap()) + updated := false + + for i := 0; i < rv.Len(); i++ { + converted, changed := f.convertNumber(rv.Index(i).Interface()) + if changed { + sl.Index(i).Set(reflect.ValueOf(converted)) + updated = true + } + } + + return sl.Interface(), updated + + case reflect.Map: + keyType := reflect.TypeOf(value).Key() + mapType := reflect.MapOf(keyType, reflect.TypeOf((*interface{})(nil)).Elem()) + mp := reflect.MakeMap(mapType) + updated := false + + iter := rv.MapRange() + for iter.Next() { + converted, changed := f.convertNumber(iter.Value().Interface()) + if changed { + mp.SetMapIndex(iter.Key(), reflect.ValueOf(converted)) + updated = true + } + } + + return mp.Interface(), updated + } + + return value, false +} + +type formatNumber struct { + value *big.Float + f *DefaultFormatter +} + +func newFormatNumber(value interface{}, f *DefaultFormatter) *formatNumber { + if flt := extractBigFloat(value); flt != nil { + return &formatNumber{ + value: flt, + f: f, + } + } + return nil +} + +func (fn formatNumber) MarshalJSON() ([]byte, error) { + if flt, accuracy := fn.value.Float32(); accuracy == big.Exact { + return []byte(fn.f.formatFloatValue(float64(flt), 32)), nil + } + + if flt, accuracy := fn.value.Float64(); accuracy == big.Exact { + return []byte(fn.f.formatFloatValue(flt, 64)), nil + } + + return fn.value.MarshalText() +} + +func (fn formatNumber) LitterDump(w io.Writer) { + if flt, accuracy := fn.value.Float32(); accuracy == big.Exact { + s := fn.f.reformatNumber(fn.f.formatFloatValue(float64(flt), 32)) + w.Write([]byte(s)) + return + } + + if flt, accuracy := fn.value.Float64(); accuracy == big.Exact { + s := fn.f.reformatNumber(fn.f.formatFloatValue(flt, 64)) + w.Write([]byte(s)) + return + } + + if bytes, err := fn.value.MarshalText(); err == nil { + w.Write(bytes) + } +} + func (f *DefaultFormatter) formatFloatValue(value float64, bits int) string { switch f.FloatFormat { case FloatFormatAuto: @@ -855,6 +947,23 @@ func extractFloat64(value interface{}) *float64 { } } +func extractBigFloat(value interface{}) *big.Float { + if flt, err := strconv.ParseFloat(fmt.Sprint(value), 64); err == nil { + return big.NewFloat(flt) + } + + switch value := value.(type) { + case *big.Float: + return value + + case json.Number: + if flt, err := value.Float64(); err == nil { + return big.NewFloat(flt) + } + } + return nil +} + func exctractRange(value interface{}) *AssertionRange { switch rng := value.(type) { case AssertionRange: @@ -1081,8 +1190,9 @@ var defaultTemplateFuncs = template.FuncMap{ } var parsedInput interface{} - err := json.Unmarshal([]byte(input), &parsedInput) - if err != nil { + decoder := json.NewDecoder(strings.NewReader(input)) + decoder.UseNumber() + if err := decoder.Decode(&parsedInput); err != nil { return color.New(fallbackColor).Sprint(input) } diff --git a/formatter_test.go b/formatter_test.go index a78d1d255..e6ab8ed19 100644 --- a/formatter_test.go +++ b/formatter_test.go @@ -1,7 +1,9 @@ package httpexpect import ( + "encoding/json" "fmt" + "math/big" "strings" "testing" "time" @@ -696,6 +698,158 @@ func TestFormatter_FloatFormat(t *testing.T) { value: int(12345678), wantText: "12_345_678", }, + // slice of floats + { + name: "slice of float auto", + format: FloatFormatAuto, + value: []float32{1.234, 0.0056, 78000}, + wantText: "[\n 1.234,\n 0.0056,\n 78000\n]", + }, + { + name: "slice of float decimal", + format: FloatFormatDecimal, + value: []float32{1.234, 0.0056, 78000}, + wantText: "[\n 1.234,\n 0.0056,\n 78000\n]", + }, + { + name: "slice of float scientific", + format: FloatFormatScientific, + value: []float32{1.234, 0.0056, 78000}, + wantText: "[\n 1.234e+00,\n 5.6e-03,\n 7.8e+04\n]", + }, + // slice of json.Number + { + name: "slice of json.Number auto", + format: FloatFormatAuto, + value: []json.Number{"12.34", ".0056", "789"}, + wantText: "[\n 12.34,\n 0.0056,\n 789\n]", + }, + { + name: "slice of json.Number decimal", + format: FloatFormatDecimal, + value: []json.Number{"12.34", ".0056", "789"}, + wantText: "[\n 12.34,\n 0.0056,\n 789\n]", + }, + { + name: "slice of json.Number scientific", + format: FloatFormatScientific, + value: []json.Number{"12.34", ".0056", "789"}, + wantText: "[\n 1.234e+01,\n 5.6e-03,\n 7.89e+02\n]", + }, + // slice of big.Float + { + name: "slice of big.Float auto", + format: FloatFormatAuto, + value: []*big.Float{big.NewFloat(1234.5678), big.NewFloat(0.000234)}, + wantText: "[\n 1234.5678,\n 0.000234\n]", + }, + { + name: "slice of big.Float decimal", + format: FloatFormatDecimal, + value: []*big.Float{big.NewFloat(1234.5678), big.NewFloat(0.000234)}, + wantText: "[\n 1234.5678,\n 0.000234\n]", + }, + { + name: "slice of big.Float scientific", + format: FloatFormatScientific, + value: []*big.Float{big.NewFloat(1234.5678), big.NewFloat(0.000234)}, + wantText: "[\n 1.2345678e+03,\n 2.34e-04\n]", + }, + // slice of slice + { + name: "slice of slice auto", + format: FloatFormatAuto, + value: []interface{}{[]float64{.01, 20}, big.NewFloat(.0025), []json.Number{"23.45"}, 10}, + wantText: "[\n [\n 0.01,\n 20\n ],\n 0.0025,\n [\n 23.45\n ],\n 10\n]", + }, + { + name: "slice of slice decimal", + format: FloatFormatDecimal, + value: []interface{}{[]float64{.01, 20}, big.NewFloat(.0025), []json.Number{"23.45"}, 10}, + wantText: "[\n [\n 0.01,\n 20\n ],\n 0.0025,\n [\n 23.45\n ],\n 10\n]", + }, + { + name: "slice of slice scientific", + format: FloatFormatScientific, + value: []interface{}{[]float64{.01, 20}, big.NewFloat(.0025), []json.Number{"23.45"}, 10}, + wantText: "[\n [\n 1e-02,\n 2e+01\n ],\n 2.5e-03,\n [\n 2.345e+01\n ],\n 1e+01\n]", + }, + // map of floats + { + name: "map of float auto", + format: FloatFormatAuto, + value: map[string]float32{"a": 123.45, "b": 0.00678}, + wantText: "{\n \"a\": 123.45,\n \"b\": 0.00678\n}", + }, + { + name: "map of float decimal", + format: FloatFormatDecimal, + value: map[string]float32{"a": 123.45, "b": 0.00678}, + wantText: "{\n \"a\": 123.45,\n \"b\": 0.00678\n}", + }, + { + name: "map of float scientific", + format: FloatFormatScientific, + value: map[string]float32{"a": 123.45, "b": 0.00678}, + wantText: "{\n \"a\": 1.2345e+02,\n \"b\": 6.78e-03\n}", + }, + // map of json.Number + { + name: "map of json.Number auto", + format: FloatFormatAuto, + value: map[int]json.Number{1: "0.123", 2: "45.67", 3: "100"}, + wantText: "{\n \"1\": 0.123,\n \"2\": 45.67,\n \"3\": 100\n}", + }, + { + name: "map of json.Number decimal", + format: FloatFormatDecimal, + value: map[int]json.Number{1: "0.123", 2: "45.67", 3: "100"}, + wantText: "{\n \"1\": 0.123,\n \"2\": 45.67,\n \"3\": 100\n}", + }, + { + name: "map of json.Number scientific", + format: FloatFormatScientific, + value: map[int]json.Number{1: "0.123", 2: "45.67", 3: "100"}, + wantText: "{\n \"1\": 1.23e-01,\n \"2\": 4.567e+01,\n \"3\": 1e+02\n}", + }, + // map of any + { + name: "map of any auto", + format: FloatFormatAuto, + value: map[string]interface{}{"a": []float32{12.34}, "b": big.NewFloat(45.67), "c": []int{100}}, + wantText: "{\n \"a\": [\n 12.34\n ],\n \"b\": 45.67,\n \"c\": [\n 100\n ]\n}", + }, + { + name: "map of any decimal", + format: FloatFormatDecimal, + value: map[string]interface{}{"a": []float32{12.34}, "b": big.NewFloat(45.67), "c": []int{100}}, + wantText: "{\n \"a\": [\n 12.34\n ],\n \"b\": 45.67,\n \"c\": [\n 100\n ]\n}", + }, + { + name: "map of any scientific", + format: FloatFormatScientific, + value: map[string]interface{}{"a": []float32{12.34}, "b": big.NewFloat(45.67), "c": []int{100}}, + wantText: "{\n \"a\": [\n 1.234e+01\n ],\n \"b\": 4.567e+01,\n \"c\": [\n 1e+02\n ]\n}", + }, + // dump go values + { + name: "go values auto", + format: FloatFormatAuto, + value: map[[1]int][]float64{{1}: {12345.678}}, + wantText: "map[[1]int]interface {}{\n [1]int{\n 1,\n }: []interface {}{\n *httpexpect.formatNumber12_345.678,\n },\n}", + }, + { + name: "go values decimal", + format: FloatFormatDecimal, + value: map[[1]int][]float64{{1}: {12345.678}}, + wantText: "map[[1]int]interface {}{\n [1]int{\n 1,\n }: []interface {}{\n *httpexpect.formatNumber12_345.678,\n },\n}", + }, + { + name: "go values scientific", + format: FloatFormatScientific, + value: map[[1]int][]float64{{1}: {12345.678}}, + wantText: "map[[1]int]interface {}{\n [1]int{\n 1,\n }: []interface {}{\n *httpexpect.formatNumber1.234_567_8e+04,\n },\n}", + }, } for _, tc := range cases { From 5bd5c8425abba2415c1aba813109f9ca5cd55f05 Mon Sep 17 00:00:00 2001 From: Nayeem Hasan Date: Thu, 12 Oct 2023 17:37:57 +0600 Subject: [PATCH 2/2] Fix linter issues --- e2e/report_test.go | 5 ++-- formatter.go | 13 ++++---- formatter_test.go | 74 ++++++++++++++++++++++++++++------------------ 3 files changed, 56 insertions(+), 36 deletions(-) diff --git a/e2e/report_test.go b/e2e/report_test.go index 933e235c2..118cddb02 100644 --- a/e2e/report_test.go +++ b/e2e/report_test.go @@ -71,7 +71,7 @@ func TestE2EReport_Values(t *testing.T) { }) mux.HandleFunc("/slice/float", func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", "application/json") - _, _ = w.Write([]byte("[12.34, 0.0056, 78]")) + _, _ = w.Write([]byte("[12.34, 0.056]")) }) server := httptest.NewServer(mux) @@ -248,7 +248,8 @@ func TestE2EReport_Values(t *testing.T) { "missing Errors", ) assert.Contains( - t, reporter.recorded, "[\n 1.234e+01,\n 5.6e-03,\n 7.8e+01\n ]", "missing Actual", + t, reporter.recorded, "[\n 1.234e+01,\n 5.6e-02\n ]", + "missing Actual", ) }) } diff --git a/formatter.go b/formatter.go index 45b7077c8..871334e9b 100644 --- a/formatter.go +++ b/formatter.go @@ -644,7 +644,7 @@ func (f *DefaultFormatter) convertNumber(value interface{}) (interface{}, bool) rv := reflect.ValueOf(value) - switch rv.Kind() { + switch rv.Kind() { //nolint case reflect.Slice: sl := reflect.MakeSlice(reflect.TypeOf([]interface{}{}), rv.Len(), rv.Cap()) updated := false @@ -675,9 +675,10 @@ func (f *DefaultFormatter) convertNumber(value interface{}) (interface{}, bool) } return mp.Interface(), updated - } - return value, false + default: + return value, false + } } type formatNumber struct { @@ -710,18 +711,18 @@ func (fn formatNumber) MarshalJSON() ([]byte, error) { func (fn formatNumber) LitterDump(w io.Writer) { if flt, accuracy := fn.value.Float32(); accuracy == big.Exact { s := fn.f.reformatNumber(fn.f.formatFloatValue(float64(flt), 32)) - w.Write([]byte(s)) + _, _ = w.Write([]byte(s)) return } if flt, accuracy := fn.value.Float64(); accuracy == big.Exact { s := fn.f.reformatNumber(fn.f.formatFloatValue(flt, 64)) - w.Write([]byte(s)) + _, _ = w.Write([]byte(s)) return } if bytes, err := fn.value.MarshalText(); err == nil { - w.Write(bytes) + _, _ = w.Write(bytes) } } diff --git a/formatter_test.go b/formatter_test.go index e6ab8ed19..53d525f21 100644 --- a/formatter_test.go +++ b/formatter_test.go @@ -759,20 +759,20 @@ func TestFormatter_FloatFormat(t *testing.T) { { name: "slice of slice auto", format: FloatFormatAuto, - value: []interface{}{[]float64{.01, 20}, big.NewFloat(.0025), []json.Number{"23.45"}, 10}, - wantText: "[\n [\n 0.01,\n 20\n ],\n 0.0025,\n [\n 23.45\n ],\n 10\n]", + value: []interface{}{[]float64{.01, 20}, []json.Number{"23.45"}, 10}, + wantText: "[\n [\n 0.01,\n 20\n ],\n [\n 23.45\n ],\n 10\n]", }, { name: "slice of slice decimal", format: FloatFormatDecimal, - value: []interface{}{[]float64{.01, 20}, big.NewFloat(.0025), []json.Number{"23.45"}, 10}, - wantText: "[\n [\n 0.01,\n 20\n ],\n 0.0025,\n [\n 23.45\n ],\n 10\n]", + value: []interface{}{[]float64{.01, 20}, []json.Number{"23.45"}, 10}, + wantText: "[\n [\n 0.01,\n 20\n ],\n [\n 23.45\n ],\n 10\n]", }, { name: "slice of slice scientific", format: FloatFormatScientific, - value: []interface{}{[]float64{.01, 20}, big.NewFloat(.0025), []json.Number{"23.45"}, 10}, - wantText: "[\n [\n 1e-02,\n 2e+01\n ],\n 2.5e-03,\n [\n 2.345e+01\n ],\n 1e+01\n]", + value: []interface{}{[]float64{.01, 20}, []json.Number{"23.45"}, 10}, + wantText: "[\n [\n 1e-02,\n 2e+01\n ],\n [\n 2.345e+01\n ],\n 1e+01\n]", }, // map of floats { @@ -816,39 +816,57 @@ func TestFormatter_FloatFormat(t *testing.T) { { name: "map of any auto", format: FloatFormatAuto, - value: map[string]interface{}{"a": []float32{12.34}, "b": big.NewFloat(45.67), "c": []int{100}}, - wantText: "{\n \"a\": [\n 12.34\n ],\n \"b\": 45.67,\n \"c\": [\n 100\n ]\n}", + value: map[string]interface{}{"a": []float32{12.34}, "b": []int{100}}, + wantText: "{\n \"a\": [\n 12.34\n ],\n \"b\": [\n 100\n ]\n}", }, { name: "map of any decimal", format: FloatFormatDecimal, - value: map[string]interface{}{"a": []float32{12.34}, "b": big.NewFloat(45.67), "c": []int{100}}, - wantText: "{\n \"a\": [\n 12.34\n ],\n \"b\": 45.67,\n \"c\": [\n 100\n ]\n}", + value: map[string]interface{}{"a": []float32{12.34}, "b": []int{100}}, + wantText: "{\n \"a\": [\n 12.34\n ],\n \"b\": [\n 100\n ]\n}", }, { name: "map of any scientific", format: FloatFormatScientific, - value: map[string]interface{}{"a": []float32{12.34}, "b": big.NewFloat(45.67), "c": []int{100}}, - wantText: "{\n \"a\": [\n 1.234e+01\n ],\n \"b\": 4.567e+01,\n \"c\": [\n 1e+02\n ]\n}", + value: map[string]interface{}{"a": []float32{12.3}, "b": []int{100}}, + wantText: "{\n \"a\": [\n 1.23e+01\n ],\n \"b\": [\n 1e+02\n ]\n}", }, // dump go values { - name: "go values auto", - format: FloatFormatAuto, - value: map[[1]int][]float64{{1}: {12345.678}}, - wantText: "map[[1]int]interface {}{\n [1]int{\n 1,\n }: []interface {}{\n *httpexpect.formatNumber12_345.678,\n },\n}", - }, - { - name: "go values decimal", - format: FloatFormatDecimal, - value: map[[1]int][]float64{{1}: {12345.678}}, - wantText: "map[[1]int]interface {}{\n [1]int{\n 1,\n }: []interface {}{\n *httpexpect.formatNumber12_345.678,\n },\n}", - }, - { - name: "go values scientific", - format: FloatFormatScientific, - value: map[[1]int][]float64{{1}: {12345.678}}, - wantText: "map[[1]int]interface {}{\n [1]int{\n 1,\n }: []interface {}{\n *httpexpect.formatNumber1.234_567_8e+04,\n },\n}", + name: "go values auto", + format: FloatFormatAuto, + value: map[[1]int][]float64{{1}: {12345.678}}, + wantText: `map[[1]int]interface {}{ + [1]int{ + 1, + }: []interface {}{ + *httpexpect.formatNumber12_345.678, + }, +}`, + }, + { + name: "go values decimal", + format: FloatFormatDecimal, + value: map[[1]int][]float64{{1}: {12345.678}}, + wantText: `map[[1]int]interface {}{ + [1]int{ + 1, + }: []interface {}{ + *httpexpect.formatNumber12_345.678, + }, +}`, + }, + { + name: "go values scientific", + format: FloatFormatScientific, + value: map[[1]int][]float64{{1}: {12345.678}}, + wantText: `map[[1]int]interface {}{ + [1]int{ + 1, + }: []interface {}{ + *httpexpect.formatNumber1.234_567_8e+04, + }, +}`, }, }