- Issue #4: currently there is no official release yet so we cant download from installer - I added an installer debugger here to test (f091dbf13bdf109b631951e2558651f560ca8b84) - Issue #5: my another library [fuzzy](https://github.com/versenilvis/fuzzy/) has a logic bug so it causes crash, fix d479c6cb4d8453e50366c9839daaabb45a98a2df - Other: - remove unneeded comments (edd9d05fdd16e682b6b416913e2fa9c3f6599956) - Add some tests - Fix reload crash (3a76e78e655fd8daafb635ec0937667325f75ee4) - New optimized build command (56a4a815f836271b861f32795d2df3b25a3dffc0)
This commit is contained in:
@@ -3,3 +3,4 @@ iris.log
|
|||||||
autocomplete
|
autocomplete
|
||||||
temp
|
temp
|
||||||
docs/guide.md
|
docs/guide.md
|
||||||
|
task
|
||||||
+19
-7
@@ -2,13 +2,19 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/versenilvis/iris/integration/shell"
|
"github.com/versenilvis/iris/integration/shell"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ShellAliases = map[string]string{}
|
var (
|
||||||
|
ShellAliases = map[string]string{}
|
||||||
|
shellAliasesMu sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
func GetAlias(name string) (string, bool) {
|
func GetAlias(name string) (string, bool) {
|
||||||
|
shellAliasesMu.RLock()
|
||||||
|
defer shellAliasesMu.RUnlock()
|
||||||
val, ok := ShellAliases[name]
|
val, ok := ShellAliases[name]
|
||||||
return val, ok
|
return val, ok
|
||||||
}
|
}
|
||||||
@@ -19,9 +25,16 @@ func GetAlias(name string) (string, bool) {
|
|||||||
// e.g. Lookup("git checkout ") -> suggests branch names via generator
|
// e.g. Lookup("git checkout ") -> suggests branch names via generator
|
||||||
func Lookup(input string) []Suggestion {
|
func Lookup(input string) []Suggestion {
|
||||||
if shell.Current != nil {
|
if shell.Current != nil {
|
||||||
ShellAliases = shell.Current.ScanAliases()
|
aliases := shell.Current.ScanAliases()
|
||||||
|
shellAliasesMu.Lock()
|
||||||
|
ShellAliases = aliases
|
||||||
|
shellAliasesMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shellAliasesMu.RLock()
|
||||||
|
aliases := ShellAliases
|
||||||
|
shellAliasesMu.RUnlock()
|
||||||
|
|
||||||
if input == "" {
|
if input == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -35,13 +48,12 @@ func Lookup(input string) []Suggestion {
|
|||||||
// in the future, maybe we will write unit test or using Lookup in another module
|
// in the future, maybe we will write unit test or using Lookup in another module
|
||||||
// it will be safer because we maybe forget to check it in that module
|
// it will be safer because we maybe forget to check it in that module
|
||||||
|
|
||||||
pathCmds = make(map[string]bool)
|
|
||||||
scanExternalCommands()
|
scanExternalCommands()
|
||||||
|
|
||||||
// if you have an alias in your shell config like: alias gca="git commit -a"
|
// if you have an alias in your shell config like: alias gca="git commit -a"
|
||||||
// if the first word match it, IRIS will suggest "git commit -a"
|
// if the first word match it, IRIS will suggest "git commit -a"
|
||||||
if len(tokens) > 1 {
|
if len(tokens) > 1 {
|
||||||
if target, ok := ShellAliases[tokens[0]]; ok {
|
if target, ok := aliases[tokens[0]]; ok {
|
||||||
aliasTokens := Tokenize(target)
|
aliasTokens := Tokenize(target)
|
||||||
if len(aliasTokens) > 0 && aliasTokens[len(aliasTokens)-1] == "" {
|
if len(aliasTokens) > 0 && aliasTokens[len(aliasTokens)-1] == "" {
|
||||||
aliasTokens = aliasTokens[:len(aliasTokens)-1]
|
aliasTokens = aliasTokens[:len(aliasTokens)-1]
|
||||||
@@ -52,7 +64,7 @@ func Lookup(input string) []Suggestion {
|
|||||||
|
|
||||||
if len(tokens) == 1 {
|
if len(tokens) == 1 {
|
||||||
query := tokens[0]
|
query := tokens[0]
|
||||||
results := topLevelSuggestions(query)
|
results := topLevelSuggestions(query, aliases)
|
||||||
|
|
||||||
if spec, exists := Registry[query]; exists {
|
if spec, exists := Registry[query]; exists {
|
||||||
hasTrailingSpace := query != "" && query[len(query)-1] == ' '
|
hasTrailingSpace := query != "" && query[len(query)-1] == ' '
|
||||||
@@ -244,10 +256,10 @@ func Lookup(input string) []Suggestion {
|
|||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
func topLevelSuggestions(query string) []Suggestion {
|
func topLevelSuggestions(query string, aliases map[string]string) []Suggestion {
|
||||||
results, seen := []Suggestion{}, make(map[string]bool)
|
results, seen := []Suggestion{}, make(map[string]bool)
|
||||||
|
|
||||||
for name, target := range ShellAliases {
|
for name, target := range aliases {
|
||||||
if !seen[name] && (query == "" || HasPrefix(name, query)) {
|
if !seen[name] && (query == "" || HasPrefix(name, query)) {
|
||||||
results = append(results, Suggestion{
|
results = append(results, Suggestion{
|
||||||
Cmd: target, Desc: "alias: " + name, Icon: "root",
|
Cmd: target, Desc: "alias: " + name, Icon: "root",
|
||||||
|
|||||||
@@ -3,13 +3,19 @@ package core
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
var pathCmds = make(map[string]bool)
|
var (
|
||||||
|
pathCmds = make(map[string]bool)
|
||||||
|
pathCmdsOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
// scanExternalCommands populates pathCmds with system executables
|
// scanExternalCommands populates pathCmds with system executables
|
||||||
func scanExternalCommands() {
|
func scanExternalCommands() {
|
||||||
|
pathCmdsOnce.Do(func() {
|
||||||
scanPath()
|
scanPath()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// scanPath populates pathCmds with all executable files found in path environment variable
|
// scanPath populates pathCmds with all executable files found in path environment variable
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
|
# NOTE: RUN "just -l" TO QUICKLY VIEW ALL COMMANDS
|
||||||
|
|
||||||
# build binary file
|
# build binary file
|
||||||
[group('dev')]
|
[group('build')]
|
||||||
build:
|
build:
|
||||||
@go build -o iris main.go
|
@go build -o iris main.go
|
||||||
|
|
||||||
|
# build with optimized binary file
|
||||||
|
[group('build')]
|
||||||
optimized-build:
|
optimized-build:
|
||||||
@GOAMD64=v3 go build -pgo=auto -ldflags="-s -w" -trimpath -o iris main.go
|
@GOAMD64=v4 go build -ldflags="-s -w" -trimpath -o iris main.go
|
||||||
|
|
||||||
# run iris
|
# run iris
|
||||||
[group('dev')]
|
[group('dev')]
|
||||||
@@ -15,7 +20,7 @@ run:
|
|||||||
[linux, macos]
|
[linux, macos]
|
||||||
reload:
|
reload:
|
||||||
@go build -o iris main.go
|
@go build -o iris main.go
|
||||||
@if [ -n "${IRIS_PID:-}" ]; then kill -USR1 $IRIS_PID; fi
|
@if [ -n "${IRIS_PID:-}" ]; then kill -USR1 $IRIS_PID 2>/dev/null || true; fi
|
||||||
@if [ -z "${IRIS_FD:-}" ]; then ./iris; fi
|
@if [ -z "${IRIS_FD:-}" ]; then ./iris; fi
|
||||||
|
|
||||||
# update pkg
|
# update pkg
|
||||||
@@ -23,8 +28,8 @@ reload:
|
|||||||
pkg:
|
pkg:
|
||||||
@go mod tidy
|
@go mod tidy
|
||||||
|
|
||||||
# debugger
|
# iris debugger
|
||||||
[group('dev')]
|
[group('debug')]
|
||||||
debug:
|
debug:
|
||||||
@./iris --debug
|
@./iris --debug
|
||||||
|
|
||||||
@@ -75,3 +80,162 @@ debug-notify version="v1.99.0":
|
|||||||
[group('debug')]
|
[group('debug')]
|
||||||
build-release version:
|
build-release version:
|
||||||
@GOAMD64=v3 go build -pgo=auto -ldflags="-s -w -X github.com/versenilvis/iris/root.Version={{version}}" -trimpath -o iris main.go
|
@GOAMD64=v3 go build -pgo=auto -ldflags="-s -w -X github.com/versenilvis/iris/root.Version={{version}}" -trimpath -o iris main.go
|
||||||
|
|
||||||
|
# test the install script locally
|
||||||
|
# usage: just debug-install v1.0.0
|
||||||
|
[group('debug')]
|
||||||
|
debug-install version="v1.0.0":
|
||||||
|
#!/bin/sh
|
||||||
|
PORT=19998
|
||||||
|
TMP=$(mktemp -d)
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
|
||||||
|
# always cleanup server + tmp, even if a test crashes
|
||||||
|
trap "kill \$SERVER_PID 2>/dev/null; rm -rf $TMP" EXIT
|
||||||
|
|
||||||
|
ok() { PASS=$((PASS+1)); printf " \033[32m✓\033[0m %s\n" "$1"; }
|
||||||
|
fail() { FAIL=$((FAIL+1)); printf " \033[31m✗\033[0m %s\n" "$1"; }
|
||||||
|
|
||||||
|
# --- detect actual arch (not hardcoded) ---
|
||||||
|
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||||
|
MACH=$(uname -m)
|
||||||
|
case "$MACH" in
|
||||||
|
x86_64|amd64) ARCH="amd64" ;;
|
||||||
|
aarch64|arm64) ARCH="arm64" ;;
|
||||||
|
*) echo "unsupported arch: $MACH"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
ARCHIVE_NAME="iris_${OS}_${ARCH}.tar.gz"
|
||||||
|
|
||||||
|
echo "building iris ({{version}}, ${OS}_${ARCH})..."
|
||||||
|
go build -ldflags="-X github.com/versenilvis/iris/root.Version={{version}}" -o "$TMP/iris" main.go
|
||||||
|
|
||||||
|
echo "packaging archive ($ARCHIVE_NAME)..."
|
||||||
|
tar -czf "$TMP/$ARCHIVE_NAME" -C "$TMP" iris
|
||||||
|
|
||||||
|
# --- start mock server ---
|
||||||
|
python3 -c 'import sys, textwrap; exec(textwrap.dedent(r"""
|
||||||
|
import os, sys
|
||||||
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
|
|
||||||
|
port = int(sys.argv[1])
|
||||||
|
tmp_dir = sys.argv[2]
|
||||||
|
version = sys.argv[3]
|
||||||
|
os_name = sys.argv[4]
|
||||||
|
arch = sys.argv[5]
|
||||||
|
|
||||||
|
class mock_handler(BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
if "/repos/versenilvis/iris/releases/latest" in self.path:
|
||||||
|
if self.path.startswith("/404"):
|
||||||
|
self.send_response(404)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(b"{\"message\":\"Not Found\",\"documentation_url\":\"https://docs.github.com/rest\"}")
|
||||||
|
elif self.path.startswith("/ratelimit"):
|
||||||
|
self.send_response(403)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(b"{\"message\":\"API rate limit exceeded for ...\",\"documentation_url\":\"https://docs.github.com/rest/overview/rate-limits-for-the-rest-api\"}")
|
||||||
|
else:
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.end_headers()
|
||||||
|
download_url = "http://localhost:" + str(port) + "/iris_" + os_name + "_" + arch + ".tar.gz"
|
||||||
|
response = "{\n \"tag_name\": \"" + version + "\",\n \"assets\": [\n {\n \"browser_download_url\": \"" + download_url + "\"\n }\n ]\n}\n"
|
||||||
|
self.wfile.write(response.encode("utf-8"))
|
||||||
|
elif self.path.endswith(".tar.gz"):
|
||||||
|
filename = os.path.basename(self.path)
|
||||||
|
filepath = os.path.join(tmp_dir, filename)
|
||||||
|
if os.path.exists(filepath):
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/octet-stream")
|
||||||
|
self.send_header("Content-Length", str(os.path.getsize(filepath)))
|
||||||
|
self.end_headers()
|
||||||
|
with open(filepath, "rb") as f:
|
||||||
|
self.wfile.write(f.read())
|
||||||
|
else:
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
else:
|
||||||
|
self.send_response(200)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def log_message(self, *args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
HTTPServer(("", port), mock_handler).serve_forever()
|
||||||
|
"""))' "$PORT" "$TMP" "{{version}}" "$OS" "$ARCH" 2>/dev/null &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
# retry loop instead of blind sleep - wait up to 3s for server to be ready
|
||||||
|
i=0
|
||||||
|
until curl -sf "http://localhost:${PORT}/" >/dev/null 2>&1; do
|
||||||
|
i=$((i+1))
|
||||||
|
[ $i -ge 30 ] && echo "mock server failed to start" && exit 1
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- running test cases ---"
|
||||||
|
|
||||||
|
# --- test 1: happy path ---
|
||||||
|
printf "test 1: happy path install... "
|
||||||
|
OUT=$(BIN_DIR="$TMP/out1" IRIS_API_URL="http://localhost:${PORT}/happy" sh scripts/install.sh 2>&1) && STATUS=0 || STATUS=$?
|
||||||
|
if [ $STATUS -eq 0 ] && echo "$OUT" | grep -q "Installation verified"; then
|
||||||
|
ok "binary installed and verified"
|
||||||
|
else
|
||||||
|
fail "happy path failed\n$OUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- test 2: version output check ---
|
||||||
|
printf "test 2: version string matches... "
|
||||||
|
BIN="$TMP/out1/iris"
|
||||||
|
if [ -x "$BIN" ]; then
|
||||||
|
VER=$("$BIN" version 2>&1)
|
||||||
|
if echo "$VER" | grep -q "{{version}}"; then
|
||||||
|
ok "got '$VER'"
|
||||||
|
else
|
||||||
|
fail "expected {{version}}, got '$VER'"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "binary not found at $BIN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- test 3: 404 (no release published) ---
|
||||||
|
printf "test 3: 404 no release error... "
|
||||||
|
OUT=$(BIN_DIR="$TMP/out3" IRIS_API_URL="http://localhost:${PORT}/404" sh scripts/install.sh 2>&1) && STATUS=0 || STATUS=1
|
||||||
|
if [ $STATUS -ne 0 ] && echo "$OUT" | grep -qi "no releases found\|not have published"; then
|
||||||
|
ok "correct error message for 404"
|
||||||
|
else
|
||||||
|
fail "expected 404 error, got: $OUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- test 4: 403 rate limit ---
|
||||||
|
printf "test 4: rate limit error... "
|
||||||
|
OUT=$(BIN_DIR="$TMP/out4" IRIS_API_URL="http://localhost:${PORT}/ratelimit" sh scripts/install.sh 2>&1) && STATUS=0 || STATUS=1
|
||||||
|
if [ $STATUS -ne 0 ] && echo "$OUT" | grep -qi "rate limit\|rate limited"; then
|
||||||
|
ok "correct error message for rate limit"
|
||||||
|
else
|
||||||
|
fail "expected rate limit error, got: $OUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- test 5: wget code path (call wget directly, same as install script would) ---
|
||||||
|
printf "test 5: wget code path... "
|
||||||
|
if ! command -v wget >/dev/null 2>&1; then
|
||||||
|
ok "skipped (wget not installed)"
|
||||||
|
else
|
||||||
|
# directly invoke wget the same way install.sh does and check it parses correctly
|
||||||
|
RELEASES=$(wget -qO- "http://localhost:${PORT}/happy/repos/versenilvis/iris/releases/latest" 2>&1) && WS=0 || WS=$?
|
||||||
|
URL=$(echo "$RELEASES" | grep "browser_download_url" | grep "${OS}_${ARCH}" | head -1 | cut -d '"' -f 4)
|
||||||
|
if [ -n "$URL" ]; then
|
||||||
|
ok "wget parses download URL correctly ($URL)"
|
||||||
|
else
|
||||||
|
fail "wget could not parse URL from response: $RELEASES"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- summary ---
|
||||||
|
echo ""
|
||||||
|
printf "results: \033[32m%d passed\033[0m, \033[31m%d failed\033[0m\n" "$PASS" "$FAIL"
|
||||||
|
[ $FAIL -eq 0 ] || exit 1
|
||||||
+21
-9
@@ -6,6 +6,8 @@ set -e
|
|||||||
|
|
||||||
REPO="versenilvis/iris"
|
REPO="versenilvis/iris"
|
||||||
BIN_DIR="${BIN_DIR:-/usr/local/bin}"
|
BIN_DIR="${BIN_DIR:-/usr/local/bin}"
|
||||||
|
# allow overriding the GitHub API base URL for local testing
|
||||||
|
IRIS_API_URL="${IRIS_API_URL:-https://api.github.com}"
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
echo "Installing iris..."
|
echo "Installing iris..."
|
||||||
@@ -55,7 +57,7 @@ main() {
|
|||||||
echo "Installed iris to ${BIN_DIR}/iris"
|
echo "Installed iris to ${BIN_DIR}/iris"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if "${BIN_DIR}/iris" --version >/dev/null 2>&1; then
|
if "${BIN_DIR}/iris" version >/dev/null 2>&1; then
|
||||||
echo "Installation verified."
|
echo "Installation verified."
|
||||||
else
|
else
|
||||||
echo "Warning: could not verify binary"
|
echo "Warning: could not verify binary"
|
||||||
@@ -89,24 +91,34 @@ get_download_url() {
|
|||||||
arch="$1"
|
arch="$1"
|
||||||
|
|
||||||
if command -v curl >/dev/null 2>&1; then
|
if command -v curl >/dev/null 2>&1; then
|
||||||
releases=$(curl -sL \
|
http_response=$(curl -sL -w "\n%{http_code}" \
|
||||||
${GITHUB_TOKEN:+-H "Authorization: Bearer ${GITHUB_TOKEN}"} \
|
${GITHUB_TOKEN:+-H "Authorization: Bearer ${GITHUB_TOKEN}"} \
|
||||||
"https://api.github.com/repos/${REPO}/releases/latest")
|
"${IRIS_API_URL}/repos/${REPO}/releases/latest")
|
||||||
|
http_code=$(echo "${http_response}" | tail -1)
|
||||||
|
releases=$(echo "${http_response}" | sed '$d')
|
||||||
elif command -v wget >/dev/null 2>&1; then
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
releases=$(wget -qO- \
|
tmp_headers=$(mktemp)
|
||||||
|
releases=$(wget -S -qO- \
|
||||||
${GITHUB_TOKEN:+--header "Authorization: Bearer ${GITHUB_TOKEN}"} \
|
${GITHUB_TOKEN:+--header "Authorization: Bearer ${GITHUB_TOKEN}"} \
|
||||||
"https://api.github.com/repos/${REPO}/releases/latest")
|
"${IRIS_API_URL}/repos/${REPO}/releases/latest" 2>"$tmp_headers" || true)
|
||||||
|
http_code=$(grep "HTTP/" "$tmp_headers" | tail -1 | sed -e 's/^[[:space:]]*//' | cut -d' ' -f2)
|
||||||
|
[ -z "${http_code}" ] && http_code="000"
|
||||||
|
rm -f "$tmp_headers"
|
||||||
else
|
else
|
||||||
err "curl or wget is required"
|
err "curl or wget is required"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if echo "${releases}" | grep -q "rate limit"; then
|
if [ "${http_code}" = "404" ]; then
|
||||||
err "GitHub API rate limited. Try again later or set GITHUB_TOKEN env variable"
|
err "no releases found for ${REPO}. the project may not have published a release yet"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if echo "${releases}" | grep -q '"message"'; then
|
if [ "${http_code}" = "403" ] || echo "${releases}" | grep -q "rate limit"; then
|
||||||
|
err "GitHub API rate limited. try again later or set GITHUB_TOKEN env variable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${http_code}" != "200" ]; then
|
||||||
msg=$(echo "${releases}" | grep '"message"' | head -1 | cut -d '"' -f 4)
|
msg=$(echo "${releases}" | grep '"message"' | head -1 | cut -d '"' -f 4)
|
||||||
err "GitHub API error: ${msg}"
|
err "GitHub API error (HTTP ${http_code}): ${msg}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
url=$(echo "${releases}" | grep "browser_download_url" | grep "${arch}" | head -1 | cut -d '"' -f 4)
|
url=$(echo "${releases}" | grep "browser_download_url" | grep "${arch}" | head -1 | cut -d '"' -f 4)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func TestFileGenerator(t *testing.T) {
|
|||||||
_ = os.Chdir(tmp)
|
_ = os.Chdir(tmp)
|
||||||
defer func() { _ = os.Chdir(oldWd) }()
|
defer func() { _ = os.Chdir(oldWd) }()
|
||||||
|
|
||||||
// REQUIREMENT: dirOnly shows only dirs
|
|
||||||
t.Run("dirOnly shows only dirs", func(t *testing.T) {
|
t.Run("dirOnly shows only dirs", func(t *testing.T) {
|
||||||
gen := core.FileGenerator("/")
|
gen := core.FileGenerator("/")
|
||||||
results := gen([]string{"cd", ""}, "cd ", "")
|
results := gen([]string{"cd", ""}, "cd ", "")
|
||||||
@@ -33,7 +33,7 @@ func TestFileGenerator(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// REQUIREMENT: Filter extension shows only matching files
|
|
||||||
t.Run("Filter extension", func(t *testing.T) {
|
t.Run("Filter extension", func(t *testing.T) {
|
||||||
gen := core.FileGenerator(".go")
|
gen := core.FileGenerator(".go")
|
||||||
results := gen([]string{"ls", ""}, "ls ", "")
|
results := gen([]string{"ls", ""}, "ls ", "")
|
||||||
@@ -51,7 +51,7 @@ func TestFileGenerator(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// REQUIREMENT: Nested path (src/mai -> correct dir + prefix)
|
|
||||||
t.Run("Nested path", func(t *testing.T) {
|
t.Run("Nested path", func(t *testing.T) {
|
||||||
gen := core.FileGenerator()
|
gen := core.FileGenerator()
|
||||||
results := gen([]string{"ls", "src/u"}, "ls src/u", "src/u")
|
results := gen([]string{"ls", "src/u"}, "ls src/u", "src/u")
|
||||||
@@ -66,7 +66,7 @@ func TestFileGenerator(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// REQUIREMENT: Deep scan 1 level finds files in subdir
|
|
||||||
t.Run("Deep scan 1 level", func(t *testing.T) {
|
t.Run("Deep scan 1 level", func(t *testing.T) {
|
||||||
gen := core.FileGenerator()
|
gen := core.FileGenerator()
|
||||||
results := gen([]string{"ls", "src/"}, "ls src/", "src/")
|
results := gen([]string{"ls", "src/"}, "ls src/", "src/")
|
||||||
@@ -81,10 +81,9 @@ func TestFileGenerator(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// REQUIREMENT: Deep scan does not go deeper than 1 level
|
|
||||||
// (This is implicitly tested by the logic in FileGenerator)
|
|
||||||
|
|
||||||
// REQUIREMENT: Hidden files are skipped
|
|
||||||
|
|
||||||
t.Run("Hidden files are skipped", func(t *testing.T) {
|
t.Run("Hidden files are skipped", func(t *testing.T) {
|
||||||
gen := core.FileGenerator()
|
gen := core.FileGenerator()
|
||||||
results := gen([]string{"ls", ""}, "ls ", "")
|
results := gen([]string{"ls", ""}, "ls ", "")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package tests
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/versenilvis/iris/commands/core"
|
"github.com/versenilvis/iris/commands/core"
|
||||||
@@ -31,21 +32,13 @@ func TestLookup(t *testing.T) {
|
|||||||
minResults int
|
minResults int
|
||||||
mustContain string
|
mustContain string
|
||||||
}{
|
}{
|
||||||
// REQUIREMENT: Token 1, no trailing space -> top-level suggestions
|
|
||||||
{"Top-level", "gi", 1, "git"},
|
{"Top-level", "gi", 1, "git"},
|
||||||
// REQUIREMENT: Token 1, with trailing space -> subcommand suggestions
|
|
||||||
{"Subcommand", "git ", 1, "git commit"},
|
{"Subcommand", "git ", 1, "git commit"},
|
||||||
// REQUIREMENT: Alias expansion (gca -> git commit -a)
|
|
||||||
{"Alias expansion", "gca", 1, "git commit -a"},
|
{"Alias expansion", "gca", 1, "git commit -a"},
|
||||||
// REQUIREMENT: Alias value with space (ta -> tmux a -t)
|
|
||||||
{"Alias with space", "ta", 1, "tmux a -t"},
|
{"Alias with space", "ta", 1, "tmux a -t"},
|
||||||
// REQUIREMENT: Subcommand depth 2+ (git remote add)
|
|
||||||
{"Deep subcommand", "git remote ", 1, "git remote add"},
|
{"Deep subcommand", "git remote ", 1, "git remote add"},
|
||||||
// REQUIREMENT: Option dedup (do not suggest --verbose if already typed)
|
|
||||||
{"Option dedup", "git --verbose -", 0, ""},
|
{"Option dedup", "git --verbose -", 0, ""},
|
||||||
// REQUIREMENT: --flag=value does not count into argCount
|
|
||||||
{"Flag with value ignore", "git --output=json ", 2, "git --output=json commit"},
|
{"Flag with value ignore", "git --output=json ", 2, "git --output=json commit"},
|
||||||
// REQUIREMENT: Unknown root command -> nil
|
|
||||||
{"Unknown root command", "unknowncmd ", 0, ""},
|
{"Unknown root command", "unknowncmd ", 0, ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,3 +63,33 @@ func TestLookup(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLookupConcurrent(t *testing.T) {
|
||||||
|
core.Registry = make(map[string]*core.Spec)
|
||||||
|
core.Register(&core.Spec{
|
||||||
|
Name: "git",
|
||||||
|
Subcommands: []core.Subcommand{
|
||||||
|
{Name: "commit", Options: []core.Option{{Name: "--message"}}, MaxArgs: 1},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
core.ShellAliases = map[string]string{
|
||||||
|
"gca": "git commit -a",
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
const goroutines = 10
|
||||||
|
const iterations = 50
|
||||||
|
|
||||||
|
for range goroutines {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for range iterations {
|
||||||
|
_ = core.Lookup("gca")
|
||||||
|
_ = core.Lookup("git ")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,19 +13,12 @@ func TestTokenize(t *testing.T) {
|
|||||||
input string
|
input string
|
||||||
expected []string
|
expected []string
|
||||||
}{
|
}{
|
||||||
// REQUIREMENT: Empty input
|
|
||||||
{"Empty input", "", []string{""}},
|
{"Empty input", "", []string{""}},
|
||||||
// REQUIREMENT: Trailing space("git " -> 2 tokens, the last token is "")
|
|
||||||
{"Trailing space", "git ", []string{"git", ""}},
|
{"Trailing space", "git ", []string{"git", ""}},
|
||||||
// REQUIREMENT: Multi-space("git add")
|
|
||||||
{"Multi-space", "git add", []string{"git", "add"}},
|
{"Multi-space", "git add", []string{"git", "add"}},
|
||||||
// REQUIREMENT: Quoted string("git commit -m \"hello world\"")
|
|
||||||
{"Quoted string", "git commit -m \"hello world\"", []string{"git", "commit", "-m", "hello world"}},
|
{"Quoted string", "git commit -m \"hello world\"", []string{"git", "commit", "-m", "hello world"}},
|
||||||
// REQUIREMENT: Quote not closed
|
|
||||||
{"Quote not closed", "git commit -m \"hello", []string{"git", "commit", "-m", "hello"}},
|
{"Quote not closed", "git commit -m \"hello", []string{"git", "commit", "-m", "hello"}},
|
||||||
// REQUIREMENT: Single quote vs double quote
|
|
||||||
{"Single quote", "git commit -m 'hello world'", []string{"git", "commit", "-m", "hello world"}},
|
{"Single quote", "git commit -m 'hello world'", []string{"git", "commit", "-m", "hello world"}},
|
||||||
// REQUIREMENT: Backslash escape
|
|
||||||
{"Backslash escape", "git commit -m \"hello\\ world\"", []string{"git", "commit", "-m", "hello world"}},
|
{"Backslash escape", "git commit -m \"hello\\ world\"", []string{"git", "commit", "-m", "hello world"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,13 +39,9 @@ func TestHasPrefix(t *testing.T) {
|
|||||||
prefix string
|
prefix string
|
||||||
want bool
|
want bool
|
||||||
}{
|
}{
|
||||||
// REQUIREMENT: Case insensitive match
|
|
||||||
{"Case insensitive", "Hello", "hel", true},
|
{"Case insensitive", "Hello", "hel", true},
|
||||||
// REQUIREMENT: Unicode support (Vietnamese)
|
|
||||||
{"Unicode support", "Thử nghiệm", "thử", true},
|
{"Unicode support", "Thử nghiệm", "thử", true},
|
||||||
// REQUIREMENT: Prefix longer than string -> false
|
|
||||||
{"Prefix longer", "Iris", "Iris-Longer", false},
|
{"Prefix longer", "Iris", "Iris-Longer", false},
|
||||||
// REQUIREMENT: Empty prefix -> true
|
|
||||||
{"Empty prefix", "Iris", "", true},
|
{"Empty prefix", "Iris", "", true},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func TestZoxideGenerator(t *testing.T) {
|
|||||||
|
|
||||||
gen := fs.ZoxideGenerator()
|
gen := fs.ZoxideGenerator()
|
||||||
|
|
||||||
// REQUIREMENT: Query returns the correct result when partial = ""
|
|
||||||
t.Run("Query returns correct result when partial is empty", func(t *testing.T) {
|
t.Run("Query returns correct result when partial is empty", func(t *testing.T) {
|
||||||
results := gen([]string{"z", ""}, "z ", "")
|
results := gen([]string{"z", ""}, "z ", "")
|
||||||
if len(results) == 0 {
|
if len(results) == 0 {
|
||||||
@@ -33,7 +33,7 @@ func TestZoxideGenerator(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// REQUIREMENT: Path replaces home dir with ~
|
|
||||||
t.Run("Path replaces home dir with ~", func(t *testing.T) {
|
t.Run("Path replaces home dir with ~", func(t *testing.T) {
|
||||||
home, _ := os.UserHomeDir()
|
home, _ := os.UserHomeDir()
|
||||||
results := gen([]string{"z", ""}, "z ", "")
|
results := gen([]string{"z", ""}, "z ", "")
|
||||||
@@ -49,7 +49,7 @@ func TestZoxideGenerator(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// REQUIREMENT: Sort by descending score
|
|
||||||
t.Run("Sort by descending score", func(t *testing.T) {
|
t.Run("Sort by descending score", func(t *testing.T) {
|
||||||
results := gen([]string{"z", "i"}, "z ", "i")
|
results := gen([]string{"z", "i"}, "z ", "i")
|
||||||
if len(results) >= 1 {
|
if len(results) >= 1 {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestMergeResults(t *testing.T) {
|
func TestMergeResults(t *testing.T) {
|
||||||
// REQUIREMENT: Dedup exact match
|
|
||||||
t.Run("Dedup exact match", func(t *testing.T) {
|
t.Run("Dedup exact match", func(t *testing.T) {
|
||||||
// Mock history items that might conflict with specs
|
// Mock history items that might conflict with specs
|
||||||
res := root.MergeResults("git", "spec")
|
res := root.MergeResults("git", "spec")
|
||||||
@@ -20,7 +20,7 @@ func TestMergeResults(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// REQUIREMENT: Limit 100
|
|
||||||
t.Run("Limit 100", func(t *testing.T) {
|
t.Run("Limit 100", func(t *testing.T) {
|
||||||
res := root.MergeResults("a", "history")
|
res := root.MergeResults("a", "history")
|
||||||
if len(res) > 100 {
|
if len(res) > 100 {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestScanPosixAliases(t *testing.T) {
|
func TestScanPosixAliases(t *testing.T) {
|
||||||
// REQUIREMENT: Parse single alias, multi alias, comments, value with space
|
|
||||||
input := `
|
input := `
|
||||||
alias gca='git commit -a'
|
alias gca='git commit -a'
|
||||||
alias ta="tmux a -t" # this is a comment
|
alias ta="tmux a -t" # this is a comment
|
||||||
@@ -34,10 +34,10 @@ func TestSplitAliasTokens(t *testing.T) {
|
|||||||
input string
|
input string
|
||||||
expected []string
|
expected []string
|
||||||
}{
|
}{
|
||||||
// REQUIREMENT: Parse multi alias on one line
|
|
||||||
{"Single", "a='b'", []string{"a='b'"}},
|
{"Single", "a='b'", []string{"a='b'"}},
|
||||||
{"Multi", "a='b' c=\"d\"", []string{"a='b'", "c=\"d\""}},
|
{"Multi", "a='b' c=\"d\"", []string{"a='b'", "c=\"d\""}},
|
||||||
// REQUIREMENT: Value with space in quote
|
|
||||||
{"With Space", "ta='tmux a -t' l='ls -l'", []string{"ta='tmux a -t'", "l='ls -l'"}},
|
{"With Space", "ta='tmux a -t' l='ls -l'", []string{"ta='tmux a -t'", "l='ls -l'"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user