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