package cobra import ( "bytes" "context" "fmt" "io" "reflect" "strings" "testing" commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta" spfcobra "github.com/spf13/cobra" ) type Command = commandmeta.Command type Argument = commandmeta.Argument type Flag = commandmeta.Flag type Failure = commandmeta.Failure type Result = commandmeta.Result type ParseOptions = commandmeta.ParseOptions func TestParseRoutesBeforeOptionsAndKeepsFlagsLocal(t *testing.T) { tree := &Command{Name: "app", Children: []*Command{ {Name: "config", Children: []*Command{ {Name: "show", Runnable: true, Flags: []Flag{{Name: "scope"}}}, }}, }} result := Parse(tree, []string{"config", "show", "--scope", "app"}, ParseOptions{}) if result.Failure != nil { t.Fatalf("parse failed: %s", result.Failure.Message) } if !reflect.DeepEqual(result.Path, []string{"config", "show"}) || !reflect.DeepEqual(result.FlagValues, [][]string{{"app"}}) { t.Fatalf("selected result = %#v", result) } result = Parse(tree, []string{"config", "--scope", "app", "show"}, ParseOptions{}) if result.Failure == nil { t.Fatal("accepted a child flag before the complete command path") } } func TestParseRequiresExactCommandPathWhenPrefixMatchingIsEnabled(t *testing.T) { previous := spfcobra.EnablePrefixMatching spfcobra.EnablePrefixMatching = true t.Cleanup(func() { spfcobra.EnablePrefixMatching = previous }) tree := &Command{Name: "app", Children: []*Command{{Name: "config", Runnable: true}}} result := Parse(tree, []string{"con"}, ParseOptions{}) if result.Failure == nil { t.Fatal("accepted a prefixed command path") } } func TestParseInterspersedOperandsBooleanFlagsAndTerminator(t *testing.T) { tree := &Command{Name: "app", Children: []*Command{ {Name: "restore", Runnable: true, Flags: []Flag{ {Name: "enabled", Boolean: true}, {Name: "tag", Repeated: true}, }}, }} result := Parse(tree, []string{"restore", "before", "--enabled", "--tag", "a,b", "--tag=c", "--", "--literal"}, ParseOptions{}) if result.Failure != nil { t.Fatalf("parse failed: %s", result.Failure.Message) } if !reflect.DeepEqual(result.FlagValues, [][]string{{"true"}, {"a,b", "c"}}) { t.Fatalf("flag values = %#v", result.FlagValues) } if !reflect.DeepEqual(result.Operands, []string{"before", "--literal"}) { t.Fatalf("operands = %#v", result.Operands) } result = Parse(tree, []string{"restore", "--enabled=false"}, ParseOptions{}) if result.Failure != nil || !reflect.DeepEqual(result.FlagValues[0], []string{"false"}) { t.Fatalf("explicit false = %#v", result) } } func TestReportingFlagsParseWithoutChangingConsumerFlagIndexes(t *testing.T) { tree := &Command{Name: "app", CoreFlags: commandmeta.ReportingFlags(), Children: []*Command{ {Name: "config", CoreFlags: commandmeta.ReportingFlags(), Children: []*Command{ {Name: "show", Runnable: true, CoreFlags: commandmeta.ReportingFlags(), Flags: []Flag{{Name: "format"}}}, }}, }} result := Parse(tree, []string{"config", "show", "--quiet", "-v", "--format=yaml"}, ParseOptions{ FlagAliases: []commandmeta.FlagAlias{{Source: "-v", Target: "--verbose"}}, }) if result.Failure != nil { t.Fatalf("parse failed: %s", result.Failure.Message) } if !reflect.DeepEqual(result.FlagValues, [][]string{{"yaml"}}) { t.Fatalf("consumer binding values = %#v", result.FlagValues) } if !reflect.DeepEqual(result.FlagSpellings, [][]string{{"--format"}}) { t.Fatalf("consumer binding spellings = %#v", result.FlagSpellings) } } func TestReportingFlagsAppearInCanonicalAndDiscoveryHelp(t *testing.T) { flags := commandmeta.ReportingFlags() root := &Command{Name: "app", CoreFlags: flags, Children: []*Command{ {Name: "group", CoreFlags: commandmeta.ReportingFlags(), Children: []*Command{ {Name: "run", Runnable: true, CoreFlags: commandmeta.ReportingFlags()}, }}, {Name: "version", Runnable: true, CoreFlags: commandmeta.ReportingFlags()}, }} session := Prepare(root, ParseOptions{}) for _, path := range [][]string{nil, {"group"}, {"group", "run"}, {"version"}} { help, failure := session.RenderHelp(path) if failure != nil { t.Fatalf("help for %v failed: %s", path, failure.Message) } for _, name := range []string{"quiet", "verbose", "debug", "progress"} { if !strings.Contains(help, "--"+name) { t.Errorf("help for %v omits --%s: %q", path, name, help) } } } discovery, failure := session.RenderHelpView(commandmeta.HelpView{ EnteredPath: []string{"shortcut"}, Discovery: true, CoreFlags: commandmeta.ReportingFlags(), }) if failure != nil { t.Fatal(failure.Message) } for _, name := range []string{"quiet", "verbose", "debug", "progress"} { if !strings.Contains(discovery, "--"+name) { t.Errorf("discovery help omits --%s: %q", name, discovery) } } if strings.Contains(discovery, "Available Commands:") || len(session.Metadata().Children) != 2 { t.Fatalf("discovery help manufactured canonical command metadata: %q", discovery) } } func TestParseHelpIsLocalAndFalseDoesNotRequestHelp(t *testing.T) { tree := &Command{Name: "app", Children: []*Command{ {Name: "run", Runnable: true, Flags: []Flag{{Name: "required", Required: true}}}, }} result := Parse(tree, []string{"run", "--help"}, ParseOptions{}) if result.Failure != nil || !result.HelpRequested { t.Fatalf("help parse = %#v", result) } result = Parse(tree, []string{"run", "--help=false"}, ParseOptions{}) if result.Failure != nil || result.HelpRequested { t.Fatalf("false help parse = %#v", result) } result = Parse(tree, []string{"run", "--help=1"}, ParseOptions{}) if result.Failure != nil || !result.HelpRequested { t.Fatalf("strconv boolean help parse = %#v", result) } result = Parse(tree, []string{"run", "--help=invalid", "--help"}, ParseOptions{}) if result.Failure == nil { t.Fatal("accepted an invalid earlier help occurrence") } } func TestParseRunnableParentAndGroupUnknownCommand(t *testing.T) { tree := &Command{Name: "app", Children: []*Command{ {Name: "deploy", Runnable: true, Flags: []Flag{{Name: "scope"}}, Children: []*Command{{Name: "now", Runnable: true}}}, {Name: "config", Children: []*Command{{Name: "show", Runnable: true}}}, }} result := Parse(tree, []string{"deploy", "target"}, ParseOptions{}) if result.Failure != nil || !reflect.DeepEqual(result.Path, []string{"deploy"}) || !reflect.DeepEqual(result.Operands, []string{"target"}) { t.Fatalf("runnable parent result = %#v", result) } result = Parse(tree, []string{"deploy", "--scope", "local"}, ParseOptions{}) if result.Failure != nil || !reflect.DeepEqual(result.FlagValues, [][]string{{"local"}}) { t.Fatalf("runnable parent local flag = %#v", result) } result = Parse(tree, []string{"deploy", "now"}, ParseOptions{}) if result.Failure != nil || !reflect.DeepEqual(result.Path, []string{"deploy", "now"}) { t.Fatalf("nested runnable child = %#v", result) } result = Parse(tree, []string{"config", "mystery"}, ParseOptions{}) if result.Failure == nil { t.Fatal("accepted an unknown trailing command beneath a group") } } func TestParseReturnsNeutralFailuresAndFreshState(t *testing.T) { tree := &Command{Name: "app", Children: []*Command{ {Name: "run", Runnable: true, Flags: []Flag{{Name: "value"}}}, }} options := ParseOptions{Context: context.Background(), In: strings.NewReader("unused"), Out: io.Discard, Err: io.Discard} first := Parse(tree, []string{"run", "--value=old"}, options) second := Parse(tree, []string{"run"}, options) if first.Failure != nil || second.Failure != nil { t.Fatalf("parse results = %#v, %#v", first, second) } if !reflect.DeepEqual(first.FlagValues, [][]string{{"old"}}) || !reflect.DeepEqual(second.FlagValues, [][]string{nil}) { t.Fatalf("state leaked between parses: %#v then %#v", first.FlagValues, second.FlagValues) } failed := Parse(tree, []string{"run", "--unknown"}, options) if failed.Failure == nil || failed.Failure.Message == "" { t.Fatalf("unknown flag failure = %#v", failed.Failure) } } func TestSelectedPathsResolveThroughBackendParents(t *testing.T) { root := &Command{Name: "app"} for index := 0; index < 128; index++ { root.Children = append(root.Children, &Command{Name: fmt.Sprintf("wide%03d", index), Runnable: true}) } path := make([]string, 24) current := root for index := range path { path[index] = fmt.Sprintf("deep%02d", index) child := &Command{Name: path[index]} if index == len(path)-1 { child.Runnable = true } current.Children = append(current.Children, child) current = child } result := Parse(root, path, ParseOptions{}) if result.Failure != nil || !reflect.DeepEqual(result.Path, path) { t.Fatalf("deep parse path = %#v", result) } rootResult := Parse(root, nil, ParseOptions{}) if rootResult.Failure != nil || len(rootResult.Path) != 0 { t.Fatalf("root parse path = %#v", rootResult) } help, failure := RenderHelp(root, path) if failure != nil || !strings.Contains(help, path[len(path)-1]) { t.Fatalf("deep help = %q, %v", help, failure) } if _, failure := RenderHelp(root, []string{"wide128"}); failure == nil { t.Fatal("help accepted an unknown sibling") } line, failure := UsageLine(root, path) if failure != nil || !strings.Contains(line, path[len(path)-1]) { t.Fatalf("deep usage line = %q, %v", line, failure) } if _, failure := UsageLine(root, []string{"wide128"}); failure == nil { t.Fatal("usage accepted an unknown sibling") } if _, failure := RenderHelp(root, nil); failure != nil { t.Fatalf("root help failed: %v", failure) } if _, failure := UsageLine(root, nil); failure != nil { t.Fatalf("root usage failed: %v", failure) } } func TestSessionReusesTreeForParseHelpAndUsageWithFreshState(t *testing.T) { tree := &Command{Name: "app", Children: []*Command{{Name: "run", Runnable: true, Flags: []Flag{{Name: "value"}}}}} var output, errors bytes.Buffer session := Prepare(tree, ParseOptions{Context: context.Background(), Out: &output, Err: &errors}) first := session.Parse([]string{"run", "--value=old"}) if first.Failure != nil || !reflect.DeepEqual(first.FlagValues, [][]string{{"old"}}) { t.Fatalf("first session parse = %#v", first) } help, failure := session.RenderHelp([]string{"run"}) if failure != nil || !strings.Contains(help, "run") { t.Fatalf("session help = %q, %v", help, failure) } line, failure := session.UsageLine([]string{"run"}) if failure != nil || !strings.Contains(line, "run") { t.Fatalf("session usage = %q, %v", line, failure) } if output.Len() != 0 || errors.Len() != 0 { t.Fatalf("help escaped session delivery control: out=%q err=%q", output.String(), errors.String()) } var secondOutput, secondErrors bytes.Buffer second := Prepare(tree, ParseOptions{Context: context.Background(), Out: &secondOutput, Err: &secondErrors}) secondResult := second.Parse([]string{"run"}) if secondResult.Failure != nil || !reflect.DeepEqual(secondResult.FlagValues, [][]string{nil}) { t.Fatalf("fresh session state = %#v", secondResult) } if output.Len() != 0 || errors.Len() != 0 || secondOutput.Len() != 0 || secondErrors.Len() != 0 { t.Fatal("prepared session streams were unexpectedly used") } } func TestMissingSessionTreeReturnsNeutralFailures(t *testing.T) { session := Prepare(nil, ParseOptions{}) if result := session.Parse(nil); result.Failure == nil || result.Failure.Message != "command tree is nil" { t.Fatalf("missing-tree parse = %#v", result) } if _, failure := session.RenderHelp(nil); failure == nil || failure.Message != "command tree is nil" { t.Fatalf("missing-tree help failure = %#v", failure) } if _, failure := session.UsageLine(nil); failure == nil || failure.Message != "command tree is nil" { t.Fatalf("missing-tree usage failure = %#v", failure) } }