gitee.com/ks-custle/core-gm@v0.0.0-20230922171213-b83bdd97b62c/grpc/xds/csds/csds_test.go (about) 1 /* 2 * 3 * Copyright 2021 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19 package csds 20 21 import ( 22 "context" 23 "fmt" 24 "sort" 25 "strings" 26 "testing" 27 "time" 28 29 grpc "gitee.com/ks-custle/core-gm/grpc" 30 "gitee.com/ks-custle/core-gm/grpc/internal/testutils" 31 "gitee.com/ks-custle/core-gm/grpc/internal/xds" 32 _ "gitee.com/ks-custle/core-gm/grpc/xds/internal/httpfilter/router" 33 "gitee.com/ks-custle/core-gm/grpc/xds/internal/testutils/e2e" 34 "gitee.com/ks-custle/core-gm/grpc/xds/internal/xdsclient" 35 "gitee.com/ks-custle/core-gm/grpc/xds/internal/xdsclient/xdsresource" 36 "github.com/golang/protobuf/proto" 37 "github.com/google/go-cmp/cmp" 38 "github.com/google/uuid" 39 "google.golang.org/protobuf/testing/protocmp" 40 "google.golang.org/protobuf/types/known/anypb" 41 42 v3adminpb "gitee.com/ks-custle/core-gm/go-control-plane/envoy/admin/v3" 43 v2corepb "gitee.com/ks-custle/core-gm/go-control-plane/envoy/api/v2/core" 44 v3clusterpb "gitee.com/ks-custle/core-gm/go-control-plane/envoy/config/cluster/v3" 45 v3corepb "gitee.com/ks-custle/core-gm/go-control-plane/envoy/config/core/v3" 46 v3endpointpb "gitee.com/ks-custle/core-gm/go-control-plane/envoy/config/endpoint/v3" 47 v3listenerpb "gitee.com/ks-custle/core-gm/go-control-plane/envoy/config/listener/v3" 48 v3routepb "gitee.com/ks-custle/core-gm/go-control-plane/envoy/config/route/v3" 49 v3statuspb "gitee.com/ks-custle/core-gm/go-control-plane/envoy/service/status/v3" 50 v3statuspbgrpc "gitee.com/ks-custle/core-gm/go-control-plane/envoy/service/status/v3" 51 ) 52 53 const ( 54 defaultTestTimeout = 10 * time.Second 55 ) 56 57 var cmpOpts = cmp.Options{ 58 cmp.Transformer("sort", func(in []*v3statuspb.ClientConfig_GenericXdsConfig) []*v3statuspb.ClientConfig_GenericXdsConfig { 59 out := append([]*v3statuspb.ClientConfig_GenericXdsConfig(nil), in...) 60 sort.Slice(out, func(i, j int) bool { 61 a, b := out[i], out[j] 62 if a == nil { 63 return true 64 } 65 if b == nil { 66 return false 67 } 68 if strings.Compare(a.TypeUrl, b.TypeUrl) == 0 { 69 return strings.Compare(a.Name, b.Name) < 0 70 } 71 return strings.Compare(a.TypeUrl, b.TypeUrl) < 0 72 }) 73 return out 74 }), 75 protocmp.Transform(), 76 } 77 78 // filterFields clears unimportant fields in the proto messages. 79 // 80 // protocmp.IgnoreFields() doesn't work on nil messages (it panics). 81 func filterFields(ms []*v3statuspb.ClientConfig_GenericXdsConfig) []*v3statuspb.ClientConfig_GenericXdsConfig { 82 out := append([]*v3statuspb.ClientConfig_GenericXdsConfig{}, ms...) 83 for _, m := range out { 84 if m == nil { 85 continue 86 } 87 m.LastUpdated = nil 88 if m.ErrorState != nil { 89 m.ErrorState.Details = "blahblah" 90 m.ErrorState.LastUpdateAttempt = nil 91 } 92 } 93 return out 94 } 95 96 var ( 97 ldsTargets = []string{"lds.target.good:0000", "lds.target.good:1111"} 98 listeners = make([]*v3listenerpb.Listener, len(ldsTargets)) 99 listenerAnys = make([]*anypb.Any, len(ldsTargets)) 100 101 rdsTargets = []string{"route-config-0", "route-config-1"} 102 routes = make([]*v3routepb.RouteConfiguration, len(rdsTargets)) 103 routeAnys = make([]*anypb.Any, len(rdsTargets)) 104 105 cdsTargets = []string{"cluster-0", "cluster-1"} 106 clusters = make([]*v3clusterpb.Cluster, len(cdsTargets)) 107 clusterAnys = make([]*anypb.Any, len(cdsTargets)) 108 109 edsTargets = []string{"endpoints-0", "endpoints-1"} 110 endpoints = make([]*v3endpointpb.ClusterLoadAssignment, len(edsTargets)) 111 endpointAnys = make([]*anypb.Any, len(edsTargets)) 112 ips = []string{"0.0.0.0", "1.1.1.1"} 113 ports = []uint32{123, 456} 114 ) 115 116 func init() { 117 for i := range ldsTargets { 118 listeners[i] = e2e.DefaultClientListener(ldsTargets[i], rdsTargets[i]) 119 listenerAnys[i] = testutils.MarshalAny(listeners[i]) 120 } 121 for i := range rdsTargets { 122 routes[i] = e2e.DefaultRouteConfig(rdsTargets[i], ldsTargets[i], cdsTargets[i]) 123 routeAnys[i] = testutils.MarshalAny(routes[i]) 124 } 125 for i := range cdsTargets { 126 clusters[i] = e2e.DefaultCluster(cdsTargets[i], edsTargets[i], e2e.SecurityLevelNone) 127 clusterAnys[i] = testutils.MarshalAny(clusters[i]) 128 } 129 for i := range edsTargets { 130 endpoints[i] = e2e.DefaultEndpoint(edsTargets[i], ips[i], ports[i:i+1]) 131 endpointAnys[i] = testutils.MarshalAny(endpoints[i]) 132 } 133 } 134 135 func TestCSDS(t *testing.T) { 136 const retryCount = 10 137 138 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 139 defer cancel() 140 xdsC, mgmServer, nodeID, stream, cleanup := commonSetup(ctx, t) 141 defer cleanup() 142 143 for _, target := range ldsTargets { 144 xdsC.WatchListener(target, func(xdsresource.ListenerUpdate, error) {}) 145 } 146 for _, target := range rdsTargets { 147 xdsC.WatchRouteConfig(target, func(xdsresource.RouteConfigUpdate, error) {}) 148 } 149 for _, target := range cdsTargets { 150 xdsC.WatchCluster(target, func(xdsresource.ClusterUpdate, error) {}) 151 } 152 for _, target := range edsTargets { 153 xdsC.WatchEndpoints(target, func(xdsresource.EndpointsUpdate, error) {}) 154 } 155 156 for i := 0; i < retryCount; i++ { 157 err := checkForRequested(stream) 158 if err == nil { 159 break 160 } 161 if i == retryCount-1 { 162 t.Fatalf("%v", err) 163 } 164 time.Sleep(time.Millisecond * 100) 165 } 166 167 if err := mgmServer.Update(ctx, e2e.UpdateOptions{ 168 NodeID: nodeID, 169 Listeners: listeners, 170 Routes: routes, 171 Clusters: clusters, 172 Endpoints: endpoints, 173 }); err != nil { 174 t.Fatal(err) 175 } 176 for i := 0; i < retryCount; i++ { 177 err := checkForACKed(stream) 178 if err == nil { 179 break 180 } 181 if i == retryCount-1 { 182 t.Fatalf("%v", err) 183 } 184 time.Sleep(time.Millisecond * 100) 185 } 186 187 const nackResourceIdx = 0 188 var ( 189 nackListeners = append([]*v3listenerpb.Listener{}, listeners...) 190 nackRoutes = append([]*v3routepb.RouteConfiguration{}, routes...) 191 nackClusters = append([]*v3clusterpb.Cluster{}, clusters...) 192 nackEndpoints = append([]*v3endpointpb.ClusterLoadAssignment{}, endpoints...) 193 ) 194 nackListeners[0] = &v3listenerpb.Listener{Name: ldsTargets[nackResourceIdx], ApiListener: &v3listenerpb.ApiListener{}} // 0 will be nacked. 1 will stay the same. 195 nackRoutes[0] = &v3routepb.RouteConfiguration{ 196 Name: rdsTargets[nackResourceIdx], VirtualHosts: []*v3routepb.VirtualHost{{Routes: []*v3routepb.Route{{}}}}, 197 } 198 nackClusters[0] = &v3clusterpb.Cluster{ 199 Name: cdsTargets[nackResourceIdx], ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_STATIC}, 200 } 201 nackEndpoints[0] = &v3endpointpb.ClusterLoadAssignment{ 202 ClusterName: edsTargets[nackResourceIdx], Endpoints: []*v3endpointpb.LocalityLbEndpoints{{}}, 203 } 204 if err := mgmServer.Update(ctx, e2e.UpdateOptions{ 205 NodeID: nodeID, 206 Listeners: nackListeners, 207 Routes: nackRoutes, 208 Clusters: nackClusters, 209 Endpoints: nackEndpoints, 210 SkipValidation: true, 211 }); err != nil { 212 t.Fatal(err) 213 } 214 for i := 0; i < retryCount; i++ { 215 err := checkForNACKed(nackResourceIdx, stream) 216 if err == nil { 217 break 218 } 219 if i == retryCount-1 { 220 t.Fatalf("%v", err) 221 } 222 time.Sleep(time.Millisecond * 100) 223 } 224 } 225 226 func commonSetup(ctx context.Context, t *testing.T) (xdsclient.XDSClient, *e2e.ManagementServer, string, v3statuspbgrpc.ClientStatusDiscoveryService_StreamClientStatusClient, func()) { 227 t.Helper() 228 229 // Spin up a xDS management server on a local port. 230 nodeID := uuid.New().String() 231 fs, err := e2e.StartManagementServer() 232 if err != nil { 233 t.Fatal(err) 234 } 235 236 // Create a bootstrap file in a temporary directory. 237 bootstrapCleanup, err := xds.SetupBootstrapFile(xds.BootstrapOptions{ 238 Version: xds.TransportV3, 239 NodeID: nodeID, 240 ServerURI: fs.Address, 241 }) 242 if err != nil { 243 t.Fatal(err) 244 } 245 // Create xds_client. 246 xdsC, err := xdsclient.New() 247 if err != nil { 248 t.Fatalf("failed to create xds client: %v", err) 249 } 250 oldNewXDSClient := newXDSClient 251 newXDSClient = func() xdsclient.XDSClient { return xdsC } 252 253 // Initialize an gRPC server and register CSDS on it. 254 server := grpc.NewServer() 255 csdss, err := NewClientStatusDiscoveryServer() 256 if err != nil { 257 t.Fatal(err) 258 } 259 v3statuspbgrpc.RegisterClientStatusDiscoveryServiceServer(server, csdss) 260 // Create a local listener and pass it to Serve(). 261 lis, err := testutils.LocalTCPListener() 262 if err != nil { 263 t.Fatalf("testutils.LocalTCPListener() failed: %v", err) 264 } 265 go func() { 266 if err := server.Serve(lis); err != nil { 267 t.Errorf("Serve() failed: %v", err) 268 } 269 }() 270 271 // Create CSDS client. 272 conn, err := grpc.Dial(lis.Addr().String(), grpc.WithInsecure()) 273 if err != nil { 274 t.Fatalf("cannot connect to server: %v", err) 275 } 276 c := v3statuspbgrpc.NewClientStatusDiscoveryServiceClient(conn) 277 stream, err := c.StreamClientStatus(ctx, grpc.WaitForReady(true)) 278 if err != nil { 279 t.Fatalf("cannot get ServerReflectionInfo: %v", err) 280 } 281 282 return xdsC, fs, nodeID, stream, func() { 283 fs.Stop() 284 conn.Close() 285 server.Stop() 286 csdss.Close() 287 newXDSClient = oldNewXDSClient 288 xdsC.Close() 289 bootstrapCleanup() 290 } 291 } 292 293 func checkForRequested(stream v3statuspbgrpc.ClientStatusDiscoveryService_StreamClientStatusClient) error { 294 if err := stream.Send(&v3statuspb.ClientStatusRequest{Node: nil}); err != nil { 295 return fmt.Errorf("failed to send request: %v", err) 296 } 297 r, err := stream.Recv() 298 if err != nil { 299 // io.EOF is not ok. 300 return fmt.Errorf("failed to recv response: %v", err) 301 } 302 303 if n := len(r.Config); n != 1 { 304 return fmt.Errorf("got %d configs, want 1: %v", n, proto.MarshalTextString(r)) 305 } 306 307 var want []*v3statuspb.ClientConfig_GenericXdsConfig 308 // Status is Requested, but version and xds config are all unset. 309 for i := range ldsTargets { 310 want = append(want, &v3statuspb.ClientConfig_GenericXdsConfig{ 311 TypeUrl: listenerTypeURL, Name: ldsTargets[i], ClientStatus: v3adminpb.ClientResourceStatus_REQUESTED, 312 }) 313 } 314 for i := range rdsTargets { 315 want = append(want, &v3statuspb.ClientConfig_GenericXdsConfig{ 316 TypeUrl: routeConfigTypeURL, Name: rdsTargets[i], ClientStatus: v3adminpb.ClientResourceStatus_REQUESTED, 317 }) 318 } 319 for i := range cdsTargets { 320 want = append(want, &v3statuspb.ClientConfig_GenericXdsConfig{ 321 TypeUrl: clusterTypeURL, Name: cdsTargets[i], ClientStatus: v3adminpb.ClientResourceStatus_REQUESTED, 322 }) 323 } 324 for i := range edsTargets { 325 want = append(want, &v3statuspb.ClientConfig_GenericXdsConfig{ 326 TypeUrl: endpointsTypeURL, Name: edsTargets[i], ClientStatus: v3adminpb.ClientResourceStatus_REQUESTED, 327 }) 328 } 329 if diff := cmp.Diff(filterFields(r.Config[0].GenericXdsConfigs), want, cmpOpts); diff != "" { 330 return fmt.Errorf(diff) 331 } 332 return nil 333 } 334 335 func checkForACKed(stream v3statuspbgrpc.ClientStatusDiscoveryService_StreamClientStatusClient) error { 336 const wantVersion = "1" 337 338 if err := stream.Send(&v3statuspb.ClientStatusRequest{Node: nil}); err != nil { 339 return fmt.Errorf("failed to send: %v", err) 340 } 341 r, err := stream.Recv() 342 if err != nil { 343 // io.EOF is not ok. 344 return fmt.Errorf("failed to recv response: %v", err) 345 } 346 347 if n := len(r.Config); n != 1 { 348 return fmt.Errorf("got %d configs, want 1: %v", n, proto.MarshalTextString(r)) 349 } 350 351 var want []*v3statuspb.ClientConfig_GenericXdsConfig 352 // Status is Acked, config is filled with the prebuilt Anys. 353 for i := range ldsTargets { 354 want = append(want, &v3statuspb.ClientConfig_GenericXdsConfig{ 355 TypeUrl: listenerTypeURL, 356 Name: ldsTargets[i], 357 VersionInfo: wantVersion, 358 XdsConfig: listenerAnys[i], 359 ClientStatus: v3adminpb.ClientResourceStatus_ACKED, 360 }) 361 } 362 for i := range rdsTargets { 363 want = append(want, &v3statuspb.ClientConfig_GenericXdsConfig{ 364 TypeUrl: routeConfigTypeURL, 365 Name: rdsTargets[i], 366 VersionInfo: wantVersion, 367 XdsConfig: routeAnys[i], 368 ClientStatus: v3adminpb.ClientResourceStatus_ACKED, 369 }) 370 } 371 for i := range cdsTargets { 372 want = append(want, &v3statuspb.ClientConfig_GenericXdsConfig{ 373 TypeUrl: clusterTypeURL, 374 Name: cdsTargets[i], 375 VersionInfo: wantVersion, 376 XdsConfig: clusterAnys[i], 377 ClientStatus: v3adminpb.ClientResourceStatus_ACKED, 378 }) 379 } 380 for i := range edsTargets { 381 want = append(want, &v3statuspb.ClientConfig_GenericXdsConfig{ 382 TypeUrl: endpointsTypeURL, 383 Name: edsTargets[i], 384 VersionInfo: wantVersion, 385 XdsConfig: endpointAnys[i], 386 ClientStatus: v3adminpb.ClientResourceStatus_ACKED, 387 }) 388 } 389 if diff := cmp.Diff(filterFields(r.Config[0].GenericXdsConfigs), want, cmpOpts); diff != "" { 390 return fmt.Errorf(diff) 391 } 392 return nil 393 } 394 395 func checkForNACKed(nackResourceIdx int, stream v3statuspbgrpc.ClientStatusDiscoveryService_StreamClientStatusClient) error { 396 const ( 397 ackVersion = "1" 398 nackVersion = "2" 399 ) 400 if err := stream.Send(&v3statuspb.ClientStatusRequest{Node: nil}); err != nil { 401 return fmt.Errorf("failed to send: %v", err) 402 } 403 r, err := stream.Recv() 404 if err != nil { 405 // io.EOF is not ok. 406 return fmt.Errorf("failed to recv response: %v", err) 407 } 408 409 if n := len(r.Config); n != 1 { 410 return fmt.Errorf("got %d configs, want 1: %v", n, proto.MarshalTextString(r)) 411 } 412 413 var want []*v3statuspb.ClientConfig_GenericXdsConfig 414 // Resources with the nackIdx are NACKed. 415 for i := range ldsTargets { 416 config := &v3statuspb.ClientConfig_GenericXdsConfig{ 417 TypeUrl: listenerTypeURL, 418 Name: ldsTargets[i], 419 VersionInfo: nackVersion, 420 XdsConfig: listenerAnys[i], 421 ClientStatus: v3adminpb.ClientResourceStatus_ACKED, 422 } 423 if i == nackResourceIdx { 424 config.VersionInfo = ackVersion 425 config.ClientStatus = v3adminpb.ClientResourceStatus_NACKED 426 config.ErrorState = &v3adminpb.UpdateFailureState{ 427 Details: "blahblah", 428 VersionInfo: nackVersion, 429 } 430 } 431 want = append(want, config) 432 } 433 for i := range rdsTargets { 434 config := &v3statuspb.ClientConfig_GenericXdsConfig{ 435 TypeUrl: routeConfigTypeURL, 436 Name: rdsTargets[i], 437 VersionInfo: nackVersion, 438 XdsConfig: routeAnys[i], 439 ClientStatus: v3adminpb.ClientResourceStatus_ACKED, 440 } 441 if i == nackResourceIdx { 442 config.VersionInfo = ackVersion 443 config.ClientStatus = v3adminpb.ClientResourceStatus_NACKED 444 config.ErrorState = &v3adminpb.UpdateFailureState{ 445 Details: "blahblah", 446 VersionInfo: nackVersion, 447 } 448 } 449 want = append(want, config) 450 } 451 for i := range cdsTargets { 452 config := &v3statuspb.ClientConfig_GenericXdsConfig{ 453 TypeUrl: clusterTypeURL, 454 Name: cdsTargets[i], 455 VersionInfo: nackVersion, 456 XdsConfig: clusterAnys[i], 457 ClientStatus: v3adminpb.ClientResourceStatus_ACKED, 458 } 459 if i == nackResourceIdx { 460 config.VersionInfo = ackVersion 461 config.ClientStatus = v3adminpb.ClientResourceStatus_NACKED 462 config.ErrorState = &v3adminpb.UpdateFailureState{ 463 Details: "blahblah", 464 VersionInfo: nackVersion, 465 } 466 } 467 want = append(want, config) 468 } 469 for i := range edsTargets { 470 config := &v3statuspb.ClientConfig_GenericXdsConfig{ 471 TypeUrl: endpointsTypeURL, 472 Name: edsTargets[i], 473 VersionInfo: nackVersion, 474 XdsConfig: endpointAnys[i], 475 ClientStatus: v3adminpb.ClientResourceStatus_ACKED, 476 } 477 if i == nackResourceIdx { 478 config.VersionInfo = ackVersion 479 config.ClientStatus = v3adminpb.ClientResourceStatus_NACKED 480 config.ErrorState = &v3adminpb.UpdateFailureState{ 481 Details: "blahblah", 482 VersionInfo: nackVersion, 483 } 484 } 485 want = append(want, config) 486 } 487 if diff := cmp.Diff(filterFields(r.Config[0].GenericXdsConfigs), want, cmpOpts); diff != "" { 488 return fmt.Errorf(diff) 489 } 490 return nil 491 } 492 493 func TestCSDSNoXDSClient(t *testing.T) { 494 oldNewXDSClient := newXDSClient 495 newXDSClient = func() xdsclient.XDSClient { return nil } 496 defer func() { newXDSClient = oldNewXDSClient }() 497 498 // Initialize an gRPC server and register CSDS on it. 499 server := grpc.NewServer() 500 csdss, err := NewClientStatusDiscoveryServer() 501 if err != nil { 502 t.Fatal(err) 503 } 504 defer csdss.Close() 505 v3statuspbgrpc.RegisterClientStatusDiscoveryServiceServer(server, csdss) 506 // Create a local listener and pass it to Serve(). 507 lis, err := testutils.LocalTCPListener() 508 if err != nil { 509 t.Fatalf("testutils.LocalTCPListener() failed: %v", err) 510 } 511 go func() { 512 if err := server.Serve(lis); err != nil { 513 t.Errorf("Serve() failed: %v", err) 514 } 515 }() 516 defer server.Stop() 517 518 // Create CSDS client. 519 conn, err := grpc.Dial(lis.Addr().String(), grpc.WithInsecure()) 520 if err != nil { 521 t.Fatalf("cannot connect to server: %v", err) 522 } 523 defer conn.Close() 524 c := v3statuspbgrpc.NewClientStatusDiscoveryServiceClient(conn) 525 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 526 defer cancel() 527 stream, err := c.StreamClientStatus(ctx, grpc.WaitForReady(true)) 528 if err != nil { 529 t.Fatalf("cannot get ServerReflectionInfo: %v", err) 530 } 531 532 if err := stream.Send(&v3statuspb.ClientStatusRequest{Node: nil}); err != nil { 533 t.Fatalf("failed to send: %v", err) 534 } 535 r, err := stream.Recv() 536 if err != nil { 537 // io.EOF is not ok. 538 t.Fatalf("failed to recv response: %v", err) 539 } 540 if n := len(r.Config); n != 0 { 541 t.Fatalf("got %d configs, want 0: %v", n, proto.MarshalTextString(r)) 542 } 543 } 544 545 func Test_nodeProtoToV3(t *testing.T) { 546 const ( 547 testID = "test-id" 548 testCluster = "test-cluster" 549 testZone = "test-zone" 550 ) 551 tests := []struct { 552 name string 553 n proto.Message 554 want *v3corepb.Node 555 }{ 556 { 557 name: "v3", 558 n: &v3corepb.Node{ 559 Id: testID, 560 Cluster: testCluster, 561 Locality: &v3corepb.Locality{Zone: testZone}, 562 }, 563 want: &v3corepb.Node{ 564 Id: testID, 565 Cluster: testCluster, 566 Locality: &v3corepb.Locality{Zone: testZone}, 567 }, 568 }, 569 { 570 name: "v2", 571 n: &v2corepb.Node{ 572 Id: testID, 573 Cluster: testCluster, 574 Locality: &v2corepb.Locality{Zone: testZone}, 575 }, 576 want: &v3corepb.Node{ 577 Id: testID, 578 Cluster: testCluster, 579 Locality: &v3corepb.Locality{Zone: testZone}, 580 }, 581 }, 582 { 583 name: "not node", 584 n: &v2corepb.Locality{Zone: testZone}, 585 want: nil, // Input is not a node, should return nil. 586 }, 587 } 588 for _, tt := range tests { 589 t.Run(tt.name, func(t *testing.T) { 590 got := nodeProtoToV3(tt.n) 591 if diff := cmp.Diff(got, tt.want, protocmp.Transform()); diff != "" { 592 t.Errorf("nodeProtoToV3() got unexpected result, diff (-got, +want): %v", diff) 593 } 594 }) 595 } 596 }