| // 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 | |
| } |