diff --git a/internal/command/test.go b/internal/command/test.go index 75e3f6344e..cf4381a0af 100644 --- a/internal/command/test.go +++ b/internal/command/test.go @@ -63,7 +63,7 @@ func (c *TestCommand) Run(rawArgs []string) int { loader, err := c.initConfigLoader() diags = diags.Append(err) if err != nil { - c.View.Diagnostics(diags) + view.Diagnostics(nil, nil, diags) return 1 } c.loader = loader @@ -71,7 +71,7 @@ func (c *TestCommand) Run(rawArgs []string) int { config, configDiags := loader.LoadConfigWithTests(".", "tests") diags = diags.Append(configDiags) if configDiags.HasErrors() { - c.View.Diagnostics(diags) + view.Diagnostics(nil, nil, diags) return 1 } @@ -113,7 +113,7 @@ func (c *TestCommand) ExecuteTestSuite(suite *moduletest.Suite, config *configs. diags = diags.Append(err) if err != nil { suite.Status = suite.Status.Merge(moduletest.Error) - c.View.Diagnostics(diags) + view.Diagnostics(nil, nil, diags) return } @@ -121,10 +121,10 @@ func (c *TestCommand) ExecuteTestSuite(suite *moduletest.Suite, config *configs. diags = diags.Append(ctxDiags) if ctxDiags.HasErrors() { suite.Status = suite.Status.Merge(moduletest.Error) - c.View.Diagnostics(diags) + view.Diagnostics(nil, nil, diags) return } - c.View.Diagnostics(diags) // Print out any warnings from the setup. + view.Diagnostics(nil, nil, diags) // Print out any warnings from the setup. var files []string for name := range suite.Files { @@ -148,7 +148,7 @@ func (c *TestCommand) ExecuteTestFile(ctx *terraform.Context, file *moduletest.F if diags.HasErrors() { file.Status = file.Status.Merge(moduletest.Error) view.File(file) - c.View.Diagnostics(diags) + view.Diagnostics(nil, file, diags) return } @@ -166,48 +166,13 @@ func (c *TestCommand) ExecuteTestFile(ctx *terraform.Context, file *moduletest.F if planDiags.HasErrors() { // This is bad, we need to tell the user that we couldn't clean up // and they need to go and manually delete some resources. - - c.Streams.Eprintf("Terraform encountered an error destroying resources created during the test.\n\n") - c.View.Diagnostics(planDiags) - - if state.HasManagedResourceInstanceObjects() { - c.Streams.Eprintf("Terraform left the following resources in state, they need to be cleaned up manually:\n\n") - for _, resource := range state.AllResourceInstanceObjectAddrs() { - if resource.DeposedKey != states.NotDeposed { - c.Streams.Eprintf(" - %s (%s)\n", resource.Instance, resource.DeposedKey) - continue - } - c.Streams.Eprintf(" - %s\n", resource.Instance) - } - } - + view.DestroySummary(planDiags, file, state) return } - c.View.Diagnostics(planDiags) // Print out any warnings from the destroy plan. + view.Diagnostics(nil, file, planDiags) // Print out any warnings from the destroy plan. finalState, applyDiags := ctx.Apply(plan, config) - if applyDiags.HasErrors() { - // This is bad, we need to tell the user that we couldn't clean up - // and they need to go and manually delete some resources. - - c.Streams.Eprintf("Terraform encountered an error destroying resources created during the test.\n\n") - } - c.View.Diagnostics(applyDiags) // Print out any warnings from the destroy apply. - - if finalState.HasManagedResourceInstanceObjects() { - // Then we need to print dialog telling the user they need to clean - // things up, and we should mark the overall test as errored. - - c.Streams.Eprintf("Terraform left the following resources in state, they need to be cleaned up manually:\n\n") - for _, resource := range state.AllResourceInstanceObjectAddrs() { - if resource.DeposedKey != states.NotDeposed { - c.Streams.Eprintf(" - %s (%s)\n", resource.Instance, resource.DeposedKey) - continue - } - c.Streams.Eprintf(" - %s\n", resource.Instance) - } - - } + view.DestroySummary(applyDiags, file, finalState) }() file.Status = file.Status.Merge(moduletest.Pass) @@ -222,7 +187,7 @@ func (c *TestCommand) ExecuteTestFile(ctx *terraform.Context, file *moduletest.F } view.File(file) - c.View.Diagnostics(diags) + view.Diagnostics(nil, file, diags) for _, run := range file.Runs { view.Run(run, file) diff --git a/internal/command/test_test.go b/internal/command/test_test.go index 6f2f067a13..3972ccdc62 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -4,6 +4,15 @@ import ( "path" "strings" "testing" + + "github.com/mitchellh/cli" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + testing_command "github.com/hashicorp/terraform/internal/command/testing" + "github.com/hashicorp/terraform/internal/command/views" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/terminal" ) func TestTest(t *testing.T) { @@ -11,6 +20,7 @@ func TestTest(t *testing.T) { args []string expected string code int + skip bool }{ "simple_pass": { expected: "1 passed, 0 failed.", @@ -24,6 +34,10 @@ func TestTest(t *testing.T) { 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, @@ -32,23 +46,75 @@ func TestTest(t *testing.T) { expected: "2 passed, 0 failed.", code: 0, }, + "expect_failures_checks": { + expected: "1 passed, 0 failed.", + code: 0, + // TODO(liamcervante): Enable this when support for expect_failures + // has been added. + skip: true, + }, + "expect_failures_inputs": { + expected: "1 passed, 0 failed.", + code: 0, + // TODO(liamcervante): Enable this when support for expect_failures + // has been added. + skip: true, + }, + "expect_failures_outputs": { + expected: "1 passed, 0 failed.", + code: 0, + // TODO(liamcervante): Enable this when support for expect_failures + // has been added. + skip: true, + }, + "expect_failures_resources": { + expected: "1 passed, 0 failed.", + code: 0, + // TODO(liamcervante): Enable this when support for expect_failures + // has been added. + skip: true, + }, "simple_fail": { expected: "0 passed, 1 failed.", code: 1, }, + "custom_condition_checks": { + expected: "0 passed, 1 failed.", + code: 1, + // TODO(liamcervante): Enable this, at the moment checks aren't + // causing the tests to fail when they should. Also, it's not + // skipping warnings during the plan when it should. + skip: true, + }, + "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, + }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { + if tc.skip { + t.Skip() + } + td := t.TempDir() testCopyDir(t, testFixturePath(path.Join("test", name)), td) defer testChdir(t, td)() - p := planFixtureProvider() + provider := testing_command.NewProvider(nil) view, done := testView(t) c := &TestCommand{ Meta: Meta{ - testingOverrides: metaOverridesForProvider(p), + testingOverrides: metaOverridesForProvider(provider.Provider), View: view, }, } @@ -63,6 +129,158 @@ func TestTest(t *testing.T) { 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_ProviderAlias(t *testing.T) { + // TODO(liamcervante): Enable this test once we have added support for + // provider aliasing and customisation into the testing framework. + t.Skip() + + 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) { + // TODO(liamcervante): Enable this test once we have added support for + // module customisation into the testing framework. + t.Skip() + + 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()) + } + } +} diff --git a/internal/command/testdata/test/custom_condition_checks/main.tf b/internal/command/testdata/test/custom_condition_checks/main.tf new file mode 100644 index 0000000000..d010847f85 --- /dev/null +++ b/internal/command/testdata/test/custom_condition_checks/main.tf @@ -0,0 +1,15 @@ + +variable "input" { + type = string +} + +resource "test_resource" "resource" { + value = var.input +} + +check "expected_to_fail" { + assert { + condition = test_resource.resource.value != var.input + error_message = "this really should fail" + } +} diff --git a/internal/command/testdata/test/custom_condition_checks/main.tftest b/internal/command/testdata/test/custom_condition_checks/main.tftest new file mode 100644 index 0000000000..d3ead1fe15 --- /dev/null +++ b/internal/command/testdata/test/custom_condition_checks/main.tftest @@ -0,0 +1,5 @@ +variables { + input = "some value" +} + +run "test" {} diff --git a/internal/command/testdata/test/custom_condition_inputs/main.tf b/internal/command/testdata/test/custom_condition_inputs/main.tf new file mode 100644 index 0000000000..027c8e8fdf --- /dev/null +++ b/internal/command/testdata/test/custom_condition_inputs/main.tf @@ -0,0 +1,13 @@ + +variable "input" { + type = string + + validation { + condition = var.input == "something very specific" + error_message = "this should definitely fail" + } +} + +resource "test_resource" "resource" { + value = var.input +} diff --git a/internal/command/testdata/test/custom_condition_inputs/main.tftest b/internal/command/testdata/test/custom_condition_inputs/main.tftest new file mode 100644 index 0000000000..d3ead1fe15 --- /dev/null +++ b/internal/command/testdata/test/custom_condition_inputs/main.tftest @@ -0,0 +1,5 @@ +variables { + input = "some value" +} + +run "test" {} diff --git a/internal/command/testdata/test/custom_condition_outputs/main.tf b/internal/command/testdata/test/custom_condition_outputs/main.tf new file mode 100644 index 0000000000..af05c486a0 --- /dev/null +++ b/internal/command/testdata/test/custom_condition_outputs/main.tf @@ -0,0 +1,13 @@ + +variable "input" { + type = string +} + +output "output" { + value = var.input + + precondition { + condition = var.input == "something incredibly specific" + error_message = "this should fail" + } +} diff --git a/internal/command/testdata/test/custom_condition_outputs/main.tftest b/internal/command/testdata/test/custom_condition_outputs/main.tftest new file mode 100644 index 0000000000..d3ead1fe15 --- /dev/null +++ b/internal/command/testdata/test/custom_condition_outputs/main.tftest @@ -0,0 +1,5 @@ +variables { + input = "some value" +} + +run "test" {} diff --git a/internal/command/testdata/test/custom_condition_resources/main.tf b/internal/command/testdata/test/custom_condition_resources/main.tf new file mode 100644 index 0000000000..0b12d8cda0 --- /dev/null +++ b/internal/command/testdata/test/custom_condition_resources/main.tf @@ -0,0 +1,15 @@ + +variable "input" { + type = string +} + +resource "test_resource" "resource" { + value = var.input + + lifecycle { + postcondition { + condition = self.value != var.input + error_message = "this really should fail" + } + } +} diff --git a/internal/command/testdata/test/custom_condition_resources/main.tftest b/internal/command/testdata/test/custom_condition_resources/main.tftest new file mode 100644 index 0000000000..d3ead1fe15 --- /dev/null +++ b/internal/command/testdata/test/custom_condition_resources/main.tftest @@ -0,0 +1,5 @@ +variables { + input = "some value" +} + +run "test" {} diff --git a/internal/command/testdata/test/expect_failures_checks/main.tf b/internal/command/testdata/test/expect_failures_checks/main.tf new file mode 100644 index 0000000000..d010847f85 --- /dev/null +++ b/internal/command/testdata/test/expect_failures_checks/main.tf @@ -0,0 +1,15 @@ + +variable "input" { + type = string +} + +resource "test_resource" "resource" { + value = var.input +} + +check "expected_to_fail" { + assert { + condition = test_resource.resource.value != var.input + error_message = "this really should fail" + } +} diff --git a/internal/command/testdata/test/expect_failures_checks/main.tftest b/internal/command/testdata/test/expect_failures_checks/main.tftest new file mode 100644 index 0000000000..bcac23b653 --- /dev/null +++ b/internal/command/testdata/test/expect_failures_checks/main.tftest @@ -0,0 +1,9 @@ +variables { + input = "some value" +} + +run "test" { + expect_failures = [ + check.expected_to_fail + ] +} diff --git a/internal/command/testdata/test/expect_failures_inputs/main.tf b/internal/command/testdata/test/expect_failures_inputs/main.tf new file mode 100644 index 0000000000..027c8e8fdf --- /dev/null +++ b/internal/command/testdata/test/expect_failures_inputs/main.tf @@ -0,0 +1,13 @@ + +variable "input" { + type = string + + validation { + condition = var.input == "something very specific" + error_message = "this should definitely fail" + } +} + +resource "test_resource" "resource" { + value = var.input +} diff --git a/internal/command/testdata/test/expect_failures_inputs/main.tftest b/internal/command/testdata/test/expect_failures_inputs/main.tftest new file mode 100644 index 0000000000..afd38e24a4 --- /dev/null +++ b/internal/command/testdata/test/expect_failures_inputs/main.tftest @@ -0,0 +1,9 @@ +variables { + input = "some value" +} + +run "test" { + expect_failures = [ + var.input + ] +} diff --git a/internal/command/testdata/test/expect_failures_outputs/main.tf b/internal/command/testdata/test/expect_failures_outputs/main.tf new file mode 100644 index 0000000000..af05c486a0 --- /dev/null +++ b/internal/command/testdata/test/expect_failures_outputs/main.tf @@ -0,0 +1,13 @@ + +variable "input" { + type = string +} + +output "output" { + value = var.input + + precondition { + condition = var.input == "something incredibly specific" + error_message = "this should fail" + } +} diff --git a/internal/command/testdata/test/expect_failures_outputs/main.tftest b/internal/command/testdata/test/expect_failures_outputs/main.tftest new file mode 100644 index 0000000000..3c6fc5b707 --- /dev/null +++ b/internal/command/testdata/test/expect_failures_outputs/main.tftest @@ -0,0 +1,9 @@ +variables { + input = "some value" +} + +run "test" { + expect_failures = [ + output.output + ] +} diff --git a/internal/command/testdata/test/expect_failures_resources/main.tf b/internal/command/testdata/test/expect_failures_resources/main.tf new file mode 100644 index 0000000000..0b12d8cda0 --- /dev/null +++ b/internal/command/testdata/test/expect_failures_resources/main.tf @@ -0,0 +1,15 @@ + +variable "input" { + type = string +} + +resource "test_resource" "resource" { + value = var.input + + lifecycle { + postcondition { + condition = self.value != var.input + error_message = "this really should fail" + } + } +} diff --git a/internal/command/testdata/test/expect_failures_resources/main.tftest b/internal/command/testdata/test/expect_failures_resources/main.tftest new file mode 100644 index 0000000000..500588a989 --- /dev/null +++ b/internal/command/testdata/test/expect_failures_resources/main.tftest @@ -0,0 +1,15 @@ +variables { + input = "some value" +} + +run "test" { + + assert { + condition = test_resource.resource.value == "some value" + error_message = "since we used a postcondition, it should still have actually created the resource" + } + + expect_failures = [ + test_resource.resource + ] +} diff --git a/internal/command/testdata/test/pass_with_locals/main.tf b/internal/command/testdata/test/pass_with_locals/main.tf index 1085ed7ddd..d398a7b8fa 100644 --- a/internal/command/testdata/test/pass_with_locals/main.tf +++ b/internal/command/testdata/test/pass_with_locals/main.tf @@ -1,7 +1,7 @@ -resource "test_instance" "foo" { - ami = "bar" +resource "test_resource" "foo" { + value = "bar" } locals { - value = test_instance.foo.ami + value = test_resource.foo.value } diff --git a/internal/command/testdata/test/pass_with_locals/main.tftest b/internal/command/testdata/test/pass_with_locals/main.tftest index a7dd27abcb..396eb3021a 100644 --- a/internal/command/testdata/test/pass_with_locals/main.tftest +++ b/internal/command/testdata/test/pass_with_locals/main.tftest @@ -1,6 +1,6 @@ -run "validate_test_instance" { +run "validate_test_resource" { assert { condition = local.value == "bar" - error_message = "invalid ami value" + error_message = "invalid value" } } diff --git a/internal/command/testdata/test/pass_with_outputs/main.tf b/internal/command/testdata/test/pass_with_outputs/main.tf new file mode 100644 index 0000000000..7354f944a1 --- /dev/null +++ b/internal/command/testdata/test/pass_with_outputs/main.tf @@ -0,0 +1,7 @@ +resource "test_resource" "foo" { + value = "bar" +} + +output "value" { + value = test_resource.foo.value +} diff --git a/internal/command/testdata/test/pass_with_outputs/main.tftest b/internal/command/testdata/test/pass_with_outputs/main.tftest new file mode 100644 index 0000000000..bdf84aa556 --- /dev/null +++ b/internal/command/testdata/test/pass_with_outputs/main.tftest @@ -0,0 +1,6 @@ +run "validate_test_resource" { + assert { + condition = output.value == "bar" + error_message = "invalid value" + } +} diff --git a/internal/command/testdata/test/pass_with_variables/main.tf b/internal/command/testdata/test/pass_with_variables/main.tf index 572e59f798..3d98070d87 100644 --- a/internal/command/testdata/test/pass_with_variables/main.tf +++ b/internal/command/testdata/test/pass_with_variables/main.tf @@ -2,6 +2,6 @@ variable "input" { type = string } -resource "test_instance" "foo" { - ami = var.input +resource "test_resource" "foo" { + value = var.input } diff --git a/internal/command/testdata/test/pass_with_variables/main.tftest b/internal/command/testdata/test/pass_with_variables/main.tftest index 6e8af5800a..8068956094 100644 --- a/internal/command/testdata/test/pass_with_variables/main.tftest +++ b/internal/command/testdata/test/pass_with_variables/main.tftest @@ -2,20 +2,20 @@ variables { input = "bar" } -run "validate_test_instance" { +run "validate_test_resource" { assert { - condition = test_instance.foo.ami == "bar" - error_message = "invalid ami value" + condition = test_resource.foo.value == "bar" + error_message = "invalid value" } } -run "validate_test_instance" { +run "validate_test_resource" { variables { input = "zap" } assert { - condition = test_instance.foo.ami == "zap" - error_message = "invalid ami value" + condition = test_resource.foo.value == "zap" + error_message = "invalid value" } } diff --git a/internal/command/testdata/test/plan_then_apply/main.tf b/internal/command/testdata/test/plan_then_apply/main.tf index 2b976525ac..41cc84e5c4 100644 --- a/internal/command/testdata/test/plan_then_apply/main.tf +++ b/internal/command/testdata/test/plan_then_apply/main.tf @@ -1,3 +1,3 @@ -resource "test_instance" "foo" { - ami = "bar" +resource "test_resource" "foo" { + value = "bar" } diff --git a/internal/command/testdata/test/plan_then_apply/main.tftest b/internal/command/testdata/test/plan_then_apply/main.tftest index a2a07bc478..1eda6d4d7c 100644 --- a/internal/command/testdata/test/plan_then_apply/main.tftest +++ b/internal/command/testdata/test/plan_then_apply/main.tftest @@ -1,16 +1,16 @@ -run "validate_test_instance" { +run "validate_test_resource" { command = plan assert { - condition = test_instance.foo.ami == "bar" - error_message = "invalid ami value" + condition = test_resource.foo.value == "bar" + error_message = "invalid value" } } -run "validate_test_instance" { +run "validate_test_resource" { assert { - condition = test_instance.foo.ami == "bar" - error_message = "invalid ami value" + condition = test_resource.foo.value == "bar" + error_message = "invalid value" } } diff --git a/internal/command/testdata/test/simple_fail/main.tf b/internal/command/testdata/test/simple_fail/main.tf index 2b976525ac..41cc84e5c4 100644 --- a/internal/command/testdata/test/simple_fail/main.tf +++ b/internal/command/testdata/test/simple_fail/main.tf @@ -1,3 +1,3 @@ -resource "test_instance" "foo" { - ami = "bar" +resource "test_resource" "foo" { + value = "bar" } diff --git a/internal/command/testdata/test/simple_fail/main.tftest b/internal/command/testdata/test/simple_fail/main.tftest index bd4c45f0e0..319a217673 100644 --- a/internal/command/testdata/test/simple_fail/main.tftest +++ b/internal/command/testdata/test/simple_fail/main.tftest @@ -1,6 +1,6 @@ -run "validate_test_instance" { +run "validate_test_resource" { assert { - condition = test_instance.foo.ami == "zap" - error_message = "invalid ami value" + condition = test_resource.foo.value == "zap" + error_message = "invalid value" } } diff --git a/internal/command/testdata/test/simple_pass/main.tf b/internal/command/testdata/test/simple_pass/main.tf index 2b976525ac..41cc84e5c4 100644 --- a/internal/command/testdata/test/simple_pass/main.tf +++ b/internal/command/testdata/test/simple_pass/main.tf @@ -1,3 +1,3 @@ -resource "test_instance" "foo" { - ami = "bar" +resource "test_resource" "foo" { + value = "bar" } diff --git a/internal/command/testdata/test/simple_pass/main.tftest b/internal/command/testdata/test/simple_pass/main.tftest index 3021f7be19..6feaf3cc5c 100644 --- a/internal/command/testdata/test/simple_pass/main.tftest +++ b/internal/command/testdata/test/simple_pass/main.tftest @@ -1,6 +1,6 @@ -run "validate_test_instance" { +run "validate_test_resource" { assert { - condition = test_instance.foo.ami == "bar" - error_message = "invalid ami value" + condition = test_resource.foo.value == "bar" + error_message = "invalid value" } } diff --git a/internal/command/testdata/test/simple_pass_nested/main.tf b/internal/command/testdata/test/simple_pass_nested/main.tf index 2b976525ac..41cc84e5c4 100644 --- a/internal/command/testdata/test/simple_pass_nested/main.tf +++ b/internal/command/testdata/test/simple_pass_nested/main.tf @@ -1,3 +1,3 @@ -resource "test_instance" "foo" { - ami = "bar" +resource "test_resource" "foo" { + value = "bar" } diff --git a/internal/command/testdata/test/simple_pass_nested/tests/main.tftest b/internal/command/testdata/test/simple_pass_nested/tests/main.tftest index 3021f7be19..6feaf3cc5c 100644 --- a/internal/command/testdata/test/simple_pass_nested/tests/main.tftest +++ b/internal/command/testdata/test/simple_pass_nested/tests/main.tftest @@ -1,6 +1,6 @@ -run "validate_test_instance" { +run "validate_test_resource" { assert { - condition = test_instance.foo.ami == "bar" - error_message = "invalid ami value" + condition = test_resource.foo.value == "bar" + error_message = "invalid value" } } diff --git a/internal/command/testdata/test/with_provider_alias/main.tf b/internal/command/testdata/test/with_provider_alias/main.tf new file mode 100644 index 0000000000..3b60dc01cc --- /dev/null +++ b/internal/command/testdata/test/with_provider_alias/main.tf @@ -0,0 +1,12 @@ + +variable "managed_id" { + type = string +} + +data "test_data_source" "managed_data" { + id = var.managed_id +} + +resource "test_resource" "created" { + value = data.test_data_source.managed_data.value +} diff --git a/internal/command/testdata/test/with_provider_alias/main.tftest b/internal/command/testdata/test/with_provider_alias/main.tftest new file mode 100644 index 0000000000..7529a5ef42 --- /dev/null +++ b/internal/command/testdata/test/with_provider_alias/main.tftest @@ -0,0 +1,37 @@ +provider "test" { + data_prefix = "data" + resource_prefix = "resource" +} + +provider "test" { + alias = "setup" + + # The setup provider will write into the main providers data sources. + resource_prefix = "data" +} + +variables { + managed_id = "B853C121" +} + +run "setup" { + module { + source = "./setup" + } + + variables { + value = "Hello, world!" + id = "B853C121" + } + + providers = { + test = test.setup + } +} + +run "test" { + assert { + condition = test_resource.created.value == "Hello, world!" + error_message = "bad value" + } +} diff --git a/internal/command/testdata/test/with_provider_alias/setup/main.tf b/internal/command/testdata/test/with_provider_alias/setup/main.tf new file mode 100644 index 0000000000..03a9936262 --- /dev/null +++ b/internal/command/testdata/test/with_provider_alias/setup/main.tf @@ -0,0 +1,12 @@ +variable "value" { + type = string +} + +variable "id" { + type = string +} + +resource "test_resource" "managed" { + id = var.id + value = var.value +} diff --git a/internal/command/testdata/test/with_setup_module/main.tf b/internal/command/testdata/test/with_setup_module/main.tf new file mode 100644 index 0000000000..3b60dc01cc --- /dev/null +++ b/internal/command/testdata/test/with_setup_module/main.tf @@ -0,0 +1,12 @@ + +variable "managed_id" { + type = string +} + +data "test_data_source" "managed_data" { + id = var.managed_id +} + +resource "test_resource" "created" { + value = data.test_data_source.managed_data.value +} diff --git a/internal/command/testdata/test/with_setup_module/main.tftest b/internal/command/testdata/test/with_setup_module/main.tftest new file mode 100644 index 0000000000..1f2a6a94aa --- /dev/null +++ b/internal/command/testdata/test/with_setup_module/main.tftest @@ -0,0 +1,21 @@ +variables { + managed_id = "B853C121" +} + +run "setup" { + module { + source = "./setup" + } + + variables { + value = "Hello, world!" + id = "B853C121" + } +} + +run "test" { + assert { + condition = test_resource.created.value == "Hello, world!" + error_message = "bad value" + } +} diff --git a/internal/command/testdata/test/with_setup_module/setup/main.tf b/internal/command/testdata/test/with_setup_module/setup/main.tf new file mode 100644 index 0000000000..49056bbea7 --- /dev/null +++ b/internal/command/testdata/test/with_setup_module/setup/main.tf @@ -0,0 +1,13 @@ +variable "value" { + type = string +} + +variable "id" { + type = string +} + +resource "test_resource" "managed" { + provider = setup + id = var.id + value = var.value +} diff --git a/internal/command/testing/test_provider.go b/internal/command/testing/test_provider.go new file mode 100644 index 0000000000..a1cedddc75 --- /dev/null +++ b/internal/command/testing/test_provider.go @@ -0,0 +1,271 @@ +package testing + +import ( + "fmt" + "path" + "strings" + + "github.com/hashicorp/go-uuid" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +var ( + ProviderSchema = &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "data_prefix": {Type: cty.String, Optional: true}, + "resource_prefix": {Type: cty.String, Optional: true}, + }, + }, + }, + ResourceTypes: map[string]providers.Schema{ + "test_resource": { + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "value": {Type: cty.String, Optional: true}, + }, + }, + }, + }, + DataSources: map[string]providers.Schema{ + "test_data_source": { + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Required: true}, + "value": {Type: cty.String, Computed: true}, + }, + }, + }, + }, + } +) + +// TestProvider is a wrapper around terraform.MockProvider that defines dynamic +// schemas, and keeps track of the resources and data sources that it contains. +type TestProvider struct { + Provider *terraform.MockProvider + + data, resource cty.Value + + Store *ResourceStore +} + +func NewProvider(store *ResourceStore) *TestProvider { + if store == nil { + store = &ResourceStore{ + Data: make(map[string]cty.Value), + } + } + + provider := &TestProvider{ + Provider: new(terraform.MockProvider), + Store: store, + } + + provider.Provider.GetProviderSchemaResponse = ProviderSchema + provider.Provider.ConfigureProviderFn = provider.ConfigureProvider + provider.Provider.PlanResourceChangeFn = provider.PlanResourceChange + provider.Provider.ApplyResourceChangeFn = provider.ApplyResourceChange + provider.Provider.ReadResourceFn = provider.ReadResource + provider.Provider.ReadDataSourceFn = provider.ReadDataSource + + return provider +} + +func (provider *TestProvider) DataPrefix() string { + var prefix string + if !provider.data.IsNull() && provider.data.IsKnown() { + prefix = provider.data.AsString() + } + return prefix +} + +func (provider *TestProvider) SetDataPrefix(prefix string) { + provider.data = cty.StringVal(prefix) +} + +func (provider *TestProvider) GetDataKey(id string) string { + if !provider.data.IsNull() && provider.data.IsKnown() { + return path.Join(provider.data.AsString(), id) + } + return id +} + +func (provider *TestProvider) ResourcePrefix() string { + var prefix string + if !provider.resource.IsNull() && provider.resource.IsKnown() { + prefix = provider.resource.AsString() + } + return prefix +} + +func (provider *TestProvider) SetResourcePrefix(prefix string) { + provider.resource = cty.StringVal(prefix) +} + +func (provider *TestProvider) GetResourceKey(id string) string { + if !provider.resource.IsNull() && provider.resource.IsKnown() { + return path.Join(provider.resource.AsString(), id) + } + return id +} + +func (provider *TestProvider) ResourceString() string { + return provider.string(provider.ResourcePrefix()) +} + +func (provider *TestProvider) ResourceCount() int { + return provider.count(provider.ResourcePrefix()) +} + +func (provider *TestProvider) DataSourceString() string { + return provider.string(provider.DataPrefix()) +} + +func (provider *TestProvider) DataSourceCount() int { + return provider.count(provider.DataPrefix()) +} + +func (provider *TestProvider) count(prefix string) int { + if len(prefix) == 0 { + return len(provider.Store.Data) + } + + count := 0 + for key := range provider.Store.Data { + if strings.HasPrefix(key, prefix) { + count++ + } + } + return count +} + +func (provider *TestProvider) string(prefix string) string { + var keys []string + for key := range provider.Store.Data { + if strings.HasPrefix(key, prefix) { + keys = append(keys, key) + } + } + return strings.Join(keys, ", ") +} + +func (provider *TestProvider) ConfigureProvider(request providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { + provider.resource = request.Config.GetAttr("resource_prefix") + provider.data = request.Config.GetAttr("data_prefix") + return providers.ConfigureProviderResponse{} +} + +func (provider *TestProvider) PlanResourceChange(request providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + if request.ProposedNewState.IsNull() { + // Then this is a delete operation. + return providers.PlanResourceChangeResponse{ + PlannedState: request.ProposedNewState, + } + } + + resource := request.ProposedNewState + if id := resource.GetAttr("id"); !id.IsKnown() || id.IsNull() { + vals := resource.AsValueMap() + vals["id"] = cty.UnknownVal(cty.String) + resource = cty.ObjectVal(vals) + } + + return providers.PlanResourceChangeResponse{ + PlannedState: resource, + } +} + +func (provider *TestProvider) ApplyResourceChange(request providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + if request.PlannedState.IsNull() { + // Then this is a delete operation. + provider.Store.Delete(provider.GetResourceKey(request.PriorState.GetAttr("id").AsString())) + return providers.ApplyResourceChangeResponse{ + NewState: request.PlannedState, + } + } + + resource := request.PlannedState + id := resource.GetAttr("id") + if !id.IsKnown() { + val, err := uuid.GenerateUUID() + if err != nil { + panic(fmt.Errorf("failed to generate uuid: %v", err)) + } + + id = cty.StringVal(val) + + vals := resource.AsValueMap() + vals["id"] = id + resource = cty.ObjectVal(vals) + } + + provider.Store.Put(provider.GetResourceKey(id.AsString()), resource) + return providers.ApplyResourceChangeResponse{ + NewState: resource, + } +} + +func (provider *TestProvider) ReadResource(request providers.ReadResourceRequest) providers.ReadResourceResponse { + var diags tfdiags.Diagnostics + + id := request.PriorState.GetAttr("id").AsString() + resource := provider.Store.Get(provider.GetResourceKey(id)) + if resource == cty.NilVal { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "not found", fmt.Sprintf("%s does not exist", id))) + } + + return providers.ReadResourceResponse{ + NewState: resource, + Diagnostics: diags, + } +} + +func (provider *TestProvider) ReadDataSource(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + var diags tfdiags.Diagnostics + + id := request.Config.GetAttr("id").AsString() + resource := provider.Store.Get(provider.GetDataKey(id)) + if resource == cty.NilVal { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "not found", fmt.Sprintf("%s does not exist", id))) + } + + return providers.ReadDataSourceResponse{ + State: resource, + Diagnostics: diags, + } +} + +// ResourceStore manages a set of cty.Value resources that can be shared between +// TestProvider providers. +type ResourceStore struct { + Data map[string]cty.Value +} + +func (store *ResourceStore) Delete(key string) cty.Value { + if resource, ok := store.Data[key]; ok { + delete(store.Data, key) + return resource + } + return cty.NilVal +} + +func (store *ResourceStore) Get(key string) cty.Value { + if resource, ok := store.Data[key]; ok { + return resource + } + return cty.NilVal +} + +func (store *ResourceStore) Put(key string, resource cty.Value) cty.Value { + old := store.Get(key) + store.Data[key] = resource + return old +}