fortrangoingonforty/ferp / 429310c

Browse files

update fixtures, make tests more robust

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
429310c43b61e4a4e1abc6f768243b332c9c0766
Parents
7614d9b
Tree
6166a53

18 changed files

StatusFile+-
A tests/generated_fixtures/basic.txt 8 0
A tests/generated_fixtures/binary.bin bin
A tests/generated_fixtures/cases.txt 8 0
A tests/generated_fixtures/count.txt 6 0
A tests/generated_fixtures/empty.txt 0 0
A tests/generated_fixtures/empty_lines.txt 5 0
A tests/generated_fixtures/lines.txt 5 0
A tests/generated_fixtures/longline.txt 2 0
A tests/generated_fixtures/multi1.txt 2 0
A tests/generated_fixtures/multi2.txt 2 0
A tests/generated_fixtures/multi3.txt 2 0
A tests/generated_fixtures/no_newline.txt 1 0
A tests/generated_fixtures/numbered.txt 10 0
A tests/generated_fixtures/special.txt 14 0
A tests/generated_fixtures/unicode.txt 2 0
A tests/generated_fixtures/whitespace.txt 3 0
A tests/generated_fixtures/words.txt 10 0
A tests/grep_comparison_test.sh 836 0
tests/generated_fixtures/basic.txtadded
@@ -0,0 +1,8 @@
1
+hello world
2
+Hello World
3
+HELLO WORLD
4
+goodbye world
5
+foo bar baz
6
+The quick brown fox
7
+testing 123 testing
8
+line with hello in the middle
tests/generated_fixtures/binary.binadded
Binary file changed.
tests/generated_fixtures/cases.txtadded
@@ -0,0 +1,8 @@
1
+apple
2
+Apple
3
+APPLE
4
+aPpLe
5
+apples
6
+Apples
7
+APPLES
8
+pineapple
tests/generated_fixtures/count.txtadded
@@ -0,0 +1,6 @@
1
+match one
2
+no hit
3
+match two
4
+match three
5
+no hit
6
+match four
tests/generated_fixtures/empty.txtadded
tests/generated_fixtures/empty_lines.txtadded
@@ -0,0 +1,5 @@
1
+first line
2
+
3
+third line
4
+
5
+fifth line
tests/generated_fixtures/lines.txtadded
@@ -0,0 +1,5 @@
1
+exact
2
+exact match
3
+not exact
4
+exactly
5
+EXACT
tests/generated_fixtures/longline.txtadded
@@ -0,0 +1,2 @@
1
+start xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx middle yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy end
2
+short line
tests/generated_fixtures/multi1.txtadded
@@ -0,0 +1,2 @@
1
+has pattern
2
+another line
tests/generated_fixtures/multi2.txtadded
@@ -0,0 +1,2 @@
1
+no hits here
2
+nothing
tests/generated_fixtures/multi3.txtadded
@@ -0,0 +1,2 @@
1
+also has pattern
2
+more
tests/generated_fixtures/no_newline.txtadded
@@ -0,0 +1,1 @@
1
+no trailing newline
tests/generated_fixtures/numbered.txtadded
@@ -0,0 +1,10 @@
1
+line 1 - before
2
+line 2 - before
3
+line 3 - before
4
+line 4 - MATCH
5
+line 5 - after
6
+line 6 - after
7
+line 7 - after
8
+line 8 - before
9
+line 9 - MATCH
10
+line 10 - after
tests/generated_fixtures/special.txtadded
@@ -0,0 +1,14 @@
1
+price is $100
2
+rate is 50%
3
+path/to/file
4
+array[0]
5
+func()
6
+a+b=c
7
+a*b
8
+a.b
9
+start^
10
+end$
11
+back\slash
12
+pipe|char
13
+question?
14
+curly{brace}
tests/generated_fixtures/unicode.txtadded
@@ -0,0 +1,2 @@
1
+café résumé naïve
2
+日本語テスト
tests/generated_fixtures/whitespace.txtadded
@@ -0,0 +1,3 @@
1
+tab	here
2
+  spaces  
3
+mixed	  	tabs
tests/generated_fixtures/words.txtadded
@@ -0,0 +1,10 @@
1
+test
2
+testing
3
+tested
4
+tester
5
+a test here
6
+test-case
7
+pre-test
8
+pretest
9
+TEST
10
+Test
tests/grep_comparison_test.shadded
@@ -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 "$@"