diff --git a/examples/__snapshots__/simple_test.snap b/examples/__snapshots__/simple_test.snap index fcd5ef0..7a905fb 100755 --- a/examples/__snapshots__/simple_test.snap +++ b/examples/__snapshots__/simple_test.snap @@ -68,3 +68,11 @@ it's okay [TestSimple/.* - 1] ignore regex patterns on names --- + +[TestSimpleTable/buffer#01 - 1] +&bytes.Buffer{ + buf: {0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x20, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67}, + off: 0, + lastRead: 0, +} +--- diff --git a/go.mod b/go.mod index b670c0a..1bcca07 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.16 require ( github.com/gkampitakis/ciinfo v0.1.1 github.com/gkampitakis/go-diff v1.3.0 - github.com/kr/pretty v0.3.0 ) -require github.com/rogpeppe/go-internal v1.9.0 // indirect +require github.com/kr/text v0.2.0 diff --git a/go.sum b/go.sum index bdb851e..d407cd6 100644 --- a/go.sum +++ b/go.sum @@ -3,16 +3,5 @@ github.com/gkampitakis/ciinfo v0.1.1 h1:dz1LCkOd+zmZ3YYlFNpr0hRDqGY7Ox2mcaltHzda github.com/gkampitakis/ciinfo v0.1.1/go.mod h1:bVaOGziPqf8PoeYZxatq1HmCsJUmv191hLnFboYxd9Y= github.com/gkampitakis/go-diff v1.3.0 h1:Szdbo5w73LSQ9sQ02h+NSSf2ZlW/E8naJCI1ZzQtWgE= github.com/gkampitakis/go-diff v1.3.0/go.mod h1:QUJDQRA0JkEX0d7tgDaBHzJv9IH6k6e91TByC+9/RFk= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= diff --git a/snaps/internal/formatter/formatter.go b/snaps/internal/formatter/formatter.go new file mode 100644 index 0000000..663de6c --- /dev/null +++ b/snaps/internal/formatter/formatter.go @@ -0,0 +1,338 @@ +package formatter + +import ( + "fmt" + "io" + "reflect" + "strconv" + "text/tabwriter" + + "github.com/kr/text" +) + +type formatter struct { + v reflect.Value + force bool + quote bool +} + +// Formatter makes a wrapper, f, that will format x as go source with line +// breaks and tabs. Object f responds to the "%v" formatting verb when both the +// "#" and " " (space) flags are set, for example: +// +// fmt.Sprintf("%# v", Formatter(x)) +// +// If one of these two flags is not set, or any other verb is used, f will +// format x according to the usual rules of package fmt. +// In particular, if x satisfies fmt.Formatter, then x.Format will be called. +func Formatter(x interface{}) (f fmt.Formatter) { + return formatter{v: reflect.ValueOf(x), quote: true} +} + +func (fo formatter) String() string { + return fmt.Sprint(fo.v.Interface()) // unwrap it +} + +func (fo formatter) passThrough(f fmt.State, c rune) { + s := "%" + for i := 0; i < 128; i++ { + if f.Flag(i) { + s += fmt.Sprint(i) + } + } + if w, ok := f.Width(); ok { + s += fmt.Sprintf("%d", w) + } + if p, ok := f.Precision(); ok { + s += fmt.Sprintf(".%d", p) + } + s += string(c) + fmt.Fprintf(f, s, fo.v.Interface()) +} + +func (fo formatter) Format(f fmt.State, c rune) { + if fo.force || c == 'v' && f.Flag('#') && f.Flag(' ') { + w := tabwriter.NewWriter(f, 4, 4, 1, ' ', 0) + p := &printer{tw: w, Writer: w, visited: make(map[visit]int)} + p.printValue(fo.v, true, fo.quote) + w.Flush() + return + } + fo.passThrough(f, c) +} + +type printer struct { + io.Writer + tw *tabwriter.Writer + visited map[visit]int + depth int +} + +func (p *printer) indent() *printer { + q := *p + q.tw = tabwriter.NewWriter(p.Writer, 4, 4, 1, ' ', 0) + q.Writer = text.NewIndentWriter(q.tw, []byte{'\t'}) + return &q +} + +func (p *printer) printInline(v reflect.Value, x interface{}, showType bool) { + if showType { + io.WriteString(p, v.Type().String()) + fmt.Fprintf(p, "(%#v)", x) + } else { + fmt.Fprintf(p, "%#v", x) + } +} + +// printValue must keep track of already-printed pointer values to avoid +// infinite recursion. +type visit struct { + v uintptr + typ reflect.Type +} + +func (p *printer) printValue(v reflect.Value, showType, quote bool) { + if p.depth > 10 { + io.WriteString(p, "!%v(DEPTH EXCEEDED)") + return + } + + switch v.Kind() { + case reflect.Bool: + p.printInline(v, v.Bool(), showType) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + p.printInline(v, v.Int(), showType) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + p.printInline(v, v.Uint(), showType) + case reflect.Float32, reflect.Float64: + p.printInline(v, v.Float(), showType) + case reflect.Complex64, reflect.Complex128: + fmt.Fprintf(p, "%#v", v.Complex()) + case reflect.String: + p.fmtString(v.String(), quote) + case reflect.Map: + t := v.Type() + if showType { + io.WriteString(p, t.String()) + } + writeByte(p, '{') + if nonzero(v) { + expand := !canInline(v.Type()) + pp := p + if expand { + writeByte(p, '\n') + pp = p.indent() + } + keys := v.MapKeys() + for i := 0; i < v.Len(); i++ { + showTypeInStruct := true + k := keys[i] + mv := v.MapIndex(k) + pp.printValue(k, false, true) + writeByte(pp, ':') + if expand { + writeByte(pp, '\t') + } + showTypeInStruct = t.Elem().Kind() == reflect.Interface + pp.printValue(mv, showTypeInStruct, true) + if expand { + io.WriteString(pp, ",\n") + } else if i < v.Len()-1 { + io.WriteString(pp, ", ") + } + } + if expand { + pp.tw.Flush() + } + } + writeByte(p, '}') + case reflect.Struct: + t := v.Type() + if v.CanAddr() { + addr := v.UnsafeAddr() + vis := visit{addr, t} + if vd, ok := p.visited[vis]; ok && vd < p.depth { + p.fmtString(t.String()+"{(CYCLIC REFERENCE)}", false) + break // don't print v again + } + p.visited[vis] = p.depth + } + + if showType { + io.WriteString(p, t.String()) + } + writeByte(p, '{') + if nonzero(v) { + expand := !canInline(v.Type()) + pp := p + if expand { + writeByte(p, '\n') + pp = p.indent() + } + + numfields := v.NumField() + writecomma := false + for i := 0; i < numfields; i++ { + fieldTag := t.Field(i) + val := fieldTag.Tag.Get("go-snaps") + if val == "-" { + continue + } + if writecomma && !expand && i > 0 { + io.WriteString(pp, ", ") + } + showTypeInStruct := true + if f := t.Field(i); f.Name != "" { + io.WriteString(pp, f.Name) + writeByte(pp, ':') + if expand { + writeByte(pp, '\t') + } + showTypeInStruct = labelType(f.Type) + writecomma = true + } + pp.printValue(getField(v, i), showTypeInStruct, true) + if expand { + io.WriteString(pp, ",\n") + } + } + if expand { + pp.tw.Flush() + } + } + writeByte(p, '}') + case reflect.Interface: + switch e := v.Elem(); { + case e.Kind() == reflect.Invalid: + io.WriteString(p, "nil") + case e.IsValid(): + pp := *p + pp.depth++ + pp.printValue(e, showType, true) + default: + io.WriteString(p, v.Type().String()) + io.WriteString(p, "(nil)") + } + case reflect.Array, reflect.Slice: + t := v.Type() + if showType { + io.WriteString(p, t.String()) + } + if v.Kind() == reflect.Slice && v.IsNil() && showType { + io.WriteString(p, "(nil)") + break + } + if v.Kind() == reflect.Slice && v.IsNil() { + io.WriteString(p, "nil") + break + } + writeByte(p, '{') + expand := !canInline(v.Type()) + pp := p + if expand { + writeByte(p, '\n') + pp = p.indent() + } + for i := 0; i < v.Len(); i++ { + showTypeInSlice := t.Elem().Kind() == reflect.Interface + pp.printValue(v.Index(i), showTypeInSlice, true) + if expand { + io.WriteString(pp, ",\n") + } else if i < v.Len()-1 { + io.WriteString(pp, ", ") + } + } + if expand { + pp.tw.Flush() + } + writeByte(p, '}') + case reflect.Ptr: + e := v.Elem() + if !e.IsValid() { + writeByte(p, '(') + io.WriteString(p, v.Type().String()) + io.WriteString(p, ")(nil)") + } else { + pp := *p + pp.depth++ + writeByte(pp, '&') + pp.printValue(e, true, true) + } + case reflect.Chan: + x := v.Pointer() + if showType { + writeByte(p, '(') + io.WriteString(p, v.Type().String()) + fmt.Fprintf(p, ")(%#v)", x) + } else { + fmt.Fprintf(p, "%#v", x) + } + case reflect.Func: + io.WriteString(p, v.Type().String()) + io.WriteString(p, " {...}") + case reflect.UnsafePointer: + p.printInline(v, v.Pointer(), showType) + case reflect.Invalid: + io.WriteString(p, "nil") + } +} + +func canInline(t reflect.Type) bool { + switch t.Kind() { + case reflect.Map: + return !canExpand(t.Elem()) + case reflect.Struct: + for i := 0; i < t.NumField(); i++ { + if canExpand(t.Field(i).Type) { + return false + } + } + return true + case reflect.Interface: + return false + case reflect.Array, reflect.Slice: + return !canExpand(t.Elem()) + case reflect.Ptr: + return false + case reflect.Chan, reflect.Func, reflect.UnsafePointer: + return false + } + return true +} + +func canExpand(t reflect.Type) bool { + switch t.Kind() { + case reflect.Map, reflect.Struct, + reflect.Interface, reflect.Array, reflect.Slice, + reflect.Ptr: + return true + } + return false +} + +func labelType(t reflect.Type) bool { + switch t.Kind() { + case reflect.Interface, reflect.Struct: + return true + } + return false +} + +func (p *printer) fmtString(s string, quote bool) { + if quote { + s = strconv.Quote(s) + } + io.WriteString(p, s) +} + +func writeByte(w io.Writer, b byte) { + w.Write([]byte{b}) +} + +func getField(v reflect.Value, i int) reflect.Value { + val := v.Field(i) + if val.Kind() == reflect.Interface && !val.IsNil() { + val = val.Elem() + } + return val +} diff --git a/snaps/internal/formatter/formatter_test.go b/snaps/internal/formatter/formatter_test.go new file mode 100644 index 0000000..e346c78 --- /dev/null +++ b/snaps/internal/formatter/formatter_test.go @@ -0,0 +1,328 @@ +package formatter + +import ( + "fmt" + "io" + "strings" + "testing" + "unsafe" +) + +type test struct { + v interface{} + s string +} + +type passtest struct { + v interface{} + f, s string +} + +type LongStructTypeName struct { + longFieldName interface{} + otherLongFieldName interface{} +} + +type SA struct { + t *T + v T +} + +type T struct { + x, y int +} + +type F int + +func (f F) Format(s fmt.State, c rune) { + fmt.Fprintf(s, "F(%d)", int(f)) +} + +type Stringer struct{ i int } + +func (s *Stringer) String() string { return "foo" } + +var long = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +var passthrough = []passtest{ + {1, "%d", "1"}, + {"a", "%s", "a"}, + {&Stringer{}, "%s", "foo"}, +} + +func TestPassthrough(t *testing.T) { + for _, tt := range passthrough { + s := fmt.Sprintf(tt.f, Formatter(tt.v)) + if tt.s != s { + t.Errorf("expected %q", tt.s) + t.Errorf("got %q", s) + t.Errorf("expraw\n%s", tt.s) + t.Errorf("gotraw\n%s", s) + } + } +} + +var gosyntax = []test{ + {nil, `nil`}, + {"", `""`}, + {"a", `"a"`}, + {1, "int(1)"}, + {1.0, "float64(1)"}, + {[]int(nil), "[]int(nil)"}, + {[0]int{}, "[0]int{}"}, + {complex(1, 0), "(1+0i)"}, + //{make(chan int), "(chan int)(0x1234)"}, + {unsafe.Pointer(uintptr(unsafe.Pointer(&long))), fmt.Sprintf("unsafe.Pointer(0x%02x)", uintptr(unsafe.Pointer(&long)))}, + {func(int) {}, "func(int) {...}"}, + {map[int]int{1: 1}, "map[int]int{1:1}"}, + {int32(1), "int32(1)"}, + {io.EOF, `&errors.errorString{s:"EOF"}`}, + {[]string{"a"}, `[]string{"a"}`}, + { + []string{long}, + `[]string{"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"}`, + }, + {F(5), "formatter.F(5)"}, + { + SA{&T{1, 2}, T{3, 4}}, + `formatter.SA{ + t: &formatter.T{x:1, y:2}, + v: formatter.T{x:3, y:4}, +}`, + }, + { + map[int][]byte{1: {}}, + `map[int][]uint8{ + 1: {}, +}`, + }, + { + map[int]T{1: {}}, + `map[int]formatter.T{ + 1: {}, +}`, + }, + { + long, + `"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"`, + }, + { + LongStructTypeName{ + longFieldName: LongStructTypeName{}, + otherLongFieldName: long, + }, + `formatter.LongStructTypeName{ + longFieldName: formatter.LongStructTypeName{}, + otherLongFieldName: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", +}`, + }, + { + &LongStructTypeName{ + longFieldName: &LongStructTypeName{}, + otherLongFieldName: (*LongStructTypeName)(nil), + }, + `&formatter.LongStructTypeName{ + longFieldName: &formatter.LongStructTypeName{}, + otherLongFieldName: (*formatter.LongStructTypeName)(nil), +}`, + }, + { + []LongStructTypeName{ + {nil, nil}, + {3, 3}, + {long, nil}, + }, + `[]formatter.LongStructTypeName{ + {}, + { + longFieldName: int(3), + otherLongFieldName: int(3), + }, + { + longFieldName: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + otherLongFieldName: nil, + }, +}`, + }, + { + []interface{}{ + LongStructTypeName{nil, nil}, + []byte{1, 2, 3}, + T{3, 4}, + LongStructTypeName{long, nil}, + }, + `[]interface {}{ + formatter.LongStructTypeName{}, + []uint8{0x1, 0x2, 0x3}, + formatter.T{x:3, y:4}, + formatter.LongStructTypeName{ + longFieldName: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + otherLongFieldName: nil, + }, +}`, + }, +} + +func TestGoSyntax(t *testing.T) { + for _, tt := range gosyntax { + s := fmt.Sprintf("%# v", Formatter(tt.v)) + if tt.s != s { + t.Errorf("expected %q", tt.s) + t.Errorf("got %q", s) + t.Errorf("expraw\n%s", tt.s) + t.Errorf("gotraw\n%s", s) + } + } +} + +type I struct { + i int + R interface{} +} + +func (i *I) I() *I { return i.R.(*I) } + +func TestCycle(t *testing.T) { + type A struct{ *A } + v := &A{} + v.A = v + + // panics from stack overflow without cycle detection + t.Logf("Example cycle:\n%# v", Formatter(v)) + + p := &A{} + s := fmt.Sprintf("%# v", Formatter([]*A{p, p})) + if strings.Contains(s, "CYCLIC") { + t.Errorf("Repeated address detected as cyclic reference:\n%s", s) + } + + type R struct { + i int + *R + } + r := &R{ + i: 1, + R: &R{ + i: 2, + R: &R{ + i: 3, + }, + }, + } + r.R.R.R = r + t.Logf("Example longer cycle:\n%# v", Formatter(r)) + + r = &R{ + i: 1, + R: &R{ + i: 2, + R: &R{ + i: 3, + R: &R{ + i: 4, + R: &R{ + i: 5, + R: &R{ + i: 6, + R: &R{ + i: 7, + R: &R{ + i: 8, + R: &R{ + i: 9, + R: &R{ + i: 10, + R: &R{ + i: 11, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + // here be pirates + r.R.R.R.R.R.R.R.R.R.R.R = r + t.Logf("Example very long cycle:\n%# v", Formatter(r)) + + i := &I{ + i: 1, + R: &I{ + i: 2, + R: &I{ + i: 3, + R: &I{ + i: 4, + R: &I{ + i: 5, + R: &I{ + i: 6, + R: &I{ + i: 7, + R: &I{ + i: 8, + R: &I{ + i: 9, + R: &I{ + i: 10, + R: &I{ + i: 11, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + iv := i.I().I().I().I().I().I().I().I().I().I() + *iv = *i + t.Logf("Example long interface cycle:\n%# v", Formatter(i)) +} + +func TestStructFieldTags1(t *testing.T) { + type testFieldTagCase struct { + tc interface{} + want string + } + type testFieldTagT struct { + z string `go-snaps:"-"` + x, y int + } + type testFieldTagT2 struct { + x, y int `go-snaps:"-"` + z string + } + type testFieldTagT3 struct { + x, y int + private string `go-snaps:"-"` + a, b, c int + } + type testFieldTagT4 struct { + fields []interface{} + } + for _, tc := range []testFieldTagCase{ + {tc: testFieldTagT{x: 1, y: 2, z: "ignored"}, want: `formatter.testFieldTagT{x:1, y:2}`}, + {tc: testFieldTagT2{x: 1, y: 2, z: "displayed"}, want: `formatter.testFieldTagT2{z:"displayed"}`}, + {tc: testFieldTagT3{x: 1, y: 2, private: "displayed", a: 42, b: 3, c: 4}, want: `formatter.testFieldTagT3{x:1, y:2, a:42, b:3, c:4}`}, + {tc: testFieldTagT4{fields: []interface{}{testFieldTagT3{x: 1, y: 2, private: "displayed", a: 42, b: 3, c: 4}}}, want: `formatter.testFieldTagT4{ + fields: { + formatter.testFieldTagT3{x:1, y:2, a:42, b:3, c:4}, + }, +}`}, + } { + got := Sprint(tc.tc) + if got != tc.want { + t.Logf("Got: %q", got) + t.Logf("Wanted: %q", tc.want) + t.Fail() + } + } +} diff --git a/snaps/internal/formatter/sprint.go b/snaps/internal/formatter/sprint.go new file mode 100644 index 0000000..dcde0d4 --- /dev/null +++ b/snaps/internal/formatter/sprint.go @@ -0,0 +1,23 @@ +package formatter + +import ( + "fmt" + "reflect" +) + +// Sprint is a convenience wrapper for fmt.Sprintf. +// +// Calling Sprint(x, y) is equivalent to +// fmt.Sprint(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Sprint(a ...interface{}) string { + return fmt.Sprint(wrap(a, true)...) +} + +func wrap(a []interface{}, force bool) []interface{} { + w := make([]interface{}, len(a)) + for i, x := range a { + w[i] = formatter{v: reflect.ValueOf(x), force: force} + } + return w +} diff --git a/snaps/internal/formatter/zero.go b/snaps/internal/formatter/zero.go new file mode 100644 index 0000000..a1b3be4 --- /dev/null +++ b/snaps/internal/formatter/zero.go @@ -0,0 +1,39 @@ +package formatter + +import "reflect" + +func nonzero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Bool: + return v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() != 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() != 0 + case reflect.Float32, reflect.Float64: + return v.Float() != 0 + case reflect.Complex64, reflect.Complex128: + return v.Complex() != complex(0, 0) + case reflect.String: + return v.String() != "" + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + if nonzero(getField(v, i)) { + return true + } + } + return false + case reflect.Array: + for i := 0; i < v.Len(); i++ { + if nonzero(v.Index(i)) { + return true + } + } + return false + case reflect.Map, reflect.Interface, reflect.Slice, reflect.Ptr, reflect.Chan, reflect.Func: + return !v.IsNil() + case reflect.UnsafePointer: + return v.Pointer() != 0 + } + return true +} diff --git a/snaps/matchSnapshot.go b/snaps/matchSnapshot.go index f889250..ba06b65 100644 --- a/snaps/matchSnapshot.go +++ b/snaps/matchSnapshot.go @@ -2,9 +2,9 @@ package snaps import ( "errors" + "github.com/gkampitakis/go-snaps/snaps/internal/formatter" "github.com/gkampitakis/go-snaps/snaps/internal/colors" - "github.com/kr/pretty" ) // MatchSnapshot verifies the values match the most recent snap file @@ -77,7 +77,7 @@ func takeSnapshot(objects []interface{}) string { var snapshot string for i := 0; i < len(objects); i++ { - snapshot += pretty.Sprint(objects[i]) + "\n" + snapshot += formatter.Sprint(objects[i]) + "\n" } return escapeEndChars(snapshot)