github.com/opentofu/opentofu@v1.7.1/internal/command/apply_destroy_test.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package command
     7  
     8  import (
     9  	"os"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/davecgh/go-spew/spew"
    14  	"github.com/mitchellh/cli"
    15  	"github.com/zclconf/go-cty/cty"
    16  
    17  	"github.com/opentofu/opentofu/internal/addrs"
    18  	"github.com/opentofu/opentofu/internal/configs/configschema"
    19  	"github.com/opentofu/opentofu/internal/encryption"
    20  	"github.com/opentofu/opentofu/internal/providers"
    21  	"github.com/opentofu/opentofu/internal/states"
    22  	"github.com/opentofu/opentofu/internal/states/statefile"
    23  )
    24  
    25  func TestApply_destroy(t *testing.T) {
    26  	// Create a temporary working directory that is empty
    27  	td := t.TempDir()
    28  	testCopyDir(t, testFixturePath("apply"), td)
    29  	defer testChdir(t, td)()
    30  
    31  	originalState := states.BuildState(func(s *states.SyncState) {
    32  		s.SetResourceInstanceCurrent(
    33  			addrs.Resource{
    34  				Mode: addrs.ManagedResourceMode,
    35  				Type: "test_instance",
    36  				Name: "foo",
    37  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
    38  			&states.ResourceInstanceObjectSrc{
    39  				AttrsJSON: []byte(`{"id":"bar"}`),
    40  				Status:    states.ObjectReady,
    41  			},
    42  			addrs.AbsProviderConfig{
    43  				Provider: addrs.NewDefaultProvider("test"),
    44  				Module:   addrs.RootModule,
    45  			},
    46  		)
    47  	})
    48  	statePath := testStateFile(t, originalState)
    49  
    50  	p := testProvider()
    51  	p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
    52  		ResourceTypes: map[string]providers.Schema{
    53  			"test_instance": {
    54  				Block: &configschema.Block{
    55  					Attributes: map[string]*configschema.Attribute{
    56  						"id":  {Type: cty.String, Computed: true},
    57  						"ami": {Type: cty.String, Optional: true},
    58  					},
    59  				},
    60  			},
    61  		},
    62  	}
    63  
    64  	view, done := testView(t)
    65  	c := &ApplyCommand{
    66  		Destroy: true,
    67  		Meta: Meta{
    68  			testingOverrides: metaOverridesForProvider(p),
    69  			View:             view,
    70  		},
    71  	}
    72  
    73  	// Run the apply command pointing to our existing state
    74  	args := []string{
    75  		"-auto-approve",
    76  		"-state", statePath,
    77  	}
    78  	code := c.Run(args)
    79  	output := done(t)
    80  	if code != 0 {
    81  		t.Log(output.Stdout())
    82  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
    83  	}
    84  
    85  	// Verify a new state exists
    86  	if _, err := os.Stat(statePath); err != nil {
    87  		t.Fatalf("err: %s", err)
    88  	}
    89  
    90  	f, err := os.Open(statePath)
    91  	if err != nil {
    92  		t.Fatalf("err: %s", err)
    93  	}
    94  	defer f.Close()
    95  
    96  	stateFile, err := statefile.Read(f, encryption.StateEncryptionDisabled())
    97  	if err != nil {
    98  		t.Fatalf("err: %s", err)
    99  	}
   100  	if stateFile.State == nil {
   101  		t.Fatal("state should not be nil")
   102  	}
   103  
   104  	actualStr := strings.TrimSpace(stateFile.State.String())
   105  	expectedStr := strings.TrimSpace(testApplyDestroyStr)
   106  	if actualStr != expectedStr {
   107  		t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr)
   108  	}
   109  
   110  	// Should have a backup file
   111  	f, err = os.Open(statePath + DefaultBackupExtension)
   112  	if err != nil {
   113  		t.Fatalf("err: %s", err)
   114  	}
   115  
   116  	backupStateFile, err := statefile.Read(f, encryption.StateEncryptionDisabled())
   117  	f.Close()
   118  	if err != nil {
   119  		t.Fatalf("err: %s", err)
   120  	}
   121  
   122  	actualStr = strings.TrimSpace(backupStateFile.State.String())
   123  	expectedStr = strings.TrimSpace(originalState.String())
   124  	if actualStr != expectedStr {
   125  		t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr)
   126  	}
   127  }
   128  
   129  func TestApply_destroyApproveNo(t *testing.T) {
   130  	// Create a temporary working directory that is empty
   131  	td := t.TempDir()
   132  	testCopyDir(t, testFixturePath("apply"), td)
   133  	defer testChdir(t, td)()
   134  
   135  	// Create some existing state
   136  	originalState := states.BuildState(func(s *states.SyncState) {
   137  		s.SetResourceInstanceCurrent(
   138  			addrs.Resource{
   139  				Mode: addrs.ManagedResourceMode,
   140  				Type: "test_instance",
   141  				Name: "foo",
   142  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   143  			&states.ResourceInstanceObjectSrc{
   144  				AttrsJSON: []byte(`{"id":"bar"}`),
   145  				Status:    states.ObjectReady,
   146  			},
   147  			addrs.AbsProviderConfig{
   148  				Provider: addrs.NewDefaultProvider("test"),
   149  				Module:   addrs.RootModule,
   150  			},
   151  		)
   152  	})
   153  	statePath := testStateFile(t, originalState)
   154  
   155  	p := applyFixtureProvider()
   156  
   157  	defer testInputMap(t, map[string]string{
   158  		"approve": "no",
   159  	})()
   160  
   161  	// Do not use the NewMockUi initializer here, as we want to delay
   162  	// the call to init until after setting up the input mocks
   163  	ui := new(cli.MockUi)
   164  	view, done := testView(t)
   165  	c := &ApplyCommand{
   166  		Destroy: true,
   167  		Meta: Meta{
   168  			testingOverrides: metaOverridesForProvider(p),
   169  			Ui:               ui,
   170  			View:             view,
   171  		},
   172  	}
   173  
   174  	args := []string{
   175  		"-state", statePath,
   176  	}
   177  	code := c.Run(args)
   178  	output := done(t)
   179  	if code != 1 {
   180  		t.Fatalf("bad: %d\n\n%s", code, output.Stdout())
   181  	}
   182  	if got, want := output.Stdout(), "Destroy cancelled"; !strings.Contains(got, want) {
   183  		t.Fatalf("expected output to include %q, but was:\n%s", want, got)
   184  	}
   185  
   186  	state := testStateRead(t, statePath)
   187  	if state == nil {
   188  		t.Fatal("state should not be nil")
   189  	}
   190  	actualStr := strings.TrimSpace(state.String())
   191  	expectedStr := strings.TrimSpace(originalState.String())
   192  	if actualStr != expectedStr {
   193  		t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr)
   194  	}
   195  }
   196  
   197  func TestApply_destroyApproveYes(t *testing.T) {
   198  	// Create a temporary working directory that is empty
   199  	td := t.TempDir()
   200  	testCopyDir(t, testFixturePath("apply"), td)
   201  	defer testChdir(t, td)()
   202  
   203  	// Create some existing state
   204  	originalState := states.BuildState(func(s *states.SyncState) {
   205  		s.SetResourceInstanceCurrent(
   206  			addrs.Resource{
   207  				Mode: addrs.ManagedResourceMode,
   208  				Type: "test_instance",
   209  				Name: "foo",
   210  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   211  			&states.ResourceInstanceObjectSrc{
   212  				AttrsJSON: []byte(`{"id":"bar"}`),
   213  				Status:    states.ObjectReady,
   214  			},
   215  			addrs.AbsProviderConfig{
   216  				Provider: addrs.NewDefaultProvider("test"),
   217  				Module:   addrs.RootModule,
   218  			},
   219  		)
   220  	})
   221  	statePath := testStateFile(t, originalState)
   222  
   223  	p := applyFixtureProvider()
   224  
   225  	defer testInputMap(t, map[string]string{
   226  		"approve": "yes",
   227  	})()
   228  
   229  	// Do not use the NewMockUi initializer here, as we want to delay
   230  	// the call to init until after setting up the input mocks
   231  	ui := new(cli.MockUi)
   232  	view, done := testView(t)
   233  	c := &ApplyCommand{
   234  		Destroy: true,
   235  		Meta: Meta{
   236  			testingOverrides: metaOverridesForProvider(p),
   237  			Ui:               ui,
   238  			View:             view,
   239  		},
   240  	}
   241  
   242  	args := []string{
   243  		"-state", statePath,
   244  	}
   245  	code := c.Run(args)
   246  	output := done(t)
   247  	if code != 0 {
   248  		t.Log(output.Stdout())
   249  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   250  	}
   251  
   252  	if _, err := os.Stat(statePath); err != nil {
   253  		t.Fatalf("err: %s", err)
   254  	}
   255  
   256  	state := testStateRead(t, statePath)
   257  	if state == nil {
   258  		t.Fatal("state should not be nil")
   259  	}
   260  
   261  	actualStr := strings.TrimSpace(state.String())
   262  	expectedStr := strings.TrimSpace(testApplyDestroyStr)
   263  	if actualStr != expectedStr {
   264  		t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr)
   265  	}
   266  }
   267  
   268  func TestApply_destroyLockedState(t *testing.T) {
   269  	// Create a temporary working directory that is empty
   270  	td := t.TempDir()
   271  	testCopyDir(t, testFixturePath("apply"), td)
   272  	defer testChdir(t, td)()
   273  
   274  	originalState := states.BuildState(func(s *states.SyncState) {
   275  		s.SetResourceInstanceCurrent(
   276  			addrs.Resource{
   277  				Mode: addrs.ManagedResourceMode,
   278  				Type: "test_instance",
   279  				Name: "foo",
   280  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   281  			&states.ResourceInstanceObjectSrc{
   282  				AttrsJSON: []byte(`{"id":"bar"}`),
   283  				Status:    states.ObjectReady,
   284  			},
   285  			addrs.AbsProviderConfig{
   286  				Provider: addrs.NewDefaultProvider("test"),
   287  				Module:   addrs.RootModule,
   288  			},
   289  		)
   290  	})
   291  	statePath := testStateFile(t, originalState)
   292  
   293  	unlock, err := testLockState(t, testDataDir, statePath)
   294  	if err != nil {
   295  		t.Fatal(err)
   296  	}
   297  	defer unlock()
   298  
   299  	p := testProvider()
   300  	view, done := testView(t)
   301  	c := &ApplyCommand{
   302  		Destroy: true,
   303  		Meta: Meta{
   304  			testingOverrides: metaOverridesForProvider(p),
   305  			View:             view,
   306  		},
   307  	}
   308  
   309  	// Run the apply command pointing to our existing state
   310  	args := []string{
   311  		"-auto-approve",
   312  		"-state", statePath,
   313  	}
   314  
   315  	code := c.Run(args)
   316  	output := done(t)
   317  	if code == 0 {
   318  		t.Fatalf("bad: %d\n\n%s", code, output.Stdout())
   319  	}
   320  
   321  	if !strings.Contains(output.Stderr(), "lock") {
   322  		t.Fatal("command output does not look like a lock error:", output.Stderr())
   323  	}
   324  }
   325  
   326  func TestApply_destroyPlan(t *testing.T) {
   327  	// Create a temporary working directory that is empty
   328  	td := t.TempDir()
   329  	testCopyDir(t, testFixturePath("apply"), td)
   330  	defer testChdir(t, td)()
   331  
   332  	planPath := testPlanFileNoop(t)
   333  
   334  	p := testProvider()
   335  	view, done := testView(t)
   336  	c := &ApplyCommand{
   337  		Destroy: true,
   338  		Meta: Meta{
   339  			testingOverrides: metaOverridesForProvider(p),
   340  			View:             view,
   341  		},
   342  	}
   343  
   344  	// Run the apply command pointing to our existing state
   345  	args := []string{
   346  		planPath,
   347  	}
   348  	code := c.Run(args)
   349  	output := done(t)
   350  	if code != 1 {
   351  		t.Fatalf("bad: %d\n\n%s", code, output.Stdout())
   352  	}
   353  	if !strings.Contains(output.Stderr(), "plan file") {
   354  		t.Fatal("expected command output to refer to plan file, but got:", output.Stderr())
   355  	}
   356  }
   357  
   358  func TestApply_destroyPath(t *testing.T) {
   359  	// Create a temporary working directory that is empty
   360  	td := t.TempDir()
   361  	testCopyDir(t, testFixturePath("apply"), td)
   362  	defer testChdir(t, td)()
   363  
   364  	p := applyFixtureProvider()
   365  
   366  	view, done := testView(t)
   367  	c := &ApplyCommand{
   368  		Destroy: true,
   369  		Meta: Meta{
   370  			testingOverrides: metaOverridesForProvider(p),
   371  			View:             view,
   372  		},
   373  	}
   374  
   375  	args := []string{
   376  		"-auto-approve",
   377  		testFixturePath("apply"),
   378  	}
   379  	code := c.Run(args)
   380  	output := done(t)
   381  	if code != 1 {
   382  		t.Fatalf("bad: %d\n\n%s", code, output.Stdout())
   383  	}
   384  	if !strings.Contains(output.Stderr(), "-chdir") {
   385  		t.Fatal("expected command output to refer to -chdir flag, but got:", output.Stderr())
   386  	}
   387  }
   388  
   389  // Config with multiple resources with dependencies, targeting destroy of a
   390  // root node, expecting all other resources to be destroyed due to
   391  // dependencies.
   392  func TestApply_destroyTargetedDependencies(t *testing.T) {
   393  	// Create a temporary working directory that is empty
   394  	td := t.TempDir()
   395  	testCopyDir(t, testFixturePath("apply-destroy-targeted"), td)
   396  	defer testChdir(t, td)()
   397  
   398  	originalState := states.BuildState(func(s *states.SyncState) {
   399  		s.SetResourceInstanceCurrent(
   400  			addrs.Resource{
   401  				Mode: addrs.ManagedResourceMode,
   402  				Type: "test_instance",
   403  				Name: "foo",
   404  			}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
   405  			&states.ResourceInstanceObjectSrc{
   406  				AttrsJSON: []byte(`{"id":"i-ab123"}`),
   407  				Status:    states.ObjectReady,
   408  			},
   409  			addrs.AbsProviderConfig{
   410  				Provider: addrs.NewDefaultProvider("test"),
   411  				Module:   addrs.RootModule,
   412  			},
   413  		)
   414  		s.SetResourceInstanceCurrent(
   415  			addrs.Resource{
   416  				Mode: addrs.ManagedResourceMode,
   417  				Type: "test_load_balancer",
   418  				Name: "foo",
   419  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   420  			&states.ResourceInstanceObjectSrc{
   421  				AttrsJSON:    []byte(`{"id":"i-abc123"}`),
   422  				Dependencies: []addrs.ConfigResource{mustResourceAddr("test_instance.foo")},
   423  				Status:       states.ObjectReady,
   424  			},
   425  			addrs.AbsProviderConfig{
   426  				Provider: addrs.NewDefaultProvider("test"),
   427  				Module:   addrs.RootModule,
   428  			},
   429  		)
   430  	})
   431  	statePath := testStateFile(t, originalState)
   432  
   433  	p := testProvider()
   434  	p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
   435  		ResourceTypes: map[string]providers.Schema{
   436  			"test_instance": {
   437  				Block: &configschema.Block{
   438  					Attributes: map[string]*configschema.Attribute{
   439  						"id": {Type: cty.String, Computed: true},
   440  					},
   441  				},
   442  			},
   443  			"test_load_balancer": {
   444  				Block: &configschema.Block{
   445  					Attributes: map[string]*configschema.Attribute{
   446  						"id":        {Type: cty.String, Computed: true},
   447  						"instances": {Type: cty.List(cty.String), Optional: true},
   448  					},
   449  				},
   450  			},
   451  		},
   452  	}
   453  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
   454  		return providers.PlanResourceChangeResponse{
   455  			PlannedState: req.ProposedNewState,
   456  		}
   457  	}
   458  
   459  	view, done := testView(t)
   460  	c := &ApplyCommand{
   461  		Destroy: true,
   462  		Meta: Meta{
   463  			testingOverrides: metaOverridesForProvider(p),
   464  			View:             view,
   465  		},
   466  	}
   467  
   468  	// Run the apply command pointing to our existing state
   469  	args := []string{
   470  		"-auto-approve",
   471  		"-target", "test_instance.foo",
   472  		"-state", statePath,
   473  	}
   474  	code := c.Run(args)
   475  	output := done(t)
   476  	if code != 0 {
   477  		t.Log(output.Stdout())
   478  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   479  	}
   480  
   481  	// Verify a new state exists
   482  	if _, err := os.Stat(statePath); err != nil {
   483  		t.Fatalf("err: %s", err)
   484  	}
   485  
   486  	f, err := os.Open(statePath)
   487  	if err != nil {
   488  		t.Fatalf("err: %s", err)
   489  	}
   490  	defer f.Close()
   491  
   492  	stateFile, err := statefile.Read(f, encryption.StateEncryptionDisabled())
   493  	if err != nil {
   494  		t.Fatalf("err: %s", err)
   495  	}
   496  	if stateFile == nil || stateFile.State == nil {
   497  		t.Fatal("state should not be nil")
   498  	}
   499  
   500  	spew.Config.DisableMethods = true
   501  	if !stateFile.State.Empty() {
   502  		t.Fatalf("unexpected final state\ngot: %s\nwant: empty state", spew.Sdump(stateFile.State))
   503  	}
   504  
   505  	// Should have a backup file
   506  	f, err = os.Open(statePath + DefaultBackupExtension)
   507  	if err != nil {
   508  		t.Fatalf("err: %s", err)
   509  	}
   510  
   511  	backupStateFile, err := statefile.Read(f, encryption.StateEncryptionDisabled())
   512  	f.Close()
   513  	if err != nil {
   514  		t.Fatalf("err: %s", err)
   515  	}
   516  
   517  	actualStr := strings.TrimSpace(backupStateFile.State.String())
   518  	expectedStr := strings.TrimSpace(originalState.String())
   519  	if actualStr != expectedStr {
   520  		t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\nb%s", actualStr, expectedStr)
   521  	}
   522  }
   523  
   524  // Config with multiple resources with dependencies, targeting destroy of a
   525  // leaf node, expecting the other resources to remain.
   526  func TestApply_destroyTargeted(t *testing.T) {
   527  	// Create a temporary working directory that is empty
   528  	td := t.TempDir()
   529  	testCopyDir(t, testFixturePath("apply-destroy-targeted"), td)
   530  	defer testChdir(t, td)()
   531  
   532  	originalState := states.BuildState(func(s *states.SyncState) {
   533  		s.SetResourceInstanceCurrent(
   534  			addrs.Resource{
   535  				Mode: addrs.ManagedResourceMode,
   536  				Type: "test_instance",
   537  				Name: "foo",
   538  			}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
   539  			&states.ResourceInstanceObjectSrc{
   540  				AttrsJSON: []byte(`{"id":"i-ab123"}`),
   541  				Status:    states.ObjectReady,
   542  			},
   543  			addrs.AbsProviderConfig{
   544  				Provider: addrs.NewDefaultProvider("test"),
   545  				Module:   addrs.RootModule,
   546  			},
   547  		)
   548  		s.SetResourceInstanceCurrent(
   549  			addrs.Resource{
   550  				Mode: addrs.ManagedResourceMode,
   551  				Type: "test_load_balancer",
   552  				Name: "foo",
   553  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   554  			&states.ResourceInstanceObjectSrc{
   555  				AttrsJSON:    []byte(`{"id":"i-abc123"}`),
   556  				Dependencies: []addrs.ConfigResource{mustResourceAddr("test_instance.foo")},
   557  				Status:       states.ObjectReady,
   558  			},
   559  			addrs.AbsProviderConfig{
   560  				Provider: addrs.NewDefaultProvider("test"),
   561  				Module:   addrs.RootModule,
   562  			},
   563  		)
   564  	})
   565  	wantState := states.BuildState(func(s *states.SyncState) {
   566  		s.SetResourceInstanceCurrent(
   567  			addrs.Resource{
   568  				Mode: addrs.ManagedResourceMode,
   569  				Type: "test_instance",
   570  				Name: "foo",
   571  			}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
   572  			&states.ResourceInstanceObjectSrc{
   573  				AttrsJSON: []byte(`{"id":"i-ab123"}`),
   574  				Status:    states.ObjectReady,
   575  			},
   576  			addrs.AbsProviderConfig{
   577  				Provider: addrs.NewDefaultProvider("test"),
   578  				Module:   addrs.RootModule,
   579  			},
   580  		)
   581  	})
   582  	statePath := testStateFile(t, originalState)
   583  
   584  	p := testProvider()
   585  	p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
   586  		ResourceTypes: map[string]providers.Schema{
   587  			"test_instance": {
   588  				Block: &configschema.Block{
   589  					Attributes: map[string]*configschema.Attribute{
   590  						"id": {Type: cty.String, Computed: true},
   591  					},
   592  				},
   593  			},
   594  			"test_load_balancer": {
   595  				Block: &configschema.Block{
   596  					Attributes: map[string]*configschema.Attribute{
   597  						"id":        {Type: cty.String, Computed: true},
   598  						"instances": {Type: cty.List(cty.String), Optional: true},
   599  					},
   600  				},
   601  			},
   602  		},
   603  	}
   604  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
   605  		return providers.PlanResourceChangeResponse{
   606  			PlannedState: req.ProposedNewState,
   607  		}
   608  	}
   609  
   610  	view, done := testView(t)
   611  	c := &ApplyCommand{
   612  		Destroy: true,
   613  		Meta: Meta{
   614  			testingOverrides: metaOverridesForProvider(p),
   615  			View:             view,
   616  		},
   617  	}
   618  
   619  	// Run the apply command pointing to our existing state
   620  	args := []string{
   621  		"-auto-approve",
   622  		"-target", "test_load_balancer.foo",
   623  		"-state", statePath,
   624  	}
   625  	code := c.Run(args)
   626  	output := done(t)
   627  	if code != 0 {
   628  		t.Log(output.Stdout())
   629  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   630  	}
   631  
   632  	// Verify a new state exists
   633  	if _, err := os.Stat(statePath); err != nil {
   634  		t.Fatalf("err: %s", err)
   635  	}
   636  
   637  	f, err := os.Open(statePath)
   638  	if err != nil {
   639  		t.Fatalf("err: %s", err)
   640  	}
   641  	defer f.Close()
   642  
   643  	stateFile, err := statefile.Read(f, encryption.StateEncryptionDisabled())
   644  	if err != nil {
   645  		t.Fatalf("err: %s", err)
   646  	}
   647  	if stateFile == nil || stateFile.State == nil {
   648  		t.Fatal("state should not be nil")
   649  	}
   650  
   651  	actualStr := strings.TrimSpace(stateFile.State.String())
   652  	expectedStr := strings.TrimSpace(wantState.String())
   653  	if actualStr != expectedStr {
   654  		t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\nb%s", actualStr, expectedStr)
   655  	}
   656  
   657  	// Should have a backup file
   658  	f, err = os.Open(statePath + DefaultBackupExtension)
   659  	if err != nil {
   660  		t.Fatalf("err: %s", err)
   661  	}
   662  
   663  	backupStateFile, err := statefile.Read(f, encryption.StateEncryptionDisabled())
   664  	f.Close()
   665  	if err != nil {
   666  		t.Fatalf("err: %s", err)
   667  	}
   668  
   669  	backupActualStr := strings.TrimSpace(backupStateFile.State.String())
   670  	backupExpectedStr := strings.TrimSpace(originalState.String())
   671  	if backupActualStr != backupExpectedStr {
   672  		t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\nb%s", backupActualStr, backupExpectedStr)
   673  	}
   674  }
   675  
   676  const testApplyDestroyStr = `
   677  <no state>
   678  `