knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/kmp/reporters.go (about)

     1  /*
     2  Copyright 2019 The Knative Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package kmp
    18  
    19  import (
    20  	"fmt"
    21  	"reflect"
    22  	"sort"
    23  	"strings"
    24  
    25  	"github.com/google/go-cmp/cmp"
    26  )
    27  
    28  // fieldListReporter implements the cmp.Reporter interface. It keeps
    29  // track of the field names that differ between two structs and reports
    30  // them through the Fields() function.
    31  type fieldListReporter struct {
    32  	path       cmp.Path
    33  	fieldNames []string
    34  }
    35  
    36  // PushStep implements the cmp.Reporter.
    37  func (r *fieldListReporter) PushStep(ps cmp.PathStep) {
    38  	r.path = append(r.path, ps)
    39  }
    40  
    41  // fieldName returns a readable name for the field. If the field has JSON annotations it
    42  // returns the JSON key. If the field does not have JSON annotations or the JSON annotation
    43  // marks the field as ignored it returns the field's go name
    44  func (r *fieldListReporter) fieldName() string {
    45  	if len(r.path) < 2 {
    46  		return r.path.Index(0).String()
    47  	}
    48  	fieldName := strings.TrimPrefix(r.path.Index(1).String(), ".")
    49  	// Prefer JSON name to fieldName if it exists
    50  	structField, exists := r.path.Index(0).Type().FieldByName(fieldName)
    51  	if exists {
    52  		tag := structField.Tag.Get("json")
    53  		if tag != "" && tag != "-" {
    54  			return strings.SplitN(tag, ",", 2)[0]
    55  		}
    56  	}
    57  
    58  	return fieldName
    59  }
    60  
    61  // Report implements the cmp.Reporter.
    62  func (r *fieldListReporter) Report(rs cmp.Result) {
    63  	if rs.Equal() {
    64  		return
    65  	}
    66  	name := r.fieldName()
    67  	// Only append elements we don't already have.
    68  	for _, v := range r.fieldNames {
    69  		if name == v {
    70  			return
    71  		}
    72  	}
    73  	r.fieldNames = append(r.fieldNames, name)
    74  }
    75  
    76  // PopStep implements cmp.Reporter.
    77  func (r *fieldListReporter) PopStep() {
    78  	r.path = r.path[:len(r.path)-1]
    79  }
    80  
    81  // Fields returns the field names that differed between the two
    82  // objects after calling cmp.Equal with the FieldListReporter. Field names
    83  // are returned in alphabetical order.
    84  func (r *fieldListReporter) Fields() []string {
    85  	sort.Strings(r.fieldNames)
    86  	return r.fieldNames
    87  }
    88  
    89  // shortDiffReporter implements the cmp.Reporter interface. It reports
    90  // on fields which have diffing values in a short zero-context, unified diff
    91  // format.
    92  type shortDiffReporter struct {
    93  	path  cmp.Path
    94  	diffs []string
    95  	err   error
    96  }
    97  
    98  // PushStep implements the cmp.Reporter.
    99  func (r *shortDiffReporter) PushStep(ps cmp.PathStep) {
   100  	r.path = append(r.path, ps)
   101  }
   102  
   103  // Report implements the cmp.Reporter.
   104  func (r *shortDiffReporter) Report(rs cmp.Result) {
   105  	if rs.Equal() {
   106  		return
   107  	}
   108  	cur := r.path.Last()
   109  	vx, vy := cur.Values()
   110  	t := cur.Type()
   111  	// Prefix struct values with the types to add clarity in output
   112  	if !vx.IsValid() && !vy.IsValid() {
   113  		r.err = fmt.Errorf("unable to diff %+v and %+v on path %#v", vx, vy, r.path)
   114  		return
   115  	}
   116  	diff := fmt.Sprintf("%#v:\n", r.path)
   117  	if vx.IsValid() {
   118  		diff += r.diffString("-", t, vx)
   119  	}
   120  	if vy.IsValid() {
   121  		diff += r.diffString("+", t, vy)
   122  	}
   123  	r.diffs = append(r.diffs, diff)
   124  }
   125  
   126  func (r *shortDiffReporter) diffString(diffType string, t reflect.Type, v reflect.Value) string {
   127  	if t.Kind() == reflect.Struct {
   128  		return fmt.Sprintf("\t%s: %+v: \"%+v\"\n", diffType, t, v)
   129  	}
   130  	return fmt.Sprintf("\t%s: \"%+v\"\n", diffType, v)
   131  }
   132  
   133  // PopStep implements the cmp.Reporter.
   134  func (r *shortDiffReporter) PopStep() {
   135  	r.path = r.path[:len(r.path)-1]
   136  }
   137  
   138  // Diff returns the generated short diff for this object.
   139  // cmp.Equal should be called before this method.
   140  func (r *shortDiffReporter) Diff() (string, error) {
   141  	if r.err != nil {
   142  		return "", r.err
   143  	}
   144  	return strings.Join(r.diffs, ""), nil
   145  }