🦆 Duck Typing - add a ConformsToType helper (#220)

* duck typing - add a ConformsToType helper

Unlike VerifyType, ConformsToType will return the following:
- an error when any marshalling/unmarshalling fails
- false when the concrete type does not implement the duck type
- true when the concrete type implements the duck type

* use knative/pkg kmp to handle panics raised by go-cmp
This commit is contained in:
Dave Protasowski 2019-01-09 22:58:42 -05:00 committed by Knative Prow Robot
parent b6044a7d17
commit d6a2e27f7b
4 changed files with 193 additions and 19 deletions

View File

@ -20,7 +20,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/google/go-cmp/cmp" "github.com/knative/pkg/kmp"
) )
// Implementable is implemented by the Fooable duck type that consumers // Implementable is implemented by the Fooable duck type that consumers
@ -56,30 +56,55 @@ func VerifyType(instance interface{}, iface Implementable) error {
// that we will compare at the end. // that we will compare at the end.
input, output := iface.GetFullType(), iface.GetFullType() input, output := iface.GetFullType(), iface.GetFullType()
if err := roundTrip(instance, input, output); err != nil {
return err
}
// Now verify that we were able to roundtrip all of our fields through the type
// we are checking.
if diff, err := kmp.SafeDiff(input, output); err != nil {
return err
} else if diff != "" {
return fmt.Errorf("%T does not implement the duck type %T, the following fields were lost: %s",
instance, iface, diff)
}
return nil
}
// ConformsToType will return true or false depending on whether a
// concrete resource properly implements the provided Implementable
// duck type.
//
// It will return an error if marshal/unmarshalling fails
func ConformsToType(instance interface{}, iface Implementable) (bool, error) {
input, output := iface.GetFullType(), iface.GetFullType()
if err := roundTrip(instance, input, output); err != nil {
return false, err
}
return kmp.SafeEqual(input, output)
}
func roundTrip(instance interface{}, input, output Populatable) error {
// Populate our input resource with values we will roundtrip. // Populate our input resource with values we will roundtrip.
input.Populate() input.Populate()
// Serialize the input to JSON and deserialize that into the provided instance // Serialize the input to JSON and deserialize that into the provided instance
// of the type that we are checking. // of the type that we are checking.
if before, err := json.Marshal(input); err != nil { if before, err := json.Marshal(input); err != nil {
return fmt.Errorf("error serializing duck type %T", input) return fmt.Errorf("error serializing duck type %T error: %s", input, err)
} else if err := json.Unmarshal(before, instance); err != nil { } else if err := json.Unmarshal(before, instance); err != nil {
return fmt.Errorf("error deserializing duck type %T into %T", input, instance) return fmt.Errorf("error deserializing duck type %T into %T error: %s", input, instance, err)
} }
// Serialize the instance we are checking to JSON and deserialize that into the // Serialize the instance we are checking to JSON and deserialize that into the
// output resource. // output resource.
if after, err := json.Marshal(instance); err != nil { if after, err := json.Marshal(instance); err != nil {
return fmt.Errorf("error serializing %T", instance) return fmt.Errorf("error serializing %T error: %s", instance, err)
} else if err := json.Unmarshal(after, output); err != nil { } else if err := json.Unmarshal(after, output); err != nil {
return fmt.Errorf("error deserializing %T into dock type %T", instance, output) return fmt.Errorf("error deserializing %T into duck type %T error: %s", instance, output, err)
} }
// Now verify that we were able to roundtrip all of our fields through the type
// we are checking.
if diff := cmp.Diff(input, output); diff != "" {
return fmt.Errorf("%T does not implement the duck type %T, the following fields were lost: %s",
instance, iface, diff)
}
return nil return nil
} }

View File

