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