| 1 | #!/bin/sh |
| 2 | # ===================================== |
| 3 | # POSIX Compliance - Previously Untested Features |
| 4 | # ===================================== |
| 5 | # Tests for POSIX features that are implemented but lack test coverage |
| 6 | |
| 7 | # Colors (POSIX-compliant way) |
| 8 | RED='\033[0;31m' |
| 9 | GREEN='\033[0;32m' |
| 10 | YELLOW='\033[1;33m' |
| 11 | BLUE='\033[0;34m' |
| 12 | NC='\033[0m' |
| 13 | |
| 14 | # Test identification |
| 15 | TEST_PREFIX="[posix-untested]" |
| 16 | CURRENT_SECTION="" |
| 17 | TEST_NUM=0 |
| 18 | |
| 19 | PASSED=0 |
| 20 | FAILED=0 |
| 21 | SKIPPED=0 |
| 22 | FAILED_TESTS_LIST="" |
| 23 | |
| 24 | # Get script directory (POSIX way) |
| 25 | SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) |
| 26 | SHELL_BIN="${SHELL_BIN:?ERROR: SHELL_BIN must be set}" |
| 27 | BASH_REF="${BASH_REF:-bash}" |
| 28 | |
| 29 | # Check if shell binary exists |
| 30 | if [ ! -x "$SHELL_BIN" ]; then |
| 31 | printf "${RED}ERROR${NC}: shell binary not found at $SHELL_BIN\n" |
| 32 | printf "Please set SHELL_BIN or set SHELL_BIN environment variable\n" |
| 33 | exit 1 |
| 34 | fi |
| 35 | |
| 36 | # Test result trackers |
| 37 | pass() { |
| 38 | TEST_NUM=$((TEST_NUM + 1)) |
| 39 | printf "${GREEN}✓ PASS${NC} ${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}: %s\n" "$1" |
| 40 | PASSED=$((PASSED + 1)) |
| 41 | } |
| 42 | |
| 43 | fail() { |
| 44 | TEST_NUM=$((TEST_NUM + 1)) |
| 45 | TEST_ID="${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}" |
| 46 | printf "${RED}✗ FAIL${NC} ${TEST_ID}: %s\n" "$1" |
| 47 | FAILED_TESTS_LIST="${FAILED_TESTS_LIST} ${TEST_ID}: $1\n" |
| 48 | if [ -n "$2" ]; then |
| 49 | printf " posix: %s\n" "$2" |
| 50 | fi |
| 51 | if [ -n "$3" ]; then |
| 52 | printf " shell: %s\n" "$3" |
| 53 | fi |
| 54 | FAILED=$((FAILED + 1)) |
| 55 | } |
| 56 | |
| 57 | skip() { |
| 58 | TEST_NUM=$((TEST_NUM + 1)) |
| 59 | printf "${YELLOW}⊘ SKIP${NC} ${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}: %s\n" "$1" |
| 60 | SKIPPED=$((SKIPPED + 1)) |
| 61 | } |
| 62 | |
| 63 | section() { |
| 64 | # Extract section number from header like "131. COMMAND BUILTIN" |
| 65 | CURRENT_SECTION=$(echo "$1" | grep -oE '^[0-9]+' || echo "0") |
| 66 | TEST_NUM=0 |
| 67 | printf "\n${BLUE}==========================================\n" |
| 68 | printf "%s\n" "$1" |
| 69 | printf "==========================================${NC}\n" |
| 70 | } |
| 71 | |
| 72 | # Compare output between sh and fortsh |
| 73 | compare_posix_output() { |
| 74 | test_name="$1" |
| 75 | test_cmd="$2" |
| 76 | |
| 77 | # Run with POSIX sh |
| 78 | posix_output=$(env $RC_DISABLE_ENV "$BASH_REF" -c "$test_cmd" 2>&1) |
| 79 | posix_exit=$? |
| 80 | |
| 81 | # Run with fortsh |
| 82 | shell_output=$(env $RC_DISABLE_ENV "$SHELL_BIN" -c "$test_cmd" 2>&1) |
| 83 | shell_exit=$? |
| 84 | |
| 85 | # Compare outputs |
| 86 | if [ "$posix_output" = "$shell_output" ] && [ "$posix_exit" = "$shell_exit" ]; then |
| 87 | pass "$test_name" |
| 88 | else |
| 89 | fail "$test_name" "$posix_output" "$shell_output" |
| 90 | fi |
| 91 | } |
| 92 | |
| 93 | # Normalize shell error messages by stripping shell name and "line N: " prefix |
| 94 | # POSIX doesn't mandate error message format, so we normalize for comparison |
| 95 | normalize_error() { |
| 96 | echo "$1" | sed -e 's|^[^ ]*/[a-z]*sh[0-9]*: |sh: |' -e 's|^[a-z]*sh[0-9]*: |sh: |' -e 's/line [0-9]*: //' |
| 97 | } |
| 98 | |
| 99 | # Compare error output, normalizing line number differences |
| 100 | compare_posix_error() { |
| 101 | test_name="$1" |
| 102 | test_cmd="$2" |
| 103 | |
| 104 | posix_output=$(env $RC_DISABLE_ENV "$BASH_REF" -c "$test_cmd" 2>&1) |
| 105 | posix_exit=$? |
| 106 | |
| 107 | shell_output=$(env $RC_DISABLE_ENV "$SHELL_BIN" -c "$test_cmd" 2>&1) |
| 108 | shell_exit=$? |
| 109 | |
| 110 | posix_norm=$(normalize_error "$posix_output") |
| 111 | shell_norm=$(normalize_error "$shell_output") |
| 112 | |
| 113 | if [ "$posix_norm" = "$shell_norm" ] && [ "$posix_exit" = "$shell_exit" ]; then |
| 114 | pass "$test_name" |
| 115 | else |
| 116 | fail "$test_name" "$posix_output" "$shell_output" |
| 117 | fi |
| 118 | } |
| 119 | |
| 120 | # Test that fortsh accepts an option/command without error |
| 121 | test_accepts() { |
| 122 | test_name="$1" |
| 123 | test_cmd="$2" |
| 124 | |
| 125 | if env $RC_DISABLE_ENV "$SHELL_BIN" -c "$test_cmd" >/dev/null 2>&1; then |
| 126 | pass "$test_name" |
| 127 | else |
| 128 | fail "$test_name" "should succeed" "failed or not implemented" |
| 129 | fi |
| 130 | } |
| 131 | |
| 132 | # Test that command fails as expected |
| 133 | test_fails() { |
| 134 | test_name="$1" |
| 135 | test_cmd="$2" |
| 136 | |
| 137 | if ! env $RC_DISABLE_ENV "$SHELL_BIN" -c "$test_cmd" >/dev/null 2>&1; then |
| 138 | pass "$test_name" |
| 139 | else |
| 140 | fail "$test_name" "should fail" "succeeded unexpectedly" |
| 141 | fi |
| 142 | } |
| 143 | |
| 144 | # ===================================== |
| 145 | # TESTS START HERE |
| 146 | # ===================================== |
| 147 | |
| 148 | section "131. COMMAND BUILTIN - PATH SEARCH" |
| 149 | |
| 150 | compare_posix_output "command -v ls" 'command -v ls >/dev/null && echo found' |
| 151 | compare_posix_output "command -v nonexistent" 'command -v nonexistent >/dev/null || echo notfound' |
| 152 | test_accepts "command -p ls" 'command -p ls / >/dev/null' |
| 153 | compare_posix_output "command without options" 'command echo test' |
| 154 | |
| 155 | section "132. MULTI-FD REDIRECTIONS" |
| 156 | |
| 157 | # Create test file |
| 158 | compare_posix_output "fd 3 redirect write" 'exec 3>/tmp/posix_fd3.txt; echo test >&3; exec 3>&-; cat /tmp/posix_fd3.txt; rm /tmp/posix_fd3.txt' |
| 159 | compare_posix_output "fd 4 redirect read" 'echo data > /tmp/posix_fd4.txt; exec 4</tmp/posix_fd4.txt; read line <&4; echo $line; exec 4<&-; rm /tmp/posix_fd4.txt' |
| 160 | compare_posix_output "fd duplication" 'exec 5>&1; echo stdout >&5; exec 5>&-' |
| 161 | compare_posix_output "fd 3 and 4 together" 'exec 3>/tmp/posix_3.txt 4>/tmp/posix_4.txt; echo a >&3; echo b >&4; exec 3>&- 4>&-; cat /tmp/posix_3.txt /tmp/posix_4.txt; rm /tmp/posix_3.txt /tmp/posix_4.txt' |
| 162 | |
| 163 | section "133. EXEC WITH SHELL REDIRECTIONS" |
| 164 | |
| 165 | # Note: These redirect the shell itself, not just one command |
| 166 | # Save stdout to FD 3, redirect, write, restore, then cat (otherwise cat hangs on file still open for writing) |
| 167 | compare_posix_output "exec redirect stdout" 'exec 3>&1; exec >/tmp/posix_exec_out.txt; echo redirected; exec >&3; cat /tmp/posix_exec_out.txt; rm /tmp/posix_exec_out.txt' |
| 168 | compare_posix_output "exec redirect stdin" 'echo testdata > /tmp/posix_exec_in.txt; exec </tmp/posix_exec_in.txt; read data; echo $data; rm /tmp/posix_exec_in.txt' |
| 169 | |
| 170 | section "134. SET OPTION INTERACTIONS" |
| 171 | |
| 172 | compare_posix_output "set -e with true" 'set -e; true; echo ok' |
| 173 | compare_posix_output "set -u with unset var" 'set -u; echo ${UNSET_VAR:-default}' |
| 174 | compare_posix_output "set -C noclobber" 'set -C; echo test > /tmp/posix_clobber.txt; echo ok; rm /tmp/posix_clobber.txt' |
| 175 | compare_posix_output "set -C override with >|" 'set -C; echo test > /tmp/posix_clobber2.txt; echo override >| /tmp/posix_clobber2.txt; cat /tmp/posix_clobber2.txt; rm /tmp/posix_clobber2.txt' |
| 176 | |
| 177 | section "135. SPECIAL BUILTIN ERROR HANDLING" |
| 178 | |
| 179 | # POSIX: Special builtins must exit non-interactive shell on error |
| 180 | # Testing with subshells to avoid killing the test script |
| 181 | |
| 182 | compare_posix_error "readonly error exits" '(readonly VAR=1; VAR=2 2>/dev/null; echo should not print) || echo exited' |
| 183 | compare_posix_output "export invalid" '(export 123INVALID=value 2>/dev/null; echo should not print) || echo exited' |
| 184 | compare_posix_output "set invalid option" '(set -@ 2>/dev/null; echo should not print) || echo exited' |
| 185 | |
| 186 | section "136. ULIMIT TESTS" |
| 187 | |
| 188 | test_accepts "ulimit display" 'ulimit' |
| 189 | test_accepts "ulimit -a all" 'ulimit -a >/dev/null' |
| 190 | test_accepts "ulimit -n files" 'ulimit -n >/dev/null' |
| 191 | test_accepts "ulimit -s stack" 'ulimit -s >/dev/null 2>&1' |
| 192 | |
| 193 | section "137. NESTED PARAMETER EXPANSION" |
| 194 | |
| 195 | compare_posix_output "nested default" 'unset A B; echo ${A:-${B:-default}}' |
| 196 | compare_posix_output "nested with set var" 'A=inner; unset B; echo ${B:-${A}}' |
| 197 | compare_posix_output "double nested" 'unset A B C; echo ${A:-${B:-${C:-triple}}}' |
| 198 | |
| 199 | section "138. PARAMETER LENGTH EDGE CASES" |
| 200 | |
| 201 | compare_posix_output "length of $@" 'set -- a b c; echo ${#@}' |
| 202 | compare_posix_output "length of $*" 'set -- a b c; echo ${#*}' |
| 203 | compare_posix_output "length of empty" 'VAR=; echo ${#VAR}' |
| 204 | compare_posix_output "length of unset" 'unset VAR; echo ${#VAR}' |
| 205 | |
| 206 | section "139. QUOTING EDGE CASES" |
| 207 | |
| 208 | compare_posix_output "empty double quotes" 'VAR=""; echo x${VAR}y' |
| 209 | compare_posix_output "empty single quotes" "VAR=''; echo x\${VAR}y" |
| 210 | compare_posix_output "adjacent quotes concat" 'echo "a"b"c"' |
| 211 | compare_posix_output "quote in quote" 'echo "it'\''s"' |
| 212 | |
| 213 | section "140. BACKSLASH NEWLINE CONTINUATION" |
| 214 | |
| 215 | compare_posix_output "line continuation in string" 'echo "test\ |
| 216 | continuation"' |
| 217 | compare_posix_output "line continuation in command" 'ec\ |
| 218 | ho test' |
| 219 | |
| 220 | section "141. COMMENT IN COMMAND SUBSTITUTION" |
| 221 | |
| 222 | compare_posix_output "comment in subshell" '$(# this is a comment |
| 223 | echo test)' |
| 224 | compare_posix_output "comment in backtick" '`# comment |
| 225 | echo test`' |
| 226 | |
| 227 | section "142. REDIRECTION EDGE CASES" |
| 228 | |
| 229 | compare_posix_output "close stdin" 'cat <&- 2>&1 | head -1' |
| 230 | compare_posix_error "close stdout" 'echo test >&- 2>&1' |
| 231 | compare_posix_output "read/write mode" 'echo data > /tmp/posix_rw.txt; cat <> /tmp/posix_rw.txt; rm /tmp/posix_rw.txt' |
| 232 | |
| 233 | section "143. ERROR HANDLING EDGE CASES" |
| 234 | |
| 235 | test_fails "assign to positional param" '$1=value 2>/dev/null' |
| 236 | test_fails "readonly reassign" 'readonly VAR=1; VAR=2 2>/dev/null' |
| 237 | # Test that execution continues after arithmetic error |
| 238 | # Check that "continued" appears in the output (error format may vary) |
| 239 | test_accepts "division by zero" 'echo $((5/0)) | cat; echo continued 2>&1 | grep -q continued' |
| 240 | |
| 241 | section "144. SET -n (NOEXEC) TESTING" |
| 242 | |
| 243 | # This tests if set -n is implemented |
| 244 | compare_posix_output "set -n parse only" 'set -n; echo "should not execute"; false' || skip "set -n not implemented" |
| 245 | |
| 246 | section "145. SET -m (MONITOR) TESTING" |
| 247 | |
| 248 | test_accepts "set -m monitor mode" 'set -m; set +m' |
| 249 | compare_posix_output "set -m doesn't affect output" 'set -m; echo test; set +m' |
| 250 | |
| 251 | # ===================================== |
| 252 | # SUMMARY |
| 253 | # ===================================== |
| 254 | |
| 255 | printf "\n==========================================\n" |
| 256 | printf "UNTESTED FEATURES TEST RESULTS ${TEST_PREFIX}\n" |
| 257 | printf "==========================================\n" |
| 258 | printf "${GREEN}Passed:${NC} %d\n" "$PASSED" |
| 259 | printf "${RED}Failed:${NC} %d\n" "$FAILED" |
| 260 | printf "${YELLOW}Skipped:${NC} %d\n" "$SKIPPED" |
| 261 | printf "Total: %d\n" "$((PASSED + FAILED + SKIPPED))" |
| 262 | printf "==========================================\n" |
| 263 | |
| 264 | # Calculate pass rate |
| 265 | if [ "$((PASSED + FAILED))" -gt 0 ]; then |
| 266 | pass_rate=$((PASSED * 100 / (PASSED + FAILED))) |
| 267 | printf "Pass rate: %d%%\n" "$pass_rate" |
| 268 | fi |
| 269 | |
| 270 | if [ "$FAILED" -gt 0 ]; then |
| 271 | printf "\n${RED}Failed tests:${NC}\n" |
| 272 | printf "%b" "$FAILED_TESTS_LIST" |
| 273 | printf "==========================================\n" |
| 274 | fi |
| 275 | |
| 276 | if [ "$FAILED" -eq 0 ]; then |
| 277 | printf "${GREEN}ALL UNTESTED FEATURES TESTS PASSED!${NC} ✓\n" |
| 278 | exit 0 |
| 279 | else |
| 280 | printf "${RED}SOME TESTS FAILED${NC} ✗\n" |
| 281 | exit 1 |
| 282 | fi |