github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/terraform/context_test.go (about)

     1  package terraform
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"path/filepath"
     8  	"sort"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/google/go-cmp/cmp"
    14  	"github.com/google/go-cmp/cmp/cmpopts"
    15  	"github.com/hashicorp/go-version"
    16  	"github.com/hashicorp/terraform/internal/configs"
    17  	"github.com/hashicorp/terraform/internal/configs/configload"
    18  	"github.com/hashicorp/terraform/internal/configs/configschema"
    19  	"github.com/hashicorp/terraform/internal/configs/hcl2shim"
    20  	"github.com/hashicorp/terraform/internal/plans"
    21  	"github.com/hashicorp/terraform/internal/plans/planfile"
    22  	"github.com/hashicorp/terraform/internal/providers"
    23  	"github.com/hashicorp/terraform/internal/provisioners"
    24  	"github.com/hashicorp/terraform/internal/states"
    25  	"github.com/hashicorp/terraform/internal/states/statefile"
    26  	"github.com/hashicorp/terraform/internal/tfdiags"
    27  	tfversion "github.com/hashicorp/terraform/version"
    28  	"github.com/zclconf/go-cty/cty"
    29  )
    30  
    31  var (
    32  	equateEmpty   = cmpopts.EquateEmpty()
    33  	typeComparer  = cmp.Comparer(cty.Type.Equals)
    34  	valueComparer = cmp.Comparer(cty.Value.RawEquals)
    35  	valueTrans    = cmp.Transformer("hcl2shim", hcl2shim.ConfigValueFromHCL2)
    36  )
    37  
    38  func TestNewContextRequiredVersion(t *testing.T) {
    39  	cases := []struct {
    40  		Name    string
    41  		Module  string
    42  		Version string
    43  		Value   string
    44  		Err     bool
    45  	}{
    46  		{
    47  			"no requirement",
    48  			"",
    49  			"0.1.0",
    50  			"",
    51  			false,
    52  		},
    53  
    54  		{
    55  			"doesn't match",
    56  			"",
    57  			"0.1.0",
    58  			"> 0.6.0",
    59  			true,
    60  		},
    61  
    62  		{
    63  			"matches",
    64  			"",
    65  			"0.7.0",
    66  			"> 0.6.0",
    67  			false,
    68  		},
    69  
    70  		{
    71  			"prerelease doesn't match with inequality",
    72  			"",
    73  			"0.8.0",
    74  			"> 0.7.0-beta",
    75  			true,
    76  		},
    77  
    78  		{
    79  			"prerelease doesn't match with equality",
    80  			"",
    81  			"0.7.0",
    82  			"0.7.0-beta",
    83  			true,
    84  		},
    85  
    86  		{
    87  			"module matches",
    88  			"context-required-version-module",
    89  			"0.5.0",
    90  			"",
    91  			false,
    92  		},
    93  
    94  		{
    95  			"module doesn't match",
    96  			"context-required-version-module",
    97  			"0.4.0",
    98  			"",
    99  			true,
   100  		},
   101  	}
   102  
   103  	for i, tc := range cases {
   104  		t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
   105  			// Reset the version for the tests
   106  			old := tfversion.SemVer
   107  			tfversion.SemVer = version.Must(version.NewVersion(tc.Version))
   108  			defer func() { tfversion.SemVer = old }()
   109  
   110  			name := "context-required-version"
   111  			if tc.Module != "" {
   112  				name = tc.Module
   113  			}
   114  			mod := testModule(t, name)
   115  			if tc.Value != "" {
   116  				constraint, err := version.NewConstraint(tc.Value)
   117  				if err != nil {
   118  					t.Fatalf("can't parse %q as version constraint", tc.Value)
   119  				}
   120  				mod.Module.CoreVersionConstraints = append(mod.Module.CoreVersionConstraints, configs.VersionConstraint{
   121  					Required: constraint,
   122  				})
   123  			}
   124  			c, diags := NewContext(&ContextOpts{})
   125  			if diags.HasErrors() {
   126  				t.Fatalf("unexpected NewContext errors: %s", diags.Err())
   127  			}
   128  
   129  			diags = c.Validate(mod)
   130  			if diags.HasErrors() != tc.Err {
   131  				t.Fatalf("err: %s", diags.Err())
   132  			}
   133  		})
   134  	}
   135  }
   136  
   137  func TestContext_missingPlugins(t *testing.T) {
   138  	ctx, diags := NewContext(&ContextOpts{})
   139  	assertNoDiagnostics(t, diags)
   140  
   141  	configSrc := `
   142  terraform {
   143  	required_providers {
   144  		explicit = {
   145  			source = "example.com/foo/beep"
   146  		}
   147  		builtin = {
   148  			source = "terraform.io/builtin/nonexist"
   149  		}
   150  	}
   151  }
   152  
   153  resource "implicit_thing" "a" {
   154  	provisioner "nonexist" {
   155  	}
   156  }
   157  
   158  resource "implicit_thing" "b" {
   159  	provider = implicit2
   160  }
   161  `
   162  
   163  	cfg := testModuleInline(t, map[string]string{
   164  		"main.tf": configSrc,
   165  	})
   166  
   167  	// Validate and Plan are the two entry points where we explicitly verify
   168  	// the available plugins match what the configuration needs. For other
   169  	// operations we typically fail more deeply in Terraform Core, with
   170  	// potentially-less-helpful error messages, because getting there would
   171  	// require doing some pretty weird things that aren't common enough to
   172  	// be worth the complexity to check for them.
   173  
   174  	validateDiags := ctx.Validate(cfg)
   175  	_, planDiags := ctx.Plan(cfg, nil, DefaultPlanOpts)
   176  
   177  	tests := map[string]tfdiags.Diagnostics{
   178  		"validate": validateDiags,
   179  		"plan":     planDiags,
   180  	}
   181  
   182  	for testName, gotDiags := range tests {
   183  		t.Run(testName, func(t *testing.T) {
   184  			var wantDiags tfdiags.Diagnostics
   185  			wantDiags = wantDiags.Append(
   186  				tfdiags.Sourceless(
   187  					tfdiags.Error,
   188  					"Missing required provider",
   189  					"This configuration requires built-in provider terraform.io/builtin/nonexist, but that provider isn't available in this Terraform version.",
   190  				),
   191  				tfdiags.Sourceless(
   192  					tfdiags.Error,
   193  					"Missing required provider",
   194  					"This configuration requires provider example.com/foo/beep, but that provider isn't available. You may be able to install it automatically by running:\n  terraform init",
   195  				),
   196  				tfdiags.Sourceless(
   197  					tfdiags.Error,
   198  					"Missing required provider",
   199  					"This configuration requires provider registry.terraform.io/hashicorp/implicit, but that provider isn't available. You may be able to install it automatically by running:\n  terraform init",
   200  				),
   201  				tfdiags.Sourceless(
   202  					tfdiags.Error,
   203  					"Missing required provider",
   204  					"This configuration requires provider registry.terraform.io/hashicorp/implicit2, but that provider isn't available. You may be able to install it automatically by running:\n  terraform init",
   205  				),
   206  				tfdiags.Sourceless(
   207  					tfdiags.Error,
   208  					"Missing required provisioner plugin",
   209  					`This configuration requires provisioner plugin "nonexist", which isn't available. If you're intending to use an external provisioner plugin, you must install it manually into one of the plugin search directories before running Terraform.`,
   210  				),
   211  			)
   212  			assertDiagnosticsMatch(t, gotDiags, wantDiags)
   213  		})
   214  	}
   215  }
   216  
   217  func testContext2(t *testing.T, opts *ContextOpts) *Context {
   218  	t.Helper()
   219  
   220  	ctx, diags := NewContext(opts)
   221  	if diags.HasErrors() {
   222  		t.Fatalf("failed to create test context\n\n%s\n", diags.Err())
   223  	}
   224  
   225  	return ctx
   226  }
   227  
   228  func testApplyFn(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
   229  	resp.NewState = req.PlannedState
   230  	if req.PlannedState.IsNull() {
   231  		resp.NewState = cty.NullVal(req.PriorState.Type())
   232  		return
   233  	}
   234  
   235  	planned := req.PlannedState.AsValueMap()
   236  	if planned == nil {
   237  		planned = map[string]cty.Value{}
   238  	}
   239  
   240  	id, ok := planned["id"]
   241  	if !ok || id.IsNull() || !id.IsKnown() {
   242  		planned["id"] = cty.StringVal("foo")
   243  	}
   244  
   245  	// our default schema has a computed "type" attr
   246  	if ty, ok := planned["type"]; ok && !ty.IsNull() {
   247  		planned["type"] = cty.StringVal(req.TypeName)
   248  	}
   249  
   250  	if cmp, ok := planned["compute"]; ok && !cmp.IsNull() {
   251  		computed := cmp.AsString()
   252  		if val, ok := planned[computed]; ok && !val.IsKnown() {
   253  			planned[computed] = cty.StringVal("computed_value")
   254  		}
   255  	}
   256  
   257  	for k, v := range planned {
   258  		if k == "unknown" {
   259  			// "unknown" should cause an error
   260  			continue
   261  		}
   262  
   263  		if !v.IsKnown() {
   264  			switch k {
   265  			case "type":
   266  				planned[k] = cty.StringVal(req.TypeName)
   267  			default:
   268  				planned[k] = cty.NullVal(v.Type())
   269  			}
   270  		}
   271  	}
   272  
   273  	resp.NewState = cty.ObjectVal(planned)
   274  	return
   275  }
   276  
   277  func testDiffFn(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
   278  	var planned map[string]cty.Value
   279  
   280  	// this is a destroy plan
   281  	if req.ProposedNewState.IsNull() {
   282  		resp.PlannedState = req.ProposedNewState
   283  		resp.PlannedPrivate = req.PriorPrivate
   284  		return resp
   285  	}
   286  
   287  	if !req.ProposedNewState.IsNull() {
   288  		planned = req.ProposedNewState.AsValueMap()
   289  	}
   290  	if planned == nil {
   291  		planned = map[string]cty.Value{}
   292  	}
   293  
   294  	// id is always computed for the tests
   295  	if id, ok := planned["id"]; ok && id.IsNull() {
   296  		planned["id"] = cty.UnknownVal(cty.String)
   297  	}
   298  
   299  	// the old tests have require_new replace on every plan
   300  	if _, ok := planned["require_new"]; ok {
   301  		resp.RequiresReplace = append(resp.RequiresReplace, cty.Path{cty.GetAttrStep{Name: "require_new"}})
   302  	}
   303  
   304  	for k := range planned {
   305  		requiresNewKey := "__" + k + "_requires_new"
   306  		_, ok := planned[requiresNewKey]
   307  		if ok {
   308  			resp.RequiresReplace = append(resp.RequiresReplace, cty.Path{cty.GetAttrStep{Name: requiresNewKey}})
   309  		}
   310  	}
   311  
   312  	if v, ok := planned["compute"]; ok && !v.IsNull() {
   313  		k := v.AsString()
   314  		unknown := cty.UnknownVal(cty.String)
   315  		if strings.HasSuffix(k, ".#") {
   316  			k = k[:len(k)-2]
   317  			unknown = cty.UnknownVal(cty.List(cty.String))
   318  		}
   319  		planned[k] = unknown
   320  	}
   321  
   322  	if t, ok := planned["type"]; ok && t.IsNull() {
   323  		planned["type"] = cty.UnknownVal(cty.String)
   324  	}
   325  
   326  	resp.PlannedState = cty.ObjectVal(planned)
   327  	return
   328  }
   329  
   330  func testProvider(prefix string) *MockProvider {
   331  	p := new(MockProvider)
   332  	p.GetProviderSchemaResponse = testProviderSchema(prefix)
   333  
   334  	return p
   335  }
   336  
   337  func testProvisioner() *MockProvisioner {
   338  	p := new(MockProvisioner)
   339  	p.GetSchemaResponse = provisioners.GetSchemaResponse{
   340  		Provisioner: &configschema.Block{
   341  			Attributes: map[string]*configschema.Attribute{
   342  				"command": {
   343  					Type:     cty.String,
   344  					Optional: true,
   345  				},
   346  				"order": {
   347  					Type:     cty.String,
   348  					Optional: true,
   349  				},
   350  				"when": {
   351  					Type:     cty.String,
   352  					Optional: true,
   353  				},
   354  			},
   355  		},
   356  	}
   357  	return p
   358  }
   359  
   360  func checkStateString(t *testing.T, state *states.State, expected string) {
   361  	t.Helper()
   362  	actual := strings.TrimSpace(state.String())
   363  	expected = strings.TrimSpace(expected)
   364  
   365  	if actual != expected {
   366  		t.Fatalf("incorrect state\ngot:\n%s\n\nwant:\n%s", actual, expected)
   367  	}
   368  }
   369  
   370  // Test helper that gives a function 3 seconds to finish, assumes deadlock and
   371  // fails test if it does not.
   372  func testCheckDeadlock(t *testing.T, f func()) {
   373  	t.Helper()
   374  	timeout := make(chan bool, 1)
   375  	done := make(chan bool, 1)
   376  	go func() {
   377  		time.Sleep(3 * time.Second)
   378  		timeout <- true
   379  	}()
   380  	go func(f func(), done chan bool) {
   381  		defer func() { done <- true }()
   382  		f()
   383  	}(f, done)
   384  	select {
   385  	case <-timeout:
   386  		t.Fatalf("timed out! probably deadlock")
   387  	case <-done:
   388  		// ok
   389  	}
   390  }
   391  
   392  func testProviderSchema(name string) *providers.GetProviderSchemaResponse {
   393  	return getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
   394  		Provider: &configschema.Block{
   395  			Attributes: map[string]*configschema.Attribute{
   396  				"region": {
   397  					Type:     cty.String,
   398  					Optional: true,
   399  				},
   400  				"foo": {
   401  					Type:     cty.String,
   402  					Optional: true,
   403  				},
   404  				"value": {
   405  					Type:     cty.String,
   406  					Optional: true,
   407  				},
   408  				"root": {
   409  					Type:     cty.Number,
   410  					Optional: true,
   411  				},
   412  			},
   413  		},
   414  		ResourceTypes: map[string]*configschema.Block{
   415  			name + "_instance": {
   416  				Attributes: map[string]*configschema.Attribute{
   417  					"id": {
   418  						Type:     cty.String,
   419  						Computed: true,
   420  					},
   421  					"ami": {
   422  						Type:     cty.String,
   423  						Optional: true,
   424  					},
   425  					"dep": {
   426  						Type:     cty.String,
   427  						Optional: true,
   428  					},
   429  					"num": {
   430  						Type:     cty.Number,
   431  						Optional: true,
   432  					},
   433  					"require_new": {
   434  						Type:     cty.String,
   435  						Optional: true,
   436  					},
   437  					"var": {
   438  						Type:     cty.String,
   439  						Optional: true,
   440  					},
   441  					"foo": {
   442  						Type:     cty.String,
   443  						Optional: true,
   444  						Computed: true,
   445  					},
   446  					"bar": {
   447  						Type:     cty.String,
   448  						Optional: true,
   449  					},
   450  					"compute": {
   451  						Type:     cty.String,
   452  						Optional: true,
   453  						Computed: false,
   454  					},
   455  					"compute_value": {
   456  						Type:     cty.String,
   457  						Optional: true,
   458  						Computed: true,
   459  					},
   460  					"value": {
   461  						Type:     cty.String,
   462  						Optional: true,
   463  						Computed: true,
   464  					},
   465  					"output": {
   466  						Type:     cty.String,
   467  						Optional: true,
   468  					},
   469  					"write": {
   470  						Type:     cty.String,
   471  						Optional: true,
   472  					},
   473  					"instance": {
   474  						Type:     cty.String,
   475  						Optional: true,
   476  					},
   477  					"vpc_id": {
   478  						Type:     cty.String,
   479  						Optional: true,
   480  					},
   481  					"type": {
   482  						Type:     cty.String,
   483  						Computed: true,
   484  					},
   485  
   486  					// Generated by testDiffFn if compute = "unknown" is set in the test config
   487  					"unknown": {
   488  						Type:     cty.String,
   489  						Computed: true,
   490  					},
   491  				},
   492  			},
   493  			name + "_eip": {
   494  				Attributes: map[string]*configschema.Attribute{
   495  					"id": {
   496  						Type:     cty.String,
   497  						Computed: true,
   498  					},
   499  					"instance": {
   500  						Type:     cty.String,
   501  						Optional: true,
   502  					},
   503  				},
   504  			},
   505  			name + "_resource": {
   506  				Attributes: map[string]*configschema.Attribute{
   507  					"id": {
   508  						Type:     cty.String,
   509  						Computed: true,
   510  					},
   511  					"value": {
   512  						Type:     cty.String,
   513  						Optional: true,
   514  					},
   515  					"sensitive_value": {
   516  						Type:      cty.String,
   517  						Sensitive: true,
   518  						Optional:  true,
   519  					},
   520  					"random": {
   521  						Type:     cty.String,
   522  						Optional: true,
   523  					},
   524  				},
   525  				BlockTypes: map[string]*configschema.NestedBlock{
   526  					"nesting_single": {
   527  						Block: configschema.Block{
   528  							Attributes: map[string]*configschema.Attribute{
   529  								"value":           {Type: cty.String, Optional: true},
   530  								"sensitive_value": {Type: cty.String, Optional: true, Sensitive: true},
   531  							},
   532  						},
   533  						Nesting: configschema.NestingSingle,
   534  					},
   535  				},
   536  			},
   537  			name + "_ami_list": {
   538  				Attributes: map[string]*configschema.Attribute{
   539  					"id": {
   540  						Type:     cty.String,
   541  						Optional: true,
   542  						Computed: true,
   543  					},
   544  					"ids": {
   545  						Type:     cty.List(cty.String),
   546  						Optional: true,
   547  						Computed: true,
   548  					},
   549  				},
   550  			},
   551  			name + "_remote_state": {
   552  				Attributes: map[string]*configschema.Attribute{
   553  					"id": {
   554  						Type:     cty.String,
   555  						Optional: true,
   556  					},
   557  					"foo": {
   558  						Type:     cty.String,
   559  						Optional: true,
   560  					},
   561  					"output": {
   562  						Type:     cty.Map(cty.String),
   563  						Computed: true,
   564  					},
   565  				},
   566  			},
   567  			name + "_file": {
   568  				Attributes: map[string]*configschema.Attribute{
   569  					"id": {
   570  						Type:     cty.String,
   571  						Optional: true,
   572  					},
   573  					"template": {
   574  						Type:     cty.String,
   575  						Optional: true,
   576  					},
   577  					"rendered": {
   578  						Type:     cty.String,
   579  						Computed: true,
   580  					},
   581  					"__template_requires_new": {
   582  						Type:     cty.String,
   583  						Optional: true,
   584  					},
   585  				},
   586  			},
   587  		},
   588  		DataSources: map[string]*configschema.Block{
   589  			name + "_data_source": {
   590  				Attributes: map[string]*configschema.Attribute{
   591  					"id": {
   592  						Type:     cty.String,
   593  						Computed: true,
   594  					},
   595  					"foo": {
   596  						Type:     cty.String,
   597  						Optional: true,
   598  						Computed: true,
   599  					},
   600  				},
   601  			},
   602  			name + "_remote_state": {
   603  				Attributes: map[string]*configschema.Attribute{
   604  					"id": {
   605  						Type:     cty.String,
   606  						Optional: true,
   607  					},
   608  					"foo": {
   609  						Type:     cty.String,
   610  						Optional: true,
   611  					},
   612  					"output": {
   613  						Type:     cty.Map(cty.String),
   614  						Optional: true,
   615  					},
   616  				},
   617  			},
   618  			name + "_file": {
   619  				Attributes: map[string]*configschema.Attribute{
   620  					"id": {
   621  						Type:     cty.String,
   622  						Optional: true,
   623  					},
   624  					"template": {
   625  						Type:     cty.String,
   626  						Optional: true,
   627  					},
   628  					"rendered": {
   629  						Type:     cty.String,
   630  						Computed: true,
   631  					},
   632  				},
   633  			},
   634  		},
   635  	})
   636  }
   637  
   638  // contextOptsForPlanViaFile is a helper that creates a temporary plan file,
   639  // then reads it back in again and produces a ContextOpts object containing the
   640  // planned changes, prior state and config from the plan file.
   641  //
   642  // This is intended for testing the separated plan/apply workflow in a more
   643  // convenient way than spelling out all of these steps every time. Normally
   644  // only the command and backend packages need to deal with such things, but
   645  // our context tests try to exercise lots of stuff at once and so having them
   646  // round-trip things through on-disk files is often an important part of
   647  // fully representing an old bug in a regression test.
   648  func contextOptsForPlanViaFile(t *testing.T, configSnap *configload.Snapshot, plan *plans.Plan) (*ContextOpts, *configs.Config, *plans.Plan, error) {
   649  	dir := t.TempDir()
   650  
   651  	// We'll just create a dummy statefile.File here because we're not going
   652  	// to run through any of the codepaths that care about Lineage/Serial/etc
   653  	// here anyway.
   654  	stateFile := &statefile.File{
   655  		State: plan.PriorState,
   656  	}
   657  	prevStateFile := &statefile.File{
   658  		State: plan.PrevRunState,
   659  	}
   660  
   661  	// To make life a little easier for test authors, we'll populate a simple
   662  	// backend configuration if they didn't set one, since the backend is
   663  	// usually dealt with in a calling package and so tests in this package
   664  	// don't really care about it.
   665  	if plan.Backend.Config == nil {
   666  		cfg, err := plans.NewDynamicValue(cty.EmptyObjectVal, cty.EmptyObject)
   667  		if err != nil {
   668  			panic(fmt.Sprintf("NewDynamicValue failed: %s", err)) // shouldn't happen because we control the inputs
   669  		}
   670  		plan.Backend.Type = "local"
   671  		plan.Backend.Config = cfg
   672  		plan.Backend.Workspace = "default"
   673  	}
   674  
   675  	filename := filepath.Join(dir, "tfplan")
   676  	err := planfile.Create(filename, planfile.CreateArgs{
   677  		ConfigSnapshot:       configSnap,
   678  		PreviousRunStateFile: prevStateFile,
   679  		StateFile:            stateFile,
   680  		Plan:                 plan,
   681  	})
   682  	if err != nil {
   683  		return nil, nil, nil, err
   684  	}
   685  
   686  	pr, err := planfile.Open(filename)
   687  	if err != nil {
   688  		return nil, nil, nil, err
   689  	}
   690  
   691  	config, diags := pr.ReadConfig()
   692  	if diags.HasErrors() {
   693  		return nil, nil, nil, diags.Err()
   694  	}
   695  
   696  	plan, err = pr.ReadPlan()
   697  	if err != nil {
   698  		return nil, nil, nil, err
   699  	}
   700  
   701  	// Note: This has grown rather silly over the course of ongoing refactoring,
   702  	// because ContextOpts is no longer actually responsible for carrying
   703  	// any information from a plan file and instead all of the information
   704  	// lives inside the config and plan objects. We continue to return a
   705  	// silly empty ContextOpts here just to keep all of the calling tests
   706  	// working.
   707  	return &ContextOpts{}, config, plan, nil
   708  }
   709  
   710  // legacyPlanComparisonString produces a string representation of the changes
   711  // from a plan and a given state togther, as was formerly produced by the
   712  // String method of terraform.Plan.
   713  //
   714  // This is here only for compatibility with existing tests that predate our
   715  // new plan and state types, and should not be used in new tests. Instead, use
   716  // a library like "cmp" to do a deep equality check and diff on the two
   717  // data structures.
   718  func legacyPlanComparisonString(state *states.State, changes *plans.Changes) string {
   719  	return fmt.Sprintf(
   720  		"DIFF:\n\n%s\n\nSTATE:\n\n%s",
   721  		legacyDiffComparisonString(changes),
   722  		state.String(),
   723  	)
   724  }
   725  
   726  // legacyDiffComparisonString produces a string representation of the changes
   727  // from a planned changes object, as was formerly produced by the String method
   728  // of terraform.Diff.
   729  //
   730  // This is here only for compatibility with existing tests that predate our
   731  // new plan types, and should not be used in new tests. Instead, use a library
   732  // like "cmp" to do a deep equality check and diff on the two data structures.
   733  func legacyDiffComparisonString(changes *plans.Changes) string {
   734  	// The old string representation of a plan was grouped by module, but
   735  	// our new plan structure is not grouped in that way and so we'll need
   736  	// to preprocess it in order to produce that grouping.
   737  	type ResourceChanges struct {
   738  		Current *plans.ResourceInstanceChangeSrc
   739  		Deposed map[states.DeposedKey]*plans.ResourceInstanceChangeSrc
   740  	}
   741  	byModule := map[string]map[string]*ResourceChanges{}
   742  	resourceKeys := map[string][]string{}
   743  	var moduleKeys []string
   744  	for _, rc := range changes.Resources {
   745  		if rc.Action == plans.NoOp {
   746  			// We won't mention no-op changes here at all, since the old plan
   747  			// model we are emulating here didn't have such a concept.
   748  			continue
   749  		}
   750  		moduleKey := rc.Addr.Module.String()
   751  		if _, exists := byModule[moduleKey]; !exists {
   752  			moduleKeys = append(moduleKeys, moduleKey)
   753  			byModule[moduleKey] = make(map[string]*ResourceChanges)
   754  		}
   755  		resourceKey := rc.Addr.Resource.String()
   756  		if _, exists := byModule[moduleKey][resourceKey]; !exists {
   757  			resourceKeys[moduleKey] = append(resourceKeys[moduleKey], resourceKey)
   758  			byModule[moduleKey][resourceKey] = &ResourceChanges{
   759  				Deposed: make(map[states.DeposedKey]*plans.ResourceInstanceChangeSrc),
   760  			}
   761  		}
   762  
   763  		if rc.DeposedKey == states.NotDeposed {
   764  			byModule[moduleKey][resourceKey].Current = rc
   765  		} else {
   766  			byModule[moduleKey][resourceKey].Deposed[rc.DeposedKey] = rc
   767  		}
   768  	}
   769  	sort.Strings(moduleKeys)
   770  	for _, ks := range resourceKeys {
   771  		sort.Strings(ks)
   772  	}
   773  
   774  	var buf bytes.Buffer
   775  
   776  	for _, moduleKey := range moduleKeys {
   777  		rcs := byModule[moduleKey]
   778  		var mBuf bytes.Buffer
   779  
   780  		for _, resourceKey := range resourceKeys[moduleKey] {
   781  			rc := rcs[resourceKey]
   782  
   783  			crud := "UPDATE"
   784  			if rc.Current != nil {
   785  				switch rc.Current.Action {
   786  				case plans.DeleteThenCreate:
   787  					crud = "DESTROY/CREATE"
   788  				case plans.CreateThenDelete:
   789  					crud = "CREATE/DESTROY"
   790  				case plans.Delete:
   791  					crud = "DESTROY"
   792  				case plans.Create:
   793  					crud = "CREATE"
   794  				}
   795  			} else {
   796  				// We must be working on a deposed object then, in which
   797  				// case destroying is the only possible action.
   798  				crud = "DESTROY"
   799  			}
   800  
   801  			extra := ""
   802  			if rc.Current == nil && len(rc.Deposed) > 0 {
   803  				extra = " (deposed only)"
   804  			}
   805  
   806  			fmt.Fprintf(
   807  				&mBuf, "%s: %s%s\n",
   808  				crud, resourceKey, extra,
   809  			)
   810  
   811  			attrNames := map[string]bool{}
   812  			var oldAttrs map[string]string
   813  			var newAttrs map[string]string
   814  			if rc.Current != nil {
   815  				if before := rc.Current.Before; before != nil {
   816  					ty, err := before.ImpliedType()
   817  					if err == nil {
   818  						val, err := before.Decode(ty)
   819  						if err == nil {
   820  							oldAttrs = hcl2shim.FlatmapValueFromHCL2(val)
   821  							for k := range oldAttrs {
   822  								attrNames[k] = true
   823  							}
   824  						}
   825  					}
   826  				}
   827  				if after := rc.Current.After; after != nil {
   828  					ty, err := after.ImpliedType()
   829  					if err == nil {
   830  						val, err := after.Decode(ty)
   831  						if err == nil {
   832  							newAttrs = hcl2shim.FlatmapValueFromHCL2(val)
   833  							for k := range newAttrs {
   834  								attrNames[k] = true
   835  							}
   836  						}
   837  					}
   838  				}
   839  			}
   840  			if oldAttrs == nil {
   841  				oldAttrs = make(map[string]string)
   842  			}
   843  			if newAttrs == nil {
   844  				newAttrs = make(map[string]string)
   845  			}
   846  
   847  			attrNamesOrder := make([]string, 0, len(attrNames))
   848  			keyLen := 0
   849  			for n := range attrNames {
   850  				attrNamesOrder = append(attrNamesOrder, n)
   851  				if len(n) > keyLen {
   852  					keyLen = len(n)
   853  				}
   854  			}
   855  			sort.Strings(attrNamesOrder)
   856  
   857  			for _, attrK := range attrNamesOrder {
   858  				v := newAttrs[attrK]
   859  				u := oldAttrs[attrK]
   860  
   861  				if v == hcl2shim.UnknownVariableValue {
   862  					v = "<computed>"
   863  				}
   864  				// NOTE: we don't support <sensitive> here because we would
   865  				// need schema to do that. Excluding sensitive values
   866  				// is now done at the UI layer, and so should not be tested
   867  				// at the core layer.
   868  
   869  				updateMsg := ""
   870  				// TODO: Mark " (forces new resource)" in updateMsg when appropriate.
   871  
   872  				fmt.Fprintf(
   873  					&mBuf, "  %s:%s %#v => %#v%s\n",
   874  					attrK,
   875  					strings.Repeat(" ", keyLen-len(attrK)),
   876  					u, v,
   877  					updateMsg,
   878  				)
   879  			}
   880  		}
   881  
   882  		if moduleKey == "" { // root module
   883  			buf.Write(mBuf.Bytes())
   884  			buf.WriteByte('\n')
   885  			continue
   886  		}
   887  
   888  		fmt.Fprintf(&buf, "%s:\n", moduleKey)
   889  		s := bufio.NewScanner(&mBuf)
   890  		for s.Scan() {
   891  			buf.WriteString(fmt.Sprintf("  %s\n", s.Text()))
   892  		}
   893  	}
   894  
   895  	return buf.String()
   896  }
   897  
   898  // assertNoDiagnostics fails the test in progress (using t.Fatal) if the given
   899  // diagnostics is non-empty.
   900  func assertNoDiagnostics(t *testing.T, diags tfdiags.Diagnostics) {
   901  	t.Helper()
   902  	if len(diags) == 0 {
   903  		return
   904  	}
   905  	logDiagnostics(t, diags)
   906  	t.FailNow()
   907  }
   908  
   909  // assertNoDiagnostics fails the test in progress (using t.Fatal) if the given
   910  // diagnostics has any errors.
   911  func assertNoErrors(t *testing.T, diags tfdiags.Diagnostics) {
   912  	t.Helper()
   913  	if !diags.HasErrors() {
   914  		return
   915  	}
   916  	logDiagnostics(t, diags)
   917  	t.FailNow()
   918  }
   919  
   920  // assertDiagnosticsMatch fails the test in progress (using t.Fatal) if the
   921  // two sets of diagnostics don't match after being normalized using the
   922  // "ForRPC" processing step, which eliminates the specific type information
   923  // and HCL expression information of each diagnostic.
   924  //
   925  // assertDiagnosticsMatch sorts the two sets of diagnostics in the usual way
   926  // before comparing them, though diagnostics only have a partial order so that
   927  // will not totally normalize the ordering of all diagnostics sets.
   928  func assertDiagnosticsMatch(t *testing.T, got, want tfdiags.Diagnostics) {
   929  	got = got.ForRPC()
   930  	want = want.ForRPC()
   931  	got.Sort()
   932  	want.Sort()
   933  	if diff := cmp.Diff(want, got); diff != "" {
   934  		t.Fatalf("wrong diagnostics\n%s", diff)
   935  	}
   936  }
   937  
   938  // logDiagnostics is a test helper that logs the given diagnostics to to the
   939  // given testing.T using t.Log, in a way that is hopefully useful in debugging
   940  // a test. It does not generate any errors or fail the test. See
   941  // assertNoDiagnostics and assertNoErrors for more specific helpers that can
   942  // also fail the test.
   943  func logDiagnostics(t *testing.T, diags tfdiags.Diagnostics) {
   944  	t.Helper()
   945  	for _, diag := range diags {
   946  		desc := diag.Description()
   947  		rng := diag.Source()
   948  
   949  		var severity string
   950  		switch diag.Severity() {
   951  		case tfdiags.Error:
   952  			severity = "ERROR"
   953  		case tfdiags.Warning:
   954  			severity = "WARN"
   955  		default:
   956  			severity = "???" // should never happen
   957  		}
   958  
   959  		if subj := rng.Subject; subj != nil {
   960  			if desc.Detail == "" {
   961  				t.Logf("[%s@%s] %s", severity, subj.StartString(), desc.Summary)
   962  			} else {
   963  				t.Logf("[%s@%s] %s: %s", severity, subj.StartString(), desc.Summary, desc.Detail)
   964  			}
   965  		} else {
   966  			if desc.Detail == "" {
   967  				t.Logf("[%s] %s", severity, desc.Summary)
   968  			} else {
   969  				t.Logf("[%s] %s: %s", severity, desc.Summary, desc.Detail)
   970  			}
   971  		}
   972  	}
   973  }
   974  
   975  const testContextRefreshModuleStr = `
   976  aws_instance.web: (tainted)
   977    ID = bar
   978    provider = provider["registry.terraform.io/hashicorp/aws"]
   979  
   980  module.child:
   981    aws_instance.web:
   982      ID = new
   983      provider = provider["registry.terraform.io/hashicorp/aws"]
   984  `
   985  
   986  const testContextRefreshOutputStr = `
   987  aws_instance.web:
   988    ID = foo
   989    provider = provider["registry.terraform.io/hashicorp/aws"]
   990    foo = bar
   991  
   992  Outputs:
   993  
   994  foo = bar
   995  `
   996  
   997  const testContextRefreshOutputPartialStr = `
   998  <no state>
   999  `
  1000  
  1001  const testContextRefreshTaintedStr = `
  1002  aws_instance.web: (tainted)
  1003    ID = foo
  1004    provider = provider["registry.terraform.io/hashicorp/aws"]
  1005  `