github.com/bazelbuild/remote-apis-sdks@v0.0.0-20240425170053-8a36686a6350/go/pkg/client/retries_test.go (about) 1 package client_test 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "net" 8 "os" 9 "path/filepath" 10 "reflect" 11 "strings" 12 "sync" 13 "testing" 14 "time" 15 16 "github.com/bazelbuild/remote-apis-sdks/go/pkg/client" 17 "github.com/bazelbuild/remote-apis-sdks/go/pkg/digest" 18 "github.com/google/go-cmp/cmp" 19 "github.com/google/go-cmp/cmp/cmpopts" 20 "github.com/klauspost/compress/zstd" 21 "google.golang.org/grpc" 22 "google.golang.org/grpc/codes" 23 "google.golang.org/grpc/status" 24 25 // Redundant imports are required for the google3 mirror. Aliases should not be changed. 26 regrpc "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2" 27 repb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2" 28 bsgrpc "google.golang.org/genproto/googleapis/bytestream" 29 bspb "google.golang.org/genproto/googleapis/bytestream" 30 opgrpc "google.golang.org/genproto/googleapis/longrunning" 31 oppb "google.golang.org/genproto/googleapis/longrunning" 32 anypb "google.golang.org/protobuf/types/known/anypb" 33 emptypb "google.golang.org/protobuf/types/known/emptypb" 34 ) 35 36 var zstdEncoder, _ = zstd.NewWriter(nil, zstd.WithZeroFrames(true)) 37 38 type flakyServer struct { 39 // TODO(jsharpe): This is a hack to work around WaitOperation not existing in some versions of 40 // the long running operations API that we need to support. 41 opgrpc.OperationsServer 42 mu sync.RWMutex // Protects numCalls. 43 numCalls map[string]int // A counter of calls the server encountered thus far, by method. 44 muOffset sync.RWMutex // Protects initialOffsets. 45 initialOffsets map[string]int64 // Stores an initial offset to verify if retries are started over from a correct offset. 46 retriableForever bool // Set to true to make the flaky server return a retriable error forever, rather than eventually a non-retriable error. 47 sleepDelay time.Duration // How long to sleep on each RPC. 48 useBSCompression bool // Whether to use/expect compression on ByteStream calls. 49 } 50 51 func (f *flakyServer) incNumCalls(method string) int { 52 f.mu.Lock() 53 defer f.mu.Unlock() 54 f.numCalls[method]++ 55 return f.numCalls[method] 56 } 57 58 func (f *flakyServer) setInitialOffset(name string, offset int64) { 59 f.muOffset.Lock() 60 defer f.muOffset.Unlock() 61 f.initialOffsets[name] = offset 62 } 63 64 func (f *flakyServer) fetchInitialOffset(name string) int64 { 65 f.muOffset.Lock() 66 defer f.muOffset.Unlock() 67 offset, ok := f.initialOffsets[name] 68 if !ok { 69 return 0 70 } 71 return offset 72 } 73 74 func (f *flakyServer) Write(stream bsgrpc.ByteStream_WriteServer) error { 75 numCalls := f.incNumCalls("Write") 76 if numCalls < 3 { 77 time.Sleep(f.sleepDelay) 78 return status.Error(codes.Canceled, "transient error!") 79 } 80 81 req, err := stream.Recv() 82 if err != nil { 83 return err 84 } 85 // Verify that the client sends the first chunk, because they should retry from scratch. 86 initialOffset := f.fetchInitialOffset(req.ResourceName) 87 if req.WriteOffset != initialOffset || req.FinishWrite { 88 return status.Error(codes.FailedPrecondition, fmt.Sprintf("expected first chunk, got %v", req)) 89 } 90 if numCalls < 5 { 91 return status.Error(codes.Internal, "another transient error!") 92 } 93 return stream.SendAndClose(&bspb.WriteResponse{CommittedSize: 4}) 94 } 95 96 func (f *flakyServer) Read(req *bspb.ReadRequest, stream bsgrpc.ByteStream_ReadServer) error { 97 numCalls := f.incNumCalls("Read") 98 if numCalls < 3 { 99 time.Sleep(f.sleepDelay) 100 return status.Error(codes.Canceled, "transient error!") 101 } 102 if numCalls < 4 { 103 b := []byte("bl") 104 if f.useBSCompression { 105 b = zstdEncoder.EncodeAll(b, nil) 106 } 107 // We send the 4 byte test blob in two chunks. 108 if err := stream.Send(&bspb.ReadResponse{Data: b}); err != nil { 109 return err 110 } 111 return status.Error(codes.Internal, "another transient error!") 112 } 113 // Client now will only ask for the remaining two bytes. 114 if numCalls < 5 { 115 time.Sleep(f.sleepDelay) 116 return status.Error(codes.Aborted, "yet another transient error!") 117 } 118 b := []byte("ob") 119 if f.useBSCompression { 120 b = zstdEncoder.EncodeAll(b, nil) 121 } 122 return stream.Send(&bspb.ReadResponse{Data: b}) 123 } 124 125 func (f *flakyServer) flakeAndFail(method string) error { 126 numCalls := f.incNumCalls(method) 127 if numCalls == 1 { 128 if f.sleepDelay != 0 { 129 time.Sleep(f.sleepDelay) 130 // The error we return here should not matter; the deadline should have passed by now and the 131 // retrier should retry DeadlineExceeded. 132 return status.Error(codes.InvalidArgument, "non retriable error") 133 } 134 return status.Error(codes.DeadlineExceeded, "transient error!") 135 } 136 if f.retriableForever || numCalls < 4 { 137 return status.Error(codes.Canceled, "transient error!") 138 } 139 return status.Error(codes.Unimplemented, "a non retriable error") 140 } 141 142 func (f *flakyServer) QueryWriteStatus(context.Context, *bspb.QueryWriteStatusRequest) (*bspb.QueryWriteStatusResponse, error) { 143 return nil, f.flakeAndFail("QueryWriteStatus") 144 } 145 146 func (f *flakyServer) GetActionResult(ctx context.Context, req *repb.GetActionResultRequest) (*repb.ActionResult, error) { 147 return nil, f.flakeAndFail("GetActionResult") 148 } 149 150 func (f *flakyServer) UpdateActionResult(ctx context.Context, req *repb.UpdateActionResultRequest) (*repb.ActionResult, error) { 151 return nil, f.flakeAndFail("UpdateActionResult") 152 } 153 154 func (f *flakyServer) FindMissingBlobs(ctx context.Context, req *repb.FindMissingBlobsRequest) (*repb.FindMissingBlobsResponse, error) { 155 return nil, f.flakeAndFail("FindMissingBlobs") 156 } 157 158 func (f *flakyServer) BatchUpdateBlobs(ctx context.Context, req *repb.BatchUpdateBlobsRequest) (*repb.BatchUpdateBlobsResponse, error) { 159 return nil, f.flakeAndFail("BatchUpdateBlobs") 160 } 161 162 func (f *flakyServer) BatchReadBlobs(ctx context.Context, req *repb.BatchReadBlobsRequest) (*repb.BatchReadBlobsResponse, error) { 163 return nil, f.flakeAndFail("BatchReadBlobs") 164 } 165 166 func (f *flakyServer) GetTree(req *repb.GetTreeRequest, stream regrpc.ContentAddressableStorage_GetTreeServer) error { 167 numCalls := f.incNumCalls("GetTree") 168 if numCalls < 3 { 169 return status.Error(codes.Canceled, "transient error!") 170 } 171 if numCalls < 4 { 172 // Send one directory then cut the stream. 173 resp := &repb.GetTreeResponse{ 174 Directories: []*repb.Directory{{Files: []*repb.FileNode{{Name: "I'm a file!"}}}}, 175 NextPageToken: "I should be a base64-encoded token, but I'm not", 176 } 177 if err := stream.Send(resp); err != nil { 178 return err 179 } 180 return status.Error(codes.Internal, "another transient error!") 181 } 182 // Client now will only ask for the remaining directories. 183 if numCalls < 5 { 184 return status.Error(codes.Aborted, "yet another transient error!") 185 } 186 resp := &repb.GetTreeResponse{ 187 Directories: []*repb.Directory{{Files: []*repb.FileNode{{Name: "I, too, am a file."}}}}, 188 } 189 return stream.Send(resp) 190 } 191 192 func (f *flakyServer) Execute(req *repb.ExecuteRequest, stream regrpc.Execution_ExecuteServer) error { 193 numCalls := f.incNumCalls("Execute") 194 if numCalls < 2 { 195 return status.Error(codes.Canceled, "transient error!") 196 } 197 stream.Send(&oppb.Operation{Done: false, Name: "dummy"}) 198 // After this error, retries should to go the WaitExecution method. 199 return status.Error(codes.Internal, "another transient error!") 200 } 201 202 func (f *flakyServer) WaitExecution(req *repb.WaitExecutionRequest, stream regrpc.Execution_WaitExecutionServer) error { 203 numCalls := f.incNumCalls("WaitExecution") 204 if numCalls < 2 { 205 return status.Error(codes.Canceled, "transient error!") 206 } 207 if numCalls < 4 { 208 stream.Send(&oppb.Operation{Done: false, Name: "dummy"}) 209 return status.Error(codes.Internal, "another transient error!") 210 } 211 // Execute (above) will fail twice (and be retried twice) before ExecuteAndWait() switches to 212 // WaitExecution. WaitExecution will fail 4 more times more before succeeding, for a total of 6 retries. 213 execResp := &repb.ExecuteResponse{Status: status.New(codes.Aborted, "transient operation failure!").Proto()} 214 any, e := anypb.New(execResp) 215 if e != nil { 216 return e 217 } 218 return stream.Send(&oppb.Operation{Name: "op", Done: true, Result: &oppb.Operation_Response{Response: any}}) 219 } 220 221 func (f *flakyServer) GetOperation(ctx context.Context, req *oppb.GetOperationRequest) (*oppb.Operation, error) { 222 return nil, f.flakeAndFail("GetOperation") 223 } 224 225 func (f *flakyServer) ListOperations(ctx context.Context, req *oppb.ListOperationsRequest) (*oppb.ListOperationsResponse, error) { 226 return nil, f.flakeAndFail("ListOperations") 227 } 228 229 func (f *flakyServer) CancelOperation(ctx context.Context, req *oppb.CancelOperationRequest) (*emptypb.Empty, error) { 230 return nil, f.flakeAndFail("CancelOperation") 231 } 232 233 func (f *flakyServer) DeleteOperation(ctx context.Context, req *oppb.DeleteOperationRequest) (*emptypb.Empty, error) { 234 return nil, f.flakeAndFail("DeleteOperation") 235 } 236 237 type flakyFixture struct { 238 client *client.Client 239 listener net.Listener 240 server *grpc.Server 241 fake *flakyServer 242 ctx context.Context 243 } 244 245 func setup(t *testing.T) *flakyFixture { 246 f := &flakyFixture{ctx: context.Background()} 247 var err error 248 f.listener, err = net.Listen("tcp", ":0") 249 if err != nil { 250 t.Fatalf("Cannot listen: %v", err) 251 } 252 f.server = grpc.NewServer() 253 f.fake = &flakyServer{numCalls: make(map[string]int), initialOffsets: make(map[string]int64)} 254 bsgrpc.RegisterByteStreamServer(f.server, f.fake) 255 regrpc.RegisterActionCacheServer(f.server, f.fake) 256 regrpc.RegisterContentAddressableStorageServer(f.server, f.fake) 257 regrpc.RegisterExecutionServer(f.server, f.fake) 258 opgrpc.RegisterOperationsServer(f.server, f.fake) 259 go f.server.Serve(f.listener) 260 f.client, err = client.NewClient(f.ctx, instance, client.DialParams{ 261 Service: f.listener.Addr().String(), 262 NoSecurity: true, 263 }, client.StartupCapabilities(false), client.ChunkMaxSize(2)) 264 if err != nil { 265 t.Fatalf("Error connecting to server: %v", err) 266 } 267 return f 268 } 269 270 func (f *flakyFixture) shutDown() { 271 f.client.Close() 272 f.listener.Close() 273 f.server.Stop() 274 } 275 276 func compressionBoolToValue(use bool) client.CompressedBytestreamThreshold { 277 if use { 278 return client.CompressedBytestreamThreshold(0) 279 } 280 return client.CompressedBytestreamThreshold(-1) 281 } 282 283 func TestWriteRetries(t *testing.T) { 284 t.Parallel() 285 for _, sleep := range []bool{false, true} { 286 sleep := sleep 287 t.Run(fmt.Sprintf("sleep=%t", sleep), func(t *testing.T) { 288 t.Parallel() 289 f := setup(t) 290 defer f.shutDown() 291 if sleep { 292 f.fake.sleepDelay = time.Second 293 client.RPCTimeouts(map[string]time.Duration{"default": 500 * time.Millisecond}).Apply(f.client) 294 } 295 296 blob := []byte("blob") 297 gotDg, err := f.client.WriteBlob(f.ctx, blob) 298 if err != nil { 299 t.Errorf("client.WriteBlob(ctx, blob) gave error %s, wanted nil", err) 300 } 301 if diff := cmp.Diff(digest.NewFromBlob(blob), gotDg); diff != "" { 302 t.Errorf("client.WriteBlob(ctx, blob) had diff on digest returned (want -> got):\n%s", diff) 303 } 304 }) 305 } 306 } 307 308 func TestRetryWriteBytesAtRemoteOffset(t *testing.T) { 309 tests := []struct { 310 description string 311 initialOffset int64 312 }{ 313 { 314 description: "offset 0", 315 initialOffset: 0, 316 }, 317 { 318 description: "offset 3", 319 initialOffset: 3, 320 }, 321 } 322 323 for _, doNotFinalize := range []bool{true, false} { 324 for _, test := range tests { 325 t.Run(fmt.Sprintf("%s and doNotFinalize %t", test.description, doNotFinalize), func(t *testing.T) { 326 f := setup(t) 327 defer f.shutDown() 328 name := test.description 329 data := []byte("Hello World!") 330 if test.initialOffset > 0 { 331 f.fake.setInitialOffset(name, test.initialOffset) 332 } 333 334 writtenBytes, err := f.client.WriteBytesAtRemoteOffset(f.ctx, name, data[test.initialOffset:], doNotFinalize, test.initialOffset) 335 if err != nil { 336 t.Errorf("client.WriteBytesAtRemoteOffset(ctx, name, %s, %t, %d) gave error %s, want nil", string(data), doNotFinalize, test.initialOffset, err) 337 } 338 if int64(len(data))-test.initialOffset != writtenBytes { 339 t.Errorf("client.WriteBytesAtRemoteOffset(ctx, name, %s, %t, %d) gave %d byte(s), want %d", string(data), doNotFinalize, test.initialOffset, writtenBytes, int64(len(data))-test.initialOffset) 340 } 341 }) 342 } 343 } 344 } 345 346 func TestReadRetries(t *testing.T) { 347 t.Parallel() 348 for _, sleep := range []bool{false, true} { 349 for _, comp := range []bool{false, true} { 350 sleep := sleep 351 comp := comp 352 t.Run(fmt.Sprintf("sleep=%t,comp=%t", sleep, comp), func(t *testing.T) { 353 t.Parallel() 354 f := setup(t) 355 defer f.shutDown() 356 f.fake.useBSCompression = comp 357 compOpt := compressionBoolToValue(comp) 358 compOpt.Apply(f.client) 359 if sleep { 360 f.fake.sleepDelay = time.Second 361 client.RPCTimeouts(map[string]time.Duration{"default": 500 * time.Millisecond}).Apply(f.client) 362 } 363 364 blob := []byte("blob") 365 got, _, err := f.client.ReadBlob(f.ctx, digest.NewFromBlob(blob)) 366 if err != nil { 367 t.Errorf("client.ReadBlob(ctx, digest) gave error %s, want nil", err) 368 } 369 if diff := cmp.Diff(blob, got, cmpopts.EquateEmpty()); diff != "" { 370 t.Errorf("client.ReadBlob(ctx, digest) gave diff (-want, +got):\n%s", diff) 371 } 372 }) 373 } 374 } 375 } 376 377 func TestReadToFileRetries(t *testing.T) { 378 t.Parallel() 379 for _, sleep := range []bool{false, true} { 380 for _, comp := range []bool{false, true} { 381 sleep := sleep 382 comp := comp 383 t.Run(fmt.Sprintf("sleep=%t", sleep), func(t *testing.T) { 384 t.Parallel() 385 f := setup(t) 386 defer f.shutDown() 387 f.fake.useBSCompression = comp 388 compOpt := compressionBoolToValue(comp) 389 compOpt.Apply(f.client) 390 391 if sleep { 392 f.fake.sleepDelay = time.Second 393 client.RPCTimeouts(map[string]time.Duration{"default": 500 * time.Millisecond}).Apply(f.client) 394 } 395 396 blob := []byte("blob") 397 path := filepath.Join(t.TempDir(), strings.Replace(t.Name(), "/", "_", -1)) 398 stats, err := f.client.ReadBlobToFile(f.ctx, digest.NewFromBlob(blob), path) 399 if err != nil { 400 t.Errorf("client.ReadBlobToFile(ctx, digest) gave error %s, want nil", err) 401 } 402 if stats.LogicalMoved != int64(len(blob)) { 403 t.Errorf("client.ReadBlobToFile(ctx, digest) returned %d read bytes, wanted %d", stats.LogicalMoved, len(blob)) 404 } 405 if comp && stats.LogicalMoved == stats.RealMoved { 406 t.Errorf("client.ReadBlobToFile(ctx, digest) = %v - compression on but same real and logical bytes", stats) 407 } 408 409 contents, err := os.ReadFile(path) 410 if err != nil { 411 t.Errorf("error reading from %s: %v", path, err) 412 } 413 if !bytes.Equal(contents, blob) { 414 t.Errorf("expected %s to contain %v, got %v", path, blob, contents) 415 } 416 }) 417 } 418 } 419 } 420 421 // Verify for one arbitrary method that when retries are exhausted, we get the retriable error code 422 // back. 423 func TestBatchWriteBlobsRpcRetriesExhausted(t *testing.T) { 424 t.Parallel() 425 f := setup(t) 426 f.fake.retriableForever = true 427 defer f.shutDown() 428 429 blobs := map[digest.Digest][]byte{ 430 digest.TestNew("a", 1): []byte{1}, 431 digest.TestNew("b", 1): []byte{2}, 432 } 433 err := f.client.BatchWriteBlobs(f.ctx, blobs) 434 if err == nil { 435 t.Error("BatchWriteBlobs(ctx, {}) = nil; expected Canceled error got nil") 436 } else if s, ok := status.FromError(err); ok && s.Code() != codes.Canceled { 437 t.Errorf("BatchWriteBlobs(ctx, {}) = %v; expected Canceled error, got %v", err, s.Code()) 438 } else if !ok { 439 t.Errorf("BatchWriteBlobs(ctx, {}) = %v; expected Canceled error (status.FromError failed)", err) 440 } 441 } 442 443 func TestGetTreeRetries(t *testing.T) { 444 t.Parallel() 445 f := setup(t) 446 defer f.shutDown() 447 448 blob := []byte("blob") 449 got, err := f.client.GetDirectoryTree(f.ctx, digest.NewFromBlob(blob).ToProto()) 450 if err != nil { 451 t.Errorf("client.GetDirectoryTree(ctx, digest) gave err %s, want nil", err) 452 } 453 if len(got) != 2 { 454 t.Errorf("client.GetDirectoryTree(ctx, digest) gave %d directories, want 2", len(got)) 455 } 456 } 457 458 func TestExecuteAndWaitRetries(t *testing.T) { 459 t.Parallel() 460 f := setup(t) 461 defer f.shutDown() 462 463 op, err := f.client.ExecuteAndWait(f.ctx, &repb.ExecuteRequest{}) 464 if err != nil { 465 t.Fatalf("client.WaitExecution(ctx, {}) = %v", err) 466 } 467 st := client.OperationStatus(op) 468 if st == nil { 469 t.Errorf("client.WaitExecution(ctx, {}) returned no status, expected Aborted") 470 } 471 if st != nil && st.Code() != codes.Aborted { 472 t.Errorf("client.WaitExecution(ctx, {}) returned unexpected status code %s", st.Code()) 473 } 474 // 2 separate transient Execute errors. 475 if f.fake.numCalls["Execute"] != 2 { 476 t.Errorf("Expected 2 Execute calls, got %v", f.fake.numCalls["Execute"]) 477 } 478 // 3 separate transient WaitExecution errors + the final successful call. 479 if f.fake.numCalls["WaitExecution"] != 4 { 480 t.Errorf("Expected 4 WaitExecution calls, got %v", f.fake.numCalls["WaitExecution"]) 481 } 482 } 483 484 func TestNonStreamingRpcRetries(t *testing.T) { 485 t.Parallel() 486 testcases := []struct { 487 name string 488 rpc func(*flakyFixture) (interface{}, error) 489 }{ 490 { 491 name: "QueryWriteStatus", 492 rpc: func(f *flakyFixture) (interface{}, error) { 493 return f.client.QueryWriteStatus(f.ctx, &bspb.QueryWriteStatusRequest{}) 494 }, 495 }, 496 { 497 name: "GetActionResult", 498 rpc: func(f *flakyFixture) (interface{}, error) { 499 return f.client.GetActionResult(f.ctx, &repb.GetActionResultRequest{}) 500 }, 501 }, 502 { 503 name: "UpdateActionResult", 504 rpc: func(f *flakyFixture) (interface{}, error) { 505 return f.client.UpdateActionResult(f.ctx, &repb.UpdateActionResultRequest{}) 506 }, 507 }, 508 { 509 name: "FindMissingBlobs", 510 rpc: func(f *flakyFixture) (interface{}, error) { 511 return f.client.FindMissingBlobs(f.ctx, &repb.FindMissingBlobsRequest{}) 512 }, 513 }, 514 { 515 name: "BatchUpdateBlobs", 516 rpc: func(f *flakyFixture) (interface{}, error) { 517 return f.client.BatchUpdateBlobs(f.ctx, &repb.BatchUpdateBlobsRequest{}) 518 }, 519 }, 520 { 521 name: "GetOperation", 522 rpc: func(f *flakyFixture) (interface{}, error) { 523 return f.client.GetOperation(f.ctx, &oppb.GetOperationRequest{}) 524 }, 525 }, 526 { 527 name: "ListOperations", 528 rpc: func(f *flakyFixture) (interface{}, error) { 529 return f.client.ListOperations(f.ctx, &oppb.ListOperationsRequest{}) 530 }, 531 }, 532 { 533 name: "CancelOperation", 534 rpc: func(f *flakyFixture) (interface{}, error) { 535 return f.client.CancelOperation(f.ctx, &oppb.CancelOperationRequest{}) 536 }, 537 }, 538 { 539 name: "DeleteOperation", 540 rpc: func(f *flakyFixture) (interface{}, error) { 541 return f.client.DeleteOperation(f.ctx, &oppb.DeleteOperationRequest{}) 542 }, 543 }, 544 } 545 for _, tc := range testcases { 546 tc := tc 547 t.Run(tc.name, func(t *testing.T) { 548 t.Parallel() 549 f := setup(t) 550 defer f.shutDown() 551 552 got, err := tc.rpc(f) 553 if !reflect.ValueOf(got).IsNil() { 554 t.Errorf("%s(ctx, {}) gave result %s, want nil", tc.name, got) 555 } 556 if err == nil { 557 t.Errorf("%s(ctx, {}) = nil; expected Unimplemented error got nil", tc.name) 558 } else if s, ok := status.FromError(err); ok && s.Code() != codes.Unimplemented { 559 t.Errorf("%s(ctx, {}) = %v; expected Unimplemented error, got %v", tc.name, err, s.Code()) 560 } else if !ok { 561 t.Errorf("%s(ctx, {}) = %v; expected Unimplemented error (status.FromError failed)", tc.name, err) 562 } 563 }) 564 } 565 } 566 567 func TestNonStreamingRpcRetriesSleep(t *testing.T) { 568 t.Parallel() 569 f := setup(t) 570 defer f.shutDown() 571 f.fake.sleepDelay = time.Second 572 client.RPCTimeouts(map[string]time.Duration{"QueryWriteStatus": 500 * time.Millisecond}).Apply(f.client) 573 574 got, err := f.client.QueryWriteStatus(f.ctx, &bspb.QueryWriteStatusRequest{}) 575 if got != nil { 576 t.Errorf("client.QueryWriteStatus(ctx, digest) gave result %s, want nil", got) 577 } 578 if err == nil { 579 t.Error("QueryWriteStatus(ctx, {}) = nil; expected Unimplemented error got nil") 580 } else if s, ok := status.FromError(err); ok && s.Code() != codes.Unimplemented { 581 t.Errorf("QueryWriteStatus(ctx, {}) = %v; expected Unimplemented error, got %v", err, s.Code()) 582 } else if !ok { 583 t.Errorf("QueryWriteStatus(ctx, {}) = %v; expected Unimplemented error (status.FromError failed)", err) 584 } 585 }