github.com/opentofu/opentofu@v1.7.1/internal/states/remote/state_test.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package remote 7 8 import ( 9 "log" 10 "sync" 11 "testing" 12 13 "github.com/google/go-cmp/cmp" 14 "github.com/google/go-cmp/cmp/cmpopts" 15 "github.com/zclconf/go-cty/cty" 16 17 "github.com/opentofu/opentofu/internal/addrs" 18 "github.com/opentofu/opentofu/internal/encryption" 19 "github.com/opentofu/opentofu/internal/states" 20 "github.com/opentofu/opentofu/internal/states/statefile" 21 "github.com/opentofu/opentofu/internal/states/statemgr" 22 "github.com/opentofu/opentofu/version" 23 tfaddr "github.com/opentofu/registry-address" 24 ) 25 26 func TestState_impl(t *testing.T) { 27 var _ statemgr.Reader = new(State) 28 var _ statemgr.Writer = new(State) 29 var _ statemgr.Persister = new(State) 30 var _ statemgr.Refresher = new(State) 31 var _ statemgr.OutputReader = new(State) 32 var _ statemgr.Locker = new(State) 33 } 34 35 func TestStateRace(t *testing.T) { 36 s := NewState(nilClient{}, encryption.StateEncryptionDisabled()) 37 38 current := states.NewState() 39 40 var wg sync.WaitGroup 41 42 for i := 0; i < 100; i++ { 43 wg.Add(1) 44 go func() { 45 defer wg.Done() 46 s.WriteState(current) 47 s.PersistState(nil) 48 s.RefreshState() 49 }() 50 } 51 wg.Wait() 52 } 53 54 // testCase encapsulates a test state test 55 type testCase struct { 56 name string 57 // A function to mutate state and return a cleanup function 58 mutationFunc func(*State) (*states.State, func()) 59 // The expected requests to have taken place 60 expectedRequests []mockClientRequest 61 // Mark this case as not having a request 62 noRequest bool 63 } 64 65 // isRequested ensures a test that is specified as not having 66 // a request doesn't have one by checking if a method exists 67 // on the expectedRequest. 68 func (tc testCase) isRequested(t *testing.T) bool { 69 for _, expectedMethod := range tc.expectedRequests { 70 hasMethod := expectedMethod.Method != "" 71 if tc.noRequest && hasMethod { 72 t.Fatalf("expected no content for %q but got: %v", tc.name, expectedMethod) 73 } 74 } 75 return !tc.noRequest 76 } 77 78 func TestStatePersist(t *testing.T) { 79 testCases := []testCase{ 80 { 81 name: "first state persistence", 82 mutationFunc: func(mgr *State) (*states.State, func()) { 83 mgr.state = &states.State{ 84 Modules: map[string]*states.Module{"": {}}, 85 } 86 s := mgr.State() 87 s.RootModule().SetResourceInstanceCurrent( 88 addrs.Resource{ 89 Mode: addrs.ManagedResourceMode, 90 Name: "myfile", 91 Type: "local_file", 92 }.Instance(addrs.NoKey), 93 &states.ResourceInstanceObjectSrc{ 94 AttrsFlat: map[string]string{ 95 "filename": "file.txt", 96 }, 97 Status: states.ObjectReady, 98 }, 99 addrs.AbsProviderConfig{ 100 Provider: tfaddr.Provider{Namespace: "local"}, 101 }, 102 ) 103 return s, func() {} 104 }, 105 expectedRequests: []mockClientRequest{ 106 // Expect an initial refresh, which returns nothing since there is no remote state. 107 { 108 Method: "Get", 109 Content: nil, 110 }, 111 // Expect a second refresh, since the read state is nil 112 { 113 Method: "Get", 114 Content: nil, 115 }, 116 // Expect an initial push with values and a serial of 1 117 { 118 Method: "Put", 119 Content: map[string]interface{}{ 120 "version": 4.0, // encoding/json decodes this as float64 by default 121 "lineage": "some meaningless value", 122 "serial": 1.0, // encoding/json decodes this as float64 by default 123 "terraform_version": version.Version, 124 "outputs": map[string]interface{}{}, 125 "resources": []interface{}{ 126 map[string]interface{}{ 127 "instances": []interface{}{ 128 map[string]interface{}{ 129 "attributes_flat": map[string]interface{}{ 130 "filename": "file.txt", 131 }, 132 "schema_version": 0.0, 133 "sensitive_attributes": []interface{}{}, 134 }, 135 }, 136 "mode": "managed", 137 "name": "myfile", 138 "provider": `provider["/local/"]`, 139 "type": "local_file", 140 }, 141 }, 142 "check_results": nil, 143 }, 144 }, 145 }, 146 }, 147 // If lineage changes, expect the serial to increment 148 { 149 name: "change lineage", 150 mutationFunc: func(mgr *State) (*states.State, func()) { 151 mgr.lineage = "mock-lineage" 152 return mgr.State(), func() {} 153 }, 154 expectedRequests: []mockClientRequest{ 155 { 156 Method: "Put", 157 Content: map[string]interface{}{ 158 "version": 4.0, // encoding/json decodes this as float64 by default 159 "lineage": "mock-lineage", 160 "serial": 2.0, // encoding/json decodes this as float64 by default 161 "terraform_version": version.Version, 162 "outputs": map[string]interface{}{}, 163 "resources": []interface{}{ 164 map[string]interface{}{ 165 "instances": []interface{}{ 166 map[string]interface{}{ 167 "attributes_flat": map[string]interface{}{ 168 "filename": "file.txt", 169 }, 170 "schema_version": 0.0, 171 "sensitive_attributes": []interface{}{}, 172 }, 173 }, 174 "mode": "managed", 175 "name": "myfile", 176 "provider": `provider["/local/"]`, 177 "type": "local_file", 178 }, 179 }, 180 "check_results": nil, 181 }, 182 }, 183 }, 184 }, 185 // removing resources should increment the serial 186 { 187 name: "remove resources", 188 mutationFunc: func(mgr *State) (*states.State, func()) { 189 mgr.state.RootModule().Resources = map[string]*states.Resource{} 190 return mgr.State(), func() {} 191 }, 192 expectedRequests: []mockClientRequest{ 193 { 194 Method: "Put", 195 Content: map[string]interface{}{ 196 "version": 4.0, // encoding/json decodes this as float64 by default 197 "lineage": "mock-lineage", 198 "serial": 3.0, // encoding/json decodes this as float64 by default 199 "terraform_version": version.Version, 200 "outputs": map[string]interface{}{}, 201 "resources": []interface{}{}, 202 "check_results": nil, 203 }, 204 }, 205 }, 206 }, 207 // If the remote serial is incremented, then we increment it once more. 208 { 209 name: "change serial", 210 mutationFunc: func(mgr *State) (*states.State, func()) { 211 originalSerial := mgr.serial 212 mgr.serial++ 213 return mgr.State(), func() { 214 mgr.serial = originalSerial 215 } 216 }, 217 expectedRequests: []mockClientRequest{ 218 { 219 Method: "Put", 220 Content: map[string]interface{}{ 221 "version": 4.0, // encoding/json decodes this as float64 by default 222 "lineage": "mock-lineage", 223 "serial": 5.0, // encoding/json decodes this as float64 by default 224 "terraform_version": version.Version, 225 "outputs": map[string]interface{}{}, 226 "resources": []interface{}{}, 227 "check_results": nil, 228 }, 229 }, 230 }, 231 }, 232 // Adding an output should cause the serial to increment as well. 233 { 234 name: "add output to state", 235 mutationFunc: func(mgr *State) (*states.State, func()) { 236 s := mgr.State() 237 s.RootModule().SetOutputValue("foo", cty.StringVal("bar"), false) 238 return s, func() {} 239 }, 240 expectedRequests: []mockClientRequest{ 241 { 242 Method: "Put", 243 Content: map[string]interface{}{ 244 "version": 4.0, // encoding/json decodes this as float64 by default 245 "lineage": "mock-lineage", 246 "serial": 4.0, // encoding/json decodes this as float64 by default 247 "terraform_version": version.Version, 248 "outputs": map[string]interface{}{ 249 "foo": map[string]interface{}{ 250 "type": "string", 251 "value": "bar", 252 }, 253 }, 254 "resources": []interface{}{}, 255 "check_results": nil, 256 }, 257 }, 258 }, 259 }, 260 // ...as should changing an output 261 { 262 name: "mutate state bar -> baz", 263 mutationFunc: func(mgr *State) (*states.State, func()) { 264 s := mgr.State() 265 s.RootModule().SetOutputValue("foo", cty.StringVal("baz"), false) 266 return s, func() {} 267 }, 268 expectedRequests: []mockClientRequest{ 269 { 270 Method: "Put", 271 Content: map[string]interface{}{ 272 "version": 4.0, // encoding/json decodes this as float64 by default 273 "lineage": "mock-lineage", 274 "serial": 5.0, // encoding/json decodes this as float64 by default 275 "terraform_version": version.Version, 276 "outputs": map[string]interface{}{ 277 "foo": map[string]interface{}{ 278 "type": "string", 279 "value": "baz", 280 }, 281 }, 282 "resources": []interface{}{}, 283 "check_results": nil, 284 }, 285 }, 286 }, 287 }, 288 { 289 name: "nothing changed", 290 mutationFunc: func(mgr *State) (*states.State, func()) { 291 s := mgr.State() 292 return s, func() {} 293 }, 294 noRequest: true, 295 }, 296 // If the remote state's serial is less (force push), then we 297 // increment it once from there. 298 { 299 name: "reset serial (force push style)", 300 mutationFunc: func(mgr *State) (*states.State, func()) { 301 mgr.serial = 2 302 return mgr.State(), func() {} 303 }, 304 expectedRequests: []mockClientRequest{ 305 { 306 Method: "Put", 307 Content: map[string]interface{}{ 308 "version": 4.0, // encoding/json decodes this as float64 by default 309 "lineage": "mock-lineage", 310 "serial": 3.0, // encoding/json decodes this as float64 by default 311 "terraform_version": version.Version, 312 "outputs": map[string]interface{}{ 313 "foo": map[string]interface{}{ 314 "type": "string", 315 "value": "baz", 316 }, 317 }, 318 "resources": []interface{}{}, 319 "check_results": nil, 320 }, 321 }, 322 }, 323 }, 324 } 325 326 // Initial setup of state just to give us a fixed starting point for our 327 // test assertions below, or else we'd need to deal with 328 // random lineage. 329 mgr := NewState( 330 &mockClient{}, 331 encryption.StateEncryptionDisabled(), 332 ) 333 334 // In normal use (during a OpenTofu operation) we always refresh and read 335 // before any writes would happen, so we'll mimic that here for realism. 336 // NB This causes a GET to be logged so the first item in the test cases 337 // must account for this 338 if err := mgr.RefreshState(); err != nil { 339 t.Fatalf("failed to RefreshState: %s", err) 340 } 341 342 // Our client is a mockClient which has a log we 343 // use to check that operations generate expected requests 344 mockClient := mgr.Client.(*mockClient) 345 346 // logIdx tracks the current index of the log separate from 347 // the loop iteration so we can check operations that don't 348 // cause any requests to be generated 349 logIdx := 0 350 351 // Run tests in order. 352 for _, tc := range testCases { 353 t.Run(tc.name, func(t *testing.T) { 354 s, cleanup := tc.mutationFunc(mgr) 355 356 if err := mgr.WriteState(s); err != nil { 357 t.Fatalf("failed to WriteState for %q: %s", tc.name, err) 358 } 359 if err := mgr.PersistState(nil); err != nil { 360 t.Fatalf("failed to PersistState for %q: %s", tc.name, err) 361 } 362 363 if tc.isRequested(t) { 364 // Get captured request from the mock client log 365 // based on the index of the current test 366 if logIdx >= len(mockClient.log) { 367 t.Fatalf("request lock and index are out of sync on %q: idx=%d len=%d", tc.name, logIdx, len(mockClient.log)) 368 } 369 for expectedRequestIdx := 0; expectedRequestIdx < len(tc.expectedRequests); expectedRequestIdx++ { 370 loggedRequest := mockClient.log[logIdx] 371 logIdx++ 372 if diff := cmp.Diff(tc.expectedRequests[expectedRequestIdx], loggedRequest, cmpopts.IgnoreMapEntries(func(key string, value interface{}) bool { 373 // This is required since the initial state creation causes the lineage to be a UUID that is not known at test time. 374 return tc.name == "first state persistence" && key == "lineage" 375 })); len(diff) > 0 { 376 t.Logf("incorrect client requests for %q:\n%s", tc.name, diff) 377 t.Fail() 378 } 379 } 380 } 381 cleanup() 382 }) 383 } 384 logCnt := len(mockClient.log) 385 if logIdx != logCnt { 386 t.Fatalf("not all requests were read. Expected logIdx to be %d but got %d", logCnt, logIdx) 387 } 388 } 389 390 func TestState_GetRootOutputValues(t *testing.T) { 391 // Initial setup of state with outputs already defined 392 mgr := NewState( 393 &mockClient{ 394 current: []byte(` 395 { 396 "version": 4, 397 "lineage": "mock-lineage", 398 "serial": 1, 399 "terraform_version":"0.0.0", 400 "outputs": {"foo": {"value":"bar", "type": "string"}}, 401 "resources": [] 402 } 403 `), 404 }, 405 encryption.StateEncryptionDisabled(), 406 ) 407 408 outputs, err := mgr.GetRootOutputValues() 409 if err != nil { 410 t.Errorf("Expected GetRootOutputValues to not return an error, but it returned %v", err) 411 } 412 413 if len(outputs) != 1 { 414 t.Errorf("Expected %d outputs, but received %d", 1, len(outputs)) 415 } 416 } 417 418 type migrationTestCase struct { 419 name string 420 // A function to generate a statefile 421 stateFile func(*State) *statefile.File 422 // The expected request to have taken place 423 expectedRequest mockClientRequest 424 // Mark this case as not having a request 425 expectedError string 426 // force flag passed to client 427 force bool 428 } 429 430 func TestWriteStateForMigration(t *testing.T) { 431 mgr := NewState( 432 &mockClient{ 433 current: []byte(` 434 { 435 "version": 4, 436 "lineage": "mock-lineage", 437 "serial": 3, 438 "terraform_version":"0.0.0", 439 "outputs": {"foo": {"value":"bar", "type": "string"}}, 440 "resources": [] 441 } 442 `), 443 }, 444 encryption.StateEncryptionDisabled(), 445 ) 446 447 testCases := []migrationTestCase{ 448 // Refreshing state before we run the test loop causes a GET 449 { 450 name: "refresh state", 451 stateFile: func(mgr *State) *statefile.File { 452 return mgr.StateForMigration() 453 }, 454 expectedRequest: mockClientRequest{ 455 Method: "Get", 456 Content: map[string]interface{}{ 457 "version": 4.0, 458 "lineage": "mock-lineage", 459 "serial": 3.0, 460 "terraform_version": "0.0.0", 461 "outputs": map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}}, 462 "resources": []interface{}{}, 463 }, 464 }, 465 }, 466 { 467 name: "cannot import lesser serial without force", 468 stateFile: func(mgr *State) *statefile.File { 469 return statefile.New(mgr.state, mgr.lineage, 1) 470 }, 471 expectedError: "cannot import state with serial 1 over newer state with serial 3", 472 }, 473 { 474 name: "cannot import differing lineage without force", 475 stateFile: func(mgr *State) *statefile.File { 476 return statefile.New(mgr.state, "different-lineage", mgr.serial) 477 }, 478 expectedError: `cannot import state with lineage "different-lineage" over unrelated state with lineage "mock-lineage"`, 479 }, 480 { 481 name: "can import lesser serial with force", 482 stateFile: func(mgr *State) *statefile.File { 483 return statefile.New(mgr.state, mgr.lineage, 1) 484 }, 485 expectedRequest: mockClientRequest{ 486 Method: "Put", 487 Content: map[string]interface{}{ 488 "version": 4.0, 489 "lineage": "mock-lineage", 490 "serial": 2.0, 491 "terraform_version": version.Version, 492 "outputs": map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}}, 493 "resources": []interface{}{}, 494 "check_results": nil, 495 }, 496 }, 497 force: true, 498 }, 499 { 500 name: "cannot import differing lineage without force", 501 stateFile: func(mgr *State) *statefile.File { 502 return statefile.New(mgr.state, "different-lineage", mgr.serial) 503 }, 504 expectedRequest: mockClientRequest{ 505 Method: "Put", 506 Content: map[string]interface{}{ 507 "version": 4.0, 508 "lineage": "different-lineage", 509 "serial": 3.0, 510 "terraform_version": version.Version, 511 "outputs": map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}}, 512 "resources": []interface{}{}, 513 "check_results": nil, 514 }, 515 }, 516 force: true, 517 }, 518 } 519 520 // In normal use (during a OpenTofu operation) we always refresh and read 521 // before any writes would happen, so we'll mimic that here for realism. 522 // NB This causes a GET to be logged so the first item in the test cases 523 // must account for this 524 if err := mgr.RefreshState(); err != nil { 525 t.Fatalf("failed to RefreshState: %s", err) 526 } 527 528 if err := mgr.WriteState(mgr.State()); err != nil { 529 t.Fatalf("failed to write initial state: %s", err) 530 } 531 532 // Our client is a mockClient which has a log we 533 // use to check that operations generate expected requests 534 mockClient := mgr.Client.(*mockClient) 535 536 // logIdx tracks the current index of the log separate from 537 // the loop iteration so we can check operations that don't 538 // cause any requests to be generated 539 logIdx := 0 540 541 for _, tc := range testCases { 542 t.Run(tc.name, func(t *testing.T) { 543 sf := tc.stateFile(mgr) 544 err := mgr.WriteStateForMigration(sf, tc.force) 545 shouldError := tc.expectedError != "" 546 547 // If we are expecting and error check it and move on 548 if shouldError { 549 if err == nil { 550 t.Fatalf("test case %q should have failed with error %q", tc.name, tc.expectedError) 551 } else if err.Error() != tc.expectedError { 552 t.Fatalf("test case %q expected error %q but got %q", tc.name, tc.expectedError, err) 553 } 554 return 555 } 556 557 if err != nil { 558 t.Fatalf("test case %q failed: %v", tc.name, err) 559 } 560 561 // At this point we should just do a normal write and persist 562 // as would happen from the CLI 563 mgr.WriteState(mgr.State()) 564 mgr.PersistState(nil) 565 566 if logIdx >= len(mockClient.log) { 567 t.Fatalf("request lock and index are out of sync on %q: idx=%d len=%d", tc.name, logIdx, len(mockClient.log)) 568 } 569 loggedRequest := mockClient.log[logIdx] 570 logIdx++ 571 if diff := cmp.Diff(tc.expectedRequest, loggedRequest); len(diff) > 0 { 572 t.Fatalf("incorrect client requests for %q:\n%s", tc.name, diff) 573 } 574 }) 575 } 576 577 logCnt := len(mockClient.log) 578 if logIdx != logCnt { 579 log.Fatalf("not all requests were read. Expected logIdx to be %d but got %d", logCnt, logIdx) 580 } 581 } 582 583 // This test runs the same test cases as above, but with 584 // a client that implements EnableForcePush -- this allows 585 // us to test that -force continues to work for backends without 586 // this interface, but that this interface works for those that do. 587 func TestWriteStateForMigrationWithForcePushClient(t *testing.T) { 588 mgr := NewState( 589 &mockClientForcePusher{ 590 current: []byte(` 591 { 592 "version": 4, 593 "lineage": "mock-lineage", 594 "serial": 3, 595 "terraform_version":"0.0.0", 596 "outputs": {"foo": {"value":"bar", "type": "string"}}, 597 "resources": [] 598 } 599 `), 600 }, 601 encryption.StateEncryptionDisabled(), 602 ) 603 604 testCases := []migrationTestCase{ 605 // Refreshing state before we run the test loop causes a GET 606 { 607 name: "refresh state", 608 stateFile: func(mgr *State) *statefile.File { 609 return mgr.StateForMigration() 610 }, 611 expectedRequest: mockClientRequest{ 612 Method: "Get", 613 Content: map[string]interface{}{ 614 "version": 4.0, 615 "lineage": "mock-lineage", 616 "serial": 3.0, 617 "terraform_version": "0.0.0", 618 "outputs": map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}}, 619 "resources": []interface{}{}, 620 }, 621 }, 622 }, 623 { 624 name: "cannot import lesser serial without force", 625 stateFile: func(mgr *State) *statefile.File { 626 return statefile.New(mgr.state, mgr.lineage, 1) 627 }, 628 expectedError: "cannot import state with serial 1 over newer state with serial 3", 629 }, 630 { 631 name: "cannot import differing lineage without force", 632 stateFile: func(mgr *State) *statefile.File { 633 return statefile.New(mgr.state, "different-lineage", mgr.serial) 634 }, 635 expectedError: `cannot import state with lineage "different-lineage" over unrelated state with lineage "mock-lineage"`, 636 }, 637 { 638 name: "can import lesser serial with force", 639 stateFile: func(mgr *State) *statefile.File { 640 return statefile.New(mgr.state, mgr.lineage, 1) 641 }, 642 expectedRequest: mockClientRequest{ 643 Method: "Force Put", 644 Content: map[string]interface{}{ 645 "version": 4.0, 646 "lineage": "mock-lineage", 647 "serial": 2.0, 648 "terraform_version": version.Version, 649 "outputs": map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}}, 650 "resources": []interface{}{}, 651 "check_results": nil, 652 }, 653 }, 654 force: true, 655 }, 656 { 657 name: "cannot import differing lineage without force", 658 stateFile: func(mgr *State) *statefile.File { 659 return statefile.New(mgr.state, "different-lineage", mgr.serial) 660 }, 661 expectedRequest: mockClientRequest{ 662 Method: "Force Put", 663 Content: map[string]interface{}{ 664 "version": 4.0, 665 "lineage": "different-lineage", 666 "serial": 3.0, 667 "terraform_version": version.Version, 668 "outputs": map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}}, 669 "resources": []interface{}{}, 670 "check_results": nil, 671 }, 672 }, 673 force: true, 674 }, 675 } 676 677 // In normal use (during a OpenTofu operation) we always refresh and read 678 // before any writes would happen, so we'll mimic that here for realism. 679 // NB This causes a GET to be logged so the first item in the test cases 680 // must account for this 681 if err := mgr.RefreshState(); err != nil { 682 t.Fatalf("failed to RefreshState: %s", err) 683 } 684 685 if err := mgr.WriteState(mgr.State()); err != nil { 686 t.Fatalf("failed to write initial state: %s", err) 687 } 688 689 // Our client is a mockClientForcePusher which has a log we 690 // use to check that operations generate expected requests 691 mockClient := mgr.Client.(*mockClientForcePusher) 692 693 if mockClient.force { 694 t.Fatalf("client should not default to force") 695 } 696 697 // logIdx tracks the current index of the log separate from 698 // the loop iteration so we can check operations that don't 699 // cause any requests to be generated 700 logIdx := 0 701 702 for _, tc := range testCases { 703 t.Run(tc.name, func(t *testing.T) { 704 // Always reset client to not be force pushing 705 mockClient.force = false 706 sf := tc.stateFile(mgr) 707 err := mgr.WriteStateForMigration(sf, tc.force) 708 shouldError := tc.expectedError != "" 709 710 // If we are expecting and error check it and move on 711 if shouldError { 712 if err == nil { 713 t.Fatalf("test case %q should have failed with error %q", tc.name, tc.expectedError) 714 } else if err.Error() != tc.expectedError { 715 t.Fatalf("test case %q expected error %q but got %q", tc.name, tc.expectedError, err) 716 } 717 return 718 } 719 720 if err != nil { 721 t.Fatalf("test case %q failed: %v", tc.name, err) 722 } 723 724 if tc.force && !mockClient.force { 725 t.Fatalf("test case %q should have enabled force push", tc.name) 726 } 727 728 // At this point we should just do a normal write and persist 729 // as would happen from the CLI 730 mgr.WriteState(mgr.State()) 731 mgr.PersistState(nil) 732 733 if logIdx >= len(mockClient.log) { 734 t.Fatalf("request lock and index are out of sync on %q: idx=%d len=%d", tc.name, logIdx, len(mockClient.log)) 735 } 736 loggedRequest := mockClient.log[logIdx] 737 logIdx++ 738 if diff := cmp.Diff(tc.expectedRequest, loggedRequest); len(diff) > 0 { 739 t.Fatalf("incorrect client requests for %q:\n%s", tc.name, diff) 740 } 741 }) 742 } 743 744 logCnt := len(mockClient.log) 745 if logIdx != logCnt { 746 log.Fatalf("not all requests were read. Expected logIdx to be %d but got %d", logCnt, logIdx) 747 } 748 }