| 1 | #!/bin/sh |
| 2 | # ===================================== |
| 3 | # POSIX Compliance - Job Control Tests |
| 4 | # ===================================== |
| 5 | # Tests for POSIX job control features (jobs, fg, bg, job specs) |
| 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-jobcontrol]" |
| 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 | FORTSH_BIN="${FORTSH_BIN:-$SCRIPT_DIR/../bin/fortsh}" |
| 27 | |
| 28 | # Check if fortsh exists |
| 29 | if [ ! -x "$FORTSH_BIN" ]; then |
| 30 | printf "${RED}ERROR${NC}: fortsh binary not found at $FORTSH_BIN\n" |
| 31 | printf "Please run 'make' first or set FORTSH_BIN environment variable\n" |
| 32 | exit 1 |
| 33 | fi |
| 34 | |
| 35 | # Test result trackers |
| 36 | pass() { |
| 37 | TEST_NUM=$((TEST_NUM + 1)) |
| 38 | printf "${GREEN}✓ PASS${NC} ${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}: %s\n" "$1" |
| 39 | PASSED=$((PASSED + 1)) |
| 40 | } |
| 41 | |
| 42 | fail() { |
| 43 | TEST_NUM=$((TEST_NUM + 1)) |
| 44 | TEST_ID="${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}" |
| 45 | printf "${RED}✗ FAIL${NC} ${TEST_ID}: %s\n" "$1" |
| 46 | FAILED_TESTS_LIST="${FAILED_TESTS_LIST} ${TEST_ID}: $1\n" |
| 47 | if [ -n "$2" ]; then |
| 48 | printf " expected: %s\n" "$2" |
| 49 | fi |
| 50 | if [ -n "$3" ]; then |
| 51 | printf " got: %s\n" "$3" |
| 52 | fi |
| 53 | FAILED=$((FAILED + 1)) |
| 54 | } |
| 55 | |
| 56 | skip() { |
| 57 | TEST_NUM=$((TEST_NUM + 1)) |
| 58 | printf "${YELLOW}⊘ SKIP${NC} ${TEST_PREFIX} ${CURRENT_SECTION}.${TEST_NUM}: %s\n" "$1" |
| 59 | SKIPPED=$((SKIPPED + 1)) |
| 60 | } |
| 61 | |
| 62 | section() { |
| 63 | # Extract section number from header like "146. BASIC JOB CONTROL" |
| 64 | CURRENT_SECTION=$(echo "$1" | grep -oE '^[0-9]+' || echo "0") |
| 65 | TEST_NUM=0 |
| 66 | printf "\n${BLUE}==========================================\n" |
| 67 | printf "%s\n" "$1" |
| 68 | printf "==========================================${NC}\n" |
| 69 | } |
| 70 | |
| 71 | # Test that command succeeds |
| 72 | test_succeeds() { |
| 73 | test_name="$1" |
| 74 | test_cmd="$2" |
| 75 | |
| 76 | if FORTSH_RC_FILE=/dev/null "$FORTSH_BIN" -c "$test_cmd" >/dev/null 2>&1; then |
| 77 | pass "$test_name" |
| 78 | else |
| 79 | fail "$test_name" "should succeed" "failed" |
| 80 | fi |
| 81 | } |
| 82 | |
| 83 | # Test that command produces expected output |
| 84 | test_output() { |
| 85 | test_name="$1" |
| 86 | test_cmd="$2" |
| 87 | expected="$3" |
| 88 | |
| 89 | output=$(FORTSH_RC_FILE=/dev/null "$FORTSH_BIN" -c "$test_cmd" 2>&1) |
| 90 | if [ "$output" = "$expected" ]; then |
| 91 | pass "$test_name" |
| 92 | else |
| 93 | fail "$test_name" "$expected" "$output" |
| 94 | fi |
| 95 | } |
| 96 | |
| 97 | # Test that output contains pattern |
| 98 | test_contains() { |
| 99 | test_name="$1" |
| 100 | test_cmd="$2" |
| 101 | pattern="$3" |
| 102 | |
| 103 | output=$(FORTSH_RC_FILE=/dev/null "$FORTSH_BIN" -c "$test_cmd" 2>&1) |
| 104 | if echo "$output" | grep -q "$pattern"; then |
| 105 | pass "$test_name" |
| 106 | else |
| 107 | fail "$test_name" "output containing '$pattern'" "$output" |
| 108 | fi |
| 109 | } |
| 110 | |
| 111 | # ===================================== |
| 112 | # TESTS START HERE |
| 113 | # ===================================== |
| 114 | |
| 115 | section "146. BASIC JOB CONTROL" |
| 116 | |
| 117 | # Note: Many job control features require interactive mode |
| 118 | # We test what we can in non-interactive mode |
| 119 | |
| 120 | test_succeeds "jobs builtin exists" 'jobs >/dev/null 2>&1 || true' |
| 121 | test_succeeds "bg builtin exists" 'bg 2>/dev/null || true' |
| 122 | test_succeeds "fg builtin exists" 'fg 2>/dev/null || true' |
| 123 | |
| 124 | section "147. BACKGROUND JOBS" |
| 125 | |
| 126 | test_succeeds "simple background job" 'sleep 0.1 &' |
| 127 | test_succeeds "background with wait" 'sleep 0.1 & wait' |
| 128 | test_output "background job count" 'sleep 0.2 & sleep 0.2 & jobs | wc -l | tr -d " "' '2' |
| 129 | test_succeeds "wait for specific job" 'sleep 0.1 & PID=$!; wait $PID' |
| 130 | |
| 131 | section "148. JOB EXIT STATUS" |
| 132 | |
| 133 | test_output "background true exit" 'true & wait $!; echo $?' '0' |
| 134 | test_output "background false exit" 'false & wait $!; echo $?' '1' |
| 135 | test_output "wait preserves status" 'sh -c "exit 42" & wait $!; echo $?' '42' |
| 136 | |
| 137 | section "149. JOBS BUILTIN OUTPUT" |
| 138 | |
| 139 | test_succeeds "jobs with no jobs" 'jobs' |
| 140 | test_succeeds "jobs after background" 'sleep 0.5 & jobs; wait' |
| 141 | # Test that jobs shows running processes |
| 142 | test_contains "jobs shows running" 'sleep 0.5 & jobs' 'sleep' |
| 143 | |
| 144 | section "150. BACKGROUND PIPELINES" |
| 145 | |
| 146 | test_succeeds "pipeline in background" 'echo test | cat &' |
| 147 | test_succeeds "multi-stage background pipeline" 'echo test | cat | cat & wait' |
| 148 | # Pipeline output appears before exit status since it runs in background |
| 149 | test_output "background pipeline exit" 'true | cat & wait $!; echo $?' '0' |
| 150 | |
| 151 | section "151. $! LAST BACKGROUND PID" |
| 152 | |
| 153 | test_succeeds "$! is set after background" 'sleep 0.1 & test -n "$!"' |
| 154 | # Use test -gt to check numeric - pattern [!0-9] has portability issues |
| 155 | test_succeeds "$! is numeric" 'sleep 0.1 & test "$!" -gt 0' |
| 156 | test_succeeds "wait for $!" 'sleep 0.1 & wait $!' |
| 157 | |
| 158 | section "152. JOB SPECIFICATIONS (if supported)" |
| 159 | |
| 160 | # These may not work in non-interactive mode, so we're lenient |
| 161 | skip "job spec %1 (interactive feature)" |
| 162 | skip "job spec %% (interactive feature)" |
| 163 | skip "job spec %+ (interactive feature)" |
| 164 | skip "job spec %- (interactive feature)" |
| 165 | |
| 166 | section "153. FG/BG WITH SUSPENDED JOBS" |
| 167 | |
| 168 | # Suspending jobs requires interactive terminal (Ctrl-Z) |
| 169 | skip "fg with suspended job (requires interactive tty)" |
| 170 | skip "bg with suspended job (requires interactive tty)" |
| 171 | |
| 172 | section "154. WAIT EDGE CASES" |
| 173 | |
| 174 | test_output "wait with no args" 'sleep 0.1 & sleep 0.1 & wait; echo done' 'done' |
| 175 | test_output "wait nonexistent PID" 'wait 999999 2>&1 | grep -q "not found\|No such" && echo error || echo ok' 'error' |
| 176 | test_succeeds "multiple waits" 'sleep 0.1 & P1=$!; sleep 0.1 & P2=$!; wait $P1; wait $P2' |
| 177 | |
| 178 | section "155. SET -m MONITOR MODE" |
| 179 | |
| 180 | test_succeeds "set -m enables" 'set -m' |
| 181 | test_succeeds "set +m disables" 'set -m; set +m' |
| 182 | test_output "monitor doesn't affect output" 'set -m; echo test; set +m' 'test' |
| 183 | |
| 184 | section "156. JOB CONTROL WITH FUNCTIONS" |
| 185 | |
| 186 | test_succeeds "background function" 'f() { echo test; }; f &' |
| 187 | test_output "wait for function" 'f() { echo ok; }; f & wait; echo done' 'ok |
| 188 | done' |
| 189 | |
| 190 | section "157. DISOWN (if implemented)" |
| 191 | |
| 192 | # disown is not strictly POSIX but common |
| 193 | skip "disown builtin (not required by POSIX)" |
| 194 | |
| 195 | section "158. AMPERSAND SEMANTICS" |
| 196 | |
| 197 | # Background jobs still output to stdout - just test it succeeds |
| 198 | test_succeeds "& at end" 'echo test &' |
| 199 | # Background output order is nondeterministic; check all values are present |
| 200 | test_output_unordered() { |
| 201 | test_name="$1" |
| 202 | test_cmd="$2" |
| 203 | shift 2 |
| 204 | |
| 205 | output=$(FORTSH_RC_FILE=/dev/null "$FORTSH_BIN" -c "$test_cmd" 2>&1) |
| 206 | for word in "$@"; do |
| 207 | if ! echo "$output" | grep -qF "$word"; then |
| 208 | fail "$test_name" "output containing '$word'" "$output" |
| 209 | return |
| 210 | fi |
| 211 | done |
| 212 | pass "$test_name" |
| 213 | } |
| 214 | test_output_unordered "multiple & commands" 'echo a & echo b & wait; echo done' a b done |
| 215 | |
| 216 | section "159. BACKGROUND SUBSHELLS" |
| 217 | |
| 218 | test_succeeds "subshell in background" '(sleep 0.1) &' |
| 219 | test_output "background subshell isolation" 'VAR=outer; (VAR=inner) & wait; echo $VAR' 'outer' |
| 220 | |
| 221 | section "160. JOB CONTROL ERROR CASES" |
| 222 | |
| 223 | # Test error messages for invalid job control operations |
| 224 | test_contains "fg with no jobs" 'fg 2>&1' 'no.*job\|No current job' |
| 225 | test_contains "bg with no jobs" 'bg 2>&1' 'no.*job\|No current job' |
| 226 | |
| 227 | # ===================================== |
| 228 | # SUMMARY |
| 229 | # ===================================== |
| 230 | |
| 231 | printf "\n==========================================\n" |
| 232 | printf "JOB CONTROL TEST RESULTS ${TEST_PREFIX}\n" |
| 233 | printf "==========================================\n" |
| 234 | printf "${GREEN}Passed:${NC} %d\n" "$PASSED" |
| 235 | printf "${RED}Failed:${NC} %d\n" "$FAILED" |
| 236 | printf "${YELLOW}Skipped:${NC} %d\n" "$SKIPPED" |
| 237 | printf "Total: %d\n" "$((PASSED + FAILED + SKIPPED))" |
| 238 | printf "==========================================\n" |
| 239 | |
| 240 | # Calculate pass rate (excluding skipped) |
| 241 | if [ "$((PASSED + FAILED))" -gt 0 ]; then |
| 242 | pass_rate=$((PASSED * 100 / (PASSED + FAILED))) |
| 243 | printf "Pass rate: %d%% (excluding skipped)\n" "$pass_rate" |
| 244 | fi |
| 245 | |
| 246 | if [ "$FAILED" -gt 0 ]; then |
| 247 | printf "\n${RED}Failed tests:${NC}\n" |
| 248 | printf "%b" "$FAILED_TESTS_LIST" |
| 249 | printf "==========================================\n" |
| 250 | fi |
| 251 | |
| 252 | if [ "$FAILED" -eq 0 ]; then |
| 253 | printf "${GREEN}ALL NON-SKIPPED JOB CONTROL TESTS PASSED!${NC} ✓\n" |
| 254 | exit 0 |
| 255 | else |
| 256 | printf "${RED}SOME TESTS FAILED${NC} ✗\n" |
| 257 | exit 1 |
| 258 | fi |