github.com/muratcelep/terraform@v1.1.0-beta2-not-internal-4/not-internal/terraform/context_test.go (about)

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