| 1 | #!/usr/bin/env bash |
| 2 | # ============================================================================== |
| 3 | # POSIX Compliance - Builtin Commands & Shell Options Test Suite |
| 4 | # ============================================================================== |
| 5 | # Tests all gaps identified in POSIX compliance analysis (2025-10-17) |
| 6 | # Covers: P0 (critical), P1 (important), P2 (nice-to-have) issues |
| 7 | # ============================================================================== |
| 8 | |
| 9 | # Configuration |
| 10 | SHELL_BIN="${SHELL_BIN:?ERROR: SHELL_BIN must be set}" |
| 11 | VERBOSE="${VERBOSE:-0}" |
| 12 | |
| 13 | # Test identification |
| 14 | TEST_PREFIX="[posix-builtins]" |
| 15 | |
| 16 | # Test counters |
| 17 | TOTAL_TESTS=0 |
| 18 | PASSED_TESTS=0 |
| 19 | FAILED_TESTS=0 |
| 20 | FAILED_TESTS_LIST="" |
| 21 | |
| 22 | # Color codes (if terminal supports them) |
| 23 | if [ -t 1 ]; then |
| 24 | RED='\033[0;31m' |
| 25 | GREEN='\033[0;32m' |
| 26 | YELLOW='\033[1;33m' |
| 27 | BLUE='\033[0;34m' |
| 28 | NC='\033[0m' # No Color |
| 29 | else |
| 30 | RED='' |
| 31 | GREEN='' |
| 32 | YELLOW='' |
| 33 | BLUE='' |
| 34 | NC='' |
| 35 | fi |
| 36 | |
| 37 | # Test result functions |
| 38 | pass() { |
| 39 | PASSED_TESTS=$((PASSED_TESTS + 1)) |
| 40 | TOTAL_TESTS=$((TOTAL_TESTS + 1)) |
| 41 | echo -e "${GREEN}✓${NC} ${TEST_PREFIX} $1" |
| 42 | } |
| 43 | |
| 44 | fail() { |
| 45 | FAILED_TESTS=$((FAILED_TESTS + 1)) |
| 46 | TOTAL_TESTS=$((TOTAL_TESTS + 1)) |
| 47 | echo -e "${RED}✗${NC} ${TEST_PREFIX} $1" |
| 48 | FAILED_TESTS_LIST="${FAILED_TESTS_LIST} ${TEST_PREFIX} $1\n" |
| 49 | if [ "$VERBOSE" = "1" ]; then |
| 50 | echo -e "${RED} Details: $2${NC}" |
| 51 | fi |
| 52 | } |
| 53 | |
| 54 | section() { |
| 55 | echo "" |
| 56 | echo -e "${BLUE}========================================${NC}" |
| 57 | echo -e "${BLUE}$1${NC}" |
| 58 | echo -e "${BLUE}========================================${NC}" |
| 59 | } |
| 60 | |
| 61 | # ============================================================================== |
| 62 | # P0: CRITICAL TESTS (Must pass for basic POSIX compliance) |
| 63 | # ============================================================================== |
| 64 | |
| 65 | test_p0_readonly_enforcement() { |
| 66 | section "P0-1: readonly Variable Enforcement" |
| 67 | |
| 68 | # Test 1: Cannot modify readonly variable |
| 69 | output=$($SHELL_BIN -c 'readonly VAR=test; VAR=other' 2>&1) |
| 70 | if echo "$output" | grep -q "readonly variable"; then |
| 71 | pass "P0-1.1: Readonly violation produces error message" |
| 72 | else |
| 73 | fail "P0-1.1: Readonly violation produces error message" "No error message found" |
| 74 | fi |
| 75 | |
| 76 | # Test 2: Exit code is 1 for readonly violation (bash compatibility) |
| 77 | $SHELL_BIN -c 'readonly VAR=test; VAR=other' >/dev/null 2>&1 |
| 78 | if [ $? -eq 1 ]; then |
| 79 | pass "P0-1.2: Readonly violation returns exit code 1" |
| 80 | else |
| 81 | fail "P0-1.2: Readonly violation returns exit code 1" "Got exit code $?" |
| 82 | fi |
| 83 | |
| 84 | # Test 3: Command after readonly violation should not execute |
| 85 | output=$($SHELL_BIN -c 'readonly VAR=test; VAR=other; echo SHOULD_NOT_PRINT' 2>&1) |
| 86 | if ! echo "$output" | grep -q "SHOULD_NOT_PRINT"; then |
| 87 | pass "P0-1.3: Commands after readonly violation do not execute" |
| 88 | else |
| 89 | fail "P0-1.3: Commands after readonly violation do not execute" "Command was executed" |
| 90 | fi |
| 91 | |
| 92 | # Test 4: Readonly variable preserves original value (test removed - see P0-1.3) |
| 93 | # Note: POSIX requires non-interactive shells to exit on readonly violations, |
| 94 | # so we cannot test value preservation after failed assignment in same script. |
| 95 | # Value preservation is implicitly tested by P0-1.1 (error occurs) and P0-1.3 (execution stops). |
| 96 | |
| 97 | # Test 5: Multiple readonly violations |
| 98 | $SHELL_BIN -c 'readonly A=1; readonly B=2; A=x; B=y' >/dev/null 2>&1 |
| 99 | if [ $? -eq 1 ]; then |
| 100 | pass "P0-1.5: Multiple readonly violations handled correctly" |
| 101 | else |
| 102 | fail "P0-1.5: Multiple readonly violations handled correctly" "Got exit code $?" |
| 103 | fi |
| 104 | } |
| 105 | |
| 106 | test_p0_set_u() { |
| 107 | section "P0-2: set -u (nounset option)" |
| 108 | |
| 109 | # Test 1: Undefined variable in parameter expansion |
| 110 | output=$($SHELL_BIN -c 'set -u; echo ${UNDEFINED_VAR}' 2>&1) |
| 111 | if echo "$output" | grep -q "unbound variable"; then |
| 112 | pass "P0-2.1: set -u detects undefined variable in \${VAR}" |
| 113 | else |
| 114 | fail "P0-2.1: set -u detects undefined variable in \${VAR}" "No error for undefined variable" |
| 115 | fi |
| 116 | |
| 117 | # Test 2: Undefined variable in simple expansion |
| 118 | output=$($SHELL_BIN -c 'set -u; echo $UNDEFINED_SIMPLE' 2>&1) |
| 119 | if echo "$output" | grep -q "unbound variable"; then |
| 120 | pass "P0-2.2: set -u detects undefined variable in \$VAR" |
| 121 | else |
| 122 | fail "P0-2.2: set -u detects undefined variable in \$VAR" |
| 123 | fi |
| 124 | |
| 125 | # Test 3: Non-interactive shell exits on unbound variable |
| 126 | # Bash uses exit code 127 for expansion errors (matching bash behavior) |
| 127 | $SHELL_BIN -c 'set -u; echo $UNDEF; echo SHOULD_NOT_PRINT' >/dev/null 2>&1 |
| 128 | exit_code=$? |
| 129 | output=$($SHELL_BIN -c 'set -u; echo $UNDEF; echo SHOULD_NOT_PRINT' 2>&1) |
| 130 | if [ $exit_code -eq 127 ] && ! echo "$output" | grep -q "SHOULD_NOT_PRINT"; then |
| 131 | pass "P0-2.3: Non-interactive shell exits on unbound variable" |
| 132 | else |
| 133 | fail "P0-2.3: Non-interactive shell exits on unbound variable" "Exit code: $exit_code" |
| 134 | fi |
| 135 | |
| 136 | # Test 4: Defined variable works with set -u |
| 137 | output=$($SHELL_BIN -c 'set -u; VAR=value; echo $VAR' 2>&1) |
| 138 | if echo "$output" | grep -q "value"; then |
| 139 | pass "P0-2.4: Defined variables work correctly with set -u" |
| 140 | else |
| 141 | fail "P0-2.4: Defined variables work correctly with set -u" |
| 142 | fi |
| 143 | |
| 144 | # Test 5: set +u disables the option |
| 145 | output=$($SHELL_BIN -c 'set -u; set +u; echo $UNDEFINED_AFTER_DISABLE' 2>&1) |
| 146 | if ! echo "$output" | grep -q "unbound variable"; then |
| 147 | pass "P0-2.5: set +u correctly disables nounset option" |
| 148 | else |
| 149 | fail "P0-2.5: set +u correctly disables nounset option" |
| 150 | fi |
| 151 | |
| 152 | # Test 6: Special variables always defined ($?, $$, etc.) |
| 153 | output=$($SHELL_BIN -c 'set -u; echo $? $$ $0' 2>&1) |
| 154 | if ! echo "$output" | grep -q "unbound variable"; then |
| 155 | pass "P0-2.6: Special variables don't trigger set -u" |
| 156 | else |
| 157 | fail "P0-2.6: Special variables don't trigger set -u" |
| 158 | fi |
| 159 | } |
| 160 | |
| 161 | test_p0_trap_exit() { |
| 162 | section "P0-3: trap EXIT Execution" |
| 163 | |
| 164 | # Test 1: EXIT trap executes on exit builtin |
| 165 | output=$($SHELL_BIN -c 'trap "echo TRAPPED" EXIT; exit 0' 2>&1) |
| 166 | if echo "$output" | grep -q "TRAPPED"; then |
| 167 | pass "P0-3.1: EXIT trap executes on exit builtin" |
| 168 | else |
| 169 | fail "P0-3.1: EXIT trap executes on exit builtin" "Trap did not execute" |
| 170 | fi |
| 171 | |
| 172 | # Test 2: EXIT trap preserves exit code |
| 173 | $SHELL_BIN -c 'trap "echo cleanup" EXIT; exit 42' >/dev/null 2>&1 |
| 174 | if [ $? -eq 42 ]; then |
| 175 | pass "P0-3.2: EXIT trap preserves original exit code" |
| 176 | else |
| 177 | fail "P0-3.2: EXIT trap preserves original exit code" "Got exit code $?" |
| 178 | fi |
| 179 | |
| 180 | # Test 3: EXIT trap executes on natural shell exit |
| 181 | output=$(echo 'trap "echo CLEANUP" EXIT; echo done' | $SHELL_BIN 2>&1) |
| 182 | if echo "$output" | grep -q "CLEANUP"; then |
| 183 | pass "P0-3.3: EXIT trap executes on natural shell termination" |
| 184 | else |
| 185 | fail "P0-3.3: EXIT trap executes on natural shell termination" |
| 186 | fi |
| 187 | |
| 188 | # Test 4: EXIT trap executes only once |
| 189 | output=$($SHELL_BIN -c 'trap "echo ONCE" EXIT; exit 0' 2>&1) |
| 190 | count=$(echo "$output" | grep -c "ONCE") |
| 191 | if [ "$count" -eq 1 ]; then |
| 192 | pass "P0-3.4: EXIT trap executes exactly once" |
| 193 | else |
| 194 | fail "P0-3.4: EXIT trap executes exactly once" "Executed $count times" |
| 195 | fi |
| 196 | |
| 197 | # Test 5: EXIT trap can access variables |
| 198 | output=$($SHELL_BIN -c 'VAR=value; trap "echo \$VAR" EXIT; exit 0' 2>&1) |
| 199 | if echo "$output" | grep -q "value"; then |
| 200 | pass "P0-3.5: EXIT trap can access shell variables" |
| 201 | else |
| 202 | fail "P0-3.5: EXIT trap can access shell variables" |
| 203 | fi |
| 204 | |
| 205 | # Test 6: EXIT trap with non-zero exit from command |
| 206 | $SHELL_BIN -c 'trap "echo cleanup" EXIT; false' >/dev/null 2>&1 |
| 207 | if [ $? -eq 1 ]; then |
| 208 | pass "P0-3.6: EXIT trap preserves command failure exit code" |
| 209 | else |
| 210 | fail "P0-3.6: EXIT trap preserves command failure exit code" |
| 211 | fi |
| 212 | } |
| 213 | |
| 214 | # ============================================================================== |
| 215 | # P1: IMPORTANT TESTS (Correctness and error handling) |
| 216 | # ============================================================================== |
| 217 | |
| 218 | test_p1_exit_code_126() { |
| 219 | section "P1-1: Exit Code 126 (Non-executable File)" |
| 220 | |
| 221 | # Test 1: Non-executable file returns 126 |
| 222 | touch /tmp/bensch_test_nonexec |
| 223 | chmod 644 /tmp/bensch_test_nonexec |
| 224 | $SHELL_BIN -c '/tmp/bensch_test_nonexec' >/dev/null 2>&1 |
| 225 | exit_code=$? |
| 226 | rm -f /tmp/bensch_test_nonexec |
| 227 | |
| 228 | if [ $exit_code -eq 126 ]; then |
| 229 | pass "P1-1.1: Non-executable file returns exit code 126" |
| 230 | else |
| 231 | fail "P1-1.1: Non-executable file returns exit code 126" "Got exit code $exit_code" |
| 232 | fi |
| 233 | |
| 234 | # Test 2: Non-existent file still returns 127 |
| 235 | $SHELL_BIN -c '/nonexistent/command/path' >/dev/null 2>&1 |
| 236 | if [ $? -eq 127 ]; then |
| 237 | pass "P1-1.2: Non-existent command returns exit code 127" |
| 238 | else |
| 239 | fail "P1-1.2: Non-existent command returns exit code 127" "Got exit code $?" |
| 240 | fi |
| 241 | |
| 242 | # Test 3: Executable file returns its own exit code |
| 243 | echo '#!/bin/sh' > /tmp/bensch_test_exec |
| 244 | echo 'exit 5' >> /tmp/bensch_test_exec |
| 245 | chmod 755 /tmp/bensch_test_exec |
| 246 | $SHELL_BIN -c '/tmp/bensch_test_exec' >/dev/null 2>&1 |
| 247 | exit_code=$? |
| 248 | rm -f /tmp/bensch_test_exec |
| 249 | |
| 250 | if [ $exit_code -eq 5 ]; then |
| 251 | pass "P1-1.3: Executable file returns its own exit code" |
| 252 | else |
| 253 | fail "P1-1.3: Executable file returns its own exit code" "Got exit code $exit_code" |
| 254 | fi |
| 255 | } |
| 256 | |
| 257 | test_p1_set_c_noclobber() { |
| 258 | section "P1-2: set -C (noclobber option)" |
| 259 | |
| 260 | # Test 1: set -C prevents overwriting existing files |
| 261 | echo "original" > /tmp/bensch_test_noclobber |
| 262 | $SHELL_BIN -c 'set -C; echo new > /tmp/bensch_test_noclobber' >/dev/null 2>&1 |
| 263 | exit_code=$? |
| 264 | content=$(cat /tmp/bensch_test_noclobber 2>/dev/null) |
| 265 | rm -f /tmp/bensch_test_noclobber |
| 266 | |
| 267 | if [ $exit_code -ne 0 ] && [ "$content" = "original" ]; then |
| 268 | pass "P1-2.1: set -C prevents overwriting existing files" |
| 269 | else |
| 270 | fail "P1-2.1: set -C prevents overwriting existing files" "File was overwritten" |
| 271 | fi |
| 272 | |
| 273 | # Test 2: set -C allows writing to new files |
| 274 | rm -f /tmp/bensch_test_noclobber_new |
| 275 | $SHELL_BIN -c 'set -C; echo content > /tmp/bensch_test_noclobber_new' >/dev/null 2>&1 |
| 276 | if [ -f /tmp/bensch_test_noclobber_new ]; then |
| 277 | pass "P1-2.2: set -C allows writing to non-existent files" |
| 278 | rm -f /tmp/bensch_test_noclobber_new |
| 279 | else |
| 280 | fail "P1-2.2: set -C allows writing to non-existent files" |
| 281 | fi |
| 282 | |
| 283 | # Test 3: >| forces overwrite even with noclobber |
| 284 | echo "original" > /tmp/bensch_test_force |
| 285 | $SHELL_BIN -c 'set -C; echo forced >| /tmp/bensch_test_force' >/dev/null 2>&1 |
| 286 | content=$(cat /tmp/bensch_test_force 2>/dev/null) |
| 287 | rm -f /tmp/bensch_test_force |
| 288 | |
| 289 | if [ "$content" = "forced" ]; then |
| 290 | pass "P1-2.3: >| forces overwrite with noclobber set" |
| 291 | else |
| 292 | fail "P1-2.3: >| forces overwrite with noclobber set" "Content: $content" |
| 293 | fi |
| 294 | |
| 295 | # Test 4: set +C disables noclobber |
| 296 | echo "original" > /tmp/bensch_test_disable |
| 297 | $SHELL_BIN -c 'set -C; set +C; echo new > /tmp/bensch_test_disable' >/dev/null 2>&1 |
| 298 | content=$(cat /tmp/bensch_test_disable 2>/dev/null) |
| 299 | rm -f /tmp/bensch_test_disable |
| 300 | |
| 301 | if [ "$content" = "new" ]; then |
| 302 | pass "P1-2.4: set +C disables noclobber option" |
| 303 | else |
| 304 | fail "P1-2.4: set +C disables noclobber option" |
| 305 | fi |
| 306 | } |
| 307 | |
| 308 | test_p1_trap_removal() { |
| 309 | section "P1-3: trap - SIGNAL (Trap Removal)" |
| 310 | |
| 311 | # Test 1: trap - INT removes INT trap |
| 312 | output=$($SHELL_BIN -c 'trap "echo caught" INT; trap - INT; kill -INT $$' 2>&1) |
| 313 | if ! echo "$output" | grep -q "caught"; then |
| 314 | pass "P1-3.1: trap - INT removes INT trap" |
| 315 | else |
| 316 | fail "P1-3.1: trap - INT removes INT trap" "Trap still executed" |
| 317 | fi |
| 318 | |
| 319 | # Test 2: trap - TERM removes TERM trap |
| 320 | $SHELL_BIN -c 'trap "echo term" TERM; trap - TERM; exit 0' >/dev/null 2>&1 |
| 321 | if [ $? -eq 0 ]; then |
| 322 | pass "P1-3.2: trap - TERM removes TERM trap" |
| 323 | else |
| 324 | fail "P1-3.2: trap - TERM removes TERM trap" |
| 325 | fi |
| 326 | |
| 327 | # Test 3: trap - EXIT removes EXIT trap |
| 328 | output=$($SHELL_BIN -c 'trap "echo exit" EXIT; trap - EXIT; exit 0' 2>&1) |
| 329 | if ! echo "$output" | grep -q "exit"; then |
| 330 | pass "P1-3.3: trap - EXIT removes EXIT trap" |
| 331 | else |
| 332 | fail "P1-3.3: trap - EXIT removes EXIT trap" |
| 333 | fi |
| 334 | |
| 335 | # Test 4: Listing traps after removal |
| 336 | output=$($SHELL_BIN -c 'trap "echo test" INT; trap - INT; trap' 2>&1) |
| 337 | if ! echo "$output" | grep -q "INT"; then |
| 338 | pass "P1-3.4: Removed traps don't appear in trap listing" |
| 339 | else |
| 340 | fail "P1-3.4: Removed traps don't appear in trap listing" |
| 341 | fi |
| 342 | } |
| 343 | |
| 344 | test_p1_set_e_conditionals() { |
| 345 | section "P1-4: set -e in Conditional Contexts" |
| 346 | |
| 347 | # Test 1: set -e doesn't exit on false in if condition |
| 348 | output=$($SHELL_BIN -c 'set -e; if false; then echo no; else echo YES; fi' 2>&1) |
| 349 | if echo "$output" | grep -q "YES"; then |
| 350 | pass "P1-4.1: set -e doesn't exit on false in if condition" |
| 351 | else |
| 352 | fail "P1-4.1: set -e doesn't exit on false in if condition" |
| 353 | fi |
| 354 | |
| 355 | # Test 2: set -e doesn't exit on false in while condition |
| 356 | output=$($SHELL_BIN -c 'set -e; count=0; while false; do count=$((count+1)); done; echo AFTER' 2>&1) |
| 357 | if echo "$output" | grep -q "AFTER"; then |
| 358 | pass "P1-4.2: set -e doesn't exit on false in while condition" |
| 359 | else |
| 360 | fail "P1-4.2: set -e doesn't exit on false in while condition" |
| 361 | fi |
| 362 | |
| 363 | # Test 3: set -e exits on command failure outside conditionals |
| 364 | $SHELL_BIN -c 'set -e; false; echo SHOULD_NOT_PRINT' >/dev/null 2>&1 |
| 365 | exit_code=$? |
| 366 | if [ $exit_code -ne 0 ]; then |
| 367 | pass "P1-4.3: set -e exits on command failure outside conditionals" |
| 368 | else |
| 369 | fail "P1-4.3: set -e exits on command failure outside conditionals" |
| 370 | fi |
| 371 | |
| 372 | # Test 4: set -e with && and || operators |
| 373 | output=$($SHELL_BIN -c 'set -e; false || echo AFTER_OR' 2>&1) |
| 374 | if echo "$output" | grep -q "AFTER_OR"; then |
| 375 | pass "P1-4.4: set -e doesn't exit on false before ||" |
| 376 | else |
| 377 | fail "P1-4.4: set -e doesn't exit on false before ||" |
| 378 | fi |
| 379 | |
| 380 | # Test 5: set -e in until loop |
| 381 | output=$($SHELL_BIN -c 'set -e; count=0; until [ $count -eq 1 ]; do count=1; done; echo DONE' 2>&1) |
| 382 | if echo "$output" | grep -q "DONE"; then |
| 383 | pass "P1-4.5: set -e doesn't exit on false in until condition" |
| 384 | else |
| 385 | fail "P1-4.5: set -e doesn't exit on false in until condition" |
| 386 | fi |
| 387 | } |
| 388 | |
| 389 | # ============================================================================== |
| 390 | # P2: NICE-TO-HAVE TESTS (Quality of life features) |
| 391 | # ============================================================================== |
| 392 | |
| 393 | test_p2_background_pid() { |
| 394 | section "P2-1: \$! (Background Process PID)" |
| 395 | |
| 396 | # Test 1: $! contains PID of last background job |
| 397 | output=$($SHELL_BIN -c 'sleep 0.1 & echo $!' 2>&1) |
| 398 | if echo "$output" | grep -qE '^[0-9]+$'; then |
| 399 | pass "P2-1.1: \$! expands to numeric PID" |
| 400 | else |
| 401 | fail "P2-1.1: \$! expands to numeric PID" "Got: $output" |
| 402 | fi |
| 403 | |
| 404 | # Test 2: $! updates after each background job |
| 405 | output=$($SHELL_BIN -c 'sleep 0.1 & pid1=$!; sleep 0.1 & pid2=$!; if [ "$pid1" != "$pid2" ]; then echo DIFFERENT; fi' 2>&1) |
| 406 | if echo "$output" | grep -q "DIFFERENT"; then |
| 407 | pass "P2-1.2: \$! updates for each background job" |
| 408 | else |
| 409 | fail "P2-1.2: \$! updates for each background job" "Got: [$output]" |
| 410 | fi |
| 411 | |
| 412 | # Test 3: $! is empty/zero before any background jobs |
| 413 | output=$($SHELL_BIN -c 'echo "|$!|"' 2>&1) |
| 414 | if echo "$output" | grep -qE '\|[0-9]*\|'; then |
| 415 | pass "P2-1.3: \$! has valid value before background jobs" |
| 416 | else |
| 417 | fail "P2-1.3: \$! has valid value before background jobs" |
| 418 | fi |
| 419 | } |
| 420 | |
| 421 | test_p2_dot_with_args() { |
| 422 | section "P2-2: dot (.) Source with Arguments" |
| 423 | |
| 424 | # Test 1: Source script with positional parameters |
| 425 | echo 'echo "arg1=$1 arg2=$2"' > /tmp/bensch_test_source |
| 426 | output=$($SHELL_BIN -c '. /tmp/bensch_test_source hello world' 2>&1) |
| 427 | rm -f /tmp/bensch_test_source |
| 428 | |
| 429 | if echo "$output" | grep -q "arg1=hello arg2=world"; then |
| 430 | pass "P2-2.1: dot command passes arguments to sourced script" |
| 431 | else |
| 432 | fail "P2-2.1: dot command passes arguments to sourced script" "Got: $output" |
| 433 | fi |
| 434 | |
| 435 | # Test 2: Source script preserves caller's variables |
| 436 | echo 'SOURCED_VAR=from_script' > /tmp/bensch_test_source2 |
| 437 | output=$($SHELL_BIN -c '. /tmp/bensch_test_source2; echo $SOURCED_VAR' 2>&1) |
| 438 | rm -f /tmp/bensch_test_source2 |
| 439 | |
| 440 | if echo "$output" | grep -q "from_script"; then |
| 441 | pass "P2-2.2: Sourced script sets variables in caller" |
| 442 | else |
| 443 | fail "P2-2.2: Sourced script sets variables in caller" |
| 444 | fi |
| 445 | |
| 446 | # Test 3: Source with absolute path |
| 447 | echo 'echo sourced' > /tmp/bensch_abs_source |
| 448 | output=$($SHELL_BIN -c '. /tmp/bensch_abs_source' 2>&1) |
| 449 | rm -f /tmp/bensch_abs_source |
| 450 | |
| 451 | if echo "$output" | grep -q "sourced"; then |
| 452 | pass "P2-2.3: dot command works with absolute paths" |
| 453 | else |
| 454 | fail "P2-2.3: dot command works with absolute paths" |
| 455 | fi |
| 456 | } |
| 457 | |
| 458 | test_p2_readonly_unset() { |
| 459 | section "P2-3: Readonly Unset Prevention" |
| 460 | |
| 461 | # Test 1: Cannot unset readonly variable |
| 462 | output=$($SHELL_BIN -c 'readonly VAR=test; unset VAR' 2>&1) |
| 463 | if echo "$output" | grep -qE '(readonly|cannot unset)'; then |
| 464 | pass "P2-3.1: unset readonly variable produces error" |
| 465 | else |
| 466 | fail "P2-3.1: unset readonly variable produces error" |
| 467 | fi |
| 468 | |
| 469 | # Test 2: Readonly variable still exists after unset attempt |
| 470 | output=$($SHELL_BIN -c 'readonly VAR=value; unset VAR 2>/dev/null; echo $VAR' 2>&1) |
| 471 | if echo "$output" | grep -q "value"; then |
| 472 | pass "P2-3.2: Readonly variable survives unset attempt" |
| 473 | else |
| 474 | fail "P2-3.2: Readonly variable survives unset attempt" |
| 475 | fi |
| 476 | |
| 477 | # Test 3: unset returns non-zero for readonly variables |
| 478 | $SHELL_BIN -c 'readonly VAR=x; unset VAR' >/dev/null 2>&1 |
| 479 | if [ $? -ne 0 ]; then |
| 480 | pass "P2-3.3: unset readonly variable returns non-zero" |
| 481 | else |
| 482 | fail "P2-3.3: unset readonly variable returns non-zero" |
| 483 | fi |
| 484 | |
| 485 | # Test 4: Can unset non-readonly variables |
| 486 | output=$($SHELL_BIN -c 'VAR=test; unset VAR; echo "|$VAR|"' 2>&1) |
| 487 | if echo "$output" | grep -q "||"; then |
| 488 | pass "P2-3.4: unset works for non-readonly variables" |
| 489 | else |
| 490 | fail "P2-3.4: unset works for non-readonly variables" |
| 491 | fi |
| 492 | } |
| 493 | |
| 494 | # ============================================================================== |
| 495 | # ADDITIONAL ROBUSTNESS TESTS |
| 496 | # ============================================================================== |
| 497 | |
| 498 | test_additional_builtins() { |
| 499 | section "BONUS: Additional Builtin Tests" |
| 500 | |
| 501 | # Test shift builtin |
| 502 | output=$($SHELL_BIN -c 'set -- a b c; shift; echo $1' 2>&1) |
| 503 | if echo "$output" | grep -q "b"; then |
| 504 | pass "BONUS-1: shift builtin works correctly" |
| 505 | else |
| 506 | fail "BONUS-1: shift builtin works correctly" |
| 507 | fi |
| 508 | |
| 509 | # Test times builtin |
| 510 | output=$($SHELL_BIN -c 'times' 2>&1) |
| 511 | if echo "$output" | grep -qE '[0-9]'; then |
| 512 | pass "BONUS-2: times builtin produces output" |
| 513 | else |
| 514 | fail "BONUS-2: times builtin produces output" |
| 515 | fi |
| 516 | |
| 517 | # Test hash builtin |
| 518 | output=$($SHELL_BIN -c 'hash' 2>&1) |
| 519 | if [ $? -eq 0 ]; then |
| 520 | pass "BONUS-3: hash builtin executes without error" |
| 521 | else |
| 522 | fail "BONUS-3: hash builtin executes without error" |
| 523 | fi |
| 524 | |
| 525 | # Test type builtin |
| 526 | output=$($SHELL_BIN -c 'type echo' 2>&1) |
| 527 | if echo "$output" | grep -qiE '(builtin|command)'; then |
| 528 | pass "BONUS-4: type builtin identifies commands" |
| 529 | else |
| 530 | fail "BONUS-4: type builtin identifies commands" |
| 531 | fi |
| 532 | |
| 533 | # Test umask builtin |
| 534 | output=$($SHELL_BIN -c 'umask' 2>&1) |
| 535 | if echo "$output" | grep -qE '^[0-9]{4}$'; then |
| 536 | pass "BONUS-5: umask builtin displays mask" |
| 537 | else |
| 538 | fail "BONUS-5: umask builtin displays mask" |
| 539 | fi |
| 540 | |
| 541 | # Test getopts builtin |
| 542 | output=$($SHELL_BIN -c 'getopts "a:b" opt -a value; echo $opt' 2>&1) |
| 543 | if echo "$output" | grep -q "a"; then |
| 544 | pass "BONUS-6: getopts builtin parses options" |
| 545 | else |
| 546 | fail "BONUS-6: getopts builtin parses options" |
| 547 | fi |
| 548 | } |
| 549 | |
| 550 | test_edge_cases() { |
| 551 | section "BONUS: Edge Cases" |
| 552 | |
| 553 | # Test empty command |
| 554 | $SHELL_BIN -c '' >/dev/null 2>&1 |
| 555 | if [ $? -eq 0 ]; then |
| 556 | pass "EDGE-1: Empty command succeeds" |
| 557 | else |
| 558 | fail "EDGE-1: Empty command succeeds" |
| 559 | fi |
| 560 | |
| 561 | # Test semicolon alone |
| 562 | # POSIX: Semicolon alone is a syntax error (exit code 2), not success |
| 563 | $SHELL_BIN -c ';' >/dev/null 2>&1 |
| 564 | if [ $? -eq 2 ]; then |
| 565 | pass "EDGE-2: Semicolon alone is syntax error" |
| 566 | else |
| 567 | fail "EDGE-2: Semicolon alone is syntax error" "Expected exit 2, got $?" |
| 568 | fi |
| 569 | |
| 570 | # Test comment handling |
| 571 | output=$($SHELL_BIN -c 'echo visible # this is comment' 2>&1) |
| 572 | if echo "$output" | grep -q "visible" && ! echo "$output" | grep -q "comment"; then |
| 573 | pass "EDGE-3: Comments are ignored correctly" |
| 574 | else |
| 575 | fail "EDGE-3: Comments are ignored correctly" |
| 576 | fi |
| 577 | |
| 578 | # Test multiple semicolons |
| 579 | output=$($SHELL_BIN -c 'echo a;; echo b' 2>&1) |
| 580 | if echo "$output" | grep -q "b"; then |
| 581 | pass "EDGE-4: Multiple semicolons handled" |
| 582 | else |
| 583 | fail "EDGE-4: Multiple semicolons handled" |
| 584 | fi |
| 585 | } |
| 586 | |
| 587 | # ============================================================================== |
| 588 | # MAIN TEST EXECUTION |
| 589 | # ============================================================================== |
| 590 | |
| 591 | main() { |
| 592 | echo "==========================================" |
| 593 | echo "POSIX Compliance - Builtin Test Suite" |
| 594 | echo "==========================================" |
| 595 | echo "Testing: $SHELL_BIN" |
| 596 | echo "Date: $(date)" |
| 597 | echo "" |
| 598 | |
| 599 | # Verify shell binary exists |
| 600 | if [ ! -x "$SHELL_BIN" ]; then |
| 601 | echo -e "${RED}ERROR: shell binary not found or not executable: $SHELL_BIN${NC}" |
| 602 | echo "Please build fortsh or set SHELL_BIN environment variable" |
| 603 | exit 1 |
| 604 | fi |
| 605 | |
| 606 | # Run all test suites |
| 607 | test_p0_readonly_enforcement |
| 608 | test_p0_set_u |
| 609 | test_p0_trap_exit |
| 610 | |
| 611 | test_p1_exit_code_126 |
| 612 | test_p1_set_c_noclobber |
| 613 | test_p1_trap_removal |
| 614 | test_p1_set_e_conditionals |
| 615 | |
| 616 | test_p2_background_pid |
| 617 | test_p2_dot_with_args |
| 618 | test_p2_readonly_unset |
| 619 | |
| 620 | test_additional_builtins |
| 621 | test_edge_cases |
| 622 | |
| 623 | # Print summary |
| 624 | echo "" |
| 625 | echo "==========================================" |
| 626 | echo "TEST SUMMARY ${TEST_PREFIX}" |
| 627 | echo "==========================================" |
| 628 | echo -e "Total Tests: $TOTAL_TESTS" |
| 629 | echo -e "${GREEN}Passed: $PASSED_TESTS${NC}" |
| 630 | echo -e "${RED}Failed: $FAILED_TESTS${NC}" |
| 631 | |
| 632 | if [ $FAILED_TESTS -gt 0 ]; then |
| 633 | echo "" |
| 634 | echo -e "${RED}Failed tests:${NC}" |
| 635 | echo -e "$FAILED_TESTS_LIST" |
| 636 | echo "==========================================" |
| 637 | fi |
| 638 | |
| 639 | if [ $FAILED_TESTS -eq 0 ]; then |
| 640 | echo "" |
| 641 | echo -e "${GREEN}==========================================" |
| 642 | echo -e "✓ ALL TESTS PASSED!" |
| 643 | echo -e "==========================================${NC}" |
| 644 | exit 0 |
| 645 | else |
| 646 | echo "" |
| 647 | echo -e "${RED}==========================================" |
| 648 | echo -e "✗ SOME TESTS FAILED" |
| 649 | echo -e "==========================================${NC}" |
| 650 | echo "" |
| 651 | echo "Run with VERBOSE=1 for detailed failure information:" |
| 652 | echo " VERBOSE=1 $0" |
| 653 | exit 1 |
| 654 | fi |
| 655 | } |
| 656 | |
| 657 | # Run main function |
| 658 | main "$@" |