diff --git a/internal/command/arguments/fmt.go b/internal/command/arguments/fmt.go new file mode 100644 index 0000000000..3eeb19e82b --- /dev/null +++ b/internal/command/arguments/fmt.go @@ -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 +} diff --git a/internal/command/arguments/fmt_test.go b/internal/command/arguments/fmt_test.go new file mode 100644 index 0000000000..92645768fa --- /dev/null +++ b/internal/command/arguments/fmt_test.go @@ -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 +} diff --git a/internal/command/fmt.go b/internal/command/fmt.go index 5ce138e07e..5451f323e8 100644 --- a/internal/command/fmt.go +++ b/internal/command/fmt.go @@ -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, stdout, true) + fileDiags := c.processFile("", 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. diff --git a/internal/command/fmt_test.go b/internal/command/fmt_test.go index 0aeebc3e0f..c2075ecdcb 100644 --- a/internal/command/fmt_test.go +++ b/internal/command/fmt_test.go @@ -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) } } diff --git a/internal/command/views/fmt.go b/internal/command/views/fmt.go new file mode 100644 index 0000000000..d10dc288ae --- /dev/null +++ b/internal/command/views/fmt.go @@ -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 +} diff --git a/internal/command/views/fmt_test.go b/internal/command/views/fmt_test.go new file mode 100644 index 0000000000..32fe26e5ff --- /dev/null +++ b/internal/command/views/fmt_test.go @@ -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) + } +}