Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/v1.13/ENHANCEMENTS-20250602-152211.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: ENHANCEMENTS
body: '`terraform test`: Test authors can now specify definitions for external variables that are referenced within test files directly within the test file itself.'
time: 2025-06-02T15:22:11.453413+02:00
custom:
Issue: "37195"
5 changes: 5 additions & 0 deletions .changes/v1.13/UPGRADE NOTES-20250602-152009.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: UPGRADE NOTES
body: '`terraform test`: External variables referenced within test files should now be accompanied by a `variable` definition block within the test file. This is optional, but users with complex external variables may see error diagnostics without the additional variable definition.'
time: 2025-06-02T15:20:09.188388+02:00
custom:
Issue: "37195"
30 changes: 17 additions & 13 deletions internal/backend/local/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,26 +110,30 @@ func (runner *TestSuiteRunner) Test() (moduletest.Status, tfdiags.Diagnostics) {
}

file := suite.Files[name]
evalCtx := graph.NewEvalContext(graph.EvalContextOpts{
CancelCtx: runner.CancelledCtx,
StopCtx: runner.StoppedCtx,
Verbose: runner.Verbose,
Render: runner.View,
})

// TODO(liamcervante): Do the variables in the EvalContextTransformer
// as well as the run blocks.

currentGlobalVariables := runner.GlobalVariables
if filepath.Dir(file.Name) == runner.TestingDirectory {
// If the file is in the test directory, we'll use the union of the
// global variables and the global test variables.
currentGlobalVariables = testDirectoryGlobalVariables
}
evalCtx.VariableCaches = hcltest.NewVariableCaches(func(vc *hcltest.VariableCaches) {
maps.Copy(vc.GlobalVariables, currentGlobalVariables)
vc.FileVariables = file.Config.Variables

evalCtx := graph.NewEvalContext(graph.EvalContextOpts{
CancelCtx: runner.CancelledCtx,
StopCtx: runner.StoppedCtx,
Verbose: runner.Verbose,
Render: runner.View,
VariableCache: &hcltest.VariableCache{

// TODO(liamcervante): Do the variables in the EvalContextTransformer
// as well as the run blocks.

ExternalVariableValues: currentGlobalVariables,
TestFileVariableDefinitions: file.Config.VariableDefinitions,
TestFileVariableExpressions: file.Config.Variables,
},
})

fileRunner := &TestFileRunner{
Suite: runner,
EvalContext: evalCtx,
Expand Down Expand Up @@ -248,7 +252,7 @@ func (runner *TestFileRunner) Test(file *moduletest.File) {
// Build the graph for the file.
b := graph.TestGraphBuilder{
File: file,
GlobalVars: runner.EvalContext.VariableCaches.GlobalVariables,
GlobalVars: runner.EvalContext.VariableCache.ExternalVariableValues,
ContextOpts: runner.Suite.Opts,
}
g, diags := b.Build()
Expand Down
5 changes: 5 additions & 0 deletions internal/command/test_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,11 @@ func TestTest_Runs(t *testing.T) {
expectedOut: []string{"1 passed, 0 failed."},
code: 0,
},
"with-default-variables": {
args: []string{"-var=input_two=universe"},
expectedOut: []string{"1 passed, 0 failed."},
code: 0,
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
Expand Down
12 changes: 12 additions & 0 deletions internal/command/testdata/test/with-default-variables/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

variable "input_one" {
type = string
}

variable "input_two" {
type = string
}

resource "test_resource" "resource" {
value = "${var.input_one} - ${var.input_two}"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

variable "input_one" {
type = string
default = "hello"
}

variable "input_two" {
type = string
default = "world" // we will override this an external value
}

run "test" {
assert {
condition = test_resource.resource.value == "hello - universe"
error_message = "bad concatenation"
}
}
51 changes: 49 additions & 2 deletions internal/configs/test_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ const (
// A test file is made up of a sequential list of run blocks, each designating
// a command to execute and a series of validations to check after the command.
type TestFile struct {

// VariableDefinitions allows users to specify variables that should be
// provided externally (eg. from the command line or external files).
//
// This conflicts with the Variables block. Variables specified in the
// VariableDefinitions cannot also be specified within the Variables block.
VariableDefinitions map[string]*Variable

// Variables defines a set of global variable definitions that should be set
// for every run block within the test file.
Variables map[string]hcl.Expression
Expand Down Expand Up @@ -327,8 +335,9 @@ type TestRunOptions struct {
func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) {
var diags hcl.Diagnostics
tf := &TestFile{
Providers: make(map[string]*Provider),
Overrides: addrs.MakeMap[addrs.Targetable, *Override](),
VariableDefinitions: make(map[string]*Variable),
Providers: make(map[string]*Provider),
Overrides: addrs.MakeMap[addrs.Targetable, *Override](),
}

// we need to retrieve the file config block first, because the run blocks
Expand Down Expand Up @@ -370,6 +379,31 @@ func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) {
}
runBlockNames[run.Name] = run.DeclRange

case "variable":
variable, variableDiags := decodeVariableBlock(block, false)
diags = append(diags, variableDiags...)
if !variableDiags.HasErrors() {
if existing, exists := tf.VariableDefinitions[variable.Name]; exists {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate \"variable\" block names",
Detail: fmt.Sprintf("This test file already has a variable named %s defined at %s.", variable.Name, existing.DeclRange),
Subject: variable.DeclRange.Ptr(),
})
continue
}
tf.VariableDefinitions[variable.Name] = variable

if existing, exists := tf.Variables[variable.Name]; exists {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate \"variable\" block names",
Detail: fmt.Sprintf("This test file already has a variable named %s defined at %s.", variable.Name, existing.Range()),
Subject: variable.DeclRange.Ptr(),
})
}
}

case "variables":
if tf.Variables != nil {
diags = append(diags, &hcl.Diagnostic{
Expand All @@ -388,6 +422,15 @@ func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) {
diags = append(diags, varsDiags...)
for _, v := range vars {
tf.Variables[v.Name] = v.Expr

if existing, exists := tf.VariableDefinitions[v.Name]; exists {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate \"variable\" block names",
Detail: fmt.Sprintf("This test file already has a variable named %s defined at %s.", v.Name, v.Range),
Subject: existing.DeclRange.Ptr(),
})
}
}
case "provider":
provider, providerDiags := decodeProviderBlock(block, true)
Expand Down Expand Up @@ -888,6 +931,10 @@ var testFileSchema = &hcl.BodySchema{
Type: "mock_provider",
LabelNames: []string{"name"},
},
{
Type: "variable",
LabelNames: []string{"name"},
},
{
Type: "variables",
},
Expand Down
17 changes: 7 additions & 10 deletions internal/moduletest/graph/eval_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ type TestFileState struct {
// within the suite.
// The struct provides concurrency-safe access to the various maps it contains.
type EvalContext struct {
VariableCaches *hcltest.VariableCaches
VariableCache *hcltest.VariableCache

// runOutputs is a mapping from run addresses to cty object values
// representing the collected output values from the module under test.
Expand Down Expand Up @@ -84,10 +84,11 @@ type EvalContext struct {
}

type EvalContextOpts struct {
Verbose bool
Render views.Test
CancelCtx context.Context
StopCtx context.Context
Verbose bool
Render views.Test
CancelCtx context.Context
StopCtx context.Context
VariableCache *hcltest.VariableCache
}

// NewEvalContext constructs a new graph evaluation context for use in
Expand All @@ -104,7 +105,7 @@ func NewEvalContext(opts EvalContextOpts) *EvalContext {
providersLock: sync.Mutex{},
FileStates: make(map[string]*TestFileState),
stateLock: sync.Mutex{},
VariableCaches: hcltest.NewVariableCaches(),
VariableCache: opts.VariableCache,
cancelContext: cancelCtx,
cancelFunc: cancel,
stopContext: stopCtx,
Expand Down Expand Up @@ -326,10 +327,6 @@ func (ec *EvalContext) GetOutputs() map[addrs.Run]cty.Value {
return outputCopy
}

func (ec *EvalContext) GetCache(run *moduletest.Run) *hcltest.VariableCache {
return ec.VariableCaches.GetCache(run.Name, run.ModuleConfig)
}

// ProviderExists returns true if the provider exists for the run inside the context.
func (ec *EvalContext) ProviderExists(run *moduletest.Run, key string) bool {
ec.providersLock.Lock()
Expand Down
4 changes: 2 additions & 2 deletions internal/moduletest/graph/transform_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func TransformConfigForRun(ctx *EvalContext, run *moduletest.Run, file *modulete
AliasRange: ref.InChild.AliasRange,
Config: &hcltest.ProviderConfig{
Original: testProvider.Config,
VariableCache: ctx.GetCache(run),
VariableCache: ctx.VariableCache,
AvailableRunOutputs: runOutputs,
},
Mock: testProvider.Mock,
Expand All @@ -114,7 +114,7 @@ func TransformConfigForRun(ctx *EvalContext, run *moduletest.Run, file *modulete
AliasRange: provider.AliasRange,
Config: &hcltest.ProviderConfig{
Original: provider.Config,
VariableCache: ctx.GetCache(run),
VariableCache: ctx.VariableCache,
AvailableRunOutputs: runOutputs,
},
Mock: provider.Mock,
Expand Down
51 changes: 27 additions & 24 deletions internal/moduletest/graph/variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,8 @@ func (n *NodeTestRun) GetVariables(ctx *EvalContext, includeWarnings bool) (terr
refs, refDiags := langrefs.ReferencesInExpr(addrs.ParseRefFromTestingScope, expr)
for _, ref := range refs {
if addr, ok := ref.Subject.(addrs.InputVariable); ok {
cache := ctx.GetCache(run)

value, valueDiags := cache.GetFileVariable(addr.Name)
diags = diags.Append(valueDiags)
if value != nil {
requiredValues[addr.Name] = value.Value
continue
}

// Otherwise, it might be a global variable.
value, valueDiags = cache.GetGlobalVariable(addr.Name)
diags = diags.Append(valueDiags)
if value != nil {
if value, valueDiags := ctx.VariableCache.GetVariableValue(addr.Name); value != nil {
diags = diags.Append(valueDiags)
requiredValues[addr.Name] = value.Value
continue
}
Expand Down Expand Up @@ -118,18 +107,14 @@ func (n *NodeTestRun) GetVariables(ctx *EvalContext, includeWarnings bool) (terr
continue
}

// Otherwise, we'll get it from the cache as a file-level or global
// variable.
cache := ctx.GetCache(run)

value, valueDiags := cache.GetFileVariable(variable)
diags = diags.Append(valueDiags)
if value != nil {
values[variable] = value
if _, exists := run.ModuleConfig.Module.Variables[variable]; exists {
// We'll deal with this later.
continue
}

value, valueDiags = cache.GetGlobalVariable(variable)
// Otherwise, we'll get it from the cache as a file-level or global
// variable.
value, valueDiags := ctx.VariableCache.GetVariableValue(variable)
diags = diags.Append(valueDiags)
if value != nil {
values[variable] = value
Expand All @@ -143,11 +128,29 @@ func (n *NodeTestRun) GetVariables(ctx *EvalContext, includeWarnings bool) (terr

for name, variable := range run.ModuleConfig.Module.Variables {
if _, exists := values[name]; exists {
// Then we've provided a variable for this. It's all good.
// Then we've provided a variable for this explicitly. It's all
// good.
continue
}

// Otherwise, we're going to give these variables a value. They'll be
// The user might have provided a value for this externally or at the
// file level, so we can also just pass it through.

if ctx.VariableCache.HasVariableDefinition(variable.Name) {
if value, valueDiags := ctx.VariableCache.GetVariableValue(variable.Name); value != nil {
diags = diags.Append(valueDiags)
values[name] = value
continue
}
} else {
if value, valueDiags := ctx.VariableCache.EvaluateExternalVariable(name, variable); value != nil {
diags = diags.Append(valueDiags)
values[name] = value
continue
}
}

// Finally, we're going to give these variables a value. They'll be
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this comment is now out of date

// processed by the Terraform graph and provided a default value later
// if they have one.

Expand Down
4 changes: 2 additions & 2 deletions internal/moduletest/hcl/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,15 @@ func (p *ProviderConfig) transformAttributes(originals hcl.Attributes) (hcl.Attr
refs, _ := langrefs.ReferencesInExpr(addrs.ParseRefFromTestingScope, original.Expr)
for _, ref := range refs {
if addr, ok := ref.Subject.(addrs.InputVariable); ok {
value, valueDiags := p.VariableCache.GetFileVariable(addr.Name)
value, valueDiags := p.VariableCache.GetVariableValue(addr.Name)
diags = append(diags, valueDiags.ToHCL()...)
if value != nil {
availableVariables[addr.Name] = value.Value
continue
}

// If the variable wasn't a file variable, it might be a global.
value, valueDiags = p.VariableCache.GetGlobalVariable(addr.Name)
value, valueDiags = p.VariableCache.GetVariableValue(addr.Name)
diags = append(diags, valueDiags.ToHCL()...)
if value != nil {
availableVariables[addr.Name] = value.Value
Expand Down
10 changes: 5 additions & 5 deletions internal/moduletest/hcl/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,19 +184,19 @@ func TestProviderConfig(t *testing.T) {
outputs[addr] = cty.ObjectVal(attrs)
}

variableCaches := NewVariableCaches(func(vc *VariableCaches) {
vc.FileVariables = func() map[string]hcl.Expression {
variableCache := &VariableCache{
TestFileVariableExpressions: func() map[string]hcl.Expression {
variables := make(map[string]hcl.Expression)
for name, value := range tc.variables {
variables[name] = hcl.StaticExpr(value, hcl.Range{})
}
return variables
}()
})
}(),
}

config := ProviderConfig{
Original: file.Body,
VariableCache: variableCaches.GetCache("test", nil),
VariableCache: variableCache,
AvailableRunOutputs: outputs,
}

Expand Down
Loading