Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export the Reporter API #123

Merged
merged 1 commit into from
Mar 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions cmp/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand All @@ -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})
}
}

Expand Down
59 changes: 59 additions & 0 deletions cmp/example_reporter_test.go
Original file line number Diff line number Diff line change
@@ -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>
}
73 changes: 46 additions & 27 deletions cmp/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
}

Expand Down
8 changes: 4 additions & 4 deletions cmp/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
}}

Expand Down
4 changes: 2 additions & 2 deletions cmp/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
41 changes: 21 additions & 20 deletions cmp/report_value.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}