forked from z2_cc/gitlink-cli
297 lines
7.0 KiB
Go
297 lines
7.0 KiB
Go
package interactive
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
|
)
|
|
|
|
// TestExecutor_CaptureOutput verifies that Execute captures stdout output
|
|
// even when the Run function writes directly to os.Stdout.
|
|
func TestExecutor_CaptureOutput(t *testing.T) {
|
|
e := &Executor{}
|
|
s := &common.Shortcut{
|
|
Name: "test",
|
|
Description: "test command",
|
|
Run: func(ctx *common.RuntimeContext) error {
|
|
// Do nothing — output should be empty
|
|
return nil
|
|
},
|
|
}
|
|
|
|
output, err := e.Execute(s, map[string]string{})
|
|
if err != nil {
|
|
t.Fatalf("Execute returned error: %v", err)
|
|
}
|
|
if output != "" {
|
|
t.Fatalf("expected empty output, got %q", output)
|
|
}
|
|
}
|
|
|
|
// TestExecutor_CaptureOutputPrints verifies that stdout written inside Run
|
|
// is captured and returned as a string.
|
|
func TestExecutor_CaptureOutputPrints(t *testing.T) {
|
|
e := &Executor{}
|
|
s := &common.Shortcut{
|
|
Name: "test",
|
|
Description: "test command",
|
|
Run: func(ctx *common.RuntimeContext) error {
|
|
fmt.Fprint(os.Stdout, "hello from command")
|
|
return nil
|
|
},
|
|
}
|
|
|
|
output, err := e.Execute(s, map[string]string{})
|
|
if err != nil {
|
|
t.Fatalf("Execute returned error: %v", err)
|
|
}
|
|
if output != "hello from command" {
|
|
t.Fatalf("expected %q, got %q", "hello from command", output)
|
|
}
|
|
}
|
|
|
|
// TestExecutor_RestoresStdout verifies that os.Stdout is restored after Execute.
|
|
func TestExecutor_RestoresStdout(t *testing.T) {
|
|
original := os.Stdout
|
|
e := &Executor{}
|
|
s := &common.Shortcut{
|
|
Name: "test",
|
|
Description: "test command",
|
|
Run: func(ctx *common.RuntimeContext) error {
|
|
return nil
|
|
},
|
|
}
|
|
|
|
_, _ = e.Execute(s, map[string]string{})
|
|
|
|
if os.Stdout != original {
|
|
t.Fatal("os.Stdout was not restored after Execute")
|
|
}
|
|
}
|
|
|
|
// TestParseDirectCommand uses table-driven tests for parseDirectCommand.
|
|
func TestParseDirectCommand(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
group string
|
|
cmd string
|
|
flagStr string
|
|
ok bool
|
|
}{
|
|
{
|
|
name: "empty string",
|
|
input: "",
|
|
ok: false,
|
|
},
|
|
{
|
|
name: "single word only",
|
|
input: "issue",
|
|
ok: false,
|
|
},
|
|
{
|
|
name: "second word without plus",
|
|
input: "issue list",
|
|
ok: false,
|
|
},
|
|
{
|
|
name: "two words with plus",
|
|
input: "issue +list",
|
|
group: "issue",
|
|
cmd: "list",
|
|
flagStr: "",
|
|
ok: true,
|
|
},
|
|
{
|
|
name: "with flags",
|
|
input: "issue +list --state open",
|
|
group: "issue",
|
|
cmd: "list",
|
|
flagStr: "--state open",
|
|
ok: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
group, cmd, flagStr, ok := parseDirectCommand(tt.input)
|
|
if ok != tt.ok {
|
|
t.Errorf("ok = %v, want %v", ok, tt.ok)
|
|
}
|
|
if ok {
|
|
if group != tt.group {
|
|
t.Errorf("group = %q, want %q", group, tt.group)
|
|
}
|
|
if cmd != tt.cmd {
|
|
t.Errorf("cmd = %q, want %q", cmd, tt.cmd)
|
|
}
|
|
if flagStr != tt.flagStr {
|
|
t.Errorf("flagStr = %q, want %q", flagStr, tt.flagStr)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestParseFlags uses table-driven tests for parseFlagString.
|
|
func TestParseFlags(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
flagStr string
|
|
shortMap map[string]string
|
|
want map[string]string
|
|
}{
|
|
{
|
|
name: "long flags only",
|
|
flagStr: "--title hello --body world",
|
|
shortMap: map[string]string{},
|
|
want: map[string]string{
|
|
"title": "hello",
|
|
"body": "world",
|
|
},
|
|
},
|
|
{
|
|
name: "short flag expanded",
|
|
flagStr: "-n 42",
|
|
shortMap: map[string]string{"n": "number"},
|
|
want: map[string]string{
|
|
"number": "42",
|
|
},
|
|
},
|
|
{
|
|
name: "equals syntax",
|
|
flagStr: "--title=hello",
|
|
shortMap: map[string]string{},
|
|
want: map[string]string{
|
|
"title": "hello",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := parseFlagString(tt.flagStr, tt.shortMap)
|
|
if len(got) != len(tt.want) {
|
|
t.Fatalf("got %d flags, want %d flags: %v", len(got), len(tt.want), got)
|
|
}
|
|
for k, v := range tt.want {
|
|
if got[k] != v {
|
|
t.Errorf("flag %q = %q, want %q", k, got[k], v)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFindShortcut covers three scenarios: found, wrong group, wrong cmd.
|
|
func TestFindShortcut(t *testing.T) {
|
|
listShortcut := &common.Shortcut{Name: "list", Description: "list issues"}
|
|
createShortcut := &common.Shortcut{Name: "create", Description: "create issue"}
|
|
all := map[string][]*common.Shortcut{
|
|
"issue": {listShortcut, createShortcut},
|
|
"pr": {{Name: "list", Description: "list PRs"}},
|
|
}
|
|
|
|
// Scenario 1: found
|
|
s, ok := findShortcut(all, "issue", "list")
|
|
if !ok {
|
|
t.Fatal("expected to find issue +list")
|
|
}
|
|
if s != listShortcut {
|
|
t.Fatal("returned wrong shortcut")
|
|
}
|
|
|
|
// Scenario 2: group not found
|
|
_, ok = findShortcut(all, "repo", "list")
|
|
if ok {
|
|
t.Fatal("expected not found for unknown group")
|
|
}
|
|
|
|
// Scenario 3: group found but cmd not found
|
|
_, ok = findShortcut(all, "issue", "close")
|
|
if ok {
|
|
t.Fatal("expected not found for unknown cmd in valid group")
|
|
}
|
|
}
|
|
|
|
// TestBuildShortMap verifies short-to-long flag mapping construction.
|
|
func TestBuildShortMap(t *testing.T) {
|
|
s := &common.Shortcut{
|
|
Name: "list",
|
|
Flags: []common.Flag{
|
|
{Name: "state", Short: "s"},
|
|
{Name: "number", Short: "n"},
|
|
{Name: "verbose"},
|
|
},
|
|
}
|
|
|
|
m := buildShortMap(s)
|
|
if len(m) != 2 {
|
|
t.Fatalf("expected 2 entries in short map, got %d", len(m))
|
|
}
|
|
if m["s"] != "state" {
|
|
t.Errorf("short 's' = %q, want 'state'", m["s"])
|
|
}
|
|
if m["n"] != "number" {
|
|
t.Errorf("short 'n' = %q, want 'number'", m["n"])
|
|
}
|
|
}
|
|
|
|
// TestMissingRequiredFlags checks that required-but-missing flags are reported.
|
|
func TestMissingRequiredFlags(t *testing.T) {
|
|
s := &common.Shortcut{
|
|
Name: "create",
|
|
Flags: []common.Flag{
|
|
{Name: "title", Required: true},
|
|
{Name: "body", Required: true},
|
|
{Name: "state", Required: false},
|
|
},
|
|
}
|
|
|
|
// Provide title but not body
|
|
args := map[string]string{"title": "hello"}
|
|
missing := missingRequiredFlags(s, args)
|
|
|
|
if len(missing) != 1 {
|
|
t.Fatalf("expected 1 missing flag, got %d: %v", len(missing), missing)
|
|
}
|
|
if missing[0] != "--body" {
|
|
t.Errorf("missing flag = %q, want '--body'", missing[0])
|
|
}
|
|
|
|
// Provide all required
|
|
args2 := map[string]string{"title": "hello", "body": "world"}
|
|
missing2 := missingRequiredFlags(s, args2)
|
|
if len(missing2) != 0 {
|
|
t.Fatalf("expected 0 missing flags, got %d: %v", len(missing2), missing2)
|
|
}
|
|
}
|
|
|
|
// TestFormatCommandDisplay verifies the display string format.
|
|
func TestFormatCommandDisplay(t *testing.T) {
|
|
s := &common.Shortcut{
|
|
Name: "create",
|
|
Description: "create an issue",
|
|
}
|
|
args := map[string]string{
|
|
"title": "hello",
|
|
"body": "world",
|
|
}
|
|
|
|
got := formatCommandDisplay("issue", s, args)
|
|
|
|
// The order of flags may vary, so check that all parts are present
|
|
if !strings.HasPrefix(got, "issue +create") {
|
|
t.Fatalf("expected prefix 'issue +create', got %q", got)
|
|
}
|
|
if !strings.Contains(got, "--title hello") {
|
|
t.Fatalf("expected '--title hello' in output, got %q", got)
|
|
}
|
|
if !strings.Contains(got, "--body world") {
|
|
t.Fatalf("expected '--body world' in output, got %q", got)
|
|
}
|
|
}
|