Files
commandkit/internal/cobra/flags_test.go

295 lines
14 KiB
Go

package cobra
import (
"reflect"
"strings"
"testing"
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
)
func flagGrammarTree() *commandmeta.Command {
return &commandmeta.Command{Name: "app", Children: []*commandmeta.Command{{
Name: "run", Runnable: true,
Flags: []commandmeta.Flag{
{Name: "color", Short: "c"},
{Name: "verbose", Short: "v", Boolean: true},
{Name: "quiet", Short: "q", Boolean: true},
{Name: "message", Short: "m"},
{Name: "tag", Short: "t", Repeated: true},
},
}}}
}
func flagGrammarOptions() ParseOptions {
return ParseOptions{FlagAliases: []commandmeta.FlagAlias{
{Source: "--colour", Target: "--color"},
{Source: "-C", Target: "--color"},
{Source: "-w", Target: "--verbose"},
{Source: "-z", Target: "--quiet"},
{Source: "--taggy", Target: "-t"},
{Source: "--hlep", Target: "--help"},
}}
}
func reportingFlagTree() *commandmeta.Command {
return &commandmeta.Command{Name: "app", Children: []*commandmeta.Command{{
Name: "run", Runnable: true, CoreFlags: commandmeta.ReportingFlags(),
Flags: []commandmeta.Flag{{Name: "message"}},
}}}
}
func reportingFlagOptions() ParseOptions {
return ParseOptions{FlagAliases: []commandmeta.FlagAlias{
{Source: "-q", Target: "--quiet"},
{Source: "-v", Target: "--verbose"},
{Source: "-d", Target: "--debug"},
}}
}
func TestParseReportsFlagOccurrencesInEnteredOrder(t *testing.T) {
result := Parse(reportingFlagTree(), []string{
"run", "--verbose=true", "-qd", "--progress=false", "--message=--quiet", "--", "--debug",
}, reportingFlagOptions())
if result.Failure != nil {
t.Fatalf("parse failed: %s", result.Failure.Message)
}
want := []commandmeta.FlagOccurrence{
{Name: "verbose", Spelling: "--verbose", RawValue: "true"},
{Name: "quiet", Spelling: "-q", RawValue: "true"},
{Name: "debug", Spelling: "-d", RawValue: "true"},
{Name: "progress", Spelling: "--progress", RawValue: "false"},
{Name: "message", Spelling: "--message", RawValue: "--quiet"},
}
if !reflect.DeepEqual(result.FlagOccurrences, want) {
t.Fatalf("flag occurrences = %#v, want %#v", result.FlagOccurrences, want)
}
if !reflect.DeepEqual(result.FlagValues, [][]string{{"--quiet"}}) || !reflect.DeepEqual(result.FlagSpellings, [][]string{{"--message"}}) {
t.Fatalf("consumer values or spellings changed: %#v, %#v", result.FlagValues, result.FlagSpellings)
}
}
func TestParseFlagOccurrencesRetainOnlyConsumedPrefixOnFailure(t *testing.T) {
result := Parse(reportingFlagTree(), []string{"run", "--verbose", "--unknown", "--quiet"}, reportingFlagOptions())
if result.Failure == nil || len(result.FlagOccurrences) != 1 || result.FlagOccurrences[0].Name != "verbose" {
t.Fatalf("unknown flag result = %#v", result)
}
result = Parse(reportingFlagTree(), []string{"run", "-qd", "--message"}, reportingFlagOptions())
if result.Failure == nil || len(result.FlagOccurrences) != 2 || result.FlagOccurrences[0].Name != "quiet" || result.FlagOccurrences[1].Name != "debug" {
t.Fatalf("missing value result = %#v", result)
}
result = Parse(reportingFlagTree(), []string{"missing", "--quiet"}, reportingFlagOptions())
if result.Failure == nil || len(result.FlagOccurrences) != 0 {
t.Fatalf("malformed path result = %#v", result)
}
}
func TestParseFlagOccurrenceKeepsOptionLookingValueAndConsumesTerminator(t *testing.T) {
result := Parse(reportingFlagTree(), []string{"run", "--message", "--quiet", "--", "--verbose"}, reportingFlagOptions())
if result.Failure != nil {
t.Fatalf("parse failed: %s", result.Failure.Message)
}
want := []commandmeta.FlagOccurrence{{Name: "message", Spelling: "--message", RawValue: "--quiet"}}
if !reflect.DeepEqual(result.FlagOccurrences, want) {
t.Fatalf("flag occurrences = %#v, want %#v", result.FlagOccurrences, want)
}
if !reflect.DeepEqual(result.Operands, []string{"--verbose"}) {
t.Fatalf("operands = %#v", result.Operands)
}
}
func TestParseCanonicalAndAliasShortGrammar(t *testing.T) {
for _, test := range []struct {
name string
args []string
values [][]string
spellings [][]string
}{
{"boolean cluster", []string{"run", "-vq"}, [][]string{nil, {"true"}, {"true"}, nil, nil}, [][]string{nil, {"-v"}, {"-q"}, nil, nil}},
{"aliased boolean cluster", []string{"run", "-wz"}, [][]string{nil, {"true"}, {"true"}, nil, nil}, [][]string{nil, {"-w"}, {"-z"}, nil, nil}},
{"value short separated", []string{"run", "-c", "blue"}, [][]string{{"blue"}, nil, nil, nil, nil}, [][]string{{"-c"}, nil, nil, nil, nil}},
{"value short equals", []string{"run", "-c=blue"}, [][]string{{"blue"}, nil, nil, nil, nil}, [][]string{{"-c"}, nil, nil, nil, nil}},
{"value short attached", []string{"run", "-cblue"}, [][]string{{"blue"}, nil, nil, nil, nil}, [][]string{{"-c"}, nil, nil, nil, nil}},
{"long alias", []string{"run", "--colour=blue"}, [][]string{{"blue"}, nil, nil, nil, nil}, [][]string{{"--colour"}, nil, nil, nil, nil}},
{"short alias", []string{"run", "-Cblue"}, [][]string{{"blue"}, nil, nil, nil, nil}, [][]string{{"-C"}, nil, nil, nil, nil}},
{"cluster value consumes suffix", []string{"run", "-vqcblue"}, [][]string{{"blue"}, {"true"}, {"true"}, nil, nil}, [][]string{{"-c"}, {"-v"}, {"-q"}, nil, nil}},
{"cluster value consumes next", []string{"run", "-vqc", "blue"}, [][]string{{"blue"}, {"true"}, {"true"}, nil, nil}, [][]string{{"-c"}, {"-v"}, {"-q"}, nil, nil}},
{"mixed canonical and alias cluster", []string{"run", "-wqcblue"}, [][]string{{"blue"}, {"true"}, {"true"}, nil, nil}, [][]string{{"-c"}, {"-w"}, {"-q"}, nil, nil}},
{"value takes remaining suffix", []string{"run", "-cvq"}, [][]string{{"vq"}, nil, nil, nil, nil}, [][]string{{"-c"}, nil, nil, nil, nil}},
{"boolean explicit false", []string{"run", "-v=false"}, [][]string{nil, {"false"}, nil, nil, nil}, [][]string{nil, {"-v"}, nil, nil, nil}},
{"boolean explicit false in cluster", []string{"run", "-qv=false"}, [][]string{nil, {"false"}, {"true"}, nil, nil}, [][]string{nil, {"-v"}, {"-q"}, nil, nil}},
{"empty canonical short", []string{"run", "-c="}, [][]string{{""}, nil, nil, nil, nil}, [][]string{{"-c"}, nil, nil, nil, nil}},
{"empty canonical long", []string{"run", "--color="}, [][]string{{""}, nil, nil, nil, nil}, [][]string{{"--color"}, nil, nil, nil, nil}},
{"empty separated short", []string{"run", "-c", ""}, [][]string{{""}, nil, nil, nil, nil}, [][]string{{"-c"}, nil, nil, nil, nil}},
{"empty aliased long", []string{"run", "--colour="}, [][]string{{""}, nil, nil, nil, nil}, [][]string{{"--colour"}, nil, nil, nil, nil}},
{"empty aliased short", []string{"run", "-C="}, [][]string{{""}, nil, nil, nil, nil}, [][]string{{"-C"}, nil, nil, nil, nil}},
{"empty boolean short", []string{"run", "-v="}, [][]string{nil, {""}, nil, nil, nil}, [][]string{nil, {"-v"}, nil, nil, nil}},
} {
t.Run(test.name, func(t *testing.T) {
result := Parse(flagGrammarTree(), test.args, flagGrammarOptions())
if result.Failure != nil {
t.Fatalf("parse failed: %s", result.Failure.Message)
}
if !reflect.DeepEqual(result.FlagValues, test.values) {
t.Errorf("values = %#v, want %#v", result.FlagValues, test.values)
}
if !reflect.DeepEqual(result.FlagSpellings, test.spellings) {
t.Errorf("spellings = %#v, want %#v", result.FlagSpellings, test.spellings)
}
})
}
}
func TestParseFlagAliasesPreserveValuesAndTerminators(t *testing.T) {
result := Parse(flagGrammarTree(), []string{"run", "--message", "--hlep", "--message=--hlep", "--taggy", "one", "--", "--taggy"}, flagGrammarOptions())
if result.Failure != nil {
t.Fatalf("parse failed: %s", result.Failure.Message)
}
if got, want := result.FlagValues[3], []string{"--hlep", "--hlep"}; !reflect.DeepEqual(got, want) {
t.Fatalf("message values = %#v, want %#v", got, want)
}
if got, want := result.FlagSpellings[3], []string{"--message", "--message"}; !reflect.DeepEqual(got, want) {
t.Fatalf("message spellings = %#v, want %#v", got, want)
}
if got, want := result.FlagValues[4], []string{"one"}; !reflect.DeepEqual(got, want) {
t.Fatalf("tag values = %#v, want %#v", got, want)
}
if got, want := result.Operands, []string{"--taggy"}; !reflect.DeepEqual(got, want) {
t.Fatalf("operands = %#v, want %#v", got, want)
}
}
func TestParseConsumesDoubleDashAsAFlagValue(t *testing.T) {
for _, spelling := range []string{"--color", "--colour", "-c", "-C"} {
t.Run(spelling, func(t *testing.T) {
result := Parse(flagGrammarTree(), []string{"run", spelling, "--", "--taggy=one", "--colour=red", "--", "--taggy=operand"}, flagGrammarOptions())
if result.Failure != nil {
t.Fatalf("parse failed: %s", result.Failure.Message)
}
if !reflect.DeepEqual(result.FlagValues[0], []string{"--", "red"}) || !reflect.DeepEqual(result.FlagValues[4], []string{"one"}) {
t.Errorf("flag values = %#v", result.FlagValues)
}
if !reflect.DeepEqual(result.FlagSpellings[0], []string{spelling, "--colour"}) || !reflect.DeepEqual(result.Operands, []string{"--taggy=operand"}) {
t.Errorf("spellings = %#v, operands = %#v", result.FlagSpellings, result.Operands)
}
})
}
for _, args := range [][]string{{"run", "-v", "--", "--colour"}, {"run", "--color", "blue", "--", "--colour"}} {
result := Parse(flagGrammarTree(), args, flagGrammarOptions())
if result.Failure != nil || !reflect.DeepEqual(result.Operands, []string{"--colour"}) {
t.Errorf("Parse(%q) = %#v", args, result)
}
}
}
func TestParseHelpAliasAndUnknownShortMembers(t *testing.T) {
result := Parse(flagGrammarTree(), []string{"run", "--hlep"}, flagGrammarOptions())
if result.Failure != nil || !result.HelpRequested {
t.Fatalf("help alias result = %#v", result)
}
result = Parse(flagGrammarTree(), []string{"run", "-vx"}, flagGrammarOptions())
if result.Failure == nil || result.Failure.Message == "" {
t.Fatalf("unknown short cluster member result = %#v", result)
}
result = Parse(flagGrammarTree(), []string{"run", "--color"}, flagGrammarOptions())
if result.Failure == nil || result.Failure.Message == "" {
t.Fatalf("missing value result = %#v", result)
}
result = Parse(flagGrammarTree(), []string{"run", "--colour"}, flagGrammarOptions())
if result.Failure == nil || result.Failure.Message == "" {
t.Fatalf("missing aliased value result = %#v", result)
}
result = Parse(flagGrammarTree(), []string{"run", "-c"}, flagGrammarOptions())
if result.Failure == nil || result.Failure.Message == "" {
t.Fatalf("missing short value result = %#v", result)
}
result = Parse(flagGrammarTree(), []string{"run", "-C"}, flagGrammarOptions())
if result.Failure == nil || result.Failure.Message == "" {
t.Fatalf("missing aliased short value result = %#v", result)
}
result = Parse(flagGrammarTree(), []string{"run", "-vfalse"}, flagGrammarOptions())
if result.Failure == nil || result.Failure.Message == "" {
t.Fatalf("boolean suffix without equals result = %#v", result)
}
}
func TestParseFlagAliasOccurrenceOrderAndSpellings(t *testing.T) {
result := Parse(flagGrammarTree(), []string{"run", "--taggy=first", "-t", "second", "--taggy", "third"}, flagGrammarOptions())
if result.Failure != nil {
t.Fatalf("parse failed: %s", result.Failure.Message)
}
if got, want := result.FlagValues[4], []string{"first", "second", "third"}; !reflect.DeepEqual(got, want) {
t.Fatalf("tag values = %#v, want %#v", got, want)
}
if got, want := result.FlagSpellings[4], []string{"--taggy", "-t", "--taggy"}; !reflect.DeepEqual(got, want) {
t.Fatalf("tag spellings = %#v, want %#v", got, want)
}
}
func TestParseFailuresRetainEnteredFlagSpellings(t *testing.T) {
for _, spelling := range []string{"--color", "--colour", "-c", "-C"} {
t.Run("missing/"+spelling, func(t *testing.T) {
result := Parse(flagGrammarTree(), []string{"run", spelling}, flagGrammarOptions())
if result.Failure == nil || !strings.Contains(result.Failure.Message, ": "+spelling) {
t.Fatalf("missing value failure = %#v, want entered spelling %q", result.Failure, spelling)
}
})
}
result := Parse(flagGrammarTree(), []string{"run", "-wC"}, flagGrammarOptions())
if result.Failure == nil || !strings.Contains(result.Failure.Message, ": -C") {
t.Fatalf("cluster missing value failure = %#v", result.Failure)
}
for _, test := range []struct {
args []string
spelling string
}{
{[]string{"--hlep=bad"}, "--hlep"},
{[]string{"-h=bad"}, "-h"},
{[]string{"--help=bad"}, "--help"},
{[]string{"--hlep=bad", "--help=true"}, "--hlep"},
{[]string{"-h=bad", "--hlep=true"}, "-h"},
{[]string{"--help=true", "--hlep=bad"}, "--hlep"},
} {
t.Run(strings.Join(test.args, " "), func(t *testing.T) {
result := Parse(flagGrammarTree(), append([]string{"run"}, test.args...), flagGrammarOptions())
if result.Failure == nil || !strings.Contains(result.Failure.Message, "for "+test.spelling) {
t.Fatalf("help failure = %#v, want entered spelling %q", result.Failure, test.spelling)
}
})
}
result = Parse(flagGrammarTree(), []string{"run", "--unknown", "--colour"}, flagGrammarOptions())
if result.Failure == nil || !strings.Contains(result.Failure.Message, "unknown") || strings.Contains(result.Failure.Message, "needs an argument") {
t.Fatalf("earlier parser failure = %#v", result.Failure)
}
}
func TestFlagInterpretationStopsAtTheBackendFailurePrefix(t *testing.T) {
for _, test := range []struct {
name string
args []string
want []string
}{
{"long", []string{"--debug", "--unknown"}, []string{"--debug", "--unknown"}},
{"short cluster", []string{"-dqz"}, []string{"--debug", "--quiet", "-z"}},
} {
t.Run(test.name, func(t *testing.T) {
// The later value must not be canonicalized or retained in spellings.
args := append(append([]string(nil), test.args...), "--message="+strings.Repeat("value", 1024), "--quiet=false")
flags := append(commandmeta.ReportingFlags(), commandmeta.Flag{Name: "message"})
interpreted, spellings, occurrences, failure := interpretFlagTokens(args, flags, reportingFlagOptions().FlagAliases)
if failure != nil || !reflect.DeepEqual(interpreted, test.want) || len(spellings["message"]) != 0 {
t.Fatalf("interpreted=%q spellings=%v failure=%v", interpreted, spellings, failure)
}
if len(occurrences) != len(test.want)-1 || occurrences[0].Name != "debug" {
t.Fatalf("consumed occurrences=%#v", occurrences)
}
result := Parse(reportingFlagTree(), append([]string{"run"}, args...), reportingFlagOptions())
if result.Failure == nil || !strings.Contains(result.Failure.Message, "unknown") || !reflect.DeepEqual(result.FlagOccurrences, occurrences) {
t.Fatalf("backend result=%#v", result)
}
})
}
}