| 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 |