Skip to content

Commit 53eb2da

Browse files
author
Liam Cervante
committed
terraform test: check specific dependencies before skipping run blocks
1 parent 643266d commit 53eb2da

File tree

11 files changed

+154
-38
lines changed

11 files changed

+154
-38
lines changed

internal/command/test_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,11 @@ func TestTest_Runs(t *testing.T) {
339339
expectedOut: []string{"1 passed, 0 failed."},
340340
code: 0,
341341
},
342+
"parallel-errors": {
343+
expectedOut: []string{"1 passed, 1 failed, 1 skipped."},
344+
expectedErr: []string{"Invalid condition run"},
345+
code: 1,
346+
},
342347
}
343348
for name, tc := range tcs {
344349
t.Run(name, func(t *testing.T) {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
variable "input" {
2+
type = string
3+
}
4+
5+
resource "test_resource" "foo" {
6+
value = var.input
7+
}
8+
9+
output "value" {
10+
value = test_resource.foo.value
11+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
test {
2+
parallel = true
3+
}
4+
5+
run "one" {
6+
state_key = "one"
7+
8+
variables {
9+
input = "one"
10+
}
11+
12+
assert {
13+
condition = output.value
14+
error_message = "something"
15+
}
16+
}
17+
18+
run "two" {
19+
state_key = "two"
20+
21+
variables {
22+
input = run.one.value
23+
}
24+
}
25+
26+
run "three" {
27+
state_key = "three"
28+
29+
variables {
30+
input = "three"
31+
}
32+
}

internal/moduletest/graph/apply.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,7 @@ func (n *NodeTestRun) testApply(ctx *EvalContext, variables terraform.InputValue
104104
newStatus, outputVals, moreDiags := ctx.EvaluateRun(run, applyScope, testOnlyVariables)
105105
run.Status = newStatus
106106
run.Diagnostics = run.Diagnostics.Append(moreDiags)
107-
108-
// Now we've successfully validated this run block, lets add it into
109-
// our prior run outputs so future run blocks can access it.
110-
ctx.SetOutput(run, outputVals)
107+
run.Outputs = outputVals
111108

112109
// Only update the most recent run and state if the state was
113110
// actually updated by this change. We want to use the run that

internal/moduletest/graph/eval_context.go

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"context"
88
"fmt"
99
"log"
10-
"maps"
1110
"sort"
1211
"sync"
1312

@@ -44,14 +43,8 @@ type TestFileState struct {
4443
type EvalContext struct {
4544
VariableCache *hcltest.VariableCache
4645

47-
// runOutputs is a mapping from run addresses to cty object values
48-
// representing the collected output values from the module under test.
49-
//
50-
// This is used to allow run blocks to refer back to the output values of
51-
// previous run blocks. It is passed into the Evaluate functions that
52-
// validate the test assertions, and used when calculating values for
53-
// variables within run blocks.
54-
runOutputs map[addrs.Run]cty.Value
46+
// runBlocks caches all the known run blocks that this EvalContext manages.
47+
runBlocks map[addrs.Run]*moduletest.Run
5548
outputsLock sync.Mutex
5649

5750
// configProviders is a cache of config keys mapped to all the providers
@@ -99,7 +92,7 @@ func NewEvalContext(opts EvalContextOpts) *EvalContext {
9992
cancelCtx, cancel := context.WithCancel(opts.CancelCtx)
10093
stopCtx, stop := context.WithCancel(opts.StopCtx)
10194
return &EvalContext{
102-
runOutputs: make(map[addrs.Run]cty.Value),
95+
runBlocks: make(map[addrs.Run]*moduletest.Run),
10396
outputsLock: sync.Mutex{},
10497
configProviders: make(map[string]map[string]bool),
10598
providersLock: sync.Mutex{},
@@ -313,17 +306,28 @@ func (ec *EvalContext) EvaluateRun(run *moduletest.Run, resultScope *lang.Scope,
313306
return status, cty.ObjectVal(outputVals), diags
314307
}
315308

316-
func (ec *EvalContext) SetOutput(run *moduletest.Run, output cty.Value) {
309+
func (ec *EvalContext) AddRunBlock(run *moduletest.Run) {
317310
ec.outputsLock.Lock()
318311
defer ec.outputsLock.Unlock()
319-
ec.runOutputs[run.Addr()] = output
312+
ec.runBlocks[run.Addr()] = run
320313
}
321314

322315
func (ec *EvalContext) GetOutputs() map[addrs.Run]cty.Value {
323316
ec.outputsLock.Lock()
324317
defer ec.outputsLock.Unlock()
325-
outputCopy := make(map[addrs.Run]cty.Value, len(ec.runOutputs))
326-
maps.Copy(outputCopy, ec.runOutputs) // don't use clone here, so we can return a non-nil map
318+
outputCopy := make(map[addrs.Run]cty.Value, len(ec.runBlocks))
319+
for addr, run := range ec.runBlocks {
320+
// Normally, we should check the run.Status before reading the outputs
321+
// to make sure they are actually valid. But, for now we are tracking
322+
// a difference between run blocks not yet executed and run blocks that
323+
// do not exist by setting cty.NilVal for run blocks that haven't
324+
// executed yet so we do actually just want to include all run blocks
325+
// here.
326+
// TODO(liamcervante): Validate run status before adding to this map
327+
// once providers and variables are in the graph and we don't need to
328+
// rely on this hack.
329+
outputCopy[addr] = run.Outputs
330+
}
327331
return outputCopy
328332
}
329333

@@ -378,6 +382,60 @@ func (ec *EvalContext) GetFileState(key string) *TestFileState {
378382
return ec.FileStates[key]
379383
}
380384

385+
// ReferencesCompleted returns true if all the listed references were actually
386+
// executed successfully. This allows nodes in the graph to decide if they
387+
// should execute or not based on the
388+
//
389+
// TODO(liamcervante): Expand this with providers and variables once we've added
390+
// them to the graph.
391+
func (ec *EvalContext) ReferencesCompleted(refs []*addrs.Reference) bool {
392+
for _, ref := range refs {
393+
switch ref := ref.Subject.(type) {
394+
case addrs.Run:
395+
ec.outputsLock.Lock()
396+
if run, ok := ec.runBlocks[ref]; ok {
397+
if run.Status != moduletest.Pass && run.Status != moduletest.Fail {
398+
ec.outputsLock.Unlock()
399+
400+
// see also prior runs completed
401+
402+
return false
403+
}
404+
}
405+
ec.outputsLock.Unlock()
406+
}
407+
}
408+
return true
409+
}
410+
411+
// PriorRunsCompleted checks a list of run blocks against our internal log of
412+
// completed run blocks and makes sure that any that do exist successfully
413+
// executed to completion.
414+
//
415+
// Note that run blocks that are not in the list indicate a bad reference,
416+
// which we ignore here. This is actually the problem of the caller to identify
417+
// and error.
418+
func (ec *EvalContext) PriorRunsCompleted(runs map[addrs.Run]*moduletest.Run) bool {
419+
ec.outputsLock.Lock()
420+
defer ec.outputsLock.Unlock()
421+
422+
for addr := range runs {
423+
if run, ok := ec.runBlocks[addr]; ok {
424+
if run.Status != moduletest.Pass && run.Status != moduletest.Fail {
425+
426+
// pass and fail indicate the run block still executed the plan
427+
// or apply operate and wrote outputs. fail means the
428+
// post-execution checks failed, but we still had data to check.
429+
// this is in contrast to pending, skip, or error which indicate
430+
// that we never even wrote data for this run block.
431+
432+
return false
433+
}
434+
}
435+
}
436+
return true
437+
}
438+
381439
// evaluationData augments an underlying lang.Data -- presumably resulting
382440
// from a terraform.Context.PlanAndEval or terraform.Context.ApplyAndEval call --
383441
// with results from prior runs that should therefore be available when

internal/moduletest/graph/eval_context_test.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -730,16 +730,21 @@ func TestEvalContext_Evaluate(t *testing.T) {
730730
ModuleConfig: config,
731731
}
732732

733-
priorOutputs := make(map[addrs.Run]cty.Value, len(test.priorOutputs))
734-
for name, val := range test.priorOutputs {
735-
priorOutputs[addrs.Run{Name: name}] = val
736-
}
737-
738733
testCtx := NewEvalContext(EvalContextOpts{
739734
CancelCtx: context.Background(),
740735
StopCtx: context.Background(),
741736
})
742-
testCtx.runOutputs = priorOutputs
737+
testCtx.runBlocks = make(map[addrs.Run]*moduletest.Run)
738+
for ix, block := range file.Runs[:len(file.Runs)-1] {
739+
740+
// all prior run blocks we just mark as having passed, and with
741+
// the output data specified by the test
742+
743+
run := moduletest.NewRun(block, config, ix)
744+
run.Status = moduletest.Pass
745+
run.Outputs = test.priorOutputs[run.Name]
746+
testCtx.runBlocks[run.Addr()] = run
747+
}
743748
gotStatus, gotOutputs, diags := testCtx.EvaluateRun(run, planScope, test.testOnlyVars)
744749

745750
if got, want := gotStatus, test.expectedStatus; got != want {

internal/moduletest/graph/node_test_run.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ var (
2323
)
2424

2525
type NodeTestRun struct {
26-
run *moduletest.Run
27-
opts *graphOptions
26+
run *moduletest.Run
27+
priorRuns map[addrs.Run]*moduletest.Run
28+
opts *graphOptions
2829
}
2930

3031
func (n *NodeTestRun) Run() *moduletest.Run {
@@ -62,10 +63,9 @@ func (n *NodeTestRun) Execute(evalCtx *EvalContext) {
6263
file.UpdateStatus(run.Status)
6364
}()
6465

65-
if file.GetStatus() == moduletest.Error {
66-
// If the overall test file has errored, we don't keep trying to
67-
// execute tests. Instead, we mark all remaining run blocks as
68-
// skipped, print the status, and move on.
66+
if !evalCtx.PriorRunsCompleted(n.priorRuns) || !evalCtx.ReferencesCompleted(n.References()) {
67+
// If any of our prior runs or references weren't completed successfully
68+
// then we will just skip this run block.
6969
run.Status = moduletest.Skip
7070
return
7171
}

internal/moduletest/graph/plan.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,7 @@ func (n *NodeTestRun) testPlan(ctx *EvalContext, variables terraform.InputValues
7272
newStatus, outputVals, moreDiags := ctx.EvaluateRun(run, planScope, testOnlyVariables)
7373
run.Status = newStatus
7474
run.Diagnostics = run.Diagnostics.Append(moreDiags)
75-
76-
// Now we've successfully validated this run block, lets add it into
77-
// our prior run outputs so future run blocks can access it.
78-
ctx.SetOutput(run, outputVals)
75+
run.Outputs = outputVals
7976
}
8077

8178
func (n *NodeTestRun) plan(ctx *EvalContext, tfCtx *terraform.Context, variables terraform.InputValues, waiter *operationWaiter) (*lang.Scope, *plans.Plan, tfdiags.Diagnostics) {

internal/moduletest/graph/transform_context.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
package graph
55

66
import (
7-
"github.com/zclconf/go-cty/cty"
8-
97
"github.com/hashicorp/terraform/internal/dag"
108
"github.com/hashicorp/terraform/internal/moduletest"
119
"github.com/hashicorp/terraform/internal/states"
@@ -31,7 +29,7 @@ func (e *EvalContextTransformer) Transform(graph *terraform.Graph) error {
3129
// TODO(liamcervante): Once providers are embedded in the graph
3230
// we don't need to track run blocks in this way anymore.
3331

34-
ctx.SetOutput(run, cty.NilVal)
32+
ctx.AddRunBlock(run)
3533

3634
// We also want to set an empty state file for every state key
3735
// we're going to be executing within the graph.

internal/moduletest/graph/transform_test_run.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
package graph
55

66
import (
7+
"github.com/hashicorp/terraform/internal/addrs"
78
"github.com/hashicorp/terraform/internal/dag"
9+
"github.com/hashicorp/terraform/internal/moduletest"
810
"github.com/hashicorp/terraform/internal/terraform"
911
)
1012

@@ -18,7 +20,7 @@ func (t *TestRunTransformer) Transform(g *terraform.Graph) error {
1820
// Create and add nodes for each run
1921
var nodes []*NodeTestRun
2022
for _, run := range t.opts.File.Runs {
21-
node := &NodeTestRun{run: run, opts: t.opts}
23+
node := &NodeTestRun{run: run, opts: t.opts, priorRuns: make(map[addrs.Run]*moduletest.Run)}
2224
g.Add(node)
2325
nodes = append(nodes, node)
2426
}
@@ -49,6 +51,7 @@ func (t *TestRunTransformer) controlParallelism(g *terraform.Graph, nodes []*Nod
4951
// Connect to all previous runs
5052
for j := 0; j < i; j++ {
5153
g.Connect(dag.BasicEdge(node, nodes[j]))
54+
node.priorRuns[nodes[j].run.Addr()] = nodes[j].run
5255
}
5356

5457
// Connect to all subsequent runs
@@ -66,6 +69,7 @@ func (t *TestRunTransformer) connectSameStateRuns(g *terraform.Graph, nodes []*N
6669
}
6770
for _, runs := range stateRuns {
6871
for i := 1; i < len(runs); i++ {
72+
runs[i].priorRuns[runs[i-1].run.Addr()] = runs[i-1].run
6973
g.Connect(dag.BasicEdge(runs[i], runs[i-1]))
7074
}
7175
}

0 commit comments

Comments
 (0)