| // 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 |
| } |