Skip to content

Commit

Permalink
Merge pull request #504 from numtide/feat/verbosity-changes
Browse files Browse the repository at this point in the history
Verbosity changes
  • Loading branch information
brianmcgee authored Jan 11, 2025
2 parents 25f44e5 + 10c9a72 commit 45881a4
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 41 deletions.
6 changes: 3 additions & 3 deletions cmd/format/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,9 @@ func Run(v *viper.Viper, statz *stats.Stats, cmd *cobra.Command, paths []string)
return fmt.Errorf("failed to close walker: %w", err)
}

// print stats to stdout, unless we are processing from stdin and therefore outputting the results to stdout
if !cfg.Stdin {
statz.Print()
// print stats to stderr
if !cfg.Quiet {
statz.PrintToStderr()
}

if formatErr != nil {
Expand Down
22 changes: 14 additions & 8 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func runE(v *viper.Viper, statz *stats.Stats, cmd *cobra.Command, args []string)
return fmt.Errorf("failed to find treefmt config file: %w", err)
}

log.Infof("using config file: %s", configFile)
log.Debugf("using config file: %s", configFile)

// read in the config
v.SetConfigFile(configFile)
Expand All @@ -144,13 +144,19 @@ func runE(v *viper.Viper, statz *stats.Stats, cmd *cobra.Command, args []string)
log.SetOutput(os.Stderr)
log.SetReportTimestamp(false)

switch v.GetInt("verbose") {
case 0:
log.SetLevel(log.WarnLevel)
case 1:
log.SetLevel(log.InfoLevel)
default:
log.SetLevel(log.DebugLevel)
if v.GetBool("quiet") {
// if quiet, we only log errors
log.SetLevel(log.ErrorLevel)
} else {
// otherwise, the verbose flag controls the log level
switch v.GetInt("verbose") {
case 0:
log.SetLevel(log.WarnLevel)
case 1:
log.SetLevel(log.InfoLevel)
default:
log.SetLevel(log.DebugLevel)
}
}

// format
Expand Down
106 changes: 81 additions & 25 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ func TestOnUnmatched(t *testing.T) {
}
}

// default is WARN
// default is INFO
t.Run("default", func(t *testing.T) {
treefmt(t, withNoError(t), withOutput(checkOutput(log.WarnLevel)))
treefmt(t, withArgs("-v"), withNoError(t), withStderr(checkOutput(log.InfoLevel)))
})

// should exit with error when using fatal
Expand All @@ -99,15 +99,15 @@ func TestOnUnmatched(t *testing.T) {
treefmt(t,
withArgs("-vv", "--on-unmatched", levelStr),
withNoError(t),
withOutput(checkOutput(level)),
withStderr(checkOutput(level)),
)

t.Setenv("TREEFMT_ON_UNMATCHED", levelStr)

treefmt(t,
withArgs("-vv"),
withNoError(t),
withOutput(checkOutput(level)),
withStderr(checkOutput(level)),
)
})
}
Expand All @@ -131,6 +131,33 @@ func TestOnUnmatched(t *testing.T) {
})
}

func TestQuiet(t *testing.T) {
as := require.New(t)
tempDir := test.TempExamples(t)

test.ChangeWorkDir(t, tempDir)

// allow missing formatter
t.Setenv("TREEFMT_ALLOW_MISSING_FORMATTER", "true")

noOutput := func(out []byte) {
as.Empty(out)
}

treefmt(t, withArgs("-q"), withNoError(t), withStdout(noOutput), withStderr(noOutput))
treefmt(t, withArgs("--quiet"), withNoError(t), withStdout(noOutput), withStderr(noOutput))

t.Setenv("TREEFMT_QUIET", "true")
treefmt(t, withNoError(t), withStdout(noOutput), withStderr(noOutput))

t.Setenv("TREEFMT_ALLOW_MISSING_FORMATTER", "false")

// check it doesn't suppress errors
treefmt(t, withError(func(err error) {
as.ErrorContains(err, "error looking up 'foo-fmt'")
}))
}

