Bash · 17940 bytes Raw Blame History
1 #!/usr/bin/env bash
2 # POSIX Compliance Coverage Tests - Filling Identified Gaps
3 # These tests cover edge cases and behaviors not covered by other test suites
4
5 SHELL_BIN="${SHELL_BIN:?ERROR: SHELL_BIN must be set}"
6 BASH_REF="${BASH_REF:-bash}"
7
8 # Colors
9 RED='\033[0;31m'
10 GREEN='\033[0;32m'
11 YELLOW='\033[1;33m'
12 BLUE='\033[0;34m'
13 NC='\033[0m'
14
15 # Test identification
16 TEST_PREFIX="[posix-coverage]"
17 CURRENT_SECTION=""
18 TEST_NUM=0
19
20 PASSED=0
21 FAILED=0
22 SKIPPED=0
23 FAILED_TESTS_LIST=""
24
25 pass() {
26 TEST_NUM=$((TEST_NUM + 1))
27 printf "${GREEN}✓ PASS${NC} ${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}: %s\n" "$1"
28 PASSED=$((PASSED + 1))
29 }
30
31 fail() {
32 TEST_NUM=$((TEST_NUM + 1))
33 TEST_ID="${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}"
34 printf "${RED}✗ FAIL${NC} ${TEST_ID}: %s\n" "$1"
35 FAILED_TESTS_LIST="${FAILED_TESTS_LIST} ${TEST_ID}: $1\n"
36 if [ -n "$2" ]; then
37 printf " expected: %s\n" "$2"
38 fi
39 if [ -n "$3" ]; then
40 printf " got: %s\n" "$3"
41 fi
42 FAILED=$((FAILED + 1))
43 }
44
45 skip() {
46 TEST_NUM=$((TEST_NUM + 1))
47 printf "${YELLOW}⊘ SKIP${NC} ${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}: %s\n" "$1"
48 SKIPPED=$((SKIPPED + 1))
49 }
50
51 section() {
52 # Extract section number from header like "200. ARITHMETIC EDGE CASES"
53 CURRENT_SECTION=$(echo "$1" | grep -oE '^[0-9]+' || echo "0")
54 TEST_NUM=0
55 printf "\n${BLUE}==========================================\n"
56 printf "%s\n" "$1"
57 printf "==========================================${NC}\n"
58 }
59
60 # Normalize output by stripping shell name prefix
61 normalize_output() {
62 sed -e 's|^[^ ]*/[a-z]*sh[0-9]*: |sh: |' -e 's|^[a-z]*sh[0-9]*: |sh: |' -e 's/line [0-9]*: //'
63 }
64
65 # Compare output between sh and fortsh
66 compare_posix_output() {
67 test_name="$1"
68 test_cmd="$2"
69
70 posix_output=$(env $RC_DISABLE_ENV "$BASH_REF" -c "$test_cmd" 2>&1 | normalize_output)
71 posix_exit=$?
72
73 shell_output=$(env $RC_DISABLE_ENV "$SHELL_BIN" -c "$test_cmd" 2>&1 | normalize_output)
74 shell_exit=$?
75
76 if [ "$posix_output" = "$shell_output" ] && [ "$posix_exit" = "$shell_exit" ]; then
77 pass "$test_name"
78 else
79 fail "$test_name" "$posix_output" "$shell_output"
80 fi
81 }
82
83 # Normalize shell error messages by stripping shell name and "line N: " prefix
84 # POSIX doesn't mandate error message format, so we normalize for comparison
85 normalize_error() {
86 echo "$1" | sed -e 's|^[^ ]*bash: |sh: |g' -e 's|[^ ]*bash: |sh: |g' -e 's|^[a-z]*sh[0-9]*: |sh: |g' -e 's|[a-z]*sh[0-9]*: |sh: |g' -e 's/line [0-9]*: //g' -e 's/-c: //g'
87 }
88
89 # Compare error output, normalizing line number differences
90 compare_posix_error() {
91 test_name="$1"
92 test_cmd="$2"
93
94 posix_output=$(env $RC_DISABLE_ENV "$BASH_REF" -c "$test_cmd" 2>&1)
95 posix_exit=$?
96
97 shell_output=$(env $RC_DISABLE_ENV "$SHELL_BIN" -c "$test_cmd" 2>&1)
98 shell_exit=$?
99
100 posix_norm=$(normalize_error "$posix_output")
101 shell_norm=$(normalize_error "$shell_output")
102
103 if [ "$posix_norm" = "$shell_norm" ] && [ "$posix_exit" = "$shell_exit" ]; then
104 pass "$test_name"
105 else
106 fail "$test_name" "$posix_output" "$shell_output"
107 fi
108 }
109
110 # Test that fortsh accepts without error
111 test_accepts() {
112 test_name="$1"
113 test_cmd="$2"
114
115 if env $RC_DISABLE_ENV "$SHELL_BIN" -c "$test_cmd" >/dev/null 2>&1; then
116 pass "$test_name"
117 else
118 fail "$test_name" "should succeed" "failed or not implemented"
119 fi
120 }
121
122 # Test that command fails
123 test_fails() {
124 test_name="$1"
125 test_cmd="$2"
126
127 if ! env $RC_DISABLE_ENV "$SHELL_BIN" -c "$test_cmd" >/dev/null 2>&1; then
128 pass "$test_name"
129 else
130 fail "$test_name" "should fail" "succeeded unexpectedly"
131 fi
132 }
133
134 # =====================================
135 # ARITHMETIC EDGE CASES
136 # =====================================
137
138 section "200. ARITHMETIC - OCTAL AND HEX LITERALS"
139
140 compare_posix_output "octal literal" 'echo $((010))'
141 compare_posix_output "hex literal lowercase" 'echo $((0xff))'
142 compare_posix_output "hex literal uppercase" 'echo $((0xFF))'
143 compare_posix_output "octal in expression" 'echo $((010 + 1))'
144 compare_posix_output "hex in expression" 'echo $((0x10 + 1))'
145 compare_posix_output "mixed bases" 'echo $((0x10 + 010 + 10))'
146
147 section "201. ARITHMETIC - NEGATIVE NUMBERS"
148
149 compare_posix_output "negative literal" 'echo $((-5))'
150 compare_posix_output "negative in addition" 'echo $((10 + -3))'
151 compare_posix_output "negative in subtraction" 'echo $((5 - -3))'
152 compare_posix_output "double negative" 'echo $((--5))'
153 compare_posix_output "negative multiplication" 'echo $((-3 * -4))'
154 compare_posix_output "negative division" 'echo $((-10 / 3))'
155 compare_posix_output "negative modulo" 'echo $((-10 % 3))'
156
157 section "202. ARITHMETIC - NESTED EXPRESSIONS"
158
159 compare_posix_output "nested arithmetic" 'echo $((1 + $((2 + 3))))'
160 compare_posix_output "deeply nested" 'echo $((1 + $((2 + $((3 + 4))))))'
161 compare_posix_output "nested with vars" 'a=5; echo $((a + $((a * 2))))'
162
163 section "203. ARITHMETIC - COMMA OPERATOR"
164
165 compare_posix_output "comma operator" 'echo $((1, 2, 3))'
166 compare_posix_output "comma with assignment" 'echo $((a=1, b=2, a+b))'
167 compare_posix_output "comma side effects" 'a=0; echo $((a=1, a=a+1, a))'
168
169 # =====================================
170 # WORD SPLITTING EDGE CASES
171 # =====================================
172
173 section "210. WORD SPLITTING - EMPTY IFS"
174
175 compare_posix_output "empty IFS no split" 'IFS=""; var="a b c"; set -- $var; echo $#'
176 compare_posix_output "empty IFS preserves spaces" 'IFS=""; var="a b"; set -- $var; echo "$1"'
177
178 section "211. WORD SPLITTING - IFS VARIATIONS"
179
180 compare_posix_output "IFS whitespace only" 'IFS=" "; var="a b"; set -- $var; echo $#'
181 compare_posix_output "IFS non-whitespace" 'IFS=":"; var="a:b:c"; set -- $var; echo $#'
182 compare_posix_output "IFS mixed" 'IFS=" :"; var="a : b"; set -- $var; echo $#'
183 compare_posix_output "IFS tab" 'IFS=" "; var="a b c"; set -- $var; echo $#'
184
185 section "212. WORD SPLITTING - $@ VS $*"
186
187 compare_posix_output "$* unquoted" 'set -- "a b" "c d"; for x in $*; do echo "[$x]"; done'
188 compare_posix_output "$@ unquoted" 'set -- "a b" "c d"; for x in $@; do echo "[$x]"; done'
189 compare_posix_output "$* quoted" 'set -- "a b" "c d"; for x in "$*"; do echo "[$x]"; done'
190 compare_posix_output "$@ quoted" 'set -- "a b" "c d"; for x in "$@"; do echo "[$x]"; done'
191 compare_posix_output "$* with IFS" 'IFS=:; set -- a b c; echo "$*"'
192 compare_posix_output "$@ with IFS" 'IFS=:; set -- a b c; echo "$@"'
193
194 # =====================================
195 # PARAMETER EXPANSION EDGE CASES
196 # =====================================
197
198 section "220. PARAMETER EXPANSION - EMPTY VS UNSET"
199
200 compare_posix_output ":+ empty var" 'var=""; echo "${var:+set}"'
201 compare_posix_output ":+ unset var" 'unset var; echo "${var:+set}"'
202 compare_posix_output "+ empty var" 'var=""; echo "${var+set}"'
203 compare_posix_output "+ unset var" 'unset var; echo "${var+set}"'
204 compare_posix_output ":- empty var" 'var=""; echo "${var:-default}"'
205 compare_posix_output "- empty var" 'var=""; echo "${var-default}"'
206
207 section "221. PARAMETER EXPANSION - ASSIGNMENT FORMS"
208
209 compare_posix_output ":= assigns when empty" 'unset var; echo "${var:=default}"; echo "$var"'
210 compare_posix_output "= assigns when unset" 'unset var; echo "${var=default}"; echo "$var"'
211 compare_posix_output ":= with empty" 'var=""; echo "${var:=default}"; echo "$var"'
212 compare_posix_output "= with empty" 'var=""; echo "${var=default}"; echo "$var"'
213
214 section "222. PARAMETER EXPANSION - PATTERN IN EXPANSION"
215
216 compare_posix_output "nested pattern removal" 'suffix=.txt; file=test.txt; echo "${file%$suffix}"'
217 compare_posix_output "var in pattern" 'pat=t; var=test; echo "${var#$pat}"'
218 compare_posix_output "var in suffix pattern" 'pat=st; var=test; echo "${var%$pat}"'
219
220 # =====================================
221 # SIGNAL AND TRAP EDGE CASES
222 # =====================================
223
224 section "230. TRAP - IGNORE VS RESET"
225
226 # Test trap behavior by checking patterns (bash versions may differ in exact output format)
227 test_trap_behavior() {
228 test_name="$1"
229 test_cmd="$2"
230 expected_pattern="$3"
231
232 output=$(env $RC_DISABLE_ENV "$SHELL_BIN" -c "$test_cmd" 2>&1)
233
234 if echo "$output" | grep -qE "$expected_pattern"; then
235 pass "$test_name"
236 else
237 fail "$test_name" "expected pattern: $expected_pattern" "got: $output"
238 fi
239 }
240
241 # trap "" INT sets ignore, trap shows it, trap - INT resets, trap shows nothing for INT
242 test_trap_behavior "trap ignore" 'trap "" INT; trap; trap - INT; trap' "trap.*INT"
243 test_trap_behavior "trap reset" 'trap "echo caught" INT; trap - INT; trap' "^$"
244 compare_posix_output "trap empty string" 'trap "" EXIT; echo done'
245
246 section "231. TRAP - IN SUBSHELLS"
247
248 # Non-ignored traps should show in parent but subshell inherits parent's trap display
249 # The EXIT trap runs when done, printing "trap"
250 test_trap_behavior "trap not inherited" 'trap "echo trap" EXIT; (trap); echo done' "done"
251 # Ignored traps ARE inherited by subshells
252 test_trap_behavior "ignore trap inherited" 'trap "" INT; (trap)' "trap.*INT"
253
254 section "232. TRAP - MULTIPLE SIGNALS"
255
256 compare_posix_output "trap multiple" 'trap "echo exit" EXIT; trap "echo err" ERR 2>/dev/null || true; echo done'
257
258 # =====================================
259 # ERROR HANDLING EDGE CASES
260 # =====================================
261
262 section "240. SET -e EDGE CASES"
263
264 compare_posix_output "set -e with &&" 'set -e; false && true; echo reached'
265 compare_posix_output "set -e with ||" 'set -e; false || true; echo reached'
266 compare_posix_output "set -e with !" 'set -e; ! false; echo reached'
267 compare_posix_output "set -e in if condition" 'set -e; if false; then echo no; fi; echo reached'
268 compare_posix_output "set -e in while condition" 'set -e; while false; do echo no; done; echo reached'
269
270 section "241. SET -e IN SUBSHELLS"
271
272 compare_posix_output "set -e inherited" 'set -e; (false; echo no) || echo caught'
273 compare_posix_output "set -e in command sub" 'set -e; x=$(false; echo yes); echo "x=$x"'
274
275 section "242. COMMAND NOT FOUND"
276
277 # Test exit status for command not found (should be 127)
278 test_cmd='nonexistent_command_12345 2>/dev/null; echo $?'
279 compare_posix_output "command not found status" "$test_cmd"
280
281 # =====================================
282 # FUNCTION EDGE CASES
283 # =====================================
284
285 section "250. FUNCTION - BUILTIN SHADOWING"
286
287 compare_posix_output "function shadows builtin" 'echo() { printf "custom: %s\n" "$1"; }; echo test; unset -f echo'
288 compare_posix_output "function named cd" 'cd() { echo "custom cd"; }; cd /tmp; unset -f cd'
289
290 section "251. FUNCTION - REDEFINITION"
291
292 compare_posix_output "redefine function" 'f() { echo v1; }; f; f() { echo v2; }; f'
293
294 section "252. FUNCTION - INDIRECT RECURSION"
295
296 compare_posix_output "mutual recursion" 'a() { [ $1 -le 0 ] && echo done || b $(($1-1)); }; b() { a $1; }; a 3'
297
298 section "253. FUNCTION - RETURN EDGE CASES"
299
300 compare_posix_output "return outside function" '(return 5 2>/dev/null); echo $?'
301 compare_posix_output "return in sourced file" 'echo "return 42" > /tmp/ret.sh; . /tmp/ret.sh; echo $?; rm /tmp/ret.sh'
302
303 # =====================================
304 # REDIRECTION EDGE CASES
305 # =====================================
306
307 section "260. REDIRECTION - CLOSED FD OPERATIONS"
308
309 compare_posix_error "write to closed fd" 'exec 3>&-; echo test >&3 2>&1 | head -1'
310 compare_posix_error "read from closed fd" 'exec 3<&-; cat <&3 2>&1 | head -1'
311
312 section "261. REDIRECTION - MULTIPLE HEREDOCS"
313
314 compare_posix_output "two heredocs" 'cat <<EOF1; cat <<EOF2
315 first
316 EOF1
317 second
318 EOF2'
319
320 section "262. REDIRECTION - NOCLOBBER WITH APPEND"
321
322 compare_posix_output "noclobber allows append" 'set -C; echo a > /tmp/nc_test; echo b >> /tmp/nc_test; cat /tmp/nc_test; rm /tmp/nc_test'
323
324 # =====================================
325 # PIPELINE EDGE CASES
326 # =====================================
327
328 section "270. PIPELINE - BUILTIN ONLY"
329
330 compare_posix_output "pipeline all builtins" 'echo test | { read x; echo "got: $x"; }'
331 compare_posix_output "pipeline with :" ': | echo piped'
332
333 section "271. PIPELINE - LONG CHAINS"
334
335 compare_posix_output "5-stage pipeline" 'echo test | cat | cat | cat | cat | cat'
336 # Pipeline timing can produce non-deterministic pipe errors — filter them out
337 TEST_NUM=$((TEST_NUM + 1))
338 _pf_expected=$(env $RC_DISABLE_ENV "$BASH_REF" -c 'echo ok | false | cat; echo $?' 2>&1 | grep -v 'Broken pipe' | normalize_output)
339 _pf_actual=$(env $RC_DISABLE_ENV "$SHELL_BIN" -c 'echo ok | false | cat; echo $?' 2>&1 | grep -v 'Broken pipe' | normalize_output)
340 if [ "$_pf_expected" = "$_pf_actual" ]; then pass "pipeline with failures"
341 else fail "pipeline with failures" "$_pf_expected" "$_pf_actual"; fi
342
343 # =====================================
344 # ALIAS EDGE CASES
345 # =====================================
346
347 section "280. ALIAS - SPECIAL CASES"
348
349 compare_posix_error "alias with quotes" "alias greet='echo hello'; greet; unalias greet"
350 compare_posix_error "alias with semicolon" "alias both='echo a; echo b'; both; unalias both"
351 compare_posix_output "alias -p format" 'alias foo=bar; alias -p | grep foo; unalias foo'
352
353 section "281. ALIAS - EXPANSION CONTEXT"
354
355 # Aliases don't expand in non-interactive mode - both bash and fortsh should fail
356 # Test that we get "command not found" error (exact format varies by bash version)
357 test_alias_behavior() {
358 test_name="$1"
359 test_cmd="$2"
360 expected_pattern="$3"
361
362 output=$(env $RC_DISABLE_ENV "$SHELL_BIN" -c "$test_cmd" 2>&1)
363
364 if echo "$output" | grep -qiE "$expected_pattern"; then
365 pass "$test_name"
366 else
367 fail "$test_name" "expected pattern: $expected_pattern" "got: $output"
368 fi
369 }
370
371 test_alias_behavior "alias in function" "alias e=echo; f() { e test; }; f; unalias e" "command not found|not found"
372
373 # =====================================
374 # DOT (SOURCE) EDGE CASES
375 # =====================================
376
377 section "290. DOT - PATH HANDLING"
378
379 compare_posix_output "dot with arguments" 'echo "echo args: \$1 \$2" > /tmp/src.sh; . /tmp/src.sh a b; rm /tmp/src.sh'
380 compare_posix_output "dot return value" 'echo "false" > /tmp/src.sh; . /tmp/src.sh; echo $?; rm /tmp/src.sh'
381 compare_posix_output "dot modifies vars" 'echo "X=modified" > /tmp/src.sh; X=original; . /tmp/src.sh; echo $X; rm /tmp/src.sh'
382
383 # =====================================
384 # SPECIAL BUILTIN EDGE CASES
385 # =====================================
386
387 section "300. SPECIAL BUILTINS - ERROR BEHAVIOR"
388
389 compare_posix_output "break outside loop" '(break 2>/dev/null); echo $?'
390 compare_posix_output "continue outside loop" '(continue 2>/dev/null); echo $?'
391 compare_posix_output "colon with redirect" ': > /tmp/colon_test; test -f /tmp/colon_test && echo exists; rm /tmp/colon_test'
392
393 # =====================================
394 # ENVIRONMENT EDGE CASES
395 # =====================================
396
397 section "310. ENVIRONMENT - PATH HANDLING"
398
399 compare_posix_output "empty PATH component" 'echo "echo found" > /tmp/pathtest; chmod +x /tmp/pathtest; PATH="/tmp:$PATH" pathtest; rm /tmp/pathtest'
400
401 section "311. ENVIRONMENT - EXPORT BEHAVIOR"
402
403 compare_posix_output "export in subshell" 'X=1; (export X=2); echo $X'
404 compare_posix_output "export preserves" 'export X=1; X=2; sh -c "echo \$X"'
405
406 # =====================================
407 # GLOB EDGE CASES
408 # =====================================
409
410 section "320. GLOB - SPECIAL FILENAMES"
411
412 compare_posix_output "glob with spaces" 'mkdir -p /tmp/gt; touch "/tmp/gt/a b"; echo /tmp/gt/*; rm -r /tmp/gt'
413 compare_posix_output "glob no match" 'echo /nonexistent/path/*.xyz 2>/dev/null'
414
415 section "321. GLOB - CHARACTER CLASSES"
416
417 compare_posix_output "glob [:alpha:]" 'mkdir -p /tmp/gc; touch /tmp/gc/a1 /tmp/gc/1a; echo /tmp/gc/[[:alpha:]]*; rm -r /tmp/gc'
418 compare_posix_output "glob [:digit:]" 'mkdir -p /tmp/gc; touch /tmp/gc/a1 /tmp/gc/1a; echo /tmp/gc/[[:digit:]]*; rm -r /tmp/gc'
419
420 # =====================================
421 # QUOTING EDGE CASES
422 # =====================================
423
424 section "330. QUOTING - COMPLEX NESTING"
425
426 compare_posix_output "quotes in command sub" 'echo "$(echo "inner")"'
427 compare_posix_output "escaped quotes" 'echo "say \"hello\""'
428 compare_posix_output "single in double" "echo \"it's\""
429 compare_posix_output "backslash in single" "echo 'back\\slash'"
430
431 section "331. QUOTING - EMPTY STRINGS"
432
433 compare_posix_output "empty preserves arg" 'set -- "" "a"; echo $#'
434 compare_posix_output "unquoted empty gone" 'e=""; set -- $e a; echo $#'
435
436 # =====================================
437 # MISCELLANEOUS EDGE CASES
438 # =====================================
439
440 section "340. MISC - COMPOUND COMMANDS"
441
442 compare_posix_output "if with compound cond" 'if true && true; then echo yes; fi'
443 compare_posix_output "while with pipeline cond" 'n=0; while echo $n | grep -q 0 && [ $n -lt 1 ]; do n=$((n+1)); done; echo $n'
444
445 section "341. MISC - COMPLEX COMBINATIONS"
446
447 compare_posix_output "redirect in loop" 'for i in 1 2 3; do echo $i; done > /tmp/loop_out; cat /tmp/loop_out; rm /tmp/loop_out'
448 compare_posix_output "case with patterns" 'x=abc; case $x in a*) echo match;; esac'
449
450 # =====================================
451 # SUMMARY
452 # =====================================
453
454 printf "\n==========================================\n"
455 printf "COVERAGE GAP TEST RESULTS ${TEST_PREFIX}\n"
456 printf "==========================================\n"
457 printf "${GREEN}Passed:${NC} %d\n" "$PASSED"
458 printf "${RED}Failed:${NC} %d\n" "$FAILED"
459 printf "${YELLOW}Skipped:${NC} %d\n" "$SKIPPED"
460 printf "Total: %d\n" "$((PASSED + FAILED + SKIPPED))"
461 printf "==========================================\n"
462
463 if [ "$((PASSED + FAILED))" -gt 0 ]; then
464 pass_rate=$((PASSED * 100 / (PASSED + FAILED)))
465 printf "Pass rate: %d%%\n" "$pass_rate"
466 fi
467
468 if [ "$FAILED" -gt 0 ]; then
469 printf "\n${RED}Failed tests:${NC}\n"
470 printf "%b" "$FAILED_TESTS_LIST"
471 printf "==========================================\n"
472 fi
473
474 if [ "$FAILED" -eq 0 ]; then
475 printf "${GREEN}ALL COVERAGE GAP TESTS PASSED!${NC} ✓\n"
476 exit 0
477 else
478 printf "${RED}SOME TESTS FAILED${NC} ✗\n"
479 exit 1
480 fi