github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/command/apply_destroy_test.go (about)

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