opentofu/internal/command/test_test.go
Oleksandr Levchenkov 5f8eee4708
add simulated state serialization between tofu test runs (#2274)
Signed-off-by: ollevche <ollevche@gmail.com>
2024-12-10 16:34:25 +02:00

1467 lines
38 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package command
import (
"path"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/mitchellh/cli"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
testing_command "github.com/opentofu/opentofu/internal/command/testing"
"github.com/opentofu/opentofu/internal/command/views"
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/providers"
"github.com/opentofu/opentofu/internal/terminal"
)
func TestTest(t *testing.T) {
tcs := map[string]struct {
override string
args []string
expected string
code int
skip bool
}{
"simple_pass": {
expected: "1 passed, 0 failed.",
code: 0,
},
"simple_pass_nested": {
expected: "1 passed, 0 failed.",
code: 0,
},
"simple_pass_nested_alternate": {
args: []string{"-test-directory", "other"},
expected: "1 passed, 0 failed.",
code: 0,
},
"simple_pass_very_nested": {
args: []string{"-test-directory", "tests/subdir"},
expected: "1 passed, 0 failed.",
code: 0,
},
"simple_pass_very_nested_alternate": {
override: "simple_pass_very_nested",
args: []string{"-test-directory", "./tests/subdir"},
expected: "1 passed, 0 failed.",
code: 0,
},
"pass_with_locals": {
expected: "1 passed, 0 failed.",
code: 0,
},
"pass_with_outputs": {
expected: "1 passed, 0 failed.",
code: 0,
},
"pass_with_variables": {
expected: "2 passed, 0 failed.",
code: 0,
},
"plan_then_apply": {
expected: "2 passed, 0 failed.",
code: 0,
},
"expect_failures_checks": {
expected: "1 passed, 0 failed.",
code: 0,
},
"expect_failures_inputs": {
expected: "1 passed, 0 failed.",
code: 0,
},
"expect_failures_outputs": {
expected: "1 passed, 0 failed.",
code: 0,
},
"expect_runtime_check_fail": {
expected: "0 passed, 1 failed.",
code: 1,
},
"expect_runtime_check_pass_with_expect": {
expected: "1 passed, 0 failed.",
code: 0,
},
"expect_runtime_check_pass_command_plan_expected": {
expected: "1 passed, 0 failed.",
code: 0,
},
"expect_runtime_check_fail_command_plan": {
expected: "0 passed, 1 failed.",
code: 1,
},
"expect_failures_resources": {
expected: "1 passed, 0 failed.",
code: 0,
},
"multiple_files": {
expected: "2 passed, 0 failed",
code: 0,
},
"multiple_files_with_filter": {
override: "multiple_files",
args: []string{"-filter=one.tftest.hcl"},
expected: "1 passed, 0 failed",
code: 0,
},
"variables": {
expected: "2 passed, 0 failed",
code: 0,
},
"variables_overridden": {
override: "variables",
args: []string{"-var=input=foo"},
expected: "1 passed, 1 failed",
code: 1,
},
"simple_fail": {
expected: "0 passed, 1 failed.",
code: 1,
},
"custom_condition_checks": {
expected: "0 passed, 1 failed.",
code: 1,
},
"custom_condition_inputs": {
expected: "0 passed, 1 failed.",
code: 1,
},
"custom_condition_outputs": {
expected: "0 passed, 1 failed.",
code: 1,
},
"custom_condition_resources": {
expected: "0 passed, 1 failed.",
code: 1,
},
"no_providers_in_main": {
expected: "1 passed, 0 failed",
code: 0,
},
"default_variables": {
expected: "1 passed, 0 failed.",
code: 0,
},
"undefined_variables": {
expected: "1 passed, 0 failed.",
code: 0,
},
"refresh_only": {
expected: "3 passed, 0 failed.",
code: 0,
},
"null_output": {
expected: "1 passed, 0 failed.",
code: 0,
},
"pass_with_tests_dir_variables": {
expected: "1 passed, 0 failed.",
code: 0,
},
"override_with_tests_dir_variables": {
expected: "1 passed, 0 failed.",
code: 0,
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
if tc.skip {
t.Skip()
}
file := name
if len(tc.override) > 0 {
file = tc.override
}
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", file)), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
view, done := testView(t)
c := &TestCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
View: view,
},
}
code := c.Run(tc.args)
output := done(t)
if code != tc.code {
t.Errorf("expected status code %d but got %d", tc.code, code)
}
if !strings.Contains(output.Stdout(), tc.expected) {
t.Errorf("output didn't contain expected string:\n\n%s", output.All())
}
if provider.ResourceCount() > 0 {
t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
}
})
}
}
func TestTest_Full_Output(t *testing.T) {
tcs := map[string]struct {
override string
args []string
expected string
code int
skip bool
}{
"broken_no_valid_hcl": {
expected: "Unsupported block type",
code: 1,
},
"expect_runtime_check_fail_command_plan": {
expected: "Check block assertion known after apply",
code: 1,
},
"broken_wrong_block_resource": {
expected: "Blocks of type \"resource\" are not expected here.",
code: 1,
},
"broken_wrong_block_data": {
expected: "Blocks of type \"data\" are not expected here.",
code: 1,
},
"broken_wrong_block_output": {
expected: "Blocks of type \"output\" are not expected here.",
code: 1,
},
"broken_wrong_block_check": {
expected: "Blocks of type \"check\" are not expected here.",
code: 1,
},
"not_exists_output": {
expected: "Error: Reference to undeclared output value",
args: []string{"-no-color"},
code: 1,
},
"refresh_conflicting_config": {
expected: "Incompatible plan options",
code: 1,
},
"is_sorted": {
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",
code: 0,
args: []string{"-no-color"},
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
if tc.skip {
t.Skip()
}
file := name
if len(tc.override) > 0 {
file = tc.override
}
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", file)), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
view, done := testView(t)
c := &TestCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
View: view,
},
}
code := c.Run(tc.args)
output := done(t)
if code != tc.code {
t.Errorf("expected status code %d but got %d", tc.code, code)
}
if !strings.Contains(output.All(), tc.expected) {
t.Errorf("output didn't contain expected string:\n\n%s \n\n----\n\nexpected: %s", output.All(), tc.expected)
}
if provider.ResourceCount() > 0 {
t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
}
})
}
}
func TestTest_Interrupt(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", "with_interrupt")), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
view, done := testView(t)
interrupt := make(chan struct{})
provider.Interrupt = interrupt
c := &TestCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
View: view,
ShutdownCh: interrupt,
},
}
c.Run(nil)
output := done(t).All()
if !strings.Contains(output, "Interrupt received") {
t.Errorf("output didn't produce the right output:\n\n%s", output)
}
if provider.ResourceCount() > 0 {
// we asked for a nice stop in this one, so it should still have tidied everything up.
t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
}
}
func TestTest_DoubleInterrupt(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", "with_double_interrupt")), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
view, done := testView(t)
interrupt := make(chan struct{})
provider.Interrupt = interrupt
c := &TestCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
View: view,
ShutdownCh: interrupt,
},
}
c.Run(nil)
output := done(t).All()
if !strings.Contains(output, "Two interrupts received") {
t.Errorf("output didn't produce the right output:\n\n%s", output)
}
cleanupMessage := `OpenTofu was interrupted while executing main.tftest.hcl, and may not have
performed the expected cleanup operations.
OpenTofu has already created the following resources from the module under
test:
- test_resource.primary
- test_resource.secondary
- test_resource.tertiary`
// It's really important that the above message is printed, so we're testing
// for it specifically and making sure it contains all the resources.
if !strings.Contains(output, cleanupMessage) {
t.Errorf("output didn't produce the right output:\n\n%s", output)
}
// This time the test command shouldn't have cleaned up the resource because
// of the hard interrupt.
if provider.ResourceCount() != 3 {
// we asked for a nice stop in this one, so it should still have tidied everything up.
t.Errorf("should not have deleted all resources on completion but left %v", provider.ResourceString())
}
}
func TestTest_ProviderAlias(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", "with_provider_alias")), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
providerSource, close := newMockProviderSource(t, map[string][]string{
"test": {"1.0.0"},
})
defer close()
streams, done := terminal.StreamsForTesting(t)
view := views.NewView(streams)
ui := new(cli.MockUi)
meta := Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
Ui: ui,
View: view,
Streams: streams,
ProviderSource: providerSource,
}
init := &InitCommand{
Meta: meta,
}
if code := init.Run(nil); code != 0 {
t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
}
command := &TestCommand{
Meta: meta,
}
code := command.Run(nil)
output := done(t)
printedOutput := false
if code != 0 {
printedOutput = true
t.Errorf("expected status code 0 but got %d: %s", code, output.All())
}
if provider.ResourceCount() > 0 {
if !printedOutput {
t.Errorf("should have deleted all resources on completion but left %s\n\n%s", provider.ResourceString(), output.All())
} else {
t.Errorf("should have deleted all resources on completion but left %s", provider.ResourceString())
}
}
}
func TestTest_ModuleDependencies(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", "with_setup_module")), td)
defer testChdir(t, td)()
// Our two providers will share a common set of values to make things
// easier.
store := &testing_command.ResourceStore{
Data: make(map[string]cty.Value),
}
// We set it up so the module provider will update the data sources
// available to the core mock provider.
test := testing_command.NewProvider(store)
setup := testing_command.NewProvider(store)
test.SetDataPrefix("data")
test.SetResourcePrefix("resource")
// Let's make the setup provider write into the data for test provider.
setup.SetResourcePrefix("data")
providerSource, close := newMockProviderSource(t, map[string][]string{
"test": {"1.0.0"},
"setup": {"1.0.0"},
})
defer close()
streams, done := terminal.StreamsForTesting(t)
view := views.NewView(streams)
ui := new(cli.MockUi)
meta := Meta{
testingOverrides: &testingOverrides{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): providers.FactoryFixed(test.Provider),
addrs.NewDefaultProvider("setup"): providers.FactoryFixed(setup.Provider),
},
},
Ui: ui,
View: view,
Streams: streams,
ProviderSource: providerSource,
}
init := &InitCommand{
Meta: meta,
}
if code := init.Run(nil); code != 0 {
t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
}
command := &TestCommand{
Meta: meta,
}
code := command.Run(nil)
output := done(t)
printedOutput := false
if code != 0 {
printedOutput = true
t.Errorf("expected status code 0 but got %d: %s", code, output.All())
}
if test.ResourceCount() > 0 {
if !printedOutput {
printedOutput = true
t.Errorf("should have deleted all resources on completion but left %s\n\n%s", test.ResourceString(), output.All())
} else {
t.Errorf("should have deleted all resources on completion but left %s", test.ResourceString())
}
}
if setup.ResourceCount() > 0 {
if !printedOutput {
t.Errorf("should have deleted all resources on completion but left %s\n\n%s", setup.ResourceString(), output.All())
} else {
t.Errorf("should have deleted all resources on completion but left %s", setup.ResourceString())
}
}
}
func TestTest_CatchesErrorsBeforeDestroy(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", "invalid_default_state")), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
view, done := testView(t)
c := &TestCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
View: view,
},
}
code := c.Run([]string{"-no-color"})
output := done(t)
if code != 1 {
t.Errorf("expected status code 0 but got %d", code)
}
expectedOut := `main.tftest.hcl... fail
run "test"... fail
Failure! 0 passed, 1 failed.
`
expectedErr := `
Error: No value for required variable
on main.tf line 2:
2: variable "input" {
The root module input variable "input" is not set, and has no default value.
Use a -var or -var-file command line argument to provide a value for this
variable.
`
actualOut := output.Stdout()
actualErr := output.Stderr()
if diff := cmp.Diff(actualOut, expectedOut); len(diff) > 0 {
t.Errorf("std out didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedOut, actualOut, diff)
}
if diff := cmp.Diff(actualErr, expectedErr); len(diff) > 0 {
t.Errorf("std err didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedErr, actualErr, diff)
}
if provider.ResourceCount() > 0 {
t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
}
}
func TestTest_Verbose(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", "plan_then_apply")), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
view, done := testView(t)
c := &TestCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
View: view,
},
}
code := c.Run([]string{"-verbose", "-no-color"})
output := done(t)
if code != 0 {
t.Errorf("expected status code 0 but got %d", code)
}
expected := `main.tftest.hcl... pass
run "validate_test_resource"... pass
OpenTofu used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
OpenTofu will perform the following actions:
# test_resource.foo will be created
+ resource "test_resource" "foo" {
+ id = "constant_value"
+ value = "bar"
}
Plan: 1 to add, 0 to change, 0 to destroy.
run "validate_test_resource"... pass
# test_resource.foo:
resource "test_resource" "foo" {
id = "constant_value"
value = "bar"
}
Success! 2 passed, 0 failed.
`
actual := output.All()
if diff := cmp.Diff(actual, expected); len(diff) > 0 {
t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff)
}
if provider.ResourceCount() > 0 {
t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
}
}
func TestTest_ValidatesBeforeExecution(t *testing.T) {
tcs := map[string]struct {
expectedOut string
expectedErr string
}{
"invalid": {
expectedOut: `main.tftest.hcl... fail
run "invalid"... fail
Failure! 0 passed, 1 failed.
`,
expectedErr: `
Error: Invalid ` + "`expect_failures`" + ` reference
on main.tftest.hcl line 5, in run "invalid":
5: local.my_value,
You cannot expect failures from local.my_value. You can only expect failures
from checkable objects such as input variables, output values, check blocks,
managed resources and data sources.
`,
},
"invalid-module": {
expectedOut: `main.tftest.hcl... fail
run "invalid"... fail
run "test"... skip
Failure! 0 passed, 1 failed, 1 skipped.
`,
expectedErr: `
Error: Reference to undeclared input variable
on setup/main.tf line 3, in resource "test_resource" "setup":
3: value = var.not_real // Oh no!
An input variable with the name "not_real" has not been declared. This
variable can be declared with a variable "not_real" {} block.
`,
},
"missing-provider": {
expectedOut: `main.tftest.hcl... fail
run "passes_validation"... fail
Failure! 0 passed, 1 failed.
`,
expectedErr: `
Error: Provider configuration not present
To work with test_resource.secondary its original provider configuration at
provider["registry.opentofu.org/hashicorp/test"].secondary is required, but
it has been removed. This occurs when a provider configuration is removed
while objects created by that provider still exist in the state. Re-add the
provider configuration to destroy test_resource.secondary, after which you
can remove the provider configuration again.
`,
},
"missing-provider-in-run-block": {
expectedOut: `main.tftest.hcl... fail
run "passes_validation"... fail
Failure! 0 passed, 1 failed.
`,
expectedErr: `
Error: Provider configuration not present
To work with test_resource.secondary its original provider configuration at
provider["registry.opentofu.org/hashicorp/test"].secondary is required, but
it has been removed. This occurs when a provider configuration is removed
while objects created by that provider still exist in the state. Re-add the
provider configuration to destroy test_resource.secondary, after which you
can remove the provider configuration again.
`,
},
"missing-provider-in-test-module": {
expectedOut: `main.tftest.hcl... fail
run "passes_validation_primary"... pass
run "passes_validation_secondary"... fail
Failure! 1 passed, 1 failed.
`,
expectedErr: `
Error: Provider configuration not present
To work with test_resource.secondary its original provider configuration at
provider["registry.opentofu.org/hashicorp/test"].secondary is required, but
it has been removed. This occurs when a provider configuration is removed
while objects created by that provider still exist in the state. Re-add the
provider configuration to destroy test_resource.secondary, after which you
can remove the provider configuration again.
`,
},
}
for file, tc := range tcs {
t.Run(file, func(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", file)), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
providerSource, close := newMockProviderSource(t, map[string][]string{
"test": {"1.0.0"},
})
defer close()
streams, done := terminal.StreamsForTesting(t)
view := views.NewView(streams)
ui := new(cli.MockUi)
meta := Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
Ui: ui,
View: view,
Streams: streams,
ProviderSource: providerSource,
}
init := &InitCommand{
Meta: meta,
}
if code := init.Run(nil); code != 0 {
t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
}
c := &TestCommand{
Meta: meta,
}
code := c.Run([]string{"-no-color"})
output := done(t)
if code != 1 {
t.Errorf("expected status code 1 but got %d", code)
}
actualOut, expectedOut := output.Stdout(), tc.expectedOut
actualErr, expectedErr := output.Stderr(), tc.expectedErr
if diff := cmp.Diff(actualOut, expectedOut); len(diff) > 0 {
t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedOut, actualOut, diff)
}
if diff := cmp.Diff(actualErr, expectedErr); len(diff) > 0 {
t.Errorf("error didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedErr, actualErr, diff)
}
if provider.ResourceCount() > 0 {
t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
}
})
}
}
func TestTest_Modules(t *testing.T) {
tcs := map[string]struct {
expected string
code int
skip bool
}{
"pass_module_with_no_resource": {
expected: "main.tftest.hcl... pass\n run \"run\"... pass\n\nSuccess! 1 passed, 0 failed.\n",
code: 0,
},
"with_nested_setup_modules": {
expected: "main.tftest.hcl... pass\n run \"load_module\"... pass\n\nSuccess! 1 passed, 0 failed.\n",
code: 0,
},
"with_verify_module": {
expected: "main.tftest.hcl... pass\n run \"test\"... pass\n run \"verify\"... pass\n\nSuccess! 2 passed, 0 failed.\n",
code: 0,
},
"only_modules": {
expected: "main.tftest.hcl... pass\n run \"first\"... pass\n run \"second\"... pass\n\nSuccess! 2 passed, 0 failed.\n",
code: 0,
},
"variables_reference": {
expected: "main.tftest.hcl... pass\n run \"setup\"... pass\n run \"test\"... pass\n\nSuccess! 2 passed, 0 failed.\n",
code: 0,
},
"destroyed_mod_outputs": {
expected: "main.tftest.hcl... pass\n run \"first_apply\"... pass\n run \"second_apply\"... pass\n\nSuccess! 2 passed, 0 failed.\n",
code: 0,
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
if tc.skip {
t.Skip()
}
file := name
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", file)), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
providerSource, close := newMockProviderSource(t, map[string][]string{
"test": {"1.0.0"},
})
defer close()
streams, done := terminal.StreamsForTesting(t)
view := views.NewView(streams)
ui := new(cli.MockUi)
meta := Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
Ui: ui,
View: view,
Streams: streams,
ProviderSource: providerSource,
}
init := &InitCommand{
Meta: meta,
}
if code := init.Run(nil); code != 0 {
t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
}
command := &TestCommand{
Meta: meta,
}
code := command.Run([]string{"-no-color"})
output := done(t)
printedOutput := false
if code != tc.code {
printedOutput = true
t.Errorf("expected status code %d but got %d: %s", tc.code, code, output.All())
}
actual := output.All()
if diff := cmp.Diff(actual, tc.expected); len(diff) > 0 {
t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", tc.expected, actual, diff)
}
if provider.ResourceCount() > 0 {
if !printedOutput {
t.Errorf("should have deleted all resources on completion but left %s\n\n%s", provider.ResourceString(), output.All())
} else {
t.Errorf("should have deleted all resources on completion but left %s", provider.ResourceString())
}
}
if provider.DataSourceCount() > 0 {
if !printedOutput {
t.Errorf("should have deleted all data sources on completion but left %s\n\n%s", provider.DataSourceString(), output.All())
} else {
t.Errorf("should have deleted all data sources on completion but left %s", provider.DataSourceString())
}
}
})
}
}
func TestTest_StatePropagation(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", "state_propagation")), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
providerSource, close := newMockProviderSource(t, map[string][]string{
"test": {"1.0.0"},
})
defer close()
streams, done := terminal.StreamsForTesting(t)
view := views.NewView(streams)
ui := new(cli.MockUi)
meta := Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
Ui: ui,
View: view,
Streams: streams,
ProviderSource: providerSource,
}
init := &InitCommand{
Meta: meta,
}
if code := init.Run(nil); code != 0 {
t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
}
c := &TestCommand{
Meta: meta,
}
code := c.Run([]string{"-verbose", "-no-color"})
output := done(t)
if code != 0 {
t.Errorf("expected status code 0 but got %d", code)
}
expected := `main.tftest.hcl... pass
run "initial_apply_example"... pass
# test_resource.module_resource:
resource "test_resource" "module_resource" {
id = "df6h8as9"
value = "start"
}
run "initial_apply"... pass
# test_resource.resource:
resource "test_resource" "resource" {
id = "598318e0"
value = "start"
}
run "plan_second_example"... pass
OpenTofu used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
OpenTofu will perform the following actions:
# test_resource.second_module_resource will be created
+ resource "test_resource" "second_module_resource" {
+ id = "b6a1d8cb"
+ value = "start"
}
Plan: 1 to add, 0 to change, 0 to destroy.
run "plan_update"... pass
OpenTofu used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
~ update in-place
OpenTofu will perform the following actions:
# test_resource.resource will be updated in-place
~ resource "test_resource" "resource" {
id = "598318e0"
~ value = "start" -> "update"
}
Plan: 0 to add, 1 to change, 0 to destroy.
run "plan_update_example"... pass
OpenTofu used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
~ update in-place
OpenTofu will perform the following actions:
# test_resource.module_resource will be updated in-place
~ resource "test_resource" "module_resource" {
id = "df6h8as9"
~ value = "start" -> "update"
}
Plan: 0 to add, 1 to change, 0 to destroy.
Success! 5 passed, 0 failed.
`
actual := output.All()
if diff := cmp.Diff(actual, expected); len(diff) > 0 {
t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff)
}
if provider.ResourceCount() > 0 {
t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
}
}
func TestTest_PartialUpdates(t *testing.T) {
tcs := map[string]struct {
expectedOut string
expectedErr string
expectedCode int
}{
"partial_updates": {
expectedOut: `main.tftest.hcl... pass
run "first"... pass
Warning: Resource targeting is in effect
You are creating a plan with either the -target option or the -exclude
option, which means that the result of this plan may not represent all of the
changes requested by the current configuration.
The -target and -exclude options are not for routine use, and are provided
only for exceptional situations such as recovering from errors or mistakes,
or when OpenTofu specifically suggests to use it as part of an error message.
Warning: Applied changes may be incomplete
The plan was created with the -target or the -exclude option in effect, so
some changes requested in the configuration may have been ignored and the
output values may not be fully updated. Run the following command to verify
that no other changes are pending:
tofu plan
Note that the -target and -exclude options are not suitable for routine use,
and are provided only for exceptional situations such as recovering from
errors or mistakes, or when OpenTofu specifically suggests to use it as part
of an error message.
run "second"... pass
Success! 2 passed, 0 failed.
`,
expectedCode: 0,
},
"partial_update_failure": {
expectedOut: `main.tftest.hcl... fail
run "partial"... fail
Warning: Resource targeting is in effect
You are creating a plan with either the -target option or the -exclude
option, which means that the result of this plan may not represent all of the
changes requested by the current configuration.
The -target and -exclude options are not for routine use, and are provided
only for exceptional situations such as recovering from errors or mistakes,
or when OpenTofu specifically suggests to use it as part of an error message.
Warning: Applied changes may be incomplete
The plan was created with the -target or the -exclude option in effect, so
some changes requested in the configuration may have been ignored and the
output values may not be fully updated. Run the following command to verify
that no other changes are pending:
tofu plan
Note that the -target and -exclude options are not suitable for routine use,
and are provided only for exceptional situations such as recovering from
errors or mistakes, or when OpenTofu specifically suggests to use it as part
of an error message.
Failure! 0 passed, 1 failed.
`,
expectedErr: `
Error: Unknown condition run
on main.tftest.hcl line 7, in run "partial":
7: condition = test_resource.bar.value == "bar"
Condition expression could not be evaluated at this time.
`,
expectedCode: 1,
},
}
for file, tc := range tcs {
t.Run(file, func(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", file)), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
view, done := testView(t)
c := &TestCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
View: view,
},
}
code := c.Run([]string{"-no-color"})
output := done(t)
actualOut, expectedOut := output.Stdout(), tc.expectedOut
actualErr, expectedErr := output.Stderr(), tc.expectedErr
expectedCode := tc.expectedCode
if code != expectedCode {
t.Errorf("expected status code %d but got %d", expectedCode, code)
}
if diff := cmp.Diff(actualOut, expectedOut); len(diff) > 0 {
t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedOut, actualOut, diff)
}
if diff := cmp.Diff(actualErr, expectedErr); len(diff) > 0 {
t.Errorf("error didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedErr, actualErr, diff)
}
if provider.ResourceCount() > 0 {
t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
}
})
}
}
func TestTest_LocalVariables(t *testing.T) {
tcs := map[string]struct {
expected string
code int
skip bool
}{
"pass_with_local_variable": {
expected: `tests/test.tftest.hcl... pass
run "first"... pass
Outputs:
foo = "bar"
run "second"... pass
No changes. Your infrastructure matches the configuration.
OpenTofu has compared your real infrastructure against your configuration and
found no differences, so no changes are needed.
Success! 2 passed, 0 failed.
`,
code: 0,
},
"pass_var_inside_variables": {
expected: `main.tftest.hcl... pass
run "first"... pass
Outputs:
sss = "false"
Success! 1 passed, 0 failed.
`,
code: 0,
},
"pass_var_with_default_value_inside_variables": {
expected: `main.tftest.hcl... pass
run "first"... pass
Outputs:
sss = "true"
Success! 1 passed, 0 failed.
`,
code: 0,
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
if tc.skip {
t.Skip()
}
file := name
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", file)), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
providerSource, providerClose := newMockProviderSource(t, map[string][]string{
"test": {"1.0.0"},
})
defer providerClose()
streams, done := terminal.StreamsForTesting(t)
view := views.NewView(streams)
ui := new(cli.MockUi)
meta := Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
Ui: ui,
View: view,
Streams: streams,
ProviderSource: providerSource,
}
init := &InitCommand{
Meta: meta,
}
if code := init.Run(nil); code != 0 {
t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
}
command := &TestCommand{
Meta: meta,
}
code := command.Run([]string{"-verbose", "-no-color"})
output := done(t)
if code != tc.code {
t.Errorf("expected status code %d but got %d: %s", tc.code, code, output.All())
}
actual := output.All()
if diff := cmp.Diff(actual, tc.expected); len(diff) > 0 {
t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", tc.expected, actual, diff)
}
})
}
}
func TestTest_InvalidLocalVariables(t *testing.T) {
tcs := map[string]struct {
contains []string
notContains []string
code int
skip bool
}{
"invalid_variable_warning_expected": {
contains: []string{
"Warning: Invalid Variable in test file",
"Error: Invalid value for variable",
"This was checked by the validation rule at main.tf:5,3-13.",
"This was checked by the validation rule at main.tf:14,3-13.",
},
code: 1,
},
"invalid_variable_warning_no_expected": {
contains: []string{
"Error: Invalid value for variable",
"This was checked by the validation rule at main.tf:5,3-13.",
"This was checked by the validation rule at main.tf:14,3-13.",
},
notContains: []string{
"Warning: Invalid Variable in test file",
},
code: 1,
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
if tc.skip {
t.Skip()
}
file := name
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", file)), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
providerSource, providerClose := newMockProviderSource(t, map[string][]string{
"test": {"1.0.0"},
})
defer providerClose()
streams, done := terminal.StreamsForTesting(t)
view := views.NewView(streams)
ui := new(cli.MockUi)
meta := Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
Ui: ui,
View: view,
Streams: streams,
ProviderSource: providerSource,
}
init := &InitCommand{
Meta: meta,
}
if code := init.Run(nil); code != 0 {
t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
}
command := &TestCommand{
Meta: meta,
}
code := command.Run([]string{"-verbose", "-no-color"})
output := done(t)
if code != tc.code {
t.Errorf("expected status code %d but got %d: %s", tc.code, code, output.All())
}
actual := output.All()
for _, containsString := range tc.contains {
if !strings.Contains(actual, containsString) {
t.Errorf("expected '%s' in output but didn't find it: \n%s", containsString, output.All())
}
}
for _, notContainsString := range tc.notContains {
if strings.Contains(actual, notContainsString) {
t.Errorf("expected not to find '%s' in output: \n%s", notContainsString, output.All())
}
}
})
}
}
func TestTest_RunBlock(t *testing.T) {
tcs := map[string]struct {
expected string
code int
skip bool
}{
"invalid_run_block_name": {
expected: `
Error: Invalid run block name
on tests/main.tftest.hcl line 1, in run "sample run":
1: run "sample run" {
A name must start with a letter or underscore and may contain only letters,
digits, underscores, and dashes.
`,
code: 1,
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
if tc.skip {
t.Skip()
}
file := name
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", file)), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
providerSource, close := newMockProviderSource(t, map[string][]string{
"test": {"1.0.0"},
})
defer close()
streams, _ := terminal.StreamsForTesting(t)
view := views.NewView(streams)
ui := new(cli.MockUi)
meta := Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
Ui: ui,
View: view,
Streams: streams,
ProviderSource: providerSource,
}
init := &InitCommand{
Meta: meta,
}
if code := init.Run(nil); code != tc.code {
t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
}
})
}
}
// TestTest_MockProviderValidation checks if tofu test runs proper validation for
// mock_provider. Even if provider schema has required fields, tofu test should
// ignore it completely, because the provider is mocked.
func TestTest_MockProviderValidation(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("test/mock_provider_validation"), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
providerSource, closePS := newMockProviderSource(t, map[string][]string{
"test": {"1.0.0"},
})
defer closePS()
provider.Provider.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
Provider: providers.Schema{
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"required_field": {
Type: cty.String,
Required: true,
},
},
},
},
ResourceTypes: map[string]providers.Schema{
"test_resource": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {
Type: cty.String,
Optional: true,
},
"computed_value": {
Type: cty.String,
Computed: true,
},
},
},
},
},
}
streams, _ := terminal.StreamsForTesting(t)
view := views.NewView(streams)
ui := new(cli.MockUi)
meta := Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
Ui: ui,
View: view,
Streams: streams,
ProviderSource: providerSource,
}
testCmd := &TestCommand{
Meta: meta,
}
if code := testCmd.Run(nil); code != 0 {
t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
}
}