Skip to content

Commit 3aa740a

Browse files
committed
Continue test execution after an expected failure
1 parent cadc46d commit 3aa740a

File tree

4 files changed

+89
-13
lines changed

4 files changed

+89
-13
lines changed

internal/backend/local/test.go

+49-8
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ func (runner *TestFileRunner) Test(file *moduletest.File) {
282282
}
283283

284284
// walk and execute the graph
285-
diags = runner.walkGraph(graph)
285+
diags = runner.walkGraph(graph, file)
286286

287287
// If the graph walk was terminated, we don't want to add the diagnostics.
288288
// The error the user receives will just be:
@@ -298,8 +298,9 @@ func (runner *TestFileRunner) Test(file *moduletest.File) {
298298
}
299299

300300
// walkGraph goes through the graph and execute each run it finds.
301-
func (runner *TestFileRunner) walkGraph(g *terraform.Graph) tfdiags.Diagnostics {
301+
func (runner *TestFileRunner) walkGraph(g *terraform.Graph, file *moduletest.File) tfdiags.Diagnostics {
302302
sem := runner.Suite.semaphore
303+
collectRunStatus, updateFileStatus := runner.trackRunStatuses(file)
303304

304305
// Walk the graph.
305306
walkFn := func(v dag.Vertex) (diags tfdiags.Diagnostics) {
@@ -376,9 +377,13 @@ func (runner *TestFileRunner) walkGraph(g *terraform.Graph) tfdiags.Diagnostics
376377
}
377378

378379
startTime := time.Now().UTC()
379-
runner.run(run, file, startTime)
380+
deferFileStatus := runner.run(run, file, startTime)
380381
runner.Suite.View.Run(run, file, moduletest.Complete, 0)
381-
file.UpdateStatus(run.Status)
382+
// If the run block is done, but it was due to an expected failure, we
383+
// don't want to update the file status immediately. We'll collect the
384+
// status of this run block and update the file status at the end of the
385+
// file execution.
386+
collectRunStatus(run, deferFileStatus)
382387
case graph.GraphNodeExecutable:
383388
diags = v.Execute(runner.EvalContext)
384389
return diags
@@ -389,10 +394,12 @@ func (runner *TestFileRunner) walkGraph(g *terraform.Graph) tfdiags.Diagnostics
389394
return
390395
}
391396

392-
return g.AcyclicGraph.Walk(walkFn)
397+
diags := g.AcyclicGraph.Walk(walkFn)
398+
updateFileStatus()
399+
return diags
393400
}
394401

