testing framework: allow providers to reference run blocks (#34118)

* testing framework: allow providers to reference run block

* missing copywrite header
pull/34190/head
Liam Cervante 3 years ago committed by GitHub
parent 10f4567fcc
commit f90d71f723
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -62,6 +62,13 @@ type TestSuiteRunner struct {
// Verbose tells the runner to print out plan files during each test run.
Verbose bool
// configProviders is a cache of config keys mapped to all the providers
// referenced by the given config.
//
// The config keys are globally unique across an entire test suite, so we
// store this at the suite runner level to get maximum efficiency.
configProviders map[string]map[string]bool
}
func (runner *TestSuiteRunner) Stop() {
@ -75,6 +82,9 @@ func (runner *TestSuiteRunner) Cancel() {
func (runner *TestSuiteRunner) Test() (moduletest.Status, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
// First thing, initialise the config providers map.
runner.configProviders = make(map[string]map[string]bool)
suite, suiteDiags := runner.collectTests()
diags = diags.Append(suiteDiags)
if suiteDiags.HasErrors() {
@ -349,7 +359,13 @@ func (runner *TestFileRunner) run(run *moduletest.Run, file *moduletest.File, st
return state, false
}
resetConfig, configDiags := configtest.TransformConfigForTest(config, run, file, runner.globalVariables)
key := MainStateIdentifier
if run.Config.ConfigUnderTest != nil {
key = run.Config.Module.Source.String()
}
runner.gatherProviders(key, config)
resetConfig, configDiags := configtest.TransformConfigForTest(config, run, file, runner.globalVariables, runner.PriorStates, runner.Suite.configProviders[key])
defer resetConfig()
run.Diagnostics = run.Diagnostics.Append(configDiags)
@ -372,7 +388,7 @@ func (runner *TestFileRunner) run(run *moduletest.Run, file *moduletest.File, st
return state, false
}
variables, variableDiags := runner.GetVariables(config, run, file, references)
variables, variableDiags := runner.GetVariables(config, run, references)
run.Diagnostics = run.Diagnostics.Append(variableDiags)
if variableDiags.HasErrors() {
run.Status = moduletest.Error
@ -563,7 +579,7 @@ func (runner *TestFileRunner) destroy(config *configs.Config, state *states.Stat
var diags tfdiags.Diagnostics
variables, variableDiags := runner.GetVariables(config, run, file, nil)
variables, variableDiags := runner.GetVariables(config, run, nil)
diags = diags.Append(variableDiags)
if diags.HasErrors() {
@ -845,7 +861,7 @@ func (runner *TestFileRunner) cleanup(file *moduletest.File) {
diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Inconsistent state", fmt.Sprintf("Found inconsistent state while cleaning up %s. This is a bug in Terraform - please report it", file.Name)))
}
} else {
reset, configDiags := configtest.TransformConfigForTest(runner.Suite.Config, main.Run, file, runner.globalVariables)
reset, configDiags := configtest.TransformConfigForTest(runner.Suite.Config, main.Run, file, runner.globalVariables, runner.PriorStates, runner.Suite.configProviders[MainStateIdentifier])
diags = diags.Append(configDiags)
if !configDiags.HasErrors() {
@ -920,7 +936,7 @@ func (runner *TestFileRunner) cleanup(file *moduletest.File) {
var diags tfdiags.Diagnostics
reset, configDiags := configtest.TransformConfigForTest(state.Run.Config.ConfigUnderTest, state.Run, file, runner.globalVariables)
reset, configDiags := configtest.TransformConfigForTest(state.Run.Config.ConfigUnderTest, state.Run, file, runner.globalVariables, runner.PriorStates, runner.Suite.configProviders[state.Run.Config.Module.Source.String()])
diags = diags.Append(configDiags)
updated := state.State
@ -951,7 +967,7 @@ func (runner *TestFileRunner) cleanup(file *moduletest.File) {
// more variables than are required by the config. FilterVariablesToConfig
// should be called before trying to use these variables within a Terraform
// plan, apply, or destroy operation.
func (runner *TestFileRunner) GetVariables(config *configs.Config, run *moduletest.Run, file *moduletest.File, references []*addrs.Reference) (terraform.InputValues, tfdiags.Diagnostics) {
func (runner *TestFileRunner) GetVariables(config *configs.Config, run *moduletest.Run, references []*addrs.Reference) (terraform.InputValues, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
// relevantVariables contains the variables that are of interest to this
@ -1039,7 +1055,7 @@ func (runner *TestFileRunner) GetVariables(config *configs.Config, run *modulete
variables[name] = value.Value
}
ctx, ctxDiags := hcltest.EvalContext(exprs, variables, runner.PriorStates)
ctx, ctxDiags := hcltest.EvalContext(hcltest.TargetRunBlock, exprs, variables, runner.PriorStates)
diags = diags.Append(ctxDiags)
var failedContext bool
@ -1188,3 +1204,51 @@ func (runner *TestFileRunner) initVariables(file *moduletest.File) {
runner.globalVariables[name] = unparsedTestVariableValue{expr}
}
}
func (runner *TestFileRunner) gatherProviders(key string, config *configs.Config) {
if _, exists := runner.Suite.configProviders[key]; exists {
// Then we've processed this key before, so skip it.
return
}
providers := make(map[string]bool)
// First, let's look at the required providers first.
for _, provider := range config.Module.ProviderRequirements.RequiredProviders {
providers[provider.Name] = true
for _, alias := range provider.Aliases {
providers[alias.StringCompact()] = true
}
}
// Second, we look at the defined provider configs.
for _, provider := range config.Module.ProviderConfigs {
providers[provider.Addr().StringCompact()] = true
}
// Third, we look at the resources and data sources.
for _, resource := range config.Module.ManagedResources {
if resource.ProviderConfigRef != nil {
providers[resource.ProviderConfigRef.String()] = true
continue
}
providers[resource.Provider.Type] = true
}
for _, datasource := range config.Module.DataResources {
if datasource.ProviderConfigRef != nil {
providers[datasource.ProviderConfigRef.String()] = true
continue
}
providers[datasource.Provider.Type] = 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[provider.InParent.String()] = true
}
}
runner.Suite.configProviders[key] = providers
}

@ -1146,8 +1146,12 @@ is declared in run block "test".
run "finalise"... skip
main.tftest.hcl... tearing down
main.tftest.hcl... fail
providers.tftest.hcl... in progress
run "test"... fail
providers.tftest.hcl... tearing down
providers.tftest.hcl... fail
Failure! 1 passed, 1 failed, 1 skipped.
Failure! 1 passed, 2 failed, 1 skipped.
`
actualOut := output.Stdout()
if diff := cmp.Diff(actualOut, expectedOut); len(diff) > 0 {
@ -1169,9 +1173,9 @@ Error: Reference to unavailable run block
on main.tftest.hcl line 16, in run "test":
16: input_two = run.finalise.response
The run block "finalise" is not available to the current run block. You can
only reference run blocks that are in the same test file and will execute
before the current run block.
The run block "finalise" has not executed yet. You can only reference run
blocks that are in the same test file and will execute before the current run
block.
Error: Reference to unknown run block
@ -1181,6 +1185,15 @@ Error: Reference to unknown run block
The run block "madeup" does not exist within this test file. You can only
reference run blocks that are in the same test file and will execute before
the current run block.
Error: Reference to unavailable variable
on providers.tftest.hcl line 3, in provider "test":
3: resource_prefix = var.default
The input variable "default" is not available to the current run block. You
can only reference variables defined at the file or global levels when
populating the variables block within a run block.
`
actualErr := output.Stderr()
if diff := cmp.Diff(actualErr, expectedErr); len(diff) > 0 {
@ -1690,3 +1703,156 @@ func TestTest_LongRunningTestJSON(t *testing.T) {
t.Errorf("unexpected output\n\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", strings.Join(expected, "\n"), strings.Join(messages, "\n"), diff)
}
}
func TestTest_RunBlocksInProviders(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", "provider_runs")), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
providerSource, close := newMockProviderSource(t, map[string][]string{
"test": {"1.0.0"},
})
defer close()
streams, done := terminal.StreamsForTesting(t)
view := views.NewView(streams)
ui := new(cli.MockUi)
meta := Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
Ui: ui,
View: view,
Streams: streams,
ProviderSource: providerSource,
}
init := &InitCommand{
Meta: meta,
}
if code := init.Run(nil); code != 0 {
t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
}
test := &TestCommand{
Meta: meta,
}
code := test.Run([]string{"-no-color"})
output := done(t)
if code != 0 {
t.Errorf("expected status code 0 but got %d", code)
}
expected := `main.tftest.hcl... in progress
run "setup"... pass
run "main"... pass
main.tftest.hcl... tearing down
main.tftest.hcl... pass
Success! 2 passed, 0 failed.
`
actual := output.All()
if diff := cmp.Diff(actual, expected); len(diff) > 0 {
t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff)
}
if provider.ResourceCount() > 0 {
t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
}
}
func TestTest_RunBlocksInProviders_BadReferences(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", "provider_runs_invalid")), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
providerSource, close := newMockProviderSource(t, map[string][]string{
"test": {"1.0.0"},
})
defer close()
streams, done := terminal.StreamsForTesting(t)
view := views.NewView(streams)
ui := new(cli.MockUi)
meta := Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
Ui: ui,
View: view,
Streams: streams,
ProviderSource: providerSource,
}
init := &InitCommand{
Meta: meta,
}
if code := init.Run(nil); code != 0 {
t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
}
test := &TestCommand{
Meta: meta,
}
code := test.Run([]string{"-no-color"})
output := done(t)
if code != 1 {
t.Errorf("expected status code 1 but got %d", code)
}
expectedOut := `missing_run_block.tftest.hcl... in progress
run "main"... fail
missing_run_block.tftest.hcl... tearing down
missing_run_block.tftest.hcl... fail
unavailable_run_block.tftest.hcl... in progress
run "main"... fail
unavailable_run_block.tftest.hcl... tearing down
unavailable_run_block.tftest.hcl... fail
unused_provider.tftest.hcl... in progress
run "main"... pass
unused_provider.tftest.hcl... tearing down
unused_provider.tftest.hcl... pass
Failure! 1 passed, 2 failed.
`
actualOut := output.Stdout()
if diff := cmp.Diff(actualOut, expectedOut); len(diff) > 0 {
t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedOut, actualOut, diff)
}
expectedErr := `
Error: Reference to unknown run block
on missing_run_block.tftest.hcl line 2, in provider "test":
2: resource_prefix = run.missing.resource_directory
The run block "missing" does not exist within this test file. You can only
reference run blocks that are in the same test file and will execute before
the provider is required.
Error: Reference to unavailable run block
on unavailable_run_block.tftest.hcl line 2, in provider "test":
2: resource_prefix = run.main.resource_directory
The run block "main" has not executed yet. You can only reference run blocks
that are in the same test file and will execute before the provider is
required.
`
actualErr := output.Stderr()
if diff := cmp.Diff(actualErr, expectedErr); len(diff) > 0 {
t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedErr, actualErr, diff)
}
if provider.ResourceCount() > 0 {
t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
}
}

@ -0,0 +1,6 @@
provider "test" {
resource_prefix = var.default
}
run "test" {}

@ -0,0 +1 @@
resource "test_resource" "foo" {}

@ -0,0 +1,24 @@
variables {
resource_directory = "resources"
}
provider "test" {
alias = "setup"
resource_prefix = var.resource_directory
}
run "setup" {
module {
source = "./setup"
}
providers = {
test = test.setup
}
}
provider "test" {
resource_prefix = run.setup.resource_directory
}
run "main" {}

@ -0,0 +1,11 @@
variable "resource_directory" {
type = string
}
resource "test_resource" "foo" {
value = var.resource_directory
}
output "resource_directory" {
value = test_resource.foo.value
}

@ -0,0 +1,11 @@
variable "resource_directory" {
type = string
}
resource "test_resource" "foo" {
value = var.resource_directory
}
output "resource_directory" {
value = test_resource.foo.value
}

@ -0,0 +1,9 @@
provider "test" {
resource_prefix = run.missing.resource_directory
}
run "main" {
variables {
resource_directory = "resource"
}
}

@ -0,0 +1,9 @@
provider "test" {
resource_prefix = run.main.resource_directory
}
run "main" {
variables {
resource_directory = "resource"
}
}

@ -0,0 +1,17 @@
provider "test" {
resource_prefix = run.main.resource_directory
}
provider "test" {
alias = "usable"
}
run "main" {
providers = {
test = test.usable
}
variables {
resource_directory = "resource"
}
}

@ -12,6 +12,7 @@ import (
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/moduletest"
hcltest "github.com/hashicorp/terraform/internal/moduletest/hcl"
"github.com/hashicorp/terraform/internal/terraform"
)
// TransformConfigForTest transforms the provided configuration ready for the
@ -25,7 +26,7 @@ import (
// We also return a reset function that should be called to return the
// configuration to it's original state before the next run block or test file
// needs to use it.
func TransformConfigForTest(config *configs.Config, run *moduletest.Run, file *moduletest.File, availableVariables map[string]backend.UnparsedVariableValue) (func(), hcl.Diagnostics) {
func TransformConfigForTest(config *configs.Config, run *moduletest.Run, file *moduletest.File, availableVariables map[string]backend.UnparsedVariableValue, availableRunBlocks map[string]*terraform.TestContext, requiredProviders map[string]bool) (func(), hcl.Diagnostics) {
var diags hcl.Diagnostics
// Currently, we only need to override the provider settings.
@ -63,7 +64,7 @@ func TransformConfigForTest(config *configs.Config, run *moduletest.Run, file *m
next[key] = value
}
if run != nil && len(run.Config.Providers) > 0 {
if len(run.Config.Providers) > 0 {
// Then we'll only copy over and overwrite the specific providers asked
// for by this run block.
@ -93,6 +94,7 @@ func TransformConfigForTest(config *configs.Config, run *moduletest.Run, file *m
Original: testProvider.Config,
ConfigVariables: config.Module.Variables,
AvailableVariables: availableVariables,
AvailableRunBlocks: availableRunBlocks,
},
DeclRange: testProvider.DeclRange,
}
@ -102,6 +104,13 @@ func TransformConfigForTest(config *configs.Config, run *moduletest.Run, file *m
// Otherwise, let's copy over and overwrite all providers specified by
// the test file itself.
for key, provider := range file.Config.Providers {
if !requiredProviders[key] {
// Then we don't actually need this provider for this
// configuration, so skip it.
continue
}
next[key] = &configs.Provider{
Name: provider.Name,
NameRange: provider.NameRange,
@ -112,6 +121,7 @@ func TransformConfigForTest(config *configs.Config, run *moduletest.Run, file *m
Original: provider.Config,
ConfigVariables: config.Module.Variables,
AvailableVariables: availableVariables,
AvailableRunBlocks: availableRunBlocks,
},
DeclRange: provider.DeclRange,
}

@ -208,6 +208,16 @@ func TestTransformForTest(t *testing.T) {
"foo.secondary": "source = \"testfile\"",
},
},
"ignores unexpected providers in test file": {
configProviders: make(map[string]string),
fileProviders: map[string]string{
"foo": "source = \"testfile\"",
"bar": "source = \"testfile\"",
},
expectedProviders: map[string]string{
"foo": "source = \"testfile\"",
},
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
@ -229,7 +239,12 @@ func TestTransformForTest(t *testing.T) {
},
}
reset, diags := TransformConfigForTest(config, run, file, nil)
availableProviders := make(map[string]bool, len(tc.expectedProviders))
for provider := range tc.expectedProviders {
availableProviders[provider] = true
}
reset, diags := TransformConfigForTest(config, run, file, nil, nil, availableProviders)
var actualErrs []string
for _, err := range diags.Errs() {

@ -17,6 +17,13 @@ import (
"github.com/hashicorp/terraform/internal/tfdiags"
)
type EvalContextTarget string
const (
TargetRunBlock EvalContextTarget = "run"
TargetProvider EvalContextTarget = "provider"
)
// EvalContext builds hcl.EvalContext objects for use directly within the
// testing framework.
//
@ -39,7 +46,7 @@ import (
// expressions to be evaluated will pass evaluation. Anything present in the
// expressions argument will be validated to make sure the only reference the
// availableVariables and availableRunBlocks.
func EvalContext(expressions []hcl.Expression, availableVariables map[string]cty.Value, availableRunBlocks map[string]*terraform.TestContext) (*hcl.EvalContext, tfdiags.Diagnostics) {
func EvalContext(target EvalContextTarget, expressions []hcl.Expression, availableVariables map[string]cty.Value, availableRunBlocks map[string]*terraform.TestContext) (*hcl.EvalContext, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
runs := make(map[string]cty.Value)
@ -96,31 +103,22 @@ func EvalContext(expressions []hcl.Expression, availableVariables map[string]cty
for _, ref := range refs {
if addr, ok := ref.Subject.(addrs.Run); ok {
ctx, exists := availableRunBlocks[addr.Name]
if availableRunBlocks == nil {
// Then run blocks are never available from this context.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid reference",
Detail: "You cannot reference run blocks from within provider configurations. You can only reference run blocks from other run blocks that execute after them.",
Subject: ref.SourceRange.ToHCL().Ptr(),
})
continue
var diagPrefix string
switch target {
case TargetRunBlock:
diagPrefix = "You can only reference run blocks that are in the same test file and will execute before the current run block."
case TargetProvider:
diagPrefix = "You can only reference run blocks that are in the same test file and will execute before the provider is required."
}
// For the error messages here, we know the reference is coming
// from a run block as that is the only place that reference
// other run blocks.
ctx, exists := availableRunBlocks[addr.Name]
if !exists {
// Then this is a made up run block.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to unknown run block",
Detail: fmt.Sprintf("The run block %q does not exist within this test file. You can only reference run blocks that are in the same test file and will execute before the current run block.", addr.Name),
Detail: fmt.Sprintf("The run block %q does not exist within this test file. %s", addr.Name, diagPrefix),
Subject: ref.SourceRange.ToHCL().Ptr(),
})
@ -132,7 +130,7 @@ func EvalContext(expressions []hcl.Expression, availableVariables map[string]cty
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to unavailable run block",
Detail: fmt.Sprintf("The run block %q is not available to the current run block. You can only reference run blocks that are in the same test file and will execute before the current run block.", addr.Name),
Detail: fmt.Sprintf("The run block %q has not executed yet. %s", addr.Name, diagPrefix),
Subject: ref.SourceRange.ToHCL().Ptr(),
})

@ -11,6 +11,7 @@ import (
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/lang"
"github.com/hashicorp/terraform/internal/terraform"
)
var _ hcl.Body = (*ProviderConfig)(nil)
@ -28,6 +29,7 @@ type ProviderConfig struct {
ConfigVariables map[string]*configs.Variable
AvailableVariables map[string]backend.UnparsedVariableValue
AvailableRunBlocks map[string]*terraform.TestContext
}
func (p *ProviderConfig) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
@ -51,7 +53,7 @@ func (p *ProviderConfig) PartialContent(schema *hcl.BodySchema) (*hcl.BodyConten
Attributes: attrs,
Blocks: p.transformBlocks(content.Blocks),
MissingItemRange: content.MissingItemRange,
}, &ProviderConfig{rest, p.ConfigVariables, p.AvailableVariables}, diags
}, &ProviderConfig{rest, p.ConfigVariables, p.AvailableVariables, p.AvailableRunBlocks}, diags
}
func (p *ProviderConfig) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
@ -67,18 +69,29 @@ func (p *ProviderConfig) MissingItemRange() hcl.Range {
func (p *ProviderConfig) transformAttributes(originals hcl.Attributes) (hcl.Attributes, hcl.Diagnostics) {
var diags hcl.Diagnostics
relevantVariables := make(map[string]cty.Value)
availableVariables := make(map[string]cty.Value)
var exprs []hcl.Expression
for _, original := range originals {
exprs = append(exprs, original.Expr)
// We revalidate this later, so we actually only care about the
// references we can extract.
// We also need to parse the variables we're going to use, so we extract
// the references from this expression now and see if they reference any
// input variables. If we find an input variable, we'll copy it into
// our availableVariables local.
refs, _ := lang.ReferencesInExpr(addrs.ParseRefFromTestingScope, original.Expr)
for _, ref := range refs {
if addr, ok := ref.Subject.(addrs.InputVariable); ok {
if _, exists := availableVariables[addr.Name]; exists {
// Then we've processed this variable before. This just
// means it's referenced twice in this provider config -
// which is fine, we just don't need to do it again.
continue
}
if variable, exists := p.AvailableVariables[addr.Name]; exists {
// Then we have a value for this variable! So we think we'll
// be able to process it - let's parse it now.
parsingMode := configs.VariableParseHCL
if config, exists := p.ConfigVariables[addr.Name]; exists {
@ -88,17 +101,17 @@ func (p *ProviderConfig) transformAttributes(originals hcl.Attributes) (hcl.Attr
value, valueDiags := variable.ParseVariableValue(parsingMode)
diags = append(diags, valueDiags.ToHCL()...)
if value != nil {
relevantVariables[addr.Name] = value.Value
availableVariables[addr.Name] = value.Value
}
}
}
}
}
ctx, ctxDiags := EvalContext(exprs, relevantVariables, nil)
ctx, ctxDiags := EvalContext(TargetProvider, exprs, availableVariables, p.AvailableRunBlocks)
diags = append(diags, ctxDiags.ToHCL()...)
if ctxDiags.HasErrors() {
return originals, diags
return nil, diags
}
attrs := make(hcl.Attributes, len(originals))
@ -106,7 +119,7 @@ func (p *ProviderConfig) transformAttributes(originals hcl.Attributes) (hcl.Attr
value, valueDiags := attr.Expr.Value(ctx)
diags = append(diags, valueDiags...)
if valueDiags.HasErrors() {
attrs[name] = attr
continue
} else {
attrs[name] = &hcl.Attribute{
Name: name,
@ -125,7 +138,7 @@ func (p *ProviderConfig) transformBlocks(originals hcl.Blocks) hcl.Blocks {
blocks[name] = &hcl.Block{
Type: block.Type,
Labels: block.Labels,
Body: &ProviderConfig{block.Body, p.ConfigVariables, p.AvailableVariables},
Body: &ProviderConfig{block.Body, p.ConfigVariables, p.AvailableVariables, p.AvailableRunBlocks},
DefRange: block.DefRange,
TypeRange: block.TypeRange,
LabelRanges: block.LabelRanges,

@ -0,0 +1,272 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package hcl
import (
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
)
func TestProviderConfig(t *testing.T) {
tcs := map[string]struct {
content string
schema *hcl.BodySchema
variables map[string]cty.Value
runBlockOutputs map[string]map[string]cty.Value
validate func(t *testing.T, content *hcl.BodyContent)
expectedErrors []string
}{
"simple_no_vars": {
content: "attribute = \"string\"",
schema: &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "attribute",
},
},
},
validate: func(t *testing.T, content *hcl.BodyContent) {
equals(t, content, "attribute", cty.StringVal("string"))
},
},
"simple_var_ref": {
content: "attribute = var.input",
schema: &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "attribute",
},
},
},
variables: map[string]cty.Value{
"input": cty.StringVal("string"),
},
validate: func(t *testing.T, content *hcl.BodyContent) {
equals(t, content, "attribute", cty.StringVal("string"))
},
},
"missing_var_ref": {
content: "attribute = var.missing",
schema: &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "attribute",
},
},
},
variables: map[string]cty.Value{
"input": cty.StringVal("string"),
},
expectedErrors: []string{
"The input variable \"missing\" is not available to the current run block. You can only reference variables defined at the file or global levels when populating the variables block within a run block.",
},
validate: func(t *testing.T, content *hcl.BodyContent) {
if len(content.Attributes) > 0 {
t.Errorf("should have excluded the invalid attribute but found %d", len(content.Attributes))
}
},
},
"simple_run_block": {
content: "attribute = run.setup.value",
schema: &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "attribute",
},
},
},
runBlockOutputs: map[string]map[string]cty.Value{
"setup": {
"value": cty.StringVal("string"),
},
},
validate: func(t *testing.T, content *hcl.BodyContent) {
equals(t, content, "attribute", cty.StringVal("string"))
},
},
"missing_run_block": {
content: "attribute = run.missing.value",
schema: &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "attribute",
},
},
},
runBlockOutputs: map[string]map[string]cty.Value{
"setup": {
"value": cty.StringVal("string"),
},
},
expectedErrors: []string{
"The run block \"missing\" does not exist within this test file. You can only reference run blocks that are in the same test file and will execute before the provider is required.",
},
validate: func(t *testing.T, content *hcl.BodyContent) {
if len(content.Attributes) > 0 {
t.Errorf("should have excluded the invalid attribute but found %d", len(content.Attributes))
}
},
},
"late_run_block": {
content: "attribute = run.setup.value",
schema: &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "attribute",
},
},
},
runBlockOutputs: map[string]map[string]cty.Value{
"setup": nil,
},
expectedErrors: []string{
"The run block \"setup\" has not executed yet. You can only reference run blocks that are in the same test file and will execute before the provider is required.",
},
validate: func(t *testing.T, content *hcl.BodyContent) {
if len(content.Attributes) > 0 {
t.Errorf("should have excluded the invalid attribute but found %d", len(content.Attributes))
}
},
},
"invalid_ref": {
content: "attribute = data.type.name.value",
schema: &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "attribute",
},
},
},
runBlockOutputs: map[string]map[string]cty.Value{
"setup": nil,
},
expectedErrors: []string{
"You can only reference earlier run blocks, file level, and global variables while defining variables from inside a run block.",
},
validate: func(t *testing.T, content *hcl.BodyContent) {
if len(content.Attributes) > 0 {
t.Errorf("should have excluded the invalid attribute but found %d", len(content.Attributes))
}
},
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
file, diags := hclsyntax.ParseConfig([]byte(tc.content), "main.tf", hcl.Pos{Line: 1, Column: 1})
if diags.HasErrors() {
t.Fatalf("failed to parse hcl: %s", diags.Error())
}
config := ProviderConfig{
Original: file.Body,
ConfigVariables: func() map[string]*configs.Variable {
variables := make(map[string]*configs.Variable)
for variable := range tc.variables {
variables[variable] = &configs.Variable{
Name: variable,
}
}
return variables
}(),
AvailableVariables: func() map[string]backend.UnparsedVariableValue {
variables := make(map[string]backend.UnparsedVariableValue)
for name, value := range tc.variables {
variables[name] = &variable{value}
}
return variables
}(),
AvailableRunBlocks: func() map[string]*terraform.TestContext {
statuses := make(map[string]*terraform.TestContext)
for name, values := range tc.runBlockOutputs {
if values == nil {
statuses[name] = nil
continue
}
state := states.BuildState(func(state *states.SyncState) {
for name, value := range values {
state.SetOutputValue(addrs.AbsOutputValue{
Module: addrs.RootModuleInstance,
OutputValue: addrs.OutputValue{
Name: name,
},
}, value, false)
}
})
config := &configs.Config{
Module: &configs.Module{
Outputs: func() map[string]*configs.Output {
outputs := make(map[string]*configs.Output)
for name := range values {
outputs[name] = &configs.Output{
Name: name,
}
}
return outputs
}(),
},
}
statuses[name] = &terraform.TestContext{
Config: config,
State: state,
}
}
return statuses
}(),
}
content, diags := config.Content(tc.schema)
var actualErrs []string
for _, diag := range diags {
actualErrs = append(actualErrs, diag.Detail)
}
if diff := cmp.Diff(actualErrs, tc.expectedErrors); len(diff) > 0 {
t.Errorf("unmatched errors\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", strings.Join(tc.expectedErrors, "\n"), strings.Join(actualErrs, "\n"), diff)
}
tc.validate(t, content)
})
}
}
func equals(t *testing.T, content *hcl.BodyContent, attribute string, expected cty.Value) {
value, diags := content.Attributes[attribute].Expr.Value(nil)
if diags.HasErrors() {
t.Errorf("failed to get value from attribute %s: %s", attribute, diags.Error())
}
if !value.RawEquals(expected) {
t.Errorf("expected:\n%s\nbut got:\n%s", expected.GoString(), value.GoString())
}
}
var _ backend.UnparsedVariableValue = (*variable)(nil)
type variable struct {
value cty.Value
}
func (v *variable) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) {
return &terraform.InputValue{
Value: v.value,
SourceType: terraform.ValueFromUnknown,
}, nil
}
Loading…
Cancel
Save