go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/testing/assert/structuraldiff/structural_diff.go (about) 1 // Copyright 2024 The LUCI Authors. 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 structuraldiff 16 17 import ( 18 "bytes" 19 "reflect" 20 21 "github.com/kylelemons/godebug/pretty" 22 "github.com/sergi/go-diff/diffmatchpatch" 23 ) 24 25 // DebugDump is an extremely abstraction-breaking function that will print a Go Value. 26 // 27 // It prints zero values and unexported field, but does NOT ignore user-defined methods for pretty-printing. 28 // It SHOULD ignore user-defined methods for pretty-printing, but this causes issues with infinite recursion 29 // when printing protos. 30 func DebugDump(val any) string { 31 config := &pretty.Config{ 32 Compact: false, 33 Diffable: true, 34 IncludeUnexported: true, 35 // I would prefer this to be false. I really would, but disabling this feature 36 // causes an infinite loop when printing stuff. 37 // 38 // Long-term, I'm going to write my own thing. 39 PrintStringers: true, 40 PrintTextMarshalers: false, 41 SkipZeroFields: false, 42 ShortList: 30, 43 Formatter: nil, 44 TrackCycles: true, 45 } 46 return config.Sprint(val) 47 } 48 49 // A Result is either a list of diffs 50 type Result struct { 51 diffs []diffmatchpatch.Diff 52 message string 53 } 54 55 // String prints a result as a single string with no coloration. 56 // 57 // Sometimes you want colors, like when printing to a terminal, but by default you don't. 58 // Regardless, stringifying with colors should be opt-in and should always be on. 59 func (result *Result) String() string { 60 if result == nil { 61 return "" 62 } 63 if result.message != "" { 64 return result.message 65 } 66 return diffToString(result.diffs) 67 } 68 69 const sorry = `No difference between arguments but they are not equal.` 70 71 // DebugCompare compares two values of the same type and produces a structural diff between them. 72 // 73 // If we can't detect a difference between the two structures, return an apologetic message but 74 // NOT a nil result. That way we can compare the Result to nil to see if there were any problems. 75 func DebugCompare[T any](left T, right T) *Result { 76 if reflect.DeepEqual(left, right) { 77 return nil 78 } 79 patcher := diffmatchpatch.New() 80 out := patcher.DiffMain(DebugDump(left), DebugDump(right), true) 81 if len(out) == 0 { 82 return &Result{ 83 message: sorry, 84 } 85 } 86 return &Result{ 87 diffs: out, 88 } 89 } 90 91 // diffToString writes a sequence of diffs as a string without formatting. 92 func diffToString(diffs []diffmatchpatch.Diff) string { 93 var buf bytes.Buffer 94 for _, diff := range diffs { 95 if diff.Type == diffmatchpatch.DiffEqual { 96 continue 97 } 98 _, _ = buf.WriteString(diff.Type.String()) 99 _, _ = buf.WriteString(" ") 100 _, _ = buf.WriteString(diff.Text) 101 _, _ = buf.WriteString("\n") 102 } 103 return buf.String() 104 }