diff --git a/.changes/v1.14/BUG FIXES-20250924-110416.yaml b/.changes/v1.14/BUG FIXES-20250924-110416.yaml new file mode 100644 index 0000000000..3ddb833d6e --- /dev/null +++ b/.changes/v1.14/BUG FIXES-20250924-110416.yaml @@ -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" diff --git a/internal/command/test_test.go b/internal/command/test_test.go index 36b05cb9e4..7d8e6a91c5 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -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() diff --git a/internal/command/testdata/test/invalid-reference-with-target/main.tf b/internal/command/testdata/test/invalid-reference-with-target/main.tf new file mode 100644 index 0000000000..1b912297a3 --- /dev/null +++ b/internal/command/testdata/test/invalid-reference-with-target/main.tf @@ -0,0 +1,10 @@ + +variable "input" { + type = string +} + +resource "test_resource" "one" { + value = var.input +} + +resource "test_resource" "two" {} \ No newline at end of file diff --git a/internal/command/testdata/test/invalid-reference-with-target/main.tftest.hcl b/internal/command/testdata/test/invalid-reference-with-target/main.tftest.hcl new file mode 100644 index 0000000000..9b9aac6612 --- /dev/null +++ b/internal/command/testdata/test/invalid-reference-with-target/main.tftest.hcl @@ -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" + } +} \ No newline at end of file diff --git a/internal/terraform/context_plan_actions_test.go b/internal/terraform/context_plan_actions_test.go index c82245b726..99a4826829 100644 --- a/internal/terraform/context_plan_actions_test.go +++ b/internal/terraform/context_plan_actions_test.go @@ -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" { diff --git a/internal/terraform/evaluate.go b/internal/terraform/evaluate.go index 6a1cb5163c..68c9d8aba5 100644 --- a/internal/terraform/evaluate.go +++ b/internal/terraform/evaluate.go @@ -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 {