Bash · 32357 bytes Raw Blame History
1 #!/bin/sh
2 # ============================================================
3 # Stress tests for allocatable string / widened buffer changes
4 # ============================================================
5 # These tests verify that fortsh handles large values, deep
6 # nesting, many variables, and boundary conditions correctly
7 # after the MAX_VAR_VALUE_LEN 1024->4096 refactoring.
8 #
9 # Tests marked with skip() are known pre-existing issues not
10 # related to the buffer refactoring.
11
12 TEST_PREFIX="STRESS"
13 SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
14 . "$SCRIPT_DIR/test_harness.sh"
15
16 # Platform-aware timeouts: ARM64 (flang-new) is ~300x slower on loops
17 case "$(uname -m)" in
18 arm64|aarch64) TEST_TIMEOUT=90 ;;
19 *) TEST_TIMEOUT=30 ;;
20 esac
21
22 # ==========================================
23 # 1. Long variable values
24 # ==========================================
25 section "1 - Long variable values"
26
27 compare_output "2000-char variable via printf %0*d" \
28 'x=$(printf "%0*d" 2000 0); echo ${#x}'
29
30 compare_output "3000-char variable via printf %0*d" \
31 'x=$(printf "%0*d" 3000 0); echo ${#x}'
32
33 compare_output "4000-char variable (near limit)" \
34 'x=$(printf "%0*d" 4000 0); echo ${#x}'
35
36 compare_output "variable with 1000 'a' chars" \
37 'x=$(printf "a%.0s" $(seq 1 1000)); echo ${#x}'
38
39 # printf "a%.0s" with many args hits command expansion buffer
40 # use python or head /dev/zero instead for large single-char strings
41 compare_output "2000-char variable via head" \
42 'x=$(head -c 2000 /dev/zero | tr "\0" "a"); echo ${#x}'
43
44 compare_output "3000-char variable via head" \
45 'x=$(head -c 3000 /dev/zero | tr "\0" "a"); echo ${#x}'
46
47 # BUG: printf "a%.0s" with many args truncates at ~1041 chars (#37)
48 compare_output "2000-char via printf repeat" \
49 'x=$(printf "a%.0s" $(seq 1 2000)); echo ${#x}'
50
51 compare_output "long variable substring" \
52 'x=$(printf "%0*d" 3000 0); echo ${#x}; y=${x:500:1000}; echo ${#y}'
53
54 compare_output "long variable pattern removal" \
55 'x="aaa$(printf "%0*d" 2000 0)aaa"; y=${x##aaa}; echo ${#y}'
56
57 # ==========================================
58 # 2. Long command lines
59 # ==========================================
60 section "2 - Long command lines"
61
62 compare_output "echo with 500 args" \
63 'echo $(seq 1 500) | wc -w'
64
65 compare_output "echo with 1000 args" \
66 'echo $(seq 1 1000) | wc -w'
67
68 compare_output "long pipe chain" \
69 'echo hello | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat'
70
71 compare_output "long command with semicolons" \
72 'a=1; b=2; c=3; d=4; e=5; f=6; g=7; h=8; i=9; j=10; echo $a $b $c $d $e $f $g $h $i $j'
73
74 # ==========================================
75 # 3. Array stress tests
76 # ==========================================
77 section "3 - Array stress tests"
78
79 # Use explicit indices to avoid arr[$i]= parsing differences
80 compare_output "array direct assignment 10 elements" \
81 'arr=(a b c d e f g h i j); echo ${#arr[@]}'
82
83 compare_output "array with explicit long values" \
84 'x=$(printf "%0*d" 100 0); arr[0]=$x; arr[1]=$x; echo ${#arr[0]} ${#arr[1]}'
85
86 # BUG: array element assignment truncates long values (#35)
87 compare_output "array element from command substitution" \
88 'arr[0]=$(printf "%0*d" 1000 0); echo ${#arr[0]}'
89
90 compare_output "array append" \
91 'arr=(); for i in $(seq 1 30); do arr+=("item$i"); done; echo ${#arr[@]}'
92
93 compare_output "array slice" \
94 'arr=(a b c d e f g h i j); echo ${arr[@]:3:4}'
95
96 compare_output "unset array elements" \
97 'arr=(1 2 3 4 5 6 7 8 9 10); unset arr[4]; unset arr[7]; echo ${#arr[@]}'
98
99 # ==========================================
100 # 4. Associative array stress tests
101 # ==========================================
102 section "4 - Associative array stress tests"
103
104 compare_output "basic assoc array" \
105 'declare -A m; m[a]=1; m[b]=2; m[c]=3; echo ${#m[@]}'
106
107 compare_output "assoc array with long value via var" \
108 'declare -A m; x=$(printf "%0*d" 1000 0); m[k]=$x; echo ${#m[k]}'
109
110 # BUG: assoc element assignment truncates long values (#35)
111 compare_output "assoc array element from command substitution" \
112 'declare -A m; m[x]=$(printf "%0*d" 1000 0); echo ${#m[x]}'
113
114 compare_output "assoc array overwrite" \
115 'declare -A m; m[k]=old; m[k]=new; echo ${m[k]}'
116
117 # ==========================================
118 # 5. Deep nesting
119 # ==========================================
120 section "5 - Deep nesting"
121
122 compare_output "nested if 10 levels" \
123 'x=1; if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then echo deep; fi; fi; fi; fi; fi; fi; fi; fi; fi; fi'
124
125 compare_output "nested loops 5 levels" \
126 'for a in 1 2; do for b in 1 2; do for c in 1 2; do for d in 1 2; do for e in 1 2; do echo "$a$b$c$d$e"; done; done; done; done; done | wc -l'
127
128 compare_output "nested command substitution 5 levels" \
129 'echo $(echo $(echo $(echo $(echo hello))))'
130
131 compare_output "nested case in loops" \
132 'for i in a b c; do case $i in a) echo A;; b) for j in 1 2; do case $j in 1) echo B1;; 2) echo B2;; esac; done;; c) echo C;; esac; done'
133
134 # ==========================================
135 # 6. Many variables
136 # ==========================================
137 section "6 - Many variables"
138
139 compare_output "100 variables" \
140 'for i in $(seq 1 100); do eval "var_$i=$i"; done; echo $var_1 $var_50 $var_100'
141
142 compare_output "export 50 variables" \
143 'for i in $(seq 1 50); do export "ev_$i=$i"; done; env | grep "^ev_" | wc -l'
144
145 compare_output "rapid set/unset cycle" \
146 'for i in $(seq 1 50); do x=$i; unset x; done; echo ${x:-unset}'
147
148 # ==========================================
149 # 7. Function stress tests
150 # ==========================================
151 section "7 - Function stress tests"
152
153 compare_output "function with many params" \
154 'f() { echo $# $1 $5 ${10}; }; f a b c d e f g h i j'
155
156 compare_output "recursive function 10 levels" \
157 'f() { if [ $1 -le 0 ]; then echo done; else f $(($1 - 1)); fi; }; f 10'
158
159 compare_output "recursive function 20 levels" \
160 'f() { if [ $1 -le 0 ]; then echo $1; return; fi; f $(($1 - 1)); }; f 20'
161
162 # BUG: recursive function 50 levels causes core dump (#36)
163 compare_output "recursive function 50 levels" \
164 'f() { if [ $1 -le 0 ]; then echo $1; return; fi; f $(($1 - 1)); }; f 50'
165
166 compare_output "function redefine in loop" \
167 'for i in 1 2 3; do eval "f() { echo $i; }"; f; done'
168
169 compare_output "function local var scoping" \
170 'f() { local x=inner; echo $x; }; x=outer; f; echo $x'
171
172 compare_output "nested function calls 5 levels" \
173 'a() { echo a; b; }; b() { echo b; c; }; c() { echo c; d; }; d() { echo d; e; }; e() { echo e; }; a'
174
175 # ==========================================
176 # 8. Alias stress tests
177 # ==========================================
178 section "8 - Alias stress tests"
179
180 check_output "alias with long command" \
181 'shopt -s expand_aliases; alias longcmd="echo hello_from_alias"; longcmd' \
182 'hello_from_alias'
183
184 compare_output "many aliases" \
185 'shopt -s expand_aliases; for i in $(seq 1 20); do alias "a$i=echo $i"; done; alias | wc -l'
186
187 # ==========================================
188 # 9. Trap stress tests
189 # ==========================================
190 section "9 - Trap stress tests"
191
192 compare_output "EXIT trap fires" \
193 'trap "echo trapped" EXIT; true'
194
195 compare_output "multiple trap signals" \
196 'trap "echo exit" EXIT; trap "echo hup" HUP; trap "echo usr1" USR1; trap -p | grep -cE "(EXIT|HUP|USR1)"'
197
198 compare_output "trap set/unset cycle" \
199 'for i in $(seq 1 10); do trap "echo $i" EXIT; done; trap -p EXIT'
200
201 # ==========================================
202 # 10. Heredoc stress tests
203 # ==========================================
204 section "10 - Heredoc stress tests"
205
206 compare_output "heredoc basic" \
207 'cat <<EOF
208 line1
209 line2
210 line3
211 EOF'
212
213 compare_output "heredoc with variable expansion" \
214 'x=world; cat <<EOF
215 hello $x
216 EOF'
217
218 compare_output "heredoc with long delimiter" \
219 'cat <<VERYLONGDELIMITERTHATISTOTALLYUNNECESSARY
220 hello
221 VERYLONGDELIMITERTHATISTOTALLYUNNECESSARY'
222
223 # ==========================================
224 # 11. String operations on large values
225 # ==========================================
226 section "11 - String operations"
227
228 compare_output "string length of 3000-char var" \
229 'x=$(head -c 3000 /dev/zero | tr "\0" "a"); echo ${#x}'
230
231 compare_output "uppercase transform large string" \
232 'x=$(head -c 1000 /dev/zero | tr "\0" "a"); echo ${x^^} | wc -c'
233
234 compare_output "string replacement large string" \
235 'x=$(head -c 1000 /dev/zero | tr "\0" "a"); echo ${x//a/b} | wc -c'
236
237 compare_output "concatenation stress" \
238 'x=""; for i in $(seq 1 100); do x="${x}abc"; done; echo ${#x}'
239
240 # ==========================================
241 # 12. Parameter expansion stress
242 # ==========================================
243 section "12 - Parameter expansion stress"
244
245 compare_output "default value expansion chain" \
246 'echo ${a:-${b:-${c:-${d:-${e:-deep}}}}}'
247
248 compare_output "indirect expansion" \
249 'x=hello; ref=x; echo ${!ref}'
250
251 compare_output "many positional params" \
252 'set -- $(seq 1 50); echo $# $1 ${25} ${50}'
253
254 compare_output "shift through many params" \
255 'set -- $(seq 1 30); shift 15; echo $1 $#'
256
257 compare_output "all positional params" \
258 'set -- $(seq 1 20); echo "$@" | wc -w'
259
260 # ==========================================
261 # 13. Pipeline stress
262 # ==========================================
263 section "13 - Pipeline stress"
264
265 compare_output "10-stage pipeline" \
266 'seq 1 100 | sort -n | head -50 | tail -10 | wc -l'
267
268 compare_output "pipeline with large data" \
269 'seq 1 10000 | wc -l'
270
271 compare_output "pipeline with grep chain" \
272 'seq 1 1000 | grep 5 | grep -v 50 | wc -l'
273
274 # ==========================================
275 # 14. Brace expansion stress
276 # ==========================================
277 section "14 - Brace expansion stress"
278
279 compare_output "large sequence" \
280 'echo {1..100} | wc -w'
281
282 compare_output "nested braces" \
283 'echo {a,b}{1,2,3}{x,y} | wc -w'
284
285 compare_output "alpha sequence" \
286 'echo {a..z} | wc -w'
287
288 # ==========================================
289 # 15. Boundary conditions
290 # ==========================================
291 section "15 - Boundary conditions"
292
293 compare_output "empty variable operations" \
294 'x=""; echo "${#x}" "${x:-default}" "${x:+set}"'
295
296 compare_output "single char variable" \
297 'x=a; echo ${#x} ${x}'
298
299 compare_output "null byte handling" \
300 'printf "a\0b" | wc -c'
301
302 compare_output "empty command substitution" \
303 'x=$(true); echo "[$x]"'
304
305 compare_output "whitespace-only variable" \
306 'x=" "; echo "${#x}" "$x"'
307
308 compare_output "long assignment value" \
309 'x=$(head -c 2000 /dev/zero | tr "\0" "z"); echo ${#x}'
310
311 compare_output "variable with special chars" \
312 'x="hello'\''world\"test"; echo "$x"'
313
314 # ==========================================
315 # 16. Pipeline overflow (formerly 4096 ceiling)
316 # ==========================================
317 section "16 - Pipeline overflow (>4096 byte values)"
318
319 compare_output "5000-char variable via head" \
320 'x=$(head -c 5000 /dev/zero | tr "\0" "a"); echo ${#x}'
321
322 compare_output "10000-char variable via head" \
323 'x=$(head -c 10000 /dev/zero | tr "\0" "a"); echo ${#x}'
324
325 compare_output "5000-char in echo" \
326 'x=$(head -c 5000 /dev/zero | tr "\0" "b"); echo ${#x}'
327
328 compare_output "8000-char variable via python" \
329 'x=$(python3 -c "print(\"c\"*8000, end=\"\")"); echo ${#x}'
330
331 compare_output "5000-char array assignment via word splitting" \
332 'x=$(head -c 5000 /dev/zero | tr "\0" "d"); arr=($x); echo ${#arr[0]}'
333
334 compare_output "10000-char array element" \
335 'x=$(head -c 10000 /dev/zero | tr "\0" "e"); arr=($x); echo ${#arr[0]}'
336
337 compare_output "multiple 5000-char array elements" \
338 'a=$(head -c 5000 /dev/zero | tr "\0" "f"); b=$(head -c 5000 /dev/zero | tr "\0" "g"); arr=($a $b); echo ${#arr[0]} ${#arr[1]}'
339
340 compare_output "5000-char variable string length" \
341 'x=$(head -c 5000 /dev/zero | tr "\0" "h"); echo ${#x}'
342
343 compare_output "5000-char variable substring" \
344 'x=$(head -c 5000 /dev/zero | tr "\0" "i"); y=${x:1000:2000}; echo ${#y}'
345
346 compare_output "command substitution preserves 10000 bytes" \
347 'echo $(head -c 10000 /dev/zero | tr "\0" "j") | wc -c'
348
349 compare_output "nested command sub with large output" \
350 'x=$(echo $(head -c 5000 /dev/zero | tr "\0" "k")); echo ${#x}'
351
352 compare_output "export 5000-char variable" \
353 'export BIGVAR=$(head -c 5000 /dev/zero | tr "\0" "m"); echo ${#BIGVAR}'
354
355 # ==========================================
356 # 17. Indirect expansion edge cases
357 # ==========================================
358 section "17 - Indirect expansion edge cases"
359
360 compare_output "indirect to unset variable" \
361 'ref=nosuchvar; echo "${!ref}"'
362
363 compare_output "indirect with default value" \
364 'x=hello; ref=x; echo ${!ref:-fallback}'
365
366 compare_output "indirect chain (not recursive)" \
367 'a=hello; b=a; echo ${!b}'
368
369 compare_output "indirect to empty variable" \
370 'x=""; ref=x; echo "[${!ref}]"'
371
372 compare_output "indirect to integer variable" \
373 'x=42; ref=x; echo ${!ref}'
374
375 compare_output "indirect to PATH" \
376 'ref=PATH; val=${!ref}; echo "${val:0:1}"'
377
378 compare_output "indirect with nounset on valid ref" \
379 'set -u; x=hello; ref=x; echo ${!ref}; set +u'
380
381 # ==========================================
382 # 18. Hardcoded limit boundary tests
383 # ==========================================
384 section "18 - Hardcoded limit boundaries"
385
386 # Case statement patterns (grammar_parser patterns(10) limit)
387 # NOTE: increasing case limits causes stack frame overflow in nested parsing.
388 # Keeping limits at patterns(10)/items(20) to avoid stack corruption.
389 # Issue tracked for future heap-allocation refactor.
390 compare_output "case with 9 patterns" \
391 'x=9; case $x in 1) echo a;; 2) echo b;; 3) echo c;; 4) echo d;; 5) echo e;; 6) echo f;; 7) echo g;; 8) echo h;; 9) echo i;; esac'
392
393 compare_output "case with 10 patterns" \
394 'x=10; case $x in 1) echo a;; 2) echo b;; 3) echo c;; 4) echo d;; 5) echo e;; 6) echo f;; 7) echo g;; 8) echo h;; 9) echo i;; 10) echo j;; esac'
395
396 compare_output "case with 18 items" \
397 'x=18; case $x in 1) echo a;; 2) echo b;; 3) echo c;; 4) echo d;; 5) echo e;; 6) echo f;; 7) echo g;; 8) echo h;; 9) echo i;; 10) echo j;; 11) echo k;; 12) echo l;; 13) echo m;; 14) echo n;; 15) echo o;; 16) echo p;; 17) echo q;; 18) echo r;; esac'
398
399 # Many function definitions (function_ast_cache(20) limit)
400 compare_output "25 function definitions" \
401 'for i in $(seq 1 25); do eval "f_$i() { echo $i; }"; done; f_1; f_13; f_25'
402
403 compare_output "30 function definitions" \
404 'for i in $(seq 1 30); do eval "f_$i() { echo $i; }"; done; f_1; f_15; f_30'
405
406 # Many prefix assignments (saved_var_names(10) limit)
407 compare_output "5 prefix assignments" \
408 'A=1 B=2 C=3 D=4 E=5 env | grep -c "^[ABCDE]="'
409
410 compare_output "12 prefix assignments" \
411 'A=1 B=2 C=3 D=4 E=5 F=6 G=7 H=8 I=9 J=10 K=11 L=12 env | grep -c "^[A-L]="'
412
413 # Many shell variables (MAX_SHELL_VARS=512)
414 compare_output "200 variables" \
415 'for i in $(seq 1 200); do eval "var_$i=$i"; done; echo $var_1 $var_100 $var_200'
416
417 compare_output "400 variables" \
418 'i=1; while [ $i -le 400 ]; do eval "var_$i=$i"; i=$((i+1)); done; echo $var_1 $var_200 $var_400'
419
420 # Control flow nesting depth (MAX_CONTROL_DEPTH=20)
421 compare_output "nested if 15 levels" \
422 'x=1; if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then if [ $x -eq 1 ]; then echo deep15; fi; fi; fi; fi; fi; fi; fi; fi; fi; fi; fi; fi; fi; fi; fi'
423
424 # Long variable names (MAX_VAR_NAME_LEN=256)
425 compare_output "100-char variable name" \
426 'eval "$(printf "a%.0s" $(seq 1 100))=hello"; eval "echo \$$(printf "a%.0s" $(seq 1 100))"'
427
428 compare_output "200-char variable name" \
429 'eval "$(printf "a%.0s" $(seq 1 200))=world"; eval "echo \$$(printf "a%.0s" $(seq 1 200))"'
430
431 # ==========================================
432 # 19. Recursion & stack pressure
433 # ==========================================
434 section "19 - Recursion and stack pressure"
435
436 compare_output "recursive function 75 levels" \
437 'f() { if [ $1 -le 0 ]; then echo $1; return; fi; f $(($1 - 1)); }; f 75'
438
439 compare_output "recursive function 100 levels" \
440 'f() { if [ $1 -le 0 ]; then echo $1; return; fi; f $(($1 - 1)); }; f 100'
441
442 compare_output "mutual recursion 20 levels" \
443 'a() { if [ $1 -le 0 ]; then echo done; return; fi; b $(($1-1)); }; b() { a "$1"; }; a 20'
444
445 compare_output "nested subshells 10 levels" \
446 '(echo 1; (echo 2; (echo 3; (echo 4; (echo 5; (echo 6; (echo 7; (echo 8; (echo 9; (echo 10))))))))))'
447
448 compare_output "nested command substitution 10 levels" \
449 'echo $(echo $(echo $(echo $(echo $(echo $(echo $(echo $(echo $(echo hello)))))))))'
450
451 compare_output "nested arithmetic 20 levels" \
452 'echo $(( ((((((((((((((((((((1+1)))))))))))))))))))) ))'
453
454 # ==========================================
455 # 20. Memory & buffer boundary tests
456 # ==========================================
457 section "20 - Buffer boundaries"
458
459 # Command substitution at exact power-of-2 boundaries
460 compare_output "cmd sub exactly 4096 bytes" \
461 'x=$(head -c 4096 /dev/zero | tr "\0" "a"); echo ${#x}'
462
463 compare_output "cmd sub exactly 4097 bytes" \
464 'x=$(head -c 4097 /dev/zero | tr "\0" "a"); echo ${#x}'
465
466 compare_output "cmd sub exactly 8192 bytes" \
467 'x=$(head -c 8192 /dev/zero | tr "\0" "a"); echo ${#x}'
468
469 compare_output "cmd sub exactly 8193 bytes" \
470 'x=$(head -c 8193 /dev/zero | tr "\0" "a"); echo ${#x}'
471
472 compare_output "cmd sub exactly 16384 bytes" \
473 'x=$(head -c 16384 /dev/zero | tr "\0" "a"); echo ${#x}'
474
475 compare_output "cmd sub exactly 65536 bytes" \
476 'x=$(head -c 65536 /dev/zero | tr "\0" "a"); echo ${#x}'
477
478 # Pattern matching on large strings
479 compare_output "pattern removal on 5000-char string" \
480 'x=$(head -c 5000 /dev/zero | tr "\0" "a"); y=${x#aaa}; echo ${#y}'
481
482 compare_output "pattern replace on 5000-char string" \
483 'x=$(head -c 5000 /dev/zero | tr "\0" "a"); echo ${x//a/b} | wc -c'
484
485 # Sparse array
486 compare_output "sparse array index 10000" \
487 'arr[10000]=hello; echo ${arr[10000]}'
488
489 compare_output "sparse array index 99999" \
490 'arr[99999]=world; echo ${arr[99999]}'
491
492 # Arithmetic integer boundaries
493 compare_output "arithmetic max int" \
494 'echo $((9223372036854775807))'
495
496 compare_output "arithmetic large multiplication" \
497 'echo $((1000000 * 1000000))'
498
499 compare_output "arithmetic negative" \
500 'echo $((-2147483648))'
501
502 # Substring at boundary offsets
503 compare_output "substring negative offset" \
504 'x=hello_world; echo ${x: -5}'
505
506 compare_output "substring offset beyond length" \
507 'x=hi; echo "[${x:100:5}]"'
508
509 compare_output "substring zero length" \
510 'x=hello; echo "[${x:2:0}]"'
511
512 # ==========================================
513 # 21. Tokenization limits
514 # ==========================================
515 section "21 - Tokenization limits"
516
517 compare_output "echo with 200 arguments" \
518 'echo $(seq 1 200) | wc -w'
519
520 compare_output "echo with 400 arguments" \
521 'echo $(seq 1 400) | wc -w'
522
523 compare_output "many semicolons 30 commands" \
524 'a=0; a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); a=$((a+1)); echo $a'
525
526 # ==========================================
527 # 22. FD & redirection stress
528 # ==========================================
529 section "22 - FD and redirection stress"
530
531 compare_output "rapid redirect cycling 100 iterations" \
532 'i=1; while [ $i -le 100 ]; do echo $i > /tmp/fortsh_test_fd_$$; i=$((i+1)); done; cat /tmp/fortsh_test_fd_$$; rm -f /tmp/fortsh_test_fd_$$'
533
534 compare_output "append redirect 200 lines" \
535 'rm -f /tmp/fortsh_test_append_$$; i=1; while [ $i -le 200 ]; do echo $i >> /tmp/fortsh_test_append_$$; i=$((i+1)); done; wc -l < /tmp/fortsh_test_append_$$; rm -f /tmp/fortsh_test_append_$$'
536
537 compare_output "stderr redirect in loop" \
538 'for i in 1 2 3 4 5; do echo "err$i" >&2; done 2>&1 | wc -l'
539
540 compare_output "output and error merge" \
541 '{ echo stdout; echo stderr >&2; } 2>&1 | sort'
542
543 compare_output "dev null redirect in loop" \
544 'i=1; while [ $i -le 100 ]; do echo $i > /dev/null; i=$((i+1)); done; echo done'
545
546 # ==========================================
547 # 23. Pipeline & fork stress
548 # ==========================================
549 section "23 - Pipeline and fork stress"
550
551 compare_output "15-stage pipeline" \
552 'echo hello | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat'
553
554 compare_output "20-stage pipeline" \
555 'echo hello | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat'
556
557 compare_output "pipeline with 100000 lines" \
558 'seq 1 100000 | wc -l'
559
560 compare_output "pipeline with sort 10000 lines" \
561 'seq 1 10000 | sort -rn 2>/dev/null | head -1'
562
563 compare_output "background job wait cycling" \
564 'i=1; while [ $i -le 50 ]; do true & i=$((i+1)); done; wait; echo done'
565
566 compare_output "100 background jobs then wait" \
567 'i=1; while [ $i -le 100 ]; do true & i=$((i+1)); done; wait; echo done'
568
569 compare_output "subshell pipeline isolation" \
570 'x=outer; echo $x | (read y; echo $y) | cat'
571
572 # ==========================================
573 # 24. Array scaling
574 # ==========================================
575 section "24 - Array scaling"
576
577 # BUG: arr[$i]=$i doesn't expand $i in subscript — use eval workaround
578 compare_output "indexed array 100 elements" \
579 'i=0; while [ $i -lt 100 ]; do eval "arr[$i]=$i"; i=$((i+1)); done; echo ${#arr[@]} ${arr[0]} ${arr[50]} ${arr[99]}'
580
581 compare_output "indexed array 500 elements" \
582 'i=0; while [ $i -lt 500 ]; do eval "arr[$i]=$i"; i=$((i+1)); done; echo ${#arr[@]} ${arr[0]} ${arr[250]} ${arr[499]}'
583
584 # BUG: arr=($(cmd)) doesn't word-split command substitution into elements
585 compare_output "indexed array via init list 20" \
586 'arr=(a b c d e f g h i j k l m n o p q r s t); echo ${#arr[@]} ${arr[0]} ${arr[19]}'
587
588 compare_output "array append 500 times" \
589 'arr=(); i=0; while [ $i -lt 500 ]; do arr+=("$i"); i=$((i+1)); done; echo ${#arr[@]}'
590
591 # BUG: m["key$i"] doesn't expand $i in subscript — use eval workaround
592 compare_output "assoc array 100 keys" \
593 'declare -A m; i=1; while [ $i -le 100 ]; do eval "m[key$i]=$i"; i=$((i+1)); done; echo ${#m[@]} ${m[key1]} ${m[key50]} ${m[key100]}'
594
595 compare_output "assoc array 200 keys" \
596 'declare -A m; i=1; while [ $i -le 200 ]; do eval "m[key$i]=$i"; i=$((i+1)); done; echo ${#m[@]}'
597
598 compare_output "assoc array overwrite cycle" \
599 'declare -A m; i=1; while [ $i -le 100 ]; do m[k]=$i; i=$((i+1)); done; echo ${m[k]}'
600
601 compare_output "array element 10000-char value" \
602 'x=$(head -c 10000 /dev/zero | tr "\0" "a"); arr[0]=$x; echo ${#arr[0]}'
603
604 # ==========================================
605 # 25. Control flow edge cases
606 # ==========================================
607 section "25 - Control flow edge cases"
608
609 compare_output "mixed loop nesting for-while-until" \
610 'for i in 1 2; do j=0; while [ $j -lt 2 ]; do k=5; until [ $k -le 3 ]; do k=$((k-1)); done; echo "$i $j $k"; j=$((j+1)); done; done'
611
612 compare_output "break 2 from inner loop" \
613 'for i in 1 2 3; do for j in a b c; do if [ "$j" = "b" ]; then break 2; fi; echo "$i$j"; done; done; echo end'
614
615 compare_output "break 3 from triple nested" \
616 'for i in 1 2; do for j in a b; do for k in x y; do if [ "$k" = "y" ]; then break 3; fi; echo "$i$j$k"; done; done; done; echo end'
617
618 compare_output "continue 2 from inner loop" \
619 'for i in 1 2 3; do for j in a b c; do if [ "$j" = "b" ]; then continue 2; fi; echo "$i$j"; done; done'
620
621 compare_output "loop 5000 iterations" \
622 'x=0; i=0; while [ $i -lt 5000 ]; do x=$((x+1)); i=$((i+1)); done; echo $x'
623
624 # 10000 iterations takes ~72s on ARM64, ~1s on x86_64
625 case "$(uname -m)" in
626 arm64|aarch64) TEST_TIMEOUT=180 ;;
627 *) TEST_TIMEOUT=60 ;;
628 esac
629 compare_output "loop 10000 iterations" \
630 'x=0; i=0; while [ $i -lt 10000 ]; do x=$((x+1)); i=$((i+1)); done; echo $x'
631 case "$(uname -m)" in
632 arm64|aarch64) TEST_TIMEOUT=90 ;;
633 *) TEST_TIMEOUT=30 ;;
634 esac
635
636 compare_output "while with complex condition" \
637 'i=0; while [ $i -lt 10 ] && [ $((i % 2)) -eq 0 -o $i -lt 5 ]; do echo $i; i=$((i+1)); done'
638
639 compare_output "nested case in loop" \
640 'for i in $(seq 1 20); do case $((i % 4)) in 0) echo "a$i";; 1) echo "b$i";; 2) echo "c$i";; 3) echo "d$i";; esac; done | wc -l'
641
642 # ==========================================
643 # 26. Trap & signal stress
644 # ==========================================
645 section "26 - Trap and signal stress"
646
647 compare_output "set traps on 10 signals" \
648 'trap "echo 1" HUP; trap "echo 2" INT; trap "echo 3" QUIT; trap "echo 4" TERM; trap "echo 5" USR1; trap "echo 6" USR2; trap "echo 7" PIPE; trap "echo 8" ALRM; trap "echo 9" CONT; trap "echo exit" EXIT; trap -p | wc -l'
649
650 compare_output "trap reset cycling 50 times" \
651 'i=1; while [ $i -le 50 ]; do trap "echo $i" EXIT; i=$((i+1)); done; trap -p EXIT'
652
653 compare_output "trap with long handler" \
654 'handler=""; i=1; while [ $i -le 100 ]; do handler="${handler}echo $i;"; i=$((i+1)); done; trap "$handler" EXIT; echo before'
655
656 compare_output "modify trap inside trap" \
657 'trap '\''trap "echo inner" EXIT; echo outer'\'' EXIT; exit 0'
658
659 compare_output "trap unset then reset" \
660 'trap "echo first" EXIT; trap - EXIT; trap "echo second" EXIT; exit 0'
661
662 # ==========================================
663 # 27. String & expansion stress
664 # ==========================================
665 section "27 - String and expansion stress"
666
667 compare_output "uppercase 5000-char variable" \
668 'x=$(head -c 5000 /dev/zero | tr "\0" "a"); echo ${x^^} | wc -c'
669
670 compare_output "lowercase 5000-char variable" \
671 'x=$(head -c 5000 /dev/zero | tr "\0" "A"); echo ${x,,} | wc -c'
672
673 compare_output "global replace 5000-char variable" \
674 'x=$(head -c 5000 /dev/zero | tr "\0" "a"); echo ${x//a/b} | wc -c'
675
676 compare_output "concatenation loop 5000 chars" \
677 'x=""; for i in $(seq 1 1000); do x="${x}abcde"; done; echo ${#x}'
678
679 compare_output "prefix removal on 10000-char string" \
680 'x=$(head -c 10000 /dev/zero | tr "\0" "a"); y=${x#aaaa}; echo ${#y}'
681
682 compare_output "suffix removal on 10000-char string" \
683 'x=$(head -c 10000 /dev/zero | tr "\0" "a"); y=${x%aaaa}; echo ${#y}'
684
685 compare_output "default expansion chain 8 deep" \
686 'echo ${a:-${b:-${c:-${d:-${e:-${f:-${g:-${h:-deep8}}}}}}}}'
687
688 compare_output "assign-default expansion" \
689 'unset x; echo ${x:=hello}; echo $x'
690
691 compare_output "error-if-unset expansion" \
692 'x=set; echo ${x:?should not error}'
693
694 # ==========================================
695 # 28. Heredoc scaling
696 # ==========================================
697 section "28 - Heredoc scaling"
698
699 compare_output "heredoc 50 lines" \
700 'cat <<EOF | wc -l
701 $(seq 1 50)
702 EOF'
703
704 compare_output "heredoc with 20 variable expansions" \
705 'a=1; b=2; c=3; d=4; e=5; f=6; g=7; h=8; i=9; j=10; cat <<EOF
706 $a $b $c $d $e $f $g $h $i $j $a $b $c $d $e $f $g $h $i $j
707 EOF'
708
709 compare_output "heredoc with command substitution" \
710 'cat <<EOF
711 $(echo hello) $(echo world) $(echo from) $(echo heredoc)
712 EOF'
713
714 compare_output "heredoc quoted delimiter no expansion" \
715 'x=should_not_expand; cat <<'\''EOF'\''
716 $x $(echo nope)
717 EOF'
718
719 compare_output "multiple heredocs sequential" \
720 'cat <<A; cat <<B; cat <<C
721 line_a
722 A
723 line_b
724 B
725 line_c
726 C'
727
728 # ==========================================
729 # 29. Glob expansion stress
730 # ==========================================
731 section "29 - Glob expansion"
732
733 compare_output "glob with many matches" \
734 'mkdir -p /tmp/fortsh_glob_$$; i=1; while [ $i -le 200 ]; do touch /tmp/fortsh_glob_$$/f$i.txt; i=$((i+1)); done; ls /tmp/fortsh_glob_$$/*.txt | wc -l; rm -rf /tmp/fortsh_glob_$$'
735
736 # Use check_output for glob no-match since $$ differs between bash and fortsh
737 check_output "glob with no matches nullglob off" \
738 'echo /tmp/no_such_dir_xyz/*.abc' \
739 '/tmp/no_such_dir_xyz/*.abc'
740
741 compare_output "glob bracket expression" \
742 'mkdir -p /tmp/fortsh_glob2_$$; touch /tmp/fortsh_glob2_$$/a1 /tmp/fortsh_glob2_$$/b2 /tmp/fortsh_glob2_$$/c3; ls /tmp/fortsh_glob2_$$/[abc][0-9] | wc -l; rm -rf /tmp/fortsh_glob2_$$'
743
744 compare_output "glob question mark" \
745 'mkdir -p /tmp/fortsh_glob3_$$; touch /tmp/fortsh_glob3_$$/ax /tmp/fortsh_glob3_$$/bx /tmp/fortsh_glob3_$$/cx; echo /tmp/fortsh_glob3_$$/?x | wc -w; rm -rf /tmp/fortsh_glob3_$$'
746
747 # ==========================================
748 # 30. Eval & dynamic execution stress
749 # ==========================================
750 section "30 - Eval and dynamic execution"
751
752 compare_output "eval with nested quoting" \
753 'x="echo hello"; eval "$x"'
754
755 compare_output "eval defining 50 variables" \
756 'for i in $(seq 1 50); do eval "v$i=$((i*2))"; done; echo $v1 $v25 $v50'
757
758 compare_output "eval redefining function 20 times" \
759 'for i in $(seq 1 20); do eval "f() { echo $i; }"; done; f'
760
761 compare_output "eval with command substitution" \
762 'x="echo \$(echo nested)"; eval "$x"'
763
764 compare_output "eval chain 5 deep" \
765 'eval "eval \"eval \\\"eval \\\\\\\"echo deep\\\\\\\"\\\"\""'
766
767 # ==========================================
768 # 31. Rapid set/unset & variable churn
769 # ==========================================
770 section "31 - Variable churn"
771
772 compare_output "set/unset 500 cycle" \
773 'i=0; while [ $i -lt 500 ]; do x=$i; unset x; i=$((i+1)); done; echo ${x:-done}'
774
775 compare_output "rapid export/unexport" \
776 'i=0; while [ $i -lt 100 ]; do export v=$i; unset v; i=$((i+1)); done; echo ${v:-done}'
777
778 compare_output "overwrite same variable 1000 times" \
779 'i=0; while [ $i -lt 1000 ]; do x=$i; i=$((i+1)); done; echo $x'
780
781 compare_output "local variable scoping 10 deep" \
782 'f() { local x=$1; if [ $1 -le 0 ]; then echo $x; return; fi; f $(($1-1)); echo $x; }; f 10 | head -1'
783
784 compare_output "local var does not leak" \
785 'f() { local inner=secret; }; f; echo "${inner:-not_leaked}"'
786
787 # ==========================================
788 # 32. Quoting & escaping edge cases
789 # ==========================================
790 section "32 - Quoting edge cases"
791
792 compare_output "single quotes preserve specials" \
793 'echo '\''$HOME $(echo no) `echo no`'\'''
794
795 compare_output "double quotes allow expansion" \
796 'x=works; echo "it $x"'
797
798 compare_output "mixed quote concatenation" \
799 "echo 'hello'\"world\"'more'"
800
801 compare_output "backslash in double quotes" \
802 'echo "a\\b" "c\"d" "e\$f"'
803
804 # dollar-single-quote ($'...') ANSI-C quoting
805 compare_output "dollar-single-quote escapes" \
806 "echo \$'hello\\tworld'"
807
808 compare_output "empty string arguments preserved" \
809 'f() { echo $#; }; f "" "" ""'
810
811 compare_output "IFS splitting edge case" \
812 'IFS=:; x="a:b::d"; for w in $x; do echo "[$w]"; done'
813
814 # ==========================================
815 # 33. Process substitution & subshell stress
816 # ==========================================
817 section "33 - Subshell isolation"
818
819 compare_output "subshell variable isolation" \
820 'x=outer; (x=inner; echo $x); echo $x'
821
822 compare_output "subshell exit code" \
823 '(exit 42); echo $?'
824
825 compare_output "nested subshell variable" \
826 'x=1; (x=2; (x=3; echo $x); echo $x); echo $x'
827
828 compare_output "subshell with loop" \
829 '(for i in 1 2 3; do echo $i; done) | wc -l'
830
831 compare_output "subshell does not affect parent vars" \
832 'x=before; (x=after; export x); echo $x'
833
834 # ==========================================
835 # 34. Compound command stress
836 # ==========================================
837 section "34 - Compound commands"
838
839 compare_output "brace group with many commands" \
840 '{ echo a; echo b; echo c; echo d; echo e; echo f; echo g; echo h; echo i; echo j; } | wc -l'
841
842 compare_output "brace group pipeline" \
843 '{ echo hello; echo world; } | sort'
844
845 compare_output "brace group with redirect" \
846 '{ echo line1; echo line2; echo line3; } > /tmp/fortsh_brace_$$; wc -l < /tmp/fortsh_brace_$$; rm -f /tmp/fortsh_brace_$$'
847
848 compare_output "nested brace groups" \
849 '{ { { echo deep; }; }; }'
850
851 compare_output "command list with mixed operators" \
852 'true && echo a; false || echo b; true && false || echo c'
853
854 compare_output "long AND chain" \
855 'true && true && true && true && true && true && true && true && true && true && echo all_true'
856
857 compare_output "long OR chain" \
858 'false || false || false || false || false || false || false || false || false || false || echo found'
859
860 compare_output "mixed AND/OR chain" \
861 'true && false || true && echo result'
862
863 print_summary