// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package graph import ( "fmt" "log" "time" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/addrs" "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" ) var ( _ GraphNodeExecutable = (*NodeTestRun)(nil) _ GraphNodeReferenceable = (*NodeTestRun)(nil) _ GraphNodeReferencer = (*NodeTestRun)(nil) ) type NodeTestRun struct { run *moduletest.Run priorRuns map[string]*moduletest.Run opts *graphOptions } func (n *NodeTestRun) Run() *moduletest.Run { return n.run } func (n *NodeTestRun) File() *moduletest.File { return n.opts.File } func (n *NodeTestRun) Name() string { return fmt.Sprintf("%s.%s", n.opts.File.Name, n.run.Addr().String()) } func (n *NodeTestRun) Referenceable() addrs.Referenceable { return n.run.Addr() } func (n *NodeTestRun) References() []*addrs.Reference { references, _ := moduletest.GetRunReferences(n.run.Config) for _, run := range n.priorRuns { // we'll also draw an implicit reference to all prior runs to make sure // they execute first references = append(references, &addrs.Reference{ Subject: run.Addr(), SourceRange: tfdiags.SourceRangeFromHCL(n.run.Config.DeclRange), }) } for name, variable := range n.run.ModuleConfig.Module.Variables { // because we also draw implicit references back to any variables // defined in the test file with the same name as actual variables, then // we'll count these as references as well. if _, ok := n.run.Config.Variables[name]; ok { // BUT, if the variable is defined within the list of variables // within the run block then we don't want to draw an implicit // reference as the data comes from that expression. continue } references = append(references, &addrs.Reference{ Subject: addrs.InputVariable{Name: name}, SourceRange: tfdiags.SourceRangeFromHCL(variable.DeclRange), }) } return references } // Execute executes the test run block and update the status of the run block // based on the result of the execution. func (n *NodeTestRun) Execute(evalCtx *EvalContext) { log.Printf("[TRACE] TestFileRunner: executing run block %s/%s", n.File().Name, n.run.Name) startTime := time.Now().UTC() file, run := n.File(), n.run // At the end of the function, we'll update the status of the file based on // the status of the run block, and render the run summary. defer func() { evalCtx.Renderer().Run(run, file, moduletest.Complete, 0) file.UpdateStatus(run.Status) evalCtx.AddRunBlock(run) }() if !evalCtx.PriorRunsCompleted(n.priorRuns) || !evalCtx.ReferencesCompleted(n.References()) { // If any of our prior runs or references weren't completed successfully // then we will just skip this run block. run.Status = moduletest.Skip return } if evalCtx.Cancelled() { // A cancellation signal has been received. // Don't do anything, just give up and return immediately. // The surrounding functions should stop this even being called, but in // case of race conditions or something we can still verify this. return } if evalCtx.Stopped() { // Then the test was requested to be stopped, so we just mark each // following test as skipped, print the status, and move on. run.Status = moduletest.Skip return } // Create a waiter which handles waiting for terraform operations to complete. // While waiting, the wait will also respond to cancellation signals, and // handle them appropriately. // The test progress is updated periodically, and the progress status // depends on the async operation being waited on. // Before the terraform operation is started, the operation updates the // waiter with the cleanup context on cancellation, as well as the // progress status. waiter := NewOperationWaiter(nil, evalCtx, file, run, moduletest.Running, startTime.UnixMilli()) cancelled := waiter.Run(func() { defer logging.PanicHandler() n.execute(evalCtx, waiter) }) if cancelled { n.run.Diagnostics = n.run.Diagnostics.Append(tfdiags.Sourceless(tfdiags.Error, "Test interrupted", "The test operation could not be completed due to an interrupt signal. Please read the remaining diagnostics carefully for any sign of failed state cleanup or dangling resources.")) } // If we got far enough to actually attempt to execute the run then // we'll give the view some additional metadata about the execution. n.run.ExecutionMeta = &moduletest.RunExecutionMeta{ Start: startTime, Duration: time.Since(startTime), } } func (n *NodeTestRun) execute(ctx *EvalContext, waiter *operationWaiter) { file, run := n.File(), n.run ctx.Renderer().Run(run, file, moduletest.Starting, 0) providers, mocks, providerDiags := getProviders(ctx, file.Config, run.Config, run.ModuleConfig) if !ctx.ProvidersCompleted(providers) { run.Status = moduletest.Skip return } run.Diagnostics = run.Diagnostics.Append(providerDiags) if providerDiags.HasErrors() { run.Status = moduletest.Error return } // Evaluate the override blocks for this test run. // We use a context that only contains functions, and thus references are currently // not supported in the override/mock 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 } variables, variableDiags := GetVariables(ctx, run.Config, run.ModuleConfig, true) run.Diagnostics = run.Diagnostics.Append(variableDiags) if variableDiags.HasErrors() { run.Status = moduletest.Error return } if run.Config.Command == configs.PlanTestCommand { n.testPlan(ctx, variables, providers, waiter) } else { n.testApply(ctx, variables, providers, waiter) } } // Validating the module config which the run acts on func (n *NodeTestRun) testValidate(providers map[addrs.RootProviderConfig]providers.Interface, waiter *operationWaiter) { run := n.run file := n.File() config := run.ModuleConfig log.Printf("[TRACE] TestFileRunner: called validate for %s/%s", file.Name, run.Name) tfCtx, ctxDiags := terraform.NewContext(n.opts.ContextOpts) if ctxDiags.HasErrors() { return } waiter.update(tfCtx, moduletest.Running, nil) validateDiags := tfCtx.Validate(config, &terraform.ValidateOpts{ ExternalProviders: providers, AllowRootEphemeralOutputs: true, }) run.Diagnostics = run.Diagnostics.Append(validateDiags) if validateDiags.HasErrors() { run.Status = moduletest.Error return } } func getProviders(ctx *EvalContext, file *configs.TestFile, run *configs.TestRun, module *configs.Config) (map[addrs.RootProviderConfig]providers.Interface, map[addrs.RootProviderConfig]*configs.MockData, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics if len(run.Providers) > 0 { // Then we'll only provide the specific providers asked for by the run // block. providers := make(map[addrs.RootProviderConfig]providers.Interface, len(run.Providers)) mocks := make(map[addrs.RootProviderConfig]*configs.MockData) for _, ref := range run.Providers { testAddr := addrs.RootProviderConfig{ Provider: ctx.ProviderForConfigAddr(ref.InParent.Addr()), Alias: ref.InParent.Alias, } moduleAddr := addrs.RootProviderConfig{ Provider: module.ProviderForConfigAddr(ref.InChild.Addr()), Alias: ref.InChild.Alias, } if !testAddr.Provider.Equals(moduleAddr.Provider) { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Mismatched provider configuration", Detail: fmt.Sprintf("Expected %q but was %q.", moduleAddr.Provider, testAddr.Provider), Subject: ref.InChild.NameRange.Ptr(), }) continue } if provider, ok := ctx.GetProvider(testAddr); ok { providers[moduleAddr] = provider config := file.Providers[ref.InParent.String()] if config.Mock { mocks[moduleAddr] = config.MockData } } else { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Missing provider", Detail: fmt.Sprintf("Provider %q was not defined within the test file.", ref.InParent.String()), Subject: ref.InParent.NameRange.Ptr(), }) } } return providers, mocks, diags } else { // Otherwise, let's copy over all the relevant providers. providers := make(map[addrs.RootProviderConfig]providers.Interface) mocks := make(map[addrs.RootProviderConfig]*configs.MockData) for addr := range requiredProviders(module) { if provider, ok := ctx.GetProvider(addr); ok { providers[addr] = provider local := ctx.LocalNameForProvider(addr) if len(addr.Alias) > 0 { local = fmt.Sprintf("%s.%s", local, addr.Alias) } config := file.Providers[local] if config.Mock { mocks[addr] = config.MockData } } } return providers, mocks, diags } } func requiredProviders(config *configs.Config) map[addrs.RootProviderConfig]bool { providers := make(map[addrs.RootProviderConfig]bool) // First, let's look at the required providers first. for _, provider := range config.Module.ProviderRequirements.RequiredProviders { providers[addrs.RootProviderConfig{ Provider: provider.Type, }] = true for _, alias := range provider.Aliases { providers[addrs.RootProviderConfig{ Provider: provider.Type, Alias: alias.Alias, }] = true } } // Second, we look at the defined provider configs. for _, provider := range config.Module.ProviderConfigs { providers[addrs.RootProviderConfig{ Provider: config.ProviderForConfigAddr(provider.Addr()), Alias: provider.Alias, }] = true } // Third, we look at the resources and data sources. for _, resource := range config.Module.ManagedResources { if resource.ProviderConfigRef != nil { providers[addrs.RootProviderConfig{ Provider: config.ProviderForConfigAddr(resource.ProviderConfigRef.Addr()), Alias: resource.ProviderConfigRef.Alias, }] = true continue } providers[addrs.RootProviderConfig{ Provider: resource.Provider, }] = true } for _, datasource := range config.Module.DataResources { if datasource.ProviderConfigRef != nil { providers[addrs.RootProviderConfig{ Provider: config.ProviderForConfigAddr(datasource.ProviderConfigRef.Addr()), Alias: datasource.ProviderConfigRef.Alias, }] = true continue } providers[addrs.RootProviderConfig{ Provider: datasource.Provider, }] = true } // Finally, we look at any module calls to see if any providers are used // in there. for _, module := range config.Module.ModuleCalls { for _, provider := range module.Providers { providers[addrs.RootProviderConfig{ Provider: config.ProviderForConfigAddr(provider.InParent.Addr()), Alias: provider.InParent.Alias, }] = true } } return providers }