mirror of
https://github.com/opentofu/opentofu.git
synced 2026-06-09 08:33:23 -04:00
Refactor fmt command to use View instead of Ui and to use the arguments package (#3805)
Signed-off-by: Andrei Ciobanu <andrei.ciobanu@opentofu.org>
This commit is contained in:
parent
383d6b3595
commit
5fcfb23eb5
6 changed files with 474 additions and 132 deletions
80
internal/command/arguments/fmt.go
Normal file
80
internal/command/arguments/fmt.go
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package arguments
|
||||
|
||||
import (
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
const (
|
||||
stdinArg = "-"
|
||||
)
|
||||
|
||||
// Fmt represents the command-line arguments for the fmt command.
|
||||
type Fmt struct {
|
||||
// Paths contains the file paths that the formatter will handle.
|
||||
// When no arguments given to the command, it will use the current directory.
|
||||
// If the first argument is -, it will read the content to format from [os.Stdin].
|
||||
Paths []string
|
||||
|
||||
// List controls the output of the formatted list. If disabled, it will not print the
|
||||
// names of the formatted files.
|
||||
List bool
|
||||
// Write controls if the formatter should write the content back to the check file or not.
|
||||
Write bool
|
||||
// Diff tells to the formatter to print the diff between the before and after formatting
|
||||
// process.
|
||||
Diff bool
|
||||
// Check can be used to instruct the command to return a non-zero error code if it finds
|
||||
// any file that is not properly formatted.
|
||||
Check bool
|
||||
// Recursive indicates that the formatting should be done recursive through all the
|
||||
// subdirectories.
|
||||
Recursive bool
|
||||
|
||||
// ViewOptions specifies which view options to use
|
||||
ViewOptions ViewOptions
|
||||
}
|
||||
|
||||
// ParseFmt processes CLI arguments, returning a Fmt value, a closer function, and errors.
|
||||
// If errors are encountered, a Fmt value is still returned representing
|
||||
// the best effort interpretation of the arguments.
|
||||
func ParseFmt(args []string) (*Fmt, func(), tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
ret := &Fmt{}
|
||||
|
||||
cmdFlags := defaultFlagSet("fmt")
|
||||
cmdFlags.BoolVar(&ret.List, "list", true, "list")
|
||||
cmdFlags.BoolVar(&ret.Write, "write", true, "write")
|
||||
cmdFlags.BoolVar(&ret.Diff, "diff", false, "diff")
|
||||
cmdFlags.BoolVar(&ret.Check, "check", false, "check")
|
||||
cmdFlags.BoolVar(&ret.Recursive, "recursive", false, "recursive")
|
||||
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to parse command-line flags",
|
||||
err.Error(),
|
||||
))
|
||||
}
|
||||
|
||||
args = cmdFlags.Args()
|
||||
if len(args) == 0 {
|
||||
ret.Paths = []string{"."}
|
||||
} else if args[0] == stdinArg {
|
||||
ret.List = false
|
||||
ret.Write = false
|
||||
} else {
|
||||
ret.Paths = args
|
||||
}
|
||||
|
||||
// we only parse but do not register the views flags since this command does not need it
|
||||
closer, moreDiags := ret.ViewOptions.Parse()
|
||||
diags = diags.Append(moreDiags)
|
||||
|
||||
return ret, closer, diags
|
||||
}
|
||||
119
internal/command/arguments/fmt_test.go
Normal file
119
internal/command/arguments/fmt_test.go
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package arguments
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
)
|
||||
|
||||
func TestParseFmt_basicValidation(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
want *Fmt
|
||||
}{
|
||||
"defaults": {
|
||||
nil,
|
||||
fmtArgsWithDefaults(nil),
|
||||
},
|
||||
"list": {
|
||||
[]string{"-list=false"},
|
||||
fmtArgsWithDefaults(func(v *Fmt) {
|
||||
v.List = false
|
||||
}),
|
||||
},
|
||||
"write": {
|
||||
[]string{"-write=false"},
|
||||
fmtArgsWithDefaults(func(v *Fmt) {
|
||||
v.Write = false
|
||||
}),
|
||||
},
|
||||
"diff": {
|
||||
[]string{"-diff"},
|
||||
fmtArgsWithDefaults(func(v *Fmt) {
|
||||
v.Diff = true
|
||||
}),
|
||||
},
|
||||
"diff with value": {
|
||||
[]string{"-diff=true"},
|
||||
fmtArgsWithDefaults(func(v *Fmt) {
|
||||
v.Diff = true
|
||||
}),
|
||||
},
|
||||
"check": {
|
||||
[]string{"-check"},
|
||||
fmtArgsWithDefaults(func(v *Fmt) {
|
||||
v.Check = true
|
||||
}),
|
||||
},
|
||||
"recursive": {
|
||||
[]string{"-recursive"},
|
||||
fmtArgsWithDefaults(func(v *Fmt) {
|
||||
v.Recursive = true
|
||||
}),
|
||||
},
|
||||
"file args": {
|
||||
[]string{"foo", "bar"},
|
||||
fmtArgsWithDefaults(func(v *Fmt) {
|
||||
v.Paths = []string{"foo", "bar"}
|
||||
}),
|
||||
},
|
||||
"args with stdin in front": {
|
||||
[]string{"-", "bar"},
|
||||
fmtArgsWithDefaults(func(v *Fmt) {
|
||||
v.Paths = nil
|
||||
v.List = false
|
||||
v.Write = false
|
||||
}),
|
||||
},
|
||||
"args with stdin not on the first index": {
|
||||
[]string{"foo", "-", "bar"},
|
||||
fmtArgsWithDefaults(func(v *Fmt) {
|
||||
v.Paths = []string{"foo", "-", "bar"}
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
cmpOpts := cmpopts.IgnoreUnexported(ViewOptions{})
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, closer, diags := ParseFmt(tc.args)
|
||||
defer closer()
|
||||
|
||||
if len(diags) > 0 {
|
||||
t.Fatalf("unexpected diags: %v", diags)
|
||||
}
|
||||
if diff := cmp.Diff(tc.want, got, cmpOpts); diff != "" {
|
||||
t.Errorf("unexpected result\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func fmtArgsWithDefaults(mutate func(v *Fmt)) *Fmt {
|
||||
ret := &Fmt{
|
||||
Paths: []string{"."},
|
||||
List: true,
|
||||
Write: true,
|
||||
Diff: false,
|
||||
Check: false,
|
||||
Recursive: false,
|
||||
ViewOptions: ViewOptions{
|
||||
jsonFlag: false,
|
||||
jsonIntoFlag: "",
|
||||
ViewType: ViewHuman,
|
||||
InputEnabled: false,
|
||||
JSONInto: nil,
|
||||
},
|
||||
}
|
||||
if mutate != nil {
|
||||
mutate(ret)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
|
@ -20,15 +20,12 @@ import (
|
|||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/hashicorp/hcl/v2/hclwrite"
|
||||
"github.com/mitchellh/cli"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/command/arguments"
|
||||
"github.com/opentofu/opentofu/internal/command/views"
|
||||
"github.com/opentofu/opentofu/internal/configs"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
const (
|
||||
stdinArg = "-"
|
||||
)
|
||||
|
||||
var (
|
||||
fmtSupportedExts = []string{
|
||||
".tf",
|
||||
|
|
@ -43,89 +40,87 @@ var (
|
|||
// files to a canonical format and style.
|
||||
type FmtCommand struct {
|
||||
Meta
|
||||
list bool
|
||||
write bool
|
||||
diff bool
|
||||
check bool
|
||||
recursive bool
|
||||
input io.Reader // STDIN if nil
|
||||
input io.Reader // STDIN if nil
|
||||
}
|
||||
|
||||
func (c *FmtCommand) Run(args []string) int {
|
||||
func (c *FmtCommand) Run(rawArgs []string) int {
|
||||
if c.input == nil {
|
||||
c.input = os.Stdin
|
||||
}
|
||||
|
||||
args = c.Meta.process(args)
|
||||
cmdFlags := c.Meta.defaultFlagSet("fmt")
|
||||
cmdFlags.BoolVar(&c.list, "list", true, "list")
|
||||
cmdFlags.BoolVar(&c.write, "write", true, "write")
|
||||
cmdFlags.BoolVar(&c.diff, "diff", false, "diff")
|
||||
cmdFlags.BoolVar(&c.check, "check", false, "check")
|
||||
cmdFlags.BoolVar(&c.recursive, "recursive", false, "recursive")
|
||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
|
||||
return 1
|
||||
}
|
||||
// new view
|
||||
common, rawArgs := arguments.ParseView(rawArgs)
|
||||
c.View.Configure(common)
|
||||
// Because the legacy UI was using println to show diagnostics and the new view is using, by default, print,
|
||||
// in order to keep functional parity, we setup the view to add a new line after each diagnostic.
|
||||
c.View.DiagsWithNewline()
|
||||
|
||||
args = cmdFlags.Args()
|
||||
// Propagate -no-color for legacy use of Ui. The remote backend and
|
||||
// cloud package use this; it should be removed when/if they are
|
||||
// migrated to views.
|
||||
c.Meta.color = !common.NoColor
|
||||
c.Meta.Color = c.Meta.color
|
||||
|
||||
var paths []string
|
||||
if len(args) == 0 {
|
||||
paths = []string{"."}
|
||||
} else if args[0] == stdinArg {
|
||||
c.list = false
|
||||
c.write = false
|
||||
} else {
|
||||
paths = args
|
||||
// Parse and validate flags
|
||||
args, closer, diags := arguments.ParseFmt(rawArgs)
|
||||
defer closer()
|
||||
|
||||
// Instantiate the view, even if there are flag errors, so that we render
|
||||
// diagnostics according to the desired view
|
||||
view := views.NewFmt(c.View)
|
||||
// ... and initialise the Meta.Ui to wrap Meta.View into a new implementation
|
||||
// that is able to print by using View abstraction and use the Meta.Ui
|
||||
// to ask for the user input.
|
||||
c.Meta.configureUiFromView(args.ViewOptions)
|
||||
|
||||
if diags.HasErrors() {
|
||||
view.Diagnostics(diags)
|
||||
return cli.RunResultHelp
|
||||
}
|
||||
|
||||
var output io.Writer
|
||||
list := c.list // preserve the original value of -list
|
||||
if c.check {
|
||||
list := args.List // preserve the original value of -list
|
||||
if args.Check {
|
||||
// set to true so we can use the list output to check
|
||||
// if the input needs formatting
|
||||
c.list = true
|
||||
c.write = false
|
||||
args.List = true
|
||||
args.Write = false
|
||||
output = &bytes.Buffer{}
|
||||
} else {
|
||||
output = &cli.UiWriter{Ui: c.Ui}
|
||||
output = view.UserOutputWriter()
|
||||
}
|
||||
|
||||
diags := c.fmt(paths, c.input, output)
|
||||
c.showDiagnostics(diags)
|
||||
diags = diags.Append(c.fmt(args.Paths, c.input, output, *args))
|
||||
view.Diagnostics(diags)
|
||||
if diags.HasErrors() {
|
||||
return 2
|
||||
}
|
||||
|
||||
if c.check {
|
||||
if args.Check {
|
||||
buf := output.(*bytes.Buffer)
|
||||
ok := buf.Len() == 0
|
||||
if list {
|
||||
if _, err := io.Copy(&cli.UiWriter{Ui: c.Ui}, buf); err != nil {
|
||||
if _, err := io.Copy(view.UserOutputWriter(), buf); err != nil {
|
||||
log.Printf("[ERROR] Unable to write UI output: %s", err)
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
return 0
|
||||
} else {
|
||||
if !ok {
|
||||
return 3
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *FmtCommand) fmt(paths []string, stdin io.Reader, stdout io.Writer) tfdiags.Diagnostics {
|
||||
func (c *FmtCommand) fmt(paths []string, stdin io.Reader, stdout io.Writer, args arguments.Fmt) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
if len(paths) == 0 { // Assuming stdin, then.
|
||||
if c.write {
|
||||
if args.Write {
|
||||
diags = diags.Append(fmt.Errorf("Option -write cannot be used when reading from stdin"))
|
||||
return diags
|
||||
}
|
||||
fileDiags := c.processFile("<stdin>", stdin, stdout, true)
|
||||
fileDiags := c.processFile("<stdin>", stdin, stdout, args)
|
||||
diags = diags.Append(fileDiags)
|
||||
return diags
|
||||
}
|
||||
|
|
@ -138,7 +133,7 @@ func (c *FmtCommand) fmt(paths []string, stdin io.Reader, stdout io.Writer) tfdi
|
|||
return diags
|
||||
}
|
||||
if info.IsDir() {
|
||||
dirDiags := c.processDir(path, stdout)
|
||||
dirDiags := c.processDir(path, stdout, args)
|
||||
diags = diags.Append(dirDiags)
|
||||
} else {
|
||||
fmtd := false
|
||||
|
|
@ -152,9 +147,9 @@ func (c *FmtCommand) fmt(paths []string, stdin io.Reader, stdout io.Writer) tfdi
|
|||
continue
|
||||
}
|
||||
|
||||
fileDiags := c.processFile(c.Meta.WorkingDir.NormalizePath(path), f, stdout, false)
|
||||
fileDiags := c.processFile(c.Meta.WorkingDir.NormalizePath(path), f, stdout, args)
|
||||
diags = diags.Append(fileDiags)
|
||||
f.Close()
|
||||
_ = f.Close()
|
||||
|
||||
// Take note that we processed the file.
|
||||
fmtd = true
|
||||
|
|
@ -174,7 +169,7 @@ func (c *FmtCommand) fmt(paths []string, stdin io.Reader, stdout io.Writer) tfdi
|
|||
return diags
|
||||
}
|
||||
|
||||
func (c *FmtCommand) processFile(path string, r io.Reader, w io.Writer, isStdout bool) tfdiags.Diagnostics {
|
||||
func (c *FmtCommand) processFile(path string, r io.Reader, w io.Writer, args arguments.Fmt) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
log.Printf("[TRACE] tofu fmt: Formatting %s", path)
|
||||
|
|
@ -202,17 +197,17 @@ func (c *FmtCommand) processFile(path string, r io.Reader, w io.Writer, isStdout
|
|||
|
||||
if !bytes.Equal(src, result) {
|
||||
// Something was changed
|
||||
if c.list {
|
||||
fmt.Fprintln(w, path)
|
||||
if args.List {
|
||||
_, _ = fmt.Fprintln(w, path)
|
||||
}
|
||||
if c.write {
|
||||
if args.Write {
|
||||
err := os.WriteFile(path, result, 0644)
|
||||
if err != nil {
|
||||
diags = diags.Append(fmt.Errorf("Failed to write %s", path))
|
||||
return diags
|
||||
}
|
||||
}
|
||||
if c.diff {
|
||||
if args.Diff {
|
||||
diff, err := bytesDiff(src, result, path)
|
||||
if err != nil {
|
||||
diags = diags.Append(fmt.Errorf("Failed to generate diff for %s: %w", path, err))
|
||||
|
|
@ -224,7 +219,7 @@ func (c *FmtCommand) processFile(path string, r io.Reader, w io.Writer, isStdout
|
|||
}
|
||||
}
|
||||
|
||||
if !c.list && !c.write && !c.diff {
|
||||
if !args.List && !args.Write && !args.Diff {
|
||||
_, err = w.Write(result)
|
||||
if err != nil {
|
||||
diags = diags.Append(fmt.Errorf("Failed to write result"))
|
||||
|
|
@ -234,7 +229,7 @@ func (c *FmtCommand) processFile(path string, r io.Reader, w io.Writer, isStdout
|
|||
return diags
|
||||
}
|
||||
|
||||
func (c *FmtCommand) processDir(path string, stdout io.Writer) tfdiags.Diagnostics {
|
||||
func (c *FmtCommand) processDir(path string, stdout io.Writer, args arguments.Fmt) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
log.Printf("[TRACE] tofu fmt: looking for files in %s", path)
|
||||
|
|
@ -259,8 +254,8 @@ func (c *FmtCommand) processDir(path string, stdout io.Writer) tfdiags.Diagnosti
|
|||
}
|
||||
subPath := filepath.Join(path, name)
|
||||
if info.IsDir() {
|
||||
if c.recursive {
|
||||
subDiags := c.processDir(subPath, stdout)
|
||||
if args.Recursive {
|
||||
subDiags := c.processDir(subPath, stdout, args)
|
||||
diags = diags.Append(subDiags)
|
||||
}
|
||||
|
||||
|
|
@ -280,9 +275,9 @@ func (c *FmtCommand) processDir(path string, stdout io.Writer) tfdiags.Diagnosti
|
|||
continue
|
||||
}
|
||||
|
||||
fileDiags := c.processFile(c.Meta.WorkingDir.NormalizePath(subPath), f, stdout, false)
|
||||
fileDiags := c.processFile(c.Meta.WorkingDir.NormalizePath(subPath), f, stdout, args)
|
||||
diags = diags.Append(fileDiags)
|
||||
f.Close()
|
||||
_ = f.Close()
|
||||
|
||||
// Don't need to check the remaining extensions.
|
||||
break
|
||||
|
|
@ -297,7 +292,7 @@ func (c *FmtCommand) processDir(path string, stdout io.Writer) tfdiags.Diagnosti
|
|||
// is selected (directly or indirectly) on the command line.
|
||||
func (c *FmtCommand) formatSourceCode(src []byte, filename string) []byte {
|
||||
f, diags := hclwrite.ParseConfig(src, filename, hcl.InitialPos)
|
||||
if diags.HasErrors() {
|
||||
if diags.HasErrors() || f == nil { // ensure that f is not nil to avoid possible nil pointer dereference later
|
||||
// It would be weird to get here because the caller should already have
|
||||
// checked for syntax errors and returned them. We'll just do nothing
|
||||
// in this case, returning the input exactly as given.
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/opentofu/opentofu/internal/command/workdir"
|
||||
)
|
||||
|
||||
|
|
@ -59,17 +58,19 @@ func TestFmt_TestFiles(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
view, done := testView(t)
|
||||
c := &FmtCommand{
|
||||
Meta: Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
args := []string{gotFile}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("fmt command was unsuccessful:\n%s", ui.ErrorWriter.String())
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
if code != 0 {
|
||||
t.Fatalf("fmt command was unsuccessful:\n%s", output.Stderr())
|
||||
}
|
||||
|
||||
got, err := os.ReadFile(gotFile)
|
||||
|
|
@ -124,17 +125,19 @@ func TestFmt(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
view, done := testView(t)
|
||||
c := &FmtCommand{
|
||||
Meta: Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
args := []string{gotFile}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("fmt command was unsuccessful:\n%s", ui.ErrorWriter.String())
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
if code != 0 {
|
||||
t.Fatalf("fmt command was unsuccessful:\n%s", output.Stderr())
|
||||
}
|
||||
|
||||
got, err := os.ReadFile(gotFile)
|
||||
|
|
@ -152,23 +155,25 @@ func TestFmt(t *testing.T) {
|
|||
func TestFmt_nonexist(t *testing.T) {
|
||||
tempDir := fmtFixtureWriteDir(t)
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
view, done := testView(t)
|
||||
c := &FmtCommand{
|
||||
Meta: Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
missingDir := filepath.Join(tempDir, "doesnotexist")
|
||||
args := []string{missingDir}
|
||||
if code := c.Run(args); code != 2 {
|
||||
t.Fatalf("wrong exit code. errors: \n%s", ui.ErrorWriter.String())
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
if code != 2 {
|
||||
t.Fatalf("wrong exit code. got %d. errors: \n%s", code, output.Stderr())
|
||||
}
|
||||
|
||||
expected := "No file or directory at"
|
||||
if actual := ui.ErrorWriter.String(); !strings.Contains(actual, expected) {
|
||||
if actual := output.Stderr(); !strings.Contains(actual, expected) {
|
||||
t.Fatalf("expected:\n%s\n\nto include: %q", actual, expected)
|
||||
}
|
||||
}
|
||||
|
|
@ -185,22 +190,24 @@ a = 1 +
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
view, done := testView(t)
|
||||
c := &FmtCommand{
|
||||
Meta: Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{tempDir}
|
||||
if code := c.Run(args); code != 2 {
|
||||
t.Fatalf("wrong exit code. errors: \n%s", ui.ErrorWriter.String())
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
if code != 2 {
|
||||
t.Fatalf("wrong exit code. got %d. errors: \n%s", code, output.Stderr())
|
||||
}
|
||||
|
||||
expected := "Invalid expression"
|
||||
if actual := ui.ErrorWriter.String(); !strings.Contains(actual, expected) {
|
||||
if actual := output.Stderr(); !strings.Contains(actual, expected) {
|
||||
t.Fatalf("expected:\n%s\n\nto include: %q", actual, expected)
|
||||
}
|
||||
}
|
||||
|
|
@ -215,18 +222,20 @@ func TestFmt_snippetInError(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
view, done := testView(t)
|
||||
c := &FmtCommand{
|
||||
Meta: Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{tempDir}
|
||||
if code := c.Run(args); code != 2 {
|
||||
t.Fatalf("wrong exit code. errors: \n%s", ui.ErrorWriter.String())
|
||||
args := []string{"-no-color", tempDir}
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
if code != 2 {
|
||||
t.Fatalf("wrong exit code. got %d. errors: \n%s", code, output.Stderr())
|
||||
}
|
||||
|
||||
substrings := []string{
|
||||
|
|
@ -235,7 +244,7 @@ func TestFmt_snippetInError(t *testing.T) {
|
|||
`1: terraform {backend "s3" {}}`,
|
||||
}
|
||||
for _, substring := range substrings {
|
||||
if actual := ui.ErrorWriter.String(); !strings.Contains(actual, substring) {
|
||||
if actual := output.Stderr(); !strings.Contains(actual, substring) {
|
||||
t.Errorf("expected:\n%s\n\nto include: %q", actual, substring)
|
||||
}
|
||||
}
|
||||
|
|
@ -251,12 +260,12 @@ func TestFmt_manyArgs(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
view, done := testView(t)
|
||||
c := &FmtCommand{
|
||||
Meta: Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -264,11 +273,13 @@ func TestFmt_manyArgs(t *testing.T) {
|
|||
filepath.Join(tempDir, "main.tf"),
|
||||
filepath.Join(tempDir, "second.tf"),
|
||||
}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("wrong exit code. errors: \n%s", ui.ErrorWriter.String())
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
if code != 0 {
|
||||
t.Fatalf("wrong exit code. got %d. errors: \n%s", code, output.Stderr())
|
||||
}
|
||||
|
||||
got, err := filepath.Abs(strings.TrimSpace(ui.OutputWriter.String()))
|
||||
got, err := filepath.Abs(strings.TrimSpace(output.Stdout()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -283,27 +294,29 @@ func TestFmt_workingDirectory(t *testing.T) {
|
|||
tempDir := fmtFixtureWriteDir(t)
|
||||
t.Chdir(tempDir)
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
view, done := testView(t)
|
||||
c := &FmtCommand{
|
||||
Meta: Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("wrong exit code. errors: \n%s", ui.ErrorWriter.String())
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
if code != 0 {
|
||||
t.Fatalf("wrong exit code. got %d. errors: \n%s", code, output.Stderr())
|
||||
}
|
||||
|
||||
output := strings.Split(strings.TrimSpace(ui.OutputWriter.String()), "\n")
|
||||
stdout := strings.Split(strings.TrimSpace(output.Stdout()), "\n")
|
||||
|
||||
// Consistent order
|
||||
sort.Strings(output)
|
||||
sort.Strings(stdout)
|
||||
|
||||
for i, expected := range []string{fmtFixture.filename, fmtFixture.altFilename} {
|
||||
actual := output[i]
|
||||
actual := stdout[i]
|
||||
if actual != expected {
|
||||
t.Fatalf("got: %q\nexpected: %q", actual, expected)
|
||||
}
|
||||
|
|
@ -313,27 +326,29 @@ func TestFmt_workingDirectory(t *testing.T) {
|
|||
func TestFmt_directoryArg(t *testing.T) {
|
||||
tempDir := fmtFixtureWriteDir(t)
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
view, done := testView(t)
|
||||
c := &FmtCommand{
|
||||
Meta: Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{tempDir}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("wrong exit code. errors: \n%s", ui.ErrorWriter.String())
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
if code != 0 {
|
||||
t.Fatalf("wrong exit code. got %d. errors: \n%s", code, output.Stderr())
|
||||
}
|
||||
|
||||
output := strings.Split(strings.TrimSpace(ui.OutputWriter.String()), "\n")
|
||||
stdout := strings.Split(strings.TrimSpace(output.Stdout()), "\n")
|
||||
|
||||
// Consistent order
|
||||
sort.Strings(output)
|
||||
sort.Strings(stdout)
|
||||
|
||||
for i, check := range []string{fmtFixture.filename, fmtFixture.altFilename} {
|
||||
got, err := filepath.Abs(output[i])
|
||||
got, err := filepath.Abs(stdout[i])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -348,21 +363,23 @@ func TestFmt_directoryArg(t *testing.T) {
|
|||
func TestFmt_fileArg(t *testing.T) {
|
||||
tempDir := fmtFixtureWriteDir(t)
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
view, done := testView(t)
|
||||
c := &FmtCommand{
|
||||
Meta: Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{filepath.Join(tempDir, fmtFixture.filename)}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("wrong exit code. errors: \n%s", ui.ErrorWriter.String())
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
if code != 0 {
|
||||
t.Fatalf("wrong exit code. got %d. errors: \n%s", code, output.Stderr())
|
||||
}
|
||||
|
||||
got, err := filepath.Abs(strings.TrimSpace(ui.OutputWriter.String()))
|
||||
got, err := filepath.Abs(strings.TrimSpace(output.Stdout()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -377,23 +394,25 @@ func TestFmt_stdinArg(t *testing.T) {
|
|||
input := new(bytes.Buffer)
|
||||
input.Write(fmtFixture.input)
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
view, done := testView(t)
|
||||
c := &FmtCommand{
|
||||
Meta: Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
input: input,
|
||||
}
|
||||
|
||||
args := []string{"-"}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("wrong exit code. errors: \n%s", ui.ErrorWriter.String())
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
if code != 0 {
|
||||
t.Fatalf("wrong exit code. got %d. errors: \n%s", code, output.Stderr())
|
||||
}
|
||||
|
||||
expected := fmtFixture.golden
|
||||
if actual := ui.OutputWriter.Bytes(); !bytes.Equal(actual, expected) {
|
||||
if actual := []byte(output.Stdout()); !bytes.Equal(actual, expected) {
|
||||
t.Fatalf("got: %q\nexpected: %q", actual, expected)
|
||||
}
|
||||
}
|
||||
|
|
@ -401,12 +420,12 @@ func TestFmt_stdinArg(t *testing.T) {
|
|||
func TestFmt_nonDefaultOptions(t *testing.T) {
|
||||
tempDir := fmtFixtureWriteDir(t)
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
view, done := testView(t)
|
||||
c := &FmtCommand{
|
||||
Meta: Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -416,12 +435,14 @@ func TestFmt_nonDefaultOptions(t *testing.T) {
|
|||
"-diff",
|
||||
tempDir,
|
||||
}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("wrong exit code. errors: \n%s", ui.ErrorWriter.String())
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
if code != 0 {
|
||||
t.Fatalf("wrong exit code. got %d. errors: \n%s", code, output.Stderr())
|
||||
}
|
||||
|
||||
expected := fmt.Sprintf("-%s+%s", fmtFixture.input, fmtFixture.golden)
|
||||
if actual := ui.OutputWriter.String(); !strings.Contains(actual, expected) {
|
||||
if actual := output.Stdout(); !strings.Contains(actual, expected) {
|
||||
t.Fatalf("expected:\n%s\n\nto include: %q", actual, expected)
|
||||
}
|
||||
}
|
||||
|
|
@ -429,12 +450,12 @@ func TestFmt_nonDefaultOptions(t *testing.T) {
|
|||
func TestFmt_check(t *testing.T) {
|
||||
tempDir := fmtFixtureWriteDir(t)
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
view, done := testView(t)
|
||||
c := &FmtCommand{
|
||||
Meta: Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -442,7 +463,9 @@ func TestFmt_check(t *testing.T) {
|
|||
"-check",
|
||||
tempDir,
|
||||
}
|
||||
if code := c.Run(args); code != 3 {
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
if code != 3 {
|
||||
t.Fatalf("wrong exit code. expected 3")
|
||||
}
|
||||
|
||||
|
|
@ -450,7 +473,7 @@ func TestFmt_check(t *testing.T) {
|
|||
// dir so that we're comparing against a relative-ized (normalized) path
|
||||
tempDir = c.Meta.WorkingDir.NormalizePath(tempDir)
|
||||
|
||||
if actual := ui.OutputWriter.String(); !strings.Contains(actual, tempDir) {
|
||||
if actual := output.Stdout(); !strings.Contains(actual, tempDir) {
|
||||
t.Fatalf("expected:\n%s\n\nto include: %q", actual, tempDir)
|
||||
}
|
||||
}
|
||||
|
|
@ -459,12 +482,12 @@ func TestFmt_checkStdin(t *testing.T) {
|
|||
input := new(bytes.Buffer)
|
||||
input.Write(fmtFixture.input)
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
view, done := testView(t)
|
||||
c := &FmtCommand{
|
||||
Meta: Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
input: input,
|
||||
}
|
||||
|
|
@ -473,12 +496,15 @@ func TestFmt_checkStdin(t *testing.T) {
|
|||
"-check",
|
||||
"-",
|
||||
}
|
||||
if code := c.Run(args); code != 3 {
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
if code != 3 {
|
||||
t.Fatalf("wrong exit code. expected 3, got %d", code)
|
||||
}
|
||||
|
||||
if ui.OutputWriter != nil {
|
||||
t.Fatalf("expected no output, got: %q", ui.OutputWriter.String())
|
||||
stdout := output.Stdout()
|
||||
if len(stdout) > 0 {
|
||||
t.Fatalf("expected no output, got: %q", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
38
internal/command/views/fmt.go
Normal file
38
internal/command/views/fmt.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package views
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
type Fmt interface {
|
||||
Diagnostics(diags tfdiags.Diagnostics)
|
||||
UserOutputWriter() io.Writer
|
||||
}
|
||||
|
||||
// NewFmt returns an initialized Fmt implementation.
|
||||
func NewFmt(view *View) Fmt {
|
||||
return &FmtHuman{view: view}
|
||||
}
|
||||
|
||||
type FmtHuman struct {
|
||||
view *View
|
||||
}
|
||||
|
||||
var _ Fmt = (*FmtHuman)(nil)
|
||||
|
||||
func (v *FmtHuman) Diagnostics(diags tfdiags.Diagnostics) {
|
||||
v.view.Diagnostics(diags)
|
||||
}
|
||||
|
||||
// UserOutputWriter returns a [io.Writer] that uses the [FmtHuman.view] as a proxy to write
|
||||
// the user facing information during formatting.
|
||||
func (v *FmtHuman) UserOutputWriter() io.Writer {
|
||||
return v.view.streams.Stdout.File
|
||||
}
|
||||
84
internal/command/views/fmt_test.go
Normal file
84
internal/command/views/fmt_test.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package views
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
func TestFmtViews(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
viewCall func(get Fmt)
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
}{
|
||||
// Diagnostics
|
||||
"warning": {
|
||||
viewCall: func(v Fmt) {
|
||||
diags := tfdiags.Diagnostics{
|
||||
tfdiags.Sourceless(tfdiags.Warning, "A warning occurred", "foo bar"),
|
||||
}
|
||||
v.Diagnostics(diags)
|
||||
},
|
||||
wantStdout: withNewline("\nWarning: A warning occurred\n\nfoo bar"),
|
||||
wantStderr: "",
|
||||
},
|
||||
"error": {
|
||||
viewCall: func(v Fmt) {
|
||||
diags := tfdiags.Diagnostics{
|
||||
tfdiags.Sourceless(tfdiags.Error, "An error occurred", "foo bar"),
|
||||
}
|
||||
v.Diagnostics(diags)
|
||||
},
|
||||
wantStdout: "",
|
||||
wantStderr: withNewline("\nError: An error occurred\n\nfoo bar"),
|
||||
},
|
||||
"multiple_diagnostics": {
|
||||
viewCall: func(v Fmt) {
|
||||
diags := tfdiags.Diagnostics{
|
||||
tfdiags.Sourceless(tfdiags.Warning, "A warning", "foo bar warning"),
|
||||
tfdiags.Sourceless(tfdiags.Error, "An error", "foo bar error"),
|
||||
}
|
||||
v.Diagnostics(diags)
|
||||
},
|
||||
wantStdout: withNewline("\nWarning: A warning\n\nfoo bar warning"),
|
||||
wantStderr: withNewline("\nError: An error\n\nfoo bar error"),
|
||||
},
|
||||
"content writer": {
|
||||
viewCall: func(v Fmt) {
|
||||
in := `resource foo_instance foo {
|
||||
instance_type = "${var.instance_type}"
|
||||
}
|
||||
`
|
||||
_, _ = v.UserOutputWriter().Write([]byte(in))
|
||||
},
|
||||
// The new line at the end is from the printer. The one in the input has been trimmed out
|
||||
wantStdout: "resource foo_instance foo {\n instance_type = \"${var.instance_type}\"\n}\n",
|
||||
wantStderr: "",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
testFmtHuman(t, tc.viewCall, tc.wantStdout, tc.wantStderr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testFmtHuman(t *testing.T, call func(get Fmt), wantStdout, wantStderr string) {
|
||||
view, done := testView(t)
|
||||
v := NewFmt(view)
|
||||
call(v)
|
||||
output := done(t)
|
||||
if diff := cmp.Diff(wantStderr, output.Stderr()); diff != "" {
|
||||
t.Errorf("invalid stderr (-want, +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(wantStdout, output.Stdout()); diff != "" {
|
||||
t.Errorf("invalid stdout (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue