mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
[Testing Framework] Add module block to test run blocks (#33456)
* [testing framework] prepare for beta phase of development * [Testing Framework] Add module block to test run blocks
This commit is contained in:
parent
c9bc7e8479
commit
5acc95dda7
@ -158,7 +158,7 @@ func (c *InitCommand) Run(args []string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load just the root module to begin backend and module initialization
|
// Load just the root module to begin backend and module initialization
|
||||||
rootModEarly, earlyConfDiags := c.loadSingleModule(path)
|
rootModEarly, earlyConfDiags := c.loadSingleModuleWithTests(path, "tests")
|
||||||
|
|
||||||
// There may be parsing errors in config loading but these will be shown later _after_
|
// There may be parsing errors in config loading but these will be shown later _after_
|
||||||
// checking for core version requirement errors. Not meeting the version requirement should
|
// checking for core version requirement errors. Not meeting the version requirement should
|
||||||
@ -329,7 +329,16 @@ func (c *InitCommand) Run(args []string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *InitCommand) getModules(path string, earlyRoot *configs.Module, upgrade bool) (output bool, abort bool, diags tfdiags.Diagnostics) {
|
func (c *InitCommand) getModules(path string, earlyRoot *configs.Module, upgrade bool) (output bool, abort bool, diags tfdiags.Diagnostics) {
|
||||||
if len(earlyRoot.ModuleCalls) == 0 {
|
testModules := false // We can also have modules buried in test files.
|
||||||
|
for _, file := range earlyRoot.Tests {
|
||||||
|
for _, run := range file.Runs {
|
||||||
|
if run.Module != nil {
|
||||||
|
testModules = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(earlyRoot.ModuleCalls) == 0 && !testModules {
|
||||||
// Nothing to do
|
// Nothing to do
|
||||||
return false, false, nil
|
return false, false, nil
|
||||||
}
|
}
|
||||||
|
@ -2711,6 +2711,72 @@ func TestInit_invalidSyntaxInvalidBackend(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInit_tests(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := t.TempDir()
|
||||||
|
testCopyDir(t, testFixturePath("init-with-tests"), td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
provider := applyFixtureProvider() // We just want the types from this provider.
|
||||||
|
|
||||||
|
providerSource, close := newMockProviderSource(t, map[string][]string{
|
||||||
|
"hashicorp/test": {"1.0.0"},
|
||||||
|
})
|
||||||
|
defer close()
|
||||||
|
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
view, _ := testView(t)
|
||||||
|
c := &InitCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
testingOverrides: metaOverridesForProvider(provider),
|
||||||
|
Ui: ui,
|
||||||
|
View: view,
|
||||||
|
ProviderSource: providerSource,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInit_testsWithModule(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := t.TempDir()
|
||||||
|
testCopyDir(t, testFixturePath("init-with-tests-with-module"), td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
provider := applyFixtureProvider() // We just want the types from this provider.
|
||||||
|
|
||||||
|
providerSource, close := newMockProviderSource(t, map[string][]string{
|
||||||
|
"hashicorp/test": {"1.0.0"},
|
||||||
|
})
|
||||||
|
defer close()
|
||||||
|
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
view, _ := testView(t)
|
||||||
|
c := &InitCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
testingOverrides: metaOverridesForProvider(provider),
|
||||||
|
Ui: ui,
|
||||||
|
View: view,
|
||||||
|
ProviderSource: providerSource,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check output
|
||||||
|
output := ui.OutputWriter.String()
|
||||||
|
if !strings.Contains(output, "test.main.setup in setup") {
|
||||||
|
t.Fatalf("doesn't look like we installed the test module': %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// newMockProviderSource is a helper to succinctly construct a mock provider
|
// newMockProviderSource is a helper to succinctly construct a mock provider
|
||||||
// source that contains a set of packages matching the given provider versions
|
// source that contains a set of packages matching the given provider versions
|
||||||
// that are available for installation (from temporary local files).
|
// that are available for installation (from temporary local files).
|
||||||
|
@ -12,6 +12,9 @@ import (
|
|||||||
|
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
"github.com/zclconf/go-cty/cty/convert"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/internal/configs"
|
"github.com/hashicorp/terraform/internal/configs"
|
||||||
"github.com/hashicorp/terraform/internal/configs/configload"
|
"github.com/hashicorp/terraform/internal/configs/configload"
|
||||||
"github.com/hashicorp/terraform/internal/configs/configschema"
|
"github.com/hashicorp/terraform/internal/configs/configschema"
|
||||||
@ -19,8 +22,6 @@ import (
|
|||||||
"github.com/hashicorp/terraform/internal/registry"
|
"github.com/hashicorp/terraform/internal/registry"
|
||||||
"github.com/hashicorp/terraform/internal/terraform"
|
"github.com/hashicorp/terraform/internal/terraform"
|
||||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
"github.com/zclconf/go-cty/cty/convert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// normalizePath normalizes a given path so that it is, if possible, relative
|
// normalizePath normalizes a given path so that it is, if possible, relative
|
||||||
@ -73,6 +74,23 @@ func (m *Meta) loadSingleModule(dir string) (*configs.Module, tfdiags.Diagnostic
|
|||||||
return module, diags
|
return module, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadSingleModuleWithTests matches loadSingleModule except it also loads any
|
||||||
|
// tests for the target module.
|
||||||
|
func (m *Meta) loadSingleModuleWithTests(dir string, testDir string) (*configs.Module, tfdiags.Diagnostics) {
|
||||||
|
var diags tfdiags.Diagnostics
|
||||||
|
dir = m.normalizePath(dir)
|
||||||
|
|
||||||
|
loader, err := m.initConfigLoader()
|
||||||
|
if err != nil {
|
||||||
|
diags = diags.Append(err)
|
||||||
|
return nil, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
module, hclDiags := loader.Parser().LoadConfigDirWithTests(dir, testDir)
|
||||||
|
diags = diags.Append(hclDiags)
|
||||||
|
return module, diags
|
||||||
|
}
|
||||||
|
|
||||||
// dirIsConfigPath checks if the given path is a directory that contains at
|
// dirIsConfigPath checks if the given path is a directory that contains at
|
||||||
// least one Terraform configuration file (.tf or .tf.json), returning true
|
// least one Terraform configuration file (.tf or .tf.json), returning true
|
||||||
// if so.
|
// if so.
|
||||||
|
@ -142,60 +142,43 @@ func (c *TestCommand) ExecuteTestSuite(suite *moduletest.Suite, config *configs.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *TestCommand) ExecuteTestFile(ctx *terraform.Context, file *moduletest.File, config *configs.Config, view views.Test) {
|
func (c *TestCommand) ExecuteTestFile(ctx *terraform.Context, file *moduletest.File, config *configs.Config, view views.Test) {
|
||||||
var diags tfdiags.Diagnostics
|
|
||||||
|
|
||||||
globalVariableValues, diags := c.CollectDefaultVariables(file.Config.Variables, config)
|
mgr := new(TestStateManager)
|
||||||
if diags.HasErrors() {
|
mgr.c = c
|
||||||
file.Status = file.Status.Merge(moduletest.Error)
|
mgr.State = states.NewState()
|
||||||
view.File(file)
|
defer mgr.cleanupStates(ctx, view, file, config)
|
||||||
view.Diagnostics(nil, file, diags)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
state := states.NewState()
|
|
||||||
defer func() {
|
|
||||||
|
|
||||||
// Whatever happens, at the end of this test we don't want to leave
|
|
||||||
// active resources behind. So we'll do a destroy action against the
|
|
||||||
// state in a deferred function.
|
|
||||||
|
|
||||||
plan, planDiags := ctx.Plan(config, state, &terraform.PlanOpts{
|
|
||||||
Mode: plans.DestroyMode,
|
|
||||||
SetVariables: globalVariableValues,
|
|
||||||
})
|
|
||||||
if planDiags.HasErrors() {
|
|
||||||
// This is bad, we need to tell the user that we couldn't clean up
|
|
||||||
// and they need to go and manually delete some resources.
|
|
||||||
view.DestroySummary(planDiags, file, state)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
view.Diagnostics(nil, file, planDiags) // Print out any warnings from the destroy plan.
|
|
||||||
|
|
||||||
finalState, applyDiags := ctx.Apply(plan, config)
|
|
||||||
view.DestroySummary(applyDiags, file, finalState)
|
|
||||||
}()
|
|
||||||
|
|
||||||
file.Status = file.Status.Merge(moduletest.Pass)
|
file.Status = file.Status.Merge(moduletest.Pass)
|
||||||
for _, run := range file.Runs {
|
for _, run := range file.Runs {
|
||||||
if file.Status == moduletest.Error {
|
if file.Status == moduletest.Error {
|
||||||
|
// If the overall test file has errored, we don't keep trying to
|
||||||
|
// execute tests. Instead, we mark all remaining run blocks as
|
||||||
|
// skipped.
|
||||||
run.Status = moduletest.Skip
|
run.Status = moduletest.Skip
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
state = c.ExecuteTestRun(ctx, run, state, config, globalVariableValues)
|
if run.Config.ConfigUnderTest != nil {
|
||||||
|
// Then we want to execute a different module under a kind of
|
||||||
|
// sandbox.
|
||||||
|
state := c.ExecuteTestRun(ctx, run, states.NewState(), run.Config.ConfigUnderTest, file.Config.Variables)
|
||||||
|
mgr.States = append(mgr.States, &TestModuleState{
|
||||||
|
State: state,
|
||||||
|
Run: run,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
mgr.State = c.ExecuteTestRun(ctx, run, mgr.State, config, file.Config.Variables)
|
||||||
|
}
|
||||||
file.Status = file.Status.Merge(run.Status)
|
file.Status = file.Status.Merge(run.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
view.File(file)
|
view.File(file)
|
||||||
view.Diagnostics(nil, file, diags)
|
|
||||||
|
|
||||||
for _, run := range file.Runs {
|
for _, run := range file.Runs {
|
||||||
view.Run(run, file)
|
view.Run(run, file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TestCommand) ExecuteTestRun(ctx *terraform.Context, run *moduletest.Run, state *states.State, config *configs.Config, defaults terraform.InputValues) *states.State {
|
func (c *TestCommand) ExecuteTestRun(ctx *terraform.Context, run *moduletest.Run, state *states.State, config *configs.Config, globals map[string]hcl.Expression) *states.State {
|
||||||
|
|
||||||
var targets []addrs.Targetable
|
var targets []addrs.Targetable
|
||||||
for _, target := range run.Config.Options.Target {
|
for _, target := range run.Config.Options.Target {
|
||||||
addr, diags := addrs.ParseTarget(target)
|
addr, diags := addrs.ParseTarget(target)
|
||||||
@ -230,7 +213,7 @@ func (c *TestCommand) ExecuteTestRun(ctx *terraform.Context, run *moduletest.Run
|
|||||||
replaces = append(replaces, addr)
|
replaces = append(replaces, addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
variables, diags := c.OverrideDefaultVariables(run.Config.Variables, config, defaults)
|
variables, diags := c.GetInputValues(run.Config.Variables, globals, config)
|
||||||
run.Diagnostics = run.Diagnostics.Append(diags)
|
run.Diagnostics = run.Diagnostics.Append(diags)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
run.Status = moduletest.Error
|
run.Status = moduletest.Error
|
||||||
@ -287,9 +270,27 @@ func (c *TestCommand) ExecuteTestRun(ctx *terraform.Context, run *moduletest.Run
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TestCommand) CollectDefaultVariables(exprs map[string]hcl.Expression, config *configs.Config) (terraform.InputValues, tfdiags.Diagnostics) {
|
func (c *TestCommand) GetInputValues(locals map[string]hcl.Expression, globals map[string]hcl.Expression, config *configs.Config) (terraform.InputValues, tfdiags.Diagnostics) {
|
||||||
|
variables := make(map[string]hcl.Expression)
|
||||||
|
for name := range config.Module.Variables {
|
||||||
|
if expr, exists := locals[name]; exists {
|
||||||
|
// Local variables take precedence.
|
||||||
|
variables[name] = expr
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if expr, exists := globals[name]; exists {
|
||||||
|
// If it's not set locally, it maybe set globally.
|
||||||
|
variables[name] = expr
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's not set at all that might be okay if the variable is optional
|
||||||
|
// so we'll just not add anything to the map.
|
||||||
|
}
|
||||||
|
|
||||||
unparsed := make(map[string]backend.UnparsedVariableValue)
|
unparsed := make(map[string]backend.UnparsedVariableValue)
|
||||||
for key, value := range exprs {
|
for key, value := range variables {
|
||||||
unparsed[key] = unparsedVariableValueExpression{
|
unparsed[key] = unparsedVariableValueExpression{
|
||||||
expr: value,
|
expr: value,
|
||||||
sourceType: terraform.ValueFromConfig,
|
sourceType: terraform.ValueFromConfig,
|
||||||
@ -298,33 +299,83 @@ func (c *TestCommand) CollectDefaultVariables(exprs map[string]hcl.Expression, c
|
|||||||
return backend.ParseVariableValues(unparsed, config.Module.Variables)
|
return backend.ParseVariableValues(unparsed, config.Module.Variables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TestCommand) OverrideDefaultVariables(exprs map[string]hcl.Expression, config *configs.Config, existing terraform.InputValues) (terraform.InputValues, tfdiags.Diagnostics) {
|
func (c *TestCommand) cleanupState(ctx *terraform.Context, view views.Test, run *moduletest.Run, file *moduletest.File, config *configs.Config, state *states.State) {
|
||||||
if len(exprs) == 0 {
|
if state.Empty() {
|
||||||
return existing, nil
|
// Nothing to do.
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
decls := make(map[string]*configs.Variable)
|
var locals map[string]hcl.Expression
|
||||||
unparsed := make(map[string]backend.UnparsedVariableValue)
|
if run != nil {
|
||||||
for name, variable := range exprs {
|
locals = run.Config.Variables
|
||||||
|
|
||||||
if config, ok := config.Module.Variables[name]; ok {
|
|
||||||
decls[name] = config
|
|
||||||
}
|
|
||||||
|
|
||||||
unparsed[name] = unparsedVariableValueExpression{
|
|
||||||
expr: variable,
|
|
||||||
sourceType: terraform.ValueFromConfig,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
overrides, diags := backend.ParseVariableValues(unparsed, decls)
|
variables, variableDiags := c.GetInputValues(locals, file.Config.Variables, config)
|
||||||
values := make(terraform.InputValues)
|
if variableDiags.HasErrors() {
|
||||||
for name, value := range existing {
|
// This shouldn't really trigger, as we will have created something
|
||||||
if override, ok := overrides[name]; ok {
|
// using these variables at an earlier stage so for them to have a
|
||||||
values[name] = override
|
// problem now would be strange. But just to be safe we'll handle this.
|
||||||
continue
|
view.DestroySummary(variableDiags, run, file, state)
|
||||||
}
|
return
|
||||||
values[name] = value
|
}
|
||||||
|
view.Diagnostics(nil, file, variableDiags)
|
||||||
|
|
||||||
|
plan, planDiags := ctx.Plan(config, state, &terraform.PlanOpts{
|
||||||
|
Mode: plans.DestroyMode,
|
||||||
|
SetVariables: variables,
|
||||||
|
})
|
||||||
|
if planDiags.HasErrors() {
|
||||||
|
// This is bad, we need to tell the user that we couldn't clean up
|
||||||
|
// and they need to go and manually delete some resources.
|
||||||
|
view.DestroySummary(planDiags, run, file, state)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
view.Diagnostics(nil, file, planDiags) // Print out any warnings from the destroy plan.
|
||||||
|
|
||||||
|
finalState, applyDiags := ctx.Apply(plan, config)
|
||||||
|
view.DestroySummary(applyDiags, run, file, finalState)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStateManager is a helper struct to maintain the various state objects
|
||||||
|
// that a test file has to keep track of.
|
||||||
|
type TestStateManager struct {
|
||||||
|
c *TestCommand
|
||||||
|
|
||||||
|
// State is the main state of the module under test during a single test
|
||||||
|
// file execution. This state will be updated by every run block without
|
||||||
|
// a modifier module block within the test file. At the end of the test
|
||||||
|
// file's execution everything in this state should be executed.
|
||||||
|
State *states.State
|
||||||
|
|
||||||
|
// States contains the states of every run block within a test file that
|
||||||
|
// executed using an alternative module. Any resources created by these
|
||||||
|
// run blocks also need to be tidied up, but only after the main state file
|
||||||
|
// has been handled.
|
||||||
|
States []*TestModuleState
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestModuleState holds the config and the state for a given run block that
|
||||||
|
// executed with a custom module.
|
||||||
|
type TestModuleState struct {
|
||||||
|
// State is the state after the module executed.
|
||||||
|
State *states.State
|
||||||
|
|
||||||
|
// File is the config for the file containing the Run.
|
||||||
|
File *moduletest.File
|
||||||
|
|
||||||
|
// Run is the config for the given run block, that contains the config
|
||||||
|
// under test and the variable values.
|
||||||
|
Run *moduletest.Run
|
||||||
|
}
|
||||||
|
|
||||||
|
func (manager *TestStateManager) cleanupStates(ctx *terraform.Context, view views.Test, file *moduletest.File, config *configs.Config) {
|
||||||
|
// First, we'll clean up the main state.
|
||||||
|
manager.c.cleanupState(ctx, view, nil, file, config, manager.State)
|
||||||
|
|
||||||
|
// Then we'll clean up the additional states for custom modules in reverse
|
||||||
|
// order.
|
||||||
|
for ix := len(manager.States); ix > 0; ix-- {
|
||||||
|
state := manager.States[ix-1]
|
||||||
|
manager.c.cleanupState(ctx, view, state.Run, file, state.Run.Config.ConfigUnderTest, state.State)
|
||||||
}
|
}
|
||||||
return values, diags
|
|
||||||
}
|
}
|
||||||
|
@ -197,10 +197,6 @@ func TestTest_ProviderAlias(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTest_ModuleDependencies(t *testing.T) {
|
func TestTest_ModuleDependencies(t *testing.T) {
|
||||||
// TODO(liamcervante): Enable this test once we have added support for
|
|
||||||
// module customisation into the testing framework.
|
|
||||||
t.Skip()
|
|
||||||
|
|
||||||
td := t.TempDir()
|
td := t.TempDir()
|
||||||
testCopyDir(t, testFixturePath(path.Join("test", "with_setup_module")), td)
|
testCopyDir(t, testFixturePath(path.Join("test", "with_setup_module")), td)
|
||||||
defer testChdir(t, td)()
|
defer testChdir(t, td)()
|
||||||
|
3
internal/command/testdata/init-with-tests-with-module/main.tf
vendored
Normal file
3
internal/command/testdata/init-with-tests-with-module/main.tf
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
resource "test_instance" "foo" {
|
||||||
|
ami = "bar"
|
||||||
|
}
|
12
internal/command/testdata/init-with-tests-with-module/main.tftest
vendored
Normal file
12
internal/command/testdata/init-with-tests-with-module/main.tftest
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
run "setup" {
|
||||||
|
module {
|
||||||
|
source = "./setup"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run "test" {
|
||||||
|
assert {
|
||||||
|
condition = test_instance.foo.ami == "bar"
|
||||||
|
error_message = "incorrect value"
|
||||||
|
}
|
||||||
|
}
|
3
internal/command/testdata/init-with-tests-with-module/setup/main.tf
vendored
Normal file
3
internal/command/testdata/init-with-tests-with-module/setup/main.tf
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
resource "test_instance" "baz" {
|
||||||
|
ami = "baz"
|
||||||
|
}
|
3
internal/command/testdata/init-with-tests/main.tf
vendored
Normal file
3
internal/command/testdata/init-with-tests/main.tf
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
resource "test_instance" "foo" {
|
||||||
|
ami = "bar"
|
||||||
|
}
|
6
internal/command/testdata/init-with-tests/main.tftest
vendored
Normal file
6
internal/command/testdata/init-with-tests/main.tftest
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
run "test" {
|
||||||
|
assert {
|
||||||
|
condition = test_instance.foo.ami == "bar"
|
||||||
|
error_message = "incorrect value"
|
||||||
|
}
|
||||||
|
}
|
@ -34,7 +34,7 @@ type Test interface {
|
|||||||
|
|
||||||
// DestroySummary prints out the summary of the destroy step of each test
|
// DestroySummary prints out the summary of the destroy step of each test
|
||||||
// file. If everything goes well, this should be empty.
|
// file. If everything goes well, this should be empty.
|
||||||
DestroySummary(diags tfdiags.Diagnostics, file *moduletest.File, state *states.State)
|
DestroySummary(diags tfdiags.Diagnostics, run *moduletest.Run, file *moduletest.File, state *states.State)
|
||||||
|
|
||||||
// Diagnostics prints out the provided diagnostics.
|
// Diagnostics prints out the provided diagnostics.
|
||||||
Diagnostics(run *moduletest.Run, file *moduletest.File, diags tfdiags.Diagnostics)
|
Diagnostics(run *moduletest.Run, file *moduletest.File, diags tfdiags.Diagnostics)
|
||||||
@ -112,14 +112,19 @@ func (t *TestHuman) Run(run *moduletest.Run, file *moduletest.File) {
|
|||||||
t.Diagnostics(run, file, run.Diagnostics)
|
t.Diagnostics(run, file, run.Diagnostics)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TestHuman) DestroySummary(diags tfdiags.Diagnostics, file *moduletest.File, state *states.State) {
|
func (t *TestHuman) DestroySummary(diags tfdiags.Diagnostics, run *moduletest.Run, file *moduletest.File, state *states.State) {
|
||||||
if diags.HasErrors() {
|
identifier := file.Name
|
||||||
t.view.streams.Eprintf("Terraform encountered an error destroying resources created while executing %s.\n", file.Name)
|
if run != nil {
|
||||||
|
identifier = fmt.Sprintf("%s/%s", identifier, run.Name)
|
||||||
}
|
}
|
||||||
t.Diagnostics(nil, file, diags)
|
|
||||||
|
if diags.HasErrors() {
|
||||||
|
t.view.streams.Eprintf("Terraform encountered an error destroying resources created while executing %s.\n", identifier)
|
||||||
|
}
|
||||||
|
t.Diagnostics(run, file, diags)
|
||||||
|
|
||||||
if state.HasManagedResourceInstanceObjects() {
|
if state.HasManagedResourceInstanceObjects() {
|
||||||
t.view.streams.Eprintf("\nTerraform left the following resources in state after executing %s, they need to be cleaned up manually:\n", file.Name)
|
t.view.streams.Eprintf("\nTerraform left the following resources in state after executing %s, they need to be cleaned up manually:\n", identifier)
|
||||||
for _, resource := range state.AllResourceInstanceObjectAddrs() {
|
for _, resource := range state.AllResourceInstanceObjectAddrs() {
|
||||||
if resource.DeposedKey != states.NotDeposed {
|
if resource.DeposedKey != states.NotDeposed {
|
||||||
t.view.streams.Eprintf(" - %s (%s)\n", resource.Instance, resource.DeposedKey)
|
t.view.streams.Eprintf(" - %s (%s)\n", resource.Instance, resource.DeposedKey)
|
||||||
@ -239,7 +244,7 @@ func (t TestJSON) Run(run *moduletest.Run, file *moduletest.File) {
|
|||||||
t.Diagnostics(run, file, run.Diagnostics)
|
t.Diagnostics(run, file, run.Diagnostics)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t TestJSON) DestroySummary(diags tfdiags.Diagnostics, file *moduletest.File, state *states.State) {
|
func (t TestJSON) DestroySummary(diags tfdiags.Diagnostics, run *moduletest.Run, file *moduletest.File, state *states.State) {
|
||||||
if state.HasManagedResourceInstanceObjects() {
|
if state.HasManagedResourceInstanceObjects() {
|
||||||
cleanup := json.TestFileCleanup{}
|
cleanup := json.TestFileCleanup{}
|
||||||
for _, resource := range state.AllResourceInstanceObjectAddrs() {
|
for _, resource := range state.AllResourceInstanceObjectAddrs() {
|
||||||
@ -249,14 +254,24 @@ func (t TestJSON) DestroySummary(diags tfdiags.Diagnostics, file *moduletest.Fil
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
t.view.log.Error(
|
if run != nil {
|
||||||
fmt.Sprintf("Terraform left some resources in state after executing %s, they need to be cleaned up manually.", file.Name),
|
t.view.log.Error(
|
||||||
"type", json.MessageTestCleanup,
|
fmt.Sprintf("Terraform left some resources in state after executing %s/%s, they need to be cleaned up manually.", file.Name, run.Name),
|
||||||
json.MessageTestCleanup, cleanup,
|
"type", json.MessageTestCleanup,
|
||||||
"@testfile", file.Name)
|
json.MessageTestCleanup, cleanup,
|
||||||
|
"@testfile", file.Name,
|
||||||
|
"@testrun", run.Name)
|
||||||
|
} else {
|
||||||
|
t.view.log.Error(
|
||||||
|
fmt.Sprintf("Terraform left some resources in state after executing %s, they need to be cleaned up manually.", file.Name),
|
||||||
|
"type", json.MessageTestCleanup,
|
||||||
|
json.MessageTestCleanup, cleanup,
|
||||||
|
"@testfile", file.Name)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Diagnostics(nil, file, diags)
|
t.Diagnostics(run, file, diags)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t TestJSON) Diagnostics(run *moduletest.Run, file *moduletest.File, diags tfdiags.Diagnostics) {
|
func (t TestJSON) Diagnostics(run *moduletest.Run, file *moduletest.File, diags tfdiags.Diagnostics) {
|
||||||
|
@ -555,6 +555,7 @@ something bad happened during this test
|
|||||||
func TestTestHuman_DestroySummary(t *testing.T) {
|
func TestTestHuman_DestroySummary(t *testing.T) {
|
||||||
tcs := map[string]struct {
|
tcs := map[string]struct {
|
||||||
diags tfdiags.Diagnostics
|
diags tfdiags.Diagnostics
|
||||||
|
run *moduletest.Run
|
||||||
file *moduletest.File
|
file *moduletest.File
|
||||||
state *states.State
|
state *states.State
|
||||||
stdout string
|
stdout string
|
||||||
@ -603,6 +604,20 @@ some thing not very bad happened again
|
|||||||
|
|
||||||
Error: first error
|
Error: first error
|
||||||
|
|
||||||
|
this time it is very bad
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"error_from_run": {
|
||||||
|
diags: tfdiags.Diagnostics{
|
||||||
|
tfdiags.Sourceless(tfdiags.Error, "first error", "this time it is very bad"),
|
||||||
|
},
|
||||||
|
run: &moduletest.Run{Name: "run_block"},
|
||||||
|
file: &moduletest.File{Name: "main.tftest"},
|
||||||
|
state: states.NewState(),
|
||||||
|
stderr: `Terraform encountered an error destroying resources created while executing main.tftest/run_block.
|
||||||
|
|
||||||
|
Error: first error
|
||||||
|
|
||||||
this time it is very bad
|
this time it is very bad
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
@ -746,7 +761,7 @@ Terraform left the following resources in state after executing main.tftest, the
|
|||||||
streams, done := terminal.StreamsForTesting(t)
|
streams, done := terminal.StreamsForTesting(t)
|
||||||
view := NewTest(arguments.ViewHuman, NewView(streams))
|
view := NewTest(arguments.ViewHuman, NewView(streams))
|
||||||
|
|
||||||
view.DestroySummary(tc.diags, tc.file, tc.state)
|
view.DestroySummary(tc.diags, tc.run, tc.file, tc.state)
|
||||||
|
|
||||||
output := done(t)
|
output := done(t)
|
||||||
actual, expected := output.Stdout(), tc.stdout
|
actual, expected := output.Stdout(), tc.stdout
|
||||||
@ -1355,6 +1370,7 @@ func TestTestJSON_Conclusion(t *testing.T) {
|
|||||||
func TestTestJSON_DestroySummary(t *testing.T) {
|
func TestTestJSON_DestroySummary(t *testing.T) {
|
||||||
tcs := map[string]struct {
|
tcs := map[string]struct {
|
||||||
file *moduletest.File
|
file *moduletest.File
|
||||||
|
run *moduletest.Run
|
||||||
state *states.State
|
state *states.State
|
||||||
diags tfdiags.Diagnostics
|
diags tfdiags.Diagnostics
|
||||||
want []map[string]interface{}
|
want []map[string]interface{}
|
||||||
@ -1440,6 +1456,42 @@ func TestTestJSON_DestroySummary(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"state_from_run": {
|
||||||
|
file: &moduletest.File{Name: "main.tftest"},
|
||||||
|
run: &moduletest.Run{Name: "run_block"},
|
||||||
|
state: states.BuildState(func(state *states.SyncState) {
|
||||||
|
state.SetResourceInstanceCurrent(
|
||||||
|
addrs.Resource{
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Type: "test",
|
||||||
|
Name: "foo",
|
||||||
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||||
|
&states.ResourceInstanceObjectSrc{
|
||||||
|
Status: states.ObjectReady,
|
||||||
|
},
|
||||||
|
addrs.AbsProviderConfig{
|
||||||
|
Module: addrs.RootModule,
|
||||||
|
Provider: addrs.NewDefaultProvider("test"),
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
want: []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"@level": "error",
|
||||||
|
"@message": "Terraform left some resources in state after executing main.tftest/run_block, they need to be cleaned up manually.",
|
||||||
|
"@module": "terraform.ui",
|
||||||
|
"@testfile": "main.tftest",
|
||||||
|
"@testrun": "run_block",
|
||||||
|
"test_cleanup": map[string]interface{}{
|
||||||
|
"failed_resources": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"instance": "test.foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": "test_cleanup",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
"state_only_warnings": {
|
"state_only_warnings": {
|
||||||
diags: tfdiags.Diagnostics{
|
diags: tfdiags.Diagnostics{
|
||||||
tfdiags.Sourceless(tfdiags.Warning, "first warning", "something not very bad happened"),
|
tfdiags.Sourceless(tfdiags.Warning, "first warning", "something not very bad happened"),
|
||||||
@ -1651,7 +1703,7 @@ func TestTestJSON_DestroySummary(t *testing.T) {
|
|||||||
streams, done := terminal.StreamsForTesting(t)
|
streams, done := terminal.StreamsForTesting(t)
|
||||||
view := NewTest(arguments.ViewJSON, NewView(streams))
|
view := NewTest(arguments.ViewJSON, NewView(streams))
|
||||||
|
|
||||||
view.DestroySummary(tc.diags, tc.file, tc.state)
|
view.DestroySummary(tc.diags, tc.run, tc.file, tc.state)
|
||||||
testJSONViewOutputEquals(t, done(t).All(), tc.want)
|
testJSONViewOutputEquals(t, done(t).All(), tc.want)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -5,10 +5,13 @@ package configs
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
version "github.com/hashicorp/go-version"
|
version "github.com/hashicorp/go-version"
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/internal/addrs"
|
"github.com/hashicorp/terraform/internal/addrs"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -26,6 +29,7 @@ func BuildConfig(root *Module, walker ModuleWalker) (*Config, hcl.Diagnostics) {
|
|||||||
}
|
}
|
||||||
cfg.Root = cfg // Root module is self-referential.
|
cfg.Root = cfg // Root module is self-referential.
|
||||||
cfg.Children, diags = buildChildModules(cfg, walker)
|
cfg.Children, diags = buildChildModules(cfg, walker)
|
||||||
|
diags = append(diags, buildTestModules(cfg, walker)...)
|
||||||
|
|
||||||
// Skip provider resolution if there are any errors, since the provider
|
// Skip provider resolution if there are any errors, since the provider
|
||||||
// configurations themselves may not be valid.
|
// configurations themselves may not be valid.
|
||||||
@ -40,6 +44,64 @@ func BuildConfig(root *Module, walker ModuleWalker) (*Config, hcl.Diagnostics) {
|
|||||||
return cfg, diags
|
return cfg, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildTestModules(root *Config, walker ModuleWalker) hcl.Diagnostics {
|
||||||
|
var diags hcl.Diagnostics
|
||||||
|
|
||||||
|
for name, file := range root.Module.Tests {
|
||||||
|
for _, run := range file.Runs {
|
||||||
|
if run.Module == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want to make sure the path for the testing modules are unique
|
||||||
|
// so we create a dedicated path for them.
|
||||||
|
//
|
||||||
|
// Some examples:
|
||||||
|
// - file: main.tftest, run: setup - test.main.setup
|
||||||
|
// - file: tests/main.tftest, run: setup - test.tests.main.setup
|
||||||
|
|
||||||
|
dir := path.Dir(name)
|
||||||
|
base := path.Base(name)
|
||||||
|
|
||||||
|
path := addrs.Module{}
|
||||||
|
path = append(path, "test")
|
||||||
|
if dir != "." {
|
||||||
|
path = append(path, strings.Split(dir, "/")...)
|
||||||
|
}
|
||||||
|
path = append(path, strings.TrimSuffix(base, ".tftest"), run.Name)
|
||||||
|
|
||||||
|
req := ModuleRequest{
|
||||||
|
Name: run.Name,
|
||||||
|
Path: path,
|
||||||
|
SourceAddr: run.Module.Source,
|
||||||
|
SourceAddrRange: run.Module.SourceDeclRange,
|
||||||
|
VersionConstraint: run.Module.Version,
|
||||||
|
Parent: root,
|
||||||
|
CallRange: run.Module.DeclRange,
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, modDiags := loadModule(root, &req, walker)
|
||||||
|
diags = append(diags, modDiags...)
|
||||||
|
|
||||||
|
if cfg != nil {
|
||||||
|
// To get the loader to work, we need to set a bunch of values
|
||||||
|
// (like the name, path, and parent) as if the module was being
|
||||||
|
// loaded as a child of the root config.
|
||||||
|
//
|
||||||
|
// In actuality, when this is executed it will be as if the
|
||||||
|
// module was the root. So, we'll post-process some things to
|
||||||
|
// get it to behave as expected later.
|
||||||
|
cfg.Path = addrs.RootModule
|
||||||
|
cfg.Parent = nil
|
||||||
|
cfg.Root = cfg
|
||||||
|
run.ConfigUnderTest = cfg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
func buildChildModules(parent *Config, walker ModuleWalker) (map[string]*Config, hcl.Diagnostics) {
|
func buildChildModules(parent *Config, walker ModuleWalker) (map[string]*Config, hcl.Diagnostics) {
|
||||||
var diags hcl.Diagnostics
|
var diags hcl.Diagnostics
|
||||||
ret := map[string]*Config{}
|
ret := map[string]*Config{}
|
||||||
@ -69,54 +131,67 @@ func buildChildModules(parent *Config, walker ModuleWalker) (map[string]*Config,
|
|||||||
Parent: parent,
|
Parent: parent,
|
||||||
CallRange: call.DeclRange,
|
CallRange: call.DeclRange,
|
||||||
}
|
}
|
||||||
|
child, modDiags := loadModule(parent.Root, &req, walker)
|
||||||
mod, ver, modDiags := walker.LoadModule(&req)
|
|
||||||
diags = append(diags, modDiags...)
|
diags = append(diags, modDiags...)
|
||||||
if mod == nil {
|
if child == nil {
|
||||||
// nil can be returned if the source address was invalid and so
|
// This means an error occurred, there should be diagnostics within
|
||||||
// nothing could be loaded whatsoever. LoadModule should've
|
// modDiags for this.
|
||||||
// returned at least one error diagnostic in that case.
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
child := &Config{
|
|
||||||
Parent: parent,
|
|
||||||
Root: parent.Root,
|
|
||||||
Path: path,
|
|
||||||
Module: mod,
|
|
||||||
CallRange: call.DeclRange,
|
|
||||||
SourceAddr: call.SourceAddr,
|
|
||||||
SourceAddrRange: call.SourceAddrRange,
|
|
||||||
Version: ver,
|
|
||||||
}
|
|
||||||
|
|
||||||
child.Children, modDiags = buildChildModules(child, walker)
|
|
||||||
diags = append(diags, modDiags...)
|
|
||||||
|
|
||||||
if mod.Backend != nil {
|
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagWarning,
|
|
||||||
Summary: "Backend configuration ignored",
|
|
||||||
Detail: "Any selected backend applies to the entire configuration, so Terraform expects provider configurations only in the root module.\n\nThis is a warning rather than an error because it's sometimes convenient to temporarily call a root module as a child module for testing purposes, but this backend configuration block will have no effect.",
|
|
||||||
Subject: mod.Backend.DeclRange.Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(mod.Import) > 0 {
|
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid import configuration",
|
|
||||||
Detail: fmt.Sprintf("An import block was detected in %q. Import blocks are only allowed in the root module.", child.Path),
|
|
||||||
Subject: mod.Import[0].DeclRange.Ptr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ret[call.Name] = child
|
ret[call.Name] = child
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret, diags
|
return ret, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadModule(root *Config, req *ModuleRequest, walker ModuleWalker) (*Config, hcl.Diagnostics) {
|
||||||
|
var diags hcl.Diagnostics
|
||||||
|
|
||||||
|
mod, ver, modDiags := walker.LoadModule(req)
|
||||||
|
diags = append(diags, modDiags...)
|
||||||
|
if mod == nil {
|
||||||
|
// nil can be returned if the source address was invalid and so
|
||||||
|
// nothing could be loaded whatsoever. LoadModule should've
|
||||||
|
// returned at least one error diagnostic in that case.
|
||||||
|
return nil, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &Config{
|
||||||
|
Parent: req.Parent,
|
||||||
|
Root: root,
|
||||||
|
Path: req.Path,
|
||||||
|
Module: mod,
|
||||||
|
CallRange: req.CallRange,
|
||||||
|
SourceAddr: req.SourceAddr,
|
||||||
|
SourceAddrRange: req.SourceAddrRange,
|
||||||
|
Version: ver,
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Children, modDiags = buildChildModules(cfg, walker)
|
||||||
|
diags = append(diags, modDiags...)
|
||||||
|
|
||||||
|
if mod.Backend != nil {
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagWarning,
|
||||||
|
Summary: "Backend configuration ignored",
|
||||||
|
Detail: "Any selected backend applies to the entire configuration, so Terraform expects provider configurations only in the root module.\n\nThis is a warning rather than an error because it's sometimes convenient to temporarily call a root module as a child module for testing purposes, but this backend configuration block will have no effect.",
|
||||||
|
Subject: mod.Backend.DeclRange.Ptr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mod.Import) > 0 {
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Invalid import configuration",
|
||||||
|
Detail: fmt.Sprintf("An import block was detected in %q. Import blocks are only allowed in the root module.", cfg.Path),
|
||||||
|
Subject: mod.Import[0].DeclRange.Ptr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, diags
|
||||||
|
}
|
||||||
|
|
||||||
// A ModuleWalker knows how to find and load a child module given details about
|
// A ModuleWalker knows how to find and load a child module given details about
|
||||||
// the module to be loaded and a reference to its partially-loaded parent
|
// the module to be loaded and a reference to its partially-loaded parent
|
||||||
// Config.
|
// Config.
|
||||||
|
@ -282,3 +282,59 @@ func TestBuildConfigInvalidModules(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildConfig_WithTestModule(t *testing.T) {
|
||||||
|
parser := NewParser(nil)
|
||||||
|
mod, diags := parser.LoadConfigDirWithTests("testdata/valid-modules/with-tests-module", "tests")
|
||||||
|
assertNoDiagnostics(t, diags)
|
||||||
|
if mod == nil {
|
||||||
|
t.Fatal("got nil root module; want non-nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, diags := BuildConfig(mod, ModuleWalkerFunc(
|
||||||
|
func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) {
|
||||||
|
// For the sake of this test we're going to just treat our
|
||||||
|
// SourceAddr as a path relative to our fixture directory.
|
||||||
|
// A "real" implementation of ModuleWalker should accept the
|
||||||
|
// various different source address syntaxes Terraform supports.
|
||||||
|
sourcePath := filepath.Join("testdata/valid-modules/with-tests-module", req.SourceAddr.String())
|
||||||
|
|
||||||
|
mod, diags := parser.LoadConfigDir(sourcePath)
|
||||||
|
version, _ := version.NewVersion("1.0.0")
|
||||||
|
return mod, version, diags
|
||||||
|
},
|
||||||
|
))
|
||||||
|
assertNoDiagnostics(t, diags)
|
||||||
|
if cfg == nil {
|
||||||
|
t.Fatal("got nil config; want non-nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should have loaded our test case, and one of the test runs should
|
||||||
|
// have loaded an alternate module.
|
||||||
|
|
||||||
|
if len(cfg.Module.Tests) != 1 {
|
||||||
|
t.Fatalf("expected exactly one test case but found %d", len(cfg.Module.Tests))
|
||||||
|
}
|
||||||
|
|
||||||
|
test := cfg.Module.Tests["main.tftest"]
|
||||||
|
if len(test.Runs) != 2 {
|
||||||
|
t.Fatalf("expected two test runs but found %d", len(test.Runs))
|
||||||
|
}
|
||||||
|
|
||||||
|
run := test.Runs[0]
|
||||||
|
if run.ConfigUnderTest == nil {
|
||||||
|
t.Fatalf("the first test run should have loaded config but did not")
|
||||||
|
}
|
||||||
|
|
||||||
|
if run.ConfigUnderTest.Parent != nil {
|
||||||
|
t.Errorf("config under test should not have a parent")
|
||||||
|
}
|
||||||
|
|
||||||
|
if run.ConfigUnderTest.Root != run.ConfigUnderTest {
|
||||||
|
t.Errorf("config under test root should be itself")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(run.ConfigUnderTest.Path) > 0 {
|
||||||
|
t.Errorf("config under test path should be the root module")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,6 +5,9 @@ import (
|
|||||||
|
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
"github.com/hashicorp/hcl/v2/gohcl"
|
"github.com/hashicorp/hcl/v2/gohcl"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/internal/addrs"
|
||||||
|
"github.com/hashicorp/terraform/internal/getmodules"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestCommand represents the Terraform a given run block will execute, plan
|
// TestCommand represents the Terraform a given run block will execute, plan
|
||||||
@ -78,6 +81,23 @@ type TestRun struct {
|
|||||||
// checked by this run block.
|
// checked by this run block.
|
||||||
CheckRules []*CheckRule
|
CheckRules []*CheckRule
|
||||||
|
|
||||||
|
// Module defines an address of another module that should be loaded and
|
||||||
|
// executed as part of this run block instead of the module under test.
|
||||||
|
//
|
||||||
|
// In the initial version of the testing framework we will only support
|
||||||
|
// loading alternate modules from local directories or the registry.
|
||||||
|
Module *TestRunModuleCall
|
||||||
|
|
||||||
|
// ConfigUnderTest describes the configuration this run block should execute
|
||||||
|
// against.
|
||||||
|
//
|
||||||
|
// In typical cases, this will be null and the config under test is the
|
||||||
|
// configuration within the directory the terraform test command is
|
||||||
|
// executing within. However, when Module is set the config under test is
|
||||||
|
// whichever config is defined by Module. This field is then set during the
|
||||||
|
// configuration load process and should be used when the test is executed.
|
||||||
|
ConfigUnderTest *Config
|
||||||
|
|
||||||
// ExpectFailures should be a list of checkable objects that are expected
|
// ExpectFailures should be a list of checkable objects that are expected
|
||||||
// to report a failure from their custom conditions as part of this test
|
// to report a failure from their custom conditions as part of this test
|
||||||
// run.
|
// run.
|
||||||
@ -88,6 +108,19 @@ type TestRun struct {
|
|||||||
DeclRange hcl.Range
|
DeclRange hcl.Range
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestRunModuleCall specifies which module should be executed by a given run
|
||||||
|
// block.
|
||||||
|
type TestRunModuleCall struct {
|
||||||
|
// Source is the source of the module to test.
|
||||||
|
Source addrs.ModuleSource
|
||||||
|
|
||||||
|
// Version is the version of the module to load from the registry.
|
||||||
|
Version VersionConstraint
|
||||||
|
|
||||||
|
DeclRange hcl.Range
|
||||||
|
SourceDeclRange hcl.Range
|
||||||
|
}
|
||||||
|
|
||||||
// TestRunOptions contains the plan options for a given run block.
|
// TestRunOptions contains the plan options for a given run block.
|
||||||
type TestRunOptions struct {
|
type TestRunOptions struct {
|
||||||
// Mode is the planning mode to run in. One of ['normal', 'refresh-only'].
|
// Mode is the planning mode to run in. One of ['normal', 'refresh-only'].
|
||||||
@ -200,7 +233,21 @@ func decodeTestRunBlock(block *hcl.Block) (*TestRun, hcl.Diagnostics) {
|
|||||||
for _, v := range vars {
|
for _, v := range vars {
|
||||||
r.Variables[v.Name] = v.Expr
|
r.Variables[v.Name] = v.Expr
|
||||||
}
|
}
|
||||||
|
case "module":
|
||||||
|
if r.Module != nil {
|
||||||
|
diags = append(diags, &hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Multiple \"module\" blocks",
|
||||||
|
Detail: fmt.Sprintf("This run block already has a module block defined at %s.", r.Module.DeclRange),
|
||||||
|
Subject: block.DefRange.Ptr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module, moduleDiags := decodeTestRunModuleBlock(block)
|
||||||
|
diags = append(diags, moduleDiags...)
|
||||||
|
if !moduleDiags.HasErrors() {
|
||||||
|
r.Module = module
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,6 +294,110 @@ func decodeTestRunBlock(block *hcl.Block) (*TestRun, hcl.Diagnostics) {
|
|||||||
return &r, diags
|
return &r, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decodeTestRunModuleBlock(block *hcl.Block) (*TestRunModuleCall, hcl.Diagnostics) {
|
||||||
|
var diags hcl.Diagnostics
|
||||||
|
|
||||||
|
content, contentDiags := block.Body.Content(testRunModuleBlockSchema)
|
||||||
|
diags = append(diags, contentDiags...)
|
||||||
|
|
||||||
|
module := TestRunModuleCall{
|
||||||
|
DeclRange: block.DefRange,
|
||||||
|
}
|
||||||
|
|
||||||
|
haveVersionArg := false
|
||||||
|
if attr, exists := content.Attributes["version"]; exists {
|
||||||
|
var versionDiags hcl.Diagnostics
|
||||||
|
module.Version, versionDiags = decodeVersionConstraint(attr)
|
||||||
|
diags = append(diags, versionDiags...)
|
||||||
|
haveVersionArg = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if attr, exists := content.Attributes["source"]; exists {
|
||||||
|
module.SourceDeclRange = attr.Range
|
||||||
|
|
||||||
|
var raw string
|
||||||
|
rawDiags := gohcl.DecodeExpression(attr.Expr, nil, &raw)
|
||||||
|
diags = append(diags, rawDiags...)
|
||||||
|
if !rawDiags.HasErrors() {
|
||||||
|
var err error
|
||||||
|
if haveVersionArg {
|
||||||
|
module.Source, err = addrs.ParseModuleSourceRegistry(raw)
|
||||||
|
} else {
|
||||||
|
module.Source, err = addrs.ParseModuleSource(raw)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
// NOTE: We leave mc.SourceAddr as nil for any situation where the
|
||||||
|
// source attribute is invalid, so any code which tries to carefully
|
||||||
|
// use the partial result of a failed config decode must be
|
||||||
|
// resilient to that.
|
||||||
|
module.Source = nil
|
||||||
|
|
||||||
|
// NOTE: In practice it's actually very unlikely to end up here,
|
||||||
|
// because our source address parser can turn just about any string
|
||||||
|
// into some sort of remote package address, and so for most errors
|
||||||
|
// we'll detect them only during module installation. There are
|
||||||
|
// still a _few_ purely-syntax errors we can catch at parsing time,
|
||||||
|
// though, mostly related to remote package sub-paths and local
|
||||||
|
// paths.
|
||||||
|
switch err := err.(type) {
|
||||||
|
case *getmodules.MaybeRelativePathErr:
|
||||||
|
diags = append(diags, &hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Invalid module source address",
|
||||||
|
Detail: fmt.Sprintf(
|
||||||
|
"Terraform failed to determine your intended installation method for remote module package %q.\n\nIf you intended this as a path relative to the current module, use \"./%s\" instead. The \"./\" prefix indicates that the address is a relative filesystem path.",
|
||||||
|
err.Addr, err.Addr,
|
||||||
|
),
|
||||||
|
Subject: module.SourceDeclRange.Ptr(),
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
if haveVersionArg {
|
||||||
|
// In this case we'll include some extra context that
|
||||||
|
// we assumed a registry source address due to the
|
||||||
|
// version argument.
|
||||||
|
diags = append(diags, &hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Invalid registry module source address",
|
||||||
|
Detail: fmt.Sprintf("Failed to parse module registry address: %s.\n\nTerraform assumed that you intended a module registry source address because you also set the argument \"version\", which applies only to registry modules.", err),
|
||||||
|
Subject: module.SourceDeclRange.Ptr(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
diags = append(diags, &hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Invalid module source address",
|
||||||
|
Detail: fmt.Sprintf("Failed to parse module source address: %s.", err),
|
||||||
|
Subject: module.SourceDeclRange.Ptr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch module.Source.(type) {
|
||||||
|
case addrs.ModuleSourceRemote:
|
||||||
|
// We only support local or registry modules when loading
|
||||||
|
// modules directly from alternate sources during a test
|
||||||
|
// execution.
|
||||||
|
diags = append(diags, &hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Invalid module source address",
|
||||||
|
Detail: "Only local or registry module sources are currently supported from within test run blocks.",
|
||||||
|
Subject: module.SourceDeclRange.Ptr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Must have a source attribute.
|
||||||
|
diags = append(diags, &hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Missing \"source\" attribute for module block",
|
||||||
|
Detail: "You must specify a source attribute when executing alternate modules during test executions.",
|
||||||
|
Subject: module.DeclRange.Ptr(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &module, diags
|
||||||
|
}
|
||||||
|
|
||||||
func decodeTestRunOptionsBlock(block *hcl.Block) (*TestRunOptions, hcl.Diagnostics) {
|
func decodeTestRunOptionsBlock(block *hcl.Block) (*TestRunOptions, hcl.Diagnostics) {
|
||||||
var diags hcl.Diagnostics
|
var diags hcl.Diagnostics
|
||||||
|
|
||||||
@ -334,6 +485,9 @@ var testRunBlockSchema = &hcl.BodySchema{
|
|||||||
{
|
{
|
||||||
Type: "variables",
|
Type: "variables",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Type: "module",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,3 +499,10 @@ var testRunOptionsBlockSchema = &hcl.BodySchema{
|
|||||||
{Name: "target"},
|
{Name: "target"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var testRunModuleBlockSchema = &hcl.BodySchema{
|
||||||
|
Attributes: []hcl.AttributeSchema{
|
||||||
|
{Name: "source"},
|
||||||
|
{Name: "version"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
12
internal/configs/testdata/valid-modules/with-tests-module/main.tf
vendored
Normal file
12
internal/configs/testdata/valid-modules/with-tests-module/main.tf
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
variable "managed_id" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
data "test_data_source" "managed_data" {
|
||||||
|
id = var.managed_id
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "test_resource" "created" {
|
||||||
|
value = data.test_data_source.managed_data.value
|
||||||
|
}
|
21
internal/configs/testdata/valid-modules/with-tests-module/main.tftest
vendored
Normal file
21
internal/configs/testdata/valid-modules/with-tests-module/main.tftest
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
variables {
|
||||||
|
managed_id = "B853C121"
|
||||||
|
}
|
||||||
|
|
||||||
|
run "setup" {
|
||||||
|
module {
|
||||||
|
source = "./setup"
|
||||||
|
}
|
||||||
|
|
||||||
|
variables {
|
||||||
|
value = "Hello, world!"
|
||||||
|
id = "B853C121"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run "test" {
|
||||||
|
assert {
|
||||||
|
condition = test_resource.created.value == "Hello, world!"
|
||||||
|
error_message = "bad value"
|
||||||
|
}
|
||||||
|
}
|
13
internal/configs/testdata/valid-modules/with-tests-module/setup/main.tf
vendored
Normal file
13
internal/configs/testdata/valid-modules/with-tests-module/setup/main.tf
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
variable "value" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "id" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "test_resource" "managed" {
|
||||||
|
provider = setup
|
||||||
|
id = var.id
|
||||||
|
value = var.value
|
||||||
|
}
|
@ -89,7 +89,7 @@ func (i *ModuleInstaller) InstallModules(ctx context.Context, rootDir string, up
|
|||||||
log.Printf("[TRACE] ModuleInstaller: installing child modules for %s into %s", rootDir, i.modsDir)
|
log.Printf("[TRACE] ModuleInstaller: installing child modules for %s into %s", rootDir, i.modsDir)
|
||||||
var diags tfdiags.Diagnostics
|
var diags tfdiags.Diagnostics
|
||||||
|
|
||||||
rootMod, mDiags := i.loader.Parser().LoadConfigDir(rootDir)
|
rootMod, mDiags := i.loader.Parser().LoadConfigDirWithTests(rootDir, "tests")
|
||||||
if rootMod == nil {
|
if rootMod == nil {
|
||||||
// We drop the diagnostics here because we only want to report module
|
// We drop the diagnostics here because we only want to report module
|
||||||
// loading errors after checking the core version constraints, which we
|
// loading errors after checking the core version constraints, which we
|
||||||
|
@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
version "github.com/hashicorp/go-version"
|
version "github.com/hashicorp/go-version"
|
||||||
svchost "github.com/hashicorp/terraform-svchost"
|
svchost "github.com/hashicorp/terraform-svchost"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/internal/addrs"
|
"github.com/hashicorp/terraform/internal/addrs"
|
||||||
"github.com/hashicorp/terraform/internal/configs"
|
"github.com/hashicorp/terraform/internal/configs"
|
||||||
"github.com/hashicorp/terraform/internal/configs/configload"
|
"github.com/hashicorp/terraform/internal/configs/configload"
|
||||||
@ -703,6 +704,159 @@ func TestLoaderInstallModules_goGetter(t *testing.T) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestModuleInstaller_fromTests(t *testing.T) {
|
||||||
|
fixtureDir := filepath.Clean("testdata/local-module-from-test")
|
||||||
|
dir, done := tempChdir(t, fixtureDir)
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
hooks := &testInstallHooks{}
|
||||||
|
|
||||||
|
modulesDir := filepath.Join(dir, ".terraform/modules")
|
||||||
|
loader, close := configload.NewLoaderForTests(t)
|
||||||
|
defer close()
|
||||||
|
inst := NewModuleInstaller(modulesDir, loader, nil)
|
||||||
|
_, diags := inst.InstallModules(context.Background(), ".", false, hooks)
|
||||||
|
assertNoDiagnostics(t, diags)
|
||||||
|
|
||||||
|
wantCalls := []testInstallHookCall{
|
||||||
|
{
|
||||||
|
Name: "Install",
|
||||||
|
ModuleAddr: "test.tests.main.setup",
|
||||||
|
PackageAddr: "",
|
||||||
|
LocalPath: "setup",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if assertResultDeepEqual(t, hooks.Calls, wantCalls) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loader, err := configload.NewLoader(&configload.Config{
|
||||||
|
ModulesDir: modulesDir,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the configuration is loadable now.
|
||||||
|
// (This ensures that correct information is recorded in the manifest.)
|
||||||
|
config, loadDiags := loader.LoadConfigWithTests(".", "tests")
|
||||||
|
assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags))
|
||||||
|
|
||||||
|
if config.Module.Tests["tests/main.tftest"].Runs[0].ConfigUnderTest == nil {
|
||||||
|
t.Fatalf("should have loaded config into the relevant run block but did not")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadInstallModules_registryFromTest(t *testing.T) {
|
||||||
|
if os.Getenv("TF_ACC") == "" {
|
||||||
|
t.Skip("this test accesses registry.terraform.io and github.com; set TF_ACC=1 to run it")
|
||||||
|
}
|
||||||
|
|
||||||
|
fixtureDir := filepath.Clean("testdata/registry-module-from-test")
|
||||||
|
tmpDir, done := tempChdir(t, fixtureDir)
|
||||||
|
// the module installer runs filepath.EvalSymlinks() on the destination
|
||||||
|
// directory before copying files, and the resultant directory is what is
|
||||||
|
// returned by the install hooks. Without this, tests could fail on machines
|
||||||
|
// where the default temp dir was a symlink.
|
||||||
|
dir, err := filepath.EvalSymlinks(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
hooks := &testInstallHooks{}
|
||||||
|
modulesDir := filepath.Join(dir, ".terraform/modules")
|
||||||
|
|
||||||
|
loader, close := configload.NewLoaderForTests(t)
|
||||||
|
defer close()
|
||||||
|
inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil))
|
||||||
|
_, diags := inst.InstallModules(context.Background(), dir, false, hooks)
|
||||||
|
assertNoDiagnostics(t, diags)
|
||||||
|
|
||||||
|
v := version.Must(version.NewVersion("0.0.1"))
|
||||||
|
wantCalls := []testInstallHookCall{
|
||||||
|
// the configuration builder visits each level of calls in lexicographical
|
||||||
|
// order by name, so the following list is kept in the same order.
|
||||||
|
|
||||||
|
// setup access acctest directly.
|
||||||
|
{
|
||||||
|
Name: "Download",
|
||||||
|
ModuleAddr: "test.main.setup",
|
||||||
|
PackageAddr: "registry.terraform.io/hashicorp/module-installer-acctest/aws", // intentionally excludes the subdir because we're downloading the whole package here
|
||||||
|
Version: v,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Install",
|
||||||
|
ModuleAddr: "test.main.setup",
|
||||||
|
Version: v,
|
||||||
|
// NOTE: This local path and the other paths derived from it below
|
||||||
|
// can vary depending on how the registry is implemented. At the
|
||||||
|
// time of writing this test, registry.terraform.io returns
|
||||||
|
// git repository source addresses and so this path refers to the
|
||||||
|
// root of the git clone, but historically the registry referred
|
||||||
|
// to GitHub-provided tar archives which meant that there was an
|
||||||
|
// extra level of subdirectory here for the typical directory
|
||||||
|
// nesting in tar archives, which would've been reflected as
|
||||||
|
// an extra segment on this path. If this test fails due to an
|
||||||
|
// additional path segment in future, then a change to the upstream
|
||||||
|
// registry might be the root cause.
|
||||||
|
LocalPath: filepath.Join(dir, ".terraform/modules/test.main.setup"),
|
||||||
|
},
|
||||||
|
|
||||||
|
// main.tftest.setup.child_a
|
||||||
|
// (no download because it's a relative path inside acctest_child_a)
|
||||||
|
{
|
||||||
|
Name: "Install",
|
||||||
|
ModuleAddr: "test.main.setup.child_a",
|
||||||
|
LocalPath: filepath.Join(dir, ".terraform/modules/test.main.setup/modules/child_a"),
|
||||||
|
},
|
||||||
|
|
||||||
|
// main.tftest.setup.child_a.child_b
|
||||||
|
// (no download because it's a relative path inside main.tftest.setup.child_a)
|
||||||
|
{
|
||||||
|
Name: "Install",
|
||||||
|
ModuleAddr: "test.main.setup.child_a.child_b",
|
||||||
|
LocalPath: filepath.Join(dir, ".terraform/modules/test.main.setup/modules/child_b"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(wantCalls, hooks.Calls); diff != "" {
|
||||||
|
t.Fatalf("wrong installer calls\n%s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
//check that the registry reponses were cached
|
||||||
|
packageAddr := addrs.ModuleRegistryPackage{
|
||||||
|
Host: svchost.Hostname("registry.terraform.io"),
|
||||||
|
Namespace: "hashicorp",
|
||||||
|
Name: "module-installer-acctest",
|
||||||
|
TargetSystem: "aws",
|
||||||
|
}
|
||||||
|
if _, ok := inst.registryPackageVersions[packageAddr]; !ok {
|
||||||
|
t.Errorf("module versions cache was not populated\ngot: %s\nwant: key hashicorp/module-installer-acctest/aws", spew.Sdump(inst.registryPackageVersions))
|
||||||
|
}
|
||||||
|
if _, ok := inst.registryPackageSources[moduleVersion{module: packageAddr, version: "0.0.1"}]; !ok {
|
||||||
|
t.Errorf("module download url cache was not populated\ngot: %s", spew.Sdump(inst.registryPackageSources))
|
||||||
|
}
|
||||||
|
|
||||||
|
loader, err = configload.NewLoader(&configload.Config{
|
||||||
|
ModulesDir: modulesDir,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the configuration is loadable now.
|
||||||
|
// (This ensures that correct information is recorded in the manifest.)
|
||||||
|
config, loadDiags := loader.LoadConfigWithTests(".", "tests")
|
||||||
|
assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags))
|
||||||
|
|
||||||
|
if config.Module.Tests["main.tftest"].Runs[0].ConfigUnderTest == nil {
|
||||||
|
t.Fatalf("should have loaded config into the relevant run block but did not")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type testInstallHooks struct {
|
type testInstallHooks struct {
|
||||||
Calls []testInstallHookCall
|
Calls []testInstallHookCall
|
||||||
}
|
}
|
||||||
|
2
internal/initwd/testdata/local-module-from-test/main.tf
vendored
Normal file
2
internal/initwd/testdata/local-module-from-test/main.tf
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Keep this empty, we just want to make sure the test file loads the setup
|
||||||
|
# module.
|
4
internal/initwd/testdata/local-module-from-test/setup/main.tf
vendored
Normal file
4
internal/initwd/testdata/local-module-from-test/setup/main.tf
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
variable "v" {
|
||||||
|
description = "in setup module"
|
||||||
|
default = ""
|
||||||
|
}
|
5
internal/initwd/testdata/local-module-from-test/tests/main.tftest
vendored
Normal file
5
internal/initwd/testdata/local-module-from-test/tests/main.tftest
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
run "setup" {
|
||||||
|
module {
|
||||||
|
source = "./setup"
|
||||||
|
}
|
||||||
|
}
|
2
internal/initwd/testdata/registry-module-from-test/main.tf
vendored
Normal file
2
internal/initwd/testdata/registry-module-from-test/main.tf
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Deliberately empty, we just want to make sure the module is loaded from the
|
||||||
|
# tests.
|
8
internal/initwd/testdata/registry-module-from-test/main.tftest
vendored
Normal file
8
internal/initwd/testdata/registry-module-from-test/main.tftest
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
run "setup" {
|
||||||
|
# We have a dedicated repo for this test module.
|
||||||
|
# See ../registry-modules/root.tf for more info.
|
||||||
|
module {
|
||||||
|
source = "hashicorp/module-installer-acctest/aws"
|
||||||
|
version = "0.0.1"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user