github.com/opentofu/opentofu@v1.7.1/internal/command/test_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 command
     7  
     8  import (
     9  	"path"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/google/go-cmp/cmp"
    14  	"github.com/mitchellh/cli"
    15  	"github.com/zclconf/go-cty/cty"
    16  
    17  	"github.com/opentofu/opentofu/internal/addrs"
    18  	testing_command "github.com/opentofu/opentofu/internal/command/testing"
    19  	"github.com/opentofu/opentofu/internal/command/views"
    20  	"github.com/opentofu/opentofu/internal/providers"
    21  	"github.com/opentofu/opentofu/internal/terminal"
    22  )
    23  
    24  func TestTest(t *testing.T) {
    25  	tcs := map[string]struct {
    26  		override string
    27  		args     []string
    28  		expected string
    29  		code     int
    30  		skip     bool
    31  	}{
    32  		"simple_pass": {
    33  			expected: "1 passed, 0 failed.",
    34  			code:     0,
    35  		},
    36  		"simple_pass_nested": {
    37  			expected: "1 passed, 0 failed.",
    38  			code:     0,
    39  		},
    40  		"simple_pass_nested_alternate": {
    41  			args:     []string{"-test-directory", "other"},
    42  			expected: "1 passed, 0 failed.",
    43  			code:     0,
    44  		},
    45  		"simple_pass_very_nested": {
    46  			args:     []string{"-test-directory", "tests/subdir"},
    47  			expected: "1 passed, 0 failed.",
    48  			code:     0,
    49  		},
    50  		"simple_pass_very_nested_alternate": {
    51  			override: "simple_pass_very_nested",
    52  			args:     []string{"-test-directory", "./tests/subdir"},
    53  			expected: "1 passed, 0 failed.",
    54  			code:     0,
    55  		},
    56  		"pass_with_locals": {
    57  			expected: "1 passed, 0 failed.",
    58  			code:     0,
    59  		},
    60  		"pass_with_outputs": {
    61  			expected: "1 passed, 0 failed.",
    62  			code:     0,
    63  		},
    64  		"pass_with_variables": {
    65  			expected: "2 passed, 0 failed.",
    66  			code:     0,
    67  		},
    68  		"plan_then_apply": {
    69  			expected: "2 passed, 0 failed.",
    70  			code:     0,
    71  		},
    72  		"expect_failures_checks": {
    73  			expected: "1 passed, 0 failed.",
    74  			code:     0,
    75  		},
    76  		"expect_failures_inputs": {
    77  			expected: "1 passed, 0 failed.",
    78  			code:     0,
    79  		},
    80  		"expect_failures_outputs": {
    81  			expected: "1 passed, 0 failed.",
    82  			code:     0,
    83  		},
    84  		"expect_runtime_check_fail": {
    85  			expected: "0 passed, 1 failed.",
    86  			code:     1,
    87  		},
    88  		"expect_runtime_check_pass_with_expect": {
    89  			expected: "1 passed, 0 failed.",
    90  			code:     0,
    91  		},
    92  		"expect_runtime_check_pass_command_plan_expected": {
    93  			expected: "1 passed, 0 failed.",
    94  			code:     0,
    95  		},
    96  		"expect_runtime_check_fail_command_plan": {
    97  			expected: "0 passed, 1 failed.",
    98  			code:     1,
    99  		},
   100  		"expect_failures_resources": {
   101  			expected: "1 passed, 0 failed.",
   102  			code:     0,
   103  		},
   104  		"multiple_files": {
   105  			expected: "2 passed, 0 failed",
   106  			code:     0,
   107  		},
   108  		"multiple_files_with_filter": {
   109  			override: "multiple_files",
   110  			args:     []string{"-filter=one.tftest.hcl"},
   111  			expected: "1 passed, 0 failed",
   112  			code:     0,
   113  		},
   114  		"variables": {
   115  			expected: "2 passed, 0 failed",
   116  			code:     0,
   117  		},
   118  		"variables_overridden": {
   119  			override: "variables",
   120  			args:     []string{"-var=input=foo"},
   121  			expected: "1 passed, 1 failed",
   122  			code:     1,
   123  		},
   124  		"simple_fail": {
   125  			expected: "0 passed, 1 failed.",
   126  			code:     1,
   127  		},
   128  		"custom_condition_checks": {
   129  			expected: "0 passed, 1 failed.",
   130  			code:     1,
   131  		},
   132  		"custom_condition_inputs": {
   133  			expected: "0 passed, 1 failed.",
   134  			code:     1,
   135  		},
   136  		"custom_condition_outputs": {
   137  			expected: "0 passed, 1 failed.",
   138  			code:     1,
   139  		},
   140  		"custom_condition_resources": {
   141  			expected: "0 passed, 1 failed.",
   142  			code:     1,
   143  		},
   144  		"no_providers_in_main": {
   145  			expected: "1 passed, 0 failed",
   146  			code:     0,
   147  		},
   148  		"default_variables": {
   149  			expected: "1 passed, 0 failed.",
   150  			code:     0,
   151  		},
   152  		"undefined_variables": {
   153  			expected: "1 passed, 0 failed.",
   154  			code:     0,
   155  		},
   156  		"refresh_only": {
   157  			expected: "3 passed, 0 failed.",
   158  			code:     0,
   159  		},
   160  		"null_output": {
   161  			expected: "1 passed, 0 failed.",
   162  			code:     0,
   163  		},
   164  		"pass_with_tests_dir_variables": {
   165  			expected: "1 passed, 0 failed.",
   166  			code:     0,
   167  		},
   168  		"override_with_tests_dir_variables": {
   169  			expected: "1 passed, 0 failed.",
   170  			code:     0,
   171  		},
   172  	}
   173  	for name, tc := range tcs {
   174  		t.Run(name, func(t *testing.T) {
   175  			if tc.skip {
   176  				t.Skip()
   177  			}
   178  
   179  			file := name
   180  			if len(tc.override) > 0 {
   181  				file = tc.override
   182  			}
   183  
   184  			td := t.TempDir()
   185  			testCopyDir(t, testFixturePath(path.Join("test", file)), td)
   186  			defer testChdir(t, td)()
   187  
   188  			provider := testing_command.NewProvider(nil)
   189  			view, done := testView(t)
   190  
   191  			c := &TestCommand{
   192  				Meta: Meta{
   193  					testingOverrides: metaOverridesForProvider(provider.Provider),
   194  					View:             view,
   195  				},
   196  			}
   197  
   198  			code := c.Run(tc.args)
   199  			output := done(t)
   200  
   201  			if code != tc.code {
   202  				t.Errorf("expected status code %d but got %d", tc.code, code)
   203  			}
   204  
   205  			if !strings.Contains(output.Stdout(), tc.expected) {
   206  				t.Errorf("output didn't contain expected string:\n\n%s", output.All())
   207  			}
   208  
   209  			if provider.ResourceCount() > 0 {
   210  				t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
   211  			}
   212  		})
   213  	}
   214  }
   215  func TestTest_Full_Output(t *testing.T) {
   216  	tcs := map[string]struct {
   217  		override string
   218  		args     []string
   219  		expected string
   220  		code     int
   221  		skip     bool
   222  	}{
   223  		"broken_no_valid_hcl": {
   224  			expected: "Unsupported block type",
   225  			code:     1,
   226  		},
   227  		"expect_runtime_check_fail_command_plan": {
   228  			expected: "Check block assertion known after apply",
   229  			code:     1,
   230  		},
   231  		"broken_wrong_block_resource": {
   232  			expected: "Blocks of type \"resource\" are not expected here.",
   233  			code:     1,
   234  		},
   235  		"broken_wrong_block_data": {
   236  			expected: "Blocks of type \"data\" are not expected here.",
   237  			code:     1,
   238  		},
   239  		"broken_wrong_block_output": {
   240  			expected: "Blocks of type \"output\" are not expected here.",
   241  			code:     1,
   242  		},
   243  		"broken_wrong_block_check": {
   244  			expected: "Blocks of type \"check\" are not expected here.",
   245  			code:     1,
   246  		},
   247  		"not_exists_output": {
   248  			expected: "Error: Reference to undeclared output value",
   249  			args:     []string{"-no-color"},
   250  			code:     1,
   251  		},
   252  		"refresh_conflicting_config": {
   253  			expected: "Incompatible plan options",
   254  			code:     1,
   255  		},
   256  		"is_sorted": {
   257  			expected: "1.tftest.hcl... pass\n  run \"a\"... pass\n2.tftest.hcl... pass\n  run \"b\"... pass\n3.tftest.hcl... pass\n  run \"c\"... pass",
   258  			code:     0,
   259  			args:     []string{"-no-color"},
   260  		},
   261  	}
   262  	for name, tc := range tcs {
   263  		t.Run(name, func(t *testing.T) {
   264  			if tc.skip {
   265  				t.Skip()
   266  			}
   267  
   268  			file := name
   269  			if len(tc.override) > 0 {
   270  				file = tc.override
   271  			}
   272  
   273  			td := t.TempDir()
   274  			testCopyDir(t, testFixturePath(path.Join("test", file)), td)
   275  			defer testChdir(t, td)()
   276  
   277  			provider := testing_command.NewProvider(nil)
   278  			view, done := testView(t)
   279  
   280  			c := &TestCommand{
   281  				Meta: Meta{
   282  					testingOverrides: metaOverridesForProvider(provider.Provider),
   283  					View:             view,
   284  				},
   285  			}
   286  
   287  			code := c.Run(tc.args)
   288  			output := done(t)
   289  
   290  			if code != tc.code {
   291  				t.Errorf("expected status code %d but got %d", tc.code, code)
   292  			}
   293  
   294  			if !strings.Contains(output.All(), tc.expected) {
   295  				t.Errorf("output didn't contain expected string:\n\n%s \n\n----\n\nexpected: %s", output.All(), tc.expected)
   296  			}
   297  
   298  			if provider.ResourceCount() > 0 {
   299  				t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
   300  			}
   301  		})
   302  	}
   303  }
   304  
   305  func TestTest_Interrupt(t *testing.T) {
   306  	td := t.TempDir()
   307  	testCopyDir(t, testFixturePath(path.Join("test", "with_interrupt")), td)
   308  	defer testChdir(t, td)()
   309  
   310  	provider := testing_command.NewProvider(nil)
   311  	view, done := testView(t)
   312  
   313  	interrupt := make(chan struct{})
   314  	provider.Interrupt = interrupt
   315  
   316  	c := &TestCommand{
   317  		Meta: Meta{
   318  			testingOverrides: metaOverridesForProvider(provider.Provider),
   319  			View:             view,
   320  			ShutdownCh:       interrupt,
   321  		},
   322  	}
   323  
   324  	c.Run(nil)
   325  	output := done(t).All()
   326  
   327  	if !strings.Contains(output, "Interrupt received") {
   328  		t.Errorf("output didn't produce the right output:\n\n%s", output)
   329  	}
   330  
   331  	if provider.ResourceCount() > 0 {
   332  		// we asked for a nice stop in this one, so it should still have tidied everything up.
   333  		t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
   334  	}
   335  }
   336  
   337  func TestTest_DoubleInterrupt(t *testing.T) {
   338  	td := t.TempDir()
   339  	testCopyDir(t, testFixturePath(path.Join("test", "with_double_interrupt")), td)
   340  	defer testChdir(t, td)()
   341  
   342  	provider := testing_command.NewProvider(nil)
   343  	view, done := testView(t)
   344  
   345  	interrupt := make(chan struct{})
   346  	provider.Interrupt = interrupt
   347  
   348  	c := &TestCommand{
   349  		Meta: Meta{
   350  			testingOverrides: metaOverridesForProvider(provider.Provider),
   351  			View:             view,
   352  			ShutdownCh:       interrupt,
   353  		},
   354  	}
   355  
   356  	c.Run(nil)
   357  	output := done(t).All()
   358  
   359  	if !strings.Contains(output, "Two interrupts received") {
   360  		t.Errorf("output didn't produce the right output:\n\n%s", output)
   361  	}
   362  
   363  	cleanupMessage := `OpenTofu was interrupted while executing main.tftest.hcl, and may not have
   364  performed the expected cleanup operations.
   365  
   366  OpenTofu has already created the following resources from the module under
   367  test:
   368    - test_resource.primary
   369    - test_resource.secondary
   370    - test_resource.tertiary`
   371  
   372  	// It's really important that the above message is printed, so we're testing
   373  	// for it specifically and making sure it contains all the resources.
   374  	if !strings.Contains(output, cleanupMessage) {
   375  		t.Errorf("output didn't produce the right output:\n\n%s", output)
   376  	}
   377  
   378  	// This time the test command shouldn't have cleaned up the resource because
   379  	// of the hard interrupt.
   380  	if provider.ResourceCount() != 3 {
   381  		// we asked for a nice stop in this one, so it should still have tidied everything up.
   382  		t.Errorf("should not have deleted all resources on completion but left %v", provider.ResourceString())
   383  	}
   384  }
   385  
   386  func TestTest_ProviderAlias(t *testing.T) {
   387  	td := t.TempDir()
   388  	testCopyDir(t, testFixturePath(path.Join("test", "with_provider_alias")), td)
   389  	defer testChdir(t, td)()
   390  
   391  	provider := testing_command.NewProvider(nil)
   392  
   393  	providerSource, close := newMockProviderSource(t, map[string][]string{
   394  		"test": {"1.0.0"},
   395  	})
   396  	defer close()
   397  
   398  	streams, done := terminal.StreamsForTesting(t)
   399  	view := views.NewView(streams)
   400  	ui := new(cli.MockUi)
   401  
   402  	meta := Meta{
   403  		testingOverrides: metaOverridesForProvider(provider.Provider),
   404  		Ui:               ui,
   405  		View:             view,
   406  		Streams:          streams,
   407  		ProviderSource:   providerSource,
   408  	}
   409  
   410  	init := &InitCommand{
   411  		Meta: meta,
   412  	}
   413  
   414  	if code := init.Run(nil); code != 0 {
   415  		t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
   416  	}
   417  
   418  	command := &TestCommand{
   419  		Meta: meta,
   420  	}
   421  
   422  	code := command.Run(nil)
   423  	output := done(t)
   424  
   425  	printedOutput := false
   426  
   427  	if code != 0 {
   428  		printedOutput = true
   429  		t.Errorf("expected status code 0 but got %d: %s", code, output.All())
   430  	}
   431  
   432  	if provider.ResourceCount() > 0 {
   433  		if !printedOutput {
   434  			t.Errorf("should have deleted all resources on completion but left %s\n\n%s", provider.ResourceString(), output.All())
   435  		} else {
   436  			t.Errorf("should have deleted all resources on completion but left %s", provider.ResourceString())
   437  		}
   438  	}
   439  }
   440  
   441  func TestTest_ModuleDependencies(t *testing.T) {
   442  	td := t.TempDir()
   443  	testCopyDir(t, testFixturePath(path.Join("test", "with_setup_module")), td)
   444  	defer testChdir(t, td)()
   445  
   446  	// Our two providers will share a common set of values to make things
   447  	// easier.
   448  	store := &testing_command.ResourceStore{
   449  		Data: make(map[string]cty.Value),
   450  	}
   451  
   452  	// We set it up so the module provider will update the data sources
   453  	// available to the core mock provider.
   454  	test := testing_command.NewProvider(store)
   455  	setup := testing_command.NewProvider(store)
   456  
   457  	test.SetDataPrefix("data")
   458  	test.SetResourcePrefix("resource")
   459  
   460  	// Let's make the setup provider write into the data for test provider.
   461  	setup.SetResourcePrefix("data")
   462  
   463  	providerSource, close := newMockProviderSource(t, map[string][]string{
   464  		"test":  {"1.0.0"},
   465  		"setup": {"1.0.0"},
   466  	})
   467  	defer close()
   468  
   469  	streams, done := terminal.StreamsForTesting(t)
   470  	view := views.NewView(streams)
   471  	ui := new(cli.MockUi)
   472  
   473  	meta := Meta{
   474  		testingOverrides: &testingOverrides{
   475  			Providers: map[addrs.Provider]providers.Factory{
   476  				addrs.NewDefaultProvider("test"):  providers.FactoryFixed(test.Provider),
   477  				addrs.NewDefaultProvider("setup"): providers.FactoryFixed(setup.Provider),
   478  			},
   479  		},
   480  		Ui:             ui,
   481  		View:           view,
   482  		Streams:        streams,
   483  		ProviderSource: providerSource,
   484  	}
   485  
   486  	init := &InitCommand{
   487  		Meta: meta,
   488  	}
   489  
   490  	if code := init.Run(nil); code != 0 {
   491  		t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
   492  	}
   493  
   494  	command := &TestCommand{
   495  		Meta: meta,
   496  	}
   497  
   498  	code := command.Run(nil)
   499  	output := done(t)
   500  
   501  	printedOutput := false
   502  
   503  	if code != 0 {
   504  		printedOutput = true
   505  		t.Errorf("expected status code 0 but got %d: %s", code, output.All())
   506  	}
   507  
   508  	if test.ResourceCount() > 0 {
   509  		if !printedOutput {
   510  			printedOutput = true
   511  			t.Errorf("should have deleted all resources on completion but left %s\n\n%s", test.ResourceString(), output.All())
   512  		} else {
   513  			t.Errorf("should have deleted all resources on completion but left %s", test.ResourceString())
   514  		}
   515  	}
   516  
   517  	if setup.ResourceCount() > 0 {
   518  		if !printedOutput {
   519  			t.Errorf("should have deleted all resources on completion but left %s\n\n%s", setup.ResourceString(), output.All())
   520  		} else {
   521  			t.Errorf("should have deleted all resources on completion but left %s", setup.ResourceString())
   522  		}
   523  	}
   524  }
   525  
   526  func TestTest_CatchesErrorsBeforeDestroy(t *testing.T) {
   527  	td := t.TempDir()
   528  	testCopyDir(t, testFixturePath(path.Join("test", "invalid_default_state")), td)
   529  	defer testChdir(t, td)()
   530  
   531  	provider := testing_command.NewProvider(nil)
   532  	view, done := testView(t)
   533  
   534  	c := &TestCommand{
   535  		Meta: Meta{
   536  			testingOverrides: metaOverridesForProvider(provider.Provider),
   537  			View:             view,
   538  		},
   539  	}
   540  
   541  	code := c.Run([]string{"-no-color"})
   542  	output := done(t)
   543  
   544  	if code != 1 {
   545  		t.Errorf("expected status code 0 but got %d", code)
   546  	}
   547  
   548  	expectedOut := `main.tftest.hcl... fail
   549    run "test"... fail
   550  
   551  Failure! 0 passed, 1 failed.
   552  `
   553  
   554  	expectedErr := `
   555  Error: No value for required variable
   556  
   557    on main.tf line 2:
   558     2: variable "input" {
   559  
   560  The root module input variable "input" is not set, and has no default value.
   561  Use a -var or -var-file command line argument to provide a value for this
   562  variable.
   563  `
   564  
   565  	actualOut := output.Stdout()
   566  	actualErr := output.Stderr()
   567  
   568  	if diff := cmp.Diff(actualOut, expectedOut); len(diff) > 0 {
   569  		t.Errorf("std out didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedOut, actualOut, diff)
   570  	}
   571  
   572  	if diff := cmp.Diff(actualErr, expectedErr); len(diff) > 0 {
   573  		t.Errorf("std err didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedErr, actualErr, diff)
   574  	}
   575  
   576  	if provider.ResourceCount() > 0 {
   577  		t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
   578  	}
   579  }
   580  
   581  func TestTest_Verbose(t *testing.T) {
   582  	td := t.TempDir()
   583  	testCopyDir(t, testFixturePath(path.Join("test", "plan_then_apply")), td)
   584  	defer testChdir(t, td)()
   585  
   586  	provider := testing_command.NewProvider(nil)
   587  	view, done := testView(t)
   588  
   589  	c := &TestCommand{
   590  		Meta: Meta{
   591  			testingOverrides: metaOverridesForProvider(provider.Provider),
   592  			View:             view,
   593  		},
   594  	}
   595  
   596  	code := c.Run([]string{"-verbose", "-no-color"})
   597  	output := done(t)
   598  
   599  	if code != 0 {
   600  		t.Errorf("expected status code 0 but got %d", code)
   601  	}
   602  
   603  	expected := `main.tftest.hcl... pass
   604    run "validate_test_resource"... pass
   605  
   606  OpenTofu used the selected providers to generate the following execution
   607  plan. Resource actions are indicated with the following symbols:
   608    + create
   609  
   610  OpenTofu will perform the following actions:
   611  
   612    # test_resource.foo will be created
   613    + resource "test_resource" "foo" {
   614        + id    = "constant_value"
   615        + value = "bar"
   616      }
   617  
   618  Plan: 1 to add, 0 to change, 0 to destroy.
   619    run "validate_test_resource"... pass
   620  # test_resource.foo:
   621  resource "test_resource" "foo" {
   622      id    = "constant_value"
   623      value = "bar"
   624  }
   625  
   626  Success! 2 passed, 0 failed.
   627  `
   628  
   629  	actual := output.All()
   630  
   631  	if diff := cmp.Diff(actual, expected); len(diff) > 0 {
   632  		t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff)
   633  	}
   634  
   635  	if provider.ResourceCount() > 0 {
   636  		t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
   637  	}
   638  }
   639  
   640  func TestTest_ValidatesBeforeExecution(t *testing.T) {
   641  	tcs := map[string]struct {
   642  		expectedOut string
   643  		expectedErr string
   644  	}{
   645  		"invalid": {
   646  			expectedOut: `main.tftest.hcl... fail
   647    run "invalid"... fail
   648  
   649  Failure! 0 passed, 1 failed.
   650  `,
   651  			expectedErr: `
   652  Error: Invalid ` + "`expect_failures`" + ` reference
   653  
   654    on main.tftest.hcl line 5, in run "invalid":
   655     5:         local.my_value,
   656  
   657  You cannot expect failures from local.my_value. You can only expect failures
   658  from checkable objects such as input variables, output values, check blocks,
   659  managed resources and data sources.
   660  `,
   661  		},
   662  		"invalid-module": {
   663  			expectedOut: `main.tftest.hcl... fail
   664    run "invalid"... fail
   665    run "test"... skip
   666  
   667  Failure! 0 passed, 1 failed, 1 skipped.
   668  `,
   669  			expectedErr: `
   670  Error: Reference to undeclared input variable
   671  
   672    on setup/main.tf line 3, in resource "test_resource" "setup":
   673     3:     value = var.not_real // Oh no!
   674  
   675  An input variable with the name "not_real" has not been declared. This
   676  variable can be declared with a variable "not_real" {} block.
   677  `,
   678  		},
   679  		"missing-provider": {
   680  			expectedOut: `main.tftest.hcl... fail
   681    run "passes_validation"... fail
   682  
   683  Failure! 0 passed, 1 failed.
   684  `,
   685  			expectedErr: `
   686  Error: Provider configuration not present
   687  
   688  To work with test_resource.secondary its original provider configuration at
   689  provider["registry.opentofu.org/hashicorp/test"].secondary is required, but
   690  it has been removed. This occurs when a provider configuration is removed
   691  while objects created by that provider still exist in the state. Re-add the
   692  provider configuration to destroy test_resource.secondary, after which you
   693  can remove the provider configuration again.
   694  `,
   695  		},
   696  		"missing-provider-in-run-block": {
   697  			expectedOut: `main.tftest.hcl... fail
   698    run "passes_validation"... fail
   699  
   700  Failure! 0 passed, 1 failed.
   701  `,
   702  			expectedErr: `
   703  Error: Provider configuration not present
   704  
   705  To work with test_resource.secondary its original provider configuration at
   706  provider["registry.opentofu.org/hashicorp/test"].secondary is required, but
   707  it has been removed. This occurs when a provider configuration is removed
   708  while objects created by that provider still exist in the state. Re-add the
   709  provider configuration to destroy test_resource.secondary, after which you
   710  can remove the provider configuration again.
   711  `,
   712  		},
   713  		"missing-provider-in-test-module": {
   714  			expectedOut: `main.tftest.hcl... fail
   715    run "passes_validation_primary"... pass
   716    run "passes_validation_secondary"... fail
   717  
   718  Failure! 1 passed, 1 failed.
   719  `,
   720  			expectedErr: `
   721  Error: Provider configuration not present
   722  
   723  To work with test_resource.secondary its original provider configuration at
   724  provider["registry.opentofu.org/hashicorp/test"].secondary is required, but
   725  it has been removed. This occurs when a provider configuration is removed
   726  while objects created by that provider still exist in the state. Re-add the
   727  provider configuration to destroy test_resource.secondary, after which you
   728  can remove the provider configuration again.
   729  `,
   730  		},
   731  	}
   732  
   733  	for file, tc := range tcs {
   734  		t.Run(file, func(t *testing.T) {
   735  
   736  			td := t.TempDir()
   737  			testCopyDir(t, testFixturePath(path.Join("test", file)), td)
   738  			defer testChdir(t, td)()
   739  
   740  			provider := testing_command.NewProvider(nil)
   741  
   742  			providerSource, close := newMockProviderSource(t, map[string][]string{
   743  				"test": {"1.0.0"},
   744  			})
   745  			defer close()
   746  
   747  			streams, done := terminal.StreamsForTesting(t)
   748  			view := views.NewView(streams)
   749  			ui := new(cli.MockUi)
   750  
   751  			meta := Meta{
   752  				testingOverrides: metaOverridesForProvider(provider.Provider),
   753  				Ui:               ui,
   754  				View:             view,
   755  				Streams:          streams,
   756  				ProviderSource:   providerSource,
   757  			}
   758  
   759  			init := &InitCommand{
   760  				Meta: meta,
   761  			}
   762  
   763  			if code := init.Run(nil); code != 0 {
   764  				t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
   765  			}
   766  
   767  			c := &TestCommand{
   768  				Meta: meta,
   769  			}
   770  
   771  			code := c.Run([]string{"-no-color"})
   772  			output := done(t)
   773  
   774  			if code != 1 {
   775  				t.Errorf("expected status code 1 but got %d", code)
   776  			}
   777  
   778  			actualOut, expectedOut := output.Stdout(), tc.expectedOut
   779  			actualErr, expectedErr := output.Stderr(), tc.expectedErr
   780  
   781  			if diff := cmp.Diff(actualOut, expectedOut); len(diff) > 0 {
   782  				t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedOut, actualOut, diff)
   783  			}
   784  
   785  			if diff := cmp.Diff(actualErr, expectedErr); len(diff) > 0 {
   786  				t.Errorf("error didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedErr, actualErr, diff)
   787  			}
   788  
   789  			if provider.ResourceCount() > 0 {
   790  				t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
   791  			}
   792  		})
   793  	}
   794  }
   795  
   796  func TestTest_Modules(t *testing.T) {
   797  	tcs := map[string]struct {
   798  		expected string
   799  		code     int
   800  		skip     bool
   801  	}{
   802  		"pass_module_with_no_resource": {
   803  			expected: "main.tftest.hcl... pass\n  run \"run\"... pass\n\nSuccess! 1 passed, 0 failed.\n",
   804  			code:     0,
   805  		},
   806  		"with_nested_setup_modules": {
   807  			expected: "main.tftest.hcl... pass\n  run \"load_module\"... pass\n\nSuccess! 1 passed, 0 failed.\n",
   808  			code:     0,
   809  		},
   810  		"with_verify_module": {
   811  			expected: "main.tftest.hcl... pass\n  run \"test\"... pass\n  run \"verify\"... pass\n\nSuccess! 2 passed, 0 failed.\n",
   812  			code:     0,
   813  		},
   814  		"only_modules": {
   815  			expected: "main.tftest.hcl... pass\n  run \"first\"... pass\n  run \"second\"... pass\n\nSuccess! 2 passed, 0 failed.\n",
   816  			code:     0,
   817  		},
   818  		"variables_reference": {
   819  			expected: "main.tftest.hcl... pass\n  run \"setup\"... pass\n  run \"test\"... pass\n\nSuccess! 2 passed, 0 failed.\n",
   820  			code:     0,
   821  		},
   822  	}
   823  
   824  	for name, tc := range tcs {
   825  		t.Run(name, func(t *testing.T) {
   826  			if tc.skip {
   827  				t.Skip()
   828  			}
   829  
   830  			file := name
   831  
   832  			td := t.TempDir()
   833  			testCopyDir(t, testFixturePath(path.Join("test", file)), td)
   834  			defer testChdir(t, td)()
   835  
   836  			provider := testing_command.NewProvider(nil)
   837  			providerSource, close := newMockProviderSource(t, map[string][]string{
   838  				"test": {"1.0.0"},
   839  			})
   840  			defer close()
   841  
   842  			streams, done := terminal.StreamsForTesting(t)
   843  			view := views.NewView(streams)
   844  			ui := new(cli.MockUi)
   845  			meta := Meta{
   846  				testingOverrides: metaOverridesForProvider(provider.Provider),
   847  				Ui:               ui,
   848  				View:             view,
   849  				Streams:          streams,
   850  				ProviderSource:   providerSource,
   851  			}
   852  
   853  			init := &InitCommand{
   854  				Meta: meta,
   855  			}
   856  
   857  			if code := init.Run(nil); code != 0 {
   858  				t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
   859  			}
   860  
   861  			command := &TestCommand{
   862  				Meta: meta,
   863  			}
   864  
   865  			code := command.Run([]string{"-no-color"})
   866  			output := done(t)
   867  			printedOutput := false
   868  
   869  			if code != tc.code {
   870  				printedOutput = true
   871  				t.Errorf("expected status code %d but got %d: %s", tc.code, code, output.All())
   872  			}
   873  
   874  			actual := output.All()
   875  
   876  			if diff := cmp.Diff(actual, tc.expected); len(diff) > 0 {
   877  				t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", tc.expected, actual, diff)
   878  			}
   879  
   880  			if provider.ResourceCount() > 0 {
   881  				if !printedOutput {
   882  					t.Errorf("should have deleted all resources on completion but left %s\n\n%s", provider.ResourceString(), output.All())
   883  				} else {
   884  					t.Errorf("should have deleted all resources on completion but left %s", provider.ResourceString())
   885  				}
   886  			}
   887  
   888  			if provider.DataSourceCount() > 0 {
   889  				if !printedOutput {
   890  					t.Errorf("should have deleted all data sources on completion but left %s\n\n%s", provider.DataSourceString(), output.All())
   891  				} else {
   892  					t.Errorf("should have deleted all data sources on completion but left %s", provider.DataSourceString())
   893  				}
   894  			}
   895  		})
   896  	}
   897  }
   898  
   899  func TestTest_StatePropagation(t *testing.T) {
   900  	td := t.TempDir()
   901  	testCopyDir(t, testFixturePath(path.Join("test", "state_propagation")), td)
   902  	defer testChdir(t, td)()
   903  
   904  	provider := testing_command.NewProvider(nil)
   905  
   906  	providerSource, close := newMockProviderSource(t, map[string][]string{
   907  		"test": {"1.0.0"},
   908  	})
   909  	defer close()
   910  
   911  	streams, done := terminal.StreamsForTesting(t)
   912  	view := views.NewView(streams)
   913  	ui := new(cli.MockUi)
   914  
   915  	meta := Meta{
   916  		testingOverrides: metaOverridesForProvider(provider.Provider),
   917  		Ui:               ui,
   918  		View:             view,
   919  		Streams:          streams,
   920  		ProviderSource:   providerSource,
   921  	}
   922  
   923  	init := &InitCommand{
   924  		Meta: meta,
   925  	}
   926  
   927  	if code := init.Run(nil); code != 0 {
   928  		t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
   929  	}
   930  
   931  	c := &TestCommand{
   932  		Meta: meta,
   933  	}
   934  
   935  	code := c.Run([]string{"-verbose", "-no-color"})
   936  	output := done(t)
   937  
   938  	if code != 0 {
   939  		t.Errorf("expected status code 0 but got %d", code)
   940  	}
   941  
   942  	expected := `main.tftest.hcl... pass
   943    run "initial_apply_example"... pass
   944  # test_resource.module_resource:
   945  resource "test_resource" "module_resource" {
   946      id    = "df6h8as9"
   947      value = "start"
   948  }
   949    run "initial_apply"... pass
   950  # test_resource.resource:
   951  resource "test_resource" "resource" {
   952      id    = "598318e0"
   953      value = "start"
   954  }
   955    run "plan_second_example"... pass
   956  
   957  OpenTofu used the selected providers to generate the following execution
   958  plan. Resource actions are indicated with the following symbols:
   959    + create
   960  
   961  OpenTofu will perform the following actions:
   962  
   963    # test_resource.second_module_resource will be created
   964    + resource "test_resource" "second_module_resource" {
   965        + id    = "b6a1d8cb"
   966        + value = "start"
   967      }
   968  
   969  Plan: 1 to add, 0 to change, 0 to destroy.
   970    run "plan_update"... pass
   971  
   972  OpenTofu used the selected providers to generate the following execution
   973  plan. Resource actions are indicated with the following symbols:
   974    ~ update in-place
   975  
   976  OpenTofu will perform the following actions:
   977  
   978    # test_resource.resource will be updated in-place
   979    ~ resource "test_resource" "resource" {
   980          id    = "598318e0"
   981        ~ value = "start" -> "update"
   982      }
   983  
   984  Plan: 0 to add, 1 to change, 0 to destroy.
   985    run "plan_update_example"... pass
   986  
   987  OpenTofu used the selected providers to generate the following execution
   988  plan. Resource actions are indicated with the following symbols:
   989    ~ update in-place
   990  
   991  OpenTofu will perform the following actions:
   992  
   993    # test_resource.module_resource will be updated in-place
   994    ~ resource "test_resource" "module_resource" {
   995          id    = "df6h8as9"
   996        ~ value = "start" -> "update"
   997      }
   998  
   999  Plan: 0 to add, 1 to change, 0 to destroy.
  1000  
  1001  Success! 5 passed, 0 failed.
  1002  `
  1003  
  1004  	actual := output.All()
  1005  
  1006  	if diff := cmp.Diff(actual, expected); len(diff) > 0 {
  1007  		t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff)
  1008  	}
  1009  
  1010  	if provider.ResourceCount() > 0 {
  1011  		t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
  1012  	}
  1013  }
  1014  
  1015  func TestTest_PartialUpdates(t *testing.T) {
  1016  	tcs := map[string]struct {
  1017  		expectedOut  string
  1018  		expectedErr  string
  1019  		expectedCode int
  1020  	}{
  1021  		"partial_updates": {
  1022  			expectedOut: `main.tftest.hcl... pass
  1023    run "first"... pass
  1024  
  1025  Warning: Resource targeting is in effect
  1026  
  1027  You are creating a plan with the -target option, which means that the result
  1028  of this plan may not represent all of the changes requested by the current
  1029  configuration.
  1030  
  1031  The -target option is not for routine use, and is provided only for
  1032  exceptional situations such as recovering from errors or mistakes, or when
  1033  OpenTofu specifically suggests to use it as part of an error message.
  1034  
  1035  Warning: Applied changes may be incomplete
  1036  
  1037  The plan was created with the -target option in effect, so some changes
  1038  requested in the configuration may have been ignored and the output values
  1039  may not be fully updated. Run the following command to verify that no other
  1040  changes are pending:
  1041      tofu plan
  1042  	
  1043  Note that the -target option is not suitable for routine use, and is provided
  1044  only for exceptional situations such as recovering from errors or mistakes,
  1045  or when OpenTofu specifically suggests to use it as part of an error message.
  1046    run "second"... pass
  1047  
  1048  Success! 2 passed, 0 failed.
  1049  `,
  1050  			expectedCode: 0,
  1051  		},
  1052  		"partial_update_failure": {
  1053  			expectedOut: `main.tftest.hcl... fail
  1054    run "partial"... fail
  1055  
  1056  Warning: Resource targeting is in effect
  1057  
  1058  You are creating a plan with the -target option, which means that the result
  1059  of this plan may not represent all of the changes requested by the current
  1060  configuration.
  1061  
  1062  The -target option is not for routine use, and is provided only for
  1063  exceptional situations such as recovering from errors or mistakes, or when
  1064  OpenTofu specifically suggests to use it as part of an error message.
  1065  
  1066  Warning: Applied changes may be incomplete
  1067  
  1068  The plan was created with the -target option in effect, so some changes
  1069  requested in the configuration may have been ignored and the output values
  1070  may not be fully updated. Run the following command to verify that no other
  1071  changes are pending:
  1072      tofu plan
  1073  	
  1074  Note that the -target option is not suitable for routine use, and is provided
  1075  only for exceptional situations such as recovering from errors or mistakes,
  1076  or when OpenTofu specifically suggests to use it as part of an error message.
  1077  
  1078  Failure! 0 passed, 1 failed.
  1079  `,
  1080  			expectedErr: `
  1081  Error: Unknown condition run
  1082  
  1083    on main.tftest.hcl line 7, in run "partial":
  1084     7:     condition = test_resource.bar.value == "bar"
  1085  
  1086  Condition expression could not be evaluated at this time.
  1087  `,
  1088  			expectedCode: 1,
  1089  		},
  1090  	}
  1091  
  1092  	for file, tc := range tcs {
  1093  		t.Run(file, func(t *testing.T) {
  1094  			td := t.TempDir()
  1095  			testCopyDir(t, testFixturePath(path.Join("test", file)), td)
  1096  			defer testChdir(t, td)()
  1097  
  1098  			provider := testing_command.NewProvider(nil)
  1099  			view, done := testView(t)
  1100  
  1101  			c := &TestCommand{
  1102  				Meta: Meta{
  1103  					testingOverrides: metaOverridesForProvider(provider.Provider),
  1104  					View:             view,
  1105  				},
  1106  			}
  1107  
  1108  			code := c.Run([]string{"-no-color"})
  1109  			output := done(t)
  1110  
  1111  			actualOut, expectedOut := output.Stdout(), tc.expectedOut
  1112  			actualErr, expectedErr := output.Stderr(), tc.expectedErr
  1113  			expectedCode := tc.expectedCode
  1114  
  1115  			if code != expectedCode {
  1116  				t.Errorf("expected status code %d but got %d", expectedCode, code)
  1117  			}
  1118  
  1119  			if diff := cmp.Diff(actualOut, expectedOut); len(diff) > 0 {
  1120  				t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedOut, actualOut, diff)
  1121  			}
  1122  
  1123  			if diff := cmp.Diff(actualErr, expectedErr); len(diff) > 0 {
  1124  				t.Errorf("error didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedErr, actualErr, diff)
  1125  			}
  1126  
  1127  			if provider.ResourceCount() > 0 {
  1128  				t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
  1129  			}
  1130  		})
  1131  	}
  1132  }
  1133  
  1134  func TestTest_LocalVariables(t *testing.T) {
  1135  	td := t.TempDir()
  1136  	testCopyDir(t, testFixturePath(path.Join("test", "pass_with_local_variable")), td)
  1137  	defer testChdir(t, td)()
  1138  
  1139  	provider := testing_command.NewProvider(nil)
  1140  
  1141  	providerSource, close := newMockProviderSource(t, map[string][]string{
  1142  		"test": {"1.0.0"},
  1143  	})
  1144  	defer close()
  1145  
  1146  	streams, done := terminal.StreamsForTesting(t)
  1147  	view := views.NewView(streams)
  1148  	ui := new(cli.MockUi)
  1149  
  1150  	meta := Meta{
  1151  		testingOverrides: metaOverridesForProvider(provider.Provider),
  1152  		Ui:               ui,
  1153  		View:             view,
  1154  		Streams:          streams,
  1155  		ProviderSource:   providerSource,
  1156  	}
  1157  
  1158  	init := &InitCommand{
  1159  		Meta: meta,
  1160  	}
  1161  
  1162  	if code := init.Run(nil); code != 0 {
  1163  		t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
  1164  	}
  1165  
  1166  	c := &TestCommand{
  1167  		Meta: meta,
  1168  	}
  1169  	code := c.Run([]string{"-verbose", "-no-color"})
  1170  	output := done(t)
  1171  
  1172  	if code != 0 {
  1173  		t.Errorf("expected status code 0 but got %d", code)
  1174  	}
  1175  
  1176  	expected := `tests/test.tftest.hcl... pass
  1177    run "first"... pass
  1178  
  1179  
  1180  Outputs:
  1181  
  1182  foo = "bar"
  1183    run "second"... pass
  1184  
  1185  No changes. Your infrastructure matches the configuration.
  1186  
  1187  OpenTofu has compared your real infrastructure against your configuration and
  1188  found no differences, so no changes are needed.
  1189  
  1190  Success! 2 passed, 0 failed.
  1191  `
  1192  
  1193  	actual := output.All()
  1194  
  1195  	if diff := cmp.Diff(actual, expected); len(diff) > 0 {
  1196  		t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff)
  1197  	}
  1198  
  1199  	if provider.ResourceCount() > 0 {
  1200  		t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
  1201  	}
  1202  }
  1203  
  1204  func TestTest_RunBlock(t *testing.T) {
  1205  	tcs := map[string]struct {
  1206  		expected string
  1207  		code     int
  1208  		skip     bool
  1209  	}{
  1210  		"invalid_run_block_name": {
  1211  			expected: `
  1212  Error: Invalid run block name
  1213  
  1214    on tests/main.tftest.hcl line 1, in run "sample run":
  1215     1: run "sample run" {
  1216  
  1217  A name must start with a letter or underscore and may contain only letters,
  1218  digits, underscores, and dashes.
  1219  `,
  1220  			code: 1,
  1221  		},
  1222  	}
  1223  
  1224  	for name, tc := range tcs {
  1225  		t.Run(name, func(t *testing.T) {
  1226  			if tc.skip {
  1227  				t.Skip()
  1228  			}
  1229  
  1230  			file := name
  1231  
  1232  			td := t.TempDir()
  1233  			testCopyDir(t, testFixturePath(path.Join("test", file)), td)
  1234  			defer testChdir(t, td)()
  1235  
  1236  			provider := testing_command.NewProvider(nil)
  1237  			providerSource, close := newMockProviderSource(t, map[string][]string{
  1238  				"test": {"1.0.0"},
  1239  			})
  1240  			defer close()
  1241  
  1242  			streams, _ := terminal.StreamsForTesting(t)
  1243  			view := views.NewView(streams)
  1244  			ui := new(cli.MockUi)
  1245  			meta := Meta{
  1246  				testingOverrides: metaOverridesForProvider(provider.Provider),
  1247  				Ui:               ui,
  1248  				View:             view,
  1249  				Streams:          streams,
  1250  				ProviderSource:   providerSource,
  1251  			}
  1252  
  1253  			init := &InitCommand{
  1254  				Meta: meta,
  1255  			}
  1256  
  1257  			if code := init.Run(nil); code != tc.code {
  1258  				t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
  1259  			}
  1260  		})
  1261  	}
  1262  }