From f63f81873826d859da1ca8640c9ab3e593377a5a Mon Sep 17 00:00:00 2001 From: Samsondeen Dare Date: Tue, 4 Nov 2025 17:28:00 +0100 Subject: [PATCH 1/5] Allow functions in override blocks --- internal/command/test_test.go | 37 +++++- .../test/simple_pass_function/main.tf | 3 + .../test/simple_pass_function/main.tftest.hcl | 13 +++ internal/configs/mock_provider.go | 11 +- internal/terraform/node_output.go | 108 +++++++++++------- .../node_resource_abstract_instance.go | 56 +++++---- .../terraform/node_resource_plan_instance.go | 7 +- 7 files changed, 168 insertions(+), 67 deletions(-) create mode 100644 internal/command/testdata/test/simple_pass_function/main.tf create mode 100644 internal/command/testdata/test/simple_pass_function/main.tftest.hcl diff --git a/internal/command/test_test.go b/internal/command/test_test.go index 4b8e4c5380f6..9eeedfc7bbe3 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -15,6 +15,7 @@ import ( "path/filepath" "regexp" "runtime" + "slices" "sort" "strings" "testing" @@ -52,6 +53,7 @@ func TestTest_Runs(t *testing.T) { initCode int skip bool description string + selectFiles []string }{ "simple_pass": { expectedOut: []string{"1 passed, 0 failed."}, @@ -297,7 +299,6 @@ func TestTest_Runs(t *testing.T) { }, "mocking-invalid": { expectedErr: []string{ - "Invalid outputs attribute", "The override_during attribute must be a value of plan or apply.", }, initCode: 1, @@ -418,6 +419,18 @@ func TestTest_Runs(t *testing.T) { "no-tests": { code: 0, }, + "simple_pass_function": { + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, + "mocking-invalid-outputs": { + override: "mocking-invalid", + expectedErr: []string{ + "Invalid outputs attribute", + }, + selectFiles: []string{"module_mocked_invalid_type.tftest.hcl"}, + code: 1, + }, } for name, tc := range tcs { t.Run(name, func(t *testing.T) { @@ -441,6 +454,28 @@ func TestTest_Runs(t *testing.T) { td := t.TempDir() testCopyDir(t, testFixturePath(path.Join("test", file)), td) + if len(tc.selectFiles) > 0 { + dirs, _ := os.ReadDir(td) + dirs2, _ := os.ReadDir(filepath.Join(td, "tests")) + for _, dir := range dirs { + dirName := dir.Name() + if !slices.Contains(tc.selectFiles, dirName) && strings.HasSuffix(dirName, "tftest.hcl") { + err := os.Remove(filepath.Join(td, dirName)) + if err != nil { + t.Errorf("failed to remove file %s: %v", dirName, err) + } + } + } + for _, dir := range dirs2 { + dirName := dir.Name() + if !slices.Contains(tc.selectFiles, dirName) && strings.HasSuffix(dirName, "tftest.hcl") { + err := os.Remove(filepath.Join(td, "tests", dirName)) + if err != nil { + t.Errorf("failed to remove file %s: %v", dirName, err) + } + } + } + } t.Chdir(td) store := &testing_command.ResourceStore{ diff --git a/internal/command/testdata/test/simple_pass_function/main.tf b/internal/command/testdata/test/simple_pass_function/main.tf new file mode 100644 index 000000000000..41cc84e5c4ea --- /dev/null +++ b/internal/command/testdata/test/simple_pass_function/main.tf @@ -0,0 +1,3 @@ +resource "test_resource" "foo" { + value = "bar" +} diff --git a/internal/command/testdata/test/simple_pass_function/main.tftest.hcl b/internal/command/testdata/test/simple_pass_function/main.tftest.hcl new file mode 100644 index 000000000000..8bc0bc034877 --- /dev/null +++ b/internal/command/testdata/test/simple_pass_function/main.tftest.hcl @@ -0,0 +1,13 @@ +override_resource { + target = test_resource.foo + values = { + id = format("f-%s", "bar") + } +} + +run "validate_test_resource" { + assert { + condition = test_resource.foo.id == "f-bar" + error_message = "invalid value" + } +} diff --git a/internal/configs/mock_provider.go b/internal/configs/mock_provider.go index 97118a9b237c..6e1a8108db30 100644 --- a/internal/configs/mock_provider.go +++ b/internal/configs/mock_provider.go @@ -208,6 +208,11 @@ type Override struct { Target *addrs.Target Values cty.Value + BlockName string + + // The raw expression of the values/outputs block + RawValue hcl.Expression + // UseForPlan is true if the values should be computed during the planning // phase. UseForPlan bool @@ -453,6 +458,7 @@ func decodeOverrideBlock(block *hcl.Block, attributeName string, blockName strin Source: source, Range: block.DefRange, TypeRange: block.TypeRange, + BlockName: blockName, } if target, exists := content.Attributes["target"]; exists { @@ -474,10 +480,9 @@ func decodeOverrideBlock(block *hcl.Block, attributeName string, blockName strin } if attribute, exists := content.Attributes[attributeName]; exists { - var valueDiags hcl.Diagnostics override.ValuesRange = attribute.Range - override.Values, valueDiags = attribute.Expr.Value(nil) - diags = append(diags, valueDiags...) + override.Values = cty.EmptyObjectVal + override.RawValue = attribute.Expr } else { // It's fine if we don't have any values, just means we'll generate // values for everything ourselves. We set this to an empty object so diff --git a/internal/terraform/node_output.go b/internal/terraform/node_output.go index 3e505a28fb79..960f68d19669 100644 --- a/internal/terraform/node_output.go +++ b/internal/terraform/node_output.go @@ -138,7 +138,7 @@ func (n *nodeExpandOutput) DynamicExpand(ctx EvalContext) (*Graph, tfdiags.Diagn RefreshOnly: n.RefreshOnly, DestroyApply: n.Destroying, Planning: n.Planning, - Override: n.getOverrideValue(absAddr.Module), + Overrides: n.Overrides, Dependencies: n.Dependencies, AllowRootEphemeralOutputs: n.AllowRootEphemeralOutputs, } @@ -228,41 +228,6 @@ func (n *nodeExpandOutput) References() []*addrs.Reference { return referencesForOutput(n.Config) } -func (n *nodeExpandOutput) getOverrideValue(inst addrs.ModuleInstance) cty.Value { - // First check if we have any overrides at all, this is a shorthand for - // "are we running terraform test". - if n.Overrides.Empty() { - // cty.NilVal means no override - return cty.NilVal - } - - // We have overrides, let's see if we have one for this module instance. - if override, ok := n.Overrides.GetModuleOverride(inst); ok { - - output := n.Addr.Name - values := override.Values - - // The values.Type() should be an object type, but it might have - // been set to nil by a test or something. We can handle it in the - // same way as the attribute just not being specified. It's - // functionally the same for us and not something we need to raise - // alarms about. - if values.Type().IsObjectType() && values.Type().HasAttribute(output) { - return values.GetAttr(output) - } - - // If we don't have a value provided for an output, then we'll - // just set it to be null. - // - // TODO(liamcervante): Can we generate a value here? Probably - // not as we don't know the type. - return cty.NullVal(cty.DynamicPseudoType) - } - - // cty.NilVal indicates no override. - return cty.NilVal -} - // NodeApplyableOutput represents an output that is "applyable": // it is ready to be applied. type NodeApplyableOutput struct { @@ -281,9 +246,10 @@ type NodeApplyableOutput struct { Planning bool - // Override provides the value to use for this output, if any. This can be - // set by testing framework when a module is overridden. - Override cty.Value + // Overrides is the set of overrides applied by the testing framework. We + // may need to override the value for this output and if we do the value + // comes from here. + Overrides *mocking.Overrides // Dependencies is the full set of resources that are referenced by this // output. @@ -326,6 +292,60 @@ func (n *NodeApplyableOutput) ModulePath() addrs.Module { return n.Addr.Module.Module() } +func (n *NodeApplyableOutput) getOverrideValue(ctx EvalContext) (cty.Value, tfdiags.Diagnostics) { + // First check if we have any overrides at all, this is a shorthand for + // "are we running terraform test". + if n.Overrides.Empty() { + // cty.NilVal means no override + return cty.NilVal, nil + } + + // We have overrides, let's see if we have one for this module instance. + if override, ok := n.Overrides.GetModuleOverride(n.Addr.Module); ok { + + // If there is an override block with no specified outputs block, + // we set the value to null. + if override.RawValue == nil { + return cty.NullVal(cty.DynamicPseudoType), nil + } + + output := n.Addr.OutputValue.Name + values, diags := ctx.EvaluateExpr(override.RawValue, cty.DynamicPseudoType, nil) + if diags.HasErrors() { + return cty.NilVal, diags + } + + if !values.Type().IsObjectType() { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid outputs attribute", + Detail: fmt.Sprintf("%s blocks must specify an outputs attribute that is an object.", override.BlockName), + Subject: override.ValuesRange.Ptr(), + }) + return cty.NilVal, diags + } + + // The values.Type() should be an object type, but it might have + // been set to nil by a test or something. We can handle it in the + // same way as the attribute just not being specified. It's + // functionally the same for us and not something we need to raise + // alarms about. + if values.Type().IsObjectType() && values.Type().HasAttribute(output) { + return values.GetAttr(output), nil + } + + // If we don't have a value provided for an output, then we'll + // just set it to be null. + // + // TODO(liamcervante): Can we generate a value here? Probably + // not as we don't know the type. + return cty.NullVal(cty.DynamicPseudoType), nil + } + + // cty.NilVal indicates no override. + return cty.NilVal, nil +} + func referenceOutsideForOutput(addr addrs.AbsOutputValue) (selfPath, referencePath addrs.Module) { // Output values have their expressions resolved in the context of the // module where they are defined. @@ -420,7 +440,13 @@ func (n *NodeApplyableOutput) Execute(ctx EvalContext, op walkOperation) (diags // be valid, or may not have been registered at all. // We also don't evaluate checks for overridden outputs. This is because // any references within the checks will likely not have been created. - if !n.DestroyApply && n.Override == cty.NilVal { + override, overrideDiags := n.getOverrideValue(ctx) + if overrideDiags.HasErrors() { + diags = diags.Append(overrideDiags) + return + } + + if !n.DestroyApply && override == cty.NilVal { checkRuleSeverity := tfdiags.Error if n.RefreshOnly { checkRuleSeverity = tfdiags.Warning @@ -443,7 +469,7 @@ func (n *NodeApplyableOutput) Execute(ctx EvalContext, op walkOperation) (diags // First, we check if we have an overridden value. If we do, then we // use that and we don't try and evaluate the underlying expression. - val = n.Override + val = override if val == cty.NilVal { // This has to run before we have a state lock, since evaluation also // reads the state diff --git a/internal/terraform/node_resource_abstract_instance.go b/internal/terraform/node_resource_abstract_instance.go index c5569b5491df..c3eed04fc7ca 100644 --- a/internal/terraform/node_resource_abstract_instance.go +++ b/internal/terraform/node_resource_abstract_instance.go @@ -955,19 +955,10 @@ func (n *NodeAbstractResourceInstance) plan( if n.override != nil { // Then we have an override to apply for this change. But, overrides // only matter when we are creating a resource for the first time as we - // only apply computed values. if priorVal.IsNull() { // Then we are actually creating something, so let's populate the // computed values from our override value. - override, overrideDiags := mocking.PlanComputedValuesForResource(proposedNewVal, &mocking.MockedData{ - Value: n.override.Values, - Range: n.override.Range, - ComputedAsUnknown: !n.override.UseForPlan, - }, schema.Body) - resp = providers.PlanResourceChangeResponse{ - PlannedState: ephemeral.StripWriteOnlyAttributes(override, schema.Body), - Diagnostics: overrideDiags, - } + resp = n.planOverride(ctx, proposedNewVal, schema.Body) } else { // This is an update operation, and we don't actually have any // computed values that need to be applied. @@ -1168,15 +1159,7 @@ func (n *NodeAbstractResourceInstance) plan( if n.override != nil { // In this case, we are always creating the resource so we don't // do any validation, and just call out to the mocking library. - override, overrideDiags := mocking.PlanComputedValuesForResource(proposedNewVal, &mocking.MockedData{ - Value: n.override.Values, - Range: n.override.Range, - ComputedAsUnknown: !n.override.UseForPlan, - }, schema.Body) - resp = providers.PlanResourceChangeResponse{ - PlannedState: ephemeral.StripWriteOnlyAttributes(override, schema.Body), - Diagnostics: overrideDiags, - } + resp = n.planOverride(ctx, proposedNewVal, schema.Body) } else { resp = provider.PlanResourceChange(providers.PlanResourceChangeRequest{ TypeName: n.Addr.Resource.Resource.Type, @@ -1341,6 +1324,29 @@ func (n *NodeAbstractResourceInstance) plan( return plan, state, deferred, keyData, diags } +func (n *NodeAbstractResourceInstance) planOverride(ctx EvalContext, original cty.Value, schema *configschema.Block) providers.PlanResourceChangeResponse { + values, diags := ctx.EvaluateExpr(n.override.RawValue, cty.DynamicPseudoType, nil) + if diags.HasErrors() { + return providers.PlanResourceChangeResponse{ + Diagnostics: diags, + } + } + + // In this case, we are always creating the resource so we don't + // do any validation, and just call out to the mocking library. + override, overrideDiags := mocking.PlanComputedValuesForResource(original, &mocking.MockedData{ + Value: values, + Range: n.override.Range, + ComputedAsUnknown: !n.override.UseForPlan, + }, schema) + resp := providers.PlanResourceChangeResponse{ + PlannedState: ephemeral.StripWriteOnlyAttributes(override, schema), + Diagnostics: overrideDiags, + } + + return resp +} + func (n *NodeAbstractResource) processIgnoreChanges(prior, config cty.Value, schema *configschema.Block) (cty.Value, tfdiags.Diagnostics) { // ignore_changes only applies when an object already exists, since we // can't ignore changes to a thing we've not created yet. @@ -1653,8 +1659,12 @@ func (n *NodeAbstractResourceInstance) readDataSource(ctx EvalContext, configVal var resp providers.ReadDataSourceResponse if n.override != nil { + values, diags := ctx.EvaluateExpr(n.override.RawValue, cty.DynamicPseudoType, nil) + if diags.HasErrors() { + return newVal, deferred, diags + } override, overrideDiags := mocking.ComputedValuesForDataSource(configVal, &mocking.MockedData{ - Value: n.override.Values, + Value: values, Range: n.override.Range, ComputedAsUnknown: false, }, schema.Body) @@ -2628,8 +2638,12 @@ func (n *NodeAbstractResourceInstance) apply( // values the first time the object is created. Otherwise, we're happy // to just apply whatever the user asked for. if change.Action == plans.Create { + values, diags := ctx.EvaluateExpr(n.override.RawValue, cty.DynamicPseudoType, nil) + if diags.HasErrors() { + return nil, diags + } override, overrideDiags := mocking.ApplyComputedValuesForResource(unmarkedAfter, &mocking.MockedData{ - Value: n.override.Values, + Value: values, Range: n.override.Range, ComputedAsUnknown: false, }, schema.Body) diff --git a/internal/terraform/node_resource_plan_instance.go b/internal/terraform/node_resource_plan_instance.go index 74f7140a37c9..5265e8b344fb 100644 --- a/internal/terraform/node_resource_plan_instance.go +++ b/internal/terraform/node_resource_plan_instance.go @@ -642,8 +642,13 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs. // Let's pretend we're reading the value as a data source so we // pre-compute values now as if the resource has already been created. + values, diags := ctx.EvaluateExpr(n.override.RawValue, cty.DynamicPseudoType, nil) + if diags.HasErrors() { + return nil, deferred, diags + } + override, overrideDiags := mocking.ComputedValuesForDataSource(configVal, &mocking.MockedData{ - Value: n.override.Values, + Value: values, Range: n.override.Range, ComputedAsUnknown: false, }, schema.Body) From 3c610926ba0db7e9d9ea4bc8f8133a09b8d51d3e Mon Sep 17 00:00:00 2001 From: Samsondeen Dare Date: Thu, 6 Nov 2025 18:15:16 +0100 Subject: [PATCH 2/5] handle empty values --- internal/configs/mock_provider.go | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/internal/configs/mock_provider.go b/internal/configs/mock_provider.go index 6e1a8108db30..9e12ac195ae9 100644 --- a/internal/configs/mock_provider.go +++ b/internal/configs/mock_provider.go @@ -478,40 +478,16 @@ func decodeOverrideBlock(block *hcl.Block, attributeName string, blockName strin Subject: override.Range.Ptr(), }) } - if attribute, exists := content.Attributes[attributeName]; exists { override.ValuesRange = attribute.Range override.Values = cty.EmptyObjectVal override.RawValue = attribute.Expr - } else { - // It's fine if we don't have any values, just means we'll generate - // values for everything ourselves. We set this to an empty object so - // it's equivalent to `values = {}` which makes later processing easier. - override.Values = cty.EmptyObjectVal } useForPlan, useForPlanDiags := useForPlan(content, useForPlanDefault) diags = append(diags, useForPlanDiags...) override.UseForPlan = useForPlan - if !override.Values.Type().IsObjectType() { - - var attributePreposition string - switch attributeName { - case "outputs": - attributePreposition = "an" - default: - attributePreposition = "a" - } - - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: fmt.Sprintf("Invalid %s attribute", attributeName), - Detail: fmt.Sprintf("%s blocks must specify %s %s attribute that is an object.", blockName, attributePreposition, attributeName), - Subject: override.ValuesRange.Ptr(), - }) - } - return override, diags } From 42e5ade06358a8d7381c8ccbb28dafc08c0371d1 Mon Sep 17 00:00:00 2001 From: Samsondeen Dare Date: Fri, 7 Nov 2025 10:41:37 +0100 Subject: [PATCH 3/5] execute mock values block within the test runtime --- internal/command/test_test.go | 5 +- .../test/simple_pass_function/main.tf | 4 + .../test/simple_pass_function/main.tftest.hcl | 20 +++- internal/moduletest/graph/apply.go | 4 +- internal/moduletest/graph/eval_context.go | 12 ++ .../moduletest/graph/node_state_cleanup.go | 16 ++- internal/moduletest/graph/node_test_run.go | 21 +++- internal/moduletest/graph/plan.go | 9 +- internal/moduletest/mocking/overrides.go | 32 +++++- internal/moduletest/mocking/overrides_test.go | 2 +- internal/terraform/node_output.go | 108 +++++++----------- .../node_resource_abstract_instance.go | 56 ++++----- .../terraform/node_resource_plan_instance.go | 7 +- 13 files changed, 165 insertions(+), 131 deletions(-) diff --git a/internal/command/test_test.go b/internal/command/test_test.go index 9eeedfc7bbe3..1e7352a05733 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -420,8 +420,9 @@ func TestTest_Runs(t *testing.T) { code: 0, }, "simple_pass_function": { - expectedOut: []string{"1 passed, 0 failed."}, - code: 0, + expectedOut: []string{"2 passed, 0 failed."}, + code: 0, + expectedResourceCount: 0, }, "mocking-invalid-outputs": { override: "mocking-invalid", diff --git a/internal/command/testdata/test/simple_pass_function/main.tf b/internal/command/testdata/test/simple_pass_function/main.tf index 41cc84e5c4ea..6675af5dfb0d 100644 --- a/internal/command/testdata/test/simple_pass_function/main.tf +++ b/internal/command/testdata/test/simple_pass_function/main.tf @@ -1,3 +1,7 @@ resource "test_resource" "foo" { + value = "foo" +} + +resource "test_resource" "bar" { value = "bar" } diff --git a/internal/command/testdata/test/simple_pass_function/main.tftest.hcl b/internal/command/testdata/test/simple_pass_function/main.tftest.hcl index 8bc0bc034877..9ec34da2b96f 100644 --- a/internal/command/testdata/test/simple_pass_function/main.tftest.hcl +++ b/internal/command/testdata/test/simple_pass_function/main.tftest.hcl @@ -1,13 +1,27 @@ override_resource { target = test_resource.foo values = { - id = format("f-%s", "bar") + id = format("f-%s", "foo") } } -run "validate_test_resource" { +override_resource { + target = test_resource.bar + values = { + id = format("%s-%s", uuid(), "bar") + } +} + +run "validate_test_resource_foo" { + assert { + condition = test_resource.foo.id == "f-foo" + error_message = "invalid value" + } +} + +run "validate_test_resource_bar" { assert { - condition = test_resource.foo.id == "f-bar" + condition = length(test_resource.bar.id) > 10 error_message = "invalid value" } } diff --git a/internal/moduletest/graph/apply.go b/internal/moduletest/graph/apply.go index 0c9dfa21e9eb..7d46f50f3be6 100644 --- a/internal/moduletest/graph/apply.go +++ b/internal/moduletest/graph/apply.go @@ -23,7 +23,7 @@ import ( // testApply defines how to execute a run block representing an apply command // // See also: (n *NodeTestRun).testPlan -func (n *NodeTestRun) testApply(ctx *EvalContext, variables terraform.InputValues, providers map[addrs.RootProviderConfig]providers.Interface, mocks map[addrs.RootProviderConfig]*configs.MockData, waiter *operationWaiter) { +func (n *NodeTestRun) testApply(ctx *EvalContext, variables terraform.InputValues, providers map[addrs.RootProviderConfig]providers.Interface, waiter *operationWaiter) { file, run := n.File(), n.run config := run.ModuleConfig key := n.run.Config.StateKey @@ -37,7 +37,7 @@ func (n *NodeTestRun) testApply(ctx *EvalContext, variables terraform.InputValue tfCtx, _ := terraform.NewContext(n.opts.ContextOpts) // execute the terraform plan operation - _, plan, planDiags := plan(ctx, tfCtx, file.Config, run.Config, run.ModuleConfig, setVariables, providers, mocks, waiter) + _, plan, planDiags := plan(ctx, tfCtx, file.Config, run.Config, run.ModuleConfig, setVariables, providers, waiter) // Any error during the planning prevents our apply from // continuing which is an error. diff --git a/internal/moduletest/graph/eval_context.go b/internal/moduletest/graph/eval_context.go index dc356639a569..f4dbe780b166 100644 --- a/internal/moduletest/graph/eval_context.go +++ b/internal/moduletest/graph/eval_context.go @@ -23,6 +23,7 @@ import ( "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/lang/langrefs" "github.com/hashicorp/terraform/internal/moduletest" + "github.com/hashicorp/terraform/internal/moduletest/mocking" teststates "github.com/hashicorp/terraform/internal/moduletest/states" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" @@ -89,6 +90,8 @@ type EvalContext struct { // repair is true if the test suite is being run in cleanup repair mode. // It is only set when in test cleanup mode. repair bool + + overrides map[string]*mocking.Overrides } type EvalContextOpts struct { @@ -135,6 +138,7 @@ func NewEvalContext(opts EvalContextOpts) *EvalContext { mode: opts.Mode, deferralAllowed: opts.DeferralAllowed, evalSem: terraform.NewSemaphore(opts.Concurrency), + overrides: make(map[string]*mocking.Overrides), } } @@ -720,6 +724,14 @@ func (ec *EvalContext) PriorRunsCompleted(runs map[string]*moduletest.Run) bool return true } +func (ec *EvalContext) SetOverrides(run *moduletest.Run, overrides *mocking.Overrides) { + ec.overrides[run.Name] = overrides +} + +func (ec *EvalContext) GetOverrides(runName string) *mocking.Overrides { + return ec.overrides[runName] +} + // evaluationData augments an underlying lang.Data -- presumably resulting // from a terraform.Context.PlanAndEval or terraform.Context.ApplyAndEval call -- // with results from prior runs that should therefore be available when diff --git a/internal/moduletest/graph/node_state_cleanup.go b/internal/moduletest/graph/node_state_cleanup.go index 96072bbe1cc4..fb5b95ef2f2d 100644 --- a/internal/moduletest/graph/node_state_cleanup.go +++ b/internal/moduletest/graph/node_state_cleanup.go @@ -117,7 +117,7 @@ func (n *NodeStateCleanup) restore(ctx *EvalContext, file *configs.TestFile, run // we ignore the diagnostics from here, because we will have reported them // during the initial execution of the run block and we would not have // executed the run block if there were any errors. - providers, mocks, _ := getProviders(ctx, file, run, module) + providers, _, _ := getProviders(ctx, file, run, module) // During the destroy operation, we don't add warnings from this operation. // Anything that would have been reported here was already reported during @@ -128,7 +128,7 @@ func (n *NodeStateCleanup) restore(ctx *EvalContext, file *configs.TestFile, run planOpts := &terraform.PlanOpts{ Mode: plans.NormalMode, SetVariables: setVariables, - Overrides: mocking.PackageOverrides(run, file, mocks), + Overrides: ctx.GetOverrides(run.Name), ExternalProviders: providers, SkipRefresh: true, OverridePreventDestroy: true, @@ -177,10 +177,20 @@ func (n *NodeStateCleanup) destroy(ctx *EvalContext, file *configs.TestFile, run // we care about. setVariables, _, _ := FilterVariablesToModule(module, variables) + // TODO: Do we need the exact same overrides used in the plan? + hclctx, diags := ctx.HclContext(nil) + if diags != nil { + return state, diags + } + overrides, diags := mocking.PackageOverrides(hclctx, run, file, mocks) + if diags != nil { + return state, diags + } + planOpts := &terraform.PlanOpts{ Mode: plans.DestroyMode, SetVariables: setVariables, - Overrides: mocking.PackageOverrides(run, file, mocks), + Overrides: overrides, ExternalProviders: providers, SkipRefresh: true, OverridePreventDestroy: true, diff --git a/internal/moduletest/graph/node_test_run.go b/internal/moduletest/graph/node_test_run.go index 39413f383dcb..9fe268adec27 100644 --- a/internal/moduletest/graph/node_test_run.go +++ b/internal/moduletest/graph/node_test_run.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/moduletest" + "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" @@ -161,6 +162,22 @@ func (n *NodeTestRun) execute(ctx *EvalContext, waiter *operationWaiter) { return } + // Evaluate the override blocks + hclCtx, diags := ctx.HclContext(nil) + if diags != nil { + run.Status = moduletest.Error + run.Diagnostics = run.Diagnostics.Append(diags) + return + } + + overrides, diags := mocking.PackageOverrides(hclCtx, run.Config, file.Config, mocks) + if diags != nil { + run.Status = moduletest.Error + run.Diagnostics = run.Diagnostics.Append(diags) + return + } + ctx.SetOverrides(n.run, overrides) + n.testValidate(providers, waiter) if run.Diagnostics.HasErrors() { return @@ -174,9 +191,9 @@ func (n *NodeTestRun) execute(ctx *EvalContext, waiter *operationWaiter) { } if run.Config.Command == configs.PlanTestCommand { - n.testPlan(ctx, variables, providers, mocks, waiter) + n.testPlan(ctx, variables, providers, waiter) } else { - n.testApply(ctx, variables, providers, mocks, waiter) + n.testApply(ctx, variables, providers, waiter) } } diff --git a/internal/moduletest/graph/plan.go b/internal/moduletest/graph/plan.go index 17eebf233127..434537141b45 100644 --- a/internal/moduletest/graph/plan.go +++ b/internal/moduletest/graph/plan.go @@ -14,7 +14,6 @@ import ( "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/moduletest" - "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/terraform" @@ -24,7 +23,7 @@ import ( // testPlan defines how to execute a run block representing a plan command // // See also: (n *NodeTestRun).testApply -func (n *NodeTestRun) testPlan(ctx *EvalContext, variables terraform.InputValues, providers map[addrs.RootProviderConfig]providers.Interface, mocks map[addrs.RootProviderConfig]*configs.MockData, waiter *operationWaiter) { +func (n *NodeTestRun) testPlan(ctx *EvalContext, variables terraform.InputValues, providers map[addrs.RootProviderConfig]providers.Interface, waiter *operationWaiter) { file, run := n.File(), n.run config := run.ModuleConfig @@ -37,7 +36,7 @@ func (n *NodeTestRun) testPlan(ctx *EvalContext, variables terraform.InputValues tfCtx, _ := terraform.NewContext(n.opts.ContextOpts) // execute the terraform plan operation - planScope, plan, originalDiags := plan(ctx, tfCtx, file.Config, run.Config, run.ModuleConfig, setVariables, providers, mocks, waiter) + planScope, plan, originalDiags := plan(ctx, tfCtx, file.Config, run.Config, run.ModuleConfig, setVariables, providers, waiter) // We exclude the diagnostics that are expected to fail from the plan // diagnostics, and if an expected failure is not found, we add a new error diagnostic. planDiags := moduletest.ValidateExpectedFailures(run.Config, originalDiags) @@ -90,7 +89,7 @@ func (n *NodeTestRun) testPlan(ctx *EvalContext, variables terraform.InputValues run.Outputs = outputVals } -func plan(ctx *EvalContext, tfCtx *terraform.Context, file *configs.TestFile, run *configs.TestRun, module *configs.Config, variables terraform.InputValues, providers map[addrs.RootProviderConfig]providers.Interface, mocks map[addrs.RootProviderConfig]*configs.MockData, waiter *operationWaiter) (*lang.Scope, *plans.Plan, tfdiags.Diagnostics) { +func plan(ctx *EvalContext, tfCtx *terraform.Context, file *configs.TestFile, run *configs.TestRun, module *configs.Config, variables terraform.InputValues, providers map[addrs.RootProviderConfig]providers.Interface, waiter *operationWaiter) (*lang.Scope, *plans.Plan, tfdiags.Diagnostics) { log.Printf("[TRACE] TestFileRunner: called plan for %s", run.Name) var diags tfdiags.Diagnostics @@ -133,7 +132,7 @@ func plan(ctx *EvalContext, tfCtx *terraform.Context, file *configs.TestFile, ru SetVariables: variables, ExternalReferences: references, ExternalProviders: providers, - Overrides: mocking.PackageOverrides(run, file, mocks), + Overrides: ctx.GetOverrides(run.Name), DeferralAllowed: ctx.deferralAllowed, AllowRootEphemeralOutputs: true, } diff --git a/internal/moduletest/mocking/overrides.go b/internal/moduletest/mocking/overrides.go index 6e02bd411fc4..2519e7c87a61 100644 --- a/internal/moduletest/mocking/overrides.go +++ b/internal/moduletest/mocking/overrides.go @@ -4,8 +4,11 @@ package mocking import ( + "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" ) // Overrides contains a summary of all the overrides that should apply for a @@ -18,16 +21,30 @@ type Overrides struct { localOverrides addrs.Map[addrs.Targetable, *configs.Override] } -func PackageOverrides(run *configs.TestRun, file *configs.TestFile, mocks map[addrs.RootProviderConfig]*configs.MockData) *Overrides { +func PackageOverrides(ctx *hcl.EvalContext, run *configs.TestRun, file *configs.TestFile, mocks map[addrs.RootProviderConfig]*configs.MockData) (*Overrides, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics overrides := &Overrides{ providerOverrides: make(map[addrs.RootProviderConfig]addrs.Map[addrs.Targetable, *configs.Override]), localOverrides: addrs.MakeMap[addrs.Targetable, *configs.Override](), } + evalAndPut := func(container addrs.Map[addrs.Targetable, *configs.Override], target addrs.Targetable, override *configs.Override) tfdiags.Diagnostics { + var hclDiags hcl.Diagnostics + values := cty.EmptyObjectVal + if override.RawValue != nil { + values, hclDiags = override.RawValue.Value(ctx) + } + override.Values = values + container.Put(target, override) + return diags.Append(hclDiags) + } + // The run block overrides have the highest priority, we always include all // of them. for _, elem := range run.Overrides.Elems { - overrides.localOverrides.PutElement(elem) + if diags := evalAndPut(overrides.localOverrides, elem.Key, elem.Value); diags.HasErrors() { + return overrides, diags + } } // The file overrides are second, we include these as long as there isn't @@ -41,7 +58,9 @@ func PackageOverrides(run *configs.TestRun, file *configs.TestFile, mocks map[ad continue } - overrides.localOverrides.PutElement(elem) + if diags := evalAndPut(overrides.localOverrides, elem.Key, elem.Value); diags.HasErrors() { + return overrides, diags + } } // Finally, we want to include the overrides for any mock providers we have. @@ -58,11 +77,14 @@ func PackageOverrides(run *configs.TestRun, file *configs.TestFile, mocks map[ad if _, exists := overrides.providerOverrides[key]; !exists { overrides.providerOverrides[key] = addrs.MakeMap[addrs.Targetable, *configs.Override]() } - overrides.providerOverrides[key].PutElement(elem) + + if diags := evalAndPut(overrides.providerOverrides[key], elem.Key, elem.Value); diags.HasErrors() { + return overrides, diags + } } } - return overrides + return overrides, diags } // IsOverridden returns true if the module is either overridden directly or diff --git a/internal/moduletest/mocking/overrides_test.go b/internal/moduletest/mocking/overrides_test.go index 5788d64c4013..157798bcb09a 100644 --- a/internal/moduletest/mocking/overrides_test.go +++ b/internal/moduletest/mocking/overrides_test.go @@ -75,7 +75,7 @@ func TestPackageOverrides(t *testing.T) { }, } - overrides := PackageOverrides(run, file, mocks) + overrides, _ := PackageOverrides(nil, run, file, mocks) // We now expect that the run and file overrides took precedence. first, fOk := overrides.GetResourceOverride(primary, addrs.AbsProviderConfig{ diff --git a/internal/terraform/node_output.go b/internal/terraform/node_output.go index 960f68d19669..3e505a28fb79 100644 --- a/internal/terraform/node_output.go +++ b/internal/terraform/node_output.go @@ -138,7 +138,7 @@ func (n *nodeExpandOutput) DynamicExpand(ctx EvalContext) (*Graph, tfdiags.Diagn RefreshOnly: n.RefreshOnly, DestroyApply: n.Destroying, Planning: n.Planning, - Overrides: n.Overrides, + Override: n.getOverrideValue(absAddr.Module), Dependencies: n.Dependencies, AllowRootEphemeralOutputs: n.AllowRootEphemeralOutputs, } @@ -228,6 +228,41 @@ func (n *nodeExpandOutput) References() []*addrs.Reference { return referencesForOutput(n.Config) } +func (n *nodeExpandOutput) getOverrideValue(inst addrs.ModuleInstance) cty.Value { + // First check if we have any overrides at all, this is a shorthand for + // "are we running terraform test". + if n.Overrides.Empty() { + // cty.NilVal means no override + return cty.NilVal + } + + // We have overrides, let's see if we have one for this module instance. + if override, ok := n.Overrides.GetModuleOverride(inst); ok { + + output := n.Addr.Name + values := override.Values + + // The values.Type() should be an object type, but it might have + // been set to nil by a test or something. We can handle it in the + // same way as the attribute just not being specified. It's + // functionally the same for us and not something we need to raise + // alarms about. + if values.Type().IsObjectType() && values.Type().HasAttribute(output) { + return values.GetAttr(output) + } + + // If we don't have a value provided for an output, then we'll + // just set it to be null. + // + // TODO(liamcervante): Can we generate a value here? Probably + // not as we don't know the type. + return cty.NullVal(cty.DynamicPseudoType) + } + + // cty.NilVal indicates no override. + return cty.NilVal +} + // NodeApplyableOutput represents an output that is "applyable": // it is ready to be applied. type NodeApplyableOutput struct { @@ -246,10 +281,9 @@ type NodeApplyableOutput struct { Planning bool - // Overrides is the set of overrides applied by the testing framework. We - // may need to override the value for this output and if we do the value - // comes from here. - Overrides *mocking.Overrides + // Override provides the value to use for this output, if any. This can be + // set by testing framework when a module is overridden. + Override cty.Value // Dependencies is the full set of resources that are referenced by this // output. @@ -292,60 +326,6 @@ func (n *NodeApplyableOutput) ModulePath() addrs.Module { return n.Addr.Module.Module() } -func (n *NodeApplyableOutput) getOverrideValue(ctx EvalContext) (cty.Value, tfdiags.Diagnostics) { - // First check if we have any overrides at all, this is a shorthand for - // "are we running terraform test". - if n.Overrides.Empty() { - // cty.NilVal means no override - return cty.NilVal, nil - } - - // We have overrides, let's see if we have one for this module instance. - if override, ok := n.Overrides.GetModuleOverride(n.Addr.Module); ok { - - // If there is an override block with no specified outputs block, - // we set the value to null. - if override.RawValue == nil { - return cty.NullVal(cty.DynamicPseudoType), nil - } - - output := n.Addr.OutputValue.Name - values, diags := ctx.EvaluateExpr(override.RawValue, cty.DynamicPseudoType, nil) - if diags.HasErrors() { - return cty.NilVal, diags - } - - if !values.Type().IsObjectType() { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid outputs attribute", - Detail: fmt.Sprintf("%s blocks must specify an outputs attribute that is an object.", override.BlockName), - Subject: override.ValuesRange.Ptr(), - }) - return cty.NilVal, diags - } - - // The values.Type() should be an object type, but it might have - // been set to nil by a test or something. We can handle it in the - // same way as the attribute just not being specified. It's - // functionally the same for us and not something we need to raise - // alarms about. - if values.Type().IsObjectType() && values.Type().HasAttribute(output) { - return values.GetAttr(output), nil - } - - // If we don't have a value provided for an output, then we'll - // just set it to be null. - // - // TODO(liamcervante): Can we generate a value here? Probably - // not as we don't know the type. - return cty.NullVal(cty.DynamicPseudoType), nil - } - - // cty.NilVal indicates no override. - return cty.NilVal, nil -} - func referenceOutsideForOutput(addr addrs.AbsOutputValue) (selfPath, referencePath addrs.Module) { // Output values have their expressions resolved in the context of the // module where they are defined. @@ -440,13 +420,7 @@ func (n *NodeApplyableOutput) Execute(ctx EvalContext, op walkOperation) (diags // be valid, or may not have been registered at all. // We also don't evaluate checks for overridden outputs. This is because // any references within the checks will likely not have been created. - override, overrideDiags := n.getOverrideValue(ctx) - if overrideDiags.HasErrors() { - diags = diags.Append(overrideDiags) - return - } - - if !n.DestroyApply && override == cty.NilVal { + if !n.DestroyApply && n.Override == cty.NilVal { checkRuleSeverity := tfdiags.Error if n.RefreshOnly { checkRuleSeverity = tfdiags.Warning @@ -469,7 +443,7 @@ func (n *NodeApplyableOutput) Execute(ctx EvalContext, op walkOperation) (diags // First, we check if we have an overridden value. If we do, then we // use that and we don't try and evaluate the underlying expression. - val = override + val = n.Override if val == cty.NilVal { // This has to run before we have a state lock, since evaluation also // reads the state diff --git a/internal/terraform/node_resource_abstract_instance.go b/internal/terraform/node_resource_abstract_instance.go index c3eed04fc7ca..c5569b5491df 100644 --- a/internal/terraform/node_resource_abstract_instance.go +++ b/internal/terraform/node_resource_abstract_instance.go @@ -955,10 +955,19 @@ func (n *NodeAbstractResourceInstance) plan( if n.override != nil { // Then we have an override to apply for this change. But, overrides // only matter when we are creating a resource for the first time as we + // only apply computed values. if priorVal.IsNull() { // Then we are actually creating something, so let's populate the // computed values from our override value. - resp = n.planOverride(ctx, proposedNewVal, schema.Body) + override, overrideDiags := mocking.PlanComputedValuesForResource(proposedNewVal, &mocking.MockedData{ + Value: n.override.Values, + Range: n.override.Range, + ComputedAsUnknown: !n.override.UseForPlan, + }, schema.Body) + resp = providers.PlanResourceChangeResponse{ + PlannedState: ephemeral.StripWriteOnlyAttributes(override, schema.Body), + Diagnostics: overrideDiags, + } } else { // This is an update operation, and we don't actually have any // computed values that need to be applied. @@ -1159,7 +1168,15 @@ func (n *NodeAbstractResourceInstance) plan( if n.override != nil { // In this case, we are always creating the resource so we don't // do any validation, and just call out to the mocking library. - resp = n.planOverride(ctx, proposedNewVal, schema.Body) + override, overrideDiags := mocking.PlanComputedValuesForResource(proposedNewVal, &mocking.MockedData{ + Value: n.override.Values, + Range: n.override.Range, + ComputedAsUnknown: !n.override.UseForPlan, + }, schema.Body) + resp = providers.PlanResourceChangeResponse{ + PlannedState: ephemeral.StripWriteOnlyAttributes(override, schema.Body), + Diagnostics: overrideDiags, + } } else { resp = provider.PlanResourceChange(providers.PlanResourceChangeRequest{ TypeName: n.Addr.Resource.Resource.Type, @@ -1324,29 +1341,6 @@ func (n *NodeAbstractResourceInstance) plan( return plan, state, deferred, keyData, diags } -func (n *NodeAbstractResourceInstance) planOverride(ctx EvalContext, original cty.Value, schema *configschema.Block) providers.PlanResourceChangeResponse { - values, diags := ctx.EvaluateExpr(n.override.RawValue, cty.DynamicPseudoType, nil) - if diags.HasErrors() { - return providers.PlanResourceChangeResponse{ - Diagnostics: diags, - } - } - - // In this case, we are always creating the resource so we don't - // do any validation, and just call out to the mocking library. - override, overrideDiags := mocking.PlanComputedValuesForResource(original, &mocking.MockedData{ - Value: values, - Range: n.override.Range, - ComputedAsUnknown: !n.override.UseForPlan, - }, schema) - resp := providers.PlanResourceChangeResponse{ - PlannedState: ephemeral.StripWriteOnlyAttributes(override, schema), - Diagnostics: overrideDiags, - } - - return resp -} - func (n *NodeAbstractResource) processIgnoreChanges(prior, config cty.Value, schema *configschema.Block) (cty.Value, tfdiags.Diagnostics) { // ignore_changes only applies when an object already exists, since we // can't ignore changes to a thing we've not created yet. @@ -1659,12 +1653,8 @@ func (n *NodeAbstractResourceInstance) readDataSource(ctx EvalContext, configVal var resp providers.ReadDataSourceResponse if n.override != nil { - values, diags := ctx.EvaluateExpr(n.override.RawValue, cty.DynamicPseudoType, nil) - if diags.HasErrors() { - return newVal, deferred, diags - } override, overrideDiags := mocking.ComputedValuesForDataSource(configVal, &mocking.MockedData{ - Value: values, + Value: n.override.Values, Range: n.override.Range, ComputedAsUnknown: false, }, schema.Body) @@ -2638,12 +2628,8 @@ func (n *NodeAbstractResourceInstance) apply( // values the first time the object is created. Otherwise, we're happy // to just apply whatever the user asked for. if change.Action == plans.Create { - values, diags := ctx.EvaluateExpr(n.override.RawValue, cty.DynamicPseudoType, nil) - if diags.HasErrors() { - return nil, diags - } override, overrideDiags := mocking.ApplyComputedValuesForResource(unmarkedAfter, &mocking.MockedData{ - Value: values, + Value: n.override.Values, Range: n.override.Range, ComputedAsUnknown: false, }, schema.Body) diff --git a/internal/terraform/node_resource_plan_instance.go b/internal/terraform/node_resource_plan_instance.go index 5265e8b344fb..74f7140a37c9 100644 --- a/internal/terraform/node_resource_plan_instance.go +++ b/internal/terraform/node_resource_plan_instance.go @@ -642,13 +642,8 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs. // Let's pretend we're reading the value as a data source so we // pre-compute values now as if the resource has already been created. - values, diags := ctx.EvaluateExpr(n.override.RawValue, cty.DynamicPseudoType, nil) - if diags.HasErrors() { - return nil, deferred, diags - } - override, overrideDiags := mocking.ComputedValuesForDataSource(configVal, &mocking.MockedData{ - Value: values, + Value: n.override.Values, Range: n.override.Range, ComputedAsUnknown: false, }, schema.Body) From 2aafe2f4738758bea4a89244e59a76709c949508 Mon Sep 17 00:00:00 2001 From: Samsondeen Dare Date: Fri, 7 Nov 2025 20:55:38 +0100 Subject: [PATCH 4/5] handle functions in mock_* blocks --- .../test/simple_pass_function/main.tftest.hcl | 9 +++---- internal/configs/mock_provider.go | 6 ++--- internal/moduletest/graph/node_provider.go | 24 +++++++++++++++---- .../moduletest/graph/transform_providers.go | 19 ++++++++------- internal/moduletest/mocking/overrides.go | 11 +++++++++ 5 files changed, 48 insertions(+), 21 deletions(-) diff --git a/internal/command/testdata/test/simple_pass_function/main.tftest.hcl b/internal/command/testdata/test/simple_pass_function/main.tftest.hcl index 9ec34da2b96f..768bd51edb12 100644 --- a/internal/command/testdata/test/simple_pass_function/main.tftest.hcl +++ b/internal/command/testdata/test/simple_pass_function/main.tftest.hcl @@ -1,7 +1,8 @@ -override_resource { - target = test_resource.foo - values = { - id = format("f-%s", "foo") +mock_provider "test" { + mock_resource "test_resource" { + defaults = { + id = format("f-%s", "foo") + } } } diff --git a/internal/configs/mock_provider.go b/internal/configs/mock_provider.go index 9e12ac195ae9..15c386525d64 100644 --- a/internal/configs/mock_provider.go +++ b/internal/configs/mock_provider.go @@ -181,6 +181,7 @@ type MockResource struct { Type string Defaults cty.Value + RawValue hcl.Expression // UseForPlan is true if the values should be computed during the planning // phase. @@ -330,10 +331,8 @@ func decodeMockResourceBlock(block *hcl.Block, useForPlanDefault bool) (*MockRes } if defaults, exists := content.Attributes["defaults"]; exists { - var defaultDiags hcl.Diagnostics resource.DefaultsRange = defaults.Range - resource.Defaults, defaultDiags = defaults.Expr.Value(nil) - diags = append(diags, defaultDiags...) + resource.RawValue = defaults.Expr } else { // It's fine if we don't have any defaults, just means we'll generate // values for everything ourselves. @@ -480,7 +479,6 @@ func decodeOverrideBlock(block *hcl.Block, attributeName string, blockName strin } if attribute, exists := content.Attributes[attributeName]; exists { override.ValuesRange = attribute.Range - override.Values = cty.EmptyObjectVal override.RawValue = attribute.Expr } diff --git a/internal/moduletest/graph/node_provider.go b/internal/moduletest/graph/node_provider.go index d3bba441bc3f..eb79da5a5119 100644 --- a/internal/moduletest/graph/node_provider.go +++ b/internal/moduletest/graph/node_provider.go @@ -26,11 +26,12 @@ var ( type NodeProviderConfigure struct { name, alias string - Addr addrs.RootProviderConfig - File *moduletest.File - Config *configs.Provider - Provider providers.Interface - Schema providers.GetProviderSchemaResponse + Addr addrs.RootProviderConfig + File *moduletest.File + Config *configs.Provider + Provider providers.Interface + Schema providers.GetProviderSchemaResponse + MockProvider *providers.Mock } func (n *NodeProviderConfigure) Name() string { @@ -78,6 +79,19 @@ func (n *NodeProviderConfigure) Execute(ctx *EvalContext) { return } + if n.MockProvider != nil { + for _, res := range n.MockProvider.Data.MockResources { + values, hclDiags := res.RawValue.Value(hclContext) + n.File.Diagnostics.Append(hclDiags) + res.Defaults = values + } + for _, res := range n.MockProvider.Data.MockDataSources { + values, hclDiags := res.RawValue.Value(hclContext) + n.File.Diagnostics.Append(hclDiags) + res.Defaults = values + } + } + body, hclDiags := hcldec.Decode(n.Config.Config, spec, hclContext) n.File.AppendDiagnostics(moreDiags) if hclDiags.HasErrors() { diff --git a/internal/moduletest/graph/transform_providers.go b/internal/moduletest/graph/transform_providers.go index 34a1ddd028bb..4c2dd837d52d 100644 --- a/internal/moduletest/graph/transform_providers.go +++ b/internal/moduletest/graph/transform_providers.go @@ -44,12 +44,14 @@ func (t *TestProvidersTransformer) Transform(g *terraform.Graph) error { if err != nil { return fmt.Errorf("could not create provider instance: %w", err) } + var mock *providers.Mock if config.Mock { - impl = &providers.Mock{ + mock = &providers.Mock{ Provider: impl, Data: config.MockData, } + impl = mock } addr := addrs.RootProviderConfig{ @@ -58,13 +60,14 @@ func (t *TestProvidersTransformer) Transform(g *terraform.Graph) error { } configure := &NodeProviderConfigure{ - name: config.Name, - alias: config.Alias, - Addr: addr, - File: t.File, - Config: config, - Provider: impl, - Schema: impl.GetProviderSchema(), + name: config.Name, + alias: config.Alias, + Addr: addr, + File: t.File, + Config: config, + Provider: impl, + Schema: impl.GetProviderSchema(), + MockProvider: mock, } g.Add(configure) diff --git a/internal/moduletest/mocking/overrides.go b/internal/moduletest/mocking/overrides.go index 2519e7c87a61..05911ddef95a 100644 --- a/internal/moduletest/mocking/overrides.go +++ b/internal/moduletest/mocking/overrides.go @@ -4,6 +4,8 @@ package mocking import ( + "fmt" + "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" @@ -33,6 +35,15 @@ func PackageOverrides(ctx *hcl.EvalContext, run *configs.TestRun, file *configs. values := cty.EmptyObjectVal if override.RawValue != nil { values, hclDiags = override.RawValue.Value(ctx) + if values != cty.NilVal && !values.Type().IsObjectType() { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid outputs attribute", + Detail: fmt.Sprintf("%s blocks must specify an outputs attribute that is an object.", override.BlockName), + Subject: override.ValuesRange.Ptr(), + }) + return diags + } } override.Values = values container.Put(target, override) From 9b18bfa2e1ca144e6b6e329e957cfee556b09da2 Mon Sep 17 00:00:00 2001 From: Samsondeen Dare Date: Tue, 11 Nov 2025 13:07:57 +0100 Subject: [PATCH 5/5] Update eval_context.go --- internal/moduletest/graph/eval_context.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/moduletest/graph/eval_context.go b/internal/moduletest/graph/eval_context.go index f4dbe780b166..5e7b92fd2e7b 100644 --- a/internal/moduletest/graph/eval_context.go +++ b/internal/moduletest/graph/eval_context.go @@ -91,7 +91,8 @@ type EvalContext struct { // It is only set when in test cleanup mode. repair bool - overrides map[string]*mocking.Overrides + overrides map[string]*mocking.Overrides + overrideLock sync.Mutex } type EvalContextOpts struct { @@ -725,10 +726,14 @@ func (ec *EvalContext) PriorRunsCompleted(runs map[string]*moduletest.Run) bool } func (ec *EvalContext) SetOverrides(run *moduletest.Run, overrides *mocking.Overrides) { + ec.overrideLock.Lock() + defer ec.overrideLock.Unlock() ec.overrides[run.Name] = overrides } func (ec *EvalContext) GetOverrides(runName string) *mocking.Overrides { + ec.overrideLock.Lock() + defer ec.overrideLock.Unlock() return ec.overrides[runName] }