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  }