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:
Andrei Ciobanu 2026-03-05 14:47:37 +02:00 committed by GitHub
parent 383d6b3595
commit 5fcfb23eb5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 474 additions and 132 deletions

View 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
}

View 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
}

View file

@ -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.

View file

@ -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)
}
}

View 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
}

View 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)
}
}