| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package configs | |
| import ( | |
| "fmt" | |
| "os" | |
| "path" | |
| "path/filepath" | |
| "strings" | |
| "testing" | |
| "github.com/apparentlymart/go-versions/versions/constraints" | |
| "github.com/hashicorp/hcl/v2" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| ) | |
| // TestParseLoadConfigDirSuccess is a simple test that just verifies that | |
| // a number of test configuration directories (in testdata/valid-modules) | |
| // can be parsed without raising any diagnostics. | |
| // | |
| // It also re-tests the individual files in testdata/valid-files as if | |
| // they were single-file modules, to ensure that they can be bundled into | |
| // modules correctly. | |
| // | |
| // This test does not verify that reading these modules produces the correct | |
| // module element contents. More detailed assertions may be made on some subset | |
| // of these configuration files in other tests. | |
| func TestParserLoadConfigDirSuccess(t *testing.T) { | |
| dirs, err := os.ReadDir("testdata/valid-modules") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| for _, info := range dirs { | |
| name := info.Name() | |
| t.Run(name, func(t *testing.T) { | |
| parser := NewParser(nil) | |
| if strings.Contains(name, "state-store") { | |
| // The PSS project is currently gated as experimental | |
| // TODO(SarahFrench/radeksimko) - remove this from the test once | |
| // the feature is GA. | |
| parser.AllowLanguageExperiments(true) | |
| } | |
| path := filepath.Join("testdata/valid-modules", name) | |
| mod, diags := parser.LoadConfigDir(path) | |
| if len(diags) != 0 && len(mod.ActiveExperiments) != 0 { | |
| // As a special case to reduce churn while we're working | |
| // through experimental features, we'll ignore the warning | |
| // that an experimental feature is active if the module | |
| // intentionally opted in to that feature. | |
| // If you want to explicitly test for the feature warning | |
| // to be generated, consider using testdata/warning-files | |
| // instead. | |
| filterDiags := make(hcl.Diagnostics, 0, len(diags)) | |
| for _, diag := range diags { | |
| if diag.Severity != hcl.DiagWarning { | |
| continue | |
| } | |
| match := false | |
| for exp := range mod.ActiveExperiments { | |
| allowedSummary := fmt.Sprintf("Experimental feature %q is active", exp.Keyword()) | |
| if diag.Summary == allowedSummary { | |
| match = true | |
| break | |
| } | |
| } | |
| if !match { | |
| filterDiags = append(filterDiags, diag) | |
| } | |
| } | |
| diags = filterDiags | |
| } | |
| if len(diags) != 0 { | |
| t.Errorf("unexpected diagnostics") | |
| for _, diag := range diags { | |
| t.Logf("- %s", diag) | |
| } | |
| } | |
| if mod.SourceDir != path { | |
| t.Errorf("wrong SourceDir value %q; want %s", mod.SourceDir, path) | |
| } | |
| if len(mod.Tests) > 0 { | |
| // We only load tests when requested, and we didn't request this | |
| // time. | |
| t.Errorf("should not have loaded tests, but found %d", len(mod.Tests)) | |
| } | |
| }) | |
| } | |
| // The individual files in testdata/valid-files should also work | |
| // when loaded as modules. | |
| files, err := os.ReadDir("testdata/valid-files") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| for _, info := range files { | |
| name := info.Name() | |
| t.Run(fmt.Sprintf("%s as module", name), func(t *testing.T) { | |
| src, err := os.ReadFile(filepath.Join("testdata/valid-files", name)) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| parser := testParser(map[string]string{ | |
| "mod/" + name: string(src), | |
| }) | |
| _, diags := parser.LoadConfigDir("mod") | |
| if diags.HasErrors() { | |
| t.Errorf("unexpected error diagnostics") | |
| for _, diag := range diags { | |
| t.Logf("- %s", diag) | |
| } | |
| } | |
| }) | |
| } | |
| } | |
| func TestParserLoadConfigDirWithTests(t *testing.T) { | |
| directories := []string{ | |
| "testdata/valid-modules/with-tests", | |
| "testdata/valid-modules/with-tests-backend", | |
| "testdata/valid-modules/with-tests-same-backend-across-files", | |
| "testdata/valid-modules/with-tests-expect-failures", | |
| "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 { | |
| t.Run(directory, func(t *testing.T) { | |
| testDirectory := DefaultTestDirectory | |
| if directory == "testdata/valid-modules/with-tests-very-nested" { | |
| testDirectory = "very/nested" | |
| } | |
| parser := NewParser(nil) | |
| parser.AllowLanguageExperiments(true) | |
| mod, diags := parser.LoadConfigDir(directory, MatchTestFiles(testDirectory)) | |
| if len(diags) > 0 { // We don't want any warnings or errors. | |
| t.Errorf("unexpected diagnostics") | |
| for _, diag := range diags { | |
| t.Logf("- %s", diag) | |
| } | |
| } | |
| if len(mod.Tests) != 2 { | |
| t.Errorf("incorrect number of test files found: %d", len(mod.Tests)) | |
| } | |
| }) | |
| } | |
| } | |
| func TestParserLoadConfigDirWithQueries(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| directory string | |
| diagnostics []string | |
| listResources int | |
| managedResources int | |
| }{ | |
| { | |
| name: "simple", | |
| directory: "testdata/query-files/valid/simple", | |
| listResources: 3, | |
| }, | |
| { | |
| name: "mixed", | |
| directory: "testdata/query-files/valid/mixed", | |
| listResources: 3, | |
| managedResources: 1, | |
| }, | |
| { | |
| name: "loading query lists with no-experiments", | |
| directory: "testdata/query-files/valid/mixed", | |
| managedResources: 1, | |
| listResources: 3, | |
| }, | |
| { | |
| name: "no-provider", | |
| directory: "testdata/query-files/invalid/no-provider", | |
| diagnostics: []string{ | |
| "testdata/query-files/invalid/no-provider/main.tfquery.hcl:1,1-27: Missing \"provider\" attribute; You must specify a provider attribute when defining a list block.", | |
| }, | |
| }, | |
| { | |
| name: "with-depends-on", | |
| directory: "testdata/query-files/invalid/with-depends-on", | |
| diagnostics: []string{ | |
| "testdata/query-files/invalid/with-depends-on/main.tfquery.hcl:23,3-13: Unsupported argument; An argument named \"depends_on\" is not expected here.", | |
| }, | |
| listResources: 2, | |
| }, | |
| } | |
| for _, test := range tests { | |
| t.Run(test.name, func(t *testing.T) { | |
| parser := NewParser(nil) | |
| mod, diags := parser.LoadConfigDir(test.directory, MatchQueryFiles()) | |
| if len(test.diagnostics) > 0 { | |
| if !diags.HasErrors() { | |
| t.Errorf("expected errors, but found none") | |
| } | |
| if len(diags) != len(test.diagnostics) { | |
| t.Fatalf("expected %d errors, but found %d", len(test.diagnostics), len(diags)) | |
| } | |
| for i, diag := range diags { | |
| if diag.Error() != test.diagnostics[i] { | |
| t.Errorf("expected error to be %q, but found %q", test.diagnostics[i], diag.Error()) | |
| } | |
| } | |
| } else { | |
| if len(diags) > 0 { // We don't want any warnings or errors. | |
| t.Errorf("unexpected diagnostics") | |
| for _, diag := range diags { | |
| t.Logf("- %s", diag) | |
| } | |
| } | |
| } | |
| if len(mod.ListResources) != test.listResources { | |
| t.Errorf("incorrect number of list blocks found: %d", len(mod.ListResources)) | |
| } | |
| if len(mod.ManagedResources) != test.managedResources { | |
| t.Errorf("incorrect number of managed blocks found: %d", len(mod.ManagedResources)) | |
| } | |
| }) | |
| } | |
| } | |
| // Testing happy path use of 'from { backend }'. | |
| func TestParserLoadConfigDirWithStateMigrations_from_backend(t *testing.T) { | |
| testFixtures := "testdata/state-migration-files/valid/migration-from-backend" | |
| // Below are specified in the config above | |
| backendType := "s3" | |
| bucketName := "foobar" | |
| // Parse the directory, including .tfmigrate.hcl files | |
| parser := NewParser(nil) | |
| mod, diags := parser.LoadConfigDir(testFixtures, MatchStateMigrateFiles()) | |
| if diags.HasErrors() { | |
| t.Fatalf("unexpected errors: %s", diags) | |
| } | |
| if mod.StateMigrationInstructions == nil || mod.StateMigrationInstructions.Backend == nil { | |
| t.Fatalf("expected mod.StateMigrationInstructions.MigrateFromBackend to be initialized, got:\n mod.StateMigrationInstructions = %#v\n mod.StateMigrationInstructions.MigrateFromBackend = %#v", | |
| mod.StateMigrationInstructions, | |
| mod.StateMigrationInstructions.Backend, | |
| ) | |
| } | |
| // Assert that the module includes expected information from 'from { backend }' block | |
| b := mod.StateMigrationInstructions.Backend | |
| if b.Type != backendType { | |
| t.Fatalf("wrong backend type, got %q, want %q", b.Type, backendType) | |
| } | |
| attributes, diags := b.Config.JustAttributes() | |
| if diags.HasErrors() { | |
| t.Fatalf("unexpected error inspecting backend config: %s", diags) | |
| } | |
| gotBucketName, diags := attributes["bucket"].Expr.Value(nil) | |
| if diags.HasErrors() { | |
| t.Fatalf("unexpected error inspecting bucket attribute: %s", diags) | |
| } | |
| if gotBucketName.AsString() != bucketName { | |
| t.Fatalf("wrong bucket name, got %q, want %q", gotBucketName, bucketName) | |
| } | |
| } | |
| // Testing happy path use of 'from { state_store }'. This requires use of the state_store_provider | |
| // block as well, so this also checks the happy path for that block. | |
| func TestParserLoadConfigDirWithStateMigrations_from_state_store(t *testing.T) { | |
| testFixtures := "testdata/state-migration-files/valid/migration-from-state-store" | |
| // Below are specified in the config above | |
| stateStoreType := "test_store" | |
| // Parse the directory, including .tfmigrate.hcl files | |
| parser := NewParser(nil) | |
| mod, diags := parser.LoadConfigDir(testFixtures, MatchStateMigrateFiles()) | |
| if diags.HasErrors() { | |
| t.Fatalf("unexpected errors: %s", diags) | |
| } | |
| if mod.StateMigrationInstructions == nil || mod.StateMigrationInstructions.StateStore == nil || mod.StateMigrationInstructions.StateStoreProvider == nil { | |
| t.Fatalf("expected MigrateFromStateStore and StateStoreProvider to be initialized, got:\n mod.StateMigrationInstructions = %#v\n mod.StateMigrationInstructions.MigrateFromStateStore = %#v\n mod.StateMigrationInstructions.StateStoreProvider = %#v", | |
| mod.StateMigrationInstructions, | |
| mod.StateMigrationInstructions.StateStore, | |
| mod.StateMigrationInstructions.StateStoreProvider, | |
| ) | |
| } | |
| // Assert that the module includes expected information from 'from { state_store }' block | |
| ss := mod.StateMigrationInstructions.StateStore | |
| if ss.Type != stateStoreType { | |
| t.Fatalf("wrong state store type, got %q, want %q", ss.Type, stateStoreType) | |
| } | |
| if ss.Config == nil { | |
| t.Fatalf("expected config to be non-nil") | |
| } | |
| if !ss.ProviderAddr.Equals(mod.StateMigrationInstructions.StateStoreProvider.Type) { | |
| t.Fatalf("expected state store description's provider addr to have been populated with %q, but got %q", mod.StateMigrationInstructions.StateStoreProvider.Type.ForDisplay(), ss.ProviderAddr.ForDisplay()) | |
| } | |
| if ss.ProviderSupplyMode != "" { | |
| // This is expected to be populated by calling code | |
| // that is reading the config, not by the parser itself. | |
| t.Fatal("unexpected data in ProviderSupplyMode") | |
| } | |
| // Assert that the module includes expected information from state_store_provider block | |
| ssp := mod.StateMigrationInstructions.StateStoreProvider | |
| expectedAddr := addrs.NewDefaultProvider("test") | |
| if ssp.Source != "hashicorp/test" || !ssp.Type.Equals(expectedAddr) { | |
| t.Fatalf("unexpected state store provider info, got:\n Type: %q\n Source: %q", | |
| ssp.Type, ssp.Source, | |
| ) | |
| } | |
| // Making assertions about the version constraint looks a little hairy, | |
| // but essentially we want to know that: | |
| // - only one requirement is in the map made by parsing | |
| // - the underlying version constraint matches what we'd expect after parsing "1.0.0" or "=1.0.0", | |
| // which are equivalent ways to pin a single version. | |
| reqs := ssp.Requirement[expectedAddr] | |
| if len(reqs) != 1 { | |
| t.Fatalf("expected parsed state_store_provider block to represent a single version constraint, got %d:\n %#v", len(reqs), reqs) | |
| } | |
| req := reqs[0] | |
| if req.Operator != constraints.OpEqual { | |
| t.Fatalf("expected parsed state_store_provider block's version attribute to represent a an exact version constraint with operator = (%s), got:\n %#v", constraints.OpEqual, req.Operator) | |
| } | |
| if req.Boundary.Major.Num != 1 || req.Boundary.Minor.Num != 0 || req.Boundary.Patch.Num != 0 { | |
| t.Fatalf("expected parsed state_store_provider block's version attribute to represent version constraint of 1.0.0, got:\n %#v", req) | |
| } | |
| } | |
| func TestParserLoadConfigDirWithStateMigrations_error_cases(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| directory string | |
| diagnosticSummary string | |
| source string | |
| }{ | |
| // Duplicated blocks | |
| { | |
| name: "duplicated 'from' block", | |
| directory: "testdata/state-migration-files/invalid/duplicate-from-block-same-file", | |
| diagnosticSummary: "Duplicate \"from\" configuration block", | |
| // Assert the source because we reference the second parsed 'from' block | |
| source: "1-file.tfmigrate.hcl:17,1-5", | |
| }, | |
| { | |
| name: "duplicated 'from' block across multiple files", | |
| directory: "testdata/state-migration-files/invalid/duplicate-from-block-multiple-files", | |
| diagnosticSummary: "Duplicate \"from\" configuration block", | |
| // Assert the source because we reference the 'from' block in the second parsed file | |
| source: "2-file.tfmigrate.hcl:1,1-5", | |
| }, | |
| { | |
| name: "duplicate 'backend' block in 'from' block", | |
| directory: "testdata/state-migration-files/invalid/duplicate-nested-backend-block", | |
| diagnosticSummary: "Duplicate \"backend\" configuration block", | |
| }, | |
| { | |
| name: "duplicate 'state_store' block in 'from' block", | |
| directory: "testdata/state-migration-files/invalid/duplicate-nested-state-store-block", | |
| diagnosticSummary: "Duplicate \"state_store\" configuration block", | |
| }, | |
| // Mutually exclusive blocks | |
| { | |
| name: "backend and state_store are mutually exclusive in same 'from' block", | |
| directory: "testdata/state-migration-files/invalid/both-nested-state-store-and-backend-blocks", | |
| diagnosticSummary: `Invalid combination of "backend" and "state_store"`, | |
| // Assert the source because we reference the 'from' block as incorrect, instead of one of the nested blocks | |
| source: "main.tfmigrate.hcl:4,1-5", | |
| }, | |
| { | |
| name: "backend and state_store_provider are mutually exclusive", | |
| directory: "testdata/state-migration-files/invalid/backend-and-state-store-provider-same-file", | |
| diagnosticSummary: `Invalid combination of "backend" and "state_store_provider"`, | |
| }, | |
| { | |
| name: "backend and state_store_provider are mutually exclusive across multiple files", | |
| directory: "testdata/state-migration-files/invalid/backend-and-state-store-provider-multiple-files", | |
| diagnosticSummary: `Invalid combination of "backend" and "state_store_provider"`, | |
| }, | |
| // Missing blocks | |
| { | |
| name: "only state_store_provider block, missing state_store", | |
| directory: "testdata/state-migration-files/invalid/only-state-store-provider-block", | |
| diagnosticSummary: `Missing "state_store" block for state store migration`, | |
| }, | |
| { | |
| name: "only state_store block, missing state_store_provider", | |
| directory: "testdata/state-migration-files/invalid/only-state-store-block", | |
| diagnosticSummary: `Missing "state_store_provider" block for state store migration`, | |
| }, | |
| { | |
| name: "no blocks present in the files", | |
| directory: "testdata/state-migration-files/invalid/no-blocks", | |
| diagnosticSummary: `Empty state migration configuration`, | |
| }, | |
| // Invalid contents of state_store_provider block | |
| { | |
| name: "not pinned version in state_store_provider block", | |
| directory: "testdata/state-migration-files/invalid/state-store-provider-range-version-constraint", | |
| diagnosticSummary: `Invalid provider version in "state_store_provider" configuration block`, | |
| }, | |
| { | |
| name: "multi-part version constraint in state_store_provider block", | |
| directory: "testdata/state-migration-files/invalid/state-store-provider-multipart-version-constraint", | |
| diagnosticSummary: `Invalid provider version in "state_store_provider" configuration block`, | |
| }, | |
| { | |
| name: "incorrect data type for version attribute in state_store_provider block", | |
| directory: "testdata/state-migration-files/invalid/state-store-provider-version-incorrect-data-type", | |
| diagnosticSummary: `Invalid provider version in "state_store_provider" configuration block`, | |
| }, | |
| { | |
| name: "unexpected attribute in state_store_provider block", | |
| directory: "testdata/state-migration-files/invalid/unexpected-attribute-state-store-provider-block", | |
| diagnosticSummary: `Invalid state_store_provider object; state_store_provider objects can only contain "version" and "source" attributes.`, | |
| }, | |
| { | |
| name: "different providers in migrate_from_state_store and state_store_provider blocks", | |
| directory: "testdata/state-migration-files/invalid/different-providers-between-blocks", | |
| diagnosticSummary: `Inconsistent provider information for state migration`, | |
| }, | |
| { | |
| name: "multiple providers described in a state_store_provider block", | |
| directory: "testdata/state-migration-files/invalid/multiple-providers-in-state-store-provider-block", | |
| diagnosticSummary: `Unexpected number of providers described in "state_store_provider" configuration block.`, | |
| }, | |
| } | |
| for _, test := range tests { | |
| t.Run(test.name, func(t *testing.T) { | |
| parser := NewParser(nil) | |
| _, diags := parser.LoadConfigDir(test.directory, MatchStateMigrateFiles()) | |
| if !diags.HasErrors() { | |
| t.Fatalf("expected errors but got none: %s", diags) | |
| } | |
| if len(diags) != 1 { | |
| for _, diag := range diags { | |
| t.Log(diag) | |
| } | |
| t.Fatalf("expected only a single diagnostic to be returned, but got %d: \n%#v", len(diags), diags) | |
| } | |
| if !strings.Contains(diags.Error(), test.diagnosticSummary) { | |
| t.Fatalf("expected error to contain %q, but got %q", test.diagnosticSummary, diags.Error()) | |
| } | |
| if test.source != "" { | |
| // We're only asserting source content in cases where the fromBlockSource value is used. | |
| expectedSource := path.Join(test.directory, test.source) | |
| if diags[0].Subject.String() != expectedSource { | |
| t.Fatalf("expected error subject to be %q, but got %q", expectedSource, diags[0].Subject.String()) | |
| } | |
| } | |
| }) | |
| } | |
| } | |
| 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_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.", | |
| }, | |
| "duplicate_file_config": { | |
| "duplicate_file_config.tftest.hcl:3,1-5: Multiple \"test\" blocks; This test file already has a \"test\" block defined at duplicate_file_config.tftest.hcl:1,1-5.", | |
| "duplicate_file_config.tftest.hcl:5,1-5: Multiple \"test\" blocks; This test file already has a \"test\" block defined at duplicate_file_config.tftest.hcl:1,1-5.", | |
| }, | |
| "duplicate_backend_blocks_in_test": { | |
| "duplicate_backend_blocks_in_test.tftest.hcl:15,3-18: Duplicate backend blocks; The run \"test\" already uses an internal state file that's loaded by a backend in the run \"setup\". Please ensure that a backend block is only in the first apply run block for a given internal state file.", | |
| }, | |
| "duplicate_backend_blocks_in_run": { | |
| "duplicate_backend_blocks_in_run.tftest.hcl:6,3-18: Duplicate backend blocks; A backend block has already been defined inside the run \"setup\" at duplicate_backend_blocks_in_run.tftest.hcl:3,3-18.", | |
| }, | |
| "backend_block_in_plan_run": { | |
| "backend_block_in_plan_run.tftest.hcl:6,3-18: Invalid backend block; A backend block can only be used in the first apply run block for a given internal state file. It cannot be included in a block to run a plan command.", | |
| }, | |
| "backend_block_in_second_apply_run": { | |
| "backend_block_in_second_apply_run.tftest.hcl:10,3-18: Invalid backend block; The run \"test_2\" cannot load in state using a backend block, because internal state has already been created by an apply command in run \"test_1\". Backend blocks can only be present in the first apply command for a given internal state.", | |
| }, | |
| "non_state_storage_backend_in_test": { | |
| "non_state_storage_backend_in_test.tftest.hcl:4,3-19: Invalid backend block; The \"remote\" backend type cannot be used in the backend block in run \"test\" at non_state_storage_backend_in_test.tftest.hcl:4,3-19. Only state storage backends can be used in a test run.", | |
| }, | |
| "skip_cleanup_after_backend": { | |
| "skip_cleanup_after_backend.tftest.hcl:13,3-15: Duplicate \"skip_cleanup\" block; The run \"skip_cleanup\" has a skip_cleanup attribute set, but shares state with an earlier run \"backend\" that has a backend defined. The later run takes precedence, but the backend will still be used to manage this state.", | |
| }, | |
| } | |
| 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), | |
| }) | |
| parser.AllowLanguageExperiments(true) | |
| _, 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") | |
| if len(diags) != 1 { | |
| t.Errorf("expected exactly 1 diagnostic, but found %d", len(diags)) | |
| } else { | |
| if diags[0].Severity != hcl.DiagWarning { | |
| t.Errorf("expected warning severity but found %d", diags[0].Severity) | |
| } | |
| if diags[0].Summary != "Test directory does not exist" { | |
| t.Errorf("expected summary to be \"Test directory does not exist\" but was \"%s\"", diags[0].Summary) | |
| } | |
| if !strings.HasPrefix(diags[0].Detail, "Requested test directory testdata/valid-modules/with-tests/not_real does not exist.") { | |
| t.Errorf("expected detail to be \"Requested test directory testdata/valid-modules/with-tests/not_real does not exist.\" but was \"%s\"", diags[0].Detail) | |
| } | |
| } | |
| // Despite the warning, should still have loaded the tests in the | |
| // configuration directory. | |
| if len(mod.Tests) != 2 { | |
| t.Errorf("incorrect number of test files found: %d", len(mod.Tests)) | |
| } | |
| } | |
| // TestParseLoadConfigDirFailure is a simple test that just verifies that | |
| // a number of test configuration directories (in testdata/invalid-modules) | |
| // produce diagnostics when parsed. | |
| // | |
| // It also re-tests the individual files in testdata/invalid-files as if | |
| // they were single-file modules, to ensure that their errors are still | |
| // detected when loading as part of a module. | |
| // | |
| // This test does not verify that reading these modules produces any | |
| // diagnostics in particular. More detailed assertions may be made on some subset | |
| // of these configuration files in other tests. | |
| func TestParserLoadConfigDirFailure(t *testing.T) { | |
| dirs, err := os.ReadDir("testdata/invalid-modules") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| for _, info := range dirs { | |
| name := info.Name() | |
| t.Run(name, func(t *testing.T) { | |
| parser := NewParser(nil) | |
| path := filepath.Join("testdata/invalid-modules", name) | |
| _, diags := parser.LoadConfigDir(path, MatchTestFiles("tests")) | |
| if !diags.HasErrors() { | |
| t.Errorf("no errors; want at least one") | |
| for _, diag := range diags { | |
| t.Logf("- %s", diag) | |
| } | |
| } | |
| }) | |
| } | |
| // The individual files in testdata/valid-files should also work | |
| // when loaded as modules. | |
| files, err := os.ReadDir("testdata/invalid-files") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| for _, info := range files { | |
| name := info.Name() | |
| t.Run(fmt.Sprintf("%s as module", name), func(t *testing.T) { | |
| src, err := os.ReadFile(filepath.Join("testdata/invalid-files", name)) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| parser := testParser(map[string]string{ | |
| "mod/" + name: string(src), | |
| }) | |
| _, diags := parser.LoadConfigDir("mod") | |
| if !diags.HasErrors() { | |
| t.Errorf("no errors; want at least one") | |
| for _, diag := range diags { | |
| t.Logf("- %s", diag) | |
| } | |
| } | |
| }) | |
| } | |
| } | |
| func TestIsEmptyDir(t *testing.T) { | |
| val, err := IsEmptyDir(filepath.Join("testdata", "valid-files"), "") | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| if val { | |
| t.Fatal("should not be empty") | |
| } | |
| } | |
| func TestIsEmptyDir_noExist(t *testing.T) { | |
| val, err := IsEmptyDir(filepath.Join("testdata", "nopenopenope"), "") | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| if !val { | |
| t.Fatal("should be empty") | |
| } | |
| } | |
| func TestIsEmptyDir_noConfigsAndTests(t *testing.T) { | |
| val, err := IsEmptyDir(filepath.Join("testdata", "dir-empty"), "") | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| if !val { | |
| t.Fatal("should be empty") | |
| } | |
| } | |
| func TestIsEmptyDir_noConfigsButHasTests(t *testing.T) { | |
| // The top directory has no configs, but it contains test files | |
| val, err := IsEmptyDir(filepath.Join("testdata", "only-test-files"), "tests") | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| if val { | |
| t.Fatal("should not be empty") | |
| } | |
| } | |
| func TestIsEmptyDir_nestedTestsOnly(t *testing.T) { | |
| // The top directory has no configs and no test files, but the nested | |
| // directory has test files | |
| val, err := IsEmptyDir(filepath.Join("testdata", "only-nested-test-files"), "tests") | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| if val { | |
| t.Fatal("should not be empty") | |
| } | |
| } |