aboutsummaryrefslogtreecommitdiff
path: root/cmp
diff options
context:
space:
mode:
authorJoe Tsai <joetsai@digital-static.net>2019-03-11 18:07:37 -0700
committerGitHub <noreply@github.com>2019-03-11 18:07:37 -0700
commit3177a94b4e99d833e6c0d9f864faeb482f8a7f6d (patch)
tree6ee4458d84d5f5724989dc9b65ec646909418cb8 /cmp
parent2940eda701e08ed0bd3cda4a6c69efb50af6db51 (diff)
downloadgo-cmp-3177a94b4e99d833e6c0d9f864faeb482f8a7f6d.tar.gz
Export the Reporter API (#123)
The Reporter option allows users to hook in their own custom reporters to programatically interpret the diff structure. The Reporter API uses a push/pop mechanism, which is strictly more powerful than an API that only calls report on leaf nodes. The Reporter.Report method takes in a Result type to provide flexibility in what properties can be reported in the future since new properties can be added, but new methods cannot be easily added to the reporter interface.
Diffstat (limited to 'cmp')
-rw-r--r--cmp/compare.go19
-rw-r--r--cmp/example_reporter_test.go59
-rw-r--r--cmp/options.go73
-rw-r--r--cmp/options_test.go8
-rw-r--r--cmp/report.go4
-rw-r--r--cmp/report_value.go41
6 files changed, 142 insertions, 62 deletions
diff --git a/cmp/compare.go b/cmp/compare.go
index 2762733..9fe9a4c 100644
--- a/cmp/compare.go
+++ b/cmp/compare.go
@@ -120,10 +120,11 @@ func Equal(x, y interface{}, opts ...Option) bool {
// readable outputs. In such cases, the string is prefixed with either an
// 's' or 'e' character, respectively, to indicate that the method was called.
//
-// Do not depend on this output being stable.
+// Do not depend on this output being stable. If you need the ability to
+// programmatically interpret the difference, consider using a custom Reporter.
func Diff(x, y interface{}, opts ...Option) string {
r := new(defaultReporter)
- opts = Options{Options(opts), reporter(r)}
+ opts = Options{Options(opts), Reporter(r)}
eq := Equal(x, y, opts...)
d := r.String()
if (d == "") != eq {
@@ -135,9 +136,9 @@ func Diff(x, y interface{}, opts ...Option) string {
type state struct {
// These fields represent the "comparison state".
// Calling statelessCompare must not result in observable changes to these.
- result diff.Result // The current result of comparison
- curPath Path // The current path in the value tree
- reporters []reporterOption // Optional reporters
+ result diff.Result // The current result of comparison
+ curPath Path // The current path in the value tree
+ reporters []reporter // Optional reporters
// recChecker checks for infinite cycles applying the same set of
// transformers upon the output of itself.
@@ -183,7 +184,7 @@ func (s *state) processOption(opt Option) {
for t := range opt {
s.exporters[t] = true
}
- case reporterOption:
+ case reporter:
s.reporters = append(s.reporters, opt)
default:
panic(fmt.Sprintf("unknown option %T", opt))
@@ -529,8 +530,8 @@ func (s *state) compareMap(t reflect.Type, vx, vy reflect.Value) {
}
}
-func (s *state) report(eq bool, rf reportFlags) {
- if rf&reportIgnored == 0 {
+func (s *state) report(eq bool, rf resultFlags) {
+ if rf&reportByIgnore == 0 {
if eq {
s.result.NumSame++
rf |= reportEqual
@@ -540,7 +541,7 @@ func (s *state) report(eq bool, rf reportFlags) {
}
}
for _, r := range s.reporters {
- r.Report(rf)
+ r.Report(Result{flags: rf})
}
}
diff --git a/cmp/example_reporter_test.go b/cmp/example_reporter_test.go
new file mode 100644
index 0000000..bc1932e
--- /dev/null
+++ b/cmp/example_reporter_test.go
@@ -0,0 +1,59 @@
+// Copyright 2019, The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE.md file.
+
+package cmp_test
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+// DiffReporter is a simple custom reporter that only records differences
+// detected during comparison.
+type DiffReporter struct {
+ path cmp.Path
+ diffs []string
+}
+
+func (r *DiffReporter) PushStep(ps cmp.PathStep) {
+ r.path = append(r.path, ps)
+}
+
+func (r *DiffReporter) Report(rs cmp.Result) {
+ if !rs.Equal() {
+ vx, vy := r.path.Last().Values()
+ r.diffs = append(r.diffs, fmt.Sprintf("%#v:\n\t-: %+v\n\t+: %+v\n", r.path, vx, vy))
+ }
+}
+
+func (r *DiffReporter) PopStep() {
+ r.path = r.path[:len(r.path)-1]
+}
+
+func (r *DiffReporter) String() string {
+ return strings.Join(r.diffs, "\n")
+}
+
+func ExampleReporter() {
+ x, y := MakeGatewayInfo()
+
+ var r DiffReporter
+ cmp.Equal(x, y, cmp.Reporter(&r))
+ fmt.Print(r.String())
+
+ // Output:
+ // {cmp_test.Gateway}.IPAddress:
+ // -: 192.168.0.1
+ // +: 192.168.0.2
+ //
+ // {cmp_test.Gateway}.Clients[4].IPAddress:
+ // -: 192.168.0.219
+ // +: 192.168.0.221
+ //
+ // {cmp_test.Gateway}.Clients[5->?]:
+ // -: {Hostname:americano IPAddress:192.168.0.188 LastSeen:2009-11-10 23:03:05 +0000 UTC}
+ // +: <invalid reflect.Value>
+}
diff --git a/cmp/options.go b/cmp/options.go
index a265597..6e52fee 100644
--- a/cmp/options.go
+++ b/cmp/options.go
@@ -199,7 +199,7 @@ type ignore struct{ core }
func (ignore) isFiltered() bool { return false }
func (ignore) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { return ignore{} }
-func (ignore) apply(s *state, _, _ reflect.Value) { s.report(true, reportIgnored) }
+func (ignore) apply(s *state, _, _ reflect.Value) { s.report(true, reportByIgnore) }
func (ignore) String() string { return "Ignore()" }
// validator is a sentinel Option type to indicate that some options could not
@@ -407,68 +407,87 @@ func (visibleStructs) filter(_ *state, _ reflect.Type, _, _ reflect.Value) appli
panic("not implemented")
}
-// reportFlags is a bit-set representing how a comparison was determined.
-type reportFlags uint
+// Result represents the comparison result for a single node and
+// is provided by cmp when calling Result (see Reporter).
+type Result struct {
+ _ [0]func() // Make Result incomparable
+ flags resultFlags
+}
+
+// Equal reports whether the node was determined to be equal or not.
+// As a special case, ignored nodes are considered equal.
+func (r Result) Equal() bool {
+ return r.flags&(reportEqual|reportByIgnore) != 0
+}
+
+// ByIgnore reports whether the node is equal because it was ignored.
+// This never reports true if Equal reports false.
+func (r Result) ByIgnore() bool {
+ return r.flags&reportByIgnore != 0
+}
+
+// ByMethod reports whether the Equal method determined equality.
+func (r Result) ByMethod() bool {
+ return r.flags&reportByMethod != 0
+}
+
+// ByFunc reports whether a Comparer function determined equality.
+func (r Result) ByFunc() bool {
+ return r.flags&reportByFunc != 0
+}
+
+type resultFlags uint
const (
- _ reportFlags = (1 << iota) / 2
+ _ resultFlags = (1 << iota) / 2
- // reportEqual reports whether the node is equal.
- // This may be ORed with reportByMethod or reportByFunc.
reportEqual
- // reportUnequal reports whether the node is not equal.
- // This may be ORed with reportByMethod or reportByFunc.
reportUnequal
- // reportIgnored reports whether the node was ignored.
- reportIgnored
-
- // reportByMethod reports whether equality was determined by calling the
- // Equal method. This may be ORed with reportEqual or reportUnequal.
+ reportByIgnore
reportByMethod
- // reportByFunc reports whether equality was determined by calling a custom
- // Comparer function. This may be ORed with reportEqual or reportUnequal.
reportByFunc
)
-// reporter is an Option that can be passed to Equal. When Equal traverses
+// Reporter is an Option that can be passed to Equal. When Equal traverses
// the value trees, it calls PushStep as it descends into each node in the
// tree and PopStep as it ascend out of the node. The leaves of the tree are
// either compared (determined to be equal or not equal) or ignored and reported
// as such by calling the Report method.
-func reporter(r interface {
- // TODO: Export this option.
-
+func Reporter(r interface {
// PushStep is called when a tree-traversal operation is performed.
// The PathStep itself is only valid until the step is popped.
- // The PathStep.Values are valid for the duration of the entire traversal.
+ // The PathStep.Values are valid for the duration of the entire traversal
+ // and must not be mutated.
//
- // Equal always call PushStep at the start to provide an operation-less
+ // Equal always calls PushStep at the start to provide an operation-less
// PathStep used to report the root values.
//
+ // Within a slice, the exact set of inserted, removed, or modified elements
+ // is unspecified and may change in future implementations.
// The entries of a map are iterated through in an unspecified order.
PushStep(PathStep)
- // Report is called at exactly once on leaf nodes to report whether the
+ // Report is called exactly once on leaf nodes to report whether the
// comparison identified the node as equal, unequal, or ignored.
// A leaf node is one that is immediately preceded by and followed by
// a pair of PushStep and PopStep calls.
- Report(reportFlags)
+ Report(Result)
// PopStep ascends back up the value tree.
// There is always a matching pop call for every push call.
PopStep()
}) Option {
- return reporterOption{r}
+ return reporter{r}
}
-type reporterOption struct{ reporterIface }
+type reporter struct{ reporterIface }
type reporterIface interface {
PushStep(PathStep)
- Report(reportFlags)
+ Report(Result)
PopStep()
}
-func (reporterOption) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption {
+func (reporter) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption {
panic("not implemented")
}
diff --git a/cmp/options_test.go b/cmp/options_test.go
index f876eab..f8066c7 100644
--- a/cmp/options_test.go
+++ b/cmp/options_test.go
@@ -128,7 +128,7 @@ func TestOptionPanic(t *testing.T) {
}, {
label: "FilterPath",
fnc: FilterPath,
- args: []interface{}{func(Path) bool { return true }, reporter(&defaultReporter{})},
+ args: []interface{}{func(Path) bool { return true }, Reporter(&defaultReporter{})},
wantPanic: "invalid option type",
}, {
label: "FilterPath",
@@ -137,7 +137,7 @@ func TestOptionPanic(t *testing.T) {
}, {
label: "FilterPath",
fnc: FilterPath,
- args: []interface{}{func(Path) bool { return true }, Options{Ignore(), reporter(&defaultReporter{})}},
+ args: []interface{}{func(Path) bool { return true }, Options{Ignore(), Reporter(&defaultReporter{})}},
wantPanic: "invalid option type",
}, {
label: "FilterValues",
@@ -170,7 +170,7 @@ func TestOptionPanic(t *testing.T) {
}, {
label: "FilterValues",
fnc: FilterValues,
- args: []interface{}{func(int, int) bool { return true }, reporter(&defaultReporter{})},
+ args: []interface{}{func(int, int) bool { return true }, Reporter(&defaultReporter{})},
wantPanic: "invalid option type",
}, {
label: "FilterValues",
@@ -179,7 +179,7 @@ func TestOptionPanic(t *testing.T) {
}, {
label: "FilterValues",
fnc: FilterValues,
- args: []interface{}{func(int, int) bool { return true }, Options{Ignore(), reporter(&defaultReporter{})}},
+ args: []interface{}{func(int, int) bool { return true }, Options{Ignore(), Reporter(&defaultReporter{})}},
wantPanic: "invalid option type",
}}
diff --git a/cmp/report.go b/cmp/report.go
index 6810a50..6ddf299 100644
--- a/cmp/report.go
+++ b/cmp/report.go
@@ -26,8 +26,8 @@ func (r *defaultReporter) PushStep(ps PathStep) {
r.root = r.curr
}
}
-func (r *defaultReporter) Report(f reportFlags) {
- r.curr.Report(f)
+func (r *defaultReporter) Report(rs Result) {
+ r.curr.Report(rs)
}
func (r *defaultReporter) PopStep() {
r.curr = r.curr.PopStep()
diff --git a/cmp/report_value.go b/cmp/report_value.go
index fcff486..83031a7 100644
--- a/cmp/report_value.go
+++ b/cmp/report_value.go
@@ -80,41 +80,42 @@ func (parent *valueNode) PushStep(ps PathStep) (child *valueNode) {
return child
}
-func (r *valueNode) Report(f reportFlags) {
+func (r *valueNode) Report(rs Result) {
assert(r.MaxDepth == 0) // May only be called on leaf nodes
- if f&reportEqual > 0 {
- r.NumSame++
- }
- if f&reportUnequal > 0 {
- r.NumDiff++
- }
- if f&reportIgnored > 0 {
+ if rs.ByIgnore() {
r.NumIgnored++
+ } else {
+ if rs.Equal() {
+ r.NumSame++
+ } else {
+ r.NumDiff++
+ }
}
assert(r.NumSame+r.NumDiff+r.NumIgnored == 1)
- if f&reportByMethod > 0 {
+ if rs.ByMethod() {
r.NumCompared++
}
- if f&reportByFunc > 0 {
+ if rs.ByFunc() {
r.NumCompared++
}
assert(r.NumCompared <= 1)
}
func (child *valueNode) PopStep() (parent *valueNode) {
+ if child.parent == nil {
+ return nil
+ }
parent = child.parent
- if parent != nil {
- parent.NumSame += child.NumSame
- parent.NumDiff += child.NumDiff
- parent.NumIgnored += child.NumIgnored
- parent.NumCompared += child.NumCompared
- parent.NumTransformed += child.NumTransformed
- parent.NumChildren += child.NumChildren + 1
- if parent.MaxDepth < child.MaxDepth+1 {
- parent.MaxDepth = child.MaxDepth + 1
- }
+ parent.NumSame += child.NumSame
+ parent.NumDiff += child.NumDiff
+ parent.NumIgnored += child.NumIgnored
+ parent.NumCompared += child.NumCompared
+ parent.NumTransformed += child.NumTransformed
+ parent.NumChildren += child.NumChildren + 1
+ if parent.MaxDepth < child.MaxDepth+1 {
+ parent.MaxDepth = child.MaxDepth + 1
}
return parent
}