From a0c635c0aede2cb0c2f8bb0f0d70485fbdd11bd4 Mon Sep 17 00:00:00 2001 From: Daniel Schroeder Date: Tue, 7 May 2019 02:47:17 +0200 Subject: [PATCH 1/7] fix typo in Println description (#60) --- pretty.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pretty.go b/pretty.go index 49423ec..b4ca583 100644 --- a/pretty.go +++ b/pretty.go @@ -75,7 +75,7 @@ func Printf(format string, a ...interface{}) (n int, errno error) { // Println pretty-prints its operands and writes to standard output. // -// Calling Print(x, y) is equivalent to +// Calling Println(x, y) is equivalent to // fmt.Println(Formatter(x), Formatter(y)), but each operand is // formatted with "%# v". func Println(a ...interface{}) (n int, errno error) { From 71e7e49937503c662b9b636fd6b2c14b1aa818a5 Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Sat, 20 Jul 2019 12:14:28 +0200 Subject: [PATCH 2/7] fix ineffectual assignment of showTypeInStruct (#61) It's enough to declare and assign it when we actually need it a couple of lines below. --- formatter.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/formatter.go b/formatter.go index a317d7b..df61d8d 100644 --- a/formatter.go +++ b/formatter.go @@ -125,7 +125,6 @@ func (p *printer) printValue(v reflect.Value, showType, quote bool) { } keys := v.MapKeys() for i := 0; i < v.Len(); i++ { - showTypeInStruct := true k := keys[i] mv := v.MapIndex(k) pp.printValue(k, false, true) @@ -133,7 +132,7 @@ func (p *printer) printValue(v reflect.Value, showType, quote bool) { if expand { writeByte(pp, '\t') } - showTypeInStruct = t.Elem().Kind() == reflect.Interface + showTypeInStruct := t.Elem().Kind() == reflect.Interface pp.printValue(mv, showTypeInStruct, true) if expand { io.WriteString(pp, ",\n") From 4e0886370c3a67530192c6a238cff68f56c141b0 Mon Sep 17 00:00:00 2001 From: Paul Jolly Date: Tue, 31 Dec 2019 23:40:08 +0000 Subject: [PATCH 3/7] Fix MIT license for pkg.go.dev; tidy go.mod and create go.sum (#62) Currently the documentation for github.com/kr/pretty is not displayed on pkg.go.dev because of license restrictions: https://pkg.go.dev/github.com/kr/pretty?tab=doc This is almost certainly because the License file includes an errant header: The MIT License (MIT) The actual MIT license does not include this header: https://opensource.org/licenses/MIT GitHub and other interfaces surface the type of the license through automatic detection (as will pkg.go.dev). Hence this header is superfluous in any case. Whilst we are at it, tidy up go.mod and add go.sum. --- License | 2 -- go.mod | 6 ++++-- go.sum | 3 +++ 3 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 go.sum diff --git a/License b/License index 05c783c..480a328 100644 --- a/License +++ b/License @@ -1,5 +1,3 @@ -The MIT License (MIT) - Copyright 2012 Keith Rarick Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/go.mod b/go.mod index 1e29533..9a27b6e 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ -module "github.com/kr/pretty" +module github.com/kr/pretty -require "github.com/kr/text" v0.1.0 +go 1.12 + +require github.com/kr/text v0.1.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..714f82a --- /dev/null +++ b/go.sum @@ -0,0 +1,3 @@ +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= From ead452280cd055b2ae8a7f0db5eb37a878d902f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert-Andr=C3=A9=20Mauchin?= <30413512+eclipseo@users.noreply.github.com> Date: Wed, 29 Jul 2020 06:02:43 +0200 Subject: [PATCH 4/7] Convert int to string using rune() (#66) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See https://github.com/golang/go/issues/32479 Fix #65. Signed-off-by: Robert-André Mauchin --- formatter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/formatter.go b/formatter.go index df61d8d..bf4b598 100644 --- a/formatter.go +++ b/formatter.go @@ -37,7 +37,7 @@ func (fo formatter) passThrough(f fmt.State, c rune) { s := "%" for i := 0; i < 128; i++ { if f.Flag(i) { - s += string(i) + s += string(rune(i)) } } if w, ok := f.Width(); ok { From 44035784bff0f01338f57bad55b2d91364b5b537 Mon Sep 17 00:00:00 2001 From: James Henstridge Date: Mon, 10 Aug 2020 15:43:38 +0800 Subject: [PATCH 5/7] diff: detect cycles in the values being compared. (#64) This prevents infinite recursion, similar to the fixes to the formatter in #13. In addition to detecting cycles, we also check that the two values contain the same cyclic structure. --- diff.go | 32 ++++++++++++++++++++++++- diff_test.go | 68 ++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 87 insertions(+), 13 deletions(-) diff --git a/diff.go b/diff.go index 6aa7f74..40a09dc 100644 --- a/diff.go +++ b/diff.go @@ -41,7 +41,12 @@ type Printfer interface { // It calls Printf once for each difference, with no trailing newline. // The standard library log.Logger is a Printfer. func Pdiff(p Printfer, a, b interface{}) { - diffPrinter{w: p}.diff(reflect.ValueOf(a), reflect.ValueOf(b)) + d := diffPrinter{ + w: p, + aVisited: make(map[visit]visit), + bVisited: make(map[visit]visit), + } + d.diff(reflect.ValueOf(a), reflect.ValueOf(b)) } type Logfer interface { @@ -66,6 +71,9 @@ func Ldiff(l Logfer, a, b interface{}) { type diffPrinter struct { w Printfer l string // label + + aVisited map[visit]visit + bVisited map[visit]visit } func (w diffPrinter) printf(f string, a ...interface{}) { @@ -96,6 +104,28 @@ func (w diffPrinter) diff(av, bv reflect.Value) { return } + if av.CanAddr() && bv.CanAddr() { + avis := visit{av.UnsafeAddr(), at} + bvis := visit{bv.UnsafeAddr(), bt} + var cycle bool + + // Have we seen this value before? + if vis, ok := w.aVisited[avis]; ok { + cycle = true + if vis != bvis { + w.printf("%# v (previously visited) != %# v", formatter{v: av, quote: true}, formatter{v: bv, quote: true}) + } + } else if _, ok := w.bVisited[bvis]; ok { + cycle = true + w.printf("%# v != %# v (previously visited)", formatter{v: av, quote: true}, formatter{v: bv, quote: true}) + } + w.aVisited[avis] = bvis + w.bVisited[bvis] = avis + if cycle { + return + } + } + switch kind := at.Kind(); kind { case reflect.Bool: if a, b := av.Bool(), bv.Bool(); a != b { diff --git a/diff_test.go b/diff_test.go index a951e4b..08ec64b 100644 --- a/diff_test.go +++ b/diff_test.go @@ -130,20 +130,23 @@ var diffs = []difftest{ func TestDiff(t *testing.T) { for _, tt := range diffs { - got := Diff(tt.a, tt.b) - eq := len(got) == len(tt.exp) - if eq { - for i := range got { - eq = eq && got[i] == tt.exp[i] - } - } - if !eq { - t.Errorf("diffing % #v", tt.a) - t.Errorf("with % #v", tt.b) - diffdiff(t, got, tt.exp) - continue + expectDiffOutput(t, tt.a, tt.b, tt.exp) + } +} + +func expectDiffOutput(t *testing.T, a, b interface{}, exp []string) { + got := Diff(a, b) + eq := len(got) == len(exp) + if eq { + for i := range got { + eq = eq && got[i] == exp[i] } } + if !eq { + t.Errorf("diffing % #v", a) + t.Errorf("with % #v", b) + diffdiff(t, got, exp) + } } func TestKeyEqual(t *testing.T) { @@ -193,6 +196,47 @@ func TestFdiff(t *testing.T) { } } +func TestDiffCycle(t *testing.T) { + // Diff two cyclic structs + a := &I{i: 1, R: nil} + a.R = a + b := &I{i: 2, R: nil} + b.R = b + expectDiffOutput(t, a, b, []string{ + `i: 1 != 2`, + }) + + // Diff two equal cyclic structs + b.i = 1 + expectDiffOutput(t, a, b, []string{}) + + // Diff two structs with different cycles + b2 := &I{i: 1, R: b} + b.R = b2 + expectDiffOutput(t, a, b, []string{`R: pretty.I{ + i: 1, + R: &pretty.I{(CYCLIC REFERENCE)}, +} (previously visited) != pretty.I{ + i: 1, + R: &pretty.I{ + i: 1, + R: &pretty.I{(CYCLIC REFERENCE)}, + }, +}`}) + + // ... and the same in the other direction + expectDiffOutput(t, b, a, []string{`R: pretty.I{ + i: 1, + R: &pretty.I{ + i: 1, + R: &pretty.I{(CYCLIC REFERENCE)}, + }, +} != pretty.I{ + i: 1, + R: &pretty.I{(CYCLIC REFERENCE)}, +} (previously visited)`}) +} + func diffdiff(t *testing.T, got, exp []string) { minus(t, "unexpected:", got, exp) minus(t, "missing:", exp, got) From 814ac30b4b181fedaa28ed1b6763ee87f0096826 Mon Sep 17 00:00:00 2001 From: James Henstridge Date: Mon, 10 Aug 2020 15:44:40 +0800 Subject: [PATCH 6/7] .github: add a workflow to build and test pushes and pull requests (#63) --- .github/workflows/build-test.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/build-test.yml diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 0000000..db23318 --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,17 @@ +name: build-test +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v1 + with: + go-version: 1.13.x + - uses: actions/checkout@v2 + - name: Build + run: go build . + - name: Test + run: go test -v . From 868cc3a4a27d84a80b604bb8a7a2f9fcb1475af0 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Ruel Date: Wed, 19 Aug 2015 14:06:28 -0400 Subject: [PATCH 7/7] Sort map keys. Expand maps unconditionally when len() > 2 Implements a new function less() to compare two map key value. Include 92% of test coverage of this function. --- formatter.go | 88 ++++++++++++++++++++++++++++++++++++++++++++--- formatter_test.go | 67 ++++++++++++++++++++++++++++++++++-- 2 files changed, 148 insertions(+), 7 deletions(-) diff --git a/formatter.go b/formatter.go index bf4b598..b29ac8f 100644 --- a/formatter.go +++ b/formatter.go @@ -4,6 +4,8 @@ import ( "fmt" "io" "reflect" + "runtime" + "sort" "strconv" "text/tabwriter" @@ -117,15 +119,15 @@ func (p *printer) printValue(v reflect.Value, showType, quote bool) { } writeByte(p, '{') if nonzero(v) { - expand := !canInline(v.Type()) + expand := !canInline(v.Type()) || v.Len() > 2 pp := p if expand { writeByte(p, '\n') pp = p.indent() } - keys := v.MapKeys() - for i := 0; i < v.Len(); i++ { - k := keys[i] + keys := sortableValues(v.MapKeys()) + sort.Sort(keys) + for i, k := range keys { mv := v.MapIndex(k) pp.printValue(k, false, true) writeByte(pp, ':') @@ -266,6 +268,84 @@ func (p *printer) printValue(v reflect.Value, showType, quote bool) { } } +// sortableValues implement sort.Interface for reflect.Value to sort map keys. +type sortableValues []reflect.Value + +func (s sortableValues) Len() int { return len(s) } +func (s sortableValues) Less(i, j int) bool { return less(s[i], s[j]) } +func (s sortableValues) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +func less(i, j reflect.Value) bool { + kind := i.Kind() + if kind != j.Kind() { + panic("different kinds in same slice, unexpected") + } + switch i.Kind() { + case reflect.Bool: + return !i.Bool() && j.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return i.Int() < j.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return i.Uint() < j.Uint() + case reflect.Float32, reflect.Float64: + return i.Float() < j.Float() + case reflect.Complex64, reflect.Complex128: + ii := i.Complex() + jj := j.Complex() + if real(ii) < real(jj) { + return true + } + if real(ii) > real(jj) { + return false + } + return imag(ii) < imag(jj) + case reflect.String: + return i.String() < j.String() + case reflect.Array, reflect.Slice: + // TODO(maruel): Use Less(i, j int) bool when available. + l := i.Len() + if ll := j.Len(); ll < l { + l = ll + } + for index := 0; index < l; index++ { + ii := i.Index(index) + jj := j.Index(index) + if less(ii, jj) { + return true + } + if less(jj, ii) { + return false + } + } + return i.Len() < j.Len() + case reflect.Chan, reflect.Map: + return i.Len() < j.Len() + case reflect.Interface, reflect.Ptr: + ii := nonzero(i) + jj := nonzero(j) + if !ii { + return jj + } + if !jj { + return false + } + return less(i.Elem(), j.Elem()) + case reflect.Func: + return getFuncName(i) < getFuncName(j) + case reflect.Struct: + // TODO(maruel): Compare members one by one. + return false + case reflect.UnsafePointer: + fallthrough + default: + return i.Pointer() < j.Pointer() + } +} + +func getFuncName(i reflect.Value) string { + return runtime.FuncForPC(i.Pointer()).Name() +} + func canInline(t reflect.Type) bool { switch t.Kind() { case reflect.Map: diff --git a/formatter_test.go b/formatter_test.go index c8c0b51..13177db 100644 --- a/formatter_test.go +++ b/formatter_test.go @@ -3,6 +3,7 @@ package pretty import ( "fmt" "io" + "reflect" "strings" "testing" "unsafe" @@ -38,7 +39,7 @@ func (f F) Format(s fmt.State, c rune) { fmt.Fprintf(s, "F(%d)", int(f)) } -type Stringer struct { i int } +type Stringer struct{ i int } func (s *Stringer) String() string { return "foo" } @@ -91,15 +92,36 @@ var gosyntax = []test{ }`, }, { - map[int][]byte{1: {}}, + map[int]string{1: "a", 2: "b"}, + `map[int]string{1:"a", 2:"b"}`, + }, + { + map[int]string{1: "a", 2: "b", 3: "c"}, + `map[int]string{ + 1: "a", + 2: "b", + 3: "c", +}`, + }, + { + map[int][]byte{1: {}, 2: {}}, `map[int][]uint8{ 1: {}, + 2: {}, }`, }, { - map[int]T{1: {}}, + map[int]T{1: {}, 2: {}}, `map[int]pretty.T{ 1: {}, + 2: {}, +}`, + }, + { + map[string]T{"a": {}, "b": {}}, + `map[string]pretty.T{ + "a": {}, + "b": {}, }`, }, { @@ -286,3 +308,42 @@ func TestCycle(t *testing.T) { *iv = *i t.Logf("Example long interface cycle:\n%# v", Formatter(i)) } + +func TestLess(t *testing.T) { + zero := new(int) + one := new(int) + *one = 1 + data := []struct { + i, j interface{} + less bool + }{ + {true, true, false}, + {false, true, true}, + {uint(0), uint(0), false}, + {uint(0), uint(1), true}, + {0., 0., false}, + {0., 1., true}, + {complex(0., 0.), complex(0., 0.), false}, + {complex(0., 0.), complex(1., 0.), true}, + {complex(1., 0.), complex(0., 0.), false}, + {[]int{0, 0}, []int{0}, false}, + {[]int{0, 0}, []int{1}, true}, + {[]int{1}, []int{0}, false}, + {map[int]int{}, map[int]int{0: 0}, true}, + //{interface{}(0), interface{}(0), false}, + {TestLess, TestCycle, false}, + {TestCycle, TestLess, true}, + {zero, zero, false}, + {zero, one, true}, + {zero, (*int)(nil), false}, + {(*int)(nil), (*int)(nil), false}, + {(*int)(nil), zero, true}, + } + for _, line := range data { + if less(reflect.ValueOf(line.i), reflect.ValueOf(line.j)) != line.less { + t.Errorf("less(%q, %q) != %t", line.i, line.j, line.less) + } + } + // Return value is non-deterministic. Just ensure it doesn't crash. + less(reflect.ValueOf(struct{}{}), reflect.ValueOf(struct{}{})) +}