go.uber.org/yarpc@v1.72.1/encoding/thrift/thriftrw-plugin-yarpc/roundtrip_test.go (about)

     1  // Copyright (c) 2022 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  package main_test
    21  
    22  import (
    23  	"bytes"
    24  	"context"
    25  	"errors"
    26  	"fmt"
    27  	"reflect"
    28  	"strings"
    29  	"testing"
    30  
    31  	"github.com/stretchr/testify/assert"
    32  	"github.com/stretchr/testify/require"
    33  	"go.uber.org/thriftrw/protocol/binary"
    34  	"go.uber.org/thriftrw/ptr"
    35  	"go.uber.org/yarpc"
    36  	"go.uber.org/yarpc/api/transport"
    37  	"go.uber.org/yarpc/encoding/thrift"
    38  	"go.uber.org/yarpc/encoding/thrift/thriftrw-plugin-yarpc/internal/tests/atomic"
    39  	"go.uber.org/yarpc/encoding/thrift/thriftrw-plugin-yarpc/internal/tests/atomic/readonlystoreclient"
    40  	"go.uber.org/yarpc/encoding/thrift/thriftrw-plugin-yarpc/internal/tests/atomic/readonlystoreserver"
    41  	"go.uber.org/yarpc/encoding/thrift/thriftrw-plugin-yarpc/internal/tests/atomic/storeclient"
    42  	"go.uber.org/yarpc/encoding/thrift/thriftrw-plugin-yarpc/internal/tests/atomic/storeserver"
    43  	"go.uber.org/yarpc/encoding/thrift/thriftrw-plugin-yarpc/internal/tests/common/baseserviceclient"
    44  	"go.uber.org/yarpc/encoding/thrift/thriftrw-plugin-yarpc/internal/tests/common/baseserviceserver"
    45  	"go.uber.org/yarpc/encoding/thrift/thriftrw-plugin-yarpc/internal/tests/common/emptyserviceclient"
    46  	"go.uber.org/yarpc/encoding/thrift/thriftrw-plugin-yarpc/internal/tests/common/emptyserviceserver"
    47  	"go.uber.org/yarpc/encoding/thrift/thriftrw-plugin-yarpc/internal/tests/common/extendemptyclient"
    48  	"go.uber.org/yarpc/encoding/thrift/thriftrw-plugin-yarpc/internal/tests/common/extendemptyserver"
    49  	"go.uber.org/yarpc/encoding/thrift/thriftrw-plugin-yarpc/internal/tests/common/extendonlyclient"
    50  	"go.uber.org/yarpc/encoding/thrift/thriftrw-plugin-yarpc/internal/tests/common/extendonlyserver"
    51  	"go.uber.org/yarpc/internal/testtime"
    52  	"go.uber.org/yarpc/internal/yarpctest"
    53  	"go.uber.org/yarpc/transport/http"
    54  	"go.uber.org/yarpc/yarpcerrors"
    55  )
    56  
    57  func TestRoundTrip(t *testing.T) {
    58  	tests := []struct{ enveloped, multiplexed, nowireServer, nowireClient bool }{
    59  		{true, true, true, true},
    60  		{true, true, true, false},
    61  		{true, true, false, true},
    62  		{true, true, false, false},
    63  		// Skipping for now until flaky test fixed.
    64  		// Uncomment this when fixed.
    65  		// https://github.com/yarpc/yarpc-go/issues/1171
    66  		//{true, false, true, true},
    67  		//{true, false, true, false},
    68  		//{true, false, false, true},
    69  		//{true, false, false, false},
    70  		{false, true, true, true},
    71  		{false, true, true, false},
    72  		{false, true, false, true},
    73  		{false, true, false, false},
    74  		{false, false, true, true},
    75  		{false, false, true, false},
    76  		{false, false, false, true},
    77  		{false, false, false, false},
    78  	}
    79  
    80  	for _, tt := range tests {
    81  		name := fmt.Sprintf("enveloped(%v)/multiplexed(%v)/nowireServer(%v)/nowireClient(%v)", tt.enveloped, tt.multiplexed, tt.nowireServer, tt.nowireClient)
    82  		t.Run(name, func(t *testing.T) { testRoundTrip(t, tt.enveloped, tt.multiplexed, tt.nowireServer, tt.nowireClient) })
    83  	}
    84  }
    85  
    86  func testRoundTrip(t *testing.T, enveloped, multiplexed, nowireServer, nowireClient bool) {
    87  	t.Helper()
    88  
    89  	var serverOpts []thrift.RegisterOption
    90  	if enveloped {
    91  		serverOpts = append(serverOpts, thrift.Enveloped)
    92  	}
    93  	serverOpts = append(serverOpts, thrift.NoWire(nowireServer))
    94  
    95  	var clientOpts []string
    96  	if enveloped {
    97  		clientOpts = append(clientOpts, "enveloped")
    98  	}
    99  	if multiplexed {
   100  		clientOpts = append(clientOpts, "multiplexed")
   101  	}
   102  	if nowireClient {
   103  		clientOpts = append(clientOpts, "nowire")
   104  	}
   105  
   106  	var thriftTag string
   107  	if len(clientOpts) > 0 {
   108  		thriftTag = fmt.Sprintf(` thrift:"%v"`, strings.Join(clientOpts, ","))
   109  	}
   110  
   111  	tests := []struct {
   112  		desc          string
   113  		procedures    []transport.Procedure
   114  		newClientFunc interface{}
   115  
   116  		// if method is non-empty, client.method(ctx, methodArgs...) will be called
   117  		method     string
   118  		methodArgs []interface{}
   119  
   120  		wantAck    bool
   121  		wantResult interface{}
   122  		wantError  error
   123  	}{
   124  		{
   125  			desc:          "empty service",
   126  			procedures:    emptyserviceserver.New(struct{}{}, serverOpts...),
   127  			newClientFunc: emptyserviceclient.New,
   128  		},
   129  		{
   130  			desc:          "extend empty: hello",
   131  			procedures:    extendemptyserver.New(extendEmptyHandler{}, serverOpts...),
   132  			newClientFunc: extendemptyclient.New,
   133  			method:        "Hello",
   134  		},
   135  		{
   136  			desc:          "base: healthy",
   137  			procedures:    baseserviceserver.New(extendEmptyHandler{}, serverOpts...),
   138  			newClientFunc: baseserviceclient.New,
   139  			method:        "Healthy",
   140  			wantResult:    true,
   141  		},
   142  		{
   143  			desc:          "extend only: healthy",
   144  			procedures:    extendonlyserver.New(&storeHandler{healthy: true}, serverOpts...),
   145  			newClientFunc: extendonlyclient.New,
   146  			method:        "Healthy",
   147  			wantResult:    true,
   148  		},
   149  		{
   150  			desc:          "store: healthy",
   151  			procedures:    storeserver.New(&storeHandler{healthy: true}, serverOpts...),
   152  			newClientFunc: storeclient.New,
   153  			method:        "Healthy",
   154  			wantResult:    true,
   155  		},
   156  		{
   157  			desc:          "store: unhealthy",
   158  			procedures:    storeserver.New(&storeHandler{}, serverOpts...),
   159  			newClientFunc: storeclient.New,
   160  			method:        "Healthy",
   161  			wantResult:    false,
   162  		},
   163  		{
   164  			desc:          "store: increment",
   165  			procedures:    storeserver.New(&storeHandler{}, serverOpts...),
   166  			newClientFunc: storeclient.New,
   167  			method:        "Increment",
   168  			methodArgs:    []interface{}{ptr.String("foo"), ptr.Int64(42)},
   169  		},
   170  		{
   171  			desc:          "store: compare and swap",
   172  			procedures:    storeserver.New(&storeHandler{}, serverOpts...),
   173  			newClientFunc: storeclient.New,
   174  			method:        "CompareAndSwap",
   175  			methodArgs: []interface{}{
   176  				&atomic.CompareAndSwap{
   177  					Key:          "foo",
   178  					CurrentValue: 42,
   179  					NewValue:     420,
   180  				},
   181  			},
   182  		},
   183  		{
   184  			desc: "store: compare and swap failure",
   185  			procedures: storeserver.New(&storeHandler{
   186  				failWith: &atomic.IntegerMismatchError{
   187  					ExpectedValue: 42,
   188  					GotValue:      43,
   189  				},
   190  			}, serverOpts...),
   191  			newClientFunc: storeclient.New,
   192  			method:        "CompareAndSwap",
   193  			methodArgs: []interface{}{
   194  				&atomic.CompareAndSwap{
   195  					Key:          "foo",
   196  					CurrentValue: 42,
   197  					NewValue:     420,
   198  				},
   199  			},
   200  			wantError: &atomic.IntegerMismatchError{
   201  				ExpectedValue: 42,
   202  				GotValue:      43,
   203  			},
   204  		},
   205  		{
   206  			desc:          "readonly store: integer with readonly client",
   207  			procedures:    readonlystoreserver.New(&storeHandler{integer: 42}, serverOpts...),
   208  			newClientFunc: readonlystoreclient.New,
   209  			method:        "Integer",
   210  			methodArgs:    []interface{}{ptr.String("foo")},
   211  			wantResult:    int64(42),
   212  		},
   213  		{
   214  			desc: "store: integer failure",
   215  			procedures: storeserver.New(&storeHandler{
   216  				failWith: &atomic.KeyDoesNotExist{Key: ptr.String("foo")},
   217  			}, serverOpts...),
   218  			newClientFunc: storeclient.New,
   219  			method:        "Integer",
   220  			methodArgs:    []interface{}{ptr.String("foo")},
   221  			wantError:     &atomic.KeyDoesNotExist{Key: ptr.String("foo")},
   222  		},
   223  		{
   224  			desc:          "store: forget",
   225  			procedures:    storeserver.New(&storeHandler{}, serverOpts...),
   226  			newClientFunc: storeclient.New,
   227  			method:        "Forget",
   228  			methodArgs:    []interface{}{ptr.String("foo")},
   229  			wantAck:       true,
   230  		},
   231  		{
   232  			desc:          "store: forget error",
   233  			procedures:    storeserver.New(&storeHandler{failWith: errors.New("great sadness")}, serverOpts...),
   234  			newClientFunc: storeclient.New,
   235  			method:        "Forget",
   236  			methodArgs:    []interface{}{ptr.String("foo")},
   237  			wantAck:       true,
   238  		},
   239  	}
   240  
   241  	ctx := context.Background()
   242  	for _, tt := range tests {
   243  		t.Run(tt.desc, func(t *testing.T) {
   244  			httpInbound := http.NewTransport().NewInbound("127.0.0.1:0")
   245  
   246  			server := yarpc.NewDispatcher(yarpc.Config{
   247  				Name:     "roundtrip-server",
   248  				Inbounds: yarpc.Inbounds{httpInbound},
   249  			})
   250  			server.Register(tt.procedures)
   251  			require.NoError(t, server.Start())
   252  			defer server.Stop()
   253  
   254  			outbound := http.NewTransport().NewSingleOutbound(
   255  				fmt.Sprintf("http://%v", yarpctest.ZeroAddrToHostPort(httpInbound.Addr())))
   256  
   257  			dispatcher := yarpc.NewDispatcher(yarpc.Config{
   258  				Name: "roundtrip-client",
   259  				Outbounds: yarpc.Outbounds{
   260  					"roundtrip-server": {
   261  						Unary:  outbound,
   262  						Oneway: outbound,
   263  					},
   264  				},
   265  			})
   266  			require.NoError(t, dispatcher.Start())
   267  			defer dispatcher.Stop()
   268  
   269  			// Verify that newClientFunc was valid
   270  			newClientFuncType := reflect.TypeOf(tt.newClientFunc)
   271  			require.Equal(t, reflect.Func, newClientFuncType.Kind(),
   272  				"invalid test: newClientFunc must be a function")
   273  			require.Equal(t, 1, newClientFuncType.NumOut(),
   274  				"invalid test: newClientFunc must return a single result")
   275  
   276  			clientType := newClientFuncType.Out(0)
   277  			require.Equal(t, reflect.Interface, clientType.Kind(),
   278  				"invalid test: newClientFunc must return an Interface")
   279  
   280  			// The following blob is equivalent to,
   281  			//
   282  			// 	var clientHolder struct {
   283  			// 		Client ${service}client.Interface `service:"roundtrip-server"`
   284  			// 	}
   285  			// 	yarpc.InjectClients(dispatcher, &clientHolder)
   286  			// 	client := clientHolder.Client
   287  			//
   288  			// Optionally with the `thrift:"..."` tag if thriftTag was
   289  			// non-empty.
   290  			structType := reflect.StructOf([]reflect.StructField{
   291  				{
   292  					Name: "Client",
   293  					Type: clientType,
   294  					Tag:  reflect.StructTag(`service:"roundtrip-server"` + thriftTag),
   295  				},
   296  			})
   297  			clientHolder := reflect.New(structType).Elem()
   298  			yarpc.InjectClients(dispatcher, clientHolder.Addr().Interface())
   299  			client := clientHolder.Field(0)
   300  			assert.NotNil(t, client.Interface(), "InjectClients did not provide a client")
   301  
   302  			if tt.method == "" {
   303  				return
   304  			}
   305  
   306  			// Equivalent to,
   307  			//
   308  			// 	... := client.$method(ctx, $methodArgs...)
   309  			method := client.MethodByName(tt.method)
   310  			assert.True(t, method.IsValid(), "Method %q not found", tt.method)
   311  
   312  			ctx, cancel := context.WithTimeout(ctx, 200*testtime.Millisecond)
   313  			defer cancel()
   314  
   315  			args := append([]interface{}{ctx}, tt.methodArgs...)
   316  			returns := method.Call(values(args...))
   317  
   318  			switch len(returns) {
   319  			case 1: // error
   320  				err, _ := returns[0].Interface().(error)
   321  				assert.Equal(t, tt.wantError, err)
   322  			case 2: // (ack/result, err)
   323  				result := returns[0].Interface()
   324  				err, _ := returns[1].Interface().(error)
   325  				if tt.wantError != nil {
   326  					assert.Equal(t, tt.wantError, err)
   327  				} else {
   328  					if !assert.NoError(t, err, "expected success") {
   329  						return
   330  					}
   331  
   332  					if tt.wantAck {
   333  						assert.Implements(t, (*transport.Ack)(nil), result, "expected a non-nil ack")
   334  						assert.NotNil(t, result, "expected a non-nil ack")
   335  					} else {
   336  						assert.Equal(t, tt.wantResult, result)
   337  					}
   338  				}
   339  			default:
   340  				t.Fatalf(
   341  					"impossible: %q returned %d results; only up to 2 are allowed", tt.method, len(returns))
   342  			}
   343  		})
   344  	}
   345  }
   346  
   347  func values(xs ...interface{}) []reflect.Value {
   348  	vs := make([]reflect.Value, len(xs))
   349  	for i, x := range xs {
   350  		vs[i] = reflect.ValueOf(x)
   351  	}
   352  	return vs
   353  }
   354  
   355  type storeHandler struct {
   356  	healthy  bool
   357  	failWith error
   358  	integer  int64
   359  }
   360  
   361  func (h *storeHandler) Healthy(ctx context.Context) (bool, error) {
   362  	return h.healthy, h.failWith
   363  }
   364  
   365  func (h *storeHandler) CompareAndSwap(ctx context.Context, req *atomic.CompareAndSwap) error {
   366  	return h.failWith
   367  }
   368  
   369  func (h *storeHandler) Forget(ctx context.Context, key *string) error {
   370  	return h.failWith
   371  }
   372  
   373  func (h *storeHandler) Increment(ctx context.Context, key *string, value *int64) error {
   374  	return h.failWith
   375  }
   376  
   377  func (h *storeHandler) Integer(ctx context.Context, key *string) (int64, error) {
   378  	return h.integer, h.failWith
   379  }
   380  
   381  type extendEmptyHandler struct{}
   382  
   383  func (extendEmptyHandler) Hello(ctx context.Context) error {
   384  	return nil
   385  }
   386  
   387  func (extendEmptyHandler) Healthy(ctx context.Context) (bool, error) {
   388  	return true, nil
   389  }
   390  
   391  func TestFromWireInvalidArg(t *testing.T) {
   392  	procedures := storeserver.New(nil /*server impl*/)
   393  	require.Len(t, procedures, 5, "unexpected number of procedures")
   394  
   395  	procedure := procedures[2]
   396  	require.Equal(t, "Store::compareAndSwap", procedure.Name)
   397  	require.Equal(t, transport.Unary, procedure.HandlerSpec.Type())
   398  
   399  	// This struct is identical to the `CompareAndSwap` wrapper
   400  	// `Store_CompareAndSwap_Args`, except all fields are optional. This will
   401  	// allow us to produce an invalid payload.
   402  	//
   403  	// The handler will fail to unmarshal this type as it is missing required
   404  	// fields.
   405  	request := &atomic.OptionalCompareAndSwapWrapper{Cas: &atomic.OptionalCompareAndSwap{}}
   406  	val, err := request.ToWire()
   407  	require.NoError(t, err, "unable to covert type to wire.Value")
   408  
   409  	var body bytes.Buffer
   410  	err = binary.Default.Encode(val, &body)
   411  	require.NoError(t, err, "could not marshal message to bytes")
   412  
   413  	err = procedure.HandlerSpec.Unary().Handle(context.Background(), &transport.Request{
   414  		Encoding: thrift.Encoding,
   415  		Body:     &body,
   416  	}, nil /*response writer*/)
   417  
   418  	require.Error(t, err, "expected handler error")
   419  	assert.True(t, yarpcerrors.IsStatus(err), "unkown error")
   420  	assert.Equal(t, yarpcerrors.CodeInvalidArgument, yarpcerrors.FromError(err).Code(), "unexpected code")
   421  	assert.Contains(t, yarpcerrors.FromError(err).Message(), "Store", "expected Thrift service name in message")
   422  	assert.Contains(t, yarpcerrors.FromError(err).Message(), "CompareAndSwap", "expected Thrift procedure name in message")
   423  }