225 lines
6.8 KiB
Go
225 lines
6.8 KiB
Go
// exitcode_test.go 驗證結束碼映射:用法錯誤 → 2、API 錯誤 → 3、其他 → 1,
|
||
// 以及全域選項剖析(README「輸出與結束碼」「全域介面」)。
|
||
package cli
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
|
||
"gitea.alterminal.com/alterminal/teai/internal/gitea"
|
||
)
|
||
|
||
func TestExitCodeMapping(t *testing.T) {
|
||
// want 直接寫 README「輸出與結束碼」的數值(0/2/3;內部錯誤 1),
|
||
// 不以常數互比,避免常數與規格再次脫鉤(#10)。
|
||
cases := []struct {
|
||
name string
|
||
err error
|
||
want int
|
||
}{
|
||
{"usage error", &ErrUsage{Msg: "bad flag"}, 2},
|
||
{"api error 401", &gitea.ErrAPI{StatusCode: 401, Op: "GET /api/v1/user"}, 3},
|
||
{"api error transport", &gitea.ErrAPI{Op: "GET /api/v1/user", Err: errString("dial tcp")}, 3},
|
||
{"wrapped api error", wrapped(&gitea.ErrAPI{StatusCode: 500}), 3},
|
||
{"other error", errString("boom"), 1},
|
||
{"nil maps to OK", nil, 0},
|
||
}
|
||
for _, tc := range cases {
|
||
if got := int(exitCodeFor(tc.err)); got != tc.want {
|
||
t.Errorf("%s: exitCodeFor = %d, want %d", tc.name, got, tc.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestRunExitCodes(t *testing.T) {
|
||
t.Run("unknown command exits 2", func(t *testing.T) {
|
||
_, _, code := run("no-such")
|
||
if code != 2 {
|
||
t.Fatalf("code = %d, want 2", code)
|
||
}
|
||
})
|
||
t.Run("unknown global option exits 2", func(t *testing.T) {
|
||
_, _, code := run("--wat", "version")
|
||
if code != 2 {
|
||
t.Fatalf("code = %d, want 2", code)
|
||
}
|
||
})
|
||
t.Run("missing option value exits 2", func(t *testing.T) {
|
||
_, _, code := run("--output")
|
||
if code != 2 {
|
||
t.Fatalf("code = %d, want 2", code)
|
||
}
|
||
})
|
||
t.Run("invalid output value exits 2", func(t *testing.T) {
|
||
_, _, code := run("--output=yaml", "version")
|
||
if code != 2 {
|
||
t.Fatalf("code = %d, want 2", code)
|
||
}
|
||
})
|
||
t.Run("invalid timeout exits 2", func(t *testing.T) {
|
||
_, _, code := run("--timeout=soon", "version")
|
||
if code != 2 {
|
||
t.Fatalf("code = %d, want 2", code)
|
||
}
|
||
})
|
||
t.Run("version still works with globals", func(t *testing.T) {
|
||
stdout, _, code := run("--output=table", "--timeout=10s", "version")
|
||
if code != 0 || !strings.Contains(stdout, "teai version ") {
|
||
t.Fatalf("code = %d stdout = %q", code, stdout)
|
||
}
|
||
})
|
||
}
|
||
|
||
func TestExtractGlobals(t *testing.T) {
|
||
g := defaultGlobals()
|
||
rest, err := extractGlobals(&g, []string{
|
||
"--url", "https://example.com",
|
||
"--token=tok",
|
||
"--config", "/tmp/c.yml",
|
||
"--output", "table",
|
||
"--timeout", "45s",
|
||
"next",
|
||
}, nil)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(rest) != 1 || rest[0] != "next" {
|
||
t.Fatalf("rest = %v, want [next]", rest)
|
||
}
|
||
if g.URL != "https://example.com" || g.Token != "tok" || g.ConfigPath != "/tmp/c.yml" {
|
||
t.Fatalf("globals = %+v", g)
|
||
}
|
||
if g.Output != gitea.FormatTable {
|
||
t.Fatalf("output = %v, want table", g.Output)
|
||
}
|
||
if g.Timeout.String() != "45s" {
|
||
t.Fatalf("timeout = %s", g.Timeout)
|
||
}
|
||
}
|
||
|
||
func TestExtractGlobalsAfterCommand(t *testing.T) {
|
||
// 全域選項出現在命令之後也應抽出(README:teai next --output table)。
|
||
g := defaultGlobals()
|
||
rest, err := extractGlobals(&g, []string{"version", "--output", "table"}, nil)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(rest) != 1 || rest[0] != "version" {
|
||
t.Fatalf("rest = %v, want [version]", rest)
|
||
}
|
||
if g.Output != gitea.FormatTable {
|
||
t.Fatalf("output = %v, want table", g.Output)
|
||
}
|
||
}
|
||
|
||
func TestExtractGlobalsKeepsCommandFlags(t *testing.T) {
|
||
// 非全域選項屬於子命令:原樣依序交回,不能攔成 ErrUsage。
|
||
cases := [][]string{
|
||
{"members", "--has-work"},
|
||
{"pulls", "--mine"},
|
||
{"pulls", "--reviewer"},
|
||
{"pulls", "--repo", "alterminal/teai"},
|
||
{"members", "--has-work", "--output", "table"},
|
||
{"pulls", "--mine", "--repo", "alterminal/teai", "--output", "table"},
|
||
}
|
||
for _, args := range cases {
|
||
g := defaultGlobals()
|
||
rest, err := extractGlobals(&g, args, nil)
|
||
if err != nil {
|
||
t.Fatalf("extractGlobals(%v) 不應報錯:%v", args, err)
|
||
}
|
||
want := args
|
||
if strings.HasSuffix(strings.Join(args, " "), "--output table") {
|
||
want = args[:len(args)-2]
|
||
}
|
||
if len(rest) != len(want) {
|
||
t.Fatalf("extractGlobals(%v) rest = %v, want %v", args, rest, want)
|
||
}
|
||
for i := range want {
|
||
if rest[i] != want[i] {
|
||
t.Fatalf("extractGlobals(%v) rest = %v, want %v", args, rest, want)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestExtractGlobalsDoubleDashStops(t *testing.T) {
|
||
g := defaultGlobals()
|
||
rest, err := extractGlobals(&g, []string{"--output", "table", "--", "--url", "x"}, nil)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// -- 之後全部交回,--url 不再視為全域選項。
|
||
if len(rest) != 2 || rest[0] != "--url" || rest[1] != "x" {
|
||
t.Fatalf("rest = %v", rest)
|
||
}
|
||
if g.Output != gitea.FormatTable {
|
||
t.Fatalf("output = %v, want table", g.Output)
|
||
}
|
||
}
|
||
|
||
// TestExtractGlobalsReservedFlags 驗證 #18:與全域同名的命令旗標
|
||
// (登記於 commandReservedFlags)必須留給命令,其餘全域選項照常抽取。
|
||
func TestExtractGlobalsReservedFlags(t *testing.T) {
|
||
g := defaultGlobals()
|
||
rest, err := extractGlobals(&g, []string{
|
||
"add",
|
||
"--url", "https://srv.example.com", // login add 自有旗標:保留
|
||
"--token=tok", // 同上(inline 形式):保留
|
||
"--name", "test-site",
|
||
"--output", "table", // 非保留:仍是全域
|
||
}, reservedFlags("login"))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
want := []string{"add", "--url", "https://srv.example.com", "--token=tok", "--name", "test-site"}
|
||
if len(rest) != len(want) {
|
||
t.Fatalf("rest = %v, want %v", rest, want)
|
||
}
|
||
for i := range want {
|
||
if rest[i] != want[i] {
|
||
t.Fatalf("rest = %v, want %v", rest, want)
|
||
}
|
||
}
|
||
if g.Output != gitea.FormatTable {
|
||
t.Fatalf("output = %v, want table", g.Output)
|
||
}
|
||
if g.URL != defaultGlobals().URL || g.Token != "" {
|
||
t.Fatalf("reserved flags must not leak into globals: %+v", g)
|
||
}
|
||
}
|
||
|
||
// TestExtractGlobalsNonReservedCommand 驗證 #13 行為不回歸:未登記保留
|
||
// 旗標的命令(如 next/members/pulls),命令後的全域選項照常抽取、
|
||
// 命令自己的旗標(--has-work 等)原樣保留。
|
||
func TestExtractGlobalsNonReservedCommand(t *testing.T) {
|
||
g := defaultGlobals()
|
||
rest, err := extractGlobals(&g, []string{
|
||
"members", "--has-work", "--output", "table",
|
||
}, reservedFlags("members"))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(rest) != 2 || rest[0] != "members" || rest[1] != "--has-work" {
|
||
t.Fatalf("rest = %v, want [members --has-work]", rest)
|
||
}
|
||
if g.Output != gitea.FormatTable {
|
||
t.Fatalf("output = %v, want table", g.Output)
|
||
}
|
||
}
|
||
|
||
// errString 把字串轉成 error(測試輔助)。
|
||
type errStr string
|
||
|
||
func (e errStr) Error() string { return string(e) }
|
||
|
||
func errString(s string) error { return errStr(s) }
|
||
|
||
// wrapped 包一層 error(測試 errors.As 穿透)。
|
||
type wrappedErr struct{ inner error }
|
||
|
||
func (w wrappedErr) Error() string { return "wrapped: " + w.inner.Error() }
|
||
func (w wrappedErr) Unwrap() error { return w.inner }
|
||
|
||
func wrapped(inner error) error { return wrappedErr{inner} }
|