diff --git a/internal/addrs/module.go b/internal/addrs/module.go index 0ade0bf9f7..aa309c0f4f 100644 --- a/internal/addrs/module.go +++ b/internal/addrs/module.go @@ -66,6 +66,14 @@ func (m Module) Equal(other Module) bool { return true } +type moduleKey string + +func (m Module) UniqueKey() UniqueKey { + return moduleKey(m.String()) +} + +func (mk moduleKey) uniqueKeySigil() {} + func (m Module) targetableSigil() { // Module is targetable } diff --git a/internal/addrs/targetable.go b/internal/addrs/targetable.go index 2bcf84b80c..d179d8129e 100644 --- a/internal/addrs/targetable.go +++ b/internal/addrs/targetable.go @@ -6,6 +6,8 @@ package addrs // Targetable is an interface implemented by all address types that can be // used as "targets" for selecting sub-graphs of a graph. type Targetable interface { + UniqueKeyer + targetableSigil() // TargetContains returns true if the receiver is considered to contain diff --git a/internal/configs/mock_provider.go b/internal/configs/mock_provider.go new file mode 100644 index 0000000000..766852d8e1 --- /dev/null +++ b/internal/configs/mock_provider.go @@ -0,0 +1,383 @@ +package configs + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/gohcl" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func decodeMockProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) { + var diags hcl.Diagnostics + + content, config, moreDiags := block.Body.PartialContent(mockProviderSchema) + diags = append(diags, moreDiags...) + + name := block.Labels[0] + nameDiags := checkProviderNameNormalized(name, block.DefRange) + diags = append(diags, nameDiags...) + if nameDiags.HasErrors() { + // If the name is invalid then we mustn't produce a result because + // downstream could try to use it as a provider type and then crash. + return nil, diags + } + + provider := &Provider{ + Name: name, + NameRange: block.LabelRanges[0], + DeclRange: block.DefRange, + + Config: config, + + // Mark this provider as being mocked. + Mock: true, + } + + if attr, exists := content.Attributes["alias"]; exists { + valDiags := gohcl.DecodeExpression(attr.Expr, nil, &provider.Alias) + diags = append(diags, valDiags...) + provider.AliasRange = attr.Expr.Range().Ptr() + + if !hclsyntax.ValidIdentifier(provider.Alias) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid provider configuration alias", + Detail: fmt.Sprintf("An alias must be a valid name. %s", badIdentifierDetail), + }) + } + } + + var dataDiags hcl.Diagnostics + provider.MockData, dataDiags = decodeMockDataBody(config) + diags = append(diags, dataDiags...) + + // TODO(liamcervante): Add support for the "source" attribute before the + // v1.7 release. + + return provider, diags +} + +// MockData packages up all the available mock and override data available to +// a mocked provider. +type MockData struct { + MockResources map[string]*MockResource + MockDataSources map[string]*MockResource + Overrides addrs.Map[addrs.Targetable, *Override] +} + +// MockResource maps a resource or data source type and name to a set of values +// for that resource. +type MockResource struct { + Mode addrs.ResourceMode + Type string + + Defaults cty.Value + + Range hcl.Range + TypeRange hcl.Range + DefaultsRange hcl.Range +} + +// Override targets a specific module, resource or data source with a set of +// replacement values that should be used in place of whatever the underlying +// provider would normally do. +type Override struct { + Target *addrs.Target + Values cty.Value + + Range hcl.Range + TypeRange hcl.Range + TargetRange hcl.Range + ValuesRange hcl.Range +} + +func decodeMockDataBody(body hcl.Body) (*MockData, hcl.Diagnostics) { + var diags hcl.Diagnostics + + content, contentDiags := body.Content(mockDataSchema) + diags = append(diags, contentDiags...) + + data := &MockData{ + MockResources: make(map[string]*MockResource), + MockDataSources: make(map[string]*MockResource), + Overrides: addrs.MakeMap[addrs.Targetable, *Override](), + } + + for _, block := range content.Blocks { + switch block.Type { + case "mock_resource", "mock_data": + resource, resourceDiags := decodeMockResourceBlock(block) + diags = append(diags, resourceDiags...) + + if resource != nil { + switch resource.Mode { + case addrs.ManagedResourceMode: + if previous, ok := data.MockResources[resource.Type]; ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate mock_resource block", + Detail: fmt.Sprintf("A mock_resource block for %s has already been defined at %s.", resource.Type, previous.Range), + Subject: resource.TypeRange.Ptr(), + }) + continue + } + data.MockResources[resource.Type] = resource + case addrs.DataResourceMode: + if previous, ok := data.MockDataSources[resource.Type]; ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate mock_data block", + Detail: fmt.Sprintf("A mock_data block for %s has already been defined at %s.", resource.Type, previous.Range), + Subject: resource.TypeRange.Ptr(), + }) + continue + } + data.MockDataSources[resource.Type] = resource + } + } + case "override_resource": + override, overrideDiags := decodeOverrideResourceBlock(block) + diags = append(diags, overrideDiags...) + + if override != nil && override.Target != nil { + subject := override.Target.Subject + if previous, ok := data.Overrides.GetOk(subject); ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate override_resource block", + Detail: fmt.Sprintf("An override_resource block targeting %s has already been defined at %s.", subject, previous.Range), + Subject: override.Range.Ptr(), + }) + continue + } + data.Overrides.Put(subject, override) + } + case "override_data": + override, overrideDiags := decodeOverrideDataBlock(block) + diags = append(diags, overrideDiags...) + + if override != nil && override.Target != nil { + subject := override.Target.Subject + if previous, ok := data.Overrides.GetOk(subject); ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate override_data block", + Detail: fmt.Sprintf("An override_data block targeting %s has already been defined at %s.", subject, previous.Range), + Subject: override.Range.Ptr(), + }) + continue + } + data.Overrides.Put(subject, override) + } + } + } + + return data, diags +} + +func decodeMockResourceBlock(block *hcl.Block) (*MockResource, hcl.Diagnostics) { + var diags hcl.Diagnostics + + content, contentDiags := block.Body.Content(mockResourceSchema) + diags = append(diags, contentDiags...) + + resource := &MockResource{ + Type: block.Labels[0], + Range: block.DefRange, + TypeRange: block.LabelRanges[0], + } + + switch block.Type { + case "mock_resource": + resource.Mode = addrs.ManagedResourceMode + case "mock_data": + resource.Mode = addrs.DataResourceMode + } + + if defaults, exists := content.Attributes["defaults"]; exists { + var defaultDiags hcl.Diagnostics + resource.DefaultsRange = defaults.Range + resource.Defaults, defaultDiags = defaults.Expr.Value(nil) + diags = append(diags, defaultDiags...) + } else { + // It's fine if we don't have any defaults, just means we'll generate + // values for everything ourselves. + resource.Defaults = cty.NilVal + } + + return resource, diags +} + +func decodeOverrideModuleBlock(block *hcl.Block) (*Override, hcl.Diagnostics) { + override, diags := decodeOverrideBlock(block, "outputs", "override_module") + + if override.Target != nil { + switch override.Target.Subject.AddrType() { + case addrs.ModuleAddrType, addrs.ModuleInstanceAddrType: + // Do nothing, we're good here. + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid override target", + Detail: fmt.Sprintf("You can only target modules from override_module blocks, not %s.", override.Target.Subject), + Subject: override.TargetRange.Ptr(), + }) + return nil, diags + } + } + + return override, diags +} + +func decodeOverrideResourceBlock(block *hcl.Block) (*Override, hcl.Diagnostics) { + override, diags := decodeOverrideBlock(block, "values", "override_resource") + + if override.Target != nil { + var mode addrs.ResourceMode + + switch override.Target.Subject.AddrType() { + case addrs.AbsResourceInstanceAddrType: + subject := override.Target.Subject.(addrs.AbsResourceInstance) + mode = subject.Resource.Resource.Mode + case addrs.AbsResourceAddrType: + subject := override.Target.Subject.(addrs.AbsResource) + mode = subject.Resource.Mode + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid override target", + Detail: fmt.Sprintf("You can only target resources from override_resource blocks, not %s.", override.Target.Subject), + Subject: override.TargetRange.Ptr(), + }) + return nil, diags + } + + if mode != addrs.ManagedResourceMode { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid override target", + Detail: fmt.Sprintf("You can only target resources from override_resource blocks, not %s.", override.Target.Subject), + Subject: override.TargetRange.Ptr(), + }) + return nil, diags + } + } + + return override, diags +} + +func decodeOverrideDataBlock(block *hcl.Block) (*Override, hcl.Diagnostics) { + override, diags := decodeOverrideBlock(block, "values", "override_data") + + if override.Target != nil { + var mode addrs.ResourceMode + + switch override.Target.Subject.AddrType() { + case addrs.AbsResourceInstanceAddrType: + subject := override.Target.Subject.(addrs.AbsResourceInstance) + mode = subject.Resource.Resource.Mode + case addrs.AbsResourceAddrType: + subject := override.Target.Subject.(addrs.AbsResource) + mode = subject.Resource.Mode + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid override target", + Detail: fmt.Sprintf("You can only target data sources from override_data blocks, not %s.", override.Target.Subject), + Subject: override.TargetRange.Ptr(), + }) + return nil, diags + } + + if mode != addrs.DataResourceMode { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid override target", + Detail: fmt.Sprintf("You can only target data sources from override_data blocks, not %s.", override.Target.Subject), + Subject: override.TargetRange.Ptr(), + }) + return nil, diags + } + } + + return override, diags +} + +func decodeOverrideBlock(block *hcl.Block, attributeName string, blockName string) (*Override, hcl.Diagnostics) { + var diags hcl.Diagnostics + + content, contentDiags := block.Body.Content(&hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: "target"}, + {Name: attributeName}, + }, + }) + diags = append(diags, contentDiags...) + + override := &Override{ + Range: block.DefRange, + TypeRange: block.TypeRange, + } + + if target, exists := content.Attributes["target"]; exists { + override.TargetRange = target.Range + traversal, traversalDiags := hcl.AbsTraversalForExpr(target.Expr) + diags = append(diags, traversalDiags...) + if traversal != nil { + var targetDiags tfdiags.Diagnostics + override.Target, targetDiags = addrs.ParseTarget(traversal) + diags = append(diags, targetDiags.ToHCL()...) + } + } else { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing target attribute", + Detail: fmt.Sprintf("%s blocks must specify a target address.", blockName), + Subject: override.Range.Ptr(), + }) + } + + if attribute, exists := content.Attributes[attributeName]; exists { + var valueDiags hcl.Diagnostics + override.ValuesRange = attribute.Range + override.Values, valueDiags = attribute.Expr.Value(nil) + diags = append(diags, valueDiags...) + } else { + // It's fine if we don't have any values, just means we'll generate + // values for everything ourselves. + override.Values = cty.NilVal + } + + return override, diags +} + +var mockProviderSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "alias", + }, + { + Name: "source", + }, + }, +} + +var mockDataSchema = &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + {Type: "mock_resource", LabelNames: []string{"type"}}, + {Type: "mock_data", LabelNames: []string{"type"}}, + {Type: "override_resource"}, + {Type: "override_data"}, + }, +} + +var mockResourceSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: "defaults"}, + }, +} diff --git a/internal/configs/parser_config_dir_test.go b/internal/configs/parser_config_dir_test.go index 6b06c0640e..7f6132fc71 100644 --- a/internal/configs/parser_config_dir_test.go +++ b/internal/configs/parser_config_dir_test.go @@ -6,6 +6,7 @@ package configs import ( "fmt" "io/ioutil" + "os" "path/filepath" "testing" @@ -120,6 +121,7 @@ func TestParserLoadConfigDirWithTests(t *testing.T) { "testdata/valid-modules/with-tests-nested", "testdata/valid-modules/with-tests-very-nested", "testdata/valid-modules/with-tests-json", + "testdata/valid-modules/with-mocks", } for _, directory := range directories { @@ -146,6 +148,88 @@ func TestParserLoadConfigDirWithTests(t *testing.T) { } } +func TestParserLoadTestFiles_Invalid(t *testing.T) { + + tcs := map[string][]string{ + "duplicate_data_overrides": { + "duplicate_data_overrides.tftest.hcl:7,3-16: Duplicate override_data block; An override_data block targeting data.aws_instance.test has already been defined at duplicate_data_overrides.tftest.hcl:2,3-16.", + "duplicate_data_overrides.tftest.hcl:18,1-14: Duplicate override_data block; An override_data block targeting data.aws_instance.test has already been defined at duplicate_data_overrides.tftest.hcl:13,1-14.", + "duplicate_data_overrides.tftest.hcl:29,3-16: Duplicate override_data block; An override_data block targeting data.aws_instance.test has already been defined at duplicate_data_overrides.tftest.hcl:24,3-16.", + }, + "duplicate_mixed_providers": { + "duplicate_mixed_providers.tftest.hcl:3,1-20: Duplicate provider block; A provider for aws is already defined at duplicate_mixed_providers.tftest.hcl:1,10-15.", + "duplicate_mixed_providers.tftest.hcl:9,1-20: Duplicate provider block; A provider for aws.test is already defined at duplicate_mixed_providers.tftest.hcl:5,10-15.", + }, + "duplicate_mock_data_sources": { + "duplicate_mock_data_sources.tftest.hcl:7,13-27: Duplicate mock_data block; A mock_data block for aws_instance has already been defined at duplicate_mock_data_sources.tftest.hcl:3,3-27.", + }, + "duplicate_mock_providers": { + "duplicate_mock_providers.tftest.hcl:3,1-20: Duplicate provider block; A provider for aws is already defined at duplicate_mock_providers.tftest.hcl:1,15-20.", + "duplicate_mock_providers.tftest.hcl:9,1-20: Duplicate provider block; A provider for aws.test is already defined at duplicate_mock_providers.tftest.hcl:5,15-20.", + }, + "duplicate_mock_resources": { + "duplicate_mock_resources.tftest.hcl:7,17-31: Duplicate mock_resource block; A mock_resource block for aws_instance has already been defined at duplicate_mock_resources.tftest.hcl:3,3-31.", + }, + "duplicate_module_overrides": { + "duplicate_module_overrides.tftest.hcl:7,1-16: Duplicate override_module block; An override_module block targeting module.child has already been defined at duplicate_module_overrides.tftest.hcl:2,1-16.", + "duplicate_module_overrides.tftest.hcl:18,3-18: Duplicate override_module block; An override_module block targeting module.child has already been defined at duplicate_module_overrides.tftest.hcl:13,3-18.", + }, + "duplicate_providers": { + "duplicate_providers.tftest.hcl:3,1-15: Duplicate provider block; A provider for aws is already defined at duplicate_providers.tftest.hcl:1,10-15.", + "duplicate_providers.tftest.hcl:9,1-15: Duplicate provider block; A provider for aws.test is already defined at duplicate_providers.tftest.hcl:5,10-15.", + }, + "duplicate_resource_overrides": { + "duplicate_resource_overrides.tftest.hcl:7,3-20: Duplicate override_resource block; An override_resource block targeting aws_instance.test has already been defined at duplicate_resource_overrides.tftest.hcl:2,3-20.", + "duplicate_resource_overrides.tftest.hcl:18,1-18: Duplicate override_resource block; An override_resource block targeting aws_instance.test has already been defined at duplicate_resource_overrides.tftest.hcl:13,1-18.", + "duplicate_resource_overrides.tftest.hcl:29,3-20: Duplicate override_resource block; An override_resource block targeting aws_instance.test has already been defined at duplicate_resource_overrides.tftest.hcl:24,3-20.", + }, + "invalid_data_override": { + "invalid_data_override.tftest.hcl:6,1-14: Missing target attribute; override_data blocks must specify a target address.", + }, + "invalid_data_override_target": { + "invalid_data_override_target.tftest.hcl:8,3-24: Invalid override target; You can only target data sources from override_data blocks, not module.child.", + "invalid_data_override_target.tftest.hcl:3,3-31: Invalid override target; You can only target data sources from override_data blocks, not aws_instance.target.", + }, + "invalid_mock_data_sources": { + "invalid_mock_data_sources.tftest.hcl:7,13-16: Variables not allowed; Variables may not be used here.", + }, + "invalid_mock_resources": { + "invalid_mock_resources.tftest.hcl:7,13-16: Variables not allowed; Variables may not be used here.", + }, + "invalid_module_override": { + "invalid_module_override.tftest.hcl:5,1-16: Missing target attribute; override_module blocks must specify a target address.", + "invalid_module_override.tftest.hcl:11,3-9: Unsupported argument; An argument named \"values\" is not expected here.", + }, + "invalid_module_override_target": { + "invalid_module_override_target.tftest.hcl:3,3-31: Invalid override target; You can only target modules from override_module blocks, not aws_instance.target.", + "invalid_module_override_target.tftest.hcl:8,3-36: Invalid override target; You can only target modules from override_module blocks, not data.aws_instance.target.", + }, + "invalid_resource_override": { + "invalid_resource_override.tftest.hcl:6,1-18: Missing target attribute; override_resource blocks must specify a target address.", + }, + "invalid_resource_override_target": { + "invalid_resource_override_target.tftest.hcl:3,3-36: Invalid override target; You can only target resources from override_resource blocks, not data.aws_instance.target.", + "invalid_resource_override_target.tftest.hcl:8,3-24: Invalid override target; You can only target resources from override_resource blocks, not module.child.", + }, + } + + for name, expected := range tcs { + t.Run(name, func(t *testing.T) { + src, err := os.ReadFile(fmt.Sprintf("testdata/invalid-test-files/%s.tftest.hcl", name)) + if err != nil { + t.Fatal(err) + } + + parser := testParser(map[string]string{ + fmt.Sprintf("%s.tftest.hcl", name): string(src), + }) + + _, actual := parser.LoadTestFile(fmt.Sprintf("%s.tftest.hcl", name)) + assertExactDiagnostics(t, actual, expected) + }) + } +} + func TestParserLoadConfigDirWithTests_ReturnsWarnings(t *testing.T) { parser := NewParser(nil) mod, diags := parser.LoadConfigDirWithTests("testdata/valid-modules/with-tests", "not_real") diff --git a/internal/configs/provider.go b/internal/configs/provider.go index 309beb169d..b9b0cf3d99 100644 --- a/internal/configs/provider.go +++ b/internal/configs/provider.go @@ -35,6 +35,12 @@ type Provider struct { // export this so providers don't need to be re-resolved. // This same field is also added to the ProviderConfigRef struct. providerType addrs.Provider + + // Mock and MockData declare this provider as a "mock_provider", which means + // it should use the data in MockData instead of actually initialising the + // provider. + Mock bool + MockData *MockData } func decodeProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) { @@ -60,6 +66,10 @@ func decodeProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) { NameRange: block.LabelRanges[0], Config: config, DeclRange: block.DefRange, + + // We'll just explicitly mark real providers as not being mocks even + // though this is the default. + Mock: false, } if attr, exists := content.Attributes["alias"]; exists { diff --git a/internal/configs/test_file.go b/internal/configs/test_file.go index 6154f1edd7..89f704d827 100644 --- a/internal/configs/test_file.go +++ b/internal/configs/test_file.go @@ -51,10 +51,16 @@ type TestFile struct { // Providers defines a set of providers that are available to run blocks // within this test file. // + // Some or all of these providers may be mocked providers. + // // If empty, tests should use the default providers for the module under // test. Providers map[string]*Provider + // Overrides contains any specific overrides that should be applied for this + // test outside any mock providers. + Overrides addrs.Map[addrs.Targetable, *Override] + // Runs defines the sequential list of run blocks that should be executed in // order. Runs []*TestRun @@ -88,6 +94,10 @@ type TestRun struct { // take precedence over the global definition. Variables map[string]hcl.Expression + // Overrides contains any specific overrides that should be applied for this + // run block only outside any mock providers or overrides from the file. + Overrides addrs.Map[addrs.Targetable, *Override] + // Providers specifies the set of providers that should be loaded into the // module for this run block. // @@ -181,7 +191,7 @@ type TestRunOptions struct { // Refresh is analogous to the -refresh=false Terraform plan option. Refresh bool - // Replace is analogous to the -refresh=ADDRESS Terraform plan option. + // Replace is analogous to the -replace=ADDRESS Terraform plan option. Replace []hcl.Traversal // Target is analogous to the -target=ADDRESS Terraform plan option. @@ -198,6 +208,7 @@ func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) { tf := TestFile{ Providers: make(map[string]*Provider), + Overrides: addrs.MakeMap[addrs.Targetable, *Override](), } runBlockNames := make(map[string]hcl.Range) @@ -245,7 +256,84 @@ func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) { provider, providerDiags := decodeProviderBlock(block) diags = append(diags, providerDiags...) if provider != nil { - tf.Providers[provider.moduleUniqueKey()] = provider + key := provider.moduleUniqueKey() + if previous, exists := tf.Providers[key]; exists { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate provider block", + Detail: fmt.Sprintf("A provider for %s is already defined at %s.", key, previous.NameRange), + Subject: provider.DeclRange.Ptr(), + }) + continue + } + tf.Providers[key] = provider + } + case "mock_provider": + provider, providerDiags := decodeMockProviderBlock(block) + diags = append(diags, providerDiags...) + if provider != nil { + key := provider.moduleUniqueKey() + if previous, exists := tf.Providers[key]; exists { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate provider block", + Detail: fmt.Sprintf("A provider for %s is already defined at %s.", key, previous.NameRange), + Subject: provider.DeclRange.Ptr(), + }) + continue + } + tf.Providers[key] = provider + } + case "override_resource": + override, overrideDiags := decodeOverrideResourceBlock(block) + diags = append(diags, overrideDiags...) + + if override != nil && override.Target != nil { + subject := override.Target.Subject + if previous, ok := tf.Overrides.GetOk(subject); ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate override_resource block", + Detail: fmt.Sprintf("An override_resource block targeting %s has already been defined at %s.", subject, previous.Range), + Subject: override.Range.Ptr(), + }) + continue + } + tf.Overrides.Put(subject, override) + } + case "override_data": + override, overrideDiags := decodeOverrideDataBlock(block) + diags = append(diags, overrideDiags...) + + if override != nil && override.Target != nil { + subject := override.Target.Subject + if previous, ok := tf.Overrides.GetOk(subject); ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate override_data block", + Detail: fmt.Sprintf("An override_data block targeting %s has already been defined at %s.", subject, previous.Range), + Subject: override.Range.Ptr(), + }) + continue + } + tf.Overrides.Put(subject, override) + } + case "override_module": + override, overrideDiags := decodeOverrideModuleBlock(block) + diags = append(diags, overrideDiags...) + + if override != nil && override.Target != nil { + subject := override.Target.Subject + if previous, ok := tf.Overrides.GetOk(subject); ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate override_module block", + Detail: fmt.Sprintf("An override_module block targeting %s has already been defined at %s.", subject, previous.Range), + Subject: override.Range.Ptr(), + }) + continue + } + tf.Overrides.Put(subject, override) } } } @@ -260,6 +348,8 @@ func decodeTestRunBlock(block *hcl.Block) (*TestRun, hcl.Diagnostics) { diags = append(diags, contentDiags...) r := TestRun{ + Overrides: addrs.MakeMap[addrs.Targetable, *Override](), + Name: block.Labels[0], NameDeclRange: block.LabelRanges[0], DeclRange: block.DefRange, @@ -322,6 +412,57 @@ func decodeTestRunBlock(block *hcl.Block) (*TestRun, hcl.Diagnostics) { if !moduleDiags.HasErrors() { r.Module = module } + case "override_resource": + override, overrideDiags := decodeOverrideResourceBlock(block) + diags = append(diags, overrideDiags...) + + if override != nil && override.Target != nil { + subject := override.Target.Subject + if previous, ok := r.Overrides.GetOk(subject); ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate override_resource block", + Detail: fmt.Sprintf("An override_resource block targeting %s has already been defined at %s.", subject, previous.Range), + Subject: override.Range.Ptr(), + }) + continue + } + r.Overrides.Put(subject, override) + } + case "override_data": + override, overrideDiags := decodeOverrideDataBlock(block) + diags = append(diags, overrideDiags...) + + if override != nil && override.Target != nil { + subject := override.Target.Subject + if previous, ok := r.Overrides.GetOk(subject); ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate override_data block", + Detail: fmt.Sprintf("An override_data block targeting %s has already been defined at %s.", subject, previous.Range), + Subject: override.Range.Ptr(), + }) + continue + } + r.Overrides.Put(subject, override) + } + case "override_module": + override, overrideDiags := decodeOverrideModuleBlock(block) + diags = append(diags, overrideDiags...) + + if override != nil && override.Target != nil { + subject := override.Target.Subject + if previous, ok := r.Overrides.GetOk(subject); ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate override_module block", + Detail: fmt.Sprintf("An override_module block targeting %s has already been defined at %s.", subject, previous.Range), + Subject: override.Range.Ptr(), + }) + continue + } + r.Overrides.Put(subject, override) + } } } @@ -548,9 +689,22 @@ var testFileSchema = &hcl.BodySchema{ Type: "provider", LabelNames: []string{"name"}, }, + { + Type: "mock_provider", + LabelNames: []string{"name"}, + }, { Type: "variables", }, + { + Type: "override_resource", + }, + { + Type: "override_data", + }, + { + Type: "override_module", + }, }, } @@ -573,6 +727,15 @@ var testRunBlockSchema = &hcl.BodySchema{ { Type: "module", }, + { + Type: "override_resource", + }, + { + Type: "override_data", + }, + { + Type: "override_module", + }, }, } diff --git a/internal/configs/testdata/invalid-test-files/duplicate_data_overrides.tftest.hcl b/internal/configs/testdata/invalid-test-files/duplicate_data_overrides.tftest.hcl new file mode 100644 index 0000000000..f77b788d96 --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/duplicate_data_overrides.tftest.hcl @@ -0,0 +1,33 @@ +mock_provider "aws" { + override_data { + target = data.aws_instance.test + values = {} + } + + override_data { + target = data.aws_instance.test + values = {} + } +} + +override_data { + target = data.aws_instance.test + values = {} +} + +override_data { + target = data.aws_instance.test + values = {} +} + +run "test" { + override_data { + target = data.aws_instance.test + values = {} + } + + override_data { + target = data.aws_instance.test + values = {} + } +} diff --git a/internal/configs/testdata/invalid-test-files/duplicate_mixed_providers.tftest.hcl b/internal/configs/testdata/invalid-test-files/duplicate_mixed_providers.tftest.hcl new file mode 100644 index 0000000000..b319143f20 --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/duplicate_mixed_providers.tftest.hcl @@ -0,0 +1,13 @@ +provider "aws" {} + +mock_provider "aws" {} + +provider "aws" { + alias = "test" +} + +mock_provider "aws" { + alias = "test" +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/duplicate_mock_data_sources.tftest.hcl b/internal/configs/testdata/invalid-test-files/duplicate_mock_data_sources.tftest.hcl new file mode 100644 index 0000000000..8ff1685093 --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/duplicate_mock_data_sources.tftest.hcl @@ -0,0 +1,13 @@ +mock_provider "aws" { + + mock_data "aws_instance" { + defaults = {} + } + + mock_data "aws_instance" { + defaults = {} + } + +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/duplicate_mock_providers.tftest.hcl b/internal/configs/testdata/invalid-test-files/duplicate_mock_providers.tftest.hcl new file mode 100644 index 0000000000..0ac2ae2bfe --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/duplicate_mock_providers.tftest.hcl @@ -0,0 +1,13 @@ +mock_provider "aws" {} + +mock_provider "aws" {} + +mock_provider "aws" { + alias = "test" +} + +mock_provider "aws" { + alias = "test" +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/duplicate_mock_resources.tftest.hcl b/internal/configs/testdata/invalid-test-files/duplicate_mock_resources.tftest.hcl new file mode 100644 index 0000000000..d95a5b1c50 --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/duplicate_mock_resources.tftest.hcl @@ -0,0 +1,13 @@ +mock_provider "aws" { + + mock_resource "aws_instance" { + defaults = {} + } + + mock_resource "aws_instance" { + defaults = {} + } + +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/duplicate_module_overrides.tftest.hcl b/internal/configs/testdata/invalid-test-files/duplicate_module_overrides.tftest.hcl new file mode 100644 index 0000000000..176d3e5f5f --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/duplicate_module_overrides.tftest.hcl @@ -0,0 +1,22 @@ + +override_module { + target = module.child + outputs = {} +} + +override_module { + target = module.child + outputs = {} +} + +run "test" { + override_module { + target = module.child + outputs = {} + } + + override_module { + target = module.child + outputs = {} + } +} diff --git a/internal/configs/testdata/invalid-test-files/duplicate_providers.tftest.hcl b/internal/configs/testdata/invalid-test-files/duplicate_providers.tftest.hcl new file mode 100644 index 0000000000..a0dac23dfa --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/duplicate_providers.tftest.hcl @@ -0,0 +1,13 @@ +provider "aws" {} + +provider "aws" {} + +provider "aws" { + alias = "test" +} + +provider "aws" { + alias = "test" +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/duplicate_resource_overrides.tftest.hcl b/internal/configs/testdata/invalid-test-files/duplicate_resource_overrides.tftest.hcl new file mode 100644 index 0000000000..1494449197 --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/duplicate_resource_overrides.tftest.hcl @@ -0,0 +1,33 @@ +mock_provider "aws" { + override_resource { + target = aws_instance.test + values = {} + } + + override_resource { + target = aws_instance.test + values = {} + } +} + +override_resource { + target = aws_instance.test + values = {} +} + +override_resource { + target = aws_instance.test + values = {} +} + +run "test" { + override_resource { + target = aws_instance.test + values = {} + } + + override_resource { + target = aws_instance.test + values = {} + } +} diff --git a/internal/configs/testdata/invalid-test-files/invalid_data_override.tftest.hcl b/internal/configs/testdata/invalid-test-files/invalid_data_override.tftest.hcl new file mode 100644 index 0000000000..3196ce49d9 --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/invalid_data_override.tftest.hcl @@ -0,0 +1,10 @@ + +override_data { + target = data.aws_instance.target +} + +override_data { + values = {} +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/invalid_data_override_target.tftest.hcl b/internal/configs/testdata/invalid-test-files/invalid_data_override_target.tftest.hcl new file mode 100644 index 0000000000..f3172477fd --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/invalid_data_override_target.tftest.hcl @@ -0,0 +1,12 @@ + +override_data { + target = aws_instance.target + values = {} +} + +override_data { + target = module.child + values = {} +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/invalid_mock_data_sources.tftest.hcl b/internal/configs/testdata/invalid-test-files/invalid_mock_data_sources.tftest.hcl new file mode 100644 index 0000000000..e45371de8d --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/invalid_mock_data_sources.tftest.hcl @@ -0,0 +1,13 @@ +mock_provider "aws" { + + mock_data "aws_instance" {} + + mock_data "aws_ami_instance" { + defaults = { + ami = var.ami + } + } + +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/invalid_mock_resources.tftest.hcl b/internal/configs/testdata/invalid-test-files/invalid_mock_resources.tftest.hcl new file mode 100644 index 0000000000..936e57dc96 --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/invalid_mock_resources.tftest.hcl @@ -0,0 +1,13 @@ +mock_provider "aws" { + + mock_resource "aws_instance" {} + + mock_resource "aws_ami_instance" { + defaults = { + ami = var.ami + } + } + +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/invalid_module_override.tftest.hcl b/internal/configs/testdata/invalid-test-files/invalid_module_override.tftest.hcl new file mode 100644 index 0000000000..7562d08796 --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/invalid_module_override.tftest.hcl @@ -0,0 +1,14 @@ +override_module { + target = module.child +} + +override_module { + outputs = {} +} + +override_module { + target = module.other + values = {} +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/invalid_module_override_target.tftest.hcl b/internal/configs/testdata/invalid-test-files/invalid_module_override_target.tftest.hcl new file mode 100644 index 0000000000..ed5081bc24 --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/invalid_module_override_target.tftest.hcl @@ -0,0 +1,12 @@ + +override_module { + target = aws_instance.target + outputs = {} +} + +override_module { + target = data.aws_instance.target + outputs = {} +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/invalid_resource_override.tftest.hcl b/internal/configs/testdata/invalid-test-files/invalid_resource_override.tftest.hcl new file mode 100644 index 0000000000..2dd0f6f6b9 --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/invalid_resource_override.tftest.hcl @@ -0,0 +1,10 @@ + +override_resource { + target = aws_instance.target +} + +override_resource { + values = {} +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/invalid_resource_override_target.tftest.hcl b/internal/configs/testdata/invalid-test-files/invalid_resource_override_target.tftest.hcl new file mode 100644 index 0000000000..ca3e5a317a --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/invalid_resource_override_target.tftest.hcl @@ -0,0 +1,12 @@ + +override_resource { + target = data.aws_instance.target + values = {} +} + +override_resource { + target = module.child + values = {} +} + +run "test" {} diff --git a/internal/configs/testdata/valid-modules/with-mocks/child/main.tf b/internal/configs/testdata/valid-modules/with-mocks/child/main.tf new file mode 100644 index 0000000000..1ceca4ccdc --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-mocks/child/main.tf @@ -0,0 +1,8 @@ + +output "string" { + value = "Hello, world!" +} + +output "number" { + value = 0 +} diff --git a/internal/configs/testdata/valid-modules/with-mocks/main.tf b/internal/configs/testdata/valid-modules/with-mocks/main.tf new file mode 100644 index 0000000000..572c350aab --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-mocks/main.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + } + } +} + +resource "aws_instance" "first" {} + +resource "aws_instance" "second" {} + +resource "aws_instance" "third" {} + +data "aws_secretsmanager_secret" "creds" {} + +module "child" { + source = "./child" +} diff --git a/internal/configs/testdata/valid-modules/with-mocks/test_case_one.tftest.hcl b/internal/configs/testdata/valid-modules/with-mocks/test_case_one.tftest.hcl new file mode 100644 index 0000000000..5df5f5950b --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-mocks/test_case_one.tftest.hcl @@ -0,0 +1,48 @@ + +mock_provider "aws" { + + mock_resource "aws_instance" { + defaults = { + arn = "aws:instance" + } + } + + mock_data "aws_secretsmanager_secret" {} + + override_resource { + target = aws_instance.second + values = {} + } + + override_data { + target = data.aws_secretsmanager_secret.creds + values = { + arn = "aws:secretsmanager" + } + } +} + +override_module { + target = module.child + outputs = { + string = "testfile" + number = -1 + } +} + +run "test" { + override_resource { + target = aws_instance.first + values = { + arn = "aws:instance:first" + } + } + + override_module { + target = module.child + outputs = { + string = "testrun" + number = -1 + } + } +} diff --git a/internal/configs/testdata/valid-modules/with-mocks/test_case_two.tftest.hcl b/internal/configs/testdata/valid-modules/with-mocks/test_case_two.tftest.hcl new file mode 100644 index 0000000000..9133e6aeb3 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-mocks/test_case_two.tftest.hcl @@ -0,0 +1,25 @@ + +provider "aws" {} + +override_data { + target = data.aws_secretsmanager_secret.creds + values = { + arn = "aws:secretsmanager" + } +} + +run "test" { + override_resource { + target = aws_instance.first + values = { + arn = "aws:instance:first" + } + } + + override_data { + target = data.aws_secretsmanager_secret.creds + values = { + arn = "aws:secretsmanager" + } + } +}