github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/apply_destroy_test.go (about)

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