diff --git a/cmd/terramate/cli/cli.go b/cmd/terramate/cli/cli.go index a43b8386a..8a9d041ed 100644 --- a/cmd/terramate/cli/cli.go +++ b/cmd/terramate/cli/cli.go @@ -74,6 +74,10 @@ type cliSpec struct { DisableCheckGitUntracked bool `optional:"true" default:"false" help:"Disable git check for untracked files"` DisableCheckGitUncommitted bool `optional:"true" default:"false" help:"Disable git check for uncommitted files"` + Fmt struct { + Check bool `help:"Lists unformatted files, exit with 0 if all is formatted, 1 otherwise"` + } `cmd:"" help:"List stacks"` + List struct { Why bool `help:"Shows the reason why the stack has changed"` } `cmd:"" help:"List stacks"` @@ -322,6 +326,8 @@ func (c *cli) run() { logger.Debug().Msg("Handle command.") switch c.ctx.Command() { + case "fmt": + c.format() case "list": c.printStacks() case "run": @@ -343,7 +349,7 @@ func (c *cli) run() { case "experimental run-order": c.printRunOrder() default: - log.Fatal().Msg("unexpected command sequence") + logger.Fatal().Msg("unexpected command sequence") } } @@ -465,6 +471,52 @@ func (c *cli) listStacks(mgr *terramate.Manager, isChanged bool) (*terramate.Sta return mgr.List() } +func (c *cli) format() { + logger := log.With(). + Str("workingDir", c.wd()). + Str("action", "format()"). + Logger() + + logger.Trace().Msg("formatting all files recursively") + results, err := hcl.FormatTree(c.wd()) + if err != nil { + logger.Fatal().Err(err).Msg("formatting files") + } + + logger.Trace().Msg("listing formatted files") + for _, res := range results { + path := strings.TrimPrefix(res.Path(), c.wd()+string(filepath.Separator)) + c.log(path) + } + + if c.parsedArgs.Fmt.Check { + logger.Trace().Msg("checking if we have unformatted files") + if len(results) > 0 { + logger.Trace().Msg("we have unformatted files") + os.Exit(1) + } + logger.Trace().Msg("all files formatted, nothing else to do") + return + } + + logger.Trace().Msg("saving formatted files") + + errs := errors.L() + for _, res := range results { + logger := log.With(). + Str("workingDir", c.wd()). + Str("filepath", res.Path()). + Str("action", "format()"). + Logger() + logger.Trace().Msg("saving formatted file") + errs.Append(res.Save()) + } + + if err := errs.AsError(); err != nil { + logger.Fatal().Err(err).Msg("saving files") + } +} + func (c *cli) printStacks() { logger := log.With(). Str("action", "printStacks()"). diff --git a/cmd/terramate/e2etests/fmt_test.go b/cmd/terramate/e2etests/fmt_test.go new file mode 100644 index 000000000..c62fe855d --- /dev/null +++ b/cmd/terramate/e2etests/fmt_test.go @@ -0,0 +1,180 @@ +// Copyright 2022 Mineiros GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package e2etest + +import ( + "fmt" + "path/filepath" + "strings" + "testing" + + "github.com/madlambda/spells/assert" + "github.com/mineiros-io/terramate/hcl" + "github.com/mineiros-io/terramate/test/sandbox" +) + +func TestFormatRecursively(t *testing.T) { + const unformattedHCL = ` +globals { +name = "name" + description = "desc" + test = true + } + ` + formattedHCL, err := hcl.Format(unformattedHCL, "") + assert.NoError(t, err) + + s := sandbox.New(t) + cli := newCLI(t, s.RootDir()) + + t.Run("checking succeeds when there is no Terramate files", func(t *testing.T) { + assertRunResult(t, cli.run("fmt", "--check"), runExpected{}) + }) + + t.Run("formatting succeeds when there is no Terramate files", func(t *testing.T) { + assertRunResult(t, cli.run("fmt"), runExpected{}) + }) + + sprintf := fmt.Sprintf + writeUnformattedFiles := func() { + s.BuildTree([]string{ + sprintf("f:globals.tm:%s", unformattedHCL), + sprintf("f:another-stacks/globals.tm.hcl:%s", unformattedHCL), + sprintf("f:another-stacks/stack-1/globals.tm.hcl:%s", unformattedHCL), + sprintf("f:another-stacks/stack-2/globals.tm.hcl:%s", unformattedHCL), + sprintf("f:stacks/globals.tm:%s", unformattedHCL), + sprintf("f:stacks/stack-1/globals.tm:%s", unformattedHCL), + sprintf("f:stacks/stack-2/globals.tm:%s", unformattedHCL), + }) + } + + writeUnformattedFiles() + + wantedFiles := []string{ + "globals.tm", + "another-stacks/globals.tm.hcl", + "another-stacks/stack-1/globals.tm.hcl", + "another-stacks/stack-2/globals.tm.hcl", + "stacks/globals.tm", + "stacks/stack-1/globals.tm", + "stacks/stack-2/globals.tm", + } + filesListOutput := func(files []string) string { + return strings.Join(files, "\n") + "\n" + } + wantedFilesStr := filesListOutput(wantedFiles) + + assertFileContents := func(t *testing.T, path string, want string) { + t.Helper() + got := s.RootEntry().ReadFile(path) + assert.EqualStrings(t, want, string(got)) + } + + assertWantedFilesContents := func(t *testing.T, want string) { + t.Helper() + + for _, file := range wantedFiles { + assertFileContents(t, file, want) + } + } + + t.Run("checking fails with unformatted files", func(t *testing.T) { + assertRunResult(t, cli.run("fmt", "--check"), runExpected{ + Status: 1, + Stdout: wantedFilesStr, + }) + assertWantedFilesContents(t, unformattedHCL) + }) + + t.Run("checking fails with unformatted files on subdirs", func(t *testing.T) { + subdir := filepath.Join(s.RootDir(), "another-stacks") + cli := newCLI(t, subdir) + assertRunResult(t, cli.run("fmt", "--check"), runExpected{ + Status: 1, + Stdout: filesListOutput([]string{ + "globals.tm.hcl", + "stack-1/globals.tm.hcl", + "stack-2/globals.tm.hcl", + }), + }) + assertWantedFilesContents(t, unformattedHCL) + }) + + t.Run("update unformatted files in place", func(t *testing.T) { + assertRunResult(t, cli.run("fmt"), runExpected{ + Stdout: wantedFilesStr, + }) + assertWantedFilesContents(t, formattedHCL) + }) + + t.Run("checking succeeds when all files are formatted", func(t *testing.T) { + assertRunResult(t, cli.run("fmt", "--check"), runExpected{}) + assertWantedFilesContents(t, formattedHCL) + }) + + t.Run("formatting succeeds when all files are formatted", func(t *testing.T) { + assertRunResult(t, cli.run("fmt"), runExpected{}) + assertWantedFilesContents(t, formattedHCL) + }) + + t.Run("update unformatted files in subdirs", func(t *testing.T) { + writeUnformattedFiles() + + anotherStacks := filepath.Join(s.RootDir(), "another-stacks") + cli := newCLI(t, anotherStacks) + assertRunResult(t, cli.run("fmt"), runExpected{ + Stdout: filesListOutput([]string{ + "globals.tm.hcl", + "stack-1/globals.tm.hcl", + "stack-2/globals.tm.hcl", + }), + }) + + assertFileContents(t, "another-stacks/globals.tm.hcl", formattedHCL) + assertFileContents(t, "another-stacks/stack-1/globals.tm.hcl", formattedHCL) + assertFileContents(t, "another-stacks/stack-2/globals.tm.hcl", formattedHCL) + + assertFileContents(t, "globals.tm", unformattedHCL) + assertFileContents(t, "stacks/globals.tm", unformattedHCL) + assertFileContents(t, "stacks/stack-1/globals.tm", unformattedHCL) + assertFileContents(t, "stacks/stack-2/globals.tm", unformattedHCL) + + stacks := filepath.Join(s.RootDir(), "stacks") + cli = newCLI(t, stacks) + assertRunResult(t, cli.run("fmt"), runExpected{ + Stdout: filesListOutput([]string{ + "globals.tm", + "stack-1/globals.tm", + "stack-2/globals.tm", + }), + }) + + assertFileContents(t, "another-stacks/globals.tm.hcl", formattedHCL) + assertFileContents(t, "another-stacks/stack-1/globals.tm.hcl", formattedHCL) + assertFileContents(t, "another-stacks/stack-2/globals.tm.hcl", formattedHCL) + assertFileContents(t, "stacks/globals.tm", formattedHCL) + assertFileContents(t, "stacks/stack-1/globals.tm", formattedHCL) + assertFileContents(t, "stacks/stack-2/globals.tm", formattedHCL) + + assertFileContents(t, "globals.tm", unformattedHCL) + + cli = newCLI(t, s.RootDir()) + assertRunResult(t, cli.run("fmt"), runExpected{ + Stdout: filesListOutput([]string{"globals.tm"}), + }) + + assertWantedFilesContents(t, formattedHCL) + }) +} diff --git a/generate/genhcl/genhcl.go b/generate/genhcl/genhcl.go index 0cdcaa620..e5449a503 100644 --- a/generate/genhcl/genhcl.go +++ b/generate/genhcl/genhcl.go @@ -39,7 +39,7 @@ type StackHCLs struct { // about the origin of the generated code. type HCL struct { origin string - body []byte + body string } const ( @@ -139,16 +139,19 @@ func Load(rootdir string, sm stack.Metadata, globals stack.Globals) (StackHCLs, gen := hclwrite.NewEmptyFile() if err := hcl.CopyBody(gen.Body(), loadedHCL.block.Body, evalctx); err != nil { - return StackHCLs{}, errors.E( - ErrEval, - sm, - err, + return StackHCLs{}, errors.E(ErrEval, sm, err, "failed to generate block %q", name, ) } + formatted, err := hcl.Format(string(gen.Bytes()), loadedHCL.origin) + if err != nil { + return StackHCLs{}, errors.E(sm, err, + "failed to format generated code for block %q", name, + ) + } res.hcls[name] = HCL{ origin: loadedHCL.origin, - body: hclwrite.Format(gen.Bytes()), + body: formatted, } } diff --git a/hcl/eval/partial_fuzz_test.go b/hcl/eval/partial_fuzz_test.go index a4289dc09..895c333cb 100644 --- a/hcl/eval/partial_fuzz_test.go +++ b/hcl/eval/partial_fuzz_test.go @@ -31,8 +31,6 @@ import ( "github.com/madlambda/spells/assert" "github.com/rs/zerolog" "github.com/zclconf/go-cty/cty" - - tmhclwrite "github.com/mineiros-io/terramate/test/hclwrite" ) func FuzzPartialEval(f *testing.F) { @@ -92,13 +90,9 @@ func FuzzPartialEval(f *testing.F) { const testattr = "attr" - cfg := hcldoc( - expr(testattr, str), - ) - - cfgString := cfg.String() + cfg := fmt.Sprintf("%s = %s", testattr, str) parser := hclparse.NewParser() - file, diags := parser.ParseHCL([]byte(cfgString), "fuzz") + file, diags := parser.ParseHCL([]byte(cfg), "fuzz") if diags.HasErrors() { return } @@ -108,7 +102,7 @@ func FuzzPartialEval(f *testing.F) { parsedExpr := attr.Expr exprRange := parsedExpr.Range() - exprBytes := cfgString[exprRange.Start.Byte:exprRange.End.Byte] + exprBytes := cfg[exprRange.Start.Byte:exprRange.End.Byte] parsedTokens, diags := hclsyntax.LexExpression([]byte(exprBytes), "fuzz", hcl.Pos{}) if diags.HasErrors() { @@ -123,9 +117,9 @@ func FuzzPartialEval(f *testing.F) { engine := newPartialEvalEngine(want, ctx) got, err := engine.Eval() - if strings.Contains(cfgString, "global") || - strings.Contains(cfgString, "terramate") || - strings.Contains(cfgString, "tm_") { + if strings.Contains(cfg, "global") || + strings.Contains(cfg, "terramate") || + strings.Contains(cfg, "tm_") { // TODO(katcipis): Validate generated code properties when // substitution is in play. return @@ -156,14 +150,6 @@ func tokensStr(t hclwrite.Tokens) string { return "[" + strings.Join(tokensStrs, ",") + "]" } -func hcldoc(builders ...tmhclwrite.BlockBuilder) *tmhclwrite.Block { - return tmhclwrite.BuildHCL(builders...) -} - -func expr(name string, expr string) tmhclwrite.BlockBuilder { - return tmhclwrite.Expression(name, expr) -} - func init() { zerolog.SetGlobalLevel(zerolog.Disabled) } diff --git a/hcl/fmt.go b/hcl/fmt.go new file mode 100644 index 000000000..d397df07e --- /dev/null +++ b/hcl/fmt.go @@ -0,0 +1,153 @@ +// Copyright 2022 Mineiros GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hcl + +import ( + "os" + "path/filepath" + + "github.com/hashicorp/hcl/v2/hclparse" + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/mineiros-io/terramate/errors" + "github.com/rs/zerolog/log" +) + +// FormatResult represents the result of a formatting operation. +type FormatResult struct { + path string + formatted string +} + +// Format will format the given source code. It returns an error if the given +// source is invalid HCL. +func Format(src, filename string) (string, error) { + p := hclparse.NewParser() + _, diags := p.ParseHCL([]byte(src), filename) + if err := errors.L(diags).AsError(); err != nil { + return "", errors.E(ErrHCLSyntax, err) + } + // For now we just use plain hclwrite.Format + // but we plan on customizing formatting in the near future. + return string(hclwrite.Format([]byte(src))), nil +} + +// FormatTree will format all Terramate configuration files +// in the given tree starting at the given dir. It will recursively +// navigate on sub directories. Directories starting with "." are ignored. +// +// Only Terramate configuration files will be formatted. +// +// Files that are already formatted are ignored. If all files are formatted +// this function returns an empty result. +// +// All files will be left untouched. To save the formatted result on disk you +// can use FormatResult.Save for each FormatResult. +func FormatTree(dir string) ([]FormatResult, error) { + logger := log.With(). + Str("action", "hcl.FormatTree()"). + Str("dir", dir). + Logger() + + logger.Trace().Msg("listing terramate files") + + files, err := listTerramateFiles(dir) + if err != nil { + return nil, errors.E(errFormatTree, err) + } + + results := []FormatResult{} + errs := errors.L() + + for _, f := range files { + logger := log.With(). + Str("file", f). + Logger() + + logger.Trace().Msg("reading file") + + path := filepath.Join(dir, f) + fileContents, err := os.ReadFile(path) + if err != nil { + errs.Append(err) + continue + } + + logger.Trace().Msg("formatting file") + + currentCode := string(fileContents) + formatted, err := Format(currentCode, path) + if err != nil { + errs.Append(err) + continue + } + + if currentCode == formatted { + logger.Trace().Msg("file already formatted") + continue + } + + logger.Trace().Msg("file needs formatting, adding to results") + + results = append(results, FormatResult{ + path: path, + formatted: formatted, + }) + } + + dirs, err := listTerramateDirs(dir) + if err != nil { + errs.Append(err) + return nil, errors.E(errFormatTree, errs) + } + + for _, d := range dirs { + logger := log.With(). + Str("subdir", d). + Logger() + + logger.Trace().Msg("recursively formatting") + subres, err := FormatTree(filepath.Join(dir, d)) + if err != nil { + errs.Append(err) + continue + } + results = append(results, subres...) + } + + if err := errs.AsError(); err != nil { + return nil, err + } + return results, nil +} + +// Save will save the formatted result on the original file, replacing +// its original contents. +func (f FormatResult) Save() error { + return os.WriteFile(f.path, []byte(f.formatted), 0644) +} + +// Path is the absolute path of the original file. +func (f FormatResult) Path() string { + return f.path +} + +// Formatted is the contents of the original file after formatting. +func (f FormatResult) Formatted() string { + return f.formatted +} + +const ( + errFormatTree errors.Kind = "formatting tree" +) diff --git a/hcl/fmt_test.go b/hcl/fmt_test.go new file mode 100644 index 000000000..2de33cef2 --- /dev/null +++ b/hcl/fmt_test.go @@ -0,0 +1,224 @@ +// Copyright 2022 Mineiros GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hcl_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/madlambda/spells/assert" + "github.com/mineiros-io/terramate/errors" + "github.com/mineiros-io/terramate/hcl" + "github.com/mineiros-io/terramate/test" + + errtest "github.com/mineiros-io/terramate/test/errors" +) + +func TestFormatHCL(t *testing.T) { + type testcase struct { + name string + input string + want string + wantErrs []error + } + + tcases := []testcase{ + { + name: "attributes alignment", + input: ` +a = 1 + b = "la" + c = 666 + d = [] +`, + want: ` +a = 1 +b = "la" +c = 666 +d = [] +`, + }, + { + name: "fails on syntax errors", + input: ` + string = hi" + bool = rue + list = [ + obj = { + `, + wantErrs: []error{ + errors.E(hcl.ErrHCLSyntax), + errors.E(mkrange(start(2, 17, 17), end(3, 1, 18))), + errors.E(mkrange(start(3, 17, 34), end(4, 1, 35))), + errors.E(mkrange(start(4, 15, 49), end(5, 1, 50))), + errors.E(mkrange(start(5, 15, 64), end(6, 1, 65))), + errors.E(mkrange(start(2, 16, 16), end(2, 17, 17))), + }, + }, + } + + for _, tcase := range tcases { + t.Run(tcase.name, func(t *testing.T) { + const filename = "test-input.hcl" + got, err := hcl.Format(tcase.input, filename) + + addFilenameToErrorsFileRanges(tcase.wantErrs, filename) + errtest.AssertErrorList(t, err, tcase.wantErrs) + assert.EqualStrings(t, tcase.want, got) + }) + + // piggyback on the overall formatting scenarios to check + // for hcl.FormatTree behavior. + t.Run("Tree/"+tcase.name, func(t *testing.T) { + const ( + filename = "file.tm" + subdirName = "subdir" + ) + + rootdir := t.TempDir() + test.Mkdir(t, rootdir, subdirName) + subdir := filepath.Join(rootdir, subdirName) + + test.WriteFile(t, rootdir, filename, tcase.input) + test.WriteFile(t, subdir, filename, tcase.input) + + got, err := hcl.FormatTree(rootdir) + + // Since we have identical files we expect the same + // set of errors for each filepath to be present. + wantFilepath := filepath.Join(rootdir, filename) + wantSubdirFilepath := filepath.Join(subdir, filename) + wantErrs := []error{} + + for _, path := range []string{wantFilepath, wantSubdirFilepath} { + for _, wantErr := range tcase.wantErrs { + if e, ok := wantErr.(*errors.Error); ok { + err := *e + err.FileRange.Filename = path + wantErrs = append(wantErrs, &err) + continue + } + + wantErrs = append(wantErrs, wantErr) + } + + } + errtest.AssertErrorList(t, err, wantErrs) + if err != nil { + return + } + assert.EqualInts(t, 2, len(got), "want 2 formatted files, got: %v", got) + + for _, res := range got { + assert.EqualStrings(t, tcase.want, res.Formatted()) + assertFileContains(t, res.Path(), tcase.input) + } + + assert.EqualStrings(t, wantFilepath, got[0].Path()) + assert.EqualStrings(t, wantSubdirFilepath, got[1].Path()) + + t.Run("saving format results", func(t *testing.T) { + for _, res := range got { + assert.NoError(t, res.Save()) + assertFileContains(t, res.Path(), res.Formatted()) + } + + got, err := hcl.FormatTree(rootdir) + assert.NoError(t, err) + + if len(got) > 0 { + t.Fatalf("after formatting want 0 fmt results, got: %v", got) + } + }) + }) + } +} + +func TestFormatTreeReturnsEmptyResultsForEmptyDir(t *testing.T) { + tmpdir := t.TempDir() + got, err := hcl.FormatTree(tmpdir) + assert.NoError(t, err) + assert.EqualInts(t, 0, len(got), "want no results, got: %v", got) +} + +func TestFormatTreeFailsOnNonExistentDir(t *testing.T) { + tmpdir := t.TempDir() + _, err := hcl.FormatTree(filepath.Join(tmpdir, "non-existent")) + assert.Error(t, err) +} + +func TestFormatTreeFailsOnNonAccessibleSubdir(t *testing.T) { + const subdir = "subdir" + tmpdir := t.TempDir() + test.Mkdir(t, tmpdir, subdir) + + assert.NoError(t, os.Chmod(filepath.Join(tmpdir, subdir), 0)) + + _, err := hcl.FormatTree(tmpdir) + assert.Error(t, err) +} + +func TestFormatTreeFailsOnNonAccessibleFile(t *testing.T) { + const filename = "filename.tm" + + tmpdir := t.TempDir() + test.WriteFile(t, tmpdir, filename, `globals{ + a = 2 + b = 3 + }`) + + assert.NoError(t, os.Chmod(filepath.Join(tmpdir, filename), 0)) + + _, err := hcl.FormatTree(tmpdir) + assert.Error(t, err) +} + +func TestFormatTreeIgnoresNonTerramateFiles(t *testing.T) { + const ( + subdirName = ".dotdir" + unformattedCode = ` +a = 1 + b = "la" + c = 666 + d = [] +` + ) + + tmpdir := t.TempDir() + test.WriteFile(t, tmpdir, ".file.tm", unformattedCode) + test.WriteFile(t, tmpdir, "file.tf", unformattedCode) + test.WriteFile(t, tmpdir, "file.hcl", unformattedCode) + + test.Mkdir(t, tmpdir, subdirName) + subdir := filepath.Join(tmpdir, subdirName) + test.WriteFile(t, subdir, ".file.tm", unformattedCode) + test.WriteFile(t, subdir, "file.tm", unformattedCode) + test.WriteFile(t, subdir, "file.tm.hcl", unformattedCode) + + got, err := hcl.FormatTree(tmpdir) + assert.NoError(t, err) + assert.EqualInts(t, 0, len(got), "want no results, got: %v", got) +} + +func assertFileContains(t *testing.T, filepath, got string) { + t.Helper() + + data, err := os.ReadFile(filepath) + assert.NoError(t, err, "reading file") + + want := string(data) + assert.EqualStrings(t, want, got, "file %q contents don't match", filepath) +} diff --git a/hcl/hcl.go b/hcl/hcl.go index 4dd04e68d..26bd37911 100644 --- a/hcl/hcl.go +++ b/hcl/hcl.go @@ -1127,6 +1127,46 @@ func listTerramateFiles(dir string) ([]string, error) { return files, nil } +// listTerramateDirs lists Terramate dirs, which are any dirs +// except ones starting with ".". +func listTerramateDirs(dir string) ([]string, error) { + logger := log.With(). + Str("action", "listTerramateDirs()"). + Str("dir", dir). + Logger() + + logger.Trace().Msg("listing dirs") + + dirEntries, err := os.ReadDir(dir) + if err != nil { + return nil, errors.E(err, "reading dir to list Terramate dirs") + } + + logger.Trace().Msg("looking for Terramate directories") + + dirs := []string{} + + for _, dirEntry := range dirEntries { + logger := logger.With(). + Str("entryName", dirEntry.Name()). + Logger() + + if !dirEntry.IsDir() { + logger.Trace().Msg("ignoring non-dir") + continue + } + + if strings.HasPrefix(dirEntry.Name(), ".") { + logger.Trace().Msg("ignoring dotdir") + continue + } + + dirs = append(dirs, dirEntry.Name()) + } + + return dirs, nil +} + func isTerramateFile(filename string) bool { return strings.HasSuffix(filename, ".tm") || strings.HasSuffix(filename, ".tm.hcl") } diff --git a/hcl/hcl_test.go b/hcl/hcl_test.go index c5f6d8b41..7fb1f6d21 100644 --- a/hcl/hcl_test.go +++ b/hcl/hcl_test.go @@ -212,10 +212,8 @@ module "test" { addFilenameToErrorsFileRanges(tc.want.errs, tfpath) modules, err := hcl.ParseModules(tfpath) - errtest.AssertIsErrors(t, err, tc.want.errs) - if err != nil { - errtest.AssertAsErrorsList(t, err) - } + + errtest.AssertErrorList(t, err, tc.want.errs) assert.EqualInts(t, len(tc.want.modules), len(modules), diff --git a/test/errors/errors.go b/test/errors/errors.go index 90da4d27d..bedd4cf90 100644 --- a/test/errors/errors.go +++ b/test/errors/errors.go @@ -75,6 +75,18 @@ func AssertIsErrors(t *testing.T, err error, targets []error) { } } +// AssertErrorList will check that the given err is an *errors.List +// and that all given errors on targets are contained on it +// using errors.Is. +func AssertErrorList(t *testing.T, err error, targets []error) { + t.Helper() + + if err != nil { + AssertAsErrorsList(t, err) + } + AssertIsErrors(t, err, targets) +} + // AssertAsErrorsList will check if the given error can be handled // as an *errors.List by calling errors.As. It fails if the error fails // to be an *errors.List. diff --git a/test/hclwrite/hclwrite.go b/test/hclwrite/hclwrite.go index ad80dd923..19afbcd97 100644 --- a/test/hclwrite/hclwrite.go +++ b/test/hclwrite/hclwrite.go @@ -35,7 +35,7 @@ import ( "github.com/hashicorp/hcl/v2/hclparse" "github.com/hashicorp/hcl/v2/hclsyntax" - "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/mineiros-io/terramate/hcl" "github.com/zclconf/go-cty/cty" ) @@ -222,7 +222,11 @@ func NumberInt(name string, val int64) BlockBuilder { // Format formats the given HCL code. func Format(code string) string { - return strings.Trim(string(hclwrite.Format([]byte(code))), "\n ") + formatted, err := hcl.Format(code, "gen.hcl") + if err != nil { + panic(fmt.Errorf("invalid code:\n%s\ncan't be formatted: %v", code, err)) + } + return strings.Trim(formatted, "\n ") } // Build calls the underlying builder function to build the given block.