From b76481b1abca0af2417871945eb56c7e63e51b99 Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Mon, 19 Feb 2024 15:20:11 +0100 Subject: [PATCH] test: allow using global variables in suite-level variable definitions Closes #34534 --- internal/backend/local/test.go | 75 +++++++++++++++++++++++++++--- internal/moduletest/hcl/context.go | 2 +- 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/internal/backend/local/test.go b/internal/backend/local/test.go index 235ea6f610a6..9149000c7e34 100644 --- a/internal/backend/local/test.go +++ b/internal/backend/local/test.go @@ -244,7 +244,10 @@ type TestFileRunner struct { // variables within run blocks. PriorOutputs map[addrs.Run]cty.Value + // globalVariables are globally defined variables, e.g. through tfvars or CLI flags globalVariables map[string]backend.UnparsedVariableValue + // fileVariables are defined in the variables section of a test file + fileVariables map[string]hcl.Expression } // TestFileState is a helper struct that just maps a run block to the state that @@ -1008,11 +1011,16 @@ func (runner *TestFileRunner) GetVariables(config *configs.Config, run *modulete } // Finally, we'll check to see which variables are actually defined within - // the configuration. for name := range config.Module.Variables { relevantVariables[name] = true } + // We also include all global variables since they might be used in the + // suites variables section + for name := range runner.globalVariables { + relevantVariables[name] = true + } + // Now we know which variables are actually needed by this run block. // We're going to run over all the sets of variables we have access to: @@ -1033,6 +1041,7 @@ func (runner *TestFileRunner) GetVariables(config *configs.Config, run *modulete parsingMode := configs.VariableParseHCL cfg, exists := config.Module.Variables[name] + if exists { // Unless we have some configuration that can actually tell us // what parsing mode to use. @@ -1057,29 +1066,80 @@ func (runner *TestFileRunner) GetVariables(config *configs.Config, run *modulete values[name] = value } - // Second, we'll check the run level variables. + // Second, we'll check the file level variables + var exprs []hcl.Expression + for _, expr := range runner.fileVariables { + exprs = append(exprs, expr) + } + + // Preformat the variables we've processed already - these will be made + // available to the eval context. + variables := make(map[string]cty.Value) + for name, value := range values { + variables[name] = value.Value + } + + ctx, ctxDiags := hcltest.EvalContext(hcltest.TargetRunBlock, exprs, variables, runner.PriorOutputs) + diags = diags.Append(ctxDiags) + + var failedContext bool + if ctxDiags.HasErrors() { + // If we couldn't build the context, we won't actually process these + // variables. Instead, we'll fill them with an empty value but still + // make a note that the user did provide them. + failedContext = true + } + + for name, expr := range runner.fileVariables { + if !relevantVariables[name] { + // We'll add a warning for this. Since we're right in the run block + // users shouldn't be defining variables that are not relevant. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Value for undeclared variable", + Detail: fmt.Sprintf("The module under test does not declare a variable named %q, but it is declared in run block %q.", name, run.Name), + Subject: expr.Range().Ptr(), + }) + continue + } + + value := cty.NilVal + if !failedContext { + var valueDiags hcl.Diagnostics + value, valueDiags = expr.Value(ctx) + diags = diags.Append(valueDiags) + } + + values[name] = &terraform.InputValue{ + Value: value, + SourceType: terraform.ValueFromConfig, + SourceRange: tfdiags.SourceRangeFromHCL(expr.Range()), + } + } + + // Third, we'll check the run level variables. // This is a bit more complicated, as the run level variables can reference // previously defined variables. // Preload the available expressions, we're going to validate them when we // build the context. - var exprs []hcl.Expression + exprs = []hcl.Expression{} for _, expr := range run.Config.Variables { exprs = append(exprs, expr) } // Preformat the variables we've processed already - these will be made // available to the eval context. - variables := make(map[string]cty.Value) + variables = make(map[string]cty.Value) for name, value := range values { variables[name] = value.Value } - ctx, ctxDiags := hcltest.EvalContext(hcltest.TargetRunBlock, exprs, variables, runner.PriorOutputs) + ctx, ctxDiags = hcltest.EvalContext(hcltest.TargetRunBlock, exprs, variables, runner.PriorOutputs) diags = diags.Append(ctxDiags) - var failedContext bool + failedContext = false if ctxDiags.HasErrors() { // If we couldn't build the context, we won't actually process these // variables. Instead, we'll fill them with an empty value but still @@ -1260,8 +1320,9 @@ func (runner *TestFileRunner) initVariables(file *moduletest.File) { runner.globalVariables[name] = value } } + runner.fileVariables = make(map[string]hcl.Expression) for name, expr := range file.Config.Variables { - runner.globalVariables[name] = unparsedTestVariableValue{expr} + runner.fileVariables[name] = expr } } diff --git a/internal/moduletest/hcl/context.go b/internal/moduletest/hcl/context.go index 471dd08f804d..a446325c9753 100644 --- a/internal/moduletest/hcl/context.go +++ b/internal/moduletest/hcl/context.go @@ -132,7 +132,7 @@ func EvalContext(target EvalContextTarget, expressions []hcl.Expression, availab if _, exists := availableVariables[addr.Name]; !exists { // This variable reference doesn't exist. - detail := fmt.Sprintf("The input variable %q is not available to the current run block. You can only reference variables defined at the file or global levels when populating the variables block within a run block.", addr.Name) + detail := fmt.Sprintf("The input variable %q is not available to the current context. Within the variables block of a run block you can only reference variables defined at the file or global levels; within the variables block of a suite you can only reference variables defined at the global levels.", addr.Name) if availableRunOutputs == nil { detail = fmt.Sprintf("The input variable %q is not available to the current provider configuration. You can only reference variables defined at the file or global levels within provider configurations.", addr.Name) }