Skip to content

Commit

Permalink
feat: add terramate fmt (#351)
Browse files Browse the repository at this point in the history
  • Loading branch information
katcipis committed May 19, 2022
1 parent 1116995 commit 866c004
Show file tree
Hide file tree
Showing 10 changed files with 685 additions and 33 deletions.
54 changes: 53 additions & 1 deletion cmd/terramate/cli/cli.go
Expand Up @@ -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"`
Expand Down Expand Up @@ -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":
Expand All @@ -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")
}
}

Expand Down Expand Up @@ -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()").
Expand Down
180 changes: 180 additions & 0 deletions 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)
})
}
15 changes: 9 additions & 6 deletions generate/genhcl/genhcl.go
Expand Up @@ -39,7 +39,7 @@ type StackHCLs struct {
// about the origin of the generated code.
type HCL struct {
origin string
body []byte
body string
}

const (
Expand Down Expand Up @@ -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,
}
}

Expand Down
26 changes: 6 additions & 20 deletions hcl/eval/partial_fuzz_test.go
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}
Expand All @@ -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() {
Expand All @@ -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
Expand Down Expand Up @@ -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)
}

0 comments on commit 866c004

Please sign in to comment.