Repository navigation
Expand file tree
/
Copy pathsearch_test.go
More file actions
744 lines (625 loc) · 19 KB
/
Copy pathsearch_test.go
File metadata and controls
744 lines (625 loc) · 19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
package main
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
. "github.com/antonmedv/fx/internal/jsonx"
)
// doSearch is a test helper that performs a synchronous search using executeSearch.
func doSearch(m *model, s string) {
if s == "" {
return
}
result, err := executeSearch(m.top, nil, s, nil)
if err != nil {
m.search = newSearch()
m.search.err = err
return
}
m.search = result
m.selectSearchResult(0)
}
func TestBasicSearch(t *testing.T) {
jsonData := `{
"name": "John Doe",
"age": 30,
"email": "john@example.com",
"active": true,
"skills": ["JavaScript", "Go", "Python"]
}`
head, err := Parse([]byte(jsonData))
require.NoError(t, err)
m := &model{
viewState: viewState{
top: head,
head: head,
search: newSearch(),
},
}
testCases := []struct {
searchTerm string
expectedResults int
description string
}{
{"John", 1, "Simple string search"},
{"30", 1, "Number search"},
{"example.com", 1, "Domain search"},
{"JavaScript", 1, "Array element search"},
{"active", 1, "Boolean key search"},
{"nonexistent", 0, "No match search"},
{"", 0, "Empty search"},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
doSearch(m, tc.searchTerm)
if tc.expectedResults == 0 {
assert.Equal(t, 0, len(m.search.results), "Should find no results for: %s", tc.searchTerm)
} else {
assert.Greater(t, len(m.search.results), 0, "Should find results for: %s", tc.searchTerm)
}
assert.Nil(t, m.search.err, "Search should not error for: %s", tc.searchTerm)
})
}
}
func TestRegexSearch(t *testing.T) {
jsonData := `{
"users": [
{"id": "USER-001", "email": "alice@company.com", "score": 95.5},
{"id": "USER-002", "email": "bob@company.com", "score": 87.2},
{"id": "ADMIN-001", "email": "admin@company.com", "score": 100.0}
],
"metadata": {
"version": "v1.2.3",
"timestamp": "2024-01-15T10:30:00Z"
}
}`
head, err := Parse([]byte(jsonData))
require.NoError(t, err)
m := &model{
viewState: viewState{
top: head,
head: head,
search: newSearch(),
},
}
testCases := []struct {
pattern string
shouldMatch bool
description string
}{
// Pattern matching
{"USER-\\d+", true, "User ID pattern"},
{"\\d+\\.\\d+", true, "Decimal number pattern"},
{"[a-z]+@[a-z]+\\.com", true, "Email pattern"},
{"v\\d+\\.\\d+\\.\\d+", true, "Version pattern"},
{"\\d{4}-\\d{2}-\\d{2}", true, "Date pattern"},
// Anchored searches
{"^\"id\"", true, "Key at start of line"},
{"com\"$", true, "Value at end of line"},
// Character classes
{"[A-Z]{4,}", true, "Uppercase letters"},
{"[0-9]{3}", true, "Three digits"},
// Quantifiers
{"o{2}", true, "Double 'o'"},
{"a+", true, "One or more 'a'"},
{"z*", true, "Zero or more 'z'"},
// Invalid patterns should error
{"[", false, "Invalid bracket"},
{"(", false, "Unclosed parenthesis"},
{"*", false, "Invalid quantifier"},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
doSearch(m, tc.pattern)
if tc.shouldMatch {
assert.Nil(t, m.search.err, "Pattern should be valid: %s", tc.pattern)
} else {
assert.NotNil(t, m.search.err, "Pattern should be invalid: %s", tc.pattern)
}
})
}
}
func TestCaseInsensitiveSearch(t *testing.T) {
jsonData := `{
"Company": "ACME Corporation",
"employees": [
{"Name": "Alice Johnson", "Department": "Engineering"},
{"name": "bob smith", "department": "marketing"},
{"NAME": "CHARLIE BROWN", "DEPARTMENT": "SALES"}
]
}`
head, err := Parse([]byte(jsonData))
require.NoError(t, err)
m := &model{
viewState: viewState{
top: head,
head: head,
search: newSearch(),
},
}
testCases := []struct {
searchTerm string
description string
}{
{"alice", "Lowercase search for mixed case"},
{"ALICE", "Uppercase search for mixed case"},
{"Alice", "Proper case search"},
{"aLiCe", "Mixed case search"},
{"company", "Lowercase search for uppercase"},
{"ENGINEERING", "Uppercase search for proper case"},
{"marketing", "Exact case match"},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
doSearch(m, tc.searchTerm)
// Case insensitive search should find matches regardless of case
assert.Greater(t, len(m.search.results), 0, "Should find case-insensitive match for: %s", tc.searchTerm)
assert.Nil(t, m.search.err, "Search should not error")
})
}
}
func TestSearchInDifferentNodeTypes(t *testing.T) {
jsonData := `{
"string_field": "hello world",
"number_field": 42,
"boolean_field": true,
"null_field": null,
"array_field": [1, "two", 3.14, false],
"object_field": {
"nested_string": "nested value",
"nested_number": 99
}
}`
head, err := Parse([]byte(jsonData))
require.NoError(t, err)
m := &model{
viewState: viewState{
top: head,
head: head,
search: newSearch(),
},
}
testCases := []struct {
searchTerm string
nodeType string
description string
}{
{"hello", "string", "Search in string values"},
{"42", "number", "Search in number values"},
{"true", "boolean", "Search in boolean values"},
{"null", "null", "Search in null values"},
{"two", "array element", "Search in array elements"},
{"nested", "object property", "Search in nested objects"},
{"string_field", "key", "Search in JSON keys"},
{"3.14", "float", "Search in floating point numbers"},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
doSearch(m, tc.searchTerm)
assert.Greater(t, len(m.search.results), 0, "Should find %s in %s", tc.searchTerm, tc.nodeType)
assert.Nil(t, m.search.err, "Search should not error")
})
}
}
func TestSearchResultDetails(t *testing.T) {
jsonData := `{
"message": "The quick brown fox jumps over the lazy dog",
"words": ["fox", "dog", "fox"]
}`
head, err := Parse([]byte(jsonData))
require.NoError(t, err)
m := &model{
viewState: viewState{
top: head,
head: head,
search: newSearch(),
},
}
// Test multiple matches in same value
doSearch(m, "fox")
assert.Greater(t, len(m.search.results), 0, "Should find fox matches")
assert.Nil(t, m.search.err, "Search should not error")
// Verify that we have both key matches and value matches
hasKeyMatches := len(m.search.keys) > 0
hasValueMatches := len(m.search.values) > 0
assert.True(t, hasKeyMatches || hasValueMatches, "Should have either key or value matches")
// Test that search cursor is initialized
if len(m.search.results) > 0 {
assert.GreaterOrEqual(t, m.search.cursor, 0, "Search cursor should be valid")
assert.Less(t, m.search.cursor, len(m.search.results), "Search cursor should be within bounds")
}
}
func TestSearchNavigation(t *testing.T) {
jsonData := `{
"items": [
{"name": "apple", "color": "red"},
{"name": "banana", "color": "yellow"},
{"name": "apple", "color": "green"}
]
}`
head, err := Parse([]byte(jsonData))
require.NoError(t, err)
m := &model{
viewState: viewState{
top: head,
head: head,
search: newSearch(),
},
}
// Search for term with multiple matches
doSearch(m, "apple")
require.Greater(t, len(m.search.results), 1, "Should find multiple apple matches")
initialCursor := m.search.cursor
// Test forward navigation
m.selectSearchResult(m.search.cursor + 1)
assert.NotEqual(t, initialCursor, m.search.cursor, "Cursor should move forward")
assert.GreaterOrEqual(t, m.search.cursor, 0, "Cursor should be valid")
assert.Less(t, m.search.cursor, len(m.search.results), "Cursor should be within bounds")
// Test wrap-around (next from last should go to first)
lastIndex := len(m.search.results) - 1
m.selectSearchResult(lastIndex + 1)
assert.Equal(t, 0, m.search.cursor, "Should wrap to first result")
// Test backward wrap-around (previous from first should go to last)
m.selectSearchResult(-1)
assert.Equal(t, lastIndex, m.search.cursor, "Should wrap to last result")
}
func TestSpecialCharacterSearch(t *testing.T) {
jsonData := `{
"symbols": "!@#$%^&*()_+-={}[]|\\:;\"'<>?,./'",
"escaped": "Line 1\nLine 2\tTabbed\r\nWindows line",
"unicode": "café, naïve, résumé, 中文, 🚀",
"quotes": "He said \"Hello world!\"",
"backslash": "C:\\Users\\Name\\file.txt"
}`
head, err := Parse([]byte(jsonData))
require.NoError(t, err)
m := &model{
viewState: viewState{
top: head,
head: head,
search: newSearch(),
},
}
testCases := []struct {
searchTerm string
description string
}{
{"@", "At symbol"},
{"\\$", "Dollar sign (escaped)"},
{"\\*", "Asterisk (escaped)"},
{"\\[", "Square bracket (escaped)"},
{"\\\\n", "Newline escape sequence"},
{"\\\\t", "Tab escape sequence"},
{"café", "Unicode characters"},
{"中文", "Chinese characters"},
{"🚀", "Emoji"},
{"\\\"", "Escaped quotes"},
{"C:", "Drive letter"},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
doSearch(m, tc.searchTerm)
// Should either find matches or have valid regex (no panic)
assert.Nil(t, m.search.err, "Should handle special characters without error: %s", tc.searchTerm)
})
}
}
func TestEmptyAndEdgeCases(t *testing.T) {
testCases := []struct {
jsonData string
searchTerm string
description string
}{
{`{}`, "anything", "Empty object"},
{`[]`, "anything", "Empty array"},
{`""`, "empty", "Empty string value"},
{`{"": "empty key"}`, "", "Empty key search"},
{`{"key": ""}`, "key", "Search in empty value"},
{`null`, "null", "Null document"},
{`false`, "false", "Boolean document"},
{`0`, "0", "Zero value"},
{`{"very_long_key_name_that_exceeds_normal_length": "value"}`, "very_long", "Long key names"},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
head, err := Parse([]byte(tc.jsonData))
require.NoError(t, err)
m := &model{
viewState: viewState{
top: head,
head: head,
search: newSearch(),
},
}
doSearch(m, tc.searchTerm)
// Should not panic or error on edge cases
assert.Nil(t, m.search.err, "Should handle edge case without error")
assert.NotNil(t, m.search.results, "Results should not be nil")
})
}
}
func TestLargeJSONSearch(t *testing.T) {
// Build a larger JSON structure
jsonBuilder := `{"users": [`
for i := 0; i < 100; i++ {
if i > 0 {
jsonBuilder += ","
}
jsonBuilder += `{"id": ` + string(rune(i)) + `, "name": "User` + string(rune(i)) + `", "active": `
if i%2 == 0 {
jsonBuilder += "true"
} else {
jsonBuilder += "false"
}
jsonBuilder += `}`
}
jsonBuilder += `]}`
// Use simpler approach for test
jsonData := `{
"users": [
{"id": 1, "name": "User1", "active": true},
{"id": 2, "name": "User2", "active": false},
{"id": 3, "name": "User3", "active": true}
],
"repeated_data": ["test", "test", "test", "test", "test"]
}`
head, err := Parse([]byte(jsonData))
require.NoError(t, err)
m := &model{
viewState: viewState{
top: head,
head: head,
search: newSearch(),
},
}
// Test that search completes in reasonable time
doSearch(m, "User")
assert.Greater(t, len(m.search.results), 0, "Should find users in large JSON")
assert.Nil(t, m.search.err, "Should not error on large JSON")
// Test repeated terms
doSearch(m, "test")
assert.Greater(t, len(m.search.results), 3, "Should find multiple instances of repeated term")
}
func TestSearchInWrappedStrings(t *testing.T) {
// JSON with a long string that will be wrapped
jsonData := `{
"description": "This is a very long string that should be wrapped across multiple lines when the terminal width is narrow enough to trigger wrapping behavior"
}`
head, err := Parse([]byte(jsonData))
require.NoError(t, err)
// Apply wrapping with a narrow terminal width to force chunks
Wrap(head, 40)
m := &model{
viewState: viewState{
top: head,
head: head,
search: newSearch(),
},
wrap: true,
termWidth: 40,
}
testCases := []struct {
searchTerm string
expectMatch bool
description string
}{
{"very long string", true, "Multi-word search in wrapped text"},
{"wrapped across", true, "Search spanning potential chunk boundary"},
{"terminal width", true, "Search near end of wrapped text"},
{"This is a", true, "Search at beginning of wrapped text"},
{"behavior", true, "Search at very end of wrapped text"},
{"nonexistent phrase", false, "No match in wrapped text"},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
doSearch(m, tc.searchTerm)
if tc.expectMatch {
assert.Greater(t, len(m.search.results), 0, "Should find '%s' in wrapped text", tc.searchTerm)
} else {
assert.Equal(t, 0, len(m.search.results), "Should not find '%s' in wrapped text", tc.searchTerm)
}
assert.Nil(t, m.search.err, "Search should not error")
})
}
}
func TestSearchChunkBoundaryMatches(t *testing.T) {
// Create a string where we know approximately where chunk boundaries will be
// With termWidth=30 and some indentation, each chunk will be roughly 25-28 chars
jsonData := `{"text": "AAAAA BBBBB CCCCC DDDDD EEEEE FFFFF GGGGG HHHHH IIIII JJJJJ"}`
head, err := Parse([]byte(jsonData))
require.NoError(t, err)
// Apply wrapping with narrow width
Wrap(head, 30)
m := &model{
viewState: viewState{
top: head,
head: head,
search: newSearch(),
},
wrap: true,
termWidth: 30,
}
// Verify chunks were created
textNode := head.Next // Skip the opening brace to get to the "text" key node
require.NotNil(t, textNode, "Should have text node")
require.NotEmpty(t, textNode.Chunk, "Text node should have chunks (Chunk field set)")
require.NotNil(t, textNode.ChunkEnd, "Text node should have ChunkEnd set")
testCases := []struct {
pattern string
description string
}{
{"AAAAA", "Match in first chunk"},
{"JJJJJ", "Match in last chunk"},
{"[A-Z]{5}", "Regex matching all groups"},
{"BBBBB CCCCC", "Match potentially spanning chunks"},
{"FFFFF GGGGG HHHHH", "Match spanning multiple chunks"},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
doSearch(m, tc.pattern)
assert.Greater(t, len(m.search.results), 0, "Should find matches for pattern: %s", tc.pattern)
assert.Nil(t, m.search.err, "Search should not error")
// Verify matches are recorded in the values map
assert.Greater(t, len(m.search.values), 0, "Should have value matches recorded")
})
}
}
func TestSearchMultipleMatchesInChunks(t *testing.T) {
// String with repeated pattern that will span multiple chunks
jsonData := `{"data": "test123 test456 test789 test000 test111 test222 test333 test444"}`
head, err := Parse([]byte(jsonData))
require.NoError(t, err)
Wrap(head, 35)
m := &model{
viewState: viewState{
top: head,
head: head,
search: newSearch(),
},
wrap: true,
termWidth: 35,
}
// Search for pattern that should match multiple times
doSearch(m, "test\\d{3}")
assert.Greater(t, len(m.search.results), 0, "Should find test patterns")
assert.Nil(t, m.search.err, "Search should not error")
// Check that matches are distributed across chunk nodes
totalMatches := 0
for _, matches := range m.search.values {
totalMatches += len(matches)
}
assert.Greater(t, totalMatches, 0, "Should have matches across chunks")
}
func TestSearchWrappedVsUnwrapped(t *testing.T) {
jsonData := `{"message": "The quick brown fox jumps over the lazy dog multiple times today"}`
head, err := Parse([]byte(jsonData))
require.NoError(t, err)
// First test without wrapping
m := &model{
viewState: viewState{
top: head,
head: head,
search: newSearch(),
},
wrap: false,
termWidth: 80,
}
doSearch(m, "fox")
unwrappedResults := len(m.search.results)
assert.Greater(t, unwrappedResults, 0, "Should find 'fox' without wrapping")
// Now apply wrapping and search again
Wrap(head, 30)
m.wrap = true
m.termWidth = 30
doSearch(m, "fox")
wrappedResults := len(m.search.results)
assert.Equal(t, unwrappedResults, wrappedResults, "Should find same number of results with or without wrapping")
}
func TestSearchChunkIndexMapping(t *testing.T) {
// Test that search result indices are correctly mapped to chunks
jsonData := `{"value": "START middle portion of text END"}`
head, err := Parse([]byte(jsonData))
require.NoError(t, err)
// Use very narrow width to force multiple chunks
Wrap(head, 20)
m := &model{
viewState: viewState{
top: head,
head: head,
search: newSearch(),
},
wrap: true,
termWidth: 20,
}
// Search for terms at different positions
testCases := []struct {
term string
description string
}{
{"START", "Beginning of string"},
{"middle", "Middle of string"},
{"END", "End of string"},
{"portion", "Another middle term"},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
doSearch(m, tc.term)
assert.Greater(t, len(m.search.results), 0, "Should find '%s'", tc.term)
assert.Nil(t, m.search.err)
// Verify that matches are recorded in the values map
assert.Greater(t, len(m.search.values), 0, "Should have value matches recorded for '%s'", tc.term)
})
}
}
func TestSearchEmptyAndShortStringsWithWrap(t *testing.T) {
testCases := []struct {
jsonData string
searchTerm string
expectMatch bool
description string
}{
{`{"a": ""}`, "anything", false, "Empty string value"},
{`{"a": "x"}`, "x", true, "Single char string"},
{`{"a": "ab"}`, "ab", true, "Two char string"},
{`{"a": "short"}`, "short", true, "Short string (no wrap needed)"},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
head, err := Parse([]byte(tc.jsonData))
require.NoError(t, err)
// Apply wrapping even for short strings
Wrap(head, 30)
m := &model{
viewState: viewState{
top: head,
head: head,
search: newSearch(),
},
wrap: true,
termWidth: 30,
}
doSearch(m, tc.searchTerm)
if tc.expectMatch {
assert.Greater(t, len(m.search.results), 0, "Should find '%s'", tc.searchTerm)
} else {
assert.Equal(t, 0, len(m.search.results), "Should not find '%s'", tc.searchTerm)
}
assert.Nil(t, m.search.err)
})
}
}
func TestSearchRegexAcrossChunks(t *testing.T) {
// Test regex patterns that might match across chunk boundaries
jsonData := `{"content": "email: user@example.com phone: 123-456-7890 date: 2024-01-15"}`
head, err := Parse([]byte(jsonData))
require.NoError(t, err)
Wrap(head, 25)
m := &model{
viewState: viewState{
top: head,
head: head,
search: newSearch(),
},
wrap: true,
termWidth: 25,
}
testCases := []struct {
pattern string
expectMinNum int
description string
}{
{`\w+@\w+\.\w+`, 1, "Email pattern"},
{`\d{3}-\d{3}-\d{4}`, 1, "Phone pattern"},
{`\d{4}-\d{2}-\d{2}`, 1, "Date pattern"},
{`\d+`, 6, "All number sequences (123,456,7890,2024,01,15)"},
{`[a-z]+:`, 3, "Labels (email:, phone:, date:)"},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
doSearch(m, tc.pattern)
assert.GreaterOrEqual(t, len(m.search.results), tc.expectMinNum,
"Pattern '%s' should find at least %d matches", tc.pattern, tc.expectMinNum)
assert.Nil(t, m.search.err)
})
}
}