evaluate: return diagnostics instead of unknown for uninitialised locals and resources (#37663)

* evaluate: return diagnostics instead of unknown for uninitialised locals and resources

* changelog

* also input variables
pull/37672/head
Liam Cervante 5 months ago committed by GitHub
parent a7504719ef
commit 1e414491c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
kind: BUG FIXES
body: 'console and test: return explicit diagnostics when referencing resources that were not included in the most recent operation.'
time: 2025-09-24T11:04:16.860364+02:00
custom:
Issue: "37663"

@ -412,11 +412,6 @@ func TestTest_Runs(t *testing.T) {
"no-tests": {
code: 0,
},
"expect-failures-assertions": {
expectedOut: []string{"0 passed, 1 failed."},
expectedErr: []string{"Test assertion failed"},
code: 1,
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
@ -5454,6 +5449,130 @@ func TestTest_JUnitOutput(t *testing.T) {
}
}
func TestTest_ReferencesIntoIncompletePlan(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", "expect-failures-assertions")), td)
t.Chdir(td)
provider := testing_command.NewProvider(nil)
providerSource, close := newMockProviderSource(t, map[string][]string{
"test": {"1.0.0"},
})
defer close()
streams, done := terminal.StreamsForTesting(t)
view := views.NewView(streams)
ui := new(cli.MockUi)
meta := Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
Ui: ui,
View: view,
Streams: streams,
ProviderSource: providerSource,
}
init := &InitCommand{
Meta: meta,
}
if code := init.Run(nil); code != 0 {
output := done(t)
t.Fatalf("expected status code %d but got %d: %s", 0, code, output.All())
}
// Reset the streams for the next command.
streams, done = terminal.StreamsForTesting(t)
meta.Streams = streams
meta.View = views.NewView(streams)
c := &TestCommand{
Meta: meta,
}
code := c.Run([]string{"-no-color"})
if code != 1 {
t.Errorf("expected status code %d but got %d", 0, code)
}
output := done(t)
out, err := output.Stdout(), output.Stderr()
expectedOut := `main.tftest.hcl... in progress
run "fail"... fail
main.tftest.hcl... tearing down
main.tftest.hcl... fail
Failure! 0 passed, 1 failed.
`
if diff := cmp.Diff(out, expectedOut); len(diff) > 0 {
t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expectedOut, out, diff)
}
if !strings.Contains(err, "Reference to uninitialized resource") {
t.Errorf("missing reference to uninitialized resource error: \n%s", err)
}
if !strings.Contains(err, "Reference to uninitialized local") {
t.Errorf("missing reference to uninitialized local error: \n%s", err)
}
}
func TestTest_ReferencesIntoTargetedPlan(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", "invalid-reference-with-target")), td)
t.Chdir(td)
provider := testing_command.NewProvider(nil)
providerSource, close := newMockProviderSource(t, map[string][]string{
"test": {"1.0.0"},
})
defer close()
streams, done := terminal.StreamsForTesting(t)
view := views.NewView(streams)
ui := new(cli.MockUi)
meta := Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
Ui: ui,
View: view,
Streams: streams,
ProviderSource: providerSource,
}
init := &InitCommand{
Meta: meta,
}
if code := init.Run(nil); code != 0 {
output := done(t)
t.Fatalf("expected status code %d but got %d: %s", 0, code, output.All())
}
// Reset the streams for the next command.
streams, done = terminal.StreamsForTesting(t)
meta.Streams = streams
meta.View = views.NewView(streams)
c := &TestCommand{
Meta: meta,
}
code := c.Run([]string{"-no-color"})
if code != 1 {
t.Errorf("expected status code %d but got %d", 0, code)
}
output := done(t)
err := output.Stderr()
if !strings.Contains(err, "Reference to uninitialized variable") {
t.Errorf("missing reference to uninitialized variable error: \n%s", err)
}
}
// https://github.com/hashicorp/terraform/issues/37546
func TestTest_TeardownOrder(t *testing.T) {
td := t.TempDir()

@ -0,0 +1,10 @@
variable "input" {
type = string
}
resource "test_resource" "one" {
value = var.input
}
resource "test_resource" "two" {}

@ -0,0 +1,17 @@
run "test" {
command = plan
plan_options {
target = [test_resource.two]
}
variables {
input = "hello"
}
assert {
condition = var.input == "hello"
error_message = "wrong input"
}
}

@ -176,6 +176,7 @@ list "test_resource" "test1" {
},
},
"query run, action references resource": {
toBeImplemented: true, // TODO: Fix the graph built by query operations.
module: map[string]string{
"main.tf": `
action "test_action" "hello" {

@ -297,7 +297,18 @@ func (d *evaluationStateData) GetInputVariable(addr addrs.InputVariable, rng tfd
return ret, diags
}
val := d.Evaluator.NamedValues.GetInputVariableValue(d.ModulePath.InputVariable(addr.Name))
var val cty.Value
if target := d.ModulePath.InputVariable(addr.Name); !d.Evaluator.NamedValues.HasInputVariableValue(target) {
val = cty.DynamicVal
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to uninitialized variable",
Detail: fmt.Sprintf("The variable %s was not processed by the most recent operation, this likely means the previous operation either failed or was incomplete due to targeting.", addr),
Subject: rng.ToHCL().Ptr(),
})
} else {
val = d.Evaluator.NamedValues.GetInputVariableValue(target)
}
// Mark if sensitive and/or ephemeral
if config.Sensitive {
@ -342,11 +353,17 @@ func (d *evaluationStateData) GetLocalValue(addr addrs.LocalValue, rng tfdiags.S
return cty.DynamicVal, diags
}
if target := addr.Absolute(d.ModulePath); d.Evaluator.NamedValues.HasLocalValue(target) {
return d.Evaluator.NamedValues.GetLocalValue(addr.Absolute(d.ModulePath)), diags
target := addr.Absolute(d.ModulePath)
if !d.Evaluator.NamedValues.HasLocalValue(target) {
return cty.DynamicVal, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to uninitialized local value",
Detail: fmt.Sprintf("The local value %s was not processed by the most recent operation, this likely means the previous operation either failed or was incomplete due to targeting.", addr),
Subject: rng.ToHCL().Ptr(),
})
}
return cty.DynamicVal, diags
return d.Evaluator.NamedValues.GetLocalValue(target), diags
}
func (d *evaluationStateData) GetModule(addr addrs.ModuleCall, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
@ -556,7 +573,12 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc
if addr.Mode == addrs.EphemeralResourceMode {
unknownVal = unknownVal.Mark(marks.Ephemeral)
}
return unknownVal, diags
return unknownVal, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to uninitialized resource",
Detail: fmt.Sprintf("The resource %s was not processed by the most recent operation, this likely means the previous operation either failed or was incomplete due to targeting.", addr),
Subject: rng.ToHCL().Ptr(),
})
}
if _, _, hasUnknownKeys := d.Evaluator.Instances.ResourceInstanceKeys(addr.Absolute(moduleAddr)); hasUnknownKeys {

Loading…
Cancel
Save