Skip to content

Commit dae1ca4

Browse files
committed
Add powershell test and fixes. (#90)
1 parent e4947f1 commit dae1ca4

7 files changed

Lines changed: 432 additions & 286 deletions

src/completion-scripts_.toit

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@
22
// Use of this source code is governed by an MIT-style license that can be
33
// found in the package's LICENSE file.
44
5+
import fs
6+
57
/**
6-
Extracts the basename from the given $path, stripping any directory components.
8+
Extracts the basename from the given $path, stripping any directory components
9+
and the .exe extension on Windows.
710
*/
811
basename_ path/string -> string:
9-
slash := path.index-of --last "/"
10-
if slash >= 0: return path[slash + 1..]
11-
return path
12+
name := fs.basename path
13+
// Strip .exe suffix so that completions work on Windows where
14+
// system.program-path includes the extension but users type without it.
15+
if name.ends-with ".exe": name = name[..name.size - 4]
16+
return name
1217

1318
/**
1419
Sanitizes the given $name for use as a shell function name.
@@ -175,12 +180,19 @@ powershell-completion-script_ --program-path/string -> string:
175180
param(\$wordToComplete, \$commandAst, \$cursorPosition)
176181
177182
\$tokens = \$commandAst.ToString() -split '\\s+'
178-
\$args = \$tokens[1..(\$tokens.Length - 1)]
183+
if (\$tokens.Length -gt 1) {
184+
\$completionArgs = \$tokens[1..(\$tokens.Length - 1)]
185+
} else {
186+
\$completionArgs = @()
187+
}
188+
if (\$completionArgs.Length -eq 0 -or \$completionArgs[-1] -ne \$wordToComplete) {
189+
\$completionArgs += \$wordToComplete
190+
}
179191
180-
\$output = & $program-path __complete -- @args 2>\$null
181-
if (\$LASTEXITCODE -ne 0) { return }
192+
\$output = & '$program-path' __complete -- @completionArgs 2>\$null
193+
if (\$LASTEXITCODE -ne 0 -or -not \$output) { return }
182194
183-
\$lines = \$output -split '\\n'
195+
\$lines = \$output -split '\\r?\\n'
184196
\$directive = (\$lines[-1] -replace '^:', '')
185197
\$lines = \$lines[0..(\$lines.Length - 2)]
186198
@@ -193,6 +205,7 @@ powershell-completion-script_ --program-path/string -> string:
193205
\$value = \$line
194206
\$desc = \$line
195207
}
208+
if (\$value -notlike "\$wordToComplete*") { continue }
196209
[System.Management.Automation.CompletionResult]::new(
197210
\$value,
198211
\$value,

tests/completion_shell.toit

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright (C) 2026 Toit contributors.
2+
// Use of this source code is governed by a Zero-Clause BSD license that can
3+
// be found in the tests/LICENSE file.
4+
5+
import fs
6+
import host.directory
7+
import host.file
8+
import host.pipe
9+
import system
10+
11+
/**
12+
A tmux session wrapper for testing interactive shell completions.
13+
*/
14+
class Tmux:
15+
/** The socket name, unique per session to avoid server conflicts. */
16+
socket-name/string
17+
18+
constructor .socket-name --shell-cmd/List --width/int=200 --height/int=50:
19+
args := [
20+
"tmux",
21+
"-L", socket-name, // Use a dedicated server socket.
22+
"new-session",
23+
"-d", // Detached.
24+
"-s", socket-name,
25+
"-x", "$width",
26+
"-y", "$height",
27+
] + shell-cmd
28+
exit-code := pipe.run-program args
29+
if exit-code != 0:
30+
throw "tmux new-session failed with exit code $exit-code for shell '$shell-cmd'"
31+
// Wait for the shell to initialize.
32+
send-line "echo tmux-ready"
33+
wait-for "tmux-ready"
34+
35+
/**
36+
Sends keystrokes to the tmux session.
37+
Each argument is a tmux key name (e.g. "Enter", "Tab", "C-c").
38+
*/
39+
send-keys keys/List -> none:
40+
pipe.run-program ["tmux", "-L", socket-name, "send-keys", "-t", socket-name] + keys
41+
42+
/** Sends text followed by Enter. */
43+
send-line text/string -> none:
44+
send-keys [text, "Enter"]
45+
46+
/** Sends Ctrl-C to cancel the current line, then waits for the shell to be ready. */
47+
cancel -> none:
48+
send-keys ["C-c"]
49+
// Echo a marker and wait for it so we know the shell is ready.
50+
marker := "ready-$Time.monotonic-us"
51+
send-line "echo $marker"
52+
wait-for marker
53+
54+
/** Captures the current pane content as a string. */
55+
capture -> string:
56+
return pipe.backticks ["tmux", "-L", socket-name, "capture-pane", "-t", socket-name, "-p"]
57+
58+
/**
59+
Waits until the pane contains the given $expected string, or throws on timeout.
60+
*/
61+
wait-for expected/string --timeout-ms/int=10000 -> none:
62+
deadline := Time.monotonic-us + timeout-ms * 1000
63+
delay-ms := 10
64+
while Time.monotonic-us < deadline:
65+
content := capture
66+
if content.contains expected: return
67+
sleep --ms=delay-ms
68+
delay-ms = min 500 (delay-ms * 2)
69+
content := capture
70+
throw "Timed out waiting for '$expected' in tmux pane. Content:\n$content"
71+
72+
/** Kills the tmux server (and its session). */
73+
close -> none:
74+
catch: pipe.run-program ["tmux", "-L", socket-name, "kill-server"]
75+
76+
// Unique session prefix for this test run.
77+
session-id_ := 0
78+
79+
next-session-name_ -> string:
80+
session-id_++
81+
return "completion-test-$Time.monotonic-us-$session-id_"
82+
83+
with-tmp-dir [block]:
84+
tmpdir := directory.mkdtemp "/tmp/completion-shell-test-"
85+
try:
86+
block.call tmpdir
87+
finally:
88+
directory.rmdir --recursive --force tmpdir
89+
90+
has-command_ name/string -> bool:
91+
exit-code := pipe.run-program ["which", name]
92+
return exit-code == 0
93+
94+
/**
95+
Compiles the test app binary and creates OptionPath test artifacts.
96+
Returns the path to the compiled binary.
97+
The $tmpdir must already exist.
98+
*/
99+
setup-test-binary_ tmpdir/string -> string:
100+
test-dir := fs.dirname system.program-path
101+
app-source := "$test-dir/completion_shell_test_app.toit"
102+
binary := "$tmpdir/fleet"
103+
if system.platform == system.PLATFORM-WINDOWS:
104+
binary = "$tmpdir/fleet.exe"
105+
print "Compiling test app..."
106+
pipe.run-program ["toit", "compile", "-o", binary, app-source]
107+
print "Binary compiled: $binary"
108+
109+
// Create artifacts for OptionPath completion testing.
110+
file.write-contents --path="$tmpdir/xfirmware.bin" ""
111+
directory.mkdir "$tmpdir/xreleases"
112+
113+
return binary
114+
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (C) 2026 Toit contributors.
2+
// Use of this source code is governed by a Zero-Clause BSD license that can
3+
// be found in the tests/LICENSE file.
4+
5+
import expect show *
6+
import host.pipe
7+
import .completion_shell
8+
9+
main:
10+
if not has-command_ "tmux":
11+
print "tmux not found, skipping bash completion tests."
12+
return
13+
14+
// Bash 3.x (macOS default) lacks compopt and has limited programmable
15+
// completion support. Skip if bash is too old.
16+
bash-version := (pipe.backticks ["bash", "-c", "echo \$BASH_VERSINFO"]).trim
17+
if bash-version == "" or bash-version[0] < '4':
18+
print ""
19+
print "=== Skipping bash tests (bash $bash-version too old, need 4+) ==="
20+
return
21+
22+
with-tmp-dir: | tmpdir |
23+
binary := setup-test-binary_ tmpdir
24+
test-bash binary tmpdir
25+
26+
print ""
27+
print "All bash completion tests passed!"
28+
29+
test-bash binary/string tmpdir/string:
30+
print ""
31+
print "=== Testing bash completion ==="
32+
tmux := Tmux (next-session-name_) --shell-cmd=["bash", "--norc", "--noprofile"]
33+
try:
34+
tmux.send-line "source <($binary completion bash); echo sourced"
35+
tmux.wait-for "sourced"
36+
37+
// Subcommand completion (double Tab for ambiguous matches).
38+
tmux.send-keys ["$binary ", "Tab", "Tab"]
39+
tmux.wait-for "deploy"
40+
content := tmux.capture
41+
expect (content.contains "status")
42+
expect (content.contains "help")
43+
expect (content.contains "completion")
44+
tmux.cancel
45+
46+
// Unique prefix auto-completes inline.
47+
tmux.send-keys ["$binary dep", "Tab"]
48+
tmux.wait-for "deploy"
49+
tmux.cancel
50+
51+
// Enum value completion.
52+
tmux.send-keys ["$binary deploy --channel ", "Tab", "Tab"]
53+
tmux.wait-for "stable"
54+
content = tmux.capture
55+
expect (content.contains "beta")
56+
expect (content.contains "dev")
57+
tmux.cancel
58+
59+
// Short option -d triggers device completion.
60+
tmux.send-keys ["$binary deploy -d ", "Tab", "Tab"]
61+
tmux.wait-for "d3b07384"
62+
tmux.cancel
63+
64+
// OptionPath: file option falls back to file completion.
65+
tmux.send-line "cd $tmpdir && echo cd-done"
66+
tmux.wait-for "cd-done"
67+
tmux.send-keys ["$binary deploy --firmware xfirm", "Tab"]
68+
tmux.wait-for "xfirmware.bin"
69+
tmux.cancel
70+
71+
// OptionPath --directory: falls back to directory-only completion.
72+
tmux.send-keys ["$binary deploy --output-dir xrel", "Tab"]
73+
tmux.wait-for "xreleases"
74+
tmux.cancel
75+
76+
print " All bash tests passed."
77+
finally:
78+
tmux.close
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (C) 2026 Toit contributors.
2+
// Use of this source code is governed by a Zero-Clause BSD license that can
3+
// be found in the tests/LICENSE file.
4+
5+
import expect show *
6+
import .completion_shell
7+
8+
main:
9+
if not has-command_ "tmux":
10+
print "tmux not found, skipping fish completion tests."
11+
return
12+
13+
if not has-command_ "fish":
14+
print ""
15+
print "=== Skipping fish tests (fish not installed) ==="
16+
return
17+
18+
with-tmp-dir: | tmpdir |
19+
binary := setup-test-binary_ tmpdir
20+
test-fish binary tmpdir
21+
22+
print ""
23+
print "All fish completion tests passed!"
24+
25+
test-fish binary/string tmpdir/string:
26+
print ""
27+
print "=== Testing fish completion ==="
28+
tmux := Tmux (next-session-name_) --shell-cmd=["fish", "--no-config"]
29+
try:
30+
tmux.send-line "$binary completion fish | source; echo sourced"
31+
tmux.wait-for "sourced"
32+
33+
// Subcommand completion.
34+
tmux.send-keys ["$binary ", "Tab"]
35+
tmux.wait-for "deploy"
36+
content := tmux.capture
37+
expect (content.contains "status")
38+
tmux.cancel
39+
40+
// Enum value completion.
41+
tmux.send-keys ["$binary deploy --channel ", "Tab"]
42+
tmux.wait-for "stable"
43+
content = tmux.capture
44+
expect (content.contains "beta")
45+
tmux.cancel
46+
47+
// Device completion with descriptions.
48+
tmux.send-keys ["$binary deploy --device ", "Tab"]
49+
tmux.wait-for "Living Room Sensor"
50+
tmux.cancel
51+
52+
// OptionPath: file option falls back to file completion.
53+
tmux.send-line "cd $tmpdir && echo cd-done"
54+
tmux.wait-for "cd-done"
55+
tmux.send-keys ["$binary deploy --firmware xfirm", "Tab"]
56+
tmux.wait-for "xfirmware.bin"
57+
tmux.cancel
58+
59+
// OptionPath --directory: falls back to directory-only completion.
60+
tmux.send-keys ["$binary deploy --output-dir xrel", "Tab"]
61+
tmux.wait-for "xreleases"
62+
tmux.cancel
63+
64+
print " All fish tests passed."
65+
finally:
66+
tmux.close

0 commit comments

Comments
 (0)