From 917509aa8a9bf55d79346f1180d5d11cf6f928af Mon Sep 17 00:00:00 2001 From: Cormac Shannon <> Date: Sun, 22 Mar 2026 17:58:09 +0000 Subject: [PATCH] Refactor lush tests: remove duplication, fill coverage gaps, wire into all.lua - Wire all 11 lush test files into testes/all.lua (was only 4) - Delete dead commands-interactive.lua (disabled via os.exit(0)) - Remove duplicate regression test from piping.lua - Add user-defined command tests (lush.commands): basic, int return, table return, error handling, removal, API call - Add lush.subcmd() and lush.interactive() direct call tests - Add argv edge cases: single-quote backslash, \$ escape, unknown escape, adjacent quote concatenation - Add globbing edge cases: brace suffix, no-comma braces, bracket glob, mid-word tilde passthrough - Add signal exit code test (128+9=137) - Add escaped-pipe and empty interactive command tests --- testes/all.lua | 7 + testes/lush/argv.lua | 24 ++ testes/lush/commands-interactive.lua | 366 --------------------------- testes/lush/commands.lua | 6 + testes/lush/globbing.lua | 36 +++ testes/lush/interactive.lua | 10 + testes/lush/lushlib.lua | 87 +++++++ testes/lush/piping.lua | 13 +- 8 files changed, 176 insertions(+), 373 deletions(-) delete mode 100644 testes/lush/commands-interactive.lua diff --git a/testes/all.lua b/testes/all.lua index 33f80421..a4deda85 100755 --- a/testes/all.lua +++ b/testes/all.lua @@ -204,6 +204,13 @@ dofile('lush/commands.lua') dofile('lush/argv.lua') dofile('lush/interpolation.lua') dofile('lush/envvars.lua') +dofile('lush/piping.lua') +dofile('lush/globbing.lua') +dofile('lush/interactive.lua') +dofile('lush/builtins.lua') +dofile('lush/lushlib.lua') +dofile('lush/config.lua') +dofile('lush/prompt.lua') if #msgs > 0 then local m = table.concat(msgs, "\n ") diff --git a/testes/lush/argv.lua b/testes/lush/argv.lua index 5ea4b565..48299df8 100644 --- a/testes/lush/argv.lua +++ b/testes/lush/argv.lua @@ -50,4 +50,28 @@ do assert(r.stdout == "a\tb\n") end +-- backslash in single quotes is literal (no escape processing) +do + local r = `echo 'hello\\nworld'` + assert(r.stdout == "hello\\nworld\n") +end + +-- \$ in double quotes → literal $ +do + local r = `echo "\$HOME"` + assert(r.stdout == "$HOME\n") +end + +-- unknown escape in double quotes preserved +do + local r = `echo "\q"` + assert(r.stdout == "\\q\n") +end + +-- adjacent quotes form a single token +do + local r = `echo "hello"'world'` + assert(r.stdout == "helloworld\n") +end + print "OK" diff --git a/testes/lush/commands-interactive.lua b/testes/lush/commands-interactive.lua deleted file mode 100644 index 67dc1c0c..00000000 --- a/testes/lush/commands-interactive.lua +++ /dev/null @@ -1,366 +0,0 @@ --- testes/lush/commands-interactive.lua --- Tests for interactive command execution (issue #10). --- This file serves as a design playground: it documents how bare-word --- commands should behave alongside Lua in both scripts and the REPL. - -print "Skipping interactive commands - not implemented yet" -os.exit(0) --[[ - -print "testing interactive commands" - --- ===== RESULT TABLE STRUCTURE ===== - --- basic command, result is a table with code/stdout/stderr -do - echo hello - assert(type(_) == "table") - assert(type(_.code) == "number") - assert(type(_.stdout) == "string") - assert(type(_.stderr) == "string") -end - --- ===== EXIT CODES ===== - --- successful command returns exit code 0 -do - sh -c "exit 0" - assert(_.code == 0) -end - --- failed command returns non-zero exit code -do - sh -c "exit 1" - assert(_.code == 1) -end - --- specific exit codes are preserved -do - sh -c "exit 42" - assert(_.code == 42) -end - --- command not found returns 127 -do - nonexistent_command_xyz_999 - assert(_.code == 127) -end - --- ===== INTERACTIVE MODE: NO STDOUT/STDERR CAPTURE ===== --- interactive commands inherit the terminal; stdout/stderr go directly --- to the user's screen, so _.stdout and _.stderr are always empty. - -do - echo hello - assert(_.stdout == "") -end - -do - sh -c "echo err >&2" - assert(_.stderr == "") - assert(_.stdout == "") -end - -do - sh -c "echo out; echo err >&2" - assert(_.stdout == "") - assert(_.stderr == "") -end - -do - echo hello world - assert(_.stdout == "") - assert(_.code == 0) -end - --- ===== PARSER HEURISTIC: IDENTIFIER + NON-EXCEPTION TOKEN ===== --- the parser detects shell commands when an identifier at statement --- position is followed by a token NOT in the exception list: --- ( string { . : [ = , - --- bare identifier, no arguments (next token is keyword/identifier/EOF) -do - ls - assert(_.code == 0) -end - --- identifier + dash flag -do - ls -la / - assert(_.code == 0) -end - --- identifier + slash (path argument) -do - ls /tmp - assert(_.code == 0) -end - --- identifier + identifier (subcommand pattern) -do - git --version - assert(_.code == 0) -end - --- ===== COMMANDS INSIDE LUA BLOCKS ===== --- bare commands work anywhere a Lua statement can appear. - --- inside do/end -do - echo inside-do - assert(_.code == 0) -end - --- inside if/end -do - if true then - echo inside-if - assert(_.code == 0) - end -end - --- inside for/end -do - for i = 1, 3 do - echo loop - assert(_.code == 0) - end -end - --- inside while/end -do - local n = 0 - while n < 2 do - echo while-loop - assert(_.code == 0) - n = n + 1 - end -end - --- inside function body -do - local function run_cmd() - ls / - return _.code - end - assert(run_cmd() == 0) -end - --- nested blocks -do - if true then - for i = 1, 2 do - echo nested - assert(_.code == 0) - end - end -end - --- ===== _ BEHAVIOR ===== - --- _ is overwritten by subsequent commands -do - sh -c "exit 0" - assert(_.code == 0) - sh -c "exit 5" - assert(_.code == 5) -end - --- _ persists across block boundaries (it's a global) -do - sh -c "exit 3" -end -assert(_.code == 3) - --- ===== RUNTIME FALLBACK: STRING-ARG FUNCTION CALL SYNTAX ===== --- echo "hello" parses as Lua echo("hello") because string literal --- is in the exception list. at runtime, echo is nil → "attempt to --- call a nil value" → check PATH → found → run as shell command. - -do - echo "hello" - assert(_.code == 0) - assert(_.stdout == "") -end - -do - printf "hello\n" - assert(_.code == 0) -end - --- undefined name NOT in PATH → original Lua error preserved -do - local ok, err = pcall(function() - pirnt "hello" - end) - assert(not ok) - assert(string.find(err, "pirnt")) -end - --- ===== LUA VARIABLE SHADOWS SHELL COMMAND ===== --- if a Lua variable with the same name as a shell command is defined, --- the Lua variable wins. the shell fallback only triggers on nil. - --- string-arg sugar: echo "hello" parses as echo("hello"). --- echo is a local function, so Lua calls it — NOT /bin/echo. -do - local func_called = false - local echo = function(x) func_called = true end - echo "hello" - assert(func_called == true) -end - --- table-arg sugar: same principle with { } syntax -do - local received = nil - local grep = function(t) received = t end - grep {"pattern", "file.txt"} - assert(received[1] == "pattern") -end - --- paren call: unambiguously Lua, local wins -do - local func_called = false - local ls = function(...) func_called = true end - ls("/tmp") - assert(func_called == true) -end - --- global function shadows command name -do - local func_called = false - function echo(x) func_called = true end - echo "test" - assert(func_called == true) - echo = nil -- clean up global -end - --- ===== LUA SYNTAX PRESERVED ===== --- all exception-list tokens correctly route to Lua parsing. - --- multi-line function call: string arg on next line -do - local function my_func(arg) - return arg - end - - local r = my_func - "hello world" - assert(r == "hello world") -end - --- multi-line function call: paren arg on next line -do - local function add(a, b) return a + b end - - local r = add - (1, 2) - assert(r == 3) -end - --- multi-line function call: table arg on next line -do - local function first(t) return t[1] end - - local r = first - {42} - assert(r == 42) -end - --- assignment -do - local x = 5 - assert(x == 5) -end - --- multi-assignment -do - local a, b = 1, 2 - assert(a == 1 and b == 2) -end - --- field access -do - local t = {field = 10} - assert(t.field == 10) - t.field = 20 - assert(t.field == 20) -end - --- method calls -do - local s = "hello" - assert(s:upper() == "HELLO") -end - --- indexing -do - local t = {10, 20, 30} - assert(t[2] == 20) - t[2] = 99 - assert(t[2] == 99) -end - --- table-arg function call -do - local function f(t) return t[1] end - assert(f {42} == 42) -end - --- keyword-led statements -do - local x = 1 - if x == 1 then x = 2 end - assert(x == 2) - for i = 1, 1 do x = 3 end - assert(x == 3) - while x > 3 do x = x - 1 end - assert(x == 3) - repeat x = x - 1 until x == 0 - assert(x == 0) -end - --- ===== INTERLEAVED LUA AND SHELL ===== - -do - local x = 10 - ls / - assert(_.code == 0) - local y = x + 20 - assert(y == 30) - echo hello - assert(_.code == 0) - local z = y * 2 - assert(z == 60) -end - --- ===== EDGE CASES ===== - --- double-dash flags (--) look like Lua comments to the lexer. --- the parser must capture raw source text BEFORE the lexer consumes --- the comment, so the full argument string is preserved. -do - git --version - assert(_.code == 0) - ls --color=auto /tmp - assert(_.code == 0) -- may fail if ls doesn't support --color -end - --- commands where first arg is another known command name -do - env ls - -- env runs ls; both are valid commands, this is identifier + identifier - assert(type(_.code) == "number") -end - --- semicolons: Lua uses ; as optional statement separator. --- with the heuristic, ls followed by ; is ambiguous. --- for now, use separate lines instead: -do - ls /tmp - echo done - assert(_.code == 0) -end - -print "OK" - ---]] \ No newline at end of file diff --git a/testes/lush/commands.lua b/testes/lush/commands.lua index 8da0d51c..e4ffa99b 100644 --- a/testes/lush/commands.lua +++ b/testes/lush/commands.lua @@ -87,4 +87,10 @@ do assert(r.stdout == "a\nb\nc\n") end +-- process killed by signal returns 128 + signal number +do + local r = `sh -c "kill -9 $$"` + assert(r.code == 137, "expected 128+9=137, got: " .. r.code) +end + print "OK" diff --git a/testes/lush/globbing.lua b/testes/lush/globbing.lua index bc50b11c..2191f811 100644 --- a/testes/lush/globbing.lua +++ b/testes/lush/globbing.lua @@ -132,4 +132,40 @@ do end +-- brace with suffix +do + os.execute("mkdir -p /tmp/_lush_brace_test") + os.execute("touch /tmp/_lush_brace_test/a.txt /tmp/_lush_brace_test/b.txt") + local r = `echo /tmp/_lush_brace_test/{a,b}.txt` + local out = r.stdout:gsub("\n$", "") + assert(out == "/tmp/_lush_brace_test/a.txt /tmp/_lush_brace_test/b.txt", + "brace suffix failed, got: " .. out) + os.execute("rm -rf /tmp/_lush_brace_test") +end + +-- no commas in braces → no expansion +do + local r = `echo {abc}` + local out = r.stdout:gsub("\n$", "") + assert(out == "{abc}", "no-comma braces should not expand, got: " .. out) +end + +-- [?] glob metacharacter +do + os.execute("mkdir -p /tmp/_lush_bracket_test") + os.execute("touch /tmp/_lush_bracket_test/fa /tmp/_lush_bracket_test/fb") + local r = `echo /tmp/_lush_bracket_test/f[ab]` + local out = r.stdout:gsub("\n$", "") + assert(out:find("fa"), "bracket glob should match fa, got: " .. out) + assert(out:find("fb"), "bracket glob should match fb, got: " .. out) + os.execute("rm -rf /tmp/_lush_bracket_test") +end + +-- tilde NOT expanded mid-word +do + local r = `echo foo~bar` + local out = r.stdout:gsub("\n$", "") + assert(out == "foo~bar", "mid-word tilde should not expand, got: " .. out) +end + print "OK" diff --git a/testes/lush/interactive.lua b/testes/lush/interactive.lua index 30ee964f..aba9c7d0 100644 --- a/testes/lush/interactive.lua +++ b/testes/lush/interactive.lua @@ -102,4 +102,14 @@ do assert(_.code == 0) end +-- empty interactive command (whitespace only) +do + _ = nil +! + assert(type(_) == "table") + assert(_.code == 0) + assert(_.stdout == "") + assert(_.stderr == "") +end + print "OK" diff --git a/testes/lush/lushlib.lua b/testes/lush/lushlib.lua index 6d6ecbcb..5550e56c 100644 --- a/testes/lush/lushlib.lua +++ b/testes/lush/lushlib.lua @@ -189,4 +189,91 @@ do end +-- === lush.commands (user-defined commands) === + +do + assert(type(lush.commands) == "table", "lush.commands missing") +end + +-- basic user command via backtick +do + lush.commands.greetcmd = function(name, ...) + print(table.concat({...}, " ")) + end + local r = `greetcmd hello world` + assert(r.code == 0, "user command failed: " .. r.code) + assert(r.stdout == "hello world\n", + "expected 'hello world\\n', got: " .. r.stdout) + lush.commands.greetcmd = nil +end + +-- user command returning integer exit code +do + lush.commands.exitwith = function(name, code) + return tonumber(code) + end + local r = `exitwith 42` + assert(r.code == 42, "expected code 42, got: " .. r.code) + lush.commands.exitwith = nil +end + +-- user command returning table with code field +do + lush.commands.tblret = function(name) + return { code = 7 } + end + local r = `tblret` + assert(r.code == 7, "expected code 7, got: " .. r.code) + lush.commands.tblret = nil +end + +-- user command error → nonzero exit + stderr +do + lush.commands.failcmd = function(name) + error("deliberate error") + end + local r = `failcmd` + assert(r.code ~= 0, "erroring command should have nonzero exit") + assert(r.stderr:find("deliberate error"), + "expected error in stderr, got: " .. r.stderr) + lush.commands.failcmd = nil +end + +-- removed user command falls through to exec (command not found) +do + lush.commands.tmptest = function() end + lush.commands.tmptest = nil + local r = `tmptest` + assert(r.code == 127, "removed command should not be found") +end + +-- user command via lush.command() +do + lush.commands.apicmd = function(name, ...) + print("api " .. table.concat({...}, " ")) + end + local r = lush.command("apicmd x y") + assert(r.stdout == "api x y\n", + "expected 'api x y\\n', got: " .. r.stdout) + lush.commands.apicmd = nil +end + + +-- === lush.subcmd() direct call === + +do + local result = lush.subcmd("echo hello") + assert(result == "hello", "subcmd should return stdout without trailing newline, got: " .. tostring(result)) +end + + +-- === lush.interactive() direct call === + +do + lush.interactive("true") + assert(type(_) == "table") + assert(_.code == 0) +end + + print "OK" diff --git a/testes/lush/piping.lua b/testes/lush/piping.lua index 332d4b8a..0d90ed38 100644 --- a/testes/lush/piping.lua +++ b/testes/lush/piping.lua @@ -54,13 +54,6 @@ do assert(r.stderr == "", "middle stderr not captured: '" .. r.stderr .. "'") end --- single command unchanged (regression check) -do - local r = `echo hello` - assert(r.stdout == "hello\n") - assert(r.code == 0) -end - -- pipe preserves multi-line output do local r = `printf "line1\nline2\nline3\n" | cat` @@ -80,4 +73,10 @@ do assert(r.stderr == "piperr\n", "last stage stderr: " .. r.stderr) end +-- backslash-escaped pipe is NOT a separator +do + local r = `echo a\|b` + assert(r.stdout == "a|b\n", "escaped pipe: " .. r.stdout) +end + print "OK"