Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion internal/command/test_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"path/filepath"
"regexp"
"runtime"
"slices"
"sort"
"strings"
"testing"
Expand Down Expand Up @@ -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."},
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -418,6 +419,19 @@ func TestTest_Runs(t *testing.T) {
"no-tests": {
code: 0,
},
"simple_pass_function": {
expectedOut: []string{"2 passed, 0 failed."},
code: 0,
expectedResourceCount: 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) {
Expand All @@ -441,6 +455,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{
Expand Down
7 changes: 7 additions & 0 deletions internal/command/testdata/test/simple_pass_function/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
resource "test_resource" "foo" {
value = "foo"
}

resource "test_resource" "bar" {
value = "bar"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
mock_provider "test" {
mock_resource "test_resource" {
defaults = {
id = format("f-%s", "foo")
}
}
}

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 = length(test_resource.bar.id) > 10
error_message = "invalid value"
}
}
39 changes: 9 additions & 30 deletions internal/configs/mock_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -208,6 +209,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
Expand Down Expand Up @@ -325,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.
Expand Down Expand Up @@ -453,6 +457,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 {
Expand All @@ -472,41 +477,15 @@ func decodeOverrideBlock(block *hcl.Block, attributeName string, blockName strin
Subject: override.Range.Ptr(),
})
}

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...)
} 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
override.RawValue = attribute.Expr
}

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
}

Expand Down
4 changes: 2 additions & 2 deletions internal/moduletest/graph/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
17 changes: 17 additions & 0 deletions internal/moduletest/graph/eval_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -89,6 +90,9 @@ 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
overrideLock sync.Mutex
}

type EvalContextOpts struct {
Expand Down Expand Up @@ -135,6 +139,7 @@ func NewEvalContext(opts EvalContextOpts) *EvalContext {
mode: opts.Mode,
deferralAllowed: opts.DeferralAllowed,
evalSem: terraform.NewSemaphore(opts.Concurrency),
overrides: make(map[string]*mocking.Overrides),
}
}

Expand Down Expand Up @@ -720,6 +725,18 @@ func (ec *EvalContext) PriorRunsCompleted(runs map[string]*moduletest.Run) bool
return true
}

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

// 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
Expand Down
24 changes: 19 additions & 5 deletions internal/moduletest/graph/node_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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() {
Expand Down
16 changes: 13 additions & 3 deletions internal/moduletest/graph/node_state_cleanup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
21 changes: 19 additions & 2 deletions internal/moduletest/graph/node_test_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
}

Expand Down
Loading
Loading