mirror of
https://github.com/go-task/task.git
synced 2026-07-10 04:22:03 +02:00
test(completion): add cross-shell completion test suite and CI job
Add completion/tests/ harnesses that exercise the engine protocol and every shell wrapper without a TTY: the engine via `task __complete`, bash/zsh by stubbing the completion-system helpers, fish via `complete -C`, and PowerShell via the completion API. A `test:completion` task and a matching CI job (with a strict mode that fails when an expected shell is missing) run them all. Writing the suite surfaced and fixed real wrapper bugs: - fish: `math` has no bitwise `&`, so every directive check errored; test bits with integer division + modulo instead. Also filter FilterFileExt results ourselves, as __fish_complete_suffix only prioritizes the extension. - powershell: `Get-ChildItem -Include` is ignored without -Recurse, so file extension filtering returned nothing; filter with Where-Object instead. - bash: guard empty-array expansion so completion with zero candidates does not trip `set -u` on bash 3.2. Also name the completion directive bits (NO_SPACE, FILTER_FILE_EXT, …) in every wrapper instead of using raw numbers.
This commit is contained in:
33
.github/workflows/test.yml
vendored
33
.github/workflows/test.yml
vendored
@@ -31,3 +31,36 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
run: go run ./cmd/task test
|
||||
|
||||
completion:
|
||||
name: Completion
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{matrix.platform}}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version: 1.26.x
|
||||
|
||||
# zsh and pwsh are preinstalled on the runners; only fish is missing
|
||||
# (plus zsh on the Linux image).
|
||||
- name: Install shells (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get update && sudo apt-get install -y zsh fish
|
||||
|
||||
- name: Install shells (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
run: brew install fish
|
||||
|
||||
- name: Test completion
|
||||
# Strict mode fails the run if any shell is missing, so we never get a
|
||||
# false pass when a runner image stops shipping one (e.g. pwsh).
|
||||
env:
|
||||
TASK_COMPLETION_STRICT: "1"
|
||||
run: go run ./cmd/task test:completion
|
||||
|
||||
@@ -152,6 +152,15 @@ tasks:
|
||||
cmds:
|
||||
- gotestsum -f '{{.GOTESTSUM_FORMAT}}' -tags 'signals watch' ./...
|
||||
|
||||
test:completion:
|
||||
desc: Tests the shell completion engine and wrappers (bash, zsh, fish, powershell)
|
||||
sources:
|
||||
- internal/complete/**/*.go
|
||||
- cmd/task/**/*.go
|
||||
- completion/**/*
|
||||
cmds:
|
||||
- bash completion/tests/run.sh
|
||||
|
||||
goreleaser:test:
|
||||
desc: Tests release process without publishing
|
||||
cmds:
|
||||
|
||||
@@ -7,6 +7,10 @@ TASK_CMD="${TASK_EXE:-task}"
|
||||
|
||||
_task() {
|
||||
local cur prev words cword
|
||||
|
||||
# Completion directives, mirroring internal/complete/complete.go.
|
||||
local -ri NO_SPACE=2 NO_FILE_COMP=4 FILTER_FILE_EXT=8 FILTER_DIRS=16
|
||||
|
||||
# Exclude both `=` and `:` from the word breaks so `--output=` and
|
||||
# `docs:serve` reach the engine as single tokens.
|
||||
_init_completion -n =: || return
|
||||
@@ -36,16 +40,18 @@ _task() {
|
||||
local directive="${lines[$last_idx]#:}"
|
||||
unset 'lines[$last_idx]'
|
||||
|
||||
if (( directive & 8 )); then
|
||||
if (( directive & FILTER_FILE_EXT )); then
|
||||
local exts=""
|
||||
for line in "${lines[@]}"; do
|
||||
# ${arr[@]+…} guards against "unbound variable" on an empty array under
|
||||
# `set -u` in bash 3.2 (macOS).
|
||||
for line in ${lines[@]+"${lines[@]}"}; do
|
||||
exts+="${exts:+|}$line"
|
||||
done
|
||||
_filedir "@($exts)"
|
||||
return
|
||||
fi
|
||||
|
||||
if (( directive & 16 )); then
|
||||
if (( directive & FILTER_DIRS )); then
|
||||
_filedir -d
|
||||
return
|
||||
fi
|
||||
@@ -54,20 +60,20 @@ _task() {
|
||||
# word list on IFS, which mangles any suggestion value containing a space.
|
||||
local value
|
||||
COMPREPLY=()
|
||||
for line in "${lines[@]}"; do
|
||||
for line in ${lines[@]+"${lines[@]}"}; do
|
||||
value="${line%%$'\t'*}"
|
||||
if [[ -z "$cur" || "$value" == "$cur"* ]]; then
|
||||
COMPREPLY+=( "$value" )
|
||||
fi
|
||||
done
|
||||
|
||||
if (( directive & 2 )); then
|
||||
if (( directive & NO_SPACE )); then
|
||||
compopt -o nospace 2>/dev/null
|
||||
fi
|
||||
|
||||
__ltrim_colon_completions "$cur"
|
||||
|
||||
if (( ${#COMPREPLY[@]} == 0 )) && ! (( directive & 4 )); then
|
||||
if (( ${#COMPREPLY[@]} == 0 )) && ! (( directive & NO_FILE_COMP )); then
|
||||
_filedir
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -3,6 +3,19 @@
|
||||
|
||||
set -l GO_TASK_PROGNAME (if set -q GO_TASK_PROGNAME; echo $GO_TASK_PROGNAME; else if set -q TASK_EXE; echo $TASK_EXE; else; echo task; end)
|
||||
|
||||
# Completion directives, mirroring internal/complete/complete.go. fish's `math`
|
||||
# has no bitwise operators, so bits are stored as their power-of-two value and
|
||||
# tested with integer division + modulo via __task_test_bit.
|
||||
set -g __task_directive_no_space 2
|
||||
set -g __task_directive_no_file_comp 4
|
||||
set -g __task_directive_filter_file_ext 8
|
||||
set -g __task_directive_filter_dirs 16
|
||||
set -g __task_directive_keep_order 32
|
||||
|
||||
function __task_test_bit --argument-names value bit
|
||||
test (math "floor($value / $bit) % 2") -eq 1
|
||||
end
|
||||
|
||||
function __task_complete --inherit-variable GO_TASK_PROGNAME
|
||||
set -l tokens (commandline -opc)
|
||||
set -l current (commandline -ct)
|
||||
@@ -36,15 +49,27 @@ function __task_complete --inherit-variable GO_TASK_PROGNAME
|
||||
# served here, otherwise nothing is offered (e.g. `--cacert`, after `--`).
|
||||
|
||||
# FilterFileExt: the engine emits the allowed extensions as the data lines.
|
||||
if test (math "$directive & 8") -ne 0
|
||||
for ext in $data
|
||||
__fish_complete_suffix ".$ext"
|
||||
# __fish_complete_suffix only *prioritizes* the extension, so filter the file
|
||||
# list ourselves — keeping directories so the user can still descend into them.
|
||||
if __task_test_bit $directive $__task_directive_filter_file_ext
|
||||
for entry in (__fish_complete_path $current)
|
||||
set -l name (string split -f1 \t -- $entry)
|
||||
if string match -qr '/$' -- $name
|
||||
printf '%s\n' $entry
|
||||
continue
|
||||
end
|
||||
for ext in $data
|
||||
if string match -qr "\.$ext\$" -- $name
|
||||
printf '%s\n' $entry
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
# FilterDirs: complete directories only.
|
||||
if test (math "$directive & 16") -ne 0
|
||||
if __task_test_bit $directive $__task_directive_filter_dirs
|
||||
__fish_complete_directories $current
|
||||
return
|
||||
end
|
||||
@@ -55,9 +80,9 @@ function __task_complete --inherit-variable GO_TASK_PROGNAME
|
||||
printf '%s\n' $line
|
||||
end
|
||||
|
||||
# NoFileComp (bit 4) unset → also offer files, since `--no-files` suppressed
|
||||
# the native fallback. Covers DirectiveDefault (e.g. `--cacert`, after `--`).
|
||||
if test (math "$directive & 4") -eq 0
|
||||
# NoFileComp unset → also offer files, since `--no-files` suppressed the
|
||||
# native fallback. Covers DirectiveDefault (e.g. `--cacert`, after `--`).
|
||||
if not __task_test_bit $directive $__task_directive_no_file_comp
|
||||
__fish_complete_path $current
|
||||
end
|
||||
end
|
||||
|
||||
@@ -38,20 +38,31 @@ Register-ArgumentCompleter -Native -CommandName $cmdNames -ScriptBlock {
|
||||
$directive = [int]($last.Substring(1))
|
||||
$data = if ($lines.Count -gt 1) { $lines[0..($lines.Count - 2)] } else { @() }
|
||||
|
||||
# Completion directives, mirroring internal/complete/complete.go.
|
||||
$NoFileComp = 4
|
||||
$FilterFileExt = 8
|
||||
$FilterDirs = 16
|
||||
|
||||
# Note: DirectiveNoSpace (bit 2) cannot be honored here — PowerShell's
|
||||
# CompletionResult API has no per-item "no trailing space" option, so a
|
||||
# suggestion like `VAR=` gets a trailing space. This is a PowerShell limit.
|
||||
|
||||
# FilterFileExt
|
||||
if ($directive -band 8) {
|
||||
$patterns = $data | ForEach-Object { "*.$_" }
|
||||
return Get-ChildItem -Path . -Include $patterns -File -ErrorAction SilentlyContinue |
|
||||
ForEach-Object { [CompletionResult]::new($_.Name, $_.Name, [CompletionResultType]::ProviderItem, $_.Name) }
|
||||
# FilterFileExt: keep files whose extension matches, plus directories so the
|
||||
# user can still descend into them. `-Include` is unreliable without
|
||||
# `-Recurse`, so filter with Where-Object instead.
|
||||
if ($directive -band $FilterFileExt) {
|
||||
$exts = $data | ForEach-Object { ".$_" }
|
||||
return Get-ChildItem -Path "$wordToComplete*" -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.PSIsContainer -or $exts -contains $_.Extension } |
|
||||
ForEach-Object {
|
||||
$type = if ($_.PSIsContainer) { [CompletionResultType]::ProviderContainer } else { [CompletionResultType]::ProviderItem }
|
||||
[CompletionResult]::new($_.Name, $_.Name, $type, $_.Name)
|
||||
}
|
||||
}
|
||||
|
||||
# FilterDirs
|
||||
if ($directive -band 16) {
|
||||
return Get-ChildItem -Path . -Directory -ErrorAction SilentlyContinue |
|
||||
if ($directive -band $FilterDirs) {
|
||||
return Get-ChildItem -Path "$wordToComplete*" -Directory -ErrorAction SilentlyContinue |
|
||||
ForEach-Object { [CompletionResult]::new($_.Name, $_.Name, [CompletionResultType]::ProviderContainer, $_.Name) }
|
||||
}
|
||||
|
||||
@@ -68,7 +79,7 @@ Register-ArgumentCompleter -Native -CommandName $cmdNames -ScriptBlock {
|
||||
|
||||
# NoFileComp (bit 4) unset and nothing matched → fall back to file completion,
|
||||
# since the engine returned DirectiveDefault (e.g. --cacert, after `--`).
|
||||
if ($results.Count -eq 0 -and -not ($directive -band 4)) {
|
||||
if ($results.Count -eq 0 -and -not ($directive -band $NoFileComp)) {
|
||||
return Get-ChildItem -Path . -ErrorAction SilentlyContinue |
|
||||
ForEach-Object { [CompletionResult]::new($_.Name, $_.Name, [CompletionResultType]::ProviderItem, $_.Name) }
|
||||
}
|
||||
|
||||
85
completion/tests/engine.sh
Executable file
85
completion/tests/engine.sh
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests the `task __complete` protocol directly (shell-agnostic). This is the
|
||||
# backbone: it validates the candidates and directive the engine emits, which
|
||||
# is what drives every shell wrapper.
|
||||
#
|
||||
# Requires: TASK_BIN (path to the task binary), TASK_FIXTURE (dir with a
|
||||
# Taskfile.yml). Exits non-zero on the first failure.
|
||||
set -u
|
||||
|
||||
: "${TASK_BIN:?TASK_BIN must point to the task binary}"
|
||||
: "${TASK_FIXTURE:?TASK_FIXTURE must point to the fixture directory}"
|
||||
cd "$TASK_FIXTURE" || exit 1
|
||||
|
||||
fails=0
|
||||
out() { "$TASK_BIN" __complete "$@" 2>/dev/null; }
|
||||
vals() { out "$@" | sed '$d' | cut -f1; } # candidate values, sans the :N line
|
||||
dirv() { out "$@" | tail -1; } # the :N directive line
|
||||
|
||||
has() { # LABEL VALUE ARGS...
|
||||
local label=$1 value=$2; shift 2
|
||||
if vals "$@" | grep -qxF -- "$value"; then
|
||||
echo " ok $label"
|
||||
else
|
||||
echo " FAIL $label — expected value '$value' among: $(vals "$@" | tr '\n' ' ')"
|
||||
fails=$((fails + 1))
|
||||
fi
|
||||
}
|
||||
hasnot() { # LABEL VALUE ARGS...
|
||||
local label=$1 value=$2; shift 2
|
||||
if vals "$@" | grep -qxF -- "$value"; then
|
||||
echo " FAIL $label — value '$value' should be absent"
|
||||
fails=$((fails + 1))
|
||||
else
|
||||
echo " ok $label"
|
||||
fi
|
||||
}
|
||||
directive() { # LABEL EXPECTED ARGS...
|
||||
local label=$1 expected=$2; shift 2
|
||||
local got; got=$(dirv "$@")
|
||||
if [[ "$got" == "$expected" ]]; then
|
||||
echo " ok $label"
|
||||
else
|
||||
echo " FAIL $label — expected directive '$expected', got '$got'"
|
||||
fails=$((fails + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
echo "engine: task names"
|
||||
has "lists tasks" build ''
|
||||
has "lists aliases" dep ''
|
||||
directive "NoFileComp" ':4' ''
|
||||
|
||||
echo "engine: completion-control flags"
|
||||
hasnot "--no-aliases drops aliases" dep --no-aliases ''
|
||||
has "--no-aliases keeps tasks" deploy --no-aliases ''
|
||||
|
||||
echo "engine: flags"
|
||||
has "lists flags" --taskfile -
|
||||
directive "flags NoFileComp" ':4' -
|
||||
|
||||
echo "engine: flag values"
|
||||
has "inline --output= is full form" --output=interleaved --output=
|
||||
directive "inline NoFileComp" ':4' --output=
|
||||
has "separate --output is bare" interleaved --output ''
|
||||
has "--sort values" alphanumeric --sort ''
|
||||
|
||||
echo "engine: file/dir directives"
|
||||
has "--taskfile emits yml" yml --taskfile ''
|
||||
has "--taskfile emits yaml" yaml --taskfile ''
|
||||
directive "--taskfile FilterFileExt" ':8' --taskfile ''
|
||||
directive "--dir FilterDirs" ':16' --dir ''
|
||||
|
||||
echo "engine: task variables"
|
||||
has "required var with enum" ENV=dev deploy ''
|
||||
has "required var without enum" REGION= deploy ''
|
||||
directive "vars NoSpace|NoFileComp|KeepOrder" ':38' deploy ''
|
||||
|
||||
echo "engine: after --"
|
||||
directive "after -- is default" ':0' deploy -- ''
|
||||
|
||||
if (( fails )); then
|
||||
echo "engine: $fails failure(s)"
|
||||
exit 1
|
||||
fi
|
||||
echo "engine: all passed"
|
||||
91
completion/tests/run.sh
Executable file
91
completion/tests/run.sh
Executable file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs the completion test suite: builds the task binary, creates a fixture
|
||||
# Taskfile with sample files and directories, then exercises the engine and
|
||||
# every installed shell wrapper. Skips shells that are not installed.
|
||||
set -u
|
||||
|
||||
here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
root=$(cd "$here/../.." && pwd)
|
||||
|
||||
# Build the binary under test.
|
||||
bindir=$(mktemp -d)
|
||||
if ! go build -o "$bindir/task" "$root/cmd/task"; then
|
||||
echo "failed to build task binary" >&2
|
||||
exit 1
|
||||
fi
|
||||
export TASK_BIN="$bindir/task"
|
||||
# fish and PowerShell register completion for the command name `task`, so make
|
||||
# `task` on PATH resolve to the binary under test.
|
||||
export PATH="$bindir:$PATH"
|
||||
|
||||
# Fixture: a Taskfile plus files/dirs so file/dir completion has real entries.
|
||||
fixture=$(mktemp -d)
|
||||
cat > "$fixture/Taskfile.yml" <<'YML'
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
desc: Build it
|
||||
deploy:
|
||||
desc: Deploy it
|
||||
aliases: [dep]
|
||||
requires:
|
||||
vars:
|
||||
- name: ENV
|
||||
enum: [dev, prod]
|
||||
- REGION
|
||||
docs:serve:
|
||||
desc: Serve docs
|
||||
YML
|
||||
touch "$fixture/extra.yaml" "$fixture/notes.txt"
|
||||
mkdir -p "$fixture/sub" "$fixture/other"
|
||||
export TASK_FIXTURE="$fixture"
|
||||
|
||||
# In strict mode (set TASK_COMPLETION_STRICT=1, used in CI) a missing shell is
|
||||
# a failure instead of a skip, so we never get a false pass when a shell the
|
||||
# environment was expected to provide (e.g. pwsh on CI runners) is absent.
|
||||
strict=${TASK_COMPLETION_STRICT:-}
|
||||
|
||||
fails=0
|
||||
run() { # LABEL COMMAND...
|
||||
echo "== $1 =="
|
||||
if "${@:2}"; then :; else fails=$((fails + 1)); fi
|
||||
echo
|
||||
}
|
||||
skip() { # LABEL
|
||||
if [[ -n "$strict" ]]; then
|
||||
echo "== $1 == (MISSING — required under TASK_COMPLETION_STRICT)"
|
||||
fails=$((fails + 1))
|
||||
else
|
||||
echo "== $1 == (skipped: not installed)"
|
||||
fi
|
||||
echo
|
||||
}
|
||||
|
||||
run "engine" bash "$here/engine.sh"
|
||||
run "bash wrapper" bash "$here/wrapper.bash"
|
||||
|
||||
if command -v zsh >/dev/null 2>&1; then
|
||||
run "zsh wrapper" zsh "$here/wrapper.zsh"
|
||||
else
|
||||
skip "zsh wrapper"
|
||||
fi
|
||||
|
||||
if command -v fish >/dev/null 2>&1; then
|
||||
run "fish wrapper" fish "$here/wrapper.fish"
|
||||
else
|
||||
skip "fish wrapper"
|
||||
fi
|
||||
|
||||
pwsh_bin=$(command -v pwsh || command -v pwsh-preview || true)
|
||||
if [[ -n "$pwsh_bin" ]]; then
|
||||
run "powershell wrapper" "$pwsh_bin" -NoProfile -File "$here/wrapper.ps1"
|
||||
else
|
||||
skip "powershell wrapper"
|
||||
fi
|
||||
|
||||
if ((fails)); then
|
||||
echo "completion tests: $fails suite(s) failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "completion tests: all suites passed"
|
||||
86
completion/tests/wrapper.bash
Executable file
86
completion/tests/wrapper.bash
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests the bash wrapper by stubbing the bash-completion helpers it calls
|
||||
# (_init_completion / _filedir / compopt / __ltrim_colon_completions) and
|
||||
# asserting the resulting COMPREPLY and file routing. Deterministic, no TTY,
|
||||
# and works without the bash-completion package installed.
|
||||
#
|
||||
# Requires: TASK_BIN (task binary), TASK_FIXTURE (dir with a Taskfile.yml).
|
||||
set -u
|
||||
|
||||
: "${TASK_BIN:?}"; : "${TASK_FIXTURE:?}"
|
||||
export TASK_EXE="$TASK_BIN"
|
||||
cd "$TASK_FIXTURE" || exit 1
|
||||
|
||||
fails=0
|
||||
CAP=""
|
||||
|
||||
# Stubs standing in for the bash-completion runtime.
|
||||
_init_completion() {
|
||||
words=("${TEST_WORDS[@]}")
|
||||
cword=$TEST_CWORD
|
||||
cur="${TEST_WORDS[$TEST_CWORD]}"
|
||||
prev="${TEST_WORDS[$((TEST_CWORD - 1))]}"
|
||||
return 0
|
||||
}
|
||||
_filedir() { CAP+="filedir:$*"$'\n'; }
|
||||
compopt() { CAP+="compopt:$*"$'\n'; }
|
||||
__ltrim_colon_completions() { :; }
|
||||
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/../bash/task.bash"
|
||||
|
||||
run() {
|
||||
CAP=""
|
||||
TEST_WORDS=("$@")
|
||||
TEST_CWORD=$((${#TEST_WORDS[@]} - 1))
|
||||
COMPREPLY=()
|
||||
_task
|
||||
}
|
||||
|
||||
reply_has() { # LABEL VALUE
|
||||
local v
|
||||
for v in "${COMPREPLY[@]}"; do [[ "$v" == "$2" ]] && { echo " ok $1"; return; }; done
|
||||
echo " FAIL $1 — '$2' missing from COMPREPLY: ${COMPREPLY[*]}"
|
||||
fails=$((fails + 1))
|
||||
}
|
||||
cap_has() { # LABEL PATTERN
|
||||
if [[ "$CAP" == *"$2"* ]]; then echo " ok $1"; else
|
||||
echo " FAIL $1 — expected '$2' in: $CAP"; fails=$((fails + 1)); fi
|
||||
}
|
||||
cap_hasnot() { # LABEL PATTERN
|
||||
if [[ "$CAP" == *"$2"* ]]; then
|
||||
echo " FAIL $1 — '$2' should be absent in: $CAP"; fails=$((fails + 1)); else
|
||||
echo " ok $1"; fi
|
||||
}
|
||||
|
||||
echo "bash: task names (no file fallback)"
|
||||
run task ''
|
||||
reply_has "lists tasks" build
|
||||
reply_has "lists aliases" dep
|
||||
cap_hasnot "no file fallback" "filedir:"
|
||||
|
||||
echo "bash: task variables"
|
||||
run task deploy ''
|
||||
reply_has "required vars" "ENV=dev"
|
||||
cap_has "NoSpace nospace" "compopt:-o nospace"
|
||||
|
||||
echo "bash: inline --output= is full form"
|
||||
run task '--output='
|
||||
reply_has "full-form value" "--output=interleaved"
|
||||
|
||||
echo "bash: --dir routes to directory completion"
|
||||
run task --dir ''
|
||||
cap_has "filedir -d" "filedir:-d"
|
||||
|
||||
echo "bash: --taskfile routes to extension-filtered files"
|
||||
run task --taskfile ''
|
||||
cap_has "filedir ext glob" "filedir:@(yml|yaml)"
|
||||
|
||||
echo "bash: after -- falls back to files"
|
||||
run task build -- ''
|
||||
cap_has "filedir after --" "filedir:"
|
||||
|
||||
if ((fails)); then
|
||||
echo "bash: $fails failure(s)"
|
||||
exit 1
|
||||
fi
|
||||
echo "bash: all passed"
|
||||
61
completion/tests/wrapper.fish
Executable file
61
completion/tests/wrapper.fish
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env fish
|
||||
# Tests the fish wrapper end-to-end via `complete -C`, which asks fish for the
|
||||
# real completions of a command line without a TTY. The `task` command must
|
||||
# resolve to the binary under test (run.sh puts a symlink on PATH).
|
||||
#
|
||||
# Requires: TASK_FIXTURE (dir with a Taskfile.yml and sample files/dirs).
|
||||
|
||||
cd $TASK_FIXTURE
|
||||
source (dirname (status -f))/../fish/task.fish
|
||||
|
||||
set -g fails 0
|
||||
|
||||
function cands
|
||||
complete -C $argv[1] | string split -f1 \t
|
||||
end
|
||||
|
||||
function has # LABEL LINE VALUE
|
||||
if contains -- $argv[3] (cands $argv[2])
|
||||
echo " ok $argv[1]"
|
||||
else
|
||||
echo " FAIL $argv[1] — '$argv[3]' missing from: "(cands $argv[2])
|
||||
set fails (math $fails + 1)
|
||||
end
|
||||
end
|
||||
|
||||
function hasnot # LABEL LINE VALUE
|
||||
if contains -- $argv[3] (cands $argv[2])
|
||||
echo " FAIL $argv[1] — '$argv[3]' should be absent"
|
||||
set fails (math $fails + 1)
|
||||
else
|
||||
echo " ok $argv[1]"
|
||||
end
|
||||
end
|
||||
|
||||
echo "fish: task names (no files)"
|
||||
has "lists tasks" 'task ' build
|
||||
has "lists aliases" 'task ' dep
|
||||
hasnot "no files for tasks" 'task ' notes.txt
|
||||
|
||||
echo "fish: task variables"
|
||||
has "required vars" 'task deploy ' ENV=dev
|
||||
|
||||
echo "fish: flag values"
|
||||
has "enum values" 'task --output ' interleaved
|
||||
|
||||
echo "fish: --dir completes directories only"
|
||||
has "dirs offered" 'task --dir ' sub/
|
||||
hasnot "no plain files" 'task --dir ' notes.txt
|
||||
|
||||
echo "fish: --taskfile filters by extension"
|
||||
has "yaml offered" 'task --taskfile ' Taskfile.yml
|
||||
hasnot "txt filtered out" 'task --taskfile ' notes.txt
|
||||
|
||||
echo "fish: after -- completes files"
|
||||
has "files after --" 'task build -- ' notes.txt
|
||||
|
||||
if test $fails -ne 0
|
||||
echo "fish: $fails failure(s)"
|
||||
exit 1
|
||||
end
|
||||
echo "fish: all passed"
|
||||
62
completion/tests/wrapper.ps1
Normal file
62
completion/tests/wrapper.ps1
Normal file
@@ -0,0 +1,62 @@
|
||||
# Tests the PowerShell wrapper end-to-end via the completion API, which returns
|
||||
# the real completions of a command line without a TTY. The `task` command must
|
||||
# resolve to the binary under test (run.sh puts a symlink on PATH).
|
||||
#
|
||||
# Requires: $env:TASK_FIXTURE (dir with a Taskfile.yml and sample files/dirs).
|
||||
|
||||
Set-Location $env:TASK_FIXTURE
|
||||
. "$PSScriptRoot/../ps/task.ps1"
|
||||
|
||||
$fails = 0
|
||||
|
||||
function Cands($line) {
|
||||
([System.Management.Automation.CommandCompletion]::CompleteInput($line, $line.Length, $null)).CompletionMatches |
|
||||
ForEach-Object { $_.CompletionText }
|
||||
}
|
||||
|
||||
function Has($label, $line, $value) {
|
||||
if ((Cands $line) -contains $value) {
|
||||
Write-Output " ok $label"
|
||||
} else {
|
||||
Write-Output " FAIL $label — '$value' missing from: $((Cands $line) -join ' ')"
|
||||
$script:fails++
|
||||
}
|
||||
}
|
||||
|
||||
function HasNot($label, $line, $value) {
|
||||
if ((Cands $line) -contains $value) {
|
||||
Write-Output " FAIL $label — '$value' should be absent"
|
||||
$script:fails++
|
||||
} else {
|
||||
Write-Output " ok $label"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Output "powershell: task names (no files)"
|
||||
Has "lists tasks" 'task ' 'build'
|
||||
Has "lists aliases" 'task ' 'dep'
|
||||
HasNot "no files for tasks" 'task ' 'notes.txt'
|
||||
|
||||
Write-Output "powershell: prefix filtering"
|
||||
Has "filters by prefix" 'task b' 'build'
|
||||
HasNot "prefix excludes" 'task b' 'deploy'
|
||||
|
||||
Write-Output "powershell: task variables"
|
||||
Has "required vars" 'task deploy ' 'ENV=dev'
|
||||
|
||||
Write-Output "powershell: flag values"
|
||||
Has "enum values" 'task --output ' 'interleaved'
|
||||
|
||||
Write-Output "powershell: --dir completes directories only"
|
||||
Has "dirs offered" 'task --dir ' 'sub'
|
||||
HasNot "no plain files" 'task --dir ' 'notes.txt'
|
||||
|
||||
Write-Output "powershell: --taskfile filters by extension"
|
||||
Has "yaml offered" 'task --taskfile ' 'Taskfile.yml'
|
||||
HasNot "txt filtered out" 'task --taskfile ' 'notes.txt'
|
||||
|
||||
if ($fails -ne 0) {
|
||||
Write-Output "powershell: $fails failure(s)"
|
||||
exit 1
|
||||
}
|
||||
Write-Output "powershell: all passed"
|
||||
81
completion/tests/wrapper.zsh
Executable file
81
completion/tests/wrapper.zsh
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env zsh
|
||||
# Tests the zsh wrapper by stubbing the completion-system functions it calls
|
||||
# (_describe / _files / _path_files) and asserting how it routes each directive.
|
||||
# This is deterministic and needs no TTY.
|
||||
#
|
||||
# Requires: TASK_BIN (task binary), TASK_FIXTURE (dir with a Taskfile.yml).
|
||||
|
||||
export TASK_EXE=$TASK_BIN
|
||||
cd $TASK_FIXTURE
|
||||
|
||||
integer fails=0
|
||||
local CAP
|
||||
compdef() { } # no-op: we call _task directly, not through compinit
|
||||
|
||||
_describe() {
|
||||
local arr=$4
|
||||
CAP+="describe_opts:${@[5,-1]}"$'\n'
|
||||
local c; for c in ${(P)arr}; do CAP+="cand:$c"$'\n'; done
|
||||
}
|
||||
_files() { CAP+="files:$*"$'\n' }
|
||||
_path_files() { CAP+="path_files:$*"$'\n' }
|
||||
|
||||
# Sourcing (not autoloading) defines _task and avoids the autoload first-call
|
||||
# quirk; the trailing `compdef` call is stubbed above.
|
||||
source ${0:A:h}/../zsh/_task
|
||||
|
||||
run() {
|
||||
CAP=""
|
||||
local -a words=("$@")
|
||||
integer CURRENT=$#words
|
||||
local curcontext=":completion:complete:task:"
|
||||
_task
|
||||
}
|
||||
|
||||
has() { # LABEL PATTERN
|
||||
if [[ "$CAP" == *"$2"* ]]; then
|
||||
echo " ok $1"
|
||||
else
|
||||
echo " FAIL $1 — expected '$2' in:"$'\n'"$CAP"
|
||||
(( fails++ ))
|
||||
fi
|
||||
}
|
||||
hasnot() { # LABEL PATTERN
|
||||
if [[ "$CAP" == *"$2"* ]]; then
|
||||
echo " FAIL $1 — '$2' should be absent in:"$'\n'"$CAP"
|
||||
(( fails++ ))
|
||||
else
|
||||
echo " ok $1"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "zsh: task names (no file fallback)"
|
||||
run task ''
|
||||
has "lists tasks" "cand:build"
|
||||
has "lists aliases" "cand:dep"
|
||||
hasnot "no file fallback" "files:"
|
||||
|
||||
echo "zsh: task variables"
|
||||
run task deploy ''
|
||||
has "required vars" "cand:ENV=dev"
|
||||
has "NoSpace -> -S" "describe_opts:-S"
|
||||
has "KeepOrder -> -V" "-V"
|
||||
|
||||
echo "zsh: --dir routes to directory completion"
|
||||
run task --dir ''
|
||||
has "path_files -/" "path_files:-/"
|
||||
|
||||
echo "zsh: --taskfile routes to extension-filtered files"
|
||||
run task --taskfile ''
|
||||
has "files glob" "files:"
|
||||
has "yml in glob" "yml"
|
||||
|
||||
echo "zsh: after -- falls back to files"
|
||||
run task build -- ''
|
||||
has "files after --" "files:"
|
||||
|
||||
if (( fails )); then
|
||||
echo "zsh: $fails failure(s)"
|
||||
exit 1
|
||||
fi
|
||||
echo "zsh: all passed"
|
||||
@@ -9,6 +9,9 @@ _task() {
|
||||
local -a args lines completions opts ctl
|
||||
local output directive line
|
||||
|
||||
# Completion directives, mirroring internal/complete/complete.go.
|
||||
local -ri NO_SPACE=2 NO_FILE_COMP=4 FILTER_FILE_EXT=8 FILTER_DIRS=16 KEEP_ORDER=32
|
||||
|
||||
# Map the zsh completion zstyles to engine flags. `-T` is true when the
|
||||
# style is unset (its default) or explicitly true, so a flag is only passed
|
||||
# when the user turned the style off.
|
||||
@@ -30,7 +33,7 @@ _task() {
|
||||
directive="${lines[-1]#:}"
|
||||
lines=("${(@)lines[1,-2]}")
|
||||
|
||||
if (( directive & 8 )); then
|
||||
if (( directive & FILTER_FILE_EXT )); then
|
||||
local -a globs
|
||||
for line in "${lines[@]}"; do
|
||||
globs+=("*.${line}")
|
||||
@@ -39,7 +42,7 @@ _task() {
|
||||
return
|
||||
fi
|
||||
|
||||
if (( directive & 16 )); then
|
||||
if (( directive & FILTER_DIRS )); then
|
||||
_path_files -/
|
||||
return
|
||||
fi
|
||||
@@ -57,14 +60,14 @@ _task() {
|
||||
fi
|
||||
done
|
||||
|
||||
(( directive & 2 )) && opts+=(-S '')
|
||||
(( directive & 32 )) && opts+=(-V)
|
||||
(( directive & NO_SPACE )) && opts+=(-S '')
|
||||
(( directive & KEEP_ORDER )) && opts+=(-V)
|
||||
|
||||
if (( ${#completions} > 0 )); then
|
||||
_describe -t tasks 'task' completions "${opts[@]}"
|
||||
fi
|
||||
|
||||
(( directive & 4 )) && return
|
||||
(( directive & NO_FILE_COMP )) && return
|
||||
_files
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user