package commandkit import ( "reflect" "testing" commandmeta "go.maximumdirect.net/commandkit/internal/commandmeta" ) func TestCompletionFlagAndValuePositions(t *testing.T) { parse := func(value string) (string, error) { return value, nil } choice := CompleteChoices(CompletionChoice{Value: "blue"}, CompletionChoice{Value: "green"}) command := NewCommand(CommandSpec[struct{}]{ Name: "run", Flags: []Flag[struct{}]{ ValueFlag(FlagMeta{Name: "color", Short: "c", Completion: choice}, (*string)(nil), parse, func(*struct{}, Supplied[string]) {}), RepeatFlag(FlagMeta{Name: "tag", Short: "t", Completion: choice}, nil, parse, func(*struct{}, Supplied[[]string]) {}), BoolFlag(FlagMeta{Name: "switch", Short: "s"}, false, func(*struct{}, Supplied[bool]) {}), }, Run: func(Invocation, struct{}) Outcome { return Success() }, }) app, err := New(Identity{Name: "example"}, command, FlagAliasFor("run", "--colour", "--color"), FlagAliasFor("run", "-C", "--color"), FlagAlias("-v", "--verbose")) if err != nil { t.Fatal(err) } snapshot := app.routeSnapshot() for _, tc := range []struct { name, current, prefix, value string completed []string kind completionTargetKind hint commandmeta.CompletionKind }{ {"pending takes option", "--quiet", "", "--quiet", []string{"run", "--color"}, completionTargetValue, commandmeta.CompletionChoices}, {"terminator consumed as value", "-x", "", "-x", []string{"run", "--color", "--", "--tag"}, completionTargetValue, commandmeta.CompletionChoices}, {"repeated singleton value", "--color=gr", "--color=", "gr", []string{"run", "--color=blue"}, completionTargetValue, commandmeta.CompletionChoices}, {"long alias value", "--colour=bl", "--colour=", "bl", []string{"run"}, completionTargetValue, commandmeta.CompletionChoices}, {"long empty value", "--color=", "--color=", "", []string{"run"}, completionTargetValue, commandmeta.CompletionChoices}, {"short alias value", "-sCbl", "-sC", "bl", []string{"run"}, completionTargetValue, commandmeta.CompletionChoices}, {"short equals", "-sC=bl", "-sC=", "bl", []string{"run"}, completionTargetValue, commandmeta.CompletionChoices}, {"reporting boolean", "--verbose=f", "--verbose=", "f", []string{"run"}, completionTargetValue, commandmeta.CompletionChoices}, {"short boolean alias", "-v=f", "-v=", "f", []string{"run"}, completionTargetValue, commandmeta.CompletionChoices}, {"application boolean", "--switch=f", "--switch=", "f", []string{"run"}, completionTargetValue, commandmeta.CompletionNone}, {"scoped alias unavailable at root", "--colour=bl", "", "", nil, completionTargetNone, commandmeta.CompletionNone}, {"unknown completed", "--c", "", "", []string{"run", "--unknown"}, completionTargetNone, commandmeta.CompletionNone}, {"unknown attached", "--unknown=x", "", "", []string{"run"}, completionTargetNone, commandmeta.CompletionNone}, {"terminator", "--color", "", "", []string{"run", "--"}, completionTargetNone, commandmeta.CompletionNone}, } { t.Run(tc.name, func(t *testing.T) { target := completionTargetFor(snapshot, resolveCompletionContext(snapshot, tc.completed, tc.current)) if target.kind != tc.kind || target.hint.Kind != tc.hint || target.prefix != tc.prefix || target.valuePrefix != tc.value { t.Fatalf("target=%#v", target) } }) } context := resolveCompletionContext(snapshot, []string{"run", "--colour=blue", "--tag=one", "--verbose"}, "--") target := completionTargetFor(snapshot, context) names := completionChoiceNames(target.choices) if containsCompletionName(names, "--color") || !containsCompletionName(names, "--tag") || !containsCompletionName(names, "--verbose") { t.Fatalf("flag suggestions=%q", names) } cluster := completionTargetFor(snapshot, resolveCompletionContext(snapshot, []string{"run"}, "-sv")) if cluster.kind != completionTargetFlags || !containsCompletionName(completionChoiceNames(cluster.choices), "-svc") || containsCompletionName(completionChoiceNames(cluster.choices), "-svs") { t.Fatalf("short cluster=%#v", cluster) } dash := completionTargetFor(snapshot, resolveCompletionContext(snapshot, []string{"run"}, "-")) if !containsCompletionName(completionChoiceNames(dash.choices), "--color") || !containsCompletionName(completionChoiceNames(dash.choices), "-c") { t.Fatalf("dash suggestions=%#v", dash) } } func TestCompletionOperandAllocation(t *testing.T) { arguments := []commandmeta.Argument{ {Name: "optional", Min: 0, Max: 1, Completion: commandmeta.CompletionHint{Kind: commandmeta.CompletionFiles}}, {Name: "repeated", Min: 0, Max: -1, Completion: commandmeta.CompletionHint{Kind: commandmeta.CompletionDirectories}}, {Name: "required", Min: 1, Max: 1, Completion: commandmeta.BooleanCompletion()}, } for _, tc := range []struct { completed int want commandmeta.CompletionKind }{ {0, commandmeta.CompletionChoices}, {1, commandmeta.CompletionChoices}, {2, commandmeta.CompletionChoices}, } { hint, ok := completionOperandHint(arguments, tc.completed) if !ok || hint.Kind != tc.want { t.Fatalf("%d operands: %#v, %v", tc.completed, hint, ok) } } bounded := []commandmeta.Argument{{Name: "only", Max: 1, Completion: commandmeta.BooleanCompletion()}} if _, ok := completionOperandHint(bounded, 1); ok { t.Fatal("excess operand received a hint") } trailing := []commandmeta.Argument{{Name: "first", Min: 1, Max: 1, Completion: commandmeta.BooleanCompletion()}, {Name: "rest", Min: 0, Max: -1, Completion: commandmeta.CompletionHint{Kind: commandmeta.CompletionDirectories}}} if hint, ok := completionOperandHint(trailing, 1); !ok || hint.Kind != commandmeta.CompletionDirectories { t.Fatalf("repeated trailing operand: %#v, %v", hint, ok) } if got, want := completionChoiceNames(commandmeta.BooleanCompletion().Choices), []string{"true", "false"}; !reflect.DeepEqual(got, want) { t.Fatalf("boolean choices=%q", got) } } func TestCompletionOperandAndFlagContexts(t *testing.T) { command := &commandmeta.Command{ Arguments: []commandmeta.Argument{{Name: "item", Min: 0, Max: 1, Completion: commandmeta.CompletionHint{Kind: commandmeta.CompletionFiles}}}, Flags: []commandmeta.Flag{{Name: "name", Completion: commandmeta.BooleanCompletion()}}, } for _, tc := range []struct { name, current string tail []string kind completionTargetKind hint commandmeta.CompletionKind }{ {"empty chooses operand", "", nil, completionTargetOperand, commandmeta.CompletionFiles}, {"dash chooses flag", "--n", nil, completionTargetFlags, commandmeta.CompletionNone}, {"after terminator", "--n", []string{"--"}, completionTargetOperand, commandmeta.CompletionFiles}, {"interspersed flag value", "value", []string{"--name", "--quiet"}, completionTargetOperand, commandmeta.CompletionFiles}, {"excess operand", "value", []string{"already"}, completionTargetNone, commandmeta.CompletionNone}, } { t.Run(tc.name, func(t *testing.T) { target := completionTargetFor(nil, completionContext{command: command, tail: tc.tail, current: tc.current}) if target.kind != tc.kind || target.hint.Kind != tc.hint { t.Fatalf("target=%#v", target) } }) } } func completionChoiceNames(choices []commandmeta.CompletionChoice) []string { var names []string for _, choice := range choices { names = append(names, choice.Value) } return names } func containsCompletionName(names []string, want string) bool { for _, name := range names { if name == want { return true } } return false }