From 3bea1171aff32504ea5e95ba7b129f35f8d92cd8 Mon Sep 17 00:00:00 2001 From: Liam Cervante Date: Tue, 1 Aug 2023 09:59:29 +0200 Subject: [PATCH] test framework: expand variables available to test assertions (#33611) --- internal/command/test.go | 79 +++++++++++++++++-- internal/command/test_test.go | 8 ++ .../testdata/test/default_variables/main.tf | 5 ++ .../test/default_variables/main.tftest.hcl | 7 ++ .../testdata/test/undefined_variables/main.tf | 5 ++ .../test/undefined_variables/main.tftest.hcl | 13 +++ 6 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 internal/command/testdata/test/default_variables/main.tf create mode 100644 internal/command/testdata/test/default_variables/main.tftest.hcl create mode 100644 internal/command/testdata/test/undefined_variables/main.tf create mode 100644 internal/command/testdata/test/undefined_variables/main.tftest.hcl diff --git a/internal/command/test.go b/internal/command/test.go index e98b65449f..7a547e5255 100644 --- a/internal/command/test.go +++ b/internal/command/test.go @@ -9,6 +9,8 @@ import ( "strings" "time" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/command/arguments" @@ -507,7 +509,9 @@ func (runner *TestRunner) ExecuteTestRun(mgr *TestStateManager, run *moduletest. run.Diagnostics = run.Diagnostics.Append(diags) } - variables, diags := buildInputVariablesForAssertions(run, file, globals) + variables, reset, diags := prepareInputVariablesForAssertions(config, run, file, globals) + defer reset() + run.Diagnostics = run.Diagnostics.Append(diags) if diags.HasErrors() { run.Status = moduletest.Error @@ -943,7 +947,7 @@ func (manager *TestStateManager) cleanupStates(file *moduletest.File, globals ma // buildInputVariablesForTest creates a terraform.InputValues mapping for // variable values that are relevant to the config being tested. // -// Crucially, it differs from buildInputVariablesForAssertions in that it only +// Crucially, it differs from prepareInputVariablesForAssertions in that it only // includes variables that are reference by the config and not everything that // is defined within the test run block and test file. func buildInputVariablesForTest(run *moduletest.Run, file *moduletest.File, config *configs.Config, globals map[string]backend.UnparsedVariableValue) (terraform.InputValues, tfdiags.Diagnostics) { @@ -986,15 +990,19 @@ func buildInputVariablesForTest(run *moduletest.Run, file *moduletest.File, conf return backend.ParseVariableValues(variables, config.Module.Variables) } -// buildInputVariablesForAssertions creates a terraform.InputValues mapping that -// contains all the variables defined for a given run and file, alongside any -// unset variables that have defaults within the provided config. +// prepareInputVariablesForAssertions creates a terraform.InputValues mapping +// that contains all the variables defined for a given run and file, alongside +// any unset variables that have defaults within the provided config. // // Crucially, it differs from buildInputVariablesForTest in that the returned // input values include all variables available even if they are not defined // within the config. This allows the assertions to refer to variables defined // solely within the test file, and not only those within the configuration. -func buildInputVariablesForAssertions(run *moduletest.Run, file *moduletest.File, globals map[string]backend.UnparsedVariableValue) (terraform.InputValues, tfdiags.Diagnostics) { +// +// In addition, it modifies the provided config so that any variables that are +// available are also defined in the config. It returns a function that resets +// the config which must be called so the config can be reused going forward. +func prepareInputVariablesForAssertions(config *configs.Config, run *moduletest.Run, file *moduletest.File, globals map[string]backend.UnparsedVariableValue) (terraform.InputValues, func(), tfdiags.Diagnostics) { variables := make(map[string]backend.UnparsedVariableValue) if run != nil { @@ -1030,6 +1038,9 @@ func buildInputVariablesForAssertions(run *moduletest.Run, file *moduletest.File variables[name] = variable } + // We've gathered all the values we have, let's convert them into + // terraform.InputValues so they can be passed into the Terraform graph. + inputs := make(terraform.InputValues, len(variables)) var diags tfdiags.Diagnostics for name, variable := range variables { @@ -1037,5 +1048,59 @@ func buildInputVariablesForAssertions(run *moduletest.Run, file *moduletest.File diags = diags.Append(valueDiags) inputs[name] = value } - return inputs, diags + + // Next, we're going to apply any default values from the configuration. + // We do this after the conversion into terraform.InputValues, as the + // defaults have already been converted into cty.Value objects. + + for name, variable := range config.Module.Variables { + if _, exists := variables[name]; exists { + // Then we don't want to apply the default for this variable as we + // already have a value. + continue + } + + if variable.Default != cty.NilVal { + inputs[name] = &terraform.InputValue{ + Value: variable.Default, + SourceType: terraform.ValueFromConfig, + SourceRange: tfdiags.SourceRangeFromHCL(variable.DeclRange), + } + } + } + + // Finally, we're going to do a some modifications to the config. + // If we have got variable values from the test file we need to make sure + // they have an equivalent entry in the configuration. We're going to do + // that dynamically here. + + // First, take a backup of the existing configuration so we can easily + // restore it later. + currentVars := make(map[string]*configs.Variable) + for name, variable := range config.Module.Variables { + currentVars[name] = variable + } + + // Next, let's go through our entire inputs and add any that aren't already + // defined into the config. + for name, value := range inputs { + if _, exists := config.Module.Variables[name]; exists { + continue + } + + config.Module.Variables[name] = &configs.Variable{ + Name: name, + Type: value.Value.Type(), + ConstraintType: value.Value.Type(), + DeclRange: value.SourceRange.ToHCL(), + } + } + + // We return our input values, a function that will reset the variables + // within the config so it can be used again, and any diagnostics reporting + // variables that we couldn't parse. + + return inputs, func() { + config.Module.Variables = currentVars + }, diags } diff --git a/internal/command/test_test.go b/internal/command/test_test.go index 5f767b605d..0077435187 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -124,6 +124,14 @@ func TestTest(t *testing.T) { 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, + }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { diff --git a/internal/command/testdata/test/default_variables/main.tf b/internal/command/testdata/test/default_variables/main.tf new file mode 100644 index 0000000000..ce7d9e83d8 --- /dev/null +++ b/internal/command/testdata/test/default_variables/main.tf @@ -0,0 +1,5 @@ + +variable "input" { + type = string + default = "Hello, world!" +} diff --git a/internal/command/testdata/test/default_variables/main.tftest.hcl b/internal/command/testdata/test/default_variables/main.tftest.hcl new file mode 100644 index 0000000000..a6292d0923 --- /dev/null +++ b/internal/command/testdata/test/default_variables/main.tftest.hcl @@ -0,0 +1,7 @@ + +run "applies_defaults" { + assert { + condition = var.input == "Hello, world!" + error_message = "should have applied default value" + } +} diff --git a/internal/command/testdata/test/undefined_variables/main.tf b/internal/command/testdata/test/undefined_variables/main.tf new file mode 100644 index 0000000000..ce7d9e83d8 --- /dev/null +++ b/internal/command/testdata/test/undefined_variables/main.tf @@ -0,0 +1,5 @@ + +variable "input" { + type = string + default = "Hello, world!" +} diff --git a/internal/command/testdata/test/undefined_variables/main.tftest.hcl b/internal/command/testdata/test/undefined_variables/main.tftest.hcl new file mode 100644 index 0000000000..d3009c37bc --- /dev/null +++ b/internal/command/testdata/test/undefined_variables/main.tftest.hcl @@ -0,0 +1,13 @@ + +variables { + # config_free isn't defined in the config, but we'll + # still let users refer to it within the assertions. + config_free = "Hello, world!" +} + +run "applies_defaults" { + assert { + condition = var.input == var.config_free + error_message = "should have applied default value" + } +}