-
Notifications
You must be signed in to change notification settings - Fork 41
Expand file tree
/
Copy pathinternal_test.go
More file actions
315 lines (266 loc) · 10.3 KB
/
Copy pathinternal_test.go
File metadata and controls
315 lines (266 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
package gcli
import (
"fmt"
"io"
"os"
"strings"
"testing"
"github.com/gookit/gcli/v3/gevent"
"github.com/gookit/gcli/v3/gflag"
"github.com/gookit/goutil/x/assert"
)
// white-box test: findCommandName 现为无副作用纯函数,逐分支覆盖。
func TestApp_findCommandName_pure(t *testing.T) {
is := assert.New(t)
app := NewApp(func(a *App) { a.ExitOnEnd = false })
app.Add(NewCommand("top", "top desc", func(c *Command) {
c.AddSubs(NewCommand("sub", "sub desc"))
}))
app.AddAliases("top", "tp")
t.Run("founded normal", func(t *testing.T) {
fc := app.findCommandName([]string{"top", "-a", "x"})
is.Eq(Founded, fc.state)
is.Eq("top", fc.name)
is.Eq("top", fc.raw)
is.Eq([]string{"-a", "x"}, fc.args)
})
t.Run("founded via alias", func(t *testing.T) {
fc := app.findCommandName([]string{"tp", "arg"})
is.Eq(Founded, fc.state)
is.Eq("top", fc.name)
is.Eq("tp", fc.raw)
is.Eq([]string{"arg"}, fc.args)
})
t.Run("founded via command-ID expands sub into args", func(t *testing.T) {
fc := app.findCommandName([]string{"top:sub", "x"})
is.Eq(Founded, fc.state)
is.Eq("top", fc.name)
is.Eq("top:sub", fc.raw)
is.Eq([]string{"sub", "x"}, fc.args)
})
t.Run("notfound option", func(t *testing.T) {
fc := app.findCommandName([]string{"-h"})
is.Eq(NotFound, fc.state)
is.Eq("", fc.name)
is.Eq("", fc.raw)
})
t.Run("notfound unknown name", func(t *testing.T) {
fc := app.findCommandName([]string{"nope", "x"})
is.Eq(NotFound, fc.state)
is.Eq("nope", fc.name)
is.Eq("nope", fc.raw)
is.Eq([]string{"x"}, fc.args)
})
t.Run("founded via default command on empty args", func(t *testing.T) {
app.SetDefaultCommand("top")
fc := app.findCommandName(nil)
is.Eq(Founded, fc.state)
is.Eq("top", fc.name)
is.Empty(fc.args)
app.SetDefaultCommand("")
})
t.Run("notfound empty on empty args without default", func(t *testing.T) {
fc := app.findCommandName(nil)
is.Eq(NotFound, fc.state)
is.Eq("", fc.name)
})
// 关键:上述调用不应改动 app 状态(无副作用)
is.Empty(app.args)
is.Eq("", app.inputName)
}
// newCompletionApp 构造一个用于补全测试的应用夹具:
// - 顶层命令: build(别名 b)、run、help 为内置;
// - build 含子命令 module(别名 mod) 与选项 --output/-o、--verbose/-v、
// 带候选值的 --format/-f、bool 选项 --force/-F;
// - run 含选项 --name/-n。
func newCompletionApp() *App {
app := NewApp(func(a *App) { a.ExitOnEnd = false })
build := NewCommand("build", "build desc", func(c *Command) {
var out, verbose, format string
var force bool
c.StrOpt(&out, "output", "o", "the output dir")
c.StrOpt(&verbose, "verbose", "v", "verbose mode")
// 带候选值的取值型选项, 用于选项值补全
c.StrOpt2(&format, "format,f", "output format", gflag.WithChoices("json", "yaml", "text"))
// bool 选项: 不取值, 其后补全应落到子命令
c.BoolOpt(&force, "force", "F", false, "force build")
c.AddSubs(NewCommand("module", "module desc", func(sc *Command) {
sc.Aliases = []string{"mod"}
}))
})
app.Add(build)
app.AddAliases("build", "b")
run := NewCommand("run", "run desc", func(c *Command) {
var name string
c.StrOpt(&name, "name", "n", "the name")
})
app.Add(run)
return app
}
func TestApp_resolveCompletion(t *testing.T) {
is := assert.New(t)
app := newCompletionApp()
t.Run("top level on empty words", func(t *testing.T) {
got := app.resolveCompletion(nil)
// 顶层命令名 + 别名 + help, 去重排序
is.Eq([]string{"b", "build", "help", "run"}, got)
})
t.Run("top level prefix filter", func(t *testing.T) {
got := app.resolveCompletion([]string{"b"})
// 前缀 b: 命中 b、build
is.Eq([]string{"b", "build"}, got)
})
t.Run("sub commands of a command", func(t *testing.T) {
got := app.resolveCompletion([]string{"build", ""})
// build 的子命令名 + 子命令别名
is.Eq([]string{"mod", "module"}, got)
})
t.Run("sub commands prefix filter", func(t *testing.T) {
got := app.resolveCompletion([]string{"build", "mod"})
is.Eq([]string{"mod", "module"}, got)
})
t.Run("option names when cur is dash", func(t *testing.T) {
got := app.resolveCompletion([]string{"build", "-"})
// build 的选项: 长选项 + 短选项(顶层全局选项不参与命令级补全)
is.Contains(got, "--output")
is.Contains(got, "-o")
is.Contains(got, "--verbose")
is.Contains(got, "-v")
})
t.Run("option names with long prefix", func(t *testing.T) {
got := app.resolveCompletion([]string{"build", "--o"})
is.Eq([]string{"--output"}, got)
})
t.Run("alias resolves then drill down", func(t *testing.T) {
// 顶层别名 b -> build, 应能下钻并产出 build 的子命令
got := app.resolveCompletion([]string{"b", ""})
is.Eq([]string{"mod", "module"}, got)
})
t.Run("sub command alias options", func(t *testing.T) {
// build mod -> build module, mod 别名解析后定位到 module 节点(无子命令、无选项)
got := app.resolveCompletion([]string{"build", "mod", ""})
is.Empty(got)
})
t.Run("stop after unknown word", func(t *testing.T) {
// arg 不是命令(视为参数), 上下文停留在 build, cur 为空 -> 仍补全 build 子命令
got := app.resolveCompletion([]string{"build", "arg", ""})
is.Eq([]string{"mod", "module"}, got)
})
t.Run("option words are skipped on locating", func(t *testing.T) {
// 中间的选项词 -o val 在定位时被跳过(注意: -o 跳过, val 视为参数会停止下钻),
// 这里用纯选项词验证跳过逻辑: build --verbose -> 仍在 build, 补全选项
got := app.resolveCompletion([]string{"build", "--verbose", "-"})
is.Contains(got, "--output")
is.Contains(got, "--verbose")
})
t.Run("run command options", func(t *testing.T) {
got := app.resolveCompletion([]string{"run", "-"})
is.Contains(got, "--name")
is.Contains(got, "-n")
})
t.Run("hidden global option excluded", func(t *testing.T) {
// 顶层选项补全: 可见全局选项可补全, 但隐藏的内部选项 --in-completion 不应出现
got := app.resolveCompletion([]string{"-"})
is.Contains(got, "--help")
is.Contains(got, "--gen-completion")
is.NotContains(got, "--in-completion")
})
t.Run("option value choices", func(t *testing.T) {
// build --format <cur> -> 补全该选项的候选值(去重排序)
got := app.resolveCompletion([]string{"build", "--format", ""})
is.Eq([]string{"json", "text", "yaml"}, got)
})
t.Run("option value choices prefix", func(t *testing.T) {
got := app.resolveCompletion([]string{"build", "--format", "j"})
is.Eq([]string{"json"}, got)
})
t.Run("option value choices via short", func(t *testing.T) {
// 短名 -f 同样解析到 --format 并补全候选值
got := app.resolveCompletion([]string{"build", "-f", ""})
is.Eq([]string{"json", "text", "yaml"}, got)
})
t.Run("value option without choices returns empty", func(t *testing.T) {
// --output 取值但无 Choices: 返回空(交给 shell 文件名补全), 不落到子命令
got := app.resolveCompletion([]string{"build", "--output", ""})
is.Empty(got)
})
t.Run("bool option falls through to subcommands", func(t *testing.T) {
// --force 是 bool(不取值), 其后应补全 build 的子命令
got := app.resolveCompletion([]string{"build", "--force", ""})
is.Eq([]string{"mod", "module"}, got)
})
}
// captureStdout 捕获 fn 执行期间写入 os.Stdout 的内容(showAutoCompletion 用 fmt.Println 直接写 stdout)。
func captureStdout(fn func()) string {
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
fn()
_ = w.Close()
os.Stdout = old
out, _ := io.ReadAll(r)
return string(out)
}
// TestApp_completionMode_suppressHooks 验证补全模式静默: 当本次运行是补全请求时,
// 用户 init 钩子(OnAppInitAfter)不被触发, 且 stdout 只剩候选, 不含钩子里的噪声输出。
//
// 注意: initialize() 常由首个 Add() 触发(早于 Run), 因此真实判定来自 os.Args。
// 这里临时改写 os.Args 以忠实复现真实执行路径(钩子在 NewApp config 中注册、Add 触发 init)。
func TestApp_completionMode_suppressHooks(t *testing.T) {
is := assert.New(t)
const noise = "init app event app.init.after"
hookFired := false
// 临时改写 os.Args 模拟真实补全请求: bin --in-completion build ""
oldArgs := os.Args
os.Args = []string{"app", "--in-completion", "build", ""}
defer func() { os.Args = oldArgs }()
// Run 会改写包级 gOpts 单例, 用例结束后重置避免污染其他用例
defer ResetGOpts()
// 在 NewApp 的 config 函数里注册钩子(早于 Add 触发的 initialize)
app := NewApp(func(a *App) {
a.ExitOnEnd = false
a.On(gevent.OnAppInitAfter, func(_ *HookCtx) bool {
hookFired = true
fmt.Println(noise) // 钩子里的噪声输出, 委托式脚本会误解析
return false
})
})
build := NewCommand("build", "build desc", func(c *Command) {
c.AddSubs(NewCommand("module", "module desc", func(sc *Command) {
sc.Aliases = []string{"mod"}
}))
})
// Add 会触发 initialize(); 此时应已根据 os.Args 进入 completion 模式, 钩子被抑制
app.Add(build)
// 补全请求: 当前正在补全 build 的子命令(末尾空词)
out := captureStdout(func() {
app.Run([]string{"--in-completion", "build", ""})
})
// ① 钩子未触发(completion 模式下 init 钩子被抑制)
is.False(hookFired, "OnAppInitAfter hook should NOT fire in completion mode")
// ② stdout 不含噪声, 只有候选
is.False(strings.Contains(out, noise), "stdout should not contain hook noise, got: %q", out)
// ③ 输出包含真实候选(build 的子命令 module/别名 mod)
is.True(strings.Contains(out, "module"), "stdout should contain candidate 'module', got: %q", out)
}
// TestApp_completionMode_hooksFireNormally 反向验证: 非补全模式下 init 钩子正常触发。
func TestApp_completionMode_hooksFireNormally(t *testing.T) {
is := assert.New(t)
// 确保 os.Args 不含补全元选项
oldArgs := os.Args
os.Args = []string{"app", "--version"}
defer func() { os.Args = oldArgs }()
defer ResetGOpts()
hookFired := false
app := NewApp(func(a *App) {
a.ExitOnEnd = false
a.On(gevent.OnAppInitAfter, func(_ *HookCtx) bool {
hookFired = true
return false
})
})
app.Add(NewCommand("build", "build desc", nil))
// 普通运行(无补全元选项): 钩子应正常触发
app.Run([]string{"--version"})
is.True(hookFired, "OnAppInitAfter hook SHOULD fire in normal mode")
}