blob: 9e652aacec3f357ade92c355e02b789916afe3e5 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package graph
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/dag"
"github.com/hashicorp/terraform/internal/moduletest"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// TestRunTransformer is a GraphTransformer that adds all the test runs,
// and the variables defined in each run block, to the graph.
type TestRunTransformer struct {
opts *graphOptions
}
func (t *TestRunTransformer) Transform(g *terraform.Graph) error {
// Create and add nodes for each run
var nodes []*NodeTestRun
for _, run := range t.opts.File.Runs {
node := &NodeTestRun{run: run, opts: t.opts}
g.Add(node)
nodes = append(nodes, node)
}
// Connect nodes based on dependencies
if diags := t.connectDependencies(g, nodes); diags.HasErrors() {
return tfdiags.DiagnosticsAsError{Diagnostics: diags}
}
// Runs with the same state key inherently depend on each other, so we
// connect them sequentially.
t.connectSameStateRuns(g, nodes)
return nil
}
func (t *TestRunTransformer) connectDependencies(g *terraform.Graph, nodes []*NodeTestRun) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
nodeMap := make(map[string]*NodeTestRun)
// add all nodes to the map. They are initialized to nil,
// and we will update them as we iterate through the nodes in the next loop.
for _, node := range nodes {
nodeMap[node.run.Name] = nil
}
for _, node := range nodes {
nodeMap[node.run.Name] = node // node encountered, so update the map
// check for variable references
varRefs := t.getVariableNames(node.run)
refs, refDiags := node.run.GetReferences()
if refDiags.HasErrors() {
return diags.Append(refDiags)
}
for _, ref := range refs {
switch subj := ref.Subject.(type) {
case addrs.Run:
dependency, ok := nodeMap[subj.Name]
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 = diags.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", subj.Name, diagPrefix),
Subject: ref.SourceRange.ToHCL().Ptr(),
})
continue
}
// This run block exists, but it is after the current run block.
if dependency == nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to unavailable run block",
Detail: fmt.Sprintf("The run block %q has not executed yet. %s", subj.Name, diagPrefix),
Subject: ref.SourceRange.ToHCL().Ptr(),
})
continue
}
g.Connect(dag.BasicEdge(node, dependency))
case addrs.InputVariable:
if _, ok := varRefs[subj.Name]; !ok {
diags = diags.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.", subj.Name),
Subject: ref.SourceRange.ToHCL().Ptr(),
})
}
}
}
}
// If there is a run that has opted out of parallelism, we will connect it
// sequentially to all previous and subsequent runs. This effectively
// divides the parallelizable runs into separate groups, ensuring that
// non-parallelizable runs are executed in sequence with respect to all
// other runs.
for i, node := range nodes {
if node.run.Config.Parallel {
continue
}
// Connect to all previous runs
for j := 0; j < i; j++ {
g.Connect(dag.BasicEdge(node, nodes[j]))
}
// Connect to all subsequent runs
for j := i + 1; j < len(nodes); j++ {
g.Connect(dag.BasicEdge(nodes[j], node))
}
}
return diags
}
func (t *TestRunTransformer) connectSameStateRuns(g *terraform.Graph, nodes []*NodeTestRun) {
stateRuns := make(map[string][]*NodeTestRun)
for _, node := range nodes {
key := node.run.GetStateKey()
stateRuns[key] = append(stateRuns[key], node)
}
for _, runs := range stateRuns {
for i := 1; i < len(runs); i++ {
g.Connect(dag.BasicEdge(runs[i], runs[i-1]))
}
}
}
func (t *TestRunTransformer) getVariableNames(run *moduletest.Run) map[string]struct{} {
set := make(map[string]struct{})
for name := range t.opts.GlobalVars {
set[name] = struct{}{}
}
for name := range run.Config.Variables {
set[name] = struct{}{}
}
for name := range t.opts.File.Config.Variables {
set[name] = struct{}{}
}
for name := range run.ModuleConfig.Module.Variables {
set[name] = struct{}{}
}
return set
}