| 1 | #!/bin/sh |
| 2 | # ===================================== |
| 3 | # POSIX Control Flow Gap Tests |
| 4 | # ===================================== |
| 5 | # Tests for POSIX control flow constructs |
| 6 | # Split from posix_compliance_gaps.sh for better organization |
| 7 | |
| 8 | # Colors (POSIX-compliant way) |
| 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="[gaps-control]" |
| 17 | CURRENT_SECTION="" |
| 18 | TEST_NUM=0 |
| 19 | |
| 20 | PASSED=0 |
| 21 | FAILED=0 |
| 22 | SKIPPED=0 |
| 23 | FAILED_TESTS_LIST="" |
| 24 | |
| 25 | # Get script directory (POSIX way) |
| 26 | SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) |
| 27 | FORTSH_BIN="${FORTSH_BIN:-$SCRIPT_DIR/../bin/fortsh}" |
| 28 | BASH_REF="${BASH_REF:-bash}" |
| 29 | |
| 30 | # Check if fortsh exists |
| 31 | if [ ! -x "$FORTSH_BIN" ]; then |
| 32 | printf "${RED}ERROR${NC}: fortsh binary not found at $FORTSH_BIN\n" |
| 33 | printf "Please run 'make' first or set FORTSH_BIN environment variable\n" |
| 34 | exit 1 |
| 35 | fi |
| 36 | |
| 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 printf " posix: %s\n" "$2"; fi |
| 49 | if [ -n "$3" ]; then printf " fortsh: %s\n" "$3"; fi |
| 50 | FAILED=$((FAILED + 1)) |
| 51 | } |
| 52 | |
| 53 | section() { |
| 54 | CURRENT_SECTION=$(echo "$1" | grep -oE '^[0-9]+' || echo "0") |
| 55 | TEST_NUM=0 |
| 56 | printf "\n${BLUE}==========================================\n%s\n==========================================${NC}\n" "$1" |
| 57 | } |
| 58 | |
| 59 | normalize_output() { sed -e 's/^bash: /sh: /' -e 's/line [0-9]*: //'; } |
| 60 | |
| 61 | compare_posix_output() { |
| 62 | test_name="$1"; command="$2" |
| 63 | posix_out=$("$BASH_REF" -c "$command" 2>&1 | normalize_output) |
| 64 | fortsh_out=$("$FORTSH_BIN" -c "$command" 2>&1 | normalize_output) |
| 65 | if [ "$posix_out" = "$fortsh_out" ]; then pass "$test_name" |
| 66 | else fail "$test_name" "$posix_out" "$fortsh_out"; fi |
| 67 | } |
| 68 | |
| 69 | # ============================================================================ |
| 70 | # IF STATEMENTS |
| 71 | # ============================================================================ |
| 72 | |
| 73 | section "1. IF BASIC" |
| 74 | compare_posix_output "if true" 'if true; then echo yes; fi' |
| 75 | compare_posix_output "if false" 'if false; then echo yes; fi; echo done' |
| 76 | compare_posix_output "if else" 'if false; then echo no; else echo yes; fi' |
| 77 | compare_posix_output "if elif" 'if false; then echo 1; elif true; then echo 2; fi' |
| 78 | compare_posix_output "if elif else" 'if false; then echo 1; elif false; then echo 2; else echo 3; fi' |
| 79 | compare_posix_output "if nested" 'if true; then if true; then echo deep; fi; fi' |
| 80 | |
| 81 | section "2. IF CONDITIONS" |
| 82 | compare_posix_output "if and" 'if true && true; then echo yes; fi' |
| 83 | compare_posix_output "if or" 'if false || true; then echo yes; fi' |
| 84 | compare_posix_output "if not" 'if ! false; then echo yes; fi' |
| 85 | |
| 86 | # ============================================================================ |
| 87 | # FOR LOOPS |
| 88 | # ============================================================================ |
| 89 | |
| 90 | section "3. FOR BASIC" |
| 91 | compare_posix_output "for simple" 'for i in a b c; do echo $i; done' |
| 92 | compare_posix_output "for numbers" 'for i in 1 2 3; do echo $i; done | wc -l' |
| 93 | compare_posix_output "for empty" 'for i in; do echo $i; done; echo done' |
| 94 | compare_posix_output "for single" "for i in one; do echo \$i; done" |
| 95 | |
| 96 | section "4. FOR EDGE CASES" |
| 97 | compare_posix_output "for with break" 'for i in 1 2 3; do echo $i; break; done' |
| 98 | compare_posix_output "for with continue" 'for i in 1 2 3; do if [ $i = 2 ]; then continue; fi; echo $i; done' |
| 99 | # Inline test with debug output for CI diagnosis |
| 100 | _glob_cmd="touch /tmp/posix_gaps_for1_$$.txt /tmp/posix_gaps_for2_$$.txt /tmp/posix_gaps_for3_$$.txt 2>/dev/null; for f in /tmp/posix_gaps_for*_$$.txt; do test -f \$f && echo yes; done | head -1; rm -f /tmp/posix_gaps_for*_$$.txt" |
| 101 | _glob_posix=$("$BASH_REF" -c "$_glob_cmd" 2>&1) |
| 102 | _glob_fortsh=$("$FORTSH_BIN" -c "$_glob_cmd" 2>&1) |
| 103 | _glob_debug="[DEBUG glob] posix_hex=[$(printf '%s' "$_glob_posix" | od -A n -t x1 | tr -d '\n')] fortsh_hex=[$(printf '%s' "$_glob_fortsh" | od -A n -t x1 | tr -d '\n')] posix_raw=[$_glob_posix] fortsh_raw=[$_glob_fortsh]" |
| 104 | _glob_posix_n=$(printf '%s' "$_glob_posix" | normalize_output) |
| 105 | _glob_fortsh_n=$(printf '%s' "$_glob_fortsh" | normalize_output) |
| 106 | if [ "$_glob_posix_n" = "$_glob_fortsh_n" ]; then pass "for glob expansion" |
| 107 | else fail "for glob expansion" "$_glob_posix_n" "$_glob_fortsh_n"; fi |
| 108 | compare_posix_output "for preserves IFS" "IFS=:; for i in a b c; do echo \$i; done; echo \$IFS | od -A n -t x1 | grep -c 3a" |
| 109 | |
| 110 | # ============================================================================ |
| 111 | # WHILE LOOPS |
| 112 | # ============================================================================ |
| 113 | |
| 114 | section "5. WHILE BASIC" |
| 115 | compare_posix_output "while basic" 'i=0; while [ $i -lt 3 ]; do echo $i; i=$((i+1)); done' |
| 116 | compare_posix_output "while false" 'while false; do echo no; done; echo done' |
| 117 | compare_posix_output "while break" 'while true; do echo once; break; done' |
| 118 | compare_posix_output "while count" 'n=3; while [ $n -gt 0 ]; do n=$((n-1)); done; echo $n' |
| 119 | |
| 120 | section "6. WHILE EDGE CASES" |
| 121 | compare_posix_output "while true with break" "i=0; while true; do i=\$((i+1)); test \$i -eq 3 && break; done; echo \$i" |
| 122 | compare_posix_output "while with exit status" "i=5; while [ \$i -gt 0 ]; do i=\$((i-1)); done; echo \$?" |
| 123 | |
| 124 | # ============================================================================ |
| 125 | # UNTIL LOOPS |
| 126 | # ============================================================================ |
| 127 | |
| 128 | section "7. UNTIL BASIC" |
| 129 | compare_posix_output "until basic" 'i=0; until [ $i -ge 3 ]; do echo $i; i=$((i+1)); done' |
| 130 | compare_posix_output "until true" 'until true; do echo no; done; echo done' |
| 131 | compare_posix_output "until count" 'n=0; until [ $n -eq 3 ]; do n=$((n+1)); done; echo $n' |
| 132 | |
| 133 | section "8. UNTIL EDGE CASES" |
| 134 | compare_posix_output "until false with break" "i=0; until false; do i=\$((i+1)); test \$i -eq 3 && break; done; echo \$i" |
| 135 | compare_posix_output "until complex condition" "i=0; until [ \$i -gt 3 ] && [ \$i -lt 10 ]; do i=\$((i+1)); done; echo \$i" |
| 136 | |
| 137 | # ============================================================================ |
| 138 | # CASE STATEMENTS |
| 139 | # ============================================================================ |
| 140 | |
| 141 | section "9. CASE BASIC" |
| 142 | compare_posix_output "case single" 'case x in x) echo yes;; esac' |
| 143 | compare_posix_output "case default" 'case y in x) echo no;; *) echo yes;; esac' |
| 144 | compare_posix_output "case multi" 'case b in a|b|c) echo abc;; esac' |
| 145 | compare_posix_output "case glob" 'case abc in a*) echo yes;; esac' |
| 146 | compare_posix_output "case nested" 'case x in x) case y in y) echo deep;; esac;; esac' |
| 147 | |
| 148 | section "10. CASE PATTERNS" |
| 149 | compare_posix_output "case glob star" 'case abc in a*) echo star;; esac' |
| 150 | compare_posix_output "case glob question" 'case abc in a?c) echo match;; esac' |
| 151 | compare_posix_output "case glob bracket" 'case abc in [a-z]*) echo match;; esac' |
| 152 | compare_posix_output "case no match" 'case x in y) echo no;; esac; echo done' |
| 153 | compare_posix_output "case empty" 'case "" in "") echo empty;; esac' |
| 154 | compare_posix_output "case with var" 'x=test; case $x in test) echo yes;; esac' |
| 155 | compare_posix_output "case quoted" 'case "a b" in "a b") echo space;; esac' |
| 156 | |
| 157 | section "11. CASE EDGE CASES" |
| 158 | compare_posix_output "case empty pattern" "x=''; case \$x in '') echo empty;; esac" |
| 159 | compare_posix_output "case with quotes" "x='a b'; case \$x in 'a b') echo match;; esac" |
| 160 | compare_posix_output "case glob vs literal" "x='*'; case \$x in '*') echo literal;; esac" |
| 161 | compare_posix_output "case bracket range" "x=b; case \$x in [a-c]) echo range;; esac" |
| 162 | compare_posix_output "case multiple patterns order" "x=a; case \$x in a|b) echo first;; a) echo second;; esac" |
| 163 | compare_posix_output "case with variable pattern" "P='a*'; x=abc; case \$x in \$P) echo var_pattern;; esac" |
| 164 | |
| 165 | # ============================================================================ |
| 166 | # BREAK AND CONTINUE |
| 167 | # ============================================================================ |
| 168 | |
| 169 | section "12. BREAK" |
| 170 | compare_posix_output "break simple" 'for i in 1 2 3; do echo $i; break; done' |
| 171 | compare_posix_output "break nested" 'for i in 1 2; do for j in a b; do echo $i$j; break; done; done' |
| 172 | compare_posix_output "break 2" 'for i in 1 2; do for j in a b; do echo $i$j; break 2; done; done' |
| 173 | |
| 174 | section "13. CONTINUE" |
| 175 | compare_posix_output "continue simple" 'for i in 1 2 3; do if [ $i = 2 ]; then continue; fi; echo $i; done' |
| 176 | compare_posix_output "continue 2" 'for i in 1 2; do for j in a b; do if [ $j = a ]; then continue 2; fi; echo $i$j; done; done' |
| 177 | |
| 178 | section "14. BREAK/CONTINUE EDGE CASES" |
| 179 | compare_posix_output "break with level 0" "for i in 1 2; do break 0 2>/dev/null || break; echo \$i; done || echo ok" |
| 180 | compare_posix_output "break level too high" "for i in 1 2; do break 10 2>/dev/null || break; echo \$i; done || echo done" |
| 181 | compare_posix_output "continue with level" "for i in 1 2; do for j in a b; do continue 2 2>/dev/null || continue; echo \$i\$j; done; done || echo ok" |
| 182 | compare_posix_output "break outside loop" "break 2>/dev/null || echo ok" |
| 183 | compare_posix_output "continue outside loop" "continue 2>/dev/null || echo ok" |
| 184 | |
| 185 | # ============================================================================ |
| 186 | # FUNCTIONS |
| 187 | # ============================================================================ |
| 188 | |
| 189 | section "15. FUNCTION DEFINITION" |
| 190 | compare_posix_output "func basic" 'f() { echo hello; }; f' |
| 191 | compare_posix_output "func args" 'f() { echo $1 $2; }; f a b' |
| 192 | compare_posix_output "func return" 'f() { return 5; }; f; echo $?' |
| 193 | compare_posix_output "func positional" 'f() { echo $# $@; }; f a b c' |
| 194 | |
| 195 | section "16. RETURN AND EXIT" |
| 196 | compare_posix_output "return basic" 'f() { return; }; f; echo $?' |
| 197 | compare_posix_output "return code" 'f() { return 5; }; f; echo $?' |
| 198 | compare_posix_output "exit subshell" '(exit 5); echo $?' |
| 199 | compare_posix_output "exit code" '(exit 42); echo $?' |
| 200 | |
| 201 | # ============================================================================ |
| 202 | # SUBSHELL AND BRACE GROUPS |
| 203 | # ============================================================================ |
| 204 | |
| 205 | section "17. SUBSHELL" |
| 206 | compare_posix_output "subshell basic" '(echo hello)' |
| 207 | compare_posix_output "subshell var" 'x=1; (x=2); echo $x' |
| 208 | compare_posix_output "subshell exit" '(exit 5); echo $?' |
| 209 | |
| 210 | section "18. SUBSHELL VARIABLE ISOLATION" |
| 211 | compare_posix_output "subshell doesnt modify parent" "(X=inner); echo \${X:-unset}" |
| 212 | compare_posix_output "subshell inherits variables" "X=outer; (echo \$X)" |
| 213 | compare_posix_output "nested subshells" "X=1; (X=2; (X=3; echo \$X); echo \$X); echo \$X" |
| 214 | compare_posix_output "subshell with exports" "export X=exp; (X=inner; echo \$X); echo \$X" |
| 215 | |
| 216 | section "19. BRACE GROUPS" |
| 217 | compare_posix_output "brace basic" '{ echo hello; }' |
| 218 | compare_posix_output "brace var" 'x=1; { x=2; }; echo $x' |
| 219 | compare_posix_output "brace redir" '{ echo a; echo b; } > /tmp/b$$; wc -l < /tmp/b$$; rm /tmp/b$$' |
| 220 | compare_posix_output "brace group modifies parent" "X=1; { X=2; }; echo \$X" |
| 221 | compare_posix_output "brace group with redirects" "{ echo a; echo b; } | wc -l" |
| 222 | compare_posix_output "nested brace groups" "X=1; { { X=2; }; echo \$X; }; echo \$X" |
| 223 | |
| 224 | # ============================================================================ |
| 225 | # LOGICAL OPERATORS |
| 226 | # ============================================================================ |
| 227 | |
| 228 | section "20. LOGICAL OPERATORS" |
| 229 | compare_posix_output "and both true" 'true && echo yes' |
| 230 | compare_posix_output "and first false" 'false && echo no; echo done' |
| 231 | compare_posix_output "or first true" 'true || echo no; echo done' |
| 232 | compare_posix_output "or first false" 'false || echo yes' |
| 233 | compare_posix_output "not true" '! true; echo $?' |
| 234 | compare_posix_output "not false" '! false; echo $?' |
| 235 | |
| 236 | # ============================================================================ |
| 237 | # NESTED LOOPS |
| 238 | # ============================================================================ |
| 239 | |
| 240 | section "21. NESTED LOOPS" |
| 241 | compare_posix_output "nested for" 'for i in 1 2; do for j in a b; do echo $i$j; done; done | wc -l' |
| 242 | compare_posix_output "loop with pipe" 'for i in 1 2 3; do echo $i; done | head -2' |
| 243 | |
| 244 | # Summary |
| 245 | printf "\n==========================================\n" |
| 246 | if [ -n "$_glob_debug" ]; then printf "%s\n" "$_glob_debug"; fi |
| 247 | printf "CONTROL FLOW GAP TEST RESULTS\n" |
| 248 | printf "==========================================\n" |
| 249 | printf "${GREEN}Passed:${NC} %d\n" "$PASSED" |
| 250 | printf "${RED}Failed:${NC} %d\n" "$FAILED" |
| 251 | printf "Total: %d\n" "$((PASSED + FAILED))" |
| 252 | if [ "$FAILED" -gt 0 ]; then |
| 253 | printf "\n${RED}Failed tests:${NC}\n%b" "$FAILED_TESTS_LIST" |
| 254 | exit 1 |
| 255 | fi |
| 256 | exit 0 |