Skip to content

Commit

Permalink
Merge pull request #303 from numtide/feat/show-unmatched
Browse files Browse the repository at this point in the history
--on-unwatched
  • Loading branch information
zimbatm authored May 29, 2024
2 parents 53b0dc5 + bbe50fb commit 7afdc7a
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 32 deletions.
4 changes: 3 additions & 1 deletion cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ type Format struct {
Version bool `name:"version" short:"V" help:"Print version."`
Init bool `name:"init" short:"i" help:"Create a new treefmt.toml."`

OnUnmatched log.Level `name:"on-unmatched" short:"u" default:"warn" help:"Log paths that did not match any formatters at the specified log level, with fatal exiting the process with an error. Possible values are <debug|info|warn|error|fatal>."`

Paths []string `name:"paths" arg:"" type:"path" optional:"" help:"Paths to format. Defaults to formatting the whole tree."`
Stdin bool `help:"Format the context passed in via stdin."`

CpuProfile string `optional:"" help:"The file into which a cpu profile will be written."`
}

func ConfigureLogging() {
func configureLogging() {
log.SetReportTimestamp(false)

if Cli.Verbosity == 0 {
Expand Down
9 changes: 7 additions & 2 deletions cli/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ var (
)

func (f *Format) Run() (err error) {
// set log level and other options
configureLogging()

// cpu profiling
if Cli.CpuProfile != "" {
cpuProfile, err := os.Create(Cli.CpuProfile)
Expand Down Expand Up @@ -355,8 +358,10 @@ func applyFormatters(ctx context.Context) func() error {
}

if len(matches) == 0 {
// no match, so we send it direct to the processed channel
log.Debugf("no match found: %s", file.Path)
if Cli.OnUnmatched == log.FatalLevel {
return fmt.Errorf("no formatter for path: %s", file.Path)
}
log.Logf(Cli.OnUnmatched, "no formatter for path: %s", file.Path)
processedCh <- file
} else {
// record the match
Expand Down
117 changes: 92 additions & 25 deletions cli/format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ package cli

import (
"bufio"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"testing"

config2 "git.numtide.com/numtide/treefmt/config"
"git.numtide.com/numtide/treefmt/config"
"git.numtide.com/numtide/treefmt/format"
"git.numtide.com/numtide/treefmt/test"

Expand All @@ -21,6 +22,63 @@ import (
"github.com/stretchr/testify/require"
)

func TestOnUnmatched(t *testing.T) {
as := require.New(t)

// capture current cwd, so we can replace it after the test is finished
cwd, err := os.Getwd()
as.NoError(err)

t.Cleanup(func() {
// return to the previous working directory
as.NoError(os.Chdir(cwd))
})

tempDir := test.TempExamples(t)

paths := []string{
"go/go.mod",
"haskell/haskell.cabal",
"haskell/treefmt.toml",
"html/scripts/.gitkeep",
"nixpkgs.toml",
"python/requirements.txt",
"rust/Cargo.toml",
"touch.toml",
"treefmt.toml",
}

out, err := cmd(t, "-C", tempDir, "--allow-missing-formatter", "--on-unmatched", "fatal")
as.ErrorContains(err, fmt.Sprintf("no formatter for path: %s/%s", tempDir, paths[0]))

checkOutput := func(level string, output []byte) {
for _, p := range paths {
as.Contains(string(output), fmt.Sprintf("%s format: no formatter for path: %s/%s", level, tempDir, p))
}
}

// default is warn
out, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "-c")
as.NoError(err)
checkOutput("WARN", out)

out, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "-c", "--on-unmatched", "warn")
as.NoError(err)
checkOutput("WARN", out)

out, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "-c", "-u", "error")
as.NoError(err)
checkOutput("ERRO", out)

out, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "-c", "-v", "--on-unmatched", "info")
as.NoError(err)
checkOutput("INFO", out)

out, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "-c", "-vv", "-u", "debug")
as.NoError(err)
checkOutput("DEBU", out)
}

func TestCpuProfile(t *testing.T) {
as := require.New(t)
tempDir := test.TempExamples(t)
Expand All @@ -47,8 +105,8 @@ func TestAllowMissingFormatter(t *testing.T) {
tempDir := test.TempExamples(t)
configPath := tempDir + "/treefmt.toml"

test.WriteConfig(t, configPath, config2.Config{
Formatters: map[string]*config2.Formatter{
test.WriteConfig(t, configPath, config.Config{
Formatters: map[string]*config.Formatter{
"foo-fmt": {
Command: "foo-fmt",
},
Expand All @@ -65,8 +123,8 @@ func TestAllowMissingFormatter(t *testing.T) {
func TestSpecifyingFormatters(t *testing.T) {
as := require.New(t)

cfg := config2.Config{
Formatters: map[string]*config2.Formatter{
cfg := config.Config{
Formatters: map[string]*config.Formatter{
"elm": {
Command: "touch",
Options: []string{"-m"},
Expand Down Expand Up @@ -131,8 +189,8 @@ func TestIncludesAndExcludes(t *testing.T) {
configPath := tempDir + "/touch.toml"

// test without any excludes
cfg := config2.Config{
Formatters: map[string]*config2.Formatter{
cfg := config.Config{
Formatters: map[string]*config.Formatter{
"echo": {
Command: "echo",
Includes: []string{"*"},
Expand Down Expand Up @@ -203,8 +261,8 @@ func TestCache(t *testing.T) {
configPath := tempDir + "/touch.toml"

// test without any excludes
cfg := config2.Config{
Formatters: map[string]*config2.Formatter{
cfg := config.Config{
Formatters: map[string]*config.Formatter{
"echo": {
Command: "echo",
Includes: []string{"*"},
Expand Down Expand Up @@ -261,8 +319,8 @@ func TestChangeWorkingDirectory(t *testing.T) {
configPath := tempDir + "/treefmt.toml"

// test without any excludes
cfg := config2.Config{
Formatters: map[string]*config2.Formatter{
cfg := config.Config{
Formatters: map[string]*config.Formatter{
"echo": {
Command: "echo",
Includes: []string{"*"},
Expand All @@ -286,8 +344,8 @@ func TestFailOnChange(t *testing.T) {
configPath := tempDir + "/touch.toml"

// test without any excludes
cfg := config2.Config{
Formatters: map[string]*config2.Formatter{
cfg := config.Config{
Formatters: map[string]*config.Formatter{
"touch": {
Command: "touch",
Includes: []string{"*"},
Expand Down Expand Up @@ -322,8 +380,8 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
as.NoError(os.Setenv("PATH", binPath+":"+os.Getenv("PATH")))

// start with 2 formatters
cfg := config2.Config{
Formatters: map[string]*config2.Formatter{
cfg := config.Config{
Formatters: map[string]*config.Formatter{
"python": {
Command: "black",
Includes: []string{"*.py"},
Expand Down Expand Up @@ -367,7 +425,7 @@ func TestBustCacheOnFormatterChange(t *testing.T) {
assertStats(t, as, 31, 0, 0, 0)

// add go formatter
cfg.Formatters["go"] = &config2.Formatter{
cfg.Formatters["go"] = &config.Formatter{
Command: "gofmt",
Options: []string{"-w"},
Includes: []string{"*.go"},
Expand Down Expand Up @@ -417,8 +475,8 @@ func TestGitWorktree(t *testing.T) {
configPath := filepath.Join(tempDir, "/treefmt.toml")

// basic config
cfg := config2.Config{
Formatters: map[string]*config2.Formatter{
cfg := config.Config{
Formatters: map[string]*config.Formatter{
"echo": {
Command: "echo",
Includes: []string{"*"},
Expand Down Expand Up @@ -484,8 +542,8 @@ func TestPathsArg(t *testing.T) {
as.NoError(os.Chdir(tempDir))

// basic config
cfg := config2.Config{
Formatters: map[string]*config2.Formatter{
cfg := config.Config{
Formatters: map[string]*config.Formatter{
"echo": {
Command: "echo",
Includes: []string{"*"},
Expand Down Expand Up @@ -528,8 +586,8 @@ func TestStdIn(t *testing.T) {
as.NoError(os.Chdir(tempDir))

// basic config
cfg := config2.Config{
Formatters: map[string]*config2.Formatter{
cfg := config.Config{
Formatters: map[string]*config.Formatter{
"echo": {
Command: "echo",
Includes: []string{"*"},
Expand Down Expand Up @@ -568,11 +626,20 @@ go/main.go
func TestDeterministicOrderingInPipeline(t *testing.T) {
as := require.New(t)

// capture current cwd, so we can replace it after the test is finished
cwd, err := os.Getwd()
as.NoError(err)

t.Cleanup(func() {
// return to the previous working directory
as.NoError(os.Chdir(cwd))
})

tempDir := test.TempExamples(t)
configPath := tempDir + "/treefmt.toml"

test.WriteConfig(t, configPath, config2.Config{
Formatters: map[string]*config2.Formatter{
test.WriteConfig(t, configPath, config.Config{
Formatters: map[string]*config.Formatter{
// a and b have no priority set, which means they default to 0 and should execute first
// a and b should execute in lexicographical order
// c should execute first since it has a priority of 1
Expand All @@ -595,7 +662,7 @@ func TestDeterministicOrderingInPipeline(t *testing.T) {
},
})

_, err := cmd(t, "-C", tempDir)
_, err = cmd(t, "-C", tempDir)
as.NoError(err)

matcher := regexp.MustCompile("^fmt-(.*)")
Expand Down
7 changes: 6 additions & 1 deletion cli/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"path/filepath"
"testing"

"github.com/charmbracelet/log"

"git.numtide.com/numtide/treefmt/stats"

"git.numtide.com/numtide/treefmt/test"
Expand All @@ -33,7 +35,7 @@ func cmd(t *testing.T, args ...string) ([]byte, error) {
t.Helper()

// create a new kong context
p := newKong(t, &Cli)
p := newKong(t, &Cli, Options...)
ctx, err := p.Parse(args)
if err != nil {
return nil, err
Expand All @@ -50,6 +52,8 @@ func cmd(t *testing.T, args ...string) ([]byte, error) {
os.Stdout = tempOut
os.Stderr = tempOut

log.SetOutput(tempOut)

// run the command
if err = ctx.Run(); err != nil {
return nil, err
Expand All @@ -68,6 +72,7 @@ func cmd(t *testing.T, args ...string) ([]byte, error) {
// swap outputs back
os.Stdout = stdout
os.Stderr = stderr
log.SetOutput(stderr)

return out, nil
}
Expand Down
39 changes: 39 additions & 0 deletions cli/mappers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package cli

import (
"fmt"
"reflect"

"github.com/alecthomas/kong"
"github.com/charmbracelet/log"
)

var Options []kong.Option

func init() {
Options = []kong.Option{
kong.TypeMapper(reflect.TypeOf(log.DebugLevel), logLevelDecoder()),
}
}

func logLevelDecoder() kong.MapperFunc {
return func(ctx *kong.DecodeContext, target reflect.Value) error {
t, err := ctx.Scan.PopValue("string")
if err != nil {
return err
}
var str string
switch v := t.Value.(type) {
case string:
str = v
default:
return fmt.Errorf("expected a string but got %q (%T)", t, t.Value)
}
level, err := log.ParseLevel(str)
if err != nil {
return fmt.Errorf("failed to parse '%v' as log level: %w", level, err)
}
target.Set(reflect.ValueOf(level))
return nil
}
}
13 changes: 12 additions & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Flags:
-v, --verbose Set the verbosity of logs e.g. -vv ($LOG_LEVEL).
-V, --version Print version.
-i, --init Create a new treefmt.toml.
-u, --on-unmatched=warn Log paths that did not match any formatters at the specified log level, with fatal exiting the process with an error. Possible values are <debug|info|warn|error|fatal>.
--stdin Format the context passed in via stdin.
--cpu-profile=STRING The file into which a cpu profile will be written.
```
Expand Down Expand Up @@ -95,9 +96,19 @@ while `-vv` will also show `[DEBUG]` messages.

Create a new `treefmt.toml`.

### `-u --on-unmatched`

Log paths that did not match any formatters at the specified log level, with fatal exiting the process with an error. Possible values are <debug|info|warn|error|fatal>.

[default: warn]

### `--stdin`

Format the content passed in via stdin.
Format the context passed in via stdin.

### `--cpu-profile`

The file into which a cpu profile will be written.

### `-V, --version`

Expand Down
3 changes: 1 addition & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ func main() {
}
}

ctx := kong.Parse(&cli.Cli)
cli.ConfigureLogging()
ctx := kong.Parse(&cli.Cli, cli.Options...)
ctx.FatalIfErrorf(ctx.Run())
}

0 comments on commit 7afdc7a

Please sign in to comment.