Bash · 55374 bytes Raw Blame History
1 #!/bin/bash
2 #
3 # FERP Integration Test Suite
4 # Comprehensive tests for all command-line flags
5 #
6 # Usage: ./integration_test.sh [--compare-grep] [--verbose] [--filter PATTERN]
7 #
8 # Options:
9 # --compare-grep Compare ferp output against GNU grep
10 # --verbose Show detailed output for each test
11 # --filter PATTERN Only run tests matching PATTERN
12 #
13
14 set -uo pipefail
15 # Note: not using -e because we want tests to continue on failure
16
17 # Colors for output
18 RED='\033[0;31m'
19 GREEN='\033[0;32m'
20 YELLOW='\033[1;33m'
21 BLUE='\033[0;34m'
22 NC='\033[0m' # No Color
23
24 # Test counters
25 TESTS_RUN=0
26 TESTS_PASSED=0
27 TESTS_FAILED=0
28 TESTS_SKIPPED=0
29
30 # Options
31 COMPARE_GREP=false
32 VERBOSE=false
33 FILTER=""
34
35 # Paths
36 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
37 FERP="${SCRIPT_DIR}/../ferp"
38 FIXTURES="${SCRIPT_DIR}/fixtures"
39
40 # Parse arguments
41 while [[ $# -gt 0 ]]; do
42 case $1 in
43 --compare-grep)
44 COMPARE_GREP=true
45 shift
46 ;;
47 --verbose)
48 VERBOSE=true
49 shift
50 ;;
51 --filter)
52 FILTER="$2"
53 shift 2
54 ;;
55 *)
56 echo "Unknown option: $1"
57 exit 1
58 ;;
59 esac
60 done
61
62 # Check ferp exists
63 if [[ ! -x "$FERP" ]]; then
64 echo -e "${RED}Error: ferp binary not found at $FERP${NC}"
65 echo "Run 'make' first to build ferp"
66 exit 1
67 fi
68
69 #------------------------------------------------------------------------------
70 # Test Framework Functions
71 #------------------------------------------------------------------------------
72
73 log_test() {
74 local name="$1"
75 if [[ "$VERBOSE" == "true" ]]; then
76 echo -e "${BLUE}Running:${NC} $name"
77 fi
78 }
79
80 pass() {
81 local name="$1"
82 ((TESTS_PASSED++))
83 ((TESTS_RUN++))
84 echo -e "${GREEN}PASS${NC}: $name"
85 }
86
87 fail() {
88 local name="$1"
89 local reason="${2:-}"
90 ((TESTS_FAILED++))
91 ((TESTS_RUN++))
92 echo -e "${RED}FAIL${NC}: $name"
93 if [[ -n "$reason" ]]; then
94 echo -e " ${YELLOW}Reason:${NC} $reason"
95 fi
96 }
97
98 skip() {
99 local name="$1"
100 local reason="${2:-}"
101 ((TESTS_SKIPPED++))
102 echo -e "${YELLOW}SKIP${NC}: $name${reason:+ ($reason)}"
103 }
104
105 should_run() {
106 local name="$1"
107 if [[ -n "$FILTER" && ! "$name" =~ $FILTER ]]; then
108 return 1
109 fi
110 return 0
111 }
112
113 # Run ferp and check exit code
114 # Usage: run_ferp_expect EXIT_CODE ARGS...
115 run_ferp_expect() {
116 local expected_exit="$1"
117 shift
118 local actual_exit=0
119 "$FERP" "$@" >/dev/null 2>&1 || actual_exit=$?
120 [[ "$actual_exit" -eq "$expected_exit" ]]
121 }
122
123 # Run ferp and capture output
124 # Usage: run_ferp ARGS...
125 run_ferp() {
126 "$FERP" "$@" 2>/dev/null || true
127 }
128
129 # Compare ferp output to expected string
130 # Usage: assert_output "expected" ferp_args...
131 assert_output() {
132 local expected="$1"
133 shift
134 local actual
135 actual=$("$FERP" "$@" 2>/dev/null) || true
136 [[ "$actual" == "$expected" ]]
137 }
138
139 # Compare ferp output to grep output
140 # Usage: assert_matches_grep ARGS...
141 assert_matches_grep() {
142 if [[ "$COMPARE_GREP" != "true" ]]; then
143 return 0
144 fi
145 local ferp_out grep_out
146 ferp_out=$("$FERP" "$@" 2>/dev/null) || true
147 grep_out=$(grep "$@" 2>/dev/null) || true
148 [[ "$ferp_out" == "$grep_out" ]]
149 }
150
151 # Count output lines
152 # Usage: assert_line_count EXPECTED ferp_args...
153 assert_line_count() {
154 local expected="$1"
155 shift
156 local actual
157 actual=$("$FERP" "$@" 2>/dev/null | wc -l) || true
158 [[ "$actual" -eq "$expected" ]]
159 }
160
161 # Check that output contains a string
162 # Usage: assert_contains "substring" ferp_args...
163 assert_contains() {
164 local substring="$1"
165 shift
166 local output
167 output=$("$FERP" "$@" 2>/dev/null) || true
168 [[ "$output" == *"$substring"* ]]
169 }
170
171 # Check that output does NOT contain a string
172 # Usage: assert_not_contains "substring" ferp_args...
173 assert_not_contains() {
174 local substring="$1"
175 shift
176 local output
177 output=$("$FERP" "$@" 2>/dev/null) || true
178 [[ "$output" != *"$substring"* ]]
179 }
180
181 #------------------------------------------------------------------------------
182 # Test Categories
183 #------------------------------------------------------------------------------
184
185 section() {
186 echo ""
187 echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
188 echo -e "${BLUE} $1${NC}"
189 echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
190 }
191
192 #------------------------------------------------------------------------------
193 # PATTERN TYPE TESTS (-E, -F, -G, -P)
194 #------------------------------------------------------------------------------
195
196 test_pattern_types() {
197 section "Pattern Type Tests (-E, -F, -G, -P)"
198 local name
199
200 # -G / --basic-regexp (BRE - default)
201 name="-G: BRE is default mode"
202 should_run "$name" && {
203 log_test "$name"
204 # "hello" should match lines containing "hello"
205 if assert_contains "hello world" "hello" "$FIXTURES/simple.txt"; then
206 pass "$name"
207 else
208 fail "$name"
209 fi
210 }
211
212 name="-G: BRE treats + as literal"
213 should_run "$name" && {
214 log_test "$name"
215 if assert_contains "a+b equals c" "a+b" "$FIXTURES/special_chars.txt"; then
216 pass "$name"
217 else
218 fail "$name"
219 fi
220 }
221
222 name="-G: BRE treats | as literal"
223 should_run "$name" && {
224 log_test "$name"
225 if assert_contains "pipe|character" "pipe|character" "$FIXTURES/special_chars.txt"; then
226 pass "$name"
227 else
228 fail "$name"
229 fi
230 }
231
232 name="-G: BRE escaped grouping \\(\\)"
233 should_run "$name" && {
234 log_test "$name"
235 if echo "abab" | "$FERP" '\(ab\)\{2\}' | grep -q "abab"; then
236 pass "$name"
237 else
238 fail "$name"
239 fi
240 }
241
242 # -E / --extended-regexp (ERE)
243 name="-E: ERE + quantifier"
244 should_run "$name" && {
245 log_test "$name"
246 if echo "helllo" | "$FERP" -E "hel+o" | grep -q "helllo"; then
247 pass "$name"
248 else
249 fail "$name"
250 fi
251 }
252
253 name="-E: ERE ? quantifier (match)"
254 should_run "$name" && {
255 log_test "$name"
256 if echo "helo" | "$FERP" -E "hel?o" | grep -q "helo"; then
257 pass "$name"
258 else
259 fail "$name"
260 fi
261 }
262
263 name="-E: ERE ? quantifier (zero match)"
264 should_run "$name" && {
265 log_test "$name"
266 if echo "heo" | "$FERP" -E "hel?o" | grep -q "heo"; then
267 pass "$name"
268 else
269 fail "$name"
270 fi
271 }
272
273 name="-E: ERE alternation |"
274 should_run "$name" && {
275 log_test "$name"
276 local out
277 out=$(printf "cat\ndog\nbird\n" | "$FERP" -E "cat|dog")
278 if [[ "$out" == $'cat\ndog' ]]; then
279 pass "$name"
280 else
281 fail "$name" "got: $out"
282 fi
283 }
284
285 name="-E: ERE unescaped grouping ()"
286 should_run "$name" && {
287 log_test "$name"
288 if echo "abab" | "$FERP" -E "(ab){2}" | grep -q "abab"; then
289 pass "$name"
290 else
291 fail "$name"
292 fi
293 }
294
295 name="--extended-regexp: long form works"
296 should_run "$name" && {
297 log_test "$name"
298 if echo "hello" | "$FERP" --extended-regexp "hel+" | grep -q "hello"; then
299 pass "$name"
300 else
301 fail "$name"
302 fi
303 }
304
305 # -F / --fixed-strings
306 name="-F: treats regex chars as literal"
307 should_run "$name" && {
308 log_test "$name"
309 if assert_contains "regex: foo.*bar" -F "foo.*bar" "$FIXTURES/special_chars.txt"; then
310 pass "$name"
311 else
312 fail "$name"
313 fi
314 }
315
316 name="-F: literal $ match"
317 should_run "$name" && {
318 log_test "$name"
319 if assert_contains 'price is $100' -F '$100' "$FIXTURES/special_chars.txt"; then
320 pass "$name"
321 else
322 fail "$name"
323 fi
324 }
325
326 name="-F: literal brackets match"
327 should_run "$name" && {
328 log_test "$name"
329 if assert_contains "brackets [test] here" -F "[test]" "$FIXTURES/special_chars.txt"; then
330 pass "$name"
331 else
332 fail "$name"
333 fi
334 }
335
336 name="--fixed-strings: long form works"
337 should_run "$name" && {
338 log_test "$name"
339 if assert_contains "a+b equals c" --fixed-strings "a+b" "$FIXTURES/special_chars.txt"; then
340 pass "$name"
341 else
342 fail "$name"
343 fi
344 }
345
346 # -P / --perl-regexp (PCRE)
347 name="-P: basic PCRE match"
348 should_run "$name" && {
349 log_test "$name"
350 if echo "hello world" | "$FERP" -P "hello" | grep -q "hello"; then
351 pass "$name"
352 else
353 fail "$name"
354 fi
355 }
356
357 name="-P: PCRE \\d digit class"
358 should_run "$name" && {
359 log_test "$name"
360 if echo "test123" | "$FERP" -P '\d+' | grep -q "test123"; then
361 pass "$name"
362 else
363 fail "$name"
364 fi
365 }
366
367 name="-P: PCRE \\w word class"
368 should_run "$name" && {
369 log_test "$name"
370 if echo "hello_world" | "$FERP" -P '\w+' | grep -q "hello_world"; then
371 pass "$name"
372 else
373 fail "$name"
374 fi
375 }
376
377 name="-P: PCRE positive lookahead (?=)"
378 should_run "$name" && {
379 log_test "$name"
380 if echo "foobar" | "$FERP" -P 'foo(?=bar)' | grep -q "foobar"; then
381 pass "$name"
382 else
383 fail "$name"
384 fi
385 }
386
387 name="-P: PCRE negative lookahead (?!)"
388 should_run "$name" && {
389 log_test "$name"
390 if echo "foobaz" | "$FERP" -P 'foo(?!bar)' | grep -q "foobaz"; then
391 pass "$name"
392 else
393 fail "$name"
394 fi
395 }
396
397 name="-P: PCRE positive lookbehind (?<=)"
398 should_run "$name" && {
399 log_test "$name"
400 if echo "foobar" | "$FERP" -P '(?<=foo)bar' | grep -q "foobar"; then
401 pass "$name"
402 else
403 fail "$name"
404 fi
405 }
406
407 name="-P: PCRE non-capturing group (?:)"
408 should_run "$name" && {
409 log_test "$name"
410 if echo "abcabc" | "$FERP" -P '(?:abc)+' | grep -q "abcabc"; then
411 pass "$name"
412 else
413 fail "$name"
414 fi
415 }
416 }
417
418 #------------------------------------------------------------------------------
419 # MATCHING CONTROL TESTS (-i, -v, -w, -x)
420 #------------------------------------------------------------------------------
421
422 test_matching_control() {
423 section "Matching Control Tests (-i, -v, -w, -x)"
424 local name
425
426 # -i / --ignore-case
427 name="-i: case insensitive match (lowercase pattern)"
428 should_run "$name" && {
429 log_test "$name"
430 local out
431 out=$("$FERP" -i "hello" "$FIXTURES/simple.txt" | wc -l)
432 if [[ "$out" -eq 4 ]]; then # hello, Hello, HELLO, line with hello
433 pass "$name"
434 else
435 fail "$name" "expected 4 lines, got $out"
436 fi
437 }
438
439 name="-i: case insensitive match (uppercase pattern)"
440 should_run "$name" && {
441 log_test "$name"
442 local out
443 out=$("$FERP" -i "HELLO" "$FIXTURES/simple.txt" | wc -l)
444 if [[ "$out" -eq 4 ]]; then
445 pass "$name"
446 else
447 fail "$name" "expected 4 lines, got $out"
448 fi
449 }
450
451 name="-i: case insensitive with -F"
452 should_run "$name" && {
453 log_test "$name"
454 local out
455 out=$("$FERP" -iF "HELLO" "$FIXTURES/simple.txt" | wc -l)
456 if [[ "$out" -eq 4 ]]; then
457 pass "$name"
458 else
459 fail "$name" "expected 4 lines, got $out"
460 fi
461 }
462
463 name="-i: case insensitive with -E"
464 should_run "$name" && {
465 log_test "$name"
466 local out
467 out=$("$FERP" -iE "hel+" "$FIXTURES/simple.txt" | wc -l)
468 if [[ "$out" -eq 4 ]]; then
469 pass "$name"
470 else
471 fail "$name" "expected 4 lines, got $out"
472 fi
473 }
474
475 name="--ignore-case: long form works"
476 should_run "$name" && {
477 log_test "$name"
478 local out
479 out=$("$FERP" --ignore-case "HELLO" "$FIXTURES/simple.txt" | wc -l)
480 if [[ "$out" -eq 4 ]]; then
481 pass "$name"
482 else
483 fail "$name"
484 fi
485 }
486
487 name="--no-ignore-case: disables case insensitivity"
488 should_run "$name" && {
489 log_test "$name"
490 local out
491 out=$("$FERP" -i --no-ignore-case "hello" "$FIXTURES/simple.txt" | wc -l)
492 if [[ "$out" -eq 2 ]]; then # only lowercase hello
493 pass "$name"
494 else
495 fail "$name" "expected 2 lines, got $out"
496 fi
497 }
498
499 # -v / --invert-match
500 name="-v: invert match"
501 should_run "$name" && {
502 log_test "$name"
503 if assert_not_contains "hello" -v "hello" "$FIXTURES/simple.txt"; then
504 pass "$name"
505 else
506 fail "$name"
507 fi
508 }
509
510 name="-v: inverted results count"
511 should_run "$name" && {
512 log_test "$name"
513 local total matching inverted
514 total=$("$FERP" "" "$FIXTURES/simple.txt" | wc -l)
515 matching=$("$FERP" "hello" "$FIXTURES/simple.txt" | wc -l)
516 inverted=$("$FERP" -v "hello" "$FIXTURES/simple.txt" | wc -l)
517 if [[ $((matching + inverted)) -eq "$total" ]]; then
518 pass "$name"
519 else
520 fail "$name" "matching($matching) + inverted($inverted) != total($total)"
521 fi
522 }
523
524 name="--invert-match: long form works"
525 should_run "$name" && {
526 log_test "$name"
527 if assert_not_contains "hello" --invert-match "hello" "$FIXTURES/simple.txt"; then
528 pass "$name"
529 else
530 fail "$name"
531 fi
532 }
533
534 # -w / --word-regexp
535 name="-w: matches whole word only"
536 should_run "$name" && {
537 log_test "$name"
538 local out
539 out=$("$FERP" -w "test" "$FIXTURES/words.txt")
540 # Should match "test" but not "testing", "tested", "tester"
541 if [[ "$out" == "test testing tested tester" ]]; then
542 pass "$name"
543 else
544 fail "$name" "got: $out"
545 fi
546 }
547
548 name="-w: does not match partial word"
549 should_run "$name" && {
550 log_test "$name"
551 local out
552 out=$("$FERP" -w "foo" "$FIXTURES/words.txt" | wc -l)
553 # "foobar foo bar foo-bar" - should match "foo" twice (standalone and in foo-bar)
554 if [[ "$out" -eq 1 ]]; then
555 pass "$name"
556 else
557 fail "$name" "expected 1 line, got $out"
558 fi
559 }
560
561 name="-w: word with -F"
562 should_run "$name" && {
563 log_test "$name"
564 local out
565 out=$("$FERP" -wF "hello" "$FIXTURES/words.txt")
566 if [[ "$out" == "hello helloworld worldhello" ]]; then
567 pass "$name"
568 else
569 fail "$name" "got: $out"
570 fi
571 }
572
573 name="--word-regexp: long form works"
574 should_run "$name" && {
575 log_test "$name"
576 local out
577 out=$("$FERP" --word-regexp "test" "$FIXTURES/words.txt")
578 if [[ "$out" == "test testing tested tester" ]]; then
579 pass "$name"
580 else
581 fail "$name"
582 fi
583 }
584
585 # -x / --line-regexp
586 name="-x: matches whole line only"
587 should_run "$name" && {
588 log_test "$name"
589 local out
590 out=$("$FERP" -x "hello world" "$FIXTURES/simple.txt")
591 if [[ "$out" == "hello world" ]]; then
592 pass "$name"
593 else
594 fail "$name" "got: $out"
595 fi
596 }
597
598 name="-x: does not match partial line"
599 should_run "$name" && {
600 log_test "$name"
601 if run_ferp_expect 1 -x "hello" "$FIXTURES/simple.txt"; then
602 pass "$name"
603 else
604 fail "$name"
605 fi
606 }
607
608 name="-x: with regex"
609 should_run "$name" && {
610 log_test "$name"
611 local out
612 out=$("$FERP" -xE "hello.*" "$FIXTURES/simple.txt")
613 # Should match lines starting with hello
614 if echo "$out" | grep -q "hello world"; then
615 pass "$name"
616 else
617 fail "$name"
618 fi
619 }
620
621 name="--line-regexp: long form works"
622 should_run "$name" && {
623 log_test "$name"
624 local out
625 out=$("$FERP" --line-regexp "line 5" "$FIXTURES/numbers.txt")
626 if [[ "$out" == "line 5" ]]; then
627 pass "$name"
628 else
629 fail "$name"
630 fi
631 }
632
633 # Combined flags
634 name="-iv: case insensitive inverted"
635 should_run "$name" && {
636 log_test "$name"
637 local out
638 out=$("$FERP" -iv "HELLO" "$FIXTURES/simple.txt" | wc -l)
639 # Should exclude all hello variants
640 if [[ "$out" -eq 4 ]]; then # 8 lines - 4 with hello
641 pass "$name"
642 else
643 fail "$name" "expected 4 lines, got $out"
644 fi
645 }
646
647 name="-iw: case insensitive word match"
648 should_run "$name" && {
649 log_test "$name"
650 local out
651 out=$(echo -e "TEST\nTesting\ntest" | "$FERP" -iw "test" | wc -l)
652 if [[ "$out" -eq 2 ]]; then # TEST and test, not Testing
653 pass "$name"
654 else
655 fail "$name" "expected 2, got $out"
656 fi
657 }
658 }
659
660 #------------------------------------------------------------------------------
661 # OUTPUT CONTROL TESTS (-c, -l, -L, -o, -q, -s)
662 #------------------------------------------------------------------------------
663
664 test_output_control() {
665 section "Output Control Tests (-c, -l, -L, -o, -q, -s)"
666 local name
667
668 # -c / --count
669 name="-c: count matching lines"
670 should_run "$name" && {
671 log_test "$name"
672 local out
673 out=$("$FERP" -c "hello" "$FIXTURES/simple.txt")
674 if [[ "$out" == "2" ]]; then
675 pass "$name"
676 else
677 fail "$name" "expected 2, got $out"
678 fi
679 }
680
681 name="-c: count with multiple files shows filename"
682 should_run "$name" && {
683 log_test "$name"
684 local out
685 out=$("$FERP" -c "match" "$FIXTURES/multifile1.txt" "$FIXTURES/multifile2.txt")
686 if echo "$out" | grep -q "multifile1.txt:2" && echo "$out" | grep -q "multifile2.txt:1"; then
687 pass "$name"
688 else
689 fail "$name" "got: $out"
690 fi
691 }
692
693 name="-c: zero count when no matches"
694 should_run "$name" && {
695 log_test "$name"
696 local out
697 out=$("$FERP" -c "nonexistent" "$FIXTURES/simple.txt")
698 if [[ "$out" == "0" ]]; then
699 pass "$name"
700 else
701 fail "$name" "expected 0, got $out"
702 fi
703 }
704
705 name="--count: long form works"
706 should_run "$name" && {
707 log_test "$name"
708 local out
709 out=$("$FERP" --count "line" "$FIXTURES/numbers.txt")
710 if [[ "$out" == "10" ]]; then
711 pass "$name"
712 else
713 fail "$name"
714 fi
715 }
716
717 # -l / --files-with-matches
718 name="-l: list files with matches"
719 should_run "$name" && {
720 log_test "$name"
721 local out
722 out=$("$FERP" -l "match" "$FIXTURES/multifile1.txt" "$FIXTURES/multifile2.txt" "$FIXTURES/multifile3.txt")
723 if echo "$out" | grep -q "multifile1.txt" && echo "$out" | grep -q "multifile2.txt" && ! echo "$out" | grep -q "multifile3.txt"; then
724 pass "$name"
725 else
726 fail "$name" "got: $out"
727 fi
728 }
729
730 name="-l: only prints filename once per file"
731 should_run "$name" && {
732 log_test "$name"
733 local out
734 out=$("$FERP" -l "match" "$FIXTURES/multifile1.txt" | wc -l)
735 if [[ "$out" -eq 1 ]]; then
736 pass "$name"
737 else
738 fail "$name" "expected 1 line, got $out"
739 fi
740 }
741
742 name="--files-with-matches: long form works"
743 should_run "$name" && {
744 log_test "$name"
745 local out
746 out=$("$FERP" --files-with-matches "match" "$FIXTURES/multifile1.txt")
747 if echo "$out" | grep -q "multifile1.txt"; then
748 pass "$name"
749 else
750 fail "$name"
751 fi
752 }
753
754 # -L / --files-without-match
755 name="-L: list files without matches"
756 should_run "$name" && {
757 log_test "$name"
758 local out
759 out=$("$FERP" -L "match" "$FIXTURES/multifile1.txt" "$FIXTURES/multifile2.txt" "$FIXTURES/multifile3.txt")
760 if echo "$out" | grep -q "multifile3.txt" && ! echo "$out" | grep -q "multifile1.txt"; then
761 pass "$name"
762 else
763 fail "$name" "got: $out"
764 fi
765 }
766
767 name="--files-without-match: long form works"
768 should_run "$name" && {
769 log_test "$name"
770 local out
771 out=$("$FERP" --files-without-match "match" "$FIXTURES/multifile3.txt")
772 if echo "$out" | grep -q "multifile3.txt"; then
773 pass "$name"
774 else
775 fail "$name"
776 fi
777 }
778
779 # -o / --only-matching
780 name="-o: show only matching part"
781 should_run "$name" && {
782 log_test "$name"
783 local out
784 out=$("$FERP" -o "hello" "$FIXTURES/simple.txt")
785 # Should output "hello" for each match, not the whole line
786 if [[ "$out" == $'hello\nhello' ]]; then
787 pass "$name"
788 else
789 fail "$name" "got: $out"
790 fi
791 }
792
793 name="-o: multiple matches per line"
794 should_run "$name" && {
795 log_test "$name"
796 local out
797 out=$(echo "hello world hello" | "$FERP" -o "hello" | wc -l)
798 if [[ "$out" -eq 2 ]]; then
799 pass "$name"
800 else
801 fail "$name" "expected 2 lines, got $out"
802 fi
803 }
804
805 name="-o: with regex captures match"
806 should_run "$name" && {
807 log_test "$name"
808 local out
809 out=$(echo "test123test456" | "$FERP" -oE "[0-9]+")
810 if [[ "$out" == $'123\n456' ]]; then
811 pass "$name"
812 else
813 fail "$name" "got: $out"
814 fi
815 }
816
817 name="--only-matching: long form works"
818 should_run "$name" && {
819 log_test "$name"
820 local out
821 out=$(echo "abc123def" | "$FERP" --only-matching -E "[0-9]+")
822 if [[ "$out" == "123" ]]; then
823 pass "$name"
824 else
825 fail "$name"
826 fi
827 }
828
829 # -q / --quiet / --silent
830 name="-q: no output on match"
831 should_run "$name" && {
832 log_test "$name"
833 local out
834 out=$("$FERP" -q "hello" "$FIXTURES/simple.txt")
835 if [[ -z "$out" ]]; then
836 pass "$name"
837 else
838 fail "$name" "expected empty output, got: $out"
839 fi
840 }
841
842 name="-q: exit 0 on match"
843 should_run "$name" && {
844 log_test "$name"
845 if run_ferp_expect 0 -q "hello" "$FIXTURES/simple.txt"; then
846 pass "$name"
847 else
848 fail "$name"
849 fi
850 }
851
852 name="-q: exit 1 on no match"
853 should_run "$name" && {
854 log_test "$name"
855 if run_ferp_expect 1 -q "nonexistent" "$FIXTURES/simple.txt"; then
856 pass "$name"
857 else
858 fail "$name"
859 fi
860 }
861
862 name="--quiet: long form works"
863 should_run "$name" && {
864 log_test "$name"
865 if run_ferp_expect 0 --quiet "hello" "$FIXTURES/simple.txt"; then
866 pass "$name"
867 else
868 fail "$name"
869 fi
870 }
871
872 name="--silent: alias works"
873 should_run "$name" && {
874 log_test "$name"
875 if run_ferp_expect 0 --silent "hello" "$FIXTURES/simple.txt"; then
876 pass "$name"
877 else
878 fail "$name"
879 fi
880 }
881
882 # -s / --no-messages
883 name="-s: suppress error messages"
884 should_run "$name" && {
885 log_test "$name"
886 local err
887 err=$("$FERP" -s "test" "/nonexistent/file" 2>&1) || true
888 if [[ -z "$err" ]]; then
889 pass "$name"
890 else
891 fail "$name" "expected no output, got: $err"
892 fi
893 }
894
895 name="--no-messages: long form works"
896 should_run "$name" && {
897 log_test "$name"
898 local err
899 err=$("$FERP" --no-messages "test" "/nonexistent/file" 2>&1) || true
900 if [[ -z "$err" ]]; then
901 pass "$name"
902 else
903 fail "$name"
904 fi
905 }
906 }
907
908 #------------------------------------------------------------------------------
909 # LINE PREFIX TESTS (-n, -b, -H, -h, -Z, -T)
910 #------------------------------------------------------------------------------
911
912 test_line_prefix() {
913 section "Line Prefix Tests (-n, -b, -H, -h, -Z, -T)"
914 local name
915
916 # -n / --line-number
917 name="-n: show line numbers"
918 should_run "$name" && {
919 log_test "$name"
920 local out
921 out=$("$FERP" -n "line 5" "$FIXTURES/numbers.txt")
922 if [[ "$out" == "5:line 5" ]]; then
923 pass "$name"
924 else
925 fail "$name" "got: $out"
926 fi
927 }
928
929 name="-n: line numbers start at 1"
930 should_run "$name" && {
931 log_test "$name"
932 local out
933 # Use -x for exact line match to avoid "line 10" also matching
934 out=$("$FERP" -nx "line 1" "$FIXTURES/numbers.txt")
935 if [[ "$out" == "1:line 1" ]]; then
936 pass "$name"
937 else
938 fail "$name" "got: $out"
939 fi
940 }
941
942 name="--line-number: long form works"
943 should_run "$name" && {
944 log_test "$name"
945 local out
946 out=$("$FERP" --line-number "line 3" "$FIXTURES/numbers.txt")
947 if [[ "$out" == "3:line 3" ]]; then
948 pass "$name"
949 else
950 fail "$name"
951 fi
952 }
953
954 # -b / --byte-offset
955 name="-b: show byte offset"
956 should_run "$name" && {
957 log_test "$name"
958 local out
959 # Use -x for exact line match to avoid "line 10" also matching
960 out=$("$FERP" -bx "line 1" "$FIXTURES/numbers.txt")
961 if [[ "$out" == "0:line 1" ]]; then
962 pass "$name"
963 else
964 fail "$name" "got: $out"
965 fi
966 }
967
968 name="-b: byte offset increases with lines"
969 should_run "$name" && {
970 log_test "$name"
971 local out
972 out=$("$FERP" -b "line 2" "$FIXTURES/numbers.txt")
973 # "line 1\n" is 7 bytes, so line 2 starts at 7
974 if [[ "$out" == "7:line 2" ]]; then
975 pass "$name"
976 else
977 fail "$name" "expected 7:line 2, got: $out"
978 fi
979 }
980
981 name="-nb: line number and byte offset together"
982 should_run "$name" && {
983 log_test "$name"
984 local out
985 out=$("$FERP" -nb "line 2" "$FIXTURES/numbers.txt")
986 if [[ "$out" == "2:7:line 2" ]]; then
987 pass "$name"
988 else
989 fail "$name" "got: $out"
990 fi
991 }
992
993 name="--byte-offset: long form works"
994 should_run "$name" && {
995 log_test "$name"
996 local out
997 # Use -x for exact line match
998 out=$("$FERP" --byte-offset -x "line 1" "$FIXTURES/numbers.txt")
999 if [[ "$out" == "0:line 1" ]]; then
1000 pass "$name"
1001 else
1002 fail "$name"
1003 fi
1004 }
1005
1006 # -H / --with-filename
1007 name="-H: show filename on single file"
1008 should_run "$name" && {
1009 log_test "$name"
1010 local out
1011 out=$("$FERP" -H "hello" "$FIXTURES/simple.txt" | head -1)
1012 if echo "$out" | grep -q "simple.txt:"; then
1013 pass "$name"
1014 else
1015 fail "$name" "got: $out"
1016 fi
1017 }
1018
1019 name="--with-filename: long form works"
1020 should_run "$name" && {
1021 log_test "$name"
1022 local out
1023 out=$("$FERP" --with-filename "hello" "$FIXTURES/simple.txt" | head -1)
1024 if echo "$out" | grep -q "simple.txt:"; then
1025 pass "$name"
1026 else
1027 fail "$name"
1028 fi
1029 }
1030
1031 # -h / --no-filename
1032 name="-h: hide filename on multiple files"
1033 should_run "$name" && {
1034 log_test "$name"
1035 local out
1036 out=$("$FERP" -h "match" "$FIXTURES/multifile1.txt" "$FIXTURES/multifile2.txt" | head -1)
1037 if ! echo "$out" | grep -q ":"; then
1038 pass "$name"
1039 else
1040 fail "$name" "got: $out"
1041 fi
1042 }
1043
1044 name="--no-filename: long form works"
1045 should_run "$name" && {
1046 log_test "$name"
1047 local out
1048 out=$("$FERP" --no-filename "match" "$FIXTURES/multifile1.txt" "$FIXTURES/multifile2.txt" | head -1)
1049 if ! echo "$out" | grep -q ":"; then
1050 pass "$name"
1051 else
1052 fail "$name"
1053 fi
1054 }
1055
1056 # -Z / --null (null byte after filename)
1057 name="-Z: null byte after filename"
1058 should_run "$name" && {
1059 log_test "$name"
1060 local out
1061 # Check that null byte exists between filename and content
1062 out=$("$FERP" -HZ "hello" "$FIXTURES/simple.txt" | head -1 | od -c)
1063 if echo "$out" | grep -q '\\0'; then
1064 pass "$name"
1065 else
1066 fail "$name" "expected null byte, got: $out"
1067 fi
1068 }
1069
1070 name="--null: long form works"
1071 should_run "$name" && {
1072 log_test "$name"
1073 local out
1074 # Check that null byte exists between filename and content
1075 out=$("$FERP" -H --null "hello" "$FIXTURES/simple.txt" | head -1 | od -c)
1076 if echo "$out" | grep -q '\\0'; then
1077 pass "$name"
1078 else
1079 fail "$name"
1080 fi
1081 }
1082
1083 # -T / --initial-tab
1084 name="-T: initial tab for alignment"
1085 should_run "$name" && {
1086 log_test "$name"
1087 local out
1088 out=$("$FERP" -nT "hello" "$FIXTURES/simple.txt" | head -1)
1089 # Should have tab between prefix and content
1090 if echo "$out" | grep -qP '^\d+:\t'; then
1091 pass "$name"
1092 else
1093 fail "$name" "got: $out"
1094 fi
1095 }
1096
1097 name="--initial-tab: long form works"
1098 should_run "$name" && {
1099 log_test "$name"
1100 local out
1101 out=$("$FERP" -n --initial-tab "hello" "$FIXTURES/simple.txt" | head -1)
1102 if echo "$out" | grep -qP '^\d+:\t'; then
1103 pass "$name"
1104 else
1105 fail "$name"
1106 fi
1107 }
1108 }
1109
1110 #------------------------------------------------------------------------------
1111 # CONTEXT CONTROL TESTS (-A, -B, -C, -NUM)
1112 #------------------------------------------------------------------------------
1113
1114 test_context_control() {
1115 section "Context Control Tests (-A, -B, -C, -NUM)"
1116 local name
1117
1118 # -A / --after-context
1119 name="-A: after context lines"
1120 should_run "$name" && {
1121 log_test "$name"
1122 local out
1123 out=$("$FERP" -A2 "MATCH LINE" "$FIXTURES/context.txt" | wc -l)
1124 if [[ "$out" -eq 3 ]]; then # match + 2 after
1125 pass "$name"
1126 else
1127 fail "$name" "expected 3 lines, got $out"
1128 fi
1129 }
1130
1131 name="-A: after context content"
1132 should_run "$name" && {
1133 log_test "$name"
1134 local out
1135 out=$("$FERP" -A2 "MATCH LINE" "$FIXTURES/context.txt")
1136 if echo "$out" | grep -q "after 1" && echo "$out" | grep -q "after 2"; then
1137 pass "$name"
1138 else
1139 fail "$name" "got: $out"
1140 fi
1141 }
1142
1143 name="--after-context: long form with ="
1144 should_run "$name" && {
1145 log_test "$name"
1146 local out
1147 out=$("$FERP" --after-context=1 "MATCH LINE" "$FIXTURES/context.txt" | wc -l)
1148 if [[ "$out" -eq 2 ]]; then
1149 pass "$name"
1150 else
1151 fail "$name"
1152 fi
1153 }
1154
1155 # -B / --before-context
1156 name="-B: before context lines"
1157 should_run "$name" && {
1158 log_test "$name"
1159 local out
1160 out=$("$FERP" -B2 "MATCH LINE" "$FIXTURES/context.txt" | wc -l)
1161 if [[ "$out" -eq 3 ]]; then # 2 before + match
1162 pass "$name"
1163 else
1164 fail "$name" "expected 3 lines, got $out"
1165 fi
1166 }
1167
1168 name="-B: before context content"
1169 should_run "$name" && {
1170 log_test "$name"
1171 local out
1172 out=$("$FERP" -B2 "MATCH LINE" "$FIXTURES/context.txt")
1173 if echo "$out" | grep -q "before 2" && echo "$out" | grep -q "before 3"; then
1174 pass "$name"
1175 else
1176 fail "$name" "got: $out"
1177 fi
1178 }
1179
1180 name="--before-context: long form with ="
1181 should_run "$name" && {
1182 log_test "$name"
1183 local out
1184 out=$("$FERP" --before-context=1 "MATCH LINE" "$FIXTURES/context.txt" | wc -l)
1185 if [[ "$out" -eq 2 ]]; then
1186 pass "$name"
1187 else
1188 fail "$name"
1189 fi
1190 }
1191
1192 # -C / --context
1193 name="-C: both context lines"
1194 should_run "$name" && {
1195 log_test "$name"
1196 local out
1197 out=$("$FERP" -C2 "MATCH LINE" "$FIXTURES/context.txt" | wc -l)
1198 if [[ "$out" -eq 5 ]]; then # 2 before + match + 2 after
1199 pass "$name"
1200 else
1201 fail "$name" "expected 5 lines, got $out"
1202 fi
1203 }
1204
1205 name="--context: long form with ="
1206 should_run "$name" && {
1207 log_test "$name"
1208 local out
1209 out=$("$FERP" --context=1 "MATCH LINE" "$FIXTURES/context.txt" | wc -l)
1210 if [[ "$out" -eq 3 ]]; then
1211 pass "$name"
1212 else
1213 fail "$name"
1214 fi
1215 }
1216
1217 # -NUM shorthand
1218 name="-NUM: numeric context shorthand"
1219 should_run "$name" && {
1220 log_test "$name"
1221 local out
1222 out=$("$FERP" -2 "MATCH LINE" "$FIXTURES/context.txt" | wc -l)
1223 if [[ "$out" -eq 5 ]]; then # Same as -C2
1224 pass "$name"
1225 else
1226 fail "$name" "expected 5 lines, got $out"
1227 fi
1228 }
1229
1230 name="-3: larger context"
1231 should_run "$name" && {
1232 log_test "$name"
1233 local out
1234 out=$("$FERP" -3 "MATCH LINE" "$FIXTURES/context.txt" | wc -l)
1235 if [[ "$out" -eq 7 ]]; then # 3 before + match + 3 after
1236 pass "$name"
1237 else
1238 fail "$name" "expected 7 lines, got $out"
1239 fi
1240 }
1241
1242 # Context with multiple matches
1243 name="context: overlapping context groups"
1244 should_run "$name" && {
1245 log_test "$name"
1246 local out
1247 # Both MATCH and ANOTHER MATCH with context should not duplicate lines
1248 out=$("$FERP" -C2 "MATCH" "$FIXTURES/context.txt" | wc -l)
1249 # Should merge overlapping contexts appropriately
1250 if [[ "$out" -ge 8 ]]; then
1251 pass "$name"
1252 else
1253 fail "$name" "expected at least 8 lines, got $out"
1254 fi
1255 }
1256
1257 # --group-separator
1258 name="--group-separator: custom separator"
1259 should_run "$name" && {
1260 log_test "$name"
1261 local out
1262 out=$("$FERP" -C1 --group-separator="===" "MATCH" "$FIXTURES/context.txt")
1263 if echo "$out" | grep -q "==="; then
1264 pass "$name"
1265 else
1266 fail "$name" "got: $out"
1267 fi
1268 }
1269
1270 # --no-group-separator
1271 name="--no-group-separator: suppress separator"
1272 should_run "$name" && {
1273 log_test "$name"
1274 local out
1275 out=$("$FERP" -C1 --no-group-separator "MATCH" "$FIXTURES/context.txt")
1276 if ! echo "$out" | grep -q "^--$"; then
1277 pass "$name"
1278 else
1279 fail "$name" "separator should be suppressed, got: $out"
1280 fi
1281 }
1282
1283 # Context with line numbers
1284 name="-n with context: context lines marked differently"
1285 should_run "$name" && {
1286 log_test "$name"
1287 local out
1288 out=$("$FERP" -nB1 "MATCH LINE" "$FIXTURES/context.txt")
1289 # Context lines should use - separator, match lines use :
1290 if echo "$out" | grep -qE "^[0-9]+-" && echo "$out" | grep -qE "^[0-9]+:"; then
1291 pass "$name"
1292 else
1293 fail "$name" "got: $out"
1294 fi
1295 }
1296 }
1297
1298 #------------------------------------------------------------------------------
1299 # FILE SELECTION TESTS (-r, -R, --include, --exclude)
1300 #------------------------------------------------------------------------------
1301
1302 test_file_selection() {
1303 section "File Selection Tests (-r, -R, --include, --exclude, -d, -D)"
1304 local name
1305
1306 # -r / --recursive
1307 name="-r: recursive search"
1308 should_run "$name" && {
1309 log_test "$name"
1310 local out
1311 out=$("$FERP" -r "match" "$FIXTURES" | wc -l)
1312 # Should find matches in nested files
1313 if [[ "$out" -ge 4 ]]; then
1314 pass "$name"
1315 else
1316 fail "$name" "expected at least 4 matches, got $out"
1317 fi
1318 }
1319
1320 name="-r: searches subdirectories"
1321 should_run "$name" && {
1322 log_test "$name"
1323 local out
1324 out=$("$FERP" -r "nested" "$FIXTURES")
1325 if echo "$out" | grep -q "subdir1" && echo "$out" | grep -q "subdir2"; then
1326 pass "$name"
1327 else
1328 fail "$name" "got: $out"
1329 fi
1330 }
1331
1332 name="--recursive: long form works"
1333 should_run "$name" && {
1334 log_test "$name"
1335 local out
1336 out=$("$FERP" --recursive "match" "$FIXTURES" | wc -l)
1337 if [[ "$out" -ge 4 ]]; then
1338 pass "$name"
1339 else
1340 fail "$name"
1341 fi
1342 }
1343
1344 # --include
1345 name="--include: filter by glob"
1346 should_run "$name" && {
1347 log_test "$name"
1348 local out
1349 out=$("$FERP" -r --include="*.txt" "match" "$FIXTURES")
1350 # Should only show .txt files
1351 if echo "$out" | grep -q ".txt:" && ! echo "$out" | grep -q ".f90:"; then
1352 pass "$name"
1353 else
1354 fail "$name" "got: $out"
1355 fi
1356 }
1357
1358 name="--include: *.f90 filter"
1359 should_run "$name" && {
1360 log_test "$name"
1361 local out
1362 out=$("$FERP" -r --include="*.f90" "match" "$FIXTURES")
1363 if echo "$out" | grep -q ".f90:"; then
1364 pass "$name"
1365 else
1366 fail "$name" "got: $out"
1367 fi
1368 }
1369
1370 # --exclude
1371 name="--exclude: skip matching files"
1372 should_run "$name" && {
1373 log_test "$name"
1374 local out
1375 out=$("$FERP" -r --exclude="*multifile*" "match" "$FIXTURES")
1376 if ! echo "$out" | grep -q "multifile"; then
1377 pass "$name"
1378 else
1379 fail "$name" "got: $out"
1380 fi
1381 }
1382
1383 # --exclude-dir
1384 name="--exclude-dir: skip directories"
1385 should_run "$name" && {
1386 log_test "$name"
1387 local out
1388 out=$("$FERP" -r --exclude-dir="skipme" "match" "$FIXTURES")
1389 if ! echo "$out" | grep -q "skipme"; then
1390 pass "$name"
1391 else
1392 fail "$name" "got: $out"
1393 fi
1394 }
1395
1396 name="--exclude-dir: multiple exclusions"
1397 should_run "$name" && {
1398 log_test "$name"
1399 local out
1400 out=$("$FERP" -r --exclude-dir="skipme" --exclude-dir="subdir1" "match" "$FIXTURES")
1401 if ! echo "$out" | grep -q "skipme" && ! echo "$out" | grep -q "subdir1"; then
1402 pass "$name"
1403 else
1404 fail "$name" "got: $out"
1405 fi
1406 }
1407
1408 # -d / --directories
1409 name="-d skip: skip directories"
1410 should_run "$name" && {
1411 log_test "$name"
1412 # When given a directory without -r, -d skip should skip it silently
1413 local exit_code=0
1414 "$FERP" -d skip "test" "$FIXTURES" >/dev/null 2>&1 || exit_code=$?
1415 if [[ "$exit_code" -eq 1 ]]; then # No matches since dir was skipped
1416 pass "$name"
1417 else
1418 fail "$name" "expected exit 1, got $exit_code"
1419 fi
1420 }
1421
1422 name="--directories=recurse: same as -r"
1423 should_run "$name" && {
1424 log_test "$name"
1425 local out
1426 out=$("$FERP" --directories=recurse "nested" "$FIXTURES" | wc -l)
1427 if [[ "$out" -ge 2 ]]; then
1428 pass "$name"
1429 else
1430 fail "$name"
1431 fi
1432 }
1433 }
1434
1435 #------------------------------------------------------------------------------
1436 # MULTI-PATTERN TESTS (-e, -f)
1437 #------------------------------------------------------------------------------
1438
1439 test_multi_pattern() {
1440 section "Multi-Pattern Tests (-e, -f)"
1441 local name
1442
1443 # -e / --regexp
1444 name="-e: single explicit pattern"
1445 should_run "$name" && {
1446 log_test "$name"
1447 local out
1448 out=$("$FERP" -e "hello" "$FIXTURES/simple.txt" | wc -l)
1449 if [[ "$out" -eq 2 ]]; then
1450 pass "$name"
1451 else
1452 fail "$name" "expected 2 lines, got $out"
1453 fi
1454 }
1455
1456 name="-e: multiple patterns (OR)"
1457 should_run "$name" && {
1458 log_test "$name"
1459 local out
1460 out=$("$FERP" -e "hello" -e "goodbye" "$FIXTURES/simple.txt")
1461 if echo "$out" | grep -q "hello" && echo "$out" | grep -q "goodbye"; then
1462 pass "$name"
1463 else
1464 fail "$name" "got: $out"
1465 fi
1466 }
1467
1468 name="-e: three patterns"
1469 should_run "$name" && {
1470 log_test "$name"
1471 local out
1472 out=$("$FERP" -e "hello" -e "goodbye" -e "foo" "$FIXTURES/simple.txt" | wc -l)
1473 if [[ "$out" -eq 4 ]]; then # 2 hello + 1 goodbye + 1 foo
1474 pass "$name"
1475 else
1476 fail "$name" "expected 4 lines, got $out"
1477 fi
1478 }
1479
1480 name="--regexp=PATTERN: long form with ="
1481 should_run "$name" && {
1482 log_test "$name"
1483 local out
1484 out=$("$FERP" --regexp="hello" "$FIXTURES/simple.txt" | wc -l)
1485 if [[ "$out" -eq 2 ]]; then
1486 pass "$name"
1487 else
1488 fail "$name"
1489 fi
1490 }
1491
1492 # -f / --file
1493 name="-f: patterns from file"
1494 should_run "$name" && {
1495 log_test "$name"
1496 local out
1497 out=$("$FERP" -f "$FIXTURES/patterns.txt" "$FIXTURES/simple.txt")
1498 # patterns.txt contains: hello, world, test
1499 if echo "$out" | grep -q "hello" && echo "$out" | grep -q "world"; then
1500 pass "$name"
1501 else
1502 fail "$name" "got: $out"
1503 fi
1504 }
1505
1506 name="-f: combined with -e"
1507 should_run "$name" && {
1508 log_test "$name"
1509 local out
1510 out=$("$FERP" -f "$FIXTURES/patterns.txt" -e "foo" "$FIXTURES/simple.txt")
1511 if echo "$out" | grep -q "hello" && echo "$out" | grep -q "foo"; then
1512 pass "$name"
1513 else
1514 fail "$name" "got: $out"
1515 fi
1516 }
1517
1518 name="-f: nonexistent file error"
1519 should_run "$name" && {
1520 log_test "$name"
1521 if run_ferp_expect 2 -f "/nonexistent/patterns.txt" "test" "$FIXTURES/simple.txt"; then
1522 pass "$name"
1523 else
1524 fail "$name"
1525 fi
1526 }
1527 }
1528
1529 #------------------------------------------------------------------------------
1530 # BINARY FILE HANDLING TESTS (-a, -I, --binary-files)
1531 #------------------------------------------------------------------------------
1532
1533 test_binary_handling() {
1534 section "Binary File Handling Tests (-a, -I, --binary-files)"
1535 local name
1536
1537 # Default binary detection
1538 name="binary: detected and shows message"
1539 should_run "$name" && {
1540 log_test "$name"
1541 local out
1542 out=$("$FERP" "text" "$FIXTURES/binary.bin" 2>/dev/null) || true
1543 if echo "$out" | grep -q "Binary file"; then
1544 pass "$name"
1545 else
1546 fail "$name" "got: $out"
1547 fi
1548 }
1549
1550 # -a / --text
1551 name="-a: treat binary as text"
1552 should_run "$name" && {
1553 log_test "$name"
1554 local out
1555 out=$("$FERP" -a "match" "$FIXTURES/binary.bin" 2>/dev/null) || true
1556 if echo "$out" | grep -q "match"; then
1557 pass "$name"
1558 else
1559 fail "$name" "got: $out"
1560 fi
1561 }
1562
1563 name="--text: long form works"
1564 should_run "$name" && {
1565 log_test "$name"
1566 local out
1567 out=$("$FERP" --text "match" "$FIXTURES/binary.bin" 2>/dev/null) || true
1568 if echo "$out" | grep -q "match"; then
1569 pass "$name"
1570 else
1571 fail "$name"
1572 fi
1573 }
1574
1575 # -I (ignore binary / without-match)
1576 name="-I: skip binary files"
1577 should_run "$name" && {
1578 log_test "$name"
1579 if run_ferp_expect 1 -I "text" "$FIXTURES/binary.bin"; then
1580 pass "$name"
1581 else
1582 fail "$name"
1583 fi
1584 }
1585
1586 # --binary-files
1587 name="--binary-files=text: same as -a"
1588 should_run "$name" && {
1589 log_test "$name"
1590 local out
1591 out=$("$FERP" --binary-files=text "match" "$FIXTURES/binary.bin" 2>/dev/null) || true
1592 if echo "$out" | grep -q "match"; then
1593 pass "$name"
1594 else
1595 fail "$name"
1596 fi
1597 }
1598
1599 name="--binary-files=without-match: same as -I"
1600 should_run "$name" && {
1601 log_test "$name"
1602 if run_ferp_expect 1 --binary-files=without-match "text" "$FIXTURES/binary.bin"; then
1603 pass "$name"
1604 else
1605 fail "$name"
1606 fi
1607 }
1608
1609 name="--binary-files=binary: show message (default)"
1610 should_run "$name" && {
1611 log_test "$name"
1612 local out
1613 out=$("$FERP" --binary-files=binary "text" "$FIXTURES/binary.bin" 2>/dev/null) || true
1614 if echo "$out" | grep -q "Binary file"; then
1615 pass "$name"
1616 else
1617 fail "$name"
1618 fi
1619 }
1620 }
1621
1622 #------------------------------------------------------------------------------
1623 # MISCELLANEOUS TESTS (--max-count, --label, -z, --color)
1624 #------------------------------------------------------------------------------
1625
1626 test_misc_flags() {
1627 section "Miscellaneous Flag Tests (-m, --label, -z, --color)"
1628 local name
1629
1630 # -m / --max-count
1631 name="-m: limit match count"
1632 should_run "$name" && {
1633 log_test "$name"
1634 local out
1635 out=$("$FERP" -m2 "line" "$FIXTURES/numbers.txt" | wc -l)
1636 if [[ "$out" -eq 2 ]]; then
1637 pass "$name"
1638 else
1639 fail "$name" "expected 2 lines, got $out"
1640 fi
1641 }
1642
1643 name="-m1: stop after first match"
1644 should_run "$name" && {
1645 log_test "$name"
1646 local out
1647 out=$("$FERP" -m1 "line" "$FIXTURES/numbers.txt")
1648 if [[ "$out" == "line 1" ]]; then
1649 pass "$name"
1650 else
1651 fail "$name" "got: $out"
1652 fi
1653 }
1654
1655 name="--max-count=N: long form"
1656 should_run "$name" && {
1657 log_test "$name"
1658 local out
1659 out=$("$FERP" --max-count=3 "line" "$FIXTURES/numbers.txt" | wc -l)
1660 if [[ "$out" -eq 3 ]]; then
1661 pass "$name"
1662 else
1663 fail "$name"
1664 fi
1665 }
1666
1667 name="-m with -c: count respects limit"
1668 should_run "$name" && {
1669 log_test "$name"
1670 local out
1671 out=$("$FERP" -m5 -c "line" "$FIXTURES/numbers.txt")
1672 if [[ "$out" == "5" ]]; then
1673 pass "$name"
1674 else
1675 fail "$name" "got: $out"
1676 fi
1677 }
1678
1679 # --label
1680 name="--label: custom stdin label"
1681 should_run "$name" && {
1682 log_test "$name"
1683 local out
1684 out=$(echo "hello world" | "$FERP" -H --label="custom_input" "hello")
1685 if echo "$out" | grep -q "custom_input:"; then
1686 pass "$name"
1687 else
1688 fail "$name" "got: $out"
1689 fi
1690 }
1691
1692 # -z / --null-data
1693 name="-z: null-terminated input"
1694 should_run "$name" && {
1695 log_test "$name"
1696 local out
1697 out=$(printf 'hello\0world\0' | "$FERP" -z "hello")
1698 if [[ -n "$out" ]]; then
1699 pass "$name"
1700 else
1701 fail "$name"
1702 fi
1703 }
1704
1705 name="--null-data: long form works"
1706 should_run "$name" && {
1707 log_test "$name"
1708 local out
1709 out=$(printf 'test\0line\0' | "$FERP" --null-data "test")
1710 if [[ -n "$out" ]]; then
1711 pass "$name"
1712 else
1713 fail "$name"
1714 fi
1715 }
1716
1717 # --color
1718 name="--color=always: ANSI codes in output"
1719 should_run "$name" && {
1720 log_test "$name"
1721 local out
1722 out=$("$FERP" --color=always "hello" "$FIXTURES/simple.txt")
1723 if echo "$out" | grep -q $'\033\['; then
1724 pass "$name"
1725 else
1726 fail "$name" "expected ANSI escape codes"
1727 fi
1728 }
1729
1730 name="--color=never: no ANSI codes"
1731 should_run "$name" && {
1732 log_test "$name"
1733 local out
1734 out=$("$FERP" --color=never "hello" "$FIXTURES/simple.txt")
1735 if ! echo "$out" | grep -q $'\033\['; then
1736 pass "$name"
1737 else
1738 fail "$name" "expected no ANSI codes"
1739 fi
1740 }
1741
1742 name="--color=auto: no ANSI when piped"
1743 should_run "$name" && {
1744 log_test "$name"
1745 local out
1746 out=$("$FERP" --color=auto "hello" "$FIXTURES/simple.txt" | cat)
1747 if ! echo "$out" | grep -q $'\033\['; then
1748 pass "$name"
1749 else
1750 fail "$name" "expected no ANSI codes when piped"
1751 fi
1752 }
1753
1754 # --line-buffered
1755 name="--line-buffered: accepted"
1756 should_run "$name" && {
1757 log_test "$name"
1758 if "$FERP" --line-buffered "hello" "$FIXTURES/simple.txt" >/dev/null 2>&1; then
1759 pass "$name"
1760 else
1761 fail "$name"
1762 fi
1763 }
1764 }
1765
1766 #------------------------------------------------------------------------------
1767 # EXIT CODE TESTS
1768 #------------------------------------------------------------------------------
1769
1770 test_exit_codes() {
1771 section "Exit Code Tests"
1772 local name
1773
1774 name="exit 0: match found"
1775 should_run "$name" && {
1776 log_test "$name"
1777 if run_ferp_expect 0 "hello" "$FIXTURES/simple.txt"; then
1778 pass "$name"
1779 else
1780 fail "$name"
1781 fi
1782 }
1783
1784 name="exit 1: no match found"
1785 should_run "$name" && {
1786 log_test "$name"
1787 if run_ferp_expect 1 "nonexistent_pattern_xyz" "$FIXTURES/simple.txt"; then
1788 pass "$name"
1789 else
1790 fail "$name"
1791 fi
1792 }
1793
1794 name="exit 2: invalid regex"
1795 should_run "$name" && {
1796 log_test "$name"
1797 if run_ferp_expect 2 "[invalid" "$FIXTURES/simple.txt"; then
1798 pass "$name"
1799 else
1800 fail "$name"
1801 fi
1802 }
1803
1804 name="exit 2: missing pattern"
1805 should_run "$name" && {
1806 log_test "$name"
1807 if run_ferp_expect 2; then
1808 pass "$name"
1809 else
1810 fail "$name"
1811 fi
1812 }
1813 }
1814
1815 #------------------------------------------------------------------------------
1816 # EDGE CASES AND REGRESSIONS
1817 #------------------------------------------------------------------------------
1818
1819 test_edge_cases() {
1820 section "Edge Cases and Regressions"
1821 local name
1822
1823 name="empty pattern matches all lines"
1824 should_run "$name" && {
1825 log_test "$name"
1826 local out
1827 out=$("$FERP" "" "$FIXTURES/numbers.txt" | wc -l)
1828 if [[ "$out" -eq 10 ]]; then
1829 pass "$name"
1830 else
1831 fail "$name" "expected 10 lines, got $out"
1832 fi
1833 }
1834
1835 name="empty file no crash"
1836 should_run "$name" && {
1837 log_test "$name"
1838 local tmpfile
1839 tmpfile=$(mktemp)
1840 if run_ferp_expect 1 "test" "$tmpfile"; then
1841 pass "$name"
1842 else
1843 fail "$name"
1844 fi
1845 rm -f "$tmpfile"
1846 }
1847
1848 name="very long line handling"
1849 should_run "$name" && {
1850 log_test "$name"
1851 local tmpfile long_line
1852 tmpfile=$(mktemp)
1853 long_line=$(printf 'a%.0s' {1..10000})
1854 echo "${long_line}MATCH${long_line}" > "$tmpfile"
1855 if "$FERP" "MATCH" "$tmpfile" | grep -q "MATCH"; then
1856 pass "$name"
1857 else
1858 fail "$name"
1859 fi
1860 rm -f "$tmpfile"
1861 }
1862
1863 name="special regex chars in pattern"
1864 should_run "$name" && {
1865 log_test "$name"
1866 if "$FERP" '\$' "$FIXTURES/special_chars.txt" | grep -q "dollar"; then
1867 pass "$name"
1868 else
1869 fail "$name"
1870 fi
1871 }
1872
1873 name="multiple files with mixed results"
1874 should_run "$name" && {
1875 log_test "$name"
1876 local out
1877 out=$("$FERP" "match" "$FIXTURES/multifile1.txt" "$FIXTURES/multifile3.txt" "$FIXTURES/multifile2.txt")
1878 # Should show results from files that have matches
1879 if echo "$out" | grep -q "multifile1" && echo "$out" | grep -q "multifile2"; then
1880 pass "$name"
1881 else
1882 fail "$name" "got: $out"
1883 fi
1884 }
1885
1886 name="stdin input"
1887 should_run "$name" && {
1888 log_test "$name"
1889 local out
1890 out=$(echo "test line" | "$FERP" "test")
1891 if [[ "$out" == "test line" ]]; then
1892 pass "$name"
1893 else
1894 fail "$name" "got: $out"
1895 fi
1896 }
1897
1898 name="-- ends option parsing"
1899 should_run "$name" && {
1900 log_test "$name"
1901 # Pattern starting with - should work after --
1902 local out
1903 out=$(echo "-test-" | "$FERP" -- "-test-")
1904 if [[ "$out" == "-test-" ]]; then
1905 pass "$name"
1906 else
1907 fail "$name" "got: $out"
1908 fi
1909 }
1910
1911 name="combined short options -inv"
1912 should_run "$name" && {
1913 log_test "$name"
1914 local out
1915 out=$("$FERP" -inv "HELLO" "$FIXTURES/simple.txt" | wc -l)
1916 # Case insensitive, inverted, with line numbers
1917 if [[ "$out" -eq 4 ]]; then
1918 pass "$name"
1919 else
1920 fail "$name" "expected 4 lines, got $out"
1921 fi
1922 }
1923 }
1924
1925 #------------------------------------------------------------------------------
1926 # Main
1927 #------------------------------------------------------------------------------
1928
1929 main() {
1930 echo ""
1931 echo -e "${BLUE}╔════════════════════════════════════════════════════════════════════════════╗${NC}"
1932 echo -e "${BLUE}║ FERP Integration Test Suite ║${NC}"
1933 echo -e "${BLUE}╚════════════════════════════════════════════════════════════════════════════╝${NC}"
1934 echo ""
1935 echo "ferp binary: $FERP"
1936 echo "Fixtures: $FIXTURES"
1937 if [[ "$COMPARE_GREP" == "true" ]]; then
1938 echo "Mode: Comparing against GNU grep"
1939 fi
1940 if [[ -n "$FILTER" ]]; then
1941 echo "Filter: $FILTER"
1942 fi
1943
1944 # Run all test categories
1945 test_pattern_types
1946 test_matching_control
1947 test_output_control
1948 test_line_prefix
1949 test_context_control
1950 test_file_selection
1951 test_multi_pattern
1952 test_binary_handling
1953 test_misc_flags
1954 test_exit_codes
1955 test_edge_cases
1956
1957 # Summary
1958 echo ""
1959 echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
1960 echo -e "${BLUE} Summary${NC}"
1961 echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
1962 echo ""
1963 echo -e " Tests run: ${TESTS_RUN}"
1964 echo -e " ${GREEN}Passed: ${TESTS_PASSED}${NC}"
1965 echo -e " ${RED}Failed: ${TESTS_FAILED}${NC}"
1966 echo -e " ${YELLOW}Skipped: ${TESTS_SKIPPED}${NC}"
1967 echo ""
1968
1969 if [[ "$TESTS_FAILED" -gt 0 ]]; then
1970 echo -e "${RED}Some tests failed!${NC}"
1971 exit 1
1972 else
1973 echo -e "${GREEN}All tests passed!${NC}"
1974 exit 0
1975 fi
1976 }
1977
1978 main "$@"