Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow terraform init when only test files are present in directory #36429

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/unreleased/ENHANCEMENTS-20250205-104144.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: ENHANCEMENTS
body: Allow terraform init when only test files are present in directory
dsa0x marked this conversation as resolved.
Show resolved Hide resolved
time: 2025-02-05T10:41:44.663251+01:00
custom:
Issue: "35040"
4 changes: 2 additions & 2 deletions internal/command/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
57 changes: 57 additions & 0 deletions internal/command/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"log"
"os"
"path/filepath"
"regexp"
"strings"
"testing"

Expand All @@ -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"
Expand All @@ -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()
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion internal/command/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, "")
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
Expand Down
8 changes: 8 additions & 0 deletions internal/command/test_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
variable "sample" {
type = bool
default = true
}

output "name" {
value = var.sample
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
run "foo" {
module {
source = "./fixtures"
}
assert {
condition = output.name == true
error_message = "foo"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
variable "sample" {
type = bool
default = true
}

output "name" {
value = var.sample
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
run "foo" {
module {
source = "./fixtures"
}
assert {
condition = output.name == true
error_message = "foo"
}
}
8 changes: 4 additions & 4 deletions internal/configs/parser_config_dir.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
31 changes: 27 additions & 4 deletions internal/configs/parser_config_dir_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,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)
}
Expand All @@ -331,7 +331,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)
}
Expand All @@ -340,12 +340,35 @@ 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)
}
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")
SarahFrench marked this conversation as resolved.
Show resolved Hide resolved
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")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
variable "sample" {
type = bool
default = true
}

output "name" {
value = var.sample
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
run "foo" {
module {
source = "./fixtures"
}
assert {
condition = output.name == true
error_message = "foo"
}
}
8 changes: 8 additions & 0 deletions internal/configs/testdata/only-test-files/fixtures/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
variable "sample" {
type = bool
default = true
}

output "name" {
value = var.sample
}
9 changes: 9 additions & 0 deletions internal/configs/testdata/only-test-files/main.tftest.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
run "foo" {
module {
source = "./fixtures"
}
assert {
condition = output.name == true
error_message = "foo"
}
}
Loading