Skip to content

Commit 2cfa404

Browse files
author
Liam Cervante
committed
test: don't panic when resolving references that haven't been evaluated
1 parent b1574c6 commit 2cfa404

File tree

7 files changed

+148
-2
lines changed

7 files changed

+148
-2
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: BUG FIXES
2+
body: 'terraform test: prevent panic when resolving incomplete references'
3+
time: 2025-08-25T12:50:18.511449+02:00
4+
custom:
5+
Issue: "37484"

internal/command/test_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,11 @@ func TestTest_Runs(t *testing.T) {
402402
expectedOut: []string{"3 passed, 0 failed."},
403403
code: 0,
404404
},
405+
"expect-failures-assertions": {
406+
expectedOut: []string{"0 passed, 1 failed."},
407+
expectedErr: []string{"Test assertion failed"},
408+
code: 1,
409+
},
405410
}
406411
for name, tc := range tcs {
407412
t.Run(name, func(t *testing.T) {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
variable "input" {
3+
type = string
4+
}
5+
6+
resource "test_resource" "resource" {
7+
value = var.input
8+
}
9+
10+
output "output" {
11+
value = test_resource.resource.value
12+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
variable "input" {
3+
type = string
4+
5+
validation {
6+
condition = var.input == "allow"
7+
error_message = "invalid input value"
8+
}
9+
}
10+
11+
variable "followup" {
12+
type = string
13+
default = "allow"
14+
15+
validation {
16+
condition = var.followup == var.input
17+
error_message = "followup must match input"
18+
}
19+
}
20+
21+
locals {
22+
input = var.followup
23+
}
24+
25+
module "child" {
26+
source = "./child"
27+
input = var.input
28+
}
29+
30+
resource "test_resource" "resource" {
31+
value = local.input
32+
}
33+
34+
output "output" {
35+
value = var.input
36+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
2+
// this test runs assertions againsts parts of the module that should not
3+
// have executed because of the expected failure. this should be an error
4+
// in the test, but it shouldn't panic or anything like that.
5+
6+
run "fail" {
7+
variables {
8+
input = "deny"
9+
}
10+
11+
command = plan
12+
13+
expect_failures = [
14+
var.input,
15+
]
16+
17+
assert {
18+
condition = var.followup == "deny"
19+
error_message = "bad input"
20+
}
21+
22+
assert {
23+
condition = local.input == "deny"
24+
error_message = "bad local"
25+
}
26+
27+
assert {
28+
condition = module.child.output == "deny"
29+
error_message = "bad module output"
30+
}
31+
32+
assert {
33+
condition = test_resource.resource.value == "deny"
34+
error_message = "bad resource value"
35+
}
36+
37+
assert {
38+
condition = output.output == "deny"
39+
error_message = "bad output"
40+
}
41+
}

internal/instances/expander.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,21 @@ func (e *Expander) ExpandAbsModuleCall(addr addrs.AbsModuleCall) (keyType addrs.
181181
return keyType, instKeys, true
182182
}
183183

184+
// AbsModuleCallExpanded checks if the specified module call has been visited
185+
// and expanded previously.
186+
func (e *Expander) AbsModuleCallExpanded(addr addrs.AbsModuleCall) bool {
187+
e.mu.RLock()
188+
defer e.mu.RUnlock()
189+
190+
expParent, ok := e.findModule(addr.Module)
191+
if !ok {
192+
return false
193+
}
194+
195+
_, ok = expParent.moduleCalls[addr.Call]
196+
return ok
197+
}
198+
184199
// expandModule allows skipping unexpanded module addresses by setting skipUnregistered to true.
185200
// This is used by instances.Set, which is only concerned with the expanded
186201
// instances, and should not panic when looking up unknown addresses.
@@ -450,6 +465,20 @@ func (e *Expander) ResourceInstanceKeys(addr addrs.AbsResource) (keyType addrs.I
450465
return exp.instanceKeys()
451466
}
452467

468+
// ResourceInstanceExpanded checks if the specified resource has been visited
469+
// and expanded previously.
470+
func (e *Expander) ResourceInstanceExpanded(addr addrs.AbsResource) bool {
471+
e.mu.RLock()
472+
defer e.mu.RUnlock()
473+
474+
parentMod, known := e.findModule(addr.Module)
475+
if !known {
476+
return false
477+
}
478+
_, ok := parentMod.resources[addr.Resource]
479+
return ok
480+
}
481+
453482
// AllInstances returns a set of all of the module and resource instances known
454483
// to the expander.
455484
//

internal/terraform/evaluate.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,11 @@ func (d *evaluationStateData) GetLocalValue(addr addrs.LocalValue, rng tfdiags.S
342342
return cty.DynamicVal, diags
343343
}
344344

345-
val := d.Evaluator.NamedValues.GetLocalValue(addr.Absolute(d.ModulePath))
346-
return val, diags
345+
if target := addr.Absolute(d.ModulePath); d.Evaluator.NamedValues.HasLocalValue(target) {
346+
return d.Evaluator.NamedValues.GetLocalValue(addr.Absolute(d.ModulePath)), diags
347+
}
348+
349+
return cty.DynamicVal, diags
347350
}
348351

349352
func (d *evaluationStateData) GetModule(addr addrs.ModuleCall, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
@@ -541,6 +544,21 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc
541544
// result for _all_ of its work, rather than continuing to duplicate a bunch
542545
// of the logic we've tried to encapsulate over ther already.
543546
if d.Operation == walkPlan || d.Operation == walkApply {
547+
if !d.Evaluator.Instances.ResourceInstanceExpanded(addr.Absolute(moduleAddr)) {
548+
// Then we've asked for a resource that hasn't been evaluated yet.
549+
// This means that either something has gone wrong in the graph or
550+
// the console or test command has an errored plan and is attempting
551+
// to load an invalid resource from it.
552+
553+
unknownVal := cty.DynamicVal
554+
555+
// If an ephemeral resource is deferred we need to mark the returned unknown value as ephemeral
556+
if addr.Mode == addrs.EphemeralResourceMode {
557+
unknownVal = unknownVal.Mark(marks.Ephemeral)
558+
}
559+
return unknownVal, diags
560+
}
561+
544562
if _, _, hasUnknownKeys := d.Evaluator.Instances.ResourceInstanceKeys(addr.Absolute(moduleAddr)); hasUnknownKeys {
545563
// There really isn't anything interesting we can do in this situation,
546564
// because it means we have an unknown for_each/count, in which case

0 commit comments

Comments
 (0)