395-
func (runner *TestFileRunner) run(run *moduletest.Run, file *moduletest.File, startTime time.Time) {
402+
func (runner *TestFileRunner) run(run *moduletest.Run, file *moduletest.File, startTime time.Time) (deferFileStatus bool) {
396403
log.Printf("[TRACE] TestFileRunner: executing run block %s/%s", file.Name, run.Name)
397404
defer func() {
398405
// If we got far enough to actually execute the run then we'll give
@@ -401,6 +408,7 @@ func (runner *TestFileRunner) run(run *moduletest.Run, file *moduletest.File, st
401408
Start: startTime,
402409
Duration: time.Since(startTime),
403410
}
411+
404412
}()
405413

406414
key := run.GetStateKey()
@@ -487,9 +495,9 @@ func (runner *TestFileRunner) run(run *moduletest.Run, file *moduletest.File, st
487495

488496
planScope, plan, planDiags := runner.plan(tfCtx, config, state, run, file, setVariables, references, start)
489497
if run.Config.Command == configs.PlanTestCommand {
490-
// Then we want to assess our conditions and diagnostics differently.
491498
planDiags = run.ValidateExpectedFailures(planDiags)
492499
run.Diagnostics = run.Diagnostics.Append(planDiags)
500+
// Then we want to assess our conditions and diagnostics differently.
493501
if planDiags.HasErrors() {
494502
run.Status = moduletest.Error
495503
return
@@ -536,12 +544,21 @@ func (runner *TestFileRunner) run(run *moduletest.Run, file *moduletest.File, st
536544
return
537545
}
538546

539-
// Otherwise any error during the planning prevents our apply from
547+
// Otherwise any error (expected or unexpected) during the planning prevents our apply from
540548
// continuing which is an error.
541549
planDiags = run.ExplainExpectedFailures(planDiags)
542550
run.Diagnostics = run.Diagnostics.Append(planDiags)
543551
if planDiags.HasErrors() {
544552
run.Status = moduletest.Error
553+
// If the plan failed, but all the failures were expected, then we don't
554+
// want to mark the overall file as a failure, so that subsequent runs can
555+
// still be executed.
556+
// We will collect the status of this run instead of updating the file status.
557+
// At the end of the file execution, we will update the file status based on the
558+
// statuses of all the runs.
559+
if !run.ValidateExpectedFailures(planDiags).HasErrors() {
560+
deferFileStatus = true
561+
}
545562
return
546563
}
547564

@@ -1255,3 +1272,27 @@ func (runner *TestFileRunner) AddVariablesToConfig(run *moduletest.Run, variable
12551272
}
12561273

12571274
}
1275+
1276+
// trackRunStatuses is a helper function that returns two functions. The first
1277+
// function is used to collect the statuses of the runs, and the second function
1278+
// is used to update the overall file status based on the statuses of the runs.
1279+
func (runner *TestFileRunner) trackRunStatuses(file *moduletest.File) (func(*moduletest.Run, bool), func()) {
1280+
statuses := make([]moduletest.Status, len(file.Runs))
1281+
collector := func(run *moduletest.Run, deferred bool) {
1282+
if deferred {
1283+
statuses[run.Index] = run.Status
1284+
} else {
1285+
file.UpdateStatus(run.Status)
1286+
}
1287+
}
1288+
1289+
updater := func() {
1290+
for _, status := range statuses {
1291+
file.UpdateStatus(status)
1292+
}
1293+
}
1294+
1295+
// We'll return two functions, one to collect the statuses of the runs, and
1296+
// one to update the overall file status.
1297+
return collector, updater
1298+
}

internal/command/test_test.go

+7-5
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func TestTest_Runs(t *testing.T) {
3535
code int
3636
initCode int
3737
skip bool
38-
desc string
38+
description string
3939
}{
4040
"simple_pass": {
4141
expectedOut: []string{"1 passed, 0 failed."},
@@ -60,7 +60,7 @@ func TestTest_Runs(t *testing.T) {
6060
args: []string{"-parallelism", "1"},
6161
expectedOut: []string{"1 passed, 0 failed."},
6262
code: 0,
63-
desc: "simple_pass with parallelism set to 1",
63+
description: "simple_pass with parallelism set to 1",
6464
},
6565
"simple_pass_very_nested_alternate": {
6666
override: "simple_pass_very_nested",
@@ -104,9 +104,11 @@ func TestTest_Runs(t *testing.T) {
104104
expectedOut: []string{"1 passed, 0 failed."},
105105
code: 0,
106106
},
107-
"expect_failures_outputs": {
108-
expectedOut: []string{"1 passed, 0 failed."},
109-
code: 0,
107+
"expect_failures_continue": {
108+
expectedOut: []string{"1 passed, 1 failed.", "Expected failure while planning"},
109+
code: 1,
110+
expectedErr: []string{"Module output value precondition failed"},
111+
description: "continue test execution after an expected failure",
110112
},
111113
"expect_failures_resources": {
112114
expectedOut: []string{"1 passed, 0 failed."},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
variable "input" {
3+
type = string
4+
}
5+
6+
output "output" {
7+
value = var.input
8+
9+
precondition {
10+
condition = var.input == "something incredibly specific"
11+
error_message = "this should fail"
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
variables {
2+
input = "some value"
3+
}
4+
5+
run "test" {
6+
7+
command = apply
8+
9+
expect_failures = [
10+
output.output
11+
]
12+
}
13+
14+
run "follow-up" {
15+
command = apply
16+
17+
variables {
18+
input = "something incredibly specific"
19+
}
20+
}

0 commit comments

Comments
 (0)