mirror of https://github.com/knative/pkg.git
🦆 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:
parent
b6044a7d17
commit
d6a2e27f7b
|
@ -20,7 +20,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/knative/pkg/kmp"
|
||||
)
|
||||
|
||||
// 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.
|
||||
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.
|
||||
input.Populate()
|
||||
|
||||
// Serialize the input to JSON and deserialize that into the provided instance
|
||||
// of the type that we are checking.
|
||||
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 {
|
||||
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
|
||||
// output resource.
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
package duck
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
@ -101,6 +102,15 @@ func TestMatches(t *testing.T) {
|
|||
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 {
|
||||
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 _ Populatable = (*Foo)(nil)
|
||||
|
||||
func (_ *Fooable) GetFullType() Populatable {
|
||||
func (*Fooable) GetFullType() Populatable {
|
||||
return &Foo{}
|
||||
}
|
||||
|
||||
|
@ -191,7 +251,7 @@ type BarStatus struct {
|
|||
var _ Implementable = (*Barable)(nil)
|
||||
var _ Populatable = (*Bar)(nil)
|
||||
|
||||
func (_ *Barable) GetFullType() Populatable {
|
||||
func (*Barable) GetFullType() Populatable {
|
||||
return &Bar{}
|
||||
}
|
||||
|
||||
|
@ -218,7 +278,7 @@ type SliceStatus struct {
|
|||
var _ Implementable = (*Sliceable)(nil)
|
||||
var _ Populatable = (*Slice)(nil)
|
||||
|
||||
func (_ *Sliceable) GetFullType() Populatable {
|
||||
func (*Sliceable) GetFullType() Populatable {
|
||||
return &Slice{}
|
||||
}
|
||||
|
||||
|
@ -238,7 +298,7 @@ type StringStatus struct {
|
|||
var _ Implementable = (*Stringable)(nil)
|
||||
var _ Populatable = (*String)(nil)
|
||||
|
||||
func (_ *Stringable) GetFullType() Populatable {
|
||||
func (*Stringable) GetFullType() Populatable {
|
||||
return &String{}
|
||||
}
|
||||
|
||||
|
@ -248,3 +308,59 @@ func (f *String) Populate() {
|
|||
|
||||
// We have to do this for Stringable because we're aliasing a value type.
|
||||
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
|
||||
}
|
||||
|
|
14
kmp/diff.go
14
kmp/diff.go
|
@ -49,3 +49,17 @@ func SafeDiff(x, y interface{}, opts ...cmp.Option) (diff string, err error) {
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
package kmp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
|
||||
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 != "" {
|
||||
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) {
|
||||
|
@ -44,11 +51,23 @@ func TestRecovery(t *testing.T) {
|
|||
a := foo{"a"}
|
||||
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 {
|
||||
t.Fatalf("expected err, got nil")
|
||||
t.Errorf("expected err, got nil")
|
||||
} else if diff := cmp.Diff(want, err.Error()); 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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue