├── go.mod ├── pretty ├── .gitignore ├── doc.go ├── public_test.go ├── structure.go ├── public.go ├── reflect.go ├── structure_test.go ├── examples_test.go └── reflect_test.go ├── .travis.yml ├── README.md ├── diff ├── diff.go └── diff_test.go └── LICENSE /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kylelemons/godebug 2 | 3 | go 1.11 4 | -------------------------------------------------------------------------------- /pretty/.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | *.bench 3 | *.golden 4 | *.txt 5 | *.prof 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.14.x" 5 | - "1.15.x" 6 | 7 | arch: 8 | - amd64 9 | - arm64 10 | - ppc64le 11 | -------------------------------------------------------------------------------- /pretty/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package pretty pretty-prints Go structures. 16 | // 17 | // This package uses reflection to examine a Go value and can 18 | // print out in a nice, aligned fashion. It supports three 19 | // modes (normal, compact, and extended) for advanced use. 20 | // 21 | // See the Reflect and Print examples for what the output looks like. 22 | package pretty 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Pretty Printing for Go 2 | ====================== 3 | 4 | [![godebug build status][ciimg]][ci] 5 | 6 | Have you ever wanted to get a pretty-printed version of a Go data structure, 7 | complete with indentation? I have found this especially useful in unit tests 8 | and in debugging my code, and thus godebug was born! 9 | 10 | [ciimg]: https://travis-ci.org/kylelemons/godebug.svg?branch=master 11 | [ci]: https://travis-ci.org/kylelemons/godebug 12 | 13 | Quick Examples 14 | -------------- 15 | 16 | By default, pretty will write out a very compact representation of a data structure. 17 | From the [Print example][printex]: 18 | 19 | ``` 20 | {Name: "Spaceship Heart of Gold", 21 | Crew: {Arthur Dent: "Along for the Ride", 22 | Ford Prefect: "A Hoopy Frood", 23 | Trillian: "Human", 24 | Zaphod Beeblebrox: "Galactic President"}, 25 | Androids: 1, 26 | Stolen: true} 27 | ``` 28 | 29 | It can also produce a much more verbose, one-item-per-line representation suitable for 30 | [computing diffs][diffex]. See the documentation for more examples and customization. 31 | 32 | [printex]: https://godoc.org/github.com/kylelemons/godebug/pretty#example-Print 33 | [diffex]: https://godoc.org/github.com/kylelemons/godebug/pretty#example-Compare 34 | 35 | Documentation 36 | ------------- 37 | 38 | Documentation for this package is available at [godoc.org][doc]: 39 | 40 | * Pretty: [![godoc for godebug/pretty][prettyimg]][prettydoc] 41 | * Diff: [![godoc for godebug/diff][diffimg]][diffdoc] 42 | 43 | [doc]: https://godoc.org/ 44 | [prettyimg]: https://godoc.org/github.com/kylelemons/godebug/pretty?status.png 45 | [prettydoc]: https://godoc.org/github.com/kylelemons/godebug/pretty 46 | [diffimg]: https://godoc.org/github.com/kylelemons/godebug/diff?status.png 47 | [diffdoc]: https://godoc.org/github.com/kylelemons/godebug/diff 48 | 49 | Installation 50 | ------------ 51 | 52 | These packages are available via `go get`: 53 | 54 | ```bash 55 | $ go get -u github.com/kylelemons/godebug/{pretty,diff} 56 | ``` 57 | 58 | Other Packages 59 | -------------- 60 | 61 | If `godebug/pretty` is not granular enough, I highly recommend 62 | checking out [cmp][cmp] or [go-spew][spew]. 63 | 64 | [cmp]: https://godoc.org/github.com/google/go-cmp/cmp 65 | [spew]: http://godoc.org/github.com/davecgh/go-spew/spew 66 | -------------------------------------------------------------------------------- /pretty/public_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pretty 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | ) 21 | 22 | func TestDiff(t *testing.T) { 23 | type example struct { 24 | Name string 25 | Age int 26 | Friends []string 27 | } 28 | 29 | tests := []struct { 30 | desc string 31 | got, want interface{} 32 | diff string 33 | }{ 34 | { 35 | desc: "basic struct", 36 | got: example{ 37 | Name: "Zaphd", 38 | Age: 42, 39 | Friends: []string{ 40 | "Ford Prefect", 41 | "Trillian", 42 | "Marvin", 43 | }, 44 | }, 45 | want: example{ 46 | Name: "Zaphod", 47 | Age: 42, 48 | Friends: []string{ 49 | "Ford Prefect", 50 | "Trillian", 51 | }, 52 | }, 53 | diff: ` { 54 | - Name: "Zaphd", 55 | + Name: "Zaphod", 56 | Age: 42, 57 | Friends: [ 58 | "Ford Prefect", 59 | "Trillian", 60 | - "Marvin", 61 | ], 62 | }`, 63 | }, 64 | } 65 | 66 | for _, test := range tests { 67 | if got, want := Compare(test.got, test.want), test.diff; got != want { 68 | t.Errorf("%s:", test.desc) 69 | t.Errorf(" got: %q", got) 70 | t.Errorf(" want: %q", want) 71 | } 72 | } 73 | } 74 | 75 | func TestSkipZeroFields(t *testing.T) { 76 | type example struct { 77 | Name string 78 | Species string 79 | Age int 80 | Friends []string 81 | } 82 | 83 | tests := []struct { 84 | desc string 85 | got, want interface{} 86 | diff string 87 | }{ 88 | { 89 | desc: "basic struct", 90 | got: example{ 91 | Name: "Zaphd", 92 | Species: "Betelgeusian", 93 | Age: 42, 94 | }, 95 | want: example{ 96 | Name: "Zaphod", 97 | Species: "Betelgeusian", 98 | Age: 42, 99 | Friends: []string{ 100 | "Ford Prefect", 101 | "Trillian", 102 | "", 103 | }, 104 | }, 105 | diff: ` { 106 | - Name: "Zaphd", 107 | + Name: "Zaphod", 108 | Species: "Betelgeusian", 109 | Age: 42, 110 | + Friends: [ 111 | + "Ford Prefect", 112 | + "Trillian", 113 | + "", 114 | + ], 115 | }`, 116 | }, 117 | } 118 | 119 | cfg := *CompareConfig 120 | cfg.SkipZeroFields = true 121 | 122 | for _, test := range tests { 123 | if got, want := cfg.Compare(test.got, test.want), test.diff; got != want { 124 | t.Errorf("%s:", test.desc) 125 | t.Errorf(" got: %q", got) 126 | t.Errorf(" want: %q", want) 127 | } 128 | } 129 | } 130 | 131 | func TestRegressions(t *testing.T) { 132 | tests := []struct { 133 | issue string 134 | config *Config 135 | value interface{} 136 | want string 137 | }{ 138 | { 139 | issue: "kylelemons/godebug#13", 140 | config: &Config{ 141 | PrintStringers: true, 142 | }, 143 | value: struct{ Day *time.Weekday }{}, 144 | want: "{Day: nil}", 145 | }, 146 | } 147 | 148 | for _, test := range tests { 149 | t.Run(test.issue, func(t *testing.T) { 150 | if got, want := test.config.Sprint(test.value), test.want; got != want { 151 | t.Errorf("%#v.Sprint(%#v) = %q, want %q", test.config, test.value, got, want) 152 | } 153 | }) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /pretty/structure.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pretty 16 | 17 | import ( 18 | "bufio" 19 | "bytes" 20 | "fmt" 21 | "io" 22 | "strconv" 23 | "strings" 24 | ) 25 | 26 | // a formatter stores stateful formatting information as well as being 27 | // an io.Writer for simplicity. 28 | type formatter struct { 29 | *bufio.Writer 30 | *Config 31 | 32 | // Self-referential structure tracking 33 | tagNumbers map[int]int // tagNumbers[id] = <#n> 34 | } 35 | 36 | // newFormatter creates a new buffered formatter. For the output to be written 37 | // to the given writer, this must be accompanied by a call to write (or Flush). 38 | func newFormatter(cfg *Config, w io.Writer) *formatter { 39 | return &formatter{ 40 | Writer: bufio.NewWriter(w), 41 | Config: cfg, 42 | tagNumbers: make(map[int]int), 43 | } 44 | } 45 | 46 | func (f *formatter) write(n node) { 47 | defer f.Flush() 48 | n.format(f, "") 49 | } 50 | 51 | func (f *formatter) tagFor(id int) int { 52 | if tag, ok := f.tagNumbers[id]; ok { 53 | return tag 54 | } 55 | if f.tagNumbers == nil { 56 | return 0 57 | } 58 | tag := len(f.tagNumbers) + 1 59 | f.tagNumbers[id] = tag 60 | return tag 61 | } 62 | 63 | type node interface { 64 | format(f *formatter, indent string) 65 | } 66 | 67 | func (f *formatter) compactString(n node) string { 68 | switch k := n.(type) { 69 | case stringVal: 70 | return string(k) 71 | case rawVal: 72 | return string(k) 73 | } 74 | 75 | buf := new(bytes.Buffer) 76 | f2 := newFormatter(&Config{Compact: true}, buf) 77 | f2.tagNumbers = f.tagNumbers // reuse tagNumbers just in case 78 | f2.write(n) 79 | return buf.String() 80 | } 81 | 82 | type stringVal string 83 | 84 | func (str stringVal) format(f *formatter, indent string) { 85 | f.WriteString(strconv.Quote(string(str))) 86 | } 87 | 88 | type rawVal string 89 | 90 | func (r rawVal) format(f *formatter, indent string) { 91 | f.WriteString(string(r)) 92 | } 93 | 94 | type keyval struct { 95 | key string 96 | val node 97 | } 98 | 99 | type keyvals []keyval 100 | 101 | func (l keyvals) format(f *formatter, indent string) { 102 | f.WriteByte('{') 103 | 104 | switch { 105 | case f.Compact: 106 | // All on one line: 107 | for i, kv := range l { 108 | if i > 0 { 109 | f.WriteByte(',') 110 | } 111 | f.WriteString(kv.key) 112 | f.WriteByte(':') 113 | kv.val.format(f, indent) 114 | } 115 | case f.Diffable: 116 | f.WriteByte('\n') 117 | inner := indent + " " 118 | // Each value gets its own line: 119 | for _, kv := range l { 120 | f.WriteString(inner) 121 | f.WriteString(kv.key) 122 | f.WriteString(": ") 123 | kv.val.format(f, inner) 124 | f.WriteString(",\n") 125 | } 126 | f.WriteString(indent) 127 | default: 128 | keyWidth := 0 129 | for _, kv := range l { 130 | if kw := len(kv.key); kw > keyWidth { 131 | keyWidth = kw 132 | } 133 | } 134 | alignKey := indent + " " 135 | alignValue := strings.Repeat(" ", keyWidth) 136 | inner := alignKey + alignValue + " " 137 | // First and last line shared with bracket: 138 | for i, kv := range l { 139 | if i > 0 { 140 | f.WriteString(",\n") 141 | f.WriteString(alignKey) 142 | } 143 | f.WriteString(kv.key) 144 | f.WriteString(": ") 145 | f.WriteString(alignValue[len(kv.key):]) 146 | kv.val.format(f, inner) 147 | } 148 | } 149 | 150 | f.WriteByte('}') 151 | } 152 | 153 | type list []node 154 | 155 | func (l list) format(f *formatter, indent string) { 156 | if max := f.ShortList; max > 0 { 157 | short := f.compactString(l) 158 | if len(short) <= max { 159 | f.WriteString(short) 160 | return 161 | } 162 | } 163 | 164 | f.WriteByte('[') 165 | 166 | switch { 167 | case f.Compact: 168 | // All on one line: 169 | for i, v := range l { 170 | if i > 0 { 171 | f.WriteByte(',') 172 | } 173 | v.format(f, indent) 174 | } 175 | case f.Diffable: 176 | f.WriteByte('\n') 177 | inner := indent + " " 178 | // Each value gets its own line: 179 | for _, v := range l { 180 | f.WriteString(inner) 181 | v.format(f, inner) 182 | f.WriteString(",\n") 183 | } 184 | f.WriteString(indent) 185 | default: 186 | inner := indent + " " 187 | // First and last line shared with bracket: 188 | for i, v := range l { 189 | if i > 0 { 190 | f.WriteString(",\n") 191 | f.WriteString(inner) 192 | } 193 | v.format(f, inner) 194 | } 195 | } 196 | 197 | f.WriteByte(']') 198 | } 199 | 200 | type ref struct { 201 | id int 202 | } 203 | 204 | func (r ref) format(f *formatter, indent string) { 205 | fmt.Fprintf(f, "", f.tagFor(r.id)) 206 | } 207 | 208 | type target struct { 209 | id int 210 | value node 211 | } 212 | 213 | func (t target) format(f *formatter, indent string) { 214 | tag := fmt.Sprintf("<#%d> ", f.tagFor(t.id)) 215 | switch { 216 | case f.Diffable, f.Compact: 217 | // no indent changes 218 | default: 219 | indent += strings.Repeat(" ", len(tag)) 220 | } 221 | f.WriteString(tag) 222 | t.value.format(f, indent) 223 | } 224 | -------------------------------------------------------------------------------- /diff/diff.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package diff implements a linewise diff algorithm. 16 | package diff 17 | 18 | import ( 19 | "fmt" 20 | "strings" 21 | ) 22 | 23 | // Chunk represents a piece of the diff. A chunk will not have both added and 24 | // deleted lines. Equal lines are always after any added or deleted lines. 25 | // A Chunk may or may not have any lines in it, especially for the first or last 26 | // chunk in a computation. 27 | type Chunk struct { 28 | Added []string 29 | Deleted []string 30 | Equal []string 31 | } 32 | 33 | func (c *Chunk) empty() bool { 34 | return len(c.Added) == 0 && len(c.Deleted) == 0 && len(c.Equal) == 0 35 | } 36 | 37 | // Diff returns a string containing a line-by-line unified diff of the linewise 38 | // changes required to make A into B. Each line is prefixed with '+', '-', or 39 | // ' ' to indicate if it should be added, removed, or is correct respectively. 40 | func Diff(A, B string) string { 41 | aLines := strings.Split(A, "\n") 42 | bLines := strings.Split(B, "\n") 43 | return Render(DiffChunks(aLines, bLines)) 44 | } 45 | 46 | // Render renders the slice of chunks into a representation that prefixes 47 | // the lines with '+', '-', or ' ' depending on whether the line was added, 48 | // removed, or equal (respectively). 49 | func Render(chunks []Chunk) string { 50 | buf := new(strings.Builder) 51 | for _, c := range chunks { 52 | for _, line := range c.Added { 53 | fmt.Fprintf(buf, "+%s\n", line) 54 | } 55 | for _, line := range c.Deleted { 56 | fmt.Fprintf(buf, "-%s\n", line) 57 | } 58 | for _, line := range c.Equal { 59 | fmt.Fprintf(buf, " %s\n", line) 60 | } 61 | } 62 | return strings.TrimRight(buf.String(), "\n") 63 | } 64 | 65 | // DiffChunks uses an O(D(N+M)) shortest-edit-script algorithm 66 | // to compute the edits required from A to B and returns the 67 | // edit chunks. 68 | func DiffChunks(a, b []string) []Chunk { 69 | // algorithm: http://www.xmailserver.org/diff2.pdf 70 | 71 | // We'll need these quantities a lot. 72 | alen, blen := len(a), len(b) // M, N 73 | 74 | // At most, it will require len(a) deletions and len(b) additions 75 | // to transform a into b. 76 | maxPath := alen + blen // MAX 77 | if maxPath == 0 { 78 | // degenerate case: two empty lists are the same 79 | return nil 80 | } 81 | 82 | // Store the endpoint of the path for diagonals. 83 | // We store only the a index, because the b index on any diagonal 84 | // (which we know during the loop below) is aidx-diag. 85 | // endpoint[maxPath] represents the 0 diagonal. 86 | // 87 | // Stated differently: 88 | // endpoint[d] contains the aidx of a furthest reaching path in diagonal d 89 | endpoint := make([]int, 2*maxPath+1) // V 90 | 91 | saved := make([][]int, 0, 8) // Vs 92 | save := func() { 93 | dup := make([]int, len(endpoint)) 94 | copy(dup, endpoint) 95 | saved = append(saved, dup) 96 | } 97 | 98 | var editDistance int // D 99 | dLoop: 100 | for editDistance = 0; editDistance <= maxPath; editDistance++ { 101 | // The 0 diag(onal) represents equality of a and b. Each diagonal to 102 | // the left is numbered one lower, to the right is one higher, from 103 | // -alen to +blen. Negative diagonals favor differences from a, 104 | // positive diagonals favor differences from b. The edit distance to a 105 | // diagonal d cannot be shorter than d itself. 106 | // 107 | // The iterations of this loop cover either odds or evens, but not both, 108 | // If odd indices are inputs, even indices are outputs and vice versa. 109 | for diag := -editDistance; diag <= editDistance; diag += 2 { // k 110 | var aidx int // x 111 | switch { 112 | case diag == -editDistance: 113 | // This is a new diagonal; copy from previous iter 114 | aidx = endpoint[maxPath-editDistance+1] + 0 115 | case diag == editDistance: 116 | // This is a new diagonal; copy from previous iter 117 | aidx = endpoint[maxPath+editDistance-1] + 1 118 | case endpoint[maxPath+diag+1] > endpoint[maxPath+diag-1]: 119 | // diagonal d+1 was farther along, so use that 120 | aidx = endpoint[maxPath+diag+1] + 0 121 | default: 122 | // diagonal d-1 was farther (or the same), so use that 123 | aidx = endpoint[maxPath+diag-1] + 1 124 | } 125 | // On diagonal d, we can compute bidx from aidx. 126 | bidx := aidx - diag // y 127 | // See how far we can go on this diagonal before we find a difference. 128 | for aidx < alen && bidx < blen && a[aidx] == b[bidx] { 129 | aidx++ 130 | bidx++ 131 | } 132 | // Store the end of the current edit chain. 133 | endpoint[maxPath+diag] = aidx 134 | // If we've found the end of both inputs, we're done! 135 | if aidx >= alen && bidx >= blen { 136 | save() // save the final path 137 | break dLoop 138 | } 139 | } 140 | save() // save the current path 141 | } 142 | if editDistance == 0 { 143 | return nil 144 | } 145 | chunks := make([]Chunk, editDistance+1) 146 | 147 | x, y := alen, blen 148 | for d := editDistance; d > 0; d-- { 149 | endpoint := saved[d] 150 | diag := x - y 151 | insert := diag == -d || (diag != d && endpoint[maxPath+diag-1] < endpoint[maxPath+diag+1]) 152 | 153 | x1 := endpoint[maxPath+diag] 154 | var x0, xM, kk int 155 | if insert { 156 | kk = diag + 1 157 | x0 = endpoint[maxPath+kk] 158 | xM = x0 159 | } else { 160 | kk = diag - 1 161 | x0 = endpoint[maxPath+kk] 162 | xM = x0 + 1 163 | } 164 | y0 := x0 - kk 165 | 166 | var c Chunk 167 | if insert { 168 | c.Added = b[y0:][:1] 169 | } else { 170 | c.Deleted = a[x0:][:1] 171 | } 172 | if xM < x1 { 173 | c.Equal = a[xM:][:x1-xM] 174 | } 175 | 176 | x, y = x0, y0 177 | chunks[d] = c 178 | } 179 | if x > 0 { 180 | chunks[0].Equal = a[:x] 181 | } 182 | if chunks[0].empty() { 183 | chunks = chunks[1:] 184 | } 185 | if len(chunks) == 0 { 186 | return nil 187 | } 188 | return chunks 189 | } 190 | -------------------------------------------------------------------------------- /diff/diff_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package diff 16 | 17 | import ( 18 | "fmt" 19 | "reflect" 20 | "strings" 21 | "testing" 22 | ) 23 | 24 | func TestDiff(t *testing.T) { 25 | tests := []struct { 26 | desc string 27 | A, B []string 28 | chunks []Chunk 29 | }{ 30 | { 31 | desc: "nil", 32 | }, 33 | { 34 | desc: "empty", 35 | A: []string{}, 36 | B: []string{}, 37 | }, 38 | { 39 | desc: "same", 40 | A: []string{"foo"}, 41 | B: []string{"foo"}, 42 | }, 43 | { 44 | desc: "a empty", 45 | A: []string{}, 46 | }, 47 | { 48 | desc: "b empty", 49 | B: []string{}, 50 | }, 51 | { 52 | desc: "b nil", 53 | A: []string{"foo"}, 54 | chunks: []Chunk{ 55 | 0: {Deleted: []string{"foo"}}, 56 | }, 57 | }, 58 | { 59 | desc: "a nil", 60 | B: []string{"foo"}, 61 | chunks: []Chunk{ 62 | 0: {Added: []string{"foo"}}, 63 | }, 64 | }, 65 | { 66 | desc: "start with change", 67 | A: []string{"a", "b", "c"}, 68 | B: []string{"A", "b", "c"}, 69 | chunks: []Chunk{ 70 | 0: {Deleted: []string{"a"}}, 71 | 1: {Added: []string{"A"}, Equal: []string{"b", "c"}}, 72 | }, 73 | }, 74 | { 75 | desc: "constitution", 76 | A: []string{ 77 | "We the People of the United States, in Order to form a more perfect Union,", 78 | "establish Justice, insure domestic Tranquility, provide for the common defence,", 79 | "and secure the Blessings of Liberty to ourselves", 80 | "and our Posterity, do ordain and establish this Constitution for the United", 81 | "States of America.", 82 | }, 83 | B: []string{ 84 | "We the People of the United States, in Order to form a more perfect Union,", 85 | "establish Justice, insure domestic Tranquility, provide for the common defence,", 86 | "promote the general Welfare, and secure the Blessings of Liberty to ourselves", 87 | "and our Posterity, do ordain and establish this Constitution for the United", 88 | "States of America.", 89 | }, 90 | chunks: []Chunk{ 91 | 0: { 92 | Equal: []string{ 93 | "We the People of the United States, in Order to form a more perfect Union,", 94 | "establish Justice, insure domestic Tranquility, provide for the common defence,", 95 | }, 96 | }, 97 | 1: { 98 | Deleted: []string{ 99 | "and secure the Blessings of Liberty to ourselves", 100 | }, 101 | }, 102 | 2: { 103 | Added: []string{ 104 | "promote the general Welfare, and secure the Blessings of Liberty to ourselves", 105 | }, 106 | Equal: []string{ 107 | "and our Posterity, do ordain and establish this Constitution for the United", 108 | "States of America.", 109 | }, 110 | }, 111 | }, 112 | }, 113 | } 114 | 115 | for _, test := range tests { 116 | t.Run(test.desc, func(t *testing.T) { 117 | got := DiffChunks(test.A, test.B) 118 | if got, want := len(got), len(test.chunks); got != want { 119 | t.Errorf("edit distance = %v, want %v", got-1, want-1) 120 | return 121 | } 122 | for i := range got { 123 | got, want := got[i], test.chunks[i] 124 | if got, want := got.Added, want.Added; !reflect.DeepEqual(got, want) { 125 | t.Errorf("chunks[%d]: Added = %v, want %v", i, got, want) 126 | } 127 | if got, want := got.Deleted, want.Deleted; !reflect.DeepEqual(got, want) { 128 | t.Errorf("chunks[%d]: Deleted = %v, want %v", i, got, want) 129 | } 130 | if got, want := got.Equal, want.Equal; !reflect.DeepEqual(got, want) { 131 | t.Errorf("chunks[%d]: Equal = %v, want %v", i, got, want) 132 | } 133 | } 134 | }) 135 | } 136 | } 137 | 138 | func TestRender(t *testing.T) { 139 | tests := []struct { 140 | desc string 141 | chunks []Chunk 142 | out string 143 | }{ 144 | { 145 | desc: "ordering", 146 | chunks: []Chunk{ 147 | { 148 | Added: []string{"1"}, 149 | Deleted: []string{"2"}, 150 | Equal: []string{"3"}, 151 | }, 152 | { 153 | Added: []string{"4"}, 154 | Deleted: []string{"5"}, 155 | }, 156 | }, 157 | out: strings.TrimSpace(` 158 | +1 159 | -2 160 | 3 161 | +4 162 | -5 163 | `), 164 | }, 165 | { 166 | desc: "only_added", 167 | chunks: []Chunk{ 168 | { 169 | Added: []string{"1"}, 170 | }, 171 | }, 172 | out: strings.TrimSpace(` 173 | +1 174 | `), 175 | }, 176 | { 177 | desc: "only_deleted", 178 | chunks: []Chunk{ 179 | { 180 | Deleted: []string{"1"}, 181 | }, 182 | }, 183 | out: strings.TrimSpace(` 184 | -1 185 | `), 186 | }, 187 | } 188 | 189 | for _, test := range tests { 190 | t.Run(test.desc, func(t *testing.T) { 191 | if got, want := Render(test.chunks), test.out; got != want { 192 | t.Errorf("Render(%q):", test.chunks) 193 | t.Errorf("GOT\n%s", got) 194 | t.Errorf("WANT\n%s", want) 195 | } 196 | }) 197 | } 198 | } 199 | 200 | func ExampleDiff() { 201 | constitution := strings.TrimSpace(` 202 | We the People of the United States, in Order to form a more perfect Union, 203 | establish Justice, insure domestic Tranquility, provide for the common defence, 204 | promote the general Welfare, and secure the Blessings of Liberty to ourselves 205 | and our Posterity, do ordain and establish this Constitution for the United 206 | States of America. 207 | `) 208 | 209 | got := strings.TrimSpace(` 210 | :wq 211 | We the People of the United States, in Order to form a more perfect Union, 212 | establish Justice, insure domestic Tranquility, provide for the common defence, 213 | and secure the Blessings of Liberty to ourselves 214 | and our Posterity, do ordain and establish this Constitution for the United 215 | States of America. 216 | `) 217 | 218 | fmt.Println(Diff(got, constitution)) 219 | 220 | // Output: 221 | // -:wq 222 | // We the People of the United States, in Order to form a more perfect Union, 223 | // establish Justice, insure domestic Tranquility, provide for the common defence, 224 | // -and secure the Blessings of Liberty to ourselves 225 | // +promote the general Welfare, and secure the Blessings of Liberty to ourselves 226 | // and our Posterity, do ordain and establish this Constitution for the United 227 | // States of America. 228 | } 229 | -------------------------------------------------------------------------------- /pretty/public.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pretty 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io" 21 | "net" 22 | "reflect" 23 | "time" 24 | 25 | "github.com/kylelemons/godebug/diff" 26 | ) 27 | 28 | // A Config represents optional configuration parameters for formatting. 29 | // 30 | // Some options, notably ShortList, dramatically increase the overhead 31 | // of pretty-printing a value. 32 | type Config struct { 33 | // Verbosity options 34 | Compact bool // One-line output. Overrides Diffable. 35 | Diffable bool // Adds extra newlines for more easily diffable output. 36 | 37 | // Field and value options 38 | IncludeUnexported bool // Include unexported fields in output 39 | PrintStringers bool // Call String on a fmt.Stringer 40 | PrintTextMarshalers bool // Call MarshalText on an encoding.TextMarshaler 41 | SkipZeroFields bool // Skip struct fields that have a zero value. 42 | 43 | // Output transforms 44 | ShortList int // Maximum character length for short lists if nonzero. 45 | 46 | // Type-specific overrides 47 | // 48 | // Formatter maps a type to a function that will provide a one-line string 49 | // representation of the input value. Conceptually: 50 | // Formatter[reflect.TypeOf(v)](v) = "v as a string" 51 | // 52 | // Note that the first argument need not explicitly match the type, it must 53 | // merely be callable with it. 54 | // 55 | // When processing an input value, if its type exists as a key in Formatter: 56 | // 1) If the value is nil, no stringification is performed. 57 | // This allows overriding of PrintStringers and PrintTextMarshalers. 58 | // 2) The value will be called with the input as its only argument. 59 | // The function must return a string as its first return value. 60 | // 61 | // In addition to func literals, two common values for this will be: 62 | // fmt.Sprint (function) func Sprint(...interface{}) string 63 | // Type.String (method) func (Type) String() string 64 | // 65 | // Note that neither of these work if the String method is a pointer 66 | // method and the input will be provided as a value. In that case, 67 | // use a function that calls .String on the formal value parameter. 68 | Formatter map[reflect.Type]interface{} 69 | 70 | // If TrackCycles is enabled, pretty will detect and track 71 | // self-referential structures. If a self-referential structure (aka a 72 | // "recursive" value) is detected, numbered placeholders will be emitted. 73 | // 74 | // Pointer tracking is disabled by default for performance reasons. 75 | TrackCycles bool 76 | } 77 | 78 | // Default Config objects 79 | var ( 80 | // DefaultFormatter is the default set of overrides for stringification. 81 | DefaultFormatter = map[reflect.Type]interface{}{ 82 | reflect.TypeOf(time.Time{}): fmt.Sprint, 83 | reflect.TypeOf(net.IP{}): fmt.Sprint, 84 | reflect.TypeOf((*error)(nil)).Elem(): fmt.Sprint, 85 | } 86 | 87 | // CompareConfig is the default configuration used for Compare. 88 | CompareConfig = &Config{ 89 | Diffable: true, 90 | IncludeUnexported: true, 91 | Formatter: DefaultFormatter, 92 | } 93 | 94 | // DefaultConfig is the default configuration used for all other top-level functions. 95 | DefaultConfig = &Config{ 96 | Formatter: DefaultFormatter, 97 | } 98 | 99 | // CycleTracker is a convenience config for formatting and comparing recursive structures. 100 | CycleTracker = &Config{ 101 | Diffable: true, 102 | Formatter: DefaultFormatter, 103 | TrackCycles: true, 104 | } 105 | ) 106 | 107 | func (cfg *Config) fprint(buf *bytes.Buffer, vals ...interface{}) { 108 | ref := &reflector{ 109 | Config: cfg, 110 | } 111 | if cfg.TrackCycles { 112 | ref.pointerTracker = new(pointerTracker) 113 | } 114 | for i, val := range vals { 115 | if i > 0 { 116 | buf.WriteByte('\n') 117 | } 118 | newFormatter(cfg, buf).write(ref.val2node(reflect.ValueOf(val))) 119 | } 120 | } 121 | 122 | // Print writes the DefaultConfig representation of the given values to standard output. 123 | func Print(vals ...interface{}) { 124 | DefaultConfig.Print(vals...) 125 | } 126 | 127 | // Print writes the configured presentation of the given values to standard output. 128 | func (cfg *Config) Print(vals ...interface{}) { 129 | fmt.Println(cfg.Sprint(vals...)) 130 | } 131 | 132 | // Sprint returns a string representation of the given value according to the DefaultConfig. 133 | func Sprint(vals ...interface{}) string { 134 | return DefaultConfig.Sprint(vals...) 135 | } 136 | 137 | // Sprint returns a string representation of the given value according to cfg. 138 | func (cfg *Config) Sprint(vals ...interface{}) string { 139 | buf := new(bytes.Buffer) 140 | cfg.fprint(buf, vals...) 141 | return buf.String() 142 | } 143 | 144 | // Fprint writes the representation of the given value to the writer according to the DefaultConfig. 145 | func Fprint(w io.Writer, vals ...interface{}) (n int64, err error) { 146 | return DefaultConfig.Fprint(w, vals...) 147 | } 148 | 149 | // Fprint writes the representation of the given value to the writer according to the cfg. 150 | func (cfg *Config) Fprint(w io.Writer, vals ...interface{}) (n int64, err error) { 151 | buf := new(bytes.Buffer) 152 | cfg.fprint(buf, vals...) 153 | return buf.WriteTo(w) 154 | } 155 | 156 | // Compare returns a string containing a line-by-line unified diff of the 157 | // values in a and b, using the CompareConfig. 158 | // 159 | // Each line in the output is prefixed with '+', '-', or ' ' to indicate which 160 | // side it's from. Lines from the a side are marked with '-', lines from the 161 | // b side are marked with '+' and lines that are the same on both sides are 162 | // marked with ' '. 163 | // 164 | // The comparison is based on the intentionally-untyped output of Print, and as 165 | // such this comparison is pretty forviving. In particular, if the types of or 166 | // types within in a and b are different but have the same representation, 167 | // Compare will not indicate any differences between them. 168 | func Compare(a, b interface{}) string { 169 | return CompareConfig.Compare(a, b) 170 | } 171 | 172 | // Compare returns a string containing a line-by-line unified diff of the 173 | // values in got and want according to the cfg. 174 | // 175 | // Each line in the output is prefixed with '+', '-', or ' ' to indicate which 176 | // side it's from. Lines from the a side are marked with '-', lines from the 177 | // b side are marked with '+' and lines that are the same on both sides are 178 | // marked with ' '. 179 | // 180 | // The comparison is based on the intentionally-untyped output of Print, and as 181 | // such this comparison is pretty forviving. In particular, if the types of or 182 | // types within in a and b are different but have the same representation, 183 | // Compare will not indicate any differences between them. 184 | func (cfg *Config) Compare(a, b interface{}) string { 185 | diffCfg := *cfg 186 | diffCfg.Diffable = true 187 | return diff.Diff(cfg.Sprint(a), cfg.Sprint(b)) 188 | } 189 | -------------------------------------------------------------------------------- /pretty/reflect.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pretty 16 | 17 | import ( 18 | "encoding" 19 | "fmt" 20 | "reflect" 21 | "sort" 22 | ) 23 | 24 | func isZeroVal(val reflect.Value) bool { 25 | if !val.CanInterface() { 26 | return false 27 | } 28 | z := reflect.Zero(val.Type()).Interface() 29 | return reflect.DeepEqual(val.Interface(), z) 30 | } 31 | 32 | // pointerTracker is a helper for tracking pointer chasing to detect cycles. 33 | type pointerTracker struct { 34 | addrs map[uintptr]int // addr[address] = seen count 35 | 36 | lastID int 37 | ids map[uintptr]int // ids[address] = id 38 | } 39 | 40 | // track tracks following a reference (pointer, slice, map, etc). Every call to 41 | // track should be paired with a call to untrack. 42 | func (p *pointerTracker) track(ptr uintptr) { 43 | if p.addrs == nil { 44 | p.addrs = make(map[uintptr]int) 45 | } 46 | p.addrs[ptr]++ 47 | } 48 | 49 | // untrack registers that we have backtracked over the reference to the pointer. 50 | func (p *pointerTracker) untrack(ptr uintptr) { 51 | p.addrs[ptr]-- 52 | if p.addrs[ptr] == 0 { 53 | delete(p.addrs, ptr) 54 | } 55 | } 56 | 57 | // seen returns whether the pointer was previously seen along this path. 58 | func (p *pointerTracker) seen(ptr uintptr) bool { 59 | _, ok := p.addrs[ptr] 60 | return ok 61 | } 62 | 63 | // keep allocates an ID for the given address and returns it. 64 | func (p *pointerTracker) keep(ptr uintptr) int { 65 | if p.ids == nil { 66 | p.ids = make(map[uintptr]int) 67 | } 68 | if _, ok := p.ids[ptr]; !ok { 69 | p.lastID++ 70 | p.ids[ptr] = p.lastID 71 | } 72 | return p.ids[ptr] 73 | } 74 | 75 | // id returns the ID for the given address. 76 | func (p *pointerTracker) id(ptr uintptr) (int, bool) { 77 | if p.ids == nil { 78 | p.ids = make(map[uintptr]int) 79 | } 80 | id, ok := p.ids[ptr] 81 | return id, ok 82 | } 83 | 84 | // reflector adds local state to the recursive reflection logic. 85 | type reflector struct { 86 | *Config 87 | *pointerTracker 88 | } 89 | 90 | // follow handles following a possiblly-recursive reference to the given value 91 | // from the given ptr address. 92 | func (r *reflector) follow(ptr uintptr, val reflect.Value) node { 93 | if r.pointerTracker == nil { 94 | // Tracking disabled 95 | return r.val2node(val) 96 | } 97 | 98 | // If a parent already followed this, emit a reference marker 99 | if r.seen(ptr) { 100 | id := r.keep(ptr) 101 | return ref{id} 102 | } 103 | 104 | // Track the pointer we're following while on this recursive branch 105 | r.track(ptr) 106 | defer r.untrack(ptr) 107 | n := r.val2node(val) 108 | 109 | // If the recursion used this ptr, wrap it with a target marker 110 | if id, ok := r.id(ptr); ok { 111 | return target{id, n} 112 | } 113 | 114 | // Otherwise, return the node unadulterated 115 | return n 116 | } 117 | 118 | func (r *reflector) val2node(val reflect.Value) (ret node) { 119 | if !val.IsValid() { 120 | return rawVal("nil") 121 | } 122 | 123 | if val.CanInterface() { 124 | // Detect panics in calling functions on nil pointers. 125 | // 126 | // We still want to call them, as it's possible that a nil value is 127 | // valid for the particular type. 128 | // 129 | // If we detect a panic, just return raw nil. 130 | if val.Kind() == reflect.Ptr && val.IsNil() { 131 | defer func() { 132 | if r := recover(); r != nil { 133 | ret = rawVal("nil") 134 | } 135 | }() 136 | } 137 | 138 | v := val.Interface() 139 | if formatter, ok := r.Formatter[val.Type()]; ok { 140 | if formatter != nil { 141 | res := reflect.ValueOf(formatter).Call([]reflect.Value{val}) 142 | return rawVal(res[0].Interface().(string)) 143 | } 144 | } else { 145 | if s, ok := v.(fmt.Stringer); ok && r.PrintStringers { 146 | return stringVal(s.String()) 147 | } 148 | if t, ok := v.(encoding.TextMarshaler); ok && r.PrintTextMarshalers { 149 | if raw, err := t.MarshalText(); err == nil { // if NOT an error 150 | return stringVal(string(raw)) 151 | } 152 | } 153 | } 154 | } 155 | 156 | switch kind := val.Kind(); kind { 157 | case reflect.Ptr: 158 | if val.IsNil() { 159 | return rawVal("nil") 160 | } 161 | return r.follow(val.Pointer(), val.Elem()) 162 | case reflect.Interface: 163 | if val.IsNil() { 164 | return rawVal("nil") 165 | } 166 | return r.val2node(val.Elem()) 167 | case reflect.String: 168 | return stringVal(val.String()) 169 | case reflect.Slice: 170 | n := list{} 171 | length := val.Len() 172 | ptr := val.Pointer() 173 | for i := 0; i < length; i++ { 174 | n = append(n, r.follow(ptr, val.Index(i))) 175 | } 176 | return n 177 | case reflect.Array: 178 | n := list{} 179 | length := val.Len() 180 | for i := 0; i < length; i++ { 181 | n = append(n, r.val2node(val.Index(i))) 182 | } 183 | return n 184 | case reflect.Map: 185 | // Extract the keys and sort them for stable iteration 186 | keys := val.MapKeys() 187 | pairs := make([]mapPair, 0, len(keys)) 188 | for _, key := range keys { 189 | pairs = append(pairs, mapPair{ 190 | key: new(formatter).compactString(r.val2node(key)), // can't be cyclic 191 | value: val.MapIndex(key), 192 | }) 193 | } 194 | sort.Sort(byKey(pairs)) 195 | 196 | // Process the keys into the final representation 197 | ptr, n := val.Pointer(), keyvals{} 198 | for _, pair := range pairs { 199 | n = append(n, keyval{ 200 | key: pair.key, 201 | val: r.follow(ptr, pair.value), 202 | }) 203 | } 204 | return n 205 | case reflect.Struct: 206 | n := keyvals{} 207 | typ := val.Type() 208 | fields := typ.NumField() 209 | for i := 0; i < fields; i++ { 210 | sf := typ.Field(i) 211 | if !r.IncludeUnexported && sf.PkgPath != "" { 212 | continue 213 | } 214 | field := val.Field(i) 215 | if r.SkipZeroFields && isZeroVal(field) { 216 | continue 217 | } 218 | n = append(n, keyval{sf.Name, r.val2node(field)}) 219 | } 220 | return n 221 | case reflect.Bool: 222 | if val.Bool() { 223 | return rawVal("true") 224 | } 225 | return rawVal("false") 226 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 227 | return rawVal(fmt.Sprintf("%d", val.Int())) 228 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 229 | return rawVal(fmt.Sprintf("%d", val.Uint())) 230 | case reflect.Uintptr: 231 | return rawVal(fmt.Sprintf("0x%X", val.Uint())) 232 | case reflect.Float32, reflect.Float64: 233 | return rawVal(fmt.Sprintf("%v", val.Float())) 234 | case reflect.Complex64, reflect.Complex128: 235 | return rawVal(fmt.Sprintf("%v", val.Complex())) 236 | } 237 | 238 | // Fall back to the default %#v if we can 239 | if val.CanInterface() { 240 | return rawVal(fmt.Sprintf("%#v", val.Interface())) 241 | } 242 | 243 | return rawVal(val.String()) 244 | } 245 | 246 | type mapPair struct { 247 | key string 248 | value reflect.Value 249 | } 250 | 251 | type byKey []mapPair 252 | 253 | func (v byKey) Len() int { return len(v) } 254 | func (v byKey) Swap(i, j int) { v[i], v[j] = v[j], v[i] } 255 | func (v byKey) Less(i, j int) bool { return v[i].key < v[j].key } 256 | -------------------------------------------------------------------------------- /pretty/structure_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pretty 16 | 17 | import ( 18 | "bytes" 19 | "strings" 20 | "testing" 21 | ) 22 | 23 | func TestFormat(t *testing.T) { 24 | tests := []struct { 25 | desc string 26 | node node 27 | 28 | // All strings have a leading newline trimmed before comparison: 29 | normal string 30 | diffable string 31 | }{ 32 | { 33 | desc: "string", 34 | node: stringVal("zaphod"), 35 | normal: `"zaphod"`, 36 | diffable: `"zaphod"`, 37 | }, 38 | { 39 | desc: "raw", 40 | node: rawVal("42"), 41 | normal: `42`, 42 | diffable: `42`, 43 | }, 44 | { 45 | desc: "keyvals", 46 | node: keyvals{ 47 | {"name", stringVal("zaphod")}, 48 | {"age", rawVal("42")}, 49 | }, 50 | normal: ` 51 | {name: "zaphod", 52 | age: 42}`, 53 | diffable: ` 54 | { 55 | name: "zaphod", 56 | age: 42, 57 | }`, 58 | }, 59 | { 60 | desc: "empty list", 61 | node: list{}, 62 | normal: ` 63 | []`, 64 | diffable: ` 65 | [ 66 | ]`, 67 | }, 68 | { 69 | desc: "empty nested list", 70 | node: list{list{}}, 71 | normal: ` 72 | [[]]`, 73 | diffable: ` 74 | [ 75 | [ 76 | ], 77 | ]`, 78 | }, 79 | { 80 | desc: "list", 81 | node: list{ 82 | stringVal("zaphod"), 83 | rawVal("42"), 84 | }, 85 | normal: ` 86 | ["zaphod", 87 | 42]`, 88 | diffable: ` 89 | [ 90 | "zaphod", 91 | 42, 92 | ]`, 93 | }, 94 | { 95 | desc: "empty keyvals", 96 | node: keyvals{}, 97 | normal: ` 98 | {}`, 99 | diffable: ` 100 | { 101 | }`, 102 | }, 103 | { 104 | desc: "empty nested keyvals", 105 | node: keyvals{{"k", keyvals{}}}, 106 | normal: ` 107 | {k: {}}`, 108 | diffable: ` 109 | { 110 | k: { 111 | }, 112 | }`, 113 | }, 114 | { 115 | desc: "nested", 116 | node: list{ 117 | stringVal("first"), 118 | list{rawVal("1"), rawVal("2"), rawVal("3")}, 119 | keyvals{ 120 | {"trillian", keyvals{ 121 | {"race", stringVal("human")}, 122 | {"age", rawVal("36")}, 123 | }}, 124 | {"zaphod", keyvals{ 125 | {"occupation", stringVal("president of the galaxy")}, 126 | {"features", stringVal("two heads")}, 127 | }}, 128 | }, 129 | keyvals{}, 130 | }, 131 | normal: ` 132 | ["first", 133 | [1, 134 | 2, 135 | 3], 136 | {trillian: {race: "human", 137 | age: 36}, 138 | zaphod: {occupation: "president of the galaxy", 139 | features: "two heads"}}, 140 | {}]`, 141 | diffable: ` 142 | [ 143 | "first", 144 | [ 145 | 1, 146 | 2, 147 | 3, 148 | ], 149 | { 150 | trillian: { 151 | race: "human", 152 | age: 36, 153 | }, 154 | zaphod: { 155 | occupation: "president of the galaxy", 156 | features: "two heads", 157 | }, 158 | }, 159 | { 160 | }, 161 | ]`, 162 | }, 163 | { 164 | desc: "recursive", 165 | node: target{1, keyvals{ 166 | {"Value", rawVal("1")}, 167 | {"Next", keyvals{ 168 | {"Value", rawVal("2")}, 169 | {"Next", keyvals{ 170 | {"Value", rawVal("3")}, 171 | {"Next", ref{1}}, 172 | }}, 173 | }}, 174 | }}, 175 | normal: ` 176 | <#1> {Value: 1, 177 | Next: {Value: 2, 178 | Next: {Value: 3, 179 | Next: }}}`, 180 | diffable: ` 181 | <#1> { 182 | Value: 1, 183 | Next: { 184 | Value: 2, 185 | Next: { 186 | Value: 3, 187 | Next: , 188 | }, 189 | }, 190 | }`, 191 | }, 192 | { 193 | desc: "print in order", 194 | node: list{ 195 | target{2, keyvals{ 196 | {"Next", ref{1}}, 197 | }}, 198 | target{1, keyvals{ 199 | {"Next", ref{2}}, 200 | }}, 201 | }, 202 | normal: ` 203 | [<#1> {Next: }, 204 | <#2> {Next: }]`, 205 | diffable: ` 206 | [ 207 | <#1> { 208 | Next: , 209 | }, 210 | <#2> { 211 | Next: , 212 | }, 213 | ]`, 214 | }, 215 | } 216 | 217 | normal := &Config{} 218 | diffable := &Config{Diffable: true} 219 | for _, test := range tests { 220 | // For readability, we have a newline that won't be there in the output 221 | test.normal = strings.TrimPrefix(test.normal, "\n") 222 | test.diffable = strings.TrimPrefix(test.diffable, "\n") 223 | 224 | buf := new(bytes.Buffer) 225 | newFormatter(normal, buf).write(test.node) 226 | if got, want := buf.String(), test.normal; got != want { 227 | t.Errorf("%s: normal rendendered incorrectly\ngot:\n%s\nwant:\n%s", test.desc, got, want) 228 | } 229 | buf.Reset() 230 | 231 | newFormatter(diffable, buf).write(test.node) 232 | if got, want := buf.String(), test.diffable; got != want { 233 | t.Errorf("%s: diffable rendendered incorrectly\ngot:\n%s\nwant:\n%s", test.desc, got, want) 234 | } 235 | } 236 | } 237 | 238 | func TestCompactString(t *testing.T) { 239 | tests := []struct { 240 | node 241 | compact string 242 | }{ 243 | { 244 | stringVal("abc"), 245 | "abc", 246 | }, 247 | { 248 | rawVal("2"), 249 | "2", 250 | }, 251 | { 252 | list{ 253 | rawVal("2"), 254 | rawVal("3"), 255 | }, 256 | "[2,3]", 257 | }, 258 | { 259 | keyvals{ 260 | {"name", stringVal("zaphod")}, 261 | {"age", rawVal("42")}, 262 | }, 263 | `{name:"zaphod",age:42}`, 264 | }, 265 | { 266 | list{ 267 | list{ 268 | rawVal("0"), 269 | rawVal("1"), 270 | rawVal("2"), 271 | rawVal("3"), 272 | }, 273 | list{ 274 | rawVal("1"), 275 | rawVal("2"), 276 | rawVal("3"), 277 | rawVal("0"), 278 | }, 279 | list{ 280 | rawVal("2"), 281 | rawVal("3"), 282 | rawVal("0"), 283 | rawVal("1"), 284 | }, 285 | }, 286 | `[[0,1,2,3],[1,2,3,0],[2,3,0,1]]`, 287 | }, 288 | } 289 | 290 | for _, test := range tests { 291 | if got, want := new(formatter).compactString(test.node), test.compact; got != want { 292 | t.Errorf("%#v: compact = %q, want %q", test.node, got, want) 293 | } 294 | } 295 | } 296 | 297 | func TestShortList(t *testing.T) { 298 | cfg := &Config{ 299 | ShortList: 16, 300 | } 301 | 302 | tests := []struct { 303 | node 304 | want string 305 | }{ 306 | { 307 | list{ 308 | list{ 309 | rawVal("0"), 310 | rawVal("1"), 311 | rawVal("2"), 312 | rawVal("3"), 313 | }, 314 | list{ 315 | rawVal("1"), 316 | rawVal("2"), 317 | rawVal("3"), 318 | rawVal("0"), 319 | }, 320 | list{ 321 | rawVal("2"), 322 | rawVal("3"), 323 | rawVal("0"), 324 | rawVal("1"), 325 | }, 326 | }, 327 | `[[0,1,2,3], 328 | [1,2,3,0], 329 | [2,3,0,1]]`, 330 | }, 331 | } 332 | 333 | for _, test := range tests { 334 | buf := new(bytes.Buffer) 335 | newFormatter(cfg, buf).write(test.node) 336 | if got, want := buf.String(), test.want; got != want { 337 | t.Errorf("%#v:\ngot:\n%s\nwant:\n%s", test.node, got, want) 338 | } 339 | } 340 | } 341 | 342 | var benchNode = keyvals{ 343 | {"list", list{ 344 | rawVal("0"), 345 | rawVal("1"), 346 | rawVal("2"), 347 | rawVal("3"), 348 | }}, 349 | {"keyvals", keyvals{ 350 | {"a", stringVal("b")}, 351 | {"c", stringVal("e")}, 352 | {"d", stringVal("f")}, 353 | }}, 354 | } 355 | 356 | func benchOpts(b *testing.B, cfg *Config) { 357 | buf := new(bytes.Buffer) 358 | newFormatter(cfg, buf).write(benchNode) 359 | b.SetBytes(int64(buf.Len())) 360 | b.ResetTimer() 361 | 362 | for i := 0; i < b.N; i++ { 363 | buf.Reset() 364 | newFormatter(cfg, buf).write(benchNode) 365 | } 366 | } 367 | 368 | func BenchmarkWriteDefault(b *testing.B) { benchOpts(b, DefaultConfig) } 369 | func BenchmarkWriteShortList(b *testing.B) { benchOpts(b, &Config{ShortList: 16}) } 370 | func BenchmarkWriteCompact(b *testing.B) { benchOpts(b, &Config{Compact: true}) } 371 | func BenchmarkWriteDiffable(b *testing.B) { benchOpts(b, &Config{Diffable: true}) } 372 | -------------------------------------------------------------------------------- /pretty/examples_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pretty_test 16 | 17 | import ( 18 | "fmt" 19 | "net" 20 | "reflect" 21 | 22 | "github.com/kylelemons/godebug/pretty" 23 | ) 24 | 25 | func ExampleConfig_Sprint() { 26 | type Pair [2]int 27 | type Map struct { 28 | Name string 29 | Players map[string]Pair 30 | Obstacles map[Pair]string 31 | } 32 | 33 | m := Map{ 34 | Name: "Rock Creek", 35 | Players: map[string]Pair{ 36 | "player1": {1, 3}, 37 | "player2": {0, -1}, 38 | }, 39 | Obstacles: map[Pair]string{ 40 | Pair{0, 0}: "rock", 41 | Pair{2, 1}: "pond", 42 | Pair{1, 1}: "stream", 43 | Pair{0, 1}: "stream", 44 | }, 45 | } 46 | 47 | // Specific output formats 48 | compact := &pretty.Config{ 49 | Compact: true, 50 | } 51 | diffable := &pretty.Config{ 52 | Diffable: true, 53 | } 54 | 55 | // Print out a summary 56 | fmt.Printf("Players: %s\n", compact.Sprint(m.Players)) 57 | 58 | // Print diffable output 59 | fmt.Printf("Map State:\n%s", diffable.Sprint(m)) 60 | 61 | // Output: 62 | // Players: {player1:[1,3],player2:[0,-1]} 63 | // Map State: 64 | // { 65 | // Name: "Rock Creek", 66 | // Players: { 67 | // player1: [ 68 | // 1, 69 | // 3, 70 | // ], 71 | // player2: [ 72 | // 0, 73 | // -1, 74 | // ], 75 | // }, 76 | // Obstacles: { 77 | // [0,0]: "rock", 78 | // [0,1]: "stream", 79 | // [1,1]: "stream", 80 | // [2,1]: "pond", 81 | // }, 82 | // } 83 | } 84 | 85 | func ExampleConfig_fmtFormatter() { 86 | pretty.DefaultFormatter[reflect.TypeOf(&net.IPNet{})] = fmt.Sprint 87 | pretty.DefaultFormatter[reflect.TypeOf(net.HardwareAddr{})] = fmt.Sprint 88 | pretty.Print(&net.IPNet{ 89 | IP: net.IPv4(192, 168, 1, 100), 90 | Mask: net.CIDRMask(24, 32), 91 | }) 92 | pretty.Print(net.HardwareAddr{1, 2, 3, 4, 5, 6}) 93 | 94 | // Output: 95 | // 192.168.1.100/24 96 | // 01:02:03:04:05:06 97 | } 98 | 99 | func ExampleConfig_customFormatter() { 100 | pretty.DefaultFormatter[reflect.TypeOf(&net.IPNet{})] = func(n *net.IPNet) string { 101 | return fmt.Sprintf("CIDR=%s", n) 102 | } 103 | pretty.Print(&net.IPNet{ 104 | IP: net.IPv4(192, 168, 1, 100), 105 | Mask: net.CIDRMask(24, 32), 106 | }) 107 | 108 | // Output: 109 | // CIDR=192.168.1.100/24 110 | } 111 | 112 | func ExamplePrint() { 113 | type ShipManifest struct { 114 | Name string 115 | Crew map[string]string 116 | Androids int 117 | Stolen bool 118 | } 119 | 120 | manifest := &ShipManifest{ 121 | Name: "Spaceship Heart of Gold", 122 | Crew: map[string]string{ 123 | "Zaphod Beeblebrox": "Galactic President", 124 | "Trillian": "Human", 125 | "Ford Prefect": "A Hoopy Frood", 126 | "Arthur Dent": "Along for the Ride", 127 | }, 128 | Androids: 1, 129 | Stolen: true, 130 | } 131 | 132 | pretty.Print(manifest) 133 | 134 | // Output: 135 | // {Name: "Spaceship Heart of Gold", 136 | // Crew: {Arthur Dent: "Along for the Ride", 137 | // Ford Prefect: "A Hoopy Frood", 138 | // Trillian: "Human", 139 | // Zaphod Beeblebrox: "Galactic President"}, 140 | // Androids: 1, 141 | // Stolen: true} 142 | } 143 | 144 | var t = struct { 145 | Errorf func(string, ...interface{}) 146 | }{ 147 | Errorf: func(format string, args ...interface{}) { 148 | fmt.Println(fmt.Sprintf(format, args...) + "\n") 149 | }, 150 | } 151 | 152 | func ExampleCompare_testing() { 153 | // Code under test: 154 | 155 | type ShipManifest struct { 156 | Name string 157 | Crew map[string]string 158 | Androids int 159 | Stolen bool 160 | } 161 | 162 | // AddCrew tries to add the given crewmember to the manifest. 163 | AddCrew := func(m *ShipManifest, name, title string) { 164 | if m.Crew == nil { 165 | m.Crew = make(map[string]string) 166 | } 167 | m.Crew[title] = name 168 | } 169 | 170 | // Test function: 171 | tests := []struct { 172 | desc string 173 | before *ShipManifest 174 | name, title string 175 | after *ShipManifest 176 | }{ 177 | { 178 | desc: "add first", 179 | before: &ShipManifest{}, 180 | name: "Zaphod Beeblebrox", 181 | title: "Galactic President", 182 | after: &ShipManifest{ 183 | Crew: map[string]string{ 184 | "Zaphod Beeblebrox": "Galactic President", 185 | }, 186 | }, 187 | }, 188 | { 189 | desc: "add another", 190 | before: &ShipManifest{ 191 | Crew: map[string]string{ 192 | "Zaphod Beeblebrox": "Galactic President", 193 | }, 194 | }, 195 | name: "Trillian", 196 | title: "Human", 197 | after: &ShipManifest{ 198 | Crew: map[string]string{ 199 | "Zaphod Beeblebrox": "Galactic President", 200 | "Trillian": "Human", 201 | }, 202 | }, 203 | }, 204 | { 205 | desc: "overwrite", 206 | before: &ShipManifest{ 207 | Crew: map[string]string{ 208 | "Zaphod Beeblebrox": "Galactic President", 209 | }, 210 | }, 211 | name: "Zaphod Beeblebrox", 212 | title: "Just this guy, you know?", 213 | after: &ShipManifest{ 214 | Crew: map[string]string{ 215 | "Zaphod Beeblebrox": "Just this guy, you know?", 216 | }, 217 | }, 218 | }, 219 | } 220 | 221 | for _, test := range tests { 222 | AddCrew(test.before, test.name, test.title) 223 | if diff := pretty.Compare(test.before, test.after); diff != "" { 224 | t.Errorf("%s: post-AddCrew diff: (-got +want)\n%s", test.desc, diff) 225 | } 226 | } 227 | 228 | // Output: 229 | // add first: post-AddCrew diff: (-got +want) 230 | // { 231 | // Name: "", 232 | // Crew: { 233 | // - Galactic President: "Zaphod Beeblebrox", 234 | // + Zaphod Beeblebrox: "Galactic President", 235 | // }, 236 | // Androids: 0, 237 | // Stolen: false, 238 | // } 239 | // 240 | // add another: post-AddCrew diff: (-got +want) 241 | // { 242 | // Name: "", 243 | // Crew: { 244 | // - Human: "Trillian", 245 | // + Trillian: "Human", 246 | // Zaphod Beeblebrox: "Galactic President", 247 | // }, 248 | // Androids: 0, 249 | // Stolen: false, 250 | // } 251 | // 252 | // overwrite: post-AddCrew diff: (-got +want) 253 | // { 254 | // Name: "", 255 | // Crew: { 256 | // - Just this guy, you know?: "Zaphod Beeblebrox", 257 | // - Zaphod Beeblebrox: "Galactic President", 258 | // + Zaphod Beeblebrox: "Just this guy, you know?", 259 | // }, 260 | // Androids: 0, 261 | // Stolen: false, 262 | // } 263 | } 264 | 265 | func ExampleCompare_debugging() { 266 | type ShipManifest struct { 267 | Name string 268 | Crew map[string]string 269 | Androids int 270 | Stolen bool 271 | } 272 | 273 | reported := &ShipManifest{ 274 | Name: "Spaceship Heart of Gold", 275 | Crew: map[string]string{ 276 | "Zaphod Beeblebrox": "Galactic President", 277 | "Trillian": "Human", 278 | "Ford Prefect": "A Hoopy Frood", 279 | "Arthur Dent": "Along for the Ride", 280 | }, 281 | Androids: 1, 282 | Stolen: true, 283 | } 284 | 285 | expected := &ShipManifest{ 286 | Name: "Spaceship Heart of Gold", 287 | Crew: map[string]string{ 288 | "Trillian": "Human", 289 | "Rowan Artosok": "Captain", 290 | }, 291 | Androids: 1, 292 | Stolen: false, 293 | } 294 | 295 | fmt.Println(pretty.Compare(reported, expected)) 296 | // Output: 297 | // { 298 | // Name: "Spaceship Heart of Gold", 299 | // Crew: { 300 | // - Arthur Dent: "Along for the Ride", 301 | // - Ford Prefect: "A Hoopy Frood", 302 | // + Rowan Artosok: "Captain", 303 | // Trillian: "Human", 304 | // - Zaphod Beeblebrox: "Galactic President", 305 | // }, 306 | // Androids: 1, 307 | // - Stolen: true, 308 | // + Stolen: false, 309 | // } 310 | } 311 | 312 | type ListNode struct { 313 | Value int 314 | Next *ListNode 315 | } 316 | 317 | func circular(nodes int) *ListNode { 318 | final := &ListNode{ 319 | Value: nodes, 320 | } 321 | final.Next = final 322 | 323 | recent := final 324 | for i := nodes - 1; i > 0; i-- { 325 | n := &ListNode{ 326 | Value: i, 327 | Next: recent, 328 | } 329 | final.Next = n 330 | recent = n 331 | } 332 | return recent 333 | } 334 | 335 | func ExamplePrint_withCycles() { 336 | pretty.CycleTracker.Print(circular(3)) 337 | 338 | // Output: 339 | // <#1> { 340 | // Value: 1, 341 | // Next: { 342 | // Value: 2, 343 | // Next: { 344 | // Value: 3, 345 | // Next: , 346 | // }, 347 | // }, 348 | // } 349 | } 350 | 351 | func ExampleCompare_withCycles() { 352 | got, want := circular(3), circular(3) 353 | 354 | // Make the got one broken 355 | got.Next.Next.Next = got.Next 356 | 357 | fmt.Printf("Diff: (-got +want)\n%s", pretty.CycleTracker.Compare(got, want)) 358 | 359 | // Output: 360 | // Diff: (-got +want) 361 | // -{ 362 | // +<#1> { 363 | // Value: 1, 364 | // - Next: <#1> { 365 | // + Next: { 366 | // Value: 2, 367 | // Next: { 368 | // Value: 3, 369 | // Next: , 370 | // }, 371 | // }, 372 | // } 373 | } 374 | -------------------------------------------------------------------------------- /pretty/reflect_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Google Inc. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pretty 16 | 17 | import ( 18 | "fmt" 19 | "net" 20 | "reflect" 21 | "testing" 22 | "time" 23 | ) 24 | 25 | func TestVal2nodeDefault(t *testing.T) { 26 | err := fmt.Errorf("err") 27 | var errNil error 28 | 29 | tests := []struct { 30 | desc string 31 | raw interface{} 32 | want node 33 | }{ 34 | { 35 | desc: "nil", 36 | raw: nil, 37 | want: rawVal("nil"), 38 | }, 39 | { 40 | desc: "nil ptr", 41 | raw: (*int)(nil), 42 | want: rawVal("nil"), 43 | }, 44 | { 45 | desc: "nil slice", 46 | raw: []string(nil), 47 | want: list{}, 48 | }, 49 | { 50 | desc: "nil map", 51 | raw: map[string]string(nil), 52 | want: keyvals{}, 53 | }, 54 | { 55 | desc: "string", 56 | raw: "zaphod", 57 | want: stringVal("zaphod"), 58 | }, 59 | { 60 | desc: "slice", 61 | raw: []string{"a", "b"}, 62 | want: list{stringVal("a"), stringVal("b")}, 63 | }, 64 | { 65 | desc: "map", 66 | raw: map[string]string{ 67 | "zaphod": "beeblebrox", 68 | "ford": "prefect", 69 | }, 70 | want: keyvals{ 71 | {"ford", stringVal("prefect")}, 72 | {"zaphod", stringVal("beeblebrox")}, 73 | }, 74 | }, 75 | { 76 | desc: "map of [2]int", 77 | raw: map[[2]int]string{ 78 | [2]int{-1, 2}: "school", 79 | [2]int{0, 0}: "origin", 80 | [2]int{1, 3}: "home", 81 | }, 82 | want: keyvals{ 83 | {"[-1,2]", stringVal("school")}, 84 | {"[0,0]", stringVal("origin")}, 85 | {"[1,3]", stringVal("home")}, 86 | }, 87 | }, 88 | { 89 | desc: "struct", 90 | raw: struct{ Zaphod, Ford string }{"beeblebrox", "prefect"}, 91 | want: keyvals{ 92 | {"Zaphod", stringVal("beeblebrox")}, 93 | {"Ford", stringVal("prefect")}, 94 | }, 95 | }, 96 | { 97 | desc: "int", 98 | raw: 3, 99 | want: rawVal("3"), 100 | }, 101 | { 102 | desc: "time.Time", 103 | raw: time.Unix(1257894000, 0).UTC(), 104 | want: rawVal("2009-11-10 23:00:00 +0000 UTC"), 105 | }, 106 | { 107 | desc: "net.IP", 108 | raw: net.IPv4(127, 0, 0, 1), 109 | want: rawVal("127.0.0.1"), 110 | }, 111 | { 112 | desc: "error", 113 | raw: &err, 114 | want: rawVal("err"), 115 | }, 116 | { 117 | desc: "nil error", 118 | raw: &errNil, 119 | want: rawVal(""), 120 | }, 121 | } 122 | 123 | for _, test := range tests { 124 | ref := &reflector{ 125 | Config: DefaultConfig, 126 | } 127 | if got, want := ref.val2node(reflect.ValueOf(test.raw)), test.want; !reflect.DeepEqual(got, want) { 128 | t.Errorf("%s: got %#v, want %#v", test.desc, got, want) 129 | } 130 | } 131 | } 132 | 133 | func TestVal2node(t *testing.T) { 134 | tests := []struct { 135 | desc string 136 | raw interface{} 137 | cfg *Config 138 | want node 139 | }{ 140 | { 141 | desc: "struct default", 142 | raw: struct{ Zaphod, Ford, foo string }{"beeblebrox", "prefect", "BAD"}, 143 | cfg: DefaultConfig, 144 | want: keyvals{ 145 | {"Zaphod", stringVal("beeblebrox")}, 146 | {"Ford", stringVal("prefect")}, 147 | }, 148 | }, 149 | { 150 | desc: "struct with IncludeUnexported", 151 | raw: struct{ Zaphod, Ford, foo string }{"beeblebrox", "prefect", "GOOD"}, 152 | cfg: &Config{ 153 | IncludeUnexported: true, 154 | }, 155 | want: keyvals{ 156 | {"Zaphod", stringVal("beeblebrox")}, 157 | {"Ford", stringVal("prefect")}, 158 | {"foo", stringVal("GOOD")}, 159 | }, 160 | }, 161 | { 162 | desc: "time default", 163 | raw: struct{ Date time.Time }{time.Unix(1234567890, 0).UTC()}, 164 | cfg: DefaultConfig, 165 | want: keyvals{ 166 | {"Date", rawVal("2009-02-13 23:31:30 +0000 UTC")}, 167 | }, 168 | }, 169 | { 170 | desc: "time with nil Formatter", 171 | raw: struct{ Date time.Time }{time.Unix(1234567890, 0).UTC()}, 172 | cfg: &Config{ 173 | PrintStringers: true, 174 | Formatter: map[reflect.Type]interface{}{ 175 | reflect.TypeOf(time.Time{}): nil, 176 | }, 177 | }, 178 | want: keyvals{ 179 | {"Date", keyvals{}}, 180 | }, 181 | }, 182 | { 183 | desc: "time with PrintTextMarshalers", 184 | raw: struct{ Date time.Time }{time.Unix(1234567890, 0).UTC()}, 185 | cfg: &Config{ 186 | PrintTextMarshalers: true, 187 | }, 188 | want: keyvals{ 189 | {"Date", stringVal("2009-02-13T23:31:30Z")}, 190 | }, 191 | }, 192 | { 193 | desc: "time with PrintStringers", 194 | raw: struct{ Date time.Time }{time.Unix(1234567890, 0).UTC()}, 195 | cfg: &Config{ 196 | PrintStringers: true, 197 | }, 198 | want: keyvals{ 199 | {"Date", stringVal("2009-02-13 23:31:30 +0000 UTC")}, 200 | }, 201 | }, 202 | { 203 | desc: "nil with PrintStringers", 204 | raw: struct{ Date *time.Time }{}, 205 | cfg: &Config{ 206 | PrintStringers: true, 207 | }, 208 | want: keyvals{ 209 | {"Date", rawVal("nil")}, 210 | }, 211 | }, 212 | { 213 | desc: "nilIsFine with PrintStringers", 214 | raw: struct{ V *nilIsFine }{}, 215 | cfg: &Config{ 216 | PrintStringers: true, 217 | }, 218 | want: keyvals{ 219 | {"V", stringVal("")}, 220 | }, 221 | }, 222 | { 223 | desc: "nilIsFine non-nil with PrintStringers", 224 | raw: struct{ V *nilIsFine }{V: new(nilIsFine)}, 225 | cfg: &Config{ 226 | PrintStringers: true, 227 | }, 228 | want: keyvals{ 229 | {"V", stringVal("")}, 230 | }, 231 | }, 232 | { 233 | desc: "circular list", 234 | raw: circular(3), 235 | cfg: CycleTracker, 236 | want: target{1, keyvals{ 237 | {"Value", rawVal("1")}, 238 | {"Next", keyvals{ 239 | {"Value", rawVal("2")}, 240 | {"Next", keyvals{ 241 | {"Value", rawVal("3")}, 242 | {"Next", ref{1}}, 243 | }}, 244 | }}, 245 | }}, 246 | }, 247 | { 248 | desc: "self referential maps", 249 | raw: selfRef(), 250 | cfg: CycleTracker, 251 | want: target{1, keyvals{ 252 | {"ID", rawVal("1")}, 253 | {"Child", keyvals{ 254 | {"2", target{2, keyvals{ 255 | {"ID", rawVal("2")}, 256 | {"Child", keyvals{ 257 | {"3", target{3, keyvals{ 258 | {"ID", rawVal("3")}, 259 | {"Child", keyvals{ 260 | {"1", ref{1}}, 261 | {"2", ref{2}}, 262 | {"3", ref{3}}, 263 | }}, 264 | }}}, 265 | }}, 266 | }}}, 267 | }}, 268 | }}, 269 | }, 270 | { 271 | desc: "maps of cycles", 272 | raw: map[string]*ListNode{ 273 | "1. one": circular(1), 274 | "2. two": circular(2), 275 | "3. three": circular(1), 276 | }, 277 | cfg: CycleTracker, 278 | want: keyvals{ 279 | {"1. one", target{1, keyvals{ 280 | {"Value", rawVal("1")}, 281 | {"Next", ref{1}}, 282 | }}}, 283 | {"2. two", target{2, keyvals{ 284 | {"Value", rawVal("1")}, 285 | {"Next", keyvals{ 286 | {"Value", rawVal("2")}, 287 | {"Next", ref{2}}, 288 | }}, 289 | }}}, 290 | {"3. three", target{3, keyvals{ 291 | {"Value", rawVal("1")}, 292 | {"Next", ref{3}}, 293 | }}}, 294 | }, 295 | }, 296 | } 297 | 298 | for _, test := range tests { 299 | t.Run(test.desc, func(t *testing.T) { 300 | ref := &reflector{ 301 | Config: test.cfg, 302 | } 303 | if test.cfg.TrackCycles { 304 | ref.pointerTracker = new(pointerTracker) 305 | } 306 | if got, want := ref.val2node(reflect.ValueOf(test.raw)), test.want; !reflect.DeepEqual(got, want) { 307 | t.Errorf(" got %#v", got) 308 | t.Errorf("want %#v", want) 309 | t.Errorf("Diff: (-got +want)\n%s", Compare(got, want)) 310 | } 311 | }) 312 | } 313 | } 314 | 315 | type ListNode struct { 316 | Value int 317 | Next *ListNode 318 | } 319 | 320 | func circular(nodes int) *ListNode { 321 | final := &ListNode{ 322 | Value: nodes, 323 | } 324 | final.Next = final 325 | 326 | recent := final 327 | for i := nodes - 1; i > 0; i-- { 328 | n := &ListNode{ 329 | Value: i, 330 | Next: recent, 331 | } 332 | final.Next = n 333 | recent = n 334 | } 335 | return recent 336 | } 337 | 338 | type SelfReferential struct { 339 | ID int 340 | Child map[int]*SelfReferential 341 | } 342 | 343 | func selfRef() *SelfReferential { 344 | sr1 := &SelfReferential{ 345 | ID: 1, 346 | Child: make(map[int]*SelfReferential), 347 | } 348 | sr2 := &SelfReferential{ 349 | ID: 2, 350 | Child: make(map[int]*SelfReferential), 351 | } 352 | sr3 := &SelfReferential{ 353 | ID: 3, 354 | Child: make(map[int]*SelfReferential), 355 | } 356 | 357 | // Build a cycle 358 | sr1.Child[2] = sr2 359 | sr2.Child[3] = sr3 360 | sr3.Child[1] = sr1 361 | 362 | // Throw in some other stuff for funzies 363 | sr3.Child[2] = sr2 364 | sr3.Child[3] = sr3 365 | return sr1 366 | } 367 | 368 | func BenchmarkVal2node(b *testing.B) { 369 | benchmarks := []struct { 370 | desc string 371 | cfg *Config 372 | raw interface{} 373 | }{ 374 | { 375 | desc: "struct", 376 | cfg: DefaultConfig, 377 | raw: struct{ Zaphod, Ford string }{"beeblebrox", "prefect"}, 378 | }, 379 | { 380 | desc: "map", 381 | cfg: DefaultConfig, 382 | raw: map[[2]int]string{ 383 | [2]int{-1, 2}: "school", 384 | [2]int{0, 0}: "origin", 385 | [2]int{1, 3}: "home", 386 | }, 387 | }, 388 | { 389 | desc: "track/struct", 390 | cfg: CycleTracker, 391 | raw: struct{ Zaphod, Ford string }{"beeblebrox", "prefect"}, 392 | }, 393 | { 394 | desc: "track/map", 395 | cfg: CycleTracker, 396 | raw: map[[2]int]string{ 397 | [2]int{-1, 2}: "school", 398 | [2]int{0, 0}: "origin", 399 | [2]int{1, 3}: "home", 400 | }, 401 | }, 402 | { 403 | desc: "circlist/small", 404 | cfg: CycleTracker, 405 | raw: circular(3), 406 | }, 407 | { 408 | desc: "circlist/med", 409 | cfg: CycleTracker, 410 | raw: circular(300), 411 | }, 412 | { 413 | desc: "circlist/large", 414 | cfg: CycleTracker, 415 | raw: circular(3000), 416 | }, 417 | { 418 | desc: "mapofcirc/small", 419 | cfg: CycleTracker, 420 | raw: map[string]*ListNode{ 421 | "1. one": circular(1), 422 | "2. two": circular(2), 423 | "3. three": circular(1), 424 | }, 425 | }, 426 | { 427 | desc: "selfrefmap/small", 428 | cfg: CycleTracker, 429 | raw: selfRef, 430 | }, 431 | } 432 | 433 | for _, bench := range benchmarks { 434 | b.Run(bench.desc, func(b *testing.B) { 435 | b.ReportAllocs() 436 | for i := 0; i < b.N; i++ { 437 | ref := &reflector{ 438 | Config: bench.cfg, 439 | } 440 | if bench.cfg.TrackCycles { 441 | ref.pointerTracker = new(pointerTracker) 442 | } 443 | ref.val2node(reflect.ValueOf(bench.raw)) 444 | } 445 | }) 446 | } 447 | } 448 | 449 | type nilIsFine struct{} 450 | 451 | func (n *nilIsFine) String() string { 452 | if n == nil { 453 | return "" 454 | } 455 | return "" 456 | } 457 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------