go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/testing/assertions/proto_tests.go (about)

     1  // Copyright 2018 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package assertions
    16  
    17  import (
    18  	"fmt"
    19  	"reflect"
    20  
    21  	"google.golang.org/protobuf/encoding/protojson"
    22  	"google.golang.org/protobuf/encoding/prototext"
    23  	"google.golang.org/protobuf/proto"
    24  	"google.golang.org/protobuf/testing/protocmp"
    25  
    26  	protoLegacy "github.com/golang/protobuf/proto"
    27  
    28  	"github.com/google/go-cmp/cmp"
    29  )
    30  
    31  // ShouldResembleProto determines if two values are deeply equal, using
    32  // the following rules:
    33  //   - proto equality is defined by proto.Equal,
    34  //   - if a type has an .Equal method, equality is defined by
    35  //     that method,
    36  //   - in the absence of the above, deep equality is defined
    37  //     by recursing over structs, maps and slices in the usual way.
    38  //
    39  // See github.com/google/go-cmp/cmp#Equal for more details.
    40  //
    41  // This method is similar to goconvey's ShouldResemble, except that
    42  // supports proto messages at the top-level or nested inside
    43  // other types.
    44  func ShouldResembleProto(actual any, expected ...any) string {
    45  	if len(expected) != 1 {
    46  		return fmt.Sprintf("ShouldResembleProto expects 1 value, got %d", len(expected))
    47  	}
    48  	exp := expected[0]
    49  
    50  	// Compare all unexported fields.
    51  	exportAll := cmp.Exporter(func(t reflect.Type) bool {
    52  		return true
    53  	})
    54  	diff := cmp.Diff(exp, actual, protocmp.Transform(), exportAll)
    55  	if diff != "" {
    56  		return fmt.Sprintf("Unexpected difference (-want +got):\n%s", diff)
    57  	}
    58  
    59  	return "" // Success
    60  }
    61  
    62  // ShouldResembleProtoText is like ShouldResembleProto, but expected
    63  // is protobuf text.
    64  // actual must be a message. A slice of messages is not supported.
    65  func ShouldResembleProtoText(actual any, expected ...any) string {
    66  	return shouldResembleProtoUnmarshal(
    67  		func(s string, m proto.Message) error {
    68  			return prototext.Unmarshal([]byte(s), m)
    69  		},
    70  		actual,
    71  		expected...)
    72  }
    73  
    74  // ShouldResembleProtoJSON is like ShouldResembleProto, but expected
    75  // is protobuf text.
    76  // actual must be a message. A slice of messages is not supported.
    77  func ShouldResembleProtoJSON(actual any, expected ...any) string {
    78  	return shouldResembleProtoUnmarshal(
    79  		func(s string, m proto.Message) error {
    80  			return protojson.Unmarshal([]byte(s), m)
    81  		},
    82  		actual,
    83  		expected...)
    84  }
    85  
    86  func shouldResembleProtoUnmarshal(unmarshal func(string, proto.Message) error, actual any, expected ...any) string {
    87  	if _, ok := protoMessage(actual); !ok {
    88  		return fmt.Sprintf("ShouldResembleProtoText expects a proto message, got %T", actual)
    89  	}
    90  
    91  	if len(expected) != 1 {
    92  		return fmt.Sprintf("ShouldResembleProtoText expects 1 value, got %d", len(expected))
    93  	}
    94  	expText, ok := expected[0].(string)
    95  	if !ok {
    96  		return fmt.Sprintf("ShouldResembleProtoText expects a string value, got %T", expected[0])
    97  	}
    98  
    99  	expMsg := reflect.New(reflect.TypeOf(actual).Elem()).Interface().(proto.Message)
   100  
   101  	if err := unmarshal(expText, expMsg); err != nil {
   102  		return err.Error()
   103  	}
   104  	return ShouldResembleProto(actual, expMsg)
   105  }
   106  
   107  var textPBMultiline = prototext.MarshalOptions{
   108  	Multiline: true,
   109  }
   110  
   111  // protoMessage returns V2 proto message, converting v1 on the fly.
   112  func protoMessage(a any) (proto.Message, bool) {
   113  	if m, ok := a.(proto.Message); ok {
   114  		return m, true
   115  	}
   116  	if m, ok := a.(protoLegacy.Message); ok {
   117  		return protoLegacy.MessageV2(m), true
   118  	}
   119  	return nil, false
   120  }