func TestCpuProfile(t *testing.T) {
as := require.New(t)
tempDir := test.TempExamples(t)
Expand Down Expand Up @@ -1583,7 +1610,7 @@ func TestStdin(t *testing.T) {
withError(func(err error) {
as.EqualError(err, "exactly one path should be specified when using the --stdin flag")
}),
withOutput(func(out []byte) {
withStderr(func(out []byte) {
as.Equal("Error: exactly one path should be specified when using the --stdin flag\n", string(out))
}),
)
Expand All @@ -1600,7 +1627,7 @@ func TestStdin(t *testing.T) {
stats.Formatted: 1,
stats.Changed: 1,
}),
withOutput(func(out []byte) {
withStdout(func(out []byte) {
as.Equal(`{ ...}: "hello"
`, string(out))
}),
Expand All @@ -1616,7 +1643,7 @@ func TestStdin(t *testing.T) {
withError(func(err error) {
as.Errorf(err, "path ../test.nix not inside the tree root %s", tempDir)
}),
withOutput(func(out []byte) {
withStderr(func(out []byte) {
as.Contains(string(out), "Error: path ../test.nix not inside the tree root")
}),
)
Expand All @@ -1639,7 +1666,7 @@ func TestStdin(t *testing.T) {
stats.Formatted: 1,
stats.Changed: 1,
}),
withOutput(func(out []byte) {
withStdout(func(out []byte) {
as.Equal(`| col1 | col2 |
| ------ | --------- |
| nice | fits |
Expand Down Expand Up @@ -1806,7 +1833,9 @@ type options struct {
value *config.Config
}

assertOut func([]byte)
assertStdout func([]byte)
assertStderr func([]byte)

assertError func(error)
assertStats func(*stats.Stats)

Expand Down Expand Up @@ -1873,9 +1902,15 @@ func withNoError(t *testing.T) option {
}
}

func withOutput(fn func([]byte)) option {
func withStdout(fn func([]byte)) option {
return func(o *options) {
o.assertStdout = fn
}
}

func withStderr(fn func([]byte)) option {
return func(o *options) {
o.assertOut = fn
o.assertStderr = fn
}
}

Expand Down Expand Up @@ -1931,17 +1966,19 @@ func treefmt(
t.Logf("treefmt %s", strings.Join(args, " "))

tempDir := t.TempDir()
tempOut := test.TempFile(t, tempDir, "combined_output", nil)

tempStdout := test.TempFile(t, tempDir, "stdout", nil)
tempStderr := test.TempFile(t, tempDir, "stderr", nil)

// capture standard outputs before swapping them
stdout := os.Stdout
stderr := os.Stderr

// swap them temporarily
os.Stdout = tempOut
os.Stderr = tempOut
os.Stdout = tempStdout
os.Stderr = tempStderr

log.SetOutput(tempOut)
log.SetOutput(tempStdout)

defer func() {
// swap outputs back
Expand All @@ -1954,30 +1991,49 @@ func treefmt(
root, statz := cmd.NewRoot()

root.SetArgs(args)
root.SetOut(tempOut)
root.SetErr(tempOut)
root.SetOut(tempStdout)
root.SetErr(tempStderr)

// execute the command
cmdErr := root.Execute()

// reset and read the temporary output
if _, resetErr := tempOut.Seek(0, 0); resetErr != nil {
// reset and read the temporary outputs
if _, resetErr := tempStdout.Seek(0, 0); resetErr != nil {
t.Fatal(fmt.Errorf("failed to reset temp output for reading: %w", resetErr))
}

out, readErr := io.ReadAll(tempOut)
if _, resetErr := tempStderr.Seek(0, 0); resetErr != nil {
t.Fatal(fmt.Errorf("failed to reset temp output for reading: %w", resetErr))
}

// read back stderr and validate
out, readErr := io.ReadAll(tempStderr)
if readErr != nil {
t.Fatal(fmt.Errorf("failed to read temp output: %w", readErr))
t.Fatal(fmt.Errorf("failed to read temp stderr: %w", readErr))
}

if opts.assertStderr != nil {
opts.assertStderr(out)
}

t.Log("\n" + string(out))

if opts.assertStats != nil {
opts.assertStats(statz)
// read back stdout and validate
out, readErr = io.ReadAll(tempStdout)
if readErr != nil {
t.Fatal(fmt.Errorf("failed to read temp stdout: %w", readErr))
}

if opts.assertOut != nil {
opts.assertOut(out)
t.Log("\n" + string(out))

if opts.assertStdout != nil {
opts.assertStdout(out)
}

// assert other properties

if opts.assertStats != nil {
opts.assertStats(statz)
}

if opts.assertError != nil {
Expand Down
6 changes: 5 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Config struct {
Formatters []string `mapstructure:"formatters" toml:"formatters,omitempty"`
NoCache bool `mapstructure:"no-cache" toml:"-"` // not allowed in config
OnUnmatched string `mapstructure:"on-unmatched" toml:"on-unmatched,omitempty"`
Quiet bool `mapstructure:"quiet" toml:"-"` // not allowed in config
TreeRoot string `mapstructure:"tree-root" toml:"tree-root,omitempty"`
TreeRootFile string `mapstructure:"tree-root-file" toml:"tree-root-file,omitempty"`
Verbose uint8 `mapstructure:"verbose" toml:"verbose,omitempty"`
Expand Down Expand Up @@ -89,7 +90,7 @@ func SetFlags(fs *pflag.FlagSet) {
"Ignore the evaluation cache entirely. Useful for CI. (env $TREEFMT_NO_CACHE)",
)
fs.StringP(
"on-unmatched", "u", "warn",
"on-unmatched", "u", "info",
"Log paths that did not match any formatters at the specified log level. Possible values are "+
"<debug|info|warn|error|fatal>. (env $TREEFMT_ON_UNMATCHED)",
)
Expand All @@ -110,6 +111,9 @@ func SetFlags(fs *pflag.FlagSet) {
"verbose", "v",
"Set the verbosity of logs e.g. -vv. (env $TREEFMT_VERBOSE)",
)
fs.BoolP(
"quiet", "q", false, "Disable all logs except errors. (env $TREEFMT_QUIET)",
)
fs.String(
"walk", "auto",
"The method used to traverse the files within the tree root. Currently supports "+
Expand Down
32 changes: 31 additions & 1 deletion config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,36 @@ func TestNoCache(t *testing.T) {
checkValue(true)
}

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

cfg := &config.Config{}
v, flags := newViper(t)

checkValue := func(expected bool) {
readValue(t, v, cfg, func(cfg *config.Config) {
as.Equal(expected, cfg.Quiet)
})
}

// default with no flag, env or config
checkValue(false)

// set config value and check that it has no effect
// you are not allowed to set no-cache in config
cfg.Quiet = true

checkValue(false)

// env override
t.Setenv("TREEFMT_QUIET", "false")
checkValue(false)

// flag override
as.NoError(flags.Set("quiet", "true"))
checkValue(true)
}

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

Expand All @@ -336,7 +366,7 @@ func TestOnUnmatched(t *testing.T) {
}

// default with no flag, env or config
checkValue("warn")
checkValue("info")

// set config value
cfg.OnUnmatched = "error"
Expand Down
16 changes: 16 additions & 0 deletions docs/content/getting-started/configure.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,22 @@ Possible values are `<debug|info|warn|error|fatal>`.
on-unmatched = "debug"
```

### `quiet`

Suppress all output except for errors.

=== "Flag"

```console
treefmt --quiet
```

=== "Env"

```console
TREEFMT_QUIET=true treefmt
```

### `stdin`

Format the context passed in via stdin.
Expand Down
5 changes: 5 additions & 0 deletions docs/content/guides/unmatched-formatters.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,28 @@ This helps you decide whether to add formatters for specific files or ignore the
## Customizing Notifications

### Reducing Log Verbosity

If you find the unmatched file warnings too noisy, you can lower the logging level in your config:

`treefmt.toml`:

```toml
on-unmatched = "debug"
```

To later find out what files are unmatched, you can override this setting via the command line:

```console
$ treefmt --on-unmatched warn
```

### Enforcing Strict Matching

Another stricter policy approach is to fail the run if any unmatched files are found.
This can be paired with an `excludes` list to ignore specific files:

`treefmt.toml`:

```toml
# Fail if any unmatched files are found
on-unmatched = "fatal"
Expand Down
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
site_name: Treefmt
site_url: https://treefmt.com
site_description: >-
The formatter multiplexer.
The formatter multiplexer.
# Repository
repo_name: numtide/treefmt
Expand Down
Loading

0 comments on commit 45881a4

Please # to comment.