diff --git a/ygot/diff.go b/ygot/diff.go index f35a2587..3cec08c3 100644 --- a/ygot/diff.go +++ b/ygot/diff.go @@ -24,6 +24,7 @@ import ( "github.com/openconfig/gnmi/errlist" "github.com/openconfig/ygot/internal/yreflect" "github.com/openconfig/ygot/util" + "google.golang.org/protobuf/encoding/prototext" "google.golang.org/protobuf/proto" gnmipb "github.com/openconfig/gnmi/proto/gnmi" @@ -488,6 +489,41 @@ func Diff(original, modified GoStruct, opts ...DiffOpt) (*gnmipb.Notification, e } } +// FormatDiff formats the output of ygot.Diff as a multiline string. This +// function is only intended for human consumption and ignores errors. Do not +// depend on the output being stable. It may change over time across different +// versions of the program. +func FormatDiff(n *gnmipb.Notification) string { + if n == nil { + return " Notification" + } + var build strings.Builder + for _, d := range n.Delete { + path, err := PathToString(d) + if err != nil { + path = prototext.Format(d) + } + if build.Len() != 0 { + build.WriteRune('\n') + } + build.WriteString(fmt.Sprintf("deleted: %s", path)) + } + for _, u := range n.Update { + path, err := PathToString(u.GetPath()) + if err != nil { + path = prototext.Format(u.GetPath()) + } + if build.Len() != 0 { + build.WriteRune('\n') + } + build.WriteString(fmt.Sprintf("new/updated %s: %v", path, u.GetVal())) + } + if build.Len() == 0 { + return "no diff" + } + return build.String() +} + // DiffWithAtomic takes an original and modified GoStruct, which must be of the same // type and returns a slice of gNMI Notifications that represents the diff // between them in a way that can be used to update a local gNMI.Subscribe diff --git a/ygot/diff_test.go b/ygot/diff_test.go index 70c7b968..628fe542 100644 --- a/ygot/diff_test.go +++ b/ygot/diff_test.go @@ -1576,6 +1576,9 @@ func TestDiff(t *testing.T) { t.Run(tt.desc+"Diff", func(t *testing.T) { got, err := Diff(tt.inOrig, tt.inMod, tt.inOpts...) testDiffSingleNotif(t, "Diff", got, err) + if diffout := FormatDiff(got); diffout == "" { + t.Errorf("FormatDiff returned empty") + } }) t.Run(tt.desc+"DiffWithAtomic", func(t *testing.T) { var got *gnmipb.Notification @@ -1589,6 +1592,53 @@ func TestDiff(t *testing.T) { t.Fatalf("Got multiple Notifications for DiffWithAtomic: %d, this is not expected.", len(gots)) } testDiffSingleNotif(t, "DiffWithAtomic", got, err) + if diffout := FormatDiff(got); diffout == "" { + t.Errorf("FormatDiff returned empty") + } + }) + } +} + +func TestFormatDiff(t *testing.T) { + tests := []struct { + desc string + inNotif *gnmipb.Notification + want string + }{{ + desc: "basic", + inNotif: &gnmipb.Notification{ + Delete: []*gnmipb.Path{{ + Elem: []*gnmipb.PathElem{{ + Name: "floatval", + }}, + }}, + Update: []*gnmipb.Update{{ + Path: &gnmipb.Path{ + Elem: []*gnmipb.PathElem{{ + Name: "int-val", + }}, + }, + Val: &gnmipb.TypedValue{Value: &gnmipb.TypedValue_IntVal{10}}, + }}, + }, + want: `deleted: /floatval +new/updated /int-val: int_val:10`, + }, { + desc: "empty", + inNotif: &gnmipb.Notification{}, + want: `no diff`, + }, { + desc: "nil", + inNotif: nil, + want: ` Notification`, + }} + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + got := FormatDiff(tt.inNotif) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("(-want, +got):\n%s", diff) + } }) } }