Bash · 17429 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 FORTSH_BIN="${FORTSH_BIN:-./bin/fortsh}"
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/^bash: /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=$(FORTSH_RC_FILE=/dev/null "$BASH_REF" -c "$test_cmd" 2>&1 | normalize_output)
71 posix_exit=$?
72
73 fortsh_output=$(FORTSH_RC_FILE=/dev/null "$FORTSH_BIN" -c "$test_cmd" 2>&1 | normalize_output)
74 fortsh_exit=$?
75
76 if [ "$posix_output" = "$fortsh_output" ] && [ "$posix_exit" = "$fortsh_exit" ]; then
77 pass "$test_name"
78 else
79 fail "$test_name" "$posix_output" "$fortsh_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/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=$(FORTSH_RC_FILE=/dev/null "$BASH_REF" -c "$test_cmd" 2>&1)
95 posix_exit=$?
96
97 fortsh_output=$(FORTSH_RC_FILE=/dev/null "$FORTSH_BIN" -c "$test_cmd" 2>&1)
98 fortsh_exit=$?
99
100 posix_norm=$(normalize_error "$posix_output")
101 fortsh_norm=$(normalize_error "$fortsh_output")
102
103 if [ "$posix_norm" = "$fortsh_norm" ] && [ "$posix_exit" = "$fortsh_exit" ]; then
104 pass "$test_name"
105 else
106 fail "$test_name" "$posix_output" "$fortsh_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 FORTSH_RC_FILE=/dev/null "$FORTSH_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 ! FORTSH_RC_FILE=/dev/null "$FORTSH_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=$(FORTSH_RC_FILE=/dev/null "$FORTSH_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 compare_posix_output "pipeline with failures" 'echo ok | false | cat; echo $?'
337
338 # =====================================
339 # ALIAS EDGE CASES
340 # =====================================
341
342 section "280. ALIAS - SPECIAL CASES"
343
344 compare_posix_error "alias with quotes" "alias greet='echo hello'; greet; unalias greet"
345 compare_posix_error "alias with semicolon" "alias both='echo a; echo b'; both; unalias both"
346 compare_posix_output "alias -p format" 'alias foo=bar; alias -p | grep foo; unalias foo'
347
348 section "281. ALIAS - EXPANSION CONTEXT"
349
350 # Aliases don't expand in non-interactive mode - both bash and fortsh should fail
351 # Test that we get "command not found" error (exact format varies by bash version)
352 test_alias_behavior() {
353 test_name="$1"
354 test_cmd="$2"
355 expected_pattern="$3"
356
357 output=$(FORTSH_RC_FILE=/dev/null "$FORTSH_BIN" -c "$test_cmd" 2>&1)
358
359 if echo "$output" | grep -qiE "$expected_pattern"; then
360 pass "$test_name"
361 else
362 fail "$test_name" "expected pattern: $expected_pattern" "got: $output"
363 fi
364 }
365
366 test_alias_behavior "alias in function" "alias e=echo; f() { e test; }; f; unalias e" "command not found|not found"
367
368 # =====================================
369 # DOT (SOURCE) EDGE CASES
370 # =====================================
371
372 section "290. DOT - PATH HANDLING"
373
374 compare_posix_output "dot with arguments" 'echo "echo args: \$1 \$2" > /tmp/src.sh; . /tmp/src.sh a b; rm /tmp/src.sh'
375 compare_posix_output "dot return value" 'echo "false" > /tmp/src.sh; . /tmp/src.sh; echo $?; rm /tmp/src.sh'
376 compare_posix_output "dot modifies vars" 'echo "X=modified" > /tmp/src.sh; X=original; . /tmp/src.sh; echo $X; rm /tmp/src.sh'
377
378 # =====================================
379 # SPECIAL BUILTIN EDGE CASES
380 # =====================================
381
382 section "300. SPECIAL BUILTINS - ERROR BEHAVIOR"
383
384 compare_posix_output "break outside loop" '(break 2>/dev/null); echo $?'
385 compare_posix_output "continue outside loop" '(continue 2>/dev/null); echo $?'
386 compare_posix_output "colon with redirect" ': > /tmp/colon_test; test -f /tmp/colon_test && echo exists; rm /tmp/colon_test'
387
388 # =====================================
389 # ENVIRONMENT EDGE CASES
390 # =====================================
391
392 section "310. ENVIRONMENT - PATH HANDLING"
393
394 compare_posix_output "empty PATH component" 'echo "echo found" > /tmp/pathtest; chmod +x /tmp/pathtest; PATH="/tmp:$PATH" pathtest; rm /tmp/pathtest'
395
396 section "311. ENVIRONMENT - EXPORT BEHAVIOR"
397
398 compare_posix_output "export in subshell" 'X=1; (export X=2); echo $X'
399 compare_posix_output "export preserves" 'export X=1; X=2; sh -c "echo \$X"'
400
401 # =====================================
402 # GLOB EDGE CASES
403 # =====================================
404
405 section "320. GLOB - SPECIAL FILENAMES"
406
407 compare_posix_output "glob with spaces" 'mkdir -p /tmp/gt; touch "/tmp/gt/a b"; echo /tmp/gt/*; rm -r /tmp/gt'
408 compare_posix_output "glob no match" 'echo /nonexistent/path/*.xyz 2>/dev/null'
409
410 section "321. GLOB - CHARACTER CLASSES"
411
412 compare_posix_output "glob [:alpha:]" 'mkdir -p /tmp/gc; touch /tmp/gc/a1 /tmp/gc/1a; echo /tmp/gc/[[:alpha:]]*; rm -r /tmp/gc'
413 compare_posix_output "glob [:digit:]" 'mkdir -p /tmp/gc; touch /tmp/gc/a1 /tmp/gc/1a; echo /tmp/gc/[[:digit:]]*; rm -r /tmp/gc'
414
415 # =====================================
416 # QUOTING EDGE CASES
417 # =====================================
418
419 section "330. QUOTING - COMPLEX NESTING"
420
421 compare_posix_output "quotes in command sub" 'echo "$(echo "inner")"'
422 compare_posix_output "escaped quotes" 'echo "say \"hello\""'
423 compare_posix_output "single in double" "echo \"it's\""
424 compare_posix_output "backslash in single" "echo 'back\\slash'"
425
426 section "331. QUOTING - EMPTY STRINGS"
427
428 compare_posix_output "empty preserves arg" 'set -- "" "a"; echo $#'
429 compare_posix_output "unquoted empty gone" 'e=""; set -- $e a; echo $#'
430
431 # =====================================
432 # MISCELLANEOUS EDGE CASES
433 # =====================================
434
435 section "340. MISC - COMPOUND COMMANDS"
436
437 compare_posix_output "if with compound cond" 'if true && true; then echo yes; fi'
438 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'
439
440 section "341. MISC - COMPLEX COMBINATIONS"
441
442 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'
443 compare_posix_output "case with patterns" 'x=abc; case $x in a*) echo match;; esac'
444
445 # =====================================
446 # SUMMARY
447 # =====================================
448
449 printf "\n==========================================\n"
450 printf "COVERAGE GAP TEST RESULTS ${TEST_PREFIX}\n"
451 printf "==========================================\n"
452 printf "${GREEN}Passed:${NC} %d\n" "$PASSED"
453 printf "${RED}Failed:${NC} %d\n" "$FAILED"
454 printf "${YELLOW}Skipped:${NC} %d\n" "$SKIPPED"
455 printf "Total: %d\n" "$((PASSED + FAILED + SKIPPED))"
456 printf "==========================================\n"
457
458 if [ "$((PASSED + FAILED))" -gt 0 ]; then
459 pass_rate=$((PASSED * 100 / (PASSED + FAILED)))
460 printf "Pass rate: %d%%\n" "$pass_rate"
461 fi
462
463 if [ "$FAILED" -gt 0 ]; then
464 printf "\n${RED}Failed tests:${NC}\n"
465 printf "%b" "$FAILED_TESTS_LIST"
466 printf "==========================================\n"
467 fi
468
469 if [ "$FAILED" -eq 0 ]; then
470 printf "${GREEN}ALL COVERAGE GAP TESTS PASSED!${NC} ✓\n"
471 exit 0
472 else
473 printf "${RED}SOME TESTS FAILED${NC} ✗\n"
474 exit 1
475 fi