From 9b841552654109b63a5098d30fded9996323903a Mon Sep 17 00:00:00 2001 From: Brian McGee Date: Mon, 25 Dec 2023 12:26:18 +0000 Subject: [PATCH] feat: allow specifying formatters in cli Closes #9 --- go.mod | 1 + go.sum | 4 ++ internal/cli/cli.go | 17 +++--- internal/cli/format.go | 30 +++++++++- internal/cli/format_test.go | 113 +++++++++++++++++++++++++++++++++--- 5 files changed, 148 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 63ab324..d37faea 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/charmbracelet/log v0.3.1 github.com/gobwas/glob v0.2.3 github.com/juju/errors v1.0.0 + github.com/otiai10/copy v1.14.0 github.com/stretchr/testify v1.8.4 github.com/vmihailenco/msgpack/v5 v5.4.1 github.com/ztrue/shutdown v0.1.1 diff --git a/go.sum b/go.sum index 8346b8b..aec11ba 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,10 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= +github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= +github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= +github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 6303029..4d70b71 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -1,20 +1,23 @@ package cli -import "github.com/charmbracelet/log" +import ( + "github.com/charmbracelet/log" +) var Cli = Options{} type Options struct { - AllowMissingFormatter bool `default:"false" help:"Do not exit with error if a configured formatter is missing"` - ClearCache bool `short:"c" help:"Reset the evaluation cache. Use in case the cache is not precise enough"` - ConfigFile string `type:"existingfile" default:"./treefmt.toml"` - TreeRoot string `type:"existingdir" default:"."` - Verbosity int `name:"verbose" short:"v" type:"counter" default:"0" env:"LOG_LEVEL" help:"Set the verbosity of logs e.g. -vv"` + AllowMissingFormatter bool `default:"false" help:"Do not exit with error if a configured formatter is missing"` + ClearCache bool `short:"c" help:"Reset the evaluation cache. Use in case the cache is not precise enough"` + ConfigFile string `type:"existingfile" default:"./treefmt.toml"` + Formatters []string `help:"Specify formatters to apply. Defaults to all formatters"` + TreeRoot string `type:"existingdir" default:"."` + Verbosity int `name:"verbose" short:"v" type:"counter" default:"0" env:"LOG_LEVEL" help:"Set the verbosity of logs e.g. -vv"` Format Format `cmd:"" default:"."` } -func (c *Options) ConfigureLogger() { +func (c *Options) Configure() { log.SetReportTimestamp(false) if c.Verbosity == 0 { diff --git a/internal/cli/format.go b/internal/cli/format.go index 009d8da..2d1b890 100644 --- a/internal/cli/format.go +++ b/internal/cli/format.go @@ -19,7 +19,7 @@ type Format struct{} func (f *Format) Run() error { start := time.Now() - Cli.ConfigureLogger() + Cli.Configure() l := log.WithPrefix("format") @@ -42,8 +42,34 @@ func (f *Format) Run() error { return errors.Annotate(err, "failed to read config file") } + // create optional formatter filter set + formatterSet := make(map[string]bool) + for _, name := range Cli.Formatters { + _, ok := cfg.Formatters[name] + if !ok { + return errors.Errorf("formatter not found in config: %v", name) + } + formatterSet[name] = true + } + + includeFormatter := func(name string) bool { + if len(formatterSet) == 0 { + return true + } else { + _, include := formatterSet[name] + return include + } + } + // init formatters for name, formatter := range cfg.Formatters { + if !includeFormatter(name) { + // remove this formatter + delete(cfg.Formatters, name) + l.Debugf("formatter %v is not in formatter list %v, skipping", name, Cli.Formatters) + continue + } + err = formatter.Init(name) if err == format.ErrFormatterNotFound && Cli.AllowMissingFormatter { l.Debugf("formatter not found: %v", name) @@ -126,7 +152,7 @@ func (f *Format) Run() error { } changes += count - println(fmt.Sprintf("%v files changed in %v", changes, time.Now().Sub(start))) + fmt.Printf("%v files changed in %v", changes, time.Now().Sub(start)) return nil }) diff --git a/internal/cli/format_test.go b/internal/cli/format_test.go index 44f0737..0c0d356 100644 --- a/internal/cli/format_test.go +++ b/internal/cli/format_test.go @@ -1,12 +1,16 @@ package cli import ( + "io" "os" + "path/filepath" "testing" "git.numtide.com/numtide/treefmt/internal/format" "github.com/BurntSushi/toml" "github.com/alecthomas/kong" + "github.com/juju/errors" + cp "github.com/otiai10/copy" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -37,10 +41,56 @@ func newKong(t *testing.T, cli interface{}, options ...kong.Option) *kong.Kong { return parser } -func newCli(t *testing.T, args ...string) (*kong.Context, error) { +func tempFile(t *testing.T, path string) *os.File { t.Helper() + file, err := os.Create(path) + if err != nil { + t.Fatalf("failed to create temporary file: %v", err) + } + return file +} + +func cmd(t *testing.T, args ...string) ([]byte, error) { + t.Helper() + + // create a new kong context p := newKong(t, &Cli) - return p.Parse(args) + ctx, err := p.Parse(args) + if err != nil { + return nil, err + } + + tempDir := t.TempDir() + tempOut := tempFile(t, filepath.Join(tempDir, "combined_output")) + + // capture standard outputs before swapping them + stdout := os.Stdout + stderr := os.Stderr + + // swap them temporarily + os.Stdout = tempOut + os.Stderr = tempOut + + // run the command + if err = ctx.Run(); err != nil { + return nil, err + } + + // reset and read the temporary output + if _, err = tempOut.Seek(0, 0); err != nil { + return nil, errors.Annotate(err, "failed to reset temp output for reading") + } + + out, err := io.ReadAll(tempOut) + if err != nil { + return nil, errors.Annotate(err, "failed to read temp output") + } + + // swap outputs back + os.Stdout = stdout + os.Stderr = stderr + + return out, nil } func TestAllowMissingFormatter(t *testing.T) { @@ -57,12 +107,59 @@ func TestAllowMissingFormatter(t *testing.T) { }, }) - ctx, err := newCli(t, "--config-file", configPath, "--tree-root", tempDir) - as.NoError(err) - as.Error(ctx.Run(), format.ErrFormatterNotFound) + _, err := cmd(t, "--config-file", configPath, "--tree-root", tempDir) + as.ErrorIs(err, format.ErrFormatterNotFound) - ctx, err = newCli(t, "--config-file", configPath, "--tree-root", tempDir, "--allow-missing-formatter") + _, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "--allow-missing-formatter") as.NoError(err) - - as.NoError(ctx.Run()) +} + +func TestSpecifyingFormatters(t *testing.T) { + as := require.New(t) + + tempDir := t.TempDir() + configPath := tempDir + "/treefmt.toml" + + as.NoError(cp.Copy("../../test/examples", tempDir), "failed to copy test data to temp dir") + + writeConfig(t, configPath, format.Config{ + Formatters: map[string]*format.Formatter{ + "elm": { + Command: "echo", + Includes: []string{"*.elm"}, + }, + "nix": { + Command: "echo", + Includes: []string{"*.nix"}, + }, + "ruby": { + Command: "echo", + Includes: []string{"*.rb"}, + }, + }, + }) + + out, err := cmd(t, "--clear-cache", "--config-file", configPath, "--tree-root", tempDir) + as.NoError(err) + as.Contains(string(out), "3 files changed") + + out, err = cmd(t, "--clear-cache", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "elm,nix") + as.NoError(err) + as.Contains(string(out), "2 files changed") + + out, err = cmd(t, "--clear-cache", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "ruby,nix") + as.NoError(err) + as.Contains(string(out), "2 files changed") + + out, err = cmd(t, "--clear-cache", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "nix") + as.NoError(err) + as.Contains(string(out), "1 files changed") + + // test bad names + + out, err = cmd(t, "--clear-cache", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "foo") + as.Errorf(err, "formatter not found in config: foo") + + out, err = cmd(t, "--clear-cache", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "bar,foo") + as.Errorf(err, "formatter not found in config: bar") }