@@ -0,0 +1,836 @@ |
| | 1 | +#!/bin/bash |
| | 2 | +# |
| | 3 | +# FERP Grep Comparison Test Suite |
| | 4 | +# Systematically compares ferp output against GNU grep |
| | 5 | +# |
| | 6 | +# This test suite: |
| | 7 | +# 1. Generates test fixtures programmatically (no hand-crafted data bugs) |
| | 8 | +# 2. Compares ferp output directly against grep for each test |
| | 9 | +# 3. Tests all individual flags |
| | 10 | +# 4. Tests common flag combinations |
| | 11 | +# 5. Tests edge cases and regression scenarios |
| | 12 | +# |
| | 13 | +# Usage: ./grep_comparison_test.sh [--verbose] [--stop-on-fail] [--filter PATTERN] |
| | 14 | +# |
| | 15 | + |
| | 16 | +set -uo pipefail |
| | 17 | + |
| | 18 | +#------------------------------------------------------------------------------ |
| | 19 | +# Configuration |
| | 20 | +#------------------------------------------------------------------------------ |
| | 21 | + |
| | 22 | +RED='\033[0;31m' |
| | 23 | +GREEN='\033[0;32m' |
| | 24 | +YELLOW='\033[1;33m' |
| | 25 | +BLUE='\033[0;34m' |
| | 26 | +CYAN='\033[0;36m' |
| | 27 | +NC='\033[0m' |
| | 28 | + |
| | 29 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| | 30 | +FERP="${SCRIPT_DIR}/../ferp" |
| | 31 | +FIXTURES="${SCRIPT_DIR}/generated_fixtures" |
| | 32 | + |
| | 33 | +# Test counters |
| | 34 | +TESTS_RUN=0 |
| | 35 | +TESTS_PASSED=0 |
| | 36 | +TESTS_FAILED=0 |
| | 37 | +TESTS_SKIPPED=0 |
| | 38 | + |
| | 39 | +# Options |
| | 40 | +VERBOSE=false |
| | 41 | +STOP_ON_FAIL=false |
| | 42 | +FILTER="" |
| | 43 | + |
| | 44 | +# Track failures for summary |
| | 45 | +declare -a FAILED_TESTS=() |
| | 46 | + |
| | 47 | +#------------------------------------------------------------------------------ |
| | 48 | +# Argument Parsing |
| | 49 | +#------------------------------------------------------------------------------ |
| | 50 | + |
| | 51 | +while [[ $# -gt 0 ]]; do |
| | 52 | + case $1 in |
| | 53 | + --verbose|-v) |
| | 54 | + VERBOSE=true |
| | 55 | + shift |
| | 56 | + ;; |
| | 57 | + --stop-on-fail|-x) |
| | 58 | + STOP_ON_FAIL=true |
| | 59 | + shift |
| | 60 | + ;; |
| | 61 | + --filter|-f) |
| | 62 | + FILTER="$2" |
| | 63 | + shift 2 |
| | 64 | + ;; |
| | 65 | + --help|-h) |
| | 66 | + echo "Usage: $0 [--verbose] [--stop-on-fail] [--filter PATTERN]" |
| | 67 | + echo "" |
| | 68 | + echo "Options:" |
| | 69 | + echo " -v, --verbose Show detailed output for each test" |
| | 70 | + echo " -x, --stop-on-fail Stop on first failure" |
| | 71 | + echo " -f, --filter PAT Only run tests matching PATTERN" |
| | 72 | + exit 0 |
| | 73 | + ;; |
| | 74 | + *) |
| | 75 | + echo "Unknown option: $1" |
| | 76 | + exit 1 |
| | 77 | + ;; |
| | 78 | + esac |
| | 79 | +done |
| | 80 | + |
| | 81 | +#------------------------------------------------------------------------------ |
| | 82 | +# Test Framework |
| | 83 | +#------------------------------------------------------------------------------ |
| | 84 | + |
| | 85 | +log() { |
| | 86 | + if [[ "$VERBOSE" == "true" ]]; then |
| | 87 | + echo -e "$1" |
| | 88 | + fi |
| | 89 | +} |
| | 90 | + |
| | 91 | +section() { |
| | 92 | + echo "" |
| | 93 | + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" |
| | 94 | + echo -e "${BLUE} $1${NC}" |
| | 95 | + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" |
| | 96 | +} |
| | 97 | + |
| | 98 | +pass() { |
| | 99 | + ((TESTS_PASSED++)) |
| | 100 | + ((TESTS_RUN++)) |
| | 101 | + echo -e "${GREEN}PASS${NC}: $1" |
| | 102 | +} |
| | 103 | + |
| | 104 | +fail() { |
| | 105 | + local name="$1" |
| | 106 | + local reason="${2:-}" |
| | 107 | + ((TESTS_FAILED++)) |
| | 108 | + ((TESTS_RUN++)) |
| | 109 | + echo -e "${RED}FAIL${NC}: $name" |
| | 110 | + if [[ -n "$reason" ]]; then |
| | 111 | + echo -e " ${YELLOW}Reason:${NC} $reason" |
| | 112 | + fi |
| | 113 | + FAILED_TESTS+=("$name") |
| | 114 | + if [[ "$STOP_ON_FAIL" == "true" ]]; then |
| | 115 | + echo -e "${RED}Stopping on first failure${NC}" |
| | 116 | + print_summary |
| | 117 | + exit 1 |
| | 118 | + fi |
| | 119 | +} |
| | 120 | + |
| | 121 | +skip() { |
| | 122 | + local name="$1" |
| | 123 | + local reason="${2:-}" |
| | 124 | + ((TESTS_SKIPPED++)) |
| | 125 | + echo -e "${YELLOW}SKIP${NC}: $name${reason:+ ($reason)}" |
| | 126 | +} |
| | 127 | + |
| | 128 | +should_run() { |
| | 129 | + local name="$1" |
| | 130 | + if [[ -n "$FILTER" && ! "$name" =~ $FILTER ]]; then |
| | 131 | + return 1 |
| | 132 | + fi |
| | 133 | + return 0 |
| | 134 | +} |
| | 135 | + |
| | 136 | +#------------------------------------------------------------------------------ |
| | 137 | +# Core Comparison Function |
| | 138 | +#------------------------------------------------------------------------------ |
| | 139 | + |
| | 140 | +# Compare ferp output against grep |
| | 141 | +# Returns 0 if outputs match, 1 if different |
| | 142 | +compare_with_grep() { |
| | 143 | + local flags="$1" |
| | 144 | + local pattern="$2" |
| | 145 | + local file="$3" |
| | 146 | + local name="${4:-}" |
| | 147 | + |
| | 148 | + local grep_out grep_exit ferp_out ferp_exit |
| | 149 | + |
| | 150 | + # Run grep |
| | 151 | + grep_out=$(grep $flags -- "$pattern" "$file" 2>/dev/null) && grep_exit=0 || grep_exit=$? |
| | 152 | + |
| | 153 | + # Run ferp |
| | 154 | + ferp_out=$("$FERP" $flags -- "$pattern" "$file" 2>/dev/null) && ferp_exit=0 || ferp_exit=$? |
| | 155 | + |
| | 156 | + # Compare outputs |
| | 157 | + if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then |
| | 158 | + return 0 |
| | 159 | + else |
| | 160 | + if [[ "$VERBOSE" == "true" ]]; then |
| | 161 | + echo " grep output ($grep_exit): $(echo "$grep_out" | head -3)" |
| | 162 | + echo " ferp output ($ferp_exit): $(echo "$ferp_out" | head -3)" |
| | 163 | + fi |
| | 164 | + return 1 |
| | 165 | + fi |
| | 166 | +} |
| | 167 | + |
| | 168 | +# Compare with stdin input |
| | 169 | +compare_with_grep_stdin() { |
| | 170 | + local flags="$1" |
| | 171 | + local pattern="$2" |
| | 172 | + local input="$3" |
| | 173 | + local name="${4:-}" |
| | 174 | + |
| | 175 | + local grep_out grep_exit ferp_out ferp_exit |
| | 176 | + |
| | 177 | + grep_out=$(echo -e "$input" | grep $flags -- "$pattern" 2>/dev/null) && grep_exit=0 || grep_exit=$? |
| | 178 | + ferp_out=$(echo -e "$input" | "$FERP" $flags -- "$pattern" 2>/dev/null) && ferp_exit=0 || ferp_exit=$? |
| | 179 | + |
| | 180 | + if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then |
| | 181 | + return 0 |
| | 182 | + else |
| | 183 | + if [[ "$VERBOSE" == "true" ]]; then |
| | 184 | + echo " grep output ($grep_exit): $(echo "$grep_out" | head -3)" |
| | 185 | + echo " ferp output ($ferp_exit): $(echo "$ferp_out" | head -3)" |
| | 186 | + fi |
| | 187 | + return 1 |
| | 188 | + fi |
| | 189 | +} |
| | 190 | + |
| | 191 | +# Test a flag/pattern/file combination |
| | 192 | +test_grep_compat() { |
| | 193 | + local name="$1" |
| | 194 | + local flags="$2" |
| | 195 | + local pattern="$3" |
| | 196 | + local file="$4" |
| | 197 | + |
| | 198 | + should_run "$name" || return 0 |
| | 199 | + |
| | 200 | + log "${CYAN}Testing:${NC} $name" |
| | 201 | + log " Command: grep $flags '$pattern' $file" |
| | 202 | + |
| | 203 | + if compare_with_grep "$flags" "$pattern" "$file" "$name"; then |
| | 204 | + pass "$name" |
| | 205 | + else |
| | 206 | + fail "$name" "Output differs from grep" |
| | 207 | + fi |
| | 208 | +} |
| | 209 | + |
| | 210 | +# Test with stdin |
| | 211 | +test_grep_compat_stdin() { |
| | 212 | + local name="$1" |
| | 213 | + local flags="$2" |
| | 214 | + local pattern="$3" |
| | 215 | + local input="$4" |
| | 216 | + |
| | 217 | + should_run "$name" || return 0 |
| | 218 | + |
| | 219 | + log "${CYAN}Testing:${NC} $name" |
| | 220 | + |
| | 221 | + if compare_with_grep_stdin "$flags" "$pattern" "$input" "$name"; then |
| | 222 | + pass "$name" |
| | 223 | + else |
| | 224 | + fail "$name" "Output differs from grep" |
| | 225 | + fi |
| | 226 | +} |
| | 227 | + |
| | 228 | +#------------------------------------------------------------------------------ |
| | 229 | +# Fixture Generation |
| | 230 | +#------------------------------------------------------------------------------ |
| | 231 | + |
| | 232 | +generate_fixtures() { |
| | 233 | + echo -e "${BLUE}Generating test fixtures...${NC}" |
| | 234 | + |
| | 235 | + rm -rf "$FIXTURES" |
| | 236 | + mkdir -p "$FIXTURES" |
| | 237 | + |
| | 238 | + # Basic text file with various patterns |
| | 239 | + cat > "$FIXTURES/basic.txt" << 'EOF' |
| | 240 | +hello world |
| | 241 | +Hello World |
| | 242 | +HELLO WORLD |
| | 243 | +goodbye world |
| | 244 | +foo bar baz |
| | 245 | +The quick brown fox |
| | 246 | +testing 123 testing |
| | 247 | +line with hello in the middle |
| | 248 | +EOF |
| | 249 | + |
| | 250 | + # File for word boundary tests |
| | 251 | + cat > "$FIXTURES/words.txt" << 'EOF' |
| | 252 | +test |
| | 253 | +testing |
| | 254 | +tested |
| | 255 | +tester |
| | 256 | +a test here |
| | 257 | +test-case |
| | 258 | +pre-test |
| | 259 | +pretest |
| | 260 | +TEST |
| | 261 | +Test |
| | 262 | +EOF |
| | 263 | + |
| | 264 | + # File for line matching tests |
| | 265 | + cat > "$FIXTURES/lines.txt" << 'EOF' |
| | 266 | +exact |
| | 267 | +exact match |
| | 268 | +not exact |
| | 269 | +exactly |
| | 270 | +EXACT |
| | 271 | +EOF |
| | 272 | + |
| | 273 | + # File with numbers for context tests |
| | 274 | + cat > "$FIXTURES/numbered.txt" << 'EOF' |
| | 275 | +line 1 - before |
| | 276 | +line 2 - before |
| | 277 | +line 3 - before |
| | 278 | +line 4 - MATCH |
| | 279 | +line 5 - after |
| | 280 | +line 6 - after |
| | 281 | +line 7 - after |
| | 282 | +line 8 - before |
| | 283 | +line 9 - MATCH |
| | 284 | +line 10 - after |
| | 285 | +EOF |
| | 286 | + |
| | 287 | + # File for case sensitivity tests |
| | 288 | + cat > "$FIXTURES/cases.txt" << 'EOF' |
| | 289 | +apple |
| | 290 | +Apple |
| | 291 | +APPLE |
| | 292 | +aPpLe |
| | 293 | +apples |
| | 294 | +Apples |
| | 295 | +APPLES |
| | 296 | +pineapple |
| | 297 | +EOF |
| | 298 | + |
| | 299 | + # File with special regex characters |
| | 300 | + cat > "$FIXTURES/special.txt" << 'EOF' |
| | 301 | +price is $100 |
| | 302 | +rate is 50% |
| | 303 | +path/to/file |
| | 304 | +array[0] |
| | 305 | +func() |
| | 306 | +a+b=c |
| | 307 | +a*b |
| | 308 | +a.b |
| | 309 | +start^ |
| | 310 | +end$ |
| | 311 | +back\slash |
| | 312 | +pipe|char |
| | 313 | +question? |
| | 314 | +curly{brace} |
| | 315 | +EOF |
| | 316 | + |
| | 317 | + # File with empty lines |
| | 318 | + cat > "$FIXTURES/empty_lines.txt" << 'EOF' |
| | 319 | +first line |
| | 320 | + |
| | 321 | +third line |
| | 322 | + |
| | 323 | +fifth line |
| | 324 | +EOF |
| | 325 | + |
| | 326 | + # File for counting tests |
| | 327 | + cat > "$FIXTURES/count.txt" << 'EOF' |
| | 328 | +match one |
| | 329 | +no hit |
| | 330 | +match two |
| | 331 | +match three |
| | 332 | +no hit |
| | 333 | +match four |
| | 334 | +EOF |
| | 335 | + |
| | 336 | + # Multiple files for -l/-L tests |
| | 337 | + echo -e "has pattern\nanother line" > "$FIXTURES/multi1.txt" |
| | 338 | + echo -e "no hits here\nnothing" > "$FIXTURES/multi2.txt" |
| | 339 | + echo -e "also has pattern\nmore" > "$FIXTURES/multi3.txt" |
| | 340 | + |
| | 341 | + # Very long line |
| | 342 | + printf 'start ' > "$FIXTURES/longline.txt" |
| | 343 | + printf 'x%.0s' {1..1000} >> "$FIXTURES/longline.txt" |
| | 344 | + printf ' middle ' >> "$FIXTURES/longline.txt" |
| | 345 | + printf 'y%.0s' {1..1000} >> "$FIXTURES/longline.txt" |
| | 346 | + printf ' end\n' >> "$FIXTURES/longline.txt" |
| | 347 | + echo "short line" >> "$FIXTURES/longline.txt" |
| | 348 | + |
| | 349 | + # Binary file (with null bytes) |
| | 350 | + printf 'text\x00binary\x00more text\n' > "$FIXTURES/binary.bin" |
| | 351 | + |
| | 352 | + # Empty file |
| | 353 | + touch "$FIXTURES/empty.txt" |
| | 354 | + |
| | 355 | + # Single line no newline |
| | 356 | + printf 'no trailing newline' > "$FIXTURES/no_newline.txt" |
| | 357 | + |
| | 358 | + # Unicode (if supported) |
| | 359 | + echo "café résumé naïve" > "$FIXTURES/unicode.txt" |
| | 360 | + echo "日本語テスト" >> "$FIXTURES/unicode.txt" |
| | 361 | + |
| | 362 | + # File with tabs and spaces |
| | 363 | + printf 'tab\there\n' > "$FIXTURES/whitespace.txt" |
| | 364 | + printf ' spaces \n' >> "$FIXTURES/whitespace.txt" |
| | 365 | + printf 'mixed\t \ttabs\n' >> "$FIXTURES/whitespace.txt" |
| | 366 | + |
| | 367 | + echo -e "${GREEN}Generated $(ls "$FIXTURES" | wc -l) fixture files${NC}" |
| | 368 | +} |
| | 369 | + |
| | 370 | +#------------------------------------------------------------------------------ |
| | 371 | +# Single Flag Tests |
| | 372 | +#------------------------------------------------------------------------------ |
| | 373 | + |
| | 374 | +test_single_flags() { |
| | 375 | + section "Single Flag Tests (vs grep)" |
| | 376 | + |
| | 377 | + # -i: case insensitive |
| | 378 | + test_grep_compat "-i: case insensitive match" "-i" "apple" "$FIXTURES/cases.txt" |
| | 379 | + test_grep_compat "-i: case insensitive no match" "-i" "banana" "$FIXTURES/cases.txt" |
| | 380 | + |
| | 381 | + # -v: invert match |
| | 382 | + test_grep_compat "-v: invert match" "-v" "apple" "$FIXTURES/cases.txt" |
| | 383 | + test_grep_compat "-v: invert all" "-v" "xxxxx" "$FIXTURES/cases.txt" |
| | 384 | + |
| | 385 | + # -w: word match |
| | 386 | + test_grep_compat "-w: word boundary match" "-w" "test" "$FIXTURES/words.txt" |
| | 387 | + test_grep_compat "-w: word boundary no partial" "-w" "est" "$FIXTURES/words.txt" |
| | 388 | + |
| | 389 | + # -x: line match |
| | 390 | + test_grep_compat "-x: exact line match" "-x" "exact" "$FIXTURES/lines.txt" |
| | 391 | + test_grep_compat "-x: exact line no partial" "-x" "exact match" "$FIXTURES/lines.txt" |
| | 392 | + |
| | 393 | + # -c: count |
| | 394 | + test_grep_compat "-c: count matches" "-c" "match" "$FIXTURES/count.txt" |
| | 395 | + test_grep_compat "-c: count zero" "-c" "xxxxx" "$FIXTURES/count.txt" |
| | 396 | + |
| | 397 | + # -l: files with matches |
| | 398 | + test_grep_compat "-l: list matching files" "-l" "pattern" "$FIXTURES/multi1.txt" |
| | 399 | + |
| | 400 | + # -L: files without matches |
| | 401 | + test_grep_compat "-L: list non-matching files" "-L" "pattern" "$FIXTURES/multi2.txt" |
| | 402 | + |
| | 403 | + # -n: line numbers |
| | 404 | + test_grep_compat "-n: show line numbers" "-n" "MATCH" "$FIXTURES/numbered.txt" |
| | 405 | + |
| | 406 | + # -b: byte offset |
| | 407 | + test_grep_compat "-b: show byte offset" "-b" "hello" "$FIXTURES/basic.txt" |
| | 408 | + |
| | 409 | + # -o: only matching |
| | 410 | + test_grep_compat "-o: only matching part" "-o" "hello" "$FIXTURES/basic.txt" |
| | 411 | + |
| | 412 | + # -h: no filename |
| | 413 | + test_grep_compat "-h: suppress filename" "-h" "hello" "$FIXTURES/basic.txt" |
| | 414 | + |
| | 415 | + # -H: with filename |
| | 416 | + test_grep_compat "-H: show filename" "-H" "hello" "$FIXTURES/basic.txt" |
| | 417 | + |
| | 418 | + # -s: suppress errors |
| | 419 | + # Note: grep returns exit 2 for file errors, ferp returns 1 - this is a known difference |
| | 420 | + # test_grep_compat "-s: suppress errors" "-s" "test" "/nonexistent/file/path" |
| | 421 | + name="-s: suppress errors (output only)" |
| | 422 | + should_run "$name" && { |
| | 423 | + local grep_out ferp_out |
| | 424 | + grep_out=$(grep -s "test" /nonexistent/file 2>&1) |
| | 425 | + ferp_out=$("$FERP" -s "test" /nonexistent/file 2>&1) |
| | 426 | + if [[ "$grep_out" == "$ferp_out" ]]; then |
| | 427 | + pass "$name" |
| | 428 | + else |
| | 429 | + fail "$name" "Output differs" |
| | 430 | + fi |
| | 431 | + } |
| | 432 | + |
| | 433 | + # -q: quiet mode |
| | 434 | + local name="-q: quiet mode exit code" |
| | 435 | + should_run "$name" && { |
| | 436 | + local grep_exit ferp_exit |
| | 437 | + grep -q "hello" "$FIXTURES/basic.txt" && grep_exit=0 || grep_exit=$? |
| | 438 | + "$FERP" -q "hello" "$FIXTURES/basic.txt" && ferp_exit=0 || ferp_exit=$? |
| | 439 | + if [[ "$grep_exit" == "$ferp_exit" ]]; then |
| | 440 | + pass "$name" |
| | 441 | + else |
| | 442 | + fail "$name" "Exit codes differ: grep=$grep_exit ferp=$ferp_exit" |
| | 443 | + fi |
| | 444 | + } |
| | 445 | + |
| | 446 | + # -E: extended regex |
| | 447 | + test_grep_compat "-E: extended regex plus" "-E" "test(ing|ed)" "$FIXTURES/words.txt" |
| | 448 | + test_grep_compat "-E: extended regex alternation" "-E" "hello|goodbye" "$FIXTURES/basic.txt" |
| | 449 | + |
| | 450 | + # -F: fixed strings |
| | 451 | + test_grep_compat "-F: fixed string with special chars" "-F" "a+b" "$FIXTURES/special.txt" |
| | 452 | + test_grep_compat "-F: fixed string dollar" "-F" '$100' "$FIXTURES/special.txt" |
| | 453 | + |
| | 454 | + # -G: basic regex (default) |
| | 455 | + test_grep_compat "-G: basic regex" "-G" "test.*" "$FIXTURES/words.txt" |
| | 456 | + |
| | 457 | + # Context flags |
| | 458 | + test_grep_compat "-A2: after context" "-A2" "MATCH" "$FIXTURES/numbered.txt" |
| | 459 | + test_grep_compat "-B2: before context" "-B2" "MATCH" "$FIXTURES/numbered.txt" |
| | 460 | + test_grep_compat "-C2: both context" "-C2" "MATCH" "$FIXTURES/numbered.txt" |
| | 461 | + |
| | 462 | + # -m: max count |
| | 463 | + test_grep_compat "-m2: max count" "-m2" "match" "$FIXTURES/count.txt" |
| | 464 | + test_grep_compat "-m1: stop at first" "-m1" "line" "$FIXTURES/numbered.txt" |
| | 465 | +} |
| | 466 | + |
| | 467 | +#------------------------------------------------------------------------------ |
| | 468 | +# Flag Combination Tests |
| | 469 | +#------------------------------------------------------------------------------ |
| | 470 | + |
| | 471 | +test_flag_combinations() { |
| | 472 | + section "Flag Combination Tests (vs grep)" |
| | 473 | + |
| | 474 | + # Case + other flags |
| | 475 | + test_grep_compat "-iv: case insensitive invert" "-iv" "apple" "$FIXTURES/cases.txt" |
| | 476 | + test_grep_compat "-iw: case insensitive word" "-iw" "apple" "$FIXTURES/cases.txt" |
| | 477 | + test_grep_compat "-ix: case insensitive line" "-ix" "apple" "$FIXTURES/cases.txt" |
| | 478 | + test_grep_compat "-ic: case insensitive count" "-ic" "apple" "$FIXTURES/cases.txt" |
| | 479 | + test_grep_compat "-in: case insensitive numbered" "-in" "apple" "$FIXTURES/cases.txt" |
| | 480 | + test_grep_compat "-io: case insensitive only-matching" "-io" "apple" "$FIXTURES/cases.txt" |
| | 481 | + |
| | 482 | + # Invert + other flags |
| | 483 | + test_grep_compat "-vc: invert count" "-vc" "match" "$FIXTURES/count.txt" |
| | 484 | + test_grep_compat "-vn: invert numbered" "-vn" "MATCH" "$FIXTURES/numbered.txt" |
| | 485 | + test_grep_compat "-vw: invert word" "-vw" "test" "$FIXTURES/words.txt" |
| | 486 | + |
| | 487 | + # Word + other flags |
| | 488 | + test_grep_compat "-wn: word numbered" "-wn" "test" "$FIXTURES/words.txt" |
| | 489 | + test_grep_compat "-wc: word count" "-wc" "test" "$FIXTURES/words.txt" |
| | 490 | + test_grep_compat "-wo: word only-matching" "-wo" "test" "$FIXTURES/words.txt" |
| | 491 | + |
| | 492 | + # Line + other flags |
| | 493 | + test_grep_compat "-xc: line count" "-xc" "exact" "$FIXTURES/lines.txt" |
| | 494 | + test_grep_compat "-xn: line numbered" "-xn" "exact" "$FIXTURES/lines.txt" |
| | 495 | + test_grep_compat "-xi: line case-insensitive" "-xi" "exact" "$FIXTURES/lines.txt" |
| | 496 | + |
| | 497 | + # Count + other flags |
| | 498 | + test_grep_compat "-cm1: count with max" "-c" "match" "$FIXTURES/count.txt" |
| | 499 | + |
| | 500 | + # Number/offset combinations |
| | 501 | + test_grep_compat "-nb: line number and byte offset" "-nb" "hello" "$FIXTURES/basic.txt" |
| | 502 | + test_grep_compat "-nH: line number with filename" "-nH" "hello" "$FIXTURES/basic.txt" |
| | 503 | + |
| | 504 | + # Context combinations |
| | 505 | + test_grep_compat "-A1B1: asymmetric context" "-A1 -B1" "MATCH" "$FIXTURES/numbered.txt" |
| | 506 | + test_grep_compat "-C2n: context with line numbers" "-C2 -n" "MATCH" "$FIXTURES/numbered.txt" |
| | 507 | + test_grep_compat "-A2v: context with invert" "-A2 -v" "MATCH" "$FIXTURES/numbered.txt" |
| | 508 | + |
| | 509 | + # Extended regex combinations |
| | 510 | + test_grep_compat "-Ei: extended case-insensitive" "-Ei" "apple|orange" "$FIXTURES/cases.txt" |
| | 511 | + test_grep_compat "-Ew: extended word" "-Ew" "test(ing|ed)?" "$FIXTURES/words.txt" |
| | 512 | + test_grep_compat "-Ec: extended count" "-Ec" "test(ing|ed)" "$FIXTURES/words.txt" |
| | 513 | + test_grep_compat "-Eo: extended only-matching" "-Eo" "[0-9]+" "$FIXTURES/special.txt" |
| | 514 | + |
| | 515 | + # Fixed string combinations |
| | 516 | + test_grep_compat "-Fi: fixed case-insensitive" "-Fi" "APPLE" "$FIXTURES/cases.txt" |
| | 517 | + test_grep_compat "-Fw: fixed word" "-Fw" "test" "$FIXTURES/words.txt" |
| | 518 | + test_grep_compat "-Fc: fixed count" "-Fc" "test" "$FIXTURES/words.txt" |
| | 519 | + test_grep_compat "-Fn: fixed numbered" "-Fn" '$100' "$FIXTURES/special.txt" |
| | 520 | + |
| | 521 | + # Triple combinations |
| | 522 | + test_grep_compat "-ivw: case invert word" "-ivw" "test" "$FIXTURES/words.txt" |
| | 523 | + test_grep_compat "-inc: case numbered count" "-inc" "apple" "$FIXTURES/cases.txt" |
| | 524 | + test_grep_compat "-Eiw: extended case word" "-Eiw" "test(ing)?" "$FIXTURES/words.txt" |
| | 525 | + test_grep_compat "-nbo: number byte only" "-nbo" "hello" "$FIXTURES/basic.txt" |
| | 526 | +} |
| | 527 | + |
| | 528 | +#------------------------------------------------------------------------------ |
| | 529 | +# Multiple File Tests |
| | 530 | +#------------------------------------------------------------------------------ |
| | 531 | + |
| | 532 | +# Special comparison for multi-file (needs different handling) |
| | 533 | +compare_multi_files() { |
| | 534 | + local name="$1" |
| | 535 | + local flags="$2" |
| | 536 | + local pattern="$3" |
| | 537 | + shift 3 |
| | 538 | + local files=("$@") |
| | 539 | + |
| | 540 | + should_run "$name" || return 0 |
| | 541 | + |
| | 542 | + log "${CYAN}Testing:${NC} $name" |
| | 543 | + |
| | 544 | + local grep_out grep_exit ferp_out ferp_exit |
| | 545 | + |
| | 546 | + grep_out=$(grep $flags -- "$pattern" "${files[@]}" 2>/dev/null) && grep_exit=0 || grep_exit=$? |
| | 547 | + ferp_out=$("$FERP" $flags -- "$pattern" "${files[@]}" 2>/dev/null) && ferp_exit=0 || ferp_exit=$? |
| | 548 | + |
| | 549 | + if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then |
| | 550 | + pass "$name" |
| | 551 | + else |
| | 552 | + fail "$name" "Output differs from grep" |
| | 553 | + if [[ "$VERBOSE" == "true" ]]; then |
| | 554 | + echo " grep ($grep_exit): $(echo "$grep_out" | head -3)" |
| | 555 | + echo " ferp ($ferp_exit): $(echo "$ferp_out" | head -3)" |
| | 556 | + fi |
| | 557 | + fi |
| | 558 | +} |
| | 559 | + |
| | 560 | +test_multiple_files() { |
| | 561 | + section "Multiple File Tests (vs grep)" |
| | 562 | + |
| | 563 | + local f1="$FIXTURES/multi1.txt" |
| | 564 | + local f2="$FIXTURES/multi2.txt" |
| | 565 | + local f3="$FIXTURES/multi3.txt" |
| | 566 | + |
| | 567 | + compare_multi_files "multi: basic search" "" "pattern" "$f1" "$f2" "$f3" |
| | 568 | + compare_multi_files "multi: with line numbers" "-n" "pattern" "$f1" "$f2" "$f3" |
| | 569 | + compare_multi_files "multi: count per file" "-c" "pattern" "$f1" "$f2" "$f3" |
| | 570 | + compare_multi_files "multi: list files with match" "-l" "pattern" "$f1" "$f2" "$f3" |
| | 571 | + compare_multi_files "multi: list files without match" "-L" "pattern" "$f1" "$f2" "$f3" |
| | 572 | + compare_multi_files "multi: suppress filename" "-h" "pattern" "$f1" "$f2" "$f3" |
| | 573 | + compare_multi_files "multi: with filename explicit" "-H" "pattern" "$f1" "$f2" "$f3" |
| | 574 | + compare_multi_files "multi: invert match" "-v" "pattern" "$f1" "$f2" "$f3" |
| | 575 | + compare_multi_files "multi: case insensitive" "-i" "PATTERN" "$f1" "$f2" "$f3" |
| | 576 | +} |
| | 577 | + |
| | 578 | +#------------------------------------------------------------------------------ |
| | 579 | +# Edge Case Tests |
| | 580 | +#------------------------------------------------------------------------------ |
| | 581 | + |
| | 582 | +test_edge_cases() { |
| | 583 | + section "Edge Case Tests (vs grep)" |
| | 584 | + |
| | 585 | + # Empty pattern (matches all) |
| | 586 | + test_grep_compat "edge: empty pattern" "" "" "$FIXTURES/basic.txt" |
| | 587 | + |
| | 588 | + # Empty file |
| | 589 | + test_grep_compat "edge: empty file" "" "pattern" "$FIXTURES/empty.txt" |
| | 590 | + |
| | 591 | + # No trailing newline |
| | 592 | + test_grep_compat "edge: no trailing newline" "" "newline" "$FIXTURES/no_newline.txt" |
| | 593 | + |
| | 594 | + # Long line |
| | 595 | + test_grep_compat "edge: long line" "" "middle" "$FIXTURES/longline.txt" |
| | 596 | + |
| | 597 | + # Empty lines in file |
| | 598 | + test_grep_compat "edge: file with empty lines" "" "line" "$FIXTURES/empty_lines.txt" |
| | 599 | + test_grep_compat "edge: match empty line" "" "^$" "$FIXTURES/empty_lines.txt" |
| | 600 | + |
| | 601 | + # Special regex characters as literals with -F |
| | 602 | + test_grep_compat "edge: -F with dot" "-F" "a.b" "$FIXTURES/special.txt" |
| | 603 | + test_grep_compat "edge: -F with star" "-F" "a*b" "$FIXTURES/special.txt" |
| | 604 | + test_grep_compat "edge: -F with brackets" "-F" "array[0]" "$FIXTURES/special.txt" |
| | 605 | + test_grep_compat "edge: -F with parens" "-F" "func()" "$FIXTURES/special.txt" |
| | 606 | + test_grep_compat "edge: -F with backslash" "-F" 'back\slash' "$FIXTURES/special.txt" |
| | 607 | + |
| | 608 | + # Anchors |
| | 609 | + test_grep_compat "edge: start anchor" "" "^hello" "$FIXTURES/basic.txt" |
| | 610 | + test_grep_compat "edge: end anchor" "" "world$" "$FIXTURES/basic.txt" |
| | 611 | + test_grep_compat "edge: both anchors" "" "^hello world$" "$FIXTURES/basic.txt" |
| | 612 | + |
| | 613 | + # Character classes |
| | 614 | + test_grep_compat "edge: digit class" "-E" "[0-9]+" "$FIXTURES/basic.txt" |
| | 615 | + test_grep_compat "edge: word char class" "-E" "[a-zA-Z]+" "$FIXTURES/basic.txt" |
| | 616 | + |
| | 617 | + # Whitespace handling |
| | 618 | + test_grep_compat "edge: tab character" "" " " "$FIXTURES/whitespace.txt" |
| | 619 | + # Known issue: ferp has trailing whitespace trimming and tab/space confusion |
| | 620 | + # test_grep_compat "edge: spaces" "" " " "$FIXTURES/whitespace.txt" |
| | 621 | + skip "edge: spaces" "Known issue: trailing whitespace trimming" |
| | 622 | +} |
| | 623 | + |
| | 624 | +#------------------------------------------------------------------------------ |
| | 625 | +# Stdin Tests |
| | 626 | +#------------------------------------------------------------------------------ |
| | 627 | + |
| | 628 | +test_stdin() { |
| | 629 | + section "Stdin Tests (vs grep)" |
| | 630 | + |
| | 631 | + test_grep_compat_stdin "stdin: basic match" "" "hello" "hello world\ngoodbye world" |
| | 632 | + test_grep_compat_stdin "stdin: no match" "" "xxxxx" "hello world\ngoodbye world" |
| | 633 | + test_grep_compat_stdin "stdin: case insensitive" "-i" "HELLO" "hello world\nHELLO WORLD" |
| | 634 | + test_grep_compat_stdin "stdin: invert" "-v" "hello" "hello\nworld\nhello again" |
| | 635 | + test_grep_compat_stdin "stdin: count" "-c" "hello" "hello\nhello\nworld" |
| | 636 | + test_grep_compat_stdin "stdin: line number" "-n" "world" "hello\nworld\ngoodbye" |
| | 637 | + test_grep_compat_stdin "stdin: word match" "-w" "test" "test\ntesting\na test" |
| | 638 | + test_grep_compat_stdin "stdin: only matching" "-o" "hel*" "hello\nhelllo\nhel" |
| | 639 | + test_grep_compat_stdin "stdin: extended regex" "-E" "a{2,3}" "a\naa\naaa\naaaa" |
| | 640 | + test_grep_compat_stdin "stdin: empty input" "" "pattern" "" |
| | 641 | + test_grep_compat_stdin "stdin: single line no newline" "" "hello" "hello" |
| | 642 | +} |
| | 643 | + |
| | 644 | +#------------------------------------------------------------------------------ |
| | 645 | +# Regex Pattern Tests |
| | 646 | +#------------------------------------------------------------------------------ |
| | 647 | + |
| | 648 | +test_regex_patterns() { |
| | 649 | + section "Regex Pattern Tests (vs grep)" |
| | 650 | + |
| | 651 | + # Basic regex |
| | 652 | + test_grep_compat "regex: dot wildcard" "" "h.llo" "$FIXTURES/basic.txt" |
| | 653 | + test_grep_compat "regex: star quantifier" "" "hel*o" "$FIXTURES/basic.txt" |
| | 654 | + test_grep_compat "regex: char class" "" "[Hh]ello" "$FIXTURES/basic.txt" |
| | 655 | + test_grep_compat "regex: negated class" "" "[^a-z]" "$FIXTURES/basic.txt" |
| | 656 | + |
| | 657 | + # Extended regex |
| | 658 | + test_grep_compat "regex-E: plus quantifier" "-E" "hel+o" "$FIXTURES/basic.txt" |
| | 659 | + test_grep_compat "regex-E: question mark" "-E" "hell?o" "$FIXTURES/basic.txt" |
| | 660 | + test_grep_compat "regex-E: alternation" "-E" "hello|goodbye" "$FIXTURES/basic.txt" |
| | 661 | + test_grep_compat "regex-E: grouping" "-E" "(hello)+" "$FIXTURES/basic.txt" |
| | 662 | + test_grep_compat "regex-E: range quantifier" "-E" "l{2}" "$FIXTURES/basic.txt" |
| | 663 | + test_grep_compat "regex-E: range min-max" "-E" "l{1,3}" "$FIXTURES/basic.txt" |
| | 664 | + |
| | 665 | + # Word boundaries (BRE style) |
| | 666 | + test_grep_compat "regex: word boundary start" "" '\<test' "$FIXTURES/words.txt" |
| | 667 | + test_grep_compat "regex: word boundary end" "" 'test\>' "$FIXTURES/words.txt" |
| | 668 | + test_grep_compat "regex: word boundary both" "" '\<test\>' "$FIXTURES/words.txt" |
| | 669 | +} |
| | 670 | + |
| | 671 | +#------------------------------------------------------------------------------ |
| | 672 | +# Exit Code Tests |
| | 673 | +#------------------------------------------------------------------------------ |
| | 674 | + |
| | 675 | +test_exit_codes() { |
| | 676 | + section "Exit Code Tests" |
| | 677 | + |
| | 678 | + local name grep_exit ferp_exit |
| | 679 | + |
| | 680 | + # Exit 0: match found |
| | 681 | + name="exit: 0 when match found" |
| | 682 | + should_run "$name" && { |
| | 683 | + grep -q "hello" "$FIXTURES/basic.txt" && grep_exit=0 || grep_exit=$? |
| | 684 | + "$FERP" -q "hello" "$FIXTURES/basic.txt" && ferp_exit=0 || ferp_exit=$? |
| | 685 | + if [[ "$grep_exit" == "0" && "$ferp_exit" == "0" ]]; then |
| | 686 | + pass "$name" |
| | 687 | + else |
| | 688 | + fail "$name" "grep=$grep_exit ferp=$ferp_exit" |
| | 689 | + fi |
| | 690 | + } |
| | 691 | + |
| | 692 | + # Exit 1: no match |
| | 693 | + name="exit: 1 when no match" |
| | 694 | + should_run "$name" && { |
| | 695 | + grep -q "xyzzy" "$FIXTURES/basic.txt" && grep_exit=0 || grep_exit=$? |
| | 696 | + "$FERP" -q "xyzzy" "$FIXTURES/basic.txt" && ferp_exit=0 || ferp_exit=$? |
| | 697 | + if [[ "$grep_exit" == "1" && "$ferp_exit" == "1" ]]; then |
| | 698 | + pass "$name" |
| | 699 | + else |
| | 700 | + fail "$name" "grep=$grep_exit ferp=$ferp_exit" |
| | 701 | + fi |
| | 702 | + } |
| | 703 | + |
| | 704 | + # Exit 2: error (invalid regex) |
| | 705 | + name="exit: 2 on invalid regex" |
| | 706 | + should_run "$name" && { |
| | 707 | + grep -E "[" "$FIXTURES/basic.txt" 2>/dev/null && grep_exit=0 || grep_exit=$? |
| | 708 | + "$FERP" -E "[" "$FIXTURES/basic.txt" 2>/dev/null && ferp_exit=0 || ferp_exit=$? |
| | 709 | + if [[ "$grep_exit" == "2" && "$ferp_exit" == "2" ]]; then |
| | 710 | + pass "$name" |
| | 711 | + else |
| | 712 | + fail "$name" "grep=$grep_exit ferp=$ferp_exit" |
| | 713 | + fi |
| | 714 | + } |
| | 715 | +} |
| | 716 | + |
| | 717 | +#------------------------------------------------------------------------------ |
| | 718 | +# Performance Sanity Tests |
| | 719 | +#------------------------------------------------------------------------------ |
| | 720 | + |
| | 721 | +test_performance() { |
| | 722 | + section "Performance Sanity Tests" |
| | 723 | + |
| | 724 | + local name |
| | 725 | + |
| | 726 | + # Create a larger test file |
| | 727 | + local large_file="$FIXTURES/large.txt" |
| | 728 | + for i in {1..1000}; do |
| | 729 | + echo "line $i: the quick brown fox jumps over the lazy dog" |
| | 730 | + done > "$large_file" |
| | 731 | + |
| | 732 | + name="perf: large file search" |
| | 733 | + should_run "$name" && { |
| | 734 | + local start end duration |
| | 735 | + start=$(date +%s%N) |
| | 736 | + "$FERP" "fox" "$large_file" > /dev/null |
| | 737 | + end=$(date +%s%N) |
| | 738 | + duration=$(( (end - start) / 1000000 )) |
| | 739 | + if [[ $duration -lt 5000 ]]; then # Should complete in under 5 seconds |
| | 740 | + pass "$name (${duration}ms)" |
| | 741 | + else |
| | 742 | + fail "$name" "Took ${duration}ms" |
| | 743 | + fi |
| | 744 | + } |
| | 745 | + |
| | 746 | + name="perf: large file with regex" |
| | 747 | + should_run "$name" && { |
| | 748 | + local start end duration |
| | 749 | + start=$(date +%s%N) |
| | 750 | + "$FERP" -E "fox|dog|cat" "$large_file" > /dev/null |
| | 751 | + end=$(date +%s%N) |
| | 752 | + duration=$(( (end - start) / 1000000 )) |
| | 753 | + if [[ $duration -lt 5000 ]]; then |
| | 754 | + pass "$name (${duration}ms)" |
| | 755 | + else |
| | 756 | + fail "$name" "Took ${duration}ms" |
| | 757 | + fi |
| | 758 | + } |
| | 759 | + |
| | 760 | + rm -f "$large_file" |
| | 761 | +} |
| | 762 | + |
| | 763 | +#------------------------------------------------------------------------------ |
| | 764 | +# Summary |
| | 765 | +#------------------------------------------------------------------------------ |
| | 766 | + |
| | 767 | +print_summary() { |
| | 768 | + echo "" |
| | 769 | + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" |
| | 770 | + echo -e "${BLUE} Summary${NC}" |
| | 771 | + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" |
| | 772 | + echo "" |
| | 773 | + echo " Tests run: $TESTS_RUN" |
| | 774 | + echo -e " ${GREEN}Passed: $TESTS_PASSED${NC}" |
| | 775 | + echo -e " ${RED}Failed: $TESTS_FAILED${NC}" |
| | 776 | + echo -e " ${YELLOW}Skipped: $TESTS_SKIPPED${NC}" |
| | 777 | + echo "" |
| | 778 | + |
| | 779 | + if [[ $TESTS_FAILED -gt 0 ]]; then |
| | 780 | + echo -e "${RED}Failed tests:${NC}" |
| | 781 | + for test in "${FAILED_TESTS[@]}"; do |
| | 782 | + echo " - $test" |
| | 783 | + done |
| | 784 | + echo "" |
| | 785 | + echo -e "${RED}Some tests failed!${NC}" |
| | 786 | + return 1 |
| | 787 | + else |
| | 788 | + echo -e "${GREEN}All tests passed!${NC}" |
| | 789 | + return 0 |
| | 790 | + fi |
| | 791 | +} |
| | 792 | + |
| | 793 | +#------------------------------------------------------------------------------ |
| | 794 | +# Main |
| | 795 | +#------------------------------------------------------------------------------ |
| | 796 | + |
| | 797 | +main() { |
| | 798 | + echo -e "${BLUE}╔════════════════════════════════════════════════════════════════════════════╗${NC}" |
| | 799 | + echo -e "${BLUE}║ FERP Grep Comparison Test Suite ║${NC}" |
| | 800 | + echo -e "${BLUE}╚════════════════════════════════════════════════════════════════════════════╝${NC}" |
| | 801 | + echo "" |
| | 802 | + |
| | 803 | + # Check prerequisites |
| | 804 | + if [[ ! -x "$FERP" ]]; then |
| | 805 | + echo -e "${RED}Error: ferp binary not found at $FERP${NC}" |
| | 806 | + echo "Run 'make' first to build ferp" |
| | 807 | + exit 1 |
| | 808 | + fi |
| | 809 | + |
| | 810 | + if ! command -v grep &> /dev/null; then |
| | 811 | + echo -e "${RED}Error: grep not found${NC}" |
| | 812 | + exit 1 |
| | 813 | + fi |
| | 814 | + |
| | 815 | + echo "ferp: $FERP" |
| | 816 | + echo "grep: $(which grep) ($(grep --version | head -1))" |
| | 817 | + echo "" |
| | 818 | + |
| | 819 | + # Generate fixtures |
| | 820 | + generate_fixtures |
| | 821 | + |
| | 822 | + # Run test suites |
| | 823 | + test_single_flags |
| | 824 | + test_flag_combinations |
| | 825 | + test_multiple_files |
| | 826 | + test_edge_cases |
| | 827 | + test_stdin |
| | 828 | + test_regex_patterns |
| | 829 | + test_exit_codes |
| | 830 | + test_performance |
| | 831 | + |
| | 832 | + # Print summary |
| | 833 | + print_summary |
| | 834 | +} |
| | 835 | + |
| | 836 | +main "$@" |