@ -17,6 +17,7 @@ limitations under the License.
package duck package duck
import ( import (
"errors"
"testing" "testing"
) )
@ -101,6 +102,15 @@ func TestMatches(t *testing.T) {
t.Error(err) t.Error(err)
} }
ok, err := ConformsToType(test.instance, test.iface)
if err != nil {
t.Error(err)
}
if !ok {
t.Errorf("Expected %T to conform to %T", test.instance, test.iface)
}
}) })
} }
} }
@ -145,6 +155,56 @@ func TestMismatches(t *testing.T) {
if err := VerifyType(test.instance, test.iface); err == nil { if err := VerifyType(test.instance, test.iface); err == nil {
t.Errorf("Unexpected success %T implements %T", test.instance, test.iface) t.Errorf("Unexpected success %T implements %T", test.instance, test.iface)
} }
ok, err := ConformsToType(test.instance, test.iface)
if err != nil {
t.Error(err)
}
if ok {
t.Errorf("Expected %T to not conform to %T", test.instance, test.iface)
}
})
}
}
func TestErrors(t *testing.T) {
tests := []struct {
name string
instance interface{}
iface Implementable
}{{
name: "duck type - fails to marshal",
instance: &Foo{},
iface: &UnableToMarshal{},
}, {
name: "duck type - fails to unmarshal",
instance: &Foo{},
iface: &UnableToUnmarshal{},
}, {
name: "instance - fails to unmarshal",
instance: &UnableToUnmarshal{},
iface: &Fooable{},
}, {
name: "instance - fails to marshal",
instance: &UnableToMarshal{},
iface: &Fooable{},
}, {
name: "duck type - unexported fields",
instance: &Foo{},
iface: &UnexportedFields{},
}}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if err := VerifyType(test.instance, test.iface); err == nil {
t.Error("expected VerifyType to return an error")
}
if _, err := ConformsToType(test.instance, test.iface); err == nil {
t.Error("expected ConformsToType to return an error")
}
}) })
} }
} }
@ -164,7 +224,7 @@ type FooStatus struct {
var _ Implementable = (*Fooable)(nil) var _ Implementable = (*Fooable)(nil)
var _ Populatable = (*Foo)(nil) var _ Populatable = (*Foo)(nil)
func (_ *Fooable) GetFullType() Populatable { func (*Fooable) GetFullType() Populatable {
return &Foo{} return &Foo{}
} }
@ -191,7 +251,7 @@ type BarStatus struct {
var _ Implementable = (*Barable)(nil) var _ Implementable = (*Barable)(nil)
var _ Populatable = (*Bar)(nil) var _ Populatable = (*Bar)(nil)
func (_ *Barable) GetFullType() Populatable { func (*Barable) GetFullType() Populatable {
return &Bar{} return &Bar{}
} }
@ -218,7 +278,7 @@ type SliceStatus struct {
var _ Implementable = (*Sliceable)(nil) var _ Implementable = (*Sliceable)(nil)
var _ Populatable = (*Slice)(nil) var _ Populatable = (*Slice)(nil)
func (_ *Sliceable) GetFullType() Populatable { func (*Sliceable) GetFullType() Populatable {
return &Slice{} return &Slice{}
} }
@ -238,7 +298,7 @@ type StringStatus struct {
var _ Implementable = (*Stringable)(nil) var _ Implementable = (*Stringable)(nil)
var _ Populatable = (*String)(nil) var _ Populatable = (*String)(nil)
func (_ *Stringable) GetFullType() Populatable { func (*Stringable) GetFullType() Populatable {
return &String{} return &String{}
} }
@ -248,3 +308,59 @@ func (f *String) Populate() {
// We have to do this for Stringable because we're aliasing a value type. // We have to do this for Stringable because we're aliasing a value type.
var emptyStringable Stringable var emptyStringable Stringable
// For testing this doubles as the 'Implementable'
// and 'Populataable'
type UnableToMarshal struct{}
var _ Implementable = (*UnableToMarshal)(nil)
var _ Populatable = (*UnableToMarshal)(nil)
func (u *UnableToMarshal) GetFullType() Populatable {
return u
}
func (u *UnableToMarshal) Populate() {
return
}
func (u *UnableToMarshal) MarshalJSON() ([]byte, error) {
return nil, errors.New("I will never marshal for you")
}
// For testing this doubles as the 'Implementable'
// and 'Populatable'
type UnableToUnmarshal struct{}
var _ Implementable = (*UnableToUnmarshal)(nil)
var _ Populatable = (*UnableToUnmarshal)(nil)
func (u *UnableToUnmarshal) GetFullType() Populatable {
return u
}
func (u *UnableToUnmarshal) Populate() {
return
}
func (u *UnableToUnmarshal) UnmarshalJSON([]byte) error {
return errors.New("I will never unmarshal for you")
}
// For testing this doubles as the 'Implementable'
// and 'Populatable'
type UnexportedFields struct {
a string
}
var _ Implementable = (*UnexportedFields)(nil)
var _ Populatable = (*UnexportedFields)(nil)
func (u *UnexportedFields) GetFullType() Populatable {
return &UnexportedFields{}
}
func (u *UnexportedFields) Populate() {
u.a = "hello"
return
}

View File

@ -49,3 +49,17 @@ func SafeDiff(x, y interface{}, opts ...cmp.Option) (diff string, err error) {
return return
} }
func SafeEqual(x, y interface{}, opts ...cmp.Option) (equal bool, err error) {
// cmp.Equal will panic if we miss something; return error instead of crashing.
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered in kmp.SafeEqual: %v", r)
}
}()
opts = append(opts, defaultOpts...)
equal = cmp.Equal(x, y, opts...)
return
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package kmp package kmp
import ( import (
"fmt"
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
@ -30,10 +31,16 @@ func TestCompareKcmpDefault(t *testing.T) {
want := "{resource.Quantity}:\n\t-: resource.Quantity{i: resource.int64Amount{value: 50, scale: resource.Scale(-3)}, s: \"50m\", Format: resource.Format(\"DecimalSI\")}\n\t+: resource.Quantity{i: resource.int64Amount{value: 100, scale: resource.Scale(-3)}, s: \"100m\", Format: resource.Format(\"DecimalSI\")}\n" want := "{resource.Quantity}:\n\t-: resource.Quantity{i: resource.int64Amount{value: 50, scale: resource.Scale(-3)}, s: \"50m\", Format: resource.Format(\"DecimalSI\")}\n\t+: resource.Quantity{i: resource.int64Amount{value: 100, scale: resource.Scale(-3)}, s: \"100m\", Format: resource.Format(\"DecimalSI\")}\n"
if got, err := SafeDiff(a, b); err != nil { if got, err := SafeDiff(a, b); err != nil {
t.Fatalf("unexpected SafeDiff err: %v", err) t.Errorf("unexpected SafeDiff err: %v", err)
} else if diff := cmp.Diff(want, got); diff != "" { } else if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("SafeDiff (-want, +got): %v", diff) t.Errorf("SafeDiff (-want, +got): %v", diff)
} }
if got, err := SafeEqual(a, b); err != nil {
t.Fatalf("unexpected SafeEqual err: %v", err)
} else if diff := cmp.Diff(false, got); diff != "" {
t.Errorf("SafeEqual(-want, +got): %v", diff)
}
} }
func TestRecovery(t *testing.T) { func TestRecovery(t *testing.T) {
@ -44,11 +51,23 @@ func TestRecovery(t *testing.T) {
a := foo{"a"} a := foo{"a"}
b := foo{"b"} b := foo{"b"}
want := "recovered in kmp.SafeDiff: cannot handle unexported field: {kmp.foo}.bar\nconsider using AllowUnexported or cmpopts.IgnoreUnexported" want := recoveryErrorMessageFor("SafeDiff")
if _, err := SafeDiff(a, b); err == nil { if _, err := SafeDiff(a, b); err == nil {
t.Fatalf("expected err, got nil") t.Errorf("expected err, got nil")
} else if diff := cmp.Diff(want, err.Error()); diff != "" { } else if diff := cmp.Diff(want, err.Error()); diff != "" {
t.Errorf("SafeDiff (-want, +got): %v", diff) t.Errorf("SafeDiff (-want, +got): %v", diff)
} }
want = recoveryErrorMessageFor("SafeEqual")
if _, err := SafeEqual(a, b); err == nil {
t.Errorf("expected err, got nil")
} else if diff := cmp.Diff(want, err.Error()); diff != "" {
t.Errorf("SafeEqual (-want, +got): %v", diff)
}
}
func recoveryErrorMessageFor(funcName string) string {
return fmt.Sprintf(
`recovered in kmp.%v: cannot handle unexported field: {kmp.foo}.bar
consider using AllowUnexported or cmpopts.IgnoreUnexported`, funcName)
} }