package testing import ( "bytes" "encoding/hex" "fmt" "io" "math" "reflect" "github.com/aws/smithy-go/document" "github.com/aws/smithy-go/middleware" ) // CompareValues compares two values to determine if they are equal, // specialized for comparison of SDK operation output types. // // CompareValues expects the two values to be of the same underlying type. // Doing otherwise will result in undefined behavior. // // The third variadic argument is vestigial from a previous implementation that // depended on go-cmp. Values passed therein have no effect. func CompareValues(expect, actual interface{}, _ ...interface{}) error { return deepEqual(reflect.ValueOf(expect), reflect.ValueOf(actual), "") } func deepEqual(expect, actual reflect.Value, path string) error { if et, at := expect.Kind(), actual.Kind(); et != at { return fmt.Errorf("%s: kind %s != %s", path, et, at) } // there are a handful of short-circuit cases here within the context of // operation responses: // - ResultMetadata (we don't care) // - document.Interface (check for marshaled []byte equality) // - io.Reader (check for Read() []byte equality) ei, ai := expect.Interface(), actual.Interface() if _, _, ok := asMetadatas(ei, ai); ok { return nil } if e, a, ok := asDocuments(ei, ai); ok { if !compareDocumentTypes(e, a) { return fmt.Errorf("%s: document values unequal", path) } return nil } if e, a, ok := asReaders(ei, ai); ok { if err := CompareReaders(e, a); err != nil { return fmt.Errorf("%s: %w", path, err) } return nil } switch expect.Kind() { case reflect.Pointer: if expect.Type() != actual.Type() { return fmt.Errorf("%s: type mismatch", path) } expect = deref(expect) actual = deref(actual) ek, ak := expect.Kind(), actual.Kind() if ek == reflect.Invalid || ak == reflect.Invalid { // one was a nil pointer, so they both must be nil if ek == ak { return nil } return fmt.Errorf("%s: %s != %s", path, fmtNil(ek), fmtNil(ak)) } if err := deepEqual(expect, actual, path); err != nil { return err } return nil case reflect.Slice: if expect.Len() != actual.Len() { return fmt.Errorf("%s: slice length unequal", path) } for i := 0; i < expect.Len(); i++ { ipath := fmt.Sprintf("%s[%d]", path, i) if err := deepEqual(expect.Index(i), actual.Index(i), ipath); err != nil { return err } } return nil case reflect.Map: if expect.Len() != actual.Len() { return fmt.Errorf("%s: map length unequal", path) } for _, k := range expect.MapKeys() { kpath := fmt.Sprintf("%s[%q]", path, k.String()) if err := deepEqual(expect.MapIndex(k), actual.MapIndex(k), kpath); err != nil { return err } } return nil case reflect.Struct: for i := 0; i < expect.NumField(); i++ { if !expect.Field(i).CanInterface() { continue // unexported } fpath := fmt.Sprintf("%s.%s", path, expect.Type().Field(i).Name) if err := deepEqual(expect.Field(i), actual.Field(i), fpath); err != nil { return err } } return nil case reflect.Float32, reflect.Float64: // NaN != NaN by definition but we just care about bitwise equality ef, af := math.Float64bits(expect.Float()), math.Float64bits(actual.Float()) if ef != af { return fmt.Errorf("%s: float 0x%x != 0x%x", path, ef, af) } return nil default: // everything else is just scalars and can be delegated if !reflect.DeepEqual(ei, ai) { return fmt.Errorf("%s: %v != %v", path, ei, ai) } return nil } } func asMetadatas(i, j interface{}) (ii, jj middleware.Metadata, ok bool) { ii, iok := i.(middleware.Metadata) jj, jok := j.(middleware.Metadata) return ii, jj, iok || jok } func asDocuments(i, j interface{}) (ii, jj documentInterface, ok bool) { ii, iok := i.(documentInterface) jj, jok := j.(documentInterface) return ii, jj, iok || jok } func asReaders(i, j interface{}) (ii, jj io.Reader, ok bool) { ii, iok := i.(io.Reader) jj, jok := j.(io.Reader) return ii, jj, iok || jok } func deref(v reflect.Value) reflect.Value { switch v.Kind() { case reflect.Interface, reflect.Ptr: for v.Kind() == reflect.Interface || v.Kind() == reflect.Ptr { v = v.Elem() } } return v } type documentInterface interface { document.Marshaler document.Unmarshaler } func compareDocumentTypes(x documentInterface, y documentInterface) bool { if x == nil { x = nopMarshaler{} } if y == nil { y = nopMarshaler{} } xBytes, err := x.MarshalSmithyDocument() if err != nil { panic(fmt.Sprintf("MarshalSmithyDocument error: %v", err)) } yBytes, err := y.MarshalSmithyDocument() if err != nil { panic(fmt.Sprintf("MarshalSmithyDocument error: %v", err)) } return JSONEqual(xBytes, yBytes) == nil } // CompareReaders two io.Reader values together to determine if they are equal. // Will read the contents of the readers until they are empty. func CompareReaders(expect, actual io.Reader) error { if expect == nil { expect = nopReader{} } if actual == nil { actual = nopReader{} } e, err := io.ReadAll(expect) if err != nil { return fmt.Errorf("failed to read expect body, %w", err) } a, err := io.ReadAll(actual) if err != nil { return fmt.Errorf("failed to read actual body, %w", err) } if !bytes.Equal(e, a) { return fmt.Errorf("bytes do not match\nexpect:\n%s\nactual:\n%s", hex.Dump(e), hex.Dump(a)) } return nil } func fmtNil(k reflect.Kind) string { if k == reflect.Invalid { return "nil" } return "non-nil" } type nopReader struct{} func (nopReader) Read(p []byte) (int, error) { return 0, io.EOF } type nopMarshaler struct{} func (nopMarshaler) MarshalSmithyDocument() ([]byte, error) { return nil, nil } func (nopMarshaler) UnmarshalSmithyDocument(v interface{}) error { return nil }