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 }