| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: BUSL-1.1 |
| |
| package local |
| |
| import ( |
| "errors" |
| "fmt" |
| "os" |
| "path/filepath" |
| "reflect" |
| "strings" |
| "testing" |
| |
| "github.com/hashicorp/terraform/internal/backend" |
| "github.com/hashicorp/terraform/internal/backend/backendrun" |
| "github.com/hashicorp/terraform/internal/configs/configschema" |
| "github.com/hashicorp/terraform/internal/states" |
| "github.com/hashicorp/terraform/internal/states/statefile" |
| "github.com/hashicorp/terraform/internal/states/statemgr" |
| "github.com/hashicorp/terraform/internal/tfdiags" |
| "github.com/zclconf/go-cty/cty" |
| ) |
| |
| func TestLocal_impl(t *testing.T) { |
| var _ backendrun.OperationsBackend = New() |
| var _ backendrun.Local = New() |
| var _ backendrun.CLI = New() |
| } |
| |
| func TestLocal_backend(t *testing.T) { |
| _ = testTmpDir(t) |
| b := New() |
| backend.TestBackendStates(t, b) |
| backend.TestBackendStateLocks(t, b, b) |
| } |
| |
| func TestLocal_PrepareConfig(t *testing.T) { |
| // Setup |
| _ = testTmpDir(t) |
| |
| b := New() |
| |
| // PATH ATTR |
| // Empty string path attribute isn't valid |
| config := cty.ObjectVal(map[string]cty.Value{ |
| "path": cty.StringVal(""), |
| "workspace_dir": cty.NullVal(cty.String), |
| }) |
| _, diags := b.PrepareConfig(config) |
| if !diags.HasErrors() { |
| t.Fatalf("expected an error from PrepareConfig but got none") |
| } |
| expectedErr := `The "path" attribute value must not be empty` |
| if !strings.Contains(diags.Err().Error(), expectedErr) { |
| t.Fatalf("expected an error containing %q, got: %q", expectedErr, diags.Err()) |
| } |
| |
| // PrepareConfig doesn't enforce the path value has .tfstate extension |
| config = cty.ObjectVal(map[string]cty.Value{ |
| "path": cty.StringVal("path/to/state/my-state.docx"), |
| "workspace_dir": cty.NullVal(cty.String), |
| }) |
| _, diags = b.PrepareConfig(config) |
| if diags.HasErrors() { |
| t.Fatalf("unexpected error returned from PrepareConfig") |
| } |
| |
| // WORKSPACE_DIR ATTR |
| // Empty string workspace_dir attribute isn't valid |
| config = cty.ObjectVal(map[string]cty.Value{ |
| "path": cty.NullVal(cty.String), |
| "workspace_dir": cty.StringVal(""), |
| }) |
| _, diags = b.PrepareConfig(config) |
| if !diags.HasErrors() { |
| t.Fatalf("expected an error from PrepareConfig but got none") |
| } |
| expectedErr = `The "workspace_dir" attribute value must not be empty` |
| if !strings.Contains(diags.Err().Error(), expectedErr) { |
| t.Fatalf("expected an error containing %q, got: %q", expectedErr, diags.Err()) |
| } |
| |
| // Existence of directory isn't checked during PrepareConfig |
| // (Non-existent directories are created as a side-effect of WriteState) |
| config = cty.ObjectVal(map[string]cty.Value{ |
| "path": cty.NullVal(cty.String), |
| "workspace_dir": cty.StringVal("this/does/not/exist"), |
| }) |
| _, diags = b.PrepareConfig(config) |
| if diags.HasErrors() { |
| t.Fatalf("unexpected error returned from PrepareConfig") |
| } |
| } |
| |
| // The `path` attribute should only affect the default workspace's state |
| // file location and name. |
| // |
| // Non-default workspaces' states names and locations are unaffected. |
| func TestLocal_useOfPathAttribute(t *testing.T) { |
| // Setup |
| td := testTmpDir(t) |
| |
| b := New() |
| |
| // Configure local state-storage backend (skip call to PrepareConfig) |
| path := "path/to/foobar.tfstate" |
| config := cty.ObjectVal(map[string]cty.Value{ |
| "path": cty.StringVal(path), // Set |
| "workspace_dir": cty.NullVal(cty.String), |
| }) |
| diags := b.Configure(config) |
| if diags.HasErrors() { |
| t.Fatalf("unexpected error returned from Configure") |
| } |
| |
| // State file at the `path` location doesn't exist yet |
| workspace := backend.DefaultStateName |
| stmgr, err := b.StateMgr(workspace) |
| if err != nil { |
| t.Fatalf("unexpected error returned from StateMgr") |
| } |
| defaultStatePath := fmt.Sprintf("%s/%s", td, path) |
| if _, err := os.Stat(defaultStatePath); !strings.Contains(err.Error(), "no such file or directory") { |
| if err != nil { |
| t.Fatalf("expected \"no such file or directory\" error when accessing file %q, got: %s", path, err) |
| } |
| t.Fatalf("expected the state file %q to not exist, but it did", path) |
| } |
| |
| // Writing to the default workspace's state creates a file |
| // at the `path` location. |
| // Directories are created to enable the path. |
| s := states.NewState() |
| s.RootOutputValues = map[string]*states.OutputValue{ |
| "foobar": { |
| Value: cty.StringVal("foobar"), |
| }, |
| } |
| err = stmgr.WriteState(s) |
| if err != nil { |
| t.Fatalf("unexpected error returned from WriteState") |
| } |
| _, err = os.Stat(defaultStatePath) |
| if err != nil { |
| // The file should exist post-WriteState |
| t.Fatalf("unexpected error when getting stats on the state file %q", path) |
| } |
| |
| // Writing to a non-default workspace's state creates a file |
| // that's unaffected by the `path` location |
| workspace = "fizzbuzz" |
| stmgr, err = b.StateMgr(workspace) |
| if err != nil { |
| t.Fatalf("unexpected error returned from StateMgr") |
| } |
| fizzbuzzStatePath := fmt.Sprintf("%s/terraform.tfstate.d/%s/terraform.tfstate", td, workspace) |
| err = stmgr.WriteState(s) |
| if err != nil { |
| t.Fatalf("unexpected error returned from WriteState") |
| } |
| |
| // The file should exist post-WriteState |
| checkState(t, fizzbuzzStatePath, s.String()) |
| } |
| |
| // Using non-tfstate file extensions in the value of the `path` attribute |
| // doesn't affect writing to state |
| func TestLocal_pathAttributeWrongExtension(t *testing.T) { |
| // Setup |
| td := testTmpDir(t) |
| |
| b := New() |
| |
| // The path value doesn't have the expected .tfstate file extension |
| path := "foobar.docx" |
| fullPath := fmt.Sprintf("%s/%s", td, path) |
| config := cty.ObjectVal(map[string]cty.Value{ |
| "path": cty.StringVal(path), // Set |
| "workspace_dir": cty.NullVal(cty.String), |
| }) |
| diags := b.Configure(config) |
| if diags.HasErrors() { |
| t.Fatalf("unexpected error returned from Configure") |
| } |
| |
| // Writing to the default workspace's state creates a file |
| workspace := backend.DefaultStateName |
| stmgr, err := b.StateMgr(workspace) |
| if err != nil { |
| t.Fatalf("unexpected error returned from StateMgr") |
| } |
| s := states.NewState() |
| s.RootOutputValues = map[string]*states.OutputValue{ |
| "foobar": { |
| Value: cty.StringVal("foobar"), |
| }, |
| } |
| err = stmgr.WriteState(s) |
| if err != nil { |
| t.Fatalf("unexpected error returned from WriteState") |
| } |
| |
| // The file should exist post-WriteState, despite the odd file extension, |
| // be readable, and contain the correct state |
| checkState(t, fullPath, s.String()) |
| } |
| |
| // The `workspace_dir` attribute should only affect where non-default workspaces' |
| // state files are saved. |
| // |
| // The default workspace's name and location are unaffected by this attribute. |
| func TestLocal_useOfWorkspaceDirAttribute(t *testing.T) { |
| // Setup |
| td := testTmpDir(t) |
| |
| b := New() |
| |
| // Configure local state-storage backend (skip call to PrepareConfig) |
| workspaceDir := "path/to/workspaces" |
| config := cty.ObjectVal(map[string]cty.Value{ |
| "path": cty.NullVal(cty.String), |
| "workspace_dir": cty.StringVal(workspaceDir), // set |
| }) |
| diags := b.Configure(config) |
| if diags.HasErrors() { |
| t.Fatalf("unexpected error returned from Configure") |
| } |
| |
| // Writing to the default workspace's state creates a file. |
| // As path attribute was left null, the default location |
| // ./terraform.tfstate is used. |
| // Unaffected by the `workspace_dir` location. |
| workspace := backend.DefaultStateName |
| defaultStatePath := fmt.Sprintf("%s/terraform.tfstate", td) |
| stmgr, err := b.StateMgr(workspace) |
| if err != nil { |
| t.Fatalf("unexpected error returned from StateMgr") |
| } |
| s := states.NewState() |
| s.RootOutputValues = map[string]*states.OutputValue{ |
| "foobar": { |
| Value: cty.StringVal("foobar"), |
| }, |
| } |
| err = stmgr.WriteState(s) |
| if err != nil { |
| t.Fatalf("unexpected error returned from WriteState") |
| } |
| // Assert state |
| checkState(t, defaultStatePath, s.String()) |
| |
| // Writing to a non-default workspace's state creates a file |
| // that's affected by the `workspace_dir` location |
| workspace = "fizzbuzz" |
| fizzbuzzStatePath := fmt.Sprintf("%s/%s/%s/terraform.tfstate", td, workspaceDir, workspace) |
| stmgr, err = b.StateMgr(workspace) |
| if err != nil { |
| t.Fatalf("unexpected error returned from StateMgr") |
| } |
| err = stmgr.WriteState(s) |
| if err != nil { |
| t.Fatalf("unexpected error returned from WriteState") |
| } |
| // Assert state |
| checkState(t, fizzbuzzStatePath, s.String()) |
| } |
| |
| // When using the local state storage you cannot delete the default workspace's state |
| func TestLocal_cannotDeleteDefaultState(t *testing.T) { |
| // Setup |
| _ = testTmpDir(t) |
| dflt := backend.DefaultStateName |
| expectedStates := []string{dflt} |
| |
| b := New() |
| |
| // Only default workspace exists initially. |
| states, err := b.Workspaces() |
| if err != nil { |
| t.Fatal(err) |
| } |
| if !reflect.DeepEqual(states, expectedStates) { |
| t.Fatalf("expected []string{%q}, got %q", dflt, states) |
| } |
| |
| // Attempt to delete default state - force=false |
| err = b.DeleteWorkspace(dflt, false) |
| if err == nil { |
| t.Fatal("expected error but there was none") |
| } |
| expectedErr := "cannot delete default state" |
| if err.Error() != expectedErr { |
| t.Fatalf("expected error %q, got: %q", expectedErr, err) |
| } |
| |
| // Setting force=true doesn't change outcome |
| err = b.DeleteWorkspace(dflt, true) |
| if err == nil { |
| t.Fatal("expected error but there was none") |
| } |
| if err.Error() != expectedErr { |
| t.Fatalf("expected error %q, got: %q", expectedErr, err) |
| } |
| } |
| |
| func TestLocal_addAndRemoveStates(t *testing.T) { |
| // Setup |
| _ = testTmpDir(t) |
| dflt := backend.DefaultStateName |
| expectedStates := []string{dflt} |
| |
| b := New() |
| |
| // Only the default workspace exists initially. |
| states, err := b.Workspaces() |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| if !reflect.DeepEqual(states, expectedStates) { |
| t.Fatalf("expected []string{%q}, got %q", dflt, states) |
| } |
| |
| // Calling StateMgr with a new workspace name creates that workspace's state file. |
| expectedA := "test_A" |
| if _, err := b.StateMgr(expectedA); err != nil { |
| t.Fatal(err) |
| } |
| |
| states, err = b.Workspaces() |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| expectedStates = append(expectedStates, expectedA) |
| if !reflect.DeepEqual(states, expectedStates) { |
| t.Fatalf("expected %q, got %q", expectedStates, states) |
| } |
| |
| // Creating another workspace appends it to the list of present workspaces. |
| expectedB := "test_B" |
| if _, err := b.StateMgr(expectedB); err != nil { |
| t.Fatal(err) |
| } |
| |
| states, err = b.Workspaces() |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| expectedStates = append(expectedStates, expectedB) |
| if !reflect.DeepEqual(states, expectedStates) { |
| t.Fatalf("expected %q, got %q", expectedStates, states) |
| } |
| |
| // Can delete a given workspace |
| if err := b.DeleteWorkspace(expectedA, true); err != nil { |
| t.Fatal(err) |
| } |
| |
| states, err = b.Workspaces() |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| expectedStates = []string{dflt, expectedB} |
| if !reflect.DeepEqual(states, expectedStates) { |
| t.Fatalf("expected %q, got %q", expectedStates, states) |
| } |
| |
| // Can delete another workspace |
| if err := b.DeleteWorkspace(expectedB, true); err != nil { |
| t.Fatal(err) |
| } |
| |
| states, err = b.Workspaces() |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| expectedStates = []string{dflt} |
| if !reflect.DeepEqual(states, expectedStates) { |
| t.Fatalf("expected %q, got %q", expectedStates, states) |
| } |
| |
| // You cannot delete the default workspace |
| if err := b.DeleteWorkspace(dflt, true); err == nil { |
| t.Fatal("expected error deleting default state") |
| } |
| } |
| |
| func TestLocal_StatePaths_defaultWorkspace(t *testing.T) { |
| |
| // Default paths are returned for the default workspace |
| // when nothing is set via config or overrides |
| b := New() |
| path, out, back := b.StatePaths("") |
| |
| if path != DefaultStateFilename { |
| t.Fatalf("expected %q, got %q", DefaultStateFilename, path) |
| } |
| |
| if out != DefaultStateFilename { |
| t.Fatalf("expected %q, got %q", DefaultStateFilename, out) |
| } |
| |
| dfltBackup := DefaultStateFilename + DefaultBackupExtension |
| if back != dfltBackup { |
| t.Fatalf("expected %q, got %q", dfltBackup, back) |
| } |
| |
| // If `path` is set in the config, this impacts returned paths for the default workspace |
| b = New() |
| configPath := "new-path.tfstate" |
| b.StatePath = configPath // equivalent of path = "new-path.tfstate" in config |
| b.StateOutPath = configPath // equivalent of path = "new-path.tfstate" in config |
| |
| path, out, back = b.StatePaths("") |
| |
| if path != configPath { |
| t.Fatalf("expected %q, got %q", configPath, path) |
| } |
| |
| if out != configPath { |
| t.Fatalf("expected %q, got %q", configPath, out) |
| } |
| |
| altBackup := configPath + DefaultBackupExtension |
| if back != altBackup { |
| t.Fatalf("expected %q, got %q", altBackup, back) |
| } |
| |
| // If overrides are set, they override default values or those from config |
| b = New() |
| b.StatePath = configPath // equivalent of path = "new-path.tfstate" in config |
| b.StateOutPath = configPath // equivalent of path = "new-path.tfstate" in config |
| override := "override.tfstate" |
| b.OverrideStatePath = override |
| b.OverrideStateOutPath = override |
| b.OverrideStateBackupPath = override |
| |
| path, out, back = b.StatePaths("") |
| |
| if path != override { |
| t.Fatalf("expected %q, got %q", override, path) |
| } |
| |
| if out != override { |
| t.Fatalf("expected %q, got %q", override, out) |
| } |
| |
| if back != override { |
| t.Fatalf("expected %q, got %q", override, back) |
| } |
| } |
| |
| func TestLocal_StatePaths_nonDefaultWorkspace(t *testing.T) { |
| |
| // Default paths are returned for a custom workspace |
| // when nothing is set via config or overrides |
| b := New() |
| workspace := "test_env" |
| path, out, back := b.StatePaths(workspace) |
| |
| expectedPath := filepath.Join(DefaultWorkspaceDir, workspace, DefaultStateFilename) |
| expectedOut := expectedPath |
| expectedBackup := expectedPath + DefaultBackupExtension |
| |
| if path != expectedPath { |
| t.Fatalf("expected %q, got %q", expectedPath, path) |
| } |
| |
| if out != expectedOut { |
| t.Fatalf("expected %q, got %q", expectedOut, out) |
| } |
| |
| if back != expectedBackup { |
| t.Fatalf("expected %q, got %q", expectedBackup, back) |
| } |
| |
| // This is unaffected by a user setting the path attribute |
| b = New() |
| b.StatePath = "path-from-config.tfstate" // equivalent of setting path = "path-from-config.tfstate" in config |
| b.StateOutPath = "path-from-config.tfstate" |
| |
| path, out, back = b.StatePaths(workspace) |
| |
| if path != expectedPath { |
| t.Fatalf("expected %q, got %q", expectedPath, path) |
| } |
| |
| if out != expectedOut { |
| t.Fatalf("expected %q, got %q", expectedOut, out) |
| } |
| |
| if back != expectedBackup { |
| t.Fatalf("expected %q, got %q", expectedBackup, back) |
| } |
| |
| // If a user set working_dir in config it affects returned values |
| b = New() |
| workingDir := "my/alternative/state/dir" |
| b.StateWorkspaceDir = workingDir // equivalent of setting working_dir = "my/alternative/state/dir" in config |
| |
| path, out, back = b.StatePaths(workspace) |
| |
| expectedPath = filepath.Join(workingDir, workspace, DefaultStateFilename) |
| expectedOut = filepath.Join(workingDir, workspace, DefaultStateFilename) |
| expectedBackup = filepath.Join(workingDir, workspace, DefaultStateFilename) + DefaultBackupExtension |
| |
| if path != expectedPath { |
| t.Fatalf("expected %q, got %q", expectedPath, path) |
| } |
| |
| if out != expectedOut { |
| t.Fatalf("expected %q, got %q", expectedOut, out) |
| } |
| |
| if back != expectedBackup { |
| t.Fatalf("expected %q, got %q", expectedBackup, back) |
| } |
| |
| // Overrides affect returned values regardless of config |
| b = New() |
| b.StateWorkspaceDir = workingDir // equivalent of setting working_dir = "my/alternative/state/dir" in config |
| override := "override.tfstate" |
| b.OverrideStatePath = override |
| b.OverrideStateOutPath = override |
| b.OverrideStateBackupPath = override |
| |
| path, out, back = b.StatePaths(workspace) |
| |
| if path != override { |
| t.Fatalf("expected %q, got %q", override, path) |
| } |
| |
| if out != override { |
| t.Fatalf("expected %q, got %q", override, out) |
| } |
| |
| if back != override { |
| t.Fatalf("expected %q, got %q", override, back) |
| } |
| } |
| |
| // TestLocal_PathsConflictWith does not include testing the effects of CLI commands -state, -state-out, and -state-backup |
| // because PathsConflictWith is only used during state migrations, and the init command does not accept those flags. |
| // Those flags would cause the local backend struct to have override fields set. |
| func TestLocal_PathsConflictWith(t *testing.T) { |
| // Create a working directory with default and non-default workspace states |
| td := testTmpDir(t) |
| exampleState := states.NewState() |
| exampleState.RootOutputValues = map[string]*states.OutputValue{ |
| "foobar": { |
| Value: cty.StringVal("foobar"), |
| }, |
| } |
| foobar := "foobar" |
| originalBackend := New() |
| |
| // Create a default workspace state file in a non-root directory |
| originalBackend.StatePath = "foobar/terraform.tfstate" |
| defaultStatePath := filepath.Join(td, originalBackend.StatePath) |
| stmgrDefault, _ := originalBackend.StateMgr("") |
| err := stmgrDefault.WriteState(exampleState) |
| if err != nil { |
| t.Fatalf("unexpected error returned from WriteState") |
| } |
| checkState(t, defaultStatePath, exampleState.String()) |
| |
| // Create a non-default workspace and state file there |
| stmgrFoobar, _ := originalBackend.StateMgr(foobar) |
| err = stmgrFoobar.WriteState(exampleState) |
| if err != nil { |
| t.Fatalf("unexpected error returned from WriteState") |
| } |
| foobarStatePath := filepath.Join(td, DefaultWorkspaceDir, foobar, DefaultStateFilename) |
| checkState(t, foobarStatePath, exampleState.String()) |
| |
| // Scenario where: |
| // * original backend has state for a 'foobar' workspace at terraform.tfstate.d/foobar/terraform.tfstate |
| // * new local backend is configured via `path` to store 'default' state at terraform.tfstate.d/foobar/terraform.tfstate |
| scenario1 := New() |
| scenario1.StatePath = foobarStatePath |
| |
| if !originalBackend.PathsConflictWith(scenario1) { |
| t.Fatal("expected conflict but got none") |
| } |
| |
| // Scenario where: |
| // * original backend has state for the default workspace at ./foobar/terrform.tfstate |
| // * local backend is configured to store non-default workspace state in the root dir |
| // this means a foobar workspace would also store state at ./foobar/terrform.tfstate |
| scenario2 := New() |
| scenario2.StateWorkspaceDir = "." |
| |
| if !originalBackend.PathsConflictWith(scenario2) { |
| t.Fatal("expected conflict but got none") |
| } |
| } |
| |
| // a local backend which returns errors for methods to |
| // verify it's being called. |
| type testDelegateBackend struct { |
| *Local |
| } |
| |
| var errTestDelegatePrepareConfig = errors.New("prepare config called") |
| var errTestDelegateConfigure = errors.New("configure called") |
| var errTestDelegateState = errors.New("state called") |
| var errTestDelegateStates = errors.New("states called") |
| var errTestDelegateDeleteState = errors.New("delete called") |
| |
| func (b *testDelegateBackend) ConfigSchema() *configschema.Block { |
| return nil |
| } |
| |
| func (b *testDelegateBackend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) { |
| var diags tfdiags.Diagnostics |
| return cty.NilVal, diags.Append(errTestDelegatePrepareConfig) |
| } |
| |
| func (b *testDelegateBackend) Configure(obj cty.Value) tfdiags.Diagnostics { |
| var diags tfdiags.Diagnostics |
| return diags.Append(errTestDelegateConfigure) |
| } |
| |
| func (b *testDelegateBackend) StateMgr(name string) (statemgr.Full, error) { |
| return nil, errTestDelegateState |
| } |
| |
| func (b *testDelegateBackend) Workspaces() ([]string, error) { |
| return nil, errTestDelegateStates |
| } |
| |
| func (b *testDelegateBackend) DeleteWorkspace(name string, force bool) error { |
| return errTestDelegateDeleteState |
| } |
| |
| // Verify that all backend.Backend methods are dispatched to the correct Backend when |
| // the local backend created with a separate state storage backend. |
| // |
| // The Local struct type implements both backendrun.OperationsBackend and backend.Backend interfaces. |
| // If the Local struct is not created with a separate state storage backend then it'll use its own |
| // backend.Backend method implementations. If a separate state storage backend IS supplied, then |
| // it should pass those method calls through to the separate backend.Backend. |
| func TestLocal_callsMethodsOnStateBackend(t *testing.T) { |
| // assign a separate backend where we can read the state |
| b := NewWithBackend(&testDelegateBackend{}) |
| |
| if schema := b.ConfigSchema(); schema != nil { |
| t.Fatal("expected a nil schema, got:", schema) |
| } |
| |
| if _, diags := b.PrepareConfig(cty.NilVal); !diags.HasErrors() { |
| t.Fatal("expected errTestDelegatePrepareConfig error, got:", diags) |
| } |
| |
| if diags := b.Configure(cty.NilVal); !diags.HasErrors() { |
| t.Fatal("expected errTestDelegateConfigure error, got:", diags) |
| } |
| |
| if _, err := b.StateMgr("test"); err != errTestDelegateState { |
| t.Fatal("expected errTestDelegateState, got:", err) |
| } |
| |
| if _, err := b.Workspaces(); err != errTestDelegateStates { |
| t.Fatal("expected errTestDelegateStates, got:", err) |
| } |
| |
| if err := b.DeleteWorkspace("test", true); err != errTestDelegateDeleteState { |
| t.Fatal("expected errTestDelegateDeleteState, got:", err) |
| } |
| } |
| |
| // testTmpDir changes into a tmp dir and change back automatically when the test |
| // and all its subtests complete. |
| func testTmpDir(t *testing.T) string { |
| tmp := t.TempDir() |
| |
| old, err := os.Getwd() |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| if err := os.Chdir(tmp); err != nil { |
| t.Fatal(err) |
| } |
| |
| t.Cleanup(func() { |
| // ignore errors and try to clean up |
| os.Chdir(old) |
| }) |
| |
| return tmp |
| } |
| |
| func checkState(t *testing.T, path, expected string) { |
| t.Helper() |
| // Read the state |
| f, err := os.Open(path) |
| if err != nil { |
| t.Fatalf("err: %s", err) |
| } |
| |
| state, err := statefile.Read(f) |
| f.Close() |
| if err != nil { |
| t.Fatalf("err: %s", err) |
| } |
| |
| actual := state.State.String() |
| expected = strings.TrimSpace(expected) |
| if actual != expected { |
| t.Fatalf("state does not match! actual:\n%s\n\nexpected:\n%s", actual, expected) |
| } |
| } |