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