| package remote |
| |
| import ( |
| "log" |
| "sync" |
| "testing" |
| |
| "github.com/google/go-cmp/cmp" |
| "github.com/google/go-cmp/cmp/cmpopts" |
| "github.com/zclconf/go-cty/cty" |
| |
| tfaddr "github.com/hashicorp/terraform-registry-address" |
| "github.com/hashicorp/terraform/internal/addrs" |
| "github.com/hashicorp/terraform/internal/states" |
| "github.com/hashicorp/terraform/internal/states/statefile" |
| "github.com/hashicorp/terraform/internal/states/statemgr" |
| "github.com/hashicorp/terraform/version" |
| ) |
| |
| func TestState_impl(t *testing.T) { |
| var _ statemgr.Reader = new(State) |
| var _ statemgr.Writer = new(State) |
| var _ statemgr.Persister = new(State) |
| var _ statemgr.Refresher = new(State) |
| var _ statemgr.OutputReader = new(State) |
| var _ statemgr.Locker = new(State) |
| } |
| |
| func TestStateRace(t *testing.T) { |
| s := &State{ |
| Client: nilClient{}, |
| } |
| |
| current := states.NewState() |
| |
| var wg sync.WaitGroup |
| |
| for i := 0; i < 100; i++ { |
| wg.Add(1) |
| go func() { |
| defer wg.Done() |
| s.WriteState(current) |
| s.PersistState(nil) |
| s.RefreshState() |
| }() |
| } |
| wg.Wait() |
| } |
| |
| // testCase encapsulates a test state test |
| type testCase struct { |
| name string |
| // A function to mutate state and return a cleanup function |
| mutationFunc func(*State) (*states.State, func()) |
| // The expected requests to have taken place |
| expectedRequests []mockClientRequest |
| // Mark this case as not having a request |
| noRequest bool |
| } |
| |
| // isRequested ensures a test that is specified as not having |
| // a request doesn't have one by checking if a method exists |
| // on the expectedRequest. |
| func (tc testCase) isRequested(t *testing.T) bool { |
| for _, expectedMethod := range tc.expectedRequests { |
| hasMethod := expectedMethod.Method != "" |
| if tc.noRequest && hasMethod { |
| t.Fatalf("expected no content for %q but got: %v", tc.name, expectedMethod) |
| } |
| } |
| return !tc.noRequest |
| } |
| |
| func TestStatePersist(t *testing.T) { |
| testCases := []testCase{ |
| { |
| name: "first state persistence", |
| mutationFunc: func(mgr *State) (*states.State, func()) { |
| mgr.state = &states.State{ |
| Modules: map[string]*states.Module{"": {}}, |
| } |
| s := mgr.State() |
| s.RootModule().SetResourceInstanceCurrent( |
| addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Name: "myfile", |
| Type: "local_file", |
| }.Instance(addrs.NoKey), |
| &states.ResourceInstanceObjectSrc{ |
| AttrsFlat: map[string]string{ |
| "filename": "file.txt", |
| }, |
| Status: states.ObjectReady, |
| }, |
| addrs.AbsProviderConfig{ |
| Provider: tfaddr.Provider{Namespace: "local"}, |
| }, |
| ) |
| return s, func() {} |
| }, |
| expectedRequests: []mockClientRequest{ |
| // Expect an initial refresh, which returns nothing since there is no remote state. |
| { |
| Method: "Get", |
| Content: nil, |
| }, |
| // Expect a second refresh, since the read state is nil |
| { |
| Method: "Get", |
| Content: nil, |
| }, |
| // Expect an initial push with values and a serial of 1 |
| { |
| Method: "Put", |
| Content: map[string]interface{}{ |
| "version": 4.0, // encoding/json decodes this as float64 by default |
| "lineage": "some meaningless value", |
| "serial": 1.0, // encoding/json decodes this as float64 by default |
| "terraform_version": version.Version, |
| "outputs": map[string]interface{}{}, |
| "resources": []interface{}{ |
| map[string]interface{}{ |
| "instances": []interface{}{ |
| map[string]interface{}{ |
| "attributes_flat": map[string]interface{}{ |
| "filename": "file.txt", |
| }, |
| "schema_version": 0.0, |
| "sensitive_attributes": []interface{}{}, |
| }, |
| }, |
| "mode": "managed", |
| "name": "myfile", |
| "provider": `provider["/local/"]`, |
| "type": "local_file", |
| }, |
| }, |
| "check_results": nil, |
| }, |
| }, |
| }, |
| }, |
| // If lineage changes, expect the serial to increment |
| { |
| name: "change lineage", |
| mutationFunc: func(mgr *State) (*states.State, func()) { |
| mgr.lineage = "mock-lineage" |
| return mgr.State(), func() {} |
| }, |
| expectedRequests: []mockClientRequest{ |
| { |
| Method: "Put", |
| Content: map[string]interface{}{ |
| "version": 4.0, // encoding/json decodes this as float64 by default |
| "lineage": "mock-lineage", |
| "serial": 2.0, // encoding/json decodes this as float64 by default |
| "terraform_version": version.Version, |
| "outputs": map[string]interface{}{}, |
| "resources": []interface{}{ |
| map[string]interface{}{ |
| "instances": []interface{}{ |
| map[string]interface{}{ |
| "attributes_flat": map[string]interface{}{ |
| "filename": "file.txt", |
| }, |
| "schema_version": 0.0, |
| "sensitive_attributes": []interface{}{}, |
| }, |
| }, |
| "mode": "managed", |
| "name": "myfile", |
| "provider": `provider["/local/"]`, |
| "type": "local_file", |
| }, |
| }, |
| "check_results": nil, |
| }, |
| }, |
| }, |
| }, |
| // removing resources should increment the serial |
| { |
| name: "remove resources", |
| mutationFunc: func(mgr *State) (*states.State, func()) { |
| mgr.state.RootModule().Resources = map[string]*states.Resource{} |
| return mgr.State(), func() {} |
| }, |
| expectedRequests: []mockClientRequest{ |
| { |
| Method: "Put", |
| Content: map[string]interface{}{ |
| "version": 4.0, // encoding/json decodes this as float64 by default |
| "lineage": "mock-lineage", |
| "serial": 3.0, // encoding/json decodes this as float64 by default |
| "terraform_version": version.Version, |
| "outputs": map[string]interface{}{}, |
| "resources": []interface{}{}, |
| "check_results": nil, |
| }, |
| }, |
| }, |
| }, |
| // If the remote serial is incremented, then we increment it once more. |
| { |
| name: "change serial", |
| mutationFunc: func(mgr *State) (*states.State, func()) { |
| originalSerial := mgr.serial |
| mgr.serial++ |
| return mgr.State(), func() { |
| mgr.serial = originalSerial |
| } |
| }, |
| expectedRequests: []mockClientRequest{ |
| { |
| Method: "Put", |
| Content: map[string]interface{}{ |
| "version": 4.0, // encoding/json decodes this as float64 by default |
| "lineage": "mock-lineage", |
| "serial": 5.0, // encoding/json decodes this as float64 by default |
| "terraform_version": version.Version, |
| "outputs": map[string]interface{}{}, |
| "resources": []interface{}{}, |
| "check_results": nil, |
| }, |
| }, |
| }, |
| }, |
| // Adding an output should cause the serial to increment as well. |
| { |
| name: "add output to state", |
| mutationFunc: func(mgr *State) (*states.State, func()) { |
| s := mgr.State() |
| s.RootModule().SetOutputValue("foo", cty.StringVal("bar"), false) |
| return s, func() {} |
| }, |
| expectedRequests: []mockClientRequest{ |
| { |
| Method: "Put", |
| Content: map[string]interface{}{ |
| "version": 4.0, // encoding/json decodes this as float64 by default |
| "lineage": "mock-lineage", |
| "serial": 4.0, // encoding/json decodes this as float64 by default |
| "terraform_version": version.Version, |
| "outputs": map[string]interface{}{ |
| "foo": map[string]interface{}{ |
| "type": "string", |
| "value": "bar", |
| }, |
| }, |
| "resources": []interface{}{}, |
| "check_results": nil, |
| }, |
| }, |
| }, |
| }, |
| // ...as should changing an output |
| { |
| name: "mutate state bar -> baz", |
| mutationFunc: func(mgr *State) (*states.State, func()) { |
| s := mgr.State() |
| s.RootModule().SetOutputValue("foo", cty.StringVal("baz"), false) |
| return s, func() {} |
| }, |
| expectedRequests: []mockClientRequest{ |
| { |
| Method: "Put", |
| Content: map[string]interface{}{ |
| "version": 4.0, // encoding/json decodes this as float64 by default |
| "lineage": "mock-lineage", |
| "serial": 5.0, // encoding/json decodes this as float64 by default |
| "terraform_version": version.Version, |
| "outputs": map[string]interface{}{ |
| "foo": map[string]interface{}{ |
| "type": "string", |
| "value": "baz", |
| }, |
| }, |
| "resources": []interface{}{}, |
| "check_results": nil, |
| }, |
| }, |
| }, |
| }, |
| { |
| name: "nothing changed", |
| mutationFunc: func(mgr *State) (*states.State, func()) { |
| s := mgr.State() |
| return s, func() {} |
| }, |
| noRequest: true, |
| }, |
| // If the remote state's serial is less (force push), then we |
| // increment it once from there. |
| { |
| name: "reset serial (force push style)", |
| mutationFunc: func(mgr *State) (*states.State, func()) { |
| mgr.serial = 2 |
| return mgr.State(), func() {} |
| }, |
| expectedRequests: []mockClientRequest{ |
| { |
| Method: "Put", |
| Content: map[string]interface{}{ |
| "version": 4.0, // encoding/json decodes this as float64 by default |
| "lineage": "mock-lineage", |
| "serial": 3.0, // encoding/json decodes this as float64 by default |
| "terraform_version": version.Version, |
| "outputs": map[string]interface{}{ |
| "foo": map[string]interface{}{ |
| "type": "string", |
| "value": "baz", |
| }, |
| }, |
| "resources": []interface{}{}, |
| "check_results": nil, |
| }, |
| }, |
| }, |
| }, |
| } |
| |
| // Initial setup of state just to give us a fixed starting point for our |
| // test assertions below, or else we'd need to deal with |
| // random lineage. |
| mgr := &State{ |
| Client: &mockClient{}, |
| } |
| |
| // In normal use (during a Terraform operation) we always refresh and read |
| // before any writes would happen, so we'll mimic that here for realism. |
| // NB This causes a GET to be logged so the first item in the test cases |
| // must account for this |
| if err := mgr.RefreshState(); err != nil { |
| t.Fatalf("failed to RefreshState: %s", err) |
| } |
| |
| // Our client is a mockClient which has a log we |
| // use to check that operations generate expected requests |
| mockClient := mgr.Client.(*mockClient) |
| |
| // logIdx tracks the current index of the log separate from |
| // the loop iteration so we can check operations that don't |
| // cause any requests to be generated |
| logIdx := 0 |
| |
| // Run tests in order. |
| for _, tc := range testCases { |
| t.Run(tc.name, func(t *testing.T) { |
| s, cleanup := tc.mutationFunc(mgr) |
| |
| if err := mgr.WriteState(s); err != nil { |
| t.Fatalf("failed to WriteState for %q: %s", tc.name, err) |
| } |
| if err := mgr.PersistState(nil); err != nil { |
| t.Fatalf("failed to PersistState for %q: %s", tc.name, err) |
| } |
| |
| if tc.isRequested(t) { |
| // Get captured request from the mock client log |
| // based on the index of the current test |
| if logIdx >= len(mockClient.log) { |
| t.Fatalf("request lock and index are out of sync on %q: idx=%d len=%d", tc.name, logIdx, len(mockClient.log)) |
| } |
| for expectedRequestIdx := 0; expectedRequestIdx < len(tc.expectedRequests); expectedRequestIdx++ { |
| loggedRequest := mockClient.log[logIdx] |
| logIdx++ |
| if diff := cmp.Diff(tc.expectedRequests[expectedRequestIdx], loggedRequest, cmpopts.IgnoreMapEntries(func(key string, value interface{}) bool { |
| // This is required since the initial state creation causes the lineage to be a UUID that is not known at test time. |
| return tc.name == "first state persistence" && key == "lineage" |
| })); len(diff) > 0 { |
| t.Logf("incorrect client requests for %q:\n%s", tc.name, diff) |
| t.Fail() |
| } |
| } |
| } |
| cleanup() |
| }) |
| } |
| logCnt := len(mockClient.log) |
| if logIdx != logCnt { |
| t.Fatalf("not all requests were read. Expected logIdx to be %d but got %d", logCnt, logIdx) |
| } |
| } |
| |
| func TestState_GetRootOutputValues(t *testing.T) { |
| // Initial setup of state with outputs already defined |
| mgr := &State{ |
| Client: &mockClient{ |
| current: []byte(` |
| { |
| "version": 4, |
| "lineage": "mock-lineage", |
| "serial": 1, |
| "terraform_version":"0.0.0", |
| "outputs": {"foo": {"value":"bar", "type": "string"}}, |
| "resources": [] |
| } |
| `), |
| }, |
| } |
| |
| outputs, err := mgr.GetRootOutputValues() |
| if err != nil { |
| t.Errorf("Expected GetRootOutputValues to not return an error, but it returned %v", err) |
| } |
| |
| if len(outputs) != 1 { |
| t.Errorf("Expected %d outputs, but received %d", 1, len(outputs)) |
| } |
| } |
| |
| type migrationTestCase struct { |
| name string |
| // A function to generate a statefile |
| stateFile func(*State) *statefile.File |
| // The expected request to have taken place |
| expectedRequest mockClientRequest |
| // Mark this case as not having a request |
| expectedError string |
| // force flag passed to client |
| force bool |
| } |
| |
| func TestWriteStateForMigration(t *testing.T) { |
| mgr := &State{ |
| Client: &mockClient{ |
| current: []byte(` |
| { |
| "version": 4, |
| "lineage": "mock-lineage", |
| "serial": 3, |
| "terraform_version":"0.0.0", |
| "outputs": {"foo": {"value":"bar", "type": "string"}}, |
| "resources": [] |
| } |
| `), |
| }, |
| } |
| |
| testCases := []migrationTestCase{ |
| // Refreshing state before we run the test loop causes a GET |
| { |
| name: "refresh state", |
| stateFile: func(mgr *State) *statefile.File { |
| return mgr.StateForMigration() |
| }, |
| expectedRequest: mockClientRequest{ |
| Method: "Get", |
| Content: map[string]interface{}{ |
| "version": 4.0, |
| "lineage": "mock-lineage", |
| "serial": 3.0, |
| "terraform_version": "0.0.0", |
| "outputs": map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}}, |
| "resources": []interface{}{}, |
| }, |
| }, |
| }, |
| { |
| name: "cannot import lesser serial without force", |
| stateFile: func(mgr *State) *statefile.File { |
| return statefile.New(mgr.state, mgr.lineage, 1) |
| }, |
| expectedError: "cannot import state with serial 1 over newer state with serial 3", |
| }, |
| { |
| name: "cannot import differing lineage without force", |
| stateFile: func(mgr *State) *statefile.File { |
| return statefile.New(mgr.state, "different-lineage", mgr.serial) |
| }, |
| expectedError: `cannot import state with lineage "different-lineage" over unrelated state with lineage "mock-lineage"`, |
| }, |
| { |
| name: "can import lesser serial with force", |
| stateFile: func(mgr *State) *statefile.File { |
| return statefile.New(mgr.state, mgr.lineage, 1) |
| }, |
| expectedRequest: mockClientRequest{ |
| Method: "Put", |
| Content: map[string]interface{}{ |
| "version": 4.0, |
| "lineage": "mock-lineage", |
| "serial": 2.0, |
| "terraform_version": version.Version, |
| "outputs": map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}}, |
| "resources": []interface{}{}, |
| "check_results": nil, |
| }, |
| }, |
| force: true, |
| }, |
| { |
| name: "cannot import differing lineage without force", |
| stateFile: func(mgr *State) *statefile.File { |
| return statefile.New(mgr.state, "different-lineage", mgr.serial) |
| }, |
| expectedRequest: mockClientRequest{ |
| Method: "Put", |
| Content: map[string]interface{}{ |
| "version": 4.0, |
| "lineage": "different-lineage", |
| "serial": 3.0, |
| "terraform_version": version.Version, |
| "outputs": map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}}, |
| "resources": []interface{}{}, |
| "check_results": nil, |
| }, |
| }, |
| force: true, |
| }, |
| } |
| |
| // In normal use (during a Terraform operation) we always refresh and read |
| // before any writes would happen, so we'll mimic that here for realism. |
| // NB This causes a GET to be logged so the first item in the test cases |
| // must account for this |
| if err := mgr.RefreshState(); err != nil { |
| t.Fatalf("failed to RefreshState: %s", err) |
| } |
| |
| if err := mgr.WriteState(mgr.State()); err != nil { |
| t.Fatalf("failed to write initial state: %s", err) |
| } |
| |
| // Our client is a mockClient which has a log we |
| // use to check that operations generate expected requests |
| mockClient := mgr.Client.(*mockClient) |
| |
| // logIdx tracks the current index of the log separate from |
| // the loop iteration so we can check operations that don't |
| // cause any requests to be generated |
| logIdx := 0 |
| |
| for _, tc := range testCases { |
| t.Run(tc.name, func(t *testing.T) { |
| sf := tc.stateFile(mgr) |
| err := mgr.WriteStateForMigration(sf, tc.force) |
| shouldError := tc.expectedError != "" |
| |
| // If we are expecting and error check it and move on |
| if shouldError { |
| if err == nil { |
| t.Fatalf("test case %q should have failed with error %q", tc.name, tc.expectedError) |
| } else if err.Error() != tc.expectedError { |
| t.Fatalf("test case %q expected error %q but got %q", tc.name, tc.expectedError, err) |
| } |
| return |
| } |
| |
| if err != nil { |
| t.Fatalf("test case %q failed: %v", tc.name, err) |
| } |
| |
| // At this point we should just do a normal write and persist |
| // as would happen from the CLI |
| mgr.WriteState(mgr.State()) |
| mgr.PersistState(nil) |
| |
| if logIdx >= len(mockClient.log) { |
| t.Fatalf("request lock and index are out of sync on %q: idx=%d len=%d", tc.name, logIdx, len(mockClient.log)) |
| } |
| loggedRequest := mockClient.log[logIdx] |
| logIdx++ |
| if diff := cmp.Diff(tc.expectedRequest, loggedRequest); len(diff) > 0 { |
| t.Fatalf("incorrect client requests for %q:\n%s", tc.name, diff) |
| } |
| }) |
| } |
| |
| logCnt := len(mockClient.log) |
| if logIdx != logCnt { |
| log.Fatalf("not all requests were read. Expected logIdx to be %d but got %d", logCnt, logIdx) |
| } |
| } |
| |
| // This test runs the same test cases as above, but with |
| // a client that implements EnableForcePush -- this allows |
| // us to test that -force continues to work for backends without |
| // this interface, but that this interface works for those that do. |
| func TestWriteStateForMigrationWithForcePushClient(t *testing.T) { |
| mgr := &State{ |
| Client: &mockClientForcePusher{ |
| current: []byte(` |
| { |
| "version": 4, |
| "lineage": "mock-lineage", |
| "serial": 3, |
| "terraform_version":"0.0.0", |
| "outputs": {"foo": {"value":"bar", "type": "string"}}, |
| "resources": [] |
| } |
| `), |
| }, |
| } |
| |
| testCases := []migrationTestCase{ |
| // Refreshing state before we run the test loop causes a GET |
| { |
| name: "refresh state", |
| stateFile: func(mgr *State) *statefile.File { |
| return mgr.StateForMigration() |
| }, |
| expectedRequest: mockClientRequest{ |
| Method: "Get", |
| Content: map[string]interface{}{ |
| "version": 4.0, |
| "lineage": "mock-lineage", |
| "serial": 3.0, |
| "terraform_version": "0.0.0", |
| "outputs": map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}}, |
| "resources": []interface{}{}, |
| }, |
| }, |
| }, |
| { |
| name: "cannot import lesser serial without force", |
| stateFile: func(mgr *State) *statefile.File { |
| return statefile.New(mgr.state, mgr.lineage, 1) |
| }, |
| expectedError: "cannot import state with serial 1 over newer state with serial 3", |
| }, |
| { |
| name: "cannot import differing lineage without force", |
| stateFile: func(mgr *State) *statefile.File { |
| return statefile.New(mgr.state, "different-lineage", mgr.serial) |
| }, |
| expectedError: `cannot import state with lineage "different-lineage" over unrelated state with lineage "mock-lineage"`, |
| }, |
| { |
| name: "can import lesser serial with force", |
| stateFile: func(mgr *State) *statefile.File { |
| return statefile.New(mgr.state, mgr.lineage, 1) |
| }, |
| expectedRequest: mockClientRequest{ |
| Method: "Force Put", |
| Content: map[string]interface{}{ |
| "version": 4.0, |
| "lineage": "mock-lineage", |
| "serial": 2.0, |
| "terraform_version": version.Version, |
| "outputs": map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}}, |
| "resources": []interface{}{}, |
| "check_results": nil, |
| }, |
| }, |
| force: true, |
| }, |
| { |
| name: "cannot import differing lineage without force", |
| stateFile: func(mgr *State) *statefile.File { |
| return statefile.New(mgr.state, "different-lineage", mgr.serial) |
| }, |
| expectedRequest: mockClientRequest{ |
| Method: "Force Put", |
| Content: map[string]interface{}{ |
| "version": 4.0, |
| "lineage": "different-lineage", |
| "serial": 3.0, |
| "terraform_version": version.Version, |
| "outputs": map[string]interface{}{"foo": map[string]interface{}{"type": string("string"), "value": string("bar")}}, |
| "resources": []interface{}{}, |
| "check_results": nil, |
| }, |
| }, |
| force: true, |
| }, |
| } |
| |
| // In normal use (during a Terraform operation) we always refresh and read |
| // before any writes would happen, so we'll mimic that here for realism. |
| // NB This causes a GET to be logged so the first item in the test cases |
| // must account for this |
| if err := mgr.RefreshState(); err != nil { |
| t.Fatalf("failed to RefreshState: %s", err) |
| } |
| |
| if err := mgr.WriteState(mgr.State()); err != nil { |
| t.Fatalf("failed to write initial state: %s", err) |
| } |
| |
| // Our client is a mockClientForcePusher which has a log we |
| // use to check that operations generate expected requests |
| mockClient := mgr.Client.(*mockClientForcePusher) |
| |
| if mockClient.force { |
| t.Fatalf("client should not default to force") |
| } |
| |
| // logIdx tracks the current index of the log separate from |
| // the loop iteration so we can check operations that don't |
| // cause any requests to be generated |
| logIdx := 0 |
| |
| for _, tc := range testCases { |
| t.Run(tc.name, func(t *testing.T) { |
| // Always reset client to not be force pushing |
| mockClient.force = false |
| sf := tc.stateFile(mgr) |
| err := mgr.WriteStateForMigration(sf, tc.force) |
| shouldError := tc.expectedError != "" |
| |
| // If we are expecting and error check it and move on |
| if shouldError { |
| if err == nil { |
| t.Fatalf("test case %q should have failed with error %q", tc.name, tc.expectedError) |
| } else if err.Error() != tc.expectedError { |
| t.Fatalf("test case %q expected error %q but got %q", tc.name, tc.expectedError, err) |
| } |
| return |
| } |
| |
| if err != nil { |
| t.Fatalf("test case %q failed: %v", tc.name, err) |
| } |
| |
| if tc.force && !mockClient.force { |
| t.Fatalf("test case %q should have enabled force push", tc.name) |
| } |
| |
| // At this point we should just do a normal write and persist |
| // as would happen from the CLI |
| mgr.WriteState(mgr.State()) |
| mgr.PersistState(nil) |
| |
| if logIdx >= len(mockClient.log) { |
| t.Fatalf("request lock and index are out of sync on %q: idx=%d len=%d", tc.name, logIdx, len(mockClient.log)) |
| } |
| loggedRequest := mockClient.log[logIdx] |
| logIdx++ |
| if diff := cmp.Diff(tc.expectedRequest, loggedRequest); len(diff) > 0 { |
| t.Fatalf("incorrect client requests for %q:\n%s", tc.name, diff) |
| } |
| }) |
| } |
| |
| logCnt := len(mockClient.log) |
| if logIdx != logCnt { |
| log.Fatalf("not all requests were read. Expected logIdx to be %d but got %d", logCnt, logIdx) |
| } |
| } |