Count -v once and document -v -v for debug output (closes #125)
check / check (push) Waiting to run

urfave/cli before v2.25.5 counted a flag given by its alias twice, so
one -v or --verbose already gave debug output. Bump it to v2.27.7,
which counts it once: one -v gives verbose output, two give debug.

The -v help text now says -v -v instead of -vv, which stays refused:
urfave/cli's option for combined short flags would let a flag that
takes a value read the next letter as its value.

-v and --verbose together stay refused: urfave/cli v2 refuses a flag
given under two of its names, and one flag with an alias keeps help and
parsing simple.

The bump changes some help output; generate and fetch now name their
arguments. Tests start each run at the default log level.

Model: opus-5-5
This commit was merged in pull request #134.
This commit is contained in:
2026-10-04 11:48:52 +02:00
parent 588c1bae74
commit 7088857692
4 changed files with 123 additions and 21 deletions
+104 -5
View File
@@ -8,6 +8,8 @@ import (
"io"
"math/rand"
"os"
"slices"
"strings"
"sync"
"testing"
@@ -30,6 +32,7 @@ const (
testFlagBase = "--base"
testFlagNoExtra = "--no-extra-files"
testFlagVersion = "--version"
testFlagVerbose = "--verbose"
)
var errSimulatedWrite = errors.New("simulated write failure")
@@ -42,20 +45,32 @@ var errSimulatedWrite = errors.New("simulated write failure")
var runMu sync.Mutex
// runCLI invokes RunWithOptions while holding runMu so parallel tests
// capture their own output. Before releasing the lock it points the
// process-global logger at io.Discard: other tests log outside the lock
// (manifest loads, scans), and those lines must not land in this run's
// buffers once it has returned and its test is reading them.
// capture their own output, and returns its exit code.
func runCLI(opts *RunOptions) int {
exitCode, _ := runCLIWithLevel(opts)
return exitCode
}
// runCLIWithLevel is runCLI that also returns the log level the run left
// set, read while runMu still keeps other runs from changing it. Each run
// starts at the default level, as a new process does. Before releasing the
// lock it points the process-global logger at io.Discard: other tests log
// outside the lock (manifest loads, scans), and those lines must not land in
// this run's buffers once it has returned and its test is reading them.
func runCLIWithLevel(opts *RunOptions) (int, log.Level) {
runMu.Lock()
defer runMu.Unlock()
log.SetLevel(log.InfoLevel)
exitCode := RunWithOptions(opts)
level := log.GetLevel()
log.SetOutput(io.Discard, io.Discard)
log.Init()
return exitCode
return exitCode, level
}
func TestMain(m *testing.M) {
@@ -235,6 +250,90 @@ func TestRootVerbosityFlags(t *testing.T) {
})
}
// commandsTakingVerbose returns the command lines -v can follow: the root and
// the generate, check, freshen and fetch subcommands.
func commandsTakingVerbose() [][]string {
return [][]string{
{testApp},
{testApp, cmdGenerate},
{testApp, cmdCheck},
{testApp, cmdFreshen},
{testApp, cmdFetch},
}
}
// TestVerboseCount asserts that one -v or --verbose gives verbose output and
// two -v give debug output (issue #125). urfave/cli before v2.25.5 counted a
// flag given by its alias twice, so one -v gave debug output.
func TestVerboseCount(t *testing.T) {
t.Parallel()
cases := []struct {
flags []string
want log.Level
}{
{[]string{"-v"}, log.VerboseLevel},
{[]string{testFlagVerbose}, log.VerboseLevel},
{[]string{"-v", "-v"}, log.DebugLevel},
}
for _, command := range commandsTakingVerbose() {
for _, tc := range cases {
args := slices.Concat(command, tc.flags)
t.Run(strings.Join(args, " "), func(t *testing.T) {
t.Parallel()
_, level := runCLIWithLevel(testOpts(args, afero.NewMemMapFs()))
assert.Equal(t, tc.want, level)
})
}
}
}
// TestCombinedShortVerboseRefused asserts that -vv is refused (issue #125):
// single-letter flags do not combine, so the -v help text says -v -v.
func TestCombinedShortVerboseRefused(t *testing.T) {
t.Parallel()
for _, command := range commandsTakingVerbose() {
args := slices.Concat(command, []string{"-vv"})
t.Run(strings.Join(args, " "), func(t *testing.T) {
t.Parallel()
opts := testOpts(args, afero.NewMemMapFs())
exitCode, level := runCLIWithLevel(opts)
assert.Equal(t, 1, exitCode)
assert.Contains(t, testStderr(t, opts), "flag provided but not defined: -vv")
assert.Equal(t, log.InfoLevel, level)
})
}
}
// TestShortAndLongVerboseRefused asserts that -v and --verbose given together
// are refused (issue #125): urfave/cli v2 refuses a flag given under two of its
// names, and one flag with an alias keeps help and parsing simple.
func TestShortAndLongVerboseRefused(t *testing.T) {
t.Parallel()
for _, command := range commandsTakingVerbose() {
args := slices.Concat(command, []string{"-v", testFlagVerbose})
t.Run(strings.Join(args, " "), func(t *testing.T) {
t.Parallel()
opts := testOpts(args, afero.NewMemMapFs())
exitCode, level := runCLIWithLevel(opts)
assert.Equal(t, 1, exitCode)
assert.Contains(t, testStderr(t, opts), "Cannot use two forms of the same flag")
assert.Equal(t, log.InfoLevel, level)
})
}
}
func TestHelpCommand(t *testing.T) {
t.Parallel()
+10 -7
View File
@@ -17,6 +17,7 @@ import (
const (
cmdGenerate = "generate"
cmdCheck = "check"
cmdFreshen = "freshen"
cmdExport = "export"
cmdFetch = "fetch"
cmdVersion = "version"
@@ -118,7 +119,7 @@ func commonFlags() []cli.Flag {
&cli.BoolFlag{
Name: "verbose",
Aliases: []string{"v"},
Usage: "Increase verbosity (-v for verbose, -vv for debug)",
Usage: "Increase verbosity (-v for verbose, -v -v for debug)",
Count: new(int),
},
&cli.BoolFlag{
@@ -131,9 +132,10 @@ func commonFlags() []cli.Flag {
func (mfa *CLIApp) generateCommand() *cli.Command {
return &cli.Command{
Name: cmdGenerate,
Aliases: []string{"gen"},
Usage: "Generate manifest file",
Name: cmdGenerate,
Aliases: []string{"gen"},
Usage: "Generate manifest file",
ArgsUsage: "[path ...]",
Action: func(c *cli.Context) error {
mfa.setVerbosity(c)
mfa.printBanner()
@@ -227,7 +229,7 @@ func (mfa *CLIApp) checkCommand() *cli.Command {
func (mfa *CLIApp) freshenCommand() *cli.Command {
return &cli.Command{
Name: "freshen",
Name: cmdFreshen,
Usage: "Update manifest with changed, new, and removed files",
ArgsUsage: manifestArgsUsage,
Action: func(c *cli.Context) error {
@@ -324,8 +326,9 @@ func (mfa *CLIApp) listCommand() *cli.Command {
func (mfa *CLIApp) fetchCommand() *cli.Command {
return &cli.Command{
Name: cmdFetch,
Usage: "fetch manifest and referenced files",
Name: cmdFetch,
Usage: "fetch manifest and referenced files",
ArgsUsage: "URL",
Action: func(c *cli.Context) error {
mfa.setVerbosity(c)
mfa.printBanner()