| // Copyright IBM Corp. 2014, 2026 |
| // SPDX-License-Identifier: BUSL-1.1 |
| |
| package graph |
| |
| import ( |
| "fmt" |
| "log" |
| "path/filepath" |
| |
| "github.com/hashicorp/hcl/v2" |
| |
| "github.com/hashicorp/terraform/internal/addrs" |
| "github.com/hashicorp/terraform/internal/configs" |
| "github.com/hashicorp/terraform/internal/lang" |
| "github.com/hashicorp/terraform/internal/moduletest" |
| "github.com/hashicorp/terraform/internal/plans" |
| "github.com/hashicorp/terraform/internal/providers" |
| "github.com/hashicorp/terraform/internal/terraform" |
| "github.com/hashicorp/terraform/internal/tfdiags" |
| ) |
| |
| // 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, waiter *operationWaiter) { |
| file, run := n.File(), n.run |
| config := run.ModuleConfig |
| |
| // FilterVariablesToModule only returns warnings, so we don't check the |
| // returned diags for errors. |
| setVariables, testOnlyVariables, setVariableDiags := FilterVariablesToModule(run.ModuleConfig, variables) |
| run.Diagnostics = run.Diagnostics.Append(setVariableDiags) |
| |
| // ignore diags because validate has covered it |
| 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, 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) |
| |
| if ctx.Verbose() { |
| // in verbose mode, we still add all the original diagnostics for |
| // display. |
| run.Diagnostics = run.Diagnostics.Append(originalDiags) |
| } else { |
| run.Diagnostics = run.Diagnostics.Append(planDiags) |
| } |
| |
| if planDiags.HasErrors() { |
| run.Status = moduletest.Error |
| return |
| } |
| |
| if ctx.Verbose() { |
| schemas, diags := tfCtx.Schemas(config, plan.PriorState) |
| |
| // If we're going to fail to render the plan, let's not fail the overall |
| // test. It can still have succeeded. So we'll add the diagnostics, but |
| // still report the test status as a success. |
| if diags.HasErrors() { |
| // This is very unlikely. |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Warning, |
| "Failed to print verbose output", |
| fmt.Sprintf("Terraform failed to print the verbose output for %s, other diagnostics will contain more details as to why.", filepath.Join(file.Name, run.Name)))) |
| } else { |
| run.Verbose = &moduletest.Verbose{ |
| Plan: plan, |
| State: nil, // We don't have a state to show in plan mode. |
| Config: config, |
| Providers: schemas.Providers, |
| Provisioners: schemas.Provisioners, |
| } |
| } |
| |
| run.Diagnostics = run.Diagnostics.Append(diags) |
| } |
| |
| // Evaluate the run block directly in the graph context to validate the assertions |
| // of the run. We also pass in all the |
| // previous contexts so this run block can refer to outputs from |
| // previous run blocks. |
| status, outputVals, moreDiags := ctx.EvaluateRun(run.Config, run.ModuleConfig.Module, planScope, testOnlyVariables) |
| run.Status = run.Status.Merge(status) |
| run.Diagnostics = run.Diagnostics.Append(moreDiags) |
| 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, waiter *operationWaiter) (*lang.Scope, *plans.Plan, tfdiags.Diagnostics) { |
| log.Printf("[TRACE] TestFileRunner: called plan for %s", run.Name) |
| |
| var diags tfdiags.Diagnostics |
| |
| targets, targetDiags := moduletest.GetRunTargets(run) |
| diags = diags.Append(targetDiags) |
| |
| replaces, replaceDiags := moduletest.GetRunReplaces(run) |
| diags = diags.Append(replaceDiags) |
| |
| references, referenceDiags := moduletest.GetRunReferences(run) |
| diags = diags.Append(referenceDiags) |
| |
| state, err := ctx.LoadState(run) |
| if err != nil { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Failed to load state", |
| Detail: fmt.Sprintf("Could not retrieve state for run %s: %s.", run.Name, err), |
| Subject: run.Backend.DeclRange.Ptr(), |
| }) |
| } |
| |
| if diags.HasErrors() { |
| return nil, nil, diags |
| } |
| |
| planOpts := &terraform.PlanOpts{ |
| Mode: func() plans.Mode { |
| switch run.Options.Mode { |
| case configs.RefreshOnlyTestMode: |
| return plans.RefreshOnlyMode |
| default: |
| return plans.NormalMode |
| } |
| }(), |
| Targets: targets, |
| ForceReplace: replaces, |
| SkipRefresh: !run.Options.Refresh, |
| SetVariables: variables, |
| ExternalReferences: references, |
| ExternalProviders: providers, |
| Overrides: ctx.GetOverrides(run.Name), |
| DeferralAllowed: ctx.deferralAllowed, |
| AllowRootEphemeralOutputs: true, |
| } |
| |
| waiter.update(tfCtx, moduletest.Running, nil) |
| log.Printf("[DEBUG] TestFileRunner: starting plan for %s", run.Name) |
| plan, scope, planDiags := tfCtx.PlanAndEval(module, state, planOpts, addrs.RootModuleInstance) |
| log.Printf("[DEBUG] TestFileRunner: completed plan for %s", run.Name) |
| diags = diags.Append(planDiags) |
| |
| return scope, plan, diags |
| } |