From 7f29df96a09157f92d226e5fc6d7d2258a5e5abe Mon Sep 17 00:00:00 2001 From: Samsondeen <40821565+dsa0x@users.noreply.github.com> Date: Thu, 6 Feb 2025 09:20:09 +0100 Subject: [PATCH] Allow terraform init when only test files are present in directory (#36429) --- .../ENHANCEMENTS-20250205-104144.yaml | 5 ++ internal/command/init.go | 4 +- internal/command/init_test.go | 57 +++++++++++++++++++ internal/command/providers.go | 2 +- internal/command/test_test.go | 8 +++ .../fixtures/main.tf | 8 +++ .../tests/main.tftest.hcl | 9 +++ .../top-dir-only-test-files/fixtures/main.tf | 8 +++ .../top-dir-only-test-files/main.tftest.hcl | 9 +++ internal/configs/parser_config_dir.go | 8 +-- internal/configs/parser_config_dir_test.go | 31 ++++++++-- .../only-nested-test-files/fixtures/main.tf | 8 +++ .../tests/main.tftest.hcl | 9 +++ .../testdata/only-test-files/fixtures/main.tf | 8 +++ .../testdata/only-test-files/main.tftest.hcl | 9 +++ 15 files changed, 172 insertions(+), 11 deletions(-) create mode 100644 .changes/unreleased/ENHANCEMENTS-20250205-104144.yaml create mode 100644 internal/command/testdata/test/top-dir-only-nested-test-files/fixtures/main.tf create mode 100644 internal/command/testdata/test/top-dir-only-nested-test-files/tests/main.tftest.hcl create mode 100644 internal/command/testdata/test/top-dir-only-test-files/fixtures/main.tf create mode 100644 internal/command/testdata/test/top-dir-only-test-files/main.tftest.hcl create mode 100644 internal/configs/testdata/only-nested-test-files/fixtures/main.tf create mode 100644 internal/configs/testdata/only-nested-test-files/tests/main.tftest.hcl create mode 100644 internal/configs/testdata/only-test-files/fixtures/main.tf create mode 100644 internal/configs/testdata/only-test-files/main.tftest.hcl diff --git a/.changes/unreleased/ENHANCEMENTS-20250205-104144.yaml b/.changes/unreleased/ENHANCEMENTS-20250205-104144.yaml new file mode 100644 index 000000000000..11e4dcce3b83 --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20250205-104144.yaml @@ -0,0 +1,5 @@ +kind: ENHANCEMENTS +body: Allow terraform init when tests are present but no configuration files are directly inside the current directory +time: 2025-02-05T10:41:44.663251+01:00 +custom: + Issue: "35040" diff --git a/internal/command/init.go b/internal/command/init.go index 8a223f95b72f..4ba9448edfdc 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -108,7 +108,7 @@ func (c *InitCommand) Run(args []string) int { if initArgs.FromModule != "" { src := initArgs.FromModule - empty, err := configs.IsEmptyDir(path) + empty, err := configs.IsEmptyDir(path, initArgs.TestsDirectory) if err != nil { diags = diags.Append(fmt.Errorf("Error validating destination directory: %s", err)) view.Diagnostics(diags) @@ -148,7 +148,7 @@ func (c *InitCommand) Run(args []string) int { // If our directory is empty, then we're done. We can't get or set up // the backend with an empty directory. - empty, err := configs.IsEmptyDir(path) + empty, err := configs.IsEmptyDir(path, initArgs.TestsDirectory) if err != nil { diags = diags.Append(fmt.Errorf("Error checking configuration: %s", err)) view.Diagnostics(diags) diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 5b877a53a46e..290e3f274620 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -9,6 +9,7 @@ import ( "log" "os" "path/filepath" + "regexp" "strings" "testing" @@ -20,6 +21,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/command/arguments" + "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/depsfile" @@ -32,6 +34,25 @@ import ( "github.com/hashicorp/terraform/internal/states/statemgr" ) +// cleanString removes newlines, and redundant spaces. +func cleanString(s string) string { + // Replace newlines with a single space. + s = strings.ReplaceAll(s, "\n", " ") + + // Remove other special characters like \r, \t + s = strings.ReplaceAll(s, "\r", "") + s = strings.ReplaceAll(s, "\t", "") + + // Replace multiple spaces with a single space. + spaceRegex := regexp.MustCompile(`\s+`) + s = spaceRegex.ReplaceAllString(s, " ") + + // Trim any leading or trailing spaces. + s = strings.TrimSpace(s) + + return s +} + func TestInit_empty(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() @@ -52,6 +73,42 @@ func TestInit_empty(t *testing.T) { if code := c.Run(args); code != 0 { t.Fatalf("bad: \n%s", done(t).All()) } + exp := views.MessageRegistry[views.OutputInitEmptyMessage].JSONValue + actual := cleanString(done(t).All()) + if !strings.Contains(actual, cleanString(exp)) { + t.Fatalf("expected output to be %q\n, got %q", exp, actual) + } +} + +func TestInit_only_test_files(t *testing.T) { + // Create a temporary working directory that has only test files and no tf configuration + td := t.TempDir() + os.MkdirAll(td, 0755) + defer testChdir(t, td)() + + if _, err := os.Create("main.tftest.hcl"); err != nil { + t.Fatalf("err: %s", err) + } + + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + args := []string{} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", done(t).All()) + } + exp := views.MessageRegistry[views.OutputInitSuccessCLIMessage].JSONValue + actual := cleanString(done(t).All()) + if !strings.Contains(actual, cleanString(exp)) { + t.Fatalf("expected output to be %q\n, got %q", exp, actual) + } } func TestInit_multipleArgs(t *testing.T) { diff --git a/internal/command/providers.go b/internal/command/providers.go index c4142fb6024c..d0695e23c8cf 100644 --- a/internal/command/providers.go +++ b/internal/command/providers.go @@ -49,7 +49,7 @@ func (c *ProvidersCommand) Run(args []string) int { var diags tfdiags.Diagnostics - empty, err := configs.IsEmptyDir(configPath) + empty, err := configs.IsEmptyDir(configPath, testsDirectory) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, diff --git a/internal/command/test_test.go b/internal/command/test_test.go index 8b7aee1b48a5..01ace5651f6f 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -41,6 +41,14 @@ func TestTest_Runs(t *testing.T) { expectedOut: []string{"1 passed, 0 failed."}, code: 0, }, + "top-dir-only-test-files": { + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, + "top-dir-only-nested-test-files": { + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, "simple_pass_nested": { expectedOut: []string{"1 passed, 0 failed."}, code: 0, diff --git a/internal/command/testdata/test/top-dir-only-nested-test-files/fixtures/main.tf b/internal/command/testdata/test/top-dir-only-nested-test-files/fixtures/main.tf new file mode 100644 index 000000000000..8e891884ead2 --- /dev/null +++ b/internal/command/testdata/test/top-dir-only-nested-test-files/fixtures/main.tf @@ -0,0 +1,8 @@ +variable "sample" { + type = bool + default = true +} + +output "name" { + value = var.sample +} \ No newline at end of file diff --git a/internal/command/testdata/test/top-dir-only-nested-test-files/tests/main.tftest.hcl b/internal/command/testdata/test/top-dir-only-nested-test-files/tests/main.tftest.hcl new file mode 100644 index 000000000000..387693d859eb --- /dev/null +++ b/internal/command/testdata/test/top-dir-only-nested-test-files/tests/main.tftest.hcl @@ -0,0 +1,9 @@ +run "foo" { + module { + source = "./fixtures" + } + assert { + condition = output.name == true + error_message = "foo" + } +} \ No newline at end of file diff --git a/internal/command/testdata/test/top-dir-only-test-files/fixtures/main.tf b/internal/command/testdata/test/top-dir-only-test-files/fixtures/main.tf new file mode 100644 index 000000000000..8e891884ead2 --- /dev/null +++ b/internal/command/testdata/test/top-dir-only-test-files/fixtures/main.tf @@ -0,0 +1,8 @@ +variable "sample" { + type = bool + default = true +} + +output "name" { + value = var.sample +} \ No newline at end of file diff --git a/internal/command/testdata/test/top-dir-only-test-files/main.tftest.hcl b/internal/command/testdata/test/top-dir-only-test-files/main.tftest.hcl new file mode 100644 index 000000000000..387693d859eb --- /dev/null +++ b/internal/command/testdata/test/top-dir-only-test-files/main.tftest.hcl @@ -0,0 +1,9 @@ +run "foo" { + module { + source = "./fixtures" + } + assert { + condition = output.name == true + error_message = "foo" + } +} \ No newline at end of file diff --git a/internal/configs/parser_config_dir.go b/internal/configs/parser_config_dir.go index fd659858c7f7..59d4d9d1007d 100644 --- a/internal/configs/parser_config_dir.go +++ b/internal/configs/parser_config_dir.go @@ -324,21 +324,21 @@ func IsIgnoredFile(name string) bool { } // IsEmptyDir returns true if the given filesystem path contains no Terraform -// configuration files. +// configuration or test files. // // Unlike the methods of the Parser type, this function always consults the // real filesystem, and thus it isn't appropriate to use when working with // configuration loaded from a plan file. -func IsEmptyDir(path string) (bool, error) { +func IsEmptyDir(path, testDir string) (bool, error) { if _, err := os.Stat(path); err != nil && os.IsNotExist(err) { return true, nil } p := NewParser(nil) - fs, os, _, diags := p.dirFiles(path, "") + fs, os, tests, diags := p.dirFiles(path, testDir) if diags.HasErrors() { return false, diags } - return len(fs) == 0 && len(os) == 0, nil + return len(fs) == 0 && len(os) == 0 && len(tests) == 0, nil } diff --git a/internal/configs/parser_config_dir_test.go b/internal/configs/parser_config_dir_test.go index 9fc1887bb23e..53d2145024d3 100644 --- a/internal/configs/parser_config_dir_test.go +++ b/internal/configs/parser_config_dir_test.go @@ -325,7 +325,7 @@ func TestParserLoadConfigDirFailure(t *testing.T) { } func TestIsEmptyDir(t *testing.T) { - val, err := IsEmptyDir(filepath.Join("testdata", "valid-files")) + val, err := IsEmptyDir(filepath.Join("testdata", "valid-files"), "") if err != nil { t.Fatalf("err: %s", err) } @@ -335,7 +335,7 @@ func TestIsEmptyDir(t *testing.T) { } func TestIsEmptyDir_noExist(t *testing.T) { - val, err := IsEmptyDir(filepath.Join("testdata", "nopenopenope")) + val, err := IsEmptyDir(filepath.Join("testdata", "nopenopenope"), "") if err != nil { t.Fatalf("err: %s", err) } @@ -344,8 +344,8 @@ func TestIsEmptyDir_noExist(t *testing.T) { } } -func TestIsEmptyDir_noConfigs(t *testing.T) { - val, err := IsEmptyDir(filepath.Join("testdata", "dir-empty")) +func TestIsEmptyDir_noConfigsAndTests(t *testing.T) { + val, err := IsEmptyDir(filepath.Join("testdata", "dir-empty"), "") if err != nil { t.Fatalf("err: %s", err) } @@ -353,3 +353,26 @@ func TestIsEmptyDir_noConfigs(t *testing.T) { 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") + } +} diff --git a/internal/configs/testdata/only-nested-test-files/fixtures/main.tf b/internal/configs/testdata/only-nested-test-files/fixtures/main.tf new file mode 100644 index 000000000000..8e891884ead2 --- /dev/null +++ b/internal/configs/testdata/only-nested-test-files/fixtures/main.tf @@ -0,0 +1,8 @@ +variable "sample" { + type = bool + default = true +} + +output "name" { + value = var.sample +} \ No newline at end of file diff --git a/internal/configs/testdata/only-nested-test-files/tests/main.tftest.hcl b/internal/configs/testdata/only-nested-test-files/tests/main.tftest.hcl new file mode 100644 index 000000000000..387693d859eb --- /dev/null +++ b/internal/configs/testdata/only-nested-test-files/tests/main.tftest.hcl @@ -0,0 +1,9 @@ +run "foo" { + module { + source = "./fixtures" + } + assert { + condition = output.name == true + error_message = "foo" + } +} \ No newline at end of file diff --git a/internal/configs/testdata/only-test-files/fixtures/main.tf b/internal/configs/testdata/only-test-files/fixtures/main.tf new file mode 100644 index 000000000000..8e891884ead2 --- /dev/null +++ b/internal/configs/testdata/only-test-files/fixtures/main.tf @@ -0,0 +1,8 @@ +variable "sample" { + type = bool + default = true +} + +output "name" { + value = var.sample +} \ No newline at end of file diff --git a/internal/configs/testdata/only-test-files/main.tftest.hcl b/internal/configs/testdata/only-test-files/main.tftest.hcl new file mode 100644 index 000000000000..387693d859eb --- /dev/null +++ b/internal/configs/testdata/only-test-files/main.tftest.hcl @@ -0,0 +1,9 @@ +run "foo" { + module { + source = "./fixtures" + } + assert { + condition = output.name == true + error_message = "foo" + } +} \ No newline at end of file