Files
grpc-go/status/status_test.go
Joe Tsai 2d2f65684c cleanup: fix generic comparisons on protobuf messages (#3153)
Generated protobuf messages contain internal data structures
that general purpose comparison functions (e.g., reflect.DeepEqual,
pretty.Compare, etc) do not properly compare. It is already the case
today that these functions may report a difference when two messages
are actually semantically equivalent.

Fix all usages by either calling proto.Equal directly if
the top-level types are themselves proto.Message, or by calling
cmp.Equal with the cmp.Comparer(proto.Equal) option specified.
This option teaches cmp to use proto.Equal anytime it encounters
proto.Message types.
2019-11-06 14:25:07 -08:00

367 lines
9.4 KiB
Go

/*
*
* Copyright 2017 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package status
import (
"context"
"errors"
"fmt"
"testing"
"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/ptypes"
apb "github.com/golang/protobuf/ptypes/any"
dpb "github.com/golang/protobuf/ptypes/duration"
"github.com/google/go-cmp/cmp"
cpb "google.golang.org/genproto/googleapis/rpc/code"
epb "google.golang.org/genproto/googleapis/rpc/errdetails"
spb "google.golang.org/genproto/googleapis/rpc/status"
"google.golang.org/grpc/codes"
)
// errEqual is essentially a copy of testutils.StatusErrEqual(), to avoid a
// cyclic dependency.
func errEqual(err1, err2 error) bool {
status1, ok := FromError(err1)
if !ok {
return false
}
status2, ok := FromError(err2)
if !ok {
return false
}
return proto.Equal(status1.Proto(), status2.Proto())
}
func TestErrorsWithSameParameters(t *testing.T) {
const description = "some description"
e1 := Errorf(codes.AlreadyExists, description)
e2 := Errorf(codes.AlreadyExists, description)
if e1 == e2 || !errEqual(e1, e2) {
t.Fatalf("Errors should be equivalent but unique - e1: %v, %v e2: %p, %v", e1.(*statusError), e1, e2.(*statusError), e2)
}
}
func TestFromToProto(t *testing.T) {
s := &spb.Status{
Code: int32(codes.Internal),
Message: "test test test",
Details: []*apb.Any{{TypeUrl: "foo", Value: []byte{3, 2, 1}}},
}
err := FromProto(s)
if got := err.Proto(); !proto.Equal(s, got) {
t.Fatalf("Expected errors to be identical - s: %v got: %v", s, got)
}
}
func TestFromNilProto(t *testing.T) {
tests := []*Status{nil, FromProto(nil)}
for _, s := range tests {
if c := s.Code(); c != codes.OK {
t.Errorf("s: %v - Expected s.Code() = OK; got %v", s, c)
}
if m := s.Message(); m != "" {
t.Errorf("s: %v - Expected s.Message() = \"\"; got %q", s, m)
}
if p := s.Proto(); p != nil {
t.Errorf("s: %v - Expected s.Proto() = nil; got %q", s, p)
}
if e := s.Err(); e != nil {
t.Errorf("s: %v - Expected s.Err() = nil; got %v", s, e)
}
}
}
func TestError(t *testing.T) {
err := Error(codes.Internal, "test description")
if got, want := err.Error(), "rpc error: code = Internal desc = test description"; got != want {
t.Fatalf("err.Error() = %q; want %q", got, want)
}
s, _ := FromError(err)
if got, want := s.Code(), codes.Internal; got != want {
t.Fatalf("err.Code() = %s; want %s", got, want)
}
if got, want := s.Message(), "test description"; got != want {
t.Fatalf("err.Message() = %s; want %s", got, want)
}
}
func TestErrorOK(t *testing.T) {
err := Error(codes.OK, "foo")
if err != nil {
t.Fatalf("Error(codes.OK, _) = %p; want nil", err.(*statusError))
}
}
func TestErrorProtoOK(t *testing.T) {
s := &spb.Status{Code: int32(codes.OK)}
if got := ErrorProto(s); got != nil {
t.Fatalf("ErrorProto(%v) = %v; want nil", s, got)
}
}
func TestFromError(t *testing.T) {
code, message := codes.Internal, "test description"
err := Error(code, message)
s, ok := FromError(err)
if !ok || s.Code() != code || s.Message() != message || s.Err() == nil {
t.Fatalf("FromError(%v) = %v, %v; want <Code()=%s, Message()=%q, Err()!=nil>, true", err, s, ok, code, message)
}
}
func TestFromErrorOK(t *testing.T) {
code, message := codes.OK, ""
s, ok := FromError(nil)
if !ok || s.Code() != code || s.Message() != message || s.Err() != nil {
t.Fatalf("FromError(nil) = %v, %v; want <Code()=%s, Message()=%q, Err=nil>, true", s, ok, code, message)
}
}
type customError struct {
Code codes.Code
Message string
Details []*apb.Any
}
func (c customError) Error() string {
return fmt.Sprintf("rpc error: code = %s desc = %s", c.Code, c.Message)
}
func (c customError) GRPCStatus() *Status {
return &Status{
s: &spb.Status{
Code: int32(c.Code),
Message: c.Message,
Details: c.Details,
},
}
}
func TestFromErrorImplementsInterface(t *testing.T) {
code, message := codes.Internal, "test description"
details := []*apb.Any{{
TypeUrl: "testUrl",
Value: []byte("testValue"),
}}
err := customError{
Code: code,
Message: message,
Details: details,
}
s, ok := FromError(err)
if !ok || s.Code() != code || s.Message() != message || s.Err() == nil {
t.Fatalf("FromError(%v) = %v, %v; want <Code()=%s, Message()=%q, Err()!=nil>, true", err, s, ok, code, message)
}
pd := s.Proto().GetDetails()
if len(pd) != 1 || !proto.Equal(pd[0], details[0]) {
t.Fatalf("s.Proto.GetDetails() = %v; want <Details()=%s>", pd, details)
}
}
func TestFromErrorUnknownError(t *testing.T) {
code, message := codes.Unknown, "unknown error"
err := errors.New("unknown error")
s, ok := FromError(err)
if ok || s.Code() != code || s.Message() != message {
t.Fatalf("FromError(%v) = %v, %v; want <Code()=%s, Message()=%q>, false", err, s, ok, code, message)
}
}
func TestConvertKnownError(t *testing.T) {
code, message := codes.Internal, "test description"
err := Error(code, message)
s := Convert(err)
if s.Code() != code || s.Message() != message {
t.Fatalf("Convert(%v) = %v; want <Code()=%s, Message()=%q>", err, s, code, message)
}
}
func TestConvertUnknownError(t *testing.T) {
code, message := codes.Unknown, "unknown error"
err := errors.New("unknown error")
s := Convert(err)
if s.Code() != code || s.Message() != message {
t.Fatalf("Convert(%v) = %v; want <Code()=%s, Message()=%q>", err, s, code, message)
}
}
func TestStatus_ErrorDetails(t *testing.T) {
tests := []struct {
code codes.Code
details []proto.Message
}{
{
code: codes.NotFound,
details: nil,
},
{
code: codes.NotFound,
details: []proto.Message{
&epb.ResourceInfo{
ResourceType: "book",
ResourceName: "projects/1234/books/5678",
Owner: "User",
},
},
},
{
code: codes.Internal,
details: []proto.Message{
&epb.DebugInfo{
StackEntries: []string{
"first stack",
"second stack",
},
},
},
},
{
code: codes.Unavailable,
details: []proto.Message{
&epb.RetryInfo{
RetryDelay: &dpb.Duration{Seconds: 60},
},
&epb.ResourceInfo{
ResourceType: "book",
ResourceName: "projects/1234/books/5678",
Owner: "User",
},
},
},
}
for _, tc := range tests {
s, err := New(tc.code, "").WithDetails(tc.details...)
if err != nil {
t.Fatalf("(%v).WithDetails(%+v) failed: %v", str(s), tc.details, err)
}
details := s.Details()
for i := range details {
if !proto.Equal(details[i].(proto.Message), tc.details[i]) {
t.Fatalf("(%v).Details()[%d] = %+v, want %+v", str(s), i, details[i], tc.details[i])
}
}
}
}
func TestStatus_WithDetails_Fail(t *testing.T) {
tests := []*Status{
nil,
FromProto(nil),
New(codes.OK, ""),
}
for _, s := range tests {
if s, err := s.WithDetails(); err == nil || s != nil {
t.Fatalf("(%v).WithDetails(%+v) = %v, %v; want nil, non-nil", str(s), []proto.Message{}, s, err)
}
}
}
func TestStatus_ErrorDetails_Fail(t *testing.T) {
tests := []struct {
s *Status
i []interface{}
}{
{
nil,
nil,
},
{
FromProto(nil),
nil,
},
{
New(codes.OK, ""),
[]interface{}{},
},
{
FromProto(&spb.Status{
Code: int32(cpb.Code_CANCELLED),
Details: []*apb.Any{
{
TypeUrl: "",
Value: []byte{},
},
mustMarshalAny(&epb.ResourceInfo{
ResourceType: "book",
ResourceName: "projects/1234/books/5678",
Owner: "User",
}),
},
}),
[]interface{}{
errors.New(`message type url "" is invalid`),
&epb.ResourceInfo{
ResourceType: "book",
ResourceName: "projects/1234/books/5678",
Owner: "User",
},
},
},
}
for _, tc := range tests {
got := tc.s.Details()
if !cmp.Equal(got, tc.i, cmp.Comparer(proto.Equal), cmp.Comparer(equalError)) {
t.Errorf("(%v).Details() = %+v, want %+v", str(tc.s), got, tc.i)
}
}
}
func equalError(x, y error) bool {
return x == y || (x != nil && y != nil && x.Error() == y.Error())
}
func str(s *Status) string {
if s == nil {
return "nil"
}
if s.s == nil {
return "<Code=OK>"
}
return fmt.Sprintf("<Code=%v, Message=%q, Details=%+v>", codes.Code(s.s.GetCode()), s.s.GetMessage(), s.s.GetDetails())
}
// mustMarshalAny converts a protobuf message to an any.
func mustMarshalAny(msg proto.Message) *apb.Any {
any, err := ptypes.MarshalAny(msg)
if err != nil {
panic(fmt.Sprintf("ptypes.MarshalAny(%+v) failed: %v", msg, err))
}
return any
}
func TestFromContextError(t *testing.T) {
testCases := []struct {
in error
want *Status
}{
{in: nil, want: New(codes.OK, "")},
{in: context.DeadlineExceeded, want: New(codes.DeadlineExceeded, context.DeadlineExceeded.Error())},
{in: context.Canceled, want: New(codes.Canceled, context.Canceled.Error())},
{in: errors.New("other"), want: New(codes.Unknown, "other")},
}
for _, tc := range testCases {
got := FromContextError(tc.in)
if got.Code() != tc.want.Code() || got.Message() != tc.want.Message() {
t.Errorf("FromContextError(%v) = %v; want %v", tc.in, got, tc.want)
}
}
}