github.com/opentofu/opentofu@v1.7.1/internal/tofu/context_test.go (about)

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