From eeeb05b85c2ad62439a6647c810db973a90cf305 Mon Sep 17 00:00:00 2001 From: Samsondeen Dare Date: Thu, 16 Jan 2025 22:52:51 +0100 Subject: [PATCH] check input variable references too --- internal/backend/local/test.go | 2 +- internal/command/test_test.go | 8 ++ internal/terraformtest/test_graph_builder.go | 6 +- internal/terraformtest/transform_test_run.go | 105 +++++++++++++------ 4 files changed, 87 insertions(+), 34 deletions(-) diff --git a/internal/backend/local/test.go b/internal/backend/local/test.go index 24a6612c232c..73e0f9439cbb 100644 --- a/internal/backend/local/test.go +++ b/internal/backend/local/test.go @@ -322,7 +322,7 @@ func (runner *TestFileRunner) Test(file *moduletest.File) { } // Build the graph for the file. - b := terraformtest.TestGraphBuilder{File: file} + b := terraformtest.TestGraphBuilder{File: file, GlobalVars: runner.VariableCaches.GlobalVariables} graph, diags := b.Build(addrs.RootModuleInstance) file.Diagnostics = file.Diagnostics.Append(diags) if diags.HasErrors() { diff --git a/internal/command/test_test.go b/internal/command/test_test.go index 7c747107d73d..8300fe157b69 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -1571,6 +1571,14 @@ Failure! 0 passed, 1 failed. } expectedErr := ` +Error: Reference to unavailable variable + + on main.tftest.hcl line 15, in run "test": + 15: input_one = var.notreal + +The input variable "notreal" is not available to the current run block. You +can only reference variables defined at the file or global levels. + Error: Reference to unavailable run block on main.tftest.hcl line 16, in run "test": diff --git a/internal/terraformtest/test_graph_builder.go b/internal/terraformtest/test_graph_builder.go index 3273a079a376..bb794aa370a2 100644 --- a/internal/terraformtest/test_graph_builder.go +++ b/internal/terraformtest/test_graph_builder.go @@ -7,6 +7,7 @@ import ( "log" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/moduletest" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" @@ -16,7 +17,8 @@ import ( // a terraform test file. The file may contain multiple runs, and each run may have // dependencies on other runs. type TestGraphBuilder struct { - File *moduletest.File + File *moduletest.File + GlobalVars map[string]backendrun.UnparsedVariableValue } // See GraphBuilder @@ -31,7 +33,7 @@ func (b *TestGraphBuilder) Build(path addrs.ModuleInstance) (*terraform.Graph, t // See GraphBuilder func (b *TestGraphBuilder) Steps() []terraform.GraphTransformer { steps := []terraform.GraphTransformer{ - &TestRunTransformer{File: b.File}, + &TestRunTransformer{File: b.File, globalVars: b.GlobalVars}, &CloseTestGraphTransformer{}, &terraform.TransitiveReductionTransformer{}, } diff --git a/internal/terraformtest/transform_test_run.go b/internal/terraformtest/transform_test_run.go index 9bc443683357..c44cc041b258 100644 --- a/internal/terraformtest/transform_test_run.go +++ b/internal/terraformtest/transform_test_run.go @@ -82,46 +82,71 @@ func (t *TestRunTransformer) connectDependencies(g *terraform.Graph, nodes []*No } for _, node := range nodes { nodeMap[node.run.Name] = node // node encountered, so update the map + + // check for variable references + varRefs, err := t.getVariableNames(node.run) + if err != nil { + errs = append(errs, err) + continue + } + refs, err := getRefs(node.run) if err != nil { return err } for _, ref := range refs { subjectStr := ref.Subject.String() - if !strings.HasPrefix(subjectStr, "run.") { - continue - } - runName := strings.TrimPrefix(subjectStr, "run.") - if runName == "" { - continue - } - dependency, ok := nodeMap[runName] - diagPrefix := "You can only reference run blocks that are in the same test file and will execute before the current run block." - // Then this is a made up run block, and it doesn't exist at all. - if !ok { - diags := tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Reference to unknown run block", - Detail: fmt.Sprintf("The run block %q does not exist within this test file. %s", runName, diagPrefix), - Subject: ref.SourceRange.ToHCL().Ptr(), - }) - errs = append(errs, tfdiags.NonFatalError{Diagnostics: diags}) + switch true { + case strings.HasPrefix(subjectStr, "run."): + runName := strings.TrimPrefix(subjectStr, "run.") + if runName == "" { + continue + } + dependency, ok := nodeMap[runName] + diagPrefix := "You can only reference run blocks that are in the same test file and will execute before the current run block." + // Then this is a made up run block, and it doesn't exist at all. + if !ok { + diags := tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to unknown run block", + Detail: fmt.Sprintf("The run block %q does not exist within this test file. %s", runName, diagPrefix), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + errs = append(errs, tfdiags.NonFatalError{Diagnostics: diags}) + continue + } + + // This run block exists, but it is after the current run block. + if dependency == nil { + diags := tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to unavailable run block", + Detail: fmt.Sprintf("The run block %q has not executed yet. %s", runName, diagPrefix), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + errs = append(errs, tfdiags.NonFatalError{Diagnostics: diags}) + continue + } + + g.Connect(dag.BasicEdge(node, dependency)) + case strings.HasPrefix(subjectStr, "var."): + varName := strings.TrimPrefix(subjectStr, "var.") + if varName == "" { + continue + } + if _, ok := varRefs[varName]; !ok { + diags := tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to unavailable variable", + 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.", varName), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + errs = append(errs, tfdiags.NonFatalError{Diagnostics: diags}) + } + default: continue } - // This run block exists, but it is after the current run block. - if dependency == nil { - diags := tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Reference to unavailable run block", - Detail: fmt.Sprintf("The run block %q has not executed yet. %s", runName, diagPrefix), - Subject: ref.SourceRange.ToHCL().Ptr(), - }) - errs = append(errs, tfdiags.NonFatalError{Diagnostics: diags}) - continue - } - - g.Connect(dag.BasicEdge(node, dependency)) } } return errors.Join(errs...) @@ -156,6 +181,24 @@ func getRefs(run *moduletest.Run) ([]*addrs.Reference, error) { return refs, nil } +func (t *TestRunTransformer) getVariableNames(run *moduletest.Run) (map[string]struct{}, error) { + set := make(map[string]struct{}) + for name := range t.globalVars { + set[name] = struct{}{} + } + for name := range run.Config.Variables { + set[name] = struct{}{} + } + + for name := range t.File.Config.Variables { + set[name] = struct{}{} + } + for name := range run.ModuleConfig.Module.Variables { + set[name] = struct{}{} + } + return set, nil +} + // -------------------------------------------------------- CloseTestGraphTransformer -------------------------------------------------------- // CloseTestGraphTransformer is a GraphTransformer that adds a root to the graph.