Skip to content

Commit 7f29df9

Browse files
authored
Allow terraform init when only test files are present in directory (#36429)
1 parent 86295f5 commit 7f29df9

File tree

15 files changed

+172
-11
lines changed

15 files changed

+172
-11
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: ENHANCEMENTS
2+
body: Allow terraform init when tests are present but no configuration files are directly inside the current directory
3+
time: 2025-02-05T10:41:44.663251+01:00
4+
custom:
5+
Issue: "35040"

internal/command/init.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ func (c *InitCommand) Run(args []string) int {
108108
if initArgs.FromModule != "" {
109109
src := initArgs.FromModule
110110

111-
empty, err := configs.IsEmptyDir(path)
111+
empty, err := configs.IsEmptyDir(path, initArgs.TestsDirectory)
112112
if err != nil {
113113
diags = diags.Append(fmt.Errorf("Error validating destination directory: %s", err))
114114
view.Diagnostics(diags)
@@ -148,7 +148,7 @@ func (c *InitCommand) Run(args []string) int {
148148

149149
// If our directory is empty, then we're done. We can't get or set up
150150
// the backend with an empty directory.
151-
empty, err := configs.IsEmptyDir(path)
151+
empty, err := configs.IsEmptyDir(path, initArgs.TestsDirectory)
152152
if err != nil {
153153
diags = diags.Append(fmt.Errorf("Error checking configuration: %s", err))
154154
view.Diagnostics(diags)

internal/command/init_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"log"
1010
"os"
1111
"path/filepath"
12+
"regexp"
1213
"strings"
1314
"testing"
1415

@@ -20,6 +21,7 @@ import (
2021

2122
"github.com/hashicorp/terraform/internal/addrs"
2223
"github.com/hashicorp/terraform/internal/command/arguments"
24+
"github.com/hashicorp/terraform/internal/command/views"
2325
"github.com/hashicorp/terraform/internal/configs"
2426
"github.com/hashicorp/terraform/internal/configs/configschema"
2527
"github.com/hashicorp/terraform/internal/depsfile"
@@ -32,6 +34,25 @@ import (
3234
"github.com/hashicorp/terraform/internal/states/statemgr"
3335
)
3436

37+
// cleanString removes newlines, and redundant spaces.
38+
func cleanString(s string) string {
39+
// Replace newlines with a single space.
40+
s = strings.ReplaceAll(s, "\n", " ")
41+
42+
// Remove other special characters like \r, \t
43+
s = strings.ReplaceAll(s, "\r", "")
44+
s = strings.ReplaceAll(s, "\t", "")
45+
46+
// Replace multiple spaces with a single space.
47+
spaceRegex := regexp.MustCompile(`\s+`)
48+
s = spaceRegex.ReplaceAllString(s, " ")
49+
50+
// Trim any leading or trailing spaces.
51+
s = strings.TrimSpace(s)
52+
53+
return s
54+
}
55+
3556
func TestInit_empty(t *testing.T) {
3657
// Create a temporary working directory that is empty
3758
td := t.TempDir()
@@ -52,6 +73,42 @@ func TestInit_empty(t *testing.T) {
5273
if code := c.Run(args); code != 0 {
5374
t.Fatalf("bad: \n%s", done(t).All())
5475
}
76+
exp := views.MessageRegistry[views.OutputInitEmptyMessage].JSONValue
77+
actual := cleanString(done(t).All())
78+
if !strings.Contains(actual, cleanString(exp)) {
79+
t.Fatalf("expected output to be %q\n, got %q", exp, actual)
80+
}
81+
}
82+
83+
func TestInit_only_test_files(t *testing.T) {
84+
// Create a temporary working directory that has only test files and no tf configuration
85+
td := t.TempDir()
86+
os.MkdirAll(td, 0755)
87+
defer testChdir(t, td)()
88+
89+
if _, err := os.Create("main.tftest.hcl"); err != nil {
90+
t.Fatalf("err: %s", err)
91+
}
92+
93+
ui := new(cli.MockUi)
94+
view, done := testView(t)
95+
c := &InitCommand{
96+
Meta: Meta{
97+
testingOverrides: metaOverridesForProvider(testProvider()),
98+
Ui: ui,
99+
View: view,
100+
},
101+
}
102+
103+
args := []string{}
104+
if code := c.Run(args); code != 0 {
105+
t.Fatalf("bad: \n%s", done(t).All())
106+
}
107+
exp := views.MessageRegistry[views.OutputInitSuccessCLIMessage].JSONValue
108+
actual := cleanString(done(t).All())
109+
if !strings.Contains(actual, cleanString(exp)) {
110+
t.Fatalf("expected output to be %q\n, got %q", exp, actual)
111+
}
55112
}
56113

57114
func TestInit_multipleArgs(t *testing.T) {

internal/command/providers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func (c *ProvidersCommand) Run(args []string) int {
4949

5050
var diags tfdiags.Diagnostics
5151

52-
empty, err := configs.IsEmptyDir(configPath)
52+
empty, err := configs.IsEmptyDir(configPath, testsDirectory)
5353
if err != nil {
5454
diags = diags.Append(tfdiags.Sourceless(
5555
tfdiags.Error,

internal/command/test_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ func TestTest_Runs(t *testing.T) {
4141
expectedOut: []string{"1 passed, 0 failed."},
4242
code: 0,
4343
},
44+
"top-dir-only-test-files": {
45+
expectedOut: []string{"1 passed, 0 failed."},
46+
code: 0,
47+
},
48+
"top-dir-only-nested-test-files": {
49+
expectedOut: []string{"1 passed, 0 failed."},
50+
code: 0,
51+
},
4452
"simple_pass_nested": {
4553
expectedOut: []string{"1 passed, 0 failed."},
4654
code: 0,
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
variable "sample" {
2+
type = bool
3+
default = true
4+
}
5+
6+
output "name" {
7+
value = var.sample
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
run "foo" {
2+
module {
3+
source = "./fixtures"
4+
}
5+
assert {
6+
condition = output.name == true
7+
error_message = "foo"
8+
}
9+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
variable "sample" {
2+
type = bool
3+
default = true
4+
}
5+
6+
output "name" {
7+
value = var.sample
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
run "foo" {
2+
module {
3+
source = "./fixtures"
4+
}
5+
assert {
6+
condition = output.name == true
7+
error_message = "foo"
8+
}
9+
}

internal/configs/parser_config_dir.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -324,21 +324,21 @@ func IsIgnoredFile(name string) bool {
324324
}
325325

326326
// IsEmptyDir returns true if the given filesystem path contains no Terraform
327-
// configuration files.
327+
// configuration or test files.
328328
//
329329
// Unlike the methods of the Parser type, this function always consults the
330330
// real filesystem, and thus it isn't appropriate to use when working with
331331
// configuration loaded from a plan file.
332-
func IsEmptyDir(path string) (bool, error) {
332+
func IsEmptyDir(path, testDir string) (bool, error) {
333333
if _, err := os.Stat(path); err != nil && os.IsNotExist(err) {
334334
return true, nil
335335
}
336336

337337
p := NewParser(nil)
338-
fs, os, _, diags := p.dirFiles(path, "")
338+
fs, os, tests, diags := p.dirFiles(path, testDir)
339339
if diags.HasErrors() {
340340
return false, diags
341341
}
342342

343-
return len(fs) == 0 && len(os) == 0, nil
343+
return len(fs) == 0 && len(os) == 0 && len(tests) == 0, nil
344344
}

internal/configs/parser_config_dir_test.go

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ func TestParserLoadConfigDirFailure(t *testing.T) {
325325
}
326326

327327
func TestIsEmptyDir(t *testing.T) {
328-
val, err := IsEmptyDir(filepath.Join("testdata", "valid-files"))
328+
val, err := IsEmptyDir(filepath.Join("testdata", "valid-files"), "")
329329
if err != nil {
330330
t.Fatalf("err: %s", err)
331331
}
@@ -335,7 +335,7 @@ func TestIsEmptyDir(t *testing.T) {
335335
}
336336

337337
func TestIsEmptyDir_noExist(t *testing.T) {
338-
val, err := IsEmptyDir(filepath.Join("testdata", "nopenopenope"))
338+
val, err := IsEmptyDir(filepath.Join("testdata", "nopenopenope"), "")
339339
if err != nil {
340340
t.Fatalf("err: %s", err)
341341
}
@@ -344,12 +344,35 @@ func TestIsEmptyDir_noExist(t *testing.T) {
344344
}
345345
}
346346

347-
func TestIsEmptyDir_noConfigs(t *testing.T) {
348-
val, err := IsEmptyDir(filepath.Join("testdata", "dir-empty"))
347+
func TestIsEmptyDir_noConfigsAndTests(t *testing.T) {
348+
val, err := IsEmptyDir(filepath.Join("testdata", "dir-empty"), "")
349349
if err != nil {
350350
t.Fatalf("err: %s", err)
351351
}
352352
if !val {
353353
t.Fatal("should be empty")
354354
}
355355
}
356+
357+
func TestIsEmptyDir_noConfigsButHasTests(t *testing.T) {
358+
// The top directory has no configs, but it contains test files
359+
val, err := IsEmptyDir(filepath.Join("testdata", "only-test-files"), "tests")
360+
if err != nil {
361+
t.Fatalf("err: %s", err)
362+
}
363+
if val {
364+
t.Fatal("should not be empty")
365+
}
366+
}
367+
368+
func TestIsEmptyDir_nestedTestsOnly(t *testing.T) {
369+
// The top directory has no configs and no test files, but the nested
370+
// directory has test files
371+
val, err := IsEmptyDir(filepath.Join("testdata", "only-nested-test-files"), "tests")
372+
if err != nil {
373+
t.Fatalf("err: %s", err)
374+
}
375+
if val {
376+
t.Fatal("should not be empty")
377+
}
378+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
variable "sample" {
2+
type = bool
3+
default = true
4+
}
5+
6+
output "name" {
7+
value = var.sample
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
run "foo" {
2+
module {
3+
source = "./fixtures"
4+
}
5+
assert {
6+
condition = output.name == true
7+
error_message = "foo"
8+
}
9+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
variable "sample" {
2+
type = bool
3+
default = true
4+
}
5+
6+
output "name" {
7+
value = var.sample
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
run "foo" {
2+
module {
3+
source = "./fixtures"
4+
}
5+
assert {
6+
condition = output.name == true
7+
error_message = "foo"
8+
}
9+
}

0 commit comments

Comments
 (0)