From ac60ddcd40714684209e0495a05d29df4060dbdf Mon Sep 17 00:00:00 2001 From: James Bardin Date: Tue, 1 Aug 2017 12:11:04 -0400 Subject: [PATCH 1/7] Support named states in inmeme backend Used to expand test coverage --- backend/remote-state/inmem/backend.go | 162 +++++++++++++++++++-- backend/remote-state/inmem/backend_test.go | 63 ++++++++ backend/remote-state/inmem/client.go | 38 +---- backend/remote-state/inmem/client_test.go | 11 +- 4 files changed, 222 insertions(+), 52 deletions(-) create mode 100644 backend/remote-state/inmem/backend_test.go diff --git a/backend/remote-state/inmem/backend.go b/backend/remote-state/inmem/backend.go index effa1381c..8d434b5fb 100644 --- a/backend/remote-state/inmem/backend.go +++ b/backend/remote-state/inmem/backend.go @@ -2,40 +2,172 @@ package inmem import ( "context" + "errors" + "fmt" + "sort" + "sync" + "time" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/backend/remote-state" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state/remote" ) +// we keep the states and locks in package-level variables, so that they can be +// accessed from multiple instances of the backend. This better emulates +// backend instances accessing a single remote data store. +var states = stateMap{ + m: map[string]*remote.State{}, +} + +var locks = lockMap{ + m: map[string]*state.LockInfo{}, +} + // New creates a new backend for Inmem remote state. func New() backend.Backend { - return &remotestate.Backend{ - ConfigureFunc: configure, - - // Set the schema - Backend: &schema.Backend{ - Schema: map[string]*schema.Schema{ - "lock_id": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Description: "initializes the state in a locked configuration", - }, + // Set the schema + s := &schema.Backend{ + Schema: map[string]*schema.Schema{ + "lock_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "initializes the state in a locked configuration", }, }, } + backend := &Backend{Backend: s} + backend.Backend.ConfigureFunc = backend.configure + return backend } -func configure(ctx context.Context) (remote.Client, error) { +type Backend struct { + *schema.Backend +} + +func (b *Backend) configure(ctx context.Context) error { + states.Lock() + defer states.Unlock() + + defaultClient := &RemoteClient{ + Name: backend.DefaultStateName, + } + + states.m[backend.DefaultStateName] = &remote.State{ + Client: defaultClient, + } + + // set the default client lock info per the test config data := schema.FromContextBackendConfig(ctx) if v, ok := data.GetOk("lock_id"); ok && v.(string) != "" { info := state.NewLockInfo() info.ID = v.(string) info.Operation = "test" info.Info = "test config" - return &RemoteClient{LockInfo: info}, nil + + locks.lock(backend.DefaultStateName, info) } - return &RemoteClient{}, nil + + return nil +} + +func (b *Backend) States() ([]string, error) { + states.Lock() + defer states.Unlock() + + var workspaces []string + + for s := range states.m { + workspaces = append(workspaces, s) + } + + sort.Strings(workspaces) + return workspaces, nil +} + +func (b *Backend) DeleteState(name string) error { + states.Lock() + defer states.Unlock() + + if name == backend.DefaultStateName || name == "" { + return fmt.Errorf("can't delete default state") + } + + delete(states.m, name) + return nil +} + +func (b *Backend) State(name string) (state.State, error) { + states.Lock() + defer states.Unlock() + + s := states.m[name] + if s == nil { + s = &remote.State{ + Client: &RemoteClient{ + Name: name, + }, + } + states.m[name] = s + } + + return s, nil +} + +type stateMap struct { + sync.Mutex + m map[string]*remote.State +} + +// Global level locks for inmem backends. +type lockMap struct { + sync.Mutex + m map[string]*state.LockInfo +} + +func (l *lockMap) lock(name string, info *state.LockInfo) (string, error) { + l.Lock() + defer l.Unlock() + + lockInfo := l.m[name] + if lockInfo != nil { + lockErr := &state.LockError{ + Info: lockInfo, + } + + lockErr.Err = errors.New("state locked") + // make a copy of the lock info to avoid any testing shenanigans + *lockErr.Info = *lockInfo + return "", lockErr + } + + info.Created = time.Now().UTC() + l.m[name] = info + + return info.ID, nil +} + +func (l *lockMap) unlock(name, id string) error { + l.Lock() + defer l.Unlock() + + lockInfo := l.m[name] + + if lockInfo == nil { + return errors.New("state not locked") + } + + lockErr := &state.LockError{ + Info: &state.LockInfo{}, + } + + if id != lockInfo.ID { + lockErr.Err = errors.New("invalid lock id") + *lockErr.Info = *lockInfo + return lockErr + } + + delete(l.m, name) + return nil } diff --git a/backend/remote-state/inmem/backend_test.go b/backend/remote-state/inmem/backend_test.go new file mode 100644 index 000000000..4398591bd --- /dev/null +++ b/backend/remote-state/inmem/backend_test.go @@ -0,0 +1,63 @@ +package inmem + +import ( + "testing" + + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/state/remote" +) + +func TestBackend_impl(t *testing.T) { + var _ backend.Backend = new(Backend) +} + +// reset the states and locks between tests +func reset() { + states = stateMap{ + m: map[string]*remote.State{}, + } + + locks = lockMap{ + m: map[string]*state.LockInfo{}, + } +} + +func TestBackendConfig(t *testing.T) { + defer reset() + testID := "test_lock_id" + + config := map[string]interface{}{ + "lock_id": testID, + } + + b := backend.TestBackendConfig(t, New(), config).(*Backend) + + s, err := b.State(backend.DefaultStateName) + if err != nil { + t.Fatal(err) + } + + c := s.(*remote.State).Client.(*RemoteClient) + if c.Name != backend.DefaultStateName { + t.Fatal("client name is not configured") + } + + if err := locks.unlock(backend.DefaultStateName, testID); err != nil { + t.Fatalf("default state should have been locked: %s", err) + } +} + +func TestBackend(t *testing.T) { + defer reset() + b := backend.TestBackendConfig(t, New(), nil).(*Backend) + backend.TestBackend(t, b, nil) +} + +func TestBackendLocked(t *testing.T) { + defer reset() + b1 := backend.TestBackendConfig(t, New(), nil).(*Backend) + b2 := backend.TestBackendConfig(t, New(), nil).(*Backend) + + backend.TestBackend(t, b1, b2) +} diff --git a/backend/remote-state/inmem/client.go b/backend/remote-state/inmem/client.go index 703d4a267..51c8d7251 100644 --- a/backend/remote-state/inmem/client.go +++ b/backend/remote-state/inmem/client.go @@ -2,8 +2,6 @@ package inmem import ( "crypto/md5" - "errors" - "time" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state/remote" @@ -13,8 +11,7 @@ import ( type RemoteClient struct { Data []byte MD5 []byte - - LockInfo *state.LockInfo + Name string } func (c *RemoteClient) Get() (*remote.Payload, error) { @@ -43,37 +40,8 @@ func (c *RemoteClient) Delete() error { } func (c *RemoteClient) Lock(info *state.LockInfo) (string, error) { - lockErr := &state.LockError{ - Info: &state.LockInfo{}, - } - - if c.LockInfo != nil { - lockErr.Err = errors.New("state locked") - // make a copy of the lock info to avoid any testing shenanigans - *lockErr.Info = *c.LockInfo - return "", lockErr - } - - info.Created = time.Now().UTC() - c.LockInfo = info - - return c.LockInfo.ID, nil + return locks.lock(c.Name, info) } - func (c *RemoteClient) Unlock(id string) error { - if c.LockInfo == nil { - return errors.New("state not locked") - } - - lockErr := &state.LockError{ - Info: &state.LockInfo{}, - } - if id != c.LockInfo.ID { - lockErr.Err = errors.New("invalid lock id") - *lockErr.Info = *c.LockInfo - return lockErr - } - - c.LockInfo = nil - return nil + return locks.unlock(c.Name, id) } diff --git a/backend/remote-state/inmem/client_test.go b/backend/remote-state/inmem/client_test.go index f3de56715..c040a6e72 100644 --- a/backend/remote-state/inmem/client_test.go +++ b/backend/remote-state/inmem/client_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/hashicorp/terraform/backend" - remotestate "github.com/hashicorp/terraform/backend/remote-state" "github.com/hashicorp/terraform/state/remote" ) @@ -14,11 +13,19 @@ func TestRemoteClient_impl(t *testing.T) { } func TestRemoteClient(t *testing.T) { + defer reset() b := backend.TestBackendConfig(t, New(), nil) - remotestate.TestClient(t, b) + + s, err := b.State(backend.DefaultStateName) + if err != nil { + t.Fatal(err) + } + + remote.TestClient(t, s.(*remote.State).Client) } func TestInmemLocks(t *testing.T) { + defer reset() s, err := backend.TestBackendConfig(t, New(), nil).State(backend.DefaultStateName) if err != nil { t.Fatal(err) From 5deef9621ddd7c2fa8a9203c6fcece5608924bbd Mon Sep 17 00:00:00 2001 From: James Bardin Date: Tue, 1 Aug 2017 16:15:52 -0400 Subject: [PATCH 2/7] export Reset() This package is used for testing, so there needs to be an easy method for reinitializing the stored data between tests. --- backend/remote-state/inmem/backend.go | 22 ++++++++++++++++++---- backend/remote-state/inmem/backend_test.go | 18 +++--------------- backend/remote-state/inmem/client_test.go | 4 ++-- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/backend/remote-state/inmem/backend.go b/backend/remote-state/inmem/backend.go index 8d434b5fb..3e66d1274 100644 --- a/backend/remote-state/inmem/backend.go +++ b/backend/remote-state/inmem/backend.go @@ -17,12 +17,26 @@ import ( // we keep the states and locks in package-level variables, so that they can be // accessed from multiple instances of the backend. This better emulates // backend instances accessing a single remote data store. -var states = stateMap{ - m: map[string]*remote.State{}, +var ( + states stateMap + locks lockMap +) + +func init() { + Reset() } -var locks = lockMap{ - m: map[string]*state.LockInfo{}, +// Reset clears out all existing state and lock data. +// This is used to initialize the package during init, as well as between +// tests. +func Reset() { + states = stateMap{ + m: map[string]*remote.State{}, + } + + locks = lockMap{ + m: map[string]*state.LockInfo{}, + } } // New creates a new backend for Inmem remote state. diff --git a/backend/remote-state/inmem/backend_test.go b/backend/remote-state/inmem/backend_test.go index 4398591bd..1f4ab1138 100644 --- a/backend/remote-state/inmem/backend_test.go +++ b/backend/remote-state/inmem/backend_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state/remote" ) @@ -12,19 +11,8 @@ func TestBackend_impl(t *testing.T) { var _ backend.Backend = new(Backend) } -// reset the states and locks between tests -func reset() { - states = stateMap{ - m: map[string]*remote.State{}, - } - - locks = lockMap{ - m: map[string]*state.LockInfo{}, - } -} - func TestBackendConfig(t *testing.T) { - defer reset() + defer Reset() testID := "test_lock_id" config := map[string]interface{}{ @@ -49,13 +37,13 @@ func TestBackendConfig(t *testing.T) { } func TestBackend(t *testing.T) { - defer reset() + defer Reset() b := backend.TestBackendConfig(t, New(), nil).(*Backend) backend.TestBackend(t, b, nil) } func TestBackendLocked(t *testing.T) { - defer reset() + defer Reset() b1 := backend.TestBackendConfig(t, New(), nil).(*Backend) b2 := backend.TestBackendConfig(t, New(), nil).(*Backend) diff --git a/backend/remote-state/inmem/client_test.go b/backend/remote-state/inmem/client_test.go index c040a6e72..3a0fa8f30 100644 --- a/backend/remote-state/inmem/client_test.go +++ b/backend/remote-state/inmem/client_test.go @@ -13,7 +13,7 @@ func TestRemoteClient_impl(t *testing.T) { } func TestRemoteClient(t *testing.T) { - defer reset() + defer Reset() b := backend.TestBackendConfig(t, New(), nil) s, err := b.State(backend.DefaultStateName) @@ -25,7 +25,7 @@ func TestRemoteClient(t *testing.T) { } func TestInmemLocks(t *testing.T) { - defer reset() + defer Reset() s, err := backend.TestBackendConfig(t, New(), nil).State(backend.DefaultStateName) if err != nil { t.Fatal(err) From 18d71f273e34985053aa2a8cb390361af5979e76 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Tue, 1 Aug 2017 17:25:24 -0400 Subject: [PATCH 3/7] make inmem behave more like remote backends When remote backend imeplemtations create a new named state, they may need to acquire a lock and/or save an actual empty state to the backend. Copy this behavior in the inmem backend for testing. --- backend/remote-state/inmem/backend.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/backend/remote-state/inmem/backend.go b/backend/remote-state/inmem/backend.go index 3e66d1274..5eab8d0c6 100644 --- a/backend/remote-state/inmem/backend.go +++ b/backend/remote-state/inmem/backend.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state/remote" + "github.com/hashicorp/terraform/terraform" ) // we keep the states and locks in package-level variables, so that they can be @@ -124,6 +125,26 @@ func (b *Backend) State(name string) (state.State, error) { }, } states.m[name] = s + + // to most closely replicate other implementations, we are going to + // take a lock and create a new state if it doesn't exist. + lockInfo := state.NewLockInfo() + lockInfo.Operation = "init" + lockID, err := s.Lock(lockInfo) + if err != nil { + return nil, fmt.Errorf("failed to lock inmem state: %s", err) + } + defer s.Unlock(lockID) + + // If we have no state, we have to create an empty state + if v := s.State(); v == nil { + if err := s.WriteState(terraform.NewState()); err != nil { + return nil, err + } + if err := s.PersistState(); err != nil { + return nil, err + } + } } return s, nil From 16e8e405c77581f9bcb563f17ba7d9e303169fdc Mon Sep 17 00:00:00 2001 From: James Bardin Date: Tue, 1 Aug 2017 16:41:34 -0400 Subject: [PATCH 4/7] create failing test cases for remote lineage issue Some remote backend would fail on `-state push -force`, or `workspace new -state` because of a new strict lineage check in remote.State. --- command/state_push_test.go | 55 +++++++++++++++++++++ command/test-fixtures/inmem-backend/main.tf | 3 ++ command/unlock_test.go | 2 + command/workspace_command_test.go | 16 +++++- 4 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 command/test-fixtures/inmem-backend/main.tf diff --git a/command/state_push_test.go b/command/state_push_test.go index 2e2e8700f..bee9d4775 100644 --- a/command/state_push_test.go +++ b/command/state_push_test.go @@ -5,6 +5,8 @@ import ( "os" "testing" + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/backend/remote-state/inmem" "github.com/hashicorp/terraform/helper/copy" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" @@ -190,3 +192,56 @@ func TestStatePush_serialOlder(t *testing.T) { t.Fatalf("bad: %#v", actual) } } + +func TestStatePush_forceRemoteState(t *testing.T) { + td := tempDir(t) + copy.CopyDir(testFixturePath("inmem-backend"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + defer inmem.Reset() + + s := terraform.NewState() + statePath := testStateFile(t, s) + + // init the backend + ui := new(cli.MockUi) + initCmd := &InitCommand{ + Meta: Meta{Ui: ui}, + } + if code := initCmd.Run([]string{}); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + // create a new workspace + ui = new(cli.MockUi) + newCmd := &WorkspaceNewCommand{ + Meta: Meta{Ui: ui}, + } + if code := newCmd.Run([]string{"test"}); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) + } + + // put a dummy state in place, so we have something to force + b := backend.TestBackendConfig(t, inmem.New(), nil) + sMgr, err := b.State("test") + if err != nil { + t.Fatal(err) + } + if err := sMgr.WriteState(terraform.NewState()); err != nil { + t.Fatal(err) + } + if err := sMgr.PersistState(); err != nil { + t.Fatal(err) + } + + // push our local state to that new workspace + ui = new(cli.MockUi) + c := &StatePushCommand{ + Meta: Meta{Ui: ui}, + } + + args := []string{"-force", statePath} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } +} diff --git a/command/test-fixtures/inmem-backend/main.tf b/command/test-fixtures/inmem-backend/main.tf new file mode 100644 index 000000000..df9309a5c --- /dev/null +++ b/command/test-fixtures/inmem-backend/main.tf @@ -0,0 +1,3 @@ +terraform { + backend "inmem" {} +} diff --git a/command/unlock_test.go b/command/unlock_test.go index 342df3b67..b6dfceb47 100644 --- a/command/unlock_test.go +++ b/command/unlock_test.go @@ -4,6 +4,7 @@ import ( "os" "testing" + "github.com/hashicorp/terraform/backend/remote-state/inmem" "github.com/hashicorp/terraform/helper/copy" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" @@ -57,6 +58,7 @@ func TestUnlock_inmemBackend(t *testing.T) { copy.CopyDir(testFixturePath("backend-inmem-locked"), td) defer os.RemoveAll(td) defer testChdir(t, td)() + defer inmem.Reset() // init backend ui := new(cli.MockUi) diff --git a/command/workspace_command_test.go b/command/workspace_command_test.go index cfa261b48..7611850a0 100644 --- a/command/workspace_command_test.go +++ b/command/workspace_command_test.go @@ -9,6 +9,8 @@ import ( "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/backend/local" + "github.com/hashicorp/terraform/backend/remote-state/inmem" + "github.com/hashicorp/terraform/helper/copy" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" @@ -211,9 +213,19 @@ func TestWorkspace_createInvalid(t *testing.T) { func TestWorkspace_createWithState(t *testing.T) { td := tempDir(t) - os.MkdirAll(td, 0755) + copy.CopyDir(testFixturePath("inmem-backend"), td) defer os.RemoveAll(td) defer testChdir(t, td)() + defer inmem.Reset() + + // init the backend + ui := new(cli.MockUi) + initCmd := &InitCommand{ + Meta: Meta{Ui: ui}, + } + if code := initCmd.Run([]string{}); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } // create a non-empty state originalState := &terraform.State{ @@ -238,7 +250,7 @@ func TestWorkspace_createWithState(t *testing.T) { } args := []string{"-state", "test.tfstate", "test"} - ui := new(cli.MockUi) + ui = new(cli.MockUi) newCmd := &WorkspaceNewCommand{ Meta: Meta{Ui: ui}, } From c3e943bed2878d4c47ed4ef7eb50e05ed3690b23 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Tue, 1 Aug 2017 17:50:53 -0400 Subject: [PATCH 5/7] add another failing test for remote.State lineage Want to make sure we don't hit this again. --- backend/remote-state/inmem/backend_test.go | 39 ++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/backend/remote-state/inmem/backend_test.go b/backend/remote-state/inmem/backend_test.go index 1f4ab1138..005e66a1e 100644 --- a/backend/remote-state/inmem/backend_test.go +++ b/backend/remote-state/inmem/backend_test.go @@ -5,6 +5,7 @@ import ( "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/state/remote" + "github.com/hashicorp/terraform/terraform" ) func TestBackend_impl(t *testing.T) { @@ -49,3 +50,41 @@ func TestBackendLocked(t *testing.T) { backend.TestBackend(t, b1, b2) } + +// use the this backen to test the remote.State implementation +func TestRemoteState(t *testing.T) { + defer Reset() + b := backend.TestBackendConfig(t, New(), nil) + + workspace := "workspace" + + // create a new workspace in this backend + s, err := b.State(workspace) + if err != nil { + t.Fatal(err) + } + + // force overwriting the remote state + newState := terraform.NewState() + + if err := s.WriteState(newState); err != nil { + t.Fatal(err) + } + + if err := s.PersistState(); err != nil { + t.Fatal(err) + } + + if err := s.RefreshState(); err != nil { + t.Fatal(err) + } + + savedState := s.State() + if err != nil { + t.Fatal(err) + } + + if savedState.Lineage != newState.Lineage { + t.Fatal("saved state has incorrect lineage") + } +} From 32ae05c3427159a9905fcd7851b33856ec98e1db Mon Sep 17 00:00:00 2001 From: James Bardin Date: Tue, 1 Aug 2017 17:51:35 -0400 Subject: [PATCH 6/7] fix strict remote.State lineage check We can't check lineage in the remote state instance, because we may need to overwrite a state with a new lineage. Whil it's tempting to add an optional interface for this, like OverwriteState(), optional interfaces are never _really_ optional, and will have to be implemented by any wrapper types as well. Another solution may be to add a State.Supersedes field to indicate that we intend to replace an existing state, but that may not be worth the extra check either. --- state/remote/state.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/state/remote/state.go b/state/remote/state.go index 8e157101d..575e4d187 100644 --- a/state/remote/state.go +++ b/state/remote/state.go @@ -2,7 +2,7 @@ package remote import ( "bytes" - "fmt" + "log" "sync" "github.com/hashicorp/terraform/state" @@ -35,7 +35,10 @@ func (s *State) WriteState(state *terraform.State) error { defer s.mu.Unlock() if s.readState != nil && !state.SameLineage(s.readState) { - return fmt.Errorf("incompatible state lineage; given %s but want %s", state.Lineage, s.readState.Lineage) + // This can't error here, because we need to be able to overwrite the + // state in some cases, like `state push -force` or `workspace new + // -state=` + log.Printf("[WARN] incompatible state lineage; given %s but want %s", state.Lineage, s.readState.Lineage) } // We create a deep copy of the state here, because the caller also has From 07b0101fb5203311165862c03c704acf839f7718 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Tue, 1 Aug 2017 18:13:04 -0400 Subject: [PATCH 7/7] update workspace new test for inmem backend The existing test assumed local state files. --- command/workspace_command_test.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/command/workspace_command_test.go b/command/workspace_command_test.go index 7611850a0..7baabbed5 100644 --- a/command/workspace_command_test.go +++ b/command/workspace_command_test.go @@ -249,7 +249,9 @@ func TestWorkspace_createWithState(t *testing.T) { t.Fatal(err) } - args := []string{"-state", "test.tfstate", "test"} + workspace := "test_workspace" + + args := []string{"-state", "test.tfstate", workspace} ui = new(cli.MockUi) newCmd := &WorkspaceNewCommand{ Meta: Meta{Ui: ui}, @@ -265,7 +267,14 @@ func TestWorkspace_createWithState(t *testing.T) { t.Fatal(err) } - newState := envState.State() + b := backend.TestBackendConfig(t, inmem.New(), nil) + sMgr, err := b.State(workspace) + if err != nil { + t.Fatal(err) + } + + newState := sMgr.State() + originalState.Version = newState.Version // the round-trip through the state manager implicitly populates version if !originalState.Equal(newState) { t.Fatalf("states not equal\norig: %s\nnew: %s", originalState, newState)